diff --git a/.gitea/workflows/apk.yml b/.gitea/workflows/apk.yml new file mode 100644 index 0000000..bfe5053 --- /dev/null +++ b/.gitea/workflows/apk.yml @@ -0,0 +1,68 @@ +# Construye la APK y la publica en las releases. +# +# Con un tag v* se publica directamente: crear el tag ya es la decision de sacar +# version, y es la que enlaza el README (releases/latest). Lanzada a mano desde +# la interfaz se queda en borrador, para revisarla antes. +# +# NO se dispara sola con cada commit: la APK son mas de 100 MB y el backend tarda +# en empaquetarse. Se lanza a mano desde la interfaz de gitea, o al crear un tag. +# +# El runner necesita: JDK 17 con compilador, el SDK de Android (plataforma 35 y +# build-tools), node, zip y unzip. Un runner pelado NO vale. +# +# Secretos que hay que dar de alta en el repositorio: +# OASIS_KEYSTORE_B64 el almacen de claves en base64 +# OASIS_KEYSTORE_PASS su contraseña +# OASIS_KEY_ALIAS el alias (por defecto "oasis") +# +# El token para publicar lo pone gitea solo en GITEA_TOKEN. + +name: APK + +on: + workflow_dispatch: + push: + tags: + - 'v*' + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # release.sh cuenta commits para el versionCode + + - name: Traer el APK oficial de la misma version (node_modules y libnode.so) + run: | + v=$(node -p "require('./src/server/package.json').version") + curl -sfL -o /tmp/oasis-base.apk \ + "https://github.com/epsylon/oasis/releases/download/oasis-android-v$v/oasis-v$v.apk" + echo "OASIS_APK_BASE=/tmp/oasis-base.apk" >> "$GITHUB_ENV" + + - name: Restaurar el almacen de claves + run: | + echo "${{ secrets.OASIS_KEYSTORE_B64 }}" | base64 -d > /tmp/oasis.jks + chmod 600 /tmp/oasis.jks + + - name: Construir, verificar y publicar + env: + JAVA_HOME: /usr/lib/jvm/temurin-17-jdk-amd64 + ANDROID_HOME: /opt/android-sdk + OASIS_KEYSTORE: /tmp/oasis.jks + OASIS_KEYSTORE_PASS: ${{ secrets.OASIS_KEYSTORE_PASS }} + OASIS_KEY_ALIAS: ${{ secrets.OASIS_KEY_ALIAS }} + OASIS_GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} + OASIS_GITEA_URL: ${{ github.server_url }} + OASIS_GITEA_REPO: ${{ github.repository }} + run: | + cd android + if [ "${{ github.ref_type }}" = "tag" ]; then + ./scripts/release.sh --release --publish --public + else + ./scripts/release.sh --release --publish + fi + + - name: Borrar el almacen de claves + if: always() + run: rm -f /tmp/oasis.jks /tmp/oasis-base.apk diff --git a/.gitignore b/.gitignore index c2658d7..1fafaa8 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,6 @@ node_modules/ + +# pruebas en local: el node_modules suele ser un enlace y el estado de la llamada lo escribe el movil +src/server/node_modules +.phone_state.json +android/.tools/ diff --git a/GUIA_DE_DESARROLLO.md b/GUIA_DE_DESARROLLO.md new file mode 100644 index 0000000..f9bb560 --- /dev/null +++ b/GUIA_DE_DESARROLLO.md @@ -0,0 +1,228 @@ +# Guía de desarrollo + +Notas para quien vaya a meter mano en la rama del móvil. Leedlas antes de empezar, +que nos ahorramos disgustos. + +Esta rama va encima de Oasis, que es de epsylon. Él saca versión casi cada día +(39 entre la 1.0.1 y la 1.3.3, en siete semanas) y cada versión llega en un solo +commit. Si metemos cosas en sus ficheros, en cada integración hay que volver a +pelearse con ellas. Por eso la idea es sencilla: lo de Oasis lo cambia él; nosotros +cambiamos cómo se ve y se usa en el móvil, y lo hacemos en ficheros nuestros. + +Los números y de dónde salen están en `docs/FRONTERA_UPSTREAM.md`. + + +## Lo que no se toca + +- `src/backend/backend.js`. Cambia en 34 de cada 39 versiones. Ni rutas nuevas, ni + middleware, ni envolver funciones suyas como `checkMod`. +- La cadena de middleware del final de `backend.js`. Crece en cada versión. +- Los modelos de `src/models/`, salvo el nuestro (`fork_workflows.js`). Ahí se decide qué se publica en la red y cómo se cuenta. + Si lo cambiamos, nuestro móvil cuenta distinto que el escritorio de al lado. Un + ejemplo real: quien crea una votación no puede votar en ella. No nos gusta, pero + es una regla del modelo, así que se explica en pantalla y punto. +- `src/server/` (arranque de SSB, `ssb_config.js`, `package.json`). +- `src/backend/request_guards.js`, que lleva la CSP. Nada de `style=""` ni de + ` + + diff --git a/src/games/minesweeper/thumbnail.svg b/src/games/minesweeper/thumbnail.svg new file mode 100644 index 0000000..97f31cd --- /dev/null +++ b/src/games/minesweeper/thumbnail.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + 2 + 1 + 2 + 3 + 2 + 1 + 1 + 1 + 1 + + + + + + + + + + + + + MINESWEEPER + diff --git a/src/games/visualdj/index.html b/src/games/visualdj/index.html new file mode 100644 index 0000000..f2a2664 --- /dev/null +++ b/src/games/visualdj/index.html @@ -0,0 +1,945 @@ + + + + + + Visual DJ + + + +
+ ← Back to Games + VISUAL DJ +
+
+
+
Status: Paused | FPS: -- | BPM: 120 | Time: 0.0 s
+
Deck A: Plasma | Deck B: Tunnel | Mix: 50% A/B | Blend: Screen | Mic: Off
+
+ + + + + + +
+
+ +
+
+ Live controls:
+ Drag on the stage to move the crossfader, click to flash.
+ 1-8 pattern on deck A · Q-I pattern on deck B · Z-/ effects · ← → crossfade · Space flash · T tap tempo · P play/pause · F fullscreen · Shift+1-4 scenes +
+
+ +
+
+
+

Deck A

+
+
+ + + + + + + + + + +
+
+ +
+

Deck B

+
+
+ + + + + + + + + + +
+
+
+ +
+

Mixer

+
+ + + + + + + + + + + + + + + + +
+
+ +
+

Effects

+
+
+ + + + +
+
+ +
+ Scenes + +
+
+
+
+
+ + + + diff --git a/src/games/visualdj/thumbnail.svg b/src/games/visualdj/thumbnail.svg new file mode 100644 index 0000000..677c916 --- /dev/null +++ b/src/games/visualdj/thumbnail.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + VISUALDJ + diff --git a/src/maps/data/coast_50m.json b/src/maps/data/coast_50m.json new file mode 100644 index 0000000..25d07e1 --- /dev/null +++ b/src/maps/data/coast_50m.json @@ -0,0 +1 @@ +{"f":[{"b":[178.497,-16.976,180,-16.153],"l":[180,-16.153,179.848,-16.214,179.789,-16.221,179.715,-16.208,179.635,-16.223,179.552,-16.25,179.475,-16.294,179.359,-16.38,179.294,-16.399,179.225,-16.405,179.091,-16.437,178.961,-16.483,178.866,-16.54,178.805,-16.631,178.745,-16.634,178.686,-16.666,178.634,-16.649,178.584,-16.622,178.568,-16.664,178.542,-16.7,178.514,-16.726,178.497,-16.788,178.604,-16.801,178.638,-16.851,178.665,-16.92,178.707,-16.976,178.803,-16.952,178.884,-16.886,178.95,-16.904,179.007,-16.9,179.055,-16.814,179.202,-16.713,179.3,-16.71,179.323,-16.718,179.346,-16.77,179.375,-16.792,179.419,-16.807,179.465,-16.806,179.589,-16.787,179.715,-16.744,179.821,-16.737,179.928,-16.744,179.89,-16.667,179.906,-16.584,179.927,-16.552,179.93,-16.519,179.885,-16.518,179.841,-16.537,179.697,-16.632,179.568,-16.747,179.564,-16.637,179.619,-16.528,179.748,-16.446,179.794,-16.37,179.848,-16.302,179.999,-16.169]},{"b":[177.183,-17.164,177.287,-17.049],"l":[177.258,-17.054,177.287,-17.049,177.276,-17.105,177.234,-17.147,177.183,-17.164,177.21,-17.084,177.239,-17.059,177.258,-17.054]},{"b":[127.286,0.758,127.373,0.862],"l":[127.373,0.791,127.354,0.847,127.32,0.862,127.293,0.842,127.286,0.812,127.306,0.769,127.338,0.758,127.373,0.791]},{"b":[-81.422,24.63,-81.32,24.75],"l":[-81.322,24.685,-81.42,24.75,-81.422,24.733,-81.379,24.666,-81.379,24.636,-81.365,24.63,-81.335,24.65,-81.32,24.668,-81.322,24.685]},{"b":[-80.848,24.804,-80.785,24.846],"l":[-80.799,24.846,-80.839,24.818,-80.848,24.804,-80.829,24.804,-80.787,24.821,-80.785,24.835,-80.799,24.846]},{"b":[-80.665,24.898,-80.615,24.941],"l":[-80.626,24.941,-80.665,24.898,-80.638,24.903,-80.615,24.938,-80.626,24.941]},{"b":[113.185,-39.146,153.617,-10.707],"l":[129.569,-31.627,129.188,-31.66,128.946,-31.703,128.546,-31.888,128.068,-32.067,127.678,-32.151,127.32,-32.264,127.084,-32.297,126.779,-32.311,126.137,-32.257,125.917,-32.297,125.567,-32.506,125.464,-32.557,125.267,-32.614,124.759,-32.883,124.525,-32.94,124.373,-32.958,124.244,-33.015,124.126,-33.129,123.967,-33.446,123.868,-33.596,123.65,-33.836,123.507,-33.916,123.365,-33.905,123.208,-33.988,123.068,-33.901,122.956,-33.884,122.778,-33.891,122.151,-33.992,122.061,-33.874,121.946,-33.857,121.73,-33.862,121.405,-33.827,120.815,-33.871,120.531,-33.92,120.418,-33.963,120.209,-33.935,119.854,-33.975,119.729,-34.042,119.635,-34.101,119.451,-34.368,119.248,-34.456,119.081,-34.459,118.895,-34.48,118.52,-34.737,118.136,-34.987,118.006,-35.013,117.863,-35.055,117.675,-35.075,117.582,-35.098,117.144,-35.034,116.865,-35.027,116.517,-34.988,116.217,-34.866,115.987,-34.795,115.726,-34.526,115.565,-34.426,115.278,-34.304,115.195,-34.308,115.128,-34.342,115.009,-34.256,115.006,-34.145,114.973,-34.051,114.976,-33.804,114.994,-33.515,115.099,-33.58,115.182,-33.643,115.359,-33.64,115.515,-33.531,115.604,-33.372,115.683,-33.193,115.671,-33.002,115.619,-32.667,115.654,-32.597,115.708,-32.568,115.725,-32.401,115.738,-31.888,115.698,-31.695,115.455,-31.303,115.294,-30.962,115.177,-30.808,115.078,-30.56,114.995,-30.216,114.969,-30.042,114.942,-29.722,114.971,-29.54,114.959,-29.434,114.857,-29.143,114.628,-28.872,114.591,-28.772,114.592,-28.666,114.537,-28.543,114.354,-28.295,114.165,-28.081,114.133,-27.976,114.098,-27.544,114.028,-27.347,113.709,-26.848,113.333,-26.417,113.231,-26.241,113.185,-26.182,113.211,-26.174,113.253,-26.197,113.3,-26.24,113.323,-26.244,113.345,-26.208,113.343,-26.126,113.356,-26.08,113.389,-26.106,113.427,-26.198,113.547,-26.437,113.582,-26.558,113.734,-26.595,113.78,-26.563,113.836,-26.501,113.853,-26.332,113.776,-26.256,113.706,-26.224,113.589,-26.099,113.513,-25.898,113.395,-25.713,113.397,-25.647,113.451,-25.599,113.539,-25.625,113.621,-25.732,113.713,-25.831,113.698,-26.004,113.684,-26.052,113.692,-26.092,113.724,-26.13,113.766,-26.16,113.812,-26.116,113.854,-26.014,113.88,-26.028,113.942,-26.259,113.992,-26.321,114.09,-26.394,114.176,-26.337,114.216,-26.289,114.203,-26.126,114.229,-25.969,114.214,-25.852,113.993,-25.545,113.792,-25.166,113.671,-24.977,113.569,-24.693,113.504,-24.595,113.418,-24.436,113.413,-24.254,113.421,-24.132,113.49,-23.87,113.553,-23.733,113.757,-23.418,113.767,-23.283,113.765,-23.18,113.795,-23.024,113.795,-22.915,113.768,-22.813,113.683,-22.638,113.795,-22.332,113.958,-21.939,114.023,-21.881,114.124,-21.829,114.143,-21.91,114.093,-22.181,114.164,-22.323,114.142,-22.483,114.205,-22.456,114.304,-22.425,114.378,-22.342,114.417,-22.261,114.603,-21.942,114.709,-21.823,114.859,-21.736,115.162,-21.631,115.456,-21.492,115.596,-21.358,115.771,-21.242,115.894,-21.117,116.011,-21.03,116.606,-20.713,116.707,-20.654,116.836,-20.647,116.995,-20.658,117.139,-20.641,117.293,-20.713,117.406,-20.721,117.684,-20.643,117.832,-20.573,118.087,-20.419,118.199,-20.375,118.458,-20.327,118.751,-20.262,119.104,-19.995,119.359,-20.012,119.586,-20.038,119.768,-19.958,120.196,-19.909,120.434,-19.842,120.878,-19.665,120.998,-19.604,121.18,-19.478,121.338,-19.32,121.494,-19.106,121.589,-18.915,121.631,-18.817,121.722,-18.66,121.785,-18.536,121.834,-18.477,122.006,-18.394,122.262,-18.159,122.345,-18.112,122.361,-18.037,122.306,-17.995,122.237,-17.969,122.191,-17.72,122.147,-17.549,122.143,-17.428,122.16,-17.314,122.261,-17.136,122.333,-17.059,122.432,-16.97,122.523,-16.943,122.598,-16.865,122.72,-16.788,122.772,-16.71,122.848,-16.552,122.917,-16.433,122.971,-16.437,123.074,-16.715,123.142,-16.863,123.266,-17.037,123.383,-17.293,123.479,-17.41,123.525,-17.486,123.563,-17.521,123.571,-17.472,123.562,-17.415,123.608,-17.22,123.586,-17.083,123.594,-17.03,123.618,-17.008,123.664,-17.023,123.754,-17.1,123.799,-17.127,123.831,-17.121,123.829,-16.997,123.874,-16.919,123.856,-16.865,123.778,-16.868,123.745,-16.801,123.68,-16.724,123.607,-16.668,123.518,-16.541,123.49,-16.491,123.525,-16.468,123.581,-16.471,123.626,-16.416,123.646,-16.343,123.607,-16.224,123.647,-16.18,123.729,-16.192,123.859,-16.382,123.915,-16.364,123.961,-16.287,124.044,-16.265,124.13,-16.279,124.186,-16.334,124.3,-16.388,124.453,-16.382,124.53,-16.395,124.692,-16.386,124.772,-16.403,124.757,-16.373,124.669,-16.339,124.57,-16.332,124.454,-16.335,124.405,-16.299,124.388,-16.203,124.416,-16.133,124.435,-16.104,124.51,-16.116,124.577,-16.114,124.585,-16.02,124.609,-15.937,124.649,-15.87,124.648,-15.805,124.607,-15.823,124.504,-15.972,124.455,-15.851,124.382,-15.758,124.397,-15.626,124.44,-15.494,124.506,-15.475,124.562,-15.496,124.644,-15.419,124.691,-15.36,124.68,-15.311,124.693,-15.274,124.75,-15.285,124.972,-15.404,125.016,-15.467,125.063,-15.442,125.078,-15.375,125.073,-15.307,125.024,-15.317,124.909,-15.31,124.883,-15.272,124.893,-15.241,124.839,-15.161,124.914,-15.11,124.979,-15.107,125.023,-15.072,125.024,-15.024,125.038,-15.004,125.073,-15.032,125.189,-15.045,125.302,-15.107,125.356,-15.12,125.376,-15.087,125.384,-15.016,125.243,-14.945,125.239,-14.875,125.18,-14.794,125.179,-14.715,125.267,-14.648,125.285,-14.584,125.335,-14.558,125.436,-14.557,125.504,-14.502,125.58,-14.483,125.598,-14.362,125.597,-14.278,125.628,-14.257,125.705,-14.291,125.681,-14.388,125.681,-14.48,125.662,-14.529,125.691,-14.525,125.708,-14.505,125.738,-14.444,125.82,-14.469,125.84,-14.534,125.85,-14.597,125.891,-14.618,125.946,-14.52,126.021,-14.495,126.017,-14.371,126.045,-14.283,126.054,-14.217,126.101,-14.184,126.111,-14.114,126.073,-14.066,126.054,-13.977,126.119,-13.958,126.184,-14.002,126.228,-14.113,126.258,-14.164,126.299,-14.136,126.323,-14.062,126.403,-14.019,126.482,-14.079,126.57,-14.161,126.679,-14.089,126.781,-13.955,126.764,-13.873,126.776,-13.788,126.903,-13.744,127.006,-13.777,127.099,-13.867,127.293,-13.935,127.458,-14.031,127.531,-14.095,127.673,-14.195,127.763,-14.299,127.888,-14.485,128.18,-14.712,128.199,-14.752,128.16,-14.827,128.124,-14.924,128.08,-15.088,128.069,-15.329,128.112,-15.312,128.155,-15.226,128.202,-15.243,128.255,-15.299,128.259,-15.246,128.227,-15.214,128.173,-15.102,128.175,-15.043,128.218,-14.996,128.285,-14.939,128.358,-14.902,128.403,-14.869,128.41,-14.829,128.477,-14.788,128.576,-14.775,128.636,-14.781,129.058,-14.884,129.165,-14.988,129.175,-15.115,129.216,-15.16,129.238,-15.08,129.234,-14.906,129.268,-14.871,129.381,-14.898,129.459,-14.933,129.567,-15.047,129.588,-15.103,129.635,-15.14,129.65,-15.087,129.628,-15.012,129.613,-14.926,129.637,-14.851,129.763,-14.845,129.849,-14.829,129.808,-14.8,129.754,-14.79,129.663,-14.721,129.605,-14.647,129.699,-14.575,129.698,-14.557,129.608,-14.56,129.484,-14.49,129.379,-14.392,129.459,-14.213,129.62,-14.038,129.71,-13.98,129.718,-13.921,129.762,-13.812,129.789,-13.72,129.797,-13.648,129.839,-13.573,129.938,-13.502,130.073,-13.476,130.136,-13.448,130.199,-13.383,130.26,-13.302,130.135,-13.146,130.145,-13.059,130.168,-12.957,130.318,-12.883,130.4,-12.688,130.454,-12.659,130.572,-12.664,130.617,-12.647,130.61,-12.491,130.623,-12.431,130.672,-12.407,130.736,-12.428,130.777,-12.495,130.867,-12.558,130.898,-12.524,130.883,-12.455,130.874,-12.367,130.957,-12.348,131.023,-12.343,131.03,-12.271,131.02,-12.214,131.046,-12.19,131.22,-12.178,131.265,-12.119,131.292,-12.068,131.314,-12.096,131.342,-12.21,131.438,-12.277,131.726,-12.278,131.888,-12.232,131.957,-12.259,132.064,-12.281,132.182,-12.227,132.253,-12.186,132.372,-12.239,132.411,-12.295,132.442,-12.176,132.511,-12.135,132.584,-12.11,132.676,-12.13,132.713,-12.123,132.63,-12.035,132.635,-11.955,132.63,-11.836,132.645,-11.727,132.674,-11.649,132.475,-11.492,132.278,-11.468,132.134,-11.501,132.073,-11.475,131.945,-11.349,131.822,-11.302,131.812,-11.271,131.962,-11.181,132.019,-11.196,132.106,-11.281,132.155,-11.311,132.198,-11.305,132.225,-11.239,132.263,-11.204,132.334,-11.224,132.557,-11.367,132.683,-11.506,132.747,-11.469,132.857,-11.391,132.961,-11.407,133.025,-11.453,133.114,-11.622,133.185,-11.706,133.356,-11.728,133.443,-11.76,133.533,-11.816,133.654,-11.811,133.904,-11.832,134.139,-11.94,134.237,-12.008,134.351,-12.026,134.417,-12.053,134.538,-12.061,134.73,-11.984,134.816,-12.055,134.855,-12.103,135.03,-12.194,135.218,-12.222,135.352,-12.129,135.549,-12.061,135.686,-11.956,135.788,-11.907,135.885,-11.822,135.922,-11.826,135.844,-11.905,135.834,-11.951,135.896,-11.97,135.889,-11.993,135.804,-12.055,135.703,-12.152,135.704,-12.21,135.744,-12.242,135.791,-12.228,135.857,-12.179,135.938,-12.152,136.008,-12.191,136.031,-12.331,136.082,-12.422,136.193,-12.435,136.261,-12.434,136.329,-12.306,136.292,-12.196,136.25,-12.173,136.27,-12.132,136.443,-11.951,136.54,-11.958,136.61,-12.134,136.719,-12.226,136.836,-12.219,136.897,-12.244,136.947,-12.35,136.537,-12.784,136.518,-12.833,136.573,-12.912,136.594,-13.004,136.461,-13.225,136.412,-13.236,136.365,-13.176,136.294,-13.138,136.232,-13.165,136.166,-13.181,135.927,-13.304,135.929,-13.622,135.99,-13.81,135.954,-13.935,135.883,-14.153,135.806,-14.234,135.745,-14.287,135.539,-14.585,135.473,-14.657,135.405,-14.758,135.428,-14.856,135.453,-14.923,135.531,-15,135.833,-15.16,135.97,-15.27,136.205,-15.403,136.259,-15.495,136.291,-15.57,136.462,-15.655,136.584,-15.707,136.619,-15.693,136.644,-15.676,136.675,-15.675,136.705,-15.685,136.7,-15.752,136.687,-15.788,136.698,-15.835,136.785,-15.894,136.923,-15.892,137.002,-15.878,137.09,-15.941,137.169,-15.982,137.299,-16.066,137.526,-16.167,137.704,-16.233,137.913,-16.477,138.072,-16.617,138.245,-16.718,138.506,-16.79,138.626,-16.778,138.82,-16.861,139.01,-16.899,139.11,-17.014,139.145,-17.101,139.154,-17.168,139.248,-17.329,139.441,-17.381,139.69,-17.541,139.895,-17.611,139.946,-17.654,140.036,-17.703,140.21,-17.704,140.511,-17.625,140.648,-17.544,140.83,-17.414,140.916,-17.193,140.966,-17.015,141.219,-16.646,141.291,-16.463,141.356,-16.221,141.412,-16.07,141.393,-15.905,141.452,-15.605,141.581,-15.195,141.625,-15.057,141.604,-14.853,141.523,-14.47,141.559,-14.338,141.594,-14.153,141.535,-14.019,141.481,-13.927,141.473,-13.798,141.534,-13.554,141.589,-13.425,141.645,-13.259,141.614,-12.943,141.735,-12.833,141.782,-12.779,141.876,-12.778,141.92,-12.803,141.93,-12.74,141.893,-12.681,141.878,-12.613,141.852,-12.579,141.795,-12.567,141.747,-12.529,141.678,-12.491,141.689,-12.351,141.806,-12.08,141.871,-11.976,141.913,-12.019,141.961,-12.054,141.968,-11.976,141.952,-11.896,142.041,-11.632,142.139,-11.273,142.168,-10.947,142.326,-10.884,142.407,-10.802,142.456,-10.707,142.545,-10.707,142.605,-10.748,142.565,-10.819,142.553,-10.874,142.723,-11.01,142.78,-11.115,142.803,-11.214,142.837,-11.307,142.853,-11.432,142.851,-11.632,142.873,-11.821,142.934,-11.881,142.988,-11.919,143.066,-11.924,143.179,-11.954,143.153,-12.076,143.105,-12.17,143.099,-12.226,143.11,-12.304,143.191,-12.361,143.254,-12.398,143.29,-12.499,143.402,-12.64,143.398,-12.736,143.458,-12.856,143.512,-13.095,143.529,-13.304,143.587,-13.444,143.548,-13.741,143.589,-13.863,143.643,-13.964,143.707,-14.165,143.756,-14.349,143.822,-14.401,143.962,-14.463,144.106,-14.395,144.21,-14.302,144.322,-14.279,144.473,-14.232,144.586,-14.355,144.648,-14.492,144.916,-14.674,145.064,-14.791,145.18,-14.857,145.288,-14.943,145.277,-15.029,145.252,-15.097,145.276,-15.204,145.293,-15.327,145.272,-15.477,145.35,-15.702,145.375,-15.881,145.458,-16.056,145.452,-16.237,145.436,-16.305,145.426,-16.406,145.49,-16.532,145.55,-16.625,145.638,-16.726,145.755,-16.879,145.838,-16.91,145.912,-16.912,145.902,-17.07,146.05,-17.381,146.126,-17.635,146.074,-17.977,146.023,-18.176,146.032,-18.273,146.223,-18.51,146.333,-18.554,146.312,-18.667,146.297,-18.841,146.383,-18.977,146.481,-19.079,146.587,-19.139,146.692,-19.187,146.829,-19.236,147.003,-19.256,147.093,-19.333,147.139,-19.393,147.278,-19.414,147.342,-19.403,147.419,-19.378,147.471,-19.419,147.51,-19.474,147.586,-19.623,147.742,-19.77,147.853,-19.795,147.916,-19.869,148.004,-19.89,148.081,-19.899,148.19,-19.956,148.367,-20.087,148.527,-20.109,148.6,-20.145,148.759,-20.29,148.821,-20.366,148.885,-20.481,148.805,-20.492,148.73,-20.468,148.684,-20.58,148.789,-20.736,148.912,-20.845,149.061,-20.961,149.205,-21.125,149.241,-21.25,149.28,-21.3,149.329,-21.476,149.454,-21.579,149.46,-21.765,149.524,-22.024,149.596,-22.258,149.645,-22.328,149.704,-22.441,149.772,-22.426,149.822,-22.39,149.92,-22.501,149.974,-22.551,150.006,-22.522,149.942,-22.308,149.981,-22.184,150.021,-22.168,150.076,-22.164,150.143,-22.265,150.235,-22.373,150.405,-22.469,150.541,-22.559,150.58,-22.556,150.564,-22.486,150.569,-22.384,150.623,-22.367,150.672,-22.418,150.764,-22.576,150.783,-22.903,150.783,-23.177,150.843,-23.458,150.931,-23.532,150.989,-23.602,151.088,-23.696,151.154,-23.784,151.236,-23.825,151.501,-24.012,151.575,-24.034,151.691,-24.038,151.832,-24.123,151.903,-24.201,152.055,-24.494,152.13,-24.598,152.282,-24.699,152.353,-24.733,152.456,-24.802,152.493,-24.904,152.502,-24.964,152.563,-25.072,152.654,-25.202,152.789,-25.274,152.913,-25.432,152.921,-25.689,152.985,-25.816,153.028,-25.87,153.125,-25.923,153.165,-25.964,153.084,-26.304,153.162,-26.983,153.117,-27.194,153.198,-27.405,153.386,-27.769,153.428,-27.898,153.455,-28.048,153.576,-28.241,153.569,-28.533,153.617,-28.673,153.605,-28.854,153.462,-29.05,153.348,-29.29,153.347,-29.497,153.272,-29.892,153.224,-29.999,153.188,-30.164,153.031,-30.563,153.024,-30.72,153.048,-30.907,153.022,-31.087,152.982,-31.209,152.944,-31.435,152.786,-31.786,152.559,-32.046,152.545,-32.243,152.517,-32.33,152.47,-32.439,152.331,-32.558,152.247,-32.609,152.216,-32.678,152.137,-32.678,152.135,-32.7,152.188,-32.722,152.164,-32.757,151.954,-32.82,151.813,-32.901,151.668,-33.099,151.608,-33.202,151.53,-33.301,151.484,-33.347,151.463,-33.397,151.432,-33.522,151.358,-33.544,151.292,-33.581,151.323,-33.699,151.288,-33.835,151.28,-33.927,151.245,-33.985,151.202,-33.964,151.168,-33.973,151.125,-34.005,151.191,-34.015,151.232,-34.03,151.09,-34.162,150.96,-34.297,150.927,-34.387,150.871,-34.499,150.822,-34.749,150.781,-34.892,150.809,-34.994,150.805,-35.013,150.775,-35.02,150.756,-35.007,150.697,-35.042,150.681,-35.077,150.706,-35.12,150.722,-35.135,150.715,-35.155,150.69,-35.178,150.634,-35.178,150.567,-35.214,150.374,-35.584,150.292,-35.682,150.195,-35.834,150.158,-35.971,150.129,-36.12,150.095,-36.373,150.063,-36.55,149.988,-36.723,149.96,-36.846,149.951,-37.08,149.986,-37.258,149.963,-37.353,149.962,-37.444,149.933,-37.529,149.809,-37.548,149.709,-37.617,149.565,-37.73,149.481,-37.771,149.298,-37.802,148.944,-37.788,148.263,-37.831,148.131,-37.856,147.877,-37.934,147.631,-38.056,147.396,-38.219,146.857,-38.663,146.436,-38.712,146.356,-38.712,146.293,-38.7,146.217,-38.727,146.216,-38.783,146.286,-38.84,146.337,-38.894,146.427,-38.82,146.467,-38.84,146.482,-38.978,146.484,-39.065,146.457,-39.112,146.4,-39.146,146.34,-39.124,146.332,-39.077,146.254,-38.964,146.158,-38.866,146.07,-38.834,146.018,-38.867,145.935,-38.902,145.866,-38.776,145.791,-38.667,145.692,-38.656,145.606,-38.657,145.535,-38.61,145.397,-38.535,145.424,-38.477,145.463,-38.416,145.542,-38.394,145.518,-38.311,145.476,-38.244,145.366,-38.226,145.293,-38.238,145.249,-38.291,145.191,-38.384,144.96,-38.501,144.847,-38.436,144.718,-38.34,144.78,-38.347,144.911,-38.344,145.02,-38.258,145.067,-38.205,145.12,-38.091,145.05,-38.011,144.985,-37.952,144.891,-37.9,144.538,-38.077,144.465,-38.103,144.396,-38.137,144.518,-38.166,144.589,-38.158,144.665,-38.21,144.544,-38.284,144.448,-38.304,144.329,-38.348,144.102,-38.462,143.812,-38.699,143.687,-38.767,143.539,-38.821,143.338,-38.758,143.226,-38.743,143.083,-38.646,142.84,-38.581,142.612,-38.452,142.456,-38.386,142.345,-38.372,142.188,-38.399,141.925,-38.284,141.725,-38.271,141.594,-38.388,141.492,-38.38,141.424,-38.363,141.214,-38.172,141.011,-38.077,140.627,-38.028,140.39,-37.897,140.212,-37.642,139.875,-37.352,139.784,-37.246,139.742,-37.142,139.738,-37.06,139.784,-36.903,139.847,-36.748,139.857,-36.662,139.729,-36.371,139.549,-36.097,139.466,-36.01,139.245,-35.827,139.038,-35.689,138.985,-35.618,138.969,-35.581,139.067,-35.598,139.113,-35.542,139.178,-35.523,139.231,-35.598,139.289,-35.611,139.292,-35.486,139.325,-35.427,139.303,-35.399,139.283,-35.375,139.193,-35.347,139.094,-35.39,139.018,-35.443,138.915,-35.489,138.875,-35.537,138.771,-35.538,138.73,-35.551,138.522,-35.642,138.389,-35.645,138.184,-35.613,138.252,-35.487,138.333,-35.412,138.4,-35.326,138.511,-35.024,138.49,-34.764,138.436,-34.656,138.264,-34.44,138.186,-34.307,138.089,-34.17,138.041,-34.25,138.012,-34.334,137.919,-34.456,137.874,-34.727,137.692,-35.143,137.566,-35.148,137.46,-35.131,137.272,-35.179,137.144,-35.236,137.03,-35.237,136.967,-35.255,136.884,-35.24,137.014,-34.916,137.128,-34.925,137.252,-34.912,137.308,-34.917,137.391,-34.913,137.454,-34.764,137.493,-34.598,137.469,-34.49,137.459,-34.379,137.484,-34.252,137.494,-34.161,137.65,-33.859,137.781,-33.703,137.932,-33.579,137.914,-33.461,137.866,-33.314,137.852,-33.201,137.924,-33.165,137.993,-33.094,137.913,-32.771,137.863,-32.674,137.783,-32.578,137.782,-32.702,137.791,-32.823,137.68,-32.978,137.536,-33.089,137.442,-33.194,137.354,-33.43,137.237,-33.629,137.13,-33.703,137.034,-33.72,136.937,-33.75,136.783,-33.83,136.636,-33.897,136.526,-33.984,136.431,-34.03,136.121,-34.429,135.98,-34.562,135.951,-34.616,135.891,-34.661,135.903,-34.724,135.951,-34.767,135.999,-34.944,135.97,-34.982,135.919,-34.962,135.792,-34.863,135.713,-34.899,135.648,-34.94,135.481,-34.758,135.412,-34.716,135.324,-34.643,135.231,-34.58,135.191,-34.573,135.123,-34.586,135.13,-34.537,135.176,-34.497,135.217,-34.487,135.292,-34.546,135.379,-34.598,135.427,-34.602,135.45,-34.581,135.368,-34.376,135.312,-34.196,135.286,-34.142,135.219,-33.96,135.185,-33.907,135.042,-33.778,134.889,-33.626,134.847,-33.445,134.791,-33.328,134.719,-33.255,134.608,-33.19,134.301,-33.165,134.174,-32.979,134.1,-32.749,134.158,-32.733,134.227,-32.731,134.249,-32.659,134.234,-32.549,133.93,-32.412,133.787,-32.269,133.665,-32.207,133.551,-32.183,133.401,-32.188,133.212,-32.184,132.757,-31.956,132.649,-31.949,132.324,-32.02,132.215,-32.007,131.721,-31.696,131.393,-31.549,131.285,-31.521,131.144,-31.496,131.029,-31.532,130.948,-31.566,130.783,-31.604,130.13,-31.579,129.569,-31.627]},{"b":[177.255,-18.264,178.668,-17.313],"l":[178.575,-17.749,178.596,-17.699,178.592,-17.651,178.523,-17.596,178.411,-17.523,178.339,-17.438,178.309,-17.435,178.28,-17.416,178.28,-17.372,178.247,-17.329,178.188,-17.313,178.128,-17.339,177.94,-17.395,177.818,-17.388,177.618,-17.461,177.504,-17.54,177.401,-17.632,177.406,-17.682,177.423,-17.737,177.411,-17.754,177.386,-17.762,177.366,-17.786,177.36,-17.82,177.316,-17.846,177.264,-17.863,177.255,-17.915,177.263,-17.969,177.321,-18.078,177.383,-18.121,177.457,-18.148,177.636,-18.181,177.771,-18.22,177.847,-18.255,177.955,-18.264,178.064,-18.25,178.16,-18.25,178.244,-18.184,178.332,-18.135,178.423,-18.124,178.461,-18.139,178.487,-18.112,178.597,-18.109,178.668,-18.081,178.618,-17.933,178.604,-17.839,178.575,-17.749]},{"b":[164.037,-22.376,167.004,-20.142],"l":[166.942,-22.09,166.82,-22.017,166.69,-21.953,166.588,-21.873,166.493,-21.783,166.303,-21.637,166.058,-21.484,165.95,-21.442,165.885,-21.389,165.823,-21.364,165.775,-21.312,165.663,-21.267,165.582,-21.18,165.447,-21.081,165.421,-21.043,165.413,-20.981,165.381,-20.936,165.307,-20.887,165.252,-20.818,165.192,-20.769,165.112,-20.745,164.976,-20.681,164.588,-20.381,164.436,-20.282,164.315,-20.309,164.202,-20.246,164.06,-20.142,164.041,-20.173,164.037,-20.234,164.065,-20.279,164.124,-20.305,164.158,-20.348,164.152,-20.415,164.17,-20.48,164.313,-20.633,164.375,-20.739,164.455,-20.829,164.559,-20.906,164.656,-20.992,164.855,-21.202,164.927,-21.29,165.01,-21.327,165.242,-21.525,165.329,-21.58,165.428,-21.615,165.62,-21.724,165.744,-21.777,165.823,-21.854,165.933,-21.908,166.096,-21.957,166.124,-21.989,166.143,-22.044,166.177,-22.089,166.292,-22.155,166.416,-22.196,166.438,-22.232,166.468,-22.256,166.522,-22.249,166.571,-22.266,166.774,-22.376,166.835,-22.355,166.9,-22.353,166.97,-22.323,167.004,-22.262,166.942,-22.09]},{"b":[127.42,-0.893,128.9,2.199],"l":[127.687,-0.08,127.681,0.035,127.685,0.149,127.709,0.288,127.669,0.337,127.616,0.383,127.555,0.49,127.537,0.611,127.542,0.681,127.567,0.743,127.601,0.796,127.608,0.848,127.52,0.924,127.429,1.14,127.42,1.252,127.537,1.467,127.535,1.572,127.558,1.634,127.571,1.7,127.632,1.844,127.731,1.966,127.9,2.137,127.964,2.175,128.036,2.199,128.043,2.157,128.031,2.12,127.907,1.946,127.89,1.906,127.887,1.833,127.946,1.79,128.011,1.701,128.024,1.583,128.026,1.458,128.012,1.332,127.988,1.29,127.885,1.163,127.653,1.014,127.633,0.977,127.634,0.936,127.677,0.887,127.733,0.848,127.805,0.826,127.881,0.832,127.919,0.877,127.929,0.935,127.967,1.043,128.055,1.116,128.117,1.127,128.161,1.158,128.153,1.238,128.157,1.317,128.222,1.401,128.424,1.518,128.539,1.559,128.688,1.573,128.705,1.528,128.688,1.464,128.717,1.367,128.703,1.106,128.669,1.069,128.515,0.979,128.346,0.907,128.299,0.877,128.257,0.805,128.261,0.734,128.398,0.639,128.611,0.55,128.655,0.508,128.684,0.438,128.692,0.36,128.743,0.323,128.815,0.305,128.863,0.268,128.9,0.216,128.54,0.338,128.446,0.392,128.333,0.398,128.221,0.414,128.106,0.461,127.983,0.472,127.924,0.438,127.901,0.372,127.887,0.298,127.915,0.206,127.912,0.151,127.889,0.05,127.978,-0.248,128.089,-0.485,128.254,-0.732,128.335,-0.816,128.425,-0.893,128.278,-0.87,128.233,-0.788,128.046,-0.706,128.011,-0.657,127.889,-0.424,127.853,-0.38,127.741,-0.3,127.692,-0.242,127.675,-0.163,127.687,-0.08]},{"b":[68.77,-49.71,70.555,-48.656],"l":[70.32,-49.059,70.285,-49.076,70.208,-49.135,70.061,-49.136,69.984,-49.16,69.854,-49.222,69.771,-49.248,69.667,-49.265,69.611,-49.266,69.542,-49.256,69.405,-49.182,69.436,-49.124,69.572,-49.129,69.644,-49.117,69.587,-49.072,69.593,-48.971,69.535,-48.974,69.314,-49.106,69.265,-49.115,69.185,-49.11,69.052,-49.082,69.033,-49.018,69.099,-48.938,69.104,-48.9,69.136,-48.861,69.123,-48.766,69.072,-48.753,69.093,-48.724,69.081,-48.679,69.057,-48.656,69.002,-48.661,68.959,-48.694,68.9,-48.776,68.832,-48.849,68.837,-48.926,68.797,-48.995,68.77,-49.066,68.79,-49.104,68.817,-49.135,68.854,-49.141,68.883,-49.165,68.814,-49.192,68.799,-49.232,68.841,-49.285,68.818,-49.354,68.862,-49.392,68.872,-49.444,68.848,-49.5,68.811,-49.55,68.791,-49.6,68.783,-49.651,68.815,-49.7,68.873,-49.71,68.993,-49.705,69.086,-49.653,69.153,-49.53,69.275,-49.543,69.353,-49.563,69.478,-49.617,69.613,-49.651,69.682,-49.642,69.747,-49.602,69.804,-49.614,69.826,-49.645,69.919,-49.689,70.075,-49.709,70.124,-49.704,70.207,-49.665,70.216,-49.629,70.259,-49.601,70.307,-49.583,70.248,-49.531,70.166,-49.509,70.073,-49.518,70.063,-49.589,69.986,-49.582,69.856,-49.544,69.78,-49.49,69.749,-49.448,69.76,-49.43,69.818,-49.438,69.861,-49.421,69.902,-49.389,69.916,-49.349,69.993,-49.345,70.166,-49.343,70.238,-49.372,70.298,-49.425,70.338,-49.435,70.386,-49.434,70.411,-49.411,70.39,-49.366,70.485,-49.328,70.537,-49.266,70.555,-49.201,70.531,-49.137,70.484,-49.084,70.406,-49.061,70.32,-49.059]},{"b":[62.52,80.698,65.437,81.198],"l":[63.116,80.967,63.615,80.981,63.856,80.981,64.096,80.998,64.166,81.036,64.21,81.106,64.256,81.144,64.31,81.175,64.575,81.198,64.802,81.197,65.028,81.169,65.172,81.144,65.31,81.096,65.382,81.057,65.36,81.008,65.372,80.968,65.437,80.931,64.997,80.819,64.548,80.755,63.374,80.7,63.188,80.698,63.002,80.713,62.76,80.763,62.52,80.822,62.593,80.853,62.819,80.894,63.116,80.967]},{"b":[57.011,80.104,59.255,80.494],"l":[57.392,80.139,57.332,80.158,57.281,80.194,57.214,80.328,57.212,80.368,57.186,80.396,57.083,80.445,57.011,80.468,57.075,80.494,57.522,80.475,58.48,80.465,58.972,80.416,59.116,80.388,59.255,80.343,58.398,80.319,58.284,80.298,58.286,80.248,58.255,80.202,58.163,80.197,57.956,80.123,57.8,80.104,57.392,80.139]},{"b":[-28.037,70.454,-25.346,71.044],"l":[-25.468,70.78,-25.421,70.795,-25.401,70.811,-25.394,70.835,-25.397,70.862,-25.432,70.921,-25.458,70.943,-25.612,70.976,-25.661,70.998,-25.727,71.042,-25.819,71.044,-26.337,70.919,-26.622,70.876,-26.976,70.863,-27.239,70.868,-27.387,70.876,-27.617,70.914,-27.709,70.897,-27.744,70.79,-27.714,70.713,-27.805,70.642,-27.94,70.615,-27.968,70.595,-28.037,70.514,-28.035,70.487,-28.003,70.467,-27.898,70.454,-27.69,70.479,-27.105,70.531,-26.605,70.553,-26.339,70.511,-26.274,70.454,-26.218,70.454,-26.05,70.509,-25.911,70.573,-25.801,70.599,-25.402,70.653,-25.346,70.693,-25.352,70.714,-25.38,70.741,-25.468,70.78]},{"b":[159.607,-9.929,160.819,-9.269],"l":[160.682,-9.692,160.625,-9.589,160.525,-9.536,160.355,-9.422,160.065,-9.419,159.971,-9.433,159.75,-9.273,159.686,-9.269,159.626,-9.311,159.607,-9.354,159.612,-9.471,159.622,-9.532,159.68,-9.637,159.755,-9.726,159.803,-9.763,159.854,-9.792,160.002,-9.812,160.321,-9.821,160.482,-9.895,160.649,-9.929,160.713,-9.914,160.802,-9.878,160.819,-9.863,160.794,-9.767,160.751,-9.715,160.682,-9.692]},{"b":[117.219,8.367,119.687,11.346],"l":[119.297,10.751,119.313,10.687,119.287,10.574,119.224,10.477,119.186,10.439,119.143,10.409,119.08,10.386,119.024,10.354,118.845,10.131,118.82,10.105,118.728,10.035,118.533,9.794,118.344,9.603,118.115,9.347,118.024,9.27,117.983,9.253,117.932,9.251,117.885,9.241,117.745,9.098,117.593,8.968,117.53,8.903,117.418,8.767,117.35,8.714,117.256,8.541,117.229,8.457,117.219,8.367,117.311,8.44,117.413,8.496,117.469,8.511,117.517,8.538,117.54,8.596,117.572,8.642,117.68,8.678,117.78,8.729,117.889,8.798,117.99,8.877,118.069,8.984,118.134,9.101,118.229,9.168,118.35,9.201,118.435,9.256,118.504,9.333,118.57,9.423,118.774,9.767,118.755,9.862,118.782,9.916,118.835,9.949,118.949,9.993,119.192,10.061,119.219,10.101,119.232,10.152,119.285,10.252,119.369,10.327,119.422,10.354,119.541,10.379,119.595,10.407,119.687,10.5,119.684,10.552,119.616,10.707,119.527,10.953,119.562,11.046,119.533,11.102,119.535,11.157,119.56,11.267,119.553,11.314,119.501,11.346,119.465,11.294,119.341,11.033,119.306,10.974,119.261,10.845,119.297,10.751]},{"b":[121.891,10.444,123.158,11.897],"l":[123.103,11.541,122.895,11.441,122.901,11.487,122.931,11.529,122.838,11.596,122.726,11.608,122.613,11.564,122.496,11.615,122.399,11.702,122.291,11.772,122.087,11.855,122.029,11.895,121.964,11.897,121.916,11.854,121.891,11.791,121.941,11.758,122.067,11.724,122.101,11.681,122.104,11.643,122.06,11.326,122.051,11.097,122.021,10.979,121.964,10.872,121.95,10.757,121.972,10.699,121.98,10.639,121.934,10.494,121.938,10.471,121.954,10.444,121.988,10.458,122.052,10.514,122.109,10.576,122.198,10.623,122.522,10.692,122.673,10.801,122.77,10.824,122.791,10.88,122.789,10.941,122.803,10.99,122.847,11.022,122.939,11.058,123.017,11.117,123.075,11.197,123.12,11.287,123.144,11.364,123.156,11.443,123.158,11.536,123.103,11.541]},{"b":[122.4,9.053,123.568,10.994],"l":[123.511,10.923,123.257,10.994,123.222,10.989,123.024,10.912,122.983,10.887,122.97,10.836,122.969,10.766,122.958,10.698,122.906,10.603,122.856,10.553,122.817,10.504,122.852,10.395,122.867,10.284,122.866,10.125,122.856,10.087,122.713,9.99,122.648,9.982,122.523,9.979,122.471,9.962,122.426,9.896,122.4,9.823,122.411,9.694,122.563,9.483,122.61,9.443,122.665,9.41,122.772,9.371,122.867,9.32,122.948,9.108,122.995,9.059,123.065,9.053,123.131,9.064,123.192,9.088,123.229,9.121,123.293,9.217,123.321,9.273,123.322,9.317,123.308,9.357,123.15,9.606,123.149,9.659,123.163,9.715,123.162,9.864,123.187,9.933,123.266,10.059,123.296,10.125,123.344,10.325,123.407,10.459,123.493,10.582,123.528,10.662,123.568,10.781,123.563,10.816,123.511,10.923]},{"b":[124.308,10.033,125.268,11.535],"l":[125.197,10.457,125.188,10.585,125.164,10.637,125.128,10.685,125.084,10.722,125.034,10.751,125.013,10.786,125.04,10.952,125.044,11.135,125.027,11.212,124.994,11.256,124.93,11.373,124.821,11.401,124.724,11.322,124.644,11.308,124.575,11.343,124.548,11.395,124.511,11.424,124.436,11.457,124.374,11.515,124.331,11.535,124.308,11.486,124.331,11.427,124.366,11.371,124.412,11.15,124.446,10.924,124.503,10.904,124.616,10.962,124.663,10.962,124.739,10.88,124.787,10.781,124.797,10.732,124.799,10.682,124.738,10.44,124.79,10.328,124.792,10.275,124.781,10.168,124.813,10.135,124.929,10.096,125.027,10.033,125.024,10.115,125.005,10.197,124.988,10.368,125.044,10.323,125.105,10.218,125.143,10.189,125.14,10.235,125.148,10.272,125.253,10.264,125.268,10.308,125.26,10.35,125.197,10.457]},{"b":[124.295,11.05,125.749,12.573],"l":[125.627,11.234,125.574,11.238,125.582,11.279,125.609,11.323,125.593,11.378,125.506,11.544,125.492,11.594,125.5,11.655,125.497,11.714,125.464,11.772,125.457,11.953,125.513,12.055,125.503,12.136,125.536,12.191,125.481,12.252,125.409,12.285,125.352,12.293,125.32,12.322,125.328,12.387,125.31,12.446,125.24,12.528,125.15,12.573,124.84,12.535,124.566,12.526,124.295,12.569,124.326,12.404,124.385,12.244,124.446,12.153,124.529,12.079,124.572,12.055,124.677,12.021,124.75,11.933,124.796,11.896,124.821,11.852,124.884,11.775,124.936,11.755,124.995,11.765,124.998,11.702,124.979,11.638,124.917,11.558,124.945,11.479,125.034,11.341,125.088,11.287,125.156,11.267,125.233,11.145,125.312,11.142,125.432,11.113,125.628,11.132,125.674,11.121,125.736,11.05,125.749,11.074,125.704,11.165,125.627,11.234]},{"b":[120.338,12.219,121.541,13.522],"l":[121.48,12.837,121.475,12.932,121.49,13.02,121.539,13.089,121.523,13.131,121.442,13.188,121.357,13.265,121.284,13.374,121.203,13.432,121.122,13.381,121.079,13.411,121.025,13.429,120.981,13.486,120.915,13.501,120.755,13.471,120.704,13.479,120.653,13.498,120.468,13.522,120.401,13.517,120.353,13.473,120.338,13.412,120.388,13.402,120.438,13.405,120.455,13.394,120.481,13.311,120.508,13.26,120.573,13.209,120.651,13.169,120.68,13.131,120.764,12.97,120.769,12.841,120.776,12.791,120.796,12.748,120.855,12.704,120.899,12.646,120.921,12.581,120.922,12.512,120.963,12.447,121.049,12.36,121.083,12.339,121.108,12.304,121.117,12.253,121.155,12.236,121.237,12.219,121.289,12.277,121.322,12.304,121.357,12.313,121.394,12.301,121.4,12.361,121.418,12.389,121.412,12.423,121.458,12.508,121.519,12.584,121.541,12.638,121.48,12.837]},{"b":[154.709,-6.863,155.958,-5.444],"l":[155.823,-6.38,155.734,-6.296,155.638,-6.221,155.581,-6.196,155.519,-6.182,155.467,-6.145,155.373,-5.974,155.323,-5.932,155.228,-5.865,155.198,-5.828,155.187,-5.777,155.094,-5.62,154.997,-5.54,154.871,-5.521,154.818,-5.494,154.773,-5.454,154.729,-5.444,154.741,-5.545,154.709,-5.747,154.721,-5.817,154.759,-5.931,154.782,-5.971,154.87,-6.061,154.94,-6.106,155.01,-6.21,155.045,-6.234,155.202,-6.308,155.234,-6.412,155.209,-6.527,155.261,-6.626,155.344,-6.722,155.427,-6.783,155.521,-6.83,155.617,-6.856,155.719,-6.863,155.763,-6.834,155.805,-6.796,155.892,-6.762,155.915,-6.797,155.933,-6.78,155.958,-6.687,155.928,-6.565,155.882,-6.47,155.823,-6.38]},{"b":[150.746,-4.832,153.133,-2.573],"l":[153.017,-4.106,152.38,-3.582,152.329,-3.521,152.179,-3.41,152.065,-3.28,152.033,-3.251,151.807,-3.173,151.69,-3.073,151.586,-3.003,151.475,-2.942,151.315,-2.875,151.226,-2.87,151.175,-2.789,150.995,-2.688,150.825,-2.573,150.843,-2.644,150.826,-2.713,150.746,-2.739,150.848,-2.78,150.968,-2.78,151.067,-2.829,151.405,-3.037,151.465,-3.101,151.579,-3.154,151.793,-3.338,151.88,-3.4,151.973,-3.453,152.023,-3.469,152.136,-3.487,152.192,-3.506,152.279,-3.582,152.356,-3.668,152.598,-3.995,152.668,-4.132,152.697,-4.282,152.693,-4.356,152.678,-4.429,152.681,-4.498,152.74,-4.636,152.787,-4.699,152.846,-4.762,152.892,-4.832,152.966,-4.756,153.023,-4.666,153.046,-4.576,153.044,-4.476,153.112,-4.392,153.133,-4.352,153.124,-4.252,153.017,-4.106]},{"b":[137.65,-8.412,138.989,-7.38],"l":[138.77,-7.39,138.544,-7.38,138.296,-7.438,138.185,-7.495,138.082,-7.566,138.008,-7.642,137.833,-7.932,137.685,-8.262,137.65,-8.386,137.688,-8.412,137.872,-8.38,137.983,-8.382,138.296,-8.405,138.535,-8.274,138.612,-8.198,138.786,-8.059,138.893,-7.882,138.989,-7.696,138.963,-7.588,138.899,-7.512,138.802,-7.415,138.77,-7.39]},{"b":[127.878,-3.858,130.86,-2.786],"l":[130.773,-3.419,130.718,-3.411,130.671,-3.392,130.642,-3.312,130.626,-3.228,130.57,-3.131,130.379,-2.989,130.304,-2.979,130.103,-2.993,129.984,-2.977,129.755,-2.866,129.6,-2.806,129.543,-2.79,129.484,-2.786,129.427,-2.791,129.371,-2.821,129.28,-2.889,129.174,-2.933,129.116,-2.937,129.074,-2.895,129.058,-2.838,128.991,-2.829,128.911,-2.85,128.791,-2.857,128.57,-2.842,128.199,-2.866,128.113,-2.935,127.878,-3.222,127.897,-3.282,127.928,-3.341,127.928,-3.397,127.902,-3.496,127.92,-3.506,127.97,-3.444,128.03,-3.341,128.044,-3.303,128.056,-3.239,128.082,-3.184,128.132,-3.157,128.181,-3.172,128.233,-3.203,128.28,-3.241,128.419,-3.416,128.466,-3.44,128.517,-3.449,128.639,-3.433,128.677,-3.397,128.751,-3.3,128.802,-3.266,128.863,-3.235,128.925,-3.229,128.958,-3.241,128.964,-3.272,128.952,-3.304,128.967,-3.326,129.108,-3.349,129.212,-3.393,129.333,-3.409,129.468,-3.453,129.522,-3.434,129.52,-3.363,129.512,-3.329,129.545,-3.319,129.627,-3.317,129.844,-3.327,129.953,-3.392,129.981,-3.439,130.02,-3.475,130.27,-3.579,130.363,-3.625,130.58,-3.749,130.805,-3.858,130.86,-3.57,130.846,-3.533,130.773,-3.419]},{"b":[126.026,-3.824,127.244,-3.065],"l":[127.163,-3.338,127.125,-3.311,127.092,-3.278,127.063,-3.217,127.025,-3.166,126.861,-3.088,126.808,-3.069,126.555,-3.065,126.306,-3.103,126.22,-3.148,126.088,-3.105,126.05,-3.128,126.026,-3.171,126.034,-3.356,126.057,-3.421,126.147,-3.523,126.178,-3.579,126.215,-3.605,126.411,-3.711,126.547,-3.772,126.686,-3.824,126.74,-3.814,126.794,-3.789,126.87,-3.783,126.941,-3.765,127.085,-3.671,127.155,-3.647,127.23,-3.633,127.244,-3.471,127.227,-3.391,127.163,-3.338]},{"b":[119.807,-8.935,123.006,-8.093],"l":[122.978,-8.152,122.917,-8.106,122.846,-8.093,122.792,-8.127,122.759,-8.186,122.919,-8.222,122.85,-8.304,122.75,-8.353,122.604,-8.402,122.556,-8.432,122.514,-8.47,122.484,-8.514,122.467,-8.566,122.433,-8.601,122.323,-8.628,122.263,-8.625,122.067,-8.497,122.02,-8.472,121.967,-8.455,121.912,-8.482,121.863,-8.494,121.747,-8.507,121.683,-8.506,121.61,-8.526,121.548,-8.575,121.498,-8.585,121.445,-8.578,121.372,-8.551,121.277,-8.478,121.118,-8.424,121.009,-8.366,120.886,-8.327,120.751,-8.321,120.71,-8.308,120.61,-8.24,120.547,-8.26,120.486,-8.266,120.425,-8.249,120.354,-8.258,120.231,-8.29,120.099,-8.378,119.964,-8.436,119.918,-8.445,119.875,-8.42,119.866,-8.473,119.848,-8.523,119.818,-8.571,119.807,-8.623,119.808,-8.698,119.841,-8.764,119.879,-8.808,119.909,-8.858,120.012,-8.81,120.121,-8.777,120.32,-8.82,120.55,-8.802,120.781,-8.849,120.982,-8.928,121.035,-8.935,121.086,-8.926,121.138,-8.904,121.191,-8.896,121.328,-8.917,121.415,-8.815,121.5,-8.812,121.585,-8.821,121.621,-8.854,121.651,-8.899,121.738,-8.87,121.839,-8.86,122.094,-8.745,122.186,-8.73,122.321,-8.738,122.417,-8.735,122.47,-8.725,122.554,-8.681,122.642,-8.647,122.783,-8.612,122.82,-8.596,122.847,-8.562,122.811,-8.481,122.902,-8.416,122.924,-8.381,122.955,-8.354,123.006,-8.329,122.978,-8.152]},{"b":[118.959,-10.294,120.833,-9.301],"l":[120.7,-9.903,120.633,-9.806,120.556,-9.719,120.504,-9.674,120.444,-9.646,120.365,-9.655,120.291,-9.648,120.258,-9.603,120.248,-9.543,120.221,-9.506,120.058,-9.42,120.013,-9.375,119.974,-9.322,119.942,-9.301,119.851,-9.36,119.795,-9.38,119.615,-9.352,119.424,-9.37,119.296,-9.367,119.186,-9.384,119.031,-9.44,118.994,-9.472,118.959,-9.519,118.977,-9.573,119.008,-9.621,119.042,-9.669,119.085,-9.707,119.363,-9.772,119.417,-9.771,119.47,-9.761,119.601,-9.774,119.813,-9.917,119.931,-9.967,119.998,-10.04,120.052,-10.123,120.145,-10.2,120.255,-10.242,120.395,-10.263,120.439,-10.294,120.562,-10.236,120.64,-10.228,120.698,-10.207,120.804,-10.108,120.833,-10.037,120.784,-9.957,120.7,-9.903]},{"b":[116.768,-9.099,119.13,-8.089],"l":[119.044,-8.457,118.988,-8.338,118.926,-8.298,118.846,-8.293,118.794,-8.306,118.748,-8.331,118.714,-8.415,118.692,-8.393,118.671,-8.323,118.612,-8.281,118.552,-8.27,118.491,-8.271,118.433,-8.293,118.338,-8.354,118.292,-8.357,118.242,-8.318,118.203,-8.267,118.151,-8.15,118.117,-8.122,117.921,-8.089,117.868,-8.101,117.755,-8.15,117.738,-8.205,117.766,-8.279,117.815,-8.342,117.979,-8.459,118.018,-8.467,118.061,-8.464,118.1,-8.475,118.174,-8.528,118.235,-8.592,118.206,-8.652,118.104,-8.65,117.97,-8.728,117.893,-8.704,117.806,-8.711,117.712,-8.583,117.673,-8.563,117.643,-8.536,117.622,-8.46,117.567,-8.426,117.435,-8.435,117.357,-8.429,117.224,-8.375,117.165,-8.367,117.064,-8.444,116.953,-8.503,116.886,-8.508,116.835,-8.532,116.801,-8.598,116.783,-8.665,116.807,-8.811,116.772,-8.894,116.768,-8.955,116.788,-9.006,116.871,-9.046,116.958,-9.076,117.061,-9.099,117.161,-9.069,117.21,-9.034,117.265,-9.026,117.326,-9.034,117.388,-9.032,117.508,-9.008,117.732,-8.92,117.795,-8.92,117.861,-8.931,118.071,-8.851,118.132,-8.856,118.19,-8.841,118.234,-8.808,118.379,-8.675,118.4,-8.704,118.398,-8.813,118.427,-8.855,118.479,-8.856,118.674,-8.812,118.728,-8.805,118.808,-8.838,118.833,-8.833,118.837,-8.809,118.818,-8.791,118.756,-8.774,118.746,-8.735,118.821,-8.712,118.903,-8.703,118.939,-8.713,118.971,-8.741,119.006,-8.75,119.079,-8.73,119.104,-8.71,119.13,-8.668,119.101,-8.628,119.063,-8.6,119.042,-8.561,119.044,-8.457]},{"b":[115.857,-8.929,116.734,-8.204],"l":[116.402,-8.204,116.304,-8.238,116.22,-8.295,116.061,-8.437,116.078,-8.611,116.076,-8.745,116.032,-8.765,115.914,-8.758,115.869,-8.743,115.857,-8.788,115.875,-8.826,116.027,-8.873,116.239,-8.912,116.29,-8.906,116.377,-8.929,116.587,-8.886,116.559,-8.854,116.514,-8.821,116.641,-8.614,116.734,-8.387,116.719,-8.336,116.688,-8.304,116.647,-8.283,116.402,-8.204]},{"b":[114.468,-8.849,115.704,-8.066],"l":[115.549,-8.208,115.448,-8.155,115.34,-8.115,115.191,-8.067,115.154,-8.066,114.998,-8.174,114.938,-8.187,114.833,-8.183,114.62,-8.128,114.504,-8.117,114.475,-8.119,114.468,-8.166,114.479,-8.215,114.502,-8.261,114.571,-8.345,114.613,-8.378,114.731,-8.394,114.842,-8.429,114.952,-8.496,115.055,-8.573,115.106,-8.629,115.142,-8.697,115.14,-8.769,115.092,-8.829,115.145,-8.849,115.194,-8.835,115.22,-8.82,115.236,-8.798,115.247,-8.758,115.295,-8.664,115.334,-8.616,115.56,-8.514,115.661,-8.448,115.704,-8.407,115.691,-8.364,115.549,-8.208]},{"b":[105.133,-3.072,106.818,-1.505],"l":[106.366,-2.465,106.209,-2.189,106.162,-1.867,106.127,-1.8,106.08,-1.738,106.046,-1.669,106.027,-1.593,105.981,-1.539,105.91,-1.505,105.816,-1.506,105.72,-1.534,105.754,-1.659,105.701,-1.731,105.668,-1.68,105.64,-1.61,105.585,-1.527,105.46,-1.575,105.413,-1.611,105.373,-1.657,105.364,-1.705,105.387,-1.751,105.375,-1.813,105.316,-1.861,105.191,-1.917,105.138,-1.973,105.133,-2.043,105.248,-2.079,105.293,-2.114,105.343,-2.125,105.553,-2.079,105.599,-2.103,105.705,-2.133,105.786,-2.181,105.807,-2.307,105.862,-2.415,105.908,-2.452,105.939,-2.493,105.908,-2.643,105.937,-2.744,105.999,-2.825,106.126,-2.855,106.25,-2.894,106.342,-2.949,106.397,-2.967,106.449,-2.994,106.496,-3.029,106.547,-3.056,106.611,-3.071,106.667,-3.072,106.658,-3.001,106.619,-2.936,106.612,-2.896,106.679,-2.704,106.707,-2.658,106.744,-2.618,106.818,-2.573,106.366,-2.465]},{"b":[21.854,57.931,23.323,58.627],"l":[22.964,58.606,22.82,58.622,22.754,58.605,22.688,58.597,22.617,58.621,22.547,58.627,22.474,58.605,22.328,58.581,22.267,58.508,22.206,58.521,22.169,58.516,22.081,58.478,22.002,58.51,21.924,58.514,21.862,58.497,21.984,58.387,21.965,58.349,21.924,58.316,21.891,58.305,21.854,58.302,21.882,58.262,22.035,58.213,22.104,58.172,22.188,58.154,22.153,58.115,21.986,57.995,21.978,57.963,21.997,57.931,22.076,57.936,22.152,57.967,22.227,58.052,22.269,58.161,22.372,58.217,22.498,58.236,22.73,58.231,22.757,58.261,22.885,58.311,22.98,58.364,23.035,58.372,23.083,58.398,23.127,58.436,23.323,58.451,23.293,58.483,22.964,58.606]},{"b":[20.228,77.267,24.902,78.598],"l":[24.238,77.899,23.883,77.865,23.684,77.875,23.331,77.958,23.117,77.992,23.152,78.088,23.365,78.121,23.452,78.149,23.352,78.186,23.119,78.239,22.989,78.252,22.735,78.24,22.449,78.215,22.3,78.228,22.207,78.408,22.043,78.577,21.746,78.572,21.608,78.596,21.455,78.598,21.047,78.557,20.363,78.515,20.228,78.478,20.373,78.412,20.56,78.419,20.528,78.326,20.786,78.252,20.845,78.166,21.035,78.059,21.21,78.006,21.653,77.924,21.608,77.916,21.431,77.812,21.334,77.772,21.251,77.711,21.201,77.619,20.873,77.565,20.928,77.46,21.05,77.441,21.856,77.494,22.057,77.501,22.255,77.529,22.397,77.57,22.448,77.571,22.62,77.55,22.685,77.554,22.733,77.539,22.679,77.5,22.442,77.429,22.487,77.36,22.469,77.331,22.427,77.316,22.554,77.267,22.802,77.276,22.9,77.311,22.997,77.361,23.101,77.385,23.381,77.38,23.505,77.401,23.736,77.462,23.841,77.498,23.955,77.558,24.062,77.631,24.13,77.658,24.902,77.757,24.571,77.834,24.238,77.899]},{"b":[59.288,80.419,62.228,80.893],"l":[62.103,80.867,61.851,80.886,61.597,80.893,61.313,80.863,60.82,80.827,60.482,80.804,60.278,80.801,60.235,80.838,60.095,80.849,59.716,80.836,59.592,80.817,59.549,80.784,59.495,80.767,59.387,80.713,59.306,80.618,59.288,80.573,59.304,80.522,59.346,80.505,59.65,80.431,59.9,80.446,60.278,80.494,60.722,80.435,61.051,80.419,61.285,80.505,61.597,80.535,61.681,80.586,61.769,80.601,62.076,80.617,62.115,80.684,62.192,80.73,62.228,80.794,62.168,80.835,62.103,80.867]},{"b":[44.905,80.447,48.687,80.854],"l":[47.011,80.562,47.145,80.609,47.198,80.615,47.304,80.606,47.414,80.675,47.512,80.688,47.6,80.742,47.705,80.765,47.777,80.756,48.044,80.668,48.625,80.629,48.684,80.633,48.687,80.718,48.547,80.779,48.446,80.806,48.345,80.819,48.243,80.823,47.9,80.813,47.442,80.854,47.352,80.853,47.021,80.814,46.799,80.755,46.327,80.735,45.125,80.652,44.905,80.611,45.149,80.599,45.389,80.56,45.641,80.537,45.969,80.569,46.024,80.541,46.06,80.484,46.141,80.447,46.378,80.457,46.514,80.476,46.624,80.541,46.678,80.561,47.011,80.562]},{"b":[46.644,80.082,51.704,80.927],"l":[51.455,80.745,50.918,80.89,50.801,80.914,50.431,80.911,50.278,80.927,50.124,80.924,49.508,80.865,49.244,80.821,49.147,80.712,49.193,80.656,49.185,80.559,49.088,80.516,48.625,80.508,48.465,80.558,48.403,80.569,48.306,80.562,48.208,80.544,47.896,80.529,47.656,80.501,47.403,80.445,46.644,80.3,46.738,80.258,46.846,80.237,46.991,80.183,47.249,80.18,47.343,80.189,47.444,80.23,47.642,80.245,47.893,80.239,47.978,80.213,47.723,80.151,47.632,80.112,47.737,80.082,47.94,80.089,48.026,80.099,48.096,80.122,48.167,80.133,48.386,80.096,48.467,80.11,48.533,80.158,48.555,80.183,48.582,80.195,48.797,80.161,48.892,80.155,48.978,80.163,49.011,80.207,48.991,80.242,48.96,80.266,48.922,80.277,48.689,80.29,48.677,80.3,48.811,80.354,48.896,80.369,49.586,80.377,49.794,80.425,49.75,80.472,49.846,80.498,50.28,80.527,50.961,80.54,51.146,80.604,51.704,80.688,51.591,80.741,51.455,80.745]},{"b":[48.279,68.733,50.283,69.511],"l":[49.225,69.511,48.953,69.509,48.845,69.495,48.631,69.436,48.414,69.346,48.32,69.269,48.296,69.184,48.28,69.097,48.279,69.04,48.294,68.984,48.316,68.942,48.439,68.805,48.667,68.733,48.91,68.743,49.18,68.778,49.626,68.86,49.84,68.974,49.921,69.053,50.094,69.126,50.141,69.098,50.164,69.038,50.221,69.049,50.283,69.089,50.265,69.186,50.167,69.257,49.996,69.309,49.225,69.511]},{"b":[58.473,69.688,60.481,70.465],"l":[60.393,69.962,60.172,70.023,59.956,70.108,59.636,70.197,59.529,70.249,59.426,70.311,59.31,70.362,59.088,70.437,59.048,70.46,59.005,70.465,58.794,70.433,58.678,70.36,58.615,70.351,58.52,70.318,58.473,70.267,58.568,70.156,58.606,70.129,58.634,70.088,58.68,70.051,58.953,69.893,59.004,69.883,59.083,69.911,59.144,69.922,59.268,69.898,59.382,69.89,59.503,69.866,59.581,69.791,59.578,69.739,59.637,69.721,59.725,69.706,59.813,69.696,59.92,69.697,60.026,69.717,60.216,69.688,60.327,69.715,60.44,69.726,60.477,69.794,60.481,69.885,60.45,69.935,60.393,69.962]},{"b":[69.92,73.037,71.63,73.514],"l":[71.445,73.342,71.351,73.372,71.232,73.448,71.141,73.478,71.023,73.504,70.94,73.514,70.35,73.478,70.15,73.445,69.996,73.359,70.019,73.224,69.986,73.169,69.93,73.127,69.92,73.085,70.041,73.037,70.119,73.056,70.298,73.044,70.38,73.048,70.674,73.095,70.887,73.12,71.356,73.162,71.626,73.174,71.63,73.225,71.59,73.283,71.445,73.342]},{"b":[91.07,79.675,93.803,80.052],"l":[92.593,79.997,92.173,80.045,91.752,80.052,91.426,80.049,91.229,80.031,91.07,79.981,91.126,79.905,91.376,79.835,91.684,79.791,92.154,79.685,92.441,79.675,92.683,79.685,92.926,79.704,93.155,79.738,93.382,79.784,93.604,79.817,93.803,79.905,93.482,79.941,92.593,79.997]},{"b":[111.503,74.095,113.387,74.549],"l":[112.084,74.549,112.008,74.527,111.989,74.496,111.983,74.456,111.949,74.389,111.88,74.364,111.638,74.374,111.57,74.368,111.503,74.353,111.643,74.273,111.912,74.219,112.105,74.163,112.196,74.146,112.782,74.095,112.811,74.103,112.978,74.197,113.19,74.239,113.259,74.273,113.299,74.317,113.353,74.353,113.387,74.4,113.286,74.441,112.952,74.48,112.084,74.549]},{"b":[140.102,73.919,141.097,74.274],"l":[141.039,74.243,140.944,74.265,140.849,74.274,140.407,74.266,140.3,74.257,140.194,74.237,140.102,74.184,140.183,74.005,140.409,73.922,140.507,73.919,141.01,73.999,141.047,74.05,141.097,74.168,141.079,74.209,141.039,74.243]},{"b":[23.548,34.934,26.32,35.656],"l":[26.168,35.215,26.028,35.215,25.893,35.179,25.837,35.133,25.791,35.123,25.745,35.143,25.735,35.184,25.756,35.326,25.73,35.349,25.57,35.328,25.476,35.306,25.297,35.339,25.104,35.347,25.003,35.41,24.721,35.425,24.627,35.41,24.535,35.381,24.445,35.366,24.354,35.359,24.313,35.364,24.275,35.386,24.258,35.423,24.255,35.469,24.179,35.46,24.123,35.484,24.109,35.496,24.124,35.511,24.198,35.537,24.166,35.595,24.093,35.594,24.034,35.535,24.013,35.529,23.921,35.528,23.852,35.535,23.793,35.556,23.771,35.634,23.737,35.656,23.715,35.605,23.715,35.55,23.673,35.514,23.627,35.53,23.609,35.566,23.57,35.535,23.548,35.416,23.562,35.295,23.593,35.257,23.638,35.235,23.704,35.233,23.884,35.246,23.994,35.222,24.464,35.16,24.583,35.115,24.709,35.089,24.735,35.058,24.744,35.014,24.745,34.951,24.8,34.934,25.206,34.959,25.611,35.007,25.83,35.025,26.047,35.014,26.166,35.019,26.244,35.045,26.256,35.095,26.281,35.159,26.299,35.269,26.32,35.315,26.286,35.31,26.168,35.215]},{"b":[22.87,37.97,24.588,39.035],"l":[24.464,38.145,24.36,38.163,24.276,38.22,24.22,38.339,24.188,38.463,24.211,38.504,24.2,38.541,24.155,38.588,24.128,38.648,24.099,38.671,23.878,38.687,23.688,38.765,23.636,38.77,23.525,38.813,23.472,38.85,23.415,38.959,23.313,39.035,23.258,39.031,23.146,39.003,22.986,38.916,22.87,38.871,22.881,38.848,22.936,38.84,23.029,38.873,23.144,38.845,23.252,38.801,23.364,38.735,23.465,38.656,23.505,38.613,23.553,38.582,23.617,38.553,23.651,38.443,23.759,38.401,23.886,38.401,24.04,38.39,24.042,38.374,24.064,38.337,24.103,38.317,24.144,38.243,24.189,38.204,24.193,38.152,24.212,38.118,24.318,38.06,24.359,38.019,24.417,38.017,24.446,38.005,24.473,37.981,24.502,37.97,24.537,37.98,24.579,38.02,24.588,38.124,24.563,38.148,24.464,38.145]},{"b":[32.301,34.57,34.556,35.662],"l":[34.272,35.57,34.192,35.546,34.063,35.474,33.608,35.354,33.459,35.336,33.308,35.342,33.123,35.358,32.942,35.39,32.926,35.278,32.88,35.181,32.772,35.16,32.713,35.171,32.652,35.183,32.556,35.156,32.475,35.09,32.391,35.05,32.301,35.083,32.317,34.953,32.414,34.778,32.449,34.729,32.506,34.706,32.693,34.649,32.75,34.648,32.867,34.661,32.914,34.635,32.942,34.576,33.008,34.57,33.024,34.6,33.025,34.637,33.062,34.675,33.116,34.696,33.176,34.698,33.297,34.718,33.415,34.751,33.514,34.806,33.699,34.97,33.759,34.973,33.822,34.966,33.937,34.971,34.05,34.988,34.024,35.046,34.004,35.065,33.931,35.14,33.908,35.202,33.942,35.292,34.463,35.594,34.556,35.662,34.411,35.629,34.272,35.57]},{"b":[18.105,56.921,19.076,57.915],"l":[18.841,57.9,18.805,57.833,18.722,57.864,18.537,57.831,18.405,57.757,18.283,57.655,18.205,57.611,18.137,57.557,18.129,57.449,18.152,57.339,18.105,57.272,18.164,57.212,18.21,57.133,18.285,57.083,18.207,57.01,18.146,56.921,18.249,56.932,18.34,56.978,18.387,57.088,18.477,57.163,18.538,57.197,18.7,57.243,18.743,57.324,18.785,57.361,18.844,57.386,18.908,57.398,18.791,57.483,18.814,57.706,18.878,57.73,18.945,57.742,18.994,57.812,19.076,57.836,18.956,57.9,18.901,57.915,18.841,57.9]},{"b":[10.979,54.773,12.608,56.122],"l":[12.073,54.977,12.066,55.07,12.09,55.188,12.322,55.237,12.413,55.286,12.385,55.386,12.275,55.414,12.215,55.467,12.243,55.538,12.321,55.588,12.407,55.616,12.507,55.637,12.545,55.656,12.571,55.685,12.569,55.785,12.525,55.918,12.543,55.959,12.608,56.033,12.579,56.064,12.526,56.083,12.428,56.106,12.323,56.122,12.219,56.119,12.04,56.052,11.866,55.968,11.913,55.937,11.935,55.896,11.922,55.828,11.885,55.808,11.858,55.772,11.82,55.698,11.784,55.702,11.691,55.729,11.682,55.829,11.696,55.908,11.628,55.957,11.475,55.943,11.46,55.907,11.464,55.879,11.322,55.753,11.275,55.736,11.224,55.731,11.05,55.74,10.979,55.722,11.009,55.644,11.07,55.629,11.121,55.601,11.12,55.566,11.128,55.535,11.19,55.466,11.171,55.329,11.286,55.204,11.31,55.198,11.407,55.215,11.476,55.212,11.654,55.187,11.697,55.096,11.704,55.039,11.74,54.972,11.741,54.915,11.862,54.773,12.05,54.815,12.069,54.909,12.073,54.977]},{"b":[9.859,55.049,10.819,55.613],"l":[10.785,55.133,10.808,55.203,10.785,55.27,10.819,55.322,10.738,55.446,10.687,55.558,10.645,55.61,10.623,55.613,10.505,55.558,10.424,55.56,10.354,55.599,10.286,55.611,9.994,55.535,9.861,55.515,9.859,55.357,9.93,55.229,9.967,55.205,9.989,55.163,10.255,55.088,10.443,55.049,10.624,55.052,10.785,55.133]},{"b":[2.37,39.301,3.462,39.971],"l":[3.241,39.757,3.145,39.79,3.159,39.837,3.191,39.861,3.199,39.89,3.167,39.908,3.164,39.924,3.198,39.961,3.159,39.971,2.905,39.908,2.785,39.855,2.371,39.613,2.37,39.572,2.394,39.54,2.459,39.53,2.5,39.478,2.576,39.531,2.634,39.556,2.701,39.542,2.746,39.51,2.77,39.41,2.8,39.385,2.9,39.368,3.073,39.301,3.155,39.333,3.245,39.387,3.293,39.477,3.349,39.556,3.415,39.627,3.462,39.698,3.449,39.761,3.396,39.777,3.342,39.787,3.241,39.757]},{"b":[-123.346,-74.403,-120.272,-73.673],"l":[-120.722,-73.752,-121.497,-73.733,-121.967,-73.712,-122.436,-73.682,-122.91,-73.68,-123.012,-73.673,-123.112,-73.682,-123.249,-73.739,-123.292,-73.803,-123.346,-73.843,-123.191,-73.849,-123.035,-73.838,-122.992,-73.844,-122.951,-73.867,-122.625,-73.966,-122.71,-73.994,-122.881,-74.099,-122.875,-74.141,-122.794,-74.19,-122.765,-74.219,-122.891,-74.227,-122.956,-74.24,-122.938,-74.302,-122.859,-74.343,-122.287,-74.403,-121.062,-74.337,-121.002,-74.326,-121.005,-74.293,-121.036,-74.279,-121.054,-74.26,-121.019,-74.173,-120.99,-74.157,-120.272,-73.989,-120.312,-73.922,-120.378,-73.856,-120.556,-73.756,-120.722,-73.752]},{"b":[-127.429,-74.256,-123.8,-73.279],"l":[-127.233,-73.586,-127.232,-73.713,-127.212,-73.724,-127.122,-73.734,-127.006,-73.726,-126.902,-73.677,-126.838,-73.657,-126.711,-73.654,-126.583,-73.67,-126.538,-73.68,-126.496,-73.7,-126.466,-73.746,-126.472,-73.812,-126.244,-73.891,-125.887,-73.955,-125.683,-74.035,-125.549,-74.063,-125.421,-74.07,-125.09,-74.182,-124.873,-74.208,-124.199,-74.226,-123.982,-74.256,-123.937,-74.256,-123.837,-74.226,-123.839,-74.168,-123.811,-74.117,-123.8,-74.076,-123.851,-74.057,-123.932,-74.008,-124.129,-73.971,-124.152,-73.944,-124.101,-73.907,-124.042,-73.88,-124.129,-73.834,-124.54,-73.74,-124.617,-73.735,-124.694,-73.75,-124.993,-73.83,-125.109,-73.826,-125.224,-73.801,-125.327,-73.796,-125.552,-73.82,-125.674,-73.822,-125.799,-73.802,-125.857,-73.78,-125.86,-73.749,-125.828,-73.718,-125.723,-73.703,-125.612,-73.711,-125.276,-73.691,-125.264,-73.666,-125.326,-73.618,-125.504,-73.562,-125.561,-73.536,-125.627,-73.453,-125.736,-73.406,-125.857,-73.388,-125.976,-73.357,-126.065,-73.315,-126.33,-73.286,-126.597,-73.279,-126.83,-73.291,-126.978,-73.308,-127.124,-73.294,-127.268,-73.304,-127.394,-73.382,-127.429,-73.447,-127.414,-73.516,-127.332,-73.567,-127.233,-73.586]},{"b":[-67.809,-80.081,-65.504,-79.528],"l":[-66.785,-79.608,-66.881,-79.582,-66.979,-79.569,-67.438,-79.56,-67.688,-79.528,-67.809,-79.546,-67.771,-79.589,-67.719,-79.62,-67.077,-79.762,-66.994,-79.793,-66.962,-79.873,-66.904,-79.909,-66.41,-79.973,-66.367,-80.054,-66.319,-80.075,-66.267,-80.081,-66.174,-80.078,-65.989,-80.054,-65.899,-80.041,-65.504,-79.954,-65.54,-79.837,-65.579,-79.771,-65.87,-79.738,-66.014,-79.624,-66.274,-79.612,-66.785,-79.608]},{"b":[-58.438,-64.41,-57.222,-63.804],"l":[-57.295,-64.367,-57.338,-64.318,-57.414,-64.296,-57.328,-64.238,-57.222,-64.221,-57.273,-64.166,-57.249,-64.097,-57.517,-64.011,-57.48,-63.962,-57.593,-63.967,-57.71,-64.015,-57.741,-64.048,-57.774,-64.062,-57.809,-64.068,-57.846,-64.054,-57.827,-63.949,-57.781,-63.907,-57.779,-63.868,-57.831,-63.804,-57.926,-63.806,-57.971,-63.835,-58.07,-63.847,-58.146,-63.878,-58.275,-63.916,-58.342,-63.994,-58.425,-64.068,-58.438,-64.113,-58.398,-64.135,-58.352,-64.131,-58.25,-64.107,-58.147,-64.097,-58.162,-64.161,-58.138,-64.206,-58.02,-64.242,-58.304,-64.315,-58.214,-64.37,-58.169,-64.369,-58.022,-64.322,-57.971,-64.32,-57.921,-64.331,-57.952,-64.394,-57.91,-64.41,-57.871,-64.401,-57.823,-64.302,-57.703,-64.293,-57.671,-64.311,-57.683,-64.357,-57.581,-64.35,-57.388,-64.379,-57.295,-64.367]},{"b":[-64.272,-64.834,-62.837,-64.261],"l":[-63.131,-64.572,-63.277,-64.573,-63.181,-64.47,-63.271,-64.381,-63.23,-64.324,-63.334,-64.266,-63.486,-64.261,-63.534,-64.273,-63.606,-64.314,-63.683,-64.343,-63.668,-64.384,-63.674,-64.421,-63.916,-64.457,-63.897,-64.487,-63.867,-64.51,-64.171,-64.582,-64.226,-64.635,-64.272,-64.698,-64.184,-64.71,-64.099,-64.733,-64.007,-64.769,-63.804,-64.792,-63.77,-64.808,-63.74,-64.834,-63.647,-64.803,-63.558,-64.734,-63.458,-64.727,-63.355,-64.734,-63.275,-64.717,-63.203,-64.68,-63.026,-64.611,-62.837,-64.572,-62.928,-64.519,-63.032,-64.535,-63.131,-64.572]},{"b":[-76.512,-71.133,-73.55,-70.553],"l":[-74.205,-70.924,-73.695,-70.794,-73.55,-70.723,-73.707,-70.635,-73.879,-70.578,-73.958,-70.561,-74.038,-70.553,-74.113,-70.577,-74.115,-70.655,-74.225,-70.615,-74.401,-70.576,-74.456,-70.587,-74.469,-70.727,-74.527,-70.77,-74.59,-70.792,-74.79,-70.631,-74.898,-70.591,-74.954,-70.59,-75.007,-70.609,-75.038,-70.651,-75.06,-70.706,-75.127,-70.752,-75.21,-70.773,-76.035,-70.836,-76.249,-70.864,-76.378,-70.894,-76.5,-70.941,-76.512,-70.991,-76.421,-71.09,-76.364,-71.117,-76.271,-71.133,-76.176,-71.132,-74.806,-71.012,-74.505,-70.973,-74.205,-70.924]},{"b":[-75.813,-70.179,-74.438,-69.728],"l":[-74.849,-70.179,-74.672,-70.132,-74.578,-69.998,-74.46,-69.972,-74.438,-69.95,-74.465,-69.917,-74.55,-69.861,-74.81,-69.752,-74.987,-69.728,-75.136,-69.741,-75.179,-69.735,-75.265,-69.749,-75.314,-69.817,-75.34,-69.84,-75.681,-69.882,-75.76,-69.916,-75.813,-69.984,-75.804,-70.038,-75.764,-70.085,-75.727,-70.096,-75.268,-70.149,-74.849,-70.179]},{"b":[-102.314,-72.578,-95.531,-71.764],"l":[-99.735,-72.033,-99.784,-72.044,-99.833,-72.046,-99.985,-71.939,-100.085,-71.837,-100.219,-71.833,-100.401,-71.866,-102.128,-71.985,-102.237,-72.009,-102.288,-72.032,-102.314,-72.081,-102.265,-72.135,-102.022,-72.185,-101.903,-72.19,-101.785,-72.178,-101.602,-72.176,-100.357,-72.278,-100.195,-72.273,-100.104,-72.287,-100.014,-72.312,-99.672,-72.38,-99.434,-72.407,-99.149,-72.472,-98.882,-72.473,-98.641,-72.49,-98.408,-72.548,-98.163,-72.556,-97.828,-72.557,-97.596,-72.548,-97.366,-72.522,-97.25,-72.521,-97.028,-72.574,-96.915,-72.578,-96.804,-72.558,-96.693,-72.548,-96.052,-72.577,-96.03,-72.554,-96.014,-72.525,-96.078,-72.454,-95.826,-72.439,-95.575,-72.41,-95.531,-72.249,-95.61,-72.175,-95.609,-72.068,-95.685,-72.057,-95.906,-72.122,-96.482,-72.208,-96.718,-72.255,-96.799,-72.259,-96.89,-72.247,-96.979,-72.222,-96.715,-72.132,-96.298,-72.045,-96.125,-71.896,-96.383,-71.836,-96.869,-71.851,-97.089,-71.944,-97.155,-72.045,-97.196,-72.091,-97.242,-72.132,-97.345,-72.189,-97.46,-72.188,-97.526,-72.149,-97.582,-72.095,-97.473,-72,-97.585,-71.883,-97.816,-71.919,-97.923,-72.117,-98.168,-72.123,-98.176,-72.018,-98.091,-71.912,-98.189,-71.82,-98.394,-71.782,-98.615,-71.764,-98.965,-71.854,-99.082,-71.933,-99.254,-71.972,-99.563,-71.945,-99.735,-72.033]},{"b":[-76.096,-73.611,-74.224,-72.82],"l":[-75.377,-72.82,-75.731,-72.879,-75.702,-72.911,-75.6,-72.953,-75.439,-72.994,-75.244,-73.009,-75.276,-73.05,-75.417,-73.052,-75.468,-73.101,-75.506,-73.109,-75.775,-73.054,-75.898,-73.056,-76.018,-73.085,-76.062,-73.109,-76.096,-73.15,-76.09,-73.203,-76.053,-73.255,-76.003,-73.288,-75.901,-73.333,-74.575,-73.611,-74.452,-73.565,-74.366,-73.464,-74.467,-73.427,-74.551,-73.369,-74.575,-73.328,-74.615,-73.311,-74.668,-73.275,-74.522,-73.244,-74.499,-73.229,-74.354,-73.098,-74.224,-72.997,-74.276,-72.951,-74.336,-72.919,-74.474,-72.894,-75.377,-72.82]},{"b":[-69.138,-67.753,-67.681,-66.624],"l":[-68.901,-67.744,-68.818,-67.753,-68.734,-67.746,-68.664,-67.723,-68.622,-67.723,-68.58,-67.733,-68.507,-67.707,-68.439,-67.656,-68.381,-67.555,-68.325,-67.532,-68.25,-67.54,-68.175,-67.558,-68.093,-67.539,-67.988,-67.474,-67.969,-67.45,-68.007,-67.418,-68.144,-67.382,-68.235,-67.372,-68.175,-67.344,-68.03,-67.3,-67.956,-67.255,-67.848,-67.219,-67.688,-67.147,-67.761,-67.123,-67.828,-67.082,-67.876,-67.062,-67.949,-67.045,-67.968,-67.032,-67.969,-66.982,-67.932,-66.845,-67.741,-66.746,-67.681,-66.709,-67.711,-66.633,-67.831,-66.624,-67.938,-66.657,-68.336,-66.802,-68.417,-66.853,-68.575,-66.993,-68.656,-67.07,-68.734,-67.157,-68.82,-67.234,-69.082,-67.403,-69.133,-67.453,-69.138,-67.515,-69.12,-67.578,-69.098,-67.603,-68.982,-67.68,-68.901,-67.744]},{"b":[-180,70.919,-177.498,71.596],"l":[-180,71.538,-179.845,71.551,-179.691,71.578,-179.546,71.582,-179.402,71.567,-179.256,71.572,-179.112,71.596,-178.994,71.593,-178.876,71.577,-178.439,71.541,-178.354,71.529,-178.215,71.482,-178.134,71.465,-178.057,71.438,-177.975,71.391,-177.817,71.34,-177.584,71.282,-177.532,71.263,-177.498,71.219,-177.524,71.167,-177.822,71.068,-178.063,71.042,-178.528,71.015,-179.157,70.94,-179.416,70.919,-179.507,70.923,-179.734,70.972,-180,70.993]},{"b":[-59.649,-52.308,-57.792,-51.27],"l":[-57.792,-51.636,-57.866,-51.605,-57.96,-51.583,-57.915,-51.534,-57.808,-51.518,-57.923,-51.404,-57.977,-51.384,-58.206,-51.405,-58.259,-51.417,-58.289,-51.458,-58.276,-51.506,-58.241,-51.551,-58.235,-51.579,-58.272,-51.575,-58.474,-51.509,-58.509,-51.484,-58.519,-51.424,-58.467,-51.412,-58.407,-51.418,-58.379,-51.373,-58.426,-51.324,-58.506,-51.308,-58.698,-51.329,-58.85,-51.27,-58.917,-51.272,-58.887,-51.358,-59.097,-51.491,-59.099,-51.59,-59.065,-51.65,-59.06,-51.685,-59.095,-51.704,-59.18,-51.712,-59.262,-51.737,-59.309,-51.78,-59.571,-51.925,-59.537,-51.971,-59.649,-52.077,-59.649,-52.134,-59.532,-52.236,-59.396,-52.308,-59.342,-52.196,-59.256,-52.183,-59.163,-52.202,-59.068,-52.173,-59.196,-52.018,-59.131,-52.008,-58.653,-52.099,-58.638,-52.023,-58.643,-51.995,-58.683,-51.936,-58.336,-51.864,-58.218,-51.822,-58.151,-51.765,-58.004,-51.743,-57.838,-51.709,-57.831,-51.685,-57.792,-51.636]},{"b":[-38.017,-54.867,-35.799,-53.984],"l":[-35.895,-54.555,-35.965,-54.568,-36.033,-54.568,-36.073,-54.554,-36.117,-54.458,-36.173,-54.382,-36.236,-54.36,-36.285,-54.289,-36.326,-54.251,-36.386,-54.279,-36.407,-54.303,-36.449,-54.308,-36.541,-54.248,-36.647,-54.262,-36.607,-54.19,-36.704,-54.108,-36.76,-54.108,-36.805,-54.101,-36.849,-54.085,-36.929,-54.081,-37.006,-54.114,-37.103,-54.066,-37.233,-54.061,-37.369,-54.009,-37.382,-53.984,-37.536,-53.994,-37.946,-53.996,-38.017,-54.008,-37.913,-54.029,-37.619,-54.042,-37.689,-54.077,-37.692,-54.135,-37.631,-54.167,-37.498,-54.156,-37.158,-54.271,-37.083,-54.312,-37.007,-54.341,-36.886,-54.339,-36.852,-54.366,-36.824,-54.404,-36.735,-54.467,-36.628,-54.496,-36.507,-54.511,-36.472,-54.534,-36.446,-54.571,-36.311,-54.694,-36.252,-54.78,-36.124,-54.853,-36.085,-54.867,-35.939,-54.834,-35.867,-54.792,-35.799,-54.763,-35.913,-54.711,-35.922,-54.637,-35.895,-54.555]},{"b":[-74.387,-43.372,-73.423,-41.796],"l":[-74.114,-43.358,-73.99,-43.357,-73.919,-43.372,-73.849,-43.367,-73.773,-43.346,-73.738,-43.291,-73.75,-43.159,-73.65,-43.127,-73.541,-43.074,-73.473,-42.993,-73.436,-42.937,-73.511,-42.847,-73.568,-42.762,-73.673,-42.704,-73.767,-42.622,-73.789,-42.586,-73.715,-42.545,-73.653,-42.529,-73.634,-42.508,-73.549,-42.493,-73.471,-42.466,-73.525,-42.393,-73.533,-42.314,-73.439,-42.278,-73.423,-42.193,-73.454,-42.166,-73.478,-42.047,-73.517,-41.981,-73.528,-41.896,-73.731,-41.877,-74.037,-41.796,-74.063,-41.823,-74.031,-41.854,-74.019,-41.891,-74.057,-42.002,-74.059,-42.056,-74.072,-42.106,-74.16,-42.216,-74.17,-42.269,-74.164,-42.325,-74.174,-42.382,-74.194,-42.436,-74.199,-42.481,-74.156,-42.591,-74.209,-42.879,-74.289,-43.079,-74.373,-43.186,-74.387,-43.232,-74.355,-43.264,-74.239,-43.319,-74.114,-43.358]},{"b":[-60.961,-52.195,-59.268,-51.354],"l":[-59.573,-51.681,-59.514,-51.627,-59.437,-51.593,-59.392,-51.556,-59.354,-51.511,-59.294,-51.479,-59.268,-51.428,-59.321,-51.384,-59.388,-51.36,-59.465,-51.411,-59.493,-51.396,-59.711,-51.439,-59.788,-51.446,-59.842,-51.403,-59.917,-51.388,-60.009,-51.411,-60.142,-51.481,-60.286,-51.462,-60.445,-51.399,-60.516,-51.354,-60.568,-51.358,-60.518,-51.428,-60.523,-51.463,-60.506,-51.485,-60.415,-51.545,-60.303,-51.58,-60.245,-51.639,-60.281,-51.656,-60.467,-51.697,-60.528,-51.696,-60.583,-51.713,-60.5,-51.757,-60.38,-51.735,-60.328,-51.718,-60.277,-51.717,-60.238,-51.734,-60.238,-51.772,-60.289,-51.801,-60.334,-51.84,-60.45,-51.877,-60.591,-51.952,-60.762,-51.946,-60.961,-52.057,-60.812,-52.148,-60.686,-52.188,-60.508,-52.195,-60.484,-52.17,-60.452,-52.16,-60.384,-52.154,-60.353,-52.14,-60.288,-52.074,-60.246,-51.986,-60.194,-51.983,-60.132,-51.994,-59.99,-51.984,-59.921,-51.97,-59.715,-51.808,-59.573,-51.681]},{"b":[-75.57,-50.019,-74.459,-48.705],"l":[-75.032,-49.836,-74.994,-49.752,-74.991,-49.606,-74.981,-49.564,-74.96,-49.533,-74.744,-49.422,-74.724,-49.424,-74.719,-49.437,-74.727,-49.452,-74.781,-49.489,-74.805,-49.516,-74.812,-49.605,-74.859,-49.634,-74.882,-49.692,-74.88,-49.726,-74.822,-49.814,-74.825,-49.879,-74.811,-49.93,-74.763,-50.011,-74.703,-50.019,-74.595,-50.007,-74.57,-49.991,-74.543,-49.919,-74.496,-49.859,-74.472,-49.786,-74.459,-49.691,-74.471,-49.669,-74.516,-49.66,-74.522,-49.623,-74.484,-49.442,-74.467,-49.295,-74.476,-49.148,-74.531,-48.813,-74.546,-48.767,-74.567,-48.755,-74.652,-48.75,-74.747,-48.709,-74.793,-48.705,-74.896,-48.733,-74.97,-48.791,-74.981,-48.819,-74.945,-48.889,-74.949,-48.96,-75.037,-49.022,-75.184,-49.084,-75.21,-49.148,-75.094,-49.185,-75.086,-49.27,-75.217,-49.293,-75.27,-49.263,-75.327,-49.269,-75.433,-49.322,-75.467,-49.359,-75.429,-49.408,-75.364,-49.462,-75.306,-49.494,-75.337,-49.628,-75.521,-49.622,-75.57,-49.697,-75.55,-49.791,-75.451,-49.77,-75.3,-49.847,-75.167,-49.856,-75.066,-49.852,-75.032,-49.836]},{"b":[-73.445,-44.86,-72.764,-44.335],"l":[-73.397,-44.774,-73.35,-44.833,-73.228,-44.86,-72.986,-44.78,-72.897,-44.712,-72.845,-44.638,-72.764,-44.549,-72.776,-44.509,-72.842,-44.458,-73.028,-44.384,-73.208,-44.335,-73.26,-44.35,-73.272,-44.394,-73.266,-44.44,-73.282,-44.49,-73.315,-44.531,-73.404,-44.596,-73.445,-44.641,-73.42,-44.725,-73.397,-44.774]},{"b":[-64.485,49.079,-61.696,49.944],"l":[-63.885,49.658,-63.777,49.602,-63.676,49.534,-63.626,49.46,-63.566,49.399,-63.042,49.225,-62.8,49.171,-62.553,49.141,-62.22,49.079,-61.801,49.094,-61.746,49.106,-61.696,49.139,-61.736,49.204,-61.817,49.284,-62.043,49.39,-62.133,49.407,-62.633,49.624,-62.859,49.705,-63.089,49.773,-63.292,49.817,-63.76,49.875,-64.131,49.942,-64.244,49.944,-64.373,49.926,-64.485,49.887,-64.44,49.828,-63.885,49.658]},{"b":[-61.495,45.573,-59.828,47.01],"l":[-59.842,45.942,-59.828,45.965,-59.934,46.019,-59.881,46.062,-59.849,46.113,-59.85,46.141,-59.865,46.16,-59.961,46.191,-60.092,46.206,-60.226,46.196,-60.244,46.27,-60.298,46.311,-60.377,46.285,-60.431,46.256,-60.505,46.204,-60.586,46.117,-60.573,46.061,-60.733,45.957,-60.705,45.933,-60.461,45.969,-60.472,45.947,-60.699,45.773,-60.738,45.751,-60.806,45.738,-60.878,45.748,-60.93,45.748,-61.059,45.703,-61.092,45.748,-61.052,45.795,-60.972,45.838,-60.971,45.856,-61.038,45.882,-60.984,45.911,-60.868,45.949,-60.865,45.983,-60.937,45.986,-61.071,45.937,-61.105,45.945,-60.912,46.045,-60.831,46.074,-60.745,46.093,-60.577,46.172,-60.534,46.215,-60.495,46.27,-60.508,46.303,-60.482,46.414,-60.384,46.613,-60.333,46.737,-60.332,46.768,-60.425,46.923,-60.431,46.963,-60.408,47.004,-60.489,47.01,-60.571,46.999,-60.617,46.976,-60.76,46.863,-60.87,46.797,-60.932,46.729,-60.983,46.65,-61.241,46.303,-61.302,46.244,-61.409,46.17,-61.481,46.06,-61.495,45.941,-61.45,45.716,-61.408,45.669,-61.323,45.598,-61.284,45.574,-61.236,45.573,-61.186,45.585,-61.084,45.582,-60.979,45.606,-60.872,45.611,-60.764,45.591,-60.673,45.591,-60.386,45.655,-60.205,45.743,-60.114,45.819,-60.016,45.88,-59.842,45.942]},{"b":[-171.819,62.956,-168.716,63.727],"l":[-168.996,63.347,-169.221,63.349,-169.295,63.358,-169.428,63.348,-169.555,63.373,-169.587,63.407,-169.624,63.431,-169.777,63.448,-170.017,63.492,-170.056,63.527,-170.082,63.577,-170.122,63.618,-170.171,63.641,-170.299,63.681,-170.43,63.699,-170.552,63.688,-170.673,63.669,-170.875,63.594,-171.035,63.585,-171.197,63.609,-171.343,63.62,-171.448,63.616,-171.463,63.64,-171.646,63.727,-171.746,63.703,-171.804,63.581,-171.818,63.53,-171.819,63.477,-171.791,63.425,-171.738,63.394,-171.632,63.351,-171.519,63.332,-171.401,63.339,-171.287,63.372,-171.176,63.416,-171.061,63.446,-170.954,63.453,-170.848,63.444,-170.527,63.379,-170.424,63.349,-170.324,63.311,-170.273,63.284,-170.243,63.232,-170.19,63.196,-170.115,63.194,-169.988,63.173,-169.863,63.14,-169.819,63.122,-169.778,63.094,-169.72,62.99,-169.676,62.956,-169.623,62.969,-169.571,62.997,-169.559,63.058,-169.471,63.121,-169.365,63.171,-169.109,63.185,-168.852,63.171,-168.761,63.214,-168.716,63.311,-168.996,63.347]},{"b":[-167.436,59.764,-165.592,60.397],"l":[-165.631,60.028,-165.712,60.069,-165.707,60.101,-165.714,60.173,-165.689,60.224,-165.696,60.282,-165.73,60.314,-165.784,60.336,-165.841,60.346,-165.995,60.331,-166.044,60.334,-166.135,60.384,-166.185,60.397,-166.247,60.391,-166.364,60.365,-166.42,60.382,-166.476,60.383,-166.599,60.339,-166.731,60.316,-166.784,60.296,-166.836,60.217,-167.252,60.234,-167.344,60.224,-167.436,60.207,-167.295,60.096,-167.139,60.009,-166.985,59.984,-166.628,59.865,-166.343,59.834,-166.262,59.815,-166.188,59.774,-166.149,59.764,-166.107,59.775,-166.131,59.82,-166.1,59.85,-165.947,59.89,-165.769,59.893,-165.592,59.913,-165.605,59.973,-165.631,60.028]},{"b":[-164.904,54.404,-163.083,55.051],"l":[-163.275,54.766,-163.337,54.783,-163.379,54.816,-163.476,54.981,-163.511,55.014,-163.553,55.038,-163.607,55.051,-163.807,55.049,-163.868,55.039,-164.145,54.955,-164.274,54.9,-164.424,54.913,-164.479,54.907,-164.53,54.881,-164.706,54.692,-164.751,54.663,-164.888,54.608,-164.904,54.568,-164.904,54.545,-164.866,54.461,-164.823,54.419,-164.744,54.407,-164.591,54.404,-164.463,54.427,-164.404,54.448,-164.347,54.482,-164.235,54.571,-164.171,54.603,-164.073,54.621,-163.583,54.626,-163.531,54.638,-163.358,54.736,-163.083,54.669,-163.089,54.686,-163.135,54.723,-163.187,54.748,-163.275,54.766]},{"b":[-78.34,17.715,-76.211,18.522],"l":[-76.35,18.152,-76.701,18.257,-76.793,18.304,-76.908,18.39,-76.959,18.402,-77.014,18.403,-77.14,18.421,-77.261,18.457,-77.354,18.466,-77.452,18.467,-77.873,18.522,-77.927,18.501,-77.978,18.468,-78.095,18.445,-78.217,18.448,-78.252,18.426,-78.326,18.35,-78.34,18.287,-78.294,18.218,-78.074,18.191,-78.044,18.174,-77.963,18.048,-77.881,18.019,-77.849,17.988,-77.768,17.877,-77.671,17.86,-77.464,17.856,-77.361,17.834,-77.28,17.78,-77.205,17.715,-77.158,17.845,-77.119,17.88,-77.071,17.901,-77.036,17.854,-76.944,17.849,-76.896,17.904,-76.853,17.974,-76.795,17.976,-76.748,17.965,-76.774,17.94,-76.669,17.928,-76.625,17.901,-76.525,17.866,-76.416,17.868,-76.301,17.88,-76.211,17.914,-76.233,17.97,-76.35,18.152]},{"b":[-67.264,17.947,-65.621,18.522],"l":[-65.629,18.381,-65.756,18.402,-65.879,18.444,-66.07,18.469,-66.093,18.469,-66.068,18.428,-66.098,18.425,-66.129,18.445,-66.153,18.471,-66.189,18.476,-66.813,18.493,-67.06,18.522,-67.113,18.515,-67.159,18.499,-67.172,18.436,-67.213,18.394,-67.264,18.365,-67.239,18.321,-67.204,18.283,-67.172,18.224,-67.174,18.153,-67.197,17.994,-67.142,17.967,-67.013,17.968,-66.961,17.954,-66.9,17.948,-66.838,17.955,-66.772,17.987,-66.598,17.978,-66.511,17.987,-66.409,17.951,-66.326,17.964,-66.286,17.95,-66.245,17.947,-66.135,17.949,-65.971,17.974,-65.834,18.057,-65.782,18.129,-65.718,18.187,-65.621,18.242,-65.629,18.381]},{"b":[-154.712,56.774,-152.215,57.972],"l":[-152.411,57.646,-152.483,57.703,-152.485,57.734,-152.419,57.782,-152.412,57.806,-152.429,57.826,-152.512,57.851,-152.616,57.849,-152.696,57.832,-152.85,57.776,-152.891,57.769,-152.898,57.824,-152.85,57.897,-152.943,57.936,-153.16,57.972,-153.226,57.958,-153.169,57.911,-153.175,57.879,-153.201,57.863,-153.2,57.82,-153.217,57.796,-153.252,57.79,-153.357,57.805,-153.39,57.798,-153.423,57.779,-153.454,57.747,-153.488,57.731,-153.524,57.731,-153.569,57.761,-153.663,57.858,-153.696,57.871,-153.769,57.88,-153.806,57.875,-153.842,57.863,-153.904,57.82,-153.906,57.791,-153.879,57.757,-153.808,57.715,-153.693,57.663,-153.69,57.641,-153.799,57.647,-153.838,57.636,-153.818,57.596,-153.798,57.443,-153.757,57.367,-153.688,57.305,-153.755,57.325,-153.805,57.358,-153.882,57.439,-153.947,57.53,-154.008,57.556,-154.016,57.567,-153.995,57.587,-154.03,57.631,-154.116,57.651,-154.179,57.652,-154.281,57.638,-154.387,57.59,-154.535,57.559,-154.673,57.446,-154.712,57.366,-154.706,57.335,-154.569,57.206,-154.499,57.037,-154.339,56.921,-154.261,56.912,-154.208,56.964,-154.184,57.005,-154.191,57.036,-154.209,57.063,-154.239,57.087,-154.27,57.099,-154.381,57.097,-154.377,57.107,-154.324,57.132,-154.244,57.143,-154.135,57.141,-154.065,57.134,-154.035,57.122,-154.025,57.108,-154.08,57.061,-154.103,57.021,-154.084,57.02,-153.999,57.05,-153.88,57.004,-153.804,56.998,-153.793,56.99,-154.071,56.821,-154.07,56.805,-154.051,56.788,-154.027,56.778,-153.973,56.774,-153.757,56.858,-153.643,56.961,-153.631,56.984,-153.633,57.01,-153.647,57.03,-153.733,57.052,-153.588,57.078,-153.524,57.103,-153.504,57.138,-153.444,57.167,-153.274,57.226,-153.052,57.238,-152.99,57.282,-152.879,57.321,-152.789,57.321,-152.714,57.331,-152.679,57.345,-152.693,57.38,-152.72,57.411,-152.781,57.453,-152.957,57.46,-152.997,57.469,-152.941,57.498,-152.912,57.508,-152.831,57.503,-152.631,57.472,-152.412,57.455,-152.381,57.46,-152.337,57.482,-152.216,57.577,-152.215,57.598,-152.237,57.615,-152.411,57.646]},{"b":[-153.381,57.994,-151.974,58.451],"l":[-152.165,58.178,-152.109,58.161,-152.069,58.178,-151.983,58.244,-151.974,58.31,-151.998,58.314,-152.037,58.307,-152.079,58.312,-152.125,58.374,-152.198,58.363,-152.316,58.413,-152.343,58.412,-152.381,58.352,-152.417,58.36,-152.478,58.4,-152.544,58.428,-152.612,58.446,-152.675,58.451,-152.841,58.416,-152.844,58.396,-152.769,58.346,-152.772,58.279,-152.815,58.276,-152.895,58.294,-152.976,58.297,-153.116,58.239,-153.381,58.087,-153.305,58.063,-152.983,57.997,-152.928,57.994,-152.841,58.014,-152.782,58.016,-152.764,58.031,-152.683,58.063,-152.639,58.102,-152.598,58.163,-152.571,58.168,-152.558,58.119,-152.538,58.101,-152.452,58.129,-152.381,58.124,-152.309,58.134,-152.305,58.154,-152.333,58.187,-152.334,58.208,-152.268,58.252,-152.252,58.251,-152.224,58.214,-152.187,58.185,-152.165,58.178]},{"b":[-131.846,55.166,-130.965,55.955],"l":[-130.998,55.728,-131.121,55.857,-131.236,55.949,-131.269,55.955,-131.625,55.832,-131.648,55.586,-131.759,55.503,-131.846,55.416,-131.842,55.359,-131.811,55.223,-131.762,55.166,-131.724,55.218,-131.641,55.299,-131.522,55.341,-131.475,55.373,-131.448,55.409,-131.422,55.368,-131.451,55.316,-131.421,55.276,-131.367,55.266,-131.316,55.269,-131.262,55.22,-131.188,55.206,-131.083,55.267,-131.014,55.379,-130.979,55.489,-130.965,55.568,-130.966,55.67,-130.998,55.728]},{"b":[-133.755,54.713,-131.976,56.339],"l":[-132.469,54.938,-132.37,54.922,-132.341,54.907,-132.266,54.802,-132.189,54.735,-132.134,54.713,-132.065,54.713,-132.022,54.726,-131.981,54.805,-131.983,54.835,-131.997,54.869,-131.997,54.901,-131.978,54.94,-131.978,54.969,-132,55.034,-131.976,55.209,-132.005,55.231,-132.166,55.218,-132.207,55.224,-132.215,55.237,-132.19,55.255,-132.158,55.3,-132.16,55.323,-132.215,55.384,-132.272,55.399,-132.418,55.483,-132.592,55.464,-132.631,55.473,-132.582,55.503,-132.548,55.544,-132.529,55.59,-132.511,55.594,-132.296,55.507,-132.196,55.479,-132.173,55.481,-132.215,55.519,-132.289,55.558,-132.43,55.687,-132.497,55.798,-132.534,55.842,-132.598,55.895,-132.758,55.995,-133.097,56.09,-133.101,56.1,-133.08,56.129,-133.075,56.156,-133.082,56.194,-133.104,56.235,-133.144,56.279,-133.203,56.32,-133.377,56.318,-133.566,56.339,-133.599,56.316,-133.594,56.216,-133.544,56.177,-133.531,56.146,-133.599,56.094,-133.755,55.999,-133.743,55.965,-133.684,55.943,-133.539,55.999,-133.371,56.036,-133.289,56.019,-133.252,55.957,-133.242,55.921,-133.308,55.886,-133.322,55.845,-133.412,55.798,-133.447,55.797,-133.537,55.832,-133.584,55.837,-133.664,55.804,-133.68,55.785,-133.64,55.749,-133.553,55.691,-133.503,55.696,-133.369,55.689,-133.343,55.651,-133.298,55.607,-133.244,55.595,-133.09,55.613,-133.033,55.59,-133.078,55.535,-133.082,55.504,-132.959,55.396,-132.971,55.376,-133.03,55.378,-133.103,55.36,-133.119,55.328,-133.061,55.301,-132.913,55.188,-132.782,55.048,-132.704,55.03,-132.683,55.074,-132.702,55.131,-132.665,55.147,-132.622,55.136,-132.627,55.11,-132.588,55.052,-132.594,54.996,-132.549,54.953,-132.486,54.95,-132.469,54.938]},{"b":[-133.979,56.452,-132.951,57.069],"l":[-133.979,57.01,-133.917,56.967,-133.823,56.924,-133.758,56.877,-133.681,56.798,-133.688,56.71,-133.658,56.596,-133.649,56.517,-133.631,56.484,-133.603,56.464,-133.484,56.452,-133.383,56.474,-133.213,56.465,-133.181,56.474,-133.158,56.495,-133.145,56.528,-133.144,56.567,-133.157,56.611,-133.178,56.645,-133.227,56.689,-133.24,56.726,-133.309,56.786,-133.332,56.819,-133.329,56.83,-133.244,56.796,-133.132,56.683,-133.035,56.621,-133.004,56.624,-132.976,56.647,-132.959,56.677,-132.954,56.713,-132.963,56.783,-132.951,56.85,-132.954,56.88,-132.996,56.93,-133.071,56.974,-133.196,57.003,-133.264,57.005,-133.3,56.972,-133.366,57.004,-133.708,57.063,-133.866,57.069,-133.962,57.043,-133.979,57.01]},{"b":[-135.823,56.216,-134.611,57.534],"l":[-134.885,57.242,-134.97,57.351,-135.065,57.417,-135.131,57.432,-135.346,57.533,-135.449,57.534,-135.487,57.517,-135.57,57.425,-135.581,57.39,-135.625,57.354,-135.681,57.333,-135.787,57.317,-135.823,57.28,-135.821,57.23,-135.768,57.1,-135.782,57.058,-135.812,57.01,-135.662,57.034,-135.609,57.071,-135.502,57.244,-135.455,57.249,-135.375,57.188,-135.341,57.082,-135.267,57.049,-135.211,57.045,-135.2,57.027,-135.315,56.932,-135.338,56.894,-135.341,56.851,-135.331,56.822,-135.285,56.8,-135.163,56.824,-135.147,56.802,-135.159,56.725,-135.097,56.703,-135.018,56.66,-134.928,56.667,-134.883,56.679,-134.875,56.67,-134.933,56.616,-134.967,56.596,-134.982,56.564,-134.981,56.519,-134.95,56.457,-134.848,56.323,-134.806,56.281,-134.75,56.241,-134.682,56.216,-134.654,56.227,-134.63,56.302,-134.632,56.436,-134.657,56.523,-134.652,56.556,-134.624,56.579,-134.611,56.603,-134.621,56.718,-134.634,56.762,-134.677,56.842,-134.769,57.054,-134.823,57.157,-134.885,57.242]},{"b":[-134.933,57.043,-133.823,58.355],"l":[-134.1,57.3,-133.925,57.337,-133.911,57.353,-133.909,57.369,-133.974,57.451,-133.921,57.492,-133.937,57.582,-133.961,57.614,-134.084,57.712,-134.267,57.885,-134.3,57.963,-134.307,58.034,-134.292,58.045,-134.25,58.049,-134.213,58.038,-134.18,58.011,-134.178,57.982,-134.105,57.879,-134.067,57.84,-134.032,57.821,-133.996,57.778,-133.925,57.671,-133.827,57.618,-133.823,57.629,-133.869,57.708,-133.904,57.789,-133.966,57.874,-134.07,57.995,-134.24,58.144,-134.426,58.139,-134.68,58.162,-134.733,58.225,-134.837,58.32,-134.923,58.355,-134.933,58.329,-134.908,58.263,-134.87,58.202,-134.82,58.147,-134.781,58.078,-134.754,57.995,-134.695,57.736,-134.66,57.638,-134.595,57.568,-134.487,57.482,-134.489,57.42,-134.576,57.232,-134.62,57.196,-134.613,57.138,-134.592,57.092,-134.555,57.058,-134.516,57.043,-134.435,57.057,-134.26,57.147,-134.1,57.3]},{"b":[-136.569,57.42,-134.873,58.269],"l":[-135.692,57.42,-135.618,57.48,-135.621,57.597,-135.609,57.651,-135.564,57.666,-135.498,57.662,-135.22,57.574,-135.085,57.511,-134.931,57.481,-134.873,57.589,-134.897,57.648,-134.979,57.724,-135.25,57.733,-135.338,57.769,-135.231,57.816,-135.165,57.796,-135.103,57.794,-134.971,57.817,-134.928,57.953,-134.955,58.015,-135.002,58.051,-135.163,58.096,-135.347,58.124,-135.375,58.122,-135.421,58.102,-135.572,58.009,-135.613,57.992,-135.671,58.012,-135.693,58.039,-135.615,58.057,-135.586,58.124,-135.587,58.147,-135.73,58.244,-135.787,58.269,-135.882,58.247,-135.947,58.206,-135.994,58.197,-136.094,58.198,-136.142,58.154,-136.144,58.098,-136.246,58.157,-136.322,58.219,-136.37,58.143,-136.454,58.108,-136.512,58.096,-136.525,58.051,-136.569,57.972,-136.46,57.873,-136.378,57.84,-136.077,57.675,-135.997,57.535,-135.911,57.447,-135.692,57.42]},{"b":[-129.184,52.29,-128.507,53.28],"l":[-128.766,52.598,-128.749,52.556,-128.736,52.468,-128.731,52.357,-128.679,52.29,-128.624,52.34,-128.577,52.452,-128.51,52.519,-128.507,52.621,-128.552,52.94,-128.633,53.113,-128.74,53.179,-128.858,53.229,-128.97,53.274,-129.033,53.28,-129.06,53.241,-129.084,53.14,-129.111,53.091,-129.178,53.018,-129.184,52.991,-129.176,52.965,-129.095,52.892,-129.085,52.822,-129.023,52.756,-128.9,52.674,-128.831,52.679,-128.77,52.751,-128.746,52.763,-128.766,52.598]},{"b":[-133.098,53.189,-131.668,54.159],"l":[-132.347,53.189,-132.011,53.265,-131.957,53.309,-131.928,53.379,-131.922,53.588,-131.889,53.714,-131.821,53.842,-131.703,53.986,-131.685,54.023,-131.668,54.141,-131.696,54.143,-131.82,54.077,-131.941,54.042,-132.134,54.034,-132.136,53.996,-132.111,53.9,-132.114,53.86,-132.152,53.807,-132.172,53.707,-132.187,53.685,-132.464,53.653,-132.535,53.652,-132.567,53.664,-132.574,53.675,-132.565,53.688,-132.215,53.815,-132.175,53.847,-132.155,53.875,-132.166,53.955,-132.216,54.028,-132.262,54.076,-132.303,54.099,-132.344,54.106,-132.564,54.069,-132.656,54.127,-132.893,54.141,-132.991,54.158,-133.048,54.159,-133.064,54.144,-133.098,54.006,-133.098,53.92,-133.079,53.837,-133.052,53.778,-132.913,53.629,-132.9,53.605,-132.898,53.563,-132.845,53.508,-132.67,53.459,-132.431,53.35,-132.425,53.337,-132.462,53.338,-132.546,53.359,-132.655,53.371,-132.693,53.368,-132.748,53.31,-132.675,53.263,-132.52,53.194,-132.347,53.189]},{"b":[-132.547,52.154,-131.116,53.231],"l":[-131.116,52.219,-131.143,52.291,-131.259,52.292,-131.32,52.303,-131.327,52.318,-131.26,52.39,-131.26,52.416,-131.274,52.426,-131.383,52.416,-131.43,52.422,-131.444,52.453,-131.591,52.578,-131.573,52.623,-131.455,52.702,-131.611,52.745,-131.727,52.756,-131.81,52.819,-131.904,52.867,-131.972,52.88,-131.916,52.909,-131.88,52.915,-131.795,52.885,-131.635,52.922,-131.622,53.02,-131.652,53.103,-131.754,53.196,-131.853,53.23,-131.893,53.231,-131.99,53.202,-132.036,53.179,-132.154,53.16,-132.345,53.136,-132.524,53.145,-132.547,53.138,-132.505,53.087,-132.469,53.072,-132.144,52.999,-132.145,52.957,-132.23,52.948,-132.258,52.934,-132.26,52.907,-132.239,52.867,-132.165,52.783,-132.092,52.753,-131.81,52.542,-131.624,52.444,-131.562,52.4,-131.511,52.322,-131.422,52.238,-131.222,52.154,-131.116,52.219]},{"b":[-64.403,45.967,-62.024,47.062],"l":[-62.164,46.487,-62.423,46.478,-62.682,46.459,-62.712,46.45,-62.964,46.428,-63.129,46.422,-63.286,46.46,-63.369,46.508,-63.413,46.512,-63.456,46.504,-63.534,46.541,-63.681,46.562,-63.737,46.481,-63.784,46.455,-63.811,46.469,-63.834,46.494,-63.906,46.509,-63.876,46.539,-63.864,46.572,-63.879,46.609,-63.903,46.639,-64.088,46.775,-63.981,46.913,-63.997,46.982,-63.994,47.062,-64.157,46.955,-64.223,46.901,-64.28,46.836,-64.355,46.769,-64.403,46.692,-64.388,46.641,-64.236,46.631,-64.136,46.6,-64.107,46.562,-64.111,46.425,-64.02,46.405,-63.861,46.408,-63.759,46.398,-63.751,46.384,-63.763,46.37,-63.801,46.367,-63.732,46.289,-63.641,46.23,-63.569,46.209,-63.277,46.153,-63.213,46.16,-63.153,46.188,-63.271,46.2,-63.195,46.237,-63.117,46.253,-63.057,46.295,-62.978,46.316,-62.995,46.292,-63.053,46.27,-63.056,46.224,-63.015,46.19,-62.953,46.195,-62.895,46.124,-63.022,46.067,-62.995,46.058,-62.904,46.068,-62.878,46.001,-62.805,45.973,-62.743,45.967,-62.531,45.977,-62.478,46,-62.503,46.023,-62.543,46.029,-62.539,46.098,-62.552,46.166,-62.526,46.203,-62.32,46.278,-62.172,46.355,-62.024,46.422,-62.041,46.446,-62.074,46.466,-62.164,46.487]},{"b":[-74.032,40.571,-71.903,41.153],"l":[-72.102,41.015,-72.151,41.051,-72.184,41.047,-72.287,41.024,-72.409,40.972,-72.461,40.934,-72.517,40.915,-72.581,40.921,-72.51,40.986,-72.427,41.039,-72.274,41.153,-72.373,41.126,-72.544,41.027,-72.625,40.992,-72.829,40.972,-73.034,40.966,-73.111,40.957,-73.186,40.93,-73.278,40.924,-73.373,40.944,-73.407,40.941,-73.441,40.927,-73.487,40.92,-73.574,40.92,-73.61,40.906,-73.643,40.881,-73.652,40.838,-73.695,40.87,-73.757,40.834,-73.879,40.792,-73.965,40.725,-74.003,40.683,-74.032,40.639,-74.015,40.581,-73.929,40.599,-73.875,40.652,-73.823,40.656,-73.799,40.641,-73.801,40.622,-73.9,40.571,-73.767,40.593,-73.621,40.6,-73.266,40.664,-73.229,40.652,-73.194,40.654,-72.763,40.778,-72.676,40.791,-72.556,40.826,-72.428,40.875,-72.339,40.894,-71.903,41.061,-72.004,41.044,-72.102,41.015]},{"b":[148.337,-6.301,152.406,-4.2],"l":[152.197,-4.285,152.117,-4.212,151.968,-4.317,151.916,-4.297,151.865,-4.261,151.819,-4.217,151.704,-4.2,151.593,-4.201,151.561,-4.247,151.544,-4.299,151.552,-4.346,151.665,-4.637,151.679,-4.761,151.671,-4.883,151.573,-4.937,151.44,-4.931,151.381,-4.941,151.327,-4.96,151.138,-5.113,151.069,-5.204,151.022,-5.321,150.953,-5.424,150.9,-5.447,150.843,-5.454,150.784,-5.471,150.734,-5.51,150.626,-5.521,150.519,-5.46,150.404,-5.473,150.299,-5.536,150.183,-5.524,150.106,-5.429,150.072,-5.31,150.082,-5.186,150.109,-5.136,150.17,-5.071,150.122,-5.018,150.09,-5.012,150.045,-5.035,150.012,-5.14,149.963,-5.448,149.831,-5.524,149.681,-5.524,149.632,-5.516,149.475,-5.573,149.359,-5.584,149.245,-5.573,149.124,-5.523,148.999,-5.485,148.783,-5.512,148.724,-5.493,148.666,-5.487,148.616,-5.507,148.565,-5.508,148.432,-5.472,148.345,-5.545,148.337,-5.669,148.401,-5.765,148.51,-5.805,148.625,-5.831,148.719,-5.867,148.808,-5.916,149.099,-6.117,149.127,-6.128,149.273,-6.079,149.382,-6.078,149.483,-6.125,149.598,-6.261,149.653,-6.29,149.75,-6.301,149.851,-6.293,150.191,-6.289,150.428,-6.276,150.474,-6.263,150.588,-6.188,150.706,-6.149,150.76,-6.114,150.809,-6.071,150.92,-6.027,151.043,-6.015,151.09,-5.997,151.229,-5.92,151.331,-5.839,151.422,-5.747,151.455,-5.703,151.48,-5.655,151.481,-5.591,151.515,-5.552,151.695,-5.544,151.865,-5.565,151.968,-5.529,152.077,-5.458,152.143,-5.357,152.077,-5.247,151.994,-5.149,151.984,-5.074,152.013,-5.004,152.167,-4.993,152.216,-4.979,152.258,-4.955,152.351,-4.822,152.4,-4.731,152.404,-4.629,152.376,-4.56,152.364,-4.491,152.406,-4.341,152.299,-4.321,152.197,-4.285]},{"b":[123.589,-10.347,127.296,-8.316],"l":[124.198,-9.256,123.977,-9.373,123.877,-9.453,123.709,-9.615,123.666,-9.705,123.636,-9.838,123.589,-9.967,123.599,-10.015,123.716,-10.079,123.69,-10.129,123.648,-10.168,123.614,-10.215,123.605,-10.27,123.644,-10.311,123.747,-10.347,123.858,-10.344,123.971,-10.295,124.176,-10.183,124.327,-10.17,124.428,-10.149,124.508,-10.086,124.602,-9.993,124.708,-9.914,124.842,-9.76,124.963,-9.666,124.998,-9.565,125.21,-9.404,125.408,-9.276,125.735,-9.161,125.84,-9.13,125.895,-9.132,125.946,-9.124,126.073,-9.044,126.164,-8.997,126.265,-8.973,126.383,-8.958,126.487,-8.913,126.569,-8.833,126.665,-8.782,126.792,-8.755,126.915,-8.715,127.115,-8.584,127.296,-8.425,127.257,-8.395,127.215,-8.373,127.058,-8.348,126.966,-8.316,126.905,-8.342,126.846,-8.377,126.735,-8.423,126.62,-8.459,126.531,-8.471,126.173,-8.489,125.905,-8.487,125.804,-8.492,125.382,-8.575,125.323,-8.591,125.178,-8.648,125.116,-8.708,125.027,-8.859,124.89,-8.968,124.708,-9.062,124.646,-9.117,124.575,-9.155,124.198,-9.256]},{"b":[144.646,-43.619,148.343,-40.672],"l":[147.786,-43.22,147.699,-43.123,147.648,-43.021,147.687,-42.98,147.774,-43.003,147.8,-42.98,147.807,-42.954,147.8,-42.928,147.693,-42.872,147.574,-42.846,147.536,-42.878,147.549,-42.975,147.537,-42.996,147.452,-43.033,147.408,-42.894,147.298,-42.791,147.302,-42.841,147.348,-42.927,147.343,-42.964,147.325,-43.013,147.281,-43.032,147.26,-43.071,147.26,-43.126,147.245,-43.216,147.173,-43.256,146.997,-43.156,146.985,-43.19,146.988,-43.219,147.077,-43.276,147.036,-43.319,147.005,-43.37,146.955,-43.502,146.874,-43.612,146.834,-43.619,146.699,-43.602,146.549,-43.509,146.413,-43.52,146.187,-43.513,146.043,-43.547,146.013,-43.445,145.982,-43.408,145.994,-43.376,146.109,-43.354,146.226,-43.355,146.208,-43.316,146.176,-43.302,146.125,-43.311,145.975,-43.277,145.873,-43.292,145.803,-43.244,145.682,-43.076,145.61,-42.998,145.567,-42.968,145.518,-42.951,145.488,-42.927,145.268,-42.544,145.237,-42.456,145.199,-42.231,145.373,-42.338,145.435,-42.407,145.468,-42.493,145.527,-42.388,145.517,-42.354,145.36,-42.228,145.34,-42.191,145.331,-42.147,145.294,-42.191,145.235,-42.197,145.259,-42.107,145.238,-42.02,145.055,-41.827,144.916,-41.644,144.778,-41.419,144.766,-41.39,144.764,-41.342,144.698,-41.191,144.662,-41.079,144.646,-40.981,144.71,-40.783,144.719,-40.672,144.819,-40.722,145.043,-40.787,145.159,-40.791,145.224,-40.765,145.283,-40.77,145.349,-40.826,145.429,-40.858,145.485,-40.852,145.533,-40.864,145.576,-40.904,145.686,-40.939,145.734,-40.962,145.775,-40.997,145.821,-41.025,146.111,-41.118,146.317,-41.163,146.574,-41.142,146.651,-41.116,146.723,-41.078,146.786,-41.114,146.848,-41.168,146.836,-41.109,146.857,-41.058,146.919,-41.018,146.99,-40.992,147.106,-40.994,147.219,-40.983,147.269,-40.96,147.321,-40.956,147.388,-40.986,147.455,-41.002,147.501,-40.964,147.579,-40.876,147.622,-40.845,147.818,-40.872,147.873,-40.873,147.969,-40.78,148.033,-40.781,148.215,-40.855,148.293,-40.947,148.285,-41.115,148.292,-41.175,148.306,-41.233,148.312,-41.35,148.29,-41.465,148.287,-41.555,148.297,-41.646,148.288,-41.816,148.316,-41.928,148.302,-42.004,148.301,-42.04,148.328,-42.074,148.341,-42.111,148.331,-42.159,148.343,-42.215,148.331,-42.262,148.29,-42.255,148.277,-42.219,148.285,-42.173,148.277,-42.136,148.256,-42.103,148.183,-42.065,148.204,-42.042,148.242,-42.022,148.214,-41.97,148.167,-42.012,148.141,-42.07,148.156,-42.088,148.128,-42.104,148.067,-42.17,148.023,-42.259,148.005,-42.345,148.009,-42.436,147.974,-42.506,147.924,-42.572,147.912,-42.658,147.915,-42.816,147.958,-42.96,147.981,-43.157,147.945,-43.182,147.839,-43.195,147.786,-43.22]},{"b":[91.524,80.01,97.87,81.28],"l":[97.87,80.763,97.832,80.798,97.703,80.827,97.414,80.842,96.755,80.958,96.693,80.994,96.563,81.03,96.527,81.076,96.471,81.099,96.187,81.184,96.075,81.193,95.984,81.211,95.902,81.261,95.801,81.28,95.16,81.271,95.061,81.188,94.838,81.139,94.612,81.115,94.375,81.107,94.14,81.089,93.889,81.058,93.637,81.038,93.497,81.039,93.359,81.032,93.065,80.988,92.939,80.926,92.765,80.893,92.71,80.872,92.61,80.81,92.593,80.781,92.773,80.769,93.263,80.791,92.981,80.703,92.827,80.619,92.578,80.533,92.247,80.499,91.897,80.478,91.688,80.419,91.524,80.359,91.637,80.27,91.892,80.249,92.092,80.223,92.202,80.179,93.002,80.102,93.655,80.01,93.872,80.01,94.328,80.076,94.565,80.126,94.661,80.123,94.961,80.15,95.856,80.177,97.175,80.241,97.298,80.273,97.417,80.323,97.287,80.343,97.25,80.363,97.115,80.497,97.073,80.52,97.025,80.536,97.113,80.614,97.221,80.652,97.665,80.678,97.747,80.699,97.856,80.698,97.87,80.763]},{"b":[93.071,78.788,100.061,80.168],"l":[99.517,79.13,99.317,79.227,99.042,79.293,99.104,79.305,99.167,79.306,99.388,79.275,99.537,79.277,99.681,79.323,99.722,79.385,99.706,79.463,99.721,79.492,99.749,79.515,99.771,79.568,99.782,79.628,99.805,79.653,99.839,79.669,99.916,79.738,100.061,79.777,99.947,79.849,99.818,79.898,99.727,79.92,99.536,79.941,99.473,79.97,99.371,79.986,99.295,80.016,98.866,80.045,98.596,80.052,98.532,80.044,98.472,80.009,98.499,79.953,98.353,79.884,98.273,79.874,98.065,79.901,97.871,79.853,97.725,79.781,97.652,79.761,97.591,79.775,97.627,79.85,97.76,79.896,97.808,79.956,97.907,80.004,98.018,80.023,97.904,80.095,97.675,80.158,97.587,80.168,97.121,80.153,96.417,80.104,96.277,80.11,96.163,80.097,95.858,80.11,95.498,80.106,95.391,80.073,95.338,80.042,95.281,80.031,94.987,80.097,94.947,80.089,94.815,80.035,94.719,80.011,94.347,79.942,94.257,79.83,94.038,79.756,93.847,79.702,93.405,79.632,93.071,79.495,93.272,79.458,93.479,79.463,93.759,79.451,94.219,79.402,94.314,79.308,94.482,79.219,94.62,79.192,94.632,79.141,94.652,79.127,94.791,79.087,95.02,79.053,95.133,79.05,95.437,79.099,95.531,79.098,95.703,79.012,95.796,79.001,96.347,79.016,96.43,79.003,96.808,78.985,96.871,78.964,96.933,78.934,97.248,78.868,97.555,78.827,97.689,78.827,97.905,78.81,98.054,78.821,98.283,78.795,98.411,78.788,98.82,78.818,99.44,78.834,99.541,78.853,99.929,78.961,99.9,79.006,99.815,79.096,99.751,79.108,99.517,79.13]},{"b":[99.287,77.957,105.343,79.433],"l":[105.146,78.819,105.015,78.843,104.881,78.855,104.633,78.835,104.452,78.88,104.477,78.923,104.449,78.964,104.404,78.977,104.091,79.013,104.004,79.063,103.926,79.123,103.801,79.149,103.673,79.15,103.433,79.126,103.199,79.071,103.076,79.056,102.95,79.056,102.845,79.014,102.748,78.95,102.587,78.871,102.412,78.835,102.448,78.877,102.746,79.106,102.787,79.176,102.885,79.254,102.94,79.271,103.052,79.283,103.098,79.299,103.042,79.332,102.79,79.392,102.405,79.433,102.282,79.43,102.225,79.413,102.181,79.373,102.177,79.313,102.251,79.256,102.129,79.252,102.005,79.264,101.912,79.312,101.824,79.37,101.761,79.372,101.643,79.361,101.591,79.35,101.555,79.313,101.543,79.254,101.31,79.233,101.196,79.204,101.149,79.157,101.052,79.123,101.068,79.096,101.031,79.023,100.965,79.007,100.901,78.98,100.865,78.926,100.856,78.898,100.898,78.812,100.956,78.788,100.876,78.784,100.62,78.797,100.516,78.788,100.416,78.753,100.284,78.679,100.263,78.631,100.257,78.574,100.215,78.536,100.163,78.504,100.124,78.47,100.058,78.38,100.019,78.339,99.678,78.233,99.546,78.179,99.439,78.084,99.287,78.038,99.392,78.001,99.5,77.976,99.845,77.957,100.082,77.975,100.541,78.048,101.04,78.143,101.204,78.192,101.692,78.194,102.18,78.205,102.617,78.225,102.673,78.202,102.734,78.19,102.797,78.188,103.003,78.256,103.719,78.258,104.297,78.335,104.519,78.349,104.742,78.34,104.833,78.353,105.313,78.5,105.343,78.594,105.31,78.666,105.256,78.733,105.205,78.78,105.146,78.819]},{"b":[136.948,74.657,145.36,76.2],"l":[143.686,75.864,143.56,75.86,143.311,75.822,143.185,75.814,142.927,75.827,142.67,75.863,142.46,75.904,142.001,76.044,141.742,76.108,141.485,76.137,141.299,76.064,141.033,75.989,140.985,75.965,140.95,75.927,140.926,75.867,140.927,75.799,140.94,75.75,140.944,75.7,140.889,75.652,140.816,75.631,140.657,75.634,140.602,75.644,140.547,75.663,140.496,75.69,140.389,75.796,140.274,75.822,140.152,75.81,140.049,75.829,139.743,75.953,139.529,76.013,139.211,76.081,139.109,76.108,139.018,76.16,138.92,76.197,138.814,76.2,138.431,76.13,138.208,76.115,138.096,76.081,138.039,76.047,137.977,76.028,137.774,76.016,137.625,75.988,137.561,75.955,137.501,75.91,137.594,75.823,137.707,75.76,137.358,75.782,137.269,75.749,137.215,75.554,137.29,75.349,137.166,75.347,136.982,75.365,136.948,75.326,136.962,75.27,137.006,75.235,137.218,75.124,137.447,75.054,137.568,75.041,137.683,75.009,137.915,74.871,138.001,74.827,138.092,74.797,138.866,74.701,138.982,74.674,139.099,74.657,139.215,74.66,139.326,74.687,139.43,74.749,139.512,74.838,139.548,74.904,139.606,74.946,139.681,74.964,139.758,74.964,140.011,74.895,140.268,74.847,140.464,74.856,140.661,74.882,141.31,74.923,141.53,74.947,141.748,74.983,141.987,74.991,142.1,74.951,142.184,74.9,142.287,74.85,142.378,74.829,142.473,74.82,142.626,74.837,142.778,74.868,143.128,74.97,142.93,75.062,142.697,75.103,142.617,75.133,142.265,75.346,142.199,75.393,142.151,75.458,142.086,75.661,142.308,75.692,142.552,75.721,142.942,75.713,143.002,75.66,142.986,75.633,142.868,75.572,142.734,75.545,142.7,75.449,142.729,75.338,142.82,75.268,142.922,75.217,143.17,75.117,143.396,75.083,143.626,75.084,144.02,75.045,144.216,75.059,144.408,75.102,144.883,75.269,144.814,75.325,144.727,75.366,144.803,75.416,145.023,75.49,145.36,75.53,145.31,75.564,145.255,75.586,143.686,75.864]},{"b":[139.686,73.221,143.491,73.915],"l":[143.344,73.569,142.639,73.803,142.435,73.852,142.185,73.896,141.932,73.915,141.682,73.904,141.312,73.872,141.19,73.876,141.085,73.866,140.984,73.832,140.884,73.778,140.697,73.629,140.594,73.565,140.381,73.483,140.155,73.458,139.92,73.449,139.686,73.426,139.786,73.355,139.925,73.355,140.027,73.361,140.392,73.435,140.663,73.452,140.754,73.446,141.183,73.389,141.597,73.311,142.126,73.282,142.342,73.253,142.587,73.253,142.842,73.245,143.193,73.221,143.451,73.231,143.491,73.246,143.464,73.459,143.411,73.521,143.344,73.569]},{"b":[146.149,74.772,150.822,75.582],"l":[150.69,75.155,150.613,75.12,150.531,75.1,150.417,75.134,150.281,75.164,150.104,75.219,149.645,75.245,149.083,75.262,148.892,75.228,148.59,75.236,148.475,75.272,148.489,75.309,148.519,75.336,148.509,75.387,148.432,75.414,147.497,75.441,147.444,75.438,147.06,75.364,146.795,75.371,146.748,75.429,146.751,75.51,146.538,75.582,146.438,75.558,146.343,75.481,146.258,75.394,146.186,75.296,146.149,75.198,146.703,75.114,146.925,75.063,147.144,74.998,147.257,74.984,147.627,74.959,147.741,74.932,147.972,74.857,148.092,74.826,148.297,74.8,149.05,74.772,149.597,74.773,149.838,74.795,150.331,74.867,150.58,74.919,150.646,74.945,150.822,75.157,150.757,75.162,150.69,75.155]},{"b":[172.706,-41.611,178.536,-34.429],"l":[175.781,-36.805,175.842,-36.875,175.876,-36.958,175.921,-37.205,175.99,-37.437,176.115,-37.538,176.129,-37.587,176.053,-37.562,176.03,-37.576,176.038,-37.601,176.108,-37.645,176.191,-37.667,176.243,-37.664,176.292,-37.68,176.615,-37.831,176.77,-37.89,177.162,-37.986,177.274,-37.993,177.336,-37.991,177.453,-37.957,177.558,-37.897,177.649,-37.808,177.727,-37.706,177.813,-37.656,177.909,-37.617,177.958,-37.581,178.009,-37.555,178.272,-37.567,178.361,-37.618,178.476,-37.66,178.536,-37.692,178.516,-37.758,178.447,-37.854,178.394,-37.96,178.347,-38.201,178.315,-38.444,178.268,-38.551,178.181,-38.634,178.085,-38.694,177.976,-38.722,177.932,-38.86,177.91,-39.022,177.917,-39.062,177.951,-39.095,177.966,-39.142,177.909,-39.24,177.875,-39.225,177.829,-39.145,177.786,-39.111,177.656,-39.086,177.523,-39.074,177.408,-39.081,177.297,-39.116,177.129,-39.186,177.077,-39.222,177.031,-39.267,176.954,-39.368,176.936,-39.491,176.939,-39.555,176.967,-39.605,177.11,-39.673,176.968,-39.911,176.842,-40.158,176.771,-40.228,176.689,-40.293,176.612,-40.442,176.476,-40.57,176.385,-40.668,176.314,-40.769,176.252,-40.877,176.119,-41.029,176.06,-41.13,175.983,-41.213,175.84,-41.32,175.687,-41.412,175.447,-41.538,175.38,-41.58,175.31,-41.611,175.222,-41.574,175.204,-41.535,175.185,-41.449,175.166,-41.417,175.054,-41.391,174.906,-41.433,174.881,-41.424,174.875,-41.404,174.875,-41.278,174.9,-41.243,174.866,-41.223,174.832,-41.231,174.82,-41.263,174.841,-41.291,174.757,-41.325,174.67,-41.326,174.643,-41.313,174.635,-41.289,174.657,-41.251,174.685,-41.218,174.848,-41.059,175.017,-40.848,175.163,-40.622,175.2,-40.505,175.254,-40.289,175.21,-40.199,175.156,-40.115,175.009,-39.952,174.814,-39.86,174.687,-39.847,174.567,-39.813,174.455,-39.735,174.352,-39.643,174.149,-39.568,173.934,-39.509,173.812,-39.426,173.783,-39.376,173.764,-39.319,173.766,-39.265,173.782,-39.211,173.806,-39.17,173.844,-39.139,174.071,-39.031,174.312,-38.971,174.356,-38.972,174.398,-38.963,174.458,-38.926,174.566,-38.842,174.597,-38.785,174.619,-38.605,174.653,-38.428,174.715,-38.226,174.809,-38.1,174.84,-38.023,174.802,-37.896,174.837,-37.849,174.88,-37.821,174.928,-37.804,174.846,-37.685,174.749,-37.505,174.729,-37.449,174.744,-37.393,174.768,-37.339,174.707,-37.325,174.673,-37.273,174.586,-37.098,174.61,-37.07,174.66,-37.089,174.734,-37.215,174.746,-37.15,174.804,-37.11,174.864,-37.089,174.929,-37.085,174.782,-36.944,174.733,-36.949,174.668,-36.972,174.601,-36.986,174.537,-36.973,174.476,-36.942,174.445,-36.883,174.406,-36.768,174.382,-36.726,174.189,-36.492,174.246,-36.485,174.402,-36.602,174.432,-36.565,174.454,-36.511,174.447,-36.451,174.41,-36.406,174.354,-36.376,174.353,-36.323,174.395,-36.274,174.393,-36.24,174.304,-36.171,174.268,-36.163,174.252,-36.196,174.278,-36.244,174.254,-36.249,174.036,-36.122,173.969,-36.021,173.914,-35.909,173.909,-35.954,173.917,-36.018,174.003,-36.146,174.142,-36.289,174.166,-36.328,174.146,-36.377,174.097,-36.391,174.055,-36.36,173.991,-36.237,173.945,-36.176,173.412,-35.543,173.48,-35.459,173.586,-35.389,173.61,-35.357,173.626,-35.319,173.582,-35.313,173.542,-35.33,173.496,-35.362,173.454,-35.399,173.402,-35.481,173.376,-35.5,173.314,-35.443,173.29,-35.408,173.291,-35.366,173.275,-35.34,173.228,-35.331,173.16,-35.248,173.117,-35.205,173.189,-35.124,173.191,-35.016,173.117,-34.903,173.03,-34.8,172.861,-34.632,172.706,-34.455,172.874,-34.433,173.044,-34.429,172.964,-34.535,173,-34.596,173.054,-34.648,173.171,-34.807,173.181,-34.853,173.241,-34.899,173.269,-34.935,173.285,-34.981,173.34,-34.948,173.381,-34.896,173.448,-34.844,173.439,-34.929,173.473,-34.947,173.694,-35.006,173.739,-35.055,173.786,-35.069,173.813,-35.041,173.844,-35.026,173.924,-35.057,174.104,-35.143,174.119,-35.172,174.11,-35.216,174.119,-35.263,174.143,-35.3,174.203,-35.309,174.283,-35.254,174.32,-35.247,174.373,-35.325,174.393,-35.369,174.385,-35.367,174.419,-35.411,174.465,-35.454,174.543,-35.582,174.532,-35.627,174.509,-35.667,174.581,-35.786,174.533,-35.794,174.391,-35.774,174.396,-35.797,174.479,-35.884,174.549,-36.007,174.605,-36.081,174.802,-36.309,174.772,-36.391,174.777,-36.445,174.752,-36.491,174.819,-36.612,174.777,-36.65,174.749,-36.774,174.719,-36.796,174.722,-36.841,174.802,-36.853,174.85,-36.873,174.891,-36.909,174.917,-36.865,174.952,-36.853,175.047,-36.912,175.245,-36.971,175.3,-36.993,175.326,-37.041,175.347,-37.156,175.385,-37.207,175.461,-37.217,175.542,-37.201,175.568,-37.159,175.552,-37.046,175.493,-36.866,175.493,-36.807,175.501,-36.748,175.487,-36.69,175.458,-36.634,175.426,-36.592,175.386,-36.556,175.4,-36.501,175.461,-36.476,175.498,-36.523,175.528,-36.579,175.681,-36.747,175.772,-36.735,175.781,-36.805]},{"b":[166.478,-46.631,174.37,-40.49],"l":[172.889,-43.124,173.072,-43.06,173.149,-43.023,173.221,-42.977,173.348,-42.841,173.545,-42.518,173.589,-42.474,173.84,-42.271,173.889,-42.212,173.888,-42.13,173.974,-42.081,174.047,-42.003,174.215,-41.85,174.243,-41.813,174.283,-41.741,174.217,-41.678,174.17,-41.657,174.084,-41.671,174.161,-41.562,174.092,-41.505,174.073,-41.472,174.069,-41.429,174.102,-41.366,174.17,-41.327,174.237,-41.312,174.297,-41.264,174.368,-41.188,174.37,-41.104,174.284,-41.172,174.138,-41.248,174.039,-41.242,174.103,-41.217,174.2,-41.16,174.214,-41.126,174.274,-41.069,174.303,-41.02,174.224,-41.024,174.212,-40.985,174.153,-40.991,174.121,-41.005,174.081,-41.006,173.999,-40.993,173.998,-41.028,174.024,-41.072,173.958,-41.1,173.915,-41.158,173.933,-41.187,173.897,-41.239,173.798,-41.272,173.862,-41.192,173.86,-41.124,173.915,-41.07,173.88,-41.031,173.89,-41.007,173.953,-40.985,174.002,-40.918,173.947,-40.924,173.898,-40.951,173.784,-40.972,173.738,-40.989,173.563,-41.102,173.447,-41.151,173.338,-41.211,173.231,-41.284,173.115,-41.279,173.069,-41.186,173.052,-41.079,173.042,-40.954,172.989,-40.848,172.869,-40.82,172.767,-40.773,172.729,-40.724,172.704,-40.668,172.711,-40.605,172.733,-40.544,172.944,-40.519,172.83,-40.49,172.711,-40.497,172.641,-40.518,172.468,-40.622,172.273,-40.759,172.139,-40.947,172.093,-41.202,172.011,-41.445,171.948,-41.539,171.831,-41.655,171.732,-41.72,171.672,-41.745,171.536,-41.758,171.486,-41.795,171.421,-41.973,171.36,-42.08,171.323,-42.189,171.296,-42.303,171.252,-42.402,171.296,-42.431,171.313,-42.46,171.257,-42.465,171.221,-42.479,171.19,-42.5,171.028,-42.696,171.048,-42.802,171.038,-42.862,171.012,-42.885,171.018,-42.819,171.011,-42.764,170.97,-42.718,170.84,-42.849,170.742,-42.927,170.725,-42.975,170.735,-43.03,170.665,-42.961,170.616,-42.972,170.524,-43.009,170.536,-43.041,170.612,-43.092,170.536,-43.058,170.459,-43.038,170.379,-43.066,170.303,-43.108,170.374,-43.135,170.396,-43.182,170.356,-43.154,170.3,-43.145,170.24,-43.164,170.19,-43.222,170.149,-43.248,170.104,-43.265,170.018,-43.349,169.859,-43.426,169.908,-43.447,169.891,-43.462,169.835,-43.459,169.824,-43.497,169.834,-43.537,169.769,-43.538,169.662,-43.591,169.515,-43.624,169.323,-43.702,169.17,-43.777,169.136,-43.82,169.179,-43.913,169.136,-43.9,169.067,-43.863,168.99,-43.89,168.806,-43.992,168.775,-43.996,168.651,-43.972,168.457,-44.031,168.367,-44.082,168.196,-44.224,168.018,-44.359,167.857,-44.501,167.866,-44.592,167.902,-44.625,167.909,-44.665,167.859,-44.625,167.787,-44.595,167.698,-44.641,167.578,-44.741,167.485,-44.771,167.46,-44.802,167.456,-44.838,167.482,-44.874,167.479,-44.915,167.466,-44.958,167.411,-44.828,167.195,-44.963,167.172,-44.997,167.205,-45.048,167.259,-45.082,167.188,-45.094,167.128,-45.051,167.026,-45.124,167.023,-45.177,167.033,-45.222,167.127,-45.266,167.207,-45.28,167.23,-45.29,167.145,-45.302,167.118,-45.318,167.112,-45.354,167.156,-45.411,167.052,-45.383,166.909,-45.307,166.869,-45.311,166.876,-45.368,166.92,-45.408,166.778,-45.41,166.743,-45.468,166.734,-45.544,166.869,-45.55,166.991,-45.532,166.826,-45.603,166.798,-45.646,166.81,-45.699,167.003,-45.712,166.953,-45.75,166.836,-45.775,166.513,-45.812,166.488,-45.832,166.478,-45.903,166.493,-45.964,166.613,-45.955,166.718,-45.889,166.734,-45.928,166.727,-45.963,166.65,-46.042,166.73,-46.053,166.856,-45.981,166.917,-45.957,166.712,-46.134,166.732,-46.198,166.831,-46.225,167.1,-46.249,167.369,-46.242,167.414,-46.229,167.491,-46.155,167.539,-46.149,167.682,-46.193,167.722,-46.227,167.842,-46.366,167.9,-46.368,168.077,-46.353,168.189,-46.362,168.23,-46.386,168.266,-46.419,168.32,-46.447,168.343,-46.489,168.326,-46.546,168.357,-46.588,168.382,-46.605,168.466,-46.588,168.572,-46.611,168.631,-46.588,168.767,-46.566,168.838,-46.578,168.966,-46.613,169.099,-46.631,169.342,-46.621,169.687,-46.552,169.729,-46.521,169.761,-46.48,169.918,-46.334,170.186,-46.161,170.267,-46.083,170.335,-45.992,170.419,-45.941,170.674,-45.896,170.722,-45.878,170.776,-45.871,170.791,-45.844,170.788,-45.792,170.74,-45.756,170.7,-45.714,170.701,-45.684,170.815,-45.519,170.89,-45.374,170.94,-45.216,170.991,-45.151,171.113,-45.039,171.134,-44.978,171.023,-44.937,170.999,-44.911,171.146,-44.912,171.198,-44.768,171.213,-44.612,171.231,-44.521,171.313,-44.302,171.285,-44.279,171.241,-44.264,171.365,-44.255,171.417,-44.209,171.443,-44.136,171.518,-44.118,171.659,-44.117,171.712,-44.097,171.808,-44.042,171.891,-44.007,171.978,-43.984,172.081,-43.946,172.18,-43.896,172.137,-43.834,172.052,-43.74,172.036,-43.702,172.146,-43.764,172.221,-43.825,172.297,-43.868,172.35,-43.859,172.385,-43.83,172.396,-43.778,172.43,-43.746,172.48,-43.727,172.527,-43.739,172.584,-43.774,172.476,-43.833,172.503,-43.844,172.555,-43.831,172.749,-43.813,172.818,-43.87,172.921,-43.891,173.023,-43.885,173.066,-43.875,173.094,-43.844,173.117,-43.798,173.098,-43.704,173.073,-43.676,172.947,-43.659,172.807,-43.621,172.767,-43.562,172.749,-43.517,172.74,-43.468,172.693,-43.444,172.527,-43.465,172.562,-43.436,172.632,-43.428,172.7,-43.4,172.735,-43.355,172.688,-43.315,172.627,-43.3,172.624,-43.272,172.719,-43.259,172.808,-43.198,172.889,-43.124]},{"b":[121.904,5.599,126.593,9.759],"l":[126.593,7.547,126.57,7.677,126.544,7.725,126.494,7.757,126.435,7.833,126.425,7.927,126.457,8.149,126.459,8.203,126.38,8.327,126.365,8.484,126.282,8.539,126.173,8.56,126.14,8.596,126.142,8.627,126.22,8.696,126.263,8.744,126.32,8.845,126.305,8.952,126.209,9.081,126.192,9.125,126.193,9.277,126.088,9.261,126.006,9.321,125.955,9.427,125.877,9.513,125.642,9.654,125.521,9.759,125.471,9.757,125.414,9.669,125.51,9.276,125.533,9.141,125.499,9.015,125.376,8.992,125.248,9.027,125.21,9.027,125.176,8.922,125.141,8.869,125.046,8.891,124.944,8.957,124.869,8.972,124.806,8.924,124.787,8.874,124.762,8.69,124.731,8.563,124.622,8.523,124.451,8.606,124.405,8.6,124.358,8.559,124.325,8.508,124.283,8.386,124.226,8.271,124.198,8.23,124.159,8.201,123.997,8.159,123.931,8.128,123.799,8.049,123.753,8.058,123.853,8.145,123.877,8.189,123.861,8.376,123.849,8.433,123.783,8.548,123.68,8.621,123.564,8.647,123.499,8.682,123.435,8.703,123.38,8.616,123.341,8.57,123.293,8.541,123.147,8.516,123.096,8.481,123.051,8.434,123.018,8.398,122.999,8.356,123.003,8.287,122.996,8.221,122.911,8.156,122.804,8.134,122.673,8.133,122.589,8.093,122.387,8.046,122.337,8.028,122.243,7.945,122.132,7.81,122.12,7.765,122.115,7.66,122.047,7.364,121.991,7.279,121.925,7.2,121.904,7.075,121.964,6.968,122.028,6.929,122.098,6.914,122.142,6.95,122.176,7.004,122.251,7.17,122.32,7.34,122.449,7.561,122.474,7.639,122.498,7.673,122.616,7.763,122.714,7.774,122.792,7.722,122.819,7.558,122.843,7.529,122.917,7.531,122.99,7.546,123.049,7.614,123.097,7.7,123.121,7.667,123.139,7.63,123.151,7.575,123.178,7.529,123.282,7.464,123.391,7.408,123.476,7.665,123.482,7.71,123.477,7.756,123.493,7.808,123.553,7.832,123.609,7.832,123.666,7.818,123.717,7.785,123.765,7.743,123.968,7.665,124.068,7.578,124.182,7.437,124.207,7.396,124.213,7.332,124.191,7.267,124.158,7.219,124.118,7.175,124.045,7.114,123.985,6.994,123.981,6.93,123.988,6.863,124.048,6.667,124.05,6.533,124.078,6.404,124.213,6.233,124.399,6.12,124.636,5.998,124.927,5.875,124.975,5.866,125.035,5.871,125.076,5.906,125.174,6.047,125.191,6.063,125.232,6.07,125.268,6.033,125.265,5.926,125.233,5.808,125.241,5.757,125.288,5.632,125.346,5.599,125.456,5.664,125.608,5.87,125.668,5.979,125.671,6.225,125.588,6.466,125.565,6.5,125.487,6.574,125.433,6.607,125.381,6.69,125.401,6.796,125.465,6.911,125.542,7.017,125.641,7.105,125.66,7.161,125.67,7.222,125.689,7.263,125.774,7.322,125.824,7.333,125.901,7.117,125.962,7.033,125.985,6.944,126.043,6.843,126.08,6.733,126.11,6.49,126.142,6.398,126.189,6.31,126.221,6.483,126.24,6.734,126.192,6.853,126.217,6.891,126.294,6.882,126.439,7.012,126.547,7.176,126.582,7.248,126.589,7.325,126.593,7.547]},{"b":[119.762,12.567,124.143,18.615],"l":[122.294,18.234,122.223,18.157,122.179,18.064,122.151,17.756,122.152,17.664,122.175,17.576,122.237,17.435,122.269,17.395,122.362,17.345,122.388,17.307,122.393,17.238,122.408,17.178,122.468,17.155,122.519,17.125,122.5,17.058,122.468,16.99,122.426,16.823,122.226,16.435,122.214,16.352,122.135,16.185,121.975,16.158,121.789,16.077,121.685,16.015,121.595,15.933,121.561,15.827,121.59,15.778,121.609,15.726,121.607,15.67,121.579,15.623,121.49,15.51,121.452,15.417,121.412,15.375,121.392,15.324,121.399,15.267,121.435,15.216,121.544,14.999,121.661,14.79,121.686,14.765,121.695,14.737,121.627,14.682,121.628,14.581,121.649,14.481,121.752,14.234,121.767,14.168,121.8,14.114,121.853,14.063,121.912,14.02,122.08,13.947,122.144,13.933,122.212,13.93,122.228,13.979,122.288,13.996,122.274,14.045,122.203,14.112,122.2,14.148,122.238,14.175,122.283,14.191,122.384,14.264,122.491,14.322,122.627,14.318,122.761,14.285,122.856,14.251,122.934,14.188,123.015,14.08,123.071,13.96,123.071,13.903,123.057,13.845,123.06,13.789,123.102,13.75,123.231,13.747,123.297,13.836,123.305,13.937,123.259,13.975,123.28,14.025,123.32,14.062,123.377,14.029,123.432,13.966,123.633,13.898,123.684,13.897,123.726,13.884,123.816,13.837,123.858,13.8,123.806,13.722,123.607,13.704,123.55,13.646,123.608,13.528,123.704,13.432,123.765,13.354,123.819,13.269,123.817,13.192,123.785,13.111,123.873,13.117,123.955,13.1,124.069,13.032,124.105,13.025,124.143,13.036,124.137,12.791,124.06,12.567,123.962,12.595,123.878,12.69,123.895,12.805,123.949,12.916,123.918,12.94,123.864,12.931,123.802,12.906,123.736,12.897,123.627,12.912,123.402,13.033,123.311,13.044,123.29,13.099,123.296,13.216,123.206,13.354,123.192,13.403,123.163,13.442,122.896,13.592,122.863,13.617,122.781,13.737,122.595,13.908,122.543,13.925,122.486,13.93,122.468,13.887,122.494,13.82,122.504,13.763,122.5,13.703,122.508,13.657,122.596,13.562,122.609,13.517,122.668,13.395,122.675,13.253,122.6,13.194,122.515,13.26,122.513,13.314,122.498,13.364,122.407,13.493,122.377,13.521,122.205,13.648,122.073,13.788,121.778,13.938,121.743,13.946,121.692,13.935,121.643,13.916,121.501,13.842,121.451,13.791,121.446,13.712,121.344,13.649,121.204,13.64,121.096,13.679,121.006,13.758,120.932,13.762,120.841,13.885,120.729,13.901,120.637,13.804,120.617,13.995,120.617,14.188,120.643,14.244,120.688,14.291,120.922,14.493,120.952,14.558,120.941,14.645,120.888,14.716,120.804,14.759,120.708,14.777,120.638,14.816,120.584,14.881,120.547,14.766,120.583,14.595,120.589,14.483,120.556,14.441,120.496,14.44,120.439,14.453,120.396,14.493,120.365,14.608,120.284,14.684,120.251,14.793,120.214,14.809,120.138,14.8,120.082,14.851,120.045,14.978,120.037,15.115,120.005,15.229,119.959,15.34,119.933,15.431,119.892,15.838,119.881,15.875,119.86,15.906,119.808,15.952,119.769,16.008,119.762,16.055,119.773,16.255,119.79,16.303,119.831,16.327,119.886,16.287,119.93,16.239,119.985,16.215,120.033,16.185,120.124,16.066,120.16,16.048,120.271,16.051,120.337,16.066,120.369,16.11,120.389,16.161,120.389,16.222,120.325,16.4,120.305,16.529,120.304,16.645,120.321,16.762,120.409,16.956,120.42,17.09,120.412,17.27,120.427,17.377,120.425,17.438,120.372,17.535,120.358,17.638,120.505,18.163,120.551,18.264,120.584,18.369,120.6,18.508,120.709,18.546,120.814,18.603,120.868,18.599,120.925,18.585,121.051,18.614,121.102,18.615,121.254,18.563,121.593,18.376,121.717,18.33,121.846,18.295,121.948,18.285,122.038,18.328,122.077,18.372,122.147,18.487,122.221,18.501,122.266,18.459,122.3,18.403,122.315,18.32,122.294,18.234]},{"b":[130.996,-10.655,150.85,-0.347],"l":[140.662,-8.847,140.581,-8.728,140.49,-8.62,140.102,-8.301,140.003,-8.196,139.983,-8.167,139.993,-8.139,140.037,-8.084,140.117,-7.924,140.034,-8.023,139.935,-8.101,139.791,-8.106,139.649,-8.125,139.519,-8.173,139.386,-8.189,139.319,-8.166,139.279,-8.107,139.258,-8.047,139.249,-7.982,139.193,-8.086,139.083,-8.143,138.933,-8.262,138.891,-8.238,138.865,-8.192,138.856,-8.145,138.885,-8.095,138.905,-8.041,138.936,-7.913,139.003,-7.838,139.046,-7.691,139.074,-7.639,139.088,-7.587,139.049,-7.528,138.983,-7.508,138.938,-7.472,138.886,-7.373,138.853,-7.34,138.794,-7.299,138.748,-7.251,138.798,-7.216,138.865,-7.201,138.919,-7.204,139.018,-7.226,139.063,-7.227,139.177,-7.19,139.113,-7.202,139.049,-7.201,138.846,-7.136,138.72,-7.07,138.601,-6.937,138.6,-6.911,138.684,-6.887,138.865,-6.858,138.808,-6.79,138.727,-6.731,138.698,-6.626,138.642,-6.56,138.522,-6.454,138.439,-6.343,138.368,-6.119,138.296,-5.949,138.314,-5.887,138.375,-5.844,138.283,-5.839,138.2,-5.807,138.244,-5.724,138.34,-5.676,138.252,-5.688,138.167,-5.712,138.127,-5.717,138.087,-5.709,138.066,-5.676,138.063,-5.629,138.076,-5.546,138.061,-5.465,137.985,-5.428,137.922,-5.37,137.887,-5.349,137.84,-5.35,137.795,-5.312,137.759,-5.256,137.307,-5.014,137.28,-4.945,137.238,-4.976,137.196,-4.99,137.144,-4.951,137.089,-4.924,137.03,-4.929,136.975,-4.907,136.917,-4.895,136.857,-4.893,136.619,-4.819,136.394,-4.701,136.211,-4.651,136.097,-4.585,135.98,-4.531,135.717,-4.478,135.45,-4.443,135.354,-4.442,135.273,-4.453,135.196,-4.451,134.754,-4.195,134.68,-4.079,134.687,-4.011,134.707,-3.955,134.887,-3.938,134.76,-3.922,134.708,-3.93,134.603,-3.976,134.547,-3.979,134.467,-3.949,134.391,-3.91,134.266,-3.946,134.202,-3.887,134.18,-3.825,134.147,-3.797,134.1,-3.8,134.037,-3.822,133.974,-3.818,133.933,-3.776,133.904,-3.72,133.861,-3.68,133.808,-3.65,133.723,-3.578,133.678,-3.479,133.683,-3.309,133.697,-3.248,133.782,-3.149,133.842,-3.055,133.767,-3.044,133.7,-3.087,133.672,-3.132,133.661,-3.186,133.653,-3.364,133.599,-3.416,133.518,-3.412,133.542,-3.516,133.509,-3.616,133.415,-3.732,133.407,-3.785,133.422,-3.843,133.401,-3.899,133.249,-4.062,133.198,-4.07,133.085,-4.069,132.969,-4.095,132.914,-4.057,132.87,-4.007,132.837,-3.949,132.791,-3.828,132.754,-3.704,132.87,-3.551,132.83,-3.413,132.751,-3.295,132.554,-3.131,132.348,-2.975,132.255,-2.943,132.102,-2.93,132.054,-2.915,132.006,-2.856,131.971,-2.789,132.067,-2.76,132.231,-2.68,132.323,-2.684,132.575,-2.727,132.653,-2.766,132.725,-2.789,132.897,-2.658,133.034,-2.487,133.119,-2.45,133.191,-2.438,133.265,-2.454,133.411,-2.514,133.527,-2.542,133.609,-2.547,133.652,-2.601,133.7,-2.625,133.711,-2.544,133.753,-2.451,133.835,-2.422,133.878,-2.415,133.905,-2.391,133.899,-2.304,133.791,-2.294,133.85,-2.22,133.902,-2.184,133.921,-2.147,133.922,-2.102,133.71,-2.189,133.488,-2.226,133.356,-2.216,133.225,-2.214,132.963,-2.273,132.863,-2.27,132.631,-2.247,132.503,-2.218,132.403,-2.24,132.308,-2.242,132.207,-2.176,132.122,-2.092,132.08,-2.033,132.023,-1.99,131.998,-1.933,131.936,-1.715,131.93,-1.56,131.83,-1.557,131.731,-1.541,131.294,-1.393,131.241,-1.43,131.179,-1.448,131.118,-1.455,131.057,-1.448,130.996,-1.425,131.001,-1.384,131.046,-1.284,131.091,-1.247,131.152,-1.219,131.191,-1.166,131.254,-1.007,131.259,-0.953,131.252,-0.897,131.257,-0.855,131.296,-0.834,131.462,-0.782,131.804,-0.704,131.891,-0.657,131.962,-0.582,132.046,-0.537,132.084,-0.491,132.128,-0.454,132.394,-0.355,132.508,-0.347,132.625,-0.359,132.856,-0.417,133.077,-0.512,133.268,-0.636,133.473,-0.726,133.724,-0.741,133.85,-0.731,133.975,-0.744,134.025,-0.77,134.112,-0.847,134.087,-0.897,134.072,-1.002,134.116,-1.102,134.188,-1.203,134.247,-1.311,134.26,-1.363,134.237,-1.474,134.217,-1.529,134.145,-1.621,134.106,-1.721,134.131,-1.845,134.145,-1.969,134.143,-2.083,134.156,-2.195,134.195,-2.309,134.362,-2.621,134.46,-2.832,134.491,-2.714,134.483,-2.583,134.518,-2.536,134.567,-2.51,134.627,-2.537,134.645,-2.59,134.649,-2.706,134.702,-2.934,134.77,-2.944,134.843,-2.909,134.855,-2.979,134.853,-3.108,134.887,-3.21,134.917,-3.25,135.037,-3.333,135.092,-3.349,135.252,-3.369,135.372,-3.375,135.487,-3.345,135.561,-3.269,135.628,-3.186,135.859,-2.995,135.926,-2.904,135.991,-2.764,136.013,-2.734,136.243,-2.583,136.27,-2.529,136.303,-2.426,136.352,-2.325,136.39,-2.273,136.612,-2.224,136.843,-2.198,137.072,-2.105,137.171,-2.025,137.176,-1.973,137.125,-1.881,137.123,-1.841,137.176,-1.802,137.381,-1.686,137.617,-1.566,137.806,-1.483,137.911,-1.484,138.008,-1.557,138.111,-1.616,138.65,-1.791,138.736,-1.846,138.811,-1.918,138.919,-1.968,139.039,-1.992,139.149,-2.039,139.253,-2.099,139.482,-2.212,139.79,-2.348,139.868,-2.356,140.155,-2.35,140.204,-2.376,140.251,-2.412,140.295,-2.42,140.623,-2.446,140.673,-2.472,140.721,-2.508,140.747,-2.607,141.105,-2.611,141.186,-2.628,141.687,-2.845,141.837,-2.932,141.888,-2.953,141.938,-2.953,141.986,-2.964,142.212,-3.083,142.549,-3.205,142.905,-3.321,143.016,-3.345,143.13,-3.355,143.378,-3.395,143.509,-3.431,143.701,-3.573,143.797,-3.617,143.888,-3.697,144.016,-3.784,144.066,-3.805,144.122,-3.815,144.248,-3.818,144.374,-3.803,144.427,-3.81,144.478,-3.825,144.525,-3.855,144.548,-3.913,144.627,-3.993,144.738,-4.029,144.843,-4.101,144.938,-4.188,145.008,-4.275,145.088,-4.349,145.208,-4.38,145.335,-4.385,145.767,-4.823,145.788,-4.891,145.793,-5.178,145.745,-5.402,145.853,-5.471,145.999,-5.497,146.205,-5.545,146.403,-5.617,147.034,-5.919,147.121,-5.945,147.248,-5.955,147.377,-5.951,147.423,-5.966,147.519,-6.021,147.567,-6.057,147.653,-6.155,147.73,-6.261,147.763,-6.292,147.802,-6.315,147.825,-6.373,147.854,-6.551,147.846,-6.662,147.81,-6.704,147.71,-6.724,147.356,-6.742,147.119,-6.722,146.954,-6.834,146.949,-6.883,146.961,-6.929,147.105,-7.167,147.19,-7.378,147.26,-7.464,147.365,-7.534,147.459,-7.616,147.545,-7.711,147.724,-7.876,147.822,-7.937,147.936,-7.975,148.127,-8.104,148.152,-8.16,148.206,-8.339,148.23,-8.46,148.234,-8.51,148.247,-8.554,148.414,-8.664,148.451,-8.695,148.526,-8.939,148.583,-9.052,148.679,-9.092,148.792,-9.089,149.097,-9.017,149.142,-9.015,149.198,-9.031,149.248,-9.071,149.264,-9.181,149.216,-9.296,149.203,-9.407,149.263,-9.498,149.419,-9.569,149.476,-9.588,149.756,-9.611,149.866,-9.63,149.974,-9.661,150.011,-9.688,149.985,-9.737,149.928,-9.761,149.864,-9.771,149.761,-9.806,149.763,-9.869,149.821,-9.934,149.874,-10.013,149.919,-10.042,149.968,-10.061,150.089,-10.088,150.206,-10.126,150.284,-10.163,150.364,-10.19,150.539,-10.207,150.667,-10.257,150.85,-10.236,150.691,-10.318,150.637,-10.338,150.446,-10.307,150.41,-10.339,150.489,-10.426,150.605,-10.484,150.647,-10.518,150.618,-10.558,150.482,-10.637,150.426,-10.649,150.32,-10.655,150.142,-10.621,150.017,-10.577,149.982,-10.518,149.948,-10.483,149.835,-10.399,149.754,-10.353,149.651,-10.337,149.544,-10.338,149.353,-10.29,148.937,-10.255,148.838,-10.234,148.713,-10.167,148.654,-10.157,148.591,-10.178,148.431,-10.191,148.383,-10.185,148.269,-10.128,148.15,-10.107,148.101,-10.125,148.051,-10.128,147.89,-10.087,147.769,-10.07,147.669,-10.013,147.614,-9.96,147.553,-9.912,147.496,-9.79,147.408,-9.675,147.299,-9.58,147.064,-9.426,147.017,-9.388,146.925,-9.247,146.93,-9.154,146.964,-9.06,146.913,-9.092,146.856,-9.088,146.697,-9.025,146.631,-8.951,146.524,-8.75,146.456,-8.644,146.296,-8.456,146.251,-8.344,146.184,-8.246,146.143,-8.21,146.109,-8.168,146.079,-8.114,146.033,-8.076,145.811,-7.993,145.772,-7.966,145.729,-7.952,145.563,-7.944,145.468,-7.93,145.288,-7.862,145.194,-7.841,145.082,-7.828,144.974,-7.802,144.921,-7.777,144.885,-7.734,144.864,-7.632,144.773,-7.642,144.684,-7.625,144.598,-7.589,144.51,-7.567,144.45,-7.598,144.431,-7.679,144.403,-7.684,144.352,-7.667,144.326,-7.677,144.27,-7.714,144.225,-7.765,144.143,-7.757,143.974,-7.706,143.898,-7.674,143.834,-7.616,143.779,-7.55,143.723,-7.498,143.655,-7.46,143.742,-7.55,143.942,-7.944,143.892,-7.952,143.841,-7.942,143.888,-8.018,143.833,-8.029,143.779,-8.028,143.665,-7.996,143.552,-7.985,143.518,-8.001,143.542,-8.029,143.582,-8.113,143.614,-8.2,143.45,-8.24,143.282,-8.264,143.095,-8.311,142.905,-8.314,142.808,-8.287,142.709,-8.272,142.615,-8.287,142.524,-8.322,142.448,-8.316,142.399,-8.255,142.376,-8.208,142.347,-8.167,142.276,-8.174,142.207,-8.196,142.325,-8.198,142.361,-8.25,142.391,-8.313,142.475,-8.369,142.576,-8.336,142.798,-8.345,143.014,-8.444,143.065,-8.455,143.112,-8.475,143.223,-8.572,143.307,-8.661,143.377,-8.762,143.392,-8.802,143.388,-8.908,143.366,-8.961,143.227,-9.036,143.078,-9.092,142.859,-9.203,142.647,-9.328,142.536,-9.303,142.435,-9.237,142.396,-9.219,142.293,-9.183,142.23,-9.17,141.979,-9.198,141.727,-9.213,141.622,-9.211,141.519,-9.19,141.406,-9.151,141.294,-9.168,141.217,-9.214,141.133,-9.221,140.925,-9.085,140.787,-8.974,140.662,-8.847]},{"b":[118.783,-5.693,125.234,1.701],"l":[119.772,-0.484,119.722,-0.088,119.736,-0.051,119.787,-0.057,119.838,-0.022,119.866,0.04,119.812,0.187,119.809,0.239,119.913,0.445,119.998,0.52,120.035,0.567,120.056,0.693,120.101,0.74,120.157,0.774,120.23,0.861,120.27,0.971,120.294,0.979,120.322,0.983,120.367,0.888,120.416,0.849,120.517,0.818,120.603,0.854,120.626,0.902,120.659,0.944,120.711,0.987,120.755,1.036,120.804,1.149,120.868,1.253,120.912,1.289,120.965,1.312,121.025,1.326,121.082,1.328,121.208,1.263,121.282,1.25,121.357,1.255,121.404,1.244,121.44,1.214,121.473,1.156,121.513,1.105,121.551,1.08,121.592,1.068,121.867,1.089,122.108,1.031,122.437,1.018,122.549,0.984,122.657,0.941,122.79,0.863,122.838,0.846,122.892,0.85,122.96,0.923,123.013,0.939,123.067,0.942,123.278,0.928,123.847,0.838,123.931,0.85,124.274,1.022,124.411,1.185,124.534,1.23,124.575,1.304,124.6,1.392,124.644,1.416,124.747,1.441,124.788,1.468,124.861,1.576,124.947,1.672,124.989,1.701,125.111,1.686,125.165,1.644,125.234,1.502,125.222,1.479,125.141,1.408,125.117,1.379,125.028,1.18,124.967,1.083,124.889,0.995,124.698,0.826,124.64,0.744,124.589,0.655,124.514,0.557,124.428,0.471,124.384,0.445,124.278,0.398,124.217,0.38,124.101,0.375,123.754,0.306,123.64,0.297,123.526,0.3,123.31,0.318,123.265,0.327,123.179,0.416,123.083,0.486,122.997,0.494,122.91,0.486,122.281,0.481,122.061,0.468,121.842,0.437,121.723,0.451,121.605,0.486,121.516,0.498,121.426,0.495,121.013,0.442,120.909,0.447,120.7,0.515,120.579,0.528,120.46,0.51,120.349,0.449,120.307,0.408,120.192,0.269,120.127,0.167,120.078,0.04,120.036,-0.09,120.013,-0.196,120.012,-0.307,120.032,-0.432,120.063,-0.556,120.097,-0.65,120.241,-0.868,120.27,-0.899,120.425,-0.961,120.518,-1.039,120.605,-1.258,120.667,-1.37,120.729,-1.371,120.797,-1.364,120.916,-1.378,121.034,-1.407,121.149,-1.339,121.213,-1.212,121.277,-1.118,121.431,-0.939,121.519,-0.856,121.576,-0.829,121.633,-0.84,121.681,-0.888,121.738,-0.926,121.853,-0.946,121.97,-0.933,122.094,-0.875,122.138,-0.839,122.175,-0.794,122.28,-0.757,122.53,-0.757,122.659,-0.77,122.889,-0.755,122.886,-0.722,122.841,-0.687,122.829,-0.659,122.872,-0.641,123.02,-0.6,123.171,-0.571,123.281,-0.592,123.38,-0.649,123.417,-0.707,123.434,-0.778,123.396,-0.962,123.378,-1.004,123.3,-1.026,123.226,-1.002,123.153,-0.907,123.049,-0.872,122.903,-0.901,122.853,-0.928,122.807,-0.966,122.725,-1.064,122.656,-1.175,122.507,-1.348,122.334,-1.498,122.251,-1.555,122.158,-1.594,121.859,-1.693,121.78,-1.767,121.719,-1.863,121.651,-1.895,121.573,-1.906,121.514,-1.888,121.395,-1.834,121.355,-1.878,121.349,-1.946,121.408,-1.97,121.502,-2.045,121.575,-2.151,121.622,-2.174,121.726,-2.208,121.77,-2.241,121.848,-2.332,121.972,-2.542,122.014,-2.656,122.083,-2.75,122.292,-2.908,122.303,-2.952,122.29,-3.004,122.307,-3.052,122.381,-3.142,122.399,-3.201,122.317,-3.275,122.313,-3.383,122.263,-3.527,122.251,-3.576,122.253,-3.62,122.288,-3.662,122.329,-3.694,122.385,-3.711,122.435,-3.74,122.529,-3.853,122.579,-3.882,122.61,-3.923,122.607,-3.985,122.65,-4.021,122.69,-4.084,122.75,-4.1,122.779,-4.082,122.798,-4.054,122.848,-4.065,122.877,-4.109,122.894,-4.166,122.9,-4.229,122.897,-4.349,122.872,-4.392,122.818,-4.39,122.72,-4.341,122.715,-4.376,122.722,-4.411,122.672,-4.422,122.615,-4.417,122.471,-4.422,122.207,-4.496,122.114,-4.54,122.054,-4.62,122.05,-4.675,122.073,-4.792,122.038,-4.832,121.917,-4.848,121.748,-4.817,121.646,-4.786,121.589,-4.76,121.514,-4.681,121.487,-4.581,121.541,-4.283,121.557,-4.245,121.583,-4.211,121.612,-4.156,121.618,-4.093,121.537,-4.015,121.416,-3.984,121.313,-3.919,120.914,-3.556,120.892,-3.521,120.891,-3.46,120.907,-3.404,121.038,-3.205,121.054,-3.167,121.07,-3.01,121.067,-2.881,121.052,-2.752,120.99,-2.67,120.879,-2.646,120.765,-2.642,120.654,-2.668,120.544,-2.733,120.341,-2.87,120.261,-2.949,120.254,-3.053,120.3,-3.154,120.36,-3.247,120.392,-3.348,120.437,-3.707,120.435,-3.748,120.383,-3.852,120.363,-4.086,120.385,-4.415,120.42,-4.617,120.405,-4.727,120.31,-4.963,120.281,-5.093,120.279,-5.146,120.391,-5.393,120.417,-5.49,120.43,-5.591,120.312,-5.542,120.256,-5.544,120.201,-5.559,120.077,-5.575,119.952,-5.578,119.908,-5.596,119.818,-5.662,119.764,-5.688,119.717,-5.693,119.557,-5.611,119.463,-5.522,119.376,-5.425,119.36,-5.314,119.391,-5.201,119.434,-5.079,119.52,-4.877,119.516,-4.742,119.545,-4.631,119.594,-4.523,119.612,-4.424,119.624,-4.034,119.611,-4,119.494,-3.769,119.48,-3.73,119.479,-3.667,119.492,-3.608,119.495,-3.554,119.467,-3.513,119.42,-3.475,119.362,-3.459,119.24,-3.475,118.995,-3.538,118.922,-3.483,118.868,-3.398,118.833,-3.28,118.813,-3.157,118.822,-3.041,118.858,-2.929,118.829,-2.85,118.784,-2.765,118.783,-2.721,118.809,-2.682,118.853,-2.65,118.908,-2.631,118.958,-2.597,119.092,-2.483,119.135,-2.382,119.138,-2.258,119.172,-2.14,119.241,-2.031,119.322,-1.93,119.348,-1.825,119.308,-1.66,119.324,-1.584,119.31,-1.496,119.309,-1.408,119.359,-1.243,119.508,-0.907,119.654,-0.728,119.711,-0.681,119.787,-0.764,119.844,-0.862,119.845,-0.773,119.83,-0.686,119.772,-0.484]},{"b":[105.243,-8.77,114.599,-5.904],"l":[110.607,-8.149,110.83,-8.202,111.055,-8.24,111.339,-8.262,111.51,-8.305,112.115,-8.324,112.352,-8.354,112.586,-8.4,112.679,-8.409,112.772,-8.396,112.898,-8.361,113.019,-8.313,113.134,-8.288,113.253,-8.287,113.693,-8.478,113.94,-8.568,114.16,-8.626,114.277,-8.615,114.339,-8.647,114.383,-8.705,114.459,-8.741,114.584,-8.77,114.599,-8.727,114.595,-8.685,114.482,-8.604,114.449,-8.559,114.387,-8.405,114.381,-8.334,114.385,-8.263,114.443,-8.005,114.444,-7.896,114.409,-7.792,114.383,-7.771,114.071,-7.633,114.037,-7.632,113.876,-7.677,113.747,-7.703,113.498,-7.724,113.248,-7.718,113.014,-7.658,112.795,-7.552,112.783,-7.432,112.794,-7.304,112.752,-7.265,112.649,-7.221,112.626,-7.178,112.587,-7.051,112.539,-6.926,112.434,-6.903,112.312,-6.894,112.137,-6.905,112.087,-6.893,111.99,-6.806,111.738,-6.773,111.688,-6.742,111.644,-6.699,111.54,-6.648,111.484,-6.652,111.387,-6.693,111.342,-6.7,111.182,-6.687,111.154,-6.669,111.001,-6.465,110.972,-6.436,110.835,-6.424,110.784,-6.443,110.737,-6.472,110.701,-6.518,110.674,-6.57,110.634,-6.69,110.584,-6.806,110.521,-6.897,110.426,-6.947,110.373,-6.948,110.321,-6.938,110.261,-6.912,110.198,-6.895,110.067,-6.899,109.936,-6.916,109.821,-6.902,109.587,-6.843,109.501,-6.81,109.404,-6.86,109.294,-6.867,109.018,-6.817,108.899,-6.808,108.78,-6.808,108.678,-6.791,108.604,-6.729,108.538,-6.516,108.516,-6.471,108.419,-6.383,108.33,-6.286,108.295,-6.265,108.254,-6.267,108.197,-6.289,108.138,-6.297,108.009,-6.277,107.884,-6.233,107.776,-6.219,107.667,-6.216,107.563,-6.183,107.475,-6.122,107.374,-6.008,107.332,-5.978,107.162,-5.957,107.046,-5.904,107.012,-6.008,106.932,-6.073,106.878,-6.092,106.825,-6.098,106.676,-6.038,106.569,-6.022,106.459,-6.018,106.35,-5.984,106.166,-5.965,106.075,-5.914,106.029,-5.934,105.936,-6.017,105.868,-6.116,105.787,-6.457,105.757,-6.48,105.706,-6.498,105.655,-6.47,105.608,-6.617,105.581,-6.671,105.484,-6.782,105.46,-6.787,105.405,-6.768,105.387,-6.751,105.371,-6.664,105.336,-6.674,105.273,-6.729,105.243,-6.778,105.255,-6.835,105.303,-6.841,105.362,-6.826,105.421,-6.833,105.478,-6.854,105.601,-6.86,105.725,-6.846,105.835,-6.846,105.944,-6.859,106.198,-6.928,106.52,-7.054,106.492,-7.114,106.448,-7.177,106.417,-7.239,106.411,-7.312,106.455,-7.369,106.535,-7.394,106.631,-7.416,107.071,-7.447,107.285,-7.472,107.547,-7.542,107.598,-7.567,107.696,-7.636,107.804,-7.688,107.917,-7.724,108.221,-7.782,108.336,-7.794,108.452,-7.797,108.518,-7.736,108.571,-7.707,108.741,-7.667,108.856,-7.668,108.987,-7.704,109.194,-7.695,109.282,-7.705,109.853,-7.828,110.039,-7.891,110.607,-8.149]},{"b":[95.207,-5.908,106.058,5.629],"l":[100.817,2.14,100.818,2.194,100.828,2.243,100.877,2.283,100.936,2.295,101.046,2.257,101.225,2.102,101.301,2.012,101.358,1.887,101.405,1.757,101.477,1.693,101.575,1.671,101.684,1.661,101.785,1.621,102.02,1.442,102.098,1.358,102.157,1.259,102.198,1.142,102.223,1.019,102.239,0.99,102.39,0.842,102.469,0.779,102.566,0.749,102.849,0.715,102.949,0.664,103.032,0.579,103.067,0.492,103.008,0.415,102.786,0.298,102.55,0.216,102.78,0.244,102.896,0.279,103.003,0.332,103.109,0.4,103.277,0.495,103.339,0.514,103.412,0.507,103.479,0.48,103.579,0.387,103.673,0.289,103.743,0.174,103.787,0.047,103.706,-0.02,103.589,-0.069,103.429,-0.192,103.412,-0.24,103.444,-0.272,103.405,-0.362,103.495,-0.418,103.509,-0.466,103.431,-0.534,103.439,-0.576,103.533,-0.755,103.578,-0.796,103.721,-0.887,103.94,-0.979,104.061,-1.021,104.199,-1.054,104.258,-1.053,104.361,-1.038,104.381,-1.074,104.426,-1.251,104.447,-1.362,104.478,-1.6,104.519,-1.699,104.516,-1.819,104.569,-1.922,104.676,-1.987,104.791,-2.041,104.845,-2.093,104.845,-2.172,104.826,-2.234,104.787,-2.283,104.668,-2.386,104.647,-2.43,104.631,-2.543,104.651,-2.595,104.698,-2.598,104.736,-2.571,104.878,-2.419,104.917,-2.392,104.971,-2.371,105.026,-2.358,105.287,-2.356,105.397,-2.38,105.495,-2.43,105.582,-2.492,105.899,-2.888,106.044,-3.106,106.056,-3.161,106.058,-3.217,106.034,-3.261,105.901,-3.41,105.885,-3.451,105.844,-3.614,105.852,-3.731,105.896,-3.78,105.93,-3.833,105.928,-3.881,105.841,-4.122,105.831,-4.163,105.887,-4.554,105.891,-4.66,105.879,-4.794,105.887,-5.01,105.816,-5.677,105.803,-5.716,105.748,-5.818,105.677,-5.818,105.619,-5.8,105.578,-5.761,105.556,-5.712,105.523,-5.673,105.349,-5.55,105.304,-5.57,105.128,-5.723,105.081,-5.746,105.023,-5.727,104.93,-5.681,104.64,-5.52,104.622,-5.572,104.618,-5.642,104.676,-5.816,104.684,-5.893,104.631,-5.908,104.602,-5.905,104.481,-5.803,104.37,-5.691,104.243,-5.539,104.15,-5.467,104.067,-5.386,103.831,-5.08,103.77,-5.033,103.406,-4.816,103.332,-4.765,103.239,-4.676,103.139,-4.596,102.919,-4.471,102.538,-4.152,102.372,-3.969,102.188,-3.675,102.128,-3.599,101.818,-3.378,101.649,-3.244,101.579,-3.167,101.414,-2.899,101.366,-2.808,101.306,-2.729,101.206,-2.664,101.119,-2.588,100.944,-2.345,100.89,-2.249,100.848,-2.144,100.855,-1.934,100.487,-1.299,100.394,-1.101,100.308,-0.827,100.289,-0.799,100.088,-0.553,100.017,-0.474,99.931,-0.4,99.86,-0.314,99.721,-0.033,99.67,0.045,99.598,0.102,99.335,0.209,99.236,0.268,99.159,0.352,99.112,0.459,99.06,0.686,98.936,1.032,98.796,1.495,98.703,1.702,98.595,1.865,98.564,1.902,98.087,2.195,98.005,2.238,97.919,2.264,97.795,2.283,97.701,2.359,97.662,2.494,97.641,2.676,97.617,2.785,97.591,2.847,97.391,2.975,97.313,3.077,97.248,3.189,97.188,3.276,96.969,3.575,96.894,3.654,96.801,3.709,96.525,3.767,96.445,3.816,96.311,3.986,96.23,4.073,95.988,4.263,95.579,4.662,95.495,4.761,95.432,4.865,95.381,4.976,95.207,5.284,95.221,5.346,95.247,5.411,95.243,5.464,95.224,5.517,95.228,5.565,95.28,5.593,95.396,5.629,95.517,5.625,95.629,5.609,95.737,5.579,95.841,5.515,96.027,5.351,96.133,5.294,96.251,5.267,96.493,5.229,96.615,5.22,96.843,5.274,96.968,5.269,97.086,5.23,97.19,5.207,97.451,5.236,97.5,5.228,97.547,5.206,97.588,5.17,97.707,5.04,97.908,4.88,97.967,4.777,98,4.662,98.021,4.635,98.248,4.415,98.273,4.322,98.241,4.195,98.307,4.093,98.528,3.998,98.659,3.928,98.687,3.886,98.706,3.835,98.778,3.759,98.869,3.71,99.151,3.581,99.521,3.311,99.732,3.183,99.907,2.988,99.969,2.895,100.021,2.794,100.127,2.648,100.307,2.467,100.353,2.411,100.401,2.332,100.457,2.257,100.524,2.189,100.604,2.137,100.685,2.12,100.817,1.989,100.888,1.948,100.877,2.051,100.817,2.14]},{"b":[108.906,-4.17,119.266,6.99],"l":[112.119,2.915,111.728,2.854,111.623,2.818,111.513,2.743,111.443,2.634,111.44,2.498,111.451,2.424,111.444,2.382,111.406,2.368,111.351,2.364,111.296,2.399,111.242,2.436,111.209,2.38,111.196,2.297,111.209,2.198,111.268,2.14,111.251,2.064,111.198,1.985,111.17,1.902,111.154,1.739,111.11,1.684,111.047,1.634,111.029,1.558,111.058,1.487,111.123,1.449,111.223,1.396,111.145,1.387,111.098,1.401,110.94,1.517,110.895,1.532,110.782,1.521,110.675,1.548,110.4,1.7,110.349,1.72,110.298,1.701,110.246,1.695,110.114,1.699,109.985,1.718,109.865,1.764,109.72,1.858,109.694,1.889,109.629,2.028,109.379,1.923,109.318,1.821,109.273,1.705,109.167,1.607,109.076,1.496,109.055,1.438,109.01,1.24,109.065,1.247,109.096,1.258,109.132,1.254,109.088,1.224,109.031,1.204,108.959,1.135,108.917,0.913,108.906,0.794,108.923,0.533,108.945,0.356,109.075,0.253,109.149,0.168,109.181,0.117,109.22,0.074,109.247,0.056,109.258,0.031,109.195,-0.009,109.165,-0.142,109.15,-0.186,109.122,-0.265,109.121,-0.391,109.13,-0.445,109.161,-0.495,109.257,-0.577,109.373,-0.638,109.366,-0.667,109.312,-0.68,109.271,-0.732,109.259,-0.807,109.289,-0.846,109.333,-0.875,109.454,-0.869,109.682,-0.944,109.787,-1.011,109.873,-1.101,109.938,-1.181,109.983,-1.275,110.019,-1.399,110.036,-1.526,110.023,-1.643,109.964,-1.743,109.96,-1.863,110.075,-1.946,110.097,-2.001,110.124,-2.234,110.224,-2.689,110.233,-2.925,110.256,-2.966,110.303,-2.985,110.351,-2.947,110.378,-2.934,110.574,-2.891,110.668,-3.005,110.703,-3.021,110.736,-2.989,110.811,-2.938,110.899,-2.909,110.852,-2.946,110.83,-2.995,110.869,-3.049,110.93,-3.071,111.044,-3.056,111.259,-2.956,111.368,-2.934,111.495,-2.973,111.625,-2.975,111.658,-2.926,111.695,-2.889,111.76,-2.939,111.809,-3.008,111.823,-3.057,111.836,-3.308,111.834,-3.42,111.822,-3.533,111.858,-3.552,111.907,-3.553,111.955,-3.53,112.127,-3.381,112.285,-3.321,112.444,-3.371,112.6,-3.4,112.758,-3.322,112.971,-3.187,113.034,-2.933,113.343,-3.246,113.367,-3.224,113.409,-3.229,113.526,-3.184,113.566,-3.178,113.61,-3.196,113.63,-3.246,113.637,-3.332,113.634,-3.42,113.705,-3.455,113.796,-3.456,113.959,-3.394,114.082,-3.279,114.109,-3.285,114.128,-3.327,114.178,-3.354,114.236,-3.361,114.293,-3.306,114.344,-3.235,114.302,-3.365,114.305,-3.41,114.344,-3.444,114.397,-3.471,114.446,-3.482,114.526,-3.377,114.536,-3.494,114.606,-3.703,114.625,-4.112,114.653,-4.152,114.694,-4.17,115.258,-3.907,115.956,-3.595,115.999,-3.523,116.017,-3.433,116.058,-3.348,116.15,-3.233,116.167,-3.183,116.205,-3.149,116.257,-3.126,116.172,-3.025,116.154,-2.984,116.166,-2.935,116.226,-2.977,116.289,-2.959,116.331,-2.902,116.353,-2.833,116.372,-2.707,116.375,-2.578,116.307,-2.603,116.317,-2.552,116.353,-2.522,116.401,-2.52,116.45,-2.538,116.529,-2.511,116.549,-2.411,116.565,-2.3,116.528,-2.208,116.418,-2.187,116.369,-2.158,116.314,-2.14,116.424,-2.053,116.452,-1.923,116.43,-1.864,116.424,-1.785,116.353,-1.779,116.275,-1.785,116.3,-1.744,116.332,-1.712,116.478,-1.633,116.518,-1.598,116.545,-1.553,116.554,-1.474,116.612,-1.429,116.715,-1.376,116.753,-1.327,116.771,-1.267,116.759,-1.207,116.729,-1.151,116.726,-1.098,116.74,-1.044,116.761,-1.117,116.797,-1.184,116.849,-1.218,116.914,-1.224,117.003,-1.188,117.07,-1.113,117.146,-1.009,117.241,-0.926,117.357,-0.867,117.522,-0.797,117.563,-0.771,117.574,-0.728,117.557,-0.675,117.549,-0.554,117.463,-0.324,117.464,-0.2,117.522,0.236,117.553,0.341,117.745,0.73,117.777,0.754,117.853,0.789,117.923,0.831,117.964,0.89,117.977,0.964,117.952,1.032,117.912,1.099,118.016,1.039,118.096,0.929,118.196,0.874,118.312,0.847,118.535,0.814,118.757,0.839,118.892,0.887,118.985,0.982,118.963,1.044,118.853,1.096,118.639,1.319,118.472,1.416,118.157,1.64,118.08,1.702,117.928,1.867,117.865,1.968,117.831,2.002,117.789,2.027,117.881,2.061,117.889,2.087,117.957,2.16,118.042,2.215,118.066,2.263,118.067,2.318,118.034,2.378,117.886,2.542,117.805,2.669,117.786,2.747,117.75,2.776,117.667,2.807,117.639,2.825,117.665,2.859,117.698,2.887,117.637,2.915,117.569,2.929,117.638,2.951,117.612,3.005,117.611,3.064,117.567,3.098,117.507,3.105,117.422,3.165,117.352,3.194,117.322,3.244,117.385,3.365,117.346,3.427,117.166,3.592,117.114,3.613,117.056,3.623,117.172,3.639,117.288,3.639,117.45,3.629,117.495,3.666,117.51,3.73,117.567,3.678,117.63,3.636,117.714,3.645,117.777,3.689,117.762,3.734,117.732,3.77,117.728,3.797,117.639,3.878,117.566,3.93,117.559,3.988,117.465,4.076,117.497,4.133,117.566,4.162,117.604,4.2,117.65,4.304,117.696,4.343,117.741,4.338,117.896,4.263,118.008,4.25,118.117,4.288,118.229,4.316,118.364,4.336,118.498,4.362,118.548,4.379,118.586,4.41,118.595,4.461,118.563,4.502,118.324,4.669,118.185,4.829,118.261,4.989,118.32,5.012,118.382,5.019,118.551,4.968,118.672,4.964,118.913,5.023,119.132,5.1,119.22,5.16,119.25,5.199,119.263,5.246,119.266,5.308,119.256,5.366,119.223,5.413,119.178,5.431,119.05,5.415,119.003,5.418,118.957,5.429,118.714,5.559,118.595,5.592,118.563,5.685,118.514,5.729,118.456,5.763,118.353,5.806,118.3,5.82,118.249,5.821,118.145,5.754,118.031,5.712,117.974,5.706,117.928,5.769,117.935,5.788,118.072,5.832,118.116,5.863,118.062,5.983,118.004,6.053,117.896,5.972,117.818,5.94,117.501,5.885,117.617,5.941,117.645,6.002,117.65,6.074,117.616,6.197,117.696,6.272,117.694,6.35,117.67,6.427,117.646,6.474,117.61,6.513,117.499,6.571,117.38,6.612,117.294,6.677,117.255,6.783,117.245,6.833,117.252,6.919,117.23,6.94,117.129,6.969,117.078,6.917,117.019,6.797,116.913,6.66,116.812,6.608,116.788,6.606,116.808,6.691,116.842,6.772,116.85,6.827,116.833,6.952,116.776,6.99,116.75,6.977,116.538,6.583,116.495,6.522,116.138,6.13,116.11,6.003,116.06,5.882,115.918,5.725,115.877,5.614,115.797,5.536,115.741,5.533,115.685,5.535,115.625,5.549,115.604,5.603,115.556,5.567,115.419,5.413,115.422,5.331,115.467,5.254,115.582,5.194,115.554,5.094,115.52,5.049,115.428,4.969,115.375,4.933,115.14,4.9,115.027,4.9,115.047,4.962,115.048,5.016,114.995,5.022,114.841,4.946,114.741,4.881,114.646,4.798,114.545,4.725,114.424,4.66,114.299,4.607,114.178,4.591,114.054,4.593,114.013,4.575,113.984,4.546,113.99,4.483,113.988,4.421,113.953,4.289,113.924,4.243,113.712,4.001,113.446,3.741,113.32,3.561,113.14,3.344,113.045,3.205,112.988,3.162,112.921,3.131,112.737,3.07,112.119,2.915]},{"b":[53.763,73.297,68.942,77.012],"l":[58.618,74.227,58.665,74.289,58.646,74.328,58.562,74.422,58.502,74.464,58.535,74.499,58.928,74.463,59.04,74.486,59.101,74.508,59.146,74.552,59.157,74.611,59.182,74.666,59.24,74.693,59.596,74.614,59.674,74.61,59.753,74.637,59.771,74.664,59.735,74.695,59.747,74.746,59.982,74.745,60.08,74.756,60.222,74.797,60.301,74.837,60.439,74.875,60.501,74.905,60.455,74.946,60.241,74.971,60.277,75.008,60.476,75.055,60.534,75.059,60.655,75.055,60.719,75.069,60.829,75.111,60.936,75.164,61.147,75.223,61.249,75.281,61.356,75.315,61.487,75.311,61.616,75.32,62.066,75.428,63.046,75.576,63.317,75.603,63.659,75.669,63.779,75.673,64.263,75.72,64.745,75.788,65.202,75.839,65.619,75.905,66.282,75.984,66.657,76.047,66.893,76.072,67.127,76.108,67.365,76.161,67.765,76.238,68.165,76.285,68.222,76.313,68.559,76.449,68.9,76.573,68.858,76.61,68.891,76.66,68.942,76.708,68.912,76.761,68.873,76.79,68.699,76.871,68.486,76.934,68.017,76.991,67.652,77.012,67.535,77.008,67.264,76.964,66.829,76.924,66.345,76.821,66.063,76.746,65.959,76.688,65.863,76.613,65.755,76.579,65.637,76.579,65.528,76.568,65.31,76.518,65.197,76.5,65.073,76.497,64.95,76.484,64.708,76.426,64.463,76.378,63.526,76.31,62.971,76.237,62.782,76.245,62.471,76.23,62.237,76.242,61.787,76.291,61.569,76.298,61.202,76.282,61.157,76.274,61.034,76.233,61.037,76.169,61.054,76.12,60.998,76.089,60.942,76.071,60.801,76.069,60.731,76.104,60.606,76.109,60.279,76.096,60.118,76.067,60.036,75.984,59.782,75.946,59.347,75.907,59.11,75.874,58.995,75.872,58.881,75.855,58.653,75.777,58.418,75.72,58.058,75.663,58.073,75.619,58.094,75.593,57.783,75.507,57.708,75.454,57.632,75.356,57.607,75.341,57.302,75.373,57.088,75.384,56.989,75.375,56.844,75.351,56.809,75.328,56.829,75.278,56.876,75.244,56.57,75.098,56.485,75.096,56.389,75.138,56.289,75.164,56.162,75.187,56.036,75.194,55.921,75.168,55.81,75.125,55.821,75.091,55.863,75.059,55.998,75.003,56.34,75.013,56.429,74.973,56.499,74.957,56.218,74.898,55.914,74.796,55.66,74.656,55.582,74.628,55.61,74.591,55.662,74.556,55.752,74.541,55.947,74.542,56.137,74.496,56.078,74.481,55.416,74.436,55.341,74.42,55.023,74.187,54.92,74.129,54.831,74.096,54.733,74.034,54.643,73.96,54.606,73.951,54.386,73.936,54.174,73.886,53.963,73.822,53.851,73.801,53.763,73.766,53.839,73.697,54.205,73.542,54.132,73.481,54.3,73.351,54.566,73.419,54.769,73.449,55.007,73.454,55.28,73.392,55.549,73.357,56.035,73.346,56.228,73.314,56.43,73.297,56.634,73.304,56.964,73.367,57.134,73.504,57.46,73.61,57.543,73.658,57.464,73.746,57.291,73.815,57.313,73.838,57.449,73.826,57.604,73.775,57.657,73.768,57.756,73.769,57.845,73.805,57.872,73.85,57.853,73.898,57.778,73.974,57.767,74.014,58.441,74.129,58.618,74.227]},{"b":[51.429,70.563,57.625,73.388],"l":[51.583,72.071,51.653,72.099,51.805,72.142,51.885,72.153,52.069,72.131,52.252,72.13,52.332,72.154,52.407,72.197,52.462,72.252,52.586,72.284,52.622,72.301,52.662,72.337,52.706,72.391,52.714,72.437,52.749,72.483,52.864,72.55,52.823,72.591,52.839,72.619,52.917,72.669,52.683,72.682,52.605,72.704,52.529,72.737,52.551,72.769,52.579,72.791,52.812,72.875,52.913,72.9,53.024,72.914,53.135,72.913,53.254,72.904,53.37,72.917,53.247,72.973,53.237,73.011,53.189,73.104,53.198,73.148,53.251,73.183,53.358,73.225,53.512,73.238,53.634,73.26,53.753,73.293,53.866,73.299,54.091,73.276,54.202,73.281,54.328,73.299,54.676,73.37,54.804,73.388,54.941,73.383,55.121,73.357,55.32,73.308,55.787,73.269,56.138,73.256,56.35,73.226,56.43,73.201,56.397,73.139,56.335,73.114,56.189,73.033,56.167,72.983,56.193,72.905,56.171,72.848,56.122,72.807,56.084,72.789,55.82,72.79,55.723,72.766,55.718,72.722,55.701,72.672,55.616,72.599,55.441,72.575,55.403,72.549,55.417,72.501,55.356,72.465,55.36,72.409,55.39,72.378,55.399,72.314,55.518,72.221,55.495,72.182,55.403,72.107,55.375,72.015,55.298,71.935,55.471,71.869,55.547,71.783,55.614,71.69,55.819,71.508,56.043,71.346,56.454,71.107,56.895,70.927,57.066,70.876,57.484,70.792,57.556,70.766,57.625,70.729,57.447,70.661,57.264,70.636,57.247,70.605,57.146,70.589,56.649,70.647,56.622,70.655,56.569,70.697,56.51,70.729,56.386,70.734,56.26,70.715,56.335,70.677,56.417,70.665,56.561,70.594,56.5,70.566,56.435,70.563,56.142,70.658,56.115,70.646,56.087,70.618,55.942,70.649,55.907,70.626,55.797,70.616,55.707,70.642,55.706,70.675,55.687,70.692,55.237,70.666,55.052,70.667,54.867,70.678,54.645,70.742,54.608,70.713,54.601,70.68,54.517,70.693,54.333,70.745,54.199,70.765,53.722,70.814,53.384,70.874,53.468,70.901,53.614,70.915,53.616,70.951,53.593,71.001,53.588,71.052,53.671,71.087,53.857,71.07,53.834,71.127,53.922,71.138,54.094,71.105,54.156,71.125,53.886,71.196,53.591,71.297,53.622,71.333,53.515,71.343,53.41,71.34,53.319,71.399,53.333,71.477,53.412,71.53,53.364,71.542,52.909,71.495,52.679,71.506,52.419,71.537,52.18,71.49,51.938,71.475,51.813,71.491,51.692,71.525,51.59,71.571,51.511,71.648,51.439,71.777,51.429,71.826,51.444,71.934,51.482,71.98,51.583,72.071]},{"b":[141.661,45.917,144.714,54.416],"l":[142.335,54.281,142.467,54.149,142.67,53.968,142.706,53.896,142.683,53.816,142.643,53.737,142.689,53.73,142.68,53.674,142.553,53.653,142.509,53.588,142.583,53.537,142.526,53.447,142.424,53.411,142.371,53.403,142.319,53.405,142.18,53.484,142.142,53.496,141.964,53.456,141.852,53.389,141.824,53.34,141.839,53.138,141.874,53.039,141.856,52.794,141.803,52.556,141.748,52.455,141.682,52.359,141.661,52.273,141.668,51.933,141.721,51.847,141.808,51.789,141.772,51.752,141.722,51.736,141.772,51.69,141.873,51.63,142.006,51.521,142.091,51.429,142.207,51.223,142.208,50.998,142.147,50.89,142.1,50.776,142.066,50.63,142.071,50.515,142.143,50.312,142.153,50.217,142.142,49.569,142.109,49.44,142.067,49.312,142.02,49.078,141.98,48.972,141.866,48.75,141.873,48.702,141.897,48.655,142.029,48.477,142.135,48.29,142.182,48.013,142.149,47.902,142.076,47.808,142.016,47.701,141.964,47.587,141.963,47.544,141.984,47.348,142.017,47.245,142.039,47.14,142.011,47.03,141.867,46.694,141.83,46.451,141.916,46.171,141.93,46.088,141.962,46.013,142.016,45.962,142.077,45.917,142.15,45.999,142.209,46.089,142.304,46.358,142.35,46.459,142.406,46.555,142.479,46.644,142.578,46.701,142.636,46.716,142.692,46.711,142.747,46.671,142.796,46.62,142.829,46.605,143.048,46.593,143.282,46.559,143.352,46.476,143.37,46.358,143.419,46.222,143.432,46.029,143.463,46.069,143.482,46.116,143.491,46.175,143.509,46.23,143.581,46.361,143.579,46.406,143.54,46.575,143.486,46.752,143.447,46.792,143.384,46.806,143.319,46.807,143.218,46.795,143.178,46.844,143.089,47.001,143.006,47.223,142.94,47.323,142.905,47.362,142.864,47.392,142.801,47.416,142.745,47.452,142.67,47.537,142.579,47.684,142.557,47.738,142.546,47.885,142.574,48.072,142.651,48.247,142.972,48.918,143.027,49.105,143.105,49.199,143.236,49.263,143.382,49.291,143.732,49.312,143.819,49.309,143.968,49.276,144.049,49.249,144.125,49.209,144.284,49.07,144.412,48.986,144.536,48.894,144.621,48.815,144.673,48.679,144.714,48.64,144.707,48.82,144.686,48.871,144.607,48.936,144.432,49.051,144.341,49.181,144.272,49.311,144.24,49.432,144.2,49.55,144.141,49.661,144.048,49.896,143.816,50.283,143.736,50.507,143.534,51.246,143.489,51.277,143.473,51.299,143.467,51.402,143.455,51.471,143.418,51.521,143.321,51.583,143.3,51.632,143.295,51.744,143.251,51.848,143.191,51.944,143.156,52.084,143.172,52.349,143.201,52.443,143.264,52.479,143.295,52.529,143.324,52.614,143.333,52.7,143.325,52.963,143.288,53.134,143.26,53.217,143.224,53.296,143.096,53.489,142.918,53.794,142.936,53.811,142.911,53.878,142.927,53.956,142.967,54.029,142.986,54.086,142.976,54.141,142.761,54.394,142.693,54.416,142.666,54.358,142.616,54.304,142.552,54.279,142.335,54.281]},{"b":[139.821,41.423,145.833,45.51],"l":[145.214,43.578,145.14,43.663,145.101,43.765,145.126,43.869,145.245,44.076,145.352,44.23,145.37,44.281,145.37,44.327,145.343,44.334,145.102,44.166,144.872,43.982,144.799,43.94,144.715,43.928,144.597,43.93,144.482,43.95,144.101,44.102,144.005,44.117,143.95,44.112,143.824,44.117,143.759,44.132,143.655,44.221,143.512,44.278,143.289,44.397,143.075,44.535,142.885,44.67,142.704,44.819,142.416,45.125,142.172,45.326,142.016,45.438,141.981,45.483,141.938,45.51,141.879,45.483,141.829,45.439,141.778,45.419,141.668,45.401,141.654,45.377,141.653,45.349,141.594,45.256,141.583,45.156,141.656,45.051,141.719,44.941,141.782,44.716,141.761,44.483,141.716,44.371,141.661,44.264,141.645,44.019,141.601,43.919,141.447,43.749,141.398,43.643,141.398,43.513,141.412,43.381,141.374,43.28,141.296,43.2,141.245,43.185,141.138,43.18,140.954,43.201,140.819,43.205,140.781,43.215,140.585,43.312,140.487,43.338,140.392,43.303,140.379,43.237,140.397,43.167,140.486,43.05,140.432,42.954,140.329,42.867,140.224,42.796,140.115,42.733,140.015,42.685,139.951,42.671,139.891,42.649,139.86,42.582,139.829,42.448,139.821,42.388,139.835,42.278,139.895,42.19,140.024,42.1,140.057,42.067,140.108,41.913,140.084,41.803,140.021,41.696,139.995,41.576,140.009,41.521,140.037,41.474,140.085,41.434,140.149,41.423,140.27,41.456,140.385,41.519,140.432,41.567,140.489,41.672,140.593,41.769,140.66,41.816,140.816,41.76,140.908,41.743,141,41.737,141.079,41.76,141.151,41.805,141.108,41.848,140.912,41.978,140.734,42.116,140.684,42.123,140.578,42.119,140.528,42.132,140.417,42.201,140.327,42.293,140.315,42.334,140.324,42.376,140.351,42.435,140.385,42.487,140.48,42.559,140.548,42.57,140.617,42.571,140.71,42.556,140.788,42.5,140.948,42.36,140.986,42.342,141.407,42.547,141.851,42.579,142.088,42.472,142.508,42.258,142.906,42.118,143.112,42.022,143.237,42,143.279,42.038,143.314,42.084,143.327,42.151,143.332,42.22,143.369,42.325,143.429,42.419,143.581,42.599,143.762,42.748,143.969,42.881,144.197,42.974,144.302,42.984,144.516,42.944,144.631,42.947,144.807,42.994,144.921,43.001,145.029,43.032,145.127,43.089,145.23,43.135,145.347,43.177,145.405,43.18,145.505,43.174,145.537,43.193,145.624,43.291,145.726,43.343,145.833,43.386,145.751,43.396,145.674,43.389,145.583,43.328,145.488,43.28,145.436,43.282,145.341,43.303,145.273,43.463,145.214,43.578]},{"b":[130.889,33.487,141.993,41.506],"l":[133.142,34.302,133.019,34.33,132.775,34.255,132.657,34.246,132.534,34.287,132.421,34.353,132.313,34.325,132.238,34.227,132.202,34.032,132.159,33.944,132.146,33.839,132.09,33.855,131.763,34.045,131.741,34.052,131.476,34.019,131.408,34.004,131.323,33.965,131.233,33.948,131.15,33.976,131.072,34.021,130.996,34.007,130.919,33.976,130.889,34.262,130.904,34.3,130.952,34.35,131.004,34.393,131.132,34.407,131.262,34.393,131.354,34.413,131.433,34.47,131.515,34.55,131.608,34.615,131.734,34.667,131.856,34.726,131.963,34.809,132.065,34.9,132.158,34.967,132.26,35.022,132.414,35.156,132.619,35.307,132.698,35.418,132.747,35.449,132.923,35.511,133.157,35.559,133.267,35.557,133.376,35.459,133.435,35.472,133.495,35.497,133.615,35.511,133.739,35.495,133.86,35.495,133.981,35.507,134.214,35.539,134.337,35.578,134.456,35.628,134.882,35.663,135.174,35.747,135.221,35.741,135.265,35.722,135.269,35.66,135.232,35.592,135.268,35.551,135.327,35.526,135.602,35.518,135.68,35.503,135.795,35.55,135.903,35.607,136.016,35.683,136.095,35.768,136.022,35.874,136.006,35.991,136.067,36.117,136.156,36.223,136.262,36.288,136.359,36.362,136.556,36.572,136.698,36.742,136.749,36.951,136.719,37.198,136.843,37.382,136.962,37.414,137.199,37.497,137.323,37.522,137.341,37.485,137.338,37.437,137.152,37.283,137.046,37.22,136.982,37.2,136.924,37.172,136.9,37.118,136.994,37.027,137.019,36.96,137.013,36.895,137.017,36.837,137.124,36.774,137.246,36.753,137.298,36.754,137.343,36.77,137.483,36.925,137.514,36.952,137.913,37.065,138.11,37.151,138.218,37.173,138.32,37.218,138.548,37.392,138.633,37.472,138.709,37.561,138.771,37.663,138.819,37.775,138.885,37.844,139.247,38.009,139.364,38.099,139.401,38.143,139.446,38.269,139.477,38.4,139.521,38.503,139.58,38.599,139.66,38.697,139.749,38.788,139.802,38.882,139.879,39.105,139.912,39.229,139.939,39.273,139.977,39.311,140.011,39.358,140.037,39.411,140.048,39.464,140.065,39.624,140.055,39.749,139.995,39.855,139.945,39.885,139.891,39.887,139.81,39.878,139.742,39.921,139.755,39.959,139.826,39.966,139.874,39.986,139.908,40.022,139.972,40.137,140.011,40.26,140.014,40.315,139.964,40.414,139.924,40.534,139.923,40.598,139.967,40.673,140.029,40.733,140.085,40.747,140.146,40.752,140.201,40.775,140.252,40.809,140.281,40.846,140.326,40.948,140.344,41.006,140.315,41.161,140.344,41.203,140.386,41.23,140.441,41.21,140.498,41.206,140.564,41.212,140.628,41.195,140.64,41.156,140.679,40.893,140.702,40.858,140.749,40.83,140.801,40.834,140.846,40.875,140.876,40.93,140.936,40.941,141.119,40.882,141.183,40.924,141.225,40.988,141.262,41.103,141.244,41.206,141.2,41.244,141.155,41.237,141.115,41.208,141.07,41.193,140.801,41.139,140.802,41.254,140.86,41.425,140.892,41.48,140.937,41.506,141.05,41.476,141.106,41.456,141.229,41.373,141.269,41.354,141.455,41.405,141.42,41.251,141.4,41.096,141.414,40.839,141.43,40.723,141.463,40.611,141.542,40.531,141.646,40.474,141.797,40.291,141.878,40.067,141.935,39.958,141.978,39.844,141.991,39.792,141.992,39.74,141.979,39.668,141.993,39.611,141.977,39.429,141.909,39.219,141.901,39.111,141.842,39.09,141.807,39.04,141.776,39.017,141.742,39,141.694,38.995,141.659,38.975,141.645,38.918,141.622,38.865,141.58,38.817,141.546,38.763,141.519,38.632,141.509,38.498,141.467,38.404,141.368,38.38,141.254,38.381,141.108,38.338,141.077,38.313,140.962,38.149,140.929,38.053,140.928,37.95,140.96,37.823,141.003,37.698,141.036,37.467,141.002,37.115,140.968,37.002,140.895,36.926,140.84,36.89,140.792,36.847,140.73,36.732,140.627,36.503,140.619,36.445,140.619,36.386,140.592,36.308,140.574,36.231,140.59,36.142,140.622,36.059,140.76,35.846,140.813,35.783,140.874,35.725,140.639,35.661,140.597,35.632,140.457,35.51,140.413,35.395,140.417,35.267,140.393,35.221,140.355,35.181,140.315,35.155,140.159,35.096,140.059,35.038,139.96,34.947,139.92,34.9,139.844,34.915,139.799,34.957,139.843,35.01,139.83,35.072,139.851,35.232,139.826,35.297,139.906,35.345,139.944,35.423,140.027,35.485,140.086,35.54,140.097,35.585,140.044,35.633,139.988,35.668,139.91,35.668,139.835,35.658,139.786,35.612,139.77,35.55,139.774,35.52,139.768,35.495,139.65,35.409,139.666,35.319,139.7,35.274,139.744,35.252,139.731,35.222,139.675,35.149,139.636,35.142,139.564,35.243,139.474,35.299,139.363,35.298,139.249,35.278,139.163,35.211,139.134,35.155,139.116,35.097,139.122,34.956,139.086,34.839,139.016,34.736,138.983,34.698,138.897,34.628,138.838,34.619,138.795,34.651,138.761,34.699,138.804,34.876,138.803,34.975,138.904,35.025,138.821,35.096,138.72,35.124,138.577,35.086,138.537,35.044,138.51,34.987,138.433,34.915,138.349,34.848,138.253,34.733,138.189,34.596,137.979,34.641,137.864,34.651,137.749,34.647,137.543,34.664,137.318,34.636,137.062,34.583,137.077,34.621,137.288,34.704,137.295,34.728,137.275,34.773,137.223,34.775,137.097,34.759,137.032,34.766,137.006,34.814,136.963,34.835,136.935,34.815,136.944,34.722,136.913,34.709,136.871,34.733,136.885,34.806,136.856,34.913,136.853,34.979,136.897,35.036,136.852,35.06,136.804,35.05,136.744,35.023,136.69,34.984,136.577,34.79,136.533,34.678,136.616,34.589,136.842,34.464,136.88,34.434,136.881,34.38,136.854,34.324,136.792,34.299,136.544,34.258,136.33,34.177,136.268,34.095,136.073,33.778,135.916,33.562,135.695,33.487,135.453,33.553,135.394,33.629,135.347,33.722,135.257,33.806,135.175,33.898,135.128,34.007,135.135,34.183,135.1,34.288,135.132,34.317,135.266,34.381,135.309,34.417,135.385,34.5,135.412,34.547,135.416,34.617,135.355,34.654,135.198,34.653,135.042,34.631,134.93,34.662,134.785,34.747,134.74,34.765,134.584,34.771,134.472,34.755,134.363,34.724,134.247,34.714,134.208,34.698,134.075,34.593,133.968,34.527,133.876,34.495,133.678,34.486,133.579,34.465,133.474,34.43,133.445,34.433,133.336,34.385,133.21,34.344,133.142,34.302]},{"b":[132.033,32.752,134.739,34.358],"l":[134.695,33.928,134.655,33.983,134.635,34.044,134.638,34.227,134.496,34.215,134.357,34.256,134.219,34.319,134.076,34.358,133.948,34.348,133.826,34.307,133.706,34.237,133.656,34.233,133.603,34.244,133.643,34.135,133.627,34.069,133.582,34.017,133.472,33.973,133.35,33.977,133.299,33.969,133.193,33.933,133.134,33.927,133.051,33.997,132.99,34.088,132.935,34.095,132.839,34.021,132.784,33.992,132.752,33.906,132.716,33.852,132.699,33.791,132.643,33.69,132.536,33.633,132.366,33.512,132.288,33.47,132.114,33.395,132.033,33.34,132.086,33.34,132.281,33.417,132.375,33.434,132.413,33.43,132.405,33.331,132.445,33.305,132.511,33.293,132.515,33.255,132.505,33.211,132.477,33.181,132.476,33.126,132.428,33.059,132.493,33.008,132.495,32.917,132.602,32.92,132.709,32.902,132.642,32.762,132.692,32.776,132.804,32.752,132.87,32.755,132.977,32.842,133.016,32.984,133.051,33.012,133.101,33.028,133.146,33.083,133.24,33.25,133.286,33.36,133.632,33.511,133.686,33.516,133.854,33.493,133.959,33.448,134.124,33.287,134.182,33.247,134.206,33.347,134.243,33.439,134.307,33.527,134.377,33.608,134.549,33.729,134.739,33.821,134.675,33.848,134.695,33.928]},{"b":[129.58,31.015,132.009,33.928],"l":[131.977,32.844,132.002,32.882,132.009,32.919,131.91,32.974,131.937,33.01,131.949,33.047,131.903,33.088,131.848,33.118,131.855,33.182,131.897,33.255,131.717,33.252,131.537,33.274,131.616,33.392,131.711,33.502,131.724,33.554,131.696,33.603,131.643,33.638,131.583,33.652,131.499,33.624,131.419,33.584,131.366,33.571,131.309,33.573,131.175,33.603,131.058,33.673,131.009,33.776,130.953,33.872,130.84,33.918,130.716,33.928,130.67,33.915,130.484,33.835,130.457,33.789,130.439,33.734,130.365,33.634,130.275,33.598,130.168,33.598,130.131,33.578,130.103,33.54,130.072,33.522,129.919,33.483,129.826,33.437,129.837,33.404,129.858,33.375,129.844,33.322,129.702,33.36,129.66,33.365,129.61,33.344,129.58,33.236,129.665,33.187,129.799,33.084,129.897,33.022,129.922,32.988,129.992,32.852,129.901,32.852,129.828,32.893,129.778,32.986,129.679,33.06,129.662,32.995,129.668,32.929,129.69,32.875,129.786,32.782,129.827,32.725,129.808,32.645,129.769,32.571,129.853,32.622,129.951,32.722,130.054,32.771,130.152,32.748,130.193,32.706,130.246,32.677,130.298,32.675,130.34,32.702,130.361,32.756,130.354,32.81,130.326,32.853,130.28,32.867,130.222,32.847,130.175,32.851,130.168,32.932,130.173,33.013,130.127,33.105,130.177,33.145,130.238,33.178,130.287,33.155,130.382,33.093,130.44,32.951,130.547,32.832,130.569,32.734,130.498,32.657,130.563,32.626,130.641,32.619,130.56,32.456,130.462,32.305,130.395,32.219,130.319,32.144,130.214,32.115,130.194,32.091,130.196,31.95,130.211,31.849,130.188,31.769,130.224,31.73,130.269,31.696,130.322,31.601,130.307,31.488,130.294,31.451,130.261,31.437,130.147,31.408,130.201,31.292,130.251,31.273,130.311,31.267,130.589,31.179,130.621,31.218,130.645,31.267,130.566,31.352,130.54,31.403,130.528,31.46,130.556,31.563,130.613,31.665,130.655,31.718,130.715,31.718,130.776,31.706,130.797,31.671,130.796,31.624,130.78,31.604,130.749,31.598,130.704,31.577,130.709,31.526,130.774,31.383,130.79,31.269,130.758,31.156,130.736,31.122,130.704,31.094,130.684,31.059,130.686,31.015,130.902,31.112,131.098,31.256,131.035,31.378,131.071,31.437,131.14,31.442,131.25,31.41,131.337,31.405,131.46,31.671,131.475,31.778,131.46,31.883,131.506,32.002,131.531,32.117,131.565,32.223,131.61,32.325,131.66,32.466,131.732,32.593,131.977,32.844]},{"b":[120.072,21.925,121.929,25.277],"l":[121.905,25.056,121.853,25.104,121.733,25.154,121.687,25.182,121.643,25.232,121.594,25.275,121.517,25.277,121.45,25.249,121.365,25.159,121.095,25.065,121.041,25.033,120.964,24.928,120.902,24.813,120.836,24.723,120.757,24.642,120.63,24.479,120.159,23.709,120.132,23.653,120.125,23.527,120.143,23.399,120.121,23.305,120.086,23.212,120.072,23.15,120.083,23.094,120.122,23.037,120.15,22.975,120.233,22.718,120.273,22.627,120.326,22.542,120.316,22.548,120.388,22.485,120.48,22.442,120.581,22.356,120.608,22.313,120.678,22.16,120.69,22.033,120.743,21.956,120.84,21.925,120.864,22.033,120.878,22.142,120.877,22.262,120.897,22.379,120.947,22.503,121.009,22.62,121.161,22.776,121.296,22.967,121.352,23.067,121.397,23.173,121.477,23.424,121.526,23.668,121.583,23.861,121.613,24.053,121.639,24.13,121.737,24.285,121.828,24.534,121.826,24.641,121.813,24.746,121.82,24.825,121.856,24.895,121.929,24.974,121.905,25.056]},{"b":[108.636,18.218,111.014,20.138],"l":[110.971,19.883,110.938,19.948,110.889,19.992,110.809,20.014,110.745,20.059,110.679,20.137,110.652,20.138,110.598,20.098,110.589,20.072,110.588,19.976,110.418,20.055,110.394,20.059,110.388,20.018,110.392,19.976,110.344,20.039,110.213,20.056,110.172,20.054,110.083,19.993,109.906,19.963,109.651,19.984,109.584,19.97,109.514,19.904,109.418,19.889,109.315,19.904,109.263,19.883,109.219,19.843,109.177,19.768,109.22,19.757,109.277,19.761,109.179,19.674,109.063,19.614,108.903,19.481,108.791,19.418,108.694,19.338,108.666,19.304,108.65,19.265,108.636,18.908,108.638,18.866,108.676,18.75,108.702,18.535,108.922,18.416,109.03,18.368,109.183,18.325,109.341,18.3,109.4,18.281,109.519,18.218,109.59,18.226,109.681,18.247,109.703,18.259,109.76,18.348,109.816,18.397,109.968,18.422,110.02,18.416,110.067,18.448,110.066,18.476,110.049,18.505,110.156,18.57,110.252,18.656,110.291,18.67,110.334,18.673,110.4,18.698,110.451,18.748,110.478,18.813,110.519,18.97,110.566,19.099,110.563,19.135,110.572,19.172,110.603,19.207,110.641,19.291,110.822,19.558,110.913,19.586,111.014,19.655,110.998,19.765,110.971,19.883]},{"b":[79.708,5.949,81.877,9.813],"l":[81.832,7.428,81.797,7.465,81.727,7.625,81.683,7.684,81.676,7.711,81.679,7.742,81.665,7.782,81.436,8.119,81.422,8.148,81.422,8.215,81.373,8.431,81.334,8.472,81.275,8.484,81.227,8.506,81.216,8.549,81.219,8.608,81.198,8.662,81.016,8.933,80.979,8.957,80.935,8.971,80.91,9.025,80.893,9.086,80.711,9.366,80.376,9.642,80.253,9.796,80.181,9.81,80.078,9.807,79.982,9.813,79.967,9.793,79.954,9.742,79.979,9.699,80.046,9.65,80.258,9.611,80.385,9.549,80.428,9.481,80.368,9.48,80.318,9.465,80.256,9.495,80.196,9.538,80.086,9.578,80.111,9.453,80.118,9.327,80.1,9.21,80.065,9.096,79.929,8.899,79.928,8.846,79.944,8.741,79.942,8.692,79.851,8.412,79.832,8.304,79.809,8.05,79.783,8.018,79.75,8.049,79.75,8.294,79.713,8.182,79.708,8.066,79.76,7.796,79.792,7.585,79.859,6.829,79.947,6.585,80.007,6.364,80.095,6.153,80.267,6.01,80.496,5.949,80.724,5.979,80.971,6.088,81.306,6.204,81.38,6.241,81.637,6.425,81.713,6.512,81.768,6.614,81.819,6.756,81.861,6.901,81.877,7.02,81.874,7.288,81.832,7.428]},{"b":[43.257,-25.571,50.483,-12.08],"l":[44.405,-19.922,44.432,-19.674,44.453,-19.551,44.449,-19.429,44.387,-19.303,44.239,-19.075,44.234,-19.033,44.246,-18.863,44.233,-18.741,44.179,-18.619,44.109,-18.504,44.04,-18.288,44.007,-17.933,44.014,-17.804,43.994,-17.69,43.944,-17.581,43.979,-17.392,44.421,-16.703,44.436,-16.621,44.418,-16.411,44.427,-16.289,44.442,-16.244,44.476,-16.217,44.552,-16.204,44.909,-16.175,44.955,-16.153,45.044,-16.095,45.167,-15.983,45.223,-15.95,45.271,-15.962,45.302,-16.01,45.342,-16.037,45.486,-15.986,45.542,-15.984,45.598,-15.993,45.625,-15.946,45.641,-15.883,45.662,-15.839,45.7,-15.814,45.886,-15.8,46.004,-15.782,46.158,-15.738,46.191,-15.747,46.314,-15.905,46.352,-15.918,46.4,-15.925,46.442,-15.896,46.341,-15.813,46.326,-15.767,46.331,-15.714,46.385,-15.6,46.475,-15.513,46.675,-15.382,46.882,-15.23,46.942,-15.219,46.993,-15.243,47.032,-15.423,47.027,-15.452,47.061,-15.456,47.099,-15.434,47.133,-15.362,47.135,-15.302,47.107,-15.244,47.094,-15.195,47.093,-15.15,47.198,-15.044,47.28,-14.943,47.319,-14.822,47.352,-14.766,47.439,-14.703,47.465,-14.713,47.485,-14.764,47.496,-14.818,47.474,-14.872,47.442,-14.925,47.429,-14.996,47.478,-15.009,47.525,-14.992,47.593,-14.864,47.67,-14.743,47.716,-14.68,47.774,-14.637,47.87,-14.646,47.964,-14.673,47.812,-14.545,47.773,-14.37,47.955,-14.067,47.957,-14.004,47.983,-13.985,47.996,-13.96,47.901,-13.858,47.884,-13.808,47.896,-13.731,47.941,-13.662,47.982,-13.615,48.04,-13.596,48.086,-13.623,48.187,-13.707,48.255,-13.719,48.338,-13.639,48.405,-13.538,48.506,-13.469,48.621,-13.426,48.796,-13.267,48.91,-12.936,48.919,-12.839,48.894,-12.722,48.854,-12.61,48.786,-12.471,48.804,-12.44,48.9,-12.458,48.932,-12.439,49.036,-12.316,49.207,-12.08,49.263,-12.08,49.312,-12.124,49.33,-12.189,49.364,-12.236,49.48,-12.348,49.538,-12.432,49.584,-12.537,49.638,-12.637,49.805,-12.88,49.876,-12.973,49.938,-13.072,49.967,-13.27,50.073,-13.578,50.174,-14.04,50.205,-14.514,50.235,-14.732,50.313,-14.937,50.441,-15.149,50.483,-15.386,50.405,-15.629,50.292,-15.858,50.262,-15.902,50.209,-15.96,50.185,-15.958,50.094,-15.899,50.02,-15.802,49.927,-15.574,49.893,-15.458,49.853,-15.439,49.744,-15.45,49.664,-15.522,49.65,-15.567,49.667,-15.696,49.697,-15.811,49.71,-15.929,49.713,-16.077,49.742,-16.121,49.786,-16.159,49.831,-16.256,49.839,-16.487,49.811,-16.603,49.734,-16.703,49.739,-16.758,49.767,-16.815,49.74,-16.849,49.637,-16.893,49.595,-16.931,49.54,-17.033,49.449,-17.241,49.437,-17.347,49.494,-17.67,49.478,-17.899,49.363,-18.336,49.297,-18.544,49.203,-18.792,49.06,-19.12,48.918,-19.53,48.797,-19.953,48.708,-20.207,48.607,-20.458,48.469,-20.9,48.351,-21.349,48.176,-21.843,47.934,-22.394,47.908,-22.466,47.858,-22.747,47.804,-22.992,47.739,-23.233,47.604,-23.633,47.589,-23.756,47.558,-23.875,47.428,-24.125,47.373,-24.218,47.334,-24.318,47.312,-24.443,47.273,-24.564,47.177,-24.787,47.035,-24.979,46.938,-25.049,46.729,-25.15,46.622,-25.17,46.387,-25.173,46.159,-25.23,45.921,-25.341,45.692,-25.468,45.605,-25.529,45.508,-25.563,45.206,-25.571,45.115,-25.543,44.813,-25.334,44.696,-25.3,44.474,-25.271,44.407,-25.253,44.346,-25.226,44.256,-25.117,44.078,-25.025,44.035,-24.996,44.008,-24.932,43.99,-24.863,43.944,-24.787,43.91,-24.641,43.852,-24.538,43.688,-24.358,43.67,-24.3,43.657,-24.109,43.662,-23.979,43.646,-23.742,43.665,-23.63,43.722,-23.53,43.699,-23.421,43.638,-23.307,43.615,-23.188,43.57,-23.08,43.398,-22.886,43.358,-22.791,43.33,-22.692,43.265,-22.384,43.257,-22.276,43.267,-22.049,43.291,-21.933,43.332,-21.851,43.343,-21.79,43.37,-21.738,43.411,-21.696,43.438,-21.647,43.502,-21.356,43.583,-21.292,43.704,-21.255,43.8,-21.179,43.856,-21.077,43.911,-20.866,44.063,-20.656,44.117,-20.546,44.24,-20.38,44.348,-20.146,44.381,-20.035,44.405,-19.922]},{"b":[53.019,38.756,53.11,39.097],"l":[53.055,39.038,53.11,38.803,53.1,38.756,53.046,38.897,53.019,39.053,53.053,39.097,53.092,39.094,53.055,39.038]},{"b":[49.995,44.826,50.184,45.082],"l":[50.095,44.831,49.995,44.937,50.023,45.045,50.059,45.067,50.11,45.082,50.117,45.058,50.045,45.01,50.039,44.949,50.098,44.882,50.184,44.855,50.149,44.826,50.095,44.831]},{"b":[50.256,44.959,50.35,45.083],"l":[50.295,45.076,50.35,45.083,50.331,44.998,50.312,44.972,50.277,44.959,50.256,45.022,50.295,45.076]},{"b":[-164.282,-79.845,-158.997,-78.719],"l":[-160.25,-79.271,-160.764,-79.132,-161.283,-79.007,-161.643,-78.901,-162.161,-78.793,-162.39,-78.76,-162.622,-78.742,-162.873,-78.725,-163.124,-78.719,-163.256,-78.722,-163.345,-78.78,-163.66,-78.856,-163.815,-78.929,-164.126,-78.995,-164.2,-79.051,-164.245,-79.138,-164.282,-79.246,-164.226,-79.321,-163.971,-79.389,-163.712,-79.442,-163.317,-79.505,-161.866,-79.704,-160.807,-79.812,-160.302,-79.845,-159.053,-79.807,-159.01,-79.78,-158.997,-79.735,-159.051,-79.695,-159.119,-79.675,-159.19,-79.637,-159.256,-79.591,-159.366,-79.545,-159.419,-79.508,-159.684,-79.402,-159.964,-79.324,-160.25,-79.271]},{"b":[-71.784,-79.683,-66.728,-78.316],"l":[-66.728,-78.384,-66.84,-78.35,-67.038,-78.316,-67.479,-78.362,-69.398,-78.686,-69.748,-78.769,-69.972,-78.809,-70.544,-78.884,-70.626,-78.902,-71.254,-79.06,-71.454,-79.129,-71.667,-79.246,-71.784,-79.444,-71.778,-79.501,-71.735,-79.539,-71.687,-79.568,-71.526,-79.624,-71.414,-79.64,-70.984,-79.674,-70.553,-79.683,-70.334,-79.68,-70.116,-79.666,-69.732,-79.618,-69.634,-79.518,-69.686,-79.443,-69.394,-79.28,-69.251,-79.21,-68.638,-79.013,-68.157,-78.871,-67.481,-78.682,-67.166,-78.57,-67.046,-78.514,-66.787,-78.422,-66.728,-78.384]},{"b":[-54.372,-80.87,-42.945,-77.785],"l":[-43.947,-78.598,-43.852,-78.53,-43.788,-78.433,-43.777,-78.385,-43.785,-78.336,-43.809,-78.287,-43.854,-78.258,-44.094,-78.167,-44.34,-78.093,-44.594,-78.035,-44.852,-77.988,-45.53,-77.881,-45.993,-77.827,-46.258,-77.805,-46.826,-77.785,-47.03,-77.791,-47.463,-77.819,-47.692,-77.84,-49.081,-78.047,-49.144,-78.094,-49.354,-78.222,-49.94,-78.462,-50.142,-78.557,-50.22,-78.605,-50.294,-78.696,-50.377,-78.78,-50.335,-78.818,-50.242,-78.833,-50.298,-78.882,-50.38,-78.923,-50.502,-78.95,-50.514,-78.98,-50.502,-79.022,-50.52,-79.104,-50.573,-79.172,-50.649,-79.233,-50.733,-79.283,-50.464,-79.313,-50.42,-79.321,-50.379,-79.338,-50.331,-79.381,-50.295,-79.43,-50.339,-79.479,-50.402,-79.512,-50.664,-79.627,-51.184,-79.82,-51.711,-79.99,-52.297,-80.141,-52.338,-80.126,-52.357,-80.078,-52.461,-80.067,-52.567,-80.099,-52.807,-80.156,-53.053,-80.175,-53.176,-80.161,-53.346,-80.114,-53.394,-80.109,-53.482,-80.189,-53.676,-80.284,-54.045,-80.487,-54.13,-80.517,-54.347,-80.569,-54.372,-80.624,-54.351,-80.76,-54.241,-80.847,-54.202,-80.864,-54.162,-80.87,-49.773,-80.784,-49.701,-80.753,-49.63,-80.712,-49.41,-80.667,-49.187,-80.643,-43.528,-80.191,-43.489,-80.178,-43.454,-80.155,-43.459,-80.123,-43.49,-80.096,-43.758,-80.021,-43.742,-80.003,-43.704,-79.99,-43.6,-79.974,-43.496,-79.969,-43.267,-79.979,-43.066,-79.891,-42.99,-79.801,-42.945,-79.579,-42.965,-79.477,-43.119,-79.35,-43.211,-79.3,-43.267,-79.163,-43.364,-79.085,-43.451,-78.99,-43.544,-78.902,-43.627,-78.846,-43.722,-78.818,-44.041,-78.807,-44.566,-78.804,-45.092,-78.814,-45.223,-78.811,-45.352,-78.791,-45.295,-78.74,-45.213,-78.687,-45.068,-78.661,-43.947,-78.598]},{"b":[-66.771,-80.948,-59.322,-79.741],"l":[-59.498,-80.115,-59.788,-80.101,-59.786,-80.001,-59.752,-79.938,-59.706,-79.875,-59.873,-79.777,-60.579,-79.741,-61.026,-79.809,-61.115,-79.862,-61.343,-79.887,-61.346,-79.951,-61.246,-79.978,-61.302,-79.996,-61.684,-80.02,-61.717,-80.069,-61.694,-80.134,-61.597,-80.206,-61.485,-80.244,-61.194,-80.257,-61.313,-80.307,-61.633,-80.344,-62.232,-80.369,-62.519,-80.373,-65.98,-80.384,-66.115,-80.361,-66.168,-80.346,-66.217,-80.258,-66.296,-80.235,-66.377,-80.222,-66.483,-80.224,-66.588,-80.239,-66.681,-80.26,-66.771,-80.294,-66.734,-80.319,-66.591,-80.358,-66.184,-80.442,-65.203,-80.607,-64.268,-80.749,-64.22,-80.734,-64.126,-80.668,-64.065,-80.65,-63.715,-80.617,-63.144,-80.595,-63.068,-80.627,-62.986,-80.735,-62.94,-80.766,-62.671,-80.834,-62.023,-80.889,-60.583,-80.948,-60.268,-80.881,-60.125,-80.841,-59.926,-80.774,-59.825,-80.733,-59.771,-80.657,-59.774,-80.558,-59.734,-80.344,-59.683,-80.315,-59.612,-80.255,-59.53,-80.208,-59.427,-80.198,-59.322,-80.196,-59.408,-80.151,-59.498,-80.115]},{"b":[-75.383,-72.67,-68.228,-68.789],"l":[-70.543,-72.664,-70.063,-72.626,-69.209,-72.534,-69.149,-72.427,-68.64,-72.21,-68.543,-72.158,-68.461,-72.085,-68.394,-71.975,-68.241,-71.822,-68.228,-71.725,-68.252,-71.313,-68.278,-71.097,-68.314,-70.912,-68.336,-70.856,-68.451,-70.818,-68.459,-70.683,-68.554,-70.581,-68.731,-70.408,-69.091,-70.09,-69.234,-69.909,-69.353,-69.666,-69.417,-69.583,-69.708,-69.321,-69.855,-69.277,-69.913,-69.267,-70.079,-69.311,-70.051,-69.189,-70.053,-69.14,-70.105,-68.959,-70.154,-68.923,-70.312,-68.832,-70.417,-68.789,-71.392,-68.874,-71.869,-68.941,-71.99,-68.971,-72.058,-69.001,-72.109,-69.06,-72.138,-69.115,-72.135,-69.177,-72.114,-69.225,-72.081,-69.267,-71.963,-69.329,-71.834,-69.367,-71.743,-69.423,-71.718,-69.524,-71.767,-69.649,-71.852,-69.807,-71.868,-69.847,-71.879,-69.909,-71.854,-69.969,-71.81,-70.005,-71.729,-70.054,-71.696,-70.068,-71.121,-70.196,-71.024,-70.201,-70.926,-70.192,-70.72,-70.139,-70.328,-70.16,-70.234,-70.18,-70.118,-70.234,-69.883,-70.305,-69.618,-70.398,-69.66,-70.412,-69.702,-70.415,-69.975,-70.36,-70.09,-70.351,-70.328,-70.361,-70.562,-70.404,-71.061,-70.537,-71.19,-70.66,-71.173,-70.713,-71.049,-70.762,-70.917,-70.786,-70.661,-70.818,-70.299,-70.836,-70.196,-70.851,-70.094,-70.883,-69.993,-70.897,-69.933,-70.88,-69.876,-70.876,-69.83,-70.914,-69.823,-70.973,-69.823,-71.034,-69.835,-71.093,-69.87,-71.126,-69.916,-71.134,-70.268,-70.965,-70.323,-70.951,-70.381,-70.946,-70.741,-70.993,-71.194,-70.985,-71.308,-71.011,-71.504,-71.112,-71.719,-71.145,-72.356,-71.075,-72.71,-71.073,-73.06,-71.127,-72.995,-71.187,-72.905,-71.223,-72.43,-71.275,-72.212,-71.335,-72.622,-71.388,-72.821,-71.384,-73.02,-71.369,-73.397,-71.321,-73.474,-71.325,-73.604,-71.351,-73.617,-71.397,-73.592,-71.448,-73.38,-71.528,-73.427,-71.559,-73.479,-71.579,-73.545,-71.573,-73.724,-71.517,-73.937,-71.438,-74.041,-71.41,-74.187,-71.383,-74.236,-71.388,-74.308,-71.4,-74.375,-71.415,-74.425,-71.457,-74.42,-71.507,-74.38,-71.579,-74.373,-71.618,-74.392,-71.638,-74.419,-71.643,-74.487,-71.642,-74.636,-71.617,-74.863,-71.543,-75.1,-71.555,-75.293,-71.615,-75.335,-71.645,-75.353,-71.68,-75.325,-71.726,-75.331,-71.752,-75.373,-71.78,-75.383,-71.828,-75.353,-71.878,-75.259,-71.914,-75.13,-71.964,-75.024,-71.988,-74.908,-72.033,-74.786,-72.064,-74.663,-72.07,-74.429,-72.056,-74.322,-72.073,-74.209,-72.142,-74.153,-72.159,-73.996,-72.17,-73.899,-72.152,-73.537,-72.022,-73.572,-71.981,-73.691,-71.929,-73.83,-71.87,-73.776,-71.849,-73.633,-71.835,-73.41,-71.853,-73.167,-71.905,-72.972,-71.924,-72.928,-71.922,-72.412,-71.662,-72.337,-71.632,-72.259,-71.641,-72.046,-71.74,-71.816,-71.822,-71.574,-71.851,-71.465,-71.838,-71.355,-71.836,-70.821,-71.907,-70.845,-71.946,-70.891,-71.987,-71.034,-72.035,-71.107,-72.047,-71.898,-72.121,-71.892,-72.153,-71.661,-72.25,-71.413,-72.284,-71.178,-72.264,-70.945,-72.229,-70.641,-72.17,-70.533,-72.163,-70.424,-72.168,-70.314,-72.191,-70.206,-72.228,-70.428,-72.323,-70.671,-72.356,-70.873,-72.366,-71.605,-72.359,-72.135,-72.331,-72.376,-72.296,-72.618,-72.275,-72.737,-72.281,-72.855,-72.304,-73.086,-72.408,-73.057,-72.448,-73.007,-72.484,-72.888,-72.547,-72.78,-72.581,-72.67,-72.596,-72.531,-72.59,-72.48,-72.617,-72.433,-72.658,-72.367,-72.67,-71.846,-72.639,-71.159,-72.627,-70.923,-72.613,-70.731,-72.623,-70.543,-72.664]},{"b":[-71.928,-55.032,-65.179,-52.486],"l":[-65.747,-54.653,-65.993,-54.599,-66.236,-54.533,-66.462,-54.441,-66.67,-54.314,-66.865,-54.223,-67.069,-54.148,-67.294,-54.05,-67.503,-53.922,-67.678,-53.787,-67.861,-53.662,-67.94,-53.619,-68.008,-53.564,-68.144,-53.319,-68.161,-53.306,-68.393,-53.295,-68.489,-53.261,-68.521,-53.222,-68.521,-53.177,-68.479,-53.114,-68.431,-53.055,-68.333,-53.02,-68.24,-53.082,-68.278,-52.984,-68.339,-52.9,-68.571,-52.695,-68.63,-52.653,-68.659,-52.632,-68.758,-52.582,-68.79,-52.577,-69.08,-52.674,-69.167,-52.668,-69.414,-52.486,-69.498,-52.491,-69.572,-52.549,-69.663,-52.646,-69.764,-52.731,-69.883,-52.799,-69.935,-52.821,-69.994,-52.821,-70.088,-52.769,-70.19,-52.724,-70.335,-52.734,-70.38,-52.752,-70.297,-52.817,-70.259,-52.857,-70.163,-52.899,-70.14,-52.919,-70.131,-52.943,-70.161,-52.97,-70.196,-52.99,-70.256,-53.004,-70.32,-53.001,-70.391,-53.026,-70.443,-53.086,-70.46,-53.143,-70.46,-53.206,-70.416,-53.305,-70.329,-53.378,-70.213,-53.414,-70.09,-53.418,-69.874,-53.35,-69.756,-53.337,-69.637,-53.334,-69.513,-53.342,-69.394,-53.373,-69.356,-53.416,-69.352,-53.48,-69.39,-53.499,-69.69,-53.601,-69.95,-53.672,-70.091,-53.722,-70.149,-53.761,-70.151,-53.888,-70.086,-54.011,-69.988,-54.109,-69.196,-54.354,-69.044,-54.407,-69.045,-54.428,-69.077,-54.445,-69.128,-54.458,-69.169,-54.483,-69.253,-54.557,-69.312,-54.571,-69.322,-54.543,-69.325,-54.488,-69.365,-54.438,-69.419,-54.407,-69.622,-54.364,-69.742,-54.306,-69.809,-54.321,-69.867,-54.367,-69.99,-54.381,-70.169,-54.379,-70.243,-54.348,-70.246,-54.277,-70.38,-54.181,-70.535,-54.136,-70.63,-54.006,-70.461,-54.006,-70.38,-53.987,-70.443,-53.893,-70.531,-53.627,-70.619,-53.655,-70.696,-53.727,-70.644,-53.823,-70.868,-53.884,-70.857,-53.996,-70.863,-54.11,-70.76,-54.241,-70.636,-54.262,-70.54,-54.303,-70.468,-54.373,-70.298,-54.486,-70.311,-54.529,-70.418,-54.502,-70.573,-54.504,-70.701,-54.485,-70.688,-54.415,-70.699,-54.349,-70.797,-54.327,-70.898,-54.338,-70.928,-54.36,-70.946,-54.398,-70.966,-54.42,-71.08,-54.444,-71.159,-54.451,-71.355,-54.395,-71.393,-54.4,-71.5,-54.445,-71.573,-54.495,-71.606,-54.497,-71.716,-54.444,-71.8,-54.434,-71.823,-54.474,-71.907,-54.495,-71.928,-54.529,-71.902,-54.602,-71.832,-54.626,-71.441,-54.62,-71.229,-54.694,-70.925,-54.714,-70.735,-54.751,-70.497,-54.81,-70.282,-54.752,-70.259,-54.756,-70.238,-54.778,-70.138,-54.819,-70.031,-54.816,-69.899,-54.782,-69.772,-54.739,-69.723,-54.712,-69.588,-54.813,-69.486,-54.859,-69.082,-54.91,-68.844,-54.877,-68.804,-54.854,-68.653,-54.854,-68.619,-54.834,-68.491,-54.836,-68.332,-54.816,-68.22,-54.818,-68.007,-54.848,-67.793,-54.869,-67.127,-54.904,-66.93,-54.925,-66.628,-55.013,-66.511,-55.032,-66.399,-55.009,-66.287,-54.978,-66.172,-54.975,-66.061,-54.957,-65.954,-54.919,-65.842,-54.91,-65.723,-54.926,-65.603,-54.928,-65.471,-54.915,-65.346,-54.878,-65.252,-54.789,-65.179,-54.678,-65.252,-54.638,-65.369,-54.632,-65.747,-54.653]},{"b":[-84.887,19.855,-74.137,23.19],"l":[-74.513,20.385,-74.662,20.522,-74.732,20.573,-74.883,20.651,-74.96,20.673,-75.213,20.714,-75.338,20.702,-75.525,20.717,-75.643,20.733,-75.725,20.715,-75.753,20.736,-75.76,20.776,-75.74,20.812,-75.597,20.838,-75.663,20.898,-75.639,20.947,-75.596,20.995,-75.634,21.061,-75.723,21.111,-75.899,21.114,-76.074,21.133,-76.259,21.227,-76.455,21.274,-76.552,21.272,-76.647,21.285,-76.689,21.34,-76.726,21.359,-76.765,21.362,-76.867,21.33,-76.86,21.365,-76.836,21.4,-76.928,21.459,-77.099,21.589,-77.141,21.539,-77.208,21.479,-77.253,21.483,-77.27,21.538,-77.366,21.613,-77.245,21.594,-77.181,21.598,-77.144,21.644,-77.222,21.672,-77.3,21.712,-77.342,21.755,-77.497,21.872,-77.583,21.889,-77.573,21.868,-77.507,21.811,-77.497,21.788,-77.545,21.775,-77.637,21.797,-77.865,21.901,-77.971,21.972,-78.143,22.109,-78.686,22.367,-78.719,22.358,-78.776,22.367,-78.835,22.391,-78.902,22.396,-79.183,22.388,-79.276,22.408,-79.358,22.449,-79.456,22.51,-79.549,22.578,-79.677,22.743,-79.851,22.827,-79.82,22.887,-79.924,22.869,-79.96,22.877,-80.075,22.942,-80.168,22.949,-80.266,22.935,-80.365,22.943,-80.459,22.975,-80.55,23.017,-80.613,23.084,-80.65,23.103,-81.008,23.09,-81.145,23.055,-81.179,23.06,-81.272,23.129,-81.262,23.157,-81.364,23.13,-81.575,23.117,-81.837,23.163,-82.101,23.19,-82.351,23.154,-82.588,23.065,-82.666,23.044,-83.177,22.983,-83.258,22.968,-84.045,22.666,-84.122,22.619,-84.281,22.474,-84.361,22.379,-84.383,22.256,-84.326,22.074,-84.373,22.036,-84.433,22.031,-84.494,22.042,-84.533,22.031,-84.877,21.894,-84.887,21.857,-84.838,21.828,-84.786,21.842,-84.683,21.899,-84.627,21.92,-84.56,21.933,-84.501,21.93,-84.491,21.854,-84.503,21.776,-84.449,21.792,-84.241,21.898,-84.138,21.929,-84.031,21.943,-83.998,21.98,-83.963,22.092,-83.933,22.15,-83.901,22.17,-83.687,22.18,-83.643,22.189,-83.602,22.209,-83.544,22.209,-83.486,22.187,-83.38,22.223,-83.292,22.303,-83.189,22.355,-83.144,22.386,-83.107,22.43,-83.009,22.514,-82.861,22.595,-82.786,22.658,-82.738,22.689,-81.903,22.679,-81.839,22.672,-81.79,22.657,-81.746,22.633,-81.703,22.592,-81.683,22.535,-81.71,22.497,-81.757,22.467,-81.973,22.422,-82.078,22.388,-81.973,22.291,-81.849,22.214,-81.816,22.2,-81.441,22.184,-81.355,22.104,-81.284,22.109,-81.222,22.143,-81.2,22.203,-81.185,22.268,-81.141,22.207,-81.117,22.134,-81.083,22.098,-81.036,22.074,-80.962,22.053,-80.499,22.064,-80.485,22.087,-80.485,22.123,-80.393,22.034,-80.311,21.933,-80.231,21.872,-80.138,21.829,-79.91,21.743,-79.357,21.585,-79.274,21.563,-79.189,21.553,-78.823,21.619,-78.728,21.593,-78.636,21.516,-78.577,21.414,-78.537,21.297,-78.491,21.054,-78.454,21.011,-78.406,20.974,-78.314,20.927,-78.116,20.762,-77.997,20.715,-77.857,20.714,-77.593,20.69,-77.467,20.69,-77.348,20.672,-77.23,20.644,-77.205,20.611,-77.189,20.56,-77.108,20.492,-77.093,20.453,-77.104,20.408,-77.149,20.347,-77.213,20.3,-77.554,20.082,-77.715,19.855,-77.463,19.861,-77.212,19.894,-76.999,19.893,-76.89,19.921,-76.78,19.94,-76.516,19.957,-76.253,19.987,-76.158,19.99,-75.765,19.96,-75.657,19.932,-75.552,19.891,-75.29,19.893,-75.219,19.924,-75.177,19.959,-75.152,20.008,-75.122,19.954,-75.124,19.925,-75.116,19.901,-75.003,19.929,-74.955,19.958,-74.85,20.002,-74.635,20.058,-74.412,20.075,-74.253,20.08,-74.217,20.117,-74.154,20.169,-74.137,20.232,-74.167,20.292,-74.198,20.311,-74.234,20.326,-74.273,20.317,-74.384,20.33,-74.513,20.385]},{"b":[17.917,79.194,27.199,80.478],"l":[26.861,80.16,26.437,80.175,25.836,80.175,25.751,80.188,25.667,80.21,25.471,80.233,24.907,80.277,24.786,80.301,24.736,80.301,24.614,80.286,24.547,80.295,24.403,80.355,24.298,80.36,24.28,80.329,24.234,80.303,24.143,80.295,23.953,80.305,23.773,80.244,23.688,80.207,23.353,80.179,23.115,80.187,23.225,80.318,23.25,80.381,23.315,80.425,23.251,80.447,23.008,80.474,22.897,80.469,22.793,80.433,22.672,80.413,22.549,80.416,22.451,80.402,22.418,80.366,22.446,80.308,22.443,80.19,22.376,80.09,22.29,80.049,22.19,80.06,21.898,80.132,21.781,80.139,21.697,80.159,21.655,80.218,21.549,80.243,20.998,80.239,20.898,80.25,20.693,80.299,20.476,80.372,20.359,80.401,20.104,80.43,19.851,80.471,19.733,80.478,19.614,80.463,19.691,80.402,19.777,80.353,19.81,80.327,19.802,80.295,19.751,80.227,19.568,80.25,19.327,80.323,19.264,80.336,19.178,80.332,19.157,80.302,19.191,80.263,19.355,80.185,19.537,80.163,19.343,80.116,19.143,80.139,18.962,80.175,18.779,80.194,18.089,80.171,17.917,80.143,18.129,80.093,18.344,80.06,18.856,80.037,18.726,79.996,18.595,79.967,18.255,79.929,18.285,79.887,18.325,79.86,18.428,79.825,18.725,79.761,18.942,79.736,19.4,79.727,19.638,79.729,19.899,79.744,20.123,79.779,20.461,79.775,20.784,79.749,20.687,79.707,20.565,79.691,20.493,79.633,20.187,79.632,20.015,79.64,19.821,79.634,19.747,79.618,19.675,79.591,19.9,79.534,20.128,79.49,20.4,79.463,20.761,79.442,20.806,79.41,20.861,79.398,21.911,79.381,22.866,79.412,22.696,79.329,22.789,79.264,22.904,79.231,23.759,79.206,23.948,79.194,24.133,79.215,24.257,79.263,24.383,79.302,24.751,79.365,24.843,79.367,25.145,79.339,25.239,79.345,25.641,79.403,25.726,79.44,25.902,79.561,26.006,79.617,26.221,79.677,27.08,79.865,27.199,79.907,27.148,80.059,27.017,80.125,26.861,80.16]},{"b":[10.682,76.579,21.389,80.053],"l":[20.725,78.672,21.096,78.676,21.244,78.699,21.389,78.74,21.353,78.772,21.312,78.796,21.09,78.853,20.72,78.907,20.501,78.981,20.767,79.059,20.611,79.107,20.458,79.129,20.163,79.146,20.114,79.125,20.114,79.077,19.894,79.056,19.751,79.147,19.49,79.176,19.089,79.157,18.979,79.179,18.88,79.234,18.772,79.26,18.678,79.262,18.72,79.281,18.807,79.303,18.823,79.337,18.832,79.385,18.815,79.427,18.785,79.461,18.748,79.488,18.581,79.572,18.397,79.605,18.333,79.611,18.272,79.601,17.861,79.437,17.669,79.386,17.715,79.431,17.734,79.481,17.688,79.533,17.733,79.57,17.86,79.635,17.956,79.704,17.835,79.8,17.685,79.857,17.578,79.885,17.219,79.941,16.966,79.959,16.926,79.943,16.889,79.915,16.838,79.905,16.787,79.907,16.524,80.021,16.387,80.053,16.246,80.049,16.094,80.007,16.057,79.954,16.1,79.884,15.956,79.835,15.845,79.734,15.826,79.709,15.816,79.682,15.841,79.587,15.875,79.519,16.028,79.342,16.254,79.112,16.344,78.976,16.295,78.981,15.858,79.16,15.764,79.174,15.66,79.235,15.444,79.407,15.251,79.545,15.052,79.675,14.832,79.766,14.594,79.799,14.38,79.726,14.178,79.619,14.04,79.586,14.02,79.539,14.011,79.482,14.026,79.429,14.056,79.383,14.03,79.344,13.957,79.34,13.834,79.376,13.716,79.429,13.601,79.457,13.432,79.471,13.384,79.481,13.334,79.575,13.215,79.588,12.555,79.569,13.039,79.685,13.778,79.715,13.907,79.752,13.921,79.762,13.926,79.793,13.914,79.817,13.693,79.861,13.108,79.832,12.754,79.776,12.602,79.773,12.28,79.816,12.219,79.798,12.245,79.75,12.288,79.713,12.205,79.719,12.102,79.738,11.702,79.821,11.344,79.799,11.251,79.785,11.185,79.72,11.15,79.717,11.05,79.76,10.866,79.797,10.804,79.799,10.746,79.789,10.682,79.758,10.686,79.734,10.755,79.69,10.811,79.641,10.737,79.582,10.725,79.556,10.738,79.52,10.834,79.463,10.888,79.415,10.926,79.35,10.975,79.305,11.107,79.233,11.208,79.13,11.339,79.109,11.521,79.151,11.616,79.205,11.58,79.283,11.679,79.291,11.978,79.293,12.084,79.268,12.016,79.213,11.893,79.152,11.902,79.112,11.926,79.077,11.982,79.025,12.046,78.983,12.087,78.975,12.253,78.975,12.375,78.966,12.403,78.953,12.323,78.914,12.275,78.904,11.548,78.983,11.456,78.973,11.365,78.95,11.611,78.883,11.861,78.832,11.755,78.812,11.746,78.766,11.774,78.716,11.866,78.674,11.962,78.642,12.138,78.606,12.258,78.595,12.435,78.483,12.665,78.385,12.822,78.351,12.87,78.331,12.913,78.301,13.15,78.238,13.655,78.245,13.908,78.267,14.11,78.271,14.238,78.31,14.363,78.36,14.5,78.392,14.638,78.415,14.546,78.462,14.432,78.492,14.468,78.541,14.515,78.581,14.505,78.631,14.467,78.675,14.578,78.705,14.689,78.721,14.744,78.721,14.792,78.706,14.839,78.666,14.892,78.639,15.016,78.63,15.137,78.664,15.225,78.732,15.323,78.781,15.384,78.771,15.392,78.721,15.348,78.663,15.265,78.608,15.254,78.589,15.279,78.554,15.36,78.488,15.417,78.473,15.681,78.471,15.944,78.493,16.158,78.538,16.446,78.639,16.53,78.656,16.783,78.664,16.697,78.613,16.449,78.504,16.535,78.449,16.727,78.407,16.811,78.397,16.992,78.4,17.172,78.417,17.003,78.369,16.777,78.35,16.15,78.353,15.875,78.339,15.784,78.327,15.657,78.299,15.659,78.265,15.698,78.228,15.519,78.233,15.341,78.221,14.995,78.151,14.248,78.071,14.307,78.005,14.048,78.067,13.937,78.086,13.824,78.085,13.77,78.075,13.718,78.058,13.681,78.028,13.714,77.919,13.75,77.883,13.791,77.854,13.963,77.796,14.09,77.771,14.604,77.766,14.847,77.779,15.097,77.809,15.345,77.857,15.585,77.869,15.826,77.847,16.06,77.847,16.54,77.88,16.853,77.912,16.914,77.898,16.969,77.842,17.033,77.798,16.619,77.799,16.206,77.782,14.921,77.689,14.695,77.525,14.596,77.538,14.488,77.571,14.378,77.58,14.071,77.564,14.026,77.545,13.996,77.508,14.004,77.445,14.05,77.403,14.145,77.336,14.248,77.282,14.366,77.234,14.487,77.199,14.738,77.162,15.124,77.085,15.547,76.886,16.004,76.761,16.124,76.739,16.238,76.702,16.346,76.645,16.462,76.609,16.7,76.579,16.935,76.606,17.063,76.659,17.036,76.72,16.98,76.779,16.977,76.812,17.142,76.895,17.249,76.969,17.188,77.011,17.153,77.049,17.349,77.157,17.442,77.225,17.623,77.399,17.847,77.497,18.137,77.507,18.228,77.523,18.299,77.579,18.362,77.682,18.404,77.794,18.439,77.942,18.431,77.991,18.439,78.025,18.575,78.048,18.712,78.04,18.822,78.042,18.995,78.081,19.009,78.132,18.958,78.182,18.984,78.234,19.056,78.319,19.15,78.379,19.381,78.48,19.619,78.562,19.655,78.598,19.677,78.61,19.769,78.623,20.387,78.643,20.725,78.672]},{"b":[8.181,38.91,9.805,41.257],"l":[9.682,40.818,9.632,40.882,9.574,40.915,9.554,40.932,9.59,40.992,9.621,41.005,9.615,41.017,9.576,41.031,9.539,41.054,9.5,41.106,9.455,41.15,9.351,41.196,9.283,41.202,9.228,41.257,9.182,41.242,9.163,41.185,9.107,41.143,8.998,41.11,8.821,40.95,8.699,40.895,8.572,40.85,8.468,40.834,8.363,40.846,8.31,40.858,8.245,40.907,8.224,40.913,8.204,40.871,8.181,40.771,8.19,40.652,8.23,40.606,8.296,40.559,8.353,40.501,8.385,40.443,8.409,40.352,8.471,40.293,8.471,40.131,8.455,40.078,8.409,40.038,8.399,39.978,8.408,39.917,8.496,39.897,8.548,39.839,8.539,39.77,8.541,39.732,8.511,39.722,8.471,39.748,8.451,39.722,8.461,39.648,8.447,39.563,8.419,39.523,8.399,39.482,8.411,39.292,8.418,39.206,8.486,39.11,8.553,39.03,8.595,38.964,8.649,38.927,8.719,38.927,8.801,38.91,8.881,38.913,8.967,38.964,9.023,39.043,9.056,39.239,9.102,39.211,9.149,39.197,9.207,39.214,9.264,39.217,9.388,39.168,9.486,39.14,9.563,39.166,9.584,39.254,9.617,39.354,9.686,39.924,9.707,40.017,9.701,40.092,9.659,40.159,9.643,40.268,9.754,40.4,9.783,40.442,9.805,40.5,9.794,40.556,9.682,40.818]},{"b":[8.566,41.385,9.556,43.021],"l":[8.642,42.118,8.701,42.096,8.703,42.043,8.653,41.996,8.615,41.959,8.622,41.931,8.674,41.922,8.74,41.925,8.759,41.87,8.719,41.804,8.718,41.761,8.771,41.737,8.887,41.701,8.879,41.669,8.83,41.628,8.808,41.588,8.842,41.559,8.895,41.516,9.003,41.477,9.186,41.385,9.253,41.46,9.331,41.627,9.374,41.679,9.395,41.731,9.401,41.926,9.428,41.972,9.551,42.13,9.556,42.161,9.526,42.553,9.509,42.586,9.473,42.616,9.454,42.659,9.48,42.805,9.479,42.86,9.461,42.945,9.463,42.981,9.415,43.021,9.363,43.017,9.331,42.944,9.323,42.814,9.338,42.767,9.313,42.713,9.288,42.695,9.254,42.712,9.198,42.729,9.138,42.733,9.088,42.705,9.044,42.662,8.995,42.645,8.815,42.608,8.713,42.55,8.64,42.427,8.588,42.385,8.566,42.358,8.592,42.345,8.626,42.343,8.675,42.284,8.608,42.258,8.566,42.219,8.588,42.161,8.642,42.118]},{"b":[12.436,36.688,15.635,38.296],"l":[15.341,38.217,15.28,38.23,15.224,38.211,15.176,38.168,15.119,38.153,14.982,38.168,14.846,38.172,14.79,38.167,14.737,38.151,14.637,38.085,14.506,38.046,14.416,38.043,14.288,38.017,14.05,38.041,13.937,38.024,13.789,37.981,13.735,37.984,13.682,38.001,13.491,38.103,13.433,38.11,13.383,38.127,13.352,38.181,13.291,38.191,13.16,38.19,13.057,38.131,13.049,38.084,12.955,38.041,12.903,38.035,12.851,38.064,12.734,38.183,12.702,38.142,12.664,38.108,12.602,38.085,12.548,38.053,12.487,37.939,12.436,37.82,12.454,37.774,12.527,37.67,12.64,37.594,12.699,37.572,12.757,37.567,12.871,37.575,12.924,37.571,13.04,37.507,13.17,37.479,13.221,37.452,13.265,37.41,13.361,37.349,13.587,37.254,13.801,37.136,13.905,37.101,14.024,37.107,14.143,37.104,14.259,37.046,14.367,36.973,14.502,36.799,14.555,36.777,14.614,36.767,14.776,36.71,14.89,36.724,15.002,36.694,15.113,36.688,15.116,36.736,15.104,36.785,15.116,36.839,15.142,36.892,15.185,36.935,15.295,37.013,15.296,37.055,15.289,37.097,15.236,37.139,15.174,37.209,15.23,37.244,15.194,37.283,15.146,37.308,15.117,37.335,15.106,37.375,15.1,37.459,15.131,37.532,15.165,37.59,15.19,37.651,15.207,37.721,15.234,37.785,15.476,38.063,15.509,38.107,15.577,38.22,15.635,38.268,15.568,38.296,15.499,38.291,15.341,38.217]},{"b":[-54.919,69.257,-51.9,70.317],"l":[-52.113,69.489,-51.988,69.55,-51.9,69.605,-51.908,69.637,-51.982,69.664,-52.007,69.686,-51.985,69.704,-51.977,69.722,-51.983,69.743,-52.011,69.782,-52.045,69.807,-52.398,69.863,-52.731,69.945,-53.103,70.141,-53.297,70.205,-53.375,70.221,-54.007,70.296,-54.372,70.317,-54.706,70.256,-54.786,70.218,-54.816,70.189,-54.83,70.161,-54.831,70.133,-54.809,70.085,-54.774,70.053,-54.652,70.011,-54.323,69.942,-54.363,69.924,-54.665,69.966,-54.788,69.95,-54.841,69.902,-54.919,69.714,-54.866,69.665,-54.804,69.631,-54.734,69.611,-54.497,69.577,-54.133,69.565,-53.994,69.553,-53.921,69.534,-53.825,69.54,-53.783,69.506,-53.722,69.491,-53.658,69.465,-53.89,69.437,-54.047,69.437,-54.158,69.428,-54.183,69.404,-54.121,69.364,-54.051,69.337,-53.902,69.302,-53.793,69.264,-53.754,69.26,-53.578,69.257,-53.003,69.343,-52.77,69.364,-52.113,69.489]},{"b":[-128.35,48.323,-123.284,50.861],"l":[-123.415,48.698,-123.443,48.69,-123.458,48.674,-123.473,48.602,-123.497,48.582,-123.627,48.824,-123.752,48.951,-123.82,49.083,-123.854,49.119,-123.937,49.171,-123.996,49.224,-124.186,49.301,-124.496,49.38,-124.643,49.429,-124.831,49.53,-124.931,49.643,-124.932,49.67,-124.905,49.685,-124.935,49.732,-125.066,49.848,-125.233,50.012,-125.314,50.107,-125.42,50.255,-125.482,50.317,-125.534,50.342,-125.615,50.359,-125.839,50.381,-126.204,50.454,-126.701,50.516,-127.197,50.64,-127.713,50.821,-127.918,50.861,-128.101,50.858,-128.242,50.828,-128.301,50.794,-128.346,50.744,-128.35,50.697,-128.267,50.609,-128.136,50.521,-128.058,50.498,-127.964,50.493,-127.865,50.499,-127.731,50.536,-127.75,50.578,-127.751,50.607,-127.526,50.597,-127.466,50.583,-127.529,50.537,-127.524,50.496,-127.489,50.427,-127.487,50.405,-127.578,50.465,-127.641,50.479,-127.832,50.471,-127.874,50.464,-127.906,50.445,-127.963,50.346,-127.947,50.326,-127.851,50.314,-127.839,50.293,-127.828,50.211,-127.873,50.15,-127.864,50.128,-127.816,50.118,-127.77,50.121,-127.675,50.163,-127.467,50.163,-127.43,50.131,-127.398,50.085,-127.349,50.052,-127.29,50.071,-127.272,50.096,-127.268,50.129,-127.25,50.138,-127.216,50.121,-127.192,50.1,-127.18,50.073,-127.179,50.05,-127.208,49.992,-127.196,49.949,-127.166,49.91,-127.114,49.88,-127.049,49.872,-126.977,49.883,-126.948,49.903,-126.926,49.935,-126.903,49.944,-126.849,49.923,-126.745,49.905,-126.683,49.876,-126.593,49.764,-126.558,49.733,-126.525,49.72,-126.463,49.72,-126.403,49.678,-126.348,49.661,-126.134,49.672,-126.158,49.65,-126.443,49.619,-126.542,49.59,-126.557,49.579,-126.564,49.543,-126.549,49.419,-126.519,49.397,-126.5,49.4,-126.445,49.451,-126.419,49.449,-126.304,49.382,-126.28,49.392,-126.27,49.432,-126.244,49.443,-126.169,49.415,-126.1,49.421,-126.075,49.409,-126.048,49.379,-126.02,49.368,-125.935,49.401,-125.938,49.38,-125.984,49.288,-125.952,49.248,-125.918,49.25,-125.835,49.277,-125.796,49.26,-125.728,49.2,-125.694,49.19,-125.655,49.193,-125.644,49.186,-125.702,49.139,-125.812,49.107,-125.829,49.092,-125.66,49.029,-125.543,48.953,-125.489,48.934,-125.46,48.941,-125.363,48.998,-125.168,48.991,-124.927,49.014,-124.904,49.031,-124.868,49.079,-124.839,49.139,-124.821,49.207,-124.813,49.213,-124.8,49.142,-124.817,49.083,-124.85,49.028,-124.935,48.956,-125.136,48.822,-125.14,48.803,-125.121,48.761,-125.017,48.711,-124.868,48.654,-124.689,48.597,-124.376,48.515,-124.115,48.436,-123.917,48.387,-123.595,48.334,-123.573,48.323,-123.536,48.345,-123.485,48.4,-123.446,48.427,-123.335,48.406,-123.311,48.411,-123.284,48.455,-123.366,48.606,-123.39,48.67,-123.415,48.698]},{"b":[-74.478,17.636,-68.339,19.928],"l":[-68.685,18.905,-68.901,18.988,-69.031,19.013,-69.163,19.028,-69.28,19.052,-69.395,19.086,-69.508,19.108,-69.624,19.118,-69.623,19.16,-69.606,19.206,-69.52,19.212,-69.323,19.201,-69.264,19.226,-69.232,19.272,-69.325,19.328,-69.739,19.299,-69.823,19.367,-69.878,19.473,-69.891,19.59,-69.957,19.672,-70.014,19.673,-70.129,19.636,-70.194,19.638,-70.305,19.676,-70.436,19.771,-70.479,19.777,-70.636,19.776,-70.686,19.793,-70.785,19.851,-70.834,19.887,-70.954,19.914,-71.082,19.89,-71.236,19.848,-71.281,19.847,-71.442,19.894,-71.558,19.895,-71.616,19.877,-71.667,19.849,-71.706,19.795,-71.735,19.735,-71.779,19.718,-71.835,19.697,-71.954,19.722,-72.22,19.745,-72.43,19.813,-72.637,19.901,-72.877,19.928,-73.118,19.904,-73.218,19.884,-73.315,19.855,-73.401,19.807,-73.438,19.722,-73.396,19.659,-73.316,19.637,-73.053,19.611,-72.863,19.526,-72.703,19.441,-72.742,19.342,-72.768,19.241,-72.741,19.131,-72.811,19.072,-72.649,18.894,-72.465,18.744,-72.348,18.675,-72.347,18.624,-72.376,18.574,-72.418,18.559,-72.618,18.551,-72.66,18.515,-72.696,18.468,-72.739,18.442,-72.789,18.435,-72.917,18.456,-73.592,18.522,-73.687,18.565,-73.862,18.575,-73.976,18.601,-74.1,18.641,-74.228,18.663,-74.284,18.657,-74.387,18.625,-74.478,18.45,-74.46,18.393,-74.419,18.346,-74.195,18.269,-74.085,18.215,-73.989,18.143,-73.885,18.042,-73.839,18.058,-73.825,18.122,-73.747,18.19,-73.644,18.229,-73.515,18.245,-73.385,18.251,-73.272,18.234,-73.16,18.206,-72.877,18.152,-72.755,18.156,-72.633,18.176,-72.592,18.187,-72.553,18.208,-72.504,18.22,-72.06,18.229,-72.002,18.212,-71.946,18.186,-71.853,18.119,-71.768,18.039,-71.712,18.005,-71.674,17.954,-71.657,17.889,-71.658,17.821,-71.632,17.774,-71.569,17.757,-71.518,17.725,-71.439,17.636,-71.396,17.646,-71.358,17.694,-71.267,17.85,-71.106,18.07,-71.083,18.128,-71.082,18.224,-71.07,18.25,-71.028,18.273,-70.924,18.292,-70.759,18.346,-70.645,18.336,-70.565,18.268,-70.48,18.217,-70.183,18.252,-70.142,18.277,-70.063,18.346,-70.018,18.374,-69.896,18.418,-69.771,18.444,-69.645,18.436,-69.519,18.416,-69.397,18.42,-69.275,18.44,-69.072,18.399,-68.935,18.408,-68.82,18.339,-68.778,18.266,-68.721,18.218,-68.687,18.215,-68.659,18.222,-68.612,18.306,-68.564,18.355,-68.493,18.379,-68.445,18.418,-68.359,18.538,-68.339,18.612,-68.381,18.671,-68.445,18.714,-68.685,18.905]},{"b":[-50.796,-1.801,-48.38,-0.116],"l":[-48.38,-0.353,-48.393,-0.297,-48.444,-0.272,-48.515,-0.248,-48.588,-0.232,-48.787,-0.216,-49.117,-0.164,-49.215,-0.159,-49.314,-0.168,-49.403,-0.215,-49.535,-0.234,-49.629,-0.229,-50.248,-0.116,-50.462,-0.157,-50.646,-0.273,-50.694,-0.364,-50.716,-0.47,-50.703,-0.529,-50.72,-0.583,-50.771,-0.645,-50.781,-0.69,-50.796,-0.906,-50.783,-1.01,-50.71,-1.078,-50.593,-1.073,-50.577,-1.103,-50.581,-1.139,-50.596,-1.147,-50.668,-1.131,-50.729,-1.127,-50.76,-1.24,-50.724,-1.371,-50.673,-1.516,-50.617,-1.638,-50.602,-1.698,-50.508,-1.788,-50.443,-1.801,-50.338,-1.756,-50.109,-1.748,-50.066,-1.704,-50.01,-1.708,-49.911,-1.763,-49.805,-1.79,-49.749,-1.755,-49.651,-1.738,-49.588,-1.712,-49.526,-1.63,-49.507,-1.512,-49.407,-1.556,-49.345,-1.595,-49.234,-1.6,-49.205,-1.559,-49.182,-1.485,-49.173,-1.413,-49.087,-1.505,-49.038,-1.514,-48.986,-1.505,-48.929,-1.482,-48.834,-1.39,-48.804,-1.327,-48.829,-1.277,-48.84,-1.227,-48.79,-1.173,-48.729,-1.132,-48.705,-1.107,-48.624,-0.987,-48.571,-0.893,-48.55,-0.848,-48.54,-0.801,-48.567,-0.684,-48.523,-0.691,-48.497,-0.665,-48.464,-0.535,-48.428,-0.442,-48.38,-0.353]},{"b":[-6.134,50.021,1.747,58.65],"l":[-3.988,57.581,-3.868,57.6,-3.628,57.662,-3.403,57.708,-3.295,57.71,-3.084,57.673,-3.036,57.672,-2.947,57.689,-2.856,57.692,-2.244,57.681,-2.074,57.702,-1.962,57.677,-1.867,57.612,-1.778,57.494,-1.781,57.474,-1.835,57.42,-1.934,57.352,-2.02,57.259,-2.046,57.209,-2.062,57.153,-2.09,57.103,-2.26,56.863,-2.427,56.731,-2.501,56.637,-2.593,56.562,-2.681,56.514,-2.775,56.483,-3.047,56.449,-3.124,56.425,-3.214,56.384,-3.31,56.363,-3.198,56.366,-3.087,56.389,-2.885,56.398,-2.653,56.318,-2.674,56.253,-2.768,56.202,-2.98,56.194,-3.178,56.08,-3.268,56.045,-3.362,56.028,-3.48,56.033,-3.695,56.063,-3.789,56.095,-3.704,56.043,-3.608,56.016,-3.049,55.952,-3.015,55.959,-2.837,56.026,-2.599,56.027,-2.147,55.903,-2.017,55.808,-1.83,55.672,-1.729,55.619,-1.655,55.57,-1.61,55.498,-1.523,55.26,-1.423,55.026,-1.292,54.774,-1.232,54.704,-1.154,54.654,-0.759,54.541,-0.671,54.504,-0.518,54.395,-0.37,54.279,-0.233,54.19,-0.084,54.118,-0.156,54.081,-0.206,54.022,-0.169,53.942,-0.108,53.865,0.011,53.743,0.115,53.609,0.077,53.629,0.036,53.641,-0.019,53.637,-0.074,53.644,-0.174,53.685,-0.27,53.737,-0.461,53.716,-0.568,53.725,-0.66,53.724,-0.485,53.694,-0.294,53.692,0.128,53.468,0.271,53.335,0.356,53.16,0.298,53.081,0.208,53.03,0.124,52.972,0.046,52.906,0.28,52.809,0.33,52.812,0.382,52.825,0.432,52.858,0.516,52.938,0.559,52.967,0.704,52.977,0.827,52.971,0.949,52.953,1.056,52.959,1.271,52.925,1.382,52.894,1.657,52.754,1.716,52.677,1.743,52.579,1.747,52.469,1.7,52.369,1.647,52.279,1.615,52.162,1.591,52.12,1.559,52.087,1.413,51.995,1.317,51.957,1.276,51.974,1.232,51.971,1.228,51.949,1.274,51.902,1.274,51.845,1.188,51.803,1.101,51.785,0.955,51.808,0.752,51.73,0.898,51.689,0.927,51.647,0.891,51.571,0.799,51.538,0.698,51.523,0.593,51.519,0.507,51.501,0.425,51.466,0.528,51.484,0.6,51.468,0.646,51.405,0.687,51.387,0.889,51.36,1.015,51.36,1.257,51.375,1.373,51.375,1.415,51.363,1.416,51.311,1.398,51.182,1.366,51.155,1.044,51.047,0.979,50.972,0.96,50.926,0.772,50.934,0.684,50.886,0.532,50.853,0.415,50.819,0.3,50.776,0.205,50.763,-0.204,50.814,-0.451,50.81,-0.785,50.765,-0.871,50.773,-1.001,50.816,-1.133,50.845,-1.285,50.857,-1.416,50.897,-1.334,50.821,-1.517,50.747,-1.601,50.733,-1.688,50.735,-1.866,50.715,-2.031,50.725,-2.006,50.673,-1.962,50.628,-1.998,50.608,-2.036,50.603,-2.35,50.637,-2.395,50.631,-2.433,50.599,-2.548,50.616,-2.659,50.67,-2.777,50.706,-2.901,50.722,-2.999,50.717,-3.405,50.632,-3.485,50.548,-3.526,50.428,-3.584,50.322,-3.68,50.24,-3.793,50.229,-3.9,50.286,-4.103,50.349,-4.173,50.391,-4.195,50.393,-4.217,50.378,-4.297,50.359,-4.379,50.358,-4.507,50.341,-4.728,50.29,-4.817,50.256,-5.01,50.161,-5.049,50.134,-5.119,50.038,-5.225,50.021,-5.323,50.083,-5.434,50.104,-5.551,50.083,-5.622,50.051,-5.655,50.077,-5.656,50.132,-5.571,50.197,-5.342,50.246,-5.142,50.374,-5.043,50.452,-5.004,50.495,-4.956,50.523,-4.894,50.534,-4.861,50.582,-4.583,50.776,-4.56,50.821,-4.546,50.901,-4.523,50.977,-4.296,51.027,-4.188,51.189,-4.158,51.201,-3.842,51.231,-3.608,51.229,-3.375,51.197,-3.256,51.194,-3.136,51.205,-3.042,51.249,-2.881,51.406,-2.791,51.475,-2.687,51.537,-2.59,51.609,-2.433,51.741,-2.539,51.695,-2.668,51.623,-2.742,51.581,-2.979,51.539,-3.08,51.496,-3.259,51.398,-3.293,51.39,-3.562,51.414,-3.763,51.54,-3.891,51.592,-3.944,51.598,-3.998,51.582,-4.115,51.566,-4.235,51.569,-4.174,51.627,-4.091,51.66,-4.276,51.683,-4.328,51.7,-4.386,51.741,-4.531,51.748,-4.601,51.738,-4.718,51.684,-4.902,51.626,-5.125,51.706,-5.168,51.741,-5.167,51.808,-5.201,51.861,-5.262,51.88,-5.183,51.95,-5.088,51.996,-4.879,52.042,-4.561,52.151,-4.383,52.197,-4.218,52.277,-4.149,52.326,-4.1,52.393,-4.051,52.475,-3.98,52.542,-4.048,52.558,-4.079,52.608,-4.071,52.659,-4.039,52.704,-4.067,52.761,-4.118,52.82,-4.115,52.866,-4.101,52.915,-4.229,52.913,-4.356,52.897,-4.472,52.862,-4.584,52.815,-4.683,52.806,-4.681,52.844,-4.638,52.891,-4.526,52.958,-4.405,53.014,-4.362,53.056,-4.328,53.105,-4.269,53.145,-4.111,53.219,-3.809,53.303,-3.764,53.308,-3.646,53.298,-3.53,53.311,-3.428,53.341,-3.326,53.347,-3.098,53.26,-3.166,53.395,-3.065,53.427,-2.919,53.305,-2.864,53.293,-2.75,53.31,-2.794,53.331,-2.845,53.332,-2.913,53.35,-2.97,53.389,-3.065,53.513,-3.059,53.586,-2.996,53.663,-2.925,53.733,-2.984,53.747,-3.032,53.774,-3.045,53.844,-3.027,53.906,-2.9,53.961,-2.862,54.044,-2.846,54.135,-2.868,54.177,-2.994,54.171,-3.055,54.153,-3.11,54.126,-3.166,54.128,-3.322,54.229,-3.41,54.306,-3.569,54.468,-3.592,54.564,-3.465,54.773,-3.268,54.907,-3.036,54.953,-3.081,54.962,-3.434,54.964,-3.55,54.947,-3.658,54.893,-3.719,54.876,-3.783,54.87,-3.842,54.843,-3.899,54.805,-3.958,54.781,-4.076,54.787,-4.133,54.779,-4.174,54.801,-4.208,54.837,-4.253,54.847,-4.304,54.836,-4.41,54.787,-4.517,54.758,-4.648,54.789,-4.818,54.846,-4.852,54.825,-4.89,54.772,-4.911,54.689,-5.032,54.761,-5.135,54.858,-5.17,54.918,-5.173,54.986,-5.117,55.012,-5.056,54.988,-4.965,55.149,-4.785,55.359,-4.721,55.421,-4.677,55.501,-4.684,55.554,-4.724,55.598,-4.892,55.699,-4.89,55.781,-4.872,55.874,-4.826,55.93,-4.807,55.94,-4.584,55.939,-4.671,55.967,-4.844,56.051,-4.841,56.081,-4.8,56.158,-4.819,56.15,-4.856,56.115,-4.927,56.028,-4.97,56.008,-5.093,55.987,-5.115,55.945,-5.135,55.933,-5.196,55.929,-5.215,55.889,-5.228,55.886,-5.246,55.929,-5.247,56,-5.223,56.066,-5.176,56.117,-4.997,56.233,-5.084,56.197,-5.282,56.09,-5.383,56.019,-5.41,55.995,-5.419,55.975,-5.418,55.952,-5.373,55.828,-5.386,55.77,-5.556,55.39,-5.589,55.351,-5.618,55.331,-5.647,55.327,-5.731,55.334,-5.768,55.363,-5.768,55.395,-5.752,55.443,-5.681,55.624,-5.651,55.674,-5.605,55.721,-5.504,55.802,-5.507,55.808,-5.574,55.792,-5.602,55.797,-5.623,55.813,-5.61,56.055,-5.555,56.135,-5.535,56.251,-5.488,56.35,-5.433,56.422,-5.392,56.515,-5.329,56.556,-5.313,56.619,-5.243,56.687,-5.188,56.758,-5.218,56.751,-5.564,56.566,-5.652,56.532,-5.773,56.541,-5.865,56.562,-5.937,56.606,-5.969,56.69,-6.058,56.692,-6.134,56.707,-6.133,56.718,-6.035,56.764,-5.878,56.78,-5.731,56.853,-5.861,56.903,-5.85,56.918,-5.736,56.961,-5.591,57.102,-5.562,57.233,-5.631,57.294,-5.656,57.334,-5.795,57.379,-5.818,57.436,-5.802,57.468,-5.757,57.499,-5.689,57.524,-5.582,57.547,-5.679,57.572,-5.715,57.601,-5.742,57.644,-5.745,57.668,-5.695,57.778,-5.665,57.824,-5.608,57.881,-5.349,57.878,-5.319,57.904,-5.29,57.905,-5.157,57.881,-5.177,57.906,-5.394,58.044,-5.413,58.07,-5.351,58.144,-5.347,58.177,-5.356,58.212,-5.338,58.239,-5.27,58.251,-5.06,58.25,-5.008,58.263,-5.032,58.298,-5.081,58.345,-5.09,58.385,-5.079,58.419,-5.076,58.489,-5.067,58.52,-5.017,58.567,-4.976,58.58,-4.925,58.588,-4.81,58.573,-4.766,58.554,-4.715,58.51,-4.678,58.514,-4.535,58.562,-4.492,58.568,-4.433,58.513,-4.189,58.557,-3.86,58.577,-3.662,58.606,-3.454,58.617,-3.259,58.65,-3.053,58.635,-3.046,58.616,-3.057,58.589,-3.11,58.515,-3.101,58.434,-3.113,58.409,-3.137,58.378,-3.212,58.321,-3.411,58.24,-3.775,58.052,-3.99,57.959,-4.02,57.914,-4.036,57.852,-3.907,57.84,-3.857,57.819,-3.888,57.787,-4.078,57.677,-4.135,57.578,-3.988,57.581]},{"b":[-10.39,51.474,-5.47,55.366],"l":[-9.542,51.664,-9.525,51.681,-9.58,51.689,-9.899,51.647,-10.01,51.611,-10.121,51.601,-10.069,51.656,-9.926,51.731,-9.85,51.766,-9.803,51.78,-9.75,51.824,-9.599,51.874,-10.084,51.771,-10.212,51.784,-10.242,51.812,-10.341,51.799,-10.379,51.869,-10.232,51.975,-10.146,52.02,-10.044,52.045,-9.946,52.08,-9.91,52.123,-9.956,52.137,-10.25,52.126,-10.39,52.135,-10.383,52.169,-10.357,52.207,-10.211,52.272,-10.132,52.282,-10.062,52.276,-9.993,52.259,-9.937,52.238,-9.772,52.25,-9.841,52.291,-9.853,52.375,-9.906,52.404,-9.838,52.443,-9.761,52.466,-9.632,52.547,-9.586,52.559,-9.331,52.579,-9.056,52.621,-8.783,52.68,-8.923,52.712,-8.99,52.755,-9.098,52.668,-9.175,52.635,-9.394,52.617,-9.463,52.627,-9.561,52.654,-9.591,52.644,-9.62,52.623,-9.764,52.58,-9.917,52.57,-9.74,52.648,-9.515,52.781,-9.465,52.823,-9.394,52.896,-9.416,52.929,-9.462,52.947,-9.299,53.098,-9.242,53.125,-9.138,53.129,-9.061,53.153,-9.027,53.153,-8.997,53.162,-8.93,53.207,-9.034,53.236,-9.14,53.25,-9.471,53.235,-9.514,53.238,-9.555,53.252,-9.582,53.272,-9.602,53.323,-9.626,53.334,-9.701,53.334,-9.774,53.319,-9.825,53.32,-9.876,53.343,-9.795,53.395,-9.899,53.407,-10.004,53.397,-10.091,53.413,-10.094,53.446,-10.054,53.478,-10.106,53.509,-10.117,53.549,-10.062,53.568,-10.001,53.561,-9.878,53.59,-9.721,53.604,-9.856,53.633,-9.91,53.658,-9.912,53.695,-9.902,53.727,-9.745,53.781,-9.578,53.805,-9.591,53.841,-9.579,53.88,-9.748,53.891,-9.914,53.864,-9.896,53.938,-9.856,54.004,-9.848,54.048,-9.856,54.095,-9.934,54.075,-9.944,54.142,-9.977,54.187,-10.093,54.156,-10.09,54.216,-10.056,54.258,-9.996,54.276,-9.936,54.268,-9.825,54.269,-9.717,54.3,-9.562,54.309,-9.316,54.299,-9.146,54.21,-9.102,54.226,-9.034,54.282,-9.002,54.288,-8.747,54.263,-8.588,54.231,-8.546,54.241,-8.568,54.304,-8.623,54.347,-8.554,54.404,-8.471,54.442,-8.415,54.461,-8.287,54.485,-8.23,54.507,-8.193,54.58,-8.133,54.641,-8.457,54.609,-8.764,54.681,-8.715,54.732,-8.65,54.761,-8.538,54.783,-8.528,54.809,-8.471,54.832,-8.377,54.889,-8.412,54.965,-8.393,55.02,-8.326,55.056,-8.305,55.108,-8.275,55.146,-8.138,55.16,-8.006,55.195,-7.959,55.192,-7.803,55.2,-7.751,55.186,-7.763,55.248,-7.667,55.256,-7.63,55.244,-7.613,55.2,-7.57,55.171,-7.557,55.122,-7.586,55.084,-7.634,55.055,-7.59,55.025,-7.659,54.971,-7.584,54.994,-7.478,55.047,-7.484,55.09,-7.502,55.145,-7.531,55.194,-7.518,55.248,-7.458,55.282,-7.302,55.299,-7.366,55.36,-7.309,55.366,-7.247,55.353,-7.155,55.305,-7.06,55.268,-6.962,55.238,-7.056,55.178,-7.173,55.137,-7.219,55.092,-7.179,55.057,-7.101,55.048,-7.031,55.081,-6.947,55.183,-6.889,55.189,-6.825,55.181,-6.699,55.193,-6.475,55.241,-6.375,55.242,-6.234,55.217,-6.129,55.217,-6.036,55.145,-5.986,55.03,-5.869,54.916,-5.717,54.817,-5.711,54.757,-5.765,54.725,-5.879,54.684,-5.879,54.641,-5.803,54.663,-5.739,54.673,-5.583,54.663,-5.528,54.62,-5.49,54.554,-5.47,54.5,-5.484,54.442,-5.526,54.46,-5.569,54.513,-5.616,54.537,-5.671,54.55,-5.646,54.478,-5.656,54.382,-5.632,54.373,-5.558,54.371,-5.607,54.273,-5.708,54.246,-5.826,54.236,-5.855,54.201,-5.876,54.156,-5.938,54.089,-6.019,54.051,-6.12,54.059,-6.218,54.089,-6.176,54.054,-6.157,54.017,-6.231,54.004,-6.308,54.011,-6.345,53.987,-6.348,53.941,-6.322,53.882,-6.27,53.84,-6.229,53.746,-6.195,53.641,-6.142,53.578,-6.131,53.499,-6.139,53.46,-6.129,53.391,-6.152,53.366,-6.135,53.301,-6.072,53.166,-6.045,53.091,-6.027,52.927,-6.071,52.866,-6.131,52.807,-6.169,52.738,-6.199,52.663,-6.217,52.543,-6.345,52.402,-6.4,52.367,-6.463,52.345,-6.325,52.247,-6.438,52.203,-6.561,52.189,-6.697,52.214,-6.782,52.21,-6.86,52.179,-6.89,52.159,-6.915,52.169,-6.966,52.25,-7.003,52.166,-7.082,52.139,-7.216,52.145,-7.441,52.123,-7.527,52.099,-7.563,52.062,-7.59,52.019,-7.625,51.993,-7.665,51.98,-7.838,51.948,-7.872,51.935,-7.952,51.866,-8.058,51.826,-8.145,51.814,-8.222,51.854,-8.254,51.878,-8.29,51.891,-8.409,51.889,-8.372,51.876,-8.347,51.848,-8.336,51.793,-8.349,51.739,-8.408,51.712,-8.478,51.707,-8.588,51.651,-8.734,51.636,-8.813,51.585,-9.296,51.498,-9.324,51.497,-9.391,51.519,-9.463,51.529,-9.535,51.522,-9.737,51.474,-9.835,51.483,-9.71,51.604,-9.542,51.664]},{"b":[-180,64.28,-169.729,68.983],"l":[-180,68.983,-179.799,68.94,-179.595,68.906,-179.515,68.917,-179.471,68.912,-179.356,68.853,-179.279,68.825,-178.874,68.754,-178.689,68.675,-178.539,68.586,-178.614,68.603,-178.751,68.66,-178.737,68.593,-178.693,68.546,-178.474,68.502,-178.244,68.467,-178.097,68.425,-178.049,68.388,-178.019,68.323,-178.056,68.265,-177.922,68.287,-177.797,68.338,-177.862,68.378,-178.285,68.519,-178.373,68.566,-178.25,68.541,-177.683,68.363,-177.527,68.294,-177.593,68.281,-177.639,68.241,-177.589,68.224,-177.521,68.237,-177.408,68.245,-177.297,68.223,-177.172,68.175,-176.907,68.119,-175.345,67.678,-175.31,67.602,-175.266,67.567,-175.24,67.521,-175.233,67.447,-175.375,67.357,-175.155,67.365,-175.123,67.377,-175.066,67.413,-175.003,67.438,-174.918,67.408,-174.85,67.349,-174.87,67.269,-174.93,67.203,-174.938,67.093,-174.885,67,-174.829,66.961,-174.784,66.917,-174.771,66.784,-174.87,66.725,-174.925,66.623,-174.864,66.613,-174.675,66.603,-174.612,66.585,-174.504,66.538,-174.478,66.492,-174.454,66.43,-174.419,66.372,-174.394,66.344,-174.366,66.348,-174.257,66.428,-174.206,66.452,-174.085,66.473,-174.018,66.383,-174.065,66.23,-174.025,66.23,-173.994,66.246,-173.955,66.287,-173.9,66.31,-173.832,66.366,-173.774,66.435,-173.843,66.488,-173.921,66.522,-174.102,66.541,-174.196,66.581,-174.232,66.632,-174.14,66.653,-174.061,66.69,-174.006,66.779,-174.019,66.827,-174.041,66.875,-174.086,66.943,-174.154,66.982,-174.284,67.002,-174.342,67.04,-174.431,67.038,-174.519,67.049,-174.554,67.063,-174.55,67.091,-174.448,67.103,-173.884,67.106,-173.68,67.145,-173.587,67.133,-173.494,67.105,-173.158,67.069,-173.168,67.052,-173.224,67.035,-173.324,66.955,-173.343,66.909,-173.347,66.851,-173.259,66.84,-173.175,66.865,-173.216,66.911,-173.228,66.969,-173.193,66.994,-173.147,66.999,-173.058,66.956,-172.963,66.942,-172.641,66.925,-172.549,66.931,-172.52,66.952,-172.583,66.978,-173.002,67.034,-173.008,67.065,-172.621,67.027,-172.447,66.992,-172.274,66.966,-172.031,66.973,-171.796,66.932,-171.57,66.819,-171.36,66.677,-171.149,66.593,-170.927,66.53,-170.556,66.357,-170.51,66.344,-170.473,66.32,-170.543,66.291,-170.604,66.249,-170.483,66.278,-170.361,66.298,-170.301,66.294,-170.247,66.272,-170.212,66.236,-170.192,66.201,-170.244,66.169,-169.889,66.163,-169.778,66.143,-169.729,66.058,-169.832,65.999,-169.892,66.006,-169.949,66.031,-170.004,66.033,-170.159,66.008,-170.401,65.929,-170.541,65.865,-170.563,65.824,-170.541,65.71,-170.561,65.656,-170.666,65.622,-170.897,65.643,-171.001,65.665,-171.119,65.695,-171.232,65.737,-171.377,65.804,-171.422,65.81,-171.451,65.794,-171.402,65.752,-171.303,65.698,-171.134,65.628,-171.054,65.55,-171.106,65.511,-171.17,65.502,-171.216,65.503,-171.364,65.527,-171.466,65.533,-171.79,65.51,-171.907,65.496,-171.947,65.508,-171.957,65.542,-172.131,65.567,-172.234,65.57,-172.282,65.582,-172.322,65.618,-172.436,65.67,-172.608,65.69,-172.719,65.692,-172.783,65.681,-172.557,65.612,-172.354,65.496,-172.392,65.475,-172.418,65.45,-172.306,65.448,-172.233,65.456,-172.212,65.425,-172.27,65.303,-172.309,65.276,-172.662,65.249,-172.573,65.228,-172.482,65.222,-172.379,65.227,-172.286,65.206,-172.224,65.129,-172.213,65.048,-172.304,65.002,-172.399,64.965,-172.593,64.908,-172.792,64.883,-172.897,64.889,-172.999,64.877,-173.066,64.847,-173.086,64.817,-172.998,64.837,-172.897,64.826,-172.801,64.791,-172.812,64.761,-172.903,64.729,-172.924,64.705,-172.889,64.664,-172.901,64.629,-172.854,64.61,-172.747,64.603,-172.616,64.578,-172.487,64.544,-172.437,64.515,-172.394,64.475,-172.379,64.432,-172.401,64.414,-172.695,64.407,-172.739,64.412,-172.756,64.46,-172.792,64.499,-172.903,64.526,-172.949,64.507,-172.916,64.369,-172.96,64.328,-173.009,64.297,-173.157,64.28,-173.275,64.29,-173.376,64.355,-173.376,64.41,-173.309,64.443,-173.309,64.487,-173.327,64.54,-173.396,64.479,-173.475,64.429,-173.604,64.365,-173.666,64.357,-173.73,64.365,-173.898,64.41,-174.001,64.449,-174.205,64.578,-174.318,64.638,-174.571,64.718,-174.83,64.776,-175.036,64.814,-175.146,64.809,-175.256,64.794,-175.395,64.802,-175.442,64.817,-175.483,64.849,-175.521,64.867,-175.716,64.946,-175.854,65.011,-175.859,65.054,-175.83,65.106,-175.856,65.233,-175.923,65.352,-176.093,65.471,-176.547,65.548,-176.922,65.601,-177.056,65.614,-177.175,65.602,-177.489,65.504,-177.699,65.49,-178.31,65.485,-178.412,65.496,-178.505,65.537,-178.526,65.593,-178.499,65.697,-178.502,65.74,-178.526,65.755,-178.559,65.754,-178.679,65.795,-178.791,65.865,-178.879,65.936,-178.939,66.033,-178.858,66.038,-178.747,66.014,-178.731,66.037,-178.694,66.124,-178.616,66.166,-178.587,66.198,-178.534,66.317,-178.527,66.402,-178.616,66.355,-178.753,66.237,-178.821,66.203,-178.868,66.187,-178.916,66.18,-179.026,66.204,-179.105,66.232,-179.107,66.346,-179.143,66.375,-179.178,66.353,-179.193,66.313,-179.293,66.305,-179.34,66.288,-179.316,66.22,-179.327,66.163,-179.423,66.141,-179.616,66.128,-179.683,66.184,-179.741,66.106,-179.784,66.018,-179.79,65.901,-179.728,65.804,-179.641,65.758,-179.449,65.688,-179.366,65.639,-179.344,65.575,-179.352,65.517,-179.452,65.445,-179.519,65.386,-179.635,65.244,-179.705,65.187,-180,65.067]},{"b":[-24.476,63.407,-13.556,66.526],"l":[-14.788,66.331,-14.912,66.284,-15.03,66.178,-15.117,66.126,-15.116,66.102,-15.01,66.061,-14.894,66.038,-14.787,66.059,-14.74,66.051,-14.698,66.02,-14.674,65.99,-14.669,65.96,-14.688,65.897,-14.753,65.834,-14.839,65.781,-14.827,65.764,-14.758,65.756,-14.426,65.79,-14.392,65.787,-14.373,65.77,-14.369,65.739,-14.351,65.71,-14.318,65.684,-14.328,65.658,-14.473,65.575,-14.302,65.628,-14.167,65.642,-13.935,65.616,-13.841,65.586,-13.785,65.533,-13.705,65.551,-13.67,65.55,-13.618,65.519,-13.616,65.487,-13.654,65.441,-13.668,65.399,-13.708,65.382,-13.783,65.369,-13.805,65.355,-13.772,65.323,-13.723,65.291,-13.653,65.29,-13.641,65.275,-13.64,65.257,-13.649,65.237,-13.672,65.223,-13.707,65.215,-13.755,65.193,-13.581,65.143,-13.559,65.125,-13.556,65.098,-13.57,65.068,-13.599,65.036,-13.652,65.017,-13.777,65.014,-13.854,64.993,-13.828,64.958,-13.83,64.914,-13.853,64.862,-13.952,64.784,-14.044,64.742,-14.135,64.715,-14.297,64.724,-14.385,64.745,-14.375,64.677,-14.465,64.636,-14.448,64.601,-14.417,64.583,-14.433,64.538,-14.475,64.494,-14.547,64.446,-14.628,64.416,-14.79,64.38,-14.927,64.32,-15.022,64.296,-15.256,64.297,-15.495,64.258,-15.833,64.177,-16.06,64.111,-16.236,64.037,-16.468,63.916,-16.64,63.865,-16.74,63.852,-16.933,63.841,-17.095,63.808,-17.633,63.747,-17.816,63.713,-17.839,63.682,-17.915,63.636,-17.92,63.62,-17.886,63.607,-17.88,63.59,-17.947,63.536,-18.08,63.496,-18.143,63.497,-18.219,63.531,-18.252,63.53,-18.265,63.525,-18.266,63.514,-18.222,63.473,-18.303,63.454,-18.654,63.407,-19.25,63.442,-19.487,63.479,-19.778,63.537,-19.952,63.552,-20.198,63.556,-20.4,63.637,-20.494,63.687,-20.502,63.708,-20.491,63.732,-20.47,63.748,-20.438,63.757,-20.372,63.758,-20.363,63.765,-20.414,63.805,-20.463,63.792,-20.593,63.735,-20.651,63.737,-20.727,63.766,-20.73,63.793,-20.879,63.804,-21.008,63.838,-21.137,63.888,-21.156,63.907,-21.094,63.934,-21.106,63.94,-21.152,63.945,-21.246,63.935,-21.388,63.873,-21.449,63.858,-22.373,63.844,-22.607,63.837,-22.652,63.828,-22.693,63.869,-22.729,63.959,-22.743,64.019,-22.734,64.048,-22.701,64.083,-22.651,64.077,-22.603,64.05,-22.56,64.01,-22.51,63.991,-22.188,64.039,-22.057,64.071,-22.001,64.102,-21.935,64.154,-21.866,64.18,-21.833,64.205,-21.768,64.285,-21.723,64.322,-21.669,64.349,-21.606,64.367,-21.463,64.379,-21.557,64.398,-21.647,64.398,-21.951,64.314,-22.053,64.314,-22.049,64.327,-22.006,64.351,-21.901,64.392,-21.973,64.395,-22.001,64.413,-22.004,64.452,-21.95,64.515,-21.702,64.598,-21.617,64.61,-21.591,64.626,-21.623,64.64,-21.675,64.648,-21.924,64.563,-22.106,64.533,-22.16,64.539,-22.254,64.572,-22.284,64.587,-22.325,64.624,-22.32,64.647,-22.234,64.714,-22.248,64.727,-22.307,64.733,-22.467,64.795,-22.72,64.789,-23.347,64.824,-23.476,64.809,-23.69,64.757,-23.819,64.739,-23.879,64.751,-23.933,64.779,-23.982,64.816,-24.026,64.863,-24.007,64.896,-23.924,64.915,-23.864,64.924,-23.693,64.913,-23.485,64.946,-23.353,64.953,-23.315,64.958,-23.237,64.993,-23.198,65.002,-23.138,64.99,-23.109,64.966,-22.9,65.003,-22.828,65.022,-22.82,65.033,-22.788,65.046,-22.684,65.026,-22.6,65.026,-22.494,65.04,-22.308,65.046,-21.892,65.049,-21.83,65.079,-21.8,65.106,-21.764,65.174,-21.78,65.188,-22.04,65.125,-22.099,65.126,-22.4,65.159,-22.509,65.197,-22.473,65.227,-22.314,65.292,-22.149,65.344,-21.907,65.4,-21.85,65.422,-21.844,65.447,-22.006,65.493,-22.311,65.481,-22.39,65.535,-22.644,65.568,-22.813,65.547,-22.902,65.58,-23.122,65.535,-23.605,65.469,-23.796,65.423,-23.9,65.408,-24.019,65.445,-24.224,65.487,-24.455,65.5,-24.476,65.525,-24.341,65.601,-24.249,65.615,-24.156,65.608,-23.979,65.555,-23.857,65.538,-24.01,65.616,-24.006,65.646,-24.018,65.691,-24.065,65.71,-24.112,65.76,-24.093,65.776,-24.032,65.782,-23.909,65.766,-23.616,65.68,-23.472,65.695,-23.393,65.727,-23.285,65.75,-23.316,65.762,-23.569,65.764,-23.705,65.781,-23.773,65.806,-23.833,65.849,-23.812,65.869,-23.741,65.885,-23.525,65.88,-23.667,65.954,-23.767,65.997,-23.777,66.018,-23.771,66.043,-23.757,66.061,-23.737,66.069,-23.489,66.026,-23.434,66.024,-23.485,66.052,-23.594,66.093,-23.599,66.109,-23.553,66.122,-23.53,66.145,-23.528,66.164,-23.453,66.181,-23.377,66.182,-23.3,66.167,-23.063,66.086,-23.029,66.064,-23.017,66.034,-23.029,65.997,-23.019,65.982,-22.926,65.995,-22.852,65.979,-22.815,65.983,-22.723,66.039,-22.66,66.026,-22.622,66,-22.61,65.976,-22.604,65.944,-22.62,65.877,-22.616,65.867,-22.552,65.905,-22.442,65.908,-22.428,65.927,-22.424,65.998,-22.433,66.058,-22.445,66.07,-22.806,66.153,-22.869,66.172,-22.948,66.213,-22.932,66.233,-22.862,66.251,-22.756,66.259,-22.509,66.258,-22.484,66.266,-22.532,66.288,-22.646,66.302,-22.673,66.314,-22.686,66.338,-22.821,66.325,-22.972,66.324,-23.117,66.339,-23.12,66.357,-23.063,66.384,-22.944,66.429,-22.889,66.441,-22.724,66.433,-22.559,66.445,-22.426,66.43,-22.32,66.385,-22.17,66.307,-21.967,66.257,-21.948,66.241,-21.84,66.2,-21.625,66.09,-21.407,66.026,-21.397,66.009,-21.433,65.99,-21.517,65.968,-21.497,65.955,-21.388,65.939,-21.309,65.895,-21.303,65.876,-21.375,65.742,-21.413,65.713,-21.457,65.698,-21.658,65.724,-21.61,65.681,-21.466,65.635,-21.434,65.61,-21.455,65.585,-21.439,65.579,-21.387,65.592,-21.365,65.578,-21.374,65.536,-21.396,65.502,-21.432,65.474,-21.422,65.462,-21.365,65.466,-21.313,65.459,-21.23,65.421,-21.163,65.304,-21.13,65.267,-21.106,65.3,-21.076,65.385,-21.047,65.428,-21.021,65.43,-20.998,65.445,-20.979,65.471,-20.94,65.565,-20.804,65.636,-20.74,65.658,-20.679,65.663,-20.649,65.654,-20.548,65.579,-20.487,65.567,-20.455,65.571,-20.412,65.622,-20.357,65.719,-20.344,65.828,-20.374,65.948,-20.357,66.033,-20.292,66.084,-20.208,66.1,-20.103,66.08,-20.026,66.049,-19.875,65.93,-19.753,65.868,-19.648,65.801,-19.594,65.779,-19.49,65.768,-19.462,65.772,-19.443,65.788,-19.434,65.814,-19.456,65.985,-19.427,66.038,-19.383,66.076,-19.195,66.098,-19.093,66.122,-18.994,66.16,-18.911,66.181,-18.846,66.184,-18.778,66.169,-18.706,66.136,-18.595,66.071,-18.455,65.965,-18.277,65.885,-18.184,65.758,-18.164,65.737,-18.142,65.734,-18.118,65.751,-18.103,65.774,-18.099,65.83,-18.149,65.905,-18.315,66.093,-18.318,66.129,-18.297,66.157,-18.18,66.161,-17.907,66.143,-17.82,66.114,-17.634,65.999,-17.582,65.971,-17.55,65.964,-17.539,65.978,-17.467,66,-17.417,66.026,-17.334,66.089,-17.153,66.203,-17.115,66.206,-17.062,66.197,-16.97,66.167,-16.925,66.143,-16.838,66.125,-16.748,66.132,-16.625,66.172,-16.485,66.196,-16.437,66.253,-16.428,66.278,-16.541,66.447,-16.493,66.481,-16.249,66.523,-16.036,66.526,-15.985,66.515,-15.851,66.433,-15.76,66.392,-15.714,66.359,-15.703,66.286,-15.647,66.259,-15.543,66.229,-15.428,66.225,-15.241,66.259,-15.162,66.282,-14.97,66.36,-14.856,66.381,-14.681,66.376,-14.596,66.382,-14.594,66.374,-14.702,66.342,-14.788,66.331]},{"b":[-65.024,18.321,-64.845,18.385],"l":[-64.845,18.33,-64.889,18.374,-64.942,18.385,-65.024,18.368,-64.92,18.321,-64.845,18.33]},{"b":[178.475,51.899,178.607,51.995],"l":[178.571,51.978,178.509,51.995,178.475,51.968,178.478,51.943,178.512,51.899,178.575,51.916,178.607,51.953,178.571,51.978]},{"b":[14.18,36.012,14.321,36.076],"l":[14.18,36.06,14.194,36.042,14.254,36.012,14.313,36.028,14.321,36.036,14.304,36.062,14.263,36.076,14.18,36.06]},{"b":[13.854,40.697,13.962,40.762],"l":[13.871,40.762,13.854,40.724,13.868,40.709,13.894,40.697,13.938,40.706,13.961,40.718,13.962,40.739,13.871,40.762]},{"b":[11.936,36.746,12.051,36.843],"l":[11.948,36.843,11.936,36.829,11.941,36.78,12.003,36.746,12.051,36.757,12.048,36.776,12.024,36.821,11.948,36.843]},{"b":[6.159,53.477,6.333,53.515],"l":[6.193,53.477,6.333,53.511,6.291,53.515,6.168,53.494,6.159,53.484,6.193,53.477]},{"b":[5.654,53.443,5.929,53.475],"l":[5.708,53.473,5.654,53.467,5.665,53.455,5.732,53.443,5.929,53.459,5.928,53.465,5.876,53.475,5.708,53.473]},{"b":[4.908,53.235,5.109,53.31],"l":[5.027,53.31,4.908,53.246,4.924,53.235,5.109,53.308,5.027,53.31]},{"b":[5.19,53.378,5.583,53.444],"l":[5.415,53.431,5.19,53.392,5.233,53.378,5.326,53.386,5.583,53.438,5.557,53.444,5.415,53.431]},{"b":[-179.079,-17.992,-178.988,-17.92],"l":[-179.039,-17.988,-179.018,-17.992,-178.988,-17.977,-178.999,-17.947,-179.048,-17.92,-179.079,-17.944,-179.064,-17.972,-179.039,-17.988]},{"b":[-178.848,-18.252,-178.762,-18.186],"l":[-178.848,-18.202,-178.827,-18.222,-178.774,-18.252,-178.762,-18.234,-178.763,-18.191,-178.791,-18.186,-178.848,-18.202]},{"b":[-178.357,-17.963,-178.251,-17.876],"l":[-178.325,-17.876,-178.357,-17.921,-178.307,-17.963,-178.251,-17.953,-178.255,-17.93,-178.28,-17.886,-178.325,-17.876]},{"b":[-151.512,-16.665,-151.438,-16.574],"l":[-151.506,-16.574,-151.512,-16.619,-151.504,-16.647,-151.485,-16.665,-151.467,-16.658,-151.438,-16.623,-151.457,-16.604,-151.506,-16.574]},{"b":[-149.912,-17.571,-149.782,-17.47],"l":[-149.814,-17.545,-149.782,-17.488,-149.809,-17.474,-149.902,-17.47,-149.912,-17.501,-149.905,-17.528,-149.887,-17.553,-149.845,-17.571,-149.814,-17.545]},{"b":[-120.442,34.022,-120.307,34.073],"l":[-120.353,34.061,-120.368,34.073,-120.413,34.056,-120.442,34.033,-120.36,34.022,-120.307,34.025,-120.353,34.061]},{"b":[-119.575,33.215,-119.438,33.282],"l":[-119.525,33.282,-119.575,33.278,-119.544,33.225,-119.483,33.215,-119.438,33.217,-119.442,33.232,-119.479,33.275,-119.525,33.282]},{"b":[-67.85,-55.892,-67.51,-55.816],"l":[-67.762,-55.816,-67.834,-55.828,-67.85,-55.843,-67.846,-55.857,-67.832,-55.865,-67.7,-55.873,-67.611,-55.892,-67.575,-55.89,-67.545,-55.877,-67.51,-55.844,-67.517,-55.833,-67.545,-55.826,-67.762,-55.816]},{"b":[-70.066,12.423,-69.896,12.614],"l":[-69.973,12.568,-70.035,12.614,-70.051,12.597,-70.066,12.547,-70.004,12.5,-69.942,12.439,-69.896,12.423,-69.899,12.452,-69.912,12.48,-69.973,12.568]},{"b":[-65.415,10.884,-65.213,10.974],"l":[-65.383,10.974,-65.415,10.938,-65.365,10.906,-65.266,10.884,-65.213,10.906,-65.227,10.93,-65.302,10.974,-65.383,10.974]},{"b":[-73.285,18.707,-72.805,18.967],"l":[-73.171,18.967,-73.276,18.954,-73.285,18.897,-73.078,18.791,-72.822,18.707,-72.805,18.778,-72.919,18.861,-73.069,18.932,-73.171,18.967]},{"b":[-25.198,36.942,-25.027,37.024],"l":[-25.164,37.019,-25.198,36.997,-25.16,36.943,-25.088,36.949,-25.032,36.942,-25.027,36.96,-25.044,37,-25.083,37.024,-25.164,37.019]},{"b":[16.651,42.896,17.188,42.997],"l":[16.978,42.928,17.089,42.915,17.188,42.917,17.17,42.933,17.094,42.964,16.971,42.981,16.836,42.969,16.651,42.997,16.666,42.96,16.696,42.934,16.739,42.913,16.851,42.896,16.978,42.928]},{"b":[16.376,43.115,17.194,43.229],"l":[17.124,43.115,17.194,43.126,17.061,43.144,16.697,43.175,16.656,43.214,16.521,43.229,16.376,43.214,16.406,43.197,16.55,43.144,16.679,43.123,17.124,43.115]},{"b":[16.423,43.268,16.891,43.387],"l":[16.627,43.268,16.49,43.286,16.423,43.317,16.428,43.343,16.449,43.387,16.602,43.382,16.834,43.351,16.891,43.315,16.874,43.298,16.785,43.271,16.627,43.268]},{"b":[-2.646,49.429,-2.512,49.507],"l":[-2.646,49.468,-2.639,49.451,-2.547,49.429,-2.512,49.495,-2.521,49.506,-2.542,49.507,-2.646,49.468]},{"b":[-2.236,49.17,-2.01,49.266],"l":[-2.221,49.266,-2.236,49.176,-2.166,49.187,-2.091,49.187,-2.054,49.17,-2.01,49.181,-2.019,49.231,-2.082,49.255,-2.221,49.266]},{"b":[145.646,18.725,145.708,18.807],"l":[145.678,18.725,145.708,18.763,145.707,18.79,145.69,18.802,145.646,18.807,145.653,18.753,145.678,18.725]},{"b":[145.631,16.332,145.72,16.38],"l":[145.631,16.378,145.636,16.352,145.658,16.336,145.69,16.332,145.712,16.339,145.72,16.36,145.696,16.38,145.631,16.378]},{"b":[145.152,14.111,145.265,14.189],"l":[145.152,14.164,145.157,14.137,145.18,14.121,145.215,14.111,145.265,14.158,145.265,14.18,145.232,14.189,145.152,14.164]},{"b":[-60.117,43.904,-59.727,44.003],"l":[-59.866,43.947,-59.936,43.94,-60.117,43.953,-60.114,43.939,-60.038,43.907,-59.922,43.904,-59.788,43.94,-59.727,44.003,-59.866,43.947]},{"b":[-97.036,27.899,-96.84,28.133],"l":[-96.84,28.089,-96.857,28.133,-96.899,28.117,-96.979,28.014,-96.988,27.981,-97.036,27.899,-97.014,27.902,-96.921,28.016,-96.84,28.089]},{"b":[-88.944,29.66,-88.873,29.753],"l":[-88.941,29.68,-88.944,29.66,-88.889,29.713,-88.873,29.753,-88.901,29.733,-88.941,29.68]},{"b":[-88.723,30.205,-88.558,30.264],"l":[-88.723,30.264,-88.713,30.245,-88.659,30.226,-88.571,30.205,-88.558,30.216,-88.574,30.229,-88.723,30.264]},{"b":[-88.316,30.231,-88.071,30.274],"l":[-88.109,30.274,-88.264,30.255,-88.316,30.24,-88.29,30.233,-88.159,30.231,-88.071,30.252,-88.109,30.274]},{"b":[-63.125,18.019,-63.009,18.115],"l":[-63.115,18.091,-63.125,18.064,-63.09,18.041,-63.023,18.019,-63.012,18.045,-63.009,18.104,-63.025,18.113,-63.063,18.115,-63.115,18.091]},{"b":[-65.572,18.1,-65.295,18.165],"l":[-65.572,18.137,-65.555,18.108,-65.504,18.1,-65.426,18.106,-65.295,18.133,-65.303,18.144,-65.366,18.161,-65.477,18.165,-65.572,18.137]},{"b":[-62.625,17.101,-62.532,17.201],"l":[-62.625,17.13,-62.582,17.101,-62.532,17.122,-62.534,17.17,-62.575,17.201,-62.615,17.199,-62.625,17.13]},{"b":[-64.444,18.457,-64.325,18.517],"l":[-64.426,18.513,-64.444,18.473,-64.438,18.459,-64.421,18.457,-64.395,18.465,-64.325,18.517,-64.426,18.513]},{"b":[-64.788,18.328,-64.66,18.372],"l":[-64.788,18.341,-64.771,18.332,-64.726,18.328,-64.66,18.354,-64.752,18.372,-64.788,18.341]},{"b":[-64.695,18.399,-64.545,18.446],"l":[-64.651,18.443,-64.695,18.412,-64.672,18.399,-64.594,18.403,-64.545,18.438,-64.569,18.446,-64.651,18.443]},{"b":[-64.889,17.702,-64.58,17.794],"l":[-64.889,17.702,-64.686,17.706,-64.58,17.75,-64.682,17.75,-64.766,17.794,-64.885,17.772,-64.889,17.702]},{"b":[-77.562,24.993,-77.269,25.083],"l":[-77.451,25.081,-77.527,25.058,-77.562,25.03,-77.541,25.014,-77.46,24.993,-77.348,25.014,-77.269,25.044,-77.276,25.056,-77.329,25.083,-77.451,25.081]},{"b":[-81.137,24.693,-80.93,24.759],"l":[-81.085,24.734,-81.137,24.71,-81.09,24.693,-81.044,24.717,-80.989,24.728,-80.93,24.759,-81.085,24.734]},{"b":[114.134,22.195,114.247,22.297],"l":[114.134,22.292,114.139,22.268,114.207,22.195,114.232,22.211,114.244,22.234,114.247,22.264,114.187,22.297,114.134,22.292]},{"b":[-76.674,34.653,-76.546,34.7],"l":[-76.622,34.695,-76.546,34.655,-76.569,34.653,-76.608,34.664,-76.662,34.685,-76.674,34.7,-76.622,34.695]},{"b":[-175.362,-21.264,-175.078,-21.068],"l":[-175.078,-21.129,-175.084,-21.161,-175.157,-21.264,-175.202,-21.223,-175.335,-21.158,-175.362,-21.107,-175.318,-21.068,-175.323,-21.099,-175.3,-21.113,-175.225,-21.119,-175.158,-21.146,-175.2,-21.156,-175.162,-21.169,-175.148,-21.169,-175.132,-21.14,-175.078,-21.129]},{"b":[52.554,45.401,52.693,45.528],"l":[52.66,45.518,52.609,45.528,52.554,45.474,52.598,45.428,52.665,45.401,52.682,45.412,52.693,45.461,52.66,45.518]},{"b":[47.918,45.47,47.987,45.647],"l":[47.987,45.554,47.947,45.647,47.918,45.618,47.92,45.562,47.968,45.47,47.983,45.488,47.987,45.554]},{"b":[-58.541,-52.099,-58.433,-51.999],"l":[-58.497,-51.999,-58.541,-52.028,-58.513,-52.071,-58.433,-52.099,-58.439,-52.011,-58.461,-52.002,-58.497,-51.999]},{"b":[42.549,15.281,42.625,15.433],"l":[42.57,15.407,42.549,15.32,42.559,15.281,42.59,15.303,42.61,15.332,42.625,15.368,42.602,15.433,42.57,15.407]},{"b":[39.996,15.986,40.11,16.104],"l":[40.048,16.104,40.039,16.081,39.996,16.043,40.012,16.023,40.11,15.986,40.076,16.082,40.048,16.104]},{"b":[36.504,25.383,36.955,25.689],"l":[36.924,25.426,36.748,25.559,36.589,25.62,36.554,25.645,36.534,25.689,36.504,25.645,36.53,25.602,36.722,25.534,36.764,25.501,36.805,25.451,36.875,25.383,36.902,25.383,36.955,25.415,36.924,25.426]},{"b":[36.544,25.699,36.596,25.856],"l":[36.586,25.699,36.596,25.713,36.596,25.735,36.58,25.795,36.583,25.856,36.546,25.812,36.544,25.734,36.586,25.699]},{"b":[101.265,2.97,101.328,3.067],"l":[101.268,2.97,101.319,2.988,101.328,3.048,101.311,3.067,101.274,3.033,101.265,2.996,101.268,2.97]},{"b":[134.634,-6.559,134.729,-6.456],"l":[134.679,-6.456,134.634,-6.477,134.661,-6.559,134.716,-6.549,134.729,-6.506,134.679,-6.456]},{"b":[123.483,-1.711,123.616,-1.503],"l":[123.549,-1.508,123.529,-1.503,123.487,-1.535,123.483,-1.681,123.529,-1.711,123.598,-1.704,123.616,-1.627,123.582,-1.591,123.561,-1.552,123.549,-1.508]},{"b":[126.646,34.296,126.77,34.396],"l":[126.7,34.396,126.652,34.39,126.646,34.351,126.689,34.305,126.77,34.296,126.753,34.344,126.7,34.396]},{"b":[152.559,76.086,152.886,76.195],"l":[152.799,76.195,152.643,76.175,152.559,76.144,152.786,76.086,152.886,76.122,152.864,76.163,152.835,76.185,152.799,76.195]},{"b":[67.026,69.443,67.345,69.575],"l":[67.216,69.575,67.026,69.483,67.047,69.467,67.098,69.447,67.264,69.443,67.345,69.53,67.329,69.572,67.216,69.575]},{"b":[66.395,70.501,66.569,70.773],"l":[66.458,70.699,66.463,70.769,66.44,70.773,66.418,70.757,66.395,70.727,66.408,70.616,66.449,70.561,66.516,70.515,66.569,70.501,66.561,70.542,66.458,70.699]},{"b":[54.98,80.227,55.48,80.325],"l":[55.24,80.325,55.092,80.296,54.98,80.256,55.048,80.228,55.195,80.227,55.48,80.274,55.435,80.302,55.353,80.318,55.24,80.325]},{"b":[18.35,59.02,18.486,59.108],"l":[18.398,59.089,18.377,59.069,18.35,59.023,18.372,59.02,18.416,59.029,18.486,59.105,18.465,59.108,18.398,59.089]},{"b":[18.545,59.437,18.698,59.548],"l":[18.621,59.548,18.572,59.526,18.555,59.486,18.545,59.478,18.57,59.437,18.595,59.47,18.624,59.492,18.698,59.525,18.698,59.535,18.621,59.548]},{"b":[-122.912,47.185,-122.849,47.275],"l":[-122.853,47.205,-122.863,47.185,-122.877,47.186,-122.908,47.226,-122.912,47.254,-122.885,47.275,-122.849,47.216,-122.853,47.205]},{"b":[105.584,-10.564,105.725,-10.431],"l":[105.646,-10.452,105.596,-10.46,105.584,-10.512,105.644,-10.525,105.697,-10.564,105.725,-10.493,105.705,-10.431,105.67,-10.449,105.646,-10.452]},{"b":[-86.63,16.3,-86.256,16.439],"l":[-86.338,16.439,-86.256,16.428,-86.42,16.378,-86.58,16.3,-86.63,16.302,-86.557,16.362,-86.438,16.414,-86.338,16.439]},{"b":[163.737,-74.833,164.208,-74.608],"l":[163.742,-74.712,163.737,-74.734,163.763,-74.803,163.845,-74.833,163.976,-74.833,164.059,-74.753,164.098,-74.732,164.208,-74.608,164.002,-74.629,163.742,-74.712]},{"b":[164.639,-67.569,164.919,-67.259],"l":[164.684,-67.259,164.675,-67.289,164.696,-67.408,164.639,-67.5,164.692,-67.56,164.746,-67.569,164.834,-67.54,164.86,-67.504,164.919,-67.447,164.907,-67.419,164.85,-67.364,164.825,-67.326,164.684,-67.259]},{"b":[166.013,-78.306,167.643,-77.992],"l":[167.594,-78.022,167.498,-77.992,167.422,-78.006,167.364,-78.046,167.138,-78.13,166.864,-78.196,166.76,-78.198,166.567,-78.148,166.111,-78.09,166.013,-78.102,166.013,-78.131,166.051,-78.213,166.122,-78.275,166.285,-78.306,166.626,-78.284,166.936,-78.222,167.377,-78.249,167.517,-78.216,167.643,-78.141,167.64,-78.112,167.594,-78.022]},{"b":[163.09,-66.882,163.302,-66.688],"l":[163.271,-66.768,163.238,-66.709,163.156,-66.688,163.09,-66.701,163.164,-66.819,163.235,-66.868,163.284,-66.882,163.302,-66.821,163.299,-66.798,163.271,-66.768]},{"b":[166.217,-77.851,169.353,-77.162],"l":[168.451,-77.386,167.461,-77.394,167.084,-77.322,167.085,-77.307,167.107,-77.271,166.987,-77.187,166.716,-77.162,166.506,-77.189,166.413,-77.252,166.469,-77.289,166.607,-77.336,166.626,-77.377,166.458,-77.444,166.378,-77.494,166.217,-77.525,166.237,-77.547,166.533,-77.7,166.65,-77.774,166.729,-77.851,167.025,-77.756,167.279,-77.703,167.392,-77.649,167.918,-77.644,168.323,-77.683,168.519,-77.681,168.755,-77.653,169.117,-77.561,169.353,-77.525,169.276,-77.455,168.451,-77.386]},{"b":[169.479,-73.625,169.96,-73.32],"l":[169.887,-73.459,169.783,-73.324,169.74,-73.32,169.672,-73.346,169.645,-73.379,169.659,-73.418,169.479,-73.539,169.522,-73.562,169.709,-73.625,169.844,-73.605,169.859,-73.568,169.96,-73.514,169.887,-73.459]},{"b":[162.591,-75.696,162.968,-75.557],"l":[162.72,-75.597,162.591,-75.669,162.662,-75.692,162.788,-75.696,162.968,-75.567,162.917,-75.557,162.842,-75.566,162.72,-75.597]},{"b":[100.27,-65.701,101.259,-65.378],"l":[100.409,-65.466,100.324,-65.521,100.27,-65.603,100.293,-65.651,100.351,-65.673,100.512,-65.675,100.547,-65.701,100.981,-65.678,101.238,-65.565,101.259,-65.528,101.221,-65.472,101.079,-65.403,100.883,-65.378,100.607,-65.396,100.545,-65.409,100.409,-65.466]},{"b":[85.314,-66.98,85.938,-66.723],"l":[85.34,-66.723,85.314,-66.776,85.359,-66.854,85.617,-66.951,85.622,-66.965,85.65,-66.98,85.822,-66.953,85.938,-66.894,85.806,-66.775,85.553,-66.729,85.34,-66.723]},{"b":[92.248,-65.822,92.67,-65.702],"l":[92.301,-65.707,92.248,-65.74,92.263,-65.76,92.333,-65.807,92.471,-65.822,92.601,-65.808,92.67,-65.775,92.665,-65.76,92.634,-65.731,92.496,-65.702,92.301,-65.707]},{"b":[86.232,-66.792,86.652,-66.675],"l":[86.383,-66.675,86.278,-66.697,86.232,-66.733,86.337,-66.788,86.427,-66.792,86.542,-66.767,86.652,-66.718,86.557,-66.706,86.521,-66.687,86.383,-66.675]},{"b":[85.069,-66.643,85.329,-66.519],"l":[85.165,-66.522,85.121,-66.519,85.069,-66.584,85.08,-66.604,85.136,-66.637,85.222,-66.643,85.329,-66.612,85.194,-66.556,85.165,-66.522]},{"b":[69.693,-72.047,69.918,-71.894],"l":[69.796,-71.894,69.737,-71.922,69.693,-71.968,69.744,-72.044,69.792,-72.047,69.918,-71.918,69.895,-71.908,69.796,-71.894]},{"b":[68.409,-72.3,68.84,-72.089],"l":[68.667,-72.103,68.566,-72.19,68.436,-72.26,68.409,-72.3,68.462,-72.3,68.67,-72.276,68.817,-72.229,68.84,-72.165,68.729,-72.089,68.667,-72.103]},{"b":[96.307,-66.225,97.019,-66.036],"l":[96.727,-66.061,96.613,-66.036,96.5,-66.046,96.399,-66.08,96.307,-66.186,96.395,-66.225,96.934,-66.201,97.016,-66.164,97.019,-66.139,97.006,-66.097,96.932,-66.058,96.727,-66.061]},{"b":[71.637,-70.633,72.097,-70.264],"l":[72.073,-70.525,72.056,-70.501,72,-70.457,71.88,-70.406,71.851,-70.368,71.838,-70.312,71.797,-70.264,71.705,-70.284,71.647,-70.336,71.637,-70.444,71.659,-70.497,71.726,-70.549,71.841,-70.622,71.929,-70.633,72.002,-70.633,72.078,-70.609,72.097,-70.575,72.073,-70.525]},{"b":[162.297,-66.525,162.611,-66.251],"l":[162.311,-66.251,162.302,-66.265,162.297,-66.304,162.326,-66.347,162.303,-66.4,162.511,-66.52,162.557,-66.525,162.611,-66.477,162.563,-66.433,162.311,-66.251]},{"b":[100.076,-66.229,100.291,-66.112],"l":[100.174,-66.131,100.076,-66.188,100.082,-66.203,100.133,-66.229,100.265,-66.217,100.282,-66.18,100.291,-66.112,100.174,-66.131]},{"b":[98.596,-66.482,98.95,-66.369],"l":[98.655,-66.453,98.752,-66.482,98.846,-66.47,98.95,-66.421,98.749,-66.369,98.596,-66.383,98.605,-66.4,98.655,-66.453]},{"b":[102.76,-65.469,103.397,-65.13],"l":[103.186,-65.331,103.182,-65.308,103.191,-65.237,103.137,-65.191,102.893,-65.13,102.796,-65.136,102.76,-65.168,102.789,-65.236,103.054,-65.285,103.113,-65.312,103.124,-65.338,103.138,-65.435,103.176,-65.455,103.337,-65.469,103.397,-65.445,103.379,-65.426,103.261,-65.377,103.186,-65.331]},{"b":[-148.001,-76.663,-147.579,-76.558],"l":[-147.579,-76.663,-147.588,-76.65,-147.649,-76.611,-147.77,-76.577,-147.9,-76.558,-148.001,-76.577,-147.954,-76.597,-147.73,-76.653,-147.579,-76.663]},{"b":[-146.151,-75.716,-145.238,-75.505],"l":[-145.761,-75.514,-145.896,-75.505,-146.076,-75.533,-146.151,-75.574,-146.043,-75.612,-145.541,-75.693,-145.348,-75.716,-145.238,-75.711,-145.252,-75.683,-145.315,-75.641,-145.417,-75.588,-145.761,-75.514]},{"b":[-147.116,-77.006,-146.164,-76.837],"l":[-146.867,-76.837,-147.087,-76.837,-147.116,-76.866,-147.101,-76.887,-147.044,-76.93,-147.079,-76.993,-146.981,-77.006,-146.607,-76.961,-146.164,-76.949,-146.244,-76.883,-146.867,-76.837]},{"b":[-132.857,-74.498,-132.362,-74.387],"l":[-132.832,-74.422,-132.857,-74.462,-132.546,-74.498,-132.391,-74.442,-132.362,-74.41,-132.552,-74.387,-132.832,-74.422]},{"b":[-149.303,-77.019,-148.44,-76.9],"l":[-149.238,-76.9,-149.303,-76.916,-149.245,-76.993,-149.015,-77.019,-148.596,-77.007,-148.474,-76.998,-148.44,-76.977,-148.509,-76.955,-148.704,-76.936,-149.238,-76.9]},{"b":[-149.469,-76.845,-148.321,-76.717],"l":[-148.371,-76.795,-148.321,-76.772,-148.384,-76.744,-148.663,-76.721,-148.928,-76.73,-149.333,-76.717,-149.469,-76.757,-149.238,-76.818,-148.984,-76.845,-148.815,-76.841,-148.67,-76.802,-148.371,-76.795]},{"b":[-132.163,-74.605,-130.957,-74.324],"l":[-131.234,-74.414,-131.56,-74.367,-131.594,-74.33,-131.763,-74.324,-131.938,-74.349,-132.163,-74.426,-132.049,-74.464,-132.025,-74.489,-131.952,-74.514,-131.841,-74.543,-131.598,-74.554,-131.179,-74.605,-131.067,-74.584,-130.967,-74.515,-130.957,-74.456,-130.981,-74.414,-131.234,-74.414]},{"b":[-119.905,-74.122,-118.877,-73.775],"l":[-119.549,-74.11,-119.449,-74.076,-119.059,-73.998,-118.99,-73.967,-118.877,-73.878,-118.91,-73.834,-118.959,-73.809,-119.216,-73.778,-119.516,-73.775,-119.669,-73.809,-119.803,-73.815,-119.662,-73.989,-119.694,-74.006,-119.797,-74.029,-119.905,-74.082,-119.887,-74.097,-119.821,-74.12,-119.758,-74.122,-119.549,-74.11]},{"b":[-117.398,-74.193,-116.155,-73.866],"l":[-116.381,-73.866,-117.376,-74.083,-117.398,-74.122,-117.363,-74.161,-117.23,-74.193,-116.739,-74.165,-116.571,-74.126,-116.514,-74.096,-116.534,-74.083,-116.609,-74.069,-116.585,-74.056,-116.451,-74.018,-116.155,-73.91,-116.203,-73.896,-116.381,-73.866]},{"b":[-128.133,-74.641,-127.145,-74.312],"l":[-127.486,-74.405,-127.853,-74.332,-128.043,-74.312,-128.133,-74.327,-128.096,-74.466,-128.07,-74.478,-128,-74.489,-127.915,-74.543,-127.817,-74.575,-127.518,-74.641,-127.366,-74.623,-127.232,-74.578,-127.145,-74.48,-127.23,-74.425,-127.486,-74.405]},{"b":[-149.662,-77.387,-148.929,-77.275],"l":[-149.218,-77.336,-149.249,-77.315,-149.375,-77.28,-149.519,-77.275,-149.662,-77.301,-149.439,-77.371,-148.929,-77.387,-149.218,-77.336]},{"b":[-163.796,-83.142,-161.635,-82.843],"l":[-162.798,-82.865,-163.796,-82.843,-163.735,-82.857,-163.704,-82.879,-163.634,-82.902,-163.602,-82.927,-163.602,-82.969,-163.552,-82.988,-163.348,-83.022,-163.242,-83.06,-163.047,-83.097,-162.305,-83.142,-161.994,-83.119,-161.828,-83.043,-161.635,-83.027,-162.34,-82.923,-162.411,-82.899,-162.798,-82.865]},{"b":[-150.874,-76.789,-150.085,-76.691],"l":[-150.085,-76.735,-150.104,-76.719,-150.177,-76.691,-150.838,-76.714,-150.874,-76.737,-150.83,-76.762,-150.655,-76.789,-150.233,-76.776,-150.085,-76.735]},{"b":[-147.421,-76.261,-146.69,-76.063],"l":[-147.151,-76.197,-146.894,-76.261,-146.69,-76.246,-146.949,-76.098,-147.361,-76.063,-147.418,-76.073,-147.421,-76.09,-147.408,-76.105,-147.345,-76.147,-147.151,-76.197]},{"b":[-147.355,-76.714,-146.79,-76.532],"l":[-146.947,-76.555,-147.135,-76.532,-147.279,-76.553,-147.355,-76.619,-147.221,-76.671,-146.908,-76.714,-146.79,-76.633,-146.878,-76.563,-146.947,-76.555]},{"b":[-163.951,-81.598,-160.467,-81.281],"l":[-160.467,-81.589,-160.485,-81.567,-160.617,-81.522,-160.938,-81.463,-161.559,-81.397,-162.456,-81.313,-163.201,-81.281,-163.869,-81.324,-163.93,-81.352,-163.951,-81.391,-163.939,-81.404,-163.89,-81.424,-163.766,-81.445,-163.253,-81.482,-160.571,-81.598,-160.467,-81.589]},{"b":[-151.512,-77.374,-150.356,-77.22],"l":[-151.022,-77.22,-151.218,-77.226,-151.512,-77.273,-151.344,-77.296,-150.475,-77.374,-150.397,-77.369,-150.356,-77.349,-150.499,-77.335,-151.022,-77.22]},{"b":[-155.751,-80.036,-153.93,-79.766],"l":[-153.93,-80.033,-154.025,-79.988,-154.535,-79.936,-155.162,-79.851,-155.674,-79.766,-155.751,-79.83,-155.525,-79.846,-155.045,-79.9,-154.942,-79.966,-154.529,-80,-154.349,-80.026,-154.114,-80.036,-153.93,-80.033]},{"b":[-150.789,-77.137,-149.231,-76.875],"l":[-149.293,-77.137,-149.231,-77.121,-149.288,-77.093,-149.416,-77.079,-149.442,-77.049,-149.506,-77.002,-149.743,-76.927,-149.79,-76.889,-149.871,-76.875,-150.393,-76.899,-150.476,-76.926,-150.68,-76.948,-150.789,-76.982,-150.736,-77.004,-150.462,-77.076,-149.856,-77.099,-149.817,-77.114,-149.729,-77.129,-149.293,-77.137]},{"b":[-158.989,-82.112,-157.835,-81.779],"l":[-158.261,-81.947,-158.347,-81.9,-158.914,-81.78,-158.989,-81.779,-158.926,-81.819,-158.773,-81.875,-158.545,-81.949,-158.154,-82.058,-158.076,-82.112,-157.987,-82.105,-157.835,-82.031,-158.261,-81.947]},{"b":[-6.438,-70.612,-5.894,-70.405],"l":[-6.068,-70.405,-6.244,-70.446,-6.438,-70.453,-6.266,-70.55,-6.181,-70.586,-6.156,-70.612,-5.894,-70.552,-5.95,-70.432,-5.972,-70.421,-6.068,-70.405]},{"b":[2.585,-70.624,3.26,-70.382],"l":[3.072,-70.382,2.631,-70.5,2.585,-70.535,2.623,-70.593,2.698,-70.624,3.037,-70.597,3.171,-70.554,3.221,-70.519,3.26,-70.449,3.231,-70.403,3.193,-70.393,3.072,-70.382]},{"b":[-3.404,-71.237,-2.955,-71.052],"l":[-3.399,-71.062,-3.392,-71.081,-3.404,-71.12,-3.386,-71.143,-3.309,-71.201,-3.201,-71.23,-3.061,-71.237,-2.955,-71.214,-3.191,-71.095,-3.213,-71.076,-3.263,-71.052,-3.399,-71.062]},{"b":[0.95,-70.381,1.461,-70.023],"l":[1.315,-70.023,1.027,-70.05,0.95,-70.094,0.953,-70.169,0.99,-70.224,1.105,-70.304,1.156,-70.378,1.212,-70.381,1.299,-70.255,1.461,-70.136,1.412,-70.041,1.315,-70.023]},{"b":[-3.575,-71.167,-2.092,-70.674],"l":[-2.533,-70.768,-2.75,-70.694,-3.04,-70.674,-3.537,-70.683,-3.575,-70.703,-3.489,-70.736,-3.007,-70.851,-2.975,-70.883,-2.963,-70.94,-2.801,-70.982,-2.805,-71.015,-2.822,-71.057,-2.825,-71.113,-2.783,-71.167,-2.607,-71.141,-2.369,-71.044,-2.293,-70.998,-2.213,-70.902,-2.119,-70.855,-2.092,-70.821,-2.256,-70.796,-2.423,-70.8,-2.533,-70.768]},{"b":[4.07,-70.503,4.618,-70.241],"l":[4.13,-70.417,4.18,-70.451,4.365,-70.503,4.526,-70.479,4.59,-70.433,4.618,-70.369,4.586,-70.294,4.495,-70.251,4.256,-70.241,4.112,-70.267,4.07,-70.29,4.076,-70.325,4.13,-70.417]},{"b":[48.294,-66.807,48.786,-66.701],"l":[48.638,-66.701,48.358,-66.704,48.301,-66.724,48.294,-66.75,48.296,-66.774,48.304,-66.798,48.378,-66.807,48.546,-66.784,48.775,-66.778,48.786,-66.768,48.782,-66.731,48.751,-66.72,48.638,-66.701]},{"b":[15.563,-70.072,16.625,-69.705],"l":[15.91,-69.728,15.699,-69.773,15.597,-69.828,15.563,-69.863,15.571,-69.885,15.614,-69.939,15.663,-69.955,15.845,-69.982,16.159,-70.072,16.223,-70.008,16.315,-69.844,16.625,-69.75,16.573,-69.723,16.247,-69.705,15.91,-69.728]},{"b":[25.954,-70.448,26.875,-70.061],"l":[26.686,-70.114,26.605,-70.078,26.426,-70.061,26.301,-70.072,25.983,-70.2,25.954,-70.261,25.964,-70.295,26.005,-70.373,26.358,-70.434,26.47,-70.448,26.609,-70.412,26.793,-70.419,26.857,-70.381,26.875,-70.33,26.737,-70.186,26.686,-70.114]},{"b":[-32,-80.011,-29.614,-79.634],"l":[-30.985,-79.818,-31.119,-79.798,-31.605,-79.645,-31.68,-79.634,-32,-79.732,-31.824,-79.85,-31.594,-79.888,-30.844,-79.938,-30.422,-80.011,-30.029,-79.936,-29.8,-79.926,-29.721,-79.93,-29.614,-79.91,-29.871,-79.823,-30.66,-79.733,-30.78,-79.647,-30.861,-79.726,-30.841,-79.771,-30.985,-79.818]},{"b":[-36.601,-79.357,-33.934,-79.09],"l":[-33.995,-79.279,-34.392,-79.223,-35.535,-79.09,-35.597,-79.092,-35.79,-79.149,-36.048,-79.181,-36.238,-79.196,-36.566,-79.209,-36.601,-79.283,-36.481,-79.294,-34.05,-79.357,-33.934,-79.32,-33.947,-79.305,-33.995,-79.279]},{"b":[-3.497,-70.535,-2.683,-70.28],"l":[-2.738,-70.507,-2.683,-70.462,-2.684,-70.376,-2.714,-70.32,-2.805,-70.288,-2.95,-70.28,-3.173,-70.307,-3.287,-70.344,-3.497,-70.488,-3.49,-70.508,-3.442,-70.535,-3.28,-70.534,-2.738,-70.507]},{"b":[-22.035,-74.493,-20.411,-73.619],"l":[-20.6,-74.197,-20.641,-74.151,-20.654,-74.105,-20.607,-73.887,-20.521,-73.798,-20.521,-73.712,-20.58,-73.619,-20.69,-73.625,-20.867,-73.677,-20.979,-73.79,-21.025,-73.88,-21.126,-73.94,-21.288,-73.989,-21.93,-74.057,-22.035,-74.107,-21.61,-74.092,-21.167,-74.133,-21.051,-74.176,-20.977,-74.225,-20.846,-74.438,-20.818,-74.455,-20.737,-74.481,-20.489,-74.493,-20.417,-74.443,-20.411,-74.408,-20.423,-74.317,-20.6,-74.197]},{"b":[-16.517,-72.703,-16.104,-72.459],"l":[-16.303,-72.478,-16.356,-72.459,-16.455,-72.474,-16.517,-72.531,-16.51,-72.582,-16.453,-72.652,-16.318,-72.702,-16.175,-72.703,-16.104,-72.679,-16.173,-72.6,-16.303,-72.478]},{"b":[-12.963,-72.196,-12.509,-72.001],"l":[-12.789,-72.007,-12.875,-72.001,-12.915,-72.015,-12.963,-72.064,-12.944,-72.099,-12.888,-72.137,-12.72,-72.188,-12.588,-72.196,-12.509,-72.173,-12.535,-72.14,-12.637,-72.071,-12.789,-72.007]},{"b":[-32.583,-79.683,-31.933,-79.53],"l":[-31.957,-79.604,-31.933,-79.568,-32.15,-79.53,-32.377,-79.535,-32.501,-79.592,-32.583,-79.658,-32.515,-79.683,-32.343,-79.674,-32.001,-79.607,-31.957,-79.604]},{"b":[-45.956,-60.733,-45.173,-60.521],"l":[-45.78,-60.586,-45.709,-60.645,-45.398,-60.65,-45.174,-60.733,-45.173,-60.699,-45.186,-60.672,-45.211,-60.648,-45.228,-60.64,-45.357,-60.624,-45.386,-60.583,-45.5,-60.546,-45.718,-60.521,-45.834,-60.543,-45.935,-60.527,-45.956,-60.568,-45.955,-60.597,-45.937,-60.62,-45.78,-60.586]},{"b":[-54.192,-61.308,-54.024,-61.135],"l":[-54.122,-61.202,-54.192,-61.247,-54.184,-61.27,-54.115,-61.308,-54.071,-61.299,-54.041,-61.255,-54.024,-61.135,-54.05,-61.142,-54.122,-61.202]},{"b":[-68.549,-79.501,-67.069,-79.214],"l":[-68.422,-79.333,-68.549,-79.437,-68.408,-79.464,-68.161,-79.479,-67.434,-79.501,-67.262,-79.453,-67.305,-79.394,-67.24,-79.328,-67.173,-79.312,-67.069,-79.268,-67.475,-79.223,-67.714,-79.214,-68.033,-79.227,-68.233,-79.285,-68.324,-79.298,-68.422,-79.333]},{"b":[-55.44,-61.249,-54.671,-61.073],"l":[-55.387,-61.073,-55.44,-61.106,-55.369,-61.146,-55.347,-61.212,-55.297,-61.249,-55.165,-61.22,-55.058,-61.169,-54.71,-61.14,-54.671,-61.117,-55.387,-61.073]},{"b":[-56.505,-63.437,-55.075,-63.128],"l":[-55.466,-63.2,-55.528,-63.174,-55.529,-63.157,-55.59,-63.128,-56.042,-63.157,-56.385,-63.234,-56.466,-63.283,-56.461,-63.302,-56.475,-63.318,-56.505,-63.334,-56.499,-63.358,-56.463,-63.418,-56.379,-63.437,-56.083,-63.383,-56.009,-63.342,-55.83,-63.298,-55.75,-63.297,-55.594,-63.336,-55.157,-63.353,-55.075,-63.324,-55.106,-63.249,-55.156,-63.205,-55.216,-63.199,-55.466,-63.2]},{"b":[-56.235,-63.58,-55.719,-63.407],"l":[-55.762,-63.422,-55.858,-63.407,-56.21,-63.437,-56.235,-63.469,-56.178,-63.513,-55.957,-63.58,-55.873,-63.536,-55.719,-63.492,-55.762,-63.422]},{"b":[-57.448,-64.571,-56.895,-64.333],"l":[-57.446,-64.46,-57.448,-64.488,-57.433,-64.54,-57.326,-64.571,-57.24,-64.567,-56.991,-64.468,-56.945,-64.427,-56.952,-64.382,-56.895,-64.333,-57.023,-64.352,-57.315,-64.435,-57.366,-64.439,-57.446,-64.46]},{"b":[-59.202,-62.348,-58.838,-62.239],"l":[-59.064,-62.239,-59.202,-62.283,-59.175,-62.302,-59.059,-62.348,-58.838,-62.303,-58.879,-62.268,-58.962,-62.264,-58.991,-62.249,-59.064,-62.239]},{"b":[-59.004,-62.248,-57.637,-61.912],"l":[-58.562,-62.244,-58.507,-62.226,-58.467,-62.137,-58.341,-62.119,-58.183,-62.17,-58.133,-62.146,-58.172,-62.118,-58.148,-62.063,-57.963,-62.078,-57.807,-62.012,-57.64,-62.02,-57.637,-61.998,-57.677,-61.942,-57.738,-61.921,-57.849,-61.94,-57.978,-61.912,-58.265,-61.953,-58.399,-61.938,-58.684,-62.008,-58.71,-62.045,-58.955,-62.164,-59.004,-62.21,-58.819,-62.171,-58.755,-62.206,-58.746,-62.218,-58.644,-62.225,-58.594,-62.248,-58.562,-62.244]},{"b":[-62.044,-64.08,-61.798,-63.967],"l":[-62.021,-64.027,-62.044,-64.08,-61.952,-64.077,-61.911,-64.054,-61.886,-64.027,-61.798,-63.967,-61.936,-63.99,-62.021,-64.027]},{"b":[-62.568,-69.729,-61.784,-69.146],"l":[-61.978,-69.3,-62.118,-69.215,-62.239,-69.176,-62.442,-69.146,-62.516,-69.155,-62.568,-69.18,-62.496,-69.288,-62.216,-69.495,-62.172,-69.637,-62.085,-69.729,-61.998,-69.722,-61.97,-69.691,-61.908,-69.588,-61.911,-69.533,-61.807,-69.515,-61.784,-69.442,-61.816,-69.376,-61.978,-69.3]},{"b":[-63.558,-64.907,-63.177,-64.73],"l":[-63.177,-64.739,-63.219,-64.73,-63.367,-64.792,-63.459,-64.796,-63.558,-64.906,-63.474,-64.907,-63.316,-64.861,-63.257,-64.791,-63.177,-64.739]},{"b":[-62.782,-64.514,-62.058,-64.012],"l":[-62.094,-64.235,-62.058,-64.138,-62.269,-64.09,-62.268,-64.04,-62.329,-64.013,-62.451,-64.012,-62.545,-64.046,-62.586,-64.076,-62.611,-64.116,-62.59,-64.14,-62.48,-64.211,-62.504,-64.253,-62.643,-64.392,-62.721,-64.445,-62.747,-64.472,-62.782,-64.479,-62.727,-64.496,-62.58,-64.514,-62.508,-64.454,-62.455,-64.472,-62.396,-64.465,-62.326,-64.424,-62.304,-64.401,-62.186,-64.369,-62.174,-64.296,-62.094,-64.235]},{"b":[-62.655,-63.074,-62.317,-62.874],"l":[-62.411,-62.972,-62.344,-62.918,-62.317,-62.874,-62.527,-62.924,-62.639,-63.032,-62.655,-63.074,-62.615,-63.069,-62.411,-62.972]},{"b":[-59.661,-62.451,-59.353,-62.352],"l":[-59.525,-62.451,-59.389,-62.444,-59.353,-62.413,-59.396,-62.367,-59.479,-62.352,-59.661,-62.354,-59.619,-62.395,-59.525,-62.451]},{"b":[-56.614,-63.173,-56.051,-62.982],"l":[-56.354,-63.169,-56.259,-63.173,-56.06,-63.079,-56.051,-63.055,-56.058,-63.019,-56.062,-63.013,-56.14,-63.005,-56.489,-62.982,-56.614,-63.045,-56.601,-63.062,-56.546,-63.098,-56.354,-63.169]},{"b":[-61.152,-62.745,-59.85,-62.475],"l":[-60.796,-62.662,-60.697,-62.621,-60.62,-62.633,-60.378,-62.617,-60.354,-62.679,-60.322,-62.708,-60.221,-62.745,-59.85,-62.615,-60.003,-62.618,-60.14,-62.549,-60.576,-62.573,-60.625,-62.56,-60.732,-62.491,-60.799,-62.475,-60.838,-62.534,-60.975,-62.592,-61.152,-62.589,-61.15,-62.634,-61.063,-62.679,-60.995,-62.679,-60.796,-62.662]},{"b":[-57.683,-63.879,-57.104,-63.791],"l":[-57.344,-63.879,-57.248,-63.868,-57.218,-63.876,-57.104,-63.841,-57.161,-63.816,-57.36,-63.825,-57.374,-63.807,-57.439,-63.791,-57.683,-63.813,-57.616,-63.854,-57.344,-63.879]},{"b":[-61.404,-69.978,-61.108,-69.856],"l":[-61.327,-69.856,-61.386,-69.893,-61.404,-69.933,-61.378,-69.95,-61.309,-69.978,-61.158,-69.976,-61.108,-69.955,-61.152,-69.883,-61.327,-69.856]},{"b":[-61.015,-68.795,-60.656,-68.681],"l":[-60.693,-68.795,-60.656,-68.768,-60.705,-68.722,-60.814,-68.688,-60.947,-68.681,-61.015,-68.71,-60.894,-68.759,-60.82,-68.778,-60.693,-68.795]},{"b":[-60.976,-70.711,-60.449,-70.509],"l":[-60.554,-70.509,-60.884,-70.518,-60.942,-70.533,-60.976,-70.599,-60.958,-70.629,-60.896,-70.69,-60.826,-70.711,-60.741,-70.711,-60.488,-70.647,-60.449,-70.603,-60.452,-70.544,-60.554,-70.509]},{"b":[-60.946,-71.059,-60.516,-70.914],"l":[-60.516,-71,-60.533,-70.962,-60.613,-70.92,-60.783,-70.914,-60.889,-70.934,-60.946,-70.967,-60.906,-71.007,-60.79,-71.041,-60.652,-71.059,-60.552,-71.053,-60.536,-71.041,-60.516,-71]},{"b":[-60.74,-63.018,-60.505,-62.895],"l":[-60.637,-62.895,-60.706,-62.906,-60.74,-62.949,-60.693,-62.996,-60.622,-63.018,-60.564,-63.009,-60.618,-62.987,-60.62,-62.969,-60.555,-62.978,-60.505,-62.967,-60.637,-62.895]},{"b":[-60.972,-63.902,-60.562,-63.669],"l":[-60.562,-63.696,-60.715,-63.669,-60.797,-63.717,-60.81,-63.837,-60.972,-63.849,-60.852,-63.891,-60.778,-63.902,-60.653,-63.867,-60.655,-63.85,-60.689,-63.808,-60.656,-63.759,-60.562,-63.696]},{"b":[-94.113,-72.92,-93.756,-72.82],"l":[-94.004,-72.82,-94.047,-72.823,-94.113,-72.86,-94.078,-72.884,-93.966,-72.92,-93.796,-72.92,-93.756,-72.908,-93.8,-72.882,-94.004,-72.82]},{"b":[-91.67,-73.207,-90.751,-72.547],"l":[-90.78,-72.732,-90.763,-72.681,-90.807,-72.611,-90.947,-72.556,-91.304,-72.547,-91.612,-72.594,-91.67,-72.624,-91.551,-72.754,-91.382,-72.868,-91.357,-72.909,-91.45,-72.968,-91.511,-73.196,-91.344,-73.207,-91.161,-73.182,-90.998,-73.137,-90.893,-73.084,-90.776,-72.993,-90.751,-72.917,-90.776,-72.854,-90.895,-72.824,-90.78,-72.732]},{"b":[-105.132,-73.212,-104.538,-72.941],"l":[-104.972,-72.941,-105.085,-72.966,-105.132,-72.992,-105.123,-73.026,-105.053,-73.126,-104.881,-73.201,-104.66,-73.212,-104.538,-73.166,-104.972,-72.941]},{"b":[-95.273,-72.67,-94.426,-72.468],"l":[-94.566,-72.468,-94.753,-72.517,-95.216,-72.599,-95.273,-72.647,-95.219,-72.67,-95.027,-72.665,-94.426,-72.613,-94.434,-72.589,-94.514,-72.492,-94.538,-72.476,-94.566,-72.468]},{"b":[-71.795,-70.442,-71.34,-70.265],"l":[-71.551,-70.439,-71.438,-70.392,-71.34,-70.317,-71.355,-70.298,-71.432,-70.267,-71.648,-70.295,-71.695,-70.265,-71.795,-70.288,-71.782,-70.319,-71.685,-70.442,-71.551,-70.439]},{"b":[-72.957,-69.74,-71.985,-69.413],"l":[-72.202,-69.74,-71.985,-69.698,-72.331,-69.492,-72.464,-69.452,-72.726,-69.413,-72.857,-69.433,-72.937,-69.469,-72.957,-69.529,-72.777,-69.645,-72.345,-69.707,-72.202,-69.74]},{"b":[-74.147,-73.376,-73.542,-73.1],"l":[-74.049,-73.22,-74.084,-73.249,-74.134,-73.277,-74.147,-73.315,-74.038,-73.366,-73.975,-73.376,-73.878,-73.357,-73.721,-73.296,-73.682,-73.225,-73.542,-73.124,-73.674,-73.1,-73.832,-73.113,-74.049,-73.22]},{"b":[-66.181,-65.881,-65.637,-65.527],"l":[-66.064,-65.881,-65.845,-65.842,-65.836,-65.814,-65.841,-65.738,-65.814,-65.687,-65.784,-65.674,-65.67,-65.653,-65.668,-65.626,-65.637,-65.548,-65.834,-65.527,-65.968,-65.571,-66,-65.633,-66.067,-65.666,-66.05,-65.745,-66.153,-65.774,-66.181,-65.826,-66.175,-65.867,-66.064,-65.881]},{"b":[-66.868,-66.313,-66.575,-66.067],"l":[-66.867,-66.275,-66.868,-66.294,-66.85,-66.305,-66.819,-66.313,-66.595,-66.201,-66.593,-66.179,-66.623,-66.134,-66.575,-66.082,-66.631,-66.067,-66.779,-66.111,-66.792,-66.234,-66.867,-66.275]},{"b":[-67.743,-67.785,-67.149,-67.591],"l":[-67.149,-67.65,-67.175,-67.625,-67.247,-67.599,-67.418,-67.591,-67.557,-67.604,-67.743,-67.661,-67.731,-67.679,-67.69,-67.688,-67.693,-67.763,-67.545,-67.785,-67.349,-67.766,-67.3,-67.737,-67.28,-67.712,-67.149,-67.65]},{"b":[-67.593,-66.902,-67.257,-66.737],"l":[-67.257,-66.841,-67.269,-66.815,-67.332,-66.754,-67.426,-66.737,-67.511,-66.756,-67.5,-66.804,-67.593,-66.876,-67.521,-66.897,-67.409,-66.902,-67.362,-66.895,-67.257,-66.841]},{"b":[-180,-16.963,-179.822,-16.688],"l":[-180,-16.963,-179.975,-16.925,-179.868,-16.85,-179.822,-16.765,-179.861,-16.688,-179.894,-16.7,-180,-16.786]},{"b":[-180,-16.54,-179.901,-16.432],"l":[-180,-16.54,-179.929,-16.503,-179.927,-16.479,-179.901,-16.432,-179.944,-16.441,-180,-16.489]},{"b":[179.897,-17,180,-16.786],"l":[179.999,-16.963,179.926,-17,179.897,-16.964,179.931,-16.876,180,-16.786]},{"b":[-78.965,8.232,-78.839,8.46],"l":[-78.839,8.348,-78.853,8.302,-78.898,8.274,-78.918,8.232,-78.965,8.326,-78.957,8.351,-78.961,8.436,-78.916,8.458,-78.883,8.46,-78.856,8.448,-78.839,8.348]},{"b":[154.611,49.268,154.9,49.647],"l":[154.613,49.381,154.611,49.294,154.715,49.268,154.81,49.312,154.83,49.348,154.802,49.468,154.883,49.566,154.9,49.63,154.825,49.647,154.613,49.381]},{"b":[58.61,81.292,59.375,81.398],"l":[58.61,81.337,58.634,81.36,58.881,81.392,59.075,81.398,59.281,81.366,59.375,81.325,59.313,81.305,59.097,81.292,58.719,81.314,58.61,81.337]},{"b":[53.812,80.421,54.437,80.605],"l":[53.902,80.542,53.859,80.563,53.877,80.605,54.177,80.574,54.205,80.562,54.407,80.54,54.437,80.499,54.415,80.473,54.276,80.421,53.812,80.476,53.85,80.504,53.9,80.515,53.902,80.542]},{"b":[60.058,80.93,61.567,81.104],"l":[60.587,81.088,61.457,81.104,61.567,81.05,61.472,81.011,61.141,80.95,60.827,80.93,60.321,80.956,60.058,80.985,60.078,80.999,60.148,81.017,60.587,81.088]},{"b":[52.213,80.173,53.852,80.413],"l":[52.344,80.213,52.213,80.264,52.27,80.276,52.577,80.297,52.681,80.319,52.716,80.348,52.854,80.402,53.186,80.413,53.329,80.402,53.346,80.366,53.486,80.323,53.852,80.268,53.778,80.228,53.653,80.223,53.521,80.185,52.856,80.173,52.636,80.179,52.607,80.191,52.55,80.202,52.344,80.213]},{"b":[55.724,80.071,57.123,80.366],"l":[55.942,80.163,56.012,80.204,55.99,80.32,56.024,80.341,56.655,80.33,56.707,80.363,56.945,80.366,57.079,80.351,57.123,80.317,57.119,80.194,57.073,80.139,57.08,80.095,56.987,80.071,56.201,80.076,55.812,80.087,55.724,80.105,55.942,80.163]},{"b":[58.919,79.923,59.911,80.119],"l":[58.946,80.042,59.001,80.054,59.545,80.119,59.802,80.083,59.911,79.994,59.689,79.956,59.331,79.923,59.203,79.933,59.169,79.948,59.1,79.964,58.919,79.985,58.946,80.042]},{"b":[-66.637,-55.273,-66.436,-55.166],"l":[-66.542,-55.169,-66.6,-55.194,-66.625,-55.213,-66.637,-55.234,-66.63,-55.254,-66.611,-55.27,-66.552,-55.273,-66.472,-55.229,-66.436,-55.19,-66.523,-55.166,-66.542,-55.169]},{"b":[-75.142,-44.907,-75.032,-44.795],"l":[-75.124,-44.87,-75.099,-44.902,-75.067,-44.907,-75.042,-44.89,-75.032,-44.871,-75.048,-44.824,-75.079,-44.795,-75.107,-44.795,-75.142,-44.816,-75.124,-44.87]},{"b":[-75.651,-48.765,-75.156,-48.02],"l":[-75.225,-48.671,-75.434,-48.721,-75.51,-48.763,-75.623,-48.765,-75.651,-48.586,-75.518,-48.329,-75.509,-48.231,-75.554,-48.157,-75.571,-48.096,-75.561,-48.071,-75.391,-48.02,-75.338,-48.074,-75.275,-48.218,-75.156,-48.425,-75.158,-48.623,-75.225,-48.671]},{"b":[-75.477,-50.772,-75.115,-50.481],"l":[-75.304,-50.484,-75.428,-50.481,-75.42,-50.53,-75.443,-50.596,-75.477,-50.654,-75.453,-50.683,-75.439,-50.741,-75.411,-50.764,-75.33,-50.772,-75.302,-50.68,-75.292,-50.597,-75.203,-50.581,-75.16,-50.554,-75.115,-50.51,-75.156,-50.497,-75.304,-50.484]},{"b":[-74.712,-53.358,-73.135,-52.734],"l":[-73.31,-53.248,-73.451,-53.144,-73.505,-53.14,-73.549,-53.126,-73.654,-53.07,-73.782,-53.056,-73.879,-53.012,-74.066,-52.965,-74.275,-52.946,-74.33,-52.929,-74.369,-52.931,-74.386,-52.922,-74.422,-52.86,-74.475,-52.836,-74.572,-52.771,-74.67,-52.734,-74.712,-52.749,-74.712,-52.768,-74.62,-52.835,-74.558,-52.922,-74.414,-52.995,-74.27,-53.082,-74.236,-53.076,-74.139,-53.091,-73.994,-53.076,-73.867,-53.097,-73.794,-53.121,-73.617,-53.23,-73.596,-53.253,-73.583,-53.3,-73.567,-53.307,-73.501,-53.318,-73.409,-53.321,-73.226,-53.358,-73.135,-53.354,-73.143,-53.341,-73.302,-53.259,-73.31,-53.248]},{"b":[-69.988,-55.651,-68.046,-54.882],"l":[-68.305,-55.357,-68.327,-55.333,-68.323,-55.308,-68.283,-55.255,-68.33,-55.219,-68.382,-55.192,-68.586,-55.178,-68.613,-55.156,-68.598,-55.128,-68.4,-55.042,-68.458,-54.96,-68.654,-54.958,-68.901,-55.018,-69.703,-54.919,-69.884,-54.882,-69.921,-55.061,-69.947,-55.111,-69.988,-55.131,-69.98,-55.147,-69.887,-55.174,-69.866,-55.191,-69.854,-55.22,-69.824,-55.237,-69.68,-55.219,-69.657,-55.229,-69.656,-55.298,-69.646,-55.321,-69.61,-55.34,-69.509,-55.371,-69.456,-55.424,-69.412,-55.444,-69.241,-55.477,-69.181,-55.475,-69.299,-55.369,-69.359,-55.301,-69.356,-55.274,-69.297,-55.166,-69.193,-55.172,-69.151,-55.183,-69.047,-55.244,-69.008,-55.256,-68.913,-55.239,-68.89,-55.241,-68.889,-55.263,-68.932,-55.347,-68.931,-55.371,-68.896,-55.424,-68.867,-55.45,-68.785,-55.436,-68.694,-55.452,-68.594,-55.45,-68.467,-55.489,-68.338,-55.505,-68.293,-55.521,-68.23,-55.602,-68.157,-55.634,-68.083,-55.651,-68.048,-55.643,-68.046,-55.587,-68.058,-55.518,-68.09,-55.478,-68.153,-55.437,-68.305,-55.357]},{"b":[-75.641,-49.231,-75.107,-48.773],"l":[-75.115,-48.916,-75.107,-48.837,-75.119,-48.773,-75.236,-48.779,-75.297,-48.811,-75.49,-48.85,-75.535,-48.838,-75.583,-48.859,-75.619,-48.886,-75.638,-48.943,-75.576,-48.981,-75.54,-48.988,-75.515,-49.01,-75.488,-49.082,-75.573,-49.139,-75.641,-49.195,-75.58,-49.23,-75.506,-49.231,-75.39,-49.159,-75.263,-49.069,-75.115,-48.916]},{"b":[-60.276,-51.396,-60.07,-51.273],"l":[-60.249,-51.396,-60.276,-51.363,-60.275,-51.281,-60.171,-51.273,-60.07,-51.308,-60.076,-51.343,-60.112,-51.396,-60.249,-51.396]},{"b":[-26.451,-58.492,-26.26,-58.382],"l":[-26.303,-58.382,-26.401,-58.383,-26.451,-58.415,-26.415,-58.44,-26.26,-58.492,-26.264,-58.435,-26.279,-58.402,-26.303,-58.382]},{"b":[53.316,12.319,54.511,12.716],"l":[54.187,12.664,53.919,12.659,53.825,12.625,53.763,12.637,53.638,12.707,53.535,12.716,53.431,12.664,53.404,12.633,53.388,12.602,53.316,12.533,53.499,12.425,53.598,12.342,53.719,12.319,54.129,12.361,54.271,12.447,54.414,12.483,54.45,12.523,54.511,12.553,54.187,12.664]},{"b":[53.191,24.253,53.445,24.411],"l":[53.445,24.371,53.412,24.411,53.371,24.364,53.333,24.342,53.191,24.291,53.258,24.253,53.332,24.259,53.383,24.281,53.409,24.308,53.445,24.371]},{"b":[58.641,20.204,58.951,20.681],"l":[58.788,20.497,58.641,20.337,58.641,20.211,58.659,20.204,58.722,20.219,58.772,20.267,58.835,20.424,58.951,20.516,58.884,20.681,58.788,20.497]},{"b":[52.582,24.281,52.658,24.377],"l":[52.6,24.281,52.617,24.289,52.658,24.333,52.629,24.377,52.584,24.352,52.582,24.335,52.6,24.281]},{"b":[55.295,26.583,56.279,27.003],"l":[56.074,26.983,55.907,26.91,55.758,26.948,55.747,26.931,55.785,26.857,55.763,26.812,55.532,26.71,55.347,26.648,55.296,26.658,55.295,26.639,55.312,26.593,55.34,26.586,55.424,26.583,55.543,26.618,55.675,26.686,55.747,26.692,55.848,26.731,55.894,26.732,55.954,26.701,56.095,26.801,56.188,26.921,56.279,26.952,56.214,27.003,56.074,26.983]},{"b":[53.634,24.136,53.928,24.259],"l":[53.799,24.136,53.826,24.153,53.928,24.143,53.928,24.177,53.894,24.215,53.834,24.259,53.69,24.211,53.634,24.17,53.716,24.145,53.799,24.136]},{"b":[41.776,16.571,42.17,17.003],"l":[42.06,16.804,42.026,16.758,42.033,16.742,42.065,16.71,41.988,16.716,41.947,16.749,41.963,16.779,41.954,16.806,41.948,16.936,41.917,16.994,41.86,17.003,41.885,16.947,41.858,16.893,41.816,16.86,41.776,16.847,41.802,16.779,41.897,16.684,41.964,16.653,42.072,16.671,42.102,16.644,42.108,16.618,42.128,16.595,42.158,16.571,42.167,16.596,42.17,16.709,42.06,16.804]},{"b":[39.94,15.577,40.408,15.889],"l":[40.25,15.703,40.234,15.666,40.211,15.648,40.183,15.643,40.141,15.696,40.132,15.795,40.097,15.838,40.043,15.875,39.957,15.889,40,15.828,39.979,15.807,39.945,15.789,39.94,15.745,40.016,15.733,40.071,15.677,40.063,15.666,40.024,15.656,39.947,15.696,39.975,15.612,40.095,15.591,40.196,15.598,40.305,15.577,40.399,15.58,40.408,15.629,40.25,15.703]},{"b":[42.69,13.674,42.794,13.769],"l":[42.69,13.674,42.756,13.704,42.794,13.766,42.781,13.769,42.735,13.753,42.69,13.674]},{"b":[50.452,25.807,50.617,26.246],"l":[50.47,26.229,50.452,26.191,50.489,26.058,50.466,25.966,50.544,25.833,50.575,25.807,50.607,25.883,50.617,26.002,50.61,26.124,50.558,26.198,50.586,26.241,50.564,26.246,50.47,26.229]},{"b":[42.694,13.95,42.798,14.067],"l":[42.694,14.008,42.756,13.955,42.774,13.95,42.787,13.971,42.798,14.012,42.762,14.067,42.694,14.008]},{"b":[48.081,29.602,48.348,29.979],"l":[48.348,29.783,48.228,29.936,48.185,29.979,48.159,29.96,48.139,29.897,48.12,29.886,48.113,29.87,48.115,29.849,48.081,29.799,48.143,29.665,48.18,29.611,48.218,29.602,48.275,29.624,48.34,29.695,48.347,29.72,48.348,29.783]},{"b":[179.373,-17.394,179.447,-17.256],"l":[179.373,-17.256,179.389,-17.394,179.422,-17.367,179.447,-17.306,179.433,-17.272,179.408,-17.257,179.373,-17.256]},{"b":[179.254,-18.11,179.362,-17.944],"l":[179.306,-17.944,179.272,-17.971,179.256,-17.999,179.254,-18.031,179.34,-18.11,179.349,-18.102,179.362,-18.065,179.338,-17.99,179.306,-17.944]},{"b":[178.748,-17.747,178.853,-17.619],"l":[178.748,-17.686,178.776,-17.747,178.828,-17.729,178.853,-17.681,178.831,-17.619,178.787,-17.624,178.748,-17.686]},{"b":[168.124,-16.835,168.477,-16.594],"l":[168.323,-16.788,168.46,-16.835,168.477,-16.794,168.447,-16.779,168.366,-16.759,168.296,-16.684,168.265,-16.671,168.234,-16.64,168.199,-16.594,168.182,-16.6,168.135,-16.637,168.124,-16.69,168.149,-16.766,168.181,-16.804,168.212,-16.806,168.323,-16.788]},{"b":[167.929,-16.347,168.298,-16.082],"l":[168.297,-16.337,168.298,-16.299,168.276,-16.265,168.235,-16.231,168.198,-16.12,168.164,-16.082,168.064,-16.181,167.985,-16.196,167.929,-16.229,167.957,-16.272,168.022,-16.316,168.182,-16.347,168.297,-16.337]},{"b":[168.158,-17.807,168.585,-17.542],"l":[168.446,-17.542,168.341,-17.552,168.32,-17.544,168.297,-17.545,168.273,-17.552,168.191,-17.645,168.158,-17.711,168.182,-17.717,168.233,-17.698,168.278,-17.706,168.306,-17.746,168.252,-17.781,168.399,-17.807,168.525,-17.798,168.585,-17.696,168.545,-17.685,168.446,-17.542]},{"b":[167.674,-15.482,168.003,-15.283],"l":[168.003,-15.283,167.826,-15.312,167.674,-15.452,167.72,-15.477,167.844,-15.482,167.911,-15.436,168.003,-15.283]},{"b":[166.526,-15.635,167.201,-14.637],"l":[167.182,-15.39,167.132,-15.135,167.106,-15.126,167.069,-15.072,167.054,-14.974,167.076,-14.936,167.027,-14.923,166.987,-14.94,166.968,-15.062,166.923,-15.139,166.885,-15.157,166.81,-15.157,166.746,-14.827,166.663,-14.735,166.608,-14.637,166.567,-14.642,166.526,-14.76,166.527,-14.85,166.648,-15.212,166.631,-15.406,166.699,-15.516,166.759,-15.567,166.758,-15.631,166.826,-15.635,166.937,-15.578,167.094,-15.581,167.2,-15.486,167.201,-15.443,167.182,-15.39]},{"b":[168.123,-15.972,168.268,-15.462],"l":[168.16,-15.462,168.123,-15.681,168.179,-15.926,168.196,-15.972,168.213,-15.97,168.256,-15.955,168.268,-15.892,168.183,-15.508,168.16,-15.462]},{"b":[168.104,-15.391,168.189,-14.986],"l":[168.13,-15.319,168.172,-15.391,168.189,-15.329,168.187,-15.197,168.136,-14.986,168.115,-14.989,168.104,-15.017,168.13,-15.319]},{"b":[167.151,-16.574,167.837,-15.877],"l":[167.793,-16.395,167.776,-16.341,167.714,-16.314,167.681,-16.261,167.642,-16.263,167.499,-16.166,167.484,-16.118,167.459,-16.118,167.413,-16.096,167.336,-15.917,167.254,-15.877,167.2,-15.885,167.183,-15.929,167.151,-16.08,167.218,-16.155,167.246,-16.15,167.316,-16.116,167.349,-16.154,167.38,-16.246,167.401,-16.401,167.447,-16.502,167.436,-16.515,167.449,-16.555,167.526,-16.574,167.611,-16.499,167.76,-16.516,167.837,-16.45,167.793,-16.395]},{"b":[167.095,-15.75,167.234,-15.623],"l":[167.234,-15.645,167.119,-15.623,167.095,-15.685,167.201,-15.75,167.219,-15.724,167.234,-15.645]},{"b":[158.457,-8.557,159.881,-7.545],"l":[159.794,-8.406,159.843,-8.327,159.431,-8.029,159.368,-7.994,159.287,-7.976,159.198,-7.91,159.109,-7.904,159.011,-7.837,158.972,-7.789,158.863,-7.722,158.734,-7.604,158.457,-7.545,158.479,-7.577,158.565,-7.651,158.597,-7.759,158.686,-7.818,158.778,-7.907,158.832,-7.927,158.855,-7.96,158.944,-8.041,159.09,-8.103,159.239,-8.196,159.292,-8.203,159.354,-8.26,159.645,-8.372,159.746,-8.474,159.881,-8.557,159.879,-8.534,159.849,-8.463,159.794,-8.406]},{"b":[156.453,-7.426,157.519,-6.609],"l":[157.412,-7.309,157.336,-7.28,157.193,-7.16,157.148,-7.114,157.103,-6.957,157.03,-6.892,156.765,-6.764,156.604,-6.641,156.479,-6.609,156.453,-6.638,156.457,-6.715,156.495,-6.762,156.696,-6.911,156.904,-7.18,157.102,-7.324,157.243,-7.353,157.315,-7.342,157.317,-7.359,157.339,-7.393,157.441,-7.426,157.519,-7.366,157.487,-7.33,157.452,-7.314,157.412,-7.309]},{"b":[153.592,-4.123,153.663,-4.041],"l":[153.592,-4.096,153.65,-4.123,153.659,-4.099,153.663,-4.041,153.64,-4.045,153.592,-4.096]},{"b":[120.876,5.87,121.415,6.096],"l":[121.283,6.022,121.214,6.004,121.159,6.076,121.038,6.096,120.898,6.007,120.876,5.953,120.931,5.896,121.019,5.923,121.083,5.893,121.218,5.943,121.294,5.87,121.411,5.94,121.415,5.965,121.392,6.002,121.283,6.022]},{"b":[114.299,-7.173,114.413,-7.073],"l":[114.299,-7.098,114.347,-7.163,114.398,-7.173,114.413,-7.133,114.384,-7.081,114.349,-7.073,114.322,-7.08,114.299,-7.098]},{"b":[134.182,34.423,134.372,34.534],"l":[134.182,34.519,134.188,34.496,134.238,34.467,134.252,34.423,134.315,34.469,134.333,34.464,134.352,34.484,134.372,34.522,134.326,34.534,134.182,34.519]},{"b":[-75.261,-47.851,-74.916,-47.691],"l":[-75.084,-47.825,-75.051,-47.8,-74.916,-47.757,-74.926,-47.723,-75.004,-47.695,-75.09,-47.691,-75.203,-47.728,-75.261,-47.764,-75.194,-47.818,-75.186,-47.851,-75.112,-47.838,-75.084,-47.825]},{"b":[-74.843,-43.625,-74.665,-43.536],"l":[-74.665,-43.6,-74.673,-43.577,-74.697,-43.553,-74.745,-43.536,-74.818,-43.549,-74.842,-43.57,-74.843,-43.596,-74.81,-43.625,-74.669,-43.608,-74.665,-43.6]},{"b":[-73.818,-44.833,-73.617,-44.544],"l":[-73.628,-44.681,-73.641,-44.611,-73.686,-44.546,-73.724,-44.544,-73.779,-44.559,-73.817,-44.614,-73.818,-44.652,-73.8,-44.684,-73.735,-44.752,-73.725,-44.797,-73.695,-44.831,-73.665,-44.833,-73.632,-44.821,-73.617,-44.753,-73.628,-44.681]},{"b":[-75.247,-48.626,-74.567,-47.839],"l":[-74.567,-48.592,-74.618,-48.425,-74.6,-48.393,-74.602,-48.37,-74.615,-48.343,-74.664,-48.299,-74.702,-48.206,-74.715,-48.146,-74.729,-48.126,-74.805,-48.078,-74.846,-48.021,-74.827,-47.85,-74.896,-47.839,-74.975,-47.923,-75.198,-47.975,-75.247,-48.027,-75.234,-48.053,-75.213,-48.142,-75.158,-48.225,-75.132,-48.279,-75.079,-48.362,-75.052,-48.391,-75.013,-48.536,-74.923,-48.626,-74.71,-48.601,-74.586,-48.616,-74.567,-48.592]},{"b":[-64.757,-54.903,-63.815,-54.716],"l":[-64.055,-54.73,-64.105,-54.722,-64.221,-54.722,-64.439,-54.739,-64.549,-54.716,-64.581,-54.753,-64.625,-54.774,-64.689,-54.775,-64.757,-54.827,-64.731,-54.863,-64.637,-54.903,-64.509,-54.84,-64.453,-54.84,-64.323,-54.796,-64.028,-54.793,-63.971,-54.811,-63.833,-54.768,-63.815,-54.725,-63.882,-54.723,-64.032,-54.742,-64.055,-54.73]},{"b":[-62.093,-39.235,-61.866,-39.086],"l":[-61.967,-39.112,-62.093,-39.086,-62.083,-39.11,-62.042,-39.167,-61.918,-39.227,-61.866,-39.235,-61.876,-39.172,-61.907,-39.136,-61.967,-39.112]},{"b":[-59.793,-52.251,-59.681,-52.134],"l":[-59.753,-52.141,-59.793,-52.134,-59.786,-52.156,-59.785,-52.205,-59.764,-52.242,-59.747,-52.251,-59.683,-52.232,-59.681,-52.18,-59.753,-52.141]},{"b":[-61.145,-51.946,-60.876,-51.786],"l":[-60.876,-51.794,-60.947,-51.8,-61.019,-51.786,-61.052,-51.814,-61.145,-51.839,-61.116,-51.875,-61.032,-51.942,-60.948,-51.946,-60.916,-51.897,-60.876,-51.794]},{"b":[-73.845,-54.127,-72.205,-53.394],"l":[-72.471,-54.028,-72.409,-54.004,-72.369,-53.981,-72.366,-53.941,-72.306,-53.862,-72.205,-53.807,-72.307,-53.725,-72.373,-53.687,-72.459,-53.599,-72.482,-53.588,-72.685,-53.558,-72.809,-53.565,-72.882,-53.578,-72.896,-53.563,-72.923,-53.482,-72.947,-53.442,-72.971,-53.423,-73.022,-53.415,-73.054,-53.394,-73.074,-53.397,-73.111,-53.425,-73.115,-53.448,-73.099,-53.512,-73.366,-53.47,-73.447,-53.41,-73.687,-53.427,-73.845,-53.546,-73.642,-53.57,-73.582,-53.655,-73.471,-53.736,-73.36,-53.724,-73.325,-53.723,-73.314,-53.729,-73.295,-53.792,-73.293,-53.836,-73.312,-53.92,-73.305,-53.944,-73.211,-53.986,-73.12,-54.009,-73.081,-53.998,-73.071,-53.978,-73.086,-53.916,-73.073,-53.875,-73.039,-53.833,-72.984,-53.861,-72.936,-53.861,-72.872,-53.849,-72.764,-53.865,-72.782,-53.955,-72.882,-54.042,-72.959,-54.066,-72.946,-54.092,-72.907,-54.115,-72.871,-54.127,-72.84,-54.125,-72.789,-54.103,-72.677,-54.079,-72.563,-54.074,-72.471,-54.028]},{"b":[-68.301,-55.293,-67.08,-54.929],"l":[-67.737,-55.256,-67.691,-55.243,-67.585,-55.192,-67.535,-55.179,-67.495,-55.177,-67.463,-55.182,-67.443,-55.201,-67.429,-55.237,-67.399,-55.272,-67.34,-55.293,-67.257,-55.282,-67.173,-55.243,-67.109,-55.192,-67.08,-55.154,-67.085,-55.115,-67.107,-55.064,-67.245,-54.978,-67.425,-54.969,-67.874,-54.93,-68.107,-54.929,-68.301,-54.981,-68.174,-55.071,-68.135,-55.173,-68.1,-55.207,-68.07,-55.221,-67.768,-55.26,-67.737,-55.256]},{"b":[-71.437,-55.177,-70.283,-54.835],"l":[-70.283,-55.066,-70.418,-54.909,-70.535,-54.921,-70.615,-54.946,-70.749,-54.953,-70.805,-54.968,-70.928,-54.943,-70.945,-54.931,-70.992,-54.868,-71.089,-54.867,-71.197,-54.844,-71.374,-54.835,-71.411,-54.839,-71.437,-54.889,-71.427,-54.914,-71.407,-54.931,-71.389,-54.934,-71.325,-54.914,-71.299,-54.892,-71.274,-54.887,-71.203,-54.893,-71.12,-54.938,-70.991,-54.99,-70.967,-55.007,-70.965,-55.04,-70.94,-55.062,-70.815,-55.08,-70.744,-55.104,-70.711,-55.107,-70.641,-55.085,-70.597,-55.082,-70.551,-55.112,-70.539,-55.135,-70.543,-55.161,-70.476,-55.177,-70.404,-55.166,-70.298,-55.114,-70.283,-55.066]},{"b":[-67.563,-55.785,-67.262,-55.585],"l":[-67.289,-55.777,-67.267,-55.763,-67.262,-55.744,-67.31,-55.689,-67.351,-55.612,-67.374,-55.589,-67.397,-55.585,-67.449,-55.641,-67.513,-55.662,-67.546,-55.684,-67.563,-55.708,-67.56,-55.725,-67.393,-55.753,-67.352,-55.766,-67.325,-55.785,-67.289,-55.777]},{"b":[-75.3,-51.625,-74.559,-51.207],"l":[-74.593,-51.387,-74.561,-51.361,-74.559,-51.277,-74.571,-51.245,-74.612,-51.207,-74.737,-51.208,-74.881,-51.279,-75.04,-51.318,-75.154,-51.279,-75.21,-51.383,-75.238,-51.454,-75.3,-51.556,-75.289,-51.625,-75.192,-51.567,-75.146,-51.524,-75.047,-51.398,-74.937,-51.428,-74.853,-51.434,-74.797,-51.412,-74.731,-51.367,-74.691,-51.37,-74.62,-51.396,-74.593,-51.387]},{"b":[-75.449,-50.376,-74.839,-50.012],"l":[-74.963,-50.237,-74.839,-50.197,-74.876,-50.11,-75.004,-50.089,-75.123,-50.055,-75.21,-50.045,-75.327,-50.012,-75.369,-50.113,-75.377,-50.168,-75.398,-50.193,-75.412,-50.257,-75.449,-50.343,-75.308,-50.343,-75.25,-50.376,-75.055,-50.296,-74.963,-50.237]},{"b":[-75.105,-52.279,-74.532,-51.63],"l":[-74.537,-51.965,-74.647,-51.866,-74.75,-51.852,-74.78,-51.825,-74.823,-51.63,-74.91,-51.65,-74.915,-51.738,-75.008,-51.724,-75.105,-51.789,-75.051,-51.904,-75.017,-52.038,-74.918,-52.152,-74.852,-52.271,-74.694,-52.279,-74.666,-52.16,-74.532,-51.992,-74.537,-51.965]},{"b":[-74.143,-43.922,-73.79,-43.784],"l":[-73.938,-43.914,-73.904,-43.875,-73.834,-43.883,-73.79,-43.876,-73.811,-43.827,-73.841,-43.789,-73.857,-43.784,-73.967,-43.817,-74.14,-43.821,-74.143,-43.872,-74.118,-43.887,-73.956,-43.922,-73.938,-43.914]},{"b":[-72.21,-54.374,-71.005,-53.885],"l":[-71.671,-54.225,-71.558,-54.246,-71.473,-54.231,-71.305,-54.314,-71.143,-54.374,-71.118,-54.366,-71.083,-54.316,-71.028,-54.281,-71.005,-54.247,-71.023,-54.162,-71.022,-54.112,-71.169,-54.113,-71.39,-54.033,-71.554,-53.956,-71.705,-53.923,-71.996,-53.885,-72.069,-53.921,-72.146,-53.939,-72.21,-54.048,-72.092,-54.119,-71.972,-54.207,-71.949,-54.301,-71.818,-54.276,-71.761,-54.23,-71.671,-54.225]},{"b":[-74.475,-51.942,-74.115,-51.725],"l":[-74.283,-51.919,-74.172,-51.942,-74.142,-51.931,-74.119,-51.911,-74.115,-51.888,-74.133,-51.871,-74.277,-51.812,-74.326,-51.77,-74.362,-51.751,-74.451,-51.725,-74.475,-51.726,-74.437,-51.791,-74.424,-51.845,-74.339,-51.898,-74.283,-51.919]},{"b":[-74.618,-45.345,-73.703,-44.135],"l":[-73.727,-45.119,-73.786,-45.034,-73.795,-44.979,-73.792,-44.946,-73.828,-44.84,-73.877,-44.729,-73.919,-44.655,-74.002,-44.591,-73.996,-44.538,-73.983,-44.495,-73.862,-44.445,-73.785,-44.437,-73.735,-44.395,-73.704,-44.325,-73.703,-44.274,-73.818,-44.235,-73.865,-44.185,-73.9,-44.135,-73.995,-44.14,-74.083,-44.186,-74.108,-44.276,-74.097,-44.389,-74.133,-44.416,-74.212,-44.427,-74.301,-44.396,-74.422,-44.435,-74.502,-44.474,-74.481,-44.585,-74.618,-44.648,-74.499,-44.748,-74.419,-44.865,-74.35,-44.911,-74.268,-45.059,-74.195,-45.145,-74.089,-45.196,-74.099,-45.325,-74.016,-45.345,-73.849,-45.341,-73.834,-45.327,-73.83,-45.283,-73.771,-45.277,-73.752,-45.267,-73.728,-45.196,-73.722,-45.158,-73.727,-45.119]},{"b":[-74.69,-45.757,-74.229,-45.173],"l":[-74.229,-45.611,-74.24,-45.575,-74.315,-45.464,-74.285,-45.277,-74.311,-45.173,-74.422,-45.203,-74.45,-45.253,-74.502,-45.285,-74.495,-45.426,-74.558,-45.526,-74.646,-45.6,-74.69,-45.663,-74.678,-45.739,-74.562,-45.722,-74.466,-45.757,-74.368,-45.736,-74.313,-45.692,-74.244,-45.654,-74.229,-45.611]},{"b":[-73.961,45.441,-73.476,45.705],"l":[-73.687,45.561,-73.853,45.516,-73.961,45.441,-73.92,45.442,-73.775,45.468,-73.644,45.449,-73.567,45.469,-73.552,45.49,-73.539,45.546,-73.476,45.705,-73.522,45.701,-73.687,45.561]},{"b":[-73.858,45.565,-73.572,45.694],"l":[-73.695,45.585,-73.572,45.694,-73.725,45.672,-73.858,45.574,-73.816,45.565,-73.695,45.585]},{"b":[-61.082,45.482,-60.912,45.577],"l":[-61.026,45.577,-61.082,45.558,-61.076,45.537,-61.012,45.496,-61.003,45.482,-60.962,45.49,-60.953,45.516,-60.912,45.567,-61.026,45.577]},{"b":[-62.008,47.219,-61.396,47.647],"l":[-61.476,47.564,-61.396,47.638,-61.474,47.647,-61.548,47.632,-61.628,47.594,-61.827,47.469,-61.925,47.425,-62.008,47.234,-61.951,47.219,-61.834,47.223,-61.773,47.26,-61.815,47.268,-61.879,47.266,-61.914,47.285,-61.887,47.345,-61.831,47.392,-61.751,47.431,-61.684,47.499,-61.582,47.56,-61.476,47.564]},{"b":[-64.685,47.748,-64.509,47.887],"l":[-64.565,47.866,-64.591,47.872,-64.663,47.863,-64.66,47.794,-64.685,47.754,-64.665,47.748,-64.621,47.752,-64.534,47.814,-64.509,47.887,-64.565,47.866]},{"b":[-64.591,47.907,-64.476,48.014],"l":[-64.591,47.907,-64.476,47.959,-64.481,48.007,-64.5,48.014,-64.52,48.005,-64.541,47.985,-64.591,47.907]},{"b":[-71.117,46.865,-70.826,46.996],"l":[-71.095,46.9,-71.117,46.865,-71.026,46.873,-70.913,46.92,-70.826,46.995,-70.88,46.996,-70.971,46.961,-71.095,46.9]},{"b":[-66.324,44.257,-66.21,44.392],"l":[-66.324,44.257,-66.274,44.292,-66.21,44.392,-66.25,44.379,-66.312,44.292,-66.324,44.257]},{"b":[-68.703,44.176,-68.623,44.256],"l":[-68.691,44.249,-68.703,44.232,-68.702,44.183,-68.661,44.176,-68.623,44.196,-68.656,44.242,-68.677,44.256,-68.691,44.249]},{"b":[-66.897,44.629,-66.745,44.805],"l":[-66.897,44.629,-66.762,44.682,-66.753,44.71,-66.745,44.791,-66.802,44.805,-66.845,44.764,-66.897,44.629]},{"b":[-68.412,44.25,-68.187,44.456],"l":[-68.299,44.456,-68.347,44.43,-68.409,44.364,-68.412,44.294,-68.386,44.277,-68.315,44.25,-68.308,44.269,-68.309,44.321,-68.245,44.313,-68.187,44.332,-68.191,44.364,-68.238,44.438,-68.299,44.456]},{"b":[112.726,-7.224,114.083,-6.873],"l":[113.067,-6.88,112.868,-6.9,112.769,-7.001,112.726,-7.073,112.764,-7.14,113.04,-7.212,113.127,-7.224,113.142,-7.208,113.166,-7.207,113.198,-7.218,113.471,-7.218,113.546,-7.193,113.656,-7.112,113.826,-7.12,113.845,-7.105,113.885,-7.049,114.083,-6.989,114.074,-6.96,113.975,-6.873,113.067,-6.88]},{"b":[158.836,-54.749,158.959,-54.472],"l":[158.836,-54.704,158.845,-54.749,158.879,-54.71,158.946,-54.575,158.959,-54.472,158.897,-54.506,158.836,-54.704]},{"b":[-174.069,-18.699,-173.922,-18.565],"l":[-174.069,-18.64,-174.053,-18.663,-174.009,-18.698,-173.991,-18.699,-173.954,-18.639,-173.924,-18.608,-173.922,-18.589,-173.968,-18.565,-174.002,-18.571,-174.069,-18.64]},{"b":[-151.485,-16.879,-151.365,-16.74],"l":[-151.485,-16.864,-151.449,-16.879,-151.41,-16.878,-151.365,-16.864,-151.411,-16.774,-151.467,-16.74,-151.476,-16.761,-151.485,-16.864]},{"b":[-157.579,1.732,-157.176,2.029],"l":[-157.342,1.856,-157.322,1.969,-157.442,2.025,-157.492,2.029,-157.365,1.946,-157.393,1.928,-157.436,1.847,-157.508,1.886,-157.531,1.927,-157.579,1.902,-157.42,1.788,-157.246,1.732,-157.176,1.74,-157.342,1.856]},{"b":[-139.166,-9.846,-138.827,-9.695],"l":[-139.024,-9.695,-139.166,-9.77,-139.134,-9.829,-139.074,-9.846,-139.024,-9.821,-138.875,-9.793,-138.827,-9.742,-138.874,-9.747,-139.024,-9.695]},{"b":[-139.134,-10.01,-139.06,-9.915],"l":[-139.134,-9.926,-139.134,-10.01,-139.06,-9.931,-139.083,-9.915,-139.107,-9.915,-139.134,-9.926]},{"b":[-138.69,-10.532,-138.624,-10.426],"l":[-138.688,-10.532,-138.651,-10.515,-138.632,-10.492,-138.624,-10.463,-138.643,-10.446,-138.69,-10.426,-138.688,-10.532]},{"b":[-149.635,-17.862,-149.151,-17.496],"l":[-149.322,-17.69,-149.33,-17.589,-149.379,-17.522,-149.508,-17.496,-149.611,-17.532,-149.635,-17.564,-149.633,-17.618,-149.579,-17.735,-149.482,-17.753,-149.341,-17.732,-149.29,-17.822,-149.254,-17.85,-149.182,-17.862,-149.151,-17.812,-149.178,-17.737,-149.322,-17.69]},{"b":[-140.253,-8.934,-140.044,-8.782],"l":[-140.224,-8.782,-140.24,-8.798,-140.253,-8.848,-140.217,-8.93,-140.171,-8.934,-140.073,-8.91,-140.046,-8.874,-140.044,-8.838,-140.058,-8.801,-140.224,-8.782]},{"b":[-140.144,-9.444,-140.031,-9.328],"l":[-140.144,-9.359,-140.138,-9.384,-140.097,-9.444,-140.076,-9.426,-140.031,-9.345,-140.071,-9.328,-140.144,-9.359]},{"b":[-172.779,-13.804,-172.177,-13.465],"l":[-172.333,-13.465,-172.511,-13.483,-172.67,-13.524,-172.779,-13.517,-172.744,-13.579,-172.659,-13.645,-172.536,-13.792,-172.485,-13.8,-172.331,-13.775,-172.225,-13.804,-172.177,-13.685,-172.222,-13.56,-172.333,-13.465]},{"b":[-179.018,-17.307,-178.915,-17.148],"l":[-179.015,-17.182,-178.971,-17.213,-178.976,-17.237,-179.004,-17.295,-178.982,-17.307,-178.956,-17.273,-178.925,-17.249,-178.915,-17.223,-178.921,-17.208,-178.953,-17.182,-179.005,-17.148,-179.018,-17.161,-179.015,-17.182]},{"b":[-159.409,3.797,-159.259,3.924],"l":[-159.409,3.873,-159.378,3.847,-159.373,3.881,-159.354,3.881,-159.327,3.863,-159.306,3.838,-159.314,3.823,-159.359,3.815,-159.332,3.8,-159.275,3.797,-159.259,3.839,-159.339,3.924,-159.369,3.917,-159.391,3.9,-159.409,3.873]},{"b":[-172.046,-14.047,-171.45,-13.807],"l":[-171.858,-13.807,-171.985,-13.824,-172.046,-13.857,-172.028,-13.907,-171.912,-14.002,-171.864,-14.002,-171.728,-14.047,-171.454,-14.046,-171.45,-14.022,-171.461,-13.978,-171.507,-13.95,-171.565,-13.943,-171.604,-13.879,-171.858,-13.807]},{"b":[-170.821,-14.36,-170.568,-14.257],"l":[-170.689,-14.257,-170.721,-14.276,-170.821,-14.312,-170.769,-14.36,-170.726,-14.351,-170.64,-14.282,-170.568,-14.267,-170.689,-14.257]},{"b":[-169.948,-19.138,-169.793,-18.966],"l":[-169.909,-18.99,-169.948,-19.073,-169.904,-19.138,-169.803,-19.083,-169.793,-19.043,-169.834,-18.966,-169.862,-18.969,-169.909,-18.99]},{"b":[-176.195,-13.341,-176.128,-13.222],"l":[-176.195,-13.302,-176.177,-13.341,-176.161,-13.333,-176.128,-13.268,-176.148,-13.222,-176.171,-13.243,-176.195,-13.302]},{"b":[-174.973,-21.451,-174.913,-21.3],"l":[-174.973,-21.35,-174.968,-21.382,-174.919,-21.451,-174.913,-21.3,-174.923,-21.303,-174.973,-21.35]},{"b":[134.506,7.361,134.66,7.712],"l":[134.6,7.616,134.556,7.594,134.516,7.526,134.506,7.437,134.535,7.361,134.595,7.382,134.598,7.438,134.633,7.501,134.66,7.663,134.651,7.712,134.609,7.624,134.6,7.616]},{"b":[145.729,18.057,145.835,18.173],"l":[145.729,18.057,145.778,18.079,145.835,18.137,145.807,18.173,145.789,18.155,145.729,18.057]},{"b":[145.684,15.107,145.822,15.265],"l":[145.713,15.215,145.684,15.125,145.698,15.114,145.749,15.107,145.752,15.133,145.782,15.175,145.789,15.223,145.822,15.265,145.786,15.257,145.713,15.215]},{"b":[144.649,13.258,144.941,13.622],"l":[144.79,13.527,144.649,13.429,144.65,13.313,144.663,13.291,144.7,13.258,144.742,13.259,144.78,13.411,144.941,13.57,144.91,13.599,144.875,13.615,144.837,13.622,144.79,13.527]},{"b":[145.587,14.92,145.662,15.06],"l":[145.587,15.031,145.592,14.999,145.621,14.92,145.662,14.971,145.647,15.059,145.625,15.06,145.587,15.031]},{"b":[138.062,9.419,138.214,9.593],"l":[138.085,9.495,138.062,9.446,138.067,9.419,138.143,9.501,138.183,9.507,138.214,9.547,138.186,9.593,138.147,9.584,138.117,9.55,138.085,9.495]},{"b":[177.959,-19.152,178.488,-18.934],"l":[178.334,-18.934,178.282,-18.957,178.208,-18.97,178.157,-19.028,178.104,-19.066,178.052,-19.06,178.001,-19.101,177.959,-19.122,178.021,-19.152,178.162,-19.121,178.182,-19.112,178.189,-19.092,178.211,-19.067,178.288,-19.004,178.316,-19.01,178.359,-19.046,178.488,-19.017,178.488,-18.974,178.42,-18.951,178.334,-18.934]},{"b":[167.404,-14.312,167.599,-14.142],"l":[167.439,-14.168,167.411,-14.197,167.404,-14.282,167.43,-14.295,167.543,-14.312,167.585,-14.261,167.599,-14.184,167.506,-14.142,167.439,-14.168]},{"b":[167.392,-13.917,167.554,-13.709],"l":[167.499,-13.885,167.543,-13.873,167.553,-13.846,167.554,-13.814,167.547,-13.777,167.481,-13.709,167.407,-13.748,167.392,-13.788,167.451,-13.909,167.474,-13.917,167.489,-13.907,167.499,-13.885]},{"b":[169.738,-20.242,169.896,-20.145],"l":[169.751,-20.153,169.738,-20.202,169.807,-20.241,169.861,-20.242,169.896,-20.187,169.852,-20.148,169.829,-20.145,169.751,-20.153]},{"b":[169.217,-19.649,169.491,-19.322],"l":[169.36,-19.458,169.337,-19.329,169.291,-19.322,169.247,-19.345,169.217,-19.476,169.262,-19.545,169.347,-19.624,169.438,-19.649,169.491,-19.54,169.36,-19.458]},{"b":[168.987,-18.989,169.334,-18.617],"l":[169.296,-18.867,169.201,-18.796,169.256,-18.763,169.178,-18.725,169.144,-18.631,169.088,-18.617,169.016,-18.644,168.987,-18.708,168.998,-18.825,168.987,-18.871,169.248,-18.983,169.288,-18.989,169.334,-18.94,169.296,-18.867]},{"b":[167.815,-21.643,168.139,-21.338],"l":[168.011,-21.43,167.995,-21.407,167.985,-21.37,167.988,-21.338,167.926,-21.373,167.815,-21.393,167.879,-21.524,167.876,-21.582,167.941,-21.606,167.967,-21.642,168.006,-21.643,168.121,-21.616,168.139,-21.445,168.058,-21.448,168.011,-21.43]},{"b":[166.494,-20.709,166.671,-20.413],"l":[166.56,-20.561,166.558,-20.617,166.494,-20.709,166.547,-20.699,166.589,-20.662,166.623,-20.596,166.602,-20.585,166.6,-20.525,166.618,-20.478,166.671,-20.45,166.625,-20.418,166.583,-20.413,166.585,-20.45,166.56,-20.561]},{"b":[167.033,-21.169,167.431,-20.674],"l":[167.293,-20.892,167.298,-20.733,167.269,-20.701,167.204,-20.674,167.056,-20.72,167.045,-20.759,167.136,-20.766,167.189,-20.804,167.112,-20.904,167.033,-20.923,167.073,-20.997,167.134,-21.061,167.273,-21.097,167.346,-21.169,167.401,-21.161,167.43,-21.087,167.431,-21.055,167.361,-20.942,167.293,-20.892]},{"b":[167.422,-22.661,167.544,-22.541],"l":[167.443,-22.541,167.422,-22.619,167.444,-22.639,167.473,-22.653,167.513,-22.661,167.544,-22.623,167.529,-22.579,167.443,-22.541]},{"b":[159.928,-19.333,159.975,-19.115],"l":[159.928,-19.174,159.936,-19.333,159.952,-19.312,159.975,-19.238,159.96,-19.115,159.928,-19.174]},{"b":[166.747,-11.681,166.929,-11.571],"l":[166.855,-11.579,166.791,-11.571,166.747,-11.591,166.806,-11.677,166.841,-11.681,166.929,-11.665,166.875,-11.63,166.855,-11.579]},{"b":[165.79,-10.851,166.162,-10.661],"l":[166.024,-10.661,165.909,-10.674,165.86,-10.703,165.836,-10.761,165.79,-10.756,165.791,-10.785,165.819,-10.844,165.857,-10.841,165.904,-10.851,165.968,-10.779,166.028,-10.77,166.053,-10.775,166.133,-10.758,166.13,-10.745,166.162,-10.693,166.126,-10.68,166.024,-10.661]},{"b":[159.979,-11.832,160.576,-11.472],"l":[160.077,-11.493,160,-11.472,159.986,-11.495,159.979,-11.538,160.004,-11.58,160.087,-11.594,160.1,-11.611,160.15,-11.644,160.27,-11.664,160.355,-11.712,160.395,-11.789,160.443,-11.815,160.507,-11.832,160.576,-11.798,160.537,-11.759,160.449,-11.696,160.077,-11.493]},{"b":[160.59,-9.611,161.378,-8.314],"l":[161.368,-9.49,161.258,-9.317,161.257,-9.192,161.209,-9.133,161.205,-9.092,161.159,-8.962,161.043,-8.855,160.976,-8.837,160.944,-8.799,160.954,-8.699,160.988,-8.665,160.998,-8.612,160.749,-8.314,160.702,-8.317,160.685,-8.336,160.649,-8.338,160.596,-8.328,160.59,-8.373,160.714,-8.539,160.663,-8.621,160.772,-8.964,160.873,-9.157,161.024,-9.271,161.042,-9.308,161.191,-9.393,161.322,-9.59,161.367,-9.611,161.378,-9.574,161.368,-9.49]},{"b":[161.286,-10.832,162.373,-10.204],"l":[162.288,-10.776,162.287,-10.71,162.157,-10.506,162.105,-10.454,162.023,-10.477,161.914,-10.436,161.841,-10.446,161.715,-10.387,161.698,-10.371,161.654,-10.352,161.476,-10.238,161.382,-10.206,161.305,-10.204,161.286,-10.282,161.294,-10.326,161.398,-10.332,161.487,-10.361,161.499,-10.455,161.539,-10.491,161.538,-10.566,161.787,-10.717,161.906,-10.764,162.043,-10.785,162.124,-10.824,162.201,-10.808,162.301,-10.832,162.373,-10.823,162.288,-10.776]},{"b":[161.364,-9.77,161.559,-9.353],"l":[161.412,-9.6,161.41,-9.682,161.442,-9.719,161.478,-9.691,161.554,-9.77,161.559,-9.733,161.548,-9.626,161.407,-9.368,161.364,-9.353,161.402,-9.448,161.417,-9.514,161.412,-9.6]},{"b":[159.036,-9.126,159.234,-8.993],"l":[159.175,-9.126,159.189,-9.124,159.234,-9.094,159.228,-9.03,159.176,-9.022,159.154,-9.001,159.13,-8.993,159.078,-9.025,159.036,-9.075,159.071,-9.11,159.128,-9.114,159.175,-9.126]},{"b":[160.096,-9.181,160.408,-8.996],"l":[160.319,-9.061,160.254,-9.007,160.226,-9.01,160.168,-8.996,160.096,-9.034,160.105,-9.081,160.175,-9.084,160.253,-9.123,160.268,-9.163,160.276,-9.169,160.3,-9.16,160.371,-9.181,160.408,-9.14,160.319,-9.061]},{"b":[159.538,-8.521,159.688,-8.379],"l":[159.646,-8.45,159.642,-8.414,159.595,-8.379,159.553,-8.399,159.538,-8.451,159.569,-8.485,159.64,-8.521,159.688,-8.508,159.646,-8.45]},{"b":[157.218,-8.612,157.898,-7.966],"l":[157.885,-8.569,157.898,-8.506,157.826,-8.324,157.763,-8.242,157.651,-8.217,157.612,-8.165,157.599,-8.006,157.491,-7.966,157.433,-7.985,157.341,-8.096,157.322,-8.161,157.229,-8.212,157.218,-8.263,157.232,-8.315,157.302,-8.333,157.351,-8.275,157.504,-8.258,157.558,-8.27,157.565,-8.338,157.588,-8.445,157.656,-8.5,157.749,-8.524,157.819,-8.612,157.834,-8.573,157.885,-8.569]},{"b":[158.155,-8.826,158.253,-8.678],"l":[158.21,-8.678,158.155,-8.786,158.179,-8.826,158.201,-8.822,158.253,-8.797,158.236,-8.765,158.21,-8.678]},{"b":[157.879,-8.763,158.132,-8.508],"l":[157.909,-8.566,157.898,-8.587,157.879,-8.669,157.938,-8.736,158.009,-8.763,158.108,-8.684,158.104,-8.646,158.09,-8.623,158.068,-8.607,158.132,-8.557,158.105,-8.537,157.998,-8.508,157.967,-8.544,157.938,-8.561,157.909,-8.566]},{"b":[157.212,-8.728,157.411,-8.421],"l":[157.411,-8.475,157.379,-8.421,157.345,-8.432,157.234,-8.52,157.212,-8.565,157.334,-8.7,157.389,-8.728,157.389,-8.713,157.332,-8.651,157.347,-8.575,157.383,-8.555,157.411,-8.475]},{"b":[156.502,-7.937,156.809,-7.574],"l":[156.718,-7.696,156.64,-7.613,156.561,-7.574,156.502,-7.64,156.511,-7.708,156.612,-7.806,156.611,-7.866,156.635,-7.883,156.669,-7.937,156.688,-7.923,156.708,-7.877,156.79,-7.778,156.809,-7.723,156.718,-7.696]},{"b":[156.54,-8.196,156.612,-7.959],"l":[156.57,-7.959,156.551,-7.971,156.542,-8.011,156.54,-8.073,156.592,-8.196,156.604,-8.172,156.612,-8.096,156.57,-7.959]},{"b":[156.958,-8.123,157.201,-7.855],"l":[157.192,-8.082,157.201,-8.016,157.186,-7.941,157.146,-7.883,157.103,-7.855,157.024,-7.868,156.959,-7.938,156.958,-8.014,157.041,-8.117,157.15,-8.123,157.172,-8.108,157.192,-8.082]},{"b":[155.678,-7.121,155.865,-6.973],"l":[155.739,-6.973,155.705,-7.013,155.678,-7.089,155.739,-7.121,155.84,-7.097,155.865,-7.043,155.739,-6.973]},{"b":[162.921,5.277,162.993,5.335],"l":[162.921,5.318,162.93,5.301,162.993,5.277,162.983,5.326,162.958,5.335,162.921,5.318]},{"b":[158.128,6.791,158.335,6.978],"l":[158.186,6.978,158.135,6.945,158.128,6.905,158.161,6.883,158.183,6.801,158.257,6.791,158.315,6.814,158.309,6.855,158.335,6.893,158.295,6.951,158.186,6.978]},{"b":[175.337,-36.334,175.551,-36.071],"l":[175.513,-36.177,175.444,-36.115,175.409,-36.071,175.39,-36.078,175.382,-36.095,175.337,-36.135,175.346,-36.218,175.359,-36.231,175.445,-36.273,175.475,-36.314,175.551,-36.334,175.543,-36.279,175.513,-36.177]},{"b":[173.781,-40.922,173.964,-40.713],"l":[173.873,-40.749,173.812,-40.794,173.786,-40.881,173.781,-40.922,173.915,-40.864,173.958,-40.787,173.964,-40.713,173.903,-40.746,173.873,-40.749]},{"b":[166.532,-45.73,166.746,-45.615],"l":[166.567,-45.644,166.532,-45.7,166.559,-45.708,166.592,-45.702,166.642,-45.724,166.695,-45.73,166.729,-45.73,166.741,-45.705,166.746,-45.656,166.731,-45.639,166.686,-45.615,166.567,-45.644]},{"b":[-176.848,-44.117,-176.178,-43.718],"l":[-176.275,-43.765,-176.214,-43.766,-176.178,-43.74,-176.566,-43.718,-176.667,-43.765,-176.761,-43.758,-176.848,-43.824,-176.808,-43.835,-176.635,-43.82,-176.555,-43.852,-176.524,-43.901,-176.563,-43.954,-176.632,-44.006,-176.629,-44.036,-176.598,-44.107,-176.572,-44.115,-176.516,-44.117,-176.453,-44.077,-176.334,-44.048,-176.334,-44.025,-176.385,-43.951,-176.439,-43.955,-176.5,-43.86,-176.441,-43.816,-176.455,-43.805,-176.517,-43.785,-176.499,-43.768,-176.407,-43.761,-176.375,-43.791,-176.382,-43.867,-176.275,-43.765]},{"b":[-176.229,-44.331,-176.123,-44.225],"l":[-176.229,-44.237,-176.215,-44.274,-176.221,-44.331,-176.176,-44.322,-176.123,-44.268,-176.155,-44.225,-176.229,-44.237]},{"b":[169.022,-52.57,169.233,-52.485],"l":[169.079,-52.499,169.022,-52.495,169.04,-52.529,169.076,-52.552,169.128,-52.57,169.233,-52.548,169.178,-52.497,169.129,-52.485,169.079,-52.499]},{"b":[165.889,-50.846,166.267,-50.531],"l":[166.21,-50.612,166.259,-50.577,166.267,-50.559,166.254,-50.544,166.225,-50.531,166.101,-50.539,166.103,-50.573,166.074,-50.679,165.916,-50.763,165.889,-50.808,165.904,-50.821,165.971,-50.82,166.013,-50.778,166.038,-50.787,166.073,-50.823,166.188,-50.846,166.243,-50.846,166.221,-50.762,166.201,-50.751,166.179,-50.715,166.22,-50.694,166.208,-50.652,166.21,-50.612]},{"b":[167.522,-47.264,168.261,-46.694],"l":[168.156,-46.988,168.125,-46.956,168.043,-46.933,168.041,-46.888,168.145,-46.902,168.145,-46.862,167.956,-46.694,167.784,-46.7,167.765,-46.798,167.801,-46.907,167.742,-46.957,167.741,-47.014,167.654,-47.044,167.631,-47.088,167.629,-47.142,167.539,-47.199,167.522,-47.259,167.555,-47.264,167.676,-47.243,167.785,-47.176,167.811,-47.17,167.906,-47.18,168.015,-47.117,168.184,-47.102,168.241,-47.07,168.261,-47.027,168.241,-46.979,168.156,-46.988]},{"b":[136.943,-15.824,137.094,-15.595],"l":[137.065,-15.663,137.01,-15.595,136.986,-15.652,136.963,-15.666,136.943,-15.712,136.985,-15.726,136.996,-15.776,137.051,-15.824,137.094,-15.778,137.071,-15.738,137.065,-15.663]},{"b":[136.846,-15.627,136.89,-15.503],"l":[136.877,-15.503,136.846,-15.544,136.847,-15.627,136.863,-15.62,136.89,-15.589,136.877,-15.503]},{"b":[136.503,-15.632,136.612,-15.534],"l":[136.523,-15.543,136.503,-15.583,136.514,-15.627,136.531,-15.632,136.591,-15.628,136.612,-15.544,136.586,-15.534,136.523,-15.543]},{"b":[132.484,-11.318,132.629,-10.969],"l":[132.574,-11.318,132.593,-11.303,132.629,-11.169,132.597,-11.106,132.593,-10.998,132.579,-10.969,132.538,-11.028,132.484,-11.037,132.516,-11.116,132.494,-11.164,132.574,-11.318]},{"b":[148.939,-20.303,149.045,-20.154],"l":[149.004,-20.221,148.981,-20.154,148.939,-20.284,148.987,-20.302,149.02,-20.303,149.044,-20.292,149.045,-20.278,149.004,-20.221]},{"b":[148.887,-20.154,148.968,-20.044],"l":[148.968,-20.044,148.932,-20.069,148.906,-20.102,148.887,-20.144,148.913,-20.154,148.936,-20.15,148.956,-20.135,148.968,-20.044]},{"b":[146.099,-18.485,146.342,-18.226],"l":[146.278,-18.231,146.249,-18.226,146.231,-18.241,146.187,-18.255,146.099,-18.252,146.116,-18.292,146.191,-18.363,146.236,-18.451,146.299,-18.485,146.327,-18.449,146.342,-18.4,146.299,-18.326,146.278,-18.231]},{"b":[142.125,-10.762,142.275,-10.592],"l":[142.198,-10.592,142.131,-10.641,142.125,-10.668,142.137,-10.732,142.191,-10.762,142.275,-10.705,142.198,-10.592]},{"b":[142.195,-10.254,142.339,-10.14],"l":[142.219,-10.149,142.195,-10.199,142.216,-10.236,142.279,-10.254,142.339,-10.192,142.299,-10.14,142.219,-10.149]},{"b":[142.098,-10.181,142.192,-10.052],"l":[142.149,-10.052,142.098,-10.122,142.142,-10.181,142.168,-10.154,142.192,-10.085,142.149,-10.052]},{"b":[139.148,-16.742,139.698,-16.395],"l":[139.588,-16.395,139.459,-16.438,139.293,-16.467,139.229,-16.528,139.163,-16.626,139.148,-16.714,139.16,-16.742,139.239,-16.719,139.283,-16.719,139.354,-16.697,139.392,-16.649,139.431,-16.661,139.508,-16.573,139.56,-16.529,139.698,-16.515,139.604,-16.403,139.588,-16.395]},{"b":[139.408,-17.132,139.571,-16.99],"l":[139.493,-16.99,139.459,-17.049,139.408,-17.091,139.422,-17.132,139.459,-17.115,139.571,-17.094,139.56,-17.042,139.493,-16.99]},{"b":[136.335,-14.293,136.951,-13.676],"l":[136.885,-14.197,136.787,-14.158,136.75,-14.115,136.745,-14.073,136.788,-13.946,136.815,-13.907,136.843,-13.897,136.906,-13.827,136.891,-13.787,136.871,-13.764,136.845,-13.751,136.804,-13.842,136.758,-13.845,136.715,-13.804,136.696,-13.726,136.702,-13.682,136.656,-13.676,136.583,-13.721,136.534,-13.794,136.425,-13.865,136.411,-14.011,136.428,-14.126,136.392,-14.175,136.335,-14.212,136.363,-14.229,136.461,-14.235,136.65,-14.28,136.763,-14.273,136.894,-14.293,136.931,-14.246,136.951,-14.184,136.934,-14.179,136.885,-14.197]},{"b":[136.122,-13.836,136.275,-13.665],"l":[136.16,-13.737,136.134,-13.753,136.122,-13.781,136.123,-13.817,136.214,-13.836,136.237,-13.825,136.275,-13.791,136.257,-13.707,136.215,-13.665,136.16,-13.737]},{"b":[136.522,-11.439,136.78,-11.012],"l":[136.688,-11.178,136.649,-11.212,136.56,-11.358,136.522,-11.394,136.527,-11.439,136.599,-11.379,136.741,-11.195,136.78,-11.012,136.732,-11.025,136.727,-11.105,136.711,-11.158,136.688,-11.178]},{"b":[136.18,-11.677,136.479,-11.466],"l":[136.339,-11.602,136.379,-11.583,136.471,-11.509,136.479,-11.466,136.449,-11.487,136.267,-11.576,136.18,-11.677,136.339,-11.602]},{"b":[130.043,-11.836,130.606,-11.337],"l":[130.459,-11.679,130.433,-11.592,130.386,-11.51,130.377,-11.42,130.339,-11.337,130.295,-11.337,130.251,-11.361,130.153,-11.478,130.187,-11.541,130.198,-11.658,130.139,-11.697,130.072,-11.681,130.043,-11.787,130.077,-11.825,130.131,-11.825,130.317,-11.772,130.503,-11.836,130.606,-11.817,130.603,-11.773,130.58,-11.737,130.542,-11.703,130.459,-11.679]},{"b":[130.369,-11.926,131.539,-11.18],"l":[131.437,-11.313,131.321,-11.247,131.268,-11.19,131.217,-11.243,131.141,-11.263,131.023,-11.334,130.987,-11.34,130.913,-11.309,130.752,-11.384,130.619,-11.376,130.56,-11.306,130.519,-11.279,130.427,-11.183,130.403,-11.18,130.385,-11.192,130.369,-11.215,130.405,-11.305,130.423,-11.446,130.512,-11.618,130.645,-11.742,130.951,-11.926,131.292,-11.711,131.383,-11.583,131.459,-11.588,131.468,-11.51,131.539,-11.437,131.522,-11.415,131.473,-11.383,131.437,-11.313]},{"b":[125.091,-14.642,125.199,-14.456],"l":[125.117,-14.492,125.091,-14.592,125.135,-14.642,125.199,-14.579,125.194,-14.553,125.198,-14.475,125.16,-14.456,125.117,-14.492]},{"b":[124.483,-15.43,124.605,-15.267],"l":[124.504,-15.292,124.483,-15.34,124.524,-15.382,124.524,-15.421,124.56,-15.43,124.597,-15.402,124.605,-15.357,124.565,-15.311,124.551,-15.27,124.519,-15.267,124.504,-15.292]},{"b":[115.309,-20.866,115.458,-20.668],"l":[115.435,-20.668,115.354,-20.746,115.309,-20.811,115.318,-20.851,115.388,-20.866,115.446,-20.788,115.458,-20.716,115.435,-20.668]},{"b":[112.908,-26.095,113.183,-25.52],"l":[113.132,-25.883,113.096,-25.815,112.982,-25.52,112.947,-25.532,112.908,-25.57,112.964,-25.783,113.156,-26.095,113.183,-26.053,113.148,-25.974,113.132,-25.952,113.132,-25.883]},{"b":[147.105,-43.501,147.356,-43.279],"l":[147.313,-43.28,147.284,-43.279,147.234,-43.33,147.22,-43.371,147.198,-43.379,147.185,-43.408,147.163,-43.43,147.105,-43.413,147.105,-43.431,147.154,-43.5,147.231,-43.483,147.309,-43.501,147.356,-43.397,147.342,-43.346,147.313,-43.28]},{"b":[148.023,-42.719,148.17,-42.593],"l":[148.023,-42.64,148.031,-42.663,148.03,-42.715,148.048,-42.719,148.104,-42.71,148.101,-42.681,148.17,-42.652,148.143,-42.616,148.073,-42.593,148.023,-42.64]},{"b":[147.296,-43.241,147.435,-43.08],"l":[147.353,-43.08,147.327,-43.115,147.319,-43.145,147.296,-43.162,147.338,-43.183,147.349,-43.232,147.372,-43.241,147.435,-43.241,147.397,-43.118,147.353,-43.08]},{"b":[144.71,-40.589,144.791,-40.435],"l":[144.751,-40.47,144.71,-40.485,144.748,-40.589,144.784,-40.507,144.791,-40.44,144.783,-40.435,144.751,-40.47]},{"b":[143.839,-40.12,144.141,-39.58],"l":[144.121,-39.785,144.091,-39.638,144.001,-39.58,143.949,-39.584,143.939,-39.658,143.879,-39.7,143.862,-39.738,143.865,-39.824,143.839,-39.904,143.888,-39.984,143.876,-40.064,143.899,-40.12,143.928,-40.116,144.035,-40.078,144.112,-40.022,144.141,-39.954,144.106,-39.874,144.121,-39.785]},{"b":[148.01,-40.497,148.474,-40.307],"l":[148.326,-40.307,148.198,-40.358,148.059,-40.357,148.01,-40.381,148.02,-40.404,148.103,-40.452,148.214,-40.458,148.319,-40.435,148.353,-40.497,148.404,-40.487,148.474,-40.432,148.421,-40.367,148.326,-40.307]},{"b":[147.767,-40.262,148.323,-39.726],"l":[148.106,-40.262,148.21,-40.234,148.299,-40.172,148.314,-40.174,148.323,-40.144,148.251,-40.1,148.29,-40.065,148.297,-39.986,148.27,-39.967,148.178,-39.938,148,-39.758,147.933,-39.726,147.839,-39.832,147.767,-39.87,147.812,-39.91,147.876,-39.905,147.906,-39.971,147.891,-40.015,148.025,-40.172,148.047,-40.213,148.074,-40.241,148.106,-40.262]},{"b":[148.117,-40.593,148.237,-40.503],"l":[148.188,-40.593,148.237,-40.515,148.218,-40.505,148.193,-40.503,148.117,-40.521,148.127,-40.544,148.188,-40.593]},{"b":[136.541,-36.075,138.123,-35.592],"l":[138.047,-35.755,137.929,-35.726,137.836,-35.762,137.596,-35.739,137.598,-35.722,137.635,-35.656,137.585,-35.62,137.53,-35.605,137.334,-35.592,137.092,-35.664,136.639,-35.749,136.579,-35.809,136.541,-35.89,136.589,-35.935,136.755,-36.033,136.913,-36.047,137.026,-36.024,137.148,-36.039,137.21,-35.982,137.382,-36.021,137.448,-36.075,137.59,-36.027,137.622,-35.938,137.671,-35.898,137.836,-35.868,138.012,-35.908,138.067,-35.901,138.123,-35.852,138.047,-35.755]},{"b":[145.28,-38.421,145.487,-38.314],"l":[145.295,-38.319,145.286,-38.341,145.28,-38.391,145.336,-38.421,145.487,-38.355,145.427,-38.314,145.295,-38.319]},{"b":[145.128,-38.557,145.355,-38.459],"l":[145.218,-38.459,145.128,-38.528,145.271,-38.52,145.355,-38.557,145.349,-38.538,145.314,-38.491,145.288,-38.472,145.218,-38.459]},{"b":[153.396,-27.712,153.539,-27.405],"l":[153.401,-27.506,153.396,-27.665,153.427,-27.706,153.453,-27.712,153.539,-27.436,153.522,-27.422,153.435,-27.405,153.401,-27.506]},{"b":[153.365,-27.331,153.467,-27.03],"l":[153.467,-27.038,153.432,-27.03,153.38,-27.049,153.365,-27.139,153.377,-27.235,153.421,-27.331,153.442,-27.316,153.426,-27.201,153.467,-27.038]},{"b":[152.977,-25.778,153.359,-24.729],"l":[153.298,-24.915,153.282,-24.738,153.257,-24.729,153.223,-24.74,153.181,-24.765,153.144,-24.815,153.186,-24.833,153.242,-24.923,153.228,-25.006,153.189,-25.071,153.038,-25.193,153.061,-25.302,153.052,-25.354,152.999,-25.448,152.977,-25.551,153.007,-25.729,153.052,-25.778,153.077,-25.751,153.084,-25.683,153.141,-25.513,153.35,-25.063,153.359,-24.978,153.298,-24.915]},{"b":[151.033,-23.776,151.296,-23.461],"l":[151.229,-23.595,151.24,-23.53,151.212,-23.513,151.181,-23.516,151.147,-23.491,151.06,-23.461,151.033,-23.53,151.184,-23.741,151.238,-23.776,151.262,-23.762,151.296,-23.72,151.274,-23.668,151.229,-23.595]},{"b":[150.462,-22.325,150.549,-22.211],"l":[150.485,-22.268,150.462,-22.308,150.488,-22.325,150.517,-22.323,150.549,-22.307,150.521,-22.228,150.488,-22.211,150.485,-22.268]},{"b":[149.87,-22.223,149.928,-22.049],"l":[149.87,-22.15,149.894,-22.223,149.928,-22.193,149.928,-22.149,149.912,-22.049,149.875,-22.074,149.87,-22.15]},{"b":[117.272,8.191,117.355,8.315],"l":[117.281,8.315,117.272,8.254,117.287,8.191,117.355,8.215,117.354,8.289,117.33,8.308,117.281,8.315]},{"b":[116.97,7.808,117.08,8.069],"l":[116.994,8.051,116.976,8.017,116.97,7.895,117.028,7.808,117.08,7.883,117.077,8.069,116.994,8.051]},{"b":[119.764,10.455,120.008,10.64],"l":[119.852,10.64,119.764,10.552,119.793,10.455,119.916,10.486,119.981,10.539,120.008,10.57,119.95,10.605,119.852,10.64]},{"b":[119.726,11.376,119.883,11.525],"l":[119.761,11.474,119.726,11.475,119.73,11.432,119.799,11.409,119.831,11.376,119.855,11.393,119.883,11.472,119.861,11.525,119.827,11.515,119.761,11.474]},{"b":[119.866,11.994,120.341,12.319],"l":[120.228,12.22,120.194,12.167,120.155,12.152,120.1,12.168,120.078,12.198,119.964,12.27,119.916,12.319,119.897,12.313,119.886,12.3,119.88,12.28,119.892,12.273,119.87,12.244,119.866,12.199,119.896,12.179,119.957,12.069,120.011,12.008,120.1,11.994,120.174,12.02,120.243,12.005,120.315,12.012,120.341,12.077,120.261,12.142,120.228,12.22]},{"b":[119.861,11.669,120.073,11.981],"l":[119.957,11.96,119.916,11.981,119.861,11.954,119.933,11.774,119.932,11.74,119.945,11.691,119.964,11.669,120.039,11.703,120.073,11.783,120.062,11.821,120.071,11.861,120.036,11.917,119.998,11.932,119.957,11.96]},{"b":[122.517,10.425,122.737,10.741],"l":[122.626,10.695,122.538,10.608,122.517,10.493,122.538,10.425,122.597,10.461,122.622,10.459,122.65,10.473,122.681,10.498,122.737,10.655,122.729,10.706,122.701,10.741,122.673,10.739,122.648,10.723,122.626,10.695]},{"b":[123.316,9.423,124.058,11.274],"l":[124.053,11.029,124.04,11.054,124.037,11.107,124.058,11.217,124.039,11.274,123.967,11.187,123.964,11.137,123.95,11.079,123.925,11.041,123.93,10.964,123.832,10.731,123.726,10.562,123.711,10.474,123.593,10.303,123.514,10.14,123.386,9.967,123.404,9.889,123.327,9.578,123.316,9.489,123.332,9.423,123.37,9.45,123.494,9.589,123.634,9.922,123.643,10.02,123.7,10.128,123.789,10.221,123.874,10.258,123.952,10.317,124.005,10.4,124.051,10.586,124.028,10.768,124.053,10.926,124.053,11.029]},{"b":[123.708,11.151,123.816,11.283],"l":[123.757,11.283,123.741,11.279,123.708,11.248,123.737,11.151,123.816,11.151,123.757,11.283]},{"b":[123.817,9.599,124.594,10.16],"l":[124.486,10.065,124.406,10.126,124.373,10.13,124.352,10.141,124.336,10.16,124.173,10.135,124.094,10.061,124.06,10,123.909,9.92,123.864,9.879,123.817,9.817,123.83,9.761,123.872,9.676,123.936,9.624,124.122,9.599,124.36,9.63,124.403,9.654,124.478,9.748,124.506,9.754,124.584,9.75,124.594,9.787,124.582,9.83,124.555,9.879,124.577,10.027,124.486,10.065]},{"b":[124.639,9.083,124.807,9.243],"l":[124.737,9.243,124.708,9.243,124.653,9.226,124.639,9.175,124.666,9.132,124.778,9.083,124.807,9.143,124.79,9.19,124.737,9.243]},{"b":[123.493,9.103,123.706,9.279],"l":[123.698,9.237,123.655,9.279,123.626,9.268,123.535,9.214,123.494,9.216,123.493,9.192,123.541,9.13,123.614,9.103,123.706,9.134,123.698,9.237]},{"b":[119.821,5.06,120.25,5.34],"l":[120.208,5.34,120.165,5.332,120.08,5.264,119.983,5.228,119.827,5.133,119.821,5.07,119.878,5.06,119.958,5.08,120.013,5.151,120.101,5.169,120.118,5.215,120.15,5.184,120.192,5.168,120.223,5.196,120.25,5.257,120.229,5.284,120.208,5.34]},{"b":[121.809,6.415,122.324,6.741],"l":[122.058,6.741,121.915,6.676,121.832,6.664,121.809,6.614,121.872,6.563,121.88,6.518,121.959,6.416,121.991,6.415,122.093,6.428,122.201,6.483,122.252,6.58,122.324,6.602,122.288,6.639,122.058,6.741]},{"b":[125.991,9.761,126.173,10.059],"l":[126.129,9.944,126.074,10.059,125.999,9.927,125.991,9.839,126.047,9.761,126.059,9.766,126.137,9.768,126.173,9.8,126.121,9.865,126.129,9.891,126.129,9.944]},{"b":[125.495,9.886,125.703,10.44],"l":[125.703,10.072,125.647,10.245,125.685,10.392,125.667,10.44,125.648,10.437,125.606,10.38,125.58,10.364,125.525,10.31,125.522,10.192,125.495,10.119,125.534,10.09,125.591,9.998,125.649,9.944,125.673,9.886,125.69,9.914,125.692,9.939,125.684,9.963,125.703,10.072]},{"b":[120.099,13.683,120.273,13.858],"l":[120.211,13.821,120.121,13.858,120.103,13.843,120.099,13.817,120.104,13.782,120.273,13.683,120.271,13.751,120.211,13.821]},{"b":[121.815,13.205,122.122,13.566],"l":[121.996,13.547,121.977,13.537,121.915,13.54,121.866,13.566,121.815,13.424,121.829,13.329,121.876,13.282,122.005,13.205,122.042,13.236,122.055,13.269,122.122,13.365,122.107,13.421,122.115,13.463,121.996,13.547]},{"b":[121.923,12.106,122.145,12.653],"l":[122.002,12.599,121.989,12.435,121.941,12.385,121.923,12.331,121.936,12.29,121.982,12.245,121.96,12.191,122.014,12.106,122.094,12.355,122.132,12.538,122.13,12.613,122.145,12.653,122.104,12.651,122.002,12.599]},{"b":[122.248,12.498,122.311,12.593],"l":[122.248,12.557,122.261,12.503,122.28,12.498,122.311,12.529,122.288,12.589,122.278,12.593,122.248,12.557]},{"b":[122.423,12.286,122.683,12.492],"l":[122.604,12.492,122.472,12.492,122.423,12.455,122.439,12.429,122.499,12.384,122.603,12.286,122.654,12.309,122.683,12.382,122.674,12.424,122.604,12.492]},{"b":[122.949,12.701,123.367,13.116],"l":[123.044,13.113,123.017,13.116,122.958,13.107,122.949,13.059,122.973,13.035,123.054,12.993,123.166,12.876,123.274,12.805,123.367,12.701,123.282,12.853,123.206,12.905,123.044,13.113]},{"b":[123.587,12.366,123.779,12.675],"l":[123.775,12.454,123.709,12.611,123.621,12.675,123.587,12.633,123.621,12.571,123.742,12.399,123.779,12.366,123.775,12.454]},{"b":[123.156,11.752,124.056,12.583],"l":[123.211,12.107,123.156,11.968,123.158,11.926,123.293,12.036,123.419,12.194,123.474,12.217,123.531,12.197,123.612,12.09,123.668,12.069,123.675,12.05,123.736,12.003,123.725,11.952,123.754,11.934,123.848,11.914,123.983,11.819,124.046,11.752,124.056,11.812,124.04,11.967,123.908,12.169,123.717,12.287,123.575,12.407,123.559,12.445,123.463,12.501,123.337,12.542,123.236,12.583,123.24,12.495,123.267,12.395,123.245,12.328,123.211,12.107]},{"b":[124.039,13.532,124.417,14.078],"l":[124.337,13.931,124.308,13.947,124.225,14.078,124.186,14.06,124.154,14.026,124.123,13.98,124.124,13.79,124.039,13.663,124.057,13.606,124.175,13.532,124.248,13.587,124.295,13.59,124.327,13.567,124.354,13.632,124.404,13.679,124.396,13.75,124.417,13.871,124.337,13.931]},{"b":[121.946,14.008,122.175,14.229],"l":[121.959,14.229,121.946,14.205,121.946,14.181,121.956,14.156,122.172,14.008,122.175,14.049,121.959,14.229]},{"b":[121.82,14.656,122.052,15.046],"l":[121.84,15.038,121.82,14.964,121.862,14.917,121.889,14.84,121.923,14.8,121.935,14.737,121.922,14.715,121.911,14.667,121.933,14.656,121.99,14.662,122.022,14.759,121.97,14.893,122.017,14.965,122.032,14.972,122.052,14.97,122.033,15.005,121.972,15.046,121.84,15.038]},{"b":[121.825,18.823,121.988,19.01],"l":[121.889,18.992,121.86,18.937,121.861,18.913,121.825,18.843,121.858,18.823,121.922,18.895,121.988,18.957,121.943,19.01,121.889,18.992]},{"b":[121.185,19.015,121.252,19.184],"l":[121.196,19.051,121.247,19.015,121.252,19.082,121.245,19.143,121.213,19.184,121.19,19.139,121.185,19.101,121.196,19.051]},{"b":[121.375,19.271,121.531,19.399],"l":[121.392,19.399,121.376,19.38,121.375,19.356,121.383,19.328,121.472,19.273,121.531,19.271,121.521,19.362,121.392,19.399]},{"b":[121.914,20.354,122.031,20.48],"l":[121.914,20.359,121.941,20.354,121.96,20.366,122.031,20.469,121.991,20.48,121.941,20.454,121.914,20.359]},{"b":[121.791,20.7,121.878,20.841],"l":[121.796,20.747,121.791,20.701,121.83,20.7,121.878,20.782,121.867,20.839,121.848,20.841,121.796,20.747]},{"b":[147.781,-5.834,148.076,-5.491],"l":[147.846,-5.491,147.795,-5.492,147.783,-5.522,147.781,-5.627,147.875,-5.749,147.968,-5.789,147.985,-5.834,148.026,-5.826,148.06,-5.765,148.076,-5.65,148.055,-5.612,147.846,-5.491]},{"b":[147.006,-5.437,147.222,-5.191],"l":[147.131,-5.191,147.015,-5.257,147.006,-5.307,147.029,-5.342,147.12,-5.437,147.176,-5.432,147.222,-5.382,147.206,-5.252,147.131,-5.191]},{"b":[145.884,-4.756,146.053,-4.539],"l":[145.9,-4.604,145.884,-4.667,145.904,-4.733,145.952,-4.756,146.019,-4.726,146.053,-4.64,146.037,-4.573,145.996,-4.539,145.959,-4.554,145.9,-4.604]},{"b":[147.736,-2.336,147.877,-2.247],"l":[147.79,-2.306,147.736,-2.316,147.769,-2.331,147.845,-2.336,147.877,-2.283,147.836,-2.247,147.812,-2.262,147.79,-2.306]},{"b":[146.531,-2.21,147.444,-1.949],"l":[147.401,-2.025,147.068,-1.96,146.857,-1.949,146.76,-1.978,146.656,-1.974,146.596,-2.017,146.607,-2.103,146.532,-2.126,146.531,-2.154,146.546,-2.209,146.572,-2.21,146.635,-2.173,146.699,-2.183,146.748,-2.149,146.926,-2.189,147.064,-2.187,147.142,-2.167,147.206,-2.182,147.301,-2.09,147.337,-2.066,147.385,-2.071,147.438,-2.059,147.444,-2.012,147.424,-1.995,147.419,-2.001,147.423,-2.024,147.401,-2.025]},{"b":[154.54,-5.441,154.727,-5.014],"l":[154.682,-5.054,154.633,-5.014,154.606,-5.035,154.54,-5.111,154.563,-5.152,154.576,-5.221,154.584,-5.314,154.627,-5.441,154.647,-5.433,154.698,-5.383,154.727,-5.218,154.689,-5.143,154.682,-5.054]},{"b":[152.543,-3.221,152.671,-3.043],"l":[152.639,-3.043,152.57,-3.062,152.543,-3.096,152.585,-3.17,152.646,-3.221,152.671,-3.133,152.639,-3.043]},{"b":[151.955,-2.998,152.099,-2.846],"l":[151.975,-2.846,151.955,-2.871,151.971,-2.896,152.057,-2.995,152.088,-2.998,152.099,-2.947,152.075,-2.918,151.975,-2.846]},{"b":[151.93,-2.83,152.011,-2.709],"l":[152.002,-2.738,151.946,-2.709,151.93,-2.751,151.933,-2.83,151.957,-2.83,152.011,-2.809,152.002,-2.738]},{"b":[149.546,-1.589,149.765,-1.353],"l":[149.633,-1.362,149.581,-1.353,149.548,-1.408,149.546,-1.472,149.671,-1.576,149.691,-1.571,149.763,-1.589,149.765,-1.553,149.725,-1.431,149.633,-1.362]},{"b":[149.962,-2.675,150.452,-2.384],"l":[150.429,-2.47,150.227,-2.384,150.103,-2.405,149.962,-2.474,149.985,-2.492,150.043,-2.512,150.102,-2.603,150.166,-2.66,150.238,-2.675,150.437,-2.662,150.446,-2.632,150.452,-2.541,150.45,-2.513,150.429,-2.47]},{"b":[150.776,-10.195,151.296,-9.709],"l":[151.123,-10.02,151.081,-10.02,151.044,-9.983,151.051,-9.939,150.862,-9.802,150.817,-9.736,150.776,-9.709,150.79,-9.774,150.861,-9.876,150.896,-9.968,150.952,-9.998,150.959,-10.093,151.175,-10.159,151.231,-10.195,151.296,-9.957,151.256,-9.923,151.194,-9.946,151.123,-10.02]},{"b":[150.11,-9.519,150.368,-9.206],"l":[150.32,-9.264,150.208,-9.206,150.135,-9.26,150.11,-9.362,150.273,-9.5,150.331,-9.519,150.345,-9.494,150.368,-9.396,150.357,-9.349,150.32,-9.264]},{"b":[150.431,-9.703,150.899,-9.346],"l":[150.848,-9.663,150.844,-9.703,150.894,-9.667,150.899,-9.641,150.884,-9.582,150.879,-9.513,150.789,-9.418,150.746,-9.404,150.669,-9.429,150.528,-9.347,150.499,-9.346,150.437,-9.36,150.431,-9.387,150.435,-9.435,150.508,-9.536,150.495,-9.562,150.436,-9.625,150.576,-9.631,150.678,-9.657,150.81,-9.655,150.848,-9.663]},{"b":[151.005,-8.805,151.139,-8.419],"l":[151.139,-8.568,151.116,-8.522,151.118,-8.419,151.09,-8.426,151.046,-8.451,151.005,-8.524,151.083,-8.569,151.087,-8.595,151.081,-8.642,151.046,-8.728,151.124,-8.805,151.107,-8.733,151.139,-8.568]},{"b":[152.515,-9.224,152.995,-8.959],"l":[152.85,-9.025,152.81,-8.967,152.689,-8.975,152.631,-8.959,152.577,-8.97,152.515,-9.01,152.638,-9.058,152.708,-9.126,152.72,-9.167,152.759,-9.177,152.867,-9.224,152.923,-9.203,152.967,-9.209,152.959,-9.169,152.985,-9.151,152.995,-9.131,152.995,-9.108,152.953,-9.07,152.905,-9.044,152.85,-9.025]},{"b":[150.786,-10.643,150.899,-10.552],"l":[150.799,-10.554,150.786,-10.603,150.802,-10.62,150.885,-10.643,150.899,-10.565,150.872,-10.552,150.799,-10.554]},{"b":[154.023,-11.426,154.281,-11.311],"l":[154.102,-11.311,154.118,-11.366,154.023,-11.348,154.031,-11.371,154.064,-11.419,154.121,-11.426,154.23,-11.397,154.266,-11.416,154.281,-11.361,154.238,-11.339,154.102,-11.311]},{"b":[153.204,-11.631,153.76,-11.324],"l":[153.703,-11.529,153.536,-11.476,153.307,-11.356,153.204,-11.324,153.207,-11.352,153.234,-11.42,153.322,-11.471,153.287,-11.517,153.357,-11.495,153.379,-11.56,153.519,-11.595,153.554,-11.631,153.7,-11.613,153.76,-11.586,153.703,-11.529]},{"b":[143.322,-8.485,143.593,-8.368],"l":[143.543,-8.485,143.587,-8.482,143.593,-8.46,143.581,-8.391,143.528,-8.379,143.322,-8.368,143.367,-8.417,143.543,-8.485]},{"b":[143.207,-8.677,143.608,-8.423],"l":[143.443,-8.519,143.293,-8.473,143.207,-8.423,143.254,-8.49,143.324,-8.517,143.463,-8.617,143.608,-8.677,143.59,-8.633,143.443,-8.519]},{"b":[135.383,-1.217,136.375,-0.651],"l":[136.283,-1.065,136.155,-0.978,136.069,-0.878,135.894,-0.726,135.841,-0.712,135.749,-0.733,135.673,-0.688,135.596,-0.69,135.383,-0.651,135.388,-0.705,135.432,-0.769,135.483,-0.801,135.491,-0.785,135.524,-0.787,135.646,-0.882,135.747,-0.823,135.826,-1.028,135.839,-1.119,135.915,-1.178,136.003,-1.17,136.11,-1.217,136.165,-1.215,136.305,-1.173,136.375,-1.094,136.283,-1.065]},{"b":[135.47,-1.894,136.893,-1.592],"l":[136.719,-1.734,136.39,-1.722,136.202,-1.655,135.976,-1.636,135.869,-1.642,135.474,-1.592,135.47,-1.616,135.488,-1.668,135.866,-1.752,136.049,-1.824,136.193,-1.859,136.228,-1.894,136.326,-1.872,136.461,-1.89,136.622,-1.873,136.709,-1.838,136.893,-1.8,136.817,-1.754,136.719,-1.734]},{"b":[134.809,-1.134,134.996,-0.938],"l":[134.957,-1.031,134.941,-0.979,134.889,-0.938,134.828,-0.979,134.809,-1.038,134.862,-1.114,134.917,-1.134,134.965,-1.116,134.996,-1.034,134.957,-1.031]},{"b":[134.335,-2.139,134.419,-2.028],"l":[134.351,-2.037,134.335,-2.095,134.345,-2.139,134.374,-2.124,134.419,-2.052,134.391,-2.031,134.37,-2.028,134.351,-2.037]},{"b":[138.563,-8.402,138.898,-8.173],"l":[138.796,-8.174,138.763,-8.173,138.677,-8.199,138.621,-8.268,138.563,-8.309,138.567,-8.33,138.594,-8.371,138.846,-8.402,138.895,-8.389,138.898,-8.337,138.796,-8.174]},{"b":[133.321,-4.299,133.622,-4.111],"l":[133.464,-4.2,133.321,-4.111,133.333,-4.17,133.503,-4.257,133.622,-4.299,133.571,-4.246,133.464,-4.2]},{"b":[129.738,-2.064,130.425,-1.691],"l":[130.425,-1.805,130.365,-1.75,130.353,-1.691,130.318,-1.692,130.2,-1.732,130.106,-1.73,129.994,-1.759,129.738,-1.867,129.754,-1.894,129.887,-1.986,130.093,-2.028,130.133,-2.064,130.248,-2.048,130.284,-2.009,130.339,-1.982,130.373,-1.992,130.419,-1.971,130.393,-1.942,130.381,-1.903,130.404,-1.89,130.425,-1.805]},{"b":[130.668,-1.343,131.074,-0.89],"l":[131.033,-0.918,130.939,-0.915,130.897,-0.89,130.673,-0.96,130.668,-0.984,130.704,-1.05,130.712,-1.104,130.739,-1.173,130.782,-1.255,130.845,-1.317,130.967,-1.343,131.002,-1.316,131.046,-1.188,131.074,-0.968,131.033,-0.918]},{"b":[130.402,-0.924,130.905,-0.765],"l":[130.807,-0.765,130.813,-0.814,130.723,-0.822,130.635,-0.812,130.594,-0.827,130.57,-0.822,130.548,-0.826,130.527,-0.837,130.484,-0.833,130.457,-0.851,130.439,-0.887,130.402,-0.924,130.832,-0.863,130.88,-0.828,130.905,-0.777,130.807,-0.765]},{"b":[130.465,-0.53,130.684,-0.417],"l":[130.616,-0.417,130.597,-0.418,130.564,-0.441,130.526,-0.449,130.465,-0.487,130.569,-0.53,130.627,-0.529,130.684,-0.469,130.657,-0.437,130.616,-0.417]},{"b":[130.237,-0.444,131.34,-0.004],"l":[131.277,-0.15,131.026,-0.04,130.987,-0.047,130.813,-0.004,130.722,-0.03,130.584,-0.045,130.548,-0.07,130.5,-0.06,130.431,-0.098,130.363,-0.073,130.295,-0.101,130.288,-0.155,130.237,-0.21,130.341,-0.262,130.496,-0.267,130.551,-0.366,130.575,-0.362,130.607,-0.329,130.689,-0.297,130.7,-0.392,130.75,-0.444,130.896,-0.416,130.899,-0.344,130.843,-0.298,130.802,-0.302,130.761,-0.291,130.691,-0.181,130.638,-0.143,130.622,-0.086,130.683,-0.081,130.808,-0.226,130.897,-0.268,130.946,-0.338,131.005,-0.361,131.098,-0.33,131.178,-0.346,131.218,-0.374,131.258,-0.366,131.34,-0.29,131.303,-0.241,131.317,-0.204,131.277,-0.15]},{"b":[129.309,-0.19,129.549,0.045],"l":[129.309,0.045,129.37,-0.066,129.469,-0.131,129.506,-0.19,129.549,-0.187,129.542,-0.139,129.309,0.045]},{"b":[128.218,2.035,128.688,2.598],"l":[128.602,2.598,128.569,2.596,128.472,2.571,128.33,2.469,128.218,2.297,128.26,2.083,128.296,2.035,128.454,2.052,128.548,2.097,128.623,2.224,128.688,2.474,128.602,2.598]},{"b":[127.397,-0.037,127.453,0.143],"l":[127.431,0.143,127.42,0.124,127.397,0.017,127.418,0.006,127.449,-0.037,127.453,-0.006,127.449,0.069,127.431,0.143]},{"b":[127.363,0.631,127.446,0.744],"l":[127.42,0.642,127.446,0.683,127.443,0.733,127.425,0.744,127.383,0.744,127.363,0.675,127.374,0.635,127.384,0.631,127.42,0.642]},{"b":[127.104,-0.521,127.29,-0.256],"l":[127.281,-0.391,127.253,-0.319,127.29,-0.284,127.19,-0.256,127.126,-0.279,127.104,-0.414,127.119,-0.521,127.187,-0.521,127.25,-0.495,127.281,-0.391]},{"b":[127.156,-0.802,127.301,-0.619],"l":[127.258,-0.623,127.209,-0.619,127.156,-0.761,127.185,-0.775,127.289,-0.802,127.3,-0.781,127.301,-0.758,127.258,-0.623]},{"b":[127.297,-0.884,127.88,-0.307],"l":[127.804,-0.694,127.659,-0.689,127.605,-0.61,127.682,-0.468,127.567,-0.319,127.527,-0.307,127.492,-0.336,127.455,-0.406,127.371,-0.332,127.325,-0.336,127.329,-0.391,127.297,-0.46,127.3,-0.5,127.381,-0.6,127.469,-0.643,127.438,-0.739,127.463,-0.806,127.498,-0.802,127.624,-0.766,127.643,-0.784,127.668,-0.832,127.761,-0.884,127.842,-0.848,127.88,-0.809,127.863,-0.76,127.838,-0.724,127.804,-0.694]},{"b":[127.392,-1.729,128.153,-1.332],"l":[127.905,-1.439,127.743,-1.36,127.647,-1.332,127.592,-1.351,127.457,-1.454,127.395,-1.59,127.392,-1.645,127.458,-1.697,127.562,-1.729,127.741,-1.691,127.914,-1.685,128.061,-1.712,128.092,-1.701,128.153,-1.661,128.149,-1.604,128.033,-1.532,127.905,-1.439]},{"b":[134.153,-6.365,134.758,-5.427],"l":[134.744,-6.202,134.756,-6.171,134.758,-6.1,134.752,-6.05,134.712,-5.95,134.755,-5.883,134.738,-5.817,134.739,-5.746,134.747,-5.707,134.701,-5.603,134.646,-5.581,134.658,-5.539,134.646,-5.492,134.617,-5.439,134.571,-5.427,134.506,-5.438,134.49,-5.525,134.456,-5.558,134.341,-5.713,134.247,-5.682,134.205,-5.707,134.226,-5.744,134.343,-5.833,134.299,-5.971,134.302,-6.01,134.225,-6.008,134.153,-6.02,134.155,-6.063,134.175,-6.09,134.264,-6.172,134.28,-6.201,134.356,-6.271,134.441,-6.335,134.638,-6.365,134.661,-6.337,134.684,-6.328,134.714,-6.295,134.744,-6.202]},{"b":[134.059,-6.909,134.537,-6.176],"l":[134.52,-6.513,134.537,-6.442,134.415,-6.387,134.318,-6.316,134.234,-6.226,134.168,-6.176,134.115,-6.191,134.111,-6.255,134.125,-6.426,134.195,-6.46,134.185,-6.479,134.154,-6.481,134.107,-6.472,134.059,-6.769,134.091,-6.834,134.2,-6.909,134.323,-6.849,134.356,-6.815,134.413,-6.68,134.504,-6.591,134.52,-6.513]},{"b":[132.845,-5.988,133.173,-5.311],"l":[133.009,-5.621,132.938,-5.683,132.922,-5.785,132.845,-5.988,132.926,-5.902,132.971,-5.736,133.12,-5.576,133.173,-5.348,133.138,-5.318,133.115,-5.311,133.009,-5.621]},{"b":[132.63,-5.947,132.807,-5.607],"l":[132.738,-5.662,132.717,-5.648,132.698,-5.609,132.63,-5.607,132.681,-5.739,132.667,-5.856,132.681,-5.913,132.705,-5.913,132.746,-5.947,132.807,-5.851,132.804,-5.789,132.738,-5.662]},{"b":[128.585,-3.603,128.722,-3.495],"l":[128.667,-3.517,128.595,-3.495,128.585,-3.512,128.62,-3.589,128.659,-3.588,128.713,-3.603,128.72,-3.589,128.722,-3.547,128.694,-3.525,128.667,-3.517]},{"b":[128.392,-3.638,128.563,-3.515],"l":[128.452,-3.515,128.428,-3.54,128.392,-3.638,128.563,-3.585,128.536,-3.541,128.452,-3.515]},{"b":[127.925,-3.771,128.329,-3.512],"l":[128.264,-3.512,128.119,-3.587,128.016,-3.601,127.928,-3.679,127.925,-3.699,127.934,-3.743,127.978,-3.771,128.052,-3.715,128.111,-3.686,128.147,-3.677,128.159,-3.698,128.143,-3.733,128.192,-3.735,128.25,-3.711,128.276,-3.675,128.277,-3.633,128.291,-3.598,128.314,-3.564,128.329,-3.516,128.264,-3.512]},{"b":[122.585,-5.671,123.203,-4.387],"l":[123.051,-5.156,123.025,-5.162,122.981,-5.186,122.972,-5.138,122.987,-4.963,123.015,-4.91,123.018,-4.832,123.055,-4.748,123.084,-4.749,123.104,-4.74,123.119,-4.723,123.139,-4.74,123.196,-4.823,123.203,-4.766,123.18,-4.551,123.069,-4.434,123.075,-4.387,123.038,-4.395,122.947,-4.443,122.853,-4.618,122.849,-4.831,122.821,-4.944,122.804,-5,122.794,-5.052,122.768,-5.177,122.767,-5.21,122.731,-5.262,122.67,-5.331,122.643,-5.381,122.642,-5.426,122.586,-5.489,122.585,-5.545,122.645,-5.663,122.684,-5.666,122.733,-5.635,122.812,-5.671,122.85,-5.638,122.916,-5.519,122.909,-5.477,122.935,-5.437,122.969,-5.406,122.986,-5.394,123.043,-5.419,123.121,-5.393,123.187,-5.333,123.202,-5.273,123.15,-5.224,123.051,-5.156]},{"b":[122.283,-5.391,122.76,-4.619],"l":[122.74,-4.675,122.702,-4.619,122.66,-4.634,122.524,-4.707,122.369,-4.767,122.334,-4.847,122.39,-4.999,122.396,-5.07,122.329,-5.138,122.283,-5.32,122.307,-5.381,122.371,-5.383,122.392,-5.335,122.474,-5.381,122.52,-5.391,122.564,-5.387,122.619,-5.336,122.645,-5.269,122.614,-5.139,122.76,-4.934,122.74,-4.675]},{"b":[121.808,-5.465,122.062,-5.072],"l":[122.041,-5.159,122,-5.141,121.966,-5.076,121.914,-5.072,121.866,-5.096,121.874,-5.145,121.857,-5.156,121.821,-5.203,121.808,-5.256,121.859,-5.35,121.98,-5.465,122.043,-5.438,122.062,-5.221,122.041,-5.159]},{"b":[122.969,-4.233,123.247,-3.981],"l":[122.969,-4.03,122.971,-4.061,122.995,-4.148,123.076,-4.227,123.145,-4.233,123.242,-4.113,123.247,-4.041,123.212,-3.998,123.025,-3.981,122.969,-4.03]},{"b":[125.863,-2.469,126.066,-1.975],"l":[125.976,-2.168,125.993,-2.012,125.963,-1.976,125.923,-1.975,125.873,-2.036,125.863,-2.077,125.903,-2.222,125.938,-2.263,125.978,-2.415,126.038,-2.469,126.055,-2.451,126.066,-2.366,125.976,-2.168]},{"b":[125.387,-1.94,126.332,-1.79],"l":[126.024,-1.79,125.72,-1.814,125.521,-1.801,125.445,-1.809,125.387,-1.843,125.426,-1.882,125.433,-1.938,125.479,-1.94,125.839,-1.906,125.956,-1.917,126.288,-1.859,126.332,-1.823,126.024,-1.79]},{"b":[124.33,-2.007,125.32,-1.636],"l":[125.188,-1.713,125.146,-1.693,125.127,-1.699,125.096,-1.741,125.063,-1.741,124.97,-1.705,124.664,-1.636,124.483,-1.644,124.418,-1.659,124.381,-1.687,124.33,-1.859,124.418,-2.005,124.521,-2.007,124.639,-1.978,124.834,-1.894,125.007,-1.943,125.135,-1.889,125.314,-1.877,125.32,-1.81,125.305,-1.794,125.258,-1.771,125.198,-1.78,125.188,-1.713]},{"b":[123.777,-1.996,123.866,-1.878],"l":[123.783,-1.878,123.777,-1.919,123.804,-1.994,123.866,-1.996,123.848,-1.955,123.783,-1.878]},{"b":[122.811,-1.616,123.547,-1.158],"l":[123.435,-1.237,123.339,-1.254,123.238,-1.389,123.198,-1.288,123.234,-1.234,123.212,-1.171,123.158,-1.158,122.972,-1.189,122.908,-1.182,122.832,-1.283,122.811,-1.432,122.858,-1.548,122.89,-1.587,122.984,-1.511,123.105,-1.34,123.15,-1.304,123.183,-1.493,123.123,-1.556,123.13,-1.577,123.173,-1.616,123.221,-1.598,123.237,-1.577,123.275,-1.437,123.329,-1.443,123.367,-1.507,123.449,-1.499,123.512,-1.447,123.547,-1.337,123.527,-1.286,123.489,-1.259,123.435,-1.237]},{"b":[123.071,-1.899,123.153,-1.773],"l":[123.071,-1.855,123.079,-1.899,123.153,-1.817,123.138,-1.773,123.106,-1.787,123.086,-1.815,123.071,-1.855]},{"b":[121.655,-0.526,121.907,-0.407],"l":[121.797,-0.418,121.749,-0.407,121.672,-0.479,121.655,-0.526,121.681,-0.525,121.722,-0.495,121.756,-0.491,121.847,-0.49,121.881,-0.503,121.907,-0.451,121.864,-0.407,121.797,-0.418]},{"b":[126.704,4.013,126.921,4.548],"l":[126.865,4.48,126.813,4.537,126.757,4.548,126.721,4.416,126.722,4.344,126.767,4.283,126.814,4.258,126.77,4.162,126.704,4.071,126.711,4.02,126.776,4.013,126.817,4.033,126.848,4.18,126.921,4.291,126.887,4.373,126.865,4.48]},{"b":[126.638,3.833,126.74,4.042],"l":[126.638,4.042,126.661,3.928,126.722,3.833,126.719,3.875,126.74,3.918,126.686,4.001,126.638,4.042]},{"b":[126.778,3.757,126.858,3.858],"l":[126.8,3.784,126.836,3.757,126.852,3.768,126.858,3.787,126.857,3.812,126.804,3.858,126.779,3.843,126.778,3.813,126.8,3.784]},{"b":[125.455,3.405,125.658,3.733],"l":[125.586,3.571,125.543,3.67,125.469,3.733,125.455,3.684,125.469,3.639,125.501,3.593,125.518,3.55,125.512,3.461,125.633,3.405,125.658,3.436,125.644,3.477,125.586,3.571]},{"b":[125.36,2.63,125.446,2.805],"l":[125.391,2.805,125.36,2.747,125.397,2.63,125.407,2.652,125.404,2.707,125.446,2.763,125.435,2.784,125.391,2.805]},{"b":[120.436,-6.465,120.549,-5.775],"l":[120.477,-5.775,120.446,-5.876,120.452,-6.095,120.436,-6.18,120.461,-6.254,120.468,-6.406,120.487,-6.465,120.528,-6.298,120.549,-5.969,120.534,-5.904,120.477,-5.775]},{"b":[131.751,-7.251,131.982,-7.104],"l":[131.922,-7.104,131.751,-7.117,131.778,-7.144,131.823,-7.159,131.884,-7.167,131.927,-7.225,131.97,-7.251,131.982,-7.202,131.922,-7.104]},{"b":[130.775,-8.35,131.176,-8.091],"l":[131.02,-8.091,130.833,-8.271,130.775,-8.35,130.862,-8.319,130.908,-8.246,131.044,-8.212,131.176,-8.131,131.087,-8.125,131.02,-8.091]},{"b":[131.087,-8.011,131.736,-7.113],"l":[131.701,-7.14,131.643,-7.113,131.561,-7.136,131.531,-7.165,131.535,-7.221,131.483,-7.251,131.446,-7.315,131.411,-7.34,131.349,-7.425,131.297,-7.438,131.26,-7.471,131.197,-7.617,131.19,-7.672,131.138,-7.685,131.137,-7.782,131.087,-7.865,131.123,-7.922,131.114,-7.997,131.185,-7.998,131.309,-8.011,131.326,-8,131.343,-7.981,131.348,-7.948,131.377,-7.869,131.474,-7.777,131.498,-7.731,131.58,-7.682,131.624,-7.626,131.691,-7.439,131.644,-7.267,131.736,-7.197,131.701,-7.14]},{"b":[129.592,-8.046,129.844,-7.795],"l":[129.655,-7.795,129.609,-7.803,129.599,-7.831,129.592,-7.917,129.713,-8.041,129.78,-8.046,129.839,-7.955,129.844,-7.889,129.813,-7.82,129.655,-7.795]},{"b":[128.53,-7.209,128.673,-7.069],"l":[128.628,-7.069,128.577,-7.083,128.53,-7.135,128.55,-7.156,128.625,-7.209,128.67,-7.183,128.667,-7.138,128.673,-7.113,128.658,-7.091,128.628,-7.069]},{"b":[127.786,-8.255,128.119,-8.099],"l":[127.998,-8.139,127.823,-8.099,127.786,-8.12,127.821,-8.19,128.024,-8.255,128.119,-8.171,128.099,-8.135,127.998,-8.139]},{"b":[127.355,-7.646,127.475,-7.513],"l":[127.371,-7.513,127.375,-7.572,127.355,-7.646,127.419,-7.623,127.464,-7.597,127.474,-7.579,127.475,-7.531,127.371,-7.513]},{"b":[125.798,-7.985,126.814,-7.572],"l":[126.726,-7.662,126.61,-7.572,126.463,-7.608,126.359,-7.677,126.214,-7.707,126.085,-7.697,125.975,-7.663,125.843,-7.817,125.808,-7.881,125.798,-7.985,125.826,-7.979,125.952,-7.911,126.04,-7.886,126.108,-7.884,126.171,-7.912,126.313,-7.918,126.472,-7.95,126.518,-7.87,126.577,-7.808,126.693,-7.754,126.813,-7.738,126.814,-7.717,126.801,-7.668,126.726,-7.662]},{"b":[125.507,-8.312,125.646,-8.14],"l":[125.507,-8.275,125.579,-8.312,125.646,-8.14,125.621,-8.15,125.584,-8.179,125.507,-8.275]},{"b":[123.326,-10.337,123.497,-10.14],"l":[123.395,-10.171,123.326,-10.264,123.326,-10.337,123.416,-10.303,123.405,-10.227,123.497,-10.194,123.494,-10.177,123.459,-10.14,123.395,-10.171]},{"b":[122.818,-10.91,123.418,-10.472],"l":[123.34,-10.486,123.265,-10.518,123.146,-10.64,123.061,-10.698,122.846,-10.762,122.818,-10.811,122.826,-10.899,122.856,-10.91,122.949,-10.909,123.005,-10.876,123.215,-10.806,123.311,-10.698,123.418,-10.651,123.413,-10.623,123.383,-10.568,123.371,-10.475,123.358,-10.472,123.34,-10.486]},{"b":[121.705,-10.602,121.998,-10.433],"l":[121.796,-10.507,121.705,-10.556,121.726,-10.573,121.833,-10.602,121.883,-10.59,121.981,-10.528,121.998,-10.447,121.95,-10.433,121.867,-10.439,121.796,-10.507]},{"b":[124.356,-8.445,125.132,-8.135],"l":[124.752,-8.16,124.677,-8.168,124.6,-8.202,124.576,-8.141,124.509,-8.135,124.431,-8.183,124.394,-8.253,124.426,-8.296,124.356,-8.386,124.381,-8.415,124.444,-8.445,125.097,-8.353,125.132,-8.326,125.125,-8.205,125.05,-8.18,124.924,-8.166,124.752,-8.16]},{"b":[123.928,-8.552,124.304,-8.202],"l":[124.24,-8.203,124.111,-8.364,124.096,-8.356,124.069,-8.318,124.014,-8.319,123.971,-8.354,123.928,-8.449,124.017,-8.444,124.066,-8.552,124.147,-8.531,124.184,-8.499,124.226,-8.391,124.287,-8.329,124.304,-8.229,124.287,-8.209,124.266,-8.202,124.24,-8.203]},{"b":[123.23,-8.587,123.925,-8.19],"l":[123.776,-8.19,123.601,-8.291,123.573,-8.292,123.53,-8.265,123.473,-8.267,123.391,-8.28,123.395,-8.301,123.425,-8.313,123.476,-8.322,123.455,-8.354,123.325,-8.439,123.23,-8.531,123.253,-8.539,123.33,-8.536,123.411,-8.587,123.434,-8.576,123.489,-8.532,123.553,-8.567,123.58,-8.545,123.588,-8.524,123.583,-8.502,123.592,-8.478,123.629,-8.422,123.698,-8.424,123.784,-8.3,123.925,-8.272,123.896,-8.239,123.846,-8.213,123.776,-8.19]},{"b":[123.025,-8.399,123.336,-8.235],"l":[123.217,-8.235,123.133,-8.254,123.108,-8.275,123.033,-8.338,123.025,-8.396,123.297,-8.399,123.317,-8.355,123.336,-8.269,123.217,-8.235]},{"b":[119.379,-8.75,119.557,-8.429],"l":[119.471,-8.456,119.446,-8.429,119.43,-8.455,119.42,-8.539,119.379,-8.587,119.402,-8.647,119.386,-8.736,119.425,-8.75,119.464,-8.741,119.444,-8.672,119.483,-8.628,119.536,-8.589,119.555,-8.553,119.557,-8.519,119.547,-8.483,119.502,-8.481,119.482,-8.473,119.471,-8.456]},{"b":[119.021,-8.24,119.135,-8.139],"l":[119.037,-8.158,119.021,-8.2,119.03,-8.24,119.074,-8.239,119.107,-8.223,119.135,-8.197,119.128,-8.177,119.098,-8.139,119.079,-8.14,119.037,-8.158]},{"b":[115.48,-8.804,115.613,-8.67],"l":[115.48,-8.715,115.501,-8.743,115.582,-8.804,115.61,-8.77,115.613,-8.713,115.561,-8.67,115.541,-8.675,115.48,-8.715]},{"b":[115.22,-6.988,115.546,-6.838],"l":[115.414,-6.84,115.354,-6.838,115.241,-6.861,115.222,-6.905,115.22,-6.953,115.296,-6.988,115.377,-6.971,115.424,-6.941,115.546,-6.939,115.524,-6.902,115.479,-6.87,115.414,-6.84]},{"b":[112.586,-5.846,112.727,-5.726],"l":[112.586,-5.804,112.602,-5.844,112.698,-5.846,112.719,-5.811,112.727,-5.753,112.69,-5.726,112.649,-5.731,112.586,-5.804]},{"b":[117.625,4.004,117.923,4.262],"l":[117.708,4.262,117.662,4.25,117.667,4.204,117.625,4.121,117.737,4.004,117.923,4.054,117.918,4.091,117.885,4.186,117.761,4.252,117.708,4.262]},{"b":[117.538,3.248,117.681,3.436],"l":[117.548,3.432,117.538,3.386,117.56,3.328,117.646,3.248,117.658,3.281,117.681,3.408,117.637,3.436,117.548,3.432]},{"b":[116.327,-3.637,116.427,-3.4],"l":[116.327,-3.539,116.388,-3.637,116.424,-3.464,116.427,-3.4,116.395,-3.423,116.327,-3.539]},{"b":[116.018,-4.054,116.319,-3.251],"l":[116.282,-3.535,116.295,-3.495,116.287,-3.449,116.262,-3.395,116.27,-3.251,116.239,-3.26,116.117,-3.34,116.064,-3.458,116.022,-3.612,116.018,-3.7,116.077,-3.817,116.059,-4.007,116.093,-4.054,116.303,-3.868,116.289,-3.821,116.319,-3.763,116.305,-3.719,116.282,-3.535]},{"b":[108.804,-1.662,108.957,-1.54],"l":[108.877,-1.54,108.804,-1.568,108.838,-1.662,108.953,-1.62,108.957,-1.564,108.877,-1.54]},{"b":[109.428,-1.283,109.761,-0.979],"l":[109.7,-1.007,109.615,-0.979,109.476,-0.985,109.45,-1.044,109.428,-1.241,109.464,-1.278,109.511,-1.283,109.71,-1.181,109.751,-1.145,109.761,-1.105,109.743,-1.039,109.7,-1.007]},{"b":[108.787,2.853,108.888,2.999],"l":[108.787,2.886,108.839,2.853,108.888,2.905,108.886,2.999,108.867,2.992,108.787,2.886]},{"b":[108.002,3.653,108.399,4.217],"l":[108.393,3.986,108.256,4.152,108.248,4.217,108.202,4.2,108.004,4.043,108.002,3.983,108.045,3.889,108.088,3.852,108.243,3.81,108.236,3.785,108.216,3.772,108.186,3.768,108.1,3.705,108.18,3.653,108.316,3.69,108.394,3.836,108.399,3.876,108.393,3.986]},{"b":[106.201,3.088,106.285,3.23],"l":[106.215,3.129,106.284,3.088,106.285,3.157,106.271,3.216,106.224,3.23,106.201,3.205,106.215,3.129]},{"b":[105.692,2.859,105.837,3.063],"l":[105.731,3.037,105.692,3.063,105.692,3.011,105.704,2.981,105.708,2.94,105.706,2.889,105.719,2.859,105.76,2.863,105.809,2.904,105.837,2.977,105.822,2.984,105.795,2.996,105.76,3.013,105.731,3.037]},{"b":[101.403,1.716,101.774,2.127],"l":[101.641,2.127,101.545,2.061,101.45,2.068,101.41,2.022,101.403,1.901,101.468,1.759,101.501,1.733,101.603,1.716,101.719,1.789,101.734,1.883,101.774,1.943,101.762,1.997,101.708,2.078,101.641,2.127]},{"b":[102.229,0.96,102.449,1.397],"l":[102.276,1.395,102.256,1.397,102.229,1.348,102.234,1.264,102.255,1.147,102.28,1.076,102.325,1.007,102.381,0.96,102.427,0.99,102.429,1.067,102.449,1.156,102.443,1.234,102.413,1.261,102.359,1.346,102.276,1.395]},{"b":[102.018,1.331,102.499,1.625],"l":[102.492,1.459,102.47,1.51,102.042,1.625,102.024,1.608,102.018,1.586,102.021,1.558,102.079,1.499,102.161,1.465,102.274,1.453,102.367,1.415,102.425,1.364,102.499,1.331,102.492,1.459]},{"b":[102.454,0.708,103.028,1.13],"l":[102.78,0.959,102.726,0.989,102.633,1.054,102.549,1.13,102.507,1.089,102.491,0.987,102.466,0.95,102.454,0.89,102.49,0.857,102.542,0.832,102.711,0.784,102.776,0.78,102.971,0.737,103.009,0.708,103.028,0.747,103.002,0.859,102.944,0.893,102.78,0.959]},{"b":[102.702,0.842,103.166,1.165],"l":[103.068,1.015,102.999,1.068,102.79,1.165,102.726,1.159,102.702,1.054,102.726,1.041,102.788,1.031,102.886,0.997,102.964,0.943,103.033,0.882,103.087,0.848,103.137,0.842,103.166,0.87,103.068,1.015]},{"b":[103.14,0.536,103.295,0.7],"l":[103.238,0.699,103.187,0.7,103.153,0.643,103.14,0.549,103.172,0.536,103.284,0.542,103.295,0.614,103.238,0.699]},{"b":[103.315,0.993,103.43,1.134],"l":[103.38,1.134,103.355,1.117,103.315,1.071,103.363,1.007,103.43,0.993,103.424,1.048,103.405,1.073,103.38,1.134]},{"b":[103.344,0.651,103.497,0.87],"l":[103.386,0.87,103.366,0.851,103.344,0.778,103.43,0.651,103.45,0.664,103.497,0.723,103.47,0.778,103.433,0.825,103.386,0.87]},{"b":[103.74,0.772,103.834,0.891],"l":[103.752,0.891,103.74,0.872,103.742,0.83,103.834,0.772,103.829,0.801,103.807,0.846,103.752,0.891]},{"b":[103.932,0.99,104.14,1.181],"l":[104.025,1.181,104,1.137,103.955,1.137,103.947,1.087,103.932,1.071,103.94,1.046,103.964,1.013,104.066,0.99,104.127,1.092,104.138,1.128,104.14,1.166,104.088,1.137,104.025,1.181]},{"b":[104.244,0.832,104.663,1.216],"l":[104.591,1.141,104.585,1.216,104.5,1.18,104.428,1.196,104.362,1.181,104.25,1.103,104.244,1.077,104.252,1.015,104.294,1.016,104.439,1.05,104.462,0.996,104.429,0.956,104.481,0.933,104.471,0.913,104.481,0.887,104.504,0.853,104.575,0.832,104.599,0.859,104.653,0.961,104.663,1.05,104.648,1.105,104.591,1.141]},{"b":[104.098,0.805,104.239,0.944],"l":[104.171,0.897,104.123,0.944,104.108,0.934,104.101,0.917,104.098,0.896,104.177,0.805,104.239,0.834,104.227,0.88,104.171,0.897]},{"b":[104.499,0.035,104.698,0.232],"l":[104.544,0.223,104.499,0.232,104.604,0.095,104.622,0.08,104.651,0.063,104.698,0.035,104.689,0.06,104.66,0.103,104.544,0.223]},{"b":[104.447,-0.323,105.005,0.018],"l":[104.843,-0.141,104.808,-0.192,104.779,-0.176,104.713,-0.103,104.653,-0.076,104.658,-0.063,104.636,-0.018,104.543,0.018,104.497,-0.126,104.447,-0.189,104.474,-0.212,104.567,-0.246,104.702,-0.209,104.914,-0.323,104.929,-0.317,104.951,-0.284,105.005,-0.283,104.95,-0.247,104.909,-0.212,104.843,-0.141]},{"b":[104.257,-0.659,104.59,-0.335],"l":[104.364,-0.403,104.341,-0.383,104.319,-0.38,104.302,-0.386,104.257,-0.463,104.33,-0.539,104.363,-0.659,104.414,-0.584,104.485,-0.613,104.507,-0.597,104.544,-0.521,104.59,-0.467,104.568,-0.432,104.474,-0.335,104.364,-0.403]},{"b":[103.461,-0.383,103.764,-0.228],"l":[103.611,-0.231,103.549,-0.228,103.479,-0.297,103.461,-0.358,103.606,-0.383,103.737,-0.348,103.764,-0.318,103.724,-0.277,103.611,-0.231]},{"b":[107.563,-3.227,108.291,-2.53],"l":[108.215,-2.697,108.074,-2.597,107.875,-2.56,107.838,-2.53,107.666,-2.566,107.642,-2.732,107.598,-2.8,107.605,-2.863,107.563,-2.92,107.584,-2.941,107.592,-2.977,107.595,-3.058,107.637,-3.125,107.614,-3.209,107.66,-3.206,107.822,-3.161,107.837,-3.097,107.858,-3.086,107.941,-3.129,107.967,-3.167,107.977,-3.222,108.055,-3.227,108.084,-3.195,108.167,-3.143,108.192,-3.103,108.207,-2.998,108.291,-2.83,108.215,-2.697]},{"b":[107.402,-2.925,107.5,-2.835],"l":[107.433,-2.925,107.473,-2.9,107.5,-2.845,107.474,-2.835,107.419,-2.838,107.402,-2.873,107.409,-2.901,107.433,-2.925]},{"b":[106.743,-3.025,106.911,-2.899],"l":[106.797,-2.899,106.743,-2.933,106.749,-2.96,106.774,-2.987,106.814,-3.014,106.87,-3.025,106.886,-3.005,106.911,-2.934,106.797,-2.899]},{"b":[105.121,-6.662,105.277,-6.524],"l":[105.121,-6.615,105.143,-6.643,105.19,-6.662,105.253,-6.64,105.277,-6.561,105.261,-6.524,105.226,-6.529,105.192,-6.546,105.121,-6.615]},{"b":[97.079,0.564,97.932,1.54],"l":[97.786,1.146,97.698,1.184,97.482,1.465,97.356,1.54,97.343,1.528,97.324,1.482,97.244,1.424,97.079,1.425,97.297,1.187,97.369,1.057,97.405,0.947,97.461,0.941,97.604,0.834,97.683,0.641,97.684,0.596,97.82,0.564,97.876,0.628,97.902,0.884,97.932,0.974,97.903,1.018,97.786,1.146]},{"b":[102.111,-5.483,102.405,-5.286],"l":[102.372,-5.366,102.198,-5.289,102.154,-5.286,102.111,-5.323,102.136,-5.361,102.286,-5.483,102.367,-5.479,102.405,-5.405,102.372,-5.366]},{"b":[100.179,-3.329,100.469,-2.783],"l":[100.464,-3.117,100.469,-3.039,100.455,-3.002,100.246,-2.783,100.199,-2.786,100.179,-2.82,100.204,-2.987,100.26,-3.057,100.332,-3.113,100.348,-3.159,100.346,-3.229,100.465,-3.329,100.425,-3.183,100.434,-3.141,100.464,-3.117]},{"b":[99.968,-2.821,100.204,-2.51],"l":[100.012,-2.51,99.988,-2.525,99.969,-2.594,99.968,-2.61,99.997,-2.649,99.992,-2.77,100.015,-2.82,100.133,-2.821,100.204,-2.741,100.202,-2.68,100.012,-2.51]},{"b":[99.537,-2.37,99.848,-2.017],"l":[99.735,-2.178,99.686,-2.063,99.622,-2.017,99.572,-2.026,99.562,-2.051,99.559,-2.115,99.537,-2.162,99.607,-2.258,99.685,-2.282,99.848,-2.37,99.843,-2.343,99.816,-2.284,99.735,-2.178]},{"b":[98.602,-1.783,99.271,-0.916],"l":[99.131,-1.442,99.14,-1.418,99.129,-1.384,99.101,-1.34,99.065,-1.241,98.955,-1.056,98.933,-0.954,98.869,-0.916,98.676,-0.971,98.602,-1.198,98.627,-1.261,98.816,-1.538,98.828,-1.61,98.874,-1.664,99.072,-1.783,99.164,-1.778,99.271,-1.738,99.267,-1.628,99.21,-1.559,99.131,-1.442]},{"b":[98.31,-0.577,98.544,0.007],"l":[98.415,-0.018,98.375,0.007,98.323,-0.001,98.427,-0.226,98.409,-0.309,98.355,-0.379,98.34,-0.468,98.31,-0.532,98.4,-0.577,98.459,-0.53,98.52,-0.38,98.544,-0.258,98.484,-0.168,98.415,-0.018]},{"b":[97.108,2.053,97.334,2.232],"l":[97.291,2.201,97.253,2.216,97.157,2.232,97.108,2.217,97.225,2.158,97.328,2.053,97.334,2.076,97.329,2.149,97.291,2.201]},{"b":[95.717,2.351,96.464,2.916],"l":[96.417,2.515,96.18,2.661,96.13,2.721,96.102,2.741,95.998,2.781,95.896,2.889,95.806,2.916,95.772,2.855,95.717,2.826,95.733,2.767,95.809,2.656,95.88,2.641,95.938,2.598,96.022,2.596,96.29,2.43,96.341,2.372,96.401,2.351,96.464,2.36,96.459,2.416,96.443,2.466,96.417,2.515]},{"b":[95.218,5.784,95.366,5.907],"l":[95.242,5.907,95.218,5.89,95.283,5.799,95.343,5.784,95.362,5.812,95.366,5.843,95.359,5.877,95.283,5.898,95.242,5.907]},{"b":[111.3,2.402,111.389,2.768],"l":[111.376,2.576,111.378,2.709,111.355,2.764,111.333,2.768,111.3,2.741,111.312,2.438,111.359,2.402,111.389,2.415,111.38,2.459,111.376,2.576]},{"b":[117.06,7.115,117.281,7.352],"l":[117.064,7.261,117.06,7.179,117.081,7.115,117.142,7.168,117.239,7.185,117.267,7.221,117.281,7.291,117.264,7.352,117.147,7.337,117.064,7.261]},{"b":[31.509,46.155,32.15,46.373],"l":[31.585,46.303,31.509,46.373,31.529,46.307,31.564,46.258,31.7,46.214,32.009,46.168,32.15,46.155,32.012,46.204,31.638,46.273,31.585,46.303]},{"b":[22.056,58.69,23.009,59.087],"l":[22.411,58.863,22.479,58.754,22.473,58.712,22.542,58.69,22.661,58.709,22.767,58.821,22.793,58.797,22.842,58.777,22.924,58.827,23.009,58.834,22.982,58.92,22.91,58.991,22.725,59.015,22.712,59.032,22.702,59.074,22.649,59.087,22.587,59.081,22.505,59.026,22.463,58.974,22.056,58.944,22.162,58.898,22.307,58.895,22.411,58.863]},{"b":[23.063,58.54,23.356,58.678],"l":[23.165,58.678,23.109,58.659,23.063,58.611,23.26,58.54,23.344,58.55,23.356,58.576,23.333,58.649,23.165,58.678]},{"b":[35.529,64.977,35.858,65.198],"l":[35.858,65.078,35.848,65.143,35.816,65.182,35.729,65.198,35.609,65.157,35.586,65.167,35.529,65.151,35.559,65.094,35.621,65.059,35.68,65.058,35.779,64.977,35.842,65.001,35.827,65.036,35.858,65.078]},{"b":[42.46,66.688,42.714,66.796],"l":[42.631,66.782,42.547,66.796,42.469,66.786,42.46,66.77,42.477,66.735,42.676,66.688,42.714,66.702,42.691,66.735,42.631,66.782]},{"b":[26.408,78.646,27.008,78.811],"l":[26.789,78.724,26.586,78.811,26.456,78.81,26.408,78.784,26.46,78.72,26.729,78.646,26.876,78.649,27.008,78.698,26.789,78.724]},{"b":[27.889,78.829,29.697,78.971],"l":[29.047,78.912,28.845,78.971,28.511,78.967,28.415,78.961,28.374,78.927,28.121,78.908,27.889,78.852,28.038,78.829,28.495,78.887,28.881,78.88,29.311,78.852,29.697,78.905,29.645,78.922,29.345,78.906,29.047,78.912]},{"b":[49.556,80.061,50.319,80.23],"l":[50.319,80.172,50.31,80.186,50.251,80.219,49.884,80.23,49.556,80.159,49.588,80.136,49.971,80.061,50.052,80.074,50.072,80.109,50.319,80.172]},{"b":[50.091,79.921,51.435,80.094],"l":[51.243,79.991,51.238,80.01,51.254,80.049,50.936,80.094,50.676,80.049,50.473,80.035,50.091,79.981,50.454,79.924,51.076,79.932,51.431,79.921,51.435,79.932,51.409,79.944,51.327,79.972,51.243,79.991]},{"b":[31.482,80.081,33.629,80.242],"l":[31.482,80.108,31.578,80.081,32.526,80.119,33.557,80.198,33.629,80.217,33.384,80.242,33.099,80.229,33.019,80.218,31.482,80.108]},{"b":[55.466,81.136,58.564,81.565],"l":[57.456,81.543,57.365,81.535,57.092,81.541,56.973,81.511,56.719,81.423,56.405,81.387,56.157,81.303,55.782,81.329,55.466,81.311,55.573,81.228,55.717,81.188,56.192,81.224,56.364,81.179,56.513,81.175,56.669,81.198,56.822,81.238,57.159,81.178,57.451,81.136,57.77,81.17,57.913,81.198,58.015,81.255,57.912,81.303,57.859,81.368,58.372,81.387,58.564,81.418,58.436,81.464,58.017,81.484,57.863,81.506,57.81,81.546,57.717,81.565,57.456,81.543]},{"b":[54.045,80.628,57.694,81.116],"l":[54.634,81.113,54.417,80.987,54.367,80.904,54.241,80.902,54.045,80.872,54.067,80.814,54.376,80.787,54.533,80.783,54.623,80.765,54.668,80.739,55.117,80.752,55.541,80.703,55.713,80.637,55.883,80.628,56.316,80.633,56.815,80.664,57.58,80.755,57.694,80.792,57.568,80.82,56.91,80.913,56.472,80.998,56.17,81.029,55.471,81.02,54.719,81.116,54.634,81.113]},{"b":[50.368,81.041,50.946,81.171],"l":[50.754,81.047,50.789,81.072,50.946,81.108,50.879,81.151,50.716,81.171,50.592,81.169,50.522,81.158,50.506,81.144,50.465,81.126,50.368,81.123,50.377,81.103,50.412,81.084,50.518,81.046,50.616,81.041,50.754,81.047]},{"b":[57.211,80.765,58.931,81.118],"l":[58.05,81.118,57.656,81.032,57.41,81.047,57.211,81.017,57.405,80.915,57.75,80.889,57.938,80.793,58.286,80.765,58.642,80.768,58.86,80.779,58.931,80.832,58.903,80.898,58.815,80.934,58.762,80.991,58.622,81.042,58.508,81.062,58.19,81.095,58.102,81.114,58.05,81.118]},{"b":[62.106,81.597,63.782,81.719],"l":[62.885,81.609,63.529,81.597,63.651,81.609,63.782,81.65,63.767,81.664,63.71,81.687,62.795,81.719,62.284,81.707,62.106,81.679,62.515,81.659,62.531,81.647,62.573,81.633,62.885,81.609]},{"b":[57.909,81.696,59.408,81.854],"l":[59.356,81.759,59.357,81.781,59.408,81.825,59.262,81.854,58.135,81.828,57.985,81.797,57.945,81.748,57.909,81.722,57.921,81.71,57.965,81.696,58.295,81.715,59.356,81.759]},{"b":[18.162,80.246,18.742,80.358],"l":[18.525,80.246,18.742,80.301,18.519,80.348,18.292,80.358,18.206,80.332,18.162,80.288,18.525,80.246]},{"b":[10.558,78.225,12.116,78.903],"l":[11.929,78.375,11.885,78.409,11.826,78.436,11.616,78.475,11.424,78.549,11.262,78.542,11.25,78.611,11.155,78.641,11.078,78.686,11.153,78.724,11.124,78.753,10.961,78.846,10.773,78.888,10.558,78.903,10.558,78.838,10.628,78.754,10.789,78.687,10.841,78.645,11.121,78.463,11.199,78.441,11.372,78.439,11.587,78.388,11.757,78.329,11.965,78.225,12.116,78.233,12.056,78.306,11.929,78.375]},{"b":[18.797,74.352,19.275,74.518],"l":[18.861,74.514,18.797,74.486,18.918,74.411,19.099,74.352,19.219,74.391,19.275,74.457,19.262,74.479,19.183,74.518,18.861,74.514]},{"b":[52.24,70.969,53.205,71.404],"l":[53.141,71.242,53.074,71.238,52.994,71.291,52.903,71.365,52.777,71.4,52.732,71.404,52.72,71.39,52.73,71.355,52.617,71.383,52.513,71.385,52.297,71.357,52.24,71.325,52.25,71.285,52.289,71.27,52.425,71.239,52.547,71.25,52.738,71.181,52.789,71.115,52.835,71.086,52.95,71.054,53.004,71.012,53.023,70.969,53.121,70.982,53.106,70.999,53.048,71.031,53.071,71.065,53.205,71.16,53.193,71.215,53.141,71.242]},{"b":[74.1,72.864,74.962,73.13],"l":[74.409,73.13,74.199,73.109,74.142,73.074,74.1,73.022,74.181,72.975,74.435,72.908,74.588,72.881,74.638,72.864,74.661,72.873,74.697,72.908,74.66,72.929,74.647,72.969,74.743,73.033,74.962,73.063,74.725,73.108,74.6,73.122,74.409,73.13]},{"b":[75.344,73.432,76.052,73.574],"l":[76.052,73.549,76.039,73.56,75.93,73.574,75.57,73.541,75.375,73.477,75.344,73.432,75.504,73.457,75.827,73.459,75.901,73.481,76.052,73.549]},{"b":[76.083,73.44,76.756,73.555],"l":[76.251,73.555,76.14,73.554,76.083,73.523,76.234,73.476,76.659,73.44,76.756,73.446,76.251,73.555]},{"b":[76.871,72.282,78.365,72.631],"l":[77.749,72.631,77.579,72.631,77.378,72.565,77.26,72.486,77.15,72.439,76.903,72.366,76.871,72.317,76.906,72.298,77.146,72.282,77.633,72.291,77.781,72.309,78.007,72.392,78.154,72.417,78.365,72.482,78.353,72.504,78.279,72.553,77.749,72.631]},{"b":[78.633,72.711,79.541,73.094],"l":[79.413,72.983,79.357,73.039,79.164,73.094,78.657,72.892,78.633,72.851,78.69,72.803,78.881,72.752,79.431,72.711,79.501,72.722,79.538,72.769,79.541,72.919,79.413,72.983]},{"b":[82.329,74.056,82.71,74.161],"l":[82.382,74.149,82.329,74.131,82.382,74.099,82.478,74.076,82.613,74.056,82.71,74.091,82.689,74.111,82.611,74.149,82.526,74.161,82.382,74.149]},{"b":[82.818,74.04,83.618,74.152],"l":[83.15,74.152,82.903,74.129,82.818,74.092,83.159,74.075,83.411,74.04,83.45,74.052,83.496,74.048,83.549,74.072,83.618,74.089,83.513,74.122,83.15,74.152]},{"b":[84.389,74.4,84.873,74.516],"l":[84.54,74.49,84.389,74.454,84.429,74.43,84.71,74.4,84.759,74.459,84.873,74.516,84.68,74.512,84.54,74.49]},{"b":[86.259,74.831,87.124,74.993],"l":[86.653,74.981,86.605,74.993,86.504,74.966,86.331,74.939,86.259,74.894,86.391,74.851,86.692,74.848,86.927,74.831,87.012,74.862,87.124,74.94,87.052,74.983,87.001,74.992,86.737,74.963,86.653,74.981]},{"b":[81.501,75.247,82.222,75.516],"l":[82.022,75.513,81.913,75.498,81.91,75.46,81.927,75.41,81.842,75.407,81.712,75.451,81.501,75.368,81.532,75.34,81.579,75.331,81.655,75.289,81.698,75.281,81.861,75.317,81.905,75.263,81.979,75.247,82.05,75.341,82.179,75.339,82.222,75.351,82.209,75.387,82.172,75.419,82.166,75.516,82.022,75.513]},{"b":[95.311,76.122,96.614,76.305],"l":[96.271,76.305,96.151,76.272,95.786,76.294,95.594,76.25,95.38,76.289,95.322,76.262,95.311,76.215,95.679,76.194,95.845,76.16,96.109,76.155,96.301,76.122,96.353,76.177,96.351,76.212,96.487,76.234,96.59,76.221,96.614,76.264,96.532,76.278,96.271,76.305]},{"b":[97.31,76.584,97.588,76.707],"l":[97.31,76.69,97.342,76.629,97.43,76.591,97.535,76.584,97.588,76.599,97.382,76.707,97.31,76.69]},{"b":[95.27,76.975,96.562,77.206],"l":[95.854,77.098,95.421,77.056,95.27,77.019,95.364,77.012,95.681,77.021,95.766,76.991,95.855,76.975,96.091,77.003,96.21,76.992,96.254,77.007,96.285,77.027,96.424,77.071,96.561,77.13,96.562,77.154,96.528,77.206,95.854,77.098]},{"b":[89.142,77.184,89.68,77.311],"l":[89.616,77.311,89.282,77.301,89.2,77.272,89.142,77.227,89.179,77.21,89.3,77.184,89.514,77.189,89.666,77.254,89.68,77.28,89.616,77.311]},{"b":[76.052,79.49,77.589,79.664],"l":[76.467,79.643,76.373,79.615,76.249,79.651,76.148,79.664,76.052,79.645,76.072,79.626,76.154,79.579,76.458,79.545,76.637,79.544,76.65,79.493,76.81,79.49,77.589,79.502,77.549,79.524,77.36,79.557,76.467,79.643]},{"b":[78.978,80.812,80.428,80.975],"l":[79.217,80.96,79.11,80.924,78.978,80.848,79.007,80.835,79.099,80.812,80.027,80.848,80.345,80.868,80.373,80.883,80.428,80.928,80.28,80.95,79.807,80.975,79.217,80.96]},{"b":[89.901,81.064,91.567,81.214],"l":[91.478,81.184,91.109,81.199,90.07,81.214,89.901,81.171,89.919,81.149,89.976,81.113,91.223,81.064,91.567,81.141,91.478,81.184]},{"b":[106.416,78.058,107.696,78.189],"l":[106.583,78.168,106.416,78.14,107.002,78.096,107.344,78.099,107.482,78.058,107.606,78.083,107.696,78.131,107.573,78.186,107.508,78.189,106.583,78.168]},{"b":[106.024,78.199,106.72,78.34],"l":[106.679,78.265,106.719,78.265,106.72,78.294,106.691,78.317,106.64,78.336,106.457,78.34,106.351,78.273,106.058,78.265,106.024,78.22,106.151,78.199,106.27,78.206,106.472,78.245,106.505,78.262,106.679,78.265]},{"b":[107.27,77.242,107.679,77.347],"l":[107.366,77.347,107.27,77.289,107.302,77.242,107.415,77.243,107.679,77.268,107.665,77.3,107.629,77.32,107.594,77.33,107.486,77.347,107.366,77.347]},{"b":[111.969,76.45,112.661,76.644],"l":[112.154,76.549,112.297,76.538,112.395,76.484,112.532,76.45,112.575,76.452,112.587,76.483,112.614,76.499,112.661,76.51,112.633,76.553,112.478,76.621,112.394,76.644,112.281,76.618,112.011,76.633,111.969,76.626,112.003,76.603,112.154,76.549]},{"b":[119.64,73.045,120.261,73.168],"l":[120.079,73.157,119.964,73.168,119.762,73.155,119.64,73.124,119.792,73.045,120.008,73.045,120.261,73.09,120.237,73.107,120.079,73.157]},{"b":[124.336,73.848,124.653,73.943],"l":[124.43,73.943,124.337,73.928,124.336,73.91,124.366,73.875,124.482,73.848,124.543,73.85,124.653,73.888,124.637,73.9,124.548,73.934,124.43,73.943]},{"b":[135.452,75.382,136.169,75.845],"l":[136.169,75.606,136.127,75.626,135.905,75.694,135.849,75.729,135.788,75.798,135.699,75.845,135.614,75.766,135.578,75.71,135.561,75.636,135.593,75.576,135.523,75.496,135.473,75.463,135.452,75.39,135.746,75.382,135.949,75.41,136.021,75.438,135.965,75.486,135.983,75.522,136.169,75.606]},{"b":[148.399,76.648,149.406,76.782],"l":[149.406,76.782,148.72,76.747,148.448,76.677,148.399,76.648,149.15,76.66,149.205,76.677,149.268,76.747,149.406,76.782]},{"b":[135.387,73.885,136.259,74.253],"l":[136.037,74.09,135.628,74.22,135.387,74.253,135.402,74.202,135.449,74.18,135.633,74.121,135.715,74.06,136.051,73.929,136.122,73.885,136.197,73.914,136.259,73.985,136.037,74.09]},{"b":[137.064,71.423,137.96,71.588],"l":[137.282,71.58,137.168,71.557,137.129,71.556,137.082,71.543,137.064,71.53,137.079,71.502,137.266,71.456,137.344,71.461,137.403,71.477,137.458,71.483,137.512,71.475,137.613,71.434,137.712,71.423,137.96,71.508,137.934,71.543,137.858,71.583,137.817,71.588,137.282,71.58]},{"b":[160.437,70.806,160.719,70.934],"l":[160.566,70.924,160.449,70.934,160.44,70.923,160.437,70.851,160.505,70.82,160.651,70.806,160.719,70.823,160.645,70.884,160.566,70.924]},{"b":[167.789,69.571,169.434,70.016],"l":[168.358,70.016,168.196,70.008,168.06,69.975,167.865,69.901,167.814,69.873,167.789,69.837,167.821,69.82,167.993,69.776,168.144,69.713,168.348,69.664,168.916,69.571,169.201,69.58,169.246,69.601,169.263,69.629,169.299,69.735,169.332,69.77,169.418,69.779,169.434,69.832,169.421,69.856,169.375,69.883,168.358,70.016]},{"b":[163.427,58.509,164.662,59.227],"l":[164.573,59.221,164.517,59.227,164.202,59.096,163.761,59.015,163.767,58.972,163.784,58.93,163.727,58.799,163.577,58.641,163.427,58.579,163.432,58.546,163.447,58.525,163.471,58.509,163.635,58.603,163.96,58.744,164.279,58.838,164.616,58.886,164.662,58.971,164.629,59.112,164.573,59.221]},{"b":[167.442,54.513,168.081,54.857],"l":[167.711,54.77,167.592,54.798,167.512,54.857,167.442,54.856,167.488,54.795,167.677,54.698,168.081,54.513,168.039,54.565,167.883,54.69,167.711,54.77]},{"b":[165.751,54.694,166.65,55.351],"l":[166.577,54.908,166.479,54.95,166.404,55.006,166.248,55.165,166.23,55.242,166.276,55.312,166.212,55.324,165.931,55.351,165.83,55.307,165.751,55.295,165.992,55.19,166.066,55.136,166.082,55.077,166.12,55.03,166.23,54.937,166.325,54.865,166.382,54.838,166.464,54.827,166.521,54.768,166.645,54.694,166.65,54.839,166.577,54.908]},{"b":[156.168,50.634,156.488,50.862],"l":[156.376,50.862,156.213,50.785,156.168,50.732,156.196,50.702,156.326,50.639,156.365,50.634,156.405,50.658,156.483,50.751,156.488,50.843,156.456,50.86,156.376,50.862]},{"b":[155.195,50.041,156.123,50.772],"l":[156.097,50.772,156.002,50.757,155.885,50.684,155.773,50.482,155.68,50.401,155.434,50.369,155.327,50.293,155.218,50.298,155.195,50.265,155.243,50.213,155.243,50.095,155.289,50.061,155.397,50.041,155.449,50.078,155.516,50.146,155.608,50.177,155.792,50.202,155.921,50.302,156.044,50.452,156.101,50.559,156.123,50.671,156.097,50.772]},{"b":[153.992,48.739,154.228,48.905],"l":[154.126,48.904,154.092,48.832,153.992,48.773,154.001,48.756,154.043,48.739,154.081,48.79,154.205,48.857,154.228,48.892,154.199,48.905,154.126,48.904]},{"b":[155.467,50.811,155.654,50.934],"l":[155.645,50.822,155.654,50.845,155.64,50.91,155.569,50.934,155.467,50.914,155.483,50.87,155.513,50.837,155.554,50.811,155.645,50.822]},{"b":[152.984,47.706,153.101,47.809],"l":[152.984,47.728,153.004,47.713,153.054,47.706,153.101,47.763,153.079,47.809,153.049,47.797,152.984,47.728]},{"b":[151.715,46.787,152.289,47.143],"l":[151.864,46.869,151.715,46.853,151.723,46.829,151.754,46.788,151.816,46.787,152.002,46.897,152.289,47.142,152.235,47.143,152.166,47.11,152.04,47.015,151.864,46.869]},{"b":[149.447,45.591,150.553,46.213],"l":[149.962,46.022,149.796,45.876,149.666,45.84,149.447,45.593,149.539,45.591,149.688,45.642,149.883,45.783,149.954,45.822,150.057,45.849,150.195,45.933,150.235,46.012,150.553,46.209,150.349,46.213,150.309,46.2,149.962,46.022]},{"b":[146.897,44.404,148.837,45.526],"l":[148.812,45.51,148.773,45.526,148.707,45.521,148.612,45.485,148.324,45.282,148.13,45.258,148.056,45.262,147.965,45.378,147.924,45.383,147.873,45.3,147.886,45.226,147.769,45.191,147.658,45.093,147.558,45.062,147.43,44.945,147.247,44.856,147.155,44.766,147.141,44.663,146.974,44.566,146.933,44.513,146.897,44.404,147.098,44.531,147.207,44.554,147.31,44.678,147.563,44.836,147.61,44.887,147.622,44.945,147.658,44.977,147.784,44.959,147.914,44.99,148.005,45.07,148.262,45.217,148.415,45.247,148.6,45.318,148.791,45.324,148.837,45.363,148.803,45.414,148.825,45.456,148.826,45.486,148.812,45.51]},{"b":[150.47,59.007,150.728,59.16],"l":[150.666,59.16,150.592,59.097,150.47,59.054,150.472,59.035,150.511,59.007,150.59,59.019,150.728,59.095,150.713,59.122,150.666,59.16]},{"b":[137.233,54.653,138.206,55.197],"l":[137.941,55.093,137.91,55.11,137.577,55.197,137.544,55.163,137.436,55.016,137.384,55.001,137.275,54.891,137.233,54.791,137.276,54.792,137.463,54.873,137.526,54.826,137.661,54.653,137.721,54.663,137.79,54.697,137.87,54.75,137.959,54.789,137.991,54.821,138.017,54.901,138.096,54.991,138.206,55.034,138.172,55.06,138.031,55.053,137.941,55.093]},{"b":[136.715,54.924,137.179,55.108],"l":[137.078,55.092,136.996,55.093,136.795,55.009,136.715,54.956,136.765,54.946,136.903,54.961,136.969,54.924,137.055,54.927,137.179,55.1,137.156,55.108,137.078,55.092]},{"b":[146.609,43.716,146.899,43.86],"l":[146.622,43.813,146.613,43.797,146.609,43.74,146.683,43.716,146.714,43.744,146.899,43.804,146.884,43.829,146.825,43.86,146.622,43.813]},{"b":[145.426,43.665,146.568,44.5],"l":[146.356,44.425,146.208,44.498,146.112,44.5,145.94,44.273,145.89,44.249,145.852,44.193,145.773,44.129,145.748,44.072,145.666,43.999,145.462,43.871,145.426,43.81,145.439,43.737,145.556,43.665,145.587,43.845,145.767,43.941,145.887,44.048,145.914,44.104,146.112,44.246,146.173,44.269,146.296,44.281,146.437,44.376,146.516,44.375,146.568,44.44,146.356,44.425]},{"b":[124.085,24.335,124.324,24.587],"l":[124.17,24.452,124.12,24.47,124.085,24.436,124.136,24.348,124.186,24.335,124.234,24.358,124.293,24.516,124.324,24.566,124.302,24.587,124.211,24.459,124.17,24.452]},{"b":[123.68,24.266,123.935,24.414],"l":[123.752,24.348,123.68,24.318,123.681,24.288,123.75,24.283,123.826,24.266,123.889,24.28,123.928,24.324,123.935,24.362,123.771,24.414,123.754,24.391,123.752,24.348]},{"b":[125.269,24.717,125.444,24.872],"l":[125.315,24.852,125.284,24.872,125.269,24.733,125.359,24.717,125.444,24.743,125.402,24.777,125.335,24.805,125.315,24.852]},{"b":[127.65,26.095,128.332,26.882],"l":[128.255,26.882,128.217,26.797,128.122,26.711,128.098,26.668,128.047,26.643,128.03,26.647,127.994,26.679,127.907,26.694,127.895,26.675,127.891,26.631,127.946,26.594,127.926,26.556,127.82,26.466,127.796,26.449,127.729,26.434,127.727,26.308,127.655,26.199,127.65,26.154,127.653,26.095,127.729,26.097,127.804,26.153,127.806,26.171,127.786,26.209,127.79,26.255,127.849,26.319,127.905,26.328,127.869,26.381,127.867,26.442,127.951,26.456,128.038,26.534,128.127,26.552,128.163,26.607,128.259,26.653,128.311,26.721,128.332,26.812,128.255,26.882]},{"b":[128.883,27.702,129.016,27.91],"l":[128.952,27.91,128.908,27.898,128.883,27.842,128.9,27.728,128.956,27.702,128.998,27.721,129.016,27.77,128.99,27.811,128.952,27.91]},{"b":[129.165,28.128,129.715,28.517],"l":[129.598,28.476,129.577,28.461,129.561,28.431,129.51,28.398,129.465,28.395,129.322,28.36,129.251,28.314,129.248,28.283,129.217,28.263,129.165,28.25,129.275,28.201,129.366,28.128,129.453,28.209,129.439,28.255,129.457,28.272,129.513,28.299,129.575,28.361,129.642,28.411,129.71,28.432,129.715,28.47,129.69,28.517,129.598,28.476]},{"b":[129.192,28.082,129.331,28.192],"l":[129.257,28.176,129.192,28.192,129.232,28.101,129.331,28.082,129.324,28.105,129.277,28.145,129.257,28.176]},{"b":[130.388,30.241,130.673,30.466],"l":[130.497,30.466,130.388,30.388,130.446,30.265,130.508,30.241,130.623,30.263,130.673,30.367,130.644,30.389,130.497,30.466]},{"b":[130.87,30.386,131.083,30.828],"l":[130.947,30.671,130.94,30.575,130.87,30.444,130.872,30.386,130.96,30.397,130.993,30.53,131.057,30.642,131.083,30.791,131.06,30.828,131.04,30.819,131.012,30.792,130.947,30.671]},{"b":[128.649,32.586,128.895,32.784],"l":[128.839,32.763,128.806,32.776,128.761,32.772,128.704,32.757,128.665,32.784,128.649,32.662,128.657,32.628,128.693,32.605,128.75,32.586,128.79,32.637,128.821,32.646,128.895,32.652,128.879,32.693,128.839,32.763]},{"b":[129.96,32.194,130.2,32.541],"l":[130.01,32.522,130.021,32.469,129.979,32.346,130.015,32.314,130.017,32.292,129.96,32.244,129.993,32.228,130.004,32.194,130.083,32.23,130.2,32.341,130.197,32.492,130.168,32.541,130.01,32.522]},{"b":[130.242,32.419,130.461,32.527],"l":[130.242,32.463,130.256,32.431,130.293,32.419,130.381,32.424,130.419,32.458,130.461,32.516,130.365,32.527,130.242,32.463]},{"b":[133.19,36.167,133.381,36.34],"l":[133.206,36.293,133.19,36.233,133.239,36.179,133.325,36.167,133.371,36.204,133.381,36.246,133.296,36.34,133.206,36.293]},{"b":[138.225,37.82,138.575,38.316],"l":[138.51,38.259,138.504,38.316,138.461,38.291,138.306,38.161,138.25,38.078,138.246,37.995,138.322,37.991,138.322,37.97,138.288,37.896,138.283,37.854,138.225,37.829,138.249,37.82,138.344,37.822,138.497,37.904,138.575,38.066,138.454,38.076,138.463,38.124,138.51,38.259]},{"b":[139.412,42.076,139.558,42.235],"l":[139.431,42.2,139.412,42.16,139.435,42.084,139.458,42.076,139.481,42.081,139.505,42.096,139.558,42.235,139.496,42.227,139.431,42.2]},{"b":[140.972,45.269,141.073,45.465],"l":[140.972,45.465,140.982,45.364,141.034,45.269,141.073,45.333,141.07,45.4,141.057,45.45,141.002,45.465,140.972,45.465]},{"b":[141.135,45.112,141.329,45.248],"l":[141.252,45.232,141.194,45.248,141.135,45.206,141.145,45.154,141.226,45.112,141.295,45.119,141.329,45.15,141.31,45.179,141.252,45.232]},{"b":[139.769,33.045,139.874,33.129],"l":[139.776,33.078,139.824,33.045,139.841,33.056,139.874,33.094,139.809,33.129,139.777,33.125,139.769,33.107,139.776,33.078]},{"b":[139.367,34.68,139.456,34.776],"l":[139.37,34.775,139.367,34.721,139.392,34.69,139.446,34.68,139.456,34.727,139.426,34.776,139.37,34.775]},{"b":[134.668,34.203,135.005,34.545],"l":[134.834,34.473,134.757,34.368,134.668,34.294,134.683,34.247,134.731,34.209,134.824,34.203,134.933,34.288,134.905,34.398,135.005,34.544,134.961,34.545,134.904,34.519,134.834,34.473]},{"b":[128.997,32.829,129.182,33.133],"l":[129.124,33.068,129.11,33.133,129.035,32.969,128.997,32.952,129.02,32.92,129.052,32.829,129.077,32.84,129.112,32.929,129.154,32.946,129.182,32.993,129.153,33.003,129.124,33.068]},{"b":[129.37,33.176,129.57,33.361],"l":[129.462,33.331,129.423,33.257,129.417,33.231,129.37,33.176,129.421,33.176,129.492,33.223,129.508,33.284,129.57,33.361,129.538,33.358,129.462,33.331]},{"b":[129.675,33.707,129.796,33.858],"l":[129.717,33.858,129.7,33.829,129.675,33.74,129.727,33.707,129.796,33.749,129.776,33.829,129.717,33.858]},{"b":[129.186,34.083,129.337,34.321],"l":[129.215,34.321,129.186,34.145,129.214,34.083,129.279,34.123,129.335,34.231,129.337,34.285,129.215,34.321]},{"b":[129.267,34.306,129.48,34.687],"l":[129.451,34.687,129.326,34.607,129.322,34.579,129.329,34.522,129.267,34.37,129.297,34.34,129.365,34.306,129.386,34.354,129.381,34.416,129.475,34.54,129.469,34.616,129.48,34.649,129.472,34.671,129.451,34.687]},{"b":[132.461,34.075,132.578,34.173],"l":[132.578,34.115,132.56,34.127,132.543,34.173,132.524,34.164,132.496,34.122,132.461,34.087,132.549,34.075,132.578,34.115]},{"b":[132.201,33.847,132.445,33.948],"l":[132.36,33.847,132.411,33.88,132.445,33.913,132.43,33.924,132.315,33.909,132.266,33.945,132.208,33.948,132.201,33.928,132.209,33.873,132.267,33.871,132.36,33.847]},{"b":[118.287,24.414,118.451,24.522],"l":[118.339,24.469,118.287,24.477,118.295,24.436,118.433,24.414,118.451,24.456,118.407,24.522,118.339,24.469]},{"b":[118.077,24.436,118.183,24.552],"l":[118.077,24.501,118.089,24.489,118.091,24.446,118.15,24.436,118.183,24.496,118.171,24.519,118.104,24.552,118.093,24.541,118.077,24.501]},{"b":[128.489,34.737,128.741,35.014],"l":[128.722,35.014,128.668,35.009,128.586,34.932,128.489,34.865,128.52,34.82,128.647,34.737,128.741,34.799,128.722,35.014]},{"b":[127.832,34.703,128.066,34.921],"l":[127.832,34.875,127.838,34.813,127.873,34.735,127.897,34.735,127.942,34.766,127.984,34.703,128.055,34.708,128.066,34.806,128.038,34.879,127.966,34.893,127.915,34.921,127.832,34.875]},{"b":[127.737,34.584,127.799,34.682],"l":[127.737,34.631,127.788,34.584,127.799,34.615,127.787,34.682,127.737,34.631]},{"b":[126.166,33.202,126.931,33.553],"l":[126.76,33.553,126.696,33.549,126.338,33.46,126.199,33.368,126.166,33.312,126.179,33.283,126.229,33.225,126.24,33.215,126.282,33.202,126.327,33.224,126.582,33.238,126.709,33.272,126.873,33.341,126.905,33.382,126.931,33.444,126.901,33.515,126.76,33.553]},{"b":[126.109,34.355,126.38,34.563],"l":[126.344,34.545,126.247,34.563,126.227,34.533,126.123,34.444,126.109,34.399,126.134,34.39,126.17,34.355,126.234,34.371,126.335,34.426,126.38,34.498,126.344,34.545]},{"b":[126.369,37.605,126.521,37.823],"l":[126.521,37.737,126.494,37.783,126.412,37.823,126.369,37.772,126.407,37.649,126.423,37.624,126.461,37.61,126.516,37.605,126.521,37.737]},{"b":[121.211,31.464,121.863,31.805],"l":[121.577,31.637,121.542,31.674,121.492,31.694,121.464,31.756,121.339,31.797,121.211,31.805,121.227,31.758,121.336,31.644,121.52,31.55,121.78,31.464,121.863,31.492,121.844,31.526,121.808,31.552,121.577,31.637]},{"b":[121.969,29.944,122.322,30.143],"l":[122.284,30.068,122.111,30.14,121.969,30.143,121.978,30.064,122.024,30.013,122.158,30.001,122.282,29.944,122.296,29.963,122.322,30.031,122.284,30.068]},{"b":[122.043,29.66,122.173,29.782],"l":[122.12,29.782,122.062,29.773,122.043,29.736,122.084,29.725,122.169,29.66,122.173,29.679,122.165,29.701,122.12,29.782]},{"b":[122.332,29.846,122.404,29.955],"l":[122.402,29.95,122.351,29.955,122.332,29.935,122.368,29.853,122.394,29.846,122.404,29.892,122.402,29.95]},{"b":[119.696,25.411,119.839,25.653],"l":[119.797,25.623,119.778,25.653,119.723,25.639,119.696,25.591,119.723,25.551,119.699,25.495,119.7,25.433,119.747,25.411,119.821,25.457,119.832,25.48,119.809,25.508,119.839,25.56,119.838,25.591,119.829,25.607,119.797,25.623]},{"b":[110.265,20.968,110.54,21.093],"l":[110.522,21.083,110.422,21.059,110.385,21.093,110.31,21.075,110.265,21.025,110.281,21.001,110.34,20.998,110.422,21.007,110.504,20.968,110.539,21.018,110.54,21.039,110.522,21.083]},{"b":[107.404,21.092,107.603,21.269],"l":[107.476,21.269,107.453,21.235,107.404,21.094,107.459,21.092,107.603,21.217,107.563,21.22,107.476,21.269]},{"b":[107.399,20.901,107.551,21.034],"l":[107.479,20.952,107.399,20.903,107.466,20.901,107.521,20.927,107.551,20.981,107.551,21.034,107.519,21.013,107.479,20.952]},{"b":[106.911,20.743,107.064,20.867],"l":[107.044,20.837,106.953,20.867,106.911,20.824,106.99,20.743,107.031,20.747,107.042,20.761,107.064,20.8,107.064,20.817,107.044,20.837]},{"b":[106.769,20.796,106.866,20.928],"l":[106.795,20.928,106.769,20.864,106.803,20.844,106.854,20.796,106.866,20.816,106.855,20.858,106.795,20.928]},{"b":[106.568,8.681,106.659,8.766],"l":[106.568,8.701,106.589,8.681,106.617,8.683,106.653,8.701,106.65,8.723,106.659,8.766,106.568,8.701]},{"b":[103.85,10.029,104.083,10.428],"l":[104.028,10.428,103.986,10.427,103.898,10.369,103.85,10.371,103.868,10.335,103.952,10.243,104.018,10.029,104.048,10.061,104.037,10.111,104.076,10.225,104.083,10.341,104.064,10.391,104.028,10.428]},{"b":[102.993,11.275,103.045,11.39],"l":[102.993,11.29,103.011,11.276,103.027,11.275,103.045,11.285,103.037,11.39,103.008,11.383,102.995,11.348,102.993,11.29]},{"b":[102.533,11.572,102.606,11.692],"l":[102.533,11.615,102.59,11.572,102.606,11.677,102.569,11.692,102.546,11.668,102.533,11.615]},{"b":[102.273,11.965,102.43,12.152],"l":[102.319,12.142,102.277,12.152,102.273,12.119,102.302,11.981,102.36,11.974,102.378,11.983,102.43,11.965,102.427,11.989,102.408,12.025,102.378,12.073,102.319,12.142]},{"b":[103.65,1.265,103.996,1.447],"l":[103.818,1.447,103.705,1.423,103.65,1.326,103.82,1.265,103.97,1.331,103.996,1.365,103.961,1.392,103.909,1.416,103.818,1.447]},{"b":[104.129,2.721,104.223,2.872],"l":[104.185,2.872,104.17,2.857,104.129,2.767,104.147,2.728,104.173,2.721,104.222,2.732,104.223,2.774,104.185,2.872]},{"b":[100.191,5.267,100.339,5.468],"l":[100.246,5.468,100.204,5.447,100.191,5.283,100.264,5.267,100.289,5.295,100.339,5.41,100.31,5.438,100.246,5.468]},{"b":[99.646,6.263,99.919,6.466],"l":[99.848,6.466,99.822,6.445,99.749,6.41,99.711,6.427,99.646,6.418,99.657,6.367,99.705,6.338,99.744,6.263,99.783,6.272,99.823,6.313,99.866,6.297,99.883,6.311,99.919,6.359,99.848,6.466]},{"b":[99.607,6.516,99.701,6.714],"l":[99.654,6.714,99.607,6.597,99.644,6.516,99.663,6.522,99.701,6.571,99.654,6.714]},{"b":[99.983,9.68,100.074,9.794],"l":[99.983,9.794,99.998,9.748,100.026,9.712,100.064,9.68,100.074,9.697,100.073,9.749,100.043,9.792,99.983,9.794]},{"b":[99.931,9.422,100.075,9.586],"l":[99.954,9.581,99.94,9.56,99.931,9.476,99.962,9.422,100.054,9.461,100.075,9.529,100.071,9.586,100.043,9.577,99.954,9.581]},{"b":[98.529,7.917,98.604,8.109],"l":[98.58,7.917,98.592,7.934,98.604,8.057,98.529,8.109,98.58,7.917]},{"b":[98.262,7.776,98.435,8.166],"l":[98.399,7.965,98.435,8.086,98.351,8.111,98.322,8.166,98.301,8.136,98.262,7.926,98.296,7.776,98.316,7.782,98.357,7.829,98.398,7.828,98.409,7.902,98.399,7.965]},{"b":[98.251,9.041,98.313,9.139],"l":[98.251,9.041,98.308,9.051,98.313,9.08,98.301,9.139,98.274,9.13,98.258,9.095,98.251,9.041]},{"b":[93.656,6.749,93.93,7.237],"l":[93.859,7.207,93.822,7.237,93.684,7.184,93.656,7.136,93.658,7.016,93.709,7.001,93.829,6.749,93.89,6.831,93.93,6.973,93.859,7.207]},{"b":[93.597,7.262,93.734,7.411],"l":[93.655,7.38,93.614,7.358,93.597,7.319,93.638,7.262,93.734,7.356,93.692,7.411,93.655,7.38]},{"b":[93.064,8.212,93.171,8.349],"l":[93.097,8.349,93.078,8.328,93.064,8.275,93.115,8.219,93.171,8.212,93.141,8.25,93.097,8.349]},{"b":[93.309,7.877,93.447,8.018],"l":[93.375,8.018,93.334,8.007,93.309,7.964,93.342,7.919,93.365,7.877,93.443,7.878,93.447,7.899,93.434,7.948,93.375,8.018]},{"b":[93.456,8.019,93.537,8.225],"l":[93.512,8.16,93.532,8.214,93.494,8.225,93.456,8.172,93.461,8.109,93.47,8.073,93.472,8.053,93.478,8.024,93.49,8.019,93.537,8.057,93.512,8.16]},{"b":[92.713,9.131,92.809,9.244],"l":[92.713,9.205,92.717,9.165,92.744,9.131,92.788,9.137,92.809,9.173,92.786,9.241,92.762,9.244,92.739,9.231,92.713,9.205]},{"b":[92.353,10.521,92.574,10.897],"l":[92.51,10.897,92.448,10.866,92.371,10.794,92.353,10.751,92.377,10.651,92.37,10.547,92.473,10.521,92.503,10.555,92.574,10.704,92.554,10.8,92.51,10.897]},{"b":[92.596,11.361,92.693,11.509],"l":[92.69,11.463,92.64,11.509,92.634,11.427,92.596,11.386,92.645,11.361,92.693,11.381,92.687,11.411,92.69,11.463]},{"b":[92.955,11.899,93.062,12.037],"l":[93.017,12.037,92.996,12.032,92.955,12.002,92.982,11.959,93.062,11.899,93.017,12.037]},{"b":[92.68,12.8,92.731,12.962],"l":[92.68,12.939,92.686,12.8,92.718,12.865,92.731,12.949,92.711,12.962,92.694,12.957,92.68,12.939]},{"b":[92.534,11.513,93.077,13.545],"l":[93.016,13.336,93.077,13.401,93.067,13.436,93.062,13.545,93.029,13.544,92.925,13.486,92.857,13.358,92.86,13.231,92.809,13.04,92.831,13.003,92.807,12.879,92.753,12.821,92.74,12.78,92.759,12.669,92.732,12.616,92.721,12.541,92.719,12.357,92.734,12.336,92.778,12.303,92.788,12.226,92.769,12.216,92.695,12.215,92.676,12.192,92.641,12.112,92.632,12.014,92.608,11.95,92.567,11.931,92.534,11.873,92.56,11.833,92.576,11.718,92.668,11.539,92.701,11.513,92.723,11.536,92.765,11.639,92.767,11.765,92.798,11.875,92.797,11.918,92.764,11.94,92.748,11.993,92.786,12.035,92.799,12.079,92.867,12.181,92.879,12.228,92.864,12.436,92.933,12.453,92.99,12.539,92.965,12.85,92.886,12.942,92.91,12.975,92.951,13.063,93.005,13.089,93.043,13.155,93.066,13.222,93.074,13.252,93.016,13.336]},{"b":[98.477,10.959,98.542,11.087],"l":[98.527,11.087,98.477,10.98,98.498,10.964,98.519,10.959,98.542,10.962,98.527,11.087]},{"b":[98.08,10.74,98.293,10.98],"l":[98.21,10.953,98.167,10.98,98.143,10.963,98.08,10.887,98.155,10.898,98.218,10.838,98.252,10.744,98.271,10.74,98.284,10.753,98.293,10.78,98.21,10.953]},{"b":[98.118,9.875,98.292,10.051],"l":[98.221,10.045,98.14,9.975,98.118,9.878,98.134,9.875,98.183,9.933,98.231,9.954,98.283,10.008,98.292,10.051,98.221,10.045]},{"b":[98.187,11.456,98.308,11.783],"l":[98.239,11.645,98.201,11.567,98.187,11.472,98.209,11.457,98.216,11.456,98.222,11.478,98.263,11.524,98.284,11.594,98.308,11.723,98.3,11.783,98.278,11.758,98.239,11.645]},{"b":[98.454,11.899,98.61,12.084],"l":[98.525,12.005,98.466,12.084,98.454,12.061,98.474,11.899,98.516,11.905,98.576,11.925,98.61,11.957,98.525,12.005]},{"b":[98.01,11.637,98.084,11.86],"l":[98.01,11.86,98.021,11.696,98.084,11.637,98.075,11.692,98.081,11.733,98.06,11.757,98.01,11.86]},{"b":[98.376,11.539,98.554,11.805],"l":[98.376,11.792,98.4,11.715,98.397,11.684,98.435,11.567,98.465,11.567,98.528,11.539,98.554,11.745,98.524,11.805,98.376,11.792]},{"b":[98.037,12.145,98.137,12.292],"l":[98.137,12.15,98.12,12.191,98.118,12.223,98.128,12.261,98.122,12.279,98.105,12.288,98.071,12.292,98.057,12.28,98.037,12.232,98.075,12.164,98.108,12.148,98.125,12.145,98.137,12.15]},{"b":[97.939,12.279,98.066,12.398],"l":[98.045,12.387,97.99,12.394,97.939,12.346,97.952,12.322,98.002,12.279,98.06,12.354,98.066,12.39,98.06,12.398,98.045,12.387]},{"b":[98.303,12.336,98.468,12.678],"l":[98.396,12.647,98.312,12.678,98.303,12.612,98.331,12.511,98.314,12.336,98.334,12.336,98.381,12.354,98.459,12.474,98.468,12.571,98.436,12.571,98.414,12.598,98.396,12.647]},{"b":[98.251,12.935,98.315,13.202],"l":[98.265,13.202,98.255,13.189,98.251,13.104,98.259,13.014,98.309,12.935,98.315,13.099,98.299,13.152,98.269,13.189,98.265,13.202]},{"b":[97.469,16.24,97.6,16.505],"l":[97.579,16.486,97.542,16.505,97.516,16.497,97.469,16.461,97.48,16.306,97.537,16.24,97.575,16.253,97.589,16.397,97.6,16.43,97.593,16.461,97.579,16.486]},{"b":[99.038,7.471,99.104,7.637],"l":[99.038,7.548,99.068,7.496,99.104,7.471,99.078,7.592,99.045,7.637,99.038,7.626,99.038,7.548]},{"b":[94.733,15.794,94.838,15.933],"l":[94.733,15.823,94.743,15.812,94.784,15.794,94.805,15.819,94.838,15.892,94.828,15.933,94.733,15.823]},{"b":[94.388,15.848,94.619,16.206],"l":[94.494,16.075,94.388,15.994,94.412,15.848,94.477,15.946,94.566,16.019,94.619,16.141,94.601,16.206,94.546,16.153,94.494,16.075]},{"b":[93.488,18.676,93.746,18.889],"l":[93.745,18.866,93.618,18.889,93.488,18.868,93.57,18.76,93.674,18.676,93.691,18.684,93.718,18.716,93.746,18.808,93.745,18.866]},{"b":[93.413,19.755,93.513,19.95],"l":[93.492,19.893,93.413,19.95,93.42,19.878,93.445,19.806,93.513,19.755,93.492,19.893]},{"b":[92.913,19.829,93.023,20.086],"l":[92.915,20.086,92.913,20,92.975,19.868,93.023,19.829,93.01,19.924,92.96,20.046,92.915,20.086]},{"b":[93.644,19.299,93.947,19.558],"l":[93.875,19.481,93.829,19.475,93.715,19.558,93.688,19.544,93.644,19.495,93.662,19.459,93.732,19.416,93.756,19.326,93.815,19.299,93.902,19.332,93.934,19.365,93.947,19.408,93.946,19.429,93.875,19.481]},{"b":[91.82,21.75,91.883,21.927],"l":[91.851,21.927,91.835,21.885,91.82,21.81,91.838,21.75,91.874,21.832,91.883,21.884,91.861,21.927,91.851,21.927]},{"b":[91.857,21.503,91.962,21.723],"l":[91.908,21.723,91.857,21.709,91.873,21.574,91.859,21.533,91.889,21.503,91.949,21.508,91.962,21.61,91.949,21.683,91.934,21.722,91.908,21.723]},{"b":[91.045,22.105,91.178,22.52],"l":[91.079,22.52,91.045,22.105,91.151,22.175,91.178,22.283,91.158,22.365,91.079,22.52]},{"b":[91.411,22.353,91.557,22.617],"l":[91.411,22.476,91.467,22.378,91.51,22.353,91.557,22.382,91.548,22.425,91.523,22.491,91.484,22.577,91.456,22.617,91.439,22.599,91.411,22.476]},{"b":[90.536,22.92,90.66,23.035],"l":[90.58,23.035,90.536,23.015,90.562,22.975,90.604,22.946,90.66,22.92,90.642,22.963,90.58,23.035]},{"b":[90.503,22.054,90.868,22.864],"l":[90.683,22.785,90.672,22.813,90.596,22.864,90.503,22.835,90.523,22.748,90.56,22.673,90.565,22.618,90.649,22.541,90.675,22.445,90.68,22.327,90.515,22.065,90.604,22.054,90.778,22.089,90.83,22.16,90.866,22.391,90.868,22.485,90.737,22.639,90.699,22.714,90.683,22.785]},{"b":[79.846,9.62,79.97,9.744],"l":[79.97,9.631,79.912,9.679,79.888,9.741,79.872,9.744,79.859,9.734,79.846,9.715,79.857,9.686,79.907,9.62,79.97,9.631]},{"b":[79.748,8.975,79.904,9.105],"l":[79.904,8.975,79.875,9.051,79.86,9.066,79.748,9.105,79.767,9.07,79.821,9.027,79.904,8.975]},{"b":[-9.099,70.833,-7.979,71.178],"l":[-8.344,71.14,-8.521,71.031,-8.965,70.916,-9.099,70.855,-9.046,70.833,-8.954,70.839,-8.635,70.94,-8.302,70.981,-8.002,71.041,-7.979,71.117,-8.001,71.178,-8.344,71.14]},{"b":[-156.049,18.964,-154.804,20.276],"l":[-154.953,19.645,-154.989,19.732,-155.066,19.748,-155.086,19.876,-155.199,19.994,-155.622,20.163,-155.832,20.276,-155.874,20.26,-155.893,20.167,-155.82,20.014,-155.909,19.895,-155.988,19.832,-156.049,19.75,-155.966,19.591,-155.891,19.383,-155.906,19.126,-155.881,19.071,-155.681,18.968,-155.626,18.964,-155.581,19.012,-155.535,19.109,-155.31,19.26,-155.053,19.319,-154.85,19.454,-154.804,19.524,-154.841,19.568,-154.953,19.645]},{"b":[-157.29,21.056,-156.712,21.23],"l":[-156.917,21.177,-156.952,21.2,-157.002,21.188,-157.214,21.215,-157.25,21.23,-157.254,21.181,-157.279,21.152,-157.29,21.113,-157.021,21.098,-156.86,21.056,-156.748,21.104,-156.712,21.155,-156.742,21.164,-156.917,21.177]},{"b":[-156.698,20.599,-155.99,21.034],"l":[-156.104,20.84,-156.148,20.885,-156.278,20.951,-156.354,20.941,-156.461,20.915,-156.487,20.933,-156.532,20.993,-156.585,21.034,-156.657,21.025,-156.698,20.949,-156.69,20.901,-156.615,20.822,-156.544,20.79,-156.48,20.801,-156.449,20.706,-156.438,20.618,-156.409,20.605,-156.31,20.599,-156.235,20.629,-156.107,20.645,-156.014,20.715,-155.99,20.757,-156.019,20.792,-156.104,20.84]},{"b":[-160.243,21.797,-160.049,22.015],"l":[-160.221,21.897,-160.243,21.843,-160.235,21.804,-160.2,21.797,-160.18,21.841,-160.153,21.879,-160.08,21.907,-160.077,21.958,-160.049,22.005,-160.101,22.015,-160.164,21.944,-160.221,21.897]},{"b":[-158.273,21.269,-157.635,21.701],"l":[-157.83,21.471,-157.854,21.512,-157.852,21.553,-157.962,21.701,-158.02,21.692,-158.123,21.6,-158.273,21.585,-158.239,21.533,-158.239,21.489,-158.138,21.377,-158.11,21.319,-158.079,21.312,-157.981,21.316,-158.017,21.368,-157.978,21.379,-157.968,21.367,-157.958,21.327,-157.902,21.341,-157.849,21.291,-157.799,21.269,-157.691,21.28,-157.635,21.308,-157.654,21.334,-157.706,21.378,-157.721,21.458,-157.765,21.451,-157.799,21.457,-157.83,21.471]},{"b":[-159.789,21.876,-159.301,22.223],"l":[-159.579,22.223,-159.727,22.14,-159.789,22.042,-159.748,21.99,-159.646,21.952,-159.609,21.91,-159.512,21.9,-159.461,21.876,-159.373,21.932,-159.344,21.974,-159.33,22.051,-159.301,22.105,-159.305,22.154,-159.352,22.22,-159.579,22.223]},{"b":[-157.051,20.744,-156.809,20.93],"l":[-156.942,20.93,-157.051,20.912,-156.988,20.826,-156.973,20.758,-156.909,20.744,-156.85,20.773,-156.809,20.831,-156.848,20.878,-156.881,20.905,-156.942,20.93]},{"b":[178.648,51.372,179.452,51.656],"l":[179.182,51.47,179.084,51.528,178.908,51.616,178.692,51.656,178.648,51.644,178.747,51.587,178.926,51.535,179.278,51.372,179.452,51.373,179.416,51.401,179.294,51.421,179.182,51.47]},{"b":[179.498,51.88,179.78,52.03],"l":[179.627,52.03,179.504,51.98,179.498,51.933,179.55,51.894,179.645,51.88,179.728,51.905,179.78,51.967,179.627,52.03]},{"b":[177.25,51.841,177.67,52.114],"l":[177.564,52.11,177.521,52.018,177.478,51.992,177.381,51.976,177.25,51.903,177.261,51.884,177.329,51.841,177.415,51.883,177.594,51.948,177.596,51.994,177.653,52.06,177.67,52.103,177.637,52.114,177.564,52.11]},{"b":[173.402,52.357,173.776,52.504],"l":[173.658,52.504,173.517,52.451,173.425,52.438,173.402,52.405,173.616,52.391,173.658,52.357,173.723,52.36,173.745,52.447,173.776,52.495,173.658,52.504]},{"b":[172.495,52.752,173.436,53.013],"l":[173.436,52.852,173.252,52.943,173.102,52.996,172.984,52.98,172.812,53.013,172.678,53.008,172.495,52.938,172.595,52.907,172.722,52.886,172.776,52.797,172.935,52.752,173.08,52.814,173.159,52.811,173.303,52.826,173.348,52.825,173.395,52.835,173.436,52.852]},{"b":[-173.074,60.299,-172.232,60.607],"l":[-172.387,60.398,-172.526,60.392,-172.742,60.457,-172.86,60.506,-172.924,60.607,-173.048,60.568,-173.074,60.493,-172.958,60.463,-172.636,60.329,-172.397,60.331,-172.232,60.299,-172.278,60.344,-172.387,60.398]},{"b":[-170.387,57.137,-170.116,57.242],"l":[-170.387,57.203,-170.386,57.189,-170.358,57.154,-170.264,57.137,-170.161,57.184,-170.116,57.242,-170.387,57.203]},{"b":[-160.517,55.259,-160.329,55.357],"l":[-160.493,55.352,-160.517,55.334,-160.481,55.309,-160.343,55.259,-160.329,55.338,-160.362,55.357,-160.493,55.352]},{"b":[-160.847,55.145,-160.483,55.405],"l":[-160.583,55.308,-160.552,55.363,-160.553,55.381,-160.574,55.378,-160.639,55.322,-160.67,55.314,-160.685,55.315,-160.666,55.359,-160.672,55.379,-160.696,55.398,-160.724,55.405,-160.789,55.383,-160.84,55.335,-160.847,55.311,-160.825,55.174,-160.795,55.145,-160.751,55.171,-160.702,55.178,-160.609,55.159,-160.488,55.185,-160.483,55.197,-160.531,55.233,-160.583,55.308]},{"b":[-162.434,54.842,-162.234,54.983],"l":[-162.434,54.932,-162.416,54.896,-162.391,54.873,-162.322,54.842,-162.298,54.847,-162.273,54.867,-162.234,54.932,-162.238,54.955,-162.265,54.983,-162.294,54.983,-162.434,54.932]},{"b":[-162.821,54.38,-162.554,54.495],"l":[-162.821,54.495,-162.812,54.444,-162.733,54.402,-162.641,54.38,-162.554,54.401,-162.608,54.447,-162.645,54.462,-162.821,54.495]},{"b":[-159.462,54.967,-159.362,55.059],"l":[-159.462,55.059,-159.458,55.035,-159.421,54.978,-159.394,54.967,-159.362,54.972,-159.363,55,-159.39,55.041,-159.462,55.059]},{"b":[-160.227,54.923,-159.839,55.273],"l":[-159.898,55.221,-159.871,55.264,-159.887,55.273,-159.92,55.268,-159.982,55.198,-160.039,55.193,-160.102,55.134,-160.134,55.12,-160.172,55.123,-160.152,55.057,-160.154,55.038,-160.164,55.01,-160.227,54.923,-160.17,54.942,-160.038,55.044,-159.999,55.067,-159.953,55.079,-159.934,55.107,-159.873,55.129,-159.854,55.145,-159.839,55.182,-159.898,55.221]},{"b":[-159.648,55.057,-159.515,55.226],"l":[-159.515,55.152,-159.545,55.226,-159.575,55.218,-159.595,55.182,-159.588,55.165,-159.598,55.126,-159.64,55.124,-159.635,55.102,-159.648,55.075,-159.618,55.057,-159.561,55.081,-159.535,55.06,-159.52,55.072,-159.515,55.152]},{"b":[-169.992,52.792,-169.692,52.884],"l":[-169.983,52.851,-169.992,52.83,-169.981,52.806,-169.877,52.814,-169.723,52.792,-169.708,52.807,-169.692,52.847,-169.711,52.867,-169.755,52.884,-169.821,52.883,-169.983,52.851]},{"b":[-170.827,52.55,-170.585,52.698],"l":[-170.682,52.698,-170.791,52.631,-170.827,52.601,-170.816,52.562,-170.797,52.55,-170.733,52.581,-170.692,52.593,-170.649,52.593,-170.614,52.61,-170.587,52.642,-170.585,52.668,-170.608,52.685,-170.682,52.698]},{"b":[-166.106,54.047,-165.693,54.211],"l":[-165.764,54.152,-165.893,54.207,-165.966,54.211,-166.041,54.191,-166.088,54.169,-166.106,54.145,-166.103,54.114,-166.057,54.054,-166.036,54.047,-165.933,54.059,-165.91,54.049,-165.879,54.053,-165.842,54.071,-165.738,54.081,-165.693,54.1,-165.704,54.12,-165.764,54.152]},{"b":[-165.654,54.129,-165.408,54.285],"l":[-165.442,54.208,-165.488,54.222,-165.534,54.274,-165.551,54.285,-165.59,54.279,-165.654,54.253,-165.621,54.184,-165.615,54.14,-165.605,54.129,-165.561,54.137,-165.468,54.181,-165.408,54.197,-165.442,54.208]},{"b":[-169.089,52.832,-167.805,53.558],"l":[-167.805,53.485,-167.828,53.508,-167.986,53.558,-168.073,53.557,-168.193,53.533,-168.288,53.5,-168.357,53.458,-168.396,53.409,-168.405,53.354,-168.397,53.322,-168.363,53.304,-168.38,53.283,-168.437,53.257,-168.572,53.266,-168.639,53.256,-168.69,53.227,-168.76,53.175,-168.778,53.149,-168.783,53.079,-168.796,53.045,-168.836,53.02,-168.909,52.951,-168.974,52.91,-169.073,52.864,-169.089,52.832,-169.066,52.834,-168.741,52.957,-168.699,52.963,-168.597,53.016,-168.549,53.036,-168.506,53.043,-168.446,53.084,-168.37,53.16,-168.271,53.238,-167.964,53.345,-167.865,53.387,-167.843,53.435,-167.805,53.485]},{"b":[-167.809,53.259,-166.231,54.006],"l":[-166.522,53.61,-166.444,53.652,-166.355,53.674,-166.309,53.698,-166.339,53.718,-166.385,53.721,-166.549,53.701,-166.546,53.726,-166.489,53.785,-166.319,53.874,-166.231,53.933,-166.336,53.971,-166.372,53.999,-166.4,53.978,-166.443,53.925,-166.497,53.884,-166.572,53.853,-166.615,53.901,-166.627,53.996,-166.673,54.006,-166.734,54.002,-166.849,53.978,-166.978,53.963,-167.038,53.942,-167.09,53.906,-167.118,53.873,-167.121,53.843,-167.106,53.813,-167.071,53.783,-167.027,53.769,-166.973,53.771,-166.89,53.759,-166.777,53.733,-166.741,53.713,-166.804,53.685,-166.809,53.646,-166.819,53.641,-166.838,53.648,-166.894,53.697,-167.016,53.698,-167.042,53.655,-167.092,53.636,-167.136,53.526,-167.204,53.495,-167.424,53.437,-167.53,53.394,-167.639,53.387,-167.71,53.371,-167.809,53.324,-167.781,53.3,-167.669,53.26,-167.629,53.259,-167.592,53.273,-167.522,53.276,-167.48,53.292,-167.429,53.326,-167.381,53.342,-167.337,53.341,-167.3,53.35,-167.271,53.371,-167.154,53.408,-166.961,53.447,-166.851,53.453,-166.77,53.476,-166.702,53.537,-166.522,53.61]},{"b":[-166.251,53.72,-166.103,53.843],"l":[-166.234,53.784,-166.251,53.768,-166.249,53.745,-166.224,53.72,-166.21,53.723,-166.184,53.757,-166.139,53.787,-166.103,53.833,-166.114,53.843,-166.155,53.836,-166.188,53.822,-166.234,53.784]},{"b":[-172.62,52.257,-172.314,52.388],"l":[-172.47,52.388,-172.544,52.354,-172.582,52.326,-172.62,52.273,-172.539,52.257,-172.465,52.272,-172.314,52.33,-172.383,52.373,-172.47,52.388]},{"b":[-173.992,52.042,-173.023,52.144],"l":[-173.357,52.096,-173.553,52.136,-173.657,52.144,-173.779,52.118,-173.794,52.104,-173.939,52.131,-173.992,52.123,-173.99,52.104,-173.93,52.072,-173.879,52.054,-173.836,52.048,-173.673,52.063,-173.461,52.042,-173.368,52.046,-173.232,52.068,-173.179,52.063,-173.023,52.079,-173.024,52.091,-173.113,52.1,-173.357,52.096]},{"b":[-175.296,51.994,-174.018,52.42],"l":[-174.169,52.42,-174.307,52.378,-174.365,52.342,-174.436,52.317,-174.406,52.296,-174.259,52.269,-174.306,52.216,-174.474,52.184,-174.668,52.135,-174.916,52.094,-175.118,52.047,-175.214,52.038,-175.296,52.022,-175.214,51.994,-174.677,52.035,-174.344,52.078,-174.121,52.135,-174.179,52.2,-174.163,52.223,-174.055,52.246,-174.03,52.29,-174.018,52.332,-174.046,52.367,-174.169,52.42]},{"b":[-176.204,51.79,-176.009,51.886],"l":[-176.194,51.886,-176.204,51.835,-176.093,51.79,-176.009,51.812,-176.072,51.843,-176.194,51.886]},{"b":[-176.414,51.733,-176.28,51.861],"l":[-176.287,51.792,-176.28,51.803,-176.379,51.861,-176.414,51.841,-176.396,51.76,-176.35,51.733,-176.287,51.792]},{"b":[-176.962,51.604,-176.437,51.986],"l":[-176.588,51.833,-176.593,51.867,-176.552,51.92,-176.55,51.944,-176.597,51.982,-176.698,51.986,-176.745,51.895,-176.736,51.84,-176.774,51.819,-176.874,51.79,-176.962,51.604,-176.837,51.676,-176.771,51.63,-176.558,51.712,-176.511,51.746,-176.47,51.731,-176.452,51.736,-176.437,51.754,-176.437,51.82,-176.473,51.837,-176.588,51.833]},{"b":[-176.185,51.973,-175.975,52.1],"l":[-176.045,51.973,-176.022,52.002,-175.975,52.029,-175.988,52.049,-176.031,52.082,-176.077,52.1,-176.156,52.099,-176.185,52.056,-176.178,52.03,-176.143,52.004,-176.045,51.973]},{"b":[-177.67,51.677,-177.063,51.93],"l":[-177.21,51.841,-177.257,51.805,-177.335,51.776,-177.668,51.721,-177.67,51.701,-177.655,51.677,-177.578,51.694,-177.475,51.701,-177.382,51.705,-177.23,51.694,-177.177,51.704,-177.148,51.717,-177.135,51.807,-177.121,51.836,-177.08,51.867,-177.063,51.902,-177.11,51.929,-177.131,51.93,-177.166,51.909,-177.21,51.841]},{"b":[-178.195,51.616,-177.644,51.918],"l":[-177.644,51.826,-177.8,51.84,-177.866,51.86,-177.954,51.918,-178.117,51.916,-178.168,51.903,-178.195,51.882,-178.153,51.848,-178.045,51.801,-177.986,51.764,-177.977,51.738,-178,51.717,-178.078,51.691,-178.059,51.673,-177.925,51.617,-177.901,51.616,-177.879,51.65,-177.827,51.686,-177.771,51.778,-177.725,51.802,-177.644,51.826]},{"b":[73.251,-53.185,73.838,-52.966],"l":[73.586,-53.027,73.388,-53,73.305,-52.966,73.251,-52.976,73.254,-52.989,73.285,-53.021,73.336,-53.03,73.413,-53.147,73.465,-53.184,73.588,-53.185,73.707,-53.137,73.795,-53.13,73.838,-53.113,73.731,-53.091,73.586,-53.027]},{"b":[69.15,-49.067,69.395,-48.879],"l":[69.221,-49.067,69.282,-49.059,69.321,-49.034,69.395,-48.951,69.369,-48.89,69.266,-48.879,69.167,-48.883,69.15,-48.919,69.17,-48.957,69.204,-48.991,69.202,-49.034,69.221,-49.067]},{"b":[51.659,-46.449,51.835,-46.327],"l":[51.742,-46.327,51.659,-46.374,51.697,-46.428,51.762,-46.449,51.835,-46.44,51.815,-46.395,51.784,-46.359,51.742,-46.327]},{"b":[37.59,-46.963,37.888,-46.824],"l":[37.59,-46.908,37.612,-46.946,37.814,-46.963,37.857,-46.944,37.888,-46.902,37.873,-46.885,37.79,-46.837,37.685,-46.824,37.65,-46.849,37.59,-46.908]},{"b":[27.716,35.908,28.232,36.434],"l":[27.914,36.345,27.815,36.277,27.774,36.214,27.716,36.172,27.719,36.141,27.757,36.069,27.716,35.957,27.746,35.911,27.771,35.908,27.843,35.929,27.966,36.048,28.088,36.065,28.068,36.13,28.144,36.21,28.23,36.37,28.232,36.434,28.171,36.426,27.914,36.345]},{"b":[27.071,35.409,27.234,35.82],"l":[27.157,35.629,27.207,35.714,27.223,35.82,27.158,35.789,27.156,35.726,27.071,35.598,27.116,35.511,27.099,35.456,27.138,35.409,27.176,35.465,27.234,35.479,27.209,35.559,27.157,35.629]},{"b":[27.786,36.538,27.87,36.641],"l":[27.863,36.641,27.837,36.635,27.786,36.608,27.788,36.584,27.838,36.538,27.86,36.554,27.869,36.583,27.87,36.623,27.863,36.641]},{"b":[26.889,36.945,27.04,37.087],"l":[27.04,37.002,27.016,37.01,26.967,37.052,26.889,37.087,26.938,37.025,26.92,36.945,27.02,36.959,27.035,36.976,27.04,37.002]},{"b":[26.918,36.726,27.352,36.905],"l":[27.061,36.84,26.956,36.774,26.918,36.726,26.95,36.727,27.034,36.771,27.151,36.778,27.193,36.809,27.352,36.869,27.266,36.905,27.215,36.899,27.061,36.84]},{"b":[25.362,36.93,25.588,37.196],"l":[25.362,37.07,25.396,36.984,25.457,36.93,25.546,36.968,25.584,37.039,25.588,37.153,25.564,37.185,25.525,37.196,25.362,37.07]},{"b":[25.105,36.991,25.279,37.149],"l":[25.275,37.138,25.235,37.149,25.146,37.107,25.105,37.035,25.133,37,25.199,36.991,25.279,37.068,25.271,37.084,25.275,37.138]},{"b":[25.371,36.34,25.482,36.474],"l":[25.415,36.442,25.413,36.405,25.397,36.379,25.371,36.359,25.436,36.34,25.482,36.393,25.467,36.435,25.409,36.474,25.397,36.465,25.415,36.442]},{"b":[25.26,36.658,25.407,36.789],"l":[25.364,36.658,25.382,36.674,25.407,36.717,25.296,36.789,25.26,36.758,25.289,36.722,25.364,36.658]},{"b":[26.581,37.645,27.055,37.811],"l":[26.982,37.782,26.947,37.778,26.824,37.811,26.743,37.81,26.639,37.781,26.581,37.724,26.613,37.71,26.721,37.705,26.788,37.657,26.845,37.645,26.978,37.7,27.055,37.709,27.04,37.77,26.982,37.782]},{"b":[25.982,37.526,26.351,37.674],"l":[26.212,37.638,26.086,37.635,25.997,37.566,25.982,37.526,26.029,37.529,26.205,37.569,26.297,37.62,26.351,37.674,26.326,37.673,26.212,37.638]},{"b":[25.743,36.782,26.064,36.937],"l":[25.942,36.887,25.852,36.848,25.834,36.825,25.797,36.807,25.743,36.79,25.771,36.782,25.859,36.79,25.985,36.88,26.064,36.903,26.001,36.937,25.942,36.887]},{"b":[26.27,36.511,26.461,36.639],"l":[26.337,36.581,26.27,36.595,26.27,36.547,26.331,36.511,26.382,36.562,26.461,36.585,26.421,36.624,26.37,36.639,26.384,36.608,26.337,36.581]},{"b":[24.277,37.528,24.401,37.683],"l":[24.277,37.601,24.289,37.528,24.356,37.577,24.401,37.649,24.379,37.683,24.32,37.678,24.277,37.601]},{"b":[24.37,37.314,24.481,37.475],"l":[24.398,37.383,24.379,37.314,24.436,37.344,24.481,37.408,24.449,37.45,24.431,37.475,24.395,37.45,24.37,37.42,24.398,37.383]},{"b":[24.326,36.656,24.538,36.764],"l":[24.425,36.713,24.357,36.744,24.345,36.723,24.326,36.656,24.531,36.684,24.538,36.705,24.536,36.764,24.46,36.747,24.45,36.729,24.425,36.713]},{"b":[24.895,37.39,24.943,37.509],"l":[24.943,37.494,24.907,37.509,24.895,37.446,24.896,37.407,24.912,37.391,24.938,37.39,24.943,37.494]},{"b":[24.7,37.692,24.992,37.99],"l":[24.948,37.858,24.956,37.905,24.855,37.914,24.79,37.99,24.763,37.988,24.7,37.962,24.714,37.899,24.767,37.871,24.799,37.824,24.884,37.771,24.962,37.692,24.992,37.76,24.98,37.797,24.948,37.858]},{"b":[24.996,37.535,25.256,37.681],"l":[25.156,37.545,25.219,37.535,25.256,37.6,25.225,37.631,25.092,37.648,25.039,37.681,24.996,37.677,25.016,37.646,25.052,37.614,25.156,37.545]},{"b":[24.671,36.917,24.763,37.024],"l":[24.676,36.959,24.703,36.917,24.721,36.921,24.763,36.949,24.716,37.024,24.681,37.022,24.671,36.999,24.676,36.959]},{"b":[25.846,38.162,26.16,38.602],"l":[26.013,38.602,25.846,38.574,25.851,38.508,25.96,38.416,25.991,38.354,25.953,38.303,25.874,38.27,25.892,38.243,25.999,38.162,26.094,38.218,26.103,38.234,26.111,38.28,26.157,38.303,26.15,38.468,26.141,38.486,26.16,38.541,26.11,38.545,26.013,38.602]},{"b":[25.844,38.973,26.596,39.383],"l":[26.531,39.172,26.393,39.27,26.41,39.329,26.348,39.383,26.165,39.374,26.165,39.332,26.088,39.304,26.026,39.285,25.91,39.288,25.844,39.2,25.855,39.179,25.906,39.139,26.072,39.096,26.176,39.194,26.273,39.198,26.245,39.164,26.108,39.081,26.161,39.026,26.39,38.974,26.469,38.973,26.547,38.994,26.503,39.031,26.489,39.075,26.532,39.064,26.584,39.031,26.596,39.049,26.578,39.11,26.531,39.172]},{"b":[23.594,39.081,23.78,39.209],"l":[23.735,39.081,23.78,39.114,23.594,39.209,23.666,39.095,23.735,39.081]},{"b":[23.841,39.147,23.971,39.268],"l":[23.971,39.268,23.888,39.226,23.841,39.147,23.888,39.158,23.94,39.201,23.971,39.268]},{"b":[24.461,38.785,24.675,38.98],"l":[24.581,38.879,24.564,38.942,24.486,38.98,24.473,38.962,24.461,38.889,24.566,38.832,24.565,38.819,24.541,38.789,24.569,38.785,24.675,38.809,24.581,38.879]},{"b":[25.448,40.4,25.686,40.516],"l":[25.569,40.516,25.448,40.483,25.573,40.4,25.686,40.427,25.664,40.464,25.624,40.492,25.569,40.516]},{"b":[25.052,39.806,25.449,40.035],"l":[25.374,40.016,25.348,39.985,25.286,39.956,25.234,40.005,25.058,40,25.052,39.976,25.065,39.91,25.062,39.852,25.126,39.826,25.185,39.83,25.203,39.849,25.224,39.893,25.249,39.894,25.252,39.854,25.263,39.823,25.299,39.806,25.357,39.808,25.372,39.891,25.4,39.95,25.438,39.983,25.449,40.035,25.374,40.016]},{"b":[24.516,40.579,24.786,40.793],"l":[24.623,40.793,24.586,40.769,24.517,40.687,24.516,40.647,24.646,40.579,24.774,40.615,24.769,40.659,24.786,40.704,24.774,40.73,24.719,40.786,24.623,40.793]},{"b":[25.669,40.105,25.977,40.238],"l":[25.741,40.196,25.669,40.136,25.74,40.105,25.97,40.136,25.977,40.178,25.918,40.238,25.875,40.234,25.741,40.196]},{"b":[-16.236,11.045,-16.053,11.199],"l":[-16.067,11.197,-16.087,11.199,-16.105,11.191,-16.144,11.167,-16.176,11.131,-16.195,11.13,-16.236,11.113,-16.231,11.094,-16.195,11.045,-16.115,11.059,-16.072,11.084,-16.053,11.118,-16.067,11.197]},{"b":[-16.477,19.609,-16.344,19.866],"l":[-16.42,19.802,-16.477,19.71,-16.466,19.646,-16.438,19.609,-16.373,19.706,-16.344,19.866,-16.393,19.849,-16.42,19.802]},{"b":[-17.241,32.648,-16.693,32.869],"l":[-16.693,32.758,-16.774,32.774,-16.929,32.841,-17.054,32.816,-17.191,32.869,-17.241,32.807,-17.226,32.767,-17.171,32.722,-17.018,32.663,-16.837,32.648,-16.765,32.71,-16.693,32.758]},{"b":[-15.964,11.055,-15.896,11.193],"l":[-15.963,11.095,-15.951,11.087,-15.964,11.059,-15.905,11.055,-15.896,11.082,-15.905,11.148,-15.909,11.161,-15.938,11.193,-15.946,11.18,-15.963,11.095]},{"b":[-16.905,28.007,-16.119,28.576],"l":[-16.334,28.38,-16.119,28.528,-16.124,28.576,-16.319,28.558,-16.517,28.413,-16.557,28.4,-16.752,28.37,-16.843,28.376,-16.905,28.34,-16.866,28.293,-16.795,28.149,-16.658,28.007,-16.543,28.032,-16.496,28.062,-16.418,28.151,-16.334,28.38]},{"b":[-13.86,28.845,-13.423,29.237],"l":[-13.535,29.144,-13.65,29.119,-13.788,29.056,-13.824,29.013,-13.86,28.869,-13.784,28.845,-13.716,28.911,-13.555,28.96,-13.478,29.007,-13.454,29.151,-13.423,29.198,-13.464,29.237,-13.501,29.211,-13.535,29.144]},{"b":[-14.492,28.056,-13.827,28.745],"l":[-14.028,28.617,-14.153,28.407,-14.232,28.216,-14.356,28.13,-14.492,28.101,-14.469,28.082,-14.333,28.056,-14.197,28.169,-13.928,28.253,-13.863,28.409,-13.828,28.585,-13.827,28.691,-13.857,28.738,-13.886,28.745,-13.954,28.741,-14.003,28.707,-14.028,28.617]},{"b":[-12.952,7.41,-12.501,7.637],"l":[-12.615,7.637,-12.854,7.622,-12.952,7.571,-12.607,7.475,-12.541,7.41,-12.526,7.436,-12.501,7.535,-12.512,7.582,-12.544,7.607,-12.615,7.637]},{"b":[-15.809,27.747,-15.383,28.159],"l":[-15.401,28.147,-15.415,28.159,-15.433,28.154,-15.453,28.137,-15.683,28.154,-15.721,28.064,-15.809,27.994,-15.807,27.888,-15.71,27.784,-15.656,27.758,-15.559,27.747,-15.437,27.811,-15.389,27.875,-15.383,27.993,-15.407,28.071,-15.401,28.147]},{"b":[-15.78,11.175,-15.658,11.302],"l":[-15.717,11.302,-15.755,11.269,-15.78,11.195,-15.767,11.182,-15.725,11.175,-15.725,11.215,-15.687,11.234,-15.667,11.258,-15.658,11.286,-15.672,11.296,-15.717,11.302]},{"b":[-15.62,11.514,-15.482,11.632],"l":[-15.537,11.618,-15.62,11.533,-15.563,11.514,-15.553,11.537,-15.526,11.554,-15.484,11.568,-15.482,11.632,-15.537,11.618]},{"b":[-25.342,16.926,-24.98,17.194],"l":[-24.98,17.095,-25.035,17.176,-25.113,17.194,-25.337,17.091,-25.342,17.068,-25.322,17.015,-25.308,16.936,-25.267,16.926,-25.17,16.946,-25.017,17.049,-24.98,17.095]},{"b":[-25.093,16.794,-24.887,16.922],"l":[-24.991,16.913,-25.07,16.871,-25.093,16.833,-25.02,16.797,-24.969,16.794,-24.887,16.818,-24.892,16.846,-24.936,16.922,-24.991,16.913]},{"b":[-17.325,28.014,-17.101,28.203],"l":[-17.325,28.118,-17.274,28.038,-17.225,28.014,-17.185,28.022,-17.101,28.083,-17.104,28.111,-17.13,28.156,-17.214,28.199,-17.259,28.203,-17.29,28.176,-17.325,28.118]},{"b":[-16.023,11.434,-15.902,11.598],"l":[-15.949,11.434,-15.902,11.466,-15.915,11.589,-15.965,11.598,-16.019,11.527,-16.023,11.477,-15.997,11.449,-15.949,11.434]},{"b":[-16.152,11.76,-15.986,11.887],"l":[-16.102,11.773,-16.038,11.76,-15.986,11.882,-16.022,11.887,-16.152,11.877,-16.147,11.846,-16.102,11.773]},{"b":[-18.161,27.646,-17.888,27.85],"l":[-18.043,27.768,-18.161,27.761,-18.136,27.728,-18.107,27.707,-17.985,27.646,-17.888,27.81,-17.925,27.85,-18.043,27.768]},{"b":[-22.959,15.986,-22.682,16.237],"l":[-22.693,16.169,-22.749,16.222,-22.803,16.226,-22.834,16.219,-22.918,16.237,-22.916,16.148,-22.959,16.045,-22.884,15.993,-22.821,15.986,-22.71,16.043,-22.682,16.113,-22.693,16.169]},{"b":[-24.517,14.818,-24.296,15.038],"l":[-24.329,15.019,-24.392,15.038,-24.497,14.98,-24.517,14.931,-24.492,14.874,-24.441,14.835,-24.386,14.818,-24.308,14.856,-24.296,14.93,-24.329,15.019]},{"b":[-18.001,28.486,-17.727,28.847],"l":[-17.929,28.845,-18.001,28.758,-17.882,28.565,-17.859,28.486,-17.834,28.493,-17.758,28.569,-17.744,28.616,-17.752,28.689,-17.727,28.724,-17.745,28.787,-17.798,28.847,-17.929,28.845]},{"b":[-24.398,16.493,-24.033,16.678],"l":[-24.271,16.645,-24.377,16.678,-24.393,16.664,-24.398,16.618,-24.322,16.493,-24.283,16.576,-24.243,16.599,-24.094,16.561,-24.033,16.572,-24.046,16.593,-24.088,16.623,-24.271,16.645]},{"b":[-22.991,16.608,-22.888,16.844],"l":[-22.904,16.732,-22.905,16.844,-22.933,16.841,-22.991,16.809,-22.981,16.701,-22.959,16.683,-22.92,16.608,-22.888,16.659,-22.904,16.732]},{"b":[-23.252,15.133,-23.116,15.324],"l":[-23.21,15.324,-23.247,15.257,-23.242,15.241,-23.252,15.178,-23.21,15.133,-23.182,15.137,-23.116,15.167,-23.119,15.268,-23.138,15.318,-23.21,15.324]},{"b":[-23.785,14.916,-23.444,15.329],"l":[-23.535,15.139,-23.58,15.161,-23.701,15.272,-23.707,15.317,-23.748,15.329,-23.759,15.311,-23.754,15.244,-23.783,15.166,-23.785,15.077,-23.705,14.961,-23.637,14.923,-23.505,14.916,-23.444,15.008,-23.535,15.139]},{"b":[44.22,-12.357,44.527,-12.071],"l":[44.476,-12.082,44.452,-12.071,44.413,-12.093,44.407,-12.12,44.379,-12.166,44.334,-12.173,44.292,-12.165,44.22,-12.171,44.377,-12.252,44.46,-12.335,44.505,-12.357,44.526,-12.324,44.527,-12.22,44.476,-12.082]},{"b":[43.631,-12.368,43.859,-12.247],"l":[43.704,-12.256,43.631,-12.247,43.633,-12.288,43.664,-12.343,43.859,-12.368,43.789,-12.307,43.704,-12.256]},{"b":[45.043,-12.985,45.223,-12.653],"l":[45.135,-12.709,45.092,-12.653,45.043,-12.701,45.094,-12.786,45.088,-12.835,45.069,-12.896,45.088,-12.958,45.118,-12.985,45.18,-12.977,45.179,-12.92,45.209,-12.848,45.204,-12.824,45.223,-12.752,45.159,-12.713,45.135,-12.709]},{"b":[43.227,-11.915,43.492,-11.368],"l":[43.379,-11.614,43.393,-11.409,43.342,-11.368,43.299,-11.375,43.281,-11.391,43.256,-11.432,43.227,-11.752,43.303,-11.844,43.355,-11.858,43.447,-11.915,43.466,-11.901,43.492,-11.862,43.448,-11.753,43.379,-11.614]},{"b":[39.603,-7.991,39.907,-7.649],"l":[39.847,-7.73,39.717,-7.832,39.661,-7.901,39.603,-7.936,39.636,-7.978,39.657,-7.991,39.711,-7.977,39.762,-7.912,39.824,-7.901,39.898,-7.728,39.907,-7.649,39.891,-7.663,39.847,-7.73]},{"b":[40.957,-2.167,41.139,-2.037],"l":[40.976,-2.11,40.957,-2.167,40.994,-2.158,41.118,-2.1,41.137,-2.085,41.139,-2.07,41.131,-2.053,41.086,-2.037,40.976,-2.11]},{"b":[39.182,-6.454,39.573,-5.722],"l":[39.488,-6.166,39.433,-6.115,39.368,-5.951,39.357,-5.812,39.309,-5.722,39.267,-5.853,39.192,-5.931,39.206,-6.083,39.182,-6.173,39.243,-6.275,39.313,-6.279,39.383,-6.365,39.424,-6.348,39.447,-6.42,39.481,-6.454,39.509,-6.452,39.563,-6.427,39.573,-6.387,39.496,-6.175,39.488,-6.166]},{"b":[57.318,-20.513,57.792,-19.99],"l":[57.737,-20.098,57.657,-19.99,57.576,-19.997,57.515,-20.056,57.486,-20.144,57.416,-20.184,57.386,-20.229,57.362,-20.338,57.365,-20.406,57.318,-20.428,57.328,-20.45,57.383,-20.504,57.525,-20.513,57.651,-20.485,57.707,-20.435,57.725,-20.369,57.781,-20.327,57.792,-20.213,57.737,-20.098]},{"b":[55.233,-21.369,55.839,-20.865],"l":[55.662,-20.906,55.596,-20.88,55.45,-20.865,55.311,-20.904,55.25,-21.002,55.233,-21.058,55.31,-21.217,55.363,-21.274,55.558,-21.358,55.656,-21.369,55.797,-21.339,55.822,-21.278,55.839,-21.139,55.739,-21.021,55.662,-20.906]},{"b":[48.191,-13.4,48.351,-13.198],"l":[48.351,-13.31,48.309,-13.198,48.27,-13.205,48.256,-13.256,48.191,-13.26,48.212,-13.385,48.344,-13.4,48.342,-13.364,48.351,-13.31]},{"b":[49.824,-17.087,50.023,-16.695],"l":[49.856,-16.933,49.824,-17.087,49.936,-16.903,50.023,-16.695,49.986,-16.712,49.856,-16.933]},{"b":[-5.783,-16.004,-5.66,-15.906],"l":[-5.775,-15.957,-5.783,-16.004,-5.692,-15.998,-5.66,-15.971,-5.662,-15.913,-5.708,-15.906,-5.775,-15.957]},{"b":[39.647,-5.444,39.871,-4.906],"l":[39.749,-5.444,39.796,-5.394,39.853,-5.255,39.859,-5.155,39.856,-5.004,39.871,-4.957,39.865,-4.906,39.781,-4.945,39.673,-4.927,39.701,-5.114,39.647,-5.369,39.673,-5.407,39.708,-5.429,39.749,-5.444]},{"b":[55.383,-4.786,55.543,-4.559],"l":[55.481,-4.695,55.495,-4.755,55.543,-4.786,55.54,-4.693,55.456,-4.559,55.383,-4.609,55.417,-4.65,55.481,-4.695]},{"b":[8.434,3.217,8.951,3.758],"l":[8.76,3.754,8.736,3.758,8.676,3.736,8.638,3.669,8.623,3.58,8.577,3.482,8.55,3.468,8.465,3.451,8.452,3.423,8.434,3.332,8.445,3.294,8.475,3.265,8.652,3.217,8.704,3.224,8.763,3.305,8.792,3.4,8.946,3.628,8.951,3.705,8.91,3.758,8.76,3.754]},{"b":[6.468,0.047,6.75,0.404],"l":[6.626,0.4,6.524,0.34,6.478,0.28,6.468,0.227,6.497,0.117,6.52,0.066,6.557,0.047,6.66,0.121,6.75,0.243,6.75,0.326,6.687,0.404,6.626,0.4]},{"b":[7.331,1.542,7.452,1.699],"l":[7.414,1.699,7.388,1.68,7.331,1.603,7.342,1.564,7.387,1.542,7.424,1.568,7.452,1.631,7.45,1.662,7.437,1.683,7.414,1.699]},{"b":[7.14,4.388,7.328,4.527],"l":[7.227,4.527,7.14,4.395,7.204,4.388,7.301,4.418,7.328,4.487,7.271,4.499,7.227,4.527]},{"b":[-44.36,-23.212,-44.098,-23.074],"l":[-44.22,-23.083,-44.243,-23.074,-44.274,-23.116,-44.36,-23.172,-44.32,-23.212,-44.221,-23.191,-44.156,-23.167,-44.098,-23.169,-44.129,-23.142,-44.192,-23.113,-44.22,-23.083]},{"b":[-48.666,-26.414,-48.498,-26.17],"l":[-48.498,-26.219,-48.54,-26.17,-48.666,-26.29,-48.603,-26.414,-48.584,-26.402,-48.568,-26.38,-48.531,-26.313,-48.498,-26.219]},{"b":[-48.555,-27.812,-48.378,-27.4],"l":[-48.542,-27.575,-48.555,-27.812,-48.486,-27.767,-48.497,-27.707,-48.41,-27.566,-48.378,-27.451,-48.415,-27.4,-48.465,-27.436,-48.505,-27.496,-48.542,-27.575]},{"b":[-45.451,-23.941,-45.233,-23.728],"l":[-45.233,-23.825,-45.249,-23.783,-45.272,-23.752,-45.302,-23.728,-45.451,-23.896,-45.413,-23.935,-45.303,-23.915,-45.261,-23.941,-45.26,-23.889,-45.25,-23.853,-45.233,-23.825]},{"b":[11.124,34.682,11.281,34.82],"l":[11.255,34.82,11.153,34.745,11.124,34.682,11.278,34.754,11.281,34.802,11.255,34.82]},{"b":[10.722,33.687,11.038,33.893],"l":[10.922,33.893,10.745,33.889,10.734,33.856,10.722,33.739,10.757,33.717,10.785,33.718,10.857,33.687,10.883,33.69,10.931,33.717,10.958,33.722,10.993,33.746,11.038,33.785,11.034,33.805,11.018,33.823,10.922,33.893]},{"b":[-4.785,54.059,-4.338,54.407],"l":[-4.412,54.185,-4.392,54.225,-4.338,54.269,-4.377,54.393,-4.396,54.403,-4.425,54.407,-4.509,54.377,-4.615,54.267,-4.699,54.225,-4.746,54.119,-4.785,54.073,-4.766,54.069,-4.696,54.081,-4.614,54.059,-4.412,54.185]},{"b":[-6.669,56.579,-6.484,56.672],"l":[-6.57,56.661,-6.669,56.594,-6.664,56.579,-6.608,56.585,-6.53,56.627,-6.484,56.666,-6.506,56.672,-6.57,56.661]},{"b":[-5.393,55.448,-5.095,55.717],"l":[-5.105,55.574,-5.16,55.667,-5.185,55.691,-5.252,55.717,-5.318,55.709,-5.346,55.691,-5.371,55.667,-5.393,55.618,-5.331,55.481,-5.277,55.457,-5.231,55.448,-5.105,55.449,-5.095,55.494,-5.105,55.574]},{"b":[-6.326,56.289,-5.761,56.65],"l":[-5.836,56.523,-5.947,56.535,-6.03,56.61,-6.103,56.646,-6.138,56.65,-6.182,56.643,-6.286,56.612,-6.306,56.599,-6.32,56.569,-6.311,56.552,-6.139,56.491,-6.185,56.357,-6.298,56.339,-6.326,56.321,-6.313,56.294,-6.176,56.289,-5.778,56.344,-5.761,56.491,-5.836,56.523]},{"b":[-6.496,55.607,-6.055,55.931],"l":[-6.129,55.931,-6.216,55.905,-6.311,55.856,-6.344,55.874,-6.375,55.871,-6.413,55.855,-6.445,55.832,-6.463,55.808,-6.466,55.769,-6.496,55.712,-6.491,55.697,-6.452,55.704,-6.334,55.774,-6.302,55.781,-6.286,55.773,-6.302,55.728,-6.27,55.67,-6.307,55.619,-6.305,55.607,-6.253,55.607,-6.088,55.658,-6.055,55.695,-6.058,55.723,-6.093,55.802,-6.129,55.931]},{"b":[-6.072,55.804,-5.725,56.12],"l":[-5.939,56.045,-5.973,56.004,-5.97,55.992,-5.912,55.975,-6.041,55.926,-6.072,55.893,-6.071,55.848,-6.06,55.823,-6.042,55.807,-5.991,55.804,-5.97,55.815,-5.797,56.006,-5.725,56.119,-5.762,56.12,-5.8,56.109,-5.939,56.045]},{"b":[-1.166,60.502,-0.992,60.72],"l":[-1.093,60.72,-1.166,60.604,-1.068,60.502,-1.043,60.514,-1.034,60.53,-1.035,60.593,-1.049,60.647,-1.045,60.656,-1,60.658,-0.992,60.686,-1.006,60.717,-1.093,60.72]},{"b":[-1.664,59.879,-1.052,60.61],"l":[-1.236,60.485,-1.287,60.467,-1.308,60.538,-1.302,60.608,-1.364,60.61,-1.414,60.599,-1.498,60.53,-1.553,60.517,-1.572,60.494,-1.549,60.481,-1.45,60.469,-1.375,60.333,-1.494,60.292,-1.577,60.298,-1.664,60.283,-1.66,60.262,-1.641,60.237,-1.613,60.229,-1.517,60.231,-1.499,60.222,-1.497,60.194,-1.481,60.173,-1.409,60.19,-1.323,60.188,-1.291,60.153,-1.276,60.115,-1.3,60.04,-1.356,59.911,-1.299,59.879,-1.284,59.887,-1.245,59.971,-1.199,60.007,-1.179,60.114,-1.166,60.124,-1.153,60.177,-1.134,60.207,-1.066,60.382,-1.052,60.444,-1.118,60.418,-1.158,60.418,-1.236,60.485]},{"b":[-0.938,60.684,-0.774,60.832],"l":[-0.825,60.684,-0.826,60.716,-0.774,60.8,-0.774,60.812,-0.802,60.831,-0.823,60.832,-0.865,60.806,-0.891,60.816,-0.916,60.81,-0.928,60.797,-0.938,60.746,-0.922,60.697,-0.909,60.687,-0.825,60.684]},{"b":[-3.401,58.781,-3.159,58.919],"l":[-3.272,58.905,-3.357,58.919,-3.395,58.91,-3.401,58.882,-3.367,58.84,-3.279,58.782,-3.222,58.781,-3.165,58.794,-3.159,58.801,-3.212,58.814,-3.222,58.826,-3.228,58.857,-3.272,58.905]},{"b":[-4.568,53.134,-4.049,53.419],"l":[-4.315,53.417,-4.462,53.419,-4.568,53.386,-4.553,53.26,-4.472,53.176,-4.419,53.178,-4.373,53.134,-4.279,53.172,-4.2,53.218,-4.084,53.264,-4.049,53.306,-4.155,53.303,-4.197,53.321,-4.315,53.417]},{"b":[-2.662,59.23,-2.407,59.304],"l":[-2.536,59.304,-2.604,59.289,-2.662,59.23,-2.549,59.231,-2.43,59.271,-2.407,59.298,-2.536,59.304]},{"b":[-3.035,58.739,-2.896,58.836],"l":[-3.035,58.823,-2.975,58.757,-2.939,58.739,-2.929,58.742,-2.913,58.8,-2.896,58.828,-2.941,58.836,-3.035,58.823]},{"b":[-3.354,58.891,-2.762,59.144],"l":[-2.818,58.982,-2.885,58.985,-2.995,59.006,-3.071,59.005,-3.057,59.03,-3.02,59.058,-3.019,59.076,-3.051,59.099,-3.156,59.136,-3.249,59.144,-3.31,59.131,-3.347,59.065,-3.354,59.019,-3.347,58.987,-3.332,58.971,-3.304,58.967,-3.242,59,-3.233,58.99,-3.233,58.956,-3.223,58.939,-3.201,58.925,-3.167,58.919,-2.995,58.939,-2.864,58.891,-2.826,58.893,-2.793,58.907,-2.762,58.956,-2.818,58.982]},{"b":[-6.885,61.768,-6.623,61.904],"l":[-6.662,61.862,-6.791,61.895,-6.842,61.904,-6.885,61.899,-6.864,61.862,-6.839,61.841,-6.764,61.815,-6.67,61.769,-6.643,61.768,-6.623,61.806,-6.626,61.827,-6.662,61.862]},{"b":[-6.935,61.414,-6.68,61.634],"l":[-6.704,61.496,-6.741,61.536,-6.741,61.571,-6.77,61.584,-6.882,61.603,-6.906,61.631,-6.935,61.634,-6.929,61.603,-6.888,61.535,-6.771,61.452,-6.703,61.418,-6.68,61.414,-6.699,61.445,-6.704,61.496]},{"b":[-7.423,62.04,-7.065,62.151],"l":[-7.379,62.075,-7.255,62.046,-7.179,62.04,-7.117,62.047,-7.065,62.073,-7.097,62.101,-7.187,62.139,-7.235,62.151,-7.337,62.139,-7.423,62.14,-7.379,62.075]},{"b":[-7.172,61.951,-6.631,62.316],"l":[-7.014,62.094,-6.81,61.977,-6.725,61.951,-6.714,61.964,-6.723,61.99,-6.809,62.08,-6.838,62.095,-6.841,62.119,-6.823,62.139,-6.769,62.131,-6.696,62.094,-6.656,62.094,-6.631,62.228,-6.804,62.266,-6.959,62.316,-7.172,62.286,-7.014,62.094]},{"b":[-3.052,59.162,-2.72,59.347],"l":[-2.964,59.274,-2.861,59.247,-2.852,59.182,-2.815,59.162,-2.729,59.187,-2.72,59.219,-2.731,59.227,-2.815,59.241,-2.862,59.288,-2.976,59.347,-3.042,59.334,-3.052,59.324,-3.013,59.291,-2.964,59.274]},{"b":[-6.559,62.187,-6.406,62.356],"l":[-6.454,62.187,-6.406,62.259,-6.473,62.292,-6.555,62.356,-6.552,62.278,-6.559,62.225,-6.544,62.206,-6.525,62.198,-6.454,62.187]},{"b":[-7.096,57.75,-6.194,58.503],"l":[-6.854,57.827,-6.797,57.828,-6.683,57.911,-6.578,57.941,-6.425,58.021,-6.402,58.041,-6.403,58.076,-6.437,58.092,-6.555,58.093,-6.419,58.141,-6.376,58.185,-6.326,58.189,-6.199,58.363,-6.194,58.435,-6.219,58.489,-6.237,58.503,-6.297,58.487,-6.544,58.383,-6.742,58.322,-6.776,58.302,-6.788,58.284,-6.725,58.198,-6.726,58.189,-6.812,58.196,-6.886,58.183,-6.95,58.218,-7.012,58.229,-7.028,58.222,-7.045,58.202,-7.085,58.182,-7.096,58.138,-7.088,58.095,-7.077,58.079,-7.038,58.072,-7.017,58.055,-6.985,58.05,-7.052,58.018,-7.057,58.003,-7.003,57.975,-6.864,57.933,-6.857,57.924,-6.944,57.894,-6.956,57.865,-7.083,57.814,-7.013,57.762,-6.983,57.75,-6.957,57.75,-6.91,57.773,-6.854,57.827]},{"b":[-6.433,56.952,-6.261,57.051],"l":[-6.433,57.018,-6.383,56.971,-6.346,56.954,-6.309,56.952,-6.279,56.965,-6.261,56.985,-6.261,57.01,-6.278,57.031,-6.322,57.051,-6.433,57.018]},{"b":[-6.761,57.044,-5.669,57.672],"l":[-6.362,57.238,-6.323,57.202,-6.266,57.184,-6.163,57.182,-6.034,57.201,-6.015,57.052,-5.987,57.044,-5.949,57.045,-5.914,57.063,-5.795,57.147,-5.696,57.198,-5.669,57.227,-5.672,57.253,-5.706,57.269,-5.88,57.263,-6.068,57.284,-6.093,57.302,-6.136,57.314,-6.141,57.354,-6.164,57.409,-6.146,57.461,-6.145,57.505,-6.166,57.585,-6.247,57.651,-6.306,57.672,-6.358,57.667,-6.379,57.603,-6.617,57.563,-6.615,57.553,-6.583,57.521,-6.583,57.507,-6.606,57.491,-6.643,57.483,-6.704,57.496,-6.753,57.459,-6.761,57.442,-6.741,57.412,-6.675,57.363,-6.442,57.327,-6.362,57.238]},{"b":[-7.516,57.533,-7.093,57.683],"l":[-7.206,57.683,-7.271,57.657,-7.325,57.663,-7.392,57.645,-7.44,57.656,-7.47,57.653,-7.499,57.636,-7.516,57.616,-7.515,57.602,-7.321,57.534,-7.183,57.533,-7.093,57.627,-7.206,57.683]},{"b":[-7.422,57.11,-7.248,57.384],"l":[-7.296,57.384,-7.411,57.381,-7.407,57.298,-7.422,57.229,-7.416,57.192,-7.381,57.131,-7.347,57.115,-7.292,57.11,-7.25,57.115,-7.248,57.126,-7.267,57.372,-7.296,57.384]},{"b":[-10.266,53.885,-9.948,54.017],"l":[-10.14,54.005,-10.181,54.017,-10.266,53.978,-10.062,53.96,-10.027,53.92,-9.953,53.885,-9.948,53.913,-9.956,53.987,-9.996,54.003,-10.14,54.005]},{"b":[-7.543,56.952,-7.399,57.019],"l":[-7.523,57.007,-7.543,56.972,-7.537,56.96,-7.505,56.952,-7.417,56.965,-7.399,56.983,-7.407,57,-7.455,57.019,-7.523,57.007]},{"b":[21.806,60.332,21.994,60.47],"l":[21.908,60.393,21.95,60.402,21.907,60.438,21.827,60.47,21.819,60.452,21.846,60.412,21.806,60.401,21.819,60.382,21.921,60.332,21.994,60.337,21.98,60.355,21.908,60.393]},{"b":[19.519,60.135,19.667,60.246],"l":[19.551,60.244,19.519,60.185,19.537,60.145,19.58,60.135,19.6,60.163,19.629,60.17,19.667,60.165,19.662,60.187,19.629,60.246,19.551,60.244]},{"b":[19.672,60.082,20.259,60.406],"l":[20.073,60.193,20.125,60.201,20.155,60.192,20.195,60.194,20.259,60.261,20.24,60.283,20.184,60.294,20.168,60.315,20.087,60.353,20.034,60.359,20.02,60.351,19.99,60.351,19.945,60.358,19.888,60.406,19.823,60.39,19.788,60.354,19.812,60.332,19.855,60.319,19.872,60.302,19.867,60.268,19.848,60.221,19.785,60.213,19.779,60.286,19.737,60.282,19.687,60.268,19.672,60.233,19.746,60.099,19.8,60.082,20.034,60.094,20.032,60.152,20.043,60.181,20.073,60.193]},{"b":[21.084,63.152,21.422,63.278],"l":[21.366,63.262,21.287,63.228,21.229,63.223,21.218,63.241,21.222,63.259,21.236,63.278,21.084,63.278,21.149,63.199,21.253,63.152,21.31,63.163,21.318,63.179,21.367,63.207,21.378,63.199,21.416,63.197,21.422,63.246,21.366,63.262]},{"b":[19.039,57.86,19.331,57.981],"l":[19.135,57.981,19.039,57.911,19.087,57.865,19.138,57.86,19.156,57.923,19.331,57.963,19.281,57.978,19.135,57.981]},{"b":[16.394,56.24,17.118,57.345],"l":[16.961,57.25,16.902,57.175,16.865,57.091,16.728,56.902,16.63,56.877,16.412,56.569,16.394,56.484,16.401,56.311,16.432,56.244,16.477,56.24,16.529,56.291,16.778,56.805,16.838,56.841,16.884,56.985,17.054,57.208,17.058,57.229,17.05,57.28,17.118,57.32,17.089,57.332,17.025,57.345,16.996,57.318,16.961,57.25]},{"b":[14.684,55.005,15.137,55.297],"l":[14.765,55.297,14.714,55.238,14.684,55.102,14.886,55.033,15.051,55.005,15.088,55.022,15.137,55.087,15.133,55.145,14.765,55.297]},{"b":[13.156,54.246,13.734,54.699],"l":[13.602,54.425,13.58,54.464,13.603,54.488,13.671,54.535,13.658,54.56,13.636,54.577,13.491,54.615,13.45,54.65,13.423,54.699,13.337,54.697,13.24,54.638,13.231,54.583,13.177,54.544,13.181,54.509,13.156,54.397,13.162,54.365,13.19,54.326,13.364,54.246,13.415,54.25,13.482,54.337,13.595,54.338,13.707,54.281,13.734,54.315,13.709,54.383,13.602,54.425]},{"b":[12.119,54.892,12.549,55.064],"l":[12.31,55.041,12.274,55.064,12.259,55.021,12.22,54.994,12.162,54.975,12.144,54.959,12.119,54.914,12.184,54.892,12.358,54.962,12.511,54.951,12.549,54.966,12.513,54.997,12.47,55.017,12.417,55.031,12.31,55.041]},{"b":[12.52,55.554,12.666,55.68],"l":[12.52,55.615,12.551,55.556,12.572,55.554,12.666,55.597,12.648,55.647,12.62,55.679,12.599,55.68,12.57,55.65,12.52,55.615]},{"b":[11.036,54.629,11.766,54.952],"l":[11.658,54.833,11.538,54.83,11.361,54.892,11.258,54.952,11.059,54.941,11.042,54.893,11.036,54.773,11.457,54.629,11.586,54.662,11.68,54.654,11.766,54.679,11.759,54.768,11.74,54.807,11.658,54.833]},{"b":[10.2,54.837,10.505,54.963],"l":[10.347,54.906,10.266,54.949,10.2,54.963,10.216,54.941,10.341,54.859,10.417,54.837,10.484,54.848,10.505,54.861,10.414,54.897,10.347,54.906]},{"b":[9.771,54.872,10.061,55.069],"l":[9.781,55.069,9.771,55.06,9.806,54.906,9.904,54.897,9.957,54.872,10.061,54.886,10.058,54.908,9.999,54.986,9.83,55.058,9.781,55.069]},{"b":[10.622,54.745,10.951,55.158],"l":[10.738,54.962,10.692,54.903,10.622,54.851,10.629,54.826,10.69,54.745,10.734,54.751,10.765,54.8,10.921,55.062,10.951,55.156,10.925,55.158,10.857,55.052,10.738,54.962]},{"b":[10.516,55.765,10.662,55.992],"l":[10.527,55.784,10.59,55.765,10.607,55.783,10.627,55.834,10.662,55.878,10.636,55.914,10.547,55.992,10.516,55.959,10.544,55.907,10.52,55.848,10.527,55.784]},{"b":[11.012,54.416,11.283,54.533],"l":[11.085,54.533,11.043,54.515,11.012,54.466,11.071,54.456,11.129,54.416,11.283,54.418,11.28,54.438,11.234,54.501,11.085,54.533]},{"b":[10.874,57.229,11.175,57.33],"l":[10.935,57.309,10.874,57.262,11.011,57.229,11.052,57.253,11.077,57.277,11.175,57.323,11.086,57.33,10.935,57.309]},{"b":[8.285,54.767,8.63,55.059],"l":[8.451,55.055,8.405,55.059,8.296,54.908,8.285,54.767,8.308,54.787,8.347,54.848,8.601,54.865,8.63,54.892,8.38,54.9,8.371,54.929,8.39,54.986,8.404,55.015,8.451,55.055]},{"b":[-1.389,45.817,-1.178,46.05],"l":[-1.389,46.05,-1.389,46.033,-1.369,45.968,-1.28,45.897,-1.214,45.817,-1.178,45.904,-1.285,46.003,-1.389,46.05]},{"b":[-1.563,50.589,-1.066,50.773],"l":[-1.313,50.773,-1.386,50.734,-1.516,50.703,-1.563,50.666,-1.515,50.67,-1.306,50.589,-1.251,50.589,-1.196,50.599,-1.176,50.615,-1.149,50.656,-1.066,50.69,-1.144,50.735,-1.313,50.773]},{"b":[4.709,53,4.886,53.183],"l":[4.74,53.091,4.709,53.036,4.727,53.02,4.787,53,4.886,53.071,4.886,53.183,4.74,53.091]},{"b":[10.11,42.713,10.432,42.858],"l":[10.359,42.822,10.286,42.828,10.248,42.816,10.128,42.81,10.11,42.785,10.131,42.742,10.209,42.737,10.336,42.761,10.419,42.713,10.41,42.771,10.432,42.797,10.428,42.819,10.395,42.858,10.359,42.822]},{"b":[8.359,38.969,8.479,39.116],"l":[8.367,39.116,8.359,39.099,8.361,39.039,8.421,38.969,8.479,39.068,8.441,39.091,8.367,39.116]},{"b":[8.206,40.994,8.344,41.122],"l":[8.344,41.102,8.32,41.122,8.267,41.099,8.224,41.031,8.206,40.997,8.253,40.994,8.286,41.04,8.319,41.063,8.344,41.102]},{"b":[3.843,39.83,4.322,40.075],"l":[4.059,40.075,3.853,40.063,3.845,40.036,3.843,39.976,3.867,39.959,3.968,39.946,4.275,39.83,4.294,39.842,4.322,39.898,4.315,39.917,4.226,40.032,4.059,40.075]},{"b":[1.402,38.659,1.594,38.768],"l":[1.417,38.74,1.402,38.711,1.406,38.671,1.505,38.671,1.571,38.659,1.594,38.672,1.593,38.701,1.497,38.712,1.436,38.768,1.417,38.74]},{"b":[1.223,38.857,1.624,39.121],"l":[1.349,39.081,1.303,39.031,1.3,38.982,1.256,38.973,1.223,38.904,1.257,38.879,1.409,38.857,1.445,38.919,1.495,38.933,1.624,39.039,1.613,39.087,1.564,39.121,1.349,39.081]},{"b":[-31.283,39.359,-31.137,39.521],"l":[-31.261,39.497,-31.283,39.394,-31.258,39.376,-31.181,39.359,-31.137,39.407,-31.139,39.479,-31.2,39.521,-31.261,39.497]},{"b":[-27.386,38.634,-27.042,38.803],"l":[-27.127,38.79,-27.26,38.803,-27.351,38.789,-27.386,38.766,-27.362,38.698,-27.303,38.661,-27.095,38.634,-27.075,38.643,-27.042,38.679,-27.042,38.741,-27.127,38.79]},{"b":[-28.311,38.544,-27.778,38.744],"l":[-28.311,38.744,-28.187,38.655,-28.092,38.621,-27.826,38.544,-27.778,38.556,-27.963,38.636,-28.311,38.744]},{"b":[-28.842,38.522,-28.606,38.638],"l":[-28.698,38.638,-28.842,38.598,-28.744,38.522,-28.641,38.525,-28.606,38.551,-28.624,38.586,-28.655,38.614,-28.698,38.638]},{"b":[-28.549,38.385,-28.065,38.553],"l":[-28.402,38.553,-28.51,38.553,-28.549,38.519,-28.531,38.463,-28.454,38.409,-28.332,38.413,-28.231,38.385,-28.19,38.404,-28.065,38.413,-28.147,38.453,-28.402,38.553]},{"b":[-25.848,37.715,-25.182,37.911],"l":[-25.585,37.834,-25.649,37.841,-25.784,37.911,-25.846,37.894,-25.848,37.872,-25.834,37.826,-25.734,37.763,-25.439,37.715,-25.251,37.735,-25.191,37.764,-25.182,37.838,-25.267,37.849,-25.585,37.834]},{"b":[14.351,35.82,14.566,35.978],"l":[14.448,35.957,14.351,35.978,14.352,35.872,14.436,35.822,14.533,35.82,14.566,35.853,14.537,35.886,14.448,35.957]},{"b":[15.066,44.027,15.247,44.158],"l":[15.231,44.062,15.066,44.158,15.075,44.138,15.122,44.093,15.247,44.027,15.231,44.062]},{"b":[14.438,44.956,14.81,45.225],"l":[14.739,45.065,14.701,45.09,14.63,45.178,14.571,45.225,14.525,45.147,14.438,45.099,14.45,45.079,14.512,45.035,14.613,45.025,14.628,44.994,14.687,44.956,14.81,44.977,14.739,45.065]},{"b":[14.66,44.715,14.857,44.848],"l":[14.857,44.715,14.831,44.759,14.764,44.821,14.754,44.845,14.691,44.848,14.672,44.824,14.66,44.8,14.678,44.77,14.763,44.755,14.857,44.715]},{"b":[14.742,44.309,15.24,44.697],"l":[14.913,44.486,14.996,44.434,15.039,44.393,15.098,44.358,15.163,44.309,15.188,44.336,15.214,44.348,15.24,44.35,15.113,44.436,15.006,44.534,14.898,44.611,14.855,44.618,14.804,44.649,14.742,44.697,14.76,44.665,14.885,44.545,14.913,44.486]},{"b":[14.865,43.898,15.203,44.168],"l":[14.953,44.117,14.865,44.168,14.891,44.126,15.136,43.907,15.15,43.912,15.202,43.898,15.203,43.908,15.189,43.922,14.953,44.117]},{"b":[15.27,43.9,15.437,44.011],"l":[15.374,43.915,15.437,43.9,15.371,43.974,15.27,44.011,15.309,43.961,15.374,43.915]},{"b":[14.286,44.621,14.488,45.167],"l":[14.468,44.725,14.453,44.869,14.467,44.97,14.394,45.031,14.369,45.081,14.358,45.167,14.331,45.165,14.286,45.145,14.34,45.02,14.342,44.98,14.303,44.94,14.312,44.9,14.389,44.758,14.42,44.67,14.48,44.621,14.488,44.66,14.483,44.693,14.468,44.725]},{"b":[17.344,42.7,17.744,42.8],"l":[17.39,42.799,17.344,42.79,17.744,42.7,17.608,42.769,17.432,42.8,17.39,42.799]},{"b":[20.352,38.071,20.789,38.475],"l":[20.789,38.142,20.695,38.246,20.625,38.268,20.612,38.383,20.563,38.475,20.55,38.457,20.548,38.395,20.52,38.332,20.481,38.318,20.435,38.356,20.409,38.337,20.352,38.222,20.353,38.18,20.391,38.188,20.452,38.234,20.473,38.219,20.499,38.184,20.496,38.164,20.524,38.107,20.569,38.098,20.606,38.12,20.761,38.071,20.781,38.089,20.789,38.142]},{"b":[20.62,37.665,20.994,37.93],"l":[20.84,37.841,20.759,37.853,20.692,37.93,20.62,37.855,20.635,37.823,20.704,37.743,20.819,37.665,20.909,37.732,20.994,37.708,20.888,37.805,20.84,37.841]},{"b":[20.555,38.583,20.72,38.844],"l":[20.648,38.601,20.687,38.609,20.715,38.638,20.72,38.799,20.694,38.844,20.635,38.818,20.592,38.76,20.558,38.662,20.555,38.583,20.584,38.602,20.614,38.606,20.648,38.601]},{"b":[20.624,38.319,20.759,38.484],"l":[20.701,38.451,20.675,38.476,20.65,38.484,20.624,38.48,20.646,38.414,20.709,38.319,20.759,38.329,20.739,38.366,20.712,38.399,20.701,38.426,20.701,38.451]},{"b":[19.646,39.377,20.1,39.82],"l":[19.955,39.47,19.927,39.506,19.903,39.6,19.904,39.619,19.847,39.668,19.862,39.693,19.937,39.747,19.926,39.774,19.892,39.797,19.839,39.82,19.707,39.798,19.646,39.767,19.649,39.726,19.809,39.585,19.884,39.462,19.975,39.411,20.1,39.377,20.078,39.433,20.028,39.442,19.955,39.47]},{"b":[23.419,37.901,23.551,37.991],"l":[23.511,37.901,23.551,37.926,23.535,37.97,23.516,37.986,23.484,37.991,23.462,37.98,23.439,37.941,23.419,37.931,23.435,37.911,23.467,37.902,23.511,37.901]},{"b":[22.906,36.146,23.097,36.384],"l":[22.95,36.384,22.933,36.369,22.906,36.32,22.911,36.221,22.939,36.176,23.042,36.146,23.054,36.19,23.097,36.247,22.998,36.328,22.95,36.384]},{"b":[3.699,51.627,4.075,51.746],"l":[3.949,51.739,3.789,51.746,3.699,51.73,3.699,51.71,3.732,51.678,3.819,51.694,3.951,51.627,4.075,51.649,4.068,51.667,4.047,51.685,3.949,51.739]},{"b":[-51.35,69.517,-50.679,69.924],"l":[-51.35,69.855,-51.319,69.804,-51.339,69.732,-51.315,69.674,-51.234,69.552,-51.202,69.525,-51.17,69.517,-51.014,69.552,-50.97,69.583,-50.978,69.618,-50.967,69.664,-50.912,69.757,-50.754,69.798,-50.698,69.829,-50.679,69.849,-50.94,69.909,-51.095,69.924,-51.209,69.914,-51.35,69.855]},{"b":[-37.238,65.531,-36.953,65.722],"l":[-37.048,65.722,-37.223,65.695,-37.238,65.61,-37.187,65.531,-37.031,65.532,-36.987,65.576,-36.953,65.663,-37.048,65.722]},{"b":[-53.958,71.034,-53.432,71.297],"l":[-53.629,71.034,-53.535,71.041,-53.488,71.056,-53.455,71.083,-53.437,71.115,-53.432,71.153,-53.441,71.186,-53.512,71.25,-53.584,71.297,-53.701,71.283,-53.862,71.207,-53.947,71.156,-53.958,71.128,-53.941,71.104,-53.898,71.085,-53.629,71.034]},{"b":[-56.215,72.564,-55.017,72.842],"l":[-55.524,72.568,-55.274,72.684,-55.158,72.723,-55.017,72.791,-55.033,72.821,-55.206,72.842,-55.235,72.825,-55.428,72.789,-55.516,72.781,-55.574,72.78,-55.666,72.794,-55.994,72.782,-56.078,72.753,-56.215,72.719,-56.141,72.668,-56.043,72.656,-55.936,72.668,-55.869,72.662,-55.827,72.652,-55.814,72.636,-55.781,72.617,-55.687,72.61,-55.634,72.579,-55.567,72.564,-55.524,72.568]},{"b":[-52.148,70.853,-51.607,70.976],"l":[-51.97,70.976,-52.107,70.968,-52.148,70.904,-52.144,70.882,-52.119,70.871,-51.809,70.853,-51.675,70.855,-51.607,70.869,-51.631,70.892,-51.807,70.942,-51.97,70.976]},{"b":[-46.79,60.66,-46.205,60.944],"l":[-46.394,60.909,-46.79,60.78,-46.788,60.758,-46.666,60.766,-46.553,60.741,-46.496,60.687,-46.382,60.66,-46.267,60.781,-46.254,60.842,-46.219,60.889,-46.205,60.944,-46.394,60.909]},{"b":[-126.231,49.264,-126.064,49.38],"l":[-126.115,49.365,-126.209,49.38,-126.231,49.339,-126.23,49.296,-126.187,49.278,-126.064,49.264,-126.092,49.354,-126.115,49.365]},{"b":[-122.51,47.355,-122.394,47.49],"l":[-122.507,47.422,-122.51,47.358,-122.469,47.39,-122.458,47.386,-122.457,47.359,-122.437,47.355,-122.399,47.373,-122.394,47.395,-122.442,47.446,-122.469,47.49,-122.486,47.489,-122.507,47.422]},{"b":[-122.576,47.575,-122.497,47.704],"l":[-122.517,47.691,-122.55,47.704,-122.56,47.698,-122.574,47.667,-122.576,47.619,-122.558,47.598,-122.503,47.575,-122.497,47.595,-122.508,47.683,-122.517,47.691]},{"b":[-123.003,48.594,-122.769,48.71],"l":[-122.918,48.707,-122.977,48.679,-123.003,48.652,-122.986,48.627,-122.893,48.594,-122.887,48.612,-122.903,48.665,-122.883,48.661,-122.838,48.627,-122.809,48.63,-122.769,48.651,-122.782,48.673,-122.898,48.71,-122.918,48.707]},{"b":[-122.932,48.422,-122.815,48.552],"l":[-122.869,48.549,-122.885,48.552,-122.912,48.538,-122.932,48.485,-122.922,48.457,-122.89,48.435,-122.837,48.422,-122.821,48.431,-122.815,48.452,-122.862,48.502,-122.869,48.549]},{"b":[-123.17,48.468,-122.987,48.613],"l":[-123.114,48.613,-123.162,48.606,-123.17,48.587,-123.153,48.526,-123.14,48.508,-123.094,48.489,-122.987,48.468,-123.013,48.501,-123.024,48.538,-123.114,48.613]},{"b":[-122.749,47.918,-122.367,48.384],"l":[-122.503,48.08,-122.524,48.025,-122.573,48.157,-122.598,48.2,-122.624,48.214,-122.697,48.229,-122.692,48.241,-122.542,48.294,-122.536,48.321,-122.572,48.36,-122.604,48.381,-122.629,48.384,-122.669,48.351,-122.725,48.281,-122.749,48.239,-122.742,48.225,-122.69,48.174,-122.657,48.156,-122.623,48.151,-122.606,48.129,-122.603,48.055,-122.591,48.03,-122.558,47.992,-122.492,47.981,-122.462,47.964,-122.438,47.931,-122.411,47.918,-122.383,47.923,-122.367,47.939,-122.367,47.985,-122.503,48.08]},{"b":[-124.65,49.51,-124.14,49.775],"l":[-124.494,49.667,-124.457,49.634,-124.362,49.588,-124.14,49.51,-124.154,49.531,-124.309,49.667,-124.421,49.728,-124.547,49.765,-124.623,49.775,-124.65,49.758,-124.631,49.736,-124.518,49.686,-124.494,49.667]},{"b":[-123.689,48.875,-123.372,49.095],"l":[-123.385,48.875,-123.372,48.886,-123.378,48.908,-123.482,48.955,-123.689,49.095,-123.646,49.039,-123.541,48.946,-123.385,48.875]},{"b":[-123.582,48.729,-123.407,48.926],"l":[-123.468,48.867,-123.555,48.922,-123.582,48.926,-123.518,48.75,-123.5,48.732,-123.477,48.729,-123.435,48.754,-123.407,48.756,-123.423,48.793,-123.488,48.846,-123.468,48.867]},{"b":[-125.026,50.021,-124.907,50.217],"l":[-124.978,50.03,-124.908,50.071,-124.907,50.084,-124.916,50.132,-124.938,50.166,-124.991,50.217,-124.987,50.196,-124.996,50.175,-125.026,50.134,-125.002,50.021,-124.978,50.03]},{"b":[-126.951,49.601,-126.626,49.872],"l":[-126.738,49.844,-126.826,49.872,-126.926,49.838,-126.897,49.783,-126.905,49.763,-126.94,49.75,-126.951,49.736,-126.939,49.718,-126.814,49.642,-126.743,49.613,-126.68,49.601,-126.641,49.606,-126.626,49.627,-126.628,49.675,-126.65,49.746,-126.698,49.808,-126.738,49.844]},{"b":[-125.358,50.044,-125.074,50.418],"l":[-125.126,50.32,-125.14,50.34,-125.196,50.39,-125.261,50.418,-125.301,50.414,-125.345,50.354,-125.358,50.312,-125.26,50.13,-125.195,50.044,-125.184,50.097,-125.113,50.163,-125.074,50.221,-125.091,50.268,-125.126,50.32]},{"b":[-118.59,32.819,-118.35,33.033],"l":[-118.507,32.96,-118.557,33.033,-118.59,33.011,-118.529,32.936,-118.473,32.839,-118.409,32.819,-118.35,32.828,-118.383,32.849,-118.507,32.96]},{"b":[-118.401,28.904,-118.243,29.188],"l":[-118.312,29.131,-118.312,29.183,-118.368,29.188,-118.401,29.163,-118.4,29.112,-118.285,28.904,-118.243,28.942,-118.247,29.043,-118.266,29.086,-118.312,29.131]},{"b":[-120.252,33.905,-119.984,34.027],"l":[-120.252,34.014,-120.167,33.918,-120.114,33.905,-120.044,33.919,-119.984,33.973,-119.994,33.985,-120.072,34.027,-120.252,34.014]},{"b":[-119.918,33.968,-119.549,34.08],"l":[-119.679,34.028,-119.882,34.08,-119.918,34.068,-119.892,34.032,-119.885,33.995,-119.81,33.968,-119.562,34.007,-119.549,34.028,-119.569,34.053,-119.679,34.028]},{"b":[-118.569,33.312,-118.297,33.477],"l":[-118.469,33.357,-118.446,33.317,-118.37,33.321,-118.297,33.312,-118.348,33.386,-118.392,33.415,-118.555,33.477,-118.569,33.464,-118.563,33.437,-118.559,33.432,-118.507,33.427,-118.492,33.413,-118.469,33.357]},{"b":[-71.403,41.467,-71.354,41.572],"l":[-71.393,41.467,-71.365,41.485,-71.354,41.542,-71.364,41.572,-71.384,41.571,-71.403,41.515,-71.393,41.467]},{"b":[-74.253,39.529,-74.107,39.746],"l":[-74.25,39.529,-74.133,39.681,-74.107,39.746,-74.253,39.558,-74.25,39.529]},{"b":[-74.236,40.519,-74.067,40.658],"l":[-74.188,40.615,-74.236,40.519,-74.188,40.523,-74.139,40.542,-74.08,40.586,-74.067,40.615,-74.069,40.649,-74.1,40.658,-74.188,40.615]},{"b":[-64.863,32.26,-64.668,32.387],"l":[-64.771,32.308,-64.863,32.274,-64.845,32.262,-64.82,32.26,-64.73,32.293,-64.668,32.382,-64.695,32.387,-64.771,32.308]},{"b":[-70.829,41.327,-70.51,41.457],"l":[-70.674,41.449,-70.76,41.374,-70.829,41.359,-70.785,41.327,-70.51,41.376,-70.525,41.415,-70.616,41.457,-70.674,41.449]},{"b":[-71.346,41.465,-71.232,41.654],"l":[-71.346,41.469,-71.291,41.465,-71.241,41.492,-71.232,41.654,-71.264,41.638,-71.28,41.62,-71.307,41.56,-71.318,41.506,-71.346,41.469]},{"b":[-70.233,41.249,-69.978,41.397],"l":[-70.063,41.328,-70.087,41.318,-70.233,41.286,-70.055,41.249,-69.978,41.266,-69.986,41.299,-70.041,41.397,-70.044,41.374,-70.063,41.328]},{"b":[-75.984,35.119,-75.782,35.19],"l":[-75.782,35.19,-75.865,35.174,-75.984,35.123,-75.964,35.119,-75.782,35.19]},{"b":[-75.717,35.836,-75.636,35.946],"l":[-75.651,35.836,-75.636,35.856,-75.637,35.881,-75.649,35.91,-75.717,35.946,-75.651,35.836]},{"b":[-75.379,37.872,-75.098,38.298],"l":[-75.226,38.072,-75.252,38.037,-75.379,37.872,-75.333,37.888,-75.226,38.04,-75.203,38.072,-75.136,38.181,-75.098,38.298,-75.137,38.24,-75.226,38.072]},{"b":[-75.69,35.213,-75.456,35.769],"l":[-75.504,35.769,-75.504,35.735,-75.481,35.572,-75.488,35.479,-75.536,35.279,-75.69,35.222,-75.678,35.213,-75.544,35.24,-75.509,35.28,-75.465,35.449,-75.456,35.564,-75.479,35.717,-75.504,35.769]},{"b":[-76.529,34.631,-76.207,34.939],"l":[-76.437,34.756,-76.529,34.631,-76.504,34.643,-76.358,34.804,-76.207,34.939,-76.256,34.915,-76.437,34.756]},{"b":[-96.801,28.148,-96.404,28.382],"l":[-96.519,28.333,-96.682,28.23,-96.756,28.202,-96.801,28.148,-96.764,28.153,-96.544,28.276,-96.413,28.338,-96.404,28.382,-96.419,28.376,-96.453,28.341,-96.519,28.333]},{"b":[-97.385,27.243,-97.061,27.822],"l":[-97.295,27.523,-97.376,27.328,-97.385,27.243,-97.354,27.3,-97.251,27.541,-97.061,27.822,-97.13,27.779,-97.295,27.523]},{"b":[-95.09,29.136,-94.768,29.341],"l":[-95.09,29.136,-95.04,29.146,-94.865,29.253,-94.768,29.339,-94.826,29.341,-94.872,29.29,-95.09,29.136]},{"b":[-97.407,26.113,-97.171,27.196],"l":[-97.386,27.196,-97.407,27.1,-97.402,26.821,-97.267,26.33,-97.185,26.113,-97.171,26.159,-97.202,26.3,-97.351,26.801,-97.386,27.196]},{"b":[-85.117,29.607,-84.737,29.732],"l":[-85.049,29.638,-85.117,29.633,-85.008,29.607,-84.908,29.643,-84.737,29.732,-84.812,29.718,-84.877,29.679,-85.001,29.627,-85.049,29.638]},{"b":[-81.485,30.728,-81.419,30.971],"l":[-81.463,30.728,-81.419,30.971,-81.451,30.947,-81.485,30.898,-81.483,30.814,-81.463,30.728]},{"b":[-89.342,30.038,-89.185,30.169],"l":[-89.185,30.169,-89.276,30.111,-89.288,30.094,-89.31,30.079,-89.342,30.063,-89.269,30.061,-89.22,30.038,-89.224,30.084,-89.211,30.126,-89.185,30.169]},{"b":[-92.007,29.486,-91.754,29.644],"l":[-91.796,29.597,-91.875,29.641,-91.925,29.644,-92.007,29.61,-91.996,29.573,-91.831,29.486,-91.794,29.501,-91.762,29.539,-91.754,29.567,-91.768,29.585,-91.796,29.597]},{"b":[-88.867,29.776,-88.813,30.057],"l":[-88.828,29.928,-88.856,29.776,-88.827,29.808,-88.813,29.933,-88.826,30,-88.867,30.057,-88.828,29.928]},{"b":[-78,23.739,-77.519,24.288],"l":[-77.532,23.939,-77.537,23.962,-77.532,23.988,-77.562,24.137,-77.615,24.216,-77.683,24.118,-77.755,24.163,-77.656,24.227,-77.658,24.249,-77.701,24.288,-77.757,24.27,-77.85,24.258,-77.884,24.242,-77.95,24.253,-78,24.22,-77.914,24.091,-77.852,24.04,-77.806,23.884,-77.776,23.862,-77.771,23.753,-77.574,23.739,-77.519,23.869,-77.521,23.911,-77.532,23.939]},{"b":[-77.919,21.921,-77.634,22.092],"l":[-77.774,22.083,-77.855,22.092,-77.919,22.088,-77.9,22.037,-77.823,21.988,-77.784,21.97,-77.755,21.966,-77.71,21.921,-77.669,21.952,-77.646,21.996,-77.634,22.054,-77.774,22.083]},{"b":[-78.274,22.269,-78.027,22.438],"l":[-78.027,22.285,-78.062,22.306,-78.094,22.387,-78.151,22.431,-78.201,22.438,-78.274,22.424,-78.27,22.402,-78.226,22.38,-78.18,22.322,-78.102,22.306,-78.048,22.269,-78.027,22.285]},{"b":[-78.696,22.445,-78.284,22.552],"l":[-78.284,22.455,-78.351,22.539,-78.4,22.547,-78.445,22.544,-78.493,22.531,-78.63,22.552,-78.696,22.534,-78.674,22.509,-78.629,22.488,-78.548,22.464,-78.425,22.46,-78.39,22.445,-78.343,22.445,-78.284,22.455]},{"b":[-77.944,25.895,-77.038,26.94],"l":[-77.511,26.846,-77.33,26.618,-77.239,26.561,-77.206,26.489,-77.23,26.425,-77.221,26.362,-77.248,26.289,-77.247,26.156,-77.294,26.096,-77.403,26.025,-77.333,25.996,-77.246,25.895,-77.226,25.904,-77.191,25.955,-77.167,26.24,-77.038,26.333,-77.066,26.53,-77.162,26.597,-77.257,26.639,-77.269,26.663,-77.266,26.689,-77.296,26.712,-77.369,26.748,-77.449,26.836,-77.534,26.903,-77.672,26.914,-77.788,26.936,-77.863,26.94,-77.944,26.904,-77.796,26.901,-77.511,26.846]},{"b":[-74.314,22.665,-74.035,22.84],"l":[-74.242,22.715,-74.099,22.665,-74.035,22.706,-74.058,22.723,-74.175,22.76,-74.221,22.812,-74.307,22.84,-74.314,22.804,-74.303,22.764,-74.275,22.712,-74.242,22.715]},{"b":[-74.551,23.96,-74.429,24.127],"l":[-74.429,24.068,-74.45,24.125,-74.472,24.127,-74.527,24.105,-74.551,23.969,-74.509,23.96,-74.429,24.068]},{"b":[-73.687,20.937,-73.012,21.313],"l":[-73.661,20.937,-73.401,20.944,-73.165,20.979,-73.059,21.119,-73.027,21.192,-73.012,21.3,-73.058,21.313,-73.137,21.205,-73.235,21.154,-73.302,21.156,-73.425,21.202,-73.523,21.191,-73.585,21.126,-73.68,21.103,-73.67,21.082,-73.668,21.062,-73.687,21.009,-73.681,20.976,-73.661,20.937]},{"b":[-74.277,22.184,-73.837,22.731],"l":[-73.915,22.568,-73.906,22.527,-73.936,22.478,-73.995,22.449,-74.01,22.428,-74.052,22.401,-74.127,22.323,-74.261,22.236,-74.277,22.184,-74.207,22.214,-74.093,22.306,-73.975,22.361,-73.837,22.538,-73.877,22.681,-73.85,22.731,-73.954,22.716,-73.975,22.682,-73.976,22.635,-73.915,22.568]},{"b":[-76.781,24.649,-76.115,25.565],"l":[-76.344,25.332,-76.484,25.375,-76.649,25.487,-76.711,25.565,-76.727,25.552,-76.749,25.481,-76.781,25.427,-76.693,25.443,-76.621,25.432,-76.5,25.342,-76.369,25.313,-76.284,25.222,-76.16,25.119,-76.153,25.026,-76.204,24.936,-76.214,24.822,-76.32,24.818,-76.3,24.796,-76.241,24.754,-76.205,24.682,-76.17,24.649,-76.175,24.76,-76.141,24.886,-76.115,25.095,-76.127,25.141,-76.192,25.191,-76.344,25.332]},{"b":[-76.037,23.444,-75.665,23.671],"l":[-75.949,23.647,-76.01,23.671,-76.037,23.603,-75.956,23.592,-75.781,23.471,-75.706,23.444,-75.665,23.45,-75.754,23.49,-75.808,23.543,-75.949,23.647]},{"b":[-75.316,22.869,-74.84,23.668],"l":[-75.131,23.268,-75.141,23.205,-75.204,23.193,-75.223,23.165,-75.132,23.117,-74.973,23.069,-74.847,22.869,-74.84,22.894,-74.846,23,-74.937,23.088,-75.064,23.15,-75.109,23.333,-75.175,23.439,-75.217,23.547,-75.316,23.668,-75.31,23.59,-75.288,23.568,-75.241,23.475,-75.158,23.336,-75.131,23.268]},{"b":[-75.744,24.139,-75.302,24.698],"l":[-75.526,24.45,-75.654,24.681,-75.71,24.698,-75.727,24.689,-75.744,24.655,-75.661,24.59,-75.639,24.529,-75.593,24.491,-75.494,24.33,-75.409,24.266,-75.412,24.221,-75.481,24.174,-75.503,24.139,-75.468,24.14,-75.369,24.159,-75.302,24.149,-75.308,24.2,-75.518,24.427,-75.526,24.45]},{"b":[-78.042,22.125,-77.842,22.302],"l":[-77.879,22.128,-77.842,22.149,-77.889,22.201,-77.894,22.215,-77.97,22.241,-77.986,22.302,-77.999,22.299,-78.007,22.248,-78.042,22.201,-78.012,22.166,-77.912,22.125,-77.879,22.128]},{"b":[-82.201,26.428,-82.037,26.548],"l":[-82.201,26.548,-82.184,26.481,-82.145,26.447,-82.073,26.428,-82.037,26.454,-82.116,26.461,-82.139,26.477,-82.201,26.548]},{"b":[-78.435,24.287,-77.735,25.202],"l":[-78.367,24.544,-78.258,24.483,-78.192,24.466,-78.146,24.493,-78.136,24.412,-78.076,24.365,-78.045,24.287,-77.983,24.335,-77.881,24.369,-77.853,24.403,-77.745,24.463,-77.735,24.496,-77.746,24.586,-77.744,24.707,-77.84,24.794,-77.919,24.943,-77.973,25.005,-77.975,25.085,-78.033,25.143,-78.163,25.202,-78.211,25.191,-78.159,25.022,-78.184,24.917,-78.299,24.754,-78.274,24.692,-78.26,24.687,-78.243,24.654,-78.319,24.59,-78.339,24.642,-78.435,24.628,-78.367,24.544]},{"b":[-81.631,24.59,-81.532,24.689],"l":[-81.562,24.689,-81.579,24.629,-81.631,24.59,-81.567,24.6,-81.532,24.614,-81.532,24.642,-81.562,24.689]},{"b":[-80.437,27.205,-80.171,27.851],"l":[-80.262,27.376,-80.171,27.205,-80.187,27.278,-80.356,27.679,-80.396,27.795,-80.437,27.851,-80.376,27.643,-80.262,27.376]},{"b":[-83.184,21.441,-82.562,21.943],"l":[-82.682,21.821,-82.715,21.89,-82.756,21.91,-82.991,21.943,-83.078,21.833,-83.083,21.791,-82.974,21.592,-83.007,21.566,-83.055,21.549,-83.113,21.574,-83.18,21.623,-83.184,21.593,-83.142,21.532,-83.067,21.469,-82.96,21.441,-82.853,21.444,-82.655,21.519,-82.562,21.572,-82.568,21.622,-82.629,21.767,-82.682,21.821]},{"b":[-82.169,26.494,-82.084,26.701],"l":[-82.085,26.494,-82.084,26.552,-82.121,26.666,-82.169,26.701,-82.136,26.592,-82.085,26.494]},{"b":[-79.628,22.638,-79.348,22.807],"l":[-79.382,22.681,-79.579,22.807,-79.628,22.805,-79.598,22.788,-79.523,22.711,-79.348,22.638,-79.35,22.664,-79.382,22.681]},{"b":[-78.986,26.501,-77.922,26.798],"l":[-78.234,26.637,-77.926,26.663,-77.922,26.691,-77.944,26.744,-78.089,26.714,-78.268,26.723,-78.307,26.702,-78.372,26.698,-78.493,26.729,-78.597,26.798,-78.633,26.726,-78.621,26.705,-78.633,26.659,-78.712,26.599,-78.798,26.582,-78.936,26.673,-78.986,26.69,-78.799,26.528,-78.744,26.501,-78.671,26.507,-78.516,26.559,-78.234,26.637]},{"b":[-80.581,24.954,-80.257,25.348],"l":[-80.257,25.348,-80.28,25.341,-80.351,25.297,-80.355,25.234,-80.404,25.179,-80.456,25.149,-80.481,25.102,-80.559,25.001,-80.581,24.954,-80.382,25.142,-80.257,25.348]},{"b":[-87.998,17.906,-87.849,18.155],"l":[-87.849,18.14,-87.859,18.154,-87.898,18.155,-87.953,18.001,-87.959,17.964,-87.998,17.906,-87.951,17.925,-87.849,18.14]},{"b":[-87.935,17.283,-87.789,17.546],"l":[-87.903,17.426,-87.935,17.323,-87.93,17.283,-87.853,17.423,-87.798,17.48,-87.789,17.524,-87.826,17.546,-87.833,17.501,-87.859,17.463,-87.903,17.426]},{"b":[-91.816,18.654,-91.537,18.778],"l":[-91.816,18.676,-91.796,18.654,-91.684,18.677,-91.654,18.711,-91.537,18.76,-91.55,18.774,-91.589,18.778,-91.816,18.676]},{"b":[-82.322,9.334,-82.233,9.432],"l":[-82.244,9.334,-82.233,9.381,-82.259,9.43,-82.276,9.432,-82.322,9.418,-82.244,9.334]},{"b":[-81.419,19.272,-81.107,19.385],"l":[-81.285,19.363,-81.296,19.341,-81.337,19.329,-81.37,19.349,-81.391,19.385,-81.419,19.375,-81.405,19.278,-81.304,19.272,-81.277,19.277,-81.225,19.304,-81.107,19.305,-81.13,19.347,-81.285,19.363]},{"b":[-87.019,20.272,-86.755,20.579],"l":[-86.928,20.552,-86.978,20.49,-87.019,20.382,-86.991,20.272,-86.94,20.303,-86.809,20.468,-86.755,20.552,-86.763,20.579,-86.829,20.559,-86.928,20.552]},{"b":[-62.84,17.224,-62.631,17.403],"l":[-62.714,17.353,-62.795,17.403,-62.827,17.386,-62.839,17.365,-62.84,17.347,-62.839,17.339,-62.776,17.303,-62.702,17.286,-62.656,17.224,-62.631,17.24,-62.641,17.262,-62.676,17.291,-62.714,17.353]},{"b":[-61.073,13.718,-60.887,14.093],"l":[-60.908,14.093,-60.945,14.073,-60.997,14.011,-61.064,13.916,-61.073,13.866,-61.061,13.783,-60.951,13.718,-60.895,13.822,-60.887,14.011,-60.908,14.093]},{"b":[-61.22,14.426,-60.826,14.875],"l":[-60.934,14.686,-60.919,14.735,-60.927,14.755,-60.953,14.756,-61.027,14.826,-61.127,14.875,-61.181,14.872,-61.213,14.849,-61.22,14.804,-61.141,14.652,-61.104,14.621,-61.011,14.602,-61.09,14.53,-61.089,14.51,-61.064,14.467,-60.899,14.474,-60.862,14.426,-60.837,14.437,-60.826,14.494,-60.87,14.614,-60.889,14.645,-60.934,14.686]},{"b":[-61.906,10.065,-60.918,10.84],"l":[-60.918,10.84,-61.079,10.832,-61.174,10.803,-61.37,10.797,-61.465,10.764,-61.592,10.748,-61.651,10.718,-61.635,10.699,-61.541,10.664,-61.499,10.639,-61.478,10.603,-61.465,10.539,-61.499,10.269,-61.529,10.253,-61.633,10.243,-61.661,10.192,-61.906,10.069,-61.772,10.085,-61.597,10.065,-61.174,10.078,-61.012,10.134,-61.004,10.168,-61,10.261,-60.968,10.323,-61.016,10.386,-61.037,10.482,-61.019,10.558,-61.034,10.67,-60.997,10.716,-60.918,10.84]},{"b":[-68.371,12.032,-68.206,12.302],"l":[-68.369,12.302,-68.371,12.258,-68.348,12.228,-68.307,12.207,-68.287,12.172,-68.282,12.082,-68.254,12.032,-68.206,12.145,-68.219,12.231,-68.369,12.302]},{"b":[-60.811,11.169,-60.525,11.325],"l":[-60.709,11.277,-60.804,11.208,-60.811,11.169,-60.756,11.179,-60.546,11.264,-60.525,11.325,-60.563,11.324,-60.709,11.277]},{"b":[-62.223,16.681,-62.148,16.81],"l":[-62.176,16.81,-62.191,16.804,-62.223,16.752,-62.222,16.7,-62.154,16.681,-62.148,16.74,-62.176,16.81]},{"b":[-59.647,13.062,-59.428,13.318],"l":[-59.592,13.318,-59.647,13.303,-59.643,13.15,-59.611,13.102,-59.522,13.062,-59.493,13.082,-59.428,13.153,-59.488,13.197,-59.592,13.318]},{"b":[-64.402,10.881,-63.817,11.167],"l":[-64.007,11.068,-64.028,11.002,-64.113,11.006,-64.185,11.043,-64.214,11.086,-64.25,11.08,-64.349,11.052,-64.402,10.982,-64.362,10.962,-64.219,10.942,-64.161,10.959,-64.101,10.901,-64.055,10.884,-63.994,10.881,-63.918,10.888,-63.827,10.976,-63.817,11,-63.849,11.131,-63.893,11.167,-64.007,11.068]},{"b":[-63.16,18.171,-62.98,18.27],"l":[-63.16,18.171,-63.001,18.222,-62.98,18.265,-63.026,18.27,-63.153,18.2,-63.16,18.171]},{"b":[-61.782,12.008,-61.607,12.237],"l":[-61.66,12.237,-61.715,12.185,-61.75,12.109,-61.756,12.046,-61.782,12.008,-61.715,12.013,-61.627,12.054,-61.607,12.223,-61.66,12.237]},{"b":[-61.869,17.549,-61.747,17.714],"l":[-61.762,17.549,-61.747,17.575,-61.75,17.661,-61.777,17.69,-61.82,17.697,-61.852,17.714,-61.866,17.704,-61.869,17.685,-61.844,17.596,-61.762,17.549]},{"b":[-61.318,15.886,-61.203,16.006],"l":[-61.25,16.006,-61.275,15.996,-61.318,15.955,-61.311,15.895,-61.286,15.886,-61.23,15.89,-61.203,15.921,-61.212,15.96,-61.25,16.006]},{"b":[-61.887,16.997,-61.686,17.169],"l":[-61.86,17.013,-61.748,16.997,-61.716,17.037,-61.695,17.049,-61.686,17.07,-61.686,17.098,-61.708,17.105,-61.739,17.138,-61.817,17.169,-61.887,17.098,-61.882,17.063,-61.86,17.013]},{"b":[-61.481,15.227,-61.251,15.633],"l":[-61.251,15.373,-61.277,15.527,-61.32,15.585,-61.458,15.633,-61.47,15.603,-61.481,15.525,-61.416,15.4,-61.375,15.227,-61.282,15.249,-61.251,15.373]},{"b":[-61.54,16.219,-61.173,16.507],"l":[-61.173,16.256,-61.355,16.363,-61.396,16.413,-61.406,16.468,-61.471,16.507,-61.511,16.478,-61.529,16.434,-61.501,16.36,-61.54,16.3,-61.522,16.228,-61.445,16.219,-61.327,16.23,-61.173,16.256]},{"b":[-61.794,15.962,-61.552,16.355],"l":[-61.642,16.326,-61.748,16.355,-61.767,16.34,-61.794,16.301,-61.759,16.062,-61.71,15.976,-61.67,15.962,-61.59,16.007,-61.564,16.048,-61.575,16.227,-61.552,16.271,-61.597,16.292,-61.642,16.326]},{"b":[-61.277,13.142,-61.124,13.359],"l":[-61.135,13.203,-61.124,13.294,-61.139,13.359,-61.182,13.356,-61.224,13.331,-61.268,13.288,-61.277,13.21,-61.204,13.142,-61.175,13.158,-61.135,13.203]},{"b":[-73.162,22.291,-72.747,22.455],"l":[-72.981,22.369,-72.889,22.36,-72.784,22.291,-72.747,22.327,-72.763,22.344,-72.831,22.386,-72.945,22.416,-72.979,22.415,-73.041,22.429,-73.127,22.455,-73.162,22.381,-73.11,22.368,-72.981,22.369]},{"b":[-69.159,12.045,-68.751,12.38],"l":[-68.827,12.159,-69.013,12.231,-69.077,12.342,-69.118,12.373,-69.159,12.38,-69.154,12.298,-68.995,12.142,-68.803,12.045,-68.751,12.06,-68.827,12.159]},{"b":[-71.848,21.752,-71.637,21.852],"l":[-71.722,21.79,-71.665,21.752,-71.661,21.765,-71.637,21.788,-71.668,21.833,-71.806,21.852,-71.848,21.843,-71.83,21.791,-71.722,21.79]},{"b":[-73.063,21.458,-72.916,21.562],"l":[-72.916,21.507,-72.995,21.562,-73.063,21.515,-73.049,21.458,-72.916,21.507]},{"b":[-72.019,21.83,-71.88,21.952],"l":[-71.955,21.864,-71.897,21.83,-71.88,21.84,-71.9,21.863,-71.932,21.952,-72.011,21.95,-72.019,21.918,-71.985,21.893,-71.964,21.892,-71.955,21.864]},{"b":[-72.342,21.755,-72.144,21.851],"l":[-72.191,21.77,-72.182,21.78,-72.144,21.793,-72.15,21.804,-72.219,21.796,-72.333,21.851,-72.342,21.795,-72.335,21.758,-72.301,21.755,-72.191,21.77]},{"b":[-161.131,58.561,-160.715,58.795],"l":[-161.085,58.671,-161.131,58.668,-161.07,58.569,-160.992,58.561,-160.919,58.577,-160.715,58.795,-160.769,58.789,-160.986,58.736,-161.085,58.671]},{"b":[-153.375,57,-152.908,57.185],"l":[-153.157,57.094,-153.134,57.093,-153.007,57.125,-152.933,57.129,-152.908,57.14,-152.908,57.152,-152.935,57.167,-153.285,57.185,-153.354,57.132,-153.375,57.052,-153.295,57,-153.235,57.029,-153.157,57.094]},{"b":[-152.069,60.358,-151.887,60.504],"l":[-151.96,60.504,-152.004,60.407,-152.069,60.358,-152.021,60.362,-151.987,60.374,-151.887,60.473,-151.899,60.49,-151.96,60.504]},{"b":[-152.637,58.479,-152.357,58.619],"l":[-152.516,58.479,-152.486,58.485,-152.393,58.541,-152.362,58.571,-152.357,58.595,-152.368,58.611,-152.396,58.619,-152.463,58.619,-152.605,58.566,-152.637,58.542,-152.589,58.509,-152.516,58.479]},{"b":[-153.52,57.822,-153.241,57.971],"l":[-153.241,57.85,-153.29,57.898,-153.347,57.933,-153.481,57.971,-153.52,57.956,-153.517,57.942,-153.465,57.909,-153.351,57.862,-153.295,57.829,-153.269,57.822,-153.241,57.85]},{"b":[-155.737,55.79,-155.564,55.924],"l":[-155.737,55.83,-155.723,55.802,-155.681,55.792,-155.605,55.79,-155.566,55.821,-155.564,55.887,-155.573,55.921,-155.594,55.924,-155.621,55.913,-155.737,55.83]},{"b":[-154.777,56.412,-154.445,56.601],"l":[-154.518,56.601,-154.624,56.561,-154.729,56.502,-154.761,56.471,-154.777,56.44,-154.774,56.42,-154.751,56.412,-154.683,56.436,-154.511,56.521,-154.445,56.573,-154.463,56.598,-154.518,56.601]},{"b":[-154.332,56.513,-154.102,56.609],"l":[-154.217,56.609,-154.322,56.571,-154.332,56.539,-154.258,56.513,-154.209,56.515,-154.15,56.53,-154.116,56.544,-154.107,56.558,-154.102,56.582,-154.11,56.603,-154.217,56.609]},{"b":[-146.703,60.268,-146.102,60.481],"l":[-146.372,60.422,-146.394,60.45,-146.56,60.481,-146.606,60.468,-146.67,60.433,-146.703,60.409,-146.703,60.396,-146.683,60.361,-146.65,60.336,-146.618,60.274,-146.595,60.268,-146.419,60.325,-146.202,60.368,-146.128,60.393,-146.102,60.411,-146.124,60.424,-146.18,60.429,-146.372,60.422]},{"b":[-145.284,60.313,-145.102,60.401],"l":[-145.284,60.337,-145.238,60.321,-145.15,60.313,-145.119,60.337,-145.102,60.388,-145.128,60.401,-145.284,60.337]},{"b":[-144.614,59.813,-144.236,60.015],"l":[-144.542,59.878,-144.614,59.813,-144.566,59.818,-144.403,59.921,-144.249,59.982,-144.236,60.015,-144.354,59.996,-144.445,59.951,-144.542,59.878]},{"b":[-147.872,59.799,-146.958,60.363],"l":[-147.02,60.332,-147.12,60.363,-147.181,60.358,-147.205,60.311,-147.337,60.185,-147.466,60.097,-147.607,60.037,-147.734,59.954,-147.768,59.944,-147.814,59.902,-147.872,59.828,-147.846,59.799,-147.736,59.813,-147.669,59.832,-147.645,59.854,-147.602,59.866,-147.54,59.868,-147.499,59.89,-147.479,59.934,-147.448,59.96,-147.404,59.97,-147.377,59.991,-147.346,60.052,-147.318,60.075,-146.987,60.254,-146.958,60.289,-147.02,60.332]},{"b":[-148.272,60.035,-147.914,60.152],"l":[-147.914,60.092,-148.08,60.152,-148.231,60.114,-148.272,60.053,-148.074,60.035,-148.022,60.065,-147.914,60.092]},{"b":[-147.891,60.178,-147.658,60.491],"l":[-147.842,60.351,-147.855,60.321,-147.891,60.299,-147.871,60.23,-147.805,60.231,-147.822,60.203,-147.816,60.185,-147.788,60.178,-147.76,60.19,-147.732,60.222,-147.712,60.273,-147.66,60.352,-147.69,60.399,-147.659,60.424,-147.658,60.45,-147.689,60.491,-147.703,60.487,-147.737,60.447,-147.76,60.439,-147.774,60.445,-147.779,60.466,-147.795,60.46,-147.838,60.371,-147.842,60.351]},{"b":[-148.124,60.818,-147.931,60.924],"l":[-147.931,60.826,-147.943,60.875,-147.964,60.9,-148.038,60.924,-148.102,60.916,-148.1,60.895,-148.124,60.844,-148.115,60.831,-148.057,60.818,-147.931,60.826]},{"b":[-133.454,54.684,-132.617,55.326],"l":[-132.677,54.726,-132.647,54.756,-132.634,54.84,-132.617,54.892,-132.649,54.907,-132.701,54.919,-132.772,54.926,-132.813,54.89,-132.838,54.881,-132.862,54.894,-132.946,55.003,-132.982,55.033,-132.996,55.111,-133.067,55.166,-133.097,55.214,-133.297,55.326,-133.429,55.304,-133.454,55.26,-133.418,55.211,-133.325,55.185,-133.251,55.175,-133.205,55.084,-133.123,54.97,-133.081,54.949,-133.075,54.921,-133.009,54.855,-132.89,54.763,-132.807,54.709,-132.706,54.684,-132.677,54.726]},{"b":[-134.391,56.077,-133.738,56.932],"l":[-133.855,56.582,-133.809,56.611,-133.767,56.6,-133.738,56.65,-133.778,56.729,-133.831,56.781,-133.925,56.776,-133.99,56.845,-134.001,56.869,-134.052,56.898,-134.143,56.932,-134.274,56.918,-134.374,56.839,-134.391,56.749,-134.384,56.724,-134.278,56.617,-134.29,56.58,-134.15,56.513,-134.084,56.456,-134.195,56.414,-134.245,56.203,-134.19,56.077,-134.122,56.077,-134.067,56.133,-134.024,56.119,-133.994,56.101,-133.971,56.108,-133.95,56.128,-133.939,56.194,-133.885,56.292,-133.87,56.389,-133.884,56.485,-133.855,56.582]},{"b":[-133.737,55.269,-133.282,55.559],"l":[-133.634,55.539,-133.737,55.497,-133.635,55.413,-133.65,55.269,-133.547,55.317,-133.493,55.362,-133.463,55.377,-133.429,55.418,-133.426,55.431,-133.282,55.498,-133.283,55.516,-133.305,55.544,-133.346,55.559,-133.455,55.522,-133.567,55.527,-133.634,55.539]},{"b":[-131.595,54.888,-131.232,55.264],"l":[-131.431,54.996,-131.456,54.931,-131.446,54.909,-131.406,54.894,-131.33,54.888,-131.232,54.904,-131.237,54.95,-131.34,55.08,-131.405,55.213,-131.513,55.263,-131.565,55.264,-131.578,55.249,-131.578,55.201,-131.556,55.137,-131.595,55.091,-131.592,55.026,-131.54,55.048,-131.482,55.035,-131.431,54.996]},{"b":[-132.948,56.511,-132.568,56.795],"l":[-132.907,56.637,-132.936,56.607,-132.948,56.567,-132.931,56.524,-132.885,56.512,-132.758,56.511,-132.747,56.526,-132.714,56.543,-132.634,56.553,-132.568,56.576,-132.599,56.636,-132.656,56.685,-132.843,56.795,-132.871,56.696,-132.907,56.637]},{"b":[-132.699,55.929,-132.067,56.499],"l":[-132.38,56.499,-132.506,56.335,-132.539,56.324,-132.599,56.242,-132.675,56.224,-132.699,56.198,-132.691,56.13,-132.66,56.078,-132.603,56.066,-132.451,56.056,-132.406,56.029,-132.421,55.98,-132.407,55.958,-132.369,55.94,-132.287,55.929,-132.21,55.953,-132.173,55.953,-132.133,55.943,-132.112,56.109,-132.067,56.244,-132.206,56.388,-132.317,56.488,-132.38,56.499]},{"b":[-133.038,56.244,-132.629,56.454],"l":[-132.935,56.442,-133.017,56.392,-133.038,56.365,-133.035,56.341,-132.891,56.259,-132.831,56.244,-132.78,56.247,-132.669,56.287,-132.647,56.313,-132.658,56.339,-132.653,56.364,-132.632,56.388,-132.629,56.412,-132.643,56.435,-132.706,56.448,-132.902,56.454,-132.935,56.442]},{"b":[-134.662,58.204,-134.313,58.333],"l":[-134.313,58.229,-134.399,58.287,-134.52,58.333,-134.648,58.312,-134.662,58.291,-134.594,58.243,-134.456,58.207,-134.32,58.204,-134.313,58.229]},{"b":[-129.268,52.464,-128.937,52.811],"l":[-129.215,52.804,-129.246,52.811,-129.264,52.801,-129.268,52.772,-129.25,52.722,-129.151,52.605,-129.102,52.574,-128.969,52.464,-128.937,52.51,-128.94,52.601,-128.994,52.662,-129.186,52.791,-129.215,52.804]},{"b":[-130.959,54.478,-130.753,54.632],"l":[-130.922,54.615,-130.953,54.542,-130.959,54.499,-130.95,54.478,-130.927,54.479,-130.805,54.544,-130.763,54.577,-130.753,54.6,-130.758,54.614,-130.777,54.619,-130.907,54.632,-130.922,54.615]},{"b":[-131.117,51.96,-131.011,52.151],"l":[-131.107,52.137,-131.117,52.101,-131.103,52.014,-131.081,51.98,-131.047,51.96,-131.029,51.962,-131.011,52.095,-131.098,52.151,-131.107,52.137]},{"b":[-130.707,53.844,-130.236,54.089],"l":[-130.316,54.047,-130.397,54.086,-130.448,54.089,-130.495,54.074,-130.647,53.991,-130.696,53.951,-130.707,53.921,-130.703,53.892,-130.683,53.863,-130.664,53.848,-130.644,53.845,-130.638,53.86,-130.646,53.894,-130.642,53.921,-130.625,53.941,-130.59,53.94,-130.537,53.918,-130.47,53.862,-130.407,53.856,-130.384,53.844,-130.338,53.866,-130.267,53.923,-130.236,53.959,-130.298,54.036,-130.316,54.047]},{"b":[-130.518,53.165,-129.755,53.631],"l":[-130.035,53.481,-130.195,53.55,-130.395,53.62,-130.452,53.631,-130.518,53.544,-130.411,53.491,-130.306,53.407,-130.151,53.346,-129.934,53.177,-129.869,53.165,-129.849,53.168,-129.769,53.217,-129.755,53.245,-129.945,53.436,-130.035,53.481]},{"b":[-128.149,51.457,-127.916,51.708],"l":[-127.933,51.605,-127.987,51.674,-127.999,51.704,-128.032,51.708,-128.123,51.667,-128.142,51.647,-128.149,51.627,-128.092,51.511,-128.045,51.474,-127.981,51.457,-127.941,51.457,-127.925,51.474,-127.916,51.506,-127.916,51.585,-127.933,51.605]},{"b":[-128.445,52.388,-128.247,52.784],"l":[-128.298,52.548,-128.248,52.741,-128.247,52.784,-128.365,52.782,-128.44,52.696,-128.436,52.56,-128.426,52.503,-128.412,52.473,-128.42,52.441,-128.445,52.388,-128.369,52.401,-128.344,52.426,-128.324,52.459,-128.298,52.548]},{"b":[-129.515,52.984,-129.314,53.188],"l":[-129.471,53.183,-129.501,53.188,-129.515,53.179,-129.5,53.129,-129.478,53.098,-129.41,53.024,-129.37,52.998,-129.329,52.984,-129.314,52.992,-129.344,53.053,-129.451,53.175,-129.471,53.183]},{"b":[-129.331,53.111,-129.168,53.33],"l":[-129.168,53.118,-129.177,53.259,-129.195,53.293,-129.238,53.33,-129.251,53.317,-129.253,53.285,-129.314,53.212,-129.331,53.174,-129.324,53.142,-129.306,53.121,-129.277,53.111,-129.173,53.111,-129.168,53.118]},{"b":[-139.291,69.539,-138.879,69.65],"l":[-139.291,69.598,-139.257,69.579,-139.126,69.539,-139.043,69.577,-138.879,69.59,-138.932,69.617,-139.073,69.648,-139.14,69.65,-139.291,69.598]},{"b":[-61.069,8.847,-60.849,9.07],"l":[-60.998,8.867,-60.916,8.899,-60.861,8.95,-60.849,8.996,-60.9,9.032,-60.895,9.053,-60.916,9.07,-60.945,9.055,-61.05,8.974,-61.069,8.947,-61.06,8.847,-60.998,8.867]},{"b":[-80.273,-3.012,-79.909,-2.669],"l":[-80.224,-2.753,-80.25,-2.812,-80.272,-2.952,-80.273,-2.996,-80.246,-3.008,-80.151,-3.012,-80.132,-2.973,-80.093,-2.846,-80.071,-2.834,-80.013,-2.82,-79.909,-2.726,-79.997,-2.674,-80.081,-2.669,-80.146,-2.696,-80.224,-2.753]},{"b":[-51.938,-1.453,-51.161,-0.541],"l":[-51.254,-0.541,-51.424,-0.566,-51.546,-0.65,-51.678,-0.855,-51.68,-1.086,-51.802,-1.203,-51.938,-1.453,-51.833,-1.434,-51.638,-1.342,-51.465,-1.211,-51.31,-1.024,-51.276,-1.022,-51.161,-0.667,-51.254,-0.541]},{"b":[-79.128,8.21,-79.065,8.296],"l":[-79.128,8.252,-79.11,8.21,-79.065,8.254,-79.085,8.296,-79.096,8.295,-79.128,8.252]},{"b":[-78.992,1.245,-78.9,1.36],"l":[-78.992,1.293,-78.966,1.245,-78.909,1.253,-78.9,1.36,-78.923,1.349,-78.992,1.293]},{"b":[-78.21,2.52,-78.114,2.646],"l":[-78.178,2.646,-78.21,2.609,-78.192,2.559,-78.141,2.52,-78.114,2.542,-78.119,2.604,-78.138,2.634,-78.178,2.646]},{"b":[-60.941,9.106,-60.736,9.218],"l":[-60.79,9.177,-60.736,9.203,-60.759,9.216,-60.782,9.218,-60.821,9.208,-60.845,9.192,-60.907,9.179,-60.939,9.132,-60.941,9.106,-60.821,9.138,-60.79,9.177]},{"b":[-44.598,-3.038,-44.481,-2.718],"l":[-44.481,-2.718,-44.502,-2.726,-44.569,-2.785,-44.582,-2.846,-44.565,-2.924,-44.598,-3.038,-44.499,-2.94,-44.483,-2.812,-44.487,-2.79,-44.481,-2.718]},{"b":[-38.787,-13.119,-38.6,-12.88],"l":[-38.668,-12.88,-38.685,-12.975,-38.787,-13.055,-38.783,-13.119,-38.744,-13.097,-38.601,-12.993,-38.6,-12.972,-38.615,-12.924,-38.668,-12.88]},{"b":[-39.022,-13.532,-38.904,-13.398],"l":[-39.007,-13.416,-39.022,-13.446,-38.993,-13.484,-38.978,-13.524,-38.938,-13.532,-38.904,-13.473,-38.907,-13.401,-38.98,-13.398,-39.007,-13.416]},{"b":[-45.021,-1.391,-44.883,-1.267],"l":[-44.979,-1.267,-44.996,-1.348,-45.011,-1.345,-45.021,-1.372,-44.968,-1.391,-44.947,-1.366,-44.883,-1.318,-44.888,-1.277,-44.979,-1.267]},{"b":[-50.345,-0.029,-49.697,0.305],"l":[-50.128,0.227,-50.273,0.232,-50.345,0.134,-50.339,0.043,-50.286,0.029,-50.113,0.033,-50.002,-0.029,-49.917,-0.023,-49.839,0.007,-49.697,0.216,-49.738,0.268,-49.879,0.305,-50.128,0.227]},{"b":[-50.509,1.893,-50.292,2.161],"l":[-50.491,2.129,-50.509,2.03,-50.456,1.91,-50.399,1.893,-50.299,1.939,-50.292,1.98,-50.342,2.142,-50.363,2.154,-50.419,2.161,-50.491,2.129]},{"b":[-51.038,-0.327,-50.651,-0.041],"l":[-50.653,-0.132,-50.651,-0.106,-50.667,-0.058,-50.765,-0.041,-50.842,-0.05,-50.995,-0.105,-51.026,-0.172,-51.022,-0.188,-51.038,-0.226,-51.019,-0.263,-50.926,-0.327,-50.653,-0.132]},{"b":[-49.83,-0.144,-49.372,0.084],"l":[-49.709,-0.144,-49.444,-0.112,-49.381,-0.055,-49.372,0.001,-49.4,0.057,-49.503,0.084,-49.602,0.063,-49.712,0.015,-49.803,-0.052,-49.83,-0.094,-49.709,-0.144]},{"b":[-50.624,-0.008,-50.332,0.591],"l":[-50.343,0.382,-50.351,0.582,-50.373,0.591,-50.397,0.581,-50.425,0.558,-50.426,0.425,-50.452,0.327,-50.526,0.247,-50.61,0.205,-50.624,0.054,-50.444,-0.008,-50.426,0.139,-50.332,0.259,-50.343,0.382]},{"b":[-50.282,0.359,-50.037,0.638],"l":[-50.261,0.359,-50.153,0.393,-50.04,0.523,-50.037,0.595,-50.059,0.638,-50.099,0.625,-50.113,0.605,-50.251,0.585,-50.282,0.517,-50.282,0.391,-50.261,0.359]},{"b":[-90.542,-0.773,-90.185,-0.485],"l":[-90.269,-0.485,-90.47,-0.517,-90.532,-0.581,-90.542,-0.676,-90.387,-0.773,-90.335,-0.772,-90.315,-0.757,-90.261,-0.742,-90.193,-0.659,-90.185,-0.545,-90.269,-0.485]},{"b":[-89.609,-0.952,-89.259,-0.68],"l":[-89.423,-0.722,-89.48,-0.793,-89.543,-0.827,-89.609,-0.889,-89.603,-0.913,-89.577,-0.934,-89.537,-0.952,-89.419,-0.911,-89.358,-0.826,-89.295,-0.786,-89.259,-0.728,-89.267,-0.705,-89.288,-0.69,-89.318,-0.68,-89.423,-0.722]},{"b":[-81.859,7.328,-81.603,7.622],"l":[-81.71,7.486,-81.671,7.523,-81.729,7.621,-81.752,7.622,-81.812,7.592,-81.857,7.508,-81.859,7.48,-81.852,7.453,-81.77,7.37,-81.658,7.328,-81.603,7.333,-81.613,7.38,-81.695,7.425,-81.71,7.486]},{"b":[-91.654,-0.478,-91.399,-0.256],"l":[-91.399,-0.322,-91.46,-0.256,-91.647,-0.284,-91.654,-0.311,-91.647,-0.391,-91.611,-0.444,-91.526,-0.478,-91.426,-0.461,-91.4,-0.421,-91.399,-0.322]},{"b":[-90.52,-1.342,-90.379,-1.221],"l":[-90.477,-1.221,-90.52,-1.299,-90.464,-1.342,-90.424,-1.34,-90.379,-1.292,-90.399,-1.262,-90.432,-1.24,-90.477,-1.221]},{"b":[-91.597,-1.02,-90.8,0.126],"l":[-90.959,-0.595,-90.968,-0.576,-90.951,-0.525,-90.976,-0.417,-91.176,-0.223,-91.21,-0.039,-91.272,0.025,-91.306,0.091,-91.361,0.126,-91.491,0.105,-91.509,0.062,-91.597,0.002,-91.59,-0.015,-91.55,-0.047,-91.469,-0.01,-91.429,-0.023,-91.369,-0.287,-91.25,-0.374,-91.197,-0.497,-91.121,-0.559,-91.145,-0.623,-91.334,-0.706,-91.458,-0.8,-91.495,-0.861,-91.484,-0.925,-91.419,-0.997,-91.372,-1.017,-91.131,-1.02,-90.906,-0.941,-90.8,-0.752,-90.863,-0.672,-90.959,-0.595]},{"b":[-90.868,-0.364,-90.553,-0.16],"l":[-90.668,-0.19,-90.78,-0.16,-90.82,-0.192,-90.868,-0.271,-90.809,-0.329,-90.62,-0.364,-90.574,-0.334,-90.553,-0.278,-90.668,-0.19]},{"b":[-109.434,-27.171,-109.223,-27.068],"l":[-109.39,-27.068,-109.429,-27.116,-109.434,-27.171,-109.28,-27.14,-109.223,-27.101,-109.276,-27.096,-109.39,-27.068]},{"b":[-106.639,21.529,-106.502,21.712],"l":[-106.524,21.652,-106.536,21.676,-106.597,21.712,-106.639,21.698,-106.634,21.613,-106.607,21.561,-106.531,21.529,-106.502,21.611,-106.524,21.652]},{"b":[-111.064,18.72,-110.914,18.863],"l":[-110.989,18.863,-111.04,18.83,-111.064,18.782,-110.975,18.72,-110.914,18.741,-110.942,18.802,-110.989,18.863]},{"b":[-110.703,24.892,-110.539,25.088],"l":[-110.703,25.047,-110.657,24.969,-110.59,24.908,-110.539,24.892,-110.567,25.003,-110.595,25.042,-110.69,25.088,-110.699,25.081,-110.703,25.047]},{"b":[-109.9,24.148,-109.794,24.345],"l":[-109.89,24.345,-109.9,24.331,-109.878,24.201,-109.827,24.148,-109.805,24.151,-109.796,24.164,-109.794,24.183,-109.89,24.345]},{"b":[-113.594,29.035,-113.156,29.573],"l":[-113.202,29.302,-113.374,29.339,-113.376,29.417,-113.416,29.486,-113.508,29.56,-113.587,29.573,-113.594,29.463,-113.581,29.413,-113.496,29.308,-113.265,29.097,-113.163,29.035,-113.156,29.052,-113.178,29.132,-113.202,29.302]},{"b":[-114.789,31.701,-114.688,31.794],"l":[-114.789,31.747,-114.727,31.701,-114.694,31.706,-114.688,31.724,-114.709,31.757,-114.771,31.794,-114.785,31.79,-114.789,31.747]},{"b":[-115.353,28.037,-115.149,28.368],"l":[-115.234,28.368,-115.274,28.343,-115.26,28.221,-115.353,28.104,-115.184,28.037,-115.171,28.069,-115.149,28.172,-115.197,28.328,-115.234,28.368]},{"b":[-112.013,24.346,-111.699,24.551],"l":[-111.857,24.538,-111.941,24.551,-112.013,24.533,-111.712,24.346,-111.699,24.394,-111.857,24.538]},{"b":[-111.225,25.836,-111.088,26.076],"l":[-111.091,26.076,-111.139,26.07,-111.183,26.041,-111.225,25.836,-111.204,25.85,-111.135,25.999,-111.094,25.974,-111.088,25.985,-111.1,26.021,-111.091,26.076]},{"b":[-112.531,28.769,-112.203,29.24],"l":[-112.203,29.005,-112.249,29.126,-112.263,29.207,-112.285,29.24,-112.424,29.204,-112.47,29.168,-112.531,28.894,-112.514,28.848,-112.355,28.773,-112.278,28.769,-112.203,29.005]},{"b":[-112.297,24.535,-112.057,25.286],"l":[-112.164,24.8,-112.195,24.841,-112.198,24.885,-112.132,25.224,-112.159,25.286,-112.222,24.951,-112.297,24.79,-112.21,24.763,-112.175,24.73,-112.163,24.65,-112.077,24.535,-112.057,24.546,-112.067,24.584,-112.126,24.654,-112.13,24.73,-112.164,24.8]},{"b":[166.893,-45.3,167.023,-45.18],"l":[166.893,-45.241,166.931,-45.277,167.023,-45.3,166.979,-45.18,166.963,-45.18,166.893,-45.241]},{"b":[124.288,10.601,124.382,10.707],"l":[124.335,10.707,124.288,10.601,124.316,10.606,124.381,10.633,124.382,10.68,124.371,10.691,124.335,10.707]},{"b":[125.133,9.933,125.288,10.155],"l":[125.231,10.116,125.176,10.151,125.133,10.155,125.159,10.063,125.288,9.933,125.281,9.982,125.231,10.116]},{"b":[122.797,7.315,122.948,7.433],"l":[122.915,7.433,122.871,7.397,122.822,7.428,122.797,7.393,122.805,7.316,122.84,7.315,122.944,7.361,122.948,7.386,122.937,7.409,122.915,7.433]},{"b":[125.683,6.906,125.785,7.186],"l":[125.783,7.131,125.714,7.186,125.683,7.073,125.708,7.04,125.769,6.906,125.785,6.963,125.783,7.131]},{"b":[125.922,9.568,125.993,9.759],"l":[125.968,9.759,125.949,9.739,125.922,9.621,125.952,9.568,125.971,9.594,125.993,9.685,125.968,9.759]},{"b":[124.36,11.486,124.622,11.695],"l":[124.565,11.64,124.511,11.687,124.437,11.695,124.36,11.666,124.429,11.532,124.483,11.486,124.608,11.492,124.622,11.55,124.565,11.64]},{"b":[124.731,11.543,124.854,11.715],"l":[124.788,11.683,124.731,11.715,124.744,11.659,124.781,11.581,124.807,11.558,124.836,11.543,124.854,11.595,124.821,11.627,124.788,11.683]},{"b":[134.795,-6.442,134.886,-6.324],"l":[134.795,-6.393,134.795,-6.442,134.82,-6.434,134.886,-6.324,134.852,-6.325,134.823,-6.35,134.795,-6.393]},{"b":[134.629,-6.765,134.736,-6.623],"l":[134.629,-6.713,134.631,-6.733,134.657,-6.765,134.674,-6.75,134.726,-6.669,134.736,-6.623,134.698,-6.626,134.663,-6.658,134.629,-6.713]},{"b":[127.834,-3.02,127.988,-2.937],"l":[127.834,-3.004,127.85,-3.016,127.938,-3.02,127.988,-2.937,127.938,-2.952,127.834,-3.004]},{"b":[127.488,-3.359,127.629,-3.254],"l":[127.554,-3.254,127.53,-3.262,127.488,-3.288,127.531,-3.331,127.629,-3.359,127.606,-3.315,127.554,-3.254]},{"b":[123.972,-6.022,124.051,-5.88],"l":[124.023,-5.902,123.976,-5.88,123.972,-5.939,124.006,-5.967,124.042,-6.022,124.051,-5.974,124.023,-5.902]},{"b":[123.541,-5.373,123.627,-5.25],"l":[123.55,-5.332,123.583,-5.367,123.623,-5.373,123.627,-5.272,123.561,-5.25,123.543,-5.271,123.541,-5.298,123.55,-5.332]},{"b":[120.633,-7.125,120.782,-7.018],"l":[120.633,-7.018,120.641,-7.116,120.672,-7.125,120.774,-7.119,120.782,-7.063,120.746,-7.06,120.633,-7.018]},{"b":[122.888,-8.604,123.153,-8.44],"l":[123.011,-8.448,122.933,-8.497,122.904,-8.531,122.888,-8.587,122.946,-8.604,122.977,-8.545,123.03,-8.495,123.153,-8.476,123.138,-8.457,123.089,-8.44,123.011,-8.448]},{"b":[117.482,-8.368,117.669,-8.148],"l":[117.546,-8.152,117.491,-8.183,117.482,-8.239,117.506,-8.307,117.49,-8.349,117.534,-8.368,117.556,-8.367,117.669,-8.189,117.665,-8.148,117.546,-8.152]},{"b":[69.469,66.49,70.11,66.751],"l":[70.057,66.599,70.058,66.627,70.077,66.696,69.918,66.712,69.8,66.736,69.616,66.739,69.503,66.751,69.469,66.716,69.651,66.565,69.845,66.49,70.021,66.502,70.059,66.518,70.11,66.569,70.057,66.599]},{"b":[96.739,76.188,97.053,76.355],"l":[96.854,76.199,96.974,76.237,97.053,76.303,97.045,76.315,96.99,76.343,96.878,76.355,96.835,76.345,96.833,76.324,96.74,76.258,96.739,76.207,96.754,76.196,96.798,76.188,96.854,76.199]},{"b":[99.915,79.602,100.3,79.701],"l":[100.068,79.701,99.956,79.69,99.942,79.671,99.915,79.602,100.136,79.614,100.3,79.67,100.142,79.684,100.068,79.701]},{"b":[161.083,68.9,161.618,69.639],"l":[161.521,69.634,161.505,69.639,161.41,69.596,161.323,69.541,161.111,69.47,161.083,69.406,161.165,69.334,161.125,69.197,161.137,69.11,161.183,69.082,161.364,69.044,161.461,68.996,161.456,68.966,161.422,68.9,161.467,68.901,161.507,68.928,161.517,68.97,161.495,69.016,161.394,69.106,161.378,69.194,161.373,69.293,161.351,69.369,161.374,69.414,161.54,69.437,161.609,69.501,161.618,69.592,161.521,69.634]},{"b":[129.687,31.64,129.794,31.787],"l":[129.707,31.718,129.687,31.64,129.718,31.657,129.794,31.742,129.787,31.787,129.707,31.718]},{"b":[126.319,36.428,126.418,36.613],"l":[126.387,36.571,126.319,36.613,126.338,36.471,126.404,36.428,126.418,36.493,126.387,36.571]},{"b":[124.846,39.508,124.935,39.608],"l":[124.89,39.602,124.846,39.559,124.849,39.508,124.905,39.536,124.935,39.608,124.89,39.602]},{"b":[121.132,28.063,121.251,28.204],"l":[121.205,28.204,121.134,28.135,121.132,28.063,121.164,28.063,121.251,28.086,121.251,28.145,121.234,28.181,121.205,28.204]},{"b":[113.404,22.758,113.564,22.905],"l":[113.465,22.905,113.404,22.903,113.426,22.859,113.463,22.832,113.486,22.828,113.564,22.758,113.555,22.804,113.521,22.852,113.465,22.905]},{"b":[112.713,21.582,112.863,21.772],"l":[112.782,21.772,112.761,21.733,112.713,21.698,112.733,21.67,112.742,21.618,112.771,21.582,112.79,21.602,112.801,21.695,112.813,21.712,112.863,21.753,112.839,21.765,112.782,21.772]},{"b":[112.525,21.619,112.648,21.71],"l":[112.559,21.675,112.525,21.623,112.546,21.619,112.644,21.64,112.648,21.71,112.559,21.675]},{"b":[113.839,22.21,114.044,22.333],"l":[113.998,22.21,114.003,22.278,114.044,22.333,113.882,22.28,113.839,22.242,113.852,22.22,113.877,22.21,113.998,22.21]},{"b":[107.078,10.337,107.195,10.446],"l":[107.078,10.388,107.084,10.337,107.168,10.397,107.195,10.446,107.177,10.446,107.151,10.42,107.078,10.388]},{"b":[103.223,10.68,103.318,10.782],"l":[103.223,10.782,103.223,10.76,103.281,10.68,103.318,10.719,103.223,10.782]},{"b":[8.4,54.688,8.588,54.76],"l":[8.51,54.76,8.468,54.757,8.418,54.739,8.4,54.714,8.454,54.691,8.549,54.688,8.588,54.713,8.573,54.749,8.51,54.76]},{"b":[6.642,53.579,6.801,53.625],"l":[6.755,53.625,6.669,53.606,6.642,53.579,6.735,53.583,6.801,53.625,6.755,53.625]},{"b":[-90.025,61.868,-61.3,73.855],"l":[-62.624,66.016,-62.497,65.974,-62.448,65.946,-62.41,65.906,-62.388,65.868,-62.382,65.833,-62.486,65.804,-62.61,65.724,-62.659,65.64,-62.772,65.632,-62.817,65.648,-62.969,65.622,-63.169,65.657,-63.241,65.696,-63.459,65.853,-63.464,65.835,-63.41,65.756,-63.421,65.709,-63.652,65.674,-63.651,65.661,-63.509,65.636,-63.337,65.617,-63.364,65.543,-63.363,65.23,-63.402,65.118,-63.486,65.021,-63.607,64.928,-63.737,64.989,-63.789,65.051,-63.833,65.083,-63.896,65.109,-63.976,65.121,-64.061,65.122,-64.152,65.066,-64.25,65.114,-64.346,65.172,-64.31,65.325,-64.27,65.401,-64.286,65.4,-64.34,65.364,-64.47,65.253,-64.555,65.117,-64.665,65.169,-64.765,65.234,-64.847,65.3,-64.98,65.375,-65.108,65.464,-65.176,65.568,-65.207,65.59,-65.282,65.677,-65.311,65.702,-65.337,65.71,-65.402,65.764,-65.378,65.822,-65.277,65.891,-65.185,65.94,-65.032,65.989,-64.854,66.016,-64.773,66.079,-64.673,66.193,-64.564,66.272,-64.445,66.317,-64.504,66.325,-64.655,66.287,-64.761,66.231,-64.887,66.137,-65.004,66.078,-65.305,66.008,-65.416,65.995,-65.544,65.987,-65.826,65.997,-65.891,66.02,-65.857,66.086,-65.656,66.205,-65.688,66.213,-65.759,66.171,-65.856,66.142,-65.94,66.127,-66.064,66.133,-66.209,66.206,-66.277,66.229,-66.419,66.254,-66.477,66.28,-66.712,66.46,-66.76,66.508,-66.787,66.556,-66.863,66.595,-66.986,66.627,-67.015,66.622,-66.97,66.582,-66.969,66.547,-67.077,66.525,-67.19,66.533,-67.307,66.57,-67.318,66.52,-67.192,66.433,-67.19,66.322,-67.225,66.31,-67.311,66.304,-67.369,66.317,-67.56,66.4,-67.741,66.458,-67.868,66.49,-67.883,66.467,-67.801,66.367,-67.704,66.269,-67.547,66.187,-67.297,66.09,-67.183,66.034,-67.273,65.956,-67.35,65.93,-67.399,65.922,-67.551,65.922,-67.828,65.965,-67.958,66.014,-68.147,66.13,-68.46,66.249,-68.528,66.249,-68.749,66.2,-68.714,66.192,-68.572,66.189,-68.467,66.173,-68.217,66.079,-68.198,66.039,-68.261,65.995,-68.257,65.939,-68.187,65.871,-68.115,65.828,-67.968,65.797,-67.894,65.793,-67.866,65.774,-67.954,65.623,-67.962,65.582,-67.937,65.565,-67.906,65.563,-67.717,65.625,-67.638,65.64,-67.57,65.644,-67.49,65.626,-67.4,65.588,-67.346,65.549,-67.33,65.509,-67.303,65.483,-67.118,65.44,-67.135,65.421,-67.326,65.357,-67.337,65.347,-67.298,65.342,-67.178,65.304,-67.067,65.244,-66.999,65.173,-66.985,65.138,-66.986,65.105,-66.97,65.085,-66.912,65.081,-66.887,65.094,-66.861,65.092,-66.831,65.074,-66.8,65.02,-66.733,64.86,-66.697,64.815,-66.677,64.814,-66.667,64.974,-66.635,65,-66.518,64.972,-66.345,64.91,-66.224,64.854,-66.21,64.828,-66.302,64.778,-66.282,64.755,-66.215,64.722,-66.152,64.735,-66.108,64.791,-66.03,64.847,-65.939,64.886,-65.768,64.854,-65.627,64.771,-65.605,64.742,-65.513,64.706,-65.432,64.726,-65.275,64.632,-65.349,64.589,-65.513,64.526,-65.529,64.505,-65.49,64.51,-65.179,64.51,-65.095,64.485,-65.075,64.437,-65.213,64.303,-65.34,64.315,-65.507,64.318,-65.594,64.311,-65.58,64.294,-65.348,64.232,-65.282,64.182,-65.193,64.13,-65.15,64.087,-65.151,64.068,-65.187,64.038,-65.17,64.028,-65.011,64.009,-64.912,64.026,-64.788,64.033,-64.678,64.028,-64.67,64.01,-64.686,63.961,-64.798,63.916,-64.768,63.905,-64.637,63.918,-64.576,63.897,-64.498,63.79,-64.411,63.706,-64.482,63.687,-64.562,63.68,-64.55,63.573,-64.499,63.463,-64.498,63.358,-64.514,63.264,-64.587,63.243,-64.665,63.245,-64.696,63.269,-64.886,63.549,-64.933,63.599,-64.99,63.643,-65.192,63.764,-65.184,63.745,-65.134,63.689,-65.089,63.606,-65.031,63.44,-65.005,63.333,-65.017,63.293,-65.058,63.283,-65.069,63.263,-65.049,63.235,-64.895,63.126,-64.82,63.06,-64.767,62.992,-64.718,62.946,-64.672,62.922,-64.684,62.902,-64.752,62.887,-64.869,62.88,-64.923,62.889,-65.133,62.952,-65.163,62.933,-65.047,62.701,-65.05,62.646,-65.108,62.626,-65.18,62.649,-65.266,62.715,-65.397,62.788,-65.572,62.869,-65.74,62.932,-65.78,62.93,-65.806,62.912,-65.834,62.909,-65.864,62.921,-65.92,62.969,-65.979,63.001,-66.224,63.107,-66.249,63.108,-66.226,63.076,-66.201,63.006,-66.229,62.991,-66.293,62.997,-66.414,63.027,-66.496,63.097,-66.6,63.219,-66.655,63.265,-66.66,63.235,-66.631,63.119,-66.636,63.08,-66.697,63.07,-66.723,63.08,-66.749,63.111,-66.773,63.162,-66.831,63.201,-66.923,63.228,-66.975,63.256,-67,63.305,-67.018,63.317,-67.18,63.305,-67.261,63.341,-67.495,63.481,-67.709,63.634,-67.844,63.715,-67.893,63.734,-67.821,63.635,-67.743,63.489,-67.723,63.423,-67.759,63.42,-67.838,63.449,-68.244,63.637,-68.494,63.725,-68.633,63.741,-68.859,63.752,-68.911,63.703,-68.789,63.595,-68.671,63.514,-68.555,63.459,-68.374,63.352,-68.208,63.215,-68.141,63.172,-67.915,63.114,-67.797,63.098,-67.676,63.094,-67.665,63.073,-67.724,63.034,-67.737,63.01,-67.468,62.948,-67.367,62.914,-67.269,62.858,-67.213,62.844,-66.98,62.701,-66.922,62.678,-66.714,62.632,-66.645,62.602,-66.531,62.51,-66.459,62.463,-66.357,62.352,-66.281,62.303,-66.095,62.246,-66.016,62.23,-65.98,62.209,-66.004,62.158,-66.027,62.137,-66.133,62.102,-66.116,62.054,-66.056,61.967,-66.059,61.914,-66.124,61.893,-66.257,61.868,-66.324,61.87,-66.425,61.891,-66.551,61.926,-66.803,62.013,-67.181,62.073,-67.322,62.105,-67.369,62.134,-67.44,62.151,-68.379,62.235,-68.536,62.256,-68.634,62.281,-68.724,62.319,-69.082,62.405,-69.126,62.424,-69.366,62.572,-69.545,62.745,-69.605,62.768,-69.8,62.79,-69.962,62.776,-70.071,62.757,-70.236,62.763,-70.344,62.792,-70.571,62.869,-70.801,62.91,-71.002,62.978,-71.106,63.002,-71.096,63.02,-70.946,63.121,-70.993,63.119,-71.254,63.043,-71.347,63.066,-71.501,63.126,-71.617,63.187,-71.855,63.355,-71.992,63.416,-71.973,63.43,-71.819,63.435,-71.697,63.43,-71.614,63.444,-71.456,63.512,-71.387,63.555,-71.381,63.58,-71.513,63.587,-71.542,63.599,-71.566,63.627,-71.627,63.663,-71.725,63.706,-71.838,63.725,-72.223,63.709,-72.29,63.728,-72.289,63.757,-72.213,63.839,-72.172,63.872,-72.159,63.89,-72.174,63.893,-72.226,63.891,-72.45,63.818,-72.498,63.823,-72.586,63.901,-72.639,63.989,-72.678,64.02,-72.73,64.03,-72.913,64.117,-73.174,64.282,-73.27,64.333,-73.377,64.38,-73.455,64.399,-73.444,64.423,-73.278,64.56,-73.271,64.583,-73.413,64.574,-73.627,64.603,-73.728,64.568,-73.793,64.566,-73.868,64.585,-73.91,64.578,-73.95,64.466,-73.981,64.438,-74.026,64.423,-74.065,64.425,-74.099,64.444,-74.098,64.47,-74.13,64.608,-74.205,64.628,-74.416,64.633,-74.461,64.645,-74.512,64.67,-74.556,64.717,-74.593,64.786,-74.634,64.824,-74.681,64.831,-74.719,64.825,-74.748,64.807,-74.813,64.796,-74.916,64.792,-74.919,64.766,-74.823,64.717,-74.73,64.647,-74.64,64.557,-74.695,64.497,-74.894,64.466,-75.067,64.457,-75.215,64.469,-75.328,64.49,-75.488,64.541,-75.715,64.524,-75.767,64.392,-75.815,64.385,-76.032,64.388,-76.118,64.376,-76.407,64.303,-76.495,64.293,-76.562,64.302,-76.627,64.284,-76.724,64.242,-76.856,64.238,-77.024,64.271,-77.166,64.285,-77.283,64.28,-77.403,64.3,-77.527,64.344,-77.628,64.363,-77.76,64.36,-77.791,64.367,-77.985,64.461,-78.045,64.499,-78.175,64.618,-78.198,64.665,-78.201,64.715,-78.19,64.752,-78.145,64.808,-78.096,64.939,-78.055,64.983,-77.995,65.023,-77.876,65.073,-77.447,65.162,-77.361,65.197,-77.364,65.22,-77.461,65.328,-77.46,65.356,-77.428,65.372,-77.358,65.435,-77.327,65.453,-77.251,65.463,-77.094,65.431,-76.959,65.418,-76.779,65.414,-76.482,65.37,-76.067,65.285,-75.828,65.227,-75.648,65.141,-75.52,65.056,-75.502,65.013,-75.561,64.947,-75.591,64.928,-75.589,64.905,-75.556,64.879,-75.452,64.842,-75.427,64.856,-75.435,64.901,-75.414,64.939,-75.363,64.969,-75.357,65.009,-75.397,65.058,-75.446,65.1,-75.505,65.135,-75.773,65.257,-75.799,65.298,-75.709,65.316,-75.317,65.275,-75.166,65.284,-75.109,65.331,-75.048,65.364,-74.982,65.381,-74.85,65.389,-74.665,65.367,-74.575,65.364,-74.495,65.372,-74.391,65.398,-74.237,65.484,-74.138,65.503,-73.99,65.517,-73.878,65.519,-73.675,65.484,-73.551,65.485,-73.561,65.543,-73.643,65.653,-73.746,65.767,-73.826,65.805,-74.033,65.877,-74.276,66.013,-74.401,66.097,-74.434,66.139,-74.416,66.167,-74.375,66.208,-73.934,66.358,-73.584,66.507,-73.431,66.583,-73.357,66.636,-73.281,66.675,-73.201,66.699,-73.033,66.728,-72.985,66.765,-72.975,66.829,-72.947,66.883,-72.789,67.031,-72.668,67.07,-72.485,67.098,-72.365,67.133,-72.22,67.254,-72.234,67.284,-72.301,67.307,-72.353,67.342,-72.576,67.659,-72.725,67.812,-72.904,67.945,-73.063,68.107,-73.328,68.267,-73.331,68.309,-73.284,68.357,-73.307,68.368,-73.58,68.298,-73.644,68.295,-73.749,68.325,-73.821,68.363,-73.879,68.429,-73.873,68.464,-73.834,68.497,-73.783,68.578,-73.781,68.619,-73.798,68.659,-73.822,68.686,-73.852,68.701,-73.935,68.711,-74.073,68.715,-74.118,68.701,-73.966,68.579,-73.989,68.549,-74.183,68.535,-74.27,68.541,-74.35,68.556,-74.422,68.58,-74.648,68.708,-74.696,68.756,-74.681,68.79,-74.7,68.808,-74.746,68.797,-74.808,68.796,-74.893,68.808,-74.91,68.823,-74.752,68.892,-74.743,68.913,-74.816,68.936,-74.925,68.941,-74.954,68.961,-74.917,68.983,-74.769,69.021,-74.717,69.046,-74.805,69.064,-74.855,69.066,-74.954,69.025,-75.104,68.941,-75.213,68.909,-75.363,68.948,-75.457,68.961,-75.523,68.953,-75.623,68.888,-75.842,68.84,-76.235,68.728,-76.403,68.692,-76.585,68.699,-76.619,68.721,-76.616,68.76,-76.604,68.792,-76.582,68.816,-76.575,68.847,-76.588,68.974,-76.557,69.009,-76.495,69.03,-76.381,69.052,-76.089,69.026,-75.954,69.031,-75.859,69.06,-75.763,69.103,-75.668,69.159,-75.648,69.213,-75.749,69.3,-75.787,69.319,-76.046,69.386,-76.19,69.411,-76.316,69.422,-76.408,69.441,-76.465,69.469,-76.52,69.517,-76.525,69.549,-76.516,69.591,-76.463,69.62,-76.231,69.653,-76.234,69.662,-76.424,69.687,-76.513,69.684,-76.59,69.656,-76.687,69.591,-76.742,69.573,-76.916,69.611,-77.02,69.617,-77.09,69.635,-77.129,69.653,-77.105,69.671,-77.019,69.689,-76.869,69.745,-76.859,69.775,-76.962,69.825,-77.016,69.836,-77.232,69.855,-77.494,69.836,-77.592,69.846,-77.635,69.9,-77.663,69.966,-77.675,70.042,-77.722,70.171,-77.774,70.239,-77.843,70.247,-78.157,70.219,-78.231,70.219,-78.283,70.229,-78.491,70.316,-78.575,70.346,-78.621,70.353,-78.773,70.445,-78.831,70.463,-78.9,70.509,-78.98,70.581,-79.066,70.604,-79.16,70.575,-79.253,70.535,-79.347,70.482,-79.397,70.437,-79.405,70.401,-79.347,70.372,-79.018,70.325,-78.934,70.294,-78.863,70.242,-78.81,70.179,-78.775,70.104,-78.778,70.048,-78.819,70.01,-78.89,69.977,-79.093,69.925,-79.303,69.895,-79.515,69.888,-79.616,69.895,-80.162,69.996,-80.26,69.997,-80.387,70.01,-80.67,70.052,-80.826,70.057,-81.098,70.091,-81.56,70.111,-81.652,70.095,-81.529,70.048,-81.422,70.025,-81.329,70.024,-81.197,69.983,-81.024,69.9,-80.925,69.851,-80.843,69.792,-80.84,69.771,-80.922,69.731,-81.565,69.943,-81.958,69.869,-82.139,69.841,-82.294,69.837,-82.488,69.866,-82.925,69.968,-83.091,70.004,-83.15,70.009,-83.531,69.965,-83.859,69.963,-84.522,70.005,-84.765,70.034,-84.829,70.063,-84.909,70.078,-85.053,70.078,-85.432,70.111,-85.78,70.037,-86.198,70.105,-86.322,70.145,-86.361,70.173,-86.483,70.289,-86.5,70.35,-86.465,70.406,-86.431,70.445,-86.397,70.465,-86.624,70.401,-86.704,70.391,-86.809,70.388,-87.122,70.412,-87.172,70.4,-87.156,70.377,-87.074,70.345,-87.063,70.325,-87.238,70.31,-87.502,70.326,-87.618,70.319,-87.67,70.31,-87.789,70.258,-87.838,70.247,-87.901,70.252,-88.178,70.369,-88.402,70.442,-88.663,70.471,-88.783,70.494,-88.848,70.523,-89.208,70.76,-89.258,70.811,-89.372,70.996,-89.41,71.036,-89.456,71.062,-89.366,71.067,-89.025,71.045,-88.696,71.046,-88.517,71.031,-88.309,70.984,-88.039,70.951,-87.845,70.944,-87.534,70.957,-87.182,70.988,-87.14,71.012,-87.369,71.053,-87.572,71.108,-87.76,71.179,-87.872,71.209,-88.061,71.227,-88.59,71.24,-89.079,71.288,-89.418,71.352,-89.693,71.423,-89.805,71.462,-89.846,71.492,-89.889,71.586,-89.934,71.743,-89.977,71.848,-90.02,71.902,-90.025,71.949,-89.931,72.049,-89.664,72.158,-89.657,72.175,-89.711,72.18,-89.823,72.208,-89.859,72.248,-89.873,72.313,-89.874,72.367,-89.862,72.412,-89.817,72.468,-89.702,72.568,-89.536,72.69,-89.358,72.804,-89.327,72.842,-89.311,72.943,-89.288,73.017,-89.263,73.069,-89.225,73.108,-89.115,73.182,-88.977,73.252,-88.761,73.312,-88.743,73.335,-88.74,73.365,-88.727,73.388,-88.705,73.403,-88.17,73.595,-87.926,73.673,-87.72,73.723,-87.472,73.759,-86.769,73.834,-86.406,73.855,-85.951,73.85,-85.11,73.808,-85.009,73.779,-84.984,73.764,-84.947,73.722,-84.975,73.695,-85.204,73.604,-85.494,73.528,-85.682,73.461,-86.001,73.313,-86.086,73.26,-86.481,72.96,-86.575,72.911,-86.629,72.871,-86.668,72.763,-86.656,72.724,-86.595,72.661,-86.38,72.525,-86.323,72.461,-86.324,72.402,-86.348,72.262,-86.351,72.191,-86.341,72.123,-86.297,72.026,-86.218,71.899,-86.036,71.771,-85.75,71.641,-85.537,71.555,-85.327,71.492,-85.079,71.398,-85.023,71.353,-85.138,71.303,-85.405,71.227,-85.757,71.194,-85.945,71.163,-86.179,71.096,-86.473,71.043,-86.589,71.011,-86.55,70.989,-86.321,71.017,-86.127,71.049,-85.825,71.126,-85.644,71.152,-85.095,71.152,-85.002,71.137,-85.043,71.092,-85.066,71.079,-85.048,71.059,-84.988,71.032,-84.87,71.002,-84.824,71.029,-84.79,71.093,-84.709,71.359,-84.674,71.439,-84.658,71.515,-84.66,71.586,-84.699,71.631,-84.84,71.659,-85.032,71.654,-85.131,71.661,-85.25,71.675,-85.339,71.697,-85.397,71.727,-85.512,71.817,-85.596,71.866,-85.813,71.956,-85.912,71.987,-85.862,72.022,-85.665,72.063,-85.546,72.102,-85.406,72.215,-85.322,72.233,-85.019,72.218,-84.608,72.129,-84.352,72.053,-84.284,72.044,-84.282,72.058,-84.347,72.094,-84.643,72.19,-84.778,72.259,-84.842,72.308,-84.811,72.33,-84.645,72.351,-84.623,72.377,-84.849,72.406,-84.964,72.406,-85.057,72.384,-85.156,72.383,-85.341,72.422,-85.391,72.444,-85.498,72.511,-85.554,72.569,-85.616,72.605,-85.638,72.633,-85.65,72.722,-85.645,72.774,-85.619,72.819,-85.575,72.856,-85.455,72.925,-85.388,72.945,-85.262,72.954,-84.99,72.92,-84.257,72.797,-84.274,72.836,-85.094,73.003,-85.384,73.045,-85.455,73.105,-85.018,73.335,-84.616,73.39,-84.416,73.456,-84.089,73.459,-83.782,73.417,-83.777,73.428,-83.915,73.508,-83.904,73.528,-83.73,73.576,-83.41,73.632,-83.02,73.676,-82.943,73.699,-82.843,73.715,-82.66,73.73,-82.203,73.736,-81.946,73.73,-81.605,73.696,-81.406,73.635,-81.344,73.598,-81.238,73.48,-81.152,73.314,-81.025,73.245,-80.822,73.207,-80.681,73.166,-80.603,73.121,-80.583,73.065,-80.619,72.997,-80.592,72.928,-80.501,72.857,-80.431,72.816,-80.277,72.77,-80.275,72.746,-80.323,72.717,-80.424,72.679,-80.675,72.559,-80.999,72.426,-81.229,72.312,-81.241,72.278,-80.761,72.457,-80.611,72.451,-80.605,72.426,-80.702,72.338,-80.821,72.26,-80.941,72.21,-80.919,72.191,-80.691,72.103,-80.733,72.089,-80.843,72.096,-80.888,72.088,-80.921,72.072,-80.941,72.048,-80.943,72.014,-80.925,71.971,-80.927,71.938,-80.948,71.917,-80.925,71.908,-80.858,71.911,-80.802,71.929,-80.705,71.988,-80.386,72.149,-80.182,72.209,-80.116,72.214,-79.928,72.175,-79.884,72.177,-80.091,72.301,-80.109,72.332,-80.067,72.378,-80.042,72.394,-79.927,72.428,-79.831,72.446,-79.778,72.439,-79.693,72.376,-79.654,72.332,-79.584,72.315,-79.427,72.337,-79.323,72.391,-79.194,72.356,-79,72.272,-79.018,72.188,-79.018,72.104,-79.008,72.043,-78.776,71.93,-78.614,71.881,-78.585,71.881,-78.589,71.898,-78.623,71.935,-78.711,71.972,-78.791,72.03,-78.863,72.101,-78.82,72.265,-78.699,72.351,-78.582,72.329,-78.429,72.28,-78.307,72.275,-78.116,72.28,-77.726,72.18,-77.517,72.178,-77.536,72.219,-77.694,72.238,-77.926,72.294,-78.287,72.36,-78.453,72.435,-78.484,72.471,-78.479,72.509,-78.459,72.542,-78.422,72.572,-78.35,72.6,-78.001,72.688,-77.753,72.725,-77.567,72.737,-77.255,72.736,-76.894,72.721,-76.698,72.695,-76.473,72.633,-76.189,72.572,-76.087,72.561,-75.969,72.563,-75.833,72.577,-75.704,72.572,-75.294,72.481,-75.186,72.434,-75.12,72.378,-75.071,72.323,-75.04,72.27,-75.053,72.226,-75.394,72.04,-75.543,72.008,-75.641,71.937,-75.787,71.803,-75.911,71.731,-75.923,71.717,-75.897,71.714,-75.822,71.746,-75.693,71.839,-75.6,71.918,-75.428,71.984,-75.148,72.063,-74.903,72.1,-74.695,72.097,-74.52,72.086,-74.377,72.067,-74.293,72.051,-74.266,72.038,-74.209,71.979,-74.213,71.939,-74.248,71.894,-74.316,71.843,-74.621,71.786,-74.789,71.742,-74.893,71.726,-75.205,71.709,-75.191,71.692,-74.959,71.667,-74.701,71.676,-74.707,71.647,-74.829,71.571,-74.868,71.505,-74.834,71.451,-74.841,71.407,-74.931,71.314,-75.035,71.231,-74.996,71.218,-74.759,71.338,-74.695,71.469,-74.6,71.585,-74.488,71.648,-74.404,71.673,-74.139,71.682,-73.992,71.75,-73.867,71.771,-73.814,71.771,-73.707,71.746,-73.714,71.72,-73.869,71.599,-74.197,71.404,-74.063,71.426,-73.973,71.473,-73.851,71.519,-73.713,71.588,-73.622,71.526,-73.482,71.479,-73.398,71.373,-73.262,71.322,-73.181,71.283,-73.192,71.35,-73.31,71.484,-73.278,71.538,-73.187,71.565,-72.902,71.678,-72.703,71.64,-72.581,71.607,-72.519,71.616,-72.365,71.611,-72.117,71.593,-71.875,71.561,-71.641,71.516,-71.46,71.464,-71.333,71.403,-71.256,71.362,-71.229,71.339,-71.187,71.279,-71.219,71.239,-71.397,71.147,-71.495,71.105,-71.593,71.086,-71.856,71.105,-71.938,71.094,-72.024,71.065,-72.298,70.939,-72.449,70.884,-72.598,70.849,-72.633,70.831,-72.313,70.833,-72.224,70.87,-72.15,70.941,-72.009,71.013,-71.743,71.047,-71.371,70.975,-71.186,70.978,-71.045,71.05,-70.888,71.099,-70.826,71.109,-70.792,71.103,-70.673,71.052,-70.636,71.007,-70.639,70.902,-70.655,70.871,-70.762,70.792,-71.022,70.674,-71.192,70.63,-71.38,70.606,-71.586,70.566,-71.658,70.534,-71.729,70.488,-71.8,70.457,-71.89,70.432,-71.772,70.394,-71.727,70.395,-71.684,70.418,-71.565,70.506,-71.477,70.544,-71.427,70.552,-71.375,70.548,-71.325,70.531,-71.276,70.5,-71.28,70.425,-71.429,70.128,-71.405,70.129,-71.313,70.209,-71.045,70.519,-70.98,70.581,-70.851,70.644,-70.561,70.738,-70.337,70.788,-70.085,70.83,-69.95,70.845,-69.796,70.835,-69.696,70.786,-69.56,70.777,-69.395,70.789,-69.289,70.783,-69.169,70.764,-69.066,70.728,-68.891,70.687,-68.496,70.61,-68.447,70.594,-68.401,70.565,-68.358,70.523,-68.364,70.481,-68.417,70.44,-68.483,70.415,-68.561,70.406,-68.643,70.383,-68.794,70.324,-68.843,70.314,-69.079,70.289,-69.299,70.277,-69.436,70.253,-69.699,70.189,-70.061,70.071,-70.058,70.043,-69.913,70.029,-69.796,70.047,-69.635,70.129,-69.483,70.16,-69.246,70.185,-68.919,70.207,-68.778,70.204,-68.753,70.199,-68.735,70.18,-68.723,70.146,-68.777,70.101,-68.839,70.08,-69.008,69.979,-68.897,69.953,-68.744,69.941,-68.657,69.968,-68.578,70.03,-68.489,70.065,-68.391,70.072,-68.305,70.087,-68.231,70.112,-68.21,70.128,-68.319,70.161,-68.327,70.18,-68.283,70.228,-68.204,70.281,-68.121,70.315,-68.059,70.317,-67.855,70.282,-67.716,70.22,-67.364,70.034,-67.318,69.998,-67.196,69.861,-67.173,69.799,-67.193,69.757,-67.222,69.731,-67.259,69.721,-67.337,69.721,-67.806,69.777,-68.02,69.77,-68.114,69.754,-68.189,69.731,-68.248,69.701,-68.29,69.665,-68.372,69.644,-68.67,69.644,-68.837,69.624,-69.125,69.575,-69.228,69.547,-69.251,69.512,-69.075,69.518,-68.785,69.564,-68.513,69.577,-68.058,69.476,-67.908,69.46,-67.825,69.475,-67.725,69.479,-67.361,69.473,-67.237,69.46,-67.053,69.421,-66.771,69.337,-66.717,69.312,-66.685,69.286,-66.676,69.258,-66.679,69.191,-66.707,69.168,-66.803,69.153,-67.208,69.171,-67.332,69.185,-67.484,69.167,-67.607,69.173,-67.765,69.2,-67.938,69.248,-68.198,69.203,-68.406,69.232,-68.619,69.206,-69.041,69.098,-68.993,69.079,-68.416,69.172,-68.304,69.166,-68.121,69.133,-67.833,69.066,-67.752,69.039,-67.751,68.934,-67.795,68.863,-67.883,68.784,-68.016,68.795,-68.324,68.844,-68.45,68.851,-68.543,68.843,-68.667,68.811,-68.725,68.81,-69.219,68.873,-69.33,68.876,-69.343,68.869,-69.319,68.857,-68.871,68.76,-68.541,68.749,-68.333,68.733,-68.21,68.703,-68.152,68.681,-68.148,68.616,-68.038,68.551,-67.938,68.524,-67.875,68.523,-67.766,68.547,-67.656,68.551,-67.567,68.534,-67.456,68.498,-67.321,68.488,-67.202,68.466,-67.111,68.461,-66.854,68.472,-66.743,68.458,-66.714,68.446,-66.762,68.425,-66.997,68.374,-67.033,68.326,-66.9,68.264,-66.831,68.216,-66.834,68.18,-66.905,68.098,-66.923,68.066,-66.9,68.063,-66.729,68.129,-66.702,68.121,-66.685,68.029,-66.663,68.034,-66.605,68.11,-66.631,68.211,-66.531,68.25,-66.212,68.28,-66.266,68.123,-66.275,68.041,-66.414,67.904,-66.53,67.86,-66.526,67.851,-66.444,67.834,-66.392,67.832,-66.343,67.853,-66.225,67.959,-65.986,68.069,-65.942,68.071,-65.944,68.031,-65.975,67.957,-65.864,67.923,-65.759,67.957,-65.702,67.987,-65.569,67.982,-65.509,67.968,-65.491,67.936,-65.552,67.799,-65.541,67.766,-65.401,67.675,-65.387,67.68,-65.413,67.724,-65.442,67.832,-65.415,67.879,-65.3,67.94,-65.064,68.026,-64.977,68.043,-64.922,68.032,-64.835,67.99,-64.863,67.965,-64.956,67.939,-65.026,67.892,-65.071,67.824,-65.021,67.788,-64.83,67.784,-64.638,67.84,-64.528,67.813,-64.396,67.74,-64.156,67.623,-64.019,67.655,-63.85,67.566,-64.077,67.496,-64.008,67.347,-64.303,67.353,-64.469,67.342,-64.58,67.355,-64.7,67.351,-64.589,67.316,-64.376,67.301,-64.356,67.256,-64.189,67.257,-64.063,67.266,-63.836,67.264,-63.824,67.316,-63.676,67.345,-63.592,67.378,-63.521,67.358,-63.316,67.336,-63.04,67.235,-63.162,67.174,-63.195,67.117,-63.236,67.069,-63.258,67.025,-63.307,66.994,-63.702,66.822,-63.636,66.821,-63.469,66.862,-63.144,66.924,-62.962,66.949,-62.833,66.933,-62.768,66.932,-62.71,66.954,-62.603,66.929,-62.38,66.905,-62.124,67.047,-61.969,67.019,-61.824,66.932,-61.515,66.778,-61.353,66.689,-61.3,66.649,-61.307,66.609,-61.453,66.567,-61.528,66.558,-61.724,66.638,-61.904,66.678,-62.014,66.674,-62.123,66.643,-62.089,66.626,-61.653,66.503,-61.576,66.413,-61.571,66.373,-61.863,66.313,-61.956,66.309,-62.158,66.338,-62.277,66.392,-62.375,66.411,-62.51,66.417,-62.553,66.407,-62.406,66.316,-62.42,66.289,-62.496,66.271,-62.534,66.227,-62.242,66.148,-62.024,66.068,-61.992,66.035,-62.139,66.011,-62.244,66.006,-62.468,66.017,-62.59,66.034,-62.624,66.016]},{"b":[-91.684,76.174,-61.207,83.116],"l":[-68.721,81.261,-68.689,81.293,-66.914,81.485,-66.861,81.499,-66.801,81.527,-66.765,81.563,-66.626,81.616,-66.005,81.629,-65.701,81.646,-65.495,81.668,-65.399,81.715,-65.226,81.744,-64.574,81.734,-64.436,81.743,-64.128,81.794,-63.592,81.846,-62.496,82.007,-62.177,82.043,-61.969,82.11,-61.615,82.184,-61.274,82.28,-61.207,82.341,-61.302,82.4,-61.392,82.442,-61.477,82.467,-61.697,82.489,-62.475,82.52,-63.247,82.45,-63.251,82.467,-63.087,82.533,-63.085,82.565,-63.385,82.653,-63.593,82.694,-63.643,82.713,-63.621,82.729,-63.564,82.749,-63.473,82.771,-63.499,82.793,-63.641,82.813,-63.984,82.829,-64.134,82.823,-64.433,82.778,-64.504,82.778,-64.635,82.819,-64.777,82.876,-64.905,82.901,-64.984,82.902,-65.113,82.889,-65.162,82.87,-65.247,82.819,-65.299,82.8,-65.4,82.802,-65.55,82.827,-65.727,82.842,-66.12,82.807,-66.612,82.742,-66.866,82.719,-66.998,82.716,-67.397,82.668,-67.736,82.652,-68.173,82.646,-68.469,82.653,-68.358,82.677,-66.836,82.818,-66.6,82.861,-66.425,82.906,-66.423,82.927,-66.592,82.944,-67.406,82.954,-67.624,82.964,-67.925,82.956,-68.107,82.961,-68.409,83.005,-68.673,82.999,-69.489,83.017,-69.569,83.025,-69.782,83.093,-69.868,83.11,-69.97,83.116,-70.871,83.098,-71.085,83.083,-71.424,83.021,-71.402,83.001,-71.198,82.97,-70.933,82.911,-70.94,82.902,-71.132,82.923,-71.406,82.975,-71.983,83.101,-72.069,83.106,-72.812,83.081,-73.331,82.999,-73.404,82.977,-73.441,82.946,-73.442,82.905,-73.235,82.844,-72.776,82.756,-72.659,82.722,-73.272,82.772,-73.703,82.852,-73.917,82.904,-74.056,82.955,-74.198,82.989,-74.414,83.013,-75.745,83.047,-77.125,83.009,-77.041,82.968,-76.908,82.919,-76.41,82.816,-76.188,82.758,-76.087,82.724,-75.643,82.644,-75.566,82.609,-75.744,82.572,-76.009,82.535,-76.146,82.55,-76.244,82.604,-76.336,82.644,-76.421,82.671,-77.226,82.837,-77.48,82.883,-77.618,82.896,-77.969,82.906,-78.525,82.891,-79.181,82.933,-79.886,82.939,-80.155,82.911,-80.141,82.894,-79.974,82.859,-79.834,82.817,-79.642,82.785,-79.207,82.733,-78.792,82.694,-78.749,82.679,-79.035,82.675,-80.076,82.706,-80.657,82.769,-81.01,82.779,-81.128,82.762,-81.178,82.745,-81.147,82.716,-80.81,82.586,-80.862,82.572,-81.189,82.594,-81.58,82.643,-81.785,82.649,-82.117,82.629,-82.123,82.602,-81.959,82.563,-81.681,82.519,-81.718,82.506,-82.023,82.494,-82.269,82.465,-82.451,82.427,-82.448,82.395,-82.254,82.336,-81.998,82.278,-81.468,82.192,-80.13,82.028,-79.629,81.932,-79.425,81.854,-79.466,81.851,-79.686,81.886,-79.909,81.936,-80.153,81.978,-80.55,82.005,-81.584,82.121,-82.277,82.218,-82.537,82.247,-82.638,82.246,-82.709,82.229,-82.747,82.196,-82.657,82.158,-82.327,82.092,-82.356,82.066,-82.634,82.077,-82.774,82.095,-83.01,82.142,-83.176,82.187,-83.591,82.326,-83.824,82.351,-84.368,82.374,-84.553,82.398,-84.745,82.437,-84.897,82.449,-85.276,82.405,-85.481,82.366,-85.794,82.292,-85.92,82.283,-86.188,82.248,-86.616,82.219,-86.581,82.187,-85.311,82.044,-85.169,82.023,-85.052,81.995,-85.045,81.983,-85.403,81.982,-85.538,81.955,-85.646,81.953,-85.875,81.976,-86.158,82.026,-86.378,82.045,-86.627,82.051,-86.834,82.033,-86.999,81.992,-87.018,81.959,-87.218,82,-87.404,82.054,-87.639,82.085,-88.063,82.096,-88.567,82.061,-88.875,82.018,-89.156,81.955,-89.381,81.917,-89.633,81.895,-90.163,81.894,-90.49,81.877,-90.942,81.827,-91.219,81.788,-91.424,81.744,-91.648,81.684,-91.684,81.636,-91.403,81.578,-91.292,81.571,-91.103,81.592,-90.834,81.64,-90.626,81.656,-90.48,81.639,-90.331,81.632,-89.822,81.635,-89.845,81.612,-90.554,81.464,-90.609,81.43,-90.416,81.405,-90.304,81.401,-88.978,81.542,-88.479,81.565,-88.101,81.559,-87.597,81.526,-87.617,81.509,-88.127,81.519,-88.622,81.501,-88.892,81.474,-89.427,81.387,-89.674,81.329,-89.636,81.302,-89.209,81.25,-89.263,81.239,-89.563,81.226,-89.947,81.173,-89.981,81.125,-89.792,81.065,-89.623,81.032,-89.398,81.025,-88.887,81.058,-87.275,81.081,-86.623,81.123,-85.875,81.241,-85.402,81.285,-85.206,81.295,-84.941,81.286,-85.083,81.247,-85.81,81.124,-86.477,81.036,-86.929,81,-87.389,80.988,-88.413,81,-89.167,80.941,-89.263,80.914,-89.212,80.882,-89.145,80.854,-89.062,80.83,-88.921,80.806,-88.625,80.77,-88.232,80.704,-88.004,80.675,-87.712,80.656,-87.33,80.67,-87.08,80.726,-86.233,80.95,-85.967,81.012,-85.781,81.035,-84.635,81.098,-83.289,81.148,-83.349,81.103,-84.68,81.042,-85.246,80.988,-85.639,80.925,-86.252,80.79,-86.44,80.728,-86.603,80.664,-86.615,80.63,-86.532,80.605,-86.25,80.566,-86.097,80.562,-85.726,80.581,-85.307,80.526,-85.146,80.521,-84.418,80.527,-84.22,80.538,-84.076,80.556,-83.885,80.602,-83.647,80.674,-83.401,80.714,-82.78,80.736,-82.498,80.763,-82.222,80.772,-82.337,80.729,-82.768,80.631,-82.884,80.578,-82.613,80.559,-82.368,80.561,-81.553,80.623,-81.301,80.627,-81.007,80.655,-80.134,80.764,-79.761,80.842,-79.607,80.882,-79.545,80.909,-79.477,80.961,-79.402,81.037,-79.309,81.089,-79.198,81.118,-79.072,81.128,-78.932,81.119,-78.734,81.151,-78.352,81.259,-77.972,81.331,-76.885,81.43,-77.031,81.386,-77.536,81.321,-78.287,81.168,-78.464,81.114,-78.629,81.043,-78.682,81.001,-78.716,80.952,-78.551,80.921,-78.004,80.905,-77.389,80.905,-77.119,80.896,-76.85,80.878,-76.863,80.865,-77.169,80.843,-77.507,80.835,-78.386,80.784,-79.629,80.648,-79.674,80.625,-80.051,80.529,-80.98,80.445,-82.536,80.376,-82.785,80.354,-82.987,80.323,-82.961,80.278,-82.681,80.175,-82.332,80.066,-81.86,79.957,-81.644,79.89,-81.359,79.788,-81.179,79.733,-81.01,79.693,-80.714,79.675,-80.287,79.679,-80.124,79.669,-80.271,79.635,-80.476,79.606,-80.668,79.601,-81.038,79.614,-81.463,79.654,-81.688,79.686,-81.856,79.723,-82.049,79.783,-82.377,79.908,-82.677,79.993,-83.004,80.055,-83.344,80.147,-83.724,80.229,-84.057,80.262,-84.675,80.279,-85.16,80.272,-86.307,80.319,-86.499,80.258,-86.615,80.124,-86.494,80.018,-86.421,79.845,-86.147,79.743,-86.031,79.722,-85.457,79.69,-85.269,79.664,-85.09,79.612,-84.836,79.495,-84.522,79.377,-84.381,79.301,-84.197,79.225,-83.978,79.163,-83.662,79.09,-83.576,79.054,-83.825,79.059,-84.053,79.099,-84.257,79.122,-84.384,79.119,-84.53,79.101,-84.568,79.071,-84.496,79.029,-84.412,78.997,-84.316,78.975,-84.146,78.96,-83.779,78.945,-83.059,78.94,-82.644,78.908,-82.439,78.904,-82.237,78.924,-82.028,78.962,-81.889,78.975,-81.75,78.976,-81.781,78.95,-81.981,78.898,-82.151,78.864,-82.291,78.847,-82.442,78.84,-82.99,78.844,-83.147,78.808,-83.271,78.77,-83.389,78.779,-83.547,78.804,-83.908,78.839,-84.787,78.885,-85.004,78.912,-85.23,78.902,-85.691,78.844,-86.242,78.824,-86.808,78.774,-86.953,78.664,-87.164,78.558,-87.361,78.479,-87.491,78.417,-87.491,78.284,-87.552,78.177,-87.339,78.133,-86.913,78.127,-86.694,78.151,-86.427,78.197,-86.071,78.285,-85.92,78.343,-86.063,78.187,-86.218,78.081,-85.586,78.11,-85.419,78.142,-85.27,78.2,-85.024,78.312,-84.783,78.528,-84.91,78.24,-84.55,78.251,-84.388,78.206,-84.223,78.176,-84.524,78.197,-84.615,78.196,-85.031,78.062,-85.265,78.011,-85.548,77.928,-85.292,77.764,-85.289,77.559,-85.088,77.515,-84.861,77.5,-84.486,77.562,-84.168,77.523,-83.928,77.518,-83.779,77.533,-83.428,77.621,-83.304,77.674,-82.704,77.962,-82.595,77.992,-82.626,77.936,-82.665,77.889,-82.71,77.85,-82.903,77.733,-83.25,77.585,-83.477,77.514,-83.55,77.483,-83.608,77.442,-83.721,77.414,-83.974,77.391,-84.487,77.368,-84.739,77.361,-84.951,77.375,-85.588,77.461,-85.731,77.509,-85.907,77.614,-86.173,77.746,-86.385,77.809,-86.755,77.864,-87.018,77.892,-87.236,77.892,-87.497,77.872,-87.757,77.836,-88.017,77.785,-88.095,77.719,-87.938,77.6,-87.78,77.493,-87.681,77.436,-87.589,77.395,-87.43,77.348,-87.265,77.343,-87.182,77.332,-87.101,77.308,-86.874,77.2,-86.812,77.185,-86.852,77.174,-87.064,77.166,-87.362,77.136,-87.61,77.127,-87.828,77.136,-88.148,77.124,-88.398,77.104,-88.556,77.072,-88.771,76.993,-89.5,76.827,-89.544,76.66,-89.57,76.492,-89.37,76.474,-88.804,76.457,-88.546,76.421,-88.563,76.547,-88.614,76.651,-88.496,76.773,-88.482,76.58,-88.396,76.405,-88.104,76.413,-87.498,76.386,-87.49,76.586,-87.354,76.448,-86.978,76.413,-86.68,76.377,-86.562,76.517,-86.454,76.585,-86.419,76.58,-86.367,76.549,-86.296,76.492,-86.116,76.435,-85.681,76.349,-85.344,76.313,-85.141,76.305,-84.275,76.357,-84.224,76.675,-83.986,76.495,-83.886,76.453,-83.389,76.439,-82.233,76.466,-82.208,76.514,-82.262,76.575,-82.357,76.636,-82.493,76.698,-82.53,76.723,-82.393,76.69,-82.311,76.655,-82.218,76.64,-82.114,76.643,-82.034,76.629,-81.823,76.521,-81.717,76.495,-81.592,76.484,-81.474,76.488,-81.365,76.504,-81.171,76.513,-81.074,76.498,-80.975,76.47,-80.832,76.409,-80.835,76.369,-80.901,76.322,-80.955,76.27,-80.997,76.215,-80.963,76.184,-80.8,76.174,-80.69,76.176,-80.187,76.24,-79.954,76.251,-79.511,76.31,-79.286,76.355,-79.131,76.404,-78.934,76.451,-78.284,76.571,-78.119,76.644,-77.983,76.755,-77.999,76.852,-78.165,76.935,-78.289,76.978,-78.37,76.981,-78.456,76.967,-78.659,76.908,-78.792,76.884,-78.979,76.893,-79.221,76.936,-79.319,76.98,-79.274,77.026,-79.281,77.085,-79.341,77.158,-79.497,77.196,-79.924,77.194,-80.219,77.147,-80.274,77.151,-80.673,77.244,-80.798,77.259,-81.117,77.27,-81.277,77.257,-81.534,77.214,-81.756,77.204,-81.84,77.214,-81.968,77.248,-82.066,77.284,-82.057,77.297,-81.767,77.296,-81.523,77.311,-81.301,77.344,-81.278,77.365,-81.378,77.385,-81.504,77.43,-81.654,77.499,-81.659,77.525,-81.519,77.51,-81.377,77.482,-80.875,77.359,-80.573,77.315,-80.282,77.301,-79.906,77.3,-79.138,77.331,-78.87,77.333,-78.708,77.342,-78.493,77.369,-78.284,77.413,-78.168,77.458,-78.076,77.519,-78.047,77.615,-78.081,77.747,-78.084,77.846,-78.056,77.912,-78.013,77.946,-77.456,77.947,-76.974,77.927,-76.708,77.938,-76.356,77.991,-76.078,77.987,-75.97,77.993,-75.866,78.01,-75.551,78.221,-75.193,78.328,-75.237,78.356,-75.488,78.404,-76.137,78.492,-76.416,78.512,-76.373,78.521,-75.966,78.53,-75.397,78.523,-74.879,78.545,-74.547,78.62,-74.535,78.659,-74.433,78.724,-74.486,78.75,-74.618,78.758,-75.099,78.858,-75.4,78.881,-75.795,78.89,-75.953,78.959,-76.077,78.985,-76.256,79.007,-76.524,79.024,-76.825,79.018,-77.51,78.978,-77.698,78.955,-77.883,78.942,-78.037,78.964,-78.222,79.015,-78.422,79.048,-78.559,79.055,-78.582,79.075,-78.258,79.082,-77.974,79.076,-77.729,79.057,-77.398,79.057,-76.771,79.087,-76.531,79.087,-76.38,79.104,-76.158,79.1,-75.912,79.118,-75.639,79.088,-75.515,79.061,-75.233,79.036,-74.641,79.036,-74.532,79.053,-74.481,79.229,-74.727,79.235,-75.094,79.204,-75.354,79.228,-75.603,79.24,-75.948,79.311,-76.116,79.326,-76.296,79.414,-76.671,79.478,-76.855,79.488,-76.899,79.512,-76.376,79.494,-76.067,79.473,-75.774,79.431,-75.503,79.414,-75.259,79.421,-74.798,79.459,-74.406,79.454,-74.189,79.465,-74.015,79.491,-73.866,79.501,-73.466,79.495,-73.362,79.504,-73.294,79.522,-73.24,79.552,-73.201,79.597,-73.229,79.644,-73.406,79.732,-73.472,79.756,-73.642,79.771,-74.051,79.778,-74.541,79.816,-74.66,79.835,-74.394,79.874,-74.144,79.88,-73.805,79.846,-73.448,79.827,-72.437,79.694,-72.216,79.687,-71.965,79.701,-71.388,79.762,-71.299,79.783,-71.11,79.848,-71.106,79.876,-71.278,79.906,-71.356,79.911,-70.758,79.998,-70.559,80.071,-70.568,80.094,-70.758,80.119,-70.877,80.122,-71.616,80.071,-71.949,80.086,-72.063,80.106,-72.056,80.123,-71.928,80.139,-71.796,80.143,-71.661,80.136,-71.47,80.146,-71.1,80.187,-70.265,80.234,-70.213,80.278,-70.668,80.506,-70.713,80.54,-70.639,80.528,-70.403,80.459,-70.144,80.398,-69.949,80.374,-69.734,80.367,-69.551,80.383,-69.4,80.423,-68.959,80.587,-68.63,80.679,-67.774,80.859,-66.727,81.041,-66.313,81.146,-65.484,81.285,-64.833,81.439,-64.78,81.493,-65.24,81.51,-65.736,81.494,-68.318,81.261,-68.543,81.248,-68.721,81.261]},{"b":[-136.444,68.684,-120.682,70.574],"l":[-120.682,69.567,-120.815,69.617,-120.962,69.66,-121.336,69.742,-121.531,69.776,-121.742,69.798,-122.07,69.816,-122.387,69.808,-122.705,69.817,-122.785,69.808,-122.957,69.819,-123.026,69.81,-123.077,69.782,-123.11,69.738,-123.144,69.632,-123.214,69.542,-123.249,69.52,-123.361,69.497,-123.46,69.42,-123.528,69.389,-123.609,69.377,-124.05,69.373,-124.112,69.359,-124.338,69.365,-124.426,69.379,-124.472,69.4,-124.481,69.425,-124.454,69.455,-124.398,69.494,-124.138,69.653,-124.125,69.69,-124.349,69.735,-124.407,69.767,-124.472,69.919,-124.467,69.983,-124.442,70.062,-124.444,70.111,-124.503,70.141,-124.555,70.151,-124.64,70.141,-124.706,70.117,-124.745,70.08,-124.952,70.042,-124.99,70.027,-124.963,70.013,-124.92,70.006,-124.863,70.006,-124.768,69.99,-124.794,69.969,-124.889,69.936,-124.968,69.894,-125.031,69.844,-125.08,69.818,-125.114,69.815,-125.201,69.829,-125.228,69.757,-125.219,69.732,-125.346,69.662,-125.357,69.626,-125.262,69.566,-125.167,69.48,-125.172,69.428,-125.387,69.349,-125.525,69.352,-125.728,69.38,-125.907,69.419,-126.064,69.467,-126.25,69.545,-126.612,69.73,-126.685,69.777,-126.759,69.853,-126.833,69.959,-126.927,70.062,-127.138,70.239,-127.226,70.296,-127.377,70.369,-127.753,70.517,-127.862,70.549,-127.991,70.574,-128.04,70.566,-128.127,70.524,-128.168,70.48,-128.17,70.418,-128.121,70.397,-127.989,70.363,-128.044,70.329,-128.034,70.315,-127.974,70.293,-127.684,70.26,-127.765,70.222,-128.096,70.161,-128.279,70.108,-128.359,69.988,-128.387,69.96,-128.706,69.81,-128.853,69.751,-128.971,69.712,-129.054,69.701,-129.102,69.717,-129.136,69.75,-129.158,69.8,-129.138,69.833,-128.939,69.875,-128.917,69.895,-128.884,69.963,-128.899,69.966,-128.984,69.933,-129.033,69.905,-129.109,69.882,-129.265,69.855,-129.572,69.827,-130.118,69.72,-130.354,69.656,-130.516,69.57,-130.661,69.481,-130.875,69.32,-130.914,69.285,-130.971,69.209,-130.982,69.253,-130.986,69.363,-131.013,69.429,-131.063,69.451,-131.113,69.459,-131.162,69.455,-131.209,69.432,-131.294,69.364,-131.325,69.361,-131.303,69.415,-131.343,69.435,-131.563,69.461,-131.632,69.459,-131.788,69.432,-131.82,69.402,-131.781,69.389,-131.787,69.371,-131.833,69.336,-131.92,69.291,-132.134,69.234,-132.214,69.202,-132.358,69.167,-132.545,69.141,-132.719,69.079,-132.755,69.042,-132.77,69.012,-132.765,68.972,-132.739,68.922,-132.704,68.896,-132.542,68.89,-132.533,68.876,-132.578,68.848,-132.706,68.815,-133.304,68.847,-133.337,68.835,-133.32,68.82,-133.192,68.777,-133.138,68.747,-133.197,68.74,-133.348,68.77,-133.373,68.788,-133.418,68.844,-133.379,68.887,-133.228,68.967,-133.089,69.029,-132.968,69.101,-132.817,69.206,-132.687,69.26,-132.481,69.273,-132.331,69.308,-132.197,69.365,-132.129,69.402,-131.989,69.518,-131.938,69.535,-131.863,69.549,-131.473,69.579,-131.306,69.597,-131.208,69.616,-130.96,69.632,-130.832,69.651,-130.709,69.686,-130.459,69.78,-129.648,69.998,-129.538,70.074,-129.538,70.105,-129.623,70.168,-129.676,70.193,-129.73,70.192,-129.898,70.106,-129.945,70.091,-130.043,70.095,-130.175,70.086,-130.275,70.098,-130.396,70.129,-130.498,70.143,-130.665,70.127,-130.926,70.052,-130.991,70.018,-131.032,69.979,-131.136,69.907,-131.216,69.901,-131.319,69.924,-131.441,69.918,-131.582,69.882,-131.934,69.753,-132.163,69.705,-132.232,69.708,-132.334,69.752,-132.488,69.738,-132.538,69.727,-132.571,69.707,-132.568,69.698,-132.479,69.693,-132.413,69.674,-132.404,69.659,-132.452,69.647,-132.527,69.643,-132.84,69.651,-132.915,69.63,-133.028,69.508,-133.084,69.471,-133.163,69.434,-133.294,69.412,-133.476,69.405,-133.694,69.368,-133.948,69.301,-134.174,69.253,-134.165,69.281,-134.018,69.388,-133.947,69.429,-133.88,69.508,-133.9,69.528,-134.077,69.558,-134.134,69.587,-134.19,69.639,-134.242,69.669,-134.409,69.682,-134.451,69.665,-134.474,69.633,-134.495,69.572,-134.491,69.545,-134.457,69.478,-134.494,69.468,-134.853,69.486,-135.141,69.468,-135.199,69.45,-135.23,69.425,-135.255,69.324,-135.293,69.308,-135.5,69.337,-135.615,69.291,-135.691,69.311,-135.91,69.111,-135.85,69.081,-135.743,69.049,-135.651,69.031,-135.576,69.027,-135.59,69.008,-135.695,69.001,-135.873,69.001,-135.925,68.993,-135.939,68.974,-135.895,68.927,-135.876,68.917,-135.638,68.892,-135.435,68.842,-135.407,68.829,-135.231,68.694,-135.259,68.684,-135.362,68.696,-135.867,68.833,-136.122,68.882,-136.444,68.895]},{"b":[-168.088,-53.883,-34.805,71.987],"l":[-136.444,68.895,-136.499,68.897,-136.717,68.889,-137.07,68.951,-137.26,68.964,-137.869,69.093,-138.128,69.152,-138.291,69.219,-138.69,69.317,-139.182,69.516,-139.977,69.622,-140.405,69.602,-140.86,69.635,-141.002,69.651,-141.081,69.659,-141.29,69.665,-141.339,69.647,-141.408,69.653,-141.526,69.715,-141.699,69.77,-142.297,69.87,-142.422,69.94,-142.708,70.034,-143.218,70.116,-143.276,70.095,-143.357,70.09,-143.566,70.101,-143.746,70.102,-144.064,70.054,-144.417,70.039,-144.619,69.982,-145.197,70.009,-145.237,70.034,-145.44,70.051,-145.823,70.16,-146.058,70.156,-146.281,70.186,-146.745,70.192,-147.063,70.17,-147.705,70.217,-147.791,70.24,-147.87,70.303,-148.039,70.315,-148.143,70.355,-148.249,70.357,-148.371,70.315,-148.479,70.318,-148.688,70.416,-148.845,70.425,-149.269,70.501,-149.411,70.491,-149.544,70.513,-149.87,70.51,-150.152,70.444,-150.274,70.434,-150.403,70.444,-150.544,70.49,-150.663,70.51,-150.979,70.465,-151.128,70.452,-151.225,70.419,-151.945,70.452,-151.82,70.511,-151.8,70.538,-151.769,70.56,-152.173,70.557,-152.253,70.568,-152.27,70.615,-152.399,70.62,-152.471,70.654,-152.437,70.733,-152.233,70.81,-152.3,70.847,-152.491,70.881,-152.671,70.891,-152.785,70.876,-153.233,70.933,-153.498,70.891,-153.701,70.894,-153.918,70.877,-154.195,70.801,-154.392,70.838,-154.599,70.848,-154.785,70.894,-154.726,70.928,-154.674,70.987,-154.818,71.048,-154.944,71.083,-155.167,71.099,-155.23,71.082,-155.313,71.015,-155.579,70.894,-155.708,70.857,-155.872,70.835,-155.974,70.842,-156.042,70.902,-156.147,70.928,-155.804,70.995,-155.635,71.062,-155.579,71.121,-155.646,71.183,-155.811,71.188,-156.47,71.292,-156.567,71.342,-156.497,71.379,-156.395,71.397,-156.47,71.408,-156.783,71.319,-156.973,71.23,-157.195,71.093,-157.325,71.04,-157.606,70.941,-157.909,70.86,-157.998,70.845,-158.484,70.841,-158.511,70.82,-158.621,70.799,-158.996,70.802,-159.075,70.772,-159.251,70.748,-159.304,70.753,-159.34,70.781,-159.262,70.814,-159.183,70.832,-159.192,70.86,-159.232,70.877,-159.315,70.879,-159.681,70.787,-160.082,70.635,-159.962,70.634,-159.746,70.53,-159.387,70.525,-159.683,70.477,-159.815,70.497,-159.843,70.453,-159.858,70.389,-159.855,70.324,-159.866,70.279,-159.908,70.331,-160.095,70.333,-160.006,70.448,-160.106,70.473,-159.963,70.568,-160.046,70.586,-160.117,70.591,-160.634,70.446,-160.648,70.421,-160.996,70.305,-161.639,70.235,-161.768,70.197,-161.997,70.165,-162.074,70.162,-162.042,70.177,-161.912,70.205,-161.818,70.248,-161.761,70.258,-161.78,70.277,-161.813,70.29,-161.881,70.332,-161.978,70.288,-162.071,70.227,-162.35,70.094,-162.952,69.758,-163.094,69.611,-163.131,69.454,-163.161,69.388,-163.187,69.38,-163.205,69.393,-163.251,69.345,-163.536,69.17,-163.868,69.037,-164.15,68.961,-164.302,68.936,-164.89,68.902,-165.044,68.882,-165.509,68.868,-166.209,68.885,-166.182,68.797,-166.283,68.573,-166.381,68.425,-166.447,68.39,-166.57,68.361,-166.648,68.374,-166.546,68.424,-166.644,68.408,-166.786,68.36,-166.574,68.32,-166.409,68.308,-166.236,68.278,-165.96,68.156,-165.386,68.046,-164.125,67.607,-163.943,67.478,-163.8,67.271,-163.721,67.196,-163.532,67.103,-163.002,67.027,-162.761,67.036,-162.583,67.019,-162.409,67.104,-162.412,67.06,-162.392,67.02,-161.965,67.05,-161.72,67.021,-161.622,66.979,-161.731,66.923,-161.879,66.804,-161.857,66.7,-161.681,66.646,-161.572,66.592,-161.398,66.552,-161.051,66.653,-160.864,66.671,-160.644,66.605,-160.361,66.613,-160.263,66.572,-160.227,66.509,-160.232,66.42,-160.651,66.373,-160.784,66.384,-161.048,66.474,-161.156,66.495,-161.336,66.496,-161.591,66.46,-161.91,66.56,-162.051,66.667,-162.018,66.784,-162.131,66.801,-162.254,66.919,-162.362,66.947,-162.478,66.931,-162.607,66.894,-162.544,66.805,-162.467,66.736,-162.318,66.734,-162.191,66.693,-161.888,66.493,-161.917,66.412,-161.828,66.371,-161.544,66.407,-161.12,66.334,-161.07,66.295,-161.034,66.189,-161.109,66.24,-161.201,66.219,-161.345,66.247,-161.455,66.281,-161.557,66.251,-161.816,66.054,-161.934,66.043,-162.214,66.071,-162.587,66.051,-162.722,66.06,-162.886,66.099,-163.171,66.075,-163.695,66.084,-164.034,66.216,-163.965,66.257,-163.894,66.287,-163.903,66.378,-163.794,66.493,-163.775,66.531,-163.838,66.562,-163.894,66.576,-163.816,66.583,-163.638,66.575,-163.728,66.616,-164.058,66.611,-164.46,66.588,-164.674,66.555,-165.064,66.438,-165.198,66.44,-165.449,66.41,-165.776,66.319,-165.812,66.288,-165.84,66.245,-165.56,66.167,-165.59,66.145,-165.63,66.131,-165.724,66.113,-166.009,66.121,-166.057,66.127,-166.215,66.17,-166.399,66.144,-166.54,66.101,-166.748,66.052,-166.894,65.959,-166.997,65.905,-167.074,65.877,-167.405,65.859,-167.58,65.758,-167.914,65.681,-167.927,65.714,-167.931,65.748,-168.01,65.719,-168.088,65.658,-168.035,65.596,-167.987,65.568,-167.404,65.422,-166.665,65.338,-166.609,65.353,-166.197,65.306,-166.157,65.286,-166.121,65.261,-166.28,65.274,-166.452,65.247,-166.531,65.155,-166.763,65.135,-166.857,65.147,-166.906,65.164,-166.928,65.157,-166.827,65.096,-166.551,64.953,-166.415,64.927,-166.409,64.827,-166.478,64.798,-166.481,64.728,-166.325,64.626,-166.143,64.583,-165.446,64.513,-165.138,64.465,-164.979,64.454,-164.9,64.461,-164.857,64.48,-164.83,64.511,-164.765,64.53,-164.727,64.523,-164.692,64.507,-164.304,64.584,-163.713,64.588,-163.486,64.55,-163.267,64.475,-163.144,64.424,-163.104,64.479,-163.052,64.52,-163.174,64.533,-163.248,64.563,-163.303,64.606,-163.204,64.652,-162.876,64.516,-162.807,64.374,-162.711,64.378,-162.636,64.451,-162.335,64.613,-162.172,64.678,-161.868,64.743,-161.759,64.816,-161.634,64.792,-161.466,64.795,-161.187,64.924,-161.13,64.925,-161.063,64.904,-160.967,64.84,-160.887,64.796,-160.856,64.756,-160.836,64.682,-160.894,64.613,-160.932,64.579,-161.049,64.534,-161.193,64.516,-161.415,64.526,-161.491,64.434,-161.386,64.44,-161.22,64.397,-160.988,64.251,-160.904,64.031,-160.84,63.935,-160.779,63.819,-160.827,63.729,-160.927,63.661,-161.1,63.558,-161.266,63.497,-161.505,63.468,-161.974,63.453,-162.056,63.471,-162.112,63.534,-162.193,63.541,-162.283,63.529,-162.36,63.453,-162.621,63.266,-162.808,63.207,-162.948,63.115,-163.062,63.08,-163.288,63.046,-163.359,63.046,-163.423,63.085,-163.504,63.106,-163.649,63.057,-163.738,63.016,-163.749,63.03,-163.726,63.048,-163.664,63.07,-163.634,63.09,-163.616,63.125,-163.736,63.193,-163.943,63.247,-164.108,63.262,-164.409,63.215,-164.463,63.185,-164.525,63.128,-164.375,63.054,-164.384,63.03,-164.428,63.04,-164.677,63.02,-164.764,62.971,-164.8,62.918,-164.845,62.801,-164.819,62.677,-164.793,62.623,-164.689,62.677,-164.589,62.709,-164.596,62.687,-164.688,62.608,-164.844,62.581,-164.796,62.512,-164.758,62.497,-164.779,62.481,-164.892,62.518,-165,62.534,-165.116,62.513,-165.195,62.474,-165.448,62.304,-165.707,62.1,-165.744,62.012,-165.725,61.959,-165.706,61.927,-165.613,61.869,-165.834,61.837,-165.991,61.834,-166.079,61.803,-166.02,61.748,-165.809,61.696,-165.835,61.679,-166.1,61.645,-166.131,61.657,-166.168,61.651,-166.164,61.589,-166.153,61.546,-166.094,61.507,-165.961,61.551,-165.845,61.536,-165.797,61.491,-165.906,61.404,-165.864,61.336,-165.691,61.3,-165.628,61.165,-165.566,61.102,-165.48,61.095,-165.381,61.106,-165.379,61.169,-165.392,61.212,-165.334,61.266,-165.274,61.275,-165.244,61.269,-165.311,61.228,-165.345,61.198,-165.28,61.17,-165.204,61.153,-165.15,61.187,-165.128,61.192,-165.138,61.13,-165.077,61.094,-164.941,61.115,-164.869,61.112,-164.876,61.087,-165,61.044,-165.175,60.966,-165.115,60.933,-165.066,60.921,-164.754,60.931,-164.442,60.87,-163.995,60.865,-163.749,60.97,-163.659,60.938,-163.587,60.903,-163.655,60.877,-163.837,60.88,-163.907,60.854,-163.623,60.822,-163.512,60.798,-163.421,60.757,-163.529,60.665,-163.73,60.59,-163.821,60.668,-163.895,60.745,-163.936,60.758,-164,60.766,-164.132,60.692,-164.31,60.607,-164.372,60.592,-164.321,60.647,-164.266,60.725,-164.319,60.771,-164.37,60.796,-164.513,60.819,-164.682,60.872,-164.805,60.892,-164.9,60.873,-165.016,60.74,-165.354,60.541,-165.225,60.524,-165.113,60.526,-165.027,60.501,-165.049,60.464,-165.061,60.413,-164.92,60.348,-164.8,60.307,-164.662,60.304,-164.471,60.149,-164.132,59.994,-164.141,59.949,-164.143,59.897,-163.907,59.807,-163.68,59.802,-163.219,59.846,-162.878,59.923,-162.733,59.994,-162.571,59.99,-162.536,60.038,-162.5,60.127,-162.527,60.199,-162.548,60.231,-162.685,60.269,-162.6,60.297,-162.469,60.395,-162.265,60.595,-162.2,60.634,-162.138,60.686,-162.068,60.695,-161.962,60.695,-161.947,60.685,-162.139,60.614,-162.288,60.457,-162.421,60.284,-162.242,60.178,-162.138,59.98,-161.909,59.714,-161.829,59.589,-161.832,59.515,-161.872,59.428,-161.92,59.365,-162.023,59.284,-161.981,59.146,-161.891,59.076,-161.794,59.109,-161.644,59.11,-161.789,59.016,-161.79,58.95,-161.781,58.897,-161.724,58.794,-161.856,58.717,-162.009,58.685,-162.145,58.644,-161.755,58.612,-161.361,58.67,-161.328,58.744,-161.288,58.761,-161.247,58.799,-161.216,58.801,-160.924,58.872,-160.817,58.872,-160.657,58.955,-160.52,59.007,-160.363,59.051,-160.261,58.972,-160.153,58.906,-159.92,58.82,-159.832,58.836,-159.741,58.894,-159.67,58.911,-159.454,58.793,-159.358,58.721,-159.083,58.47,-158.951,58.405,-158.789,58.441,-158.772,58.52,-158.861,58.719,-158.838,58.794,-158.776,58.903,-158.809,58.974,-158.761,58.95,-158.678,58.929,-158.584,58.988,-158.514,59.073,-158.423,59.09,-158.221,59.038,-158.081,58.977,-158.189,58.98,-158.315,59.009,-158.426,58.999,-158.476,58.938,-158.503,58.85,-158.439,58.783,-158.39,58.746,-158.303,58.642,-158.191,58.614,-158.022,58.64,-157.666,58.748,-157.142,58.878,-156.963,58.989,-156.809,59.134,-156.923,58.964,-157.04,58.773,-157.009,58.744,-156.975,58.736,-157.229,58.641,-157.461,58.503,-157.524,58.421,-157.524,58.351,-157.488,58.254,-157.394,58.235,-157.339,58.235,-157.194,58.194,-157.443,58.172,-157.555,58.14,-157.611,58.051,-157.621,57.895,-157.684,57.744,-157.697,57.679,-157.681,57.638,-157.608,57.601,-157.572,57.541,-157.533,57.526,-157.474,57.518,-157.462,57.506,-157.535,57.483,-157.646,57.498,-157.674,57.514,-157.697,57.539,-157.737,57.548,-157.846,57.528,-157.894,57.511,-158.046,57.409,-158.134,57.366,-158.225,57.343,-158.321,57.298,-158.474,57.199,-158.586,57.114,-158.661,57.039,-158.677,56.997,-158.685,56.944,-158.681,56.888,-158.666,56.828,-158.675,56.795,-158.709,56.789,-158.782,56.796,-158.895,56.816,-158.918,56.847,-158.918,56.882,-158.99,56.86,-159.159,56.77,-159.283,56.689,-159.785,56.562,-160.046,56.437,-160.149,56.396,-160.302,56.314,-160.377,56.241,-160.461,56.138,-160.515,56.059,-160.539,56.006,-160.527,55.965,-160.48,55.935,-160.308,55.864,-160.27,55.832,-160.292,55.805,-160.347,55.8,-160.437,55.817,-160.498,55.838,-160.53,55.863,-160.6,55.874,-160.706,55.87,-160.758,55.855,-160.746,55.771,-160.763,55.757,-160.803,55.754,-160.851,55.772,-161.005,55.887,-161.008,55.912,-160.902,55.941,-160.878,55.971,-160.899,55.994,-160.969,55.97,-161.145,55.951,-161.193,55.954,-161.223,55.977,-161.179,56.014,-161.216,56.021,-161.697,55.907,-161.937,55.824,-162.157,55.719,-162.349,55.595,-162.513,55.45,-162.659,55.351,-162.786,55.297,-162.857,55.253,-162.872,55.219,-162.907,55.196,-162.962,55.184,-163.008,55.187,-163.045,55.205,-163.115,55.194,-163.279,55.122,-163.304,55.096,-163.306,55.059,-163.286,55.01,-163.296,54.949,-163.338,54.876,-163.335,54.839,-163.289,54.838,-163.221,54.863,-163.131,54.917,-163.1,54.974,-163.128,55.035,-163.12,55.065,-162.996,55.046,-162.865,54.955,-162.82,54.95,-162.674,54.997,-162.619,55.038,-162.614,55.071,-162.644,55.218,-162.63,55.247,-162.542,55.243,-162.495,55.208,-162.427,55.145,-162.413,55.107,-162.452,55.093,-162.457,55.078,-162.428,55.061,-162.386,55.052,-162.333,55.05,-162.275,55.073,-162.211,55.121,-162.167,55.144,-162.074,55.139,-161.98,55.199,-161.742,55.391,-161.72,55.421,-161.684,55.53,-161.654,55.563,-161.599,55.593,-161.517,55.618,-161.459,55.629,-161.357,55.612,-161.255,55.579,-161.215,55.56,-161.202,55.544,-161.313,55.559,-161.373,55.556,-161.413,55.536,-161.444,55.513,-161.477,55.465,-161.481,55.398,-161.464,55.383,-161.382,55.371,-161.178,55.389,-161.1,55.406,-161.024,55.44,-160.952,55.493,-160.897,55.514,-160.771,55.484,-160.727,55.5,-160.683,55.54,-160.625,55.552,-160.554,55.535,-160.499,55.537,-160.463,55.558,-160.407,55.614,-160.373,55.635,-160.244,55.661,-160.046,55.763,-159.962,55.795,-159.914,55.792,-159.874,55.8,-159.81,55.833,-159.771,55.841,-159.743,55.844,-159.679,55.825,-159.665,55.795,-159.67,55.645,-159.66,55.626,-159.61,55.653,-159.568,55.695,-159.541,55.748,-159.523,55.81,-159.429,55.843,-158.79,55.987,-158.705,56.043,-158.627,56.155,-158.591,56.185,-158.554,56.183,-158.543,56.167,-158.523,56.072,-158.505,56.062,-158.476,56.075,-158.432,56.111,-158.276,56.196,-158.291,56.204,-158.317,56.254,-158.344,56.28,-158.386,56.302,-158.467,56.318,-158.536,56.308,-158.552,56.313,-158.537,56.335,-158.414,56.436,-158.352,56.454,-158.189,56.478,-158.124,56.501,-158.071,56.51,-157.982,56.51,-157.93,56.52,-157.929,56.532,-157.978,56.543,-158.078,56.552,-158.028,56.592,-157.869,56.645,-157.771,56.652,-157.674,56.633,-157.61,56.628,-157.578,56.634,-157.529,56.673,-157.49,56.76,-157.441,56.79,-157.39,56.81,-157.334,56.816,-157.271,56.808,-157.206,56.812,-157.139,56.827,-157.067,56.86,-156.988,56.913,-156.923,56.942,-156.872,56.948,-156.824,56.969,-156.78,57.006,-156.713,57.016,-156.629,57.01,-156.592,57.065,-156.501,57.09,-156.475,57.105,-156.4,57.205,-156.398,57.241,-156.444,57.294,-156.474,57.311,-156.478,57.328,-156.436,57.36,-156.242,57.449,-156.156,57.463,-156.09,57.445,-156.055,57.448,-156.037,57.527,-156,57.545,-155.814,57.559,-155.778,57.568,-155.729,57.627,-155.629,57.673,-155.596,57.701,-155.59,57.734,-155.53,57.759,-155.414,57.777,-155.313,57.807,-155.147,57.882,-155.099,57.913,-155.007,58.016,-154.585,58.056,-154.582,58.11,-154.571,58.118,-154.409,58.147,-154.282,58.147,-154.247,58.159,-154.235,58.235,-154.208,58.289,-154.282,58.293,-154.289,58.304,-154.086,58.366,-154.056,58.397,-154.062,58.442,-154.02,58.493,-153.862,58.588,-153.821,58.604,-153.699,58.626,-153.617,58.655,-153.438,58.755,-153.363,58.822,-153.334,58.858,-153.327,58.884,-153.339,58.909,-153.418,58.96,-153.656,59.039,-153.788,59.068,-153.9,59.078,-154.13,59.12,-154.178,59.156,-154.139,59.24,-154.067,59.336,-154.088,59.363,-153.814,59.474,-153.753,59.51,-153.714,59.545,-153.622,59.598,-153.609,59.615,-153.671,59.635,-153.653,59.647,-153.483,59.721,-153.414,59.74,-153.366,59.73,-153.36,59.717,-153.383,59.667,-153.364,59.66,-153.236,59.671,-153.094,59.709,-153.048,59.73,-153.025,59.794,-153.04,59.81,-153.211,59.843,-153.186,59.857,-153.106,59.875,-152.857,59.898,-152.759,59.921,-152.66,59.997,-152.629,60.041,-152.63,60.084,-152.665,60.125,-152.752,60.177,-152.893,60.24,-153.031,60.289,-153.025,60.296,-152.923,60.293,-152.798,60.247,-152.727,60.237,-152.654,60.238,-152.541,60.265,-152.369,60.336,-152.292,60.381,-152.26,60.409,-152.305,60.453,-152.307,60.472,-152.271,60.528,-151.996,60.682,-151.866,60.734,-151.785,60.74,-151.75,60.755,-151.784,60.833,-151.782,60.858,-151.734,60.911,-151.594,60.98,-151.46,61.014,-151.282,61.042,-151.15,61.086,-151.065,61.146,-150.946,61.198,-150.612,61.301,-150.567,61.307,-150.533,61.3,-150.472,61.26,-150.109,61.268,-149.976,61.279,-149.945,61.294,-149.874,61.373,-149.824,61.413,-149.695,61.471,-149.625,61.486,-149.434,61.501,-149.329,61.497,-149.596,61.417,-149.737,61.363,-149.829,61.308,-149.882,61.264,-149.895,61.232,-149.927,61.213,-150.019,61.194,-150.053,61.171,-149.968,61.122,-149.592,60.994,-149.459,60.965,-149.142,60.936,-149.071,60.886,-149.075,60.876,-149.173,60.88,-149.632,60.952,-149.856,60.962,-149.998,60.935,-150.113,60.933,-150.203,60.955,-150.281,60.985,-150.349,61.023,-150.441,61.024,-150.779,60.915,-150.954,60.841,-151.322,60.743,-151.356,60.723,-151.355,60.66,-151.318,60.554,-151.313,60.466,-151.396,60.274,-151.451,60.203,-151.612,60.092,-151.735,59.988,-151.783,59.921,-151.853,59.782,-151.817,59.721,-151.764,59.7,-151.513,59.651,-151.45,59.65,-151.404,59.662,-151.089,59.789,-151.057,59.782,-151.046,59.772,-151.189,59.638,-151.262,59.586,-151.4,59.516,-151.513,59.483,-151.693,59.462,-151.85,59.406,-151.885,59.386,-151.932,59.343,-151.964,59.285,-151.95,59.265,-151.904,59.26,-151.738,59.189,-151.619,59.187,-151.477,59.231,-151.366,59.246,-151.287,59.232,-151.222,59.229,-151.171,59.237,-151.163,59.257,-151.199,59.29,-151.183,59.301,-151.064,59.278,-150.961,59.244,-150.935,59.249,-150.899,59.303,-150.853,59.342,-150.677,59.427,-150.623,59.48,-150.621,59.535,-150.607,59.563,-150.582,59.565,-150.526,59.537,-150.485,59.535,-150.338,59.581,-150.296,59.583,-150.258,59.571,-150.198,59.567,-149.967,59.69,-149.96,59.713,-150.016,59.777,-150.005,59.784,-149.965,59.782,-149.801,59.738,-149.782,59.75,-149.804,59.833,-149.795,59.856,-149.714,59.92,-149.685,59.895,-149.63,59.785,-149.613,59.767,-149.598,59.77,-149.549,59.894,-149.46,59.966,-149.432,60.001,-149.415,60.1,-149.395,60.106,-149.305,60.014,-149.267,59.998,-149.122,60.033,-149.07,60,-149.004,59.98,-148.843,59.951,-148.751,59.948,-148.644,59.957,-148.574,59.97,-148.542,59.987,-148.506,59.989,-148.465,59.975,-148.431,59.989,-148.333,60.122,-148.291,60.145,-148.245,60.147,-148.214,60.154,-148.198,60.168,-148.219,60.268,-148.216,60.323,-148.204,60.365,-148.182,60.393,-148.129,60.414,-148.046,60.428,-147.991,60.452,-147.964,60.485,-147.984,60.523,-148.051,60.567,-148.119,60.575,-148.189,60.547,-148.296,60.532,-148.44,60.53,-148.549,60.515,-148.624,60.486,-148.64,60.489,-148.597,60.524,-148.51,60.565,-148.468,60.572,-148.338,60.57,-148.305,60.583,-148.284,60.609,-148.257,60.675,-148.268,60.7,-148.341,60.724,-148.399,60.734,-148.557,60.803,-148.556,60.827,-148.471,60.835,-148.393,60.832,-148.344,60.854,-148.293,60.94,-148.209,61.03,-148.226,61.044,-148.287,61.036,-148.396,61.007,-148.411,61.011,-148.389,61.037,-148.342,61.06,-148.27,61.082,-148.209,61.088,-148.158,61.08,-148.049,61.083,-147.986,61.106,-147.845,61.186,-147.774,61.218,-147.752,61.219,-147.971,61.019,-148.005,60.969,-147.991,60.948,-147.891,60.89,-147.808,60.885,-147.656,60.91,-147.623,60.933,-147.593,60.979,-147.567,60.995,-147.523,60.97,-147.433,60.95,-147.391,60.918,-147.361,60.915,-147.321,60.925,-147.286,60.947,-147.255,60.978,-147.195,60.997,-147.106,61.003,-147.034,60.996,-146.98,60.978,-146.874,61.005,-146.716,61.078,-146.583,61.128,-146.384,61.136,-146.285,61.113,-146.599,61.054,-146.636,60.993,-146.638,60.897,-146.604,60.871,-146.532,60.839,-146.392,60.811,-146.496,60.757,-146.546,60.745,-146.57,60.729,-146.503,60.701,-146.347,60.738,-146.251,60.749,-146.182,60.735,-146.167,60.716,-146.166,60.692,-146.149,60.661,-145.675,60.651,-145.69,60.622,-145.76,60.562,-145.811,60.525,-145.899,60.478,-145.848,60.469,-145.718,60.468,-145.563,60.441,-145.382,60.389,-145.248,60.38,-145.163,60.415,-145.096,60.454,-144.984,60.537,-144.863,60.601,-144.724,60.663,-144.691,60.669,-144.787,60.585,-144.824,60.534,-144.862,60.459,-144.901,60.335,-144.852,60.295,-144.741,60.273,-144.672,60.249,-144.643,60.225,-144.53,60.205,-144.333,60.191,-144.185,60.151,-144.089,60.084,-144.084,60.063,-144.161,60.046,-144.147,60.016,-143.979,60.009,-143.805,60.013,-143.506,60.055,-142.946,60.097,-142.549,60.086,-142.104,60.033,-141.67,59.97,-141.53,59.995,-141.447,60.019,-141.41,60.042,-141.422,60.085,-141.422,60.109,-141.409,60.118,-141.362,60.105,-141.33,60.083,-141.29,60.004,-141.295,59.98,-141.408,59.903,-141.332,59.874,-140.843,59.749,-140.648,59.723,-140.42,59.711,-140.217,59.727,-139.917,59.806,-139.85,59.831,-139.612,59.973,-139.519,60.017,-139.431,60.012,-139.402,60.001,-139.242,59.893,-138.988,59.835,-139.048,59.828,-139.179,59.84,-139.221,59.82,-139.246,59.782,-139.259,59.743,-139.266,59.663,-139.276,59.62,-139.287,59.611,-139.32,59.739,-139.315,59.848,-139.331,59.877,-139.447,59.957,-139.483,59.964,-139.512,59.954,-139.554,59.933,-139.569,59.912,-139.581,59.881,-139.582,59.848,-139.558,59.79,-139.506,59.726,-139.513,59.698,-139.612,59.61,-139.674,59.587,-139.766,59.566,-139.799,59.546,-139.773,59.527,-139.714,59.504,-139.577,59.462,-139.341,59.376,-138.884,59.237,-138.704,59.188,-138.515,59.166,-138.521,59.152,-138.56,59.129,-138.537,59.115,-138.451,59.11,-138.352,59.087,-138.241,59.047,-138.027,58.941,-137.961,58.891,-137.934,58.847,-137.864,58.786,-137.75,58.707,-137.661,58.66,-137.597,58.644,-137.565,58.626,-137.557,58.59,-137.544,58.581,-137.072,58.395,-136.865,58.332,-136.699,58.266,-136.607,58.244,-136.583,58.245,-136.462,58.328,-136.13,58.35,-136.081,58.364,-136.056,58.384,-136.061,58.453,-136.103,58.506,-136.225,58.602,-136.32,58.624,-136.484,58.618,-136.404,58.68,-136.41,58.701,-136.533,58.74,-136.549,58.752,-136.568,58.786,-136.614,58.809,-136.74,58.85,-136.879,58.882,-136.963,58.884,-137.038,58.867,-137.059,58.874,-136.988,58.925,-136.948,58.935,-136.953,58.967,-137.002,59.021,-136.989,59.034,-136.831,58.984,-136.566,58.941,-136.511,58.907,-136.478,58.863,-136.451,58.846,-136.38,58.827,-136.299,58.787,-136.226,58.765,-136.186,58.77,-136.147,58.789,-136.124,58.82,-136.118,58.863,-136.124,58.893,-136.159,58.947,-136.15,59.048,-136.134,59.04,-136.101,59,-136.049,58.893,-136.017,58.874,-135.932,58.904,-135.826,58.898,-136.043,58.822,-136.046,58.789,-135.89,58.623,-135.862,58.577,-135.896,58.464,-135.898,58.4,-135.873,58.394,-135.572,58.412,-135.45,58.376,-135.363,58.298,-135.303,58.256,-135.142,58.233,-135.09,58.246,-135.06,58.279,-135.05,58.307,-135.062,58.341,-135.152,58.512,-135.185,58.59,-135.207,58.671,-135.257,58.778,-135.334,58.91,-135.386,59.088,-135.502,59.202,-135.434,59.211,-135.4,59.208,-135.417,59.242,-135.484,59.309,-135.413,59.318,-135.403,59.353,-135.364,59.419,-135.349,59.41,-135.358,59.325,-135.33,59.239,-135.217,59.077,-135.132,58.843,-135.076,58.797,-134.986,58.766,-134.965,58.742,-134.943,58.646,-134.776,58.454,-134.664,58.385,-134.485,58.367,-134.331,58.3,-134.258,58.244,-134.209,58.233,-134.131,58.279,-134.036,58.415,-133.944,58.498,-133.911,58.515,-133.877,58.518,-133.889,58.499,-133.934,58.468,-134.045,58.289,-134.063,58.211,-134.057,58.128,-134.031,58.072,-133.894,57.993,-133.821,57.936,-133.744,57.855,-133.722,57.844,-133.657,57.841,-133.626,57.857,-133.559,57.924,-133.497,57.925,-133.194,57.878,-133.212,57.866,-133.511,57.88,-133.536,57.864,-133.535,57.833,-133.515,57.775,-133.436,57.727,-133.117,57.566,-133.143,57.555,-133.342,57.631,-133.554,57.695,-133.603,57.695,-133.627,57.677,-133.649,57.642,-133.539,57.554,-133.437,57.337,-133.466,57.172,-132.913,57.047,-132.825,57.056,-132.814,57.041,-132.839,56.96,-132.83,56.931,-132.802,56.895,-132.702,56.822,-132.64,56.796,-132.487,56.766,-132.476,56.65,-132.434,56.634,-132.358,56.626,-132.337,56.603,-132.332,56.558,-132.305,56.52,-132.256,56.489,-132.182,56.421,-132.133,56.4,-132.022,56.38,-131.962,56.324,-131.927,56.273,-131.888,56.242,-131.844,56.23,-131.551,56.207,-131.738,56.161,-131.844,56.16,-132.006,55.93,-132.091,55.84,-132.158,55.781,-132.208,55.753,-132.223,55.721,-132.155,55.6,-132.119,55.57,-131.983,55.535,-131.945,55.554,-131.869,55.647,-131.834,55.735,-131.803,55.766,-131.799,55.783,-131.826,55.835,-131.815,55.854,-131.784,55.877,-131.635,55.932,-131.288,56.012,-131.033,56.088,-131.074,56.044,-131.14,55.998,-131.128,55.96,-130.977,55.812,-130.919,55.736,-130.88,55.612,-130.873,55.551,-130.88,55.46,-130.856,55.355,-130.835,55.332,-130.748,55.318,-130.75,55.297,-130.984,55.244,-131.046,55.18,-131.048,55.158,-130.98,55.061,-130.935,54.95,-130.85,54.808,-130.616,54.791,-130.575,54.77,-130.535,54.749,-130.349,54.815,-130.204,54.947,-130.214,55.026,-130.219,55.06,-130.172,55.137,-130.037,55.298,-130.039,55.344,-130.059,55.412,-130.12,55.524,-130.14,55.585,-130.147,55.654,-130.137,55.719,-130.112,55.78,-130.075,55.836,-130.025,55.888,-130.02,55.881,-130.06,55.814,-130.085,55.752,-130.095,55.695,-130.093,55.632,-130.08,55.563,-130.044,55.472,-129.985,55.359,-129.996,55.264,-130.058,55.195,-130.092,55.108,-130.048,55.057,-129.985,55.111,-129.877,55.251,-129.838,55.319,-129.816,55.418,-129.812,55.533,-129.795,55.56,-129.765,55.498,-129.734,55.458,-129.701,55.439,-129.667,55.437,-129.63,55.452,-129.561,55.463,-129.781,55.28,-129.89,55.165,-129.949,55.081,-130.109,54.887,-130.141,54.823,-130.219,54.73,-130.307,54.7,-130.35,54.655,-130.37,54.62,-130.389,54.539,-130.393,54.48,-130.43,54.421,-130.397,54.352,-130.29,54.27,-130.084,54.181,-129.898,54.226,-129.795,54.236,-129.626,54.23,-129.791,54.166,-130.043,54.134,-130.064,54.106,-130.086,53.976,-130.233,53.867,-130.335,53.724,-130.263,53.654,-130.074,53.576,-129.912,53.551,-129.822,53.413,-129.687,53.334,-129.564,53.251,-129.462,53.347,-129.284,53.393,-129.258,53.418,-129.24,53.479,-129.232,53.576,-129.208,53.642,-129.056,53.778,-129.014,53.797,-128.959,53.841,-128.944,53.84,-128.928,53.823,-128.89,53.83,-128.705,53.919,-128.651,53.919,-128.532,53.858,-128.56,53.845,-128.653,53.832,-128.715,53.81,-128.746,53.78,-128.764,53.747,-128.768,53.71,-128.751,53.661,-128.676,53.555,-128.6,53.506,-128.512,53.477,-128.47,53.471,-128.369,53.49,-128.207,53.483,-128.115,53.446,-127.95,53.33,-127.928,53.275,-128.079,53.369,-128.133,53.418,-128.291,53.458,-128.358,53.46,-128.479,53.41,-128.542,53.421,-128.833,53.549,-128.906,53.559,-128.85,53.665,-128.855,53.705,-128.936,53.715,-129.021,53.692,-129.114,53.641,-129.172,53.534,-129.13,53.442,-129.081,53.367,-128.869,53.328,-128.652,53.244,-128.525,53.141,-128.452,52.877,-128.365,52.826,-128.106,52.907,-128.053,52.911,-128.109,52.858,-128.132,52.806,-128.197,52.623,-128.275,52.435,-128.272,52.363,-128.241,52.368,-128.184,52.408,-128.038,52.531,-127.943,52.551,-127.94,52.545,-128.021,52.491,-128.052,52.453,-128.06,52.428,-128.029,52.342,-128.037,52.318,-128.358,52.159,-128.194,51.998,-128.102,51.788,-127.995,51.951,-127.902,52.151,-127.834,52.251,-127.792,52.289,-127.713,52.319,-127.56,52.343,-127.519,52.359,-127.107,52.633,-127.066,52.653,-127.035,52.682,-127.013,52.72,-127.006,52.755,-127.019,52.842,-127.008,52.843,-126.966,52.785,-126.951,52.751,-126.951,52.721,-126.995,52.658,-127.187,52.538,-127.208,52.498,-127.194,52.458,-127.161,52.395,-127.127,52.371,-126.938,52.309,-126.901,52.265,-126.895,52.225,-126.753,52.112,-126.714,52.061,-126.739,52.065,-126.826,52.125,-126.9,52.188,-126.959,52.255,-127.008,52.291,-127.176,52.315,-127.242,52.395,-127.438,52.356,-127.55,52.298,-127.673,52.253,-127.795,52.191,-127.843,52.086,-127.859,51.99,-127.728,51.993,-127.83,51.879,-127.863,51.821,-127.869,51.775,-127.851,51.673,-127.819,51.604,-127.748,51.544,-127.729,51.506,-127.714,51.49,-127.669,51.478,-127.645,51.478,-127.61,51.514,-127.576,51.563,-127.443,51.679,-127.339,51.707,-127.034,51.717,-126.691,51.703,-126.735,51.693,-126.968,51.67,-127.281,51.654,-127.347,51.642,-127.42,51.608,-127.633,51.427,-127.689,51.343,-127.714,51.269,-127.708,51.151,-127.591,51.088,-127.441,50.989,-127.357,50.946,-127.267,50.916,-127.058,50.868,-127.014,50.867,-126.96,50.894,-126.632,50.915,-126.563,50.965,-126.517,51.057,-126.485,50.96,-126.522,50.866,-126.488,50.842,-126.418,50.85,-126.375,50.837,-126.397,50.807,-126.472,50.767,-126.517,50.724,-126.514,50.679,-126.493,50.672,-126.37,50.667,-125.981,50.711,-125.904,50.705,-125.898,50.684,-126.067,50.664,-126.239,50.624,-126.416,50.607,-126.447,50.588,-126.45,50.55,-126.404,50.53,-126.237,50.523,-126.094,50.498,-126.024,50.497,-125.965,50.487,-125.84,50.511,-125.772,50.508,-125.741,50.479,-125.698,50.465,-125.641,50.466,-125.61,50.486,-125.586,50.574,-125.556,50.635,-125.539,50.649,-125.526,50.534,-125.507,50.507,-125.476,50.497,-125.21,50.476,-125.059,50.514,-124.985,50.592,-124.943,50.666,-124.931,50.718,-124.949,50.765,-124.934,50.811,-124.86,50.872,-124.875,50.826,-124.858,50.717,-124.854,50.669,-124.863,50.637,-124.937,50.537,-125.057,50.419,-125.044,50.364,-124.986,50.356,-124.933,50.298,-124.934,50.258,-124.784,50.073,-124.782,50.02,-124.702,49.958,-124.483,49.808,-124.413,49.778,-124.281,49.772,-124.142,49.793,-124.059,49.854,-123.985,49.876,-123.972,49.892,-123.971,49.97,-123.957,49.993,-123.866,50.072,-123.863,50.103,-123.946,50.184,-123.934,50.188,-123.88,50.174,-123.825,50.144,-123.788,50.107,-123.785,50.088,-123.824,50.044,-123.885,50.017,-123.904,49.981,-123.904,49.795,-123.874,49.737,-123.818,49.685,-123.763,49.658,-123.708,49.657,-123.582,49.681,-123.613,49.658,-123.739,49.594,-123.817,49.587,-123.847,49.637,-123.923,49.718,-123.96,49.736,-123.993,49.736,-124.024,49.711,-124.054,49.662,-124.029,49.603,-123.948,49.535,-123.892,49.495,-123.859,49.483,-123.531,49.397,-123.508,49.402,-123.437,49.451,-123.399,49.442,-123.336,49.459,-123.322,49.517,-123.337,49.545,-123.325,49.578,-123.187,49.68,-123.18,49.674,-123.191,49.644,-123.223,49.59,-123.248,49.443,-123.264,49.39,-123.286,49.375,-123.291,49.359,-123.277,49.344,-123.174,49.348,-123.016,49.322,-122.964,49.329,-122.879,49.399,-122.913,49.323,-122.948,49.293,-123.067,49.292,-123.184,49.278,-123.229,49.26,-123.191,49.22,-123.196,49.148,-123.182,49.129,-123.15,49.121,-123.08,49.131,-123.077,49.118,-123.109,49.085,-123.118,49.056,-123.086,48.993,-123.077,48.98,-123.063,48.978,-123.049,48.993,-123.027,49.039,-123.002,49.061,-122.963,49.075,-122.924,49.075,-122.827,49.028,-122.789,48.993,-122.722,48.853,-122.686,48.794,-122.653,48.764,-122.599,48.767,-122.58,48.78,-122.562,48.778,-122.545,48.762,-122.513,48.669,-122.515,48.555,-122.501,48.538,-122.497,48.506,-122.543,48.488,-122.628,48.498,-122.657,48.49,-122.669,48.465,-122.662,48.446,-122.638,48.433,-122.583,48.429,-122.542,48.411,-122.488,48.374,-122.409,48.294,-122.403,48.269,-122.467,48.258,-122.52,48.229,-122.529,48.199,-122.517,48.16,-122.494,48.13,-122.395,48.084,-122.387,48.09,-122.425,48.176,-122.416,48.184,-122.389,48.166,-122.353,48.114,-122.317,48.08,-122.261,48.042,-122.242,48.011,-122.318,47.933,-122.33,47.899,-122.393,47.821,-122.402,47.784,-122.382,47.752,-122.384,47.716,-122.407,47.677,-122.41,47.653,-122.381,47.628,-122.368,47.604,-122.375,47.528,-122.351,47.395,-122.354,47.372,-122.42,47.312,-122.465,47.296,-122.511,47.295,-122.542,47.276,-122.604,47.167,-122.627,47.144,-122.702,47.111,-122.73,47.112,-122.812,47.146,-122.914,47.131,-123.028,47.139,-122.988,47.173,-122.956,47.245,-122.92,47.29,-122.828,47.337,-122.813,47.329,-122.783,47.226,-122.768,47.218,-122.721,47.305,-122.708,47.316,-122.649,47.281,-122.604,47.275,-122.578,47.293,-122.554,47.405,-122.557,47.463,-122.586,47.528,-122.675,47.612,-122.664,47.617,-122.628,47.608,-122.614,47.616,-122.63,47.693,-122.618,47.713,-122.524,47.769,-122.511,47.816,-122.533,47.92,-122.586,47.928,-122.593,47.916,-122.588,47.856,-122.608,47.835,-122.718,47.762,-122.757,47.701,-122.814,47.659,-122.913,47.607,-122.982,47.559,-123.049,47.479,-123.06,47.454,-123.067,47.4,-123.018,47.401,-122.917,47.418,-122.922,47.408,-123.031,47.36,-123.104,47.348,-123.136,47.356,-123.139,47.386,-123.131,47.438,-123.051,47.552,-122.821,47.793,-122.805,47.784,-122.802,47.735,-122.778,47.738,-122.657,47.881,-122.679,47.932,-122.74,48.013,-122.769,48.076,-122.768,48.12,-122.779,48.138,-122.861,48.09,-122.909,48.077,-122.974,48.073,-123.024,48.082,-123.124,48.151,-123.162,48.155,-123.25,48.124,-123.294,48.12,-123.976,48.168,-124.099,48.2,-124.175,48.242,-124.429,48.301,-124.633,48.375,-124.71,48.38,-124.68,48.286,-124.702,48.152,-124.663,47.974,-124.621,47.904,-124.46,47.784,-124.376,47.659,-124.309,47.405,-124.199,47.209,-124.171,47.087,-124.164,47.015,-124.139,46.955,-124.117,47,-124.112,47.035,-124.042,47.03,-123.986,46.984,-123.843,46.963,-124.113,46.863,-124.072,46.745,-123.958,46.709,-123.889,46.66,-123.912,46.533,-123.946,46.433,-124.016,46.521,-124.044,46.605,-124.05,46.491,-124.045,46.373,-124.073,46.279,-123.96,46.301,-123.896,46.268,-123.688,46.3,-123.65,46.268,-123.465,46.271,-123.405,46.221,-123.299,46.171,-123.251,46.167,-123.221,46.154,-123.322,46.144,-123.402,46.155,-123.466,46.209,-123.522,46.223,-123.674,46.183,-123.912,46.182,-123.963,46.225,-123.989,46.219,-123.975,46.178,-123.947,46.141,-123.961,45.843,-123.929,45.577,-123.963,45.476,-123.949,45.401,-124.059,44.778,-124.045,44.648,-124.065,44.52,-124.047,44.425,-124.099,44.334,-124.131,44.056,-124.149,43.692,-124.184,43.652,-124.239,43.54,-124.288,43.41,-124.233,43.436,-124.197,43.423,-124.275,43.367,-124.321,43.368,-124.347,43.342,-124.454,43.012,-124.499,42.937,-124.54,42.813,-124.444,42.67,-124.406,42.584,-124.421,42.381,-124.41,42.304,-124.355,42.123,-124.228,42.001,-124.212,41.985,-124.209,41.889,-124.245,41.788,-124.163,41.719,-124.118,41.622,-124.072,41.46,-124.069,41.384,-124.14,41.156,-124.133,40.97,-124.2,40.822,-124.219,40.791,-124.223,40.775,-124.19,40.772,-124.208,40.746,-124.22,40.696,-124.251,40.704,-124.242,40.728,-124.254,40.74,-124.284,40.711,-124.325,40.598,-124.372,40.491,-124.357,40.371,-124.324,40.252,-124.108,40.095,-123.884,39.861,-123.833,39.775,-123.783,39.619,-123.778,39.515,-123.82,39.368,-123.72,39.111,-123.701,38.907,-123.425,38.676,-123.29,38.536,-123.121,38.449,-123.046,38.305,-122.987,38.277,-122.908,38.197,-122.877,38.123,-122.978,38.227,-122.968,38.097,-123.001,38.019,-122.999,37.989,-122.932,38.055,-122.873,38.026,-122.76,37.946,-122.681,37.902,-122.584,37.874,-122.521,37.826,-122.467,37.838,-122.495,37.954,-122.484,38.109,-122.393,38.145,-122.337,38.136,-122.208,38.073,-122.154,38.066,-122.031,38.124,-121.993,38.12,-121.934,38.087,-121.881,38.075,-121.749,38.08,-121.682,38.075,-121.626,38.084,-121.525,38.056,-121.573,38.052,-121.638,38.061,-121.717,38.034,-122.087,38.05,-122.217,38.041,-122.314,38.007,-122.385,37.961,-122.365,37.921,-122.333,37.897,-122.296,37.79,-122.222,37.732,-122.158,37.626,-122.124,37.544,-122.097,37.518,-122.071,37.478,-122.119,37.483,-122.166,37.502,-122.229,37.564,-122.298,37.592,-122.37,37.656,-122.39,37.741,-122.384,37.789,-122.446,37.798,-122.514,37.772,-122.5,37.653,-122.499,37.543,-122.408,37.373,-122.395,37.208,-122.164,36.991,-121.881,36.939,-121.807,36.851,-121.795,36.801,-121.79,36.732,-121.835,36.657,-121.919,36.572,-121.91,36.433,-121.877,36.331,-121.664,36.154,-121.465,35.927,-121.434,35.864,-121.344,35.792,-121.284,35.676,-121.138,35.607,-121.023,35.481,-120.9,35.425,-120.86,35.365,-120.885,35.275,-120.857,35.21,-120.707,35.158,-120.659,35.122,-120.634,35.076,-120.663,34.949,-120.625,34.812,-120.638,34.749,-120.627,34.669,-120.645,34.58,-120.56,34.544,-120.481,34.472,-120.396,34.46,-120.17,34.476,-120.053,34.469,-119.853,34.412,-119.713,34.4,-119.606,34.418,-119.414,34.339,-119.268,34.257,-119.236,34.164,-119.144,34.112,-118.832,34.024,-118.599,34.035,-118.506,34.017,-118.393,33.858,-118.41,33.744,-118.294,33.712,-118.264,33.759,-118.162,33.751,-118.081,33.722,-117.952,33.62,-117.789,33.538,-117.467,33.296,-117.319,33.1,-117.263,32.939,-117.256,32.873,-117.271,32.806,-117.243,32.664,-117.184,32.688,-117.137,32.649,-117.13,32.54,-117.128,32.533,-117.063,32.344,-117.035,32.305,-116.914,32.199,-116.848,31.997,-116.621,31.851,-116.624,31.758,-116.652,31.74,-116.702,31.744,-116.722,31.735,-116.668,31.699,-116.662,31.565,-116.61,31.499,-116.458,31.361,-116.333,31.203,-116.31,31.127,-116.31,31.051,-116.296,30.971,-116.062,30.804,-116.035,30.705,-116.029,30.564,-115.996,30.414,-115.858,30.36,-115.816,30.304,-115.79,30.084,-115.808,29.96,-115.749,29.936,-115.674,29.756,-115.565,29.68,-115.311,29.532,-115.166,29.427,-114.994,29.384,-114.937,29.352,-114.876,29.282,-114.664,29.095,-114.309,28.73,-114.146,28.605,-114.048,28.426,-114.093,28.221,-114.185,28.013,-114.266,27.934,-114.253,27.908,-114.158,27.92,-114.157,27.868,-114.175,27.831,-114.135,27.727,-114.069,27.676,-114.137,27.671,-114.233,27.718,-114.302,27.776,-114.289,27.839,-114.301,27.873,-114.373,27.841,-114.448,27.797,-114.57,27.784,-114.824,27.83,-115.036,27.842,-115.033,27.799,-114.994,27.736,-114.859,27.659,-114.716,27.54,-114.54,27.431,-114.498,27.376,-114.48,27.284,-114.445,27.218,-114.333,27.158,-114.202,27.144,-114.11,27.106,-113.996,26.988,-113.936,26.985,-113.841,26.967,-113.757,26.871,-113.701,26.791,-113.599,26.721,-113.426,26.796,-113.272,26.791,-113.206,26.857,-113.156,26.946,-113.143,26.792,-113.119,26.717,-113.021,26.583,-112.658,26.317,-112.526,26.273,-112.377,26.214,-112.174,25.913,-112.12,25.766,-112.115,25.63,-112.093,25.584,-112.07,25.573,-112.056,25.488,-112.078,25.324,-112.129,25.043,-112.119,24.934,-112.073,24.84,-111.848,24.67,-111.825,24.632,-111.822,24.573,-111.802,24.543,-111.75,24.554,-111.683,24.556,-111.578,24.443,-111.419,24.329,-111.036,24.105,-110.896,23.97,-110.765,23.877,-110.63,23.737,-110.363,23.605,-110.289,23.518,-110.244,23.412,-110.181,23.342,-110.086,23.005,-110.006,22.894,-109.923,22.886,-109.823,22.922,-109.728,22.982,-109.63,23.079,-109.496,23.16,-109.458,23.215,-109.415,23.406,-109.421,23.48,-109.51,23.598,-109.677,23.662,-109.711,23.804,-109.776,23.865,-109.811,23.939,-109.893,24.033,-109.983,24.109,-110.023,24.175,-110.263,24.345,-110.304,24.339,-110.325,24.306,-110.321,24.259,-110.297,24.195,-110.32,24.139,-110.367,24.1,-110.41,24.131,-110.4,24.165,-110.421,24.183,-110.547,24.214,-110.659,24.341,-110.735,24.59,-110.729,24.672,-110.677,24.789,-110.687,24.868,-110.756,24.995,-110.894,25.144,-111.014,25.42,-111.034,25.527,-111.15,25.573,-111.292,25.79,-111.33,25.931,-111.332,26.125,-111.405,26.265,-111.419,26.35,-111.465,26.408,-111.47,26.507,-111.546,26.579,-111.57,26.708,-111.795,26.88,-111.822,26.865,-111.817,26.756,-111.779,26.687,-111.699,26.581,-111.723,26.564,-111.754,26.573,-111.863,26.679,-111.883,26.84,-112.009,26.967,-112.016,27.01,-112.004,27.079,-112.098,27.146,-112.191,27.187,-112.283,27.347,-112.329,27.523,-112.553,27.657,-112.734,27.826,-112.758,27.901,-112.749,27.995,-112.808,28.092,-112.796,28.207,-112.868,28.292,-112.865,28.351,-112.871,28.424,-112.957,28.456,-113.034,28.473,-113.094,28.512,-113.206,28.799,-113.259,28.819,-113.321,28.813,-113.335,28.839,-113.329,28.873,-113.382,28.947,-113.5,28.927,-113.538,29.023,-113.545,29.102,-113.755,29.367,-113.829,29.439,-114.062,29.61,-114.179,29.734,-114.373,29.83,-114.403,29.896,-114.55,30.022,-114.63,30.156,-114.65,30.238,-114.633,30.507,-114.685,30.621,-114.703,30.765,-114.761,30.959,-114.845,31.08,-114.882,31.156,-114.848,31.538,-114.79,31.647,-114.84,31.799,-114.895,31.851,-114.934,31.901,-114.741,31.806,-114.698,31.777,-114.609,31.762,-114.549,31.734,-114.264,31.554,-114.149,31.507,-114.081,31.51,-114.003,31.525,-113.977,31.593,-113.948,31.629,-113.759,31.558,-113.7,31.523,-113.633,31.468,-113.623,31.346,-113.481,31.294,-113.231,31.256,-113.186,31.236,-113.084,31.207,-113.047,31.179,-113.043,31.087,-113.073,31.061,-113.108,31.077,-113.119,31.048,-113.105,31.027,-113.087,30.938,-113.11,30.793,-113.058,30.651,-112.952,30.51,-112.825,30.3,-112.759,30.126,-112.738,29.985,-112.697,29.917,-112.653,29.87,-112.573,29.72,-112.415,29.536,-112.389,29.46,-112.393,29.42,-112.378,29.348,-112.301,29.323,-112.223,29.269,-112.192,29.118,-112.162,29.019,-112.045,28.896,-111.941,28.823,-111.919,28.798,-111.907,28.752,-111.832,28.648,-111.747,28.564,-111.68,28.471,-111.472,28.384,-111.282,28.115,-111.121,27.967,-110.986,27.926,-110.921,27.889,-110.849,27.918,-110.759,27.915,-110.53,27.864,-110.578,27.796,-110.615,27.654,-110.593,27.544,-110.561,27.45,-110.519,27.396,-110.478,27.323,-110.377,27.233,-110.277,27.162,-109.944,27.079,-109.926,27.029,-109.922,26.978,-109.891,26.883,-109.828,26.77,-109.755,26.703,-109.676,26.697,-109.483,26.71,-109.276,26.534,-109.243,26.45,-109.241,26.405,-109.216,26.355,-109.146,26.306,-109.117,26.253,-109.159,26.258,-109.2,26.305,-109.271,26.243,-109.354,26.138,-109.426,26.033,-109.385,25.727,-109.304,25.633,-109.254,25.609,-109.196,25.593,-109.084,25.615,-109.008,25.642,-108.935,25.69,-108.887,25.733,-108.885,25.696,-108.973,25.588,-109.068,25.552,-109.063,25.517,-109.029,25.48,-108.893,25.512,-108.844,25.543,-108.787,25.538,-108.751,25.424,-108.696,25.383,-108.466,25.265,-108.374,25.194,-108.093,25.094,-108.051,25.067,-108.036,25.035,-108.08,25.018,-108.14,25.018,-108.192,25.031,-108.243,25.074,-108.281,25.082,-108.208,24.975,-108.015,24.783,-108.009,24.694,-107.951,24.615,-107.817,24.539,-107.71,24.525,-107.674,24.504,-107.602,24.49,-107.549,24.505,-107.512,24.489,-107.489,24.424,-107.494,24.369,-107.527,24.36,-107.727,24.472,-107.765,24.472,-107.085,24.016,-106.935,23.881,-106.729,23.611,-106.567,23.449,-106.402,23.196,-106.235,23.061,-106.022,22.829,-105.943,22.777,-105.792,22.627,-105.646,22.327,-105.649,21.988,-105.527,21.818,-105.457,21.672,-105.431,21.618,-105.209,21.491,-105.233,21.38,-105.225,21.25,-105.237,21.119,-105.302,21.027,-105.394,20.926,-105.456,20.844,-105.511,20.809,-105.492,20.777,-105.42,20.775,-105.327,20.753,-105.252,20.669,-105.245,20.634,-105.26,20.579,-105.377,20.512,-105.543,20.498,-105.642,20.436,-105.669,20.386,-105.616,20.316,-105.57,20.228,-105.532,20.075,-105.482,19.976,-105.286,19.706,-105.108,19.562,-105.045,19.443,-104.938,19.309,-104.603,19.153,-104.405,19.091,-104.277,19.011,-104.046,18.912,-103.912,18.828,-103.699,18.633,-103.58,18.484,-103.442,18.325,-103.019,18.187,-102.7,18.063,-102.547,18.041,-102.217,17.957,-101.996,17.973,-101.919,17.96,-101.847,17.922,-101.762,17.842,-101.6,17.652,-101.487,17.615,-101.385,17.514,-101.148,17.393,-101.002,17.276,-100.848,17.2,-100.432,17.064,-100.243,16.984,-100.025,16.921,-99.691,16.72,-99.348,16.665,-99.002,16.581,-98.908,16.545,-98.762,16.535,-98.52,16.305,-98.139,16.206,-97.755,15.967,-97.185,15.909,-96.808,15.726,-96.511,15.652,-96.409,15.683,-96.214,15.693,-95.772,15.888,-95.464,15.975,-95.134,16.177,-94.949,16.21,-94.799,16.21,-94.786,16.229,-94.846,16.247,-95.021,16.278,-95.024,16.306,-94.935,16.379,-94.9,16.417,-94.859,16.42,-94.793,16.365,-94.797,16.327,-94.791,16.287,-94.753,16.291,-94.651,16.352,-94.617,16.348,-94.587,16.316,-94.682,16.228,-94.662,16.202,-94.471,16.187,-94.001,16.019,-94.028,16.062,-94.193,16.146,-94.25,16.168,-94.303,16.169,-94.37,16.195,-94.426,16.226,-94.409,16.287,-94.374,16.285,-94.311,16.239,-94.24,16.205,-94.079,16.145,-93.916,16.054,-93.734,15.888,-93.541,15.75,-93.167,15.448,-93.024,15.31,-92.918,15.236,-92.809,15.139,-92.531,14.84,-92.265,14.568,-92.235,14.545,-91.819,14.228,-91.641,14.115,-91.377,13.99,-91.146,13.926,-90.607,13.929,-90.479,13.901,-90.095,13.737,-89.97,13.683,-89.804,13.56,-89.523,13.509,-89.278,13.478,-88.867,13.283,-88.512,13.184,-88.484,13.197,-88.582,13.245,-88.656,13.259,-88.686,13.281,-88.592,13.281,-88.417,13.214,-88.181,13.164,-88.023,13.169,-87.931,13.181,-87.878,13.224,-87.821,13.285,-87.838,13.386,-87.814,13.399,-87.769,13.377,-87.708,13.36,-87.602,13.386,-87.489,13.353,-87.485,13.311,-87.498,13.275,-87.458,13.215,-87.413,13.127,-87.333,13.085,-87.337,12.979,-87.339,12.95,-87.39,12.921,-87.424,12.921,-87.498,12.984,-87.543,13.04,-87.585,13.043,-87.67,12.966,-87.668,12.904,-87.46,12.758,-87.188,12.508,-87.125,12.434,-86.851,12.248,-86.756,12.157,-86.656,11.982,-86.469,11.738,-85.961,11.331,-85.829,11.199,-85.745,11.089,-85.744,11.062,-85.744,11.043,-85.752,10.985,-85.887,10.921,-85.908,10.898,-85.833,10.85,-85.715,10.791,-85.667,10.745,-85.671,10.68,-85.663,10.635,-85.703,10.563,-85.831,10.398,-85.85,10.292,-85.796,10.133,-85.681,9.959,-85.625,9.902,-85.315,9.811,-85.154,9.62,-85.115,9.582,-85.077,9.602,-85.06,9.668,-85.001,9.699,-84.886,9.821,-84.908,9.885,-84.963,9.933,-85.161,10.017,-85.237,10.107,-85.263,10.257,-85.236,10.242,-85.198,10.195,-85.025,10.116,-84.715,9.899,-84.643,9.789,-84.67,9.703,-84.659,9.647,-84.582,9.568,-84.483,9.526,-84.222,9.463,-84.118,9.379,-83.896,9.276,-83.737,9.15,-83.637,9.035,-83.616,8.96,-83.614,8.804,-83.642,8.729,-83.734,8.614,-83.605,8.48,-83.544,8.446,-83.452,8.438,-83.377,8.415,-83.292,8.406,-83.29,8.464,-83.298,8.507,-83.422,8.619,-83.47,8.707,-83.391,8.718,-83.286,8.664,-83.162,8.588,-83.13,8.505,-83.123,8.353,-83.041,8.288,-82.947,8.182,-82.879,8.071,-82.854,8.1,-82.866,8.246,-82.781,8.304,-82.68,8.322,-82.531,8.287,-82.365,8.275,-82.235,8.311,-82.224,8.23,-82.16,8.195,-82.097,8.223,-81.973,8.215,-81.86,8.165,-81.728,8.138,-81.694,8.071,-81.676,8.016,-81.504,7.721,-81.37,7.675,-81.268,7.625,-81.219,7.621,-81.195,7.668,-81.179,7.808,-81.158,7.854,-81.094,7.876,-81.064,7.9,-81.035,7.711,-80.915,7.438,-80.901,7.277,-80.846,7.22,-80.667,7.226,-80.439,7.275,-80.373,7.325,-80.348,7.386,-80.287,7.426,-80.111,7.433,-80.067,7.453,-80.011,7.5,-80.04,7.6,-80.075,7.667,-80.261,7.852,-80.366,7.998,-80.409,8.029,-80.458,8.077,-80.466,8.14,-80.459,8.214,-80.408,8.262,-80.369,8.289,-80.2,8.314,-80.126,8.35,-79.75,8.596,-79.816,8.639,-79.759,8.712,-79.731,8.775,-79.687,8.851,-79.572,8.903,-79.552,8.924,-79.507,8.97,-79.442,9.006,-79.247,9.02,-79.086,8.997,-78.955,8.933,-78.848,8.842,-78.77,8.811,-78.71,8.753,-78.67,8.742,-78.621,8.714,-78.514,8.628,-78.469,8.447,-78.436,8.403,-78.41,8.355,-78.379,8.359,-78.369,8.405,-78.388,8.443,-78.399,8.506,-78.374,8.489,-78.35,8.46,-78.256,8.454,-78.251,8.421,-78.223,8.397,-78.191,8.417,-78.162,8.454,-78.099,8.497,-78.057,8.397,-78.012,8.325,-77.853,8.216,-77.761,8.133,-77.834,8.151,-77.952,8.23,-78.048,8.285,-78.114,8.38,-78.142,8.386,-78.18,8.33,-78.281,8.248,-78.255,8.139,-78.287,8.092,-78.315,8.067,-78.368,8.071,-78.422,8.061,-78.378,7.9,-78.17,7.544,-77.93,7.256,-77.901,7.229,-77.804,7.137,-77.681,6.96,-77.646,6.87,-77.602,6.837,-77.526,6.693,-77.439,6.69,-77.369,6.576,-77.36,6.504,-77.398,6.275,-77.44,6.272,-77.473,6.286,-77.469,6.177,-77.345,5.995,-77.249,5.78,-77.325,5.676,-77.534,5.537,-77.402,5.416,-77.373,5.324,-77.359,5.215,-77.367,5.077,-77.339,4.839,-77.307,4.785,-77.286,4.722,-77.314,4.594,-77.328,4.475,-77.354,4.398,-77.414,4.348,-77.446,4.301,-77.516,4.256,-77.521,4.213,-77.409,4.248,-77.404,4.201,-77.434,4.131,-77.427,4.06,-77.358,3.945,-77.278,4.058,-77.248,4.041,-77.264,3.893,-77.212,3.867,-77.167,3.862,-77.127,3.906,-77.077,3.913,-77.243,3.585,-77.324,3.475,-77.357,3.349,-77.417,3.342,-77.472,3.234,-77.52,3.16,-77.559,3.076,-77.632,3.051,-77.694,3.04,-77.701,3.008,-77.671,2.919,-77.67,2.879,-77.777,2.787,-77.808,2.746,-77.814,2.716,-77.875,2.726,-77.901,2.699,-77.932,2.629,-77.987,2.569,-78.03,2.543,-78.067,2.509,-78.12,2.488,-78.296,2.51,-78.343,2.461,-78.417,2.483,-78.46,2.47,-78.535,2.424,-78.592,2.357,-78.617,2.307,-78.629,2.056,-78.55,1.924,-78.577,1.774,-78.793,1.849,-78.958,1.752,-79.025,1.624,-78.888,1.524,-78.86,1.455,-78.827,1.296,-78.9,1.206,-79.229,1.105,-79.465,1.06,-79.613,0.971,-79.741,0.98,-79.796,0.922,-79.904,0.86,-80.036,0.835,-80.088,0.785,-80.061,0.592,-80.025,0.41,-80.046,0.155,-80.133,-0.006,-80.237,-0.113,-80.321,-0.166,-80.482,-0.368,-80.468,-0.436,-80.385,-0.584,-80.282,-0.621,-80.358,-0.625,-80.455,-0.585,-80.505,-0.684,-80.554,-0.848,-80.624,-0.899,-80.841,-0.975,-80.902,-1.079,-80.82,-1.286,-80.801,-1.383,-80.835,-1.632,-80.763,-1.823,-80.761,-1.935,-80.77,-2.077,-80.868,-2.141,-80.963,-2.189,-80.952,-2.235,-80.932,-2.269,-80.839,-2.349,-80.685,-2.397,-80.45,-2.626,-80.379,-2.668,-80.285,-2.707,-80.255,-2.665,-80.249,-2.631,-80.127,-2.528,-80.053,-2.391,-80.007,-2.354,-80.03,-2.557,-79.989,-2.579,-79.926,-2.549,-79.88,-2.424,-79.893,-2.146,-79.842,-2.067,-79.833,-2.111,-79.84,-2.168,-79.822,-2.357,-79.746,-2.485,-79.73,-2.579,-79.823,-2.777,-79.922,-3.09,-79.963,-3.158,-80.027,-3.228,-80.1,-3.274,-80.16,-3.324,-80.303,-3.375,-80.325,-3.388,-80.504,-3.496,-80.653,-3.638,-80.799,-3.731,-80.892,-3.882,-81.232,-4.234,-81.283,-4.322,-81.337,-4.67,-81.289,-4.761,-81.195,-4.879,-81.108,-5.028,-81.151,-5.102,-81.168,-5.167,-80.943,-5.475,-80.882,-5.635,-80.883,-5.759,-80.931,-5.841,-80.992,-5.861,-81.092,-5.812,-81.164,-5.875,-81.181,-5.942,-81.142,-6.057,-81.058,-6.129,-80.812,-6.282,-80.11,-6.65,-79.995,-6.769,-79.905,-6.902,-79.762,-7.067,-79.618,-7.296,-79.589,-7.419,-79.377,-7.836,-79.313,-7.923,-79.164,-8.047,-79.012,-8.21,-78.925,-8.405,-78.762,-8.617,-78.755,-8.74,-78.665,-8.971,-78.58,-9.157,-78.446,-9.371,-78.356,-9.652,-78.276,-9.81,-78.186,-10.089,-78.095,-10.261,-77.736,-10.837,-77.664,-11.022,-77.639,-11.194,-77.633,-11.288,-77.31,-11.532,-77.22,-11.663,-77.158,-11.923,-77.153,-12.06,-77.063,-12.107,-77.038,-12.173,-76.994,-12.219,-76.832,-12.349,-76.758,-12.527,-76.637,-12.728,-76.555,-12.823,-76.502,-12.984,-76.427,-13.11,-76.224,-13.371,-76.184,-13.515,-76.259,-13.803,-76.319,-13.821,-76.376,-13.863,-76.297,-13.948,-76.289,-14.133,-76.175,-14.227,-76.136,-14.32,-76.006,-14.496,-75.934,-14.634,-75.738,-14.785,-75.534,-14.899,-75.397,-15.094,-75.275,-15.178,-75.191,-15.32,-75.104,-15.412,-74.555,-15.699,-74.373,-15.834,-74.147,-15.912,-73.825,-16.153,-73.728,-16.202,-73.4,-16.304,-73.264,-16.389,-72.958,-16.521,-72.794,-16.615,-72.468,-16.708,-72.362,-16.775,-72.269,-16.876,-72.111,-17.003,-71.967,-17.064,-71.868,-17.151,-71.774,-17.199,-71.532,-17.294,-71.436,-17.366,-71.399,-17.422,-71.365,-17.621,-71.337,-17.683,-71.057,-17.876,-70.942,-17.932,-70.817,-18.053,-70.492,-18.278,-70.418,-18.346,-70.362,-18.398,-70.336,-18.595,-70.335,-18.828,-70.276,-19.268,-70.21,-19.487,-70.198,-19.613,-70.157,-19.706,-70.148,-19.805,-70.147,-20.23,-70.194,-20.531,-70.197,-20.725,-70.088,-21.253,-70.08,-21.357,-70.088,-21.493,-70.13,-21.641,-70.155,-21.867,-70.185,-21.975,-70.229,-22.193,-70.26,-22.556,-70.332,-22.849,-70.389,-22.97,-70.45,-23.034,-70.563,-23.057,-70.569,-23.173,-70.593,-23.255,-70.588,-23.368,-70.512,-23.483,-70.42,-23.529,-70.392,-23.566,-70.41,-23.656,-70.488,-23.782,-70.507,-23.886,-70.52,-23.969,-70.507,-24.13,-70.546,-24.332,-70.574,-24.644,-70.559,-24.779,-70.445,-25.173,-70.452,-25.252,-70.49,-25.376,-70.578,-25.487,-70.633,-25.546,-70.714,-25.784,-70.7,-25.861,-70.635,-25.993,-70.662,-26.225,-70.647,-26.329,-70.687,-26.422,-70.708,-26.597,-70.803,-26.841,-70.813,-26.951,-70.898,-27.187,-70.914,-27.308,-70.909,-27.505,-70.926,-27.589,-70.946,-27.618,-71.053,-27.727,-71.087,-27.814,-71.154,-28.064,-71.186,-28.378,-71.267,-28.508,-71.307,-28.672,-71.384,-28.779,-71.494,-28.855,-71.519,-28.926,-71.486,-29.198,-71.353,-29.35,-71.327,-29.443,-71.316,-29.65,-71.348,-29.933,-71.4,-30.143,-71.669,-30.33,-71.709,-30.628,-71.706,-30.759,-71.654,-30.987,-71.662,-31.17,-71.577,-31.496,-71.526,-31.806,-71.513,-32.208,-71.421,-32.387,-71.461,-32.538,-71.452,-32.66,-71.592,-32.97,-71.636,-33.023,-71.743,-33.095,-71.697,-33.289,-71.696,-33.429,-71.636,-33.519,-71.664,-33.653,-71.831,-33.82,-71.854,-33.89,-71.927,-34.016,-72.003,-34.165,-71.992,-34.288,-72.031,-34.421,-72.056,-34.616,-72.182,-34.92,-72.224,-35.096,-72.387,-35.24,-72.455,-35.341,-72.505,-35.447,-72.562,-35.505,-72.624,-35.586,-72.587,-35.76,-72.683,-35.877,-72.778,-35.979,-72.875,-36.39,-72.968,-36.538,-73.007,-36.643,-73.118,-36.688,-73.138,-36.8,-73.151,-36.876,-73.173,-37.054,-73.216,-37.167,-73.271,-37.207,-73.375,-37.224,-73.602,-37.188,-73.634,-37.255,-73.662,-37.341,-73.603,-37.479,-73.665,-37.59,-73.662,-37.699,-73.517,-37.911,-73.465,-38.04,-73.472,-38.13,-73.533,-38.367,-73.52,-38.509,-73.481,-38.624,-73.226,-39.224,-73.25,-39.422,-73.41,-39.789,-73.482,-39.854,-73.671,-39.963,-73.669,-40.082,-73.742,-40.263,-73.784,-40.468,-73.92,-40.872,-73.984,-40.974,-73.966,-41.118,-73.876,-41.319,-73.855,-41.446,-73.811,-41.517,-73.711,-41.574,-73.624,-41.581,-73.625,-41.612,-73.688,-41.639,-73.722,-41.692,-73.735,-41.742,-73.624,-41.774,-73.521,-41.797,-73.242,-41.781,-73.174,-41.747,-73.015,-41.544,-72.953,-41.515,-72.88,-41.518,-72.805,-41.544,-72.67,-41.659,-72.601,-41.684,-72.542,-41.691,-72.428,-41.646,-72.359,-41.514,-72.318,-41.499,-72.36,-41.649,-72.486,-41.722,-72.66,-41.742,-72.744,-41.801,-72.784,-41.847,-72.824,-41.909,-72.781,-41.96,-72.738,-41.995,-72.624,-42.011,-72.499,-41.981,-72.46,-42.207,-72.412,-42.388,-72.43,-42.434,-72.548,-42.256,-72.631,-42.2,-72.707,-42.221,-72.773,-42.258,-72.785,-42.301,-72.716,-42.41,-72.632,-42.51,-72.655,-42.517,-72.774,-42.505,-72.848,-42.669,-72.845,-42.808,-72.766,-42.908,-72.755,-42.993,-72.758,-43.039,-72.878,-43.048,-72.915,-43.134,-72.94,-43.211,-73.076,-43.324,-73.101,-43.455,-72.997,-43.632,-73.069,-43.862,-73.224,-43.898,-73.241,-44.066,-73.265,-44.169,-73.141,-44.237,-73.001,-44.292,-72.828,-44.395,-72.664,-44.436,-72.68,-44.594,-72.739,-44.734,-73.078,-44.92,-73.256,-44.961,-73.362,-44.978,-73.405,-45.102,-73.445,-45.238,-73.226,-45.255,-73.064,-45.36,-72.976,-45.393,-72.941,-45.417,-72.934,-45.452,-72.978,-45.451,-73.202,-45.354,-73.266,-45.346,-73.379,-45.383,-73.55,-45.484,-73.731,-45.48,-73.78,-45.628,-73.757,-45.703,-73.662,-45.731,-73.594,-45.777,-73.592,-45.899,-73.629,-45.987,-73.652,-46.159,-73.668,-46.212,-73.662,-46.297,-73.716,-46.415,-73.77,-46.5,-73.845,-46.566,-73.944,-46.572,-73.949,-46.533,-73.935,-46.501,-73.811,-46.377,-73.708,-46.07,-73.708,-45.967,-73.695,-45.86,-73.735,-45.812,-73.813,-45.818,-73.879,-45.847,-73.929,-46.05,-73.968,-46.154,-74.09,-46.222,-74.213,-46.239,-74.372,-46.246,-74.393,-46.217,-74.357,-46.213,-74.082,-46.132,-74.02,-46.056,-74.061,-45.947,-74,-45.895,-73.96,-45.835,-73.882,-45.569,-73.844,-45.502,-73.825,-45.447,-73.92,-45.408,-73.957,-45.404,-74.038,-45.418,-74.099,-45.46,-74.123,-45.496,-74.099,-45.603,-74.083,-45.645,-74.082,-45.678,-74.096,-45.717,-74.158,-45.767,-74.301,-45.803,-74.369,-45.81,-74.463,-45.841,-74.631,-45.845,-74.763,-45.824,-75.067,-45.875,-75.075,-46.004,-74.998,-46.098,-74.924,-46.16,-75.075,-46.235,-75.247,-46.369,-75.376,-46.429,-75.437,-46.483,-75.657,-46.61,-75.706,-46.705,-75.708,-46.775,-75.635,-46.863,-75.497,-46.94,-75.43,-46.935,-75.401,-46.906,-75.386,-46.863,-75.446,-46.751,-75.528,-46.746,-75.565,-46.729,-75.54,-46.699,-75.478,-46.662,-75.337,-46.647,-75.146,-46.6,-75.019,-46.511,-74.984,-46.512,-75.053,-46.628,-75.031,-46.695,-75.006,-46.741,-74.811,-46.8,-74.691,-46.864,-74.512,-46.885,-74.48,-46.886,-74.467,-46.864,-74.489,-46.835,-74.484,-46.795,-74.454,-46.767,-74.314,-46.788,-74.209,-46.885,-74.152,-46.974,-74.208,-47.083,-74.158,-47.183,-74.216,-47.21,-74.403,-47.328,-74.483,-47.43,-74.324,-47.531,-74.243,-47.56,-74.191,-47.568,-74.134,-47.591,-74.152,-47.627,-74.243,-47.679,-74.323,-47.667,-74.404,-47.6,-74.467,-47.578,-74.534,-47.568,-74.588,-47.618,-74.655,-47.702,-74.609,-47.758,-74.569,-47.773,-74.43,-47.8,-74.376,-47.83,-74.379,-47.891,-74.351,-47.944,-74.227,-47.969,-74.085,-47.955,-73.941,-47.929,-73.847,-47.867,-73.779,-47.738,-73.748,-47.661,-73.716,-47.655,-73.635,-47.88,-73.629,-47.942,-73.61,-47.994,-73.57,-48.019,-73.501,-48.107,-73.391,-48.146,-73.384,-48.177,-73.528,-48.198,-73.854,-48.042,-74.25,-48.045,-74.4,-48.013,-74.585,-47.999,-74.591,-48.162,-74.577,-48.274,-74.499,-48.362,-74.474,-48.464,-74.343,-48.493,-74.27,-48.455,-74.172,-48.427,-74.009,-48.475,-74.057,-48.504,-74.129,-48.504,-74.176,-48.494,-74.228,-48.517,-74.341,-48.596,-74.382,-48.794,-74.38,-49.048,-74.358,-49.351,-74.367,-49.4,-74.349,-49.426,-74.302,-49.464,-74.221,-49.501,-74.185,-49.404,-74.168,-49.321,-74.14,-49.25,-74.074,-49.188,-74.061,-49.111,-74.028,-49.026,-73.935,-49.021,-73.938,-49.046,-73.985,-49.06,-74.015,-49.091,-74.006,-49.158,-74.023,-49.244,-74.049,-49.306,-74.083,-49.362,-74.094,-49.43,-73.988,-49.491,-73.892,-49.523,-73.836,-49.609,-73.892,-49.624,-73.956,-49.593,-74.102,-49.555,-74.23,-49.579,-74.291,-49.604,-74.319,-49.72,-74.324,-49.783,-74.171,-49.907,-74.073,-49.949,-74.011,-49.929,-73.959,-49.995,-74.019,-50.023,-74.334,-49.975,-74.434,-50.065,-74.63,-50.194,-74.516,-50.266,-74.425,-50.35,-74.374,-50.363,-74.306,-50.398,-74.031,-50.47,-73.95,-50.511,-74.186,-50.485,-74.197,-50.61,-74.164,-50.638,-74.097,-50.717,-73.978,-50.827,-73.892,-50.783,-73.75,-50.54,-73.68,-50.49,-73.654,-50.493,-73.693,-50.57,-73.614,-50.628,-73.618,-50.651,-73.659,-50.651,-73.741,-50.697,-73.825,-50.836,-73.807,-50.938,-73.847,-50.94,-74.139,-50.818,-74.156,-50.797,-74.19,-50.778,-74.331,-50.56,-74.366,-50.488,-74.564,-50.382,-74.644,-50.361,-74.722,-50.408,-74.776,-50.47,-74.702,-50.535,-74.649,-50.618,-74.686,-50.662,-74.837,-50.679,-75.095,-50.681,-75.055,-50.786,-74.983,-50.881,-74.815,-51.063,-74.69,-51.087,-74.587,-51.131,-74.508,-51.15,-74.414,-51.162,-74.332,-51.195,-74.21,-51.205,-74.121,-51.195,-73.94,-51.266,-73.896,-51.331,-73.93,-51.618,-74.07,-51.579,-74.197,-51.681,-74.146,-51.712,-73.973,-51.784,-73.894,-51.758,-73.858,-51.79,-73.811,-51.801,-73.753,-51.796,-73.65,-51.856,-73.582,-51.96,-73.518,-52.041,-73.383,-52.07,-73.189,-51.991,-72.928,-51.86,-72.6,-51.799,-72.583,-51.737,-72.649,-51.695,-72.705,-51.678,-72.789,-51.614,-73.115,-51.504,-73.163,-51.496,-73.197,-51.478,-73.169,-51.454,-73.127,-51.44,-72.761,-51.573,-72.543,-51.706,-72.49,-51.764,-72.494,-51.848,-72.523,-51.891,-72.625,-51.946,-72.625,-52.007,-72.614,-52.037,-72.524,-52.17,-72.519,-52.217,-72.523,-52.255,-72.533,-52.282,-72.569,-52.334,-72.631,-52.372,-72.677,-52.385,-72.714,-52.357,-72.694,-52.33,-72.583,-52.254,-72.571,-52.2,-72.588,-52.145,-72.65,-52.1,-72.695,-52.045,-72.695,-51.985,-72.735,-51.961,-72.795,-51.95,-72.843,-51.961,-72.944,-52.047,-73.137,-52.13,-73.26,-52.158,-73.327,-52.166,-73.458,-52.146,-73.532,-52.153,-73.649,-52.078,-73.684,-52.078,-73.685,-52.137,-73.703,-52.199,-73.749,-52.216,-73.834,-52.234,-74.04,-52.159,-74.134,-52.155,-74.195,-52.12,-74.265,-52.105,-74.296,-52.118,-74.266,-52.171,-74.238,-52.202,-74.177,-52.317,-74.151,-52.383,-74.094,-52.376,-74.037,-52.403,-74,-52.513,-74.036,-52.577,-74.014,-52.639,-73.915,-52.688,-73.711,-52.662,-73.586,-52.686,-73.382,-52.595,-73.244,-52.624,-73.178,-52.563,-73.184,-52.488,-73.124,-52.488,-73.073,-52.535,-73.145,-52.602,-73.241,-52.707,-73.346,-52.754,-73.645,-52.837,-73.508,-52.904,-73.46,-52.965,-73.338,-53.055,-73.122,-53.074,-73.055,-53.046,-73.023,-53.022,-73.016,-52.977,-73.02,-52.892,-72.932,-52.782,-72.802,-52.712,-72.766,-52.643,-72.777,-52.577,-72.712,-52.536,-72.645,-52.529,-72.504,-52.56,-72.478,-52.604,-72.438,-52.626,-72.315,-52.539,-72.226,-52.521,-71.812,-52.537,-71.665,-52.56,-71.511,-52.605,-71.554,-52.644,-71.591,-52.661,-71.797,-52.683,-71.979,-52.646,-72.118,-52.65,-72.453,-52.814,-72.627,-52.818,-72.632,-52.774,-72.676,-52.749,-72.728,-52.762,-72.832,-52.82,-72.889,-52.872,-72.91,-52.937,-72.916,-53.122,-72.998,-53.177,-73.053,-53.243,-72.998,-53.291,-72.727,-53.42,-72.549,-53.461,-72.531,-53.372,-72.493,-53.291,-72.458,-53.254,-72.278,-53.132,-72.129,-53.064,-71.898,-53.002,-71.388,-52.764,-71.227,-52.811,-71.155,-52.846,-71.163,-52.888,-71.18,-52.921,-71.289,-53.034,-71.4,-53.107,-71.741,-53.233,-71.791,-53.485,-71.892,-53.524,-71.903,-53.496,-71.867,-53.458,-71.828,-53.398,-71.853,-53.286,-71.942,-53.234,-72.081,-53.25,-72.249,-53.247,-72.306,-53.254,-72.413,-53.35,-72.398,-53.418,-72.377,-53.471,-72.174,-53.632,-72.101,-53.666,-71.872,-53.723,-71.694,-53.803,-71.444,-53.841,-71.298,-53.883,-71.083,-53.825,-70.996,-53.779,-70.948,-53.57,-70.985,-53.448,-70.984,-53.374,-70.952,-53.227,-70.821,-52.963,-70.839,-52.89,-70.795,-52.769,-70.68,-52.712,-70.563,-52.673,-70.391,-52.661,-69.907,-52.514,-69.763,-52.506,-69.62,-52.465,-69.561,-52.422,-69.447,-52.269,-69.241,-52.205,-69.134,-52.211,-69.007,-52.263,-68.443,-52.357,-68.394,-52.307,-68.494,-52.198,-68.691,-52.013,-68.917,-51.715,-68.965,-51.677,-69.033,-51.636,-69.18,-51.662,-69.313,-51.601,-69.409,-51.61,-69.465,-51.584,-69.361,-51.559,-69.218,-51.561,-69.058,-51.547,-69.035,-51.489,-69.03,-51.446,-69.066,-51.304,-69.144,-51.097,-69.201,-50.994,-69.268,-51.006,-69.352,-51.046,-69.359,-51.028,-69.235,-50.951,-69.155,-50.864,-69.141,-50.753,-69.09,-50.583,-69.045,-50.499,-68.939,-50.382,-68.75,-50.281,-68.589,-50.225,-68.465,-50.195,-68.422,-50.158,-68.474,-50.091,-68.533,-50.036,-68.598,-50.009,-68.753,-49.988,-68.98,-50.003,-68.913,-49.969,-68.662,-49.936,-68.638,-49.863,-68.673,-49.793,-68.668,-49.753,-68.569,-49.867,-68.488,-49.978,-68.405,-50.043,-68.257,-50.105,-68.146,-50.091,-67.914,-49.984,-67.826,-49.92,-67.783,-49.859,-67.662,-49.342,-67.694,-49.304,-67.685,-49.247,-67.466,-48.952,-67.263,-48.814,-67.131,-48.688,-67.033,-48.628,-66.783,-48.523,-66.596,-48.42,-66.393,-48.342,-66.017,-48.084,-65.943,-48.019,-65.912,-47.977,-65.81,-47.941,-65.864,-47.853,-65.934,-47.827,-66.097,-47.853,-66.172,-47.858,-66.225,-47.827,-66.041,-47.783,-65.886,-47.702,-65.814,-47.638,-65.775,-47.568,-65.738,-47.345,-65.769,-47.257,-65.854,-47.157,-65.999,-47.094,-66.65,-47.045,-66.777,-47.006,-67.387,-46.554,-67.506,-46.443,-67.563,-46.345,-67.586,-46.27,-67.609,-46.167,-67.6,-46.053,-67.557,-45.97,-67.393,-45.776,-67.258,-45.577,-66.941,-45.257,-66.882,-45.228,-66.585,-45.183,-66.533,-45.158,-66.494,-45.118,-66.348,-45.034,-66.19,-44.965,-65.758,-45.007,-65.639,-45.008,-65.606,-44.945,-65.599,-44.876,-65.698,-44.796,-65.648,-44.661,-65.361,-44.477,-65.29,-44.361,-65.266,-44.28,-65.308,-44.158,-65.239,-44.049,-65.305,-43.787,-65.284,-43.63,-65.252,-43.572,-65.19,-43.522,-64.986,-43.294,-64.84,-43.189,-64.715,-43.136,-64.432,-43.059,-64.376,-43.025,-64.319,-42.969,-64.38,-42.949,-64.442,-42.951,-64.629,-42.909,-65.027,-42.759,-64.971,-42.666,-64.812,-42.633,-64.65,-42.531,-64.488,-42.513,-64.324,-42.572,-64.248,-42.646,-64.22,-42.756,-64.131,-42.861,-64.035,-42.881,-63.692,-42.805,-63.644,-42.746,-63.617,-42.696,-63.594,-42.556,-63.596,-42.407,-63.63,-42.283,-63.685,-42.189,-63.729,-42.153,-63.796,-42.114,-63.893,-42.125,-64.083,-42.183,-64.229,-42.218,-64.253,-42.251,-64.061,-42.266,-64.062,-42.353,-64.101,-42.395,-64.265,-42.422,-64.42,-42.434,-64.571,-42.416,-64.574,-42.356,-64.524,-42.299,-64.512,-42.27,-64.538,-42.255,-64.622,-42.261,-64.7,-42.221,-64.898,-42.162,-64.986,-42.102,-65.059,-41.97,-65.007,-41.745,-65.018,-41.567,-65.128,-41.239,-65.155,-41.106,-65.152,-40.947,-65.133,-40.881,-65.069,-40.805,-64.917,-40.731,-64.869,-40.736,-64.804,-40.757,-64.82,-40.793,-64.853,-40.814,-64.621,-40.854,-64.383,-40.922,-64.123,-41.008,-63.773,-41.15,-63.622,-41.16,-63.213,-41.152,-62.959,-41.11,-62.798,-41.047,-62.395,-40.891,-62.302,-40.815,-62.246,-40.675,-62.394,-40.459,-62.427,-40.356,-62.402,-40.197,-62.324,-39.951,-62.287,-39.895,-62.254,-39.88,-62.132,-39.825,-62.083,-39.568,-62.077,-39.462,-62.131,-39.432,-62.179,-39.38,-62.054,-39.374,-62.126,-39.31,-62.209,-39.262,-62.295,-39.243,-62.338,-39.151,-62.304,-38.988,-62.374,-38.853,-62.335,-38.8,-62.189,-38.813,-62.067,-38.919,-61.848,-38.962,-61.603,-38.999,-61.383,-38.981,-61.112,-38.993,-60.904,-38.974,-59.828,-38.838,-59.676,-38.797,-59.007,-38.673,-58.179,-38.436,-57.646,-38.17,-57.547,-38.086,-57.507,-37.909,-57.396,-37.745,-57.088,-37.446,-56.727,-36.958,-56.672,-36.851,-56.668,-36.735,-56.698,-36.426,-56.717,-36.389,-56.749,-36.346,-56.937,-36.353,-57.076,-36.297,-57.265,-36.144,-57.335,-36.027,-57.375,-35.9,-57.354,-35.72,-57.159,-35.506,-57.171,-35.362,-57.304,-35.188,-57.548,-35.019,-57.764,-34.895,-58.283,-34.683,-58.419,-34.532,-58.466,-34.457,-58.525,-34.296,-58.475,-34.263,-58.435,-34.253,-58.392,-34.193,-58.409,-34.061,-58.429,-33.991,-58.457,-33.898,-58.531,-33.753,-58.547,-33.663,-58.455,-33.286,-58.424,-33.112,-58.376,-33.072,-58.309,-33.083,-58.25,-33.078,-58.201,-33.015,-58.171,-32.959,-58.22,-32.564,-58.201,-32.472,-58.162,-32.567,-58.13,-32.757,-58.082,-32.894,-58.093,-32.967,-58.154,-33.065,-58.222,-33.129,-58.292,-33.138,-58.364,-33.182,-58.353,-33.26,-58.411,-33.509,-58.438,-33.719,-58.4,-33.912,-58.207,-34.109,-57.961,-34.307,-57.902,-34.39,-57.873,-34.448,-57.829,-34.477,-57.543,-34.448,-57.171,-34.452,-56.855,-34.677,-56.463,-34.775,-56.388,-34.861,-56.25,-34.901,-56.195,-34.906,-56.118,-34.908,-55.863,-34.811,-55.673,-34.776,-55.371,-34.808,-55.238,-34.896,-55.095,-34.895,-54.902,-34.933,-54.365,-34.733,-54.272,-34.667,-54.169,-34.671,-54.01,-34.517,-53.785,-34.38,-53.743,-34.25,-53.535,-34.017,-53.472,-33.849,-53.42,-33.779,-53.371,-33.742,-52.921,-33.402,-52.763,-33.266,-52.652,-33.138,-52.508,-32.875,-52.342,-32.44,-52.275,-32.324,-52.19,-32.221,-52.127,-32.168,-52.167,-32.088,-52.192,-31.968,-52.194,-31.886,-52.12,-31.695,-52.027,-31.599,-51.995,-31.49,-51.972,-31.384,-51.927,-31.339,-51.717,-31.244,-51.506,-31.104,-51.464,-31.053,-51.485,-30.978,-51.459,-30.913,-51.376,-30.847,-51.359,-30.675,-51.316,-30.703,-51.283,-30.752,-51.288,-30.591,-51.247,-30.468,-51.188,-30.412,-51.157,-30.364,-51.282,-30.244,-51.295,-30.141,-51.298,-30.035,-51.25,-30.06,-51.234,-30.121,-51.179,-30.211,-51.04,-30.261,-51.025,-30.369,-50.932,-30.374,-50.646,-30.237,-50.564,-30.254,-50.547,-30.317,-50.582,-30.439,-50.615,-30.457,-50.685,-30.413,-50.716,-30.426,-50.689,-30.704,-50.77,-30.813,-50.941,-30.904,-50.965,-31.005,-50.954,-31.052,-50.98,-31.094,-51.106,-31.081,-51.161,-31.119,-51.158,-31.267,-51.174,-31.34,-51.272,-31.477,-51.446,-31.557,-51.681,-31.775,-51.803,-31.797,-51.841,-31.832,-51.893,-31.868,-51.995,-31.815,-52.063,-31.83,-52.06,-31.913,-52.043,-31.978,-52.069,-32.063,-52.039,-32.115,-51.92,-31.99,-51.798,-31.9,-51.46,-31.702,-51.152,-31.48,-50.921,-31.258,-50.748,-31.068,-50.62,-30.898,-50.3,-30.426,-50.033,-29.801,-49.746,-29.363,-49.5,-29.075,-49.271,-28.871,-49.024,-28.699,-48.8,-28.575,-48.797,-28.443,-48.693,-28.31,-48.648,-28.207,-48.621,-28.076,-48.606,-27.825,-48.643,-27.558,-48.572,-27.373,-48.596,-27.264,-48.554,-27.196,-48.568,-27.123,-48.593,-27.058,-48.616,-26.878,-48.678,-26.703,-48.677,-26.612,-48.658,-26.519,-48.652,-26.406,-48.701,-26.348,-48.748,-26.269,-48.714,-26.227,-48.679,-26.226,-48.619,-26.179,-48.576,-25.935,-48.613,-25.875,-48.679,-25.875,-48.666,-25.844,-48.545,-25.816,-48.401,-25.597,-48.43,-25.55,-48.507,-25.521,-48.692,-25.492,-48.732,-25.369,-48.644,-25.437,-48.564,-25.447,-48.476,-25.443,-48.428,-25.403,-48.458,-25.311,-48.402,-25.272,-48.273,-25.306,-48.186,-25.31,-48.242,-25.403,-48.203,-25.417,-48.024,-25.237,-47.929,-25.168,-47.908,-25.068,-47.959,-25.065,-47.989,-25.036,-47.914,-25,-47.877,-24.997,-47.831,-24.953,-47.592,-24.781,-47.137,-24.493,-46.867,-24.236,-46.631,-24.11,-45.972,-23.796,-45.843,-23.764,-45.665,-23.765,-45.527,-23.805,-45.464,-23.803,-45.433,-23.758,-45.423,-23.685,-45.325,-23.6,-45.215,-23.576,-44.952,-23.381,-44.667,-23.335,-44.619,-23.316,-44.57,-23.274,-44.621,-23.229,-44.674,-23.207,-44.681,-23.107,-44.637,-23.055,-44.368,-23.005,-44.148,-23.011,-44.047,-22.945,-43.866,-22.911,-43.703,-22.966,-43.676,-23.009,-43.791,-23.046,-43.899,-23.035,-43.974,-23.057,-43.899,-23.101,-43.737,-23.067,-43.533,-23.046,-43.369,-22.998,-43.224,-22.991,-43.194,-22.939,-43.209,-22.878,-43.237,-22.829,-43.242,-22.795,-43.229,-22.748,-43.154,-22.725,-43.086,-22.723,-43.065,-22.771,-43.101,-22.85,-43.081,-22.903,-43.016,-22.943,-42.958,-22.967,-42.829,-22.973,-42.581,-22.941,-42.122,-22.941,-42.042,-22.947,-41.987,-22.845,-41.941,-22.788,-41.986,-22.736,-41.998,-22.645,-41.98,-22.581,-41.706,-22.31,-41.583,-22.244,-41.123,-22.084,-41,-21.999,-40.988,-21.92,-41.022,-21.611,-41.023,-21.597,-41.047,-21.506,-40.955,-21.238,-40.829,-21.031,-40.789,-20.906,-40.727,-20.846,-40.597,-20.784,-40.396,-20.569,-40.319,-20.426,-40.299,-20.293,-40.203,-20.206,-40.142,-19.968,-40.001,-19.742,-39.845,-19.649,-39.783,-19.572,-39.731,-19.454,-39.7,-19.278,-39.742,-18.846,-39.74,-18.64,-39.651,-18.252,-39.487,-17.99,-39.413,-17.92,-39.278,-17.849,-39.154,-17.704,-39.171,-17.642,-39.215,-17.316,-39.203,-17.178,-39.164,-17.044,-39.125,-16.764,-39.063,-16.504,-38.961,-16.187,-38.881,-15.864,-38.885,-15.842,-38.943,-15.564,-38.996,-15.254,-39.013,-14.936,-39.06,-14.655,-38.942,-14.031,-38.967,-14.003,-39.008,-14.101,-39.048,-14.044,-39.035,-13.991,-39.041,-13.758,-39.001,-13.665,-38.989,-13.615,-39.009,-13.581,-39.035,-13.559,-39.089,-13.588,-39.067,-13.48,-39.031,-13.365,-38.959,-13.273,-38.835,-13.147,-38.833,-13.033,-38.764,-12.907,-38.784,-12.844,-38.852,-12.79,-38.788,-12.783,-38.744,-12.749,-38.691,-12.624,-38.654,-12.645,-38.525,-12.762,-38.499,-12.957,-38.447,-12.967,-38.402,-12.966,-38.24,-12.844,-38.019,-12.591,-37.957,-12.475,-37.689,-12.1,-37.469,-11.654,-37.412,-11.497,-37.438,-11.394,-37.359,-11.253,-37.322,-11.215,-37.321,-11.267,-37.332,-11.31,-37.355,-11.35,-37.356,-11.404,-37.315,-11.376,-37.181,-11.187,-37.183,-11.068,-37.125,-11.085,-37.093,-11.055,-36.938,-10.82,-36.768,-10.672,-36.636,-10.59,-36.412,-10.49,-36.398,-10.484,-36.224,-10.225,-36.055,-10.076,-35.885,-9.848,-35.848,-9.772,-35.891,-9.687,-35.83,-9.719,-35.764,-9.703,-35.597,-9.541,-35.341,-9.231,-35.158,-8.931,-34.967,-8.408,-34.891,-8.092,-34.835,-7.971,-34.837,-7.872,-34.879,-7.747,-34.873,-7.692,-34.855,-7.634,-34.861,-7.595,-34.858,-7.533,-34.817,-7.395,-34.805,-7.288,-34.834,-7.024,-34.876,-7.003,-34.88,-6.908,-34.93,-6.785,-34.988,-6.394,-35.095,-6.185,-35.142,-5.917,-35.235,-5.567,-35.393,-5.251,-35.482,-5.166,-35.549,-5.129,-35.98,-5.054,-36.162,-5.094,-36.387,-5.084,-36.591,-5.098,-36.747,-5.051,-36.861,-4.967,-36.955,-4.937,-37.175,-4.912,-37.301,-4.713,-37.626,-4.592,-37.796,-4.404,-38.049,-4.216,-38.272,-3.948,-38.362,-3.876,-38.476,-3.717,-38.686,-3.654,-38.896,-3.502,-39.014,-3.39,-39.353,-3.197,-39.511,-3.126,-39.609,-3.056,-39.772,-2.986,-39.965,-2.862,-40.235,-2.813,-40.475,-2.796,-40.876,-2.87,-41.195,-2.886,-41.318,-2.936,-41.48,-2.917,-41.64,-2.879,-41.722,-2.809,-41.876,-2.747,-42,-2.806,-42.25,-2.792,-42.594,-2.661,-42.676,-2.59,-42.832,-2.53,-42.937,-2.465,-43.23,-2.386,-43.38,-2.376,-43.435,-2.414,-43.455,-2.502,-43.729,-2.518,-43.864,-2.595,-43.933,-2.583,-44.013,-2.642,-44.193,-2.81,-44.225,-2.755,-44.192,-2.7,-44.113,-2.599,-44.101,-2.56,-44.106,-2.493,-44.179,-2.471,-44.229,-2.471,-44.308,-2.535,-44.381,-2.738,-44.438,-2.944,-44.623,-3.138,-44.723,-3.205,-44.721,-3.142,-44.639,-2.762,-44.611,-2.677,-44.589,-2.573,-44.562,-2.524,-44.521,-2.481,-44.52,-2.405,-44.382,-2.366,-44.391,-2.27,-44.435,-2.168,-44.52,-2.19,-44.579,-2.23,-44.662,-2.373,-44.701,-2.32,-44.756,-2.266,-44.708,-2.241,-44.659,-2.228,-44.617,-2.152,-44.58,-2.114,-44.538,-2.053,-44.547,-1.946,-44.592,-1.842,-44.651,-1.746,-44.721,-1.792,-44.779,-1.799,-44.721,-1.733,-44.79,-1.725,-44.828,-1.672,-44.92,-1.589,-45.026,-1.513,-45.076,-1.466,-45.182,-1.507,-45.239,-1.629,-45.282,-1.697,-45.329,-1.717,-45.353,-1.567,-45.459,-1.356,-45.557,-1.331,-45.645,-1.348,-45.779,-1.251,-45.972,-1.187,-46.045,-1.103,-46.14,-1.118,-46.215,-1.1,-46.219,-1.031,-46.321,-1.039,-46.422,-1.03,-46.516,-0.997,-46.617,-0.971,-46.644,-0.916,-46.77,-0.837,-46.811,-0.78,-46.894,-0.78,-46.944,-0.743,-47.025,-0.75,-47.127,-0.745,-47.201,-0.68,-47.269,-0.645,-47.398,-0.627,-47.439,-0.648,-47.46,-0.681,-47.433,-0.722,-47.419,-0.766,-47.471,-0.749,-47.557,-0.67,-47.651,-0.719,-47.687,-0.725,-47.731,-0.71,-47.774,-0.677,-47.808,-0.663,-47.883,-0.693,-47.961,-0.77,-48.033,-0.705,-48.069,-0.714,-48.115,-0.737,-48.128,-0.795,-48.202,-0.828,-48.266,-0.895,-48.318,-0.961,-48.306,-1.04,-48.45,-1.146,-48.409,-1.229,-48.478,-1.324,-48.468,-1.394,-48.451,-1.436,-48.35,-1.482,-48.446,-1.52,-48.463,-1.614,-48.53,-1.567,-48.6,-1.489,-48.71,-1.488,-48.991,-1.83,-49.155,-1.879,-49.211,-1.917,-49.408,-2.344,-49.458,-2.505,-49.524,-2.597,-49.576,-2.631,-49.637,-2.657,-49.599,-2.584,-49.553,-2.52,-49.507,-2.28,-49.46,-2.192,-49.399,-1.972,-49.314,-1.732,-49.585,-1.867,-49.72,-1.926,-49.903,-1.871,-49.999,-1.832,-50.117,-1.858,-50.173,-1.896,-50.26,-1.923,-50.403,-2.016,-50.586,-1.85,-50.639,-1.817,-50.69,-1.762,-50.675,-1.695,-50.679,-1.644,-50.786,-1.49,-50.819,-1.376,-50.826,-1.311,-50.845,-1.226,-50.897,-1.164,-50.918,-1.115,-50.838,-1.039,-50.842,-1,-50.895,-0.938,-50.992,-0.986,-51.029,-1.032,-51.202,-1.137,-51.297,-1.224,-51.531,-1.354,-51.646,-1.394,-51.948,-1.587,-52.197,-1.64,-52.31,-1.56,-52.664,-1.552,-52.553,-1.514,-52.229,-1.362,-52.02,-1.399,-51.981,-1.368,-51.934,-1.32,-51.922,-1.181,-51.819,-1.118,-51.721,-1.018,-51.722,-0.855,-51.703,-0.762,-51.555,-0.549,-51.496,-0.509,-51.404,-0.393,-51.3,-0.179,-51.283,-0.085,-51.102,-0.031,-50.967,0.13,-50.91,0.161,-50.816,0.173,-50.755,0.223,-50.582,0.421,-50.463,0.637,-50.343,0.751,-50.294,0.836,-50.071,1.015,-50.047,1.052,-49.938,1.121,-49.899,1.163,-49.906,1.269,-49.882,1.42,-49.957,1.66,-50.055,1.731,-50.188,1.786,-50.304,1.798,-50.459,1.83,-50.534,1.927,-50.576,1.999,-50.609,2.104,-50.659,2.131,-50.714,2.134,-50.677,2.179,-50.679,2.21,-50.737,2.377,-50.79,2.478,-50.817,2.573,-50.827,2.652,-50.994,3.078,-51.052,3.282,-51.076,3.672,-51.22,4.094,-51.327,4.225,-51.462,4.314,-51.547,4.311,-51.558,4.234,-51.653,4.061,-51.658,4.098,-51.653,4.139,-51.666,4.229,-51.699,4.287,-51.786,4.571,-51.828,4.636,-51.88,4.634,-51.92,4.524,-51.928,4.436,-51.955,4.399,-52.003,4.352,-52.002,4.386,-51.979,4.43,-51.962,4.514,-52.012,4.646,-52.058,4.717,-52.22,4.863,-52.325,4.771,-52.289,4.876,-52.291,4.942,-52.454,5.021,-52.765,5.273,-52.899,5.425,-53.27,5.543,-53.454,5.563,-53.847,5.782,-53.92,5.769,-53.99,5.676,-54.085,5.412,-54.156,5.359,-54.08,5.502,-54.046,5.609,-54.037,5.721,-54.054,5.808,-54.142,5.856,-54.356,5.91,-54.834,5.988,-55.148,5.993,-55.379,5.953,-55.648,5.986,-55.828,5.962,-55.91,5.893,-55.896,5.795,-55.898,5.699,-55.94,5.795,-56.236,5.885,-56.466,5.938,-56.97,5.993,-57.057,5.939,-57.105,5.829,-57.136,5.737,-57.141,5.644,-57.182,5.529,-57.195,5.548,-57.205,5.565,-57.167,5.885,-57.19,6.097,-57.228,6.178,-57.344,6.272,-57.54,6.332,-57.608,6.45,-57.793,6.599,-57.983,6.786,-58.072,6.821,-58.173,6.829,-58.298,6.879,-58.415,6.851,-58.502,6.734,-58.569,6.627,-58.594,6.452,-58.673,6.391,-58.613,6.503,-58.608,6.697,-58.583,6.844,-58.481,7.038,-58.477,7.326,-58.511,7.398,-58.627,7.546,-58.701,7.607,-58.812,7.736,-59.2,8.075,-59.477,8.254,-59.666,8.363,-59.739,8.38,-59.74,8.339,-59.757,8.34,-59.837,8.374,-59.981,8.533,-60.018,8.549,-60.167,8.617,-60.34,8.629,-60.404,8.61,-60.481,8.547,-60.801,8.592,-60.865,8.579,-61.036,8.493,-61.194,8.488,-61.304,8.41,-61.443,8.509,-61.527,8.546,-61.619,8.597,-61.247,8.6,-61.176,8.725,-61.122,8.843,-61.099,8.941,-61.093,8.966,-61.054,9.035,-61.053,9.095,-61.023,9.155,-60.971,9.215,-60.841,9.264,-60.792,9.361,-60.874,9.453,-61.013,9.556,-61.234,9.598,-61.309,9.633,-61.512,9.848,-61.589,9.895,-61.625,9.816,-61.766,9.814,-61.759,9.754,-61.732,9.702,-61.736,9.631,-61.759,9.677,-61.805,9.706,-61.831,9.733,-61.837,9.782,-61.909,9.87,-62.017,9.955,-62.077,9.975,-62.12,9.985,-62.155,9.979,-62.147,9.953,-62.17,9.879,-62.153,9.822,-62.172,9.827,-62.19,9.842,-62.221,9.883,-62.257,9.819,-62.281,9.793,-62.3,9.788,-62.32,9.783,-62.401,9.918,-62.515,10.176,-62.55,10.2,-62.6,10.217,-62.608,10.163,-62.6,10.117,-62.651,10.071,-62.741,10.056,-62.695,10.1,-62.662,10.199,-62.686,10.29,-62.706,10.333,-62.781,10.399,-62.813,10.4,-62.843,10.418,-62.843,10.507,-62.914,10.531,-62.694,10.563,-62.38,10.547,-62.233,10.634,-62.04,10.645,-61.921,10.681,-61.879,10.741,-62.242,10.7,-62.702,10.75,-62.947,10.707,-63.035,10.72,-63.19,10.709,-63.497,10.643,-63.873,10.664,-64.202,10.633,-64.298,10.635,-64.248,10.543,-64.158,10.579,-63.863,10.558,-63.732,10.503,-63.779,10.472,-63.834,10.449,-64.188,10.458,-64.85,10.098,-64.944,10.095,-65.023,10.077,-65.129,10.07,-65.317,10.122,-65.489,10.159,-65.656,10.228,-65.852,10.258,-66.09,10.473,-66.092,10.517,-66.106,10.575,-66.247,10.632,-66.989,10.611,-67.133,10.57,-67.581,10.524,-67.872,10.472,-68.14,10.493,-68.234,10.569,-68.296,10.689,-68.325,10.809,-68.272,10.88,-68.325,10.949,-68.343,11.053,-68.399,11.161,-68.616,11.309,-68.828,11.432,-69.055,11.461,-69.233,11.518,-69.526,11.5,-69.57,11.485,-69.632,11.48,-69.712,11.564,-69.762,11.676,-69.811,11.837,-69.831,11.996,-69.86,12.054,-69.914,12.115,-70.004,12.178,-70.122,12.137,-70.203,12.098,-70.245,12.004,-70.287,11.886,-70.22,11.73,-70.22,11.681,-70.193,11.625,-69.911,11.672,-69.817,11.672,-69.773,11.541,-69.805,11.474,-69.885,11.444,-70.049,11.53,-70.097,11.52,-70.16,11.428,-70.233,11.373,-70.546,11.261,-70.821,11.208,-71.264,11,-71.47,10.964,-71.461,10.836,-71.545,10.779,-71.518,10.622,-71.494,10.533,-71.463,10.469,-71.387,10.264,-71.262,10.144,-71.207,10.015,-71.082,9.833,-71.053,9.706,-71.078,9.511,-71.086,9.348,-71.205,9.222,-71.241,9.16,-71.298,9.126,-71.537,9.048,-71.62,9.048,-71.687,9.073,-71.74,9.134,-71.781,9.25,-71.761,9.336,-71.806,9.386,-71.873,9.428,-71.976,9.553,-71.993,9.642,-72.113,9.816,-71.956,10.108,-71.885,10.167,-71.794,10.316,-71.665,10.444,-71.594,10.657,-71.598,10.726,-71.69,10.835,-71.731,10.995,-71.676,10.997,-71.642,11.014,-71.791,11.135,-71.835,11.19,-71.947,11.414,-71.957,11.483,-71.957,11.57,-71.908,11.608,-71.869,11.627,-71.488,11.719,-71.415,11.755,-71.349,11.815,-71.32,11.862,-71.284,11.918,-71.137,12.046,-71.155,12.164,-71.262,12.335,-71.494,12.432,-71.597,12.434,-71.715,12.42,-71.919,12.309,-71.931,12.27,-71.97,12.238,-72.055,12.238,-72.136,12.189,-72.165,12.06,-72.275,11.889,-72.447,11.802,-72.722,11.712,-73.313,11.296,-73.677,11.271,-73.796,11.276,-73.91,11.309,-74.059,11.341,-74.143,11.321,-74.2,11.266,-74.219,11.105,-74.3,10.952,-74.35,10.814,-74.401,10.765,-74.46,10.787,-74.516,10.863,-74.492,10.934,-74.41,10.967,-74.352,10.975,-74.33,10.997,-74.454,10.989,-74.845,11.11,-74.922,11.058,-75.123,10.87,-75.248,10.783,-75.281,10.727,-75.446,10.611,-75.493,10.528,-75.554,10.328,-75.671,10.196,-75.708,10.143,-75.642,10.172,-75.558,10.236,-75.539,10.205,-75.596,10.126,-75.593,9.993,-75.637,9.834,-75.68,9.73,-75.635,9.658,-75.604,9.538,-75.639,9.45,-75.756,9.416,-75.905,9.431,-76.027,9.366,-76.135,9.266,-76.277,8.989,-76.689,8.695,-76.802,8.641,-76.888,8.62,-76.92,8.574,-76.872,8.513,-76.819,8.465,-76.772,8.311,-76.742,8.002,-76.787,7.932,-76.867,7.918,-76.897,7.939,-76.925,7.973,-76.912,8.033,-76.869,8.063,-76.852,8.09,-76.891,8.128,-76.936,8.147,-76.992,8.25,-77.13,8.401,-77.262,8.494,-77.344,8.637,-77.374,8.658,-77.697,8.889,-77.831,9.068,-78.083,9.236,-78.504,9.406,-78.697,9.435,-78.932,9.428,-78.975,9.453,-79.017,9.51,-79.112,9.537,-79.212,9.532,-79.355,9.569,-79.577,9.598,-79.652,9.558,-79.723,9.479,-79.855,9.378,-79.915,9.361,-79.978,9.344,-80.127,9.21,-80.547,9.082,-80.676,9.022,-80.839,8.887,-81.063,8.813,-81.204,8.787,-81.355,8.781,-81.546,8.827,-81.712,9.019,-81.803,9.074,-81.842,9.119,-81.894,9.14,-81.9,9.111,-81.831,9.046,-81.78,8.957,-81.826,8.944,-81.894,8.956,-82.078,8.935,-82.133,8.98,-82.244,9.031,-82.235,9.142,-82.201,9.168,-82.188,9.192,-82.205,9.215,-82.272,9.191,-82.34,9.209,-82.375,9.337,-82.363,9.382,-82.371,9.429,-82.5,9.523,-82.564,9.577,-82.61,9.616,-82.778,9.67,-82.81,9.735,-82.866,9.771,-83.029,9.991,-83.125,10.042,-83.347,10.315,-83.448,10.466,-83.575,10.735,-83.588,10.815,-83.617,10.877,-83.642,10.917,-83.714,10.934,-83.768,11.01,-83.832,11.131,-83.868,11.3,-83.859,11.354,-83.829,11.428,-83.777,11.504,-83.745,11.567,-83.652,11.642,-83.664,11.724,-83.705,11.825,-83.753,11.821,-83.793,11.836,-83.829,11.861,-83.813,11.896,-83.769,11.932,-83.773,11.977,-83.767,12.059,-83.716,12.057,-83.698,12.03,-83.68,12.024,-83.669,12.228,-83.651,12.287,-83.667,12.337,-83.716,12.407,-83.754,12.502,-83.718,12.553,-83.682,12.568,-83.625,12.613,-83.593,12.713,-83.578,12.667,-83.591,12.579,-83.624,12.515,-83.627,12.459,-83.596,12.396,-83.565,12.393,-83.511,12.412,-83.518,12.514,-83.541,12.596,-83.514,12.944,-83.567,13.32,-83.494,13.739,-83.412,13.996,-83.347,14.057,-83.281,14.154,-83.212,14.267,-83.188,14.34,-83.299,14.749,-83.341,14.765,-83.375,14.766,-83.414,14.825,-83.389,14.871,-83.344,14.902,-83.306,14.891,-83.302,14.802,-83.28,14.813,-83.216,14.932,-83.185,14.956,-83.158,14.993,-83.226,15.042,-83.291,15.079,-83.369,15.24,-83.646,15.368,-83.676,15.365,-83.551,15.294,-83.498,15.222,-83.536,15.219,-83.59,15.266,-83.672,15.261,-83.716,15.219,-83.76,15.22,-83.802,15.289,-83.871,15.353,-83.927,15.394,-84.013,15.414,-84.048,15.398,-84.095,15.401,-84.105,15.43,-84.111,15.492,-84.083,15.511,-83.973,15.52,-83.765,15.405,-83.775,15.437,-84.261,15.823,-84.52,15.873,-84.49,15.847,-84.426,15.829,-84.44,15.813,-84.492,15.794,-84.56,15.802,-84.646,15.884,-84.974,15.99,-85.048,15.974,-85.164,15.918,-85.484,15.9,-85.784,16.003,-85.986,16.024,-85.954,16.002,-85.936,15.953,-86.069,15.906,-86.181,15.885,-86.357,15.783,-86.481,15.801,-86.757,15.794,-86.907,15.762,-87.286,15.834,-87.377,15.826,-87.487,15.79,-87.545,15.832,-87.618,15.91,-87.702,15.911,-87.875,15.879,-87.907,15.863,-88.01,15.786,-88.055,15.765,-88.131,15.701,-88.228,15.729,-88.594,15.95,-88.598,15.927,-88.572,15.901,-88.536,15.85,-88.603,15.764,-88.709,15.807,-88.798,15.863,-88.84,15.869,-88.894,15.891,-88.912,15.956,-88.879,16.017,-88.695,16.248,-88.562,16.29,-88.461,16.434,-88.405,16.489,-88.313,16.633,-88.262,16.963,-88.294,17.192,-88.289,17.313,-88.267,17.393,-88.203,17.517,-88.272,17.61,-88.221,17.751,-88.207,17.846,-88.097,18.122,-88.085,18.226,-88.13,18.351,-88.247,18.355,-88.296,18.344,-88.349,18.359,-88.296,18.472,-88.276,18.515,-88.195,18.643,-88.197,18.72,-88.127,18.773,-88.074,18.834,-88.032,18.839,-88.011,18.727,-88.056,18.524,-88.039,18.484,-87.96,18.441,-87.882,18.274,-87.853,18.269,-87.804,18.357,-87.762,18.446,-87.734,18.655,-87.653,18.799,-87.594,19.046,-87.501,19.288,-87.509,19.317,-87.551,19.321,-87.622,19.25,-87.656,19.258,-87.659,19.352,-87.628,19.383,-87.567,19.416,-87.513,19.426,-87.483,19.444,-87.435,19.502,-87.425,19.583,-87.469,19.586,-87.512,19.575,-87.587,19.573,-87.645,19.554,-87.69,19.594,-87.688,19.637,-87.586,19.779,-87.507,19.827,-87.466,19.824,-87.442,19.862,-87.432,19.898,-87.466,19.999,-87.467,20.102,-87.421,20.231,-87.221,20.507,-87.06,20.631,-86.926,20.786,-86.865,20.885,-86.816,21.005,-86.772,21.151,-86.804,21.2,-86.817,21.234,-86.824,21.422,-86.912,21.463,-87.035,21.592,-87.128,21.621,-87.216,21.582,-87.276,21.572,-87.369,21.574,-87.387,21.551,-87.296,21.525,-87.249,21.527,-87.211,21.544,-87.188,21.546,-87.164,21.514,-87.188,21.477,-87.218,21.458,-87.251,21.447,-87.48,21.472,-87.689,21.536,-87.774,21.55,-88.007,21.604,-88.132,21.616,-88.171,21.604,-88.172,21.591,-88.185,21.579,-88.251,21.567,-88.467,21.569,-88.585,21.539,-88.747,21.448,-88.879,21.414,-89.82,21.275,-89.888,21.253,-90.183,21.121,-90.353,21.009,-90.435,20.758,-90.458,20.714,-90.484,20.556,-90.478,20.38,-90.486,20.224,-90.482,20.026,-90.492,19.947,-90.507,19.912,-90.65,19.796,-90.693,19.73,-90.739,19.352,-90.955,19.152,-91.059,19.098,-91.136,19.038,-91.437,18.89,-91.458,18.865,-91.469,18.833,-91.446,18.833,-91.343,18.901,-91.334,18.877,-91.368,18.806,-91.356,18.777,-91.308,18.773,-91.279,18.721,-91.275,18.624,-91.44,18.542,-91.534,18.457,-91.6,18.447,-91.803,18.471,-91.914,18.529,-91.943,18.563,-91.88,18.6,-91.88,18.638,-91.974,18.716,-92.103,18.704,-92.213,18.685,-92.441,18.675,-92.485,18.665,-92.71,18.612,-92.729,18.575,-92.769,18.524,-92.885,18.469,-93.127,18.423,-93.228,18.444,-93.552,18.43,-93.764,18.358,-93.873,18.304,-94.189,18.195,-94.392,18.166,-94.46,18.167,-94.546,18.175,-94.682,18.348,-94.798,18.515,-95.015,18.571,-95.182,18.701,-95.561,18.719,-95.697,18.775,-95.72,18.768,-95.655,18.724,-95.578,18.69,-95.627,18.691,-95.821,18.755,-95.92,18.82,-95.928,18.85,-95.81,18.804,-95.778,18.806,-95.913,18.897,-95.985,19.054,-96.073,19.106,-96.124,19.199,-96.29,19.344,-96.315,19.473,-96.368,19.567,-96.456,19.87,-96.709,20.188,-97.121,20.615,-97.186,20.717,-97.195,20.8,-97.357,21.104,-97.501,21.398,-97.501,21.432,-97.515,21.478,-97.567,21.508,-97.598,21.536,-97.638,21.604,-97.754,22.027,-97.59,21.762,-97.457,21.612,-97.383,21.567,-97.385,21.524,-97.424,21.465,-97.434,21.356,-97.409,21.273,-97.388,21.374,-97.337,21.438,-97.315,21.564,-97.36,21.615,-97.485,21.705,-97.585,21.809,-97.763,22.106,-97.782,22.279,-97.842,22.51,-97.842,22.557,-97.858,22.625,-97.817,22.776,-97.758,22.886,-97.745,22.942,-97.766,23.306,-97.727,23.732,-97.743,23.761,-97.729,23.788,-97.717,23.981,-97.668,24.39,-97.507,25.015,-97.424,25.233,-97.225,25.585,-97.164,25.755,-97.146,25.961,-97.14,26.03,-97.15,26.065,-97.214,26.068,-97.402,26.397,-97.435,26.486,-97.466,26.692,-97.494,26.76,-97.527,26.908,-97.555,26.967,-97.517,27.053,-97.476,27.118,-97.475,27.173,-97.485,27.237,-97.692,27.287,-97.768,27.458,-97.682,27.395,-97.524,27.314,-97.48,27.317,-97.439,27.328,-97.38,27.419,-97.289,27.671,-97.431,27.837,-97.404,27.859,-97.374,27.87,-97.252,27.854,-97.171,27.88,-97.073,27.986,-97.034,28.094,-97.141,28.061,-97.155,28.103,-97.156,28.144,-97.096,28.158,-97.015,28.163,-96.967,28.19,-96.933,28.224,-96.92,28.185,-96.892,28.158,-96.84,28.194,-96.807,28.22,-96.795,28.321,-96.774,28.422,-96.676,28.341,-96.562,28.367,-96.489,28.406,-96.421,28.457,-96.475,28.479,-96.525,28.489,-96.64,28.709,-96.608,28.723,-96.576,28.716,-96.56,28.684,-96.526,28.648,-96.449,28.594,-96.374,28.631,-96.373,28.657,-96.275,28.655,-96.115,28.622,-96.011,28.632,-96.132,28.561,-96.235,28.489,-96.181,28.502,-96.02,28.587,-95.853,28.64,-95.732,28.712,-95.656,28.745,-95.388,28.898,-95.273,28.964,-95.152,29.079,-95.139,29.168,-95.018,29.259,-94.888,29.371,-94.936,29.46,-94.993,29.531,-95.023,29.702,-94.982,29.713,-94.93,29.68,-94.89,29.677,-94.832,29.753,-94.742,29.75,-94.724,29.655,-94.778,29.548,-94.733,29.535,-94.605,29.568,-94.526,29.548,-94.75,29.418,-94.76,29.384,-94.574,29.485,-94.1,29.67,-93.89,29.689,-93.886,29.723,-93.946,29.815,-93.841,29.98,-93.794,29.977,-93.769,29.952,-93.773,29.914,-93.809,29.851,-93.848,29.819,-93.884,29.81,-93.866,29.756,-93.826,29.725,-93.766,29.753,-93.695,29.77,-93.388,29.777,-93.283,29.789,-93.176,29.779,-92.952,29.714,-92.791,29.635,-92.671,29.597,-92.261,29.557,-92.084,29.593,-92.059,29.617,-92.114,29.668,-92.135,29.699,-92.08,29.761,-92.017,29.8,-91.893,29.836,-91.824,29.751,-91.672,29.746,-91.565,29.605,-91.514,29.555,-91.331,29.514,-91.278,29.563,-91.249,29.564,-91.26,29.505,-91.244,29.457,-91.155,29.351,-91.151,29.318,-91.237,29.331,-91.283,29.321,-91.29,29.289,-91.003,29.194,-90.751,29.131,-90.677,29.151,-90.586,29.272,-90.502,29.3,-90.379,29.295,-90.302,29.256,-90.247,29.131,-90.213,29.105,-90.136,29.136,-90.101,29.182,-90.083,29.24,-90.074,29.297,-90.053,29.337,-90.052,29.431,-90.101,29.463,-90.141,29.48,-90.161,29.504,-90.159,29.537,-89.877,29.458,-89.818,29.416,-89.797,29.381,-89.792,29.333,-89.717,29.313,-89.672,29.317,-89.62,29.302,-89.577,29.268,-89.522,29.249,-89.443,29.194,-89.389,29.105,-89.354,29.07,-89.376,28.981,-89.331,28.999,-89.236,29.081,-89.195,29.054,-89.156,29.017,-89.133,29.046,-89.11,29.099,-89.021,29.143,-89.016,29.203,-89.065,29.218,-89.117,29.248,-89.181,29.336,-89.246,29.333,-89.514,29.42,-89.58,29.486,-89.675,29.539,-89.721,29.619,-89.689,29.646,-89.683,29.675,-89.662,29.684,-89.621,29.674,-89.559,29.698,-89.591,29.725,-89.531,29.772,-89.455,29.784,-89.354,29.82,-89.363,29.84,-89.358,29.921,-89.401,29.978,-89.414,30.011,-89.401,30.046,-89.494,30.058,-89.563,30.002,-89.59,29.915,-89.632,29.904,-89.744,29.93,-89.815,30.007,-89.777,30.046,-89.715,30.078,-89.665,30.117,-89.668,30.145,-89.737,30.172,-89.773,30.137,-89.812,30.124,-89.894,30.126,-89.994,30.059,-90.175,30.029,-90.285,30.065,-90.413,30.14,-90.332,30.278,-90.225,30.379,-90.126,30.369,-90.045,30.351,-89.954,30.269,-89.588,30.166,-89.521,30.193,-89.444,30.223,-89.321,30.345,-89.264,30.344,-89.224,30.332,-89.054,30.368,-88.905,30.415,-88.873,30.416,-88.82,30.406,-88.692,30.355,-88.4,30.371,-88.35,30.373,-88.249,30.363,-88.135,30.367,-88.117,30.415,-88.078,30.566,-88.032,30.681,-88.011,30.694,-87.949,30.627,-87.923,30.562,-87.924,30.45,-87.898,30.414,-87.857,30.407,-87.813,30.347,-87.79,30.292,-87.904,30.259,-87.985,30.254,-88.006,30.231,-87.622,30.265,-87.513,30.368,-87.49,30.378,-87.448,30.394,-87.444,30.364,-87.501,30.309,-87.476,30.294,-87.281,30.339,-87.251,30.397,-87.185,30.454,-87.171,30.539,-87.119,30.539,-87.072,30.5,-87.034,30.554,-86.998,30.57,-86.965,30.502,-86.986,30.431,-87.124,30.397,-87.164,30.374,-87.201,30.339,-86.968,30.372,-86.68,30.403,-86.606,30.425,-86.523,30.467,-86.448,30.496,-86.374,30.482,-86.257,30.493,-86.166,30.464,-86.138,30.442,-86.124,30.406,-86.24,30.429,-86.454,30.399,-86.175,30.333,-85.856,30.214,-85.791,30.172,-85.756,30.167,-85.743,30.201,-85.741,30.244,-85.676,30.279,-85.604,30.287,-85.641,30.237,-85.663,30.189,-85.61,30.148,-85.623,30.117,-85.676,30.122,-85.504,29.976,-85.354,29.876,-85.307,29.798,-85.315,29.758,-85.336,29.74,-85.383,29.785,-85.414,29.842,-85.414,29.768,-85.376,29.695,-85.319,29.68,-85.186,29.708,-85.029,29.721,-84.969,29.745,-84.889,29.778,-84.801,29.773,-84.55,29.898,-84.454,29.91,-84.383,29.907,-84.359,29.929,-84.375,29.982,-84.356,30.029,-84.31,30.065,-84.044,30.104,-83.694,29.926,-83.29,29.452,-82.769,29.052,-82.651,28.888,-82.644,28.812,-82.651,28.77,-82.661,28.486,-82.749,28.237,-82.844,27.846,-82.808,27.777,-82.775,27.734,-82.743,27.709,-82.715,27.733,-82.661,27.718,-82.626,27.746,-82.611,27.777,-82.597,27.873,-82.634,27.898,-82.675,27.964,-82.636,27.981,-82.58,27.958,-82.521,27.878,-82.498,27.868,-82.446,27.903,-82.406,27.863,-82.401,27.835,-82.431,27.771,-82.521,27.678,-82.636,27.525,-82.687,27.515,-82.715,27.5,-82.655,27.449,-82.62,27.401,-82.441,27.06,-82.354,26.936,-82.29,26.871,-82.243,26.849,-82.181,26.84,-82.169,26.874,-82.181,26.937,-82.096,26.963,-82.013,26.962,-82.067,26.892,-82.078,26.704,-82.04,26.552,-82.006,26.54,-81.97,26.552,-81.921,26.631,-81.882,26.665,-81.829,26.687,-81.896,26.597,-81.959,26.49,-81.931,26.467,-81.867,26.435,-81.811,26.146,-81.715,25.983,-81.568,25.892,-81.365,25.831,-81.345,25.732,-81.227,25.583,-81.113,25.367,-81.057,25.338,-80.98,25.312,-80.94,25.264,-80.965,25.224,-81.098,25.319,-81.136,25.31,-81.159,25.269,-81.167,25.229,-81.11,25.138,-81.012,25.133,-80.862,25.176,-80.737,25.156,-80.558,25.232,-80.485,25.23,-80.367,25.331,-80.328,25.427,-80.301,25.619,-80.219,25.742,-80.159,25.878,-80.143,25.874,-80.136,25.843,-80.126,25.833,-80.111,26.132,-80.041,26.569,-80.05,26.808,-80.089,26.994,-80.126,27.083,-80.226,27.207,-80.65,28.181,-80.686,28.272,-80.749,28.381,-80.787,28.561,-80.818,28.636,-80.838,28.758,-80.809,28.759,-80.771,28.732,-80.78,28.683,-80.766,28.633,-80.7,28.601,-80.688,28.579,-80.729,28.516,-80.732,28.463,-80.694,28.345,-80.665,28.375,-80.654,28.452,-80.633,28.518,-80.607,28.523,-80.623,28.32,-80.61,28.178,-80.5,27.934,-80.457,27.901,-80.533,28.07,-80.573,28.181,-80.585,28.272,-80.581,28.365,-80.568,28.426,-80.524,28.486,-80.564,28.556,-80.9,29.05,-81.105,29.457,-81.25,29.794,-81.337,30.141,-81.386,30.27,-81.457,30.641,-81.504,30.731,-81.516,30.802,-81.52,30.875,-81.501,30.914,-81.471,31.009,-81.453,31.088,-81.46,31.127,-81.442,31.2,-81.413,31.179,-81.365,31.172,-81.288,31.264,-81.329,31.314,-81.378,31.332,-81.381,31.353,-81.295,31.371,-81.258,31.436,-81.219,31.472,-81.175,31.531,-81.196,31.539,-81.223,31.528,-81.259,31.539,-81.242,31.574,-81.17,31.61,-81.166,31.646,-81.187,31.667,-81.198,31.704,-81.162,31.744,-81.098,31.753,-81.066,31.788,-81.065,31.813,-81.096,31.841,-81.113,31.879,-81.083,31.894,-81.046,31.892,-80.923,31.945,-80.872,32.03,-80.882,32.069,-80.849,32.114,-80.791,32.126,-80.758,32.142,-80.694,32.216,-80.702,32.246,-80.734,32.265,-80.765,32.298,-80.798,32.363,-80.803,32.448,-80.709,32.337,-80.683,32.349,-80.678,32.381,-80.647,32.396,-80.626,32.326,-80.608,32.293,-80.579,32.287,-80.514,32.324,-80.486,32.352,-80.474,32.423,-80.53,32.475,-80.634,32.512,-80.572,32.534,-80.461,32.521,-80.363,32.501,-80.268,32.537,-80.23,32.577,-80.18,32.593,-80.123,32.589,-80.022,32.62,-79.941,32.667,-79.894,32.729,-79.933,32.81,-79.805,32.787,-79.735,32.825,-79.615,32.909,-79.587,33.001,-79.499,33.027,-79.42,33.043,-79.276,33.135,-79.229,33.185,-79.281,33.315,-79.226,33.405,-79.227,33.363,-79.238,33.312,-79.194,33.244,-79.138,33.406,-78.92,33.659,-78.841,33.724,-78.578,33.873,-78.564,33.877,-78.406,33.918,-78.013,33.912,-77.971,33.993,-77.953,34.169,-77.926,34.073,-77.933,33.989,-77.928,33.94,-77.888,34.05,-77.861,34.149,-77.751,34.285,-77.697,34.332,-77.65,34.358,-77.518,34.451,-77.38,34.527,-77.402,34.555,-77.413,34.592,-77.412,34.731,-77.384,34.694,-77.358,34.62,-77.296,34.603,-77.252,34.616,-77.134,34.708,-77.05,34.697,-76.896,34.701,-76.797,34.704,-76.733,34.707,-76.707,34.752,-76.618,34.77,-76.517,34.777,-76.44,34.843,-76.362,34.937,-76.457,34.989,-76.745,34.941,-76.899,34.97,-76.975,35.025,-77.07,35.155,-76.861,35.005,-76.779,34.99,-76.628,35.073,-76.613,35.104,-76.608,35.153,-76.566,35.215,-76.513,35.27,-76.553,35.306,-76.595,35.33,-76.974,35.458,-77.04,35.527,-76.887,35.463,-76.741,35.431,-76.634,35.453,-76.611,35.53,-76.577,35.532,-76.532,35.508,-76.516,35.436,-76.49,35.397,-76.447,35.408,-76.39,35.401,-76.275,35.369,-76.174,35.354,-76.104,35.38,-75.966,35.508,-75.774,35.647,-75.745,35.765,-75.759,35.843,-75.772,35.9,-75.812,35.956,-75.854,35.96,-75.979,35.896,-76.001,35.722,-76.046,35.691,-76.084,35.691,-76.076,35.788,-76.06,35.879,-76.07,35.97,-76.207,35.991,-76.264,35.967,-76.358,35.953,-76.504,35.956,-76.611,35.944,-76.726,35.958,-76.719,36.033,-76.74,36.133,-76.734,36.229,-76.718,36.148,-76.679,36.075,-76.559,36.015,-76.479,36.028,-76.424,36.068,-76.384,36.134,-76.321,36.138,-76.227,36.116,-76.271,36.19,-76.222,36.167,-76.15,36.146,-76.141,36.215,-76.148,36.279,-76.055,36.235,-75.95,36.209,-75.883,36.176,-75.82,36.113,-75.867,36.268,-75.925,36.383,-75.978,36.429,-75.993,36.474,-75.966,36.551,-75.96,36.571,-75.973,36.6,-75.965,36.638,-75.946,36.659,-75.918,36.633,-75.894,36.567,-75.889,36.55,-75.81,36.271,-75.728,36.104,-75.58,35.872,-75.534,35.819,-75.559,35.879,-75.758,36.229,-75.857,36.551,-75.89,36.657,-75.942,36.766,-75.966,36.862,-75.999,36.913,-76.144,36.931,-76.244,36.953,-76.4,36.89,-76.488,36.897,-76.505,36.961,-76.634,37.047,-76.672,37.173,-76.765,37.184,-76.925,37.225,-77.002,37.271,-77.196,37.296,-77.227,37.309,-77.251,37.329,-77.007,37.318,-76.704,37.218,-76.631,37.222,-76.602,37.143,-76.507,37.072,-76.462,37.031,-76.401,36.991,-76.338,37.013,-76.283,37.053,-76.301,37.111,-76.327,37.149,-76.401,37.213,-76.497,37.247,-76.611,37.323,-76.738,37.449,-76.756,37.479,-76.758,37.505,-76.538,37.309,-76.454,37.274,-76.393,37.3,-76.405,37.332,-76.401,37.386,-76.263,37.357,-76.254,37.431,-76.269,37.495,-76.368,37.53,-76.306,37.571,-76.484,37.629,-76.549,37.669,-76.62,37.755,-76.715,37.81,-76.849,37.94,-76.925,38.033,-77.111,38.166,-77.071,38.167,-76.94,38.095,-76.829,37.962,-76.793,37.938,-76.492,37.682,-76.437,37.67,-76.344,37.676,-76.306,37.722,-76.293,37.794,-76.262,37.848,-76.264,37.894,-76.355,37.963,-76.472,38.011,-76.55,38.094,-76.645,38.134,-76.906,38.197,-77.047,38.357,-77.11,38.37,-77.232,38.34,-77.273,38.352,-77.314,38.397,-77.284,38.529,-77.26,38.6,-77.165,38.677,-77.092,38.72,-77.046,38.776,-77.03,38.889,-77.018,38.778,-77.054,38.706,-77.135,38.65,-77.221,38.541,-77.242,38.495,-77.233,38.408,-77.156,38.397,-77.077,38.442,-77.001,38.445,-76.988,38.394,-76.95,38.347,-76.89,38.292,-76.868,38.337,-76.868,38.39,-76.769,38.263,-76.594,38.228,-76.454,38.174,-76.402,38.125,-76.341,38.087,-76.333,38.141,-76.366,38.197,-76.409,38.268,-76.642,38.454,-76.669,38.538,-76.677,38.612,-76.659,38.58,-76.647,38.539,-76.572,38.436,-76.51,38.404,-76.439,38.362,-76.394,38.369,-76.416,38.42,-76.458,38.475,-76.501,38.532,-76.537,38.743,-76.521,38.788,-76.516,38.841,-76.52,38.898,-76.494,38.945,-76.519,39.001,-76.559,39.065,-76.546,39.068,-76.473,39.031,-76.42,39.074,-76.428,39.126,-76.489,39.159,-76.574,39.254,-76.57,39.269,-76.421,39.225,-76.403,39.253,-76.406,39.304,-76.359,39.325,-76.345,39.365,-76.347,39.388,-76.331,39.404,-76.276,39.323,-76.257,39.352,-76.248,39.439,-76.223,39.42,-76.216,39.38,-76.141,39.403,-76.097,39.433,-76.081,39.47,-76.085,39.527,-76.063,39.561,-76.006,39.569,-75.959,39.585,-75.97,39.505,-75.873,39.511,-75.913,39.468,-75.955,39.46,-76.003,39.411,-75.939,39.399,-75.876,39.376,-75.976,39.367,-76.074,39.369,-76.153,39.315,-76.236,39.192,-76.217,39.064,-76.133,39.123,-76.135,39.082,-76.186,38.991,-76.245,39.009,-76.313,39.009,-76.33,38.953,-76.331,38.909,-76.241,38.943,-76.191,38.916,-76.168,38.853,-76.247,38.823,-76.3,38.818,-76.341,38.71,-76.308,38.723,-76.278,38.772,-76.213,38.758,-76.175,38.707,-76.057,38.621,-76.017,38.625,-76.001,38.602,-76.113,38.602,-76.198,38.619,-76.264,38.6,-76.295,38.495,-76.265,38.436,-76.212,38.361,-76.117,38.318,-76.051,38.28,-76.02,38.295,-76.007,38.323,-75.986,38.332,-75.967,38.291,-75.937,38.31,-75.889,38.356,-75.859,38.362,-75.877,38.319,-75.864,38.261,-75.885,38.214,-75.928,38.169,-75.891,38.147,-75.856,38.14,-75.795,38.087,-75.829,38.033,-75.851,37.972,-75.735,37.974,-75.659,37.954,-75.719,37.821,-75.792,37.756,-75.888,37.619,-75.975,37.398,-75.997,37.264,-75.985,37.212,-75.934,37.152,-75.854,37.297,-75.812,37.425,-75.767,37.473,-75.699,37.516,-75.632,37.535,-75.587,37.559,-75.596,37.631,-75.376,38.025,-75.354,38.065,-75.292,38.129,-75.225,38.242,-75.16,38.255,-75.142,38.298,-75.134,38.384,-75.117,38.406,-75.09,38.425,-75.073,38.41,-75.074,38.366,-75.051,38.383,-75.039,38.426,-75.038,38.456,-75.036,38.503,-75.073,38.579,-75.111,38.599,-75.187,38.591,-75.128,38.632,-75.084,38.723,-75.089,38.778,-75.185,38.819,-75.31,38.967,-75.392,39.093,-75.413,39.281,-75.52,39.403,-75.574,39.477,-75.567,39.553,-75.582,39.589,-75.588,39.641,-75.502,39.717,-75.464,39.781,-75.421,39.815,-75.401,39.832,-75.321,39.865,-75.173,39.895,-75.074,39.983,-75.104,39.932,-75.154,39.871,-75.353,39.83,-75.422,39.79,-75.472,39.712,-75.524,39.602,-75.519,39.532,-75.524,39.49,-75.353,39.34,-75.231,39.284,-75.136,39.208,-75.05,39.211,-74.975,39.188,-74.897,39.145,-74.92,39.047,-74.954,38.95,-74.923,38.941,-74.794,39.002,-74.646,39.208,-74.605,39.248,-74.603,39.293,-74.579,39.316,-74.517,39.347,-74.474,39.343,-74.429,39.387,-74.411,39.455,-74.39,39.487,-74.407,39.549,-74.331,39.536,-74.257,39.614,-74.176,39.727,-74.118,39.938,-74.096,39.976,-74.065,39.993,-74.084,39.829,-74.08,39.788,-74.049,39.923,-74.028,40.073,-74.004,40.171,-73.972,40.251,-73.958,40.328,-73.972,40.4,-73.998,40.452,-74.05,40.43,-74.242,40.456,-74.264,40.529,-74.227,40.608,-74.187,40.648,-74.153,40.673,-74.116,40.687,-74.067,40.72,-74.025,40.756,-73.927,40.914,-73.91,40.992,-73.909,40.996,-73.918,41.136,-73.97,41.25,-73.925,41.218,-73.882,41.171,-73.872,41.055,-73.907,40.912,-73.949,40.839,-73.987,40.751,-73.947,40.777,-73.911,40.816,-73.851,40.831,-73.779,40.878,-73.671,40.966,-73.63,40.992,-73.583,41.022,-73.182,41.176,-73.024,41.216,-72.925,41.285,-72.847,41.266,-72.479,41.276,-72.371,41.312,-72.265,41.292,-72.074,41.326,-71.93,41.341,-71.842,41.335,-71.769,41.331,-71.523,41.379,-71.444,41.454,-71.427,41.633,-71.364,41.703,-71.39,41.795,-71.359,41.786,-71.331,41.762,-71.311,41.72,-71.271,41.681,-71.234,41.707,-71.178,41.744,-71.149,41.746,-71.204,41.641,-71.188,41.516,-71.169,41.489,-71.08,41.538,-70.974,41.549,-70.701,41.715,-70.666,41.71,-70.655,41.608,-70.668,41.558,-70.657,41.534,-70.481,41.582,-70.405,41.627,-70.06,41.677,-69.987,41.684,-69.949,41.677,-69.934,41.71,-69.942,41.808,-69.978,41.961,-70.109,42.078,-70.16,42.097,-70.204,42.101,-70.241,42.091,-70.237,42.071,-70.196,42.035,-70.173,42.063,-70.11,42.03,-70.09,41.98,-70.006,41.872,-70.001,41.826,-70.135,41.77,-70.295,41.729,-70.427,41.757,-70.515,41.803,-70.549,41.939,-70.656,41.987,-70.645,42.022,-70.618,42.04,-70.738,42.229,-70.818,42.265,-70.997,42.3,-71.046,42.331,-70.93,42.432,-70.871,42.497,-70.831,42.553,-70.752,42.57,-70.661,42.617,-70.613,42.623,-70.604,42.65,-70.624,42.672,-70.655,42.674,-70.697,42.665,-70.736,42.669,-70.781,42.721,-70.8,42.774,-70.829,42.825,-70.806,42.877,-70.778,42.941,-70.733,43.07,-70.691,43.109,-70.642,43.134,-70.521,43.349,-70.36,43.48,-70.203,43.626,-70.238,43.656,-70.269,43.672,-70.179,43.766,-70.062,43.835,-69.965,43.855,-69.975,43.818,-69.974,43.788,-69.926,43.797,-69.873,43.82,-69.84,43.79,-69.808,43.772,-69.792,43.805,-69.803,43.867,-69.795,43.911,-69.772,43.899,-69.762,43.861,-69.73,43.852,-69.699,43.955,-69.653,43.994,-69.637,43.949,-69.624,43.881,-69.59,43.887,-69.557,43.983,-69.542,43.963,-69.521,43.897,-69.481,43.905,-69.435,43.956,-69.345,44.001,-69.226,43.986,-69.137,44.038,-69.068,44.098,-69.064,44.172,-68.956,44.348,-68.961,44.434,-68.847,44.485,-68.8,44.549,-68.763,44.571,-68.766,44.51,-68.795,44.454,-68.777,44.446,-68.736,44.454,-68.71,44.443,-68.794,44.382,-68.812,44.339,-68.723,44.342,-68.612,44.311,-68.572,44.271,-68.533,44.259,-68.514,44.304,-68.521,44.38,-68.479,44.446,-68.451,44.508,-68.417,44.469,-68.374,44.445,-68.317,44.474,-68.277,44.507,-68.246,44.515,-68.198,44.515,-68.152,44.502,-68.117,44.491,-68.094,44.439,-68.057,44.384,-68.014,44.401,-67.985,44.42,-67.963,44.464,-67.907,44.454,-67.839,44.576,-67.79,44.586,-67.727,44.567,-67.653,44.562,-67.599,44.577,-67.556,44.645,-67.458,44.657,-67.364,44.697,-67.191,44.676,-66.987,44.828,-66.991,44.85,-67.014,44.868,-67.107,44.885,-67.114,44.944,-67.08,44.989,-67.102,45.088,-67.13,45.139,-67.125,45.169,-67.084,45.144,-66.977,45.157,-66.919,45.146,-66.908,45.098,-66.872,45.067,-66.707,45.083,-66.511,45.143,-66.44,45.096,-66.352,45.133,-66.252,45.189,-66.144,45.228,-66.107,45.257,-66.183,45.335,-66.09,45.376,-66.065,45.401,-66.027,45.418,-66.067,45.359,-66.11,45.317,-65.956,45.222,-65.884,45.223,-65.545,45.337,-65.282,45.473,-65.057,45.544,-64.898,45.626,-64.779,45.638,-64.594,45.814,-64.642,45.913,-64.633,45.947,-64.536,45.867,-64.482,45.806,-64.404,45.827,-64.315,45.836,-64.351,45.783,-64.397,45.756,-64.56,45.625,-64.827,45.476,-64.913,45.375,-64.873,45.355,-64.832,45.35,-64.747,45.324,-64.681,45.383,-64.6,45.41,-64.336,45.39,-64.087,45.411,-63.906,45.378,-63.614,45.394,-63.368,45.365,-63.46,45.321,-63.748,45.311,-64.093,45.217,-64.183,45.147,-64.135,45.023,-64.235,45.114,-64.354,45.138,-64.366,45.187,-64.359,45.238,-64.34,45.268,-64.331,45.309,-64.448,45.337,-64.407,45.306,-64.449,45.256,-64.751,45.18,-64.903,45.121,-65.657,44.76,-65.692,44.738,-65.728,44.697,-65.587,44.729,-65.502,44.76,-65.52,44.733,-65.616,44.68,-65.682,44.651,-65.778,44.646,-65.917,44.615,-66.022,44.562,-66.091,44.505,-66.125,44.47,-66.146,44.436,-65.942,44.576,-65.868,44.569,-66.1,44.367,-66.193,44.144,-66.193,44.08,-66.126,43.814,-66.038,43.742,-66.002,43.778,-65.978,43.815,-65.887,43.795,-65.835,43.734,-65.738,43.561,-65.662,43.534,-65.564,43.553,-65.482,43.518,-65.45,43.524,-65.429,43.561,-65.386,43.565,-65.344,43.55,-65.33,43.668,-65.235,43.727,-65.172,43.731,-65.087,43.727,-64.862,43.868,-64.826,43.929,-64.692,44.021,-64.578,44.142,-64.469,44.185,-64.378,44.304,-64.335,44.292,-64.276,44.334,-64.312,44.415,-64.339,44.445,-64.286,44.55,-64.167,44.587,-64.101,44.487,-64.045,44.545,-64.045,44.588,-64,44.645,-63.924,44.604,-63.891,44.546,-63.821,44.511,-63.761,44.486,-63.61,44.48,-63.568,44.514,-63.545,44.544,-63.558,44.611,-63.604,44.683,-63.544,44.655,-63.457,44.64,-63.381,44.652,-63.306,44.643,-63.156,44.711,-63.089,44.709,-63.032,44.715,-62.768,44.785,-62.514,44.844,-62.265,44.936,-62.027,44.994,-61.794,45.084,-61.719,45.094,-61.647,45.131,-61.569,45.154,-61.498,45.157,-61.387,45.185,-61.284,45.235,-61.165,45.256,-61.101,45.233,-61.068,45.253,-61.032,45.292,-61.071,45.33,-61.107,45.349,-61.461,45.367,-61.376,45.411,-61.282,45.441,-61.277,45.476,-61.35,45.574,-61.428,45.648,-61.492,45.687,-61.657,45.642,-61.777,45.656,-61.877,45.714,-61.912,45.799,-61.924,45.851,-61.956,45.868,-62.218,45.731,-62.422,45.665,-62.447,45.641,-62.483,45.622,-62.586,45.661,-62.75,45.648,-62.718,45.686,-62.701,45.741,-62.911,45.776,-63.108,45.782,-63.217,45.758,-63.293,45.752,-63.316,45.78,-63.358,45.811,-63.509,45.875,-63.568,45.878,-63.703,45.858,-63.875,45.959,-64.031,46.013,-64.056,46.021,-63.832,46.107,-63.873,46.146,-63.916,46.166,-64.145,46.193,-64.212,46.22,-64.542,46.24,-64.557,46.311,-64.648,46.356,-64.641,46.426,-64.69,46.512,-64.726,46.671,-64.817,46.699,-64.883,46.823,-64.906,46.888,-64.866,46.958,-64.831,47.061,-64.942,47.086,-65.042,47.089,-65.192,47.05,-65.26,47.069,-65.319,47.101,-65.086,47.234,-64.912,47.369,-64.852,47.57,-64.766,47.673,-64.703,47.725,-64.874,47.797,-65.046,47.793,-65.002,47.847,-65.228,47.811,-65.344,47.768,-65.483,47.687,-65.607,47.67,-65.666,47.696,-65.756,47.86,-65.849,47.911,-66.21,47.989,-66.36,48.061,-66.429,48.067,-66.632,48.011,-66.704,48.022,-66.449,48.12,-66.324,48.098,-66.249,48.117,-66.083,48.103,-66.013,48.147,-65.927,48.189,-65.755,48.112,-65.476,48.031,-65.36,48.011,-65.259,48.021,-65.036,48.106,-64.96,48.16,-64.822,48.196,-64.765,48.228,-64.706,48.311,-64.633,48.36,-64.349,48.423,-64.254,48.55,-64.246,48.691,-64.415,48.804,-64.514,48.841,-64.371,48.839,-64.209,48.806,-64.216,48.874,-64.262,48.922,-64.568,49.105,-64.836,49.192,-65.396,49.262,-65.523,49.266,-65.883,49.226,-66.178,49.213,-66.598,49.126,-67.117,48.964,-67.561,48.856,-67.889,48.731,-68.238,48.626,-68.431,48.542,-68.552,48.457,-68.746,48.376,-68.816,48.366,-68.987,48.275,-69.306,48.047,-69.471,47.967,-69.581,47.824,-69.802,47.623,-70.017,47.471,-70.07,47.378,-70.218,47.29,-70.388,47.117,-70.519,47.033,-70.993,46.852,-71.152,46.819,-71.261,46.756,-71.439,46.721,-71.671,46.654,-71.901,46.632,-72.109,46.551,-72.187,46.512,-72.24,46.442,-72.366,46.405,-72.496,46.353,-72.733,46.182,-72.99,46.104,-73.16,46.01,-73.253,45.864,-73.369,45.758,-73.465,45.632,-73.484,45.587,-73.519,45.459,-73.558,45.425,-73.765,45.395,-74.05,45.241,-74.269,45.188,-74.566,45.042,-74.709,45.004,-74.358,45.206,-74.34,45.214,-74.098,45.324,-73.974,45.345,-74,45.433,-74.248,45.493,-74.315,45.531,-74.038,45.502,-73.897,45.564,-73.798,45.655,-73.712,45.711,-73.477,45.738,-73.284,45.9,-73.18,46.025,-73.145,46.066,-73.022,46.12,-72.981,46.21,-72.843,46.262,-72.68,46.287,-72.257,46.485,-72.205,46.559,-72.028,46.607,-71.88,46.687,-71.757,46.674,-71.625,46.698,-71.268,46.796,-71.116,46.925,-70.973,47.007,-70.706,47.14,-70.448,47.423,-70.3,47.503,-69.994,47.74,-69.906,47.832,-69.84,47.953,-69.775,48.098,-69.866,48.172,-69.971,48.206,-70.145,48.244,-70.501,48.354,-70.671,48.353,-70.839,48.367,-70.923,48.422,-71.018,48.456,-70.384,48.367,-70.111,48.278,-70.001,48.271,-69.852,48.207,-69.762,48.191,-69.674,48.199,-69.55,48.251,-69.375,48.386,-69.231,48.574,-68.929,48.829,-68.669,48.94,-68.628,49.007,-68.544,49.056,-68.414,49.1,-68.295,49.114,-68.221,49.15,-68.282,49.197,-68.056,49.257,-67.549,49.332,-67.469,49.335,-67.372,49.348,-67.262,49.451,-67.234,49.602,-66.941,49.994,-66.741,50.066,-66.622,50.155,-66.55,50.161,-66.496,50.212,-66.411,50.224,-66.369,50.207,-66.242,50.22,-66.126,50.201,-65.955,50.294,-65.762,50.259,-65.269,50.32,-65.181,50.298,-64.868,50.275,-64.509,50.309,-64.17,50.269,-64.016,50.304,-63.854,50.314,-63.734,50.305,-63.587,50.258,-63.239,50.243,-63.136,50.294,-62.95,50.291,-62.83,50.301,-62.715,50.302,-62.541,50.285,-62.362,50.277,-62.165,50.239,-61.92,50.233,-61.835,50.197,-61.725,50.104,-61.29,50.202,-61.181,50.192,-60.956,50.205,-60.807,50.25,-60.608,50.221,-60.438,50.239,-60.08,50.255,-59.886,50.316,-59.815,50.418,-59.612,50.492,-59.378,50.675,-59.165,50.78,-59.055,50.879,-58.638,51.172,-58.615,51.237,-58.593,51.257,-58.51,51.295,-58.442,51.306,-58.27,51.295,-58.089,51.311,-58.023,51.322,-57.854,51.4,-57.77,51.426,-57.462,51.469,-57.299,51.478,-57.1,51.443,-57.096,51.443,-57.018,51.447,-56.976,51.458,-56.549,51.681,-56.283,51.797,-56.017,51.929,-55.695,52.138,-55.673,52.19,-55.691,52.242,-55.783,52.28,-55.834,52.31,-56.005,52.37,-56.012,52.394,-55.897,52.37,-55.777,52.364,-55.716,52.392,-55.706,52.428,-55.746,52.475,-55.84,52.508,-56.053,52.537,-56.228,52.536,-56.325,52.545,-56.292,52.574,-56.167,52.575,-55.848,52.623,-55.803,52.643,-55.819,52.677,-55.873,52.736,-55.858,52.823,-55.83,52.878,-55.892,53,-55.808,53.135,-55.798,53.212,-55.817,53.246,-55.855,53.286,-55.863,53.312,-55.859,53.344,-55.911,53.391,-55.966,53.471,-56.11,53.588,-56.27,53.6,-56.354,53.624,-56.444,53.718,-56.465,53.765,-56.524,53.766,-56.697,53.758,-56.841,53.739,-57.012,53.673,-57.221,53.529,-57.332,53.469,-57.386,53.561,-57.42,53.583,-57.527,53.6,-57.524,53.611,-57.489,53.633,-57.244,53.715,-57.157,53.757,-57.135,53.792,-57.149,53.848,-57.199,53.924,-57.416,54.163,-57.615,54.191,-58.192,54.228,-58.31,54.202,-58.356,54.172,-58.361,54.154,-58.317,54.114,-58.177,54.131,-58.065,54.127,-57.928,54.104,-57.936,54.091,-58.088,54.09,-58.327,54.052,-58.652,53.978,-58.92,53.875,-59.129,53.744,-59.322,53.644,-59.498,53.575,-59.621,53.537,-59.829,53.505,-59.882,53.48,-59.987,53.393,-60.148,53.307,-60.329,53.266,-60.337,53.277,-60.346,53.289,-60.273,53.317,-60.251,53.344,-60.306,53.36,-60.29,53.391,-60.157,53.45,-60.1,53.487,-60.16,53.53,-60.37,53.607,-60.395,53.653,-60.263,53.61,-60.145,53.596,-60.117,53.61,-60.1,53.634,-60.081,53.701,-60.057,53.733,-60.014,53.762,-59.873,53.808,-59.823,53.834,-59.749,53.842,-59.653,53.831,-59.497,53.834,-59.201,53.929,-59.039,53.964,-59.013,53.976,-58.978,54.01,-58.92,54.033,-58.841,54.044,-58.719,54.039,-58.633,54.05,-58.558,54.103,-58.435,54.228,-58.359,54.253,-58.22,54.286,-58.162,54.32,-58.151,54.35,-57.889,54.384,-57.699,54.387,-57.563,54.44,-57.485,54.517,-57.404,54.57,-57.404,54.591,-57.483,54.64,-57.627,54.65,-57.725,54.674,-57.827,54.719,-57.929,54.773,-57.962,54.876,-58.058,54.882,-58.195,54.866,-58.223,54.813,-58.398,54.774,-58.5,54.783,-58.78,54.838,-58.886,54.952,-58.956,55.055,-58.997,55.149,-59.086,55.183,-59.26,55.2,-59.324,55.153,-59.394,55.081,-59.429,55.056,-59.75,54.887,-59.838,54.814,-59.816,54.867,-59.742,54.943,-59.486,55.13,-59.438,55.176,-59.518,55.197,-59.605,55.173,-59.689,55.196,-59.696,55.269,-59.759,55.31,-59.862,55.295,-59.93,55.259,-60.213,55.236,-60.341,55.194,-60.557,55.067,-60.617,55.06,-60.521,55.129,-60.45,55.2,-60.433,55.243,-60.361,55.366,-60.224,55.444,-60.192,55.481,-60.308,55.557,-60.352,55.612,-60.408,55.65,-60.365,55.709,-60.341,55.785,-60.413,55.789,-60.476,55.805,-60.562,55.727,-60.593,55.815,-60.631,55.825,-60.737,55.887,-60.743,55.941,-60.832,55.958,-60.893,55.914,-60.996,55.862,-61.089,55.866,-61.123,55.889,-61.134,55.93,-61.188,55.955,-61.351,55.974,-61.45,55.996,-61.449,56.022,-61.301,56.047,-61.324,56.076,-61.365,56.216,-61.421,56.222,-61.559,56.208,-61.713,56.231,-61.707,56.289,-61.499,56.328,-61.425,56.361,-61.515,56.39,-61.692,56.397,-61.94,56.424,-62.01,56.454,-61.899,56.505,-61.76,56.511,-61.738,56.526,-61.813,56.571,-61.855,56.584,-61.992,56.591,-62.117,56.667,-62.396,56.73,-62.497,56.802,-62.46,56.818,-62.372,56.836,-62.296,56.833,-62.382,56.788,-62.366,56.767,-62.062,56.699,-61.532,56.655,-61.372,56.681,-61.373,56.776,-61.39,56.853,-61.346,56.922,-61.334,57.011,-61.629,57.183,-61.716,57.196,-61.798,57.186,-61.861,57.198,-61.945,57.228,-61.977,57.248,-61.939,57.274,-61.886,57.348,-61.85,57.37,-61.851,57.381,-61.921,57.421,-62.088,57.453,-62.194,57.455,-62.303,57.441,-62.396,57.448,-62.455,57.462,-62.496,57.489,-62.377,57.478,-62.339,57.485,-62.254,57.529,-62.167,57.537,-62.084,57.562,-61.968,57.612,-61.931,57.669,-61.995,57.769,-61.968,57.803,-61.914,57.825,-61.899,57.861,-61.959,57.912,-62.117,57.964,-62.202,57.955,-62.306,57.972,-62.486,58.154,-62.588,58.158,-62.818,58.129,-62.981,58.093,-63.22,58.002,-63.262,58.015,-63.152,58.084,-63.063,58.127,-62.812,58.2,-62.674,58.319,-62.594,58.474,-62.608,58.496,-62.737,58.492,-62.837,58.479,-63.076,58.415,-63.132,58.411,-63.12,58.442,-63.146,58.46,-63.21,58.467,-63.296,58.441,-63.474,58.331,-63.537,58.33,-63.438,58.399,-63.39,58.453,-63.219,58.52,-63.102,58.546,-62.874,58.672,-62.926,58.765,-63.008,58.855,-63.05,58.878,-63.185,58.858,-63.282,58.867,-63.222,58.911,-63.216,58.928,-63.279,59.003,-63.31,59.026,-63.304,59.034,-63.223,59.057,-63.248,59.068,-63.326,59.082,-63.399,59.08,-63.568,59.047,-63.794,59.027,-63.941,59.027,-63.971,59.054,-63.91,59.066,-63.756,59.063,-63.646,59.079,-63.506,59.115,-63.415,59.194,-63.54,59.333,-63.637,59.341,-63.752,59.277,-63.776,59.277,-63.759,59.319,-63.781,59.349,-63.945,59.38,-63.971,59.409,-63.85,59.448,-63.75,59.513,-63.841,59.574,-63.929,59.645,-63.969,59.698,-63.979,59.754,-64.056,59.823,-64.151,59.794,-64.226,59.741,-64.169,59.847,-64.183,59.973,-64.283,60.064,-64.408,60.065,-64.559,60.043,-64.733,59.998,-64.768,60.012,-64.713,60.037,-64.528,60.095,-64.42,60.171,-64.436,60.228,-64.499,60.268,-64.614,60.306,-64.706,60.336,-64.817,60.331,-64.845,60.308,-64.89,60.287,-64.931,60.252,-65.073,60.062,-65.105,59.993,-65.172,59.908,-65.181,59.867,-65.159,59.83,-65.113,59.802,-65.028,59.771,-65.054,59.753,-65.212,59.81,-65.289,59.818,-65.358,59.809,-65.406,59.795,-65.433,59.777,-65.481,59.69,-65.486,59.649,-65.475,59.617,-65.407,59.539,-65.346,59.511,-65.263,59.495,-65.171,59.462,-65.069,59.411,-65.038,59.388,-65.074,59.378,-65.274,59.464,-65.35,59.479,-65.475,59.47,-65.489,59.448,-65.407,59.33,-65.412,59.315,-65.513,59.35,-65.545,59.32,-65.578,59.245,-65.607,59.213,-65.661,59.23,-65.692,59.229,-65.7,59.213,-65.666,59.153,-65.64,59.128,-65.606,59.111,-65.496,59.091,-65.384,59.06,-65.396,59.038,-65.526,59.036,-65.544,59.012,-65.695,59.032,-65.721,59.024,-65.722,59.003,-65.704,58.971,-65.795,58.98,-65.841,58.977,-65.921,58.915,-65.918,58.896,-65.836,58.86,-65.855,58.847,-65.967,58.839,-66.043,58.821,-66.049,58.788,-66.021,58.735,-65.95,58.65,-65.928,58.611,-65.923,58.572,-65.931,58.535,-66.002,58.431,-66.017,58.431,-66.03,58.567,-66.045,58.606,-66.091,58.659,-66.168,58.727,-66.237,58.772,-66.299,58.795,-66.362,58.791,-66.48,58.731,-66.515,58.697,-66.558,58.637,-66.608,58.549,-66.722,58.491,-66.9,58.463,-67.019,58.433,-67.163,58.37,-67.382,58.3,-67.57,58.213,-67.596,58.186,-67.617,58.14,-67.632,58.076,-67.678,57.991,-67.698,58.009,-67.681,58.107,-67.688,58.14,-67.69,58.244,-67.737,58.385,-67.756,58.405,-67.805,58.365,-67.823,58.31,-67.856,58.273,-68.009,58.152,-68.064,58.139,-67.911,58.267,-67.888,58.296,-67.888,58.329,-67.981,58.461,-68.021,58.485,-68.111,58.473,-68.176,58.403,-68.289,58.178,-68.352,58.091,-68.414,58.052,-68.495,58.012,-68.781,57.976,-69.041,57.902,-69.035,57.926,-68.945,57.969,-68.826,58,-68.597,58.037,-68.468,58.076,-68.357,58.163,-68.315,58.227,-68.234,58.399,-68.229,58.485,-68.235,58.528,-68.253,58.557,-68.326,58.595,-68.381,58.744,-68.414,58.783,-68.475,58.823,-68.563,58.866,-68.637,58.893,-68.698,58.905,-68.942,58.889,-69.064,58.898,-69.173,58.897,-69.271,58.884,-69.382,58.851,-69.651,58.728,-69.79,58.689,-69.879,58.697,-70.033,58.745,-70.154,58.761,-70.16,58.789,-69.979,58.816,-69.868,58.856,-69.842,58.881,-69.829,58.905,-69.829,58.929,-69.814,58.946,-69.784,58.956,-69.753,58.94,-69.677,58.831,-69.648,58.821,-69.608,58.829,-69.532,58.869,-69.5,58.921,-69.475,59,-69.448,59.049,-69.42,59.068,-69.414,59.087,-69.46,59.152,-69.45,59.18,-69.35,59.277,-69.344,59.303,-69.4,59.338,-69.682,59.342,-69.711,59.393,-69.692,59.488,-69.656,59.565,-69.602,59.623,-69.579,59.675,-69.587,59.722,-69.63,59.822,-69.673,59.871,-69.734,59.918,-69.806,59.945,-70.327,59.971,-70.467,59.971,-70.62,59.984,-70.655,60.026,-70.509,60.015,-69.963,60.018,-69.796,60.03,-69.674,60.076,-69.63,60.122,-69.623,60.145,-69.629,60.199,-69.633,60.22,-69.708,60.286,-69.741,60.332,-69.756,60.389,-69.759,60.44,-69.751,60.487,-69.721,60.567,-69.64,60.69,-69.574,60.743,-69.49,60.78,-69.433,60.814,-69.405,60.847,-69.398,60.883,-69.414,60.922,-69.472,61.011,-69.503,61.04,-69.557,61.06,-69.624,61.05,-69.65,61.014,-69.678,60.95,-69.708,60.915,-69.8,60.907,-69.909,60.86,-69.992,60.856,-70.095,60.88,-70.146,60.922,-70.144,60.981,-70.158,61.021,-70.187,61.041,-70.279,61.069,-70.384,61.064,-70.541,61.042,-70.723,61.055,-71.035,61.126,-71.175,61.147,-71.348,61.149,-71.423,61.159,-71.552,61.213,-71.743,61.337,-71.732,61.372,-71.646,61.399,-71.645,61.413,-71.794,61.421,-71.854,61.44,-71.841,61.466,-71.756,61.527,-71.656,61.551,-71.619,61.573,-71.605,61.592,-71.638,61.617,-71.866,61.689,-71.922,61.677,-71.964,61.636,-72.023,61.612,-72.216,61.587,-72.247,61.602,-72.082,61.641,-72.043,61.665,-72.04,61.68,-72.081,61.728,-72.126,61.753,-72.178,61.802,-72.226,61.832,-72.361,61.888,-72.506,61.923,-72.574,61.907,-72.661,61.863,-72.727,61.839,-72.772,61.84,-72.666,61.955,-72.632,62.027,-72.633,62.053,-72.646,62.077,-72.671,62.114,-72.687,62.125,-72.735,62.131,-72.882,62.125,-72.992,62.18,-73.049,62.198,-73.195,62.279,-73.299,62.325,-73.428,62.369,-73.63,62.454,-73.705,62.473,-73.764,62.469,-73.878,62.434,-74.046,62.37,-74.205,62.321,-74.429,62.272,-74.646,62.211,-74.69,62.183,-74.613,62.125,-74.633,62.116,-74.908,62.23,-75.023,62.264,-75.114,62.271,-75.341,62.312,-75.409,62.307,-75.489,62.286,-75.79,62.18,-75.809,62.193,-75.676,62.25,-75.817,62.316,-76.616,62.466,-76.879,62.525,-77.205,62.55,-77.372,62.573,-77.604,62.531,-77.9,62.427,-78.068,62.355,-78.109,62.318,-78.133,62.282,-78.147,62.209,-78.137,62.107,-78.077,61.923,-78.021,61.832,-77.948,61.762,-77.89,61.729,-77.814,61.695,-77.698,61.626,-77.514,61.556,-77.649,61.479,-77.736,61.437,-77.75,61.393,-77.727,61.231,-77.731,61.206,-77.765,61.158,-77.83,61.084,-77.934,61.003,-78.16,60.852,-78.181,60.819,-78.122,60.81,-77.998,60.818,-77.872,60.786,-77.603,60.825,-77.59,60.809,-77.661,60.79,-77.734,60.697,-77.761,60.679,-77.791,60.64,-77.715,60.578,-77.639,60.567,-77.516,60.563,-77.504,60.543,-77.598,60.507,-77.681,60.427,-77.649,60.363,-77.453,60.146,-77.461,60.133,-77.572,60.101,-77.586,60.088,-77.547,60.061,-77.312,60.042,-77.289,60.022,-77.373,59.925,-77.368,59.884,-77.328,59.833,-77.332,59.797,-77.475,59.716,-77.485,59.685,-77.411,59.61,-77.349,59.579,-77.397,59.569,-77.59,59.681,-77.726,59.676,-77.748,59.658,-77.733,59.581,-77.749,59.558,-77.859,59.476,-77.845,59.444,-77.779,59.41,-77.761,59.38,-77.843,59.305,-77.988,59.246,-78.068,59.2,-78.14,59.142,-78.244,59.035,-78.431,58.902,-78.459,58.873,-78.483,58.829,-78.502,58.769,-78.515,58.682,-78.506,58.649,-78.463,58.602,-78.352,58.581,-78.014,58.399,-77.884,58.351,-77.684,58.291,-77.552,58.24,-77.489,58.195,-77.157,58.019,-76.891,57.758,-76.81,57.658,-76.786,57.599,-76.655,57.381,-76.601,57.272,-76.573,57.181,-76.526,56.892,-76.52,56.707,-76.53,56.5,-76.546,56.359,-76.604,56.2,-76.65,56.107,-76.762,55.996,-76.938,55.867,-77.073,55.756,-77.165,55.664,-77.325,55.556,-77.702,55.344,-77.775,55.291,-77.891,55.236,-78.129,55.151,-78.304,55.069,-78.475,55.011,-78.846,54.908,-78.909,54.881,-79.666,54.697,-79.712,54.672,-79.714,54.655,-79.67,54.647,-79.632,54.629,-79.598,54.602,-79.521,54.492,-79.476,54.395,-79.431,54.337,-79.356,54.263,-79.296,54.217,-79.216,54.186,-79.147,54.169,-79.139,54.157,-79.179,54.117,-79.242,54.099,-79.067,54.052,-79.01,54.024,-78.996,54.002,-79.073,53.951,-79.075,53.932,-79.032,53.881,-78.944,53.84,-78.946,53.832,-79.003,53.837,-79.04,53.818,-79.081,53.742,-79.113,53.717,-79.1,53.657,-79.043,53.56,-78.992,53.41,-78.947,53.206,-78.898,53.043,-78.854,52.976,-78.74,52.899,-78.722,52.856,-78.754,52.812,-78.766,52.76,-78.744,52.655,-78.724,52.628,-78.601,52.535,-78.557,52.492,-78.529,52.399,-78.526,52.311,-78.513,52.291,-78.448,52.261,-78.492,52.252,-78.537,52.213,-78.593,52.14,-78.702,52.033,-78.828,51.963,-78.871,51.913,-78.891,51.845,-78.928,51.799,-78.982,51.775,-78.978,51.734,-78.776,51.566,-78.736,51.527,-78.731,51.497,-78.827,51.43,-78.858,51.384,-78.898,51.272,-78.903,51.2,-78.937,51.259,-78.984,51.386,-79.005,51.425,-79.041,51.464,-79.091,51.502,-79.153,51.526,-79.226,51.537,-79.264,51.552,-79.297,51.623,-79.339,51.628,-79.498,51.57,-79.521,51.545,-79.528,51.538,-79.547,51.494,-79.586,51.452,-79.643,51.414,-79.689,51.347,-79.723,51.252,-79.737,51.186,-79.731,51.15,-79.714,51.118,-79.636,51.049,-79.52,50.966,-79.453,50.917,-79.381,50.835,-79.348,50.763,-79.456,50.876,-79.52,50.919,-79.652,51.008,-79.836,51.173,-79.96,51.235,-80.104,51.283,-80.266,51.316,-80.368,51.33,-80.478,51.307,-80.677,51.191,-80.795,51.132,-80.851,51.125,-80.673,51.265,-80.496,51.345,-80.443,51.389,-80.448,51.432,-80.496,51.525,-80.588,51.667,-80.658,51.758,-80.706,51.798,-80.969,51.972,-81.127,52.045,-81.285,52.089,-81.398,52.142,-81.466,52.204,-81.55,52.237,-81.648,52.239,-81.815,52.217,-81.828,52.224,-81.776,52.254,-81.661,52.294,-81.612,52.324,-81.572,52.367,-81.599,52.433,-81.742,52.564,-81.859,52.651,-82.02,52.812,-82.108,52.877,-82.203,52.922,-82.26,52.961,-82.292,53.031,-82.292,53.066,-82.26,53.16,-82.219,53.211,-82.159,53.264,-82.146,53.365,-82.18,53.513,-82.191,53.611,-82.15,53.74,-82.141,53.818,-82.163,53.886,-82.24,54.045,-82.264,54.073,-82.394,54.18,-82.424,54.245,-82.418,54.356,-82.371,54.483,-82.219,54.813,-82.227,54.856,-82.308,54.998,-82.393,55.068,-82.577,55.149,-82.687,55.166,-82.801,55.156,-82.868,55.161,-82.947,55.222,-82.986,55.231,-83.214,55.215,-83.569,55.262,-83.668,55.265,-83.911,55.315,-83.972,55.314,-84.023,55.298,-84.105,55.291,-84.219,55.293,-84.356,55.283,-84.518,55.259,-84.706,55.259,-84.92,55.283,-85.061,55.286,-85.129,55.266,-85.214,55.224,-85.365,55.079,-85.362,55.095,-85.212,55.297,-85.218,55.349,-85.283,55.383,-85.407,55.431,-85.478,55.474,-85.559,55.54,-85.677,55.601,-85.831,55.657,-85.984,55.696,-86.139,55.718,-86.377,55.773,-86.919,55.915,-87.287,55.975,-87.482,56.021,-87.561,56.056,-87.878,56.342,-88.075,56.467,-88.271,56.536,-88.447,56.609,-88.68,56.725,-88.826,56.814,-88.948,56.851,-89.212,56.884,-89.342,56.915,-89.791,56.981,-90.075,57.052,-90.345,57.149,-90.592,57.224,-90.897,57.257,-91.111,57.241,-92.018,57.064,-92.249,57.009,-92.356,56.971,-92.372,56.975,-92.298,57.023,-92.303,57.046,-92.456,57.037,-92.51,57.022,-92.651,56.958,-92.725,56.934,-92.798,56.922,-92.802,56.928,-92.738,56.953,-92.675,56.99,-92.614,57.039,-92.548,57.111,-92.478,57.205,-92.44,57.275,-92.433,57.32,-92.449,57.385,-92.49,57.469,-92.702,57.778,-92.74,57.844,-92.842,58.076,-92.925,58.225,-93.1,58.49,-93.127,58.564,-93.155,58.695,-93.179,58.726,-93.278,58.756,-93.375,58.741,-93.486,58.744,-93.78,58.773,-94.056,58.76,-94.123,58.737,-94.209,58.626,-94.272,58.378,-94.332,58.297,-94.333,58.339,-94.281,58.659,-94.287,58.716,-94.419,58.746,-94.54,58.848,-94.579,58.868,-94.624,58.876,-94.673,58.87,-94.713,58.903,-94.744,58.975,-94.776,59.021,-94.847,59.05,-94.957,59.069,-94.87,59.088,-94.82,59.151,-94.788,59.268,-94.777,59.478,-94.786,59.953,-94.772,60.002,-94.742,60.107,-94.67,60.301,-94.647,60.416,-94.671,60.453,-94.705,60.478,-94.762,60.498,-94.679,60.538,-94.569,60.542,-94.509,60.605,-94.427,60.731,-94.309,60.871,-94.154,61.025,-94.068,61.139,-94.05,61.211,-94.055,61.266,-94.083,61.304,-94.061,61.318,-93.942,61.308,-93.889,61.344,-93.889,61.364,-93.941,61.444,-93.913,61.481,-93.71,61.603,-93.421,61.706,-93.352,61.74,-93.312,61.767,-93.314,61.78,-93.43,61.812,-93.494,61.847,-93.527,61.872,-93.582,61.942,-93.372,61.929,-93.333,61.933,-93.273,61.961,-93.297,61.982,-93.366,62.015,-93.35,62.03,-93.167,62.034,-93.073,62.061,-93.016,62.093,-93.028,62.109,-93.07,62.128,-93.066,62.15,-92.906,62.215,-92.914,62.245,-92.988,62.286,-93.179,62.35,-93.205,62.365,-93.154,62.367,-92.866,62.306,-92.735,62.26,-92.648,62.208,-92.579,62.177,-92.528,62.168,-92.544,62.202,-92.627,62.279,-92.701,62.328,-92.766,62.35,-92.768,62.38,-92.707,62.418,-92.595,62.47,-92.562,62.503,-92.551,62.547,-92.497,62.565,-92.4,62.557,-92.324,62.565,-92.27,62.587,-92.207,62.585,-92.081,62.544,-92.008,62.541,-91.944,62.575,-91.936,62.592,-91.956,62.645,-92.149,62.665,-92.243,62.684,-92.305,62.712,-92.345,62.734,-92.378,62.772,-92.388,62.801,-92.361,62.819,-92.196,62.829,-92.152,62.839,-92.11,62.862,-92.034,62.863,-91.87,62.835,-91.449,62.804,-91.349,62.819,-91.115,62.922,-91.008,62.94,-90.871,62.946,-90.778,62.972,-90.728,63.017,-90.699,63.064,-90.691,63.111,-90.711,63.304,-90.747,63.352,-90.97,63.443,-91.103,63.476,-91.33,63.507,-91.489,63.562,-91.686,63.66,-91.842,63.698,-91.957,63.676,-92.077,63.64,-92.29,63.563,-92.465,63.555,-92.461,63.569,-92.205,63.657,-92.157,63.692,-92.196,63.708,-92.339,63.735,-92.529,63.761,-93.166,63.902,-93.25,63.927,-93.379,63.948,-93.406,63.941,-93.38,63.9,-93.327,63.872,-93.266,63.853,-93.27,63.841,-93.416,63.838,-93.56,63.865,-93.664,63.941,-93.656,63.973,-93.605,64.004,-93.597,64.041,-93.696,64.147,-93.43,64.029,-92.97,63.938,-92.55,63.83,-92.338,63.788,-92.195,63.776,-92.095,63.784,-92.038,63.813,-91.982,63.822,-91.929,63.812,-91.919,63.801,-91.954,63.787,-91.956,63.772,-91.926,63.757,-91.675,63.742,-91.539,63.726,-91.108,63.618,-90.946,63.588,-90.812,63.581,-90.707,63.597,-90.635,63.624,-90.596,63.661,-90.533,63.665,-90.446,63.636,-90.369,63.624,-90.245,63.642,-90.155,63.69,-90.013,63.804,-90.018,63.829,-90.06,63.877,-90.168,63.979,-90.142,63.982,-89.922,63.944,-89.856,63.957,-89.861,63.979,-89.954,64.081,-89.986,64.1,-90.08,64.128,-90.042,64.141,-89.811,64.181,-89.792,64.168,-89.764,64.1,-89.733,64.077,-89.616,64.031,-89.551,64.015,-89.501,64.015,-89.465,64.03,-89.404,64.04,-89.215,63.984,-89.132,63.969,-89.108,63.981,-89.209,64.105,-89.201,64.114,-89.06,64.034,-88.964,64.011,-88.818,63.992,-88.653,64.009,-88.379,64.089,-88.106,64.183,-87.998,64.244,-87.964,64.302,-87.885,64.4,-87.281,64.826,-87.183,64.927,-87.029,65.064,-87.003,65.109,-87.028,65.198,-87.108,65.225,-87.392,65.261,-87.93,65.28,-88.198,65.28,-88.974,65.348,-89.127,65.396,-89.242,65.446,-89.6,65.648,-89.788,65.737,-89.924,65.78,-90.048,65.806,-90.159,65.813,-90.597,65.885,-90.983,65.919,-91.065,65.9,-91.074,65.886,-91.041,65.83,-91.285,65.894,-91.427,65.948,-91.412,65.959,-91.305,65.965,-91.01,65.966,-90.826,65.954,-90.655,65.929,-90.513,65.921,-90.316,65.926,-90.117,65.882,-90.004,65.883,-89.89,65.869,-89.848,65.872,-89.944,65.934,-89.89,65.941,-89.749,65.936,-89.593,65.909,-89.42,65.861,-89.088,65.739,-88.946,65.703,-88.808,65.692,-88.744,65.679,-88.672,65.612,-88.587,65.588,-88.395,65.516,-88.121,65.395,-87.97,65.349,-87.678,65.335,-87.453,65.339,-87.291,65.355,-87.194,65.383,-87.081,65.441,-86.953,65.528,-86.702,65.671,-86.043,66.023,-86.012,66.048,-85.959,66.119,-85.964,66.154,-86.001,66.187,-86.113,66.225,-86.301,66.27,-86.585,66.322,-86.685,66.36,-86.747,66.417,-86.738,66.433,-86.7,66.443,-86.689,66.457,-86.737,66.511,-86.708,66.523,-86.633,66.531,-86.063,66.52,-85.792,66.533,-85.604,66.568,-85.442,66.537,-85.307,66.44,-85.192,66.37,-85.096,66.325,-84.908,66.271,-84.628,66.208,-84.478,66.179,-84.459,66.186,-84.398,66.259,-84.324,66.291,-84.293,66.292,-84.012,66.231,-83.905,66.212,-83.869,66.214,-83.798,66.238,-83.826,66.29,-83.964,66.421,-84.094,66.526,-84.153,66.59,-84.183,66.648,-84.223,66.682,-84.319,66.712,-84.59,66.857,-84.603,66.875,-84.639,66.902,-84.738,66.934,-84.857,66.941,-84.899,66.927,-84.978,66.881,-85.018,66.872,-85.111,66.891,-85.114,66.907,-85.04,66.956,-84.846,67.029,-84.693,67.017,-84.538,66.973,-84.531,66.961,-84.466,66.927,-84.31,66.863,-84.273,66.839,-84.361,66.823,-84.366,66.811,-84.324,66.782,-84.208,66.736,-84.154,66.732,-84.05,66.74,-83.998,66.729,-83.92,66.679,-83.739,66.534,-83.651,66.485,-83.628,66.461,-83.59,66.388,-83.523,66.369,-83.406,66.371,-83.298,66.392,-83.199,66.431,-82.949,66.551,-82.642,66.588,-82.554,66.621,-82.375,66.709,-82.261,66.739,-82.198,66.765,-82.113,66.825,-82.005,66.92,-81.926,66.975,-81.874,66.988,-81.722,66.986,-81.63,67.002,-81.468,67.07,-81.443,67.093,-81.387,67.189,-81.301,67.357,-81.27,67.46,-81.294,67.497,-81.412,67.595,-81.493,67.637,-81.709,67.722,-81.869,67.802,-81.976,67.862,-82.063,67.928,-82.1,67.99,-82.102,68.019,-82.092,68.051,-82.013,68.173,-82.012,68.194,-82.034,68.196,-82.078,68.18,-82.151,68.14,-82.187,68.134,-82.222,68.145,-82.393,68.285,-82.423,68.297,-82.431,68.307,-82.393,68.338,-82.413,68.357,-82.464,68.382,-82.553,68.446,-82.549,68.469,-82.499,68.479,-82.397,68.478,-82.21,68.506,-82.106,68.499,-82.006,68.463,-81.915,68.459,-81.831,68.487,-81.64,68.524,-81.527,68.556,-81.282,68.657,-81.259,68.692,-81.252,68.743,-81.264,68.781,-81.331,68.828,-81.381,68.85,-81.476,68.866,-81.687,68.879,-81.958,68.884,-81.952,68.909,-81.758,68.957,-81.611,69.003,-81.329,69.12,-81.322,69.139,-81.378,69.186,-81.412,69.198,-81.732,69.258,-81.952,69.276,-82.151,69.249,-82.228,69.249,-82.247,69.265,-82.208,69.297,-82.232,69.333,-82.31,69.41,-82.642,69.458,-82.755,69.494,-82.633,69.518,-82.496,69.532,-82.39,69.601,-82.374,69.642,-82.618,69.691,-82.746,69.695,-82.991,69.686,-83.552,69.704,-83.665,69.7,-83.917,69.745,-84.242,69.835,-84.319,69.844,-84.645,69.85,-84.834,69.835,-85.02,69.805,-85.177,69.805,-85.305,69.836,-85.415,69.85,-85.507,69.845,-85.535,69.835,-85.497,69.819,-85.446,69.778,-85.448,69.748,-85.502,69.652,-85.482,69.619,-85.43,69.581,-85.416,69.548,-85.44,69.52,-85.437,69.488,-85.409,69.452,-85.402,69.427,-85.416,69.411,-85.432,69.354,-85.428,69.318,-85.387,69.232,-85.275,69.172,-85.243,69.163,-85.114,69.166,-84.89,69.093,-84.862,69.074,-84.893,69.021,-84.895,68.989,-84.916,68.962,-85.008,68.949,-85.083,68.908,-85.104,68.871,-85.107,68.844,-84.867,68.79,-84.868,68.773,-85.275,68.741,-85.338,68.746,-85.425,68.774,-85.491,68.774,-85.518,68.77,-85.562,68.729,-85.643,68.7,-85.69,68.671,-85.734,68.63,-85.745,68.578,-85.723,68.515,-85.731,68.445,-85.789,68.328,-85.953,68.072,-85.984,68.045,-86.37,67.825,-86.398,67.8,-86.476,67.713,-86.504,67.649,-86.536,67.516,-86.561,67.482,-86.609,67.451,-86.682,67.422,-86.75,67.406,-86.813,67.402,-86.924,67.356,-87.083,67.268,-87.266,67.184,-87.32,67.173,-87.359,67.177,-87.392,67.191,-87.418,67.214,-87.471,67.325,-87.499,67.355,-87.997,67.626,-88.196,67.766,-88.314,67.95,-88.325,67.989,-88.32,68.166,-88.361,68.26,-88.347,68.288,-88.235,68.339,-88.209,68.335,-88.146,68.266,-88.111,68.251,-87.991,68.242,-87.893,68.248,-87.853,68.267,-87.828,68.3,-87.814,68.346,-87.81,68.404,-87.828,68.448,-87.866,68.478,-87.912,68.565,-87.964,68.709,-88.041,68.812,-88.224,68.915,-88.316,68.954,-88.638,69.059,-88.815,69.136,-88.954,69.22,-89.057,69.266,-89.198,69.269,-89.28,69.255,-89.352,69.227,-89.552,69.085,-89.667,69.015,-89.72,68.932,-89.751,68.812,-89.783,68.736,-89.884,68.626,-89.897,68.594,-89.879,68.522,-89.898,68.491,-90.005,68.398,-90.116,68.339,-90.157,68.306,-90.174,68.27,-90.205,68.257,-90.248,68.267,-90.285,68.292,-90.317,68.33,-90.36,68.347,-90.423,68.395,-90.528,68.432,-90.574,68.475,-90.525,68.611,-90.51,68.689,-90.543,68.786,-90.539,68.82,-90.468,68.864,-90.479,68.881,-90.587,68.947,-90.745,69.106,-91.237,69.286,-91.218,69.302,-91.148,69.303,-91.058,69.318,-91.025,69.315,-91.049,69.293,-91.005,69.277,-90.892,69.267,-90.822,69.29,-90.795,69.347,-90.749,69.392,-90.684,69.428,-90.606,69.445,-90.513,69.445,-90.416,69.457,-90.451,69.475,-90.555,69.504,-90.667,69.516,-90.786,69.509,-90.95,69.515,-91.288,69.543,-91.44,69.526,-91.427,69.538,-91.305,69.581,-91.17,69.62,-91.151,69.637,-91.202,69.645,-91.384,69.649,-91.532,69.615,-91.724,69.546,-91.912,69.531,-92.209,69.603,-92.258,69.634,-92.231,69.653,-92.312,69.673,-92.493,69.683,-92.643,69.659,-92.803,69.651,-92.855,69.655,-92.888,69.668,-92.751,69.714,-92.285,69.892,-92.069,69.984,-91.977,70.039,-92.058,70.071,-92.127,70.085,-92.446,70.083,-92.512,70.104,-92.455,70.15,-92.363,70.201,-92.321,70.235,-92.29,70.24,-92.209,70.198,-92.121,70.17,-91.995,70.143,-91.859,70.133,-91.616,70.148,-91.572,70.162,-91.564,70.178,-91.654,70.233,-91.716,70.299,-91.761,70.326,-91.82,70.342,-91.876,70.331,-91.926,70.295,-91.984,70.286,-92.047,70.303,-92.073,70.319,-92.037,70.367,-92.049,70.39,-92.214,70.493,-92.315,70.608,-92.356,70.634,-92.388,70.65,-92.567,70.693,-92.642,70.719,-92.783,70.798,-92.961,70.838,-92.981,70.852,-92.922,70.887,-92.904,70.916,-92.883,71.069,-92.89,71.122,-92.949,71.262,-92.983,71.3,-93.031,71.336,-93.256,71.461,-93.407,71.521,-93.576,71.569,-93.763,71.638,-93.782,71.674,-93.751,71.717,-93.746,71.743,-93.81,71.766,-94.086,71.771,-94.171,71.758,-94.308,71.765,-94.479,71.849,-94.491,71.916,-94.557,71.979,-94.611,71.987,-94.735,71.983,-94.887,71.963,-95.201,71.904,-95.512,71.777,-95.616,71.685,-95.838,71.598,-95.872,71.573,-95.83,71.526,-95.773,71.514,-95.674,71.504,-95.445,71.505,-95.406,71.492,-95.448,71.46,-95.564,71.337,-95.633,71.319,-95.725,71.328,-95.851,71.361,-95.924,71.393,-95.994,71.411,-96.062,71.414,-96.14,71.396,-96.271,71.339,-96.406,71.274,-96.447,71.24,-96.421,71.176,-96.445,71.159,-96.504,71.143,-96.525,71.127,-96.47,71.07,-96.491,71.002,-96.551,70.89,-96.549,70.809,-96.359,70.679,-96.258,70.642,-96.186,70.638,-95.906,70.698,-95.886,70.694,-95.988,70.617,-95.98,70.593,-95.879,70.549,-96.048,70.567,-96.123,70.561,-96.226,70.542,-96.298,70.511,-96.337,70.47,-96.546,70.327,-96.56,70.243,-96.551,70.21,-96.492,70.125,-96.269,69.992,-96.172,69.925,-96.119,69.872,-96.05,69.831,-95.965,69.803,-95.851,69.785,-95.707,69.778,-95.588,69.756,-95.491,69.718,-95.292,69.667,-94.823,69.578,-94.789,69.585,-94.713,69.649,-94.676,69.657,-94.634,69.65,-94.514,69.583,-94.419,69.517,-94.338,69.474,-94.271,69.455,-94.163,69.446,-94.015,69.447,-93.915,69.458,-93.794,69.498,-93.65,69.519,-93.532,69.481,-93.522,69.451,-93.543,69.406,-93.537,69.382,-93.431,69.375,-93.451,69.355,-93.567,69.297,-93.749,69.226,-93.82,69.253,-93.801,69.281,-93.613,69.403,-93.619,69.417,-93.854,69.376,-94.156,69.342,-94.255,69.314,-94.277,69.275,-94.285,69.242,-94.255,69.151,-94.222,69.136,-94.081,69.136,-94.084,69.123,-94.237,69.05,-94.476,68.958,-94.563,68.912,-94.6,68.803,-94.587,68.776,-94.478,68.743,-94.217,68.761,-94.065,68.785,-93.992,68.821,-93.938,68.889,-93.896,68.982,-93.881,68.997,-93.852,69,-93.811,68.993,-93.766,68.97,-93.716,68.931,-93.681,68.887,-93.663,68.838,-93.66,68.784,-93.676,68.686,-93.644,68.633,-93.606,68.624,-93.449,68.619,-93.483,68.599,-93.652,68.543,-93.928,68.474,-94.098,68.399,-94.255,68.297,-94.384,68.227,-94.485,68.19,-94.744,68.071,-94.861,68.042,-94.955,68.05,-95.126,68.083,-95.235,68.06,-95.384,68.056,-95.427,68.045,-95.461,68.021,-95.65,67.737,-95.634,67.704,-95.463,67.61,-95.39,67.518,-95.296,67.361,-95.259,67.263,-95.321,67.152,-95.354,66.981,-95.4,66.949,-95.49,66.924,-95.625,66.916,-95.972,66.952,-96.037,66.938,-96.045,66.923,-96.016,66.87,-95.772,66.726,-95.743,66.69,-95.788,66.617,-95.797,66.617,-95.813,66.69,-95.885,66.741,-96.36,66.989,-96.42,67.036,-96.423,67.052,-96.404,67.063,-96.35,67.07,-96.216,66.998,-96.095,66.994,-96.02,67.019,-95.954,67.011,-95.862,66.978,-95.769,66.967,-95.611,66.976,-95.559,66.973,-95.502,66.98,-95.457,66.989,-95.419,67.013,-95.407,67.056,-95.405,67.116,-95.416,67.156,-95.529,67.209,-95.557,67.215,-95.626,67.212,-95.778,67.185,-95.783,67.194,-95.695,67.299,-95.72,67.317,-95.879,67.298,-96.013,67.271,-96.141,67.272,-96.169,67.289,-96.185,67.376,-96.213,67.404,-96.369,67.51,-96.371,67.554,-96.228,67.679,-96.199,67.718,-96.171,67.832,-96.036,68.158,-95.97,68.249,-96.076,68.237,-96.439,68.151,-96.461,68.136,-96.494,68.085,-96.531,68.063,-96.592,68.048,-96.722,68.039,-96.725,68.061,-96.48,68.243,-96.435,68.29,-96.431,68.311,-96.628,68.25,-96.977,68.255,-97,68.265,-97.072,68.333,-97.136,68.378,-97.266,68.453,-97.336,68.479,-97.41,68.497,-97.481,68.495,-97.548,68.475,-97.64,68.482,-97.829,68.533,-97.925,68.524,-97.939,68.51,-97.911,68.45,-97.794,68.388,-98.091,68.346,-98.219,68.317,-98.469,68.382,-98.522,68.383,-98.562,68.371,-98.65,68.364,-98.633,68.331,-98.491,68.224,-98.449,68.201,-98.381,68.132,-98.386,68.115,-98.5,68.132,-98.5,68.118,-98.438,68.065,-98.193,67.923,-98.11,67.903,-97.913,67.954,-97.739,67.978,-97.547,67.961,-97.336,67.901,-97.207,67.855,-97.158,67.822,-97.14,67.796,-97.157,67.755,-97.155,67.726,-97.194,67.697,-97.274,67.666,-97.455,67.617,-97.607,67.631,-97.931,67.711,-97.978,67.739,-98.063,67.77,-98.415,67.988,-98.54,68.047,-98.632,68.073,-98.69,68.066,-98.72,68.042,-98.722,68,-98.704,67.966,-98.606,67.911,-98.468,67.856,-98.417,67.826,-98.412,67.807,-98.453,67.798,-98.697,67.78,-98.812,67.744,-98.92,67.726,-99.032,67.719,-99.147,67.724,-99.294,67.745,-99.472,67.784,-99.773,67.815,-100.213,67.839,-100.456,67.839,-100.52,67.818,-100.616,67.808,-100.746,67.809,-100.856,67.799,-101.026,67.766,-101.096,67.762,-101.555,67.693,-101.689,67.709,-101.884,67.745,-102.057,67.753,-102.21,67.733,-102.32,67.736,-102.389,67.762,-102.692,67.812,-102.842,67.853,-103.022,67.94,-103.323,68.064,-103.474,68.115,-103.657,68.069,-103.902,68.041,-104.194,68.031,-104.351,68.041,-104.487,68.063,-104.628,68.121,-104.661,68.149,-104.636,68.214,-104.653,68.23,-104.77,68.252,-104.879,68.245,-104.912,68.25,-104.937,68.303,-104.96,68.311,-104.994,68.307,-105.044,68.288,-105.101,68.298,-105.195,68.33,-105.377,68.414,-105.429,68.458,-105.457,68.578,-105.54,68.719,-105.606,68.782,-105.686,68.828,-105.798,68.865,-106.016,68.906,-106.164,68.92,-106.324,68.899,-106.713,68.819,-106.831,68.809,-107.436,68.689,-107.766,68.649,-108.313,68.611,-108.346,68.598,-108.641,68.379,-108.718,68.297,-108.687,68.277,-108.368,68.178,-108.323,68.154,-108.261,68.15,-108.105,68.169,-108.027,68.163,-107.734,68.174,-107.678,68.203,-107.734,68.252,-107.742,68.286,-107.619,68.331,-107.499,68.324,-107.298,68.296,-107.146,68.304,-107.043,68.347,-106.946,68.374,-106.854,68.387,-106.78,68.387,-106.608,68.357,-106.567,68.389,-106.543,68.461,-106.458,68.516,-106.237,68.577,-106.027,68.623,-105.932,68.637,-105.774,68.611,-105.75,68.592,-105.781,68.527,-105.857,68.475,-105.933,68.443,-106.039,68.407,-106.132,68.39,-106.271,68.383,-106.404,68.319,-106.429,68.288,-106.424,68.201,-106.459,68.196,-106.535,68.209,-106.668,68.216,-106.711,68.207,-106.791,68.145,-106.836,68.129,-106.923,68.114,-106.994,68.106,-107.125,68.108,-107.224,68.094,-107.351,68.061,-107.446,68.05,-107.509,68.059,-107.761,68.032,-107.798,68.037,-107.787,68.013,-107.729,67.959,-107.763,67.907,-107.891,67.856,-107.958,67.819,-107.972,67.732,-107.954,67.7,-107.865,67.639,-107.753,67.587,-107.65,67.511,-107.638,67.474,-107.651,67.428,-107.644,67.385,-107.567,67.273,-107.482,67.199,-107.318,67.128,-107.283,67.103,-107.348,67.055,-107.323,67.023,-107.254,66.976,-107.156,66.882,-107.2,66.883,-107.329,66.932,-107.402,66.948,-107.419,66.931,-107.451,66.927,-107.499,66.936,-107.626,67.003,-107.725,66.984,-107.746,66.961,-107.74,66.814,-107.731,66.769,-107.71,66.74,-107.564,66.619,-107.278,66.425,-107.259,66.399,-107.291,66.402,-107.374,66.435,-107.48,66.492,-107.705,66.637,-107.761,66.684,-107.958,66.781,-108.002,66.818,-108.05,66.844,-108.101,66.86,-108.158,66.893,-108.218,66.941,-108.455,67.063,-108.496,67.092,-108.459,67.09,-108.344,67.058,-108.221,67.051,-108.088,67.07,-107.991,67.095,-107.929,67.127,-107.909,67.163,-107.931,67.202,-107.989,67.256,-108.347,67.403,-108.492,67.483,-108.593,67.591,-108.613,67.598,-108.68,67.606,-108.715,67.583,-108.815,67.438,-108.852,67.422,-108.891,67.438,-108.95,67.494,-108.968,67.532,-108.994,67.637,-109.038,67.691,-109.081,67.711,-109.224,67.73,-109.63,67.733,-109.686,67.752,-109.76,67.82,-109.831,67.866,-109.904,67.874,-109.937,67.888,-110.042,67.977,-110.074,67.993,-110.102,67.992,-110.216,67.954,-110.372,67.954,-110.805,67.832,-110.99,67.791,-111.087,67.788,-111.155,67.798,-111.192,67.823,-111.291,67.815,-111.451,67.776,-111.576,67.757,-111.711,67.757,-112.101,67.732,-112.237,67.731,-112.315,67.72,-112.435,67.685,-112.503,67.682,-112.879,67.68,-113.075,67.687,-113.215,67.702,-113.682,67.7,-113.893,67.707,-114.051,67.727,-114.176,67.735,-114.267,67.731,-114.429,67.751,-114.663,67.795,-114.857,67.814,-115.011,67.806,-115.133,67.819,-115.288,67.872,-115.434,67.902,-115.427,67.924,-115.202,67.998,-115.167,68.019,-115.187,68.084,-115.176,68.104,-115.127,68.132,-114.852,68.195,-114.765,68.27,-114.275,68.248,-114.096,68.267,-114.053,68.283,-114.021,68.306,-113.964,68.399,-113.988,68.415,-114.051,68.415,-114.092,68.435,-114.111,68.477,-114.218,68.552,-114.414,68.66,-114.62,68.746,-114.994,68.85,-115.24,68.892,-115.442,68.941,-115.631,68.973,-115.806,68.987,-115.883,68.987,-115.936,68.958,-116.167,68.975,-116.243,68.974,-116.252,68.958,-116.065,68.855,-116.059,68.837,-116.223,68.847,-116.334,68.874,-116.425,68.881,-116.55,68.879,-117.026,68.916,-117.132,68.907,-117.227,68.913,-117.311,68.935,-117.83,69,-118.095,69.043,-118.307,69.093,-118.486,69.145,-118.745,69.234,-118.869,69.257,-119.853,69.342,-120.14,69.381,-120.293,69.421,-120.682,69.567]},{"b":[-82.039,52.734,-80.71,53.224],"l":[-81.136,53.206,-81.335,53.224,-81.847,53.186,-81.901,53.166,-81.951,53.132,-82.039,53.05,-82.005,53.01,-81.839,52.958,-81.352,52.852,-81.097,52.78,-81.01,52.761,-80.802,52.734,-80.732,52.747,-80.71,52.787,-80.71,52.832,-80.765,52.923,-80.9,53.037,-81.136,53.206]},{"b":[-74.749,67.783,-73.398,68.093],"l":[-73.584,68.015,-73.881,68.022,-74.111,68.061,-74.379,68.093,-74.707,68.067,-74.731,68.049,-74.749,68.018,-74.746,67.985,-74.679,67.906,-74.573,67.829,-74.481,67.805,-74.374,67.79,-74.109,67.783,-73.622,67.784,-73.407,67.793,-73.398,67.83,-73.402,67.879,-73.435,67.97,-73.459,67.99,-73.494,68.001,-73.584,68.015]},{"b":[-78.537,63.114,-77.527,63.49],"l":[-77.792,63.428,-77.877,63.471,-77.934,63.479,-78.235,63.49,-78.417,63.47,-78.507,63.451,-78.537,63.424,-78.469,63.358,-78.256,63.24,-78.024,63.139,-77.942,63.114,-77.791,63.13,-77.658,63.165,-77.594,63.188,-77.533,63.234,-77.527,63.269,-77.538,63.287,-77.655,63.396,-77.704,63.431,-77.792,63.428]},{"b":[-83.912,62.16,-81.949,62.978],"l":[-82.706,62.945,-82.966,62.874,-83.026,62.872,-83.111,62.884,-83.289,62.922,-83.376,62.905,-83.739,62.569,-83.899,62.476,-83.91,62.454,-83.912,62.426,-83.903,62.402,-83.761,62.304,-83.729,62.257,-83.714,62.174,-83.699,62.16,-83.377,62.238,-83.252,62.233,-83.13,62.204,-83.071,62.2,-83.016,62.21,-82.568,62.403,-82.491,62.447,-82.388,62.519,-82.114,62.652,-82.026,62.73,-81.99,62.776,-81.964,62.828,-81.949,62.884,-81.961,62.926,-82,62.954,-82.048,62.971,-82.129,62.978,-82.235,62.977,-82.46,62.936,-82.706,62.945]},{"b":[-80.28,61.595,-79.272,62.413],"l":[-79.466,62.385,-79.545,62.412,-79.598,62.413,-79.65,62.398,-79.713,62.395,-79.868,62.404,-79.927,62.393,-80.022,62.343,-80.179,62.213,-80.235,62.153,-80.26,62.109,-80.275,62.055,-80.28,61.99,-80.276,61.859,-80.265,61.818,-80.205,61.777,-80.092,61.747,-80.004,61.703,-79.896,61.63,-79.816,61.595,-79.763,61.596,-79.714,61.613,-79.669,61.644,-79.611,61.71,-79.542,61.808,-79.462,61.894,-79.372,61.968,-79.324,62.026,-79.306,62.104,-79.272,62.186,-79.286,62.248,-79.336,62.294,-79.466,62.385]},{"b":[-106.066,77.124,-104.454,77.754],"l":[-104.77,77.413,-104.955,77.419,-104.994,77.45,-105.007,77.507,-105.074,77.548,-105.29,77.642,-105.456,77.701,-105.588,77.736,-105.863,77.754,-106.036,77.74,-106.066,77.725,-105.883,77.627,-105.848,77.563,-105.747,77.525,-105.695,77.461,-105.556,77.353,-105.38,77.254,-105.215,77.182,-105.016,77.165,-104.711,77.124,-104.558,77.142,-104.493,77.162,-104.457,77.221,-104.454,77.249,-104.501,77.309,-104.542,77.338,-104.77,77.413]},{"b":[-96.277,77.452,-93.129,77.792],"l":[-93.129,77.66,-93.211,77.71,-93.301,77.74,-93.471,77.764,-93.583,77.771,-94.015,77.76,-94.667,77.776,-94.96,77.774,-95.233,77.754,-95.484,77.792,-95.684,77.782,-96.143,77.714,-96.195,77.701,-96.239,77.673,-96.277,77.631,-96.264,77.595,-96.056,77.503,-95.987,77.484,-94.409,77.474,-93.836,77.452,-93.74,77.465,-93.544,77.467,-93.52,77.474,-93.339,77.63,-93.129,77.66]},{"b":[-100.126,79.724,-98.769,80.144],"l":[-99.516,79.887,-99.333,79.84,-99.306,79.803,-99.302,79.784,-99.218,79.762,-98.945,79.724,-98.885,79.726,-98.841,79.737,-98.79,79.785,-98.769,79.851,-98.792,79.981,-98.823,80.037,-98.895,80.082,-99.017,80.111,-99.153,80.124,-99.425,80.126,-99.731,80.144,-99.803,80.14,-100.053,80.093,-100.079,80.081,-100.12,80.03,-100.126,80.001,-100.092,79.919,-100.057,79.898,-100,79.884,-99.857,79.879,-99.516,79.887]},{"b":[-91.185,77.21,-89.694,77.655],"l":[-90.172,77.595,-90.423,77.628,-90.675,77.649,-90.843,77.655,-91.019,77.644,-91.109,77.626,-91.149,77.608,-91.183,77.557,-91.185,77.482,-91.177,77.426,-91.147,77.387,-90.993,77.329,-90.228,77.212,-90.095,77.21,-89.833,77.268,-89.747,77.293,-89.712,77.31,-89.695,77.339,-89.694,77.378,-89.719,77.442,-89.839,77.491,-90.172,77.595]},{"b":[-96.6,74.637,-93.463,75.63],"l":[-93.498,75.137,-93.591,75.23,-93.667,75.274,-93.751,75.349,-93.909,75.423,-94.257,75.544,-94.427,75.593,-94.649,75.623,-94.878,75.63,-95.05,75.622,-95.671,75.529,-95.853,75.469,-95.955,75.444,-96.125,75.358,-96.118,75.301,-96.18,75.24,-96.292,75.219,-96.383,75.211,-96.566,75.099,-96.597,75.058,-96.6,75.032,-96.591,75.002,-96.56,74.99,-96.386,74.999,-96.343,74.982,-96.319,74.948,-96.294,74.927,-96.27,74.92,-96.182,74.951,-96.094,74.933,-95.865,74.83,-95.451,74.797,-95.286,74.794,-94.959,74.7,-94.804,74.66,-94.697,74.642,-94.535,74.637,-94.206,74.647,-93.985,74.644,-93.626,74.661,-93.573,74.669,-93.547,74.691,-93.548,74.728,-93.536,74.749,-93.509,74.756,-93.491,74.772,-93.463,74.856,-93.467,74.921,-93.478,74.952,-93.543,75.028,-93.552,75.051,-93.532,75.1,-93.498,75.137]},{"b":[-113.293,78.275,-110.004,78.758],"l":[-110.004,78.687,-110.14,78.704,-110.408,78.757,-110.618,78.758,-110.878,78.735,-111.071,78.708,-111.4,78.644,-111.52,78.603,-111.709,78.575,-112.214,78.548,-112.641,78.5,-112.856,78.467,-113.15,78.408,-113.282,78.353,-113.293,78.334,-113.223,78.298,-113.173,78.284,-113,78.293,-112.558,78.342,-112.131,78.366,-111.76,78.283,-111.517,78.275,-111.435,78.287,-111.3,78.337,-111.229,78.376,-111.169,78.386,-111.027,78.368,-110.84,78.322,-110.755,78.311,-110.418,78.295,-110.293,78.298,-110.022,78.323,-110.004,78.322]},{"b":[-110.004,78.304,-109.336,78.687],"l":[-110.004,78.322,-109.709,78.304,-109.484,78.316,-109.391,78.337,-109.352,78.369,-109.336,78.408,-109.342,78.456,-109.362,78.493,-109.467,78.567,-109.504,78.582,-109.581,78.593,-109.641,78.592,-109.816,78.65,-109.941,78.678,-110.004,78.687]},{"b":[-119.395,75.515,-117.499,76.115],"l":[-117.626,75.966,-117.499,76.077,-117.513,76.099,-117.634,76.115,-117.752,76.112,-117.889,76.076,-118.137,75.994,-118.379,75.958,-118.626,75.906,-119.003,75.77,-119.227,75.699,-119.32,75.663,-119.395,75.617,-119.383,75.601,-119.306,75.585,-119.087,75.569,-118.817,75.522,-118.614,75.515,-118.328,75.58,-118.227,75.611,-117.891,75.805,-117.716,75.921,-117.626,75.966]},{"b":[-106.95,72.915,-104.552,73.767],"l":[-104.835,73.647,-105.114,73.744,-105.318,73.767,-105.512,73.766,-106.362,73.719,-106.614,73.696,-106.695,73.67,-106.831,73.599,-106.95,73.51,-106.922,73.48,-106.75,73.458,-106.526,73.413,-106.18,73.304,-106.113,73.258,-106.071,73.196,-105.8,73.093,-105.573,72.989,-105.434,72.938,-105.339,72.915,-105.289,72.92,-105.201,72.947,-105.075,72.997,-105.003,73.038,-104.969,73.089,-104.791,73.168,-104.622,73.311,-104.583,73.354,-104.552,73.466,-104.555,73.541,-104.587,73.578,-104.649,73.614,-104.718,73.636,-104.835,73.647]},{"b":[-102.714,71.302,-96.44,73.946],"l":[-96.869,72.687,-97.052,72.637,-97.128,72.628,-97.159,72.643,-97.14,72.673,-97.073,72.718,-97.083,72.763,-97.238,72.837,-97.378,72.865,-97.371,72.878,-97.31,72.898,-97.296,72.918,-97.329,72.938,-97.476,72.992,-97.636,73.028,-97.725,73.037,-97.939,73.036,-98.061,73.021,-98.181,72.993,-98.367,72.934,-98.422,72.941,-98.431,72.958,-98.437,73,-98.417,73.023,-98.376,73.045,-98.176,73.116,-97.796,73.285,-97.484,73.339,-97.273,73.387,-97.23,73.421,-97.287,73.458,-97.35,73.481,-97.47,73.488,-97.532,73.474,-97.586,73.471,-97.615,73.481,-97.626,73.502,-97.597,73.537,-97.49,73.527,-97.395,73.564,-97.284,73.571,-97.156,73.592,-97.095,73.615,-97.002,73.667,-96.997,73.675,-97.011,73.706,-97.112,73.79,-97.171,73.825,-97.225,73.844,-97.327,73.862,-97.582,73.888,-97.67,73.888,-97.832,73.879,-97.928,73.866,-98.152,73.818,-98.519,73.792,-98.785,73.761,-99.04,73.749,-99.158,73.732,-100.002,73.946,-100.138,73.929,-100.227,73.889,-100.225,73.873,-100.154,73.844,-100.04,73.844,-99.94,73.857,-99.912,73.847,-99.991,73.795,-100.182,73.801,-100.484,73.844,-100.915,73.805,-100.963,73.791,-100.985,73.765,-100.982,73.727,-100.953,73.691,-100.898,73.658,-100.783,73.613,-100.658,73.593,-100.607,73.575,-100.536,73.51,-100.509,73.465,-100.522,73.449,-100.677,73.494,-100.854,73.571,-100.976,73.6,-101.115,73.596,-101.323,73.572,-101.463,73.534,-101.518,73.505,-101.523,73.486,-101.482,73.446,-101.451,73.431,-100.889,73.275,-100.755,73.278,-100.587,73.3,-100.498,73.316,-100.366,73.359,-100.258,73.34,-100.006,73.24,-99.825,73.214,-99.966,73.201,-100.067,73.211,-100.226,73.255,-100.341,73.265,-100.439,73.255,-100.489,73.234,-100.536,73.198,-100.55,73.164,-100.531,73.138,-100.446,73.121,-100.334,73.128,-100.283,73.12,-100.236,73.095,-100.184,73.055,-100.097,72.963,-100.092,72.945,-100.128,72.907,-100.188,72.89,-100.228,72.899,-100.368,72.978,-100.396,72.977,-100.443,72.807,-100.468,72.779,-100.485,72.773,-100.896,72.726,-101.088,72.713,-101.273,72.722,-101.351,72.746,-101.435,72.821,-101.544,72.883,-101.618,72.91,-101.755,72.943,-101.798,72.973,-101.835,73.018,-101.922,73.057,-102.02,73.07,-102.204,73.077,-102.336,73.064,-102.504,73.006,-102.551,72.978,-102.628,72.911,-102.687,72.843,-102.714,72.783,-102.709,72.765,-102.657,72.719,-102.402,72.595,-101.974,72.486,-101.909,72.431,-101.833,72.409,-101.804,72.385,-101.775,72.341,-101.724,72.315,-101.498,72.278,-101.319,72.313,-101.251,72.322,-101.209,72.317,-101.093,72.279,-101.026,72.229,-100.984,72.21,-100.8,72.199,-100.707,72.186,-100.594,72.152,-100.326,72.004,-100.124,71.912,-99.735,71.757,-99.581,71.652,-99.404,71.557,-99.276,71.424,-99.224,71.387,-99.167,71.367,-98.986,71.369,-98.899,71.352,-98.784,71.314,-98.663,71.302,-98.536,71.318,-98.412,71.349,-98.199,71.441,-98.19,71.462,-98.195,71.491,-98.231,71.559,-98.421,71.717,-98.459,71.773,-98.389,71.824,-98.323,71.852,-98.306,71.848,-98.303,71.831,-98.313,71.803,-98.307,71.765,-98.284,71.716,-98.242,71.681,-98.181,71.662,-97.582,71.63,-97.461,71.634,-97.222,71.673,-97.117,71.711,-97.025,71.761,-96.946,71.792,-96.613,71.834,-96.624,71.968,-96.717,72.025,-96.758,72.032,-96.766,72.046,-96.618,72.146,-96.601,72.173,-96.593,72.204,-96.616,72.237,-96.669,72.271,-96.796,72.314,-96.801,72.322,-96.746,72.323,-96.638,72.342,-96.52,72.393,-96.473,72.434,-96.44,72.487,-96.446,72.552,-96.489,72.63,-96.542,72.699,-96.592,72.71,-96.671,72.713,-96.869,72.687]},{"b":[-87.194,63.12,-80.261,65.919],"l":[-80.668,63.901,-80.579,63.909,-80.569,63.932,-80.608,63.972,-80.694,64.025,-80.829,64.09,-80.921,64.1,-81.005,64.033,-81.024,64.031,-81.104,64.037,-81.336,64.076,-81.716,64.022,-81.887,64.016,-81.903,64.031,-81.721,64.119,-81.681,64.146,-81.667,64.171,-81.676,64.213,-81.787,64.426,-81.929,64.559,-82.05,64.644,-82.159,64.691,-82.272,64.721,-82.586,64.762,-82.668,64.78,-82.991,64.904,-83.201,64.96,-83.222,64.968,-83.407,65.104,-83.491,65.132,-83.723,65.169,-83.9,65.181,-84.085,65.218,-84.133,65.245,-84.18,65.316,-84.266,65.367,-84.501,65.458,-84.568,65.461,-84.612,65.447,-84.771,65.305,-84.842,65.256,-84.885,65.249,-84.92,65.261,-85.056,65.437,-85.24,65.51,-85.243,65.526,-85.226,65.546,-85.13,65.592,-85.105,65.623,-85.13,65.693,-85.176,65.747,-85.241,65.796,-85.442,65.846,-85.496,65.9,-85.523,65.915,-85.555,65.919,-85.699,65.883,-85.814,65.832,-85.962,65.704,-86.017,65.64,-86.075,65.534,-86.114,65.417,-86.188,65.01,-86.228,64.896,-86.344,64.662,-86.374,64.566,-86.375,64.503,-86.354,64.377,-86.274,64.238,-86.252,64.181,-86.252,64.137,-86.309,64.094,-86.422,64.052,-86.886,63.924,-86.932,63.902,-87.032,63.83,-87.154,63.715,-87.189,63.672,-87.194,63.633,-87.177,63.595,-87.152,63.586,-87.053,63.572,-86.915,63.569,-86.847,63.575,-86.576,63.662,-86.302,63.657,-85.805,63.707,-85.769,63.7,-85.739,63.684,-85.714,63.658,-85.566,63.271,-85.496,63.139,-85.393,63.12,-85.238,63.139,-84.962,63.197,-84.796,63.247,-84.633,63.309,-84.555,63.35,-84.506,63.39,-84.387,63.529,-84.308,63.586,-84.26,63.6,-84.142,63.614,-84.022,63.66,-83.728,63.813,-83.662,63.873,-83.638,63.918,-83.617,64.013,-83.584,64.058,-83.494,64.099,-83.304,64.144,-83.186,64.158,-83.065,64.159,-83.016,64.127,-83.039,64.061,-83.034,64.023,-82.93,64,-82.571,63.961,-82.467,63.927,-82.412,63.737,-82.378,63.707,-82.146,63.691,-81.963,63.664,-81.372,63.538,-81.18,63.483,-81.046,63.462,-81.014,63.463,-80.954,63.48,-80.712,63.596,-80.504,63.674,-80.302,63.762,-80.261,63.802,-80.451,63.863,-80.668,63.901]},{"b":[-77.306,67.236,-75.062,68.337],"l":[-75.676,68.323,-75.867,68.337,-75.983,68.332,-76.088,68.314,-76.173,68.309,-76.364,68.319,-76.596,68.279,-76.688,68.254,-76.74,68.231,-76.945,68.091,-77.126,67.947,-77.229,67.85,-77.306,67.706,-77.304,67.685,-77.224,67.508,-77.157,67.408,-77.076,67.32,-77.005,67.267,-76.944,67.25,-76.859,67.24,-76.694,67.236,-76.333,67.258,-76.049,67.262,-75.78,67.284,-75.4,67.367,-75.315,67.4,-75.202,67.459,-75.127,67.537,-75.091,67.635,-75.086,67.751,-75.127,67.965,-75.124,67.985,-75.073,68.049,-75.062,68.075,-75.063,68.141,-75.078,68.173,-75.103,68.202,-75.154,68.234,-75.676,68.323]},{"b":[-80.861,72.756,-76.09,73.767],"l":[-80.736,73.483,-80.777,73.482,-80.798,73.472,-80.823,73.429,-80.824,73.381,-80.776,73.334,-80.727,73.305,-80.618,73.271,-80.293,73.246,-80.183,73.225,-80.146,73.161,-80.114,73.078,-80.052,72.977,-79.975,72.892,-79.937,72.864,-79.821,72.826,-79.501,72.756,-79.319,72.758,-79.134,72.772,-78.554,72.858,-78.314,72.882,-77.836,72.897,-77.014,72.844,-76.401,72.821,-76.183,72.843,-76.09,72.881,-76.135,72.912,-76.255,72.959,-76.31,72.998,-76.29,73.081,-76.331,73.1,-76.458,73.122,-76.57,73.159,-76.622,73.225,-76.657,73.254,-76.759,73.31,-77.005,73.356,-77.042,73.373,-77.12,73.45,-77.207,73.5,-77.382,73.537,-78.063,73.648,-78.287,73.666,-79.367,73.641,-79.537,73.654,-79.889,73.702,-80.12,73.707,-80.412,73.765,-80.621,73.767,-80.763,73.758,-80.823,73.743,-80.849,73.721,-80.861,73.671,-80.858,73.591,-80.827,73.535,-80.736,73.483]},{"b":[-102.798,74.984,-97.336,76.694],"l":[-97.863,75.738,-97.891,75.76,-97.694,75.803,-97.602,75.851,-97.603,75.879,-97.652,75.94,-97.65,75.979,-97.613,76.053,-97.531,76.109,-97.524,76.139,-97.531,76.182,-97.573,76.224,-97.707,76.304,-97.739,76.335,-97.737,76.363,-97.702,76.387,-97.69,76.422,-97.701,76.467,-97.726,76.496,-97.808,76.519,-97.967,76.533,-98.236,76.575,-98.289,76.599,-98.528,76.667,-98.711,76.694,-98.941,76.643,-99.024,76.615,-98.971,76.537,-98.89,76.466,-99.17,76.454,-99.329,76.521,-99.669,76.624,-99.814,76.632,-100.069,76.635,-100.388,76.614,-100.574,76.585,-100.83,76.524,-100.891,76.475,-100.874,76.457,-100.82,76.437,-100.651,76.396,-100.175,76.359,-100.082,76.343,-99.978,76.312,-99.983,76.3,-100.043,76.291,-100.358,76.271,-100.414,76.257,-100.414,76.243,-100.183,76.197,-99.998,76.196,-99.817,76.168,-99.541,76.146,-99.79,76.133,-100.002,76.139,-100.086,76.134,-100.113,76.117,-100.051,76.067,-99.978,76.029,-99.689,75.96,-99.701,75.941,-99.775,75.927,-99.865,75.924,-100.02,75.94,-100.106,75.96,-100.231,76.008,-100.9,76.207,-101.056,76.246,-101.094,76.272,-101.088,76.308,-101.139,76.345,-101.34,76.41,-101.415,76.425,-101.677,76.451,-101.788,76.451,-101.858,76.439,-101.964,76.399,-102.105,76.331,-102.138,76.285,-101.987,76.243,-101.91,76.234,-101.557,76.236,-101.529,76.217,-101.771,76.15,-101.861,76.101,-101.872,76.083,-101.823,76.041,-101.717,76.008,-101.431,75.992,-101.508,75.944,-101.506,75.918,-101.47,75.882,-101.415,75.846,-101.288,75.789,-101.259,75.784,-101.01,75.802,-100.973,75.798,-101.119,75.763,-101.261,75.758,-101.421,75.782,-101.6,75.833,-101.943,75.884,-102.145,75.875,-102.271,75.813,-102.252,75.778,-102.411,75.713,-102.728,75.639,-102.798,75.6,-102.7,75.544,-102.587,75.514,-102.541,75.514,-101.461,75.608,-101.207,75.59,-100.902,75.62,-99.915,75.681,-99.195,75.698,-99.209,75.669,-99.591,75.655,-99.756,75.633,-99.77,75.612,-99.965,75.569,-100.28,75.461,-100.712,75.406,-100.704,75.394,-100.731,75.347,-100.615,75.321,-100.364,75.29,-100.146,75.246,-100.152,75.236,-100.459,75.219,-100.483,75.188,-100.357,75.067,-100.292,75.028,-100.234,75.008,-99.947,75.003,-99.627,74.984,-99.421,75.044,-99.326,75.049,-99.245,75.026,-99.156,75.016,-99.01,75.021,-98.835,75.018,-98.704,75.006,-98.569,75.009,-98.295,75.032,-98.121,75.033,-97.992,75.046,-97.953,75.06,-97.99,75.111,-98.077,75.153,-98.092,75.176,-98.069,75.199,-98.045,75.201,-97.971,75.153,-97.843,75.122,-97.799,75.117,-97.674,75.127,-97.66,75.151,-97.705,75.191,-97.853,75.26,-97.878,75.416,-97.653,75.508,-97.465,75.459,-97.363,75.417,-97.336,75.42,-97.41,75.552,-97.408,75.673,-97.44,75.685,-97.863,75.738]},{"b":[-105.58,77.794,-99,79.352],"l":[-104.901,79.051,-104.747,79.027,-104.735,78.991,-104.893,78.89,-104.97,78.856,-104.97,78.829,-104.896,78.808,-104.817,78.807,-104.736,78.826,-104.395,78.956,-104.152,78.99,-104.113,78.986,-104.007,78.948,-103.887,78.919,-103.876,78.903,-104.155,78.814,-104.195,78.796,-104.185,78.781,-104.009,78.764,-103.518,78.769,-103.408,78.752,-103.372,78.736,-103.563,78.693,-103.929,78.663,-104.021,78.635,-103.588,78.623,-103.483,78.594,-103.571,78.54,-103.764,78.52,-104.214,78.54,-104.727,78.579,-104.82,78.573,-104.91,78.553,-104.996,78.519,-104.985,78.468,-104.879,78.401,-104.764,78.352,-104.513,78.295,-104.324,78.269,-103.947,78.26,-103.677,78.32,-102.731,78.371,-102.784,78.33,-102.772,78.307,-102.723,78.275,-102.668,78.256,-102.607,78.249,-102.284,78.275,-102.057,78.28,-101.829,78.264,-101.298,78.199,-101.074,78.194,-100.958,78.13,-100.826,78.088,-100.81,78.072,-100.778,77.996,-100.758,77.978,-100.68,77.931,-100.586,77.892,-100.275,77.833,-99.956,77.794,-99.659,77.824,-99.341,77.84,-99.166,77.857,-99.128,77.877,-99.061,77.966,-99,77.997,-99.005,78.016,-99.053,78.072,-99.132,78.118,-99.562,78.279,-99.751,78.303,-99.778,78.325,-99.769,78.365,-99.774,78.393,-99.848,78.438,-99.818,78.455,-99.68,78.494,-99.631,78.545,-99.582,78.563,-99.609,78.583,-99.782,78.62,-100.015,78.729,-100.435,78.82,-100.917,78.783,-101.128,78.802,-101.147,78.824,-101.116,78.858,-101.034,78.915,-101.037,78.939,-101.088,78.962,-101.145,78.973,-101.299,78.982,-101.704,79.079,-101.873,79.088,-101.974,79.079,-102.189,79.038,-102.393,79.01,-102.407,78.954,-102.425,78.933,-102.495,78.901,-102.576,78.879,-102.593,78.901,-102.581,78.93,-102.596,78.943,-102.731,78.969,-102.683,78.991,-102.648,79.027,-102.638,79.055,-102.639,79.078,-102.652,79.095,-102.914,79.231,-103.192,79.295,-103.426,79.316,-103.706,79.352,-103.965,79.348,-104.847,79.311,-105.388,79.324,-105.436,79.302,-105.515,79.242,-105.571,79.164,-105.58,79.114,-105.571,79.061,-105.536,79.033,-105.309,79.033,-104.901,79.051]},{"b":[-96.773,78.15,-85.042,81.364],"l":[-91.755,81.049,-91.886,81.133,-91.998,81.185,-92.212,81.244,-92.413,81.278,-93.035,81.346,-93.333,81.364,-93.605,81.351,-94.06,81.349,-94.179,81.339,-94.22,81.331,-94.233,81.315,-94.231,81.29,-94.219,81.265,-94.194,81.241,-94.11,81.225,-93.894,81.213,-93.407,81.209,-93.286,81.179,-93.235,81.155,-93.236,81.129,-93.287,81.1,-93.345,81.085,-93.444,81.083,-93.826,81.106,-94.216,81.057,-94.519,81.031,-94.981,81.05,-95.27,81.001,-95.509,80.863,-95.515,80.838,-95.196,80.808,-94.788,80.751,-94.596,80.641,-94.202,80.61,-94.029,80.586,-93.928,80.559,-94.485,80.558,-94.734,80.572,-94.893,80.571,-95.026,80.646,-95.226,80.686,-95.505,80.691,-95.714,80.725,-95.927,80.721,-96.133,80.691,-96.152,80.553,-95.901,80.471,-95.614,80.396,-95.549,80.367,-95.747,80.365,-96.012,80.383,-96.112,80.38,-96.334,80.353,-96.385,80.333,-96.394,80.315,-96.368,80.293,-96.308,80.267,-96.215,80.246,-96.026,80.222,-95.904,80.214,-95.646,80.231,-95.405,80.135,-95.192,80.134,-94.59,80.202,-94.263,80.195,-94.304,80.182,-94.583,80.141,-94.607,80.126,-94.613,80.103,-94.6,80.074,-94.611,80.056,-94.646,80.049,-95.394,80.053,-95.782,80.066,-96.773,80.136,-96.639,80.024,-96.607,79.978,-96.589,79.917,-96.463,79.848,-96,79.705,-95.858,79.674,-95.739,79.66,-95.552,79.653,-95.297,79.653,-94.973,79.677,-94.581,79.726,-94.402,79.736,-94.476,79.686,-94.52,79.667,-95.302,79.568,-95.563,79.55,-95.663,79.527,-95.733,79.418,-95.657,79.39,-95.317,79.355,-95.103,79.29,-95.044,79.294,-94.846,79.335,-94.405,79.391,-94.284,79.4,-94.11,79.402,-93.96,79.396,-93.94,79.386,-94.04,79.357,-94.109,79.315,-94.093,79.303,-94.04,79.295,-93.933,79.291,-93.55,79.354,-93.381,79.368,-93.028,79.429,-92.822,79.45,-92.645,79.45,-92.485,79.439,-92.248,79.373,-91.693,79.365,-91.3,79.373,-91.344,79.361,-91.868,79.317,-92.547,79.283,-92.684,79.186,-92.842,79.156,-93.068,79.155,-93.294,79.14,-93.95,79.037,-94.163,78.994,-94.17,78.973,-94.154,78.951,-94.115,78.929,-93.902,78.872,-93.336,78.808,-93.16,78.776,-93.208,78.769,-93.561,78.777,-93.623,78.768,-93.634,78.751,-93.552,78.708,-93.389,78.643,-93.267,78.608,-93.109,78.602,-92.973,78.613,-92.716,78.605,-91.935,78.562,-91.867,78.543,-92.297,78.521,-92.726,78.487,-92.848,78.46,-92.808,78.43,-92.678,78.389,-92.351,78.313,-91.899,78.237,-91.41,78.188,-90.918,78.158,-90.614,78.15,-90.387,78.163,-90.327,78.185,-90.358,78.219,-90.405,78.247,-90.469,78.269,-90.652,78.308,-90.622,78.322,-90.459,78.331,-90.297,78.328,-90.136,78.313,-90.025,78.291,-89.965,78.262,-89.873,78.238,-89.651,78.193,-89.579,78.167,-89.526,78.16,-89.49,78.172,-89.507,78.203,-89.612,78.279,-89.757,78.37,-90.001,78.496,-90.076,78.549,-90.037,78.607,-89.995,78.601,-89.926,78.573,-89.655,78.439,-89.47,78.37,-89.096,78.209,-88.97,78.184,-88.822,78.186,-88.791,78.192,-88.733,78.242,-88.648,78.334,-88.606,78.392,-88.623,78.462,-88.714,78.546,-88.742,78.584,-88.709,78.596,-88.581,78.602,-88.285,78.497,-88.148,78.477,-88.04,78.494,-87.983,78.537,-87.974,78.565,-87.982,78.595,-88.003,78.616,-88.037,78.627,-88.228,78.653,-88.254,78.672,-88.19,78.696,-88.167,78.746,-88.19,78.867,-88.164,78.933,-88.104,78.973,-88.04,78.995,-87.878,79.038,-87.829,79.045,-87.817,79.036,-87.923,78.951,-87.953,78.915,-87.961,78.893,-87.956,78.852,-87.922,78.751,-87.861,78.707,-87.617,78.676,-87.479,78.718,-87.246,78.813,-87.08,78.866,-87.016,78.899,-86.957,78.975,-86.913,78.983,-86.721,78.975,-86.629,78.991,-86.451,79.039,-86.092,79.1,-85.29,79.208,-85.181,79.234,-85.042,79.285,-85.064,79.328,-85.176,79.387,-85.501,79.53,-85.648,79.611,-85.679,79.615,-85.751,79.595,-85.804,79.573,-85.949,79.486,-86.007,79.479,-86.086,79.551,-86.18,79.605,-86.232,79.622,-86.337,79.635,-86.649,79.646,-86.861,79.598,-86.925,79.591,-87.243,79.571,-87.295,79.58,-87.22,79.63,-87.144,79.663,-87.05,79.805,-86.977,79.894,-87.076,79.967,-87.202,80.043,-87.329,80.047,-87.651,80.079,-87.861,80.088,-87.922,80.098,-87.869,80.134,-87.625,80.187,-87.618,80.207,-87.63,80.302,-87.646,80.348,-87.675,80.372,-87.96,80.416,-88.125,80.429,-88.424,80.428,-88.525,80.418,-88.644,80.387,-88.663,80.348,-88.646,80.29,-88.612,80.255,-88.381,80.225,-88.255,80.167,-88.197,80.125,-88.2,80.111,-88.329,80.134,-88.538,80.131,-88.857,80.166,-89.019,80.198,-89.198,80.263,-89.218,80.289,-89.147,80.36,-89.155,80.379,-89.197,80.394,-89.204,80.407,-89.134,80.44,-89.138,80.457,-89.167,80.48,-89.236,80.511,-89.329,80.532,-89.525,80.539,-89.674,80.531,-89.798,80.501,-89.862,80.498,-90.218,80.548,-90.537,80.576,-90.643,80.594,-90.632,80.642,-90.637,80.655,-90.683,80.688,-91.054,80.778,-91.272,80.85,-91.755,81.049]},{"b":[-96.898,74.47,-79.401,77.066],"l":[-87.593,74.47,-87.364,74.502,-86.995,74.48,-86.77,74.479,-86.666,74.489,-86.731,74.557,-86.655,74.555,-86.341,74.513,-86.211,74.536,-86.11,74.54,-85.956,74.499,-85.808,74.499,-85.544,74.535,-85.512,74.545,-85.489,74.567,-85.474,74.6,-85.442,74.601,-85.339,74.543,-85.214,74.519,-85.133,74.517,-85.087,74.528,-85.061,74.607,-85.012,74.604,-84.916,74.568,-84.818,74.542,-84.667,74.52,-84.426,74.508,-84.245,74.515,-83.868,74.564,-83.622,74.566,-83.532,74.586,-83.412,74.655,-83.394,74.67,-83.341,74.765,-83.364,74.802,-83.487,74.834,-83.51,74.848,-83.544,74.892,-83.522,74.901,-83.407,74.885,-83.22,74.828,-83.158,74.817,-83.103,74.817,-83.087,74.788,-83.112,74.732,-83.117,74.693,-83.058,74.63,-82.978,74.583,-82.931,74.566,-82.736,74.53,-82.645,74.525,-82.415,74.535,-82.069,74.482,-81.94,74.473,-81.809,74.477,-81.607,74.502,-81.34,74.554,-81.226,74.567,-80.278,74.582,-80.263,74.584,-80.221,74.657,-80.21,74.704,-80.213,74.749,-80.192,74.78,-80.149,74.796,-80.19,74.828,-80.315,74.876,-80.348,74.903,-80.289,74.908,-80.202,74.895,-79.944,74.834,-79.508,74.88,-79.401,74.918,-79.46,74.959,-79.525,74.99,-79.664,75.021,-79.733,75.021,-80.036,74.991,-80.135,74.988,-80.261,75.002,-80.382,75.034,-80.358,75.052,-79.977,75.119,-79.634,75.199,-79.509,75.26,-79.508,75.295,-79.586,75.385,-79.66,75.45,-79.738,75.461,-80.1,75.467,-80.26,75.479,-80.287,75.49,-80.126,75.542,-80.119,75.562,-80.158,75.581,-80.322,75.629,-80.528,75.642,-81.001,75.643,-81.124,75.658,-81.174,75.669,-81.193,75.684,-81.151,75.736,-81.269,75.756,-81.647,75.795,-82.154,75.831,-82.354,75.833,-82.553,75.818,-83.093,75.756,-83.237,75.751,-83.745,75.813,-83.932,75.819,-84.014,75.78,-84.128,75.763,-84.605,75.653,-84.75,75.655,-84.987,75.645,-85.372,75.573,-85.581,75.58,-85.973,75.529,-86.069,75.502,-85.905,75.442,-85.951,75.395,-86.236,75.406,-86.437,75.436,-86.545,75.463,-86.814,75.491,-87.257,75.618,-87.365,75.591,-87.539,75.485,-87.572,75.494,-87.644,75.547,-87.73,75.576,-88.201,75.512,-88.569,75.645,-88.645,75.658,-88.715,75.659,-88.784,75.647,-88.852,75.625,-88.864,75.589,-88.82,75.539,-88.804,75.502,-88.839,75.463,-88.869,75.452,-88.917,75.454,-89.28,75.564,-89.337,75.572,-89.646,75.565,-89.625,75.584,-89.361,75.646,-89.257,75.698,-89.205,75.737,-89.205,75.762,-89.278,75.795,-89.511,75.857,-89.65,75.844,-89.695,75.854,-89.793,75.925,-89.913,75.966,-90.033,75.971,-90.176,76.03,-90.251,76.053,-90.712,76.076,-90.802,76.106,-91.02,76.142,-91.279,76.16,-91.407,76.22,-91.26,76.23,-90.827,76.186,-90.312,76.158,-89.407,76.189,-89.292,76.218,-89.237,76.239,-89.219,76.258,-89.285,76.302,-90.855,76.437,-91.334,76.446,-91.415,76.456,-91.443,76.499,-91.398,76.51,-91.336,76.511,-91.263,76.5,-90.864,76.484,-90.622,76.465,-90.543,76.496,-90.555,76.516,-90.605,76.543,-90.738,76.581,-91.124,76.662,-91.305,76.681,-91.548,76.685,-91.789,76.676,-92.297,76.616,-92.716,76.603,-92.995,76.62,-93.422,76.474,-93.535,76.448,-93.485,76.492,-93.426,76.527,-93.317,76.574,-93.264,76.626,-93.201,76.669,-93.19,76.686,-93.189,76.708,-93.212,76.755,-93.23,76.77,-93.277,76.784,-93.421,76.812,-93.608,76.874,-93.811,76.914,-93.948,76.917,-94.108,76.904,-94.295,76.912,-94.616,76.958,-95.126,77.017,-95.638,77.064,-95.85,77.066,-96.061,77.05,-96.377,77.005,-96.55,76.988,-96.685,76.985,-96.758,76.972,-96.77,76.948,-96.814,76.913,-96.771,76.889,-96.661,76.855,-96.433,76.811,-96.402,76.797,-96.451,76.774,-96.59,76.763,-96.679,76.766,-96.878,76.803,-96.898,76.773,-96.898,76.754,-96.881,76.738,-96.846,76.726,-96.64,76.703,-95.971,76.57,-95.873,76.566,-95.651,76.585,-95.696,76.563,-95.789,76.537,-96.013,76.513,-96.04,76.487,-95.959,76.446,-95.842,76.416,-95.447,76.363,-95.274,76.264,-94.997,76.258,-94.737,76.293,-94.585,76.297,-94.383,76.282,-93.852,76.27,-93.665,76.273,-93.644,76.289,-93.56,76.311,-93.309,76.36,-93.192,76.366,-93.092,76.354,-92.883,76.214,-92.709,76.114,-92.474,75.986,-92.307,75.915,-92.185,75.847,-92.142,75.797,-92.099,75.727,-92.069,75.658,-92.081,75.634,-92.11,75.611,-92.331,75.479,-92.411,75.406,-92.428,75.383,-92.427,75.346,-92.408,75.297,-92.389,75.263,-92.347,75.23,-92.207,75.181,-92.076,75.124,-92.06,75.101,-92.165,75.072,-92.174,75.051,-92.103,74.948,-91.962,74.793,-91.871,74.744,-91.666,74.699,-91.549,74.656,-91.508,74.651,-91.339,74.667,-91.168,74.646,-91.135,74.65,-91.164,74.71,-91.13,74.736,-90.88,74.818,-90.878,74.801,-90.958,74.745,-90.967,74.715,-90.784,74.696,-90.553,74.613,-90.362,74.61,-90.015,74.561,-89.844,74.549,-89.559,74.555,-89.45,74.568,-89.262,74.609,-89.196,74.638,-89.189,74.667,-89.197,74.698,-89.219,74.732,-89.192,74.745,-89.115,74.738,-89.058,74.747,-89.02,74.774,-88.98,74.788,-88.94,74.79,-88.908,74.764,-88.883,74.711,-88.851,74.69,-88.778,74.715,-88.682,74.802,-88.535,74.832,-88.488,74.829,-88.431,74.804,-88.34,74.785,-88.375,74.744,-88.477,74.667,-88.538,74.609,-88.558,74.57,-88.556,74.541,-88.501,74.51,-88.423,74.494,-88.006,74.489,-87.593,74.47]},{"b":[-98.341,77.806,-94.887,78.805],"l":[-96.603,77.849,-96.477,77.872,-96.012,77.887,-95.671,77.924,-95.452,77.963,-95.371,77.971,-95.199,77.968,-95.087,77.993,-94.934,78.076,-94.936,78.106,-94.988,78.136,-95.103,78.178,-95.329,78.225,-95.268,78.263,-95.014,78.313,-94.887,78.345,-94.888,78.36,-94.915,78.391,-95.031,78.43,-95.413,78.498,-95.561,78.517,-95.968,78.505,-96.204,78.531,-96.256,78.551,-96.243,78.573,-96.265,78.595,-96.475,78.665,-96.587,78.687,-96.936,78.72,-97.169,78.758,-97.382,78.783,-97.596,78.796,-98.043,78.805,-98.212,78.805,-98.333,78.774,-98.341,78.751,-98.29,78.692,-98.096,78.587,-98.06,78.558,-98.316,78.517,-98.324,78.498,-98.317,78.477,-98.276,78.438,-98.255,78.429,-98.114,78.403,-98.069,78.386,-98.05,78.326,-97.843,78.262,-97.819,78.231,-97.323,78.203,-97.027,78.157,-96.945,78.152,-96.958,78.139,-97.041,78.117,-97.227,78.103,-97.658,78.091,-97.648,78.072,-97.621,78.05,-97.427,77.982,-97.093,77.933,-97.019,77.908,-97.052,77.881,-97.064,77.859,-97.04,77.827,-96.99,77.806,-96.834,77.812,-96.603,77.849]},{"b":[-113.283,77.344,-110.004,78.103],"l":[-110.004,78.09,-110.458,78.103,-110.727,78.097,-110.873,78.081,-111.207,78.088,-112.305,78.007,-112.805,77.942,-113.022,77.919,-113.187,77.912,-113.215,77.904,-113.269,77.86,-113.283,77.836,-113.283,77.813,-113.271,77.778,-113.19,77.718,-113.168,77.676,-113.121,77.633,-113.137,77.618,-113.189,77.6,-113.209,77.58,-113.197,77.559,-113.164,77.53,-113.046,77.511,-112.926,77.475,-112.644,77.444,-112.373,77.364,-112.177,77.344,-111.952,77.344,-111.226,77.429,-111.06,77.433,-110.894,77.426,-110.683,77.446,-110.372,77.491,-110.198,77.525,-110.139,77.572,-110.117,77.625,-110.118,77.716,-110.131,77.742,-110.153,77.763,-110.189,77.777,-110.292,77.786,-110.719,77.781,-110.812,77.803,-110.857,77.82,-110.866,77.834,-110.751,77.857,-110.2,77.905,-110.004,77.929]},{"b":[-110.004,77.929,-109.619,78.09],"l":[-110.004,77.929,-109.772,77.957,-109.679,77.999,-109.619,78.057,-109.622,78.075,-109.657,78.079,-110.004,78.09]},{"b":[-122.903,75.825,-115.47,77.548],"l":[-118.644,76.418,-118.8,76.464,-118.821,76.486,-118.791,76.513,-118.732,76.526,-118.574,76.525,-118.468,76.547,-118.431,76.588,-118.409,76.662,-118.37,76.701,-118.301,76.737,-118.203,76.76,-118.076,76.772,-117.881,76.805,-117.818,76.804,-117.78,76.784,-117.808,76.734,-117.9,76.653,-117.965,76.574,-118.005,76.497,-118.02,76.447,-117.993,76.406,-117.841,76.345,-117.732,76.317,-117.492,76.273,-117.347,76.273,-117.234,76.282,-117.154,76.298,-117.108,76.322,-117.044,76.373,-117.026,76.404,-117.013,76.469,-117.017,76.496,-116.999,76.532,-116.468,76.577,-116.22,76.611,-116.076,76.654,-115.985,76.687,-115.946,76.711,-115.945,76.736,-116.016,76.785,-116.234,76.874,-116.253,76.901,-116.183,76.916,-116.11,76.918,-115.913,76.908,-115.81,76.939,-115.857,76.969,-116.073,77.03,-116.286,77.102,-116.329,77.137,-116.214,77.178,-115.624,77.266,-115.507,77.292,-115.47,77.309,-115.476,77.324,-115.551,77.363,-116.008,77.461,-116.209,77.516,-116.363,77.543,-116.511,77.548,-116.835,77.529,-116.947,77.504,-117.04,77.465,-117.046,77.449,-117.03,77.432,-116.766,77.398,-116.704,77.38,-116.795,77.347,-116.844,77.34,-117.061,77.348,-117.149,77.361,-117.211,77.331,-117.279,77.313,-117.419,77.317,-118.005,77.381,-118.82,77.333,-119.09,77.305,-119.324,77.241,-119.495,77.177,-119.831,77.074,-120.2,76.931,-120.311,76.905,-120.358,76.887,-120.438,76.816,-120.485,76.793,-120.998,76.691,-121.102,76.661,-121.204,76.622,-121.561,76.453,-121.614,76.441,-122.365,76.401,-122.423,76.39,-122.519,76.353,-122.774,76.228,-122.878,76.165,-122.903,76.135,-122.685,76.162,-122.624,76.167,-122.593,76.162,-122.588,76.153,-122.609,76.14,-122.609,76.121,-122.548,76.097,-122.546,76.081,-122.607,76.039,-122.646,76.031,-122.64,76.009,-122.591,75.973,-122.533,75.951,-122.4,75.944,-122.303,75.96,-122.057,76.018,-121.908,76.035,-121.695,76.02,-121.428,75.981,-121.32,75.977,-121.213,75.984,-121.019,76.02,-120.9,76.163,-120.848,76.183,-120.813,76.179,-120.772,76.166,-120.729,76.134,-120.637,76.034,-120.563,76.008,-120.514,75.958,-120.458,75.87,-120.409,75.826,-120.365,75.825,-120.161,75.852,-119.913,75.859,-119.735,75.915,-119.667,75.946,-119.608,75.985,-119.538,75.982,-119.526,75.997,-119.527,76.031,-119.55,76.052,-119.725,76.1,-119.74,76.118,-119.64,76.157,-119.636,76.19,-119.651,76.244,-119.649,76.28,-119.58,76.327,-119.524,76.34,-119.489,76.32,-119.448,76.275,-119.368,76.222,-119.249,76.159,-119.168,76.127,-119.081,76.124,-118.994,76.145,-118.955,76.168,-118.851,76.258,-118.812,76.277,-118.643,76.335,-118.625,76.366,-118.644,76.418]},{"b":[-117.6,74.402,-110.003,76.506],"l":[-110.003,75.539,-110.459,75.555,-110.726,75.56,-110.89,75.547,-111.053,75.549,-111.163,75.57,-111.276,75.613,-111.373,75.676,-111.454,75.762,-111.513,75.811,-111.549,75.822,-111.709,75.832,-111.877,75.826,-112.057,75.834,-112.081,75.847,-112.047,75.866,-111.868,75.911,-111.865,75.939,-112.334,76.072,-112.698,76.202,-112.978,76.245,-113.171,76.258,-113.363,76.248,-113.823,76.207,-113.923,76.229,-114.059,76.301,-114.101,76.331,-114.113,76.349,-114.116,76.396,-114.141,76.423,-114.194,76.451,-114.299,76.475,-114.535,76.502,-114.767,76.506,-114.998,76.497,-115.581,76.438,-115.779,76.365,-115.826,76.33,-115.832,76.296,-115.822,76.27,-115.797,76.253,-115.664,76.24,-115.025,76.211,-114.88,76.195,-114.779,76.173,-114.939,76.166,-115.768,76.184,-116.059,76.202,-116.21,76.194,-116.454,76.143,-116.591,76.096,-116.61,76.074,-116.55,76.017,-116.58,75.992,-116.665,75.958,-116.654,75.929,-116.444,75.891,-116.338,75.881,-115.602,75.895,-114.992,75.896,-115.174,75.867,-115.477,75.841,-115.838,75.841,-116.39,75.808,-116.802,75.772,-116.973,75.746,-117.039,75.718,-117.164,75.645,-117.138,75.617,-117.025,75.602,-116.426,75.585,-116.034,75.607,-115.122,75.706,-115.117,75.695,-115.142,75.679,-115.251,75.639,-115.335,75.618,-116.077,75.493,-116.213,75.483,-116.891,75.481,-117.154,75.473,-117.258,75.46,-117.336,75.442,-117.388,75.421,-117.513,75.357,-117.576,75.314,-117.597,75.293,-117.6,75.272,-117.565,75.233,-117.502,75.204,-117.005,75.156,-116.841,75.152,-116.476,75.172,-116.143,75.042,-115.729,74.968,-115.683,74.974,-115.609,75.01,-115.574,75.056,-115.537,75.081,-115.478,75.104,-115.413,75.115,-115.343,75.113,-115.28,75.102,-115.174,75.049,-115.128,75.009,-115.077,74.985,-115.02,74.976,-114.859,75,-114.452,75.088,-114.356,75.141,-114.358,75.171,-114.504,75.258,-114.514,75.275,-114.483,75.285,-114.429,75.281,-114.285,75.25,-114.168,75.24,-114.125,75.291,-114.075,75.392,-114.053,75.417,-114.017,75.434,-113.984,75.43,-113.916,75.388,-113.879,75.375,-113.589,75.412,-113.467,75.416,-113.503,75.397,-113.759,75.322,-113.811,75.296,-113.853,75.259,-113.886,75.211,-113.861,75.188,-113.855,75.129,-113.845,75.112,-113.795,75.084,-113.712,75.069,-113.34,75.093,-112.951,75.108,-112.855,75.121,-112.8,75.138,-112.703,75.187,-112.652,75.205,-112.597,75.212,-112.478,75.2,-112.256,75.134,-112.214,75.133,-112,75.142,-111.781,75.166,-111.621,75.168,-111.474,75.191,-111.181,75.26,-111.093,75.256,-111.033,75.227,-111.079,75.195,-111.258,75.128,-111.503,75.056,-111.671,75.019,-111.784,75.006,-111.956,75,-112.193,75.01,-112.663,74.994,-112.836,74.976,-113.324,74.875,-113.863,74.813,-114.132,74.766,-114.313,74.715,-114.377,74.671,-114.268,74.604,-114.175,74.574,-113.837,74.489,-113.672,74.453,-113.514,74.43,-113.017,74.402,-112.519,74.417,-111.729,74.502,-111.288,74.585,-110.941,74.639,-110.749,74.688,-110.625,74.753,-110.543,74.78,-110.387,74.814,-110.176,74.84,-110.003,74.851]},{"b":[-110.003,75.506,-108.9,76.244],"l":[-110.003,76.244,-109.908,76.223,-109.71,76.212,-109.487,76.145,-109.43,76.109,-109.417,76.072,-109.425,76.043,-109.455,76.021,-109.871,75.929,-109.796,75.863,-108.945,75.699,-108.918,75.675,-108.9,75.624,-108.913,75.587,-108.947,75.542,-109.005,75.515,-109.086,75.506,-110.003,75.539]},{"b":[-110.314,76.244,-110.003,76.48],"l":[-110.003,76.48,-110.27,76.417,-110.309,76.397,-110.314,76.369,-110.285,76.333,-110.247,76.306,-110.201,76.289,-110.003,76.244]},{"b":[-110.003,74.851,-105.481,76.821],"l":[-110.003,74.851,-109.503,74.883,-109.003,75.01,-108.831,75.065,-108.666,75.04,-108.633,75.023,-108.67,75.007,-108.751,74.992,-108.594,74.96,-108.475,74.947,-108.354,74.943,-108.227,74.952,-108.024,74.986,-107.82,75,-107.462,74.952,-107.153,74.927,-107.056,74.928,-106.961,74.94,-106.588,75.015,-106.093,75.089,-105.972,75.131,-105.863,75.192,-105.703,75.413,-105.678,75.501,-105.519,75.632,-105.481,75.702,-105.481,75.746,-105.563,75.881,-105.604,75.93,-105.633,75.945,-105.711,75.967,-105.905,76.009,-106.397,76.06,-106.529,76.053,-106.677,76.024,-106.804,75.975,-106.846,75.952,-106.862,75.93,-106.82,75.872,-106.759,75.842,-106.688,75.819,-106.693,75.81,-106.892,75.782,-106.903,75.742,-106.904,75.689,-106.914,75.68,-106.971,75.773,-107.05,75.845,-107.08,75.863,-107.136,75.879,-107.216,75.892,-107.418,75.907,-107.541,75.901,-107.703,75.878,-107.918,75.802,-107.951,75.796,-108.021,75.805,-107.97,75.84,-107.755,75.94,-107.732,75.956,-107.721,75.974,-107.723,75.995,-107.777,76.035,-107.852,76.058,-108.019,76.065,-108.166,76.054,-108.292,76.057,-108.387,76.067,-108.406,76.085,-108.382,76.116,-108.306,76.154,-108.178,76.2,-108.123,76.233,-108.194,76.33,-108.345,76.392,-108.512,76.439,-108.524,76.447,-108.539,76.503,-108.56,76.536,-108.628,76.587,-108.635,76.609,-108.612,76.63,-108.512,76.68,-108.478,76.708,-108.467,76.738,-108.492,76.754,-108.554,76.758,-108.832,76.821,-109.098,76.812,-109.22,76.792,-109.339,76.76,-109.505,76.692,-109.865,76.522,-109.982,76.485,-110.003,76.48]},{"b":[-95.648,72.001,-90.355,74.179],"l":[-91.088,74.009,-91.63,74.028,-91.874,74.013,-92.223,73.972,-92.314,73.992,-92.493,74.062,-92.587,74.083,-92.778,74.114,-93.171,74.161,-93.41,74.179,-93.549,74.167,-93.785,74.118,-93.939,74.132,-94.483,74.113,-94.729,74.086,-94.974,74.041,-95.04,74.024,-95.121,73.985,-95.145,73.96,-95.153,73.933,-95.149,73.906,-95.134,73.881,-95.059,73.805,-94.897,73.716,-94.797,73.686,-94.691,73.671,-94.698,73.664,-94.817,73.663,-94.996,73.686,-95.386,73.755,-95.447,73.752,-95.569,73.728,-95.633,73.695,-95.645,73.671,-95.648,73.639,-95.644,73.557,-95.604,73.328,-95.589,73.174,-95.592,73.115,-95.612,72.999,-95.613,72.942,-95.602,72.884,-95.58,72.831,-95.548,72.782,-95.251,72.502,-95.193,72.345,-95.167,72.18,-95.193,72.027,-95.008,72.013,-94.611,72.042,-94.497,72.044,-94.144,72.001,-94.038,72.029,-93.973,72.13,-93.871,72.253,-93.555,72.421,-93.542,72.437,-93.534,72.499,-93.546,72.531,-93.572,72.559,-93.771,72.668,-93.92,72.703,-94.152,72.736,-94.211,72.757,-93.579,72.801,-93.341,72.802,-92.392,72.718,-92.235,72.727,-92.118,72.754,-91.905,72.849,-91.788,72.915,-91.621,73.026,-91.46,73.145,-91.426,73.195,-91.466,73.214,-91.554,73.236,-91.298,73.285,-91.249,73.304,-91.068,73.416,-91.002,73.467,-90.975,73.502,-90.934,73.528,-90.765,73.581,-90.566,73.686,-90.466,73.754,-90.381,73.825,-90.355,73.869,-90.458,73.908,-90.627,73.952,-91.088,74.009]},{"b":[-99.564,68.461,-95.268,69.862],"l":[-95.585,68.835,-95.751,68.898,-95.855,68.954,-95.951,69.024,-96.061,69.125,-96.184,69.259,-96.3,69.344,-96.695,69.471,-96.875,69.51,-96.989,69.554,-97.096,69.615,-97.236,69.673,-97.278,69.68,-97.306,69.673,-97.351,69.641,-97.409,69.631,-97.439,69.643,-97.469,69.667,-97.46,69.683,-97.386,69.7,-97.383,69.712,-97.411,69.738,-97.604,69.802,-97.691,69.841,-97.791,69.862,-97.889,69.858,-98.081,69.833,-98.2,69.797,-98.239,69.78,-98.268,69.754,-98.301,69.692,-98.304,69.669,-98.289,69.629,-98.163,69.512,-98.041,69.457,-98.156,69.469,-98.222,69.485,-98.389,69.565,-98.476,69.579,-98.546,69.573,-98.548,69.545,-98.534,69.527,-98.495,69.499,-98.448,69.48,-98.537,69.478,-98.559,69.461,-98.535,69.426,-98.467,69.375,-98.45,69.354,-98.456,69.335,-98.504,69.308,-98.724,69.219,-98.912,69.168,-99.085,69.15,-99.456,69.131,-99.513,69.1,-99.557,69.054,-99.564,69.034,-99.495,68.96,-99.441,68.918,-99.318,68.876,-99.254,68.863,-99.091,68.863,-99.073,68.877,-99.094,68.899,-99.057,68.918,-98.964,68.933,-98.904,68.932,-98.879,68.916,-98.864,68.894,-98.859,68.864,-98.83,68.839,-98.775,68.817,-98.704,68.803,-98.54,68.798,-98.432,68.818,-98.376,68.842,-98.321,68.843,-98.296,68.831,-98.28,68.807,-98.273,68.772,-98.258,68.749,-98.235,68.739,-97.885,68.672,-97.705,68.626,-97.472,68.544,-97.264,68.528,-97.008,68.539,-96.599,68.461,-96.402,68.471,-96.268,68.508,-96.024,68.607,-95.895,68.627,-95.802,68.686,-95.686,68.736,-95.614,68.745,-95.466,68.747,-95.359,68.778,-95.295,68.805,-95.268,68.826,-95.32,68.873,-95.374,68.892,-95.438,68.881,-95.585,68.835]},{"b":[-118.994,69.65,-110.002,73.373],"l":[-110.002,72.982,-110.008,72.984,-110.095,72.992,-110.509,72.999,-110.661,73.008,-110.689,72.945,-110.554,72.861,-110.279,72.792,-110.197,72.759,-110.208,72.681,-110.205,72.661,-110.439,72.633,-110.513,72.6,-110.782,72.534,-110.959,72.432,-111.14,72.365,-111.185,72.357,-111.268,72.364,-111.284,72.378,-111.287,72.401,-111.277,72.425,-111.253,72.449,-111.265,72.459,-111.311,72.455,-111.447,72.408,-111.544,72.351,-111.675,72.3,-111.762,72.335,-111.895,72.356,-111.816,72.386,-111.611,72.436,-111.356,72.572,-111.25,72.669,-111.27,72.714,-111.455,72.766,-112.048,72.888,-112.454,72.937,-112.754,72.986,-113.074,72.995,-113.208,72.981,-113.292,72.95,-113.45,72.863,-113.491,72.822,-113.496,72.754,-113.486,72.722,-113.5,72.694,-113.578,72.652,-113.622,72.647,-113.692,72.673,-113.958,72.651,-114.174,72.624,-114.342,72.591,-114.458,72.58,-114.522,72.593,-114.498,72.626,-114.28,72.739,-114.178,72.805,-114.109,72.861,-114.075,72.907,-114.054,72.958,-114.046,73.015,-114.052,73.071,-114.095,73.18,-114.127,73.231,-114.164,73.27,-114.206,73.298,-114.302,73.331,-114.638,73.373,-115.552,73.213,-116.573,73.055,-116.972,72.959,-117.256,72.914,-117.552,72.831,-118.133,72.633,-118.375,72.534,-118.457,72.473,-118.481,72.428,-118.449,72.399,-118.39,72.37,-118.246,72.311,-118.207,72.287,-118.213,72.263,-118.369,72.205,-118.59,72.167,-118.945,71.986,-118.96,71.972,-118.984,71.913,-118.994,71.803,-118.988,71.764,-118.952,71.732,-118.868,71.687,-118.583,71.649,-118.372,71.64,-117.888,71.661,-117.742,71.659,-117.878,71.561,-118.148,71.526,-118.226,71.467,-118.222,71.449,-118.188,71.436,-117.936,71.392,-117.723,71.391,-117.337,71.435,-116.78,71.444,-115.587,71.546,-115.338,71.511,-115.303,71.494,-115.341,71.472,-115.472,71.466,-115.734,71.485,-115.98,71.469,-116.044,71.454,-116.045,71.423,-115.922,71.401,-115.892,71.382,-116.042,71.362,-116.228,71.359,-116.422,71.338,-116.815,71.277,-117.314,71.212,-117.814,71.158,-117.934,71.135,-118.269,71.035,-118.353,71,-118.377,70.968,-118.264,70.888,-117.587,70.63,-116.993,70.604,-116.327,70.624,-116.226,70.616,-116.086,70.591,-115.991,70.586,-115.311,70.601,-114.841,70.621,-114.593,70.642,-114.331,70.675,-114.232,70.674,-113.966,70.696,-113.757,70.691,-113.397,70.652,-113.146,70.616,-112.114,70.447,-111.726,70.352,-111.633,70.309,-111.705,70.286,-111.784,70.273,-112.19,70.276,-112.266,70.255,-112.523,70.229,-112.638,70.225,-113.211,70.264,-113.666,70.27,-113.917,70.282,-114.167,70.307,-114.592,70.312,-115.529,70.257,-116.554,70.175,-117.135,70.1,-117.163,70.092,-117.195,70.054,-117.184,69.991,-117.149,69.888,-117.122,69.826,-117.104,69.804,-116.993,69.719,-116.856,69.65]},{"b":[-116.856,68.477,-100.906,73.349],"l":[-116.856,69.65,-116.712,69.576,-116.609,69.512,-116.569,69.463,-116.537,69.434,-116.513,69.425,-116.102,69.337,-115.861,69.304,-115.618,69.283,-115.159,69.265,-114.699,69.273,-114.323,69.269,-114.073,69.251,-113.694,69.195,-113.681,69.182,-113.609,69.03,-113.593,68.96,-113.617,68.838,-113.555,68.768,-113.338,68.599,-113.231,68.535,-113.128,68.494,-113.02,68.481,-112.864,68.477,-112.666,68.485,-112.305,68.516,-111.518,68.533,-111.311,68.542,-111.128,68.588,-110.957,68.594,-110.848,68.578,-110.468,68.61,-109.959,68.63,-109.472,68.677,-108.946,68.76,-108.73,68.827,-108.553,68.897,-108.365,68.935,-107.863,68.954,-107.44,69.002,-107.353,69.032,-107.123,69.152,-107.033,69.181,-106.856,69.347,-106.76,69.407,-106.659,69.44,-106.54,69.443,-106.42,69.414,-106.361,69.381,-106.344,69.34,-106.356,69.281,-106.354,69.251,-106.341,69.224,-106.27,69.195,-106.141,69.162,-106.008,69.148,-105.805,69.153,-105.533,69.134,-105.262,69.094,-105.02,69.081,-105.014,69.068,-105.022,69.052,-105.148,68.978,-105.169,68.955,-105.106,68.92,-104.571,68.872,-104.46,68.912,-104.353,68.928,-104.067,68.866,-103.82,68.848,-103.468,68.809,-103.162,68.829,-102.895,68.824,-102.835,68.833,-102.738,68.865,-102.488,68.889,-102.359,68.923,-101.981,68.989,-101.857,69.024,-101.788,69.132,-101.789,69.182,-101.823,69.217,-101.873,69.24,-101.899,69.246,-101.993,69.236,-102.046,69.258,-102.064,69.281,-102.071,69.308,-102.067,69.337,-102.053,69.36,-101.976,69.407,-101.978,69.425,-102.046,69.465,-102.151,69.488,-102.447,69.476,-102.546,69.434,-102.777,69.378,-102.884,69.341,-103.09,69.212,-103.12,69.205,-103.113,69.236,-103.04,69.368,-103.032,69.433,-103.049,69.472,-103.063,69.485,-103.102,69.483,-103.142,69.497,-103.294,69.568,-103.418,69.611,-103.465,69.644,-103.435,69.668,-103.359,69.685,-103.303,69.674,-103.059,69.595,-102.92,69.565,-102.744,69.548,-102.621,69.552,-102.563,69.574,-102.541,69.592,-102.535,69.621,-102.545,69.66,-102.565,69.692,-102.596,69.718,-102.523,69.758,-102.348,69.813,-102.234,69.842,-102.182,69.846,-102.098,69.825,-101.86,69.738,-101.734,69.704,-101.648,69.699,-101.602,69.721,-101.565,69.756,-101.508,69.833,-101.484,69.85,-101.457,69.834,-101.4,69.749,-101.337,69.71,-101.216,69.68,-101.044,69.669,-100.982,69.68,-100.935,69.715,-100.906,69.812,-100.909,69.869,-100.973,70.029,-101.043,70.111,-101.091,70.136,-101.149,70.148,-101.239,70.151,-101.562,70.135,-101.618,70.172,-101.627,70.248,-101.641,70.266,-101.676,70.278,-101.732,70.286,-101.937,70.275,-101.99,70.285,-102.369,70.413,-102.589,70.469,-102.75,70.522,-103.05,70.655,-103.089,70.65,-103.083,70.619,-103.001,70.541,-103.005,70.526,-103.021,70.516,-103.077,70.509,-103.105,70.51,-103.197,70.547,-103.295,70.572,-103.585,70.631,-103.853,70.734,-103.953,70.763,-104.167,70.927,-104.515,71.064,-104.57,71.104,-104.563,71.132,-104.487,71.248,-104.437,71.3,-104.385,71.338,-104.356,71.382,-104.35,71.434,-104.355,71.472,-104.373,71.495,-104.386,71.577,-104.518,71.699,-104.767,71.868,-104.81,71.903,-104.878,71.98,-105.234,72.415,-105.247,72.464,-105.298,72.56,-105.323,72.635,-105.355,72.67,-105.411,72.709,-105.43,72.74,-105.412,72.765,-105.415,72.788,-105.496,72.849,-105.624,72.927,-105.813,73.011,-106.082,73.072,-106.482,73.196,-106.828,73.266,-106.951,73.276,-107.033,73.245,-107.074,73.197,-107.113,73.192,-107.496,73.288,-107.72,73.329,-108.029,73.349,-108.089,73.304,-108.077,73.281,-107.987,73.233,-107.936,73.217,-107.98,73.207,-108.118,73.202,-108.204,73.183,-108.237,73.15,-108.238,73.106,-107.997,72.653,-107.934,72.588,-107.924,72.557,-107.933,72.52,-107.91,72.491,-107.856,72.468,-107.824,72.443,-107.809,72.347,-107.794,72.303,-107.696,72.149,-107.543,72.025,-107.306,71.895,-107.377,71.886,-107.382,71.875,-107.369,71.859,-107.329,71.835,-107.347,71.819,-107.687,71.716,-107.757,71.663,-107.785,71.63,-107.813,71.626,-107.925,71.639,-108.021,71.677,-108.145,71.705,-108.188,71.724,-108.21,71.751,-108.276,71.9,-108.47,72.139,-108.566,72.317,-108.628,72.412,-108.698,72.499,-108.755,72.551,-108.798,72.568,-108.951,72.583,-108.994,72.596,-108.985,72.637,-108.968,72.654,-108.987,72.671,-109.043,72.687,-109.122,72.726,-109.357,72.775,-109.469,72.808,-109.61,72.876,-110.002,72.982]},{"b":[-125.845,71.088,-115.393,74.545],"l":[-122.623,74.464,-123.468,74.436,-124.696,74.348,-124.709,74.327,-124.645,74.304,-124.629,74.27,-124.575,74.248,-124.261,73.953,-124.192,73.902,-124.088,73.857,-123.873,73.828,-123.798,73.785,-123.797,73.768,-124.03,73.644,-124.114,73.527,-124.424,73.419,-124.594,73.243,-124.647,73.204,-124.804,73.126,-124.836,73.076,-124.817,73.059,-124.736,73.023,-124.643,73.019,-124.588,73.005,-124.57,72.989,-124.561,72.965,-124.565,72.944,-124.583,72.926,-124.931,72.863,-124.97,72.843,-125,72.813,-125.015,72.776,-125.015,72.731,-125.03,72.645,-125.019,72.617,-124.985,72.604,-124.987,72.588,-125.07,72.552,-125.168,72.523,-125.306,72.451,-125.383,72.424,-125.438,72.382,-125.512,72.308,-125.575,72.265,-125.627,72.255,-125.647,72.237,-125.634,72.21,-125.613,72.193,-125.584,72.183,-125.763,72.138,-125.769,72.129,-125.76,72.083,-125.768,72.054,-125.79,72.025,-125.845,71.979,-125.829,71.966,-125.767,71.961,-125.297,71.973,-125.215,71.955,-125.126,71.924,-124.76,71.835,-124.008,71.677,-123.953,71.652,-123.756,71.528,-123.682,71.493,-123.595,71.423,-123.393,71.219,-123.315,71.169,-123.211,71.123,-123.096,71.094,-122.937,71.088,-122.84,71.097,-122.72,71.128,-122.55,71.194,-122.157,71.266,-121.749,71.445,-121.701,71.451,-121.622,71.448,-121.547,71.407,-121.472,71.389,-121.16,71.415,-120.93,71.446,-120.619,71.506,-120.52,71.557,-120.461,71.605,-120.443,71.631,-120.366,71.888,-120.31,71.984,-120.194,72.127,-120.18,72.213,-120.09,72.229,-119.767,72.244,-119.513,72.303,-119.408,72.36,-119.132,72.609,-119.078,72.64,-118.962,72.684,-117.983,72.902,-117.464,73.038,-117.065,73.107,-116.483,73.253,-116.239,73.295,-115.992,73.323,-115.524,73.417,-115.447,73.439,-115.412,73.465,-115.393,73.502,-115.408,73.542,-115.456,73.585,-115.511,73.619,-115.634,73.666,-115.958,73.748,-116.722,74.027,-116.95,74.101,-117.199,74.171,-117.515,74.232,-117.707,74.252,-117.966,74.266,-118.2,74.267,-118.544,74.245,-118.625,74.233,-118.744,74.192,-119.026,74.045,-119.083,74.021,-119.118,74.016,-119.132,74.028,-119.139,74.128,-119.15,74.168,-119.171,74.186,-119.206,74.198,-119.315,74.206,-119.471,74.201,-119.729,74.108,-119.736,74.113,-119.737,74.13,-119.715,74.154,-119.563,74.233,-119.944,74.254,-120.554,74.353,-120.882,74.421,-121.129,74.49,-121.315,74.53,-121.504,74.545,-121.748,74.541,-122.623,74.464]},{"b":[-56.243,46.753,-56.137,46.811],"l":[-56.243,46.767,-56.172,46.753,-56.151,46.762,-56.139,46.779,-56.137,46.802,-56.153,46.811,-56.185,46.807,-56.209,46.798,-56.243,46.767]},{"b":[-94.901,75.749,-94.296,75.996],"l":[-94.745,75.957,-94.84,75.954,-94.881,75.946,-94.901,75.931,-94.894,75.912,-94.86,75.889,-94.834,75.859,-94.815,75.821,-94.787,75.791,-94.751,75.77,-94.624,75.749,-94.527,75.749,-94.33,75.766,-94.304,75.776,-94.296,75.788,-94.332,75.826,-94.414,75.885,-94.443,75.917,-94.471,75.971,-94.499,75.992,-94.538,75.996,-94.745,75.957]},{"b":[-79.644,51.945,-79.27,52.098],"l":[-79.644,52.01,-79.597,51.978,-79.521,51.953,-79.426,51.945,-79.384,51.952,-79.352,51.968,-79.329,51.992,-79.317,52.024,-79.27,52.071,-79.271,52.087,-79.335,52.098,-79.644,52.01]},{"b":[-78.914,56.131,-78.657,56.439],"l":[-78.669,56.439,-78.725,56.439,-78.762,56.421,-78.799,56.383,-78.822,56.34,-78.828,56.29,-78.857,56.232,-78.907,56.166,-78.914,56.133,-78.877,56.131,-78.827,56.145,-78.761,56.175,-78.71,56.213,-78.673,56.26,-78.657,56.317,-78.669,56.439]},{"b":[-80.008,55.807,-78.907,56.6],"l":[-79.765,55.807,-79.606,55.876,-79.527,55.871,-79.495,55.875,-79.455,55.896,-79.407,55.935,-79.222,56.176,-79.182,56.212,-79.142,56.181,-79.136,56.16,-79.142,56.136,-79.274,55.922,-79.228,55.879,-79.175,55.885,-79.084,56.068,-79.018,56.165,-78.936,56.266,-78.907,56.32,-78.931,56.328,-78.942,56.345,-78.94,56.371,-78.963,56.422,-78.995,56.436,-79.078,56.454,-79.1,56.474,-79.124,56.52,-79.155,56.538,-79.21,56.549,-79.246,56.568,-79.261,56.596,-79.272,56.6,-79.305,56.463,-79.339,56.376,-79.393,56.276,-79.458,56.211,-79.536,56.18,-79.554,56.192,-79.512,56.247,-79.476,56.313,-79.432,56.447,-79.435,56.513,-79.448,56.537,-79.459,56.54,-79.469,56.523,-79.468,56.46,-79.482,56.404,-79.515,56.327,-79.596,56.244,-79.79,56.114,-80.001,55.932,-80.008,55.911,-79.987,55.892,-79.905,55.871,-79.781,55.941,-79.565,56.121,-79.545,56.128,-79.495,56.115,-79.497,56.093,-79.765,55.807]},{"b":[-79.946,56.757,-79.827,56.865],"l":[-79.946,56.827,-79.944,56.777,-79.894,56.757,-79.867,56.775,-79.835,56.816,-79.827,56.843,-79.861,56.864,-79.898,56.865,-79.946,56.827]},{"b":[-79.838,57.442,-79.713,57.608],"l":[-79.775,57.514,-79.732,57.508,-79.717,57.516,-79.713,57.555,-79.727,57.605,-79.743,57.608,-79.768,57.599,-79.811,57.559,-79.819,57.542,-79.816,57.518,-79.838,57.483,-79.808,57.442,-79.792,57.449,-79.775,57.514]},{"b":[-80.089,56.199,-79.58,56.466],"l":[-79.688,56.327,-79.633,56.387,-79.58,56.466,-79.606,56.459,-79.681,56.404,-79.749,56.377,-79.81,56.377,-79.852,56.367,-79.874,56.348,-80.005,56.318,-80.057,56.287,-80.089,56.214,-80.029,56.199,-79.978,56.207,-79.688,56.327]},{"b":[-80.325,59.621,-80.167,59.725],"l":[-80.299,59.674,-80.325,59.633,-80.317,59.621,-80.285,59.624,-80.241,59.645,-80.183,59.683,-80.167,59.709,-80.21,59.725,-80.257,59.679,-80.299,59.674]},{"b":[-80.074,53.274,-79.938,53.364],"l":[-80.05,53.364,-80.074,53.344,-80.068,53.324,-80.039,53.297,-80.004,53.28,-79.939,53.274,-79.938,53.304,-79.975,53.352,-80.05,53.364]},{"b":[-80.167,59.764,-79.899,59.877],"l":[-79.95,59.81,-79.899,59.853,-79.956,59.877,-80.041,59.87,-80.084,59.852,-80.122,59.823,-80.167,59.764,-80.064,59.771,-79.95,59.81]},{"b":[-79.584,56.644,-79.482,56.799],"l":[-79.551,56.733,-79.577,56.645,-79.553,56.644,-79.518,56.657,-79.497,56.667,-79.485,56.687,-79.482,56.714,-79.491,56.743,-79.511,56.771,-79.553,56.799,-79.57,56.796,-79.584,56.781,-79.582,56.765,-79.551,56.733]},{"b":[-97.098,72.927,-96.598,73.189],"l":[-96.944,72.927,-96.782,72.937,-96.709,72.947,-96.671,72.961,-96.635,72.992,-96.604,73.042,-96.598,73.074,-96.646,73.102,-96.744,73.126,-96.768,73.137,-96.793,73.165,-96.862,73.189,-97.015,73.157,-97.069,73.13,-97.088,73.098,-97.098,73.062,-97.093,72.997,-96.944,72.927]},{"b":[-97.75,74.466,-97.291,74.627],"l":[-97.356,74.526,-97.304,74.56,-97.291,74.576,-97.318,74.598,-97.417,74.627,-97.516,74.602,-97.75,74.511,-97.722,74.489,-97.656,74.466,-97.356,74.526]},{"b":[-99.417,73.812,-97.659,74.115],"l":[-98.974,73.812,-98.817,73.817,-98.761,73.829,-98.691,73.856,-98.558,73.847,-98.27,73.869,-98.147,73.889,-97.861,73.968,-97.755,74.006,-97.673,74.053,-97.659,74.072,-97.667,74.09,-97.698,74.109,-97.8,74.115,-98.061,74.105,-98.585,74.035,-98.818,74.021,-98.904,74.007,-98.967,73.988,-99.005,73.965,-99.097,73.948,-99.346,73.926,-99.404,73.911,-99.417,73.895,-99.385,73.879,-99.298,73.862,-98.974,73.812]},{"b":[-90.492,69.252,-90.177,69.436],"l":[-90.177,69.357,-90.2,69.419,-90.229,69.436,-90.253,69.418,-90.322,69.429,-90.377,69.416,-90.455,69.39,-90.492,69.37,-90.465,69.329,-90.364,69.263,-90.33,69.252,-90.295,69.258,-90.267,69.273,-90.177,69.357]},{"b":[-90.772,69.209,-90.485,69.374],"l":[-90.6,69.368,-90.663,69.374,-90.742,69.357,-90.766,69.336,-90.772,69.293,-90.686,69.287,-90.667,69.259,-90.626,69.251,-90.574,69.209,-90.493,69.221,-90.485,69.247,-90.511,69.29,-90.54,69.325,-90.6,69.368]},{"b":[-74.626,62.61,-73.988,62.733],"l":[-74.395,62.696,-74.501,62.727,-74.564,62.733,-74.62,62.726,-74.626,62.713,-74.5,62.669,-74.254,62.622,-74.054,62.61,-74,62.618,-73.988,62.636,-74.017,62.663,-74.109,62.68,-74.395,62.696]},{"b":[-75.403,68.342,-74.798,68.696],"l":[-74.798,68.458,-74.819,68.494,-74.885,68.545,-74.984,68.648,-75.075,68.685,-75.2,68.696,-75.287,68.688,-75.37,68.636,-75.396,68.589,-75.403,68.55,-75.4,68.525,-75.31,68.474,-75.073,68.404,-74.959,68.342,-74.881,68.349,-74.845,68.366,-74.819,68.394,-74.813,68.413,-74.828,68.424,-74.831,68.441,-74.798,68.458]},{"b":[-78.669,60.717,-78.242,60.819],"l":[-78.612,60.772,-78.669,60.731,-78.669,60.717,-78.532,60.729,-78.372,60.756,-78.279,60.784,-78.242,60.819,-78.4,60.808,-78.612,60.772]},{"b":[-79.175,68.182,-78.829,68.353],"l":[-79.153,68.335,-79.175,68.264,-79.174,68.235,-79.064,68.182,-78.983,68.193,-78.829,68.268,-78.869,68.31,-78.953,68.353,-79.153,68.335]},{"b":[-77.365,63.384,-76.652,63.682],"l":[-76.652,63.504,-76.764,63.574,-77.134,63.682,-77.365,63.588,-77.057,63.45,-76.922,63.406,-76.783,63.384,-76.678,63.394,-76.652,63.504]},{"b":[-64.956,62.384,-64.418,62.559],"l":[-64.465,62.536,-64.515,62.552,-64.632,62.548,-64.824,62.559,-64.849,62.543,-64.85,62.525,-64.827,62.505,-64.842,62.494,-64.931,62.485,-64.956,62.458,-64.901,62.421,-64.837,62.406,-64.657,62.384,-64.546,62.391,-64.478,62.418,-64.418,62.487,-64.465,62.536]},{"b":[-62.872,67.029,-62.396,67.19],"l":[-62.625,67.177,-62.664,67.148,-62.757,67.113,-62.825,67.072,-62.872,67.063,-62.805,67.029,-62.682,67.056,-62.485,67.134,-62.396,67.178,-62.417,67.188,-62.47,67.19,-62.625,67.177]},{"b":[-68.368,60.233,-67.819,60.588],"l":[-68.088,60.588,-68.142,60.562,-68.235,60.456,-68.338,60.361,-68.368,60.315,-68.365,60.254,-68.324,60.233,-68.234,60.241,-68.012,60.305,-67.922,60.34,-67.844,60.392,-67.819,60.45,-67.848,60.489,-67.914,60.54,-67.978,60.57,-68.088,60.588]},{"b":[-71.22,62.545,-70.269,62.878],"l":[-70.367,62.666,-70.443,62.734,-70.674,62.807,-70.835,62.84,-71.014,62.865,-71.135,62.878,-71.22,62.874,-71.137,62.816,-70.986,62.788,-70.851,62.704,-70.838,62.648,-70.766,62.597,-70.687,62.573,-70.542,62.552,-70.406,62.545,-70.337,62.549,-70.289,62.562,-70.269,62.578,-70.292,62.616,-70.367,62.666]},{"b":[-65.432,61.354,-64.67,61.686],"l":[-64.788,61.413,-64.732,61.438,-64.696,61.471,-64.691,61.539,-64.67,61.593,-64.756,61.638,-64.79,61.662,-64.954,61.685,-65.13,61.686,-65.332,61.668,-65.432,61.65,-65.427,61.611,-65.394,61.563,-65.092,61.453,-65.054,61.432,-64.954,61.41,-64.88,61.357,-64.857,61.354,-64.833,61.366,-64.788,61.413]},{"b":[-68.221,69.535,-67.755,69.682],"l":[-67.755,69.631,-67.829,69.675,-67.909,69.682,-67.989,69.679,-68.093,69.657,-68.221,69.617,-68.202,69.58,-67.94,69.535,-67.915,69.541,-67.845,69.592,-67.755,69.631]},{"b":[-80.795,69.51,-79.365,69.81],"l":[-79.553,69.631,-79.402,69.685,-79.365,69.712,-79.39,69.73,-79.431,69.788,-79.594,69.81,-79.715,69.796,-79.87,69.756,-79.971,69.739,-80.062,69.746,-80.125,69.737,-80.169,69.782,-80.214,69.802,-80.295,69.794,-80.424,69.798,-80.438,69.783,-80.451,69.745,-80.466,69.737,-80.653,69.751,-80.727,69.74,-80.778,69.71,-80.795,69.689,-80.778,69.677,-80.448,69.65,-80.398,69.633,-80.33,69.587,-80.3,69.583,-80.269,69.6,-80.244,69.593,-80.227,69.562,-80.161,69.536,-80.047,69.514,-79.978,69.51,-79.954,69.523,-79.971,69.556,-80.048,69.634,-79.882,69.609,-79.553,69.631]},{"b":[-78.848,69.48,-77.969,69.74],"l":[-78.579,69.639,-78.789,69.523,-78.848,69.483,-78.795,69.48,-78.662,69.503,-78.552,69.492,-78.47,69.503,-78.307,69.552,-78.04,69.608,-77.969,69.639,-77.978,69.665,-78.029,69.715,-78.145,69.739,-78.201,69.74,-78.262,69.717,-78.267,69.687,-78.296,69.667,-78.344,69.675,-78.402,69.651,-78.579,69.639]},{"b":[-77.966,63.946,-77.564,64.037],"l":[-77.714,63.946,-77.642,63.992,-77.564,64.022,-77.569,64.03,-77.617,64.037,-77.711,64.036,-77.931,64.015,-77.966,63.993,-77.958,63.976,-77.929,63.962,-77.714,63.946]},{"b":[-83.238,66.199,-82.931,66.336],"l":[-83.06,66.199,-83.011,66.208,-82.931,66.257,-82.948,66.272,-83.024,66.271,-83.123,66.283,-83.222,66.336,-83.238,66.332,-83.233,66.303,-83.214,66.277,-83.148,66.234,-83.06,66.199]},{"b":[-79.406,68.839,-78.229,69.39],"l":[-78.853,68.916,-78.779,68.95,-78.705,69.014,-78.596,69.079,-78.56,69.106,-78.552,69.129,-78.533,69.146,-78.439,69.199,-78.287,69.263,-78.229,69.304,-78.234,69.315,-78.272,69.361,-78.3,69.379,-78.333,69.386,-78.458,69.39,-78.597,69.371,-78.65,69.351,-78.689,69.325,-78.689,69.3,-78.65,69.275,-78.662,69.262,-78.772,69.252,-78.804,69.235,-78.9,69.135,-78.93,69.123,-79.145,69.087,-79.243,69.049,-79.305,68.992,-79.355,68.956,-79.391,68.94,-79.406,68.923,-79.39,68.89,-79.361,68.858,-79.28,68.839,-79.211,68.845,-79.054,68.883,-78.853,68.916]},{"b":[-77.379,69.132,-76.669,69.44],"l":[-76.994,69.412,-77.109,69.437,-77.188,69.44,-77.319,69.416,-77.341,69.404,-77.352,69.379,-77.358,69.312,-77.379,69.274,-77.322,69.194,-77.276,69.162,-77.215,69.138,-77.122,69.132,-76.995,69.144,-76.911,69.175,-76.869,69.225,-76.81,69.267,-76.687,69.328,-76.67,69.349,-76.669,69.366,-76.684,69.38,-76.746,69.404,-76.994,69.412]},{"b":[-65.235,61.733,-64.846,61.943],"l":[-65.166,61.798,-64.928,61.733,-64.897,61.733,-64.847,61.762,-64.846,61.78,-64.865,61.798,-64.924,61.824,-64.947,61.843,-64.961,61.872,-64.981,61.881,-65.008,61.87,-65.031,61.879,-65.068,61.926,-65.126,61.942,-65.174,61.943,-65.211,61.928,-65.235,61.898,-65.23,61.864,-65.166,61.798]},{"b":[-87.323,69.976,-86.515,70.147],"l":[-86.984,70.011,-86.855,69.986,-86.734,69.976,-86.558,69.995,-86.515,70.017,-86.531,70.048,-86.563,70.077,-86.613,70.106,-86.691,70.115,-86.799,70.105,-86.913,70.113,-87.107,70.147,-87.168,70.127,-87.323,70.102,-87.323,70.08,-87.264,70.044,-87.191,70.019,-87.044,70,-86.984,70.011]},{"b":[-84.467,65.63,-83.234,66.131],"l":[-84.37,66.012,-84.271,65.991,-84.223,65.97,-84.193,65.942,-84.143,65.916,-84.13,65.877,-84.118,65.772,-84.008,65.752,-83.939,65.758,-83.814,65.788,-83.787,65.77,-83.702,65.756,-83.798,65.71,-83.809,65.678,-83.788,65.669,-83.65,65.658,-83.631,65.662,-83.644,65.679,-83.636,65.692,-83.607,65.701,-83.583,65.699,-83.537,65.669,-83.495,65.656,-83.381,65.63,-83.332,65.631,-83.264,65.668,-83.234,65.697,-83.234,65.715,-83.263,65.723,-83.469,65.735,-83.598,65.757,-83.726,65.797,-83.765,65.831,-83.715,65.861,-83.694,65.89,-83.701,65.92,-83.787,65.966,-83.95,66.027,-84.122,66.078,-84.407,66.131,-84.456,66.106,-84.467,66.088,-84.451,66.064,-84.37,66.012]},{"b":[-86.96,67.735,-86.382,68.306],"l":[-86.702,68.306,-86.834,68.23,-86.885,68.191,-86.899,68.163,-86.949,68.119,-86.96,68.1,-86.938,68.068,-86.847,68.01,-86.895,67.938,-86.908,67.902,-86.908,67.867,-86.893,67.837,-86.861,67.81,-86.706,67.75,-86.638,67.735,-86.596,67.736,-86.546,67.752,-86.49,67.784,-86.447,67.817,-86.396,67.889,-86.382,67.927,-86.39,67.989,-86.42,68.074,-86.43,68.139,-86.421,68.183,-86.452,68.225,-86.57,68.288,-86.702,68.306]},{"b":[-85.176,65.564,-84.602,66.025],"l":[-85.15,66.015,-85.176,65.972,-85.174,65.944,-85.144,65.885,-85.136,65.821,-85.096,65.756,-85.072,65.737,-84.931,65.689,-84.869,65.651,-84.83,65.599,-84.783,65.57,-84.727,65.564,-84.675,65.575,-84.626,65.604,-84.602,65.631,-84.603,65.657,-84.692,65.793,-84.757,65.859,-84.87,65.942,-84.889,65.972,-84.92,65.997,-84.939,66.009,-85.031,66.025,-85.15,66.015]},{"b":[-93.197,61.842,-92.993,61.935],"l":[-93.076,61.935,-93.197,61.919,-93.177,61.893,-93.085,61.842,-93.044,61.844,-93,61.867,-92.993,61.89,-93.076,61.935]},{"b":[-104.406,75.763,-102.008,76.311],"l":[-102.58,75.78,-102.511,75.808,-102.423,75.869,-102.318,75.895,-102.047,75.928,-102.008,75.939,-102.018,75.954,-102.227,76.015,-102.426,76.086,-102.49,76.095,-102.536,76.196,-102.584,76.282,-102.728,76.307,-103.098,76.311,-103.571,76.258,-104.012,76.223,-104.351,76.182,-104.406,76.108,-104.242,76.047,-103.985,76.047,-103.801,76.037,-103.985,75.933,-103.77,75.892,-103.202,75.958,-103.042,75.919,-103.245,75.823,-103.315,75.764,-102.944,75.763,-102.58,75.78]},{"b":[-101.613,76.575,-100.269,76.752],"l":[-100.269,76.734,-100.467,76.75,-100.622,76.752,-100.886,76.743,-100.962,76.734,-101.165,76.665,-101.509,76.628,-101.613,76.605,-101.605,76.587,-101.485,76.575,-101.226,76.579,-100.747,76.649,-100.269,76.734]},{"b":[-104.603,76.326,-103.034,76.666],"l":[-103.472,76.329,-103.311,76.348,-103.2,76.371,-103.083,76.405,-103.034,76.431,-103.051,76.45,-103.19,76.477,-103.585,76.539,-103.613,76.563,-103.723,76.601,-103.821,76.598,-103.973,76.578,-104.023,76.583,-103.969,76.614,-103.959,76.639,-103.992,76.657,-104.075,76.666,-104.205,76.666,-104.5,76.63,-104.586,76.606,-104.603,76.583,-104.577,76.54,-104.506,76.479,-104.408,76.365,-104.358,76.335,-104.271,76.326,-103.472,76.329]},{"b":[-103.274,78.126,-102.788,78.271],"l":[-103.11,78.246,-103.26,78.183,-103.274,78.166,-103.271,78.151,-103.252,78.138,-103.118,78.126,-103.003,78.146,-102.788,78.218,-102.826,78.25,-102.892,78.271,-102.973,78.267,-103.11,78.246]},{"b":[-102.475,77.687,-101.002,77.9],"l":[-102.08,77.692,-101.831,77.687,-101.694,77.697,-101.585,77.718,-101.398,77.729,-101.002,77.735,-101.02,77.762,-101.046,77.778,-101.128,77.813,-101.193,77.83,-101.322,77.854,-101.639,77.892,-101.918,77.9,-102.263,77.889,-102.448,77.881,-102.472,77.873,-102.475,77.837,-102.458,77.77,-102.378,77.728,-102.08,77.692]},{"b":[-90.562,76.488,-89.694,76.837],"l":[-89.949,76.836,-90.136,76.837,-90.41,76.81,-90.525,76.788,-90.562,76.754,-90.556,76.735,-90.441,76.663,-90.294,76.579,-90.165,76.524,-90.054,76.495,-89.974,76.488,-89.924,76.501,-89.773,76.494,-89.726,76.507,-89.726,76.521,-89.773,76.534,-89.805,76.561,-89.822,76.602,-89.822,76.631,-89.788,76.66,-89.709,76.701,-89.694,76.72,-89.695,76.741,-89.725,76.763,-89.775,76.782,-89.949,76.836]},{"b":[-97.021,75.369,-95.96,75.655],"l":[-96.428,75.606,-96.523,75.584,-96.856,75.538,-96.983,75.51,-97.021,75.468,-96.97,75.413,-96.915,75.38,-96.857,75.369,-96.723,75.381,-96.679,75.394,-96.622,75.431,-96.462,75.494,-96.344,75.506,-96.237,75.475,-96.156,75.477,-96.079,75.51,-95.969,75.542,-95.96,75.554,-96.04,75.586,-96.145,75.614,-96.368,75.655,-96.397,75.647,-96.417,75.631,-96.428,75.606]},{"b":[-95.851,74.5,-95.274,74.637],"l":[-95.51,74.637,-95.66,74.637,-95.746,74.616,-95.774,74.599,-95.851,74.582,-95.834,74.569,-95.777,74.551,-95.442,74.506,-95.352,74.5,-95.307,74.505,-95.274,74.519,-95.278,74.54,-95.353,74.586,-95.51,74.637]},{"b":[-115.029,77.721,-113.618,78.078],"l":[-113.898,77.916,-114.087,77.978,-114.181,77.998,-114.28,78.004,-114.303,78.033,-114.297,78.063,-114.33,78.078,-114.607,78.04,-114.726,78.016,-114.79,77.993,-114.89,77.977,-115.029,77.968,-114.98,77.915,-114.608,77.769,-114.287,77.721,-114.106,77.721,-113.832,77.755,-113.726,77.776,-113.619,77.813,-113.618,77.832,-113.697,77.869,-113.721,77.89,-113.768,77.904,-113.898,77.916]},{"b":[-121.241,75.741,-120.879,75.938],"l":[-121.042,75.903,-121.018,75.884,-121.015,75.868,-121.026,75.848,-121.221,75.777,-121.241,75.752,-121.154,75.741,-121.076,75.745,-121.007,75.766,-120.955,75.789,-120.921,75.814,-120.897,75.845,-120.879,75.907,-120.888,75.928,-120.914,75.938,-120.993,75.927,-121.042,75.903]},{"b":[-114.835,76.711,-113.488,76.895],"l":[-113.712,76.711,-113.561,76.743,-113.488,76.783,-113.517,76.825,-113.585,76.847,-113.708,76.873,-113.892,76.895,-114.42,76.875,-114.647,76.851,-114.835,76.795,-114.808,76.774,-114.751,76.759,-113.712,76.711]},{"b":[-104.887,75.031,-103.642,75.43],"l":[-103.746,75.252,-103.758,75.289,-103.804,75.346,-103.851,75.371,-103.917,75.392,-104.075,75.425,-104.346,75.43,-104.474,75.413,-104.649,75.35,-104.69,75.321,-104.801,75.211,-104.848,75.173,-104.882,75.16,-104.887,75.148,-104.828,75.12,-104.634,75.061,-104.309,75.031,-104.12,75.036,-103.814,75.08,-103.71,75.115,-103.664,75.139,-103.642,75.163,-103.644,75.187,-103.667,75.211,-103.746,75.252]},{"b":[-59.362,46.628,-52.654,51.596],"l":[-52.672,47.622,-52.703,47.693,-52.711,47.745,-52.745,47.769,-52.782,47.769,-52.817,47.728,-52.873,47.619,-52.945,47.553,-53.057,47.483,-53.122,47.455,-53.158,47.488,-53.17,47.512,-53.176,47.653,-53.154,47.735,-53.111,47.812,-52.998,47.976,-52.955,48.029,-52.872,48.094,-52.866,48.113,-52.883,48.131,-52.921,48.147,-53.085,48.069,-53.283,47.998,-53.504,47.744,-53.604,47.662,-53.672,47.648,-53.765,47.65,-53.805,47.682,-53.838,47.727,-53.864,47.787,-53.862,47.799,-53.695,47.921,-53.658,47.969,-53.638,48.015,-53.653,48.026,-53.794,48.01,-53.87,48.02,-53.758,48.042,-53.71,48.057,-53.704,48.068,-53.569,48.088,-53.542,48.108,-53.56,48.174,-53.61,48.208,-53.531,48.232,-53.406,48.294,-53.334,48.356,-53.301,48.368,-53.225,48.364,-53.182,48.374,-53.136,48.402,-53.06,48.48,-53.037,48.516,-53.021,48.572,-53.028,48.635,-53.043,48.657,-53.057,48.659,-53.127,48.633,-53.22,48.578,-53.275,48.563,-53.361,48.573,-53.411,48.562,-53.552,48.482,-53.644,48.511,-53.739,48.496,-53.799,48.449,-53.853,48.449,-53.937,48.437,-54.104,48.388,-54.114,48.394,-54.068,48.419,-53.886,48.485,-53.795,48.526,-53.775,48.576,-53.706,48.656,-53.698,48.68,-53.784,48.695,-53.886,48.685,-53.966,48.707,-53.97,48.725,-53.962,48.739,-53.887,48.768,-53.848,48.797,-53.853,48.811,-53.951,48.807,-54.1,48.785,-54.161,48.788,-53.903,48.889,-53.845,48.925,-53.825,48.951,-53.809,48.993,-53.758,49.035,-53.671,49.078,-53.573,49.141,-53.56,49.192,-53.57,49.264,-53.619,49.322,-53.755,49.385,-53.862,49.426,-53.958,49.442,-54.271,49.419,-54.317,49.424,-54.356,49.415,-54.389,49.392,-54.448,49.329,-54.463,49.342,-54.465,49.401,-54.481,49.469,-54.469,49.53,-54.502,49.527,-54.579,49.491,-54.651,49.445,-54.718,49.389,-54.782,49.355,-54.844,49.345,-54.911,49.316,-54.983,49.268,-55.016,49.26,-55.01,49.293,-55.026,49.305,-55.063,49.297,-55.176,49.244,-55.245,49.2,-55.254,49.18,-55.247,49.139,-55.252,49.121,-55.335,49.078,-55.353,49.079,-55.332,49.126,-55.342,49.168,-55.259,49.267,-55.225,49.335,-55.2,49.408,-55.207,49.482,-55.23,49.508,-55.266,49.524,-55.283,49.514,-55.28,49.413,-55.29,49.392,-55.344,49.373,-55.355,49.381,-55.354,49.438,-55.379,49.473,-55.376,49.49,-55.49,49.463,-55.678,49.435,-55.815,49.515,-56.041,49.457,-56.087,49.452,-55.892,49.58,-55.882,49.646,-55.87,49.67,-55.902,49.681,-55.979,49.678,-56.052,49.658,-56.121,49.622,-56.14,49.619,-56.04,49.705,-55.718,49.829,-55.584,49.892,-55.527,49.937,-55.503,49.983,-55.53,49.997,-55.674,49.967,-55.765,49.96,-55.873,50.013,-55.927,50.018,-56.075,49.983,-56.161,49.94,-56.164,49.957,-56.127,50.015,-56.122,50.063,-56.148,50.1,-56.179,50.115,-56.247,50.09,-56.322,50.014,-56.376,49.934,-56.428,49.897,-56.501,49.87,-56.611,49.788,-56.757,49.652,-56.822,49.613,-56.807,49.673,-56.809,49.71,-56.829,49.725,-56.849,49.765,-56.839,49.788,-56.79,49.834,-56.754,49.883,-56.747,49.908,-56.75,49.967,-56.732,50.008,-56.694,50.06,-56.539,50.207,-56.484,50.271,-56.455,50.35,-56.454,50.38,-56.382,50.417,-56.196,50.585,-56.136,50.651,-56.121,50.734,-56.107,50.759,-56.078,50.781,-55.962,50.838,-55.871,50.907,-55.8,51.033,-55.785,51.087,-55.785,51.131,-55.796,51.166,-55.815,51.191,-55.841,51.205,-55.874,51.208,-55.961,51.191,-56,51.199,-56.031,51.227,-56.044,51.262,-56.031,51.328,-55.941,51.343,-55.731,51.359,-55.631,51.373,-55.583,51.389,-55.532,51.437,-55.459,51.537,-55.453,51.562,-55.496,51.59,-55.522,51.596,-55.666,51.579,-55.701,51.559,-55.66,51.511,-55.69,51.471,-55.866,51.508,-55.902,51.564,-56.026,51.568,-56.207,51.489,-56.518,51.399,-56.619,51.362,-56.682,51.333,-56.75,51.275,-56.805,51.144,-56.825,51.126,-56.976,51.028,-57.036,51.011,-57.037,50.996,-57.013,50.968,-57.006,50.94,-57.053,50.857,-57.132,50.787,-57.242,50.745,-57.275,50.725,-57.298,50.699,-57.294,50.673,-57.264,50.649,-57.18,50.615,-57.237,50.605,-57.331,50.605,-57.36,50.584,-57.433,50.506,-57.466,50.464,-57.608,50.199,-57.712,50.025,-57.926,49.701,-57.929,49.668,-57.897,49.6,-57.799,49.509,-57.791,49.49,-57.856,49.474,-57.961,49.532,-58.016,49.542,-58.107,49.5,-58.183,49.435,-58.213,49.387,-58.219,49.305,-58.191,49.259,-58.097,49.23,-57.98,49.23,-57.991,49.209,-58.049,49.18,-58.099,49.077,-58.082,49.045,-58.041,49.01,-57.991,48.988,-58.006,48.981,-58.05,48.988,-58.186,49.062,-58.319,49.081,-58.359,49.097,-58.404,49.084,-58.494,49.003,-58.546,48.897,-58.642,48.749,-58.687,48.622,-58.716,48.598,-58.843,48.605,-58.877,48.623,-58.906,48.65,-58.887,48.692,-58.819,48.747,-58.842,48.746,-59.063,48.628,-59.168,48.558,-59.167,48.522,-58.944,48.522,-58.723,48.541,-58.606,48.533,-58.492,48.513,-58.33,48.522,-58.336,48.514,-58.503,48.442,-58.605,48.411,-58.711,48.325,-58.961,48.159,-59.272,47.996,-59.341,47.934,-59.362,47.889,-59.362,47.866,-59.321,47.737,-59.26,47.634,-59.219,47.603,-59.117,47.571,-58.941,47.58,-58.613,47.626,-58.509,47.653,-58.428,47.683,-58.337,47.731,-58.327,47.72,-58.333,47.677,-58.239,47.669,-57.926,47.675,-57.884,47.66,-57.66,47.625,-57.473,47.631,-56.952,47.574,-56.774,47.565,-56.722,47.592,-56.46,47.617,-56.326,47.654,-56.263,47.658,-56.221,47.671,-56.151,47.775,-56.121,47.789,-56.09,47.772,-56.02,47.764,-55.918,47.792,-55.858,47.819,-55.844,47.788,-55.867,47.592,-56.084,47.525,-56.127,47.503,-56.081,47.5,-55.862,47.53,-55.811,47.516,-55.775,47.498,-55.576,47.465,-55.499,47.475,-55.461,47.485,-55.435,47.501,-55.413,47.55,-55.391,47.643,-55.366,47.661,-55.197,47.65,-55.075,47.658,-55.035,47.634,-54.946,47.621,-54.891,47.629,-54.785,47.665,-54.795,47.64,-54.87,47.571,-54.976,47.516,-55.191,47.449,-55.361,47.259,-55.401,47.221,-55.492,47.161,-55.61,47.12,-55.772,47.092,-55.838,47.072,-55.919,47.017,-55.954,46.973,-55.958,46.956,-55.95,46.928,-55.881,46.887,-55.845,46.874,-55.789,46.867,-55.652,46.881,-55.531,46.914,-55.479,46.917,-55.401,46.899,-55.316,46.906,-55.255,46.942,-55.14,47.046,-55.099,47.104,-55.09,47.174,-54.857,47.385,-54.802,47.399,-54.745,47.395,-54.651,47.408,-54.575,47.458,-54.474,47.547,-54.463,47.536,-54.542,47.425,-54.563,47.375,-54.488,47.404,-54.456,47.428,-54.434,47.462,-54.405,47.556,-54.234,47.772,-54.218,47.847,-54.192,47.86,-54.047,47.806,-53.989,47.756,-53.94,47.645,-53.901,47.509,-53.878,47.464,-53.85,47.44,-53.869,47.387,-53.971,47.262,-54.093,47.086,-54.155,46.967,-54.173,46.917,-54.174,46.88,-54.133,46.839,-54.102,46.825,-54.076,46.82,-54.01,46.84,-53.86,46.939,-53.774,47.012,-53.695,47.093,-53.636,47.138,-53.597,47.146,-53.578,47.133,-53.58,47.099,-53.612,47.01,-53.581,46.957,-53.595,46.888,-53.616,46.68,-53.59,46.639,-53.568,46.628,-53.536,46.633,-53.382,46.711,-53.323,46.718,-53.291,46.717,-53.255,46.698,-53.214,46.66,-53.167,46.646,-53.115,46.656,-53.07,46.681,-53.032,46.723,-52.962,46.819,-52.889,46.974,-52.882,47.011,-52.888,47.046,-52.912,47.103,-52.684,47.426,-52.669,47.47,-52.654,47.549,-52.672,47.622]},{"b":[-55.634,50.709,-55.469,50.801],"l":[-55.604,50.781,-55.634,50.74,-55.629,50.721,-55.601,50.709,-55.57,50.709,-55.536,50.72,-55.504,50.742,-55.473,50.776,-55.469,50.796,-55.527,50.801,-55.604,50.781]},{"b":[-54.864,49.496,-54.538,49.631],"l":[-54.819,49.514,-54.787,49.496,-54.744,49.508,-54.709,49.531,-54.554,49.589,-54.538,49.62,-54.559,49.631,-54.619,49.622,-54.733,49.562,-54.764,49.562,-54.783,49.572,-54.789,49.591,-54.813,49.599,-54.855,49.597,-54.864,49.576,-54.819,49.514]},{"b":[-55.42,51.889,-55.274,51.995],"l":[-55.274,51.995,-55.346,51.983,-55.4,51.938,-55.42,51.9,-55.409,51.889,-55.361,51.89,-55.294,51.93,-55.274,51.995]},{"b":[-54.289,49.587,-53.981,49.751],"l":[-53.981,49.662,-54.02,49.679,-54.094,49.744,-54.138,49.751,-54.199,49.689,-54.259,49.719,-54.278,49.711,-54.289,49.661,-54.286,49.595,-54.269,49.587,-54.238,49.592,-53.981,49.662]},{"b":[-56.387,46.795,-56.255,47.099],"l":[-56.315,46.954,-56.278,47.035,-56.287,47.071,-56.365,47.099,-56.378,47.09,-56.387,47.068,-56.334,46.936,-56.333,46.916,-56.377,46.848,-56.385,46.819,-56.354,46.795,-56.267,46.838,-56.255,46.861,-56.29,46.9,-56.315,46.954]},{"b":[-54.326,47.407,-54.128,47.647],"l":[-54.168,47.607,-54.215,47.585,-54.226,47.566,-54.227,47.54,-54.259,47.498,-54.32,47.439,-54.326,47.408,-54.276,47.407,-54.227,47.441,-54.148,47.573,-54.128,47.647,-54.168,47.607]},{"b":[-100.625,68.724,-100.178,69.035],"l":[-100.289,68.958,-100.33,68.998,-100.414,69.028,-100.52,69.035,-100.565,69.027,-100.601,69.009,-100.612,68.99,-100.598,68.969,-100.6,68.941,-100.625,68.865,-100.625,68.816,-100.616,68.783,-100.597,68.766,-100.573,68.766,-100.521,68.791,-100.497,68.792,-100.481,68.786,-100.443,68.748,-100.397,68.724,-100.366,68.729,-100.288,68.766,-100.249,68.775,-100.217,68.807,-100.178,68.904,-100.207,68.926,-100.289,68.958]},{"b":[-100.247,68.954,-99.995,69.129],"l":[-100.153,69.129,-100.187,69.114,-100.237,69.071,-100.247,69.053,-100.242,69.04,-100.196,68.991,-100.141,68.97,-100.018,68.954,-99.995,69.014,-100.006,69.047,-100.035,69.087,-100.073,69.111,-100.153,69.129]},{"b":[-100.678,70.488,-100.276,70.67],"l":[-100.321,70.578,-100.276,70.595,-100.434,70.649,-100.538,70.669,-100.635,70.67,-100.678,70.646,-100.667,70.596,-100.648,70.563,-100.621,70.547,-100.537,70.525,-100.321,70.488,-100.308,70.496,-100.306,70.508,-100.323,70.542,-100.321,70.578]},{"b":[-95.995,69.336,-95.381,69.626],"l":[-95.986,69.392,-95.956,69.367,-95.893,69.352,-95.858,69.382,-95.812,69.447,-95.798,69.5,-95.818,69.541,-95.806,69.56,-95.764,69.56,-95.704,69.538,-95.683,69.5,-95.666,69.439,-95.67,69.402,-95.696,69.39,-95.73,69.348,-95.684,69.336,-95.579,69.336,-95.496,69.35,-95.437,69.378,-95.399,69.42,-95.382,69.474,-95.381,69.507,-95.514,69.574,-95.602,69.602,-95.707,69.624,-95.798,69.626,-95.876,69.606,-95.936,69.567,-95.979,69.509,-95.995,69.47,-95.978,69.433,-95.986,69.392]},{"b":[-101.356,69.388,-101.001,69.576],"l":[-101.098,69.541,-101.245,69.574,-101.313,69.576,-101.351,69.559,-101.356,69.54,-101.328,69.517,-101.23,69.493,-101.207,69.48,-101.218,69.463,-101.29,69.441,-101.268,69.431,-101.262,69.418,-101.269,69.391,-101.254,69.388,-101.172,69.397,-101.127,69.415,-101.087,69.443,-101.049,69.457,-101.001,69.462,-101.031,69.495,-101.098,69.541]},{"b":[-102.308,68.585,-101.722,68.825],"l":[-102.013,68.825,-102.074,68.774,-102.153,68.74,-102.271,68.708,-102.308,68.682,-102.266,68.664,-101.945,68.603,-101.887,68.585,-101.846,68.586,-101.794,68.637,-101.732,68.652,-101.722,68.724,-101.733,68.753,-101.759,68.775,-101.828,68.799,-102.013,68.825]},{"b":[-105.051,68.402,-104.44,68.582],"l":[-104.602,68.562,-104.7,68.577,-104.907,68.582,-104.994,68.574,-105.051,68.559,-105.042,68.532,-104.965,68.492,-104.851,68.454,-104.699,68.418,-104.596,68.402,-104.541,68.406,-104.457,68.431,-104.44,68.45,-104.445,68.471,-104.472,68.504,-104.602,68.562]},{"b":[-108.152,67.318,-107.891,67.665],"l":[-107.97,67.326,-107.95,67.318,-107.9,67.402,-107.891,67.437,-107.905,67.467,-107.932,67.476,-107.989,67.514,-107.975,67.549,-107.991,67.622,-108.049,67.665,-108.128,67.629,-108.121,67.568,-108.151,67.525,-108.152,67.429,-108.073,67.385,-108.004,67.366,-107.97,67.326]},{"b":[-109.184,67.879,-108.886,67.982],"l":[-108.971,67.98,-109.054,67.972,-109.166,67.982,-109.184,67.975,-109.162,67.952,-109.096,67.924,-108.92,67.879,-108.894,67.884,-108.886,67.899,-108.91,67.939,-108.971,67.98]},{"b":[-108.093,66.858,-107.806,67.005],"l":[-107.944,66.858,-107.895,66.872,-107.833,66.921,-107.806,66.999,-107.966,66.997,-108.093,67.005,-108.06,66.947,-107.965,66.885,-107.944,66.858]},{"b":[-109.498,67.988,-109.323,68.098],"l":[-109.469,68.098,-109.498,68.047,-109.361,67.988,-109.323,67.991,-109.324,68.013,-109.342,68.046,-109.469,68.098]},{"b":[-61.196,56.015,-60.955,56.141],"l":[-61.049,56.129,-61.087,56.141,-61.158,56.118,-61.188,56.089,-61.196,56.064,-61.191,56.048,-61.137,56.033,-60.983,56.015,-60.994,56.039,-60.955,56.08,-60.966,56.099,-61.049,56.129]},{"b":[-62.011,57.416,-61.637,57.579],"l":[-61.937,57.554,-61.983,57.567,-62.007,57.558,-62.011,57.548,-61.975,57.495,-61.795,57.422,-61.637,57.416,-61.66,57.525,-61.744,57.555,-61.848,57.579,-61.893,57.573,-61.937,57.554]},{"b":[-69.353,58.961,-69.155,59.146],"l":[-69.195,59.146,-69.303,59.145,-69.33,59.121,-69.312,59.075,-69.316,59.029,-69.353,58.961,-69.331,58.962,-69.302,58.977,-69.221,58.968,-69.16,59.04,-69.155,59.064,-69.181,59.073,-69.194,59.093,-69.195,59.146]},{"b":[22.077,60.166,22.416,60.371],"l":[22.306,60.229,22.313,60.27,22.416,60.303,22.355,60.356,22.302,60.348,22.175,60.371,22.126,60.356,22.108,60.315,22.077,60.286,22.141,60.265,22.188,60.237,22.209,60.197,22.258,60.166,22.361,60.166,22.346,60.203,22.306,60.229]},{"b":[21.215,60.48,21.451,60.638],"l":[21.215,60.604,21.244,60.526,21.3,60.48,21.369,60.488,21.437,60.483,21.451,60.53,21.301,60.596,21.268,60.638,21.225,60.621,21.215,60.604]},{"b":[21.695,60.106,21.864,60.202],"l":[21.764,60.199,21.705,60.172,21.695,60.114,21.733,60.106,21.833,60.141,21.864,60.202,21.764,60.199]},{"b":[-64.836,60.298,-64.407,60.515],"l":[-64.5,60.43,-64.533,60.441,-64.646,60.515,-64.783,60.51,-64.836,60.501,-64.834,60.448,-64.809,60.41,-64.738,60.376,-64.558,60.323,-64.442,60.298,-64.407,60.367,-64.5,60.43]},{"b":[126.008,34.707,126.172,34.915],"l":[126.169,34.83,126.078,34.915,126.008,34.867,126.052,34.838,126.071,34.783,126.115,34.714,126.159,34.707,126.172,34.731,126.169,34.83]},{"b":[72.35,-7.435,72.499,-7.22],"l":[72.494,-7.262,72.446,-7.22,72.436,-7.23,72.465,-7.278,72.474,-7.31,72.462,-7.338,72.467,-7.368,72.447,-7.396,72.427,-7.3,72.373,-7.263,72.35,-7.263,72.408,-7.334,72.429,-7.435,72.469,-7.417,72.492,-7.377,72.499,-7.295,72.494,-7.262]},{"b":[-139.632,-8.948,-139.508,-8.86],"l":[-139.535,-8.875,-139.584,-8.86,-139.612,-8.872,-139.632,-8.899,-139.621,-8.948,-139.556,-8.94,-139.51,-8.916,-139.508,-8.897,-139.535,-8.875]},{"b":[157.454,-8.794,157.645,-8.697],"l":[157.623,-8.735,157.579,-8.704,157.526,-8.697,157.454,-8.706,157.458,-8.73,157.586,-8.783,157.643,-8.794,157.645,-8.759,157.623,-8.735]},{"b":[20.398,60.012,20.611,60.075],"l":[20.569,60.07,20.49,60.075,20.43,60.062,20.398,60.041,20.411,60.03,20.488,60.033,20.522,60.012,20.603,60.017,20.611,60.041,20.569,60.07]},{"b":[21.486,60.098,21.648,60.172],"l":[21.634,60.169,21.568,60.172,21.507,60.148,21.486,60.127,21.541,60.098,21.628,60.108,21.648,60.141,21.634,60.169]},{"b":[24.577,64.958,24.998,65.086],"l":[24.786,65.086,24.651,65.074,24.577,65.043,24.579,64.979,24.699,64.958,24.848,64.991,24.892,65.026,24.998,65.039,24.971,65.055,24.786,65.086]},{"b":[29.744,69.768,30.055,69.906],"l":[29.836,69.906,29.786,69.829,29.744,69.792,29.766,69.768,29.956,69.797,30.055,69.838,29.993,69.873,29.914,69.902,29.836,69.906]},{"b":[4.799,61.072,4.973,61.199],"l":[4.862,61.194,4.824,61.178,4.799,61.083,4.87,61.072,4.959,61.085,4.973,61.148,4.915,61.199,4.862,61.194]},{"b":[7.804,63.337,8.141,63.471],"l":[8.103,63.338,8.141,63.366,8.136,63.431,8.074,63.471,7.938,63.45,7.804,63.414,7.815,63.385,7.888,63.352,8.005,63.337,8.103,63.338]},{"b":[8.287,63.665,8.815,63.805],"l":[8.471,63.667,8.787,63.703,8.815,63.726,8.809,63.771,8.765,63.805,8.733,63.801,8.709,63.774,8.451,63.732,8.287,63.687,8.356,63.665,8.471,63.667]},{"b":[12.429,65.899,12.779,66.011],"l":[12.476,65.977,12.43,65.94,12.429,65.899,12.51,65.902,12.719,65.964,12.779,65.992,12.747,66.011,12.642,66.009,12.549,66.002,12.476,65.977]},{"b":[12.327,66.037,12.623,66.211],"l":[12.623,66.122,12.621,66.178,12.527,66.211,12.461,66.185,12.446,66.151,12.418,66.123,12.343,66.081,12.327,66.037,12.42,66.043,12.576,66.072,12.623,66.122]},{"b":[19.599,70.068,20.088,70.274],"l":[19.869,70.212,19.818,70.205,19.767,70.217,19.684,70.274,19.599,70.266,19.613,70.219,19.711,70.165,19.747,70.11,19.781,70.077,19.897,70.068,20.006,70.076,20.088,70.102,20.084,70.129,19.994,70.149,19.91,70.202,19.869,70.212]},{"b":[21.995,70.509,23.441,70.843],"l":[22.605,70.533,22.656,70.559,22.829,70.542,22.885,70.554,22.929,70.574,23.068,70.594,23.305,70.722,23.387,70.754,23.421,70.784,23.441,70.816,23.396,70.843,23.28,70.813,23.205,70.815,22.964,70.711,22.858,70.728,22.571,70.697,22.421,70.703,22.35,70.658,22.233,70.667,22.17,70.656,21.995,70.657,22.056,70.613,22.169,70.562,22.359,70.515,22.432,70.509,22.558,70.516,22.605,70.533]},{"b":[20.405,70.057,20.819,70.231],"l":[20.819,70.205,20.786,70.22,20.655,70.231,20.493,70.203,20.412,70.155,20.405,70.119,20.464,70.077,20.535,70.081,20.598,70.071,20.643,70.057,20.725,70.067,20.779,70.09,20.819,70.205]},{"b":[25.315,70.954,26.147,71.142],"l":[26,70.975,26.134,70.996,26.147,71.04,26.078,71.033,25.945,71.105,25.854,71.104,25.586,71.142,25.423,71.097,25.315,71.053,25.315,71.034,25.482,71.02,25.582,70.961,25.76,70.954,25.791,70.963,26,70.975]},{"b":[22.918,70.283,23.641,70.617],"l":[23.547,70.617,23.248,70.505,23.158,70.516,23.022,70.487,22.941,70.445,22.918,70.417,22.918,70.385,23.006,70.353,23.091,70.378,23.108,70.359,23.1,70.296,23.159,70.283,23.271,70.296,23.345,70.315,23.333,70.335,23.548,70.408,23.641,70.464,23.634,70.503,23.615,70.549,23.579,70.594,23.547,70.617]},{"b":[23.663,70.527,24.078,70.747],"l":[23.852,70.714,23.837,70.729,23.778,70.747,23.689,70.723,23.663,70.675,23.67,70.597,23.717,70.562,23.827,70.527,24.018,70.567,24.078,70.651,23.956,70.7,23.852,70.714]},{"b":[13.2,67.995,14.119,68.276],"l":[13.932,68.248,13.873,68.265,13.784,68.276,13.688,68.273,13.538,68.249,13.429,68.163,13.368,68.167,13.3,68.16,13.256,68.121,13.2,68.087,13.229,67.995,13.352,68.01,13.392,68.021,13.404,68.061,13.424,68.083,13.495,68.052,13.584,68.094,13.656,68.105,13.778,68.105,13.824,68.121,13.888,68.169,14.029,68.188,14.097,68.219,14.119,68.247,14.088,68.253,13.932,68.248]},{"b":[12.824,67.821,13.123,68.071],"l":[12.958,68.015,12.878,67.918,12.824,67.821,12.972,67.874,13.075,67.935,13.098,67.956,13.098,68.003,13.123,68.049,13.068,68.071,12.958,68.015]},{"b":[16.81,69.014,18.077,69.596],"l":[18.004,69.505,17.927,69.507,17.863,69.543,17.784,69.563,17.677,69.557,17.623,69.539,17.503,69.596,17.488,69.587,17.483,69.57,17.454,69.53,17.356,69.527,17.252,69.504,17.23,69.478,17.373,69.439,17.395,69.417,17.361,69.381,17.083,69.399,17.002,69.362,16.997,69.33,16.974,69.285,16.998,69.191,16.972,69.138,16.843,69.112,16.815,69.095,16.81,69.071,16.96,69.069,17.077,69.047,17.083,69.014,17.161,69.026,17.324,69.13,17.488,69.197,17.568,69.16,17.774,69.172,17.951,69.198,17.921,69.274,17.942,69.329,18.021,69.35,18.077,69.396,18.052,69.458,18.004,69.505]},{"b":[18.062,69.535,19.608,70.247],"l":[19.5,70.048,19.446,70.038,19.422,70.017,19.344,70.012,19.255,70.066,19.249,70.179,19.213,70.247,19.133,70.244,19.06,70.167,19.051,70.135,19.075,70.086,19.051,70.038,18.969,70.043,18.883,70.011,18.824,69.96,18.687,69.891,18.674,69.864,18.698,69.825,18.674,69.782,18.624,69.813,18.584,69.807,18.512,69.769,18.406,69.782,18.349,69.768,18.315,69.715,18.268,69.702,18.232,69.677,18.227,69.636,18.083,69.626,18.062,69.602,18.13,69.558,18.274,69.535,18.41,69.553,18.785,69.579,18.801,69.605,18.807,69.64,18.909,69.707,19.008,69.76,19.131,69.81,19.197,69.8,19.335,69.82,19.442,69.908,19.592,69.97,19.608,70.019,19.5,70.048]},{"b":[14.257,68.178,16.547,69.302],"l":[15.095,68.441,14.586,68.4,14.438,68.342,14.257,68.257,14.258,68.191,14.35,68.178,14.629,68.198,14.927,68.307,15.038,68.283,15.098,68.289,15.188,68.31,15.28,68.374,15.337,68.378,15.341,68.325,15.438,68.313,15.683,68.356,15.764,68.409,15.837,68.409,15.873,68.394,15.913,68.389,15.975,68.402,16.048,68.464,16.194,68.538,16.338,68.568,16.519,68.633,16.547,68.717,16.48,68.803,16.425,68.842,16.329,68.876,16.276,68.868,16.228,68.854,16.151,68.842,16.121,68.799,16.127,68.746,16.069,68.714,16.06,68.681,15.909,68.65,15.772,68.554,15.76,68.561,15.791,68.617,15.928,68.733,15.924,68.819,15.906,68.908,15.834,68.961,15.812,69.024,15.993,69.113,16.115,69.216,16.129,69.274,16.048,69.302,15.965,69.302,15.893,69.278,15.742,69.171,15.65,69.133,15.483,69.043,15.438,68.979,15.444,68.919,15.529,68.912,15.564,68.874,15.489,68.805,15.413,68.616,15.095,68.441]},{"b":[14.373,68.606,15.397,69.008],"l":[15.102,69.008,15.038,69.001,15.038,68.894,14.872,68.914,14.838,68.887,14.849,68.848,14.802,68.791,14.725,68.8,14.69,68.815,14.554,68.819,14.497,68.772,14.373,68.711,14.405,68.663,14.521,68.633,14.612,68.638,14.743,68.677,14.793,68.668,14.804,68.638,14.89,68.611,15.027,68.606,15.222,68.616,15.348,68.672,15.397,68.784,15.337,68.842,15.207,68.943,15.176,68.982,15.128,69.004,15.102,69.008]},{"b":[10.74,64.838,11.246,64.979],"l":[11.133,64.976,11.021,64.979,10.813,64.923,10.74,64.87,10.833,64.843,11.063,64.86,11.179,64.838,11.231,64.866,11.246,64.908,11.133,64.976]},{"b":[4.93,60.189,5.089,60.452],"l":[4.956,60.243,4.997,60.198,5.089,60.189,5.086,60.308,5.05,60.389,4.991,60.452,4.957,60.447,4.93,60.412,4.951,60.341,4.944,60.272,4.956,60.243]},{"b":[11.765,65.596,12.003,65.706],"l":[11.972,65.702,11.875,65.706,11.8,65.684,11.765,65.631,11.778,65.605,11.902,65.596,11.968,65.627,12.003,65.679,11.972,65.702]},{"b":[-79.699,75.826,-78.845,76.146],"l":[-79.178,76.092,-79.382,76.011,-79.551,75.958,-79.699,75.883,-79.639,75.843,-79.545,75.826,-79.356,75.831,-79.124,75.87,-79.052,75.867,-79.063,75.926,-79.057,75.985,-78.946,76.025,-78.845,76.106,-78.926,76.135,-79.009,76.146,-79.178,76.092]},{"b":[-72.495,77.316,-71.433,77.467],"l":[-72.089,77.467,-72.247,77.464,-72.436,77.448,-72.49,77.432,-72.495,77.386,-72.374,77.355,-72.024,77.316,-71.667,77.325,-71.467,77.354,-71.433,77.394,-71.552,77.403,-71.733,77.432,-71.983,77.46,-72.089,77.467]},{"b":[-47.351,82.066,-44.75,82.692],"l":[-46.399,82.692,-46.787,82.666,-47.272,82.657,-47.351,82.599,-47.308,82.533,-46.752,82.348,-46.161,82.278,-45.491,82.172,-45.067,82.066,-44.865,82.084,-44.776,82.242,-44.75,82.401,-44.917,82.481,-45.411,82.578,-46.399,82.692]},{"b":[-19.611,81.814,-18.665,82.123],"l":[-19.315,82.123,-19.495,82.117,-19.611,82.078,-19.594,81.991,-19.369,81.917,-19.031,81.827,-18.768,81.814,-18.665,81.846,-18.813,81.949,-19.067,82.049,-19.315,82.123]},{"b":[-19.138,79.711,-17.401,80.055],"l":[-18.036,79.711,-17.612,79.826,-17.401,79.94,-17.471,80.029,-17.983,80.055,-18.547,80.011,-18.997,79.94,-19.138,79.852,-19.032,79.773,-18.662,79.72,-18.036,79.711]},{"b":[-19.315,77.939,-18.882,78.441],"l":[-19.112,78.424,-19.315,78.344,-19.297,78.185,-19.218,78.044,-19.129,77.939,-18.997,77.974,-18.882,78.115,-18.953,78.212,-18.953,78.353,-18.935,78.424,-19.006,78.441,-19.112,78.424]},{"b":[-19.085,76.016,-18.583,76.704],"l":[-18.662,76.404,-18.583,76.042,-18.697,76.016,-19.085,76.43,-19.085,76.58,-19.059,76.695,-18.882,76.704,-18.733,76.642,-18.662,76.404]},{"b":[-18.891,74.993,-17.392,75.407],"l":[-18.636,75.39,-18.856,75.319,-18.882,75.196,-18.891,75.072,-18.671,75.002,-18.353,75.01,-17.586,74.993,-17.392,75.037,-17.498,75.152,-17.762,75.143,-17.886,75.204,-17.921,75.302,-18.001,75.407,-18.23,75.372,-18.45,75.328,-18.636,75.39]},{"b":[-18.22,77.642,-17.641,77.875],"l":[-17.904,77.863,-18.174,77.714,-18.22,77.668,-18.148,77.642,-17.954,77.642,-17.729,77.706,-17.641,77.782,-17.681,77.859,-17.814,77.875,-17.904,77.863]},{"b":[-72.818,59.815,-11.426,83.6],"l":[-51.21,68.42,-51.293,68.416,-51.456,68.394,-51.478,68.384,-51.475,68.365,-51.632,68.273,-51.804,68.252,-52.199,68.221,-52.379,68.219,-52.698,68.262,-52.747,68.278,-52.78,68.31,-53.173,68.303,-53.29,68.293,-53.383,68.297,-53.337,68.352,-53.213,68.413,-53.039,68.611,-52.894,68.662,-52.605,68.709,-52.303,68.701,-51.781,68.548,-51.623,68.535,-51.479,68.547,-51.133,68.598,-51.07,68.619,-50.946,68.683,-50.801,68.791,-50.808,68.817,-51.03,68.756,-51.149,68.74,-51.249,68.74,-51.156,68.938,-51.12,69.091,-51.085,69.128,-50.792,69.117,-50.393,69.137,-50.297,69.171,-50.299,69.185,-50.459,69.206,-50.537,69.248,-50.671,69.234,-50.851,69.206,-51.077,69.209,-51.058,69.275,-50.892,69.412,-50.875,69.474,-50.811,69.599,-50.804,69.663,-50.72,69.725,-50.459,69.77,-50.349,69.796,-50.343,69.825,-50.5,69.936,-50.46,69.966,-50.337,69.994,-50.292,70.014,-50.323,70.027,-50.436,70.039,-50.61,70.015,-50.802,70.003,-50.973,70.04,-51.106,70.057,-51.19,70.052,-51.419,69.989,-51.499,69.987,-51.598,70.005,-52.255,70.059,-52.336,70.078,-52.571,70.172,-52.765,70.234,-53.023,70.302,-53.358,70.353,-53.769,70.389,-54.014,70.422,-54.136,70.468,-54.343,70.571,-54.501,70.657,-54.531,70.699,-54.438,70.752,-54.344,70.789,-54.166,70.82,-53.859,70.81,-53.694,70.796,-53.513,70.767,-53.376,70.761,-53.091,70.769,-52.802,70.751,-52.63,70.73,-52.405,70.687,-51.784,70.503,-51.524,70.439,-51.412,70.432,-50.947,70.364,-50.872,70.365,-50.682,70.397,-50.663,70.418,-50.728,70.438,-50.933,70.454,-51.173,70.529,-51.323,70.589,-51.34,70.688,-51.32,70.743,-51.283,70.768,-51.257,70.853,-51.396,70.903,-51.494,70.919,-51.753,70.992,-51.774,71.01,-51.65,71.019,-51.528,71.014,-51.267,70.977,-51.13,70.972,-51.03,70.986,-51.019,71.001,-51.178,71.043,-51.377,71.119,-51.792,71.13,-52.061,71.122,-52.234,71.148,-52.417,71.19,-52.535,71.2,-52.775,71.174,-52.897,71.171,-53.008,71.18,-53.117,71.313,-53.088,71.353,-53.002,71.37,-52.937,71.413,-52.892,71.458,-52.749,71.502,-51.967,71.599,-51.77,71.672,-51.779,71.683,-51.912,71.669,-52.082,71.637,-52.196,71.63,-52.656,71.672,-52.728,71.663,-52.915,71.602,-53.168,71.536,-53.284,71.54,-53.44,71.579,-53.465,71.607,-53.476,71.64,-53.305,71.686,-53.25,71.71,-53.139,71.775,-53.145,71.807,-53.334,71.79,-53.358,71.82,-53.355,71.871,-53.374,71.936,-53.42,72,-53.575,72.098,-53.64,72.123,-53.693,72.16,-53.81,72.293,-53.776,72.326,-53.672,72.351,-53.652,72.363,-53.901,72.342,-53.928,72.319,-53.881,72.285,-53.847,72.24,-53.828,72.183,-53.793,72.134,-53.703,72.08,-53.631,72.052,-53.514,71.976,-53.462,71.894,-53.478,71.85,-53.569,71.806,-53.715,71.758,-53.76,71.718,-53.78,71.679,-53.894,71.642,-53.964,71.656,-54.02,71.658,-53.954,71.593,-53.912,71.526,-53.963,71.459,-54.099,71.419,-54.173,71.417,-54.318,71.384,-54.689,71.367,-54.818,71.375,-55.055,71.409,-55.336,71.427,-55.448,71.472,-55.594,71.554,-55.668,71.627,-55.669,71.692,-55.63,71.739,-55.549,71.768,-55.452,71.958,-55.316,72.111,-54.971,72.268,-54.873,72.325,-54.84,72.356,-54.841,72.379,-54.896,72.394,-55.32,72.2,-55.581,72.179,-55.659,72.223,-55.636,72.3,-55.589,72.319,-55.378,72.311,-55.296,72.354,-55.428,72.42,-55.569,72.437,-55.602,72.453,-55.457,72.503,-55.122,72.5,-55.046,72.534,-54.925,72.572,-54.79,72.642,-54.74,72.7,-54.729,72.75,-54.758,72.791,-54.761,72.832,-54.738,72.873,-54.773,72.918,-54.866,72.967,-55.073,73.015,-55.134,72.961,-55.198,72.938,-55.289,72.933,-55.372,72.956,-55.46,72.964,-55.545,72.985,-55.634,72.991,-55.669,73.008,-55.691,73.054,-55.693,73.113,-55.592,73.14,-55.452,73.162,-55.359,73.203,-55.297,73.262,-55.288,73.327,-55.332,73.397,-55.446,73.46,-55.656,73.399,-55.739,73.384,-55.758,73.428,-55.787,73.46,-55.876,73.505,-55.992,73.537,-56.104,73.558,-56.109,73.591,-56.083,73.627,-56.033,73.67,-55.968,73.76,-55.897,73.752,-55.838,73.76,-55.872,73.833,-55.929,73.895,-55.997,73.931,-55.999,73.946,-56.014,73.964,-56.066,74.007,-56.124,74.039,-56.225,74.129,-56.298,74.163,-56.392,74.181,-56.493,74.182,-56.655,74.159,-56.954,74.131,-57.191,74.118,-57.231,74.125,-57.112,74.159,-56.937,74.195,-56.706,74.219,-56.639,74.278,-56.664,74.33,-56.654,74.378,-56.718,74.429,-56.656,74.458,-56.446,74.486,-56.35,74.49,-56.255,74.527,-56.522,74.614,-56.801,74.672,-56.871,74.695,-56.933,74.733,-56.986,74.787,-57.072,74.84,-57.191,74.894,-57.365,74.945,-57.813,75.04,-57.967,75.105,-58.109,75.205,-58.18,75.247,-58.253,75.279,-58.566,75.353,-58.603,75.385,-58.281,75.472,-58.25,75.507,-58.381,75.612,-58.516,75.689,-58.663,75.716,-58.881,75.73,-59.082,75.765,-59.264,75.819,-59.445,75.859,-59.717,75.896,-60.173,75.993,-60.875,76.097,-61.188,76.158,-61.375,76.18,-61.621,76.186,-62.097,76.242,-62.496,76.26,-62.743,76.252,-62.823,76.262,-63.006,76.319,-63.291,76.352,-63.439,76.339,-63.622,76.278,-63.843,76.217,-63.96,76.209,-64.135,76.265,-64.223,76.303,-64.307,76.317,-64.387,76.304,-64.543,76.253,-64.692,76.216,-64.912,76.173,-65.088,76.152,-65.313,76.146,-65.37,76.131,-65.457,76.13,-65.574,76.144,-65.683,76.173,-65.785,76.215,-65.876,76.238,-65.954,76.242,-66.134,76.22,-66.362,76.155,-66.466,76.139,-66.553,76.146,-66.66,76.166,-66.874,76.218,-66.993,76.213,-67.079,76.195,-67.055,76.152,-66.854,76.05,-66.675,75.977,-66.826,75.969,-68.149,76.067,-68.317,76.091,-68.561,76.15,-68.763,76.187,-69.108,76.281,-69.373,76.332,-69.461,76.372,-69.484,76.399,-69.4,76.436,-68.865,76.561,-68.661,76.587,-68.245,76.617,-68.147,76.636,-68.114,76.651,-68.223,76.678,-68.767,76.668,-69.252,76.686,-69.674,76.736,-69.747,76.752,-69.819,76.783,-69.888,76.827,-69.872,76.877,-69.771,76.931,-69.712,76.969,-69.694,76.989,-70.224,76.855,-70.441,76.807,-70.613,76.822,-70.734,76.844,-70.793,76.869,-70.791,76.896,-70.771,76.917,-70.735,76.929,-71.015,76.985,-71.141,77.029,-71.155,77.074,-71.055,77.121,-70.958,77.154,-70.863,77.175,-70.604,77.194,-69.657,77.229,-68.978,77.195,-68.747,77.307,-68.592,77.343,-68.136,77.38,-67.434,77.385,-66.938,77.364,-66.706,77.338,-66.389,77.28,-66.371,77.298,-66.448,77.35,-66.453,77.393,-66.325,77.468,-66.266,77.515,-66.306,77.565,-66.445,77.616,-66.691,77.681,-66.824,77.687,-66.971,77.671,-67.147,77.635,-67.515,77.543,-67.688,77.524,-67.977,77.519,-68.137,77.53,-68.292,77.544,-68.533,77.593,-68.622,77.602,-68.728,77.581,-68.854,77.529,-68.975,77.493,-69.091,77.472,-69.2,77.463,-69.351,77.467,-69.977,77.548,-70.118,77.583,-70.126,77.638,-70.318,77.69,-70.535,77.7,-70.562,77.717,-70.287,77.798,-70.081,77.831,-70.114,77.841,-70.412,77.843,-70.614,77.8,-70.729,77.793,-70.994,77.792,-71.272,77.813,-71.39,77.832,-71.512,77.875,-71.65,77.9,-72.065,77.937,-72.159,77.957,-72.247,77.99,-72.586,78.085,-72.792,78.155,-72.818,78.194,-72.581,78.279,-72.571,78.299,-72.672,78.335,-72.715,78.362,-72.68,78.4,-72.473,78.482,-72.396,78.504,-72.024,78.553,-71.651,78.623,-71.516,78.639,-71.395,78.643,-70.906,78.638,-70.754,78.656,-70.625,78.69,-70.414,78.725,-69.974,78.778,-68.993,78.857,-68.93,78.867,-68.924,78.882,-69.012,78.923,-69.031,78.943,-68.83,78.98,-68.377,79.038,-68.068,79.066,-67.868,79.068,-67.708,79.08,-67.482,79.117,-67.355,79.123,-66.584,79.138,-66.243,79.118,-66.075,79.118,-65.968,79.132,-65.826,79.174,-65.56,79.276,-65.42,79.34,-65.288,79.437,-65.117,79.589,-64.989,79.737,-64.905,79.881,-64.839,79.969,-64.792,80.001,-64.632,80.041,-64.466,80.072,-64.179,80.099,-64.205,80.112,-64.327,80.134,-64.44,80.142,-64.545,80.137,-64.735,80.104,-64.982,80.082,-65.222,80.086,-65.395,80.078,-65.553,80.048,-65.81,80.024,-65.982,80.029,-66.292,80.072,-66.448,80.08,-66.844,80.076,-66.959,80.092,-67.061,80.123,-67.141,80.166,-67.201,80.222,-67.193,80.28,-67.051,80.385,-66.996,80.413,-66.61,80.53,-66.372,80.584,-66.136,80.625,-65.963,80.649,-65.801,80.66,-65.645,80.685,-65.358,80.767,-65.062,80.836,-64.694,80.966,-64.516,81,-63.892,81.056,-63.722,81.057,-63.578,81.043,-63.442,81.014,-63.059,80.886,-63.029,80.89,-63.095,80.938,-63.235,81.083,-63.212,81.143,-62.993,81.207,-62.903,81.218,-62.672,81.214,-62.299,81.194,-62.049,81.173,-61.86,81.138,-61.636,81.116,-61.519,81.117,-61.436,81.134,-61.317,81.188,-61.162,81.281,-61.1,81.396,-61.131,81.532,-61.176,81.632,-61.236,81.695,-61.203,81.747,-61.015,81.81,-60.843,81.855,-60.432,81.92,-60.099,81.937,-59.902,81.933,-59.642,81.903,-59.282,81.884,-58.957,81.825,-58.43,81.69,-58.08,81.622,-57.79,81.592,-57.505,81.54,-57.083,81.43,-56.862,81.383,-56.731,81.366,-56.615,81.363,-56.658,81.394,-56.86,81.46,-57.168,81.532,-57.853,81.662,-58.23,81.754,-58.568,81.858,-58.817,81.92,-59.268,81.982,-59.262,82.007,-58.717,82.093,-57.717,82.168,-56.589,82.227,-56.212,82.221,-55.549,82.246,-55.486,82.283,-55.344,82.3,-54.726,82.351,-54.549,82.351,-54.277,82.326,-53.987,82.279,-53.853,82.237,-53.671,82.164,-53.582,82.062,-53.596,81.738,-53.591,81.677,-53.556,81.653,-53.43,81.688,-53.28,81.754,-53.145,81.8,-53.041,81.871,-52.969,81.967,-52.926,82.038,-53.102,82.119,-53.111,82.251,-53.023,82.322,-52.776,82.322,-51.754,82.078,-51.352,82.026,-50.894,81.895,-50.36,81.909,-49.867,81.893,-49.649,81.898,-49.541,81.918,-49.694,81.972,-50.395,82.121,-50.713,82.237,-50.936,82.383,-50.99,82.46,-50.82,82.474,-50.037,82.472,-48.861,82.405,-47.357,82.174,-46.617,82.097,-45.291,81.829,-44.891,81.788,-44.729,81.78,-44.608,81.813,-44.532,81.849,-44.527,81.897,-44.591,81.957,-44.628,82.026,-44.637,82.104,-44.547,82.26,-44.333,82.311,-44.239,82.368,-44.327,82.472,-44.577,82.543,-45.552,82.725,-45.557,82.747,-45.36,82.771,-45.067,82.785,-42.651,82.741,-42.233,82.725,-42.055,82.71,-41.977,82.689,-41.876,82.68,-41.357,82.705,-41.37,82.75,-41.434,82.779,-44.239,82.857,-44.762,82.884,-45.028,82.886,-45.303,82.865,-45.873,82.855,-46.137,82.859,-46.478,82.952,-46.169,83.064,-45.909,83.061,-45.415,83.018,-45.122,83.079,-44.657,83.129,-44.197,83.147,-43.195,83.255,-43.009,83.265,-42.776,83.259,-42.26,83.232,-42.055,83.205,-41.82,83.148,-41.683,83.13,-41.522,83.127,-41.3,83.101,-40.979,83.185,-40.689,83.275,-40.357,83.332,-39.886,83.299,-39.588,83.256,-39.316,83.204,-38.931,83.175,-38.278,82.999,-38.156,82.999,-38.099,83.014,-38.037,83.046,-38.015,83.095,-37.935,83.161,-37.993,83.185,-38.54,83.258,-38.643,83.286,-38.748,83.333,-38.75,83.371,-38.648,83.401,-38.541,83.415,-38.188,83.402,-38.071,83.412,-37.961,83.438,-37.828,83.486,-37.723,83.498,-37.487,83.499,-37.123,83.468,-36.804,83.466,-36.69,83.48,-36.672,83.51,-36.644,83.529,-36.606,83.537,-35.452,83.539,-35.166,83.546,-34.942,83.568,-34.668,83.571,-34.428,83.558,-34.132,83.529,-33.837,83.53,-33.398,83.577,-32.984,83.6,-30.703,83.593,-29.953,83.565,-28.992,83.505,-28.484,83.435,-28.377,83.437,-27.688,83.41,-27.034,83.377,-25.947,83.29,-25.795,83.261,-25.912,83.238,-26.183,83.221,-27.572,83.193,-30.092,83.157,-31.534,83.089,-31.993,83.085,-32.033,82.983,-31.837,82.978,-31.516,82.992,-30.386,83.094,-29.964,83.11,-29.175,83.102,-28.151,83.064,-27.739,83.077,-27.002,83.067,-26.141,83.096,-25.123,83.16,-24.845,83.019,-24.47,82.877,-24.174,82.893,-23.92,82.885,-23.834,82.839,-23.695,82.819,-23.407,82.83,-22.525,82.789,-21.92,82.716,-21.692,82.683,-21.583,82.634,-21.521,82.595,-21.616,82.548,-21.994,82.463,-22.473,82.385,-23.118,82.325,-23.862,82.287,-29.579,82.161,-29.773,82.131,-29.887,82.055,-29.811,81.955,-29.544,81.94,-28.919,81.996,-27.84,82.049,-27.046,82.046,-25.149,82.001,-24.589,81.883,-24.293,81.701,-23.637,81.742,-23.496,81.773,-23.393,81.827,-23.311,81.885,-23.249,81.947,-23.18,81.989,-23.104,82.012,-22.94,82.031,-22.563,82.053,-21.576,82.075,-21.338,82.069,-21.167,81.984,-21.13,81.934,-21.118,81.87,-21.123,81.79,-21.147,81.695,-21.231,81.601,-21.504,81.438,-21.724,81.348,-21.961,81.284,-22.415,81.137,-22.573,81.098,-23.072,80.927,-23.196,80.847,-23.204,80.789,-23.118,80.778,-22.973,80.833,-22.919,80.872,-22.826,80.913,-22.089,81.02,-21.931,81.05,-21.45,81.178,-21.142,81.226,-20.89,81.276,-20.756,81.312,-20.016,81.564,-19.63,81.64,-19.225,81.64,-19.153,81.512,-18.667,81.492,-18.457,81.498,-18.118,81.467,-17.969,81.441,-17.717,81.428,-17.456,81.398,-17.226,81.43,-17.159,81.451,-16.937,81.544,-16.637,81.626,-16.359,81.729,-16.267,81.754,-16.121,81.777,-15.969,81.785,-15.556,81.834,-15.451,81.837,-15.227,81.822,-14.242,81.814,-13.704,81.789,-12.956,81.72,-12.434,81.683,-12.193,81.649,-11.841,81.578,-11.557,81.503,-11.426,81.481,-11.431,81.457,-11.529,81.424,-12.231,81.309,-12.461,81.233,-13.126,81.088,-13.451,81.038,-13.804,81.019,-14.197,81.014,-14.452,80.993,-14.49,80.973,-14.308,80.913,-14.229,80.87,-14.24,80.832,-14.431,80.776,-14.504,80.763,-15.194,80.721,-15.543,80.65,-15.998,80.642,-16.319,80.65,-16.761,80.573,-16.588,80.511,-16.429,80.484,-15.937,80.428,-15.933,80.395,-16.168,80.329,-16.489,80.252,-16.868,80.198,-17.011,80.19,-17.191,80.204,-17.357,80.201,-17.723,80.176,-18.071,80.172,-18.693,80.207,-19.029,80.248,-19.206,80.262,-19.429,80.258,-19.515,80.241,-19.867,80.145,-20.04,80.079,-20.15,80.011,-20.197,79.938,-20.181,79.858,-20.138,79.803,-20.069,79.774,-19.985,79.756,-19.839,79.746,-19.518,79.755,-19.392,79.75,-19.353,79.734,-19.284,79.683,-19.296,79.635,-19.354,79.567,-19.399,79.488,-19.431,79.398,-19.414,79.349,-19.284,79.338,-19.223,79.342,-19.152,79.325,-19.072,79.289,-19.011,79.251,-18.971,79.211,-18.992,79.178,-19.075,79.152,-19.262,79.123,-19.723,79.065,-19.77,79.047,-19.806,79.012,-19.832,78.959,-19.887,78.911,-19.973,78.868,-20.05,78.842,-20.2,78.83,-20.396,78.829,-20.616,78.804,-21.134,78.659,-21.141,78.643,-20.947,78.596,-20.956,78.555,-21.195,78.38,-21.26,78.293,-21.312,78.174,-21.397,78.074,-21.516,77.992,-21.633,77.897,-21.748,77.791,-21.73,77.709,-21.579,77.651,-21.38,77.698,-21.132,77.847,-20.863,77.912,-20.572,77.892,-20.319,77.862,-19.995,77.803,-19.724,77.767,-19.49,77.719,-19.394,77.678,-19.297,77.621,-19.296,77.585,-19.468,77.566,-19.524,77.572,-19.953,77.666,-20.162,77.69,-20.439,77.662,-20.681,77.619,-20.464,77.447,-20.232,77.368,-19.809,77.332,-19.588,77.294,-19.426,77.246,-19.3,77.222,-19.131,77.233,-18.903,77.28,-18.586,77.283,-18.443,77.259,-18.339,77.215,-18.292,77.133,-18.303,77.012,-18.337,76.921,-18.396,76.86,-18.51,76.778,-18.606,76.763,-18.74,76.768,-18.865,76.785,-18.981,76.814,-19.156,76.837,-19.509,76.861,-19.865,76.914,-20.064,76.928,-20.487,76.921,-20.942,76.887,-20.96,76.843,-21.615,76.688,-21.749,76.69,-21.931,76.743,-22.185,76.794,-22.334,76.794,-22.555,76.729,-22.609,76.704,-22.607,76.681,-22.444,76.625,-22.379,76.612,-22.295,76.601,-22.004,76.588,-21.877,76.573,-21.758,76.401,-21.569,76.294,-21.488,76.272,-21.417,76.264,-21.185,76.268,-20.887,76.304,-20.783,76.275,-20.564,76.24,-20.435,76.231,-20.279,76.232,-20.104,76.219,-19.863,76.121,-19.958,75.997,-19.807,75.897,-19.566,75.795,-19.509,75.758,-19.486,75.69,-19.48,75.645,-19.462,75.604,-19.431,75.567,-19.4,75.494,-19.366,75.386,-19.375,75.298,-19.426,75.23,-19.526,75.18,-19.676,75.149,-19.798,75.157,-19.893,75.205,-20.027,75.255,-20.199,75.308,-20.485,75.314,-20.906,75.157,-21.094,75.149,-21.247,75.133,-21.409,75.065,-21.649,75.023,-21.861,75.04,-22.233,75.12,-22.098,75.066,-21.904,75.004,-21.784,74.971,-21.695,74.964,-21.598,74.972,-21.457,74.998,-21.141,75.069,-21.057,75.079,-20.986,75.074,-20.928,75.053,-20.861,74.993,-20.786,74.892,-20.795,74.806,-20.89,74.735,-20.971,74.69,-21.038,74.67,-21.038,74.654,-20.862,74.636,-20.611,74.728,-20.532,74.843,-20.417,74.975,-20.214,75.019,-19.985,74.975,-19.8,74.852,-19.538,74.625,-19.427,74.601,-19.287,74.546,-19.225,74.479,-19.242,74.4,-19.272,74.343,-19.315,74.307,-19.369,74.284,-19.467,74.269,-19.646,74.258,-20.048,74.282,-20.256,74.283,-20.231,74.205,-20.653,74.137,-21.129,74.111,-21.581,74.163,-21.955,74.244,-21.832,74.357,-21.762,74.483,-21.943,74.566,-21.983,74.567,-21.92,74.439,-21.973,74.39,-22.177,74.33,-22.322,74.303,-22.334,74.286,-22.264,74.272,-22.217,74.246,-22.196,74.206,-22.22,74.166,-22.291,74.125,-22.329,74.091,-22.335,74.063,-22.271,74.03,-22.135,73.99,-21.988,73.971,-21.298,73.962,-21.022,73.941,-20.367,73.848,-20.338,73.82,-20.449,73.653,-20.51,73.493,-20.637,73.464,-21.326,73.457,-21.548,73.432,-21.873,73.358,-22.185,73.27,-22.347,73.269,-22.987,73.346,-23.233,73.398,-23.761,73.543,-24.158,73.764,-24.34,73.672,-24.451,73.629,-24.566,73.606,-24.677,73.602,-24.784,73.618,-24.905,73.653,-25.109,73.734,-25.351,73.814,-25.521,73.852,-25.528,73.841,-25.427,73.794,-25.281,73.74,-24.909,73.58,-24.778,73.54,-24.791,73.511,-25.026,73.486,-25.311,73.431,-25.45,73.391,-25.665,73.293,-25.74,73.278,-26.062,73.253,-26.169,73.259,-26.407,73.313,-26.765,73.348,-26.977,73.38,-27.27,73.436,-27.169,73.374,-26.604,73.279,-26.542,73.249,-26.658,73.192,-26.729,73.171,-26.863,73.167,-27.062,73.179,-27.265,73.176,-27.472,73.16,-27.562,73.138,-27.533,73.113,-27.483,73.089,-27.413,73.068,-27.348,73.068,-27.19,73.132,-27.07,73.137,-26.753,73.121,-26.433,73.171,-26.202,73.193,-26.029,73.199,-25.399,73.276,-25.268,73.362,-25.057,73.396,-24.587,73.423,-24.133,73.409,-23.899,73.398,-23.71,73.317,-23.456,73.259,-23.244,73.193,-22.996,73.172,-22.852,73.084,-22.45,72.986,-22.194,72.965,-22.036,72.918,-22.023,72.721,-22.007,72.635,-22.075,72.399,-22.28,72.345,-22.239,72.22,-22.293,72.12,-22.498,72.158,-22.707,72.224,-23.208,72.327,-23.674,72.393,-23.856,72.452,-24.069,72.499,-24.359,72.687,-24.547,72.922,-24.63,73.038,-24.789,73.044,-24.992,73.013,-25.171,72.98,-25.256,72.924,-25.861,72.847,-26.08,72.794,-26.206,72.796,-26.658,72.716,-26.477,72.678,-26.392,72.673,-26.209,72.694,-26.1,72.722,-25.688,72.797,-25.357,72.81,-25.237,72.843,-24.985,72.889,-24.813,72.902,-24.789,72.89,-24.771,72.869,-24.65,72.583,-24.701,72.506,-24.837,72.473,-25.128,72.419,-25.204,72.393,-25.118,72.347,-24.844,72.39,-24.667,72.437,-24.572,72.42,-24.417,72.348,-24.242,72.311,-23.798,72.201,-23.587,72.14,-23.291,72.081,-22.956,71.999,-22.869,71.971,-22.562,71.928,-22.497,71.914,-22.37,71.77,-22.265,71.754,-21.96,71.745,-22.013,71.689,-22.311,71.565,-22.465,71.525,-22.503,71.5,-22.489,71.457,-22.48,71.383,-22.418,71.249,-22.348,71.373,-22.299,71.432,-22.234,71.45,-22.17,71.453,-21.961,71.508,-21.752,71.478,-21.698,71.337,-21.671,71.206,-21.69,71.092,-21.667,70.916,-21.675,70.856,-21.625,70.805,-21.574,70.59,-21.523,70.526,-21.626,70.469,-21.944,70.443,-22.069,70.472,-22.384,70.462,-22.385,70.513,-22.4,70.571,-22.401,70.612,-22.422,70.649,-22.437,70.86,-22.526,70.808,-22.531,70.765,-22.555,70.721,-22.61,70.493,-22.691,70.437,-22.943,70.451,-23.191,70.442,-23.328,70.451,-23.792,70.555,-23.971,70.649,-24.13,70.791,-24.229,70.923,-24.266,71.046,-24.377,71.146,-24.562,71.224,-24.781,71.286,-25.033,71.334,-25.255,71.396,-25.446,71.471,-25.656,71.53,-25.885,71.572,-26.211,71.59,-26.689,71.583,-27.011,71.631,-27.087,71.627,-27.162,71.602,-27.107,71.533,-26.737,71.501,-26.452,71.494,-26.074,71.498,-25.843,71.48,-25.758,71.44,-25.699,71.368,-25.668,71.265,-25.742,71.184,-26.014,71.093,-26.158,71.05,-26.576,70.969,-26.718,70.95,-27.067,70.945,-27.336,70.953,-27.689,70.993,-27.889,71.002,-28.303,71.007,-28.398,70.993,-28.292,70.949,-28.116,70.925,-27.992,70.895,-27.979,70.84,-28.024,70.757,-28.07,70.699,-28.146,70.656,-28.417,70.574,-28.53,70.548,-29.037,70.462,-29.072,70.445,-28.953,70.447,-28.633,70.478,-28.541,70.477,-28.015,70.402,-27.596,70.407,-26.747,70.476,-26.677,70.474,-26.622,70.463,-26.565,70.438,-26.508,70.397,-26.577,70.357,-26.771,70.319,-27.073,70.281,-27.203,70.256,-27.328,70.217,-27.561,70.124,-27.629,70.028,-27.384,69.992,-27.274,70.038,-27.144,70.141,-27.028,70.201,-26.752,70.242,-26.416,70.221,-26.156,70.246,-25.625,70.347,-25.53,70.353,-24.749,70.295,-24.041,70.181,-23.667,70.139,-23.173,70.115,-22.284,70.126,-22.207,70.108,-22.235,70.068,-22.287,70.033,-22.435,69.986,-22.615,69.954,-22.726,69.945,-22.821,69.923,-23.034,69.901,-23.088,69.883,-23.015,69.805,-23.05,69.793,-23.237,69.791,-23.553,69.741,-23.812,69.744,-23.866,69.737,-23.817,69.718,-23.764,69.681,-23.709,69.627,-23.739,69.589,-23.856,69.566,-23.944,69.558,-24.248,69.59,-24.297,69.586,-24.252,69.562,-24.227,69.527,-24.221,69.479,-24.296,69.439,-24.451,69.407,-24.741,69.318,-24.867,69.293,-25.133,69.272,-25.189,69.261,-25.08,69.192,-25.092,69.165,-25.272,69.092,-25.544,69.046,-25.581,69.021,-25.606,68.954,-25.626,68.928,-25.698,68.89,-25.956,68.817,-26.139,68.781,-26.229,68.752,-26.341,68.702,-26.483,68.676,-26.654,68.673,-26.815,68.654,-27.081,68.602,-27.266,68.584,-27.851,68.494,-28.126,68.479,-28.365,68.447,-28.854,68.36,-29.088,68.332,-29.25,68.299,-29.426,68.289,-29.714,68.311,-29.869,68.312,-29.964,68.299,-30.051,68.272,-30.196,68.199,-30.318,68.193,-30.72,68.251,-30.712,68.225,-30.606,68.162,-30.611,68.118,-30.85,68.073,-30.979,68.061,-31.168,68.08,-31.419,68.128,-31.742,68.23,-32.137,68.385,-32.327,68.437,-32.314,68.388,-32.27,68.339,-32.195,68.292,-32.18,68.257,-32.224,68.236,-32.282,68.225,-32.355,68.225,-32.367,68.213,-32.249,68.139,-32.156,68.063,-32.165,67.991,-32.275,67.923,-32.37,67.883,-32.449,67.871,-32.918,67.701,-33.049,67.679,-33.108,67.658,-33.157,67.627,-33.294,67.486,-33.349,67.443,-33.458,67.387,-33.504,67.377,-33.518,67.354,-33.498,67.318,-33.528,67.258,-33.608,67.174,-33.881,66.942,-34.102,66.726,-34.198,66.655,-34.269,66.625,-34.314,66.636,-34.423,66.63,-34.476,66.592,-34.524,66.523,-34.576,66.471,-34.633,66.435,-35.075,66.279,-35.189,66.25,-35.291,66.269,-35.412,66.262,-35.662,66.344,-35.705,66.374,-35.867,66.441,-35.862,66.406,-35.835,66.387,-35.812,66.358,-35.756,66.324,-35.63,66.14,-35.729,66.102,-35.818,66.059,-36.044,65.987,-36.289,65.865,-36.379,65.831,-36.399,65.93,-36.389,65.96,-36.527,66.008,-36.523,65.973,-36.537,65.941,-36.637,65.812,-36.665,65.79,-36.715,65.795,-36.822,65.771,-36.932,65.783,-37.026,65.841,-37.063,65.871,-37.233,65.788,-37.316,65.79,-37.33,65.72,-37.41,65.656,-37.516,65.629,-37.664,65.631,-37.754,65.593,-37.955,65.634,-38.001,65.71,-37.842,65.814,-37.797,65.857,-37.827,65.91,-37.788,65.978,-37.484,66.195,-37.279,66.304,-37.291,66.324,-37.57,66.348,-37.814,66.385,-38.052,66.398,-38.157,66.386,-37.989,66.323,-37.752,66.262,-37.869,66.203,-37.969,66.141,-38.073,65.973,-38.14,65.904,-38.398,65.983,-38.52,66.01,-38.443,65.922,-38.216,65.838,-38.202,65.811,-38.203,65.712,-38.637,65.624,-39.089,65.611,-39.413,65.586,-39.961,65.556,-40.174,65.556,-40.192,65.523,-39.656,65.369,-39.578,65.341,-39.653,65.288,-39.763,65.255,-39.937,65.142,-40.028,65.103,-40.253,65.049,-40.668,65.109,-40.881,65.082,-41.084,65.101,-41.089,65.035,-41.028,64.988,-40.966,64.869,-40.829,64.878,-40.655,64.915,-40.521,64.825,-40.433,64.673,-40.278,64.596,-40.21,64.536,-40.182,64.48,-40.278,64.424,-40.478,64.344,-40.699,64.33,-40.686,64.267,-40.782,64.222,-40.985,64.235,-41.079,64.267,-41.178,64.281,-41.581,64.298,-41.175,64.177,-41.031,64.121,-40.966,64.154,-40.826,64.163,-40.618,64.132,-40.652,63.928,-40.561,63.762,-40.55,63.725,-40.772,63.626,-40.775,63.534,-40.907,63.508,-41.049,63.514,-41.056,63.412,-41.152,63.349,-41.135,63.309,-41.108,63.274,-41.195,63.209,-41.275,63.131,-41.388,63.062,-41.448,63.069,-41.628,63.065,-41.844,63.07,-42.02,63.16,-42.092,63.189,-42.175,63.209,-42.143,63.151,-42.094,63.117,-41.932,63.052,-41.634,62.972,-41.644,62.916,-41.723,62.891,-41.909,62.737,-41.975,62.734,-42.058,62.694,-42.316,62.707,-42.424,62.723,-42.741,62.713,-42.849,62.727,-42.942,62.72,-42.855,62.677,-42.674,62.638,-42.467,62.598,-42.153,62.568,-42.164,62.512,-42.243,62.466,-42.198,62.397,-42.233,62.348,-42.248,62.289,-42.321,62.153,-42.236,62.059,-42.143,62.014,-42.154,61.953,-42.11,61.857,-42.25,61.771,-42.365,61.775,-42.53,61.755,-42.585,61.717,-42.324,61.682,-42.347,61.617,-42.419,61.537,-42.494,61.363,-42.646,61.064,-42.717,60.767,-43.044,60.524,-43.16,60.517,-43.189,60.507,-43.348,60.52,-43.598,60.576,-43.792,60.595,-43.923,60.595,-43.94,60.567,-43.831,60.522,-43.665,60.503,-43.533,60.473,-43.296,60.445,-43.213,60.391,-43.156,60.333,-43.165,60.301,-43.165,60.263,-43.123,60.061,-43.235,59.991,-43.32,59.928,-43.617,59.937,-43.669,59.959,-43.955,60.025,-43.937,59.994,-43.73,59.904,-43.658,59.859,-43.706,59.849,-43.79,59.846,-43.907,59.815,-44.117,59.832,-44.105,59.878,-44.065,59.925,-44.162,59.917,-44.269,59.893,-44.331,59.902,-44.384,59.899,-44.413,59.923,-44.453,60.015,-44.405,60.061,-44.231,60.18,-44.176,60.244,-44.224,60.274,-44.348,60.205,-44.476,60.096,-44.533,60.029,-44.613,60.017,-44.812,60.05,-45.379,60.203,-45.362,60.296,-45.368,60.373,-45.202,60.383,-45.082,60.416,-44.975,60.457,-44.854,60.532,-44.742,60.655,-44.757,60.665,-45.083,60.507,-45.283,60.455,-45.381,60.445,-45.429,60.468,-45.59,60.519,-45.695,60.542,-45.934,60.579,-45.977,60.6,-46.047,60.616,-46.142,60.777,-46.019,60.972,-45.934,61.028,-45.88,61.094,-45.849,61.181,-45.87,61.218,-45.942,61.206,-45.976,61.176,-45.97,61.129,-46.012,61.097,-46.297,61.022,-46.582,60.962,-46.718,60.905,-46.806,60.86,-46.874,60.816,-46.98,60.82,-47.125,60.811,-47.224,60.783,-47.37,60.8,-47.465,60.843,-47.579,60.847,-47.707,60.827,-47.796,60.829,-47.789,60.8,-47.73,60.729,-47.828,60.725,-48.014,60.722,-48.108,60.742,-48.181,60.769,-48.242,60.807,-48.205,60.856,-47.906,60.946,-47.77,60.998,-47.859,61.016,-48.146,60.999,-48.194,61.013,-48.386,61.005,-48.378,61.138,-48.425,61.172,-48.428,61.187,-48.495,61.225,-48.558,61.234,-48.597,61.247,-48.922,61.277,-48.965,61.352,-48.987,61.429,-49.049,61.524,-49.205,61.549,-49.289,61.59,-49.222,61.632,-49.193,61.686,-49.265,61.71,-49.311,61.748,-49.304,61.772,-49.363,61.839,-49.38,61.89,-49.313,61.939,-49.13,61.993,-49.071,62.015,-49.04,62.039,-48.829,62.08,-49.008,62.108,-49.12,62.113,-49.202,62.099,-49.278,62.046,-49.349,62.01,-49.624,61.999,-49.664,62.017,-49.683,62.093,-49.668,62.151,-49.553,62.233,-49.685,62.273,-49.806,62.287,-49.943,62.324,-50.07,62.365,-50.179,62.411,-50.285,62.466,-50.319,62.473,-50.281,62.531,-50.259,62.578,-50.256,62.68,-50.299,62.722,-50.204,62.809,-50.076,62.904,-49.793,63.045,-50.092,62.977,-50.338,62.829,-50.39,62.822,-50.408,62.849,-50.502,62.945,-50.572,62.971,-50.604,63,-50.744,63.051,-50.804,63.091,-50.89,63.167,-51.013,63.258,-51.188,63.436,-51.469,63.642,-51.538,63.758,-51.451,63.905,-51.548,64.006,-51.28,64.053,-50.898,64.106,-50.699,64.149,-50.585,64.162,-50.342,64.17,-50.261,64.214,-50.396,64.203,-50.487,64.209,-50.492,64.229,-50.459,64.266,-50.437,64.313,-50.483,64.304,-50.721,64.223,-51.072,64.159,-51.347,64.123,-51.391,64.125,-51.487,64.103,-51.542,64.097,-51.585,64.103,-51.682,64.165,-51.708,64.205,-51.534,64.314,-51.404,64.463,-51.232,64.561,-51.11,64.573,-50.907,64.568,-50.835,64.559,-50.858,64.617,-50.849,64.645,-50.684,64.678,-50.492,64.693,-50.355,64.683,-50.269,64.615,-50.158,64.49,-50.009,64.447,-50.016,64.507,-50.093,64.585,-50.122,64.704,-50.22,64.754,-50.299,64.779,-50.517,64.767,-50.648,64.853,-50.678,64.885,-50.681,64.928,-50.812,65.052,-50.854,65.114,-50.924,65.197,-50.961,65.201,-50.914,65.097,-50.852,65.024,-50.765,64.863,-50.722,64.798,-50.78,64.746,-50.891,64.695,-50.989,64.665,-51.221,64.628,-51.171,64.708,-51.139,64.786,-51.255,64.758,-51.364,64.702,-51.401,64.623,-51.47,64.552,-51.677,64.377,-51.758,64.28,-51.835,64.232,-51.923,64.219,-51.999,64.257,-52.063,64.346,-52.093,64.416,-52.097,64.597,-52.089,64.682,-52.124,64.795,-52.235,65.061,-52.259,65.155,-52.448,65.205,-52.45,65.221,-52.5,65.275,-52.538,65.329,-52.506,65.348,-52.461,65.363,-52.18,65.442,-51.971,65.531,-51.721,65.67,-51.619,65.713,-51.253,65.746,-51.09,65.751,-51.092,65.776,-51.146,65.786,-51.394,65.779,-51.723,65.723,-51.78,65.703,-51.924,65.617,-52.035,65.569,-52.348,65.461,-52.551,65.461,-52.761,65.591,-52.995,65.566,-53.153,65.575,-53.199,65.594,-53.234,65.771,-53.106,65.977,-53.244,65.979,-53.272,65.987,-53.345,66.034,-53.392,66.048,-53.357,66.073,-53.018,66.171,-52.511,66.362,-52.293,66.438,-52.158,66.47,-52.056,66.507,-51.932,66.588,-51.891,66.623,-51.822,66.652,-51.676,66.684,-51.517,66.732,-51.259,66.841,-51.225,66.882,-51.281,66.891,-51.402,66.854,-51.648,66.754,-51.823,66.698,-52.421,66.447,-52.676,66.355,-52.814,66.297,-52.922,66.241,-53.036,66.201,-53.156,66.178,-53.413,66.16,-53.539,66.139,-53.615,66.154,-53.648,66.274,-53.623,66.344,-53.635,66.414,-53.571,66.513,-53.476,66.584,-53.436,66.622,-53.419,66.649,-53.223,66.721,-53.115,66.754,-53.038,66.827,-52.603,66.853,-52.491,66.85,-52.431,66.86,-52.387,66.881,-52.43,66.898,-52.56,66.909,-52.907,66.907,-53.227,66.919,-53.373,66.932,-53.444,66.925,-53.56,66.946,-53.687,66.986,-53.884,67.136,-53.805,67.327,-53.799,67.418,-53.548,67.498,-53.414,67.525,-53.224,67.585,-52.97,67.687,-52.666,67.75,-52.512,67.761,-52.384,67.752,-51.909,67.664,-51.665,67.646,-51.451,67.668,-51.181,67.637,-50.705,67.509,-50.613,67.528,-50.64,67.559,-51.171,67.694,-51.168,67.734,-51.032,67.744,-50.887,67.784,-50.969,67.807,-51.321,67.787,-51.423,67.754,-51.765,67.738,-51.944,67.765,-52.104,67.779,-52.345,67.837,-52.546,67.818,-52.673,67.795,-52.898,67.773,-52.98,67.758,-53.419,67.575,-53.604,67.536,-53.735,67.549,-53.643,67.668,-53.616,67.715,-53.616,67.767,-53.578,67.837,-53.353,67.971,-53.211,68.117,-53.152,68.208,-53.041,68.218,-52.89,68.205,-52.436,68.146,-52.058,68.075,-51.78,68.057,-51.597,68.055,-51.518,68.077,-51.456,68.116,-51.433,68.143,-51.415,68.198,-51.394,68.218,-51.333,68.242,-51.207,68.326,-51.169,68.385,-51.21,68.42]},{"b":[179.985,-16.541,180,-16.489],"l":[180,-16.54,179.987,-16.541,179.985,-16.522,180,-16.489]},{"b":[-180,-16.168,-179.945,-16.126],"l":[-180,-16.153,-179.969,-16.126,-179.945,-16.126,-179.956,-16.149,-180,-16.168]},{"b":[-85.961,16.404,-85.834,16.514],"l":[-85.961,16.43,-85.947,16.404,-85.871,16.462,-85.844,16.488,-85.834,16.511,-85.878,16.514,-85.924,16.483,-85.961,16.43]},{"b":[25.307,37.413,25.463,37.509],"l":[25.403,37.419,25.457,37.447,25.463,37.471,25.348,37.509,25.313,37.489,25.307,37.413,25.403,37.419]},{"b":[166.907,-0.551,166.958,-0.489],"l":[166.939,-0.489,166.914,-0.499,166.907,-0.524,166.916,-0.546,166.939,-0.551,166.958,-0.517,166.956,-0.497,166.939,-0.489]},{"b":[169.591,5.8,169.735,6.014],"l":[169.591,5.802,169.615,5.8,169.635,5.83,169.673,5.935,169.726,5.976,169.735,6.014,169.7,5.977,169.651,5.945,169.627,5.856,169.612,5.824,169.591,5.802]},{"b":[172.887,3.096,172.97,3.149],"l":[172.963,3.149,172.887,3.101,172.906,3.096,172.97,3.129,172.962,3.143,172.963,3.149]},{"b":[172.75,3.013,172.887,3.074],"l":[172.75,3.033,172.77,3.013,172.844,3.051,172.88,3.054,172.887,3.074,172.827,3.071,172.777,3.034,172.75,3.033]},{"b":[172.933,1.823,173.024,1.944],"l":[172.982,1.897,172.969,1.913,172.95,1.933,172.935,1.944,172.933,1.926,172.967,1.885,173.024,1.823,173.019,1.846,172.982,1.897]},{"b":[172.993,1.713,173.045,1.809],"l":[173.038,1.804,173.024,1.809,173.028,1.747,173.02,1.727,172.993,1.713,173.029,1.717,173.045,1.742,173.043,1.779,173.038,1.804]},{"b":[172.97,0.835,173.087,1.025],"l":[173.033,1.013,173.004,1.025,172.99,1.025,173.004,0.991,173.01,0.991,173.026,0.999,173.065,0.963,173.039,0.915,172.97,0.843,172.991,0.835,173.061,0.915,173.079,0.946,173.087,0.973,173.033,1.013]},{"b":[174.381,-0.829,174.509,-0.592],"l":[174.453,-0.647,174.408,-0.63,174.381,-0.592,174.394,-0.592,174.439,-0.627,174.475,-0.642,174.495,-0.726,174.509,-0.802,174.476,-0.829,174.464,-0.804,174.48,-0.774,174.453,-0.647]},{"b":[174.717,-1.263,174.779,-1.134],"l":[174.767,-1.187,174.744,-1.147,174.717,-1.134,174.741,-1.185,174.748,-1.236,174.756,-1.256,174.779,-1.263,174.773,-1.212,174.767,-1.187]},{"b":[168.675,7.294,168.83,7.336],"l":[168.679,7.336,168.675,7.322,168.719,7.303,168.815,7.294,168.83,7.309,168.755,7.322,168.679,7.336]},{"b":[171.577,6.973,171.757,7.048],"l":[171.757,6.973,171.73,6.977,171.693,7,171.659,7.01,171.614,7.007,171.593,7.016,171.577,7.048,171.615,7.027,171.688,7.028,171.757,6.973]},{"b":[171.036,7.069,171.394,7.172],"l":[171.394,7.111,171.367,7.096,171.305,7.081,171.263,7.069,171.235,7.069,171.202,7.074,171.096,7.109,171.036,7.156,171.05,7.172,171.102,7.138,171.227,7.087,171.394,7.111]},{"b":[-178.194,-14.325,-178.044,-14.232],"l":[-178.142,-14.243,-178.178,-14.232,-178.194,-14.255,-178.159,-14.312,-178.103,-14.325,-178.047,-14.318,-178.044,-14.303,-178.105,-14.284,-178.142,-14.243]},{"b":[-140.852,-17.876,-140.638,-17.67],"l":[-140.809,-17.857,-140.829,-17.849,-140.833,-17.838,-140.815,-17.804,-140.776,-17.754,-140.652,-17.683,-140.638,-17.678,-140.65,-17.67,-140.686,-17.684,-140.761,-17.718,-140.804,-17.752,-140.824,-17.788,-140.851,-17.831,-140.852,-17.867,-140.841,-17.873,-140.804,-17.876,-140.809,-17.857]},{"b":[-136.479,-18.566,-136.294,-18.463],"l":[-136.314,-18.566,-136.294,-18.544,-136.328,-18.519,-136.38,-18.497,-136.426,-18.474,-136.459,-18.463,-136.479,-18.471,-136.464,-18.485,-136.436,-18.489,-136.383,-18.514,-136.344,-18.535,-136.316,-18.545,-136.314,-18.566]},{"b":[-137.068,-18.361,-136.971,-18.265],"l":[-136.971,-18.361,-136.972,-18.342,-137.03,-18.273,-137.068,-18.265,-136.971,-18.361]},{"b":[-138.568,-20.876,-138.506,-20.771],"l":[-138.535,-20.876,-138.506,-20.857,-138.515,-20.813,-138.546,-20.771,-138.568,-20.787,-138.546,-20.795,-138.524,-20.851,-138.535,-20.876]},{"b":[-140.782,-18.416,-140.672,-18.334],"l":[-140.672,-18.416,-140.685,-18.38,-140.782,-18.334,-140.773,-18.364,-140.696,-18.399,-140.672,-18.416]},{"b":[-140.974,-18.217,-140.823,-18.059],"l":[-140.823,-18.217,-140.83,-18.189,-140.893,-18.121,-140.925,-18.084,-140.974,-18.059,-140.959,-18.085,-140.895,-18.148,-140.86,-18.199,-140.823,-18.217]},{"b":[-142.53,-16.107,-142.481,-16.018],"l":[-142.53,-16.107,-142.507,-16.028,-142.481,-16.018,-142.512,-16.096,-142.53,-16.107]},{"b":[-143.551,-16.669,-143.386,-16.612],"l":[-143.386,-16.669,-143.441,-16.62,-143.465,-16.614,-143.516,-16.612,-143.551,-16.621,-143.459,-16.635,-143.386,-16.669]},{"b":[-143.707,-16.64,-143.571,-16.581],"l":[-143.571,-16.635,-143.615,-16.618,-143.67,-16.581,-143.707,-16.581,-143.611,-16.64,-143.571,-16.635]},{"b":[-145.161,-15.901,-145.051,-15.757],"l":[-145.058,-15.901,-145.051,-15.856,-145.134,-15.762,-145.161,-15.757,-145.138,-15.788,-145.076,-15.858,-145.058,-15.901]},{"b":[-145.614,-16.347,-145.482,-16.079],"l":[-145.482,-16.347,-145.487,-16.33,-145.517,-16.278,-145.542,-16.225,-145.577,-16.16,-145.614,-16.079,-145.613,-16.132,-145.609,-16.165,-145.577,-16.201,-145.553,-16.251,-145.54,-16.295,-145.503,-16.346,-145.482,-16.347]},{"b":[173.011,1.338,173.172,1.388],"l":[173.038,1.342,173.011,1.338,173.029,1.359,173.143,1.381,173.153,1.388,173.172,1.375,173.171,1.363,173.163,1.358,173.106,1.357,173.062,1.346,173.038,1.342]},{"b":[-169.766,56.542,-169.474,56.635],"l":[-169.624,56.615,-169.755,56.635,-169.766,56.608,-169.633,56.546,-169.587,56.542,-169.474,56.594,-169.486,56.618,-169.55,56.628,-169.624,56.615]},{"b":[-72.96,19.986,-72.623,20.094],"l":[-72.851,20.094,-72.907,20.086,-72.96,20.062,-72.899,20.031,-72.878,20.027,-72.844,20.035,-72.74,20.003,-72.639,19.986,-72.623,20.014,-72.664,20.038,-72.791,20.092,-72.851,20.094]},{"b":[24.425,37.11,24.536,37.21],"l":[24.484,37.21,24.441,37.187,24.425,37.132,24.487,37.11,24.524,37.125,24.536,37.168,24.529,37.192,24.484,37.21]},{"b":[142.107,26.616,142.202,26.726],"l":[142.162,26.71,142.125,26.726,142.107,26.722,142.17,26.616,142.188,26.617,142.202,26.649,142.162,26.71]},{"b":[130.81,37.449,130.934,37.554],"l":[130.838,37.537,130.81,37.51,130.817,37.478,130.871,37.449,130.916,37.478,130.934,37.53,130.904,37.554,130.838,37.537]},{"b":[-180,-85.192,180,-63.226],"l":[-180,-84.352,-178.39,-84.337,-178.069,-84.352,-177.73,-84.395,-176.986,-84.399,-176.289,-84.418,-176.107,-84.475,-175.875,-84.51,-175.381,-84.48,-174.987,-84.465,-174.663,-84.463,-171.704,-84.542,-168.668,-84.684,-168.049,-84.729,-167.492,-84.834,-166.911,-84.819,-163.464,-84.901,-162.933,-84.901,-160.821,-84.987,-157.127,-85.186,-156.81,-85.192,-156.459,-85.186,-156.643,-85.079,-156.988,-84.982,-157.454,-84.912,-157.15,-84.891,-156.49,-84.889,-156.621,-84.84,-156.986,-84.811,-158.303,-84.778,-163.569,-84.529,-163.685,-84.513,-163.759,-84.493,-163.897,-84.47,-164.114,-84.445,-164.917,-84.431,-165.135,-84.41,-165.24,-84.381,-165.185,-84.37,-165.125,-84.375,-163.899,-84.353,-163.765,-84.324,-163.758,-84.305,-163.821,-84.291,-164.032,-84.274,-164.528,-84.191,-164.685,-84.155,-164.603,-84.097,-164.503,-84.072,-164.124,-84.054,-164.011,-84.016,-164.083,-83.946,-164.951,-83.806,-165.536,-83.757,-165.922,-83.79,-166.649,-83.792,-167.553,-83.811,-167.801,-83.791,-168.053,-83.735,-168.347,-83.637,-168.497,-83.611,-168.785,-83.529,-169.168,-83.45,-171.188,-83.256,-171.539,-83.204,-174.066,-82.9,-174.172,-82.848,-174.236,-82.793,-173.071,-82.916,-172.852,-82.917,-172.593,-82.884,-172.392,-82.893,-172.124,-82.862,-171.821,-82.847,-171.031,-82.943,-169.441,-83.096,-169.016,-83.15,-168.79,-83.188,-168.604,-83.202,-168.418,-83.229,-168.191,-83.213,-168.055,-83.227,-167.724,-83.217,-166.217,-83.201,-165.619,-83.216,-164.916,-83.29,-164.644,-83.412,-164.446,-83.468,-164.058,-83.425,-163.733,-83.373,-163.111,-83.329,-162.912,-83.347,-162.574,-83.411,-162.197,-83.519,-160.595,-83.49,-159.924,-83.495,-159.444,-83.543,-157.699,-83.381,-157.428,-83.346,-157.028,-83.234,-157.356,-83.198,-157.589,-83.187,-157.679,-83.129,-157.522,-83.107,-157.018,-83.075,-156.037,-83.027,-155.459,-82.981,-155.15,-82.858,-153.822,-82.669,-153.399,-82.586,-153.01,-82.45,-153.883,-82.177,-154.717,-81.941,-154.451,-81.868,-154.188,-81.811,-154.061,-81.765,-153.957,-81.7,-154.232,-81.623,-154.485,-81.566,-154.908,-81.51,-156.493,-81.377,-157.033,-81.319,-156.815,-81.231,-156.528,-81.162,-155.921,-81.133,-152.035,-81.029,-148.123,-80.901,-148.023,-80.836,-148.543,-80.76,-148.984,-80.742,-149.147,-80.719,-149.207,-80.67,-149.214,-80.604,-149.264,-80.593,-149.429,-80.586,-150.133,-80.51,-150.282,-80.48,-150.516,-80.409,-150.575,-80.354,-150.435,-80.211,-150.221,-80.15,-149.845,-80.118,-149.578,-80.106,-148.766,-80.108,-148.448,-80.091,-148.317,-80.071,-148.34,-80.003,-148.43,-79.971,-148.433,-79.929,-148.296,-79.907,-148.129,-79.908,-148.083,-79.857,-148.177,-79.776,-148.417,-79.731,-149.051,-79.657,-150.491,-79.546,-151.048,-79.46,-151.368,-79.393,-151.636,-79.318,-151.904,-79.281,-152.091,-79.242,-152.053,-79.193,-152.138,-79.116,-152.244,-79.103,-152.701,-79.135,-153.518,-79.117,-154.518,-79.047,-155.21,-78.965,-156.115,-78.745,-156.469,-78.635,-156.208,-78.559,-155.92,-78.51,-154.716,-78.398,-154.538,-78.359,-154.293,-78.259,-154.695,-78.217,-155.037,-78.221,-155.342,-78.192,-156.569,-78.186,-157.267,-78.2,-157.848,-78.074,-158.286,-77.951,-158.407,-77.888,-158.5,-77.778,-158.351,-77.615,-158.23,-77.498,-158.246,-77.354,-158.214,-77.157,-158.003,-77.091,-157.842,-77.079,-157.465,-77.231,-157.139,-77.242,-156.668,-77.213,-156.368,-77.135,-156.211,-77.106,-155.92,-77.098,-155.359,-77.133,-155.102,-77.12,-154.815,-77.127,-153.91,-77.227,-153.713,-77.274,-153.606,-77.31,-153.573,-77.363,-153.461,-77.416,-153.077,-77.442,-151.998,-77.413,-151.719,-77.426,-150.956,-77.574,-150.306,-77.731,-150.084,-77.771,-149.718,-77.797,-149.588,-77.774,-149.474,-77.715,-149.126,-77.643,-148.34,-77.551,-148.156,-77.462,-148.26,-77.413,-148.559,-77.361,-148.744,-77.343,-148.844,-77.284,-148.839,-77.202,-148.777,-77.125,-148.572,-77.105,-148.196,-77.211,-147.73,-77.31,-147.566,-77.325,-147.442,-77.321,-147.207,-77.286,-146.928,-77.26,-146.391,-77.472,-146.074,-77.487,-145.677,-77.488,-145.601,-77.455,-145.65,-77.398,-145.714,-77.338,-145.794,-77.33,-145.808,-77.273,-145.635,-77.221,-145.516,-77.199,-145.563,-77.162,-145.753,-77.103,-145.864,-77.094,-145.967,-77.069,-145.934,-77.029,-145.806,-77.012,-145.629,-76.954,-145.686,-76.884,-145.676,-76.797,-145.75,-76.749,-146.166,-76.658,-146.777,-76.507,-147.34,-76.438,-148.601,-76.493,-149.046,-76.458,-149.34,-76.419,-149.654,-76.365,-149.285,-76.311,-148.895,-76.272,-148.78,-76.238,-148.632,-76.168,-148.459,-76.118,-148.32,-76.104,-147.86,-76.131,-146.817,-76.318,-146.597,-76.338,-145.886,-76.424,-145.687,-76.429,-145.442,-76.409,-145.642,-76.326,-145.86,-76.267,-146.383,-76.1,-146.323,-76.02,-145.988,-75.889,-145.106,-75.879,-144.721,-75.832,-144.221,-75.731,-143.574,-75.564,-143.022,-75.543,-142.33,-75.491,-142.094,-75.53,-141.506,-75.69,-141.135,-75.746,-141.009,-75.751,-140.874,-75.746,-141.223,-75.546,-140.999,-75.52,-140.709,-75.498,-140.471,-75.447,-140.294,-75.406,-139.691,-75.213,-139.149,-75.16,-137.618,-75.076,-137.09,-75.153,-136.65,-75.162,-136.55,-75.139,-136.462,-75.036,-136.228,-74.836,-136.03,-74.765,-135.362,-74.69,-134.84,-74.694,-134.465,-74.776,-134.117,-74.83,-133.796,-74.855,-133.475,-74.852,-132.992,-74.806,-132.351,-74.789,-132.049,-74.766,-131.707,-74.811,-130.857,-74.826,-130.196,-74.891,-129.791,-74.891,-129.238,-74.829,-128.941,-74.82,-127.863,-74.719,-127.02,-74.698,-126.384,-74.743,-125.353,-74.715,-124.312,-74.736,-123.889,-74.773,-121.544,-74.75,-119.677,-74.655,-119.422,-74.622,-119.022,-74.518,-118.803,-74.422,-118.656,-74.393,-118.342,-74.382,-117.806,-74.403,-117.068,-74.473,-116.433,-74.447,-115.223,-74.487,-115.105,-74.455,-114.991,-74.275,-114.791,-73.989,-114.624,-73.903,-114.346,-73.925,-113.508,-74.089,-113.489,-74.158,-113.575,-74.208,-113.714,-74.228,-113.753,-74.367,-113.641,-74.406,-113.454,-74.394,-113.333,-74.454,-113.597,-74.559,-113.783,-74.618,-113.903,-74.644,-113.985,-74.843,-114.097,-74.909,-114.11,-74.982,-113.932,-74.982,-113.753,-74.952,-113.593,-74.944,-113.092,-74.892,-112.17,-74.832,-111.868,-74.801,-111.696,-74.792,-111.584,-74.751,-111.739,-74.653,-111.789,-74.572,-111.696,-74.504,-111.722,-74.387,-111.806,-74.27,-111.63,-74.181,-111.467,-74.201,-111.18,-74.188,-111.02,-74.23,-110.77,-74.269,-110.534,-74.289,-110.307,-74.367,-110.23,-74.536,-110.3,-74.711,-110.532,-74.836,-110.968,-74.951,-111.463,-75.133,-111.359,-75.22,-111.104,-75.191,-109.99,-75.199,-109.272,-75.185,-108.822,-75.207,-108.254,-75.253,-107.805,-75.322,-107.267,-75.334,-106.932,-75.309,-106.619,-75.344,-105.399,-75.198,-104.902,-75.115,-104.618,-75.156,-104.16,-75.121,-103.901,-75.153,-103.425,-75.101,-103.121,-75.095,-102.771,-75.117,-101.708,-75.127,-101.628,-75.222,-101.304,-75.366,-101.039,-75.422,-100.706,-75.398,-100.463,-75.353,-100.083,-75.37,-99.531,-75.309,-98.98,-75.327,-98.752,-75.317,-98.646,-75.277,-98.558,-75.19,-98.727,-75.141,-99.208,-75.079,-99.652,-74.949,-99.849,-74.922,-100.164,-74.938,-100.313,-74.914,-100.473,-74.872,-100.265,-74.823,-100.013,-74.662,-100.119,-74.515,-100.238,-74.484,-100.531,-74.489,-100.882,-74.541,-101.023,-74.505,-101.252,-74.486,-101.343,-74.35,-101.587,-74.096,-101.715,-74.024,-102.105,-73.958,-102.441,-73.926,-102.766,-73.884,-102.863,-73.784,-102.8,-73.646,-102.411,-73.616,-102.037,-73.631,-101.828,-73.655,-101.587,-73.667,-101.311,-73.695,-101.13,-73.735,-100.985,-73.757,-100.718,-73.758,-99.781,-73.72,-99.656,-73.694,-99.541,-73.645,-99.343,-73.634,-99.162,-73.641,-98.896,-73.611,-99.2,-73.571,-99.528,-73.495,-100.021,-73.403,-100.436,-73.353,-101.189,-73.318,-101.574,-73.33,-101.816,-73.311,-102.675,-73.321,-102.909,-73.285,-103.076,-73.185,-103.308,-72.945,-103.375,-72.819,-103.217,-72.772,-103.11,-72.721,-102.856,-72.716,-102.485,-72.736,-102.363,-72.76,-102.272,-72.835,-102.363,-72.911,-102.482,-72.951,-102.409,-72.987,-102.029,-72.998,-101.842,-73.021,-101.681,-73.03,-101.332,-72.995,-100.821,-72.981,-100.564,-73.016,-100.259,-73.041,-99.811,-73,-98.209,-73.022,-98.012,-73.033,-97.819,-73.102,-97.651,-73.144,-97.476,-73.126,-96.956,-73.206,-96.676,-73.269,-96.394,-73.301,-96.152,-73.309,-95.881,-73.294,-95.529,-73.241,-95.237,-73.22,-95.03,-73.239,-94.586,-73.25,-94.246,-73.313,-93.985,-73.287,-93.706,-73.215,-92.828,-73.165,-92.241,-73.178,-91.169,-73.307,-90.921,-73.319,-90.431,-73.243,-90.274,-73.119,-90.295,-72.978,-90.152,-72.945,-90.035,-72.96,-89.818,-72.863,-89.522,-72.871,-89.341,-72.89,-89.229,-72.826,-89.127,-72.693,-88.78,-72.683,-88.527,-72.702,-88.194,-72.787,-88.195,-72.859,-88.332,-72.934,-88.561,-73.121,-88.419,-73.229,-88.205,-73.22,-87.936,-73.241,-87.608,-73.195,-87.401,-73.192,-87.038,-73.354,-86.791,-73.364,-86.602,-73.354,-85.981,-73.208,-85.801,-73.192,-85.582,-73.259,-85.261,-73.413,-84.981,-73.502,-84.571,-73.557,-84.214,-73.573,-83.796,-73.645,-83.565,-73.706,-83.042,-73.707,-82.815,-73.732,-82.183,-73.857,-81.606,-73.796,-81.309,-73.738,-81.163,-73.632,-81.236,-73.474,-81.262,-73.315,-81.176,-73.249,-81.024,-73.236,-80.336,-73.414,-80.38,-73.308,-80.439,-73.225,-80.614,-73.083,-80.588,-72.978,-80.442,-72.945,-80.152,-73,-79.808,-73.028,-79.522,-73.09,-78.964,-73.312,-78.786,-73.507,-78.408,-73.556,-78.144,-73.547,-77.846,-73.515,-77.444,-73.488,-77.136,-73.496,-76.85,-73.46,-76.765,-73.566,-77.033,-73.718,-77.135,-73.818,-77.049,-73.844,-76.887,-73.821,-76.755,-73.789,-76.291,-73.805,-75.916,-73.736,-75.595,-73.711,-75.293,-73.639,-75.044,-73.645,-74.855,-73.658,-74.594,-73.715,-74.345,-73.684,-74.197,-73.696,-73.996,-73.7,-72.929,-73.448,-72.687,-73.452,-72.381,-73.438,-71.994,-73.379,-71.698,-73.353,-71.453,-73.354,-71.017,-73.263,-70.323,-73.274,-69.969,-73.226,-69.282,-73.17,-68.821,-73.105,-68,-72.936,-67.667,-72.835,-67.307,-72.611,-67.08,-72.388,-66.828,-72.09,-66.952,-71.897,-67.084,-71.812,-67.196,-71.719,-67.46,-71.527,-67.53,-71.285,-67.505,-71.058,-67.598,-70.845,-67.692,-70.686,-67.888,-70.422,-68.126,-70.25,-68.403,-70.02,-68.404,-69.809,-68.47,-69.644,-68.638,-69.526,-68.708,-69.432,-68.58,-69.413,-68.462,-69.384,-68.141,-69.348,-67.372,-69.412,-67.304,-69.318,-67.11,-69.248,-66.975,-69.161,-67.021,-69.029,-67.188,-68.974,-67.391,-68.861,-67.299,-68.771,-67.134,-68.771,-67.054,-68.671,-67.117,-68.575,-67.041,-68.453,-66.894,-68.298,-66.793,-68.24,-66.978,-68.147,-67.15,-68.025,-67.107,-67.93,-67.021,-67.831,-66.915,-67.693,-66.77,-67.593,-66.677,-67.56,-66.705,-67.527,-66.923,-67.492,-67.124,-67.485,-67.487,-67.547,-67.545,-67.535,-67.565,-67.503,-67.586,-67.435,-67.55,-67.269,-67.493,-67.113,-67.44,-67.091,-67.299,-67.071,-67.16,-66.952,-67.034,-66.945,-66.955,-66.985,-66.929,-67.144,-66.886,-67.18,-66.902,-67.256,-66.836,-67.282,-66.757,-67.233,-66.61,-67.209,-66.552,-67.263,-66.499,-67.289,-66.472,-67.243,-66.491,-67.114,-66.515,-67.062,-66.533,-66.979,-66.502,-66.94,-66.465,-66.875,-66.527,-66.741,-66.504,-66.69,-66.371,-66.609,-66.307,-66.592,-66.182,-66.592,-65.954,-66.646,-65.847,-66.65,-65.766,-66.625,-65.718,-66.573,-65.678,-66.403,-65.776,-66.343,-65.775,-66.288,-65.717,-66.254,-65.617,-66.135,-65.465,-66.129,-65.316,-66.14,-65.172,-66.117,-65.222,-66.068,-65.267,-65.994,-65.105,-65.958,-64.999,-65.946,-64.722,-65.993,-64.614,-66.019,-64.514,-65.96,-64.547,-65.9,-64.653,-65.867,-64.673,-65.814,-64.647,-65.748,-64.475,-65.781,-64.435,-65.768,-64.39,-65.708,-64.417,-65.68,-64.439,-65.641,-64.213,-65.633,-64.18,-65.617,-64.132,-65.571,-64.066,-65.554,-63.862,-65.556,-63.818,-65.532,-63.798,-65.48,-63.908,-65.467,-64.051,-65.417,-64.071,-65.278,-64.038,-65.179,-63.912,-65.093,-63.76,-65.033,-63.482,-65.085,-63.264,-65.073,-63.178,-65.126,-63.059,-65.139,-63.033,-65.08,-63.086,-65.028,-63.12,-64.942,-62.775,-64.842,-62.665,-64.858,-62.528,-64.833,-62.576,-64.756,-62.503,-64.656,-62.404,-64.643,-62.338,-64.729,-62.243,-64.747,-62.14,-64.727,-61.883,-64.625,-61.756,-64.61,-61.632,-64.605,-61.5,-64.546,-61.47,-64.476,-61.396,-64.427,-61.174,-64.362,-61.082,-64.315,-60.887,-64.15,-60.922,-64.108,-60.864,-64.073,-60.277,-63.924,-59.99,-63.91,-59.51,-63.821,-59.218,-63.714,-59.036,-63.67,-58.872,-63.552,-58.674,-63.534,-58.216,-63.451,-57.868,-63.319,-57.39,-63.226,-57.168,-63.235,-57.077,-63.262,-57.021,-63.373,-56.927,-63.506,-56.782,-63.572,-56.835,-63.631,-56.974,-63.625,-57.119,-63.638,-57.152,-63.572,-57.098,-63.524,-57.152,-63.479,-57.284,-63.491,-57.461,-63.514,-57.581,-63.547,-57.737,-63.617,-57.857,-63.657,-58.263,-63.763,-58.532,-63.915,-58.723,-64.077,-58.839,-64.187,-59.005,-64.195,-59.047,-64.234,-58.977,-64.266,-58.923,-64.279,-58.799,-64.293,-58.819,-64.339,-58.905,-64.352,-58.895,-64.389,-58.806,-64.445,-58.786,-64.524,-58.892,-64.537,-59.051,-64.451,-59.229,-64.444,-59.37,-64.404,-59.461,-64.346,-59.547,-64.359,-59.612,-64.44,-59.573,-64.531,-59.646,-64.584,-59.734,-64.559,-59.765,-64.451,-59.85,-64.434,-59.963,-64.431,-60.242,-64.547,-60.341,-64.551,-60.394,-64.609,-60.556,-64.677,-60.66,-64.729,-60.915,-64.907,-61.06,-64.981,-61.332,-65.024,-61.439,-65.018,-61.503,-65,-61.603,-64.988,-61.703,-64.987,-61.736,-65.033,-61.577,-65.186,-61.663,-65.239,-61.856,-65.235,-61.948,-65.192,-62.025,-65.233,-62.085,-65.273,-62.145,-65.332,-62.054,-65.457,-61.903,-65.513,-61.796,-65.523,-61.756,-65.569,-61.991,-65.589,-62.151,-65.699,-62.222,-65.775,-62.305,-65.84,-62.293,-65.916,-62.169,-66.031,-62.005,-66.113,-61.839,-66.12,-61.625,-66.095,-61.575,-66.071,-61.359,-66.059,-61.266,-65.98,-61.198,-65.975,-61.138,-65.989,-61.039,-65.992,-60.988,-65.94,-60.913,-65.921,-60.813,-65.934,-60.618,-65.933,-60.565,-65.979,-60.625,-66.032,-60.744,-66.105,-60.856,-66.065,-60.956,-66.072,-61.009,-66.111,-60.903,-66.191,-60.942,-66.264,-61.028,-66.337,-61.134,-66.29,-61.15,-66.212,-61.293,-66.165,-61.432,-66.145,-61.526,-66.226,-61.676,-66.25,-61.696,-66.343,-61.756,-66.429,-61.842,-66.403,-61.875,-66.296,-62.117,-66.209,-62.241,-66.197,-62.494,-66.219,-62.583,-66.217,-62.682,-66.237,-62.755,-66.31,-62.65,-66.364,-62.615,-66.436,-62.618,-66.49,-62.638,-66.511,-62.655,-66.556,-62.543,-66.621,-62.537,-66.707,-62.629,-66.706,-62.705,-66.68,-62.997,-66.453,-63.18,-66.353,-63.258,-66.264,-63.449,-66.244,-63.587,-66.242,-63.753,-66.278,-63.688,-66.32,-63.654,-66.383,-63.756,-66.409,-63.88,-66.506,-63.964,-66.589,-64.015,-66.607,-64.078,-66.654,-63.809,-66.761,-63.769,-66.803,-63.755,-66.873,-63.84,-66.912,-64.043,-66.927,-64.401,-66.853,-64.554,-66.852,-64.607,-66.8,-64.686,-66.806,-64.735,-66.894,-64.793,-66.972,-64.878,-67.025,-64.854,-67.105,-64.785,-67.124,-64.839,-67.156,-64.951,-67.183,-65.027,-67.214,-64.858,-67.243,-64.826,-67.269,-64.819,-67.307,-65.08,-67.335,-65.249,-67.342,-65.35,-67.311,-65.443,-67.326,-65.503,-67.377,-65.523,-67.445,-65.504,-67.528,-65.471,-67.588,-65.447,-67.61,-65.418,-67.66,-65.574,-67.788,-65.589,-67.816,-65.6,-67.876,-65.528,-67.93,-65.469,-68.009,-65.551,-68.048,-65.64,-68.131,-65.546,-68.147,-65.387,-68.15,-65.218,-68.14,-64.959,-68.068,-64.885,-68.056,-64.853,-68.083,-64.829,-68.127,-64.896,-68.168,-65.365,-68.287,-65.452,-68.337,-65.331,-68.364,-65.09,-68.37,-64.996,-68.408,-65.055,-68.449,-65.14,-68.489,-65.242,-68.583,-65.158,-68.618,-64.898,-68.673,-64.429,-68.746,-64.078,-68.771,-64.157,-68.687,-64.169,-68.583,-63.924,-68.498,-63.796,-68.47,-63.217,-68.419,-63.057,-68.421,-62.933,-68.443,-62.98,-68.486,-63.115,-68.471,-63.348,-68.499,-63.707,-68.592,-63.773,-68.632,-63.747,-68.705,-63.443,-68.764,-63.344,-68.81,-63.478,-68.951,-63.456,-69.042,-63.301,-69.141,-63.094,-69.253,-62.994,-69.329,-62.84,-69.372,-62.587,-69.477,-62.451,-69.584,-62.407,-69.827,-62.202,-70.028,-61.961,-70.12,-61.935,-70.2,-62.014,-70.279,-62.218,-70.233,-62.331,-70.279,-62.378,-70.365,-62.232,-70.424,-62.001,-70.497,-61.505,-70.491,-61.491,-70.57,-61.605,-70.617,-61.696,-70.676,-61.809,-70.709,-61.994,-70.729,-62.04,-70.801,-61.961,-70.901,-61.702,-70.857,-61.513,-70.851,-61.313,-70.868,-61.252,-71.002,-61.017,-71.167,-60.962,-71.245,-61.003,-71.319,-61.148,-71.342,-61.237,-71.401,-61.369,-71.452,-61.516,-71.479,-61.79,-71.616,-61.91,-71.631,-61.959,-71.658,-61.725,-71.673,-61.563,-71.675,-61.214,-71.564,-61.081,-71.589,-60.995,-71.661,-60.949,-71.747,-61.035,-71.82,-61.645,-71.863,-61.939,-71.904,-62.257,-72.018,-61.894,-72.071,-61.628,-72.053,-61.493,-72.073,-61.31,-72.113,-61.107,-72.092,-60.952,-72.05,-60.833,-72.052,-60.719,-72.073,-60.704,-72.144,-60.691,-72.27,-60.665,-72.362,-60.73,-72.426,-61.048,-72.471,-61.28,-72.468,-61.286,-72.601,-60.939,-72.7,-60.724,-72.647,-60.532,-72.673,-60.532,-72.832,-60.385,-73.007,-60.254,-73.017,-60.149,-72.938,-60.01,-72.938,-59.957,-73.031,-60.016,-73.189,-60.122,-73.275,-60.404,-73.24,-60.561,-73.211,-60.687,-73.271,-60.896,-73.32,-61.081,-73.328,-61.242,-73.25,-61.428,-73.191,-61.726,-73.161,-62.008,-73.148,-61.915,-73.216,-61.788,-73.255,-61.738,-73.375,-61.637,-73.5,-61.405,-73.467,-61.08,-73.539,-60.879,-73.612,-60.79,-73.712,-60.903,-73.871,-61.088,-73.929,-61.203,-73.957,-61.404,-73.896,-61.545,-73.896,-61.692,-73.924,-61.742,-73.996,-61.838,-74.032,-61.319,-74.036,-61.161,-74.056,-61.042,-74.122,-61.227,-74.208,-61.571,-74.195,-61.718,-74.228,-61.843,-74.29,-61.332,-74.329,-61.121,-74.307,-60.784,-74.241,-60.704,-74.307,-60.838,-74.373,-61.011,-74.478,-61.37,-74.512,-61.64,-74.514,-61.995,-74.476,-62.089,-74.453,-62.235,-74.441,-62.226,-74.506,-62.133,-74.55,-61.894,-74.713,-61.855,-74.777,-61.928,-74.863,-62.138,-74.926,-62.372,-74.952,-62.567,-74.896,-62.708,-74.737,-62.887,-74.691,-63.072,-74.678,-63.178,-74.684,-63.168,-74.765,-63.125,-74.85,-63.198,-74.909,-63.357,-74.878,-63.559,-74.906,-63.751,-74.952,-63.925,-75.004,-63.571,-75.03,-63.337,-75.035,-63.173,-75.115,-63.231,-75.154,-63.551,-75.171,-63.858,-75.206,-64.28,-75.293,-63.972,-75.329,-63.678,-75.328,-63.475,-75.336,-63.304,-75.352,-63.258,-75.399,-63.363,-75.451,-64.053,-75.58,-64.778,-75.738,-65.044,-75.787,-65.322,-75.815,-65.966,-75.952,-66.37,-76.013,-67.518,-76.11,-69.304,-76.351,-69.915,-76.522,-70.096,-76.654,-70.21,-76.674,-70.551,-76.718,-70.895,-76.739,-71.799,-76.753,-72.722,-76.689,-73.472,-76.675,-73.88,-76.697,-75.268,-76.581,-75.444,-76.587,-75.659,-76.608,-75.831,-76.608,-75.963,-76.592,-76.244,-76.585,-77.19,-76.63,-77.287,-76.702,-77.168,-76.834,-76.824,-76.993,-76.249,-77.275,-75.937,-77.334,-75.748,-77.398,-75.387,-77.474,-74.581,-77.478,-73.478,-77.536,-72.852,-77.59,-72.875,-77.694,-73.252,-77.894,-73.485,-77.971,-74.042,-78.109,-74.812,-78.178,-75.398,-78.158,-76.438,-78.044,-77.742,-77.94,-79.679,-77.843,-80.104,-77.797,-80.602,-77.752,-80.889,-77.798,-81.103,-77.842,-81.581,-77.846,-81.441,-77.886,-79.51,-78.154,-77.858,-78.351,-77.665,-78.401,-77.433,-78.435,-77.452,-78.56,-77.545,-78.66,-77.869,-78.746,-78.712,-78.752,-79.767,-78.821,-80.292,-78.823,-80.816,-78.754,-81.929,-78.559,-82.608,-78.412,-83.083,-78.247,-83.412,-78.115,-83.779,-77.984,-83.752,-78.066,-83.688,-78.148,-83.508,-78.248,-83.246,-78.357,-83.227,-78.402,-83.355,-78.408,-83.544,-78.355,-83.706,-78.404,-83.763,-78.461,-83.697,-78.537,-83.595,-78.611,-83.26,-78.774,-82.971,-78.817,-82.589,-78.916,-81.661,-79.1,-81.503,-79.163,-81.222,-79.298,-81.163,-79.4,-80.892,-79.502,-80.705,-79.517,-80.535,-79.513,-80.479,-79.426,-80.489,-79.321,-80.416,-79.295,-80.151,-79.268,-79.456,-79.304,-76.499,-79.326,-76.218,-79.387,-76.105,-79.465,-76.032,-79.627,-76.344,-79.821,-76.558,-79.904,-76.904,-79.955,-77.222,-79.994,-77.702,-80.01,-78.692,-79.995,-79.66,-79.997,-78.907,-80.09,-78.176,-80.167,-77.16,-80.153,-76.757,-80.131,-76.407,-80.095,-76.26,-80.16,-75.986,-80.295,-75.822,-80.338,-75.709,-80.383,-75.555,-80.531,-75.495,-80.617,-75.345,-80.719,-75.237,-80.803,-75.076,-80.86,-74.807,-80.887,-74.511,-80.838,-73.938,-80.816,-73.383,-80.894,-73.029,-80.917,-72.553,-80.853,-72.174,-80.764,-71.38,-80.682,-71.231,-80.647,-71.018,-80.619,-70.688,-80.626,-70.56,-80.647,-70.392,-80.735,-70.239,-80.857,-70.012,-80.918,-69.772,-80.962,-69.634,-80.966,-69.182,-81.005,-68.59,-80.968,-68.327,-81.004,-68.285,-81.074,-68.144,-81.13,-67.965,-81.148,-65.574,-81.461,-64.75,-81.522,-63.478,-81.553,-62.49,-81.557,-62.354,-81.577,-62.165,-81.636,-62.542,-81.678,-62.946,-81.684,-63.554,-81.667,-63.769,-81.676,-64.233,-81.66,-64.476,-81.672,-64.696,-81.652,-65.022,-81.696,-65.62,-81.729,-65.487,-81.775,-65.264,-81.786,-64.811,-81.803,-64.19,-81.795,-64.137,-81.869,-64.706,-81.887,-65.916,-81.902,-66.042,-81.914,-66.134,-81.953,-65.953,-81.971,-65.844,-81.993,-65.787,-82.046,-65.913,-82.183,-65.714,-82.279,-65.572,-82.294,-65.424,-82.28,-65.17,-82.318,-64.92,-82.371,-64.397,-82.374,-63.773,-82.304,-63.466,-82.307,-62.645,-82.263,-61.902,-82.271,-60.859,-82.187,-60.687,-82.189,-60.528,-82.2,-60.817,-82.276,-62.095,-82.467,-62.553,-82.503,-62.736,-82.527,-62.631,-82.621,-62.466,-82.718,-62.129,-82.822,-61.917,-82.977,-61.709,-83.01,-61.313,-82.939,-61.218,-82.992,-61.2,-83.098,-61.303,-83.184,-61.436,-83.232,-61.531,-83.279,-61.59,-83.341,-61.425,-83.396,-60.983,-83.428,-60.397,-83.441,-59.854,-83.442,-59.516,-83.458,-58.29,-83.121,-57.798,-82.959,-57.557,-82.89,-57.354,-82.84,-56.318,-82.633,-56.075,-82.57,-55.801,-82.478,-55.295,-82.465,-54.601,-82.316,-53.986,-82.201,-53.74,-82.178,-53.558,-82.169,-53.339,-82.145,-52.799,-82.154,-52.415,-82.135,-51.731,-82.062,-51.21,-82.015,-50.653,-81.975,-50.029,-81.968,-48.361,-81.892,-47.887,-81.925,-47.36,-82.004,-47.02,-82.003,-46.567,-81.979,-46.258,-81.947,-46.119,-82.04,-46.046,-82.159,-46.199,-82.271,-46.448,-82.34,-46.517,-82.455,-46.175,-82.512,-45.789,-82.495,-45.044,-82.438,-44.455,-82.366,-44.292,-82.318,-44.064,-82.331,-43.669,-82.27,-43.18,-82.017,-42.565,-81.762,-42.046,-81.598,-41.712,-81.408,-41.434,-81.298,-41.126,-81.215,-40.915,-81.172,-40.441,-81.165,-39.762,-81.032,-38.772,-80.882,-38.011,-80.954,-37.209,-81.064,-36.812,-80.975,-36.5,-80.96,-36.234,-80.921,-35.966,-80.891,-35.776,-80.813,-35.521,-80.746,-35.327,-80.651,-34.35,-80.603,-33.329,-80.54,-33.191,-80.519,-33.057,-80.532,-32.706,-80.514,-32.256,-80.461,-31.634,-80.445,-31.312,-80.45,-31.015,-80.308,-30.425,-80.28,-29.797,-80.223,-29.531,-80.182,-29.329,-80.172,-24.24,-80.062,-24.02,-80.009,-23.574,-79.965,-23.407,-79.859,-24.088,-79.815,-24.3,-79.771,-24.534,-79.758,-24.67,-79.775,-25.259,-79.762,-29.949,-79.599,-30.049,-79.585,-30.211,-79.485,-30.178,-79.304,-30.316,-79.163,-30.645,-79.124,-30.985,-79.128,-31.413,-79.145,-32.542,-79.222,-32.994,-79.229,-34.197,-79.11,-34.995,-78.978,-35.516,-78.933,-35.89,-78.844,-36.239,-78.774,-36.266,-78.616,-36.181,-78.468,-35.509,-78.041,-35.088,-77.837,-34.808,-77.821,-34.551,-77.729,-34.29,-77.522,-34.076,-77.425,-33.591,-77.311,-33.377,-77.282,-32.614,-77.141,-32.405,-77.136,-32.063,-77.16,-31.676,-77.033,-30.489,-76.762,-30.222,-76.66,-29.892,-76.598,-28.934,-76.37,-28.079,-76.258,-27.653,-76.226,-27.135,-76.157,-26.56,-76.055,-26.059,-75.957,-24.27,-75.767,-23.197,-75.718,-22.465,-75.661,-21.948,-75.694,-21.434,-75.683,-20.989,-75.634,-20.783,-75.594,-20.487,-75.492,-19.493,-75.54,-18.851,-75.47,-18.585,-75.463,-18.305,-75.431,-18.415,-75.396,-18.517,-75.39,-18.617,-75.342,-18.749,-75.242,-18.617,-75.115,-18.517,-75.052,-18.221,-74.975,-18.068,-74.863,-17.923,-74.699,-17.436,-74.379,-17.299,-74.334,-16.989,-74.32,-16.727,-74.328,-16.43,-74.324,-15.673,-74.407,-15.531,-74.376,-15.29,-74.281,-15.089,-74.163,-14.659,-73.989,-14.574,-73.937,-14.611,-73.852,-15.26,-73.889,-15.749,-73.946,-16.22,-73.916,-16.282,-73.867,-16.181,-73.83,-16.003,-73.816,-15.936,-73.758,-16.097,-73.709,-16.388,-73.681,-16.519,-73.644,-16.507,-73.556,-16.435,-73.426,-16.279,-73.388,-16.149,-73.334,-15.803,-73.152,-15.596,-73.097,-15.007,-73.047,-14.321,-73.123,-14.165,-73.102,-14,-73.001,-14.168,-72.843,-14.298,-72.785,-14.298,-72.733,-13.939,-72.756,-13.603,-72.792,-13.209,-72.785,-12.747,-72.629,-12.095,-72.498,-11.777,-72.444,-11.497,-72.413,-11.346,-72.282,-11.121,-72.032,-10.958,-71.902,-10.961,-71.822,-11.009,-71.758,-11.179,-71.777,-11.333,-71.786,-11.697,-71.719,-12.148,-71.614,-12.285,-71.495,-12.351,-71.39,-12.208,-71.332,-12.074,-71.297,-11.926,-71.289,-11.663,-71.331,-11.328,-71.44,-11.16,-71.481,-10.97,-71.56,-10.825,-71.553,-10.659,-71.443,-10.52,-71.296,-10.407,-71.25,-10.231,-71.201,-10.033,-71.131,-10.122,-71.061,-10.331,-71.024,-10.36,-70.982,-10.271,-70.936,-10.099,-70.926,-9.888,-71.027,-9.599,-71.095,-9.402,-71.118,-9.231,-71.174,-8.966,-71.361,-8.646,-71.673,-8.498,-71.675,-8.216,-71.647,-7.916,-71.635,-7.714,-71.546,-7.669,-71.324,-7.59,-71.224,-7.618,-71.121,-7.757,-71.017,-7.873,-70.94,-7.855,-70.885,-7.753,-70.843,-7.62,-70.829,-7.388,-70.787,-7.032,-70.835,-6.838,-70.845,-6.548,-70.817,-6.245,-70.756,-5.936,-70.713,-5.695,-70.745,-5.588,-70.857,-5.709,-70.968,-5.904,-71.052,-6.08,-71.154,-6.127,-71.266,-6.117,-71.326,-5.95,-71.342,-4.45,-71.328,-4.253,-71.338,-3.995,-71.339,-3.713,-71.375,-3.24,-71.36,-2.812,-71.321,-2.61,-71.321,-2.261,-71.357,-2.015,-71.433,-1.501,-71.412,-1.354,-71.387,-1.216,-71.284,-1.068,-71.266,-0.896,-71.349,-0.84,-71.54,-0.76,-71.63,-0.543,-71.713,-0.327,-71.642,-0.185,-71.559,0.154,-71.398,0.538,-71.274,0.835,-71.202,1.552,-71.08,1.909,-71.004,2.609,-70.9,3.507,-70.844,5.113,-70.656,5.644,-70.636,6.508,-70.586,6.951,-70.535,7.401,-70.494,7.677,-70.356,8.307,-70.462,8.523,-70.474,8.817,-70.391,9.142,-70.184,9.613,-70.269,9.886,-70.403,10.218,-70.508,10.969,-70.688,11.204,-70.729,11.701,-70.767,11.834,-70.737,12.068,-70.617,12.309,-70.443,12.462,-70.37,12.682,-70.309,12.929,-70.213,12.865,-70.162,12.723,-70.144,12.595,-70.117,12.626,-70.066,13.066,-70.054,13.298,-70.23,13.533,-70.287,13.823,-70.343,14.492,-70.3,15.064,-70.295,15.563,-70.331,15.807,-70.324,16.025,-70.193,16.381,-70.145,16.585,-70.204,16.709,-70.397,17.167,-70.451,18.125,-70.54,18.232,-70.518,18.351,-70.416,18.433,-70.29,18.627,-70.269,18.877,-70.201,19.009,-70.212,19.196,-70.293,19.132,-70.492,19.027,-70.674,19.153,-70.821,19.265,-70.902,19.409,-70.917,19.652,-70.921,19.944,-70.91,20.128,-70.918,21.071,-70.843,21.186,-70.681,21.337,-70.495,21.705,-70.258,21.849,-70.277,21.962,-70.3,22.216,-70.417,22.366,-70.475,22.396,-70.561,22.234,-70.643,22.278,-70.696,22.445,-70.74,22.979,-70.81,23.15,-70.796,23.407,-70.723,23.665,-70.575,23.804,-70.405,24.024,-70.413,24.236,-70.449,24.386,-70.537,24.386,-70.704,24.588,-70.82,24.757,-70.892,25.187,-70.971,25.65,-70.991,25.974,-71.037,26.499,-71.02,26.754,-70.967,26.918,-70.954,27.207,-70.911,27.509,-70.813,27.698,-70.772,28.386,-70.682,28.912,-70.583,29.464,-70.406,30.003,-70.3,30.834,-70.246,31.063,-70.225,31.379,-70.226,32.16,-70.1,32.457,-70.026,32.621,-70.001,32.81,-69.909,32.912,-69.734,32.989,-69.624,32.976,-69.517,32.903,-69.379,32.738,-69.255,32.568,-69.074,32.642,-68.869,32.776,-68.783,33.121,-68.689,33.466,-68.671,33.854,-68.683,34.193,-68.702,34.219,-68.791,34.074,-68.885,33.885,-68.979,33.772,-69.02,33.814,-69.099,34.059,-69.111,34.596,-69.095,34.75,-69.168,35.131,-69.487,35.225,-69.637,35.357,-69.681,35.568,-69.66,36.018,-69.662,36.331,-69.639,36.586,-69.638,36.719,-69.652,36.856,-69.726,37.115,-69.81,37.375,-69.748,37.56,-69.718,37.787,-69.726,38.144,-69.824,38.499,-70.056,38.886,-70.172,38.912,-70.098,38.859,-70.006,39.019,-69.924,39.211,-69.786,39.487,-69.608,39.705,-69.426,39.762,-69.173,39.864,-68.967,40.042,-68.868,40.216,-68.805,40.484,-68.739,40.817,-68.724,41.133,-68.575,41.356,-68.515,41.825,-68.433,42.409,-68.352,42.82,-68.123,42.961,-68.095,43.171,-68.06,43.554,-68.046,44.178,-67.972,44.373,-67.961,44.7,-67.904,44.99,-67.769,45.197,-67.731,45.569,-67.736,45.888,-67.66,46.154,-67.657,46.399,-67.618,46.437,-67.533,46.32,-67.477,46.317,-67.402,46.454,-67.304,46.56,-67.268,46.884,-67.275,47.154,-67.357,47.352,-67.362,47.403,-67.409,47.231,-67.468,47.117,-67.573,47.314,-67.665,47.49,-67.728,47.704,-67.716,47.959,-67.66,48.21,-67.699,48.322,-67.785,48.322,-67.917,48.375,-67.988,48.551,-67.926,48.648,-67.794,48.62,-67.625,48.63,-67.521,49.053,-67.352,49.219,-67.227,48.923,-67.2,48.714,-67.217,48.598,-67.171,48.465,-67.043,48.83,-66.938,49.247,-66.942,49.489,-67.031,50.006,-67.175,50.293,-67.172,50.553,-67.194,50.606,-67.15,50.509,-66.939,50.521,-66.82,50.306,-66.753,50.244,-66.603,50.332,-66.445,50.588,-66.356,50.937,-66.315,51.688,-66.072,51.885,-66.02,52.378,-65.969,52.955,-65.946,53.672,-65.859,54.948,-65.916,55.29,-65.954,55.504,-66.003,55.71,-66.08,55.974,-66.209,56.362,-66.373,56.859,-66.423,57,-66.475,57.185,-66.613,56.987,-66.704,56.824,-66.713,56.51,-66.659,56.295,-66.603,56.146,-66.626,56.292,-66.721,56.453,-66.78,56.48,-66.859,56.391,-66.974,55.803,-67.199,56.155,-67.265,56.366,-67.212,56.562,-67.116,56.76,-67.073,56.892,-67.056,57.361,-67.053,57.627,-67.014,57.828,-67.041,58.027,-67.103,58.317,-67.163,58.737,-67.23,59.251,-67.485,59.65,-67.459,59.868,-67.403,60.482,-67.385,61.012,-67.5,61.309,-67.54,62.174,-67.575,62.688,-67.648,63.018,-67.562,63.238,-67.527,63.699,-67.508,63.931,-67.526,64.574,-67.62,65.708,-67.716,66.488,-67.766,67.175,-67.768,67.502,-67.81,68.099,-67.854,68.328,-67.89,68.9,-67.862,69.167,-67.825,69.416,-67.743,69.559,-67.763,69.656,-67.865,69.603,-68.041,69.704,-68.161,69.789,-68.279,69.907,-68.379,69.982,-68.464,69.928,-68.535,69.762,-68.599,69.534,-68.737,69.547,-68.857,69.646,-68.932,69.531,-69.024,69.615,-69.154,69.629,-69.232,69.549,-69.294,69.372,-69.331,69.065,-69.337,68.906,-69.373,68.88,-69.47,68.959,-69.54,69.136,-69.578,69.188,-69.655,69.162,-69.77,69.083,-69.867,68.921,-69.912,68.744,-69.921,68.415,-69.902,68.178,-69.837,68.027,-69.894,67.917,-69.953,67.575,-70.088,67.417,-70.177,67.268,-70.273,67.659,-70.326,67.941,-70.423,68.559,-70.412,68.757,-70.37,69.021,-70.325,69.162,-70.334,69.25,-70.431,69.197,-70.585,69.188,-70.705,68.873,-71.035,68.768,-71.091,68.624,-71.181,68.448,-71.252,68.311,-71.287,68.037,-71.391,67.873,-71.58,67.694,-71.737,67.432,-72.003,67.281,-72.291,67.215,-72.461,67.113,-72.641,66.892,-72.949,66.746,-73,66.498,-73.125,66.569,-73.209,66.765,-73.217,67.003,-73.236,67.322,-73.3,67.749,-73.168,67.971,-73.086,68.016,-72.918,67.971,-72.751,68.107,-72.651,68.42,-72.515,69.157,-72.419,69.309,-72.409,69.555,-72.375,69.77,-72.254,69.962,-72.133,70.294,-72.055,70.573,-71.931,70.616,-71.842,70.732,-71.822,71.079,-71.737,71.168,-71.672,71.277,-71.624,71.349,-71.514,71.379,-71.309,71.465,-71.155,71.634,-70.949,71.771,-70.801,71.905,-70.707,72.263,-70.657,72.418,-70.599,72.622,-70.472,72.76,-70.396,72.744,-70.239,72.822,-70.096,73.041,-70.01,73.325,-69.849,73.676,-69.826,73.942,-69.743,74.227,-69.8,74.571,-69.88,75.148,-69.855,75.424,-69.893,75.636,-69.849,75.821,-69.725,75.891,-69.576,76.112,-69.487,76.36,-69.49,76.77,-69.34,77.192,-69.206,77.541,-69.174,77.817,-69.069,78.015,-68.892,78.229,-68.756,78.489,-68.626,78.563,-68.394,78.726,-68.278,79.035,-68.175,79.288,-68.119,80.363,-67.947,81.187,-67.831,82.017,-67.69,82.273,-67.692,82.607,-67.613,83.158,-67.611,83.304,-67.603,83.494,-67.441,83.904,-67.292,84.161,-67.244,84.485,-67.114,84.748,-67.102,85.117,-67.126,85.429,-67.161,85.711,-67.161,86.118,-67.055,86.75,-67.037,86.947,-66.986,87.085,-66.94,87.98,-66.788,88.314,-66.817,88.789,-66.792,89.077,-66.799,89.352,-66.818,89.698,-66.823,90.293,-66.77,90.547,-66.734,91.022,-66.603,91.546,-66.572,91.777,-66.537,92.073,-66.508,92.312,-66.559,92.486,-66.604,92.592,-66.614,92.731,-66.624,93.075,-66.571,93.358,-66.585,93.722,-66.643,93.964,-66.69,94.089,-66.689,94.313,-66.647,94.587,-66.544,94.84,-66.501,95.084,-66.527,95.248,-66.571,95.541,-66.631,95.991,-66.621,96.424,-66.6,96.789,-66.551,97.101,-66.499,97.388,-66.579,97.72,-66.607,98.258,-66.467,98.462,-66.499,98.603,-66.535,98.72,-66.553,98.859,-66.671,99.37,-66.648,99.824,-66.549,100.212,-66.474,100.591,-66.425,100.889,-66.358,101.327,-66.1,101.321,-66.021,101.381,-65.973,101.474,-65.951,102.174,-65.954,102.392,-65.933,102.674,-65.865,103.167,-65.917,103.639,-65.999,103.763,-65.99,103.951,-65.988,104.289,-66.039,104.667,-66.137,105,-66.164,106.387,-66.411,107.171,-66.47,107.566,-66.552,107.667,-66.58,107.785,-66.664,107.992,-66.672,108.158,-66.639,108.376,-66.766,108.91,-66.862,109.463,-66.909,109.824,-66.834,110.437,-66.621,110.622,-66.524,110.587,-66.312,110.907,-66.077,111.453,-65.961,112.13,-65.9,112.548,-65.848,113.099,-65.8,113.368,-65.849,113.502,-65.886,113.71,-65.93,113.954,-66.06,114.337,-66.36,114.619,-66.468,114.87,-66.477,115.082,-66.493,115.31,-66.561,115.635,-66.771,115.442,-66.958,115.274,-67.028,114.571,-67.108,114.26,-67.172,113.991,-67.212,113.913,-67.368,114.027,-67.441,114.319,-67.406,114.658,-67.388,114.926,-67.357,115.172,-67.308,115.384,-67.238,115.885,-67.202,116.215,-67.143,116.509,-67.108,116.713,-67.047,116.924,-67.055,117.132,-67.114,117.298,-67.109,117.745,-67.129,117.952,-67.085,118.139,-67.082,118.326,-67.115,118.519,-67.161,118.714,-67.172,118.965,-67.145,119.318,-67.071,119.768,-66.992,120.187,-66.966,120.289,-66.967,120.375,-66.984,119.954,-67.076,119.281,-67.199,118.922,-67.32,119.133,-67.371,120.4,-67.236,120.979,-67.136,121.488,-67.091,121.613,-67.057,122.033,-66.902,122.183,-66.859,122.633,-66.805,123.222,-66.745,123.667,-66.677,123.969,-66.608,124.196,-66.601,124.371,-66.652,124.598,-66.708,124.822,-66.695,125.095,-66.641,125.286,-66.516,125.398,-66.424,125.603,-66.393,125.866,-66.364,126.077,-66.396,126.424,-66.462,126.665,-66.498,126.874,-66.759,127.365,-66.99,127.541,-67.051,127.968,-67.028,128.431,-67.119,128.628,-67.107,128.816,-67.08,128.982,-67.098,129.237,-67.042,129.5,-66.753,129.741,-66.469,129.976,-66.345,130.121,-66.292,130.301,-66.268,130.579,-66.209,130.952,-66.191,131.232,-66.216,131.831,-66.236,132.32,-66.165,132.874,-66.178,133.148,-66.095,133.445,-66.081,133.843,-66.154,133.958,-66.204,134.179,-66.277,134.232,-66.348,134.289,-66.477,134.398,-66.48,134.77,-66.353,134.971,-66.33,135.352,-66.127,135.555,-66.18,136.009,-66.267,136.194,-66.292,136.553,-66.439,136.74,-66.408,136.889,-66.34,137.336,-66.346,137.754,-66.406,137.926,-66.457,138.14,-66.544,138.271,-66.564,138.376,-66.54,139.242,-66.574,139.613,-66.638,139.9,-66.715,140.902,-66.752,141.286,-66.832,141.517,-66.794,141.973,-66.807,142.159,-66.874,142.327,-66.948,142.688,-67.013,142.888,-67,143.169,-66.949,143.448,-66.877,143.73,-66.877,143.863,-66.939,143.911,-67.091,144.118,-67.088,144.348,-67.018,144.551,-67.036,144.621,-67.141,144.515,-67.283,144.26,-67.479,144.154,-67.644,143.942,-67.794,143.977,-67.865,144.189,-67.9,144.404,-67.794,144.879,-67.721,145.128,-67.626,145.556,-67.591,145.975,-67.624,146.276,-67.751,146.828,-67.965,146.852,-68.041,146.897,-68.12,146.878,-68.191,146.798,-68.274,147.094,-68.369,147.354,-68.384,147.569,-68.375,148.456,-68.467,148.881,-68.431,149.263,-68.431,149.717,-68.418,150.066,-68.42,150.342,-68.436,150.672,-68.403,150.936,-68.358,151.068,-68.385,151.121,-68.623,151.139,-68.764,151.289,-68.817,151.448,-68.764,151.562,-68.694,152.265,-68.726,152.546,-68.73,152.814,-68.768,153.082,-68.857,153.34,-68.818,153.496,-68.764,153.705,-68.729,153.767,-68.641,153.792,-68.493,153.785,-68.35,153.908,-68.323,154.032,-68.35,154.2,-68.418,154.576,-68.634,154.866,-68.774,154.988,-68.841,155.164,-68.895,155.52,-69.024,156.011,-69.078,156.489,-69.183,157.046,-69.176,157.481,-69.309,157.776,-69.205,157.933,-69.181,158.158,-69.209,158.433,-69.299,158.647,-69.32,159.386,-69.468,159.784,-69.522,159.931,-69.631,160.126,-69.734,160.126,-69.84,160.21,-69.975,160.652,-70.081,160.827,-70.182,161.037,-70.317,161.425,-70.827,161.625,-70.916,161.916,-70.907,162.189,-71.04,162.278,-71.022,162.287,-70.969,162.04,-70.625,162.022,-70.44,162.216,-70.334,162.675,-70.305,163.026,-70.501,163.349,-70.621,163.567,-70.642,163.998,-70.637,164.403,-70.51,164.716,-70.557,165.209,-70.571,165.854,-70.645,166.132,-70.633,166.627,-70.664,167.229,-70.771,167.569,-70.81,167.64,-70.854,167.799,-70.925,167.878,-71.013,167.966,-71.092,168.173,-71.183,168.383,-71.197,168.798,-71.275,169.664,-71.511,169.977,-71.581,170.162,-71.63,170.25,-71.569,170.277,-71.444,170.436,-71.419,170.603,-71.604,170.78,-71.745,170.859,-71.869,170.675,-71.969,170.409,-71.948,170.224,-71.948,170.03,-72.116,169.954,-72.403,170.127,-72.398,170.259,-72.371,170.286,-72.477,170.206,-72.565,170.048,-72.601,169.775,-72.534,169.44,-72.487,169.072,-72.469,168.719,-72.384,168.576,-72.379,168.428,-72.383,168.622,-72.473,168.82,-72.552,169.27,-72.621,169.829,-72.729,169.845,-72.795,169.712,-72.877,169.545,-73.05,169.033,-73.2,168.736,-73.091,168.381,-73.066,168.204,-73.13,167.853,-73.122,167.156,-73.147,166.883,-73.011,166.453,-72.936,166.467,-72.997,166.834,-73.224,167.226,-73.276,167.616,-73.337,167.709,-73.394,167.534,-73.447,167.296,-73.44,166.996,-73.544,166.429,-73.527,166.159,-73.534,166.001,-73.577,165.86,-73.593,165.971,-73.631,166.106,-73.735,165.913,-73.823,165.734,-73.867,165.549,-73.846,165.347,-73.879,165.25,-73.782,165.245,-73.571,165.129,-73.383,165.005,-73.375,164.813,-73.397,164.75,-73.559,164.888,-73.838,164.98,-73.926,164.906,-74.003,164.776,-74.029,165.037,-74.263,165.263,-74.426,165.4,-74.479,165.409,-74.559,165.303,-74.594,165.001,-74.563,164.853,-74.578,164.689,-74.568,164.411,-74.533,164.174,-74.523,163.936,-74.567,163.735,-74.564,163.557,-74.417,163.398,-74.382,163.266,-74.426,163.167,-74.602,162.961,-74.656,162.752,-74.736,162.604,-74.823,162.534,-75.167,162.41,-75.238,162.226,-75.235,162.088,-75.262,161.91,-75.234,161.68,-75.218,160.911,-75.335,161.033,-75.396,161.227,-75.386,161.904,-75.404,162.19,-75.467,162.239,-75.622,162.352,-75.687,162.578,-75.758,162.754,-75.793,162.816,-75.846,162.745,-75.952,162.648,-76.049,162.437,-76.155,162.498,-76.208,162.728,-76.225,162.825,-76.464,162.675,-76.569,162.745,-76.658,162.763,-76.746,162.61,-76.829,162.489,-76.869,162.45,-76.956,162.679,-77.007,162.85,-77.024,163.087,-77.032,163.25,-77.126,163.458,-77.269,163.608,-77.388,163.619,-77.582,163.766,-77.7,164.045,-77.775,164.036,-77.853,164.232,-77.877,164.421,-77.883,164.492,-77.954,164.43,-78.042,164.108,-78.147,163.978,-78.224,164.297,-78.236,164.628,-78.316,165.051,-78.226,165.274,-78.129,165.418,-78.042,165.524,-78.064,165.534,-78.154,165.663,-78.306,166.209,-78.452,166.51,-78.497,166.801,-78.522,167.058,-78.518,167.13,-78.606,167.049,-78.686,166.85,-78.68,166.525,-78.695,166.287,-78.628,166.117,-78.571,164.635,-78.603,164.301,-78.63,163.902,-78.717,163.503,-78.759,162.895,-78.845,162.639,-78.898,161.975,-78.694,161.757,-78.545,161.669,-78.536,161.51,-78.571,161.502,-78.677,161.813,-78.907,161.951,-78.968,161.951,-79.03,161.864,-79.061,161.737,-79.06,161.546,-79.015,161.191,-78.979,160.874,-79.05,160.483,-79.201,160.567,-79.302,160.67,-79.359,160.646,-79.427,160.209,-79.554,160.068,-79.561,159.976,-79.586,160.084,-79.633,160.323,-79.636,160.346,-79.692,160.265,-79.737,159.872,-79.79,159.896,-79.859,160.111,-79.892,160.387,-79.879,160.558,-79.93,160.559,-80.011,160.382,-80.054,160.179,-80.088,158.767,-80.293,158.56,-80.349,158.574,-80.423,159.065,-80.443,160.542,-80.425,160.637,-80.45,160.603,-80.508,160.521,-80.583,160.601,-80.637,160.823,-80.674,160.83,-80.73,160.72,-80.753,160.503,-80.779,160.26,-80.787,160.276,-80.847,160.607,-80.901,160.717,-80.907,160.728,-81.113,160.717,-81.2,160.54,-81.242,160.479,-81.27,160.47,-81.341,160.908,-81.39,161.582,-81.61,161.73,-81.61,161.996,-81.653,162.425,-81.765,162.577,-81.832,162.821,-81.866,162.854,-81.921,163.004,-81.969,163.602,-82.121,163.68,-82.187,162.427,-82.314,161.167,-82.408,161.283,-82.49,162.644,-82.482,163.012,-82.535,163.175,-82.519,163.269,-82.463,164.001,-82.397,164.747,-82.354,164.98,-82.385,165.982,-82.63,166.446,-82.722,166.742,-82.757,166.957,-82.765,167.116,-82.801,167.271,-82.879,167.233,-82.952,167.297,-82.986,167.404,-82.999,167.602,-83.047,167.828,-83.031,168.092,-82.975,168.276,-82.987,168.607,-83.065,168.482,-83.127,168.408,-83.155,168.32,-83.211,168.24,-83.23,167.973,-83.243,167.825,-83.243,167.674,-83.231,167.658,-83.272,167.843,-83.316,168.11,-83.362,169.838,-83.399,170.332,-83.479,170.817,-83.436,171.036,-83.448,171.221,-83.475,171.289,-83.555,171.537,-83.581,171.917,-83.644,172.45,-83.675,172.874,-83.673,173.397,-83.759,173.662,-83.761,173.822,-83.81,175.011,-83.839,175.187,-83.877,175.323,-83.94,175.606,-83.968,175.911,-83.973,177.581,-84.075,178.209,-84.13,178.353,-84.127,178.496,-84.136,178.944,-84.181,179.403,-84.206,179.62,-84.268,180,-84.352]},{"b":[178.628,70.822,180,71.538],"l":[180,71.538,179.886,71.523,179.716,71.466,179.548,71.448,179.235,71.325,178.891,71.231,178.829,71.178,178.684,71.106,178.628,71.047,178.648,71.001,178.793,70.822,178.862,70.826,179.153,70.88,179.648,70.899,179.881,70.976,180,70.993]},{"b":[46.707,36.615,54.723,47.11],"l":[54.71,40.891,54.685,40.873,54.657,40.858,54.547,40.832,54.374,40.871,54.319,40.835,54.336,40.765,54.377,40.693,54.33,40.689,54.283,40.694,54.193,40.72,54.089,40.707,53.87,40.649,53.764,40.666,53.694,40.746,53.615,40.819,53.52,40.831,53.424,40.793,53.333,40.783,53.192,40.809,53.145,40.825,53.06,40.89,52.998,40.96,52.943,41.038,52.889,40.863,52.85,40.686,52.785,40.547,52.734,40.399,52.744,40.22,52.805,40.054,52.898,39.913,52.965,39.834,53.036,39.774,52.952,39.895,52.988,39.988,53.139,39.979,53.289,39.958,53.404,39.96,53.454,39.941,53.487,39.909,53.458,39.831,53.45,39.749,53.472,39.669,53.533,39.642,53.582,39.607,53.603,39.547,53.497,39.533,53.39,39.536,53.305,39.557,53.236,39.609,53.125,39.432,53.124,39.347,53.157,39.265,53.203,39.317,53.267,39.343,53.336,39.341,53.475,39.306,53.539,39.274,53.618,39.216,53.705,39.21,53.71,39.153,53.724,39.103,53.815,39.018,53.869,38.949,53.885,38.864,53.874,38.742,53.852,38.622,53.84,38.515,53.852,38.406,53.854,38.286,53.825,38.047,53.824,37.928,53.848,37.67,53.898,37.414,53.914,37.344,53.952,37.182,54.017,36.952,54.024,36.901,54.016,36.85,53.97,36.818,53.906,36.813,53.769,36.818,53.679,36.853,53.827,36.881,53.915,36.93,53.768,36.93,53.374,36.869,52.19,36.622,51.762,36.615,51.119,36.743,50.927,36.81,50.533,37.014,50.338,37.149,50.214,37.34,50.176,37.381,50.13,37.407,49.981,37.445,49.727,37.481,49.47,37.497,49.372,37.52,49.171,37.601,49.081,37.668,49.015,37.776,48.96,37.89,48.925,38.015,48.901,38.144,48.871,38.393,48.869,38.435,48.851,38.815,48.854,38.839,48.926,38.962,48.962,39.079,49.013,39.134,49.111,39.085,49.109,39.029,49.121,39.004,49.165,39.03,49.2,39.073,49.269,39.285,49.321,39.329,49.363,39.35,49.367,39.398,49.328,39.501,49.324,39.608,49.415,39.84,49.477,40.087,49.551,40.194,49.669,40.249,49.792,40.288,49.919,40.316,50.143,40.323,50.366,40.279,50.307,40.412,50.248,40.462,50.183,40.505,50.119,40.535,49.991,40.577,49.852,40.577,49.776,40.584,49.718,40.608,49.556,40.716,49.457,40.8,49.226,41.026,49.175,41.116,49.143,41.218,49.107,41.302,49.051,41.374,48.824,41.63,48.665,41.787,48.573,41.844,48.477,41.905,48.426,41.924,48.384,41.953,48.303,42.08,48.229,42.181,48.08,42.354,47.822,42.613,47.77,42.645,47.728,42.681,47.709,42.811,47.635,42.903,47.529,42.967,47.489,43,47.463,43.035,47.513,43.219,47.512,43.271,47.49,43.382,47.509,43.51,47.568,43.685,47.628,43.806,47.646,43.885,47.563,43.835,47.463,43.555,47.429,43.78,47.362,43.993,47.307,44.103,47.23,44.192,47.123,44.262,47.024,44.343,46.916,44.387,46.753,44.421,46.721,44.452,46.707,44.503,46.716,44.561,46.755,44.657,46.841,44.718,46.957,44.783,46.984,44.826,47.003,44.876,47.039,44.838,47.084,44.817,47.115,44.906,47.162,44.97,47.221,45.024,47.296,45.149,47.351,45.218,47.391,45.295,47.413,45.421,47.454,45.433,47.489,45.455,47.515,45.491,47.529,45.53,47.524,45.602,47.463,45.68,47.479,45.688,47.508,45.674,47.574,45.634,47.633,45.584,47.65,45.657,47.701,45.686,47.764,45.666,47.83,45.663,48.053,45.721,48.159,45.737,48.258,45.778,48.487,45.935,48.537,45.942,48.589,45.935,48.637,45.906,48.69,45.889,48.73,45.897,48.75,45.921,48.703,45.976,48.687,46.029,48.684,46.086,48.743,46.101,48.81,46.1,49.08,46.189,49.111,46.228,49.125,46.282,49.246,46.292,49.232,46.337,49.206,46.386,49.286,46.437,49.362,46.41,49.344,46.486,49.347,46.519,49.437,46.537,49.584,46.545,49.632,46.568,49.761,46.571,49.886,46.596,50,46.634,50.102,46.696,50.306,46.795,50.419,46.879,50.472,46.883,50.528,46.873,50.583,46.882,50.68,46.939,50.733,46.952,50.92,47.041,51.178,47.11,51.291,47.097,51.615,47.03,51.65,47.018,51.745,46.934,51.945,46.895,52.011,46.902,52.086,46.84,52.138,46.829,52.189,46.84,52.34,46.895,52.385,46.922,52.42,46.964,52.483,46.991,52.678,46.957,52.916,46.954,53.035,46.893,53.069,46.856,53.138,46.742,53.17,46.669,53.132,46.608,53.079,46.547,53.064,46.475,53.109,46.414,53.135,46.192,53.042,45.968,52.888,45.78,52.774,45.573,52.838,45.497,53.086,45.407,53.2,45.332,53.079,45.308,52.911,45.32,52.772,45.344,52.531,45.399,52.427,45.405,52.049,45.388,51.733,45.399,51.54,45.343,51.416,45.358,51.333,45.28,51.294,45.23,51.25,45.122,51.154,45.04,51.04,44.98,51.009,44.922,51.021,44.854,51.058,44.812,51.218,44.709,51.31,44.619,51.366,44.6,51.431,44.602,51.494,44.578,51.544,44.531,51.377,44.541,51.311,44.532,51.177,44.501,51.111,44.508,51.049,44.53,50.86,44.629,50.652,44.633,50.409,44.624,50.297,44.582,50.265,44.527,50.253,44.462,50.253,44.406,50.276,44.355,50.331,44.325,50.472,44.295,50.685,44.265,50.783,44.228,50.831,44.193,50.94,43.959,51.065,43.75,51.14,43.649,51.239,43.577,51.274,43.533,51.302,43.482,51.314,43.421,51.313,43.356,51.292,43.231,51.295,43.174,51.348,43.167,51.514,43.171,51.616,43.158,51.7,43.104,51.785,43.004,51.811,42.954,51.844,42.91,51.898,42.87,51.961,42.851,52.019,42.861,52.076,42.88,52.184,42.869,52.273,42.8,52.324,42.816,52.434,42.824,52.494,42.82,52.55,42.805,52.597,42.76,52.638,42.556,52.618,42.428,52.573,42.331,52.517,42.237,52.462,42.101,52.459,42.048,52.468,41.886,52.494,41.78,52.609,41.529,52.747,41.365,52.826,41.231,52.85,41.2,52.862,41.21,52.83,41.342,52.882,41.614,52.883,41.653,52.815,41.712,52.905,41.896,52.97,41.976,53.108,42.07,53.164,42.094,53.285,42.082,53.496,42.12,53.625,42.136,53.752,42.129,53.805,42.118,53.846,42.091,53.954,41.868,53.995,41.773,54.04,41.643,54.095,41.519,54.181,41.432,54.285,41.364,54.592,41.194,54.671,41.122,54.704,41.071,54.718,41.013,54.723,40.951,54.71,40.891]},{"b":[-17.536,-34.786,180,77.73],"l":[180,68.983,179.868,69.013,179.273,69.26,178.951,69.296,178.925,69.326,178.907,69.362,178.848,69.387,178.443,69.453,177.934,69.496,177.395,69.612,176.924,69.646,176.41,69.769,176.108,69.86,175.921,69.895,175.751,69.904,175.296,69.86,174.786,69.856,174.319,69.882,173.948,69.874,173.733,69.891,173.439,69.947,173.353,69.924,173.277,69.824,173.056,69.865,172.869,69.92,172.56,69.968,171.971,70,171.247,70.076,170.868,70.096,170.487,70.108,170.525,69.938,170.503,69.857,170.36,69.751,170.201,69.683,170.161,69.627,170.582,69.583,170.714,69.388,170.884,69.264,170.997,69.135,170.995,69.045,170.538,68.825,170.066,68.799,169.61,68.786,169.415,68.92,169.311,69.08,168.946,69.163,168.588,69.228,168.423,69.24,168.303,69.271,168.23,69.447,168.15,69.577,168.048,69.626,167.95,69.699,167.857,69.728,167.628,69.74,167.073,69.554,166.884,69.5,166.82,69.5,165.98,69.546,165.761,69.584,164.513,69.609,164.16,69.719,163.946,69.735,163.705,69.702,163.498,69.693,163.201,69.715,162.945,69.683,162.376,69.649,162.166,69.612,161.945,69.545,161.537,69.38,161.48,69.3,161.48,69.202,161.566,69.064,161.566,68.905,161.495,68.85,161.365,68.823,161.23,68.654,161.104,68.563,160.856,68.538,160.997,68.608,161.129,68.654,161.341,68.905,161.31,68.982,161.141,69.039,161.036,69.098,160.982,69.334,160.929,69.459,160.911,69.606,160.739,69.655,160.119,69.73,159.833,69.785,159.729,69.87,159.839,69.99,159.831,70.081,159.89,70.159,159.983,70.221,160.006,70.31,159.959,70.424,159.912,70.506,159.805,70.605,159.728,70.65,159.351,70.791,158.702,70.935,158.037,71.039,157.447,71.075,156.685,71.094,155.895,71.096,155.596,71.039,155.029,71.034,154.414,70.974,153.794,70.88,153.461,70.879,152.798,70.836,152.509,70.834,151.762,70.982,152,71.002,152.093,71.023,151.76,71.218,151.582,71.287,151.145,71.374,150.968,71.38,150.822,71.363,150.243,71.267,150.098,71.227,150.385,71.339,150.525,71.386,150.668,71.455,150.635,71.499,150.6,71.52,150.061,71.511,150.026,71.521,149.913,71.581,149.857,71.601,149.498,71.664,149.238,71.688,148.968,71.69,148.923,71.715,148.955,71.744,148.965,71.763,149.049,71.796,149.28,71.826,149.881,71.843,150.017,71.896,149.998,71.95,149.963,71.992,149.766,72.091,149.502,72.164,148.965,72.252,148.402,72.312,147.434,72.341,147.262,72.328,147.127,72.292,146.895,72.198,146.368,71.922,146.073,71.808,145.805,71.746,145.189,71.696,145.076,71.707,144.99,71.753,145.018,71.794,145.074,71.831,145.078,71.855,145.047,71.901,145.064,71.926,145.126,71.927,145.271,71.895,145.407,71.89,145.757,71.941,145.757,72.021,145.664,72.067,145.71,72.178,145.71,72.206,145.759,72.226,145.799,72.222,146.051,72.142,146.137,72.146,146.23,72.138,146.006,71.945,146.113,71.945,146.402,72.035,146.599,72.124,146.807,72.237,146.832,72.295,146.594,72.302,145.039,72.26,144.471,72.175,144.295,72.193,144.169,72.259,144.361,72.265,144.588,72.306,144.776,72.382,144.897,72.396,145.213,72.393,145.467,72.362,146.235,72.35,146.253,72.442,146.083,72.471,145.714,72.497,145.486,72.542,145.199,72.57,144.569,72.61,144.304,72.643,143.681,72.673,143.516,72.698,142.061,72.721,141.518,72.789,141.31,72.858,140.808,72.891,140.708,72.89,140.676,72.872,140.652,72.843,140.973,72.717,140.983,72.63,141.079,72.587,140.705,72.519,140.451,72.493,139.601,72.496,139.474,72.467,139.141,72.33,139.145,72.264,139.176,72.163,139.43,72.163,139.505,72.208,139.617,72.226,140.134,72.21,140.188,72.191,140.014,72.162,139.847,72.149,139.64,71.998,139.359,71.951,139.552,71.927,139.723,71.885,139.695,71.7,139.939,71.558,139.984,71.492,139.632,71.489,139.32,71.445,139.209,71.445,139.005,71.556,138.78,71.629,138.67,71.635,138.525,71.563,138.318,71.603,138.234,71.596,138.118,71.566,138.048,71.526,137.996,71.464,137.927,71.43,137.918,71.384,138.022,71.363,138.097,71.359,138.314,71.326,138.091,71.307,138.033,71.286,138.013,71.261,137.844,71.227,137.902,71.194,137.974,71.169,137.992,71.143,137.94,71.133,137.798,71.164,137.651,71.208,137.417,71.299,137.315,71.359,137.116,71.416,136.406,71.571,136.09,71.62,135.885,71.631,135.559,71.61,135.359,71.544,135.022,71.515,134.814,71.461,134.703,71.387,134.103,71.379,133.689,71.434,133.426,71.491,133.131,71.607,132.839,71.755,132.804,71.768,132.769,71.799,132.716,71.871,132.654,71.926,132.562,71.895,132.326,71.726,132.228,71.643,132.099,71.484,132.004,71.35,131.991,71.293,132.035,71.244,131.906,71.203,131.769,71.101,131.562,70.901,131.432,70.828,131.268,70.766,131.157,70.742,131.022,70.746,130.898,70.804,130.832,70.936,130.757,70.962,130.668,70.888,130.537,70.893,130.281,70.947,130.026,71.065,129.762,71.12,129.39,71.405,129.225,71.509,129.134,71.593,128.923,71.602,128.843,71.663,128.949,71.708,129.234,71.745,129.461,71.739,129.292,71.85,129.21,71.917,129.122,71.953,129.154,71.879,129.117,71.825,129.04,71.782,128.911,71.755,128.359,72.088,128.027,72.25,127.841,72.308,127.726,72.413,127.803,72.434,128.197,72.31,128.475,72.246,128.935,72.079,129.283,72.092,129.411,72.166,129.412,72.315,129.281,72.438,129.117,72.486,128.549,72.496,128.418,72.535,128.508,72.547,128.633,72.55,128.815,72.586,129.118,72.677,129.25,72.705,129.229,72.776,129.017,72.872,128.674,72.886,128.599,72.895,128.735,72.943,128.854,72.973,129.054,73.045,129.101,73.112,129.059,73.108,128.913,73.111,128.872,73.139,128.889,73.19,128.73,73.233,128.587,73.262,128.258,73.267,128.264,73.301,128.281,73.331,128.142,73.352,128.026,73.391,127.997,73.426,127.955,73.446,127.74,73.482,127.031,73.547,126.955,73.528,126.838,73.434,126.553,73.335,126.335,73.389,126.292,73.394,126.257,73.42,126.309,73.464,126.345,73.506,126.296,73.537,126.254,73.548,126.107,73.517,125.888,73.498,125.794,73.468,125.599,73.447,125.617,73.521,124.796,73.712,124.541,73.751,124.388,73.755,124.019,73.712,123.934,73.689,123.797,73.627,123.491,73.666,123.416,73.637,123.305,73.533,123.323,73.431,123.355,73.402,123.384,73.347,123.501,73.262,123.622,73.193,123.572,73.177,123.522,73.173,123.462,73.144,123.405,73.086,123.301,73.002,123.16,72.955,122.999,72.965,122.615,73.028,122.527,73.017,122.502,72.971,122.731,72.931,122.752,72.906,122.692,72.891,122.538,72.878,122.26,72.881,122.03,72.897,121.886,72.961,121.748,72.97,121.354,72.971,120.997,72.937,120.598,72.981,119.922,72.971,119.75,72.979,119.425,73.064,118.96,73.117,118.43,73.247,118.377,73.367,118.457,73.464,118.754,73.465,118.936,73.481,118.911,73.518,118.871,73.538,118.45,73.59,117.309,73.599,116.496,73.676,115.338,73.703,114.816,73.607,114.061,73.585,113.857,73.533,113.51,73.505,113.539,73.434,113.711,73.379,113.795,73.367,113.886,73.346,113.829,73.327,113.765,73.318,113.639,73.274,113.559,73.233,113.581,73.142,113.543,73.054,113.417,72.932,113.312,72.878,113.216,72.806,113.298,72.739,113.391,72.711,113.63,72.677,113.712,72.654,113.665,72.635,113.312,72.657,113.186,72.73,113.158,72.769,113.128,72.831,113.247,72.897,113.369,72.942,113.475,73.048,113.488,73.145,113.491,73.346,113.277,73.392,113.157,73.46,113.364,73.583,113.416,73.648,113.327,73.707,113.182,73.837,113.033,73.914,112.935,73.946,112.836,73.962,112.94,73.836,112.856,73.771,112.795,73.746,112.4,73.711,112.147,73.709,111.804,73.745,111.4,73.828,111.3,73.885,111.228,73.969,111.46,74.005,111.551,74.029,111.341,74.047,111.131,74.053,111.056,73.939,110.92,73.948,110.261,74.017,110.084,73.994,109.869,73.931,109.774,73.881,109.666,73.8,109.707,73.744,109.753,73.723,110.091,73.709,110.388,73.726,110.722,73.78,110.799,73.76,110.868,73.731,110.773,73.689,110.429,73.629,109.855,73.472,109.637,73.454,109.331,73.487,109.166,73.4,109.09,73.378,108.575,73.319,108.351,73.31,108.285,73.266,108.151,73.258,108.001,73.236,107.75,73.173,107.369,73.163,107.109,73.177,106.478,73.139,106.315,73.106,106.209,73.061,106.16,73.002,106.067,72.95,105.708,72.837,105.403,72.79,105.144,72.777,105.393,72.841,105.677,72.959,106.189,73.308,106.679,73.331,106.794,73.377,107.167,73.589,107.271,73.621,107.765,73.625,108.2,73.694,109.075,74.032,109.511,74.089,109.81,74.169,109.864,74.209,109.911,74.261,109.866,74.293,109.84,74.322,110.226,74.379,110.374,74.466,110.893,74.548,111.299,74.658,111.868,74.74,112.192,74.853,112.925,75.015,113.614,75.293,113.726,75.451,113.559,75.502,113.356,75.534,113.243,75.611,113.162,75.621,112.956,75.572,112.73,75.738,112.453,75.83,112.466,75.844,112.497,75.85,112.629,75.835,113.126,75.699,113.392,75.678,113.469,75.657,113.517,75.622,113.486,75.564,113.568,75.568,113.62,75.593,113.749,75.705,113.871,75.856,113.857,75.921,113.564,75.892,113.428,76.112,113.366,76.179,113.273,76.252,113.086,76.258,112.988,76.24,113.066,76.215,113.15,76.175,113.094,76.133,113.047,76.114,112.819,76.059,112.656,76.054,112.722,76.077,112.798,76.13,112.743,76.187,112.684,76.219,112.62,76.384,112.413,76.408,112.297,76.435,112.143,76.424,111.943,76.38,112.017,76.421,112.094,76.48,111.939,76.553,111.786,76.604,111.601,76.622,111.392,76.687,111.115,76.723,110.471,76.758,109.981,76.712,109.369,76.749,108.638,76.72,108.352,76.72,108.182,76.738,108.028,76.718,107.95,76.661,107.902,76.57,107.722,76.522,107.624,76.51,107.158,76.524,106.825,76.48,106.683,76.515,106.414,76.512,106.385,76.589,106.546,76.586,106.639,76.573,106.941,76.73,107.19,76.822,107.43,76.927,107.279,76.991,106.942,77.034,106.784,77.032,106.705,77.014,106.339,77.048,106.145,77.045,105.822,76.998,105.712,77.001,105.646,77.101,105.32,77.092,104.202,77.102,104.324,77.133,104.912,77.175,105.385,77.238,105.734,77.352,106.06,77.391,105.983,77.448,105.895,77.489,105.71,77.525,105.309,77.549,104.965,77.595,104.814,77.652,104.185,77.73,104.015,77.73,103.561,77.632,103.331,77.641,103.131,77.626,102.61,77.509,101.518,77.198,101.293,77.102,101.186,77.029,100.99,76.99,100.906,76.901,100.92,76.823,101.008,76.781,101.099,76.704,101.006,76.615,100.928,76.557,101.003,76.531,101.213,76.536,101.684,76.485,101.598,76.439,101.311,76.479,101.061,76.477,100.844,76.525,100.322,76.479,99.936,76.49,99.576,76.471,98.869,76.51,98.806,76.481,98.97,76.431,99.094,76.384,99.461,76.275,99.617,76.24,99.825,76.136,99.851,75.93,99.738,75.881,99.609,75.811,99.541,75.799,99.442,75.803,99.602,75.852,99.689,75.956,99.77,76.029,99.663,76.078,99.616,76.082,99.563,76.109,99.187,76.178,98.985,76.208,98.771,76.224,98.662,76.243,98.342,76.181,98.195,76.166,98.02,76.134,97.918,76.089,97.67,76.078,97.638,76.029,97.499,75.98,97.351,76.033,97.205,76.019,96.879,75.931,96.497,75.891,96.538,75.922,96.601,75.99,96.509,76.006,95.935,75.926,95.744,75.872,95.653,75.892,95.986,76.01,96.075,76.082,95.92,76.113,95.579,76.137,95.359,76.14,95.038,76.114,94.576,76.152,94.507,76.108,94.388,76.103,94.102,76.124,93.843,76.101,93.648,76.054,93.36,76.101,93.259,76.099,93.105,76.026,92.972,76.075,92.859,75.979,92.89,75.91,92.987,75.903,93.069,75.913,93.116,75.945,93.178,75.959,93.406,75.901,93.475,75.933,93.574,75.956,93.687,75.922,94.156,75.959,94.075,75.913,93.55,75.854,92.603,75.779,92.408,75.75,91.845,75.724,91.479,75.65,91.005,75.65,90.185,75.591,89.595,75.458,89.31,75.47,88.733,75.369,88.504,75.29,87.671,75.13,87.171,75.192,87.006,75.17,86.922,75.113,86.939,75.068,87.141,75.072,87.287,75.053,87.468,75.013,87.419,74.941,87.042,74.779,86.863,74.718,86.651,74.682,86.201,74.816,86.12,74.757,86.059,74.728,85.881,74.74,85.791,74.645,86.116,74.629,86.426,74.585,86.7,74.522,86.894,74.45,87.106,74.404,87.23,74.364,86.898,74.325,86.665,74.414,86.538,74.444,86.396,74.45,86.183,74.423,86.001,74.316,86.178,74.279,86.571,74.244,86.697,74.195,87.21,73.879,87.338,73.846,87.503,73.832,87.571,73.811,87.37,73.756,87.294,73.705,87.12,73.615,86.376,73.569,86.155,73.535,86.092,73.519,85.999,73.486,85.939,73.456,85.91,73.39,85.971,73.347,86.122,73.307,86.715,73.126,86.677,73.107,86.514,73.14,86.308,73.196,86.098,73.273,85.818,73.327,85.802,73.372,85.793,73.438,85.8,73.459,85.827,73.493,86.094,73.578,86.366,73.62,86.698,73.717,87.029,73.824,86.961,73.861,86.893,73.887,86.591,73.894,85.979,73.857,85.611,73.822,85.448,73.735,85.201,73.722,85.077,73.72,84.738,73.763,84.417,73.722,83.667,73.686,83.545,73.667,81.817,73.659,81.469,73.64,80.583,73.568,80.562,73.515,80.596,73.474,80.458,73.414,80.398,73.357,80.419,73.29,80.425,73.231,80.455,73.155,80.51,73.086,80.639,73.049,80.757,73.025,80.842,72.949,80.774,72.861,80.675,72.759,80.656,72.712,80.72,72.648,80.798,72.52,80.827,72.488,81.098,72.39,81.283,72.359,81.586,72.352,81.793,72.327,82.094,72.265,82.184,72.238,82.209,72.211,82.281,72.105,82.319,72.072,82.645,71.925,82.755,71.903,83.2,71.875,83.34,71.828,83.534,71.684,83.554,71.65,83.571,71.594,83.55,71.544,83.531,71.514,83.458,71.468,83.266,71.276,83.151,71.104,83.334,70.989,83.579,70.766,83.651,70.672,83.736,70.546,83.7,70.466,83.66,70.418,83.497,70.345,83.293,70.321,83.074,70.277,83.094,70.221,83.132,70.157,83.11,70.11,83.081,70.093,82.961,70.088,82.857,70.105,82.767,70.154,82.682,70.218,82.742,70.286,82.92,70.407,83.03,70.581,83.058,70.695,83.051,70.815,83.01,70.895,82.869,70.955,82.738,70.942,82.592,70.89,82.452,70.69,82.258,70.544,82.231,70.483,82.236,70.43,82.221,70.396,82.182,70.511,82.163,70.598,82.271,70.707,82.336,70.807,82.316,70.879,82.239,70.998,82.254,71.056,82.277,71.093,82.323,71.26,82.493,71.293,82.918,71.42,82.977,71.451,83.106,71.562,83.166,71.602,83.234,71.668,83.107,71.721,82.986,71.749,82.758,71.764,82.547,71.759,82.08,71.707,81.662,71.716,81.511,71.746,80.856,71.97,80.815,72.054,80.763,72.089,80.699,72.098,80.474,72.153,79.954,72.223,79.422,72.381,78.483,72.395,78.225,72.377,77.968,72.329,77.733,72.229,77.625,72.201,77.472,72.192,77.44,72.157,77.411,72.108,77.493,72.072,77.781,72.114,78.016,72.092,78.141,72.045,78.232,71.952,78.187,71.907,77.778,71.836,77.551,71.842,77.061,72.004,76.871,72.033,76.422,72.006,76.124,71.927,76.032,71.91,76.104,71.829,76.216,71.683,76.312,71.595,76.433,71.552,76.871,71.447,77.114,71.409,77.481,71.312,77.707,71.301,77.908,71.324,78.213,71.266,78.387,71.087,78.491,71.025,78.588,70.994,78.724,70.976,78.804,70.974,78.889,70.997,79.084,71.002,79.015,70.95,78.942,70.934,78.526,70.912,78.321,70.93,78.068,70.986,77.59,71.168,76.995,71.181,76.929,71.128,76.742,71.202,76.11,71.219,75.734,71.266,75.332,71.342,75.298,71.378,75.28,71.43,75.417,71.495,75.469,71.534,75.503,71.655,75.247,71.813,75.274,71.959,75.395,71.983,75.55,72.171,75.644,72.232,75.694,72.254,75.741,72.296,75.691,72.35,75.644,72.382,75.591,72.457,75.603,72.512,75.604,72.581,75.475,72.685,75.369,72.797,75.152,72.853,74.942,72.854,74.865,72.838,74.787,72.812,74.897,72.71,75.008,72.619,75.06,72.549,75.097,72.421,75.09,72.263,75.053,72.199,74.992,72.145,74.804,72.077,74.489,71.997,74.311,71.958,73.939,71.915,73.672,71.845,73.086,71.445,73.15,71.385,73.365,71.32,73.507,71.264,73.577,71.217,73.732,71.069,74.311,70.654,74.343,70.579,74.207,70.445,73.937,70.273,73.83,70.176,73.578,69.803,73.56,69.707,73.663,69.617,73.833,69.504,73.891,69.418,73.776,69.198,73.836,69.143,73.977,69.115,74.363,69.145,74.815,69.091,75.054,69.116,75.42,69.239,75.561,69.252,76.001,69.235,76.645,69.117,77.328,68.959,77.466,68.905,77.651,68.903,77.785,68.63,77.907,68.482,77.959,68.377,77.995,68.259,77.868,68.235,77.757,68.222,77.665,68.19,77.536,68.008,77.52,67.91,77.588,67.752,78.161,67.678,78.559,67.639,78.839,67.631,78.888,67.613,78.922,67.589,78.59,67.578,77.986,67.559,77.772,67.57,77.675,67.59,77.579,67.644,77.396,67.699,77.325,67.736,77.174,67.779,77.248,67.941,77.261,68.316,77.238,68.47,77.112,68.596,76.735,68.777,76.606,68.898,76.459,68.978,76.316,68.992,76.108,68.976,75.59,68.901,75.125,68.862,74.58,68.751,74.481,68.659,74.391,68.421,74.511,68.303,74.632,68.218,74.743,68.074,74.778,67.986,74.787,67.898,74.77,67.766,74.676,67.695,74.075,67.414,73.986,67.328,73.883,67.085,73.792,66.995,73.514,66.861,73.342,66.807,72.417,66.561,72.384,66.507,72.322,66.332,72.068,66.253,71.917,66.247,71.566,66.334,71.358,66.359,71.146,66.367,70.339,66.342,69.982,66.401,69.701,66.485,69.412,66.511,69.194,66.579,69.144,66.641,69.091,66.724,69.051,66.766,69.013,66.788,69.079,66.816,69.218,66.829,69.74,66.815,69.877,66.845,69.949,66.83,70.094,66.754,70.283,66.686,70.444,66.697,70.579,66.754,70.631,66.754,70.691,66.745,70.568,66.701,70.443,66.668,70.409,66.648,70.383,66.602,70.561,66.549,70.725,66.519,70.939,66.548,71.066,66.604,71.342,66.687,71.54,66.683,71.551,66.76,71.449,66.879,71.365,66.962,71.668,66.94,71.847,67.008,72.594,67.587,72.949,67.696,73.067,67.767,73.152,67.865,73.173,67.973,73.129,68.091,73.139,68.181,73.266,68.294,73.465,68.431,73.592,68.482,73.573,68.533,73.548,68.575,73.191,68.707,72.812,68.815,72.678,68.875,72.577,68.969,72.527,69.081,72.527,69.154,72.557,69.378,72.616,69.484,72.599,69.793,72.53,70.173,72.469,70.275,72.562,70.346,72.653,70.403,72.7,70.457,72.732,70.823,72.704,70.963,72.581,71.151,72.079,71.307,71.867,71.457,71.884,71.511,71.912,71.548,72.13,71.609,72.279,71.696,72.375,71.822,72.574,72.013,72.624,72.079,72.753,72.343,72.787,72.483,72.812,72.691,72.634,72.744,72.446,72.79,72.101,72.829,71.93,72.82,71.617,72.902,71.5,72.914,70.655,72.89,70.172,72.901,69.888,72.883,69.738,72.885,69.645,72.898,69.659,72.932,69.709,72.956,69.694,72.978,69.612,72.982,69.391,72.956,69.039,72.67,68.83,72.392,68.607,72.013,68.469,71.853,68.269,71.683,67.959,71.548,67.542,71.412,67.274,71.348,66.918,71.282,66.768,71.14,66.64,71.081,66.693,71.042,66.847,71.064,66.759,70.962,66.666,70.901,66.675,70.865,66.702,70.819,66.822,70.797,67.143,70.838,67.212,70.798,67.285,70.739,67.247,70.5,67.157,70.295,67.146,70.22,67.197,70.172,67.239,70.108,67.144,70.031,67.069,70.006,66.926,70.014,66.832,69.842,66.803,69.74,66.804,69.659,66.84,69.609,66.897,69.554,66.934,69.597,66.964,69.656,67.064,69.694,67.624,69.584,67.774,69.53,68.006,69.48,68.073,69.421,68.117,69.236,68.355,69.068,68.543,68.967,68.66,68.927,68.763,68.917,68.924,68.956,69.141,68.951,69.024,68.818,68.829,68.567,68.504,68.348,68.371,68.314,68.157,68.404,67.731,68.514,67.64,68.579,67.149,68.754,67.002,68.874,66.756,68.892,66.416,68.948,66.085,69.036,65.813,69.077,65.736,69.132,65.528,69.173,65.327,69.201,65.032,69.27,64.896,69.248,64.929,69.325,64.592,69.436,64.19,69.535,63.361,69.675,62.631,69.743,61.771,69.763,61.016,69.851,60.909,69.847,60.813,69.821,60.559,69.692,60.276,69.653,60.171,69.591,60.337,69.457,60.665,69.11,60.859,69.146,60.934,68.987,60.815,68.895,60.638,68.787,60.489,68.729,60.16,68.7,59.896,68.706,59.865,68.605,59.941,68.51,59.923,68.471,59.897,68.422,59.859,68.396,59.828,68.38,59.726,68.352,59.604,68.351,59.311,68.4,59.099,68.444,59.112,68.616,59.223,68.691,59.298,68.708,59.371,68.738,59.22,68.85,59.11,68.896,59.06,68.973,59.057,69.006,58.919,69.004,58.354,68.916,58.237,68.834,58.173,68.89,57.444,68.642,57.127,68.554,56.909,68.567,56.62,68.619,56.276,68.624,56.044,68.649,55.925,68.637,55.675,68.576,55.418,68.568,55.151,68.48,54.923,68.374,54.861,68.202,54.718,68.184,54.561,68.273,54.476,68.294,54.394,68.275,54.233,68.266,54.099,68.259,53.968,68.227,53.914,68.231,53.515,68.26,53.403,68.257,53.261,68.267,53.293,68.312,53.343,68.343,53.567,68.367,53.69,68.403,53.829,68.383,53.931,68.436,53.918,68.537,53.759,68.634,53.834,68.709,53.891,68.802,53.929,68.812,53.971,68.844,53.92,68.871,53.798,68.885,53.798,68.907,53.874,68.927,54.376,68.965,54.491,68.992,54.186,69.003,53.802,68.996,53.413,68.913,52.684,68.731,52.344,68.608,52.435,68.61,52.55,68.592,52.648,68.506,52.723,68.484,52.67,68.427,52.475,68.382,52.397,68.352,52.322,68.34,52.259,68.351,52.183,68.374,52.227,68.419,52.285,68.459,52.129,68.532,52.056,68.541,51.995,68.539,51.617,68.476,51.336,68.402,51.079,68.363,50.839,68.35,50.699,68.318,50.414,68.218,50.233,68.175,49.931,68.065,49.155,67.87,48.954,67.854,48.841,67.87,48.754,67.896,48.696,67.874,48.763,67.827,48.878,67.731,48.833,67.681,48.654,67.695,48.279,67.65,47.875,67.584,47.883,67.515,47.908,67.455,47.839,67.356,47.768,67.276,47.709,67.045,47.656,66.976,47.496,66.93,46.691,66.826,46.552,66.819,46.492,66.8,46.449,66.819,46.298,66.843,46.084,66.844,45.986,66.853,45.885,66.891,45.753,66.989,45.562,67.186,45.139,67.285,44.939,67.351,44.902,67.413,44.939,67.477,45.374,67.689,45.529,67.758,46.174,67.818,46.429,67.824,46.69,67.849,46.684,67.97,46.43,68.119,46.158,68.291,45.892,68.48,45.519,68.547,45.078,68.578,44.175,68.542,44.048,68.549,43.472,68.68,43.413,68.682,43.333,68.673,43.358,68.636,43.404,68.609,44.169,68.327,44.205,68.254,44.226,68.154,44.214,68.113,44.232,68.071,44.225,67.996,44.036,67.671,43.856,67.439,43.796,67.33,43.782,67.254,43.855,67.189,44.074,67.167,44.292,67.1,44.404,67.004,44.429,66.938,44.437,66.795,44.489,66.672,44.316,66.482,44.221,66.407,44.097,66.235,44.145,66.113,44.132,66.065,44.104,66.009,44.017,66.05,43.944,66.099,43.844,66.142,43.737,66.158,43.624,66.147,43.542,66.123,43.55,66.173,43.653,66.251,43.603,66.291,43.551,66.321,43.233,66.416,43.006,66.421,42.807,66.411,42.602,66.423,42.451,66.482,42.314,66.515,42.211,66.52,42.084,66.466,41.781,66.259,41.476,66.123,41.076,66.021,40.774,65.988,40.692,65.963,40.513,65.844,40.328,65.752,39.817,65.598,39.781,65.535,39.749,65.448,39.798,65.35,39.896,65.255,40.143,65.063,40.281,64.998,40.375,64.896,40.445,64.779,40.408,64.755,40.204,64.784,40.058,64.771,39.849,64.691,39.833,64.656,39.758,64.577,39.567,64.571,39.054,64.714,38.613,64.787,38.541,64.791,38.442,64.827,38.412,64.857,38.228,64.851,38.116,64.855,38.009,64.879,37.528,65.108,37.141,65.194,37.05,65.196,36.883,65.172,36.786,64.987,36.653,64.935,36.535,64.939,36.528,64.847,36.579,64.791,36.624,64.751,36.769,64.685,37.04,64.489,37.184,64.408,37.29,64.378,37.43,64.374,37.741,64.397,37.844,64.366,37.954,64.32,37.977,64.207,38.062,64.091,38.071,64.026,37.968,63.949,37.635,63.893,37.442,63.813,37.373,63.817,36.975,63.91,36.714,63.945,36.365,64.003,36.302,64.034,36.146,64.189,35.802,64.335,35.647,64.378,35.432,64.347,35.284,64.363,35.035,64.44,34.87,64.56,34.858,64.707,34.905,64.739,34.952,64.756,34.833,64.8,34.827,64.913,34.804,64.986,34.671,65.168,34.536,65.278,34.406,65.396,34.544,65.457,34.616,65.51,34.716,65.664,34.735,65.716,34.777,65.768,34.793,65.816,34.786,65.865,34.692,65.952,34.4,66.128,34.113,66.225,33.567,66.321,33.416,66.316,33.361,66.33,33.477,66.347,33.593,66.385,33.656,66.443,33.518,66.471,33.405,66.484,33.217,66.532,33.183,66.574,33.224,66.604,33.181,66.68,32.929,66.704,32.862,66.721,32.857,66.747,32.686,66.83,32.464,66.916,32.501,67.004,32.341,67.068,32.202,67.113,31.983,67.13,31.895,67.161,32.4,67.153,32.93,67.087,32.885,67.061,32.848,67.022,33.002,66.908,33.15,66.844,33.482,66.765,33.523,66.764,33.595,66.785,33.76,66.751,33.894,66.707,34.146,66.703,34.452,66.651,34.431,66.63,34.396,66.613,34.483,66.55,34.61,66.56,34.825,66.611,35.364,66.429,35.513,66.396,36.373,66.302,36.77,66.294,36.984,66.273,37.295,66.225,37.628,66.13,37.901,66.096,38.398,66.064,38.654,66.069,39.289,66.132,40.103,66.3,40.522,66.447,41.189,66.826,41.276,66.914,41.354,67.121,41.359,67.21,41.262,67.218,41.134,67.267,41.134,67.386,41.061,67.444,40.966,67.713,40.766,67.743,40.657,67.774,40.526,67.79,40.381,67.832,40.207,67.942,40.036,68.015,39.896,68.115,39.809,68.151,39.746,68.162,39.79,68.112,39.823,68.059,39.569,68.072,38.832,68.325,38.706,68.345,38.657,68.322,38.43,68.356,38.358,68.415,37.731,68.692,36.618,69.003,35.858,69.192,35.29,69.275,35.233,69.266,35.176,69.231,35.01,69.221,34.864,69.228,34.353,69.303,34.229,69.313,33.684,69.31,33.627,69.289,33.436,69.13,33.333,69.098,33.141,69.069,33.196,69.117,33.328,69.152,33.413,69.267,33.418,69.315,33.464,69.378,33.454,69.428,33.385,69.444,33.256,69.428,32.979,69.367,32.942,69.383,33.021,69.446,33,69.47,32.884,69.461,32.637,69.489,32.378,69.479,32.331,69.554,32.161,69.597,32.092,69.633,32.177,69.674,32.754,69.606,32.915,69.602,32.995,69.626,33.013,69.671,33.008,69.722,32.942,69.752,32.565,69.806,32.392,69.869,31.985,69.954,31.969,69.914,32.031,69.835,31.998,69.81,31.879,69.832,31.789,69.816,31.666,69.721,31.547,69.697,31.453,69.69,31.05,69.769,30.87,69.783,30.714,69.796,30.595,69.79,30.484,69.795,30.428,69.722,30.397,69.733,30.349,69.835,30.238,69.862,30.18,69.841,30.155,69.746,30.088,69.718,29.99,69.737,29.792,69.728,29.695,69.745,29.636,69.78,29.621,69.818,29.621,69.874,29.647,69.944,29.601,69.977,28.804,70.093,28.781,70.145,29.926,70.096,30.263,70.125,30.469,70.198,30.944,70.274,30.961,70.344,30.926,70.401,30.596,70.524,30.422,70.547,30.213,70.543,30.203,70.562,30.238,70.622,30.065,70.703,29.959,70.694,29.796,70.643,29.738,70.647,29.722,70.669,29.639,70.705,29.398,70.734,29.321,70.761,29.219,70.83,29.102,70.861,28.832,70.864,28.75,70.842,28.609,70.76,28.485,70.619,28.437,70.501,28.31,70.443,28.28,70.403,28.193,70.249,28.166,70.288,28.166,70.36,28.191,70.44,28.203,70.577,28.272,70.668,28.216,70.704,27.999,70.664,27.898,70.678,27.951,70.718,28.272,70.798,28.327,70.825,28.383,70.869,28.392,70.975,28.142,71.043,27.815,71.059,27.733,71.081,27.597,71.091,27.332,70.997,27.235,70.947,27.269,70.91,27.556,70.827,27.546,70.804,27.309,70.804,27.184,70.744,27.147,70.681,27.071,70.608,26.989,70.511,26.666,70.422,26.585,70.41,26.584,70.454,26.601,70.503,26.628,70.551,26.645,70.636,26.558,70.669,26.675,70.741,26.734,70.854,26.661,70.94,26.507,70.913,26.231,70.783,25.988,70.625,25.471,70.341,25.419,70.235,25.212,70.136,25.044,70.109,24.983,70.144,24.994,70.218,25.146,70.324,25.209,70.489,25.274,70.552,25.468,70.672,25.666,70.777,25.781,70.817,25.768,70.853,25.712,70.87,25.65,70.873,25.57,70.901,25.436,70.912,25.376,70.892,25.325,70.849,25.265,70.844,25.171,70.872,25.042,70.929,24.832,70.978,24.765,71.008,24.658,71.001,24.442,70.892,24.263,70.826,24.268,70.773,24.404,70.745,24.42,70.702,24.356,70.695,24.286,70.662,24.038,70.485,23.897,70.479,23.661,70.4,23.379,70.247,23.329,70.207,23.286,70.105,23.31,70.064,23.4,70.02,23.354,69.983,23.258,69.993,23.177,70.029,23.046,70.102,22.983,70.237,22.941,70.305,22.852,70.34,22.685,70.375,22.421,70.338,22.385,70.278,22.322,70.265,22.219,70.309,22.054,70.276,21.996,70.293,21.78,70.23,21.539,70.258,21.356,70.233,21.346,70.208,21.4,70.174,21.608,70.098,21.803,70.066,21.893,70.004,21.975,69.835,21.932,69.815,21.78,69.887,21.59,69.938,21.433,70.013,21.254,70.003,21.163,69.89,21.032,69.887,20.971,69.916,20.84,69.907,20.622,69.914,20.546,69.851,20.533,69.692,20.563,69.633,20.662,69.585,20.743,69.535,20.739,69.521,20.487,69.542,20.198,69.371,20.107,69.341,20.054,69.333,20.044,69.356,20.277,69.536,20.338,69.617,20.333,69.677,20.387,69.868,20.355,69.922,20.324,69.945,20.223,69.927,20.146,69.897,20.069,69.883,19.961,69.825,19.865,69.722,19.737,69.504,19.642,69.424,19.64,69.504,19.696,69.613,19.722,69.782,19.687,69.805,19.197,69.748,19.038,69.66,19.011,69.624,19.007,69.588,18.991,69.561,18.883,69.523,18.767,69.517,18.674,69.52,18.614,69.491,18.624,69.434,18.75,69.378,18.916,69.336,18.859,69.314,18.646,69.322,18.483,69.365,18.379,69.44,18.293,69.475,18.26,69.471,18.188,69.433,18.079,69.325,18.075,69.233,18.117,69.181,18.101,69.156,17.705,69.1,17.546,69.001,17.49,68.879,17.391,68.799,17.131,68.693,16.885,68.685,16.652,68.626,16.58,68.593,16.514,68.533,16.525,68.491,16.585,68.466,17.202,68.459,17.426,68.482,17.48,68.474,17.502,68.461,17.571,68.447,17.553,68.426,17.479,68.426,17.336,68.41,17.094,68.368,16.951,68.355,16.865,68.355,16.619,68.406,16.388,68.39,16.204,68.317,16.175,68.281,16.26,68.145,16.319,68.102,16.392,68.092,16.372,68.062,16.309,68.036,16.259,68.001,16.312,67.881,16.261,67.887,16.121,68.027,16.065,68.2,16.038,68.218,16.008,68.229,15.851,68.182,15.657,68.164,15.487,68.103,15.316,68.069,15.293,68.036,15.357,68.004,15.606,67.988,15.621,67.948,15.507,67.926,15.401,67.92,15.274,67.961,15.134,67.973,15.048,67.956,14.799,67.809,14.821,67.75,14.781,67.675,14.855,67.663,15.041,67.683,15.304,67.765,15.346,67.734,15.284,67.708,15.219,67.655,15.249,67.602,15.354,67.544,15.487,67.515,15.661,67.543,15.692,67.521,15.576,67.444,15.594,67.349,15.553,67.352,15.465,67.451,15.409,67.474,15.289,67.483,15.121,67.555,14.962,67.574,14.755,67.499,14.579,67.386,14.537,67.34,14.448,67.298,14.442,67.271,14.479,67.256,14.582,67.267,14.824,67.268,15.3,67.257,15.435,67.247,15.416,67.202,14.776,67.194,14.601,67.174,14.473,67.143,14.34,67.159,14.206,67.111,14.109,67.119,14.022,67.073,13.88,66.965,13.808,66.961,13.727,66.938,13.652,66.907,13.704,66.852,13.917,66.819,13.959,66.794,13.788,66.782,13.621,66.795,13.52,66.742,13.45,66.716,13.312,66.702,13.211,66.641,13.192,66.537,13.105,66.539,13.068,66.431,13.119,66.231,13.352,66.237,13.416,66.253,13.499,66.252,13.681,66.274,13.973,66.32,14.034,66.298,13.916,66.247,13.76,66.221,13.674,66.18,13.387,66.183,12.784,66.1,12.795,66.069,12.976,66.019,13.033,65.956,12.983,65.942,12.817,65.953,12.689,65.902,12.628,65.806,12.345,65.63,12.273,65.568,12.206,65.486,12.122,65.362,12.134,65.279,12.2,65.245,12.263,65.256,12.334,65.241,12.364,65.193,12.418,65.184,12.512,65.195,12.715,65.266,12.82,65.317,12.916,65.339,12.738,65.214,12.508,65.099,12.307,65.086,12.227,65.145,12.16,65.179,11.489,64.976,11.35,64.906,11.304,64.829,11.297,64.755,11.392,64.773,11.562,64.818,11.633,64.814,11.524,64.744,11.331,64.686,11.225,64.679,11.09,64.615,10.932,64.578,10.834,64.494,10.566,64.418,10.236,64.18,10.01,64.083,9.939,63.982,9.864,63.918,9.708,63.865,9.615,63.795,9.567,63.706,9.595,63.679,9.657,63.697,9.767,63.7,9.832,63.617,9.893,63.576,9.924,63.522,10.055,63.513,10.339,63.571,10.935,63.77,11.047,63.845,10.967,63.902,10.914,63.921,11.075,63.988,11.214,64.031,11.307,64.049,11.429,64.025,11.458,64.003,11.295,63.948,11.176,63.899,11.214,63.878,11.308,63.876,11.348,63.838,11.371,63.805,11.226,63.764,11.118,63.719,10.953,63.698,10.779,63.651,10.725,63.625,10.674,63.558,10.707,63.536,10.76,63.461,10.704,63.464,10.591,63.447,10.34,63.469,10.189,63.455,10.081,63.433,10.021,63.391,9.979,63.395,9.936,63.479,9.892,63.492,9.832,63.524,9.697,63.625,9.602,63.61,9.521,63.586,9.324,63.57,9.156,63.459,9.084,63.463,9.076,63.5,9.158,63.566,9.136,63.594,8.842,63.646,8.674,63.623,8.576,63.601,8.398,63.535,8.361,63.499,8.387,63.445,8.48,63.424,8.594,63.426,8.641,63.392,8.636,63.342,8.58,63.313,8.271,63.287,8.184,63.237,8.158,63.162,8.235,63.082,8.339,63.042,8.609,62.881,8.623,62.846,8.311,62.966,8.211,62.996,8.101,63.091,7.86,63.113,7.736,63.104,7.654,63.109,7.572,63.1,7.389,63.023,7.008,62.958,6.94,62.93,6.928,62.903,6.782,62.79,6.735,62.721,6.78,62.701,7.025,62.729,7.111,62.752,7.242,62.752,7.408,62.712,8.046,62.771,8.096,62.732,7.805,62.721,7.538,62.672,7.518,62.646,7.527,62.61,7.691,62.586,7.653,62.564,7.57,62.548,7.492,62.543,7.284,62.602,6.961,62.627,6.745,62.638,6.618,62.621,6.439,62.61,6.353,62.611,6.273,62.584,6.238,62.52,6.165,62.482,6.118,62.447,6.136,62.407,6.262,62.416,6.457,62.448,6.692,62.468,6.62,62.423,6.58,62.407,6.209,62.353,6.083,62.35,6.026,62.376,5.98,62.407,5.908,62.416,5.796,62.385,5.718,62.379,5.533,62.311,5.484,62.239,5.422,62.207,5.358,62.152,5.294,62.154,5.241,62.189,5.143,62.16,5.096,62.027,5.16,61.957,5.267,61.936,5.473,61.946,5.664,61.923,6.131,61.852,6.396,61.851,6.682,61.887,6.731,61.87,6.467,61.807,6.016,61.788,5.793,61.827,5.465,61.897,5.117,61.885,4.985,61.9,4.93,61.878,4.91,61.81,4.928,61.711,4.997,61.645,5.099,61.62,5.168,61.543,5.268,61.505,5.339,61.485,5.258,61.455,5.172,61.457,5.003,61.434,4.99,61.378,5.022,61.251,5.107,61.188,5.325,61.108,5.451,61.102,5.647,61.148,6.083,61.167,6.383,61.134,6.493,61.155,6.543,61.245,6.6,61.29,6.626,61.279,6.61,61.229,6.657,61.207,6.794,61.19,6.943,61.161,7.174,61.166,7.276,61.181,7.298,61.214,7.276,61.284,7.331,61.372,7.443,61.435,7.453,61.419,7.347,61.301,7.404,61.222,7.604,61.211,7.545,61.177,7.04,61.091,7.047,61.015,7.078,60.966,7.039,60.953,6.981,60.994,6.972,61.056,6.903,61.102,6.778,61.142,6.61,61.137,6.418,61.084,6.293,61.081,5.984,61.117,5.505,61.056,5.288,61.047,5.192,61.054,5.095,61.071,5.009,61.038,5.025,60.936,5.011,60.859,5.049,60.708,5.116,60.636,5.244,60.57,5.447,60.617,5.589,60.694,5.648,60.688,5.546,60.625,5.168,60.485,5.137,60.446,5.184,60.308,5.286,60.206,5.417,60.154,5.574,60.158,5.658,60.154,5.689,60.123,5.495,60.07,5.376,60.067,5.265,60.086,5.206,60.088,5.174,60.046,5.22,59.979,5.187,59.907,5.119,59.834,5.105,59.732,5.111,59.668,5.146,59.639,5.186,59.642,5.234,59.692,5.264,59.71,5.353,59.76,5.495,59.826,5.557,59.908,5.699,60.01,5.801,60.026,5.877,60.07,5.904,60.151,5.967,60.206,6.102,60.29,6.153,60.346,6.347,60.419,6.806,60.501,6.996,60.512,6.95,60.478,6.787,60.454,6.72,60.418,6.661,60.367,6.527,60.153,6.526,60.214,6.574,60.361,6.518,60.408,6.349,60.353,6.141,60.233,6.105,60.165,6.112,60.132,6.07,60.083,5.996,60.031,5.784,59.913,5.73,59.863,5.763,59.808,5.834,59.795,5.967,59.813,6.059,59.816,6.212,59.832,6.217,59.818,5.991,59.745,5.867,59.734,5.772,59.661,5.579,59.687,5.53,59.713,5.472,59.713,5.404,59.656,5.305,59.643,5.242,59.564,5.185,59.454,5.132,59.226,5.173,59.163,5.362,59.166,5.468,59.204,5.564,59.291,5.657,59.31,5.718,59.33,5.845,59.353,6.017,59.414,6.159,59.49,6.279,59.535,6.404,59.561,6.415,59.547,6.306,59.506,6.199,59.438,6.051,59.368,5.952,59.299,5.937,59.234,5.969,59.186,5.949,59.135,5.889,59.098,5.889,59.06,6.017,58.988,6.099,58.952,6.321,59.016,6.363,59.001,6.214,58.945,6.137,58.875,6.099,58.87,5.854,58.959,5.612,59.013,5.556,58.975,5.522,58.823,5.517,58.727,5.586,58.62,5.707,58.524,5.977,58.432,6.055,58.375,6.389,58.268,6.492,58.259,6.618,58.266,6.66,58.263,6.677,58.234,6.692,58.224,6.606,58.176,6.555,58.123,6.591,58.097,6.731,58.068,6.767,58.082,6.771,58.132,6.803,58.155,6.877,58.151,6.912,58.143,6.895,58.121,6.89,58.102,6.903,58.071,7.005,58.024,7.194,58.048,7.466,58.021,7.876,58.08,8.037,58.147,8.166,58.145,8.312,58.224,8.521,58.301,8.928,58.57,9.178,58.675,9.194,58.712,9.239,58.739,9.323,58.748,9.396,58.806,9.31,58.857,9.551,58.933,9.618,58.946,9.657,58.971,9.696,59.01,9.627,59.068,9.557,59.113,9.635,59.118,9.8,59.027,9.843,58.958,9.96,58.968,10.083,59.029,10.179,59.009,10.205,59.039,10.243,59.062,10.431,59.28,10.455,59.377,10.446,59.444,10.407,59.456,10.398,59.519,10.494,59.542,10.57,59.587,10.534,59.696,10.595,59.765,10.604,59.68,10.634,59.602,10.631,59.428,10.645,59.389,10.743,59.296,10.834,59.184,10.945,59.171,10.999,59.164,11.091,59.142,11.132,59.143,11.366,59.105,11.386,59.066,11.388,59.037,11.295,59.087,11.196,59.078,11.167,59.046,11.147,58.989,11.169,58.923,11.208,58.866,11.224,58.68,11.272,58.476,11.252,58.424,11.248,58.369,11.33,58.38,11.432,58.34,11.449,58.118,11.549,58.001,11.703,57.973,11.729,57.764,11.735,57.718,11.879,57.679,11.885,57.613,11.917,57.522,11.962,57.426,12.053,57.447,12.152,57.227,12.421,56.906,12.573,56.823,12.718,56.663,12.793,56.649,12.884,56.618,12.92,56.516,12.857,56.452,12.773,56.456,12.656,56.441,12.691,56.384,12.742,56.347,12.802,56.264,12.753,56.242,12.706,56.235,12.507,56.293,12.471,56.291,12.521,56.246,12.593,56.138,12.835,55.882,12.942,55.806,12.974,55.748,12.978,55.694,12.963,55.613,12.939,55.533,12.941,55.482,12.886,55.411,13.321,55.346,13.806,55.429,14.08,55.392,14.174,55.397,14.342,55.528,14.276,55.636,14.203,55.729,14.215,55.833,14.262,55.888,14.402,55.977,14.473,56.014,14.559,56.049,14.656,56.02,14.755,56.033,14.714,56.134,14.782,56.162,15.051,56.172,15.327,56.151,15.51,56.183,15.627,56.186,15.722,56.164,15.827,56.125,15.92,56.167,15.997,56.223,16.151,56.501,16.217,56.59,16.349,56.709,16.408,56.809,16.458,56.927,16.528,57.068,16.507,57.142,16.479,57.188,16.476,57.265,16.631,57.43,16.652,57.501,16.604,57.568,16.584,57.642,16.586,57.761,16.555,57.812,16.597,57.913,16.695,57.918,16.7,58.161,16.77,58.214,16.717,58.303,16.652,58.434,16.824,58.46,16.924,58.493,16.788,58.585,16.683,58.6,16.478,58.613,16.391,58.602,16.318,58.628,16.214,58.637,16.316,58.664,16.639,58.651,16.978,58.654,17.103,58.711,17.348,58.781,17.457,58.858,17.67,58.916,17.765,58.965,17.829,58.955,17.975,59.003,18.098,59.062,18.285,59.109,18.322,59.132,18.373,59.18,18.414,59.29,18.499,59.292,18.618,59.327,18.56,59.394,18.509,59.408,18.459,59.397,18.396,59.369,18.336,59.375,18.271,59.367,18.211,59.331,18.133,59.316,17.98,59.329,17.964,59.359,18.164,59.43,18.217,59.421,18.276,59.438,18.338,59.477,18.402,59.49,18.578,59.566,18.64,59.601,18.719,59.657,18.896,59.733,18.971,59.757,18.99,59.828,18.933,59.942,18.884,59.98,18.853,60.026,18.787,60.079,18.601,60.119,18.535,60.153,18.558,60.254,18.4,60.337,18.25,60.362,18.163,60.408,18.011,60.511,17.956,60.59,17.872,60.58,17.742,60.539,17.661,60.535,17.631,60.585,17.593,60.628,17.555,60.643,17.457,60.642,17.36,60.641,17.251,60.701,17.261,60.763,17.279,60.812,17.203,60.952,17.213,60.986,17.186,61.147,17.18,61.249,17.164,61.278,17.2,61.312,17.178,61.358,17.138,61.382,17.164,61.458,17.147,61.505,17.131,61.576,17.216,61.656,17.196,61.725,17.335,61.692,17.465,61.684,17.417,61.741,17.398,61.782,17.375,61.866,17.412,61.966,17.447,62.023,17.51,62.166,17.563,62.212,17.634,62.233,17.535,62.264,17.429,62.335,17.373,62.427,17.378,62.463,17.41,62.508,17.509,62.483,17.571,62.451,17.646,62.451,17.718,62.501,17.834,62.503,17.947,62.578,18.037,62.601,18.007,62.626,17.93,62.641,17.903,62.659,17.941,62.68,17.974,62.721,17.933,62.786,17.896,62.831,17.88,62.873,17.907,62.887,17.951,62.834,18.094,62.836,18.078,62.812,18.074,62.791,18.17,62.789,18.215,62.812,18.248,62.849,18.463,62.896,18.483,62.928,18.487,62.959,18.502,62.989,18.313,62.996,18.344,63.032,18.408,63.038,18.531,63.064,18.578,63.126,18.606,63.178,18.667,63.177,18.76,63.198,18.819,63.197,18.859,63.207,18.85,63.224,18.792,63.238,18.817,63.257,19.034,63.238,19.236,63.347,19.288,63.429,19.354,63.477,19.495,63.424,19.491,63.46,19.502,63.509,19.59,63.487,19.656,63.458,19.722,63.463,19.782,63.538,19.914,63.611,20.205,63.662,20.371,63.723,20.454,63.774,20.678,63.826,20.763,63.868,21.018,64.178,21.256,64.299,21.465,64.38,21.494,64.416,21.52,64.463,21.394,64.544,21.332,64.629,21.279,64.725,21.205,64.774,21.138,64.809,21.196,64.877,21.294,64.941,21.425,65.013,21.574,65.126,21.581,65.161,21.546,65.207,21.506,65.245,21.438,65.283,21.41,65.317,21.447,65.321,21.567,65.255,21.609,65.261,21.613,65.299,21.596,65.317,21.545,65.331,21.523,65.359,21.533,65.387,21.566,65.408,21.681,65.403,21.88,65.424,21.913,65.437,21.95,65.47,21.903,65.508,21.92,65.532,22.087,65.53,22.148,65.553,22.133,65.57,22.096,65.584,22.086,65.611,22.254,65.598,22.267,65.622,22.275,65.725,22.288,65.751,22.336,65.791,22.366,65.843,22.401,65.862,22.465,65.853,22.539,65.794,22.62,65.807,22.747,65.871,22.919,65.786,23.102,65.735,23.155,65.75,23.221,65.786,23.418,65.804,23.592,65.805,23.691,65.829,23.891,65.782,24.155,65.805,24.238,65.812,24.404,65.78,24.533,65.822,24.592,65.858,24.628,65.859,24.623,65.832,24.582,65.757,24.675,65.671,24.764,65.656,24.839,65.66,25.242,65.546,25.348,65.479,25.308,65.353,25.298,65.243,25.256,65.143,25.34,65.099,25.362,65.065,25.373,65.009,25.271,64.984,25.228,64.951,25.281,64.916,25.288,64.86,25.214,64.853,25.134,64.875,24.942,64.884,24.748,64.852,24.658,64.806,24.558,64.801,24.53,64.739,24.441,64.68,24.278,64.515,24.022,64.386,23.925,64.274,23.861,64.258,23.653,64.134,23.599,64.041,23.494,64.034,23.249,63.896,23.134,63.865,23.014,63.822,22.756,63.683,22.532,63.648,22.528,63.58,22.398,63.491,22.319,63.504,22.313,63.473,22.346,63.442,22.273,63.455,22.243,63.438,22.286,63.377,22.316,63.346,22.32,63.31,22.12,63.244,21.896,63.21,21.8,63.238,21.545,63.204,21.549,63.156,21.569,63.114,21.651,63.039,21.474,63.033,21.458,62.95,21.196,62.791,21.144,62.74,21.118,62.689,21.104,62.623,21.142,62.515,21.166,62.414,21.323,62.343,21.343,62.277,21.354,62.224,21.302,62.113,21.256,61.99,21.385,61.915,21.471,61.812,21.546,61.703,21.552,61.667,21.606,61.592,21.598,61.578,21.592,61.568,21.522,61.567,21.498,61.552,21.527,61.523,21.552,61.51,21.565,61.484,21.507,61.484,21.502,61.455,21.521,61.411,21.513,61.281,21.479,61.171,21.451,61.127,21.378,61.059,21.361,60.967,21.379,60.85,21.404,60.767,21.412,60.697,21.411,60.637,21.436,60.596,21.528,60.57,21.613,60.531,21.727,60.583,21.805,60.594,21.854,60.505,21.934,60.5,22.258,60.401,22.521,60.377,22.56,60.385,22.585,60.381,22.576,60.359,22.512,60.281,22.517,60.263,22.588,60.256,22.59,60.228,22.564,60.206,22.513,60.199,22.47,60.201,22.443,60.157,22.471,60.147,22.439,60.09,22.439,60.072,22.463,60.029,22.646,60.028,22.697,60.038,22.75,60.057,22.793,60.077,22.819,60.101,22.844,60.187,22.867,60.216,22.912,60.21,22.994,60.099,23.08,60.047,23.148,60.041,23.198,60.022,23.188,59.972,23.116,59.913,23.01,59.869,22.964,59.826,23.021,59.816,23.181,59.845,23.327,59.926,23.464,59.986,23.593,59.968,23.722,59.966,24.025,60.009,24.343,60.042,24.446,60.021,24.518,60.046,24.6,60.114,24.849,60.158,24.958,60.157,25.156,60.194,25.268,60.248,25.456,60.261,25.548,60.302,25.656,60.333,25.715,60.267,25.758,60.268,25.846,60.315,25.946,60.347,26.036,60.342,26.04,60.372,26.006,60.425,25.956,60.474,26.036,60.475,26.205,60.407,26.378,60.424,26.456,60.467,26.496,60.552,26.569,60.625,26.606,60.628,26.602,60.596,26.551,60.546,26.52,60.472,26.535,60.413,26.607,60.438,26.721,60.455,26.951,60.471,27.076,60.525,27.205,60.543,27.242,60.539,27.462,60.465,27.525,60.491,27.669,60.499,27.762,60.533,27.798,60.536,28.179,60.571,28.513,60.677,28.578,60.653,28.651,60.611,28.64,60.543,28.622,60.492,28.492,60.54,28.522,60.483,28.643,60.375,28.813,60.332,29.069,60.191,29.37,60.176,29.569,60.202,29.721,60.195,29.872,60.121,29.977,60.026,30.06,60.003,30.173,59.957,30.157,59.904,30.123,59.874,29.67,59.956,29.147,60,29.079,59.961,29.013,59.902,28.982,59.855,28.947,59.829,28.867,59.812,28.748,59.807,28.604,59.818,28.518,59.85,28.454,59.814,28.424,59.734,28.335,59.693,28.213,59.725,28.131,59.787,28.058,59.782,28.014,59.725,28.046,59.647,28.064,59.554,28.013,59.484,28.002,59.47,27.893,59.414,27.336,59.45,26.975,59.451,26.852,59.472,26.625,59.554,26.461,59.554,25.794,59.635,25.616,59.628,25.509,59.639,25.507,59.598,25.521,59.559,25.444,59.521,24.878,59.522,24.584,59.456,24.38,59.473,24.175,59.376,24.054,59.372,24.083,59.292,23.783,59.275,23.641,59.242,23.494,59.196,23.517,59.108,23.48,59.07,23.468,59.032,23.515,58.999,23.49,58.96,23.432,58.921,23.497,58.82,23.504,58.79,23.534,58.782,23.681,58.787,23.647,58.754,23.531,58.716,23.509,58.659,23.563,58.576,23.692,58.506,23.706,58.433,23.768,58.361,24.011,58.307,24.115,58.266,24.236,58.29,24.287,58.328,24.337,58.381,24.392,58.386,24.529,58.354,24.55,58.305,24.536,58.283,24.488,58.262,24.464,58.106,24.332,57.91,24.323,57.871,24.302,57.784,24.363,57.645,24.403,57.325,24.383,57.25,24.281,57.172,24.054,57.066,23.931,57.008,23.648,56.971,23.287,57.09,23.137,57.324,23.038,57.392,22.649,57.595,22.617,57.651,22.555,57.724,22.231,57.667,21.942,57.598,21.729,57.571,21.459,57.322,21.421,57.236,21.405,57.131,21.351,57.018,21.257,56.933,21.071,56.824,21.031,56.637,21.015,56.259,21.046,56.07,21.054,56.023,21.062,55.813,21.171,55.618,21.238,55.455,21.201,55.344,21.236,55.271,21.223,55.108,21.189,54.935,20.996,54.903,20.888,54.909,20.774,54.947,20.678,54.956,20.595,54.982,20.859,55.184,21.032,55.35,21.104,55.488,21.116,55.568,21.115,55.617,21.088,55.583,21.058,55.477,21.014,55.402,20.9,55.287,20.846,55.232,20.679,55.103,20.52,54.995,20.397,54.951,20.108,54.956,19.975,54.921,19.953,54.83,19.944,54.75,19.859,54.634,19.758,54.545,19.604,54.459,19.56,54.435,19.407,54.386,18.976,54.349,18.836,54.37,18.67,54.431,18.587,54.513,18.436,54.745,18.502,54.742,18.678,54.665,18.8,54.633,18.759,54.685,18.535,54.769,18.323,54.838,18.086,54.836,17.843,54.817,17.262,54.73,17.007,54.652,16.885,54.596,16.56,54.554,16.376,54.437,16.292,54.362,16.239,54.333,16.186,54.29,16.043,54.266,15.9,54.254,15.288,54.14,14.716,54.018,14.384,53.925,14.249,53.932,14.211,53.95,14.039,54.035,13.828,54.127,13.82,54.093,13.827,54.06,13.872,54.036,13.922,53.997,13.902,53.939,13.926,53.879,14.048,53.863,14.172,53.874,14.351,53.859,14.558,53.823,14.565,53.754,14.552,53.707,14.572,53.676,14.583,53.639,14.488,53.672,14.25,53.732,14.025,53.767,13.95,53.801,13.866,53.853,13.822,54.019,13.724,54.153,13.448,54.141,13.147,54.283,13.029,54.411,12.898,54.423,12.779,54.446,12.575,54.467,12.379,54.347,12.296,54.284,12.169,54.226,12.111,54.168,11.796,54.145,11.701,54.114,11.461,53.965,11.4,53.945,11.104,54.009,10.918,53.995,10.855,54.01,10.811,54.075,11.009,54.181,11.064,54.281,11.013,54.379,10.956,54.376,10.732,54.316,10.36,54.438,10.212,54.409,10.171,54.45,10.143,54.488,9.869,54.472,9.941,54.515,10.029,54.581,10.022,54.674,9.954,54.738,9.892,54.781,9.746,54.807,9.705,54.928,9.732,54.968,9.688,55,9.645,55.023,9.572,55.041,9.454,55.04,9.505,55.116,9.643,55.205,9.671,55.266,9.64,55.344,9.626,55.414,9.591,55.493,9.661,55.557,9.773,55.608,9.81,55.651,9.899,55.708,9.999,55.736,10.024,55.761,9.962,55.813,9.904,55.843,10.017,55.876,10.107,55.874,10.159,55.854,10.183,55.865,10.227,56.005,10.319,56.213,10.374,56.252,10.427,56.276,10.539,56.2,10.621,56.202,10.753,56.242,10.856,56.296,10.894,56.359,10.926,56.443,10.883,56.493,10.846,56.522,10.49,56.521,10.384,56.555,10.283,56.621,10.297,56.781,10.287,56.823,10.296,56.999,10.338,57.021,10.437,57.172,10.524,57.243,10.518,57.379,10.537,57.449,10.445,57.562,10.46,57.615,10.481,57.649,10.61,57.737,10.533,57.735,10.259,57.617,9.962,57.581,9.815,57.478,9.554,57.232,9.434,57.174,9.299,57.147,9.036,57.155,8.952,57.151,8.812,57.11,8.619,57.111,8.427,56.984,8.284,56.852,8.266,56.815,8.268,56.754,8.347,56.712,8.468,56.665,8.603,56.71,8.772,56.725,8.876,56.887,8.993,57.016,9.11,57.044,9.255,57.012,9.21,56.808,9.196,56.702,9.14,56.75,9.067,56.794,8.995,56.775,8.888,56.735,8.736,56.627,8.718,56.544,8.672,56.496,8.608,56.515,8.553,56.56,8.473,56.565,8.281,56.617,8.232,56.618,8.164,56.607,8.13,56.321,8.121,56.14,8.202,55.982,8.181,55.901,8.132,55.6,8.345,55.51,8.616,55.418,8.651,55.329,8.67,55.156,8.573,55.134,8.638,55.046,8.661,54.986,8.682,54.792,8.79,54.696,8.881,54.594,8.957,54.538,8.952,54.468,8.831,54.428,8.648,54.398,8.626,54.354,8.645,54.295,8.736,54.295,8.78,54.313,8.852,54.3,8.907,54.261,8.904,54,8.92,53.965,8.978,53.926,9.07,53.901,9.216,53.891,9.312,53.859,9.631,53.6,9.784,53.555,9.673,53.566,9.585,53.6,9.322,53.813,9.206,53.856,8.898,53.836,8.619,53.875,8.576,53.838,8.528,53.781,8.506,53.671,8.538,53.557,8.495,53.394,8.493,53.514,8.451,53.552,8.334,53.606,8.302,53.584,8.279,53.511,8.245,53.445,8.201,53.432,8.108,53.468,8.167,53.543,8.009,53.691,7.629,53.697,7.285,53.681,7.206,53.655,7.107,53.557,7.074,53.478,7.053,53.376,7.152,53.327,7.197,53.282,7.058,53.301,6.968,53.327,6.912,53.375,6.816,53.441,6.564,53.434,6.353,53.415,6.062,53.407,5.874,53.375,5.532,53.269,5.446,53.214,5.358,53.096,5.061,52.961,4.888,52.908,4.839,52.928,4.769,52.941,4.713,52.872,4.678,52.81,4.562,52.443,4.483,52.309,4.376,52.197,4.209,52.059,4.132,52.012,4.085,51.994,4.026,51.928,3.979,51.848,3.947,51.811,4.135,51.673,4.158,51.633,4.183,51.61,4.005,51.596,4.08,51.551,4.175,51.519,4.239,51.504,4.274,51.472,4.206,51.457,4.141,51.456,3.886,51.574,3.744,51.596,3.549,51.589,3.5,51.577,3.449,51.541,3.521,51.486,3.587,51.454,3.694,51.45,3.822,51.409,4.007,51.443,4.139,51.402,4.226,51.386,4.112,51.361,4.011,51.396,3.883,51.354,3.717,51.369,3.589,51.399,3.426,51.394,3.35,51.378,3.225,51.352,2.96,51.265,2.525,51.097,2.446,51.067,1.913,50.991,1.768,50.936,1.672,50.885,1.61,50.819,1.579,50.739,1.552,50.294,1.593,50.252,1.548,50.231,1.514,50.205,1.407,50.089,1.246,49.998,0.924,49.91,0.616,49.863,0.187,49.703,0.127,49.602,0.109,49.558,0.129,49.508,0.278,49.463,0.439,49.473,0.417,49.448,0.136,49.402,-0.011,49.33,-0.163,49.297,-0.521,49.355,-0.766,49.36,-0.959,49.393,-1.139,49.388,-1.195,49.445,-1.232,49.495,-1.265,49.598,-1.259,49.68,-1.366,49.707,-1.588,49.668,-1.705,49.681,-1.856,49.684,-1.875,49.631,-1.87,49.595,-1.813,49.49,-1.69,49.313,-1.583,49.202,-1.565,48.806,-1.48,48.698,-1.376,48.653,-1.438,48.641,-1.825,48.631,-1.852,48.669,-1.906,48.697,-1.973,48.635,-2.004,48.582,-2.079,48.645,-2.446,48.648,-2.692,48.537,-2.793,48.601,-3.003,48.791,-3.231,48.841,-3.471,48.813,-3.546,48.766,-3.715,48.71,-3.856,48.695,-4.059,48.708,-4.531,48.62,-4.721,48.54,-4.762,48.45,-4.749,48.41,-4.719,48.363,-4.585,48.358,-4.525,48.372,-4.393,48.368,-4.364,48.357,-4.302,48.347,-4.241,48.304,-4.403,48.293,-4.498,48.299,-4.531,48.31,-4.577,48.29,-4.544,48.247,-4.512,48.23,-4.435,48.218,-4.329,48.17,-4.378,48.129,-4.512,48.097,-4.629,48.086,-4.679,48.04,-4.428,47.969,-4.375,47.877,-4.312,47.823,-4.226,47.81,-4.071,47.848,-3.901,47.838,-3.508,47.753,-3.444,47.711,-3.396,47.72,-3.329,47.713,-3.265,47.685,-3.222,47.694,-3.159,47.695,-3.064,47.621,-2.964,47.601,-2.859,47.614,-2.787,47.626,-2.733,47.602,-2.797,47.537,-2.77,47.514,-2.666,47.526,-2.554,47.527,-2.483,47.512,-2.428,47.471,-2.476,47.413,-2.53,47.382,-2.503,47.312,-2.434,47.291,-2.353,47.279,-1.975,47.311,-1.743,47.216,-1.821,47.225,-1.922,47.261,-2.028,47.274,-2.108,47.263,-2.149,47.224,-2.197,47.163,-2.144,47.126,-2.082,47.112,-2.019,47.038,-2.09,46.921,-2.092,46.865,-2.059,46.81,-1.921,46.685,-1.787,46.515,-1.392,46.35,-1.313,46.327,-1.239,46.325,-1.146,46.311,-1.132,46.253,-1.136,46.205,-1.104,45.925,-1.066,45.806,-1.042,45.773,-1.032,45.741,-1.114,45.769,-1.21,45.771,-1.196,45.714,-1.17,45.686,-0.881,45.538,-0.791,45.468,-0.733,45.385,-0.641,45.09,-0.582,45.051,-0.548,45.001,-0.634,45.047,-0.691,45.093,-0.767,45.314,-0.826,45.381,-0.942,45.457,-1.081,45.532,-1.149,45.343,-1.189,45.161,-1.245,44.667,-1.22,44.687,-1.2,44.726,-1.153,44.764,-1.077,44.69,-1.171,44.662,-1.246,44.56,-1.346,44.02,-1.485,43.564,-1.631,43.438,-1.794,43.407,-1.829,43.401,-1.991,43.345,-2.197,43.322,-2.337,43.328,-2.607,43.413,-2.875,43.454,-2.948,43.44,-3.046,43.372,-3.418,43.452,-3.524,43.511,-3.605,43.519,-3.774,43.478,-3.889,43.499,-4.015,43.463,-4.313,43.415,-4.523,43.416,-5.105,43.502,-5.316,43.553,-5.666,43.582,-5.847,43.645,-6.08,43.595,-6.224,43.604,-6.476,43.579,-6.617,43.592,-6.901,43.586,-7.061,43.554,-7.262,43.595,-7.399,43.696,-7.504,43.74,-7.595,43.727,-7.698,43.765,-7.853,43.707,-8.005,43.694,-8.137,43.629,-8.257,43.58,-8.289,43.54,-8.252,43.497,-8.249,43.439,-8.355,43.397,-8.422,43.386,-8.537,43.337,-8.666,43.317,-8.874,43.334,-9.025,43.239,-9.096,43.214,-9.178,43.174,-9.236,43.036,-9.235,42.977,-9.179,42.911,-9.127,42.865,-9.042,42.814,-8.927,42.799,-8.937,42.767,-9.035,42.662,-9.033,42.594,-8.988,42.586,-8.812,42.64,-8.8,42.6,-8.81,42.562,-8.812,42.47,-8.776,42.435,-8.73,42.412,-8.769,42.358,-8.81,42.334,-8.816,42.285,-8.729,42.287,-8.691,42.274,-8.772,42.211,-8.887,42.105,-8.878,41.947,-8.852,41.927,-8.777,41.941,-8.878,41.832,-8.888,41.765,-8.846,41.705,-8.755,41.698,-8.811,41.652,-8.806,41.56,-8.738,41.285,-8.675,41.154,-8.66,41.086,-8.656,41.029,-8.674,40.917,-8.685,40.753,-8.732,40.651,-8.772,40.606,-8.873,40.259,-8.887,40.179,-8.851,40.152,-8.838,40.116,-9.004,39.821,-9.148,39.543,-9.251,39.426,-9.32,39.391,-9.375,39.338,-9.357,39.284,-9.353,39.248,-9.414,39.112,-9.431,38.96,-9.475,38.853,-9.48,38.799,-9.474,38.731,-9.41,38.708,-9.357,38.698,-9.252,38.713,-9.136,38.743,-9.091,38.835,-8.954,39.016,-8.867,39.066,-8.792,39.078,-8.938,38.998,-9,38.903,-8.977,38.803,-9.021,38.747,-9.093,38.697,-9.178,38.688,-9.25,38.657,-9.203,38.539,-9.213,38.448,-9.187,38.438,-9.096,38.455,-8.915,38.512,-8.862,38.51,-8.799,38.518,-8.734,38.482,-8.668,38.424,-8.881,38.447,-8.811,38.3,-8.802,38.184,-8.879,37.959,-8.823,37.872,-8.792,37.733,-8.819,37.592,-8.814,37.431,-8.926,37.166,-8.998,37.032,-8.935,37.016,-8.848,37.076,-8.739,37.075,-8.598,37.121,-8.484,37.1,-8.137,37.077,-7.94,37.005,-7.834,37.006,-7.494,37.168,-7.406,37.179,-7.175,37.209,-6.975,37.198,-6.929,37.215,-6.864,37.279,-6.859,37.249,-6.885,37.194,-6.492,36.955,-6.396,36.832,-6.321,36.908,-6.217,36.914,-6.259,36.899,-6.328,36.848,-6.412,36.729,-6.384,36.637,-6.269,36.597,-6.258,36.565,-6.266,36.527,-6.226,36.426,-6.17,36.334,-6.041,36.188,-5.961,36.182,-5.808,36.088,-5.625,36.026,-5.551,36.039,-5.462,36.074,-5.444,36.151,-5.407,36.159,-5.382,36.134,-5.361,36.135,-5.33,36.236,-5.231,36.374,-5.171,36.424,-4.935,36.502,-4.674,36.506,-4.502,36.629,-4.435,36.7,-4.367,36.718,-3.828,36.756,-3.579,36.74,-3.431,36.708,-3.259,36.756,-3.149,36.758,-2.902,36.743,-2.788,36.715,-2.671,36.748,-2.596,36.806,-2.453,36.831,-2.306,36.82,-2.188,36.745,-2.112,36.777,-1.939,36.946,-1.798,37.233,-1.641,37.387,-1.328,37.561,-0.938,37.571,-0.822,37.581,-0.772,37.596,-0.722,37.631,-0.823,37.712,-0.815,37.77,-0.753,37.85,-0.742,37.886,-0.683,37.992,-0.647,38.152,-0.551,38.203,-0.521,38.317,-0.381,38.436,-0.053,38.586,0.136,38.697,0.202,38.759,0.155,38.825,-0.034,38.891,-0.134,38.969,-0.205,39.063,-0.329,39.417,-0.327,39.52,-0.075,39.876,0.043,40.014,0.158,40.107,0.364,40.319,0.596,40.615,0.627,40.622,0.66,40.613,0.721,40.63,0.859,40.686,0.891,40.722,0.796,40.804,0.715,40.823,0.817,40.892,1.033,41.062,1.206,41.098,1.567,41.196,2.083,41.287,2.146,41.321,2.311,41.467,3.005,41.767,3.147,41.861,3.248,41.944,3.238,42.082,3.225,42.111,3.175,42.136,3.15,42.162,3.166,42.256,3.219,42.26,3.307,42.289,3.288,42.344,3.24,42.368,3.211,42.431,3.198,42.461,3.091,42.591,3.043,42.838,3.052,42.915,3.163,43.081,3.259,43.193,3.785,43.462,3.862,43.516,3.911,43.563,4.053,43.593,4.075,43.582,4.113,43.563,4.163,43.504,4.224,43.48,4.376,43.456,4.41,43.447,4.629,43.387,4.712,43.373,4.789,43.379,4.787,43.401,4.808,43.405,4.844,43.394,4.874,43.412,4.912,43.427,4.976,43.427,5.06,43.445,5.061,43.406,5.073,43.367,5.12,43.349,5.2,43.352,5.32,43.345,5.407,43.229,5.672,43.178,5.809,43.098,6.031,43.101,6.116,43.072,6.305,43.139,6.494,43.169,6.57,43.199,6.657,43.262,6.687,43.335,6.717,43.374,6.865,43.438,7.181,43.659,7.262,43.696,7.378,43.732,7.439,43.75,7.493,43.767,7.733,43.803,8.005,43.877,8.082,43.919,8.292,44.137,8.552,44.346,8.766,44.422,8.93,44.408,9.196,44.323,9.289,44.319,9.731,44.101,10.048,44.02,10.188,43.948,10.246,43.852,10.321,43.513,10.448,43.371,10.521,43.204,10.532,43.14,10.517,43.065,10.515,42.968,10.59,42.954,10.645,42.957,10.708,42.936,10.737,42.9,10.765,42.845,10.803,42.804,10.938,42.739,11.168,42.535,11.185,42.457,11.142,42.444,11.103,42.417,11.141,42.39,11.189,42.393,11.25,42.416,11.296,42.423,11.498,42.363,11.637,42.288,11.807,42.082,12.075,41.941,12.206,41.813,12.631,41.47,12.849,41.409,13.024,41.301,13.041,41.266,13.089,41.244,13.183,41.278,13.247,41.289,13.362,41.279,13.555,41.232,13.67,41.254,13.733,41.236,13.86,41.13,14.048,40.87,14.044,40.812,14.076,40.794,14.102,40.827,14.147,40.821,14.309,40.813,14.428,40.759,14.461,40.729,14.34,40.599,14.383,40.6,14.459,40.633,14.557,40.626,14.611,40.645,14.766,40.668,14.84,40.63,14.907,40.556,14.948,40.469,14.986,40.377,14.929,40.31,14.927,40.265,14.951,40.239,15.295,40.07,15.391,40.052,15.585,40.053,15.693,39.99,15.764,39.87,15.854,39.627,16.024,39.354,16.071,39.139,16.107,39.024,16.21,38.941,16.197,38.759,16.066,38.736,15.972,38.713,15.927,38.672,15.879,38.614,15.905,38.483,15.822,38.303,15.7,38.262,15.643,38.175,15.646,38.034,15.725,37.939,16.057,37.942,16.11,38.019,16.144,38.086,16.282,38.25,16.546,38.409,16.574,38.494,16.559,38.715,16.617,38.8,16.755,38.89,16.951,38.94,17.099,38.919,17.175,38.998,17.123,39.137,17.115,39.381,16.999,39.482,16.824,39.578,16.598,39.639,16.522,39.748,16.53,39.86,16.67,40.137,16.807,40.326,16.928,40.458,17.031,40.513,17.18,40.503,17.215,40.486,17.249,40.438,17.258,40.399,17.396,40.34,17.476,40.315,17.865,40.28,18.078,39.937,18.219,39.853,18.344,39.821,18.393,39.904,18.423,39.987,18.486,40.105,18.461,40.221,18.328,40.371,18.036,40.565,17.955,40.655,17.474,40.841,17.275,40.975,17.103,41.062,16.552,41.232,16.013,41.435,15.9,41.512,15.914,41.621,16.034,41.701,16.151,41.758,16.189,41.814,16.165,41.896,16.062,41.928,15.964,41.939,15.405,41.913,15.169,41.934,14.866,42.053,14.541,42.244,14.183,42.506,14.01,42.69,13.925,42.852,13.805,43.18,13.693,43.39,13.564,43.571,13.508,43.612,13.295,43.686,12.907,43.921,12.691,43.995,12.487,44.134,12.396,44.224,12.305,44.429,12.248,44.723,12.279,44.832,12.319,44.833,12.384,44.798,12.464,44.845,12.498,44.899,12.523,44.968,12.392,45.04,12.286,45.208,12.226,45.242,12.249,45.369,12.274,45.446,12.354,45.492,12.492,45.546,12.536,45.545,12.432,45.468,12.498,45.462,12.612,45.497,12.761,45.544,12.903,45.611,13.03,45.638,13.12,45.698,13.157,45.747,13.206,45.771,13.465,45.71,13.558,45.771,13.628,45.771,13.783,45.627,13.72,45.588,13.637,45.536,13.578,45.517,13.517,45.482,13.603,45.231,13.613,45.163,13.629,45.108,13.742,44.992,13.861,44.837,13.9,44.829,13.966,44.836,14.042,44.927,14.091,44.998,14.236,45.16,14.269,45.283,14.313,45.338,14.386,45.342,14.55,45.298,14.632,45.223,14.855,45.081,14.907,44.971,14.885,44.818,14.895,44.707,14.981,44.603,15.27,44.383,15.381,44.328,15.471,44.272,15.37,44.289,15.284,44.289,15.231,44.271,15.185,44.273,15.123,44.257,15.186,44.172,15.499,43.909,15.656,43.811,15.821,43.736,15.942,43.657,15.949,43.607,15.943,43.569,15.986,43.52,16.046,43.506,16.131,43.506,16.269,43.531,16.394,43.543,16.6,43.464,16.903,43.392,17.129,43.211,17.33,43.115,17.537,42.962,17.585,42.938,17.668,42.897,17.724,42.851,17.22,43.026,17.126,43.026,17.045,43.015,17.258,42.968,17.585,42.837,17.824,42.797,18.161,42.634,18.333,42.528,18.517,42.433,18.554,42.429,18.592,42.444,18.646,42.443,18.633,42.423,18.619,42.398,18.633,42.378,18.894,42.249,19.122,42.06,19.186,41.949,19.342,41.869,19.468,41.856,19.578,41.788,19.576,41.64,19.546,41.597,19.497,41.563,19.441,41.425,19.453,41.321,19.48,41.236,19.456,41.106,19.461,40.933,19.384,40.791,19.338,40.664,19.345,40.622,19.439,40.47,19.459,40.405,19.441,40.376,19.395,40.394,19.359,40.409,19.322,40.407,19.36,40.348,19.398,40.285,19.485,40.21,19.852,40.044,19.965,39.872,19.996,39.801,20.001,39.709,20.099,39.641,20.191,39.546,20.301,39.327,20.468,39.255,20.572,39.148,20.691,39.067,20.713,39.035,20.78,39.009,20.923,39.037,21.034,39.026,21.069,39.032,21.118,39.03,21.145,38.979,21.152,38.922,21.112,38.896,21.074,38.885,20.893,38.941,20.777,38.928,20.769,38.874,20.777,38.808,20.873,38.776,20.992,38.654,21.06,38.503,21.113,38.385,21.183,38.346,21.303,38.374,21.33,38.424,21.331,38.487,21.355,38.475,21.39,38.408,21.473,38.321,21.568,38.334,21.65,38.354,21.717,38.355,21.805,38.367,21.965,38.412,22.227,38.353,22.32,38.357,22.385,38.386,22.422,38.439,22.583,38.345,22.754,38.29,22.784,38.262,22.834,38.235,22.933,38.202,22.995,38.216,23.034,38.202,23.094,38.196,23.149,38.176,23.183,38.134,23.153,38.096,23.122,38.073,22.955,38.075,22.893,38.051,22.917,38.007,22.92,37.958,22.846,37.968,22.8,37.981,22.712,38.047,22.556,38.113,22.244,38.189,21.953,38.321,21.825,38.328,21.748,38.274,21.658,38.175,21.549,38.165,21.451,38.205,21.404,38.197,21.308,38.027,21.145,37.919,21.125,37.892,21.138,37.854,21.205,37.829,21.288,37.775,21.329,37.669,21.416,37.64,21.571,37.541,21.679,37.387,21.692,37.309,21.579,37.2,21.583,37.081,21.738,36.863,21.892,36.737,21.934,36.804,21.94,36.892,21.956,36.99,22.012,37.017,22.08,37.029,22.134,36.964,22.165,36.903,22.231,36.883,22.376,36.702,22.381,36.646,22.375,36.514,22.428,36.476,22.489,36.447,22.489,36.568,22.608,36.78,22.717,36.794,22.78,36.786,22.832,36.687,22.983,36.528,23.061,36.487,23.107,36.452,23.16,36.448,23.112,36.548,23.041,36.645,23.074,36.775,23.06,36.854,22.995,37.016,22.851,37.291,22.765,37.393,22.725,37.542,22.775,37.585,22.851,37.532,22.941,37.517,23.015,37.482,23.096,37.441,23.1,37.364,23.162,37.334,23.203,37.349,23.253,37.377,23.489,37.44,23.491,37.464,23.458,37.497,23.409,37.542,23.396,37.58,23.348,37.598,23.263,37.595,23.198,37.62,23.147,37.716,23.147,37.795,23.086,37.853,23.036,37.878,23.047,37.903,23.087,37.913,23.194,37.959,23.42,37.992,23.502,38.035,23.537,38.033,23.58,38.011,23.733,37.884,23.877,37.778,23.972,37.677,24.02,37.678,24.055,37.71,24.062,37.775,24.061,37.818,24.033,37.955,24.025,38.14,24.005,38.227,23.967,38.275,23.836,38.325,23.684,38.352,23.57,38.489,23.369,38.526,23.253,38.661,23.138,38.668,23.02,38.742,22.774,38.8,22.688,38.849,22.634,38.851,22.569,38.867,22.597,38.891,22.677,38.899,22.803,38.902,22.93,38.948,23.067,39.038,22.966,39.031,22.939,39.112,22.886,39.17,22.839,39.259,22.921,39.306,22.993,39.331,23.162,39.258,23.169,39.21,23.119,39.133,23.155,39.101,23.218,39.104,23.328,39.175,23.288,39.289,23.233,39.358,23.103,39.492,22.979,39.564,22.919,39.629,22.836,39.801,22.592,40.037,22.569,40.119,22.605,40.276,22.643,40.367,22.625,40.429,22.629,40.496,22.742,40.536,22.811,40.579,22.922,40.591,22.893,40.524,22.851,40.491,22.896,40.4,23.098,40.304,23.312,40.216,23.328,40.09,23.396,39.99,23.627,39.924,23.658,39.934,23.674,39.959,23.467,40.074,23.433,40.115,23.386,40.222,23.426,40.264,23.665,40.224,23.835,40.022,23.947,39.966,23.982,39.994,24.001,40.025,23.967,40.115,23.918,40.155,23.823,40.205,23.721,40.286,23.728,40.33,23.823,40.368,23.913,40.359,24.056,40.304,24.159,40.28,24.232,40.215,24.343,40.148,24.292,40.242,24.213,40.328,24.031,40.409,23.932,40.406,23.867,40.419,23.832,40.482,23.879,40.544,23.779,40.628,23.743,40.677,23.763,40.748,23.946,40.748,24.082,40.724,24.234,40.786,24.384,40.913,24.477,40.948,24.557,40.936,24.679,40.869,24.793,40.858,25.005,40.968,25.104,40.995,25.25,40.933,25.325,40.943,25.497,40.888,25.856,40.844,26.011,40.769,26.039,40.727,26.068,40.683,26.105,40.611,26.224,40.618,26.361,40.606,26.578,40.625,26.792,40.627,26.72,40.544,26.447,40.445,26.355,40.39,26.254,40.315,26.252,40.248,26.26,40.202,26.226,40.142,26.203,40.075,26.272,40.097,26.33,40.123,26.468,40.261,26.772,40.498,26.975,40.564,27.258,40.687,27.43,40.84,27.499,40.973,27.747,41.013,27.925,40.991,28.086,41.061,28.172,41.081,28.295,41.071,28.78,40.974,28.956,41.008,28.996,41.061,29.032,41.14,29.057,41.23,28.947,41.248,28.346,41.466,28.198,41.554,28.05,41.729,27.987,41.855,28.014,41.969,27.983,42.047,27.821,42.208,27.708,42.35,27.64,42.401,27.485,42.468,27.754,42.707,27.818,42.717,27.889,42.75,27.896,43.021,27.929,43.186,27.979,43.231,28.035,43.268,28.134,43.396,28.32,43.427,28.465,43.389,28.562,43.501,28.585,43.742,28.591,43.797,28.659,43.984,28.645,44.296,28.699,44.374,28.852,44.506,28.888,44.575,28.807,44.565,28.814,44.602,28.846,44.637,28.849,44.716,28.87,44.75,28.926,44.81,28.892,44.919,28.931,44.966,28.981,44.993,29.095,44.975,29.048,44.926,29.069,44.871,29.081,44.799,29.048,44.758,29.558,44.843,29.605,44.915,29.635,44.98,29.679,45.152,29.689,45.193,29.706,45.26,29.727,45.343,29.67,45.541,29.601,45.6,29.602,45.683,29.628,45.722,29.685,45.755,29.821,45.732,29.902,45.752,30.007,45.798,30.184,45.85,30.219,45.867,30.493,46.09,30.512,46.105,30.657,46.267,30.672,46.304,30.722,46.366,30.773,46.473,30.796,46.552,31.137,46.624,31.32,46.613,31.403,46.629,31.497,46.738,31.563,46.777,31.532,46.665,31.657,46.642,31.78,46.632,31.873,46.65,31.902,46.722,31.913,46.926,31.866,47.003,31.837,47.087,31.759,47.213,31.838,47.157,31.865,47.095,31.94,46.982,31.964,46.855,31.945,46.784,31.974,46.709,32.044,46.642,32.127,46.597,32.354,46.565,32.578,46.616,32.553,46.592,32.419,46.518,32.361,46.475,32.131,46.509,31.878,46.522,31.716,46.555,31.555,46.554,31.624,46.51,31.714,46.472,31.856,46.462,32.008,46.43,32.013,46.387,31.992,46.364,31.916,46.349,31.843,46.346,31.78,46.325,31.831,46.282,31.925,46.287,32.036,46.261,32.33,46.13,32.477,46.084,32.797,46.131,32.942,46.124,33.202,46.176,33.263,46.126,33.43,46.058,33.499,46.079,33.594,46.096,33.637,46.033,33.665,45.947,33.466,45.838,33.28,45.765,33.142,45.749,32.828,45.593,32.508,45.404,32.552,45.35,32.611,45.328,32.773,45.359,32.919,45.348,33.187,45.195,33.262,45.171,33.392,45.188,33.555,45.098,33.601,44.981,33.612,44.908,33.53,44.681,33.491,44.619,33.463,44.597,33.451,44.554,33.656,44.433,33.756,44.399,33.91,44.388,34.074,44.424,34.282,44.538,34.47,44.722,34.717,44.807,34.888,44.824,35.088,44.803,35.155,44.896,35.358,44.978,35.473,45.098,35.57,45.119,35.678,45.102,35.759,45.071,35.804,45.04,35.87,45.005,36.055,45.031,36.23,45.026,36.393,45.065,36.428,45.153,36.451,45.232,36.514,45.304,36.575,45.394,36.427,45.433,36.29,45.457,36.171,45.453,36.077,45.424,36.013,45.372,35.833,45.402,35.751,45.389,35.558,45.311,35.458,45.316,35.374,45.354,35.26,45.447,35.023,45.701,34.907,45.879,34.857,45.987,34.844,46.074,34.85,46.19,34.97,46.242,35.064,46.267,35.23,46.441,35.292,46.371,35.291,46.314,35.28,46.279,35.218,46.232,35.015,46.106,35.055,46.104,35.132,46.126,35.204,46.169,35.257,46.204,35.4,46.381,35.827,46.624,36.025,46.667,36.195,46.646,36.279,46.659,36.432,46.733,36.559,46.763,36.689,46.764,36.795,46.714,36.932,46.825,37.048,46.876,37.219,46.917,37.34,46.917,37.543,47.075,37.829,47.096,38.178,47.08,38.214,47.091,38.485,47.176,38.577,47.239,38.762,47.262,38.736,47.236,38.644,47.212,38.552,47.15,38.668,47.144,38.928,47.176,39.024,47.272,39.196,47.269,39.245,47.2,39.293,47.106,39.289,47.071,39.271,47.044,39.127,47.023,38.801,46.906,38.631,46.873,38.439,46.813,38.488,46.732,38.501,46.664,38.343,46.678,38.23,46.701,38.159,46.691,37.968,46.618,37.867,46.634,37.767,46.636,37.81,46.532,37.914,46.406,37.978,46.383,38.078,46.394,38.315,46.242,38.492,46.091,38.4,46.08,38.312,46.095,38.184,46.095,38.133,46.003,38.08,45.935,38.07,45.97,38.074,46.017,38.014,46.048,37.933,46.002,37.841,45.8,37.669,45.654,37.612,45.565,37.61,45.5,37.634,45.486,37.672,45.488,37.673,45.43,37.647,45.377,37.264,45.311,37.214,45.272,37.104,45.303,36.978,45.384,36.866,45.427,36.794,45.41,36.72,45.372,36.762,45.348,36.811,45.34,36.941,45.29,36.873,45.252,36.619,45.185,36.628,45.151,36.651,45.126,36.944,45.07,37.205,44.972,37.284,44.905,37.352,44.788,37.411,44.735,37.495,44.695,37.572,44.671,37.705,44.661,37.851,44.699,38.181,44.42,38.312,44.374,38.636,44.318,38.717,44.288,39.329,43.897,39.517,43.728,39.874,43.473,39.978,43.42,40.191,43.312,40.462,43.146,40.524,43.121,40.837,43.063,41.062,42.931,41.129,42.828,41.419,42.738,41.489,42.659,41.578,42.398,41.663,42.147,41.763,41.97,41.761,41.885,41.759,41.817,41.702,41.705,41.51,41.517,41.414,41.424,41.084,41.261,40.959,41.212,40.82,41.19,40.688,41.107,40.265,40.961,40.128,40.943,40,40.977,39.911,40.966,39.808,40.983,39.426,41.106,38.852,41.018,38.557,40.937,38.381,40.925,37.91,41.002,37.766,41.079,37.431,41.114,37.066,41.184,36.992,41.275,36.778,41.363,36.647,41.353,36.587,41.327,36.51,41.263,36.405,41.275,36.278,41.336,36.179,41.427,36.052,41.683,35.978,41.705,35.92,41.714,35.558,41.634,35.298,41.729,35.209,41.794,35.122,41.891,35.114,41.957,35.141,41.99,35.155,42.028,35.006,42.063,34.75,41.957,34.193,41.964,33.381,42.018,33.285,42.005,32.947,41.892,32.542,41.806,32.306,41.73,32.086,41.589,31.458,41.32,31.347,41.158,31.255,41.108,30.81,41.085,30.345,41.197,29.919,41.151,29.322,41.228,29.148,41.221,29.094,41.177,29.067,41.102,29.046,41.008,29.082,40.963,29.114,40.938,29.26,40.847,29.365,40.809,29.801,40.76,29.849,40.76,29.845,40.738,29.508,40.708,29.054,40.649,28.958,40.631,28.788,40.534,28.841,40.503,28.895,40.482,28.974,40.467,29.055,40.424,29.007,40.39,28.739,40.391,28.63,40.376,28.289,40.403,27.963,40.37,27.929,40.38,27.964,40.435,27.995,40.467,27.99,40.489,27.875,40.513,27.769,40.51,27.732,40.481,27.849,40.382,27.789,40.351,27.728,40.329,27.476,40.32,27.333,40.376,27.314,40.415,27.285,40.456,27.122,40.452,27.012,40.396,26.738,40.4,26.475,40.197,26.313,40.025,26.181,39.99,26.15,39.873,26.155,39.657,26.101,39.569,26.096,39.521,26.113,39.467,26.351,39.484,26.484,39.521,26.827,39.563,26.899,39.55,26.911,39.517,26.813,39.419,26.711,39.34,26.682,39.292,26.719,39.261,26.854,39.116,26.849,39.057,26.808,39.014,26.815,38.961,26.866,38.923,26.92,38.934,26.97,38.919,27.014,38.887,26.909,38.776,26.79,38.736,26.764,38.71,26.788,38.66,26.795,38.626,26.838,38.558,26.907,38.482,26.97,38.448,27.144,38.452,27.099,38.416,26.861,38.373,26.77,38.388,26.727,38.419,26.696,38.405,26.674,38.336,26.641,38.352,26.595,38.419,26.61,38.487,26.587,38.557,26.514,38.629,26.441,38.641,26.378,38.624,26.372,38.562,26.43,38.441,26.416,38.368,26.344,38.37,26.291,38.277,26.333,38.242,26.428,38.214,26.525,38.162,26.582,38.149,26.621,38.176,26.683,38.198,26.807,38.138,26.879,38.055,26.944,38.063,27.159,37.987,27.232,37.979,27.255,37.882,27.224,37.725,27.078,37.688,27.068,37.658,27.148,37.604,27.204,37.491,27.219,37.389,27.29,37.349,27.376,37.341,27.401,37.307,27.52,37.249,27.535,37.164,27.368,37.122,27.3,37.127,27.25,37.079,27.263,36.977,27.311,36.982,27.349,37.02,27.668,37.007,28.134,37.029,28.242,37.029,28.224,36.996,28.083,36.92,28.005,36.832,27.934,36.809,27.631,36.787,27.555,36.759,27.467,36.746,27.454,36.712,27.54,36.684,27.656,36.675,27.804,36.736,28.084,36.751,28.014,36.67,28.019,36.634,28.112,36.646,28.196,36.686,28.304,36.812,28.484,36.804,28.718,36.701,28.817,36.675,28.896,36.674,28.97,36.715,29.038,36.693,29.058,36.638,29.066,36.59,29.116,36.52,29.143,36.397,29.224,36.324,29.348,36.259,29.689,36.157,29.789,36.168,30.083,36.249,30.232,36.307,30.295,36.288,30.387,36.243,30.446,36.27,30.484,36.31,30.506,36.451,30.558,36.526,30.582,36.797,30.644,36.866,30.95,36.849,31.241,36.822,31.353,36.801,31.778,36.613,32.022,36.535,32.131,36.449,32.284,36.268,32.378,36.184,32.534,36.101,32.795,36.036,32.929,36.096,33.1,36.103,33.442,36.153,33.523,36.144,33.695,36.182,33.955,36.295,34.023,36.341,34.3,36.604,34.601,36.784,34.704,36.817,34.811,36.799,34.943,36.726,35.176,36.635,35.393,36.575,35.537,36.597,35.626,36.653,35.661,36.724,35.734,36.764,35.802,36.778,35.905,36.848,36.049,36.911,36.135,36.852,36.18,36.807,36.188,36.743,36.188,36.659,36.032,36.523,35.883,36.406,35.811,36.31,35.887,36.159,35.957,35.998,35.893,35.917,35.84,35.849,35.764,35.572,35.902,35.421,35.916,35.351,35.918,35.3,35.943,35.224,35.89,35.06,35.888,34.949,35.899,34.852,35.976,34.629,35.978,34.547,35.921,34.493,35.804,34.437,35.648,34.248,35.612,34.032,35.511,33.88,35.336,33.503,35.251,33.393,35.204,33.259,35.155,33.16,35.109,33.084,35.077,32.967,35.006,32.827,34.922,32.614,34.804,32.196,34.678,31.896,34.484,31.592,34.477,31.585,34.387,31.484,34.198,31.323,34.176,31.304,33.903,31.181,33.667,31.13,33.378,31.131,33.194,31.085,33.157,31.126,33.13,31.168,32.902,31.111,32.854,31.118,32.685,31.074,32.603,31.069,32.533,31.101,32.324,31.256,32.251,31.295,32.216,31.294,32.243,31.247,32.282,31.201,32.207,31.119,32.102,31.093,32.066,31.153,32.008,31.221,31.902,31.24,31.771,31.293,31.876,31.414,31.892,31.482,32.076,31.344,32.136,31.341,31.964,31.502,31.889,31.541,31.839,31.526,31.607,31.456,31.524,31.458,31.194,31.588,31.083,31.603,31.052,31.592,31.031,31.508,31.002,31.463,30.841,31.44,30.7,31.404,30.563,31.417,30.884,31.522,30.924,31.567,30.571,31.473,30.395,31.458,30.344,31.403,30.312,31.357,30.262,31.317,30.223,31.258,30.128,31.256,30.049,31.265,29.93,31.227,29.592,31.012,29.429,30.927,29.279,30.867,29.16,30.835,29.072,30.83,28.973,30.857,28.807,30.943,28.515,31.05,27.968,31.097,27.83,31.195,27.62,31.192,27.54,31.213,27.248,31.378,26.769,31.47,26.457,31.512,25.893,31.621,25.382,31.513,25.225,31.534,25.15,31.655,25.115,31.712,25.025,31.883,24.951,31.954,24.879,31.984,24.684,32.016,24.48,31.997,24.13,32.009,24.039,32.037,23.898,32.127,23.798,32.159,23.286,32.214,23.106,32.331,23.11,32.397,23.13,32.448,23.091,32.619,22.917,32.687,22.754,32.741,22.523,32.794,22.341,32.88,22.187,32.918,21.839,32.909,21.721,32.942,21.636,32.937,21.425,32.799,21.319,32.778,21.062,32.776,20.621,32.58,20.371,32.431,20.121,32.219,20.031,32.108,19.973,31.999,19.926,31.818,19.961,31.556,20.02,31.411,20.104,31.301,20.141,31.196,20.151,31.079,20.112,30.964,20.013,30.801,19.713,30.488,19.59,30.414,19.292,30.288,19.124,30.266,18.936,30.29,18.67,30.416,18.19,30.777,17.949,30.852,17.83,30.928,17.349,31.081,16.782,31.215,16.451,31.227,16.123,31.264,15.832,31.361,15.706,31.426,15.596,31.531,15.496,31.657,15.414,31.834,15.363,31.971,15.359,32.16,15.267,32.312,15.177,32.391,14.513,32.511,14.424,32.55,14.237,32.681,14.156,32.71,13.835,32.792,13.648,32.799,13.536,32.824,13.283,32.915,13.138,32.897,12.754,32.801,12.427,32.829,12.28,32.859,11.813,33.094,11.657,33.119,11.505,33.182,11.401,33.225,11.338,33.209,11.234,33.234,11.203,33.249,11.232,33.272,11.27,33.286,11.257,33.309,11.15,33.369,11.085,33.563,10.958,33.626,10.898,33.534,10.828,33.519,10.723,33.514,10.704,33.61,10.713,33.689,10.454,33.663,10.305,33.728,10.159,33.85,10.049,34.056,10.04,34.14,10.065,34.212,10.118,34.28,10.2,34.346,10.535,34.545,10.691,34.678,10.866,34.884,10.956,35.034,11.12,35.24,11.043,35.335,11.032,35.454,11.001,35.552,11.004,35.634,10.784,35.772,10.689,35.8,10.591,35.887,10.506,36.032,10.477,36.175,10.488,36.255,10.526,36.323,10.642,36.42,10.798,36.493,10.967,36.743,11.057,36.841,11.127,36.874,11.077,36.967,11.054,37.073,10.951,37.059,10.766,36.93,10.571,36.879,10.518,36.791,10.412,36.732,10.293,36.781,10.334,36.865,10.189,37.034,10.196,37.206,10.087,37.251,9.988,37.258,9.876,37.254,9.879,37.213,9.896,37.182,9.83,37.135,9.784,37.211,9.816,37.255,9.838,37.309,9.759,37.33,9.688,37.34,9.142,37.195,9.059,37.156,8.824,36.998,8.577,36.937,8.127,36.91,7.91,36.856,7.792,36.88,7.608,37,7.432,37.059,7.204,37.092,7.238,36.969,7.143,36.943,6.928,36.919,6.576,37.003,6.487,37.086,6.328,37.046,6.249,36.938,6.065,36.864,5.725,36.8,5.425,36.675,5.295,36.648,5.196,36.677,4.995,36.808,4.878,36.862,4.758,36.896,3.779,36.896,3.521,36.795,2.973,36.784,2.846,36.739,2.593,36.601,2.343,36.61,1.975,36.568,1.257,36.52,0.972,36.444,0.791,36.357,0.515,36.262,0.312,36.162,0.152,36.063,0.048,35.901,-0.048,35.833,-0.189,35.819,-0.351,35.863,-0.426,35.862,-0.917,35.668,-1.088,35.579,-1.205,35.496,-1.336,35.364,-1.484,35.303,-1.674,35.183,-1.913,35.094,-2.018,35.085,-2.22,35.104,-2.424,35.123,-2.637,35.113,-2.731,35.135,-2.84,35.128,-2.87,35.173,-2.926,35.287,-2.954,35.315,-2.958,35.363,-2.972,35.407,-3.063,35.317,-3.206,35.239,-3.395,35.212,-3.591,35.228,-3.693,35.28,-3.788,35.245,-3.982,35.243,-4.33,35.161,-4.628,35.206,-4.837,35.281,-5.105,35.468,-5.253,35.615,-5.338,35.745,-5.338,35.857,-5.278,35.903,-5.397,35.93,-5.522,35.862,-5.623,35.829,-5.748,35.816,-5.925,35.786,-5.958,35.681,-6.353,34.776,-6.756,34.133,-6.901,33.969,-7.145,33.83,-7.562,33.64,-8.301,33.374,-8.513,33.252,-8.596,33.187,-8.836,32.92,-9.246,32.572,-9.249,32.486,-9.287,32.241,-9.347,32.086,-9.675,31.711,-9.809,31.425,-9.833,31.07,-9.832,30.847,-9.875,30.718,-9.854,30.645,-9.773,30.603,-9.653,30.448,-9.624,30.353,-9.667,30.109,-9.743,29.958,-9.853,29.809,-10.01,29.641,-10.201,29.38,-10.486,29.065,-10.674,28.939,-11.081,28.714,-11.299,28.526,-11.43,28.382,-11.553,28.31,-11.986,28.129,-12.469,28.009,-12.794,27.978,-12.949,27.914,-13.041,27.77,-13.176,27.656,-13.256,27.435,-13.41,27.147,-13.496,26.873,-13.576,26.735,-13.696,26.643,-13.952,26.489,-14.168,26.415,-14.312,26.297,-14.414,26.254,-14.471,26.163,-14.523,25.925,-14.602,25.809,-14.707,25.548,-14.795,25.404,-14.843,25.22,-14.856,24.872,-14.904,24.72,-15.039,24.549,-15.189,24.479,-15.586,24.073,-15.778,23.953,-15.899,23.844,-15.953,23.741,-15.981,23.67,-15.913,23.728,-15.855,23.8,-15.802,23.842,-15.789,23.793,-15.806,23.75,-15.943,23.553,-15.997,23.425,-16.114,23.228,-16.21,23.098,-16.17,23.032,-16.202,22.945,-16.304,22.835,-16.359,22.595,-16.514,22.333,-16.684,22.274,-16.793,22.16,-16.931,21.9,-17.01,21.377,-17.099,20.857,-17.048,20.806,-16.998,21.04,-16.971,21.076,-16.928,21.115,-16.876,21.086,-16.728,20.806,-16.623,20.634,-16.563,20.604,-16.535,20.654,-16.53,20.71,-16.479,20.69,-16.43,20.652,-16.334,20.416,-16.21,20.228,-16.241,20.141,-16.233,20.001,-16.283,19.787,-16.445,19.473,-16.305,19.513,-16.371,19.41,-16.475,19.391,-16.514,19.362,-16.476,19.285,-16.306,19.154,-16.213,19.003,-16.15,18.718,-16.085,18.521,-16.047,18.223,-16.03,17.888,-16.079,17.546,-16.207,17.193,-16.347,16.926,-16.464,16.602,-16.481,16.454,-16.536,16.287,-16.535,15.838,-16.571,15.734,-16.843,15.294,-17.147,14.922,-17.412,14.792,-17.536,14.755,-17.445,14.652,-17.418,14.723,-17.346,14.729,-17.261,14.701,-17.168,14.641,-17.079,14.483,-16.974,14.403,-16.881,14.208,-16.798,14.093,-16.792,14.004,-16.742,14.006,-16.667,14.036,-16.618,14.041,-16.64,14.007,-16.734,13.961,-16.767,13.905,-16.745,13.84,-16.648,13.771,-16.588,13.69,-16.562,13.587,-16.53,13.458,-16.441,13.353,-16.352,13.343,-16.135,13.448,-15.85,13.46,-15.57,13.5,-15.438,13.483,-15.427,13.468,-15.471,13.459,-15.618,13.46,-15.804,13.425,-15.986,13.409,-16.158,13.384,-16.188,13.326,-16.185,13.283,-16.272,13.294,-16.413,13.27,-16.556,13.303,-16.598,13.357,-16.615,13.435,-16.669,13.475,-16.75,13.425,-16.825,13.341,-16.769,13.148,-16.763,13.064,-16.757,12.98,-16.769,12.883,-16.759,12.702,-16.778,12.67,-16.768,12.628,-16.744,12.585,-16.701,12.603,-16.673,12.622,-16.638,12.685,-16.598,12.715,-16.549,12.664,-16.455,12.625,-16.443,12.609,-16.45,12.581,-16.488,12.582,-16.553,12.605,-16.678,12.56,-16.76,12.526,-16.785,12.473,-16.746,12.4,-16.712,12.355,-16.437,12.204,-16.312,12.243,-16.245,12.237,-16.255,12.206,-16.319,12.144,-16.328,12.052,-16.274,11.978,-16.138,11.917,-15.959,11.96,-15.92,11.938,-15.903,11.92,-15.942,11.787,-15.819,11.763,-15.651,11.818,-15.513,11.918,-15.435,11.944,-15.188,11.927,-15.112,11.97,-15.078,11.969,-15.072,11.947,-15.102,11.914,-15.133,11.907,-15.211,11.871,-15.416,11.872,-15.467,11.843,-15.5,11.778,-15.502,11.724,-15.413,11.615,-15.36,11.623,-15.317,11.669,-15.23,11.687,-15.122,11.662,-15.073,11.598,-15.164,11.581,-15.253,11.573,-15.429,11.499,-15.479,11.41,-15.449,11.39,-15.399,11.401,-15.355,11.396,-15.348,11.378,-15.395,11.334,-15.401,11.266,-15.393,11.217,-15.317,11.152,-15.263,11.161,-15.217,11.156,-15.222,11.031,-15.181,11.034,-15.097,11.14,-15.055,11.142,-15.094,11.011,-15.043,10.94,-15.051,10.835,-15.012,10.804,-14.975,10.803,-14.925,10.945,-14.887,10.968,-14.837,10.963,-14.776,10.932,-14.757,10.862,-14.693,10.741,-14.677,10.689,-14.594,10.767,-14.587,10.735,-14.614,10.618,-14.61,10.55,-14.427,10.248,-14.17,10.129,-14.086,10.127,-14.045,10.141,-14.03,10.115,-14.022,10.048,-13.955,9.969,-13.82,9.887,-13.754,9.87,-13.713,9.923,-13.69,9.928,-13.7,9.851,-13.659,9.776,-13.657,9.639,-13.691,9.536,-13.568,9.543,-13.436,9.42,-13.406,9.361,-13.396,9.314,-13.296,9.219,-13.269,9.171,-13.303,9.078,-13.293,9.049,-13.272,8.987,-13.154,8.898,-13.059,8.881,-13.071,8.856,-13.207,8.843,-13.226,8.766,-13.228,8.696,-13.182,8.577,-13.122,8.589,-13.088,8.626,-12.953,8.615,-12.904,8.656,-12.894,8.63,-12.913,8.582,-12.994,8.526,-13.085,8.425,-13.158,8.442,-13.203,8.484,-13.261,8.488,-13.273,8.43,-13.202,8.336,-13.149,8.215,-13.021,8.201,-12.957,8.145,-12.925,8.055,-12.881,7.857,-12.851,7.819,-12.782,7.791,-12.698,7.716,-12.57,7.701,-12.51,7.753,-12.48,7.753,-12.51,7.666,-12.433,7.545,-12.481,7.442,-12.486,7.386,-12.347,7.342,-11.929,7.184,-11.733,7.089,-11.548,6.947,-11.508,6.907,-11.292,6.688,-11.005,6.557,-10.849,6.465,-10.786,6.31,-10.708,6.258,-10.597,6.211,-10.418,6.167,-10.276,6.078,-9.654,5.519,-9.375,5.241,-9.132,5.055,-8.259,4.59,-7.998,4.509,-7.66,4.367,-7.545,4.351,-7.426,4.376,-7.231,4.486,-7.058,4.545,-6.923,4.638,-6.845,4.671,-6.548,4.762,-6.062,4.953,-5.914,5.011,-5.565,5.089,-5.062,5.131,-5.105,5.162,-5.266,5.16,-5.368,5.151,-5.335,5.192,-5.282,5.21,-5.024,5.204,-4.97,5.148,-4.9,5.138,-4.662,5.173,-4.037,5.23,-4.062,5.257,-4.115,5.262,-4.609,5.236,-4.553,5.28,-4.357,5.301,-4.12,5.31,-3.984,5.293,-3.871,5.221,-3.348,5.131,-3.312,5.161,-3.238,5.335,-3.2,5.354,-3.151,5.348,-3.169,5.203,-3.064,5.158,-3.026,5.151,-3.019,5.131,-3.087,5.128,-3.215,5.147,-3.246,5.114,-3.114,5.089,-3.082,5.082,-2.965,5.046,-2.723,5.014,-2.399,4.929,-2.266,4.874,-2.09,4.764,-2.002,4.762,-1.777,4.88,-1.638,4.981,-1.502,5.038,-1.064,5.183,-0.798,5.227,-0.669,5.319,-0.485,5.394,-0.349,5.501,-0.127,5.568,0.26,5.757,0.672,5.76,0.749,5.76,0.95,5.81,1.008,5.906,1.05,5.994,1.106,6.051,1.187,6.089,1.311,6.147,1.623,6.217,1.818,6.261,2.287,6.328,2.706,6.369,2.772,6.376,3.336,6.397,3.43,6.525,3.503,6.531,3.717,6.598,3.752,6.584,3.546,6.477,3.49,6.457,3.451,6.427,3.487,6.409,4.126,6.411,4.431,6.349,4.634,6.217,4.861,6.026,5.042,5.798,5.093,5.767,5.106,5.728,5.112,5.642,5.173,5.603,5.276,5.642,5.305,5.694,5.327,5.708,5.325,5.648,5.35,5.623,5.418,5.625,5.457,5.612,5.394,5.575,5.289,5.577,5.216,5.572,5.199,5.534,5.232,5.484,5.386,5.402,5.55,5.474,5.532,5.426,5.501,5.379,5.439,5.365,5.368,5.338,5.364,5.259,5.37,5.195,5.388,5.174,5.476,5.154,5.452,5.127,5.403,5.142,5.383,5.129,5.448,4.946,5.493,4.839,5.554,4.733,5.588,4.647,5.799,4.456,5.906,4.388,5.971,4.339,6.077,4.291,6.173,4.277,6.206,4.292,6.215,4.385,6.271,4.432,6.275,4.372,6.256,4.334,6.264,4.309,6.3,4.304,6.462,4.333,6.5,4.332,6.555,4.341,6.58,4.476,6.602,4.455,6.617,4.376,6.633,4.34,6.715,4.342,6.757,4.344,6.86,4.373,6.793,4.469,6.792,4.593,6.786,4.652,6.768,4.725,6.788,4.725,6.825,4.645,6.839,4.523,6.868,4.441,6.923,4.391,7.013,4.397,7.155,4.514,7.164,4.616,7.087,4.686,7.077,4.716,7.144,4.684,7.207,4.612,7.284,4.548,7.46,4.555,7.509,4.595,7.517,4.645,7.531,4.655,7.566,4.561,7.644,4.525,7.801,4.522,8.029,4.555,8.293,4.558,8.328,4.656,8.234,4.907,8.253,4.924,8.342,4.825,8.394,4.814,8.431,4.746,8.515,4.725,8.544,4.758,8.556,4.755,8.571,4.752,8.533,4.606,8.54,4.572,8.574,4.526,8.656,4.516,8.69,4.55,8.66,4.671,8.708,4.646,8.762,4.58,8.807,4.573,8.856,4.579,8.889,4.573,8.918,4.554,8.903,4.435,8.914,4.358,8.932,4.29,8.977,4.23,9,4.092,9.114,4.041,9.249,3.998,9.297,3.973,9.311,3.94,9.362,3.926,9.425,3.922,9.462,3.943,9.501,4.001,9.484,4.066,9.512,4.061,9.551,4.028,9.6,4.027,9.67,4.077,9.689,4.056,9.649,4.008,9.64,3.965,9.736,3.88,9.74,3.853,9.628,3.87,9.593,3.814,9.556,3.798,9.616,3.696,9.642,3.612,9.766,3.624,9.672,3.538,9.876,3.31,9.915,3.24,9.948,3.079,9.885,2.917,9.868,2.735,9.822,2.539,9.801,2.304,9.78,2.068,9.807,1.927,9.719,1.789,9.648,1.618,9.632,1.566,9.584,1.54,9.494,1.435,9.434,1.296,9.386,1.139,9.445,1.121,9.51,1.115,9.599,1.054,9.591,1.032,9.575,0.991,9.626,0.779,9.625,0.632,9.618,0.577,9.601,0.568,9.557,0.594,9.539,0.659,9.495,0.665,9.33,0.611,9.325,0.552,9.399,0.487,9.47,0.362,9.546,0.296,9.777,0.192,9.944,0.22,10.001,0.195,9.813,0.126,9.797,0.044,9.738,0.085,9.574,0.149,9.468,0.16,9.411,0.2,9.386,0.246,9.376,0.307,9.355,0.344,9.302,0.289,9.325,0.116,9.339,-0.058,9.297,-0.351,9.137,-0.573,9.082,-0.624,9.038,-0.637,8.995,-0.635,8.946,-0.689,8.821,-0.708,8.757,-0.615,8.703,-0.591,8.844,-0.914,8.877,-0.946,8.909,-1.025,8.942,-1.071,9.065,-1.298,9.204,-1.382,9.26,-1.374,9.297,-1.361,9.318,-1.333,9.347,-1.325,9.28,-1.482,9.296,-1.515,9.331,-1.535,9.397,-1.53,9.448,-1.509,9.501,-1.555,9.523,-1.598,9.406,-1.635,9.357,-1.638,9.319,-1.632,9.036,-1.309,9.053,-1.379,9.158,-1.528,9.258,-1.726,9.248,-1.779,9.266,-1.825,9.342,-1.829,9.483,-1.895,9.495,-1.935,9.483,-1.962,9.342,-1.894,9.299,-1.903,9.371,-1.975,9.402,-2.028,9.533,-2.164,9.574,-2.23,9.591,-2.293,9.625,-2.367,9.676,-2.416,9.769,-2.413,9.861,-2.443,9.959,-2.49,10.062,-2.55,10.034,-2.576,10.002,-2.588,9.764,-2.474,9.722,-2.468,9.759,-2.519,10.006,-2.748,10.348,-3.013,10.585,-3.278,10.641,-3.398,10.849,-3.561,10.947,-3.662,11.032,-3.826,11.13,-3.916,11.364,-4.131,11.394,-4.2,11.668,-4.434,11.778,-4.566,11.781,-4.677,11.801,-4.705,11.821,-4.755,11.893,-4.866,11.967,-4.954,12.003,-4.982,12.018,-5.004,12.04,-5.035,12.111,-5.197,12.177,-5.325,12.207,-5.468,12.18,-5.539,12.155,-5.633,12.199,-5.732,12.214,-5.759,12.24,-5.807,12.315,-5.895,12.412,-5.986,12.453,-6,12.515,-6.004,12.681,-5.961,12.792,-5.878,12.861,-5.854,13.003,-5.836,13.068,-5.865,13.01,-5.908,12.791,-6.004,12.554,-6.046,12.38,-6.084,12.303,-6.093,12.283,-6.124,12.334,-6.187,12.402,-6.353,12.521,-6.59,12.823,-6.955,12.862,-7.232,13.091,-7.78,13.379,-8.37,13.366,-8.469,13.368,-8.555,13.378,-8.625,13.359,-8.687,13.054,-9.007,13.047,-8.975,13.077,-8.934,13.093,-8.9,13.047,-8.922,12.999,-8.991,12.999,-9.048,13.076,-9.23,13.156,-9.39,13.197,-9.551,13.209,-9.703,13.288,-9.827,13.332,-9.999,13.495,-10.257,13.539,-10.421,13.633,-10.512,13.721,-10.634,13.739,-10.757,13.834,-10.93,13.847,-11.054,13.784,-11.488,13.785,-11.813,13.686,-12.124,13.598,-12.286,13.417,-12.52,13.163,-12.652,12.983,-12.776,12.898,-13.028,12.55,-13.438,12.504,-13.755,12.379,-14.039,12.28,-14.637,12.073,-15.248,12.016,-15.514,11.968,-15.634,11.9,-15.72,11.85,-15.768,11.751,-15.832,11.769,-15.915,11.797,-15.986,11.82,-16.504,11.819,-16.704,11.78,-16.871,11.743,-17.249,11.722,-17.467,11.733,-17.751,11.776,-18.002,11.951,-18.271,12.041,-18.471,12.096,-18.541,12.329,-18.751,12.458,-18.927,13.042,-20.028,13.168,-20.185,13.284,-20.524,13.451,-20.917,13.839,-21.473,13.888,-21.607,13.973,-21.768,14.322,-22.19,14.463,-22.449,14.526,-22.703,14.52,-22.805,14.496,-22.921,14.459,-22.908,14.438,-22.881,14.403,-22.968,14.424,-23.079,14.474,-23.281,14.472,-23.477,14.497,-23.643,14.483,-24.05,14.502,-24.202,14.628,-24.548,14.768,-24.788,14.837,-25.033,14.819,-25.246,14.823,-25.359,14.864,-25.534,14.845,-25.726,14.931,-25.958,14.968,-26.318,15.097,-26.426,15.139,-26.508,15.163,-26.6,15.124,-26.668,15.133,-26.788,15.216,-26.995,15.288,-27.275,15.342,-27.387,15.719,-27.966,15.891,-28.153,16.007,-28.232,16.335,-28.537,16.448,-28.618,16.481,-28.642,16.739,-29.009,16.95,-29.403,17.189,-30.1,17.347,-30.445,17.677,-31.019,17.939,-31.383,18.164,-31.655,18.211,-31.742,18.311,-32.122,18.33,-32.27,18.325,-32.505,18.251,-32.652,18.125,-32.749,18.037,-32.775,17.965,-32.709,17.895,-32.75,17.851,-32.827,17.878,-32.962,17.958,-33.046,17.993,-33.152,18.075,-33.207,18.156,-33.359,18.261,-33.422,18.309,-33.514,18.433,-33.717,18.456,-33.796,18.465,-33.888,18.354,-33.939,18.333,-34.074,18.352,-34.188,18.41,-34.296,18.462,-34.347,18.462,-34.168,18.5,-34.109,18.534,-34.086,18.605,-34.077,18.709,-34.072,18.752,-34.083,18.809,-34.108,18.826,-34.188,18.831,-34.254,18.825,-34.296,18.831,-34.364,18.902,-34.361,18.952,-34.344,19.098,-34.35,19.149,-34.417,19.245,-34.412,19.279,-34.437,19.331,-34.492,19.323,-34.571,19.298,-34.615,19.392,-34.606,19.635,-34.753,19.85,-34.757,19.926,-34.775,20.021,-34.786,20.435,-34.509,20.53,-34.463,20.775,-34.44,20.882,-34.387,20.99,-34.367,21.06,-34.365,21.249,-34.407,21.35,-34.408,21.553,-34.373,21.789,-34.373,22.246,-34.069,22.414,-34.054,22.554,-34.01,22.736,-34.01,22.926,-34.063,23.268,-34.081,23.35,-34.069,23.586,-33.985,23.698,-33.993,24.183,-34.062,24.596,-34.175,24.827,-34.169,24.906,-34.06,25.003,-33.974,25.17,-33.961,25.477,-34.028,25.574,-34.035,25.638,-34.011,25.652,-33.85,25.806,-33.737,25.99,-33.711,26.429,-33.76,26.614,-33.707,27.077,-33.521,27.364,-33.361,27.762,-33.096,27.861,-33.054,28.214,-32.769,28.449,-32.625,28.856,-32.294,29.128,-32.003,29.483,-31.675,29.735,-31.47,29.83,-31.424,29.971,-31.322,30.289,-30.97,30.472,-30.715,30.664,-30.434,30.878,-30.071,31.023,-29.901,31.17,-29.591,31.335,-29.378,31.778,-28.937,31.892,-28.912,31.955,-28.884,32.027,-28.84,32.286,-28.621,32.375,-28.498,32.535,-28.2,32.657,-27.607,32.706,-27.442,32.849,-27.08,32.886,-26.849,32.889,-26.83,32.934,-26.252,32.955,-26.084,32.916,-26.087,32.894,-26.13,32.849,-26.268,32.804,-26.241,32.77,-26.203,32.704,-26.158,32.647,-26.092,32.59,-26.004,32.656,-25.902,32.723,-25.821,32.792,-25.644,32.961,-25.49,33.347,-25.261,33.53,-25.189,33.836,-25.068,34.607,-24.821,34.992,-24.651,35.156,-24.541,35.255,-24.43,35.438,-24.171,35.49,-24.066,35.542,-23.824,35.522,-23.785,35.485,-23.784,35.462,-23.851,35.399,-23.838,35.37,-23.798,35.377,-23.708,35.494,-23.185,35.575,-22.963,35.506,-22.772,35.49,-22.658,35.542,-22.377,35.54,-22.303,35.53,-22.248,35.505,-22.19,35.494,-22.125,35.456,-22.116,35.419,-22.178,35.401,-22.316,35.408,-22.403,35.383,-22.455,35.316,-22.397,35.326,-22.26,35.329,-22.037,35.273,-21.762,35.268,-21.651,35.128,-21.395,35.118,-21.195,34.982,-20.806,34.877,-20.671,34.765,-20.562,34.705,-20.473,34.698,-20.404,34.75,-20.091,34.745,-19.929,34.756,-19.822,34.713,-19.767,34.649,-19.701,34.721,-19.71,34.852,-19.821,34.891,-19.822,34.948,-19.813,35.365,-19.494,35.651,-19.164,35.854,-18.993,35.98,-18.912,36.125,-18.842,36.183,-18.871,36.236,-18.861,36.263,-18.72,36.327,-18.793,36.404,-18.77,36.412,-18.693,36.498,-18.576,36.54,-18.518,36.756,-18.307,36.9,-18.129,36.919,-18.08,36.939,-17.993,37,-17.935,37.051,-17.909,37.245,-17.74,37.512,-17.571,37.839,-17.393,38.048,-17.321,38.087,-17.276,38.145,-17.243,38.381,-17.17,38.633,-17.078,38.67,-17.05,38.713,-17.046,38.758,-17.055,38.885,-17.042,38.956,-17.005,39.084,-16.973,39.182,-16.842,39.242,-16.793,39.625,-16.579,39.765,-16.468,39.845,-16.436,39.791,-16.295,39.86,-16.252,39.984,-16.225,40.099,-16.065,40.109,-16.025,40.109,-15.979,40.208,-15.867,40.314,-15.764,40.559,-15.473,40.651,-15.261,40.653,-15.193,40.618,-15.116,40.642,-15.082,40.694,-15.065,40.687,-15.012,40.701,-14.93,40.776,-14.842,40.835,-14.792,40.845,-14.719,40.821,-14.635,40.827,-14.569,40.812,-14.536,40.818,-14.468,40.775,-14.421,40.727,-14.421,40.646,-14.539,40.636,-14.452,40.64,-14.39,40.713,-14.291,40.716,-14.214,40.65,-14.199,40.603,-14.167,40.596,-14.123,40.591,-13.845,40.56,-13.62,40.558,-13.531,40.545,-13.463,40.583,-13.374,40.552,-13.294,40.57,-13.223,40.564,-13.115,40.573,-13.058,40.569,-12.985,40.437,-12.983,40.435,-12.936,40.448,-12.905,40.553,-12.825,40.572,-12.758,40.581,-12.636,40.548,-12.527,40.487,-12.492,40.523,-12.393,40.509,-12.313,40.501,-12.119,40.532,-12.005,40.51,-11.94,40.494,-11.844,40.433,-11.657,40.465,-11.449,40.403,-11.332,40.421,-11.266,40.491,-11.179,40.545,-11.066,40.527,-11.025,40.506,-10.998,40.516,-10.93,40.597,-10.831,40.487,-10.765,40.555,-10.716,40.612,-10.662,40.517,-10.567,40.464,-10.464,40.453,-10.443,40.436,-10.41,40.389,-10.354,40.216,-10.241,40.138,-10.203,40.084,-10.157,39.984,-10.16,39.945,-10.092,39.864,-10.022,39.725,-10,39.784,-9.915,39.775,-9.837,39.728,-9.725,39.697,-9.578,39.625,-9.409,39.641,-9.192,39.451,-8.943,39.48,-8.906,39.488,-8.862,39.377,-8.721,39.304,-8.444,39.309,-8.351,39.34,-8.243,39.441,-8.012,39.428,-7.813,39.33,-7.747,39.287,-7.788,39.288,-7.518,39.353,-7.341,39.433,-7.207,39.519,-7.124,39.546,-7.024,39.472,-6.879,39.287,-6.815,39.228,-6.685,39.125,-6.556,39.067,-6.499,38.981,-6.455,38.874,-6.331,38.855,-6.205,38.805,-6.07,38.819,-5.878,38.911,-5.626,38.978,-5.519,39.058,-5.232,39.088,-5.165,39.119,-5.065,39.123,-4.98,39.202,-4.776,39.222,-4.692,39.228,-4.666,39.288,-4.609,39.377,-4.625,39.491,-4.478,39.637,-4.153,39.658,-4.119,39.687,-4.068,39.732,-3.993,39.746,-3.955,39.761,-3.913,39.819,-3.786,39.861,-3.577,39.896,-3.536,39.937,-3.442,39.992,-3.351,40.115,-3.251,40.128,-3.173,40.195,-3.019,40.18,-2.819,40.222,-2.688,40.279,-2.629,40.404,-2.556,40.644,-2.539,40.813,-2.392,40.82,-2.336,40.898,-2.27,40.922,-2.194,40.906,-2.137,40.89,-2.024,40.917,-2.042,40.952,-2.056,40.971,-1.992,40.996,-1.951,41.059,-1.975,41.107,-1.982,41.267,-1.945,41.387,-1.867,41.533,-1.695,41.632,-1.579,41.732,-1.43,41.846,-1.203,41.888,-1.151,41.926,-1.056,41.98,-0.973,42.106,-0.856,42.219,-0.738,42.399,-0.51,42.466,-0.457,42.561,-0.321,42.634,-0.251,42.712,-0.176,43.468,0.622,43.718,0.858,44.033,1.106,44.333,1.391,44.92,1.81,45.826,2.31,46.051,2.475,46.879,3.286,47.511,3.968,47.975,4.497,48.234,4.953,48.649,5.494,49.049,6.174,49.093,6.408,49.235,6.777,49.349,6.991,49.57,7.297,49.671,7.47,49.761,7.66,49.852,7.963,50.103,8.2,50.286,8.509,50.321,8.62,50.43,8.845,50.638,9.109,50.685,9.241,50.825,9.428,50.833,9.71,50.874,9.924,50.898,10.253,50.93,10.336,51.036,10.385,51.209,10.431,51.268,10.403,51.385,10.387,51.39,10.423,51.369,10.475,51.296,10.499,51.193,10.555,51.186,10.53,51.188,10.48,51.063,10.434,51.032,10.445,51.051,10.472,51.094,10.489,51.105,10.536,51.131,10.596,51.141,10.657,51.122,11.077,51.084,11.336,51.136,11.505,51.218,11.658,51.232,11.745,51.255,11.831,51.191,11.842,50.792,11.984,50.636,11.944,50.528,11.823,50.466,11.728,50.11,11.529,49.642,11.451,49.388,11.343,49.062,11.271,48.939,11.258,48.903,11.255,48.674,11.323,48.573,11.321,48.439,11.29,48.019,11.139,47.713,11.112,47.474,11.175,47.405,11.174,47.23,11.1,46.973,10.925,46.565,10.746,46.46,10.734,46.254,10.781,46.025,10.794,45.817,10.836,45.696,10.804,45.338,10.65,44.943,10.437,44.387,10.43,44.279,10.472,44.158,10.551,43.853,10.784,43.631,11.035,43.441,11.346,43.246,11.5,43.162,11.566,43.043,11.588,42.912,11.587,42.79,11.562,42.653,11.51,42.584,11.497,42.54,11.504,42.522,11.572,42.64,11.56,42.799,11.739,43.048,11.829,43.272,11.97,43.337,12.027,43.38,12.091,43.41,12.19,43.354,12.367,43.299,12.464,43.131,12.66,43.117,12.709,43.083,12.825,42.999,12.9,42.97,12.808,42.796,12.864,42.734,13.019,42.523,13.221,42.399,13.213,42.346,13.398,42.245,13.588,41.658,13.983,41.48,14.244,41.176,14.62,40.799,14.743,40.634,14.883,40.546,14.934,40.437,14.964,40.305,14.974,40.204,15.014,40.084,15.152,40.058,15.217,40.041,15.335,39.978,15.393,39.864,15.47,39.813,15.414,39.79,15.319,39.816,15.245,39.819,15.201,39.786,15.125,39.721,15.214,39.631,15.453,39.579,15.523,39.507,15.532,39.422,15.787,39.299,15.921,39.223,16.194,39.143,16.729,39.034,17.086,38.912,17.427,38.609,18.005,38.574,18.073,38.333,18.219,38.283,18.287,38.252,18.264,38.202,18.249,38.128,18.333,38.074,18.41,37.922,18.556,37.73,18.694,37.599,18.717,37.532,18.753,37.471,18.82,37.362,19.092,37.248,19.582,37.263,19.792,37.193,20.121,37.188,20.395,37.228,20.557,37.173,20.732,37.157,20.895,37.141,20.982,37.151,21.104,37.217,21.078,37.257,21.039,37.263,21.073,37.259,21.109,37.212,21.186,37.081,21.326,36.927,21.587,36.883,21.769,36.871,21.997,36.87,22.016,36.83,22.098,36.415,22.394,36.23,22.629,35.913,22.74,35.846,22.786,35.797,22.849,35.698,22.946,35.564,23.271,35.523,23.443,35.504,23.779,35.515,23.843,35.541,23.921,35.594,23.943,35.632,23.95,35.784,23.938,35.625,24.066,35.478,24.155,35.397,24.27,35.194,24.475,34.853,25.14,34.679,25.443,34.565,25.691,34.329,26.024,34.05,26.551,33.959,26.649,33.893,27.049,33.849,27.185,33.802,27.268,33.697,27.341,33.657,27.431,33.55,27.607,33.559,27.701,33.547,27.898,33.495,27.974,33.372,28.051,33.202,28.208,33.023,28.442,32.898,28.565,32.857,28.631,32.829,28.703,32.784,28.787,32.659,28.928,32.632,28.992,32.638,29.182,32.599,29.322,32.565,29.386,32.397,29.534,32.36,29.631,32.409,29.749,32.489,29.852,32.473,29.925,32.566,29.974,32.647,29.798,32.721,29.522,32.767,29.45,32.812,29.4,32.871,29.286,33.076,29.073,33.13,28.978,33.204,28.778,33.202,28.696,33.248,28.568,33.416,28.39,33.594,28.256,33.76,28.048,34.045,27.829,34.22,27.764,34.319,27.889,34.4,28.016,34.427,28.106,34.446,28.357,34.617,28.758,34.736,29.271,34.849,29.432,34.904,29.477,34.973,29.555,34.982,29.484,34.951,29.354,34.799,28.721,34.78,28.507,34.683,28.264,34.616,28.148,34.625,28.065,34.722,28.131,34.828,28.109,35.078,28.087,35.18,28.035,35.424,27.734,35.581,27.432,35.763,27.259,35.852,27.07,36.032,26.881,36.094,26.766,36.25,26.595,36.519,26.105,36.675,26.039,36.703,25.903,36.763,25.751,36.86,25.692,36.921,25.641,37.149,25.291,37.218,25.151,37.243,25.073,37.266,24.96,37.22,24.873,37.181,24.82,37.338,24.616,37.431,24.459,37.543,24.292,37.638,24.278,37.713,24.274,37.821,24.188,37.92,24.185,37.978,24.125,38.099,24.058,38.289,23.911,38.464,23.712,38.542,23.558,38.706,23.306,38.757,23.194,38.797,23.049,38.836,22.989,38.941,22.882,38.883,22.882,38.939,22.805,39.007,22.77,39.001,22.699,39.062,22.592,39.096,22.393,39.07,22.294,39.034,22.203,39.021,22.033,38.988,21.882,39.03,21.776,39.091,21.664,39.147,21.519,39.151,21.433,39.094,21.31,39.276,20.974,39.491,20.737,39.614,20.518,39.728,20.39,39.884,20.293,40.081,20.266,40.482,19.993,40.616,19.822,40.759,19.755,40.777,19.717,40.792,19.646,40.848,19.555,40.913,19.49,41.116,19.082,41.144,18.989,41.191,18.871,41.221,18.765,41.229,18.678,41.432,18.452,41.508,18.256,41.658,18.008,41.75,17.886,42.052,17.669,42.294,17.435,42.332,17.257,42.383,17.122,42.475,17.05,42.544,16.96,42.553,16.868,42.647,16.801,42.699,16.737,42.726,16.653,42.731,16.57,42.79,16.452,42.799,16.372,42.84,16.032,42.717,15.655,42.8,15.372,42.799,15.326,42.788,15.266,42.736,15.294,42.698,15.326,42.658,15.233,42.856,15.133,42.897,15.006,42.936,14.939,42.937,14.898,42.913,14.863,42.922,14.817,42.947,14.773,43.021,14.555,43.019,14.521,43.006,14.483,43.045,14.342,43.093,14.204,43.089,14.011,43.234,13.859,43.282,13.693,43.283,13.64,43.232,13.267,43.475,12.839,43.488,12.699,43.634,12.744,43.835,12.674,43.93,12.617,44.006,12.608,44.112,12.639,44.26,12.645,44.358,12.669,44.618,12.817,44.755,12.764,44.89,12.784,45.039,12.816,45.11,12.939,45.164,12.998,45.394,13.067,45.534,13.233,45.657,13.339,45.92,13.394,46.203,13.424,46.502,13.416,46.663,13.433,46.789,13.466,46.976,13.547,47.243,13.609,47.408,13.662,47.633,13.858,47.855,13.957,47.916,14.013,47.99,14.048,48.278,13.998,48.449,14.006,48.594,14.046,48.668,14.05,48.78,14.124,48.929,14.267,49.005,14.355,49.048,14.456,49.103,14.5,49.35,14.638,49.549,14.722,49.906,14.828,50.167,14.851,50.339,14.927,50.527,15.038,51.015,15.141,51.322,15.226,51.604,15.337,51.682,15.379,51.749,15.44,51.831,15.459,51.966,15.536,52.087,15.586,52.217,15.656,52.222,15.761,52.174,15.957,52.237,16.171,52.328,16.294,52.448,16.391,52.581,16.47,53.086,16.648,53.298,16.723,53.61,16.76,53.775,16.856,53.954,16.918,54.068,17.006,54.377,17.034,54.567,17.031,54.665,17.009,54.772,16.965,55.064,17.039,55.174,17.158,55.275,17.321,55.296,17.382,55.281,17.446,55.238,17.505,55.255,17.586,55.479,17.843,55.614,17.886,55.998,17.935,56.27,17.951,56.383,17.988,56.551,18.166,56.655,18.587,56.826,18.754,56.957,18.828,57.177,18.903,57.428,18.944,57.676,18.958,57.738,18.977,57.812,19.017,57.79,19.146,57.764,19.253,57.761,19.432,57.715,19.607,57.714,19.678,57.741,19.804,57.802,19.955,57.844,20.118,57.862,20.244,57.947,20.344,58.103,20.57,58.169,20.59,58.245,20.599,58.232,20.507,58.209,20.424,58.266,20.395,58.349,20.387,58.474,20.407,58.534,20.504,58.69,20.807,58.896,21.113,59.069,21.289,59.304,21.435,59.371,21.499,59.518,21.782,59.653,21.951,59.681,22.054,59.8,22.22,59.824,22.305,59.838,22.421,59.823,22.509,59.696,22.546,59.535,22.579,59.429,22.661,59.311,22.793,59.195,22.972,59.03,23.131,58.983,23.235,58.912,23.334,58.83,23.397,58.773,23.517,58.578,23.643,58.5,23.646,58.393,23.618,58.325,23.624,58.12,23.717,57.825,23.759,57.611,23.804,57.22,23.923,57.123,23.981,56.913,24.15,56.774,24.335,56.641,24.47,56.49,24.716,56.388,24.979,56.373,25.018,56.363,25.569,56.298,25.651,56.307,25.709,56.329,25.752,56.374,25.805,56.416,26.109,56.418,26.208,56.43,26.327,56.413,26.351,56.379,26.356,56.346,26.314,56.306,26.235,56.228,26.22,56.197,26.229,56.164,26.207,56.08,26.063,56.075,26.053,56.025,25.916,55.941,25.794,55.523,25.498,55.433,25.394,55.322,25.3,55.304,25.237,55.098,25.042,54.747,24.81,54.659,24.716,54.624,24.621,54.58,24.564,54.535,24.531,54.499,24.463,54.458,24.358,54.397,24.278,54.304,24.254,54.148,24.171,53.893,24.077,53.802,24.069,53.33,24.098,53.026,24.147,52.648,24.155,52.511,24.113,52.251,23.995,52.119,23.971,51.906,23.985,51.843,24.011,51.792,24.075,51.768,24.254,51.735,24.263,51.665,24.25,51.623,24.301,51.605,24.338,51.568,24.286,51.535,24.286,51.477,24.308,51.395,24.319,51.31,24.34,51.37,24.477,51.418,24.531,51.411,24.571,51.338,24.564,51.268,24.607,51.396,24.645,51.428,24.668,51.533,24.891,51.587,24.965,51.609,25.053,51.602,25.148,51.561,25.284,51.52,25.39,51.51,25.452,51.485,25.525,51.527,25.682,51.572,25.781,51.543,25.902,51.389,26.011,51.262,26.153,51.108,26.081,51.003,25.981,50.904,25.724,50.869,25.613,50.803,25.497,50.763,25.445,50.755,25.399,50.777,25.177,50.847,24.889,50.836,24.85,50.804,24.789,50.726,24.869,50.667,24.964,50.558,25.087,50.508,25.307,50.455,25.425,50.281,25.566,50.239,25.623,50.19,25.756,50.13,25.847,50.081,25.961,50.032,26.111,50.054,26.123,50.096,26.119,50.135,26.101,50.155,26.101,50.214,26.308,50.185,26.405,50.111,26.456,50.027,26.527,50.011,26.609,50.008,26.679,50.026,26.699,50.087,26.676,50.135,26.66,50.15,26.663,49.986,26.829,49.717,26.956,49.538,27.152,49.405,27.181,49.282,27.31,49.175,27.438,49.238,27.493,49.158,27.528,49.087,27.549,48.906,27.629,48.797,27.724,48.807,27.765,48.833,27.801,48.809,27.896,48.774,27.959,48.626,28.133,48.523,28.355,48.499,28.449,48.442,28.543,48.39,28.632,48.371,28.692,48.339,28.763,48.253,28.901,48.184,28.979,48.1,29.211,48.086,29.275,48.051,29.355,47.998,29.386,47.935,29.367,47.845,29.366,47.723,29.393,47.725,29.417,47.817,29.487,47.97,29.617,48.048,29.598,48.089,29.579,48.143,29.572,48.136,29.618,48.077,29.716,48.006,29.836,47.974,29.946,47.979,29.983,47.983,30.011,48.073,30.043,48.142,30.041,48.355,29.957,48.454,29.938,48.546,29.962,48.596,29.975,48.671,30.028,48.832,30.035,48.87,30.062,48.919,30.121,48.909,30.241,48.891,30.328,48.917,30.397,49.037,30.45,49.002,30.507,49.13,30.509,49.225,30.472,49.247,30.413,49.19,30.375,49.096,30.407,49.049,30.397,49.002,30.374,49.028,30.333,49.054,30.307,49.43,30.13,49.555,30.029,49.983,30.209,50.072,30.199,50.129,30.048,50.169,29.921,50.23,29.873,50.387,29.679,50.544,29.548,50.65,29.42,50.668,29.34,50.646,29.212,50.675,29.147,50.796,29.117,50.876,29.063,50.876,29.004,50.843,28.928,50.867,28.87,51.021,28.782,51.062,28.726,51.094,28.512,51.128,28.435,51.276,28.219,51.279,28.131,51.519,27.91,51.589,27.864,51.666,27.845,51.842,27.848,52.031,27.824,52.192,27.717,52.476,27.617,52.603,27.493,52.638,27.392,52.692,27.323,52.983,27.142,53.342,27.004,53.455,26.943,53.507,26.852,53.706,26.726,53.823,26.708,54.069,26.732,54.247,26.697,54.522,26.589,54.645,26.509,54.759,26.505,54.896,26.557,55.155,26.725,55.294,26.786,55.424,26.771,55.519,26.83,55.592,26.932,55.65,26.978,55.941,27.038,56.118,27.143,56.284,27.191,56.356,27.2,56.728,27.128,56.813,27.09,56.91,26.995,56.982,26.905,57.036,26.801,57.072,26.68,57.104,26.371,57.201,26.159,57.206,26.037,57.261,25.919,57.335,25.792,57.733,25.725,57.796,25.653,57.937,25.692,58.022,25.641,58.203,25.592,58.314,25.581,58.531,25.592,58.798,25.555,59.046,25.417,59.227,25.428,59.456,25.481,59.616,25.403,59.818,25.401,59.897,25.362,60.025,25.384,60.4,25.312,60.511,25.437,60.588,25.414,60.615,25.33,60.664,25.282,61.109,25.184,61.243,25.142,61.412,25.102,61.49,25.154,61.533,25.196,61.744,25.138,61.908,25.131,62.089,25.155,62.152,25.207,62.199,25.225,62.249,25.197,62.315,25.135,62.391,25.153,62.445,25.197,62.572,25.255,62.665,25.265,63.015,25.225,63.17,25.255,63.286,25.228,63.491,25.211,63.496,25.298,63.557,25.353,63.721,25.386,63.936,25.343,63.987,25.351,64.059,25.403,64.125,25.374,64.152,25.333,64.544,25.237,64.594,25.206,64.659,25.184,64.777,25.307,65.061,25.311,65.406,25.374,65.68,25.355,65.884,25.42,66.235,25.464,66.328,25.466,66.403,25.447,66.468,25.445,66.407,25.485,66.356,25.507,66.131,25.493,66.162,25.554,66.219,25.59,66.324,25.602,66.429,25.575,66.534,25.484,66.57,25.379,66.699,25.226,66.71,25.111,66.682,24.929,66.703,24.861,67.101,24.792,67.171,24.756,67.289,24.368,67.304,24.263,67.309,24.175,67.365,24.092,67.428,24.065,67.454,24.04,67.477,24.018,67.504,23.94,67.563,23.882,67.646,23.92,67.65,23.867,67.668,23.811,67.819,23.828,67.86,23.903,67.951,23.829,68.001,23.826,68.037,23.848,68.068,23.818,68.116,23.753,68.149,23.797,68.165,23.857,68.192,23.729,68.235,23.597,68.343,23.617,68.425,23.706,68.497,23.748,68.642,23.808,68.777,23.852,68.627,23.754,68.454,23.629,68.417,23.571,68.529,23.364,68.641,23.19,68.817,23.054,69.236,22.849,69.665,22.759,69.74,22.775,69.85,22.856,70.118,22.947,70.192,22.966,70.251,22.971,70.339,22.94,70.368,22.973,70.396,23.03,70.435,23.077,70.489,23.09,70.509,23.04,70.513,23.002,70.44,22.97,70.328,22.816,70.177,22.573,70.084,22.554,70.006,22.548,69.819,22.452,69.728,22.465,69.655,22.404,69.549,22.408,69.277,22.285,69.239,22.3,69.194,22.336,69.131,22.416,69.052,22.437,68.983,22.385,68.97,22.29,69.009,22.197,69.192,21.992,69.385,21.84,69.542,21.679,69.748,21.506,70.034,21.179,70.127,21.095,70.485,20.84,70.719,20.74,70.88,20.715,71.025,20.739,71.396,20.87,71.571,20.971,72.015,21.156,72.077,21.224,72.254,21.531,72.257,21.661,72.21,21.728,72.171,21.774,72.103,21.795,72.037,21.823,72.076,21.863,72.094,21.92,72.162,21.985,72.244,22.028,72.274,22.09,72.306,22.189,72.243,22.245,72.183,22.27,72.333,22.27,72.456,22.248,72.59,22.278,72.702,22.264,72.809,22.233,72.709,22.207,72.628,22.2,72.553,22.16,72.522,21.976,72.617,21.962,72.7,21.972,72.644,21.938,72.592,21.878,72.543,21.697,72.84,21.687,72.979,21.705,73.113,21.75,73.022,21.7,72.811,21.62,72.718,21.551,72.613,21.462,72.668,21.456,72.735,21.471,72.687,21.436,72.624,21.372,72.692,21.178,72.752,21.129,72.814,21.117,72.824,21.084,72.841,20.952,72.879,20.829,72.894,20.673,72.881,20.563,72.709,20.078,72.668,19.831,72.676,19.798,72.697,19.757,72.727,19.578,72.799,19.52,72.756,19.451,72.764,19.413,72.788,19.363,72.987,19.277,72.812,19.299,72.795,19.252,72.803,19.219,72.803,19.079,72.835,18.976,72.901,19.015,72.972,19.153,73.006,19.021,72.977,18.927,72.899,18.779,72.871,18.683,72.875,18.643,72.917,18.576,72.943,18.366,72.972,18.259,72.994,18.098,73.047,17.907,73.156,17.622,73.149,17.527,73.239,17.199,73.338,16.46,73.454,16.152,73.476,16.054,73.608,15.871,73.68,15.709,73.733,15.657,73.833,15.659,73.772,15.573,73.814,15.539,73.852,15.482,73.932,15.397,73.801,15.397,73.884,15.306,73.949,15.075,74.041,14.949,74.089,14.902,74.223,14.709,74.28,14.65,74.335,14.575,74.382,14.495,74.397,14.407,74.467,14.217,74.467,14.169,74.499,14.046,74.608,13.85,74.671,13.668,74.682,13.584,74.682,13.507,74.771,13.077,74.803,12.977,74.868,12.845,74.946,12.565,75.197,12.058,75.23,12.023,75.315,11.958,75.423,11.812,75.525,11.703,75.646,11.468,75.724,11.362,75.845,11.058,75.923,10.784,76.096,10.402,76.123,10.327,76.201,10.201,76.193,10.164,76.196,10.086,76.223,10.024,76.249,10.018,76.346,9.922,76.459,9.536,76.42,9.52,76.376,9.54,76.372,9.707,76.343,9.827,76.285,9.91,76.242,9.927,76.292,9.676,76.325,9.452,76.403,9.237,76.419,9.208,76.452,9.189,76.472,9.161,76.483,9.091,76.553,8.903,76.617,8.847,76.967,8.407,77.066,8.316,77.301,8.145,77.518,8.078,77.587,8.13,77.77,8.19,78.06,8.385,78.126,8.511,78.136,8.663,78.192,8.891,78.275,8.99,78.421,9.105,78.98,9.269,79.213,9.256,79.411,9.192,79.356,9.252,79.275,9.285,79.107,9.309,79.02,9.333,78.953,9.394,78.919,9.453,78.94,9.566,78.996,9.683,79.258,10.035,79.254,10.175,79.315,10.257,79.391,10.306,79.532,10.33,79.589,10.312,79.667,10.3,79.757,10.304,79.838,10.323,79.85,10.769,79.849,11.197,79.835,11.269,79.799,11.339,79.693,11.313,79.749,11.371,79.793,11.447,79.754,11.575,79.771,11.69,79.858,11.989,79.982,12.235,80.038,12.296,80.143,12.452,80.229,12.69,80.342,13.361,80.29,13.437,80.114,13.529,80.062,13.606,80.156,13.714,80.233,13.606,80.266,13.521,80.307,13.485,80.246,13.686,80.244,13.773,80.224,13.858,80.144,14.059,80.112,14.212,80.136,14.287,80.17,14.349,80.179,14.478,80.165,14.578,80.099,14.798,80.053,15.074,80.101,15.324,80.293,15.711,80.385,15.793,80.647,15.895,80.708,15.888,80.782,15.867,80.826,15.766,80.865,15.782,80.918,15.76,80.979,15.758,80.993,15.809,81.03,15.881,81.132,15.962,81.239,16.264,81.286,16.337,81.402,16.365,81.712,16.334,81.762,16.329,82.142,16.485,82.259,16.56,82.327,16.664,82.339,16.707,82.36,16.783,82.35,16.825,82.307,16.879,82.282,16.936,82.287,16.978,82.36,17.096,82.593,17.274,82.977,17.462,83.198,17.609,83.388,17.787,83.572,18.004,83.654,18.07,84.104,18.293,84.182,18.401,84.463,18.69,84.609,18.884,84.691,18.965,84.75,19.05,84.771,19.125,85.226,19.508,85.442,19.627,85.437,19.657,85.371,19.679,85.229,19.601,85.181,19.595,85.163,19.621,85.249,19.758,85.46,19.896,85.504,19.888,85.555,19.867,85.56,19.753,85.511,19.727,85.497,19.697,85.575,19.693,85.853,19.792,86.216,19.896,86.279,19.919,86.303,19.945,86.312,19.988,86.245,20.053,86.294,20.054,86.377,20.007,86.446,20.089,86.499,20.172,86.75,20.313,86.769,20.356,86.763,20.419,86.836,20.534,86.925,20.62,86.975,20.7,86.939,20.745,86.896,20.966,86.842,21.106,86.86,21.237,86.954,21.365,87.101,21.501,87.201,21.545,87.678,21.654,87.824,21.727,87.948,21.825,88.051,22.001,88.104,22.047,88.159,22.122,88.083,22.183,88.011,22.213,87.962,22.255,87.941,22.374,87.994,22.266,88.087,22.218,88.196,22.14,88.181,22.033,88.099,21.794,88.057,21.694,88.122,21.636,88.254,21.622,88.279,21.697,88.288,21.758,88.305,21.723,88.446,21.614,88.585,21.66,88.6,21.714,88.567,21.832,88.642,22.122,88.66,22.067,88.708,22.056,88.73,22.036,88.74,22.005,88.691,21.733,88.695,21.662,88.713,21.622,88.745,21.584,88.834,21.661,88.858,21.745,88.907,21.653,88.967,21.641,89.052,21.654,89.042,21.759,89.02,21.834,88.949,21.938,89.028,21.937,89.051,22.093,89.082,22.015,89.094,21.873,89.167,21.784,89.234,21.722,89.279,21.707,89.354,21.721,89.452,21.821,89.501,21.914,89.503,22.032,89.469,22.213,89.483,22.276,89.547,21.984,89.567,21.861,89.569,21.767,89.628,21.814,89.668,21.878,89.757,21.919,89.812,21.983,89.853,22.091,89.866,22.173,89.853,22.289,89.882,22.388,89.985,22.466,89.894,22.308,89.894,22.203,89.918,22.116,89.954,22.023,90.069,22.098,90.143,22.138,90.21,22.157,90.088,22.017,90.07,21.96,90.071,21.887,90.131,21.847,90.159,21.817,90.231,21.83,90.288,21.899,90.356,22.048,90.494,22.179,90.553,22.218,90.589,22.258,90.616,22.362,90.596,22.436,90.532,22.539,90.487,22.589,90.498,22.635,90.481,22.685,90.435,22.752,90.437,22.828,90.462,22.882,90.552,22.905,90.478,22.987,90.466,23.054,90.528,23.085,90.595,23.134,90.599,23.204,90.591,23.266,90.523,23.346,90.392,23.367,90.269,23.456,90.408,23.432,90.556,23.422,90.568,23.474,90.562,23.537,90.573,23.578,90.604,23.591,90.616,23.532,90.616,23.442,90.656,23.273,90.634,23.094,90.656,23.025,90.827,22.721,90.946,22.597,91.151,22.614,91.216,22.642,91.314,22.735,91.41,22.797,91.48,22.885,91.482,22.797,91.53,22.708,91.693,22.505,91.734,22.407,91.797,22.297,91.845,22.343,91.863,22.35,91.858,22.317,91.825,22.229,91.85,22.157,91.913,21.883,92.008,21.685,92.011,21.516,92.056,21.175,92.195,20.984,92.248,20.884,92.308,20.79,92.324,20.792,92.378,20.718,92.608,20.47,92.723,20.296,92.733,20.453,92.709,20.564,92.736,20.563,92.787,20.469,92.851,20.415,92.891,20.34,92.872,20.302,92.844,20.283,92.791,20.211,92.828,20.178,92.882,20.152,92.991,20.288,93.019,20.346,93.035,20.406,93.067,20.378,93.015,20.185,93.068,20.189,93.096,20.181,93.04,20.13,93.002,20.075,93.129,19.858,93.191,19.851,93.199,19.898,93.157,20.041,93.25,20.07,93.362,20.058,93.41,20.038,93.439,20.009,93.582,19.91,93.707,19.912,93.66,19.854,93.612,19.776,93.669,19.732,93.74,19.697,93.761,19.648,93.77,19.61,93.84,19.534,93.888,19.504,93.961,19.482,93.998,19.441,93.962,19.329,93.886,19.272,93.825,19.238,93.728,19.267,93.579,19.401,93.531,19.398,93.493,19.369,93.598,19.188,93.705,19.027,93.8,18.961,93.929,18.9,93.961,18.958,93.968,18.995,93.941,19.146,94.002,19.182,94.022,19.269,94.045,19.287,94.039,19.146,94.07,18.893,94.091,18.849,94.246,18.741,94.171,18.732,94.252,18.609,94.266,18.507,94.431,18.202,94.494,17.825,94.56,17.699,94.589,17.569,94.564,17.309,94.494,17.167,94.473,17.135,94.452,16.954,94.4,16.868,94.353,16.64,94.327,16.572,94.271,16.517,94.214,16.127,94.224,16.016,94.299,16.008,94.442,16.094,94.496,16.186,94.588,16.289,94.638,16.309,94.665,16.336,94.679,16.426,94.703,16.512,94.717,16.452,94.72,16.399,94.677,16.242,94.681,16.133,94.651,16.065,94.656,15.989,94.662,15.904,94.798,15.971,94.848,16.033,94.86,16.102,94.893,16.183,94.898,16.141,94.882,16.088,94.892,16.038,94.891,15.979,94.943,15.818,95.078,15.839,95.177,15.826,95.226,15.877,95.333,16.033,95.347,16.098,95.365,15.985,95.308,15.88,95.301,15.756,95.348,15.729,95.39,15.723,95.556,15.838,95.679,15.977,95.711,16.073,95.763,16.169,96.012,16.254,96.032,16.285,96.043,16.34,96.081,16.353,96.135,16.343,96.293,16.41,96.324,16.444,96.237,16.567,96.238,16.631,96.189,16.768,96.22,16.781,96.249,16.765,96.262,16.659,96.282,16.596,96.364,16.521,96.431,16.505,96.507,16.514,96.622,16.564,96.765,16.71,96.811,16.778,96.858,16.921,96.909,17.031,96.851,17.203,96.91,17.305,96.878,17.342,96.851,17.401,96.97,17.317,97.075,17.207,97.1,17.165,97.2,17.095,97.178,17.062,97.212,16.893,97.267,16.743,97.331,16.672,97.376,16.523,97.505,16.525,97.62,16.537,97.668,16.552,97.726,16.569,97.665,16.52,97.634,16.458,97.641,16.254,97.609,16.144,97.584,16.02,97.71,15.876,97.774,15.431,97.744,15.307,97.8,15.185,97.812,14.859,97.869,14.739,97.937,14.764,98.019,14.653,97.929,14.696,97.91,14.653,97.977,14.461,97.998,14.335,98.1,14.162,98.073,13.986,98.098,13.898,98.111,13.713,98.15,13.648,98.2,13.98,98.239,13.934,98.248,13.84,98.245,13.733,98.421,13.484,98.487,13.293,98.576,13.162,98.595,12.986,98.637,12.848,98.636,12.771,98.663,12.662,98.665,12.54,98.624,12.441,98.679,12.348,98.619,12.3,98.6,12.245,98.631,12.225,98.696,12.225,98.664,12.127,98.686,12.047,98.689,11.957,98.645,11.91,98.639,11.869,98.625,11.801,98.636,11.738,98.694,11.718,98.805,11.779,98.84,11.739,98.876,11.72,98.791,11.665,98.741,11.592,98.746,11.521,98.733,11.435,98.73,11.33,98.745,11.24,98.683,11.133,98.676,10.987,98.599,10.864,98.536,10.741,98.501,10.719,98.465,10.676,98.523,10.353,98.497,10.183,98.521,10.107,98.563,10.035,98.658,10.179,98.703,10.19,98.562,9.838,98.493,9.561,98.443,9.493,98.371,9.291,98.326,8.969,98.242,8.768,98.227,8.544,98.238,8.423,98.305,8.226,98.361,8.187,98.421,8.178,98.474,8.247,98.5,8.318,98.579,8.344,98.636,8.305,98.704,8.257,98.789,8.06,98.872,8.024,98.974,7.963,99.051,7.888,99.043,7.766,99.078,7.718,99.183,7.718,99.264,7.619,99.3,7.561,99.359,7.372,99.435,7.334,99.529,7.329,99.597,7.356,99.553,7.219,99.602,7.155,99.668,7.151,99.72,7.106,99.696,6.877,99.869,6.75,100.119,6.442,100.158,6.324,100.263,6.183,100.343,5.984,100.374,5.778,100.353,5.588,100.473,5.044,100.615,4.652,100.615,4.373,100.661,4.226,100.76,4.097,100.796,4.023,100.757,4.002,100.715,3.966,100.782,3.864,100.851,3.777,101.025,3.625,101.115,3.472,101.3,3.253,101.33,3.142,101.354,3.011,101.296,2.885,101.351,2.839,101.407,2.813,101.52,2.684,101.781,2.574,101.89,2.449,102.146,2.248,102.548,2.042,102.727,1.856,102.897,1.792,103.357,1.546,103.4,1.498,103.427,1.43,103.48,1.329,103.55,1.333,103.695,1.45,103.817,1.477,103.915,1.447,103.992,1.455,103.991,1.55,103.981,1.624,104.016,1.579,104.076,1.53,104.101,1.488,104.094,1.446,104.115,1.412,104.176,1.365,104.25,1.389,104.28,1.416,104.288,1.481,104.219,1.723,103.968,2.261,103.832,2.508,103.812,2.58,103.537,2.775,103.485,2.837,103.439,2.933,103.445,3.261,103.429,3.379,103.454,3.521,103.373,3.671,103.362,3.769,103.421,3.977,103.469,4.393,103.454,4.669,103.416,4.85,103.197,5.262,103.097,5.408,102.982,5.525,102.899,5.564,102.79,5.645,102.534,5.863,102.34,6.172,102.274,6.203,102.101,6.242,101.799,6.475,101.614,6.754,101.498,6.865,101.401,6.9,101.302,6.908,101.154,6.875,101.018,6.861,100.793,6.995,100.702,7.082,100.586,7.176,100.49,7.161,100.424,7.188,100.371,7.28,100.205,7.501,100.161,7.599,100.158,7.728,100.257,7.775,100.317,7.716,100.324,7.644,100.28,7.584,100.284,7.552,100.343,7.553,100.38,7.542,100.411,7.464,100.439,7.281,100.545,7.227,100.504,7.337,100.454,7.442,100.279,8.269,100.229,8.425,100.163,8.508,100.159,8.474,100.154,8.443,100.129,8.428,100.056,8.511,99.99,8.589,99.961,8.671,99.905,9.113,99.878,9.195,99.836,9.288,99.724,9.314,99.394,9.214,99.335,9.225,99.254,9.265,99.265,9.353,99.288,9.415,99.191,9.627,99.161,9.734,99.169,9.934,99.195,10.175,99.19,10.266,99.165,10.32,99.237,10.388,99.285,10.569,99.487,10.89,99.514,11.101,99.561,11.215,99.627,11.463,99.725,11.662,99.799,11.749,99.837,11.937,99.93,12.047,99.989,12.171,100.006,12.355,99.964,12.69,99.982,12.771,100.09,13.046,100.051,13.171,99.991,13.243,100.017,13.353,100.122,13.44,100.236,13.484,100.536,13.514,100.603,13.568,100.656,13.521,100.907,13.462,100.963,13.432,100.947,13.358,100.926,13.303,100.946,13.187,100.904,13.035,100.896,12.818,100.863,12.715,100.898,12.654,100.954,12.621,101.09,12.674,101.445,12.619,101.724,12.689,101.836,12.64,101.889,12.593,101.945,12.564,102.034,12.532,102.134,12.443,102.23,12.332,102.248,12.361,102.259,12.394,102.343,12.253,102.434,12.179,102.54,12.109,102.575,12.158,102.594,12.203,102.655,12.149,102.763,12.012,102.792,11.889,102.884,11.773,102.912,11.704,102.934,11.707,102.932,11.742,102.949,11.773,103.004,11.711,103.011,11.589,103.125,11.461,103.107,11.368,103.091,11.211,103.106,11.074,103.153,10.914,103.272,10.909,103.354,10.922,103.411,10.977,103.467,11.084,103.532,11.147,103.595,11.108,103.654,11.059,103.722,10.89,103.681,10.759,103.592,10.721,103.54,10.669,103.532,10.605,103.587,10.552,103.662,10.509,103.841,10.581,103.871,10.655,103.902,10.644,103.937,10.587,104.262,10.541,104.426,10.411,104.516,10.34,104.594,10.267,104.613,10.208,104.663,10.17,104.748,10.199,104.802,10.203,104.873,10.115,104.966,10.101,105.028,10.067,105.084,9.996,105.095,9.945,105.093,9.901,104.987,9.869,104.903,9.816,104.845,9.606,104.815,9.185,104.819,8.802,104.896,8.747,104.77,8.598,104.892,8.583,105.114,8.629,105.191,8.711,105.322,8.801,105.401,8.962,105.501,9.093,106.168,9.397,106.193,9.448,106.206,9.502,106.159,9.594,106.113,9.674,105.831,10.001,105.926,9.962,106.204,9.675,106.378,9.556,106.484,9.559,106.539,9.604,106.572,9.641,106.564,9.716,106.507,9.821,106.184,10.142,106.136,10.222,106.449,9.94,106.557,9.868,106.596,9.86,106.657,9.901,106.658,9.949,106.659,9.991,106.714,10.06,106.785,10.116,106.785,10.151,106.729,10.193,106.602,10.232,106.464,10.298,106.492,10.304,106.644,10.289,106.757,10.296,106.776,10.339,106.778,10.376,106.741,10.444,106.698,10.462,106.643,10.456,106.606,10.465,106.727,10.536,106.813,10.433,106.902,10.383,106.947,10.4,106.966,10.441,106.984,10.618,107.007,10.661,107.021,10.631,107.036,10.556,107.088,10.498,107.194,10.472,107.235,10.42,107.262,10.398,107.384,10.459,107.47,10.486,107.564,10.555,107.845,10.7,108.001,10.72,108.095,10.897,108.176,10.92,108.272,10.934,108.419,11.041,108.551,11.156,108.7,11.199,108.821,11.315,108.987,11.336,109.018,11.468,109.04,11.593,109.133,11.601,109.173,11.665,109.199,11.725,109.193,11.773,109.158,11.837,109.167,11.912,109.2,11.972,109.199,11.999,109.215,12.01,109.22,11.959,109.247,11.909,109.259,11.955,109.256,11.993,109.216,12.073,109.207,12.415,109.305,12.391,109.219,12.646,109.274,12.709,109.336,12.752,109.381,12.671,109.445,12.6,109.42,12.719,109.424,12.956,109.377,13.025,109.31,13.219,109.272,13.279,109.288,13.451,109.252,13.591,109.247,13.855,109.288,13.765,109.303,13.856,109.245,14.053,109.224,14.097,109.207,14.154,109.191,14.27,109.137,14.384,109.087,14.553,109.085,14.716,109.022,14.803,108.94,15.001,108.898,15.181,108.821,15.378,108.743,15.427,108.674,15.484,108.578,15.585,108.447,15.763,108.395,15.872,108.286,15.989,108.274,16.029,108.267,16.09,108.241,16.101,108.209,16.091,108.17,16.164,108.088,16.243,108.029,16.331,107.991,16.337,107.936,16.329,107.882,16.31,107.834,16.322,107.803,16.403,107.724,16.488,107.593,16.568,107.541,16.609,107.549,16.643,107.355,16.794,107.18,16.898,107.12,17.056,106.926,17.221,106.736,17.367,106.517,17.663,106.371,17.747,106.356,17.765,106.479,17.72,106.459,17.874,106.499,17.946,106.412,18.053,106.24,18.221,106.145,18.259,106.066,18.316,105.888,18.502,105.839,18.574,105.808,18.646,105.744,18.746,105.732,18.779,105.622,18.966,105.639,19.057,105.716,19.128,105.791,19.294,105.785,19.379,105.812,19.467,105.814,19.587,105.984,19.939,106.062,19.987,106.166,19.992,106.396,20.206,106.518,20.289,106.573,20.392,106.551,20.527,106.753,20.735,106.737,20.806,106.675,20.96,106.683,21,106.725,21,106.76,20.991,106.821,20.958,106.886,20.95,106.936,20.974,106.981,20.971,107.019,20.991,107.075,20.999,107.112,20.96,107.165,20.949,107.354,21.055,107.373,21.128,107.376,21.194,107.41,21.285,107.527,21.336,107.637,21.368,107.707,21.406,107.809,21.497,107.926,21.499,107.973,21.508,108.067,21.526,108.146,21.565,108.246,21.558,108.302,21.622,108.325,21.694,108.355,21.697,108.383,21.679,108.444,21.607,108.502,21.633,108.526,21.671,108.493,21.739,108.481,21.829,108.48,21.905,108.594,21.901,108.616,21.869,108.589,21.816,108.616,21.77,108.675,21.725,108.744,21.651,108.772,21.63,108.846,21.634,108.922,21.624,109.031,21.627,109.102,21.59,109.133,21.544,109.098,21.487,109.082,21.44,109.149,21.426,109.22,21.443,109.347,21.454,109.436,21.479,109.544,21.538,109.521,21.693,109.566,21.691,109.594,21.672,109.687,21.525,109.743,21.528,109.759,21.56,109.83,21.484,109.931,21.481,109.921,21.376,109.78,21.337,109.76,21.228,109.681,21.132,109.704,21.053,109.663,20.917,109.685,20.874,109.726,20.839,109.767,20.781,109.805,20.711,109.792,20.622,109.861,20.514,109.946,20.474,109.968,20.448,109.984,20.403,109.932,20.399,109.886,20.413,109.883,20.364,109.938,20.295,110.123,20.264,110.345,20.295,110.45,20.355,110.487,20.427,110.518,20.46,110.512,20.518,110.313,20.672,110.326,20.72,110.371,20.752,110.388,20.791,110.365,20.838,110.18,20.859,110.154,20.945,110.194,21.038,110.331,21.131,110.375,21.172,110.397,21.248,110.411,21.338,110.435,21.327,110.458,21.231,110.504,21.207,110.567,21.214,110.652,21.279,110.771,21.387,110.878,21.396,110.997,21.43,111.017,21.512,111.061,21.511,111.101,21.485,111.144,21.482,111.221,21.494,111.319,21.486,111.392,21.535,111.603,21.559,111.682,21.608,111.712,21.655,111.776,21.719,111.825,21.71,111.873,21.717,111.926,21.776,111.944,21.85,112.025,21.843,112.117,21.806,112.193,21.763,112.305,21.742,112.356,21.768,112.39,21.801,112.377,21.917,112.36,21.978,112.396,21.981,112.429,21.958,112.439,21.927,112.421,21.881,112.495,21.818,112.586,21.777,112.634,21.82,112.661,21.859,112.725,21.902,112.809,21.945,112.904,21.881,112.954,21.907,112.984,21.938,113.008,22.119,113.089,22.208,113.149,22.075,113.266,22.089,113.328,22.145,113.367,22.165,113.416,22.178,113.499,22.202,113.548,22.223,113.576,22.297,113.589,22.35,113.551,22.404,113.553,22.594,113.485,22.692,113.45,22.726,113.432,22.789,113.345,22.865,113.338,22.889,113.331,22.912,113.442,22.941,113.46,22.996,113.445,23.055,113.52,23.102,113.621,23.127,113.592,23.077,113.586,23.02,113.603,22.969,113.62,22.861,113.661,22.802,113.754,22.734,113.828,22.607,113.931,22.531,114.015,22.512,114.007,22.484,113.896,22.428,113.903,22.396,113.937,22.365,114.033,22.376,114.139,22.348,114.268,22.296,114.288,22.325,114.291,22.374,114.335,22.396,114.325,22.437,114.285,22.458,114.291,22.499,114.266,22.541,114.341,22.593,114.42,22.583,114.496,22.527,114.554,22.529,114.544,22.621,114.572,22.654,114.593,22.698,114.652,22.755,114.711,22.739,114.75,22.626,114.854,22.617,114.896,22.64,114.914,22.685,115.012,22.709,115.092,22.782,115.196,22.817,115.29,22.776,115.383,22.719,115.498,22.719,115.535,22.765,115.561,22.825,115.64,22.853,115.756,22.824,115.852,22.802,116.063,22.879,116.157,22.887,116.206,22.919,116.222,22.95,116.252,22.981,116.346,22.941,116.471,22.946,116.52,23.007,116.538,23.18,116.586,23.218,116.669,23.228,116.699,23.278,116.682,23.327,116.629,23.354,116.712,23.36,116.76,23.383,116.861,23.453,116.911,23.647,117.033,23.623,117.083,23.579,117.148,23.599,117.225,23.647,117.291,23.714,117.331,23.709,117.347,23.636,117.368,23.589,117.417,23.621,117.462,23.736,117.46,23.771,117.433,23.792,117.466,23.841,117.579,23.857,117.628,23.837,117.668,23.939,117.742,24.015,117.839,24.012,117.904,24.106,118.056,24.246,118.051,24.327,118.024,24.38,117.879,24.396,117.848,24.432,117.843,24.474,117.897,24.48,117.935,24.474,118.006,24.482,118.014,24.56,118.087,24.627,118.195,24.626,118.295,24.573,118.412,24.601,118.56,24.58,118.657,24.621,118.719,24.746,118.692,24.782,118.64,24.809,118.637,24.836,118.708,24.85,118.822,24.911,118.909,24.929,118.956,25.005,118.914,25.127,118.978,25.209,119.025,25.223,119.236,25.206,119.286,25.232,119.244,25.307,119.169,25.356,119.146,25.414,119.18,25.45,119.263,25.468,119.344,25.446,119.422,25.46,119.499,25.409,119.593,25.368,119.622,25.391,119.619,25.437,119.539,25.591,119.553,25.699,119.617,25.823,119.648,25.919,119.619,26.004,119.501,26.009,119.418,25.954,119.332,25.949,119.264,25.975,119.139,26.122,119.232,26.104,119.313,26.063,119.37,26.054,119.463,26.055,119.567,26.127,119.693,26.236,119.797,26.3,119.881,26.334,119.876,26.371,119.84,26.414,119.831,26.45,119.785,26.547,119.726,26.609,119.638,26.621,119.624,26.676,119.59,26.73,119.588,26.785,119.652,26.747,119.71,26.729,119.767,26.775,119.789,26.831,119.824,26.846,119.815,26.798,119.821,26.737,119.842,26.689,119.879,26.683,119.882,26.61,119.968,26.586,120.043,26.634,120.087,26.672,120.097,26.781,120.139,26.886,120.279,27.097,120.385,27.156,120.469,27.256,120.54,27.318,120.608,27.412,120.629,27.482,120.588,27.581,120.665,27.639,120.661,27.688,120.685,27.745,120.833,27.891,120.833,27.938,120.763,27.977,120.748,28.01,120.813,28.013,120.892,28.004,120.959,28.037,121.035,28.157,121.098,28.291,121.146,28.327,121.217,28.346,121.272,28.222,121.355,28.23,121.51,28.324,121.61,28.292,121.602,28.367,121.538,28.521,121.475,28.641,121.519,28.714,121.59,28.735,121.63,28.768,121.663,28.851,121.54,28.932,121.641,28.916,121.68,28.953,121.665,29.011,121.521,29.118,121.448,29.131,121.487,29.193,121.534,29.237,121.656,29.236,121.717,29.256,121.791,29.226,121.854,29.129,121.918,29.135,121.968,29.491,121.941,29.606,121.888,29.628,121.822,29.605,121.69,29.511,121.506,29.485,121.575,29.537,121.677,29.584,121.906,29.78,122.083,29.87,122.017,29.888,121.944,29.894,121.812,29.952,121.678,29.979,121.433,30.227,121.341,30.282,121.258,30.304,121.159,30.302,120.904,30.161,120.633,30.133,120.495,30.303,120.353,30.247,120.261,30.263,120.229,30.25,120.195,30.241,120.246,30.284,120.45,30.388,120.63,30.391,120.821,30.355,120.897,30.393,120.938,30.47,120.998,30.558,121.31,30.7,121.419,30.79,121.528,30.841,121.675,30.864,121.769,30.87,121.878,30.917,121.834,31.062,121.786,31.163,121.661,31.32,121.351,31.485,121.205,31.628,121.055,31.719,120.938,31.75,120.788,31.82,120.752,31.923,120.716,31.984,120.497,32.02,120.347,31.952,120.192,31.906,120.036,31.936,120.074,31.96,120.099,31.976,120.184,31.966,120.52,32.106,120.661,32.081,120.792,32.032,120.974,31.869,121.146,31.842,121.266,31.863,121.352,31.859,121.681,31.712,121.764,31.7,121.866,31.704,121.856,31.816,121.832,31.9,121.751,31.993,121.674,32.051,121.491,32.121,121.451,32.153,121.404,32.206,121.401,32.372,121.342,32.425,121.293,32.457,120.99,32.567,120.853,32.661,120.853,32.764,120.897,32.843,120.871,33.017,120.734,33.237,120.616,33.491,120.505,33.638,120.5,33.716,120.426,33.866,120.323,34.169,120.267,34.274,120.201,34.326,119.964,34.448,119.77,34.496,119.583,34.582,119.427,34.714,119.351,34.749,119.201,34.748,119.165,34.849,119.216,35.012,119.353,35.114,119.43,35.301,119.526,35.359,119.608,35.47,119.72,35.589,119.811,35.618,119.866,35.644,119.912,35.693,119.979,35.74,120.027,35.799,120.055,35.861,120.219,35.935,120.285,35.984,120.265,36.007,120.181,36.017,120.094,36.119,120.117,36.15,120.183,36.202,120.27,36.226,120.328,36.228,120.343,36.189,120.33,36.11,120.348,36.079,120.393,36.054,120.519,36.109,120.638,36.13,120.681,36.168,120.682,36.341,120.712,36.413,120.776,36.456,120.847,36.426,120.896,36.444,120.905,36.485,120.883,36.539,120.797,36.607,120.811,36.633,120.879,36.635,120.99,36.598,121.054,36.611,121.144,36.66,121.413,36.738,121.67,36.836,121.933,36.959,122.05,36.971,122.162,36.959,122.203,36.927,122.22,36.88,122.242,36.85,122.274,36.834,122.341,36.832,122.457,36.915,122.52,36.947,122.523,37.003,122.487,37.022,122.447,37.068,122.516,37.138,122.587,37.181,122.573,37.318,122.667,37.403,122.602,37.426,122.493,37.408,122.338,37.405,122.169,37.456,122.11,37.522,122.057,37.529,122.01,37.496,121.965,37.445,121.816,37.457,121.64,37.46,121.505,37.515,121.388,37.579,121.22,37.6,121.049,37.725,120.75,37.834,120.37,37.701,120.285,37.692,120.257,37.679,120.287,37.656,120.312,37.623,120.156,37.495,119.883,37.351,119.88,37.296,119.888,37.253,119.761,37.155,119.45,37.125,119.287,37.138,119.112,37.201,118.998,37.277,118.953,37.331,118.955,37.494,118.991,37.641,119.033,37.661,119.089,37.701,119.07,37.749,119.038,37.777,119.036,37.809,119.028,37.904,118.94,38.043,118.8,38.127,118.667,38.126,118.543,38.095,118.015,38.183,117.767,38.312,117.656,38.424,117.558,38.625,117.554,38.691,117.617,38.853,117.785,39.134,117.866,39.191,118.041,39.227,118.148,39.195,118.298,39.067,118.472,39.118,118.626,39.177,118.752,39.16,118.826,39.172,118.912,39.166,118.977,39.183,119.04,39.222,119.225,39.408,119.261,39.561,119.322,39.662,119.391,39.752,119.591,39.903,119.85,39.987,120.369,40.204,120.479,40.231,120.771,40.589,120.841,40.649,120.922,40.683,121.003,40.749,121.086,40.842,121.175,40.901,121.537,40.878,121.599,40.843,121.729,40.846,121.766,40.876,121.809,40.969,121.835,40.974,121.859,40.842,122.14,40.688,122.179,40.603,122.275,40.542,122.264,40.5,122.203,40.396,122.191,40.358,121.983,40.136,121.869,40.046,121.801,39.951,121.517,39.845,121.474,39.755,121.514,39.685,121.518,39.639,121.47,39.64,121.406,39.621,121.267,39.545,121.286,39.519,121.3,39.452,121.275,39.385,121.356,39.377,121.513,39.375,121.785,39.401,121.818,39.387,121.758,39.348,121.665,39.269,121.628,39.22,121.68,39.109,121.263,38.96,121.188,38.947,121.107,38.921,121.122,38.813,121.164,38.732,121.207,38.744,121.236,38.767,121.32,38.808,121.517,38.831,121.65,38.865,121.67,38.892,121.633,38.955,121.677,39.003,121.745,39.01,121.805,38.991,121.864,38.996,121.923,39.037,121.982,39.053,122.048,39.094,122.121,39.152,122.225,39.267,122.335,39.366,122.84,39.601,122.961,39.62,123.032,39.674,123.227,39.687,123.269,39.727,123.348,39.763,123.49,39.768,123.581,39.786,123.611,39.841,123.651,39.882,123.76,39.822,124.106,39.841,124.267,39.924,124.35,40.012,124.362,40.004,124.375,39.996,124.349,39.907,124.404,39.866,124.557,39.791,124.608,39.717,124.638,39.615,124.699,39.632,124.732,39.652,124.739,39.742,124.775,39.758,124.868,39.702,125.1,39.59,125.18,39.583,125.361,39.527,125.374,39.428,125.413,39.326,125.41,39.288,125.157,38.872,125.169,38.806,125.299,38.743,125.424,38.747,125.489,38.728,125.554,38.686,125.492,38.676,125.415,38.68,125.31,38.665,125.067,38.557,124.974,38.48,124.881,38.342,124.883,38.295,124.875,38.233,124.691,38.129,124.779,38.102,124.907,38.113,124.995,38.078,125.163,38.094,125.207,38.082,125.247,38.057,125.193,38.038,124.989,37.931,125.027,37.923,125.102,37.882,125.311,37.844,125.365,37.748,125.358,37.725,125.407,37.719,125.449,37.73,125.582,37.815,125.676,37.918,125.695,37.963,125.769,37.985,125.942,37.874,126.05,37.87,126.117,37.743,126.161,37.764,126.203,37.829,126.37,37.878,126.573,37.797,126.623,37.79,126.634,37.782,126.621,37.755,126.578,37.745,126.563,37.717,126.58,37.654,126.608,37.617,126.657,37.551,126.65,37.447,126.696,37.411,126.791,37.295,126.746,37.194,126.776,37.158,126.787,37.103,126.869,36.976,126.96,36.958,126.977,36.939,126.958,36.906,126.879,36.862,126.872,36.824,126.839,36.846,126.784,36.948,126.687,36.96,126.578,37.02,126.487,37.007,126.429,36.969,126.352,36.958,126.217,36.871,126.161,36.772,126.181,36.692,126.231,36.689,126.389,36.651,126.433,36.678,126.488,36.694,126.507,36.586,126.548,36.478,126.552,36.43,126.544,36.341,126.557,36.236,126.54,36.166,126.597,36.105,126.682,36.038,126.693,36.014,126.664,35.975,126.647,35.922,126.72,35.898,126.753,35.872,126.717,35.769,126.602,35.714,126.542,35.669,126.488,35.647,126.487,35.606,126.565,35.59,126.614,35.571,126.582,35.534,126.493,35.501,126.46,35.456,126.396,35.314,126.361,35.217,126.291,35.154,126.327,35.045,126.398,34.933,126.421,34.823,126.479,34.81,126.548,34.837,126.593,34.824,126.539,34.779,126.473,34.756,126.505,34.738,126.525,34.698,126.426,34.695,126.301,34.72,126.264,34.673,126.333,34.59,126.482,34.494,126.506,34.428,126.508,34.351,126.531,34.314,126.584,34.318,126.611,34.404,126.755,34.512,126.796,34.494,126.826,34.451,126.897,34.439,127.031,34.607,127.247,34.755,127.269,34.72,127.261,34.662,127.195,34.605,127.173,34.546,127.325,34.463,127.381,34.501,127.401,34.553,127.479,34.625,127.423,34.688,127.39,34.743,127.404,34.823,127.477,34.844,127.524,34.84,127.565,34.766,127.632,34.69,127.715,34.721,127.742,34.783,127.663,34.843,127.639,34.89,127.659,34.926,127.715,34.955,127.873,34.966,127.977,35.019,128.036,35.022,128.095,34.934,128.152,34.916,128.276,34.911,128.388,34.875,128.444,34.87,128.448,34.932,128.419,35.016,128.458,35.069,128.511,35.101,128.643,35.12,128.796,35.094,128.98,35.102,129.077,35.123,129.214,35.182,129.329,35.333,129.419,35.498,129.485,35.687,129.562,35.948,129.573,36.051,129.51,36.038,129.458,36.006,129.426,36.019,129.404,36.052,129.402,36.138,129.391,36.202,129.393,36.323,129.427,36.385,129.445,36.471,129.433,36.637,129.473,36.742,129.426,36.926,129.418,37.059,129.335,37.275,129.052,37.678,128.852,37.887,128.619,38.176,128.375,38.623,128.329,38.681,128.249,38.745,128.163,38.786,128.123,38.816,127.972,38.898,127.786,39.084,127.699,39.125,127.581,39.143,127.497,39.179,127.395,39.208,127.383,39.296,127.422,39.374,127.457,39.401,127.523,39.377,127.549,39.461,127.547,39.563,127.527,39.696,127.568,39.782,127.867,39.896,127.967,39.996,128.106,40.033,128.304,40.036,128.393,40.089,128.511,40.13,128.611,40.198,128.701,40.318,128.843,40.358,128.945,40.428,129.11,40.491,129.245,40.661,129.341,40.726,129.709,40.857,129.742,40.932,129.712,41.124,129.766,41.304,129.759,41.392,129.682,41.494,129.686,41.595,129.756,41.712,129.876,41.806,129.928,41.897,130.007,41.991,130.068,42.046,130.18,42.097,130.236,42.183,130.315,42.214,130.458,42.302,130.569,42.292,130.637,42.275,130.687,42.303,130.73,42.326,130.834,42.523,130.709,42.656,130.756,42.673,130.946,42.634,131.025,42.645,131.158,42.626,131.245,42.697,131.292,42.772,131.393,42.822,131.516,42.996,131.722,43.203,131.795,43.255,131.939,43.302,131.976,43.296,132.013,43.28,131.898,43.171,131.867,43.095,131.947,43.095,132.029,43.119,132.233,43.245,132.31,43.314,132.334,43.239,132.304,42.883,132.481,42.91,132.576,42.872,132.709,42.876,132.864,42.794,132.924,42.805,132.997,42.808,133.059,42.723,133.16,42.697,133.329,42.764,133.587,42.828,133.709,42.83,134.01,42.947,134.156,43.042,134.692,43.291,134.917,43.427,135.131,43.526,135.26,43.685,135.483,43.835,135.489,43.899,135.533,43.971,135.875,44.374,135.987,44.44,136.142,44.489,136.209,44.562,136.251,44.667,136.46,44.822,136.604,44.978,136.737,45.08,136.804,45.171,137.147,45.394,137.425,45.64,137.685,45.818,137.769,45.929,138.106,46.251,138.21,46.463,138.337,46.543,138.392,46.745,138.5,46.89,138.53,46.976,138.587,47.057,139.001,47.383,139.167,47.635,139.373,47.887,139.521,47.975,139.676,48.09,139.761,48.181,139.998,48.324,140.113,48.423,140.171,48.524,140.224,48.773,140.378,48.964,140.334,48.995,140.309,49.054,140.326,49.12,140.349,49.159,140.364,49.221,140.399,49.29,140.431,49.331,140.517,49.596,140.511,49.762,140.465,49.826,140.463,49.911,140.585,50.033,140.613,50.054,140.625,50.082,140.564,50.107,140.535,50.131,140.476,50.546,140.521,50.8,140.646,50.987,140.671,51.051,140.688,51.232,140.839,51.414,140.933,51.62,141.129,51.728,141.258,51.861,141.367,51.921,141.386,52.057,141.485,52.179,141.409,52.234,141.33,52.271,141.17,52.368,141.132,52.436,141.245,52.55,141.266,52.653,141.256,52.84,141.087,52.898,140.875,53.04,140.84,53.088,140.887,53.092,141.181,53.015,141.328,53.097,141.402,53.184,141.374,53.293,141.218,53.334,141.015,53.454,141.006,53.495,140.688,53.596,140.347,53.813,140.242,54.001,140.179,54.052,139.858,54.205,139.796,54.256,139.707,54.277,139.32,54.193,139.105,54.218,138.696,54.32,138.657,54.298,138.716,54.223,138.705,54.148,138.722,54.044,138.699,53.87,138.661,53.745,138.511,53.57,138.451,53.537,138.32,53.523,138.25,53.524,138.292,53.592,138.407,53.674,138.569,53.819,138.568,53.947,138.528,53.96,138.494,53.96,138.379,53.909,138.253,53.726,137.95,53.604,137.738,53.56,137.328,53.539,137.254,53.546,137.221,53.579,137.314,53.632,137.517,53.707,137.645,53.866,137.786,53.903,137.835,53.947,137.623,53.97,137.476,54.028,137.339,54.101,137.403,54.124,137.451,54.13,137.513,54.156,137.666,54.283,137.525,54.291,137.378,54.282,137.142,54.182,137.096,54.129,137.172,54.057,137.258,54.025,137.155,53.822,137.019,53.848,136.886,53.839,136.803,53.782,136.719,53.804,136.683,53.931,136.729,54.061,136.77,54.353,136.82,54.452,136.824,54.561,136.797,54.621,136.715,54.624,136.58,54.614,136.238,54.614,135.852,54.584,135.438,54.692,135.325,54.707,135.258,54.731,135.212,54.841,135.235,54.903,135.263,54.943,135.541,55.114,135.751,55.161,136.175,55.352,136.351,55.51,136.46,55.577,136.794,55.694,137.012,55.795,137.19,55.892,137.384,55.975,137.573,56.112,137.692,56.139,138.074,56.433,138.141,56.499,138.18,56.589,138.218,56.629,138.662,56.966,138.966,57.088,139.182,57.262,139.444,57.33,139.507,57.358,139.619,57.456,139.803,57.514,139.862,57.549,140.002,57.688,140.447,57.814,140.495,57.865,140.685,58.212,140.79,58.303,140.988,58.417,141.347,58.528,141.603,58.649,141.755,58.745,142.025,59,142.33,59.153,142.58,59.24,143.192,59.37,143.524,59.344,143.869,59.411,144.123,59.408,144.483,59.376,145.555,59.414,145.756,59.374,145.829,59.33,145.932,59.198,146.05,59.171,146.273,59.221,146.444,59.43,146.537,59.457,146.804,59.373,147.04,59.366,147.514,59.269,147.688,59.291,147.875,59.388,148.257,59.414,148.491,59.262,148.727,59.258,148.914,59.283,148.965,59.369,148.89,59.4,148.744,59.374,148.709,59.449,148.797,59.532,148.925,59.475,149.133,59.481,149.205,59.488,149.175,59.527,149.128,59.559,149.065,59.631,149.29,59.728,149.425,59.761,149.643,59.77,150.203,59.651,150.326,59.639,150.457,59.591,150.667,59.556,150.54,59.525,150.484,59.494,150.615,59.507,150.729,59.469,150.823,59.461,150.863,59.475,150.912,59.523,150.983,59.571,151.034,59.586,151.17,59.583,151.348,59.561,151.486,59.524,151.798,59.323,151.942,59.284,152.104,59.291,152.17,59.278,152.261,59.224,151.99,59.16,151.733,59.147,151.505,59.164,151.121,59.083,151.327,58.875,151.705,58.867,152.088,58.91,152.165,58.997,152.32,59.031,152.401,59.026,152.576,58.954,152.818,58.926,152.882,58.939,153.078,59.082,153.196,59.094,153.273,59.091,153.361,59.215,153.695,59.225,153.892,59.114,154.011,59.076,154.247,59.109,154.376,59.188,154.458,59.217,154.704,59.141,154.824,59.188,155.017,59.196,155.16,59.19,155.153,59.27,155.167,59.36,154.971,59.45,154.583,59.54,154.358,59.481,154.272,59.475,154.213,59.483,154.15,59.529,154.209,59.6,154.269,59.658,154.267,59.73,154.293,59.833,154.39,59.877,154.441,59.884,154.578,60.095,154.971,60.377,155.428,60.55,155.716,60.682,155.853,60.777,156.056,60.996,156.344,61.155,156.483,61.206,156.63,61.272,156.68,61.481,156.791,61.53,156.892,61.565,157.084,61.676,157.371,61.747,157.469,61.799,157.799,61.795,158.07,61.754,158.152,61.765,158.334,61.826,158.547,61.811,158.824,61.85,159.077,61.922,159.189,61.929,159.295,61.914,159.423,61.808,159.496,61.781,159.552,61.719,159.722,61.758,160.183,61.903,160.238,61.904,160.309,61.894,160.321,61.839,160.317,61.793,160.247,61.648,160.163,61.538,159.931,61.324,159.883,61.292,159.914,61.234,159.949,61.129,159.835,61.014,159.79,60.957,159.883,60.943,160.004,61.007,160.184,61.048,160.281,61.045,160.379,61.025,160.226,60.832,160.201,60.73,160.177,60.691,160.174,60.638,160.287,60.667,160.368,60.709,160.482,60.74,160.767,60.753,160.915,60.893,161.037,60.963,162.188,61.541,162.393,61.662,162.506,61.67,162.608,61.65,162.699,61.653,162.718,61.695,162.752,61.711,162.856,61.705,162.922,61.598,162.994,61.544,163.047,61.554,163.085,61.571,163.139,61.611,163.198,61.645,163.258,61.699,163.208,61.737,163.009,61.792,163.018,61.891,163.131,62.05,163.118,62.153,163.163,62.26,163.213,62.313,163.258,62.337,163.302,62.373,163.244,62.455,163.287,62.511,163.332,62.551,164.256,62.697,164.418,62.705,164.567,62.675,164.792,62.571,165.044,62.517,165.397,62.494,165.417,62.447,165.28,62.463,165.214,62.448,165.226,62.406,165.208,62.374,165.124,62.412,164.888,62.432,164.671,62.474,164.598,62.471,164.288,62.347,164.207,62.292,164.074,62.045,164.068,61.874,164.02,61.711,163.883,61.64,163.837,61.558,163.804,61.461,163.973,61.42,163.992,61.388,164.005,61.344,163.893,61.24,163.62,61.111,163.589,61.084,163.554,61.026,163.71,60.917,163.585,60.877,163.466,60.85,163.352,60.8,162.973,60.783,162.713,60.659,162.266,60.537,162.068,60.466,162.004,60.42,161.846,60.232,161.754,60.152,161.449,60.027,161.219,59.846,160.855,59.627,160.711,59.602,160.547,59.547,160.35,59.394,159.847,59.127,159.592,58.804,159.453,58.696,159.308,58.611,159.211,58.519,159.037,58.424,158.687,58.281,158.449,58.163,158.321,58.083,158.275,58.009,158.21,58.025,157.975,57.986,157.666,58.02,157.45,57.799,157.217,57.777,156.986,57.83,156.872,57.804,156.83,57.78,156.792,57.748,156.9,57.677,156.948,57.616,156.964,57.561,156.977,57.466,156.849,57.29,156.728,57.152,156.529,57.021,156.067,56.782,156.025,56.752,155.983,56.695,155.717,56.072,155.643,55.794,155.555,55.348,155.564,55.199,155.62,54.865,155.706,54.521,155.905,53.928,155.95,53.744,156.099,53.006,156.11,52.866,156.154,52.747,156.229,52.626,156.365,52.509,156.377,52.367,156.49,51.913,156.5,51.475,156.521,51.38,156.543,51.312,156.671,51.227,156.713,51.124,156.724,51.047,156.748,50.969,156.847,51.007,157.202,51.213,157.49,51.409,157.531,51.48,157.629,51.535,157.823,51.605,158.104,51.81,158.332,52.091,158.463,52.305,158.493,52.383,158.5,52.46,158.481,52.627,158.534,52.688,158.609,52.874,158.56,52.922,158.432,52.957,158.472,53.032,158.565,53.05,158.64,53.015,158.684,52.935,158.745,52.909,158.952,53.048,159.136,53.117,159.586,53.238,159.772,53.23,159.947,53.125,160.025,53.13,160.002,53.275,159.898,53.381,159.899,53.448,159.956,53.552,159.914,53.621,159.871,53.673,159.844,53.784,159.922,54.008,160.01,54.131,160.074,54.189,160.289,54.288,160.517,54.431,160.773,54.541,160.936,54.578,161.13,54.598,161.294,54.521,161.625,54.516,161.726,54.533,161.967,54.689,162.106,54.752,162.08,54.886,161.996,54.998,161.824,55.139,161.785,55.205,161.729,55.358,161.724,55.496,161.776,55.655,161.924,55.84,162.085,56.09,162.146,56.128,162.334,56.188,162.461,56.235,162.528,56.261,162.489,56.399,162.589,56.455,162.671,56.49,162.878,56.476,162.944,56.508,163.038,56.522,162.975,56.449,162.893,56.399,162.713,56.331,162.628,56.232,162.84,56.066,162.972,56.034,163.047,56.045,163.189,56.137,163.261,56.174,163.336,56.233,163.294,56.448,163.243,56.565,163.257,56.688,163.165,56.725,163.046,56.741,162.922,56.723,162.85,56.757,162.803,56.811,162.791,56.875,162.815,57.023,162.808,57.103,162.762,57.244,162.762,57.284,162.779,57.358,162.957,57.477,163.109,57.565,163.188,57.637,163.214,57.687,163.226,57.79,163.145,57.837,162.718,57.946,162.654,57.948,162.522,57.904,162.467,57.766,162.391,57.717,162.392,57.745,162.411,57.778,162.197,57.829,162.098,57.875,162.04,57.918,162.002,57.981,161.96,58.077,162.049,58.273,162.142,58.447,162.453,58.709,162.643,58.8,162.847,58.939,162.935,58.964,162.97,58.986,163.004,59.02,162.94,59.114,162.975,59.137,163.01,59.148,163.085,59.131,163.273,59.303,163.269,59.52,163.321,59.705,163.365,59.781,163.41,59.835,163.494,59.887,163.574,59.914,163.69,59.978,163.744,60.028,163.78,60.041,163.913,60.037,164.018,60.017,164.135,59.984,164.113,59.898,164.252,59.974,164.377,60.058,164.44,60.073,164.525,60.061,164.67,59.997,164.779,59.874,164.854,59.841,164.954,59.844,165.019,59.861,165.074,59.946,165.085,60.099,165.193,60.125,165.285,60.135,165.416,60.205,165.583,60.236,165.942,60.357,166.18,60.48,166.352,60.485,166.308,60.414,166.292,60.346,166.23,60.178,166.168,60.089,166.136,59.979,166.149,59.922,166.187,59.849,166.273,59.856,166.332,59.872,166.453,59.947,166.964,60.307,167.227,60.406,167.626,60.469,167.746,60.509,168.138,60.574,168.463,60.592,168.67,60.563,168.788,60.564,169.227,60.596,169.276,60.557,169.618,60.438,169.815,60.265,169.854,60.25,169.887,60.218,169.898,60.148,169.927,60.104,169.983,60.067,170.154,59.986,170.351,59.966,170.396,60.01,170.423,60.048,170.512,60.26,170.589,60.343,170.59,60.394,170.608,60.435,170.799,60.496,170.949,60.523,171.49,60.726,171.729,60.843,171.831,60.837,171.918,60.864,171.998,60.901,172.067,60.916,172.213,60.998,172.393,61.062,172.362,61.117,172.396,61.167,172.497,61.186,172.585,61.19,172.697,61.249,172.69,61.295,172.731,61.314,172.789,61.311,172.868,61.293,172.908,61.312,172.838,61.376,172.807,61.436,172.857,61.469,173.055,61.406,173.132,61.407,173.391,61.557,173.623,61.716,173.822,61.679,174.139,61.795,174.285,61.818,174.514,61.824,174.611,61.868,174.715,61.948,174.798,61.939,175.192,62.034,175.268,62.102,175.366,62.121,175.442,62.128,175.614,62.184,176.328,62.346,176.437,62.411,176.703,62.506,176.907,62.536,177.159,62.561,177.19,62.592,177.008,62.627,176.965,62.659,176.963,62.693,176.99,62.722,177.024,62.777,177.091,62.79,177.173,62.75,177.259,62.75,177.298,62.784,177.339,62.781,177.36,62.737,177.316,62.685,177.296,62.644,177.293,62.599,177.351,62.587,177.663,62.583,178.019,62.547,178.964,62.355,179.045,62.324,179.121,62.32,179.134,62.396,179.177,62.469,179.289,62.51,179.477,62.613,179.571,62.688,179.571,62.773,179.511,62.863,179.381,62.884,179.302,62.94,179.26,63.008,179.329,63.058,179.405,63.078,179.389,63.147,179.332,63.19,179.028,63.282,178.921,63.345,178.919,63.4,178.793,63.54,178.775,63.51,178.787,63.442,178.744,63.395,178.679,63.402,178.669,63.44,178.706,63.522,178.654,63.557,178.466,63.574,178.44,63.606,178.626,63.651,178.681,63.651,178.731,63.667,178.692,63.842,178.65,63.965,178.536,63.976,178.451,64.011,178.475,64.089,178.477,64.128,178.381,64.261,178.313,64.314,178.229,64.364,178.164,64.309,178.131,64.235,178.045,64.22,177.953,64.222,177.688,64.305,177.433,64.444,177.41,64.573,177.467,64.737,177.427,64.763,177.388,64.774,177.05,64.719,176.843,64.634,176.731,64.625,176.508,64.682,176.141,64.586,176.219,64.642,176.283,64.664,176.351,64.705,176.301,64.784,176.247,64.843,176.169,64.885,176.057,64.905,175.946,64.865,175.859,64.825,175.678,64.782,175.331,64.747,175.097,64.747,174.699,64.681,174.549,64.684,175.098,64.777,175.396,64.784,175.781,64.844,176.061,64.961,176.429,64.855,176.557,64.84,176.831,64.849,177.069,64.787,177.148,64.805,177.223,64.862,177.123,64.947,177.037,65,176.94,65.016,176.646,65.007,176.452,65.025,176.341,65.047,176.413,65.071,176.625,65.038,176.881,65.082,177.179,65.014,177.252,64.954,177.337,64.931,177.582,64.778,177.749,64.717,178.285,64.672,178.52,64.603,178.698,64.631,179.15,64.782,179.448,64.822,179.651,64.921,179.827,65.034,180,65.067]},{"b":[145.869,43.42,145.944,43.46],"l":[145.944,43.426,145.941,43.445,145.931,43.457,145.914,43.455,145.896,43.455,145.882,43.46,145.869,43.458,145.869,43.45,145.882,43.444,145.887,43.433,145.894,43.42,145.907,43.422,145.931,43.426,145.944,43.426]},{"b":[146.274,43.62,146.359,43.652],"l":[146.333,43.647,146.31,43.652,146.284,43.639,146.274,43.63,146.288,43.625,146.332,43.62,146.359,43.625,146.35,43.644,146.333,43.647]},{"b":[146.028,43.407,146.101,43.449],"l":[146.089,43.449,146.049,43.434,146.028,43.42,146.032,43.407,146.046,43.409,146.07,43.421,146.086,43.429,146.101,43.44,146.089,43.449]},{"b":[-159.842,-21.25,-159.737,-21.186],"l":[-159.811,-21.186,-159.832,-21.2,-159.842,-21.229,-159.84,-21.238,-159.813,-21.242,-159.773,-21.25,-159.741,-21.249,-159.737,-21.241,-159.74,-21.208,-159.768,-21.188,-159.811,-21.186]},{"b":[-172.499,-8.583,-172.479,-8.546],"l":[-172.494,-8.559,-172.499,-8.548,-172.497,-8.546,-172.487,-8.556,-172.481,-8.567,-172.479,-8.581,-172.484,-8.583,-172.488,-8.572,-172.494,-8.559]},{"b":[-171.204,-9.358,-171.186,-9.333],"l":[-171.2,-9.345,-171.204,-9.333,-171.202,-9.333,-171.194,-9.339,-171.189,-9.347,-171.186,-9.355,-171.189,-9.358,-171.193,-9.352,-171.2,-9.345]},{"b":[-172.228,-4.524,-172.181,-4.492],"l":[-172.215,-4.503,-172.204,-4.5,-172.197,-4.5,-172.196,-4.495,-172.198,-4.492,-172.212,-4.494,-172.228,-4.507,-172.215,-4.524,-172.189,-4.522,-172.181,-4.515,-172.194,-4.516,-172.208,-4.518,-172.215,-4.511,-172.215,-4.503]},{"b":[-171.728,-2.856,-171.628,-2.761],"l":[-171.647,-2.856,-171.628,-2.856,-171.628,-2.847,-171.64,-2.811,-171.665,-2.786,-171.698,-2.766,-171.719,-2.761,-171.725,-2.768,-171.728,-2.774,-171.725,-2.781,-171.718,-2.779,-171.706,-2.773,-171.688,-2.779,-171.673,-2.788,-171.66,-2.799,-171.64,-2.829,-171.639,-2.847,-171.655,-2.84,-171.678,-2.825,-171.698,-2.822,-171.696,-2.826,-171.687,-2.83,-171.671,-2.844,-171.647,-2.856]},{"b":[-171.097,-3.143,-171.081,-3.115],"l":[-171.092,-3.125,-171.088,-3.115,-171.081,-3.12,-171.085,-3.135,-171.09,-3.143,-171.097,-3.137,-171.092,-3.125]},{"b":[-171.262,-4.468,-171.232,-4.442],"l":[-171.252,-4.442,-171.239,-4.444,-171.232,-4.454,-171.233,-4.463,-171.243,-4.468,-171.255,-4.467,-171.262,-4.46,-171.262,-4.449,-171.252,-4.442]},{"b":[-174.541,-4.695,-174.501,-4.657],"l":[-174.501,-4.688,-174.513,-4.675,-174.531,-4.659,-174.541,-4.657,-174.541,-4.662,-174.533,-4.665,-174.523,-4.674,-174.511,-4.686,-174.517,-4.687,-174.529,-4.682,-174.524,-4.69,-174.507,-4.694,-174.501,-4.695,-174.501,-4.688]},{"b":[-155.015,-4.094,-154.943,-4.031],"l":[-155.015,-4.048,-155.015,-4.055,-154.995,-4.071,-154.971,-4.086,-154.959,-4.094,-154.956,-4.088,-154.95,-4.056,-154.943,-4.042,-154.951,-4.031,-154.987,-4.039,-155.015,-4.048]},{"b":[-151.819,-11.457,-151.783,-11.392],"l":[-151.813,-11.392,-151.819,-11.409,-151.816,-11.431,-151.807,-11.451,-151.791,-11.457,-151.783,-11.441,-151.791,-11.414,-151.803,-11.393,-151.813,-11.392]},{"b":[-155.929,-5.632,-155.862,-5.608],"l":[-155.911,-5.609,-155.919,-5.608,-155.929,-5.608,-155.928,-5.619,-155.914,-5.632,-155.887,-5.632,-155.864,-5.627,-155.862,-5.619,-155.872,-5.611,-155.911,-5.609]},{"b":[-178.599,-19.175,-178.535,-19.109],"l":[-178.576,-19.125,-178.563,-19.119,-178.557,-19.113,-178.568,-19.109,-178.589,-19.119,-178.599,-19.137,-178.596,-19.151,-178.574,-19.165,-178.546,-19.175,-178.535,-19.166,-178.541,-19.157,-178.557,-19.154,-178.574,-19.143,-178.576,-19.125]},{"b":[-179.867,-19.003,-179.798,-18.924],"l":[-179.867,-18.978,-179.865,-18.999,-179.851,-19.003,-179.849,-18.991,-179.846,-18.971,-179.839,-18.962,-179.83,-18.956,-179.812,-18.968,-179.798,-18.97,-179.8,-18.94,-179.831,-18.924,-179.856,-18.943,-179.863,-18.964,-179.867,-18.978]},{"b":[-178.731,-20.671,-178.71,-20.645],"l":[-178.723,-20.667,-178.729,-20.66,-178.731,-20.653,-178.728,-20.645,-178.725,-20.646,-178.719,-20.652,-178.714,-20.66,-178.712,-20.668,-178.71,-20.671,-178.715,-20.67,-178.723,-20.667]},{"b":[-78.989,-33.668,-78.769,-33.575],"l":[-78.838,-33.585,-78.859,-33.578,-78.877,-33.575,-78.888,-33.576,-78.938,-33.614,-78.979,-33.644,-78.989,-33.662,-78.983,-33.668,-78.804,-33.646,-78.775,-33.642,-78.769,-33.627,-78.785,-33.61,-78.838,-33.585]},{"b":[-14.415,-7.976,-14.303,-7.883],"l":[-14.384,-7.883,-14.399,-7.906,-14.415,-7.944,-14.409,-7.967,-14.399,-7.976,-14.364,-7.974,-14.317,-7.956,-14.303,-7.935,-14.329,-7.913,-14.36,-7.886,-14.384,-7.883]},{"b":[72.772,11.196,72.796,11.263],"l":[72.788,11.216,72.78,11.202,72.773,11.196,72.772,11.214,72.782,11.243,72.793,11.263,72.796,11.26,72.793,11.242,72.788,11.216]},{"b":[73.023,8.252,73.084,8.317],"l":[73.053,8.257,73.039,8.252,73.029,8.254,73.023,8.266,73.026,8.275,73.039,8.265,73.056,8.275,73.075,8.306,73.079,8.317,73.084,8.311,73.08,8.293,73.067,8.269,73.053,8.257]},{"b":[73.382,3.229,73.443,3.29],"l":[73.417,3.231,73.395,3.229,73.382,3.246,73.385,3.271,73.402,3.289,73.428,3.29,73.443,3.274,73.435,3.25,73.417,3.231]},{"b":[73.473,4.155,73.528,4.248],"l":[73.519,4.187,73.512,4.165,73.495,4.155,73.479,4.159,73.473,4.171,73.481,4.188,73.495,4.21,73.504,4.235,73.518,4.248,73.528,4.243,73.527,4.23,73.522,4.211,73.519,4.187]},{"b":[131.135,3.022,131.188,3.061],"l":[131.137,3.039,131.135,3.025,131.15,3.022,131.172,3.026,131.186,3.042,131.188,3.056,131.172,3.061,131.152,3.054,131.137,3.039]},{"b":[151.57,7.333,151.65,7.39],"l":[151.593,7.379,151.608,7.375,151.604,7.357,151.575,7.351,151.57,7.346,151.578,7.338,151.639,7.333,151.648,7.346,151.65,7.363,151.643,7.379,151.629,7.39,151.606,7.389,151.593,7.379]},{"b":[151.856,7.427,151.913,7.467],"l":[151.882,7.467,151.865,7.466,151.86,7.457,151.856,7.432,151.864,7.427,151.881,7.432,151.913,7.454,151.911,7.46,151.882,7.467]},{"b":[166.845,11.146,166.899,11.169],"l":[166.888,11.169,166.859,11.166,166.845,11.153,166.864,11.146,166.89,11.153,166.899,11.165,166.888,11.169]},{"b":[177.006,-12.516,177.127,-12.477],"l":[177.118,-12.482,177.068,-12.477,177.026,-12.487,177.006,-12.491,177.019,-12.507,177.082,-12.516,177.121,-12.505,177.127,-12.493,177.118,-12.482]},{"b":[174.587,-21.706,174.63,-21.667],"l":[174.593,-21.702,174.587,-21.68,174.604,-21.667,174.628,-21.676,174.63,-21.695,174.622,-21.706,174.593,-21.702]},{"b":[179.196,-8.535,179.217,-8.466],"l":[179.201,-8.535,179.214,-8.524,179.217,-8.515,179.212,-8.488,179.203,-8.466,179.199,-8.47,179.198,-8.489,179.201,-8.512,179.196,-8.535,179.201,-8.535]},{"b":[178.667,-7.495,178.695,-7.458],"l":[178.667,-7.458,178.677,-7.463,178.69,-7.474,178.695,-7.483,178.693,-7.495,178.687,-7.494,178.674,-7.48,178.667,-7.458]},{"b":[178.371,-8.068,178.39,-8.032],"l":[178.387,-8.032,178.379,-8.047,178.371,-8.061,178.377,-8.068,178.39,-8.046,178.387,-8.032]},{"b":[169.523,-0.876,169.555,-0.847],"l":[169.523,-0.866,169.526,-0.853,169.539,-0.847,169.555,-0.857,169.551,-0.874,169.542,-0.876,169.523,-0.866]},{"b":[-128.35,-24.413,-128.29,-24.323],"l":[-128.33,-24.323,-128.35,-24.34,-128.342,-24.371,-128.321,-24.4,-128.3,-24.413,-128.29,-24.397,-128.291,-24.365,-128.304,-24.334,-128.33,-24.323]},{"b":[-177.39,28.196,-177.354,28.221],"l":[-177.354,28.217,-177.371,28.221,-177.389,28.209,-177.39,28.196,-177.371,28.2,-177.362,28.207,-177.354,28.217]},{"b":[-81.811,24.542,-81.739,24.577],"l":[-81.784,24.545,-81.74,24.554,-81.739,24.575,-81.768,24.577,-81.811,24.558,-81.809,24.542,-81.784,24.545]},{"b":[-86.753,21.191,-86.696,21.28],"l":[-86.727,21.264,-86.739,21.28,-86.753,21.279,-86.736,21.233,-86.714,21.197,-86.696,21.191,-86.714,21.239,-86.727,21.264]},{"b":[-166.149,66.222,-165.822,66.328],"l":[-165.822,66.328,-166.033,66.278,-166.146,66.237,-166.149,66.222,-166.11,66.227,-165.942,66.278,-165.83,66.317,-165.822,66.328]},{"b":[54.335,24.423,54.465,24.506],"l":[54.427,24.471,54.398,24.506,54.379,24.505,54.335,24.471,54.358,24.443,54.428,24.425,54.457,24.423,54.465,24.443,54.427,24.471]},{"b":[124.625,37.92,124.755,37.989],"l":[124.74,37.947,124.754,37.955,124.755,37.97,124.745,37.981,124.717,37.989,124.661,37.973,124.636,37.981,124.625,37.945,124.656,37.925,124.697,37.92,124.717,37.933]}]} \ No newline at end of file diff --git a/src/maps/data/countries_110m.json b/src/maps/data/countries_110m.json new file mode 100644 index 0000000..0779ab3 --- /dev/null +++ b/src/maps/data/countries_110m.json @@ -0,0 +1 @@ +{"f":[{"n":"Fiji","c":"FJ","r":6,"ml":3,"lx":177.98,"ly":-17.83,"b":[-180,-18.29,180,-16.02],"p":[[[180,-16.07,180,-16.56,179.36,-16.8,178.73,-17.01,178.6,-16.64,179.1,-16.43,179.41,-16.38,180,-16.07]],[[178.13,-17.5,178.37,-17.34,178.72,-17.63,178.55,-18.15,177.93,-18.29,177.38,-18.16,177.29,-17.72,177.67,-17.38,178.13,-17.5]],[[-179.79,-16.02,-179.92,-16.5,-180,-16.56,-180,-16.07,-179.79,-16.02]]]},{"n":"Tanzania","c":"TZ","r":3,"ml":3,"lx":34.96,"ly":-6.05,"b":[29.34,-11.72,40.32,-0.95],"p":[[[33.9,-0.95,34.07,-1.06,37.7,-3.1,37.77,-3.68,39.2,-4.68,38.74,-5.91,38.8,-6.48,39.44,-6.84,39.47,-7.1,39.19,-7.7,39.25,-8.01,39.19,-8.49,39.54,-9.11,39.95,-10.1,40.32,-10.32,39.52,-10.9,38.43,-11.29,37.83,-11.27,37.47,-11.57,36.78,-11.59,36.51,-11.72,35.31,-11.44,34.56,-11.52,34.28,-10.16,33.94,-9.69,33.74,-9.42,32.76,-9.23,32.19,-8.93,31.56,-8.76,31.16,-8.59,30.74,-8.34,30.2,-7.08,29.62,-6.52,29.42,-5.94,29.52,-5.42,29.34,-4.5,29.75,-4.45,30.12,-4.09,30.51,-3.57,30.75,-3.36,30.74,-3.03,30.53,-2.81,30.47,-2.41,30.76,-2.29,30.82,-1.7,30.42,-1.13,30.77,-1.01,31.87,-1.03,33.9,-0.95]]]},{"n":"Western Sahara","c":"EH","r":7,"ml":6,"lx":-12.63,"ly":23.97,"b":[-17.06,21,-8.67,27.66],"p":[[[-8.67,27.66,-8.67,27.59,-8.68,27.4,-8.69,25.88,-11.97,25.93,-11.94,23.37,-12.87,23.28,-13.12,22.77,-12.93,21.33,-16.85,21.33,-17.06,21,-17.02,21.42,-17,21.42,-14.75,21.5,-14.63,21.86,-14.22,22.31,-13.89,23.69,-12.5,24.77,-12.03,26.03,-11.72,26.1,-11.39,26.88,-10.55,26.99,-10.19,26.86,-9.74,26.86,-9.41,27.09,-8.79,27.12,-8.82,27.66,-8.67,27.66]]]},{"n":"Canada","c":"CA","r":2,"ml":1.7,"lx":-101.91,"ly":60.32,"b":[-141,41.68,-52.65,83.23],"p":[[[-122.84,49,-122.97,49,-124.91,49.98,-125.62,50.42,-127.44,50.83,-127.99,51.72,-127.85,52.33,-129.13,52.76,-129.31,53.56,-130.51,54.29,-130.54,54.8,-129.98,55.29,-130.01,55.92,-131.71,56.55,-132.73,57.69,-133.36,58.41,-134.27,58.86,-134.94,59.27,-135.48,59.79,-136.48,59.46,-137.45,58.91,-138.34,59.56,-139.04,60,-140.01,60.28,-141,60.31,-140.99,66,-140.99,69.71,-139.12,69.47,-137.55,68.99,-136.5,68.9,-135.63,69.32,-134.41,69.63,-132.93,69.51,-131.43,69.94,-129.79,70.19,-129.11,69.78,-128.36,70.01,-128.14,70.48,-127.45,70.38,-125.76,69.48,-124.42,70.16,-124.29,69.4,-123.06,69.56,-122.68,69.86,-121.47,69.8,-119.94,69.38,-117.6,69.01,-116.23,68.84,-115.25,68.91,-113.9,68.4,-115.3,67.9,-113.5,67.69,-110.8,67.81,-109.95,67.98,-108.88,67.38,-107.79,67.89,-108.81,68.31,-108.17,68.65,-106.95,68.7,-106.15,68.8,-105.34,68.56,-104.34,68.02,-103.22,68.1,-101.45,67.65,-99.9,67.81,-98.44,67.78,-98.56,68.4,-97.67,68.58,-96.12,68.24,-96.13,67.29,-95.49,68.09,-94.68,68.06,-94.23,69.07,-95.3,69.69,-96.47,70.09,-96.39,71.19,-95.21,71.92,-93.89,71.76,-92.88,71.32,-91.52,70.19,-92.41,69.7,-90.55,69.5,-90.55,68.47,-89.22,69.26,-88.02,68.62,-88.32,67.87,-87.35,67.2,-86.31,67.92,-85.58,68.78,-85.52,69.88,-84.1,69.81,-82.62,69.66,-81.28,69.16,-81.22,68.67,-81.96,68.13,-81.26,67.6,-81.39,67.11,-83.34,66.41,-84.74,66.26,-85.77,66.56,-86.07,66.06,-87.03,65.21,-87.32,64.78,-88.48,64.1,-89.91,64.03,-90.7,63.61,-90.77,62.96,-91.93,62.84,-93.16,62.02,-94.24,60.9,-94.63,60.11,-94.68,58.95,-93.22,58.78,-92.76,57.85,-92.3,57.09,-90.9,57.28,-89.04,56.85,-88.04,56.47,-87.32,56,-86.07,55.72,-85.01,55.3,-83.36,55.24,-82.27,55.15,-82.44,54.28,-82.13,53.28,-81.4,52.16,-79.91,51.21,-79.14,51.53,-78.6,52.56,-79.12,54.14,-79.83,54.67,-78.23,55.14,-77.1,55.84,-76.54,56.53,-76.62,57.2,-77.3,58.05,-78.52,58.8,-77.34,59.85,-77.77,60.76,-78.11,62.32,-77.41,62.55,-75.7,62.28,-74.67,62.18,-73.84,62.44,-72.91,62.11,-71.68,61.53,-71.37,61.14,-69.59,61.06,-69.62,60.22,-69.29,58.96,-68.37,58.8,-67.65,58.21,-66.2,58.77,-65.25,59.87,-64.58,60.34,-63.8,59.44,-62.5,58.17,-61.4,56.97,-61.8,56.34,-60.47,55.78,-59.57,55.2,-57.98,54.95,-57.33,54.63,-56.94,53.78,-56.16,53.65,-55.76,53.27,-55.68,52.15,-56.41,51.77,-57.13,51.42,-58.77,51.06,-60.03,50.24,-61.72,50.08,-63.86,50.29,-65.36,50.3,-66.4,50.23,-67.24,49.51,-68.51,49.07,-69.95,47.74,-71.1,46.82,-70.26,46.99,-68.65,48.3,-66.55,49.13,-65.06,49.23,-64.17,48.74,-65.12,48.07,-64.8,46.99,-64.47,46.24,-63.17,45.74,-61.52,45.88,-60.52,47.01,-60.45,46.28,-59.8,45.92,-61.04,45.27,-63.25,44.67,-64.25,44.27,-65.36,43.55,-66.12,43.62,-66.16,44.47,-64.43,45.29,-66.03,45.26,-67.14,45.14,-67.79,45.7,-67.79,47.07,-68.23,47.35,-68.9,47.19,-69.24,47.45,-70,46.69,-70.31,45.92,-70.66,45.46,-71.08,45.31,-71.4,45.26,-71.51,45.01,-73.35,45.01,-74.87,45,-75.32,44.82,-76.37,44.1,-76.5,44.02,-76.82,43.63,-77.74,43.63,-78.72,43.63,-79.17,43.47,-79.01,43.27,-78.92,42.97,-78.94,42.86,-80.25,42.37,-81.28,42.21,-82.44,41.68,-82.69,41.68,-83.03,41.83,-83.14,41.98,-83.12,42.08,-82.9,42.43,-82.43,42.98,-82.14,43.57,-82.34,44.44,-82.55,45.35,-83.59,45.82,-83.47,45.99,-83.62,46.12,-83.89,46.12,-84.09,46.28,-84.14,46.51,-84.34,46.41,-84.6,46.44,-84.54,46.54,-84.78,46.64,-84.88,46.9,-85.65,47.22,-86.46,47.55,-87.44,47.94,-88.38,48.3,-89.27,48.02,-89.6,48.01,-90.83,48.27,-91.64,48.14,-92.61,48.45,-93.63,48.61,-94.33,48.67,-94.64,48.84,-94.82,49.39,-95.16,49.38,-95.16,49,-97.23,49,-100.65,49,-104.05,49,-107.05,49,-110.05,49,-113,49,-116.05,49,-117.03,49,-120,49,-122.84,49]],[[-83.99,62.45,-83.25,62.91,-81.88,62.9,-81.9,62.71,-83.07,62.16,-83.77,62.18,-83.99,62.45]],[[-79.78,72.8,-80.88,73.33,-80.83,73.69,-80.35,73.76,-78.06,73.65,-76.34,73.1,-76.25,72.83,-77.31,72.86,-78.39,72.88,-79.49,72.74,-79.78,72.8]],[[-80.32,62.09,-79.93,62.39,-79.52,62.36,-79.27,62.16,-79.66,61.63,-80.1,61.72,-80.36,62.02,-80.32,62.09]],[[-93.61,74.98,-94.16,74.59,-95.61,74.67,-96.82,74.93,-96.29,75.38,-94.85,75.65,-93.98,75.3,-93.61,74.98]],[[-93.84,77.52,-94.3,77.49,-96.17,77.56,-96.44,77.83,-94.42,77.82,-93.72,77.63,-93.84,77.52]],[[-96.75,78.77,-95.56,78.42,-95.83,78.06,-97.31,77.85,-98.12,78.08,-98.55,78.46,-98.63,78.87,-97.34,78.83,-96.75,78.77]],[[-88.15,74.39,-89.76,74.52,-92.42,74.84,-92.77,75.39,-92.89,75.88,-93.89,76.32,-95.96,76.44,-97.12,76.75,-96.75,77.16,-94.68,77.1,-93.57,76.78,-91.61,76.78,-90.74,76.45,-90.97,76.07,-89.82,75.85,-89.19,75.61,-87.84,75.57,-86.38,75.48,-84.79,75.7,-82.75,75.78,-81.13,75.71,-80.06,75.34,-79.83,74.92,-80.46,74.66,-81.95,74.44,-83.23,74.56,-86.1,74.41,-88.15,74.39]],[[-111.26,78.15,-109.85,78,-110.19,77.7,-112.05,77.41,-113.53,77.73,-112.72,78.05,-111.26,78.15]],[[-110.96,78.8,-109.66,78.6,-110.88,78.41,-112.54,78.41,-112.53,78.55,-111.5,78.85,-110.96,78.8]],[[-55.6,51.32,-56.13,50.69,-56.8,49.81,-56.14,50.15,-55.47,49.94,-55.82,49.59,-54.94,49.31,-54.47,49.56,-53.48,49.25,-53.79,48.52,-53.09,48.69,-52.96,48.16,-52.65,47.54,-53.07,46.66,-53.52,46.62,-54.18,46.81,-53.96,47.63,-54.24,47.75,-55.4,46.88,-56,46.92,-55.29,47.39,-56.25,47.63,-57.33,47.57,-59.27,47.6,-59.42,47.9,-58.8,48.25,-59.23,48.52,-58.39,49.13,-57.36,50.72,-56.74,51.29,-55.87,51.63,-55.41,51.59,-55.6,51.32]],[[-83.88,65.11,-82.79,64.77,-81.64,64.46,-81.55,63.98,-80.82,64.06,-80.1,63.73,-80.99,63.41,-82.55,63.65,-83.11,64.1,-84.1,63.57,-85.52,63.05,-85.87,63.64,-87.22,63.54,-86.35,64.04,-86.22,64.82,-85.88,65.74,-85.16,65.66,-84.98,65.22,-84.46,65.37,-83.88,65.11]],[[-78.77,72.35,-77.82,72.75,-75.61,72.24,-74.23,71.77,-74.1,71.33,-72.24,71.56,-71.2,70.92,-68.79,70.53,-67.91,70.12,-66.97,69.19,-68.81,68.72,-66.45,68.07,-64.86,67.85,-63.42,66.93,-61.85,66.86,-62.16,66.16,-63.92,65,-65.15,65.43,-66.72,66.39,-68.02,66.26,-68.14,65.69,-67.09,65.11,-65.73,64.65,-65.32,64.38,-64.67,63.39,-65.01,62.67,-66.28,62.95,-68.78,63.75,-67.37,62.88,-66.33,62.28,-66.17,61.93,-68.88,62.33,-71.02,62.91,-72.24,63.4,-71.89,63.68,-73.38,64.19,-74.83,64.68,-74.82,64.39,-77.71,64.23,-78.56,64.57,-77.9,65.31,-76.02,65.33,-73.96,65.45,-74.29,65.81,-73.94,66.31,-72.65,67.28,-72.93,67.73,-73.31,68.07,-74.84,68.55,-76.87,68.89,-76.23,69.15,-77.29,69.77,-78.17,69.83,-78.96,70.17,-79.49,69.87,-81.31,69.74,-84.94,69.97,-87.06,70.26,-88.68,70.41,-89.51,70.76,-88.47,71.22,-89.89,71.22,-90.21,72.24,-89.44,73.13,-88.41,73.54,-85.83,73.8,-86.56,73.16,-85.77,72.53,-84.85,73.34,-82.32,73.75,-80.6,72.72,-80.75,72.06,-78.77,72.35]],[[-94.5,74.13,-92.42,74.1,-90.51,73.86,-92,72.97,-93.2,72.77,-94.27,72.02,-95.41,72.06,-96.03,72.94,-96.02,73.44,-95.5,73.86,-94.5,74.13]],[[-122.85,76.12,-121.16,76.86,-119.1,77.51,-117.57,77.5,-116.2,77.65,-116.34,76.88,-117.11,76.53,-118.04,76.48,-119.9,76.05,-121.5,75.9,-122.85,76.12]],[[-132.71,54.04,-131.75,54.12,-132.05,52.98,-131.18,52.18,-131.58,52.18,-132.18,52.64,-132.55,53.1,-133.05,53.41,-133.24,53.85,-133.18,54.17,-132.71,54.04]],[[-105.49,79.3,-103.53,79.17,-100.83,78.8,-100.06,78.32,-99.67,77.91,-101.3,78.02,-102.95,78.34,-105.18,78.38,-104.21,78.68,-105.42,78.92,-105.49,79.3]],[[-123.51,48.51,-124.01,48.37,-125.66,48.83,-125.95,49.18,-126.85,49.53,-127.03,49.81,-128.06,49.99,-128.44,50.54,-128.36,50.77,-127.31,50.55,-126.7,50.4,-125.76,50.3,-125.42,49.95,-124.92,49.48,-123.92,49.06,-123.51,48.51]],[[-121.54,74.45,-120.11,74.24,-117.56,74.19,-116.58,73.9,-115.51,73.48,-116.77,73.22,-119.22,72.52,-120.46,71.82,-120.46,71.38,-123.09,70.9,-123.62,71.34,-125.93,71.87,-125.5,72.29,-124.81,73.02,-123.94,73.68,-124.92,74.29,-121.54,74.45]],[[-107.82,75.85,-106.93,76.01,-105.88,75.97,-105.7,75.48,-106.31,75.01,-109.7,74.85,-112.22,74.42,-113.74,74.39,-113.87,74.72,-111.79,75.16,-116.31,75.04,-117.71,75.22,-116.35,76.2,-115.4,76.48,-112.59,76.14,-110.81,75.55,-109.07,75.47,-110.5,76.43,-109.58,76.79,-108.55,76.68,-108.21,76.2,-107.82,75.85]],[[-106.52,73.08,-105.4,72.67,-104.77,71.7,-104.46,70.99,-102.79,70.5,-100.98,70.02,-101.09,69.58,-102.73,69.5,-102.09,69.12,-102.43,68.75,-104.24,68.91,-105.96,69.18,-107.12,69.12,-109,68.78,-111.53,68.63,-113.31,68.54,-113.85,69.01,-115.22,69.28,-116.11,69.17,-117.34,69.96,-116.67,70.07,-115.13,70.24,-113.72,70.19,-112.42,70.37,-114.35,70.6,-116.49,70.52,-117.9,70.54,-118.43,70.91,-116.11,71.31,-117.66,71.3,-119.4,71.56,-118.56,72.31,-117.87,72.71,-115.19,73.31,-114.17,73.12,-114.67,72.65,-112.44,72.96,-111.05,72.45,-109.92,72.96,-109.01,72.63,-108.19,71.65,-107.69,72.07,-108.4,73.09,-107.52,73.24,-106.52,73.08]],[[-100.44,72.71,-101.54,73.36,-100.36,73.84,-99.16,73.63,-97.38,73.76,-97.12,73.47,-98.05,72.99,-96.54,72.56,-96.72,71.66,-98.36,71.27,-99.32,71.36,-100.01,71.74,-102.5,72.51,-102.48,72.83,-100.44,72.71]],[[-106.6,73.6,-105.26,73.64,-104.5,73.42,-105.38,72.76,-106.94,73.46,-106.6,73.6]],[[-98.5,76.72,-97.74,76.26,-97.7,75.74,-98.16,75,-99.81,74.9,-100.88,75.06,-100.86,75.64,-102.5,75.56,-102.57,76.34,-101.49,76.31,-99.98,76.65,-98.58,76.59,-98.5,76.72]],[[-96.02,80.6,-95.32,80.91,-94.3,80.98,-94.74,81.21,-92.41,81.26,-91.13,80.72,-89.45,80.51,-87.81,80.32,-87.02,79.66,-85.81,79.34,-87.19,79.04,-89.04,78.29,-90.8,78.22,-92.88,78.34,-93.95,78.75,-93.94,79.11,-93.15,79.38,-94.97,79.37,-96.08,79.71,-96.71,80.16,-96.02,80.6]],[[-91.59,81.89,-90.1,82.09,-88.93,82.12,-86.97,82.28,-85.5,82.65,-84.26,82.6,-83.18,82.32,-82.42,82.86,-81.1,83.02,-79.31,83.13,-76.25,83.17,-75.72,83.06,-72.83,83.23,-70.67,83.17,-68.5,83.11,-65.83,83.03,-63.68,82.9,-61.85,82.63,-61.89,82.36,-64.33,81.93,-66.75,81.73,-67.66,81.5,-65.48,81.51,-67.84,80.9,-69.47,80.62,-71.18,79.8,-73.24,79.63,-73.88,79.43,-76.91,79.32,-75.53,79.2,-76.22,79.02,-75.39,78.53,-76.34,78.18,-77.89,77.9,-78.36,77.51,-79.76,77.21,-79.62,76.98,-77.91,77.02,-77.89,76.78,-80.56,76.18,-83.17,76.45,-86.11,76.3,-87.6,76.42,-89.49,76.47,-89.62,76.95,-87.77,77.18,-88.26,77.9,-87.65,77.97,-84.98,77.54,-86.34,78.18,-87.96,78.37,-87.15,78.76,-85.38,79,-85.09,79.35,-86.51,79.74,-86.93,80.25,-84.2,80.21,-83.41,80.1,-81.85,80.46,-84.1,80.58,-87.6,80.52,-89.37,80.86,-90.2,81.26,-91.37,81.55,-91.59,81.89]],[[-75.22,67.44,-75.87,67.15,-76.99,67.1,-77.24,67.59,-76.81,68.15,-75.9,68.29,-75.11,68.01,-75.1,67.58,-75.22,67.44]],[[-96.26,69.49,-95.65,69.11,-96.27,68.76,-97.62,69.06,-98.43,68.95,-99.8,69.4,-98.92,69.71,-98.22,70.14,-97.16,69.86,-96.56,69.68,-96.26,69.49]],[[-64.52,49.87,-64.17,49.96,-62.86,49.71,-61.84,49.29,-61.81,49.11,-62.29,49.09,-63.59,49.4,-64.52,49.87]],[[-64.01,47.04,-63.66,46.55,-62.94,46.42,-62.01,46.44,-62.5,46.03,-62.87,45.97,-64.14,46.39,-64.39,46.73,-64.01,47.04]]]},{"n":"United States of America","c":"US","r":2,"ml":1.7,"lx":-97.48,"ly":39.54,"b":[-171.79,18.92,-66.96,71.36],"p":[[[-122.84,49,-120,49,-117.03,49,-116.05,49,-113,49,-110.05,49,-107.05,49,-104.05,49,-100.65,49,-97.23,49,-95.16,49,-95.16,49.38,-94.82,49.39,-94.64,48.84,-94.33,48.67,-93.63,48.61,-92.61,48.45,-91.64,48.14,-90.83,48.27,-89.6,48.01,-89.27,48.02,-88.38,48.3,-87.44,47.94,-86.46,47.55,-85.65,47.22,-84.88,46.9,-84.78,46.64,-84.54,46.54,-84.6,46.44,-84.34,46.41,-84.14,46.51,-84.09,46.28,-83.89,46.12,-83.62,46.12,-83.47,45.99,-83.59,45.82,-82.55,45.35,-82.34,44.44,-82.14,43.57,-82.43,42.98,-82.9,42.43,-83.12,42.08,-83.14,41.98,-83.03,41.83,-82.69,41.68,-82.44,41.68,-81.28,42.21,-80.25,42.37,-78.94,42.86,-78.92,42.97,-79.01,43.27,-79.17,43.47,-78.72,43.63,-77.74,43.63,-76.82,43.63,-76.5,44.02,-76.37,44.1,-75.32,44.82,-74.87,45,-73.35,45.01,-71.51,45.01,-71.4,45.26,-71.08,45.31,-70.66,45.46,-70.31,45.92,-70,46.69,-69.24,47.45,-68.9,47.19,-68.23,47.35,-67.79,47.07,-67.79,45.7,-67.14,45.14,-66.96,44.81,-68.03,44.33,-69.06,43.98,-70.12,43.68,-70.65,43.09,-70.81,42.87,-70.82,42.34,-70.49,41.81,-70.08,41.78,-70.18,42.15,-69.88,41.92,-69.97,41.64,-70.64,41.48,-71.12,41.49,-71.86,41.32,-72.29,41.27,-72.88,41.22,-73.71,40.93,-72.24,41.12,-71.94,40.93,-73.34,40.63,-73.98,40.63,-73.95,40.75,-74.26,40.47,-73.96,40.43,-74.18,39.71,-74.91,38.94,-74.98,39.2,-75.2,39.25,-75.53,39.5,-75.32,38.96,-75.07,38.78,-75.06,38.4,-75.38,38.02,-75.94,37.22,-76.03,37.26,-75.72,37.94,-76.23,38.32,-76.35,39.15,-76.54,38.72,-76.33,38.08,-76.99,38.24,-76.3,37.92,-76.26,36.97,-75.97,36.9,-75.87,36.55,-75.73,35.55,-76.36,34.81,-77.4,34.51,-78.05,33.93,-78.55,33.86,-79.06,33.49,-79.2,33.16,-80.3,32.51,-80.86,32.03,-81.34,31.44,-81.49,30.73,-81.31,30.04,-80.98,29.18,-80.54,28.47,-80.53,28.04,-80.06,26.88,-80.09,26.21,-80.13,25.82,-80.38,25.21,-80.68,25.08,-81.17,25.2,-81.33,25.64,-81.71,25.87,-82.24,26.73,-82.71,27.5,-82.86,27.89,-82.65,28.55,-82.93,29.1,-83.71,29.94,-84.1,30.09,-85.11,29.64,-85.29,29.69,-85.77,30.15,-86.4,30.4,-87.53,30.27,-88.42,30.38,-89.18,30.32,-89.59,30.16,-89.41,29.89,-89.43,29.49,-89.22,29.29,-89.41,29.16,-89.78,29.31,-90.15,29.12,-90.88,29.15,-91.63,29.68,-92.5,29.55,-93.23,29.78,-93.85,29.71,-94.69,29.48,-95.6,28.74,-96.59,28.31,-97.14,27.83,-97.37,27.38,-97.38,26.69,-97.33,26.21,-97.14,25.87,-97.53,25.84,-98.24,26.06,-99.02,26.37,-99.3,26.84,-99.52,27.54,-100.11,28.11,-100.46,28.7,-100.96,29.38,-101.66,29.78,-102.48,29.76,-103.11,28.97,-103.94,29.27,-104.46,29.57,-104.71,30.12,-105.04,30.64,-105.63,31.08,-106.14,31.4,-106.51,31.75,-108.24,31.75,-108.24,31.34,-109.03,31.34,-111.02,31.33,-113.3,32.04,-114.81,32.53,-114.72,32.72,-115.99,32.61,-117.13,32.54,-117.3,33.05,-117.94,33.62,-118.41,33.74,-118.52,34.03,-119.08,34.08,-119.44,34.35,-120.37,34.45,-120.62,34.61,-120.74,35.16,-121.71,36.16,-122.55,37.55,-122.51,37.78,-122.95,38.11,-123.73,38.95,-123.87,39.77,-124.4,40.31,-124.18,41.14,-124.21,42,-124.53,42.77,-124.14,43.71,-124.02,44.62,-123.9,45.52,-124.08,46.86,-124.4,47.72,-124.69,48.18,-124.57,48.38,-123.12,48.04,-122.59,47.1,-122.34,47.36,-122.5,48.18,-122.84,49]],[[-155.4,20.08,-155.22,19.99,-155.06,19.86,-154.81,19.51,-154.83,19.45,-155.22,19.24,-155.54,19.08,-155.69,18.92,-155.94,19.06,-155.91,19.34,-156.07,19.7,-156.02,19.81,-155.85,19.98,-155.92,20.17,-155.86,20.27,-155.79,20.25,-155.4,20.08]],[[-156,20.76,-156.08,20.64,-156.41,20.57,-156.59,20.78,-156.7,20.86,-156.71,20.93,-156.61,21.01,-156.26,20.92,-156,20.76]],[[-156.76,21.18,-156.79,21.07,-157.33,21.1,-157.25,21.22,-156.76,21.18]],[[-158.03,21.72,-157.94,21.65,-157.65,21.32,-157.71,21.26,-157.78,21.28,-158.13,21.31,-158.25,21.54,-158.29,21.58,-158.03,21.72]],[[-159.37,22.21,-159.35,21.98,-159.46,21.88,-159.8,22.07,-159.75,22.14,-159.6,22.24,-159.37,22.21]],[[-166.47,60.38,-165.67,60.29,-165.58,59.91,-166.19,59.75,-166.85,59.94,-167.46,60.21,-166.47,60.38]],[[-153.23,57.97,-152.56,57.9,-152.14,57.59,-153.01,57.12,-154.01,56.73,-154.52,56.99,-154.67,57.46,-153.76,57.82,-153.23,57.97]],[[-140.99,69.71,-140.99,66,-141,60.31,-140.01,60.28,-139.04,60,-138.34,59.56,-137.45,58.91,-136.48,59.46,-135.48,59.79,-134.94,59.27,-134.27,58.86,-133.36,58.41,-132.73,57.69,-131.71,56.55,-130.01,55.92,-129.98,55.29,-130.54,54.8,-131.09,55.18,-131.97,55.5,-132.25,56.37,-133.54,57.18,-134.08,58.12,-135.04,58.19,-136.63,58.21,-137.8,58.5,-139.87,59.54,-140.83,59.73,-142.57,60.08,-143.96,60,-145.93,60.46,-147.11,60.88,-148.22,60.67,-148.02,59.98,-148.57,59.91,-149.73,59.71,-150.61,59.37,-151.72,59.16,-151.86,59.74,-151.41,60.73,-150.35,61.03,-150.62,61.28,-151.9,60.73,-152.58,60.06,-154.02,59.35,-153.29,58.86,-154.23,58.15,-155.31,57.73,-156.31,57.42,-156.56,56.98,-158.12,56.46,-158.43,55.99,-159.6,55.57,-160.29,55.64,-161.22,55.36,-162.24,55.02,-163.07,54.69,-164.79,54.4,-164.94,54.57,-163.85,55.04,-162.87,55.35,-161.8,55.89,-160.56,56.01,-160.07,56.42,-158.68,57.02,-158.46,57.22,-157.72,57.57,-157.55,58.33,-157.04,58.92,-158.19,58.62,-158.52,58.79,-159.06,58.42,-159.71,58.93,-159.98,58.57,-160.36,59.07,-161.36,58.67,-161.97,58.67,-162.05,59.27,-161.87,59.63,-162.52,59.99,-163.82,59.8,-164.66,60.27,-165.35,60.51,-165.35,61.07,-166.12,61.5,-165.73,62.07,-164.92,62.63,-164.56,63.15,-163.75,63.22,-163.07,63.06,-162.26,63.54,-161.53,63.46,-160.77,63.77,-160.96,64.22,-161.52,64.4,-160.78,64.79,-161.39,64.78,-162.45,64.56,-162.76,64.34,-163.55,64.56,-164.96,64.45,-166.43,64.69,-166.85,65.09,-168.11,65.67,-166.71,66.09,-164.47,66.58,-163.65,66.58,-163.79,66.08,-161.68,66.12,-162.49,66.74,-163.72,67.12,-164.43,67.62,-165.39,68.04,-166.76,68.36,-166.2,68.88,-164.43,68.92,-163.17,69.37,-162.93,69.86,-161.91,70.33,-160.93,70.45,-159.04,70.89,-158.12,70.82,-156.58,71.36,-155.07,71.15,-154.34,70.7,-153.9,70.89,-152.21,70.83,-152.27,70.6,-150.74,70.43,-149.72,70.53,-147.61,70.21,-145.69,70.12,-144.92,69.99,-143.59,70.15,-142.07,69.85,-140.99,69.71]],[[-171.73,63.78,-171.11,63.59,-170.49,63.69,-169.68,63.43,-168.69,63.3,-168.77,63.19,-169.53,62.98,-170.29,63.19,-170.67,63.38,-171.55,63.32,-171.79,63.41,-171.73,63.78]]]},{"n":"Kazakhstan","c":"KZ","r":3,"ml":2.7,"lx":68.69,"ly":49.05,"b":[46.47,40.66,87.36,55.39],"p":[[[87.36,49.21,86.6,48.55,85.77,48.46,85.72,47.45,85.16,47,83.18,47.33,82.46,45.54,81.95,45.32,79.97,44.92,80.87,43.18,80.18,42.92,80.26,42.35,79.64,42.5,79.14,42.86,77.66,42.96,76,42.99,75.64,42.88,74.21,43.3,73.65,43.09,73.49,42.5,71.84,42.85,71.19,42.7,70.96,42.27,70.39,42.08,69.07,41.38,68.63,40.67,68.26,40.66,67.99,41.14,66.71,41.17,66.51,41.99,66.02,41.99,66.1,43,64.9,43.73,63.19,43.65,62.01,43.5,61.06,44.41,60.24,44.78,58.69,45.5,58.5,45.59,55.93,45,55.97,41.31,55.46,41.26,54.76,42.04,54.08,42.32,52.94,42.12,52.5,41.78,52.45,42.03,52.69,42.44,52.5,42.79,51.34,43.13,50.89,44.03,50.34,44.28,50.31,44.61,51.28,44.51,51.32,45.25,52.17,45.41,53.04,45.26,53.22,46.23,53.04,46.85,52.04,46.8,51.19,47.05,50.03,46.61,49.1,46.4,48.59,46.56,48.69,47.08,48.06,47.74,47.32,47.72,46.47,48.39,47.04,49.15,46.75,49.36,47.55,50.45,48.58,49.87,48.7,50.61,50.77,51.69,52.33,51.72,54.53,51.03,55.72,50.62,56.78,51.04,58.36,51.06,59.64,50.55,59.93,50.84,61.34,50.8,61.59,51.27,59.97,51.96,60.93,52.45,60.74,52.72,61.7,52.98,60.98,53.66,61.44,54.01,65.18,54.35,65.67,54.6,68.17,54.97,69.07,55.39,70.87,55.17,71.18,54.13,72.22,54.38,73.51,54.04,73.43,53.49,74.38,53.55,76.89,54.49,76.53,54.18,77.8,53.4,80.04,50.86,80.57,51.39,81.95,50.81,83.38,51.07,83.94,50.89,84.42,50.31,85.12,50.12,85.54,49.69,86.83,49.83,87.36,49.21]]]},{"n":"Uzbekistan","c":"UZ","r":3,"ml":3,"lx":64.01,"ly":41.69,"b":[55.93,37.14,73.06,45.59],"p":[[[55.97,41.31,55.93,45,58.5,45.59,58.69,45.5,60.24,44.78,61.06,44.41,62.01,43.5,63.19,43.65,64.9,43.73,66.1,43,66.02,41.99,66.51,41.99,66.71,41.17,67.99,41.14,68.26,40.66,68.63,40.67,69.07,41.38,70.39,42.08,70.96,42.27,71.26,42.17,70.42,41.52,71.16,41.14,71.87,41.39,73.06,40.87,71.77,40.15,71.01,40.24,70.6,40.22,70.46,40.5,70.67,40.96,69.33,40.73,69.01,40.09,68.54,39.53,67.7,39.58,67.44,39.14,68.18,38.9,68.39,38.16,67.83,37.14,67.08,37.36,66.52,37.36,66.55,37.97,65.22,38.4,64.17,38.89,63.52,39.36,62.37,40.05,61.88,41.08,61.55,41.27,60.47,41.22,60.08,41.43,59.98,42.22,58.63,42.75,57.79,42.17,56.93,41.83,57.1,41.32,55.97,41.31]]]},{"n":"Papua New Guinea","c":"PG","r":2,"ml":2.5,"lx":143.91,"ly":-5.7,"b":[141,-10.65,156.02,-2.5],"p":[[[141,-2.6,142.74,-3.29,144.58,-3.86,145.27,-4.37,145.83,-4.88,145.98,-5.47,147.65,-6.08,147.89,-6.61,146.97,-6.72,147.19,-7.39,148.08,-8.04,148.73,-9.1,149.31,-9.07,149.27,-9.51,150.04,-9.68,149.74,-9.87,150.8,-10.29,150.69,-10.58,150.03,-10.65,149.78,-10.39,148.92,-10.28,147.91,-10.13,147.14,-9.49,146.57,-8.94,146.05,-8.07,144.74,-7.63,143.9,-7.92,143.29,-8.25,143.41,-8.98,142.63,-9.33,142.07,-9.16,141.03,-9.12,141.02,-5.86,141,-2.6]],[[152.64,-3.66,153.02,-3.98,153.14,-4.5,152.83,-4.77,152.64,-4.18,152.41,-3.79,151.95,-3.46,151.38,-3.04,150.66,-2.74,150.94,-2.5,151.48,-2.78,151.82,-3,152.24,-3.24,152.64,-3.66]],[[151.3,-5.84,150.75,-6.08,150.24,-6.32,149.71,-6.32,148.89,-6.03,148.32,-5.75,148.4,-5.44,149.3,-5.58,149.85,-5.51,150,-5.03,150.14,-5,150.24,-5.53,150.81,-5.46,151.09,-5.11,151.65,-4.76,151.54,-4.17,152.14,-4.15,152.34,-4.31,152.32,-4.87,151.98,-5.48,151.46,-5.56,151.3,-5.84]],[[154.76,-5.34,155.06,-5.57,155.55,-6.2,156.02,-6.54,155.88,-6.82,155.6,-6.92,155.17,-6.54,154.73,-5.9,154.51,-5.14,154.65,-5.04,154.76,-5.34]]]},{"n":"Indonesia","c":"ID","r":2,"ml":1.7,"lx":101.89,"ly":-0.95,"b":[95.29,-10.36,141.03,5.48],"p":[[[141,-2.6,141.02,-5.86,141.03,-9.12,140.14,-8.3,139.13,-8.1,138.88,-8.38,137.61,-8.41,138.04,-7.6,138.67,-7.32,138.41,-6.23,137.93,-5.39,135.99,-4.55,135.16,-4.46,133.66,-3.54,133.37,-4.02,132.98,-4.11,132.76,-3.75,132.75,-3.31,131.99,-2.82,133.07,-2.46,133.78,-2.48,133.7,-2.21,132.23,-2.21,131.84,-1.62,130.94,-1.43,130.52,-0.94,131.87,-0.7,132.38,-0.37,133.99,-0.78,134.14,-1.15,134.42,-2.77,135.46,-3.37,136.29,-2.31,137.44,-1.7,138.33,-1.7,139.18,-2.05,139.93,-2.41,141,-2.6]],[[124.97,-8.89,125.07,-9.09,125.09,-9.39,124.44,-10.14,123.58,-10.36,123.46,-10.24,123.55,-9.9,123.98,-9.29,124.97,-8.89]],[[134.21,-6.9,134.11,-6.14,134.29,-5.78,134.5,-5.45,134.73,-5.74,134.72,-6.21,134.21,-6.9]],[[117.88,4.14,117.31,3.23,118.05,2.29,117.88,1.83,119,0.9,117.81,0.78,117.48,0.1,117.52,-0.8,116.56,-1.49,116.53,-2.48,116.15,-4.01,116,-3.66,114.86,-4.11,114.47,-3.5,113.76,-3.44,113.26,-3.12,112.07,-3.48,111.7,-2.99,111.05,-3.05,110.22,-2.93,110.07,-1.59,109.57,-1.31,109.09,-0.46,108.95,0.42,109.07,1.34,109.66,2.01,109.83,1.34,110.51,0.77,111.16,0.98,111.8,0.9,112.38,1.41,112.86,1.5,113.81,1.22,114.62,1.43,115.13,2.82,115.52,3.17,115.87,4.31,117.02,4.31,117.88,4.14]],[[129.37,-2.8,130.47,-3.09,130.83,-3.86,129.99,-3.45,129.16,-3.36,128.59,-3.43,127.9,-3.39,128.14,-2.84,129.37,-2.8]],[[126.87,-3.79,126.18,-3.61,125.99,-3.18,127,-3.13,127.25,-3.46,126.87,-3.79]],[[127.93,2.17,128,1.63,128.59,1.54,128.69,1.13,128.64,0.26,128.12,0.36,127.97,-0.25,128.38,-0.78,128.1,-0.9,127.7,-0.27,127.4,1.01,127.6,1.81,127.93,2.17]],[[122.93,0.88,124.08,0.92,125.07,1.64,125.24,1.42,124.44,0.43,123.69,0.24,122.72,0.43,121.06,0.38,120.18,0.24,120.04,-0.52,120.94,-1.41,121.48,-0.96,123.34,-0.62,123.26,-1.08,122.82,-0.93,122.39,-1.52,121.51,-1.9,122.45,-3.19,122.27,-3.53,123.17,-4.68,123.16,-5.34,122.63,-5.63,122.24,-5.28,122.72,-4.46,121.74,-4.85,121.49,-4.57,121.62,-4.19,120.9,-3.6,120.97,-2.63,120.31,-2.93,120.39,-4.1,120.43,-5.53,119.8,-5.67,119.37,-5.38,119.65,-4.46,119.5,-3.49,119.08,-3.49,118.77,-2.8,119.18,-2.15,119.32,-1.35,119.83,0.15,120.04,0.57,120.89,1.31,121.67,1.01,122.93,0.88]],[[120.3,-10.26,118.97,-9.56,119.9,-9.36,120.43,-9.67,120.78,-9.97,120.72,-10.24,120.3,-10.26]],[[121.34,-8.54,122.01,-8.46,122.9,-8.09,122.76,-8.65,121.25,-8.93,119.92,-8.81,119.92,-8.44,120.72,-8.24,121.34,-8.54]],[[118.26,-8.36,118.88,-8.28,119.13,-8.71,117.97,-8.91,117.28,-9.04,116.74,-9.03,117.08,-8.46,117.63,-8.45,117.9,-8.1,118.26,-8.36]],[[108.49,-6.42,108.62,-6.78,110.54,-6.88,110.76,-6.47,112.61,-6.95,112.98,-7.59,114.48,-7.78,115.71,-8.37,114.56,-8.75,113.46,-8.35,112.56,-8.38,111.52,-8.3,110.59,-8.12,109.43,-7.74,108.69,-7.64,108.28,-7.77,106.45,-7.35,106.28,-6.92,105.37,-6.85,106.05,-5.9,107.27,-5.95,108.07,-6.35,108.49,-6.42]],[[104.37,-1.08,104.54,-1.78,104.89,-2.34,105.62,-2.43,106.11,-3.06,105.86,-4.31,105.82,-5.85,104.71,-5.87,103.87,-5.04,102.58,-4.22,102.16,-3.61,101.4,-2.8,100.9,-2.05,100.14,-0.65,99.26,0.18,98.97,1.04,98.6,1.82,97.7,2.45,97.18,3.31,96.42,3.87,95.38,4.97,95.29,5.48,95.94,5.44,97.48,5.25,98.37,4.27,99.14,3.59,99.69,3.17,100.64,2.1,101.66,2.08,102.5,1.4,103.08,0.56,103.84,0.1,103.44,-0.71,104.01,-1.06,104.37,-1.08]]]},{"n":"Argentina","c":"AR","r":2,"ml":2,"lx":-64.17,"ly":-33.5,"b":[-73.42,-55.25,-53.63,-21.83],"p":[[[-68.63,-52.64,-68.25,-53.1,-67.75,-53.85,-66.45,-54.45,-65.05,-54.7,-65.5,-55.2,-66.45,-55.25,-66.96,-54.9,-67.56,-54.87,-68.63,-54.87,-68.63,-52.64]],[[-57.63,-30.22,-57.87,-31.02,-58.14,-32.04,-58.13,-33.04,-58.35,-33.26,-58.43,-33.91,-58.5,-34.43,-57.23,-35.29,-57.36,-35.98,-56.74,-36.41,-56.79,-36.9,-57.75,-38.18,-59.23,-38.72,-61.24,-38.93,-62.34,-38.83,-62.13,-39.42,-62.33,-40.17,-62.15,-40.68,-62.75,-41.03,-63.77,-41.17,-64.73,-40.8,-65.12,-41.06,-64.98,-42.06,-64.3,-42.36,-63.76,-42.04,-63.46,-42.56,-64.38,-42.87,-65.18,-43.5,-65.33,-44.5,-65.57,-45.04,-66.51,-45.04,-67.29,-45.55,-67.58,-46.3,-66.6,-47.03,-65.64,-47.24,-65.99,-48.13,-67.17,-48.7,-67.82,-49.87,-68.73,-50.26,-69.14,-50.73,-68.82,-51.77,-68.15,-52.35,-68.57,-52.3,-69.5,-52.14,-71.91,-52.01,-72.33,-51.43,-72.31,-50.68,-72.98,-50.74,-73.33,-50.38,-73.42,-49.32,-72.65,-48.88,-72.33,-48.24,-72.45,-47.74,-71.92,-46.88,-71.55,-45.56,-71.66,-44.97,-71.22,-44.78,-71.33,-44.41,-71.79,-44.21,-71.46,-43.79,-71.92,-43.41,-72.15,-42.25,-71.75,-42.05,-71.92,-40.83,-71.68,-39.81,-71.41,-38.92,-70.81,-38.55,-71.12,-37.58,-71.12,-36.66,-70.36,-36.01,-70.39,-35.17,-69.82,-34.19,-69.81,-33.27,-70.07,-33.09,-70.54,-31.37,-69.92,-30.34,-70.01,-29.37,-69.66,-28.46,-69,-27.52,-68.3,-26.9,-68.59,-26.51,-68.39,-26.19,-68.42,-24.52,-67.33,-24.03,-66.99,-22.99,-67.11,-22.74,-66.27,-21.83,-64.96,-22.08,-64.38,-22.8,-63.99,-21.99,-62.85,-22.03,-62.69,-22.25,-60.85,-23.88,-60.03,-24.03,-58.81,-24.77,-57.78,-25.16,-57.63,-25.6,-58.62,-27.12,-57.61,-27.4,-56.49,-27.55,-55.7,-27.39,-54.79,-26.62,-54.63,-25.74,-54.13,-25.55,-53.63,-26.12,-53.65,-26.92,-54.49,-27.47,-55.16,-27.88,-56.29,-28.85,-57.63,-30.22]]]},{"n":"Chile","c":"CL","r":2,"ml":1.7,"lx":-72.32,"ly":-38.15,"b":[-75.64,-55.61,-66.96,-17.58],"p":[[[-68.63,-52.64,-68.63,-54.87,-67.56,-54.87,-66.96,-54.9,-67.29,-55.3,-68.15,-55.61,-68.64,-55.58,-69.23,-55.5,-69.96,-55.2,-71.01,-55.05,-72.26,-54.5,-73.29,-53.96,-74.66,-52.84,-73.84,-53.05,-72.43,-53.72,-71.11,-54.07,-70.59,-53.62,-70.27,-52.93,-69.35,-52.52,-68.63,-52.64]],[[-69.59,-17.58,-69.1,-18.26,-68.97,-18.98,-68.44,-19.41,-68.76,-20.37,-68.22,-21.49,-67.83,-22.87,-67.11,-22.74,-66.99,-22.99,-67.33,-24.03,-68.42,-24.52,-68.39,-26.19,-68.59,-26.51,-68.3,-26.9,-69,-27.52,-69.66,-28.46,-70.01,-29.37,-69.92,-30.34,-70.54,-31.37,-70.07,-33.09,-69.81,-33.27,-69.82,-34.19,-70.39,-35.17,-70.36,-36.01,-71.12,-36.66,-71.12,-37.58,-70.81,-38.55,-71.41,-38.92,-71.68,-39.81,-71.92,-40.83,-71.75,-42.05,-72.15,-42.25,-71.92,-43.41,-71.46,-43.79,-71.79,-44.21,-71.33,-44.41,-71.22,-44.78,-71.66,-44.97,-71.55,-45.56,-71.92,-46.88,-72.45,-47.74,-72.33,-48.24,-72.65,-48.88,-73.42,-49.32,-73.33,-50.38,-72.98,-50.74,-72.31,-50.68,-72.33,-51.43,-71.91,-52.01,-69.5,-52.14,-68.57,-52.3,-69.46,-52.29,-69.94,-52.54,-70.85,-52.9,-71.01,-53.83,-71.43,-53.86,-72.56,-53.53,-73.7,-52.84,-74.95,-52.26,-75.26,-51.63,-74.98,-51.04,-75.48,-50.38,-75.61,-48.67,-75.18,-47.71,-74.13,-46.94,-75.64,-46.65,-74.69,-45.76,-74.35,-44.1,-73.24,-44.45,-72.72,-42.38,-73.39,-42.12,-73.7,-43.37,-74.33,-43.22,-74.02,-41.79,-73.68,-39.94,-73.22,-39.26,-73.51,-38.28,-73.59,-37.16,-73.17,-37.12,-72.55,-35.51,-71.86,-33.91,-71.44,-32.42,-71.67,-30.92,-71.37,-30.1,-71.49,-28.86,-70.91,-27.64,-70.72,-25.71,-70.4,-23.63,-70.09,-21.39,-70.16,-19.76,-70.37,-18.35,-69.86,-18.09,-69.59,-17.58]]]},{"n":"Democratic Republic of the Congo","c":"CD","r":2,"ml":2,"lx":23.46,"ly":-1.86,"b":[12.18,-13.26,31.17,5.26],"p":[[[29.34,-4.5,29.52,-5.42,29.42,-5.94,29.62,-6.52,30.2,-7.08,30.74,-8.34,30.35,-8.24,29,-8.41,28.73,-8.53,28.45,-9.16,28.67,-9.61,28.5,-10.79,28.37,-11.79,28.64,-11.97,29.34,-12.36,29.62,-12.18,29.7,-13.26,28.93,-13.25,28.52,-12.7,28.16,-12.27,27.39,-12.13,27.16,-11.61,26.55,-11.92,25.75,-11.78,25.42,-11.33,24.78,-11.24,24.31,-11.26,24.26,-10.95,23.91,-10.93,23.46,-10.87,22.84,-11.02,22.4,-10.99,22.16,-11.08,22.21,-9.89,21.88,-9.52,21.8,-8.91,21.95,-8.31,21.75,-7.92,21.73,-7.29,20.51,-7.3,20.6,-6.94,20.09,-6.94,20.04,-7.12,19.42,-7.16,19.17,-7.74,19.02,-7.99,18.46,-7.85,18.13,-7.99,17.47,-8.07,17.09,-7.55,16.86,-7.22,16.57,-6.62,16.33,-5.88,13.38,-5.86,13.02,-5.98,12.74,-5.97,12.32,-6.1,12.18,-5.79,12.44,-5.68,12.47,-5.25,12.63,-4.99,13,-4.78,13.26,-4.88,13.6,-4.5,14.14,-4.51,14.21,-4.79,14.58,-4.97,15.17,-4.34,15.75,-3.86,16.01,-3.54,15.97,-2.71,16.41,-1.74,16.87,-1.23,17.52,-0.74,17.64,-0.42,17.66,-0.06,17.83,0.29,17.77,0.86,17.9,1.74,18.09,2.37,18.39,2.9,18.45,3.5,18.54,4.2,18.93,4.71,19.47,5.03,20.29,4.69,20.93,4.32,21.66,4.22,22.41,4.03,22.7,4.63,22.84,4.71,23.3,4.61,24.41,5.11,24.81,4.9,25.13,4.93,25.28,5.17,25.65,5.26,26.4,5.15,27.04,5.13,27.37,5.23,27.98,4.41,28.43,4.29,28.7,4.46,29.16,4.39,29.72,4.6,29.95,4.17,30.83,3.51,30.77,2.34,31.17,2.2,30.85,1.85,30.47,1.58,30.09,1.06,29.88,0.6,29.82,-0.21,29.59,-0.59,29.58,-1.34,29.29,-1.62,29.25,-2.22,29.12,-2.29,29.02,-2.84,29.28,-3.29,29.34,-4.5]]]},{"n":"Somalia","c":"SO","r":6,"ml":4,"lx":45.19,"ly":3.57,"b":[40.98,-1.68,51.13,12.02],"p":[[[41.59,-1.68,40.99,-0.86,40.98,2.78,41.86,3.92,42.13,4.23,42.77,4.25,43.66,4.96,44.96,5,47.79,8,48.49,8.84,48.94,9.45,48.94,9.97,48.94,10.98,48.94,11.39,48.95,11.41,49.27,11.43,49.73,11.58,50.26,11.68,50.73,12.02,51.11,12.02,51.13,11.75,51.04,11.17,51.05,10.64,50.83,10.28,50.55,9.2,50.07,8.08,49.45,6.8,48.59,5.34,47.74,4.22,46.56,2.86,45.56,2.05,44.07,1.05,43.14,0.29,42.04,-0.92,41.81,-1.45,41.59,-1.68]]]},{"n":"Kenya","c":"KE","r":2,"ml":1.7,"lx":37.91,"ly":0.55,"b":[33.89,-4.68,41.86,5.51],"p":[[[39.2,-4.68,37.77,-3.68,37.7,-3.1,34.07,-1.06,33.9,-0.95,33.89,0.11,34.18,0.52,34.67,1.18,35.04,1.91,34.6,3.05,34.48,3.56,34.01,4.25,34.62,4.85,35.3,5.51,35.82,5.34,35.82,4.78,36.16,4.45,36.86,4.45,38.12,3.6,38.44,3.59,38.67,3.62,38.89,3.5,39.56,3.42,39.85,3.84,40.77,4.26,41.17,3.92,41.86,3.92,40.98,2.78,40.99,-0.86,41.59,-1.68,40.88,-2.08,40.64,-2.5,40.26,-2.57,40.12,-3.28,39.8,-3.68,39.6,-4.35,39.2,-4.68]]]},{"n":"Sudan","c":"SD","r":3,"ml":2.5,"lx":29.26,"ly":16.33,"b":[21.94,8.23,38.41,22],"p":[[[24.57,8.23,23.81,8.67,23.46,8.95,23.39,9.27,23.56,9.68,23.55,10.09,22.98,10.71,22.86,11.14,22.88,11.38,22.51,11.68,22.5,12.26,22.29,12.65,21.94,12.59,22.04,12.96,22.3,13.37,22.18,13.79,22.51,14.09,22.3,14.33,22.57,14.94,23.02,15.68,23.89,15.61,23.84,19.58,23.85,20,25,20,25,22,29.02,22,32.9,22,36.87,22,37.19,21.02,36.97,20.84,37.11,19.81,37.48,18.61,37.86,18.37,38.41,18,37.9,17.43,37.17,17.26,36.85,16.96,36.75,16.29,36.32,14.82,36.43,14.42,36.27,13.56,35.86,12.58,35.26,12.08,34.83,11.32,34.73,10.91,34.26,10.63,33.96,9.58,33.97,8.68,33.96,9.46,33.82,9.48,33.84,9.98,33.72,10.33,33.21,10.72,33.09,11.44,33.21,12.18,32.74,12.25,32.67,12.02,32.07,11.97,32.31,11.68,32.4,11.08,31.85,10.53,31.35,9.81,30.84,9.71,30,10.29,29.62,10.08,29.52,9.79,29,9.6,28.97,9.4,27.97,9.4,27.83,9.6,27.11,9.64,26.75,9.47,26.48,9.55,25.96,10.14,25.79,10.41,25.07,10.27,24.79,9.81,24.54,8.92,24.19,8.73,23.89,8.62,24.57,8.23]]]},{"n":"Chad","c":"TD","r":3,"ml":3,"lx":18.65,"ly":15.14,"b":[13.54,7.42,23.89,23.41],"p":[[[23.84,19.58,23.89,15.61,23.02,15.68,22.57,14.94,22.3,14.33,22.51,14.09,22.18,13.79,22.3,13.37,22.04,12.96,21.94,12.59,22.29,12.65,22.5,12.26,22.51,11.68,22.88,11.38,22.86,11.14,22.23,10.97,21.72,10.57,21,9.48,20.06,9.01,19.09,9.07,18.81,8.98,18.91,8.63,18.39,8.28,17.96,7.89,16.71,7.51,16.46,7.73,16.29,7.75,16.11,7.5,15.28,7.42,15.44,7.69,15.12,8.38,14.98,8.8,14.54,8.97,13.95,9.55,14.17,10.02,14.63,9.92,14.91,9.99,15.47,9.98,14.92,10.89,14.96,11.56,14.89,12.22,14.5,12.86,14.6,13.33,13.95,13.35,13.96,14,13.54,14.37,13.97,15.68,15.25,16.63,15.3,17.93,15.69,19.96,15.9,20.39,15.49,20.73,15.47,21.05,15.1,21.31,14.85,22.86,15.86,23.41,19.85,21.5,23.84,19.58]]]},{"n":"Haiti","c":"HT","r":5,"ml":4,"lx":-72.22,"ly":19.26,"b":[-74.46,18.03,-71.62,19.92],"p":[[[-71.71,19.71,-71.62,19.17,-71.7,18.79,-71.95,18.62,-71.69,18.32,-71.71,18.04,-72.37,18.21,-72.84,18.15,-73.45,18.22,-73.92,18.03,-74.46,18.34,-74.37,18.66,-73.45,18.53,-72.69,18.45,-72.33,18.67,-72.79,19.1,-72.78,19.48,-73.42,19.64,-73.19,19.92,-72.58,19.87,-71.71,19.71]]]},{"n":"Dominican Republic","c":"DO","r":5,"ml":4.5,"lx":-70.65,"ly":19.1,"b":[-71.95,17.6,-68.32,19.88],"p":[[[-71.71,18.04,-71.69,18.32,-71.95,18.62,-71.7,18.79,-71.62,19.17,-71.71,19.71,-71.59,19.88,-70.81,19.88,-70.21,19.62,-69.95,19.65,-69.77,19.29,-69.22,19.31,-69.25,19.02,-68.81,18.98,-68.32,18.61,-68.69,18.21,-69.16,18.42,-69.62,18.38,-69.95,18.43,-70.13,18.25,-70.52,18.18,-70.67,18.43,-71,18.28,-71.4,17.6,-71.66,17.76,-71.71,18.04]]]},{"n":"Russia","c":"RU","r":2,"ml":1.7,"lx":44.69,"ly":58.25,"b":[-180,41.15,180,81.25],"p":[[[178.73,71.1,180,71.52,180,70.83,178.9,70.78,178.73,71.1]],[[49.1,46.4,48.65,45.81,47.68,45.64,46.68,44.61,47.59,43.66,47.49,42.99,48.58,41.81,47.99,41.41,47.82,41.15,47.37,41.22,46.69,41.83,46.4,41.86,45.78,42.09,45.47,42.5,44.54,42.71,43.93,42.55,43.76,42.74,42.39,43.22,40.92,43.38,40.08,43.55,39.96,43.43,38.68,44.28,37.54,44.66,36.68,45.24,37.4,45.4,38.23,46.24,37.67,46.64,39.15,47.04,39.12,47.26,38.22,47.1,38.26,47.55,38.77,47.83,39.74,47.9,39.9,48.23,39.67,48.78,40.08,49.31,40.07,49.6,38.59,49.93,38.01,49.92,37.39,50.38,36.63,50.23,35.36,50.58,35.38,50.77,35.02,51.21,34.22,51.26,34.14,51.57,34.39,51.77,33.75,52.34,32.72,52.24,32.41,52.29,32.16,52.06,31.79,52.1,31.54,52.74,31.31,53.07,31.5,53.17,32.3,53.13,32.69,53.35,32.41,53.62,31.73,53.79,31.79,53.97,31.38,54.16,30.76,54.81,30.97,55.08,30.87,55.55,29.9,55.79,29.37,55.67,29.23,55.92,28.18,56.17,27.86,56.76,27.77,57.24,27.29,57.47,27.72,57.79,27.42,58.72,28.13,59.3,27.98,59.48,29.12,60.03,28.07,60.5,30.21,61.78,31.14,62.36,31.52,62.87,30.04,63.55,30.44,64.2,29.54,64.95,30.22,65.81,29.05,66.94,29.98,67.7,28.45,68.36,28.59,69.06,29.4,69.16,31.1,69.56,32.13,69.91,33.78,69.3,36.51,69.06,40.29,67.93,41.06,67.46,41.13,66.79,40.02,66.27,38.38,66,33.92,66.76,33.18,66.63,34.81,65.9,34.88,65.44,34.94,64.41,36.23,64.11,37.01,63.85,37.14,64.33,36.54,64.76,37.18,65.14,39.59,64.52,40.44,64.76,39.76,65.5,42.09,66.48,43.02,66.42,43.95,66.07,44.53,66.76,43.7,67.35,44.19,67.95,43.45,68.57,46.25,68.25,46.82,67.69,45.56,67.57,45.56,67.01,46.35,66.67,47.89,66.88,48.14,67.52,50.23,68,53.72,68.86,54.47,68.81,53.49,68.2,54.73,68.1,55.44,68.44,57.32,68.47,58.8,68.88,59.94,68.28,61.08,68.94,60.03,69.52,60.55,69.85,63.5,69.55,64.89,69.23,68.51,68.09,69.18,68.62,68.16,69.14,68.14,69.36,66.93,69.45,67.26,69.93,66.72,70.71,66.69,71.03,68.54,71.93,69.2,72.84,69.94,73.04,72.59,72.78,72.8,72.22,71.85,71.41,72.47,71.09,72.79,70.39,72.56,69.02,73.67,68.41,73.24,67.74,71.28,66.32,72.42,66.17,72.82,66.53,73.92,66.79,74.19,67.28,75.05,67.76,74.47,68.33,74.94,68.99,73.84,69.07,73.6,69.63,74.4,70.63,73.1,71.45,74.89,72.12,74.66,72.83,75.16,72.85,75.68,72.3,75.29,71.34,76.36,71.15,75.9,71.87,77.58,72.27,79.65,72.32,81.5,71.75,80.61,72.58,80.51,73.65,82.25,73.85,84.66,73.81,86.82,73.94,86.01,74.46,87.17,75.12,88.32,75.14,90.26,75.64,92.9,75.77,93.23,76.05,95.86,76.14,96.68,75.92,98.92,76.45,100.76,76.43,101.04,76.86,101.99,77.29,104.35,77.7,106.07,77.37,104.71,77.13,106.97,76.97,107.24,76.48,108.15,76.72,111.08,76.71,113.33,76.22,114.13,75.85,113.89,75.33,112.78,75.03,110.15,74.48,109.4,74.18,110.64,74.04,112.12,73.79,113.02,73.98,113.53,73.34,113.97,73.59,115.57,73.75,118.78,73.59,119.02,73.12,123.2,72.97,123.26,73.74,125.38,73.56,126.98,73.57,128.59,73.04,129.05,72.4,128.46,71.98,129.72,71.19,131.29,70.79,132.25,71.84,133.86,71.39,135.56,71.66,137.5,71.35,138.23,71.63,139.87,71.49,139.15,72.42,140.47,72.85,149.5,72.2,150.35,71.61,152.97,70.84,157.01,71.03,159,70.87,159.83,70.45,159.71,69.72,160.94,69.44,162.28,69.64,164.05,69.67,165.94,69.47,167.84,69.58,169.58,68.69,170.82,69.01,170.01,69.65,170.45,70.1,173.64,69.82,175.72,69.88,178.6,69.4,180,68.96,180,64.98,179.99,64.97,178.71,64.53,177.41,64.61,178.31,64.08,178.91,63.25,179.37,62.98,179.49,62.57,179.23,62.3,177.36,62.52,174.57,61.77,173.68,61.65,172.15,60.95,170.7,60.34,170.33,59.88,168.9,60.57,166.29,59.79,165.84,60.16,164.88,59.73,163.54,59.87,163.22,59.21,162.02,58.24,162.05,57.84,163.19,57.62,163.06,56.16,162.13,56.12,161.7,55.29,162.12,54.86,160.37,54.34,160.02,53.2,158.53,52.96,158.23,51.94,156.79,51.01,156.42,51.7,155.99,53.16,155.43,55.38,155.91,56.77,156.76,57.36,156.81,57.83,158.36,58.06,160.15,59.31,161.87,60.34,163.67,61.14,164.47,62.55,163.26,62.47,162.66,61.64,160.12,60.54,159.3,61.77,156.72,61.43,154.22,59.76,155.04,59.14,152.81,58.88,151.27,58.78,151.34,59.5,149.78,59.66,148.54,59.16,145.49,59.34,142.2,59.04,138.96,57.09,135.13,54.73,136.7,54.6,137.19,53.98,138.16,53.76,138.8,54.25,139.9,54.19,141.35,53.09,141.38,52.24,140.6,51.24,140.51,50.05,140.06,48.45,138.55,47,138.22,46.31,136.86,45.14,135.52,43.99,134.87,43.4,133.54,42.81,132.91,42.8,132.28,43.28,130.94,42.55,130.78,42.22,130.64,42.4,130.63,42.9,131.14,42.93,131.29,44.11,131.03,44.97,131.88,45.32,133.1,45.14,133.77,46.12,134.11,47.21,134.5,47.58,135.03,48.48,133.37,48.18,132.51,47.79,130.99,47.79,130.58,48.73,129.4,49.44,127.66,49.76,127.29,50.74,126.94,51.35,126.56,51.78,125.95,52.79,125.07,53.16,123.57,53.46,122.25,53.43,121,53.25,120.18,52.75,120.73,52.52,120.74,51.96,120.18,51.64,119.28,50.58,119.29,50.14,117.88,49.51,116.68,49.89,115.49,49.81,114.96,50.14,114.36,50.25,112.9,49.54,111.58,49.38,110.66,49.13,109.4,49.29,108.48,49.28,107.87,49.79,106.89,50.27,105.89,50.41,104.62,50.28,103.68,50.09,102.26,50.51,102.07,51.26,100.89,51.52,99.98,51.63,98.86,52.05,97.83,51.01,98.23,50.42,97.26,49.73,95.81,49.98,94.82,50.01,94.15,50.48,93.1,50.5,92.23,50.8,90.71,50.33,88.81,49.47,87.75,49.3,87.36,49.21,86.83,49.83,85.54,49.69,85.12,50.12,84.42,50.31,83.94,50.89,83.38,51.07,81.95,50.81,80.57,51.39,80.04,50.86,77.8,53.4,76.53,54.18,76.89,54.49,74.38,53.55,73.43,53.49,73.51,54.04,72.22,54.38,71.18,54.13,70.87,55.17,69.07,55.39,68.17,54.97,65.67,54.6,65.18,54.35,61.44,54.01,60.98,53.66,61.7,52.98,60.74,52.72,60.93,52.45,59.97,51.96,61.59,51.27,61.34,50.8,59.93,50.84,59.64,50.55,58.36,51.06,56.78,51.04,55.72,50.62,54.53,51.03,52.33,51.72,50.77,51.69,48.7,50.61,48.58,49.87,47.55,50.45,46.75,49.36,47.04,49.15,46.47,48.39,47.32,47.72,48.06,47.74,48.69,47.08,48.59,46.56,49.1,46.4]],[[93.78,81.02,95.94,81.25,97.88,80.75,100.19,79.78,99.94,78.88,97.76,78.76,94.97,79.04,93.31,79.43,92.55,80.14,91.18,80.34,93.78,81.02]],[[102.84,79.28,105.37,78.71,105.08,78.31,99.44,77.92,101.26,79.23,102.09,79.35,102.84,79.28]],[[138.83,76.14,141.47,76.09,145.09,75.56,144.3,74.82,140.61,74.85,138.96,74.61,136.97,75.26,137.51,75.95,138.83,76.14]],[[148.22,75.35,150.73,75.08,149.58,74.69,147.98,74.78,146.12,75.17,146.36,75.5,148.22,75.35]],[[139.86,73.37,140.81,73.77,142.06,73.86,143.48,73.48,143.6,73.21,142.09,73.21,140.04,73.32,139.86,73.37]],[[44.85,80.59,46.8,80.77,48.32,80.78,48.52,80.51,49.1,80.75,50.04,80.92,51.52,80.7,51.14,80.55,49.79,80.42,48.89,80.34,48.75,80.18,47.59,80.01,46.5,80.25,47.07,80.56,44.85,80.59]],[[22.73,54.33,20.89,54.31,19.66,54.43,19.89,54.87,21.27,55.19,22.32,55.02,22.76,54.86,22.65,54.58,22.73,54.33]],[[53.51,73.75,55.9,74.63,55.63,75.08,57.87,75.61,61.17,76.25,64.5,76.44,66.21,76.81,68.16,76.94,68.85,76.54,68.18,76.23,64.64,75.74,61.58,75.26,58.48,74.31,56.99,73.33,55.42,72.37,55.62,71.54,57.54,70.72,56.94,70.63,53.68,70.76,53.41,71.21,51.6,71.47,51.46,72.01,52.48,72.23,52.44,72.77,54.43,73.63,53.51,73.75]],[[142.91,53.7,143.26,52.74,143.24,51.76,143.65,50.75,144.65,48.98,143.17,49.31,142.56,47.86,143.53,46.84,143.51,46.14,142.75,46.74,142.09,45.97,141.91,46.81,142.02,47.78,141.9,48.86,142.14,49.62,142.18,50.95,141.59,51.94,141.68,53.3,142.61,53.76,142.21,54.23,142.65,54.37,142.91,53.7]],[[-174.93,67.21,-175.01,66.58,-174.34,66.34,-174.57,67.06,-171.86,66.91,-169.9,65.98,-170.89,65.54,-172.53,65.44,-172.55,64.46,-172.96,64.25,-173.89,64.28,-174.65,64.63,-175.98,64.92,-176.21,65.36,-177.22,65.52,-178.36,65.39,-178.9,65.74,-178.69,66.11,-179.88,65.87,-179.43,65.4,-180,64.98,-180,68.96,-177.55,68.2,-174.93,67.21]],[[-178.69,70.89,-180,70.83,-180,71.52,-179.87,71.56,-179.02,71.56,-177.58,71.27,-177.66,71.13,-178.69,70.89]],[[33.44,45.97,33.7,46.22,34.41,46.01,34.73,45.97,34.86,45.77,35.01,45.74,35.02,45.65,35.51,45.41,36.53,45.47,36.33,45.11,35.24,44.94,33.88,44.36,33.33,44.56,33.55,45.03,32.45,45.33,32.63,45.52,33.59,45.85,33.44,45.97]]]},{"n":"The Bahamas","c":"BS","r":4,"ml":4,"lx":-77.15,"ly":26.4,"b":[-78.98,23.71,-77,27.04],"p":[[[-78.98,26.79,-78.51,26.87,-77.85,26.84,-77.82,26.58,-78.91,26.42,-78.98,26.79]],[[-77.79,27.04,-77,26.59,-77.17,25.88,-77.36,26.01,-77.34,26.53,-77.79,26.93,-77.79,27.04]],[[-78.19,25.21,-77.89,25.17,-77.54,24.34,-77.53,23.76,-77.78,23.71,-78.03,24.29,-78.41,24.58,-78.19,25.21]]]},{"n":"Falkland Islands","c":"FK","r":5,"ml":4.5,"lx":-58.74,"ly":-51.61,"b":[-61.2,-52.3,-57.75,-51.1],"p":[[[-61.2,-51.85,-60,-51.25,-59.15,-51.5,-58.55,-51.1,-57.75,-51.55,-58.05,-51.9,-59.4,-52.2,-59.85,-51.85,-60.7,-52.3,-61.2,-51.85]]]},{"n":"Norway","c":"NO","r":3,"ml":3,"lx":9.68,"ly":61.36,"b":[4.99,58.08,31.29,80.66],"p":[[[15.14,79.67,15.52,80.02,16.99,80.05,18.25,79.7,21.54,78.96,19.03,78.56,18.47,77.83,17.59,77.64,17.12,76.81,15.91,76.77,13.76,77.38,14.67,77.74,13.17,78.02,11.22,78.87,10.44,79.65,13.17,80.01,13.72,79.66,15.14,79.67]],[[31.1,69.56,29.4,69.16,28.59,69.06,29.02,69.77,27.73,70.16,26.18,69.83,25.69,69.09,24.74,68.65,23.66,68.89,22.36,68.84,21.24,69.37,20.65,69.11,20.03,69.07,19.88,68.41,17.99,68.57,17.73,68.01,16.77,68.01,16.11,67.3,15.11,66.19,13.56,64.79,13.92,64.45,13.57,64.05,12.58,64.07,11.93,63.13,11.99,61.8,12.63,61.29,12.3,60.12,11.47,59.43,11.03,58.86,10.36,59.47,8.38,58.31,7.05,58.08,5.67,58.59,5.31,59.66,4.99,61.97,5.91,62.61,8.55,63.45,10.53,64.49,12.36,65.88,14.76,67.81,16.44,68.56,19.18,69.82,21.38,70.26,23.02,70.2,24.55,71.03,26.37,70.99,28.17,71.19,31.29,70.45,30.01,70.19,31.1,69.56]],[[27.41,80.06,25.92,79.52,23.02,79.4,20.08,79.57,19.9,79.84,18.46,79.86,17.37,80.32,20.46,80.6,21.91,80.36,22.92,80.66,25.45,80.41,27.41,80.06]],[[24.72,77.85,22.49,77.44,20.73,77.68,21.42,77.94,20.81,78.25,22.88,78.45,23.28,78.08,24.72,77.85]]]},{"n":"Greenland","c":"GL","r":3,"ml":1.7,"lx":-39.34,"ly":74.32,"b":[-73.3,60.04,-12.21,83.65],"p":[[[-46.76,82.63,-43.41,83.23,-39.9,83.18,-38.62,83.55,-35.09,83.65,-27.1,83.52,-20.85,82.73,-22.69,82.34,-26.52,82.3,-31.9,82.2,-31.4,82.02,-27.86,82.13,-24.84,81.79,-22.9,82.09,-22.07,81.73,-23.17,81.15,-20.62,81.52,-15.77,81.91,-12.77,81.72,-12.21,81.29,-16.29,80.58,-16.85,80.35,-20.05,80.18,-17.73,80.13,-18.9,79.4,-19.7,78.75,-19.67,77.64,-18.47,76.99,-20.04,76.94,-21.68,76.63,-19.83,76.1,-19.6,75.25,-20.67,75.16,-19.37,74.3,-21.59,74.22,-20.43,73.82,-20.76,73.46,-22.17,73.31,-23.57,73.31,-22.31,72.63,-22.3,72.18,-24.28,72.6,-24.79,72.33,-23.44,72.08,-22.13,71.47,-21.75,70.66,-23.54,70.47,-24.31,70.86,-25.54,71.43,-25.2,70.75,-26.36,70.23,-23.73,70.18,-22.35,70.13,-25.03,69.26,-27.75,68.47,-30.67,68.13,-31.78,68.12,-32.81,67.74,-34.2,66.68,-36.35,65.98,-37.04,65.94,-38.38,65.69,-39.81,65.46,-40.67,64.84,-40.68,64.14,-41.19,63.48,-42.82,62.68,-42.42,61.9,-42.87,61.07,-43.38,60.1,-44.79,60.04,-46.26,60.85,-48.26,60.86,-49.23,61.41,-49.9,62.38,-51.63,63.63,-52.14,64.28,-52.28,65.18,-53.66,66.1,-53.3,66.84,-53.97,67.19,-52.98,68.36,-51.48,68.73,-51.08,69.15,-50.87,69.93,-52.01,69.57,-52.56,69.43,-53.46,69.28,-54.68,69.61,-54.75,70.29,-54.36,70.82,-53.43,70.84,-51.39,70.57,-53.11,71.2,-54,71.55,-55,71.41,-55.83,71.65,-54.72,72.59,-55.33,72.96,-56.12,73.65,-57.32,74.71,-58.6,75.1,-58.59,75.52,-61.27,76.1,-63.39,76.18,-66.06,76.13,-68.5,76.06,-69.66,76.38,-71.4,77.01,-68.78,77.32,-66.76,77.38,-71.04,77.64,-73.3,78.04,-73.16,78.43,-69.37,78.91,-65.71,79.39,-65.32,79.76,-68.02,80.12,-67.15,80.52,-63.69,81.21,-62.23,81.32,-62.65,81.77,-60.28,82.03,-57.21,82.19,-54.13,82.2,-53.04,81.89,-50.39,82.44,-48,82.06,-46.6,81.99,-44.52,81.66,-46.9,82.2,-46.76,82.63]]]},{"n":"French Southern and Antarctic Lands","c":"TF","r":6,"ml":4,"lx":69.12,"ly":-49.3,"b":[68.72,-49.77,70.56,-48.62],"p":[[[68.94,-48.62,69.58,-48.94,70.53,-49.06,70.56,-49.25,70.28,-49.71,68.75,-49.77,68.72,-49.24,68.87,-48.83,68.94,-48.62]]]},{"n":"East Timor","c":"TL","r":5,"ml":4,"lx":125.85,"ly":-8.8,"b":[124.97,-9.39,127.34,-8.27],"p":[[[124.97,-8.89,125.09,-8.66,125.95,-8.43,126.64,-8.4,126.96,-8.27,127.34,-8.4,126.97,-8.67,125.93,-9.11,125.09,-9.39,125.07,-9.09,124.97,-8.89]]]},{"n":"South Africa","c":"ZA","r":2,"ml":1.7,"lx":23.67,"ly":-29.71,"b":[16.34,-34.82,32.83,-22.09],"p":[[[16.34,-28.58,16.82,-28.08,17.22,-28.36,17.39,-28.78,17.84,-28.86,18.46,-29.05,19,-28.97,19.89,-28.46,19.9,-24.77,20.17,-24.92,20.76,-25.87,20.67,-26.48,20.89,-26.83,21.61,-26.73,22.11,-26.28,22.58,-25.98,22.82,-25.5,23.31,-25.27,23.73,-25.39,24.21,-25.67,25.03,-25.72,25.66,-25.49,25.77,-25.17,25.94,-24.7,26.49,-24.62,26.79,-24.24,27.12,-23.57,28.02,-22.83,29.43,-22.09,29.84,-22.1,30.32,-22.27,30.66,-22.15,31.19,-22.25,31.67,-23.66,31.93,-24.37,31.75,-25.48,31.84,-25.84,31.33,-25.66,31.04,-25.73,30.95,-26.02,30.68,-26.4,30.69,-26.74,31.28,-27.29,31.87,-27.18,32.07,-26.73,32.83,-26.74,32.58,-27.47,32.46,-28.3,32.2,-28.75,31.52,-29.26,31.33,-29.4,30.9,-29.91,30.62,-30.42,30.06,-31.14,28.93,-32.17,28.22,-32.77,27.46,-33.23,26.42,-33.61,25.91,-33.67,25.78,-33.94,25.17,-33.8,24.68,-33.99,23.59,-33.79,22.99,-33.92,22.57,-33.86,21.54,-34.26,20.69,-34.42,20.07,-34.8,19.62,-34.82,19.19,-34.46,18.86,-34.44,18.42,-34,18.38,-34.14,18.24,-33.87,18.25,-33.28,17.93,-32.61,18.25,-32.43,18.22,-31.66,17.57,-30.73,17.06,-29.88,16.34,-28.58],[28.98,-28.96,28.54,-28.65,28.07,-28.85,27.53,-29.24,27,-29.88,27.75,-30.65,28.11,-30.55,28.29,-30.23,28.85,-30.07,29.02,-29.74,29.33,-29.26,28.98,-28.96]]]},{"n":"Lesotho","c":"LS","r":6,"ml":4,"lx":28.25,"ly":-29.48,"b":[27,-30.65,29.33,-28.65],"p":[[[28.98,-28.96,29.33,-29.26,29.02,-29.74,28.85,-30.07,28.29,-30.23,28.11,-30.55,27.75,-30.65,27,-29.88,27.53,-29.24,28.07,-28.85,28.54,-28.65,28.98,-28.96]]]},{"n":"Mexico","c":"MX","r":2,"ml":2,"lx":-102.29,"ly":23.92,"b":[-117.13,14.54,-86.81,32.72],"p":[[[-117.13,32.54,-115.99,32.61,-114.72,32.72,-114.81,32.53,-113.3,32.04,-111.02,31.33,-109.03,31.34,-108.24,31.34,-108.24,31.75,-106.51,31.75,-106.14,31.4,-105.63,31.08,-105.04,30.64,-104.71,30.12,-104.46,29.57,-103.94,29.27,-103.11,28.97,-102.48,29.76,-101.66,29.78,-100.96,29.38,-100.46,28.7,-100.11,28.11,-99.52,27.54,-99.3,26.84,-99.02,26.37,-98.24,26.06,-97.53,25.84,-97.14,25.87,-97.53,24.99,-97.7,24.27,-97.78,22.93,-97.87,22.44,-97.7,21.9,-97.39,21.41,-97.19,20.64,-96.53,19.89,-96.29,19.32,-95.9,18.83,-94.84,18.56,-94.43,18.14,-93.55,18.42,-92.79,18.52,-92.04,18.7,-91.41,18.88,-90.77,19.28,-90.53,19.87,-90.45,20.71,-90.28,21,-89.6,21.26,-88.54,21.49,-87.66,21.46,-87.05,21.54,-86.81,21.33,-86.85,20.85,-87.38,20.26,-87.62,19.65,-87.44,19.47,-87.59,19.04,-87.84,18.26,-88.09,18.52,-88.3,18.5,-88.49,18.49,-88.85,17.88,-89.03,18,-89.15,17.96,-89.14,17.81,-90.07,17.82,-91,17.82,-91,17.25,-91.45,17.25,-91.08,16.92,-90.71,16.69,-90.6,16.47,-90.44,16.41,-90.46,16.07,-91.75,16.07,-92.23,15.25,-92.09,15.06,-92.2,14.83,-92.23,14.54,-93.36,15.62,-93.88,15.94,-94.69,16.2,-95.25,16.13,-96.05,15.75,-96.56,15.65,-97.26,15.92,-98.01,16.11,-98.95,16.57,-99.7,16.71,-100.83,17.17,-101.67,17.65,-101.92,17.92,-102.48,17.98,-103.5,18.29,-103.92,18.75,-104.99,19.32,-105.49,19.95,-105.73,20.43,-105.4,20.53,-105.5,20.82,-105.27,21.08,-105.27,21.42,-105.6,21.87,-105.69,22.27,-106.03,22.77,-106.91,23.77,-107.92,24.55,-108.4,25.17,-109.26,25.58,-109.44,25.82,-109.29,26.44,-109.8,26.68,-110.39,27.16,-110.64,27.86,-111.18,27.94,-111.76,28.47,-112.23,28.95,-112.27,29.27,-112.81,30.02,-113.16,30.79,-113.15,31.17,-113.87,31.57,-114.21,31.52,-114.78,31.8,-114.94,31.39,-114.77,30.91,-114.67,30.16,-114.33,29.75,-113.59,29.06,-113.42,28.83,-113.27,28.75,-113.14,28.41,-112.96,28.43,-112.76,27.78,-112.46,27.53,-112.24,27.17,-111.62,26.66,-111.28,25.73,-110.99,25.29,-110.71,24.83,-110.66,24.3,-110.17,24.27,-109.77,23.81,-109.41,23.36,-109.43,23.19,-109.85,22.82,-110.03,22.82,-110.3,23.43,-110.95,24,-111.67,24.48,-112.18,24.74,-112.15,25.47,-112.3,26.01,-112.78,26.32,-113.46,26.77,-113.6,26.64,-113.85,26.9,-114.47,27.14,-115.06,27.72,-114.98,27.8,-114.57,27.74,-114.2,28.12,-114.16,28.57,-114.93,29.28,-115.52,29.56,-115.89,30.18,-116.26,30.84,-116.72,31.64,-117.13,32.54]]]},{"n":"Uruguay","c":"UY","r":4,"ml":3,"lx":-55.97,"ly":-32.96,"b":[-58.43,-34.95,-53.21,-30.11],"p":[[[-57.63,-30.22,-56.98,-30.11,-55.97,-30.88,-55.6,-30.85,-54.57,-31.49,-53.79,-32.05,-53.21,-32.73,-53.65,-33.2,-53.37,-33.77,-53.81,-34.4,-54.94,-34.95,-55.67,-34.75,-56.22,-34.86,-57.14,-34.43,-57.82,-34.46,-58.43,-33.91,-58.35,-33.26,-58.13,-33.04,-58.14,-32.04,-57.87,-31.02,-57.63,-30.22]]]},{"n":"Brazil","c":"BR","r":2,"ml":1.7,"lx":-49.56,"ly":-12.1,"b":[-73.99,-33.77,-34.73,5.24],"p":[[[-53.37,-33.77,-53.65,-33.2,-53.21,-32.73,-53.79,-32.05,-54.57,-31.49,-55.6,-30.85,-55.97,-30.88,-56.98,-30.11,-57.63,-30.22,-56.29,-28.85,-55.16,-27.88,-54.49,-27.47,-53.65,-26.92,-53.63,-26.12,-54.13,-25.55,-54.63,-25.74,-54.43,-25.16,-54.29,-24.57,-54.29,-24.02,-54.65,-23.84,-55.03,-24,-55.4,-23.96,-55.52,-23.57,-55.61,-22.66,-55.8,-22.36,-56.47,-22.09,-56.88,-22.28,-57.94,-22.09,-57.87,-20.73,-58.17,-20.18,-57.85,-19.97,-57.95,-19.4,-57.68,-18.96,-57.5,-18.17,-57.73,-17.55,-58.28,-17.27,-58.39,-16.88,-58.24,-16.3,-60.16,-16.26,-60.54,-15.09,-60.25,-15.08,-60.26,-14.65,-60.46,-14.35,-60.5,-13.78,-61.08,-13.48,-61.71,-13.49,-62.13,-13.2,-62.8,-13,-63.2,-12.63,-64.32,-12.46,-65.4,-11.57,-65.32,-10.9,-65.44,-10.51,-65.34,-9.76,-66.65,-9.93,-67.17,-10.31,-68.05,-10.71,-68.27,-11.01,-68.79,-11.04,-69.53,-10.95,-70.09,-11.12,-70.55,-11.01,-70.48,-9.49,-71.3,-10.08,-72.18,-10.05,-72.56,-9.52,-73.23,-9.46,-73.02,-9.03,-73.57,-8.42,-73.99,-7.52,-73.72,-7.34,-73.72,-6.92,-73.12,-6.63,-73.22,-6.09,-72.96,-5.74,-72.89,-5.27,-71.75,-4.59,-70.93,-4.4,-70.79,-4.25,-69.89,-4.3,-69.44,-1.56,-69.42,-1.12,-69.58,-0.55,-70.02,-0.19,-70.02,0.54,-69.45,0.71,-69.25,0.6,-69.22,0.99,-69.8,1.09,-69.82,1.71,-67.87,1.69,-67.54,2.04,-67.26,1.72,-67.07,1.13,-66.88,1.25,-66.33,0.72,-65.55,0.79,-65.35,1.1,-64.61,1.33,-64.2,1.49,-64.08,1.92,-63.37,2.2,-63.42,2.41,-64.27,2.5,-64.41,3.13,-64.37,3.8,-64.82,4.06,-64.63,4.15,-63.89,4.02,-63.09,3.77,-62.8,4.01,-62.09,4.16,-60.97,4.54,-60.6,4.92,-60.73,5.2,-60.21,5.24,-59.98,5.01,-60.11,4.57,-59.77,4.42,-59.54,3.96,-59.82,3.61,-59.97,2.76,-59.72,2.25,-59.65,1.79,-59.03,1.32,-58.54,1.27,-58.43,1.46,-58.11,1.51,-57.66,1.68,-57.34,1.95,-56.78,1.86,-56.54,1.9,-56,1.82,-55.91,2.02,-56.07,2.22,-55.97,2.51,-55.57,2.42,-55.1,2.52,-54.52,2.31,-54.09,2.11,-53.78,2.38,-53.55,2.33,-53.42,2.05,-52.94,2.12,-52.56,2.5,-52.25,3.24,-51.66,4.16,-51.32,4.2,-51.07,3.65,-50.51,1.9,-49.97,1.74,-49.95,1.05,-50.7,0.22,-50.39,-0.08,-48.62,-0.24,-48.58,-1.24,-47.82,-0.58,-46.57,-0.94,-44.91,-1.55,-44.42,-2.14,-44.58,-2.69,-43.42,-2.38,-41.47,-2.91,-39.98,-2.87,-38.5,-3.7,-37.22,-4.82,-36.45,-5.11,-35.6,-5.15,-35.24,-5.46,-34.9,-6.74,-34.73,-7.34,-35.13,-9,-35.64,-9.65,-37.05,-11.04,-37.68,-12.17,-38.42,-13.04,-38.67,-13.06,-38.95,-13.79,-38.88,-15.67,-39.16,-17.21,-39.27,-17.87,-39.58,-18.26,-39.76,-19.6,-40.77,-20.9,-40.94,-21.94,-41.75,-22.37,-41.99,-22.97,-43.07,-22.97,-44.65,-23.35,-45.35,-23.8,-46.47,-24.09,-47.65,-24.89,-48.5,-25.88,-48.64,-26.62,-48.47,-27.18,-48.66,-28.19,-48.89,-28.67,-49.59,-29.22,-50.7,-30.98,-51.58,-31.78,-52.26,-32.25,-52.71,-33.2,-53.37,-33.77]]]},{"n":"Bolivia","c":"BO","r":3,"ml":3,"lx":-64.59,"ly":-16.67,"b":[-69.59,-22.87,-57.5,-9.76],"p":[[[-69.53,-10.95,-68.79,-11.04,-68.27,-11.01,-68.05,-10.71,-67.17,-10.31,-66.65,-9.93,-65.34,-9.76,-65.44,-10.51,-65.32,-10.9,-65.4,-11.57,-64.32,-12.46,-63.2,-12.63,-62.8,-13,-62.13,-13.2,-61.71,-13.49,-61.08,-13.48,-60.5,-13.78,-60.46,-14.35,-60.26,-14.65,-60.25,-15.08,-60.54,-15.09,-60.16,-16.26,-58.24,-16.3,-58.39,-16.88,-58.28,-17.27,-57.73,-17.55,-57.5,-18.17,-57.68,-18.96,-57.95,-19.4,-57.85,-19.97,-58.17,-20.18,-58.18,-19.87,-59.12,-19.36,-60.04,-19.34,-61.79,-19.63,-62.27,-20.51,-62.29,-21.05,-62.69,-22.25,-62.85,-22.03,-63.99,-21.99,-64.38,-22.8,-64.96,-22.08,-66.27,-21.83,-67.11,-22.74,-67.83,-22.87,-68.22,-21.49,-68.76,-20.37,-68.44,-19.41,-68.97,-18.98,-69.1,-18.26,-69.59,-17.58,-68.96,-16.5,-69.39,-15.66,-69.16,-15.32,-69.34,-14.95,-68.95,-14.45,-68.93,-13.6,-68.88,-12.9,-68.67,-12.56,-69.53,-10.95]]]},{"n":"Peru","c":"PE","r":2,"ml":2,"lx":-72.9,"ly":-12.98,"b":[-81.41,-18.35,-68.67,-0.06],"p":[[[-69.89,-4.3,-70.79,-4.25,-70.93,-4.4,-71.75,-4.59,-72.89,-5.27,-72.96,-5.74,-73.22,-6.09,-73.12,-6.63,-73.72,-6.92,-73.72,-7.34,-73.99,-7.52,-73.57,-8.42,-73.02,-9.03,-73.23,-9.46,-72.56,-9.52,-72.18,-10.05,-71.3,-10.08,-70.48,-9.49,-70.55,-11.01,-70.09,-11.12,-69.53,-10.95,-68.67,-12.56,-68.88,-12.9,-68.93,-13.6,-68.95,-14.45,-69.34,-14.95,-69.16,-15.32,-69.39,-15.66,-68.96,-16.5,-69.59,-17.58,-69.86,-18.09,-70.37,-18.35,-71.38,-17.77,-71.46,-17.36,-73.44,-16.36,-75.24,-15.27,-76.01,-14.65,-76.42,-13.82,-76.26,-13.54,-77.11,-12.22,-78.09,-10.38,-79.04,-8.39,-79.45,-7.93,-79.76,-7.19,-80.54,-6.54,-81.25,-6.14,-80.93,-5.69,-81.41,-4.74,-81.1,-4.04,-80.3,-3.4,-80.18,-3.82,-80.47,-4.06,-80.44,-4.43,-80.03,-4.35,-79.62,-4.45,-79.21,-4.96,-78.64,-4.55,-78.45,-3.87,-77.84,-3,-76.64,-2.61,-75.54,-1.56,-75.23,-0.91,-75.37,-0.15,-75.11,-0.06,-74.44,-0.53,-74.12,-1,-73.66,-1.26,-73.07,-2.31,-72.33,-2.43,-71.77,-2.17,-71.41,-2.34,-70.81,-2.26,-70.05,-2.73,-70.69,-3.74,-70.39,-3.77,-69.89,-4.3]]]},{"n":"Colombia","c":"CO","r":2,"ml":3,"lx":-73.17,"ly":3.37,"b":[-78.99,-4.3,-66.88,12.44],"p":[[[-66.88,1.25,-67.07,1.13,-67.26,1.72,-67.54,2.04,-67.87,1.69,-69.82,1.71,-69.8,1.09,-69.22,0.99,-69.25,0.6,-69.45,0.71,-70.02,0.54,-70.02,-0.19,-69.58,-0.55,-69.42,-1.12,-69.44,-1.56,-69.89,-4.3,-70.39,-3.77,-70.69,-3.74,-70.05,-2.73,-70.81,-2.26,-71.41,-2.34,-71.77,-2.17,-72.33,-2.43,-73.07,-2.31,-73.66,-1.26,-74.12,-1,-74.44,-0.53,-75.11,-0.06,-75.37,-0.15,-75.8,0.08,-76.29,0.42,-76.58,0.26,-77.42,0.4,-77.67,0.83,-77.86,0.81,-78.86,1.38,-78.99,1.69,-78.62,1.77,-78.66,2.27,-78.43,2.63,-77.93,2.7,-77.51,3.33,-77.13,3.85,-77.5,4.09,-77.31,4.67,-77.53,5.58,-77.32,5.85,-77.48,6.69,-77.88,7.22,-77.75,7.71,-77.43,7.64,-77.24,7.94,-77.47,8.52,-77.35,8.67,-76.84,8.64,-76.09,9.34,-75.67,9.44,-75.66,9.77,-75.48,10.62,-74.91,11.08,-74.28,11.1,-74.2,11.31,-73.41,11.23,-72.63,11.73,-72.24,11.96,-71.75,12.44,-71.4,12.38,-71.14,12.11,-71.33,11.78,-71.97,11.61,-72.23,11.11,-72.61,10.82,-72.91,10.45,-73.03,9.74,-73.3,9.15,-72.79,9.09,-72.66,8.63,-72.44,8.41,-72.36,8,-72.48,7.63,-72.44,7.42,-72.2,7.34,-71.96,6.99,-70.67,7.09,-70.09,6.96,-69.39,6.1,-68.99,6.21,-68.27,6.15,-67.7,6.27,-67.34,6.1,-67.52,5.56,-67.74,5.22,-67.82,4.5,-67.62,3.84,-67.34,3.54,-67.3,3.32,-67.81,2.82,-67.45,2.6,-67.18,2.25,-66.88,1.25]]]},{"n":"Panama","c":"PA","r":4,"ml":4,"lx":-80.35,"ly":8.72,"b":[-82.97,7.22,-77.24,9.61],"p":[[[-77.35,8.67,-77.47,8.52,-77.24,7.94,-77.43,7.64,-77.75,7.71,-77.88,7.22,-78.21,7.51,-78.43,8.05,-78.18,8.32,-78.44,8.39,-78.62,8.72,-79.12,9,-79.56,8.93,-79.76,8.58,-80.16,8.33,-80.38,8.3,-80.48,8.09,-80,7.55,-80.28,7.42,-80.42,7.27,-80.89,7.22,-81.06,7.82,-81.19,7.65,-81.52,7.71,-81.72,8.11,-82.13,8.18,-82.39,8.29,-82.82,8.29,-82.85,8.07,-82.97,8.23,-82.91,8.42,-82.83,8.63,-82.87,8.81,-82.72,8.93,-82.93,9.07,-82.93,9.48,-82.55,9.57,-82.19,9.21,-82.21,9,-81.81,8.95,-81.71,9.03,-81.44,8.79,-80.95,8.86,-80.52,9.11,-79.91,9.31,-79.57,9.61,-79.02,9.55,-79.06,9.45,-78.5,9.42,-78.06,9.25,-77.73,8.95,-77.35,8.67]]]},{"n":"Costa Rica","c":"CR","r":5,"ml":2.5,"lx":-84.08,"ly":10.07,"b":[-85.94,8.23,-82.55,11.22],"p":[[[-82.55,9.57,-82.93,9.48,-82.93,9.07,-82.72,8.93,-82.87,8.81,-82.83,8.63,-82.91,8.42,-82.97,8.23,-83.51,8.45,-83.71,8.66,-83.6,8.83,-83.63,9.05,-83.91,9.29,-84.3,9.49,-84.65,9.62,-84.71,9.91,-84.98,10.09,-84.91,9.8,-85.11,9.56,-85.34,9.83,-85.66,9.93,-85.8,10.13,-85.79,10.44,-85.66,10.75,-85.94,10.9,-85.71,11.09,-85.56,11.22,-84.9,10.95,-84.67,11.08,-84.36,11,-84.19,10.79,-83.9,10.73,-83.66,10.94,-83.4,10.4,-83.02,9.99,-82.55,9.57]]]},{"n":"Nicaragua","c":"NI","r":5,"ml":4,"lx":-85.07,"ly":12.67,"b":[-87.67,10.73,-83.15,15.02],"p":[[[-83.66,10.94,-83.9,10.73,-84.19,10.79,-84.36,11,-84.67,11.08,-84.9,10.95,-85.56,11.22,-85.71,11.09,-86.06,11.4,-86.53,11.81,-86.75,12.14,-87.17,12.46,-87.67,12.91,-87.56,13.06,-87.39,12.91,-87.32,12.98,-87.01,13.03,-86.88,13.25,-86.73,13.26,-86.76,13.75,-86.52,13.78,-86.31,13.77,-86.1,14.04,-85.8,13.84,-85.7,13.96,-85.51,14.08,-85.17,14.35,-85.15,14.56,-85.05,14.55,-84.92,14.79,-84.82,14.82,-84.65,14.67,-84.45,14.62,-84.23,14.75,-83.98,14.75,-83.63,14.88,-83.49,15.02,-83.15,15,-83.23,14.9,-83.28,14.68,-83.18,14.31,-83.41,13.97,-83.52,13.57,-83.55,13.13,-83.5,12.87,-83.47,12.42,-83.63,12.32,-83.72,11.89,-83.65,11.63,-83.86,11.37,-83.81,11.1,-83.66,10.94]]]},{"n":"Honduras","c":"HN","r":5,"ml":4.5,"lx":-86.89,"ly":14.79,"b":[-89.35,12.98,-83.15,16.01],"p":[[[-83.15,15,-83.49,15.02,-83.63,14.88,-83.98,14.75,-84.23,14.75,-84.45,14.62,-84.65,14.67,-84.82,14.82,-84.92,14.79,-85.05,14.55,-85.15,14.56,-85.17,14.35,-85.51,14.08,-85.7,13.96,-85.8,13.84,-86.1,14.04,-86.31,13.77,-86.52,13.78,-86.76,13.75,-86.73,13.26,-86.88,13.25,-87.01,13.03,-87.32,12.98,-87.49,13.3,-87.79,13.38,-87.72,13.79,-87.86,13.89,-88.07,13.96,-88.5,13.85,-88.54,13.98,-88.84,14.14,-89.06,14.34,-89.35,14.42,-89.15,14.68,-89.23,14.87,-89.15,15.07,-88.68,15.35,-88.23,15.73,-88.12,15.69,-87.9,15.86,-87.62,15.88,-87.52,15.8,-87.37,15.85,-86.9,15.76,-86.44,15.78,-86.12,15.89,-86,16.01,-85.68,15.95,-85.44,15.89,-85.18,15.91,-84.98,16,-84.53,15.86,-84.37,15.84,-84.06,15.65,-83.77,15.42,-83.41,15.27,-83.15,15]]]},{"n":"El Salvador","c":"SV","r":6,"ml":5,"lx":-88.89,"ly":13.69,"b":[-90.1,13.15,-87.72,14.42],"p":[[[-89.35,14.42,-89.06,14.34,-88.84,14.14,-88.54,13.98,-88.5,13.85,-88.07,13.96,-87.86,13.89,-87.72,13.79,-87.79,13.38,-87.9,13.15,-88.48,13.16,-88.84,13.26,-89.26,13.46,-89.81,13.52,-90.1,13.74,-90.06,13.88,-89.72,14.13,-89.53,14.24,-89.59,14.36,-89.35,14.42]]]},{"n":"Guatemala","c":"GT","r":3,"ml":3,"lx":-90.5,"ly":14.98,"b":[-92.23,13.74,-88.23,17.82],"p":[[[-92.23,14.54,-92.2,14.83,-92.09,15.06,-92.23,15.25,-91.75,16.07,-90.46,16.07,-90.44,16.41,-90.6,16.47,-90.71,16.69,-91.08,16.92,-91.45,17.25,-91,17.25,-91,17.82,-90.07,17.82,-89.14,17.81,-89.15,17.02,-89.23,15.89,-88.93,15.89,-88.6,15.71,-88.52,15.86,-88.23,15.73,-88.68,15.35,-89.15,15.07,-89.23,14.87,-89.15,14.68,-89.35,14.42,-89.59,14.36,-89.53,14.24,-89.72,14.13,-90.06,13.88,-90.1,13.74,-90.61,13.91,-91.23,13.93,-91.69,14.13,-92.23,14.54]]]},{"n":"Belize","c":"BZ","r":6,"ml":5,"lx":-88.71,"ly":17.2,"b":[-89.23,15.89,-88.11,18.5],"p":[[[-89.14,17.81,-89.15,17.96,-89.03,18,-88.85,17.88,-88.49,18.49,-88.3,18.5,-88.3,18.35,-88.11,18.35,-88.12,18.08,-88.29,17.64,-88.2,17.49,-88.3,17.13,-88.24,17.04,-88.36,16.53,-88.55,16.27,-88.73,16.23,-88.93,15.89,-89.23,15.89,-89.15,17.02,-89.14,17.81]]]},{"n":"Venezuela","c":"VE","r":3,"ml":2.5,"lx":-64.6,"ly":7.18,"b":[-73.3,0.72,-59.76,12.16],"p":[[[-60.73,5.2,-60.6,4.92,-60.97,4.54,-62.09,4.16,-62.8,4.01,-63.09,3.77,-63.89,4.02,-64.63,4.15,-64.82,4.06,-64.37,3.8,-64.41,3.13,-64.27,2.5,-63.42,2.41,-63.37,2.2,-64.08,1.92,-64.2,1.49,-64.61,1.33,-65.35,1.1,-65.55,0.79,-66.33,0.72,-66.88,1.25,-67.18,2.25,-67.45,2.6,-67.81,2.82,-67.3,3.32,-67.34,3.54,-67.62,3.84,-67.82,4.5,-67.74,5.22,-67.52,5.56,-67.34,6.1,-67.7,6.27,-68.27,6.15,-68.99,6.21,-69.39,6.1,-70.09,6.96,-70.67,7.09,-71.96,6.99,-72.2,7.34,-72.44,7.42,-72.48,7.63,-72.36,8,-72.44,8.41,-72.66,8.63,-72.79,9.09,-73.3,9.15,-73.03,9.74,-72.91,10.45,-72.61,10.82,-72.23,11.11,-71.97,11.61,-71.33,11.78,-71.36,11.54,-71.95,11.42,-71.62,10.97,-71.63,10.45,-72.07,9.87,-71.7,9.07,-71.26,9.14,-71.04,9.86,-71.35,10.21,-71.4,10.97,-70.16,11.38,-70.29,11.85,-69.94,12.16,-69.58,11.46,-68.88,11.44,-68.23,10.89,-68.19,10.55,-67.3,10.55,-66.23,10.65,-65.66,10.2,-64.89,10.08,-64.33,10.39,-64.32,10.64,-63.08,10.7,-61.88,10.72,-62.73,10.42,-62.39,9.95,-61.59,9.87,-60.83,9.38,-60.67,8.58,-60.15,8.6,-59.76,8.37,-60.55,7.78,-60.64,7.42,-60.3,7.04,-60.54,6.86,-61.16,6.7,-61.14,6.23,-61.41,5.96,-60.73,5.2]]]},{"n":"Guyana","c":"GY","r":4,"ml":4,"lx":-58.94,"ly":5.12,"b":[-61.41,1.27,-56.54,8.37],"p":[[[-56.54,1.9,-56.78,1.86,-57.34,1.95,-57.66,1.68,-58.11,1.51,-58.43,1.46,-58.54,1.27,-59.03,1.32,-59.65,1.79,-59.72,2.25,-59.97,2.76,-59.82,3.61,-59.54,3.96,-59.77,4.42,-60.11,4.57,-59.98,5.01,-60.21,5.24,-60.73,5.2,-61.41,5.96,-61.14,6.23,-61.16,6.7,-60.54,6.86,-60.3,7.04,-60.64,7.42,-60.55,7.78,-59.76,8.37,-59.1,8,-58.48,7.35,-58.45,6.83,-58.08,6.81,-57.54,6.32,-57.15,5.97,-57.31,5.07,-57.91,4.81,-57.86,4.58,-58.04,4.06,-57.6,3.33,-57.28,3.33,-57.15,2.77,-56.54,1.9]]]},{"n":"Suriname","c":"SR","r":4,"ml":4,"lx":-55.91,"ly":4.14,"b":[-58.04,1.82,-53.96,6.03],"p":[[[-54.52,2.31,-55.1,2.52,-55.57,2.42,-55.97,2.51,-56.07,2.22,-55.91,2.02,-56,1.82,-56.54,1.9,-57.15,2.77,-57.28,3.33,-57.6,3.33,-58.04,4.06,-57.86,4.58,-57.91,4.81,-57.31,5.07,-57.15,5.97,-55.95,5.77,-55.84,5.95,-55.03,6.03,-53.96,5.76,-54.48,4.9,-54.4,4.21,-54.01,3.62,-54.18,3.19,-54.27,2.73,-54.52,2.31]]]},{"n":"France","c":"FR","r":2,"ml":1.7,"lx":2.55,"ly":46.7,"b":[-54.52,2.05,9.56,51.15],"p":[[[-51.66,4.16,-52.25,3.24,-52.56,2.5,-52.94,2.12,-53.42,2.05,-53.55,2.33,-53.78,2.38,-54.09,2.11,-54.52,2.31,-54.27,2.73,-54.18,3.19,-54.01,3.62,-54.4,4.21,-54.48,4.9,-53.96,5.76,-53.62,5.65,-52.88,5.41,-51.82,4.57,-51.66,4.16]],[[6.19,49.46,6.66,49.2,8.1,49.02,7.59,48.33,7.47,47.62,7.19,47.45,6.74,47.54,6.77,47.29,6.04,46.73,6.02,46.27,6.5,46.43,6.84,45.99,6.8,45.71,7.1,45.33,6.75,45.03,7.01,44.25,7.55,44.13,7.44,43.69,6.53,43.13,4.56,43.4,3.1,43.08,2.99,42.47,1.83,42.34,0.7,42.8,0.34,42.58,-1.5,43.03,-1.9,43.42,-1.38,44.02,-1.19,46.01,-2.23,47.06,-2.96,47.57,-4.49,47.95,-4.59,48.68,-3.3,48.9,-1.62,48.64,-1.93,49.78,-0.99,49.35,1.34,50.13,1.64,50.95,2.51,51.15,2.66,50.8,3.12,50.78,3.59,50.38,4.29,49.91,4.8,49.99,5.67,49.53,5.9,49.44,6.19,49.46]],[[8.75,42.63,9.39,43.01,9.56,42.15,9.23,41.38,8.78,41.58,8.54,42.26,8.75,42.63]]]},{"n":"Ecuador","c":"EC","r":3,"ml":3,"lx":-78.19,"ly":-1.26,"b":[-80.97,-4.96,-75.23,1.38],"p":[[[-75.37,-0.15,-75.23,-0.91,-75.54,-1.56,-76.64,-2.61,-77.84,-3,-78.45,-3.87,-78.64,-4.55,-79.21,-4.96,-79.62,-4.45,-80.03,-4.35,-80.44,-4.43,-80.47,-4.06,-80.18,-3.82,-80.3,-3.4,-79.77,-2.66,-79.99,-2.22,-80.37,-2.69,-80.97,-2.25,-80.76,-1.97,-80.93,-1.06,-80.58,-0.91,-80.4,-0.28,-80.02,0.36,-80.09,0.77,-79.54,0.98,-78.86,1.38,-77.86,0.81,-77.67,0.83,-77.42,0.4,-76.58,0.26,-76.29,0.42,-75.8,0.08,-75.37,-0.15]]]},{"n":"Puerto Rico","c":"PR","r":5,"ml":3,"lx":-66.48,"ly":18.23,"b":[-67.24,17.95,-65.59,18.52],"p":[[[-66.28,18.51,-65.77,18.43,-65.59,18.23,-65.85,17.98,-66.6,17.98,-67.18,17.95,-67.24,18.37,-67.1,18.52,-66.28,18.51]]]},{"n":"Jamaica","c":"JM","r":4,"ml":4,"lx":-77.32,"ly":18.14,"b":[-78.34,17.7,-76.2,18.52],"p":[[[-77.57,18.49,-76.9,18.4,-76.37,18.16,-76.2,17.89,-76.9,17.87,-77.21,17.7,-77.77,17.86,-78.34,18.23,-78.22,18.45,-77.8,18.52,-77.57,18.49]]]},{"n":"Cuba","c":"CU","r":3,"ml":2.7,"lx":-77.98,"ly":21.33,"b":[-84.97,19.86,-74.18,23.19],"p":[[[-82.27,23.19,-81.4,23.12,-80.62,23.11,-79.68,22.77,-79.28,22.4,-78.35,22.51,-77.99,22.28,-77.15,21.66,-76.52,21.21,-76.19,21.22,-75.6,21.02,-75.67,20.74,-74.93,20.69,-74.18,20.28,-74.3,20.05,-74.96,19.92,-75.63,19.87,-76.32,19.95,-77.76,19.86,-77.09,20.41,-77.49,20.67,-78.14,20.74,-78.48,21.03,-78.72,21.6,-79.28,21.56,-80.22,21.83,-80.52,22.04,-81.82,22.19,-82.17,22.39,-81.8,22.64,-82.78,22.69,-83.49,22.17,-83.91,22.15,-84.05,21.91,-84.55,21.8,-84.97,21.9,-84.45,22.2,-84.23,22.57,-83.78,22.79,-83.27,22.98,-82.51,23.08,-82.27,23.19]]]},{"n":"Zimbabwe","c":"ZW","r":3,"ml":2.5,"lx":29.93,"ly":-18.91,"b":[25.26,-22.27,32.85,-15.51],"p":[[[31.19,-22.25,30.66,-22.15,30.32,-22.27,29.84,-22.1,29.43,-22.09,28.79,-21.64,28.02,-21.49,27.73,-20.85,27.72,-20.5,27.3,-20.39,26.16,-19.29,25.85,-18.71,25.65,-18.54,25.26,-17.74,26.38,-17.85,26.71,-17.96,27.04,-17.94,27.6,-17.29,28.47,-16.47,28.83,-16.39,28.95,-16.04,29.52,-15.64,30.27,-15.51,30.34,-15.88,31.17,-15.86,31.64,-16.07,31.85,-16.32,32.33,-16.39,32.85,-16.71,32.85,-17.98,32.65,-18.67,32.61,-19.42,32.77,-19.72,32.66,-20.3,32.51,-20.4,32.24,-21.12,31.19,-22.25]]]},{"n":"Botswana","c":"BW","r":4,"ml":4,"lx":24.18,"ly":-22.1,"b":[19.9,-26.83,29.43,-17.66],"p":[[[29.43,-22.09,28.02,-22.83,27.12,-23.57,26.79,-24.24,26.49,-24.62,25.94,-24.7,25.77,-25.17,25.66,-25.49,25.03,-25.72,24.21,-25.67,23.73,-25.39,23.31,-25.27,22.82,-25.5,22.58,-25.98,22.11,-26.28,21.61,-26.73,20.89,-26.83,20.67,-26.48,20.76,-25.87,20.17,-24.92,19.9,-24.77,19.9,-21.85,20.88,-21.81,20.91,-18.25,21.66,-18.22,23.2,-17.87,23.58,-18.28,24.22,-17.89,24.52,-17.89,25.08,-17.66,25.26,-17.74,25.65,-18.54,25.85,-18.71,26.16,-19.29,27.3,-20.39,27.72,-20.5,27.73,-20.85,28.02,-21.49,28.79,-21.64,29.43,-22.09]]]},{"n":"Namibia","c":"NA","r":3,"ml":3,"lx":17.11,"ly":-20.58,"b":[11.73,-29.05,25.08,-16.94],"p":[[[19.9,-24.77,19.89,-28.46,19,-28.97,18.46,-29.05,17.84,-28.86,17.39,-28.78,17.22,-28.36,16.82,-28.08,16.34,-28.58,15.6,-27.82,15.21,-27.09,14.99,-26.12,14.74,-25.39,14.41,-23.85,14.39,-22.66,14.26,-22.11,13.87,-21.7,13.35,-20.87,12.83,-19.67,12.61,-19.05,11.79,-18.07,11.73,-17.3,12.22,-17.11,12.81,-16.94,13.46,-16.97,14.06,-17.42,14.21,-17.35,18.26,-17.31,18.96,-17.79,21.38,-17.93,23.22,-17.52,24.03,-17.3,24.68,-17.35,25.08,-17.58,25.08,-17.66,24.52,-17.89,24.22,-17.89,23.58,-18.28,23.2,-17.87,21.66,-18.22,20.91,-18.25,20.88,-21.81,19.9,-21.85,19.9,-24.77]]]},{"n":"Senegal","c":"SN","r":3,"ml":2.7,"lx":-14.78,"ly":15.14,"b":[-17.63,12.33,-11.47,16.6],"p":[[[-16.71,13.59,-17.13,14.37,-17.63,14.73,-17.19,14.92,-16.7,15.62,-16.46,16.14,-16.12,16.46,-15.62,16.37,-15.14,16.59,-14.58,16.6,-14.1,16.3,-13.44,16.04,-12.83,15.3,-12.17,14.62,-12.12,13.99,-11.93,13.42,-11.55,13.14,-11.47,12.75,-11.51,12.44,-11.66,12.39,-12.2,12.47,-12.28,12.35,-12.5,12.33,-13.22,12.58,-13.7,12.59,-15.55,12.63,-15.82,12.52,-16.15,12.55,-16.68,12.38,-16.84,13.15,-15.93,13.13,-15.69,13.27,-15.51,13.28,-15.14,13.51,-14.71,13.3,-14.28,13.28,-13.84,13.51,-14.05,13.79,-14.38,13.63,-14.69,13.63,-15.08,13.88,-15.4,13.86,-15.62,13.62,-16.71,13.59]]]},{"n":"Mali","c":"ML","r":3,"ml":3,"lx":-2.04,"ly":18.69,"b":[-12.17,10.1,4.27,24.97],"p":[[[-11.51,12.44,-11.47,12.75,-11.55,13.14,-11.93,13.42,-12.12,13.99,-12.17,14.62,-11.83,14.8,-11.67,15.39,-11.35,15.41,-10.65,15.13,-10.09,15.33,-9.7,15.26,-9.55,15.49,-5.54,15.5,-5.32,16.2,-5.49,16.33,-5.97,20.64,-6.45,24.96,-4.92,24.97,-1.55,22.79,1.82,20.61,2.06,20.14,2.68,19.86,3.15,19.69,3.16,19.06,4.27,19.16,4.27,16.85,3.72,16.18,3.64,15.57,2.75,15.41,1.39,15.32,1.02,14.97,0.37,14.93,-0.27,14.92,-0.52,15.12,-1.07,14.97,-2,14.56,-2.19,14.25,-2.97,13.8,-3.1,13.54,-3.52,13.34,-4.01,13.47,-4.28,13.23,-4.43,12.54,-5.22,11.71,-5.2,11.38,-5.47,10.95,-5.4,10.37,-5.82,10.22,-6.05,10.1,-6.21,10.52,-6.49,10.41,-6.67,10.43,-6.85,10.14,-7.62,10.15,-7.9,10.3,-8.03,10.21,-8.34,10.49,-8.28,10.79,-8.41,10.91,-8.62,10.81,-8.58,11.14,-8.38,11.39,-8.79,11.81,-8.91,12.09,-9.13,12.31,-9.33,12.33,-9.57,12.19,-9.89,12.06,-10.17,11.84,-10.59,11.92,-10.87,12.18,-11.04,12.21,-11.3,12.08,-11.46,12.08,-11.51,12.44]]]},{"n":"Mauritania","c":"MR","r":3,"ml":3,"lx":-9.74,"ly":19.59,"b":[-17.06,14.62,-4.92,27.4],"p":[[[-17.06,21,-16.85,21.33,-12.93,21.33,-13.12,22.77,-12.87,23.28,-11.94,23.37,-11.97,25.93,-8.69,25.88,-8.68,27.4,-4.92,24.97,-6.45,24.96,-5.97,20.64,-5.49,16.33,-5.32,16.2,-5.54,15.5,-9.55,15.49,-9.7,15.26,-10.09,15.33,-10.65,15.13,-11.35,15.41,-11.67,15.39,-11.83,14.8,-12.17,14.62,-12.83,15.3,-13.44,16.04,-14.1,16.3,-14.58,16.6,-15.14,16.59,-15.62,16.37,-16.12,16.46,-16.46,16.14,-16.55,16.67,-16.27,17.17,-16.15,18.11,-16.26,19.1,-16.38,19.59,-16.28,20.09,-16.54,20.57,-17.06,21]]]},{"n":"Benin","c":"BJ","r":5,"ml":4,"lx":2.35,"ly":10.32,"b":[0.77,6.14,3.8,12.24],"p":[[[2.69,6.26,1.87,6.14,1.62,6.83,1.66,9.13,1.46,9.33,1.43,9.83,1.08,10.18,0.77,10.47,0.9,11,1.24,11.11,1.45,11.55,1.94,11.64,2.15,11.94,2.49,12.23,2.85,12.24,3.61,11.66,3.57,11.33,3.8,10.73,3.6,10.33,3.71,10.06,3.22,9.44,2.91,9.14,2.72,8.51,2.75,7.87,2.69,6.26]]]},{"n":"Niger","c":"NE","r":3,"ml":3,"lx":9.5,"ly":17.45,"b":[0.3,11.66,15.9,23.47],"p":[[[14.85,22.86,15.1,21.31,15.47,21.05,15.49,20.73,15.9,20.39,15.69,19.96,15.3,17.93,15.25,16.63,13.97,15.68,13.54,14.37,13.96,14,13.95,13.35,14.6,13.33,14.5,12.86,14.21,12.8,14.18,12.48,14,12.46,13.32,13.56,13.08,13.6,12.3,13.04,11.53,13.33,10.99,13.39,10.7,13.25,10.11,13.28,9.52,12.85,9.01,12.83,7.8,13.34,7.33,13.1,6.82,13.12,6.45,13.49,5.44,13.87,4.37,13.75,4.11,13.53,3.97,12.96,3.68,12.55,3.61,11.66,2.85,12.24,2.49,12.23,2.15,11.94,2.18,12.63,1.02,12.85,0.99,13.34,0.43,13.99,0.3,14.44,0.37,14.93,1.02,14.97,1.39,15.32,2.75,15.41,3.64,15.57,3.72,16.18,4.27,16.85,4.27,19.16,5.68,19.6,8.57,21.57,12,23.47,13.58,23.04,14.14,22.49,14.85,22.86]]]},{"n":"Nigeria","c":"NG","r":2,"ml":1.7,"lx":7.5,"ly":9.44,"b":[2.69,4.24,14.58,13.87],"p":[[[2.69,6.26,2.75,7.87,2.72,8.51,2.91,9.14,3.22,9.44,3.71,10.06,3.6,10.33,3.8,10.73,3.57,11.33,3.61,11.66,3.68,12.55,3.97,12.96,4.11,13.53,4.37,13.75,5.44,13.87,6.45,13.49,6.82,13.12,7.33,13.1,7.8,13.34,9.01,12.83,9.52,12.85,10.11,13.28,10.7,13.25,10.99,13.39,11.53,13.33,12.3,13.04,13.08,13.6,13.32,13.56,14,12.46,14.18,12.48,14.58,12.09,14.47,11.9,14.42,11.57,13.57,10.8,13.31,10.16,13.17,9.64,12.96,9.42,12.75,8.72,12.22,8.31,12.06,7.8,11.84,7.4,11.75,6.98,11.06,6.64,10.5,7.06,10.12,7.04,9.52,6.45,9.23,6.44,8.76,5.48,8.5,4.77,7.46,4.41,7.08,4.46,6.7,4.24,5.9,4.26,5.36,4.89,5.03,5.61,4.33,6.27,3.57,6.26,2.69,6.26]]]},{"n":"Cameroon","c":"CM","r":3,"ml":3,"lx":12.47,"ly":4.59,"b":[8.49,1.73,16.01,12.86],"p":[[[14.5,12.86,14.89,12.22,14.96,11.56,14.92,10.89,15.47,9.98,14.91,9.99,14.63,9.92,14.17,10.02,13.95,9.55,14.54,8.97,14.98,8.8,15.12,8.38,15.44,7.69,15.28,7.42,14.78,6.41,14.54,6.23,14.46,5.45,14.56,5.03,14.48,4.73,14.95,4.21,15.04,3.85,15.41,3.34,15.86,3.01,15.91,2.56,16.01,2.27,15.94,1.73,15.15,1.96,14.34,2.23,13.08,2.27,12.95,2.32,12.36,2.19,11.75,2.33,11.28,2.26,9.65,2.28,9.8,3.07,9.4,3.73,8.95,3.9,8.74,4.35,8.49,4.5,8.5,4.77,8.76,5.48,9.23,6.44,9.52,6.45,10.12,7.04,10.5,7.06,11.06,6.64,11.75,6.98,11.84,7.4,12.06,7.8,12.22,8.31,12.75,8.72,12.96,9.42,13.17,9.64,13.31,10.16,13.57,10.8,14.42,11.57,14.47,11.9,14.58,12.09,14.18,12.48,14.21,12.8,14.5,12.86]]]},{"n":"Togo","c":"TG","r":6,"ml":5,"lx":1.06,"ly":8.81,"b":[-0.05,5.93,1.87,11.02],"p":[[[0.9,11,0.77,10.47,1.08,10.18,1.43,9.83,1.46,9.33,1.66,9.13,1.62,6.83,1.87,6.14,1.06,5.93,0.84,6.28,0.57,6.91,0.49,7.41,0.71,8.31,0.46,8.68,0.37,9.47,0.37,10.19,-0.05,10.71,0.02,11.02,0.9,11]]]},{"n":"Ghana","c":"GH","r":3,"ml":2.7,"lx":-1.04,"ly":7.72,"b":[-3.24,4.71,1.06,11.1],"p":[[[0.02,11.02,-0.05,10.71,0.37,10.19,0.37,9.47,0.46,8.68,0.71,8.31,0.49,7.41,0.57,6.91,0.84,6.28,1.06,5.93,-0.51,5.34,-1.06,5,-1.96,4.71,-2.86,4.99,-2.81,5.39,-3.24,6.25,-2.98,7.38,-2.56,8.22,-2.83,9.64,-2.96,10.4,-2.94,10.96,-1.2,11.01,-0.76,10.94,-0.44,11.1,0.02,11.02]]]},{"n":"Ivory Coast","c":"CI","r":3,"ml":2.5,"lx":-5.57,"ly":7.49,"b":[-8.6,4.34,-2.56,10.52],"p":[[[-8.03,10.21,-7.9,10.3,-7.62,10.15,-6.85,10.14,-6.67,10.43,-6.49,10.41,-6.21,10.52,-6.05,10.1,-5.82,10.22,-5.4,10.37,-4.95,10.15,-4.78,9.82,-4.33,9.61,-3.98,9.86,-3.51,9.9,-2.83,9.64,-2.56,8.22,-2.98,7.38,-3.24,6.25,-2.81,5.39,-2.86,4.99,-3.31,4.98,-4.01,5.18,-4.65,5.17,-5.83,4.99,-6.53,4.71,-7.52,4.34,-7.71,4.36,-7.64,5.19,-7.54,5.31,-7.57,5.71,-7.99,6.13,-8.31,6.19,-8.6,6.47,-8.39,6.91,-8.49,7.4,-8.44,7.69,-8.28,7.69,-8.22,8.12,-8.3,8.32,-8.2,8.46,-7.83,8.58,-8.08,9.38,-8.31,9.79,-8.23,10.13,-8.03,10.21]]]},{"n":"Guinea","c":"GN","r":3,"ml":3,"lx":-10.02,"ly":10.62,"b":[-15.13,7.31,-7.83,12.59],"p":[[[-13.7,12.59,-13.22,12.58,-12.5,12.33,-12.28,12.35,-12.2,12.47,-11.66,12.39,-11.51,12.44,-11.46,12.08,-11.3,12.08,-11.04,12.21,-10.87,12.18,-10.59,11.92,-10.17,11.84,-9.89,12.06,-9.57,12.19,-9.33,12.33,-9.13,12.31,-8.91,12.09,-8.79,11.81,-8.38,11.39,-8.58,11.14,-8.62,10.81,-8.41,10.91,-8.28,10.79,-8.34,10.49,-8.03,10.21,-8.23,10.13,-8.31,9.79,-8.08,9.38,-7.83,8.58,-8.2,8.46,-8.3,8.32,-8.22,8.12,-8.28,7.69,-8.44,7.69,-8.72,7.71,-8.93,7.31,-9.21,7.31,-9.4,7.53,-9.34,7.93,-9.76,8.54,-10.02,8.43,-10.23,8.41,-10.51,8.35,-10.49,8.72,-10.65,8.98,-10.62,9.27,-10.84,9.69,-11.12,10.05,-11.92,10.05,-12.15,9.86,-12.43,9.84,-12.6,9.62,-12.71,9.34,-13.25,8.9,-13.69,9.49,-14.07,9.89,-14.33,10.02,-14.58,10.21,-14.69,10.66,-14.84,10.88,-15.13,11.04,-14.69,11.53,-14.38,11.51,-14.12,11.68,-13.9,11.68,-13.74,11.81,-13.83,12.14,-13.72,12.25,-13.7,12.59]]]},{"n":"Guinea-Bissau","c":"GW","r":6,"ml":5,"lx":-14.52,"ly":12.16,"b":[-16.68,11.04,-13.7,12.63],"p":[[[-16.68,12.38,-16.15,12.55,-15.82,12.52,-15.55,12.63,-13.7,12.59,-13.72,12.25,-13.83,12.14,-13.74,11.81,-13.9,11.68,-14.12,11.68,-14.38,11.51,-14.69,11.53,-15.13,11.04,-15.66,11.46,-16.09,11.52,-16.31,11.81,-16.31,11.96,-16.61,12.17,-16.68,12.38]]]},{"n":"Liberia","c":"LR","r":4,"ml":4,"lx":-9.46,"ly":6.45,"b":[-11.44,4.36,-7.54,8.54],"p":[[[-8.44,7.69,-8.49,7.4,-8.39,6.91,-8.6,6.47,-8.31,6.19,-7.99,6.13,-7.57,5.71,-7.54,5.31,-7.64,5.19,-7.71,4.36,-7.97,4.36,-9,4.83,-9.91,5.59,-10.77,6.14,-11.44,6.79,-11.2,7.11,-11.15,7.4,-10.7,7.94,-10.23,8.41,-10.02,8.43,-9.76,8.54,-9.34,7.93,-9.4,7.53,-9.21,7.31,-8.93,7.31,-8.72,7.71,-8.44,7.69]]]},{"n":"Sierra Leone","c":"SL","r":4,"ml":4,"lx":-11.76,"ly":8.62,"b":[-13.25,6.79,-10.23,10.05],"p":[[[-13.25,8.9,-12.71,9.34,-12.6,9.62,-12.43,9.84,-12.15,9.86,-11.92,10.05,-11.12,10.05,-10.84,9.69,-10.62,9.27,-10.65,8.98,-10.49,8.72,-10.51,8.35,-10.23,8.41,-10.7,7.94,-11.15,7.4,-11.2,7.11,-11.44,6.79,-11.71,6.86,-12.43,7.26,-12.95,7.8,-13.12,8.16,-13.25,8.9]]]},{"n":"Burkina Faso","c":"BF","r":3,"ml":3,"lx":-1.36,"ly":12.67,"b":[-5.47,9.61,2.18,15.12],"p":[[[-5.4,10.37,-5.47,10.95,-5.2,11.38,-5.22,11.71,-4.43,12.54,-4.28,13.23,-4.01,13.47,-3.52,13.34,-3.1,13.54,-2.97,13.8,-2.19,14.25,-2,14.56,-1.07,14.97,-0.52,15.12,-0.27,14.92,0.37,14.93,0.3,14.44,0.43,13.99,0.99,13.34,1.02,12.85,2.18,12.63,2.15,11.94,1.94,11.64,1.45,11.55,1.24,11.11,0.9,11,0.02,11.02,-0.44,11.1,-0.76,10.94,-1.2,11.01,-2.94,10.96,-2.96,10.4,-2.83,9.64,-3.51,9.9,-3.98,9.86,-4.33,9.61,-4.78,9.82,-4.95,10.15,-5.4,10.37]]]},{"n":"Central African Republic","c":"CF","r":4,"ml":4,"lx":20.91,"ly":6.99,"b":[14.46,2.27,27.37,11.14],"p":[[[27.37,5.23,27.04,5.13,26.4,5.15,25.65,5.26,25.28,5.17,25.13,4.93,24.81,4.9,24.41,5.11,23.3,4.61,22.84,4.71,22.7,4.63,22.41,4.03,21.66,4.22,20.93,4.32,20.29,4.69,19.47,5.03,18.93,4.71,18.54,4.2,18.45,3.5,17.81,3.56,17.13,3.73,16.54,3.2,16.01,2.27,15.91,2.56,15.86,3.01,15.41,3.34,15.04,3.85,14.95,4.21,14.48,4.73,14.56,5.03,14.46,5.45,14.54,6.23,14.78,6.41,15.28,7.42,16.11,7.5,16.29,7.75,16.46,7.73,16.71,7.51,17.96,7.89,18.39,8.28,18.91,8.63,18.81,8.98,19.09,9.07,20.06,9.01,21,9.48,21.72,10.57,22.23,10.97,22.86,11.14,22.98,10.71,23.55,10.09,23.56,9.68,23.39,9.27,23.46,8.95,23.81,8.67,24.57,8.23,25.11,7.83,25.12,7.5,25.8,6.98,26.21,6.55,26.47,5.95,27.21,5.55,27.37,5.23]]]},{"n":"Republic of the Congo","c":"CG","r":4,"ml":4,"lx":15.9,"ly":0.14,"b":[11.09,-5.04,18.45,3.73],"p":[[[18.45,3.5,18.39,2.9,18.09,2.37,17.9,1.74,17.77,0.86,17.83,0.29,17.66,-0.06,17.64,-0.42,17.52,-0.74,16.87,-1.23,16.41,-1.74,15.97,-2.71,16.01,-3.54,15.75,-3.86,15.17,-4.34,14.58,-4.97,14.21,-4.79,14.14,-4.51,13.6,-4.5,13.26,-4.88,13,-4.78,12.62,-4.44,12.32,-4.61,11.91,-5.04,11.09,-3.98,11.86,-3.43,11.48,-2.77,11.82,-2.51,12.5,-2.39,12.58,-1.95,13.11,-2.43,13.99,-2.47,14.3,-2,14.43,-1.33,14.32,-0.55,13.84,0.04,14.28,1.2,14.03,1.4,13.28,1.31,13,1.83,13.08,2.27,14.34,2.23,15.15,1.96,15.94,1.73,16.01,2.27,16.54,3.2,17.13,3.73,17.81,3.56,18.45,3.5]]]},{"n":"Gabon","c":"GA","r":4,"ml":3,"lx":11.84,"ly":-0.44,"b":[8.8,-3.98,14.43,2.33],"p":[[[11.28,2.26,11.75,2.33,12.36,2.19,12.95,2.32,13.08,2.27,13,1.83,13.28,1.31,14.03,1.4,14.28,1.2,13.84,0.04,14.32,-0.55,14.43,-1.33,14.3,-2,13.99,-2.47,13.11,-2.43,12.58,-1.95,12.5,-2.39,11.82,-2.51,11.48,-2.77,11.86,-3.43,11.09,-3.98,10.07,-2.97,9.41,-2.14,8.8,-1.11,8.83,-0.78,9.05,-0.46,9.29,0.27,9.49,1.01,9.83,1.07,11.29,1.06,11.28,2.26]]]},{"n":"Equatorial Guinea","c":"GQ","r":4,"ml":4,"lx":8.99,"ly":2.33,"b":[9.31,1.01,11.29,2.28],"p":[[[9.65,2.28,11.28,2.26,11.29,1.06,9.83,1.07,9.49,1.01,9.31,1.16,9.65,2.28]]]},{"n":"Zambia","c":"ZM","r":3,"ml":3,"lx":26.4,"ly":-14.66,"b":[21.89,-17.96,33.49,-8.24],"p":[[[30.74,-8.34,31.16,-8.59,31.56,-8.76,32.19,-8.93,32.76,-9.23,33.23,-9.68,33.49,-10.53,33.32,-10.8,33.11,-11.61,33.31,-12.44,32.99,-12.78,32.69,-13.71,33.21,-13.97,30.18,-14.8,30.27,-15.51,29.52,-15.64,28.95,-16.04,28.83,-16.39,28.47,-16.47,27.6,-17.29,27.04,-17.94,26.71,-17.96,26.38,-17.85,25.26,-17.74,25.08,-17.66,25.08,-17.58,24.68,-17.35,24.03,-17.3,23.22,-17.52,22.56,-16.9,21.89,-16.08,21.93,-12.9,24.02,-12.91,23.93,-12.57,24.08,-12.19,23.9,-11.72,24.02,-11.24,23.91,-10.93,24.26,-10.95,24.31,-11.26,24.78,-11.24,25.42,-11.33,25.75,-11.78,26.55,-11.92,27.16,-11.61,27.39,-12.13,28.16,-12.27,28.52,-12.7,28.93,-13.25,29.7,-13.26,29.62,-12.18,29.34,-12.36,28.64,-11.97,28.37,-11.79,28.5,-10.79,28.67,-9.61,28.45,-9.16,28.73,-8.53,29,-8.41,30.35,-8.24,30.74,-8.34]]]},{"n":"Malawi","c":"MW","r":6,"ml":4,"lx":33.61,"ly":-13.39,"b":[32.69,-16.8,35.77,-9.23],"p":[[[32.76,-9.23,33.74,-9.42,33.94,-9.69,34.28,-10.16,34.56,-11.52,34.28,-12.28,34.56,-13.58,34.91,-13.57,35.27,-13.89,35.69,-14.61,35.77,-15.9,35.34,-16.11,35.03,-16.8,34.38,-16.18,34.31,-15.48,34.52,-15.01,34.46,-14.61,34.06,-14.36,33.79,-14.45,33.21,-13.97,32.69,-13.71,32.99,-12.78,33.31,-12.44,33.11,-11.61,33.32,-10.8,33.49,-10.53,33.23,-9.68,32.76,-9.23]]]},{"n":"Mozambique","c":"MZ","r":3,"ml":3,"lx":37.84,"ly":-13.94,"b":[30.18,-26.74,40.78,-10.32],"p":[[[34.56,-11.52,35.31,-11.44,36.51,-11.72,36.78,-11.59,37.47,-11.57,37.83,-11.27,38.43,-11.29,39.52,-10.9,40.32,-10.32,40.48,-10.77,40.44,-11.76,40.56,-12.64,40.6,-14.2,40.78,-14.69,40.48,-15.41,40.09,-16.1,39.45,-16.72,38.54,-17.1,37.41,-17.59,36.28,-18.66,35.9,-18.84,35.2,-19.55,34.79,-19.78,34.7,-20.5,35.18,-21.25,35.37,-21.84,35.39,-22.14,35.56,-22.09,35.53,-23.07,35.37,-23.54,35.61,-23.71,35.46,-24.12,35.04,-24.48,34.22,-24.82,33.01,-25.36,32.57,-25.73,32.66,-26.15,32.92,-26.22,32.83,-26.74,32.07,-26.73,31.99,-26.29,31.84,-25.84,31.75,-25.48,31.93,-24.37,31.67,-23.66,31.19,-22.25,32.24,-21.12,32.51,-20.4,32.66,-20.3,32.77,-19.72,32.61,-19.42,32.65,-18.67,32.85,-17.98,32.85,-16.71,32.33,-16.39,31.85,-16.32,31.64,-16.07,31.17,-15.86,30.34,-15.88,30.27,-15.51,30.18,-14.8,33.21,-13.97,33.79,-14.45,34.06,-14.36,34.46,-14.61,34.52,-15.01,34.31,-15.48,34.38,-16.18,35.03,-16.8,35.34,-16.11,35.77,-15.9,35.69,-14.61,35.27,-13.89,34.91,-13.57,34.56,-13.58,34.28,-12.28,34.56,-11.52]]]},{"n":"Eswatini","c":"SZ","r":4,"ml":4,"lx":31.47,"ly":-26.53,"b":[30.68,-27.29,32.07,-25.66],"p":[[[32.07,-26.73,31.87,-27.18,31.28,-27.29,30.69,-26.74,30.68,-26.4,30.95,-26.02,31.04,-25.73,31.33,-25.66,31.84,-25.84,31.99,-26.29,32.07,-26.73]]]},{"n":"Angola","c":"AO","r":3,"ml":3,"lx":17.98,"ly":-12.18,"b":[11.64,-17.93,24.08,-4.44],"p":[[[13,-4.78,12.63,-4.99,12.47,-5.25,12.44,-5.68,12.18,-5.79,11.91,-5.04,12.32,-4.61,12.62,-4.44,13,-4.78]],[[12.32,-6.1,12.74,-5.97,13.02,-5.98,13.38,-5.86,16.33,-5.88,16.57,-6.62,16.86,-7.22,17.09,-7.55,17.47,-8.07,18.13,-7.99,18.46,-7.85,19.02,-7.99,19.17,-7.74,19.42,-7.16,20.04,-7.12,20.09,-6.94,20.6,-6.94,20.51,-7.3,21.73,-7.29,21.75,-7.92,21.95,-8.31,21.8,-8.91,21.88,-9.52,22.21,-9.89,22.16,-11.08,22.4,-10.99,22.84,-11.02,23.46,-10.87,23.91,-10.93,24.02,-11.24,23.9,-11.72,24.08,-12.19,23.93,-12.57,24.02,-12.91,21.93,-12.9,21.89,-16.08,22.56,-16.9,23.22,-17.52,21.38,-17.93,18.96,-17.79,18.26,-17.31,14.21,-17.35,14.06,-17.42,13.46,-16.97,12.81,-16.94,12.22,-17.11,11.73,-17.3,11.64,-16.67,11.78,-15.79,12.12,-14.88,12.18,-14.45,12.5,-13.55,12.74,-13.14,13.31,-12.48,13.63,-12.04,13.74,-11.3,13.69,-10.73,13.39,-10.37,13.12,-9.77,12.88,-9.17,12.93,-8.96,13.24,-8.56,12.93,-7.6,12.73,-6.93,12.23,-6.29,12.32,-6.1]]]},{"n":"Burundi","c":"BI","r":6,"ml":4,"lx":29.92,"ly":-3.33,"b":[29.02,-4.5,30.75,-2.35],"p":[[[30.47,-2.41,30.53,-2.81,30.74,-3.03,30.75,-3.36,30.51,-3.57,30.12,-4.09,29.75,-4.45,29.34,-4.5,29.28,-3.29,29.02,-2.84,29.63,-2.92,29.94,-2.35,30.47,-2.41]]]},{"n":"Israel","c":"IL","r":4,"ml":3,"lx":34.85,"ly":30.91,"b":[34.27,29.5,35.84,33.28],"p":[[[35.72,32.71,35.55,32.39,35.18,32.53,34.97,31.87,35.23,31.75,34.97,31.62,34.93,31.35,35.4,31.49,35.42,31.1,34.92,29.5,34.82,29.76,34.27,31.22,34.56,31.55,34.49,31.61,34.75,32.07,34.96,32.83,35.1,33.08,35.13,33.09,35.46,33.09,35.55,33.26,35.82,33.28,35.84,32.87,35.7,32.72,35.72,32.71]]]},{"n":"Lebanon","c":"LB","r":5,"ml":4,"lx":35.99,"ly":34.13,"b":[35.13,33.09,36.61,34.64],"p":[[[35.82,33.28,35.55,33.26,35.46,33.09,35.13,33.09,35.48,33.91,35.98,34.61,36,34.64,36.45,34.59,36.61,34.2,36.07,33.82,35.82,33.28]]]},{"n":"Madagascar","c":"MG","r":3,"ml":2.7,"lx":46.7,"ly":-18.63,"b":[43.25,-25.6,50.48,-12.04],"p":[[[49.54,-12.47,49.81,-12.9,50.06,-13.56,50.22,-14.76,50.48,-15.23,50.38,-15.71,50.2,-16,49.86,-15.41,49.67,-15.71,49.86,-16.45,49.77,-16.88,49.5,-17.11,49.44,-17.95,49.04,-19.12,48.55,-20.5,47.93,-22.39,47.55,-23.78,47.1,-24.94,46.28,-25.18,45.41,-25.6,44.83,-25.35,44.04,-24.99,43.76,-24.46,43.7,-23.57,43.35,-22.78,43.25,-22.06,43.43,-21.34,43.89,-21.16,43.9,-20.83,44.37,-20.07,44.46,-19.44,44.23,-18.96,44.04,-18.33,43.96,-17.41,44.31,-16.85,44.45,-16.22,44.94,-16.18,45.5,-15.97,45.87,-15.79,46.31,-15.78,46.88,-15.21,47.71,-14.59,48.01,-14.09,47.87,-13.66,48.29,-13.78,48.85,-13.09,48.86,-12.49,49.19,-12.04,49.54,-12.47]]]},{"n":"Palestine","c":"PS","r":5,"ml":4.5,"lx":35.29,"ly":32.05,"b":[34.93,31.35,35.55,32.53],"p":[[[35.4,31.49,34.93,31.35,34.97,31.62,35.23,31.75,34.97,31.87,35.18,32.53,35.55,32.39,35.55,31.78,35.4,31.49]]]},{"n":"The Gambia","c":"GM","r":6,"ml":5,"lx":-15,"ly":13.64,"b":[-16.84,13.13,-13.84,13.88],"p":[[[-16.71,13.59,-15.62,13.62,-15.4,13.86,-15.08,13.88,-14.69,13.63,-14.38,13.63,-14.05,13.79,-13.84,13.51,-14.28,13.28,-14.71,13.3,-15.14,13.51,-15.51,13.28,-15.69,13.27,-15.93,13.13,-16.84,13.15,-16.71,13.59]]]},{"n":"Tunisia","c":"TN","r":3,"ml":3,"lx":9.01,"ly":33.69,"b":[7.52,30.31,11.49,37.35],"p":[[[9.48,30.31,9.06,32.1,8.44,32.51,8.43,32.75,7.61,33.34,7.52,34.1,8.14,34.66,8.38,35.48,8.22,36.43,8.42,36.95,9.51,37.35,10.21,37.23,10.18,36.72,11.03,37.09,11.1,36.9,10.6,36.41,10.59,35.95,10.94,35.7,10.81,34.83,10.15,34.33,10.34,33.79,10.86,33.77,11.11,33.29,11.49,33.14,11.43,32.37,10.94,32.08,10.64,31.76,9.95,31.38,10.06,30.96,9.97,30.54,9.48,30.31]]]},{"n":"Algeria","c":"DZ","r":3,"ml":2.5,"lx":2.81,"ly":27.4,"b":[-8.68,19.06,12,37.12],"p":[[[-8.68,27.4,-8.67,27.59,-8.67,27.66,-8.67,28.84,-7.06,29.58,-6.06,29.73,-5.24,30,-4.86,30.5,-3.69,30.9,-3.65,31.64,-3.07,31.72,-2.62,32.09,-1.31,32.26,-1.12,32.65,-1.39,32.86,-1.73,33.92,-1.79,34.53,-2.17,35.17,-1.21,35.71,-0.13,35.89,0.5,36.3,1.47,36.61,3.16,36.78,4.82,36.87,5.32,36.72,6.26,37.11,7.33,37.12,7.74,36.89,8.42,36.95,8.22,36.43,8.38,35.48,8.14,34.66,7.52,34.1,7.61,33.34,8.43,32.75,8.44,32.51,9.06,32.1,9.48,30.31,9.81,29.42,9.86,28.96,9.68,28.14,9.76,27.69,9.63,27.14,9.72,26.51,9.32,26.09,9.91,25.37,9.95,24.94,10.3,24.38,10.77,24.56,11.56,24.1,12,23.47,8.57,21.57,5.68,19.6,4.27,19.16,3.16,19.06,3.15,19.69,2.68,19.86,2.06,20.14,1.82,20.61,-1.55,22.79,-4.92,24.97,-8.68,27.4]]]},{"n":"Jordan","c":"JO","r":4,"ml":4,"lx":36.38,"ly":30.81,"b":[34.92,29.2,39.2,33.38],"p":[[[35.55,32.39,35.72,32.71,36.83,32.31,38.79,33.38,39.2,32.16,39,32.01,37,31.51,38,30.51,37.67,30.34,37.5,30,36.74,29.87,36.5,29.51,36.07,29.2,34.96,29.36,34.92,29.5,35.42,31.1,35.4,31.49,35.55,31.78,35.55,32.39]]]},{"n":"United Arab Emirates","c":"AE","r":4,"ml":4,"lx":54.55,"ly":23.47,"b":[51.58,22.5,56.4,26.06],"p":[[[51.58,24.25,51.76,24.29,51.79,24.02,52.58,24.18,53.4,24.15,54.01,24.12,54.69,24.8,55.44,25.44,56.07,26.06,56.26,25.71,56.4,24.92,55.89,24.92,55.8,24.27,55.98,24.13,55.53,23.93,55.53,23.52,55.23,23.11,55.21,22.71,55.01,22.5,52,23,51.62,24.01,51.58,24.25]]]},{"n":"Qatar","c":"QA","r":5,"ml":4,"lx":51.14,"ly":25.24,"b":[50.74,24.56,51.61,26.11],"p":[[[50.81,24.75,50.74,25.48,51.01,26.01,51.29,26.11,51.59,25.8,51.61,25.22,51.39,24.63,51.11,24.56,50.81,24.75]]]},{"n":"Kuwait","c":"KW","r":6,"ml":5,"lx":47.31,"ly":29.41,"b":[46.57,28.53,48.42,30.06],"p":[[[47.97,29.98,48.18,29.53,48.09,29.31,48.42,28.55,47.71,28.53,47.46,29,46.57,29.1,47.3,30.06,47.97,29.98]]]},{"n":"Iraq","c":"IQ","r":3,"ml":3,"lx":43.26,"ly":33.09,"b":[38.79,29.1,48.57,37.39],"p":[[[39.2,32.16,38.79,33.38,41.01,34.42,41.38,35.63,41.29,36.36,41.84,36.61,42.35,37.23,42.78,37.39,43.94,37.26,44.29,37,44.77,37.17,45.42,35.98,46.08,35.68,46.15,35.09,45.65,34.75,45.42,33.97,46.11,33.02,47.33,32.47,47.85,31.71,47.69,30.98,48,30.99,48.01,30.45,48.57,29.93,47.97,29.98,47.3,30.06,46.57,29.1,44.71,29.18,41.89,31.19,40.4,31.89,39.2,32.16]]]},{"n":"Oman","c":"OM","r":4,"ml":4,"lx":57.34,"ly":22.12,"b":[52,16.65,59.81,26.4],"p":[[[55.21,22.71,55.23,23.11,55.53,23.52,55.53,23.93,55.98,24.13,55.8,24.27,55.89,24.92,56.4,24.92,56.85,24.24,57.4,23.88,58.14,23.75,58.73,23.57,59.18,22.99,59.45,22.66,59.81,22.53,59.81,22.31,59.44,21.71,59.28,21.43,58.86,21.11,58.49,20.43,58.03,20.48,57.83,20.24,57.67,19.74,57.79,19.07,57.69,18.94,57.23,18.95,56.61,18.57,56.51,18.09,56.28,17.88,55.66,17.88,55.27,17.63,55.27,17.23,54.79,16.95,54.24,17.04,53.57,16.71,53.11,16.65,52.78,17.35,52,19,55,20,55.67,22,55.21,22.71]],[[56.26,25.71,56.07,26.06,56.36,26.4,56.49,26.31,56.39,25.9,56.26,25.71]]]},{"n":"Vanuatu","c":"VU","r":4,"ml":4,"lx":166.91,"ly":-15.37,"b":[166.63,-16.6,167.84,-14.63],"p":[[[167.22,-15.89,167.84,-16.47,167.52,-16.6,167.18,-16.16,167.22,-15.89]],[[166.79,-15.67,166.65,-15.39,166.63,-14.63,167.11,-14.93,167.27,-15.74,167,-15.61,166.79,-15.67]]]},{"n":"Cambodia","c":"KH","r":3,"ml":3,"lx":104.5,"ly":12.65,"b":[102.35,10.49,107.61,14.57],"p":[[[102.58,12.19,102.35,13.39,102.99,14.23,104.28,14.42,105.22,14.27,106.04,13.88,106.5,14.57,107.38,14.2,107.61,13.54,107.49,12.34,105.81,11.57,106.25,10.96,105.2,10.89,104.33,10.49,103.5,10.63,103.09,11.15,102.58,12.19]]]},{"n":"Thailand","c":"TH","r":3,"ml":2.7,"lx":101.07,"ly":15.46,"b":[97.38,5.69,105.59,20.42],"p":[[[105.22,14.27,104.28,14.42,102.99,14.23,102.35,13.39,102.58,12.19,101.69,12.65,100.83,12.63,100.98,13.41,100.1,13.41,100.02,12.31,99.48,10.85,99.15,9.96,99.22,9.24,99.87,9.21,100.28,8.3,100.46,7.43,101.02,6.86,101.62,6.74,102.14,6.22,101.81,5.81,101.15,5.69,101.08,6.2,100.26,6.64,100.09,6.46,99.69,6.85,99.52,7.34,98.99,7.91,98.5,8.38,98.34,7.79,98.15,8.35,98.26,8.97,98.55,9.93,99.04,10.96,99.59,11.89,99.2,12.8,99.21,13.27,99.1,13.83,98.43,14.62,98.19,15.12,98.54,15.31,98.9,16.18,98.49,16.84,97.86,17.57,97.38,18.45,97.8,18.63,98.25,19.71,98.96,19.75,99.54,20.19,100.12,20.42,100.55,20.11,100.61,19.51,101.28,19.46,101.04,18.41,101.06,17.51,102.11,18.11,102.41,17.93,103,17.96,103.2,18.31,103.96,18.24,104.72,17.43,104.78,16.44,105.59,15.57,105.54,14.72,105.22,14.27]]]},{"n":"Laos","c":"LA","r":4,"ml":4,"lx":102.53,"ly":19.43,"b":[100.12,13.88,107.56,22.46],"p":[[[107.38,14.2,106.5,14.57,106.04,13.88,105.22,14.27,105.54,14.72,105.59,15.57,104.78,16.44,104.72,17.43,103.96,18.24,103.2,18.31,103,17.96,102.41,17.93,102.11,18.11,101.06,17.51,101.04,18.41,101.28,19.46,100.61,19.51,100.55,20.11,100.12,20.42,100.33,20.79,101.18,21.44,101.27,21.2,101.8,21.17,101.65,22.32,102.17,22.46,102.75,21.68,103.2,20.77,104.44,20.76,104.82,19.89,104.18,19.62,103.9,19.27,105.09,18.67,105.93,17.49,106.56,16.6,107.31,15.91,107.56,15.2,107.38,14.2]]]},{"n":"Myanmar","c":"MM","r":3,"ml":3,"lx":95.8,"ly":21.57,"b":[92.3,9.93,101.18,28.34],"p":[[[100.12,20.42,99.54,20.19,98.96,19.75,98.25,19.71,97.8,18.63,97.38,18.45,97.86,17.57,98.49,16.84,98.9,16.18,98.54,15.31,98.19,15.12,98.43,14.62,99.1,13.83,99.21,13.27,99.2,12.8,99.59,11.89,99.04,10.96,98.55,9.93,98.46,10.68,98.76,11.44,98.43,12.03,98.51,13.12,98.1,13.64,97.78,14.84,97.6,16.1,97.16,16.93,96.51,16.43,95.37,15.71,94.81,15.8,94.19,16.04,94.53,17.28,94.32,18.21,93.54,19.37,93.66,19.73,93.08,19.86,92.37,20.67,92.3,21.48,92.65,21.32,92.67,22.04,93.17,22.28,93.06,22.7,93.29,23.04,93.33,24.08,94.11,23.85,94.55,24.68,94.6,25.16,95.16,26,95.12,26.57,96.42,27.26,97.13,27.08,97.05,27.7,97.4,27.88,97.33,28.26,97.91,28.34,98.25,27.75,98.68,27.51,98.71,26.74,98.67,25.92,97.72,25.08,97.6,23.9,98.66,24.06,98.9,23.14,99.53,22.95,99.24,22.12,99.98,21.74,100.42,21.56,101.15,21.85,101.18,21.44,100.33,20.79,100.12,20.42]]]},{"n":"Vietnam","c":"VN","r":2,"ml":2,"lx":105.39,"ly":21.72,"b":[102.17,8.6,109.34,23.35],"p":[[[104.33,10.49,105.2,10.89,106.25,10.96,105.81,11.57,107.49,12.34,107.61,13.54,107.38,14.2,107.56,15.2,107.31,15.91,106.56,16.6,105.93,17.49,105.09,18.67,103.9,19.27,104.18,19.62,104.82,19.89,104.44,20.76,103.2,20.77,102.75,21.68,102.17,22.46,102.71,22.71,103.5,22.7,104.48,22.82,105.33,23.35,105.81,22.98,106.73,22.79,106.57,22.22,107.04,21.81,108.05,21.55,106.72,20.7,105.88,19.75,105.66,19.06,106.43,18,107.36,16.7,108.27,16.08,108.88,15.28,109.34,13.43,109.2,11.67,108.37,11.01,107.22,10.36,106.41,9.53,105.16,8.6,104.8,9.24,105.08,9.92,104.33,10.49]]]},{"n":"North Korea","c":"KP","r":3,"ml":3,"lx":126.44,"ly":39.89,"b":[124.27,37.67,130.78,42.99],"p":[[[130.64,42.4,130.78,42.22,130.4,42.28,129.97,41.94,129.67,41.6,129.71,40.88,129.19,40.66,129.01,40.49,128.63,40.19,127.97,40.03,127.53,39.76,127.5,39.32,127.39,39.21,127.78,39.05,128.35,38.61,128.21,38.37,127.78,38.3,127.07,38.26,126.68,37.8,126.24,37.84,126.17,37.75,125.69,37.94,125.57,37.75,125.28,37.67,125.24,37.86,124.98,37.95,124.71,38.11,124.99,38.55,125.22,38.67,125.13,38.85,125.39,39.39,125.32,39.55,124.74,39.66,124.27,39.93,125.08,40.57,126.18,41.11,126.87,41.82,127.34,41.5,128.21,41.47,128.05,41.99,129.6,42.42,129.99,42.99,130.64,42.4]]]},{"n":"South Korea","c":"KR","r":2,"ml":2.5,"lx":128.13,"ly":36.38,"b":[126.12,34.39,129.47,38.61],"p":[[[126.17,37.75,126.24,37.84,126.68,37.8,127.07,38.26,127.78,38.3,128.21,38.37,128.35,38.61,129.21,37.43,129.46,36.78,129.47,35.63,129.09,35.08,128.19,34.89,127.39,34.48,126.49,34.39,126.37,34.93,126.56,35.68,126.12,36.73,126.86,36.89,126.17,37.75]]]},{"n":"Mongolia","c":"MN","r":3,"ml":3,"lx":104.15,"ly":46,"b":[87.75,41.6,119.77,52.05],"p":[[[87.75,49.3,88.81,49.47,90.71,50.33,92.23,50.8,93.1,50.5,94.15,50.48,94.82,50.01,95.81,49.98,97.26,49.73,98.23,50.42,97.83,51.01,98.86,52.05,99.98,51.63,100.89,51.52,102.07,51.26,102.26,50.51,103.68,50.09,104.62,50.28,105.89,50.41,106.89,50.27,107.87,49.79,108.48,49.28,109.4,49.29,110.66,49.13,111.58,49.38,112.9,49.54,114.36,50.25,114.96,50.14,115.49,49.81,116.68,49.89,116.19,49.13,115.49,48.14,115.74,47.73,116.31,47.85,117.3,47.7,118.06,48.07,118.87,47.75,119.77,47.05,119.66,46.69,118.87,46.81,117.42,46.67,116.72,46.39,115.99,45.73,114.46,45.34,113.46,44.81,112.44,45.01,111.87,45.1,111.35,44.46,111.67,44.07,111.83,43.74,111.13,43.41,110.41,42.87,109.24,42.52,107.74,42.48,106.13,42.13,104.96,41.6,104.52,41.91,103.31,41.91,101.83,42.51,100.85,42.66,99.52,42.52,97.45,42.75,96.35,42.73,95.76,43.32,95.31,44.24,94.69,44.35,93.48,44.98,92.13,45.12,90.95,45.29,90.59,45.72,90.97,46.89,90.28,47.69,88.85,48.07,88.01,48.6,87.75,49.3]]]},{"n":"India","c":"IN","r":2,"ml":1.7,"lx":79.36,"ly":22.69,"b":[68.18,7.97,97.4,35.49],"p":[[[97.33,28.26,97.4,27.88,97.05,27.7,97.13,27.08,96.42,27.26,95.12,26.57,95.16,26,94.6,25.16,94.55,24.68,94.11,23.85,93.33,24.08,93.29,23.04,93.06,22.7,93.17,22.28,92.67,22.04,92.15,23.63,91.87,23.62,91.71,22.99,91.16,23.5,91.47,24.07,91.92,24.13,92.38,24.98,91.8,25.15,90.87,25.13,89.92,25.27,89.83,25.97,89.36,26.01,88.56,26.45,88.21,25.77,88.93,25.24,88.31,24.87,88.08,24.5,88.7,24.23,88.53,23.63,88.88,22.88,89.03,22.06,88.89,21.69,88.21,21.7,86.98,21.5,87.03,20.74,86.5,20.15,85.06,19.48,83.94,18.3,83.19,17.67,82.19,17.02,82.19,16.56,81.69,16.31,80.79,15.95,80.32,15.9,80.03,15.14,80.23,13.84,80.29,13.01,79.86,12.06,79.86,10.36,79.34,10.31,78.89,9.55,79.19,9.22,78.28,8.93,77.94,8.25,77.54,7.97,76.59,8.9,76.13,10.3,75.75,11.31,75.4,11.78,74.86,12.74,74.62,13.99,74.44,14.62,73.53,15.99,73.12,17.93,72.82,19.21,72.82,20.42,72.63,21.36,71.18,20.76,70.47,20.88,69.16,22.09,69.64,22.45,69.35,22.84,68.18,23.69,68.84,24.36,71.04,24.36,70.84,25.22,70.28,25.72,70.17,26.49,69.51,26.94,70.62,27.99,71.78,27.91,72.82,28.96,73.45,29.98,74.42,30.98,74.41,31.69,75.26,32.27,74.45,32.76,74.1,33.44,73.75,34.32,74.24,34.75,75.76,34.5,76.87,34.65,77.84,35.49,78.91,34.32,78.81,33.51,79.21,32.99,79.18,32.48,78.46,32.62,78.74,31.52,79.72,30.88,81.11,30.18,80.48,29.73,80.09,28.79,81.06,28.42,82,27.93,83.3,27.36,84.68,27.23,85.25,26.73,86.02,26.63,87.23,26.4,88.06,26.41,88.17,26.81,88.04,27.45,88.12,27.88,88.73,28.09,88.81,27.3,88.84,27.1,89.74,26.72,90.37,26.88,91.22,26.81,92.03,26.84,92.1,27.45,91.7,27.77,92.5,27.9,93.41,28.64,94.57,29.28,95.4,29.03,96.12,29.45,96.59,28.83,96.25,28.41,97.33,28.26]]]},{"n":"Bangladesh","c":"BD","r":3,"ml":3,"lx":89.68,"ly":24.21,"b":[88.08,20.67,92.67,26.45],"p":[[[92.67,22.04,92.65,21.32,92.3,21.48,92.37,20.67,92.08,21.19,92.03,21.7,91.83,22.18,91.42,22.77,90.5,22.81,90.59,22.39,90.27,21.84,89.85,22.04,89.7,21.86,89.42,21.97,89.03,22.06,88.88,22.88,88.53,23.63,88.7,24.23,88.08,24.5,88.31,24.87,88.93,25.24,88.21,25.77,88.56,26.45,89.36,26.01,89.83,25.97,89.92,25.27,90.87,25.13,91.8,25.15,92.38,24.98,91.92,24.13,91.47,24.07,91.16,23.5,91.71,22.99,91.87,23.62,92.15,23.63,92.67,22.04]]]},{"n":"Bhutan","c":"BT","r":5,"ml":4,"lx":90.04,"ly":27.54,"b":[88.81,26.72,92.1,28.3],"p":[[[91.7,27.77,92.1,27.45,92.03,26.84,91.22,26.81,90.37,26.88,89.74,26.72,88.84,27.1,88.81,27.3,89.48,28.04,90.02,28.3,90.73,28.06,91.26,28.04,91.7,27.77]]]},{"n":"Nepal","c":"NP","r":3,"ml":3,"lx":83.64,"ly":28.3,"b":[80.09,26.4,88.17,30.42],"p":[[[88.12,27.88,88.04,27.45,88.17,26.81,88.06,26.41,87.23,26.4,86.02,26.63,85.25,26.73,84.68,27.23,83.3,27.36,82,27.93,81.06,28.42,80.09,28.79,80.48,29.73,81.11,30.18,81.53,30.42,82.33,30.12,83.34,29.46,83.9,29.32,84.23,28.84,85.01,28.64,85.82,28.2,86.95,27.97,88.12,27.88]]]},{"n":"Pakistan","c":"PK","r":2,"ml":2.7,"lx":68.55,"ly":29.33,"b":[60.87,23.69,77.84,37.13],"p":[[[77.84,35.49,76.87,34.65,75.76,34.5,74.24,34.75,73.75,34.32,74.1,33.44,74.45,32.76,75.26,32.27,74.41,31.69,74.42,30.98,73.45,29.98,72.82,28.96,71.78,27.91,70.62,27.99,69.51,26.94,70.17,26.49,70.28,25.72,70.84,25.22,71.04,24.36,68.84,24.36,68.18,23.69,67.44,23.94,67.15,24.66,66.37,25.43,64.53,25.24,62.91,25.22,61.5,25.08,61.87,26.24,63.32,26.76,63.23,27.22,62.76,27.38,62.73,28.26,61.77,28.7,61.37,29.3,60.87,29.83,62.55,29.32,63.55,29.47,64.15,29.34,64.35,29.56,65.05,29.47,66.35,29.89,66.38,30.74,66.94,31.3,67.68,31.3,67.79,31.58,68.56,31.71,68.93,31.62,69.32,31.9,69.26,32.5,69.69,33.11,70.32,33.36,69.93,34.02,70.88,33.99,71.16,34.35,71.12,34.73,71.61,35.15,71.5,35.65,71.26,36.07,71.85,36.51,72.92,36.72,74.07,36.84,74.58,37.02,75.16,37.13,75.9,36.67,76.19,35.9,77.84,35.49]]]},{"n":"Afghanistan","c":"AF","r":3,"ml":3,"lx":66.5,"ly":34.16,"b":[60.53,29.32,75.16,38.49],"p":[[[66.52,37.36,67.08,37.36,67.83,37.14,68.14,37.02,68.86,37.34,69.2,37.15,69.52,37.61,70.12,37.59,70.27,37.74,70.38,38.14,70.81,38.49,71.35,38.26,71.24,37.95,71.54,37.91,71.45,37.07,71.84,36.74,72.19,36.95,72.64,37.05,73.26,37.5,73.95,37.42,74.98,37.42,75.16,37.13,74.58,37.02,74.07,36.84,72.92,36.72,71.85,36.51,71.26,36.07,71.5,35.65,71.61,35.15,71.12,34.73,71.16,34.35,70.88,33.99,69.93,34.02,70.32,33.36,69.69,33.11,69.26,32.5,69.32,31.9,68.93,31.62,68.56,31.71,67.79,31.58,67.68,31.3,66.94,31.3,66.38,30.74,66.35,29.89,65.05,29.47,64.35,29.56,64.15,29.34,63.55,29.47,62.55,29.32,60.87,29.83,61.78,30.74,61.7,31.38,60.94,31.55,60.86,32.18,60.54,32.98,60.96,33.53,60.53,33.68,60.8,34.4,61.21,35.65,62.23,35.27,62.98,35.4,63.19,35.86,63.98,36.01,64.55,36.31,64.75,37.11,65.59,37.31,65.75,37.66,66.22,37.39,66.52,37.36]]]},{"n":"Tajikistan","c":"TJ","r":4,"ml":4,"lx":72.59,"ly":38.2,"b":[67.44,36.74,74.98,40.96],"p":[[[67.83,37.14,68.39,38.16,68.18,38.9,67.44,39.14,67.7,39.58,68.54,39.53,69.01,40.09,69.33,40.73,70.67,40.96,70.46,40.5,70.6,40.22,71.01,40.24,70.65,39.94,69.56,40.1,69.46,39.53,70.55,39.6,71.78,39.28,73.68,39.43,73.93,38.51,74.26,38.61,74.86,38.38,74.83,37.99,74.98,37.42,73.95,37.42,73.26,37.5,72.64,37.05,72.19,36.95,71.84,36.74,71.45,37.07,71.54,37.91,71.24,37.95,71.35,38.26,70.81,38.49,70.38,38.14,70.27,37.74,70.12,37.59,69.52,37.61,69.2,37.15,68.86,37.34,68.14,37.02,67.83,37.14]]]},{"n":"Kyrgyzstan","c":"KG","r":4,"ml":3,"lx":74.53,"ly":41.67,"b":[69.46,39.28,80.26,43.3],"p":[[[70.96,42.27,71.19,42.7,71.84,42.85,73.49,42.5,73.65,43.09,74.21,43.3,75.64,42.88,76,42.99,77.66,42.96,79.14,42.86,79.64,42.5,80.26,42.35,80.12,42.12,78.54,41.58,78.19,41.19,76.9,41.07,76.53,40.43,75.47,40.56,74.78,40.37,73.82,39.89,73.96,39.66,73.68,39.43,71.78,39.28,70.55,39.6,69.46,39.53,69.56,40.1,70.65,39.94,71.01,40.24,71.77,40.15,73.06,40.87,71.87,41.39,71.16,41.14,70.42,41.52,71.26,42.17,70.96,42.27]]]},{"n":"Turkmenistan","c":"TM","r":4,"ml":3,"lx":58.68,"ly":39.86,"b":[52.5,35.27,66.55,42.75],"p":[[[52.5,41.78,52.94,42.12,54.08,42.32,54.76,42.04,55.46,41.26,55.97,41.31,57.1,41.32,56.93,41.83,57.79,42.17,58.63,42.75,59.98,42.22,60.08,41.43,60.47,41.22,61.55,41.27,61.88,41.08,62.37,40.05,63.52,39.36,64.17,38.89,65.22,38.4,66.55,37.97,66.52,37.36,66.22,37.39,65.75,37.66,65.59,37.31,64.75,37.11,64.55,36.31,63.98,36.01,63.19,35.86,62.98,35.4,62.23,35.27,61.21,35.65,61.12,36.49,60.38,36.53,59.23,37.41,58.44,37.52,57.33,38.03,56.62,38.12,56.18,37.94,55.51,37.96,54.8,37.39,53.92,37.2,53.74,37.91,53.88,38.95,53.1,39.29,53.36,39.98,52.69,40.03,52.92,40.88,53.86,40.63,54.74,40.95,54.01,41.55,53.72,42.12,52.92,41.87,52.81,41.14,52.5,41.78]]]},{"n":"Iran","c":"IR","r":2,"ml":2.5,"lx":54.93,"ly":32.17,"b":[44.11,25.08,63.32,39.71],"p":[[[48.57,29.93,48.01,30.45,48,30.99,47.69,30.98,47.85,31.71,47.33,32.47,46.11,33.02,45.42,33.97,45.65,34.75,46.15,35.09,46.08,35.68,45.42,35.98,44.77,37.17,44.23,37.97,44.42,38.28,44.11,39.43,44.79,39.71,44.95,39.34,45.46,38.87,46.14,38.74,46.51,38.77,47.69,39.51,48.06,39.58,48.36,39.29,48.01,38.79,48.63,38.27,48.88,38.32,49.2,37.58,50.15,37.37,50.84,36.87,52.26,36.7,53.83,36.97,53.92,37.2,54.8,37.39,55.51,37.96,56.18,37.94,56.62,38.12,57.33,38.03,58.44,37.52,59.23,37.41,60.38,36.53,61.12,36.49,61.21,35.65,60.8,34.4,60.53,33.68,60.96,33.53,60.54,32.98,60.86,32.18,60.94,31.55,61.7,31.38,61.78,30.74,60.87,29.83,61.37,29.3,61.77,28.7,62.73,28.26,62.76,27.38,63.23,27.22,63.32,26.76,61.87,26.24,61.5,25.08,59.62,25.38,58.53,25.61,57.4,25.74,56.97,26.97,56.49,27.14,55.72,26.96,54.72,26.48,53.49,26.81,52.48,27.58,51.52,27.87,50.85,28.81,50.12,30.15,49.58,29.99,48.94,30.32,48.57,29.93]]]},{"n":"Syria","c":"SY","r":3,"ml":3,"lx":38.28,"ly":35.01,"b":[35.7,32.31,42.35,37.23],"p":[[[35.72,32.71,35.7,32.72,35.84,32.87,35.82,33.28,36.07,33.82,36.61,34.2,36.45,34.59,36,34.64,35.91,35.41,36.15,35.82,36.42,36.04,36.69,36.26,36.74,36.82,37.07,36.62,38.17,36.9,38.7,36.71,39.52,36.72,40.67,37.09,41.21,37.07,42.35,37.23,41.84,36.61,41.29,36.36,41.38,35.63,41.01,34.42,38.79,33.38,36.83,32.31,35.72,32.71]]]},{"n":"Armenia","c":"AM","r":6,"ml":5,"lx":44.8,"ly":40.46,"b":[43.58,38.74,46.51,41.25],"p":[[[46.51,38.77,46.14,38.74,45.74,39.32,45.74,39.47,45.3,39.47,45,39.74,44.79,39.71,44.4,40.01,43.66,40.25,43.75,40.74,43.58,41.09,44.97,41.25,45.18,40.99,45.56,40.81,45.36,40.56,45.89,40.22,45.61,39.9,46.03,39.63,46.48,39.46,46.51,38.77]]]},{"n":"Sweden","c":"SE","r":3,"ml":2,"lx":19.02,"ly":65.86,"b":[11.03,55.36,23.9,69.11],"p":[[[11.03,58.86,11.47,59.43,12.3,60.12,12.63,61.29,11.99,61.8,11.93,63.13,12.58,64.07,13.57,64.05,13.92,64.45,13.56,64.79,15.11,66.19,16.11,67.3,16.77,68.01,17.73,68.01,17.99,68.57,19.88,68.41,20.03,69.07,20.65,69.11,21.98,68.62,23.54,67.94,23.57,66.4,23.9,66.01,22.18,65.72,21.21,65.03,21.37,64.41,19.78,63.61,17.85,62.75,17.12,61.34,17.83,60.64,18.79,60.08,17.87,58.95,16.83,58.72,16.45,57.04,15.88,56.1,14.67,56.2,14.1,55.41,12.94,55.36,12.63,56.31,11.79,57.44,11.03,58.86]]]},{"n":"Belarus","c":"BY","r":4,"ml":3,"lx":28.42,"ly":53.82,"b":[23.2,51.32,32.69,56.17],"p":[[[28.18,56.17,29.23,55.92,29.37,55.67,29.9,55.79,30.87,55.55,30.97,55.08,30.76,54.81,31.38,54.16,31.79,53.97,31.73,53.79,32.41,53.62,32.69,53.35,32.3,53.13,31.5,53.17,31.31,53.07,31.54,52.74,31.79,52.1,30.93,52.04,30.62,51.82,30.56,51.32,30.16,51.42,29.25,51.37,28.99,51.6,28.62,51.43,28.24,51.57,27.45,51.59,26.34,51.83,25.33,51.91,24.55,51.89,24.01,51.62,23.53,51.58,23.51,52.02,23.2,52.49,23.8,52.69,23.8,53.09,23.53,53.47,23.48,53.91,24.45,53.91,25.54,54.28,25.77,54.85,26.59,55.17,26.49,55.62,27.1,55.78,28.18,56.17]]]},{"n":"Ukraine","c":"UA","r":3,"ml":2.7,"lx":32.14,"ly":49.72,"b":[22.09,45.29,40.08,52.34],"p":[[[31.79,52.1,32.16,52.06,32.41,52.29,32.72,52.24,33.75,52.34,34.39,51.77,34.14,51.57,34.22,51.26,35.02,51.21,35.38,50.77,35.36,50.58,36.63,50.23,37.39,50.38,38.01,49.92,38.59,49.93,40.07,49.6,40.08,49.31,39.67,48.78,39.9,48.23,39.74,47.9,38.77,47.83,38.26,47.55,38.22,47.1,37.43,47.02,36.76,46.7,35.82,46.65,34.96,46.27,35.01,45.74,34.86,45.77,34.73,45.97,34.41,46.01,33.7,46.22,33.44,45.97,33.3,46.08,31.74,46.33,31.68,46.71,30.75,46.58,30.38,46.03,29.6,45.29,29.15,45.46,28.68,45.3,28.23,45.49,28.49,45.6,28.66,45.94,28.93,46.26,28.86,46.44,29.07,46.52,29.17,46.38,29.76,46.35,30.02,46.42,29.84,46.53,29.91,46.67,29.56,46.93,29.42,47.35,29.05,47.51,29.12,47.85,28.67,48.12,28.26,48.16,27.52,48.47,26.86,48.37,26.62,48.22,26.2,48.22,25.95,47.99,25.21,47.89,24.87,47.74,24.4,47.98,23.76,47.99,23.14,48.1,22.71,47.88,22.64,48.15,22.09,48.42,22.28,48.83,22.56,49.09,22.78,49.03,22.52,49.48,23.43,50.31,23.92,50.42,24.03,50.71,23.53,51.58,24.01,51.62,24.55,51.89,25.33,51.91,26.34,51.83,27.45,51.59,28.24,51.57,28.62,51.43,28.99,51.6,29.25,51.37,30.16,51.42,30.56,51.32,30.62,51.82,30.93,52.04,31.79,52.1]]]},{"n":"Poland","c":"PL","r":3,"ml":2.5,"lx":19.49,"ly":51.99,"b":[14.07,49.03,24.03,54.85],"p":[[[23.48,53.91,23.53,53.47,23.8,53.09,23.8,52.69,23.2,52.49,23.51,52.02,23.53,51.58,24.03,50.71,23.92,50.42,23.43,50.31,22.52,49.48,22.78,49.03,22.56,49.09,21.61,49.47,20.89,49.33,20.42,49.43,19.83,49.22,19.32,49.57,18.91,49.44,18.85,49.5,18.39,49.99,17.65,50.05,17.55,50.36,16.87,50.47,16.72,50.22,16.18,50.42,16.24,50.7,15.49,50.78,15.02,51.11,14.61,51.75,14.69,52.09,14.44,52.62,14.07,52.98,14.35,53.25,14.12,53.76,14.8,54.05,16.36,54.51,17.62,54.85,18.62,54.68,18.7,54.44,19.66,54.43,20.89,54.31,22.73,54.33,23.24,54.22,23.48,53.91]]]},{"n":"Austria","c":"AT","r":4,"ml":3,"lx":14.13,"ly":47.52,"b":[9.48,46.43,16.98,49.04],"p":[[[16.98,48.12,16.9,47.71,16.34,47.71,16.53,47.5,16.2,46.85,16.01,46.68,15.14,46.66,14.63,46.43,13.81,46.51,12.38,46.77,12.15,47.12,11.16,46.94,11.05,46.75,10.44,46.89,9.93,46.92,9.48,47.1,9.63,47.35,9.59,47.53,9.9,47.58,10.4,47.3,10.54,47.57,11.43,47.52,12.14,47.7,12.62,47.67,12.93,47.47,13.03,47.64,12.88,48.29,13.24,48.42,13.6,48.88,14.34,48.56,14.9,48.96,15.25,49.04,16.03,48.73,16.5,48.79,16.96,48.6,16.88,48.47,16.98,48.12]]]},{"n":"Hungary","c":"HU","r":5,"ml":4,"lx":19.45,"ly":47.09,"b":[16.2,45.76,22.71,48.62],"p":[[[22.09,48.42,22.64,48.15,22.71,47.88,22.1,47.67,21.63,46.99,21.02,46.32,20.22,46.13,19.6,46.17,18.83,45.91,18.46,45.76,17.63,45.95,16.88,46.38,16.56,46.5,16.37,46.84,16.2,46.85,16.53,47.5,16.34,47.71,16.9,47.71,16.98,48.12,17.49,47.87,17.86,47.76,18.7,47.88,18.78,48.08,19.17,48.11,19.66,48.27,19.77,48.2,20.24,48.33,20.47,48.56,20.8,48.62,21.87,48.32,22.09,48.42]]]},{"n":"Moldova","c":"MD","r":6,"ml":5,"lx":28.49,"ly":47.43,"b":[26.62,45.49,30.02,48.47],"p":[[[26.62,48.22,26.86,48.37,27.52,48.47,28.26,48.16,28.67,48.12,29.12,47.85,29.05,47.51,29.42,47.35,29.56,46.93,29.91,46.67,29.84,46.53,30.02,46.42,29.76,46.35,29.17,46.38,29.07,46.52,28.86,46.44,28.93,46.26,28.66,45.94,28.49,45.6,28.23,45.49,28.05,45.94,28.16,46.37,28.13,46.81,27.55,47.41,27.23,47.83,26.92,48.12,26.62,48.22]]]},{"n":"Romania","c":"RO","r":3,"ml":3,"lx":24.97,"ly":45.73,"b":[20.22,43.69,29.63,48.22],"p":[[[28.23,45.49,28.68,45.3,29.15,45.46,29.6,45.29,29.63,45.04,29.14,44.82,28.84,44.91,28.56,43.71,27.97,43.81,27.24,44.18,26.07,43.94,25.57,43.69,24.1,43.74,23.33,43.9,22.94,43.82,22.66,44.23,22.47,44.41,22.71,44.58,22.46,44.7,22.15,44.48,21.56,44.77,21.48,45.18,20.87,45.42,20.76,45.73,20.22,46.13,21.02,46.32,21.63,46.99,22.1,47.67,22.71,47.88,23.14,48.1,23.76,47.99,24.4,47.98,24.87,47.74,25.21,47.89,25.95,47.99,26.2,48.22,26.62,48.22,26.92,48.12,27.23,47.83,27.55,47.41,28.13,46.81,28.16,46.37,28.05,45.94,28.23,45.49]]]},{"n":"Lithuania","c":"LT","r":5,"ml":4,"lx":24.09,"ly":55.1,"b":[21.06,53.91,26.59,56.37],"p":[[[26.49,55.62,26.59,55.17,25.77,54.85,25.54,54.28,24.45,53.91,23.48,53.91,23.24,54.22,22.73,54.33,22.65,54.58,22.76,54.86,22.32,55.02,21.27,55.19,21.06,56.03,22.2,56.34,23.88,56.27,24.86,56.37,25,56.16,25.53,56.1,26.49,55.62]]]},{"n":"Latvia","c":"LV","r":5,"ml":4,"lx":25.46,"ly":57.07,"b":[21.06,55.62,28.18,57.97],"p":[[[27.29,57.47,27.77,57.24,27.86,56.76,28.18,56.17,27.1,55.78,26.49,55.62,25.53,56.1,25,56.16,24.86,56.37,23.88,56.27,22.2,56.34,21.06,56.03,21.09,56.78,21.58,57.41,22.52,57.75,23.32,57.01,24.12,57.03,24.31,57.79,25.16,57.97,25.6,57.85,26.46,57.48,27.29,57.47]]]},{"n":"Estonia","c":"EE","r":6,"ml":3,"lx":25.87,"ly":58.72,"b":[23.34,57.47,28.13,59.61],"p":[[[27.98,59.48,28.13,59.3,27.42,58.72,27.72,57.79,27.29,57.47,26.46,57.48,25.6,57.85,25.16,57.97,24.31,57.79,24.43,58.38,24.06,58.26,23.43,58.61,23.34,59.19,24.6,59.47,25.86,59.61,26.95,59.45,27.98,59.48]]]},{"n":"Germany","c":"DE","r":2,"ml":1.7,"lx":9.68,"ly":50.96,"b":[5.99,47.3,15.02,54.98],"p":[[[14.12,53.76,14.35,53.25,14.07,52.98,14.44,52.62,14.69,52.09,14.61,51.75,15.02,51.11,14.57,51,14.31,51.12,14.06,50.93,13.34,50.73,12.97,50.48,12.24,50.27,12.42,49.97,12.52,49.55,13.03,49.31,13.6,48.88,13.24,48.42,12.88,48.29,13.03,47.64,12.93,47.47,12.62,47.67,12.14,47.7,11.43,47.52,10.54,47.57,10.4,47.3,9.9,47.58,9.59,47.53,8.52,47.83,8.32,47.61,7.47,47.62,7.59,48.33,8.1,49.02,6.66,49.2,6.19,49.46,6.24,49.9,6.04,50.13,6.16,50.8,5.99,51.85,6.59,51.85,6.84,52.23,7.09,53.14,6.91,53.48,7.1,53.69,7.94,53.75,8.12,53.53,8.8,54.02,8.57,54.4,8.53,54.96,9.28,54.83,9.92,54.98,9.94,54.6,10.95,54.36,10.94,54.01,11.96,54.2,12.52,54.47,13.65,54.08,14.12,53.76]]]},{"n":"Bulgaria","c":"BG","r":4,"ml":4,"lx":25.16,"ly":42.51,"b":[22.38,41.23,28.56,44.23],"p":[[[22.66,44.23,22.94,43.82,23.33,43.9,24.1,43.74,25.57,43.69,26.07,43.94,27.24,44.18,27.97,43.81,28.56,43.71,28.04,43.29,27.67,42.58,28,42.01,27.14,42.14,26.12,41.83,26.11,41.33,25.2,41.23,24.49,41.58,23.69,41.31,22.95,41.34,22.88,42,22.38,42.32,22.55,42.46,22.44,42.58,22.6,42.9,22.99,43.21,22.5,43.64,22.41,44.01,22.66,44.23]]]},{"n":"Greece","c":"GR","r":3,"ml":2.7,"lx":21.73,"ly":39.49,"b":[20.15,34.92,26.6,41.83],"p":[[[26.29,35.3,26.16,35,24.72,34.92,24.74,35.08,23.51,35.28,23.7,35.71,24.25,35.37,25.03,35.42,25.77,35.35,25.75,35.18,26.29,35.3]],[[22.95,41.34,23.69,41.31,24.49,41.58,25.2,41.23,26.11,41.33,26.12,41.83,26.6,41.56,26.29,40.94,26.06,40.82,25.45,40.85,24.93,40.95,23.71,40.69,24.41,40.12,23.9,39.96,23.34,39.96,22.81,40.48,22.63,40.26,22.85,39.66,23.35,39.19,22.97,38.97,23.53,38.51,24.03,38.22,24.04,37.66,23.12,37.92,23.41,37.41,22.77,37.31,23.15,36.42,22.49,36.41,21.67,36.84,21.3,37.64,21.12,38.31,20.73,38.77,20.22,39.34,20.15,39.62,20.62,40.11,20.67,40.44,21,40.58,21.02,40.84,21.67,40.93,22.06,41.15,22.6,41.13,22.76,41.3,22.95,41.34]]]},{"n":"Turkey","c":"TR","r":2,"ml":2,"lx":34.51,"ly":39.35,"b":[26.04,35.82,44.79,42.14],"p":[[[44.77,37.17,44.29,37,43.94,37.26,42.78,37.39,42.35,37.23,41.21,37.07,40.67,37.09,39.52,36.72,38.7,36.71,38.17,36.9,37.07,36.62,36.74,36.82,36.69,36.26,36.42,36.04,36.15,35.82,35.78,36.27,36.16,36.65,35.55,36.57,34.71,36.8,34.03,36.22,32.51,36.11,31.7,36.64,30.62,36.68,30.39,36.26,29.7,36.14,28.73,36.68,27.64,36.66,27.05,37.65,26.32,38.21,26.8,38.99,26.17,39.46,27.28,40.42,28.82,40.46,29.24,41.22,31.15,41.09,32.35,41.74,33.51,42.02,35.17,42.04,36.91,41.34,38.35,40.95,39.51,41.1,40.37,41.01,41.55,41.54,42.62,41.58,43.58,41.09,43.75,40.74,43.66,40.25,44.4,40.01,44.79,39.71,44.11,39.43,44.42,38.28,44.23,37.97,44.77,37.17]],[[26.12,41.83,27.14,42.14,28,42.01,28.12,41.62,28.99,41.3,28.81,41.05,27.62,41,27.19,40.69,26.36,40.15,26.04,40.62,26.06,40.82,26.29,40.94,26.6,41.56,26.12,41.83]]]},{"n":"Albania","c":"AL","r":6,"ml":5,"lx":20.11,"ly":40.65,"b":[19.3,39.62,21.02,42.69],"p":[[[21.02,40.84,21,40.58,20.67,40.44,20.62,40.11,20.15,39.62,19.98,39.69,19.96,39.92,19.41,40.25,19.32,40.73,19.4,41.41,19.54,41.72,19.37,41.88,19.3,42.2,19.74,42.69,19.8,42.5,20.07,42.59,20.28,42.32,20.52,42.22,20.59,41.86,20.46,41.52,20.61,41.09,21.02,40.84]]]},{"n":"Croatia","c":"HR","r":6,"ml":4,"lx":16.37,"ly":45.81,"b":[13.66,42.48,19.39,46.5],"p":[[[16.56,46.5,16.88,46.38,17.63,45.95,18.46,45.76,18.83,45.91,19.07,45.52,19.39,45.24,19.01,44.86,18.55,45.08,17.86,45.07,17,45.23,16.53,45.21,16.32,45,15.96,45.23,15.75,44.82,16.24,44.35,16.46,44.04,16.92,43.67,17.3,43.45,17.67,43.03,18.56,42.65,18.45,42.48,17.51,42.85,16.93,43.21,16.02,43.51,15.17,44.24,15.38,44.32,14.92,44.74,14.9,45.08,14.26,45.23,13.95,44.8,13.66,45.14,13.68,45.48,13.72,45.5,14.41,45.47,14.6,45.63,14.94,45.47,15.33,45.45,15.32,45.73,15.67,45.83,15.77,46.24,16.56,46.5]]]},{"n":"Switzerland","c":"CH","r":4,"ml":4,"lx":7.46,"ly":46.72,"b":[6.02,45.78,10.44,47.83],"p":[[[9.59,47.53,9.63,47.35,9.48,47.1,9.93,46.92,10.44,46.89,10.36,46.48,9.92,46.31,9.18,46.44,8.97,46.04,8.49,46.01,8.32,46.16,7.76,45.82,7.27,45.78,6.84,45.99,6.5,46.43,6.02,46.27,6.04,46.73,6.77,47.29,6.74,47.54,7.19,47.45,7.47,47.62,8.32,47.61,8.52,47.83,9.59,47.53]]]},{"n":"Luxembourg","c":"LU","r":6,"ml":5.7,"lx":6.08,"ly":49.73,"b":[5.67,49.44,6.24,50.13],"p":[[[6.04,50.13,6.24,49.9,6.19,49.46,5.9,49.44,5.67,49.53,5.78,50.09,6.04,50.13]]]},{"n":"Belgium","c":"BE","r":2,"ml":4,"lx":4.8,"ly":50.79,"b":[2.51,49.53,6.16,51.48],"p":[[[6.16,50.8,6.04,50.13,5.78,50.09,5.67,49.53,4.8,49.99,4.29,49.91,3.59,50.38,3.12,50.78,2.66,50.8,2.51,51.15,3.31,51.35,3.32,51.35,3.31,51.35,4.05,51.27,4.97,51.48,5.61,51.04,6.16,50.8]]]},{"n":"Netherlands","c":"NL","r":5,"ml":4,"lx":5.61,"ly":52.42,"b":[3.31,50.8,7.09,53.51],"p":[[[6.91,53.48,7.09,53.14,6.84,52.23,6.59,51.85,5.99,51.85,6.16,50.8,5.61,51.04,4.97,51.48,4.05,51.27,3.31,51.35,3.32,51.35,3.83,51.62,4.71,53.09,6.07,53.51,6.91,53.48]]]},{"n":"Portugal","c":"PT","r":2,"ml":3,"lx":-8.27,"ly":39.61,"b":[-9.53,36.84,-6.39,42.28],"p":[[[-9.03,41.88,-8.67,42.13,-8.26,42.28,-8.01,41.79,-7.42,41.79,-7.25,41.92,-6.67,41.88,-6.39,41.38,-6.85,41.11,-6.86,40.33,-7.03,40.18,-7.07,39.71,-7.5,39.63,-7.1,39.03,-7.37,38.37,-7.03,38.08,-7.17,37.8,-7.54,37.43,-7.45,37.1,-7.86,36.84,-8.38,36.98,-8.9,36.87,-8.75,37.65,-8.84,38.27,-9.29,38.36,-9.53,38.74,-9.45,39.39,-9.05,39.76,-8.98,40.16,-8.77,40.76,-8.79,41.18,-8.99,41.54,-9.03,41.88]]]},{"n":"Spain","c":"ES","r":2,"ml":2,"lx":-3.46,"ly":40.09,"b":[-9.39,35.95,3.04,43.75],"p":[[[-7.45,37.1,-7.54,37.43,-7.17,37.8,-7.03,38.08,-7.37,38.37,-7.1,39.03,-7.5,39.63,-7.07,39.71,-7.03,40.18,-6.86,40.33,-6.85,41.11,-6.39,41.38,-6.67,41.88,-7.25,41.92,-7.42,41.79,-8.01,41.79,-8.26,42.28,-8.67,42.13,-9.03,41.88,-8.98,42.59,-9.39,43.03,-7.98,43.75,-6.75,43.57,-5.41,43.57,-4.35,43.4,-3.52,43.46,-1.9,43.42,-1.5,43.03,0.34,42.58,0.7,42.8,1.83,42.34,2.99,42.47,3.04,41.89,2.09,41.23,0.81,41.01,0.72,40.68,0.11,40.12,-0.28,39.31,0.11,38.74,-0.47,38.29,-0.68,37.64,-1.44,37.44,-2.15,36.67,-3.42,36.66,-4.37,36.68,-5,36.32,-5.38,35.95,-5.87,36.03,-6.24,36.37,-6.52,36.94,-7.45,37.1]]]},{"n":"Ireland","c":"IE","r":3,"ml":3,"lx":-7.8,"ly":53.08,"b":[-9.98,51.67,-6.03,55.13],"p":[[[-6.2,53.87,-6.03,53.15,-6.79,52.26,-8.56,51.67,-9.98,51.82,-9.17,52.86,-9.69,53.88,-8.33,54.66,-7.57,55.13,-7.37,54.6,-7.57,54.06,-6.95,54.07,-6.2,53.87]]]},{"n":"New Caledonia","c":"NC","r":3,"ml":4.6,"lx":165.08,"ly":-21.06,"b":[164.03,-22.4,167.12,-20.11],"p":[[[165.78,-21.08,166.6,-21.7,167.12,-22.16,166.74,-22.4,166.19,-22.13,165.47,-21.68,164.83,-21.15,164.17,-20.44,164.03,-20.11,164.46,-20.12,165.02,-20.46,165.46,-20.8,165.78,-21.08]]]},{"n":"Solomon Islands","c":"SB","r":3,"ml":3,"lx":159.17,"ly":-8.03,"b":[156.49,-10.83,162.4,-6.6],"p":[[[162.12,-10.48,162.4,-10.83,161.7,-10.82,161.32,-10.2,161.92,-10.45,162.12,-10.48]],[[161.68,-9.6,161.53,-9.78,160.79,-8.92,160.58,-8.32,160.92,-8.32,161.28,-9.12,161.68,-9.6]],[[160.85,-9.87,160.46,-9.9,159.85,-9.79,159.64,-9.64,159.7,-9.24,160.36,-9.4,160.69,-9.61,160.85,-9.87]],[[159.64,-8.02,159.88,-8.34,159.92,-8.54,159.13,-8.11,158.59,-7.75,158.21,-7.42,158.36,-7.32,158.82,-7.56,159.64,-8.02]],[[157.14,-7.02,157.54,-7.35,157.34,-7.4,156.9,-7.18,156.49,-6.77,156.54,-6.6,157.14,-7.02]]]},{"n":"New Zealand","c":"NZ","r":2,"ml":2,"lx":172.79,"ly":-39.76,"b":[166.51,-46.64,178.52,-34.45],"p":[[[176.89,-40.07,176.51,-40.6,176.01,-41.29,175.24,-41.69,175.07,-41.43,174.65,-41.28,175.23,-40.46,174.9,-39.91,173.82,-39.51,173.85,-39.15,174.57,-38.8,174.74,-38.03,174.7,-37.38,174.29,-36.71,174.32,-36.53,173.84,-36.12,173.05,-35.24,172.64,-34.53,173.01,-34.45,173.55,-35.01,174.33,-35.27,174.61,-36.16,175.34,-37.21,175.36,-36.53,175.81,-36.8,175.96,-37.56,176.76,-37.88,177.44,-37.96,178.01,-37.58,178.52,-37.7,178.27,-38.58,177.97,-39.17,177.21,-39.15,176.94,-39.45,177.03,-39.88,176.89,-40.07]],[[169.67,-43.56,170.52,-43.03,171.13,-42.51,171.57,-41.77,171.95,-41.51,172.1,-40.96,172.8,-40.49,173.02,-40.92,173.25,-41.33,173.96,-40.93,174.25,-41.35,174.25,-41.77,173.88,-42.23,173.22,-42.97,172.71,-43.37,173.08,-43.85,172.31,-43.87,171.45,-44.24,171.19,-44.9,170.62,-45.91,169.83,-46.36,169.33,-46.64,168.41,-46.62,167.76,-46.29,166.68,-46.22,166.51,-45.85,167.05,-45.11,168.3,-44.12,168.95,-43.94,169.67,-43.56]]]},{"n":"Australia","c":"AU","r":2,"ml":1.7,"lx":134.05,"ly":-24.13,"b":[113.34,-43.63,153.57,-10.67],"p":[[[147.69,-40.81,148.29,-40.88,148.36,-42.06,148.02,-42.41,147.91,-43.21,147.56,-42.94,146.87,-43.63,146.66,-43.58,146.05,-43.55,145.43,-42.69,145.3,-42.03,144.72,-41.16,144.74,-40.7,145.4,-40.79,146.36,-41.14,146.91,-41,147.69,-40.81]],[[126.15,-32.22,125.09,-32.73,124.22,-32.96,124.03,-33.48,123.66,-33.89,122.81,-33.91,122.18,-34,121.3,-33.82,120.58,-33.93,119.89,-33.98,119.3,-34.51,119.01,-34.46,118.51,-34.75,118.02,-35.06,117.3,-35.03,116.63,-35.03,115.56,-34.39,115.03,-34.2,115.05,-33.62,115.55,-33.49,115.71,-33.26,115.68,-32.9,115.8,-32.21,115.69,-31.61,115.16,-30.6,115,-30.03,115.04,-29.46,114.64,-28.81,114.62,-28.52,114.17,-28.12,114.05,-27.33,113.48,-26.54,113.34,-26.12,113.78,-26.55,113.44,-25.62,113.94,-25.91,114.23,-26.3,114.22,-25.79,113.72,-25,113.63,-24.68,113.39,-24.38,113.5,-23.81,113.71,-23.56,113.84,-23.06,113.74,-22.48,114.15,-21.76,114.23,-22.52,114.65,-21.83,115.46,-21.5,115.95,-21.07,116.71,-20.7,117.17,-20.62,117.44,-20.75,118.23,-20.37,118.84,-20.26,118.99,-20.04,119.25,-19.95,119.81,-19.98,120.86,-19.68,121.4,-19.24,121.66,-18.71,122.24,-18.2,122.29,-17.8,122.31,-17.25,123.01,-16.41,123.43,-17.27,123.86,-17.07,123.5,-16.6,123.82,-16.11,124.26,-16.33,124.38,-15.57,124.93,-15.08,125.17,-14.68,125.67,-14.51,125.69,-14.23,126.13,-14.35,126.14,-14.1,126.58,-13.95,127.07,-13.82,127.8,-14.28,128.36,-14.87,128.99,-14.88,129.62,-14.97,129.41,-14.42,129.89,-13.62,130.34,-13.36,130.18,-13.11,130.62,-12.54,131.22,-12.18,131.74,-12.3,132.58,-12.11,132.56,-11.6,131.82,-11.27,132.36,-11.13,133.02,-11.38,133.55,-11.79,134.39,-12.04,134.68,-11.94,135.3,-12.25,135.88,-11.96,136.26,-12.05,136.49,-11.86,136.95,-12.35,136.69,-12.89,136.31,-13.29,135.96,-13.32,136.08,-13.72,135.78,-14.22,135.43,-14.72,135.5,-15,136.3,-15.55,137.07,-15.87,137.58,-16.22,138.3,-16.81,138.59,-16.81,139.11,-17.06,139.26,-17.37,140.22,-17.71,140.88,-17.37,141.07,-16.83,141.27,-16.39,141.4,-15.84,141.7,-15.04,141.56,-14.56,141.64,-14.27,141.52,-13.7,141.65,-12.94,141.84,-12.74,141.69,-12.41,141.93,-11.88,142.12,-11.33,142.14,-11.04,142.52,-10.67,142.8,-11.16,142.87,-11.78,143.12,-11.91,143.16,-12.33,143.52,-12.83,143.6,-13.4,143.56,-13.76,143.92,-14.55,144.56,-14.17,144.89,-14.59,145.37,-14.98,145.27,-15.43,145.49,-16.29,145.64,-16.78,145.89,-16.91,146.16,-17.76,146.06,-18.28,146.39,-18.96,147.47,-19.48,148.18,-19.96,148.85,-20.39,148.72,-20.63,149.29,-21.26,149.68,-22.34,150.08,-22.12,150.48,-22.56,150.73,-22.4,150.9,-23.46,151.61,-24.08,152.07,-24.46,152.86,-25.27,153.14,-26.07,153.16,-26.64,153.09,-27.26,153.57,-28.11,153.51,-29,153.34,-29.46,153.07,-30.35,153.09,-30.92,152.89,-31.64,152.45,-32.55,151.71,-33.04,151.34,-33.82,151.01,-34.31,150.71,-35.17,150.33,-35.67,150.08,-36.42,149.95,-37.11,150,-37.43,149.42,-37.77,148.3,-37.81,147.38,-38.22,146.92,-38.61,146.32,-39.04,145.49,-38.59,144.88,-38.42,145.03,-37.9,144.49,-38.09,143.61,-38.81,142.75,-38.54,142.18,-38.38,141.61,-38.31,140.64,-38.02,139.99,-37.4,139.81,-36.64,139.57,-36.14,139.08,-35.73,138.12,-35.61,138.45,-35.13,138.21,-34.38,137.72,-35.08,136.83,-35.26,137.35,-34.71,137.5,-34.13,137.89,-33.64,137.81,-32.9,137,-33.75,136.37,-34.09,135.99,-34.89,135.21,-34.48,135.24,-33.95,134.61,-33.22,134.09,-32.85,134.27,-32.62,132.99,-32.01,132.29,-31.98,131.33,-31.5,129.54,-31.59,128.24,-31.95,127.1,-32.28,126.15,-32.22]]]},{"n":"Sri Lanka","c":"LK","r":3,"ml":3,"lx":80.7,"ly":7.58,"b":[79.7,5.97,81.79,9.82],"p":[[[81.79,7.52,81.64,6.48,81.22,6.2,80.35,5.97,79.87,6.76,79.7,8.2,80.15,9.82,80.84,9.27,81.3,8.56,81.79,7.52]]]},{"n":"People's Republic of China","c":"CN","r":2,"ml":1.7,"lx":106.34,"ly":32.5,"b":[73.68,18.2,135.03,53.46],"p":[[[109.48,18.2,108.66,18.51,108.63,19.37,109.12,19.82,110.21,20.1,110.79,20.08,111.01,19.7,110.57,19.26,110.34,18.68,109.48,18.2]],[[80.26,42.35,80.18,42.92,80.87,43.18,79.97,44.92,81.95,45.32,82.46,45.54,83.18,47.33,85.16,47,85.72,47.45,85.77,48.46,86.6,48.55,87.36,49.21,87.75,49.3,88.01,48.6,88.85,48.07,90.28,47.69,90.97,46.89,90.59,45.72,90.95,45.29,92.13,45.12,93.48,44.98,94.69,44.35,95.31,44.24,95.76,43.32,96.35,42.73,97.45,42.75,99.52,42.52,100.85,42.66,101.83,42.51,103.31,41.91,104.52,41.91,104.96,41.6,106.13,42.13,107.74,42.48,109.24,42.52,110.41,42.87,111.13,43.41,111.83,43.74,111.67,44.07,111.35,44.46,111.87,45.1,112.44,45.01,113.46,44.81,114.46,45.34,115.99,45.73,116.72,46.39,117.42,46.67,118.87,46.81,119.66,46.69,119.77,47.05,118.87,47.75,118.06,48.07,117.3,47.7,116.31,47.85,115.74,47.73,115.49,48.14,116.19,49.13,116.68,49.89,117.88,49.51,119.29,50.14,119.28,50.58,120.18,51.64,120.74,51.96,120.73,52.52,120.18,52.75,121,53.25,122.25,53.43,123.57,53.46,125.07,53.16,125.95,52.79,126.56,51.78,126.94,51.35,127.29,50.74,127.66,49.76,129.4,49.44,130.58,48.73,130.99,47.79,132.51,47.79,133.37,48.18,135.03,48.48,134.5,47.58,134.11,47.21,133.77,46.12,133.1,45.14,131.88,45.32,131.03,44.97,131.29,44.11,131.14,42.93,130.63,42.9,130.64,42.4,129.99,42.99,129.6,42.42,128.05,41.99,128.21,41.47,127.34,41.5,126.87,41.82,126.18,41.11,125.08,40.57,124.27,39.93,122.87,39.64,122.13,39.17,121.05,38.9,121.59,39.36,121.38,39.75,122.17,40.42,121.64,40.95,120.77,40.59,119.64,39.9,119.02,39.25,118.04,39.2,117.53,38.74,118.06,38.06,118.88,37.9,118.91,37.45,119.7,37.16,120.82,37.87,121.71,37.48,122.36,37.45,122.52,36.93,121.1,36.65,120.64,36.11,119.66,35.61,119.15,34.91,120.23,34.36,120.62,33.38,121.23,32.46,121.91,31.69,121.89,30.95,121.26,30.68,121.5,30.14,122.09,29.83,121.94,29.02,121.68,28.23,121.13,28.14,120.4,27.05,119.59,25.74,118.66,24.55,117.28,23.62,115.89,22.78,114.76,22.67,114.15,22.22,113.81,22.55,113.24,22.05,111.84,21.55,110.79,21.4,110.44,20.34,109.89,20.28,109.63,21.01,109.86,21.4,108.52,21.72,108.05,21.55,107.04,21.81,106.57,22.22,106.73,22.79,105.81,22.98,105.33,23.35,104.48,22.82,103.5,22.7,102.71,22.71,102.17,22.46,101.65,22.32,101.8,21.17,101.27,21.2,101.18,21.44,101.15,21.85,100.42,21.56,99.98,21.74,99.24,22.12,99.53,22.95,98.9,23.14,98.66,24.06,97.6,23.9,97.72,25.08,98.67,25.92,98.71,26.74,98.68,27.51,98.25,27.75,97.91,28.34,97.33,28.26,96.25,28.41,96.59,28.83,96.12,29.45,95.4,29.03,94.57,29.28,93.41,28.64,92.5,27.9,91.7,27.77,91.26,28.04,90.73,28.06,90.02,28.3,89.48,28.04,88.81,27.3,88.73,28.09,88.12,27.88,86.95,27.97,85.82,28.2,85.01,28.64,84.23,28.84,83.9,29.32,83.34,29.46,82.33,30.12,81.53,30.42,81.11,30.18,79.72,30.88,78.74,31.52,78.46,32.62,79.18,32.48,79.21,32.99,78.81,33.51,78.91,34.32,77.84,35.49,76.19,35.9,75.9,36.67,75.16,37.13,74.98,37.42,74.83,37.99,74.86,38.38,74.26,38.61,73.93,38.51,73.68,39.43,73.96,39.66,73.82,39.89,74.78,40.37,75.47,40.56,76.53,40.43,76.9,41.07,78.19,41.19,78.54,41.58,80.12,42.12,80.26,42.35]]]},{"n":"Taiwan","c":"TW","r":3,"ml":4.5,"lx":120.87,"ly":23.65,"b":[120.11,21.97,121.95,25.3],"p":[[[121.78,24.39,121.18,22.79,120.75,21.97,120.22,22.81,120.11,23.56,120.69,24.54,121.5,25.3,121.95,25,121.78,24.39]]]},{"n":"Italy","c":"IT","r":2,"ml":2,"lx":11.08,"ly":44.73,"b":[6.75,36.62,18.48,47.12],"p":[[[10.44,46.89,11.05,46.75,11.16,46.94,12.15,47.12,12.38,46.77,13.81,46.51,13.7,46.02,13.94,45.59,13.14,45.74,12.33,45.38,12.38,44.89,12.26,44.6,12.59,44.09,13.53,43.59,14.03,42.76,15.14,41.96,15.93,41.96,16.17,41.74,15.89,41.54,16.79,41.18,17.52,40.88,18.38,40.36,18.48,40.17,18.29,39.81,17.74,40.28,16.87,40.44,16.45,39.8,17.17,39.42,17.05,38.9,16.64,38.84,16.1,37.99,15.68,37.91,15.69,38.21,15.89,38.75,16.11,38.96,15.72,39.54,15.41,40.05,15,40.17,14.7,40.6,14.06,40.79,13.63,41.19,12.89,41.25,12.11,41.7,11.19,42.36,10.51,42.93,10.2,43.92,9.7,44.04,8.89,44.37,8.43,44.23,7.85,43.77,7.44,43.69,7.55,44.13,7.01,44.25,6.75,45.03,7.1,45.33,6.8,45.71,6.84,45.99,7.27,45.78,7.76,45.82,8.32,46.16,8.49,46.01,8.97,46.04,9.18,46.44,9.92,46.31,10.36,46.48,10.44,46.89]],[[14.76,38.14,15.52,38.23,15.16,37.44,15.31,37.13,15.1,36.62,14.34,37,13.83,37.1,12.43,37.61,12.57,38.13,13.74,38.03,14.76,38.14]],[[8.71,40.9,9.21,41.21,9.81,40.5,9.67,39.18,9.21,39.24,8.81,38.91,8.43,39.17,8.39,40.38,8.16,40.95,8.71,40.9]]]},{"n":"Denmark","c":"DK","r":4,"ml":3,"lx":9.02,"ly":55.97,"b":[8.09,54.8,12.69,57.73],"p":[[[9.92,54.98,9.28,54.83,8.53,54.96,8.12,55.52,8.09,56.54,8.26,56.81,8.54,57.11,9.42,57.17,9.78,57.45,10.58,57.73,10.55,57.22,10.25,56.89,10.37,56.61,10.91,56.46,10.67,56.08,10.37,56.19,9.65,55.47,9.92,54.98]],[[12.37,56.11,12.69,55.61,12.09,54.8,11.04,55.36,10.9,55.78,12.37,56.11]]]},{"n":"United Kingdom","c":"GB","r":2,"ml":1.7,"lx":-2.12,"ly":54.4,"b":[-7.57,49.96,1.68,58.64],"p":[[[-6.2,53.87,-6.95,54.07,-7.57,54.06,-7.37,54.6,-7.57,55.13,-6.73,55.17,-5.66,54.55,-6.2,53.87]],[[-3.09,53.4,-2.95,53.99,-3.61,54.6,-3.63,54.62,-4.84,54.79,-5.08,55.06,-4.72,55.51,-5.05,55.78,-5.59,55.31,-5.64,56.28,-6.15,56.79,-5.79,57.82,-5.01,58.63,-4.21,58.55,-3.01,58.64,-4.07,57.55,-3.06,57.69,-1.96,57.68,-2.22,56.87,-3.12,55.97,-2.09,55.91,-2.01,55.8,-1.11,54.62,-0.43,54.46,0.18,53.33,0.47,52.93,1.68,52.74,1.56,52.1,1.05,51.81,1.45,51.29,0.55,50.77,-0.79,50.77,-2.49,50.5,-2.96,50.7,-3.62,50.23,-4.54,50.34,-5.25,49.96,-5.78,50.16,-4.31,51.21,-3.41,51.43,-3.42,51.43,-4.98,51.59,-5.27,51.99,-4.22,52.3,-4.77,52.84,-4.58,53.5,-3.09,53.4]]]},{"n":"Iceland","c":"IS","r":3,"ml":2,"lx":-18.67,"ly":64.78,"b":[-24.33,63.5,-13.61,66.53],"p":[[[-14.51,66.46,-14.74,65.81,-13.61,65.13,-14.91,64.36,-17.79,63.68,-18.66,63.5,-19.97,63.64,-22.76,63.96,-21.78,64.4,-23.96,64.89,-22.18,65.08,-22.23,65.38,-24.33,65.61,-23.65,66.26,-22.13,66.41,-20.58,65.73,-19.06,66.28,-17.8,65.99,-16.17,66.53,-14.51,66.46]]]},{"n":"Azerbaijan","c":"AZ","r":5,"ml":4,"lx":47.21,"ly":40.4,"b":[44.79,38.27,50.39,41.86],"p":[[[46.4,41.86,46.69,41.83,47.37,41.22,47.82,41.15,47.99,41.41,48.58,41.81,49.11,41.28,49.62,40.57,50.08,40.53,50.39,40.26,49.57,40.18,49.4,39.4,49.22,39.05,48.86,38.82,48.88,38.32,48.63,38.27,48.01,38.79,48.36,39.29,48.06,39.58,47.69,39.51,46.51,38.77,46.48,39.46,46.03,39.63,45.61,39.9,45.89,40.22,45.36,40.56,45.56,40.81,45.18,40.99,44.97,41.25,45.22,41.41,45.96,41.12,46.5,41.06,46.64,41.18,46.15,41.72,46.4,41.86]],[[46.14,38.74,45.46,38.87,44.95,39.34,44.79,39.71,45,39.74,45.3,39.47,45.74,39.47,45.74,39.32,46.14,38.74]]]},{"n":"Georgia","c":"GE","r":5,"ml":4,"lx":43.74,"ly":41.87,"b":[39.96,41.06,46.64,43.55],"p":[[[39.96,43.43,40.08,43.55,40.92,43.38,42.39,43.22,43.76,42.74,43.93,42.55,44.54,42.71,45.47,42.5,45.78,42.09,46.4,41.86,46.15,41.72,46.64,41.18,46.5,41.06,45.96,41.12,45.22,41.41,44.97,41.25,43.58,41.09,42.62,41.58,41.55,41.54,41.7,41.96,41.45,42.65,40.88,43.01,40.32,43.13,39.96,43.43]]]},{"n":"Philippines","c":"PH","r":2,"ml":2.5,"lx":122.47,"ly":11.2,"b":[117.17,5.58,126.54,18.51],"p":[[[120.83,12.7,120.32,13.47,121.18,13.43,121.53,13.07,121.26,12.21,120.83,12.7]],[[122.59,9.98,122.84,10.26,122.95,10.88,123.5,10.94,123.34,10.27,124.08,11.23,123.98,10.28,123.62,9.95,123.31,9.32,123,9.02,122.38,9.71,122.59,9.98]],[[126.38,8.41,126.48,7.75,126.54,7.19,126.2,6.27,125.83,7.29,125.36,6.79,125.68,6.05,125.4,5.58,124.22,6.16,123.94,6.89,124.24,7.36,123.61,7.83,123.3,7.42,122.83,7.46,122.09,6.9,121.92,7.19,122.31,8.03,122.94,8.32,123.49,8.69,123.84,8.24,124.6,8.51,124.76,8.96,125.47,8.99,125.41,9.76,126.22,9.29,126.31,8.78,126.38,8.41]],[[118.5,9.32,117.17,8.37,117.66,9.07,118.39,9.68,118.99,10.38,119.51,11.37,119.69,10.55,119.03,10,118.5,9.32]],[[122.34,18.22,122.17,17.81,122.52,17.09,122.25,16.26,121.66,15.93,121.51,15.12,121.73,14.33,122.26,14.22,122.7,14.34,123.95,13.78,123.86,13.24,124.18,13,124.08,12.54,123.3,13.03,122.93,13.55,122.67,13.19,122.03,13.78,121.13,13.64,120.63,13.86,120.68,14.27,120.99,14.53,120.69,14.76,120.56,14.4,120.07,14.97,119.92,15.41,119.88,16.36,120.29,16.03,120.39,17.6,120.72,18.51,121.32,18.5,121.94,18.22,122.25,18.48,122.34,18.22]],[[122.04,11.42,121.88,11.89,122.48,11.58,123.12,11.58,123.1,11.17,122.64,10.74,122,10.44,121.97,10.91,122.04,11.42]],[[125.5,12.16,125.78,11.05,125.01,11.31,125.03,10.98,125.28,10.36,124.8,10.13,124.76,10.84,124.46,10.89,124.3,11.5,124.89,11.42,124.88,11.79,124.27,12.56,125.23,12.54,125.5,12.16]]]},{"n":"Malaysia","c":"MY","r":3,"ml":3,"lx":113.84,"ly":2.53,"b":[100.09,0.77,119.18,6.93],"p":[[[100.09,6.46,100.26,6.64,101.08,6.2,101.15,5.69,101.81,5.81,102.14,6.22,102.37,6.13,102.96,5.52,103.38,4.86,103.44,4.18,103.33,3.73,103.43,3.38,103.5,2.79,103.85,2.52,104.25,1.63,104.23,1.29,103.52,1.23,102.57,1.97,101.39,2.76,101.27,3.27,100.7,3.94,100.56,4.77,100.2,5.31,100.31,6.04,100.09,6.46]],[[117.88,4.14,117.02,4.31,115.87,4.31,115.52,3.17,115.13,2.82,114.62,1.43,113.81,1.22,112.86,1.5,112.38,1.41,111.8,0.9,111.16,0.98,110.51,0.77,109.83,1.34,109.66,2.01,110.4,1.66,111.17,1.85,111.37,2.7,111.8,2.89,113,3.1,113.71,3.89,114.2,4.53,114.66,4.01,114.87,4.35,115.35,4.32,115.41,4.96,115.45,5.45,116.22,6.14,116.73,6.92,117.13,6.93,117.64,6.42,117.69,5.99,118.35,5.71,119.18,5.41,119.11,5.02,118.44,4.97,118.62,4.48,117.88,4.14]]]},{"n":"Brunei","c":"BN","r":6,"ml":4,"lx":114.55,"ly":4.45,"b":[114.2,4.01,115.45,5.45],"p":[[[115.45,5.45,115.41,4.96,115.35,4.32,114.87,4.35,114.66,4.01,114.2,4.53,114.6,4.9,115.45,5.45]]]},{"n":"Slovenia","c":"SI","r":6,"ml":5,"lx":14.92,"ly":46.06,"b":[13.7,45.45,16.56,46.85],"p":[[[13.81,46.51,14.63,46.43,15.14,46.66,16.01,46.68,16.2,46.85,16.37,46.84,16.56,46.5,15.77,46.24,15.67,45.83,15.32,45.73,15.33,45.45,14.94,45.47,14.6,45.63,14.41,45.47,13.72,45.5,13.94,45.59,13.7,46.02,13.81,46.51]]]},{"n":"Finland","c":"FI","r":3,"ml":3,"lx":27.28,"ly":63.25,"b":[20.65,59.85,31.52,70.16],"p":[[[28.59,69.06,28.45,68.36,29.98,67.7,29.05,66.94,30.22,65.81,29.54,64.95,30.44,64.2,30.04,63.55,31.52,62.87,31.14,62.36,30.21,61.78,28.07,60.5,26.26,60.42,24.5,60.06,22.87,59.85,22.29,60.39,21.32,60.72,21.54,61.71,21.06,62.61,21.54,63.19,22.44,63.82,24.73,64.9,25.4,65.11,25.29,65.53,23.9,66.01,23.57,66.4,23.54,67.94,21.98,68.62,20.65,69.11,21.24,69.37,22.36,68.84,23.66,68.89,24.74,68.65,25.69,69.09,26.18,69.83,27.73,70.16,29.02,69.77,28.59,69.06]]]},{"n":"Slovakia","c":"SK","r":6,"ml":4,"lx":19.05,"ly":48.73,"b":[16.88,47.76,22.56,49.57],"p":[[[22.56,49.09,22.28,48.83,22.09,48.42,21.87,48.32,20.8,48.62,20.47,48.56,20.24,48.33,19.77,48.2,19.66,48.27,19.17,48.11,18.78,48.08,18.7,47.88,17.86,47.76,17.49,47.87,16.98,48.12,16.88,48.47,16.96,48.6,17.1,48.82,17.55,48.8,17.89,48.9,17.91,49,18.1,49.04,18.17,49.27,18.4,49.32,18.55,49.5,18.85,49.5,18.91,49.44,19.32,49.57,19.83,49.22,20.42,49.43,20.89,49.33,21.61,49.47,22.56,49.09]]]},{"n":"Czech Republic","c":"CZ","r":5,"ml":4,"lx":15.38,"ly":49.88,"b":[12.24,48.56,18.85,51.12],"p":[[[15.02,51.11,15.49,50.78,16.24,50.7,16.18,50.42,16.72,50.22,16.87,50.47,17.55,50.36,17.65,50.05,18.39,49.99,18.85,49.5,18.55,49.5,18.4,49.32,18.17,49.27,18.1,49.04,17.91,49,17.89,48.9,17.55,48.8,17.1,48.82,16.96,48.6,16.5,48.79,16.03,48.73,15.25,49.04,14.9,48.96,14.34,48.56,13.6,48.88,13.03,49.31,12.52,49.55,12.42,49.97,12.24,50.27,12.97,50.48,13.34,50.73,14.06,50.93,14.31,51.12,14.57,51,15.02,51.11]]]},{"n":"Eritrea","c":"ER","r":4,"ml":4,"lx":38.29,"ly":15.79,"b":[36.32,12.46,43.08,18],"p":[[[36.43,14.42,36.32,14.82,36.75,16.29,36.85,16.96,37.17,17.26,37.9,17.43,38.41,18,38.99,16.84,39.27,15.92,39.81,15.44,41.18,14.49,41.73,13.92,42.28,13.34,42.59,13,43.08,12.7,42.78,12.46,42.35,12.54,42.01,12.87,41.6,13.45,41.16,13.77,40.9,14.12,40.03,14.52,39.34,14.53,39.1,14.74,38.51,14.51,37.91,14.96,37.59,14.21,36.43,14.42]]]},{"n":"Japan","c":"JP","r":2,"ml":1.7,"lx":138.44,"ly":36.14,"b":[129.41,31.03,145.54,45.55],"p":[[[141.88,39.18,140.96,38.17,140.98,37.14,140.6,36.34,140.77,35.84,140.25,35.14,138.98,34.67,137.22,34.61,135.79,33.46,135.12,33.85,135.08,34.6,133.34,34.38,132.16,33.9,130.99,33.89,132,33.15,131.33,31.45,130.69,31.03,130.2,31.42,130.45,32.32,129.81,32.61,129.41,33.3,130.35,33.6,130.88,34.23,131.88,34.75,132.62,35.43,134.61,35.73,135.68,35.53,136.72,37.3,137.39,36.83,138.86,37.83,139.43,38.22,140.05,39.44,139.88,40.56,140.31,41.2,141.37,41.38,141.91,39.99,141.88,39.18]],[[144.61,43.96,145.32,44.38,145.54,43.26,144.06,42.99,143.18,42,141.61,42.68,141.07,41.58,139.96,41.57,139.82,42.56,140.31,43.33,141.38,43.39,141.67,44.77,141.97,45.55,143.14,44.51,143.91,44.17,144.61,43.96]],[[132.37,33.46,132.92,34.06,133.49,33.94,133.9,34.36,134.64,34.15,134.77,33.81,134.2,33.2,133.79,33.52,133.28,33.29,133.01,32.7,132.36,32.99,132.37,33.46]]]},{"n":"Paraguay","c":"PY","r":4,"ml":3,"lx":-60.15,"ly":-21.67,"b":[-62.69,-27.55,-54.29,-19.34],"p":[[[-58.17,-20.18,-57.87,-20.73,-57.94,-22.09,-56.88,-22.28,-56.47,-22.09,-55.8,-22.36,-55.61,-22.66,-55.52,-23.57,-55.4,-23.96,-55.03,-24,-54.65,-23.84,-54.29,-24.02,-54.29,-24.57,-54.43,-25.16,-54.63,-25.74,-54.79,-26.62,-55.7,-27.39,-56.49,-27.55,-57.61,-27.4,-58.62,-27.12,-57.63,-25.6,-57.78,-25.16,-58.81,-24.77,-60.03,-24.03,-60.85,-23.88,-62.69,-22.25,-62.29,-21.05,-62.27,-20.51,-61.79,-19.63,-60.04,-19.34,-59.12,-19.36,-58.18,-19.87,-58.17,-20.18]]]},{"n":"Yemen","c":"YE","r":3,"ml":3,"lx":45.87,"ly":15.33,"b":[42.6,12.59,53.11,19],"p":[[[52,19,52.78,17.35,53.11,16.65,52.39,16.38,52.19,15.94,52.17,15.6,51.17,15.18,49.57,14.71,48.68,14,48.24,13.95,47.94,14.01,47.35,13.59,46.72,13.4,45.88,13.35,45.63,13.29,45.41,13.03,45.14,12.95,44.99,12.7,44.49,12.72,44.18,12.59,43.48,12.64,43.22,13.22,43.25,13.77,43.09,14.06,42.89,14.8,42.6,15.21,42.81,15.26,42.7,15.72,42.82,15.91,42.78,16.35,43.22,16.67,43.12,17.09,43.38,17.58,43.79,17.32,44.06,17.41,45.22,17.43,45.4,17.33,46.37,17.23,46.75,17.28,47,16.95,47.47,17.12,48.18,18.17,49.12,18.62,52,19]]]},{"n":"Saudi Arabia","c":"SA","r":2,"ml":1.7,"lx":44.7,"ly":23.81,"b":[34.63,16.35,55.67,32.16],"p":[[[34.96,29.36,36.07,29.2,36.5,29.51,36.74,29.87,37.5,30,37.67,30.34,38,30.51,37,31.51,39,32.01,39.2,32.16,40.4,31.89,41.89,31.19,44.71,29.18,46.57,29.1,47.46,29,47.71,28.53,48.42,28.55,48.81,27.69,49.3,27.46,49.47,27.11,50.15,26.69,50.21,26.28,50.11,25.94,50.24,25.61,50.53,25.33,50.66,25,50.81,24.75,51.11,24.56,51.39,24.63,51.58,24.25,51.62,24.01,52,23,55.01,22.5,55.21,22.71,55.67,22,55,20,52,19,49.12,18.62,48.18,18.17,47.47,17.12,47,16.95,46.75,17.28,46.37,17.23,45.4,17.33,45.22,17.43,44.06,17.41,43.79,17.32,43.38,17.58,43.12,17.09,43.22,16.67,42.78,16.35,42.65,16.77,42.35,17.08,42.27,17.47,41.75,17.83,41.22,18.67,40.94,19.49,40.25,20.17,39.8,20.34,39.14,21.29,39.02,21.99,39.07,22.58,38.49,23.69,38.02,24.08,37.48,24.29,37.15,24.86,37.21,25.08,36.93,25.6,36.64,25.83,36.25,26.57,35.64,27.38,35.13,28.06,34.63,28.06,34.79,28.61,34.83,28.96,34.96,29.36]]]},{"n":"Antarctica","c":"AQ","r":4,"ml":4,"lx":35.89,"ly":-79.84,"b":[-180,-90,180,-63.27],"p":[[[-48.66,-78.05,-48.15,-78.05,-46.66,-77.83,-45.15,-78.05,-43.92,-78.48,-43.49,-79.09,-43.37,-79.52,-43.33,-80.03,-44.88,-80.34,-46.51,-80.59,-48.39,-80.83,-50.48,-81.03,-52.85,-80.97,-54.16,-80.63,-53.99,-80.22,-51.85,-79.95,-50.99,-79.61,-50.36,-79.18,-49.91,-78.81,-49.31,-78.46,-48.66,-78.05]],[[-66.29,-80.26,-64.04,-80.29,-61.88,-80.39,-61.14,-79.98,-60.61,-79.63,-59.57,-80.04,-59.87,-80.55,-60.16,-81,-62.26,-80.86,-64.49,-80.92,-65.74,-80.59,-65.74,-80.55,-66.29,-80.26]],[[-73.92,-71.27,-73.23,-71.15,-72.07,-71.19,-71.78,-70.68,-71.72,-70.31,-71.74,-69.51,-71.17,-69.04,-70.25,-68.88,-69.72,-69.25,-69.49,-69.62,-69.06,-70.07,-68.73,-70.51,-68.45,-70.96,-68.33,-71.41,-68.51,-71.8,-68.78,-72.17,-69.96,-72.31,-71.08,-72.5,-72.39,-72.48,-71.9,-72.09,-73.07,-72.23,-74.19,-72.37,-74.95,-72.07,-75.01,-71.66,-73.92,-71.27]],[[-102.33,-71.89,-101.7,-71.72,-100.43,-71.85,-98.98,-71.93,-97.88,-72.07,-96.79,-71.95,-96.2,-72.52,-96.98,-72.44,-98.2,-72.48,-99.43,-72.44,-100.78,-72.5,-101.8,-72.31,-102.33,-71.89]],[[-122.62,-73.66,-122.41,-73.32,-121.21,-73.5,-119.92,-73.66,-118.72,-73.48,-119.29,-73.83,-120.23,-74.09,-121.62,-74.01,-122.62,-73.66]],[[-127.28,-73.46,-126.56,-73.25,-125.56,-73.48,-124.03,-73.87,-124.62,-73.83,-125.91,-73.74,-127.28,-73.46]],[[-163.71,-78.6,-163.11,-78.22,-161.25,-78.38,-160.25,-78.69,-159.48,-79.05,-159.21,-79.5,-161.13,-79.63,-162.44,-79.28,-163.03,-78.93,-163.07,-78.87,-163.71,-78.6]],[[180,-84.71,180,-90,-180,-90,-180,-84.71,-179.94,-84.72,-179.06,-84.14,-177.26,-84.45,-177.14,-84.42,-176.08,-84.1,-175.95,-84.11,-175.83,-84.12,-174.38,-84.53,-173.12,-84.12,-172.89,-84.06,-169.95,-83.88,-169,-84.12,-168.53,-84.24,-167.02,-84.57,-164.18,-84.83,-161.93,-85.14,-158.07,-85.37,-155.19,-85.1,-150.94,-85.3,-148.53,-85.61,-145.89,-85.32,-143.11,-85.04,-142.89,-84.57,-146.83,-84.53,-150.06,-84.3,-150.9,-83.9,-153.59,-83.69,-153.41,-83.24,-153.04,-82.83,-152.67,-82.45,-152.86,-82.04,-154.53,-81.77,-155.29,-81.42,-156.84,-81.1,-154.41,-81.16,-152.1,-81,-150.65,-81.34,-148.87,-81.04,-147.22,-80.67,-146.42,-80.34,-146.77,-79.93,-148.06,-79.65,-149.53,-79.36,-151.59,-79.3,-153.39,-79.16,-155.33,-79.06,-155.98,-78.69,-157.27,-78.38,-158.05,-78.03,-158.37,-76.89,-157.88,-76.99,-156.97,-77.3,-155.33,-77.2,-153.74,-77.07,-152.92,-77.5,-151.33,-77.4,-150,-77.18,-148.75,-76.91,-147.61,-76.58,-146.1,-76.48,-146.14,-76.11,-146.5,-75.73,-146.2,-75.38,-144.91,-75.2,-144.32,-75.54,-142.79,-75.34,-141.64,-75.09,-140.21,-75.07,-138.86,-74.97,-137.51,-74.73,-136.43,-74.52,-135.21,-74.3,-134.43,-74.36,-133.75,-74.44,-132.26,-74.3,-130.93,-74.48,-129.55,-74.46,-128.24,-74.32,-126.89,-74.42,-125.4,-74.52,-124.01,-74.48,-122.56,-74.5,-121.07,-74.52,-119.7,-74.48,-118.68,-74.19,-117.47,-74.03,-116.22,-74.24,-115.02,-74.07,-113.94,-73.71,-113.3,-74.03,-112.95,-74.38,-112.3,-74.71,-111.26,-74.42,-110.07,-74.79,-108.71,-74.91,-107.56,-75.18,-106.15,-75.13,-104.88,-74.95,-103.37,-74.99,-102.02,-75.13,-100.65,-75.3,-100.12,-74.87,-100.76,-74.54,-101.25,-74.19,-102.55,-74.11,-103.11,-73.73,-103.33,-73.36,-103.68,-72.62,-102.92,-72.75,-101.61,-72.81,-100.31,-72.75,-99.14,-72.91,-98.12,-73.21,-97.69,-73.56,-96.34,-73.62,-95.04,-73.48,-93.67,-73.28,-92.44,-73.17,-91.42,-73.4,-90.09,-73.32,-89.23,-72.56,-88.42,-73.01,-87.27,-73.19,-86.01,-73.09,-85.19,-73.48,-83.88,-73.52,-82.67,-73.64,-81.47,-73.85,-80.69,-73.48,-80.3,-73.13,-79.3,-73.52,-77.93,-73.42,-76.91,-73.64,-76.22,-73.97,-74.89,-73.87,-73.85,-73.66,-72.83,-73.4,-71.62,-73.26,-70.21,-73.15,-68.94,-73.01,-67.96,-72.79,-67.37,-72.48,-67.13,-72.05,-67.25,-71.64,-67.56,-71.25,-67.92,-70.85,-68.23,-70.46,-68.49,-70.11,-68.54,-69.72,-68.45,-69.33,-67.98,-68.95,-67.58,-68.54,-67.43,-68.15,-67.62,-67.72,-67.74,-67.33,-67.25,-66.88,-66.7,-66.58,-66.06,-66.21,-65.37,-65.9,-64.57,-65.6,-64.18,-65.17,-63.63,-64.9,-63,-64.64,-62.04,-64.58,-61.41,-64.27,-60.71,-64.07,-59.89,-63.96,-59.16,-63.7,-58.59,-63.39,-57.81,-63.27,-57.22,-63.53,-57.6,-63.86,-58.61,-64.15,-59.05,-64.37,-59.79,-64.21,-60.61,-64.31,-61.3,-64.54,-62.02,-64.8,-62.51,-65.09,-62.65,-65.48,-62.59,-65.86,-62.12,-66.19,-62.81,-66.43,-63.75,-66.5,-64.29,-66.84,-64.88,-67.15,-65.51,-67.58,-65.67,-67.95,-65.31,-68.37,-64.78,-68.68,-63.96,-68.91,-63.2,-69.23,-62.79,-69.62,-62.57,-69.99,-62.28,-70.38,-61.81,-70.72,-61.51,-71.09,-61.38,-72.01,-61.08,-72.38,-61,-72.77,-60.69,-73.17,-60.83,-73.7,-61.38,-74.11,-61.96,-74.44,-63.3,-74.58,-63.75,-74.93,-64.35,-75.26,-65.86,-75.64,-67.19,-75.79,-68.45,-76.01,-69.8,-76.22,-70.6,-76.63,-72.21,-76.67,-73.97,-76.63,-75.56,-76.71,-77.24,-76.71,-76.93,-77.1,-75.4,-77.28,-74.28,-77.56,-73.66,-77.91,-74.77,-78.22,-76.5,-78.12,-77.93,-78.38,-77.98,-78.79,-78.02,-79.18,-76.85,-79.51,-76.63,-79.89,-75.36,-80.26,-73.24,-80.42,-71.44,-80.69,-70.01,-81,-68.19,-81.32,-65.7,-81.47,-63.26,-81.75,-61.55,-82.04,-59.69,-82.38,-58.71,-82.85,-58.22,-83.22,-57.01,-82.87,-55.36,-82.57,-53.62,-82.26,-51.54,-82,-49.76,-81.73,-47.27,-81.71,-44.83,-81.85,-42.81,-82.08,-42.16,-81.65,-40.77,-81.36,-38.24,-81.34,-36.27,-81.12,-34.39,-80.91,-32.31,-80.77,-30.1,-80.59,-28.55,-80.34,-29.25,-79.99,-29.69,-79.63,-29.69,-79.26,-31.62,-79.3,-33.68,-79.46,-35.64,-79.46,-35.91,-79.08,-35.78,-78.34,-35.33,-78.12,-33.9,-77.89,-32.21,-77.65,-31,-77.36,-29.78,-77.07,-28.88,-76.67,-27.51,-76.5,-26.16,-76.36,-25.47,-76.28,-23.93,-76.24,-22.46,-76.11,-21.22,-75.91,-20.01,-75.67,-18.91,-75.44,-17.52,-75.13,-16.64,-74.79,-15.7,-74.5,-15.41,-74.11,-16.47,-73.87,-16.11,-73.46,-15.45,-73.15,-14.41,-72.95,-13.31,-72.72,-12.29,-72.4,-11.51,-72.01,-11.02,-71.54,-10.3,-71.27,-9.1,-71.32,-8.61,-71.66,-7.42,-71.7,-7.38,-71.32,-6.87,-70.93,-5.79,-71.03,-5.54,-71.4,-4.34,-71.46,-3.05,-71.29,-1.8,-71.17,-0.66,-71.23,-0.23,-71.64,0.87,-71.3,1.89,-71.13,3.02,-70.99,4.14,-70.85,5.16,-70.62,6.27,-70.46,7.14,-70.25,7.74,-69.89,8.49,-70.15,9.53,-70.01,10.25,-70.48,10.82,-70.83,11.95,-70.64,12.4,-70.25,13.42,-69.97,14.73,-70.03,15.13,-70.4,15.95,-70.03,17.03,-69.91,18.2,-69.87,19.26,-69.89,20.38,-70.01,21.45,-70.07,21.92,-70.4,22.57,-70.7,23.67,-70.52,24.84,-70.48,25.98,-70.48,27.09,-70.46,28.09,-70.32,29.15,-70.21,30.03,-69.93,30.97,-69.76,31.99,-69.66,32.75,-69.38,33.3,-68.84,33.87,-68.5,34.91,-68.66,35.3,-69.01,36.16,-69.25,37.2,-69.17,37.91,-69.52,38.65,-69.78,39.67,-69.54,40.02,-69.11,40.92,-68.93,41.96,-68.6,42.94,-68.46,44.11,-68.27,44.9,-68.05,45.72,-67.82,46.5,-67.6,47.44,-67.72,48.34,-67.37,48.99,-67.09,49.93,-67.11,50.75,-66.88,50.95,-66.52,51.79,-66.25,52.61,-66.05,53.61,-65.9,54.53,-65.82,55.41,-65.88,56.36,-65.97,57.16,-66.25,57.26,-66.68,58.14,-67.01,58.74,-67.29,59.94,-67.41,60.61,-67.68,61.43,-67.95,62.39,-68.01,63.19,-67.82,64.05,-67.41,64.99,-67.62,65.97,-67.74,66.91,-67.86,67.89,-67.93,68.89,-67.93,69.71,-68.97,69.67,-69.23,69.56,-69.68,68.6,-69.93,67.81,-70.31,67.95,-70.7,69.07,-70.68,68.93,-71.07,68.42,-71.44,67.95,-71.85,68.71,-72.17,69.87,-72.26,71.02,-72.09,71.57,-71.7,71.91,-71.32,72.45,-71.01,73.08,-70.72,73.34,-70.36,73.86,-69.87,74.49,-69.78,75.63,-69.74,76.63,-69.62,77.64,-69.46,78.13,-69.07,78.43,-68.7,79.11,-68.33,80.09,-68.07,80.94,-67.88,81.48,-67.54,82.05,-67.37,82.78,-67.21,83.78,-67.31,84.68,-67.21,85.66,-67.09,86.75,-67.15,87.48,-66.88,87.99,-66.21,88.36,-66.48,88.83,-66.95,89.67,-67.15,90.63,-67.23,91.59,-67.11,92.61,-67.19,93.55,-67.21,94.18,-67.11,95.02,-67.17,95.78,-67.39,96.68,-67.25,97.76,-67.25,98.68,-67.11,99.72,-67.25,100.38,-66.92,100.89,-66.58,101.58,-66.31,102.83,-65.56,103.48,-65.7,104.24,-65.97,104.91,-66.33,106.18,-66.93,107.16,-66.95,108.08,-66.95,109.16,-66.84,110.24,-66.7,111.06,-66.43,111.74,-66.13,112.86,-66.09,113.6,-65.88,114.39,-66.07,114.9,-66.39,115.6,-66.7,116.7,-66.66,117.38,-66.92,118.58,-67.17,119.83,-67.27,120.87,-67.19,121.65,-66.88,122.32,-66.56,123.22,-66.48,124.12,-66.62,125.16,-66.72,126.1,-66.56,127,-66.56,127.88,-66.66,128.8,-66.76,129.7,-66.58,130.78,-66.43,131.8,-66.39,132.94,-66.39,133.86,-66.29,134.76,-66.21,135.03,-65.72,135.07,-65.31,135.7,-65.58,135.87,-66.03,136.21,-66.45,136.62,-66.78,137.46,-66.95,138.6,-66.9,139.91,-66.88,140.81,-66.82,142.12,-66.82,143.06,-66.8,144.37,-66.84,145.49,-66.92,146.2,-67.23,146,-67.6,146.65,-67.9,147.72,-68.13,148.84,-68.39,150.13,-68.56,151.48,-68.72,152.5,-68.87,153.64,-68.89,154.28,-68.56,155.17,-68.84,155.93,-69.15,156.81,-69.38,158.03,-69.48,159.18,-69.6,159.67,-69.99,160.81,-70.23,161.57,-70.58,162.69,-70.74,163.84,-70.72,164.92,-70.78,166.11,-70.76,167.31,-70.83,168.43,-70.97,169.46,-71.21,170.5,-71.4,171.21,-71.7,171.09,-72.09,170.56,-72.44,170.11,-72.89,169.76,-73.24,169.29,-73.66,167.98,-73.81,167.39,-74.17,166.09,-74.38,165.64,-74.77,164.96,-75.15,164.23,-75.46,163.82,-75.87,163.57,-76.24,163.47,-76.69,163.49,-77.07,164.06,-77.46,164.27,-77.83,164.74,-78.18,166.6,-78.32,167,-78.75,165.19,-78.91,163.67,-79.12,161.77,-79.16,160.92,-79.73,160.75,-80.2,160.32,-80.57,159.79,-80.95,161.12,-81.28,161.63,-81.69,162.49,-82.06,163.71,-82.4,165.1,-82.71,166.6,-83.02,168.9,-83.34,169.4,-83.83,172.28,-84.04,172.48,-84.12,173.22,-84.41,175.99,-84.16,178.28,-84.47,180,-84.71]]]},{"n":"Turkish Republic of Northern Cyprus","c":"","r":6,"ml":6,"lx":33.69,"ly":35.22,"b":[32.73,35,34.58,35.67],"p":[[[32.73,35.14,32.8,35.15,32.95,35.39,33.67,35.37,34.58,35.67,33.9,35.25,33.97,35.06,33.87,35.09,33.68,35.02,33.53,35.04,33.48,35,33.46,35.1,33.38,35.16,33.19,35.17,32.92,35.09,32.73,35.14]]]},{"n":"Cyprus","c":"CY","r":5,"ml":4.5,"lx":33.08,"ly":34.91,"b":[32.26,34.57,34,35.17],"p":[[[32.73,35.14,32.92,35.09,33.19,35.17,33.38,35.16,33.46,35.1,33.48,35,33.53,35.04,33.68,35.02,33.87,35.09,33.97,35.06,34,34.98,32.98,34.57,32.49,34.7,32.26,35.1,32.73,35.14]]]},{"n":"Morocco","c":"MA","r":3,"ml":2.7,"lx":-7.19,"ly":31.65,"b":[-17.02,21.42,-1.12,35.76],"p":[[[-2.17,35.17,-1.79,34.53,-1.73,33.92,-1.39,32.86,-1.12,32.65,-1.31,32.26,-2.62,32.09,-3.07,31.72,-3.65,31.64,-3.69,30.9,-4.86,30.5,-5.24,30,-6.06,29.73,-7.06,29.58,-8.67,28.84,-8.67,27.66,-8.82,27.66,-8.79,27.12,-9.41,27.09,-9.74,26.86,-10.19,26.86,-10.55,26.99,-11.39,26.88,-11.72,26.1,-12.03,26.03,-12.5,24.77,-13.89,23.69,-14.22,22.31,-14.63,21.86,-14.75,21.5,-17,21.42,-17.02,21.42,-16.97,21.89,-16.59,22.16,-16.26,22.68,-16.33,23.02,-15.98,23.72,-15.43,24.36,-15.09,24.52,-14.82,25.1,-14.8,25.64,-14.44,26.25,-13.77,26.62,-13.14,27.64,-13.12,27.65,-12.62,28.04,-11.69,28.15,-10.9,28.83,-10.4,29.1,-9.56,29.93,-9.81,31.18,-9.43,32.04,-9.3,32.56,-8.66,33.24,-7.65,33.7,-6.91,34.11,-6.24,35.15,-5.93,35.76,-5.19,35.76,-4.59,35.33,-3.64,35.4,-2.6,35.18,-2.17,35.17]]]},{"n":"Egypt","c":"EG","r":2,"ml":1.7,"lx":29.45,"ly":26.19,"b":[24.7,22,36.87,31.59],"p":[[[36.87,22,32.9,22,29.02,22,25,22,25,25.68,25,29.24,24.7,30.04,24.96,30.66,24.8,31.09,25.16,31.57,26.5,31.59,27.46,31.32,28.45,31.03,28.91,30.87,29.68,31.19,30.1,31.47,30.98,31.56,31.69,31.43,31.96,30.93,32.19,31.26,32.99,31.02,33.77,30.97,34.27,31.22,34.82,29.76,34.92,29.5,34.64,29.1,34.43,28.34,34.15,27.82,33.92,27.65,33.59,27.97,33.14,28.42,32.42,29.85,32.32,29.76,32.73,28.71,33.35,27.7,34.1,26.14,34.47,25.6,34.8,25.03,35.69,23.93,35.49,23.75,35.53,23.1,36.69,22.2,36.87,22]]]},{"n":"Libya","c":"LY","r":3,"ml":3,"lx":18.01,"ly":26.64,"b":[9.32,19.58,25.16,33.14],"p":[[[25,22,25,20,23.85,20,23.84,19.58,19.85,21.5,15.86,23.41,14.85,22.86,14.14,22.49,13.58,23.04,12,23.47,11.56,24.1,10.77,24.56,10.3,24.38,9.95,24.94,9.91,25.37,9.32,26.09,9.72,26.51,9.63,27.14,9.76,27.69,9.68,28.14,9.86,28.96,9.81,29.42,9.48,30.31,9.97,30.54,10.06,30.96,9.95,31.38,10.64,31.76,10.94,32.08,11.43,32.37,11.49,33.14,12.66,32.79,13.08,32.88,13.92,32.71,15.25,32.27,15.71,31.38,16.61,31.18,18.02,30.76,19.09,30.27,19.57,30.53,20.05,30.99,19.82,31.75,20.13,32.24,20.85,32.71,21.54,32.84,22.9,32.64,23.24,32.19,23.61,32.19,23.93,32.02,24.92,31.9,25.16,31.57,24.8,31.09,24.96,30.66,24.7,30.04,25,29.24,25,25.68,25,22]]]},{"n":"Ethiopia","c":"ET","r":2,"ml":2,"lx":39.09,"ly":8.03,"b":[32.95,3.42,47.79,14.96],"p":[[[47.79,8,44.96,5,43.66,4.96,42.77,4.25,42.13,4.23,41.86,3.92,41.17,3.92,40.77,4.26,39.85,3.84,39.56,3.42,38.89,3.5,38.67,3.62,38.44,3.59,38.12,3.6,36.86,4.45,36.16,4.45,35.82,4.78,35.82,5.34,35.3,5.51,34.71,6.59,34.25,6.83,34.08,7.23,33.57,7.71,32.95,7.78,33.29,8.35,33.83,8.38,33.97,8.68,33.96,9.58,34.26,10.63,34.73,10.91,34.83,11.32,35.26,12.08,35.86,12.58,36.27,13.56,36.43,14.42,37.59,14.21,37.91,14.96,38.51,14.51,39.1,14.74,39.34,14.53,40.03,14.52,40.9,14.12,41.16,13.77,41.6,13.45,42.01,12.87,42.35,12.54,42,12.1,41.66,11.63,41.74,11.36,41.76,11.05,42.31,11.03,42.55,11.11,42.78,10.93,42.56,10.57,42.93,10.02,43.3,9.54,43.68,9.18,46.95,8,47.79,8]]]},{"n":"Djibouti","c":"DJ","r":5,"ml":4,"lx":42.5,"ly":11.98,"b":[41.66,10.93,43.32,12.7],"p":[[[42.35,12.54,42.78,12.46,43.08,12.7,43.32,12.39,43.29,11.97,42.72,11.74,43.15,11.46,42.78,10.93,42.55,11.11,42.31,11.03,41.76,11.05,41.74,11.36,41.66,11.63,42,12.1,42.35,12.54]]]},{"n":"Somaliland","c":"","r":5,"ml":4.5,"lx":46.73,"ly":9.44,"b":[42.56,8,48.95,11.46],"p":[[[48.95,11.41,48.94,11.39,48.94,10.98,48.94,9.97,48.94,9.45,48.49,8.84,47.79,8,46.95,8,43.68,9.18,43.3,9.54,42.93,10.02,42.56,10.57,42.78,10.93,43.15,11.46,43.47,11.28,43.67,10.86,44.12,10.45,44.61,10.44,45.56,10.7,46.65,10.82,47.53,11.13,48.02,11.19,48.38,11.38,48.95,11.41]]]},{"n":"Uganda","c":"UG","r":3,"ml":3,"lx":32.95,"ly":1.97,"b":[29.58,-1.44,35.04,4.25],"p":[[[33.9,-0.95,31.87,-1.03,30.77,-1.01,30.42,-1.13,29.82,-1.44,29.58,-1.34,29.59,-0.59,29.82,-0.21,29.88,0.6,30.09,1.06,30.47,1.58,30.85,1.85,31.17,2.2,30.77,2.34,30.83,3.51,31.25,3.78,31.88,3.56,32.69,3.79,33.39,3.79,34.01,4.25,34.48,3.56,34.6,3.05,35.04,1.91,34.67,1.18,34.18,0.52,33.89,0.11,33.9,-0.95]]]},{"n":"Rwanda","c":"RW","r":3,"ml":3,"lx":30.1,"ly":-1.9,"b":[29.02,-2.92,30.82,-1.13],"p":[[[30.42,-1.13,30.82,-1.7,30.76,-2.29,30.47,-2.41,29.94,-2.35,29.63,-2.92,29.02,-2.84,29.12,-2.29,29.25,-2.22,29.29,-1.62,29.58,-1.34,29.82,-1.44,30.42,-1.13]]]},{"n":"Bosnia and Herzegovina","c":"BA","r":5,"ml":4.5,"lx":18.07,"ly":44.09,"b":[15.75,42.65,19.6,45.23],"p":[[[18.56,42.65,17.67,43.03,17.3,43.45,16.92,43.67,16.46,44.04,16.24,44.35,15.75,44.82,15.96,45.23,16.32,45,16.53,45.21,17,45.23,17.86,45.07,18.55,45.08,19.01,44.86,19.37,44.86,19.12,44.42,19.6,44.04,19.45,43.57,19.22,43.52,19.03,43.43,18.71,43.2,18.56,42.65]]]},{"n":"North Macedonia","c":"MK","r":6,"ml":5,"lx":21.56,"ly":41.56,"b":[20.46,40.84,22.95,42.32],"p":[[[22.38,42.32,22.88,42,22.95,41.34,22.76,41.3,22.6,41.13,22.06,41.15,21.67,40.93,21.02,40.84,20.61,41.09,20.46,41.52,20.59,41.86,20.72,41.85,20.76,42.05,21.35,42.21,21.58,42.25,21.92,42.3,22.38,42.32]]]},{"n":"Serbia","c":"RS","r":5,"ml":4,"lx":20.79,"ly":44.19,"b":[18.83,42.25,22.99,46.17],"p":[[[18.83,45.91,19.6,46.17,20.22,46.13,20.76,45.73,20.87,45.42,21.48,45.18,21.56,44.77,22.15,44.48,22.46,44.7,22.71,44.58,22.47,44.41,22.66,44.23,22.41,44.01,22.5,43.64,22.99,43.21,22.6,42.9,22.44,42.58,22.55,42.46,22.38,42.32,21.92,42.3,21.58,42.25,21.54,42.32,21.66,42.44,21.78,42.68,21.63,42.68,21.44,42.86,21.27,42.91,21.14,43.07,20.96,43.13,20.81,43.27,20.64,43.22,20.5,42.88,20.26,42.81,20.34,42.9,19.96,43.11,19.63,43.21,19.48,43.35,19.22,43.52,19.45,43.57,19.6,44.04,19.12,44.42,19.37,44.86,19.01,44.86,19.39,45.24,19.07,45.52,18.83,45.91]]]},{"n":"Montenegro","c":"ME","r":6,"ml":5,"lx":19.14,"ly":42.8,"b":[18.45,41.88,20.34,43.52],"p":[[[20.07,42.59,19.8,42.5,19.74,42.69,19.3,42.2,19.37,41.88,19.16,41.96,18.88,42.28,18.45,42.48,18.56,42.65,18.71,43.2,19.03,43.43,19.22,43.52,19.48,43.35,19.63,43.21,19.96,43.11,20.34,42.9,20.26,42.81,20.07,42.59]]]},{"n":"Kosovo","c":"XK","r":6,"ml":5,"lx":20.86,"ly":42.59,"b":[20.07,41.85,21.78,43.27],"p":[[[20.59,41.86,20.52,42.22,20.28,42.32,20.07,42.59,20.26,42.81,20.5,42.88,20.64,43.22,20.81,43.27,20.96,43.13,21.14,43.07,21.27,42.91,21.44,42.86,21.63,42.68,21.78,42.68,21.66,42.44,21.54,42.32,21.58,42.25,21.35,42.21,20.76,42.05,20.72,41.85,20.59,41.86]]]},{"n":"Trinidad and Tobago","c":"TT","r":5,"ml":4.5,"lx":-60.92,"ly":11,"b":[-61.95,10,-60.89,10.89],"p":[[[-61.68,10.76,-61.1,10.89,-60.89,10.86,-60.93,10.11,-61.77,10,-61.95,10.09,-61.66,10.37,-61.68,10.76]]]},{"n":"South Sudan","c":"SS","r":3,"ml":3,"lx":30.39,"ly":7.23,"b":[23.89,3.51,35.3,12.25],"p":[[[30.83,3.51,29.95,4.17,29.72,4.6,29.16,4.39,28.7,4.46,28.43,4.29,27.98,4.41,27.37,5.23,27.21,5.55,26.47,5.95,26.21,6.55,25.8,6.98,25.12,7.5,25.11,7.83,24.57,8.23,23.89,8.62,24.19,8.73,24.54,8.92,24.79,9.81,25.07,10.27,25.79,10.41,25.96,10.14,26.48,9.55,26.75,9.47,27.11,9.64,27.83,9.6,27.97,9.4,28.97,9.4,29,9.6,29.52,9.79,29.62,10.08,30,10.29,30.84,9.71,31.35,9.81,31.85,10.53,32.4,11.08,32.31,11.68,32.07,11.97,32.67,12.02,32.74,12.25,33.21,12.18,33.09,11.44,33.21,10.72,33.72,10.33,33.84,9.98,33.82,9.48,33.96,9.46,33.97,8.68,33.83,8.38,33.29,8.35,32.95,7.78,33.57,7.71,34.08,7.23,34.25,6.83,34.71,6.59,35.3,5.51,34.62,4.85,34.01,4.25,33.39,3.79,32.69,3.79,31.88,3.56,31.25,3.78,30.83,3.51]]]}]} \ No newline at end of file diff --git a/src/maps/data/countries_50m.json b/src/maps/data/countries_50m.json new file mode 100644 index 0000000..9681f4f --- /dev/null +++ b/src/maps/data/countries_50m.json @@ -0,0 +1 @@ +{"f":[{"n":"Zimbabwe","c":"ZW","r":3,"ml":2.5,"lx":29.93,"ly":-18.91,"b":[25.224,-22.402,33.007,-15.643],"p":[[[31.288,-22.402,31.197,-22.345,31.073,-22.308,30.916,-22.291,30.712,-22.298,30.46,-22.329,30.19,-22.291,29.902,-22.184,29.663,-22.146,29.377,-22.193,29.365,-22.194,29.315,-22.158,29.237,-22.079,29.107,-22.066,29.071,-22.047,29.042,-22.018,29.023,-21.981,29.016,-21.94,29.037,-21.811,29.026,-21.797,28.991,-21.781,28.919,-21.766,28.748,-21.708,28.532,-21.651,28.182,-21.589,28.046,-21.573,28.014,-21.554,27.975,-21.507,27.907,-21.359,27.844,-21.262,27.693,-21.111,27.669,-21.064,27.677,-20.945,27.688,-20.848,27.704,-20.766,27.697,-20.69,27.695,-20.595,27.7,-20.531,27.679,-20.503,27.625,-20.484,27.469,-20.475,27.281,-20.479,27.275,-20.382,27.257,-20.232,27.221,-20.146,27.178,-20.101,27.092,-20.054,26.917,-19.99,26.678,-19.893,26.475,-19.749,26.241,-19.569,26.168,-19.538,26.082,-19.37,25.951,-19.082,25.959,-18.986,25.939,-18.939,25.812,-18.797,25.784,-18.724,25.761,-18.649,25.558,-18.442,25.489,-18.351,25.437,-18.235,25.384,-18.142,25.34,-18.104,25.282,-18.041,25.242,-17.969,25.224,-17.915,25.239,-17.843,25.259,-17.794,25.452,-17.845,25.557,-17.85,25.64,-17.824,25.742,-17.858,25.863,-17.952,25.996,-17.97,26.14,-17.912,26.333,-17.929,26.578,-18.023,26.78,-18.042,27.021,-17.958,27.236,-17.728,27.438,-17.512,27.637,-17.262,27.757,-17.06,27.932,-16.896,28.164,-16.77,28.4,-16.663,28.761,-16.532,28.833,-16.424,28.857,-16.306,28.857,-16.142,28.876,-16.036,28.913,-15.988,28.973,-15.95,29.051,-15.901,29.288,-15.776,29.487,-15.697,29.73,-15.645,29.995,-15.644,30.251,-15.643,30.396,-15.643,30.398,-15.801,30.409,-15.978,30.438,-15.995,30.63,-15.999,30.939,-16.012,31.236,-16.024,31.426,-16.152,31.49,-16.18,31.688,-16.214,31.94,-16.429,32.243,-16.449,32.452,-16.516,32.636,-16.589,32.742,-16.678,32.81,-16.698,32.903,-16.704,32.948,-16.712,32.938,-16.776,32.876,-16.884,32.884,-17.038,32.969,-17.252,32.981,-17.437,32.955,-17.765,32.956,-18.083,32.965,-18.196,32.979,-18.271,32.996,-18.313,32.993,-18.36,32.942,-18.493,32.902,-18.633,32.9,-18.689,32.885,-18.729,32.854,-18.764,32.722,-18.828,32.699,-18.868,32.7,-18.941,32.717,-19.002,32.766,-19.024,32.826,-19.059,32.85,-19.104,32.85,-19.152,32.831,-19.241,32.778,-19.389,32.831,-19.558,32.89,-19.668,32.973,-19.795,33.007,-19.874,33.005,-19.93,32.993,-19.985,32.87,-20.217,32.781,-20.362,32.673,-20.516,32.529,-20.613,32.492,-20.66,32.478,-20.713,32.483,-20.829,32.476,-20.95,32.354,-21.137,32.43,-21.297,32.412,-21.312,32.371,-21.335,32.195,-21.515,32.016,-21.698,31.886,-21.832,31.738,-21.983,31.571,-22.154,31.429,-22.299,31.288,-22.402]]]},{"n":"Zambia","c":"ZM","r":3,"ml":3,"lx":26.4,"ly":-14.66,"b":[21.979,-18.042,33.662,-8.194],"p":[[[30.396,-15.643,30.251,-15.643,29.995,-15.644,29.73,-15.645,29.487,-15.697,29.288,-15.776,29.051,-15.901,28.973,-15.95,28.913,-15.988,28.876,-16.036,28.857,-16.142,28.857,-16.306,28.833,-16.424,28.761,-16.532,28.4,-16.663,28.164,-16.77,27.932,-16.896,27.757,-17.06,27.637,-17.262,27.438,-17.512,27.236,-17.728,27.021,-17.958,26.78,-18.042,26.578,-18.023,26.333,-17.929,26.14,-17.912,25.996,-17.97,25.863,-17.952,25.742,-17.858,25.64,-17.824,25.557,-17.85,25.452,-17.845,25.259,-17.794,25.092,-17.634,25.002,-17.569,24.932,-17.543,24.733,-17.518,24.275,-17.481,24.227,-17.49,24.037,-17.521,23.799,-17.56,23.595,-17.599,23.381,-17.641,23.182,-17.474,22.956,-17.286,22.722,-17.075,22.546,-16.91,22.459,-16.815,22.305,-16.69,22.194,-16.628,22.151,-16.597,22.04,-16.263,21.98,-15.956,21.98,-15.724,21.98,-15.403,21.98,-15.082,21.979,-14.761,21.979,-14.441,21.979,-14.12,21.979,-13.799,21.979,-13.478,21.979,-13.157,21.979,-13.001,22.21,-13.001,22.471,-13.001,22.744,-13.001,23.042,-13.001,23.339,-13.001,23.636,-13.001,23.843,-13.001,23.897,-12.998,23.963,-12.988,23.968,-12.957,23.882,-12.799,23.887,-12.743,23.909,-12.636,23.945,-12.544,23.991,-12.422,23.996,-12.351,23.959,-12.118,23.962,-11.988,23.973,-11.853,23.984,-11.725,23.971,-11.636,23.987,-11.587,24.015,-11.518,24.029,-11.439,24.047,-11.405,24.041,-11.374,24.026,-11.316,24.01,-11.185,23.988,-11.003,23.967,-10.872,24.003,-10.879,24.078,-10.892,24.115,-10.956,24.137,-11.026,24.187,-11.03,24.32,-11.072,24.366,-11.13,24.396,-11.255,24.378,-11.319,24.335,-11.371,24.378,-11.417,24.467,-11.448,24.519,-11.438,24.668,-11.353,24.728,-11.338,24.806,-11.321,24.877,-11.299,25.076,-11.26,25.185,-11.243,25.246,-11.212,25.289,-11.212,25.319,-11.237,25.292,-11.325,25.283,-11.405,25.321,-11.554,25.349,-11.623,25.413,-11.674,25.46,-11.7,25.512,-11.753,25.619,-11.744,25.855,-11.82,25.927,-11.855,26.026,-11.89,26.096,-11.903,26.34,-11.93,26.43,-11.948,26.596,-11.972,26.73,-11.976,26.824,-11.965,26.89,-11.944,26.931,-11.919,26.95,-11.899,26.977,-11.825,27.027,-11.664,27.046,-11.616,27.095,-11.594,27.159,-11.579,27.196,-11.605,27.238,-11.783,27.424,-11.945,27.487,-12.08,27.533,-12.195,27.574,-12.227,27.644,-12.267,27.757,-12.281,27.857,-12.285,28.069,-12.368,28.237,-12.435,28.358,-12.482,28.413,-12.518,28.451,-12.577,28.474,-12.623,28.511,-12.742,28.551,-12.836,28.615,-12.854,28.673,-12.861,28.73,-12.925,28.773,-12.982,28.859,-13.119,28.922,-13.215,28.942,-13.307,29.014,-13.369,29.112,-13.395,29.202,-13.398,29.254,-13.371,29.382,-13.323,29.481,-13.268,29.554,-13.249,29.597,-13.261,29.63,-13.299,29.648,-13.373,29.652,-13.414,29.723,-13.454,29.775,-13.438,29.795,-13.393,29.796,-13.37,29.796,-13.167,29.796,-12.992,29.796,-12.827,29.796,-12.626,29.796,-12.451,29.795,-12.306,29.795,-12.155,29.75,-12.164,29.692,-12.198,29.56,-12.202,29.508,-12.228,29.492,-12.267,29.502,-12.318,29.505,-12.386,29.486,-12.418,29.428,-12.431,29.344,-12.405,29.191,-12.37,29.064,-12.349,28.973,-12.258,28.85,-12.121,28.769,-12.051,28.575,-11.908,28.542,-11.879,28.483,-11.812,28.432,-11.698,28.407,-11.623,28.383,-11.567,28.357,-11.483,28.404,-11.354,28.47,-11.11,28.518,-10.933,28.544,-10.802,28.639,-10.669,28.646,-10.55,28.607,-10.397,28.617,-10.313,28.624,-10.099,28.629,-9.919,28.63,-9.831,28.604,-9.679,28.541,-9.51,28.4,-9.275,28.401,-9.225,28.484,-9.169,28.617,-9.072,28.681,-9.015,28.759,-8.933,28.794,-8.891,28.87,-8.786,28.918,-8.701,28.934,-8.59,28.898,-8.485,28.972,-8.465,29.216,-8.428,29.484,-8.387,29.766,-8.344,30.051,-8.3,30.328,-8.258,30.578,-8.22,30.751,-8.194,30.777,-8.266,30.831,-8.386,30.892,-8.474,30.968,-8.551,31.033,-8.598,31.076,-8.612,31.351,-8.607,31.449,-8.654,31.535,-8.713,31.556,-8.805,31.613,-8.863,31.674,-8.909,31.7,-8.914,31.745,-8.903,31.818,-8.902,31.886,-8.922,31.919,-8.942,31.922,-9.019,31.943,-9.054,32.035,-9.067,32.13,-9.073,32.221,-9.126,32.319,-9.135,32.433,-9.156,32.487,-9.213,32.608,-9.271,32.757,-9.322,32.863,-9.381,32.92,-9.407,32.923,-9.434,32.951,-9.484,32.98,-9.52,32.982,-9.574,32.996,-9.623,33.038,-9.635,33.072,-9.638,33.104,-9.603,33.148,-9.604,33.196,-9.626,33.213,-9.683,33.25,-9.76,33.31,-9.812,33.351,-9.862,33.337,-9.954,33.312,-10.038,33.394,-10.121,33.5,-10.2,33.529,-10.235,33.538,-10.352,33.554,-10.391,33.626,-10.489,33.662,-10.553,33.659,-10.591,33.465,-10.783,33.403,-10.802,33.345,-10.813,33.293,-10.852,33.261,-10.893,33.273,-10.915,33.293,-10.981,33.339,-11.085,33.38,-11.158,33.346,-11.249,33.268,-11.404,33.233,-11.418,33.226,-11.535,33.25,-11.578,33.288,-11.611,33.304,-11.691,33.305,-11.8,33.301,-11.888,33.252,-12.113,33.34,-12.308,33.37,-12.33,33.491,-12.331,33.512,-12.348,33.483,-12.403,33.431,-12.46,33.398,-12.49,33.243,-12.557,33.022,-12.63,32.975,-12.701,32.946,-12.804,32.971,-12.865,33,-12.9,32.99,-12.989,32.971,-13.084,32.978,-13.159,32.968,-13.225,32.939,-13.257,32.9,-13.357,32.852,-13.457,32.814,-13.503,32.758,-13.55,32.67,-13.59,32.672,-13.61,32.772,-13.657,32.797,-13.688,32.807,-13.71,32.785,-13.731,32.765,-13.761,32.811,-13.792,32.867,-13.817,32.92,-13.884,32.968,-13.977,32.981,-14.009,32.992,-14.022,33.009,-14.024,33.042,-14.01,33.104,-13.959,33.148,-13.941,33.202,-14.013,32.987,-14.085,32.875,-14.122,32.553,-14.23,32.273,-14.323,32.2,-14.341,32.054,-14.387,31.982,-14.414,31.729,-14.496,31.623,-14.537,31.538,-14.577,31.329,-14.638,31.131,-14.695,30.915,-14.753,30.673,-14.819,30.538,-14.867,30.446,-14.908,30.232,-14.99,30.222,-15.011,30.225,-15.067,30.252,-15.183,30.306,-15.289,30.351,-15.35,30.38,-15.506,30.396,-15.643]]]},{"n":"Yemen","c":"YE","r":3,"ml":3,"lx":45.87,"ly":15.33,"b":[42.549,12.319,54.511,18.996],"p":[[[53.086,16.648,52.581,16.47,52.448,16.391,52.328,16.294,52.237,16.171,52.174,15.957,52.222,15.761,52.217,15.656,52.087,15.586,51.966,15.536,51.831,15.459,51.749,15.44,51.682,15.379,51.604,15.337,51.322,15.226,51.015,15.141,50.527,15.038,50.339,14.927,50.167,14.851,49.906,14.828,49.549,14.722,49.35,14.638,49.103,14.5,49.048,14.456,49.005,14.355,48.929,14.267,48.78,14.124,48.668,14.05,48.594,14.046,48.449,14.006,48.278,13.998,47.99,14.048,47.916,14.013,47.855,13.957,47.633,13.858,47.408,13.662,47.243,13.609,46.976,13.547,46.789,13.466,46.663,13.433,46.502,13.416,46.203,13.424,45.92,13.394,45.657,13.339,45.534,13.233,45.394,13.067,45.164,12.998,45.11,12.939,45.039,12.816,44.89,12.784,44.755,12.764,44.618,12.817,44.358,12.669,44.26,12.645,44.112,12.639,44.006,12.608,43.93,12.617,43.835,12.674,43.634,12.744,43.488,12.699,43.475,12.839,43.232,13.267,43.283,13.64,43.282,13.693,43.234,13.859,43.089,14.011,43.093,14.204,43.045,14.342,43.006,14.483,43.019,14.521,43.021,14.555,42.947,14.773,42.922,14.817,42.913,14.863,42.937,14.898,42.936,14.939,42.897,15.006,42.856,15.133,42.658,15.233,42.698,15.326,42.736,15.294,42.788,15.266,42.799,15.326,42.8,15.372,42.717,15.655,42.84,16.032,42.799,16.372,42.986,16.509,43.034,16.55,43.061,16.587,43.105,16.664,43.165,16.689,43.186,16.771,43.184,16.812,43.146,16.847,43.117,16.942,43.126,17.062,43.136,17.113,43.156,17.205,43.221,17.239,43.237,17.266,43.186,17.325,43.191,17.359,43.302,17.457,43.346,17.486,43.418,17.516,43.474,17.516,43.539,17.499,43.597,17.471,43.653,17.422,43.713,17.366,43.804,17.344,43.866,17.35,43.917,17.325,43.96,17.338,44.008,17.367,44.086,17.366,44.156,17.399,44.355,17.414,44.546,17.404,44.747,17.432,44.946,17.43,45.148,17.427,45.193,17.423,45.237,17.406,45.407,17.32,45.535,17.302,45.794,17.278,46.071,17.253,46.31,17.231,46.513,17.252,46.682,17.269,46.728,17.266,46.779,17.212,46.88,17.079,46.976,16.953,47.144,16.947,47.251,16.994,47.37,17.06,47.442,17.112,47.525,17.316,47.58,17.448,47.704,17.597,47.808,17.721,47.946,17.886,48.022,17.977,48.172,18.157,48.316,18.227,48.593,18.362,48.865,18.495,49.042,18.582,49.192,18.621,49.445,18.655,49.742,18.695,50.039,18.735,50.355,18.778,50.708,18.825,50.95,18.858,51.258,18.899,51.515,18.934,51.743,18.965,51.978,18.996,52.022,18.896,52.066,18.796,52.11,18.696,52.155,18.597,52.199,18.497,52.243,18.397,52.288,18.297,52.332,18.197,52.376,18.097,52.42,17.997,52.465,17.897,52.509,17.798,52.553,17.698,52.597,17.598,52.642,17.498,52.686,17.398,52.729,17.3,52.801,17.268,52.843,17.176,52.904,17.044,52.964,16.912,53.025,16.78,53.086,16.648]],[[42.59,15.303,42.559,15.281,42.549,15.32,42.57,15.407,42.602,15.433,42.625,15.368,42.61,15.332,42.59,15.303]],[[42.756,13.704,42.69,13.674,42.735,13.753,42.781,13.769,42.794,13.766,42.756,13.704]],[[42.787,13.971,42.774,13.95,42.756,13.955,42.694,14.008,42.762,14.067,42.798,14.012,42.787,13.971]],[[53.763,12.637,53.825,12.625,53.919,12.659,54.187,12.664,54.511,12.553,54.45,12.523,54.414,12.483,54.271,12.447,54.129,12.361,53.719,12.319,53.598,12.342,53.499,12.425,53.316,12.533,53.388,12.602,53.404,12.633,53.431,12.664,53.535,12.716,53.638,12.707,53.763,12.637]]]},{"n":"Vietnam","c":"VN","r":2,"ml":2,"lx":105.39,"ly":21.72,"b":[102.127,8.583,109.445,23.345],"p":[[[104.064,10.391,104.083,10.341,104.076,10.225,104.037,10.111,104.048,10.061,104.018,10.029,103.952,10.243,103.868,10.335,103.85,10.371,103.898,10.369,103.986,10.427,104.028,10.428,104.064,10.391]],[[107.973,21.508,107.926,21.499,107.809,21.497,107.707,21.406,107.637,21.368,107.527,21.336,107.41,21.285,107.376,21.194,107.373,21.128,107.354,21.055,107.165,20.949,107.112,20.96,107.075,20.999,107.019,20.991,106.981,20.971,106.936,20.974,106.886,20.95,106.821,20.958,106.76,20.991,106.725,21,106.683,21,106.675,20.96,106.737,20.806,106.753,20.735,106.551,20.527,106.573,20.392,106.518,20.289,106.396,20.206,106.166,19.992,106.062,19.987,105.984,19.939,105.814,19.587,105.812,19.467,105.785,19.379,105.791,19.294,105.716,19.128,105.639,19.057,105.622,18.966,105.732,18.779,105.744,18.746,105.808,18.646,105.839,18.574,105.888,18.502,106.066,18.316,106.145,18.259,106.24,18.221,106.412,18.053,106.499,17.946,106.459,17.874,106.479,17.72,106.356,17.765,106.371,17.747,106.517,17.663,106.736,17.367,106.926,17.221,107.12,17.056,107.18,16.898,107.355,16.794,107.549,16.643,107.541,16.609,107.593,16.568,107.724,16.488,107.803,16.403,107.834,16.322,107.882,16.31,107.936,16.329,107.991,16.337,108.029,16.331,108.088,16.243,108.17,16.164,108.209,16.091,108.241,16.101,108.267,16.09,108.274,16.029,108.286,15.989,108.395,15.872,108.447,15.763,108.578,15.585,108.674,15.484,108.743,15.427,108.821,15.378,108.898,15.181,108.94,15.001,109.022,14.803,109.085,14.716,109.087,14.553,109.137,14.384,109.191,14.27,109.207,14.154,109.224,14.097,109.245,14.053,109.303,13.856,109.288,13.765,109.247,13.855,109.252,13.591,109.288,13.451,109.272,13.279,109.31,13.219,109.377,13.025,109.424,12.956,109.42,12.719,109.445,12.6,109.381,12.671,109.336,12.752,109.274,12.709,109.219,12.646,109.305,12.391,109.207,12.415,109.216,12.073,109.256,11.993,109.259,11.955,109.247,11.909,109.22,11.959,109.215,12.01,109.199,11.999,109.2,11.972,109.167,11.912,109.158,11.837,109.193,11.773,109.199,11.725,109.173,11.665,109.133,11.601,109.04,11.593,109.018,11.468,108.987,11.336,108.821,11.315,108.7,11.199,108.551,11.156,108.419,11.041,108.272,10.934,108.176,10.92,108.095,10.897,108.001,10.72,107.845,10.7,107.564,10.555,107.47,10.486,107.384,10.459,107.262,10.398,107.235,10.42,107.194,10.472,107.088,10.498,107.036,10.556,107.021,10.631,107.007,10.661,106.984,10.618,106.966,10.441,106.947,10.4,106.902,10.383,106.813,10.433,106.727,10.536,106.606,10.465,106.643,10.456,106.698,10.462,106.741,10.444,106.778,10.376,106.776,10.339,106.757,10.296,106.644,10.289,106.492,10.304,106.464,10.298,106.602,10.232,106.729,10.193,106.785,10.151,106.785,10.116,106.714,10.06,106.659,9.991,106.658,9.949,106.657,9.901,106.596,9.86,106.557,9.868,106.449,9.94,106.136,10.222,106.184,10.142,106.507,9.821,106.564,9.716,106.572,9.641,106.539,9.604,106.484,9.559,106.378,9.556,106.204,9.675,105.926,9.962,105.831,10.001,106.113,9.674,106.159,9.594,106.206,9.502,106.193,9.448,106.168,9.397,105.501,9.093,105.401,8.962,105.322,8.801,105.191,8.711,105.114,8.629,104.892,8.583,104.77,8.598,104.896,8.747,104.819,8.802,104.815,9.185,104.845,9.606,104.903,9.816,104.987,9.869,105.093,9.901,105.095,9.945,105.084,9.996,105.028,10.067,104.966,10.101,104.873,10.115,104.802,10.203,104.748,10.199,104.663,10.17,104.613,10.208,104.594,10.267,104.516,10.34,104.426,10.411,104.467,10.422,104.514,10.463,104.564,10.516,104.69,10.523,104.815,10.521,104.851,10.534,104.901,10.59,104.984,10.662,105.046,10.702,105.061,10.734,105.036,10.809,105.022,10.887,105.046,10.911,105.159,10.898,105.284,10.861,105.315,10.845,105.387,10.94,105.406,10.952,105.453,10.951,105.577,10.969,105.698,10.994,105.755,10.99,105.811,10.926,105.853,10.864,105.875,10.858,105.938,10.885,105.99,10.852,106.099,10.797,106.164,10.795,106.132,10.922,106.168,11.012,106.161,11.037,106.1,11.079,105.892,11.245,105.856,11.294,105.861,11.372,105.854,11.487,105.835,11.559,105.838,11.601,105.851,11.635,105.89,11.648,105.927,11.653,105.956,11.682,106.006,11.758,106.103,11.751,106.239,11.708,106.34,11.682,106.399,11.687,106.413,11.698,106.411,11.738,106.418,11.912,106.414,11.948,106.5,11.966,106.631,11.969,106.7,11.979,106.765,12.052,106.931,12.077,107.051,12.176,107.159,12.277,107.212,12.304,107.28,12.322,107.33,12.319,107.393,12.26,107.446,12.296,107.506,12.365,107.538,12.432,107.555,12.54,107.544,12.706,107.512,12.836,107.482,12.933,107.475,13.03,107.546,13.225,107.605,13.438,107.594,13.522,107.529,13.654,107.462,13.816,107.389,13.993,107.362,14.019,107.343,14.069,107.331,14.127,107.36,14.308,107.364,14.369,107.448,14.451,107.493,14.546,107.535,14.65,107.519,14.705,107.514,14.817,107.525,14.872,107.505,14.916,107.48,14.98,107.496,15.021,107.555,15.057,107.59,15.118,107.634,15.19,107.653,15.255,107.622,15.31,107.564,15.392,107.46,15.466,107.339,15.56,107.279,15.619,107.233,15.678,107.19,15.747,107.166,15.802,107.189,15.839,107.361,15.922,107.392,15.952,107.41,15.998,107.396,16.043,107.35,16.067,107.296,16.084,107.217,16.136,107.07,16.28,107.002,16.312,106.931,16.353,106.893,16.397,106.851,16.516,106.832,16.526,106.792,16.49,106.74,16.453,106.696,16.459,106.656,16.493,106.638,16.538,106.594,16.6,106.546,16.651,106.534,16.821,106.526,16.877,106.502,16.954,106.465,16.982,106.426,17.003,106.333,17.144,106.27,17.217,106.006,17.415,105.974,17.447,105.903,17.529,105.779,17.644,105.691,17.738,105.627,17.834,105.598,17.918,105.588,17.984,105.519,18.077,105.458,18.154,105.4,18.179,105.333,18.19,105.273,18.235,105.163,18.339,105.115,18.405,105.086,18.45,105.087,18.496,105.113,18.573,105.145,18.617,105.146,18.651,105.115,18.679,104.993,18.728,104.717,18.803,104.613,18.861,104.518,18.934,104.446,18.984,104.109,19.196,104.006,19.231,103.918,19.269,103.892,19.305,103.896,19.34,103.932,19.366,104.028,19.42,104.063,19.483,104.052,19.564,104.013,19.646,104.032,19.675,104.063,19.678,104.127,19.681,104.26,19.685,104.546,19.611,104.588,19.619,104.743,19.755,104.802,19.836,104.815,19.904,104.846,19.947,104.928,20.018,104.929,20.083,104.889,20.169,104.848,20.202,104.813,20.217,104.699,20.205,104.677,20.225,104.662,20.289,104.656,20.329,104.619,20.375,104.496,20.414,104.392,20.425,104.368,20.441,104.408,20.486,104.479,20.53,104.533,20.555,104.575,20.6,104.583,20.647,104.53,20.688,104.461,20.734,104.35,20.821,104.195,20.914,104.101,20.946,104.052,20.941,103.882,20.861,103.791,20.81,103.714,20.717,103.635,20.697,103.555,20.738,103.464,20.78,103.211,20.841,103.104,20.892,102.884,21.203,102.851,21.266,102.872,21.338,102.888,21.44,102.91,21.506,102.949,21.57,102.959,21.626,102.95,21.681,102.918,21.713,102.876,21.722,102.845,21.735,102.816,21.807,102.798,21.798,102.771,21.71,102.739,21.678,102.695,21.662,102.662,21.676,102.641,21.711,102.631,21.771,102.61,21.852,102.583,21.904,102.488,21.958,102.443,22.027,102.301,22.178,102.183,22.284,102.127,22.379,102.176,22.415,102.237,22.466,102.302,22.546,102.376,22.647,102.406,22.708,102.428,22.733,102.471,22.751,102.517,22.741,102.599,22.7,102.721,22.648,102.83,22.587,102.874,22.525,102.935,22.466,102.982,22.448,103.005,22.453,103.076,22.498,103.136,22.542,103.138,22.593,103.193,22.639,103.266,22.714,103.301,22.764,103.327,22.77,103.356,22.755,103.471,22.597,103.493,22.588,103.525,22.612,103.571,22.734,103.62,22.782,103.637,22.77,103.915,22.538,103.942,22.54,103.971,22.55,103.991,22.586,104.013,22.666,104.054,22.752,104.143,22.8,104.213,22.809,104.238,22.769,104.298,22.712,104.372,22.704,104.527,22.804,104.578,22.82,104.632,22.818,104.687,22.822,104.74,22.86,104.796,22.911,104.815,23.011,104.827,23.1,104.865,23.136,104.91,23.161,104.996,23.194,105.189,23.281,105.239,23.322,105.275,23.345,105.35,23.308,105.44,23.235,105.495,23.181,105.531,23.122,105.548,23.073,105.691,23.03,105.782,22.969,105.843,22.923,105.903,22.925,105.962,22.937,106.001,22.975,106.068,22.976,106.148,22.97,106.184,22.955,106.249,22.869,106.279,22.857,106.338,22.863,106.451,22.894,106.542,22.908,106.624,22.874,106.78,22.779,106.736,22.711,106.702,22.638,106.633,22.586,106.582,22.573,106.55,22.501,106.536,22.395,106.554,22.342,106.593,22.325,106.637,22.289,106.654,22.241,106.66,22.136,106.658,22.018,106.664,21.979,106.698,21.986,106.729,22,106.794,21.982,106.875,21.951,106.925,21.92,106.971,21.924,107.006,21.893,107.02,21.835,107.062,21.794,107.179,21.717,107.272,21.711,107.351,21.609,107.433,21.642,107.471,21.598,107.641,21.614,107.759,21.655,107.802,21.645,107.908,21.56,107.973,21.508]],[[106.617,8.683,106.589,8.681,106.568,8.701,106.659,8.766,106.65,8.723,106.653,8.701,106.617,8.683]],[[107.168,10.397,107.084,10.337,107.078,10.388,107.151,10.42,107.177,10.446,107.195,10.446,107.168,10.397]],[[106.866,20.816,106.854,20.796,106.803,20.844,106.769,20.864,106.795,20.928,106.855,20.858,106.866,20.816]],[[107.603,21.217,107.459,21.092,107.404,21.094,107.453,21.235,107.476,21.269,107.563,21.22,107.603,21.217]],[[107.521,20.927,107.466,20.901,107.399,20.903,107.479,20.952,107.519,21.013,107.551,21.034,107.551,20.981,107.521,20.927]],[[107.031,20.747,106.99,20.743,106.911,20.824,106.953,20.867,107.044,20.837,107.064,20.817,107.064,20.8,107.042,20.761,107.031,20.747]]]},{"n":"Venezuela","c":"VE","r":3,"ml":2.5,"lx":-64.6,"ly":7.18,"b":[-73.366,0.688,-59.829,12.178],"p":[[[-60.821,9.138,-60.941,9.106,-60.939,9.132,-60.907,9.179,-60.845,9.192,-60.821,9.208,-60.782,9.218,-60.759,9.216,-60.736,9.203,-60.79,9.177,-60.821,9.138]],[[-63.849,11.131,-63.817,11,-63.827,10.976,-63.918,10.888,-63.994,10.881,-64.055,10.884,-64.101,10.901,-64.161,10.959,-64.219,10.942,-64.362,10.962,-64.402,10.982,-64.349,11.052,-64.25,11.08,-64.214,11.086,-64.185,11.043,-64.113,11.006,-64.028,11.002,-64.007,11.068,-63.893,11.167,-63.849,11.131]],[[-65.213,10.906,-65.266,10.884,-65.365,10.906,-65.415,10.938,-65.383,10.974,-65.302,10.974,-65.227,10.93,-65.213,10.906]],[[-60.998,8.867,-61.06,8.847,-61.069,8.947,-61.05,8.974,-60.945,9.055,-60.916,9.07,-60.895,9.053,-60.9,9.032,-60.849,8.996,-60.861,8.95,-60.916,8.899,-60.998,8.867]],[[-60.018,8.549,-59.832,8.306,-59.829,8.279,-59.849,8.249,-59.965,8.192,-59.991,8.162,-60.032,8.054,-60.178,7.994,-60.279,7.919,-60.347,7.854,-60.381,7.828,-60.514,7.813,-60.556,7.772,-60.61,7.648,-60.649,7.597,-60.719,7.536,-60.719,7.499,-60.624,7.363,-60.607,7.321,-60.636,7.257,-60.633,7.211,-60.583,7.156,-60.523,7.144,-60.465,7.167,-60.392,7.165,-60.345,7.15,-60.325,7.134,-60.322,7.092,-60.352,7.003,-60.395,6.945,-60.586,6.857,-60.671,6.806,-60.718,6.768,-60.821,6.788,-60.873,6.787,-60.914,6.758,-60.938,6.733,-61.007,6.727,-61.105,6.711,-61.146,6.695,-61.177,6.651,-61.204,6.588,-61.182,6.513,-61.151,6.447,-61.152,6.385,-61.129,6.214,-61.159,6.174,-61.225,6.129,-61.303,6.05,-61.391,5.939,-61.377,5.907,-61.167,5.674,-60.954,5.437,-60.742,5.202,-60.712,5.192,-60.672,5.164,-60.635,5.082,-60.604,4.995,-60.604,4.949,-60.628,4.893,-60.679,4.827,-60.742,4.774,-60.833,4.729,-60.906,4.687,-60.966,4.575,-61.003,4.535,-61.036,4.519,-61.102,4.505,-61.209,4.508,-61.28,4.517,-61.368,4.433,-61.479,4.402,-61.554,4.288,-61.821,4.197,-62.082,4.126,-62.153,4.098,-62.411,4.157,-62.473,4.139,-62.544,4.084,-62.61,4.042,-62.665,4.04,-62.712,4.018,-62.74,3.94,-62.765,3.673,-62.857,3.593,-62.969,3.594,-63.045,3.686,-63.136,3.756,-63.295,3.922,-63.339,3.944,-63.38,3.943,-63.527,3.894,-63.597,3.915,-63.653,3.941,-63.747,3.933,-63.915,3.931,-64.021,3.929,-64.073,3.974,-64.122,4.067,-64.154,4.1,-64.192,4.127,-64.256,4.14,-64.526,4.14,-64.576,4.14,-64.614,4.158,-64.666,4.237,-64.722,4.274,-64.789,4.276,-64.818,4.232,-64.703,4.089,-64.669,4.012,-64.568,3.9,-64.275,3.663,-64.221,3.587,-64.227,3.491,-64.229,3.344,-64.219,3.205,-64.144,3.005,-64.038,2.802,-64.009,2.672,-64.029,2.576,-64.049,2.525,-64.047,2.502,-64.025,2.482,-63.924,2.452,-63.713,2.434,-63.585,2.434,-63.389,2.412,-63.375,2.34,-63.394,2.223,-63.433,2.156,-63.464,2.136,-63.57,2.121,-63.682,2.048,-63.844,1.977,-63.937,1.967,-63.976,1.953,-64.008,1.932,-64.035,1.904,-64.067,1.771,-64.115,1.619,-64.205,1.529,-64.304,1.455,-64.405,1.447,-64.486,1.453,-64.526,1.431,-64.584,1.37,-64.667,1.294,-64.732,1.253,-64.818,1.257,-64.91,1.22,-65.027,1.158,-65.104,1.108,-65.17,1.022,-65.264,0.932,-65.361,0.869,-65.407,0.79,-65.473,0.691,-65.556,0.688,-65.563,0.748,-65.523,0.843,-65.566,0.926,-65.645,0.97,-65.681,0.983,-65.718,0.978,-65.811,0.937,-65.926,0.863,-65.996,0.81,-66.06,0.785,-66.191,0.763,-66.302,0.752,-66.347,0.767,-66.429,0.822,-66.619,0.992,-66.876,1.223,-66.896,1.29,-66.884,1.358,-66.931,1.458,-66.958,1.564,-66.982,1.601,-66.988,1.68,-67.044,1.823,-67.09,1.94,-67.131,2,-67.114,2.051,-67.131,2.101,-67.165,2.143,-67.215,2.275,-67.198,2.333,-67.211,2.39,-67.253,2.429,-67.312,2.472,-67.392,2.56,-67.486,2.644,-67.535,2.677,-67.568,2.69,-67.597,2.769,-67.619,2.794,-67.667,2.8,-67.766,2.833,-67.859,2.794,-67.861,2.855,-67.835,2.893,-67.515,3.187,-67.354,3.323,-67.336,3.343,-67.322,3.374,-67.311,3.416,-67.348,3.464,-67.499,3.691,-67.551,3.734,-67.603,3.769,-67.662,3.864,-67.732,4.087,-67.783,4.198,-67.799,4.284,-67.795,4.381,-67.814,4.455,-67.855,4.507,-67.855,4.665,-67.814,4.931,-67.804,5.133,-67.825,5.27,-67.788,5.375,-67.695,5.448,-67.642,5.559,-67.631,5.709,-67.575,5.833,-67.474,5.93,-67.439,6.026,-67.472,6.12,-67.482,6.18,-67.568,6.242,-67.727,6.285,-67.859,6.29,-67.939,6.242,-68.143,6.198,-68.472,6.157,-68.736,6.157,-68.937,6.198,-69.09,6.184,-69.195,6.115,-69.268,6.1,-69.311,6.138,-69.357,6.148,-69.427,6.124,-69.439,6.135,-69.595,6.321,-69.739,6.494,-69.904,6.7,-70.095,6.938,-70.129,6.954,-70.188,6.952,-70.266,6.948,-70.387,6.973,-70.471,7.007,-70.536,7.041,-70.655,7.083,-70.737,7.09,-70.811,7.078,-71.013,6.994,-71.129,6.987,-71.218,6.985,-71.457,7.026,-71.621,7.033,-71.811,7.006,-71.893,6.99,-72.007,7.033,-72.084,7.097,-72.157,7.25,-72.208,7.37,-72.296,7.395,-72.395,7.415,-72.443,7.455,-72.472,7.524,-72.479,7.613,-72.469,7.758,-72.46,7.81,-72.446,7.966,-72.392,8.048,-72.358,8.087,-72.364,8.153,-72.39,8.287,-72.417,8.382,-72.526,8.49,-72.665,8.628,-72.726,8.848,-72.796,9.109,-72.852,9.135,-72.904,9.122,-72.96,9.135,-73.009,9.24,-73.058,9.26,-73.137,9.223,-73.193,9.194,-73.337,9.168,-73.366,9.194,-73.356,9.227,-73.296,9.322,-73.224,9.444,-73.141,9.555,-73.064,9.668,-73.007,9.789,-72.967,10.03,-72.94,10.196,-72.869,10.491,-72.739,10.727,-72.69,10.836,-72.572,10.977,-72.518,11.054,-72.446,11.114,-72.248,11.196,-72.012,11.602,-71.958,11.666,-71.719,11.727,-71.536,11.774,-71.4,11.824,-71.356,11.85,-71.32,11.862,-71.349,11.815,-71.415,11.755,-71.488,11.719,-71.869,11.627,-71.908,11.608,-71.957,11.57,-71.957,11.483,-71.947,11.414,-71.835,11.19,-71.791,11.135,-71.642,11.014,-71.676,10.997,-71.731,10.995,-71.69,10.835,-71.598,10.726,-71.594,10.657,-71.665,10.444,-71.794,10.316,-71.885,10.167,-71.956,10.108,-72.113,9.816,-71.993,9.642,-71.976,9.553,-71.873,9.428,-71.806,9.386,-71.761,9.336,-71.781,9.25,-71.74,9.134,-71.687,9.073,-71.62,9.048,-71.537,9.048,-71.298,9.126,-71.241,9.16,-71.205,9.222,-71.086,9.348,-71.078,9.511,-71.053,9.706,-71.082,9.833,-71.207,10.015,-71.262,10.144,-71.387,10.264,-71.463,10.469,-71.494,10.533,-71.518,10.622,-71.545,10.779,-71.461,10.836,-71.47,10.964,-71.264,11,-70.821,11.208,-70.546,11.261,-70.233,11.373,-70.16,11.428,-70.097,11.52,-70.049,11.53,-69.885,11.444,-69.805,11.474,-69.773,11.541,-69.817,11.672,-69.911,11.672,-70.193,11.625,-70.22,11.681,-70.22,11.73,-70.287,11.886,-70.245,12.004,-70.203,12.098,-70.122,12.137,-70.004,12.178,-69.914,12.115,-69.86,12.054,-69.831,11.996,-69.811,11.837,-69.762,11.676,-69.712,11.564,-69.632,11.48,-69.57,11.485,-69.526,11.5,-69.233,11.518,-69.055,11.461,-68.828,11.432,-68.616,11.309,-68.399,11.161,-68.343,11.053,-68.325,10.949,-68.272,10.88,-68.325,10.809,-68.296,10.689,-68.234,10.569,-68.14,10.493,-67.872,10.472,-67.581,10.524,-67.133,10.57,-66.989,10.611,-66.247,10.632,-66.106,10.575,-66.092,10.517,-66.09,10.473,-65.852,10.258,-65.656,10.228,-65.489,10.159,-65.317,10.122,-65.129,10.07,-65.023,10.077,-64.944,10.095,-64.85,10.098,-64.188,10.458,-63.834,10.449,-63.779,10.472,-63.732,10.503,-63.863,10.558,-64.158,10.579,-64.248,10.543,-64.298,10.635,-64.202,10.633,-63.873,10.664,-63.497,10.643,-63.19,10.709,-63.035,10.72,-62.947,10.707,-62.702,10.75,-62.242,10.7,-61.879,10.741,-61.921,10.681,-62.04,10.645,-62.233,10.634,-62.38,10.547,-62.694,10.563,-62.914,10.531,-62.843,10.507,-62.843,10.418,-62.813,10.4,-62.781,10.399,-62.706,10.333,-62.686,10.29,-62.662,10.199,-62.695,10.1,-62.741,10.056,-62.651,10.071,-62.6,10.117,-62.608,10.163,-62.6,10.217,-62.55,10.2,-62.515,10.176,-62.401,9.918,-62.32,9.783,-62.3,9.788,-62.281,9.793,-62.257,9.819,-62.221,9.883,-62.19,9.842,-62.172,9.827,-62.153,9.822,-62.17,9.879,-62.147,9.953,-62.155,9.979,-62.12,9.985,-62.077,9.975,-62.017,9.955,-61.909,9.87,-61.837,9.782,-61.831,9.733,-61.805,9.706,-61.759,9.677,-61.736,9.631,-61.732,9.702,-61.759,9.754,-61.766,9.814,-61.625,9.816,-61.589,9.895,-61.512,9.848,-61.309,9.633,-61.234,9.598,-61.013,9.556,-60.874,9.453,-60.792,9.361,-60.841,9.264,-60.971,9.215,-61.023,9.155,-61.053,9.095,-61.054,9.035,-61.093,8.966,-61.099,8.941,-61.122,8.843,-61.176,8.725,-61.247,8.6,-61.619,8.597,-61.527,8.546,-61.443,8.509,-61.304,8.41,-61.194,8.488,-61.036,8.493,-60.865,8.579,-60.801,8.592,-60.481,8.547,-60.404,8.61,-60.34,8.629,-60.167,8.617,-60.018,8.549]]]},{"n":"Vatican City","c":"VA","r":6,"ml":5,"lx":12.45,"ly":41.9,"b":[12.428,41.898,12.439,41.906],"p":[[[12.439,41.898,12.431,41.898,12.428,41.901,12.431,41.905,12.438,41.906,12.439,41.898]]]},{"n":"Vanuatu","c":"VU","r":4,"ml":4,"lx":166.91,"ly":-15.37,"b":[166.526,-20.242,169.896,-13.709],"p":[[[166.746,-14.827,166.81,-15.157,166.885,-15.157,166.923,-15.139,166.968,-15.062,166.987,-14.94,167.027,-14.923,167.076,-14.936,167.054,-14.974,167.069,-15.072,167.106,-15.126,167.132,-15.135,167.182,-15.39,167.201,-15.443,167.2,-15.486,167.094,-15.581,166.937,-15.578,166.826,-15.635,166.758,-15.631,166.759,-15.567,166.699,-15.516,166.631,-15.406,166.648,-15.212,166.527,-14.85,166.526,-14.76,166.567,-14.642,166.608,-14.637,166.663,-14.735,166.746,-14.827]],[[167.413,-16.096,167.459,-16.118,167.484,-16.118,167.499,-16.166,167.642,-16.263,167.681,-16.261,167.714,-16.314,167.776,-16.341,167.793,-16.395,167.837,-16.45,167.76,-16.516,167.611,-16.499,167.526,-16.574,167.449,-16.555,167.436,-16.515,167.447,-16.502,167.401,-16.401,167.38,-16.246,167.349,-16.154,167.316,-16.116,167.246,-16.15,167.218,-16.155,167.151,-16.08,167.183,-15.929,167.2,-15.885,167.254,-15.877,167.336,-15.917,167.413,-16.096]],[[168.447,-16.779,168.477,-16.794,168.46,-16.835,168.323,-16.788,168.212,-16.806,168.181,-16.804,168.149,-16.766,168.124,-16.69,168.135,-16.637,168.182,-16.6,168.199,-16.594,168.234,-16.64,168.265,-16.671,168.296,-16.684,168.366,-16.759,168.447,-16.779]],[[168.297,-16.337,168.182,-16.347,168.022,-16.316,167.957,-16.272,167.929,-16.229,167.985,-16.196,168.064,-16.181,168.164,-16.082,168.198,-16.12,168.235,-16.231,168.276,-16.265,168.298,-16.299,168.297,-16.337]],[[168.446,-17.542,168.545,-17.685,168.585,-17.696,168.525,-17.798,168.399,-17.807,168.252,-17.781,168.306,-17.746,168.278,-17.706,168.233,-17.698,168.182,-17.717,168.158,-17.711,168.191,-17.645,168.273,-17.552,168.297,-17.545,168.32,-17.544,168.341,-17.552,168.446,-17.542]],[[167.911,-15.436,167.844,-15.482,167.72,-15.477,167.674,-15.452,167.826,-15.312,168.003,-15.283,167.911,-15.436]],[[168.213,-15.97,168.196,-15.972,168.179,-15.926,168.123,-15.681,168.16,-15.462,168.183,-15.508,168.268,-15.892,168.256,-15.955,168.213,-15.97]],[[168.189,-15.329,168.172,-15.391,168.13,-15.319,168.104,-15.017,168.115,-14.989,168.136,-14.986,168.187,-15.197,168.189,-15.329]],[[167.219,-15.724,167.201,-15.75,167.095,-15.685,167.119,-15.623,167.234,-15.645,167.219,-15.724]],[[167.585,-14.261,167.543,-14.312,167.43,-14.295,167.404,-14.282,167.411,-14.197,167.439,-14.168,167.506,-14.142,167.599,-14.184,167.585,-14.261]],[[167.489,-13.907,167.474,-13.917,167.451,-13.909,167.392,-13.788,167.407,-13.748,167.481,-13.709,167.547,-13.777,167.554,-13.814,167.553,-13.846,167.543,-13.873,167.499,-13.885,167.489,-13.907]],[[169.896,-20.187,169.861,-20.242,169.807,-20.241,169.738,-20.202,169.751,-20.153,169.829,-20.145,169.852,-20.148,169.896,-20.187]],[[169.491,-19.54,169.438,-19.649,169.347,-19.624,169.262,-19.545,169.217,-19.476,169.247,-19.345,169.291,-19.322,169.337,-19.329,169.36,-19.458,169.491,-19.54]],[[169.334,-18.94,169.288,-18.989,169.248,-18.983,168.987,-18.871,168.998,-18.825,168.987,-18.708,169.016,-18.644,169.088,-18.617,169.144,-18.631,169.178,-18.725,169.256,-18.763,169.201,-18.796,169.296,-18.867,169.334,-18.94]]]},{"n":"Uzbekistan","c":"UZ","r":3,"ml":3,"lx":64.01,"ly":41.69,"b":[55.976,37.172,73.137,45.555],"p":[[[70.947,42.249,70.979,42.267,71.036,42.285,71.13,42.25,71.213,42.206,71.232,42.186,71.229,42.163,71.032,42.078,70.91,42.038,70.857,42.031,70.842,42.02,70.803,41.923,70.728,41.905,70.631,41.875,70.563,41.831,70.455,41.725,70.181,41.571,70.177,41.54,70.201,41.514,70.29,41.497,70.408,41.45,70.471,41.413,70.646,41.46,70.689,41.45,70.734,41.401,70.782,41.263,70.86,41.225,70.963,41.196,71.026,41.187,71.111,41.153,71.223,41.14,71.299,41.152,71.393,41.123,71.408,41.136,71.421,41.342,71.5,41.307,71.546,41.308,71.586,41.333,71.606,41.367,71.62,41.435,71.602,41.503,71.638,41.534,71.665,41.541,71.685,41.533,71.697,41.516,71.701,41.454,71.758,41.428,71.792,41.413,71.826,41.361,71.858,41.311,71.879,41.195,71.958,41.187,72.052,41.165,72.115,41.187,72.164,41.174,72.181,41.118,72.181,41.067,72.187,41.026,72.213,41.014,72.295,41.04,72.364,41.043,72.427,41.019,72.506,40.982,72.62,40.884,72.658,40.87,72.831,40.862,72.867,40.842,72.926,40.842,72.99,40.86,73.132,40.829,73.137,40.811,73.113,40.786,72.774,40.65,72.749,40.609,72.68,40.556,72.604,40.525,72.567,40.524,72.402,40.578,72.383,40.565,72.369,40.543,72.37,40.52,72.406,40.463,72.389,40.427,72.358,40.402,72.254,40.424,72.235,40.439,72.233,40.454,72.193,40.454,72.131,40.439,72.013,40.341,71.971,40.29,71.956,40.259,71.903,40.241,71.845,40.234,71.773,40.188,71.692,40.152,71.667,40.179,71.651,40.208,71.63,40.217,71.58,40.21,71.52,40.209,71.457,40.242,71.376,40.275,71.305,40.287,71.095,40.271,70.991,40.255,70.958,40.239,70.899,40.235,70.653,40.201,70.603,40.214,70.566,40.267,70.534,40.325,70.47,40.345,70.398,40.361,70.372,40.384,70.37,40.412,70.377,40.439,70.383,40.454,70.549,40.563,70.698,40.661,70.712,40.669,70.726,40.688,70.751,40.722,70.751,40.74,70.639,40.779,70.635,40.797,70.657,40.815,70.657,40.84,70.578,40.911,70.442,41.023,70.402,41.035,70.373,41.028,70.319,40.919,70.292,40.892,70.136,40.82,70.006,40.771,69.773,40.684,69.713,40.657,69.671,40.662,69.628,40.679,69.498,40.767,69.414,40.797,69.357,40.767,69.309,40.724,69.314,40.635,69.26,40.588,69.206,40.567,69.304,40.327,69.294,40.297,69.22,40.288,69.275,40.198,69.228,40.188,69.11,40.209,68.952,40.223,68.653,40.183,68.631,40.167,68.622,40.147,68.64,40.129,68.785,40.127,68.927,40.136,68.966,40.12,68.972,40.09,68.956,40.071,68.908,40.068,68.805,40.05,68.793,40.031,68.789,40.013,68.824,39.961,68.864,39.927,68.869,39.907,68.852,39.891,68.832,39.884,68.798,39.909,68.778,39.904,68.768,39.882,68.758,39.856,68.735,39.836,68.687,39.846,68.639,39.839,68.61,39.743,68.586,39.635,68.507,39.563,68.463,39.537,68.399,39.529,68.303,39.538,68.245,39.548,68.077,39.564,67.909,39.594,67.719,39.621,67.542,39.558,67.492,39.519,67.46,39.482,67.426,39.466,67.35,39.242,67.358,39.217,67.4,39.197,67.617,39.15,67.648,39.131,67.667,39.109,67.677,39.008,67.694,38.995,67.769,38.982,67.876,38.983,67.96,38.993,68.044,38.984,68.104,38.962,68.133,38.928,68.149,38.891,68.048,38.669,68.056,38.589,68.087,38.474,68.144,38.383,68.251,38.295,68.333,38.238,68.35,38.211,68.354,38.17,68.341,38.117,68.294,38.033,68.237,37.96,68.174,37.928,68.088,37.835,68.011,37.721,67.864,37.571,67.814,37.487,67.798,37.245,67.759,37.172,67.753,37.2,67.7,37.227,67.607,37.223,67.546,37.236,67.517,37.267,67.442,37.258,67.32,37.21,67.196,37.235,67.069,37.335,66.828,37.371,66.522,37.348,66.511,37.459,66.511,37.599,66.526,37.786,66.629,37.932,66.626,37.96,66.606,37.987,66.575,38.011,66.39,38.051,66.335,38.072,66.264,38.118,66.173,38.167,66.095,38.2,65.971,38.244,65.857,38.269,65.79,38.25,65.729,38.226,65.671,38.226,65.613,38.239,65.4,38.349,65.077,38.539,64.821,38.672,64.66,38.736,64.622,38.756,64.532,38.816,64.31,38.977,64.163,38.954,63.953,39.058,63.764,39.161,63.721,39.188,63.506,39.377,63.292,39.5,63.058,39.633,62.907,39.717,62.651,39.858,62.525,39.944,62.483,39.976,62.442,40.036,62.375,40.332,62.298,40.467,62.188,40.541,62.095,40.683,62.018,40.894,61.954,41.031,61.903,41.094,61.8,41.163,61.645,41.24,61.497,41.276,61.444,41.275,61.417,41.265,61.388,41.252,61.329,41.195,61.242,41.189,61.179,41.191,61.12,41.211,60.933,41.229,60.867,41.249,60.755,41.246,60.514,41.216,60.455,41.222,60.2,41.349,60.09,41.399,60.067,41.427,60.069,41.476,60.106,41.545,60.138,41.594,60.124,41.645,60.076,41.701,60.076,41.76,60.109,41.793,60.176,41.782,60.201,41.803,60.192,41.834,60.156,41.857,60.107,41.907,59.963,41.954,59.942,41.974,59.949,41.995,59.974,42.019,59.979,42.068,59.982,42.132,60.001,42.165,60.006,42.191,59.985,42.212,59.937,42.236,59.858,42.295,59.763,42.302,59.451,42.3,59.354,42.323,59.277,42.356,59.199,42.482,59.16,42.511,59.123,42.524,59.036,42.528,58.931,42.54,58.877,42.561,58.73,42.676,58.589,42.778,58.532,42.682,58.477,42.663,58.353,42.672,58.26,42.688,58.206,42.666,58.152,42.628,58.162,42.603,58.204,42.576,58.289,42.527,58.418,42.407,58.477,42.34,58.486,42.317,58.474,42.299,58.457,42.292,58.431,42.292,58.397,42.292,58.377,42.312,58.371,42.347,58.327,42.399,58.283,42.429,58.234,42.448,58.166,42.462,58.075,42.487,58.029,42.488,57.983,42.459,57.946,42.42,57.923,42.335,57.856,42.231,57.814,42.19,57.686,42.165,57.382,42.156,57.291,42.124,57.229,42.084,57.114,41.957,57.034,41.915,56.964,41.857,56.985,41.669,57.018,41.451,57.077,41.39,57.114,41.372,57.119,41.35,57.095,41.331,57.064,41.307,57.018,41.263,56.966,41.265,56.861,41.276,56.774,41.288,56.48,41.301,56.242,41.311,55.977,41.322,55.977,41.552,55.977,41.781,55.977,42.011,55.977,42.24,55.977,42.47,55.977,42.7,55.977,42.929,55.977,43.159,55.976,43.388,55.976,43.618,55.976,43.847,55.976,44.077,55.976,44.306,55.976,44.536,55.976,44.765,55.976,44.995,56.1,45.023,56.258,45.059,56.409,45.094,56.589,45.135,56.792,45.181,56.965,45.221,57.172,45.268,57.329,45.304,57.477,45.337,57.667,45.377,57.961,45.44,58.125,45.474,58.291,45.509,58.449,45.543,58.555,45.555,58.669,45.508,58.807,45.442,58.945,45.376,59.083,45.31,59.221,45.244,59.36,45.179,59.498,45.113,59.636,45.047,59.774,44.981,59.912,44.916,60.05,44.85,60.188,44.784,60.327,44.718,60.465,44.652,60.603,44.587,60.741,44.521,60.879,44.455,61.008,44.394,61.065,44.348,61.097,44.248,61.161,44.169,61.271,44.082,61.385,43.994,61.526,43.877,61.624,43.796,61.723,43.714,61.888,43.577,61.99,43.492,62.072,43.489,62.238,43.51,62.459,43.537,62.634,43.558,62.846,43.584,63.048,43.608,63.207,43.628,63.445,43.613,63.68,43.599,63.848,43.588,64.013,43.578,64.209,43.566,64.318,43.559,64.443,43.551,64.496,43.572,64.604,43.613,64.706,43.653,64.812,43.694,64.905,43.715,65.003,43.649,65.085,43.574,65.171,43.494,65.271,43.418,65.367,43.372,65.496,43.311,65.571,43.205,65.67,43.065,65.736,42.972,65.803,42.877,65.901,42.915,66.006,42.955,66.1,42.991,66.089,42.873,66.079,42.767,66.063,42.605,66.05,42.473,66.016,42.315,66.013,42.194,66.011,42.089,66.01,42.005,66.193,42.001,66.329,41.998,66.499,41.995,66.515,41.889,66.538,41.741,66.573,41.607,66.602,41.494,66.645,41.349,66.669,41.271,66.71,41.179,66.75,41.157,66.814,41.142,67.039,41.153,67.225,41.162,67.372,41.17,67.528,41.177,67.735,41.187,67.805,41.164,67.866,41.18,67.936,41.197,67.991,41.13,68.02,41.096,68.059,41.061,68.113,41.029,68.09,40.96,68.057,40.861,68.048,40.809,68.112,40.754,68.16,40.722,68.292,40.656,68.415,40.619,68.496,40.609,68.573,40.623,68.601,40.66,68.594,40.711,68.557,40.765,68.559,40.829,68.584,40.876,68.663,40.962,68.737,41.042,68.851,41.124,68.987,41.205,69.043,41.264,69.065,41.367,69.154,41.425,69.249,41.46,69.368,41.491,69.401,41.542,69.565,41.629,69.664,41.672,69.788,41.697,69.96,41.754,70.096,41.821,70.226,41.946,70.329,42.028,70.416,42.079,70.489,42.08,70.54,42.039,70.584,42.036,70.613,42.055,70.663,42.107,70.715,42.169,70.765,42.194,70.86,42.207,70.947,42.249],[70.653,40.937,70.649,40.961,70.618,41.002,70.572,41.025,70.55,41.015,70.569,40.982,70.623,40.934,70.653,40.937]],[[71.206,39.893,71.156,39.883,71.064,39.885,71.012,39.895,71.044,39.976,71.045,39.993,71.041,39.995,71.014,40.006,70.974,40.039,70.961,40.088,70.976,40.133,71.005,40.152,71.024,40.149,71.08,40.08,71.13,40.06,71.229,40.048,71.179,39.98,71.216,39.907,71.206,39.893]],[[71.78,39.95,71.753,39.907,71.706,39.917,71.669,39.946,71.681,39.969,71.737,39.981,71.765,39.993,71.79,39.995,71.78,39.95]]]},{"n":"Uruguay","c":"UY","r":4,"ml":3,"lx":-55.97,"ly":-32.96,"b":[-58.438,-34.933,-53.126,-30.101],"p":[[[-53.371,-33.742,-53.42,-33.779,-53.472,-33.849,-53.535,-34.017,-53.743,-34.25,-53.785,-34.38,-54.01,-34.517,-54.169,-34.671,-54.272,-34.667,-54.365,-34.733,-54.902,-34.933,-55.095,-34.895,-55.238,-34.896,-55.371,-34.808,-55.673,-34.776,-55.863,-34.811,-56.118,-34.908,-56.195,-34.906,-56.25,-34.901,-56.388,-34.861,-56.463,-34.775,-56.855,-34.677,-57.171,-34.452,-57.543,-34.448,-57.829,-34.477,-57.873,-34.448,-57.902,-34.39,-57.961,-34.307,-58.207,-34.109,-58.4,-33.912,-58.438,-33.719,-58.411,-33.509,-58.353,-33.26,-58.364,-33.182,-58.292,-33.138,-58.222,-33.129,-58.154,-33.065,-58.093,-32.967,-58.082,-32.894,-58.13,-32.757,-58.162,-32.567,-58.201,-32.472,-58.123,-32.322,-58.12,-32.249,-58.165,-32.185,-58.177,-32.119,-58.156,-32.052,-58.16,-31.987,-58.189,-31.924,-58.167,-31.873,-58.096,-31.832,-58.042,-31.769,-58.007,-31.685,-57.989,-31.621,-57.988,-31.576,-58.01,-31.534,-58.054,-31.495,-58.033,-31.417,-57.948,-31.299,-57.893,-31.195,-57.868,-31.104,-57.87,-31.031,-57.898,-30.975,-57.886,-30.937,-57.834,-30.917,-57.811,-30.859,-57.819,-30.712,-57.873,-30.591,-57.831,-30.495,-57.713,-30.384,-57.651,-30.295,-57.646,-30.227,-57.609,-30.188,-57.552,-30.261,-57.384,-30.281,-57.214,-30.283,-57.187,-30.265,-57.121,-30.144,-57.033,-30.11,-56.937,-30.101,-56.833,-30.107,-56.722,-30.187,-56.407,-30.447,-56.176,-30.628,-56.106,-30.714,-56.045,-30.778,-55.999,-30.837,-56.018,-30.992,-56.016,-31.06,-56.005,-31.079,-55.952,-31.081,-55.874,-31.07,-55.808,-31.037,-55.756,-30.987,-55.706,-30.947,-55.665,-30.925,-55.65,-30.892,-55.627,-30.858,-55.603,-30.851,-55.557,-30.876,-55.45,-30.964,-55.366,-31.046,-55.346,-31.093,-55.313,-31.142,-55.279,-31.184,-55.255,-31.226,-55.174,-31.28,-55.091,-31.314,-55.036,-31.279,-54.896,-31.391,-54.588,-31.485,-54.531,-31.542,-54.478,-31.623,-54.37,-31.745,-54.221,-31.855,-54.1,-31.902,-53.985,-31.928,-53.921,-31.952,-53.877,-31.995,-53.806,-32.04,-53.762,-32.057,-53.747,-32.097,-53.701,-32.186,-53.654,-32.299,-53.602,-32.403,-53.489,-32.503,-53.363,-32.581,-53.231,-32.625,-53.157,-32.68,-53.126,-32.737,-53.214,-32.821,-53.31,-32.927,-53.395,-33.01,-53.483,-33.069,-53.512,-33.109,-53.531,-33.171,-53.53,-33.5,-53.538,-33.623,-53.531,-33.655,-53.519,-33.677,-53.464,-33.71,-53.398,-33.737,-53.371,-33.742]]]},{"n":"Federated States of Micronesia","c":"FM","r":6,"ml":5,"lx":158.23,"ly":6.89,"b":[138.062,5.277,162.993,9.593],"p":[[[162.983,5.326,162.993,5.277,162.93,5.301,162.921,5.318,162.958,5.335,162.983,5.326]],[[138.143,9.501,138.067,9.419,138.062,9.446,138.085,9.495,138.117,9.55,138.147,9.584,138.186,9.593,138.214,9.547,138.183,9.507,138.143,9.501]],[[151.648,7.346,151.639,7.333,151.578,7.338,151.57,7.346,151.575,7.351,151.604,7.357,151.608,7.375,151.593,7.379,151.606,7.389,151.629,7.39,151.643,7.379,151.65,7.363,151.648,7.346]],[[151.881,7.432,151.864,7.427,151.856,7.432,151.86,7.457,151.865,7.466,151.882,7.467,151.911,7.46,151.913,7.454,151.881,7.432]],[[158.315,6.814,158.257,6.791,158.183,6.801,158.161,6.883,158.128,6.905,158.135,6.945,158.186,6.978,158.295,6.951,158.335,6.893,158.309,6.855,158.315,6.814]]]},{"n":"Marshall Islands","c":"MH","r":6,"ml":5,"lx":171.19,"ly":7.08,"b":[166.845,5.8,171.757,11.169],"p":[[[169.635,5.83,169.615,5.8,169.591,5.802,169.612,5.824,169.627,5.856,169.651,5.945,169.7,5.977,169.735,6.014,169.726,5.976,169.673,5.935,169.635,5.83]],[[168.83,7.309,168.815,7.294,168.719,7.303,168.675,7.322,168.679,7.336,168.755,7.322,168.83,7.309]],[[171.577,7.048,171.615,7.027,171.688,7.028,171.757,6.973,171.73,6.977,171.693,7,171.659,7.01,171.614,7.007,171.593,7.016,171.577,7.048]],[[171.102,7.138,171.227,7.087,171.394,7.111,171.367,7.096,171.305,7.081,171.263,7.069,171.235,7.069,171.202,7.074,171.096,7.109,171.036,7.156,171.05,7.172,171.102,7.138]],[[166.89,11.153,166.864,11.146,166.845,11.153,166.859,11.166,166.888,11.169,166.899,11.165,166.89,11.153]]]},{"n":"Northern Mariana Islands","c":"MP","r":6,"ml":5,"lx":145.73,"ly":15.19,"b":[145.152,14.111,145.835,18.807],"p":[[[145.708,18.763,145.678,18.725,145.653,18.753,145.646,18.807,145.69,18.802,145.707,18.79,145.708,18.763]],[[145.712,16.339,145.69,16.332,145.658,16.336,145.636,16.352,145.631,16.378,145.696,16.38,145.72,16.36,145.712,16.339]],[[145.265,14.158,145.215,14.111,145.18,14.121,145.157,14.137,145.152,14.164,145.232,14.189,145.265,14.18,145.265,14.158]],[[145.778,18.079,145.729,18.057,145.789,18.155,145.807,18.173,145.835,18.137,145.778,18.079]],[[145.752,15.133,145.749,15.107,145.698,15.114,145.684,15.125,145.713,15.215,145.786,15.257,145.822,15.265,145.789,15.223,145.782,15.175,145.752,15.133]],[[145.662,14.971,145.621,14.92,145.592,14.999,145.587,15.031,145.625,15.06,145.647,15.059,145.662,14.971]]]},{"n":"United States Virgin Islands","c":"VI","r":6,"ml":5,"lx":-64.78,"ly":17.75,"b":[-65.024,17.702,-64.58,18.385],"p":[[[-64.845,18.33,-64.92,18.321,-65.024,18.368,-64.942,18.385,-64.889,18.374,-64.845,18.33]],[[-64.66,18.354,-64.726,18.328,-64.771,18.332,-64.788,18.341,-64.752,18.372,-64.66,18.354]],[[-64.766,17.794,-64.682,17.75,-64.58,17.75,-64.686,17.706,-64.889,17.702,-64.885,17.772,-64.766,17.794]]]},{"n":"Guam","c":"GU","r":6,"ml":3,"lx":144.7,"ly":13.35,"b":[144.649,13.258,144.941,13.622],"p":[[[144.742,13.259,144.7,13.258,144.663,13.291,144.65,13.313,144.649,13.429,144.79,13.527,144.837,13.622,144.875,13.615,144.91,13.599,144.941,13.57,144.78,13.411,144.742,13.259]]]},{"n":"American Samoa","c":"AS","r":4,"ml":4,"lx":-170.75,"ly":-14.33,"b":[-170.821,-14.36,-170.568,-14.257],"p":[[[-170.726,-14.351,-170.769,-14.36,-170.821,-14.312,-170.721,-14.276,-170.689,-14.257,-170.568,-14.267,-170.64,-14.282,-170.726,-14.351]]]},{"n":"Puerto Rico","c":"PR","r":5,"ml":3,"lx":-66.48,"ly":18.23,"b":[-67.937,17.947,-65.295,18.522],"p":[[[-66.129,18.445,-66.098,18.425,-66.068,18.428,-66.093,18.469,-66.07,18.469,-65.879,18.444,-65.756,18.402,-65.629,18.381,-65.621,18.242,-65.718,18.187,-65.782,18.129,-65.834,18.057,-65.971,17.974,-66.135,17.949,-66.245,17.947,-66.286,17.95,-66.326,17.964,-66.409,17.951,-66.511,17.987,-66.598,17.978,-66.772,17.987,-66.838,17.955,-66.9,17.948,-66.961,17.954,-67.013,17.968,-67.142,17.967,-67.197,17.994,-67.174,18.153,-67.172,18.224,-67.204,18.283,-67.239,18.321,-67.264,18.365,-67.213,18.394,-67.172,18.436,-67.159,18.499,-67.113,18.515,-67.06,18.522,-66.813,18.493,-66.189,18.476,-66.153,18.471,-66.129,18.445]],[[-65.426,18.106,-65.504,18.1,-65.555,18.108,-65.572,18.137,-65.477,18.165,-65.366,18.161,-65.303,18.144,-65.295,18.133,-65.426,18.106]],[[-67.872,18.06,-67.882,18.059,-67.891,18.06,-67.895,18.063,-67.902,18.072,-67.93,18.087,-67.937,18.101,-67.931,18.115,-67.919,18.121,-67.861,18.123,-67.855,18.121,-67.844,18.111,-67.843,18.104,-67.849,18.097,-67.859,18.08,-67.863,18.075,-67.867,18.071,-67.868,18.063,-67.872,18.06]]]},{"n":"United States of America","c":"US","r":2,"ml":1.7,"lx":-97.48,"ly":39.54,"b":[-178.195,18.964,179.78,71.408],"p":[[[-132.747,56.526,-132.758,56.511,-132.885,56.512,-132.931,56.524,-132.948,56.567,-132.936,56.607,-132.907,56.637,-132.871,56.696,-132.843,56.795,-132.656,56.685,-132.599,56.636,-132.568,56.576,-132.634,56.553,-132.714,56.543,-132.747,56.526]],[[-132.78,56.247,-132.831,56.244,-132.891,56.259,-133.035,56.341,-133.038,56.365,-133.017,56.392,-132.935,56.442,-132.902,56.454,-132.706,56.448,-132.643,56.435,-132.629,56.412,-132.632,56.388,-132.653,56.364,-132.658,56.339,-132.647,56.313,-132.669,56.287,-132.78,56.247]],[[-134.313,58.229,-134.32,58.204,-134.456,58.207,-134.594,58.243,-134.662,58.291,-134.648,58.312,-134.52,58.333,-134.399,58.287,-134.313,58.229]],[[-145.119,60.337,-145.15,60.313,-145.238,60.321,-145.284,60.337,-145.128,60.401,-145.102,60.388,-145.119,60.337]],[[-144.566,59.818,-144.614,59.813,-144.542,59.878,-144.445,59.951,-144.354,59.996,-144.236,60.015,-144.249,59.982,-144.403,59.921,-144.566,59.818]],[[-148.022,60.065,-148.074,60.035,-148.272,60.053,-148.231,60.114,-148.08,60.152,-147.914,60.092,-148.022,60.065]],[[-152.021,60.362,-152.069,60.358,-152.004,60.407,-151.96,60.504,-151.899,60.49,-151.887,60.473,-151.987,60.374,-152.021,60.362]],[[-160.329,55.338,-160.343,55.259,-160.481,55.309,-160.517,55.334,-160.493,55.352,-160.362,55.357,-160.329,55.338]],[[-159.362,54.972,-159.394,54.967,-159.421,54.978,-159.458,55.035,-159.462,55.059,-159.39,55.041,-159.363,55,-159.362,54.972]],[[-159.515,55.152,-159.52,55.072,-159.535,55.06,-159.561,55.081,-159.618,55.057,-159.648,55.075,-159.635,55.102,-159.64,55.124,-159.598,55.126,-159.588,55.165,-159.595,55.182,-159.575,55.218,-159.545,55.226,-159.515,55.152]],[[-166.21,53.723,-166.224,53.72,-166.249,53.745,-166.251,53.768,-166.234,53.784,-166.188,53.822,-166.155,53.836,-166.114,53.843,-166.103,53.833,-166.139,53.787,-166.184,53.757,-166.21,53.723]],[[-176.009,51.812,-176.093,51.79,-176.204,51.835,-176.194,51.886,-176.072,51.843,-176.009,51.812]],[[-166.11,66.227,-166.149,66.222,-166.146,66.237,-166.033,66.278,-165.822,66.328,-165.83,66.317,-165.942,66.278,-166.11,66.227]],[[-171.463,63.64,-171.448,63.616,-171.343,63.62,-171.197,63.609,-171.035,63.585,-170.875,63.594,-170.673,63.669,-170.552,63.688,-170.43,63.699,-170.299,63.681,-170.171,63.641,-170.122,63.618,-170.082,63.577,-170.056,63.527,-170.017,63.492,-169.777,63.448,-169.624,63.431,-169.587,63.407,-169.555,63.373,-169.428,63.348,-169.295,63.358,-169.221,63.349,-168.996,63.347,-168.716,63.311,-168.761,63.214,-168.852,63.171,-169.109,63.185,-169.365,63.171,-169.471,63.121,-169.559,63.058,-169.571,62.997,-169.623,62.969,-169.676,62.956,-169.72,62.99,-169.778,63.094,-169.819,63.122,-169.863,63.14,-169.988,63.173,-170.115,63.194,-170.19,63.196,-170.243,63.232,-170.273,63.284,-170.324,63.311,-170.424,63.349,-170.527,63.379,-170.848,63.444,-170.954,63.453,-171.061,63.446,-171.176,63.416,-171.287,63.372,-171.401,63.339,-171.519,63.332,-171.632,63.351,-171.738,63.394,-171.791,63.425,-171.819,63.477,-171.818,63.53,-171.804,63.581,-171.746,63.703,-171.646,63.727,-171.463,63.64]],[[-166.135,60.384,-166.044,60.334,-165.995,60.331,-165.841,60.346,-165.784,60.336,-165.73,60.314,-165.696,60.282,-165.689,60.224,-165.714,60.173,-165.707,60.101,-165.712,60.069,-165.631,60.028,-165.605,59.973,-165.592,59.913,-165.769,59.893,-165.947,59.89,-166.1,59.85,-166.131,59.82,-166.107,59.775,-166.149,59.764,-166.188,59.774,-166.262,59.815,-166.343,59.834,-166.628,59.865,-166.985,59.984,-167.139,60.009,-167.295,60.096,-167.436,60.207,-167.344,60.224,-167.252,60.234,-166.836,60.217,-166.784,60.296,-166.731,60.316,-166.599,60.339,-166.476,60.383,-166.42,60.382,-166.364,60.365,-166.247,60.391,-166.185,60.397,-166.135,60.384]],[[-152.898,57.824,-152.891,57.769,-152.85,57.776,-152.696,57.832,-152.616,57.849,-152.512,57.851,-152.429,57.826,-152.412,57.806,-152.419,57.782,-152.485,57.734,-152.483,57.703,-152.411,57.646,-152.237,57.615,-152.215,57.598,-152.216,57.577,-152.337,57.482,-152.381,57.46,-152.412,57.455,-152.631,57.472,-152.831,57.503,-152.912,57.508,-152.941,57.498,-152.997,57.469,-152.957,57.46,-152.781,57.453,-152.72,57.411,-152.693,57.38,-152.679,57.345,-152.714,57.331,-152.789,57.321,-152.879,57.321,-152.99,57.282,-153.052,57.238,-153.274,57.226,-153.444,57.167,-153.504,57.138,-153.524,57.103,-153.588,57.078,-153.733,57.052,-153.647,57.03,-153.633,57.01,-153.631,56.984,-153.643,56.961,-153.757,56.858,-153.973,56.774,-154.027,56.778,-154.051,56.788,-154.07,56.805,-154.071,56.821,-153.793,56.99,-153.804,56.998,-153.88,57.004,-153.999,57.05,-154.084,57.02,-154.103,57.021,-154.08,57.061,-154.025,57.108,-154.035,57.122,-154.065,57.134,-154.135,57.141,-154.244,57.143,-154.324,57.132,-154.377,57.107,-154.381,57.097,-154.27,57.099,-154.239,57.087,-154.209,57.063,-154.191,57.036,-154.184,57.005,-154.208,56.964,-154.261,56.912,-154.339,56.921,-154.499,57.037,-154.569,57.206,-154.706,57.335,-154.712,57.366,-154.673,57.446,-154.535,57.559,-154.387,57.59,-154.281,57.638,-154.179,57.652,-154.116,57.651,-154.03,57.631,-153.995,57.587,-154.016,57.567,-154.008,57.556,-153.947,57.53,-153.882,57.439,-153.805,57.358,-153.755,57.325,-153.688,57.305,-153.757,57.367,-153.798,57.443,-153.818,57.596,-153.838,57.636,-153.799,57.647,-153.69,57.641,-153.693,57.663,-153.808,57.715,-153.879,57.757,-153.906,57.791,-153.904,57.82,-153.842,57.863,-153.806,57.875,-153.769,57.88,-153.696,57.871,-153.663,57.858,-153.569,57.761,-153.524,57.731,-153.488,57.731,-153.454,57.747,-153.423,57.779,-153.39,57.798,-153.357,57.805,-153.252,57.79,-153.217,57.796,-153.2,57.82,-153.201,57.863,-153.175,57.879,-153.169,57.911,-153.226,57.958,-153.16,57.972,-152.943,57.936,-152.85,57.897,-152.898,57.824]],[[-130.025,55.888,-130.075,55.836,-130.112,55.78,-130.137,55.719,-130.147,55.654,-130.14,55.585,-130.12,55.524,-130.059,55.412,-130.039,55.344,-130.037,55.298,-130.172,55.137,-130.219,55.06,-130.214,55.026,-130.313,54.946,-130.493,54.834,-130.575,54.77,-130.616,54.791,-130.85,54.808,-130.935,54.95,-130.98,55.061,-131.048,55.158,-131.046,55.18,-130.984,55.244,-130.75,55.297,-130.748,55.318,-130.835,55.332,-130.856,55.355,-130.88,55.46,-130.873,55.551,-130.88,55.612,-130.919,55.736,-130.977,55.812,-131.128,55.96,-131.14,55.998,-131.074,56.044,-131.033,56.088,-131.288,56.012,-131.635,55.932,-131.784,55.877,-131.815,55.854,-131.826,55.835,-131.799,55.783,-131.803,55.766,-131.834,55.735,-131.869,55.647,-131.945,55.554,-131.983,55.535,-132.119,55.57,-132.155,55.6,-132.223,55.721,-132.208,55.753,-132.158,55.781,-132.091,55.84,-132.006,55.93,-131.844,56.16,-131.738,56.161,-131.551,56.207,-131.844,56.23,-131.888,56.242,-131.927,56.273,-131.962,56.324,-132.022,56.38,-132.133,56.4,-132.182,56.421,-132.256,56.489,-132.305,56.52,-132.332,56.558,-132.337,56.603,-132.358,56.626,-132.434,56.634,-132.476,56.65,-132.487,56.766,-132.64,56.796,-132.702,56.822,-132.802,56.895,-132.83,56.931,-132.839,56.96,-132.814,57.041,-132.825,57.056,-132.913,57.047,-133.466,57.172,-133.437,57.337,-133.539,57.554,-133.649,57.642,-133.627,57.677,-133.603,57.695,-133.554,57.695,-133.342,57.631,-133.143,57.555,-133.117,57.566,-133.436,57.727,-133.515,57.775,-133.535,57.833,-133.536,57.864,-133.511,57.88,-133.212,57.866,-133.194,57.878,-133.497,57.925,-133.559,57.924,-133.626,57.857,-133.657,57.841,-133.722,57.844,-133.744,57.855,-133.821,57.936,-133.894,57.993,-134.031,58.072,-134.057,58.128,-134.063,58.211,-134.045,58.289,-133.934,58.468,-133.889,58.499,-133.877,58.518,-133.911,58.515,-133.944,58.498,-134.036,58.415,-134.131,58.279,-134.209,58.233,-134.258,58.244,-134.331,58.3,-134.485,58.367,-134.664,58.385,-134.776,58.454,-134.943,58.646,-134.965,58.742,-134.986,58.766,-135.076,58.797,-135.132,58.843,-135.217,59.077,-135.33,59.239,-135.358,59.325,-135.349,59.41,-135.364,59.419,-135.403,59.353,-135.413,59.318,-135.484,59.309,-135.417,59.242,-135.4,59.208,-135.434,59.211,-135.502,59.202,-135.386,59.088,-135.334,58.91,-135.257,58.778,-135.207,58.671,-135.185,58.59,-135.152,58.512,-135.062,58.341,-135.05,58.307,-135.06,58.279,-135.09,58.246,-135.142,58.233,-135.303,58.256,-135.363,58.298,-135.45,58.376,-135.572,58.412,-135.873,58.394,-135.898,58.4,-135.896,58.464,-135.862,58.577,-135.89,58.623,-136.046,58.789,-136.043,58.822,-135.826,58.898,-135.932,58.904,-136.017,58.874,-136.049,58.893,-136.101,59,-136.134,59.04,-136.15,59.048,-136.159,58.947,-136.124,58.893,-136.118,58.863,-136.124,58.82,-136.147,58.789,-136.186,58.77,-136.226,58.765,-136.299,58.787,-136.38,58.827,-136.451,58.846,-136.478,58.863,-136.511,58.907,-136.566,58.941,-136.831,58.984,-136.989,59.034,-137.002,59.021,-136.953,58.967,-136.948,58.935,-136.988,58.925,-137.059,58.874,-137.038,58.867,-136.963,58.884,-136.879,58.882,-136.74,58.85,-136.614,58.809,-136.568,58.786,-136.549,58.752,-136.533,58.74,-136.41,58.701,-136.404,58.68,-136.484,58.618,-136.32,58.624,-136.225,58.602,-136.103,58.506,-136.061,58.453,-136.056,58.384,-136.081,58.364,-136.13,58.35,-136.462,58.328,-136.583,58.245,-136.607,58.244,-136.699,58.266,-136.865,58.332,-137.072,58.395,-137.544,58.581,-137.557,58.59,-137.565,58.626,-137.597,58.644,-137.661,58.66,-137.75,58.707,-137.864,58.786,-137.934,58.847,-137.961,58.891,-138.027,58.941,-138.241,59.047,-138.352,59.087,-138.451,59.11,-138.537,59.115,-138.56,59.129,-138.521,59.152,-138.515,59.166,-138.704,59.188,-138.884,59.237,-139.341,59.376,-139.577,59.462,-139.714,59.504,-139.773,59.527,-139.799,59.546,-139.766,59.566,-139.674,59.587,-139.612,59.61,-139.513,59.698,-139.506,59.726,-139.558,59.79,-139.582,59.848,-139.581,59.881,-139.569,59.912,-139.554,59.933,-139.512,59.954,-139.483,59.964,-139.447,59.957,-139.331,59.877,-139.315,59.848,-139.32,59.739,-139.287,59.611,-139.276,59.62,-139.266,59.663,-139.259,59.743,-139.246,59.782,-139.221,59.82,-139.179,59.84,-139.048,59.828,-138.988,59.835,-139.242,59.893,-139.402,60.001,-139.431,60.012,-139.519,60.017,-139.612,59.973,-139.85,59.831,-139.917,59.806,-140.217,59.727,-140.42,59.711,-140.648,59.723,-140.843,59.749,-141.332,59.874,-141.408,59.903,-141.295,59.98,-141.29,60.004,-141.33,60.083,-141.362,60.105,-141.409,60.118,-141.422,60.109,-141.422,60.085,-141.41,60.042,-141.447,60.019,-141.53,59.995,-141.67,59.97,-142.104,60.033,-142.549,60.086,-142.946,60.097,-143.506,60.055,-143.805,60.013,-143.979,60.009,-144.147,60.016,-144.161,60.046,-144.084,60.063,-144.089,60.084,-144.185,60.151,-144.333,60.191,-144.53,60.205,-144.643,60.225,-144.672,60.249,-144.741,60.273,-144.852,60.295,-144.901,60.335,-144.862,60.459,-144.824,60.534,-144.787,60.585,-144.691,60.669,-144.724,60.663,-144.863,60.601,-144.984,60.537,-145.096,60.454,-145.163,60.415,-145.248,60.38,-145.382,60.389,-145.563,60.441,-145.718,60.468,-145.848,60.469,-145.899,60.478,-145.811,60.525,-145.76,60.562,-145.69,60.622,-145.675,60.651,-146.149,60.661,-146.166,60.692,-146.167,60.716,-146.182,60.735,-146.251,60.749,-146.347,60.738,-146.503,60.701,-146.57,60.729,-146.546,60.745,-146.496,60.757,-146.392,60.811,-146.532,60.839,-146.604,60.871,-146.638,60.897,-146.636,60.993,-146.599,61.054,-146.285,61.113,-146.384,61.136,-146.583,61.128,-146.716,61.078,-146.874,61.005,-146.98,60.978,-147.034,60.996,-147.106,61.003,-147.195,60.997,-147.255,60.978,-147.286,60.947,-147.321,60.925,-147.361,60.915,-147.391,60.918,-147.433,60.95,-147.523,60.97,-147.567,60.995,-147.593,60.979,-147.623,60.933,-147.656,60.91,-147.808,60.885,-147.891,60.89,-147.991,60.948,-148.005,60.969,-147.971,61.019,-147.752,61.219,-147.774,61.218,-147.845,61.186,-147.986,61.106,-148.049,61.083,-148.158,61.08,-148.209,61.088,-148.27,61.082,-148.342,61.06,-148.389,61.037,-148.411,61.011,-148.396,61.007,-148.287,61.036,-148.226,61.044,-148.209,61.03,-148.293,60.94,-148.344,60.854,-148.393,60.832,-148.471,60.835,-148.556,60.827,-148.557,60.803,-148.399,60.734,-148.341,60.724,-148.268,60.7,-148.257,60.675,-148.284,60.609,-148.305,60.583,-148.338,60.57,-148.468,60.572,-148.51,60.565,-148.597,60.524,-148.64,60.489,-148.624,60.486,-148.549,60.515,-148.44,60.53,-148.296,60.532,-148.189,60.547,-148.119,60.575,-148.051,60.567,-147.984,60.523,-147.964,60.485,-147.991,60.452,-148.046,60.428,-148.129,60.414,-148.182,60.393,-148.204,60.365,-148.216,60.323,-148.219,60.268,-148.198,60.168,-148.214,60.154,-148.245,60.147,-148.291,60.145,-148.333,60.122,-148.431,59.989,-148.465,59.975,-148.506,59.989,-148.542,59.987,-148.574,59.97,-148.644,59.957,-148.751,59.948,-148.843,59.951,-149.004,59.98,-149.07,60,-149.122,60.033,-149.267,59.998,-149.305,60.014,-149.395,60.106,-149.415,60.1,-149.432,60.001,-149.46,59.966,-149.549,59.894,-149.598,59.77,-149.613,59.767,-149.63,59.785,-149.685,59.895,-149.714,59.92,-149.795,59.856,-149.804,59.833,-149.782,59.75,-149.801,59.738,-149.965,59.782,-150.005,59.784,-150.016,59.777,-149.96,59.713,-149.967,59.69,-150.198,59.567,-150.258,59.571,-150.296,59.583,-150.338,59.581,-150.485,59.535,-150.526,59.537,-150.582,59.565,-150.607,59.563,-150.621,59.535,-150.623,59.48,-150.677,59.427,-150.853,59.342,-150.899,59.303,-150.935,59.249,-150.961,59.244,-151.064,59.278,-151.183,59.301,-151.199,59.29,-151.163,59.257,-151.171,59.237,-151.222,59.229,-151.287,59.232,-151.366,59.246,-151.477,59.231,-151.619,59.187,-151.738,59.189,-151.904,59.26,-151.95,59.265,-151.964,59.285,-151.932,59.343,-151.885,59.386,-151.85,59.406,-151.693,59.462,-151.513,59.483,-151.4,59.516,-151.262,59.586,-151.189,59.638,-151.046,59.772,-151.057,59.782,-151.089,59.789,-151.404,59.662,-151.45,59.65,-151.513,59.651,-151.764,59.7,-151.817,59.721,-151.853,59.782,-151.783,59.921,-151.735,59.988,-151.612,60.092,-151.451,60.203,-151.396,60.274,-151.313,60.466,-151.318,60.554,-151.355,60.66,-151.356,60.723,-151.322,60.743,-150.954,60.841,-150.779,60.915,-150.441,61.024,-150.349,61.023,-150.281,60.985,-150.203,60.955,-150.113,60.933,-149.998,60.935,-149.856,60.962,-149.632,60.952,-149.173,60.88,-149.075,60.876,-149.071,60.886,-149.142,60.936,-149.459,60.965,-149.592,60.994,-149.968,61.122,-150.053,61.171,-150.019,61.194,-149.927,61.213,-149.895,61.232,-149.882,61.264,-149.829,61.308,-149.737,61.363,-149.596,61.417,-149.329,61.497,-149.434,61.501,-149.625,61.486,-149.695,61.471,-149.824,61.413,-149.874,61.373,-149.945,61.294,-149.976,61.279,-150.109,61.268,-150.472,61.26,-150.533,61.3,-150.567,61.307,-150.612,61.301,-150.946,61.198,-151.065,61.146,-151.15,61.086,-151.282,61.042,-151.46,61.014,-151.594,60.98,-151.734,60.911,-151.782,60.858,-151.784,60.833,-151.75,60.755,-151.785,60.74,-151.866,60.734,-151.996,60.682,-152.271,60.528,-152.307,60.472,-152.305,60.453,-152.26,60.409,-152.292,60.381,-152.369,60.336,-152.541,60.265,-152.654,60.238,-152.727,60.237,-152.798,60.247,-152.923,60.293,-153.025,60.296,-153.031,60.289,-152.893,60.24,-152.752,60.177,-152.665,60.125,-152.63,60.084,-152.629,60.041,-152.66,59.997,-152.759,59.921,-152.857,59.898,-153.106,59.875,-153.186,59.857,-153.211,59.843,-153.04,59.81,-153.025,59.794,-153.048,59.73,-153.094,59.709,-153.236,59.671,-153.364,59.66,-153.383,59.667,-153.36,59.717,-153.366,59.73,-153.414,59.74,-153.483,59.721,-153.653,59.647,-153.671,59.635,-153.609,59.615,-153.622,59.598,-153.714,59.545,-153.753,59.51,-153.814,59.474,-154.088,59.363,-154.067,59.336,-154.139,59.24,-154.178,59.156,-154.13,59.12,-153.9,59.078,-153.788,59.068,-153.656,59.039,-153.418,58.96,-153.339,58.909,-153.327,58.884,-153.334,58.858,-153.363,58.822,-153.438,58.755,-153.617,58.655,-153.699,58.626,-153.821,58.604,-153.862,58.588,-154.02,58.493,-154.062,58.442,-154.056,58.397,-154.086,58.366,-154.289,58.304,-154.282,58.293,-154.208,58.289,-154.235,58.235,-154.247,58.159,-154.282,58.147,-154.409,58.147,-154.571,58.118,-154.582,58.11,-154.585,58.056,-155.007,58.016,-155.099,57.913,-155.147,57.882,-155.313,57.807,-155.414,57.777,-155.53,57.759,-155.59,57.734,-155.596,57.701,-155.629,57.673,-155.729,57.627,-155.778,57.568,-155.814,57.559,-156,57.545,-156.037,57.527,-156.055,57.448,-156.09,57.445,-156.156,57.463,-156.242,57.449,-156.436,57.36,-156.478,57.328,-156.474,57.311,-156.444,57.294,-156.398,57.241,-156.4,57.205,-156.475,57.105,-156.501,57.09,-156.592,57.065,-156.629,57.01,-156.713,57.016,-156.78,57.006,-156.824,56.969,-156.872,56.948,-156.923,56.942,-156.988,56.913,-157.067,56.86,-157.139,56.827,-157.206,56.812,-157.271,56.808,-157.334,56.816,-157.39,56.81,-157.441,56.79,-157.49,56.76,-157.529,56.673,-157.578,56.634,-157.61,56.628,-157.674,56.633,-157.771,56.652,-157.869,56.645,-158.028,56.592,-158.078,56.552,-157.978,56.543,-157.929,56.532,-157.93,56.52,-157.982,56.51,-158.071,56.51,-158.124,56.501,-158.189,56.478,-158.352,56.454,-158.414,56.436,-158.537,56.335,-158.552,56.313,-158.536,56.308,-158.467,56.318,-158.386,56.302,-158.344,56.28,-158.317,56.254,-158.291,56.204,-158.276,56.196,-158.432,56.111,-158.476,56.075,-158.505,56.062,-158.523,56.072,-158.543,56.167,-158.554,56.183,-158.591,56.185,-158.627,56.155,-158.705,56.043,-158.79,55.987,-159.429,55.843,-159.523,55.81,-159.541,55.748,-159.568,55.695,-159.61,55.653,-159.66,55.626,-159.67,55.645,-159.665,55.795,-159.679,55.825,-159.743,55.844,-159.771,55.841,-159.81,55.833,-159.874,55.8,-159.914,55.792,-159.962,55.795,-160.046,55.763,-160.244,55.661,-160.373,55.635,-160.407,55.614,-160.463,55.558,-160.499,55.537,-160.554,55.535,-160.625,55.552,-160.683,55.54,-160.727,55.5,-160.771,55.484,-160.897,55.514,-160.952,55.493,-161.024,55.44,-161.1,55.406,-161.178,55.389,-161.382,55.371,-161.464,55.383,-161.481,55.398,-161.477,55.465,-161.444,55.513,-161.413,55.536,-161.373,55.556,-161.313,55.559,-161.202,55.544,-161.215,55.56,-161.255,55.579,-161.357,55.612,-161.459,55.629,-161.517,55.618,-161.599,55.593,-161.654,55.563,-161.684,55.53,-161.72,55.421,-161.742,55.391,-161.98,55.199,-162.074,55.139,-162.167,55.144,-162.211,55.121,-162.275,55.073,-162.333,55.05,-162.386,55.052,-162.428,55.061,-162.457,55.078,-162.452,55.093,-162.413,55.107,-162.427,55.145,-162.495,55.208,-162.542,55.243,-162.63,55.247,-162.644,55.218,-162.614,55.071,-162.619,55.038,-162.674,54.997,-162.82,54.95,-162.865,54.955,-162.996,55.046,-163.12,55.065,-163.128,55.035,-163.1,54.974,-163.131,54.917,-163.221,54.863,-163.289,54.838,-163.335,54.839,-163.338,54.876,-163.296,54.949,-163.286,55.01,-163.306,55.059,-163.304,55.096,-163.279,55.122,-163.115,55.194,-163.045,55.205,-163.008,55.187,-162.962,55.184,-162.907,55.196,-162.872,55.219,-162.857,55.253,-162.786,55.297,-162.659,55.351,-162.513,55.45,-162.349,55.595,-162.157,55.719,-161.937,55.824,-161.697,55.907,-161.216,56.021,-161.179,56.014,-161.223,55.977,-161.193,55.954,-161.145,55.951,-160.969,55.97,-160.899,55.994,-160.878,55.971,-160.902,55.941,-161.008,55.912,-161.005,55.887,-160.851,55.772,-160.803,55.754,-160.763,55.757,-160.746,55.771,-160.758,55.855,-160.706,55.87,-160.6,55.874,-160.53,55.863,-160.498,55.838,-160.437,55.817,-160.347,55.8,-160.292,55.805,-160.27,55.832,-160.308,55.864,-160.48,55.935,-160.527,55.965,-160.539,56.006,-160.515,56.059,-160.461,56.138,-160.377,56.241,-160.302,56.314,-160.149,56.396,-160.046,56.437,-159.785,56.562,-159.283,56.689,-159.159,56.77,-158.99,56.86,-158.918,56.882,-158.918,56.847,-158.895,56.816,-158.782,56.796,-158.709,56.789,-158.675,56.795,-158.666,56.828,-158.681,56.888,-158.685,56.944,-158.677,56.997,-158.661,57.039,-158.586,57.114,-158.474,57.199,-158.321,57.298,-158.225,57.343,-158.134,57.366,-158.046,57.409,-157.894,57.511,-157.846,57.528,-157.737,57.548,-157.697,57.539,-157.674,57.514,-157.646,57.498,-157.535,57.483,-157.462,57.506,-157.474,57.518,-157.533,57.526,-157.572,57.541,-157.608,57.601,-157.681,57.638,-157.697,57.679,-157.684,57.744,-157.621,57.895,-157.611,58.051,-157.555,58.14,-157.443,58.172,-157.194,58.194,-157.339,58.235,-157.394,58.235,-157.488,58.254,-157.524,58.351,-157.524,58.421,-157.461,58.503,-157.229,58.641,-156.975,58.736,-157.009,58.744,-157.04,58.773,-156.923,58.964,-156.809,59.134,-156.963,58.989,-157.142,58.878,-157.666,58.748,-158.022,58.64,-158.191,58.614,-158.303,58.642,-158.39,58.746,-158.439,58.783,-158.503,58.85,-158.476,58.938,-158.426,58.999,-158.315,59.009,-158.189,58.98,-158.081,58.977,-158.221,59.038,-158.423,59.09,-158.514,59.073,-158.584,58.988,-158.678,58.929,-158.761,58.95,-158.809,58.974,-158.776,58.903,-158.838,58.794,-158.861,58.719,-158.772,58.52,-158.789,58.441,-158.951,58.405,-159.083,58.47,-159.358,58.721,-159.454,58.793,-159.67,58.911,-159.741,58.894,-159.832,58.836,-159.92,58.82,-160.153,58.906,-160.261,58.972,-160.363,59.051,-160.52,59.007,-160.657,58.955,-160.817,58.872,-160.924,58.872,-161.216,58.801,-161.247,58.799,-161.288,58.761,-161.328,58.744,-161.361,58.67,-161.755,58.612,-162.145,58.644,-162.009,58.685,-161.856,58.717,-161.724,58.794,-161.781,58.897,-161.79,58.95,-161.789,59.016,-161.644,59.11,-161.794,59.109,-161.891,59.076,-161.981,59.146,-162.023,59.284,-161.92,59.365,-161.872,59.428,-161.832,59.515,-161.829,59.589,-161.909,59.714,-162.138,59.98,-162.242,60.178,-162.421,60.284,-162.288,60.457,-162.139,60.614,-161.947,60.685,-161.962,60.695,-162.068,60.695,-162.138,60.686,-162.2,60.634,-162.265,60.595,-162.469,60.395,-162.6,60.297,-162.685,60.269,-162.548,60.231,-162.527,60.199,-162.5,60.127,-162.536,60.038,-162.571,59.99,-162.733,59.994,-162.878,59.923,-163.219,59.846,-163.68,59.802,-163.907,59.807,-164.143,59.897,-164.141,59.949,-164.132,59.994,-164.471,60.149,-164.662,60.304,-164.8,60.307,-164.92,60.348,-165.061,60.413,-165.049,60.464,-165.027,60.501,-165.113,60.526,-165.225,60.524,-165.354,60.541,-165.016,60.74,-164.9,60.873,-164.805,60.892,-164.682,60.872,-164.513,60.819,-164.37,60.796,-164.319,60.771,-164.266,60.725,-164.321,60.647,-164.372,60.592,-164.31,60.607,-164.132,60.692,-164,60.766,-163.936,60.758,-163.895,60.745,-163.821,60.668,-163.73,60.59,-163.529,60.665,-163.421,60.757,-163.512,60.798,-163.623,60.822,-163.907,60.854,-163.837,60.88,-163.655,60.877,-163.587,60.903,-163.659,60.938,-163.749,60.97,-163.995,60.865,-164.442,60.87,-164.754,60.931,-165.066,60.921,-165.115,60.933,-165.175,60.966,-165,61.044,-164.876,61.087,-164.869,61.112,-164.941,61.115,-165.077,61.094,-165.138,61.13,-165.128,61.192,-165.15,61.187,-165.204,61.153,-165.28,61.17,-165.345,61.198,-165.311,61.228,-165.244,61.269,-165.274,61.275,-165.334,61.266,-165.392,61.212,-165.379,61.169,-165.381,61.106,-165.48,61.095,-165.566,61.102,-165.628,61.165,-165.691,61.3,-165.864,61.336,-165.906,61.404,-165.797,61.491,-165.845,61.536,-165.961,61.551,-166.094,61.507,-166.153,61.546,-166.164,61.589,-166.168,61.651,-166.131,61.657,-166.1,61.645,-165.835,61.679,-165.809,61.696,-166.02,61.748,-166.079,61.803,-165.991,61.834,-165.834,61.837,-165.613,61.869,-165.706,61.927,-165.725,61.959,-165.744,62.012,-165.707,62.1,-165.448,62.304,-165.195,62.474,-165.116,62.513,-165,62.534,-164.892,62.518,-164.779,62.481,-164.758,62.497,-164.796,62.512,-164.844,62.581,-164.688,62.608,-164.596,62.687,-164.589,62.709,-164.689,62.677,-164.793,62.623,-164.819,62.677,-164.845,62.801,-164.8,62.918,-164.764,62.971,-164.677,63.02,-164.428,63.04,-164.384,63.03,-164.375,63.054,-164.525,63.128,-164.463,63.185,-164.409,63.215,-164.108,63.262,-163.943,63.247,-163.736,63.193,-163.616,63.125,-163.634,63.09,-163.664,63.07,-163.726,63.048,-163.749,63.03,-163.738,63.016,-163.649,63.057,-163.504,63.106,-163.423,63.085,-163.359,63.046,-163.288,63.046,-163.062,63.08,-162.948,63.115,-162.808,63.207,-162.621,63.266,-162.36,63.453,-162.283,63.529,-162.193,63.541,-162.112,63.534,-162.056,63.471,-161.974,63.453,-161.505,63.468,-161.266,63.497,-161.1,63.558,-160.927,63.661,-160.827,63.729,-160.779,63.819,-160.84,63.935,-160.904,64.031,-160.988,64.251,-161.22,64.397,-161.386,64.44,-161.491,64.434,-161.415,64.526,-161.193,64.516,-161.049,64.534,-160.932,64.579,-160.894,64.613,-160.836,64.682,-160.856,64.756,-160.887,64.796,-160.967,64.84,-161.063,64.904,-161.13,64.925,-161.187,64.924,-161.466,64.795,-161.634,64.792,-161.759,64.816,-161.868,64.743,-162.172,64.678,-162.335,64.613,-162.636,64.451,-162.711,64.378,-162.807,64.374,-162.876,64.516,-163.204,64.652,-163.303,64.606,-163.248,64.563,-163.174,64.533,-163.052,64.52,-163.104,64.479,-163.144,64.424,-163.267,64.475,-163.486,64.55,-163.713,64.588,-164.304,64.584,-164.692,64.507,-164.727,64.523,-164.765,64.53,-164.83,64.511,-164.857,64.48,-164.9,64.461,-164.979,64.454,-165.138,64.465,-165.446,64.513,-166.143,64.583,-166.325,64.626,-166.481,64.728,-166.478,64.798,-166.409,64.827,-166.415,64.927,-166.551,64.953,-166.827,65.096,-166.928,65.157,-166.906,65.164,-166.857,65.147,-166.763,65.135,-166.531,65.155,-166.452,65.247,-166.28,65.274,-166.121,65.261,-166.157,65.286,-166.197,65.306,-166.609,65.353,-166.665,65.338,-167.404,65.422,-167.987,65.568,-168.035,65.596,-168.088,65.658,-168.01,65.719,-167.931,65.748,-167.927,65.714,-167.914,65.681,-167.58,65.758,-167.405,65.859,-167.074,65.877,-166.997,65.905,-166.894,65.959,-166.748,66.052,-166.54,66.101,-166.399,66.144,-166.215,66.17,-166.057,66.127,-166.009,66.121,-165.724,66.113,-165.63,66.131,-165.59,66.145,-165.56,66.167,-165.84,66.245,-165.812,66.288,-165.776,66.319,-165.449,66.41,-165.198,66.44,-165.064,66.438,-164.674,66.555,-164.46,66.588,-164.058,66.611,-163.728,66.616,-163.638,66.575,-163.816,66.583,-163.894,66.576,-163.838,66.562,-163.775,66.531,-163.794,66.493,-163.903,66.378,-163.894,66.287,-163.965,66.257,-164.034,66.216,-163.695,66.084,-163.171,66.075,-162.886,66.099,-162.722,66.06,-162.587,66.051,-162.214,66.071,-161.934,66.043,-161.816,66.054,-161.557,66.251,-161.455,66.281,-161.345,66.247,-161.201,66.219,-161.109,66.24,-161.034,66.189,-161.07,66.295,-161.12,66.334,-161.544,66.407,-161.828,66.371,-161.917,66.412,-161.888,66.493,-162.191,66.693,-162.318,66.734,-162.467,66.736,-162.544,66.805,-162.607,66.894,-162.478,66.931,-162.362,66.947,-162.254,66.919,-162.131,66.801,-162.018,66.784,-162.051,66.667,-161.91,66.56,-161.591,66.46,-161.336,66.496,-161.156,66.495,-161.048,66.474,-160.784,66.384,-160.651,66.373,-160.232,66.42,-160.227,66.509,-160.263,66.572,-160.361,66.613,-160.644,66.605,-160.864,66.671,-161.051,66.653,-161.398,66.552,-161.572,66.592,-161.681,66.646,-161.857,66.7,-161.879,66.804,-161.731,66.923,-161.622,66.979,-161.72,67.021,-161.965,67.05,-162.392,67.02,-162.412,67.06,-162.409,67.104,-162.583,67.019,-162.761,67.036,-163.002,67.027,-163.532,67.103,-163.721,67.196,-163.8,67.271,-163.943,67.478,-164.125,67.607,-165.386,68.046,-165.96,68.156,-166.236,68.278,-166.409,68.308,-166.574,68.32,-166.786,68.36,-166.644,68.408,-166.546,68.424,-166.648,68.374,-166.57,68.361,-166.447,68.39,-166.381,68.425,-166.283,68.573,-166.182,68.797,-166.209,68.885,-165.509,68.868,-165.044,68.882,-164.89,68.902,-164.302,68.936,-164.15,68.961,-163.868,69.037,-163.536,69.17,-163.251,69.345,-163.205,69.393,-163.187,69.38,-163.161,69.388,-163.131,69.454,-163.094,69.611,-162.952,69.758,-162.35,70.094,-162.071,70.227,-161.978,70.288,-161.881,70.332,-161.813,70.29,-161.78,70.277,-161.761,70.258,-161.818,70.248,-161.912,70.205,-162.042,70.177,-162.074,70.162,-161.997,70.165,-161.768,70.197,-161.639,70.235,-160.996,70.305,-160.648,70.421,-160.634,70.446,-160.117,70.591,-160.046,70.586,-159.963,70.568,-160.106,70.473,-160.006,70.448,-160.095,70.333,-159.908,70.331,-159.866,70.279,-159.855,70.324,-159.858,70.389,-159.843,70.453,-159.815,70.497,-159.683,70.477,-159.387,70.525,-159.746,70.53,-159.962,70.634,-160.082,70.635,-159.681,70.787,-159.315,70.879,-159.232,70.877,-159.192,70.86,-159.183,70.832,-159.262,70.814,-159.34,70.781,-159.304,70.753,-159.251,70.748,-159.075,70.772,-158.996,70.802,-158.621,70.799,-158.511,70.82,-158.484,70.841,-157.998,70.845,-157.909,70.86,-157.606,70.941,-157.325,71.04,-157.195,71.093,-156.973,71.23,-156.783,71.319,-156.47,71.408,-156.395,71.397,-156.497,71.379,-156.567,71.342,-156.47,71.292,-155.811,71.188,-155.646,71.183,-155.579,71.121,-155.635,71.062,-155.804,70.995,-156.147,70.928,-156.042,70.902,-155.974,70.842,-155.872,70.835,-155.708,70.857,-155.579,70.894,-155.313,71.015,-155.23,71.082,-155.167,71.099,-154.944,71.083,-154.818,71.048,-154.674,70.987,-154.726,70.928,-154.785,70.894,-154.599,70.848,-154.392,70.838,-154.195,70.801,-153.918,70.877,-153.701,70.894,-153.498,70.891,-153.233,70.933,-152.785,70.876,-152.671,70.891,-152.491,70.881,-152.3,70.847,-152.233,70.81,-152.437,70.733,-152.471,70.654,-152.399,70.62,-152.27,70.615,-152.253,70.568,-152.173,70.557,-151.769,70.56,-151.8,70.538,-151.82,70.511,-151.945,70.452,-151.225,70.419,-151.128,70.452,-150.979,70.465,-150.663,70.51,-150.544,70.49,-150.403,70.444,-150.274,70.434,-150.152,70.444,-149.87,70.51,-149.544,70.513,-149.411,70.491,-149.269,70.501,-148.845,70.425,-148.688,70.416,-148.479,70.318,-148.371,70.315,-148.249,70.357,-148.143,70.355,-148.039,70.315,-147.87,70.303,-147.791,70.24,-147.705,70.217,-147.063,70.17,-146.745,70.192,-146.281,70.186,-146.058,70.156,-145.823,70.16,-145.44,70.051,-145.237,70.034,-145.197,70.009,-144.619,69.982,-144.417,70.039,-144.064,70.054,-143.746,70.102,-143.566,70.101,-143.357,70.09,-143.276,70.095,-143.218,70.116,-142.708,70.034,-142.422,69.94,-142.297,69.87,-141.699,69.77,-141.526,69.715,-141.408,69.653,-141.339,69.647,-141.29,69.665,-141.081,69.659,-141.002,69.651,-141.002,69.359,-141.002,69.066,-141.002,68.774,-141.002,68.482,-141.002,68.19,-141.002,67.898,-141.002,67.605,-141.002,67.313,-141.002,67.021,-141.002,66.729,-141.002,66.437,-141.002,66.144,-141.002,65.852,-141.002,65.56,-141.002,65.268,-141.002,64.976,-141.002,64.683,-141.002,64.391,-141.002,64.099,-141.002,63.807,-141.002,63.514,-141.002,63.222,-141.002,62.93,-141.002,62.638,-141.002,62.346,-141.002,62.053,-141.002,61.761,-141.002,61.469,-141.002,61.177,-141.002,60.885,-141.002,60.592,-141.002,60.3,-140.763,60.259,-140.525,60.218,-140.453,60.3,-140.197,60.238,-139.973,60.183,-139.831,60.253,-139.676,60.328,-139.468,60.334,-139.235,60.34,-139.079,60.344,-139.079,60.279,-139.137,60.173,-139.185,60.084,-139.043,59.993,-138.869,59.946,-138.705,59.901,-138.632,59.778,-138.454,59.683,-138.318,59.611,-138.187,59.542,-138.001,59.443,-137.871,59.374,-137.697,59.281,-137.593,59.226,-137.544,59.119,-137.484,58.991,-137.521,58.915,-137.439,58.903,-137.278,58.988,-137.126,59.041,-136.939,59.106,-136.813,59.15,-136.579,59.152,-136.467,59.28,-136.466,59.459,-136.348,59.456,-136.278,59.48,-136.247,59.533,-136.322,59.605,-136.097,59.638,-135.935,59.663,-135.703,59.729,-135.476,59.793,-135.368,59.743,-135.261,59.695,-135.051,59.579,-135.037,59.551,-135.051,59.496,-135.071,59.441,-134.944,59.288,-134.907,59.271,-134.802,59.25,-134.677,59.199,-134.622,59.155,-134.441,59.085,-134.41,59.056,-134.393,59.009,-134.364,58.969,-134.33,58.94,-134.297,58.898,-134.219,58.85,-134.069,58.796,-133.966,58.758,-133.821,58.705,-133.674,58.597,-133.546,58.503,-133.401,58.411,-133.423,58.337,-133.275,58.223,-133.12,58.078,-133.001,57.949,-132.917,57.877,-132.816,57.773,-132.692,57.645,-132.55,57.5,-132.442,57.407,-132.302,57.276,-132.232,57.199,-132.279,57.145,-132.338,57.079,-132.157,57.048,-132.032,57.027,-132.063,56.953,-132.104,56.857,-131.962,56.819,-131.866,56.793,-131.886,56.742,-131.833,56.685,-131.824,56.59,-131.652,56.596,-131.575,56.599,-131.472,56.557,-131.336,56.501,-131.199,56.449,-131.083,56.405,-130.93,56.379,-130.742,56.341,-130.649,56.264,-130.477,56.231,-130.413,56.123,-130.215,56.083,-130.098,56.109,-130.056,56.065,-130.023,56.015,-130.014,55.951,-130.025,55.888]],[[-163.476,54.981,-163.379,54.816,-163.337,54.783,-163.275,54.766,-163.187,54.748,-163.135,54.723,-163.089,54.686,-163.083,54.669,-163.358,54.736,-163.531,54.638,-163.583,54.626,-164.073,54.621,-164.171,54.603,-164.235,54.571,-164.347,54.482,-164.404,54.448,-164.463,54.427,-164.591,54.404,-164.744,54.407,-164.823,54.419,-164.866,54.461,-164.904,54.545,-164.904,54.568,-164.888,54.608,-164.751,54.663,-164.706,54.692,-164.53,54.881,-164.479,54.907,-164.424,54.913,-164.274,54.9,-164.145,54.955,-163.868,55.039,-163.807,55.049,-163.607,55.051,-163.553,55.038,-163.511,55.014,-163.476,54.981]],[[-133.305,55.544,-133.283,55.516,-133.282,55.498,-133.426,55.431,-133.429,55.418,-133.463,55.377,-133.493,55.362,-133.547,55.317,-133.65,55.269,-133.635,55.413,-133.737,55.497,-133.634,55.539,-133.567,55.527,-133.455,55.522,-133.346,55.559,-133.305,55.544]],[[-131.34,55.08,-131.237,54.95,-131.232,54.904,-131.33,54.888,-131.406,54.894,-131.446,54.909,-131.456,54.931,-131.431,54.996,-131.482,55.035,-131.54,55.048,-131.592,55.026,-131.595,55.091,-131.556,55.137,-131.578,55.201,-131.578,55.249,-131.565,55.264,-131.513,55.263,-131.405,55.213,-131.34,55.08]],[[-132.112,56.109,-132.133,55.943,-132.173,55.953,-132.21,55.953,-132.287,55.929,-132.369,55.94,-132.407,55.958,-132.421,55.98,-132.406,56.029,-132.451,56.056,-132.603,56.066,-132.66,56.078,-132.691,56.13,-132.699,56.198,-132.675,56.224,-132.599,56.242,-132.539,56.324,-132.506,56.335,-132.38,56.499,-132.317,56.488,-132.206,56.388,-132.067,56.244,-132.112,56.109]],[[-130.979,55.489,-131.014,55.379,-131.083,55.267,-131.188,55.206,-131.262,55.22,-131.316,55.269,-131.367,55.266,-131.421,55.276,-131.451,55.316,-131.422,55.368,-131.448,55.409,-131.475,55.373,-131.522,55.341,-131.641,55.299,-131.724,55.218,-131.762,55.166,-131.811,55.223,-131.842,55.359,-131.846,55.416,-131.759,55.503,-131.648,55.586,-131.625,55.832,-131.269,55.955,-131.236,55.949,-131.121,55.857,-130.998,55.728,-130.966,55.67,-130.965,55.568,-130.979,55.489]],[[-133.366,57.004,-133.3,56.972,-133.264,57.005,-133.196,57.003,-133.071,56.974,-132.996,56.93,-132.954,56.88,-132.951,56.85,-132.963,56.783,-132.954,56.713,-132.959,56.677,-132.976,56.647,-133.004,56.624,-133.035,56.621,-133.132,56.683,-133.244,56.796,-133.329,56.83,-133.332,56.819,-133.309,56.786,-133.24,56.726,-133.227,56.689,-133.178,56.645,-133.157,56.611,-133.144,56.567,-133.145,56.528,-133.158,56.495,-133.181,56.474,-133.213,56.465,-133.383,56.474,-133.484,56.452,-133.603,56.464,-133.631,56.484,-133.649,56.517,-133.658,56.596,-133.688,56.71,-133.681,56.798,-133.758,56.877,-133.823,56.924,-133.917,56.967,-133.979,57.01,-133.962,57.043,-133.866,57.069,-133.708,57.063,-133.366,57.004]],[[-132.862,54.894,-132.838,54.881,-132.813,54.89,-132.772,54.926,-132.701,54.919,-132.649,54.907,-132.617,54.892,-132.634,54.84,-132.647,54.756,-132.677,54.726,-132.706,54.684,-132.807,54.709,-132.89,54.763,-133.009,54.855,-133.075,54.921,-133.081,54.949,-133.123,54.97,-133.205,55.084,-133.251,55.175,-133.325,55.185,-133.418,55.211,-133.454,55.26,-133.429,55.304,-133.297,55.326,-133.097,55.214,-133.067,55.166,-132.996,55.111,-132.982,55.033,-132.946,55.003,-132.862,54.894]],[[-146.394,60.45,-146.372,60.422,-146.18,60.429,-146.124,60.424,-146.102,60.411,-146.128,60.393,-146.202,60.368,-146.419,60.325,-146.595,60.268,-146.618,60.274,-146.65,60.336,-146.683,60.361,-146.703,60.396,-146.703,60.409,-146.67,60.433,-146.606,60.468,-146.56,60.481,-146.394,60.45]],[[-147.736,59.813,-147.846,59.799,-147.872,59.828,-147.814,59.902,-147.768,59.944,-147.734,59.954,-147.607,60.037,-147.466,60.097,-147.337,60.185,-147.205,60.311,-147.181,60.358,-147.12,60.363,-147.02,60.332,-146.958,60.289,-146.987,60.254,-147.318,60.075,-147.346,60.052,-147.377,59.991,-147.404,59.97,-147.448,59.96,-147.479,59.934,-147.499,59.89,-147.54,59.868,-147.602,59.866,-147.645,59.854,-147.669,59.832,-147.736,59.813]],[[-147.658,60.45,-147.659,60.424,-147.69,60.399,-147.66,60.352,-147.712,60.273,-147.732,60.222,-147.76,60.19,-147.788,60.178,-147.816,60.185,-147.822,60.203,-147.805,60.231,-147.871,60.23,-147.891,60.299,-147.855,60.321,-147.842,60.351,-147.838,60.371,-147.795,60.46,-147.779,60.466,-147.774,60.445,-147.76,60.439,-147.737,60.447,-147.703,60.487,-147.689,60.491,-147.658,60.45]],[[-147.931,60.826,-148.057,60.818,-148.115,60.831,-148.124,60.844,-148.1,60.895,-148.102,60.916,-148.038,60.924,-147.964,60.9,-147.943,60.875,-147.931,60.826]],[[-153.007,57.125,-153.134,57.093,-153.157,57.094,-153.235,57.029,-153.295,57,-153.375,57.052,-153.354,57.132,-153.285,57.185,-152.935,57.167,-152.908,57.152,-152.908,57.14,-152.933,57.129,-153.007,57.125]],[[-152.486,58.485,-152.516,58.479,-152.589,58.509,-152.637,58.542,-152.605,58.566,-152.463,58.619,-152.396,58.619,-152.368,58.611,-152.357,58.595,-152.362,58.571,-152.393,58.541,-152.486,58.485]],[[-153.241,57.85,-153.269,57.822,-153.295,57.829,-153.351,57.862,-153.465,57.909,-153.517,57.942,-153.52,57.956,-153.481,57.971,-153.347,57.933,-153.29,57.898,-153.241,57.85]],[[-155.566,55.821,-155.605,55.79,-155.681,55.792,-155.723,55.802,-155.737,55.83,-155.621,55.913,-155.594,55.924,-155.573,55.921,-155.564,55.887,-155.566,55.821]],[[-154.683,56.436,-154.751,56.412,-154.774,56.42,-154.777,56.44,-154.761,56.471,-154.729,56.502,-154.624,56.561,-154.518,56.601,-154.463,56.598,-154.445,56.573,-154.511,56.521,-154.683,56.436]],[[-154.209,56.515,-154.258,56.513,-154.332,56.539,-154.322,56.571,-154.217,56.609,-154.11,56.603,-154.102,56.582,-154.107,56.558,-154.116,56.544,-154.15,56.53,-154.209,56.515]],[[-160.685,55.315,-160.67,55.314,-160.639,55.322,-160.574,55.378,-160.553,55.381,-160.552,55.363,-160.583,55.308,-160.531,55.233,-160.483,55.197,-160.488,55.185,-160.609,55.159,-160.702,55.178,-160.751,55.171,-160.795,55.145,-160.825,55.174,-160.847,55.311,-160.84,55.335,-160.789,55.383,-160.724,55.405,-160.696,55.398,-160.672,55.379,-160.666,55.359,-160.685,55.315]],[[-162.298,54.847,-162.322,54.842,-162.391,54.873,-162.416,54.896,-162.434,54.932,-162.294,54.983,-162.265,54.983,-162.238,54.955,-162.234,54.932,-162.273,54.867,-162.298,54.847]],[[-162.554,54.401,-162.641,54.38,-162.733,54.402,-162.812,54.444,-162.821,54.495,-162.645,54.462,-162.608,54.447,-162.554,54.401]],[[-159.873,55.129,-159.934,55.107,-159.953,55.079,-159.999,55.067,-160.038,55.044,-160.17,54.942,-160.227,54.923,-160.164,55.01,-160.154,55.038,-160.152,55.057,-160.172,55.123,-160.134,55.12,-160.102,55.134,-160.039,55.193,-159.982,55.198,-159.92,55.268,-159.887,55.273,-159.871,55.264,-159.898,55.221,-159.839,55.182,-159.854,55.145,-159.873,55.129]],[[-165.842,54.071,-165.879,54.053,-165.91,54.049,-165.933,54.059,-166.036,54.047,-166.057,54.054,-166.103,54.114,-166.106,54.145,-166.088,54.169,-166.041,54.191,-165.966,54.211,-165.893,54.207,-165.764,54.152,-165.704,54.12,-165.693,54.1,-165.738,54.081,-165.842,54.071]],[[-165.561,54.137,-165.605,54.129,-165.615,54.14,-165.621,54.184,-165.654,54.253,-165.59,54.279,-165.551,54.285,-165.534,54.274,-165.488,54.222,-165.442,54.208,-165.408,54.197,-165.468,54.181,-165.561,54.137]],[[-160.919,58.577,-160.992,58.561,-161.07,58.569,-161.131,58.668,-161.085,58.671,-160.986,58.736,-160.769,58.789,-160.715,58.795,-160.919,58.577]],[[-172.742,60.457,-172.526,60.392,-172.387,60.398,-172.278,60.344,-172.232,60.299,-172.397,60.331,-172.636,60.329,-172.958,60.463,-173.074,60.493,-173.048,60.568,-172.924,60.607,-172.86,60.506,-172.742,60.457]],[[-170.161,57.184,-170.264,57.137,-170.358,57.154,-170.386,57.189,-170.387,57.203,-170.116,57.242,-170.161,57.184]],[[-169.692,52.847,-169.708,52.807,-169.723,52.792,-169.877,52.814,-169.981,52.806,-169.992,52.83,-169.983,52.851,-169.821,52.883,-169.755,52.884,-169.711,52.867,-169.692,52.847]],[[-170.733,52.581,-170.797,52.55,-170.816,52.562,-170.827,52.601,-170.791,52.631,-170.682,52.698,-170.608,52.685,-170.585,52.668,-170.587,52.642,-170.614,52.61,-170.649,52.593,-170.692,52.593,-170.733,52.581]],[[-172.465,52.272,-172.539,52.257,-172.62,52.273,-172.582,52.326,-172.544,52.354,-172.47,52.388,-172.383,52.373,-172.314,52.33,-172.465,52.272]],[[-169.755,56.635,-169.624,56.615,-169.55,56.628,-169.486,56.618,-169.474,56.594,-169.587,56.542,-169.633,56.546,-169.766,56.608,-169.755,56.635]],[[-176.287,51.792,-176.35,51.733,-176.396,51.76,-176.414,51.841,-176.379,51.861,-176.28,51.803,-176.287,51.792]],[[-176.022,52.002,-176.045,51.973,-176.143,52.004,-176.178,52.03,-176.185,52.056,-176.156,52.099,-176.077,52.1,-176.031,52.082,-175.988,52.049,-175.975,52.029,-176.022,52.002]],[[-177.148,51.717,-177.177,51.704,-177.23,51.694,-177.382,51.705,-177.475,51.701,-177.578,51.694,-177.655,51.677,-177.67,51.701,-177.668,51.721,-177.335,51.776,-177.257,51.805,-177.21,51.841,-177.166,51.909,-177.131,51.93,-177.11,51.929,-177.063,51.902,-177.08,51.867,-177.121,51.836,-177.135,51.807,-177.148,51.717]],[[178.575,51.916,178.512,51.899,178.478,51.943,178.475,51.968,178.509,51.995,178.571,51.978,178.607,51.953,178.575,51.916]],[[179.452,51.373,179.278,51.372,178.926,51.535,178.747,51.587,178.648,51.644,178.692,51.656,178.908,51.616,179.084,51.528,179.182,51.47,179.294,51.421,179.416,51.401,179.452,51.373]],[[173.723,52.36,173.658,52.357,173.616,52.391,173.402,52.405,173.425,52.438,173.517,52.451,173.658,52.504,173.776,52.495,173.745,52.447,173.723,52.36]],[[-134.97,57.351,-134.885,57.242,-134.823,57.157,-134.769,57.054,-134.677,56.842,-134.634,56.762,-134.621,56.718,-134.611,56.603,-134.624,56.579,-134.652,56.556,-134.657,56.523,-134.632,56.436,-134.63,56.302,-134.654,56.227,-134.682,56.216,-134.75,56.241,-134.806,56.281,-134.848,56.323,-134.95,56.457,-134.981,56.519,-134.982,56.564,-134.967,56.596,-134.933,56.616,-134.875,56.67,-134.883,56.679,-134.928,56.667,-135.018,56.66,-135.097,56.703,-135.159,56.725,-135.147,56.802,-135.163,56.824,-135.285,56.8,-135.331,56.822,-135.341,56.851,-135.338,56.894,-135.315,56.932,-135.2,57.027,-135.211,57.045,-135.267,57.049,-135.341,57.082,-135.375,57.188,-135.455,57.249,-135.502,57.244,-135.609,57.071,-135.662,57.034,-135.812,57.01,-135.782,57.058,-135.768,57.1,-135.821,57.23,-135.823,57.28,-135.787,57.317,-135.681,57.333,-135.625,57.354,-135.581,57.39,-135.57,57.425,-135.487,57.517,-135.449,57.534,-135.346,57.533,-135.131,57.432,-135.065,57.417,-134.97,57.351]],[[-134.68,58.162,-134.426,58.139,-134.24,58.144,-134.07,57.995,-133.966,57.874,-133.904,57.789,-133.869,57.708,-133.823,57.629,-133.827,57.618,-133.925,57.671,-133.996,57.778,-134.032,57.821,-134.067,57.84,-134.105,57.879,-134.178,57.982,-134.18,58.011,-134.213,58.038,-134.25,58.049,-134.292,58.045,-134.307,58.034,-134.3,57.963,-134.267,57.885,-134.084,57.712,-133.961,57.614,-133.937,57.582,-133.921,57.492,-133.974,57.451,-133.909,57.369,-133.911,57.353,-133.925,57.337,-134.1,57.3,-134.26,57.147,-134.435,57.057,-134.516,57.043,-134.555,57.058,-134.592,57.092,-134.613,57.138,-134.62,57.196,-134.576,57.232,-134.489,57.42,-134.487,57.482,-134.595,57.568,-134.66,57.638,-134.695,57.736,-134.754,57.995,-134.781,58.078,-134.82,58.147,-134.87,58.202,-134.908,58.263,-134.933,58.329,-134.923,58.355,-134.837,58.32,-134.733,58.225,-134.68,58.162]],[[-135.73,58.244,-135.587,58.147,-135.586,58.124,-135.615,58.057,-135.693,58.039,-135.671,58.012,-135.613,57.992,-135.572,58.009,-135.421,58.102,-135.375,58.122,-135.347,58.124,-135.163,58.096,-135.002,58.051,-134.955,58.015,-134.928,57.953,-134.971,57.817,-135.103,57.794,-135.165,57.796,-135.231,57.816,-135.338,57.769,-135.25,57.733,-134.979,57.724,-134.897,57.648,-134.873,57.589,-134.931,57.481,-135.085,57.511,-135.22,57.574,-135.498,57.662,-135.564,57.666,-135.609,57.651,-135.621,57.597,-135.618,57.48,-135.692,57.42,-135.911,57.447,-135.997,57.535,-136.077,57.675,-136.378,57.84,-136.46,57.873,-136.569,57.972,-136.525,58.051,-136.512,58.096,-136.454,58.108,-136.37,58.143,-136.322,58.219,-136.246,58.157,-136.144,58.098,-136.142,58.154,-136.094,58.198,-135.994,58.197,-135.947,58.206,-135.882,58.247,-135.787,58.269,-135.73,58.244]],[[-133.566,56.339,-133.377,56.318,-133.203,56.32,-133.144,56.279,-133.104,56.235,-133.082,56.194,-133.075,56.156,-133.08,56.129,-133.101,56.1,-133.097,56.09,-132.758,55.995,-132.598,55.895,-132.534,55.842,-132.497,55.798,-132.43,55.687,-132.289,55.558,-132.215,55.519,-132.173,55.481,-132.196,55.479,-132.296,55.507,-132.511,55.594,-132.529,55.59,-132.548,55.544,-132.582,55.503,-132.631,55.473,-132.592,55.464,-132.418,55.483,-132.272,55.399,-132.215,55.384,-132.16,55.323,-132.158,55.3,-132.19,55.255,-132.215,55.237,-132.207,55.224,-132.166,55.218,-132.005,55.231,-131.976,55.209,-132,55.034,-131.978,54.969,-131.978,54.94,-131.997,54.901,-131.997,54.869,-131.983,54.835,-131.981,54.805,-132.022,54.726,-132.065,54.713,-132.134,54.713,-132.189,54.735,-132.266,54.802,-132.341,54.907,-132.37,54.922,-132.469,54.938,-132.486,54.95,-132.549,54.953,-132.594,54.996,-132.588,55.052,-132.627,55.11,-132.622,55.136,-132.665,55.147,-132.702,55.131,-132.683,55.074,-132.704,55.03,-132.782,55.048,-132.913,55.188,-133.061,55.301,-133.119,55.328,-133.103,55.36,-133.03,55.378,-132.971,55.376,-132.959,55.396,-133.082,55.504,-133.078,55.535,-133.033,55.59,-133.09,55.613,-133.244,55.595,-133.298,55.607,-133.343,55.651,-133.369,55.689,-133.503,55.696,-133.553,55.691,-133.64,55.749,-133.68,55.785,-133.664,55.804,-133.584,55.837,-133.537,55.832,-133.447,55.797,-133.412,55.798,-133.322,55.845,-133.308,55.886,-133.242,55.921,-133.252,55.957,-133.289,56.019,-133.371,56.036,-133.539,55.999,-133.684,55.943,-133.743,55.965,-133.755,55.999,-133.599,56.094,-133.531,56.146,-133.544,56.177,-133.594,56.216,-133.599,56.316,-133.566,56.339]],[[-133.99,56.845,-133.925,56.776,-133.831,56.781,-133.778,56.729,-133.738,56.65,-133.767,56.6,-133.809,56.611,-133.855,56.582,-133.884,56.485,-133.87,56.389,-133.885,56.292,-133.939,56.194,-133.95,56.128,-133.971,56.108,-133.994,56.101,-134.024,56.119,-134.067,56.133,-134.122,56.077,-134.19,56.077,-134.245,56.203,-134.195,56.414,-134.084,56.456,-134.15,56.513,-134.29,56.58,-134.278,56.617,-134.384,56.724,-134.391,56.749,-134.374,56.839,-134.274,56.918,-134.143,56.932,-134.052,56.898,-134.001,56.869,-133.99,56.845]],[[-152.417,58.36,-152.381,58.352,-152.343,58.412,-152.316,58.413,-152.198,58.363,-152.125,58.374,-152.079,58.312,-152.037,58.307,-151.998,58.314,-151.974,58.31,-151.983,58.244,-152.069,58.178,-152.109,58.161,-152.165,58.178,-152.187,58.185,-152.224,58.214,-152.252,58.251,-152.268,58.252,-152.334,58.208,-152.333,58.187,-152.305,58.154,-152.309,58.134,-152.381,58.124,-152.452,58.129,-152.538,58.101,-152.558,58.119,-152.571,58.168,-152.598,58.163,-152.639,58.102,-152.683,58.063,-152.764,58.031,-152.782,58.016,-152.841,58.014,-152.928,57.994,-152.983,57.997,-153.305,58.063,-153.381,58.087,-153.116,58.239,-152.976,58.297,-152.895,58.294,-152.815,58.276,-152.772,58.279,-152.769,58.346,-152.844,58.396,-152.841,58.416,-152.675,58.451,-152.612,58.446,-152.544,58.428,-152.478,58.4,-152.417,58.36]],[[-167.964,53.345,-168.271,53.238,-168.37,53.16,-168.446,53.084,-168.506,53.043,-168.549,53.036,-168.597,53.016,-168.699,52.963,-168.741,52.957,-169.066,52.834,-169.089,52.832,-169.073,52.864,-168.974,52.91,-168.909,52.951,-168.836,53.02,-168.796,53.045,-168.783,53.079,-168.778,53.149,-168.76,53.175,-168.69,53.227,-168.639,53.256,-168.572,53.266,-168.437,53.257,-168.38,53.283,-168.363,53.304,-168.397,53.322,-168.405,53.354,-168.396,53.409,-168.357,53.458,-168.288,53.5,-168.193,53.533,-168.073,53.557,-167.986,53.558,-167.828,53.508,-167.805,53.485,-167.843,53.435,-167.865,53.387,-167.964,53.345]],[[-166.615,53.901,-166.572,53.853,-166.497,53.884,-166.443,53.925,-166.4,53.978,-166.372,53.999,-166.336,53.971,-166.231,53.933,-166.319,53.874,-166.489,53.785,-166.546,53.726,-166.549,53.701,-166.385,53.721,-166.339,53.718,-166.309,53.698,-166.355,53.674,-166.444,53.652,-166.522,53.61,-166.702,53.537,-166.77,53.476,-166.851,53.453,-166.961,53.447,-167.154,53.408,-167.271,53.371,-167.3,53.35,-167.337,53.341,-167.381,53.342,-167.429,53.326,-167.48,53.292,-167.522,53.276,-167.592,53.273,-167.629,53.259,-167.669,53.26,-167.781,53.3,-167.809,53.324,-167.71,53.371,-167.639,53.387,-167.53,53.394,-167.424,53.437,-167.204,53.495,-167.136,53.526,-167.092,53.636,-167.042,53.655,-167.016,53.698,-166.894,53.697,-166.838,53.648,-166.819,53.641,-166.809,53.646,-166.804,53.685,-166.741,53.713,-166.777,53.733,-166.89,53.759,-166.973,53.771,-167.027,53.769,-167.071,53.783,-167.106,53.813,-167.121,53.843,-167.118,53.873,-167.09,53.906,-167.038,53.942,-166.978,53.963,-166.849,53.978,-166.734,54.002,-166.673,54.006,-166.627,53.996,-166.615,53.901]],[[-173.553,52.136,-173.357,52.096,-173.113,52.1,-173.024,52.091,-173.023,52.079,-173.179,52.063,-173.232,52.068,-173.368,52.046,-173.461,52.042,-173.673,52.063,-173.836,52.048,-173.879,52.054,-173.93,52.072,-173.99,52.104,-173.992,52.123,-173.939,52.131,-173.794,52.104,-173.779,52.118,-173.657,52.144,-173.553,52.136]],[[-174.677,52.035,-175.214,51.994,-175.296,52.022,-175.214,52.038,-175.118,52.047,-174.916,52.094,-174.668,52.135,-174.474,52.184,-174.306,52.216,-174.259,52.269,-174.406,52.296,-174.436,52.317,-174.365,52.342,-174.307,52.378,-174.169,52.42,-174.046,52.367,-174.018,52.332,-174.03,52.29,-174.055,52.246,-174.163,52.223,-174.179,52.2,-174.121,52.135,-174.344,52.078,-174.677,52.035]],[[-176.593,51.867,-176.588,51.833,-176.473,51.837,-176.437,51.82,-176.437,51.754,-176.452,51.736,-176.47,51.731,-176.511,51.746,-176.558,51.712,-176.771,51.63,-176.837,51.676,-176.962,51.604,-176.874,51.79,-176.774,51.819,-176.736,51.84,-176.745,51.895,-176.698,51.986,-176.597,51.982,-176.55,51.944,-176.552,51.92,-176.593,51.867]],[[-177.879,51.65,-177.901,51.616,-177.925,51.617,-178.059,51.673,-178.078,51.691,-178,51.717,-177.977,51.738,-177.986,51.764,-178.045,51.801,-178.153,51.848,-178.195,51.882,-178.168,51.903,-178.117,51.916,-177.954,51.918,-177.866,51.86,-177.8,51.84,-177.644,51.826,-177.725,51.802,-177.771,51.778,-177.827,51.686,-177.879,51.65]],[[179.728,51.905,179.645,51.88,179.55,51.894,179.498,51.933,179.504,51.98,179.627,52.03,179.78,51.967,179.728,51.905]],[[177.415,51.883,177.329,51.841,177.261,51.884,177.25,51.903,177.381,51.976,177.478,51.992,177.521,52.018,177.564,52.11,177.637,52.114,177.67,52.103,177.653,52.06,177.596,51.994,177.594,51.948,177.415,51.883]],[[172.812,53.013,172.984,52.98,173.102,52.996,173.252,52.943,173.436,52.852,173.395,52.835,173.348,52.825,173.303,52.826,173.159,52.811,173.08,52.814,172.935,52.752,172.776,52.797,172.722,52.886,172.595,52.907,172.495,52.938,172.678,53.008,172.812,53.013]],[[-155.581,19.012,-155.626,18.964,-155.681,18.968,-155.881,19.071,-155.906,19.126,-155.891,19.383,-155.966,19.591,-156.049,19.75,-155.988,19.832,-155.909,19.895,-155.82,20.014,-155.893,20.167,-155.874,20.26,-155.832,20.276,-155.622,20.163,-155.199,19.994,-155.086,19.876,-155.066,19.748,-154.989,19.732,-154.953,19.645,-154.841,19.568,-154.804,19.524,-154.85,19.454,-155.053,19.319,-155.31,19.26,-155.535,19.109,-155.581,19.012]],[[-157.214,21.215,-157.002,21.188,-156.952,21.2,-156.917,21.177,-156.742,21.164,-156.712,21.155,-156.748,21.104,-156.86,21.056,-157.021,21.098,-157.29,21.113,-157.279,21.152,-157.254,21.181,-157.25,21.23,-157.214,21.215]],[[-156.487,20.933,-156.461,20.915,-156.354,20.941,-156.278,20.951,-156.148,20.885,-156.104,20.84,-156.019,20.792,-155.99,20.757,-156.014,20.715,-156.107,20.645,-156.235,20.629,-156.31,20.599,-156.409,20.605,-156.438,20.618,-156.449,20.706,-156.48,20.801,-156.544,20.79,-156.615,20.822,-156.69,20.901,-156.698,20.949,-156.657,21.025,-156.585,21.034,-156.532,20.993,-156.487,20.933]],[[-157.799,21.457,-157.765,21.451,-157.721,21.458,-157.706,21.378,-157.654,21.334,-157.635,21.308,-157.691,21.28,-157.799,21.269,-157.849,21.291,-157.902,21.341,-157.958,21.327,-157.968,21.367,-157.978,21.379,-158.017,21.368,-157.981,21.316,-158.079,21.312,-158.11,21.319,-158.138,21.377,-158.239,21.489,-158.239,21.533,-158.273,21.585,-158.123,21.6,-158.02,21.692,-157.962,21.701,-157.852,21.553,-157.854,21.512,-157.83,21.471,-157.799,21.457]],[[-159.373,21.932,-159.461,21.876,-159.512,21.9,-159.609,21.91,-159.646,21.952,-159.748,21.99,-159.789,22.042,-159.727,22.14,-159.579,22.223,-159.352,22.22,-159.305,22.154,-159.301,22.105,-159.33,22.051,-159.344,21.974,-159.373,21.932]],[[-160.18,21.841,-160.2,21.797,-160.235,21.804,-160.243,21.843,-160.221,21.897,-160.164,21.944,-160.101,22.015,-160.049,22.005,-160.077,21.958,-160.08,21.907,-160.153,21.879,-160.18,21.841]],[[-156.85,20.773,-156.909,20.744,-156.973,20.758,-156.988,20.826,-157.051,20.912,-156.942,20.93,-156.881,20.905,-156.848,20.878,-156.809,20.831,-156.85,20.773]],[[-74.709,45.004,-74.663,45.004,-74.43,45.004,-74.014,45.005,-73.598,45.005,-73.182,45.006,-72.766,45.006,-72.35,45.007,-71.934,45.007,-71.518,45.008,-71.419,45.2,-71.327,45.29,-71.202,45.26,-71.135,45.263,-71.06,45.309,-71,45.337,-70.96,45.333,-70.926,45.291,-70.898,45.262,-70.865,45.271,-70.837,45.311,-70.838,45.366,-70.799,45.405,-70.753,45.411,-70.711,45.409,-70.69,45.428,-70.692,45.455,-70.707,45.499,-70.702,45.551,-70.596,45.644,-70.467,45.707,-70.421,45.738,-70.408,45.802,-70.333,45.868,-70.296,45.906,-70.287,45.939,-70.306,45.98,-70.304,46.057,-70.279,46.15,-70.248,46.251,-70.18,46.342,-70.067,46.441,-70.038,46.571,-70.008,46.709,-69.872,46.843,-69.718,46.995,-69.63,47.081,-69.471,47.239,-69.359,47.351,-69.302,47.402,-69.243,47.463,-69.146,47.445,-69.05,47.427,-69.064,47.338,-69.049,47.274,-69.003,47.236,-68.937,47.211,-68.887,47.203,-68.829,47.203,-68.669,47.253,-68.48,47.286,-68.377,47.316,-68.358,47.345,-68.311,47.354,-68.235,47.346,-68.097,47.275,-67.935,47.168,-67.807,47.083,-67.803,46.936,-67.8,46.78,-67.798,46.616,-67.796,46.498,-67.793,46.337,-67.79,46.209,-67.786,46.042,-67.785,45.953,-67.767,45.927,-67.778,45.892,-67.782,45.874,-67.781,45.86,-67.774,45.843,-67.775,45.818,-67.792,45.796,-67.8,45.77,-67.802,45.728,-67.785,45.702,-67.755,45.686,-67.731,45.686,-67.699,45.671,-67.658,45.644,-67.596,45.621,-67.531,45.613,-67.487,45.618,-67.433,45.603,-67.414,45.566,-67.424,45.53,-67.455,45.514,-67.488,45.501,-67.494,45.474,-67.477,45.446,-67.454,45.421,-67.428,45.378,-67.439,45.34,-67.462,45.309,-67.473,45.276,-67.453,45.248,-67.4,45.21,-67.367,45.174,-67.315,45.154,-67.291,45.168,-67.271,45.187,-67.25,45.201,-67.213,45.193,-67.171,45.182,-67.125,45.169,-67.13,45.139,-67.102,45.088,-67.08,44.989,-67.114,44.944,-67.107,44.885,-67.014,44.868,-66.991,44.85,-66.987,44.828,-67.191,44.676,-67.364,44.697,-67.458,44.657,-67.556,44.645,-67.599,44.577,-67.653,44.562,-67.727,44.567,-67.79,44.586,-67.839,44.576,-67.907,44.454,-67.963,44.464,-67.985,44.42,-68.014,44.401,-68.057,44.384,-68.094,44.439,-68.117,44.491,-68.152,44.502,-68.198,44.515,-68.246,44.515,-68.277,44.507,-68.317,44.474,-68.374,44.445,-68.417,44.469,-68.451,44.508,-68.479,44.446,-68.521,44.38,-68.514,44.304,-68.533,44.259,-68.572,44.271,-68.612,44.311,-68.723,44.342,-68.812,44.339,-68.794,44.382,-68.71,44.443,-68.736,44.454,-68.777,44.446,-68.795,44.454,-68.766,44.51,-68.763,44.571,-68.8,44.549,-68.847,44.485,-68.961,44.434,-68.956,44.348,-69.064,44.172,-69.068,44.098,-69.137,44.038,-69.226,43.986,-69.345,44.001,-69.435,43.956,-69.481,43.905,-69.521,43.897,-69.542,43.963,-69.557,43.983,-69.59,43.887,-69.624,43.881,-69.637,43.949,-69.653,43.994,-69.699,43.955,-69.73,43.852,-69.762,43.861,-69.772,43.899,-69.795,43.911,-69.803,43.867,-69.792,43.805,-69.808,43.772,-69.84,43.79,-69.873,43.82,-69.926,43.797,-69.974,43.788,-69.975,43.818,-69.965,43.855,-70.062,43.835,-70.179,43.766,-70.269,43.672,-70.238,43.656,-70.203,43.626,-70.36,43.48,-70.521,43.349,-70.642,43.134,-70.691,43.109,-70.733,43.07,-70.778,42.941,-70.829,42.825,-70.8,42.774,-70.781,42.721,-70.736,42.669,-70.697,42.665,-70.655,42.674,-70.624,42.672,-70.604,42.65,-70.613,42.623,-70.661,42.617,-70.752,42.57,-70.831,42.553,-70.871,42.497,-70.93,42.432,-71.046,42.331,-70.997,42.3,-70.818,42.265,-70.738,42.229,-70.618,42.04,-70.645,42.022,-70.656,41.987,-70.549,41.939,-70.515,41.803,-70.427,41.757,-70.295,41.729,-70.135,41.77,-70.001,41.826,-70.006,41.872,-70.09,41.98,-70.11,42.03,-70.173,42.063,-70.196,42.035,-70.237,42.071,-70.241,42.091,-70.204,42.101,-70.16,42.097,-70.109,42.078,-69.978,41.961,-69.942,41.808,-69.934,41.71,-69.949,41.677,-69.987,41.684,-70.06,41.677,-70.405,41.627,-70.481,41.582,-70.657,41.534,-70.668,41.558,-70.655,41.608,-70.666,41.71,-70.701,41.715,-70.974,41.549,-71.08,41.538,-71.169,41.489,-71.188,41.516,-71.204,41.641,-71.149,41.746,-71.178,41.744,-71.271,41.681,-71.311,41.72,-71.331,41.762,-71.359,41.786,-71.39,41.795,-71.364,41.703,-71.427,41.633,-71.444,41.454,-71.523,41.379,-71.769,41.331,-71.93,41.341,-72.074,41.326,-72.265,41.292,-72.371,41.312,-72.479,41.276,-72.847,41.266,-72.925,41.285,-73.024,41.216,-73.182,41.176,-73.583,41.022,-73.671,40.966,-73.779,40.878,-73.851,40.831,-73.911,40.816,-73.947,40.777,-73.987,40.751,-73.949,40.839,-73.907,40.912,-73.872,41.055,-73.882,41.171,-73.925,41.218,-73.97,41.25,-73.918,41.136,-73.909,40.996,-73.927,40.914,-74.025,40.756,-74.067,40.72,-74.116,40.687,-74.153,40.673,-74.187,40.648,-74.227,40.608,-74.264,40.529,-74.242,40.456,-74.05,40.43,-73.998,40.452,-73.972,40.4,-73.958,40.328,-73.972,40.251,-74.004,40.171,-74.028,40.073,-74.049,39.923,-74.08,39.788,-74.084,39.829,-74.065,39.993,-74.096,39.976,-74.118,39.938,-74.176,39.727,-74.257,39.614,-74.331,39.536,-74.407,39.549,-74.39,39.487,-74.411,39.455,-74.429,39.387,-74.474,39.343,-74.517,39.347,-74.579,39.316,-74.603,39.293,-74.605,39.248,-74.646,39.208,-74.794,39.002,-74.923,38.941,-74.954,38.95,-74.92,39.047,-74.897,39.145,-74.975,39.188,-75.05,39.211,-75.136,39.208,-75.231,39.284,-75.353,39.34,-75.524,39.49,-75.519,39.532,-75.524,39.602,-75.472,39.712,-75.422,39.79,-75.353,39.83,-75.154,39.871,-75.104,39.932,-75.074,39.983,-75.173,39.895,-75.321,39.865,-75.401,39.832,-75.464,39.781,-75.502,39.717,-75.588,39.641,-75.582,39.589,-75.567,39.553,-75.574,39.477,-75.52,39.403,-75.413,39.281,-75.392,39.093,-75.31,38.967,-75.185,38.819,-75.089,38.778,-75.084,38.723,-75.128,38.632,-75.187,38.591,-75.111,38.599,-75.073,38.579,-75.036,38.503,-75.039,38.426,-75.051,38.383,-75.074,38.366,-75.073,38.41,-75.09,38.425,-75.117,38.406,-75.134,38.384,-75.142,38.298,-75.16,38.255,-75.225,38.242,-75.292,38.129,-75.354,38.065,-75.596,37.631,-75.587,37.559,-75.632,37.535,-75.699,37.516,-75.767,37.473,-75.812,37.425,-75.854,37.297,-75.934,37.152,-75.985,37.212,-75.997,37.264,-75.975,37.398,-75.888,37.619,-75.792,37.756,-75.719,37.821,-75.659,37.954,-75.735,37.974,-75.851,37.972,-75.829,38.033,-75.795,38.087,-75.856,38.14,-75.891,38.147,-75.928,38.169,-75.885,38.214,-75.864,38.261,-75.877,38.319,-75.859,38.362,-75.889,38.356,-75.937,38.31,-75.967,38.291,-75.986,38.332,-76.007,38.323,-76.02,38.295,-76.051,38.28,-76.117,38.318,-76.212,38.361,-76.265,38.436,-76.295,38.495,-76.264,38.6,-76.198,38.619,-76.113,38.602,-76.001,38.602,-76.017,38.625,-76.057,38.621,-76.175,38.707,-76.213,38.758,-76.278,38.772,-76.308,38.723,-76.341,38.71,-76.3,38.818,-76.247,38.823,-76.168,38.853,-76.191,38.916,-76.241,38.943,-76.331,38.909,-76.33,38.953,-76.313,39.009,-76.245,39.009,-76.186,38.991,-76.135,39.082,-76.133,39.123,-76.217,39.064,-76.236,39.192,-76.153,39.315,-76.074,39.369,-75.976,39.367,-75.876,39.376,-75.939,39.399,-76.003,39.411,-75.955,39.46,-75.913,39.468,-75.873,39.511,-75.97,39.505,-75.959,39.585,-76.006,39.569,-76.063,39.561,-76.085,39.527,-76.081,39.47,-76.097,39.433,-76.141,39.403,-76.216,39.38,-76.223,39.42,-76.248,39.439,-76.257,39.352,-76.276,39.323,-76.331,39.404,-76.347,39.388,-76.345,39.365,-76.359,39.325,-76.406,39.304,-76.403,39.253,-76.421,39.225,-76.57,39.269,-76.574,39.254,-76.489,39.159,-76.428,39.126,-76.42,39.074,-76.473,39.031,-76.546,39.068,-76.559,39.065,-76.519,39.001,-76.494,38.945,-76.52,38.898,-76.516,38.841,-76.521,38.788,-76.537,38.743,-76.501,38.532,-76.458,38.475,-76.416,38.42,-76.394,38.369,-76.439,38.362,-76.51,38.404,-76.572,38.436,-76.647,38.539,-76.659,38.58,-76.677,38.612,-76.669,38.538,-76.642,38.454,-76.409,38.268,-76.366,38.197,-76.333,38.141,-76.341,38.087,-76.402,38.125,-76.454,38.174,-76.594,38.228,-76.769,38.263,-76.868,38.39,-76.868,38.337,-76.89,38.292,-76.95,38.347,-76.988,38.394,-77.001,38.445,-77.077,38.442,-77.156,38.397,-77.233,38.408,-77.242,38.495,-77.221,38.541,-77.135,38.65,-77.054,38.706,-77.018,38.778,-77.03,38.889,-77.046,38.776,-77.092,38.72,-77.165,38.677,-77.26,38.6,-77.284,38.529,-77.314,38.397,-77.273,38.352,-77.232,38.34,-77.11,38.37,-77.047,38.357,-76.906,38.197,-76.645,38.134,-76.55,38.094,-76.472,38.011,-76.355,37.963,-76.264,37.894,-76.262,37.848,-76.293,37.794,-76.306,37.722,-76.344,37.676,-76.437,37.67,-76.492,37.682,-76.793,37.938,-76.829,37.962,-76.94,38.095,-77.071,38.167,-77.111,38.166,-76.925,38.033,-76.849,37.94,-76.715,37.81,-76.62,37.755,-76.549,37.669,-76.484,37.629,-76.306,37.571,-76.368,37.53,-76.269,37.495,-76.254,37.431,-76.263,37.357,-76.401,37.386,-76.405,37.332,-76.393,37.3,-76.454,37.274,-76.538,37.309,-76.758,37.505,-76.756,37.479,-76.738,37.449,-76.611,37.323,-76.497,37.247,-76.401,37.213,-76.327,37.149,-76.301,37.111,-76.283,37.053,-76.338,37.013,-76.401,36.991,-76.462,37.031,-76.507,37.072,-76.602,37.143,-76.631,37.222,-76.704,37.218,-77.007,37.318,-77.251,37.329,-77.227,37.309,-77.196,37.296,-77.002,37.271,-76.925,37.225,-76.765,37.184,-76.672,37.173,-76.634,37.047,-76.505,36.961,-76.488,36.897,-76.4,36.89,-76.244,36.953,-76.144,36.931,-75.999,36.913,-75.966,36.862,-75.942,36.766,-75.89,36.657,-75.758,36.229,-75.559,35.879,-75.534,35.819,-75.58,35.872,-75.728,36.104,-75.81,36.271,-75.894,36.567,-75.918,36.633,-75.946,36.659,-75.965,36.638,-75.973,36.6,-75.96,36.571,-75.993,36.474,-75.978,36.429,-75.925,36.383,-75.867,36.268,-75.82,36.113,-75.883,36.176,-75.95,36.209,-76.055,36.235,-76.148,36.279,-76.141,36.215,-76.15,36.146,-76.222,36.167,-76.271,36.19,-76.227,36.116,-76.321,36.138,-76.384,36.134,-76.424,36.068,-76.479,36.028,-76.559,36.015,-76.679,36.075,-76.718,36.148,-76.734,36.229,-76.74,36.133,-76.719,36.033,-76.726,35.958,-76.611,35.944,-76.504,35.956,-76.358,35.953,-76.264,35.967,-76.207,35.991,-76.07,35.97,-76.06,35.879,-76.076,35.788,-76.084,35.691,-76.046,35.691,-76.001,35.722,-75.979,35.896,-75.854,35.96,-75.812,35.956,-75.772,35.9,-75.759,35.843,-75.745,35.765,-75.774,35.647,-75.966,35.508,-76.104,35.38,-76.174,35.354,-76.275,35.369,-76.39,35.401,-76.447,35.408,-76.49,35.397,-76.516,35.436,-76.532,35.508,-76.577,35.532,-76.611,35.53,-76.634,35.453,-76.741,35.431,-76.887,35.463,-77.04,35.527,-76.974,35.458,-76.595,35.33,-76.553,35.306,-76.513,35.27,-76.566,35.215,-76.608,35.153,-76.613,35.104,-76.628,35.073,-76.779,34.99,-76.861,35.005,-77.07,35.155,-76.975,35.025,-76.899,34.97,-76.745,34.941,-76.457,34.989,-76.362,34.937,-76.44,34.843,-76.517,34.777,-76.618,34.77,-76.707,34.752,-76.733,34.707,-76.797,34.704,-76.896,34.701,-77.05,34.697,-77.134,34.708,-77.252,34.616,-77.296,34.603,-77.358,34.62,-77.384,34.694,-77.412,34.731,-77.413,34.592,-77.402,34.555,-77.38,34.527,-77.518,34.451,-77.65,34.358,-77.697,34.332,-77.751,34.285,-77.861,34.149,-77.888,34.05,-77.928,33.94,-77.933,33.989,-77.926,34.073,-77.953,34.169,-77.971,33.993,-78.013,33.912,-78.406,33.918,-78.578,33.873,-78.841,33.724,-78.92,33.659,-79.138,33.406,-79.194,33.244,-79.238,33.312,-79.227,33.363,-79.226,33.405,-79.281,33.315,-79.229,33.185,-79.276,33.135,-79.42,33.043,-79.499,33.027,-79.587,33.001,-79.615,32.909,-79.735,32.825,-79.805,32.787,-79.933,32.81,-79.894,32.729,-79.941,32.667,-80.022,32.62,-80.123,32.589,-80.18,32.593,-80.23,32.577,-80.268,32.537,-80.363,32.501,-80.461,32.521,-80.572,32.534,-80.634,32.512,-80.53,32.475,-80.474,32.423,-80.486,32.352,-80.514,32.324,-80.579,32.287,-80.608,32.293,-80.626,32.326,-80.647,32.396,-80.678,32.381,-80.683,32.349,-80.709,32.337,-80.803,32.448,-80.798,32.363,-80.765,32.298,-80.734,32.265,-80.702,32.246,-80.694,32.216,-80.758,32.142,-80.791,32.126,-80.849,32.114,-80.882,32.069,-80.872,32.03,-80.923,31.945,-81.046,31.892,-81.083,31.894,-81.113,31.879,-81.096,31.841,-81.065,31.813,-81.066,31.788,-81.098,31.753,-81.162,31.744,-81.198,31.704,-81.187,31.667,-81.166,31.646,-81.17,31.61,-81.242,31.574,-81.259,31.539,-81.223,31.528,-81.196,31.539,-81.175,31.531,-81.219,31.472,-81.258,31.436,-81.295,31.371,-81.381,31.353,-81.378,31.332,-81.329,31.314,-81.288,31.264,-81.365,31.172,-81.413,31.179,-81.442,31.2,-81.46,31.127,-81.453,31.088,-81.471,31.009,-81.501,30.914,-81.52,30.875,-81.516,30.802,-81.504,30.731,-81.457,30.641,-81.386,30.27,-81.337,30.141,-81.25,29.794,-81.105,29.457,-80.9,29.05,-80.564,28.556,-80.524,28.486,-80.568,28.426,-80.581,28.365,-80.585,28.272,-80.573,28.181,-80.533,28.07,-80.457,27.901,-80.5,27.934,-80.61,28.178,-80.623,28.32,-80.607,28.523,-80.633,28.518,-80.654,28.452,-80.665,28.375,-80.694,28.345,-80.732,28.463,-80.729,28.516,-80.688,28.579,-80.7,28.601,-80.766,28.633,-80.78,28.683,-80.771,28.732,-80.809,28.759,-80.838,28.758,-80.818,28.636,-80.787,28.561,-80.749,28.381,-80.686,28.272,-80.65,28.181,-80.226,27.207,-80.126,27.083,-80.089,26.994,-80.05,26.808,-80.041,26.569,-80.111,26.132,-80.126,25.833,-80.136,25.843,-80.143,25.874,-80.159,25.878,-80.219,25.742,-80.301,25.619,-80.328,25.427,-80.367,25.331,-80.485,25.23,-80.558,25.232,-80.737,25.156,-80.862,25.176,-81.012,25.133,-81.11,25.138,-81.167,25.229,-81.159,25.269,-81.136,25.31,-81.098,25.319,-80.965,25.224,-80.94,25.264,-80.98,25.312,-81.057,25.338,-81.113,25.367,-81.227,25.583,-81.345,25.732,-81.365,25.831,-81.568,25.892,-81.715,25.983,-81.811,26.146,-81.867,26.435,-81.931,26.467,-81.959,26.49,-81.896,26.597,-81.829,26.687,-81.882,26.665,-81.921,26.631,-81.97,26.552,-82.006,26.54,-82.04,26.552,-82.078,26.704,-82.067,26.892,-82.013,26.962,-82.096,26.963,-82.181,26.937,-82.169,26.874,-82.181,26.84,-82.243,26.849,-82.29,26.871,-82.354,26.936,-82.441,27.06,-82.62,27.401,-82.655,27.449,-82.715,27.5,-82.687,27.515,-82.636,27.525,-82.521,27.678,-82.431,27.771,-82.401,27.835,-82.406,27.863,-82.446,27.903,-82.498,27.868,-82.521,27.878,-82.58,27.958,-82.636,27.981,-82.675,27.964,-82.634,27.898,-82.597,27.873,-82.611,27.777,-82.626,27.746,-82.661,27.718,-82.715,27.733,-82.743,27.709,-82.775,27.734,-82.808,27.777,-82.844,27.846,-82.749,28.237,-82.661,28.486,-82.651,28.77,-82.644,28.812,-82.651,28.888,-82.769,29.052,-83.29,29.452,-83.694,29.926,-84.044,30.104,-84.31,30.065,-84.356,30.029,-84.375,29.982,-84.359,29.929,-84.383,29.907,-84.454,29.91,-84.55,29.898,-84.801,29.773,-84.889,29.778,-84.969,29.745,-85.029,29.721,-85.186,29.708,-85.319,29.68,-85.376,29.695,-85.414,29.768,-85.414,29.842,-85.383,29.785,-85.336,29.74,-85.315,29.758,-85.307,29.798,-85.354,29.876,-85.504,29.976,-85.676,30.122,-85.623,30.117,-85.61,30.148,-85.663,30.189,-85.641,30.237,-85.604,30.287,-85.676,30.279,-85.741,30.244,-85.743,30.201,-85.756,30.167,-85.791,30.172,-85.856,30.214,-86.175,30.333,-86.454,30.399,-86.24,30.429,-86.124,30.406,-86.138,30.442,-86.166,30.464,-86.257,30.493,-86.374,30.482,-86.448,30.496,-86.523,30.467,-86.606,30.425,-86.68,30.403,-86.968,30.372,-87.201,30.339,-87.164,30.374,-87.124,30.397,-86.986,30.431,-86.965,30.502,-86.998,30.57,-87.034,30.554,-87.072,30.5,-87.119,30.539,-87.171,30.539,-87.185,30.454,-87.251,30.397,-87.281,30.339,-87.476,30.294,-87.501,30.309,-87.444,30.364,-87.448,30.394,-87.513,30.368,-87.622,30.265,-88.006,30.231,-87.985,30.254,-87.904,30.259,-87.79,30.292,-87.813,30.347,-87.857,30.407,-87.898,30.414,-87.924,30.45,-87.923,30.562,-87.949,30.627,-88.011,30.694,-88.032,30.681,-88.078,30.566,-88.117,30.415,-88.135,30.367,-88.249,30.363,-88.35,30.373,-88.692,30.355,-88.82,30.406,-88.873,30.416,-88.905,30.415,-89.054,30.368,-89.224,30.332,-89.264,30.344,-89.321,30.345,-89.444,30.223,-89.588,30.166,-89.954,30.269,-90.045,30.351,-90.126,30.369,-90.225,30.379,-90.332,30.278,-90.413,30.14,-90.285,30.065,-90.175,30.029,-89.994,30.059,-89.894,30.126,-89.812,30.124,-89.773,30.137,-89.737,30.172,-89.668,30.145,-89.665,30.117,-89.715,30.078,-89.777,30.046,-89.815,30.007,-89.744,29.93,-89.632,29.904,-89.59,29.915,-89.563,30.002,-89.494,30.058,-89.401,30.046,-89.414,30.011,-89.401,29.978,-89.358,29.921,-89.363,29.84,-89.354,29.82,-89.455,29.784,-89.531,29.772,-89.591,29.725,-89.559,29.698,-89.621,29.674,-89.662,29.684,-89.683,29.675,-89.689,29.646,-89.721,29.619,-89.675,29.539,-89.58,29.486,-89.514,29.42,-89.246,29.333,-89.181,29.336,-89.117,29.248,-89.065,29.218,-89.016,29.203,-89.021,29.143,-89.11,29.099,-89.133,29.046,-89.156,29.017,-89.195,29.054,-89.236,29.081,-89.331,28.999,-89.376,28.981,-89.354,29.07,-89.389,29.105,-89.443,29.194,-89.522,29.249,-89.577,29.268,-89.62,29.302,-89.672,29.317,-89.717,29.313,-89.792,29.333,-89.797,29.381,-89.818,29.416,-89.877,29.458,-90.159,29.537,-90.161,29.504,-90.141,29.48,-90.101,29.463,-90.052,29.431,-90.053,29.337,-90.074,29.297,-90.083,29.24,-90.101,29.182,-90.136,29.136,-90.213,29.105,-90.247,29.131,-90.302,29.256,-90.379,29.295,-90.502,29.3,-90.586,29.272,-90.677,29.151,-90.751,29.131,-91.003,29.194,-91.29,29.289,-91.283,29.321,-91.237,29.331,-91.151,29.318,-91.155,29.351,-91.244,29.457,-91.26,29.505,-91.249,29.564,-91.278,29.563,-91.331,29.514,-91.514,29.555,-91.565,29.605,-91.672,29.746,-91.824,29.751,-91.893,29.836,-92.017,29.8,-92.08,29.761,-92.135,29.699,-92.114,29.668,-92.059,29.617,-92.084,29.593,-92.261,29.557,-92.671,29.597,-92.791,29.635,-92.952,29.714,-93.176,29.779,-93.283,29.789,-93.388,29.777,-93.695,29.77,-93.766,29.753,-93.826,29.725,-93.866,29.756,-93.884,29.81,-93.848,29.819,-93.809,29.851,-93.773,29.914,-93.769,29.952,-93.794,29.977,-93.841,29.98,-93.946,29.815,-93.886,29.723,-93.89,29.689,-94.1,29.67,-94.574,29.485,-94.76,29.384,-94.75,29.418,-94.526,29.548,-94.605,29.568,-94.733,29.535,-94.778,29.548,-94.724,29.655,-94.742,29.75,-94.832,29.753,-94.89,29.677,-94.93,29.68,-94.982,29.713,-95.023,29.702,-94.993,29.531,-94.936,29.46,-94.888,29.371,-95.018,29.259,-95.139,29.168,-95.152,29.079,-95.273,28.964,-95.388,28.898,-95.656,28.745,-95.732,28.712,-95.853,28.64,-96.02,28.587,-96.181,28.502,-96.235,28.489,-96.132,28.561,-96.011,28.632,-96.115,28.622,-96.275,28.655,-96.373,28.657,-96.374,28.631,-96.449,28.594,-96.526,28.648,-96.56,28.684,-96.576,28.716,-96.608,28.723,-96.64,28.709,-96.525,28.489,-96.475,28.479,-96.421,28.457,-96.489,28.406,-96.562,28.367,-96.676,28.341,-96.774,28.422,-96.795,28.321,-96.807,28.22,-96.84,28.194,-96.892,28.158,-96.92,28.185,-96.933,28.224,-96.967,28.19,-97.015,28.163,-97.096,28.158,-97.156,28.144,-97.155,28.103,-97.141,28.061,-97.034,28.094,-97.073,27.986,-97.171,27.88,-97.252,27.854,-97.374,27.87,-97.404,27.859,-97.431,27.837,-97.289,27.671,-97.38,27.419,-97.439,27.328,-97.48,27.317,-97.524,27.314,-97.682,27.395,-97.768,27.458,-97.692,27.287,-97.485,27.237,-97.475,27.173,-97.476,27.118,-97.517,27.053,-97.555,26.967,-97.527,26.908,-97.494,26.76,-97.466,26.692,-97.435,26.486,-97.402,26.397,-97.214,26.068,-97.15,26.065,-97.14,26.03,-97.146,25.961,-97.282,25.942,-97.339,25.911,-97.35,25.885,-97.358,25.871,-97.376,25.872,-97.44,25.891,-97.587,25.984,-97.801,26.042,-98.083,26.064,-98.275,26.111,-98.378,26.182,-98.486,26.225,-98.598,26.238,-98.691,26.276,-98.765,26.34,-98.873,26.381,-99.015,26.399,-99.108,26.447,-99.172,26.564,-99.172,26.566,-99.23,26.762,-99.302,26.885,-99.444,27.037,-99.456,27.057,-99.457,27.057,-99.458,27.082,-99.44,27.17,-99.455,27.234,-99.5,27.285,-99.51,27.34,-99.486,27.398,-99.484,27.467,-99.505,27.548,-99.595,27.636,-99.754,27.73,-99.89,27.867,-100.001,28.048,-100.112,28.173,-100.221,28.243,-100.296,28.328,-100.336,28.428,-100.348,28.486,-100.332,28.503,-100.399,28.614,-100.55,28.821,-100.636,28.973,-100.659,29.069,-100.755,29.183,-100.924,29.315,-101.016,29.401,-101.039,29.46,-101.304,29.634,-101.38,29.743,-101.44,29.777,-101.509,29.773,-101.545,29.784,-101.546,29.808,-101.569,29.809,-101.612,29.787,-101.752,29.782,-101.991,29.796,-102.163,29.825,-102.269,29.871,-102.343,29.865,-102.386,29.807,-102.476,29.769,-102.615,29.752,-102.734,29.644,-102.834,29.444,-102.878,29.315,-102.866,29.258,-102.892,29.216,-102.957,29.19,-103.023,29.132,-103.09,29.042,-103.168,28.998,-103.258,29.001,-103.423,29.071,-103.664,29.207,-103.853,29.291,-103.99,29.323,-104.111,29.386,-104.216,29.48,-104.312,29.542,-104.401,29.574,-104.504,29.678,-104.622,29.854,-104.681,29.991,-104.681,30.134,-104.836,30.448,-104.918,30.583,-104.979,30.646,-105.098,30.721,-105.276,30.807,-105.514,30.981,-105.813,31.241,-106.024,31.398,-106.148,31.451,-106.256,31.545,-106.347,31.679,-106.436,31.764,-106.445,31.768,-106.453,31.77,-106.673,31.771,-106.893,31.772,-107.113,31.774,-107.333,31.775,-107.552,31.776,-107.772,31.777,-107.992,31.778,-108.212,31.779,-108.212,31.667,-108.213,31.554,-108.214,31.442,-108.214,31.329,-108.568,31.329,-108.921,31.328,-109.275,31.327,-109.628,31.327,-109.982,31.326,-110.335,31.326,-110.689,31.325,-111.042,31.324,-111.516,31.472,-111.99,31.62,-112.465,31.768,-112.939,31.916,-113.413,32.064,-113.887,32.212,-114.362,32.36,-114.836,32.508,-114.788,32.565,-114.725,32.715,-114.839,32.705,-115.125,32.683,-115.411,32.662,-115.698,32.64,-115.984,32.619,-116.27,32.598,-116.556,32.576,-116.842,32.555,-117.128,32.533,-117.13,32.54,-117.137,32.649,-117.184,32.688,-117.243,32.664,-117.271,32.806,-117.256,32.873,-117.263,32.939,-117.319,33.1,-117.467,33.296,-117.789,33.538,-117.952,33.62,-118.081,33.722,-118.162,33.751,-118.264,33.759,-118.294,33.712,-118.41,33.744,-118.393,33.858,-118.506,34.017,-118.599,34.035,-118.832,34.024,-119.144,34.112,-119.236,34.164,-119.268,34.257,-119.414,34.339,-119.606,34.418,-119.713,34.4,-119.853,34.412,-120.053,34.469,-120.17,34.476,-120.396,34.46,-120.481,34.472,-120.56,34.544,-120.645,34.58,-120.627,34.669,-120.638,34.749,-120.625,34.812,-120.663,34.949,-120.634,35.076,-120.659,35.122,-120.707,35.158,-120.857,35.21,-120.885,35.275,-120.86,35.365,-120.9,35.425,-121.023,35.481,-121.138,35.607,-121.284,35.676,-121.344,35.792,-121.434,35.864,-121.465,35.927,-121.664,36.154,-121.877,36.331,-121.91,36.433,-121.919,36.572,-121.835,36.657,-121.79,36.732,-121.795,36.801,-121.807,36.851,-121.881,36.939,-122.164,36.991,-122.395,37.208,-122.408,37.373,-122.499,37.543,-122.5,37.653,-122.514,37.772,-122.446,37.798,-122.384,37.789,-122.39,37.741,-122.37,37.656,-122.298,37.592,-122.229,37.564,-122.166,37.502,-122.119,37.483,-122.071,37.478,-122.097,37.518,-122.124,37.544,-122.158,37.626,-122.222,37.732,-122.296,37.79,-122.333,37.897,-122.365,37.921,-122.385,37.961,-122.314,38.007,-122.217,38.041,-122.087,38.05,-121.717,38.034,-121.638,38.061,-121.573,38.052,-121.525,38.056,-121.626,38.084,-121.682,38.075,-121.749,38.08,-121.881,38.075,-121.934,38.087,-121.993,38.12,-122.031,38.124,-122.154,38.066,-122.208,38.073,-122.337,38.136,-122.393,38.145,-122.484,38.109,-122.495,37.954,-122.467,37.838,-122.521,37.826,-122.584,37.874,-122.681,37.902,-122.76,37.946,-122.873,38.026,-122.932,38.055,-122.999,37.989,-123.001,38.019,-122.968,38.097,-122.978,38.227,-122.877,38.123,-122.908,38.197,-122.987,38.277,-123.046,38.305,-123.121,38.449,-123.29,38.536,-123.425,38.676,-123.701,38.907,-123.72,39.111,-123.82,39.368,-123.778,39.515,-123.783,39.619,-123.833,39.775,-123.884,39.861,-124.108,40.095,-124.324,40.252,-124.357,40.371,-124.372,40.491,-124.325,40.598,-124.284,40.711,-124.254,40.74,-124.242,40.728,-124.251,40.704,-124.22,40.696,-124.208,40.746,-124.19,40.772,-124.223,40.775,-124.219,40.791,-124.2,40.822,-124.133,40.97,-124.14,41.156,-124.069,41.384,-124.072,41.46,-124.118,41.622,-124.163,41.719,-124.245,41.788,-124.209,41.889,-124.212,41.985,-124.355,42.123,-124.41,42.304,-124.421,42.381,-124.406,42.584,-124.444,42.67,-124.54,42.813,-124.499,42.937,-124.454,43.012,-124.347,43.342,-124.321,43.368,-124.275,43.367,-124.197,43.423,-124.233,43.436,-124.288,43.41,-124.239,43.54,-124.184,43.652,-124.149,43.692,-124.131,44.056,-124.099,44.334,-124.047,44.425,-124.065,44.52,-124.045,44.648,-124.059,44.778,-123.949,45.401,-123.963,45.476,-123.929,45.577,-123.961,45.843,-123.947,46.141,-123.975,46.178,-123.989,46.219,-123.963,46.225,-123.912,46.182,-123.674,46.183,-123.522,46.223,-123.466,46.209,-123.402,46.155,-123.322,46.144,-123.221,46.154,-123.251,46.167,-123.299,46.171,-123.405,46.221,-123.465,46.271,-123.65,46.268,-123.688,46.3,-123.896,46.268,-123.96,46.301,-124.073,46.279,-124.045,46.373,-124.05,46.491,-124.044,46.605,-124.016,46.521,-123.946,46.433,-123.912,46.533,-123.889,46.66,-123.958,46.709,-124.072,46.745,-124.113,46.863,-123.843,46.963,-123.986,46.984,-124.042,47.03,-124.112,47.035,-124.117,47,-124.139,46.955,-124.164,47.015,-124.171,47.087,-124.199,47.209,-124.309,47.405,-124.376,47.659,-124.46,47.784,-124.621,47.904,-124.663,47.974,-124.702,48.152,-124.68,48.286,-124.71,48.38,-124.633,48.375,-124.429,48.301,-124.175,48.242,-124.099,48.2,-123.976,48.168,-123.294,48.12,-123.25,48.124,-123.162,48.155,-123.124,48.151,-123.024,48.082,-122.974,48.073,-122.909,48.077,-122.861,48.09,-122.779,48.138,-122.768,48.12,-122.769,48.076,-122.74,48.013,-122.679,47.932,-122.657,47.881,-122.778,47.738,-122.802,47.735,-122.805,47.784,-122.821,47.793,-123.051,47.552,-123.131,47.438,-123.139,47.386,-123.136,47.356,-123.104,47.348,-123.031,47.36,-122.922,47.408,-122.917,47.418,-123.018,47.401,-123.067,47.4,-123.06,47.454,-123.049,47.479,-122.982,47.559,-122.913,47.607,-122.814,47.659,-122.757,47.701,-122.718,47.762,-122.608,47.835,-122.588,47.856,-122.593,47.916,-122.586,47.928,-122.533,47.92,-122.511,47.816,-122.524,47.769,-122.618,47.713,-122.63,47.693,-122.614,47.616,-122.628,47.608,-122.664,47.617,-122.675,47.612,-122.586,47.528,-122.557,47.463,-122.554,47.405,-122.578,47.293,-122.604,47.275,-122.649,47.281,-122.708,47.316,-122.721,47.305,-122.768,47.218,-122.783,47.226,-122.813,47.329,-122.828,47.337,-122.92,47.29,-122.956,47.245,-122.988,47.173,-123.028,47.139,-122.914,47.131,-122.812,47.146,-122.73,47.112,-122.702,47.111,-122.627,47.144,-122.604,47.167,-122.542,47.276,-122.511,47.295,-122.465,47.296,-122.42,47.312,-122.354,47.372,-122.351,47.395,-122.375,47.528,-122.368,47.604,-122.381,47.628,-122.41,47.653,-122.407,47.677,-122.384,47.716,-122.382,47.752,-122.402,47.784,-122.393,47.821,-122.33,47.899,-122.318,47.933,-122.242,48.011,-122.261,48.042,-122.317,48.08,-122.353,48.114,-122.389,48.166,-122.416,48.184,-122.425,48.176,-122.387,48.09,-122.395,48.084,-122.494,48.13,-122.517,48.16,-122.529,48.199,-122.52,48.229,-122.467,48.258,-122.403,48.269,-122.409,48.294,-122.488,48.374,-122.542,48.411,-122.583,48.429,-122.638,48.433,-122.662,48.446,-122.669,48.465,-122.657,48.49,-122.628,48.498,-122.543,48.488,-122.497,48.506,-122.501,48.538,-122.515,48.555,-122.513,48.669,-122.545,48.762,-122.562,48.778,-122.58,48.78,-122.599,48.767,-122.653,48.764,-122.686,48.794,-122.722,48.853,-122.789,48.993,-122.686,48.993,-122.26,48.993,-121.834,48.993,-121.407,48.993,-120.981,48.993,-120.554,48.993,-120.128,48.993,-119.702,48.993,-119.275,48.993,-118.849,48.993,-118.423,48.993,-117.996,48.993,-117.57,48.993,-117.143,48.993,-116.717,48.993,-116.291,48.993,-115.864,48.993,-115.438,48.993,-115.012,48.993,-114.585,48.993,-114.159,48.993,-113.732,48.993,-113.306,48.993,-112.88,48.993,-112.453,48.993,-112.027,48.993,-111.6,48.993,-111.174,48.993,-110.748,48.993,-110.321,48.993,-109.895,48.993,-109.469,48.993,-109.042,48.993,-108.616,48.993,-108.189,48.993,-107.763,48.993,-107.337,48.993,-106.91,48.993,-106.484,48.993,-106.057,48.993,-105.631,48.993,-105.205,48.993,-104.778,48.993,-104.352,48.993,-103.926,48.993,-103.499,48.993,-103.073,48.993,-102.646,48.993,-102.22,48.993,-101.794,48.993,-101.367,48.993,-100.941,48.993,-100.515,48.993,-100.088,48.993,-99.662,48.993,-99.235,48.993,-98.809,48.993,-98.383,48.993,-97.956,48.993,-97.53,48.993,-97.103,48.993,-96.677,48.993,-96.251,48.993,-95.824,48.993,-95.398,48.993,-95.162,48.992,-95.158,49.203,-95.155,49.37,-94.939,49.349,-94.875,49.319,-94.854,49.305,-94.86,49.259,-94.843,49.119,-94.803,49.003,-94.713,48.863,-94.705,48.808,-94.675,48.774,-94.621,48.743,-94.414,48.704,-94.055,48.659,-93.852,48.607,-93.804,48.549,-93.708,48.525,-93.564,48.537,-93.464,48.561,-93.378,48.617,-93.258,48.629,-93.155,48.625,-93.052,48.62,-92.996,48.612,-92.837,48.568,-92.733,48.532,-92.583,48.465,-92.501,48.435,-92.461,48.366,-92.415,48.277,-92.348,48.277,-92.299,48.329,-92.172,48.338,-92.005,48.302,-91.858,48.198,-91.647,48.105,-91.518,48.058,-91.387,48.059,-91.221,48.105,-91.043,48.194,-90.916,48.209,-90.84,48.201,-90.797,48.131,-90.744,48.105,-90.607,48.113,-90.32,48.099,-90.092,48.118,-90.04,48.078,-89.994,48.015,-89.901,47.995,-89.775,48.015,-89.551,48,-89.456,47.996,-89.273,48.02,-89.186,48.047,-89.063,48.094,-88.899,48.156,-88.612,48.264,-88.378,48.303,-88.161,48.225,-87.987,48.157,-87.921,48.13,-87.744,48.061,-87.494,47.962,-87.208,47.848,-86.922,47.735,-86.672,47.636,-86.496,47.567,-86.429,47.54,-86.234,47.46,-86.04,47.38,-85.846,47.3,-85.652,47.22,-85.458,47.14,-85.264,47.06,-85.07,46.98,-84.876,46.9,-84.827,46.767,-84.779,46.637,-84.666,46.543,-84.562,46.457,-84.502,46.462,-84.44,46.498,-84.402,46.516,-84.337,46.519,-84.192,46.55,-84.149,46.543,-84.125,46.527,-84.123,46.503,-84.128,46.484,-84.15,46.445,-84.115,46.371,-84.108,46.289,-84.088,46.227,-84.029,46.147,-83.978,46.085,-83.913,46.073,-83.763,46.109,-83.669,46.123,-83.616,46.117,-83.525,46.059,-83.48,46.024,-83.469,45.995,-83.593,45.817,-83.397,45.729,-83.179,45.633,-82.919,45.518,-82.76,45.448,-82.551,45.347,-82.515,45.204,-82.485,45.084,-82.447,44.916,-82.407,44.744,-82.368,44.573,-82.327,44.392,-82.281,44.192,-82.241,44.015,-82.197,43.822,-82.138,43.571,-82.19,43.474,-82.305,43.263,-82.408,43.073,-82.417,43.017,-82.488,42.74,-82.545,42.625,-82.645,42.558,-82.744,42.493,-82.868,42.385,-83.004,42.332,-83.073,42.3,-83.11,42.251,-83.15,42.142,-83.142,41.976,-83.03,41.833,-82.866,41.753,-82.69,41.675,-82.439,41.675,-82.213,41.779,-81.974,41.889,-81.761,41.987,-81.507,42.103,-81.278,42.209,-81.028,42.247,-80.683,42.3,-80.248,42.366,-80.036,42.441,-79.762,42.539,-79.446,42.651,-79.174,42.749,-79.037,42.802,-78.939,42.864,-78.915,42.909,-78.921,42.935,-78.946,42.961,-78.981,42.981,-79.012,42.997,-79.026,43.017,-79.029,43.062,-79.048,43.087,-79.066,43.106,-79.059,43.278,-79.083,43.331,-79.172,43.467,-79.002,43.527,-78.846,43.583,-78.72,43.625,-78.458,43.631,-78.215,43.631,-77.879,43.63,-77.597,43.629,-77.267,43.627,-77.073,43.627,-76.82,43.629,-76.696,43.785,-76.586,43.924,-76.465,44.058,-76.249,44.214,-76.186,44.242,-76.151,44.304,-76.02,44.363,-75.876,44.417,-75.819,44.468,-75.792,44.497,-75.401,44.772,-75.179,44.899,-74.996,44.97,-74.857,45.004,-74.762,44.999,-74.709,45.004]],[[-72.51,40.986,-72.581,40.921,-72.517,40.915,-72.461,40.934,-72.409,40.972,-72.287,41.024,-72.184,41.047,-72.151,41.051,-72.102,41.015,-72.004,41.044,-71.903,41.061,-72.339,40.894,-72.428,40.875,-72.556,40.826,-72.676,40.791,-72.763,40.778,-73.194,40.654,-73.229,40.652,-73.266,40.664,-73.621,40.6,-73.767,40.593,-73.9,40.571,-73.801,40.622,-73.799,40.641,-73.823,40.656,-73.875,40.652,-73.929,40.599,-74.015,40.581,-74.032,40.639,-74.003,40.683,-73.965,40.725,-73.879,40.792,-73.757,40.834,-73.695,40.87,-73.652,40.838,-73.643,40.881,-73.61,40.906,-73.574,40.92,-73.487,40.92,-73.441,40.927,-73.407,40.941,-73.373,40.944,-73.278,40.924,-73.186,40.93,-73.111,40.957,-73.034,40.966,-72.829,40.972,-72.625,40.992,-72.544,41.027,-72.373,41.126,-72.274,41.153,-72.427,41.039,-72.51,40.986]],[[-68.187,44.332,-68.245,44.313,-68.309,44.321,-68.308,44.269,-68.315,44.25,-68.386,44.277,-68.412,44.294,-68.409,44.364,-68.347,44.43,-68.299,44.456,-68.238,44.438,-68.191,44.364,-68.187,44.332]],[[-74.188,40.523,-74.236,40.519,-74.188,40.615,-74.1,40.658,-74.069,40.649,-74.067,40.615,-74.08,40.586,-74.139,40.542,-74.188,40.523]],[[-70.51,41.376,-70.785,41.327,-70.829,41.359,-70.76,41.374,-70.674,41.449,-70.616,41.457,-70.525,41.415,-70.51,41.376]],[[-71.241,41.492,-71.291,41.465,-71.346,41.469,-71.318,41.506,-71.307,41.56,-71.28,41.62,-71.264,41.638,-71.232,41.654,-71.241,41.492]],[[-69.978,41.266,-70.055,41.249,-70.233,41.286,-70.087,41.318,-70.063,41.328,-70.044,41.374,-70.041,41.397,-69.986,41.299,-69.978,41.266]],[[-75.636,35.856,-75.651,35.836,-75.717,35.946,-75.649,35.91,-75.637,35.881,-75.636,35.856]],[[-75.544,35.24,-75.678,35.213,-75.69,35.222,-75.536,35.279,-75.488,35.479,-75.481,35.572,-75.504,35.735,-75.504,35.769,-75.479,35.717,-75.456,35.564,-75.465,35.449,-75.509,35.28,-75.544,35.24]],[[-75.782,35.19,-75.964,35.119,-75.984,35.123,-75.865,35.174,-75.782,35.19]],[[-76.504,34.643,-76.529,34.631,-76.437,34.756,-76.256,34.915,-76.207,34.939,-76.358,34.804,-76.504,34.643]],[[-82.037,26.454,-82.073,26.428,-82.145,26.447,-82.184,26.481,-82.201,26.548,-82.139,26.477,-82.116,26.461,-82.037,26.454]],[[-82.084,26.552,-82.085,26.494,-82.136,26.592,-82.169,26.701,-82.121,26.666,-82.084,26.552]],[[-80.382,25.142,-80.581,24.954,-80.559,25.001,-80.481,25.102,-80.456,25.149,-80.404,25.179,-80.355,25.234,-80.351,25.297,-80.28,25.341,-80.257,25.348,-80.382,25.142]],[[-91.794,29.501,-91.831,29.486,-91.996,29.573,-92.007,29.61,-91.925,29.644,-91.875,29.641,-91.796,29.597,-91.768,29.585,-91.754,29.567,-91.762,29.539,-91.794,29.501]],[[-97.014,27.902,-97.036,27.899,-96.988,27.981,-96.979,28.014,-96.899,28.117,-96.857,28.133,-96.84,28.089,-96.921,28.016,-97.014,27.902]],[[-96.764,28.153,-96.801,28.148,-96.756,28.202,-96.682,28.23,-96.519,28.333,-96.453,28.341,-96.419,28.376,-96.404,28.382,-96.413,28.338,-96.544,28.276,-96.764,28.153]],[[-97.354,27.3,-97.385,27.243,-97.376,27.328,-97.295,27.523,-97.13,27.779,-97.061,27.822,-97.251,27.541,-97.354,27.3]],[[-95.04,29.146,-95.09,29.136,-94.872,29.29,-94.826,29.341,-94.768,29.339,-94.865,29.253,-95.04,29.146]],[[-97.171,26.159,-97.185,26.113,-97.267,26.33,-97.402,26.821,-97.407,27.1,-97.386,27.196,-97.351,26.801,-97.202,26.3,-97.171,26.159]],[[-120.307,34.025,-120.36,34.022,-120.442,34.033,-120.413,34.056,-120.368,34.073,-120.353,34.061,-120.307,34.025]],[[-119.438,33.217,-119.483,33.215,-119.544,33.225,-119.575,33.278,-119.525,33.282,-119.479,33.275,-119.442,33.232,-119.438,33.217]],[[-118.35,32.828,-118.409,32.819,-118.473,32.839,-118.529,32.936,-118.59,33.011,-118.557,33.033,-118.507,32.96,-118.383,32.849,-118.35,32.828]],[[-120.044,33.919,-120.114,33.905,-120.167,33.918,-120.252,34.014,-120.072,34.027,-119.994,33.985,-119.984,33.973,-120.044,33.919]],[[-119.882,34.08,-119.679,34.028,-119.569,34.053,-119.549,34.028,-119.562,34.007,-119.81,33.968,-119.885,33.995,-119.892,34.032,-119.918,34.068,-119.882,34.08]],[[-118.348,33.386,-118.297,33.312,-118.37,33.321,-118.446,33.317,-118.469,33.357,-118.492,33.413,-118.507,33.427,-118.559,33.432,-118.563,33.437,-118.569,33.464,-118.555,33.477,-118.392,33.415,-118.348,33.386]],[[-122.782,48.673,-122.769,48.651,-122.809,48.63,-122.838,48.627,-122.883,48.661,-122.903,48.665,-122.887,48.612,-122.893,48.594,-122.986,48.627,-123.003,48.652,-122.977,48.679,-122.918,48.707,-122.898,48.71,-122.782,48.673]],[[-123.013,48.501,-122.987,48.468,-123.094,48.489,-123.14,48.508,-123.153,48.526,-123.17,48.587,-123.162,48.606,-123.114,48.613,-123.024,48.538,-123.013,48.501]],[[-122.573,48.157,-122.524,48.025,-122.503,48.08,-122.367,47.985,-122.367,47.939,-122.383,47.923,-122.411,47.918,-122.438,47.931,-122.462,47.964,-122.492,47.981,-122.558,47.992,-122.591,48.03,-122.603,48.055,-122.606,48.129,-122.623,48.151,-122.657,48.156,-122.69,48.174,-122.742,48.225,-122.749,48.239,-122.725,48.281,-122.669,48.351,-122.629,48.384,-122.604,48.381,-122.572,48.36,-122.536,48.321,-122.542,48.294,-122.692,48.241,-122.697,48.229,-122.624,48.214,-122.598,48.2,-122.573,48.157]],[[-71.365,41.485,-71.393,41.467,-71.403,41.515,-71.384,41.571,-71.364,41.572,-71.354,41.542,-71.365,41.485]],[[-74.133,39.681,-74.25,39.529,-74.253,39.558,-74.107,39.746,-74.133,39.681]],[[-75.333,37.888,-75.379,37.872,-75.226,38.072,-75.137,38.24,-75.098,38.298,-75.136,38.181,-75.203,38.072,-75.333,37.888]],[[-76.546,34.655,-76.569,34.653,-76.608,34.664,-76.662,34.685,-76.674,34.7,-76.622,34.695,-76.546,34.655]],[[-81.335,24.65,-81.365,24.63,-81.379,24.636,-81.379,24.666,-81.422,24.733,-81.42,24.75,-81.322,24.685,-81.32,24.668,-81.335,24.65]],[[-80.829,24.804,-80.848,24.804,-80.839,24.818,-80.799,24.846,-80.785,24.835,-80.787,24.821,-80.829,24.804]],[[-80.638,24.903,-80.665,24.898,-80.626,24.941,-80.615,24.938,-80.638,24.903]],[[-81.044,24.717,-81.09,24.693,-81.137,24.71,-81.085,24.734,-80.93,24.759,-80.989,24.728,-81.044,24.717]],[[-81.419,30.971,-81.463,30.728,-81.483,30.814,-81.485,30.898,-81.451,30.947,-81.419,30.971]],[[-81.567,24.6,-81.631,24.59,-81.579,24.629,-81.562,24.689,-81.532,24.642,-81.532,24.614,-81.567,24.6]],[[-80.187,27.278,-80.171,27.205,-80.262,27.376,-80.376,27.643,-80.437,27.851,-80.396,27.795,-80.356,27.679,-80.187,27.278]],[[-81.784,24.545,-81.809,24.542,-81.811,24.558,-81.768,24.577,-81.739,24.575,-81.74,24.554,-81.784,24.545]],[[-88.889,29.713,-88.944,29.66,-88.941,29.68,-88.901,29.733,-88.873,29.753,-88.889,29.713]],[[-88.558,30.216,-88.571,30.205,-88.659,30.226,-88.713,30.245,-88.723,30.264,-88.574,30.229,-88.558,30.216]],[[-88.071,30.252,-88.159,30.231,-88.29,30.233,-88.316,30.24,-88.264,30.255,-88.109,30.274,-88.071,30.252]],[[-89.224,30.084,-89.22,30.038,-89.269,30.061,-89.342,30.063,-89.31,30.079,-89.288,30.094,-89.276,30.111,-89.185,30.169,-89.211,30.126,-89.224,30.084]],[[-88.827,29.808,-88.856,29.776,-88.828,29.928,-88.867,30.057,-88.826,30,-88.813,29.933,-88.827,29.808]],[[-84.908,29.643,-85.008,29.607,-85.117,29.633,-85.049,29.638,-85.001,29.627,-84.877,29.679,-84.812,29.718,-84.737,29.732,-84.908,29.643]],[[-122.853,47.205,-122.863,47.185,-122.877,47.186,-122.908,47.226,-122.912,47.254,-122.885,47.275,-122.849,47.216,-122.853,47.205]],[[-122.394,47.395,-122.399,47.373,-122.437,47.355,-122.457,47.359,-122.458,47.386,-122.469,47.39,-122.51,47.358,-122.507,47.422,-122.486,47.489,-122.469,47.49,-122.442,47.446,-122.394,47.395]],[[-122.497,47.595,-122.503,47.575,-122.558,47.598,-122.576,47.619,-122.574,47.667,-122.56,47.698,-122.55,47.704,-122.517,47.691,-122.508,47.683,-122.497,47.595]],[[-122.821,48.431,-122.837,48.422,-122.89,48.435,-122.922,48.457,-122.932,48.485,-122.912,48.538,-122.885,48.552,-122.869,48.549,-122.862,48.502,-122.815,48.452,-122.821,48.431]],[[-68.623,44.196,-68.661,44.176,-68.702,44.183,-68.703,44.232,-68.691,44.249,-68.677,44.256,-68.656,44.242,-68.623,44.196]]]},{"n":"South Georgia and the South Sandwich Islands","c":"GS","r":4,"ml":5,"lx":-31.06,"ly":-55.68,"b":[-38.017,-58.492,-26.26,-53.984],"p":[[[-26.264,-58.435,-26.26,-58.492,-26.415,-58.44,-26.451,-58.415,-26.401,-58.383,-26.303,-58.382,-26.279,-58.402,-26.264,-58.435]],[[-37.103,-54.066,-37.006,-54.114,-36.929,-54.081,-36.849,-54.085,-36.805,-54.101,-36.76,-54.108,-36.704,-54.108,-36.607,-54.19,-36.647,-54.262,-36.541,-54.248,-36.449,-54.308,-36.407,-54.303,-36.386,-54.279,-36.326,-54.251,-36.285,-54.289,-36.236,-54.36,-36.173,-54.382,-36.117,-54.458,-36.073,-54.554,-36.033,-54.568,-35.965,-54.568,-35.895,-54.555,-35.922,-54.637,-35.913,-54.711,-35.799,-54.763,-35.867,-54.792,-35.939,-54.834,-36.085,-54.867,-36.124,-54.853,-36.252,-54.78,-36.311,-54.694,-36.446,-54.571,-36.472,-54.534,-36.507,-54.511,-36.628,-54.496,-36.735,-54.467,-36.824,-54.404,-36.852,-54.366,-36.886,-54.339,-37.007,-54.341,-37.083,-54.312,-37.158,-54.271,-37.498,-54.156,-37.631,-54.167,-37.692,-54.135,-37.689,-54.077,-37.619,-54.042,-37.913,-54.029,-38.017,-54.008,-37.946,-53.996,-37.536,-53.994,-37.382,-53.984,-37.369,-54.009,-37.233,-54.061,-37.103,-54.066]]]},{"n":"British Indian Ocean Territory","c":"IO","r":5,"ml":5,"lx":71.35,"ly":-6.19,"b":[72.35,-7.435,72.499,-7.22],"p":[[[72.492,-7.377,72.469,-7.417,72.429,-7.435,72.408,-7.334,72.35,-7.263,72.373,-7.263,72.427,-7.3,72.447,-7.396,72.467,-7.368,72.462,-7.338,72.474,-7.31,72.465,-7.278,72.436,-7.23,72.446,-7.22,72.494,-7.262,72.499,-7.295,72.492,-7.377]]]},{"n":"Saint Helena","c":"SH","r":6,"ml":5,"lx":-5.71,"ly":-15.95,"b":[-14.415,-16.004,-5.66,-7.883],"p":[[[-5.692,-15.998,-5.783,-16.004,-5.775,-15.957,-5.708,-15.906,-5.662,-15.913,-5.66,-15.971,-5.692,-15.998]],[[-14.364,-7.974,-14.399,-7.976,-14.409,-7.967,-14.415,-7.944,-14.399,-7.906,-14.384,-7.883,-14.36,-7.886,-14.329,-7.913,-14.303,-7.935,-14.317,-7.956,-14.364,-7.974]]]},{"n":"Pitcairn Islands","c":"PN","r":4,"ml":5,"lx":-128.32,"ly":-24.36,"b":[-128.35,-24.413,-128.29,-24.323],"p":[[[-128.29,-24.397,-128.3,-24.413,-128.321,-24.4,-128.342,-24.371,-128.35,-24.34,-128.33,-24.323,-128.304,-24.334,-128.291,-24.365,-128.29,-24.397]]]},{"n":"Anguilla","c":"AI","r":6,"ml":5,"lx":-63.03,"ly":18.24,"b":[-63.16,18.171,-62.98,18.27],"p":[[[-63.001,18.222,-63.16,18.171,-63.153,18.2,-63.026,18.27,-62.98,18.265,-63.001,18.222]]]},{"n":"Falkland Islands","c":"FK","r":5,"ml":4.5,"lx":-58.74,"ly":-51.61,"b":[-61.145,-52.308,-57.792,-51.27],"p":[[[-58.85,-51.27,-58.698,-51.329,-58.506,-51.308,-58.426,-51.324,-58.379,-51.373,-58.407,-51.418,-58.467,-51.412,-58.519,-51.424,-58.509,-51.484,-58.474,-51.509,-58.272,-51.575,-58.235,-51.579,-58.241,-51.551,-58.276,-51.506,-58.289,-51.458,-58.259,-51.417,-58.206,-51.405,-57.977,-51.384,-57.923,-51.404,-57.808,-51.518,-57.915,-51.534,-57.96,-51.583,-57.866,-51.605,-57.792,-51.636,-57.831,-51.685,-57.838,-51.709,-58.004,-51.743,-58.151,-51.765,-58.218,-51.822,-58.336,-51.864,-58.683,-51.936,-58.643,-51.995,-58.638,-52.023,-58.653,-52.099,-59.131,-52.008,-59.196,-52.018,-59.068,-52.173,-59.163,-52.202,-59.256,-52.183,-59.342,-52.196,-59.396,-52.308,-59.532,-52.236,-59.649,-52.134,-59.649,-52.077,-59.537,-51.971,-59.571,-51.925,-59.309,-51.78,-59.262,-51.737,-59.18,-51.712,-59.095,-51.704,-59.06,-51.685,-59.065,-51.65,-59.099,-51.59,-59.097,-51.491,-58.887,-51.358,-58.917,-51.272,-58.85,-51.27]],[[-60.286,-51.462,-60.142,-51.481,-60.009,-51.411,-59.917,-51.388,-59.842,-51.403,-59.788,-51.446,-59.711,-51.439,-59.493,-51.396,-59.465,-51.411,-59.388,-51.36,-59.321,-51.384,-59.268,-51.428,-59.294,-51.479,-59.354,-51.511,-59.392,-51.556,-59.437,-51.593,-59.514,-51.627,-59.573,-51.681,-59.715,-51.808,-59.921,-51.97,-59.99,-51.984,-60.132,-51.994,-60.194,-51.983,-60.246,-51.986,-60.288,-52.074,-60.353,-52.14,-60.384,-52.154,-60.452,-52.16,-60.484,-52.17,-60.508,-52.195,-60.686,-52.188,-60.812,-52.148,-60.961,-52.057,-60.762,-51.946,-60.591,-51.952,-60.45,-51.877,-60.334,-51.84,-60.289,-51.801,-60.238,-51.772,-60.238,-51.734,-60.277,-51.717,-60.328,-51.718,-60.38,-51.735,-60.5,-51.757,-60.583,-51.713,-60.528,-51.696,-60.467,-51.697,-60.281,-51.656,-60.245,-51.639,-60.303,-51.58,-60.415,-51.545,-60.506,-51.485,-60.523,-51.463,-60.518,-51.428,-60.568,-51.358,-60.516,-51.354,-60.445,-51.399,-60.286,-51.462]],[[-61.019,-51.786,-60.947,-51.8,-60.876,-51.794,-60.916,-51.897,-60.948,-51.946,-61.032,-51.942,-61.116,-51.875,-61.145,-51.839,-61.052,-51.814,-61.019,-51.786]],[[-58.439,-52.011,-58.433,-52.099,-58.513,-52.071,-58.541,-52.028,-58.497,-51.999,-58.461,-52.002,-58.439,-52.011]],[[-60.112,-51.396,-60.249,-51.396,-60.276,-51.363,-60.275,-51.281,-60.171,-51.273,-60.07,-51.308,-60.076,-51.343,-60.112,-51.396]],[[-59.683,-52.232,-59.747,-52.251,-59.764,-52.242,-59.785,-52.205,-59.786,-52.156,-59.793,-52.134,-59.753,-52.141,-59.681,-52.18,-59.683,-52.232]]]},{"n":"Cayman Islands","c":"KY","r":5,"ml":5,"lx":-81.24,"ly":19.32,"b":[-81.419,19.272,-79.742,19.766],"p":[[[-81.37,19.349,-81.337,19.329,-81.296,19.341,-81.285,19.363,-81.13,19.347,-81.107,19.305,-81.225,19.304,-81.277,19.277,-81.304,19.272,-81.405,19.278,-81.419,19.375,-81.391,19.385,-81.37,19.349]],[[-79.979,19.708,-79.989,19.703,-80.021,19.707,-80.094,19.666,-80.126,19.668,-80.116,19.683,-80.101,19.696,-80.084,19.706,-80.068,19.71,-80.016,19.718,-79.992,19.719,-79.975,19.71,-79.979,19.708]],[[-79.823,19.712,-79.87,19.697,-79.906,19.703,-79.824,19.744,-79.803,19.758,-79.785,19.766,-79.766,19.766,-79.742,19.757,-79.742,19.751,-79.823,19.712]]]},{"n":"Bermuda","c":"BM","r":6,"ml":4,"lx":-64.76,"ly":32.3,"b":[-64.863,32.26,-64.668,32.387],"p":[[[-64.73,32.293,-64.82,32.26,-64.845,32.262,-64.863,32.274,-64.771,32.308,-64.695,32.387,-64.668,32.382,-64.73,32.293]]]},{"n":"British Virgin Islands","c":"VG","r":6,"ml":5,"lx":-64.64,"ly":18.43,"b":[-64.695,18.399,-64.274,18.753],"p":[[[-64.395,18.465,-64.421,18.457,-64.438,18.459,-64.444,18.473,-64.426,18.513,-64.325,18.517,-64.395,18.465]],[[-64.594,18.403,-64.672,18.399,-64.695,18.412,-64.651,18.443,-64.569,18.446,-64.545,18.438,-64.594,18.403]],[[-64.288,18.741,-64.274,18.707,-64.282,18.708,-64.339,18.731,-64.383,18.733,-64.401,18.739,-64.411,18.751,-64.323,18.753,-64.288,18.741]]]},{"n":"Turks and Caicos Islands","c":"TC","r":6,"ml":5,"lx":-71.75,"ly":21.82,"b":[-72.342,21.752,-71.637,21.952],"p":[[[-71.661,21.765,-71.665,21.752,-71.722,21.79,-71.83,21.791,-71.848,21.843,-71.806,21.852,-71.668,21.833,-71.637,21.788,-71.661,21.765]],[[-71.88,21.84,-71.897,21.83,-71.955,21.864,-71.964,21.892,-71.985,21.893,-72.019,21.918,-72.011,21.95,-71.932,21.952,-71.9,21.863,-71.88,21.84]],[[-72.333,21.851,-72.219,21.796,-72.15,21.804,-72.144,21.793,-72.182,21.78,-72.191,21.77,-72.301,21.755,-72.335,21.758,-72.342,21.795,-72.333,21.851]]]},{"n":"Montserrat","c":"MS","r":6,"ml":5,"lx":-62.19,"ly":16.74,"b":[-62.223,16.681,-62.148,16.81],"p":[[[-62.148,16.74,-62.154,16.681,-62.222,16.7,-62.223,16.752,-62.191,16.804,-62.176,16.81,-62.148,16.74]]]},{"n":"Jersey","c":"JE","r":6,"ml":5,"lx":-2.09,"ly":49.22,"b":[-2.236,49.17,-2.01,49.266],"p":[[[-2.019,49.231,-2.01,49.181,-2.054,49.17,-2.091,49.187,-2.166,49.187,-2.236,49.176,-2.221,49.266,-2.082,49.255,-2.019,49.231]]]},{"n":"Guernsey","c":"GG","r":6,"ml":5,"lx":-2.56,"ly":49.46,"b":[-2.646,49.429,-2.512,49.507],"p":[[[-2.512,49.495,-2.547,49.429,-2.639,49.451,-2.646,49.468,-2.542,49.507,-2.521,49.506,-2.512,49.495]]]},{"n":"Isle of Man","c":"IM","r":6,"ml":5,"lx":-4.53,"ly":54.22,"b":[-4.785,54.059,-4.338,54.407],"p":[[[-4.412,54.185,-4.614,54.059,-4.696,54.081,-4.766,54.069,-4.785,54.073,-4.746,54.119,-4.699,54.225,-4.615,54.267,-4.509,54.377,-4.425,54.407,-4.396,54.403,-4.377,54.393,-4.338,54.269,-4.392,54.225,-4.412,54.185]]]},{"n":"United Kingdom","c":"GB","r":2,"ml":1.7,"lx":-2.12,"ly":54.4,"b":[-8.145,50.021,1.747,60.832],"p":[[[-2.668,51.623,-2.742,51.581,-2.979,51.539,-3.08,51.496,-3.259,51.398,-3.293,51.39,-3.562,51.414,-3.763,51.54,-3.891,51.592,-3.944,51.598,-3.998,51.582,-4.115,51.566,-4.235,51.569,-4.174,51.627,-4.091,51.66,-4.276,51.683,-4.328,51.7,-4.386,51.741,-4.531,51.748,-4.601,51.738,-4.718,51.684,-4.902,51.626,-5.125,51.706,-5.168,51.741,-5.167,51.808,-5.201,51.861,-5.262,51.88,-5.183,51.95,-5.088,51.996,-4.879,52.042,-4.561,52.151,-4.383,52.197,-4.218,52.277,-4.149,52.326,-4.1,52.393,-4.051,52.475,-3.98,52.542,-4.048,52.558,-4.079,52.608,-4.071,52.659,-4.039,52.704,-4.067,52.761,-4.118,52.82,-4.115,52.866,-4.101,52.915,-4.229,52.913,-4.356,52.897,-4.472,52.862,-4.584,52.815,-4.683,52.806,-4.681,52.844,-4.638,52.891,-4.526,52.958,-4.405,53.014,-4.362,53.056,-4.328,53.105,-4.269,53.145,-4.111,53.219,-3.809,53.303,-3.764,53.308,-3.646,53.298,-3.53,53.311,-3.428,53.341,-3.326,53.347,-3.098,53.26,-3.166,53.395,-3.065,53.427,-2.919,53.305,-2.864,53.293,-2.75,53.31,-2.794,53.331,-2.845,53.332,-2.913,53.35,-2.97,53.389,-3.065,53.513,-3.059,53.586,-2.996,53.663,-2.925,53.733,-2.984,53.747,-3.032,53.774,-3.045,53.844,-3.027,53.906,-2.9,53.961,-2.862,54.044,-2.846,54.135,-2.868,54.177,-2.994,54.171,-3.055,54.153,-3.11,54.126,-3.166,54.128,-3.322,54.229,-3.41,54.306,-3.569,54.468,-3.592,54.564,-3.465,54.773,-3.268,54.907,-3.036,54.953,-3.081,54.962,-3.434,54.964,-3.55,54.947,-3.658,54.893,-3.719,54.876,-3.783,54.87,-3.842,54.843,-3.899,54.805,-3.958,54.781,-4.076,54.787,-4.133,54.779,-4.174,54.801,-4.208,54.837,-4.253,54.847,-4.304,54.836,-4.41,54.787,-4.517,54.758,-4.648,54.789,-4.818,54.846,-4.852,54.825,-4.89,54.772,-4.911,54.689,-5.032,54.761,-5.135,54.858,-5.17,54.918,-5.173,54.986,-5.117,55.012,-5.056,54.988,-4.965,55.149,-4.785,55.359,-4.721,55.421,-4.677,55.501,-4.684,55.554,-4.724,55.598,-4.892,55.699,-4.89,55.781,-4.872,55.874,-4.826,55.93,-4.807,55.94,-4.584,55.939,-4.671,55.967,-4.844,56.051,-4.841,56.081,-4.8,56.158,-4.819,56.15,-4.856,56.115,-4.927,56.028,-4.97,56.008,-5.093,55.987,-5.115,55.945,-5.135,55.933,-5.196,55.929,-5.215,55.889,-5.228,55.886,-5.246,55.929,-5.247,56,-5.223,56.066,-5.176,56.117,-4.997,56.233,-5.084,56.197,-5.282,56.09,-5.383,56.019,-5.41,55.995,-5.419,55.975,-5.418,55.952,-5.373,55.828,-5.386,55.77,-5.556,55.39,-5.589,55.351,-5.618,55.331,-5.647,55.327,-5.731,55.334,-5.768,55.363,-5.768,55.395,-5.752,55.443,-5.681,55.624,-5.651,55.674,-5.605,55.721,-5.504,55.802,-5.507,55.808,-5.574,55.792,-5.602,55.797,-5.623,55.813,-5.61,56.055,-5.555,56.135,-5.535,56.251,-5.488,56.35,-5.433,56.422,-5.392,56.515,-5.329,56.556,-5.313,56.619,-5.243,56.687,-5.188,56.758,-5.218,56.751,-5.564,56.566,-5.652,56.532,-5.773,56.541,-5.865,56.562,-5.937,56.606,-5.969,56.69,-6.058,56.692,-6.134,56.707,-6.133,56.718,-6.035,56.764,-5.878,56.78,-5.731,56.853,-5.861,56.903,-5.85,56.918,-5.736,56.961,-5.591,57.102,-5.562,57.233,-5.631,57.294,-5.656,57.334,-5.795,57.379,-5.818,57.436,-5.802,57.468,-5.757,57.499,-5.689,57.524,-5.582,57.547,-5.679,57.572,-5.715,57.601,-5.742,57.644,-5.745,57.668,-5.695,57.778,-5.665,57.824,-5.608,57.881,-5.349,57.878,-5.319,57.904,-5.29,57.905,-5.157,57.881,-5.177,57.906,-5.394,58.044,-5.413,58.07,-5.351,58.144,-5.347,58.177,-5.356,58.212,-5.338,58.239,-5.27,58.251,-5.06,58.25,-5.008,58.263,-5.032,58.298,-5.081,58.345,-5.09,58.385,-5.079,58.419,-5.076,58.489,-5.067,58.52,-5.017,58.567,-4.976,58.58,-4.925,58.588,-4.81,58.573,-4.766,58.554,-4.715,58.51,-4.678,58.514,-4.535,58.562,-4.492,58.568,-4.433,58.513,-4.189,58.557,-3.86,58.577,-3.662,58.606,-3.454,58.617,-3.259,58.65,-3.053,58.635,-3.046,58.616,-3.057,58.589,-3.11,58.515,-3.101,58.434,-3.113,58.409,-3.137,58.378,-3.212,58.321,-3.411,58.24,-3.775,58.052,-3.99,57.959,-4.02,57.914,-4.036,57.852,-3.907,57.84,-3.857,57.819,-3.888,57.787,-4.078,57.677,-4.135,57.578,-3.988,57.581,-3.868,57.6,-3.628,57.662,-3.403,57.708,-3.295,57.71,-3.084,57.673,-3.036,57.672,-2.947,57.689,-2.856,57.692,-2.244,57.681,-2.074,57.702,-1.962,57.677,-1.867,57.612,-1.778,57.494,-1.781,57.474,-1.835,57.42,-1.934,57.352,-2.02,57.259,-2.046,57.209,-2.062,57.153,-2.09,57.103,-2.26,56.863,-2.427,56.731,-2.501,56.637,-2.593,56.562,-2.681,56.514,-2.775,56.483,-3.047,56.449,-3.124,56.425,-3.214,56.384,-3.31,56.363,-3.198,56.366,-3.087,56.389,-2.885,56.398,-2.653,56.318,-2.674,56.253,-2.768,56.202,-2.98,56.194,-3.178,56.08,-3.268,56.045,-3.362,56.028,-3.48,56.033,-3.695,56.063,-3.789,56.095,-3.704,56.043,-3.608,56.016,-3.049,55.952,-3.015,55.959,-2.837,56.026,-2.599,56.027,-2.147,55.903,-2.017,55.808,-1.83,55.672,-1.729,55.619,-1.655,55.57,-1.61,55.498,-1.523,55.26,-1.423,55.026,-1.292,54.774,-1.232,54.704,-1.154,54.654,-0.759,54.541,-0.671,54.504,-0.518,54.395,-0.37,54.279,-0.233,54.19,-0.084,54.118,-0.156,54.081,-0.206,54.022,-0.169,53.942,-0.108,53.865,0.011,53.743,0.115,53.609,0.077,53.629,0.036,53.641,-0.019,53.637,-0.074,53.644,-0.174,53.685,-0.27,53.737,-0.461,53.716,-0.568,53.725,-0.66,53.724,-0.485,53.694,-0.294,53.692,0.128,53.468,0.271,53.335,0.356,53.16,0.298,53.081,0.208,53.03,0.124,52.972,0.046,52.906,0.28,52.809,0.33,52.812,0.382,52.825,0.432,52.858,0.516,52.938,0.559,52.967,0.704,52.977,0.827,52.971,0.949,52.953,1.056,52.959,1.271,52.925,1.382,52.894,1.657,52.754,1.716,52.677,1.743,52.579,1.747,52.469,1.7,52.369,1.647,52.279,1.615,52.162,1.591,52.12,1.559,52.087,1.413,51.995,1.317,51.957,1.276,51.974,1.232,51.971,1.228,51.949,1.274,51.902,1.274,51.845,1.188,51.803,1.101,51.785,0.955,51.808,0.752,51.73,0.898,51.689,0.927,51.647,0.891,51.571,0.799,51.538,0.698,51.523,0.593,51.519,0.507,51.501,0.425,51.466,0.528,51.484,0.6,51.468,0.646,51.405,0.687,51.387,0.889,51.36,1.015,51.36,1.257,51.375,1.373,51.375,1.415,51.363,1.416,51.311,1.398,51.182,1.366,51.155,1.044,51.047,0.979,50.972,0.96,50.926,0.772,50.934,0.684,50.886,0.532,50.853,0.415,50.819,0.3,50.776,0.205,50.763,-0.204,50.814,-0.451,50.81,-0.785,50.765,-0.871,50.773,-1.001,50.816,-1.133,50.845,-1.285,50.857,-1.416,50.897,-1.334,50.821,-1.517,50.747,-1.601,50.733,-1.688,50.735,-1.866,50.715,-2.031,50.725,-2.006,50.673,-1.962,50.628,-1.998,50.608,-2.036,50.603,-2.35,50.637,-2.395,50.631,-2.433,50.599,-2.548,50.616,-2.659,50.67,-2.777,50.706,-2.901,50.722,-2.999,50.717,-3.405,50.632,-3.485,50.548,-3.526,50.428,-3.584,50.322,-3.68,50.24,-3.793,50.229,-3.9,50.286,-4.103,50.349,-4.173,50.391,-4.195,50.393,-4.217,50.378,-4.297,50.359,-4.379,50.358,-4.507,50.341,-4.728,50.29,-4.817,50.256,-5.01,50.161,-5.049,50.134,-5.119,50.038,-5.225,50.021,-5.323,50.083,-5.434,50.104,-5.551,50.083,-5.622,50.051,-5.655,50.077,-5.656,50.132,-5.571,50.197,-5.342,50.246,-5.142,50.374,-5.043,50.452,-5.004,50.495,-4.956,50.523,-4.894,50.534,-4.861,50.582,-4.583,50.776,-4.56,50.821,-4.546,50.901,-4.523,50.977,-4.296,51.027,-4.188,51.189,-4.158,51.201,-3.842,51.231,-3.608,51.229,-3.375,51.197,-3.256,51.194,-3.136,51.205,-3.042,51.249,-2.881,51.406,-2.791,51.475,-2.687,51.537,-2.59,51.609,-2.433,51.741,-2.539,51.695,-2.668,51.623]],[[-4.197,53.321,-4.155,53.303,-4.049,53.306,-4.084,53.264,-4.2,53.218,-4.279,53.172,-4.373,53.134,-4.419,53.178,-4.472,53.176,-4.553,53.26,-4.568,53.386,-4.462,53.419,-4.315,53.417,-4.197,53.321]],[[-2.549,59.231,-2.662,59.23,-2.604,59.289,-2.536,59.304,-2.407,59.298,-2.43,59.271,-2.549,59.231]],[[-1.043,60.514,-1.068,60.502,-1.166,60.604,-1.093,60.72,-1.006,60.717,-0.992,60.686,-1,60.658,-1.045,60.656,-1.049,60.647,-1.035,60.593,-1.034,60.53,-1.043,60.514]],[[-1.308,60.538,-1.287,60.467,-1.236,60.485,-1.158,60.418,-1.118,60.418,-1.052,60.444,-1.066,60.382,-1.134,60.207,-1.153,60.177,-1.166,60.124,-1.179,60.114,-1.199,60.007,-1.245,59.971,-1.284,59.887,-1.299,59.879,-1.356,59.911,-1.3,60.04,-1.276,60.115,-1.291,60.153,-1.323,60.188,-1.409,60.19,-1.481,60.173,-1.497,60.194,-1.499,60.222,-1.517,60.231,-1.613,60.229,-1.641,60.237,-1.66,60.262,-1.664,60.283,-1.577,60.298,-1.494,60.292,-1.375,60.333,-1.45,60.469,-1.549,60.481,-1.572,60.494,-1.553,60.517,-1.498,60.53,-1.414,60.599,-1.364,60.61,-1.302,60.608,-1.308,60.538]],[[-0.774,60.812,-0.774,60.8,-0.826,60.716,-0.825,60.684,-0.909,60.687,-0.922,60.697,-0.938,60.746,-0.928,60.797,-0.916,60.81,-0.891,60.816,-0.865,60.806,-0.823,60.832,-0.802,60.831,-0.774,60.812]],[[-3.165,58.794,-3.222,58.781,-3.279,58.782,-3.367,58.84,-3.401,58.882,-3.395,58.91,-3.357,58.919,-3.272,58.905,-3.228,58.857,-3.222,58.826,-3.212,58.814,-3.159,58.801,-3.165,58.794]],[[-2.929,58.742,-2.939,58.739,-2.975,58.757,-3.035,58.823,-2.941,58.836,-2.896,58.828,-2.913,58.8,-2.929,58.742]],[[-3.057,59.03,-3.071,59.005,-2.995,59.006,-2.885,58.985,-2.818,58.982,-2.762,58.956,-2.793,58.907,-2.826,58.893,-2.864,58.891,-2.995,58.939,-3.167,58.919,-3.201,58.925,-3.223,58.939,-3.233,58.956,-3.233,58.99,-3.242,59,-3.304,58.967,-3.332,58.971,-3.347,58.987,-3.354,59.019,-3.347,59.065,-3.31,59.131,-3.249,59.144,-3.156,59.136,-3.051,59.099,-3.019,59.076,-3.02,59.058,-3.057,59.03]],[[-2.729,59.187,-2.815,59.162,-2.852,59.182,-2.861,59.247,-2.964,59.274,-3.013,59.291,-3.052,59.324,-3.042,59.334,-2.976,59.347,-2.862,59.288,-2.815,59.241,-2.731,59.227,-2.72,59.219,-2.729,59.187]],[[-6.608,56.585,-6.664,56.579,-6.669,56.594,-6.57,56.661,-6.506,56.672,-6.484,56.666,-6.53,56.627,-6.608,56.585]],[[-5.105,55.449,-5.231,55.448,-5.277,55.457,-5.331,55.481,-5.393,55.618,-5.371,55.667,-5.346,55.691,-5.318,55.709,-5.252,55.717,-5.185,55.691,-5.16,55.667,-5.105,55.574,-5.095,55.494,-5.105,55.449]],[[-5.778,56.344,-6.176,56.289,-6.313,56.294,-6.326,56.321,-6.298,56.339,-6.185,56.357,-6.139,56.491,-6.311,56.552,-6.32,56.569,-6.306,56.599,-6.286,56.612,-6.182,56.643,-6.138,56.65,-6.103,56.646,-6.03,56.61,-5.947,56.535,-5.836,56.523,-5.761,56.491,-5.778,56.344]],[[-6.129,55.931,-6.093,55.802,-6.058,55.723,-6.055,55.695,-6.088,55.658,-6.253,55.607,-6.305,55.607,-6.307,55.619,-6.27,55.67,-6.302,55.728,-6.286,55.773,-6.302,55.781,-6.334,55.774,-6.452,55.704,-6.491,55.697,-6.496,55.712,-6.466,55.769,-6.463,55.808,-6.445,55.832,-6.413,55.855,-6.375,55.871,-6.344,55.874,-6.311,55.856,-6.216,55.905,-6.129,55.931]],[[-5.97,55.815,-5.991,55.804,-6.042,55.807,-6.06,55.823,-6.071,55.848,-6.072,55.893,-6.041,55.926,-5.912,55.975,-5.97,55.992,-5.973,56.004,-5.939,56.045,-5.8,56.109,-5.762,56.12,-5.725,56.119,-5.797,56.006,-5.97,55.815]],[[-6.199,58.363,-6.326,58.189,-6.376,58.185,-6.419,58.141,-6.555,58.093,-6.437,58.092,-6.403,58.076,-6.402,58.041,-6.425,58.021,-6.578,57.941,-6.683,57.911,-6.797,57.828,-6.854,57.827,-6.91,57.773,-6.957,57.75,-6.983,57.75,-7.013,57.762,-7.083,57.814,-6.956,57.865,-6.944,57.894,-6.857,57.924,-6.864,57.933,-7.003,57.975,-7.057,58.003,-7.052,58.018,-6.985,58.05,-7.017,58.055,-7.038,58.072,-7.077,58.079,-7.088,58.095,-7.096,58.138,-7.085,58.182,-7.045,58.202,-7.028,58.222,-7.012,58.229,-6.95,58.218,-6.886,58.183,-6.812,58.196,-6.726,58.189,-6.725,58.198,-6.788,58.284,-6.776,58.302,-6.742,58.322,-6.544,58.383,-6.297,58.487,-6.237,58.503,-6.219,58.489,-6.194,58.435,-6.199,58.363]],[[-6.279,56.965,-6.309,56.952,-6.346,56.954,-6.383,56.971,-6.433,57.018,-6.322,57.051,-6.278,57.031,-6.261,57.01,-6.261,56.985,-6.279,56.965]],[[-6.145,57.505,-6.146,57.461,-6.164,57.409,-6.141,57.354,-6.136,57.314,-6.093,57.302,-6.068,57.284,-5.88,57.263,-5.706,57.269,-5.672,57.253,-5.669,57.227,-5.696,57.198,-5.795,57.147,-5.914,57.063,-5.949,57.045,-5.987,57.044,-6.015,57.052,-6.034,57.201,-6.163,57.182,-6.266,57.184,-6.323,57.202,-6.362,57.238,-6.442,57.327,-6.675,57.363,-6.741,57.412,-6.761,57.442,-6.753,57.459,-6.704,57.496,-6.643,57.483,-6.606,57.491,-6.583,57.507,-6.583,57.521,-6.615,57.553,-6.617,57.563,-6.379,57.603,-6.358,57.667,-6.306,57.672,-6.247,57.651,-6.166,57.585,-6.145,57.505]],[[-7.206,57.683,-7.093,57.627,-7.183,57.533,-7.321,57.534,-7.515,57.602,-7.516,57.616,-7.499,57.636,-7.47,57.653,-7.44,57.656,-7.392,57.645,-7.325,57.663,-7.271,57.657,-7.206,57.683]],[[-7.25,57.115,-7.292,57.11,-7.347,57.115,-7.381,57.131,-7.416,57.192,-7.422,57.229,-7.407,57.298,-7.411,57.381,-7.296,57.384,-7.267,57.372,-7.248,57.126,-7.25,57.115]],[[-7.417,56.965,-7.505,56.952,-7.537,56.96,-7.543,56.972,-7.523,57.007,-7.455,57.019,-7.407,57,-7.399,56.983,-7.417,56.965]],[[-6.218,54.089,-6.304,54.095,-6.364,54.077,-6.403,54.061,-6.44,54.064,-6.548,54.057,-6.65,54.059,-6.664,54.085,-6.647,54.163,-6.67,54.185,-6.767,54.196,-6.803,54.214,-6.858,54.269,-6.869,54.294,-6.877,54.329,-6.936,54.374,-7.008,54.407,-7.05,54.408,-7.133,54.355,-7.203,54.302,-7.178,54.275,-7.155,54.24,-7.193,54.214,-7.307,54.156,-7.325,54.133,-7.355,54.121,-7.409,54.137,-7.544,54.134,-7.607,54.144,-7.679,54.187,-7.855,54.215,-7.884,54.284,-7.918,54.297,-8.118,54.414,-8.145,54.454,-8.119,54.477,-8.044,54.512,-7.794,54.571,-7.754,54.595,-7.746,54.616,-7.82,54.64,-7.886,54.666,-7.909,54.683,-7.911,54.698,-7.873,54.718,-7.797,54.719,-7.737,54.71,-7.69,54.728,-7.606,54.746,-7.55,54.768,-7.502,54.825,-7.451,54.877,-7.446,54.905,-7.401,55.003,-7.377,55.028,-7.219,55.092,-7.179,55.057,-7.101,55.048,-7.031,55.081,-6.947,55.183,-6.889,55.189,-6.825,55.181,-6.699,55.193,-6.475,55.241,-6.375,55.242,-6.234,55.217,-6.129,55.217,-6.036,55.145,-5.986,55.03,-5.869,54.916,-5.717,54.817,-5.711,54.757,-5.765,54.725,-5.879,54.684,-5.879,54.641,-5.803,54.663,-5.739,54.673,-5.583,54.663,-5.528,54.62,-5.49,54.554,-5.47,54.5,-5.484,54.442,-5.526,54.46,-5.569,54.513,-5.616,54.537,-5.671,54.55,-5.646,54.478,-5.656,54.382,-5.632,54.373,-5.558,54.371,-5.607,54.273,-5.708,54.246,-5.826,54.236,-5.855,54.201,-5.876,54.156,-5.938,54.089,-6.019,54.051,-6.12,54.059,-6.218,54.089]],[[-1.066,50.69,-1.149,50.656,-1.176,50.615,-1.196,50.599,-1.251,50.589,-1.306,50.589,-1.515,50.67,-1.563,50.666,-1.516,50.703,-1.386,50.734,-1.313,50.773,-1.144,50.735,-1.066,50.69]]]},{"n":"United Arab Emirates","c":"AE","r":4,"ml":4,"lx":54.55,"ly":23.47,"b":[51.568,22.621,56.388,26.068],"p":[[[56.298,25.651,56.363,25.569,56.373,25.018,56.388,24.979,56.353,24.973,56.314,24.931,56.268,24.867,56.205,24.833,56.154,24.796,56.107,24.749,56.064,24.739,56.008,24.798,55.97,24.859,55.98,24.872,56.006,24.876,56.017,24.908,56.001,24.953,55.963,24.97,55.916,24.972,55.871,24.951,55.823,24.911,55.796,24.868,55.792,24.781,55.804,24.684,55.804,24.636,55.778,24.577,55.768,24.491,55.787,24.424,55.804,24.384,55.806,24.35,55.761,24.243,55.8,24.223,55.929,24.215,55.966,24.143,55.992,24.093,55.985,24.063,55.894,24.041,55.779,24.017,55.697,24.024,55.548,23.991,55.468,23.941,55.492,23.91,55.519,23.885,55.532,23.819,55.508,23.725,55.466,23.633,55.414,23.519,55.353,23.387,55.27,23.19,55.2,23.035,55.192,22.923,55.194,22.85,55.186,22.704,55.119,22.624,55.104,22.621,55.025,22.631,54.998,22.634,54.922,22.644,54.805,22.658,54.652,22.677,54.472,22.699,54.27,22.723,54.055,22.75,53.832,22.777,53.61,22.804,53.394,22.83,53.192,22.855,53.012,22.877,52.859,22.896,52.742,22.91,52.666,22.919,52.639,22.923,52.555,22.933,52.51,22.987,52.455,23.052,52.4,23.118,52.345,23.183,52.29,23.249,52.235,23.314,52.179,23.38,52.125,23.445,52.069,23.511,52.014,23.576,51.959,23.642,51.904,23.708,51.849,23.773,51.794,23.838,51.739,23.904,51.684,23.97,51.629,24.035,51.593,24.079,51.572,24.128,51.568,24.258,51.568,24.286,51.605,24.338,51.623,24.301,51.665,24.25,51.735,24.263,51.768,24.254,51.792,24.075,51.843,24.011,51.906,23.985,52.119,23.971,52.251,23.995,52.511,24.113,52.648,24.155,53.026,24.147,53.33,24.098,53.802,24.069,53.893,24.077,54.148,24.171,54.304,24.254,54.397,24.278,54.458,24.358,54.499,24.463,54.535,24.531,54.58,24.564,54.624,24.621,54.659,24.716,54.747,24.81,55.098,25.042,55.304,25.237,55.322,25.3,55.433,25.394,55.523,25.498,55.941,25.794,56.025,25.916,56.075,26.053,56.08,26.063,56.117,26.068,56.167,26.047,56.173,25.945,56.154,25.848,56.152,25.746,56.145,25.691,56.184,25.645,56.25,25.625,56.279,25.628,56.298,25.651],[56.282,25.236,56.288,25.279,56.277,25.301,56.234,25.304,56.217,25.267,56.211,25.213,56.24,25.209,56.282,25.236]],[[53.332,24.259,53.258,24.253,53.191,24.291,53.333,24.342,53.371,24.364,53.412,24.411,53.445,24.371,53.409,24.308,53.383,24.281,53.332,24.259]],[[52.617,24.289,52.6,24.281,52.582,24.335,52.584,24.352,52.629,24.377,52.658,24.333,52.617,24.289]],[[53.928,24.177,53.928,24.143,53.826,24.153,53.799,24.136,53.716,24.145,53.634,24.17,53.69,24.211,53.834,24.259,53.894,24.215,53.928,24.177]],[[54.465,24.443,54.457,24.423,54.428,24.425,54.358,24.443,54.335,24.471,54.379,24.505,54.398,24.506,54.427,24.471,54.465,24.443]]]},{"n":"Ukraine","c":"UA","r":3,"ml":2.7,"lx":32.14,"ly":49.72,"b":[22.132,45.234,40.128,52.354],"p":[[[38.214,47.091,38.178,47.08,37.829,47.096,37.761,47.091,37.543,47.075,37.34,46.917,37.219,46.917,37.048,46.876,36.932,46.825,36.795,46.714,36.689,46.764,36.559,46.763,36.432,46.733,36.279,46.659,36.195,46.646,36.025,46.667,35.827,46.624,35.4,46.381,35.257,46.204,35.204,46.169,35.132,46.126,35.055,46.104,35.015,46.106,35.218,46.232,35.28,46.279,35.291,46.314,35.292,46.371,35.23,46.441,35.064,46.267,34.97,46.242,34.85,46.19,34.844,46.074,34.857,45.987,34.907,45.879,35.002,45.733,34.946,45.729,34.8,45.791,34.794,45.892,34.687,45.977,34.523,45.977,34.45,45.966,34.354,46.062,34.224,46.101,34.128,46.09,34.027,46.107,33.807,46.208,33.66,46.22,33.654,46.146,33.594,46.096,33.499,46.079,33.43,46.058,33.263,46.126,33.202,46.176,32.942,46.124,32.797,46.131,32.477,46.084,32.33,46.13,32.036,46.261,31.925,46.287,31.831,46.282,31.78,46.325,31.843,46.346,31.916,46.349,31.992,46.364,32.013,46.387,32.008,46.43,31.856,46.462,31.714,46.472,31.624,46.51,31.555,46.554,31.716,46.555,31.878,46.522,32.131,46.509,32.361,46.475,32.419,46.518,32.553,46.592,32.578,46.616,32.354,46.565,32.127,46.597,32.044,46.642,31.974,46.709,31.945,46.784,31.964,46.855,31.94,46.982,31.865,47.095,31.838,47.157,31.759,47.213,31.837,47.087,31.866,47.003,31.913,46.926,31.902,46.722,31.873,46.65,31.78,46.632,31.657,46.642,31.532,46.665,31.563,46.777,31.497,46.738,31.403,46.629,31.32,46.613,31.137,46.624,30.796,46.552,30.773,46.473,30.722,46.366,30.672,46.304,30.657,46.267,30.512,46.105,30.493,46.09,30.219,45.867,30.184,45.85,30.007,45.798,29.902,45.752,29.821,45.732,29.685,45.755,29.628,45.722,29.602,45.683,29.601,45.6,29.67,45.541,29.727,45.343,29.706,45.26,29.652,45.314,29.568,45.371,29.404,45.42,29.224,45.403,29.027,45.321,28.894,45.29,28.824,45.311,28.782,45.31,28.767,45.286,28.77,45.267,28.791,45.252,28.788,45.241,28.761,45.234,28.451,45.292,28.318,45.347,28.213,45.45,28.265,45.484,28.31,45.499,28.471,45.507,28.499,45.518,28.502,45.542,28.514,45.572,28.509,45.618,28.492,45.666,28.562,45.736,28.668,45.794,28.729,45.852,28.739,45.937,28.85,45.979,28.948,46.05,28.972,46.128,29.006,46.176,28.944,46.288,28.931,46.362,28.927,46.424,28.958,46.458,29.05,46.497,29.146,46.527,29.186,46.524,29.201,46.505,29.205,46.379,29.224,46.377,29.255,46.393,29.305,46.467,29.34,46.445,29.393,46.437,29.433,46.456,29.459,46.454,29.491,46.435,29.555,46.408,29.615,46.399,29.665,46.417,29.707,46.449,29.752,46.438,29.838,46.351,29.878,46.36,30.076,46.378,30.108,46.402,30.131,46.423,29.924,46.539,29.935,46.625,29.942,46.724,29.918,46.782,29.878,46.829,29.72,46.883,29.598,46.939,29.572,46.964,29.569,46.997,29.563,47.048,29.515,47.091,29.511,47.128,29.542,47.186,29.549,47.247,29.539,47.271,29.511,47.291,29.456,47.293,29.383,47.328,29.334,47.376,29.201,47.444,29.16,47.456,29.135,47.49,29.123,47.53,29.151,47.581,29.186,47.659,29.211,47.732,29.211,47.775,29.195,47.882,29.125,47.965,29.093,47.975,29.037,47.952,28.973,47.933,28.923,47.951,28.866,47.996,28.774,48.12,28.602,48.144,28.53,48.15,28.463,48.091,28.442,48.109,28.423,48.147,28.388,48.162,28.341,48.144,28.327,48.161,28.347,48.213,28.291,48.239,28.159,48.238,28.088,48.257,28.08,48.296,28.038,48.321,27.963,48.334,27.891,48.365,27.82,48.416,27.714,48.45,27.574,48.465,27.562,48.47,27.549,48.478,27.458,48.443,27.404,48.416,27.337,48.433,27.229,48.371,27.008,48.368,26.901,48.372,26.847,48.387,26.64,48.294,26.619,48.26,26.572,48.248,26.442,48.23,26.306,48.204,26.277,48.113,26.236,48.064,26.163,47.993,25.909,47.968,25.689,47.932,25.464,47.911,25.17,47.823,25.074,47.746,24.979,47.724,24.893,47.718,24.838,47.761,24.651,47.877,24.579,47.931,24.484,47.947,24.381,47.938,24.282,47.911,24.178,47.906,24.06,47.945,24.047,47.941,24.002,47.936,23.709,47.983,23.682,47.99,23.669,47.992,23.629,47.996,23.408,47.99,23.203,48.085,23.139,48.087,23.091,48.049,23.055,48.007,22.913,47.964,22.877,47.947,22.856,47.96,22.846,47.997,22.857,48.03,22.836,48.06,22.782,48.095,22.769,48.11,22.702,48.107,22.683,48.104,22.676,48.104,22.582,48.134,22.52,48.205,22.424,48.243,22.35,48.256,22.317,48.287,22.295,48.327,22.272,48.358,22.269,48.361,22.254,48.407,22.231,48.412,22.227,48.413,22.132,48.405,22.143,48.569,22.295,48.686,22.333,48.745,22.389,48.873,22.432,48.934,22.483,48.983,22.524,49.031,22.539,49.073,22.58,49.077,22.701,49.04,22.81,49.021,22.84,49.039,22.852,49.063,22.847,49.081,22.76,49.136,22.706,49.171,22.702,49.193,22.722,49.241,22.732,49.295,22.72,49.354,22.661,49.484,22.649,49.539,22.706,49.606,22.891,49.766,22.952,49.826,23.036,49.899,23.264,50.073,23.409,50.174,23.506,50.23,23.649,50.327,23.712,50.377,23.973,50.41,24.005,50.457,24.053,50.508,24.09,50.53,24.095,50.617,24.046,50.723,24.007,50.76,23.978,50.786,23.997,50.809,24.026,50.816,24.062,50.82,24.106,50.845,24.096,50.873,23.986,50.94,23.938,50.993,23.863,51.126,23.712,51.265,23.664,51.31,23.658,51.352,23.68,51.395,23.659,51.449,23.605,51.518,23.614,51.525,23.609,51.61,23.647,51.629,23.707,51.641,23.792,51.637,23.864,51.624,23.951,51.585,23.978,51.591,24.127,51.665,24.28,51.775,24.324,51.838,24.362,51.868,24.495,51.883,24.611,51.89,24.685,51.888,24.866,51.899,24.974,51.911,25.067,51.931,25.267,51.938,25.58,51.925,25.786,51.924,25.925,51.914,26.267,51.855,26.394,51.844,26.453,51.813,26.567,51.802,26.773,51.771,26.953,51.754,27.074,51.761,27.142,51.752,27.27,51.614,27.296,51.597,27.348,51.594,27.452,51.606,27.601,51.602,27.69,51.572,27.677,51.49,27.7,51.478,27.741,51.483,27.789,51.529,27.829,51.577,27.859,51.592,28.011,51.56,28.08,51.565,28.144,51.602,28.184,51.608,28.292,51.582,28.425,51.564,28.532,51.562,28.599,51.543,28.648,51.457,28.69,51.439,28.731,51.433,28.793,51.51,28.85,51.54,28.928,51.562,28.978,51.572,29.013,51.599,29.061,51.625,29.102,51.628,29.136,51.617,29.174,51.581,29.23,51.497,29.299,51.413,29.346,51.383,29.47,51.408,29.553,51.435,29.706,51.44,29.909,51.458,30.064,51.482,30.161,51.478,30.22,51.451,30.309,51.4,30.333,51.326,30.45,51.274,30.545,51.265,30.577,51.318,30.633,51.355,30.612,51.406,30.602,51.471,30.561,51.531,30.533,51.596,30.584,51.689,30.639,51.77,30.667,51.814,30.755,51.895,30.846,51.953,30.981,52.046,31.079,52.077,31.168,52.063,31.218,52.05,31.346,52.105,31.574,52.108,31.763,52.101,31.782,52.099,31.876,52.071,31.974,52.047,32.042,52.045,32.122,52.051,32.217,52.083,32.283,52.114,32.363,52.272,32.391,52.295,32.435,52.307,32.508,52.309,32.645,52.279,32.806,52.253,32.9,52.256,33.148,52.34,33.287,52.354,33.452,52.334,33.613,52.333,33.735,52.345,33.819,52.316,33.922,52.251,34.015,52.156,34.113,51.98,34.398,51.78,34.403,51.742,34.379,51.717,34.239,51.692,34.121,51.679,34.115,51.645,34.147,51.608,34.201,51.554,34.209,51.484,34.207,51.42,34.23,51.363,34.275,51.34,34.281,51.312,34.228,51.277,34.214,51.255,34.234,51.244,34.491,51.237,34.617,51.203,34.712,51.172,34.76,51.169,34.869,51.189,34.99,51.202,35.064,51.203,35.093,51.181,35.115,51.121,35.158,51.061,35.198,51.044,35.269,51.047,35.312,51.044,35.335,51.021,35.309,50.987,35.315,50.95,35.346,50.904,35.383,50.799,35.417,50.768,35.44,50.728,35.44,50.682,35.412,50.642,35.392,50.611,35.412,50.54,35.488,50.46,35.546,50.44,35.591,50.369,35.674,50.346,35.796,50.406,35.89,50.437,36.008,50.42,36.116,50.409,36.189,50.368,36.243,50.312,36.306,50.28,36.369,50.297,36.5,50.28,36.56,50.235,36.619,50.209,36.696,50.246,36.759,50.292,36.988,50.34,37.131,50.352,37.171,50.361,37.255,50.395,37.343,50.418,37.423,50.411,37.501,50.341,37.582,50.292,37.605,50.215,37.704,50.109,37.95,49.964,38.047,49.92,38.113,49.928,38.147,49.939,38.163,49.955,38.178,50.025,38.209,50.051,38.259,50.052,38.451,49.964,38.552,49.955,38.648,49.953,38.777,49.884,38.918,49.825,39.028,49.818,39.115,49.842,39.175,49.856,39.212,49.833,39.246,49.782,39.303,49.742,39.368,49.731,39.463,49.728,39.627,49.651,39.781,49.572,39.877,49.568,39.958,49.591,40.031,49.597,40.081,49.577,40.095,49.543,40.058,49.497,40.058,49.432,40.126,49.369,40.128,49.307,40.109,49.252,40.07,49.2,39.976,49.13,39.89,49.064,39.759,49.037,39.687,49.008,39.706,48.96,39.753,48.914,39.864,48.878,39.989,48.851,40.004,48.822,39.984,48.807,39.904,48.794,39.793,48.808,39.756,48.782,39.705,48.739,39.67,48.662,39.645,48.591,39.765,48.572,39.836,48.543,39.858,48.484,39.883,48.419,39.89,48.36,39.85,48.332,39.847,48.303,39.866,48.288,39.918,48.282,39.958,48.269,39.961,48.238,39.885,48.168,39.814,48.035,39.776,47.964,39.779,47.888,39.736,47.845,39.658,47.841,39.391,47.834,39.158,47.837,39.058,47.848,38.9,47.855,38.822,47.837,38.719,47.714,38.641,47.666,38.511,47.622,38.369,47.61,38.287,47.559,38.259,47.48,38.257,47.409,38.243,47.374,38.212,47.343,38.201,47.321,38.208,47.297,38.241,47.288,38.281,47.277,38.281,47.259,38.265,47.237,38.221,47.213,38.201,47.175,38.206,47.136,38.214,47.091]],[[32.012,46.204,32.15,46.155,32.009,46.168,31.7,46.214,31.564,46.258,31.529,46.307,31.509,46.373,31.585,46.303,31.638,46.273,32.012,46.204]]]},{"n":"Uganda","c":"UG","r":3,"ml":3,"lx":32.95,"ly":1.97,"b":[29.562,-1.47,34.978,4.22],"p":[[[33.903,-1.002,33.794,-1.002,33.616,-1.002,33.438,-1.002,33.261,-1.002,33.083,-1.002,32.905,-1.002,32.727,-1.002,32.55,-1.002,32.372,-1.002,32.194,-1.002,32.016,-1.002,31.839,-1.002,31.661,-1.002,31.483,-1.002,31.305,-1.002,31.128,-1.002,30.95,-1.002,30.845,-1.002,30.824,-0.999,30.809,-0.995,30.742,-1.008,30.673,-1.051,30.599,-1.07,30.52,-1.062,30.51,-1.067,30.47,-1.066,30.412,-1.063,30.36,-1.075,30.321,-1.113,30.28,-1.179,30.207,-1.254,30.15,-1.321,30.102,-1.369,29.991,-1.447,29.93,-1.47,29.9,-1.466,29.882,-1.452,29.847,-1.352,29.825,-1.336,29.61,-1.387,29.577,-1.388,29.58,-1.357,29.564,-1.121,29.562,-0.977,29.59,-0.887,29.606,-0.783,29.608,-0.691,29.648,-0.535,29.633,-0.442,29.684,-0.114,29.698,-0.06,29.718,0.098,29.75,0.147,29.778,0.166,29.815,0.264,29.885,0.419,29.934,0.499,29.924,0.674,29.932,0.793,29.943,0.819,30.047,0.864,30.183,0.973,30.24,1.103,30.321,1.185,30.478,1.239,30.943,1.683,31.159,1.922,31.253,2.045,31.256,2.088,31.274,2.146,31.236,2.191,31.191,2.232,31.176,2.27,31.138,2.289,31.082,2.288,31.045,2.316,31.004,2.369,30.962,2.403,30.83,2.4,30.729,2.455,30.73,2.53,30.77,2.678,30.847,2.847,30.851,2.894,30.84,2.933,30.821,2.968,30.787,3.001,30.754,3.042,30.779,3.163,30.828,3.283,30.868,3.342,30.906,3.409,30.895,3.464,30.839,3.491,30.868,3.544,30.929,3.634,31.048,3.725,31.152,3.786,31.222,3.786,31.357,3.738,31.48,3.68,31.547,3.678,31.629,3.701,31.798,3.803,31.839,3.77,31.888,3.709,31.942,3.608,32.048,3.561,32.099,3.529,32.136,3.52,32.156,3.528,32.197,3.608,32.246,3.651,32.336,3.706,32.535,3.75,32.677,3.763,32.737,3.773,32.838,3.798,32.997,3.88,33.154,3.775,33.324,3.754,33.489,3.755,33.54,3.787,33.568,3.812,33.742,3.985,33.976,4.22,34.132,3.889,34.186,3.87,34.178,3.841,34.165,3.813,34.267,3.733,34.393,3.692,34.438,3.651,34.442,3.606,34.399,3.413,34.407,3.358,34.448,3.163,34.523,3.12,34.589,2.925,34.723,2.842,34.742,2.818,34.773,2.723,34.814,2.62,34.847,2.596,34.866,2.59,34.906,2.48,34.883,2.418,34.914,2.23,34.964,2.062,34.978,1.862,34.978,1.774,34.976,1.72,34.965,1.643,34.941,1.599,34.898,1.556,34.851,1.489,34.81,1.417,34.784,1.381,34.804,1.273,34.799,1.245,34.788,1.231,34.727,1.214,34.649,1.185,34.602,1.156,34.535,1.102,34.482,1.042,34.411,0.867,34.293,0.731,34.273,0.686,34.161,0.605,34.112,0.505,34.081,0.382,34.037,0.295,33.943,0.174,33.921,-0.017,33.924,-0.398,33.9,-0.832,33.903,-1.002]]]},{"n":"Turkmenistan","c":"TM","r":4,"ml":3,"lx":58.68,"ly":39.86,"b":[52.494,35.171,66.629,42.778],"p":[[[53.11,38.803,53.1,38.756,53.046,38.897,53.019,39.053,53.053,39.097,53.092,39.094,53.055,39.038,53.11,38.803]],[[55.977,41.322,56.242,41.311,56.48,41.301,56.774,41.288,56.861,41.276,56.966,41.265,57.018,41.263,57.064,41.307,57.095,41.331,57.119,41.35,57.114,41.372,57.077,41.39,57.018,41.451,56.985,41.669,56.964,41.857,57.034,41.915,57.114,41.957,57.229,42.084,57.291,42.124,57.382,42.156,57.686,42.165,57.814,42.19,57.856,42.231,57.923,42.335,57.946,42.42,57.983,42.459,58.029,42.488,58.075,42.487,58.166,42.462,58.234,42.448,58.283,42.429,58.327,42.399,58.371,42.347,58.377,42.312,58.397,42.292,58.431,42.292,58.457,42.292,58.474,42.299,58.486,42.317,58.477,42.34,58.418,42.407,58.289,42.527,58.204,42.576,58.162,42.603,58.152,42.628,58.206,42.666,58.26,42.688,58.353,42.672,58.477,42.663,58.532,42.682,58.589,42.778,58.73,42.676,58.877,42.561,58.931,42.54,59.036,42.528,59.123,42.524,59.16,42.511,59.199,42.482,59.277,42.356,59.354,42.323,59.451,42.3,59.763,42.302,59.858,42.295,59.937,42.236,59.985,42.212,60.006,42.191,60.001,42.165,59.982,42.132,59.979,42.068,59.974,42.019,59.949,41.995,59.942,41.974,59.963,41.954,60.107,41.907,60.156,41.857,60.192,41.834,60.201,41.803,60.176,41.782,60.109,41.793,60.076,41.76,60.076,41.701,60.124,41.645,60.138,41.594,60.106,41.545,60.069,41.476,60.067,41.427,60.09,41.399,60.2,41.349,60.455,41.222,60.514,41.216,60.755,41.246,60.867,41.249,60.933,41.229,61.12,41.211,61.179,41.191,61.242,41.189,61.329,41.195,61.388,41.252,61.417,41.265,61.444,41.275,61.497,41.276,61.645,41.24,61.8,41.163,61.903,41.094,61.954,41.031,62.018,40.894,62.095,40.683,62.188,40.541,62.298,40.467,62.375,40.332,62.442,40.036,62.483,39.976,62.525,39.944,62.651,39.858,62.907,39.717,63.058,39.633,63.292,39.5,63.506,39.377,63.721,39.188,63.764,39.161,63.953,39.058,64.163,38.954,64.31,38.977,64.532,38.816,64.622,38.756,64.66,38.736,64.821,38.672,65.077,38.539,65.4,38.349,65.613,38.239,65.671,38.226,65.729,38.226,65.79,38.25,65.857,38.269,65.971,38.244,66.095,38.2,66.173,38.167,66.264,38.118,66.335,38.072,66.39,38.051,66.575,38.011,66.606,37.987,66.626,37.96,66.629,37.932,66.526,37.786,66.511,37.599,66.511,37.459,66.522,37.348,66.472,37.345,66.35,37.368,66.108,37.415,65.901,37.508,65.765,37.569,65.744,37.561,65.683,37.519,65.641,37.468,65.608,37.368,65.555,37.251,65.304,37.247,65.09,37.238,64.952,37.194,64.816,37.132,64.782,37.059,64.753,36.965,64.674,36.75,64.603,36.555,64.566,36.428,64.511,36.341,64.358,36.226,64.184,36.149,64.092,36.113,64.051,36.068,64.042,36.025,64.01,36.012,63.938,36.02,63.863,36.012,63.697,35.968,63.517,35.913,63.302,35.858,63.179,35.858,63.13,35.846,63.109,35.819,63.13,35.767,63.151,35.728,63.17,35.678,63.119,35.638,63.084,35.568,63.057,35.446,62.98,35.409,62.858,35.35,62.723,35.271,62.688,35.255,62.611,35.233,62.533,35.24,62.463,35.251,62.387,35.231,62.308,35.171,62.271,35.189,62.253,35.25,62.213,35.29,62.09,35.38,61.984,35.444,61.938,35.448,61.841,35.431,61.72,35.419,61.621,35.432,61.543,35.458,61.422,35.546,61.378,35.593,61.345,35.629,61.262,35.62,61.239,35.659,61.236,35.706,61.259,35.762,61.252,35.868,61.206,35.944,61.153,35.977,61.159,36,61.183,36.053,61.212,36.099,61.212,36.191,61.175,36.29,61.16,36.433,61.17,36.572,61.12,36.643,60.708,36.643,60.341,36.638,60.321,36.654,60.178,36.829,60.063,36.963,59.949,37.042,59.687,37.138,59.562,37.179,59.455,37.253,59.367,37.334,59.345,37.445,59.327,37.481,59.302,37.511,59.274,37.524,59.241,37.521,58.937,37.65,58.815,37.683,58.701,37.656,58.65,37.652,58.55,37.688,58.436,37.639,58.387,37.635,58.318,37.647,58.262,37.666,58.109,37.783,57.981,37.83,57.888,37.861,57.711,37.905,57.521,37.928,57.424,37.948,57.354,37.973,57.336,37.99,57.337,38.033,57.331,38.089,57.308,38.13,57.26,38.18,57.194,38.216,57.079,38.21,56.907,38.213,56.775,38.25,56.67,38.257,56.544,38.25,56.441,38.249,56.367,38.223,56.324,38.191,56.297,38.095,56.272,38.08,56.229,38.073,56.171,38.078,56.05,38.078,55.841,38.095,55.578,38.1,55.381,38.051,55.225,37.981,55.076,37.902,54.9,37.778,54.849,37.723,54.745,37.502,54.699,37.47,54.64,37.445,54.579,37.44,54.459,37.408,54.3,37.354,54.192,37.332,53.914,37.344,53.898,37.414,53.848,37.67,53.824,37.928,53.825,38.047,53.854,38.286,53.852,38.406,53.84,38.515,53.852,38.622,53.874,38.742,53.885,38.864,53.869,38.949,53.815,39.018,53.724,39.103,53.71,39.153,53.705,39.21,53.618,39.216,53.539,39.274,53.475,39.306,53.336,39.341,53.267,39.343,53.203,39.317,53.157,39.265,53.124,39.347,53.125,39.432,53.236,39.609,53.305,39.557,53.39,39.536,53.497,39.533,53.603,39.547,53.582,39.607,53.533,39.642,53.472,39.669,53.45,39.749,53.458,39.831,53.487,39.909,53.454,39.941,53.404,39.96,53.289,39.958,53.139,39.979,52.988,39.988,52.952,39.895,53.036,39.774,52.965,39.834,52.898,39.913,52.805,40.054,52.744,40.22,52.734,40.399,52.785,40.547,52.85,40.686,52.889,40.863,52.943,41.038,52.998,40.96,53.06,40.89,53.145,40.825,53.192,40.809,53.333,40.783,53.424,40.793,53.52,40.831,53.615,40.819,53.694,40.746,53.764,40.666,53.87,40.649,54.089,40.707,54.193,40.72,54.283,40.694,54.33,40.689,54.377,40.693,54.336,40.765,54.319,40.835,54.374,40.871,54.547,40.832,54.657,40.858,54.685,40.873,54.71,40.891,54.723,40.951,54.718,41.013,54.704,41.071,54.671,41.122,54.592,41.194,54.285,41.364,54.181,41.432,54.095,41.519,54.04,41.643,53.995,41.773,53.954,41.868,53.846,42.091,53.805,42.118,53.752,42.129,53.625,42.136,53.496,42.12,53.285,42.082,53.164,42.094,53.108,42.07,52.97,41.976,52.905,41.896,52.815,41.712,52.883,41.653,52.882,41.614,52.83,41.342,52.862,41.21,52.85,41.2,52.826,41.231,52.747,41.365,52.609,41.529,52.494,41.78,52.697,41.944,52.871,42.061,53.013,42.131,53.056,42.148,53.25,42.206,53.501,42.258,53.685,42.297,53.926,42.33,54.005,42.336,54.121,42.335,54.215,42.304,54.272,42.28,54.473,42.18,54.678,42.078,54.854,41.965,54.904,41.919,54.932,41.864,54.952,41.81,55.102,41.639,55.162,41.56,55.25,41.458,55.32,41.408,55.388,41.347,55.434,41.296,55.487,41.272,55.545,41.263,55.679,41.279,55.839,41.311,55.935,41.324,55.977,41.322]]]},{"n":"Turkey","c":"TR","r":2,"ml":2,"lx":34.51,"ly":39.35,"b":[25.669,35.831,44.817,42.093],"p":[[[25.97,40.136,25.74,40.105,25.669,40.136,25.741,40.196,25.875,40.234,25.918,40.238,25.977,40.178,25.97,40.136]],[[41.51,41.517,41.577,41.497,41.702,41.472,41.779,41.441,41.824,41.432,41.926,41.496,42.078,41.494,42.211,41.487,42.28,41.475,42.364,41.454,42.466,41.44,42.508,41.47,42.567,41.559,42.59,41.571,42.607,41.579,42.682,41.586,42.754,41.579,42.788,41.564,42.822,41.492,42.907,41.467,43.057,41.353,43.149,41.307,43.171,41.288,43.141,41.265,43.153,41.236,43.205,41.199,43.279,41.185,43.359,41.19,43.402,41.177,43.433,41.156,43.442,41.126,43.439,41.107,43.455,41.065,43.517,41.005,43.592,40.968,43.632,40.929,43.696,40.794,43.723,40.72,43.713,40.648,43.668,40.574,43.569,40.482,43.594,40.444,43.616,40.393,43.608,40.357,43.678,40.239,43.71,40.167,43.683,40.15,43.666,40.126,43.792,40.07,43.942,40.023,44.005,40.014,44.178,40.036,44.289,40.04,44.4,39.996,44.56,39.888,44.734,39.746,44.768,39.704,44.783,39.685,44.817,39.65,44.782,39.651,44.725,39.682,44.587,39.769,44.517,39.731,44.456,39.667,44.389,39.422,44.335,39.396,44.24,39.397,44.124,39.405,44.044,39.393,44.023,39.377,44.034,39.351,44.058,39.311,44.074,39.26,44.079,39.218,44.121,39.181,44.178,39.145,44.181,39.108,44.172,39.056,44.159,39.017,44.145,38.994,44.171,38.934,44.232,38.863,44.272,38.836,44.257,38.701,44.28,38.641,44.298,38.558,44.291,38.42,44.299,38.386,44.32,38.375,44.376,38.37,44.431,38.357,44.45,38.334,44.45,38.318,44.381,38.255,44.373,38.21,44.349,38.146,44.329,38.109,44.268,38.039,44.229,37.967,44.211,37.908,44.223,37.88,44.336,37.872,44.398,37.829,44.561,37.745,44.59,37.71,44.545,37.658,44.546,37.636,44.567,37.609,44.577,37.56,44.573,37.506,44.574,37.435,44.604,37.424,44.715,37.357,44.794,37.29,44.797,37.27,44.758,37.217,44.767,37.156,44.765,37.142,44.731,37.165,44.669,37.174,44.606,37.176,44.566,37.158,44.496,37.111,44.402,37.058,44.326,37.011,44.282,36.978,44.246,36.983,44.217,37.012,44.202,37.052,44.208,37.203,44.192,37.25,44.156,37.283,44.114,37.302,44.065,37.312,44.013,37.314,43.94,37.269,43.836,37.224,43.676,37.227,43.568,37.236,43.516,37.245,43.307,37.315,43.263,37.317,43.185,37.345,43.092,37.367,42.937,37.325,42.869,37.335,42.775,37.372,42.741,37.362,42.635,37.249,42.456,37.129,42.359,37.109,42.313,37.23,42.269,37.277,42.248,37.282,42.203,37.297,42.168,37.289,42.06,37.206,41.887,37.156,41.744,37.126,41.516,37.089,41.34,37.071,41.265,37.069,41.102,37.086,40.959,37.109,40.816,37.108,40.706,37.098,40.45,37.009,40.016,36.826,39.687,36.739,39.501,36.702,39.357,36.682,39.108,36.681,38.906,36.695,38.767,36.693,38.689,36.715,38.578,36.789,38.444,36.862,38.384,36.879,38.306,36.893,38.192,36.902,37.907,36.795,37.818,36.766,37.72,36.744,37.524,36.678,37.436,36.643,37.327,36.647,37.187,36.656,37.066,36.653,36.985,36.702,36.942,36.758,36.777,36.793,36.659,36.803,36.628,36.778,36.597,36.701,36.547,36.506,36.538,36.457,36.641,36.264,36.637,36.234,36.562,36.224,36.477,36.221,36.421,36.203,36.375,36.171,36.348,36.004,36.249,35.973,36.202,35.938,36.154,35.834,36.127,35.831,35.968,35.91,35.893,35.917,35.957,35.998,35.887,36.159,35.811,36.31,35.883,36.406,36.032,36.523,36.188,36.659,36.188,36.743,36.18,36.807,36.135,36.852,36.049,36.911,35.905,36.848,35.802,36.778,35.734,36.764,35.661,36.724,35.626,36.653,35.537,36.597,35.393,36.575,35.176,36.635,34.943,36.726,34.811,36.799,34.704,36.817,34.601,36.784,34.3,36.604,34.023,36.341,33.955,36.295,33.695,36.182,33.523,36.144,33.442,36.153,33.1,36.103,32.929,36.096,32.795,36.036,32.534,36.101,32.378,36.184,32.284,36.268,32.131,36.449,32.022,36.535,31.778,36.613,31.353,36.801,31.241,36.822,30.95,36.849,30.644,36.866,30.582,36.797,30.558,36.526,30.506,36.451,30.484,36.31,30.446,36.27,30.387,36.243,30.295,36.288,30.232,36.307,30.083,36.249,29.789,36.168,29.689,36.157,29.348,36.259,29.224,36.324,29.143,36.397,29.116,36.52,29.066,36.59,29.058,36.638,29.038,36.693,28.97,36.715,28.896,36.674,28.817,36.675,28.718,36.701,28.484,36.804,28.304,36.812,28.196,36.686,28.112,36.646,28.019,36.634,28.014,36.67,28.084,36.751,27.804,36.736,27.656,36.675,27.54,36.684,27.454,36.712,27.467,36.746,27.555,36.759,27.631,36.787,27.934,36.809,28.005,36.832,28.083,36.92,28.224,36.996,28.242,37.029,28.134,37.029,27.668,37.007,27.349,37.02,27.311,36.982,27.263,36.977,27.25,37.079,27.3,37.127,27.368,37.122,27.535,37.164,27.52,37.249,27.401,37.307,27.376,37.341,27.29,37.349,27.219,37.389,27.204,37.491,27.148,37.604,27.068,37.658,27.078,37.688,27.224,37.725,27.255,37.882,27.232,37.979,27.159,37.987,26.944,38.063,26.879,38.055,26.807,38.138,26.683,38.198,26.621,38.176,26.582,38.149,26.525,38.162,26.428,38.214,26.333,38.242,26.291,38.277,26.344,38.37,26.416,38.368,26.43,38.441,26.372,38.562,26.378,38.624,26.441,38.641,26.514,38.629,26.587,38.557,26.61,38.487,26.595,38.419,26.641,38.352,26.674,38.336,26.696,38.405,26.727,38.419,26.77,38.388,26.861,38.373,27.099,38.416,27.144,38.452,26.97,38.448,26.907,38.482,26.838,38.558,26.795,38.626,26.788,38.66,26.764,38.71,26.79,38.736,26.909,38.776,27.014,38.887,26.97,38.919,26.92,38.934,26.866,38.923,26.815,38.961,26.808,39.014,26.849,39.057,26.854,39.116,26.719,39.261,26.682,39.292,26.711,39.34,26.813,39.419,26.911,39.517,26.899,39.55,26.827,39.563,26.484,39.521,26.351,39.484,26.113,39.467,26.096,39.521,26.101,39.569,26.155,39.657,26.15,39.873,26.181,39.99,26.313,40.025,26.475,40.197,26.738,40.4,27.012,40.396,27.122,40.452,27.285,40.456,27.314,40.415,27.333,40.376,27.476,40.32,27.728,40.329,27.789,40.351,27.849,40.382,27.732,40.481,27.769,40.51,27.875,40.513,27.99,40.489,27.995,40.467,27.964,40.435,27.929,40.38,27.963,40.37,28.289,40.403,28.63,40.376,28.739,40.391,29.007,40.39,29.055,40.424,28.974,40.467,28.895,40.482,28.841,40.503,28.788,40.534,28.958,40.631,29.054,40.649,29.508,40.708,29.845,40.738,29.849,40.76,29.801,40.76,29.365,40.809,29.26,40.847,29.114,40.938,29.082,40.963,29.046,41.008,29.067,41.102,29.094,41.177,29.148,41.221,29.322,41.228,29.919,41.151,30.345,41.197,30.81,41.085,31.255,41.108,31.347,41.158,31.458,41.32,32.086,41.589,32.306,41.73,32.542,41.806,32.947,41.892,33.285,42.005,33.381,42.018,34.193,41.964,34.75,41.957,35.006,42.063,35.155,42.028,35.141,41.99,35.114,41.957,35.122,41.891,35.209,41.794,35.298,41.729,35.558,41.634,35.92,41.714,35.978,41.705,36.052,41.683,36.179,41.427,36.278,41.336,36.405,41.275,36.51,41.263,36.587,41.327,36.647,41.353,36.778,41.363,36.992,41.275,37.066,41.184,37.431,41.114,37.766,41.079,37.91,41.002,38.381,40.925,38.557,40.937,38.852,41.018,39.426,41.106,39.808,40.983,39.911,40.966,40,40.977,40.128,40.943,40.265,40.961,40.688,41.107,40.82,41.19,40.959,41.212,41.084,41.261,41.414,41.424,41.51,41.517]],[[28.014,41.969,27.987,41.855,28.05,41.729,28.198,41.554,28.346,41.466,28.947,41.248,29.057,41.23,29.032,41.14,28.996,41.061,28.956,41.008,28.78,40.974,28.295,41.071,28.172,41.081,28.086,41.061,27.925,40.991,27.747,41.013,27.499,40.973,27.43,40.84,27.258,40.687,26.975,40.564,26.772,40.498,26.468,40.261,26.33,40.123,26.272,40.097,26.203,40.075,26.226,40.142,26.26,40.202,26.252,40.248,26.254,40.315,26.355,40.39,26.447,40.445,26.72,40.544,26.792,40.627,26.578,40.625,26.361,40.606,26.224,40.618,26.105,40.611,26.068,40.683,26.039,40.727,26.07,40.74,26.109,40.75,26.179,40.827,26.241,40.883,26.331,40.954,26.354,40.997,26.354,41.037,26.333,41.064,26.328,41.097,26.326,41.143,26.331,41.239,26.536,41.343,26.602,41.354,26.625,41.402,26.61,41.512,26.581,41.601,26.545,41.607,26.495,41.633,26.463,41.663,26.411,41.696,26.321,41.717,26.318,41.745,26.327,41.773,26.36,41.802,26.511,41.826,26.529,41.847,26.55,41.897,26.58,41.948,26.615,41.965,26.679,41.963,26.8,41.975,26.885,41.992,26.969,42.027,27.012,42.059,27.193,42.077,27.244,42.093,27.295,42.08,27.363,42.025,27.475,41.947,27.535,41.921,27.58,41.933,27.661,41.961,27.739,41.962,27.802,41.957,27.832,41.981,27.879,41.987,28.014,41.969]]]},{"n":"Tunisia","c":"TN","r":3,"ml":3,"lx":9.01,"ly":33.69,"b":[7.496,30.229,11.536,37.34],"p":[[[11.505,33.182,11.502,33.156,11.467,32.966,11.459,32.897,11.454,32.782,11.454,32.643,11.534,32.525,11.536,32.473,11.505,32.414,11.358,32.345,11.168,32.257,11.005,32.173,10.826,32.081,10.772,32.021,10.683,31.975,10.609,31.93,10.596,31.886,10.544,31.803,10.476,31.736,10.306,31.705,10.275,31.685,10.196,31.585,10.16,31.546,10.115,31.464,10.173,31.251,10.243,31.032,10.257,30.941,10.256,30.865,10.216,30.783,10.126,30.666,10.06,30.58,9.933,30.425,9.895,30.387,9.807,30.342,9.638,30.282,9.519,30.229,9.458,30.465,9.406,30.667,9.363,30.833,9.288,31.125,9.224,31.374,9.16,31.621,9.102,31.846,9.044,32.072,9.019,32.105,8.844,32.212,8.683,32.31,8.515,32.422,8.333,32.544,8.304,32.696,8.211,32.927,8.113,33.055,8.076,33.089,7.877,33.172,7.763,33.233,7.731,33.269,7.709,33.362,7.628,33.549,7.534,33.718,7.5,33.832,7.496,33.977,7.514,34.081,7.554,34.125,7.749,34.254,7.838,34.41,7.949,34.469,8.046,34.513,8.123,34.564,8.193,34.646,8.246,34.734,8.255,34.829,8.277,34.979,8.312,35.085,8.394,35.204,8.36,35.3,8.316,35.403,8.329,35.582,8.318,35.655,8.283,35.719,8.247,35.802,8.246,35.871,8.28,36.051,8.307,36.189,8.349,36.368,8.334,36.418,8.303,36.456,8.209,36.495,8.208,36.519,8.231,36.545,8.37,36.633,8.444,36.761,8.507,36.788,8.601,36.834,8.598,36.884,8.577,36.937,8.824,36.998,9.059,37.156,9.142,37.195,9.688,37.34,9.759,37.33,9.838,37.309,9.816,37.255,9.784,37.211,9.83,37.135,9.896,37.182,9.879,37.213,9.876,37.254,9.988,37.258,10.087,37.251,10.196,37.206,10.189,37.034,10.334,36.865,10.293,36.781,10.412,36.732,10.518,36.791,10.571,36.879,10.766,36.93,10.951,37.059,11.054,37.073,11.077,36.967,11.127,36.874,11.057,36.841,10.967,36.743,10.798,36.493,10.642,36.42,10.526,36.323,10.488,36.255,10.477,36.175,10.506,36.032,10.591,35.887,10.689,35.8,10.784,35.772,11.004,35.634,11.001,35.552,11.032,35.454,11.043,35.335,11.12,35.24,10.956,35.034,10.866,34.884,10.691,34.678,10.535,34.545,10.2,34.346,10.118,34.28,10.065,34.212,10.04,34.14,10.049,34.056,10.159,33.85,10.305,33.728,10.454,33.663,10.713,33.689,10.704,33.61,10.723,33.514,10.828,33.519,10.898,33.534,10.958,33.626,11.085,33.563,11.15,33.369,11.257,33.309,11.27,33.286,11.232,33.272,11.203,33.249,11.234,33.234,11.338,33.209,11.401,33.225,11.505,33.182]],[[11.278,34.754,11.124,34.682,11.153,34.745,11.255,34.82,11.281,34.802,11.278,34.754]],[[10.958,33.722,10.931,33.717,10.883,33.69,10.857,33.687,10.785,33.718,10.757,33.717,10.722,33.739,10.734,33.856,10.745,33.889,10.922,33.893,11.018,33.823,11.034,33.805,11.038,33.785,10.993,33.746,10.958,33.722]]]},{"n":"Trinidad and Tobago","c":"TT","r":5,"ml":4.5,"lx":-60.92,"ly":11,"b":[-61.906,10.065,-60.525,11.325],"p":[[[-60.756,11.179,-60.811,11.169,-60.804,11.208,-60.709,11.277,-60.563,11.324,-60.525,11.325,-60.546,11.264,-60.756,11.179]],[[-61.012,10.134,-61.174,10.078,-61.597,10.065,-61.772,10.085,-61.906,10.069,-61.661,10.192,-61.633,10.243,-61.529,10.253,-61.499,10.269,-61.465,10.539,-61.478,10.603,-61.499,10.639,-61.541,10.664,-61.635,10.699,-61.651,10.718,-61.592,10.748,-61.465,10.764,-61.37,10.797,-61.174,10.803,-61.079,10.832,-60.918,10.84,-60.997,10.716,-61.034,10.67,-61.019,10.558,-61.037,10.482,-61.016,10.386,-60.968,10.323,-61,10.261,-61.004,10.168,-61.012,10.134]]]},{"n":"Tonga","c":"TO","r":4,"ml":4,"lx":-175.16,"ly":-21.21,"b":[-175.362,-21.451,-173.922,-18.565],"p":[[[-175.162,-21.169,-175.148,-21.169,-175.132,-21.14,-175.078,-21.129,-175.084,-21.161,-175.157,-21.264,-175.202,-21.223,-175.335,-21.158,-175.362,-21.107,-175.318,-21.068,-175.323,-21.099,-175.3,-21.113,-175.225,-21.119,-175.158,-21.146,-175.2,-21.156,-175.162,-21.169]],[[-173.954,-18.639,-173.991,-18.699,-174.009,-18.698,-174.053,-18.663,-174.069,-18.64,-174.002,-18.571,-173.968,-18.565,-173.922,-18.589,-173.924,-18.608,-173.954,-18.639]],[[-174.913,-21.3,-174.919,-21.451,-174.968,-21.382,-174.973,-21.35,-174.923,-21.303,-174.913,-21.3]]]},{"n":"Togo","c":"TG","r":6,"ml":5,"lx":1.06,"ly":8.81,"b":[-0.09,6.089,1.778,11.116],"p":[[[0.9,10.993,0.875,10.886,0.822,10.753,0.788,10.71,0.763,10.387,0.78,10.36,0.792,10.352,0.958,10.242,1.176,10.098,1.33,9.997,1.343,9.963,1.345,9.75,1.347,9.568,1.379,9.463,1.386,9.362,1.424,9.285,1.566,9.137,1.6,9.05,1.604,8.771,1.607,8.559,1.625,8.271,1.625,8.03,1.625,7.726,1.625,7.369,1.625,6.997,1.531,6.992,1.582,6.877,1.591,6.772,1.603,6.738,1.578,6.687,1.599,6.61,1.639,6.582,1.743,6.426,1.778,6.295,1.611,6.251,1.623,6.217,1.311,6.147,1.187,6.089,1.185,6.145,1.14,6.155,1.084,6.174,1.05,6.203,1.002,6.269,0.985,6.32,0.912,6.329,0.822,6.386,0.737,6.453,0.707,6.519,0.715,6.549,0.702,6.581,0.673,6.593,0.596,6.742,0.548,6.802,0.526,6.851,0.533,6.888,0.523,6.939,0.538,6.98,0.579,7.004,0.592,7.034,0.596,7.097,0.62,7.227,0.635,7.354,0.591,7.389,0.537,7.399,0.51,7.435,0.499,7.495,0.5,7.547,0.605,7.728,0.584,8.146,0.599,8.21,0.647,8.253,0.688,8.304,0.686,8.355,0.616,8.48,0.483,8.575,0.415,8.653,0.379,8.722,0.373,8.759,0.453,8.814,0.489,8.851,0.493,8.895,0.46,8.974,0.466,9.115,0.497,9.221,0.529,9.358,0.526,9.398,0.448,9.48,0.405,9.491,0.371,9.486,0.289,9.432,0.26,9.426,0.242,9.442,0.233,9.464,0.262,9.496,0.252,9.536,0.275,9.571,0.327,9.587,0.343,9.604,0.273,9.621,0.265,9.645,0.27,9.668,0.29,9.672,0.312,9.671,0.324,9.688,0.335,9.804,0.343,9.845,0.352,9.925,0.363,10.236,0.379,10.269,0.381,10.292,0.332,10.307,0.216,10.391,0.148,10.455,0.089,10.521,0.039,10.564,-0.058,10.631,-0.086,10.673,-0.09,10.716,-0.061,10.801,-0.014,10.891,0.009,11.021,-0.005,11.056,-0.069,11.116,0.159,11.07,0.484,10.992,0.491,10.978,0.493,10.955,0.549,10.955,0.643,10.983,0.9,10.993]]]},{"n":"East Timor","c":"TL","r":5,"ml":4,"lx":125.85,"ly":-8.8,"b":[124.036,-9.512,127.296,-8.14],"p":[[[125.646,-8.14,125.579,-8.312,125.507,-8.275,125.584,-8.179,125.621,-8.15,125.646,-8.14]],[[125.068,-9.512,125.034,-9.382,124.997,-9.326,124.968,-9.294,124.959,-9.255,124.96,-9.214,124.978,-9.195,125.1,-9.19,125.149,-9.123,125.149,-9.043,125.124,-9.015,125.1,-9.004,124.973,-9.064,124.937,-9.053,124.915,-9.032,124.922,-8.942,125.027,-8.859,125.116,-8.708,125.178,-8.648,125.323,-8.591,125.382,-8.575,125.804,-8.492,125.905,-8.487,126.173,-8.489,126.531,-8.471,126.62,-8.459,126.735,-8.423,126.846,-8.377,126.905,-8.342,126.966,-8.316,127.058,-8.348,127.215,-8.373,127.257,-8.395,127.296,-8.425,127.115,-8.584,126.915,-8.715,126.792,-8.755,126.665,-8.782,126.569,-8.833,126.487,-8.913,126.383,-8.958,126.265,-8.973,126.164,-8.997,126.073,-9.044,125.946,-9.124,125.895,-9.132,125.84,-9.13,125.735,-9.161,125.408,-9.276,125.21,-9.404,125.068,-9.512]],[[124.036,-9.342,124.198,-9.256,124.444,-9.19,124.438,-9.239,124.413,-9.314,124.376,-9.35,124.319,-9.414,124.282,-9.428,124.135,-9.414,124.116,-9.423,124.09,-9.416,124.052,-9.375,124.036,-9.342]]]},{"n":"Thailand","c":"TH","r":3,"ml":2.7,"lx":101.07,"ly":15.46,"b":[97.374,5.637,105.641,20.424],"p":[[[102.606,11.677,102.59,11.572,102.533,11.615,102.546,11.668,102.569,11.692,102.606,11.677]],[[102.427,11.989,102.43,11.965,102.378,11.983,102.36,11.974,102.302,11.981,102.273,12.119,102.277,12.152,102.319,12.142,102.378,12.073,102.408,12.025,102.427,11.989]],[[98.409,7.902,98.398,7.828,98.357,7.829,98.316,7.782,98.296,7.776,98.262,7.926,98.301,8.136,98.322,8.166,98.351,8.111,98.435,8.086,98.399,7.965,98.409,7.902]],[[100.122,20.317,100.115,20.258,100.14,20.245,100.174,20.273,100.218,20.34,100.266,20.377,100.318,20.386,100.373,20.34,100.432,20.241,100.492,20.184,100.52,20.178,100.54,20.132,100.543,20.089,100.515,19.996,100.466,19.889,100.398,19.756,100.42,19.644,100.514,19.553,100.625,19.5,100.744,19.515,100.807,19.542,100.858,19.585,100.906,19.605,100.967,19.611,101.155,19.579,101.212,19.548,101.221,19.487,101.198,19.328,101.227,19.212,101.28,19.089,101.286,18.977,101.221,18.793,101.166,18.618,101.106,18.534,101.06,18.479,101.047,18.442,101.051,18.407,101.093,18.355,101.138,18.287,101.149,18.222,101.144,18.143,101.113,18.034,100.999,17.797,100.908,17.584,100.956,17.541,101.046,17.51,101.105,17.48,101.167,17.499,101.3,17.625,101.414,17.719,101.555,17.812,101.564,17.821,101.688,17.889,101.744,17.953,101.775,18.033,101.819,18.065,101.875,18.046,101.947,18.081,102.035,18.17,102.101,18.211,102.148,18.204,102.232,18.149,102.352,18.046,102.459,17.985,102.553,17.965,102.598,17.927,102.596,17.87,102.617,17.833,102.661,17.818,102.68,17.824,102.675,17.852,102.718,17.892,102.807,17.946,102.899,17.977,102.991,17.986,103.051,18.029,103.091,18.138,103.149,18.222,103.2,18.259,103.263,18.278,103.28,18.305,103.249,18.339,103.252,18.373,103.288,18.408,103.367,18.423,103.488,18.418,103.63,18.383,103.792,18.317,103.899,18.295,103.95,18.319,104.049,18.217,104.196,17.988,104.323,17.816,104.428,17.699,104.539,17.609,104.656,17.547,104.74,17.462,104.816,17.3,104.759,17.077,104.744,16.884,104.751,16.648,104.819,16.466,104.95,16.34,105.026,16.238,105.047,16.16,105.149,16.094,105.331,16.038,105.406,15.987,105.376,15.942,105.373,15.89,105.399,15.83,105.462,15.78,105.562,15.741,105.622,15.7,105.641,15.657,105.639,15.586,105.616,15.488,105.574,15.413,105.513,15.361,105.506,15.32,105.49,15.257,105.49,15.128,105.533,15.042,105.547,14.932,105.523,14.843,105.5,14.661,105.497,14.591,105.476,14.53,105.423,14.472,105.342,14.417,105.244,14.368,105.183,14.346,105.169,14.336,105.126,14.281,105.074,14.227,105.034,14.227,105.003,14.254,104.982,14.289,104.97,14.366,104.879,14.404,104.779,14.428,104.576,14.39,104.412,14.37,104.228,14.396,104.054,14.363,103.982,14.358,103.899,14.363,103.818,14.362,103.742,14.374,103.6,14.421,103.546,14.417,103.432,14.379,103.313,14.351,103.199,14.333,103.031,14.253,102.909,14.137,102.873,14.055,102.813,13.972,102.729,13.842,102.62,13.717,102.545,13.66,102.566,13.626,102.547,13.586,102.429,13.568,102.336,13.56,102.32,13.54,102.331,13.288,102.363,13.193,102.423,13.078,102.462,13.015,102.491,12.828,102.5,12.67,102.63,12.57,102.703,12.494,102.756,12.426,102.737,12.383,102.706,12.256,102.737,12.09,102.918,11.732,102.934,11.707,102.912,11.704,102.884,11.773,102.792,11.889,102.763,12.012,102.655,12.149,102.594,12.203,102.575,12.158,102.54,12.109,102.434,12.179,102.343,12.253,102.259,12.394,102.248,12.361,102.23,12.332,102.134,12.443,102.034,12.532,101.945,12.564,101.889,12.593,101.836,12.64,101.724,12.689,101.445,12.619,101.09,12.674,100.954,12.621,100.898,12.654,100.863,12.715,100.896,12.818,100.904,13.035,100.946,13.187,100.926,13.303,100.947,13.358,100.963,13.432,100.907,13.462,100.656,13.521,100.603,13.568,100.536,13.514,100.236,13.484,100.122,13.44,100.017,13.353,99.991,13.243,100.051,13.171,100.09,13.046,99.982,12.771,99.964,12.69,100.006,12.355,99.989,12.171,99.93,12.047,99.837,11.937,99.799,11.749,99.725,11.662,99.627,11.463,99.561,11.215,99.514,11.101,99.487,10.89,99.285,10.569,99.237,10.388,99.165,10.32,99.19,10.266,99.195,10.175,99.169,9.934,99.161,9.734,99.191,9.627,99.288,9.415,99.265,9.353,99.254,9.265,99.335,9.225,99.394,9.214,99.724,9.314,99.836,9.288,99.878,9.195,99.905,9.113,99.961,8.671,99.99,8.589,100.056,8.511,100.129,8.428,100.154,8.443,100.159,8.474,100.163,8.508,100.229,8.425,100.279,8.269,100.454,7.442,100.504,7.337,100.545,7.227,100.439,7.281,100.411,7.464,100.38,7.542,100.343,7.553,100.284,7.552,100.28,7.584,100.324,7.644,100.317,7.716,100.257,7.775,100.158,7.728,100.161,7.599,100.205,7.501,100.371,7.28,100.424,7.188,100.49,7.161,100.586,7.176,100.702,7.082,100.793,6.995,101.018,6.861,101.154,6.875,101.302,6.908,101.401,6.9,101.498,6.865,101.614,6.754,101.799,6.475,102.101,6.242,102.068,6.185,102.055,6.097,101.936,5.979,101.917,5.911,101.874,5.825,101.791,5.779,101.72,5.771,101.678,5.779,101.65,5.796,101.601,5.877,101.577,5.902,101.556,5.908,101.404,5.852,101.257,5.789,101.23,5.734,101.191,5.669,101.148,5.643,101.114,5.637,101.082,5.675,101.025,5.725,100.982,5.771,100.993,5.846,101.076,5.956,101.087,6.034,101.076,6.166,101.054,6.243,101.029,6.245,100.989,6.258,100.874,6.245,100.817,6.332,100.794,6.426,100.754,6.46,100.716,6.481,100.629,6.448,100.564,6.468,100.345,6.55,100.261,6.683,100.217,6.687,100.177,6.672,100.161,6.642,100.138,6.489,100.119,6.442,99.869,6.75,99.696,6.877,99.72,7.106,99.668,7.151,99.602,7.155,99.553,7.219,99.597,7.356,99.529,7.329,99.435,7.334,99.359,7.372,99.3,7.561,99.264,7.619,99.183,7.718,99.078,7.718,99.043,7.766,99.051,7.888,98.974,7.963,98.872,8.024,98.789,8.06,98.704,8.257,98.636,8.305,98.579,8.344,98.5,8.318,98.474,8.247,98.421,8.178,98.361,8.187,98.305,8.226,98.238,8.423,98.227,8.544,98.242,8.768,98.326,8.969,98.371,9.291,98.443,9.493,98.493,9.561,98.562,9.838,98.703,10.19,98.718,10.266,98.747,10.351,98.768,10.431,98.775,10.557,98.757,10.624,98.757,10.661,98.787,10.708,98.887,10.788,99.025,10.92,99.19,11.105,99.359,11.389,99.443,11.554,99.478,11.613,99.515,11.631,99.573,11.687,99.613,11.75,99.615,11.781,99.523,12.09,99.463,12.19,99.432,12.309,99.416,12.395,99.394,12.474,99.405,12.548,99.372,12.594,99.297,12.653,99.22,12.74,99.174,12.882,99.174,12.961,99.124,13.031,99.107,13.104,99.137,13.173,99.176,13.233,99.172,13.497,99.156,13.576,99.137,13.717,99.086,13.823,99.015,13.947,98.934,14.05,98.721,14.236,98.57,14.36,98.495,14.473,98.4,14.603,98.332,14.696,98.246,14.815,98.202,14.976,98.178,15.147,98.191,15.204,98.232,15.241,98.286,15.272,98.329,15.279,98.452,15.357,98.537,15.351,98.557,15.368,98.565,15.404,98.554,15.56,98.558,15.769,98.574,15.939,98.592,16.051,98.818,16.181,98.866,16.237,98.888,16.298,98.888,16.352,98.869,16.394,98.835,16.418,98.689,16.305,98.661,16.33,98.594,16.515,98.565,16.571,98.523,16.638,98.478,16.732,98.471,16.895,98.439,16.976,98.257,17.148,98.175,17.24,98.063,17.373,97.929,17.533,97.793,17.681,97.729,17.776,97.706,17.797,97.699,17.834,97.74,17.935,97.72,18.037,97.652,18.174,97.622,18.258,97.632,18.29,97.599,18.303,97.524,18.296,97.451,18.36,97.381,18.494,97.374,18.518,97.397,18.518,97.485,18.494,97.515,18.498,97.577,18.529,97.672,18.561,97.728,18.572,97.746,18.588,97.754,18.621,97.706,18.932,97.714,18.996,97.804,19.13,97.794,19.266,97.817,19.46,97.916,19.593,97.991,19.654,98.015,19.75,98.049,19.77,98.111,19.762,98.239,19.691,98.294,19.687,98.371,19.689,98.455,19.694,98.494,19.701,98.761,19.771,98.82,19.778,98.876,19.77,98.917,19.773,98.958,19.805,98.987,19.861,99.021,20.042,99.04,20.074,99.074,20.099,99.131,20.117,99.197,20.115,99.284,20.08,99.338,20.079,99.399,20.093,99.452,20.118,99.486,20.15,99.502,20.188,99.488,20.261,99.448,20.352,99.459,20.363,99.532,20.343,99.639,20.32,99.72,20.325,99.773,20.341,99.825,20.384,99.89,20.424,99.954,20.415,100.004,20.38,100.122,20.317]],[[100.074,9.697,100.064,9.68,100.026,9.712,99.998,9.748,99.983,9.794,100.043,9.792,100.073,9.749,100.074,9.697]],[[100.071,9.586,100.075,9.529,100.054,9.461,99.962,9.422,99.931,9.476,99.94,9.56,99.954,9.581,100.043,9.577,100.071,9.586]],[[99.663,6.522,99.644,6.516,99.607,6.597,99.654,6.714,99.701,6.571,99.663,6.522]],[[98.592,7.934,98.58,7.917,98.529,8.109,98.604,8.057,98.592,7.934]],[[98.308,9.051,98.251,9.041,98.258,9.095,98.274,9.13,98.301,9.139,98.313,9.08,98.308,9.051]],[[99.078,7.592,99.104,7.471,99.068,7.496,99.038,7.548,99.038,7.626,99.045,7.637,99.078,7.592]]]},{"n":"Tanzania","c":"TZ","r":3,"ml":3,"lx":34.96,"ly":-6.05,"b":[29.323,-11.716,40.464,-0.995],"p":[[[39.496,-6.175,39.573,-6.387,39.563,-6.427,39.509,-6.452,39.481,-6.454,39.447,-6.42,39.424,-6.348,39.383,-6.365,39.313,-6.279,39.243,-6.275,39.182,-6.173,39.206,-6.083,39.192,-5.931,39.267,-5.853,39.309,-5.722,39.357,-5.812,39.368,-5.951,39.433,-6.115,39.488,-6.166,39.496,-6.175]],[[39.865,-4.906,39.871,-4.957,39.856,-5.004,39.859,-5.155,39.853,-5.255,39.796,-5.394,39.749,-5.444,39.708,-5.429,39.673,-5.407,39.647,-5.369,39.701,-5.114,39.673,-4.927,39.781,-4.945,39.865,-4.906]],[[32.92,-9.407,32.863,-9.381,32.757,-9.322,32.608,-9.271,32.487,-9.213,32.433,-9.156,32.319,-9.135,32.221,-9.126,32.13,-9.073,32.035,-9.067,31.943,-9.054,31.922,-9.019,31.919,-8.942,31.886,-8.922,31.818,-8.902,31.745,-8.903,31.7,-8.914,31.674,-8.909,31.613,-8.863,31.556,-8.805,31.535,-8.713,31.449,-8.654,31.351,-8.607,31.076,-8.612,31.033,-8.598,30.968,-8.551,30.892,-8.474,30.831,-8.386,30.777,-8.266,30.751,-8.194,30.721,-8.104,30.654,-7.971,30.559,-7.782,30.486,-7.627,30.407,-7.461,30.375,-7.339,30.313,-7.204,30.213,-7.038,30.162,-6.973,30.106,-6.915,29.962,-6.803,29.798,-6.692,29.71,-6.617,29.591,-6.394,29.541,-6.314,29.506,-6.172,29.48,-6.025,29.491,-5.965,29.596,-5.776,29.607,-5.723,29.594,-5.651,29.542,-5.5,29.504,-5.401,29.476,-5.317,29.42,-5.176,29.343,-4.983,29.323,-4.899,29.326,-4.836,29.368,-4.669,29.404,-4.497,29.403,-4.449,29.718,-4.456,29.77,-4.418,29.947,-4.307,30.147,-4.085,30.187,-3.993,30.269,-3.85,30.348,-3.78,30.379,-3.731,30.4,-3.654,30.425,-3.589,30.53,-3.492,30.632,-3.419,30.625,-3.389,30.611,-3.366,30.626,-3.347,30.682,-3.309,30.79,-3.275,30.811,-3.201,30.811,-3.116,30.794,-3.069,30.797,-3.015,30.78,-2.985,30.709,-2.977,30.604,-2.935,30.515,-2.918,30.456,-2.893,30.433,-2.875,30.424,-2.824,30.441,-2.769,30.45,-2.753,30.473,-2.694,30.434,-2.659,30.424,-2.642,30.442,-2.613,30.534,-2.426,30.554,-2.4,30.593,-2.397,30.657,-2.374,30.715,-2.363,30.763,-2.372,30.798,-2.363,30.829,-2.338,30.855,-2.265,30.877,-2.143,30.865,-2.044,30.819,-1.967,30.807,-1.851,30.828,-1.694,30.813,-1.563,30.762,-1.459,30.711,-1.397,30.632,-1.367,30.508,-1.208,30.47,-1.131,30.477,-1.083,30.51,-1.067,30.52,-1.062,30.599,-1.07,30.673,-1.051,30.742,-1.008,30.809,-0.995,30.824,-0.999,30.845,-1.002,30.95,-1.002,31.128,-1.002,31.305,-1.002,31.483,-1.002,31.661,-1.002,31.839,-1.002,32.016,-1.002,32.194,-1.002,32.372,-1.002,32.55,-1.002,32.727,-1.002,32.905,-1.002,33.083,-1.002,33.261,-1.002,33.438,-1.002,33.616,-1.002,33.794,-1.002,33.903,-1.002,33.979,-1.002,34.052,-1.04,34.132,-1.085,34.345,-1.204,34.558,-1.323,34.771,-1.442,34.984,-1.561,35.197,-1.68,35.411,-1.799,35.624,-1.918,35.837,-2.037,36.05,-2.156,36.263,-2.275,36.476,-2.394,36.689,-2.513,36.903,-2.632,37.116,-2.751,37.329,-2.87,37.542,-2.989,37.644,-3.045,37.659,-3.07,37.677,-3.178,37.688,-3.246,37.682,-3.306,37.625,-3.407,37.609,-3.46,37.608,-3.497,37.622,-3.512,37.67,-3.517,37.711,-3.541,37.726,-3.56,37.757,-3.636,37.797,-3.674,37.887,-3.739,38.041,-3.85,38.194,-3.96,38.348,-4.071,38.501,-4.181,38.655,-4.292,38.808,-4.402,38.962,-4.513,39.115,-4.624,39.19,-4.677,39.222,-4.692,39.202,-4.776,39.123,-4.98,39.119,-5.065,39.088,-5.165,39.058,-5.232,38.978,-5.519,38.911,-5.626,38.819,-5.878,38.805,-6.07,38.855,-6.205,38.874,-6.331,38.981,-6.455,39.067,-6.499,39.125,-6.556,39.228,-6.685,39.287,-6.815,39.472,-6.879,39.546,-7.024,39.519,-7.124,39.433,-7.207,39.353,-7.341,39.288,-7.518,39.287,-7.788,39.33,-7.747,39.428,-7.813,39.441,-8.012,39.34,-8.243,39.309,-8.351,39.304,-8.444,39.377,-8.721,39.488,-8.862,39.48,-8.906,39.451,-8.943,39.641,-9.192,39.625,-9.409,39.697,-9.578,39.728,-9.725,39.775,-9.837,39.784,-9.915,39.725,-10,39.864,-10.022,39.945,-10.092,39.984,-10.16,40.084,-10.157,40.138,-10.203,40.216,-10.241,40.389,-10.354,40.436,-10.41,40.453,-10.443,40.464,-10.464,40.347,-10.552,40.166,-10.687,39.989,-10.821,39.817,-10.912,39.694,-10.955,39.563,-10.979,39.439,-11.035,39.322,-11.123,39.171,-11.167,38.988,-11.167,38.795,-11.229,38.603,-11.345,38.492,-11.413,38.315,-11.311,38.177,-11.279,38.017,-11.282,37.92,-11.295,37.885,-11.317,37.855,-11.379,37.829,-11.482,37.725,-11.581,37.542,-11.675,37.373,-11.71,37.218,-11.687,37.114,-11.647,37.059,-11.592,36.979,-11.567,36.873,-11.571,36.771,-11.61,36.674,-11.684,36.519,-11.716,36.306,-11.706,36.191,-11.671,36.175,-11.609,36.082,-11.537,35.911,-11.455,35.785,-11.453,35.705,-11.532,35.631,-11.582,35.564,-11.602,35.504,-11.605,35.451,-11.59,35.418,-11.583,35.183,-11.575,34.959,-11.578,34.953,-11.544,34.937,-11.463,34.891,-11.394,34.851,-11.352,34.801,-11.341,34.774,-11.342,34.752,-11.309,34.726,-11.238,34.688,-11.177,34.638,-11.127,34.608,-11.08,34.598,-11.037,34.606,-10.99,34.652,-10.873,34.667,-10.792,34.662,-10.71,34.637,-10.626,34.584,-10.525,34.59,-10.496,34.572,-10.428,34.57,-10.38,34.58,-10.32,34.57,-10.241,34.524,-10.073,34.524,-10.03,34.476,-9.949,34.328,-9.757,34.321,-9.732,34.089,-9.538,33.996,-9.495,33.962,-9.532,33.95,-9.565,33.959,-9.627,33.954,-9.658,33.944,-9.672,33.889,-9.67,33.854,-9.663,33.766,-9.611,33.698,-9.598,33.528,-9.608,33.468,-9.62,33.421,-9.608,33.331,-9.519,33.225,-9.5,33.13,-9.496,32.974,-9.395,32.937,-9.4,32.92,-9.407]],[[39.711,-7.977,39.657,-7.991,39.636,-7.978,39.603,-7.936,39.661,-7.901,39.717,-7.832,39.847,-7.73,39.891,-7.663,39.907,-7.649,39.898,-7.728,39.824,-7.901,39.762,-7.912,39.711,-7.977]]]},{"n":"Tajikistan","c":"TJ","r":4,"ml":4,"lx":72.59,"ly":38.2,"b":[67.35,36.684,75.119,41.035],"p":[[[67.759,37.172,67.798,37.245,67.814,37.487,67.864,37.571,68.011,37.721,68.088,37.835,68.174,37.928,68.237,37.96,68.294,38.033,68.341,38.117,68.354,38.17,68.35,38.211,68.333,38.238,68.251,38.295,68.144,38.383,68.087,38.474,68.056,38.589,68.048,38.669,68.149,38.891,68.133,38.928,68.104,38.962,68.044,38.984,67.96,38.993,67.876,38.983,67.769,38.982,67.694,38.995,67.677,39.008,67.667,39.109,67.648,39.131,67.617,39.15,67.4,39.197,67.358,39.217,67.35,39.242,67.426,39.466,67.46,39.482,67.492,39.519,67.542,39.558,67.719,39.621,67.909,39.594,68.077,39.564,68.245,39.548,68.303,39.538,68.399,39.529,68.463,39.537,68.507,39.563,68.586,39.635,68.61,39.743,68.639,39.839,68.687,39.846,68.735,39.836,68.758,39.856,68.768,39.882,68.778,39.904,68.798,39.909,68.832,39.884,68.852,39.891,68.869,39.907,68.864,39.927,68.824,39.961,68.789,40.013,68.793,40.031,68.805,40.05,68.908,40.068,68.956,40.071,68.972,40.09,68.966,40.12,68.927,40.136,68.785,40.127,68.64,40.129,68.622,40.147,68.631,40.167,68.653,40.183,68.952,40.223,69.11,40.209,69.228,40.188,69.275,40.198,69.22,40.288,69.294,40.297,69.304,40.327,69.206,40.567,69.26,40.588,69.314,40.635,69.309,40.724,69.357,40.767,69.414,40.797,69.498,40.767,69.628,40.679,69.671,40.662,69.713,40.657,69.773,40.684,70.006,40.771,70.136,40.82,70.292,40.892,70.319,40.919,70.373,41.028,70.402,41.035,70.442,41.023,70.578,40.911,70.657,40.84,70.657,40.815,70.635,40.797,70.639,40.779,70.751,40.74,70.751,40.722,70.726,40.688,70.712,40.669,70.698,40.661,70.549,40.563,70.383,40.454,70.377,40.439,70.37,40.412,70.372,40.384,70.398,40.361,70.47,40.345,70.534,40.325,70.566,40.267,70.603,40.214,70.653,40.201,70.899,40.235,70.958,40.239,70.961,40.221,70.946,40.188,70.739,40.131,70.644,40.083,70.624,39.999,70.599,39.975,70.557,39.954,70.515,39.95,70.451,40.049,70.379,40.07,70.274,40.105,70.071,40.173,69.967,40.202,69.765,40.158,69.53,40.097,69.494,40.06,69.469,40.021,69.471,39.991,69.488,39.95,69.476,39.92,69.432,39.91,69.365,39.947,69.307,39.969,69.279,39.918,69.245,39.827,69.229,39.761,69.28,39.666,69.298,39.525,69.392,39.532,69.463,39.532,69.599,39.574,69.667,39.575,69.772,39.557,69.956,39.553,70.102,39.561,70.137,39.558,70.171,39.584,70.209,39.575,70.245,39.543,70.392,39.582,70.501,39.587,70.568,39.576,70.608,39.564,70.679,39.471,70.733,39.413,70.799,39.395,71.005,39.412,71.065,39.493,71.118,39.514,71.203,39.52,71.273,39.535,71.329,39.569,71.404,39.598,71.47,39.604,71.503,39.582,71.517,39.554,71.506,39.517,71.503,39.479,71.546,39.453,71.673,39.447,71.732,39.423,71.735,39.378,71.726,39.307,71.779,39.278,71.806,39.276,71.991,39.351,72.043,39.352,72.084,39.311,72.147,39.261,72.23,39.208,72.25,39.216,72.287,39.274,72.358,39.337,72.49,39.357,72.563,39.377,72.64,39.386,72.872,39.36,72.949,39.357,73.109,39.362,73.235,39.375,73.336,39.412,73.387,39.443,73.47,39.461,73.576,39.458,73.632,39.449,73.636,39.397,73.623,39.298,73.607,39.229,73.69,39.105,73.744,39.045,73.796,39.002,73.805,38.969,73.795,38.941,73.73,38.915,73.707,38.886,73.696,38.854,73.717,38.817,73.754,38.699,73.802,38.607,73.869,38.563,73.97,38.534,74.026,38.54,74.065,38.608,74.131,38.661,74.187,38.658,74.277,38.66,74.514,38.6,74.745,38.51,74.812,38.46,74.836,38.404,74.772,38.275,74.775,38.192,74.79,38.104,74.842,38.038,74.891,37.926,74.9,37.833,74.921,37.805,74.938,37.773,74.912,37.687,74.894,37.601,74.916,37.573,74.986,37.53,75.097,37.451,75.119,37.386,75.079,37.344,75.008,37.294,74.918,37.25,74.891,37.232,74.875,37.242,74.83,37.286,74.731,37.357,74.659,37.394,74.524,37.382,74.445,37.396,74.349,37.419,74.26,37.415,74.204,37.372,74.167,37.329,74.078,37.316,73.949,37.283,73.75,37.232,73.654,37.239,73.628,37.262,73.649,37.291,73.717,37.329,73.734,37.376,73.721,37.419,73.657,37.43,73.633,37.437,73.605,37.446,73.481,37.472,73.383,37.462,73.211,37.408,72.896,37.268,72.757,37.173,72.657,37.029,72.359,36.983,72.154,36.901,71.942,36.766,71.802,36.694,71.734,36.684,71.666,36.697,71.597,36.733,71.531,36.845,71.472,37.015,71.433,37.128,71.455,37.272,71.48,37.436,71.505,37.603,71.546,37.796,71.58,37.864,71.582,37.91,71.552,37.933,71.488,37.932,71.39,37.906,71.32,37.902,71.279,37.918,71.283,38.008,71.333,38.17,71.256,38.307,71.052,38.418,70.879,38.456,70.736,38.423,70.616,38.334,70.519,38.192,70.418,38.075,70.313,37.985,70.239,37.941,70.215,37.924,70.199,37.886,70.255,37.765,70.251,37.664,70.189,37.582,70.12,37.544,70.045,37.547,69.985,37.566,69.941,37.6,69.821,37.61,69.626,37.594,69.492,37.553,69.42,37.487,69.399,37.399,69.43,37.291,69.414,37.208,69.354,37.15,69.304,37.117,69.265,37.108,69.18,37.158,69.05,37.267,68.96,37.325,68.912,37.334,68.885,37.328,68.855,37.317,68.838,37.303,68.824,37.271,68.782,37.258,68.723,37.268,68.669,37.258,68.637,37.224,68.546,37.183,68.387,37.138,68.3,37.088,68.285,37.036,68.261,37.013,68.212,37.022,68.068,36.95,67.958,36.972,67.834,37.064,67.766,37.14,67.759,37.172]],[[70.653,40.937,70.623,40.934,70.569,40.982,70.55,41.015,70.572,41.025,70.618,41.002,70.649,40.961,70.653,40.937]],[[70.702,39.825,70.612,39.787,70.56,39.791,70.519,39.828,70.489,39.863,70.483,39.883,70.498,39.882,70.567,39.867,70.664,39.855,70.698,39.846,70.702,39.825]]]},{"n":"Taiwan","c":"TW","r":3,"ml":4.5,"lx":120.87,"ly":23.65,"b":[118.287,21.925,121.929,25.277],"p":[[[121.009,22.62,120.947,22.503,120.897,22.379,120.877,22.262,120.878,22.142,120.864,22.033,120.84,21.925,120.743,21.956,120.69,22.033,120.678,22.16,120.608,22.313,120.581,22.356,120.48,22.442,120.388,22.485,120.316,22.548,120.326,22.542,120.273,22.627,120.233,22.718,120.15,22.975,120.122,23.037,120.083,23.094,120.072,23.15,120.086,23.212,120.121,23.305,120.143,23.399,120.125,23.527,120.132,23.653,120.159,23.709,120.63,24.479,120.757,24.642,120.836,24.723,120.902,24.813,120.964,24.928,121.041,25.033,121.095,25.065,121.365,25.159,121.45,25.249,121.517,25.277,121.594,25.275,121.643,25.232,121.687,25.182,121.733,25.154,121.853,25.104,121.905,25.056,121.929,24.974,121.856,24.895,121.82,24.825,121.813,24.746,121.826,24.641,121.828,24.534,121.737,24.285,121.639,24.13,121.613,24.053,121.583,23.861,121.526,23.668,121.477,23.424,121.397,23.173,121.352,23.067,121.296,22.967,121.161,22.776,121.009,22.62]],[[118.407,24.522,118.451,24.456,118.433,24.414,118.295,24.436,118.287,24.477,118.339,24.469,118.407,24.522]]]},{"n":"Syria","c":"SY","r":3,"ml":3,"lx":38.28,"ly":35.01,"b":[35.764,32.317,42.359,37.297],"p":[[[35.893,35.917,35.968,35.91,36.127,35.831,36.154,35.834,36.202,35.938,36.249,35.973,36.348,36.004,36.375,36.171,36.421,36.203,36.477,36.221,36.562,36.224,36.637,36.234,36.641,36.264,36.538,36.457,36.547,36.506,36.597,36.701,36.628,36.778,36.659,36.803,36.777,36.793,36.942,36.758,36.985,36.702,37.066,36.653,37.187,36.656,37.327,36.647,37.436,36.643,37.524,36.678,37.72,36.744,37.818,36.766,37.907,36.795,38.192,36.902,38.306,36.893,38.384,36.879,38.444,36.862,38.578,36.789,38.689,36.715,38.767,36.693,38.906,36.695,39.108,36.681,39.357,36.682,39.501,36.702,39.687,36.739,40.016,36.826,40.45,37.009,40.706,37.098,40.816,37.108,40.959,37.109,41.102,37.086,41.265,37.069,41.34,37.071,41.516,37.089,41.744,37.126,41.887,37.156,42.06,37.206,42.168,37.289,42.203,37.297,42.248,37.282,42.269,37.277,42.313,37.23,42.359,37.109,42.359,37.095,42.35,37.061,42.237,36.961,42.084,36.826,41.974,36.741,41.789,36.597,41.65,36.566,41.417,36.515,41.354,36.464,41.296,36.383,41.262,36.272,41.252,36.203,41.246,36.073,41.3,35.939,41.353,35.81,41.359,35.725,41.354,35.64,41.303,35.551,41.248,35.427,41.216,35.288,41.2,35.027,41.199,34.805,41.195,34.769,41.099,34.612,40.987,34.429,40.935,34.387,40.689,34.332,40.421,34.198,40.122,34.048,39.85,33.911,39.564,33.768,39.268,33.62,39.057,33.514,38.774,33.372,38.516,33.237,38.254,33.099,38.056,32.995,37.754,32.83,37.577,32.733,37.318,32.591,37.089,32.466,36.818,32.317,36.479,32.361,36.372,32.387,36.284,32.457,36.22,32.495,36.059,32.534,35.956,32.667,35.895,32.714,35.787,32.735,35.833,32.82,35.842,32.876,35.888,32.944,35.859,32.989,35.845,33.085,35.825,33.108,35.817,33.133,35.833,33.164,35.827,33.195,35.809,33.209,35.8,33.256,35.785,33.291,35.793,33.334,35.816,33.362,35.841,33.416,35.869,33.432,35.915,33.465,35.927,33.5,35.968,33.535,36.022,33.563,36.034,33.585,36.027,33.598,35.972,33.623,35.942,33.668,35.968,33.732,35.986,33.753,36.019,33.784,36.092,33.832,36.15,33.84,36.199,33.84,36.283,33.836,36.349,33.827,36.365,33.839,36.363,33.855,36.282,33.894,36.278,33.925,36.298,33.959,36.355,34.011,36.423,34.05,36.458,34.057,36.535,34.134,36.585,34.221,36.504,34.432,36.456,34.466,36.376,34.495,36.33,34.5,36.326,34.513,36.389,34.567,36.433,34.613,36.384,34.658,36.296,34.679,36.264,34.633,36.151,34.629,35.976,34.629,35.899,34.852,35.888,34.949,35.89,35.06,35.943,35.224,35.918,35.3,35.916,35.351,35.902,35.421,35.764,35.572,35.84,35.849,35.893,35.917]]]},{"n":"Switzerland","c":"CH","r":4,"ml":4,"lx":7.46,"ly":46.72,"b":[5.97,45.83,10.455,47.776],"p":[[[9.524,47.524,9.554,47.511,9.626,47.467,9.609,47.392,9.528,47.271,9.484,47.173,9.479,47.098,9.488,47.062,9.502,47.063,9.58,47.057,9.62,47.057,9.745,47.037,9.845,47.007,9.865,46.976,9.878,46.938,9.997,46.885,10.133,46.852,10.18,46.862,10.349,46.985,10.415,46.964,10.455,46.899,10.453,46.865,10.406,46.735,10.398,46.665,10.438,46.619,10.442,46.583,10.431,46.55,10.363,46.547,10.272,46.565,10.196,46.621,10.138,46.614,10.087,46.6,10.061,46.547,10.038,46.483,10.046,46.448,10.082,46.421,10.11,46.363,10.13,46.288,10.145,46.254,10.128,46.238,10.081,46.228,10.041,46.238,9.972,46.328,9.939,46.362,9.884,46.368,9.788,46.346,9.639,46.296,9.58,46.296,9.529,46.306,9.481,46.349,9.441,46.431,9.428,46.482,9.399,46.481,9.304,46.496,9.26,46.475,9.26,46.391,9.251,46.287,9.203,46.219,9.071,46.102,9.022,46.051,9.003,46.015,8.999,45.983,9.019,45.928,9.047,45.876,9.024,45.846,8.954,45.83,8.904,45.862,8.885,45.919,8.778,45.996,8.827,46.061,8.819,46.077,8.642,46.111,8.565,46.16,8.458,46.246,8.438,46.283,8.443,46.403,8.437,46.432,8.423,46.446,8.371,46.445,8.299,46.403,8.232,46.341,8.096,46.271,8.082,46.256,8.127,46.188,8.125,46.161,8.014,46.052,7.993,46.016,7.852,45.947,7.788,45.922,7.593,45.972,7.539,45.978,7.452,45.944,7.328,45.912,7.129,45.88,7.056,45.904,7.021,45.926,7.004,45.959,6.954,46.017,6.897,46.052,6.858,46.089,6.806,46.131,6.772,46.165,6.817,46.275,6.784,46.314,6.767,46.369,6.776,46.407,6.758,46.416,6.578,46.437,6.429,46.431,6.322,46.394,6.235,46.333,6.224,46.319,6.23,46.308,6.273,46.252,6.199,46.193,6.087,46.147,6.007,46.142,5.971,46.151,5.97,46.215,6.036,46.238,6.096,46.279,6.116,46.338,6.123,46.379,6.06,46.428,6.068,46.459,6.107,46.516,6.13,46.567,6.161,46.611,6.285,46.683,6.41,46.755,6.429,46.832,6.439,46.926,6.456,46.948,6.625,47.004,6.667,47.027,6.688,47.058,6.821,47.163,6.952,47.267,6.979,47.302,7.001,47.323,7.001,47.339,6.984,47.353,6.921,47.361,6.9,47.394,6.968,47.453,7.053,47.489,7.136,47.49,7.169,47.473,7.167,47.454,7.203,47.433,7.266,47.426,7.343,47.433,7.42,47.455,7.467,47.508,7.495,47.547,7.616,47.593,7.698,47.57,7.927,47.564,8.094,47.576,8.198,47.607,8.328,47.607,8.415,47.59,8.43,47.592,8.454,47.596,8.478,47.613,8.559,47.624,8.571,47.638,8.567,47.652,8.552,47.659,8.452,47.652,8.413,47.663,8.403,47.688,8.436,47.731,8.51,47.767,8.573,47.776,8.618,47.766,8.728,47.7,8.755,47.698,8.77,47.71,8.793,47.717,8.831,47.704,8.874,47.663,8.881,47.656,9.128,47.671,9.183,47.671,9.35,47.599,9.524,47.524]]]},{"n":"Sweden","c":"SE","r":3,"ml":2,"lx":19.02,"ly":65.86,"b":[11.147,55.346,24.155,69.037],"p":[[[19.076,57.836,18.994,57.812,18.945,57.742,18.878,57.73,18.814,57.706,18.791,57.483,18.908,57.398,18.844,57.386,18.785,57.361,18.743,57.324,18.7,57.243,18.538,57.197,18.477,57.163,18.387,57.088,18.34,56.978,18.249,56.932,18.146,56.921,18.207,57.01,18.285,57.083,18.21,57.133,18.164,57.212,18.105,57.272,18.152,57.339,18.129,57.449,18.137,57.557,18.205,57.611,18.283,57.655,18.405,57.757,18.537,57.831,18.722,57.864,18.805,57.833,18.841,57.9,18.901,57.915,18.956,57.9,19.076,57.836]],[[16.529,56.291,16.477,56.24,16.432,56.244,16.401,56.311,16.394,56.484,16.412,56.569,16.63,56.877,16.728,56.902,16.865,57.091,16.902,57.175,16.961,57.25,16.996,57.318,17.025,57.345,17.089,57.332,17.118,57.32,17.05,57.28,17.058,57.229,17.054,57.208,16.884,56.985,16.838,56.841,16.778,56.805,16.529,56.291]],[[11.388,59.037,11.471,58.91,11.544,58.893,11.643,58.926,11.712,59.019,11.752,59.158,11.798,59.29,11.743,59.431,11.685,59.556,11.681,59.592,11.834,59.697,11.881,59.782,11.932,59.864,11.988,59.891,12.072,59.898,12.169,59.913,12.292,59.967,12.402,60.04,12.486,60.107,12.515,60.239,12.516,60.305,12.553,60.354,12.589,60.451,12.554,60.546,12.445,60.69,12.315,60.892,12.294,61.003,12.354,61.023,12.468,61.042,12.683,61.047,12.706,61.06,12.728,61.108,12.776,61.174,12.828,61.222,12.864,61.29,12.881,61.352,12.758,61.446,12.596,61.541,12.487,61.573,12.292,61.653,12.155,61.721,12.234,61.977,12.292,62.167,12.301,62.214,12.304,62.286,12.115,62.592,12.122,62.66,12.14,62.721,12.12,62.826,12.109,62.919,12.141,62.948,12.218,63.001,12.145,63.083,12.139,63.089,12,63.292,12.212,63.492,12.175,63.596,12.302,63.671,12.533,63.844,12.663,63.94,12.69,63.957,12.793,64,12.988,64.05,13.204,64.075,13.3,64.075,13.671,64.041,13.961,64.014,14.003,64.041,14.063,64.096,14.141,64.174,14.148,64.26,14.12,64.388,14.078,64.464,13.874,64.514,13.65,64.582,13.925,64.797,14.115,64.946,14.352,65.171,14.426,65.264,14.48,65.301,14.55,65.646,14.596,65.743,14.635,65.793,14.635,65.845,14.61,65.932,14.543,66.129,14.918,66.154,15.04,66.168,15.153,66.191,15.375,66.252,15.484,66.306,15.423,66.49,15.557,66.552,15.884,66.769,16.238,66.976,16.404,67.055,16.421,67.093,16.434,67.155,16.361,67.252,16.282,67.312,16.127,67.426,16.194,67.505,16.307,67.521,16.457,67.552,16.574,67.62,16.586,67.628,16.784,67.895,17.171,68.03,17.325,68.104,17.565,68.048,17.917,67.965,18.073,68.088,18.125,68.133,18.177,68.201,18.156,68.317,18.147,68.468,18.163,68.528,18.303,68.555,18.379,68.562,18.77,68.5,18.868,68.501,19.053,68.493,19.259,68.465,19.691,68.392,19.87,68.362,19.97,68.356,20.056,68.39,20.24,68.478,19.969,68.542,20.147,68.607,20.24,68.673,20.319,68.754,20.348,68.849,20.337,68.9,20.282,68.934,20.117,69.021,20.492,69.033,20.622,69.037,20.895,68.98,20.907,68.967,20.909,68.938,20.919,68.907,21.183,68.829,21.26,68.787,21.422,68.725,21.465,68.691,21.616,68.651,21.724,68.609,21.85,68.574,21.997,68.521,22.195,68.478,22.362,68.464,22.782,68.391,22.854,68.367,22.975,68.316,23.098,68.258,23.183,68.137,23.319,68.13,23.355,68.089,23.474,68.017,23.639,67.954,23.633,67.933,23.502,67.875,23.488,67.797,23.5,67.696,23.541,67.614,23.537,67.59,23.504,67.562,23.465,67.518,23.451,67.479,23.455,67.46,23.468,67.45,23.537,67.449,23.661,67.44,23.734,67.423,23.775,67.329,23.761,67.31,23.657,67.268,23.626,67.234,23.623,67.184,23.642,67.129,23.677,67.068,23.759,67.003,23.869,66.934,23.942,66.878,23.976,66.838,23.989,66.811,23.939,66.776,23.894,66.707,23.886,66.628,23.866,66.577,23.768,66.506,23.701,66.481,23.682,66.443,23.674,66.381,23.694,66.304,23.7,66.253,23.721,66.215,23.751,66.191,23.907,66.148,23.995,66.06,24.049,65.99,24.155,65.805,23.891,65.782,23.691,65.829,23.592,65.805,23.418,65.804,23.221,65.786,23.155,65.75,23.102,65.735,22.919,65.786,22.747,65.871,22.62,65.807,22.539,65.794,22.465,65.853,22.401,65.862,22.366,65.843,22.336,65.791,22.288,65.751,22.275,65.725,22.267,65.622,22.254,65.598,22.086,65.611,22.096,65.584,22.133,65.57,22.148,65.553,22.087,65.53,21.92,65.532,21.903,65.508,21.95,65.47,21.913,65.437,21.88,65.424,21.681,65.403,21.566,65.408,21.533,65.387,21.523,65.359,21.545,65.331,21.596,65.317,21.613,65.299,21.609,65.261,21.567,65.255,21.447,65.321,21.41,65.317,21.438,65.283,21.506,65.245,21.546,65.207,21.581,65.161,21.574,65.126,21.425,65.013,21.294,64.941,21.196,64.877,21.138,64.809,21.205,64.774,21.279,64.725,21.332,64.629,21.394,64.544,21.52,64.463,21.494,64.416,21.465,64.38,21.256,64.299,21.018,64.178,20.763,63.868,20.678,63.826,20.454,63.774,20.371,63.723,20.205,63.662,19.914,63.611,19.782,63.538,19.722,63.463,19.656,63.458,19.59,63.487,19.502,63.509,19.491,63.46,19.495,63.424,19.354,63.477,19.288,63.429,19.236,63.347,19.034,63.238,18.817,63.257,18.792,63.238,18.85,63.224,18.859,63.207,18.819,63.197,18.76,63.198,18.667,63.177,18.606,63.178,18.578,63.126,18.531,63.064,18.408,63.038,18.344,63.032,18.313,62.996,18.502,62.989,18.487,62.959,18.483,62.928,18.463,62.896,18.248,62.849,18.215,62.812,18.17,62.789,18.074,62.791,18.078,62.812,18.094,62.836,17.951,62.834,17.907,62.887,17.88,62.873,17.896,62.831,17.933,62.786,17.974,62.721,17.941,62.68,17.903,62.659,17.93,62.641,18.007,62.626,18.037,62.601,17.947,62.578,17.834,62.503,17.718,62.501,17.646,62.451,17.571,62.451,17.509,62.483,17.41,62.508,17.378,62.463,17.373,62.427,17.429,62.335,17.535,62.264,17.634,62.233,17.563,62.212,17.51,62.166,17.447,62.023,17.412,61.966,17.375,61.866,17.398,61.782,17.417,61.741,17.465,61.684,17.335,61.692,17.196,61.725,17.216,61.656,17.131,61.576,17.147,61.505,17.164,61.458,17.138,61.382,17.178,61.358,17.2,61.312,17.164,61.278,17.18,61.249,17.186,61.147,17.213,60.986,17.203,60.952,17.279,60.812,17.261,60.763,17.251,60.701,17.36,60.641,17.457,60.642,17.555,60.643,17.593,60.628,17.631,60.585,17.661,60.535,17.742,60.539,17.872,60.58,17.956,60.59,18.011,60.511,18.163,60.408,18.25,60.362,18.4,60.337,18.558,60.254,18.535,60.153,18.601,60.119,18.787,60.079,18.853,60.026,18.884,59.98,18.933,59.942,18.99,59.828,18.971,59.757,18.896,59.733,18.719,59.657,18.64,59.601,18.578,59.566,18.402,59.49,18.338,59.477,18.276,59.438,18.217,59.421,18.164,59.43,17.964,59.359,17.98,59.329,18.133,59.316,18.211,59.331,18.271,59.367,18.336,59.375,18.396,59.369,18.459,59.397,18.509,59.408,18.56,59.394,18.618,59.327,18.499,59.292,18.414,59.29,18.373,59.18,18.322,59.132,18.285,59.109,18.098,59.062,17.975,59.003,17.829,58.955,17.765,58.965,17.67,58.916,17.457,58.858,17.348,58.781,17.103,58.711,16.978,58.654,16.639,58.651,16.316,58.664,16.214,58.637,16.318,58.628,16.391,58.602,16.478,58.613,16.683,58.6,16.788,58.585,16.924,58.493,16.824,58.46,16.652,58.434,16.717,58.303,16.77,58.214,16.7,58.161,16.695,57.918,16.597,57.913,16.555,57.812,16.586,57.761,16.584,57.642,16.604,57.568,16.652,57.501,16.631,57.43,16.476,57.265,16.479,57.188,16.507,57.142,16.528,57.068,16.458,56.927,16.408,56.809,16.349,56.709,16.217,56.59,16.151,56.501,15.997,56.223,15.92,56.167,15.827,56.125,15.722,56.164,15.627,56.186,15.51,56.183,15.327,56.151,15.051,56.172,14.782,56.162,14.714,56.134,14.755,56.033,14.656,56.02,14.559,56.049,14.473,56.014,14.402,55.977,14.262,55.888,14.215,55.833,14.203,55.729,14.276,55.636,14.342,55.528,14.174,55.397,14.08,55.392,13.806,55.429,13.321,55.346,12.886,55.411,12.941,55.482,12.939,55.533,12.963,55.613,12.978,55.694,12.974,55.748,12.942,55.806,12.835,55.882,12.593,56.138,12.521,56.246,12.471,56.291,12.507,56.293,12.706,56.235,12.753,56.242,12.802,56.264,12.742,56.347,12.691,56.384,12.656,56.441,12.773,56.456,12.857,56.452,12.92,56.516,12.884,56.618,12.793,56.649,12.718,56.663,12.573,56.823,12.421,56.906,12.152,57.227,12.053,57.447,11.962,57.426,11.917,57.522,11.885,57.613,11.879,57.679,11.735,57.718,11.729,57.764,11.703,57.973,11.549,58.001,11.449,58.118,11.432,58.34,11.33,58.38,11.248,58.369,11.252,58.424,11.272,58.476,11.224,58.68,11.208,58.866,11.169,58.923,11.147,58.989,11.167,59.046,11.196,59.078,11.295,59.087,11.388,59.037]],[[19.156,57.923,19.138,57.86,19.087,57.865,19.039,57.911,19.135,57.981,19.281,57.978,19.331,57.963,19.156,57.923]],[[18.416,59.029,18.372,59.02,18.35,59.023,18.377,59.069,18.398,59.089,18.465,59.108,18.486,59.105,18.416,59.029]],[[18.595,59.47,18.57,59.437,18.545,59.478,18.555,59.486,18.572,59.526,18.621,59.548,18.698,59.535,18.698,59.525,18.624,59.492,18.595,59.47]]]},{"n":"Eswatini","c":"SZ","r":4,"ml":4,"lx":31.47,"ly":-26.53,"b":[30.788,-27.31,32.113,-25.743],"p":[[[31.948,-25.958,31.968,-25.972,32.061,-26.018,32.069,-26.11,32.06,-26.215,32.041,-26.281,32.048,-26.347,32.078,-26.45,32.106,-26.52,32.113,-26.839,32.082,-26.825,32.025,-26.811,31.995,-26.817,31.967,-26.961,31.946,-27.174,31.958,-27.306,31.743,-27.31,31.47,-27.296,31.274,-27.238,31.063,-27.112,30.938,-26.916,30.883,-26.792,30.807,-26.785,30.794,-26.764,30.788,-26.614,30.789,-26.455,30.803,-26.413,30.945,-26.219,31.033,-26.098,31.088,-25.981,31.207,-25.843,31.335,-25.756,31.383,-25.743,31.415,-25.747,31.64,-25.867,31.871,-25.982,31.922,-25.969,31.948,-25.958]]]},{"n":"Suriname","c":"SR","r":4,"ml":4,"lx":-55.91,"ly":4.14,"b":[-58.054,1.842,-53.99,5.993],"p":[[[-54.156,5.359,-54.24,5.288,-54.332,5.187,-54.452,5.013,-54.447,4.959,-54.473,4.915,-54.48,4.837,-54.471,4.749,-54.44,4.692,-54.426,4.583,-54.45,4.485,-54.441,4.428,-54.416,4.338,-54.396,4.241,-54.398,4.202,-54.369,4.171,-54.342,4.14,-54.351,4.054,-54.256,3.901,-54.197,3.834,-54.113,3.769,-54.082,3.706,-54.034,3.629,-54.006,3.62,-53.99,3.59,-54.006,3.531,-54.01,3.449,-54.063,3.353,-54.188,3.179,-54.203,3.138,-54.171,2.994,-54.188,2.875,-54.196,2.818,-54.257,2.714,-54.402,2.462,-54.486,2.416,-54.536,2.343,-54.568,2.343,-54.605,2.336,-54.616,2.327,-54.662,2.328,-54.697,2.36,-54.703,2.398,-54.722,2.442,-54.767,2.455,-54.852,2.44,-54.876,2.45,-54.927,2.497,-54.968,2.548,-54.979,2.598,-55.006,2.593,-55.07,2.548,-55.114,2.539,-55.149,2.551,-55.188,2.548,-55.286,2.5,-55.344,2.489,-55.385,2.441,-55.659,2.419,-55.731,2.406,-55.894,2.49,-55.936,2.517,-55.957,2.52,-55.976,2.516,-55.994,2.498,-56.02,2.393,-56.045,2.364,-56.088,2.341,-56.129,2.3,-56.138,2.259,-56.074,2.237,-56.02,2.158,-55.962,2.095,-55.915,2.04,-55.922,1.977,-55.93,1.888,-55.963,1.857,-56.02,1.842,-56.227,1.885,-56.386,1.924,-56.453,1.932,-56.483,1.942,-56.522,1.975,-56.563,2.005,-56.627,2.016,-56.704,2.036,-56.761,2.115,-56.82,2.227,-56.841,2.277,-56.886,2.326,-56.931,2.395,-56.945,2.457,-56.979,2.513,-56.997,2.532,-57.023,2.609,-57.029,2.638,-57.042,2.641,-57.06,2.666,-57.097,2.748,-57.105,2.768,-57.121,2.776,-57.164,2.833,-57.197,2.853,-57.21,2.883,-57.207,2.963,-57.225,3.003,-57.231,3.079,-57.232,3.109,-57.249,3.142,-57.278,3.164,-57.283,3.219,-57.29,3.354,-57.304,3.377,-57.426,3.375,-57.438,3.362,-57.491,3.354,-57.55,3.353,-57.603,3.371,-57.647,3.395,-57.656,3.424,-57.649,3.517,-57.72,3.588,-57.833,3.676,-57.867,3.787,-57.908,3.857,-58.032,4.002,-58.054,4.102,-58.054,4.172,-58.011,4.236,-57.95,4.35,-57.925,4.453,-57.906,4.507,-57.875,4.577,-57.846,4.668,-57.868,4.724,-57.905,4.779,-57.917,4.82,-57.881,4.881,-57.845,4.923,-57.804,4.929,-57.752,4.954,-57.711,4.991,-57.649,5.001,-57.571,5.004,-57.412,5.005,-57.331,5.02,-57.306,5.05,-57.31,5.106,-57.269,5.157,-57.227,5.179,-57.21,5.195,-57.207,5.214,-57.218,5.232,-57.235,5.243,-57.28,5.247,-57.319,5.335,-57.292,5.374,-57.258,5.445,-57.248,5.485,-57.195,5.548,-57.182,5.529,-57.141,5.644,-57.136,5.737,-57.105,5.829,-57.057,5.939,-56.97,5.993,-56.466,5.938,-56.236,5.885,-55.94,5.795,-55.898,5.699,-55.896,5.795,-55.91,5.893,-55.828,5.962,-55.648,5.986,-55.379,5.953,-55.148,5.993,-54.834,5.988,-54.356,5.91,-54.142,5.856,-54.054,5.808,-54.037,5.721,-54.046,5.609,-54.08,5.502,-54.156,5.359]]]},{"n":"South Sudan","c":"SS","r":3,"ml":3,"lx":30.39,"ly":7.23,"b":[24.147,3.491,35.268,12.223],"p":[[[33.976,4.22,33.742,3.985,33.568,3.812,33.54,3.787,33.489,3.755,33.324,3.754,33.154,3.775,32.997,3.88,32.838,3.798,32.737,3.773,32.677,3.763,32.535,3.75,32.336,3.706,32.246,3.651,32.197,3.608,32.156,3.528,32.136,3.52,32.099,3.529,32.048,3.561,31.942,3.608,31.888,3.709,31.839,3.77,31.798,3.803,31.629,3.701,31.547,3.678,31.48,3.68,31.357,3.738,31.222,3.786,31.152,3.786,31.048,3.725,30.929,3.634,30.868,3.544,30.839,3.491,30.817,3.533,30.797,3.573,30.757,3.624,30.7,3.644,30.648,3.634,30.587,3.624,30.559,3.653,30.554,3.723,30.537,3.787,30.508,3.836,30.421,3.884,30.195,3.982,30.021,4.178,29.934,4.269,29.87,4.327,29.78,4.481,29.677,4.587,29.552,4.636,29.47,4.612,29.385,4.498,29.225,4.392,29.151,4.388,29.057,4.446,28.939,4.487,28.727,4.505,28.64,4.454,28.525,4.373,28.428,4.324,28.367,4.319,28.311,4.338,28.247,4.349,28.192,4.35,28.079,4.425,28.02,4.479,27.981,4.532,27.917,4.568,27.842,4.598,27.788,4.645,27.761,4.703,27.719,4.778,27.664,4.846,27.491,4.968,27.439,5.039,27.403,5.109,27.332,5.186,27.257,5.29,27.233,5.441,27.229,5.563,27.213,5.619,27.181,5.675,27.144,5.723,27.083,5.777,26.942,5.855,26.796,5.946,26.726,5.998,26.594,6.018,26.514,6.069,26.447,6.183,26.421,6.274,26.353,6.345,26.325,6.396,26.309,6.455,26.362,6.635,26.285,6.699,26.169,6.782,26.087,6.872,26.037,6.955,25.889,7.065,25.567,7.229,25.381,7.333,25.279,7.427,25.19,7.519,25.181,7.557,25.239,7.649,25.247,7.725,25.2,7.808,25.007,7.965,24.853,8.138,24.737,8.192,24.456,8.239,24.375,8.258,24.291,8.291,24.208,8.369,24.18,8.461,24.221,8.608,24.195,8.653,24.147,8.666,24.16,8.696,24.214,8.768,24.3,8.814,24.532,8.887,24.545,8.915,24.549,9.007,24.568,9.052,24.648,9.179,24.659,9.23,24.663,9.338,24.674,9.389,24.697,9.426,24.76,9.489,24.783,9.527,24.792,9.61,24.785,9.775,24.818,9.84,24.964,9.989,25.003,10.055,25.016,10.115,25.015,10.176,25.024,10.236,25.067,10.294,25.104,10.312,25.212,10.33,25.285,10.319,25.798,10.421,25.858,10.406,25.885,10.346,25.883,10.25,25.892,10.203,25.919,10.169,26.001,10.123,26.057,10.047,26.087,10.018,26.17,9.966,26.551,9.526,26.659,9.484,26.763,9.499,26.971,9.591,27.074,9.614,27.8,9.588,27.881,9.602,27.886,9.6,27.996,9.379,28.049,9.329,28.845,9.326,28.829,9.389,28.839,9.459,28.932,9.549,28.98,9.594,29,9.61,29.122,9.675,29.242,9.718,29.473,9.769,29.557,9.848,29.604,9.921,29.605,10.065,29.636,10.089,29.691,10.122,29.958,10.25,30.003,10.277,30.475,9.979,30.739,9.743,30.755,9.731,30.769,9.727,30.783,9.735,30.795,9.746,30.814,9.753,30.827,9.756,30.94,9.759,31.154,9.771,31.225,9.799,31.655,10.221,31.764,10.356,31.792,10.383,31.854,10.479,31.92,10.644,31.933,10.663,32.404,11.058,32.421,11.089,32.425,11.114,32.354,11.247,32.339,11.315,32.336,11.419,32.35,11.58,32.345,11.683,32.343,11.694,32.338,11.71,32.335,11.716,32.072,12.007,32.737,12.01,32.738,12.034,32.738,12.046,32.736,12.058,32.723,12.093,32.716,12.139,32.715,12.152,32.716,12.165,32.72,12.189,32.721,12.202,32.72,12.208,32.719,12.214,32.719,12.219,32.722,12.223,33.199,12.217,33.193,12.135,33.135,11.942,33.136,11.826,33.122,11.693,33.119,11.682,33.106,11.654,33.095,11.638,33.082,11.622,33.078,11.616,33.073,11.606,33.073,11.592,33.172,10.85,33.168,10.831,33.165,10.819,33.138,10.773,33.131,10.758,33.13,10.746,33.141,10.738,33.361,10.658,33.371,10.653,33.38,10.646,33.459,10.551,33.892,10.199,33.907,10.181,33.952,10.071,33.957,10.054,33.958,10.028,33.957,10.007,33.946,9.941,33.95,9.911,33.957,9.891,33.963,9.869,33.963,9.862,33.963,9.856,33.959,9.845,33.895,9.718,33.874,9.627,33.868,9.55,33.871,9.506,33.879,9.478,33.882,9.471,33.885,9.466,33.888,9.464,33.891,9.462,34.077,9.462,34.078,9.462,34.077,9.421,34.085,9.219,34.091,9.041,34.102,8.752,34.102,8.676,34.095,8.582,34.073,8.545,34.02,8.492,33.953,8.444,33.785,8.431,33.645,8.433,33.545,8.443,33.409,8.448,33.281,8.437,33.234,8.396,33.165,8.251,33.065,8.04,33.013,7.952,32.999,7.9,33.015,7.869,33.081,7.824,33.226,7.761,33.392,7.724,33.516,7.708,33.601,7.69,33.666,7.671,33.902,7.51,33.978,7.435,34.02,7.368,34.03,7.297,34.064,7.226,34.2,7.085,34.279,7.003,34.484,6.898,34.563,6.78,34.639,6.722,34.711,6.66,34.749,6.568,34.838,6.3,34.898,6.16,34.959,6.045,34.984,5.858,35.032,5.775,35.082,5.673,35.164,5.581,35.252,5.511,35.268,5.492,35.084,5.312,34.878,5.11,34.64,4.875,34.38,4.621,34.177,4.419,33.976,4.22]]]},{"n":"Sudan","c":"SD","r":3,"ml":2.5,"lx":29.26,"ly":16.33,"b":[21.825,8.666,38.609,22.202],"p":[[[34.078,9.462,34.077,9.462,33.891,9.462,33.888,9.464,33.885,9.466,33.882,9.471,33.879,9.478,33.871,9.506,33.868,9.55,33.874,9.627,33.895,9.718,33.959,9.845,33.963,9.856,33.963,9.862,33.963,9.869,33.957,9.891,33.95,9.911,33.946,9.941,33.957,10.007,33.958,10.028,33.957,10.054,33.952,10.071,33.907,10.181,33.892,10.199,33.459,10.551,33.38,10.646,33.371,10.653,33.361,10.658,33.141,10.738,33.13,10.746,33.131,10.758,33.138,10.773,33.165,10.819,33.168,10.831,33.172,10.85,33.073,11.592,33.073,11.606,33.078,11.616,33.082,11.622,33.095,11.638,33.106,11.654,33.119,11.682,33.122,11.693,33.136,11.826,33.135,11.942,33.193,12.135,33.199,12.217,32.722,12.223,32.719,12.219,32.719,12.214,32.72,12.208,32.721,12.202,32.72,12.189,32.716,12.165,32.715,12.152,32.716,12.139,32.723,12.093,32.736,12.058,32.738,12.046,32.738,12.034,32.737,12.01,32.072,12.007,32.335,11.716,32.338,11.71,32.343,11.694,32.345,11.683,32.35,11.58,32.336,11.419,32.339,11.315,32.354,11.247,32.425,11.114,32.421,11.089,32.404,11.058,31.933,10.663,31.92,10.644,31.854,10.479,31.792,10.383,31.764,10.356,31.655,10.221,31.225,9.799,31.154,9.771,30.94,9.759,30.827,9.756,30.814,9.753,30.795,9.746,30.783,9.735,30.769,9.727,30.755,9.731,30.739,9.743,30.475,9.979,30.003,10.277,29.958,10.25,29.691,10.122,29.636,10.089,29.605,10.065,29.604,9.921,29.557,9.848,29.473,9.769,29.242,9.718,29.122,9.675,29,9.61,28.98,9.594,28.932,9.549,28.839,9.459,28.829,9.389,28.845,9.326,28.049,9.329,27.996,9.379,27.886,9.6,27.881,9.602,27.8,9.588,27.074,9.614,26.971,9.591,26.763,9.499,26.659,9.484,26.551,9.526,26.17,9.966,26.087,10.018,26.057,10.047,26.001,10.123,25.919,10.169,25.892,10.203,25.883,10.25,25.885,10.346,25.858,10.406,25.798,10.421,25.285,10.319,25.212,10.33,25.104,10.312,25.067,10.294,25.024,10.236,25.015,10.176,25.016,10.115,25.003,10.055,24.964,9.989,24.818,9.84,24.785,9.775,24.792,9.61,24.783,9.527,24.76,9.489,24.697,9.426,24.674,9.389,24.663,9.338,24.659,9.23,24.648,9.179,24.568,9.052,24.549,9.007,24.545,8.915,24.532,8.887,24.3,8.814,24.214,8.768,24.16,8.696,24.147,8.666,24.048,8.691,23.922,8.71,23.679,8.732,23.583,8.766,23.537,8.816,23.552,8.943,23.528,8.971,23.489,8.993,23.463,9.048,23.468,9.115,23.596,9.262,23.623,9.341,23.643,9.614,23.656,9.71,23.646,9.823,23.545,10.03,23.457,10.174,23.312,10.388,23.256,10.458,22.964,10.752,22.931,10.795,22.86,10.92,22.895,11.029,22.938,11.192,22.943,11.267,22.923,11.345,22.849,11.403,22.783,11.41,22.754,11.44,22.697,11.483,22.641,11.516,22.591,11.58,22.556,11.67,22.581,11.99,22.564,12.033,22.49,12.045,22.472,12.068,22.475,12.129,22.435,12.312,22.39,12.463,22.414,12.546,22.352,12.66,22.233,12.709,22.121,12.695,22.001,12.672,21.928,12.678,21.878,12.699,21.843,12.741,21.825,12.791,21.842,12.865,21.908,13.001,21.99,13.113,22.158,13.215,22.203,13.269,22.228,13.33,22.233,13.399,22.221,13.472,22.202,13.538,22.153,13.626,22.108,13.73,22.106,13.8,22.128,13.85,22.173,13.911,22.262,13.979,22.283,13.992,22.339,14.029,22.388,14.056,22.51,14.127,22.539,14.162,22.528,14.203,22.498,14.237,22.449,14.284,22.439,14.342,22.425,14.441,22.4,14.504,22.382,14.55,22.416,14.585,22.468,14.633,22.532,14.663,22.632,14.688,22.671,14.722,22.682,14.789,22.679,14.851,22.715,14.898,22.763,14.999,22.802,15.044,22.867,15.097,22.932,15.162,22.961,15.238,22.97,15.311,22.934,15.533,23.009,15.626,23.105,15.703,23.243,15.697,23.458,15.714,23.604,15.746,23.708,15.745,23.946,15.704,23.965,15.713,23.971,15.722,23.983,15.78,23.983,15.928,23.983,16.374,23.983,16.82,23.982,17.266,23.982,17.712,23.981,18.158,23.981,18.605,23.981,19.051,23.98,19.497,23.98,19.621,23.98,19.746,23.98,19.871,23.98,19.996,24.227,19.996,24.474,19.996,24.72,19.996,24.967,19.995,24.97,19.997,24.973,19.999,24.976,20.001,24.979,20.003,24.98,20.501,24.98,20.999,24.98,21.498,24.98,21.996,25.362,21.996,25.744,21.996,26.126,21.996,26.508,21.996,26.89,21.996,27.272,21.996,27.654,21.995,28.036,21.995,28.419,21.995,28.801,21.995,29.183,21.995,29.565,21.995,29.947,21.995,30.329,21.995,30.711,21.995,31.093,21.995,31.209,21.995,31.261,22.002,31.358,22.189,31.4,22.202,31.464,22.192,31.486,22.148,31.466,22.085,31.434,21.996,31.622,21.996,31.95,21.996,32.278,21.996,32.606,21.996,32.934,21.996,33.262,21.996,33.59,21.996,33.918,21.996,34.246,21.996,34.575,21.996,34.903,21.996,35.231,21.996,35.559,21.996,35.887,21.997,36.215,21.997,36.543,21.997,36.871,21.997,36.883,21.769,36.927,21.587,37.081,21.326,37.212,21.186,37.259,21.109,37.263,21.073,37.257,21.039,37.217,21.078,37.151,21.104,37.141,20.982,37.157,20.895,37.173,20.732,37.228,20.557,37.188,20.395,37.193,20.121,37.263,19.792,37.248,19.582,37.362,19.092,37.471,18.82,37.532,18.753,37.599,18.717,37.73,18.694,37.922,18.556,38.074,18.41,38.128,18.333,38.202,18.249,38.252,18.264,38.283,18.287,38.333,18.219,38.574,18.073,38.609,18.005,38.523,17.939,38.422,17.824,38.397,17.778,38.386,17.751,38.374,17.717,38.347,17.684,38.29,17.637,38.267,17.617,38.254,17.585,38.219,17.564,38.182,17.563,38.149,17.549,38.099,17.526,38.025,17.538,37.95,17.518,37.923,17.492,37.863,17.47,37.803,17.466,37.782,17.458,37.726,17.421,37.657,17.368,37.576,17.335,37.547,17.324,37.51,17.288,37.453,17.109,37.411,17.062,37.34,17.057,37.249,17.057,37.17,17.041,37.062,17.061,37.009,17.059,36.995,17.021,36.976,16.867,36.979,16.801,36.936,16.722,36.888,16.625,36.905,16.46,36.914,16.296,36.826,16.05,36.813,15.994,36.725,15.799,36.679,15.726,36.566,15.362,36.522,15.25,36.427,15.132,36.448,14.94,36.471,14.736,36.492,14.544,36.524,14.257,36.444,13.988,36.447,13.842,36.391,13.626,36.346,13.526,36.307,13.467,36.274,13.406,36.212,13.271,36.16,13.093,36.137,12.911,36.135,12.805,36.125,12.757,36.108,12.726,35.988,12.706,35.821,12.685,35.731,12.661,35.67,12.624,35.596,12.537,35.45,12.301,35.373,12.156,35.252,11.957,35.112,11.817,35.083,11.748,35.06,11.621,35.008,11.42,34.961,11.277,34.969,11.162,34.925,10.962,34.931,10.865,34.882,10.811,34.816,10.759,34.771,10.746,34.675,10.805,34.602,10.865,34.572,10.88,34.508,10.843,34.431,10.788,34.344,10.659,34.276,10.528,34.315,10.252,34.311,10.191,34.292,10.125,34.185,9.919,34.159,9.853,34.12,9.73,34.079,9.513,34.078,9.462]]]},{"n":"Sri Lanka","c":"LK","r":3,"ml":3,"lx":80.7,"ly":7.58,"b":[79.708,5.949,81.877,9.813],"p":[[[79.97,9.631,79.907,9.62,79.857,9.686,79.846,9.715,79.859,9.734,79.872,9.744,79.888,9.741,79.912,9.679,79.97,9.631]],[[79.875,9.051,79.904,8.975,79.821,9.027,79.767,9.07,79.748,9.105,79.86,9.066,79.875,9.051]],[[79.982,9.813,80.078,9.807,80.181,9.81,80.253,9.796,80.376,9.642,80.711,9.366,80.893,9.086,80.91,9.025,80.935,8.971,80.979,8.957,81.016,8.933,81.198,8.662,81.219,8.608,81.216,8.549,81.227,8.506,81.275,8.484,81.334,8.472,81.373,8.431,81.422,8.215,81.422,8.148,81.436,8.119,81.665,7.782,81.679,7.742,81.676,7.711,81.683,7.684,81.727,7.625,81.797,7.465,81.832,7.428,81.874,7.288,81.877,7.02,81.861,6.901,81.819,6.756,81.768,6.614,81.713,6.512,81.637,6.425,81.38,6.241,81.306,6.204,80.971,6.088,80.724,5.979,80.496,5.949,80.267,6.01,80.095,6.153,80.007,6.364,79.947,6.585,79.859,6.829,79.792,7.585,79.76,7.796,79.708,8.066,79.713,8.182,79.75,8.294,79.75,8.049,79.783,8.018,79.809,8.05,79.832,8.304,79.851,8.412,79.942,8.692,79.944,8.741,79.928,8.846,79.929,8.899,80.065,9.096,80.1,9.21,80.118,9.327,80.111,9.453,80.086,9.578,80.196,9.538,80.256,9.495,80.318,9.465,80.368,9.48,80.428,9.481,80.385,9.549,80.258,9.611,80.046,9.65,79.979,9.699,79.954,9.742,79.967,9.793,79.982,9.813]]]},{"n":"Spain","c":"ES","r":2,"ml":2,"lx":-3.46,"ly":40.09,"b":[-18.161,27.646,4.322,43.765],"p":[[[1.594,38.672,1.571,38.659,1.505,38.671,1.406,38.671,1.402,38.711,1.417,38.74,1.436,38.768,1.497,38.712,1.593,38.701,1.594,38.672]],[[3.145,39.79,3.241,39.757,3.342,39.787,3.396,39.777,3.449,39.761,3.462,39.698,3.415,39.627,3.349,39.556,3.293,39.477,3.245,39.387,3.155,39.333,3.073,39.301,2.9,39.368,2.8,39.385,2.77,39.41,2.746,39.51,2.701,39.542,2.634,39.556,2.576,39.531,2.5,39.478,2.459,39.53,2.394,39.54,2.37,39.572,2.371,39.613,2.785,39.855,2.905,39.908,3.159,39.971,3.198,39.961,3.164,39.924,3.167,39.908,3.199,39.89,3.191,39.861,3.159,39.837,3.145,39.79]],[[4.294,39.842,4.275,39.83,3.968,39.946,3.867,39.959,3.843,39.976,3.845,40.036,3.853,40.063,4.059,40.075,4.226,40.032,4.315,39.917,4.322,39.898,4.294,39.842]],[[1.445,38.919,1.409,38.857,1.257,38.879,1.223,38.904,1.256,38.973,1.3,38.982,1.303,39.031,1.349,39.081,1.564,39.121,1.613,39.087,1.624,39.039,1.495,38.933,1.445,38.919]],[[-1.794,43.407,-1.793,43.373,-1.753,43.325,-1.713,43.307,-1.627,43.282,-1.561,43.279,-1.472,43.268,-1.411,43.24,-1.407,43.197,-1.423,43.149,-1.459,43.105,-1.48,43.071,-1.461,43.052,-1.429,43.037,-1.394,43.033,-1.371,43.038,-1.353,43.064,-1.319,43.097,-1.3,43.101,-1.302,43.082,-1.285,43.06,-1.175,43.021,-0.934,42.95,-0.839,42.948,-0.763,42.94,-0.74,42.91,-0.586,42.799,-0.55,42.802,-0.481,42.799,-0.398,42.808,-0.339,42.829,-0.299,42.825,-0.256,42.804,-0.205,42.785,-0.14,42.749,-0.081,42.704,-0.041,42.689,0.201,42.719,0.255,42.693,0.313,42.693,0.377,42.7,0.518,42.686,0.632,42.69,0.642,42.701,0.652,42.8,0.67,42.836,0.697,42.845,0.764,42.838,1.01,42.779,1.111,42.742,1.208,42.713,1.293,42.71,1.349,42.691,1.428,42.596,1.415,42.548,1.422,42.531,1.43,42.498,1.428,42.461,1.449,42.437,1.486,42.434,1.534,42.442,1.586,42.456,1.679,42.497,1.706,42.503,1.86,42.457,1.928,42.426,1.951,42.393,1.987,42.358,2.033,42.354,2.098,42.386,2.2,42.421,2.374,42.39,2.568,42.346,2.652,42.34,2.655,42.362,2.67,42.393,2.702,42.408,2.749,42.413,2.816,42.429,2.891,42.456,2.97,42.467,3.053,42.447,3.152,42.431,3.211,42.431,3.24,42.368,3.288,42.344,3.307,42.289,3.219,42.26,3.166,42.256,3.15,42.162,3.175,42.136,3.225,42.111,3.238,42.082,3.248,41.944,3.147,41.861,3.005,41.767,2.311,41.467,2.146,41.321,2.083,41.287,1.567,41.196,1.206,41.098,1.033,41.062,0.817,40.892,0.715,40.823,0.796,40.804,0.891,40.722,0.859,40.686,0.721,40.63,0.66,40.613,0.627,40.622,0.596,40.615,0.364,40.319,0.158,40.107,0.043,40.014,-0.075,39.876,-0.327,39.52,-0.329,39.417,-0.205,39.063,-0.134,38.969,-0.034,38.891,0.155,38.825,0.202,38.759,0.136,38.697,-0.053,38.586,-0.381,38.436,-0.521,38.317,-0.551,38.203,-0.647,38.152,-0.683,37.992,-0.742,37.886,-0.753,37.85,-0.815,37.77,-0.823,37.712,-0.722,37.631,-0.772,37.596,-0.822,37.581,-0.938,37.571,-1.328,37.561,-1.641,37.387,-1.798,37.233,-1.939,36.946,-2.112,36.777,-2.188,36.745,-2.306,36.82,-2.453,36.831,-2.596,36.806,-2.671,36.748,-2.788,36.715,-2.902,36.743,-3.149,36.758,-3.259,36.756,-3.431,36.708,-3.579,36.74,-3.828,36.756,-4.367,36.718,-4.435,36.7,-4.502,36.629,-4.674,36.506,-4.935,36.502,-5.171,36.424,-5.231,36.374,-5.33,36.236,-5.361,36.135,-5.382,36.134,-5.407,36.159,-5.444,36.151,-5.462,36.074,-5.551,36.039,-5.625,36.026,-5.808,36.088,-5.961,36.182,-6.041,36.188,-6.17,36.334,-6.226,36.426,-6.266,36.527,-6.258,36.565,-6.269,36.597,-6.384,36.637,-6.412,36.729,-6.328,36.848,-6.259,36.899,-6.217,36.914,-6.321,36.908,-6.396,36.832,-6.492,36.955,-6.885,37.194,-6.859,37.249,-6.864,37.279,-6.929,37.215,-6.975,37.198,-7.175,37.209,-7.406,37.179,-7.467,37.428,-7.496,37.524,-7.504,37.585,-7.444,37.728,-7.379,37.786,-7.292,37.906,-7.185,38.006,-7.073,38.03,-7.023,38.045,-6.981,38.122,-6.958,38.188,-6.975,38.194,-7.106,38.181,-7.343,38.457,-7.336,38.501,-7.306,38.567,-7.286,38.649,-7.282,38.715,-7.22,38.771,-7.125,38.827,-7.046,38.907,-7.006,38.985,-6.998,39.056,-7.043,39.107,-7.172,39.135,-7.306,39.338,-7.335,39.465,-7.363,39.478,-7.445,39.536,-7.524,39.645,-7.536,39.662,-7.454,39.681,-7.118,39.682,-7.047,39.706,-7.037,39.714,-6.975,39.798,-6.911,39.937,-6.896,40.022,-6.916,40.057,-7.028,40.143,-7.033,40.168,-7.015,40.208,-6.948,40.252,-6.859,40.301,-6.81,40.343,-6.822,40.376,-6.848,40.411,-6.852,40.443,-6.836,40.483,-6.83,40.619,-6.818,40.654,-6.836,40.777,-6.858,40.878,-6.928,41.009,-6.916,41.038,-6.883,41.062,-6.776,41.108,-6.69,41.215,-6.566,41.304,-6.403,41.375,-6.289,41.455,-6.244,41.516,-6.212,41.532,-6.222,41.56,-6.243,41.602,-6.308,41.642,-6.392,41.665,-6.485,41.664,-6.542,41.673,-6.559,41.704,-6.553,41.79,-6.558,41.874,-6.575,41.913,-6.618,41.942,-6.704,41.935,-6.777,41.958,-6.833,41.964,-6.866,41.945,-7.03,41.951,-7.099,41.964,-7.147,41.981,-7.178,41.972,-7.195,41.955,-7.198,41.929,-7.21,41.895,-7.269,41.864,-7.404,41.834,-7.513,41.836,-7.613,41.858,-7.645,41.874,-7.693,41.888,-7.896,41.871,-7.921,41.884,-7.991,41.852,-8.094,41.814,-8.152,41.812,-8.174,41.82,-8.181,41.837,-8.225,41.896,-8.213,41.927,-8.13,42.018,-8.139,42.04,-8.174,42.069,-8.204,42.112,-8.213,42.134,-8.266,42.137,-8.323,42.115,-8.538,42.069,-8.59,42.053,-8.683,42.008,-8.777,41.941,-8.852,41.927,-8.878,41.947,-8.887,42.105,-8.772,42.211,-8.691,42.274,-8.729,42.287,-8.816,42.285,-8.81,42.334,-8.769,42.358,-8.73,42.412,-8.776,42.435,-8.812,42.47,-8.81,42.562,-8.8,42.6,-8.812,42.64,-8.988,42.586,-9.033,42.594,-9.035,42.662,-8.937,42.767,-8.927,42.799,-9.042,42.814,-9.127,42.865,-9.179,42.911,-9.235,42.977,-9.236,43.036,-9.178,43.174,-9.096,43.214,-9.025,43.239,-8.874,43.334,-8.666,43.317,-8.537,43.337,-8.422,43.386,-8.355,43.397,-8.249,43.439,-8.252,43.497,-8.289,43.54,-8.257,43.58,-8.137,43.629,-8.005,43.694,-7.853,43.707,-7.698,43.765,-7.595,43.727,-7.504,43.74,-7.399,43.696,-7.262,43.595,-7.061,43.554,-6.901,43.586,-6.617,43.592,-6.476,43.579,-6.224,43.604,-6.08,43.595,-5.847,43.645,-5.666,43.582,-5.316,43.553,-5.105,43.502,-4.523,43.416,-4.313,43.415,-4.015,43.463,-3.889,43.499,-3.774,43.478,-3.605,43.519,-3.524,43.511,-3.418,43.452,-3.046,43.372,-2.948,43.44,-2.875,43.454,-2.607,43.413,-2.337,43.328,-2.197,43.322,-1.991,43.345,-1.829,43.401,-1.794,43.407]],[[-16.334,28.38,-16.418,28.151,-16.496,28.062,-16.543,28.032,-16.658,28.007,-16.795,28.149,-16.866,28.293,-16.905,28.34,-16.843,28.376,-16.752,28.37,-16.557,28.4,-16.517,28.413,-16.319,28.558,-16.124,28.576,-16.119,28.528,-16.334,28.38]],[[-13.716,28.911,-13.784,28.845,-13.86,28.869,-13.824,29.013,-13.788,29.056,-13.65,29.119,-13.535,29.144,-13.501,29.211,-13.464,29.237,-13.423,29.198,-13.454,29.151,-13.478,29.007,-13.555,28.96,-13.716,28.911]],[[-14.197,28.169,-14.333,28.056,-14.469,28.082,-14.492,28.101,-14.356,28.13,-14.232,28.216,-14.153,28.407,-14.028,28.617,-14.003,28.707,-13.954,28.741,-13.886,28.745,-13.857,28.738,-13.827,28.691,-13.828,28.585,-13.863,28.409,-13.928,28.253,-14.197,28.169]],[[-15.401,28.147,-15.407,28.071,-15.383,27.993,-15.389,27.875,-15.437,27.811,-15.559,27.747,-15.656,27.758,-15.71,27.784,-15.807,27.888,-15.809,27.994,-15.721,28.064,-15.683,28.154,-15.453,28.137,-15.433,28.154,-15.415,28.159,-15.401,28.147]],[[-17.185,28.022,-17.225,28.014,-17.274,28.038,-17.325,28.118,-17.29,28.176,-17.259,28.203,-17.214,28.199,-17.13,28.156,-17.104,28.111,-17.101,28.083,-17.185,28.022]],[[-17.888,27.81,-17.985,27.646,-18.107,27.707,-18.136,27.728,-18.161,27.761,-18.043,27.768,-17.925,27.85,-17.888,27.81]],[[-17.834,28.493,-17.859,28.486,-17.882,28.565,-18.001,28.758,-17.929,28.845,-17.798,28.847,-17.745,28.787,-17.727,28.724,-17.752,28.689,-17.744,28.616,-17.758,28.569,-17.834,28.493]]]},{"n":"South Korea","c":"KR","r":2,"ml":2.5,"lx":128.13,"ly":36.38,"b":[126.008,33.202,130.934,38.623],"p":[[[126.634,37.782,126.665,37.801,126.667,37.828,126.667,37.917,126.754,37.979,126.879,38.106,126.94,38.176,127.01,38.241,127.09,38.284,127.17,38.305,127.294,38.313,127.533,38.305,127.579,38.313,127.746,38.319,127.785,38.308,127.905,38.3,128.039,38.309,128.106,38.327,128.169,38.359,128.223,38.417,128.279,38.524,128.339,38.608,128.375,38.623,128.619,38.176,128.852,37.887,129.052,37.678,129.335,37.275,129.418,37.059,129.426,36.926,129.473,36.742,129.433,36.637,129.445,36.471,129.427,36.385,129.393,36.323,129.391,36.202,129.402,36.138,129.404,36.052,129.426,36.019,129.458,36.006,129.51,36.038,129.573,36.051,129.562,35.948,129.485,35.687,129.419,35.498,129.329,35.333,129.214,35.182,129.077,35.123,128.98,35.102,128.796,35.094,128.643,35.12,128.511,35.101,128.458,35.069,128.419,35.016,128.448,34.932,128.444,34.87,128.388,34.875,128.276,34.911,128.152,34.916,128.095,34.934,128.036,35.022,127.977,35.019,127.873,34.966,127.715,34.955,127.659,34.926,127.639,34.89,127.663,34.843,127.742,34.783,127.715,34.721,127.632,34.69,127.565,34.766,127.524,34.84,127.477,34.844,127.404,34.823,127.39,34.743,127.423,34.688,127.479,34.625,127.401,34.553,127.381,34.501,127.325,34.463,127.173,34.546,127.195,34.605,127.261,34.662,127.269,34.72,127.247,34.755,127.031,34.607,126.897,34.439,126.826,34.451,126.796,34.494,126.755,34.512,126.611,34.404,126.584,34.318,126.531,34.314,126.508,34.351,126.506,34.428,126.482,34.494,126.333,34.59,126.264,34.673,126.301,34.72,126.426,34.695,126.525,34.698,126.505,34.738,126.473,34.756,126.539,34.779,126.593,34.824,126.548,34.837,126.479,34.81,126.421,34.823,126.398,34.933,126.327,35.045,126.291,35.154,126.361,35.217,126.396,35.314,126.46,35.456,126.493,35.501,126.582,35.534,126.614,35.571,126.565,35.59,126.487,35.606,126.488,35.647,126.542,35.669,126.602,35.714,126.717,35.769,126.753,35.872,126.72,35.898,126.647,35.922,126.664,35.975,126.693,36.014,126.682,36.038,126.597,36.105,126.54,36.166,126.557,36.236,126.544,36.341,126.552,36.43,126.548,36.478,126.507,36.586,126.488,36.694,126.433,36.678,126.389,36.651,126.231,36.689,126.181,36.692,126.161,36.772,126.217,36.871,126.352,36.958,126.429,36.969,126.487,37.007,126.578,37.02,126.687,36.96,126.784,36.948,126.839,36.846,126.872,36.824,126.879,36.862,126.958,36.906,126.977,36.939,126.96,36.958,126.869,36.976,126.787,37.103,126.776,37.158,126.746,37.194,126.791,37.295,126.696,37.411,126.65,37.447,126.657,37.551,126.608,37.617,126.58,37.654,126.563,37.717,126.578,37.745,126.621,37.755,126.634,37.782]],[[128.741,34.799,128.647,34.737,128.52,34.82,128.489,34.865,128.586,34.932,128.668,35.009,128.722,35.014,128.741,34.799]],[[128.066,34.806,128.055,34.708,127.984,34.703,127.942,34.766,127.897,34.735,127.873,34.735,127.838,34.813,127.832,34.875,127.915,34.921,127.966,34.893,128.038,34.879,128.066,34.806]],[[126.234,34.371,126.17,34.355,126.134,34.39,126.109,34.399,126.123,34.444,126.227,34.533,126.247,34.563,126.344,34.545,126.38,34.498,126.335,34.426,126.234,34.371]],[[126.521,37.737,126.516,37.605,126.461,37.61,126.423,37.624,126.407,37.649,126.369,37.772,126.412,37.823,126.494,37.783,126.521,37.737]],[[126.753,34.344,126.77,34.296,126.689,34.305,126.646,34.351,126.652,34.39,126.7,34.396,126.753,34.344]],[[127.799,34.615,127.788,34.584,127.737,34.631,127.787,34.682,127.799,34.615]],[[126.418,36.493,126.404,36.428,126.338,36.471,126.319,36.613,126.387,36.571,126.418,36.493]],[[126.172,34.731,126.159,34.707,126.115,34.714,126.071,34.783,126.052,34.838,126.008,34.867,126.078,34.915,126.169,34.83,126.172,34.731]],[[130.916,37.478,130.871,37.449,130.817,37.478,130.81,37.51,130.838,37.537,130.904,37.554,130.934,37.53,130.916,37.478]],[[126.327,33.224,126.282,33.202,126.24,33.215,126.229,33.225,126.179,33.283,126.166,33.312,126.199,33.368,126.338,33.46,126.696,33.549,126.76,33.553,126.901,33.515,126.931,33.444,126.905,33.382,126.873,33.341,126.709,33.272,126.582,33.238,126.327,33.224]]]},{"n":"South Africa","c":"ZA","r":2,"ml":1.7,"lx":23.67,"ly":-29.71,"b":[16.448,-46.963,37.888,-22.146],"p":[[[29.365,-22.194,29.377,-22.193,29.663,-22.146,29.902,-22.184,30.19,-22.291,30.46,-22.329,30.712,-22.298,30.916,-22.291,31.073,-22.308,31.197,-22.345,31.288,-22.402,31.293,-22.455,31.3,-22.479,31.348,-22.618,31.419,-22.825,31.467,-23.017,31.532,-23.279,31.53,-23.426,31.546,-23.482,31.604,-23.553,31.676,-23.674,31.7,-23.743,31.724,-23.795,31.8,-23.892,31.858,-24.04,31.908,-24.236,31.951,-24.33,31.967,-24.376,31.986,-24.461,31.983,-24.638,31.984,-24.844,31.986,-25.074,31.987,-25.263,31.979,-25.359,31.985,-25.632,31.92,-25.774,31.928,-25.885,31.948,-25.958,31.922,-25.969,31.871,-25.982,31.64,-25.867,31.415,-25.747,31.383,-25.743,31.335,-25.756,31.207,-25.843,31.088,-25.981,31.033,-26.098,30.945,-26.219,30.803,-26.413,30.789,-26.455,30.788,-26.614,30.794,-26.764,30.807,-26.785,30.883,-26.792,30.938,-26.916,31.063,-27.112,31.274,-27.238,31.47,-27.296,31.743,-27.31,31.958,-27.306,31.946,-27.174,31.967,-26.961,31.995,-26.817,32.025,-26.811,32.082,-26.825,32.113,-26.839,32.2,-26.833,32.354,-26.862,32.478,-26.858,32.589,-26.856,32.777,-26.851,32.886,-26.849,32.849,-27.08,32.706,-27.442,32.657,-27.607,32.535,-28.2,32.375,-28.498,32.286,-28.621,32.027,-28.84,31.955,-28.884,31.892,-28.912,31.778,-28.937,31.335,-29.378,31.17,-29.591,31.023,-29.901,30.878,-30.071,30.664,-30.434,30.472,-30.715,30.289,-30.97,29.971,-31.322,29.83,-31.424,29.735,-31.47,29.483,-31.675,29.128,-32.003,28.856,-32.294,28.449,-32.625,28.214,-32.769,27.861,-33.054,27.762,-33.096,27.364,-33.361,27.077,-33.521,26.614,-33.707,26.429,-33.76,25.99,-33.711,25.806,-33.737,25.652,-33.85,25.638,-34.011,25.574,-34.035,25.477,-34.028,25.17,-33.961,25.003,-33.974,24.906,-34.06,24.827,-34.169,24.596,-34.175,24.183,-34.062,23.698,-33.993,23.586,-33.985,23.35,-34.069,23.268,-34.081,22.926,-34.063,22.736,-34.01,22.554,-34.01,22.414,-34.054,22.246,-34.069,21.789,-34.373,21.553,-34.373,21.35,-34.408,21.249,-34.407,21.06,-34.365,20.99,-34.367,20.882,-34.387,20.775,-34.44,20.53,-34.463,20.435,-34.509,20.021,-34.786,19.926,-34.775,19.85,-34.757,19.635,-34.753,19.392,-34.606,19.298,-34.615,19.323,-34.571,19.331,-34.492,19.279,-34.437,19.245,-34.412,19.149,-34.417,19.098,-34.35,18.952,-34.344,18.902,-34.361,18.831,-34.364,18.825,-34.296,18.831,-34.254,18.826,-34.188,18.809,-34.108,18.752,-34.083,18.709,-34.072,18.605,-34.077,18.534,-34.086,18.5,-34.109,18.462,-34.168,18.462,-34.347,18.41,-34.296,18.352,-34.188,18.333,-34.074,18.354,-33.939,18.465,-33.888,18.456,-33.796,18.433,-33.717,18.309,-33.514,18.261,-33.422,18.156,-33.359,18.075,-33.207,17.993,-33.152,17.958,-33.046,17.878,-32.962,17.851,-32.827,17.895,-32.75,17.965,-32.709,18.037,-32.775,18.125,-32.749,18.251,-32.652,18.325,-32.505,18.33,-32.27,18.311,-32.122,18.211,-31.742,18.164,-31.655,17.939,-31.383,17.677,-31.019,17.347,-30.445,17.189,-30.1,16.95,-29.403,16.739,-29.009,16.481,-28.642,16.448,-28.618,16.487,-28.573,16.626,-28.488,16.689,-28.465,16.723,-28.475,16.756,-28.452,16.788,-28.395,16.795,-28.341,16.81,-28.265,16.841,-28.219,16.875,-28.128,16.933,-28.07,17.056,-28.031,17.149,-28.082,17.188,-28.133,17.205,-28.199,17.246,-28.231,17.312,-28.229,17.359,-28.269,17.386,-28.353,17.38,-28.414,17.343,-28.452,17.348,-28.501,17.396,-28.563,17.416,-28.621,17.448,-28.698,17.617,-28.743,17.699,-28.768,17.842,-28.777,17.976,-28.811,18.103,-28.872,18.311,-28.886,18.6,-28.855,18.839,-28.869,19.026,-28.928,19.162,-28.939,19.246,-28.902,19.282,-28.848,19.271,-28.778,19.313,-28.733,19.407,-28.714,19.483,-28.662,19.54,-28.575,19.671,-28.504,19.878,-28.449,19.98,-28.451,19.98,-28.31,19.98,-27.866,19.98,-27.421,19.98,-26.976,19.98,-26.531,19.98,-26.086,19.98,-25.642,19.98,-25.197,19.98,-24.777,20.029,-24.807,20.345,-25.03,20.431,-25.147,20.473,-25.221,20.609,-25.491,20.711,-25.733,20.793,-25.916,20.799,-25.999,20.811,-26.081,20.823,-26.121,20.815,-26.165,20.757,-26.264,20.698,-26.34,20.627,-26.444,20.62,-26.581,20.641,-26.742,20.685,-26.822,20.74,-26.849,20.871,-26.809,20.954,-26.821,21.071,-26.852,21.455,-26.833,21.501,-26.843,21.646,-26.854,21.695,-26.841,21.738,-26.807,21.788,-26.71,21.833,-26.678,21.915,-26.662,22.011,-26.636,22.091,-26.58,22.218,-26.389,22.471,-26.219,22.549,-26.178,22.598,-26.133,22.64,-26.071,22.729,-25.857,22.796,-25.679,22.819,-25.595,22.879,-25.458,22.951,-25.37,23.022,-25.324,23.058,-25.312,23.149,-25.289,23.266,-25.267,23.389,-25.291,23.521,-25.344,23.671,-25.434,23.823,-25.545,23.894,-25.601,23.97,-25.626,24.104,-25.635,24.193,-25.633,24.331,-25.743,24.4,-25.75,24.556,-25.783,24.748,-25.817,24.869,-25.813,24.999,-25.754,25.092,-25.751,25.213,-25.756,25.346,-25.74,25.444,-25.714,25.518,-25.663,25.584,-25.606,25.659,-25.438,25.703,-25.302,25.77,-25.146,25.852,-24.935,25.882,-24.788,25.912,-24.747,26.032,-24.702,26.131,-24.671,26.397,-24.614,26.452,-24.583,26.502,-24.513,26.618,-24.396,26.761,-24.297,26.835,-24.241,26.971,-23.763,26.987,-23.705,27.086,-23.578,27.146,-23.524,27.186,-23.523,27.241,-23.49,27.313,-23.424,27.399,-23.384,27.499,-23.368,27.563,-23.325,27.593,-23.253,27.644,-23.218,27.717,-23.22,27.758,-23.197,27.769,-23.149,27.813,-23.108,27.891,-23.074,27.931,-23.034,27.935,-22.987,28.028,-22.874,28.21,-22.694,28.382,-22.593,28.543,-22.573,28.696,-22.535,28.84,-22.481,28.946,-22.395,29.013,-22.278,29.13,-22.213,29.365,-22.194],[28.737,-30.102,28.901,-30.038,28.975,-29.999,29.029,-29.968,29.098,-29.919,29.122,-29.801,29.142,-29.701,29.195,-29.652,29.249,-29.619,29.294,-29.567,29.349,-29.442,29.387,-29.32,29.391,-29.27,29.371,-29.218,29.336,-29.164,29.301,-29.09,29.26,-29.078,29.178,-29.037,29.058,-28.954,28.954,-28.881,28.856,-28.776,28.816,-28.759,28.722,-28.688,28.681,-28.647,28.653,-28.598,28.626,-28.582,28.583,-28.594,28.472,-28.616,28.233,-28.701,28.084,-28.78,27.96,-28.873,27.83,-28.909,27.736,-28.94,27.66,-29.047,27.59,-29.146,27.527,-29.236,27.491,-29.277,27.458,-29.303,27.425,-29.36,27.357,-29.455,27.295,-29.519,27.207,-29.554,27.095,-29.599,27.057,-29.626,27.052,-29.664,27.092,-29.754,27.13,-29.84,27.194,-29.941,27.24,-30.015,27.313,-30.106,27.355,-30.159,27.35,-30.247,27.364,-30.279,27.388,-30.316,27.409,-30.325,27.431,-30.338,27.492,-30.364,27.507,-30.381,27.549,-30.411,27.59,-30.466,27.667,-30.542,27.753,-30.6,27.902,-30.624,28.018,-30.642,28.057,-30.631,28.096,-30.585,28.129,-30.525,28.139,-30.45,28.176,-30.41,28.315,-30.218,28.392,-30.148,28.439,-30.142,28.5,-30.129,28.577,-30.123,28.634,-30.129,28.647,-30.127,28.737,-30.102]],[[37.857,-46.944,37.814,-46.963,37.612,-46.946,37.59,-46.908,37.65,-46.849,37.685,-46.824,37.79,-46.837,37.873,-46.885,37.888,-46.902,37.857,-46.944]]]},{"n":"Somalia","c":"SO","r":6,"ml":4,"lx":45.19,"ly":3.57,"b":[40.964,-1.695,51.39,11.984],"p":[[[41.533,-1.695,41.538,-1.613,41.522,-1.572,41.427,-1.45,41.25,-1.221,41.116,-1.047,40.979,-0.87,40.978,-0.729,40.977,-0.307,40.973,0.535,40.97,1.378,40.967,2.221,40.965,2.642,40.964,2.815,40.979,2.842,41.135,2.997,41.342,3.202,41.613,3.59,41.761,3.802,41.884,3.978,41.915,4.031,42.024,4.138,42.228,4.202,42.355,4.212,42.792,4.292,42.857,4.324,42.895,4.361,42.931,4.445,43.016,4.563,43.126,4.644,43.334,4.75,43.538,4.84,43.583,4.855,43.829,4.911,43.889,4.931,43.989,4.951,44.028,4.951,44.37,4.931,44.637,4.916,44.912,4.9,44.941,4.912,45.133,5.122,45.438,5.455,45.634,5.668,45.935,5.997,46.167,6.235,46.423,6.497,46.672,6.737,46.971,7.026,47.16,7.208,47.453,7.49,47.732,7.759,47.978,7.997,48.127,8.222,48.273,8.443,48.429,8.68,48.617,8.965,48.794,9.233,48.938,9.452,48.938,9.564,48.938,9.808,48.938,9.973,48.938,10.433,48.938,10.714,48.938,10.982,48.939,11.258,49.062,11.271,49.388,11.343,49.642,11.451,50.11,11.529,50.466,11.728,50.528,11.823,50.636,11.944,50.792,11.984,51.191,11.842,51.255,11.831,51.232,11.745,51.218,11.658,51.136,11.505,51.084,11.336,51.122,11.077,51.141,10.657,51.131,10.596,51.105,10.536,51.094,10.489,51.051,10.472,51.032,10.445,51.063,10.434,51.188,10.48,51.186,10.53,51.193,10.555,51.296,10.499,51.369,10.475,51.39,10.423,51.385,10.387,51.268,10.403,51.209,10.431,51.036,10.385,50.93,10.336,50.898,10.253,50.874,9.924,50.833,9.71,50.825,9.428,50.685,9.241,50.638,9.109,50.43,8.845,50.321,8.62,50.286,8.509,50.103,8.2,49.852,7.963,49.761,7.66,49.671,7.47,49.57,7.297,49.349,6.991,49.235,6.777,49.093,6.408,49.049,6.174,48.649,5.494,48.234,4.953,47.975,4.497,47.511,3.968,46.879,3.286,46.051,2.475,45.826,2.31,44.92,1.81,44.333,1.391,44.033,1.106,43.718,0.858,43.468,0.622,42.712,-0.176,42.634,-0.251,42.561,-0.321,42.466,-0.457,42.399,-0.51,42.219,-0.738,42.106,-0.856,41.98,-0.973,41.926,-1.056,41.888,-1.151,41.846,-1.203,41.732,-1.43,41.632,-1.579,41.533,-1.695]]]},{"n":"Somaliland","c":"","r":5,"ml":4.5,"lx":46.73,"ly":9.44,"b":[42.656,7.997,48.939,11.5],"p":[[[48.939,11.258,48.938,10.982,48.938,10.714,48.938,10.433,48.938,9.973,48.938,9.808,48.938,9.564,48.938,9.452,48.794,9.233,48.617,8.965,48.429,8.68,48.273,8.443,48.127,8.222,47.978,7.997,47.638,7.997,47.306,7.997,46.978,7.997,46.92,8.026,46.645,8.118,46.296,8.235,45.863,8.38,45.555,8.483,45.227,8.591,44.894,8.7,44.632,8.786,44.306,8.893,44.023,8.986,43.984,9.009,43.827,9.151,43.621,9.337,43.581,9.341,43.483,9.379,43.394,9.48,43.303,9.609,43.218,9.77,43.182,9.88,43.069,9.926,43.015,10.013,42.913,10.141,42.842,10.203,42.816,10.257,42.784,10.37,42.725,10.492,42.669,10.568,42.656,10.6,42.66,10.621,42.763,10.787,42.81,10.846,42.863,10.903,42.906,10.96,42.923,10.999,43.049,11.194,43.159,11.366,43.246,11.5,43.441,11.346,43.631,11.035,43.853,10.784,44.158,10.551,44.279,10.472,44.387,10.43,44.943,10.437,45.338,10.65,45.696,10.804,45.817,10.836,46.025,10.794,46.254,10.781,46.46,10.734,46.565,10.746,46.973,10.925,47.23,11.1,47.405,11.174,47.474,11.175,47.713,11.112,48.019,11.139,48.439,11.29,48.573,11.321,48.674,11.323,48.903,11.255,48.939,11.258]]]},{"n":"Solomon Islands","c":"SB","r":3,"ml":3,"lx":159.17,"ly":-8.03,"b":[155.678,-11.832,166.929,-6.609],"p":[[[159.688,-8.508,159.64,-8.521,159.569,-8.485,159.538,-8.451,159.553,-8.399,159.595,-8.379,159.642,-8.414,159.646,-8.45,159.688,-8.508]],[[166.929,-11.665,166.841,-11.681,166.806,-11.677,166.747,-11.591,166.791,-11.571,166.855,-11.579,166.875,-11.63,166.929,-11.665]],[[166.133,-10.758,166.053,-10.775,166.028,-10.77,165.968,-10.779,165.904,-10.851,165.857,-10.841,165.819,-10.844,165.791,-10.785,165.79,-10.756,165.836,-10.761,165.86,-10.703,165.909,-10.674,166.024,-10.661,166.126,-10.68,166.162,-10.693,166.13,-10.745,166.133,-10.758]],[[160.576,-11.798,160.507,-11.832,160.443,-11.815,160.395,-11.789,160.355,-11.712,160.27,-11.664,160.15,-11.644,160.1,-11.611,160.087,-11.594,160.004,-11.58,159.979,-11.538,159.986,-11.495,160,-11.472,160.077,-11.493,160.449,-11.696,160.537,-11.759,160.576,-11.798]],[[161.548,-9.626,161.559,-9.733,161.554,-9.77,161.478,-9.691,161.442,-9.719,161.41,-9.682,161.412,-9.6,161.417,-9.514,161.402,-9.448,161.364,-9.353,161.407,-9.368,161.548,-9.626]],[[159.189,-9.124,159.175,-9.126,159.128,-9.114,159.071,-9.11,159.036,-9.075,159.078,-9.025,159.13,-8.993,159.154,-9.001,159.176,-9.022,159.228,-9.03,159.234,-9.094,159.189,-9.124]],[[160.168,-8.996,160.226,-9.01,160.254,-9.007,160.319,-9.061,160.408,-9.14,160.371,-9.181,160.3,-9.16,160.276,-9.169,160.268,-9.163,160.253,-9.123,160.175,-9.084,160.105,-9.081,160.096,-9.034,160.168,-8.996]],[[157.763,-8.242,157.826,-8.324,157.898,-8.506,157.885,-8.569,157.834,-8.573,157.819,-8.612,157.749,-8.524,157.656,-8.5,157.588,-8.445,157.565,-8.338,157.558,-8.27,157.504,-8.258,157.351,-8.275,157.302,-8.333,157.232,-8.315,157.218,-8.263,157.229,-8.212,157.322,-8.161,157.341,-8.096,157.433,-7.985,157.491,-7.966,157.599,-8.006,157.612,-8.165,157.651,-8.217,157.763,-8.242]],[[158.201,-8.822,158.179,-8.826,158.155,-8.786,158.21,-8.678,158.236,-8.765,158.253,-8.797,158.201,-8.822]],[[158.108,-8.684,158.009,-8.763,157.938,-8.736,157.879,-8.669,157.898,-8.587,157.909,-8.566,157.938,-8.561,157.967,-8.544,157.998,-8.508,158.105,-8.537,158.132,-8.557,158.068,-8.607,158.09,-8.623,158.104,-8.646,158.108,-8.684]],[[157.389,-8.713,157.389,-8.728,157.334,-8.7,157.212,-8.565,157.234,-8.52,157.345,-8.432,157.379,-8.421,157.411,-8.475,157.383,-8.555,157.347,-8.575,157.332,-8.651,157.389,-8.713]],[[156.688,-7.923,156.669,-7.937,156.635,-7.883,156.611,-7.866,156.612,-7.806,156.511,-7.708,156.502,-7.64,156.561,-7.574,156.64,-7.613,156.718,-7.696,156.809,-7.723,156.79,-7.778,156.708,-7.877,156.688,-7.923]],[[156.604,-8.172,156.592,-8.196,156.54,-8.073,156.542,-8.011,156.551,-7.971,156.57,-7.959,156.612,-8.096,156.604,-8.172]],[[157.172,-8.108,157.15,-8.123,157.041,-8.117,156.958,-8.014,156.959,-7.938,157.024,-7.868,157.103,-7.855,157.146,-7.883,157.186,-7.941,157.201,-8.016,157.192,-8.082,157.172,-8.108]],[[155.84,-7.097,155.739,-7.121,155.678,-7.089,155.705,-7.013,155.739,-6.973,155.865,-7.043,155.84,-7.097]],[[157.645,-8.759,157.643,-8.794,157.586,-8.783,157.458,-8.73,157.454,-8.706,157.526,-8.697,157.579,-8.704,157.623,-8.735,157.645,-8.759]],[[159.75,-9.273,159.971,-9.433,160.065,-9.419,160.355,-9.422,160.525,-9.536,160.625,-9.589,160.682,-9.692,160.751,-9.715,160.794,-9.767,160.819,-9.863,160.802,-9.878,160.713,-9.914,160.649,-9.929,160.482,-9.895,160.321,-9.821,160.002,-9.812,159.854,-9.792,159.803,-9.763,159.755,-9.726,159.68,-9.637,159.622,-9.532,159.612,-9.471,159.607,-9.354,159.626,-9.311,159.686,-9.269,159.75,-9.273]],[[159.879,-8.534,159.881,-8.557,159.746,-8.474,159.645,-8.372,159.354,-8.26,159.292,-8.203,159.239,-8.196,159.09,-8.103,158.944,-8.041,158.855,-7.96,158.832,-7.927,158.778,-7.907,158.686,-7.818,158.597,-7.759,158.565,-7.651,158.479,-7.577,158.457,-7.545,158.734,-7.604,158.863,-7.722,158.972,-7.789,159.011,-7.837,159.109,-7.904,159.198,-7.91,159.287,-7.976,159.368,-7.994,159.431,-8.029,159.843,-8.327,159.794,-8.406,159.849,-8.463,159.879,-8.534]],[[157.487,-7.33,157.519,-7.366,157.441,-7.426,157.339,-7.393,157.317,-7.359,157.315,-7.342,157.243,-7.353,157.102,-7.324,156.904,-7.18,156.696,-6.911,156.495,-6.762,156.457,-6.715,156.453,-6.638,156.479,-6.609,156.604,-6.641,156.765,-6.764,157.03,-6.892,157.103,-6.957,157.148,-7.114,157.193,-7.16,157.336,-7.28,157.412,-7.309,157.452,-7.314,157.487,-7.33]],[[160.749,-8.314,160.998,-8.612,160.988,-8.665,160.954,-8.699,160.944,-8.799,160.976,-8.837,161.043,-8.855,161.159,-8.962,161.205,-9.092,161.209,-9.133,161.257,-9.192,161.258,-9.317,161.368,-9.49,161.378,-9.574,161.367,-9.611,161.322,-9.59,161.191,-9.393,161.042,-9.308,161.024,-9.271,160.873,-9.157,160.772,-8.964,160.663,-8.621,160.714,-8.539,160.59,-8.373,160.596,-8.328,160.649,-8.338,160.685,-8.336,160.702,-8.317,160.749,-8.314]],[[161.715,-10.387,161.841,-10.446,161.914,-10.436,162.023,-10.477,162.105,-10.454,162.157,-10.506,162.287,-10.71,162.288,-10.776,162.373,-10.823,162.301,-10.832,162.201,-10.808,162.124,-10.824,162.043,-10.785,161.906,-10.764,161.787,-10.717,161.538,-10.566,161.539,-10.491,161.499,-10.455,161.487,-10.361,161.398,-10.332,161.294,-10.326,161.286,-10.282,161.305,-10.204,161.382,-10.206,161.476,-10.238,161.654,-10.352,161.698,-10.371,161.715,-10.387]]]},{"n":"Slovakia","c":"SK","r":6,"ml":4,"lx":19.05,"ly":48.73,"b":[16.863,47.763,22.539,49.598],"p":[[[22.539,49.073,22.524,49.031,22.483,48.983,22.432,48.934,22.389,48.873,22.333,48.745,22.295,48.686,22.143,48.569,22.132,48.405,22.111,48.393,21.767,48.338,21.721,48.347,21.675,48.378,21.649,48.401,21.633,48.419,21.603,48.464,21.563,48.496,21.505,48.522,21.451,48.552,21.382,48.553,21.196,48.511,21.067,48.506,20.981,48.52,20.867,48.546,20.643,48.55,20.49,48.527,20.475,48.495,20.334,48.296,20.129,48.222,19.95,48.147,19.899,48.131,19.81,48.155,19.709,48.2,19.625,48.223,19.564,48.213,19.497,48.162,19.467,48.111,19.265,48.073,18.914,48.051,18.792,48,18.75,47.939,18.748,47.893,18.778,47.853,18.741,47.806,18.724,47.787,18.476,47.777,18.146,47.763,17.948,47.767,17.762,47.77,17.635,47.81,17.481,47.888,17.317,47.991,17.302,47.993,17.277,48.004,17.175,48.012,17.147,48.006,17.086,48.04,17.068,48.083,16.973,48.198,16.865,48.387,16.863,48.441,16.904,48.504,16.943,48.551,16.949,48.589,16.953,48.599,16.985,48.677,17.063,48.781,17.136,48.841,17.188,48.861,17.297,48.843,17.483,48.828,17.625,48.842,17.758,48.888,17.831,48.929,17.893,48.971,17.913,48.999,17.941,49.012,18.051,49.037,18.086,49.065,18.1,49.119,18.11,49.18,18.133,49.225,18.161,49.257,18.365,49.336,18.383,49.364,18.416,49.391,18.476,49.421,18.535,49.465,18.596,49.491,18.676,49.488,18.75,49.494,18.807,49.509,18.832,49.511,18.938,49.498,18.957,49.448,18.968,49.396,19.149,49.4,19.25,49.511,19.302,49.525,19.386,49.564,19.442,49.598,19.48,49.576,19.535,49.505,19.593,49.447,19.627,49.424,19.63,49.407,19.664,49.396,19.73,49.39,19.774,49.372,19.787,49.319,19.788,49.27,19.767,49.235,19.757,49.204,19.802,49.192,19.869,49.204,19.916,49.221,20.058,49.181,20.108,49.271,20.164,49.316,20.237,49.338,20.303,49.366,20.363,49.385,20.405,49.384,20.423,49.392,20.475,49.39,20.535,49.381,20.616,49.392,20.729,49.37,20.8,49.329,20.868,49.315,20.947,49.317,21.001,49.34,21.079,49.418,21.136,49.417,21.225,49.429,21.35,49.429,21.64,49.412,21.712,49.382,21.89,49.343,21.968,49.299,22.002,49.246,22.02,49.21,22.203,49.153,22.473,49.081,22.539,49.073]]]},{"n":"Slovenia","c":"SI","r":6,"ml":5,"lx":14.92,"ly":46.06,"b":[13.378,45.428,16.516,46.863],"p":[[[16.516,46.5,16.428,46.524,16.321,46.535,16.301,46.521,16.258,46.508,16.237,46.484,16.253,46.389,16.227,46.373,16.106,46.382,16.067,46.371,16.001,46.305,15.933,46.278,15.848,46.258,15.784,46.234,15.704,46.213,15.636,46.201,15.609,46.172,15.593,46.14,15.597,46.109,15.666,46.048,15.676,45.984,15.668,45.904,15.652,45.862,15.625,45.834,15.454,45.798,15.277,45.733,15.273,45.718,15.354,45.66,15.357,45.646,15.29,45.613,15.284,45.58,15.291,45.542,15.327,45.502,15.339,45.467,15.242,45.441,15.11,45.451,14.955,45.5,14.9,45.493,14.847,45.467,14.793,45.478,14.734,45.508,14.65,45.571,14.609,45.61,14.592,45.651,14.569,45.657,14.534,45.645,14.505,45.595,14.427,45.506,14.37,45.481,14.283,45.487,14.161,45.485,14.086,45.478,13.993,45.509,13.97,45.503,13.97,45.483,13.936,45.45,13.879,45.428,13.615,45.477,13.578,45.517,13.637,45.536,13.72,45.588,13.776,45.582,13.845,45.593,13.875,45.615,13.831,45.68,13.722,45.761,13.663,45.792,13.583,45.812,13.57,45.834,13.614,45.962,13.601,45.98,13.509,45.974,13.488,45.987,13.48,46.009,13.486,46.04,13.548,46.089,13.617,46.133,13.635,46.158,13.633,46.177,13.545,46.197,13.492,46.217,13.45,46.224,13.421,46.212,13.4,46.225,13.378,46.262,13.4,46.318,13.479,46.369,13.563,46.415,13.637,46.449,13.68,46.463,13.7,46.52,13.744,46.514,13.831,46.511,13.929,46.498,14.02,46.482,14.1,46.462,14.267,46.441,14.42,46.428,14.466,46.416,14.504,46.417,14.55,46.4,14.577,46.413,14.597,46.436,14.68,46.463,14.757,46.499,14.811,46.545,14.841,46.58,14.893,46.606,14.949,46.613,15.001,46.626,15.217,46.643,15.439,46.63,15.545,46.655,15.633,46.698,15.76,46.711,15.767,46.711,15.958,46.678,15.972,46.697,15.98,46.706,15.977,46.801,16.037,46.845,16.093,46.863,16.284,46.857,16.308,46.828,16.318,46.783,16.335,46.722,16.367,46.705,16.385,46.681,16.381,46.639,16.418,46.607,16.506,46.522,16.516,46.5]]]},{"n":"Singapore","c":"SG","r":6,"ml":4,"lx":103.82,"ly":1.37,"b":[103.65,1.265,103.996,1.447],"p":[[[103.97,1.331,103.82,1.265,103.65,1.326,103.705,1.423,103.818,1.447,103.909,1.416,103.961,1.392,103.996,1.365,103.97,1.331]]]},{"n":"Sierra Leone","c":"SL","r":4,"ml":4,"lx":-11.76,"ly":8.62,"b":[-13.293,6.907,-10.283,9.997],"p":[[[-10.283,8.485,-10.286,8.454,-10.315,8.311,-10.36,8.188,-10.39,8.158,-10.517,8.125,-10.571,8.071,-10.618,7.896,-10.647,7.759,-10.691,7.736,-10.878,7.538,-11,7.463,-11.085,7.399,-11.166,7.314,-11.268,7.233,-11.377,7.095,-11.455,6.951,-11.508,6.907,-11.548,6.947,-11.733,7.089,-11.929,7.184,-12.347,7.342,-12.486,7.386,-12.481,7.442,-12.433,7.545,-12.51,7.666,-12.48,7.753,-12.51,7.753,-12.57,7.701,-12.698,7.716,-12.782,7.791,-12.851,7.819,-12.881,7.857,-12.925,8.055,-12.957,8.145,-13.021,8.201,-13.149,8.215,-13.202,8.336,-13.273,8.43,-13.261,8.488,-13.203,8.484,-13.158,8.442,-13.085,8.425,-12.994,8.526,-12.913,8.582,-12.894,8.63,-12.904,8.656,-12.953,8.615,-13.088,8.626,-13.122,8.589,-13.182,8.577,-13.228,8.696,-13.226,8.766,-13.207,8.843,-13.071,8.856,-13.059,8.881,-13.154,8.898,-13.272,8.987,-13.293,9.049,-13.234,9.07,-13.178,9.061,-13.13,9.048,-13.077,9.07,-13.028,9.104,-12.999,9.147,-12.959,9.263,-12.831,9.302,-12.756,9.374,-12.684,9.484,-12.652,9.562,-12.622,9.601,-12.604,9.634,-12.59,9.671,-12.558,9.705,-12.524,9.787,-12.501,9.862,-12.428,9.898,-12.278,9.93,-12.142,9.875,-11.923,9.923,-11.911,9.993,-11.71,9.994,-11.472,9.995,-11.274,9.997,-11.206,9.978,-11.181,9.925,-11.116,9.843,-11.047,9.786,-10.963,9.662,-10.865,9.516,-10.759,9.385,-10.691,9.314,-10.683,9.289,-10.688,9.261,-10.721,9.194,-10.75,9.122,-10.747,9.095,-10.727,9.082,-10.616,9.059,-10.606,8.979,-10.606,8.868,-10.552,8.764,-10.501,8.688,-10.503,8.66,-10.628,8.53,-10.677,8.401,-10.702,8.364,-10.712,8.335,-10.687,8.322,-10.653,8.33,-10.604,8.319,-10.558,8.316,-10.496,8.362,-10.394,8.481,-10.36,8.496,-10.283,8.485]],[[-12.526,7.436,-12.541,7.41,-12.607,7.475,-12.952,7.571,-12.854,7.622,-12.615,7.637,-12.544,7.607,-12.512,7.582,-12.501,7.535,-12.526,7.436]]]},{"n":"Seychelles","c":"SC","r":6,"ml":5,"lx":55.48,"ly":-4.68,"b":[55.383,-4.786,55.543,-4.559],"p":[[[55.54,-4.693,55.543,-4.786,55.495,-4.755,55.481,-4.695,55.417,-4.65,55.383,-4.609,55.456,-4.559,55.54,-4.693]]]},{"n":"Serbia","c":"RS","r":5,"ml":4,"lx":20.79,"ly":44.19,"b":[18.839,42.242,22.977,46.169],"p":[[[21.36,44.827,21.523,44.79,21.597,44.755,21.636,44.71,21.74,44.681,21.909,44.666,22.027,44.62,22.093,44.542,22.201,44.561,22.351,44.676,22.498,44.706,22.642,44.651,22.721,44.606,22.734,44.57,22.701,44.556,22.62,44.562,22.554,44.54,22.502,44.49,22.495,44.435,22.531,44.378,22.582,44.338,22.648,44.316,22.683,44.286,22.688,44.248,22.705,44.238,22.667,44.22,22.627,44.194,22.603,44.149,22.597,44.075,22.469,44.018,22.421,44.007,22.399,43.97,22.365,43.862,22.37,43.781,22.387,43.74,22.395,43.707,22.436,43.665,22.474,43.602,22.499,43.519,22.555,43.454,22.697,43.391,22.768,43.354,22.82,43.301,22.86,43.252,22.977,43.188,22.968,43.142,22.942,43.097,22.915,43.076,22.857,43.018,22.8,42.986,22.706,42.884,22.558,42.878,22.523,42.87,22.467,42.842,22.439,42.792,22.466,42.751,22.463,42.709,22.436,42.629,22.472,42.543,22.524,42.504,22.532,42.481,22.524,42.441,22.446,42.359,22.422,42.329,22.344,42.314,22.317,42.322,22.277,42.35,22.24,42.358,22.147,42.325,22.052,42.305,21.978,42.32,21.904,42.322,21.853,42.308,21.815,42.303,21.739,42.268,21.618,42.242,21.563,42.248,21.542,42.281,21.519,42.328,21.53,42.35,21.61,42.387,21.619,42.423,21.731,42.595,21.752,42.652,21.753,42.67,21.724,42.682,21.663,42.681,21.391,42.751,21.403,42.832,21.323,42.875,21.237,42.913,21.223,42.956,21.127,43.043,21.057,43.092,20.968,43.116,20.891,43.152,20.844,43.173,20.824,43.214,20.824,43.238,20.801,43.261,20.763,43.259,20.701,43.226,20.623,43.199,20.61,43.178,20.638,43.13,20.658,43.1,20.649,43.071,20.624,43.034,20.475,42.953,20.458,42.925,20.487,42.879,20.469,42.858,20.344,42.828,20.348,42.853,20.34,42.893,20.268,42.935,20.168,42.969,19.944,43.082,19.858,43.097,19.781,43.11,19.671,43.164,19.614,43.173,19.552,43.212,19.415,43.343,19.298,43.414,19.219,43.45,19.196,43.485,19.192,43.521,19.194,43.533,19.254,43.584,19.301,43.592,19.36,43.593,19.4,43.568,19.451,43.562,19.48,43.595,19.495,43.643,19.488,43.704,19.364,43.845,19.257,43.943,19.245,43.965,19.268,43.983,19.305,43.993,19.345,43.985,19.449,43.978,19.55,43.987,19.584,44.011,19.584,44.043,19.547,44.073,19.43,44.154,19.339,44.226,19.232,44.281,19.152,44.303,19.128,44.33,19.118,44.36,19.127,44.415,19.132,44.484,19.151,44.527,19.223,44.61,19.292,44.697,19.334,44.781,19.357,44.859,19.349,44.881,19.313,44.897,19.237,44.914,19.132,44.9,19.042,44.871,19.007,44.869,18.996,44.904,19.01,44.919,19.038,44.918,19.061,44.911,19.085,44.927,19.1,44.974,19.063,45.137,19.13,45.152,19.131,45.175,19.137,45.196,19.206,45.168,19.303,45.167,19.388,45.173,19.401,45.189,19.4,45.213,19.382,45.231,19.352,45.245,19.33,45.268,19.273,45.278,19.093,45.337,19.005,45.4,19.008,45.466,19.033,45.502,19.064,45.515,19.055,45.527,18.954,45.558,18.918,45.601,18.947,45.656,18.895,45.767,18.839,45.836,18.894,45.866,18.901,45.908,18.905,45.932,18.928,45.931,19.016,45.96,19.048,45.983,19.066,46.01,19.087,46.016,19.146,45.987,19.208,45.984,19.278,46.003,19.33,46.029,19.393,46.05,19.421,46.064,19.458,46.087,19.531,46.155,19.613,46.169,19.725,46.152,19.844,46.146,19.934,46.161,20.161,46.142,20.21,46.126,20.242,46.109,20.301,46.051,20.359,45.975,20.438,45.941,20.533,45.9,20.581,45.869,20.653,45.779,20.709,45.735,20.728,45.737,20.747,45.749,20.76,45.758,20.775,45.75,20.776,45.723,20.779,45.662,20.766,45.597,20.786,45.536,20.787,45.517,20.772,45.5,20.774,45.484,20.794,45.468,20.871,45.428,20.942,45.365,21.024,45.322,21.1,45.294,21.148,45.292,21.226,45.241,21.382,45.205,21.431,45.193,21.465,45.172,21.49,45.148,21.492,45.122,21.468,45.11,21.434,45.075,21.421,45.033,21.396,45.022,21.371,45.021,21.353,45.009,21.357,44.991,21.378,44.973,21.41,44.958,21.472,44.942,21.533,44.919,21.532,44.901,21.52,44.881,21.442,44.873,21.384,44.87,21.358,44.862,21.36,44.827]]]},{"n":"Senegal","c":"SN","r":3,"ml":2.7,"lx":-14.78,"ly":15.14,"b":[-17.536,12.328,-11.382,16.679],"p":[[[-12.281,14.809,-12.187,14.648,-12.207,14.571,-12.228,14.459,-12.175,14.377,-12.113,14.323,-12.068,14.274,-12.019,14.206,-12.011,14.072,-12.02,13.975,-11.988,13.931,-11.961,13.875,-11.966,13.829,-11.984,13.788,-12.044,13.734,-12.054,13.633,-11.957,13.511,-11.895,13.444,-11.895,13.406,-11.878,13.365,-11.832,13.316,-11.803,13.327,-11.772,13.367,-11.758,13.395,-11.674,13.382,-11.635,13.37,-11.581,13.29,-11.562,13.237,-11.549,13.17,-11.493,13.087,-11.444,13.028,-11.434,12.992,-11.39,12.942,-11.417,12.832,-11.414,12.775,-11.444,12.628,-11.451,12.558,-11.449,12.532,-11.382,12.479,-11.389,12.404,-11.457,12.418,-11.574,12.426,-11.808,12.387,-11.889,12.403,-12.042,12.398,-12.152,12.377,-12.291,12.328,-12.399,12.34,-12.457,12.378,-12.534,12.376,-12.621,12.396,-12.713,12.433,-12.797,12.452,-12.888,12.52,-12.931,12.532,-12.961,12.514,-12.986,12.492,-13.012,12.478,-13.061,12.49,-13.08,12.536,-13.064,12.581,-13.06,12.615,-13.083,12.634,-13.138,12.64,-13.228,12.64,-13.373,12.654,-13.406,12.662,-13.729,12.674,-14.065,12.675,-14.349,12.676,-14.708,12.678,-14.961,12.679,-15.196,12.68,-15.378,12.589,-15.575,12.49,-15.84,12.438,-16.144,12.457,-16.242,12.443,-16.342,12.4,-16.416,12.368,-16.521,12.349,-16.657,12.364,-16.712,12.355,-16.746,12.4,-16.785,12.473,-16.76,12.526,-16.678,12.56,-16.553,12.605,-16.488,12.582,-16.45,12.581,-16.443,12.609,-16.455,12.625,-16.549,12.664,-16.598,12.715,-16.638,12.685,-16.673,12.622,-16.701,12.603,-16.744,12.585,-16.768,12.628,-16.778,12.67,-16.759,12.702,-16.769,12.883,-16.757,12.98,-16.763,13.064,-16.705,13.12,-16.649,13.154,-16.431,13.157,-16.228,13.16,-16.033,13.158,-15.834,13.156,-15.814,13.325,-15.752,13.338,-15.657,13.356,-15.482,13.376,-15.286,13.396,-15.245,13.429,-15.212,13.485,-15.192,13.535,-15.151,13.556,-15.096,13.54,-15.025,13.513,-14.95,13.473,-14.865,13.435,-14.808,13.411,-14.672,13.352,-14.439,13.269,-14.247,13.236,-14.015,13.296,-13.848,13.335,-13.827,13.408,-13.853,13.479,-13.977,13.543,-14.147,13.536,-14.199,13.519,-14.278,13.497,-14.326,13.489,-14.405,13.504,-14.507,13.56,-14.571,13.616,-14.66,13.643,-14.766,13.669,-14.936,13.785,-15.024,13.806,-15.108,13.812,-15.27,13.789,-15.427,13.727,-15.51,13.586,-15.667,13.588,-16.002,13.593,-16.309,13.597,-16.562,13.587,-16.588,13.69,-16.648,13.771,-16.745,13.84,-16.767,13.905,-16.734,13.961,-16.64,14.007,-16.618,14.041,-16.667,14.036,-16.742,14.006,-16.792,14.004,-16.798,14.093,-16.881,14.208,-16.974,14.403,-17.079,14.483,-17.168,14.641,-17.261,14.701,-17.346,14.729,-17.418,14.723,-17.445,14.652,-17.536,14.755,-17.412,14.792,-17.147,14.922,-16.843,15.294,-16.571,15.734,-16.535,15.838,-16.502,15.917,-16.48,16.097,-16.441,16.205,-16.404,16.225,-16.358,16.307,-16.302,16.451,-16.239,16.531,-16.168,16.547,-16.113,16.54,-16.074,16.51,-15.959,16.492,-15.768,16.485,-15.621,16.507,-15.517,16.557,-15.38,16.582,-15.211,16.583,-15.121,16.604,-15.113,16.645,-15.091,16.657,-15.055,16.641,-15.022,16.647,-14.991,16.677,-14.96,16.679,-14.929,16.654,-14.787,16.646,-14.534,16.656,-14.3,16.58,-14.086,16.419,-13.975,16.311,-13.968,16.257,-13.933,16.203,-13.868,16.148,-13.81,16.138,-13.757,16.173,-13.715,16.169,-13.685,16.127,-13.624,16.118,-13.556,16.144,-13.507,16.135,-13.498,16.11,-13.487,16.097,-13.454,16.091,-13.41,16.059,-13.348,15.973,-13.297,15.854,-13.258,15.7,-13.206,15.617,-13.142,15.603,-13.105,15.572,-13.098,15.535,-13.079,15.51,-13.049,15.497,-12.994,15.505,-12.931,15.453,-12.863,15.34,-12.852,15.29,-12.863,15.262,-12.858,15.243,-12.813,15.224,-12.77,15.187,-12.735,15.131,-12.66,15.082,-12.544,15.039,-12.46,14.975,-12.409,14.889,-12.303,14.817,-12.281,14.809]]]},{"n":"Saudi Arabia","c":"SA","r":2,"ml":1.7,"lx":44.7,"ly":23.81,"b":[34.616,16.372,55.641,32.125],"p":[[[36.902,25.383,36.875,25.383,36.805,25.451,36.764,25.501,36.722,25.534,36.53,25.602,36.504,25.645,36.534,25.689,36.554,25.645,36.589,25.62,36.748,25.559,36.924,25.426,36.955,25.415,36.902,25.383]],[[41.988,16.716,42.065,16.71,42.033,16.742,42.026,16.758,42.06,16.804,42.17,16.709,42.167,16.596,42.158,16.571,42.128,16.595,42.108,16.618,42.102,16.644,42.072,16.671,41.964,16.653,41.897,16.684,41.802,16.779,41.776,16.847,41.816,16.86,41.858,16.893,41.885,16.947,41.86,17.003,41.917,16.994,41.948,16.936,41.954,16.806,41.963,16.779,41.947,16.749,41.988,16.716]],[[51.978,18.996,51.743,18.965,51.515,18.934,51.258,18.899,50.95,18.858,50.708,18.825,50.355,18.778,50.039,18.735,49.742,18.695,49.445,18.655,49.192,18.621,49.042,18.582,48.865,18.495,48.593,18.362,48.316,18.227,48.172,18.157,48.022,17.977,47.946,17.886,47.808,17.721,47.704,17.597,47.58,17.448,47.525,17.316,47.442,17.112,47.37,17.06,47.251,16.994,47.144,16.947,46.976,16.953,46.88,17.079,46.779,17.212,46.728,17.266,46.682,17.269,46.513,17.252,46.31,17.231,46.071,17.253,45.794,17.278,45.535,17.302,45.407,17.32,45.237,17.406,45.193,17.423,45.148,17.427,44.946,17.43,44.747,17.432,44.546,17.404,44.355,17.414,44.156,17.399,44.086,17.366,44.008,17.367,43.96,17.338,43.917,17.325,43.866,17.35,43.804,17.344,43.713,17.366,43.653,17.422,43.597,17.471,43.539,17.499,43.474,17.516,43.418,17.516,43.346,17.486,43.302,17.457,43.191,17.359,43.186,17.325,43.237,17.266,43.221,17.239,43.156,17.205,43.136,17.113,43.126,17.062,43.117,16.942,43.146,16.847,43.184,16.812,43.186,16.771,43.165,16.689,43.105,16.664,43.061,16.587,43.034,16.55,42.986,16.509,42.799,16.372,42.79,16.452,42.731,16.57,42.726,16.653,42.699,16.737,42.647,16.801,42.553,16.868,42.544,16.96,42.475,17.05,42.383,17.122,42.332,17.257,42.294,17.435,42.052,17.669,41.75,17.886,41.658,18.008,41.508,18.256,41.432,18.452,41.229,18.678,41.221,18.765,41.191,18.871,41.144,18.989,41.116,19.082,40.913,19.49,40.848,19.555,40.792,19.646,40.777,19.717,40.759,19.755,40.616,19.822,40.482,19.993,40.081,20.266,39.884,20.293,39.728,20.39,39.614,20.518,39.491,20.737,39.276,20.974,39.094,21.31,39.151,21.433,39.147,21.519,39.091,21.664,39.03,21.776,38.988,21.882,39.021,22.033,39.034,22.203,39.07,22.294,39.096,22.393,39.062,22.592,39.001,22.699,39.007,22.77,38.939,22.805,38.883,22.882,38.941,22.882,38.836,22.989,38.797,23.049,38.757,23.194,38.706,23.306,38.542,23.558,38.464,23.712,38.289,23.911,38.099,24.058,37.978,24.125,37.92,24.185,37.821,24.188,37.713,24.274,37.638,24.278,37.543,24.292,37.431,24.459,37.338,24.616,37.181,24.82,37.22,24.873,37.266,24.96,37.243,25.073,37.218,25.151,37.149,25.291,36.921,25.641,36.86,25.692,36.763,25.751,36.703,25.903,36.675,26.039,36.519,26.105,36.25,26.595,36.094,26.766,36.032,26.881,35.852,27.07,35.763,27.259,35.581,27.432,35.424,27.734,35.18,28.035,35.078,28.087,34.828,28.109,34.722,28.131,34.625,28.065,34.616,28.148,34.683,28.264,34.78,28.507,34.799,28.721,34.951,29.354,35.164,29.321,35.339,29.294,35.595,29.255,35.86,29.214,36.015,29.19,36.068,29.201,36.283,29.355,36.476,29.495,36.592,29.666,36.704,29.832,36.755,29.866,36.927,29.897,37.199,29.946,37.469,29.995,37.491,30.012,37.554,30.145,37.634,30.313,37.65,30.331,37.67,30.348,37.863,30.443,37.98,30.5,37.813,30.669,37.655,30.829,37.479,31.008,37.329,31.147,37.105,31.355,36.959,31.492,37.216,31.556,37.493,31.626,37.774,31.696,38.111,31.781,38.375,31.847,38.77,31.946,38.962,31.995,38.997,32.007,39.145,32.125,39.369,32.092,39.704,32.043,40.028,31.995,40.369,31.939,40.479,31.893,40.808,31.725,41.022,31.616,41.272,31.489,41.585,31.33,41.8,31.22,42.074,31.08,42.289,30.92,42.56,30.718,42.858,30.495,43.103,30.322,43.441,30.084,43.774,29.849,44.1,29.619,44.361,29.435,44.691,29.202,44.717,29.194,45.05,29.167,45.499,29.132,45.95,29.096,46.356,29.064,46.531,29.096,46.725,29.075,46.982,29.046,47.139,29.026,47.433,28.99,47.521,28.838,47.553,28.732,47.583,28.628,47.671,28.533,47.872,28.535,48.05,28.538,48.269,28.541,48.442,28.543,48.499,28.449,48.523,28.355,48.626,28.133,48.774,27.959,48.809,27.896,48.833,27.801,48.807,27.765,48.797,27.724,48.906,27.629,49.087,27.549,49.158,27.528,49.238,27.493,49.175,27.438,49.282,27.31,49.405,27.181,49.538,27.152,49.717,26.956,49.986,26.829,50.15,26.663,50.135,26.66,50.087,26.676,50.026,26.699,50.008,26.679,50.011,26.609,50.027,26.527,50.111,26.456,50.185,26.405,50.214,26.308,50.155,26.101,50.135,26.101,50.096,26.119,50.054,26.123,50.032,26.111,50.081,25.961,50.13,25.847,50.19,25.756,50.239,25.623,50.281,25.566,50.455,25.425,50.508,25.307,50.558,25.087,50.667,24.964,50.726,24.869,50.804,24.789,50.856,24.68,50.928,24.595,50.966,24.574,51.023,24.565,51.093,24.565,51.178,24.587,51.268,24.607,51.338,24.564,51.411,24.571,51.418,24.531,51.37,24.477,51.31,24.34,51.395,24.319,51.477,24.308,51.535,24.286,51.568,24.286,51.568,24.258,51.572,24.128,51.593,24.079,51.629,24.035,51.684,23.97,51.739,23.904,51.794,23.838,51.849,23.773,51.904,23.708,51.959,23.642,52.014,23.576,52.069,23.511,52.125,23.445,52.179,23.38,52.235,23.314,52.29,23.249,52.345,23.183,52.4,23.118,52.455,23.052,52.51,22.987,52.555,22.933,52.639,22.923,52.666,22.919,52.742,22.91,52.859,22.896,53.012,22.877,53.192,22.855,53.394,22.83,53.61,22.804,53.832,22.777,54.055,22.75,54.27,22.723,54.472,22.699,54.652,22.677,54.805,22.658,54.922,22.644,54.998,22.634,55.025,22.631,55.104,22.621,55.119,22.624,55.186,22.704,55.259,22.591,55.32,22.497,55.404,22.368,55.493,22.231,55.578,22.1,55.641,22.002,55.607,21.9,55.571,21.79,55.534,21.679,55.498,21.568,55.461,21.458,55.424,21.347,55.388,21.236,55.351,21.126,55.314,21.015,55.278,20.904,55.241,20.793,55.205,20.683,55.168,20.572,55.131,20.461,55.095,20.351,55.058,20.24,55.021,20.129,54.977,19.996,54.871,19.96,54.699,19.903,54.527,19.846,54.355,19.788,54.183,19.731,54.011,19.674,53.839,19.617,53.667,19.559,53.495,19.502,53.323,19.444,53.151,19.387,52.979,19.33,52.807,19.272,52.635,19.215,52.463,19.158,52.291,19.1,52.119,19.043,51.978,18.996]],[[36.596,25.713,36.586,25.699,36.544,25.734,36.546,25.812,36.583,25.856,36.58,25.795,36.596,25.735,36.596,25.713]]]},{"n":"São Tomé and Príncipe","c":"ST","r":6,"ml":5,"lx":7.02,"ly":0.97,"b":[6.468,0.047,7.452,1.699],"p":[[[7.424,1.568,7.387,1.542,7.342,1.564,7.331,1.603,7.388,1.68,7.414,1.699,7.437,1.683,7.45,1.662,7.452,1.631,7.424,1.568]],[[6.66,0.121,6.557,0.047,6.52,0.066,6.497,0.117,6.468,0.227,6.478,0.28,6.524,0.34,6.626,0.4,6.687,0.404,6.75,0.326,6.75,0.243,6.66,0.121]]]},{"n":"San Marino","c":"SM","r":6,"ml":5,"lx":12.44,"ly":43.93,"b":[12.397,43.894,12.515,43.99],"p":[[[12.485,43.901,12.426,43.894,12.397,43.935,12.441,43.982,12.504,43.99,12.515,43.953,12.485,43.901]]]},{"n":"Samoa","c":"WS","r":4,"ml":3,"lx":-172.44,"ly":-13.64,"b":[-172.779,-14.047,-171.45,-13.465],"p":[[[-172.333,-13.465,-172.222,-13.56,-172.177,-13.685,-172.225,-13.804,-172.331,-13.775,-172.485,-13.8,-172.536,-13.792,-172.659,-13.645,-172.744,-13.579,-172.779,-13.517,-172.67,-13.524,-172.511,-13.483,-172.333,-13.465]],[[-171.454,-14.046,-171.728,-14.047,-171.864,-14.002,-171.912,-14.002,-172.028,-13.907,-172.046,-13.857,-171.985,-13.824,-171.858,-13.807,-171.604,-13.879,-171.565,-13.943,-171.507,-13.95,-171.461,-13.978,-171.45,-14.022,-171.454,-14.046]]]},{"n":"Saint Vincent and the Grenadines","c":"VC","r":6,"ml":5,"lx":-61.34,"ly":13.09,"b":[-61.354,12.695,-61.124,13.359],"p":[[[-61.175,13.158,-61.204,13.142,-61.277,13.21,-61.268,13.288,-61.224,13.331,-61.182,13.356,-61.139,13.359,-61.124,13.294,-61.135,13.203,-61.175,13.158]],[[-61.226,12.995,-61.242,12.984,-61.235,12.984,-61.242,12.976,-61.247,12.984,-61.255,12.988,-61.265,12.99,-61.277,12.99,-61.277,12.997,-61.262,12.993,-61.239,12.997,-61.239,13.005,-61.246,13.013,-61.249,13.018,-61.241,13.026,-61.213,13.043,-61.201,13.053,-61.2,13.049,-61.199,13.045,-61.198,13.042,-61.194,13.038,-61.208,13.025,-61.226,12.995]],[[-61.334,12.695,-61.345,12.695,-61.354,12.698,-61.351,12.701,-61.34,12.704,-61.334,12.71,-61.336,12.719,-61.336,12.729,-61.327,12.735,-61.32,12.735,-61.317,12.732,-61.315,12.723,-61.32,12.716,-61.326,12.71,-61.329,12.701,-61.334,12.695]]]},{"n":"Saint Lucia","c":"LC","r":6,"ml":5,"lx":-60.98,"ly":13.89,"b":[-61.073,13.718,-60.887,14.093],"p":[[[-60.895,13.822,-60.951,13.718,-61.061,13.783,-61.073,13.866,-61.064,13.916,-60.997,14.011,-60.945,14.073,-60.908,14.093,-60.887,14.011,-60.895,13.822]]]},{"n":"Saint Kitts and Nevis","c":"KN","r":6,"ml":5,"lx":-62.76,"ly":17.34,"b":[-62.84,17.101,-62.532,17.403],"p":[[[-62.532,17.122,-62.582,17.101,-62.625,17.13,-62.615,17.199,-62.575,17.201,-62.534,17.17,-62.532,17.122]],[[-62.631,17.24,-62.656,17.224,-62.702,17.286,-62.776,17.303,-62.839,17.339,-62.84,17.347,-62.839,17.365,-62.827,17.386,-62.795,17.403,-62.714,17.353,-62.676,17.291,-62.641,17.262,-62.631,17.24]]]},{"n":"Rwanda","c":"RW","r":3,"ml":3,"lx":30.1,"ly":-1.9,"b":[28.858,-2.809,30.877,-1.063],"p":[[[29.577,-1.388,29.61,-1.387,29.825,-1.336,29.847,-1.352,29.882,-1.452,29.9,-1.466,29.93,-1.47,29.991,-1.447,30.102,-1.369,30.15,-1.321,30.207,-1.254,30.28,-1.179,30.321,-1.113,30.36,-1.075,30.412,-1.063,30.47,-1.066,30.51,-1.067,30.477,-1.083,30.47,-1.131,30.508,-1.208,30.632,-1.367,30.711,-1.397,30.762,-1.459,30.813,-1.563,30.828,-1.694,30.807,-1.851,30.819,-1.967,30.865,-2.044,30.877,-2.143,30.855,-2.265,30.829,-2.338,30.798,-2.363,30.763,-2.372,30.715,-2.363,30.657,-2.374,30.593,-2.397,30.554,-2.4,30.529,-2.396,30.482,-2.376,30.408,-2.313,30.271,-2.348,30.234,-2.347,30.183,-2.377,30.142,-2.414,30.117,-2.417,30.092,-2.412,29.973,-2.337,29.93,-2.34,29.912,-2.549,29.893,-2.665,29.868,-2.716,29.783,-2.766,29.698,-2.795,29.651,-2.793,29.464,-2.808,29.39,-2.809,29.35,-2.792,29.297,-2.673,29.198,-2.62,29.102,-2.596,29.063,-2.603,29.029,-2.665,29.014,-2.72,28.922,-2.682,28.894,-2.635,28.891,-2.556,28.858,-2.447,28.876,-2.4,28.913,-2.37,28.99,-2.313,29.106,-2.233,29.132,-2.195,29.148,-2.132,29.141,-1.985,29.129,-1.86,29.143,-1.816,29.197,-1.72,29.268,-1.622,29.352,-1.518,29.402,-1.507,29.468,-1.468,29.538,-1.41,29.577,-1.388]]]},{"n":"Russia","c":"RU","r":2,"ml":1.7,"lx":44.69,"ly":58.25,"b":[-180,41.199,180,81.854],"p":[[[145.882,43.46,145.896,43.455,145.914,43.455,145.931,43.457,145.941,43.445,145.944,43.426,145.931,43.426,145.907,43.422,145.894,43.42,145.887,43.433,145.882,43.444,145.869,43.45,145.869,43.458,145.882,43.46]],[[146.359,43.625,146.332,43.62,146.288,43.625,146.274,43.63,146.284,43.639,146.31,43.652,146.333,43.647,146.35,43.644,146.359,43.625]],[[146.046,43.409,146.032,43.407,146.028,43.42,146.049,43.434,146.089,43.449,146.101,43.44,146.086,43.429,146.07,43.421,146.046,43.409]],[[137.179,55.1,137.055,54.927,136.969,54.924,136.903,54.961,136.765,54.946,136.715,54.956,136.795,55.009,136.996,55.093,137.078,55.092,137.156,55.108,137.179,55.1]],[[150.59,59.019,150.511,59.007,150.472,59.035,150.47,59.054,150.592,59.097,150.666,59.16,150.713,59.122,150.728,59.095,150.59,59.019]],[[120.261,73.09,120.008,73.045,119.792,73.045,119.64,73.124,119.762,73.155,119.964,73.168,120.079,73.157,120.237,73.107,120.261,73.09]],[[124.543,73.85,124.482,73.848,124.366,73.875,124.336,73.91,124.337,73.928,124.43,73.943,124.548,73.934,124.637,73.9,124.653,73.888,124.543,73.85]],[[106.27,78.206,106.151,78.199,106.024,78.22,106.058,78.265,106.351,78.273,106.457,78.34,106.64,78.336,106.691,78.317,106.72,78.294,106.719,78.265,106.679,78.265,106.505,78.262,106.472,78.245,106.27,78.206]],[[107.415,77.243,107.302,77.242,107.27,77.289,107.366,77.347,107.486,77.347,107.594,77.33,107.629,77.32,107.665,77.3,107.679,77.268,107.415,77.243]],[[97.588,76.599,97.535,76.584,97.43,76.591,97.342,76.629,97.31,76.69,97.382,76.707,97.588,76.599]],[[96.854,76.199,96.798,76.188,96.754,76.196,96.739,76.207,96.74,76.258,96.833,76.324,96.835,76.345,96.878,76.355,96.99,76.343,97.045,76.315,97.053,76.303,96.974,76.237,96.854,76.199]],[[100.136,79.614,99.915,79.602,99.942,79.671,99.956,79.69,100.068,79.701,100.142,79.684,100.3,79.67,100.136,79.614]],[[76.756,73.446,76.659,73.44,76.234,73.476,76.083,73.523,76.14,73.554,76.251,73.555,76.756,73.446]],[[82.71,74.091,82.613,74.056,82.478,74.076,82.382,74.099,82.329,74.131,82.382,74.149,82.526,74.161,82.611,74.149,82.689,74.111,82.71,74.091]],[[84.759,74.459,84.71,74.4,84.429,74.43,84.389,74.454,84.54,74.49,84.68,74.512,84.873,74.516,84.759,74.459]],[[86.653,74.981,86.737,74.963,87.001,74.992,87.052,74.983,87.124,74.94,87.012,74.862,86.927,74.831,86.692,74.848,86.391,74.851,86.259,74.894,86.331,74.939,86.504,74.966,86.605,74.993,86.653,74.981]],[[59.313,81.305,59.097,81.292,58.719,81.314,58.61,81.337,58.634,81.36,58.881,81.392,59.075,81.398,59.281,81.366,59.375,81.325,59.313,81.305]],[[-179.799,68.94,-179.595,68.906,-179.515,68.917,-179.471,68.912,-179.356,68.853,-179.279,68.825,-178.874,68.754,-178.689,68.675,-178.539,68.586,-178.614,68.603,-178.751,68.66,-178.737,68.593,-178.693,68.546,-178.474,68.502,-178.244,68.467,-178.097,68.425,-178.049,68.388,-178.019,68.323,-178.056,68.265,-177.922,68.287,-177.797,68.338,-177.862,68.378,-178.285,68.519,-178.373,68.566,-178.25,68.541,-177.683,68.363,-177.527,68.294,-177.593,68.281,-177.639,68.241,-177.589,68.224,-177.521,68.237,-177.408,68.245,-177.297,68.223,-177.172,68.175,-176.907,68.119,-175.345,67.678,-175.31,67.602,-175.266,67.567,-175.24,67.521,-175.233,67.447,-175.375,67.357,-175.155,67.365,-175.123,67.377,-175.066,67.413,-175.003,67.438,-174.918,67.408,-174.85,67.349,-174.87,67.269,-174.93,67.203,-174.938,67.093,-174.885,67,-174.829,66.961,-174.784,66.917,-174.771,66.784,-174.87,66.725,-174.925,66.623,-174.864,66.613,-174.675,66.603,-174.612,66.585,-174.504,66.538,-174.478,66.492,-174.454,66.43,-174.419,66.372,-174.394,66.344,-174.366,66.348,-174.257,66.428,-174.206,66.452,-174.085,66.473,-174.018,66.383,-174.065,66.23,-174.025,66.23,-173.994,66.246,-173.955,66.287,-173.9,66.31,-173.832,66.366,-173.774,66.435,-173.843,66.488,-173.921,66.522,-174.102,66.541,-174.196,66.581,-174.232,66.632,-174.14,66.653,-174.061,66.69,-174.006,66.779,-174.019,66.827,-174.041,66.875,-174.086,66.943,-174.154,66.982,-174.284,67.002,-174.342,67.04,-174.431,67.038,-174.519,67.049,-174.554,67.063,-174.55,67.091,-174.448,67.103,-173.884,67.106,-173.68,67.145,-173.587,67.133,-173.494,67.105,-173.158,67.069,-173.168,67.052,-173.224,67.035,-173.324,66.955,-173.343,66.909,-173.347,66.851,-173.259,66.84,-173.175,66.865,-173.216,66.911,-173.228,66.969,-173.193,66.994,-173.147,66.999,-173.058,66.956,-172.963,66.942,-172.641,66.925,-172.549,66.931,-172.52,66.952,-172.583,66.978,-173.002,67.034,-173.008,67.065,-172.621,67.027,-172.447,66.992,-172.274,66.966,-172.031,66.973,-171.796,66.932,-171.57,66.819,-171.36,66.677,-171.149,66.593,-170.927,66.53,-170.556,66.357,-170.51,66.344,-170.473,66.32,-170.543,66.291,-170.604,66.249,-170.483,66.278,-170.361,66.298,-170.301,66.294,-170.247,66.272,-170.212,66.236,-170.192,66.201,-170.244,66.169,-169.889,66.163,-169.778,66.143,-169.729,66.058,-169.832,65.999,-169.892,66.006,-169.949,66.031,-170.004,66.033,-170.159,66.008,-170.401,65.929,-170.541,65.865,-170.563,65.824,-170.541,65.71,-170.561,65.656,-170.666,65.622,-170.897,65.643,-171.001,65.665,-171.119,65.695,-171.232,65.737,-171.377,65.804,-171.422,65.81,-171.451,65.794,-171.402,65.752,-171.303,65.698,-171.134,65.628,-171.054,65.55,-171.106,65.511,-171.17,65.502,-171.216,65.503,-171.364,65.527,-171.466,65.533,-171.79,65.51,-171.907,65.496,-171.947,65.508,-171.957,65.542,-172.131,65.567,-172.234,65.57,-172.282,65.582,-172.322,65.618,-172.436,65.67,-172.608,65.69,-172.719,65.692,-172.783,65.681,-172.557,65.612,-172.354,65.496,-172.392,65.475,-172.418,65.45,-172.306,65.448,-172.233,65.456,-172.212,65.425,-172.27,65.303,-172.309,65.276,-172.662,65.249,-172.573,65.228,-172.482,65.222,-172.379,65.227,-172.286,65.206,-172.224,65.129,-172.213,65.048,-172.304,65.002,-172.399,64.965,-172.593,64.908,-172.792,64.883,-172.897,64.889,-172.999,64.877,-173.066,64.847,-173.086,64.817,-172.998,64.837,-172.897,64.826,-172.801,64.791,-172.812,64.761,-172.903,64.729,-172.924,64.705,-172.889,64.664,-172.901,64.629,-172.854,64.61,-172.747,64.603,-172.616,64.578,-172.487,64.544,-172.437,64.515,-172.394,64.475,-172.379,64.432,-172.401,64.414,-172.695,64.407,-172.739,64.412,-172.756,64.46,-172.792,64.499,-172.903,64.526,-172.949,64.507,-172.916,64.369,-172.96,64.328,-173.009,64.297,-173.157,64.28,-173.275,64.29,-173.376,64.355,-173.376,64.41,-173.309,64.443,-173.309,64.487,-173.327,64.54,-173.396,64.479,-173.475,64.429,-173.604,64.365,-173.666,64.357,-173.73,64.365,-173.898,64.41,-174.001,64.449,-174.205,64.578,-174.318,64.638,-174.571,64.718,-174.83,64.776,-175.036,64.814,-175.146,64.809,-175.256,64.794,-175.395,64.802,-175.442,64.817,-175.483,64.849,-175.521,64.867,-175.716,64.946,-175.854,65.011,-175.859,65.054,-175.83,65.106,-175.856,65.233,-175.923,65.352,-176.093,65.471,-176.547,65.548,-176.922,65.601,-177.056,65.614,-177.175,65.602,-177.489,65.504,-177.699,65.49,-178.31,65.485,-178.412,65.496,-178.505,65.537,-178.526,65.593,-178.499,65.697,-178.502,65.74,-178.526,65.755,-178.559,65.754,-178.679,65.795,-178.791,65.865,-178.879,65.936,-178.939,66.033,-178.858,66.038,-178.747,66.014,-178.731,66.037,-178.694,66.124,-178.616,66.166,-178.587,66.198,-178.534,66.317,-178.527,66.402,-178.616,66.355,-178.753,66.237,-178.821,66.203,-178.868,66.187,-178.916,66.18,-179.026,66.204,-179.105,66.232,-179.107,66.346,-179.143,66.375,-179.178,66.353,-179.193,66.313,-179.293,66.305,-179.34,66.288,-179.316,66.22,-179.327,66.163,-179.423,66.141,-179.616,66.128,-179.683,66.184,-179.741,66.106,-179.784,66.018,-179.79,65.901,-179.728,65.804,-179.641,65.758,-179.449,65.688,-179.366,65.639,-179.344,65.575,-179.352,65.517,-179.452,65.445,-179.519,65.386,-179.635,65.244,-179.705,65.187,-180,65.067,-180,65.312,-180,65.557,-180,65.802,-180,66.046,-180,66.291,-180,66.536,-180,66.781,-180,67.025,-180,67.27,-180,67.515,-180,67.76,-180,68.004,-180,68.249,-180,68.494,-180,68.739,-180,68.983,-179.799,68.94]],[[130.687,42.303,130.658,42.328,130.652,42.373,130.618,42.416,130.554,42.475,130.527,42.535,130.584,42.567,130.577,42.623,130.521,42.674,130.439,42.686,130.42,42.7,130.425,42.727,130.453,42.755,130.493,42.779,130.577,42.812,130.722,42.836,130.803,42.857,130.869,42.863,130.943,42.852,131.006,42.883,131.069,42.902,131.083,42.956,131.086,43.038,131.109,43.062,131.136,43.098,131.176,43.142,131.212,43.258,131.239,43.338,131.257,43.378,131.262,43.433,131.244,43.469,131.209,43.49,131.182,43.506,131.18,43.567,131.184,43.651,131.174,43.705,131.213,44.003,131.255,44.072,131.126,44.469,131.087,44.596,131.061,44.66,131.004,44.753,130.968,44.8,130.982,44.844,131.033,44.889,131.082,44.91,131.228,44.92,131.268,44.936,131.447,44.984,131.488,45.013,131.579,45.084,131.614,45.137,131.654,45.205,131.742,45.243,131.795,45.305,131.852,45.327,131.909,45.274,131.978,45.244,132.067,45.226,132.181,45.203,132.363,45.16,132.549,45.123,132.666,45.094,132.723,45.081,132.839,45.061,132.889,45.046,132.936,45.03,133.012,45.075,133.113,45.131,133.097,45.22,133.113,45.321,133.186,45.495,133.267,45.545,133.31,45.553,133.355,45.572,133.436,45.605,133.466,45.651,133.449,45.705,133.476,45.758,133.485,45.81,133.513,45.879,133.551,45.898,133.608,45.92,133.648,45.955,133.686,46.009,133.711,46.07,133.701,46.14,133.75,46.186,133.833,46.224,133.861,46.248,133.875,46.309,133.88,46.336,133.903,46.367,133.887,46.431,133.867,46.499,133.958,46.614,134.023,46.713,134.039,46.858,134.046,46.882,134.071,46.951,134.086,46.978,134.137,47.069,134.202,47.128,134.189,47.194,134.163,47.259,134.168,47.302,134.225,47.353,134.26,47.378,134.291,47.414,134.339,47.429,134.383,47.438,134.483,47.447,134.542,47.485,134.596,47.524,134.696,47.625,134.728,47.684,134.752,47.715,134.699,47.801,134.65,47.874,134.591,47.975,134.566,48.023,134.605,48.083,134.647,48.12,134.669,48.153,134.681,48.21,134.665,48.254,134.564,48.322,134.456,48.355,134.335,48.369,134.293,48.373,134.206,48.36,133.842,48.274,133.672,48.208,133.573,48.133,133.468,48.097,133.301,48.102,133.144,48.106,133.02,48.064,132.877,47.979,132.773,47.94,132.707,47.947,132.637,47.89,132.562,47.769,132.476,47.715,132.38,47.729,132.15,47.718,131.785,47.681,131.557,47.682,131.464,47.723,131.319,47.728,131.122,47.698,131.003,47.691,130.962,47.709,130.933,47.76,130.915,47.843,130.849,47.929,130.733,48.019,130.712,48.128,130.787,48.255,130.804,48.342,130.763,48.388,130.747,48.43,130.659,48.483,130.597,48.575,130.552,48.602,130.566,48.68,130.617,48.773,130.553,48.861,130.355,48.866,130.196,48.892,130.037,48.972,129.793,49.199,129.671,49.279,129.591,49.287,129.534,49.323,129.498,49.389,129.441,49.389,129.385,49.389,129.35,49.362,129.31,49.354,129.248,49.379,129.185,49.381,129.12,49.362,129.065,49.375,129.02,49.419,128.938,49.449,128.819,49.464,128.77,49.495,128.791,49.542,128.769,49.577,128.704,49.6,128.527,49.594,128.237,49.559,128,49.569,127.814,49.622,127.711,49.672,127.69,49.717,127.637,49.76,127.551,49.802,127.502,49.873,127.492,49.975,127.512,50.072,127.59,50.209,127.395,50.299,127.337,50.35,127.351,50.394,127.341,50.428,127.306,50.454,127.308,50.494,127.347,50.55,127.347,50.621,127.307,50.708,127.198,50.829,127.02,50.986,126.925,51.1,126.912,51.172,126.888,51.23,126.854,51.261,126.834,51.315,126.848,51.374,126.827,51.412,126.802,51.448,126.805,51.506,126.775,51.545,126.709,51.566,126.689,51.61,126.701,51.703,126.654,51.781,126.511,51.926,126.468,52.031,126.456,52.126,126.395,52.173,126.392,52.215,126.383,52.287,126.346,52.306,126.324,52.332,126.342,52.362,126.313,52.4,126.238,52.445,126.203,52.484,126.194,52.519,126.157,52.547,126.046,52.573,126.016,52.61,126.023,52.643,126.047,52.673,126.06,52.692,126.056,52.716,126.048,52.739,126.004,52.768,125.942,52.801,125.872,52.872,125.783,52.891,125.728,52.891,125.681,52.931,125.695,52.956,125.692,53.004,125.649,53.042,125.596,53.057,125.546,53.048,125.422,53.084,125.226,53.166,125.075,53.204,124.971,53.197,124.907,53.173,124.882,53.13,124.812,53.134,124.64,53.211,124.466,53.23,124.369,53.271,124.291,53.341,124.22,53.37,124.154,53.359,123.995,53.406,123.741,53.511,123.608,53.547,123.56,53.527,123.535,53.526,123.489,53.529,123.424,53.531,123.31,53.556,123.154,53.545,122.958,53.498,122.745,53.469,122.516,53.457,122.38,53.463,122.338,53.485,122.089,53.451,121.744,53.384,121.405,53.317,120.985,53.285,120.704,53.172,120.421,52.968,120.218,52.84,120.095,52.787,120.044,52.718,120.068,52.633,120.173,52.602,120.36,52.627,120.521,52.615,120.656,52.567,120.699,52.494,120.65,52.396,120.665,52.3,120.745,52.205,120.75,52.097,120.681,51.973,120.511,51.849,120.237,51.723,120.067,51.601,119.967,51.422,119.813,51.267,119.757,51.179,119.746,51.108,119.685,51.03,119.573,50.947,119.512,50.863,119.502,50.779,119.446,50.703,119.344,50.634,119.281,50.561,119.256,50.484,119.217,50.433,119.164,50.406,119.192,50.38,119.302,50.354,119.346,50.279,119.326,50.155,119.26,50.066,119.147,50.013,118.979,49.979,118.756,49.963,118.452,49.844,118.187,49.693,117.873,49.513,117.813,49.514,117.698,49.536,117.477,49.609,117.246,49.625,117.022,49.693,116.889,49.738,116.683,49.824,116.632,49.877,116.551,49.92,116.351,49.978,116.217,50.009,116.135,50.011,115.926,49.952,115.795,49.906,115.718,49.881,115.588,49.886,115.429,49.896,115.365,49.912,115.275,49.949,115.098,50.059,115.003,50.139,114.88,50.183,114.743,50.234,114.675,50.246,114.554,50.241,114.386,50.255,114.297,50.274,114.222,50.257,114.071,50.205,113.881,50.101,113.732,50.062,113.574,50.007,113.446,49.942,113.319,49.874,113.164,49.797,113.092,49.693,113.056,49.616,112.915,49.569,112.806,49.524,112.697,49.507,112.495,49.532,112.375,49.515,112.08,49.424,111.934,49.416,111.833,49.404,111.736,49.398,111.575,49.376,111.512,49.361,111.429,49.343,111.337,49.356,111.204,49.304,110.828,49.166,110.71,49.143,110.631,49.138,110.53,49.187,110.428,49.22,110.321,49.216,110.2,49.17,109.995,49.206,109.75,49.239,109.529,49.27,109.454,49.296,109.237,49.335,108.92,49.335,108.733,49.336,108.614,49.323,108.522,49.342,108.407,49.396,108.213,49.525,108.098,49.563,108.034,49.594,108.01,49.647,107.965,49.654,107.937,49.691,107.939,49.741,107.935,49.849,107.948,49.925,107.917,49.948,107.787,49.96,107.631,49.983,107.347,49.987,107.233,49.989,107.143,50.033,107.04,50.086,106.941,50.197,106.854,50.248,106.711,50.313,106.574,50.329,106.368,50.318,106.218,50.305,106.083,50.333,105.996,50.368,105.875,50.405,105.693,50.414,105.542,50.441,105.384,50.474,105.267,50.46,105.186,50.43,105.095,50.39,104.977,50.383,104.685,50.342,104.596,50.317,104.466,50.306,104.354,50.275,104.26,50.214,104.18,50.169,104.079,50.154,103.958,50.157,103.856,50.172,103.803,50.176,103.723,50.154,103.633,50.139,103.496,50.165,103.421,50.187,103.304,50.2,103.234,50.264,103.162,50.291,103.039,50.301,102.86,50.333,102.765,50.367,102.683,50.387,102.546,50.461,102.469,50.526,102.407,50.536,102.336,50.544,102.288,50.585,102.286,50.635,102.303,50.666,102.317,50.718,102.277,50.769,102.235,50.791,102.215,50.829,102.226,50.901,102.21,50.974,102.195,51.051,102.152,51.108,102.142,51.216,102.16,51.261,102.156,51.314,102.112,51.353,101.979,51.382,101.821,51.421,101.571,51.467,101.464,51.471,101.381,51.453,101.304,51.475,101.223,51.513,101.085,51.553,100.904,51.604,100.711,51.662,100.536,51.713,100.469,51.726,100.23,51.73,100.035,51.737,99.922,51.756,99.788,51.828,99.719,51.872,99.613,51.893,99.532,51.9,99.407,51.924,99.176,51.999,99.091,52.035,99.034,52.035,98.958,52.102,98.893,52.117,98.849,52.07,98.803,51.957,98.76,51.905,98.641,51.801,98.353,51.718,98.303,51.674,98.277,51.635,98.238,51.578,98.22,51.506,98.185,51.486,98.103,51.484,98.038,51.45,97.989,51.377,97.947,51.348,97.923,51.28,97.927,51.251,97.918,51.218,97.911,51.165,97.836,51.052,97.825,50.985,97.856,50.943,97.92,50.887,97.953,50.855,97.964,50.818,97.962,50.769,98.001,50.702,98.03,50.645,98.079,50.604,98.145,50.569,98.221,50.557,98.279,50.533,98.293,50.487,98.277,50.423,98.25,50.302,98.2,50.228,98.17,50.181,98.122,50.107,98.103,50.078,98.004,50.014,97.937,49.997,97.854,49.947,97.786,49.945,97.721,49.945,97.651,49.934,97.589,49.911,97.541,49.843,97.418,49.773,97.36,49.741,97.209,49.731,97.137,49.762,97.098,49.805,97.049,49.83,96.986,49.883,96.712,49.912,96.64,49.898,96.598,49.878,96.543,49.893,96.506,49.919,96.466,49.912,96.381,49.896,96.315,49.901,96.23,49.954,96.112,49.982,96.066,49.999,96.019,49.999,95.99,49.974,95.936,49.96,95.899,49.991,95.852,50.013,95.789,50.013,95.708,49.966,95.567,49.944,95.523,49.911,95.442,49.916,95.386,49.941,95.329,49.944,95.166,49.944,95.111,49.935,95.044,49.962,95.013,50.008,94.93,50.044,94.811,50.048,94.718,50.043,94.675,50.028,94.615,50.024,94.565,50.088,94.497,50.133,94.458,50.166,94.4,50.18,94.355,50.222,94.347,50.303,94.319,50.405,94.287,50.511,94.251,50.556,94.076,50.573,93.99,50.569,93.795,50.578,93.662,50.584,93.626,50.586,93.501,50.597,93.387,50.608,93.271,50.616,93.223,50.607,93.103,50.604,93.01,50.655,92.971,50.713,92.964,50.745,92.941,50.778,92.856,50.789,92.779,50.779,92.739,50.711,92.681,50.683,92.627,50.688,92.579,50.725,92.486,50.765,92.426,50.803,92.355,50.864,92.296,50.85,92.279,50.812,92.265,50.775,92.192,50.701,92.104,50.692,91.957,50.698,91.804,50.694,91.706,50.666,91.634,50.615,91.597,50.576,91.522,50.562,91.446,50.522,91.415,50.468,91.341,50.47,91.301,50.463,91.234,50.452,91.063,50.423,91.022,50.415,90.917,50.364,90.838,50.324,90.761,50.306,90.714,50.259,90.655,50.222,90.517,50.213,90.365,50.167,90.311,50.151,90.225,50.117,90.104,50.103,90.054,50.094,90.005,50.069,89.977,49.984,89.878,49.954,89.744,49.948,89.644,49.903,89.634,49.823,89.67,49.75,89.654,49.717,89.579,49.7,89.475,49.661,89.396,49.612,89.299,49.611,89.244,49.627,89.203,49.596,89.18,49.532,89.109,49.501,89.008,49.473,88.971,49.484,88.945,49.508,88.9,49.54,88.864,49.528,88.86,49.482,88.832,49.448,88.748,49.446,88.683,49.465,88.633,49.486,88.544,49.483,88.452,49.473,88.393,49.483,88.338,49.473,88.193,49.452,88.136,49.381,88.134,49.298,88.116,49.256,88.029,49.22,87.988,49.187,87.935,49.165,87.818,49.162,87.814,49.162,87.763,49.166,87.668,49.147,87.577,49.132,87.516,49.122,87.476,49.091,87.417,49.077,87.323,49.086,87.297,49.148,87.234,49.216,87.148,49.24,87.071,49.255,87.001,49.287,86.953,49.322,86.812,49.488,86.714,49.559,86.626,49.563,86.614,49.61,86.665,49.657,86.731,49.696,86.729,49.749,86.675,49.777,86.61,49.769,86.522,49.708,86.418,49.638,86.292,49.588,86.242,49.546,86.181,49.499,86.093,49.505,86.03,49.503,85.974,49.499,85.934,49.55,85.88,49.557,85.498,49.605,85.372,49.624,85.292,49.599,85.233,49.616,85.21,49.665,85.137,49.751,85.076,49.822,85.001,49.894,84.975,49.951,85,50.01,84.989,50.061,84.924,50.088,84.839,50.091,84.607,50.202,84.499,50.219,84.401,50.239,84.323,50.239,84.258,50.288,84.195,50.437,84.176,50.521,84.099,50.605,84.002,50.677,83.945,50.775,83.86,50.818,83.718,50.887,83.581,50.936,83.357,50.995,83.274,50.995,83.16,50.989,83.093,50.961,83.019,50.897,82.919,50.893,82.761,50.893,82.719,50.869,82.693,50.826,82.612,50.771,82.494,50.728,82.326,50.742,82.212,50.719,82.098,50.711,81.934,50.766,81.752,50.764,81.634,50.739,81.466,50.74,81.431,50.771,81.452,50.824,81.438,50.871,81.41,50.91,81.388,50.956,81.319,50.966,81.125,50.946,81.071,50.969,81.078,51.015,81.112,51.072,81.141,51.147,81.127,51.191,81.027,51.186,80.966,51.19,80.934,51.243,80.877,51.281,80.813,51.283,80.735,51.293,80.65,51.277,80.605,51.224,80.551,51.217,80.491,51.202,80.448,51.183,80.421,51.136,80.434,51.093,80.452,50.998,80.424,50.946,80.345,50.919,80.27,50.925,80.22,50.912,80.127,50.858,80.086,50.84,80.072,50.807,80.066,50.758,79.986,50.775,79.86,50.955,79.716,51.16,79.554,51.378,79.469,51.493,79.149,51.868,78.992,52.047,78.721,52.357,78.475,52.638,78.198,52.93,78.033,53.095,77.86,53.269,77.799,53.317,77.704,53.379,77.469,53.499,77.132,53.67,76.821,53.823,76.576,53.943,76.513,53.993,76.485,54.023,76.459,54.055,76.422,54.114,76.422,54.152,76.655,54.145,76.703,54.182,76.789,54.322,76.837,54.442,76.759,54.437,76.616,54.387,76.539,54.351,76.496,54.336,76.267,54.312,76.141,54.259,75.881,54.168,75.693,54.115,75.657,54.106,75.437,54.09,75.398,54.069,75.392,54.022,75.377,53.97,75.22,53.894,75.052,53.827,74.989,53.819,74.887,53.834,74.834,53.826,74.681,53.754,74.452,53.647,74.43,53.604,74.429,53.551,74.403,53.504,74.352,53.488,74.277,53.528,74.21,53.576,74.069,53.611,73.859,53.62,73.731,53.603,73.643,53.576,73.47,53.469,73.407,53.448,73.372,53.454,73.362,53.506,73.327,53.543,73.286,53.598,73.306,53.707,73.399,53.811,73.554,53.868,73.679,53.929,73.716,53.996,73.712,54.042,73.666,54.063,73.618,54.067,73.59,54.045,73.506,53.999,73.381,53.963,73.277,53.956,73.23,53.958,73.119,53.981,72.914,54.107,72.741,54.125,72.622,54.134,72.583,54.122,72.564,54.09,72.576,54.056,72.599,54.023,72.586,53.996,72.53,53.976,72.447,53.942,72.404,53.964,72.383,54.054,72.387,54.123,72.329,54.181,72.269,54.272,72.186,54.326,72.105,54.308,72.066,54.232,72.004,54.206,71.887,54.221,71.677,54.178,71.336,54.158,71.093,54.212,71.053,54.26,71.152,54.364,71.16,54.455,71.159,54.539,71.186,54.599,71.126,54.715,70.992,54.95,70.91,55.128,70.79,55.261,70.738,55.305,70.486,55.282,70.417,55.253,70.371,55.212,70.293,55.184,70.182,55.162,70.087,55.177,69.982,55.199,69.87,55.246,69.74,55.307,69.493,55.357,69.247,55.373,68.977,55.39,68.843,55.358,68.713,55.308,68.525,55.205,68.438,55.194,68.302,55.187,68.206,55.161,68.225,55.115,68.244,55.052,68.209,55.003,68.156,54.977,68.074,54.96,67.94,54.954,67.83,54.944,67.693,54.872,67.485,54.854,67.257,54.829,67.098,54.788,66.754,54.738,66.555,54.715,66.223,54.667,65.955,54.66,65.914,54.693,65.708,54.619,65.477,54.623,65.434,54.593,65.378,54.564,65.316,54.552,65.237,54.516,65.192,54.441,65.158,54.364,65.088,54.34,64.995,54.369,64.927,54.397,64.809,54.369,64.65,54.352,64.525,54.362,64.461,54.384,64.199,54.347,64.063,54.303,64.037,54.28,64.004,54.267,63.847,54.236,63.721,54.245,63.701,54.243,63.582,54.222,63.414,54.183,63.293,54.17,63.191,54.171,63.127,54.139,63.074,54.105,62.633,54.069,62.588,54.044,62.499,54.013,62.04,54.003,62.002,53.98,61.986,53.954,61.929,53.946,61.598,53.995,61.334,54.049,61.231,54.019,61.144,53.964,61.113,53.882,61.113,53.813,61.113,53.753,61.074,53.71,60.986,53.657,60.979,53.622,61.099,53.583,61.248,53.551,61.336,53.565,61.41,53.587,61.474,53.58,61.519,53.554,61.535,53.523,61.527,53.502,61.499,53.485,61.401,53.456,61.312,53.466,61.229,53.446,61.186,53.406,61.163,53.337,61.199,53.287,61.311,53.275,61.437,53.239,61.576,53.222,61.66,53.228,61.766,53.174,62.015,53.108,62.081,53.057,62.083,53.005,62.037,52.966,61.974,52.944,61.889,52.956,61.719,52.969,61.534,52.979,61.401,52.996,61.207,52.989,61.047,52.972,61.007,52.933,60.945,52.86,60.893,52.819,60.802,52.745,60.774,52.676,60.821,52.57,60.979,52.395,60.995,52.337,60.938,52.281,60.828,52.233,60.67,52.151,60.499,52.146,60.425,52.126,60.234,52.025,60.066,51.976,60.03,51.933,60.067,51.891,60.28,51.835,60.388,51.773,60.418,51.704,60.465,51.651,60.63,51.617,60.974,51.537,60.993,51.529,61.015,51.492,61.363,51.442,61.411,51.415,61.555,51.325,61.585,51.23,61.512,51.137,61.465,50.99,61.389,50.861,61.227,50.775,60.942,50.696,60.638,50.664,60.508,50.669,60.425,50.679,60.288,50.704,60.187,50.77,60.112,50.834,60.059,50.85,60.005,50.84,59.955,50.799,59.888,50.69,59.812,50.582,59.751,50.544,59.523,50.493,59.498,50.511,59.524,50.583,59.495,50.604,59.452,50.62,59.171,50.648,59.064,50.668,58.985,50.676,58.884,50.694,58.814,50.737,58.665,50.868,58.547,50.971,58.359,51.064,58.188,51.082,58.175,51.072,58.045,51.069,57.839,51.092,57.829,51.089,57.765,51.047,57.717,50.981,57.654,50.925,57.558,50.896,57.442,50.889,57.313,50.947,57.179,51.036,57.012,51.065,56.85,51.046,56.79,51.032,56.62,50.981,56.567,51.004,56.491,51.02,56.326,50.936,56.144,50.845,56.104,50.776,56.05,50.714,55.929,50.654,55.798,50.602,55.686,50.583,55.542,50.602,55.361,50.665,55.195,50.745,55.015,50.87,54.868,50.941,54.727,50.998,54.642,51.012,54.573,50.99,54.546,50.946,54.566,50.911,54.606,50.88,54.638,50.781,54.65,50.66,54.636,50.592,54.596,50.551,54.555,50.536,54.517,50.541,54.471,50.584,54.443,50.674,54.421,50.78,54.298,50.914,54.191,50.996,54.14,51.041,54.042,51.115,53.957,51.161,53.776,51.214,53.688,51.252,53.535,51.4,53.449,51.445,53.338,51.482,53.247,51.494,53.227,51.485,53.038,51.464,52.903,51.467,52.821,51.495,52.735,51.498,52.728,51.498,52.635,51.48,52.618,51.481,52.571,51.482,52.496,51.512,52.423,51.594,52.331,51.681,52.219,51.709,52.007,51.673,51.775,51.554,51.609,51.484,51.473,51.482,51.396,51.471,51.345,51.475,51.301,51.497,51.291,51.54,51.27,51.594,51.163,51.647,51.018,51.682,50.882,51.719,50.794,51.729,50.756,51.675,50.644,51.589,50.516,51.506,50.354,51.37,50.309,51.322,50.247,51.29,50.105,51.255,49.932,51.197,49.822,51.132,49.666,51.102,49.498,51.084,49.425,51.027,49.379,50.935,49.323,50.852,49.059,50.726,48.914,50.645,48.808,50.601,48.735,50.607,48.655,50.62,48.625,50.613,48.666,50.55,48.7,50.354,48.749,50.228,48.785,50.156,48.818,50.1,48.843,50.013,48.81,49.962,48.759,49.928,48.6,49.875,48.434,49.829,48.335,49.858,48.225,49.932,48.181,49.97,48.061,50.094,47.85,50.282,47.706,50.378,47.6,50.414,47.504,50.403,47.429,50.358,47.376,50.318,47.326,50.274,47.295,50.217,47.298,50.14,47.295,50.058,47.248,50.001,47.13,49.939,46.992,49.853,46.89,49.697,46.823,49.502,46.802,49.367,46.853,49.304,46.953,49.253,47.018,49.2,47.031,49.15,47.014,49.098,46.962,49.038,46.853,48.97,46.703,48.806,46.609,48.574,46.661,48.412,46.853,48.324,47.004,48.284,47.065,48.232,47.119,48.127,47.112,48.02,47.093,47.948,47.131,47.877,47.202,47.792,47.292,47.741,47.387,47.769,47.482,47.804,47.6,47.79,47.935,47.761,48.11,47.745,48.167,47.709,48.276,47.59,48.413,47.456,48.553,47.321,48.601,47.262,48.714,47.1,48.832,46.955,48.959,46.775,48.95,46.726,48.884,46.705,48.776,46.71,48.694,46.737,48.647,46.759,48.605,46.766,48.558,46.757,48.519,46.734,48.502,46.699,48.509,46.65,48.541,46.606,48.586,46.577,48.61,46.566,48.774,46.508,48.959,46.442,49.184,46.349,49.232,46.337,49.246,46.292,49.125,46.282,49.111,46.228,49.08,46.189,48.81,46.1,48.743,46.101,48.684,46.086,48.687,46.029,48.703,45.976,48.75,45.921,48.73,45.897,48.69,45.889,48.637,45.906,48.589,45.935,48.537,45.942,48.487,45.935,48.258,45.778,48.159,45.737,48.053,45.721,47.83,45.663,47.764,45.666,47.701,45.686,47.65,45.657,47.633,45.584,47.574,45.634,47.508,45.674,47.479,45.688,47.463,45.68,47.524,45.602,47.529,45.53,47.515,45.491,47.489,45.455,47.454,45.433,47.413,45.421,47.391,45.295,47.351,45.218,47.296,45.149,47.221,45.024,47.162,44.97,47.115,44.906,47.084,44.817,47.039,44.838,47.003,44.876,46.984,44.826,46.957,44.783,46.841,44.718,46.755,44.657,46.716,44.561,46.707,44.503,46.721,44.452,46.753,44.421,46.916,44.387,47.024,44.343,47.123,44.262,47.23,44.192,47.307,44.103,47.362,43.993,47.429,43.78,47.463,43.555,47.563,43.835,47.646,43.885,47.628,43.806,47.568,43.685,47.509,43.51,47.49,43.382,47.512,43.271,47.513,43.219,47.463,43.035,47.489,43,47.529,42.967,47.635,42.903,47.709,42.811,47.728,42.681,47.77,42.645,47.822,42.613,48.08,42.354,48.229,42.181,48.303,42.08,48.384,41.953,48.426,41.924,48.477,41.905,48.573,41.844,48.519,41.779,48.431,41.663,48.391,41.602,48.298,41.545,48.142,41.485,48.056,41.459,47.964,41.334,47.861,41.213,47.791,41.199,47.592,41.218,47.521,41.229,47.318,41.282,47.261,41.315,47.205,41.456,47.143,41.516,47.064,41.555,47.01,41.588,46.988,41.621,46.931,41.67,46.826,41.743,46.749,41.813,46.69,41.831,46.616,41.807,46.571,41.8,46.552,41.812,46.538,41.87,46.43,41.891,46.412,41.905,46.268,41.96,46.213,41.99,46.16,41.992,46.048,42.009,45.954,42.035,45.91,42.071,45.846,42.11,45.727,42.159,45.639,42.205,45.634,42.235,45.688,42.357,45.728,42.475,45.705,42.498,45.656,42.518,45.563,42.536,45.344,42.53,45.208,42.648,45.16,42.675,45.072,42.694,44.943,42.73,44.871,42.756,44.85,42.747,44.771,42.617,44.692,42.71,44.644,42.735,44.576,42.748,44.506,42.749,44.329,42.704,44.2,42.654,44.103,42.616,44.005,42.596,43.957,42.567,43.826,42.572,43.76,42.594,43.738,42.617,43.75,42.658,43.795,42.703,43.799,42.728,43.783,42.747,43.623,42.808,43.558,42.844,43.348,42.897,43.089,42.989,43,43.05,42.992,43.092,42.89,43.133,42.761,43.17,42.66,43.159,42.566,43.155,42.419,43.224,42.28,43.228,42.122,43.207,42.088,43.199,42.05,43.19,41.581,43.219,41.461,43.276,41.358,43.333,41.083,43.374,40.942,43.418,40.802,43.48,40.648,43.534,40.519,43.512,40.342,43.543,40.15,43.57,40.085,43.553,40.024,43.485,39.978,43.42,39.874,43.473,39.517,43.728,39.329,43.897,38.717,44.288,38.636,44.318,38.312,44.374,38.181,44.42,37.851,44.699,37.705,44.661,37.572,44.671,37.495,44.695,37.411,44.735,37.352,44.788,37.284,44.905,37.205,44.972,36.944,45.07,36.651,45.126,36.628,45.151,36.619,45.185,36.873,45.252,36.941,45.29,36.811,45.34,36.762,45.348,36.72,45.372,36.794,45.41,36.866,45.427,36.978,45.384,37.104,45.303,37.214,45.272,37.264,45.311,37.647,45.377,37.673,45.43,37.672,45.488,37.634,45.486,37.61,45.5,37.612,45.565,37.669,45.654,37.841,45.8,37.933,46.002,38.014,46.048,38.074,46.017,38.07,45.97,38.08,45.935,38.133,46.003,38.184,46.095,38.312,46.095,38.4,46.08,38.492,46.091,38.315,46.242,38.078,46.394,37.978,46.383,37.914,46.406,37.81,46.532,37.767,46.636,37.867,46.634,37.968,46.618,38.159,46.691,38.23,46.701,38.343,46.678,38.501,46.664,38.488,46.732,38.439,46.813,38.631,46.873,38.801,46.906,39.127,47.023,39.271,47.044,39.289,47.071,39.293,47.106,39.245,47.2,39.196,47.269,39.024,47.272,38.928,47.176,38.668,47.144,38.552,47.15,38.644,47.212,38.736,47.236,38.762,47.262,38.577,47.239,38.485,47.176,38.214,47.091,38.206,47.136,38.201,47.175,38.221,47.213,38.265,47.237,38.281,47.259,38.281,47.277,38.241,47.288,38.208,47.297,38.201,47.321,38.212,47.343,38.243,47.374,38.257,47.409,38.259,47.48,38.287,47.559,38.369,47.61,38.511,47.622,38.641,47.666,38.719,47.714,38.822,47.837,38.9,47.855,39.058,47.848,39.158,47.837,39.391,47.834,39.658,47.841,39.736,47.845,39.779,47.888,39.776,47.964,39.814,48.035,39.885,48.168,39.961,48.238,39.958,48.269,39.918,48.282,39.866,48.288,39.847,48.303,39.85,48.332,39.89,48.36,39.883,48.419,39.858,48.484,39.836,48.543,39.765,48.572,39.645,48.591,39.67,48.662,39.705,48.739,39.756,48.782,39.793,48.808,39.904,48.794,39.984,48.807,40.004,48.822,39.989,48.851,39.864,48.878,39.753,48.914,39.706,48.96,39.687,49.008,39.759,49.037,39.89,49.064,39.976,49.13,40.07,49.2,40.109,49.252,40.128,49.307,40.126,49.369,40.058,49.432,40.058,49.497,40.095,49.543,40.081,49.577,40.031,49.597,39.958,49.591,39.877,49.568,39.781,49.572,39.627,49.651,39.463,49.728,39.368,49.731,39.303,49.742,39.246,49.782,39.212,49.833,39.175,49.856,39.115,49.842,39.028,49.818,38.918,49.825,38.777,49.884,38.648,49.953,38.552,49.955,38.451,49.964,38.259,50.052,38.209,50.051,38.178,50.025,38.163,49.955,38.147,49.939,38.113,49.928,38.047,49.92,37.95,49.964,37.704,50.109,37.605,50.215,37.582,50.292,37.501,50.341,37.423,50.411,37.343,50.418,37.255,50.395,37.171,50.361,37.131,50.352,36.988,50.34,36.759,50.292,36.696,50.246,36.619,50.209,36.56,50.235,36.5,50.28,36.369,50.297,36.306,50.28,36.243,50.312,36.189,50.368,36.116,50.409,36.008,50.42,35.89,50.437,35.796,50.406,35.674,50.346,35.591,50.369,35.546,50.44,35.488,50.46,35.412,50.54,35.392,50.611,35.412,50.642,35.44,50.682,35.44,50.728,35.417,50.768,35.383,50.799,35.346,50.904,35.315,50.95,35.309,50.987,35.335,51.021,35.312,51.044,35.269,51.047,35.198,51.044,35.158,51.061,35.115,51.121,35.093,51.181,35.064,51.203,34.99,51.202,34.869,51.189,34.76,51.169,34.712,51.172,34.617,51.203,34.491,51.237,34.234,51.244,34.214,51.255,34.228,51.277,34.281,51.312,34.275,51.34,34.23,51.363,34.207,51.42,34.209,51.484,34.201,51.554,34.147,51.608,34.115,51.645,34.121,51.679,34.239,51.692,34.379,51.717,34.403,51.742,34.398,51.78,34.113,51.98,34.015,52.156,33.922,52.251,33.819,52.316,33.735,52.345,33.613,52.333,33.452,52.334,33.287,52.354,33.148,52.34,32.9,52.256,32.806,52.253,32.645,52.279,32.508,52.309,32.435,52.307,32.391,52.295,32.363,52.272,32.283,52.114,32.217,52.083,32.122,52.051,32.042,52.045,31.974,52.047,31.876,52.071,31.782,52.099,31.763,52.101,31.759,52.126,31.691,52.221,31.65,52.262,31.602,52.285,31.577,52.312,31.577,52.426,31.586,52.532,31.616,52.546,31.526,52.633,31.519,52.699,31.563,52.731,31.565,52.759,31.535,52.798,31.443,52.862,31.353,52.933,31.295,52.99,31.259,53.017,31.303,53.061,31.365,53.139,31.388,53.185,31.418,53.196,31.563,53.202,31.668,53.201,31.747,53.184,31.777,53.147,31.85,53.106,32.055,53.089,32.142,53.091,32.251,53.128,32.426,53.211,32.469,53.27,32.578,53.312,32.644,53.329,32.704,53.336,32.71,53.371,32.706,53.419,32.686,53.448,32.47,53.547,32.442,53.579,32.425,53.617,32.451,53.653,32.45,53.693,32.2,53.781,31.992,53.797,31.821,53.792,31.754,53.81,31.783,53.855,31.825,53.935,31.838,54.001,31.826,54.031,31.792,54.056,31.628,54.111,31.404,54.196,31.299,54.292,31.246,54.392,31.185,54.453,31.075,54.492,31.082,54.517,31.155,54.611,31.152,54.625,31.121,54.648,30.984,54.696,30.799,54.783,30.791,54.806,30.804,54.861,30.83,54.915,30.867,54.941,30.978,55.05,30.978,55.088,30.959,55.138,30.877,55.223,30.814,55.279,30.811,55.307,30.821,55.33,30.862,55.36,30.901,55.397,30.909,55.525,30.907,55.57,30.882,55.596,30.856,55.608,30.801,55.601,30.722,55.622,30.662,55.655,30.626,55.666,30.587,55.7,30.475,55.769,30.456,55.787,30.234,55.845,30.043,55.836,29.937,55.845,29.882,55.832,29.824,55.795,29.744,55.77,29.685,55.77,29.63,55.751,29.482,55.685,29.413,55.725,29.353,55.784,29.373,55.835,29.398,55.881,29.396,55.912,29.375,55.939,29.283,55.968,29.087,56.021,29.032,56.022,28.947,56.002,28.795,55.943,28.741,55.955,28.691,56.003,28.637,56.062,28.564,56.092,28.407,56.089,28.392,56.087,28.316,56.053,28.284,56.056,28.148,56.143,28.173,56.19,28.202,56.26,28.192,56.316,28.169,56.387,28.111,56.511,28.103,56.546,28.008,56.6,27.992,56.645,27.941,56.704,27.892,56.741,27.882,56.824,27.849,56.853,27.806,56.867,27.656,56.843,27.639,56.846,27.711,56.978,27.717,57.055,27.763,57.135,27.815,57.167,27.83,57.194,27.838,57.248,27.829,57.293,27.797,57.317,27.673,57.368,27.539,57.43,27.511,57.508,27.47,57.524,27.352,57.528,27.354,57.55,27.372,57.613,27.4,57.667,27.492,57.725,27.515,57.764,27.542,57.799,27.753,57.841,27.777,57.857,27.779,57.871,27.769,57.884,27.722,57.905,27.673,57.935,27.644,58.014,27.571,58.138,27.502,58.221,27.488,58.27,27.506,58.326,27.53,58.381,27.531,58.435,27.427,58.733,27.434,58.787,27.464,58.841,27.513,58.886,27.622,58.945,27.758,59.052,27.85,59.193,27.898,59.278,27.938,59.297,28.016,59.302,28.046,59.328,28.061,59.343,28.128,59.358,28.151,59.374,28.133,59.403,28.066,59.453,28.013,59.484,28.064,59.554,28.046,59.647,28.014,59.725,28.058,59.782,28.131,59.787,28.213,59.725,28.335,59.693,28.424,59.734,28.454,59.814,28.518,59.85,28.604,59.818,28.748,59.807,28.867,59.812,28.947,59.829,28.982,59.855,29.013,59.902,29.079,59.961,29.147,60,29.67,59.956,30.123,59.874,30.157,59.904,30.173,59.957,30.06,60.003,29.977,60.026,29.872,60.121,29.721,60.195,29.569,60.202,29.37,60.176,29.069,60.191,28.813,60.332,28.643,60.375,28.522,60.483,28.492,60.54,28.622,60.492,28.64,60.543,28.651,60.611,28.578,60.653,28.513,60.677,28.179,60.571,27.798,60.536,28.152,60.746,28.407,60.897,28.455,60.92,28.568,60.96,28.663,61.003,28.739,61.059,28.993,61.169,29.252,61.288,29.492,61.444,29.579,61.493,29.69,61.546,29.933,61.712,30.01,61.757,30.306,61.965,30.48,62.068,30.566,62.128,30.936,62.324,31.187,62.481,31.286,62.568,31.382,62.692,31.437,62.776,31.534,62.885,31.537,62.922,31.509,62.955,31.437,63.008,31.337,63.068,31.247,63.142,31.181,63.208,30.975,63.301,30.655,63.417,30.419,63.504,30.055,63.689,29.992,63.735,30.004,63.747,30.21,63.803,30.415,63.948,30.504,64.021,30.526,64.077,30.528,64.141,30.514,64.2,30.488,64.237,30.391,64.282,30.108,64.366,30.042,64.443,29.987,64.524,29.986,64.558,30.12,64.645,30.126,64.688,30.11,64.733,30.073,64.765,29.783,64.804,29.702,64.846,29.638,64.912,29.604,64.968,29.601,65.002,29.622,65.04,29.72,65.08,29.811,65.108,29.827,65.145,29.826,65.185,29.811,65.205,29.63,65.224,29.612,65.235,29.608,65.249,29.617,65.265,29.715,65.337,29.728,65.473,29.819,65.569,29.716,65.625,29.724,65.634,29.883,65.664,30.029,65.671,30.095,65.682,30.103,65.726,30.088,65.787,29.937,66.023,29.903,66.091,29.804,66.177,29.721,66.235,29.671,66.276,29.591,66.357,29.544,66.44,29.464,66.532,29.371,66.617,29.293,66.696,29.093,66.849,29.066,66.892,29.069,66.93,29.087,66.971,29.243,67.097,29.388,67.201,29.572,67.324,29.751,67.426,29.941,67.547,29.988,67.668,29.979,67.689,29.822,67.754,29.524,67.929,29.344,68.062,29.063,68.118,28.685,68.19,28.56,68.351,28.471,68.488,28.479,68.538,28.752,68.771,28.778,68.814,28.773,68.84,28.745,68.856,28.706,68.866,28.454,68.872,28.414,68.904,28.566,68.928,28.692,68.961,28.899,69.01,28.966,69.022,29.119,69.05,29.171,69.072,29.21,69.097,29.353,69.271,29.388,69.298,29.833,69.36,29.994,69.392,30.087,69.433,30.132,69.464,30.164,69.502,30.187,69.543,30.196,69.581,30.16,69.63,30.18,69.636,30.228,69.634,30.38,69.585,30.615,69.533,30.789,69.529,30.861,69.538,30.897,69.561,30.922,69.606,30.924,69.652,30.87,69.783,31.05,69.769,31.453,69.69,31.547,69.697,31.666,69.721,31.789,69.816,31.879,69.832,31.998,69.81,32.031,69.835,31.969,69.914,31.985,69.954,32.392,69.869,32.565,69.806,32.942,69.752,33.008,69.722,33.013,69.671,32.995,69.626,32.915,69.602,32.754,69.606,32.177,69.674,32.092,69.633,32.161,69.597,32.331,69.554,32.378,69.479,32.637,69.489,32.884,69.461,33,69.47,33.021,69.446,32.942,69.383,32.979,69.367,33.256,69.428,33.385,69.444,33.454,69.428,33.464,69.378,33.418,69.315,33.413,69.267,33.328,69.152,33.196,69.117,33.141,69.069,33.333,69.098,33.436,69.13,33.627,69.289,33.684,69.31,34.229,69.313,34.353,69.303,34.864,69.228,35.01,69.221,35.176,69.231,35.233,69.266,35.29,69.275,35.858,69.192,36.618,69.003,37.731,68.692,38.358,68.415,38.43,68.356,38.657,68.322,38.706,68.345,38.832,68.325,39.569,68.072,39.823,68.059,39.79,68.112,39.746,68.162,39.809,68.151,39.896,68.115,40.036,68.015,40.207,67.942,40.381,67.832,40.526,67.79,40.657,67.774,40.766,67.743,40.966,67.713,41.061,67.444,41.134,67.386,41.134,67.267,41.262,67.218,41.359,67.21,41.354,67.121,41.276,66.914,41.189,66.826,40.522,66.447,40.103,66.3,39.289,66.132,38.654,66.069,38.398,66.064,37.901,66.096,37.628,66.13,37.295,66.225,36.984,66.273,36.77,66.294,36.373,66.302,35.513,66.396,35.364,66.429,34.825,66.611,34.61,66.56,34.483,66.55,34.396,66.613,34.431,66.63,34.452,66.651,34.146,66.703,33.894,66.707,33.76,66.751,33.595,66.785,33.523,66.764,33.482,66.765,33.15,66.844,33.002,66.908,32.848,67.022,32.885,67.061,32.93,67.087,32.4,67.153,31.895,67.161,31.983,67.13,32.202,67.113,32.341,67.068,32.501,67.004,32.464,66.916,32.686,66.83,32.857,66.747,32.862,66.721,32.929,66.704,33.181,66.68,33.224,66.604,33.183,66.574,33.217,66.532,33.405,66.484,33.518,66.471,33.656,66.443,33.593,66.385,33.477,66.347,33.361,66.33,33.416,66.316,33.567,66.321,34.113,66.225,34.4,66.128,34.692,65.952,34.786,65.865,34.793,65.816,34.777,65.768,34.735,65.716,34.716,65.664,34.616,65.51,34.544,65.457,34.406,65.396,34.536,65.278,34.671,65.168,34.804,64.986,34.827,64.913,34.833,64.8,34.952,64.756,34.905,64.739,34.858,64.707,34.87,64.56,35.035,64.44,35.284,64.363,35.432,64.347,35.647,64.378,35.802,64.335,36.146,64.189,36.302,64.034,36.365,64.003,36.714,63.945,36.975,63.91,37.373,63.817,37.442,63.813,37.635,63.893,37.968,63.949,38.071,64.026,38.062,64.091,37.977,64.207,37.954,64.32,37.844,64.366,37.741,64.397,37.43,64.374,37.29,64.378,37.184,64.408,37.04,64.489,36.769,64.685,36.624,64.751,36.579,64.791,36.528,64.847,36.535,64.939,36.653,64.935,36.786,64.987,36.883,65.172,37.05,65.196,37.141,65.194,37.528,65.108,38.009,64.879,38.116,64.855,38.228,64.851,38.412,64.857,38.442,64.827,38.541,64.791,38.613,64.787,39.054,64.714,39.567,64.571,39.758,64.577,39.833,64.656,39.849,64.691,40.058,64.771,40.204,64.784,40.408,64.755,40.445,64.779,40.375,64.896,40.281,64.998,40.143,65.063,39.896,65.255,39.798,65.35,39.749,65.448,39.781,65.535,39.817,65.598,40.328,65.752,40.513,65.844,40.692,65.963,40.774,65.988,41.076,66.021,41.476,66.123,41.781,66.259,42.084,66.466,42.211,66.52,42.314,66.515,42.451,66.482,42.602,66.423,42.807,66.411,43.006,66.421,43.233,66.416,43.551,66.321,43.603,66.291,43.653,66.251,43.55,66.173,43.542,66.123,43.624,66.147,43.737,66.158,43.844,66.142,43.944,66.099,44.017,66.05,44.104,66.009,44.132,66.065,44.145,66.113,44.097,66.235,44.221,66.407,44.316,66.482,44.489,66.672,44.437,66.795,44.429,66.938,44.404,67.004,44.292,67.1,44.074,67.167,43.855,67.189,43.782,67.254,43.796,67.33,43.856,67.439,44.036,67.671,44.225,67.996,44.232,68.071,44.214,68.113,44.226,68.154,44.205,68.254,44.169,68.327,43.404,68.609,43.358,68.636,43.333,68.673,43.413,68.682,43.472,68.68,44.048,68.549,44.175,68.542,45.078,68.578,45.519,68.547,45.892,68.48,46.158,68.291,46.43,68.119,46.684,67.97,46.69,67.849,46.429,67.824,46.174,67.818,45.529,67.758,45.374,67.689,44.939,67.477,44.902,67.413,44.939,67.351,45.139,67.285,45.562,67.186,45.753,66.989,45.885,66.891,45.986,66.853,46.084,66.844,46.298,66.843,46.449,66.819,46.492,66.8,46.552,66.819,46.691,66.826,47.496,66.93,47.656,66.976,47.709,67.045,47.768,67.276,47.839,67.356,47.908,67.455,47.883,67.515,47.875,67.584,48.279,67.65,48.654,67.695,48.833,67.681,48.878,67.731,48.763,67.827,48.696,67.874,48.754,67.896,48.841,67.87,48.954,67.854,49.155,67.87,49.931,68.065,50.233,68.175,50.414,68.218,50.699,68.318,50.839,68.35,51.079,68.363,51.336,68.402,51.617,68.476,51.995,68.539,52.056,68.541,52.129,68.532,52.285,68.459,52.227,68.419,52.183,68.374,52.259,68.351,52.322,68.34,52.397,68.352,52.475,68.382,52.67,68.427,52.723,68.484,52.648,68.506,52.55,68.592,52.435,68.61,52.344,68.608,52.684,68.731,53.413,68.913,53.802,68.996,54.186,69.003,54.491,68.992,54.376,68.965,53.874,68.927,53.798,68.907,53.798,68.885,53.92,68.871,53.971,68.844,53.929,68.812,53.891,68.802,53.834,68.709,53.759,68.634,53.918,68.537,53.931,68.436,53.829,68.383,53.69,68.403,53.567,68.367,53.343,68.343,53.293,68.312,53.261,68.267,53.403,68.257,53.515,68.26,53.914,68.231,53.968,68.227,54.099,68.259,54.233,68.266,54.394,68.275,54.476,68.294,54.561,68.273,54.718,68.184,54.861,68.202,54.923,68.374,55.151,68.48,55.418,68.568,55.675,68.576,55.925,68.637,56.044,68.649,56.276,68.624,56.62,68.619,56.909,68.567,57.127,68.554,57.444,68.642,58.173,68.89,58.237,68.834,58.354,68.916,58.919,69.004,59.057,69.006,59.06,68.973,59.11,68.896,59.22,68.85,59.371,68.738,59.298,68.708,59.223,68.691,59.112,68.616,59.099,68.444,59.311,68.4,59.604,68.351,59.726,68.352,59.828,68.38,59.859,68.396,59.897,68.422,59.923,68.471,59.941,68.51,59.865,68.605,59.896,68.706,60.16,68.7,60.489,68.729,60.638,68.787,60.815,68.895,60.934,68.987,60.859,69.146,60.665,69.11,60.337,69.457,60.171,69.591,60.276,69.653,60.559,69.692,60.813,69.821,60.909,69.847,61.016,69.851,61.771,69.763,62.631,69.743,63.361,69.675,64.19,69.535,64.592,69.436,64.929,69.325,64.896,69.248,65.032,69.27,65.327,69.201,65.528,69.173,65.736,69.132,65.813,69.077,66.085,69.036,66.366,68.961,66.416,68.948,66.756,68.892,67.002,68.874,67.149,68.754,67.64,68.579,67.731,68.514,68.157,68.404,68.371,68.314,68.504,68.348,68.829,68.567,69.024,68.818,69.141,68.951,68.924,68.956,68.763,68.917,68.66,68.927,68.543,68.967,68.355,69.068,68.117,69.236,68.073,69.421,68.006,69.48,67.774,69.53,67.624,69.584,67.064,69.694,66.964,69.656,66.934,69.597,66.897,69.554,66.84,69.609,66.804,69.659,66.803,69.74,66.832,69.842,66.926,70.014,67.069,70.006,67.144,70.031,67.239,70.108,67.197,70.172,67.146,70.22,67.157,70.295,67.247,70.5,67.285,70.739,67.212,70.798,67.143,70.838,66.822,70.797,66.702,70.819,66.675,70.865,66.666,70.901,66.759,70.962,66.847,71.064,66.693,71.042,66.64,71.081,66.768,71.14,66.918,71.282,67.274,71.348,67.542,71.412,67.959,71.548,68.269,71.683,68.469,71.853,68.607,72.013,68.83,72.392,69.039,72.67,69.391,72.956,69.612,72.982,69.694,72.978,69.709,72.956,69.659,72.932,69.645,72.898,69.738,72.885,69.888,72.883,70.172,72.901,70.655,72.89,71.5,72.914,71.617,72.902,71.93,72.82,72.101,72.829,72.446,72.79,72.634,72.744,72.812,72.691,72.787,72.483,72.753,72.343,72.624,72.079,72.574,72.013,72.375,71.822,72.279,71.696,72.13,71.609,71.912,71.548,71.884,71.511,71.867,71.457,72.079,71.307,72.581,71.151,72.704,70.963,72.732,70.823,72.7,70.457,72.653,70.403,72.562,70.346,72.469,70.275,72.53,70.173,72.599,69.793,72.616,69.484,72.557,69.378,72.527,69.154,72.527,69.081,72.577,68.969,72.678,68.875,72.812,68.815,73.191,68.707,73.548,68.575,73.573,68.533,73.592,68.482,73.465,68.431,73.266,68.294,73.139,68.181,73.129,68.091,73.173,67.973,73.152,67.865,73.067,67.767,72.949,67.696,72.594,67.587,71.847,67.008,71.668,66.94,71.365,66.962,71.449,66.879,71.551,66.76,71.54,66.683,71.342,66.687,71.066,66.604,70.939,66.548,70.725,66.519,70.561,66.549,70.383,66.602,70.409,66.648,70.443,66.668,70.568,66.701,70.691,66.745,70.631,66.754,70.579,66.754,70.444,66.697,70.283,66.686,70.094,66.754,69.949,66.83,69.877,66.845,69.74,66.815,69.218,66.829,69.079,66.816,69.013,66.788,69.051,66.766,69.091,66.724,69.144,66.641,69.194,66.579,69.412,66.511,69.701,66.485,69.982,66.401,70.339,66.342,71.146,66.367,71.358,66.359,71.566,66.334,71.917,66.247,72.068,66.253,72.322,66.332,72.384,66.507,72.417,66.561,73.342,66.807,73.514,66.861,73.792,66.995,73.883,67.085,73.986,67.328,74.075,67.414,74.676,67.695,74.77,67.766,74.787,67.898,74.778,67.986,74.743,68.074,74.632,68.218,74.511,68.303,74.391,68.421,74.481,68.659,74.58,68.751,75.125,68.862,75.59,68.901,76.108,68.976,76.316,68.992,76.459,68.978,76.606,68.898,76.735,68.777,77.112,68.596,77.238,68.47,77.261,68.316,77.248,67.941,77.174,67.779,77.325,67.736,77.396,67.699,77.579,67.644,77.675,67.59,77.772,67.57,77.986,67.559,78.59,67.578,78.922,67.589,78.888,67.613,78.839,67.631,78.559,67.639,78.161,67.678,77.588,67.752,77.52,67.91,77.536,68.008,77.665,68.19,77.757,68.222,77.868,68.235,77.995,68.259,77.959,68.377,77.907,68.482,77.785,68.63,77.651,68.903,77.466,68.905,77.328,68.959,76.645,69.117,76.001,69.235,75.561,69.252,75.42,69.239,75.054,69.116,74.815,69.091,74.363,69.145,73.977,69.115,73.836,69.143,73.776,69.198,73.891,69.418,73.833,69.504,73.663,69.617,73.56,69.707,73.578,69.803,73.83,70.176,73.937,70.273,74.207,70.445,74.343,70.579,74.311,70.654,73.732,71.069,73.577,71.217,73.507,71.264,73.365,71.32,73.15,71.385,73.086,71.445,73.672,71.845,73.939,71.915,74.311,71.958,74.489,71.997,74.804,72.077,74.992,72.145,75.053,72.199,75.09,72.263,75.097,72.421,75.06,72.549,75.008,72.619,74.897,72.71,74.787,72.812,74.865,72.838,74.942,72.854,75.152,72.853,75.369,72.797,75.475,72.685,75.604,72.581,75.603,72.512,75.591,72.457,75.644,72.382,75.691,72.35,75.741,72.296,75.694,72.254,75.644,72.232,75.55,72.171,75.395,71.983,75.274,71.959,75.247,71.813,75.503,71.655,75.469,71.534,75.417,71.495,75.28,71.43,75.298,71.378,75.332,71.342,75.734,71.266,76.11,71.219,76.742,71.202,76.929,71.128,76.995,71.181,77.59,71.168,78.068,70.986,78.321,70.93,78.526,70.912,78.942,70.934,79.015,70.95,79.084,71.002,78.889,70.997,78.804,70.974,78.724,70.976,78.588,70.994,78.491,71.025,78.387,71.087,78.213,71.266,77.908,71.324,77.707,71.301,77.481,71.312,77.114,71.409,76.871,71.447,76.433,71.552,76.312,71.595,76.216,71.683,76.104,71.829,76.032,71.91,76.124,71.927,76.422,72.006,76.871,72.033,77.061,72.004,77.551,71.842,77.778,71.836,78.187,71.907,78.232,71.952,78.141,72.045,78.016,72.092,77.781,72.114,77.493,72.072,77.411,72.108,77.44,72.157,77.472,72.192,77.625,72.201,77.733,72.229,77.968,72.329,78.225,72.377,78.483,72.395,79.422,72.381,79.954,72.223,80.474,72.153,80.699,72.098,80.763,72.089,80.815,72.054,80.856,71.97,81.511,71.746,81.662,71.716,82.08,71.707,82.547,71.759,82.758,71.764,82.986,71.749,83.107,71.721,83.234,71.668,83.166,71.602,83.106,71.562,82.977,71.451,82.918,71.42,82.493,71.293,82.323,71.26,82.277,71.093,82.254,71.056,82.239,70.998,82.316,70.879,82.336,70.807,82.271,70.707,82.163,70.598,82.182,70.511,82.221,70.396,82.236,70.43,82.231,70.483,82.258,70.544,82.452,70.69,82.592,70.89,82.738,70.942,82.869,70.955,83.01,70.895,83.051,70.815,83.058,70.695,83.03,70.581,82.92,70.407,82.742,70.286,82.682,70.218,82.767,70.154,82.857,70.105,82.961,70.088,83.081,70.093,83.11,70.11,83.132,70.157,83.094,70.221,83.074,70.277,83.293,70.321,83.497,70.345,83.66,70.418,83.7,70.466,83.736,70.546,83.651,70.672,83.579,70.766,83.334,70.989,83.151,71.104,83.266,71.276,83.458,71.468,83.531,71.514,83.55,71.544,83.571,71.594,83.554,71.65,83.534,71.684,83.34,71.828,83.2,71.875,82.755,71.903,82.645,71.925,82.319,72.072,82.281,72.105,82.209,72.211,82.184,72.238,82.094,72.265,81.793,72.327,81.586,72.352,81.283,72.359,81.098,72.39,80.827,72.488,80.798,72.52,80.72,72.648,80.656,72.712,80.675,72.759,80.774,72.861,80.842,72.949,80.757,73.025,80.639,73.049,80.51,73.086,80.455,73.155,80.425,73.231,80.419,73.29,80.398,73.357,80.458,73.414,80.596,73.474,80.562,73.515,80.583,73.568,81.469,73.64,81.817,73.659,83.545,73.667,83.667,73.686,84.417,73.722,84.738,73.763,85.077,73.72,85.201,73.722,85.448,73.735,85.611,73.822,85.979,73.857,86.591,73.894,86.893,73.887,86.961,73.861,87.029,73.824,86.698,73.717,86.366,73.62,86.094,73.578,85.827,73.493,85.8,73.459,85.793,73.438,85.802,73.372,85.818,73.327,86.098,73.273,86.308,73.196,86.514,73.14,86.677,73.107,86.715,73.126,86.122,73.307,85.971,73.347,85.91,73.39,85.939,73.456,85.999,73.486,86.092,73.519,86.155,73.535,86.376,73.569,87.12,73.615,87.294,73.705,87.37,73.756,87.571,73.811,87.503,73.832,87.338,73.846,87.21,73.879,86.697,74.195,86.571,74.244,86.178,74.279,86.001,74.316,86.183,74.423,86.396,74.45,86.538,74.444,86.665,74.414,86.898,74.325,87.23,74.364,87.106,74.404,86.894,74.45,86.7,74.522,86.426,74.585,86.116,74.629,85.791,74.645,85.881,74.74,86.059,74.728,86.12,74.757,86.201,74.816,86.651,74.682,86.863,74.718,87.042,74.779,87.419,74.941,87.468,75.013,87.287,75.053,87.141,75.072,86.939,75.068,86.922,75.113,87.006,75.17,87.171,75.192,87.671,75.13,88.504,75.29,88.733,75.369,89.31,75.47,89.595,75.458,90.185,75.591,91.005,75.65,91.479,75.65,91.845,75.724,92.408,75.75,92.603,75.779,93.55,75.854,94.075,75.913,94.156,75.959,93.687,75.922,93.574,75.956,93.475,75.933,93.406,75.901,93.178,75.959,93.116,75.945,93.069,75.913,92.987,75.903,92.89,75.91,92.859,75.979,92.972,76.075,93.105,76.026,93.259,76.099,93.36,76.101,93.648,76.054,93.843,76.101,94.102,76.124,94.388,76.103,94.507,76.108,94.576,76.152,95.038,76.114,95.359,76.14,95.579,76.137,95.92,76.113,96.075,76.082,95.986,76.01,95.653,75.892,95.744,75.872,95.935,75.926,96.509,76.006,96.601,75.99,96.538,75.922,96.497,75.891,96.879,75.931,97.205,76.019,97.351,76.033,97.499,75.98,97.638,76.029,97.67,76.078,97.918,76.089,98.02,76.134,98.195,76.166,98.342,76.181,98.662,76.243,98.771,76.224,98.985,76.208,99.187,76.178,99.563,76.109,99.616,76.082,99.663,76.078,99.77,76.029,99.689,75.956,99.602,75.852,99.442,75.803,99.541,75.799,99.609,75.811,99.738,75.881,99.851,75.93,99.825,76.136,99.617,76.24,99.461,76.275,99.094,76.384,98.97,76.431,98.806,76.481,98.869,76.51,99.576,76.471,99.936,76.49,100.322,76.479,100.844,76.525,101.061,76.477,101.311,76.479,101.598,76.439,101.684,76.485,101.213,76.536,101.003,76.531,100.928,76.557,101.006,76.615,101.099,76.704,101.008,76.781,100.92,76.823,100.906,76.901,100.99,76.99,101.186,77.029,101.293,77.102,101.518,77.198,102.61,77.509,103.131,77.626,103.331,77.641,103.561,77.632,104.015,77.73,104.185,77.73,104.814,77.652,104.965,77.595,105.309,77.549,105.71,77.525,105.895,77.489,105.983,77.448,106.06,77.391,105.734,77.352,105.385,77.238,104.912,77.175,104.324,77.133,104.202,77.102,105.32,77.092,105.646,77.101,105.712,77.001,105.822,76.998,106.145,77.045,106.339,77.048,106.705,77.014,106.784,77.032,106.942,77.034,107.279,76.991,107.43,76.927,107.19,76.822,106.941,76.73,106.639,76.573,106.546,76.586,106.385,76.589,106.414,76.512,106.683,76.515,106.825,76.48,107.158,76.524,107.624,76.51,107.722,76.522,107.902,76.57,107.95,76.661,108.028,76.718,108.182,76.738,108.352,76.72,108.638,76.72,109.369,76.749,109.981,76.712,110.471,76.758,111.115,76.723,111.392,76.687,111.601,76.622,111.786,76.604,111.939,76.553,112.094,76.48,112.017,76.421,111.943,76.38,112.143,76.424,112.297,76.435,112.413,76.408,112.62,76.384,112.684,76.219,112.743,76.187,112.798,76.13,112.722,76.077,112.656,76.054,112.819,76.059,113.047,76.114,113.094,76.133,113.15,76.175,113.066,76.215,112.988,76.24,113.086,76.258,113.273,76.252,113.366,76.179,113.428,76.112,113.564,75.892,113.857,75.921,113.871,75.856,113.749,75.705,113.62,75.593,113.568,75.568,113.486,75.564,113.517,75.622,113.469,75.657,113.392,75.678,113.126,75.699,112.629,75.835,112.497,75.85,112.466,75.844,112.453,75.83,112.73,75.738,112.956,75.572,113.162,75.621,113.243,75.611,113.356,75.534,113.559,75.502,113.726,75.451,113.614,75.293,112.925,75.015,112.192,74.853,111.868,74.74,111.299,74.658,110.893,74.548,110.374,74.466,110.226,74.379,109.84,74.322,109.866,74.293,109.911,74.261,109.864,74.209,109.81,74.169,109.511,74.089,109.075,74.032,108.2,73.694,107.765,73.625,107.271,73.621,107.167,73.589,106.794,73.377,106.679,73.331,106.189,73.308,105.677,72.959,105.393,72.841,105.144,72.777,105.403,72.79,105.708,72.837,106.067,72.95,106.16,73.002,106.209,73.061,106.315,73.106,106.478,73.139,107.109,73.177,107.369,73.163,107.75,73.173,108.001,73.236,108.151,73.258,108.285,73.266,108.351,73.31,108.575,73.319,109.09,73.378,109.166,73.4,109.331,73.487,109.637,73.454,109.855,73.472,110.429,73.629,110.773,73.689,110.868,73.731,110.799,73.76,110.722,73.78,110.388,73.726,110.091,73.709,109.753,73.723,109.707,73.744,109.666,73.8,109.774,73.881,109.869,73.931,110.084,73.994,110.261,74.017,110.92,73.948,111.056,73.939,111.131,74.053,111.341,74.047,111.551,74.029,111.46,74.005,111.228,73.969,111.3,73.885,111.4,73.828,111.804,73.745,112.147,73.709,112.4,73.711,112.795,73.746,112.856,73.771,112.94,73.836,112.836,73.962,112.935,73.946,113.033,73.914,113.182,73.837,113.327,73.707,113.416,73.648,113.364,73.583,113.157,73.46,113.277,73.392,113.491,73.346,113.488,73.145,113.475,73.048,113.369,72.942,113.247,72.897,113.128,72.831,113.158,72.769,113.186,72.73,113.312,72.657,113.665,72.635,113.712,72.654,113.63,72.677,113.391,72.711,113.298,72.739,113.216,72.806,113.312,72.878,113.417,72.932,113.543,73.054,113.581,73.142,113.559,73.233,113.639,73.274,113.765,73.318,113.829,73.327,113.886,73.346,113.795,73.367,113.711,73.379,113.539,73.434,113.51,73.505,113.857,73.533,114.061,73.585,114.816,73.607,115.338,73.703,116.496,73.676,117.309,73.599,118.45,73.59,118.871,73.538,118.911,73.518,118.936,73.481,118.754,73.465,118.457,73.464,118.377,73.367,118.43,73.247,118.96,73.117,119.425,73.064,119.75,72.979,119.922,72.971,120.598,72.981,120.997,72.937,121.354,72.971,121.748,72.97,121.886,72.961,122.03,72.897,122.26,72.881,122.538,72.878,122.692,72.891,122.752,72.906,122.731,72.931,122.502,72.971,122.527,73.017,122.615,73.028,122.999,72.965,123.16,72.955,123.301,73.002,123.405,73.086,123.462,73.144,123.522,73.173,123.572,73.177,123.622,73.193,123.501,73.262,123.384,73.347,123.355,73.402,123.323,73.431,123.305,73.533,123.416,73.637,123.491,73.666,123.797,73.627,123.934,73.689,124.019,73.712,124.388,73.755,124.541,73.751,124.796,73.712,125.617,73.521,125.599,73.447,125.794,73.468,125.888,73.498,126.107,73.517,126.254,73.548,126.296,73.537,126.345,73.506,126.309,73.464,126.257,73.42,126.292,73.394,126.335,73.389,126.553,73.335,126.838,73.434,126.955,73.528,127.031,73.547,127.74,73.482,127.955,73.446,127.997,73.426,128.026,73.391,128.142,73.352,128.281,73.331,128.264,73.301,128.258,73.267,128.587,73.262,128.73,73.233,128.889,73.19,128.872,73.139,128.913,73.111,129.059,73.108,129.101,73.112,129.054,73.045,128.854,72.973,128.735,72.943,128.599,72.895,128.674,72.886,129.017,72.872,129.229,72.776,129.25,72.705,129.118,72.677,128.815,72.586,128.633,72.55,128.508,72.547,128.418,72.535,128.549,72.496,129.117,72.486,129.281,72.438,129.412,72.315,129.411,72.166,129.283,72.092,128.935,72.079,128.475,72.246,128.197,72.31,127.803,72.434,127.726,72.413,127.841,72.308,128.027,72.25,128.359,72.088,128.911,71.755,129.04,71.782,129.117,71.825,129.154,71.879,129.122,71.953,129.21,71.917,129.292,71.85,129.461,71.739,129.234,71.745,128.949,71.708,128.843,71.663,128.923,71.602,129.134,71.593,129.225,71.509,129.39,71.405,129.762,71.12,130.026,71.065,130.281,70.947,130.537,70.893,130.668,70.888,130.757,70.962,130.832,70.936,130.898,70.804,131.022,70.746,131.157,70.742,131.268,70.766,131.432,70.828,131.562,70.901,131.769,71.101,131.906,71.203,132.035,71.244,131.991,71.293,132.004,71.35,132.099,71.484,132.228,71.643,132.326,71.726,132.562,71.895,132.654,71.926,132.716,71.871,132.769,71.799,132.804,71.768,132.839,71.755,133.131,71.607,133.426,71.491,133.689,71.434,134.103,71.379,134.703,71.387,134.814,71.461,135.022,71.515,135.359,71.544,135.559,71.61,135.885,71.631,136.09,71.62,136.406,71.571,137.116,71.416,137.315,71.359,137.417,71.299,137.651,71.208,137.798,71.164,137.94,71.133,137.992,71.143,137.974,71.169,137.902,71.194,137.844,71.227,138.013,71.261,138.033,71.286,138.091,71.307,138.314,71.326,138.097,71.359,138.022,71.363,137.918,71.384,137.927,71.43,137.996,71.464,138.048,71.526,138.118,71.566,138.234,71.596,138.318,71.603,138.525,71.563,138.67,71.635,138.78,71.629,139.005,71.556,139.209,71.445,139.32,71.445,139.632,71.489,139.984,71.492,139.939,71.558,139.695,71.7,139.723,71.885,139.552,71.927,139.359,71.951,139.64,71.998,139.847,72.149,140.014,72.162,140.188,72.191,140.134,72.21,139.617,72.226,139.505,72.208,139.43,72.163,139.176,72.163,139.145,72.264,139.141,72.33,139.474,72.467,139.601,72.496,140.451,72.493,140.705,72.519,141.079,72.587,140.983,72.63,140.973,72.717,140.652,72.843,140.676,72.872,140.708,72.89,140.808,72.891,141.31,72.858,141.518,72.789,142.061,72.721,143.516,72.698,143.681,72.673,144.304,72.643,144.569,72.61,145.199,72.57,145.486,72.542,145.714,72.497,146.083,72.471,146.253,72.442,146.235,72.35,145.467,72.362,145.213,72.393,144.897,72.396,144.776,72.382,144.588,72.306,144.361,72.265,144.169,72.259,144.295,72.193,144.471,72.175,145.039,72.26,146.594,72.302,146.832,72.295,146.807,72.237,146.599,72.124,146.402,72.035,146.113,71.945,146.006,71.945,146.23,72.138,146.137,72.146,146.051,72.142,145.799,72.222,145.759,72.226,145.71,72.206,145.71,72.178,145.664,72.067,145.757,72.021,145.757,71.941,145.407,71.89,145.271,71.895,145.126,71.927,145.064,71.926,145.047,71.901,145.078,71.855,145.074,71.831,145.018,71.794,144.99,71.753,145.076,71.707,145.189,71.696,145.805,71.746,146.073,71.808,146.368,71.922,146.895,72.198,147.127,72.292,147.262,72.328,147.434,72.341,148.402,72.312,148.965,72.252,149.502,72.164,149.766,72.091,149.963,71.992,149.998,71.95,150.017,71.896,149.881,71.843,149.28,71.826,149.049,71.796,148.965,71.763,148.955,71.744,148.923,71.715,148.968,71.69,149.238,71.688,149.498,71.664,149.857,71.601,149.913,71.581,150.026,71.521,150.061,71.511,150.6,71.52,150.635,71.499,150.668,71.455,150.525,71.386,150.385,71.339,150.098,71.227,150.243,71.267,150.822,71.363,150.968,71.38,151.145,71.374,151.582,71.287,151.76,71.218,152.093,71.023,152,71.002,151.762,70.982,152.509,70.834,152.798,70.836,153.461,70.879,153.794,70.88,154.414,70.974,155.029,71.034,155.596,71.039,155.895,71.096,156.685,71.094,157.447,71.075,158.037,71.039,158.702,70.935,159.351,70.791,159.728,70.65,159.805,70.605,159.912,70.506,159.959,70.424,160.006,70.31,159.983,70.221,159.89,70.159,159.831,70.081,159.839,69.99,159.729,69.87,159.833,69.785,160.119,69.73,160.739,69.655,160.911,69.606,160.929,69.459,160.982,69.334,161.036,69.098,161.141,69.039,161.31,68.982,161.341,68.905,161.129,68.654,160.997,68.608,160.856,68.538,161.104,68.563,161.23,68.654,161.365,68.823,161.495,68.85,161.566,68.905,161.566,69.064,161.48,69.202,161.48,69.3,161.537,69.38,161.945,69.545,162.166,69.612,162.376,69.649,162.945,69.683,163.201,69.715,163.498,69.693,163.705,69.702,163.946,69.735,164.16,69.719,164.513,69.609,165.761,69.584,165.98,69.546,166.82,69.5,166.884,69.5,167.073,69.554,167.628,69.74,167.857,69.728,167.95,69.699,168.048,69.626,168.15,69.577,168.23,69.447,168.303,69.271,168.423,69.24,168.588,69.228,168.946,69.163,169.311,69.08,169.415,68.92,169.61,68.786,170.066,68.799,170.538,68.825,170.995,69.045,170.997,69.135,170.884,69.264,170.714,69.388,170.582,69.583,170.161,69.627,170.201,69.683,170.36,69.751,170.503,69.857,170.525,69.938,170.487,70.108,170.868,70.096,171.247,70.076,171.971,70,172.56,69.968,172.869,69.92,173.056,69.865,173.277,69.824,173.353,69.924,173.439,69.947,173.733,69.891,173.948,69.874,174.319,69.882,174.786,69.856,175.296,69.86,175.751,69.904,175.921,69.895,176.108,69.86,176.41,69.769,176.924,69.646,177.395,69.612,177.934,69.496,178.443,69.453,178.848,69.387,178.907,69.362,178.925,69.326,178.951,69.296,179.273,69.26,179.868,69.013,180,68.983,180,65.067,179.827,65.034,179.651,64.921,179.448,64.822,179.15,64.782,178.698,64.631,178.52,64.603,178.285,64.672,177.749,64.717,177.582,64.778,177.337,64.931,177.252,64.954,177.179,65.014,176.881,65.082,176.625,65.038,176.413,65.071,176.341,65.047,176.452,65.025,176.646,65.007,176.94,65.016,177.037,65,177.123,64.947,177.223,64.862,177.148,64.805,177.069,64.787,176.831,64.849,176.557,64.84,176.429,64.855,176.061,64.961,175.781,64.844,175.396,64.784,175.098,64.777,174.549,64.684,174.699,64.681,175.097,64.747,175.331,64.747,175.678,64.782,175.859,64.825,175.946,64.865,176.057,64.905,176.169,64.885,176.247,64.843,176.301,64.784,176.351,64.705,176.283,64.664,176.219,64.642,176.141,64.586,176.508,64.682,176.731,64.625,176.843,64.634,177.05,64.719,177.388,64.774,177.427,64.763,177.467,64.737,177.41,64.573,177.433,64.444,177.688,64.305,177.953,64.222,178.045,64.22,178.131,64.235,178.164,64.309,178.229,64.364,178.313,64.314,178.381,64.261,178.477,64.128,178.475,64.089,178.451,64.011,178.536,63.976,178.65,63.965,178.692,63.842,178.731,63.667,178.681,63.651,178.626,63.651,178.44,63.606,178.466,63.574,178.654,63.557,178.706,63.522,178.669,63.44,178.679,63.402,178.744,63.395,178.787,63.442,178.775,63.51,178.793,63.54,178.919,63.4,178.921,63.345,179.028,63.282,179.332,63.19,179.389,63.147,179.405,63.078,179.329,63.058,179.26,63.008,179.302,62.94,179.381,62.884,179.511,62.863,179.571,62.773,179.571,62.688,179.477,62.613,179.289,62.51,179.177,62.469,179.134,62.396,179.121,62.32,179.045,62.324,178.964,62.355,178.019,62.547,177.663,62.583,177.351,62.587,177.293,62.599,177.296,62.644,177.316,62.685,177.36,62.737,177.339,62.781,177.298,62.784,177.259,62.75,177.173,62.75,177.091,62.79,177.024,62.777,176.99,62.722,176.963,62.693,176.965,62.659,177.008,62.627,177.19,62.592,177.159,62.561,176.907,62.536,176.703,62.506,176.437,62.411,176.328,62.346,175.614,62.184,175.442,62.128,175.366,62.121,175.268,62.102,175.192,62.034,174.798,61.939,174.715,61.948,174.611,61.868,174.514,61.824,174.285,61.818,174.139,61.795,173.822,61.679,173.623,61.716,173.391,61.557,173.132,61.407,173.055,61.406,172.857,61.469,172.807,61.436,172.838,61.376,172.908,61.312,172.868,61.293,172.789,61.311,172.731,61.314,172.69,61.295,172.697,61.249,172.585,61.19,172.497,61.186,172.396,61.167,172.362,61.117,172.393,61.062,172.213,60.998,172.067,60.916,171.998,60.901,171.918,60.864,171.831,60.837,171.729,60.843,171.49,60.726,170.949,60.523,170.799,60.496,170.608,60.435,170.59,60.394,170.589,60.343,170.512,60.26,170.423,60.048,170.396,60.01,170.351,59.966,170.154,59.986,169.983,60.067,169.927,60.104,169.898,60.148,169.887,60.218,169.854,60.25,169.815,60.265,169.618,60.438,169.276,60.557,169.227,60.596,168.788,60.564,168.67,60.563,168.463,60.592,168.138,60.574,167.746,60.509,167.626,60.469,167.227,60.406,166.964,60.307,166.453,59.947,166.332,59.872,166.273,59.856,166.187,59.849,166.149,59.922,166.136,59.979,166.168,60.089,166.23,60.178,166.292,60.346,166.308,60.414,166.352,60.485,166.18,60.48,165.942,60.357,165.583,60.236,165.416,60.205,165.285,60.135,165.193,60.125,165.085,60.099,165.074,59.946,165.019,59.861,164.954,59.844,164.854,59.841,164.779,59.874,164.67,59.997,164.525,60.061,164.44,60.073,164.377,60.058,164.252,59.974,164.113,59.898,164.135,59.984,164.018,60.017,163.913,60.037,163.78,60.041,163.744,60.028,163.69,59.978,163.574,59.914,163.494,59.887,163.41,59.835,163.365,59.781,163.321,59.705,163.269,59.52,163.273,59.303,163.085,59.131,163.01,59.148,162.975,59.137,162.94,59.114,163.004,59.02,162.97,58.986,162.935,58.964,162.847,58.939,162.643,58.8,162.453,58.709,162.142,58.447,162.049,58.273,161.96,58.077,162.002,57.981,162.04,57.918,162.098,57.875,162.197,57.829,162.411,57.778,162.392,57.745,162.391,57.717,162.467,57.766,162.522,57.904,162.654,57.948,162.718,57.946,163.145,57.837,163.226,57.79,163.214,57.687,163.188,57.637,163.109,57.565,162.957,57.477,162.779,57.358,162.762,57.284,162.762,57.244,162.808,57.103,162.815,57.023,162.791,56.875,162.803,56.811,162.85,56.757,162.922,56.723,163.046,56.741,163.165,56.725,163.257,56.688,163.243,56.565,163.294,56.448,163.336,56.233,163.261,56.174,163.189,56.137,163.047,56.045,162.972,56.034,162.84,56.066,162.628,56.232,162.713,56.331,162.893,56.399,162.975,56.449,163.038,56.522,162.944,56.508,162.878,56.476,162.671,56.49,162.589,56.455,162.489,56.399,162.528,56.261,162.461,56.235,162.334,56.188,162.146,56.128,162.085,56.09,161.924,55.84,161.776,55.655,161.724,55.496,161.729,55.358,161.785,55.205,161.824,55.139,161.996,54.998,162.08,54.886,162.106,54.752,161.967,54.689,161.726,54.533,161.625,54.516,161.294,54.521,161.13,54.598,160.936,54.578,160.773,54.541,160.517,54.431,160.289,54.288,160.074,54.189,160.01,54.131,159.922,54.008,159.844,53.784,159.871,53.673,159.914,53.621,159.956,53.552,159.899,53.448,159.898,53.381,160.002,53.275,160.025,53.13,159.947,53.125,159.772,53.23,159.586,53.238,159.136,53.117,158.952,53.048,158.745,52.909,158.684,52.935,158.64,53.015,158.565,53.05,158.472,53.032,158.432,52.957,158.56,52.922,158.609,52.874,158.534,52.688,158.481,52.627,158.5,52.46,158.493,52.383,158.463,52.305,158.332,52.091,158.104,51.81,157.823,51.605,157.629,51.535,157.531,51.48,157.49,51.409,157.202,51.213,156.847,51.007,156.748,50.969,156.724,51.047,156.713,51.124,156.671,51.227,156.543,51.312,156.521,51.38,156.5,51.475,156.49,51.913,156.377,52.367,156.365,52.509,156.229,52.626,156.154,52.747,156.11,52.866,156.099,53.006,155.95,53.744,155.905,53.928,155.706,54.521,155.62,54.865,155.564,55.199,155.555,55.348,155.643,55.794,155.717,56.072,155.983,56.695,156.025,56.752,156.067,56.782,156.529,57.021,156.728,57.152,156.849,57.29,156.977,57.466,156.964,57.561,156.948,57.616,156.9,57.677,156.792,57.748,156.83,57.78,156.872,57.804,156.986,57.83,157.217,57.777,157.45,57.799,157.666,58.02,157.975,57.986,158.21,58.025,158.275,58.009,158.321,58.083,158.449,58.163,158.687,58.281,159.037,58.424,159.211,58.519,159.308,58.611,159.453,58.696,159.592,58.804,159.847,59.127,160.35,59.394,160.547,59.547,160.711,59.602,160.855,59.627,161.219,59.846,161.449,60.027,161.754,60.152,161.846,60.232,162.004,60.42,162.068,60.466,162.266,60.537,162.713,60.659,162.973,60.783,163.352,60.8,163.466,60.85,163.585,60.877,163.71,60.917,163.554,61.026,163.589,61.084,163.62,61.111,163.893,61.24,164.005,61.344,163.992,61.388,163.973,61.42,163.804,61.461,163.837,61.558,163.883,61.64,164.02,61.711,164.068,61.874,164.074,62.045,164.207,62.292,164.288,62.347,164.598,62.471,164.671,62.474,164.888,62.432,165.124,62.412,165.208,62.374,165.226,62.406,165.214,62.448,165.28,62.463,165.417,62.447,165.397,62.494,165.044,62.517,164.792,62.571,164.567,62.675,164.418,62.705,164.256,62.697,163.332,62.551,163.287,62.511,163.244,62.455,163.302,62.373,163.258,62.337,163.213,62.313,163.163,62.26,163.118,62.153,163.131,62.05,163.018,61.891,163.009,61.792,163.208,61.737,163.258,61.699,163.198,61.645,163.139,61.611,163.085,61.571,163.047,61.554,162.994,61.544,162.922,61.598,162.856,61.705,162.752,61.711,162.718,61.695,162.699,61.653,162.608,61.65,162.506,61.67,162.393,61.662,162.188,61.541,161.037,60.963,160.915,60.893,160.767,60.753,160.482,60.74,160.368,60.709,160.287,60.667,160.174,60.638,160.177,60.691,160.201,60.73,160.226,60.832,160.379,61.025,160.281,61.045,160.184,61.048,160.004,61.007,159.883,60.943,159.79,60.957,159.835,61.014,159.949,61.129,159.914,61.234,159.883,61.292,159.931,61.324,160.163,61.538,160.247,61.648,160.317,61.793,160.321,61.839,160.309,61.894,160.238,61.904,160.183,61.903,159.722,61.758,159.552,61.719,159.496,61.781,159.423,61.808,159.295,61.914,159.189,61.929,159.077,61.922,158.824,61.85,158.547,61.811,158.334,61.826,158.152,61.765,158.07,61.754,157.799,61.795,157.469,61.799,157.371,61.747,157.084,61.676,156.892,61.565,156.791,61.53,156.68,61.481,156.63,61.272,156.483,61.206,156.344,61.155,156.056,60.996,155.853,60.777,155.716,60.682,155.428,60.55,154.971,60.377,154.578,60.095,154.441,59.884,154.39,59.877,154.293,59.833,154.267,59.73,154.269,59.658,154.209,59.6,154.15,59.529,154.213,59.483,154.272,59.475,154.358,59.481,154.583,59.54,154.971,59.45,155.167,59.36,155.153,59.27,155.16,59.19,155.017,59.196,154.824,59.188,154.704,59.141,154.458,59.217,154.376,59.188,154.247,59.109,154.011,59.076,153.892,59.114,153.695,59.225,153.361,59.215,153.273,59.091,153.196,59.094,153.078,59.082,152.882,58.939,152.818,58.926,152.576,58.954,152.401,59.026,152.32,59.031,152.165,58.997,152.088,58.91,151.705,58.867,151.327,58.875,151.121,59.083,151.505,59.164,151.733,59.147,151.99,59.16,152.261,59.224,152.17,59.278,152.104,59.291,151.942,59.284,151.798,59.323,151.486,59.524,151.348,59.561,151.17,59.583,151.034,59.586,150.983,59.571,150.912,59.523,150.863,59.475,150.823,59.461,150.729,59.469,150.615,59.507,150.484,59.494,150.54,59.525,150.667,59.556,150.457,59.591,150.326,59.639,150.203,59.651,149.643,59.77,149.425,59.761,149.29,59.728,149.065,59.631,149.128,59.559,149.175,59.527,149.205,59.488,149.133,59.481,148.925,59.475,148.797,59.532,148.709,59.449,148.744,59.374,148.89,59.4,148.965,59.369,148.914,59.283,148.727,59.258,148.491,59.262,148.257,59.414,147.875,59.388,147.688,59.291,147.514,59.269,147.04,59.366,146.804,59.373,146.537,59.457,146.444,59.43,146.273,59.221,146.05,59.171,145.932,59.198,145.829,59.33,145.756,59.374,145.555,59.414,144.483,59.376,144.123,59.408,143.869,59.411,143.524,59.344,143.192,59.37,142.58,59.24,142.33,59.153,142.025,59,141.755,58.745,141.603,58.649,141.347,58.528,140.988,58.417,140.79,58.303,140.685,58.212,140.495,57.865,140.447,57.814,140.002,57.688,139.862,57.549,139.803,57.514,139.619,57.456,139.507,57.358,139.444,57.33,139.182,57.262,138.966,57.088,138.662,56.966,138.218,56.629,138.18,56.589,138.141,56.499,138.074,56.433,137.692,56.139,137.573,56.112,137.384,55.975,137.19,55.892,137.012,55.795,136.794,55.694,136.46,55.577,136.351,55.51,136.175,55.352,135.751,55.161,135.541,55.114,135.263,54.943,135.235,54.903,135.212,54.841,135.258,54.731,135.325,54.707,135.438,54.692,135.852,54.584,136.238,54.614,136.58,54.614,136.715,54.624,136.797,54.621,136.824,54.561,136.82,54.452,136.77,54.353,136.729,54.061,136.683,53.931,136.719,53.804,136.803,53.782,136.886,53.839,137.019,53.848,137.155,53.822,137.258,54.025,137.172,54.057,137.096,54.129,137.142,54.182,137.378,54.282,137.525,54.291,137.666,54.283,137.513,54.156,137.451,54.13,137.403,54.124,137.339,54.101,137.476,54.028,137.623,53.97,137.835,53.947,137.786,53.903,137.645,53.866,137.517,53.707,137.314,53.632,137.221,53.579,137.254,53.546,137.328,53.539,137.738,53.56,137.95,53.604,138.253,53.726,138.379,53.909,138.494,53.96,138.528,53.96,138.568,53.947,138.569,53.819,138.407,53.674,138.292,53.592,138.25,53.524,138.32,53.523,138.451,53.537,138.511,53.57,138.661,53.745,138.699,53.87,138.722,54.044,138.705,54.148,138.716,54.223,138.657,54.298,138.696,54.32,139.105,54.218,139.32,54.193,139.707,54.277,139.796,54.256,139.858,54.205,140.179,54.052,140.242,54.001,140.347,53.813,140.688,53.596,141.006,53.495,141.015,53.454,141.218,53.334,141.374,53.293,141.402,53.184,141.328,53.097,141.181,53.015,140.887,53.092,140.84,53.088,140.875,53.04,141.087,52.898,141.256,52.84,141.266,52.653,141.245,52.55,141.132,52.436,141.17,52.368,141.33,52.271,141.409,52.234,141.485,52.179,141.386,52.057,141.367,51.921,141.258,51.861,141.129,51.728,140.933,51.62,140.839,51.414,140.688,51.232,140.671,51.051,140.646,50.987,140.521,50.8,140.476,50.546,140.535,50.131,140.564,50.107,140.625,50.082,140.613,50.054,140.585,50.033,140.463,49.911,140.465,49.826,140.511,49.762,140.517,49.596,140.431,49.331,140.399,49.29,140.364,49.221,140.349,49.159,140.326,49.12,140.309,49.054,140.334,48.995,140.378,48.964,140.224,48.773,140.171,48.524,140.113,48.423,139.998,48.324,139.761,48.181,139.676,48.09,139.521,47.975,139.373,47.887,139.167,47.635,139.001,47.383,138.587,47.057,138.53,46.976,138.5,46.89,138.392,46.745,138.337,46.543,138.21,46.463,138.106,46.251,137.769,45.929,137.685,45.818,137.425,45.64,137.147,45.394,136.804,45.171,136.737,45.08,136.604,44.978,136.46,44.822,136.251,44.667,136.209,44.562,136.142,44.489,135.987,44.44,135.875,44.374,135.533,43.971,135.489,43.899,135.483,43.835,135.26,43.685,135.131,43.526,134.917,43.427,134.692,43.291,134.156,43.042,134.01,42.947,133.709,42.83,133.587,42.828,133.329,42.764,133.16,42.697,133.059,42.723,132.997,42.808,132.924,42.805,132.864,42.794,132.709,42.876,132.576,42.872,132.481,42.91,132.304,42.883,132.334,43.239,132.31,43.314,132.233,43.245,132.029,43.119,131.947,43.095,131.867,43.095,131.898,43.171,132.013,43.28,131.976,43.296,131.939,43.302,131.795,43.255,131.722,43.203,131.516,42.996,131.393,42.822,131.292,42.772,131.245,42.697,131.158,42.626,131.025,42.645,130.946,42.634,130.756,42.673,130.709,42.656,130.834,42.523,130.73,42.326,130.687,42.303]],[[47.442,80.854,47.9,80.813,48.243,80.823,48.345,80.819,48.446,80.806,48.547,80.779,48.687,80.718,48.684,80.633,48.625,80.629,48.044,80.668,47.777,80.756,47.705,80.765,47.6,80.742,47.512,80.688,47.414,80.675,47.304,80.606,47.198,80.615,47.145,80.609,47.011,80.562,46.678,80.561,46.624,80.541,46.514,80.476,46.378,80.457,46.141,80.447,46.06,80.484,46.024,80.541,45.969,80.569,45.641,80.537,45.389,80.56,45.149,80.599,44.905,80.611,45.125,80.652,46.327,80.735,46.799,80.755,47.021,80.814,47.352,80.853,47.442,80.854]],[[50.278,80.927,50.431,80.911,50.801,80.914,50.918,80.89,51.455,80.745,51.591,80.741,51.704,80.688,51.146,80.604,50.961,80.54,50.28,80.527,49.846,80.498,49.75,80.472,49.794,80.425,49.586,80.377,48.896,80.369,48.811,80.354,48.677,80.3,48.689,80.29,48.922,80.277,48.96,80.266,48.991,80.242,49.011,80.207,48.978,80.163,48.892,80.155,48.797,80.161,48.582,80.195,48.555,80.183,48.533,80.158,48.467,80.11,48.386,80.096,48.167,80.133,48.096,80.122,48.026,80.099,47.94,80.089,47.737,80.082,47.632,80.112,47.723,80.151,47.978,80.213,47.893,80.239,47.642,80.245,47.444,80.23,47.343,80.189,47.249,80.18,46.991,80.183,46.846,80.237,46.738,80.258,46.644,80.3,47.403,80.445,47.656,80.501,47.896,80.529,48.208,80.544,48.306,80.562,48.403,80.569,48.465,80.558,48.625,80.508,49.088,80.516,49.185,80.559,49.193,80.656,49.147,80.712,49.244,80.821,49.508,80.865,50.124,80.924,50.278,80.927]],[[67.765,76.238,67.365,76.161,67.127,76.108,66.893,76.072,66.657,76.047,66.282,75.984,65.619,75.905,65.202,75.839,64.745,75.788,64.263,75.72,63.779,75.673,63.659,75.669,63.317,75.603,63.046,75.576,62.066,75.428,61.616,75.32,61.487,75.311,61.356,75.315,61.249,75.281,61.147,75.223,60.936,75.164,60.829,75.111,60.719,75.069,60.655,75.055,60.534,75.059,60.476,75.055,60.277,75.008,60.241,74.971,60.455,74.946,60.501,74.905,60.439,74.875,60.301,74.837,60.222,74.797,60.08,74.756,59.982,74.745,59.747,74.746,59.735,74.695,59.771,74.664,59.753,74.637,59.674,74.61,59.596,74.614,59.24,74.693,59.182,74.666,59.157,74.611,59.146,74.552,59.101,74.508,59.04,74.486,58.928,74.463,58.535,74.499,58.502,74.464,58.562,74.422,58.646,74.328,58.665,74.289,58.618,74.227,58.441,74.129,57.767,74.014,57.778,73.974,57.853,73.898,57.872,73.85,57.845,73.805,57.756,73.769,57.657,73.768,57.604,73.775,57.449,73.826,57.313,73.838,57.291,73.815,57.464,73.746,57.543,73.658,57.46,73.61,57.134,73.504,56.964,73.367,56.634,73.304,56.43,73.297,56.228,73.314,56.035,73.346,55.549,73.357,55.28,73.392,55.007,73.454,54.769,73.449,54.566,73.419,54.3,73.351,54.132,73.481,54.205,73.542,53.839,73.697,53.763,73.766,53.851,73.801,53.963,73.822,54.174,73.886,54.386,73.936,54.606,73.951,54.643,73.96,54.733,74.034,54.831,74.096,54.92,74.129,55.023,74.187,55.341,74.42,55.416,74.436,56.078,74.481,56.137,74.496,55.947,74.542,55.752,74.541,55.662,74.556,55.61,74.591,55.582,74.628,55.66,74.656,55.914,74.796,56.218,74.898,56.499,74.957,56.429,74.973,56.34,75.013,55.998,75.003,55.863,75.059,55.821,75.091,55.81,75.125,55.921,75.168,56.036,75.194,56.162,75.187,56.289,75.164,56.389,75.138,56.485,75.096,56.57,75.098,56.876,75.244,56.829,75.278,56.809,75.328,56.844,75.351,56.989,75.375,57.088,75.384,57.302,75.373,57.607,75.341,57.632,75.356,57.708,75.454,57.783,75.507,58.094,75.593,58.073,75.619,58.058,75.663,58.418,75.72,58.653,75.777,58.881,75.855,58.995,75.872,59.11,75.874,59.347,75.907,59.782,75.946,60.036,75.984,60.118,76.067,60.279,76.096,60.606,76.109,60.731,76.104,60.801,76.069,60.942,76.071,60.998,76.089,61.054,76.12,61.037,76.169,61.034,76.233,61.157,76.274,61.202,76.282,61.569,76.298,61.787,76.291,62.237,76.242,62.471,76.23,62.782,76.245,62.971,76.237,63.526,76.31,64.463,76.378,64.708,76.426,64.95,76.484,65.073,76.497,65.197,76.5,65.31,76.518,65.528,76.568,65.637,76.579,65.755,76.579,65.863,76.613,65.959,76.688,66.063,76.746,66.345,76.821,66.829,76.924,67.264,76.964,67.535,77.008,67.652,77.012,68.017,76.991,68.486,76.934,68.699,76.871,68.873,76.79,68.912,76.761,68.942,76.708,68.891,76.66,68.858,76.61,68.9,76.573,68.559,76.449,68.222,76.313,68.165,76.285,67.765,76.238]],[[55.32,73.308,55.787,73.269,56.138,73.256,56.35,73.226,56.43,73.201,56.397,73.139,56.335,73.114,56.189,73.033,56.167,72.983,56.193,72.905,56.171,72.848,56.122,72.807,56.084,72.789,55.82,72.79,55.723,72.766,55.718,72.722,55.701,72.672,55.616,72.599,55.441,72.575,55.403,72.549,55.417,72.501,55.356,72.465,55.36,72.409,55.39,72.378,55.399,72.314,55.518,72.221,55.495,72.182,55.403,72.107,55.375,72.015,55.298,71.935,55.471,71.869,55.547,71.783,55.614,71.69,55.819,71.508,56.043,71.346,56.454,71.107,56.895,70.927,57.066,70.876,57.484,70.792,57.556,70.766,57.625,70.729,57.447,70.661,57.264,70.636,57.247,70.605,57.146,70.589,56.649,70.647,56.622,70.655,56.569,70.697,56.51,70.729,56.386,70.734,56.26,70.715,56.335,70.677,56.417,70.665,56.561,70.594,56.5,70.566,56.435,70.563,56.142,70.658,56.115,70.646,56.087,70.618,55.942,70.649,55.907,70.626,55.797,70.616,55.707,70.642,55.706,70.675,55.687,70.692,55.237,70.666,55.052,70.667,54.867,70.678,54.645,70.742,54.608,70.713,54.601,70.68,54.517,70.693,54.333,70.745,54.199,70.765,53.722,70.814,53.384,70.874,53.468,70.901,53.614,70.915,53.616,70.951,53.593,71.001,53.588,71.052,53.671,71.087,53.857,71.07,53.834,71.127,53.922,71.138,54.094,71.105,54.156,71.125,53.886,71.196,53.591,71.297,53.622,71.333,53.515,71.343,53.41,71.34,53.319,71.399,53.333,71.477,53.412,71.53,53.364,71.542,52.909,71.495,52.679,71.506,52.419,71.537,52.18,71.49,51.938,71.475,51.813,71.491,51.692,71.525,51.59,71.571,51.511,71.648,51.439,71.777,51.429,71.826,51.444,71.934,51.482,71.98,51.583,72.071,51.653,72.099,51.805,72.142,51.885,72.153,52.069,72.131,52.252,72.13,52.332,72.154,52.407,72.197,52.462,72.252,52.586,72.284,52.622,72.301,52.662,72.337,52.706,72.391,52.714,72.437,52.749,72.483,52.864,72.55,52.823,72.591,52.839,72.619,52.917,72.669,52.683,72.682,52.605,72.704,52.529,72.737,52.551,72.769,52.579,72.791,52.812,72.875,52.913,72.9,53.024,72.914,53.135,72.913,53.254,72.904,53.37,72.917,53.247,72.973,53.237,73.011,53.189,73.104,53.198,73.148,53.251,73.183,53.358,73.225,53.512,73.238,53.634,73.26,53.753,73.293,53.866,73.299,54.091,73.276,54.202,73.281,54.328,73.299,54.676,73.37,54.804,73.388,54.941,73.383,55.121,73.357,55.32,73.308]],[[96.527,81.076,96.563,81.03,96.693,80.994,96.755,80.958,97.414,80.842,97.703,80.827,97.832,80.798,97.87,80.763,97.856,80.698,97.747,80.699,97.665,80.678,97.221,80.652,97.113,80.614,97.025,80.536,97.073,80.52,97.115,80.497,97.25,80.363,97.287,80.343,97.417,80.323,97.298,80.273,97.175,80.241,95.856,80.177,94.961,80.15,94.661,80.123,94.565,80.126,94.328,80.076,93.872,80.01,93.655,80.01,93.002,80.102,92.202,80.179,92.092,80.223,91.892,80.249,91.637,80.27,91.524,80.359,91.688,80.419,91.897,80.478,92.247,80.499,92.578,80.533,92.827,80.619,92.981,80.703,93.263,80.791,92.773,80.769,92.593,80.781,92.61,80.81,92.71,80.872,92.765,80.893,92.939,80.926,93.065,80.988,93.359,81.032,93.497,81.039,93.637,81.038,93.889,81.058,94.14,81.089,94.375,81.107,94.612,81.115,94.838,81.139,95.061,81.188,95.16,81.271,95.801,81.28,95.902,81.261,95.984,81.211,96.075,81.193,96.187,81.184,96.471,81.099,96.527,81.076]],[[97.675,80.158,97.904,80.095,98.018,80.023,97.907,80.004,97.808,79.956,97.76,79.896,97.627,79.85,97.591,79.775,97.652,79.761,97.725,79.781,97.871,79.853,98.065,79.901,98.273,79.874,98.353,79.884,98.499,79.953,98.472,80.009,98.532,80.044,98.596,80.052,98.866,80.045,99.295,80.016,99.371,79.986,99.473,79.97,99.536,79.941,99.727,79.92,99.818,79.898,99.947,79.849,100.061,79.777,99.916,79.738,99.839,79.669,99.805,79.653,99.782,79.628,99.771,79.568,99.749,79.515,99.721,79.492,99.706,79.463,99.722,79.385,99.681,79.323,99.537,79.277,99.388,79.275,99.167,79.306,99.104,79.305,99.042,79.293,99.317,79.227,99.517,79.13,99.751,79.108,99.815,79.096,99.9,79.006,99.929,78.961,99.541,78.853,99.44,78.834,98.82,78.818,98.411,78.788,98.283,78.795,98.054,78.821,97.905,78.81,97.689,78.827,97.555,78.827,97.248,78.868,96.933,78.934,96.871,78.964,96.808,78.985,96.43,79.003,96.347,79.016,95.796,79.001,95.703,79.012,95.531,79.098,95.437,79.099,95.133,79.05,95.02,79.053,94.791,79.087,94.652,79.127,94.632,79.141,94.62,79.192,94.482,79.219,94.314,79.308,94.219,79.402,93.759,79.451,93.479,79.463,93.272,79.458,93.071,79.495,93.405,79.632,93.847,79.702,94.038,79.756,94.257,79.83,94.347,79.942,94.719,80.011,94.815,80.035,94.947,80.089,94.987,80.097,95.281,80.031,95.338,80.042,95.391,80.073,95.498,80.106,95.858,80.11,96.163,80.097,96.277,80.11,96.417,80.104,97.121,80.153,97.587,80.168,97.675,80.158]],[[102.885,79.254,102.787,79.176,102.746,79.106,102.448,78.877,102.412,78.835,102.587,78.871,102.748,78.95,102.845,79.014,102.95,79.056,103.076,79.056,103.199,79.071,103.433,79.126,103.673,79.15,103.801,79.149,103.926,79.123,104.004,79.063,104.091,79.013,104.404,78.977,104.449,78.964,104.477,78.923,104.452,78.88,104.633,78.835,104.881,78.855,105.015,78.843,105.146,78.819,105.205,78.78,105.256,78.733,105.31,78.666,105.343,78.594,105.313,78.5,104.833,78.353,104.742,78.34,104.519,78.349,104.297,78.335,103.719,78.258,103.003,78.256,102.797,78.188,102.734,78.19,102.673,78.202,102.617,78.225,102.18,78.205,101.692,78.194,101.204,78.192,101.04,78.143,100.541,78.048,100.082,77.975,99.845,77.957,99.5,77.976,99.392,78.001,99.287,78.038,99.439,78.084,99.546,78.179,99.678,78.233,100.019,78.339,100.058,78.38,100.124,78.47,100.163,78.504,100.215,78.536,100.257,78.574,100.263,78.631,100.284,78.679,100.416,78.753,100.516,78.788,100.62,78.797,100.876,78.784,100.956,78.788,100.898,78.812,100.856,78.898,100.865,78.926,100.901,78.98,100.965,79.007,101.031,79.023,101.068,79.096,101.052,79.123,101.149,79.157,101.196,79.204,101.31,79.233,101.543,79.254,101.555,79.313,101.591,79.35,101.643,79.361,101.761,79.372,101.824,79.37,101.912,79.312,102.005,79.264,102.129,79.252,102.251,79.256,102.177,79.313,102.181,79.373,102.225,79.413,102.282,79.43,102.405,79.433,102.79,79.392,103.042,79.332,103.098,79.299,103.052,79.283,102.94,79.271,102.885,79.254]],[[140.049,75.829,140.152,75.81,140.274,75.822,140.389,75.796,140.496,75.69,140.547,75.663,140.602,75.644,140.657,75.634,140.816,75.631,140.889,75.652,140.944,75.7,140.94,75.75,140.927,75.799,140.926,75.867,140.95,75.927,140.985,75.965,141.033,75.989,141.299,76.064,141.485,76.137,141.742,76.108,142.001,76.044,142.46,75.904,142.67,75.863,142.927,75.827,143.185,75.814,143.311,75.822,143.56,75.86,143.686,75.864,145.255,75.586,145.31,75.564,145.36,75.53,145.023,75.49,144.803,75.416,144.727,75.366,144.814,75.325,144.883,75.269,144.408,75.102,144.216,75.059,144.02,75.045,143.626,75.084,143.396,75.083,143.17,75.117,142.922,75.217,142.82,75.268,142.729,75.338,142.7,75.449,142.734,75.545,142.868,75.572,142.986,75.633,143.002,75.66,142.942,75.713,142.552,75.721,142.308,75.692,142.086,75.661,142.151,75.458,142.199,75.393,142.265,75.346,142.617,75.133,142.697,75.103,142.93,75.062,143.128,74.97,142.778,74.868,142.626,74.837,142.473,74.82,142.378,74.829,142.287,74.85,142.184,74.9,142.1,74.951,141.987,74.991,141.748,74.983,141.53,74.947,141.31,74.923,140.661,74.882,140.464,74.856,140.268,74.847,140.011,74.895,139.758,74.964,139.681,74.964,139.606,74.946,139.548,74.904,139.512,74.838,139.43,74.749,139.326,74.687,139.215,74.66,139.099,74.657,138.982,74.674,138.866,74.701,138.092,74.797,138.001,74.827,137.915,74.871,137.683,75.009,137.568,75.041,137.447,75.054,137.218,75.124,137.006,75.235,136.962,75.27,136.948,75.326,136.982,75.365,137.166,75.347,137.29,75.349,137.215,75.554,137.269,75.749,137.358,75.782,137.707,75.76,137.594,75.823,137.501,75.91,137.561,75.955,137.625,75.988,137.774,76.016,137.977,76.028,138.039,76.047,138.096,76.081,138.208,76.115,138.431,76.13,138.814,76.2,138.92,76.197,139.018,76.16,139.109,76.108,139.211,76.081,139.529,76.013,139.743,75.953,140.049,75.829]],[[146.795,75.371,147.06,75.364,147.444,75.438,147.497,75.441,148.432,75.414,148.509,75.387,148.519,75.336,148.489,75.309,148.475,75.272,148.59,75.236,148.892,75.228,149.083,75.262,149.645,75.245,150.104,75.219,150.281,75.164,150.417,75.134,150.531,75.1,150.613,75.12,150.69,75.155,150.757,75.162,150.822,75.157,150.646,74.945,150.58,74.919,150.331,74.867,149.838,74.795,149.597,74.773,149.05,74.772,148.297,74.8,148.092,74.826,147.972,74.857,147.741,74.932,147.627,74.959,147.257,74.984,147.144,74.998,146.925,75.063,146.703,75.114,146.149,75.198,146.186,75.296,146.258,75.394,146.343,75.481,146.438,75.558,146.538,75.582,146.751,75.51,146.748,75.429,146.795,75.371]],[[178.862,70.826,178.793,70.822,178.648,71.001,178.628,71.047,178.684,71.106,178.829,71.178,178.891,71.231,179.235,71.325,179.548,71.448,179.716,71.466,179.886,71.523,180,71.538,180,70.993,179.881,70.976,179.648,70.899,179.153,70.88,178.862,70.826]],[[142.761,54.394,142.976,54.141,142.986,54.086,142.967,54.029,142.927,53.956,142.911,53.878,142.936,53.811,142.918,53.794,143.096,53.489,143.224,53.296,143.26,53.217,143.288,53.134,143.325,52.963,143.333,52.7,143.324,52.614,143.295,52.529,143.264,52.479,143.201,52.443,143.172,52.349,143.156,52.084,143.191,51.944,143.251,51.848,143.295,51.744,143.3,51.632,143.321,51.583,143.418,51.521,143.455,51.471,143.467,51.402,143.473,51.299,143.489,51.277,143.534,51.246,143.736,50.507,143.816,50.283,144.048,49.896,144.141,49.661,144.2,49.55,144.24,49.432,144.272,49.311,144.341,49.181,144.432,49.051,144.607,48.936,144.686,48.871,144.707,48.82,144.714,48.64,144.673,48.679,144.621,48.815,144.536,48.894,144.412,48.986,144.284,49.07,144.125,49.209,144.049,49.249,143.968,49.276,143.819,49.309,143.732,49.312,143.382,49.291,143.236,49.263,143.105,49.199,143.027,49.105,142.972,48.918,142.651,48.247,142.574,48.072,142.546,47.885,142.557,47.738,142.579,47.684,142.67,47.537,142.745,47.452,142.801,47.416,142.864,47.392,142.905,47.362,142.94,47.323,143.006,47.223,143.089,47.001,143.178,46.844,143.218,46.795,143.319,46.807,143.384,46.806,143.447,46.792,143.486,46.752,143.54,46.575,143.579,46.406,143.581,46.361,143.509,46.23,143.491,46.175,143.482,46.116,143.463,46.069,143.432,46.029,143.419,46.222,143.37,46.358,143.352,46.476,143.282,46.559,143.048,46.593,142.829,46.605,142.796,46.62,142.747,46.671,142.692,46.711,142.636,46.716,142.578,46.701,142.479,46.644,142.406,46.555,142.35,46.459,142.304,46.358,142.209,46.089,142.15,45.999,142.077,45.917,142.016,45.962,141.962,46.013,141.93,46.088,141.916,46.171,141.83,46.451,141.867,46.694,142.011,47.03,142.039,47.14,142.017,47.245,141.984,47.348,141.963,47.544,141.964,47.587,142.016,47.701,142.076,47.808,142.149,47.902,142.182,48.013,142.135,48.29,142.029,48.477,141.897,48.655,141.873,48.702,141.866,48.75,141.98,48.972,142.02,49.078,142.067,49.312,142.109,49.44,142.142,49.569,142.153,50.217,142.143,50.312,142.071,50.515,142.066,50.63,142.1,50.776,142.147,50.89,142.208,50.998,142.207,51.223,142.091,51.429,142.006,51.521,141.873,51.63,141.772,51.69,141.722,51.736,141.772,51.752,141.808,51.789,141.721,51.847,141.668,51.933,141.661,52.273,141.682,52.359,141.748,52.455,141.803,52.556,141.856,52.794,141.874,53.039,141.839,53.138,141.824,53.34,141.852,53.389,141.964,53.456,142.142,53.496,142.18,53.484,142.319,53.405,142.371,53.403,142.424,53.411,142.526,53.447,142.583,53.537,142.509,53.588,142.553,53.653,142.68,53.674,142.689,53.73,142.643,53.737,142.683,53.816,142.706,53.896,142.67,53.968,142.467,54.149,142.335,54.281,142.552,54.279,142.616,54.304,142.666,54.358,142.693,54.416,142.761,54.394]],[[-178.876,71.577,-178.439,71.541,-178.354,71.529,-178.215,71.482,-178.134,71.465,-178.057,71.438,-177.975,71.391,-177.817,71.34,-177.584,71.282,-177.532,71.263,-177.498,71.219,-177.524,71.167,-177.822,71.068,-178.063,71.042,-178.528,71.015,-179.157,70.94,-179.416,70.919,-179.507,70.923,-179.734,70.972,-180,70.993,-180,71.184,-180,71.4,-180,71.538,-179.845,71.551,-179.691,71.578,-179.546,71.582,-179.402,71.567,-179.256,71.572,-179.112,71.596,-178.994,71.593,-178.876,71.577]],[[35.816,65.182,35.848,65.143,35.858,65.078,35.827,65.036,35.842,65.001,35.779,64.977,35.68,65.058,35.621,65.059,35.559,65.094,35.529,65.151,35.586,65.167,35.609,65.157,35.729,65.198,35.816,65.182]],[[42.714,66.702,42.676,66.688,42.477,66.735,42.46,66.77,42.469,66.786,42.547,66.796,42.631,66.782,42.691,66.735,42.714,66.702]],[[52.903,71.365,52.994,71.291,53.074,71.238,53.141,71.242,53.193,71.215,53.205,71.16,53.071,71.065,53.048,71.031,53.106,70.999,53.121,70.982,53.023,70.969,53.004,71.012,52.95,71.054,52.835,71.086,52.789,71.115,52.738,71.181,52.547,71.25,52.425,71.239,52.289,71.27,52.25,71.285,52.24,71.325,52.297,71.357,52.513,71.385,52.617,71.383,52.73,71.355,52.72,71.39,52.732,71.404,52.777,71.4,52.903,71.365]],[[168.039,54.565,168.081,54.513,167.677,54.698,167.488,54.795,167.442,54.856,167.512,54.857,167.592,54.798,167.711,54.77,167.883,54.69,168.039,54.565]],[[160.719,70.823,160.651,70.806,160.505,70.82,160.437,70.851,160.44,70.923,160.449,70.934,160.566,70.924,160.645,70.884,160.719,70.823]],[[161.467,68.901,161.422,68.9,161.456,68.966,161.461,68.996,161.364,69.044,161.183,69.082,161.137,69.11,161.125,69.197,161.165,69.334,161.083,69.406,161.111,69.47,161.323,69.541,161.41,69.596,161.505,69.639,161.521,69.634,161.618,69.592,161.609,69.501,161.54,69.437,161.374,69.414,161.351,69.369,161.373,69.293,161.378,69.194,161.394,69.106,161.495,69.016,161.517,68.97,161.507,68.928,161.467,68.901]],[[152.886,76.122,152.786,76.086,152.559,76.144,152.643,76.175,152.799,76.195,152.835,76.185,152.864,76.163,152.886,76.122]],[[149.15,76.66,148.399,76.648,148.448,76.677,148.72,76.747,149.406,76.782,149.268,76.747,149.205,76.677,149.15,76.66]],[[137.96,71.508,137.712,71.423,137.613,71.434,137.512,71.475,137.458,71.483,137.403,71.477,137.344,71.461,137.266,71.456,137.079,71.502,137.064,71.53,137.082,71.543,137.129,71.556,137.168,71.557,137.282,71.58,137.817,71.588,137.858,71.583,137.934,71.543,137.96,71.508]],[[107.696,78.131,107.606,78.083,107.482,78.058,107.344,78.099,107.002,78.096,106.416,78.14,106.583,78.168,107.508,78.189,107.573,78.186,107.696,78.131]],[[112.478,76.621,112.633,76.553,112.661,76.51,112.614,76.499,112.587,76.483,112.575,76.452,112.532,76.45,112.395,76.484,112.297,76.538,112.154,76.549,112.003,76.603,111.969,76.626,112.011,76.633,112.281,76.618,112.394,76.644,112.478,76.621]],[[96.532,76.278,96.614,76.264,96.59,76.221,96.487,76.234,96.351,76.212,96.353,76.177,96.301,76.122,96.109,76.155,95.845,76.16,95.679,76.194,95.311,76.215,95.322,76.262,95.38,76.289,95.594,76.25,95.786,76.294,96.151,76.272,96.271,76.305,96.532,76.278]],[[96.285,77.027,96.254,77.007,96.21,76.992,96.091,77.003,95.855,76.975,95.766,76.991,95.681,77.021,95.364,77.012,95.27,77.019,95.421,77.056,95.854,77.098,96.528,77.206,96.562,77.154,96.561,77.13,96.424,77.071,96.285,77.027]],[[89.514,77.189,89.3,77.184,89.179,77.21,89.142,77.227,89.2,77.272,89.282,77.301,89.616,77.311,89.68,77.28,89.666,77.254,89.514,77.189]],[[74.661,72.873,74.638,72.864,74.588,72.881,74.435,72.908,74.181,72.975,74.1,73.022,74.142,73.074,74.199,73.109,74.409,73.13,74.6,73.122,74.725,73.108,74.962,73.063,74.743,73.033,74.647,72.969,74.66,72.929,74.697,72.908,74.661,72.873]],[[75.504,73.457,75.344,73.432,75.375,73.477,75.57,73.541,75.93,73.574,76.039,73.56,76.052,73.549,75.901,73.481,75.827,73.459,75.504,73.457]],[[83.549,74.072,83.496,74.048,83.45,74.052,83.411,74.04,83.159,74.075,82.818,74.092,82.903,74.129,83.15,74.152,83.513,74.122,83.618,74.089,83.549,74.072]],[[67.345,69.53,67.264,69.443,67.098,69.447,67.047,69.467,67.026,69.483,67.216,69.575,67.329,69.572,67.345,69.53]],[[66.561,70.542,66.569,70.501,66.516,70.515,66.449,70.561,66.408,70.616,66.395,70.727,66.418,70.757,66.44,70.773,66.463,70.769,66.458,70.699,66.561,70.542]],[[70.021,66.502,69.845,66.49,69.651,66.565,69.469,66.716,69.503,66.751,69.616,66.739,69.8,66.736,69.918,66.712,70.077,66.696,70.058,66.627,70.057,66.599,70.11,66.569,70.059,66.518,70.021,66.502]],[[50.052,80.074,49.971,80.061,49.588,80.136,49.556,80.159,49.884,80.23,50.251,80.219,50.31,80.186,50.319,80.172,50.072,80.109,50.052,80.074]],[[51.409,79.944,51.435,79.932,51.431,79.921,51.076,79.932,50.454,79.924,50.091,79.981,50.473,80.035,50.676,80.049,50.936,80.094,51.254,80.049,51.238,80.01,51.243,79.991,51.327,79.972,51.409,79.944]],[[50.754,81.047,50.616,81.041,50.518,81.046,50.412,81.084,50.377,81.103,50.368,81.123,50.465,81.126,50.506,81.144,50.522,81.158,50.592,81.169,50.716,81.171,50.879,81.151,50.946,81.108,50.789,81.072,50.754,81.047]],[[55.48,80.274,55.195,80.227,55.048,80.228,54.98,80.256,55.092,80.296,55.24,80.325,55.353,80.318,55.435,80.302,55.48,80.274]],[[54.415,80.473,54.276,80.421,53.812,80.476,53.85,80.504,53.9,80.515,53.902,80.542,53.859,80.563,53.877,80.605,54.177,80.574,54.205,80.562,54.407,80.54,54.437,80.499,54.415,80.473]],[[59.689,79.956,59.331,79.923,59.203,79.933,59.169,79.948,59.1,79.964,58.919,79.985,58.946,80.042,59.001,80.054,59.545,80.119,59.802,80.083,59.911,79.994,59.689,79.956]],[[58.622,81.042,58.762,80.991,58.815,80.934,58.903,80.898,58.931,80.832,58.86,80.779,58.642,80.768,58.286,80.765,57.938,80.793,57.75,80.889,57.405,80.915,57.211,81.017,57.41,81.047,57.656,81.032,58.05,81.118,58.102,81.114,58.19,81.095,58.508,81.062,58.622,81.042]],[[47.983,45.488,47.968,45.47,47.92,45.562,47.918,45.618,47.947,45.647,47.987,45.554,47.983,45.488]],[[50.265,69.186,50.283,69.089,50.221,69.049,50.164,69.038,50.141,69.098,50.094,69.126,49.921,69.053,49.84,68.974,49.626,68.86,49.18,68.778,48.91,68.743,48.667,68.733,48.439,68.805,48.316,68.942,48.294,68.984,48.279,69.04,48.28,69.097,48.296,69.184,48.32,69.269,48.414,69.346,48.631,69.436,48.845,69.495,48.953,69.509,49.225,69.511,49.996,69.309,50.167,69.257,50.265,69.186]],[[63.374,80.7,63.188,80.698,63.002,80.713,62.76,80.763,62.52,80.822,62.593,80.853,62.819,80.894,63.116,80.967,63.615,80.981,63.856,80.981,64.096,80.998,64.166,81.036,64.21,81.106,64.256,81.144,64.31,81.175,64.575,81.198,64.802,81.197,65.028,81.169,65.172,81.144,65.31,81.096,65.382,81.057,65.36,81.008,65.372,80.968,65.437,80.931,64.997,80.819,64.548,80.755,63.374,80.7]],[[57.956,80.123,57.8,80.104,57.392,80.139,57.332,80.158,57.281,80.194,57.214,80.328,57.212,80.368,57.186,80.396,57.083,80.445,57.011,80.468,57.075,80.494,57.522,80.475,58.48,80.465,58.972,80.416,59.116,80.388,59.255,80.343,58.398,80.319,58.284,80.298,58.286,80.248,58.255,80.202,58.163,80.197,57.956,80.123]],[[62.168,80.835,62.228,80.794,62.192,80.73,62.115,80.684,62.076,80.617,61.769,80.601,61.681,80.586,61.597,80.535,61.285,80.505,61.051,80.419,60.722,80.435,60.278,80.494,59.9,80.446,59.65,80.431,59.346,80.505,59.304,80.522,59.288,80.573,59.306,80.618,59.387,80.713,59.495,80.767,59.549,80.784,59.592,80.817,59.716,80.836,60.095,80.849,60.235,80.838,60.278,80.801,60.482,80.804,60.82,80.827,61.313,80.863,61.597,80.893,61.851,80.886,62.103,80.867,62.168,80.835]],[[61.141,80.95,60.827,80.93,60.321,80.956,60.058,80.985,60.078,80.999,60.148,81.017,60.587,81.088,61.457,81.104,61.567,81.05,61.472,81.011,61.141,80.95]],[[53.521,80.185,52.856,80.173,52.636,80.179,52.607,80.191,52.55,80.202,52.344,80.213,52.213,80.264,52.27,80.276,52.577,80.297,52.681,80.319,52.716,80.348,52.854,80.402,53.186,80.413,53.329,80.402,53.346,80.366,53.486,80.323,53.852,80.268,53.778,80.228,53.653,80.223,53.521,80.185]],[[57.079,80.351,57.123,80.317,57.119,80.194,57.073,80.139,57.08,80.095,56.987,80.071,56.201,80.076,55.812,80.087,55.724,80.105,55.942,80.163,56.012,80.204,55.99,80.32,56.024,80.341,56.655,80.33,56.707,80.363,56.945,80.366,57.079,80.351]],[[57.81,81.546,57.863,81.506,58.017,81.484,58.436,81.464,58.564,81.418,58.372,81.387,57.859,81.368,57.912,81.303,58.015,81.255,57.913,81.198,57.77,81.17,57.451,81.136,57.159,81.178,56.822,81.238,56.669,81.198,56.513,81.175,56.364,81.179,56.192,81.224,55.717,81.188,55.573,81.228,55.466,81.311,55.782,81.329,56.157,81.303,56.405,81.387,56.719,81.423,56.973,81.511,57.092,81.541,57.365,81.535,57.456,81.543,57.717,81.565,57.81,81.546]],[[54.719,81.116,55.471,81.02,56.17,81.029,56.472,80.998,56.91,80.913,57.568,80.82,57.694,80.792,57.58,80.755,56.815,80.664,56.316,80.633,55.883,80.628,55.713,80.637,55.541,80.703,55.117,80.752,54.668,80.739,54.623,80.765,54.533,80.783,54.376,80.787,54.067,80.814,54.045,80.872,54.241,80.902,54.367,80.904,54.417,80.987,54.634,81.113,54.719,81.116]],[[63.651,81.609,63.529,81.597,62.885,81.609,62.573,81.633,62.531,81.647,62.515,81.659,62.106,81.679,62.284,81.707,62.795,81.719,63.71,81.687,63.767,81.664,63.782,81.65,63.651,81.609]],[[58.295,81.715,57.965,81.696,57.921,81.71,57.909,81.722,57.945,81.748,57.985,81.797,58.135,81.828,59.262,81.854,59.408,81.825,59.357,81.781,59.356,81.759,58.295,81.715]],[[92.683,79.685,92.441,79.675,92.154,79.685,91.684,79.791,91.376,79.835,91.126,79.905,91.07,79.981,91.229,80.031,91.426,80.049,91.752,80.052,92.173,80.045,92.593,79.997,93.482,79.941,93.803,79.905,93.604,79.817,93.382,79.784,93.155,79.738,92.926,79.704,92.683,79.685]],[[91.567,81.141,91.223,81.064,89.976,81.113,89.919,81.149,89.901,81.171,90.07,81.214,91.109,81.199,91.478,81.184,91.567,81.141]],[[141.01,73.999,140.507,73.919,140.409,73.922,140.183,74.005,140.102,74.184,140.194,74.237,140.3,74.257,140.407,74.266,140.849,74.274,140.944,74.265,141.039,74.243,141.079,74.209,141.097,74.168,141.047,74.05,141.01,73.999]],[[142.185,73.896,142.435,73.852,142.639,73.803,143.344,73.569,143.411,73.521,143.464,73.459,143.491,73.246,143.451,73.231,143.193,73.221,142.842,73.245,142.587,73.253,142.342,73.253,142.126,73.282,141.597,73.311,141.183,73.389,140.754,73.446,140.663,73.452,140.392,73.435,140.027,73.361,139.925,73.355,139.786,73.355,139.686,73.426,139.92,73.449,140.155,73.458,140.381,73.483,140.594,73.565,140.697,73.629,140.884,73.778,140.984,73.832,141.085,73.866,141.19,73.876,141.312,73.872,141.682,73.904,141.932,73.915,142.185,73.896]],[[135.949,75.41,135.746,75.382,135.452,75.39,135.473,75.463,135.523,75.496,135.593,75.576,135.561,75.636,135.578,75.71,135.614,75.766,135.699,75.845,135.788,75.798,135.849,75.729,135.905,75.694,136.127,75.626,136.169,75.606,135.983,75.522,135.965,75.486,136.021,75.438,135.949,75.41]],[[136.197,73.914,136.122,73.885,136.051,73.929,135.715,74.06,135.633,74.121,135.449,74.18,135.402,74.202,135.387,74.253,135.628,74.22,136.037,74.09,136.259,73.985,136.197,73.914]],[[137.941,55.093,138.031,55.053,138.172,55.06,138.206,55.034,138.096,54.991,138.017,54.901,137.991,54.821,137.959,54.789,137.87,54.75,137.79,54.697,137.721,54.663,137.661,54.653,137.526,54.826,137.463,54.873,137.276,54.792,137.233,54.791,137.275,54.891,137.384,55.001,137.436,55.016,137.544,55.163,137.577,55.197,137.91,55.11,137.941,55.093]],[[169.201,69.58,168.916,69.571,168.348,69.664,168.144,69.713,167.993,69.776,167.821,69.82,167.789,69.837,167.814,69.873,167.865,69.901,168.06,69.975,168.196,70.008,168.358,70.016,169.375,69.883,169.421,69.856,169.434,69.832,169.418,69.779,169.332,69.77,169.299,69.735,169.263,69.629,169.246,69.601,169.201,69.58]],[[163.635,58.603,163.471,58.509,163.447,58.525,163.432,58.546,163.427,58.579,163.577,58.641,163.727,58.799,163.784,58.93,163.767,58.972,163.761,59.015,164.202,59.096,164.517,59.227,164.573,59.221,164.629,59.112,164.662,58.971,164.616,58.886,164.279,58.838,163.96,58.744,163.635,58.603]],[[166.65,54.839,166.645,54.694,166.521,54.768,166.464,54.827,166.382,54.838,166.325,54.865,166.23,54.937,166.12,55.03,166.082,55.077,166.066,55.136,165.992,55.19,165.751,55.295,165.83,55.307,165.931,55.351,166.212,55.324,166.276,55.312,166.23,55.242,166.248,55.165,166.404,55.006,166.479,54.95,166.577,54.908,166.65,54.839]],[[154.81,49.312,154.715,49.268,154.611,49.294,154.613,49.381,154.825,49.647,154.9,49.63,154.883,49.566,154.802,49.468,154.83,49.348,154.81,49.312]],[[156.405,50.658,156.365,50.634,156.326,50.639,156.196,50.702,156.168,50.732,156.213,50.785,156.376,50.862,156.456,50.86,156.488,50.843,156.483,50.751,156.405,50.658]],[[155.921,50.302,155.792,50.202,155.608,50.177,155.516,50.146,155.449,50.078,155.397,50.041,155.289,50.061,155.243,50.095,155.243,50.213,155.195,50.265,155.218,50.298,155.327,50.293,155.434,50.369,155.68,50.401,155.773,50.482,155.885,50.684,156.002,50.757,156.097,50.772,156.123,50.671,156.101,50.559,156.044,50.452,155.921,50.302]],[[154.081,48.79,154.043,48.739,154.001,48.756,153.992,48.773,154.092,48.832,154.126,48.904,154.199,48.905,154.228,48.892,154.205,48.857,154.081,48.79]],[[155.645,50.822,155.554,50.811,155.513,50.837,155.483,50.87,155.467,50.914,155.569,50.934,155.64,50.91,155.654,50.845,155.645,50.822]],[[153.101,47.763,153.054,47.706,153.004,47.713,152.984,47.728,153.049,47.797,153.079,47.809,153.101,47.763]],[[152.002,46.897,151.816,46.787,151.754,46.788,151.723,46.829,151.715,46.853,151.864,46.869,152.04,47.015,152.166,47.11,152.235,47.143,152.289,47.142,152.002,46.897]],[[149.688,45.642,149.539,45.591,149.447,45.593,149.666,45.84,149.796,45.876,149.962,46.022,150.309,46.2,150.349,46.213,150.553,46.209,150.235,46.012,150.195,45.933,150.057,45.849,149.954,45.822,149.883,45.783,149.688,45.642]],[[148.6,45.318,148.415,45.247,148.262,45.217,148.005,45.07,147.914,44.99,147.784,44.959,147.658,44.977,147.622,44.945,147.61,44.887,147.563,44.836,147.31,44.678,147.207,44.554,147.098,44.531,146.897,44.404,146.933,44.513,146.974,44.566,147.141,44.663,147.155,44.766,147.247,44.856,147.43,44.945,147.558,45.062,147.658,45.093,147.769,45.191,147.886,45.226,147.873,45.3,147.924,45.383,147.965,45.378,148.056,45.262,148.13,45.258,148.324,45.282,148.612,45.485,148.707,45.521,148.773,45.526,148.812,45.51,148.826,45.486,148.825,45.456,148.803,45.414,148.837,45.363,148.791,45.324,148.6,45.318]],[[146.714,43.744,146.683,43.716,146.609,43.74,146.613,43.797,146.622,43.813,146.825,43.86,146.884,43.829,146.899,43.804,146.714,43.744]],[[146.208,44.498,146.356,44.425,146.568,44.44,146.516,44.375,146.437,44.376,146.296,44.281,146.173,44.269,146.112,44.246,145.914,44.104,145.887,44.048,145.767,43.941,145.587,43.845,145.556,43.665,145.439,43.737,145.426,43.81,145.462,43.871,145.666,43.999,145.748,44.072,145.773,44.129,145.852,44.193,145.89,44.249,145.94,44.273,146.112,44.5,146.208,44.498]],[[113.387,74.4,113.353,74.353,113.299,74.317,113.259,74.273,113.19,74.239,112.978,74.197,112.811,74.103,112.782,74.095,112.196,74.146,112.105,74.163,111.912,74.219,111.643,74.273,111.503,74.353,111.57,74.368,111.638,74.374,111.88,74.364,111.949,74.389,111.983,74.456,111.989,74.496,112.008,74.527,112.084,74.549,112.952,74.48,113.286,74.441,113.387,74.4]],[[76.249,79.651,76.373,79.615,76.467,79.643,77.36,79.557,77.549,79.524,77.589,79.502,76.81,79.49,76.65,79.493,76.637,79.544,76.458,79.545,76.154,79.579,76.072,79.626,76.052,79.645,76.148,79.664,76.249,79.651]],[[80.027,80.848,79.099,80.812,79.007,80.835,78.978,80.848,79.11,80.924,79.217,80.96,79.807,80.975,80.28,80.95,80.428,80.928,80.373,80.883,80.345,80.868,80.027,80.848]],[[70.674,73.095,70.38,73.048,70.298,73.044,70.119,73.056,70.041,73.037,69.92,73.085,69.93,73.127,69.986,73.169,70.019,73.224,69.996,73.359,70.15,73.445,70.35,73.478,70.94,73.514,71.023,73.504,71.141,73.478,71.232,73.448,71.351,73.372,71.445,73.342,71.59,73.283,71.63,73.225,71.626,73.174,71.356,73.162,70.887,73.12,70.674,73.095]],[[77.633,72.291,77.146,72.282,76.906,72.298,76.871,72.317,76.903,72.366,77.15,72.439,77.26,72.486,77.378,72.565,77.579,72.631,77.749,72.631,78.279,72.553,78.353,72.504,78.365,72.482,78.154,72.417,78.007,72.392,77.781,72.309,77.633,72.291]],[[79.501,72.722,79.431,72.711,78.881,72.752,78.69,72.803,78.633,72.851,78.657,72.892,79.164,73.094,79.357,73.039,79.413,72.983,79.541,72.919,79.538,72.769,79.501,72.722]],[[82.172,75.419,82.209,75.387,82.222,75.351,82.179,75.339,82.05,75.341,81.979,75.247,81.905,75.263,81.861,75.317,81.698,75.281,81.655,75.289,81.579,75.331,81.532,75.34,81.501,75.368,81.712,75.451,81.842,75.407,81.927,75.41,81.91,75.46,81.913,75.498,82.022,75.513,82.166,75.516,82.172,75.419]],[[60.45,69.935,60.481,69.885,60.477,69.794,60.44,69.726,60.327,69.715,60.216,69.688,60.026,69.717,59.92,69.697,59.813,69.696,59.725,69.706,59.637,69.721,59.578,69.739,59.581,69.791,59.503,69.866,59.382,69.89,59.268,69.898,59.144,69.922,59.083,69.911,59.004,69.883,58.953,69.893,58.68,70.051,58.634,70.088,58.606,70.129,58.568,70.156,58.473,70.267,58.52,70.318,58.615,70.351,58.678,70.36,58.794,70.433,59.005,70.465,59.048,70.46,59.088,70.437,59.31,70.362,59.426,70.311,59.529,70.249,59.636,70.197,59.956,70.108,60.172,70.023,60.393,69.962,60.45,69.935]],[[20.958,55.279,20.859,55.184,20.595,54.982,20.678,54.956,20.774,54.947,20.888,54.909,20.996,54.903,21.189,54.935,21.223,55.108,21.236,55.264,21.298,55.264,21.389,55.276,21.447,55.234,21.555,55.195,21.683,55.16,21.874,55.101,22.072,55.064,22.138,55.059,22.346,55.064,22.567,55.059,22.627,54.971,22.737,54.929,22.825,54.871,22.831,54.838,22.71,54.633,22.684,54.563,22.68,54.493,22.724,54.406,22.766,54.357,22.732,54.35,22.168,54.36,21.634,54.376,21.141,54.392,20.665,54.407,20.208,54.421,19.924,54.434,19.644,54.447,19.604,54.459,19.758,54.545,19.859,54.634,19.944,54.75,19.953,54.83,19.975,54.921,20.108,54.956,20.397,54.951,20.52,54.995,20.679,55.103,20.846,55.232,20.9,55.287,20.958,55.279]],[[33.594,46.096,33.654,46.146,33.66,46.22,33.807,46.208,34.027,46.107,34.128,46.09,34.224,46.101,34.354,46.062,34.45,45.966,34.523,45.977,34.687,45.977,34.794,45.892,34.8,45.791,34.946,45.729,35.002,45.733,35.023,45.701,35.26,45.447,35.374,45.354,35.458,45.316,35.558,45.311,35.751,45.389,35.833,45.402,36.013,45.372,36.077,45.424,36.171,45.453,36.29,45.457,36.427,45.433,36.575,45.394,36.514,45.304,36.451,45.232,36.428,45.153,36.393,45.065,36.23,45.026,36.055,45.031,35.87,45.005,35.804,45.04,35.759,45.071,35.678,45.102,35.57,45.119,35.473,45.098,35.358,44.978,35.155,44.896,35.088,44.803,34.888,44.824,34.717,44.807,34.47,44.722,34.282,44.538,34.074,44.424,33.91,44.388,33.756,44.399,33.656,44.433,33.451,44.554,33.463,44.597,33.491,44.619,33.53,44.681,33.612,44.908,33.601,44.981,33.555,45.098,33.392,45.188,33.262,45.171,33.187,45.195,32.919,45.348,32.773,45.359,32.611,45.328,32.552,45.35,32.508,45.404,32.828,45.593,33.142,45.749,33.28,45.765,33.466,45.838,33.665,45.947,33.637,46.033,33.594,46.096]]]},{"n":"Romania","c":"RO","r":3,"ml":3,"lx":24.97,"ly":45.73,"b":[20.242,43.671,29.706,48.263],"p":[[[28.213,45.45,28.318,45.347,28.451,45.292,28.761,45.234,28.788,45.241,28.791,45.252,28.77,45.267,28.767,45.286,28.782,45.31,28.824,45.311,28.894,45.29,29.027,45.321,29.224,45.403,29.404,45.42,29.568,45.371,29.652,45.314,29.706,45.26,29.689,45.193,29.679,45.152,29.635,44.98,29.605,44.915,29.558,44.843,29.048,44.758,29.081,44.799,29.069,44.871,29.048,44.926,29.095,44.975,28.981,44.993,28.931,44.966,28.892,44.919,28.926,44.81,28.87,44.75,28.849,44.716,28.846,44.637,28.814,44.602,28.807,44.565,28.888,44.575,28.852,44.506,28.699,44.374,28.645,44.296,28.659,43.984,28.591,43.797,28.585,43.742,28.423,43.74,28.375,43.745,28.222,43.773,28.05,43.822,27.949,43.919,27.884,43.987,27.739,43.956,27.711,43.965,27.671,43.998,27.561,44.02,27.425,44.021,27.121,44.146,27.087,44.167,26.848,44.146,26.489,44.084,26.216,44.007,25.933,43.871,25.819,43.767,25.686,43.712,25.497,43.671,25.16,43.686,24.808,43.738,24.431,43.794,24.227,43.763,23.951,43.787,23.535,43.854,23.225,43.874,22.919,43.834,22.868,43.865,22.856,43.899,22.868,43.948,22.911,43.987,22.985,44.017,23.024,44.047,23.029,44.078,22.945,44.127,22.775,44.195,22.705,44.238,22.688,44.248,22.683,44.286,22.648,44.316,22.582,44.338,22.531,44.378,22.495,44.435,22.502,44.49,22.554,44.54,22.62,44.562,22.701,44.556,22.734,44.57,22.721,44.606,22.642,44.651,22.498,44.706,22.351,44.676,22.201,44.561,22.093,44.542,22.027,44.62,21.909,44.666,21.74,44.681,21.636,44.71,21.597,44.755,21.523,44.79,21.36,44.827,21.358,44.862,21.384,44.87,21.442,44.873,21.52,44.881,21.532,44.901,21.533,44.919,21.472,44.942,21.41,44.958,21.378,44.973,21.357,44.991,21.353,45.009,21.371,45.021,21.396,45.022,21.421,45.033,21.434,45.075,21.468,45.11,21.492,45.122,21.49,45.148,21.465,45.172,21.431,45.193,21.382,45.205,21.226,45.241,21.148,45.292,21.1,45.294,21.024,45.322,20.942,45.365,20.871,45.428,20.794,45.468,20.774,45.484,20.772,45.5,20.787,45.517,20.786,45.536,20.766,45.597,20.779,45.662,20.776,45.723,20.775,45.75,20.76,45.758,20.747,45.749,20.728,45.737,20.709,45.735,20.653,45.779,20.581,45.869,20.533,45.9,20.438,45.941,20.359,45.975,20.301,46.051,20.242,46.109,20.281,46.133,20.508,46.167,20.614,46.133,20.661,46.146,20.707,46.173,20.733,46.194,20.737,46.217,20.76,46.246,20.837,46.26,21.04,46.242,21.122,46.282,21.152,46.304,21.17,46.353,21.192,46.392,21.265,46.412,21.263,46.448,21.252,46.486,21.295,46.572,21.32,46.608,21.361,46.621,21.411,46.648,21.497,46.704,21.477,46.753,21.494,46.79,21.584,46.878,21.653,46.964,21.651,47.007,21.661,47.044,21.722,47.085,21.785,47.138,21.869,47.305,21.899,47.333,21.954,47.364,21.995,47.396,22,47.505,22.038,47.537,22.112,47.572,22.185,47.629,22.245,47.696,22.291,47.728,22.351,47.736,22.417,47.763,22.491,47.773,22.563,47.76,22.608,47.766,22.677,47.799,22.852,47.923,22.877,47.947,22.913,47.964,23.055,48.007,23.091,48.049,23.139,48.087,23.203,48.085,23.408,47.99,23.629,47.996,23.669,47.992,23.682,47.99,23.709,47.983,24.002,47.936,24.047,47.941,24.06,47.945,24.178,47.906,24.282,47.911,24.381,47.938,24.484,47.947,24.579,47.931,24.651,47.877,24.838,47.761,24.893,47.718,24.979,47.724,25.074,47.746,25.17,47.823,25.464,47.911,25.689,47.932,25.909,47.968,26.163,47.993,26.236,48.064,26.277,48.113,26.306,48.204,26.442,48.23,26.572,48.248,26.619,48.26,26.714,48.263,26.787,48.256,26.901,48.211,26.981,48.155,27.012,48.11,27.08,48.048,27.152,47.959,27.231,47.842,27.248,47.782,27.278,47.718,27.337,47.64,27.449,47.553,27.465,47.537,27.516,47.476,27.614,47.341,27.696,47.286,27.768,47.228,27.802,47.168,27.854,47.115,27.974,47.043,28.072,46.978,28.15,46.792,28.205,46.706,28.239,46.641,28.223,46.508,28.244,46.451,28.2,46.348,28.119,46.139,28.1,45.973,28.114,45.883,28.116,45.826,28.135,45.789,28.156,45.713,28.16,45.647,28.131,45.628,28.09,45.613,28.075,45.599,28.112,45.569,28.163,45.514,28.213,45.45]]]},{"n":"Qatar","c":"QA","r":5,"ml":4,"lx":51.14,"ly":25.24,"b":[50.755,24.565,51.609,26.153],"p":[[[51.268,24.607,51.178,24.587,51.093,24.565,51.023,24.565,50.966,24.574,50.928,24.595,50.856,24.68,50.804,24.789,50.836,24.85,50.847,24.889,50.777,25.177,50.755,25.399,50.763,25.445,50.803,25.497,50.869,25.613,50.904,25.724,51.003,25.981,51.108,26.081,51.262,26.153,51.389,26.011,51.543,25.902,51.572,25.781,51.527,25.682,51.485,25.525,51.51,25.452,51.52,25.39,51.561,25.284,51.602,25.148,51.609,25.053,51.587,24.965,51.533,24.891,51.428,24.668,51.396,24.645,51.268,24.607]]]},{"n":"Portugal","c":"PT","r":2,"ml":3,"lx":-8.27,"ly":39.61,"b":[-31.283,32.648,-6.212,42.137],"p":[[[-17.191,32.869,-17.054,32.816,-16.929,32.841,-16.774,32.774,-16.693,32.758,-16.765,32.71,-16.837,32.648,-17.018,32.663,-17.171,32.722,-17.226,32.767,-17.241,32.807,-17.191,32.869]],[[-8.777,41.941,-8.683,42.008,-8.59,42.053,-8.538,42.069,-8.323,42.115,-8.266,42.137,-8.213,42.134,-8.204,42.112,-8.174,42.069,-8.139,42.04,-8.13,42.018,-8.213,41.927,-8.225,41.896,-8.181,41.837,-8.174,41.82,-8.152,41.812,-8.094,41.814,-7.991,41.852,-7.921,41.884,-7.896,41.871,-7.693,41.888,-7.645,41.874,-7.613,41.858,-7.513,41.836,-7.404,41.834,-7.269,41.864,-7.21,41.895,-7.198,41.929,-7.195,41.955,-7.178,41.972,-7.147,41.981,-7.099,41.964,-7.03,41.951,-6.866,41.945,-6.833,41.964,-6.777,41.958,-6.704,41.935,-6.618,41.942,-6.575,41.913,-6.558,41.874,-6.553,41.79,-6.559,41.704,-6.542,41.673,-6.485,41.664,-6.392,41.665,-6.308,41.642,-6.243,41.602,-6.222,41.56,-6.212,41.532,-6.244,41.516,-6.289,41.455,-6.403,41.375,-6.566,41.304,-6.69,41.215,-6.776,41.108,-6.883,41.062,-6.916,41.038,-6.928,41.009,-6.858,40.878,-6.836,40.777,-6.818,40.654,-6.83,40.619,-6.836,40.483,-6.852,40.443,-6.848,40.411,-6.822,40.376,-6.81,40.343,-6.859,40.301,-6.948,40.252,-7.015,40.208,-7.033,40.168,-7.028,40.143,-6.916,40.057,-6.896,40.022,-6.911,39.937,-6.975,39.798,-7.037,39.714,-7.047,39.706,-7.118,39.682,-7.454,39.681,-7.536,39.662,-7.524,39.645,-7.445,39.536,-7.363,39.478,-7.335,39.465,-7.306,39.338,-7.172,39.135,-7.043,39.107,-6.998,39.056,-7.006,38.985,-7.046,38.907,-7.125,38.827,-7.22,38.771,-7.282,38.715,-7.286,38.649,-7.306,38.567,-7.336,38.501,-7.343,38.457,-7.106,38.181,-6.975,38.194,-6.958,38.188,-6.981,38.122,-7.023,38.045,-7.073,38.03,-7.185,38.006,-7.292,37.906,-7.379,37.786,-7.444,37.728,-7.504,37.585,-7.496,37.524,-7.467,37.428,-7.406,37.179,-7.494,37.168,-7.834,37.006,-7.94,37.005,-8.137,37.077,-8.484,37.1,-8.598,37.121,-8.739,37.075,-8.848,37.076,-8.935,37.016,-8.998,37.032,-8.926,37.166,-8.814,37.431,-8.819,37.592,-8.792,37.733,-8.823,37.872,-8.879,37.959,-8.802,38.184,-8.811,38.3,-8.881,38.447,-8.668,38.424,-8.734,38.482,-8.799,38.518,-8.862,38.51,-8.915,38.512,-9.096,38.455,-9.187,38.438,-9.213,38.448,-9.203,38.539,-9.25,38.657,-9.178,38.688,-9.093,38.697,-9.021,38.747,-8.977,38.803,-9,38.903,-8.938,38.998,-8.792,39.078,-8.867,39.066,-8.954,39.016,-9.091,38.835,-9.136,38.743,-9.252,38.713,-9.357,38.698,-9.41,38.708,-9.474,38.731,-9.48,38.799,-9.475,38.853,-9.431,38.96,-9.414,39.112,-9.353,39.248,-9.357,39.284,-9.375,39.338,-9.32,39.391,-9.251,39.426,-9.148,39.543,-9.004,39.821,-8.838,40.116,-8.851,40.152,-8.887,40.179,-8.873,40.259,-8.772,40.606,-8.732,40.651,-8.685,40.753,-8.674,40.917,-8.656,41.029,-8.66,41.086,-8.675,41.154,-8.738,41.285,-8.806,41.56,-8.811,41.652,-8.755,41.698,-8.846,41.705,-8.888,41.765,-8.878,41.832,-8.777,41.941]],[[-25.027,36.96,-25.032,36.942,-25.088,36.949,-25.16,36.943,-25.198,36.997,-25.164,37.019,-25.083,37.024,-25.044,37,-25.027,36.96]],[[-31.137,39.407,-31.181,39.359,-31.258,39.376,-31.283,39.394,-31.261,39.497,-31.2,39.521,-31.139,39.479,-31.137,39.407]],[[-27.075,38.643,-27.095,38.634,-27.303,38.661,-27.362,38.698,-27.386,38.766,-27.351,38.789,-27.26,38.803,-27.127,38.79,-27.042,38.741,-27.042,38.679,-27.075,38.643]],[[-27.778,38.556,-27.826,38.544,-28.092,38.621,-28.187,38.655,-28.311,38.744,-27.963,38.636,-27.778,38.556]],[[-28.641,38.525,-28.744,38.522,-28.842,38.598,-28.698,38.638,-28.655,38.614,-28.624,38.586,-28.606,38.551,-28.641,38.525]],[[-28.147,38.453,-28.065,38.413,-28.19,38.404,-28.231,38.385,-28.332,38.413,-28.454,38.409,-28.531,38.463,-28.549,38.519,-28.51,38.553,-28.402,38.553,-28.147,38.453]],[[-25.649,37.841,-25.585,37.834,-25.267,37.849,-25.182,37.838,-25.191,37.764,-25.251,37.735,-25.439,37.715,-25.734,37.763,-25.834,37.826,-25.848,37.872,-25.846,37.894,-25.784,37.911,-25.649,37.841]]]},{"n":"Poland","c":"PL","r":3,"ml":2.5,"lx":19.49,"ly":51.99,"b":[14.129,49.021,24.106,54.838],"p":[[[23.605,51.518,23.659,51.449,23.68,51.395,23.658,51.352,23.664,51.31,23.712,51.265,23.863,51.126,23.938,50.993,23.986,50.94,24.096,50.873,24.106,50.845,24.062,50.82,24.026,50.816,23.997,50.809,23.978,50.786,24.007,50.76,24.046,50.723,24.095,50.617,24.09,50.53,24.053,50.508,24.005,50.457,23.973,50.41,23.712,50.377,23.649,50.327,23.506,50.23,23.409,50.174,23.264,50.073,23.036,49.899,22.952,49.826,22.891,49.766,22.706,49.606,22.649,49.539,22.661,49.484,22.72,49.354,22.732,49.295,22.722,49.241,22.702,49.193,22.706,49.171,22.76,49.136,22.847,49.081,22.852,49.063,22.84,49.039,22.81,49.021,22.701,49.04,22.58,49.077,22.539,49.073,22.473,49.081,22.203,49.153,22.02,49.21,22.002,49.246,21.968,49.299,21.89,49.343,21.712,49.382,21.64,49.412,21.35,49.429,21.225,49.429,21.136,49.417,21.079,49.418,21.001,49.34,20.947,49.317,20.868,49.315,20.8,49.329,20.729,49.37,20.616,49.392,20.535,49.381,20.475,49.39,20.423,49.392,20.405,49.384,20.363,49.385,20.303,49.366,20.237,49.338,20.164,49.316,20.108,49.271,20.058,49.181,19.916,49.221,19.869,49.204,19.802,49.192,19.757,49.204,19.767,49.235,19.788,49.27,19.787,49.319,19.774,49.372,19.73,49.39,19.664,49.396,19.63,49.407,19.627,49.424,19.593,49.447,19.535,49.505,19.48,49.576,19.442,49.598,19.386,49.564,19.302,49.525,19.25,49.511,19.149,49.4,18.968,49.396,18.957,49.448,18.938,49.498,18.832,49.511,18.829,49.54,18.807,49.614,18.595,49.758,18.569,49.818,18.577,49.841,18.562,49.879,18.516,49.902,18.348,49.93,18.305,49.914,18.266,49.93,18.205,49.965,18.099,49.993,18.088,50.007,18.05,50.032,18.028,50.035,18.015,50.02,17.984,49.999,17.875,49.972,17.831,49.983,17.792,50.007,17.747,50.057,17.681,50.101,17.627,50.116,17.596,50.14,17.589,50.157,17.709,50.194,17.735,50.231,17.72,50.299,17.702,50.307,17.655,50.284,17.555,50.264,17.462,50.255,17.415,50.255,17.152,50.378,16.981,50.416,16.88,50.427,16.869,50.415,16.915,50.345,16.993,50.26,16.99,50.237,16.895,50.202,16.842,50.187,16.779,50.157,16.725,50.116,16.679,50.097,16.639,50.102,16.597,50.122,16.488,50.248,16.35,50.345,16.334,50.367,16.291,50.372,16.231,50.394,16.21,50.424,16.241,50.455,16.283,50.483,16.357,50.5,16.379,50.517,16.392,50.542,16.42,50.574,16.413,50.585,16.36,50.621,16.282,50.656,16.066,50.63,16.007,50.612,15.974,50.635,15.949,50.67,15.894,50.677,15.819,50.709,15.731,50.74,15.644,50.749,15.464,50.794,15.395,50.796,15.354,50.812,15.313,50.846,15.277,50.883,15.259,50.959,15.126,50.993,14.994,51.014,14.984,51.003,14.99,50.927,14.983,50.887,14.896,50.861,14.809,50.859,14.814,50.872,14.917,51.009,14.964,51.095,15.017,51.253,14.953,51.377,14.936,51.435,14.906,51.463,14.725,51.524,14.711,51.545,14.739,51.627,14.725,51.662,14.681,51.698,14.624,51.771,14.602,51.832,14.675,51.905,14.693,51.958,14.725,52.031,14.748,52.071,14.753,52.082,14.705,52.11,14.692,52.15,14.705,52.207,14.68,52.25,14.616,52.278,14.574,52.314,14.555,52.36,14.57,52.431,14.619,52.529,14.514,52.646,14.254,52.783,14.129,52.878,14.139,52.933,14.194,52.982,14.293,53.027,14.369,53.106,14.411,53.199,14.412,53.217,14.415,53.283,14.299,53.556,14.28,53.625,14.266,53.707,14.259,53.73,14.488,53.672,14.583,53.639,14.572,53.676,14.552,53.707,14.565,53.754,14.558,53.823,14.351,53.859,14.214,53.871,14.198,53.919,14.211,53.95,14.249,53.932,14.384,53.925,14.716,54.018,15.288,54.14,15.9,54.254,16.043,54.266,16.186,54.29,16.239,54.333,16.292,54.362,16.376,54.437,16.56,54.554,16.885,54.596,17.007,54.652,17.262,54.73,17.843,54.817,18.086,54.836,18.323,54.838,18.535,54.769,18.759,54.685,18.8,54.633,18.678,54.665,18.502,54.742,18.436,54.745,18.587,54.513,18.67,54.431,18.836,54.37,18.976,54.349,19.407,54.386,19.56,54.435,19.604,54.459,19.644,54.447,19.924,54.434,20.208,54.421,20.665,54.407,21.141,54.392,21.634,54.376,22.168,54.36,22.732,54.35,22.766,54.357,22.824,54.396,22.894,54.391,22.977,54.366,23.016,54.348,23.032,54.328,23.042,54.304,23.088,54.299,23.17,54.281,23.282,54.24,23.37,54.2,23.454,54.143,23.481,54.079,23.483,54.006,23.478,53.959,23.485,53.94,23.599,53.599,23.789,53.271,23.859,53.112,23.887,53.028,23.909,52.905,23.916,52.819,23.915,52.77,23.901,52.704,23.845,52.664,23.48,52.552,23.411,52.516,23.303,52.428,23.204,52.338,23.181,52.307,23.175,52.287,23.197,52.257,23.327,52.208,23.458,52.17,23.501,52.14,23.598,52.103,23.633,52.07,23.652,52.04,23.651,51.973,23.607,51.88,23.626,51.809,23.581,51.762,23.545,51.71,23.54,51.619,23.605,51.518]]]},{"n":"Philippines","c":"PH","r":2,"ml":2.5,"lx":122.47,"ly":11.2,"b":[116.97,5.06,126.593,20.841],"p":[[[121.102,18.615,121.254,18.563,121.593,18.376,121.717,18.33,121.846,18.295,121.948,18.285,122.038,18.328,122.077,18.372,122.147,18.487,122.221,18.501,122.266,18.459,122.3,18.403,122.315,18.32,122.294,18.234,122.223,18.157,122.179,18.064,122.151,17.756,122.152,17.664,122.175,17.576,122.237,17.435,122.269,17.395,122.362,17.345,122.388,17.307,122.393,17.238,122.408,17.178,122.468,17.155,122.519,17.125,122.5,17.058,122.468,16.99,122.426,16.823,122.226,16.435,122.214,16.352,122.135,16.185,121.975,16.158,121.789,16.077,121.685,16.015,121.595,15.933,121.561,15.827,121.59,15.778,121.609,15.726,121.607,15.67,121.579,15.623,121.49,15.51,121.452,15.417,121.412,15.375,121.392,15.324,121.399,15.267,121.435,15.216,121.544,14.999,121.661,14.79,121.686,14.765,121.695,14.737,121.627,14.682,121.628,14.581,121.649,14.481,121.752,14.234,121.767,14.168,121.8,14.114,121.853,14.063,121.912,14.02,122.08,13.947,122.144,13.933,122.212,13.93,122.228,13.979,122.288,13.996,122.274,14.045,122.203,14.112,122.2,14.148,122.238,14.175,122.283,14.191,122.384,14.264,122.491,14.322,122.627,14.318,122.761,14.285,122.856,14.251,122.934,14.188,123.015,14.08,123.071,13.96,123.071,13.903,123.057,13.845,123.06,13.789,123.102,13.75,123.231,13.747,123.297,13.836,123.305,13.937,123.259,13.975,123.28,14.025,123.32,14.062,123.377,14.029,123.432,13.966,123.633,13.898,123.684,13.897,123.726,13.884,123.816,13.837,123.858,13.8,123.806,13.722,123.607,13.704,123.55,13.646,123.608,13.528,123.704,13.432,123.765,13.354,123.819,13.269,123.817,13.192,123.785,13.111,123.873,13.117,123.955,13.1,124.069,13.032,124.105,13.025,124.143,13.036,124.137,12.791,124.06,12.567,123.962,12.595,123.878,12.69,123.895,12.805,123.949,12.916,123.918,12.94,123.864,12.931,123.802,12.906,123.736,12.897,123.627,12.912,123.402,13.033,123.311,13.044,123.29,13.099,123.296,13.216,123.206,13.354,123.192,13.403,123.163,13.442,122.896,13.592,122.863,13.617,122.781,13.737,122.595,13.908,122.543,13.925,122.486,13.93,122.468,13.887,122.494,13.82,122.504,13.763,122.5,13.703,122.508,13.657,122.596,13.562,122.609,13.517,122.668,13.395,122.675,13.253,122.6,13.194,122.515,13.26,122.513,13.314,122.498,13.364,122.407,13.493,122.377,13.521,122.205,13.648,122.073,13.788,121.778,13.938,121.743,13.946,121.692,13.935,121.643,13.916,121.501,13.842,121.451,13.791,121.446,13.712,121.344,13.649,121.204,13.64,121.096,13.679,121.006,13.758,120.932,13.762,120.841,13.885,120.729,13.901,120.637,13.804,120.617,13.995,120.617,14.188,120.643,14.244,120.688,14.291,120.922,14.493,120.952,14.558,120.941,14.645,120.888,14.716,120.804,14.759,120.708,14.777,120.638,14.816,120.584,14.881,120.547,14.766,120.583,14.595,120.589,14.483,120.556,14.441,120.496,14.44,120.439,14.453,120.396,14.493,120.365,14.608,120.284,14.684,120.251,14.793,120.214,14.809,120.138,14.8,120.082,14.851,120.045,14.978,120.037,15.115,120.005,15.229,119.959,15.34,119.933,15.431,119.892,15.838,119.881,15.875,119.86,15.906,119.808,15.952,119.769,16.008,119.762,16.055,119.773,16.255,119.79,16.303,119.831,16.327,119.886,16.287,119.93,16.239,119.985,16.215,120.033,16.185,120.124,16.066,120.16,16.048,120.271,16.051,120.337,16.066,120.369,16.11,120.389,16.161,120.389,16.222,120.325,16.4,120.305,16.529,120.304,16.645,120.321,16.762,120.409,16.956,120.42,17.09,120.412,17.27,120.427,17.377,120.425,17.438,120.372,17.535,120.358,17.638,120.505,18.163,120.551,18.264,120.584,18.369,120.6,18.508,120.709,18.546,120.814,18.603,120.868,18.599,120.925,18.585,121.051,18.614,121.102,18.615]],[[117.311,8.44,117.219,8.367,117.229,8.457,117.256,8.541,117.35,8.714,117.418,8.767,117.53,8.903,117.593,8.968,117.745,9.098,117.885,9.241,117.932,9.251,117.983,9.253,118.024,9.27,118.115,9.347,118.344,9.603,118.533,9.794,118.728,10.035,118.82,10.105,118.845,10.131,119.024,10.354,119.08,10.386,119.143,10.409,119.186,10.439,119.224,10.477,119.287,10.574,119.313,10.687,119.297,10.751,119.261,10.845,119.306,10.974,119.341,11.033,119.465,11.294,119.501,11.346,119.553,11.314,119.56,11.267,119.535,11.157,119.533,11.102,119.562,11.046,119.527,10.953,119.616,10.707,119.684,10.552,119.687,10.5,119.595,10.407,119.541,10.379,119.422,10.354,119.369,10.327,119.285,10.252,119.232,10.152,119.219,10.101,119.192,10.061,118.949,9.993,118.835,9.949,118.782,9.916,118.755,9.862,118.774,9.767,118.57,9.423,118.504,9.333,118.435,9.256,118.35,9.201,118.229,9.168,118.134,9.101,118.069,8.984,117.99,8.877,117.889,8.798,117.78,8.729,117.68,8.678,117.572,8.642,117.54,8.596,117.517,8.538,117.469,8.511,117.413,8.496,117.311,8.44]],[[122.496,11.615,122.613,11.564,122.726,11.608,122.838,11.596,122.931,11.529,122.901,11.487,122.895,11.441,123.103,11.541,123.158,11.536,123.156,11.443,123.144,11.364,123.12,11.287,123.075,11.197,123.017,11.117,122.939,11.058,122.847,11.022,122.803,10.99,122.789,10.941,122.791,10.88,122.77,10.824,122.673,10.801,122.522,10.692,122.198,10.623,122.109,10.576,122.052,10.514,121.988,10.458,121.954,10.444,121.938,10.471,121.934,10.494,121.98,10.639,121.972,10.699,121.95,10.757,121.964,10.872,122.021,10.979,122.051,11.097,122.06,11.326,122.104,11.643,122.101,11.681,122.067,11.724,121.941,11.758,121.891,11.791,121.916,11.854,121.964,11.897,122.029,11.895,122.087,11.855,122.291,11.772,122.399,11.702,122.496,11.615]],[[123.131,9.064,123.065,9.053,122.995,9.059,122.948,9.108,122.867,9.32,122.772,9.371,122.665,9.41,122.61,9.443,122.563,9.483,122.411,9.694,122.4,9.823,122.426,9.896,122.471,9.962,122.523,9.979,122.648,9.982,122.713,9.99,122.856,10.087,122.866,10.125,122.867,10.284,122.852,10.395,122.817,10.504,122.856,10.553,122.906,10.603,122.958,10.698,122.969,10.766,122.97,10.836,122.983,10.887,123.024,10.912,123.222,10.989,123.257,10.994,123.511,10.923,123.563,10.816,123.568,10.781,123.528,10.662,123.493,10.582,123.407,10.459,123.344,10.325,123.296,10.125,123.266,10.059,123.187,9.933,123.162,9.864,123.163,9.715,123.149,9.659,123.15,9.606,123.308,9.357,123.322,9.317,123.321,9.273,123.293,9.217,123.229,9.121,123.192,9.088,123.131,9.064]],[[124.575,11.343,124.644,11.308,124.724,11.322,124.821,11.401,124.93,11.373,124.994,11.256,125.027,11.212,125.044,11.135,125.04,10.952,125.013,10.786,125.034,10.751,125.084,10.722,125.128,10.685,125.164,10.637,125.188,10.585,125.197,10.457,125.26,10.35,125.268,10.308,125.253,10.264,125.148,10.272,125.14,10.235,125.143,10.189,125.105,10.218,125.044,10.323,124.988,10.368,125.005,10.197,125.024,10.115,125.027,10.033,124.929,10.096,124.813,10.135,124.781,10.168,124.792,10.275,124.79,10.328,124.738,10.44,124.799,10.682,124.797,10.732,124.787,10.781,124.739,10.88,124.663,10.962,124.616,10.962,124.503,10.904,124.446,10.924,124.412,11.15,124.366,11.371,124.331,11.427,124.308,11.486,124.331,11.535,124.374,11.515,124.436,11.457,124.511,11.424,124.548,11.395,124.575,11.343]],[[125.24,12.528,125.31,12.446,125.328,12.387,125.32,12.322,125.352,12.293,125.409,12.285,125.481,12.252,125.536,12.191,125.503,12.136,125.513,12.055,125.457,11.953,125.464,11.772,125.497,11.714,125.5,11.655,125.492,11.594,125.506,11.544,125.593,11.378,125.609,11.323,125.582,11.279,125.574,11.238,125.627,11.234,125.704,11.165,125.749,11.074,125.736,11.05,125.674,11.121,125.628,11.132,125.432,11.113,125.312,11.142,125.233,11.145,125.156,11.267,125.088,11.287,125.034,11.341,124.945,11.479,124.917,11.558,124.979,11.638,124.998,11.702,124.995,11.765,124.936,11.755,124.884,11.775,124.821,11.852,124.796,11.896,124.75,11.933,124.677,12.021,124.572,12.055,124.529,12.079,124.446,12.153,124.385,12.244,124.326,12.404,124.295,12.569,124.566,12.526,124.84,12.535,125.15,12.573,125.24,12.528]],[[120.704,13.479,120.755,13.471,120.915,13.501,120.981,13.486,121.025,13.429,121.079,13.411,121.122,13.381,121.203,13.432,121.284,13.374,121.357,13.265,121.442,13.188,121.523,13.131,121.539,13.089,121.49,13.02,121.475,12.932,121.48,12.837,121.541,12.638,121.519,12.584,121.458,12.508,121.412,12.423,121.418,12.389,121.4,12.361,121.394,12.301,121.357,12.313,121.322,12.304,121.289,12.277,121.237,12.219,121.155,12.236,121.117,12.253,121.108,12.304,121.083,12.339,121.049,12.36,120.963,12.447,120.922,12.512,120.921,12.581,120.899,12.646,120.855,12.704,120.796,12.748,120.776,12.791,120.769,12.841,120.764,12.97,120.68,13.131,120.651,13.169,120.573,13.209,120.508,13.26,120.481,13.311,120.455,13.394,120.438,13.405,120.388,13.402,120.338,13.412,120.353,13.473,120.401,13.517,120.468,13.522,120.653,13.498,120.704,13.479]],[[126.006,9.321,126.088,9.261,126.193,9.277,126.192,9.125,126.209,9.081,126.305,8.952,126.32,8.845,126.263,8.744,126.22,8.696,126.142,8.627,126.14,8.596,126.173,8.56,126.282,8.539,126.365,8.484,126.38,8.327,126.459,8.203,126.457,8.149,126.425,7.927,126.435,7.833,126.494,7.757,126.544,7.725,126.57,7.677,126.593,7.547,126.589,7.325,126.582,7.248,126.547,7.176,126.439,7.012,126.294,6.882,126.217,6.891,126.192,6.853,126.24,6.734,126.221,6.483,126.189,6.31,126.142,6.398,126.11,6.49,126.08,6.733,126.043,6.843,125.985,6.944,125.962,7.033,125.901,7.117,125.824,7.333,125.774,7.322,125.689,7.263,125.67,7.222,125.66,7.161,125.641,7.105,125.542,7.017,125.465,6.911,125.401,6.796,125.381,6.69,125.433,6.607,125.487,6.574,125.565,6.5,125.588,6.466,125.671,6.225,125.668,5.979,125.608,5.87,125.456,5.664,125.346,5.599,125.288,5.632,125.241,5.757,125.233,5.808,125.265,5.926,125.268,6.033,125.232,6.07,125.191,6.063,125.174,6.047,125.076,5.906,125.035,5.871,124.975,5.866,124.927,5.875,124.636,5.998,124.399,6.12,124.213,6.233,124.078,6.404,124.05,6.533,124.048,6.667,123.988,6.863,123.981,6.93,123.985,6.994,124.045,7.114,124.118,7.175,124.158,7.219,124.191,7.267,124.213,7.332,124.207,7.396,124.182,7.437,124.068,7.578,123.968,7.665,123.765,7.743,123.717,7.785,123.666,7.818,123.609,7.832,123.553,7.832,123.493,7.808,123.477,7.756,123.482,7.71,123.476,7.665,123.391,7.408,123.282,7.464,123.178,7.529,123.151,7.575,123.139,7.63,123.121,7.667,123.097,7.7,123.049,7.614,122.99,7.546,122.917,7.531,122.843,7.529,122.819,7.558,122.792,7.722,122.714,7.774,122.616,7.763,122.498,7.673,122.474,7.639,122.449,7.561,122.32,7.34,122.251,7.17,122.176,7.004,122.142,6.95,122.098,6.914,122.028,6.929,121.964,6.968,121.904,7.075,121.925,7.2,121.991,7.279,122.047,7.364,122.115,7.66,122.12,7.765,122.132,7.81,122.243,7.945,122.337,8.028,122.387,8.046,122.589,8.093,122.673,8.133,122.804,8.134,122.911,8.156,122.996,8.221,123.003,8.287,122.999,8.356,123.018,8.398,123.051,8.434,123.096,8.481,123.147,8.516,123.293,8.541,123.341,8.57,123.38,8.616,123.435,8.703,123.499,8.682,123.564,8.647,123.68,8.621,123.783,8.548,123.849,8.433,123.861,8.376,123.877,8.189,123.853,8.145,123.753,8.058,123.799,8.049,123.931,8.128,123.997,8.159,124.159,8.201,124.198,8.23,124.226,8.271,124.283,8.386,124.325,8.508,124.358,8.559,124.405,8.6,124.451,8.606,124.622,8.523,124.731,8.563,124.762,8.69,124.787,8.874,124.806,8.924,124.869,8.972,124.944,8.957,125.046,8.891,125.141,8.869,125.176,8.922,125.21,9.027,125.248,9.027,125.376,8.992,125.499,9.015,125.533,9.141,125.51,9.276,125.414,9.669,125.471,9.757,125.521,9.759,125.642,9.654,125.877,9.513,125.955,9.427,126.006,9.321]],[[123.37,9.45,123.332,9.423,123.316,9.489,123.327,9.578,123.404,9.889,123.386,9.967,123.514,10.14,123.593,10.303,123.711,10.474,123.726,10.562,123.832,10.731,123.93,10.964,123.925,11.041,123.95,11.079,123.964,11.137,123.967,11.187,124.039,11.274,124.058,11.217,124.037,11.107,124.04,11.054,124.053,11.029,124.053,10.926,124.028,10.768,124.051,10.586,124.005,10.4,123.952,10.317,123.874,10.258,123.789,10.221,123.7,10.128,123.643,10.02,123.634,9.922,123.494,9.589,123.37,9.45]],[[121.922,18.895,121.858,18.823,121.825,18.843,121.861,18.913,121.86,18.937,121.889,18.992,121.943,19.01,121.988,18.957,121.922,18.895]],[[121.521,19.362,121.531,19.271,121.472,19.273,121.383,19.328,121.375,19.356,121.376,19.38,121.392,19.399,121.521,19.362]],[[121.96,20.366,121.941,20.354,121.914,20.359,121.941,20.454,121.991,20.48,122.031,20.469,121.96,20.366]],[[121.878,20.782,121.83,20.7,121.791,20.701,121.796,20.747,121.848,20.841,121.867,20.839,121.878,20.782]],[[121.159,6.076,121.214,6.004,121.283,6.022,121.392,6.002,121.415,5.965,121.411,5.94,121.294,5.87,121.218,5.943,121.083,5.893,121.019,5.923,120.931,5.896,120.876,5.953,120.898,6.007,121.038,6.096,121.159,6.076]],[[117.08,7.883,117.028,7.808,116.97,7.895,116.976,8.017,116.994,8.051,117.077,8.069,117.08,7.883]],[[119.916,10.486,119.793,10.455,119.764,10.552,119.852,10.64,119.95,10.605,120.008,10.57,119.981,10.539,119.916,10.486]],[[120.1,12.168,120.155,12.152,120.194,12.167,120.228,12.22,120.261,12.142,120.341,12.077,120.315,12.012,120.243,12.005,120.174,12.02,120.1,11.994,120.011,12.008,119.957,12.069,119.896,12.179,119.866,12.199,119.87,12.244,119.892,12.273,119.88,12.28,119.886,12.3,119.897,12.313,119.916,12.319,119.964,12.27,120.078,12.198,120.1,12.168]],[[120.039,11.703,119.964,11.669,119.945,11.691,119.932,11.74,119.933,11.774,119.861,11.954,119.916,11.981,119.957,11.96,119.998,11.932,120.036,11.917,120.071,11.861,120.062,11.821,120.073,11.783,120.039,11.703]],[[122.65,10.473,122.622,10.459,122.597,10.461,122.538,10.425,122.517,10.493,122.538,10.608,122.626,10.695,122.648,10.723,122.673,10.739,122.701,10.741,122.729,10.706,122.737,10.655,122.681,10.498,122.65,10.473]],[[124.594,9.787,124.584,9.75,124.506,9.754,124.478,9.748,124.403,9.654,124.36,9.63,124.122,9.599,123.936,9.624,123.872,9.676,123.83,9.761,123.817,9.817,123.864,9.879,123.909,9.92,124.06,10,124.094,10.061,124.173,10.135,124.336,10.16,124.352,10.141,124.373,10.13,124.406,10.126,124.486,10.065,124.577,10.027,124.555,9.879,124.582,9.83,124.594,9.787]],[[120.25,5.257,120.223,5.196,120.192,5.168,120.15,5.184,120.118,5.215,120.101,5.169,120.013,5.151,119.958,5.08,119.878,5.06,119.821,5.07,119.827,5.133,119.983,5.228,120.08,5.264,120.165,5.332,120.208,5.34,120.229,5.284,120.25,5.257]],[[122.093,6.428,121.991,6.415,121.959,6.416,121.88,6.518,121.872,6.563,121.809,6.614,121.832,6.664,121.915,6.676,122.058,6.741,122.288,6.639,122.324,6.602,122.252,6.58,122.201,6.483,122.093,6.428]],[[126.059,9.766,126.047,9.761,125.991,9.839,125.999,9.927,126.074,10.059,126.129,9.944,126.129,9.891,126.121,9.865,126.173,9.8,126.137,9.768,126.059,9.766]],[[125.69,9.914,125.673,9.886,125.649,9.944,125.591,9.998,125.534,10.09,125.495,10.119,125.522,10.192,125.525,10.31,125.58,10.364,125.606,10.38,125.648,10.437,125.667,10.44,125.685,10.392,125.647,10.245,125.703,10.072,125.684,9.963,125.692,9.939,125.69,9.914]],[[120.271,13.751,120.273,13.683,120.104,13.782,120.099,13.817,120.103,13.843,120.121,13.858,120.211,13.821,120.271,13.751]],[[121.915,13.54,121.977,13.537,121.996,13.547,122.115,13.463,122.107,13.421,122.122,13.365,122.055,13.269,122.042,13.236,122.005,13.205,121.876,13.282,121.829,13.329,121.815,13.424,121.866,13.566,121.915,13.54]],[[122.094,12.355,122.014,12.106,121.96,12.191,121.982,12.245,121.936,12.29,121.923,12.331,121.941,12.385,121.989,12.435,122.002,12.599,122.104,12.651,122.145,12.653,122.13,12.613,122.132,12.538,122.094,12.355]],[[122.654,12.309,122.603,12.286,122.499,12.384,122.439,12.429,122.423,12.455,122.472,12.492,122.604,12.492,122.674,12.424,122.683,12.382,122.654,12.309]],[[123.282,12.853,123.367,12.701,123.274,12.805,123.166,12.876,123.054,12.993,122.973,13.035,122.949,13.059,122.958,13.107,123.017,13.116,123.044,13.113,123.206,12.905,123.282,12.853]],[[123.775,12.454,123.779,12.366,123.742,12.399,123.621,12.571,123.587,12.633,123.621,12.675,123.709,12.611,123.775,12.454]],[[123.717,12.287,123.908,12.169,124.04,11.967,124.056,11.812,124.046,11.752,123.983,11.819,123.848,11.914,123.754,11.934,123.725,11.952,123.736,12.003,123.675,12.05,123.668,12.069,123.612,12.09,123.531,12.197,123.474,12.217,123.419,12.194,123.293,12.036,123.158,11.926,123.156,11.968,123.211,12.107,123.245,12.328,123.267,12.395,123.24,12.495,123.236,12.583,123.337,12.542,123.463,12.501,123.559,12.445,123.575,12.407,123.717,12.287]],[[124.354,13.632,124.327,13.567,124.295,13.59,124.248,13.587,124.175,13.532,124.057,13.606,124.039,13.663,124.124,13.79,124.123,13.98,124.154,14.026,124.186,14.06,124.225,14.078,124.308,13.947,124.337,13.931,124.417,13.871,124.396,13.75,124.404,13.679,124.354,13.632]],[[122.175,14.049,122.172,14.008,121.956,14.156,121.946,14.181,121.946,14.205,121.959,14.229,122.175,14.049]],[[122.033,15.005,122.052,14.97,122.032,14.972,122.017,14.965,121.97,14.893,122.022,14.759,121.99,14.662,121.933,14.656,121.911,14.667,121.922,14.715,121.935,14.737,121.923,14.8,121.889,14.84,121.862,14.917,121.82,14.964,121.84,15.038,121.972,15.046,122.033,15.005]],[[124.316,10.606,124.288,10.601,124.335,10.707,124.371,10.691,124.382,10.68,124.381,10.633,124.316,10.606]],[[125.281,9.982,125.288,9.933,125.159,10.063,125.133,10.155,125.176,10.151,125.231,10.116,125.281,9.982]],[[122.937,7.409,122.948,7.386,122.944,7.361,122.84,7.315,122.805,7.316,122.797,7.393,122.822,7.428,122.871,7.397,122.915,7.433,122.937,7.409]],[[125.785,6.963,125.769,6.906,125.708,7.04,125.683,7.073,125.714,7.186,125.783,7.131,125.785,6.963]],[[124.608,11.492,124.483,11.486,124.429,11.532,124.36,11.666,124.437,11.695,124.511,11.687,124.565,11.64,124.622,11.55,124.608,11.492]],[[124.807,9.143,124.778,9.083,124.666,9.132,124.639,9.175,124.653,9.226,124.708,9.243,124.737,9.243,124.79,9.19,124.807,9.143]],[[121.252,19.082,121.247,19.015,121.196,19.051,121.185,19.101,121.19,19.139,121.213,19.184,121.245,19.143,121.252,19.082]],[[119.861,11.525,119.883,11.472,119.855,11.393,119.831,11.376,119.799,11.409,119.73,11.432,119.726,11.475,119.761,11.474,119.827,11.515,119.861,11.525]],[[123.757,11.283,123.816,11.151,123.737,11.151,123.708,11.248,123.741,11.279,123.757,11.283]],[[122.311,12.529,122.28,12.498,122.261,12.503,122.248,12.557,122.278,12.593,122.288,12.589,122.311,12.529]],[[125.971,9.594,125.952,9.568,125.922,9.621,125.949,9.739,125.968,9.759,125.993,9.685,125.971,9.594]],[[124.854,11.595,124.836,11.543,124.807,11.558,124.781,11.581,124.744,11.659,124.731,11.715,124.788,11.683,124.821,11.627,124.854,11.595]],[[117.355,8.215,117.287,8.191,117.272,8.254,117.281,8.315,117.33,8.308,117.354,8.289,117.355,8.215]],[[123.698,9.237,123.706,9.134,123.614,9.103,123.541,9.13,123.493,9.192,123.494,9.216,123.535,9.214,123.626,9.268,123.655,9.279,123.698,9.237]]]},{"n":"Peru","c":"PE","r":2,"ml":2,"lx":-72.9,"ly":-12.98,"b":[-81.337,-18.346,-68.685,-0.042],"p":[[[-69.966,-4.236,-69.972,-4.301,-70.004,-4.327,-70.053,-4.333,-70.129,-4.287,-70.184,-4.298,-70.239,-4.301,-70.317,-4.247,-70.344,-4.194,-70.405,-4.15,-70.531,-4.168,-70.635,-4.169,-70.722,-4.159,-70.8,-4.173,-70.866,-4.23,-70.916,-4.295,-70.974,-4.35,-71.144,-4.387,-71.235,-4.388,-71.317,-4.424,-71.438,-4.438,-71.521,-4.47,-71.668,-4.487,-71.845,-4.504,-71.943,-4.553,-71.982,-4.575,-72.083,-4.642,-72.257,-4.749,-72.353,-4.786,-72.469,-4.901,-72.608,-5.01,-72.699,-5.067,-72.832,-5.094,-72.887,-5.123,-72.907,-5.158,-72.896,-5.198,-72.918,-5.303,-72.959,-5.495,-72.97,-5.59,-72.98,-5.635,-73.068,-5.79,-73.163,-5.933,-73.209,-6.029,-73.236,-6.098,-73.206,-6.156,-73.168,-6.261,-73.135,-6.344,-73.126,-6.401,-73.137,-6.466,-73.177,-6.525,-73.24,-6.564,-73.325,-6.575,-73.5,-6.679,-73.695,-6.834,-73.758,-6.906,-73.776,-6.974,-73.805,-7.08,-73.793,-7.135,-73.758,-7.173,-73.723,-7.263,-73.72,-7.309,-73.749,-7.335,-73.805,-7.341,-73.854,-7.35,-73.892,-7.373,-73.929,-7.367,-73.964,-7.379,-73.964,-7.417,-73.953,-7.46,-73.958,-7.507,-73.982,-7.536,-74.002,-7.556,-73.982,-7.585,-73.947,-7.611,-73.895,-7.655,-73.822,-7.739,-73.767,-7.754,-73.72,-7.783,-73.715,-7.829,-73.732,-7.875,-73.773,-7.896,-73.776,-7.936,-73.72,-7.986,-73.683,-8.021,-73.645,-8.073,-73.61,-8.145,-73.61,-8.192,-73.572,-8.25,-73.549,-8.299,-73.549,-8.346,-73.488,-8.392,-73.436,-8.427,-73.398,-8.459,-73.36,-8.479,-73.352,-8.514,-73.357,-8.567,-73.302,-8.654,-73.203,-8.719,-73.123,-8.814,-73.071,-8.883,-72.974,-8.993,-72.97,-9.12,-73.09,-9.266,-73.209,-9.411,-73.014,-9.407,-72.814,-9.41,-72.605,-9.452,-72.465,-9.492,-72.379,-9.51,-72.318,-9.557,-72.289,-9.629,-72.266,-9.688,-72.26,-9.774,-72.173,-9.844,-72.179,-9.91,-72.182,-10.004,-72.143,-10.005,-71.887,-10.006,-71.608,-10.006,-71.339,-9.989,-71.238,-9.966,-71.115,-9.852,-71.042,-9.819,-70.971,-9.766,-70.885,-9.669,-70.816,-9.625,-70.758,-9.572,-70.672,-9.518,-70.637,-9.478,-70.608,-9.464,-70.541,-9.437,-70.57,-9.49,-70.592,-9.543,-70.599,-9.621,-70.567,-9.705,-70.594,-9.767,-70.637,-9.824,-70.638,-9.972,-70.639,-10.182,-70.639,-10.361,-70.64,-10.586,-70.642,-10.841,-70.642,-11.01,-70.597,-10.977,-70.533,-10.947,-70.451,-11.025,-70.392,-11.059,-70.342,-11.067,-70.29,-11.064,-70.22,-11.048,-70.066,-10.982,-69.96,-10.93,-69.84,-10.933,-69.674,-10.954,-69.579,-10.952,-69.454,-11.169,-69.362,-11.328,-69.258,-11.509,-69.174,-11.654,-69.046,-11.876,-68.936,-12.067,-68.819,-12.27,-68.685,-12.502,-68.728,-12.561,-68.763,-12.608,-68.759,-12.687,-68.812,-12.73,-68.868,-12.755,-68.934,-12.822,-68.979,-12.88,-68.981,-12.963,-68.972,-13.382,-68.983,-13.496,-69.018,-13.594,-69.053,-13.644,-69.074,-13.683,-69.023,-13.78,-68.974,-13.976,-68.937,-14.015,-68.892,-14.094,-68.871,-14.17,-68.88,-14.199,-68.972,-14.234,-69.004,-14.265,-69.013,-14.377,-69.053,-14.418,-69.12,-14.47,-69.163,-14.531,-69.199,-14.573,-69.235,-14.597,-69.252,-14.671,-69.276,-14.746,-69.359,-14.795,-69.374,-14.887,-69.375,-14.963,-69.331,-15.039,-69.187,-15.199,-69.172,-15.237,-69.254,-15.333,-69.302,-15.399,-69.419,-15.603,-69.421,-15.641,-69.392,-15.737,-69.218,-16.149,-69.188,-16.183,-69.134,-16.222,-69.046,-16.218,-68.913,-16.262,-68.849,-16.313,-68.843,-16.338,-68.858,-16.355,-68.928,-16.389,-69.006,-16.434,-69.033,-16.476,-69.038,-16.543,-69.021,-16.642,-69.055,-16.674,-69.133,-16.713,-69.2,-16.768,-69.267,-16.861,-69.382,-17.001,-69.421,-17.04,-69.438,-17.088,-69.503,-17.105,-69.625,-17.2,-69.646,-17.249,-69.626,-17.294,-69.564,-17.333,-69.522,-17.389,-69.511,-17.46,-69.511,-17.505,-69.511,-17.506,-69.586,-17.573,-69.685,-17.65,-69.806,-17.665,-69.852,-17.704,-69.842,-17.785,-69.802,-17.9,-69.803,-17.99,-69.84,-18.093,-69.926,-18.206,-70.059,-18.283,-70.184,-18.325,-70.282,-18.325,-70.377,-18.334,-70.418,-18.346,-70.492,-18.278,-70.817,-18.053,-70.942,-17.932,-71.057,-17.876,-71.337,-17.683,-71.365,-17.621,-71.399,-17.422,-71.436,-17.366,-71.532,-17.294,-71.774,-17.199,-71.868,-17.151,-71.967,-17.064,-72.111,-17.003,-72.269,-16.876,-72.362,-16.775,-72.468,-16.708,-72.794,-16.615,-72.958,-16.521,-73.264,-16.389,-73.4,-16.304,-73.728,-16.202,-73.825,-16.153,-74.147,-15.912,-74.373,-15.834,-74.555,-15.699,-75.104,-15.412,-75.191,-15.32,-75.275,-15.178,-75.397,-15.094,-75.534,-14.899,-75.738,-14.785,-75.934,-14.634,-76.006,-14.496,-76.136,-14.32,-76.175,-14.227,-76.289,-14.133,-76.297,-13.948,-76.376,-13.863,-76.319,-13.821,-76.259,-13.803,-76.184,-13.515,-76.224,-13.371,-76.427,-13.11,-76.502,-12.984,-76.555,-12.823,-76.637,-12.728,-76.758,-12.527,-76.832,-12.349,-76.994,-12.219,-77.038,-12.173,-77.063,-12.107,-77.153,-12.06,-77.158,-11.923,-77.22,-11.663,-77.31,-11.532,-77.633,-11.288,-77.639,-11.194,-77.664,-11.022,-77.736,-10.837,-78.095,-10.261,-78.186,-10.089,-78.276,-9.81,-78.356,-9.652,-78.446,-9.371,-78.58,-9.157,-78.665,-8.971,-78.755,-8.74,-78.762,-8.617,-78.925,-8.405,-79.012,-8.21,-79.164,-8.047,-79.313,-7.923,-79.377,-7.836,-79.589,-7.419,-79.618,-7.296,-79.762,-7.067,-79.905,-6.902,-79.995,-6.769,-80.11,-6.65,-80.812,-6.282,-81.058,-6.129,-81.142,-6.057,-81.181,-5.942,-81.164,-5.875,-81.092,-5.812,-80.992,-5.861,-80.931,-5.841,-80.883,-5.759,-80.882,-5.635,-80.943,-5.475,-81.168,-5.167,-81.151,-5.102,-81.108,-5.028,-81.195,-4.879,-81.289,-4.761,-81.337,-4.67,-81.283,-4.322,-81.232,-4.234,-80.892,-3.882,-80.799,-3.731,-80.653,-3.638,-80.504,-3.496,-80.325,-3.388,-80.298,-3.406,-80.274,-3.425,-80.272,-3.461,-80.265,-3.492,-80.245,-3.522,-80.244,-3.577,-80.221,-3.613,-80.219,-3.654,-80.217,-3.711,-80.229,-3.739,-80.218,-3.788,-80.179,-3.878,-80.194,-3.906,-80.231,-3.924,-80.267,-3.949,-80.303,-4.005,-80.358,-4.003,-80.437,-3.979,-80.49,-4.01,-80.51,-4.07,-80.493,-4.119,-80.488,-4.166,-80.454,-4.205,-80.353,-4.208,-80.444,-4.336,-80.488,-4.394,-80.479,-4.43,-80.424,-4.461,-80.383,-4.464,-80.293,-4.417,-80.232,-4.349,-80.197,-4.311,-80.14,-4.296,-80.064,-4.328,-79.963,-4.39,-79.845,-4.446,-79.797,-4.476,-79.711,-4.468,-79.639,-4.455,-79.578,-4.501,-79.516,-4.539,-79.502,-4.671,-79.456,-4.766,-79.399,-4.84,-79.331,-4.928,-79.268,-4.958,-79.187,-4.958,-79.076,-4.991,-79.033,-4.969,-78.995,-4.908,-78.975,-4.873,-78.919,-4.858,-78.914,-4.819,-78.926,-4.771,-78.908,-4.714,-78.862,-4.665,-78.743,-4.593,-78.686,-4.562,-78.674,-4.518,-78.653,-4.458,-78.661,-4.425,-78.685,-4.384,-78.679,-4.326,-78.648,-4.248,-78.603,-4.157,-78.565,-4.042,-78.55,-3.987,-78.509,-3.952,-78.493,-3.902,-78.471,-3.843,-78.42,-3.777,-78.421,-3.706,-78.4,-3.674,-78.398,-3.595,-78.347,-3.431,-78.345,-3.397,-78.323,-3.388,-78.284,-3.399,-78.251,-3.436,-78.24,-3.473,-78.226,-3.489,-78.195,-3.486,-78.158,-3.465,-78.161,-3.432,-78.187,-3.4,-78.195,-3.38,-78.183,-3.35,-78.128,-3.284,-78.068,-3.207,-77.938,-3.047,-77.861,-2.982,-77.659,-2.912,-77.506,-2.86,-77.36,-2.81,-77.161,-2.738,-76.881,-2.636,-76.679,-2.563,-76.499,-2.432,-76.36,-2.331,-76.241,-2.244,-76.09,-2.133,-75.885,-1.893,-75.745,-1.728,-75.642,-1.607,-75.571,-1.531,-75.514,-1.316,-75.449,-1.071,-75.42,-0.962,-75.408,-0.924,-75.38,-0.94,-75.348,-0.967,-75.309,-0.968,-75.272,-0.967,-75.25,-0.952,-75.284,-0.707,-75.279,-0.654,-75.259,-0.59,-75.263,-0.555,-75.325,-0.507,-75.425,-0.409,-75.466,-0.322,-75.491,-0.248,-75.561,-0.2,-75.632,-0.158,-75.626,-0.123,-75.584,-0.123,-75.476,-0.157,-75.398,-0.146,-75.34,-0.142,-75.284,-0.107,-75.225,-0.042,-75.184,-0.042,-75.138,-0.05,-75.055,-0.117,-75.005,-0.156,-74.945,-0.188,-74.889,-0.199,-74.837,-0.203,-74.802,-0.2,-74.78,-0.245,-74.755,-0.299,-74.692,-0.335,-74.616,-0.37,-74.555,-0.43,-74.514,-0.47,-74.465,-0.518,-74.418,-0.581,-74.375,-0.691,-74.353,-0.767,-74.329,-0.808,-74.334,-0.851,-74.284,-0.928,-74.246,-0.971,-74.181,-0.998,-74.054,-1.029,-73.987,-1.098,-73.927,-1.125,-73.863,-1.197,-73.807,-1.218,-73.736,-1.214,-73.664,-1.249,-73.61,-1.316,-73.575,-1.401,-73.521,-1.45,-73.494,-1.537,-73.525,-1.639,-73.496,-1.693,-73.44,-1.737,-73.35,-1.784,-73.266,-1.772,-73.224,-1.788,-73.197,-1.83,-73.181,-1.88,-73.145,-2.003,-73.127,-2.081,-73.16,-2.156,-73.173,-2.208,-73.154,-2.278,-73.068,-2.312,-72.99,-2.34,-72.941,-2.394,-72.887,-2.408,-72.811,-2.405,-72.714,-2.392,-72.66,-2.361,-72.625,-2.352,-72.587,-2.365,-72.501,-2.395,-72.396,-2.429,-72.301,-2.409,-72.218,-2.4,-72.137,-2.381,-72.054,-2.325,-71.984,-2.327,-71.932,-2.289,-71.867,-2.228,-71.803,-2.166,-71.753,-2.153,-71.671,-2.182,-71.559,-2.224,-71.496,-2.279,-71.447,-2.294,-71.397,-2.334,-71.3,-2.335,-71.196,-2.313,-71.113,-2.245,-71.027,-2.226,-70.969,-2.207,-70.915,-2.219,-70.705,-2.342,-70.648,-2.406,-70.576,-2.418,-70.517,-2.453,-70.418,-2.491,-70.364,-2.529,-70.295,-2.553,-70.244,-2.607,-70.165,-2.64,-70.096,-2.658,-70.065,-2.702,-70.064,-2.731,-70.074,-2.75,-70.147,-2.864,-70.29,-3.087,-70.419,-3.288,-70.622,-3.605,-70.735,-3.782,-70.706,-3.789,-70.53,-3.866,-70.486,-3.869,-70.421,-3.85,-70.379,-3.819,-70.34,-3.814,-70.298,-3.844,-70.24,-3.883,-70.198,-3.995,-70.168,-4.05,-70.095,-4.092,-70.017,-4.162,-69.966,-4.236]]]},{"n":"Paraguay","c":"PY","r":4,"ml":3,"lx":-60.15,"ly":-21.67,"b":[-62.651,-27.554,-54.242,-19.286],"p":[[[-58.16,-20.165,-58.138,-20.237,-58.125,-20.293,-58.092,-20.333,-58.058,-20.386,-58.025,-20.416,-58.002,-20.465,-58.009,-20.522,-57.996,-20.594,-57.979,-20.657,-57.962,-20.674,-57.915,-20.69,-57.891,-20.747,-57.908,-20.776,-57.902,-20.809,-57.885,-20.842,-57.9,-20.873,-57.892,-20.897,-57.86,-20.919,-57.83,-20.998,-57.827,-21.134,-57.86,-21.206,-57.886,-21.266,-57.893,-21.302,-57.873,-21.355,-57.906,-21.418,-57.946,-21.494,-57.936,-21.547,-57.929,-21.597,-57.926,-21.65,-57.916,-21.699,-57.929,-21.752,-57.943,-21.798,-57.949,-21.851,-57.933,-21.911,-57.962,-21.967,-57.979,-22.007,-57.986,-22.046,-57.956,-22.109,-57.88,-22.136,-57.82,-22.142,-57.764,-22.109,-57.721,-22.099,-57.642,-22.129,-57.569,-22.182,-57.476,-22.189,-57.394,-22.198,-57.331,-22.215,-57.238,-22.195,-57.142,-22.215,-57.03,-22.245,-56.937,-22.271,-56.845,-22.265,-56.775,-22.261,-56.702,-22.232,-56.633,-22.235,-56.58,-22.182,-56.55,-22.136,-56.524,-22.103,-56.448,-22.076,-56.395,-22.093,-56.352,-22.179,-56.276,-22.228,-56.246,-22.265,-56.19,-22.281,-56.067,-22.284,-55.991,-22.281,-55.905,-22.308,-55.849,-22.308,-55.8,-22.354,-55.753,-22.41,-55.747,-22.513,-55.704,-22.592,-55.647,-22.622,-55.618,-22.671,-55.628,-22.741,-55.654,-22.81,-55.651,-22.886,-55.621,-22.956,-55.621,-23.025,-55.601,-23.095,-55.561,-23.154,-55.548,-23.25,-55.555,-23.32,-55.528,-23.359,-55.518,-23.416,-55.535,-23.462,-55.542,-23.525,-55.538,-23.581,-55.518,-23.627,-55.459,-23.687,-55.442,-23.793,-55.442,-23.865,-55.416,-23.951,-55.366,-23.991,-55.287,-24.004,-55.194,-24.017,-55.082,-23.998,-54.983,-23.975,-54.926,-23.951,-54.817,-23.888,-54.721,-23.852,-54.672,-23.829,-54.625,-23.812,-54.53,-23.852,-54.44,-23.902,-54.371,-23.971,-54.242,-24.047,-54.267,-24.066,-54.318,-24.128,-54.317,-24.201,-54.281,-24.306,-54.313,-24.528,-54.413,-24.867,-54.454,-25.065,-54.436,-25.121,-54.473,-25.22,-54.611,-25.433,-54.616,-25.576,-54.632,-26.006,-54.678,-26.309,-54.755,-26.533,-54.825,-26.652,-54.889,-26.667,-54.934,-26.703,-54.962,-26.759,-55.014,-26.807,-55.089,-26.845,-55.13,-26.886,-55.136,-26.931,-55.208,-26.96,-55.346,-26.973,-55.427,-27.009,-55.451,-27.068,-55.497,-27.115,-55.565,-27.15,-55.597,-27.208,-55.594,-27.288,-55.633,-27.357,-55.715,-27.415,-55.79,-27.416,-55.859,-27.362,-55.951,-27.326,-56.067,-27.308,-56.164,-27.321,-56.242,-27.367,-56.311,-27.439,-56.371,-27.537,-56.437,-27.554,-56.511,-27.488,-56.603,-27.468,-56.716,-27.494,-56.805,-27.485,-56.872,-27.441,-56.974,-27.436,-57.112,-27.47,-57.391,-27.43,-57.812,-27.317,-58.168,-27.273,-58.605,-27.314,-58.642,-27.196,-58.619,-27.132,-58.548,-27.084,-58.503,-27.029,-58.485,-26.968,-58.436,-26.922,-58.356,-26.89,-58.323,-26.858,-58.335,-26.825,-58.318,-26.796,-58.272,-26.771,-58.246,-26.731,-58.239,-26.677,-58.222,-26.65,-58.191,-26.63,-58.188,-26.593,-58.205,-26.477,-58.203,-26.381,-58.181,-26.307,-58.155,-26.263,-58.136,-26.251,-58.118,-26.225,-58.111,-26.18,-58.082,-26.139,-57.943,-26.053,-57.891,-26.007,-57.886,-25.964,-57.865,-25.907,-57.782,-25.784,-57.757,-25.726,-57.755,-25.697,-57.725,-25.667,-57.626,-25.599,-57.572,-25.534,-57.563,-25.474,-57.587,-25.405,-57.644,-25.328,-57.822,-25.136,-57.96,-25.049,-58.136,-24.977,-58.253,-24.954,-58.309,-24.979,-58.365,-24.959,-58.423,-24.894,-58.52,-24.843,-58.724,-24.787,-59.187,-24.562,-59.373,-24.454,-59.435,-24.387,-59.609,-24.267,-59.892,-24.094,-60.11,-24.009,-60.262,-24.014,-60.505,-23.964,-60.84,-23.858,-61.033,-23.756,-61.085,-23.656,-61.208,-23.557,-61.404,-23.458,-61.506,-23.392,-61.513,-23.36,-61.571,-23.319,-61.679,-23.269,-61.799,-23.182,-61.928,-23.059,-62.067,-22.869,-62.214,-22.612,-62.373,-22.439,-62.542,-22.35,-62.626,-22.29,-62.626,-22.262,-62.651,-22.234,-62.629,-22.184,-62.567,-21.989,-62.478,-21.705,-62.385,-21.412,-62.277,-21.066,-62.277,-20.821,-62.276,-20.562,-62.122,-20.35,-62.012,-20.199,-61.917,-20.055,-61.821,-19.809,-61.757,-19.645,-61.512,-19.606,-61.096,-19.521,-60.889,-19.479,-60.452,-19.389,-60.007,-19.298,-59.541,-19.292,-59.091,-19.286,-58.741,-19.49,-58.474,-19.646,-58.18,-19.818,-58.16,-19.855,-58.14,-19.999,-58.16,-20.165]]]},{"n":"Papua New Guinea","c":"PG","r":2,"ml":2.5,"lx":143.91,"ly":-5.7,"b":[140.862,-11.631,155.958,-1.353],"p":[[[152.966,-4.756,152.892,-4.832,152.846,-4.762,152.787,-4.699,152.74,-4.636,152.681,-4.498,152.678,-4.429,152.693,-4.356,152.697,-4.282,152.668,-4.132,152.598,-3.995,152.356,-3.668,152.279,-3.582,152.192,-3.506,152.136,-3.487,152.023,-3.469,151.973,-3.453,151.88,-3.4,151.793,-3.338,151.579,-3.154,151.465,-3.101,151.405,-3.037,151.067,-2.829,150.968,-2.78,150.848,-2.78,150.746,-2.739,150.826,-2.713,150.843,-2.644,150.825,-2.573,150.995,-2.688,151.175,-2.789,151.226,-2.87,151.315,-2.875,151.475,-2.942,151.586,-3.003,151.69,-3.073,151.807,-3.173,152.033,-3.251,152.065,-3.28,152.179,-3.41,152.329,-3.521,152.38,-3.582,153.017,-4.106,153.124,-4.252,153.133,-4.352,153.112,-4.392,153.044,-4.476,153.046,-4.576,153.023,-4.666,152.966,-4.756]],[[151.916,-4.297,151.968,-4.317,152.117,-4.212,152.197,-4.285,152.299,-4.321,152.406,-4.341,152.364,-4.491,152.376,-4.56,152.404,-4.629,152.4,-4.731,152.351,-4.822,152.258,-4.955,152.216,-4.979,152.167,-4.993,152.013,-5.004,151.984,-5.074,151.994,-5.149,152.077,-5.247,152.143,-5.357,152.077,-5.458,151.968,-5.529,151.865,-5.565,151.695,-5.544,151.515,-5.552,151.481,-5.591,151.48,-5.655,151.455,-5.703,151.422,-5.747,151.331,-5.839,151.229,-5.92,151.09,-5.997,151.043,-6.015,150.92,-6.027,150.809,-6.071,150.76,-6.114,150.706,-6.149,150.588,-6.188,150.474,-6.263,150.428,-6.276,150.191,-6.289,149.851,-6.293,149.75,-6.301,149.653,-6.29,149.598,-6.261,149.483,-6.125,149.382,-6.078,149.273,-6.079,149.127,-6.128,149.099,-6.117,148.808,-5.916,148.719,-5.867,148.625,-5.831,148.51,-5.805,148.401,-5.765,148.337,-5.669,148.345,-5.545,148.432,-5.472,148.565,-5.508,148.616,-5.507,148.666,-5.487,148.724,-5.493,148.783,-5.512,148.999,-5.485,149.124,-5.523,149.245,-5.573,149.359,-5.584,149.475,-5.573,149.632,-5.516,149.681,-5.524,149.831,-5.524,149.963,-5.448,150.012,-5.14,150.045,-5.035,150.09,-5.012,150.122,-5.018,150.17,-5.071,150.109,-5.136,150.082,-5.186,150.072,-5.31,150.106,-5.429,150.183,-5.524,150.299,-5.536,150.404,-5.473,150.519,-5.46,150.626,-5.521,150.734,-5.51,150.784,-5.471,150.843,-5.454,150.9,-5.447,150.953,-5.424,151.022,-5.321,151.069,-5.204,151.138,-5.113,151.327,-4.96,151.381,-4.941,151.44,-4.931,151.573,-4.937,151.671,-4.883,151.679,-4.761,151.665,-4.637,151.552,-4.346,151.544,-4.299,151.561,-4.247,151.593,-4.201,151.704,-4.2,151.819,-4.217,151.865,-4.261,151.916,-4.297]],[[140.976,-9.119,140.976,-9.106,140.976,-8.902,140.976,-8.699,140.976,-8.496,140.976,-8.292,140.976,-8.089,140.976,-7.886,140.975,-7.683,140.975,-7.479,140.975,-7.276,140.975,-7.073,140.975,-6.905,140.92,-6.84,140.862,-6.74,140.875,-6.612,140.944,-6.452,140.975,-6.346,140.975,-6.259,140.975,-6.056,140.975,-5.853,140.975,-5.65,140.975,-5.446,140.975,-5.243,140.975,-5.04,140.974,-4.836,140.974,-4.633,140.974,-4.43,140.974,-4.226,140.974,-4.023,140.974,-3.82,140.974,-3.617,140.974,-3.413,140.974,-3.21,140.974,-3.007,140.974,-2.803,140.973,-2.681,140.973,-2.614,140.973,-2.61,141.003,-2.61,141.105,-2.611,141.186,-2.628,141.687,-2.845,141.837,-2.932,141.888,-2.953,141.938,-2.953,141.986,-2.964,142.212,-3.083,142.549,-3.205,142.905,-3.321,143.016,-3.345,143.13,-3.355,143.378,-3.395,143.509,-3.431,143.701,-3.573,143.797,-3.617,143.888,-3.697,144.016,-3.784,144.066,-3.805,144.122,-3.815,144.248,-3.818,144.374,-3.803,144.427,-3.81,144.478,-3.825,144.525,-3.855,144.548,-3.913,144.627,-3.993,144.738,-4.029,144.843,-4.101,144.938,-4.188,145.008,-4.275,145.088,-4.349,145.208,-4.38,145.335,-4.385,145.767,-4.823,145.788,-4.891,145.793,-5.178,145.745,-5.402,145.853,-5.471,145.999,-5.497,146.205,-5.545,146.403,-5.617,147.034,-5.919,147.121,-5.945,147.248,-5.955,147.377,-5.951,147.423,-5.966,147.519,-6.021,147.567,-6.057,147.653,-6.155,147.73,-6.261,147.763,-6.292,147.802,-6.315,147.825,-6.373,147.854,-6.551,147.846,-6.662,147.81,-6.704,147.71,-6.724,147.356,-6.742,147.119,-6.722,146.954,-6.834,146.949,-6.883,146.961,-6.929,147.105,-7.167,147.19,-7.378,147.26,-7.464,147.365,-7.534,147.459,-7.616,147.545,-7.711,147.724,-7.876,147.822,-7.937,147.936,-7.975,148.127,-8.104,148.152,-8.16,148.206,-8.339,148.23,-8.46,148.234,-8.51,148.247,-8.554,148.414,-8.664,148.451,-8.695,148.526,-8.939,148.583,-9.052,148.679,-9.092,148.792,-9.089,149.097,-9.017,149.142,-9.015,149.198,-9.031,149.248,-9.071,149.264,-9.181,149.216,-9.296,149.203,-9.407,149.263,-9.498,149.419,-9.569,149.476,-9.588,149.756,-9.611,149.866,-9.63,149.974,-9.661,150.011,-9.688,149.985,-9.737,149.928,-9.761,149.864,-9.771,149.761,-9.806,149.763,-9.869,149.821,-9.934,149.874,-10.013,149.919,-10.042,149.968,-10.061,150.089,-10.088,150.206,-10.126,150.284,-10.163,150.364,-10.19,150.539,-10.207,150.667,-10.257,150.85,-10.236,150.691,-10.318,150.637,-10.338,150.446,-10.307,150.41,-10.339,150.489,-10.426,150.605,-10.484,150.647,-10.518,150.618,-10.558,150.482,-10.637,150.426,-10.649,150.32,-10.655,150.142,-10.621,150.017,-10.577,149.982,-10.518,149.948,-10.483,149.835,-10.399,149.754,-10.353,149.651,-10.337,149.544,-10.338,149.353,-10.29,148.937,-10.255,148.838,-10.234,148.713,-10.167,148.654,-10.157,148.591,-10.178,148.431,-10.191,148.383,-10.185,148.269,-10.128,148.15,-10.107,148.101,-10.125,148.051,-10.128,147.89,-10.087,147.769,-10.07,147.669,-10.013,147.614,-9.96,147.553,-9.912,147.496,-9.79,147.408,-9.675,147.299,-9.58,147.064,-9.426,147.017,-9.388,146.925,-9.247,146.93,-9.154,146.964,-9.06,146.913,-9.092,146.856,-9.088,146.697,-9.025,146.631,-8.951,146.524,-8.75,146.456,-8.644,146.296,-8.456,146.251,-8.344,146.184,-8.246,146.143,-8.21,146.109,-8.168,146.079,-8.114,146.033,-8.076,145.811,-7.993,145.772,-7.966,145.729,-7.952,145.563,-7.944,145.468,-7.93,145.288,-7.862,145.194,-7.841,145.082,-7.828,144.974,-7.802,144.921,-7.777,144.885,-7.734,144.864,-7.632,144.773,-7.642,144.684,-7.625,144.598,-7.589,144.51,-7.567,144.45,-7.598,144.431,-7.679,144.403,-7.684,144.352,-7.667,144.326,-7.677,144.27,-7.714,144.225,-7.765,144.143,-7.757,143.974,-7.706,143.898,-7.674,143.834,-7.616,143.779,-7.55,143.723,-7.498,143.655,-7.46,143.742,-7.55,143.942,-7.944,143.892,-7.952,143.841,-7.942,143.888,-8.018,143.833,-8.029,143.779,-8.028,143.665,-7.996,143.552,-7.985,143.518,-8.001,143.542,-8.029,143.582,-8.113,143.614,-8.2,143.45,-8.24,143.282,-8.264,143.095,-8.311,142.905,-8.314,142.808,-8.287,142.709,-8.272,142.615,-8.287,142.524,-8.322,142.448,-8.316,142.399,-8.255,142.376,-8.208,142.347,-8.167,142.276,-8.174,142.207,-8.196,142.325,-8.198,142.361,-8.25,142.391,-8.313,142.475,-8.369,142.576,-8.336,142.798,-8.345,143.014,-8.444,143.065,-8.455,143.112,-8.475,143.223,-8.572,143.307,-8.661,143.377,-8.762,143.392,-8.802,143.388,-8.908,143.366,-8.961,143.227,-9.036,143.078,-9.092,142.859,-9.203,142.647,-9.328,142.536,-9.303,142.435,-9.237,142.396,-9.219,142.293,-9.183,142.23,-9.17,141.979,-9.198,141.727,-9.213,141.622,-9.211,141.519,-9.19,141.406,-9.151,141.294,-9.168,141.217,-9.214,141.133,-9.221,140.976,-9.119]],[[148.026,-5.826,147.985,-5.834,147.968,-5.789,147.875,-5.749,147.781,-5.627,147.783,-5.522,147.795,-5.492,147.846,-5.491,148.055,-5.612,148.076,-5.65,148.06,-5.765,148.026,-5.826]],[[146.019,-4.726,145.952,-4.756,145.904,-4.733,145.884,-4.667,145.9,-4.604,145.959,-4.554,145.996,-4.539,146.037,-4.573,146.053,-4.64,146.019,-4.726]],[[152.671,-3.133,152.646,-3.221,152.585,-3.17,152.543,-3.096,152.57,-3.062,152.639,-3.043,152.671,-3.133]],[[152.099,-2.947,152.088,-2.998,152.057,-2.995,151.971,-2.896,151.955,-2.871,151.975,-2.846,152.075,-2.918,152.099,-2.947]],[[149.765,-1.553,149.763,-1.589,149.691,-1.571,149.671,-1.576,149.546,-1.472,149.548,-1.408,149.581,-1.353,149.633,-1.362,149.725,-1.431,149.765,-1.553]],[[151.081,-10.02,151.123,-10.02,151.194,-9.946,151.256,-9.923,151.296,-9.957,151.231,-10.195,151.175,-10.159,150.959,-10.093,150.952,-9.998,150.896,-9.968,150.861,-9.876,150.79,-9.774,150.776,-9.709,150.817,-9.736,150.862,-9.802,151.051,-9.939,151.044,-9.983,151.081,-10.02]],[[150.345,-9.494,150.331,-9.519,150.273,-9.5,150.11,-9.362,150.135,-9.26,150.208,-9.206,150.32,-9.264,150.357,-9.349,150.368,-9.396,150.345,-9.494]],[[151.107,-8.733,151.124,-8.805,151.046,-8.728,151.081,-8.642,151.087,-8.595,151.083,-8.569,151.005,-8.524,151.046,-8.451,151.09,-8.426,151.118,-8.419,151.116,-8.522,151.139,-8.568,151.107,-8.733]],[[154.281,-11.361,154.266,-11.416,154.23,-11.397,154.121,-11.426,154.064,-11.419,154.031,-11.371,154.023,-11.348,154.118,-11.366,154.102,-11.311,154.238,-11.339,154.281,-11.361]],[[143.587,-8.482,143.543,-8.485,143.367,-8.417,143.322,-8.368,143.528,-8.379,143.581,-8.391,143.593,-8.46,143.587,-8.482]],[[147.176,-5.432,147.12,-5.437,147.029,-5.342,147.006,-5.307,147.015,-5.257,147.131,-5.191,147.206,-5.252,147.222,-5.382,147.176,-5.432]],[[147.068,-1.96,147.401,-2.025,147.423,-2.024,147.419,-2.001,147.424,-1.995,147.444,-2.012,147.438,-2.059,147.385,-2.071,147.337,-2.066,147.301,-2.09,147.206,-2.182,147.142,-2.167,147.064,-2.187,146.926,-2.189,146.748,-2.149,146.699,-2.183,146.635,-2.173,146.572,-2.21,146.546,-2.209,146.531,-2.154,146.532,-2.126,146.607,-2.103,146.596,-2.017,146.656,-1.974,146.76,-1.978,146.857,-1.949,147.068,-1.96]],[[150.437,-2.662,150.238,-2.675,150.166,-2.66,150.102,-2.603,150.043,-2.512,149.985,-2.492,149.962,-2.474,150.103,-2.405,150.227,-2.384,150.429,-2.47,150.45,-2.513,150.452,-2.541,150.446,-2.632,150.437,-2.662]],[[150.528,-9.347,150.669,-9.429,150.746,-9.404,150.789,-9.418,150.879,-9.513,150.884,-9.582,150.899,-9.641,150.894,-9.667,150.844,-9.703,150.848,-9.663,150.81,-9.655,150.678,-9.657,150.576,-9.631,150.436,-9.625,150.495,-9.562,150.508,-9.536,150.435,-9.435,150.431,-9.387,150.437,-9.36,150.499,-9.346,150.528,-9.347]],[[152.631,-8.959,152.689,-8.975,152.81,-8.967,152.85,-9.025,152.905,-9.044,152.953,-9.07,152.995,-9.108,152.995,-9.131,152.985,-9.151,152.959,-9.169,152.967,-9.209,152.923,-9.203,152.867,-9.224,152.759,-9.177,152.72,-9.167,152.708,-9.126,152.638,-9.058,152.515,-9.01,152.577,-8.97,152.631,-8.959]],[[153.536,-11.476,153.703,-11.529,153.76,-11.586,153.7,-11.613,153.554,-11.631,153.519,-11.595,153.379,-11.56,153.357,-11.495,153.287,-11.517,153.322,-11.471,153.234,-11.42,153.207,-11.352,153.204,-11.324,153.307,-11.356,153.536,-11.476]],[[143.59,-8.633,143.608,-8.677,143.463,-8.617,143.324,-8.517,143.254,-8.49,143.207,-8.423,143.293,-8.473,143.443,-8.519,143.59,-8.633]],[[147.877,-2.283,147.845,-2.336,147.769,-2.331,147.736,-2.316,147.79,-2.306,147.812,-2.262,147.836,-2.247,147.877,-2.283]],[[151.957,-2.83,151.933,-2.83,151.93,-2.751,151.946,-2.709,152.002,-2.738,152.011,-2.809,151.957,-2.83]],[[150.899,-10.565,150.885,-10.643,150.802,-10.62,150.786,-10.603,150.799,-10.554,150.872,-10.552,150.899,-10.565]],[[153.659,-4.099,153.65,-4.123,153.592,-4.096,153.64,-4.045,153.663,-4.041,153.659,-4.099]],[[155.958,-6.687,155.933,-6.78,155.915,-6.797,155.892,-6.762,155.805,-6.796,155.763,-6.834,155.719,-6.863,155.617,-6.856,155.521,-6.83,155.427,-6.783,155.344,-6.722,155.261,-6.626,155.209,-6.527,155.234,-6.412,155.202,-6.308,155.045,-6.234,155.01,-6.21,154.94,-6.106,154.87,-6.061,154.782,-5.971,154.759,-5.931,154.721,-5.817,154.709,-5.747,154.741,-5.545,154.729,-5.444,154.773,-5.454,154.818,-5.494,154.871,-5.521,154.997,-5.54,155.094,-5.62,155.187,-5.777,155.198,-5.828,155.228,-5.865,155.323,-5.932,155.373,-5.974,155.467,-6.145,155.519,-6.182,155.581,-6.196,155.638,-6.221,155.734,-6.296,155.823,-6.38,155.882,-6.47,155.928,-6.565,155.958,-6.687]],[[154.647,-5.433,154.627,-5.441,154.584,-5.314,154.576,-5.221,154.563,-5.152,154.54,-5.111,154.606,-5.035,154.633,-5.014,154.682,-5.054,154.689,-5.143,154.727,-5.218,154.698,-5.383,154.647,-5.433]]]},{"n":"Panama","c":"PA","r":4,"ml":4,"lx":-80.35,"ly":8.72,"b":[-83.027,7.22,-77.196,9.598],"p":[[[-77.374,8.658,-77.393,8.645,-77.448,8.566,-77.479,8.498,-77.407,8.427,-77.386,8.352,-77.346,8.27,-77.283,8.187,-77.212,8.034,-77.196,7.972,-77.216,7.933,-77.283,7.908,-77.346,7.837,-77.363,7.749,-77.351,7.706,-77.538,7.566,-77.587,7.543,-77.619,7.565,-77.659,7.635,-77.706,7.691,-77.732,7.711,-77.747,7.712,-77.762,7.699,-77.769,7.668,-77.744,7.537,-77.765,7.484,-77.828,7.443,-77.901,7.229,-77.93,7.256,-78.17,7.544,-78.378,7.9,-78.422,8.061,-78.368,8.071,-78.315,8.067,-78.287,8.092,-78.255,8.139,-78.281,8.248,-78.18,8.33,-78.142,8.386,-78.114,8.38,-78.048,8.285,-77.952,8.23,-77.834,8.151,-77.761,8.133,-77.853,8.216,-78.012,8.325,-78.057,8.397,-78.099,8.497,-78.162,8.454,-78.191,8.417,-78.223,8.397,-78.251,8.421,-78.256,8.454,-78.35,8.46,-78.374,8.489,-78.399,8.506,-78.388,8.443,-78.369,8.405,-78.379,8.359,-78.41,8.355,-78.436,8.403,-78.469,8.447,-78.514,8.628,-78.621,8.714,-78.67,8.742,-78.71,8.753,-78.77,8.811,-78.848,8.842,-78.955,8.933,-79.086,8.997,-79.247,9.02,-79.442,9.006,-79.507,8.97,-79.552,8.924,-79.572,8.903,-79.687,8.851,-79.731,8.775,-79.759,8.712,-79.816,8.639,-79.75,8.596,-80.126,8.35,-80.2,8.314,-80.369,8.289,-80.408,8.262,-80.459,8.214,-80.466,8.14,-80.458,8.077,-80.409,8.029,-80.366,7.998,-80.261,7.852,-80.075,7.667,-80.04,7.6,-80.011,7.5,-80.067,7.453,-80.111,7.433,-80.287,7.426,-80.348,7.386,-80.373,7.325,-80.439,7.275,-80.667,7.226,-80.846,7.22,-80.901,7.277,-80.915,7.438,-81.035,7.711,-81.064,7.9,-81.094,7.876,-81.158,7.854,-81.179,7.808,-81.195,7.668,-81.219,7.621,-81.268,7.625,-81.37,7.675,-81.504,7.721,-81.676,8.016,-81.694,8.071,-81.728,8.138,-81.86,8.165,-81.973,8.215,-82.097,8.223,-82.16,8.195,-82.224,8.23,-82.235,8.311,-82.365,8.275,-82.531,8.287,-82.68,8.322,-82.781,8.304,-82.866,8.246,-82.854,8.1,-82.879,8.071,-82.883,8.131,-82.913,8.2,-82.948,8.257,-83.023,8.316,-83.027,8.338,-82.998,8.368,-82.862,8.454,-82.845,8.489,-82.843,8.564,-82.856,8.635,-82.917,8.74,-82.882,8.805,-82.812,8.857,-82.74,8.899,-82.728,8.916,-82.741,8.952,-82.783,8.99,-82.881,9.056,-82.94,9.06,-82.943,9.249,-82.94,9.449,-82.925,9.469,-82.889,9.481,-82.86,9.511,-82.844,9.571,-82.801,9.592,-82.723,9.546,-82.644,9.506,-82.611,9.519,-82.587,9.539,-82.569,9.558,-82.564,9.577,-82.5,9.523,-82.371,9.429,-82.363,9.382,-82.375,9.337,-82.34,9.209,-82.272,9.191,-82.205,9.215,-82.188,9.192,-82.201,9.168,-82.235,9.142,-82.244,9.031,-82.133,8.98,-82.078,8.935,-81.894,8.956,-81.826,8.944,-81.78,8.957,-81.831,9.046,-81.9,9.111,-81.894,9.14,-81.842,9.119,-81.803,9.074,-81.712,9.019,-81.546,8.827,-81.355,8.781,-81.204,8.787,-81.063,8.813,-80.839,8.887,-80.676,9.022,-80.547,9.082,-80.127,9.21,-79.978,9.344,-79.915,9.361,-79.855,9.378,-79.723,9.479,-79.652,9.558,-79.577,9.598,-79.355,9.569,-79.212,9.532,-79.112,9.537,-79.017,9.51,-78.975,9.453,-78.932,9.428,-78.697,9.435,-78.504,9.406,-78.083,9.236,-77.831,9.068,-77.697,8.889,-77.374,8.658]],[[-78.898,8.274,-78.918,8.232,-78.965,8.326,-78.957,8.351,-78.961,8.436,-78.916,8.458,-78.883,8.46,-78.856,8.448,-78.839,8.348,-78.853,8.302,-78.898,8.274]],[[-81.603,7.333,-81.658,7.328,-81.77,7.37,-81.852,7.453,-81.859,7.48,-81.857,7.508,-81.812,7.592,-81.752,7.622,-81.729,7.621,-81.671,7.523,-81.71,7.486,-81.695,7.425,-81.613,7.38,-81.603,7.333]],[[-82.233,9.381,-82.244,9.334,-82.322,9.418,-82.276,9.432,-82.259,9.43,-82.233,9.381]],[[-79.065,8.254,-79.11,8.21,-79.128,8.252,-79.096,8.295,-79.085,8.296,-79.065,8.254]]]},{"n":"Palau","c":"PW","r":6,"ml":5,"lx":134.58,"ly":7.52,"b":[131.135,3.022,134.66,7.712],"p":[[[131.172,3.026,131.15,3.022,131.135,3.025,131.137,3.039,131.152,3.054,131.172,3.061,131.188,3.056,131.186,3.042,131.172,3.026]],[[134.595,7.382,134.535,7.361,134.506,7.437,134.516,7.526,134.556,7.594,134.6,7.616,134.609,7.624,134.651,7.712,134.66,7.663,134.633,7.501,134.598,7.438,134.595,7.382]]]},{"n":"Pakistan","c":"PK","r":2,"ml":2.7,"lx":68.55,"ly":29.33,"b":[60.843,23.753,77.049,37.037],"p":[[[76.767,35.662,76.813,35.572,76.882,35.436,76.928,35.347,76.979,35.246,77.004,35.196,77.049,35.11,77.031,35.062,77.001,34.992,76.892,34.939,76.783,34.9,76.758,34.878,76.749,34.848,76.696,34.787,76.594,34.736,76.51,34.741,76.457,34.756,76.172,34.668,76.041,34.67,75.938,34.613,75.862,34.56,75.709,34.503,75.606,34.503,75.453,34.537,75.264,34.601,75.188,34.639,75.118,34.637,74.952,34.646,74.789,34.678,74.594,34.716,74.498,34.732,74.3,34.765,74.172,34.721,74.056,34.681,73.961,34.653,73.883,34.529,73.85,34.485,73.812,34.422,73.795,34.378,73.81,34.325,73.925,34.288,73.972,34.237,73.979,34.191,73.938,34.145,73.904,34.108,73.904,34.076,73.922,34.043,73.95,34.019,74.113,34.004,74.209,34.003,74.246,33.99,74.251,33.946,74.216,33.887,74.078,33.839,74.001,33.788,73.976,33.721,73.978,33.668,74.004,33.632,74.07,33.592,74.131,33.545,74.15,33.507,74.143,33.455,74.118,33.384,74.05,33.301,73.994,33.242,73.99,33.221,74.004,33.189,74.049,33.143,74.126,33.075,74.222,33.02,74.284,33.005,74.304,32.992,74.323,32.928,74.33,32.861,74.305,32.81,74.355,32.769,74.483,32.771,74.588,32.753,74.632,32.771,74.663,32.758,74.643,32.608,74.658,32.519,74.686,32.494,74.789,32.458,74.987,32.462,75.104,32.42,75.234,32.372,75.303,32.319,75.333,32.279,75.325,32.215,75.254,32.14,75.139,32.105,75.071,32.089,74.739,31.949,74.636,31.89,74.556,31.819,74.526,31.765,74.51,31.713,74.582,31.524,74.594,31.465,74.535,31.261,74.518,31.186,74.54,31.133,74.61,31.113,74.626,31.069,74.633,31.035,74.51,30.96,74.38,30.893,74.339,30.894,74.216,30.769,74.009,30.52,73.899,30.435,73.892,30.394,73.883,30.352,73.925,30.282,73.933,30.222,73.887,30.162,73.809,30.093,73.658,30.033,73.467,29.972,73.382,29.934,73.317,29.773,73.258,29.611,73.231,29.551,73.128,29.364,72.949,29.089,72.903,29.029,72.626,28.896,72.342,28.752,72.292,28.697,72.234,28.566,72.179,28.422,72.129,28.346,71.948,28.177,71.889,28.047,71.87,27.963,71.717,27.915,71.543,27.87,71.29,27.855,71.185,27.832,70.875,27.714,70.798,27.71,70.737,27.729,70.692,27.769,70.649,27.835,70.629,27.937,70.569,27.984,70.489,28.023,70.404,28.025,70.318,27.982,70.244,27.934,70.194,27.895,70.145,27.849,70.05,27.695,69.896,27.474,69.725,27.313,69.661,27.265,69.622,27.228,69.568,27.175,69.537,27.123,69.495,26.954,69.47,26.804,69.481,26.771,69.507,26.743,69.601,26.699,69.736,26.627,69.911,26.586,70.059,26.579,70.115,26.548,70.148,26.506,70.157,26.471,70.149,26.348,70.133,26.215,70.078,26.072,70.079,25.99,70.1,25.91,70.265,25.707,70.325,25.686,70.449,25.681,70.506,25.685,70.57,25.706,70.615,25.692,70.648,25.667,70.657,25.626,70.652,25.423,70.703,25.331,70.8,25.206,70.878,25.063,70.951,24.892,71.021,24.758,71.048,24.688,71.002,24.654,70.976,24.619,70.97,24.572,70.979,24.522,70.973,24.487,71.006,24.444,71.045,24.43,71.044,24.4,70.983,24.361,70.928,24.362,70.886,24.344,70.805,24.262,70.767,24.245,70.716,24.238,70.659,24.246,70.579,24.279,70.556,24.331,70.565,24.386,70.547,24.418,70.489,24.412,70.289,24.356,70.098,24.288,70.065,24.241,70.021,24.192,69.934,24.171,69.805,24.165,69.716,24.173,69.634,24.225,69.559,24.273,69.443,24.275,69.235,24.268,69.12,24.269,69.052,24.286,68.985,24.273,68.901,24.292,68.863,24.267,68.828,24.264,68.8,24.309,68.781,24.314,68.759,24.307,68.74,24.292,68.728,24.266,68.724,23.965,68.587,23.967,68.489,23.967,68.381,23.951,68.283,23.928,68.234,23.901,68.165,23.857,68.149,23.797,68.116,23.753,68.068,23.818,68.037,23.848,68.001,23.826,67.951,23.829,67.86,23.903,67.819,23.828,67.668,23.811,67.65,23.867,67.646,23.92,67.563,23.882,67.504,23.94,67.477,24.018,67.454,24.04,67.428,24.065,67.365,24.092,67.309,24.175,67.304,24.263,67.289,24.368,67.171,24.756,67.101,24.792,66.703,24.861,66.682,24.929,66.71,25.111,66.699,25.226,66.57,25.379,66.534,25.484,66.429,25.575,66.324,25.602,66.219,25.59,66.162,25.554,66.131,25.493,66.356,25.507,66.407,25.485,66.468,25.445,66.403,25.447,66.328,25.466,66.235,25.464,65.884,25.42,65.68,25.355,65.406,25.374,65.061,25.311,64.777,25.307,64.659,25.184,64.594,25.206,64.544,25.237,64.152,25.333,64.125,25.374,64.059,25.403,63.987,25.351,63.936,25.343,63.721,25.386,63.557,25.353,63.496,25.298,63.491,25.211,63.286,25.228,63.17,25.255,63.015,25.225,62.665,25.265,62.572,25.255,62.445,25.197,62.391,25.153,62.315,25.135,62.249,25.197,62.199,25.225,62.152,25.207,62.089,25.155,61.908,25.131,61.744,25.138,61.567,25.186,61.588,25.202,61.615,25.286,61.64,25.585,61.671,25.692,61.662,25.751,61.669,25.769,61.738,25.821,61.754,25.843,61.781,25.996,61.81,26.165,61.842,26.226,61.87,26.242,62.089,26.318,62.126,26.369,62.239,26.357,62.25,26.369,62.26,26.427,62.312,26.491,62.385,26.543,62.439,26.561,62.636,26.594,62.752,26.639,62.787,26.644,63.093,26.632,63.158,26.65,63.168,26.666,63.186,26.838,63.242,26.865,63.25,26.879,63.231,26.998,63.242,27.078,63.305,27.125,63.302,27.151,63.256,27.208,63.196,27.244,63.167,27.252,62.915,27.218,62.812,27.229,62.763,27.25,62.753,27.266,62.763,27.3,62.764,27.357,62.801,27.445,62.812,27.497,62.782,27.801,62.74,28.002,62.763,28.202,62.758,28.244,62.749,28.253,62.718,28.253,62.565,28.235,62.434,28.364,62.353,28.415,62.131,28.479,62.033,28.491,61.89,28.547,61.758,28.668,61.623,28.792,61.569,28.871,61.509,29.006,61.338,29.265,61.339,29.332,61.318,29.373,61.152,29.543,61.034,29.663,60.843,29.859,61.224,29.749,61.521,29.666,62.001,29.53,62.373,29.425,62.477,29.408,63.568,29.498,63.971,29.43,64.099,29.392,64.118,29.414,64.172,29.46,64.266,29.507,64.394,29.544,64.521,29.565,64.704,29.567,64.827,29.564,64.919,29.553,65.096,29.559,65.18,29.578,65.471,29.652,65.666,29.701,65.962,29.779,66.177,29.836,66.231,29.866,66.287,29.92,66.313,29.969,66.247,30.044,66.238,30.11,66.282,30.193,66.305,30.321,66.301,30.503,66.287,30.608,66.347,30.803,66.397,30.912,66.497,30.965,66.567,30.997,66.596,31.02,66.624,31.046,66.731,31.195,66.829,31.264,66.924,31.306,67.028,31.3,67.116,31.243,67.287,31.218,67.453,31.235,67.596,31.278,67.662,31.313,67.738,31.344,67.733,31.379,67.647,31.41,67.598,31.453,67.578,31.506,67.627,31.539,67.74,31.548,68.017,31.678,68.13,31.763,68.161,31.803,68.214,31.807,68.32,31.768,68.443,31.754,68.521,31.794,68.598,31.803,68.673,31.76,68.714,31.708,68.782,31.646,68.869,31.634,68.973,31.667,69.083,31.738,69.187,31.838,69.279,31.937,69.257,32.249,69.241,32.434,69.29,32.531,69.359,32.59,69.405,32.683,69.405,32.764,69.453,32.833,69.502,33.02,69.568,33.064,69.704,33.095,69.92,33.113,70.09,33.198,70.261,33.289,70.284,33.369,70.22,33.455,70.134,33.621,70.057,33.72,69.868,33.898,69.89,34.007,69.995,34.052,70.254,33.976,70.326,33.961,70.416,33.95,70.654,33.952,70.848,33.982,71.052,34.05,71.091,34.12,71.089,34.204,71.092,34.273,71.096,34.369,71.023,34.431,70.979,34.486,70.966,34.53,71.016,34.555,71.066,34.6,71.113,34.682,71.226,34.78,71.294,34.868,71.358,34.91,71.455,34.967,71.517,35.051,71.546,35.101,71.602,35.151,71.621,35.183,71.605,35.212,71.577,35.248,71.546,35.289,71.546,35.329,71.572,35.37,71.601,35.408,71.587,35.461,71.572,35.547,71.519,35.598,71.484,35.715,71.428,35.834,71.398,35.88,71.343,35.939,71.22,36.001,71.185,36.042,71.233,36.122,71.313,36.171,71.463,36.293,71.546,36.378,71.621,36.436,71.716,36.427,71.773,36.432,71.822,36.486,71.921,36.534,72.096,36.634,72.157,36.701,72.25,36.735,72.327,36.742,72.431,36.766,72.531,36.802,72.623,36.83,72.766,36.835,72.994,36.852,73.117,36.869,73.411,36.882,73.732,36.888,73.769,36.888,73.908,36.853,74.002,36.823,74.039,36.826,74.195,36.897,74.431,36.984,74.541,37.022,74.601,37.037,74.692,37.036,74.766,37.013,74.841,36.979,74.889,36.952,74.949,36.968,75.054,36.987,75.145,36.973,75.347,36.913,75.377,36.884,75.424,36.738,75.46,36.725,75.574,36.759,75.667,36.742,75.772,36.695,75.84,36.65,75.885,36.601,75.933,36.522,75.952,36.458,75.974,36.382,75.969,36.169,75.934,36.134,75.905,36.088,75.912,36.049,75.945,36.018,76.01,35.996,76.071,35.983,76.103,35.949,76.148,35.829,76.178,35.811,76.252,35.811,76.386,35.837,76.502,35.878,76.551,35.887,76.563,35.773,76.632,35.729,76.728,35.679,76.767,35.662]]]},{"n":"Oman","c":"OM","r":4,"ml":4,"lx":57.34,"ly":22.12,"b":[51.978,16.648,59.838,26.356],"p":[[[58.722,20.219,58.659,20.204,58.641,20.211,58.641,20.337,58.788,20.497,58.884,20.681,58.951,20.516,58.835,20.424,58.772,20.267,58.722,20.219]],[[53.086,16.648,53.025,16.78,52.964,16.912,52.904,17.044,52.843,17.176,52.801,17.268,52.729,17.3,52.686,17.398,52.642,17.498,52.597,17.598,52.553,17.698,52.509,17.798,52.465,17.897,52.42,17.997,52.376,18.097,52.332,18.197,52.288,18.297,52.243,18.397,52.199,18.497,52.155,18.597,52.11,18.696,52.066,18.796,52.022,18.896,51.978,18.996,52.119,19.043,52.291,19.1,52.463,19.158,52.635,19.215,52.807,19.272,52.979,19.33,53.151,19.387,53.323,19.444,53.495,19.502,53.667,19.559,53.839,19.617,54.011,19.674,54.183,19.731,54.355,19.788,54.527,19.846,54.699,19.903,54.871,19.96,54.977,19.996,55.021,20.129,55.058,20.24,55.095,20.351,55.131,20.461,55.168,20.572,55.205,20.683,55.241,20.793,55.278,20.904,55.314,21.015,55.351,21.126,55.388,21.236,55.424,21.347,55.461,21.458,55.498,21.568,55.534,21.679,55.571,21.79,55.607,21.9,55.641,22.002,55.578,22.1,55.493,22.231,55.404,22.368,55.32,22.497,55.259,22.591,55.186,22.704,55.194,22.85,55.192,22.923,55.2,23.035,55.27,23.19,55.353,23.387,55.414,23.519,55.466,23.633,55.508,23.725,55.532,23.819,55.519,23.885,55.492,23.91,55.468,23.941,55.548,23.991,55.697,24.024,55.779,24.017,55.894,24.041,55.985,24.063,55.992,24.093,55.966,24.143,55.929,24.215,55.8,24.223,55.761,24.243,55.806,24.35,55.804,24.384,55.787,24.424,55.768,24.491,55.778,24.577,55.804,24.636,55.804,24.684,55.792,24.781,55.796,24.868,55.823,24.911,55.871,24.951,55.916,24.972,55.963,24.97,56.001,24.953,56.017,24.908,56.006,24.876,55.98,24.872,55.97,24.859,56.008,24.798,56.064,24.739,56.107,24.749,56.154,24.796,56.205,24.833,56.268,24.867,56.314,24.931,56.353,24.973,56.388,24.979,56.49,24.716,56.641,24.47,56.774,24.335,56.913,24.15,57.123,23.981,57.22,23.923,57.611,23.804,57.825,23.759,58.12,23.717,58.325,23.624,58.393,23.618,58.5,23.646,58.578,23.643,58.773,23.517,58.83,23.397,58.912,23.334,58.983,23.235,59.03,23.131,59.195,22.972,59.311,22.793,59.429,22.661,59.535,22.579,59.696,22.546,59.823,22.509,59.838,22.421,59.824,22.305,59.8,22.22,59.681,22.054,59.653,21.951,59.518,21.782,59.371,21.499,59.304,21.435,59.069,21.289,58.896,21.113,58.69,20.807,58.534,20.504,58.474,20.407,58.349,20.387,58.266,20.395,58.209,20.424,58.232,20.507,58.245,20.599,58.169,20.59,58.103,20.57,57.947,20.344,57.862,20.244,57.844,20.118,57.802,19.955,57.741,19.804,57.714,19.678,57.715,19.607,57.761,19.432,57.764,19.253,57.79,19.146,57.812,19.017,57.738,18.977,57.676,18.958,57.428,18.944,57.177,18.903,56.957,18.828,56.826,18.754,56.655,18.587,56.551,18.166,56.383,17.988,56.27,17.951,55.998,17.935,55.614,17.886,55.479,17.843,55.255,17.586,55.238,17.505,55.281,17.446,55.296,17.382,55.275,17.321,55.174,17.158,55.064,17.039,54.772,16.965,54.665,17.009,54.567,17.031,54.377,17.034,54.068,17.006,53.954,16.918,53.775,16.856,53.61,16.76,53.298,16.723,53.086,16.648]],[[56.298,25.651,56.279,25.628,56.25,25.625,56.184,25.645,56.145,25.691,56.152,25.746,56.154,25.848,56.173,25.945,56.167,26.047,56.117,26.068,56.08,26.063,56.164,26.207,56.197,26.229,56.228,26.22,56.306,26.235,56.346,26.314,56.379,26.356,56.413,26.351,56.43,26.327,56.418,26.208,56.416,26.109,56.374,25.805,56.329,25.752,56.307,25.709,56.298,25.651]],[[56.282,25.236,56.24,25.209,56.211,25.213,56.217,25.267,56.234,25.304,56.277,25.301,56.288,25.279,56.282,25.236]]]},{"n":"Norway","c":"NO","r":3,"ml":3,"lx":9.68,"ly":61.36,"b":[-9.099,58.021,33.629,80.478],"p":[[[20.622,69.037,20.492,69.033,20.117,69.021,20.282,68.934,20.337,68.9,20.348,68.849,20.319,68.754,20.24,68.673,20.147,68.607,19.969,68.542,20.24,68.478,20.056,68.39,19.97,68.356,19.87,68.362,19.691,68.392,19.259,68.465,19.053,68.493,18.868,68.501,18.77,68.5,18.379,68.562,18.303,68.555,18.163,68.528,18.147,68.468,18.156,68.317,18.177,68.201,18.125,68.133,18.073,68.088,17.917,67.965,17.565,68.048,17.325,68.104,17.171,68.03,16.784,67.895,16.586,67.628,16.574,67.62,16.457,67.552,16.307,67.521,16.194,67.505,16.127,67.426,16.282,67.312,16.361,67.252,16.434,67.155,16.421,67.093,16.404,67.055,16.238,66.976,15.884,66.769,15.557,66.552,15.423,66.49,15.484,66.306,15.375,66.252,15.153,66.191,15.04,66.168,14.918,66.154,14.543,66.129,14.61,65.932,14.635,65.845,14.635,65.793,14.596,65.743,14.55,65.646,14.48,65.301,14.426,65.264,14.352,65.171,14.115,64.946,13.925,64.797,13.65,64.582,13.874,64.514,14.078,64.464,14.12,64.388,14.148,64.26,14.141,64.174,14.063,64.096,14.003,64.041,13.961,64.014,13.671,64.041,13.3,64.075,13.204,64.075,12.988,64.05,12.793,64,12.69,63.957,12.663,63.94,12.533,63.844,12.302,63.671,12.175,63.596,12.212,63.492,12,63.292,12.139,63.089,12.145,63.083,12.218,63.001,12.141,62.948,12.109,62.919,12.12,62.826,12.14,62.721,12.122,62.66,12.115,62.592,12.304,62.286,12.301,62.214,12.292,62.167,12.234,61.977,12.155,61.721,12.292,61.653,12.487,61.573,12.596,61.541,12.758,61.446,12.881,61.352,12.864,61.29,12.828,61.222,12.776,61.174,12.728,61.108,12.706,61.06,12.683,61.047,12.468,61.042,12.354,61.023,12.294,61.003,12.315,60.892,12.445,60.69,12.554,60.546,12.589,60.451,12.553,60.354,12.516,60.305,12.515,60.239,12.486,60.107,12.402,60.04,12.292,59.967,12.169,59.913,12.072,59.898,11.988,59.891,11.932,59.864,11.881,59.782,11.834,59.697,11.681,59.592,11.685,59.556,11.743,59.431,11.798,59.29,11.752,59.158,11.712,59.019,11.643,58.926,11.544,58.893,11.471,58.91,11.388,59.037,11.386,59.066,11.366,59.105,11.132,59.143,11.091,59.142,10.999,59.164,10.945,59.171,10.834,59.184,10.743,59.296,10.645,59.389,10.631,59.428,10.634,59.602,10.604,59.68,10.595,59.765,10.534,59.696,10.57,59.587,10.494,59.542,10.398,59.519,10.407,59.456,10.446,59.444,10.455,59.377,10.431,59.28,10.243,59.062,10.205,59.039,10.179,59.009,10.083,59.029,9.96,58.968,9.843,58.958,9.8,59.027,9.635,59.118,9.557,59.113,9.627,59.068,9.696,59.01,9.657,58.971,9.618,58.946,9.551,58.933,9.31,58.857,9.396,58.806,9.323,58.748,9.239,58.739,9.194,58.712,9.178,58.675,8.928,58.57,8.521,58.301,8.312,58.224,8.166,58.145,8.037,58.147,7.876,58.08,7.466,58.021,7.194,58.048,7.005,58.024,6.903,58.071,6.89,58.102,6.895,58.121,6.912,58.143,6.877,58.151,6.803,58.155,6.771,58.132,6.767,58.082,6.731,58.068,6.591,58.097,6.555,58.123,6.606,58.176,6.692,58.224,6.677,58.234,6.66,58.263,6.618,58.266,6.492,58.259,6.389,58.268,6.055,58.375,5.977,58.432,5.707,58.524,5.586,58.62,5.517,58.727,5.522,58.823,5.556,58.975,5.612,59.013,5.854,58.959,6.099,58.87,6.137,58.875,6.214,58.945,6.363,59.001,6.321,59.016,6.099,58.952,6.017,58.988,5.889,59.06,5.889,59.098,5.949,59.135,5.969,59.186,5.937,59.234,5.952,59.299,6.051,59.368,6.199,59.438,6.306,59.506,6.415,59.547,6.404,59.561,6.279,59.535,6.159,59.49,6.017,59.414,5.845,59.353,5.718,59.33,5.657,59.31,5.564,59.291,5.468,59.204,5.362,59.166,5.173,59.163,5.132,59.226,5.185,59.454,5.242,59.564,5.305,59.643,5.404,59.656,5.472,59.713,5.53,59.713,5.579,59.687,5.772,59.661,5.867,59.734,5.991,59.745,6.217,59.818,6.212,59.832,6.059,59.816,5.967,59.813,5.834,59.795,5.763,59.808,5.73,59.863,5.784,59.913,5.996,60.031,6.07,60.083,6.112,60.132,6.105,60.165,6.141,60.233,6.349,60.353,6.518,60.408,6.574,60.361,6.526,60.214,6.527,60.153,6.661,60.367,6.72,60.418,6.787,60.454,6.95,60.478,6.996,60.512,6.806,60.501,6.347,60.419,6.153,60.346,6.102,60.29,5.967,60.206,5.904,60.151,5.877,60.07,5.801,60.026,5.699,60.01,5.557,59.908,5.495,59.826,5.353,59.76,5.264,59.71,5.234,59.692,5.186,59.642,5.146,59.639,5.111,59.668,5.105,59.732,5.119,59.834,5.187,59.907,5.22,59.979,5.174,60.046,5.206,60.088,5.265,60.086,5.376,60.067,5.495,60.07,5.689,60.123,5.658,60.154,5.574,60.158,5.417,60.154,5.286,60.206,5.184,60.308,5.137,60.446,5.168,60.485,5.546,60.625,5.648,60.688,5.589,60.694,5.447,60.617,5.244,60.57,5.116,60.636,5.049,60.708,5.011,60.859,5.025,60.936,5.009,61.038,5.095,61.071,5.192,61.054,5.288,61.047,5.505,61.056,5.984,61.117,6.293,61.081,6.418,61.084,6.61,61.137,6.778,61.142,6.903,61.102,6.972,61.056,6.981,60.994,7.039,60.953,7.078,60.966,7.047,61.015,7.04,61.091,7.545,61.177,7.604,61.211,7.404,61.222,7.347,61.301,7.453,61.419,7.443,61.435,7.331,61.372,7.276,61.284,7.298,61.214,7.276,61.181,7.174,61.166,6.943,61.161,6.794,61.19,6.657,61.207,6.61,61.229,6.626,61.279,6.6,61.29,6.543,61.245,6.493,61.155,6.383,61.134,6.083,61.167,5.647,61.148,5.451,61.102,5.325,61.108,5.107,61.188,5.022,61.251,4.99,61.378,5.003,61.434,5.172,61.457,5.258,61.455,5.339,61.485,5.268,61.505,5.168,61.543,5.099,61.62,4.997,61.645,4.928,61.711,4.91,61.81,4.93,61.878,4.985,61.9,5.117,61.885,5.465,61.897,5.793,61.827,6.016,61.788,6.467,61.807,6.731,61.87,6.682,61.887,6.396,61.851,6.131,61.852,5.664,61.923,5.473,61.946,5.267,61.936,5.16,61.957,5.096,62.027,5.143,62.16,5.241,62.189,5.294,62.154,5.358,62.152,5.422,62.207,5.484,62.239,5.533,62.311,5.718,62.379,5.796,62.385,5.908,62.416,5.98,62.407,6.026,62.376,6.083,62.35,6.209,62.353,6.58,62.407,6.62,62.423,6.692,62.468,6.457,62.448,6.262,62.416,6.136,62.407,6.118,62.447,6.165,62.482,6.238,62.52,6.273,62.584,6.353,62.611,6.439,62.61,6.618,62.621,6.745,62.638,6.961,62.627,7.284,62.602,7.492,62.543,7.57,62.548,7.653,62.564,7.691,62.586,7.527,62.61,7.518,62.646,7.538,62.672,7.805,62.721,8.096,62.732,8.046,62.771,7.408,62.712,7.242,62.752,7.111,62.752,7.025,62.729,6.78,62.701,6.735,62.721,6.782,62.79,6.928,62.903,6.94,62.93,7.008,62.958,7.389,63.023,7.572,63.1,7.654,63.109,7.736,63.104,7.86,63.113,8.101,63.091,8.211,62.996,8.311,62.966,8.623,62.846,8.609,62.881,8.339,63.042,8.235,63.082,8.158,63.162,8.184,63.237,8.271,63.287,8.58,63.313,8.636,63.342,8.641,63.392,8.594,63.426,8.48,63.424,8.387,63.445,8.361,63.499,8.398,63.535,8.576,63.601,8.674,63.623,8.842,63.646,9.136,63.594,9.158,63.566,9.076,63.5,9.084,63.463,9.156,63.459,9.324,63.57,9.521,63.586,9.602,63.61,9.697,63.625,9.832,63.524,9.892,63.492,9.936,63.479,9.979,63.395,10.021,63.391,10.081,63.433,10.189,63.455,10.34,63.469,10.591,63.447,10.704,63.464,10.76,63.461,10.707,63.536,10.674,63.558,10.725,63.625,10.779,63.651,10.953,63.698,11.118,63.719,11.226,63.764,11.371,63.805,11.348,63.838,11.308,63.876,11.214,63.878,11.176,63.899,11.295,63.948,11.458,64.003,11.429,64.025,11.307,64.049,11.214,64.031,11.075,63.988,10.914,63.921,10.967,63.902,11.047,63.845,10.935,63.77,10.339,63.571,10.055,63.513,9.924,63.522,9.893,63.576,9.832,63.617,9.767,63.7,9.657,63.697,9.595,63.679,9.567,63.706,9.615,63.795,9.708,63.865,9.864,63.918,9.939,63.982,10.01,64.083,10.236,64.18,10.566,64.418,10.834,64.494,10.932,64.578,11.09,64.615,11.225,64.679,11.331,64.686,11.524,64.744,11.633,64.814,11.562,64.818,11.392,64.773,11.297,64.755,11.304,64.829,11.35,64.906,11.489,64.976,12.16,65.179,12.227,65.145,12.307,65.086,12.508,65.099,12.738,65.214,12.916,65.339,12.82,65.317,12.715,65.266,12.512,65.195,12.418,65.184,12.364,65.193,12.334,65.241,12.263,65.256,12.2,65.245,12.134,65.279,12.122,65.362,12.206,65.486,12.273,65.568,12.345,65.63,12.628,65.806,12.689,65.902,12.817,65.953,12.983,65.942,13.033,65.956,12.976,66.019,12.795,66.069,12.784,66.1,13.387,66.183,13.674,66.18,13.76,66.221,13.916,66.247,14.034,66.298,13.973,66.32,13.681,66.274,13.499,66.252,13.416,66.253,13.352,66.237,13.119,66.231,13.068,66.431,13.105,66.539,13.192,66.537,13.211,66.641,13.312,66.702,13.45,66.716,13.52,66.742,13.621,66.795,13.788,66.782,13.959,66.794,13.917,66.819,13.704,66.852,13.652,66.907,13.727,66.938,13.808,66.961,13.88,66.965,14.022,67.073,14.109,67.119,14.206,67.111,14.34,67.159,14.473,67.143,14.601,67.174,14.776,67.194,15.416,67.202,15.435,67.247,15.3,67.257,14.824,67.268,14.582,67.267,14.479,67.256,14.442,67.271,14.448,67.298,14.537,67.34,14.579,67.386,14.755,67.499,14.962,67.574,15.121,67.555,15.289,67.483,15.409,67.474,15.465,67.451,15.553,67.352,15.594,67.349,15.576,67.444,15.692,67.521,15.661,67.543,15.487,67.515,15.354,67.544,15.249,67.602,15.219,67.655,15.284,67.708,15.346,67.734,15.304,67.765,15.041,67.683,14.855,67.663,14.781,67.675,14.821,67.75,14.799,67.809,15.048,67.956,15.134,67.973,15.274,67.961,15.401,67.92,15.507,67.926,15.621,67.948,15.606,67.988,15.357,68.004,15.293,68.036,15.316,68.069,15.487,68.103,15.657,68.164,15.851,68.182,16.008,68.229,16.038,68.218,16.065,68.2,16.121,68.027,16.261,67.887,16.312,67.881,16.259,68.001,16.309,68.036,16.372,68.062,16.392,68.092,16.319,68.102,16.26,68.145,16.175,68.281,16.204,68.317,16.388,68.39,16.619,68.406,16.865,68.355,16.951,68.355,17.094,68.368,17.336,68.41,17.479,68.426,17.553,68.426,17.571,68.447,17.502,68.461,17.48,68.474,17.426,68.482,17.202,68.459,16.585,68.466,16.525,68.491,16.514,68.533,16.58,68.593,16.652,68.626,16.885,68.685,17.131,68.693,17.391,68.799,17.49,68.879,17.546,69.001,17.705,69.1,18.101,69.156,18.117,69.181,18.075,69.233,18.079,69.325,18.188,69.433,18.26,69.471,18.293,69.475,18.379,69.44,18.483,69.365,18.646,69.322,18.859,69.314,18.916,69.336,18.75,69.378,18.624,69.434,18.614,69.491,18.674,69.52,18.767,69.517,18.883,69.523,18.991,69.561,19.007,69.588,19.011,69.624,19.038,69.66,19.197,69.748,19.687,69.805,19.722,69.782,19.696,69.613,19.64,69.504,19.642,69.424,19.737,69.504,19.865,69.722,19.961,69.825,20.069,69.883,20.146,69.897,20.223,69.927,20.324,69.945,20.355,69.922,20.387,69.868,20.333,69.677,20.338,69.617,20.277,69.536,20.044,69.356,20.054,69.333,20.107,69.341,20.198,69.371,20.487,69.542,20.739,69.521,20.743,69.535,20.662,69.585,20.563,69.633,20.533,69.692,20.546,69.851,20.622,69.914,20.84,69.907,20.971,69.916,21.032,69.887,21.163,69.89,21.254,70.003,21.433,70.013,21.59,69.938,21.78,69.887,21.932,69.815,21.975,69.835,21.893,70.004,21.803,70.066,21.608,70.098,21.4,70.174,21.346,70.208,21.356,70.233,21.539,70.258,21.78,70.23,21.996,70.293,22.054,70.276,22.219,70.309,22.322,70.265,22.385,70.278,22.421,70.338,22.685,70.375,22.852,70.34,22.941,70.305,22.983,70.237,23.046,70.102,23.177,70.029,23.258,69.993,23.354,69.983,23.4,70.02,23.31,70.064,23.286,70.105,23.329,70.207,23.379,70.247,23.661,70.4,23.897,70.479,24.038,70.485,24.286,70.662,24.356,70.695,24.42,70.702,24.404,70.745,24.268,70.773,24.263,70.826,24.442,70.892,24.658,71.001,24.765,71.008,24.832,70.978,25.042,70.929,25.171,70.872,25.265,70.844,25.325,70.849,25.376,70.892,25.436,70.912,25.57,70.901,25.65,70.873,25.712,70.87,25.768,70.853,25.781,70.817,25.666,70.777,25.468,70.672,25.274,70.552,25.209,70.489,25.146,70.324,24.994,70.218,24.983,70.144,25.044,70.109,25.212,70.136,25.419,70.235,25.471,70.341,25.988,70.625,26.231,70.783,26.507,70.913,26.661,70.94,26.734,70.854,26.675,70.741,26.558,70.669,26.645,70.636,26.628,70.551,26.601,70.503,26.584,70.454,26.585,70.41,26.666,70.422,26.989,70.511,27.071,70.608,27.147,70.681,27.184,70.744,27.309,70.804,27.546,70.804,27.556,70.827,27.269,70.91,27.235,70.947,27.332,70.997,27.597,71.091,27.733,71.081,27.815,71.059,28.142,71.043,28.392,70.975,28.383,70.869,28.327,70.825,28.272,70.798,27.951,70.718,27.898,70.678,27.999,70.664,28.216,70.704,28.272,70.668,28.203,70.577,28.191,70.44,28.166,70.36,28.166,70.288,28.193,70.249,28.28,70.403,28.31,70.443,28.437,70.501,28.485,70.619,28.609,70.76,28.75,70.842,28.832,70.864,29.102,70.861,29.219,70.83,29.321,70.761,29.398,70.734,29.639,70.705,29.722,70.669,29.738,70.647,29.796,70.643,29.959,70.694,30.065,70.703,30.238,70.622,30.203,70.562,30.213,70.543,30.422,70.547,30.596,70.524,30.926,70.401,30.961,70.344,30.944,70.274,30.469,70.198,30.263,70.125,29.926,70.096,28.781,70.145,28.804,70.093,29.601,69.977,29.647,69.944,29.621,69.874,29.621,69.818,29.636,69.78,29.695,69.745,29.792,69.728,29.99,69.737,30.088,69.718,30.155,69.746,30.18,69.841,30.238,69.862,30.349,69.835,30.397,69.733,30.428,69.722,30.484,69.795,30.595,69.79,30.714,69.796,30.87,69.783,30.924,69.652,30.922,69.606,30.897,69.561,30.861,69.538,30.789,69.529,30.615,69.533,30.38,69.585,30.228,69.634,30.18,69.636,30.16,69.63,30.196,69.581,30.187,69.543,30.164,69.502,30.132,69.464,30.087,69.433,29.994,69.392,29.833,69.36,29.388,69.298,29.353,69.271,29.21,69.097,29.171,69.072,29.119,69.05,28.966,69.022,28.892,69.061,28.833,69.119,28.846,69.177,29.025,69.288,29.192,69.367,29.239,69.394,29.333,69.473,29.142,69.671,28.8,69.731,28.412,69.823,28.269,69.871,28.047,69.972,27.89,70.062,27.748,70.065,27.592,70.042,27.348,69.96,27.206,69.919,27.128,69.906,27.109,69.905,26.934,69.928,26.74,69.933,26.584,69.926,26.525,69.915,26.308,69.782,26.156,69.715,26.072,69.692,26.012,69.653,25.962,69.589,25.85,69.367,25.767,69.283,25.749,69.231,25.768,69.076,25.748,68.99,25.647,68.919,25.575,68.887,25.481,68.881,25.357,68.862,25.249,68.821,25.173,68.765,25.087,68.64,24.941,68.593,24.802,68.606,24.703,68.653,24.491,68.689,24.332,68.712,24.154,68.761,23.997,68.798,23.854,68.806,23.773,68.758,23.707,68.714,23.463,68.678,23.324,68.649,23.144,68.643,23.072,68.674,22.811,68.695,22.501,68.72,22.411,68.72,22.383,68.777,22.3,68.856,22.08,68.993,21.989,69.041,21.82,69.154,21.622,69.271,21.594,69.274,21.461,69.277,21.267,69.274,21.144,69.247,21.066,69.214,21.053,69.187,21.128,69.081,21.104,69.054,21.066,69.042,20.889,69.071,20.676,69.069,20.622,69.037]],[[4.959,61.085,4.87,61.072,4.799,61.083,4.824,61.178,4.862,61.194,4.915,61.199,4.973,61.148,4.959,61.085]],[[5.086,60.308,5.089,60.189,4.997,60.198,4.956,60.243,4.944,60.272,4.951,60.341,4.93,60.412,4.957,60.447,4.991,60.452,5.05,60.389,5.086,60.308]],[[29.956,69.797,29.766,69.768,29.744,69.792,29.786,69.829,29.836,69.906,29.914,69.902,29.993,69.873,30.055,69.838,29.956,69.797]],[[11.968,65.627,11.902,65.596,11.778,65.605,11.765,65.631,11.8,65.684,11.875,65.706,11.972,65.702,12.003,65.679,11.968,65.627]],[[8.471,63.667,8.356,63.665,8.287,63.687,8.451,63.732,8.709,63.774,8.733,63.801,8.765,63.805,8.809,63.771,8.815,63.726,8.787,63.703,8.471,63.667]],[[8.103,63.338,8.005,63.337,7.888,63.352,7.815,63.385,7.804,63.414,7.938,63.45,8.074,63.471,8.136,63.431,8.141,63.366,8.103,63.338]],[[23.441,70.816,23.421,70.784,23.387,70.754,23.305,70.722,23.068,70.594,22.929,70.574,22.885,70.554,22.829,70.542,22.656,70.559,22.605,70.533,22.558,70.516,22.432,70.509,22.359,70.515,22.169,70.562,22.056,70.613,21.995,70.657,22.17,70.656,22.233,70.667,22.35,70.658,22.421,70.703,22.571,70.697,22.858,70.728,22.964,70.711,23.205,70.815,23.28,70.813,23.396,70.843,23.441,70.816]],[[25.586,71.142,25.854,71.104,25.945,71.105,26.078,71.033,26.147,71.04,26.134,70.996,26,70.975,25.791,70.963,25.76,70.954,25.582,70.961,25.482,71.02,25.315,71.034,25.315,71.053,25.423,71.097,25.586,71.142]],[[23.615,70.549,23.634,70.503,23.641,70.464,23.548,70.408,23.333,70.335,23.345,70.315,23.271,70.296,23.159,70.283,23.1,70.296,23.108,70.359,23.091,70.378,23.006,70.353,22.918,70.385,22.918,70.417,22.941,70.445,23.022,70.487,23.158,70.516,23.248,70.505,23.547,70.617,23.579,70.594,23.615,70.549]],[[24.018,70.567,23.827,70.527,23.717,70.562,23.67,70.597,23.663,70.675,23.689,70.723,23.778,70.747,23.837,70.729,23.852,70.714,23.956,70.7,24.078,70.651,24.018,70.567]],[[13.873,68.265,13.932,68.248,14.088,68.253,14.119,68.247,14.097,68.219,14.029,68.188,13.888,68.169,13.824,68.121,13.778,68.105,13.656,68.105,13.584,68.094,13.495,68.052,13.424,68.083,13.404,68.061,13.392,68.021,13.352,68.01,13.229,67.995,13.2,68.087,13.256,68.121,13.3,68.16,13.368,68.167,13.429,68.163,13.538,68.249,13.688,68.273,13.784,68.276,13.873,68.265]],[[12.972,67.874,12.824,67.821,12.878,67.918,12.958,68.015,13.068,68.071,13.123,68.049,13.098,68.003,13.098,67.956,13.075,67.935,12.972,67.874]],[[15.207,68.943,15.337,68.842,15.397,68.784,15.348,68.672,15.222,68.616,15.027,68.606,14.89,68.611,14.804,68.638,14.793,68.668,14.743,68.677,14.612,68.638,14.521,68.633,14.405,68.663,14.373,68.711,14.497,68.772,14.554,68.819,14.69,68.815,14.725,68.8,14.802,68.791,14.849,68.848,14.838,68.887,14.872,68.914,15.038,68.894,15.038,69.001,15.102,69.008,15.128,69.004,15.176,68.982,15.207,68.943]],[[19.767,70.217,19.818,70.205,19.869,70.212,19.91,70.202,19.994,70.149,20.084,70.129,20.088,70.102,20.006,70.076,19.897,70.068,19.781,70.077,19.747,70.11,19.711,70.165,19.613,70.219,19.599,70.266,19.684,70.274,19.767,70.217]],[[20.779,70.09,20.725,70.067,20.643,70.057,20.598,70.071,20.535,70.081,20.464,70.077,20.405,70.119,20.412,70.155,20.493,70.203,20.655,70.231,20.786,70.22,20.819,70.205,20.779,70.09]],[[19.255,70.066,19.344,70.012,19.422,70.017,19.446,70.038,19.5,70.048,19.608,70.019,19.592,69.97,19.442,69.908,19.335,69.82,19.197,69.8,19.131,69.81,19.008,69.76,18.909,69.707,18.807,69.64,18.801,69.605,18.785,69.579,18.41,69.553,18.274,69.535,18.13,69.558,18.062,69.602,18.083,69.626,18.227,69.636,18.232,69.677,18.268,69.702,18.315,69.715,18.349,69.768,18.406,69.782,18.512,69.769,18.584,69.807,18.624,69.813,18.674,69.782,18.698,69.825,18.674,69.864,18.687,69.891,18.824,69.96,18.883,70.011,18.969,70.043,19.051,70.038,19.075,70.086,19.051,70.135,19.06,70.167,19.133,70.244,19.213,70.247,19.249,70.179,19.255,70.066]],[[12.51,65.902,12.429,65.899,12.43,65.94,12.476,65.977,12.549,66.002,12.642,66.009,12.747,66.011,12.779,65.992,12.719,65.964,12.51,65.902]],[[12.42,66.043,12.327,66.037,12.343,66.081,12.418,66.123,12.446,66.151,12.461,66.185,12.527,66.211,12.621,66.178,12.623,66.122,12.576,66.072,12.42,66.043]],[[11.231,64.866,11.179,64.838,11.063,64.86,10.833,64.843,10.74,64.87,10.813,64.923,11.021,64.979,11.133,64.976,11.246,64.908,11.231,64.866]],[[17.503,69.596,17.623,69.539,17.677,69.557,17.784,69.563,17.863,69.543,17.927,69.507,18.004,69.505,18.052,69.458,18.077,69.396,18.021,69.35,17.942,69.329,17.921,69.274,17.951,69.198,17.774,69.172,17.568,69.16,17.488,69.197,17.324,69.13,17.161,69.026,17.083,69.014,17.077,69.047,16.96,69.069,16.81,69.071,16.815,69.095,16.843,69.112,16.972,69.138,16.998,69.191,16.974,69.285,16.997,69.33,17.002,69.362,17.083,69.399,17.361,69.381,17.395,69.417,17.373,69.439,17.23,69.478,17.252,69.504,17.356,69.527,17.454,69.53,17.483,69.57,17.488,69.587,17.503,69.596]],[[15.76,68.561,15.772,68.554,15.909,68.65,16.06,68.681,16.069,68.714,16.127,68.746,16.121,68.799,16.151,68.842,16.228,68.854,16.276,68.868,16.329,68.876,16.425,68.842,16.48,68.803,16.547,68.717,16.519,68.633,16.338,68.568,16.194,68.538,16.048,68.464,15.975,68.402,15.913,68.389,15.873,68.394,15.837,68.409,15.764,68.409,15.683,68.356,15.438,68.313,15.341,68.325,15.337,68.378,15.28,68.374,15.188,68.31,15.098,68.289,15.038,68.283,14.927,68.307,14.629,68.198,14.35,68.178,14.258,68.191,14.257,68.257,14.438,68.342,14.586,68.4,15.095,68.441,15.413,68.616,15.489,68.805,15.564,68.874,15.529,68.912,15.444,68.919,15.438,68.979,15.483,69.043,15.65,69.133,15.742,69.171,15.893,69.278,15.965,69.302,16.048,69.302,16.129,69.274,16.115,69.216,15.993,69.113,15.812,69.024,15.834,68.961,15.906,68.908,15.924,68.819,15.928,68.733,15.791,68.617,15.76,68.561]],[[-8.954,70.839,-9.046,70.833,-9.099,70.855,-8.965,70.916,-8.521,71.031,-8.344,71.14,-8.001,71.178,-7.979,71.117,-8.002,71.041,-8.302,70.981,-8.635,70.94,-8.954,70.839]],[[19.219,74.391,19.099,74.352,18.918,74.411,18.797,74.486,18.861,74.514,19.183,74.518,19.262,74.479,19.275,74.457,19.219,74.391]],[[26.876,78.649,26.729,78.646,26.46,78.72,26.408,78.784,26.456,78.81,26.586,78.811,26.789,78.724,27.008,78.698,26.876,78.649]],[[32.526,80.119,31.578,80.081,31.482,80.108,33.019,80.218,33.099,80.229,33.384,80.242,33.629,80.217,33.557,80.198,32.526,80.119]],[[21.608,78.596,21.746,78.572,22.043,78.577,22.207,78.408,22.3,78.228,22.449,78.215,22.735,78.24,22.989,78.252,23.119,78.239,23.352,78.186,23.452,78.149,23.365,78.121,23.152,78.088,23.117,77.992,23.331,77.958,23.684,77.875,23.883,77.865,24.238,77.899,24.571,77.834,24.902,77.757,24.13,77.658,24.062,77.631,23.955,77.558,23.841,77.498,23.736,77.462,23.505,77.401,23.381,77.38,23.101,77.385,22.997,77.361,22.9,77.311,22.802,77.276,22.554,77.267,22.427,77.316,22.469,77.331,22.487,77.36,22.442,77.429,22.679,77.5,22.733,77.539,22.685,77.554,22.62,77.55,22.448,77.571,22.397,77.57,22.255,77.529,22.057,77.501,21.856,77.494,21.05,77.441,20.928,77.46,20.873,77.565,21.201,77.619,21.251,77.711,21.334,77.772,21.431,77.812,21.608,77.916,21.653,77.924,21.21,78.006,21.035,78.059,20.845,78.166,20.786,78.252,20.528,78.326,20.56,78.419,20.373,78.412,20.228,78.478,20.363,78.515,21.047,78.557,21.455,78.598,21.608,78.596]],[[20.898,80.25,20.998,80.239,21.549,80.243,21.655,80.218,21.697,80.159,21.781,80.139,21.898,80.132,22.19,80.06,22.29,80.049,22.376,80.09,22.443,80.19,22.446,80.308,22.418,80.366,22.451,80.402,22.549,80.416,22.672,80.413,22.793,80.433,22.897,80.469,23.008,80.474,23.251,80.447,23.315,80.425,23.25,80.381,23.225,80.318,23.115,80.187,23.353,80.179,23.688,80.207,23.773,80.244,23.953,80.305,24.143,80.295,24.234,80.303,24.28,80.329,24.298,80.36,24.403,80.355,24.547,80.295,24.614,80.286,24.736,80.301,24.786,80.301,24.907,80.277,25.471,80.233,25.667,80.21,25.751,80.188,25.836,80.175,26.437,80.175,26.861,80.16,27.017,80.125,27.148,80.059,27.199,79.907,27.08,79.865,26.221,79.677,26.006,79.617,25.902,79.561,25.726,79.44,25.641,79.403,25.239,79.345,25.145,79.339,24.843,79.367,24.751,79.365,24.383,79.302,24.257,79.263,24.133,79.215,23.948,79.194,23.759,79.206,22.904,79.231,22.789,79.264,22.696,79.329,22.866,79.412,21.911,79.381,20.861,79.398,20.806,79.41,20.761,79.442,20.4,79.463,20.128,79.49,19.9,79.534,19.675,79.591,19.747,79.618,19.821,79.634,20.015,79.64,20.187,79.632,20.493,79.633,20.565,79.691,20.687,79.707,20.784,79.749,20.461,79.775,20.123,79.779,19.899,79.744,19.638,79.729,19.4,79.727,18.942,79.736,18.725,79.761,18.428,79.825,18.325,79.86,18.285,79.887,18.255,79.929,18.595,79.967,18.726,79.996,18.856,80.037,18.344,80.06,18.129,80.093,17.917,80.143,18.089,80.171,18.779,80.194,18.962,80.175,19.143,80.139,19.343,80.116,19.537,80.163,19.355,80.185,19.191,80.263,19.157,80.302,19.178,80.332,19.264,80.336,19.327,80.323,19.568,80.25,19.751,80.227,19.802,80.295,19.81,80.327,19.777,80.353,19.691,80.402,19.614,80.463,19.733,80.478,19.851,80.471,20.104,80.43,20.359,80.401,20.476,80.372,20.693,80.299,20.898,80.25]],[[16.787,79.907,16.838,79.905,16.889,79.915,16.926,79.943,16.966,79.959,17.219,79.941,17.578,79.885,17.685,79.857,17.835,79.8,17.956,79.704,17.86,79.635,17.733,79.57,17.688,79.533,17.734,79.481,17.715,79.431,17.669,79.386,17.861,79.437,18.272,79.601,18.333,79.611,18.397,79.605,18.581,79.572,18.748,79.488,18.785,79.461,18.815,79.427,18.832,79.385,18.823,79.337,18.807,79.303,18.72,79.281,18.678,79.262,18.772,79.26,18.88,79.234,18.979,79.179,19.089,79.157,19.49,79.176,19.751,79.147,19.894,79.056,20.114,79.077,20.114,79.125,20.163,79.146,20.458,79.129,20.611,79.107,20.767,79.059,20.501,78.981,20.72,78.907,21.09,78.853,21.312,78.796,21.353,78.772,21.389,78.74,21.244,78.699,21.096,78.676,20.725,78.672,20.387,78.643,19.769,78.623,19.677,78.61,19.655,78.598,19.619,78.562,19.381,78.48,19.15,78.379,19.056,78.319,18.984,78.234,18.958,78.182,19.009,78.132,18.995,78.081,18.822,78.042,18.712,78.04,18.575,78.048,18.439,78.025,18.431,77.991,18.439,77.942,18.404,77.794,18.362,77.682,18.299,77.579,18.228,77.523,18.137,77.507,17.847,77.497,17.623,77.399,17.442,77.225,17.349,77.157,17.153,77.049,17.188,77.011,17.249,76.969,17.142,76.895,16.977,76.812,16.98,76.779,17.036,76.72,17.063,76.659,16.935,76.606,16.7,76.579,16.462,76.609,16.346,76.645,16.238,76.702,16.124,76.739,16.004,76.761,15.547,76.886,15.124,77.085,14.738,77.162,14.487,77.199,14.366,77.234,14.248,77.282,14.145,77.336,14.05,77.403,14.004,77.445,13.996,77.508,14.026,77.545,14.071,77.564,14.378,77.58,14.488,77.571,14.596,77.538,14.695,77.525,14.921,77.689,16.206,77.782,16.619,77.799,17.033,77.798,16.969,77.842,16.914,77.898,16.853,77.912,16.54,77.88,16.06,77.847,15.826,77.847,15.585,77.869,15.345,77.857,15.097,77.809,14.847,77.779,14.604,77.766,14.09,77.771,13.963,77.796,13.791,77.854,13.75,77.883,13.714,77.919,13.681,78.028,13.718,78.058,13.77,78.075,13.824,78.085,13.937,78.086,14.048,78.067,14.307,78.005,14.248,78.071,14.995,78.151,15.341,78.221,15.519,78.233,15.698,78.228,15.659,78.265,15.657,78.299,15.784,78.327,15.875,78.339,16.15,78.353,16.777,78.35,17.003,78.369,17.172,78.417,16.992,78.4,16.811,78.397,16.727,78.407,16.535,78.449,16.449,78.504,16.697,78.613,16.783,78.664,16.53,78.656,16.446,78.639,16.158,78.538,15.944,78.493,15.681,78.471,15.417,78.473,15.36,78.488,15.279,78.554,15.254,78.589,15.265,78.608,15.348,78.663,15.392,78.721,15.384,78.771,15.323,78.781,15.225,78.732,15.137,78.664,15.016,78.63,14.892,78.639,14.839,78.666,14.792,78.706,14.744,78.721,14.689,78.721,14.578,78.705,14.467,78.675,14.505,78.631,14.515,78.581,14.468,78.541,14.432,78.492,14.546,78.462,14.638,78.415,14.5,78.392,14.363,78.36,14.238,78.31,14.11,78.271,13.908,78.267,13.655,78.245,13.15,78.238,12.913,78.301,12.87,78.331,12.822,78.351,12.665,78.385,12.435,78.483,12.258,78.595,12.138,78.606,11.962,78.642,11.866,78.674,11.774,78.716,11.746,78.766,11.755,78.812,11.861,78.832,11.611,78.883,11.365,78.95,11.456,78.973,11.548,78.983,12.275,78.904,12.323,78.914,12.403,78.953,12.375,78.966,12.253,78.975,12.087,78.975,12.046,78.983,11.982,79.025,11.926,79.077,11.902,79.112,11.893,79.152,12.016,79.213,12.084,79.268,11.978,79.293,11.679,79.291,11.58,79.283,11.616,79.205,11.521,79.151,11.339,79.109,11.208,79.13,11.107,79.233,10.975,79.305,10.926,79.35,10.888,79.415,10.834,79.463,10.738,79.52,10.725,79.556,10.737,79.582,10.811,79.641,10.755,79.69,10.686,79.734,10.682,79.758,10.746,79.789,10.804,79.799,10.866,79.797,11.05,79.76,11.15,79.717,11.185,79.72,11.251,79.785,11.344,79.799,11.702,79.821,12.102,79.738,12.205,79.719,12.288,79.713,12.245,79.75,12.219,79.798,12.28,79.816,12.602,79.773,12.754,79.776,13.108,79.832,13.693,79.861,13.914,79.817,13.926,79.793,13.921,79.762,13.907,79.752,13.778,79.715,13.039,79.685,12.555,79.569,13.215,79.588,13.334,79.575,13.384,79.481,13.432,79.471,13.601,79.457,13.716,79.429,13.834,79.376,13.957,79.34,14.03,79.344,14.056,79.383,14.026,79.429,14.011,79.482,14.02,79.539,14.04,79.586,14.178,79.619,14.38,79.726,14.594,79.799,14.832,79.766,15.052,79.675,15.251,79.545,15.444,79.407,15.66,79.235,15.764,79.174,15.858,79.16,16.295,78.981,16.344,78.976,16.254,79.112,16.028,79.342,15.875,79.519,15.841,79.587,15.816,79.682,15.826,79.709,15.845,79.734,15.956,79.835,16.1,79.884,16.057,79.954,16.094,80.007,16.246,80.049,16.387,80.053,16.524,80.021,16.787,79.907]],[[11.25,78.611,11.262,78.542,11.424,78.549,11.616,78.475,11.826,78.436,11.885,78.409,11.929,78.375,12.056,78.306,12.116,78.233,11.965,78.225,11.757,78.329,11.587,78.388,11.372,78.439,11.199,78.441,11.121,78.463,10.841,78.645,10.789,78.687,10.628,78.754,10.558,78.838,10.558,78.903,10.773,78.888,10.961,78.846,11.124,78.753,11.153,78.724,11.078,78.686,11.155,78.641,11.25,78.611]],[[18.742,80.301,18.525,80.246,18.162,80.288,18.206,80.332,18.292,80.358,18.519,80.348,18.742,80.301]],[[29.047,78.912,29.345,78.906,29.645,78.922,29.697,78.905,29.311,78.852,28.881,78.88,28.495,78.887,28.038,78.829,27.889,78.852,28.121,78.908,28.374,78.927,28.415,78.961,28.511,78.967,28.845,78.971,29.047,78.912]]]},{"n":"North Korea","c":"KP","r":3,"ml":3,"lx":126.44,"ly":39.89,"b":[124.349,37.719,130.687,42.998],"p":[[[128.375,38.623,128.339,38.608,128.279,38.524,128.223,38.417,128.169,38.359,128.106,38.327,128.039,38.309,127.905,38.3,127.785,38.308,127.746,38.319,127.579,38.313,127.533,38.305,127.294,38.313,127.17,38.305,127.09,38.284,127.01,38.241,126.94,38.176,126.879,38.106,126.754,37.979,126.667,37.917,126.667,37.828,126.665,37.801,126.634,37.782,126.623,37.79,126.573,37.797,126.37,37.878,126.203,37.829,126.161,37.764,126.117,37.743,126.05,37.87,125.942,37.874,125.769,37.985,125.695,37.963,125.676,37.918,125.582,37.815,125.449,37.73,125.407,37.719,125.358,37.725,125.365,37.748,125.311,37.844,125.102,37.882,125.027,37.923,124.989,37.931,125.193,38.038,125.247,38.057,125.207,38.082,125.163,38.094,124.995,38.078,124.907,38.113,124.779,38.102,124.691,38.129,124.875,38.233,124.883,38.295,124.881,38.342,124.974,38.48,125.067,38.557,125.31,38.665,125.415,38.68,125.492,38.676,125.554,38.686,125.489,38.728,125.424,38.747,125.299,38.743,125.169,38.806,125.157,38.872,125.41,39.288,125.413,39.326,125.374,39.428,125.361,39.527,125.18,39.583,125.1,39.59,124.868,39.702,124.775,39.758,124.739,39.742,124.732,39.652,124.699,39.632,124.638,39.615,124.608,39.717,124.557,39.791,124.404,39.866,124.349,39.907,124.375,39.996,124.362,40.004,124.387,40.104,124.481,40.182,124.712,40.319,124.772,40.384,124.889,40.46,124.942,40.458,124.997,40.465,125.013,40.498,125.026,40.524,125.073,40.547,125.186,40.589,125.314,40.645,125.417,40.66,125.543,40.743,125.594,40.779,125.645,40.779,125.659,40.796,125.688,40.839,125.728,40.867,125.784,40.872,125.875,40.892,125.989,40.905,126.067,40.974,126.093,41.024,126.145,41.078,126.254,41.138,126.329,41.226,126.412,41.321,126.451,41.352,126.49,41.358,126.514,41.394,126.54,41.496,126.578,41.594,126.601,41.641,126.697,41.692,126.722,41.717,126.743,41.725,126.788,41.718,126.847,41.748,126.904,41.781,126.955,41.769,127.007,41.742,127.061,41.687,127.085,41.644,127.128,41.607,127.137,41.555,127.18,41.531,127.271,41.52,127.42,41.484,127.517,41.482,127.572,41.455,127.688,41.44,127.919,41.461,128.013,41.449,128.053,41.416,128.111,41.389,128.149,41.388,128.2,41.433,128.255,41.507,128.291,41.563,128.289,41.607,128.258,41.655,128.182,41.7,128.132,41.769,128.084,41.841,128.056,41.864,128.033,41.898,128.029,41.952,128.045,41.988,128.16,42.012,128.308,42.026,128.427,42.011,128.627,42.021,128.749,42.041,128.84,42.038,128.923,42.038,128.961,42.069,129.077,42.142,129.134,42.169,129.196,42.218,129.205,42.271,129.218,42.313,129.253,42.358,129.314,42.414,129.366,42.439,129.424,42.436,129.485,42.41,129.524,42.385,129.568,42.392,129.604,42.436,129.628,42.444,129.698,42.448,129.72,42.475,129.746,42.604,129.773,42.705,129.779,42.777,129.842,42.894,129.861,42.965,129.898,42.998,129.941,42.996,129.977,42.975,130.022,42.963,130.083,42.974,130.125,42.956,130.151,42.918,130.24,42.892,130.249,42.873,130.247,42.745,130.296,42.685,130.361,42.631,130.45,42.582,130.498,42.571,130.527,42.535,130.554,42.475,130.618,42.416,130.652,42.373,130.658,42.328,130.687,42.303,130.637,42.275,130.569,42.292,130.458,42.302,130.315,42.214,130.236,42.183,130.18,42.097,130.068,42.046,130.007,41.991,129.928,41.897,129.876,41.806,129.756,41.712,129.686,41.595,129.682,41.494,129.759,41.392,129.766,41.304,129.712,41.124,129.742,40.932,129.709,40.857,129.341,40.726,129.245,40.661,129.11,40.491,128.945,40.428,128.843,40.358,128.701,40.318,128.611,40.198,128.511,40.13,128.393,40.089,128.304,40.036,128.106,40.033,127.967,39.996,127.867,39.896,127.568,39.782,127.527,39.696,127.547,39.563,127.549,39.461,127.523,39.377,127.457,39.401,127.422,39.374,127.383,39.296,127.395,39.208,127.497,39.179,127.581,39.143,127.699,39.125,127.786,39.084,127.972,38.898,128.123,38.816,128.163,38.786,128.249,38.745,128.329,38.681,128.375,38.623]],[[124.905,39.536,124.849,39.508,124.846,39.559,124.89,39.602,124.935,39.608,124.905,39.536]]]},{"n":"Nigeria","c":"NG","r":2,"ml":1.7,"lx":7.5,"ly":9.44,"b":[2.686,4.277,14.627,13.873],"p":[[[7.301,4.418,7.204,4.388,7.14,4.395,7.227,4.527,7.271,4.499,7.328,4.487,7.301,4.418]],[[13.606,13.705,13.763,13.49,13.932,13.258,14.064,13.079,14.16,12.613,14.17,12.524,14.178,12.484,14.185,12.447,14.197,12.384,14.273,12.356,14.415,12.344,14.519,12.298,14.581,12.222,14.587,12.209,14.62,12.151,14.627,12.109,14.618,11.987,14.597,11.83,14.562,11.729,14.582,11.591,14.575,11.532,14.56,11.492,14.496,11.446,14.409,11.401,14.202,11.268,14.143,11.249,14.057,11.245,13.981,11.212,13.892,11.14,13.7,10.873,13.535,10.605,13.479,10.383,13.415,10.171,13.27,10.036,13.25,9.96,13.244,9.916,13.239,9.814,13.221,9.645,13.199,9.564,13.175,9.54,13.019,9.488,12.929,9.426,12.876,9.304,12.856,9.171,12.824,9.019,12.807,8.887,12.782,8.818,12.731,8.746,12.652,8.668,12.583,8.624,12.404,8.596,12.311,8.42,12.233,8.282,12.231,8.227,12.156,7.942,12.025,7.728,12.017,7.652,12.016,7.59,11.852,7.401,11.809,7.345,11.767,7.272,11.809,7.202,11.855,7.138,11.861,7.116,11.787,7.056,11.658,6.952,11.58,6.889,11.563,6.855,11.552,6.697,11.529,6.655,11.478,6.597,11.402,6.534,11.325,6.485,11.237,6.451,11.153,6.438,11.106,6.458,11.08,6.506,11.033,6.698,11.009,6.739,10.954,6.777,10.846,6.882,10.738,6.988,10.606,7.063,10.578,7.058,10.556,7.037,10.519,6.93,10.482,6.891,10.413,6.878,10.293,6.877,10.205,6.892,10.186,6.913,10.168,6.959,10.144,6.996,10.039,6.921,9.874,6.803,9.821,6.784,9.78,6.76,9.726,6.65,9.66,6.532,9.574,6.47,9.49,6.419,9.442,6.373,9.373,6.32,9.239,6.186,9.06,6.009,8.997,5.918,8.935,5.781,8.899,5.63,8.859,5.464,8.801,5.197,8.716,5.047,8.641,4.927,8.585,4.833,8.556,4.755,8.544,4.758,8.515,4.725,8.431,4.746,8.394,4.814,8.342,4.825,8.253,4.924,8.234,4.907,8.328,4.656,8.293,4.558,8.029,4.555,7.801,4.522,7.644,4.525,7.566,4.561,7.531,4.655,7.517,4.645,7.509,4.595,7.46,4.555,7.284,4.548,7.207,4.612,7.144,4.684,7.077,4.716,7.087,4.686,7.164,4.616,7.155,4.514,7.013,4.397,6.923,4.391,6.868,4.441,6.839,4.523,6.825,4.645,6.788,4.725,6.768,4.725,6.786,4.652,6.792,4.593,6.793,4.469,6.86,4.373,6.757,4.344,6.715,4.342,6.633,4.34,6.617,4.376,6.602,4.455,6.58,4.476,6.555,4.341,6.5,4.332,6.462,4.333,6.3,4.304,6.264,4.309,6.256,4.334,6.275,4.372,6.271,4.432,6.215,4.385,6.206,4.292,6.173,4.277,6.077,4.291,5.971,4.339,5.906,4.388,5.799,4.456,5.588,4.647,5.554,4.733,5.493,4.839,5.448,4.946,5.383,5.129,5.403,5.142,5.452,5.127,5.476,5.154,5.388,5.174,5.37,5.195,5.364,5.259,5.368,5.338,5.439,5.365,5.501,5.379,5.532,5.426,5.55,5.474,5.386,5.402,5.232,5.484,5.199,5.534,5.216,5.572,5.289,5.577,5.394,5.575,5.457,5.612,5.418,5.625,5.35,5.623,5.325,5.648,5.327,5.708,5.305,5.694,5.276,5.642,5.173,5.603,5.112,5.642,5.106,5.728,5.093,5.767,5.042,5.798,4.861,6.026,4.634,6.217,4.431,6.349,4.126,6.411,3.487,6.409,3.451,6.427,3.49,6.457,3.546,6.477,3.752,6.584,3.717,6.598,3.503,6.531,3.43,6.525,3.336,6.397,2.772,6.376,2.706,6.369,2.708,6.428,2.736,6.596,2.754,6.662,2.775,6.712,2.753,6.772,2.732,6.853,2.721,6.98,2.748,7.02,2.757,7.068,2.751,7.143,2.75,7.395,2.766,7.423,2.784,7.443,2.785,7.477,2.751,7.542,2.719,7.616,2.72,7.723,2.708,7.827,2.686,7.874,2.702,8.05,2.712,8.273,2.703,8.372,2.724,8.442,2.735,8.614,2.733,8.783,2.775,9.049,2.898,9.061,3.045,9.084,3.11,9.188,3.148,9.321,3.136,9.452,3.165,9.495,3.223,9.566,3.329,9.667,3.325,9.778,3.354,9.813,3.405,9.839,3.477,9.852,3.557,9.907,3.602,10.005,3.646,10.16,3.577,10.268,3.578,10.292,3.604,10.351,3.647,10.409,3.68,10.428,3.758,10.413,3.772,10.418,3.784,10.436,3.834,10.607,3.83,10.654,3.757,10.769,3.745,10.85,3.734,10.972,3.716,11.08,3.695,11.12,3.656,11.155,3.639,11.177,3.488,11.395,3.491,11.499,3.554,11.632,3.595,11.696,3.653,11.732,3.665,11.762,3.647,11.8,3.618,11.828,3.612,11.887,3.62,11.927,3.641,11.97,3.633,12.062,3.634,12.202,3.644,12.405,3.647,12.53,3.769,12.622,3.948,12.775,4.039,12.935,4.087,13.055,4.148,13.458,4.191,13.482,4.242,13.501,4.421,13.648,4.559,13.702,4.665,13.733,4.823,13.76,4.922,13.749,5.101,13.743,5.242,13.757,5.362,13.837,5.416,13.859,5.492,13.873,5.838,13.765,6.184,13.664,6.247,13.673,6.3,13.659,6.386,13.604,6.514,13.485,6.59,13.409,6.627,13.364,6.804,13.108,6.871,13.043,6.937,13.008,7.005,12.996,7.057,13,7.106,13.029,7.173,13.086,7.275,13.112,7.358,13.107,7.789,13.338,7.83,13.341,7.956,13.323,8.095,13.291,8.456,13.06,8.751,12.908,8.958,12.857,9.202,12.821,9.616,12.811,9.929,13.135,10.045,13.206,10.185,13.27,10.23,13.281,10.476,13.33,10.959,13.372,11.412,13.354,11.501,13.341,11.693,13.298,11.99,13.192,12.118,13.09,12.319,13.074,12.463,13.094,12.51,13.194,12.655,13.327,12.76,13.38,12.872,13.449,13.048,13.535,13.194,13.573,13.324,13.671,13.427,13.702,13.606,13.705]]]},{"n":"Niger","c":"NE","r":3,"ml":3,"lx":9.5,"ly":17.45,"b":[0.164,11.696,15.963,23.518],"p":[[[13.606,13.705,13.427,13.702,13.324,13.671,13.194,13.573,13.048,13.535,12.872,13.449,12.76,13.38,12.655,13.327,12.51,13.194,12.463,13.094,12.319,13.074,12.118,13.09,11.99,13.192,11.693,13.298,11.501,13.341,11.412,13.354,10.959,13.372,10.476,13.33,10.23,13.281,10.185,13.27,10.045,13.206,9.929,13.135,9.616,12.811,9.202,12.821,8.958,12.857,8.751,12.908,8.456,13.06,8.095,13.291,7.956,13.323,7.83,13.341,7.789,13.338,7.358,13.107,7.275,13.112,7.173,13.086,7.106,13.029,7.057,13,7.005,12.996,6.937,13.008,6.871,13.043,6.804,13.108,6.627,13.364,6.59,13.409,6.514,13.485,6.386,13.604,6.3,13.659,6.247,13.673,6.184,13.664,5.838,13.765,5.492,13.873,5.416,13.859,5.362,13.837,5.242,13.757,5.101,13.743,4.922,13.749,4.823,13.76,4.665,13.733,4.559,13.702,4.421,13.648,4.242,13.501,4.191,13.482,4.148,13.458,4.087,13.055,4.039,12.935,3.948,12.775,3.769,12.622,3.647,12.53,3.644,12.405,3.634,12.202,3.633,12.062,3.641,11.97,3.62,11.927,3.612,11.887,3.618,11.828,3.647,11.8,3.665,11.762,3.653,11.732,3.595,11.696,3.532,11.787,3.45,11.852,3.36,11.88,3.299,11.927,3.267,11.992,3.15,12.118,2.878,12.368,2.85,12.374,2.805,12.384,2.729,12.354,2.681,12.313,2.648,12.297,2.598,12.294,2.469,12.263,2.366,12.222,2.363,12.188,2.413,11.999,2.389,11.897,2.343,11.946,2.194,12.136,2.091,12.278,2.073,12.309,2.058,12.358,2.069,12.379,2.109,12.394,2.204,12.413,2.221,12.427,2.226,12.466,2.212,12.538,2.16,12.636,2.105,12.701,2.074,12.714,2.017,12.716,1.956,12.707,1.841,12.628,1.79,12.613,1.671,12.62,1.565,12.635,1.5,12.676,1.309,12.834,1.097,13.001,1.008,13.025,0.987,13.042,0.973,13.17,0.977,13.325,0.988,13.365,1.077,13.341,1.171,13.33,1.201,13.358,1.126,13.412,1.018,13.468,0.978,13.552,0.947,13.581,0.898,13.611,0.842,13.626,0.786,13.65,0.748,13.675,0.685,13.685,0.618,13.703,0.522,13.84,0.429,13.972,0.374,14.076,0.355,14.139,0.383,14.246,0.355,14.288,0.251,14.396,0.164,14.497,0.185,14.653,0.203,14.783,0.204,14.865,0.217,14.911,0.229,14.964,0.286,14.98,0.433,14.979,0.719,14.955,0.947,14.982,0.96,14.987,1.121,15.126,1.3,15.272,1.569,15.286,1.859,15.302,2.088,15.309,2.421,15.32,2.69,15.33,3.001,15.341,3.011,15.408,3.029,15.425,3.06,15.427,3.289,15.391,3.504,15.356,3.521,15.483,3.71,15.642,3.817,15.674,3.843,15.702,3.877,15.755,3.898,15.838,3.907,15.897,3.947,15.946,3.976,16.036,4.015,16.193,4.121,16.358,4.182,16.582,4.191,16.798,4.203,16.963,4.235,16.996,4.234,17.288,4.233,17.582,4.232,17.831,4.231,18.139,4.23,18.411,4.229,18.704,4.228,18.968,4.228,19.143,4.446,19.185,4.671,19.228,5.001,19.291,5.359,19.36,5.748,19.434,5.837,19.479,6.131,19.732,6.263,19.846,6.527,20.073,6.731,20.248,6.989,20.471,7.263,20.694,7.482,20.873,7.825,21.076,8.343,21.381,8.861,21.686,9.379,21.991,9.896,22.297,10.414,22.602,10.932,22.907,11.45,23.213,11.968,23.518,12.489,23.402,12.984,23.291,13.481,23.18,13.599,23.12,13.863,22.902,14.201,22.624,14.216,22.62,14.231,22.618,14.556,22.783,14.979,22.996,15.089,22.418,15.172,21.922,15.178,21.606,15.182,21.523,15.216,21.467,15.294,21.412,15.607,20.954,15.54,20.875,15.587,20.733,15.668,20.672,15.929,20.4,15.963,20.346,15.949,20.303,15.766,19.983,15.735,19.904,15.699,19.495,15.673,19.207,15.638,18.811,15.596,18.337,15.562,17.937,15.517,17.408,15.474,16.908,15.212,16.634,14.747,16.147,14.368,15.75,14.178,15.485,13.807,14.966,13.642,14.631,13.514,14.456,13.448,14.381,13.506,14.134,13.606,13.705]]]},{"n":"Nicaragua","c":"NI","r":5,"ml":4,"lx":-85.07,"ly":12.67,"b":[-87.67,10.735,-83.158,15.008],"p":[[[-83.158,14.993,-83.185,14.956,-83.216,14.932,-83.28,14.813,-83.302,14.802,-83.306,14.891,-83.344,14.902,-83.389,14.871,-83.414,14.825,-83.375,14.766,-83.341,14.765,-83.299,14.749,-83.188,14.34,-83.212,14.267,-83.281,14.154,-83.347,14.057,-83.412,13.996,-83.494,13.739,-83.567,13.32,-83.514,12.944,-83.541,12.596,-83.518,12.514,-83.511,12.412,-83.565,12.393,-83.596,12.396,-83.627,12.459,-83.624,12.515,-83.591,12.579,-83.578,12.667,-83.593,12.713,-83.625,12.613,-83.682,12.568,-83.718,12.553,-83.754,12.502,-83.716,12.407,-83.667,12.337,-83.651,12.287,-83.669,12.228,-83.68,12.024,-83.698,12.03,-83.716,12.057,-83.767,12.059,-83.773,11.977,-83.769,11.932,-83.813,11.896,-83.829,11.861,-83.793,11.836,-83.753,11.821,-83.705,11.825,-83.664,11.724,-83.652,11.642,-83.745,11.567,-83.777,11.504,-83.829,11.428,-83.859,11.354,-83.868,11.3,-83.832,11.131,-83.768,11.01,-83.714,10.934,-83.642,10.917,-83.659,10.837,-83.713,10.786,-83.811,10.743,-83.919,10.735,-84.096,10.776,-84.168,10.78,-84.197,10.802,-84.205,10.841,-84.256,10.901,-84.348,10.98,-84.402,10.974,-84.489,10.992,-84.634,11.046,-84.701,11.052,-84.797,11.006,-84.909,10.945,-85.179,11.04,-85.368,11.106,-85.539,11.166,-85.584,11.189,-85.621,11.184,-85.654,11.153,-85.691,11.097,-85.703,11.082,-85.722,11.066,-85.744,11.062,-85.745,11.089,-85.829,11.199,-85.961,11.331,-86.469,11.738,-86.656,11.982,-86.756,12.157,-86.851,12.248,-87.125,12.434,-87.188,12.508,-87.46,12.758,-87.668,12.904,-87.67,12.966,-87.585,13.043,-87.543,13.04,-87.498,12.984,-87.424,12.921,-87.39,12.921,-87.339,12.95,-87.337,12.979,-87.059,12.991,-87.009,13.008,-86.959,13.054,-86.933,13.118,-86.929,13.179,-86.918,13.224,-86.874,13.267,-86.792,13.28,-86.729,13.284,-86.711,13.313,-86.73,13.407,-86.764,13.635,-86.771,13.699,-86.759,13.746,-86.734,13.763,-86.61,13.775,-86.377,13.756,-86.332,13.77,-86.238,13.899,-86.151,13.995,-86.089,14.037,-86.04,14.05,-85.984,13.966,-85.787,13.844,-85.753,13.852,-85.734,13.859,-85.728,13.876,-85.731,13.932,-85.682,13.983,-85.58,14.028,-85.477,14.109,-85.374,14.224,-85.284,14.292,-85.208,14.312,-85.179,14.343,-85.198,14.386,-85.192,14.447,-85.161,14.525,-85.117,14.571,-85.059,14.583,-85.037,14.608,-85.049,14.645,-85.037,14.686,-84.985,14.752,-84.86,14.81,-84.789,14.79,-84.73,14.713,-84.646,14.661,-84.538,14.633,-84.454,14.644,-84.394,14.692,-84.34,14.706,-84.292,14.687,-84.267,14.698,-84.264,14.739,-84.239,14.748,-84.192,14.726,-84.151,14.72,-84.114,14.731,-84.1,14.751,-84.093,14.771,-84.066,14.786,-83.972,14.771,-83.867,14.794,-83.751,14.856,-83.674,14.884,-83.635,14.876,-83.59,14.908,-83.537,14.977,-83.415,15.008,-83.158,14.993]]]},{"n":"New Zealand","c":"NZ","r":2,"ml":2,"lx":172.79,"ly":-39.76,"b":[-176.848,-52.57,178.536,-8.546],"p":[[[173.115,-41.279,173.231,-41.284,173.338,-41.211,173.447,-41.151,173.563,-41.102,173.738,-40.989,173.784,-40.972,173.898,-40.951,173.947,-40.924,174.002,-40.918,173.953,-40.985,173.89,-41.007,173.88,-41.031,173.915,-41.07,173.86,-41.124,173.862,-41.192,173.798,-41.272,173.897,-41.239,173.933,-41.187,173.915,-41.158,173.958,-41.1,174.024,-41.072,173.998,-41.028,173.999,-40.993,174.081,-41.006,174.121,-41.005,174.153,-40.991,174.212,-40.985,174.224,-41.024,174.303,-41.02,174.274,-41.069,174.214,-41.126,174.2,-41.16,174.103,-41.217,174.039,-41.242,174.138,-41.248,174.284,-41.172,174.37,-41.104,174.368,-41.188,174.297,-41.264,174.237,-41.312,174.17,-41.327,174.102,-41.366,174.069,-41.429,174.073,-41.472,174.092,-41.505,174.161,-41.562,174.084,-41.671,174.17,-41.657,174.217,-41.678,174.283,-41.741,174.243,-41.813,174.215,-41.85,174.047,-42.003,173.974,-42.081,173.888,-42.13,173.889,-42.212,173.84,-42.271,173.589,-42.474,173.545,-42.518,173.348,-42.841,173.221,-42.977,173.149,-43.023,173.072,-43.06,172.889,-43.124,172.808,-43.198,172.719,-43.259,172.624,-43.272,172.627,-43.3,172.688,-43.315,172.735,-43.355,172.7,-43.4,172.632,-43.428,172.562,-43.436,172.527,-43.465,172.693,-43.444,172.74,-43.468,172.749,-43.517,172.767,-43.562,172.807,-43.621,172.947,-43.659,173.073,-43.676,173.098,-43.704,173.117,-43.798,173.094,-43.844,173.066,-43.875,173.023,-43.885,172.921,-43.891,172.818,-43.87,172.749,-43.813,172.555,-43.831,172.503,-43.844,172.476,-43.833,172.584,-43.774,172.527,-43.739,172.48,-43.727,172.43,-43.746,172.396,-43.778,172.385,-43.83,172.35,-43.859,172.297,-43.868,172.221,-43.825,172.146,-43.764,172.036,-43.702,172.052,-43.74,172.137,-43.834,172.18,-43.896,172.081,-43.946,171.978,-43.984,171.891,-44.007,171.808,-44.042,171.712,-44.097,171.659,-44.117,171.518,-44.118,171.443,-44.136,171.417,-44.209,171.365,-44.255,171.241,-44.264,171.285,-44.279,171.313,-44.302,171.231,-44.521,171.213,-44.612,171.198,-44.768,171.146,-44.912,170.999,-44.911,171.023,-44.937,171.134,-44.978,171.113,-45.039,170.991,-45.151,170.94,-45.216,170.89,-45.374,170.815,-45.519,170.701,-45.684,170.7,-45.714,170.74,-45.756,170.788,-45.792,170.791,-45.844,170.776,-45.871,170.722,-45.878,170.674,-45.896,170.419,-45.941,170.335,-45.992,170.267,-46.083,170.186,-46.161,169.918,-46.334,169.761,-46.48,169.729,-46.521,169.687,-46.552,169.342,-46.621,169.099,-46.631,168.966,-46.613,168.838,-46.578,168.767,-46.566,168.631,-46.588,168.572,-46.611,168.466,-46.588,168.382,-46.605,168.357,-46.588,168.326,-46.546,168.343,-46.489,168.32,-46.447,168.266,-46.419,168.23,-46.386,168.189,-46.362,168.077,-46.353,167.9,-46.368,167.842,-46.366,167.722,-46.227,167.682,-46.193,167.539,-46.149,167.491,-46.155,167.414,-46.229,167.369,-46.242,167.1,-46.249,166.831,-46.225,166.732,-46.198,166.712,-46.134,166.917,-45.957,166.856,-45.981,166.73,-46.053,166.65,-46.042,166.727,-45.963,166.734,-45.928,166.718,-45.889,166.613,-45.955,166.493,-45.964,166.478,-45.903,166.488,-45.832,166.513,-45.812,166.836,-45.775,166.953,-45.75,167.003,-45.712,166.81,-45.699,166.798,-45.646,166.826,-45.603,166.991,-45.532,166.869,-45.55,166.734,-45.544,166.743,-45.468,166.778,-45.41,166.92,-45.408,166.876,-45.368,166.869,-45.311,166.909,-45.307,167.052,-45.383,167.156,-45.411,167.112,-45.354,167.118,-45.318,167.145,-45.302,167.23,-45.29,167.207,-45.28,167.127,-45.266,167.033,-45.222,167.023,-45.177,167.026,-45.124,167.128,-45.051,167.188,-45.094,167.259,-45.082,167.205,-45.048,167.172,-44.997,167.195,-44.963,167.411,-44.828,167.466,-44.958,167.479,-44.915,167.482,-44.874,167.456,-44.838,167.46,-44.802,167.485,-44.771,167.578,-44.741,167.698,-44.641,167.787,-44.595,167.859,-44.625,167.909,-44.665,167.902,-44.625,167.866,-44.592,167.857,-44.501,168.018,-44.359,168.196,-44.224,168.367,-44.082,168.457,-44.031,168.651,-43.972,168.775,-43.996,168.806,-43.992,168.99,-43.89,169.067,-43.863,169.136,-43.9,169.179,-43.913,169.136,-43.82,169.17,-43.777,169.323,-43.702,169.515,-43.624,169.662,-43.591,169.769,-43.538,169.834,-43.537,169.824,-43.497,169.835,-43.459,169.891,-43.462,169.908,-43.447,169.859,-43.426,170.018,-43.349,170.104,-43.265,170.149,-43.248,170.19,-43.222,170.24,-43.164,170.3,-43.145,170.356,-43.154,170.396,-43.182,170.374,-43.135,170.303,-43.108,170.379,-43.066,170.459,-43.038,170.536,-43.058,170.612,-43.092,170.536,-43.041,170.524,-43.009,170.616,-42.972,170.665,-42.961,170.735,-43.03,170.725,-42.975,170.742,-42.927,170.84,-42.849,170.97,-42.718,171.011,-42.764,171.018,-42.819,171.012,-42.885,171.038,-42.862,171.048,-42.802,171.028,-42.696,171.19,-42.5,171.221,-42.479,171.257,-42.465,171.313,-42.46,171.296,-42.431,171.252,-42.402,171.296,-42.303,171.323,-42.189,171.36,-42.08,171.421,-41.973,171.486,-41.795,171.536,-41.758,171.672,-41.745,171.732,-41.72,171.831,-41.655,171.948,-41.539,172.011,-41.445,172.093,-41.202,172.139,-40.947,172.273,-40.759,172.468,-40.622,172.641,-40.518,172.711,-40.497,172.83,-40.49,172.944,-40.519,172.733,-40.544,172.711,-40.605,172.704,-40.668,172.729,-40.724,172.767,-40.773,172.869,-40.82,172.989,-40.848,173.042,-40.954,173.052,-41.079,173.069,-41.186,173.115,-41.279]],[[173.915,-40.864,173.781,-40.922,173.786,-40.881,173.812,-40.794,173.873,-40.749,173.903,-40.746,173.964,-40.713,173.958,-40.787,173.915,-40.864]],[[166.746,-45.656,166.741,-45.705,166.729,-45.73,166.695,-45.73,166.642,-45.724,166.592,-45.702,166.559,-45.708,166.532,-45.7,166.567,-45.644,166.686,-45.615,166.731,-45.639,166.746,-45.656]],[[166.979,-45.18,167.023,-45.3,166.931,-45.277,166.893,-45.241,166.963,-45.18,166.979,-45.18]],[[168.145,-46.862,168.145,-46.902,168.041,-46.888,168.043,-46.933,168.125,-46.956,168.156,-46.988,168.241,-46.979,168.261,-47.027,168.241,-47.07,168.184,-47.102,168.015,-47.117,167.906,-47.18,167.811,-47.17,167.785,-47.176,167.676,-47.243,167.555,-47.264,167.522,-47.259,167.539,-47.199,167.629,-47.142,167.631,-47.088,167.654,-47.044,167.741,-47.014,167.742,-46.957,167.801,-46.907,167.765,-46.798,167.784,-46.7,167.956,-46.694,168.145,-46.862]],[[175.543,-36.279,175.551,-36.334,175.475,-36.314,175.445,-36.273,175.359,-36.231,175.346,-36.218,175.337,-36.135,175.382,-36.095,175.39,-36.078,175.409,-36.071,175.444,-36.115,175.513,-36.177,175.543,-36.279]],[[173.269,-34.935,173.285,-34.981,173.34,-34.948,173.381,-34.896,173.448,-34.844,173.439,-34.929,173.473,-34.947,173.694,-35.006,173.739,-35.055,173.786,-35.069,173.813,-35.041,173.844,-35.026,173.924,-35.057,174.104,-35.143,174.119,-35.172,174.11,-35.216,174.119,-35.263,174.143,-35.3,174.203,-35.309,174.283,-35.254,174.32,-35.247,174.373,-35.325,174.393,-35.369,174.385,-35.367,174.419,-35.411,174.465,-35.454,174.543,-35.582,174.532,-35.627,174.509,-35.667,174.581,-35.786,174.533,-35.794,174.391,-35.774,174.396,-35.797,174.479,-35.884,174.549,-36.007,174.605,-36.081,174.802,-36.309,174.772,-36.391,174.777,-36.445,174.752,-36.491,174.819,-36.612,174.777,-36.65,174.749,-36.774,174.719,-36.796,174.722,-36.841,174.802,-36.853,174.85,-36.873,174.891,-36.909,174.917,-36.865,174.952,-36.853,175.047,-36.912,175.245,-36.971,175.3,-36.993,175.326,-37.041,175.347,-37.156,175.385,-37.207,175.461,-37.217,175.542,-37.201,175.568,-37.159,175.552,-37.046,175.493,-36.866,175.493,-36.807,175.501,-36.748,175.487,-36.69,175.458,-36.634,175.426,-36.592,175.386,-36.556,175.4,-36.501,175.461,-36.476,175.498,-36.523,175.528,-36.579,175.681,-36.747,175.772,-36.735,175.781,-36.805,175.842,-36.875,175.876,-36.958,175.921,-37.205,175.99,-37.437,176.115,-37.538,176.129,-37.587,176.053,-37.562,176.03,-37.576,176.038,-37.601,176.108,-37.645,176.191,-37.667,176.243,-37.664,176.292,-37.68,176.615,-37.831,176.77,-37.89,177.162,-37.986,177.274,-37.993,177.336,-37.991,177.453,-37.957,177.558,-37.897,177.649,-37.808,177.727,-37.706,177.813,-37.656,177.909,-37.617,177.958,-37.581,178.009,-37.555,178.272,-37.567,178.361,-37.618,178.476,-37.66,178.536,-37.692,178.516,-37.758,178.447,-37.854,178.394,-37.96,178.347,-38.201,178.315,-38.444,178.268,-38.551,178.181,-38.634,178.085,-38.694,177.976,-38.722,177.932,-38.86,177.91,-39.022,177.917,-39.062,177.951,-39.095,177.966,-39.142,177.909,-39.24,177.875,-39.225,177.829,-39.145,177.786,-39.111,177.656,-39.086,177.523,-39.074,177.408,-39.081,177.297,-39.116,177.129,-39.186,177.077,-39.222,177.031,-39.267,176.954,-39.368,176.936,-39.491,176.939,-39.555,176.967,-39.605,177.11,-39.673,176.968,-39.911,176.842,-40.158,176.771,-40.228,176.689,-40.293,176.612,-40.442,176.476,-40.57,176.385,-40.668,176.314,-40.769,176.252,-40.877,176.119,-41.029,176.06,-41.13,175.983,-41.213,175.84,-41.32,175.687,-41.412,175.447,-41.538,175.38,-41.58,175.31,-41.611,175.222,-41.574,175.204,-41.535,175.185,-41.449,175.166,-41.417,175.054,-41.391,174.906,-41.433,174.881,-41.424,174.875,-41.404,174.875,-41.278,174.9,-41.243,174.866,-41.223,174.832,-41.231,174.82,-41.263,174.841,-41.291,174.757,-41.325,174.67,-41.326,174.643,-41.313,174.635,-41.289,174.657,-41.251,174.685,-41.218,174.848,-41.059,175.017,-40.848,175.163,-40.622,175.2,-40.505,175.254,-40.289,175.21,-40.199,175.156,-40.115,175.009,-39.952,174.814,-39.86,174.687,-39.847,174.567,-39.813,174.455,-39.735,174.352,-39.643,174.149,-39.568,173.934,-39.509,173.812,-39.426,173.783,-39.376,173.764,-39.319,173.766,-39.265,173.782,-39.211,173.806,-39.17,173.844,-39.139,174.071,-39.031,174.312,-38.971,174.356,-38.972,174.398,-38.963,174.458,-38.926,174.566,-38.842,174.597,-38.785,174.619,-38.605,174.653,-38.428,174.715,-38.226,174.809,-38.1,174.84,-38.023,174.802,-37.896,174.837,-37.849,174.88,-37.821,174.928,-37.804,174.846,-37.685,174.749,-37.505,174.729,-37.449,174.744,-37.393,174.768,-37.339,174.707,-37.325,174.673,-37.273,174.586,-37.098,174.61,-37.07,174.66,-37.089,174.734,-37.215,174.746,-37.15,174.804,-37.11,174.864,-37.089,174.929,-37.085,174.782,-36.944,174.733,-36.949,174.668,-36.972,174.601,-36.986,174.537,-36.973,174.476,-36.942,174.445,-36.883,174.406,-36.768,174.382,-36.726,174.189,-36.492,174.246,-36.485,174.402,-36.602,174.432,-36.565,174.454,-36.511,174.447,-36.451,174.41,-36.406,174.354,-36.376,174.353,-36.323,174.395,-36.274,174.393,-36.24,174.304,-36.171,174.268,-36.163,174.252,-36.196,174.278,-36.244,174.254,-36.249,174.036,-36.122,173.969,-36.021,173.914,-35.909,173.909,-35.954,173.917,-36.018,174.003,-36.146,174.142,-36.289,174.166,-36.328,174.146,-36.377,174.097,-36.391,174.055,-36.36,173.991,-36.237,173.945,-36.176,173.412,-35.543,173.48,-35.459,173.586,-35.389,173.61,-35.357,173.626,-35.319,173.582,-35.313,173.542,-35.33,173.496,-35.362,173.454,-35.399,173.402,-35.481,173.376,-35.5,173.314,-35.443,173.29,-35.408,173.291,-35.366,173.275,-35.34,173.228,-35.331,173.16,-35.248,173.117,-35.205,173.189,-35.124,173.191,-35.016,173.117,-34.903,173.03,-34.8,172.861,-34.632,172.706,-34.455,172.874,-34.433,173.044,-34.429,172.964,-34.535,173,-34.596,173.054,-34.648,173.171,-34.807,173.181,-34.853,173.241,-34.899,173.269,-34.935]],[[169.178,-52.497,169.233,-52.548,169.128,-52.57,169.076,-52.552,169.04,-52.529,169.022,-52.495,169.079,-52.499,169.129,-52.485,169.178,-52.497]],[[166.221,-50.762,166.243,-50.846,166.188,-50.846,166.073,-50.823,166.038,-50.787,166.013,-50.778,165.971,-50.82,165.904,-50.821,165.889,-50.808,165.916,-50.763,166.074,-50.679,166.103,-50.573,166.101,-50.539,166.225,-50.531,166.254,-50.544,166.267,-50.559,166.259,-50.577,166.21,-50.612,166.208,-50.652,166.22,-50.694,166.179,-50.715,166.201,-50.751,166.221,-50.762]],[[-176.178,-43.74,-176.214,-43.766,-176.275,-43.765,-176.382,-43.867,-176.375,-43.791,-176.407,-43.761,-176.499,-43.768,-176.517,-43.785,-176.455,-43.805,-176.441,-43.816,-176.5,-43.86,-176.439,-43.955,-176.385,-43.951,-176.334,-44.025,-176.334,-44.048,-176.453,-44.077,-176.516,-44.117,-176.572,-44.115,-176.598,-44.107,-176.629,-44.036,-176.632,-44.006,-176.563,-43.954,-176.524,-43.901,-176.555,-43.852,-176.635,-43.82,-176.808,-43.835,-176.848,-43.824,-176.761,-43.758,-176.667,-43.765,-176.566,-43.718,-176.178,-43.74]],[[-176.176,-44.322,-176.221,-44.331,-176.215,-44.274,-176.229,-44.237,-176.155,-44.225,-176.123,-44.268,-176.176,-44.322]],[[-172.479,-8.581,-172.484,-8.583,-172.488,-8.572,-172.494,-8.559,-172.499,-8.548,-172.497,-8.546,-172.487,-8.556,-172.481,-8.567,-172.479,-8.581]],[[-171.186,-9.355,-171.189,-9.358,-171.193,-9.352,-171.2,-9.345,-171.204,-9.333,-171.202,-9.333,-171.194,-9.339,-171.189,-9.347,-171.186,-9.355]]]},{"n":"Niue","c":"NU","r":4,"ml":4,"lx":-169.86,"ly":-19.05,"b":[-169.948,-19.138,-169.793,-18.966],"p":[[[-169.803,-19.083,-169.904,-19.138,-169.948,-19.073,-169.909,-18.99,-169.862,-18.969,-169.834,-18.966,-169.793,-19.043,-169.803,-19.083]]]},{"n":"Cook Islands","c":"CK","r":4,"ml":4,"lx":-159.79,"ly":-21.22,"b":[-159.842,-21.25,-159.737,-21.186],"p":[[[-159.741,-21.249,-159.773,-21.25,-159.813,-21.242,-159.84,-21.238,-159.842,-21.229,-159.832,-21.2,-159.811,-21.186,-159.768,-21.188,-159.74,-21.208,-159.737,-21.241,-159.741,-21.249]]]},{"n":"Netherlands","c":"NL","r":5,"ml":4,"lx":5.61,"ly":52.42,"b":[-68.371,12.032,7.197,53.625],"p":[[[5.994,50.75,5.892,50.753,5.797,50.755,5.747,50.76,5.694,50.775,5.669,50.806,5.639,50.844,5.648,50.867,5.737,50.932,5.75,50.95,5.741,50.96,5.75,50.989,5.818,51.086,5.827,51.126,5.796,51.153,5.752,51.169,5.609,51.198,5.54,51.239,5.509,51.275,5.477,51.285,5.43,51.273,5.311,51.26,5.214,51.279,5.1,51.346,5.073,51.407,5.059,51.453,5.031,51.469,4.993,51.445,4.944,51.408,4.848,51.403,4.821,51.412,4.816,51.433,4.811,51.453,4.784,51.477,4.756,51.491,4.634,51.422,4.589,51.422,4.532,51.449,4.503,51.475,4.441,51.46,4.385,51.428,4.404,51.367,4.374,51.356,4.304,51.362,4.226,51.386,4.139,51.402,4.007,51.443,3.822,51.409,3.694,51.45,3.587,51.454,3.521,51.486,3.449,51.541,3.5,51.577,3.549,51.589,3.744,51.596,3.886,51.574,4.141,51.456,4.206,51.457,4.274,51.472,4.239,51.504,4.175,51.519,4.08,51.551,4.005,51.596,4.183,51.61,4.158,51.633,4.135,51.673,3.947,51.811,3.979,51.848,4.026,51.928,4.085,51.994,4.132,52.012,4.209,52.059,4.376,52.197,4.483,52.309,4.562,52.443,4.678,52.81,4.713,52.872,4.769,52.941,4.839,52.928,4.888,52.908,5.061,52.961,5.358,53.096,5.446,53.214,5.532,53.269,5.874,53.375,6.062,53.407,6.353,53.415,6.564,53.434,6.816,53.441,6.912,53.375,6.968,53.327,7.058,53.301,7.197,53.282,7.189,53.187,7.19,53,7.179,52.966,7.117,52.887,7.051,52.745,7.033,52.651,7.013,52.634,6.748,52.634,6.711,52.618,6.705,52.598,6.719,52.574,6.712,52.55,6.692,52.53,6.703,52.499,6.749,52.464,6.833,52.442,6.922,52.44,6.968,52.444,7.002,52.419,7.035,52.38,7.033,52.331,7.02,52.266,6.977,52.206,6.855,52.136,6.8,52.111,6.749,52.099,6.725,52.08,6.713,52.057,6.716,52.036,6.802,51.98,6.8,51.967,6.775,51.938,6.742,51.911,6.518,51.854,6.425,51.858,6.372,51.83,6.356,51.825,6.297,51.851,6.167,51.881,6.117,51.87,6.09,51.854,6.008,51.834,5.949,51.803,5.949,51.762,6.053,51.658,6.089,51.638,6.091,51.599,6.142,51.55,6.193,51.489,6.199,51.45,6.193,51.411,6.166,51.355,6.076,51.224,6.075,51.199,6.082,51.18,6.113,51.175,6.137,51.165,6.13,51.147,5.961,51.057,5.939,51.041,5.868,51.045,5.858,51.03,5.867,51.006,5.895,50.984,5.955,50.973,6.007,50.95,6.048,50.905,5.994,50.75]],[[4.226,51.386,4.211,51.349,4.173,51.307,4.04,51.247,3.902,51.208,3.831,51.213,3.782,51.233,3.756,51.255,3.682,51.276,3.58,51.286,3.517,51.264,3.472,51.242,3.433,51.246,3.403,51.264,3.38,51.291,3.35,51.378,3.426,51.394,3.589,51.399,3.717,51.369,3.883,51.354,4.011,51.396,4.112,51.361,4.226,51.386]],[[6.333,53.511,6.193,53.477,6.159,53.484,6.168,53.494,6.291,53.515,6.333,53.511]],[[5.929,53.459,5.732,53.443,5.665,53.455,5.654,53.467,5.708,53.473,5.876,53.475,5.928,53.465,5.929,53.459]],[[5.109,53.308,4.924,53.235,4.908,53.246,5.027,53.31,5.109,53.308]],[[5.326,53.386,5.233,53.378,5.19,53.392,5.415,53.431,5.557,53.444,5.583,53.438,5.326,53.386]],[[4.886,53.071,4.787,53,4.727,53.02,4.709,53.036,4.74,53.091,4.886,53.183,4.886,53.071]],[[3.949,51.739,4.047,51.685,4.068,51.667,4.075,51.649,3.951,51.627,3.819,51.694,3.732,51.678,3.699,51.71,3.699,51.73,3.789,51.746,3.949,51.739]],[[6.735,53.583,6.642,53.579,6.669,53.606,6.755,53.625,6.801,53.625,6.735,53.583]],[[-68.206,12.145,-68.254,12.032,-68.282,12.082,-68.287,12.172,-68.307,12.207,-68.348,12.228,-68.371,12.258,-68.369,12.302,-68.219,12.231,-68.206,12.145]],[[-62.937,17.496,-62.962,17.475,-62.983,17.477,-62.997,17.497,-63,17.53,-62.979,17.521,-62.972,17.516,-62.965,17.509,-62.937,17.496]],[[-63.233,17.623,-63.242,17.62,-63.254,17.629,-63.252,17.645,-63.242,17.652,-63.233,17.647,-63.227,17.634,-63.233,17.623]]]},{"n":"Aruba","c":"AW","r":5,"ml":5,"lx":-69.97,"ly":12.52,"b":[-70.066,12.423,-69.896,12.614],"p":[[[-69.899,12.452,-69.896,12.423,-69.942,12.439,-70.004,12.5,-70.066,12.547,-70.051,12.597,-70.035,12.614,-69.973,12.568,-69.912,12.48,-69.899,12.452]]]},{"n":"Curaçao","c":"CW","r":5,"ml":5,"lx":-68.92,"ly":12.15,"b":[-69.159,12.045,-68.751,12.38],"p":[[[-68.751,12.06,-68.803,12.045,-68.995,12.142,-69.154,12.298,-69.159,12.38,-69.118,12.373,-69.077,12.342,-69.013,12.231,-68.827,12.159,-68.751,12.06]]]},{"n":"Nepal","c":"NP","r":3,"ml":3,"lx":83.64,"ly":28.3,"b":[80.052,26.36,88.162,30.388],"p":[[[88.11,27.871,88.15,27.843,88.154,27.799,88.147,27.749,88.106,27.642,88.068,27.567,88.024,27.409,87.984,27.134,87.993,27.086,88.111,26.928,88.157,26.807,88.162,26.725,88.112,26.586,88.055,26.43,88.027,26.395,87.995,26.382,87.849,26.437,87.749,26.429,87.633,26.399,87.513,26.405,87.414,26.423,87.287,26.36,87.167,26.394,87.09,26.433,87.038,26.542,87.016,26.555,86.763,26.442,86.701,26.435,86.544,26.496,86.414,26.556,86.366,26.574,86.242,26.598,86.129,26.612,86.007,26.649,85.856,26.6,85.795,26.604,85.737,26.64,85.707,26.713,85.7,26.782,85.648,26.829,85.568,26.84,85.456,26.797,85.293,26.741,85.24,26.75,85.192,26.767,85.174,26.782,85.152,26.847,85.125,26.861,85.087,26.863,85.02,26.879,84.937,26.927,84.685,27.041,84.654,27.092,84.655,27.204,84.641,27.25,84.61,27.299,84.481,27.348,84.23,27.428,84.091,27.491,84.025,27.462,83.897,27.435,83.829,27.378,83.747,27.396,83.552,27.456,83.447,27.465,83.384,27.445,83.369,27.41,83.29,27.371,83.214,27.402,83.064,27.445,82.933,27.468,82.733,27.519,82.711,27.597,82.677,27.673,82.63,27.687,82.451,27.672,82.288,27.757,82.112,27.865,82.037,27.901,81.988,27.914,81.945,27.899,81.897,27.874,81.853,27.867,81.757,27.914,81.636,27.98,81.486,28.062,81.311,28.176,81.239,28.241,81.206,28.289,81.169,28.335,81.017,28.41,80.896,28.469,80.751,28.54,80.726,28.554,80.671,28.596,80.587,28.65,80.518,28.665,80.496,28.636,80.479,28.605,80.419,28.612,80.325,28.666,80.227,28.723,80.15,28.776,80.071,28.83,80.052,28.87,80.085,28.994,80.13,29.1,80.17,29.124,80.233,29.195,80.256,29.318,80.255,29.423,80.317,29.572,80.402,29.73,80.549,29.9,80.613,29.956,80.684,29.994,80.82,30.119,80.848,30.14,80.908,30.172,80.966,30.18,81.01,30.165,81.056,30.099,81.11,30.037,81.177,30.04,81.255,30.093,81.417,30.338,81.642,30.388,81.855,30.362,82.043,30.327,82.099,30.245,82.135,30.159,82.159,30.115,82.221,30.064,82.487,29.942,82.641,29.831,82.854,29.683,83.014,29.618,83.155,29.613,83.235,29.555,83.355,29.439,83.457,29.306,83.583,29.184,83.671,29.188,83.79,29.227,83.936,29.279,84.022,29.254,84.101,29.22,84.128,29.156,84.176,29.036,84.229,28.912,84.312,28.868,84.411,28.804,84.465,28.753,84.651,28.66,84.677,28.622,84.714,28.596,84.759,28.579,84.797,28.56,84.855,28.554,85.069,28.61,85.126,28.603,85.159,28.592,85.16,28.572,85.121,28.484,85.089,28.372,85.122,28.316,85.212,28.293,85.411,28.276,85.678,28.277,85.759,28.221,85.84,28.135,85.922,27.99,85.954,27.928,85.995,27.91,86.064,27.935,86.075,27.995,86.079,28.084,86.137,28.114,86.174,28.092,86.218,28.022,86.329,27.96,86.409,27.929,86.485,27.94,86.517,27.964,86.554,28.085,86.614,28.103,86.691,28.095,86.72,28.071,86.75,28.022,86.842,27.999,86.934,27.968,87.02,27.929,87.141,27.838,87.291,27.822,87.464,27.824,87.555,27.822,87.623,27.815,87.683,27.821,87.861,27.886,87.933,27.891,88.023,27.883,88.11,27.871]]]},{"n":"Nauru","c":"NR","r":6,"ml":5,"lx":166.93,"ly":-0.52,"b":[166.907,-0.551,166.958,-0.489],"p":[[[166.958,-0.517,166.939,-0.551,166.916,-0.546,166.907,-0.524,166.914,-0.499,166.939,-0.489,166.956,-0.497,166.958,-0.517]]]},{"n":"Namibia","c":"NA","r":3,"ml":3,"lx":17.11,"ly":-20.58,"b":[11.722,-28.939,25.259,-16.968],"p":[[[23.381,-17.641,23.595,-17.599,23.799,-17.56,24.037,-17.521,24.227,-17.49,24.275,-17.481,24.733,-17.518,24.932,-17.543,25.002,-17.569,25.092,-17.634,25.259,-17.794,25.216,-17.788,24.909,-17.821,24.792,-17.865,24.531,-18.053,24.475,-18.029,24.412,-17.989,24.359,-17.978,24.244,-18.023,24.129,-18.078,24.003,-18.154,23.898,-18.229,23.864,-18.27,23.7,-18.424,23.647,-18.449,23.6,-18.46,23.581,-18.453,23.56,-18.386,23.46,-18.231,23.299,-18.027,23.252,-18.008,23.219,-18,23.1,-18.01,22.753,-18.067,22.46,-18.116,22.011,-18.199,21.53,-18.266,21.233,-18.307,20.974,-18.319,20.974,-18.521,20.975,-18.929,20.976,-19.336,20.976,-19.744,20.977,-20.152,20.977,-20.56,20.978,-20.968,20.979,-21.376,20.979,-21.784,20.979,-21.962,20.971,-22,20.823,-22,20.488,-22,20.205,-22,19.977,-22,19.978,-22.243,19.978,-22.529,19.978,-22.816,19.979,-23.103,19.979,-23.389,19.979,-23.676,19.98,-23.962,19.98,-24.249,19.98,-24.536,19.98,-24.752,19.98,-24.777,19.98,-25.197,19.98,-25.642,19.98,-26.086,19.98,-26.531,19.98,-26.976,19.98,-27.421,19.98,-27.866,19.98,-28.31,19.98,-28.451,19.878,-28.449,19.671,-28.504,19.54,-28.575,19.483,-28.662,19.407,-28.714,19.313,-28.733,19.271,-28.778,19.282,-28.848,19.246,-28.902,19.162,-28.939,19.026,-28.928,18.839,-28.869,18.6,-28.855,18.311,-28.886,18.103,-28.872,17.976,-28.811,17.842,-28.777,17.699,-28.768,17.617,-28.743,17.448,-28.698,17.416,-28.621,17.396,-28.563,17.348,-28.501,17.343,-28.452,17.38,-28.414,17.386,-28.353,17.359,-28.269,17.312,-28.229,17.246,-28.231,17.205,-28.199,17.188,-28.133,17.149,-28.082,17.056,-28.031,16.933,-28.07,16.875,-28.128,16.841,-28.219,16.81,-28.265,16.795,-28.341,16.788,-28.395,16.756,-28.452,16.723,-28.475,16.689,-28.465,16.626,-28.488,16.487,-28.573,16.448,-28.618,16.335,-28.537,16.007,-28.232,15.891,-28.153,15.719,-27.966,15.342,-27.387,15.288,-27.275,15.216,-26.995,15.133,-26.788,15.124,-26.668,15.163,-26.6,15.139,-26.508,15.097,-26.426,14.968,-26.318,14.931,-25.958,14.845,-25.726,14.864,-25.534,14.823,-25.359,14.819,-25.246,14.837,-25.033,14.768,-24.788,14.628,-24.548,14.502,-24.202,14.483,-24.05,14.497,-23.643,14.472,-23.477,14.474,-23.281,14.424,-23.079,14.403,-22.968,14.438,-22.881,14.459,-22.908,14.496,-22.921,14.52,-22.805,14.526,-22.703,14.463,-22.449,14.322,-22.19,13.973,-21.768,13.888,-21.607,13.839,-21.473,13.451,-20.917,13.284,-20.524,13.168,-20.185,13.042,-20.028,12.458,-18.927,12.329,-18.751,12.096,-18.541,12.041,-18.471,11.951,-18.271,11.776,-18.002,11.733,-17.751,11.722,-17.467,11.743,-17.249,11.903,-17.227,12.014,-17.169,12.114,-17.165,12.213,-17.21,12.318,-17.213,12.359,-17.206,12.548,-17.213,12.657,-17.161,12.785,-17.108,12.859,-17.063,12.963,-17.015,13.101,-16.968,13.179,-16.972,13.276,-16.99,13.404,-17.008,13.476,-17.04,13.562,-17.141,13.694,-17.233,13.792,-17.288,13.904,-17.361,13.938,-17.389,13.987,-17.404,14.017,-17.409,14.226,-17.398,14.415,-17.388,14.618,-17.388,15.001,-17.389,15.383,-17.389,15.766,-17.39,16.148,-17.39,16.531,-17.391,16.914,-17.391,17.296,-17.392,17.679,-17.393,17.835,-17.393,18.109,-17.396,18.396,-17.399,18.428,-17.405,18.46,-17.425,18.487,-17.443,18.588,-17.57,18.718,-17.703,18.826,-17.766,18.955,-17.804,19.076,-17.818,19.189,-17.808,19.377,-17.825,19.639,-17.869,19.912,-17.881,20.194,-17.864,20.393,-17.887,20.508,-17.953,20.625,-17.997,20.746,-18.02,20.908,-18.006,21.113,-17.956,21.288,-17.963,21.369,-18,21.417,-18.001,21.718,-17.948,21.961,-17.905,22.324,-17.837,22.624,-17.782,23.068,-17.699,23.381,-17.641]]]},{"n":"Mozambique","c":"MZ","r":3,"ml":3,"lx":37.84,"ly":-13.94,"b":[30.222,-26.862,40.845,-10.464],"p":[[[31.288,-22.402,31.429,-22.299,31.571,-22.154,31.738,-21.983,31.886,-21.832,32.016,-21.698,32.195,-21.515,32.371,-21.335,32.412,-21.312,32.43,-21.297,32.354,-21.137,32.476,-20.95,32.483,-20.829,32.478,-20.713,32.492,-20.66,32.529,-20.613,32.673,-20.516,32.781,-20.362,32.87,-20.217,32.993,-19.985,33.005,-19.93,33.007,-19.874,32.973,-19.795,32.89,-19.668,32.831,-19.558,32.778,-19.389,32.831,-19.241,32.85,-19.152,32.85,-19.104,32.826,-19.059,32.766,-19.024,32.717,-19.002,32.7,-18.941,32.699,-18.868,32.722,-18.828,32.854,-18.764,32.885,-18.729,32.9,-18.689,32.902,-18.633,32.942,-18.493,32.993,-18.36,32.996,-18.313,32.979,-18.271,32.965,-18.196,32.956,-18.083,32.955,-17.765,32.981,-17.437,32.969,-17.252,32.884,-17.038,32.876,-16.884,32.938,-16.776,32.948,-16.712,32.903,-16.704,32.81,-16.698,32.742,-16.678,32.636,-16.589,32.452,-16.516,32.243,-16.449,31.94,-16.429,31.688,-16.214,31.49,-16.18,31.426,-16.152,31.236,-16.024,30.939,-16.012,30.63,-15.999,30.438,-15.995,30.409,-15.978,30.398,-15.801,30.396,-15.643,30.38,-15.506,30.351,-15.35,30.306,-15.289,30.252,-15.183,30.225,-15.067,30.222,-15.011,30.232,-14.99,30.446,-14.908,30.538,-14.867,30.673,-14.819,30.915,-14.753,31.131,-14.695,31.329,-14.638,31.538,-14.577,31.623,-14.537,31.729,-14.496,31.982,-14.414,32.054,-14.387,32.2,-14.341,32.273,-14.323,32.553,-14.23,32.875,-14.122,32.987,-14.085,33.202,-14.013,33.244,-14.043,33.39,-14.289,33.505,-14.434,33.636,-14.568,33.658,-14.562,33.696,-14.53,33.761,-14.517,33.97,-14.487,34.049,-14.485,34.102,-14.449,34.209,-14.424,34.333,-14.409,34.375,-14.425,34.505,-14.598,34.524,-14.731,34.551,-14.922,34.558,-15.016,34.555,-15.141,34.541,-15.297,34.435,-15.477,34.415,-15.567,34.358,-15.705,34.283,-15.773,34.246,-15.829,34.248,-15.887,34.288,-15.936,34.376,-16.024,34.403,-16.08,34.395,-16.131,34.396,-16.199,34.416,-16.247,34.441,-16.274,34.528,-16.319,34.613,-16.432,34.759,-16.567,34.933,-16.76,35.015,-16.82,35.08,-16.834,35.112,-16.899,35.094,-16.974,35.044,-17.017,35.065,-17.079,35.093,-17.111,35.125,-17.127,35.201,-17.131,35.273,-17.118,35.29,-17.097,35.281,-16.808,35.23,-16.639,35.178,-16.573,35.167,-16.56,35.185,-16.505,35.243,-16.375,35.292,-16.247,35.322,-16.193,35.358,-16.161,35.599,-16.126,35.709,-16.096,35.755,-16.058,35.791,-15.959,35.82,-15.68,35.83,-15.419,35.805,-15.266,35.84,-15.035,35.893,-14.892,35.867,-14.864,35.847,-14.671,35.69,-14.466,35.488,-14.201,35.376,-14.059,35.247,-13.897,35.014,-13.643,34.907,-13.552,34.85,-13.516,34.662,-13.487,34.612,-13.438,34.564,-13.36,34.546,-13.216,34.543,-13.109,34.521,-12.926,34.483,-12.667,34.466,-12.591,34.412,-12.396,34.361,-12.211,34.358,-12.165,34.376,-12.12,34.463,-11.984,34.525,-11.887,34.554,-11.834,34.606,-11.69,34.619,-11.62,34.66,-11.589,34.827,-11.576,34.959,-11.578,35.183,-11.575,35.418,-11.583,35.451,-11.59,35.504,-11.605,35.564,-11.602,35.631,-11.582,35.705,-11.532,35.785,-11.453,35.911,-11.455,36.082,-11.537,36.175,-11.609,36.191,-11.671,36.306,-11.706,36.519,-11.716,36.674,-11.684,36.771,-11.61,36.873,-11.571,36.979,-11.567,37.059,-11.592,37.114,-11.647,37.218,-11.687,37.373,-11.71,37.542,-11.675,37.725,-11.581,37.829,-11.482,37.855,-11.379,37.885,-11.317,37.92,-11.295,38.017,-11.282,38.177,-11.279,38.315,-11.311,38.492,-11.413,38.603,-11.345,38.795,-11.229,38.988,-11.167,39.171,-11.167,39.322,-11.123,39.439,-11.035,39.563,-10.979,39.694,-10.955,39.817,-10.912,39.989,-10.821,40.166,-10.687,40.347,-10.552,40.464,-10.464,40.517,-10.567,40.612,-10.662,40.555,-10.716,40.487,-10.765,40.597,-10.831,40.516,-10.93,40.506,-10.998,40.527,-11.025,40.545,-11.066,40.491,-11.179,40.421,-11.266,40.403,-11.332,40.465,-11.449,40.433,-11.657,40.494,-11.844,40.51,-11.94,40.532,-12.005,40.501,-12.119,40.509,-12.313,40.523,-12.393,40.487,-12.492,40.548,-12.527,40.581,-12.636,40.572,-12.758,40.553,-12.825,40.448,-12.905,40.435,-12.936,40.437,-12.983,40.569,-12.985,40.573,-13.058,40.564,-13.115,40.57,-13.223,40.552,-13.294,40.583,-13.374,40.545,-13.463,40.558,-13.531,40.56,-13.62,40.591,-13.845,40.596,-14.123,40.603,-14.167,40.65,-14.199,40.716,-14.214,40.713,-14.291,40.64,-14.39,40.636,-14.452,40.646,-14.539,40.727,-14.421,40.775,-14.421,40.818,-14.468,40.812,-14.536,40.827,-14.569,40.821,-14.635,40.845,-14.719,40.835,-14.792,40.776,-14.842,40.701,-14.93,40.687,-15.012,40.694,-15.065,40.642,-15.082,40.618,-15.116,40.653,-15.193,40.651,-15.261,40.559,-15.473,40.314,-15.764,40.208,-15.867,40.109,-15.979,40.109,-16.025,40.099,-16.065,39.984,-16.225,39.86,-16.252,39.791,-16.295,39.845,-16.436,39.765,-16.468,39.625,-16.579,39.242,-16.793,39.182,-16.842,39.084,-16.973,38.956,-17.005,38.885,-17.042,38.758,-17.055,38.713,-17.046,38.67,-17.05,38.633,-17.078,38.381,-17.17,38.145,-17.243,38.087,-17.276,38.048,-17.321,37.839,-17.393,37.512,-17.571,37.245,-17.74,37.051,-17.909,37,-17.935,36.939,-17.993,36.919,-18.08,36.9,-18.129,36.756,-18.307,36.54,-18.518,36.498,-18.576,36.412,-18.693,36.404,-18.77,36.327,-18.793,36.263,-18.72,36.236,-18.861,36.183,-18.871,36.125,-18.842,35.98,-18.912,35.854,-18.993,35.651,-19.164,35.365,-19.494,34.948,-19.813,34.891,-19.822,34.852,-19.821,34.721,-19.71,34.649,-19.701,34.713,-19.767,34.756,-19.822,34.745,-19.929,34.75,-20.091,34.698,-20.404,34.705,-20.473,34.765,-20.562,34.877,-20.671,34.982,-20.806,35.118,-21.195,35.128,-21.395,35.268,-21.651,35.273,-21.762,35.329,-22.037,35.326,-22.26,35.316,-22.397,35.383,-22.455,35.408,-22.403,35.401,-22.316,35.419,-22.178,35.456,-22.116,35.494,-22.125,35.505,-22.19,35.53,-22.248,35.54,-22.303,35.542,-22.377,35.49,-22.658,35.506,-22.772,35.575,-22.963,35.494,-23.185,35.377,-23.708,35.37,-23.798,35.399,-23.838,35.462,-23.851,35.485,-23.784,35.522,-23.785,35.542,-23.824,35.49,-24.066,35.438,-24.171,35.255,-24.43,35.156,-24.541,34.992,-24.651,34.607,-24.821,33.836,-25.068,33.53,-25.189,33.347,-25.261,32.961,-25.49,32.792,-25.644,32.723,-25.821,32.656,-25.902,32.59,-26.004,32.647,-26.092,32.704,-26.158,32.77,-26.203,32.804,-26.241,32.849,-26.268,32.894,-26.13,32.916,-26.087,32.955,-26.084,32.934,-26.252,32.889,-26.83,32.886,-26.849,32.777,-26.851,32.589,-26.856,32.478,-26.858,32.354,-26.862,32.2,-26.833,32.113,-26.839,32.106,-26.52,32.078,-26.45,32.048,-26.347,32.041,-26.281,32.06,-26.215,32.069,-26.11,32.061,-26.018,31.968,-25.972,31.948,-25.958,31.928,-25.885,31.92,-25.774,31.985,-25.632,31.979,-25.359,31.987,-25.263,31.986,-25.074,31.984,-24.844,31.983,-24.638,31.986,-24.461,31.967,-24.376,31.951,-24.33,31.908,-24.236,31.858,-24.04,31.8,-23.892,31.724,-23.795,31.7,-23.743,31.676,-23.674,31.604,-23.553,31.546,-23.482,31.53,-23.426,31.532,-23.279,31.467,-23.017,31.419,-22.825,31.348,-22.618,31.3,-22.479,31.293,-22.455,31.288,-22.402],[34.642,-12.014,34.624,-11.985,34.591,-11.971,34.554,-11.982,34.542,-12.019,34.58,-12.066,34.622,-12.067,34.642,-12.014],[34.719,-12.111,34.746,-12.088,34.756,-12.059,34.756,-12.031,34.739,-12.013,34.715,-12.003,34.68,-12.009,34.667,-12.048,34.662,-12.101,34.684,-12.119,34.719,-12.111]]]},{"n":"Morocco","c":"MA","r":3,"ml":2.7,"lx":-7.19,"ly":31.65,"b":[-17.003,21.421,-1.066,35.93],"p":[[[-2.22,35.104,-2.191,35.03,-2.132,34.971,-1.921,34.836,-1.796,34.752,-1.792,34.723,-1.832,34.655,-1.85,34.607,-1.817,34.557,-1.739,34.496,-1.733,34.467,-1.752,34.433,-1.792,34.368,-1.707,34.176,-1.693,33.99,-1.715,33.858,-1.714,33.782,-1.703,33.717,-1.631,33.567,-1.679,33.319,-1.625,33.183,-1.551,33.074,-1.51,32.878,-1.45,32.785,-1.352,32.703,-1.296,32.676,-1.188,32.608,-1.111,32.552,-1.066,32.468,-1.163,32.399,-1.24,32.338,-1.262,32.271,-1.226,32.165,-1.226,32.107,-1.275,32.089,-1.477,32.095,-1.635,32.1,-1.817,32.105,-2.073,32.115,-2.231,32.121,-2.448,32.13,-2.523,32.126,-2.723,32.096,-2.863,32.075,-2.887,32.069,-2.931,32.043,-2.961,31.964,-2.988,31.874,-3.017,31.834,-3.44,31.705,-3.605,31.687,-3.7,31.7,-3.768,31.69,-3.827,31.662,-3.847,31.62,-3.85,31.566,-3.837,31.512,-3.796,31.437,-3.789,31.362,-3.815,31.309,-3.821,31.255,-3.833,31.198,-3.812,31.167,-3.771,31.162,-3.73,31.135,-3.673,31.111,-3.625,31.066,-3.627,31.001,-3.667,30.964,-3.702,30.944,-3.86,30.927,-3.985,30.914,-4.149,30.81,-4.323,30.699,-4.529,30.626,-4.62,30.605,-4.779,30.552,-4.968,30.465,-5.062,30.326,-5.18,30.166,-5.294,30.059,-5.449,29.957,-5.593,29.918,-5.775,29.869,-6.004,29.831,-6.167,29.819,-6.215,29.811,-6.358,29.808,-6.428,29.816,-6.48,29.82,-6.501,29.809,-6.508,29.784,-6.511,29.726,-6.521,29.66,-6.566,29.604,-6.598,29.579,-6.635,29.569,-6.755,29.584,-6.856,29.602,-7.095,29.625,-7.142,29.62,-7.16,29.613,-7.235,29.575,-7.35,29.495,-7.428,29.425,-7.486,29.392,-7.625,29.375,-7.685,29.35,-7.944,29.175,-7.999,29.132,-8.265,28.981,-8.34,28.93,-8.399,28.88,-8.558,28.768,-8.66,28.719,-8.678,28.689,-8.683,28.621,-8.683,28.469,-8.683,28.324,-8.683,28.112,-8.683,27.9,-8.683,27.656,-8.818,27.656,-8.814,27.614,-8.785,27.531,-8.774,27.461,-8.789,27.417,-8.803,27.361,-8.797,27.308,-8.774,27.251,-8.754,27.191,-8.754,27.151,-8.795,27.121,-8.889,27.104,-9.002,27.09,-9.084,27.09,-9.208,27.1,-9.286,27.098,-9.353,27.098,-9.413,27.088,-9.487,27.05,-9.57,26.991,-9.673,26.911,-9.735,26.861,-9.818,26.85,-9.9,26.85,-9.981,26.89,-10.033,26.911,-10.067,26.909,-10.123,26.88,-10.189,26.861,-10.251,26.861,-10.355,26.901,-10.479,26.961,-10.551,26.991,-10.654,27.001,-10.758,27.02,-10.83,27.01,-10.923,27.01,-11.047,26.97,-11.15,26.941,-11.264,26.911,-11.393,26.883,-11.361,26.794,-11.317,26.745,-11.317,26.684,-11.338,26.633,-11.4,26.584,-11.471,26.52,-11.512,26.47,-11.553,26.401,-11.584,26.361,-11.637,26.296,-11.685,26.213,-11.699,26.163,-11.718,26.104,-11.755,26.087,-11.881,26.071,-11.961,26.05,-12.031,26.031,-12.057,25.996,-12.061,25.991,-12.081,25.921,-12.081,25.871,-12.101,25.831,-12.131,25.731,-12.171,25.64,-12.201,25.52,-12.231,25.421,-12.271,25.26,-12.311,25.111,-12.361,24.97,-12.401,24.88,-12.431,24.831,-12.501,24.77,-12.561,24.731,-12.631,24.68,-12.711,24.63,-12.821,24.571,-12.911,24.52,-12.948,24.497,-12.991,24.47,-13.061,24.401,-13.121,24.3,-13.161,24.22,-13.231,24.09,-13.281,24.02,-13.311,23.981,-13.391,23.941,-13.481,23.911,-13.581,23.871,-13.661,23.831,-13.771,23.791,-13.841,23.751,-13.891,23.691,-13.931,23.621,-13.981,23.52,-14.021,23.411,-14.041,23.34,-14.101,23.1,-14.121,22.961,-14.141,22.871,-14.171,22.76,-14.191,22.59,-14.191,22.451,-14.211,22.371,-14.221,22.31,-14.271,22.241,-14.311,22.191,-14.381,22.121,-14.441,22.081,-14.461,22.041,-14.521,21.991,-14.581,21.911,-14.631,21.861,-14.621,21.821,-14.611,21.751,-14.641,21.68,-14.671,21.6,-14.751,21.501,-14.841,21.451,-14.971,21.441,-15.151,21.441,-15.291,21.451,-15.461,21.451,-15.611,21.47,-15.751,21.491,-15.921,21.501,-16.041,21.501,-16.191,21.481,-16.581,21.481,-16.731,21.47,-16.951,21.43,-17.003,21.421,-16.931,21.9,-16.793,22.16,-16.684,22.274,-16.514,22.333,-16.359,22.595,-16.304,22.835,-16.202,22.945,-16.17,23.032,-16.21,23.098,-16.114,23.228,-15.997,23.425,-15.943,23.553,-15.806,23.75,-15.789,23.793,-15.802,23.842,-15.855,23.8,-15.913,23.728,-15.981,23.67,-15.953,23.741,-15.899,23.844,-15.778,23.953,-15.586,24.073,-15.189,24.479,-15.039,24.549,-14.904,24.72,-14.856,24.872,-14.843,25.22,-14.795,25.404,-14.707,25.548,-14.602,25.809,-14.523,25.925,-14.471,26.163,-14.414,26.254,-14.312,26.297,-14.168,26.415,-13.952,26.489,-13.696,26.643,-13.576,26.735,-13.496,26.873,-13.41,27.147,-13.256,27.435,-13.177,27.652,-13.176,27.656,-13.041,27.77,-12.949,27.914,-12.794,27.978,-12.469,28.009,-11.986,28.129,-11.553,28.31,-11.43,28.382,-11.299,28.526,-11.081,28.714,-10.674,28.939,-10.486,29.065,-10.201,29.38,-10.01,29.641,-9.853,29.809,-9.743,29.958,-9.667,30.109,-9.624,30.353,-9.653,30.448,-9.773,30.603,-9.854,30.645,-9.875,30.718,-9.832,30.847,-9.833,31.07,-9.809,31.425,-9.675,31.711,-9.347,32.086,-9.287,32.241,-9.249,32.486,-9.246,32.572,-8.836,32.92,-8.596,33.187,-8.513,33.252,-8.301,33.374,-7.562,33.64,-7.145,33.83,-6.901,33.969,-6.756,34.133,-6.353,34.776,-5.958,35.681,-5.925,35.786,-5.748,35.816,-5.623,35.829,-5.522,35.862,-5.397,35.93,-5.278,35.903,-5.338,35.857,-5.338,35.745,-5.253,35.615,-5.105,35.468,-4.837,35.281,-4.628,35.206,-4.33,35.161,-3.982,35.243,-3.788,35.245,-3.693,35.28,-3.591,35.228,-3.395,35.212,-3.206,35.239,-3.063,35.317,-2.972,35.407,-2.958,35.363,-2.954,35.315,-2.926,35.287,-2.87,35.173,-2.84,35.128,-2.731,35.135,-2.637,35.113,-2.424,35.123,-2.22,35.104]]]},{"n":"Western Sahara","c":"EH","r":7,"ml":6,"lx":-12.63,"ly":23.97,"b":[-17.099,20.806,-8.682,27.656],"p":[[[-8.818,27.656,-8.683,27.656,-8.683,27.49,-8.683,27.286,-8.683,27.119,-8.683,26.921,-8.683,26.723,-8.682,26.498,-8.682,26.273,-8.682,26.109,-8.682,25.996,-8.886,25.996,-9.072,25.996,-9.258,25.996,-9.445,25.996,-9.631,25.996,-9.817,25.995,-10.004,25.995,-10.19,25.995,-10.376,25.995,-10.562,25.995,-10.749,25.995,-10.935,25.995,-11.121,25.995,-11.308,25.995,-11.494,25.995,-11.68,25.995,-11.867,25.995,-12.016,25.995,-12.016,25.876,-12.016,25.74,-12.016,25.604,-12.016,25.468,-12.016,25.332,-12.016,25.196,-12.016,25.059,-12.016,24.923,-12.016,24.787,-12.016,24.651,-12.016,24.515,-12.016,24.379,-12.016,24.242,-12.016,24.106,-12.016,23.97,-12.016,23.834,-12.016,23.698,-12.016,23.576,-12.023,23.468,-12.083,23.435,-12.226,23.377,-12.373,23.318,-12.559,23.291,-12.62,23.271,-12.74,23.193,-12.896,23.09,-13.031,23,-13.121,22.884,-13.153,22.821,-13.167,22.753,-13.156,22.689,-13.107,22.561,-13.094,22.496,-13.087,22.383,-13.078,22.26,-13.07,22.128,-13.061,21.996,-13.051,21.855,-13.042,21.714,-13.032,21.572,-13.025,21.467,-13.016,21.334,-13.167,21.334,-13.397,21.334,-13.626,21.333,-13.855,21.333,-14.085,21.333,-14.314,21.332,-14.543,21.332,-14.773,21.332,-15.002,21.332,-15.231,21.331,-15.461,21.331,-15.69,21.331,-15.919,21.33,-16.148,21.33,-16.378,21.33,-16.607,21.33,-16.836,21.329,-16.965,21.329,-17.006,21.142,-17.042,21.008,-17.064,20.899,-17.048,20.806,-17.099,20.857,-17.01,21.377,-17.003,21.421,-16.951,21.43,-16.731,21.47,-16.581,21.481,-16.191,21.481,-16.041,21.501,-15.921,21.501,-15.751,21.491,-15.611,21.47,-15.461,21.451,-15.291,21.451,-15.151,21.441,-14.971,21.441,-14.841,21.451,-14.751,21.501,-14.671,21.6,-14.641,21.68,-14.611,21.751,-14.621,21.821,-14.631,21.861,-14.581,21.911,-14.521,21.991,-14.461,22.041,-14.441,22.081,-14.381,22.121,-14.311,22.191,-14.271,22.241,-14.221,22.31,-14.211,22.371,-14.191,22.451,-14.191,22.59,-14.171,22.76,-14.141,22.871,-14.121,22.961,-14.101,23.1,-14.041,23.34,-14.021,23.411,-13.981,23.52,-13.931,23.621,-13.891,23.691,-13.841,23.751,-13.771,23.791,-13.661,23.831,-13.581,23.871,-13.481,23.911,-13.391,23.941,-13.311,23.981,-13.281,24.02,-13.231,24.09,-13.161,24.22,-13.121,24.3,-13.061,24.401,-12.991,24.47,-12.948,24.497,-12.911,24.52,-12.821,24.571,-12.711,24.63,-12.631,24.68,-12.561,24.731,-12.501,24.77,-12.431,24.831,-12.401,24.88,-12.361,24.97,-12.311,25.111,-12.271,25.26,-12.231,25.421,-12.201,25.52,-12.171,25.64,-12.131,25.731,-12.101,25.831,-12.081,25.871,-12.081,25.921,-12.061,25.991,-12.057,25.996,-12.031,26.031,-11.961,26.05,-11.881,26.071,-11.755,26.087,-11.718,26.104,-11.699,26.163,-11.685,26.213,-11.637,26.296,-11.584,26.361,-11.553,26.401,-11.512,26.47,-11.471,26.52,-11.4,26.584,-11.338,26.633,-11.317,26.684,-11.317,26.745,-11.361,26.794,-11.393,26.883,-11.264,26.911,-11.15,26.941,-11.047,26.97,-10.923,27.01,-10.83,27.01,-10.758,27.02,-10.654,27.001,-10.551,26.991,-10.479,26.961,-10.355,26.901,-10.251,26.861,-10.189,26.861,-10.123,26.88,-10.067,26.909,-10.033,26.911,-9.981,26.89,-9.9,26.85,-9.818,26.85,-9.735,26.861,-9.673,26.911,-9.57,26.991,-9.487,27.05,-9.413,27.088,-9.353,27.098,-9.286,27.098,-9.208,27.1,-9.084,27.09,-9.002,27.09,-8.889,27.104,-8.795,27.121,-8.754,27.151,-8.754,27.191,-8.774,27.251,-8.797,27.308,-8.803,27.361,-8.789,27.417,-8.774,27.461,-8.785,27.531,-8.814,27.614,-8.818,27.656]]]},{"n":"Montenegro","c":"ME","r":6,"ml":5,"lx":19.14,"ly":42.8,"b":[18.436,41.869,20.348,43.542],"p":[[[19.194,43.533,19.192,43.521,19.196,43.485,19.219,43.45,19.298,43.414,19.415,43.343,19.552,43.212,19.614,43.173,19.671,43.164,19.781,43.11,19.858,43.097,19.944,43.082,20.168,42.969,20.268,42.935,20.34,42.893,20.348,42.853,20.344,42.828,20.215,42.799,20.193,42.755,20.13,42.76,20.054,42.76,20.029,42.732,20.066,42.686,20.089,42.632,20.07,42.557,20.064,42.547,20.046,42.55,19.939,42.507,19.86,42.486,19.788,42.476,19.754,42.497,19.738,42.525,19.741,42.607,19.728,42.635,19.703,42.648,19.654,42.629,19.597,42.565,19.545,42.492,19.465,42.415,19.4,42.342,19.329,42.249,19.281,42.173,19.331,42.129,19.361,42.069,19.352,42.024,19.361,41.998,19.346,41.919,19.342,41.869,19.186,41.949,19.122,42.06,18.894,42.249,18.633,42.378,18.619,42.398,18.633,42.423,18.646,42.443,18.592,42.444,18.554,42.429,18.517,42.433,18.477,42.481,18.438,42.523,18.436,42.56,18.454,42.565,18.48,42.579,18.535,42.62,18.546,42.642,18.543,42.674,18.466,42.777,18.455,42.844,18.444,42.968,18.46,42.998,18.488,43.012,18.624,43.028,18.622,43.125,18.63,43.154,18.657,43.194,18.674,43.231,18.749,43.284,18.851,43.346,18.896,43.348,18.935,43.339,18.979,43.285,19.027,43.292,19.037,43.357,18.974,43.442,18.94,43.497,18.951,43.527,18.974,43.542,19.028,43.533,19.08,43.518,19.113,43.528,19.164,43.535,19.194,43.533]]]},{"n":"Mongolia","c":"MN","r":3,"ml":3,"lx":104.15,"ly":46,"b":[87.743,41.596,119.898,52.117],"p":[[[87.814,49.162,87.818,49.162,87.935,49.165,87.988,49.187,88.029,49.22,88.116,49.256,88.134,49.298,88.136,49.381,88.193,49.452,88.338,49.473,88.393,49.483,88.452,49.473,88.544,49.483,88.633,49.486,88.683,49.465,88.748,49.446,88.832,49.448,88.86,49.482,88.864,49.528,88.9,49.54,88.945,49.508,88.971,49.484,89.008,49.473,89.109,49.501,89.18,49.532,89.203,49.596,89.244,49.627,89.299,49.611,89.396,49.612,89.475,49.661,89.579,49.7,89.654,49.717,89.67,49.75,89.634,49.823,89.644,49.903,89.744,49.948,89.878,49.954,89.977,49.984,90.005,50.069,90.054,50.094,90.104,50.103,90.225,50.117,90.311,50.151,90.365,50.167,90.517,50.213,90.655,50.222,90.714,50.259,90.761,50.306,90.838,50.324,90.917,50.364,91.022,50.415,91.063,50.423,91.234,50.452,91.301,50.463,91.341,50.47,91.415,50.468,91.446,50.522,91.522,50.562,91.597,50.576,91.634,50.615,91.706,50.666,91.804,50.694,91.957,50.698,92.104,50.692,92.192,50.701,92.265,50.775,92.279,50.812,92.296,50.85,92.355,50.864,92.426,50.803,92.486,50.765,92.579,50.725,92.627,50.688,92.681,50.683,92.739,50.711,92.779,50.779,92.856,50.789,92.941,50.778,92.964,50.745,92.971,50.713,93.01,50.655,93.103,50.604,93.223,50.607,93.271,50.616,93.387,50.608,93.501,50.597,93.626,50.586,93.662,50.584,93.795,50.578,93.99,50.569,94.076,50.573,94.251,50.556,94.287,50.511,94.319,50.405,94.347,50.303,94.355,50.222,94.4,50.18,94.458,50.166,94.497,50.133,94.565,50.088,94.615,50.024,94.675,50.028,94.718,50.043,94.811,50.048,94.93,50.044,95.013,50.008,95.044,49.962,95.111,49.935,95.166,49.944,95.329,49.944,95.386,49.941,95.442,49.916,95.523,49.911,95.567,49.944,95.708,49.966,95.789,50.013,95.852,50.013,95.899,49.991,95.936,49.96,95.99,49.974,96.019,49.999,96.066,49.999,96.112,49.982,96.23,49.954,96.315,49.901,96.381,49.896,96.466,49.912,96.506,49.919,96.543,49.893,96.598,49.878,96.64,49.898,96.712,49.912,96.986,49.883,97.049,49.83,97.098,49.805,97.137,49.762,97.209,49.731,97.36,49.741,97.418,49.773,97.541,49.843,97.589,49.911,97.651,49.934,97.721,49.945,97.786,49.945,97.854,49.947,97.937,49.997,98.004,50.014,98.103,50.078,98.122,50.107,98.17,50.181,98.2,50.228,98.25,50.302,98.277,50.423,98.293,50.487,98.279,50.533,98.221,50.557,98.145,50.569,98.079,50.604,98.03,50.645,98.001,50.702,97.962,50.769,97.964,50.818,97.953,50.855,97.92,50.887,97.856,50.943,97.825,50.985,97.836,51.052,97.911,51.165,97.918,51.218,97.927,51.251,97.923,51.28,97.947,51.348,97.989,51.377,98.038,51.45,98.103,51.484,98.185,51.486,98.22,51.506,98.238,51.578,98.277,51.635,98.303,51.674,98.353,51.718,98.641,51.801,98.76,51.905,98.803,51.957,98.849,52.07,98.893,52.117,98.958,52.102,99.034,52.035,99.091,52.035,99.176,51.999,99.407,51.924,99.532,51.9,99.613,51.893,99.719,51.872,99.788,51.828,99.922,51.756,100.035,51.737,100.23,51.73,100.469,51.726,100.536,51.713,100.711,51.662,100.904,51.604,101.085,51.553,101.223,51.513,101.304,51.475,101.381,51.453,101.464,51.471,101.571,51.467,101.821,51.421,101.979,51.382,102.112,51.353,102.156,51.314,102.16,51.261,102.142,51.216,102.152,51.108,102.195,51.051,102.21,50.974,102.226,50.901,102.215,50.829,102.235,50.791,102.277,50.769,102.317,50.718,102.303,50.666,102.286,50.635,102.288,50.585,102.336,50.544,102.407,50.536,102.469,50.526,102.546,50.461,102.683,50.387,102.765,50.367,102.86,50.333,103.039,50.301,103.162,50.291,103.234,50.264,103.304,50.2,103.421,50.187,103.496,50.165,103.633,50.139,103.723,50.154,103.803,50.176,103.856,50.172,103.958,50.157,104.079,50.154,104.18,50.169,104.26,50.214,104.354,50.275,104.466,50.306,104.596,50.317,104.685,50.342,104.977,50.383,105.095,50.39,105.186,50.43,105.267,50.46,105.384,50.474,105.542,50.441,105.693,50.414,105.875,50.405,105.996,50.368,106.083,50.333,106.218,50.305,106.368,50.318,106.574,50.329,106.711,50.313,106.854,50.248,106.941,50.197,107.04,50.086,107.143,50.033,107.233,49.989,107.347,49.987,107.631,49.983,107.787,49.96,107.917,49.948,107.948,49.925,107.935,49.849,107.939,49.741,107.937,49.691,107.965,49.654,108.01,49.647,108.034,49.594,108.098,49.563,108.213,49.525,108.407,49.396,108.522,49.342,108.614,49.323,108.733,49.336,108.92,49.335,109.237,49.335,109.454,49.296,109.529,49.27,109.75,49.239,109.995,49.206,110.2,49.17,110.321,49.216,110.428,49.22,110.53,49.187,110.631,49.138,110.71,49.143,110.828,49.166,111.204,49.304,111.337,49.356,111.429,49.343,111.512,49.361,111.575,49.376,111.736,49.398,111.833,49.404,111.934,49.416,112.08,49.424,112.375,49.515,112.495,49.532,112.697,49.507,112.806,49.524,112.915,49.569,113.056,49.616,113.092,49.693,113.164,49.797,113.319,49.874,113.446,49.942,113.574,50.007,113.732,50.062,113.881,50.101,114.071,50.205,114.222,50.257,114.297,50.274,114.386,50.255,114.554,50.241,114.675,50.246,114.743,50.234,114.88,50.183,115.003,50.139,115.098,50.059,115.275,49.949,115.365,49.912,115.429,49.896,115.588,49.886,115.718,49.881,115.795,49.906,115.926,49.952,116.135,50.011,116.217,50.009,116.351,49.978,116.551,49.92,116.632,49.877,116.683,49.824,116.59,49.685,116.402,49.406,116.243,49.17,116.16,49.037,116.098,48.936,116.034,48.84,116.025,48.782,115.954,48.689,115.821,48.577,115.792,48.456,115.797,48.346,115.786,48.248,115.639,48.186,115.525,48.131,115.558,47.945,115.616,47.875,115.712,47.799,115.812,47.738,115.898,47.687,115.994,47.711,116.075,47.79,116.231,47.858,116.317,47.86,116.378,47.844,116.513,47.84,116.652,47.865,116.761,47.87,116.901,47.853,116.952,47.837,117.07,47.806,117.197,47.74,117.286,47.666,117.351,47.652,117.384,47.676,117.455,47.741,117.555,47.805,117.677,47.908,117.768,47.988,117.84,48,117.979,48,118.042,48.019,118.147,48.029,118.24,48,118.498,47.984,118.568,47.943,118.691,47.822,118.76,47.758,118.88,47.725,118.953,47.703,119.018,47.685,119.082,47.654,119.097,47.616,119.123,47.558,119.162,47.525,119.235,47.493,119.291,47.473,119.309,47.431,119.326,47.41,119.377,47.381,119.527,47.256,119.6,47.222,119.711,47.15,119.757,47.09,119.76,47.027,119.788,46.979,119.863,46.907,119.898,46.858,119.884,46.791,119.896,46.733,119.867,46.672,119.747,46.627,119.707,46.606,119.62,46.604,119.474,46.627,119.332,46.614,119.162,46.639,119.029,46.692,118.957,46.735,118.844,46.76,118.79,46.747,118.723,46.692,118.649,46.702,118.58,46.692,118.404,46.703,118.309,46.717,118.157,46.679,118.071,46.667,117.91,46.619,117.813,46.538,117.741,46.518,117.671,46.522,117.621,46.552,117.547,46.588,117.438,46.586,117.406,46.571,117.392,46.538,117.356,46.437,117.357,46.391,117.333,46.362,117.269,46.352,117.156,46.355,116.979,46.362,116.859,46.388,116.787,46.377,116.689,46.322,116.619,46.313,116.563,46.29,116.517,46.209,116.445,46.159,116.358,46.097,116.265,45.963,116.213,45.887,116.229,45.846,116.241,45.796,116.198,45.739,116.11,45.687,116.04,45.677,115.934,45.626,115.789,45.535,115.681,45.458,115.539,45.44,115.439,45.42,115.217,45.396,115.163,45.39,114.919,45.378,114.739,45.42,114.644,45.413,114.56,45.39,114.517,45.365,114.502,45.316,114.487,45.272,114.419,45.203,114.281,45.111,114.167,45.05,114.08,44.971,114.03,44.943,113.931,44.912,113.877,44.896,113.752,44.826,113.653,44.763,113.587,44.746,113.508,44.762,113.456,44.767,113.301,44.792,113.196,44.795,113.049,44.81,112.707,44.883,112.597,44.918,112.499,45.011,112.411,45.058,112.292,45.063,112.113,45.063,112.033,45.082,111.898,45.064,111.751,44.97,111.681,44.899,111.621,44.827,111.547,44.673,111.515,44.57,111.489,44.512,111.411,44.419,111.402,44.367,111.43,44.322,111.486,44.272,111.52,44.192,111.603,44.107,111.684,44.041,111.837,43.935,111.88,43.879,111.932,43.815,111.943,43.752,111.933,43.711,111.878,43.68,111.771,43.665,111.72,43.621,111.641,43.563,111.547,43.496,111.504,43.493,111.451,43.475,111.187,43.392,111.087,43.369,111.007,43.341,110.913,43.257,110.84,43.194,110.749,43.111,110.709,43.074,110.628,42.991,110.521,42.895,110.462,42.844,110.43,42.814,110.4,42.774,110.289,42.743,110.197,42.71,110.058,42.661,109.859,42.606,109.698,42.554,109.596,42.511,109.443,42.456,109.34,42.438,109.132,42.441,108.875,42.426,108.687,42.416,108.546,42.429,108.334,42.437,108.171,42.447,108.062,42.427,107.806,42.406,107.749,42.401,107.292,42.349,107.091,42.322,106.906,42.309,106.77,42.289,106.693,42.264,106.579,42.227,106.519,42.212,106.317,42.141,105.868,41.994,105.566,41.875,105.517,41.855,105.314,41.771,105.197,41.738,105.115,41.663,105.051,41.616,104.982,41.596,104.86,41.644,104.774,41.641,104.498,41.659,104.498,41.877,104.305,41.846,103.997,41.797,103.711,41.751,103.45,41.856,103.248,41.937,103.073,42.006,102.807,42.052,102.575,42.092,102.157,42.158,101.973,42.216,101.88,42.292,101.714,42.466,101.66,42.5,101.579,42.524,101.495,42.539,101.314,42.538,101.092,42.551,100.773,42.588,100.519,42.617,100.086,42.671,99.984,42.677,99.757,42.629,99.468,42.568,98.947,42.616,98.716,42.639,98.248,42.685,97.719,42.736,97.206,42.79,96.833,42.76,96.625,42.744,96.385,42.72,96.352,42.747,96.342,42.849,96.3,42.929,96.168,43.015,96.08,43.096,95.913,43.206,95.86,43.276,95.842,43.384,95.687,43.664,95.591,43.854,95.567,43.892,95.526,43.954,95.471,43.986,95.356,44.006,95.326,44.039,95.326,44.105,95.344,44.195,95.367,44.262,95.35,44.278,95.05,44.259,94.866,44.303,94.712,44.351,94.494,44.473,94.365,44.519,94.199,44.645,93.958,44.675,93.868,44.724,93.755,44.832,93.656,44.901,93.516,44.944,93.294,44.983,92.916,45.02,92.788,45.036,92.579,45.011,92.424,45.009,92.173,45.035,92.03,45.069,91.853,45.069,91.738,45.069,91.584,45.077,91.51,45.098,91.441,45.125,91.312,45.118,91.222,45.145,91.138,45.194,91.05,45.217,90.954,45.216,90.914,45.194,90.877,45.196,90.853,45.263,90.763,45.371,90.75,45.419,90.694,45.475,90.662,45.525,90.671,45.595,90.71,45.731,90.796,45.854,90.852,45.885,90.887,45.922,90.96,45.985,91.002,46.036,90.997,46.105,90.948,46.177,90.912,46.271,90.918,46.324,90.971,46.388,91.034,46.529,91.029,46.566,91.004,46.596,90.998,46.661,90.986,46.749,90.911,46.883,90.87,46.954,90.799,46.985,90.716,47.004,90.643,47.1,90.553,47.214,90.496,47.285,90.476,47.329,90.467,47.408,90.425,47.504,90.381,47.557,90.347,47.597,90.331,47.655,90.313,47.676,90.191,47.702,90.103,47.745,90.067,47.804,90.054,47.85,90.028,47.878,89.959,47.886,89.91,47.844,89.831,47.823,89.778,47.827,89.726,47.852,89.693,47.879,89.638,47.909,89.561,48.004,89.479,48.029,89.33,48.025,89.196,47.981,89.116,47.988,89.048,48.003,88.971,48.05,88.918,48.089,88.838,48.102,88.682,48.171,88.576,48.22,88.567,48.317,88.517,48.384,88.414,48.403,88.31,48.472,88.158,48.509,88.063,48.538,87.98,48.555,87.967,48.581,87.972,48.603,88.011,48.64,88.05,48.675,88.06,48.707,88.028,48.736,87.942,48.765,87.832,48.792,87.809,48.836,87.743,48.882,87.755,48.919,87.807,48.946,87.86,48.966,87.872,49,87.835,49.032,87.816,49.08,87.825,49.116,87.814,49.162]]]},{"n":"Moldova","c":"MD","r":6,"ml":5,"lx":28.49,"ly":47.43,"b":[26.619,45.45,30.131,48.478],"p":[[[26.619,48.26,26.64,48.294,26.847,48.387,26.901,48.372,27.008,48.368,27.229,48.371,27.337,48.433,27.404,48.416,27.458,48.443,27.549,48.478,27.562,48.47,27.574,48.465,27.714,48.45,27.82,48.416,27.891,48.365,27.963,48.334,28.038,48.321,28.08,48.296,28.088,48.257,28.159,48.238,28.291,48.239,28.347,48.213,28.327,48.161,28.341,48.144,28.388,48.162,28.423,48.147,28.442,48.109,28.463,48.091,28.53,48.15,28.602,48.144,28.774,48.12,28.866,47.996,28.923,47.951,28.973,47.933,29.037,47.952,29.093,47.975,29.125,47.965,29.195,47.882,29.211,47.775,29.211,47.732,29.186,47.659,29.151,47.581,29.123,47.53,29.135,47.49,29.16,47.456,29.201,47.444,29.334,47.376,29.383,47.328,29.456,47.293,29.511,47.291,29.539,47.271,29.549,47.247,29.542,47.186,29.511,47.128,29.515,47.091,29.563,47.048,29.569,46.997,29.572,46.964,29.598,46.939,29.72,46.883,29.878,46.829,29.918,46.782,29.942,46.724,29.935,46.625,29.924,46.539,30.131,46.423,30.108,46.402,30.076,46.378,29.878,46.36,29.838,46.351,29.752,46.438,29.707,46.449,29.665,46.417,29.615,46.399,29.555,46.408,29.491,46.435,29.459,46.454,29.433,46.456,29.393,46.437,29.34,46.445,29.305,46.467,29.255,46.393,29.224,46.377,29.205,46.379,29.201,46.505,29.186,46.524,29.146,46.527,29.05,46.497,28.958,46.458,28.927,46.424,28.931,46.362,28.944,46.288,29.006,46.176,28.972,46.128,28.948,46.05,28.85,45.979,28.739,45.937,28.729,45.852,28.668,45.794,28.562,45.736,28.492,45.666,28.509,45.618,28.514,45.572,28.502,45.542,28.499,45.518,28.471,45.507,28.31,45.499,28.265,45.484,28.213,45.45,28.163,45.514,28.112,45.569,28.075,45.599,28.09,45.613,28.131,45.628,28.16,45.647,28.156,45.713,28.135,45.789,28.116,45.826,28.114,45.883,28.1,45.973,28.119,46.139,28.2,46.348,28.244,46.451,28.223,46.508,28.239,46.641,28.205,46.706,28.15,46.792,28.072,46.978,27.974,47.043,27.854,47.115,27.802,47.168,27.768,47.228,27.696,47.286,27.614,47.341,27.516,47.476,27.465,47.537,27.449,47.553,27.337,47.64,27.278,47.718,27.248,47.782,27.231,47.842,27.152,47.959,27.08,48.048,27.012,48.11,26.981,48.155,26.901,48.211,26.787,48.256,26.714,48.263,26.619,48.26]]]},{"n":"Monaco","c":"MC","r":6,"ml":5,"lx":7.4,"ly":43.74,"b":[7.378,43.732,7.439,43.771],"p":[[[7.439,43.75,7.378,43.732,7.38,43.753,7.395,43.765,7.414,43.771,7.437,43.761,7.439,43.75]]]},{"n":"Mexico","c":"MX","r":2,"ml":2,"lx":-102.29,"ly":23.92,"b":[-118.401,14.545,-86.696,32.715],"p":[[[-117.128,32.533,-116.842,32.555,-116.556,32.576,-116.27,32.598,-115.984,32.619,-115.698,32.64,-115.411,32.662,-115.125,32.683,-114.839,32.705,-114.725,32.715,-114.788,32.565,-114.836,32.508,-114.362,32.36,-113.887,32.212,-113.413,32.064,-112.939,31.916,-112.465,31.768,-111.99,31.62,-111.516,31.472,-111.042,31.324,-110.689,31.325,-110.335,31.326,-109.982,31.326,-109.628,31.327,-109.275,31.327,-108.921,31.328,-108.568,31.329,-108.214,31.329,-108.214,31.442,-108.213,31.554,-108.212,31.667,-108.212,31.779,-107.992,31.778,-107.772,31.777,-107.552,31.776,-107.333,31.775,-107.113,31.774,-106.893,31.772,-106.673,31.771,-106.453,31.77,-106.445,31.768,-106.436,31.764,-106.347,31.679,-106.256,31.545,-106.148,31.451,-106.024,31.398,-105.813,31.241,-105.514,30.981,-105.276,30.807,-105.098,30.721,-104.979,30.646,-104.918,30.583,-104.836,30.448,-104.681,30.134,-104.681,29.991,-104.622,29.854,-104.504,29.678,-104.401,29.574,-104.312,29.542,-104.216,29.48,-104.111,29.386,-103.99,29.323,-103.853,29.291,-103.664,29.207,-103.423,29.071,-103.258,29.001,-103.168,28.998,-103.09,29.042,-103.023,29.132,-102.957,29.19,-102.892,29.216,-102.866,29.258,-102.878,29.315,-102.834,29.444,-102.734,29.644,-102.615,29.752,-102.476,29.769,-102.386,29.807,-102.343,29.865,-102.269,29.871,-102.163,29.825,-101.991,29.796,-101.752,29.782,-101.612,29.787,-101.569,29.809,-101.546,29.808,-101.545,29.784,-101.509,29.773,-101.44,29.777,-101.38,29.743,-101.304,29.634,-101.039,29.46,-101.016,29.401,-100.924,29.315,-100.755,29.183,-100.659,29.069,-100.636,28.973,-100.55,28.821,-100.399,28.614,-100.332,28.503,-100.348,28.486,-100.336,28.428,-100.296,28.328,-100.221,28.243,-100.112,28.173,-100.001,28.048,-99.89,27.867,-99.754,27.73,-99.595,27.636,-99.505,27.548,-99.484,27.467,-99.486,27.398,-99.51,27.34,-99.5,27.285,-99.455,27.234,-99.44,27.17,-99.458,27.082,-99.457,27.057,-99.456,27.057,-99.444,27.037,-99.302,26.885,-99.23,26.762,-99.172,26.566,-99.172,26.564,-99.108,26.447,-99.015,26.399,-98.873,26.381,-98.765,26.34,-98.691,26.276,-98.598,26.238,-98.486,26.225,-98.378,26.182,-98.275,26.111,-98.083,26.064,-97.801,26.042,-97.587,25.984,-97.44,25.891,-97.376,25.872,-97.358,25.871,-97.35,25.885,-97.339,25.911,-97.282,25.942,-97.146,25.961,-97.164,25.755,-97.225,25.585,-97.424,25.233,-97.507,25.015,-97.668,24.39,-97.717,23.981,-97.729,23.788,-97.743,23.761,-97.727,23.732,-97.766,23.306,-97.745,22.942,-97.758,22.886,-97.817,22.776,-97.858,22.625,-97.842,22.557,-97.842,22.51,-97.782,22.279,-97.763,22.106,-97.585,21.809,-97.485,21.705,-97.36,21.615,-97.315,21.564,-97.337,21.438,-97.388,21.374,-97.409,21.273,-97.434,21.356,-97.424,21.465,-97.385,21.524,-97.383,21.567,-97.457,21.612,-97.59,21.762,-97.754,22.027,-97.638,21.604,-97.598,21.536,-97.567,21.508,-97.515,21.478,-97.501,21.432,-97.501,21.398,-97.357,21.104,-97.195,20.8,-97.186,20.717,-97.121,20.615,-96.709,20.188,-96.456,19.87,-96.368,19.567,-96.315,19.473,-96.29,19.344,-96.124,19.199,-96.073,19.106,-95.985,19.054,-95.913,18.897,-95.778,18.806,-95.81,18.804,-95.928,18.85,-95.92,18.82,-95.821,18.755,-95.627,18.691,-95.578,18.69,-95.655,18.724,-95.72,18.768,-95.697,18.775,-95.561,18.719,-95.182,18.701,-95.015,18.571,-94.798,18.515,-94.682,18.348,-94.546,18.175,-94.46,18.167,-94.392,18.166,-94.189,18.195,-93.873,18.304,-93.764,18.358,-93.552,18.43,-93.228,18.444,-93.127,18.423,-92.885,18.469,-92.769,18.524,-92.729,18.575,-92.71,18.612,-92.485,18.665,-92.441,18.675,-92.213,18.685,-92.103,18.704,-91.974,18.716,-91.88,18.638,-91.88,18.6,-91.943,18.563,-91.914,18.529,-91.803,18.471,-91.6,18.447,-91.534,18.457,-91.44,18.542,-91.275,18.624,-91.279,18.721,-91.308,18.773,-91.356,18.777,-91.368,18.806,-91.334,18.877,-91.343,18.901,-91.446,18.833,-91.469,18.833,-91.458,18.865,-91.437,18.89,-91.136,19.038,-91.059,19.098,-90.955,19.152,-90.739,19.352,-90.693,19.73,-90.65,19.796,-90.507,19.912,-90.492,19.947,-90.482,20.026,-90.486,20.224,-90.478,20.38,-90.484,20.556,-90.458,20.714,-90.435,20.758,-90.353,21.009,-90.183,21.121,-89.888,21.253,-89.82,21.275,-88.879,21.414,-88.747,21.448,-88.585,21.539,-88.467,21.569,-88.251,21.567,-88.185,21.579,-88.172,21.591,-88.171,21.604,-88.132,21.616,-88.007,21.604,-87.774,21.55,-87.689,21.536,-87.48,21.472,-87.251,21.447,-87.218,21.458,-87.188,21.477,-87.164,21.514,-87.188,21.546,-87.211,21.544,-87.249,21.527,-87.296,21.525,-87.387,21.551,-87.369,21.574,-87.276,21.572,-87.216,21.582,-87.128,21.621,-87.035,21.592,-86.912,21.463,-86.824,21.422,-86.817,21.234,-86.804,21.2,-86.772,21.151,-86.816,21.005,-86.865,20.885,-86.926,20.786,-87.06,20.631,-87.221,20.507,-87.421,20.231,-87.467,20.102,-87.466,19.999,-87.432,19.898,-87.442,19.862,-87.466,19.824,-87.507,19.827,-87.586,19.779,-87.688,19.637,-87.69,19.594,-87.645,19.554,-87.587,19.573,-87.512,19.575,-87.469,19.586,-87.425,19.583,-87.435,19.502,-87.483,19.444,-87.513,19.426,-87.567,19.416,-87.628,19.383,-87.659,19.352,-87.656,19.258,-87.622,19.25,-87.551,19.321,-87.509,19.317,-87.501,19.288,-87.594,19.046,-87.653,18.799,-87.734,18.655,-87.762,18.446,-87.804,18.357,-87.853,18.269,-87.882,18.274,-87.96,18.441,-88.039,18.484,-88.056,18.524,-88.011,18.727,-88.032,18.839,-88.074,18.834,-88.127,18.773,-88.197,18.72,-88.195,18.643,-88.276,18.515,-88.296,18.472,-88.372,18.482,-88.461,18.477,-88.523,18.446,-88.586,18.291,-88.744,18.072,-88.806,17.966,-88.857,17.929,-88.898,17.915,-88.943,17.94,-89.05,18,-89.134,17.971,-89.162,17.902,-89.161,17.815,-89.372,17.815,-89.729,17.815,-90.184,17.816,-90.622,17.816,-90.989,17.816,-90.99,17.621,-90.992,17.447,-90.993,17.252,-91.196,17.254,-91.41,17.256,-91.392,17.236,-91.319,17.2,-91.224,17.112,-91.112,16.976,-90.976,16.868,-90.816,16.787,-90.711,16.708,-90.66,16.631,-90.634,16.565,-90.634,16.511,-90.576,16.468,-90.471,16.44,-90.417,16.391,-90.417,16.351,-90.45,16.261,-90.46,16.162,-90.447,16.073,-90.522,16.071,-90.703,16.071,-90.98,16.071,-91.234,16.071,-91.434,16.07,-91.737,16.07,-91.819,15.932,-91.957,15.703,-92.082,15.496,-92.187,15.321,-92.204,15.275,-92.204,15.238,-92.075,15.074,-92.099,15.027,-92.144,15.002,-92.159,14.964,-92.156,14.901,-92.186,14.818,-92.176,14.761,-92.16,14.691,-92.187,14.63,-92.209,14.571,-92.235,14.545,-92.265,14.568,-92.531,14.84,-92.809,15.139,-92.918,15.236,-93.024,15.31,-93.167,15.448,-93.541,15.75,-93.734,15.888,-93.916,16.054,-94.079,16.145,-94.24,16.205,-94.311,16.239,-94.374,16.285,-94.409,16.287,-94.426,16.226,-94.37,16.195,-94.303,16.169,-94.25,16.168,-94.193,16.146,-94.028,16.062,-94.001,16.019,-94.471,16.187,-94.662,16.202,-94.682,16.228,-94.587,16.316,-94.617,16.348,-94.651,16.352,-94.753,16.291,-94.791,16.287,-94.797,16.327,-94.793,16.365,-94.859,16.42,-94.9,16.417,-94.935,16.379,-95.024,16.306,-95.021,16.278,-94.846,16.247,-94.786,16.229,-94.799,16.21,-94.949,16.21,-95.134,16.177,-95.464,15.975,-95.772,15.888,-96.214,15.693,-96.409,15.683,-96.511,15.652,-96.808,15.726,-97.185,15.909,-97.755,15.967,-98.139,16.206,-98.52,16.305,-98.762,16.535,-98.908,16.545,-99.002,16.581,-99.348,16.665,-99.691,16.72,-100.025,16.921,-100.243,16.984,-100.432,17.064,-100.848,17.2,-101.002,17.276,-101.148,17.393,-101.385,17.514,-101.487,17.615,-101.6,17.652,-101.762,17.842,-101.847,17.922,-101.919,17.96,-101.996,17.973,-102.217,17.957,-102.547,18.041,-102.7,18.063,-103.019,18.187,-103.442,18.325,-103.58,18.484,-103.699,18.633,-103.912,18.828,-104.046,18.912,-104.277,19.011,-104.405,19.091,-104.603,19.153,-104.938,19.309,-105.045,19.443,-105.108,19.562,-105.286,19.706,-105.482,19.976,-105.532,20.075,-105.57,20.228,-105.616,20.316,-105.669,20.386,-105.642,20.436,-105.543,20.498,-105.377,20.512,-105.26,20.579,-105.245,20.634,-105.252,20.669,-105.327,20.753,-105.42,20.775,-105.492,20.777,-105.511,20.809,-105.456,20.844,-105.394,20.926,-105.302,21.027,-105.237,21.119,-105.225,21.25,-105.233,21.38,-105.209,21.491,-105.431,21.618,-105.457,21.672,-105.527,21.818,-105.649,21.988,-105.646,22.327,-105.792,22.627,-105.943,22.777,-106.022,22.829,-106.235,23.061,-106.402,23.196,-106.567,23.449,-106.729,23.611,-106.935,23.881,-107.085,24.016,-107.765,24.472,-107.727,24.472,-107.527,24.36,-107.494,24.369,-107.489,24.424,-107.512,24.489,-107.549,24.505,-107.602,24.49,-107.674,24.504,-107.71,24.525,-107.817,24.539,-107.951,24.615,-108.009,24.694,-108.015,24.783,-108.208,24.975,-108.281,25.082,-108.243,25.074,-108.192,25.031,-108.14,25.018,-108.08,25.018,-108.036,25.035,-108.051,25.067,-108.093,25.094,-108.374,25.194,-108.466,25.265,-108.696,25.383,-108.751,25.424,-108.787,25.538,-108.844,25.543,-108.893,25.512,-109.029,25.48,-109.063,25.517,-109.068,25.552,-108.973,25.588,-108.885,25.696,-108.887,25.733,-108.935,25.69,-109.008,25.642,-109.084,25.615,-109.196,25.593,-109.254,25.609,-109.304,25.633,-109.385,25.727,-109.426,26.033,-109.354,26.138,-109.271,26.243,-109.2,26.305,-109.159,26.258,-109.117,26.253,-109.146,26.306,-109.216,26.355,-109.241,26.405,-109.243,26.45,-109.276,26.534,-109.483,26.71,-109.676,26.697,-109.755,26.703,-109.828,26.77,-109.891,26.883,-109.922,26.978,-109.926,27.029,-109.944,27.079,-110.277,27.162,-110.377,27.233,-110.478,27.323,-110.519,27.396,-110.561,27.45,-110.593,27.544,-110.615,27.654,-110.578,27.796,-110.53,27.864,-110.759,27.915,-110.849,27.918,-110.921,27.889,-110.986,27.926,-111.121,27.967,-111.282,28.115,-111.472,28.384,-111.68,28.471,-111.747,28.564,-111.832,28.648,-111.907,28.752,-111.919,28.798,-111.941,28.823,-112.045,28.896,-112.162,29.019,-112.192,29.118,-112.223,29.269,-112.301,29.323,-112.378,29.348,-112.393,29.42,-112.389,29.46,-112.415,29.536,-112.573,29.72,-112.653,29.87,-112.697,29.917,-112.738,29.985,-112.759,30.126,-112.825,30.3,-112.952,30.51,-113.058,30.651,-113.11,30.793,-113.087,30.938,-113.105,31.027,-113.119,31.048,-113.108,31.077,-113.073,31.061,-113.043,31.087,-113.047,31.179,-113.084,31.207,-113.186,31.236,-113.231,31.256,-113.481,31.294,-113.623,31.346,-113.633,31.468,-113.7,31.523,-113.759,31.558,-113.948,31.629,-113.977,31.593,-114.003,31.525,-114.081,31.51,-114.149,31.507,-114.264,31.554,-114.549,31.734,-114.609,31.762,-114.698,31.777,-114.741,31.806,-114.934,31.901,-114.895,31.851,-114.84,31.799,-114.79,31.647,-114.848,31.538,-114.882,31.156,-114.845,31.08,-114.761,30.959,-114.703,30.765,-114.685,30.621,-114.633,30.507,-114.65,30.238,-114.63,30.156,-114.55,30.022,-114.403,29.896,-114.373,29.83,-114.179,29.734,-114.062,29.61,-113.829,29.439,-113.755,29.367,-113.545,29.102,-113.538,29.023,-113.5,28.927,-113.382,28.947,-113.329,28.873,-113.335,28.839,-113.321,28.813,-113.259,28.819,-113.206,28.799,-113.094,28.512,-113.034,28.473,-112.957,28.456,-112.871,28.424,-112.865,28.351,-112.868,28.292,-112.796,28.207,-112.808,28.092,-112.749,27.995,-112.758,27.901,-112.734,27.826,-112.553,27.657,-112.329,27.523,-112.283,27.347,-112.191,27.187,-112.098,27.146,-112.004,27.079,-112.016,27.01,-112.009,26.967,-111.883,26.84,-111.863,26.679,-111.754,26.573,-111.723,26.564,-111.699,26.581,-111.779,26.687,-111.817,26.756,-111.822,26.865,-111.795,26.88,-111.57,26.708,-111.546,26.579,-111.47,26.507,-111.465,26.408,-111.419,26.35,-111.405,26.265,-111.332,26.125,-111.33,25.931,-111.292,25.79,-111.15,25.573,-111.034,25.527,-111.014,25.42,-110.894,25.144,-110.756,24.995,-110.687,24.868,-110.677,24.789,-110.729,24.672,-110.735,24.59,-110.659,24.341,-110.547,24.214,-110.421,24.183,-110.4,24.165,-110.41,24.131,-110.367,24.1,-110.32,24.139,-110.297,24.195,-110.321,24.259,-110.325,24.306,-110.304,24.339,-110.263,24.345,-110.023,24.175,-109.983,24.109,-109.893,24.033,-109.811,23.939,-109.776,23.865,-109.711,23.804,-109.677,23.662,-109.51,23.598,-109.421,23.48,-109.415,23.406,-109.458,23.215,-109.496,23.16,-109.63,23.079,-109.728,22.982,-109.823,22.922,-109.923,22.886,-110.006,22.894,-110.086,23.005,-110.181,23.342,-110.244,23.412,-110.289,23.518,-110.363,23.605,-110.63,23.737,-110.765,23.877,-110.896,23.97,-111.036,24.105,-111.419,24.329,-111.578,24.443,-111.683,24.556,-111.75,24.554,-111.802,24.543,-111.822,24.573,-111.825,24.632,-111.848,24.67,-112.073,24.84,-112.119,24.934,-112.129,25.043,-112.078,25.324,-112.056,25.488,-112.07,25.573,-112.093,25.584,-112.115,25.63,-112.12,25.766,-112.174,25.913,-112.377,26.214,-112.526,26.273,-112.658,26.317,-113.021,26.583,-113.119,26.717,-113.143,26.792,-113.156,26.946,-113.206,26.857,-113.272,26.791,-113.426,26.796,-113.599,26.721,-113.701,26.791,-113.757,26.871,-113.841,26.967,-113.936,26.985,-113.996,26.988,-114.11,27.106,-114.202,27.144,-114.333,27.158,-114.445,27.218,-114.48,27.284,-114.498,27.376,-114.54,27.431,-114.716,27.54,-114.859,27.659,-114.994,27.736,-115.033,27.799,-115.036,27.842,-114.824,27.83,-114.57,27.784,-114.448,27.797,-114.373,27.841,-114.301,27.873,-114.289,27.839,-114.302,27.776,-114.233,27.718,-114.137,27.671,-114.069,27.676,-114.135,27.727,-114.175,27.831,-114.157,27.868,-114.158,27.92,-114.253,27.908,-114.266,27.934,-114.185,28.013,-114.093,28.221,-114.048,28.426,-114.146,28.605,-114.309,28.73,-114.664,29.095,-114.876,29.282,-114.937,29.352,-114.994,29.384,-115.166,29.427,-115.311,29.532,-115.565,29.68,-115.674,29.756,-115.749,29.936,-115.808,29.96,-115.79,30.084,-115.816,30.304,-115.858,30.36,-115.996,30.414,-116.029,30.564,-116.035,30.705,-116.062,30.804,-116.296,30.971,-116.31,31.051,-116.31,31.127,-116.333,31.203,-116.458,31.361,-116.61,31.499,-116.662,31.565,-116.668,31.699,-116.722,31.735,-116.702,31.744,-116.652,31.74,-116.624,31.758,-116.621,31.851,-116.848,31.997,-116.914,32.199,-117.035,32.305,-117.063,32.344,-117.128,32.533]],[[-86.94,20.303,-86.991,20.272,-87.019,20.382,-86.978,20.49,-86.928,20.552,-86.829,20.559,-86.763,20.579,-86.755,20.552,-86.809,20.468,-86.94,20.303]],[[-106.502,21.611,-106.531,21.529,-106.607,21.561,-106.634,21.613,-106.639,21.698,-106.597,21.712,-106.536,21.676,-106.524,21.652,-106.502,21.611]],[[-110.914,18.741,-110.975,18.72,-111.064,18.782,-111.04,18.83,-110.989,18.863,-110.942,18.802,-110.914,18.741]],[[-110.567,25.003,-110.539,24.892,-110.59,24.908,-110.657,24.969,-110.703,25.047,-110.699,25.081,-110.69,25.088,-110.595,25.042,-110.567,25.003]],[[-113.156,29.052,-113.163,29.035,-113.265,29.097,-113.496,29.308,-113.581,29.413,-113.594,29.463,-113.587,29.573,-113.508,29.56,-113.416,29.486,-113.376,29.417,-113.374,29.339,-113.202,29.302,-113.178,29.132,-113.156,29.052]],[[-115.171,28.069,-115.184,28.037,-115.353,28.104,-115.26,28.221,-115.274,28.343,-115.234,28.368,-115.197,28.328,-115.149,28.172,-115.171,28.069]],[[-112.203,29.005,-112.278,28.769,-112.355,28.773,-112.514,28.848,-112.531,28.894,-112.47,29.168,-112.424,29.204,-112.285,29.24,-112.263,29.207,-112.249,29.126,-112.203,29.005]],[[-118.243,28.942,-118.285,28.904,-118.4,29.112,-118.401,29.163,-118.368,29.188,-118.312,29.183,-118.312,29.131,-118.266,29.086,-118.247,29.043,-118.243,28.942]],[[-86.714,21.239,-86.696,21.191,-86.714,21.197,-86.736,21.233,-86.753,21.279,-86.739,21.28,-86.727,21.264,-86.714,21.239]],[[-91.684,18.677,-91.796,18.654,-91.816,18.676,-91.589,18.778,-91.55,18.774,-91.537,18.76,-91.654,18.711,-91.684,18.677]],[[-109.805,24.151,-109.827,24.148,-109.878,24.201,-109.9,24.331,-109.89,24.345,-109.794,24.183,-109.796,24.164,-109.805,24.151]],[[-114.694,31.706,-114.727,31.701,-114.789,31.747,-114.785,31.79,-114.771,31.794,-114.709,31.757,-114.688,31.724,-114.694,31.706]],[[-111.1,26.021,-111.088,25.985,-111.094,25.974,-111.135,25.999,-111.204,25.85,-111.225,25.836,-111.183,26.041,-111.139,26.07,-111.091,26.076,-111.1,26.021]],[[-111.699,24.394,-111.712,24.346,-112.013,24.533,-111.941,24.551,-111.857,24.538,-111.699,24.394]],[[-112.057,24.546,-112.077,24.535,-112.163,24.65,-112.175,24.73,-112.21,24.763,-112.297,24.79,-112.222,24.951,-112.159,25.286,-112.132,25.224,-112.198,24.885,-112.195,24.841,-112.164,24.8,-112.13,24.73,-112.126,24.654,-112.067,24.584,-112.057,24.546]]]},{"n":"Mauritius","c":"MU","r":5,"ml":4,"lx":57.57,"ly":-20.3,"b":[57.318,-20.513,57.792,-19.99],"p":[[[57.651,-20.485,57.525,-20.513,57.383,-20.504,57.328,-20.45,57.318,-20.428,57.365,-20.406,57.362,-20.338,57.386,-20.229,57.416,-20.184,57.486,-20.144,57.515,-20.056,57.576,-19.997,57.657,-19.99,57.737,-20.098,57.792,-20.213,57.781,-20.327,57.725,-20.369,57.707,-20.435,57.651,-20.485]]]},{"n":"Mauritania","c":"MR","r":3,"ml":3,"lx":-9.74,"ly":19.59,"b":[-17.064,14.745,-4.823,27.286],"p":[[[-16.373,19.706,-16.438,19.609,-16.466,19.646,-16.477,19.71,-16.42,19.802,-16.393,19.849,-16.344,19.866,-16.373,19.706]],[[-12.281,14.809,-12.303,14.817,-12.409,14.889,-12.46,14.975,-12.544,15.039,-12.66,15.082,-12.735,15.131,-12.77,15.187,-12.813,15.224,-12.858,15.243,-12.863,15.262,-12.852,15.29,-12.863,15.34,-12.931,15.453,-12.994,15.505,-13.049,15.497,-13.079,15.51,-13.098,15.535,-13.105,15.572,-13.142,15.603,-13.206,15.617,-13.258,15.7,-13.297,15.854,-13.348,15.973,-13.41,16.059,-13.454,16.091,-13.487,16.097,-13.498,16.11,-13.507,16.135,-13.556,16.144,-13.624,16.118,-13.685,16.127,-13.715,16.169,-13.757,16.173,-13.81,16.138,-13.868,16.148,-13.933,16.203,-13.968,16.257,-13.975,16.311,-14.086,16.419,-14.3,16.58,-14.534,16.656,-14.787,16.646,-14.929,16.654,-14.96,16.679,-14.991,16.677,-15.022,16.647,-15.055,16.641,-15.091,16.657,-15.113,16.645,-15.121,16.604,-15.211,16.583,-15.38,16.582,-15.517,16.557,-15.621,16.507,-15.768,16.485,-15.959,16.492,-16.074,16.51,-16.113,16.54,-16.168,16.547,-16.239,16.531,-16.302,16.451,-16.358,16.307,-16.404,16.225,-16.441,16.205,-16.48,16.097,-16.502,15.917,-16.535,15.838,-16.536,16.287,-16.481,16.454,-16.464,16.602,-16.347,16.926,-16.207,17.193,-16.079,17.546,-16.03,17.888,-16.047,18.223,-16.085,18.521,-16.15,18.718,-16.213,19.003,-16.306,19.154,-16.476,19.285,-16.514,19.362,-16.475,19.391,-16.371,19.41,-16.305,19.513,-16.445,19.473,-16.283,19.787,-16.233,20.001,-16.241,20.141,-16.21,20.228,-16.334,20.416,-16.43,20.652,-16.479,20.69,-16.53,20.71,-16.535,20.654,-16.563,20.604,-16.623,20.634,-16.728,20.806,-16.876,21.086,-16.928,21.115,-16.971,21.076,-16.998,21.04,-17.048,20.806,-17.064,20.899,-17.042,21.008,-17.006,21.142,-16.965,21.329,-16.836,21.329,-16.607,21.33,-16.378,21.33,-16.148,21.33,-15.919,21.33,-15.69,21.331,-15.461,21.331,-15.231,21.331,-15.002,21.332,-14.773,21.332,-14.543,21.332,-14.314,21.332,-14.085,21.333,-13.855,21.333,-13.626,21.333,-13.397,21.334,-13.167,21.334,-13.016,21.334,-13.025,21.467,-13.032,21.572,-13.042,21.714,-13.051,21.855,-13.061,21.996,-13.07,22.128,-13.078,22.26,-13.087,22.383,-13.094,22.496,-13.107,22.561,-13.156,22.689,-13.167,22.753,-13.153,22.821,-13.121,22.884,-13.031,23,-12.896,23.09,-12.74,23.193,-12.62,23.271,-12.559,23.291,-12.373,23.318,-12.226,23.377,-12.083,23.435,-12.023,23.468,-12.016,23.576,-12.016,23.698,-12.016,23.834,-12.016,23.97,-12.016,24.106,-12.016,24.242,-12.016,24.379,-12.016,24.515,-12.016,24.651,-12.016,24.787,-12.016,24.923,-12.016,25.059,-12.016,25.196,-12.016,25.332,-12.016,25.468,-12.016,25.604,-12.016,25.74,-12.016,25.876,-12.016,25.995,-11.867,25.995,-11.68,25.995,-11.494,25.995,-11.308,25.995,-11.121,25.995,-10.935,25.995,-10.749,25.995,-10.562,25.995,-10.376,25.995,-10.19,25.995,-10.004,25.995,-9.817,25.995,-9.631,25.996,-9.445,25.996,-9.258,25.996,-9.072,25.996,-8.886,25.996,-8.682,25.996,-8.682,26.109,-8.682,26.273,-8.682,26.498,-8.683,26.723,-8.683,26.921,-8.683,27.119,-8.683,27.286,-8.495,27.175,-8.307,27.065,-8.119,26.954,-7.931,26.844,-7.743,26.733,-7.555,26.622,-7.367,26.512,-7.179,26.401,-6.991,26.291,-6.803,26.18,-6.615,26.069,-6.427,25.959,-6.239,25.848,-6.051,25.738,-5.863,25.627,-5.675,25.516,-5.517,25.424,-5.275,25.275,-5.05,25.135,-4.823,24.996,-5.173,24.995,-5.641,24.995,-5.96,24.995,-6.287,24.995,-6.594,24.995,-6.567,24.767,-6.539,24.518,-6.51,24.269,-6.482,24.021,-6.454,23.772,-6.425,23.523,-6.397,23.275,-6.368,23.026,-6.34,22.777,-6.311,22.529,-6.283,22.28,-6.254,22.032,-6.226,21.783,-6.197,21.534,-6.169,21.286,-6.14,21.037,-6.112,20.788,-6.083,20.54,-6.055,20.291,-6.026,20.042,-5.998,19.794,-5.969,19.545,-5.941,19.296,-5.912,19.048,-5.884,18.799,-5.856,18.55,-5.827,18.302,-5.799,18.053,-5.77,17.804,-5.742,17.556,-5.713,17.307,-5.685,17.058,-5.656,16.81,-5.629,16.569,-5.51,16.442,-5.36,16.283,-5.404,16.058,-5.456,15.789,-5.512,15.496,-5.724,15.496,-5.928,15.496,-6.132,15.496,-6.336,15.496,-6.54,15.496,-6.744,15.496,-6.948,15.496,-7.152,15.496,-7.355,15.496,-7.559,15.496,-7.763,15.496,-7.967,15.496,-8.171,15.496,-8.375,15.496,-8.579,15.496,-8.783,15.496,-8.987,15.496,-9.177,15.496,-9.294,15.503,-9.335,15.526,-9.351,15.677,-9.385,15.668,-9.427,15.623,-9.448,15.575,-9.44,15.512,-9.447,15.458,-9.578,15.437,-9.755,15.401,-9.941,15.374,-10.13,15.384,-10.194,15.396,-10.262,15.416,-10.412,15.438,-10.493,15.44,-10.587,15.435,-10.697,15.423,-10.732,15.395,-10.815,15.282,-10.896,15.15,-10.948,15.151,-11.007,15.223,-11.169,15.359,-11.366,15.537,-11.455,15.625,-11.503,15.637,-11.597,15.573,-11.676,15.512,-11.76,15.426,-11.798,15.343,-11.829,15.245,-11.842,15.129,-11.873,14.995,-11.941,14.887,-12.022,14.805,-12.082,14.766,-12.105,14.745,-12.281,14.809]]]},{"n":"Malta","c":"MT","r":5,"ml":4,"lx":14.43,"ly":35.89,"b":[14.18,35.82,14.566,36.076],"p":[[[14.566,35.853,14.533,35.82,14.436,35.822,14.352,35.872,14.351,35.978,14.448,35.957,14.537,35.886,14.566,35.853]],[[14.313,36.028,14.254,36.012,14.194,36.042,14.18,36.06,14.263,36.076,14.304,36.062,14.321,36.036,14.313,36.028]]]},{"n":"Mali","c":"ML","r":3,"ml":3,"lx":-2.04,"ly":18.69,"b":[-12.281,10.143,4.235,24.996],"p":[[[-11.389,12.404,-11.382,12.479,-11.449,12.532,-11.451,12.558,-11.444,12.628,-11.414,12.775,-11.417,12.832,-11.39,12.942,-11.434,12.992,-11.444,13.028,-11.493,13.087,-11.549,13.17,-11.562,13.237,-11.581,13.29,-11.635,13.37,-11.674,13.382,-11.758,13.395,-11.772,13.367,-11.803,13.327,-11.832,13.316,-11.878,13.365,-11.895,13.406,-11.895,13.444,-11.957,13.511,-12.054,13.633,-12.044,13.734,-11.984,13.788,-11.966,13.829,-11.961,13.875,-11.988,13.931,-12.02,13.975,-12.011,14.072,-12.019,14.206,-12.068,14.274,-12.113,14.323,-12.175,14.377,-12.228,14.459,-12.207,14.571,-12.187,14.648,-12.281,14.809,-12.105,14.745,-12.082,14.766,-12.022,14.805,-11.941,14.887,-11.873,14.995,-11.842,15.129,-11.829,15.245,-11.798,15.343,-11.76,15.426,-11.676,15.512,-11.597,15.573,-11.503,15.637,-11.455,15.625,-11.366,15.537,-11.169,15.359,-11.007,15.223,-10.948,15.151,-10.896,15.15,-10.815,15.282,-10.732,15.395,-10.697,15.423,-10.587,15.435,-10.493,15.44,-10.412,15.438,-10.262,15.416,-10.194,15.396,-10.13,15.384,-9.941,15.374,-9.755,15.401,-9.578,15.437,-9.447,15.458,-9.44,15.512,-9.448,15.575,-9.427,15.623,-9.385,15.668,-9.351,15.677,-9.335,15.526,-9.294,15.503,-9.177,15.496,-8.987,15.496,-8.783,15.496,-8.579,15.496,-8.375,15.496,-8.171,15.496,-7.967,15.496,-7.763,15.496,-7.559,15.496,-7.355,15.496,-7.152,15.496,-6.948,15.496,-6.744,15.496,-6.54,15.496,-6.336,15.496,-6.132,15.496,-5.928,15.496,-5.724,15.496,-5.512,15.496,-5.456,15.789,-5.404,16.058,-5.36,16.283,-5.51,16.442,-5.629,16.569,-5.656,16.81,-5.685,17.058,-5.713,17.307,-5.742,17.556,-5.77,17.804,-5.799,18.053,-5.827,18.302,-5.856,18.55,-5.884,18.799,-5.912,19.048,-5.941,19.296,-5.969,19.545,-5.998,19.794,-6.026,20.042,-6.055,20.291,-6.083,20.54,-6.112,20.788,-6.14,21.037,-6.169,21.286,-6.197,21.534,-6.226,21.783,-6.254,22.032,-6.283,22.28,-6.311,22.529,-6.34,22.777,-6.368,23.026,-6.397,23.275,-6.425,23.523,-6.454,23.772,-6.482,24.021,-6.51,24.269,-6.539,24.518,-6.567,24.767,-6.594,24.995,-6.287,24.995,-5.96,24.995,-5.641,24.995,-5.173,24.995,-4.823,24.996,-4.517,24.804,-4.24,24.624,-3.913,24.409,-3.585,24.195,-3.258,23.981,-2.93,23.767,-2.603,23.553,-2.275,23.339,-1.948,23.125,-1.62,22.911,-1.293,22.697,-0.965,22.482,-0.638,22.268,-0.311,22.054,0.017,21.84,0.344,21.626,0.672,21.412,0.999,21.198,1.146,21.102,1.159,21.063,1.173,20.982,1.164,20.891,1.166,20.817,1.209,20.767,1.29,20.714,1.611,20.556,1.636,20.524,1.647,20.459,1.685,20.378,1.753,20.332,1.832,20.297,1.929,20.273,2.219,20.248,2.281,20.21,2.406,20.064,2.474,20.035,2.668,19.993,2.808,19.969,2.866,19.956,2.992,19.917,3.13,19.85,3.204,19.79,3.203,19.771,3.203,19.718,3.202,19.56,3.227,19.474,3.256,19.411,3.254,19.373,3.22,19.345,3.192,19.312,3.177,19.268,3.138,19.212,3.106,19.15,3.12,19.103,3.174,19.073,3.256,19.013,3.323,18.988,3.356,18.987,3.401,18.988,3.439,18.996,3.683,19.042,3.91,19.084,4.228,19.143,4.228,18.968,4.229,18.704,4.23,18.411,4.231,18.139,4.232,17.831,4.233,17.582,4.234,17.288,4.235,16.996,4.203,16.963,4.191,16.798,4.182,16.582,4.121,16.358,4.015,16.193,3.976,16.036,3.947,15.946,3.907,15.897,3.898,15.838,3.877,15.755,3.843,15.702,3.817,15.674,3.71,15.642,3.521,15.483,3.504,15.356,3.289,15.391,3.06,15.427,3.029,15.425,3.011,15.408,3.001,15.341,2.69,15.33,2.421,15.32,2.088,15.309,1.859,15.302,1.569,15.286,1.3,15.272,1.121,15.126,0.96,14.987,0.947,14.982,0.719,14.955,0.433,14.979,0.286,14.98,0.229,14.964,0.217,14.911,0.007,14.985,-0.236,15.059,-0.405,15.013,-0.432,15.029,-0.454,15.06,-0.537,15.078,-0.666,15.07,-0.76,15.048,-0.908,14.937,-1.019,14.841,-1.05,14.82,-1.205,14.762,-1.494,14.626,-1.657,14.527,-1.695,14.508,-1.768,14.486,-1.88,14.481,-1.973,14.457,-2.057,14.195,-2.113,14.168,-2.457,14.274,-2.527,14.258,-2.587,14.228,-2.779,14.074,-2.874,13.951,-2.926,13.787,-2.919,13.736,-2.917,13.679,-2.951,13.648,-2.997,13.637,-3.039,13.639,-3.198,13.673,-3.249,13.658,-3.27,13.577,-3.267,13.401,-3.302,13.281,-3.397,13.244,-3.47,13.196,-3.528,13.183,-3.576,13.194,-3.853,13.374,-3.947,13.402,-4.051,13.382,-4.151,13.306,-4.196,13.256,-4.259,13.197,-4.329,13.119,-4.31,13.052,-4.261,12.975,-4.225,12.879,-4.227,12.794,-4.481,12.672,-4.46,12.63,-4.422,12.582,-4.422,12.493,-4.429,12.338,-4.48,12.282,-4.546,12.226,-4.587,12.155,-4.627,12.12,-4.699,12.076,-4.798,12.032,-4.969,11.993,-5.106,11.968,-5.158,11.942,-5.23,11.89,-5.288,11.828,-5.302,11.76,-5.291,11.683,-5.27,11.62,-5.245,11.577,-5.229,11.522,-5.25,11.376,-5.3,11.206,-5.347,11.13,-5.424,11.089,-5.49,11.042,-5.469,10.931,-5.457,10.771,-5.476,10.644,-5.479,10.565,-5.507,10.483,-5.524,10.426,-5.557,10.44,-5.694,10.433,-5.844,10.39,-5.896,10.355,-5.908,10.307,-5.941,10.275,-5.989,10.239,-6.035,10.195,-6.117,10.202,-6.197,10.232,-6.238,10.262,-6.241,10.279,-6.215,10.322,-6.193,10.369,-6.191,10.4,-6.218,10.476,-6.24,10.558,-6.231,10.598,-6.25,10.718,-6.261,10.724,-6.366,10.693,-6.404,10.685,-6.426,10.672,-6.433,10.649,-6.408,10.572,-6.424,10.559,-6.483,10.561,-6.565,10.586,-6.654,10.656,-6.676,10.634,-6.686,10.578,-6.692,10.512,-6.669,10.392,-6.693,10.349,-6.753,10.357,-6.834,10.357,-6.904,10.345,-6.95,10.342,-6.979,10.3,-6.992,10.252,-6.964,10.199,-6.968,10.176,-6.989,10.156,-7.017,10.143,-7.04,10.145,-7.105,10.204,-7.182,10.226,-7.363,10.259,-7.385,10.34,-7.415,10.341,-7.457,10.384,-7.498,10.44,-7.533,10.437,-7.562,10.421,-7.661,10.427,-7.749,10.342,-7.814,10.237,-7.884,10.186,-7.961,10.163,-7.991,10.163,-7.974,10.23,-7.986,10.278,-8.007,10.322,-8.231,10.438,-8.267,10.486,-8.302,10.618,-8.324,10.75,-8.322,10.827,-8.306,10.896,-8.313,10.95,-8.337,10.991,-8.404,11.03,-8.475,11.048,-8.564,10.997,-8.606,10.987,-8.646,10.99,-8.667,11.009,-8.664,11.036,-8.567,11.177,-8.52,11.236,-8.464,11.281,-8.425,11.305,-8.401,11.339,-8.399,11.367,-8.407,11.386,-8.471,11.412,-8.569,11.478,-8.621,11.485,-8.665,11.515,-8.711,11.618,-8.733,11.638,-8.78,11.648,-8.822,11.673,-8.82,11.807,-8.818,11.923,-8.914,12.109,-8.951,12.226,-8.999,12.346,-9.043,12.402,-9.12,12.45,-9.216,12.483,-9.3,12.49,-9.365,12.479,-9.395,12.465,-9.394,12.442,-9.341,12.366,-9.332,12.324,-9.34,12.283,-9.358,12.255,-9.405,12.252,-9.487,12.229,-9.588,12.182,-9.658,12.143,-9.715,12.042,-9.754,12.03,-9.821,12.042,-10.011,12.116,-10.167,12.177,-10.275,12.213,-10.34,12.19,-10.373,12.18,-10.466,12.139,-10.59,11.99,-10.619,11.941,-10.644,11.926,-10.677,11.899,-10.709,11.899,-10.735,11.916,-10.743,11.927,-10.806,12.034,-10.876,12.152,-10.933,12.205,-11.005,12.208,-11.066,12.171,-11.129,12.095,-11.21,12.025,-11.261,12.004,-11.305,12.015,-11.415,12.104,-11.492,12.167,-11.502,12.199,-11.475,12.247,-11.448,12.319,-11.418,12.378,-11.389,12.404]]]},{"n":"Maldives","c":"MV","r":5,"ml":4,"lx":73.51,"ly":4.17,"b":[73.382,3.229,73.528,4.248],"p":[[[73.417,3.231,73.395,3.229,73.382,3.246,73.385,3.271,73.402,3.289,73.428,3.29,73.443,3.274,73.435,3.25,73.417,3.231]],[[73.512,4.165,73.495,4.155,73.479,4.159,73.473,4.171,73.481,4.188,73.495,4.21,73.504,4.235,73.518,4.248,73.528,4.243,73.527,4.23,73.522,4.211,73.519,4.187,73.512,4.165]]]},{"n":"Malaysia","c":"MY","r":3,"ml":3,"lx":113.84,"ly":2.53,"b":[99.646,0.862,119.266,7.352],"p":[[[100.119,6.442,100.138,6.489,100.161,6.642,100.177,6.672,100.217,6.687,100.261,6.683,100.345,6.55,100.564,6.468,100.629,6.448,100.716,6.481,100.754,6.46,100.794,6.426,100.817,6.332,100.874,6.245,100.989,6.258,101.029,6.245,101.054,6.243,101.076,6.166,101.087,6.034,101.076,5.956,100.993,5.846,100.982,5.771,101.025,5.725,101.082,5.675,101.114,5.637,101.148,5.643,101.191,5.669,101.23,5.734,101.257,5.789,101.404,5.852,101.556,5.908,101.577,5.902,101.601,5.877,101.65,5.796,101.678,5.779,101.72,5.771,101.791,5.779,101.874,5.825,101.917,5.911,101.936,5.979,102.055,6.097,102.068,6.185,102.101,6.242,102.274,6.203,102.34,6.172,102.534,5.863,102.79,5.645,102.899,5.564,102.982,5.525,103.097,5.408,103.197,5.262,103.416,4.85,103.454,4.669,103.469,4.393,103.421,3.977,103.362,3.769,103.373,3.671,103.454,3.521,103.429,3.379,103.445,3.261,103.439,2.933,103.485,2.837,103.537,2.775,103.812,2.58,103.832,2.508,103.968,2.261,104.219,1.723,104.288,1.481,104.28,1.416,104.25,1.389,104.176,1.365,104.115,1.412,104.094,1.446,104.101,1.488,104.076,1.53,104.016,1.579,103.981,1.624,103.991,1.55,103.992,1.455,103.915,1.447,103.817,1.477,103.695,1.45,103.55,1.333,103.48,1.329,103.427,1.43,103.4,1.498,103.357,1.546,102.897,1.792,102.727,1.856,102.548,2.042,102.146,2.248,101.89,2.449,101.781,2.574,101.52,2.684,101.407,2.813,101.351,2.839,101.296,2.885,101.354,3.011,101.33,3.142,101.3,3.253,101.115,3.472,101.025,3.625,100.851,3.777,100.782,3.864,100.715,3.966,100.757,4.002,100.796,4.023,100.76,4.097,100.661,4.226,100.615,4.373,100.615,4.652,100.473,5.044,100.353,5.588,100.374,5.778,100.343,5.984,100.263,6.183,100.158,6.324,100.119,6.442]],[[117.574,4.171,117.537,4.171,117.451,4.193,117.278,4.299,117.101,4.337,116.844,4.34,116.698,4.355,116.639,4.339,116.589,4.338,116.553,4.36,116.515,4.371,116.415,4.308,116.368,4.327,116.32,4.354,116.236,4.363,116.134,4.355,116.022,4.291,115.896,4.349,115.861,4.348,115.837,4.333,115.782,4.254,115.679,4.193,115.628,4.082,115.596,3.976,115.568,3.939,115.561,3.733,115.545,3.634,115.571,3.502,115.566,3.446,115.52,3.362,115.514,3.342,115.49,3.209,115.499,3.173,115.493,3.128,115.454,3.034,115.384,3.009,115.31,2.994,115.247,3.026,115.19,2.974,115.118,2.895,115.086,2.841,115.087,2.791,115.094,2.758,115.079,2.723,115.077,2.687,115.081,2.634,115.13,2.612,115.181,2.567,115.179,2.523,115.151,2.493,115.087,2.446,114.969,2.351,114.836,2.269,114.786,2.25,114.768,2.213,114.759,2.162,114.788,2.052,114.816,2.019,114.831,1.98,114.813,1.934,114.8,1.894,114.751,1.869,114.704,1.851,114.686,1.819,114.661,1.686,114.632,1.617,114.567,1.514,114.546,1.467,114.513,1.452,114.387,1.5,114.275,1.471,114.126,1.452,114,1.455,113.902,1.434,113.835,1.38,113.76,1.311,113.682,1.261,113.622,1.236,113.513,1.308,113.458,1.302,113.359,1.327,113.126,1.408,113.069,1.432,113.007,1.434,112.988,1.457,112.998,1.496,112.988,1.548,112.943,1.567,112.476,1.559,112.342,1.515,112.251,1.48,112.186,1.439,112.167,1.338,112.129,1.244,112.079,1.143,111.923,1.113,111.809,1.012,111.77,0.999,111.691,1.014,111.607,1.023,111.547,0.994,111.483,0.996,111.287,1.043,111.101,1.051,110.996,1.026,110.938,1.017,110.615,0.878,110.506,0.862,110.461,0.882,110.399,0.939,110.315,0.996,110.115,1.19,110.041,1.236,109.992,1.283,109.945,1.338,109.879,1.398,109.818,1.439,109.736,1.523,109.654,1.615,109.636,1.777,109.571,1.806,109.549,1.848,109.539,1.896,109.629,2.028,109.694,1.889,109.72,1.858,109.865,1.764,109.985,1.718,110.114,1.699,110.246,1.695,110.298,1.701,110.349,1.72,110.4,1.7,110.675,1.548,110.782,1.521,110.895,1.532,110.94,1.517,111.098,1.401,111.145,1.387,111.223,1.396,111.123,1.449,111.058,1.487,111.029,1.558,111.047,1.634,111.11,1.684,111.154,1.739,111.17,1.902,111.198,1.985,111.251,2.064,111.268,2.14,111.209,2.198,111.196,2.297,111.209,2.38,111.242,2.436,111.296,2.399,111.351,2.364,111.406,2.368,111.444,2.382,111.451,2.424,111.44,2.498,111.443,2.634,111.513,2.743,111.623,2.818,111.728,2.854,112.119,2.915,112.737,3.07,112.921,3.131,112.988,3.162,113.045,3.205,113.14,3.344,113.32,3.561,113.446,3.741,113.712,4.001,113.924,4.243,113.953,4.289,113.988,4.421,113.99,4.483,113.984,4.546,114.013,4.575,114.054,4.593,114.064,4.593,114.095,4.565,114.169,4.527,114.224,4.478,114.261,4.414,114.288,4.355,114.29,4.304,114.323,4.263,114.417,4.256,114.447,4.204,114.512,4.114,114.572,4.049,114.608,4.024,114.654,4.038,114.725,4.097,114.776,4.169,114.81,4.267,114.783,4.281,114.831,4.354,114.84,4.393,114.818,4.429,114.79,4.464,114.779,4.553,114.76,4.667,114.747,4.718,114.784,4.755,114.865,4.802,114.945,4.856,115.027,4.9,115.029,4.821,115.027,4.691,115.052,4.583,115.107,4.39,115.171,4.364,115.247,4.347,115.291,4.353,115.319,4.365,115.327,4.381,115.279,4.456,115.267,4.634,115.228,4.751,115.168,4.867,115.14,4.9,115.375,4.933,115.428,4.969,115.52,5.049,115.554,5.094,115.582,5.194,115.467,5.254,115.422,5.331,115.419,5.413,115.556,5.567,115.604,5.603,115.625,5.549,115.685,5.535,115.741,5.533,115.797,5.536,115.877,5.614,115.918,5.725,116.06,5.882,116.11,6.003,116.138,6.13,116.495,6.522,116.538,6.583,116.75,6.977,116.776,6.99,116.833,6.952,116.85,6.827,116.842,6.772,116.808,6.691,116.788,6.606,116.812,6.608,116.913,6.66,117.019,6.797,117.078,6.917,117.129,6.969,117.23,6.94,117.252,6.919,117.245,6.833,117.255,6.783,117.294,6.677,117.38,6.612,117.499,6.571,117.61,6.513,117.646,6.474,117.67,6.427,117.694,6.35,117.696,6.272,117.616,6.197,117.65,6.074,117.645,6.002,117.617,5.941,117.501,5.885,117.818,5.94,117.896,5.972,118.004,6.053,118.062,5.983,118.116,5.863,118.072,5.832,117.935,5.788,117.928,5.769,117.974,5.706,118.031,5.712,118.145,5.754,118.249,5.821,118.3,5.82,118.353,5.806,118.456,5.763,118.514,5.729,118.563,5.685,118.595,5.592,118.714,5.559,118.957,5.429,119.003,5.418,119.05,5.415,119.178,5.431,119.223,5.413,119.256,5.366,119.266,5.308,119.263,5.246,119.25,5.199,119.22,5.16,119.132,5.1,118.913,5.023,118.672,4.964,118.551,4.968,118.382,5.019,118.32,5.012,118.261,4.989,118.185,4.829,118.324,4.669,118.563,4.502,118.595,4.461,118.586,4.41,118.548,4.379,118.498,4.362,118.364,4.336,118.229,4.316,118.117,4.288,118.008,4.25,117.896,4.263,117.741,4.338,117.696,4.343,117.65,4.304,117.604,4.2,117.574,4.171]],[[104.222,2.732,104.173,2.721,104.147,2.728,104.129,2.767,104.17,2.857,104.185,2.872,104.223,2.774,104.222,2.732]],[[100.289,5.295,100.264,5.267,100.191,5.283,100.204,5.447,100.246,5.468,100.31,5.438,100.339,5.41,100.289,5.295]],[[99.848,6.466,99.919,6.359,99.883,6.311,99.866,6.297,99.823,6.313,99.783,6.272,99.744,6.263,99.705,6.338,99.657,6.367,99.646,6.418,99.711,6.427,99.749,6.41,99.822,6.445,99.848,6.466]],[[117.142,7.168,117.081,7.115,117.06,7.179,117.064,7.261,117.147,7.337,117.264,7.352,117.281,7.291,117.267,7.221,117.239,7.185,117.142,7.168]],[[101.319,2.988,101.268,2.97,101.265,2.996,101.274,3.033,101.311,3.067,101.328,3.048,101.319,2.988]],[[111.389,2.415,111.359,2.402,111.312,2.438,111.3,2.741,111.333,2.768,111.355,2.764,111.378,2.709,111.376,2.576,111.38,2.459,111.389,2.415]],[[117.885,4.186,117.745,4.167,117.649,4.169,117.667,4.204,117.662,4.25,117.708,4.262,117.761,4.252,117.885,4.186]]]},{"n":"Malawi","c":"MW","r":6,"ml":4,"lx":33.61,"ly":-13.39,"b":[32.67,-17.131,35.893,-9.395],"p":[[[34.719,-12.111,34.684,-12.119,34.662,-12.101,34.667,-12.048,34.68,-12.009,34.715,-12.003,34.739,-12.013,34.756,-12.031,34.756,-12.059,34.746,-12.088,34.719,-12.111]],[[34.642,-12.014,34.622,-12.067,34.58,-12.066,34.542,-12.019,34.554,-11.982,34.591,-11.971,34.624,-11.985,34.642,-12.014]],[[33.202,-14.013,33.148,-13.941,33.104,-13.959,33.042,-14.01,33.009,-14.024,32.992,-14.022,32.981,-14.009,32.968,-13.977,32.92,-13.884,32.867,-13.817,32.811,-13.792,32.765,-13.761,32.785,-13.731,32.807,-13.71,32.797,-13.688,32.772,-13.657,32.672,-13.61,32.67,-13.59,32.758,-13.55,32.814,-13.503,32.852,-13.457,32.9,-13.357,32.939,-13.257,32.968,-13.225,32.978,-13.159,32.971,-13.084,32.99,-12.989,33,-12.9,32.971,-12.865,32.946,-12.804,32.975,-12.701,33.022,-12.63,33.243,-12.557,33.398,-12.49,33.431,-12.46,33.483,-12.403,33.512,-12.348,33.491,-12.331,33.37,-12.33,33.34,-12.308,33.252,-12.113,33.301,-11.888,33.305,-11.8,33.304,-11.691,33.288,-11.611,33.25,-11.578,33.226,-11.535,33.233,-11.418,33.268,-11.404,33.346,-11.249,33.38,-11.158,33.339,-11.085,33.293,-10.981,33.273,-10.915,33.261,-10.893,33.293,-10.852,33.345,-10.813,33.403,-10.802,33.465,-10.783,33.659,-10.591,33.662,-10.553,33.626,-10.489,33.554,-10.391,33.538,-10.352,33.529,-10.235,33.5,-10.2,33.394,-10.121,33.312,-10.038,33.337,-9.954,33.351,-9.862,33.31,-9.812,33.25,-9.76,33.213,-9.683,33.196,-9.626,33.148,-9.604,33.104,-9.603,33.072,-9.638,33.038,-9.635,32.996,-9.623,32.982,-9.574,32.98,-9.52,32.951,-9.484,32.923,-9.434,32.92,-9.407,32.937,-9.4,32.974,-9.395,33.13,-9.496,33.225,-9.5,33.331,-9.519,33.421,-9.608,33.468,-9.62,33.528,-9.608,33.698,-9.598,33.766,-9.611,33.854,-9.663,33.889,-9.67,33.944,-9.672,33.954,-9.658,33.959,-9.627,33.95,-9.565,33.962,-9.532,33.996,-9.495,34.089,-9.538,34.321,-9.732,34.328,-9.757,34.476,-9.949,34.524,-10.03,34.524,-10.073,34.57,-10.241,34.58,-10.32,34.57,-10.38,34.572,-10.428,34.59,-10.496,34.584,-10.525,34.637,-10.626,34.662,-10.71,34.667,-10.792,34.652,-10.873,34.606,-10.99,34.598,-11.037,34.608,-11.08,34.638,-11.127,34.688,-11.177,34.726,-11.238,34.752,-11.309,34.774,-11.342,34.801,-11.341,34.851,-11.352,34.891,-11.394,34.937,-11.463,34.953,-11.544,34.959,-11.578,34.827,-11.576,34.66,-11.589,34.619,-11.62,34.606,-11.69,34.554,-11.834,34.525,-11.887,34.463,-11.984,34.376,-12.12,34.358,-12.165,34.361,-12.211,34.412,-12.396,34.466,-12.591,34.483,-12.667,34.521,-12.926,34.543,-13.109,34.546,-13.216,34.564,-13.36,34.612,-13.438,34.662,-13.487,34.85,-13.516,34.907,-13.552,35.014,-13.643,35.247,-13.897,35.376,-14.059,35.488,-14.201,35.69,-14.466,35.847,-14.671,35.867,-14.864,35.893,-14.892,35.84,-15.035,35.805,-15.266,35.83,-15.419,35.82,-15.68,35.791,-15.959,35.755,-16.058,35.709,-16.096,35.599,-16.126,35.358,-16.161,35.322,-16.193,35.292,-16.247,35.243,-16.375,35.185,-16.505,35.167,-16.56,35.178,-16.573,35.23,-16.639,35.281,-16.808,35.29,-17.097,35.273,-17.118,35.201,-17.131,35.125,-17.127,35.093,-17.111,35.065,-17.079,35.044,-17.017,35.094,-16.974,35.112,-16.899,35.08,-16.834,35.015,-16.82,34.933,-16.76,34.759,-16.567,34.613,-16.432,34.528,-16.319,34.441,-16.274,34.416,-16.247,34.396,-16.199,34.395,-16.131,34.403,-16.08,34.376,-16.024,34.288,-15.936,34.248,-15.887,34.246,-15.829,34.283,-15.773,34.358,-15.705,34.415,-15.567,34.435,-15.477,34.541,-15.297,34.555,-15.141,34.558,-15.016,34.551,-14.922,34.524,-14.731,34.505,-14.598,34.375,-14.425,34.333,-14.409,34.209,-14.424,34.102,-14.449,34.049,-14.485,33.97,-14.487,33.761,-14.517,33.696,-14.53,33.658,-14.562,33.636,-14.568,33.505,-14.434,33.39,-14.289,33.244,-14.043,33.202,-14.013]]]},{"n":"Madagascar","c":"MG","r":3,"ml":2.7,"lx":46.7,"ly":-18.63,"b":[43.257,-25.571,50.483,-12.08],"p":[[[49.538,-12.432,49.584,-12.537,49.638,-12.637,49.805,-12.88,49.876,-12.973,49.938,-13.072,49.967,-13.27,50.073,-13.578,50.174,-14.04,50.205,-14.514,50.235,-14.732,50.313,-14.937,50.441,-15.149,50.483,-15.386,50.405,-15.629,50.292,-15.858,50.262,-15.902,50.209,-15.96,50.185,-15.958,50.094,-15.899,50.02,-15.802,49.927,-15.574,49.893,-15.458,49.853,-15.439,49.744,-15.45,49.664,-15.522,49.65,-15.567,49.667,-15.696,49.697,-15.811,49.71,-15.929,49.713,-16.077,49.742,-16.121,49.786,-16.159,49.831,-16.256,49.839,-16.487,49.811,-16.603,49.734,-16.703,49.739,-16.758,49.767,-16.815,49.74,-16.849,49.637,-16.893,49.595,-16.931,49.54,-17.033,49.449,-17.241,49.437,-17.347,49.494,-17.67,49.478,-17.899,49.363,-18.336,49.297,-18.544,49.203,-18.792,49.06,-19.12,48.918,-19.53,48.797,-19.953,48.708,-20.207,48.607,-20.458,48.469,-20.9,48.351,-21.349,48.176,-21.843,47.934,-22.394,47.908,-22.466,47.858,-22.747,47.804,-22.992,47.739,-23.233,47.604,-23.633,47.589,-23.756,47.558,-23.875,47.428,-24.125,47.373,-24.218,47.334,-24.318,47.312,-24.443,47.273,-24.564,47.177,-24.787,47.035,-24.979,46.938,-25.049,46.729,-25.15,46.622,-25.17,46.387,-25.173,46.159,-25.23,45.921,-25.341,45.692,-25.468,45.605,-25.529,45.508,-25.563,45.206,-25.571,45.115,-25.543,44.813,-25.334,44.696,-25.3,44.474,-25.271,44.407,-25.253,44.346,-25.226,44.256,-25.117,44.078,-25.025,44.035,-24.996,44.008,-24.932,43.99,-24.863,43.944,-24.787,43.91,-24.641,43.852,-24.538,43.688,-24.358,43.67,-24.3,43.657,-24.109,43.662,-23.979,43.646,-23.742,43.665,-23.63,43.722,-23.53,43.699,-23.421,43.638,-23.307,43.615,-23.188,43.57,-23.08,43.398,-22.886,43.358,-22.791,43.33,-22.692,43.265,-22.384,43.257,-22.276,43.267,-22.049,43.291,-21.933,43.332,-21.851,43.343,-21.79,43.37,-21.738,43.411,-21.696,43.438,-21.647,43.502,-21.356,43.583,-21.292,43.704,-21.255,43.8,-21.179,43.856,-21.077,43.911,-20.866,44.063,-20.656,44.117,-20.546,44.24,-20.38,44.348,-20.146,44.381,-20.035,44.405,-19.922,44.432,-19.674,44.453,-19.551,44.449,-19.429,44.387,-19.303,44.239,-19.075,44.234,-19.033,44.246,-18.863,44.233,-18.741,44.179,-18.619,44.109,-18.504,44.04,-18.288,44.007,-17.933,44.014,-17.804,43.994,-17.69,43.944,-17.581,43.979,-17.392,44.421,-16.703,44.436,-16.621,44.418,-16.411,44.427,-16.289,44.442,-16.244,44.476,-16.217,44.552,-16.204,44.909,-16.175,44.955,-16.153,45.044,-16.095,45.167,-15.983,45.223,-15.95,45.271,-15.962,45.302,-16.01,45.342,-16.037,45.486,-15.986,45.542,-15.984,45.598,-15.993,45.625,-15.946,45.641,-15.883,45.662,-15.839,45.7,-15.814,45.886,-15.8,46.004,-15.782,46.158,-15.738,46.191,-15.747,46.314,-15.905,46.352,-15.918,46.4,-15.925,46.442,-15.896,46.341,-15.813,46.326,-15.767,46.331,-15.714,46.385,-15.6,46.475,-15.513,46.675,-15.382,46.882,-15.23,46.942,-15.219,46.993,-15.243,47.032,-15.423,47.027,-15.452,47.061,-15.456,47.099,-15.434,47.133,-15.362,47.135,-15.302,47.107,-15.244,47.094,-15.195,47.093,-15.15,47.198,-15.044,47.28,-14.943,47.319,-14.822,47.352,-14.766,47.439,-14.703,47.465,-14.713,47.485,-14.764,47.496,-14.818,47.474,-14.872,47.442,-14.925,47.429,-14.996,47.478,-15.009,47.525,-14.992,47.593,-14.864,47.67,-14.743,47.716,-14.68,47.774,-14.637,47.87,-14.646,47.964,-14.673,47.812,-14.545,47.773,-14.37,47.955,-14.067,47.957,-14.004,47.983,-13.985,47.996,-13.96,47.901,-13.858,47.884,-13.808,47.896,-13.731,47.941,-13.662,47.982,-13.615,48.04,-13.596,48.086,-13.623,48.187,-13.707,48.255,-13.719,48.338,-13.639,48.405,-13.538,48.506,-13.469,48.621,-13.426,48.796,-13.267,48.91,-12.936,48.919,-12.839,48.894,-12.722,48.854,-12.61,48.786,-12.471,48.804,-12.44,48.9,-12.458,48.932,-12.439,49.036,-12.316,49.207,-12.08,49.263,-12.08,49.312,-12.124,49.33,-12.189,49.364,-12.236,49.48,-12.348,49.538,-12.432]],[[48.342,-13.364,48.344,-13.4,48.212,-13.385,48.191,-13.26,48.256,-13.256,48.27,-13.205,48.309,-13.198,48.351,-13.31,48.342,-13.364]],[[49.936,-16.903,49.824,-17.087,49.856,-16.933,49.986,-16.712,50.023,-16.695,49.936,-16.903]]]},{"n":"North Macedonia","c":"MK","r":6,"ml":5,"lx":21.56,"ly":41.56,"b":[20.449,40.85,23.006,42.358],"p":[[[21.563,42.248,21.618,42.242,21.739,42.268,21.815,42.303,21.853,42.308,21.904,42.322,21.978,42.32,22.052,42.305,22.147,42.325,22.24,42.358,22.277,42.35,22.317,42.322,22.344,42.314,22.498,42.165,22.583,42.105,22.682,42.059,22.796,42.026,22.837,41.994,22.909,41.835,22.944,41.775,22.992,41.757,23.004,41.74,23.006,41.717,22.951,41.606,22.93,41.356,22.916,41.336,22.859,41.337,22.784,41.332,22.755,41.313,22.725,41.179,22.604,41.14,22.494,41.119,22.401,41.123,22.238,41.155,22.184,41.159,22.139,41.141,21.993,41.131,21.929,41.107,21.779,40.95,21.628,40.896,21.576,40.869,21.46,40.904,21.404,40.907,21.324,40.867,21.148,40.863,21.1,40.856,20.964,40.85,20.959,40.872,20.933,40.903,20.87,40.918,20.741,40.905,20.709,40.928,20.656,41.062,20.614,41.083,20.568,41.128,20.489,41.273,20.487,41.336,20.492,41.391,20.449,41.521,20.476,41.554,20.516,41.575,20.517,41.627,20.505,41.706,20.553,41.862,20.566,41.874,20.579,41.866,20.695,41.854,20.725,41.874,20.744,41.904,20.75,42.018,20.778,42.071,21.06,42.171,21.142,42.175,21.206,42.129,21.256,42.1,21.287,42.1,21.298,42.13,21.332,42.187,21.39,42.22,21.561,42.248,21.563,42.248]]]},{"n":"Luxembourg","c":"LU","r":6,"ml":5.7,"lx":6.08,"ly":49.73,"b":[5.725,49.445,6.494,50.167],"p":[[[6.117,50.121,6.108,50.094,6.11,50.034,6.138,49.974,6.205,49.915,6.256,49.872,6.325,49.838,6.441,49.805,6.487,49.798,6.494,49.754,6.485,49.708,6.445,49.682,6.407,49.645,6.378,49.6,6.348,49.513,6.344,49.453,6.277,49.478,6.242,49.494,6.181,49.499,6.12,49.485,6.074,49.455,6.011,49.445,5.959,49.455,5.929,49.478,5.901,49.49,5.823,49.505,5.79,49.538,5.815,49.554,5.838,49.578,5.857,49.613,5.88,49.645,5.804,49.732,5.788,49.759,5.725,49.808,5.726,49.833,5.741,49.857,5.735,49.876,5.744,49.92,5.788,49.961,5.817,50.013,5.867,50.083,5.976,50.167,6.055,50.154,6.089,50.155,6.11,50.124,6.117,50.121]]]},{"n":"Lithuania","c":"LT","r":5,"ml":4,"lx":24.09,"ly":55.1,"b":[20.9,53.893,26.776,56.411],"p":[[[20.958,55.279,20.9,55.287,21.014,55.402,21.058,55.477,21.088,55.583,21.115,55.617,21.116,55.568,21.104,55.488,21.032,55.35,20.958,55.279]],[[22.766,54.357,22.724,54.406,22.68,54.493,22.684,54.563,22.71,54.633,22.831,54.838,22.825,54.871,22.737,54.929,22.627,54.971,22.567,55.059,22.346,55.064,22.138,55.059,22.072,55.064,21.874,55.101,21.683,55.16,21.555,55.195,21.447,55.234,21.389,55.276,21.298,55.264,21.236,55.264,21.236,55.271,21.201,55.344,21.238,55.455,21.171,55.618,21.062,55.813,21.054,56.023,21.046,56.07,21.315,56.188,21.654,56.315,21.731,56.326,22.043,56.401,22.085,56.407,22.366,56.393,22.587,56.375,22.773,56.377,22.876,56.396,22.968,56.38,23.043,56.324,23.12,56.331,23.196,56.367,23.613,56.334,23.707,56.335,23.813,56.329,24.008,56.295,24.121,56.264,24.368,56.283,24.474,56.284,24.529,56.296,24.7,56.381,24.773,56.396,24.841,56.411,24.903,56.398,24.944,56.326,25.07,56.2,25.207,56.178,25.586,56.13,25.663,56.105,25.876,55.994,26.004,55.94,26.086,55.897,26.21,55.812,26.281,55.75,26.401,55.704,26.543,55.672,26.594,55.668,26.591,55.623,26.567,55.546,26.519,55.448,26.47,55.372,26.458,55.342,26.495,55.318,26.681,55.306,26.76,55.293,26.776,55.273,26.734,55.247,26.675,55.225,26.648,55.204,26.601,55.13,26.292,55.14,26.251,55.125,26.231,55.09,26.216,55.05,26.175,55.003,26.093,54.962,25.964,54.947,25.859,54.919,25.781,54.833,25.722,54.718,25.724,54.636,25.732,54.59,25.725,54.564,25.685,54.536,25.62,54.46,25.568,54.377,25.547,54.332,25.558,54.311,25.617,54.31,25.703,54.293,25.748,54.26,25.765,54.221,25.765,54.18,25.749,54.157,25.681,54.14,25.573,54.14,25.51,54.16,25.497,54.175,25.527,54.215,25.506,54.265,25.461,54.293,25.371,54.251,25.284,54.251,25.179,54.214,25.111,54.155,25.046,54.133,24.87,54.145,24.826,54.119,24.789,53.998,24.768,53.975,24.621,53.98,24.479,53.932,24.318,53.893,24.237,53.92,24.191,53.95,24.104,53.945,24.008,53.932,23.944,53.939,23.873,53.936,23.734,53.912,23.559,53.92,23.485,53.94,23.478,53.959,23.483,54.006,23.481,54.079,23.454,54.143,23.37,54.2,23.282,54.24,23.17,54.281,23.088,54.299,23.042,54.304,23.032,54.328,23.016,54.348,22.977,54.366,22.894,54.391,22.824,54.396,22.766,54.357]]]},{"n":"Liechtenstein","c":"LI","r":6,"ml":5,"lx":9.56,"ly":47.11,"b":[9.479,47.057,9.611,47.271],"p":[[[9.58,47.057,9.502,47.063,9.488,47.062,9.479,47.098,9.484,47.173,9.528,47.271,9.537,47.255,9.542,47.234,9.551,47.212,9.556,47.185,9.572,47.158,9.601,47.132,9.611,47.107,9.596,47.076,9.58,47.057]]]},{"n":"Libya","c":"LY","r":3,"ml":3,"lx":18.01,"ly":26.64,"b":[9.31,19.497,25.15,33.182],"p":[[[9.519,30.229,9.638,30.282,9.807,30.342,9.895,30.387,9.933,30.425,10.06,30.58,10.126,30.666,10.216,30.783,10.256,30.865,10.257,30.941,10.243,31.032,10.173,31.251,10.115,31.464,10.16,31.546,10.196,31.585,10.275,31.685,10.306,31.705,10.476,31.736,10.544,31.803,10.596,31.886,10.609,31.93,10.683,31.975,10.772,32.021,10.826,32.081,11.005,32.173,11.168,32.257,11.358,32.345,11.505,32.414,11.536,32.473,11.534,32.525,11.454,32.643,11.454,32.782,11.459,32.897,11.467,32.966,11.502,33.156,11.505,33.182,11.657,33.119,11.813,33.094,12.28,32.859,12.427,32.829,12.754,32.801,13.138,32.897,13.283,32.915,13.536,32.824,13.648,32.799,13.835,32.792,14.156,32.71,14.237,32.681,14.424,32.55,14.513,32.511,15.177,32.391,15.267,32.312,15.359,32.16,15.363,31.971,15.414,31.834,15.496,31.657,15.596,31.531,15.706,31.426,15.832,31.361,16.123,31.264,16.451,31.227,16.782,31.215,17.349,31.081,17.83,30.928,17.949,30.852,18.19,30.777,18.67,30.416,18.936,30.29,19.124,30.266,19.292,30.288,19.59,30.414,19.713,30.488,20.013,30.801,20.112,30.964,20.151,31.079,20.141,31.196,20.104,31.301,20.02,31.411,19.961,31.556,19.926,31.818,19.973,31.999,20.031,32.108,20.121,32.219,20.371,32.431,20.621,32.58,21.062,32.776,21.319,32.778,21.425,32.799,21.636,32.937,21.721,32.942,21.839,32.909,22.187,32.918,22.341,32.88,22.523,32.794,22.754,32.741,22.917,32.687,23.091,32.619,23.13,32.448,23.11,32.397,23.106,32.331,23.286,32.214,23.798,32.159,23.898,32.127,24.039,32.037,24.13,32.009,24.48,31.997,24.684,32.016,24.879,31.984,24.951,31.954,25.025,31.883,25.115,31.712,25.15,31.655,25.112,31.627,25.057,31.567,25.023,31.514,24.93,31.427,24.853,31.335,24.86,31.199,24.878,31.061,24.929,30.926,24.974,30.777,24.961,30.679,24.923,30.558,24.878,30.458,24.726,30.251,24.703,30.201,24.712,30.132,24.804,29.886,24.811,29.809,24.866,29.57,24.916,29.376,24.972,29.224,24.98,29.182,24.98,28.957,24.98,28.733,24.98,28.508,24.98,28.284,24.98,28.059,24.98,27.835,24.98,27.61,24.98,27.385,24.98,27.161,24.98,26.936,24.98,26.712,24.98,26.487,24.98,26.263,24.98,26.038,24.98,25.813,24.98,25.589,24.98,25.364,24.98,25.14,24.98,24.915,24.98,24.691,24.98,24.466,24.98,24.242,24.98,24.017,24.98,23.792,24.98,23.568,24.98,23.343,24.98,23.119,24.98,22.894,24.98,22.67,24.98,22.445,24.98,22.22,24.98,21.996,24.98,21.498,24.98,20.999,24.98,20.501,24.979,20.003,24.976,20.001,24.973,19.999,24.97,19.997,24.967,19.995,24.72,19.996,24.474,19.996,24.227,19.996,23.98,19.996,23.98,19.871,23.98,19.746,23.98,19.621,23.98,19.497,23.501,19.733,23.022,19.97,22.543,20.206,22.064,20.443,21.585,20.679,21.106,20.916,20.627,21.153,20.148,21.389,19.669,21.626,19.189,21.862,18.71,22.099,18.231,22.336,17.752,22.572,17.273,22.809,16.794,23.045,16.315,23.282,15.984,23.445,15.627,23.286,15.347,23.161,14.979,22.996,14.556,22.783,14.231,22.618,14.216,22.62,14.201,22.624,13.863,22.902,13.599,23.12,13.481,23.18,12.984,23.291,12.489,23.402,11.968,23.518,11.873,23.695,11.767,23.893,11.624,24.14,11.537,24.291,11.508,24.314,11.108,24.434,10.686,24.551,10.439,24.48,10.396,24.486,10.326,24.53,10.256,24.591,10.219,24.676,10.12,24.79,10.028,25.051,10.019,25.259,10.001,25.332,9.781,25.624,9.581,25.89,9.448,26.067,9.422,26.147,9.438,26.246,9.491,26.334,9.685,26.438,9.859,26.552,9.883,26.631,9.894,26.848,9.837,26.916,9.795,27.045,9.753,27.219,9.748,27.331,9.825,27.553,9.916,27.786,9.858,28.043,9.816,28.56,9.843,28.967,9.821,29.115,9.805,29.177,9.746,29.369,9.673,29.567,9.64,29.636,9.546,29.796,9.391,29.994,9.31,30.115,9.421,30.179,9.519,30.229]]]},{"n":"Liberia","c":"LR","r":4,"ml":4,"lx":-9.46,"ly":6.45,"b":[-11.508,4.351,-7.4,8.538],"p":[[[-11.508,6.907,-11.455,6.951,-11.377,7.095,-11.268,7.233,-11.166,7.314,-11.085,7.399,-11,7.463,-10.878,7.538,-10.691,7.736,-10.647,7.759,-10.618,7.896,-10.571,8.071,-10.517,8.125,-10.39,8.158,-10.36,8.188,-10.315,8.311,-10.286,8.454,-10.283,8.485,-10.233,8.489,-10.147,8.52,-10.098,8.506,-10.076,8.465,-10.064,8.43,-9.805,8.519,-9.782,8.538,-9.768,8.535,-9.736,8.454,-9.717,8.459,-9.701,8.482,-9.684,8.484,-9.664,8.474,-9.643,8.436,-9.61,8.402,-9.554,8.379,-9.518,8.346,-9.522,8.26,-9.508,8.176,-9.484,8.157,-9.471,8.107,-9.465,8.052,-9.451,8.023,-9.442,7.968,-9.446,7.908,-9.436,7.867,-9.395,7.795,-9.369,7.704,-9.369,7.64,-9.384,7.572,-9.411,7.51,-9.46,7.443,-9.464,7.416,-9.435,7.398,-9.392,7.395,-9.355,7.409,-9.263,7.378,-9.215,7.333,-9.173,7.278,-9.135,7.251,-9.118,7.216,-9.052,7.225,-8.977,7.259,-8.961,7.275,-8.938,7.266,-8.89,7.263,-8.856,7.323,-8.828,7.392,-8.769,7.467,-8.74,7.496,-8.733,7.544,-8.729,7.605,-8.708,7.659,-8.66,7.688,-8.607,7.688,-8.579,7.677,-8.564,7.625,-8.522,7.586,-8.486,7.558,-8.467,7.547,-8.437,7.516,-8.409,7.412,-8.297,7.074,-8.302,6.981,-8.325,6.92,-8.325,6.86,-8.333,6.802,-8.401,6.705,-8.604,6.508,-8.588,6.491,-8.54,6.468,-8.49,6.456,-8.45,6.463,-8.399,6.413,-8.345,6.351,-8.287,6.319,-8.204,6.291,-8.131,6.288,-8.069,6.298,-7.982,6.286,-7.889,6.235,-7.856,6.15,-7.833,6.076,-7.801,6.039,-7.797,5.975,-7.73,5.919,-7.636,5.908,-7.514,5.842,-7.483,5.846,-7.469,5.854,-7.454,5.841,-7.424,5.651,-7.4,5.551,-7.412,5.51,-7.429,5.478,-7.43,5.325,-7.485,5.236,-7.494,5.14,-7.51,5.108,-7.569,5.081,-7.569,5.006,-7.585,4.917,-7.591,4.822,-7.575,4.572,-7.572,4.386,-7.545,4.351,-7.66,4.367,-7.998,4.509,-8.259,4.59,-9.132,5.055,-9.375,5.241,-9.654,5.519,-10.276,6.078,-10.418,6.167,-10.597,6.211,-10.708,6.258,-10.786,6.31,-10.849,6.465,-11.005,6.557,-11.292,6.688,-11.508,6.907]]]},{"n":"Lesotho","c":"LS","r":6,"ml":4,"lx":28.25,"ly":-29.48,"b":[27.052,-30.642,29.391,-28.582],"p":[[[28.737,-30.102,28.647,-30.127,28.634,-30.129,28.577,-30.123,28.5,-30.129,28.439,-30.142,28.392,-30.148,28.315,-30.218,28.176,-30.41,28.139,-30.45,28.129,-30.525,28.096,-30.585,28.057,-30.631,28.018,-30.642,27.902,-30.624,27.753,-30.6,27.667,-30.542,27.59,-30.466,27.549,-30.411,27.507,-30.381,27.492,-30.364,27.431,-30.338,27.409,-30.325,27.388,-30.316,27.364,-30.279,27.35,-30.247,27.355,-30.159,27.313,-30.106,27.24,-30.015,27.194,-29.941,27.13,-29.84,27.092,-29.754,27.052,-29.664,27.057,-29.626,27.095,-29.599,27.207,-29.554,27.295,-29.519,27.357,-29.455,27.425,-29.36,27.458,-29.303,27.491,-29.277,27.527,-29.236,27.59,-29.146,27.66,-29.047,27.736,-28.94,27.83,-28.909,27.96,-28.873,28.084,-28.78,28.233,-28.701,28.472,-28.616,28.583,-28.594,28.626,-28.582,28.653,-28.598,28.681,-28.647,28.722,-28.688,28.816,-28.759,28.856,-28.776,28.954,-28.881,29.058,-28.954,29.178,-29.037,29.26,-29.078,29.301,-29.09,29.336,-29.164,29.371,-29.218,29.391,-29.27,29.387,-29.32,29.349,-29.442,29.294,-29.567,29.249,-29.619,29.195,-29.652,29.142,-29.701,29.122,-29.801,29.098,-29.919,29.029,-29.968,28.975,-29.999,28.901,-30.038,28.737,-30.102]]]},{"n":"Lebanon","c":"LB","r":5,"ml":4,"lx":35.99,"ly":34.13,"b":[35.109,33.076,36.585,34.679],"p":[[[35.976,34.629,36.151,34.629,36.264,34.633,36.296,34.679,36.384,34.658,36.433,34.613,36.389,34.567,36.326,34.513,36.33,34.5,36.376,34.495,36.456,34.466,36.504,34.432,36.585,34.221,36.535,34.134,36.458,34.057,36.423,34.05,36.355,34.011,36.298,33.959,36.278,33.925,36.282,33.894,36.363,33.855,36.365,33.839,36.349,33.827,36.283,33.836,36.199,33.84,36.15,33.84,36.092,33.832,36.019,33.784,35.986,33.753,35.968,33.732,35.942,33.668,35.972,33.623,36.027,33.598,36.034,33.585,36.022,33.563,35.968,33.535,35.927,33.5,35.915,33.465,35.869,33.432,35.841,33.416,35.788,33.37,35.734,33.333,35.627,33.275,35.603,33.241,35.579,33.271,35.533,33.25,35.493,33.119,35.411,33.076,35.309,33.08,35.223,33.092,35.109,33.084,35.155,33.16,35.204,33.259,35.251,33.393,35.336,33.503,35.511,33.88,35.612,34.032,35.648,34.248,35.804,34.437,35.921,34.493,35.978,34.547,35.976,34.629]]]},{"n":"Latvia","c":"LV","r":5,"ml":4,"lx":25.46,"ly":57.07,"b":[21.015,55.668,28.202,58.063],"p":[[[26.594,55.668,26.543,55.672,26.401,55.704,26.281,55.75,26.21,55.812,26.086,55.897,26.004,55.94,25.876,55.994,25.663,56.105,25.586,56.13,25.207,56.178,25.07,56.2,24.944,56.326,24.903,56.398,24.841,56.411,24.773,56.396,24.7,56.381,24.529,56.296,24.474,56.284,24.368,56.283,24.121,56.264,24.008,56.295,23.813,56.329,23.707,56.335,23.613,56.334,23.196,56.367,23.12,56.331,23.043,56.324,22.968,56.38,22.876,56.396,22.773,56.377,22.587,56.375,22.366,56.393,22.085,56.407,22.043,56.401,21.731,56.326,21.654,56.315,21.315,56.188,21.046,56.07,21.015,56.259,21.031,56.637,21.071,56.824,21.257,56.933,21.351,57.018,21.405,57.131,21.421,57.236,21.459,57.322,21.729,57.571,21.942,57.598,22.231,57.667,22.555,57.724,22.617,57.651,22.649,57.595,23.038,57.392,23.137,57.324,23.287,57.09,23.648,56.971,23.931,57.008,24.054,57.066,24.281,57.172,24.383,57.25,24.403,57.325,24.363,57.645,24.302,57.784,24.323,57.871,24.363,57.866,24.459,57.908,24.776,57.985,24.839,57.989,24.911,58.005,25.111,58.063,25.175,58.032,25.229,57.997,25.258,57.996,25.273,58.009,25.269,58.032,25.283,58.048,25.34,58.039,25.571,57.943,25.66,57.92,25.721,57.914,25.794,57.869,25.991,57.838,26.015,57.815,26.03,57.786,26.215,57.663,26.298,57.601,26.462,57.544,26.533,57.531,26.82,57.589,26.9,57.609,26.966,57.609,27.033,57.579,27.187,57.538,27.327,57.525,27.352,57.528,27.47,57.524,27.511,57.508,27.539,57.43,27.673,57.368,27.797,57.317,27.829,57.293,27.838,57.248,27.83,57.194,27.815,57.167,27.763,57.135,27.717,57.055,27.711,56.978,27.639,56.846,27.656,56.843,27.806,56.867,27.849,56.853,27.882,56.824,27.892,56.741,27.941,56.704,27.992,56.645,28.008,56.6,28.103,56.546,28.111,56.511,28.169,56.387,28.192,56.316,28.202,56.26,28.173,56.19,28.148,56.143,28.118,56.146,28.032,56.133,27.896,56.076,27.694,55.942,27.642,55.912,27.589,55.809,27.577,55.799,27.459,55.804,27.427,55.806,27.309,55.804,27.053,55.831,26.953,55.813,26.822,55.709,26.772,55.694,26.62,55.68,26.594,55.668]]]},{"n":"Laos","c":"LA","r":4,"ml":4,"lx":102.53,"ly":19.43,"b":[100.115,13.921,107.653,22.495],"p":[[[102.127,22.379,102.183,22.284,102.301,22.178,102.443,22.027,102.488,21.958,102.583,21.904,102.61,21.852,102.631,21.771,102.641,21.711,102.662,21.676,102.695,21.662,102.739,21.678,102.771,21.71,102.798,21.798,102.816,21.807,102.845,21.735,102.876,21.722,102.918,21.713,102.95,21.681,102.959,21.626,102.949,21.57,102.91,21.506,102.888,21.44,102.872,21.338,102.851,21.266,102.884,21.203,103.104,20.892,103.211,20.841,103.464,20.78,103.555,20.738,103.635,20.697,103.714,20.717,103.791,20.81,103.882,20.861,104.052,20.941,104.101,20.946,104.195,20.914,104.35,20.821,104.461,20.734,104.53,20.688,104.583,20.647,104.575,20.6,104.533,20.555,104.479,20.53,104.408,20.486,104.368,20.441,104.392,20.425,104.496,20.414,104.619,20.375,104.656,20.329,104.662,20.289,104.677,20.225,104.699,20.205,104.813,20.217,104.848,20.202,104.889,20.169,104.929,20.083,104.928,20.018,104.846,19.947,104.815,19.904,104.802,19.836,104.743,19.755,104.588,19.619,104.546,19.611,104.26,19.685,104.127,19.681,104.063,19.678,104.032,19.675,104.013,19.646,104.052,19.564,104.063,19.483,104.028,19.42,103.932,19.366,103.896,19.34,103.892,19.305,103.918,19.269,104.006,19.231,104.109,19.196,104.446,18.984,104.518,18.934,104.613,18.861,104.717,18.803,104.993,18.728,105.115,18.679,105.146,18.651,105.145,18.617,105.113,18.573,105.087,18.496,105.086,18.45,105.115,18.405,105.163,18.339,105.273,18.235,105.333,18.19,105.4,18.179,105.458,18.154,105.519,18.077,105.588,17.984,105.598,17.918,105.627,17.834,105.691,17.738,105.779,17.644,105.903,17.529,105.974,17.447,106.006,17.415,106.27,17.217,106.333,17.144,106.426,17.003,106.465,16.982,106.502,16.954,106.526,16.877,106.534,16.821,106.546,16.651,106.594,16.6,106.638,16.538,106.656,16.493,106.696,16.459,106.74,16.453,106.792,16.49,106.832,16.526,106.851,16.516,106.893,16.397,106.931,16.353,107.002,16.312,107.07,16.28,107.217,16.136,107.296,16.084,107.35,16.067,107.396,16.043,107.41,15.998,107.392,15.952,107.361,15.922,107.189,15.839,107.166,15.802,107.19,15.747,107.233,15.678,107.279,15.619,107.339,15.56,107.46,15.466,107.564,15.392,107.622,15.31,107.653,15.255,107.634,15.19,107.59,15.118,107.555,15.057,107.496,15.021,107.48,14.98,107.505,14.916,107.525,14.872,107.514,14.817,107.519,14.705,107.465,14.665,107.415,14.563,107.38,14.555,107.293,14.592,107.262,14.572,107.207,14.498,107.109,14.417,107.062,14.416,107.03,14.426,106.992,14.391,106.938,14.327,106.913,14.329,106.82,14.315,106.783,14.335,106.738,14.388,106.665,14.441,106.599,14.479,106.564,14.505,106.531,14.549,106.501,14.578,106.447,14.515,106.355,14.455,106.268,14.466,106.225,14.476,106.191,14.388,106.165,14.372,106.008,14.357,105.979,14.343,106.004,14.263,106.097,14.127,106.125,14.049,106.067,13.921,105.904,13.925,105.831,13.977,105.764,14.049,105.74,14.085,105.532,14.156,105.393,14.107,105.35,14.11,105.285,14.161,105.246,14.201,105.207,14.259,105.186,14.319,105.183,14.346,105.244,14.368,105.342,14.417,105.423,14.472,105.476,14.53,105.497,14.591,105.5,14.661,105.523,14.843,105.547,14.932,105.533,15.042,105.49,15.128,105.49,15.257,105.506,15.32,105.513,15.361,105.574,15.413,105.616,15.488,105.639,15.586,105.641,15.657,105.622,15.7,105.562,15.741,105.462,15.78,105.399,15.83,105.373,15.89,105.376,15.942,105.406,15.987,105.331,16.038,105.149,16.094,105.047,16.16,105.026,16.238,104.95,16.34,104.819,16.466,104.751,16.648,104.744,16.884,104.759,17.077,104.816,17.3,104.74,17.462,104.656,17.547,104.539,17.609,104.428,17.699,104.323,17.816,104.196,17.988,104.049,18.217,103.95,18.319,103.899,18.295,103.792,18.317,103.63,18.383,103.488,18.418,103.367,18.423,103.288,18.408,103.252,18.373,103.249,18.339,103.28,18.305,103.263,18.278,103.2,18.259,103.149,18.222,103.091,18.138,103.051,18.029,102.991,17.986,102.899,17.977,102.807,17.946,102.718,17.892,102.675,17.852,102.68,17.824,102.661,17.818,102.617,17.833,102.596,17.87,102.598,17.927,102.553,17.965,102.459,17.985,102.352,18.046,102.232,18.149,102.148,18.204,102.101,18.211,102.035,18.17,101.947,18.081,101.875,18.046,101.819,18.065,101.775,18.033,101.744,17.953,101.688,17.889,101.564,17.821,101.555,17.812,101.414,17.719,101.3,17.625,101.167,17.499,101.105,17.48,101.046,17.51,100.956,17.541,100.908,17.584,100.999,17.797,101.113,18.034,101.144,18.143,101.149,18.222,101.138,18.287,101.093,18.355,101.051,18.407,101.047,18.442,101.06,18.479,101.106,18.534,101.166,18.618,101.221,18.793,101.286,18.977,101.28,19.089,101.227,19.212,101.198,19.328,101.221,19.487,101.212,19.548,101.155,19.579,100.967,19.611,100.906,19.605,100.858,19.585,100.807,19.542,100.744,19.515,100.625,19.5,100.514,19.553,100.42,19.644,100.398,19.756,100.466,19.889,100.515,19.996,100.543,20.089,100.54,20.132,100.52,20.178,100.492,20.184,100.432,20.241,100.373,20.34,100.318,20.386,100.266,20.377,100.218,20.34,100.174,20.273,100.14,20.245,100.115,20.258,100.122,20.317,100.13,20.372,100.184,20.589,100.249,20.73,100.326,20.796,100.407,20.823,100.493,20.813,100.565,20.825,100.623,20.86,100.618,20.879,100.549,20.884,100.522,20.922,100.536,20.992,100.567,21.038,100.614,21.059,100.659,21.13,100.703,21.251,100.757,21.313,100.82,21.314,100.928,21.366,101.08,21.469,101.139,21.567,101.197,21.522,101.175,21.408,101.206,21.383,101.22,21.342,101.212,21.278,101.224,21.224,101.248,21.197,101.281,21.184,101.444,21.231,101.542,21.234,101.584,21.204,101.622,21.184,101.669,21.17,101.705,21.15,101.728,21.156,101.783,21.204,101.801,21.213,101.802,21.236,101.763,21.279,101.723,21.315,101.724,21.395,101.743,21.534,101.747,21.606,101.744,21.778,101.737,21.827,101.7,21.882,101.603,21.99,101.576,22.055,101.56,22.121,101.562,22.162,101.537,22.21,101.525,22.254,101.568,22.276,101.62,22.327,101.646,22.405,101.671,22.462,101.708,22.487,101.739,22.495,101.76,22.49,101.842,22.388,101.945,22.439,102.024,22.439,102.092,22.412,102.127,22.379]]]},{"n":"Kyrgyzstan","c":"KG","r":4,"ml":3,"lx":74.53,"ly":41.67,"b":[69.229,39.208,80.246,43.24],"p":[[[70.958,40.239,70.991,40.255,71.095,40.271,71.305,40.287,71.376,40.275,71.457,40.242,71.52,40.209,71.58,40.21,71.63,40.217,71.651,40.208,71.667,40.179,71.692,40.152,71.773,40.188,71.845,40.234,71.903,40.241,71.956,40.259,71.971,40.29,72.013,40.341,72.131,40.439,72.193,40.454,72.233,40.454,72.235,40.439,72.254,40.424,72.358,40.402,72.389,40.427,72.406,40.463,72.37,40.52,72.369,40.543,72.383,40.565,72.402,40.578,72.567,40.524,72.604,40.525,72.68,40.556,72.749,40.609,72.774,40.65,73.113,40.786,73.137,40.811,73.132,40.829,72.99,40.86,72.926,40.842,72.867,40.842,72.831,40.862,72.658,40.87,72.62,40.884,72.506,40.982,72.427,41.019,72.364,41.043,72.295,41.04,72.213,41.014,72.187,41.026,72.181,41.067,72.181,41.118,72.164,41.174,72.115,41.187,72.052,41.165,71.958,41.187,71.879,41.195,71.858,41.311,71.826,41.361,71.792,41.413,71.758,41.428,71.701,41.454,71.697,41.516,71.685,41.533,71.665,41.541,71.638,41.534,71.602,41.503,71.62,41.435,71.606,41.367,71.586,41.333,71.546,41.308,71.5,41.307,71.421,41.342,71.408,41.136,71.393,41.123,71.299,41.152,71.223,41.14,71.111,41.153,71.026,41.187,70.963,41.196,70.86,41.225,70.782,41.263,70.734,41.401,70.689,41.45,70.646,41.46,70.471,41.413,70.408,41.45,70.29,41.497,70.201,41.514,70.177,41.54,70.181,41.571,70.455,41.725,70.563,41.831,70.631,41.875,70.728,41.905,70.803,41.923,70.842,42.02,70.857,42.031,70.91,42.038,71.032,42.078,71.229,42.163,71.232,42.186,71.213,42.206,71.13,42.25,71.036,42.285,70.979,42.267,70.947,42.249,70.893,42.294,70.893,42.34,70.952,42.419,71.002,42.459,71.023,42.535,71.094,42.587,71.167,42.667,71.257,42.734,71.422,42.783,71.514,42.767,71.601,42.779,71.735,42.819,71.761,42.821,71.817,42.822,72.162,42.761,72.276,42.758,72.543,42.678,72.666,42.66,72.753,42.638,72.792,42.603,72.855,42.561,73.191,42.527,73.283,42.504,73.316,42.467,73.412,42.42,73.493,42.409,73.422,42.594,73.45,42.703,73.556,43.003,73.612,43.048,73.719,43.088,73.886,43.133,73.949,43.195,74.086,43.189,74.146,43.194,74.187,43.205,74.209,43.24,74.364,43.179,74.622,43.056,74.818,42.978,75.048,42.904,75.366,42.837,75.636,42.815,75.682,42.83,75.79,42.933,75.84,42.938,75.932,42.929,76.218,42.924,76.509,42.919,76.646,42.929,76.944,42.971,76.988,42.974,77.057,42.971,77.236,42.913,77.369,42.904,77.459,42.905,77.512,42.9,77.622,42.902,77.802,42.895,78.023,42.858,78.29,42.864,78.376,42.871,78.524,42.865,78.642,42.829,78.792,42.791,78.885,42.775,78.948,42.767,79.06,42.764,79.127,42.776,79.165,42.759,79.203,42.666,79.296,42.605,79.368,42.547,79.428,42.483,79.49,42.458,79.598,42.457,79.803,42.438,79.921,42.413,80.071,42.303,80.209,42.19,80.229,42.13,80.246,42.06,80.235,42.043,80.216,42.032,79.91,42.015,79.84,41.996,79.766,41.899,79.504,41.821,79.354,41.781,79.294,41.783,79.148,41.719,78.743,41.56,78.543,41.46,78.443,41.418,78.362,41.372,78.349,41.325,78.346,41.281,78.123,41.076,77.956,41.051,77.815,41.056,77.719,41.024,77.582,40.993,77.284,41.014,77.182,41.011,76.987,41.039,76.908,41.024,76.824,40.982,76.708,40.818,76.661,40.78,76.64,40.742,76.622,40.662,76.578,40.578,76.521,40.511,76.48,40.45,76.396,40.39,76.319,40.352,76.258,40.431,76.206,40.408,76.157,40.376,76.062,40.388,76.004,40.371,75.872,40.303,75.677,40.306,75.656,40.329,75.617,40.517,75.583,40.605,75.556,40.625,75.521,40.628,75.241,40.48,75.111,40.454,75.004,40.45,74.866,40.494,74.835,40.483,74.811,40.459,74.801,40.429,74.842,40.345,74.83,40.329,74.768,40.33,74.68,40.311,74.613,40.272,74.412,40.137,74.243,40.092,74.085,40.074,74.021,40.059,73.992,40.043,73.939,39.979,73.885,39.878,73.856,39.829,73.835,39.8,73.84,39.763,73.883,39.715,73.915,39.606,73.907,39.579,73.873,39.533,73.823,39.489,73.716,39.462,73.632,39.449,73.576,39.458,73.47,39.461,73.387,39.443,73.336,39.412,73.235,39.375,73.109,39.362,72.949,39.357,72.872,39.36,72.64,39.386,72.563,39.377,72.49,39.357,72.358,39.337,72.287,39.274,72.25,39.216,72.23,39.208,72.147,39.261,72.084,39.311,72.043,39.352,71.991,39.351,71.806,39.276,71.779,39.278,71.726,39.307,71.735,39.378,71.732,39.423,71.673,39.447,71.546,39.453,71.503,39.479,71.506,39.517,71.517,39.554,71.503,39.582,71.47,39.604,71.404,39.598,71.329,39.569,71.273,39.535,71.203,39.52,71.118,39.514,71.065,39.493,71.005,39.412,70.799,39.395,70.733,39.413,70.679,39.471,70.608,39.564,70.568,39.576,70.501,39.587,70.392,39.582,70.245,39.543,70.209,39.575,70.171,39.584,70.137,39.558,70.102,39.561,69.956,39.553,69.772,39.557,69.667,39.575,69.599,39.574,69.463,39.532,69.392,39.532,69.298,39.525,69.28,39.666,69.229,39.761,69.245,39.827,69.279,39.918,69.307,39.969,69.365,39.947,69.432,39.91,69.476,39.92,69.488,39.95,69.471,39.991,69.469,40.021,69.494,40.06,69.53,40.097,69.765,40.158,69.967,40.202,70.071,40.173,70.274,40.105,70.379,40.07,70.451,40.049,70.515,39.95,70.557,39.954,70.599,39.975,70.624,39.999,70.644,40.083,70.739,40.131,70.946,40.188,70.961,40.221,70.958,40.239],[71.206,39.893,71.216,39.907,71.179,39.98,71.229,40.048,71.13,40.06,71.08,40.08,71.024,40.149,71.005,40.152,70.976,40.133,70.961,40.088,70.974,40.039,71.014,40.006,71.041,39.995,71.045,39.993,71.044,39.976,71.012,39.895,71.064,39.885,71.156,39.883,71.206,39.893],[71.78,39.95,71.79,39.995,71.765,39.993,71.737,39.981,71.681,39.969,71.669,39.946,71.706,39.917,71.753,39.907,71.78,39.95],[70.702,39.825,70.698,39.846,70.664,39.855,70.567,39.867,70.498,39.882,70.483,39.883,70.489,39.863,70.519,39.828,70.56,39.791,70.612,39.787,70.702,39.825]]]},{"n":"Kuwait","c":"KW","r":6,"ml":5,"lx":47.31,"ly":29.41,"b":[46.531,28.533,48.442,30.097],"p":[[[48.275,29.624,48.218,29.602,48.18,29.611,48.143,29.665,48.081,29.799,48.115,29.849,48.113,29.87,48.12,29.886,48.139,29.897,48.159,29.96,48.185,29.979,48.228,29.936,48.348,29.783,48.347,29.72,48.34,29.695,48.275,29.624]],[[48.442,28.543,48.269,28.541,48.05,28.538,47.872,28.535,47.671,28.533,47.583,28.628,47.553,28.732,47.521,28.838,47.433,28.99,47.139,29.026,46.982,29.046,46.725,29.075,46.531,29.096,46.694,29.26,46.769,29.347,46.906,29.538,46.976,29.673,47.044,29.823,47.102,29.94,47.114,29.961,47.148,30.001,47.223,30.042,47.331,30.08,47.515,30.096,47.644,30.097,47.673,30.096,47.754,30.077,47.979,29.983,47.974,29.946,48.006,29.836,48.077,29.716,48.136,29.618,48.143,29.572,48.089,29.579,48.048,29.598,47.97,29.617,47.817,29.487,47.725,29.417,47.723,29.393,47.845,29.366,47.935,29.367,47.998,29.386,48.051,29.355,48.086,29.275,48.1,29.211,48.184,28.979,48.253,28.901,48.339,28.763,48.371,28.692,48.39,28.632,48.442,28.543]]]},{"n":"Kosovo","c":"XK","r":6,"ml":5,"lx":20.86,"ly":42.59,"b":[20.029,41.854,21.753,43.261],"p":[[[20.344,42.828,20.469,42.858,20.487,42.879,20.458,42.925,20.475,42.953,20.624,43.034,20.649,43.071,20.658,43.1,20.638,43.13,20.61,43.178,20.623,43.199,20.701,43.226,20.763,43.259,20.801,43.261,20.824,43.238,20.824,43.214,20.844,43.173,20.891,43.152,20.968,43.116,21.057,43.092,21.127,43.043,21.223,42.956,21.237,42.913,21.323,42.875,21.403,42.832,21.391,42.751,21.663,42.681,21.724,42.682,21.753,42.67,21.752,42.652,21.731,42.595,21.619,42.423,21.61,42.387,21.53,42.35,21.519,42.328,21.542,42.281,21.563,42.248,21.561,42.248,21.39,42.22,21.332,42.187,21.298,42.13,21.287,42.1,21.256,42.1,21.206,42.129,21.142,42.175,21.06,42.171,20.778,42.071,20.75,42.018,20.744,41.904,20.725,41.874,20.695,41.854,20.579,41.866,20.566,41.874,20.581,41.917,20.575,42.013,20.523,42.171,20.485,42.223,20.408,42.275,20.348,42.309,20.241,42.339,20.186,42.426,20.104,42.525,20.064,42.547,20.07,42.557,20.089,42.632,20.066,42.686,20.029,42.732,20.054,42.76,20.13,42.76,20.193,42.755,20.215,42.799,20.344,42.828]]]},{"n":"Kiribati","c":"KI","r":6,"ml":5,"lx":-157.38,"ly":1.82,"b":[-174.541,-11.457,174.779,3.924],"p":[[[172.97,3.129,172.906,3.096,172.887,3.101,172.963,3.149,172.962,3.143,172.97,3.129]],[[172.844,3.051,172.77,3.013,172.75,3.033,172.777,3.034,172.827,3.071,172.887,3.074,172.88,3.054,172.844,3.051]],[[173.019,1.846,173.024,1.823,172.967,1.885,172.933,1.926,172.935,1.944,172.95,1.933,172.969,1.913,172.982,1.897,173.019,1.846]],[[173.029,1.717,172.993,1.713,173.02,1.727,173.028,1.747,173.024,1.809,173.038,1.804,173.043,1.779,173.045,1.742,173.029,1.717]],[[173.033,1.013,173.087,0.973,173.079,0.946,173.061,0.915,172.991,0.835,172.97,0.843,173.039,0.915,173.065,0.963,173.026,0.999,173.01,0.991,173.004,0.991,172.99,1.025,173.004,1.025,173.033,1.013]],[[174.509,-0.802,174.476,-0.829,174.464,-0.804,174.48,-0.774,174.453,-0.647,174.408,-0.63,174.381,-0.592,174.394,-0.592,174.439,-0.627,174.475,-0.642,174.495,-0.726,174.509,-0.802]],[[174.773,-1.212,174.779,-1.263,174.756,-1.256,174.748,-1.236,174.741,-1.185,174.717,-1.134,174.744,-1.147,174.767,-1.187,174.773,-1.212]],[[173.038,1.342,173.011,1.338,173.029,1.359,173.143,1.381,173.153,1.388,173.172,1.375,173.171,1.363,173.163,1.358,173.106,1.357,173.062,1.346,173.038,1.342]],[[169.551,-0.874,169.542,-0.876,169.523,-0.866,169.526,-0.853,169.539,-0.847,169.555,-0.857,169.551,-0.874]],[[-172.215,-4.511,-172.208,-4.518,-172.194,-4.516,-172.181,-4.515,-172.189,-4.522,-172.215,-4.524,-172.228,-4.507,-172.212,-4.494,-172.198,-4.492,-172.196,-4.495,-172.197,-4.5,-172.204,-4.5,-172.215,-4.503,-172.215,-4.511]],[[-171.085,-3.135,-171.09,-3.143,-171.097,-3.137,-171.092,-3.125,-171.088,-3.115,-171.081,-3.12,-171.085,-3.135]],[[-171.233,-4.463,-171.243,-4.468,-171.255,-4.467,-171.262,-4.46,-171.262,-4.449,-171.252,-4.442,-171.239,-4.444,-171.232,-4.454,-171.233,-4.463]],[[-174.513,-4.675,-174.501,-4.688,-174.501,-4.695,-174.507,-4.694,-174.524,-4.69,-174.529,-4.682,-174.517,-4.687,-174.511,-4.686,-174.523,-4.674,-174.533,-4.665,-174.541,-4.662,-174.541,-4.657,-174.531,-4.659,-174.513,-4.675]],[[-154.956,-4.088,-154.959,-4.094,-154.971,-4.086,-154.995,-4.071,-155.015,-4.055,-155.015,-4.048,-154.987,-4.039,-154.951,-4.031,-154.943,-4.042,-154.95,-4.056,-154.956,-4.088]],[[-151.783,-11.441,-151.791,-11.457,-151.807,-11.451,-151.816,-11.431,-151.819,-11.409,-151.813,-11.392,-151.803,-11.393,-151.791,-11.414,-151.783,-11.441]],[[-155.864,-5.627,-155.887,-5.632,-155.914,-5.632,-155.928,-5.619,-155.929,-5.608,-155.919,-5.608,-155.911,-5.609,-155.872,-5.611,-155.862,-5.619,-155.864,-5.627]],[[-157.342,1.856,-157.176,1.74,-157.246,1.732,-157.42,1.788,-157.579,1.902,-157.531,1.927,-157.508,1.886,-157.436,1.847,-157.393,1.928,-157.365,1.946,-157.492,2.029,-157.442,2.025,-157.322,1.969,-157.342,1.856]],[[-159.339,3.924,-159.259,3.839,-159.275,3.797,-159.332,3.8,-159.359,3.815,-159.314,3.823,-159.306,3.838,-159.327,3.863,-159.354,3.881,-159.373,3.881,-159.378,3.847,-159.409,3.873,-159.391,3.9,-159.369,3.917,-159.339,3.924]],[[-171.698,-2.766,-171.665,-2.786,-171.64,-2.811,-171.628,-2.847,-171.628,-2.856,-171.647,-2.856,-171.671,-2.844,-171.687,-2.83,-171.696,-2.826,-171.698,-2.822,-171.678,-2.825,-171.655,-2.84,-171.639,-2.847,-171.64,-2.829,-171.66,-2.799,-171.673,-2.788,-171.688,-2.779,-171.706,-2.773,-171.718,-2.779,-171.725,-2.781,-171.728,-2.774,-171.725,-2.768,-171.719,-2.761,-171.698,-2.766]]]},{"n":"Kenya","c":"KE","r":2,"ml":1.7,"lx":37.91,"ly":0.55,"b":[33.9,-4.692,41.884,5.492],"p":[[[40.994,-2.158,40.957,-2.167,40.976,-2.11,41.086,-2.037,41.131,-2.053,41.139,-2.07,41.137,-2.085,41.118,-2.1,40.994,-2.158]],[[33.903,-1.002,33.9,-0.832,33.924,-0.398,33.921,-0.017,33.943,0.174,34.037,0.295,34.081,0.382,34.112,0.505,34.161,0.605,34.273,0.686,34.293,0.731,34.411,0.867,34.482,1.042,34.535,1.102,34.602,1.156,34.649,1.185,34.727,1.214,34.788,1.231,34.799,1.245,34.804,1.273,34.784,1.381,34.81,1.417,34.851,1.489,34.898,1.556,34.941,1.599,34.965,1.643,34.976,1.72,34.978,1.774,34.978,1.862,34.964,2.062,34.914,2.23,34.883,2.418,34.906,2.48,34.866,2.59,34.847,2.596,34.814,2.62,34.773,2.723,34.742,2.818,34.723,2.842,34.589,2.925,34.523,3.12,34.448,3.163,34.407,3.358,34.399,3.413,34.442,3.606,34.438,3.651,34.393,3.692,34.267,3.733,34.165,3.813,34.178,3.841,34.186,3.87,34.132,3.889,33.976,4.22,34.177,4.419,34.38,4.621,34.64,4.875,34.878,5.11,35.084,5.312,35.268,5.492,35.264,5.458,35.265,5.412,35.288,5.384,35.325,5.365,35.378,5.385,35.424,5.413,35.469,5.419,35.745,5.344,35.791,5.279,35.788,5.208,35.8,5.157,35.779,5.106,35.756,4.95,35.763,4.808,35.846,4.703,35.92,4.62,35.979,4.504,36.022,4.468,36.082,4.45,36.272,4.445,36.553,4.437,36.824,4.43,36.848,4.427,36.906,4.411,37.155,4.255,37.383,4.111,37.575,3.986,37.763,3.865,37.945,3.747,38.086,3.649,38.225,3.619,38.452,3.605,38.608,3.6,38.753,3.559,38.968,3.521,39.128,3.501,39.225,3.479,39.494,3.456,39.539,3.469,39.658,3.578,39.79,3.754,39.842,3.851,40.014,3.948,40.316,4.083,40.529,4.178,40.765,4.273,40.873,4.19,41.021,4.057,41.087,3.992,41.14,3.963,41.221,3.944,41.319,3.943,41.372,3.946,41.482,3.963,41.738,3.979,41.884,3.978,41.761,3.802,41.613,3.59,41.342,3.202,41.135,2.997,40.979,2.842,40.964,2.815,40.965,2.642,40.967,2.221,40.97,1.378,40.973,0.535,40.977,-0.307,40.978,-0.729,40.979,-0.87,41.116,-1.047,41.25,-1.221,41.427,-1.45,41.522,-1.572,41.538,-1.613,41.533,-1.695,41.387,-1.867,41.267,-1.945,41.107,-1.982,41.059,-1.975,40.996,-1.951,40.971,-1.992,40.952,-2.056,40.917,-2.042,40.89,-2.024,40.906,-2.137,40.922,-2.194,40.898,-2.27,40.82,-2.336,40.813,-2.392,40.644,-2.539,40.404,-2.556,40.279,-2.629,40.222,-2.688,40.18,-2.819,40.195,-3.019,40.128,-3.173,40.115,-3.251,39.992,-3.351,39.937,-3.442,39.896,-3.536,39.861,-3.577,39.819,-3.786,39.761,-3.913,39.746,-3.955,39.732,-3.993,39.687,-4.068,39.658,-4.119,39.637,-4.153,39.491,-4.478,39.377,-4.625,39.288,-4.609,39.228,-4.666,39.222,-4.692,39.19,-4.677,39.115,-4.624,38.962,-4.513,38.808,-4.402,38.655,-4.292,38.501,-4.181,38.348,-4.071,38.194,-3.96,38.041,-3.85,37.887,-3.739,37.797,-3.674,37.757,-3.636,37.726,-3.56,37.711,-3.541,37.67,-3.517,37.622,-3.512,37.608,-3.497,37.609,-3.46,37.625,-3.407,37.682,-3.306,37.688,-3.246,37.677,-3.178,37.659,-3.07,37.644,-3.045,37.542,-2.989,37.329,-2.87,37.116,-2.751,36.903,-2.632,36.689,-2.513,36.476,-2.394,36.263,-2.275,36.05,-2.156,35.837,-2.037,35.624,-1.918,35.411,-1.799,35.197,-1.68,34.984,-1.561,34.771,-1.442,34.558,-1.323,34.345,-1.204,34.132,-1.085,34.052,-1.04,33.979,-1.002,33.903,-1.002]]]},{"n":"Kazakhstan","c":"KZ","r":3,"ml":2.7,"lx":68.69,"ly":49.05,"b":[46.609,40.609,87.323,55.39],"p":[[[50.184,44.855,50.149,44.826,50.095,44.831,49.995,44.937,50.023,45.045,50.059,45.067,50.11,45.082,50.117,45.058,50.045,45.01,50.039,44.949,50.098,44.882,50.184,44.855]],[[50.312,44.972,50.277,44.959,50.256,45.022,50.295,45.076,50.35,45.083,50.331,44.998,50.312,44.972]],[[52.682,45.412,52.665,45.401,52.598,45.428,52.554,45.474,52.609,45.528,52.66,45.518,52.693,45.461,52.682,45.412]],[[70.947,42.249,70.86,42.207,70.765,42.194,70.715,42.169,70.663,42.107,70.613,42.055,70.584,42.036,70.54,42.039,70.489,42.08,70.416,42.079,70.329,42.028,70.226,41.946,70.096,41.821,69.96,41.754,69.788,41.697,69.664,41.672,69.565,41.629,69.401,41.542,69.368,41.491,69.249,41.46,69.154,41.425,69.065,41.367,69.043,41.264,68.987,41.205,68.851,41.124,68.737,41.042,68.663,40.962,68.584,40.876,68.559,40.829,68.557,40.765,68.594,40.711,68.601,40.66,68.573,40.623,68.496,40.609,68.415,40.619,68.292,40.656,68.16,40.722,68.112,40.754,68.048,40.809,68.057,40.861,68.09,40.96,68.113,41.029,68.059,41.061,68.02,41.096,67.991,41.13,67.936,41.197,67.866,41.18,67.805,41.164,67.735,41.187,67.528,41.177,67.372,41.17,67.225,41.162,67.039,41.153,66.814,41.142,66.75,41.157,66.71,41.179,66.669,41.271,66.645,41.349,66.602,41.494,66.573,41.607,66.538,41.741,66.515,41.889,66.499,41.995,66.329,41.998,66.193,42.001,66.01,42.005,66.011,42.089,66.013,42.194,66.016,42.315,66.05,42.473,66.063,42.605,66.079,42.767,66.089,42.873,66.1,42.991,66.006,42.955,65.901,42.915,65.803,42.877,65.736,42.972,65.67,43.065,65.571,43.205,65.496,43.311,65.367,43.372,65.271,43.418,65.171,43.494,65.085,43.574,65.003,43.649,64.905,43.715,64.812,43.694,64.706,43.653,64.604,43.613,64.496,43.572,64.443,43.551,64.318,43.559,64.209,43.566,64.013,43.578,63.848,43.588,63.68,43.599,63.445,43.613,63.207,43.628,63.048,43.608,62.846,43.584,62.634,43.558,62.459,43.537,62.238,43.51,62.072,43.489,61.99,43.492,61.888,43.577,61.723,43.714,61.624,43.796,61.526,43.877,61.385,43.994,61.271,44.082,61.161,44.169,61.097,44.248,61.065,44.348,61.008,44.394,60.879,44.455,60.741,44.521,60.603,44.587,60.465,44.652,60.327,44.718,60.188,44.784,60.05,44.85,59.912,44.916,59.774,44.981,59.636,45.047,59.498,45.113,59.36,45.179,59.221,45.244,59.083,45.31,58.945,45.376,58.807,45.442,58.669,45.508,58.555,45.555,58.449,45.543,58.291,45.509,58.125,45.474,57.961,45.44,57.667,45.377,57.477,45.337,57.329,45.304,57.172,45.268,56.965,45.221,56.792,45.181,56.589,45.135,56.409,45.094,56.258,45.059,56.1,45.023,55.976,44.995,55.976,44.765,55.976,44.536,55.976,44.306,55.976,44.077,55.976,43.847,55.976,43.618,55.976,43.388,55.977,43.159,55.977,42.929,55.977,42.7,55.977,42.47,55.977,42.24,55.977,42.011,55.977,41.781,55.977,41.552,55.977,41.322,55.935,41.324,55.839,41.311,55.679,41.279,55.545,41.263,55.487,41.272,55.434,41.296,55.388,41.347,55.32,41.408,55.25,41.458,55.162,41.56,55.102,41.639,54.952,41.81,54.932,41.864,54.904,41.919,54.854,41.965,54.678,42.078,54.473,42.18,54.272,42.28,54.215,42.304,54.121,42.335,54.005,42.336,53.926,42.33,53.685,42.297,53.501,42.258,53.25,42.206,53.056,42.148,53.013,42.131,52.871,42.061,52.697,41.944,52.494,41.78,52.468,41.886,52.459,42.048,52.462,42.101,52.517,42.237,52.573,42.331,52.618,42.428,52.638,42.556,52.597,42.76,52.55,42.805,52.494,42.82,52.434,42.824,52.324,42.816,52.273,42.8,52.184,42.869,52.076,42.88,52.019,42.861,51.961,42.851,51.898,42.87,51.844,42.91,51.811,42.954,51.785,43.004,51.7,43.104,51.616,43.158,51.514,43.171,51.348,43.167,51.295,43.174,51.292,43.231,51.313,43.356,51.314,43.421,51.302,43.482,51.274,43.533,51.239,43.577,51.14,43.649,51.065,43.75,50.94,43.959,50.831,44.193,50.783,44.228,50.685,44.265,50.472,44.295,50.331,44.325,50.276,44.355,50.253,44.406,50.253,44.462,50.265,44.527,50.297,44.582,50.409,44.624,50.652,44.633,50.86,44.629,51.049,44.53,51.111,44.508,51.177,44.501,51.311,44.532,51.377,44.541,51.544,44.531,51.494,44.578,51.431,44.602,51.366,44.6,51.31,44.619,51.218,44.709,51.058,44.812,51.021,44.854,51.009,44.922,51.04,44.98,51.154,45.04,51.25,45.122,51.294,45.23,51.333,45.28,51.416,45.358,51.54,45.343,51.733,45.399,52.049,45.388,52.427,45.405,52.531,45.399,52.772,45.344,52.911,45.32,53.079,45.308,53.2,45.332,53.086,45.407,52.838,45.497,52.774,45.573,52.888,45.78,53.042,45.968,53.135,46.192,53.109,46.414,53.064,46.475,53.079,46.547,53.132,46.608,53.17,46.669,53.138,46.742,53.069,46.856,53.035,46.893,52.916,46.954,52.678,46.957,52.483,46.991,52.42,46.964,52.385,46.922,52.34,46.895,52.189,46.84,52.138,46.829,52.086,46.84,52.011,46.902,51.945,46.895,51.745,46.934,51.65,47.018,51.615,47.03,51.291,47.097,51.178,47.11,50.92,47.041,50.733,46.952,50.68,46.939,50.583,46.882,50.528,46.873,50.472,46.883,50.419,46.879,50.306,46.795,50.102,46.696,50,46.634,49.886,46.596,49.761,46.571,49.632,46.568,49.584,46.545,49.437,46.537,49.347,46.519,49.344,46.486,49.362,46.41,49.286,46.437,49.206,46.386,49.232,46.337,49.184,46.349,48.959,46.442,48.774,46.508,48.61,46.566,48.586,46.577,48.541,46.606,48.509,46.65,48.502,46.699,48.519,46.734,48.558,46.757,48.605,46.766,48.647,46.759,48.694,46.737,48.776,46.71,48.884,46.705,48.95,46.726,48.959,46.775,48.832,46.955,48.714,47.1,48.601,47.262,48.553,47.321,48.413,47.456,48.276,47.59,48.167,47.709,48.11,47.745,47.935,47.761,47.6,47.79,47.482,47.804,47.387,47.769,47.292,47.741,47.202,47.792,47.131,47.877,47.093,47.948,47.112,48.02,47.119,48.127,47.065,48.232,47.004,48.284,46.853,48.324,46.661,48.412,46.609,48.574,46.703,48.806,46.853,48.97,46.962,49.038,47.014,49.098,47.031,49.15,47.018,49.2,46.953,49.253,46.853,49.304,46.802,49.367,46.823,49.502,46.89,49.697,46.992,49.853,47.13,49.939,47.248,50.001,47.295,50.058,47.298,50.14,47.295,50.217,47.326,50.274,47.376,50.318,47.429,50.358,47.504,50.403,47.6,50.414,47.706,50.378,47.85,50.282,48.061,50.094,48.181,49.97,48.225,49.932,48.335,49.858,48.434,49.829,48.6,49.875,48.759,49.928,48.81,49.962,48.843,50.013,48.818,50.1,48.785,50.156,48.749,50.228,48.7,50.354,48.666,50.55,48.625,50.613,48.655,50.62,48.735,50.607,48.808,50.601,48.914,50.645,49.059,50.726,49.323,50.852,49.379,50.935,49.425,51.027,49.498,51.084,49.666,51.102,49.822,51.132,49.932,51.197,50.105,51.255,50.247,51.29,50.309,51.322,50.354,51.37,50.516,51.506,50.644,51.589,50.756,51.675,50.794,51.729,50.882,51.719,51.018,51.682,51.163,51.647,51.27,51.594,51.291,51.54,51.301,51.497,51.345,51.475,51.396,51.471,51.473,51.482,51.609,51.484,51.775,51.554,52.007,51.673,52.219,51.709,52.331,51.681,52.423,51.594,52.496,51.512,52.571,51.482,52.618,51.481,52.635,51.48,52.728,51.498,52.735,51.498,52.821,51.495,52.903,51.467,53.038,51.464,53.227,51.485,53.247,51.494,53.338,51.482,53.449,51.445,53.535,51.4,53.688,51.252,53.776,51.214,53.957,51.161,54.042,51.115,54.14,51.041,54.191,50.996,54.298,50.914,54.421,50.78,54.443,50.674,54.471,50.584,54.517,50.541,54.555,50.536,54.596,50.551,54.636,50.592,54.65,50.66,54.638,50.781,54.606,50.88,54.566,50.911,54.546,50.946,54.573,50.99,54.642,51.012,54.727,50.998,54.868,50.941,55.015,50.87,55.195,50.745,55.361,50.665,55.542,50.602,55.686,50.583,55.798,50.602,55.929,50.654,56.05,50.714,56.104,50.776,56.144,50.845,56.326,50.936,56.491,51.02,56.567,51.004,56.62,50.981,56.79,51.032,56.85,51.046,57.012,51.065,57.179,51.036,57.313,50.947,57.442,50.889,57.558,50.896,57.654,50.925,57.717,50.981,57.765,51.047,57.829,51.089,57.839,51.092,58.045,51.069,58.175,51.072,58.188,51.082,58.359,51.064,58.547,50.971,58.665,50.868,58.814,50.737,58.884,50.694,58.985,50.676,59.064,50.668,59.171,50.648,59.452,50.62,59.495,50.604,59.524,50.583,59.498,50.511,59.523,50.493,59.751,50.544,59.812,50.582,59.888,50.69,59.955,50.799,60.005,50.84,60.059,50.85,60.112,50.834,60.187,50.77,60.288,50.704,60.425,50.679,60.508,50.669,60.638,50.664,60.942,50.696,61.227,50.775,61.389,50.861,61.465,50.99,61.512,51.137,61.585,51.23,61.555,51.325,61.411,51.415,61.363,51.442,61.015,51.492,60.993,51.529,60.974,51.537,60.63,51.617,60.465,51.651,60.418,51.704,60.388,51.773,60.28,51.835,60.067,51.891,60.03,51.933,60.066,51.976,60.234,52.025,60.425,52.126,60.499,52.146,60.67,52.151,60.828,52.233,60.938,52.281,60.995,52.337,60.979,52.395,60.821,52.57,60.774,52.676,60.802,52.745,60.893,52.819,60.945,52.86,61.007,52.933,61.047,52.972,61.207,52.989,61.401,52.996,61.534,52.979,61.719,52.969,61.889,52.956,61.974,52.944,62.037,52.966,62.083,53.005,62.081,53.057,62.015,53.108,61.766,53.174,61.66,53.228,61.576,53.222,61.437,53.239,61.311,53.275,61.199,53.287,61.163,53.337,61.186,53.406,61.229,53.446,61.312,53.466,61.401,53.456,61.499,53.485,61.527,53.502,61.535,53.523,61.519,53.554,61.474,53.58,61.41,53.587,61.336,53.565,61.248,53.551,61.099,53.583,60.979,53.622,60.986,53.657,61.074,53.71,61.113,53.753,61.113,53.813,61.113,53.882,61.144,53.964,61.231,54.019,61.334,54.049,61.598,53.995,61.929,53.946,61.986,53.954,62.002,53.98,62.04,54.003,62.499,54.013,62.588,54.044,62.633,54.069,63.074,54.105,63.127,54.139,63.191,54.171,63.293,54.17,63.414,54.183,63.582,54.222,63.701,54.243,63.721,54.245,63.847,54.236,64.004,54.267,64.037,54.28,64.063,54.303,64.199,54.347,64.461,54.384,64.525,54.362,64.65,54.352,64.809,54.369,64.927,54.397,64.995,54.369,65.088,54.34,65.158,54.364,65.192,54.441,65.237,54.516,65.316,54.552,65.378,54.564,65.434,54.593,65.477,54.623,65.708,54.619,65.914,54.693,65.955,54.66,66.223,54.667,66.555,54.715,66.754,54.738,67.098,54.788,67.257,54.829,67.485,54.854,67.693,54.872,67.83,54.944,67.94,54.954,68.074,54.96,68.156,54.977,68.209,55.003,68.244,55.052,68.225,55.115,68.206,55.161,68.302,55.187,68.438,55.194,68.525,55.205,68.713,55.308,68.843,55.358,68.977,55.39,69.247,55.373,69.493,55.357,69.74,55.307,69.87,55.246,69.982,55.199,70.087,55.177,70.182,55.162,70.293,55.184,70.371,55.212,70.417,55.253,70.486,55.282,70.738,55.305,70.79,55.261,70.91,55.128,70.992,54.95,71.126,54.715,71.186,54.599,71.159,54.539,71.16,54.455,71.152,54.364,71.053,54.26,71.093,54.212,71.336,54.158,71.677,54.178,71.887,54.221,72.004,54.206,72.066,54.232,72.105,54.308,72.186,54.326,72.269,54.272,72.329,54.181,72.387,54.123,72.383,54.054,72.404,53.964,72.447,53.942,72.53,53.976,72.586,53.996,72.599,54.023,72.576,54.056,72.564,54.09,72.583,54.122,72.622,54.134,72.741,54.125,72.914,54.107,73.119,53.981,73.23,53.958,73.277,53.956,73.381,53.963,73.506,53.999,73.59,54.045,73.618,54.067,73.666,54.063,73.712,54.042,73.716,53.996,73.679,53.929,73.554,53.868,73.399,53.811,73.306,53.707,73.286,53.598,73.327,53.543,73.362,53.506,73.372,53.454,73.407,53.448,73.47,53.469,73.643,53.576,73.731,53.603,73.859,53.62,74.069,53.611,74.21,53.576,74.277,53.528,74.352,53.488,74.403,53.504,74.429,53.551,74.43,53.604,74.452,53.647,74.681,53.754,74.834,53.826,74.887,53.834,74.989,53.819,75.052,53.827,75.22,53.894,75.377,53.97,75.392,54.022,75.398,54.069,75.437,54.09,75.657,54.106,75.693,54.115,75.881,54.168,76.141,54.259,76.267,54.312,76.496,54.336,76.539,54.351,76.616,54.387,76.759,54.437,76.837,54.442,76.789,54.322,76.703,54.182,76.655,54.145,76.422,54.152,76.422,54.114,76.459,54.055,76.485,54.023,76.513,53.993,76.576,53.943,76.821,53.823,77.132,53.67,77.469,53.499,77.704,53.379,77.799,53.317,77.86,53.269,78.033,53.095,78.198,52.93,78.475,52.638,78.721,52.357,78.992,52.047,79.149,51.868,79.469,51.493,79.554,51.378,79.716,51.16,79.86,50.955,79.986,50.775,80.066,50.758,80.072,50.807,80.086,50.84,80.127,50.858,80.22,50.912,80.27,50.925,80.345,50.919,80.424,50.946,80.452,50.998,80.434,51.093,80.421,51.136,80.448,51.183,80.491,51.202,80.551,51.217,80.605,51.224,80.65,51.277,80.735,51.293,80.813,51.283,80.877,51.281,80.934,51.243,80.966,51.19,81.027,51.186,81.127,51.191,81.141,51.147,81.112,51.072,81.078,51.015,81.071,50.969,81.125,50.946,81.319,50.966,81.388,50.956,81.41,50.91,81.438,50.871,81.452,50.824,81.431,50.771,81.466,50.74,81.634,50.739,81.752,50.764,81.934,50.766,82.098,50.711,82.212,50.719,82.326,50.742,82.494,50.728,82.612,50.771,82.693,50.826,82.719,50.869,82.761,50.893,82.919,50.893,83.019,50.897,83.093,50.961,83.16,50.989,83.274,50.995,83.357,50.995,83.581,50.936,83.718,50.887,83.86,50.818,83.945,50.775,84.002,50.677,84.099,50.605,84.176,50.521,84.195,50.437,84.258,50.288,84.323,50.239,84.401,50.239,84.499,50.219,84.607,50.202,84.839,50.091,84.924,50.088,84.989,50.061,85,50.01,84.975,49.951,85.001,49.894,85.076,49.822,85.137,49.751,85.21,49.665,85.233,49.616,85.292,49.599,85.372,49.624,85.498,49.605,85.88,49.557,85.934,49.55,85.974,49.499,86.03,49.503,86.093,49.505,86.181,49.499,86.242,49.546,86.292,49.588,86.418,49.638,86.522,49.708,86.61,49.769,86.675,49.777,86.729,49.749,86.731,49.696,86.665,49.657,86.614,49.61,86.626,49.563,86.714,49.559,86.812,49.488,86.953,49.322,87.001,49.287,87.071,49.255,87.148,49.24,87.234,49.216,87.297,49.148,87.323,49.086,87.23,49.106,87.049,49.11,86.938,49.098,86.886,49.091,86.808,49.05,86.753,49.009,86.729,48.939,86.758,48.861,86.718,48.697,86.664,48.636,86.549,48.529,86.483,48.505,86.373,48.486,86.266,48.455,86.056,48.424,85.83,48.408,85.749,48.385,85.692,48.312,85.652,48.251,85.626,48.204,85.562,48.052,85.526,47.916,85.562,47.746,85.588,47.558,85.587,47.494,85.642,47.397,85.67,47.338,85.657,47.255,85.577,47.188,85.53,47.101,85.485,47.064,85.355,47.047,85.233,47.036,85.111,46.961,85.012,46.909,84.858,46.843,84.786,46.831,84.746,46.864,84.72,46.939,84.667,46.972,84.592,46.975,84.532,46.976,84.339,46.996,84.215,46.995,84.122,46.979,84.016,46.971,83.833,46.998,83.714,47.021,83.634,47.043,83.444,47.109,83.193,47.187,83.09,47.209,83.029,47.186,83.02,47.141,83.004,47.033,82.975,46.966,82.8,46.624,82.692,46.387,82.555,46.159,82.512,46.006,82.43,45.812,82.348,45.672,82.315,45.595,82.312,45.564,82.327,45.52,82.452,45.472,82.583,45.443,82.612,45.424,82.626,45.374,82.621,45.293,82.597,45.216,82.559,45.155,82.521,45.125,82.479,45.124,82.397,45.162,82.323,45.206,82.267,45.219,82.123,45.195,81.989,45.162,81.945,45.161,81.867,45.182,81.79,45.226,81.759,45.311,81.692,45.349,81.602,45.311,81.335,45.246,81.04,45.169,80.853,45.129,80.78,45.136,80.635,45.127,80.509,45.105,80.415,45.075,80.228,45.034,80.059,45.006,79.95,44.944,79.872,44.884,79.875,44.861,79.932,44.825,79.997,44.797,80.128,44.804,80.255,44.808,80.361,44.77,80.455,44.746,80.482,44.715,80.455,44.684,80.401,44.677,80.381,44.655,80.391,44.627,80.355,44.552,80.336,44.438,80.355,44.327,80.365,44.223,80.359,44.171,80.355,44.097,80.396,44.047,80.432,43.952,80.496,43.892,80.593,43.685,80.651,43.564,80.704,43.427,80.665,43.353,80.668,43.31,80.73,43.274,80.757,43.204,80.786,43.162,80.778,43.119,80.751,43.102,80.617,43.128,80.507,43.086,80.39,43.043,80.375,43.02,80.371,42.996,80.383,42.974,80.451,42.936,80.544,42.912,80.539,42.873,80.424,42.856,80.25,42.797,80.202,42.734,80.165,42.666,80.162,42.626,80.179,42.518,80.206,42.399,80.255,42.274,80.259,42.235,80.233,42.208,80.209,42.19,80.071,42.303,79.921,42.413,79.803,42.438,79.598,42.457,79.49,42.458,79.428,42.483,79.368,42.547,79.296,42.605,79.203,42.666,79.165,42.759,79.127,42.776,79.06,42.764,78.948,42.767,78.885,42.775,78.792,42.791,78.642,42.829,78.524,42.865,78.376,42.871,78.29,42.864,78.023,42.858,77.802,42.895,77.622,42.902,77.512,42.9,77.459,42.905,77.369,42.904,77.236,42.913,77.057,42.971,76.988,42.974,76.944,42.971,76.646,42.929,76.509,42.919,76.218,42.924,75.932,42.929,75.84,42.938,75.79,42.933,75.682,42.83,75.636,42.815,75.366,42.837,75.048,42.904,74.818,42.978,74.622,43.056,74.364,43.179,74.209,43.24,74.187,43.205,74.146,43.194,74.086,43.189,73.949,43.195,73.886,43.133,73.719,43.088,73.612,43.048,73.556,43.003,73.45,42.703,73.422,42.594,73.493,42.409,73.412,42.42,73.316,42.467,73.283,42.504,73.191,42.527,72.855,42.561,72.792,42.603,72.753,42.638,72.666,42.66,72.543,42.678,72.276,42.758,72.162,42.761,71.817,42.822,71.761,42.821,71.735,42.819,71.601,42.779,71.514,42.767,71.422,42.783,71.257,42.734,71.167,42.667,71.094,42.587,71.023,42.535,71.002,42.459,70.952,42.419,70.893,42.34,70.893,42.294,70.947,42.249]]]},{"n":"Jordan","c":"JO","r":4,"ml":4,"lx":36.38,"ly":30.81,"b":[34.951,29.19,39.293,33.372],"p":[[[35.787,32.735,35.895,32.714,35.956,32.667,36.059,32.534,36.22,32.495,36.284,32.457,36.372,32.387,36.479,32.361,36.818,32.317,37.089,32.466,37.318,32.591,37.577,32.733,37.754,32.83,38.056,32.995,38.254,33.099,38.516,33.237,38.774,33.372,38.845,33.151,38.915,32.935,38.987,32.711,39.058,32.493,38.982,32.473,39.041,32.306,39.14,32.331,39.247,32.351,39.293,32.244,39.145,32.125,38.997,32.007,38.962,31.995,38.77,31.946,38.375,31.847,38.111,31.781,37.774,31.696,37.493,31.626,37.216,31.556,36.959,31.492,37.105,31.355,37.329,31.147,37.479,31.008,37.655,30.829,37.813,30.669,37.98,30.5,37.863,30.443,37.67,30.348,37.65,30.331,37.634,30.313,37.554,30.145,37.491,30.012,37.469,29.995,37.199,29.946,36.927,29.897,36.755,29.866,36.704,29.832,36.592,29.666,36.476,29.495,36.283,29.355,36.068,29.201,36.015,29.19,35.86,29.214,35.595,29.255,35.339,29.294,35.164,29.321,34.951,29.354,34.982,29.484,34.973,29.555,35.024,29.787,35.053,29.897,35.068,29.978,35.142,30.142,35.133,30.195,35.148,30.384,35.141,30.421,35.174,30.524,35.237,30.673,35.298,30.802,35.32,30.86,35.383,30.982,35.439,31.132,35.41,31.214,35.401,31.231,35.424,31.325,35.423,31.325,35.451,31.479,35.465,31.562,35.499,31.672,35.559,31.766,35.531,31.985,35.535,32.103,35.572,32.238,35.551,32.396,35.569,32.62,35.573,32.641,35.595,32.668,35.611,32.682,35.734,32.729,35.787,32.735]]]},{"n":"Japan","c":"JP","r":2,"ml":1.7,"lx":138.44,"ly":36.14,"b":[123.68,24.266,145.833,45.51],"p":[[[133.371,36.204,133.325,36.167,133.239,36.179,133.19,36.233,133.206,36.293,133.296,36.34,133.381,36.246,133.371,36.204]],[[138.344,37.822,138.249,37.82,138.225,37.829,138.283,37.854,138.288,37.896,138.322,37.97,138.322,37.991,138.246,37.995,138.25,38.078,138.306,38.161,138.461,38.291,138.504,38.316,138.51,38.259,138.463,38.124,138.454,38.076,138.575,38.066,138.497,37.904,138.344,37.822]],[[134.933,34.288,134.824,34.203,134.731,34.209,134.683,34.247,134.668,34.294,134.757,34.368,134.834,34.473,134.904,34.519,134.961,34.545,135.005,34.544,134.905,34.398,134.933,34.288]],[[130.623,30.263,130.508,30.241,130.446,30.265,130.388,30.388,130.497,30.466,130.644,30.389,130.673,30.367,130.623,30.263]],[[130.96,30.397,130.872,30.386,130.87,30.444,130.94,30.575,130.947,30.671,131.012,30.792,131.04,30.819,131.06,30.828,131.083,30.791,131.057,30.642,130.993,30.53,130.96,30.397]],[[130.083,32.23,130.004,32.194,129.993,32.228,129.96,32.244,130.017,32.292,130.015,32.314,129.979,32.346,130.021,32.469,130.01,32.522,130.168,32.541,130.197,32.492,130.2,32.341,130.083,32.23]],[[129.279,34.123,129.214,34.083,129.186,34.145,129.215,34.321,129.337,34.285,129.335,34.231,129.279,34.123]],[[129.386,34.354,129.365,34.306,129.297,34.34,129.267,34.37,129.329,34.522,129.322,34.579,129.326,34.607,129.451,34.687,129.472,34.671,129.48,34.649,129.469,34.616,129.475,34.54,129.381,34.416,129.386,34.354]],[[128.665,32.784,128.704,32.757,128.761,32.772,128.806,32.776,128.839,32.763,128.879,32.693,128.895,32.652,128.821,32.646,128.79,32.637,128.75,32.586,128.693,32.605,128.657,32.628,128.649,32.662,128.665,32.784]],[[130.381,32.424,130.293,32.419,130.256,32.431,130.242,32.463,130.365,32.527,130.461,32.516,130.419,32.458,130.381,32.424]],[[141.073,45.333,141.034,45.269,140.982,45.364,140.972,45.465,141.002,45.465,141.057,45.45,141.07,45.4,141.073,45.333]],[[141.295,45.119,141.226,45.112,141.145,45.154,141.135,45.206,141.194,45.248,141.252,45.232,141.31,45.179,141.329,45.15,141.295,45.119]],[[139.481,42.081,139.458,42.076,139.435,42.084,139.412,42.16,139.431,42.2,139.496,42.227,139.558,42.235,139.505,42.096,139.481,42.081]],[[134.352,34.484,134.333,34.464,134.315,34.469,134.252,34.423,134.238,34.467,134.188,34.496,134.182,34.519,134.326,34.534,134.372,34.522,134.352,34.484]],[[139.456,34.727,139.446,34.68,139.392,34.69,139.367,34.721,139.37,34.775,139.426,34.776,139.456,34.727]],[[129.077,32.84,129.052,32.829,129.02,32.92,128.997,32.952,129.035,32.969,129.11,33.133,129.124,33.068,129.153,33.003,129.182,32.993,129.154,32.946,129.112,32.929,129.077,32.84]],[[129.492,33.223,129.421,33.176,129.37,33.176,129.417,33.231,129.423,33.257,129.462,33.331,129.538,33.358,129.57,33.361,129.508,33.284,129.492,33.223]],[[129.796,33.749,129.727,33.707,129.675,33.74,129.7,33.829,129.717,33.858,129.776,33.829,129.796,33.749]],[[132.578,34.115,132.549,34.075,132.461,34.087,132.496,34.122,132.524,34.164,132.543,34.173,132.56,34.127,132.578,34.115]],[[132.266,33.945,132.315,33.909,132.43,33.924,132.445,33.913,132.411,33.88,132.36,33.847,132.267,33.871,132.209,33.873,132.201,33.928,132.208,33.948,132.266,33.945]],[[129.718,31.657,129.687,31.64,129.707,31.718,129.787,31.787,129.794,31.742,129.718,31.657]],[[142.188,26.617,142.17,26.616,142.107,26.722,142.125,26.726,142.162,26.71,142.202,26.649,142.188,26.617]],[[143.824,44.117,143.95,44.112,144.005,44.117,144.101,44.102,144.482,43.95,144.597,43.93,144.715,43.928,144.799,43.94,144.872,43.982,145.102,44.166,145.343,44.334,145.37,44.327,145.37,44.281,145.352,44.23,145.245,44.076,145.126,43.869,145.101,43.765,145.14,43.663,145.214,43.578,145.273,43.463,145.341,43.303,145.436,43.282,145.488,43.28,145.583,43.328,145.674,43.389,145.751,43.396,145.833,43.386,145.726,43.343,145.624,43.291,145.537,43.193,145.505,43.174,145.405,43.18,145.347,43.177,145.23,43.135,145.127,43.089,145.029,43.032,144.921,43.001,144.807,42.994,144.631,42.947,144.516,42.944,144.302,42.984,144.197,42.974,143.969,42.881,143.762,42.748,143.581,42.599,143.429,42.419,143.369,42.325,143.332,42.22,143.327,42.151,143.314,42.084,143.279,42.038,143.237,42,143.112,42.022,142.906,42.118,142.508,42.258,142.088,42.472,141.851,42.579,141.407,42.547,140.986,42.342,140.948,42.36,140.788,42.5,140.71,42.556,140.617,42.571,140.548,42.57,140.48,42.559,140.385,42.487,140.351,42.435,140.324,42.376,140.315,42.334,140.327,42.293,140.417,42.201,140.528,42.132,140.578,42.119,140.684,42.123,140.734,42.116,140.912,41.978,141.108,41.848,141.151,41.805,141.079,41.76,141,41.737,140.908,41.743,140.816,41.76,140.66,41.816,140.593,41.769,140.489,41.672,140.432,41.567,140.385,41.519,140.27,41.456,140.149,41.423,140.085,41.434,140.037,41.474,140.009,41.521,139.995,41.576,140.021,41.696,140.084,41.803,140.108,41.913,140.057,42.067,140.024,42.1,139.895,42.19,139.835,42.278,139.821,42.388,139.829,42.448,139.86,42.582,139.891,42.649,139.951,42.671,140.015,42.685,140.115,42.733,140.224,42.796,140.329,42.867,140.432,42.954,140.486,43.05,140.397,43.167,140.379,43.237,140.392,43.303,140.487,43.338,140.585,43.312,140.781,43.215,140.819,43.205,140.954,43.201,141.138,43.18,141.245,43.185,141.296,43.2,141.374,43.28,141.412,43.381,141.398,43.513,141.398,43.643,141.447,43.749,141.601,43.919,141.645,44.019,141.661,44.264,141.716,44.371,141.761,44.483,141.782,44.716,141.719,44.941,141.656,45.051,141.583,45.156,141.594,45.256,141.653,45.349,141.654,45.377,141.668,45.401,141.778,45.419,141.829,45.439,141.879,45.483,141.938,45.51,141.981,45.483,142.016,45.438,142.172,45.326,142.416,45.125,142.704,44.819,142.885,44.67,143.075,44.535,143.289,44.397,143.512,44.278,143.655,44.221,143.759,44.132,143.824,44.117]],[[131.175,33.603,131.309,33.573,131.366,33.571,131.419,33.584,131.499,33.624,131.583,33.652,131.643,33.638,131.696,33.603,131.724,33.554,131.711,33.502,131.616,33.392,131.537,33.274,131.717,33.252,131.897,33.255,131.855,33.182,131.848,33.118,131.903,33.088,131.949,33.047,131.937,33.01,131.91,32.974,132.009,32.919,132.002,32.882,131.977,32.844,131.732,32.593,131.66,32.466,131.61,32.325,131.565,32.223,131.531,32.117,131.506,32.002,131.46,31.883,131.475,31.778,131.46,31.671,131.337,31.405,131.25,31.41,131.14,31.442,131.071,31.437,131.035,31.378,131.098,31.256,130.902,31.112,130.686,31.015,130.684,31.059,130.704,31.094,130.736,31.122,130.758,31.156,130.79,31.269,130.774,31.383,130.709,31.526,130.704,31.577,130.749,31.598,130.78,31.604,130.796,31.624,130.797,31.671,130.776,31.706,130.715,31.718,130.655,31.718,130.613,31.665,130.556,31.563,130.528,31.46,130.54,31.403,130.566,31.352,130.645,31.267,130.621,31.218,130.589,31.179,130.311,31.267,130.251,31.273,130.201,31.292,130.147,31.408,130.261,31.437,130.294,31.451,130.307,31.488,130.322,31.601,130.269,31.696,130.224,31.73,130.188,31.769,130.211,31.849,130.196,31.95,130.194,32.091,130.214,32.115,130.319,32.144,130.395,32.219,130.462,32.305,130.56,32.456,130.641,32.619,130.563,32.626,130.498,32.657,130.569,32.734,130.547,32.832,130.44,32.951,130.382,33.093,130.287,33.155,130.238,33.178,130.177,33.145,130.127,33.105,130.173,33.013,130.168,32.932,130.175,32.851,130.222,32.847,130.28,32.867,130.326,32.853,130.354,32.81,130.361,32.756,130.34,32.702,130.298,32.675,130.246,32.677,130.193,32.706,130.152,32.748,130.054,32.771,129.951,32.722,129.853,32.622,129.769,32.571,129.808,32.645,129.827,32.725,129.786,32.782,129.69,32.875,129.668,32.929,129.662,32.995,129.679,33.06,129.778,32.986,129.828,32.893,129.901,32.852,129.992,32.852,129.922,32.988,129.897,33.022,129.799,33.084,129.665,33.187,129.58,33.236,129.61,33.344,129.66,33.365,129.702,33.36,129.844,33.322,129.858,33.375,129.837,33.404,129.826,33.437,129.919,33.483,130.072,33.522,130.103,33.54,130.131,33.578,130.168,33.598,130.275,33.598,130.365,33.634,130.439,33.734,130.457,33.789,130.484,33.835,130.67,33.915,130.716,33.928,130.84,33.918,130.953,33.872,131.009,33.776,131.058,33.673,131.175,33.603]],[[134.357,34.256,134.496,34.215,134.638,34.227,134.635,34.044,134.655,33.983,134.695,33.928,134.675,33.848,134.739,33.821,134.549,33.729,134.377,33.608,134.307,33.527,134.243,33.439,134.206,33.347,134.182,33.247,134.124,33.287,133.959,33.448,133.854,33.493,133.686,33.516,133.632,33.511,133.286,33.36,133.24,33.25,133.146,33.083,133.101,33.028,133.051,33.012,133.016,32.984,132.977,32.842,132.87,32.755,132.804,32.752,132.692,32.776,132.642,32.762,132.709,32.902,132.602,32.92,132.495,32.917,132.493,33.008,132.428,33.059,132.476,33.126,132.477,33.181,132.505,33.211,132.515,33.255,132.511,33.293,132.445,33.305,132.405,33.331,132.413,33.43,132.375,33.434,132.281,33.417,132.086,33.34,132.033,33.34,132.114,33.395,132.288,33.47,132.366,33.512,132.536,33.633,132.643,33.69,132.699,33.791,132.716,33.852,132.752,33.906,132.784,33.992,132.839,34.021,132.935,34.095,132.99,34.088,133.051,33.997,133.134,33.927,133.193,33.933,133.299,33.969,133.35,33.977,133.472,33.973,133.582,34.017,133.627,34.069,133.643,34.135,133.603,34.244,133.656,34.233,133.706,34.237,133.826,34.307,133.948,34.348,134.076,34.358,134.219,34.319,134.357,34.256]],[[141.229,41.373,141.269,41.354,141.455,41.405,141.42,41.251,141.4,41.096,141.414,40.839,141.43,40.723,141.463,40.611,141.542,40.531,141.646,40.474,141.797,40.291,141.878,40.067,141.935,39.958,141.978,39.844,141.991,39.792,141.992,39.74,141.979,39.668,141.993,39.611,141.977,39.429,141.909,39.219,141.901,39.111,141.842,39.09,141.807,39.04,141.776,39.017,141.742,39,141.694,38.995,141.659,38.975,141.645,38.918,141.622,38.865,141.58,38.817,141.546,38.763,141.519,38.632,141.509,38.498,141.467,38.404,141.368,38.38,141.254,38.381,141.108,38.338,141.077,38.313,140.962,38.149,140.929,38.053,140.928,37.95,140.96,37.823,141.003,37.698,141.036,37.467,141.002,37.115,140.968,37.002,140.895,36.926,140.84,36.89,140.792,36.847,140.73,36.732,140.627,36.503,140.619,36.445,140.619,36.386,140.592,36.308,140.574,36.231,140.59,36.142,140.622,36.059,140.76,35.846,140.813,35.783,140.874,35.725,140.639,35.661,140.597,35.632,140.457,35.51,140.413,35.395,140.417,35.267,140.393,35.221,140.355,35.181,140.315,35.155,140.159,35.096,140.059,35.038,139.96,34.947,139.92,34.9,139.844,34.915,139.799,34.957,139.843,35.01,139.83,35.072,139.851,35.232,139.826,35.297,139.906,35.345,139.944,35.423,140.027,35.485,140.086,35.54,140.097,35.585,140.044,35.633,139.988,35.668,139.91,35.668,139.835,35.658,139.786,35.612,139.77,35.55,139.774,35.52,139.768,35.495,139.65,35.409,139.666,35.319,139.7,35.274,139.744,35.252,139.731,35.222,139.675,35.149,139.636,35.142,139.564,35.243,139.474,35.299,139.363,35.298,139.249,35.278,139.163,35.211,139.134,35.155,139.116,35.097,139.122,34.956,139.086,34.839,139.016,34.736,138.983,34.698,138.897,34.628,138.838,34.619,138.795,34.651,138.761,34.699,138.804,34.876,138.803,34.975,138.904,35.025,138.821,35.096,138.72,35.124,138.577,35.086,138.537,35.044,138.51,34.987,138.433,34.915,138.349,34.848,138.253,34.733,138.189,34.596,137.979,34.641,137.864,34.651,137.749,34.647,137.543,34.664,137.318,34.636,137.062,34.583,137.077,34.621,137.288,34.704,137.295,34.728,137.275,34.773,137.223,34.775,137.097,34.759,137.032,34.766,137.006,34.814,136.963,34.835,136.935,34.815,136.944,34.722,136.913,34.709,136.871,34.733,136.885,34.806,136.856,34.913,136.853,34.979,136.897,35.036,136.852,35.06,136.804,35.05,136.744,35.023,136.69,34.984,136.577,34.79,136.533,34.678,136.616,34.589,136.842,34.464,136.88,34.434,136.881,34.38,136.854,34.324,136.792,34.299,136.544,34.258,136.33,34.177,136.268,34.095,136.073,33.778,135.916,33.562,135.695,33.487,135.453,33.553,135.394,33.629,135.347,33.722,135.257,33.806,135.175,33.898,135.128,34.007,135.135,34.183,135.1,34.288,135.132,34.317,135.266,34.381,135.309,34.417,135.385,34.5,135.412,34.547,135.416,34.617,135.355,34.654,135.198,34.653,135.042,34.631,134.93,34.662,134.785,34.747,134.74,34.765,134.584,34.771,134.472,34.755,134.363,34.724,134.247,34.714,134.208,34.698,134.075,34.593,133.968,34.527,133.876,34.495,133.678,34.486,133.579,34.465,133.474,34.43,133.445,34.433,133.336,34.385,133.21,34.344,133.142,34.302,133.019,34.33,132.775,34.255,132.657,34.246,132.534,34.287,132.421,34.353,132.313,34.325,132.238,34.227,132.202,34.032,132.159,33.944,132.146,33.839,132.09,33.855,131.763,34.045,131.741,34.052,131.476,34.019,131.408,34.004,131.323,33.965,131.233,33.948,131.15,33.976,131.072,34.021,130.996,34.007,130.919,33.976,130.889,34.262,130.904,34.3,130.952,34.35,131.004,34.393,131.132,34.407,131.262,34.393,131.354,34.413,131.433,34.47,131.515,34.55,131.608,34.615,131.734,34.667,131.856,34.726,131.963,34.809,132.065,34.9,132.158,34.967,132.26,35.022,132.414,35.156,132.619,35.307,132.698,35.418,132.747,35.449,132.923,35.511,133.157,35.559,133.267,35.557,133.376,35.459,133.435,35.472,133.495,35.497,133.615,35.511,133.739,35.495,133.86,35.495,133.981,35.507,134.214,35.539,134.337,35.578,134.456,35.628,134.882,35.663,135.174,35.747,135.221,35.741,135.265,35.722,135.269,35.66,135.232,35.592,135.268,35.551,135.327,35.526,135.602,35.518,135.68,35.503,135.795,35.55,135.903,35.607,136.016,35.683,136.095,35.768,136.022,35.874,136.006,35.991,136.067,36.117,136.156,36.223,136.262,36.288,136.359,36.362,136.556,36.572,136.698,36.742,136.749,36.951,136.719,37.198,136.843,37.382,136.962,37.414,137.199,37.497,137.323,37.522,137.341,37.485,137.338,37.437,137.152,37.283,137.046,37.22,136.982,37.2,136.924,37.172,136.9,37.118,136.994,37.027,137.019,36.96,137.013,36.895,137.017,36.837,137.124,36.774,137.246,36.753,137.298,36.754,137.343,36.77,137.483,36.925,137.514,36.952,137.913,37.065,138.11,37.151,138.218,37.173,138.32,37.218,138.548,37.392,138.633,37.472,138.709,37.561,138.771,37.663,138.819,37.775,138.885,37.844,139.247,38.009,139.364,38.099,139.401,38.143,139.446,38.269,139.477,38.4,139.521,38.503,139.58,38.599,139.66,38.697,139.749,38.788,139.802,38.882,139.879,39.105,139.912,39.229,139.939,39.273,139.977,39.311,140.011,39.358,140.037,39.411,140.048,39.464,140.065,39.624,140.055,39.749,139.995,39.855,139.945,39.885,139.891,39.887,139.81,39.878,139.742,39.921,139.755,39.959,139.826,39.966,139.874,39.986,139.908,40.022,139.972,40.137,140.011,40.26,140.014,40.315,139.964,40.414,139.924,40.534,139.923,40.598,139.967,40.673,140.029,40.733,140.085,40.747,140.146,40.752,140.201,40.775,140.252,40.809,140.281,40.846,140.326,40.948,140.344,41.006,140.315,41.161,140.344,41.203,140.386,41.23,140.441,41.21,140.498,41.206,140.564,41.212,140.628,41.195,140.64,41.156,140.679,40.893,140.702,40.858,140.749,40.83,140.801,40.834,140.846,40.875,140.876,40.93,140.936,40.941,141.119,40.882,141.183,40.924,141.225,40.988,141.262,41.103,141.244,41.206,141.2,41.244,141.155,41.237,141.115,41.208,141.07,41.193,140.801,41.139,140.802,41.254,140.86,41.425,140.892,41.48,140.937,41.506,141.05,41.476,141.106,41.456,141.229,41.373]],[[124.293,24.516,124.234,24.358,124.186,24.335,124.136,24.348,124.085,24.436,124.12,24.47,124.17,24.452,124.211,24.459,124.302,24.587,124.324,24.566,124.293,24.516]],[[123.889,24.28,123.826,24.266,123.75,24.283,123.681,24.288,123.68,24.318,123.752,24.348,123.754,24.391,123.771,24.414,123.935,24.362,123.928,24.324,123.889,24.28]],[[125.444,24.743,125.359,24.717,125.269,24.733,125.284,24.872,125.315,24.852,125.335,24.805,125.402,24.777,125.444,24.743]],[[128.259,26.653,128.163,26.607,128.127,26.552,128.038,26.534,127.951,26.456,127.867,26.442,127.869,26.381,127.905,26.328,127.849,26.319,127.79,26.255,127.786,26.209,127.806,26.171,127.804,26.153,127.729,26.097,127.653,26.095,127.65,26.154,127.655,26.199,127.727,26.308,127.729,26.434,127.796,26.449,127.82,26.466,127.926,26.556,127.946,26.594,127.891,26.631,127.895,26.675,127.907,26.694,127.994,26.679,128.03,26.647,128.047,26.643,128.098,26.668,128.122,26.711,128.217,26.797,128.255,26.882,128.332,26.812,128.311,26.721,128.259,26.653]],[[128.998,27.721,128.956,27.702,128.9,27.728,128.883,27.842,128.908,27.898,128.952,27.91,128.99,27.811,129.016,27.77,128.998,27.721]],[[129.453,28.209,129.366,28.128,129.275,28.201,129.165,28.25,129.217,28.263,129.248,28.283,129.251,28.314,129.322,28.36,129.465,28.395,129.51,28.398,129.561,28.431,129.577,28.461,129.598,28.476,129.69,28.517,129.715,28.47,129.71,28.432,129.642,28.411,129.575,28.361,129.513,28.299,129.457,28.272,129.439,28.255,129.453,28.209]],[[129.324,28.105,129.331,28.082,129.232,28.101,129.192,28.192,129.257,28.176,129.277,28.145,129.324,28.105]],[[139.841,33.056,139.824,33.045,139.776,33.078,139.769,33.107,139.777,33.125,139.809,33.129,139.874,33.094,139.841,33.056]]]},{"n":"Jamaica","c":"JM","r":4,"ml":4,"lx":-77.32,"ly":18.14,"b":[-78.34,17.715,-76.211,18.522],"p":[[[-77.261,18.457,-77.14,18.421,-77.014,18.403,-76.959,18.402,-76.908,18.39,-76.793,18.304,-76.701,18.257,-76.35,18.152,-76.233,17.97,-76.211,17.914,-76.301,17.88,-76.416,17.868,-76.525,17.866,-76.625,17.901,-76.669,17.928,-76.774,17.94,-76.748,17.965,-76.795,17.976,-76.853,17.974,-76.896,17.904,-76.944,17.849,-77.036,17.854,-77.071,17.901,-77.119,17.88,-77.158,17.845,-77.205,17.715,-77.28,17.78,-77.361,17.834,-77.464,17.856,-77.671,17.86,-77.768,17.877,-77.849,17.988,-77.881,18.019,-77.963,18.048,-78.044,18.174,-78.074,18.191,-78.294,18.218,-78.34,18.287,-78.326,18.35,-78.252,18.426,-78.217,18.448,-78.095,18.445,-77.978,18.468,-77.927,18.501,-77.873,18.522,-77.452,18.467,-77.354,18.466,-77.261,18.457]]]},{"n":"Italy","c":"IT","r":2,"ml":2,"lx":11.08,"ly":44.73,"b":[6.628,36.688,18.486,47.082],"p":[[[7.021,45.926,7.056,45.904,7.129,45.88,7.328,45.912,7.452,45.944,7.539,45.978,7.593,45.972,7.788,45.922,7.852,45.947,7.993,46.016,8.014,46.052,8.125,46.161,8.127,46.188,8.082,46.256,8.096,46.271,8.232,46.341,8.299,46.403,8.371,46.445,8.423,46.446,8.437,46.432,8.443,46.403,8.438,46.283,8.458,46.246,8.565,46.16,8.642,46.111,8.819,46.077,8.827,46.061,8.778,45.996,8.885,45.919,8.904,45.862,8.954,45.83,9.024,45.846,9.047,45.876,9.019,45.928,8.999,45.983,9.003,46.015,9.022,46.051,9.071,46.102,9.203,46.219,9.251,46.287,9.26,46.391,9.26,46.475,9.304,46.496,9.399,46.481,9.428,46.482,9.441,46.431,9.481,46.349,9.529,46.306,9.58,46.296,9.639,46.296,9.788,46.346,9.884,46.368,9.939,46.362,9.972,46.328,10.041,46.238,10.081,46.228,10.128,46.238,10.145,46.254,10.13,46.288,10.11,46.363,10.082,46.421,10.046,46.448,10.038,46.483,10.061,46.547,10.087,46.6,10.138,46.614,10.196,46.621,10.272,46.565,10.363,46.547,10.431,46.55,10.442,46.583,10.438,46.619,10.398,46.665,10.406,46.735,10.453,46.865,10.479,46.855,10.58,46.854,10.689,46.846,10.76,46.793,10.829,46.775,10.927,46.769,10.993,46.777,11.025,46.797,11.063,46.859,11.134,46.936,11.244,46.976,11.433,46.983,11.528,46.997,11.625,46.997,11.699,46.985,11.776,46.986,11.97,47.04,12.169,47.082,12.197,47.075,12.201,47.061,12.166,47.028,12.131,46.985,12.154,46.935,12.268,46.836,12.33,46.76,12.388,46.703,12.479,46.673,12.599,46.654,12.7,46.647,12.806,46.626,13.169,46.573,13.352,46.558,13.49,46.556,13.7,46.52,13.68,46.463,13.637,46.449,13.563,46.415,13.479,46.369,13.4,46.318,13.378,46.262,13.4,46.225,13.421,46.212,13.45,46.224,13.492,46.217,13.545,46.197,13.633,46.177,13.635,46.158,13.617,46.133,13.548,46.089,13.486,46.04,13.48,46.009,13.488,45.987,13.509,45.974,13.601,45.98,13.614,45.962,13.57,45.834,13.583,45.812,13.663,45.792,13.722,45.761,13.831,45.68,13.875,45.615,13.845,45.593,13.776,45.582,13.72,45.588,13.783,45.627,13.628,45.771,13.558,45.771,13.465,45.71,13.206,45.771,13.157,45.747,13.12,45.698,13.03,45.638,12.903,45.611,12.761,45.544,12.612,45.497,12.498,45.462,12.432,45.468,12.536,45.545,12.492,45.546,12.354,45.492,12.274,45.446,12.249,45.369,12.226,45.242,12.286,45.208,12.392,45.04,12.523,44.968,12.498,44.899,12.464,44.845,12.384,44.798,12.319,44.833,12.279,44.832,12.248,44.723,12.305,44.429,12.396,44.224,12.487,44.134,12.691,43.995,12.907,43.921,13.295,43.686,13.508,43.612,13.564,43.571,13.693,43.39,13.805,43.18,13.925,42.852,14.01,42.69,14.183,42.506,14.541,42.244,14.866,42.053,15.169,41.934,15.405,41.913,15.964,41.939,16.062,41.928,16.165,41.896,16.189,41.814,16.151,41.758,16.034,41.701,15.914,41.621,15.9,41.512,16.013,41.435,16.552,41.232,17.103,41.062,17.275,40.975,17.474,40.841,17.955,40.655,18.036,40.565,18.328,40.371,18.461,40.221,18.486,40.105,18.423,39.987,18.393,39.904,18.344,39.821,18.219,39.853,18.078,39.937,17.865,40.28,17.476,40.315,17.396,40.34,17.258,40.399,17.249,40.438,17.215,40.486,17.18,40.503,17.031,40.513,16.928,40.458,16.807,40.326,16.67,40.137,16.53,39.86,16.522,39.748,16.598,39.639,16.824,39.578,16.999,39.482,17.115,39.381,17.123,39.137,17.175,38.998,17.099,38.919,16.951,38.94,16.755,38.89,16.617,38.8,16.559,38.715,16.574,38.494,16.546,38.409,16.282,38.25,16.144,38.086,16.11,38.019,16.057,37.942,15.725,37.939,15.646,38.034,15.643,38.175,15.7,38.262,15.822,38.303,15.905,38.483,15.879,38.614,15.927,38.672,15.972,38.713,16.066,38.736,16.197,38.759,16.21,38.941,16.107,39.024,16.071,39.139,16.024,39.354,15.854,39.627,15.764,39.87,15.693,39.99,15.585,40.053,15.391,40.052,15.295,40.07,14.951,40.239,14.927,40.265,14.929,40.31,14.986,40.377,14.948,40.469,14.907,40.556,14.84,40.63,14.766,40.668,14.611,40.645,14.557,40.626,14.459,40.633,14.383,40.6,14.34,40.599,14.461,40.729,14.428,40.759,14.309,40.813,14.147,40.821,14.102,40.827,14.076,40.794,14.044,40.812,14.048,40.87,13.86,41.13,13.733,41.236,13.67,41.254,13.555,41.232,13.362,41.279,13.247,41.289,13.183,41.278,13.089,41.244,13.041,41.266,13.024,41.301,12.849,41.409,12.631,41.47,12.206,41.813,12.075,41.941,11.807,42.082,11.637,42.288,11.498,42.363,11.296,42.423,11.25,42.416,11.189,42.393,11.141,42.39,11.103,42.417,11.142,42.444,11.185,42.457,11.168,42.535,10.938,42.739,10.803,42.804,10.765,42.845,10.737,42.9,10.708,42.936,10.645,42.957,10.59,42.954,10.515,42.968,10.517,43.065,10.532,43.14,10.521,43.204,10.448,43.371,10.321,43.513,10.246,43.852,10.188,43.948,10.048,44.02,9.731,44.101,9.289,44.319,9.196,44.323,8.93,44.408,8.766,44.422,8.552,44.346,8.292,44.137,8.082,43.919,8.005,43.877,7.733,43.803,7.493,43.767,7.491,43.823,7.482,43.865,7.523,43.911,7.59,43.965,7.651,44.034,7.677,44.083,7.665,44.116,7.637,44.165,7.599,44.168,7.371,44.127,7.319,44.138,7.149,44.202,6.967,44.28,6.9,44.336,6.875,44.392,6.894,44.428,6.879,44.463,6.843,44.511,6.875,44.565,6.932,44.632,6.96,44.677,7.008,44.689,7.031,44.717,6.993,44.827,6.973,44.845,6.94,44.859,6.889,44.86,6.801,44.883,6.738,44.921,6.725,44.973,6.691,45.023,6.635,45.068,6.628,45.118,6.692,45.144,6.78,45.145,6.842,45.136,6.981,45.216,7.032,45.223,7.078,45.24,7.117,45.349,7.146,45.382,7.153,45.401,7.126,45.424,7.014,45.5,6.962,45.581,6.881,45.67,6.806,45.71,6.791,45.741,6.789,45.78,6.804,45.815,6.941,45.868,7.021,45.926],[12.439,41.898,12.438,41.906,12.431,41.905,12.428,41.901,12.431,41.898,12.439,41.898],[12.485,43.901,12.515,43.953,12.504,43.99,12.441,43.982,12.397,43.935,12.426,43.894,12.485,43.901]],[[10.395,42.858,10.428,42.819,10.432,42.797,10.41,42.771,10.419,42.713,10.336,42.761,10.209,42.737,10.131,42.742,10.11,42.785,10.128,42.81,10.248,42.816,10.286,42.828,10.359,42.822,10.395,42.858]],[[13.938,40.706,13.894,40.697,13.868,40.709,13.854,40.724,13.871,40.762,13.962,40.739,13.961,40.718,13.938,40.706]],[[12.051,36.757,12.003,36.746,11.941,36.78,11.936,36.829,11.948,36.843,12.024,36.821,12.048,36.776,12.051,36.757]],[[15.577,38.22,15.509,38.107,15.476,38.063,15.234,37.785,15.207,37.721,15.19,37.651,15.165,37.59,15.131,37.532,15.1,37.459,15.106,37.375,15.117,37.335,15.146,37.308,15.194,37.283,15.23,37.244,15.174,37.209,15.236,37.139,15.289,37.097,15.296,37.055,15.295,37.013,15.185,36.935,15.142,36.892,15.116,36.839,15.104,36.785,15.116,36.736,15.113,36.688,15.002,36.694,14.89,36.724,14.776,36.71,14.614,36.767,14.555,36.777,14.502,36.799,14.367,36.973,14.259,37.046,14.143,37.104,14.024,37.107,13.905,37.101,13.801,37.136,13.587,37.254,13.361,37.349,13.265,37.41,13.221,37.452,13.17,37.479,13.04,37.507,12.924,37.571,12.871,37.575,12.757,37.567,12.699,37.572,12.64,37.594,12.527,37.67,12.454,37.774,12.436,37.82,12.487,37.939,12.548,38.053,12.602,38.085,12.664,38.108,12.702,38.142,12.734,38.183,12.851,38.064,12.903,38.035,12.955,38.041,13.049,38.084,13.057,38.131,13.16,38.19,13.291,38.191,13.352,38.181,13.383,38.127,13.433,38.11,13.491,38.103,13.682,38.001,13.735,37.984,13.789,37.981,13.937,38.024,14.05,38.041,14.288,38.017,14.416,38.043,14.506,38.046,14.637,38.085,14.737,38.151,14.79,38.167,14.846,38.172,14.982,38.168,15.119,38.153,15.176,38.168,15.224,38.211,15.28,38.23,15.341,38.217,15.499,38.291,15.568,38.296,15.635,38.268,15.577,38.22]],[[9.632,40.882,9.682,40.818,9.794,40.556,9.805,40.5,9.783,40.442,9.754,40.4,9.643,40.268,9.659,40.159,9.701,40.092,9.707,40.017,9.686,39.924,9.617,39.354,9.584,39.254,9.563,39.166,9.486,39.14,9.388,39.168,9.264,39.217,9.207,39.214,9.149,39.197,9.102,39.211,9.056,39.239,9.023,39.043,8.967,38.964,8.881,38.913,8.801,38.91,8.719,38.927,8.649,38.927,8.595,38.964,8.553,39.03,8.486,39.11,8.418,39.206,8.411,39.292,8.399,39.482,8.419,39.523,8.447,39.563,8.461,39.648,8.451,39.722,8.471,39.748,8.511,39.722,8.541,39.732,8.539,39.77,8.548,39.839,8.496,39.897,8.408,39.917,8.399,39.978,8.409,40.038,8.455,40.078,8.471,40.131,8.471,40.293,8.409,40.352,8.385,40.443,8.353,40.501,8.296,40.559,8.23,40.606,8.19,40.652,8.181,40.771,8.204,40.871,8.224,40.913,8.245,40.907,8.31,40.858,8.363,40.846,8.468,40.834,8.572,40.85,8.699,40.895,8.821,40.95,8.998,41.11,9.107,41.143,9.163,41.185,9.182,41.242,9.228,41.257,9.283,41.202,9.351,41.196,9.455,41.15,9.5,41.106,9.539,41.054,9.576,41.031,9.615,41.017,9.621,41.005,9.59,40.992,9.554,40.932,9.574,40.915,9.632,40.882]],[[8.479,39.068,8.421,38.969,8.361,39.039,8.359,39.099,8.367,39.116,8.441,39.091,8.479,39.068]],[[8.286,41.04,8.253,40.994,8.206,40.997,8.224,41.031,8.267,41.099,8.32,41.122,8.344,41.102,8.319,41.063,8.286,41.04]]]},{"n":"Israel","c":"IL","r":4,"ml":3,"lx":34.85,"ly":30.91,"b":[34.245,29.477,35.888,33.416],"p":[[[35.841,33.416,35.816,33.362,35.793,33.334,35.785,33.291,35.8,33.256,35.809,33.209,35.827,33.195,35.833,33.164,35.817,33.133,35.825,33.108,35.845,33.085,35.859,32.989,35.888,32.944,35.842,32.876,35.833,32.82,35.787,32.735,35.734,32.729,35.611,32.682,35.595,32.668,35.573,32.641,35.569,32.62,35.551,32.396,35.484,32.402,35.403,32.451,35.387,32.493,35.362,32.507,35.304,32.513,35.193,32.534,35.065,32.46,35.011,32.338,35,32.281,34.956,32.161,34.971,32.087,34.979,31.992,34.99,31.913,34.978,31.866,34.954,31.841,34.961,31.823,34.983,31.817,35.053,31.838,35.127,31.817,35.198,31.776,35.204,31.75,35.153,31.734,35.035,31.673,34.951,31.602,34.929,31.537,34.873,31.397,34.88,31.368,34.908,31.351,35.101,31.366,35.277,31.423,35.409,31.483,35.451,31.479,35.423,31.325,35.424,31.325,35.401,31.231,35.41,31.214,35.439,31.132,35.383,30.982,35.32,30.86,35.298,30.802,35.237,30.673,35.174,30.524,35.141,30.421,35.148,30.384,35.133,30.195,35.142,30.142,35.068,29.978,35.053,29.897,35.024,29.787,34.973,29.555,34.904,29.477,34.87,29.564,34.791,29.812,34.735,29.982,34.659,30.191,34.53,30.446,34.518,30.507,34.49,30.596,34.401,30.828,34.329,30.995,34.245,31.208,34.348,31.293,34.35,31.363,34.526,31.526,34.524,31.542,34.477,31.585,34.484,31.592,34.678,31.896,34.804,32.196,34.922,32.614,35.006,32.827,35.077,32.967,35.109,33.084,35.223,33.092,35.309,33.08,35.411,33.076,35.493,33.119,35.533,33.25,35.579,33.271,35.603,33.241,35.627,33.275,35.734,33.333,35.788,33.37,35.841,33.416]]]},{"n":"Palestine","c":"PS","r":5,"ml":4.5,"lx":35.29,"ly":32.05,"b":[34.198,31.208,35.572,32.534],"p":[[[34.477,31.585,34.524,31.542,34.526,31.526,34.35,31.363,34.348,31.293,34.245,31.208,34.213,31.292,34.198,31.323,34.387,31.484,34.477,31.585]],[[35.551,32.396,35.572,32.238,35.535,32.103,35.531,31.985,35.559,31.766,35.499,31.672,35.465,31.562,35.451,31.479,35.409,31.483,35.277,31.423,35.101,31.366,34.908,31.351,34.88,31.368,34.873,31.397,34.929,31.537,34.951,31.602,35.035,31.673,35.153,31.734,35.204,31.75,35.198,31.776,35.127,31.817,35.053,31.838,34.983,31.817,34.961,31.823,34.954,31.841,34.978,31.866,34.99,31.913,34.979,31.992,34.971,32.087,34.956,32.161,35,32.281,35.011,32.338,35.065,32.46,35.193,32.534,35.304,32.513,35.362,32.507,35.387,32.493,35.403,32.451,35.484,32.402,35.551,32.396]]]},{"n":"Ireland","c":"IE","r":3,"ml":3,"lx":-7.8,"ly":53.08,"b":[-10.39,51.474,-6.027,55.366],"p":[[[-9.948,53.913,-9.952,53.885,-10.027,53.921,-10.062,53.96,-10.266,53.978,-10.181,54.017,-10.14,54.005,-9.996,54.004,-9.956,53.987,-9.948,53.913]],[[-7.219,55.092,-7.377,55.028,-7.401,55.003,-7.446,54.905,-7.451,54.877,-7.502,54.825,-7.55,54.768,-7.606,54.746,-7.69,54.728,-7.737,54.71,-7.797,54.719,-7.873,54.718,-7.911,54.698,-7.909,54.683,-7.886,54.666,-7.82,54.64,-7.746,54.616,-7.754,54.595,-7.794,54.571,-8.044,54.512,-8.119,54.477,-8.145,54.454,-8.118,54.414,-7.918,54.297,-7.884,54.284,-7.855,54.215,-7.679,54.187,-7.607,54.144,-7.544,54.134,-7.409,54.137,-7.355,54.121,-7.325,54.133,-7.307,54.156,-7.193,54.214,-7.155,54.24,-7.178,54.275,-7.203,54.302,-7.133,54.355,-7.05,54.408,-7.008,54.407,-6.936,54.374,-6.877,54.329,-6.869,54.294,-6.858,54.269,-6.803,54.214,-6.767,54.196,-6.67,54.185,-6.647,54.163,-6.664,54.085,-6.65,54.059,-6.548,54.057,-6.44,54.064,-6.403,54.061,-6.364,54.077,-6.304,54.095,-6.218,54.089,-6.176,54.054,-6.157,54.017,-6.231,54.004,-6.308,54.011,-6.345,53.987,-6.348,53.941,-6.322,53.882,-6.27,53.84,-6.229,53.746,-6.195,53.641,-6.142,53.578,-6.131,53.499,-6.139,53.46,-6.129,53.391,-6.152,53.366,-6.135,53.301,-6.072,53.166,-6.045,53.091,-6.027,52.927,-6.071,52.866,-6.131,52.807,-6.169,52.738,-6.199,52.663,-6.217,52.543,-6.345,52.402,-6.4,52.367,-6.463,52.345,-6.325,52.247,-6.438,52.203,-6.561,52.189,-6.697,52.214,-6.782,52.21,-6.86,52.179,-6.89,52.159,-6.915,52.169,-6.966,52.25,-7.003,52.166,-7.082,52.139,-7.216,52.145,-7.441,52.123,-7.527,52.099,-7.563,52.062,-7.59,52.019,-7.625,51.993,-7.665,51.98,-7.838,51.948,-7.872,51.935,-7.952,51.866,-8.058,51.826,-8.145,51.814,-8.222,51.854,-8.254,51.878,-8.29,51.891,-8.409,51.889,-8.372,51.876,-8.347,51.848,-8.336,51.793,-8.349,51.739,-8.408,51.712,-8.478,51.707,-8.588,51.651,-8.734,51.636,-8.813,51.585,-9.296,51.498,-9.324,51.497,-9.391,51.519,-9.463,51.529,-9.535,51.522,-9.737,51.474,-9.835,51.483,-9.71,51.604,-9.542,51.664,-9.525,51.681,-9.58,51.689,-9.899,51.647,-10.01,51.611,-10.121,51.601,-10.069,51.656,-9.926,51.731,-9.85,51.766,-9.803,51.78,-9.75,51.824,-9.599,51.874,-10.084,51.771,-10.212,51.784,-10.242,51.812,-10.341,51.799,-10.379,51.869,-10.232,51.975,-10.146,52.02,-10.044,52.045,-9.946,52.08,-9.91,52.123,-9.956,52.137,-10.25,52.126,-10.39,52.135,-10.383,52.169,-10.357,52.207,-10.211,52.272,-10.132,52.282,-10.062,52.276,-9.993,52.259,-9.937,52.238,-9.772,52.25,-9.841,52.291,-9.853,52.375,-9.906,52.404,-9.838,52.443,-9.761,52.466,-9.632,52.547,-9.586,52.559,-9.331,52.579,-9.056,52.621,-8.783,52.68,-8.923,52.712,-8.99,52.755,-9.098,52.668,-9.175,52.635,-9.394,52.617,-9.463,52.627,-9.561,52.654,-9.591,52.644,-9.62,52.623,-9.764,52.58,-9.917,52.57,-9.74,52.648,-9.515,52.781,-9.465,52.823,-9.394,52.896,-9.416,52.929,-9.462,52.947,-9.299,53.098,-9.242,53.125,-9.138,53.129,-9.061,53.153,-9.027,53.153,-8.997,53.162,-8.93,53.207,-9.034,53.236,-9.14,53.25,-9.471,53.235,-9.514,53.238,-9.555,53.252,-9.582,53.272,-9.602,53.323,-9.626,53.334,-9.701,53.334,-9.774,53.319,-9.825,53.32,-9.876,53.343,-9.795,53.395,-9.899,53.407,-10.004,53.397,-10.091,53.413,-10.094,53.446,-10.054,53.478,-10.106,53.509,-10.117,53.549,-10.062,53.568,-10.001,53.561,-9.878,53.59,-9.721,53.604,-9.856,53.633,-9.91,53.658,-9.912,53.695,-9.902,53.727,-9.745,53.781,-9.578,53.805,-9.591,53.841,-9.579,53.88,-9.748,53.891,-9.914,53.864,-9.896,53.938,-9.856,54.004,-9.848,54.048,-9.856,54.095,-9.934,54.075,-9.944,54.142,-9.977,54.187,-10.093,54.156,-10.09,54.216,-10.056,54.258,-9.996,54.276,-9.936,54.268,-9.825,54.269,-9.717,54.3,-9.562,54.309,-9.316,54.299,-9.146,54.21,-9.102,54.226,-9.034,54.282,-9.002,54.288,-8.747,54.263,-8.588,54.231,-8.546,54.241,-8.568,54.304,-8.623,54.347,-8.554,54.404,-8.471,54.442,-8.415,54.461,-8.287,54.485,-8.23,54.507,-8.193,54.58,-8.133,54.641,-8.457,54.609,-8.764,54.681,-8.715,54.732,-8.65,54.761,-8.538,54.783,-8.528,54.809,-8.471,54.832,-8.377,54.889,-8.412,54.965,-8.393,55.02,-8.326,55.056,-8.305,55.108,-8.275,55.146,-8.138,55.16,-8.006,55.195,-7.959,55.192,-7.803,55.2,-7.751,55.186,-7.763,55.248,-7.667,55.256,-7.63,55.244,-7.613,55.2,-7.57,55.171,-7.557,55.122,-7.586,55.084,-7.634,55.055,-7.59,55.025,-7.659,54.971,-7.584,54.994,-7.478,55.047,-7.484,55.09,-7.502,55.145,-7.531,55.194,-7.518,55.248,-7.458,55.282,-7.302,55.299,-7.366,55.36,-7.309,55.366,-7.247,55.353,-7.155,55.305,-7.06,55.268,-6.962,55.238,-7.056,55.178,-7.173,55.137,-7.219,55.092]]]},{"n":"Iraq","c":"IQ","r":3,"ml":3,"lx":43.26,"ly":33.09,"b":[38.774,29.064,48.546,37.372],"p":[[[42.359,37.109,42.456,37.129,42.635,37.249,42.741,37.362,42.775,37.372,42.869,37.335,42.937,37.325,43.092,37.367,43.185,37.345,43.263,37.317,43.307,37.315,43.516,37.245,43.568,37.236,43.676,37.227,43.836,37.224,43.94,37.269,44.013,37.314,44.065,37.312,44.114,37.302,44.156,37.283,44.192,37.25,44.208,37.203,44.202,37.052,44.217,37.012,44.246,36.983,44.282,36.978,44.326,37.011,44.402,37.058,44.496,37.111,44.566,37.158,44.606,37.176,44.669,37.174,44.731,37.165,44.765,37.142,44.765,37.135,44.798,37.064,44.881,36.799,44.928,36.766,44.981,36.738,45.019,36.698,45.034,36.659,45.029,36.598,45.031,36.526,45.053,36.472,45.084,36.43,45.112,36.409,45.155,36.407,45.207,36.397,45.241,36.356,45.351,36.055,45.362,36.015,45.408,36.003,45.484,36.009,45.562,35.977,45.645,35.928,45.723,35.837,45.776,35.822,45.941,35.835,46.167,35.821,46.273,35.773,46.263,35.744,46.181,35.711,46.037,35.673,45.995,35.608,45.971,35.524,45.975,35.477,46.011,35.425,46.112,35.322,46.118,35.284,46.136,35.232,46.155,35.197,46.134,35.128,46.042,35.08,45.921,35.029,45.678,34.798,45.66,34.749,45.662,34.613,45.638,34.574,45.561,34.575,45.501,34.582,45.498,34.534,45.459,34.47,45.438,34.415,45.527,34.285,45.543,34.216,45.529,34.153,45.446,34.044,45.397,33.971,45.409,33.954,45.473,33.925,45.674,33.687,45.738,33.603,45.823,33.625,45.854,33.623,45.879,33.61,45.895,33.582,45.895,33.546,45.873,33.492,45.981,33.47,46.02,33.416,46.146,33.23,46.141,33.174,46.081,33.087,46.08,33.028,46.093,32.976,46.113,32.958,46.298,32.95,46.377,32.929,46.57,32.834,46.789,32.688,46.969,32.568,47.121,32.467,47.285,32.474,47.33,32.456,47.371,32.424,47.418,32.34,47.512,32.151,47.592,32.088,47.715,31.936,47.83,31.794,47.754,31.601,47.679,31.401,47.679,31.142,47.679,31.002,47.836,30.996,48.011,30.99,48.012,30.824,48.013,30.656,48.015,30.466,48.066,30.458,48.148,30.417,48.182,30.355,48.226,30.321,48.279,30.316,48.331,30.285,48.383,30.23,48.401,30.188,48.388,30.16,48.399,30.11,48.435,30.038,48.479,30.004,48.546,29.962,48.454,29.938,48.355,29.957,48.142,30.041,48.073,30.043,47.983,30.011,47.979,29.983,47.754,30.077,47.673,30.096,47.644,30.097,47.515,30.096,47.331,30.08,47.223,30.042,47.148,30.001,47.114,29.961,47.102,29.94,47.044,29.823,46.976,29.673,46.906,29.538,46.769,29.347,46.694,29.26,46.531,29.096,46.356,29.064,45.95,29.096,45.499,29.132,45.05,29.167,44.717,29.194,44.691,29.202,44.361,29.435,44.1,29.619,43.774,29.849,43.441,30.084,43.103,30.322,42.858,30.495,42.56,30.718,42.289,30.92,42.074,31.08,41.8,31.22,41.585,31.33,41.272,31.489,41.022,31.616,40.808,31.725,40.479,31.893,40.369,31.939,40.028,31.995,39.704,32.043,39.369,32.092,39.145,32.125,39.293,32.244,39.247,32.351,39.14,32.331,39.041,32.306,38.982,32.473,39.058,32.493,38.987,32.711,38.915,32.935,38.845,33.151,38.774,33.372,39.057,33.514,39.268,33.62,39.564,33.768,39.85,33.911,40.122,34.048,40.421,34.198,40.689,34.332,40.935,34.387,40.987,34.429,41.099,34.612,41.195,34.769,41.199,34.805,41.2,35.027,41.216,35.288,41.248,35.427,41.303,35.551,41.354,35.64,41.359,35.725,41.353,35.81,41.3,35.939,41.246,36.073,41.252,36.203,41.262,36.272,41.296,36.383,41.354,36.464,41.417,36.515,41.65,36.566,41.789,36.597,41.974,36.741,42.084,36.826,42.237,36.961,42.35,37.061,42.359,37.095,42.359,37.109]]]},{"n":"Iran","c":"IR","r":2,"ml":2.5,"lx":54.93,"ly":32.17,"b":[44.023,25.102,63.305,39.769],"p":[[[56.188,26.921,56.095,26.801,55.954,26.701,55.894,26.732,55.848,26.731,55.747,26.692,55.675,26.686,55.543,26.618,55.424,26.583,55.34,26.586,55.312,26.593,55.295,26.639,55.296,26.658,55.347,26.648,55.532,26.71,55.763,26.812,55.785,26.857,55.747,26.931,55.758,26.948,55.907,26.91,56.074,26.983,56.214,27.003,56.279,26.952,56.188,26.921]],[[53.914,37.344,54.192,37.332,54.3,37.354,54.459,37.408,54.579,37.44,54.64,37.445,54.699,37.47,54.745,37.502,54.849,37.723,54.9,37.778,55.076,37.902,55.225,37.981,55.381,38.051,55.578,38.1,55.841,38.095,56.05,38.078,56.171,38.078,56.229,38.073,56.272,38.08,56.297,38.095,56.324,38.191,56.367,38.223,56.441,38.249,56.544,38.25,56.67,38.257,56.775,38.25,56.907,38.213,57.079,38.21,57.194,38.216,57.26,38.18,57.308,38.13,57.331,38.089,57.337,38.033,57.336,37.99,57.354,37.973,57.424,37.948,57.521,37.928,57.711,37.905,57.888,37.861,57.981,37.83,58.109,37.783,58.262,37.666,58.318,37.647,58.387,37.635,58.436,37.639,58.55,37.688,58.65,37.652,58.701,37.656,58.815,37.683,58.937,37.65,59.241,37.521,59.274,37.524,59.302,37.511,59.327,37.481,59.345,37.445,59.367,37.334,59.455,37.253,59.562,37.179,59.687,37.138,59.949,37.042,60.063,36.963,60.178,36.829,60.321,36.654,60.341,36.638,60.708,36.643,61.12,36.643,61.17,36.572,61.16,36.433,61.175,36.29,61.212,36.191,61.212,36.099,61.183,36.053,61.159,36,61.153,35.977,61.206,35.944,61.252,35.868,61.259,35.762,61.236,35.706,61.239,35.659,61.262,35.62,61.282,35.553,61.279,35.514,61.246,35.474,61.226,35.424,61.199,35.362,61.189,35.312,61.14,35.289,61.1,35.272,61.107,35.209,61.126,35.157,61.15,35.094,61.123,35.051,61.107,35.001,61.07,34.922,61.08,34.856,61.04,34.799,60.991,34.75,60.958,34.71,60.951,34.654,60.915,34.634,60.845,34.588,60.802,34.555,60.739,34.545,60.726,34.518,60.736,34.492,60.763,34.475,60.804,34.418,60.889,34.319,60.643,34.307,60.57,34.22,60.486,34.095,60.527,33.842,60.486,33.712,60.511,33.639,60.574,33.588,60.655,33.56,60.806,33.559,60.907,33.539,60.917,33.505,60.859,33.456,60.767,33.364,60.718,33.324,60.561,33.138,60.562,33.059,60.577,32.995,60.645,32.794,60.71,32.6,60.829,32.249,60.827,32.168,60.79,31.987,60.788,31.877,60.804,31.734,60.792,31.661,60.821,31.495,60.854,31.483,61.111,31.451,61.346,31.422,61.66,31.382,61.755,31.285,61.814,31.073,61.811,30.913,61.784,30.832,61.559,30.599,61.332,30.364,61.104,30.128,60.843,29.859,61.034,29.663,61.152,29.543,61.318,29.373,61.339,29.332,61.338,29.265,61.509,29.006,61.569,28.871,61.623,28.792,61.758,28.668,61.89,28.547,62.033,28.491,62.131,28.479,62.353,28.415,62.434,28.364,62.565,28.235,62.718,28.253,62.749,28.253,62.758,28.244,62.763,28.202,62.74,28.002,62.782,27.801,62.812,27.497,62.801,27.445,62.764,27.357,62.763,27.3,62.753,27.266,62.763,27.25,62.812,27.229,62.915,27.218,63.167,27.252,63.196,27.244,63.256,27.208,63.302,27.151,63.305,27.125,63.242,27.078,63.231,26.998,63.25,26.879,63.242,26.865,63.186,26.838,63.168,26.666,63.158,26.65,63.093,26.632,62.787,26.644,62.752,26.639,62.636,26.594,62.439,26.561,62.385,26.543,62.312,26.491,62.26,26.427,62.25,26.369,62.239,26.357,62.126,26.369,62.089,26.318,61.87,26.242,61.842,26.226,61.81,26.165,61.781,25.996,61.754,25.843,61.738,25.821,61.669,25.769,61.662,25.751,61.671,25.692,61.64,25.585,61.615,25.286,61.588,25.202,61.533,25.196,61.49,25.154,61.412,25.102,61.243,25.142,61.109,25.184,60.664,25.282,60.615,25.33,60.588,25.414,60.511,25.437,60.4,25.312,60.025,25.384,59.897,25.362,59.818,25.401,59.616,25.403,59.456,25.481,59.227,25.428,59.046,25.417,58.798,25.555,58.531,25.592,58.314,25.581,58.203,25.592,58.022,25.641,57.937,25.692,57.796,25.653,57.733,25.725,57.335,25.792,57.261,25.919,57.206,26.037,57.201,26.159,57.104,26.371,57.072,26.68,57.036,26.801,56.982,26.905,56.91,26.995,56.813,27.09,56.728,27.128,56.356,27.2,56.284,27.191,56.118,27.143,55.941,27.038,55.65,26.978,55.592,26.932,55.519,26.83,55.424,26.771,55.294,26.786,55.155,26.725,54.896,26.557,54.759,26.505,54.645,26.509,54.522,26.589,54.247,26.697,54.069,26.732,53.823,26.708,53.706,26.726,53.507,26.852,53.455,26.943,53.342,27.004,52.983,27.142,52.692,27.323,52.638,27.392,52.603,27.493,52.476,27.617,52.192,27.717,52.031,27.824,51.842,27.848,51.666,27.845,51.589,27.864,51.519,27.91,51.279,28.131,51.276,28.219,51.128,28.435,51.094,28.512,51.062,28.726,51.021,28.782,50.867,28.87,50.843,28.928,50.876,29.004,50.876,29.063,50.796,29.117,50.675,29.147,50.646,29.212,50.668,29.34,50.65,29.42,50.544,29.548,50.387,29.679,50.23,29.873,50.169,29.921,50.129,30.048,50.072,30.199,49.983,30.209,49.555,30.029,49.43,30.13,49.054,30.307,49.028,30.333,49.002,30.374,49.049,30.397,49.096,30.407,49.19,30.375,49.247,30.413,49.225,30.472,49.13,30.509,49.002,30.507,49.037,30.45,48.917,30.397,48.891,30.328,48.909,30.241,48.919,30.121,48.87,30.062,48.832,30.035,48.671,30.028,48.596,29.975,48.546,29.962,48.479,30.004,48.435,30.038,48.399,30.11,48.388,30.16,48.401,30.188,48.383,30.23,48.331,30.285,48.279,30.316,48.226,30.321,48.182,30.355,48.148,30.417,48.066,30.458,48.015,30.466,48.013,30.656,48.012,30.824,48.011,30.99,47.836,30.996,47.679,31.002,47.679,31.142,47.679,31.401,47.754,31.601,47.83,31.794,47.715,31.936,47.592,32.088,47.512,32.151,47.418,32.34,47.371,32.424,47.33,32.456,47.285,32.474,47.121,32.467,46.969,32.568,46.789,32.688,46.57,32.834,46.377,32.929,46.298,32.95,46.113,32.958,46.093,32.976,46.08,33.028,46.081,33.087,46.141,33.174,46.146,33.23,46.02,33.416,45.981,33.47,45.873,33.492,45.895,33.546,45.895,33.582,45.879,33.61,45.854,33.623,45.823,33.625,45.738,33.603,45.674,33.687,45.473,33.925,45.409,33.954,45.397,33.971,45.446,34.044,45.529,34.153,45.543,34.216,45.527,34.285,45.438,34.415,45.459,34.47,45.498,34.534,45.501,34.582,45.561,34.575,45.638,34.574,45.662,34.613,45.66,34.749,45.678,34.798,45.921,35.029,46.042,35.08,46.134,35.128,46.155,35.197,46.136,35.232,46.118,35.284,46.112,35.322,46.011,35.425,45.975,35.477,45.971,35.524,45.995,35.608,46.037,35.673,46.181,35.711,46.263,35.744,46.273,35.773,46.167,35.821,45.941,35.835,45.776,35.822,45.723,35.837,45.645,35.928,45.562,35.977,45.484,36.009,45.408,36.003,45.362,36.015,45.351,36.055,45.241,36.356,45.207,36.397,45.155,36.407,45.112,36.409,45.084,36.43,45.053,36.472,45.031,36.526,45.029,36.598,45.034,36.659,45.019,36.698,44.981,36.738,44.928,36.766,44.881,36.799,44.798,37.064,44.765,37.135,44.765,37.142,44.767,37.156,44.758,37.217,44.797,37.27,44.794,37.29,44.715,37.357,44.604,37.424,44.574,37.435,44.573,37.506,44.577,37.56,44.567,37.609,44.546,37.636,44.545,37.658,44.59,37.71,44.561,37.745,44.398,37.829,44.336,37.872,44.223,37.88,44.211,37.908,44.229,37.967,44.268,38.039,44.329,38.109,44.349,38.146,44.373,38.21,44.381,38.255,44.45,38.318,44.45,38.334,44.431,38.357,44.376,38.37,44.32,38.375,44.299,38.386,44.291,38.42,44.298,38.558,44.28,38.641,44.257,38.701,44.272,38.836,44.232,38.863,44.171,38.934,44.145,38.994,44.159,39.017,44.172,39.056,44.181,39.108,44.178,39.145,44.121,39.181,44.079,39.218,44.074,39.26,44.058,39.311,44.034,39.351,44.023,39.377,44.044,39.393,44.124,39.405,44.24,39.397,44.335,39.396,44.389,39.422,44.456,39.667,44.517,39.731,44.587,39.769,44.725,39.682,44.782,39.651,44.817,39.65,44.838,39.629,45,39.424,45.072,39.363,45.113,39.312,45.141,39.254,45.191,39.216,45.256,39.195,45.336,39.139,45.389,39.096,45.48,39.006,45.575,38.973,45.922,38.908,46.114,38.878,46.17,38.869,46.318,38.913,46.491,38.907,46.555,38.904,46.783,39.087,46.853,39.148,46.989,39.18,47.065,39.253,47.188,39.341,47.338,39.424,47.476,39.498,47.582,39.543,47.773,39.649,47.892,39.685,47.996,39.684,48.151,39.561,48.282,39.448,48.322,39.399,48.257,39.355,48.136,39.312,48.113,39.281,48.104,39.241,48.109,39.203,48.125,39.172,48.274,39.099,48.291,39.059,48.292,39.019,48.275,38.994,48.242,38.979,48.139,38.959,48.05,38.935,48.019,38.912,47.993,38.884,47.996,38.854,48.023,38.819,48.205,38.724,48.225,38.689,48.261,38.642,48.306,38.613,48.381,38.606,48.417,38.586,48.593,38.411,48.636,38.399,48.84,38.437,48.869,38.435,48.871,38.393,48.901,38.144,48.925,38.015,48.96,37.89,49.015,37.776,49.081,37.668,49.171,37.601,49.372,37.52,49.47,37.497,49.727,37.481,49.981,37.445,50.13,37.407,50.176,37.381,50.214,37.34,50.338,37.149,50.533,37.014,50.927,36.81,51.119,36.743,51.762,36.615,52.19,36.622,53.374,36.869,53.768,36.93,53.915,36.93,53.827,36.881,53.679,36.853,53.769,36.818,53.906,36.813,53.97,36.818,54.016,36.85,54.024,36.901,54.017,36.952,53.952,37.182,53.914,37.344]]]},{"n":"Indonesia","c":"ID","r":2,"ml":1.7,"lx":101.89,"ly":-0.95,"b":[95.207,-10.91,140.976,5.907],"p":[[[97.482,1.465,97.698,1.184,97.786,1.146,97.903,1.018,97.932,0.974,97.902,0.884,97.876,0.628,97.82,0.564,97.684,0.596,97.683,0.641,97.604,0.834,97.461,0.941,97.405,0.947,97.369,1.057,97.297,1.187,97.079,1.425,97.244,1.424,97.324,1.482,97.343,1.528,97.356,1.54,97.482,1.465]],[[99.164,-1.778,99.072,-1.783,98.874,-1.664,98.828,-1.61,98.816,-1.538,98.627,-1.261,98.602,-1.198,98.676,-0.971,98.869,-0.916,98.933,-0.954,98.955,-1.056,99.065,-1.241,99.101,-1.34,99.129,-1.384,99.14,-1.418,99.131,-1.442,99.21,-1.559,99.267,-1.628,99.271,-1.738,99.164,-1.778]],[[116.641,-8.614,116.514,-8.821,116.559,-8.854,116.587,-8.886,116.377,-8.929,116.29,-8.906,116.239,-8.912,116.027,-8.873,115.875,-8.826,115.857,-8.788,115.869,-8.743,115.914,-8.758,116.032,-8.765,116.076,-8.745,116.078,-8.611,116.061,-8.437,116.22,-8.295,116.304,-8.238,116.402,-8.204,116.647,-8.283,116.688,-8.304,116.719,-8.336,116.734,-8.387,116.641,-8.614]],[[115.448,-8.155,115.549,-8.208,115.691,-8.364,115.704,-8.407,115.661,-8.448,115.56,-8.514,115.334,-8.616,115.295,-8.664,115.247,-8.758,115.236,-8.798,115.22,-8.82,115.194,-8.835,115.145,-8.849,115.092,-8.829,115.14,-8.769,115.142,-8.697,115.106,-8.629,115.055,-8.573,114.952,-8.496,114.842,-8.429,114.731,-8.394,114.613,-8.378,114.571,-8.345,114.502,-8.261,114.479,-8.215,114.468,-8.166,114.475,-8.119,114.504,-8.117,114.62,-8.128,114.833,-8.183,114.938,-8.187,114.998,-8.174,115.154,-8.066,115.191,-8.067,115.34,-8.115,115.448,-8.155]],[[106.046,-1.669,106.08,-1.738,106.127,-1.8,106.162,-1.867,106.209,-2.189,106.366,-2.465,106.818,-2.573,106.744,-2.618,106.707,-2.658,106.679,-2.704,106.612,-2.896,106.619,-2.936,106.658,-3.001,106.667,-3.072,106.611,-3.071,106.547,-3.056,106.496,-3.029,106.449,-2.994,106.397,-2.967,106.342,-2.949,106.25,-2.894,106.126,-2.855,105.999,-2.825,105.937,-2.744,105.908,-2.643,105.939,-2.493,105.908,-2.452,105.862,-2.415,105.807,-2.307,105.786,-2.181,105.705,-2.133,105.599,-2.103,105.553,-2.079,105.343,-2.125,105.293,-2.114,105.248,-2.079,105.133,-2.043,105.138,-1.973,105.191,-1.917,105.316,-1.861,105.375,-1.813,105.387,-1.751,105.364,-1.705,105.373,-1.657,105.413,-1.611,105.46,-1.575,105.585,-1.527,105.64,-1.61,105.668,-1.68,105.701,-1.731,105.754,-1.659,105.72,-1.534,105.816,-1.506,105.91,-1.505,105.981,-1.539,106.027,-1.593,106.046,-1.669]],[[123.18,-4.551,123.203,-4.766,123.196,-4.823,123.139,-4.74,123.119,-4.723,123.104,-4.74,123.084,-4.749,123.055,-4.748,123.018,-4.832,123.015,-4.91,122.987,-4.963,122.972,-5.138,122.981,-5.186,123.025,-5.162,123.051,-5.156,123.15,-5.224,123.202,-5.273,123.187,-5.333,123.121,-5.393,123.043,-5.419,122.986,-5.394,122.969,-5.406,122.935,-5.437,122.909,-5.477,122.916,-5.519,122.85,-5.638,122.812,-5.671,122.733,-5.635,122.684,-5.666,122.645,-5.663,122.585,-5.545,122.586,-5.489,122.642,-5.426,122.643,-5.381,122.67,-5.331,122.731,-5.262,122.767,-5.21,122.768,-5.177,122.794,-5.052,122.804,-5,122.821,-4.944,122.849,-4.831,122.853,-4.618,122.947,-4.443,123.038,-4.395,123.075,-4.387,123.069,-4.434,123.18,-4.551]],[[122.645,-5.269,122.619,-5.336,122.564,-5.387,122.52,-5.391,122.474,-5.381,122.392,-5.335,122.371,-5.383,122.307,-5.381,122.283,-5.32,122.329,-5.138,122.396,-5.07,122.39,-4.999,122.334,-4.847,122.369,-4.767,122.524,-4.707,122.66,-4.634,122.702,-4.619,122.74,-4.675,122.76,-4.934,122.614,-5.139,122.645,-5.269]],[[108.316,3.69,108.18,3.653,108.1,3.705,108.186,3.768,108.216,3.772,108.236,3.785,108.243,3.81,108.088,3.852,108.045,3.889,108.002,3.983,108.004,4.043,108.202,4.2,108.248,4.217,108.256,4.152,108.393,3.986,108.399,3.876,108.394,3.836,108.316,3.69]],[[108.207,-2.998,108.192,-3.103,108.167,-3.143,108.084,-3.195,108.055,-3.227,107.977,-3.222,107.967,-3.167,107.941,-3.129,107.858,-3.086,107.837,-3.097,107.822,-3.161,107.66,-3.206,107.614,-3.209,107.637,-3.125,107.595,-3.058,107.592,-2.977,107.584,-2.941,107.563,-2.92,107.605,-2.863,107.598,-2.8,107.642,-2.732,107.666,-2.566,107.838,-2.53,107.875,-2.56,108.074,-2.597,108.215,-2.697,108.291,-2.83,108.207,-2.998]],[[135.383,-0.651,135.596,-0.69,135.673,-0.688,135.749,-0.733,135.841,-0.712,135.894,-0.726,136.069,-0.878,136.155,-0.978,136.283,-1.065,136.375,-1.094,136.305,-1.173,136.165,-1.215,136.11,-1.217,136.003,-1.17,135.915,-1.178,135.839,-1.119,135.826,-1.028,135.747,-0.823,135.646,-0.882,135.524,-0.787,135.491,-0.785,135.483,-0.801,135.432,-0.769,135.388,-0.705,135.383,-0.651]],[[135.474,-1.592,135.869,-1.642,135.976,-1.636,136.202,-1.655,136.39,-1.722,136.719,-1.734,136.817,-1.754,136.893,-1.8,136.709,-1.838,136.622,-1.873,136.461,-1.89,136.326,-1.872,136.228,-1.894,136.193,-1.859,136.049,-1.824,135.866,-1.752,135.488,-1.668,135.47,-1.616,135.474,-1.592]],[[130.813,-0.004,130.987,-0.047,131.026,-0.04,131.277,-0.15,131.317,-0.204,131.303,-0.241,131.34,-0.29,131.258,-0.366,131.218,-0.374,131.178,-0.346,131.098,-0.33,131.005,-0.361,130.946,-0.338,130.897,-0.268,130.808,-0.226,130.683,-0.081,130.622,-0.086,130.638,-0.143,130.691,-0.181,130.761,-0.291,130.802,-0.302,130.843,-0.298,130.899,-0.344,130.896,-0.416,130.75,-0.444,130.7,-0.392,130.689,-0.297,130.607,-0.329,130.575,-0.362,130.551,-0.366,130.496,-0.267,130.341,-0.262,130.237,-0.21,130.288,-0.155,130.295,-0.101,130.363,-0.073,130.431,-0.098,130.5,-0.06,130.548,-0.07,130.584,-0.045,130.722,-0.03,130.813,-0.004]],[[128.454,2.052,128.296,2.035,128.26,2.083,128.218,2.297,128.33,2.469,128.472,2.571,128.569,2.596,128.602,2.598,128.688,2.474,128.623,2.224,128.548,2.097,128.454,2.052]],[[128.153,-1.661,128.092,-1.701,128.061,-1.712,127.914,-1.685,127.741,-1.691,127.562,-1.729,127.458,-1.697,127.392,-1.645,127.395,-1.59,127.457,-1.454,127.592,-1.351,127.647,-1.332,127.743,-1.36,127.905,-1.439,128.033,-1.532,128.149,-1.604,128.153,-1.661]],[[126.055,-2.451,126.038,-2.469,125.978,-2.415,125.938,-2.263,125.903,-2.222,125.863,-2.077,125.873,-2.036,125.923,-1.975,125.963,-1.976,125.993,-2.012,125.976,-2.168,126.066,-2.366,126.055,-2.451]],[[126.024,-1.79,126.332,-1.823,126.288,-1.859,125.956,-1.917,125.839,-1.906,125.479,-1.94,125.433,-1.938,125.426,-1.882,125.387,-1.843,125.445,-1.809,125.521,-1.801,125.72,-1.814,126.024,-1.79]],[[124.97,-1.705,125.063,-1.741,125.096,-1.741,125.127,-1.699,125.146,-1.693,125.188,-1.713,125.198,-1.78,125.258,-1.771,125.305,-1.794,125.32,-1.81,125.314,-1.877,125.135,-1.889,125.007,-1.943,124.834,-1.894,124.639,-1.978,124.521,-2.007,124.418,-2.005,124.33,-1.859,124.381,-1.687,124.418,-1.659,124.483,-1.644,124.664,-1.636,124.97,-1.705]],[[131.326,-8,131.309,-8.011,131.185,-7.998,131.114,-7.997,131.123,-7.922,131.087,-7.865,131.137,-7.782,131.138,-7.685,131.19,-7.672,131.197,-7.617,131.26,-7.471,131.297,-7.438,131.349,-7.425,131.411,-7.34,131.446,-7.315,131.483,-7.251,131.535,-7.221,131.531,-7.165,131.561,-7.136,131.643,-7.113,131.701,-7.14,131.736,-7.197,131.644,-7.267,131.691,-7.439,131.624,-7.626,131.58,-7.682,131.498,-7.731,131.474,-7.777,131.377,-7.869,131.348,-7.948,131.343,-7.981,131.326,-8]],[[126.801,-7.668,126.814,-7.717,126.813,-7.738,126.693,-7.754,126.577,-7.808,126.518,-7.87,126.472,-7.95,126.313,-7.918,126.171,-7.912,126.108,-7.884,126.04,-7.886,125.952,-7.911,125.826,-7.979,125.798,-7.985,125.808,-7.881,125.843,-7.817,125.975,-7.663,126.085,-7.697,126.214,-7.707,126.359,-7.677,126.463,-7.608,126.61,-7.572,126.726,-7.662,126.801,-7.668]],[[124.576,-8.141,124.6,-8.202,124.677,-8.168,124.752,-8.16,124.924,-8.166,125.05,-8.18,125.125,-8.205,125.132,-8.326,125.097,-8.353,124.444,-8.445,124.381,-8.415,124.356,-8.386,124.426,-8.296,124.394,-8.253,124.431,-8.183,124.509,-8.135,124.576,-8.141]],[[131.002,-1.316,130.967,-1.343,130.845,-1.317,130.782,-1.255,130.739,-1.173,130.712,-1.104,130.704,-1.05,130.668,-0.984,130.673,-0.96,130.897,-0.89,130.939,-0.915,131.033,-0.918,131.074,-0.968,131.046,-1.188,131.002,-1.316]],[[96.493,5.229,96.615,5.22,96.843,5.274,96.968,5.269,97.086,5.23,97.19,5.207,97.451,5.236,97.5,5.228,97.547,5.206,97.588,5.17,97.707,5.04,97.908,4.88,97.967,4.777,98,4.662,98.021,4.635,98.248,4.415,98.273,4.322,98.241,4.195,98.307,4.093,98.528,3.998,98.659,3.928,98.687,3.886,98.706,3.835,98.778,3.759,98.869,3.71,99.151,3.581,99.521,3.311,99.732,3.183,99.907,2.988,99.969,2.895,100.021,2.794,100.127,2.648,100.307,2.467,100.353,2.411,100.401,2.332,100.457,2.257,100.524,2.189,100.604,2.137,100.685,2.12,100.817,1.989,100.888,1.948,100.877,2.051,100.817,2.14,100.818,2.194,100.828,2.243,100.877,2.283,100.936,2.295,101.046,2.257,101.225,2.102,101.301,2.012,101.358,1.887,101.405,1.757,101.477,1.693,101.575,1.671,101.684,1.661,101.785,1.621,102.02,1.442,102.098,1.358,102.157,1.259,102.198,1.142,102.223,1.019,102.239,0.99,102.39,0.842,102.469,0.779,102.566,0.749,102.849,0.715,102.949,0.664,103.032,0.579,103.067,0.492,103.008,0.415,102.786,0.298,102.55,0.216,102.78,0.244,102.896,0.279,103.003,0.332,103.109,0.4,103.277,0.495,103.339,0.514,103.412,0.507,103.479,0.48,103.579,0.387,103.673,0.289,103.743,0.174,103.787,0.047,103.706,-0.02,103.589,-0.069,103.429,-0.192,103.412,-0.24,103.444,-0.272,103.405,-0.362,103.495,-0.418,103.509,-0.466,103.431,-0.534,103.439,-0.576,103.533,-0.755,103.578,-0.796,103.721,-0.887,103.94,-0.979,104.061,-1.021,104.199,-1.054,104.258,-1.053,104.361,-1.038,104.381,-1.074,104.426,-1.251,104.447,-1.362,104.478,-1.6,104.519,-1.699,104.516,-1.819,104.569,-1.922,104.676,-1.987,104.791,-2.041,104.845,-2.093,104.845,-2.172,104.826,-2.234,104.787,-2.283,104.668,-2.386,104.647,-2.43,104.631,-2.543,104.651,-2.595,104.698,-2.598,104.736,-2.571,104.878,-2.419,104.917,-2.392,104.971,-2.371,105.026,-2.358,105.287,-2.356,105.397,-2.38,105.495,-2.43,105.582,-2.492,105.899,-2.888,106.044,-3.106,106.056,-3.161,106.058,-3.217,106.034,-3.261,105.901,-3.41,105.885,-3.451,105.844,-3.614,105.852,-3.731,105.896,-3.78,105.93,-3.833,105.928,-3.881,105.841,-4.122,105.831,-4.163,105.887,-4.554,105.891,-4.66,105.879,-4.794,105.887,-5.01,105.816,-5.677,105.803,-5.716,105.748,-5.818,105.677,-5.818,105.619,-5.8,105.578,-5.761,105.556,-5.712,105.523,-5.673,105.349,-5.55,105.304,-5.57,105.128,-5.723,105.081,-5.746,105.023,-5.727,104.93,-5.681,104.64,-5.52,104.622,-5.572,104.618,-5.642,104.676,-5.816,104.684,-5.893,104.631,-5.908,104.602,-5.905,104.481,-5.803,104.37,-5.691,104.243,-5.539,104.15,-5.467,104.067,-5.386,103.831,-5.08,103.77,-5.033,103.406,-4.816,103.332,-4.765,103.239,-4.676,103.139,-4.596,102.919,-4.471,102.538,-4.152,102.372,-3.969,102.188,-3.675,102.128,-3.599,101.818,-3.378,101.649,-3.244,101.579,-3.167,101.414,-2.899,101.366,-2.808,101.306,-2.729,101.206,-2.664,101.119,-2.588,100.944,-2.345,100.89,-2.249,100.848,-2.144,100.855,-1.934,100.487,-1.299,100.394,-1.101,100.308,-0.827,100.289,-0.799,100.088,-0.553,100.017,-0.474,99.931,-0.4,99.86,-0.314,99.721,-0.033,99.67,0.045,99.598,0.102,99.335,0.209,99.236,0.268,99.159,0.352,99.112,0.459,99.06,0.686,98.936,1.032,98.796,1.495,98.703,1.702,98.595,1.865,98.564,1.902,98.087,2.195,98.005,2.238,97.919,2.264,97.795,2.283,97.701,2.359,97.662,2.494,97.641,2.676,97.617,2.785,97.591,2.847,97.391,2.975,97.313,3.077,97.248,3.189,97.188,3.276,96.969,3.575,96.894,3.654,96.801,3.709,96.525,3.767,96.445,3.816,96.311,3.986,96.23,4.073,95.988,4.263,95.579,4.662,95.495,4.761,95.432,4.865,95.381,4.976,95.207,5.284,95.221,5.346,95.247,5.411,95.243,5.464,95.224,5.517,95.228,5.565,95.28,5.593,95.396,5.629,95.517,5.625,95.629,5.609,95.737,5.579,95.841,5.515,96.027,5.351,96.133,5.294,96.251,5.267,96.493,5.229]],[[122.783,-8.612,122.642,-8.647,122.554,-8.681,122.47,-8.725,122.417,-8.735,122.321,-8.738,122.186,-8.73,122.094,-8.745,121.839,-8.86,121.738,-8.87,121.651,-8.899,121.621,-8.854,121.585,-8.821,121.5,-8.812,121.415,-8.815,121.328,-8.917,121.191,-8.896,121.138,-8.904,121.086,-8.926,121.035,-8.935,120.982,-8.928,120.781,-8.849,120.55,-8.802,120.32,-8.82,120.121,-8.777,120.012,-8.81,119.909,-8.858,119.879,-8.808,119.841,-8.764,119.808,-8.698,119.807,-8.623,119.818,-8.571,119.848,-8.523,119.866,-8.473,119.875,-8.42,119.918,-8.445,119.964,-8.436,120.099,-8.378,120.231,-8.29,120.354,-8.258,120.425,-8.249,120.486,-8.266,120.547,-8.26,120.61,-8.24,120.71,-8.308,120.751,-8.321,120.886,-8.327,121.009,-8.366,121.118,-8.424,121.277,-8.478,121.372,-8.551,121.445,-8.578,121.498,-8.585,121.548,-8.575,121.61,-8.526,121.683,-8.506,121.747,-8.507,121.863,-8.494,121.912,-8.482,121.967,-8.455,122.02,-8.472,122.067,-8.497,122.263,-8.625,122.323,-8.628,122.433,-8.601,122.467,-8.566,122.484,-8.514,122.514,-8.47,122.556,-8.432,122.604,-8.402,122.75,-8.353,122.85,-8.304,122.919,-8.222,122.759,-8.186,122.792,-8.127,122.846,-8.093,122.917,-8.106,122.978,-8.152,123.006,-8.329,122.955,-8.354,122.924,-8.381,122.902,-8.416,122.811,-8.481,122.847,-8.562,122.82,-8.596,122.783,-8.612]],[[120.013,-9.375,120.058,-9.42,120.221,-9.506,120.248,-9.543,120.258,-9.603,120.291,-9.648,120.365,-9.655,120.444,-9.646,120.504,-9.674,120.556,-9.719,120.633,-9.806,120.7,-9.903,120.784,-9.957,120.833,-10.037,120.804,-10.108,120.698,-10.207,120.64,-10.228,120.562,-10.236,120.439,-10.294,120.395,-10.263,120.255,-10.242,120.145,-10.2,120.052,-10.123,119.998,-10.04,119.931,-9.967,119.813,-9.917,119.601,-9.774,119.47,-9.761,119.417,-9.771,119.363,-9.772,119.085,-9.707,119.042,-9.669,119.008,-9.621,118.977,-9.573,118.959,-9.519,118.994,-9.472,119.031,-9.44,119.186,-9.384,119.296,-9.367,119.424,-9.37,119.615,-9.352,119.795,-9.38,119.851,-9.36,119.942,-9.301,119.974,-9.322,120.013,-9.375]],[[118.242,-8.318,118.292,-8.357,118.338,-8.354,118.433,-8.293,118.491,-8.271,118.552,-8.27,118.612,-8.281,118.671,-8.323,118.692,-8.393,118.714,-8.415,118.748,-8.331,118.794,-8.306,118.846,-8.293,118.926,-8.298,118.988,-8.338,119.044,-8.457,119.042,-8.561,119.063,-8.6,119.101,-8.628,119.13,-8.668,119.104,-8.71,119.079,-8.73,119.006,-8.75,118.971,-8.741,118.939,-8.713,118.903,-8.703,118.821,-8.712,118.746,-8.735,118.756,-8.774,118.818,-8.791,118.837,-8.809,118.833,-8.833,118.808,-8.838,118.728,-8.805,118.674,-8.812,118.479,-8.856,118.427,-8.855,118.398,-8.813,118.4,-8.704,118.379,-8.675,118.234,-8.808,118.19,-8.841,118.132,-8.856,118.071,-8.851,117.861,-8.931,117.795,-8.92,117.732,-8.92,117.508,-9.008,117.388,-9.032,117.326,-9.034,117.265,-9.026,117.21,-9.034,117.161,-9.069,117.061,-9.099,116.958,-9.076,116.871,-9.046,116.788,-9.006,116.768,-8.955,116.772,-8.894,116.807,-8.811,116.783,-8.665,116.801,-8.598,116.835,-8.532,116.886,-8.508,116.953,-8.503,117.064,-8.444,117.165,-8.367,117.224,-8.375,117.357,-8.429,117.435,-8.435,117.567,-8.426,117.622,-8.46,117.643,-8.536,117.673,-8.563,117.712,-8.583,117.806,-8.711,117.893,-8.704,117.97,-8.728,118.104,-8.65,118.206,-8.652,118.235,-8.592,118.174,-8.528,118.1,-8.475,118.061,-8.464,118.018,-8.467,117.979,-8.459,117.815,-8.342,117.766,-8.279,117.738,-8.205,117.755,-8.15,117.868,-8.101,117.921,-8.089,118.117,-8.122,118.151,-8.15,118.203,-8.267,118.242,-8.318]],[[124.889,0.995,124.698,0.826,124.64,0.744,124.589,0.655,124.514,0.557,124.428,0.471,124.384,0.445,124.278,0.398,124.217,0.38,124.101,0.375,123.754,0.306,123.64,0.297,123.526,0.3,123.31,0.318,123.265,0.327,123.179,0.416,123.083,0.486,122.997,0.494,122.91,0.486,122.281,0.481,122.061,0.468,121.842,0.437,121.723,0.451,121.605,0.486,121.516,0.498,121.426,0.495,121.013,0.442,120.909,0.447,120.7,0.515,120.579,0.528,120.46,0.51,120.349,0.449,120.307,0.408,120.192,0.269,120.127,0.167,120.078,0.04,120.036,-0.09,120.013,-0.196,120.012,-0.307,120.032,-0.432,120.063,-0.556,120.097,-0.65,120.241,-0.868,120.27,-0.899,120.425,-0.961,120.518,-1.039,120.605,-1.258,120.667,-1.37,120.729,-1.371,120.797,-1.364,120.916,-1.378,121.034,-1.407,121.149,-1.339,121.213,-1.212,121.277,-1.118,121.431,-0.939,121.519,-0.856,121.576,-0.829,121.633,-0.84,121.681,-0.888,121.738,-0.926,121.853,-0.946,121.97,-0.933,122.094,-0.875,122.138,-0.839,122.175,-0.794,122.28,-0.757,122.53,-0.757,122.659,-0.77,122.889,-0.755,122.886,-0.722,122.841,-0.687,122.829,-0.659,122.872,-0.641,123.02,-0.6,123.171,-0.571,123.281,-0.592,123.38,-0.649,123.417,-0.707,123.434,-0.778,123.396,-0.962,123.378,-1.004,123.3,-1.026,123.226,-1.002,123.153,-0.907,123.049,-0.872,122.903,-0.901,122.853,-0.928,122.807,-0.966,122.725,-1.064,122.656,-1.175,122.507,-1.348,122.334,-1.498,122.251,-1.555,122.158,-1.594,121.859,-1.693,121.78,-1.767,121.719,-1.863,121.651,-1.895,121.573,-1.906,121.514,-1.888,121.395,-1.834,121.355,-1.878,121.349,-1.946,121.408,-1.97,121.502,-2.045,121.575,-2.151,121.622,-2.174,121.726,-2.208,121.77,-2.241,121.848,-2.332,121.972,-2.542,122.014,-2.656,122.083,-2.75,122.292,-2.908,122.303,-2.952,122.29,-3.004,122.307,-3.052,122.381,-3.142,122.399,-3.201,122.317,-3.275,122.313,-3.383,122.263,-3.527,122.251,-3.576,122.253,-3.62,122.288,-3.662,122.329,-3.694,122.385,-3.711,122.435,-3.74,122.529,-3.853,122.579,-3.882,122.61,-3.923,122.607,-3.985,122.65,-4.021,122.69,-4.084,122.75,-4.1,122.779,-4.082,122.798,-4.054,122.848,-4.065,122.877,-4.109,122.894,-4.166,122.9,-4.229,122.897,-4.349,122.872,-4.392,122.818,-4.39,122.72,-4.341,122.715,-4.376,122.722,-4.411,122.672,-4.422,122.615,-4.417,122.471,-4.422,122.207,-4.496,122.114,-4.54,122.054,-4.62,122.05,-4.675,122.073,-4.792,122.038,-4.832,121.917,-4.848,121.748,-4.817,121.646,-4.786,121.589,-4.76,121.514,-4.681,121.487,-4.581,121.541,-4.283,121.557,-4.245,121.583,-4.211,121.612,-4.156,121.618,-4.093,121.537,-4.015,121.416,-3.984,121.313,-3.919,120.914,-3.556,120.892,-3.521,120.891,-3.46,120.907,-3.404,121.038,-3.205,121.054,-3.167,121.07,-3.01,121.067,-2.881,121.052,-2.752,120.99,-2.67,120.879,-2.646,120.765,-2.642,120.654,-2.668,120.544,-2.733,120.341,-2.87,120.261,-2.949,120.254,-3.053,120.3,-3.154,120.36,-3.247,120.392,-3.348,120.437,-3.707,120.435,-3.748,120.383,-3.852,120.363,-4.086,120.385,-4.415,120.42,-4.617,120.405,-4.727,120.31,-4.963,120.281,-5.093,120.279,-5.146,120.391,-5.393,120.417,-5.49,120.43,-5.591,120.312,-5.542,120.256,-5.544,120.201,-5.559,120.077,-5.575,119.952,-5.578,119.908,-5.596,119.818,-5.662,119.764,-5.688,119.717,-5.693,119.557,-5.611,119.463,-5.522,119.376,-5.425,119.36,-5.314,119.391,-5.201,119.434,-5.079,119.52,-4.877,119.516,-4.742,119.545,-4.631,119.594,-4.523,119.612,-4.424,119.624,-4.034,119.611,-4,119.494,-3.769,119.48,-3.73,119.479,-3.667,119.492,-3.608,119.495,-3.554,119.467,-3.513,119.42,-3.475,119.362,-3.459,119.24,-3.475,118.995,-3.538,118.922,-3.483,118.868,-3.398,118.833,-3.28,118.813,-3.157,118.822,-3.041,118.858,-2.929,118.829,-2.85,118.784,-2.765,118.783,-2.721,118.809,-2.682,118.853,-2.65,118.908,-2.631,118.958,-2.597,119.092,-2.483,119.135,-2.382,119.138,-2.258,119.172,-2.14,119.241,-2.031,119.322,-1.93,119.348,-1.825,119.308,-1.66,119.324,-1.584,119.31,-1.496,119.309,-1.408,119.359,-1.243,119.508,-0.907,119.654,-0.728,119.711,-0.681,119.787,-0.764,119.844,-0.862,119.845,-0.773,119.83,-0.686,119.772,-0.484,119.722,-0.088,119.736,-0.051,119.787,-0.057,119.838,-0.022,119.866,0.04,119.812,0.187,119.809,0.239,119.913,0.445,119.998,0.52,120.035,0.567,120.056,0.693,120.101,0.74,120.157,0.774,120.23,0.861,120.27,0.971,120.294,0.979,120.322,0.983,120.367,0.888,120.416,0.849,120.517,0.818,120.603,0.854,120.626,0.902,120.659,0.944,120.711,0.987,120.755,1.036,120.804,1.149,120.868,1.253,120.912,1.289,120.965,1.312,121.025,1.326,121.082,1.328,121.208,1.263,121.282,1.25,121.357,1.255,121.404,1.244,121.44,1.214,121.473,1.156,121.513,1.105,121.551,1.08,121.592,1.068,121.867,1.089,122.108,1.031,122.437,1.018,122.549,0.984,122.657,0.941,122.79,0.863,122.838,0.846,122.892,0.85,122.96,0.923,123.013,0.939,123.067,0.942,123.278,0.928,123.847,0.838,123.931,0.85,124.274,1.022,124.411,1.185,124.534,1.23,124.575,1.304,124.6,1.392,124.644,1.416,124.747,1.441,124.788,1.468,124.861,1.576,124.947,1.672,124.989,1.701,125.111,1.686,125.165,1.644,125.234,1.502,125.222,1.479,125.141,1.408,125.117,1.379,125.028,1.18,124.967,1.083,124.889,0.995]],[[107.374,-6.008,107.475,-6.122,107.563,-6.183,107.667,-6.216,107.776,-6.219,107.884,-6.233,108.009,-6.277,108.138,-6.297,108.197,-6.289,108.254,-6.267,108.295,-6.265,108.33,-6.286,108.419,-6.383,108.516,-6.471,108.538,-6.516,108.604,-6.729,108.678,-6.791,108.78,-6.808,108.899,-6.808,109.018,-6.817,109.294,-6.867,109.404,-6.86,109.501,-6.81,109.587,-6.843,109.821,-6.902,109.936,-6.916,110.067,-6.899,110.198,-6.895,110.261,-6.912,110.321,-6.938,110.373,-6.948,110.426,-6.947,110.521,-6.897,110.584,-6.806,110.634,-6.69,110.674,-6.57,110.701,-6.518,110.737,-6.472,110.784,-6.443,110.835,-6.424,110.972,-6.436,111.001,-6.465,111.154,-6.669,111.182,-6.687,111.342,-6.7,111.387,-6.693,111.484,-6.652,111.54,-6.648,111.644,-6.699,111.688,-6.742,111.738,-6.773,111.99,-6.806,112.087,-6.893,112.137,-6.905,112.312,-6.894,112.434,-6.903,112.539,-6.926,112.587,-7.051,112.626,-7.178,112.649,-7.221,112.752,-7.265,112.794,-7.304,112.783,-7.432,112.795,-7.552,113.014,-7.658,113.248,-7.718,113.498,-7.724,113.747,-7.703,113.876,-7.677,114.037,-7.632,114.071,-7.633,114.383,-7.771,114.409,-7.792,114.444,-7.896,114.443,-8.005,114.385,-8.263,114.381,-8.334,114.387,-8.405,114.449,-8.559,114.482,-8.604,114.595,-8.685,114.599,-8.727,114.584,-8.77,114.459,-8.741,114.383,-8.705,114.339,-8.647,114.277,-8.615,114.16,-8.626,113.94,-8.568,113.693,-8.478,113.253,-8.287,113.134,-8.288,113.019,-8.313,112.898,-8.361,112.772,-8.396,112.679,-8.409,112.586,-8.4,112.352,-8.354,112.115,-8.324,111.51,-8.305,111.339,-8.262,111.055,-8.24,110.83,-8.202,110.607,-8.149,110.039,-7.891,109.853,-7.828,109.282,-7.705,109.194,-7.695,108.987,-7.704,108.856,-7.668,108.741,-7.667,108.571,-7.707,108.518,-7.736,108.452,-7.797,108.336,-7.794,108.221,-7.782,107.917,-7.724,107.804,-7.688,107.696,-7.636,107.598,-7.567,107.547,-7.542,107.285,-7.472,107.071,-7.447,106.631,-7.416,106.535,-7.394,106.455,-7.369,106.411,-7.312,106.417,-7.239,106.448,-7.177,106.492,-7.114,106.52,-7.054,106.198,-6.928,105.944,-6.859,105.835,-6.846,105.725,-6.846,105.601,-6.86,105.478,-6.854,105.421,-6.833,105.362,-6.826,105.303,-6.841,105.255,-6.835,105.243,-6.778,105.273,-6.729,105.336,-6.674,105.371,-6.664,105.387,-6.751,105.405,-6.768,105.46,-6.787,105.484,-6.782,105.581,-6.671,105.608,-6.617,105.655,-6.47,105.706,-6.498,105.757,-6.48,105.787,-6.457,105.868,-6.116,105.936,-6.017,106.029,-5.934,106.075,-5.914,106.166,-5.965,106.35,-5.984,106.459,-6.018,106.569,-6.022,106.676,-6.038,106.825,-6.098,106.878,-6.092,106.932,-6.073,107.012,-6.008,107.046,-5.904,107.162,-5.957,107.332,-5.978,107.374,-6.008]],[[109.629,2.028,109.539,1.896,109.549,1.848,109.571,1.806,109.636,1.777,109.654,1.615,109.736,1.523,109.818,1.439,109.879,1.398,109.945,1.338,109.992,1.283,110.041,1.236,110.115,1.19,110.315,0.996,110.399,0.939,110.461,0.882,110.506,0.862,110.615,0.878,110.938,1.017,110.996,1.026,111.101,1.051,111.287,1.043,111.483,0.996,111.547,0.994,111.607,1.023,111.691,1.014,111.77,0.999,111.809,1.012,111.923,1.113,112.079,1.143,112.129,1.244,112.167,1.338,112.186,1.439,112.251,1.48,112.342,1.515,112.476,1.559,112.943,1.567,112.988,1.548,112.998,1.496,112.988,1.457,113.007,1.434,113.069,1.432,113.126,1.408,113.359,1.327,113.458,1.302,113.513,1.308,113.622,1.236,113.682,1.261,113.76,1.311,113.835,1.38,113.902,1.434,114,1.455,114.126,1.452,114.275,1.471,114.387,1.5,114.513,1.452,114.546,1.467,114.567,1.514,114.632,1.617,114.661,1.686,114.686,1.819,114.704,1.851,114.751,1.869,114.8,1.894,114.813,1.934,114.831,1.98,114.816,2.019,114.788,2.052,114.759,2.162,114.768,2.213,114.786,2.25,114.836,2.269,114.969,2.351,115.087,2.446,115.151,2.493,115.179,2.523,115.181,2.567,115.13,2.612,115.081,2.634,115.077,2.687,115.079,2.723,115.094,2.758,115.087,2.791,115.086,2.841,115.118,2.895,115.19,2.974,115.247,3.026,115.31,2.994,115.384,3.009,115.454,3.034,115.493,3.128,115.499,3.173,115.49,3.209,115.514,3.342,115.52,3.362,115.566,3.446,115.571,3.502,115.545,3.634,115.561,3.733,115.568,3.939,115.596,3.976,115.628,4.082,115.679,4.193,115.782,4.254,115.837,4.333,115.861,4.348,115.896,4.349,116.022,4.291,116.134,4.355,116.236,4.363,116.32,4.354,116.368,4.327,116.415,4.308,116.515,4.371,116.553,4.36,116.589,4.338,116.639,4.339,116.698,4.355,116.844,4.34,117.101,4.337,117.278,4.299,117.451,4.193,117.537,4.171,117.574,4.171,117.566,4.162,117.497,4.133,117.465,4.076,117.559,3.988,117.566,3.93,117.639,3.878,117.728,3.797,117.732,3.77,117.762,3.734,117.777,3.689,117.714,3.645,117.63,3.636,117.567,3.678,117.51,3.73,117.495,3.666,117.45,3.629,117.288,3.639,117.172,3.639,117.056,3.623,117.114,3.613,117.166,3.592,117.346,3.427,117.385,3.365,117.322,3.244,117.352,3.194,117.422,3.165,117.507,3.105,117.567,3.098,117.611,3.064,117.612,3.005,117.638,2.951,117.569,2.929,117.637,2.915,117.698,2.887,117.665,2.859,117.639,2.825,117.667,2.807,117.75,2.776,117.786,2.747,117.805,2.669,117.886,2.542,118.034,2.378,118.067,2.318,118.066,2.263,118.042,2.215,117.957,2.16,117.889,2.087,117.881,2.061,117.789,2.027,117.831,2.002,117.865,1.968,117.928,1.867,118.08,1.702,118.157,1.64,118.472,1.416,118.639,1.319,118.853,1.096,118.963,1.044,118.985,0.982,118.892,0.887,118.757,0.839,118.535,0.814,118.312,0.847,118.196,0.874,118.096,0.929,118.016,1.039,117.912,1.099,117.952,1.032,117.977,0.964,117.964,0.89,117.923,0.831,117.853,0.789,117.777,0.754,117.745,0.73,117.553,0.341,117.522,0.236,117.464,-0.2,117.463,-0.324,117.549,-0.554,117.557,-0.675,117.574,-0.728,117.563,-0.771,117.522,-0.797,117.357,-0.867,117.241,-0.926,117.146,-1.009,117.07,-1.113,117.003,-1.188,116.914,-1.224,116.849,-1.218,116.797,-1.184,116.761,-1.117,116.74,-1.044,116.726,-1.098,116.729,-1.151,116.759,-1.207,116.771,-1.267,116.753,-1.327,116.715,-1.376,116.612,-1.429,116.554,-1.474,116.545,-1.553,116.518,-1.598,116.478,-1.633,116.332,-1.712,116.3,-1.744,116.275,-1.785,116.353,-1.779,116.424,-1.785,116.43,-1.864,116.452,-1.923,116.424,-2.053,116.314,-2.14,116.369,-2.158,116.418,-2.187,116.528,-2.208,116.565,-2.3,116.549,-2.411,116.529,-2.511,116.45,-2.538,116.401,-2.52,116.353,-2.522,116.317,-2.552,116.307,-2.603,116.375,-2.578,116.372,-2.707,116.353,-2.833,116.331,-2.902,116.289,-2.959,116.226,-2.977,116.166,-2.935,116.154,-2.984,116.172,-3.025,116.257,-3.126,116.205,-3.149,116.167,-3.183,116.15,-3.233,116.058,-3.348,116.017,-3.433,115.999,-3.523,115.956,-3.595,115.258,-3.907,114.694,-4.17,114.653,-4.152,114.625,-4.112,114.606,-3.703,114.536,-3.494,114.526,-3.377,114.446,-3.482,114.397,-3.471,114.344,-3.444,114.305,-3.41,114.302,-3.365,114.344,-3.235,114.293,-3.306,114.236,-3.361,114.178,-3.354,114.128,-3.327,114.109,-3.285,114.082,-3.279,113.959,-3.394,113.796,-3.456,113.705,-3.455,113.634,-3.42,113.637,-3.332,113.63,-3.246,113.61,-3.196,113.566,-3.178,113.526,-3.184,113.409,-3.229,113.367,-3.224,113.343,-3.246,113.034,-2.933,112.971,-3.187,112.758,-3.322,112.6,-3.4,112.444,-3.371,112.285,-3.321,112.127,-3.381,111.955,-3.53,111.907,-3.553,111.858,-3.552,111.822,-3.533,111.834,-3.42,111.836,-3.308,111.823,-3.057,111.809,-3.008,111.76,-2.939,111.695,-2.889,111.658,-2.926,111.625,-2.975,111.495,-2.973,111.368,-2.934,111.259,-2.956,111.044,-3.056,110.93,-3.071,110.869,-3.049,110.83,-2.995,110.852,-2.946,110.899,-2.909,110.811,-2.938,110.736,-2.989,110.703,-3.021,110.668,-3.005,110.574,-2.891,110.378,-2.934,110.351,-2.947,110.303,-2.985,110.256,-2.966,110.233,-2.925,110.224,-2.689,110.124,-2.234,110.097,-2.001,110.075,-1.946,109.96,-1.863,109.964,-1.743,110.023,-1.643,110.036,-1.526,110.019,-1.399,109.983,-1.275,109.938,-1.181,109.873,-1.101,109.787,-1.011,109.682,-0.944,109.454,-0.869,109.333,-0.875,109.289,-0.846,109.259,-0.807,109.271,-0.732,109.312,-0.68,109.366,-0.667,109.373,-0.638,109.257,-0.577,109.161,-0.495,109.13,-0.445,109.121,-0.391,109.122,-0.265,109.15,-0.186,109.165,-0.142,109.195,-0.009,109.258,0.031,109.247,0.056,109.22,0.074,109.181,0.117,109.149,0.168,109.075,0.253,108.945,0.356,108.923,0.533,108.906,0.794,108.917,0.913,108.959,1.135,109.031,1.204,109.088,1.224,109.132,1.254,109.096,1.258,109.065,1.247,109.01,1.24,109.055,1.438,109.076,1.496,109.167,1.607,109.273,1.705,109.318,1.821,109.379,1.923,109.629,2.028]],[[127.733,0.848,127.805,0.826,127.881,0.832,127.919,0.877,127.929,0.935,127.967,1.043,128.055,1.116,128.117,1.127,128.161,1.158,128.153,1.238,128.157,1.317,128.222,1.401,128.424,1.518,128.539,1.559,128.688,1.573,128.705,1.528,128.688,1.464,128.717,1.367,128.703,1.106,128.669,1.069,128.515,0.979,128.346,0.907,128.299,0.877,128.257,0.805,128.261,0.734,128.398,0.639,128.611,0.55,128.655,0.508,128.684,0.438,128.692,0.36,128.743,0.323,128.815,0.305,128.863,0.268,128.9,0.216,128.54,0.338,128.446,0.392,128.333,0.398,128.221,0.414,128.106,0.461,127.983,0.472,127.924,0.438,127.901,0.372,127.887,0.298,127.915,0.206,127.912,0.151,127.889,0.05,127.978,-0.248,128.089,-0.485,128.254,-0.732,128.335,-0.816,128.425,-0.893,128.278,-0.87,128.233,-0.788,128.046,-0.706,128.011,-0.657,127.889,-0.424,127.853,-0.38,127.741,-0.3,127.692,-0.242,127.675,-0.163,127.687,-0.08,127.681,0.035,127.685,0.149,127.709,0.288,127.669,0.337,127.616,0.383,127.555,0.49,127.537,0.611,127.542,0.681,127.567,0.743,127.601,0.796,127.608,0.848,127.52,0.924,127.429,1.14,127.42,1.252,127.537,1.467,127.535,1.572,127.558,1.634,127.571,1.7,127.632,1.844,127.731,1.966,127.9,2.137,127.964,2.175,128.036,2.199,128.043,2.157,128.031,2.12,127.907,1.946,127.89,1.906,127.887,1.833,127.946,1.79,128.011,1.701,128.024,1.583,128.026,1.458,128.012,1.332,127.988,1.29,127.885,1.163,127.653,1.014,127.633,0.977,127.634,0.936,127.677,0.887,127.733,0.848]],[[129.755,-2.866,129.984,-2.977,130.103,-2.993,130.304,-2.979,130.379,-2.989,130.57,-3.131,130.626,-3.228,130.642,-3.312,130.671,-3.392,130.718,-3.411,130.773,-3.419,130.846,-3.533,130.86,-3.57,130.805,-3.858,130.58,-3.749,130.363,-3.625,130.27,-3.579,130.02,-3.475,129.981,-3.439,129.953,-3.392,129.844,-3.327,129.627,-3.317,129.545,-3.319,129.512,-3.329,129.52,-3.363,129.522,-3.434,129.468,-3.453,129.333,-3.409,129.212,-3.393,129.108,-3.349,128.967,-3.326,128.952,-3.304,128.964,-3.272,128.958,-3.241,128.925,-3.229,128.863,-3.235,128.802,-3.266,128.751,-3.3,128.677,-3.397,128.639,-3.433,128.517,-3.449,128.466,-3.44,128.419,-3.416,128.28,-3.241,128.233,-3.203,128.181,-3.172,128.132,-3.157,128.082,-3.184,128.056,-3.239,128.044,-3.303,128.03,-3.341,127.97,-3.444,127.92,-3.506,127.902,-3.496,127.928,-3.397,127.928,-3.341,127.897,-3.282,127.878,-3.222,128.113,-2.935,128.199,-2.866,128.57,-2.842,128.791,-2.857,128.911,-2.85,128.991,-2.829,129.058,-2.838,129.074,-2.895,129.116,-2.937,129.174,-2.933,129.28,-2.889,129.371,-2.821,129.427,-2.791,129.484,-2.786,129.543,-2.79,129.6,-2.806,129.755,-2.866]],[[126.861,-3.088,127.025,-3.166,127.063,-3.217,127.092,-3.278,127.125,-3.311,127.163,-3.338,127.227,-3.391,127.244,-3.471,127.23,-3.633,127.155,-3.647,127.085,-3.671,126.941,-3.765,126.87,-3.783,126.794,-3.789,126.74,-3.814,126.686,-3.824,126.547,-3.772,126.411,-3.711,126.215,-3.605,126.178,-3.579,126.147,-3.523,126.057,-3.421,126.034,-3.356,126.026,-3.171,126.05,-3.128,126.088,-3.105,126.22,-3.148,126.306,-3.103,126.555,-3.065,126.808,-3.069,126.861,-3.088]],[[124.922,-8.942,124.915,-9.032,124.937,-9.053,124.973,-9.064,125.1,-9.004,125.124,-9.015,125.149,-9.043,125.149,-9.123,125.1,-9.19,124.978,-9.195,124.96,-9.214,124.959,-9.255,124.968,-9.294,124.997,-9.326,125.034,-9.382,125.068,-9.512,124.998,-9.565,124.963,-9.666,124.842,-9.76,124.708,-9.914,124.602,-9.993,124.508,-10.086,124.428,-10.149,124.327,-10.17,124.176,-10.183,123.971,-10.295,123.858,-10.344,123.747,-10.347,123.644,-10.311,123.605,-10.27,123.614,-10.215,123.648,-10.168,123.69,-10.129,123.716,-10.079,123.599,-10.015,123.589,-9.967,123.636,-9.838,123.666,-9.705,123.709,-9.615,123.877,-9.453,123.977,-9.373,124.036,-9.342,124.052,-9.375,124.09,-9.416,124.116,-9.423,124.135,-9.414,124.282,-9.428,124.319,-9.414,124.376,-9.35,124.413,-9.314,124.438,-9.239,124.444,-9.19,124.575,-9.155,124.646,-9.117,124.708,-9.062,124.89,-8.968,124.922,-8.942]],[[134.747,-5.707,134.739,-5.746,134.738,-5.817,134.755,-5.883,134.712,-5.95,134.752,-6.05,134.758,-6.1,134.756,-6.171,134.744,-6.202,134.714,-6.295,134.684,-6.328,134.661,-6.337,134.638,-6.365,134.441,-6.335,134.356,-6.271,134.28,-6.201,134.264,-6.172,134.175,-6.09,134.155,-6.063,134.153,-6.02,134.225,-6.008,134.302,-6.01,134.299,-5.971,134.343,-5.833,134.226,-5.744,134.205,-5.707,134.247,-5.682,134.341,-5.713,134.456,-5.558,134.49,-5.525,134.506,-5.438,134.571,-5.427,134.617,-5.439,134.646,-5.492,134.658,-5.539,134.646,-5.581,134.701,-5.603,134.747,-5.707]],[[134.537,-6.442,134.52,-6.513,134.504,-6.591,134.413,-6.68,134.356,-6.815,134.323,-6.849,134.2,-6.909,134.091,-6.834,134.059,-6.769,134.107,-6.472,134.154,-6.481,134.185,-6.479,134.195,-6.46,134.125,-6.426,134.111,-6.255,134.115,-6.191,134.168,-6.176,134.234,-6.226,134.318,-6.316,134.415,-6.387,134.537,-6.442]],[[138.535,-8.274,138.296,-8.405,137.983,-8.382,137.872,-8.38,137.688,-8.412,137.65,-8.386,137.685,-8.262,137.833,-7.932,138.008,-7.642,138.082,-7.566,138.185,-7.495,138.296,-7.438,138.544,-7.38,138.77,-7.39,138.802,-7.415,138.899,-7.512,138.963,-7.588,138.989,-7.696,138.893,-7.882,138.786,-8.059,138.612,-8.198,138.535,-8.274]],[[140.973,-2.61,140.973,-2.614,140.973,-2.681,140.974,-2.803,140.974,-3.007,140.974,-3.21,140.974,-3.413,140.974,-3.617,140.974,-3.82,140.974,-4.023,140.974,-4.226,140.974,-4.43,140.974,-4.633,140.974,-4.836,140.975,-5.04,140.975,-5.243,140.975,-5.446,140.975,-5.65,140.975,-5.853,140.975,-6.056,140.975,-6.259,140.975,-6.346,140.944,-6.452,140.875,-6.612,140.862,-6.74,140.92,-6.84,140.975,-6.905,140.975,-7.073,140.975,-7.276,140.975,-7.479,140.975,-7.683,140.976,-7.886,140.976,-8.089,140.976,-8.292,140.976,-8.496,140.976,-8.699,140.976,-8.902,140.976,-9.106,140.976,-9.119,140.925,-9.085,140.787,-8.974,140.662,-8.847,140.581,-8.728,140.49,-8.62,140.102,-8.301,140.003,-8.196,139.983,-8.167,139.993,-8.139,140.037,-8.084,140.117,-7.924,140.034,-8.023,139.935,-8.101,139.791,-8.106,139.649,-8.125,139.519,-8.173,139.386,-8.189,139.319,-8.166,139.279,-8.107,139.258,-8.047,139.249,-7.982,139.193,-8.086,139.083,-8.143,138.933,-8.262,138.891,-8.238,138.865,-8.192,138.856,-8.145,138.885,-8.095,138.905,-8.041,138.936,-7.913,139.003,-7.838,139.046,-7.691,139.074,-7.639,139.088,-7.587,139.049,-7.528,138.983,-7.508,138.938,-7.472,138.886,-7.373,138.853,-7.34,138.794,-7.299,138.748,-7.251,138.798,-7.216,138.865,-7.201,138.919,-7.204,139.018,-7.226,139.063,-7.227,139.177,-7.19,139.113,-7.202,139.049,-7.201,138.846,-7.136,138.72,-7.07,138.601,-6.937,138.6,-6.911,138.684,-6.887,138.865,-6.858,138.808,-6.79,138.727,-6.731,138.698,-6.626,138.642,-6.56,138.522,-6.454,138.439,-6.343,138.368,-6.119,138.296,-5.949,138.314,-5.887,138.375,-5.844,138.283,-5.839,138.2,-5.807,138.244,-5.724,138.34,-5.676,138.252,-5.688,138.167,-5.712,138.127,-5.717,138.087,-5.709,138.066,-5.676,138.063,-5.629,138.076,-5.546,138.061,-5.465,137.985,-5.428,137.922,-5.37,137.887,-5.349,137.84,-5.35,137.795,-5.312,137.759,-5.256,137.307,-5.014,137.28,-4.945,137.238,-4.976,137.196,-4.99,137.144,-4.951,137.089,-4.924,137.03,-4.929,136.975,-4.907,136.917,-4.895,136.857,-4.893,136.619,-4.819,136.394,-4.701,136.211,-4.651,136.097,-4.585,135.98,-4.531,135.717,-4.478,135.45,-4.443,135.354,-4.442,135.273,-4.453,135.196,-4.451,134.754,-4.195,134.68,-4.079,134.687,-4.011,134.707,-3.955,134.887,-3.938,134.76,-3.922,134.708,-3.93,134.603,-3.976,134.547,-3.979,134.467,-3.949,134.391,-3.91,134.266,-3.946,134.202,-3.887,134.18,-3.825,134.147,-3.797,134.1,-3.8,134.037,-3.822,133.974,-3.818,133.933,-3.776,133.904,-3.72,133.861,-3.68,133.808,-3.65,133.723,-3.578,133.678,-3.479,133.683,-3.309,133.697,-3.248,133.782,-3.149,133.842,-3.055,133.767,-3.044,133.7,-3.087,133.672,-3.132,133.661,-3.186,133.653,-3.364,133.599,-3.416,133.518,-3.412,133.542,-3.516,133.509,-3.616,133.415,-3.732,133.407,-3.785,133.422,-3.843,133.401,-3.899,133.249,-4.062,133.198,-4.07,133.085,-4.069,132.969,-4.095,132.914,-4.057,132.87,-4.007,132.837,-3.949,132.791,-3.828,132.754,-3.704,132.87,-3.551,132.83,-3.413,132.751,-3.295,132.554,-3.131,132.348,-2.975,132.255,-2.943,132.102,-2.93,132.054,-2.915,132.006,-2.856,131.971,-2.789,132.067,-2.76,132.231,-2.68,132.323,-2.684,132.575,-2.727,132.653,-2.766,132.725,-2.789,132.897,-2.658,133.034,-2.487,133.119,-2.45,133.191,-2.438,133.265,-2.454,133.411,-2.514,133.527,-2.542,133.609,-2.547,133.652,-2.601,133.7,-2.625,133.711,-2.544,133.753,-2.451,133.835,-2.422,133.878,-2.415,133.905,-2.391,133.899,-2.304,133.791,-2.294,133.85,-2.22,133.902,-2.184,133.921,-2.147,133.922,-2.102,133.71,-2.189,133.488,-2.226,133.356,-2.216,133.225,-2.214,132.963,-2.273,132.863,-2.27,132.631,-2.247,132.503,-2.218,132.403,-2.24,132.308,-2.242,132.207,-2.176,132.122,-2.092,132.08,-2.033,132.023,-1.99,131.998,-1.933,131.936,-1.715,131.93,-1.56,131.83,-1.557,131.731,-1.541,131.294,-1.393,131.241,-1.43,131.179,-1.448,131.118,-1.455,131.057,-1.448,130.996,-1.425,131.001,-1.384,131.046,-1.284,131.091,-1.247,131.152,-1.219,131.191,-1.166,131.254,-1.007,131.259,-0.953,131.252,-0.897,131.257,-0.855,131.296,-0.834,131.462,-0.782,131.804,-0.704,131.891,-0.657,131.962,-0.582,132.046,-0.537,132.084,-0.491,132.128,-0.454,132.394,-0.355,132.508,-0.347,132.625,-0.359,132.856,-0.417,133.077,-0.512,133.268,-0.636,133.473,-0.726,133.724,-0.741,133.85,-0.731,133.975,-0.744,134.025,-0.77,134.112,-0.847,134.087,-0.897,134.072,-1.002,134.116,-1.102,134.188,-1.203,134.247,-1.311,134.26,-1.363,134.237,-1.474,134.217,-1.529,134.145,-1.621,134.106,-1.721,134.131,-1.845,134.145,-1.969,134.143,-2.083,134.156,-2.195,134.195,-2.309,134.362,-2.621,134.46,-2.832,134.491,-2.714,134.483,-2.583,134.518,-2.536,134.567,-2.51,134.627,-2.537,134.645,-2.59,134.649,-2.706,134.702,-2.934,134.77,-2.944,134.843,-2.909,134.855,-2.979,134.853,-3.108,134.887,-3.21,134.917,-3.25,135.037,-3.333,135.092,-3.349,135.252,-3.369,135.372,-3.375,135.487,-3.345,135.561,-3.269,135.628,-3.186,135.859,-2.995,135.926,-2.904,135.991,-2.764,136.013,-2.734,136.243,-2.583,136.27,-2.529,136.303,-2.426,136.352,-2.325,136.39,-2.273,136.612,-2.224,136.843,-2.198,137.072,-2.105,137.171,-2.025,137.176,-1.973,137.125,-1.881,137.123,-1.841,137.176,-1.802,137.381,-1.686,137.617,-1.566,137.806,-1.483,137.911,-1.484,138.008,-1.557,138.111,-1.616,138.65,-1.791,138.736,-1.846,138.811,-1.918,138.919,-1.968,139.039,-1.992,139.149,-2.039,139.253,-2.099,139.482,-2.212,139.79,-2.348,139.868,-2.356,140.155,-2.35,140.204,-2.376,140.251,-2.412,140.295,-2.42,140.623,-2.446,140.673,-2.472,140.721,-2.508,140.747,-2.607,140.973,-2.61]],[[138.895,-8.389,138.846,-8.402,138.594,-8.371,138.567,-8.33,138.563,-8.309,138.621,-8.268,138.677,-8.199,138.763,-8.173,138.796,-8.174,138.898,-8.337,138.895,-8.389]],[[101.708,2.078,101.762,1.997,101.774,1.943,101.734,1.883,101.719,1.789,101.603,1.716,101.501,1.733,101.468,1.759,101.403,1.901,101.41,2.022,101.45,2.068,101.545,2.061,101.641,2.127,101.708,2.078]],[[102.427,0.99,102.381,0.96,102.325,1.007,102.28,1.076,102.255,1.147,102.234,1.264,102.229,1.348,102.256,1.397,102.276,1.395,102.359,1.346,102.413,1.261,102.443,1.234,102.449,1.156,102.429,1.067,102.427,0.99]],[[102.492,1.459,102.499,1.331,102.425,1.364,102.367,1.415,102.274,1.453,102.161,1.465,102.079,1.499,102.021,1.558,102.018,1.586,102.024,1.608,102.042,1.625,102.47,1.51,102.492,1.459]],[[103.028,0.747,103.009,0.708,102.971,0.737,102.776,0.78,102.711,0.784,102.542,0.832,102.49,0.857,102.454,0.89,102.466,0.95,102.491,0.987,102.507,1.089,102.549,1.13,102.633,1.054,102.726,0.989,102.78,0.959,102.944,0.893,103.002,0.859,103.028,0.747]],[[103.166,0.87,103.137,0.842,103.087,0.848,103.033,0.882,102.964,0.943,102.886,0.997,102.788,1.031,102.726,1.041,102.702,1.054,102.726,1.159,102.79,1.165,102.999,1.068,103.068,1.015,103.166,0.87]],[[103.284,0.542,103.172,0.536,103.14,0.549,103.153,0.643,103.187,0.7,103.238,0.699,103.295,0.614,103.284,0.542]],[[103.45,0.664,103.43,0.651,103.344,0.778,103.366,0.851,103.386,0.87,103.433,0.825,103.47,0.778,103.497,0.723,103.45,0.664]],[[104.025,1.181,104.088,1.137,104.14,1.166,104.138,1.128,104.127,1.092,104.066,0.99,103.964,1.013,103.94,1.046,103.932,1.071,103.947,1.087,103.955,1.137,104,1.137,104.025,1.181]],[[104.585,1.216,104.591,1.141,104.648,1.105,104.663,1.05,104.653,0.961,104.599,0.859,104.575,0.832,104.504,0.853,104.481,0.887,104.471,0.913,104.481,0.933,104.429,0.956,104.462,0.996,104.439,1.05,104.294,1.016,104.252,1.015,104.244,1.077,104.25,1.103,104.362,1.181,104.428,1.196,104.5,1.18,104.585,1.216]],[[104.779,-0.176,104.808,-0.192,104.843,-0.141,104.909,-0.212,104.95,-0.247,105.005,-0.283,104.951,-0.284,104.929,-0.317,104.914,-0.323,104.702,-0.209,104.567,-0.246,104.474,-0.212,104.447,-0.189,104.497,-0.126,104.543,0.018,104.636,-0.018,104.658,-0.063,104.653,-0.076,104.713,-0.103,104.779,-0.176]],[[104.474,-0.335,104.568,-0.432,104.59,-0.467,104.544,-0.521,104.507,-0.597,104.485,-0.613,104.414,-0.584,104.363,-0.659,104.33,-0.539,104.257,-0.463,104.302,-0.386,104.319,-0.38,104.341,-0.383,104.364,-0.403,104.474,-0.335]],[[103.737,-0.348,103.606,-0.383,103.461,-0.358,103.479,-0.297,103.549,-0.228,103.611,-0.231,103.724,-0.277,103.764,-0.318,103.737,-0.348]],[[109.71,-1.181,109.511,-1.283,109.464,-1.278,109.428,-1.241,109.45,-1.044,109.476,-0.985,109.615,-0.979,109.7,-1.007,109.743,-1.039,109.761,-1.105,109.751,-1.145,109.71,-1.181]],[[113.845,-7.105,113.826,-7.12,113.656,-7.112,113.546,-7.193,113.471,-7.218,113.198,-7.218,113.166,-7.207,113.142,-7.208,113.127,-7.224,113.04,-7.212,112.764,-7.14,112.726,-7.073,112.769,-7.001,112.868,-6.9,113.067,-6.88,113.975,-6.873,114.074,-6.96,114.083,-6.989,113.885,-7.049,113.845,-7.105]],[[134.965,-1.116,134.917,-1.134,134.862,-1.114,134.809,-1.038,134.828,-0.979,134.889,-0.938,134.941,-0.979,134.957,-1.031,134.996,-1.034,134.965,-1.116]],[[127.25,-0.495,127.187,-0.521,127.119,-0.521,127.104,-0.414,127.126,-0.279,127.19,-0.256,127.29,-0.284,127.253,-0.319,127.281,-0.391,127.25,-0.495]],[[127.567,-0.319,127.682,-0.468,127.605,-0.61,127.659,-0.689,127.804,-0.694,127.838,-0.724,127.863,-0.76,127.88,-0.809,127.842,-0.848,127.761,-0.884,127.668,-0.832,127.643,-0.784,127.624,-0.766,127.498,-0.802,127.463,-0.806,127.438,-0.739,127.469,-0.643,127.381,-0.6,127.3,-0.5,127.297,-0.46,127.329,-0.391,127.325,-0.336,127.371,-0.332,127.455,-0.406,127.492,-0.336,127.527,-0.307,127.567,-0.319]],[[132.926,-5.902,132.845,-5.988,132.922,-5.785,132.938,-5.683,133.009,-5.621,133.115,-5.311,133.138,-5.318,133.173,-5.348,133.12,-5.576,132.971,-5.736,132.926,-5.902]],[[132.807,-5.851,132.746,-5.947,132.705,-5.913,132.681,-5.913,132.667,-5.856,132.681,-5.739,132.63,-5.607,132.698,-5.609,132.717,-5.648,132.738,-5.662,132.804,-5.789,132.807,-5.851]],[[128.276,-3.675,128.25,-3.711,128.192,-3.735,128.143,-3.733,128.159,-3.698,128.147,-3.677,128.111,-3.686,128.052,-3.715,127.978,-3.771,127.934,-3.743,127.925,-3.699,127.928,-3.679,128.016,-3.601,128.119,-3.587,128.264,-3.512,128.329,-3.516,128.314,-3.564,128.291,-3.598,128.277,-3.633,128.276,-3.675]],[[126.817,4.033,126.776,4.013,126.711,4.02,126.704,4.071,126.77,4.162,126.814,4.258,126.767,4.283,126.722,4.344,126.721,4.416,126.757,4.548,126.813,4.537,126.865,4.48,126.887,4.373,126.921,4.291,126.848,4.18,126.817,4.033]],[[125.658,3.436,125.633,3.405,125.512,3.461,125.518,3.55,125.501,3.593,125.469,3.639,125.455,3.684,125.469,3.733,125.543,3.67,125.586,3.571,125.644,3.477,125.658,3.436]],[[130.862,-8.319,130.775,-8.35,130.833,-8.271,131.02,-8.091,131.087,-8.125,131.176,-8.131,131.044,-8.212,130.908,-8.246,130.862,-8.319]],[[129.839,-7.955,129.78,-8.046,129.713,-8.041,129.592,-7.917,129.599,-7.831,129.609,-7.803,129.655,-7.795,129.813,-7.82,129.844,-7.889,129.839,-7.955]],[[127.823,-8.099,127.998,-8.139,128.099,-8.135,128.119,-8.171,128.024,-8.255,127.821,-8.19,127.786,-8.12,127.823,-8.099]],[[130.353,-1.691,130.365,-1.75,130.425,-1.805,130.404,-1.89,130.381,-1.903,130.393,-1.942,130.419,-1.971,130.373,-1.992,130.339,-1.982,130.284,-2.009,130.248,-2.048,130.133,-2.064,130.093,-2.028,129.887,-1.986,129.754,-1.894,129.738,-1.867,129.994,-1.759,130.106,-1.73,130.2,-1.732,130.318,-1.692,130.353,-1.691]],[[102.367,-5.479,102.286,-5.483,102.136,-5.361,102.111,-5.323,102.154,-5.286,102.198,-5.289,102.372,-5.366,102.405,-5.405,102.367,-5.479]],[[100.425,-3.183,100.465,-3.329,100.346,-3.229,100.348,-3.159,100.332,-3.113,100.26,-3.057,100.204,-2.987,100.179,-2.82,100.199,-2.786,100.246,-2.783,100.455,-3.002,100.469,-3.039,100.464,-3.117,100.434,-3.141,100.425,-3.183]],[[100.204,-2.741,100.133,-2.821,100.015,-2.82,99.992,-2.77,99.997,-2.649,99.968,-2.61,99.969,-2.594,99.988,-2.525,100.012,-2.51,100.202,-2.68,100.204,-2.741]],[[99.843,-2.343,99.848,-2.37,99.685,-2.282,99.607,-2.258,99.537,-2.162,99.559,-2.115,99.562,-2.051,99.572,-2.026,99.622,-2.017,99.686,-2.063,99.735,-2.178,99.816,-2.284,99.843,-2.343]],[[98.459,-0.53,98.4,-0.577,98.31,-0.532,98.34,-0.468,98.355,-0.379,98.409,-0.309,98.427,-0.226,98.323,-0.001,98.375,0.007,98.415,-0.018,98.484,-0.168,98.544,-0.258,98.52,-0.38,98.459,-0.53]],[[96.464,2.36,96.401,2.351,96.341,2.372,96.29,2.43,96.022,2.596,95.938,2.598,95.88,2.641,95.809,2.656,95.733,2.767,95.717,2.826,95.772,2.855,95.806,2.916,95.896,2.889,95.998,2.781,96.102,2.741,96.13,2.721,96.18,2.661,96.417,2.515,96.443,2.466,96.459,2.416,96.464,2.36]],[[122.043,-5.438,121.98,-5.465,121.859,-5.35,121.808,-5.256,121.821,-5.203,121.857,-5.156,121.874,-5.145,121.866,-5.096,121.914,-5.072,121.966,-5.076,122,-5.141,122.041,-5.159,122.062,-5.221,122.043,-5.438]],[[123.212,-1.171,123.234,-1.234,123.198,-1.288,123.238,-1.389,123.339,-1.254,123.435,-1.237,123.489,-1.259,123.527,-1.286,123.547,-1.337,123.512,-1.447,123.449,-1.499,123.367,-1.507,123.329,-1.443,123.275,-1.437,123.237,-1.577,123.221,-1.598,123.173,-1.616,123.13,-1.577,123.123,-1.556,123.183,-1.493,123.15,-1.304,123.105,-1.34,122.984,-1.511,122.89,-1.587,122.858,-1.548,122.811,-1.432,122.832,-1.283,122.908,-1.182,122.972,-1.189,123.158,-1.158,123.212,-1.171]],[[121.864,-0.407,121.907,-0.451,121.881,-0.503,121.847,-0.49,121.756,-0.491,121.722,-0.495,121.681,-0.525,121.655,-0.526,121.672,-0.479,121.749,-0.407,121.797,-0.418,121.864,-0.407]],[[120.528,-6.298,120.487,-6.465,120.468,-6.406,120.461,-6.254,120.436,-6.18,120.452,-6.095,120.446,-5.876,120.477,-5.775,120.534,-5.904,120.549,-5.969,120.528,-6.298]],[[115.377,-6.971,115.296,-6.988,115.22,-6.953,115.222,-6.905,115.241,-6.861,115.354,-6.838,115.414,-6.84,115.479,-6.87,115.524,-6.902,115.546,-6.939,115.424,-6.941,115.377,-6.971]],[[116.303,-3.868,116.093,-4.054,116.059,-4.007,116.077,-3.817,116.018,-3.7,116.022,-3.612,116.064,-3.458,116.117,-3.34,116.239,-3.26,116.27,-3.251,116.262,-3.395,116.287,-3.449,116.295,-3.495,116.282,-3.535,116.305,-3.719,116.319,-3.763,116.289,-3.821,116.303,-3.868]],[[122.949,-10.909,122.856,-10.91,122.826,-10.899,122.818,-10.811,122.846,-10.762,123.061,-10.698,123.146,-10.64,123.265,-10.518,123.34,-10.486,123.358,-10.472,123.371,-10.475,123.383,-10.568,123.413,-10.623,123.418,-10.651,123.311,-10.698,123.215,-10.806,123.005,-10.876,122.949,-10.909]],[[124.287,-8.329,124.226,-8.391,124.184,-8.499,124.147,-8.531,124.066,-8.552,124.017,-8.444,123.928,-8.449,123.971,-8.354,124.014,-8.319,124.069,-8.318,124.096,-8.356,124.111,-8.364,124.24,-8.203,124.266,-8.202,124.287,-8.209,124.304,-8.229,124.287,-8.329]],[[123.925,-8.272,123.784,-8.3,123.698,-8.424,123.629,-8.422,123.592,-8.478,123.583,-8.502,123.588,-8.524,123.58,-8.545,123.553,-8.567,123.489,-8.532,123.434,-8.576,123.411,-8.587,123.33,-8.536,123.253,-8.539,123.23,-8.531,123.325,-8.439,123.455,-8.354,123.476,-8.322,123.425,-8.313,123.395,-8.301,123.391,-8.28,123.473,-8.267,123.53,-8.265,123.573,-8.292,123.601,-8.291,123.776,-8.19,123.846,-8.213,123.896,-8.239,123.925,-8.272]],[[123.317,-8.355,123.297,-8.399,123.025,-8.396,123.033,-8.338,123.108,-8.275,123.133,-8.254,123.217,-8.235,123.336,-8.269,123.317,-8.355]],[[134.716,-6.549,134.661,-6.559,134.634,-6.477,134.679,-6.456,134.729,-6.506,134.716,-6.549]],[[103.424,1.048,103.43,0.993,103.363,1.007,103.315,1.071,103.355,1.117,103.38,1.134,103.405,1.073,103.424,1.048]],[[103.829,0.801,103.834,0.772,103.742,0.83,103.74,0.872,103.752,0.891,103.807,0.846,103.829,0.801]],[[104.239,0.834,104.177,0.805,104.098,0.896,104.101,0.917,104.108,0.934,104.123,0.944,104.171,0.897,104.227,0.88,104.239,0.834]],[[104.689,0.06,104.698,0.035,104.651,0.063,104.622,0.08,104.604,0.095,104.499,0.232,104.544,0.223,104.66,0.103,104.689,0.06]],[[106.285,3.157,106.284,3.088,106.215,3.129,106.201,3.205,106.224,3.23,106.271,3.216,106.285,3.157]],[[105.76,2.863,105.719,2.859,105.706,2.889,105.708,2.94,105.704,2.981,105.692,3.011,105.692,3.063,105.731,3.037,105.76,3.013,105.795,2.996,105.822,2.984,105.837,2.977,105.809,2.904,105.76,2.863]],[[108.888,2.905,108.839,2.853,108.787,2.886,108.867,2.992,108.886,2.999,108.888,2.905]],[[108.953,-1.62,108.838,-1.662,108.804,-1.568,108.877,-1.54,108.957,-1.564,108.953,-1.62]],[[107.473,-2.9,107.433,-2.925,107.409,-2.901,107.402,-2.873,107.419,-2.838,107.474,-2.835,107.5,-2.845,107.473,-2.9]],[[106.886,-3.005,106.87,-3.025,106.814,-3.014,106.774,-2.987,106.749,-2.96,106.743,-2.933,106.797,-2.899,106.911,-2.934,106.886,-3.005]],[[114.413,-7.133,114.398,-7.173,114.347,-7.163,114.299,-7.098,114.322,-7.08,114.349,-7.073,114.384,-7.081,114.413,-7.133]],[[112.719,-5.811,112.698,-5.846,112.602,-5.844,112.586,-5.804,112.649,-5.731,112.69,-5.726,112.727,-5.753,112.719,-5.811]],[[105.253,-6.64,105.19,-6.662,105.143,-6.643,105.121,-6.615,105.192,-6.546,105.226,-6.529,105.261,-6.524,105.277,-6.561,105.253,-6.64]],[[97.334,2.076,97.328,2.053,97.225,2.158,97.108,2.217,97.157,2.232,97.253,2.216,97.291,2.201,97.329,2.149,97.334,2.076]],[[95.362,5.812,95.343,5.784,95.283,5.799,95.218,5.89,95.242,5.907,95.283,5.898,95.359,5.877,95.366,5.843,95.362,5.812]],[[123.598,-1.704,123.529,-1.711,123.483,-1.681,123.487,-1.535,123.529,-1.503,123.549,-1.508,123.561,-1.552,123.582,-1.591,123.616,-1.627,123.598,-1.704]],[[123.242,-4.113,123.145,-4.233,123.076,-4.227,122.995,-4.148,122.971,-4.061,122.969,-4.03,123.025,-3.981,123.212,-3.998,123.247,-4.041,123.242,-4.113]],[[123.848,-1.955,123.866,-1.996,123.804,-1.994,123.777,-1.919,123.783,-1.878,123.848,-1.955]],[[123.153,-1.817,123.079,-1.899,123.071,-1.855,123.086,-1.815,123.106,-1.787,123.138,-1.773,123.153,-1.817]],[[119.464,-8.741,119.425,-8.75,119.386,-8.736,119.402,-8.647,119.379,-8.587,119.42,-8.539,119.43,-8.455,119.446,-8.429,119.471,-8.456,119.482,-8.473,119.502,-8.481,119.547,-8.483,119.557,-8.519,119.555,-8.553,119.536,-8.589,119.483,-8.628,119.444,-8.672,119.464,-8.741]],[[119.074,-8.239,119.03,-8.24,119.021,-8.2,119.037,-8.158,119.079,-8.14,119.098,-8.139,119.128,-8.177,119.135,-8.197,119.107,-8.223,119.074,-8.239]],[[115.61,-8.77,115.582,-8.804,115.501,-8.743,115.48,-8.715,115.541,-8.675,115.561,-8.67,115.613,-8.713,115.61,-8.77]],[[117.649,4.169,117.745,4.167,117.885,4.186,117.918,4.091,117.923,4.054,117.737,4.004,117.625,4.121,117.649,4.169]],[[117.658,3.281,117.646,3.248,117.56,3.328,117.538,3.386,117.548,3.432,117.637,3.436,117.681,3.408,117.658,3.281]],[[124.051,-5.974,124.042,-6.022,124.006,-5.967,123.972,-5.939,123.976,-5.88,124.023,-5.902,124.051,-5.974]],[[123.627,-5.272,123.623,-5.373,123.583,-5.367,123.55,-5.332,123.541,-5.298,123.543,-5.271,123.561,-5.25,123.627,-5.272]],[[120.774,-7.119,120.672,-7.125,120.641,-7.116,120.633,-7.018,120.746,-7.06,120.782,-7.063,120.774,-7.119]],[[117.556,-8.367,117.534,-8.368,117.49,-8.349,117.506,-8.307,117.482,-8.239,117.491,-8.183,117.546,-8.152,117.665,-8.148,117.669,-8.189,117.556,-8.367]],[[116.424,-3.464,116.388,-3.637,116.327,-3.539,116.395,-3.423,116.427,-3.4,116.424,-3.464]],[[127.373,0.791,127.338,0.758,127.306,0.769,127.286,0.812,127.293,0.842,127.32,0.862,127.354,0.847,127.373,0.791]],[[134.374,-2.124,134.345,-2.139,134.335,-2.095,134.351,-2.037,134.37,-2.028,134.391,-2.031,134.419,-2.052,134.374,-2.124]],[[133.571,-4.246,133.622,-4.299,133.503,-4.257,133.333,-4.17,133.321,-4.111,133.464,-4.2,133.571,-4.246]],[[130.905,-0.777,130.88,-0.828,130.832,-0.863,130.402,-0.924,130.439,-0.887,130.457,-0.851,130.484,-0.833,130.527,-0.837,130.548,-0.826,130.57,-0.822,130.594,-0.827,130.635,-0.812,130.723,-0.822,130.813,-0.814,130.807,-0.765,130.905,-0.777]],[[130.627,-0.529,130.569,-0.53,130.465,-0.487,130.526,-0.449,130.564,-0.441,130.597,-0.418,130.616,-0.417,130.657,-0.437,130.684,-0.469,130.627,-0.529]],[[129.549,-0.187,129.506,-0.19,129.469,-0.131,129.37,-0.066,129.309,0.045,129.542,-0.139,129.549,-0.187]],[[127.453,-0.006,127.449,-0.037,127.418,0.006,127.397,0.017,127.42,0.124,127.431,0.143,127.449,0.069,127.453,-0.006]],[[127.42,0.642,127.384,0.631,127.374,0.635,127.363,0.675,127.383,0.744,127.425,0.744,127.443,0.733,127.446,0.683,127.42,0.642]],[[127.3,-0.781,127.289,-0.802,127.185,-0.775,127.156,-0.761,127.209,-0.619,127.258,-0.623,127.301,-0.758,127.3,-0.781]],[[128.722,-3.547,128.72,-3.589,128.713,-3.603,128.659,-3.588,128.62,-3.589,128.585,-3.512,128.595,-3.495,128.667,-3.517,128.694,-3.525,128.722,-3.547]],[[128.563,-3.585,128.392,-3.638,128.428,-3.54,128.452,-3.515,128.536,-3.541,128.563,-3.585]],[[126.719,3.875,126.722,3.833,126.661,3.928,126.638,4.042,126.686,4.001,126.74,3.918,126.719,3.875]],[[126.852,3.768,126.836,3.757,126.8,3.784,126.778,3.813,126.779,3.843,126.804,3.858,126.857,3.812,126.858,3.787,126.852,3.768]],[[125.407,2.652,125.397,2.63,125.36,2.747,125.391,2.805,125.435,2.784,125.446,2.763,125.404,2.707,125.407,2.652]],[[131.982,-7.202,131.97,-7.251,131.927,-7.225,131.884,-7.167,131.823,-7.159,131.778,-7.144,131.751,-7.117,131.922,-7.104,131.982,-7.202]],[[128.67,-7.183,128.625,-7.209,128.55,-7.156,128.53,-7.135,128.577,-7.083,128.628,-7.069,128.658,-7.091,128.673,-7.113,128.667,-7.138,128.67,-7.183]],[[127.419,-7.623,127.355,-7.646,127.375,-7.572,127.371,-7.513,127.475,-7.531,127.474,-7.579,127.464,-7.597,127.419,-7.623]],[[123.416,-10.303,123.326,-10.337,123.326,-10.264,123.395,-10.171,123.459,-10.14,123.494,-10.177,123.497,-10.194,123.405,-10.227,123.416,-10.303]],[[121.883,-10.59,121.833,-10.602,121.726,-10.573,121.705,-10.556,121.796,-10.507,121.867,-10.439,121.95,-10.433,121.998,-10.447,121.981,-10.528,121.883,-10.59]],[[134.82,-6.434,134.795,-6.442,134.795,-6.393,134.823,-6.35,134.852,-6.325,134.886,-6.324,134.82,-6.434]],[[134.674,-6.75,134.657,-6.765,134.631,-6.733,134.629,-6.713,134.663,-6.658,134.698,-6.626,134.736,-6.623,134.726,-6.669,134.674,-6.75]],[[127.988,-2.937,127.938,-3.02,127.85,-3.016,127.834,-3.004,127.938,-2.952,127.988,-2.937]],[[127.606,-3.315,127.629,-3.359,127.531,-3.331,127.488,-3.288,127.53,-3.262,127.554,-3.254,127.606,-3.315]],[[122.977,-8.545,122.946,-8.604,122.888,-8.587,122.904,-8.531,122.933,-8.497,123.011,-8.448,123.089,-8.44,123.138,-8.457,123.153,-8.476,123.03,-8.495,122.977,-8.545]]]},{"n":"India","c":"IN","r":2,"ml":1.7,"lx":79.36,"ly":22.69,"b":[68.165,6.749,97.344,35.496],"p":[[[68.165,23.857,68.234,23.901,68.283,23.928,68.381,23.951,68.489,23.967,68.587,23.967,68.724,23.965,68.728,24.266,68.74,24.292,68.759,24.307,68.781,24.314,68.8,24.309,68.828,24.264,68.863,24.267,68.901,24.292,68.985,24.273,69.052,24.286,69.12,24.269,69.235,24.268,69.443,24.275,69.559,24.273,69.634,24.225,69.716,24.173,69.805,24.165,69.934,24.171,70.021,24.192,70.065,24.241,70.098,24.288,70.289,24.356,70.489,24.412,70.547,24.418,70.565,24.386,70.556,24.331,70.579,24.279,70.659,24.246,70.716,24.238,70.767,24.245,70.805,24.262,70.886,24.344,70.928,24.362,70.983,24.361,71.044,24.4,71.045,24.43,71.006,24.444,70.973,24.487,70.979,24.522,70.97,24.572,70.976,24.619,71.002,24.654,71.048,24.688,71.021,24.758,70.951,24.892,70.878,25.063,70.8,25.206,70.703,25.331,70.652,25.423,70.657,25.626,70.648,25.667,70.615,25.692,70.57,25.706,70.506,25.685,70.449,25.681,70.325,25.686,70.265,25.707,70.1,25.91,70.079,25.99,70.078,26.072,70.133,26.215,70.149,26.348,70.157,26.471,70.148,26.506,70.115,26.548,70.059,26.579,69.911,26.586,69.736,26.627,69.601,26.699,69.507,26.743,69.481,26.771,69.47,26.804,69.495,26.954,69.537,27.123,69.568,27.175,69.622,27.228,69.661,27.265,69.725,27.313,69.896,27.474,70.05,27.695,70.145,27.849,70.194,27.895,70.244,27.934,70.318,27.982,70.404,28.025,70.489,28.023,70.569,27.984,70.629,27.937,70.649,27.835,70.692,27.769,70.737,27.729,70.798,27.71,70.875,27.714,71.185,27.832,71.29,27.855,71.543,27.87,71.717,27.915,71.87,27.963,71.889,28.047,71.948,28.177,72.129,28.346,72.179,28.422,72.234,28.566,72.292,28.697,72.342,28.752,72.626,28.896,72.903,29.029,72.949,29.089,73.128,29.364,73.231,29.551,73.258,29.611,73.317,29.773,73.382,29.934,73.467,29.972,73.658,30.033,73.809,30.093,73.887,30.162,73.933,30.222,73.925,30.282,73.883,30.352,73.892,30.394,73.899,30.435,74.009,30.52,74.216,30.769,74.339,30.894,74.38,30.893,74.51,30.96,74.633,31.035,74.626,31.069,74.61,31.113,74.54,31.133,74.518,31.186,74.535,31.261,74.594,31.465,74.582,31.524,74.51,31.713,74.526,31.765,74.556,31.819,74.636,31.89,74.739,31.949,75.071,32.089,75.139,32.105,75.254,32.14,75.325,32.215,75.333,32.279,75.303,32.319,75.234,32.372,75.104,32.42,74.987,32.462,74.789,32.458,74.686,32.494,74.658,32.519,74.643,32.608,74.663,32.758,74.632,32.771,74.588,32.753,74.483,32.771,74.355,32.769,74.305,32.81,74.33,32.861,74.323,32.928,74.304,32.992,74.284,33.005,74.222,33.02,74.126,33.075,74.049,33.143,74.004,33.189,73.99,33.221,73.994,33.242,74.05,33.301,74.118,33.384,74.143,33.455,74.15,33.507,74.131,33.545,74.07,33.592,74.004,33.632,73.978,33.668,73.976,33.721,74.001,33.788,74.078,33.839,74.216,33.887,74.251,33.946,74.246,33.99,74.209,34.003,74.113,34.004,73.95,34.019,73.922,34.043,73.904,34.076,73.904,34.108,73.938,34.145,73.979,34.191,73.972,34.237,73.925,34.288,73.81,34.325,73.795,34.378,73.812,34.422,73.85,34.485,73.883,34.529,73.961,34.653,74.056,34.681,74.172,34.721,74.3,34.765,74.498,34.732,74.594,34.716,74.789,34.678,74.952,34.646,75.118,34.637,75.188,34.639,75.264,34.601,75.453,34.537,75.606,34.503,75.709,34.503,75.862,34.56,75.938,34.613,76.041,34.67,76.172,34.668,76.457,34.756,76.51,34.741,76.594,34.736,76.696,34.787,76.749,34.848,76.758,34.878,76.783,34.9,76.892,34.939,77.001,34.992,77.031,35.062,77.049,35.11,77.169,35.172,77.293,35.236,77.423,35.303,77.572,35.379,77.697,35.443,77.799,35.496,77.803,35.493,77.811,35.485,77.852,35.461,77.895,35.449,77.946,35.472,78.009,35.49,78.043,35.48,78.047,35.449,78.009,35.307,78.012,35.251,78.076,35.135,78.158,34.946,78.236,34.77,78.282,34.654,78.327,34.606,78.516,34.558,78.671,34.518,78.763,34.453,78.865,34.39,78.936,34.352,78.97,34.303,78.977,34.258,78.971,34.228,78.932,34.189,78.753,34.088,78.732,34.056,78.727,34.013,78.762,33.888,78.784,33.809,78.79,33.65,78.802,33.5,78.865,33.431,78.917,33.387,78.948,33.347,79.013,33.291,79.067,33.25,79.113,33.226,79.135,33.172,79.122,33.108,79.103,33.053,79.109,33.023,79.146,33.001,79.202,32.946,79.21,32.865,79.206,32.809,79.228,32.759,79.234,32.703,79.217,32.564,79.219,32.508,79.219,32.501,79.17,32.497,79.127,32.476,79.067,32.388,78.998,32.365,78.919,32.358,78.838,32.412,78.771,32.468,78.754,32.499,78.737,32.558,78.701,32.597,78.632,32.579,78.526,32.571,78.413,32.558,78.392,32.545,78.39,32.52,78.417,32.467,78.441,32.397,78.455,32.3,78.486,32.236,78.496,32.216,78.678,32.023,78.726,31.984,78.735,31.958,78.72,31.888,78.687,31.806,78.693,31.74,78.754,31.668,78.803,31.618,78.755,31.55,78.727,31.472,78.759,31.437,78.744,31.324,78.758,31.302,78.792,31.294,78.845,31.302,78.9,31.331,78.946,31.337,78.974,31.329,79.011,31.414,79.044,31.426,79.107,31.403,79.233,31.242,79.339,31.106,79.37,31.08,79.388,31.064,79.493,30.994,79.565,30.949,79.664,30.965,79.795,30.968,79.872,30.925,79.917,30.894,79.919,30.89,79.925,30.889,80.081,30.782,80.149,30.79,80.194,30.759,80.207,30.684,80.186,30.605,80.191,30.568,80.261,30.561,80.41,30.509,80.541,30.464,80.609,30.449,80.682,30.415,80.747,30.36,80.874,30.291,80.985,30.237,81.01,30.165,80.966,30.18,80.908,30.172,80.848,30.14,80.82,30.119,80.684,29.994,80.613,29.956,80.549,29.9,80.402,29.73,80.317,29.572,80.255,29.423,80.256,29.318,80.233,29.195,80.17,29.124,80.13,29.1,80.085,28.994,80.052,28.87,80.071,28.83,80.15,28.776,80.227,28.723,80.325,28.666,80.419,28.612,80.479,28.605,80.496,28.636,80.518,28.665,80.587,28.65,80.671,28.596,80.726,28.554,80.751,28.54,80.896,28.469,81.017,28.41,81.169,28.335,81.206,28.289,81.239,28.241,81.311,28.176,81.486,28.062,81.636,27.98,81.757,27.914,81.853,27.867,81.897,27.874,81.945,27.899,81.988,27.914,82.037,27.901,82.112,27.865,82.288,27.757,82.451,27.672,82.63,27.687,82.677,27.673,82.711,27.597,82.733,27.519,82.933,27.468,83.064,27.445,83.214,27.402,83.29,27.371,83.369,27.41,83.384,27.445,83.447,27.465,83.552,27.456,83.747,27.396,83.829,27.378,83.897,27.435,84.025,27.462,84.091,27.491,84.23,27.428,84.481,27.348,84.61,27.299,84.641,27.25,84.655,27.204,84.654,27.092,84.685,27.041,84.937,26.927,85.02,26.879,85.087,26.863,85.125,26.861,85.152,26.847,85.174,26.782,85.192,26.767,85.24,26.75,85.293,26.741,85.456,26.797,85.568,26.84,85.648,26.829,85.7,26.782,85.707,26.713,85.737,26.64,85.795,26.604,85.856,26.6,86.007,26.649,86.129,26.612,86.242,26.598,86.366,26.574,86.414,26.556,86.544,26.496,86.701,26.435,86.763,26.442,87.016,26.555,87.038,26.542,87.09,26.433,87.167,26.394,87.287,26.36,87.414,26.423,87.513,26.405,87.633,26.399,87.749,26.429,87.849,26.437,87.995,26.382,88.027,26.395,88.055,26.43,88.112,26.586,88.162,26.725,88.157,26.807,88.111,26.928,87.993,27.086,87.984,27.134,88.024,27.409,88.068,27.567,88.106,27.642,88.147,27.749,88.154,27.799,88.15,27.843,88.11,27.871,88.099,27.905,88.109,27.933,88.141,27.949,88.275,27.969,88.426,28.012,88.486,28.034,88.532,28.057,88.578,28.093,88.621,28.092,88.756,28.04,88.804,28.007,88.829,27.907,88.849,27.869,88.83,27.767,88.749,27.522,88.765,27.43,88.833,27.363,88.891,27.316,88.882,27.297,88.76,27.218,88.739,27.176,88.766,27.134,88.814,27.099,88.835,27.066,88.858,26.961,88.919,26.932,89.041,26.865,89.148,26.816,89.332,26.849,89.384,26.827,89.475,26.803,89.545,26.796,89.586,26.779,89.609,26.762,89.606,26.741,89.61,26.719,89.711,26.714,89.764,26.702,89.943,26.724,90.123,26.755,90.206,26.848,90.242,26.854,90.346,26.89,90.448,26.851,90.56,26.797,90.62,26.78,90.74,26.772,90.856,26.778,91.134,26.803,91.287,26.79,91.427,26.867,91.456,26.867,91.518,26.807,91.672,26.802,91.754,26.831,91.842,26.853,91.899,26.86,91.944,26.861,91.998,26.855,92.05,26.875,92.073,26.915,92.068,26.975,92.031,27.041,91.999,27.079,91.992,27.1,92.003,27.147,92.031,27.214,92.083,27.291,92.045,27.365,91.991,27.45,91.951,27.458,91.851,27.439,91.743,27.443,91.658,27.494,91.595,27.558,91.579,27.611,91.598,27.677,91.626,27.737,91.632,27.76,91.713,27.76,91.825,27.746,91.909,27.73,91.978,27.73,92.101,27.808,92.158,27.812,92.222,27.827,92.25,27.842,92.27,27.83,92.341,27.821,92.415,27.825,92.481,27.846,92.547,27.879,92.664,27.949,92.688,27.989,92.688,28.026,92.666,28.05,92.643,28.062,92.653,28.093,92.702,28.147,92.882,28.228,93.035,28.328,93.119,28.402,93.158,28.493,93.207,28.591,93.252,28.629,93.361,28.654,93.665,28.69,93.761,28.73,93.902,28.803,93.974,28.861,94.013,28.908,94.018,28.96,94.112,28.976,94.193,29.06,94.293,29.145,94.468,29.216,94.623,29.312,94.677,29.297,94.733,29.252,94.763,29.201,94.769,29.176,94.967,29.144,94.999,29.149,95.145,29.104,95.279,29.05,95.353,29.036,95.389,29.037,95.42,29.054,95.457,29.102,95.494,29.137,95.517,29.151,95.516,29.206,95.71,29.314,95.885,29.391,96.035,29.447,96.08,29.424,96.129,29.381,96.195,29.272,96.235,29.246,96.337,29.261,96.356,29.249,96.34,29.21,96.271,29.161,96.181,29.118,96.122,29.082,96.141,28.963,96.137,28.923,96.162,28.91,96.347,29.027,96.436,29.051,96.467,29.022,96.477,28.959,96.55,28.83,96.581,28.764,96.396,28.607,96.327,28.525,96.33,28.497,96.326,28.469,96.279,28.428,96.281,28.412,96.32,28.387,96.366,28.367,96.389,28.368,96.428,28.406,96.603,28.46,96.653,28.45,96.776,28.367,96.833,28.362,96.981,28.338,97.075,28.369,97.145,28.34,97.289,28.237,97.322,28.218,97.31,28.155,97.303,28.086,97.339,28.031,97.344,27.982,97.335,27.938,97.306,27.907,97.226,27.89,97.158,27.837,97.05,27.76,96.963,27.698,96.9,27.644,96.877,27.587,96.884,27.515,96.902,27.44,97.104,27.163,97.102,27.115,97.038,27.102,96.953,27.133,96.88,27.178,96.798,27.296,96.732,27.331,96.666,27.339,96.274,27.278,96.191,27.261,96.061,27.217,95.971,27.128,95.905,27.047,95.837,27.014,95.738,26.95,95.464,26.756,95.305,26.672,95.201,26.641,95.129,26.597,95.089,26.525,95.06,26.474,95.051,26.347,95.069,26.191,95.108,26.091,95.129,26.07,95.132,26.041,95.093,25.987,95.041,25.941,95.015,25.913,94.992,25.77,94.946,25.7,94.861,25.597,94.786,25.519,94.668,25.459,94.623,25.41,94.58,25.32,94.554,25.243,94.553,25.216,94.567,25.192,94.616,25.165,94.675,25.139,94.704,25.098,94.708,25.049,94.663,24.931,94.584,24.767,94.493,24.638,94.399,24.514,94.377,24.474,94.293,24.322,94.22,24.113,94.17,23.973,94.128,23.876,94.075,23.872,94.011,23.903,93.855,23.944,93.756,23.977,93.683,24.007,93.633,24.005,93.564,23.986,93.494,23.973,93.452,23.987,93.356,24.074,93.326,24.064,93.307,24.022,93.373,23.774,93.415,23.682,93.408,23.528,93.391,23.339,93.366,23.133,93.349,23.085,93.308,23.03,93.254,23.015,93.204,23.037,93.164,23.032,93.151,22.997,93.163,22.908,93.114,22.806,93.079,22.718,93.088,22.633,93.105,22.547,93.162,22.36,93.162,22.292,93.151,22.231,93.121,22.205,93.071,22.209,93.043,22.184,93.022,22.146,92.965,22.004,92.909,21.989,92.854,22.01,92.771,22.105,92.721,22.132,92.689,22.131,92.675,22.106,92.653,22.049,92.63,22.011,92.575,21.978,92.561,22.048,92.532,22.41,92.51,22.526,92.491,22.685,92.464,22.734,92.43,22.822,92.393,22.897,92.362,22.929,92.341,23.07,92.334,23.242,92.334,23.324,92.289,23.492,92.246,23.684,92.187,23.676,92.152,23.722,92.127,23.721,92.044,23.678,91.979,23.692,91.93,23.686,91.929,23.598,91.938,23.505,91.919,23.471,91.79,23.361,91.754,23.287,91.758,23.21,91.774,23.106,91.751,23.054,91.695,23.005,91.62,22.98,91.554,22.992,91.511,23.034,91.471,23.141,91.436,23.2,91.399,23.214,91.371,23.198,91.367,23.13,91.369,23.075,91.359,23.068,91.339,23.077,91.315,23.104,91.254,23.374,91.166,23.581,91.16,23.661,91.192,23.763,91.232,23.92,91.336,24.019,91.35,24.06,91.367,24.094,91.393,24.1,91.526,24.091,91.571,24.107,91.611,24.153,91.669,24.19,91.727,24.205,91.772,24.211,91.846,24.175,91.877,24.195,91.899,24.261,91.931,24.326,91.952,24.357,92.001,24.371,92.064,24.374,92.085,24.386,92.102,24.408,92.117,24.494,92.198,24.686,92.227,24.771,92.231,24.786,92.228,24.881,92.251,24.895,92.385,24.849,92.443,24.849,92.475,24.869,92.485,24.903,92.468,24.944,92.373,25.015,92.205,25.111,92.05,25.169,91.763,25.161,91.48,25.142,91.397,25.152,91.293,25.178,91.038,25.174,90.73,25.159,90.613,25.168,90.555,25.167,90.439,25.158,90.25,25.185,90.12,25.22,90.004,25.258,89.866,25.293,89.833,25.293,89.814,25.305,89.801,25.336,89.796,25.376,89.825,25.56,89.8,25.84,89.823,25.941,89.71,26.171,89.671,26.214,89.619,26.216,89.586,26.186,89.573,26.132,89.591,26.072,89.55,26.005,89.467,25.984,89.37,26.006,89.289,26.038,89.186,26.106,89.108,26.202,89.102,26.308,89.067,26.377,89.019,26.41,88.983,26.42,88.952,26.412,88.924,26.375,88.948,26.338,88.982,26.286,88.97,26.251,88.941,26.245,88.896,26.26,88.828,26.252,88.762,26.279,88.722,26.282,88.683,26.292,88.681,26.353,88.62,26.431,88.518,26.518,88.418,26.572,88.37,26.564,88.346,26.505,88.351,26.483,88.386,26.472,88.437,26.437,88.448,26.401,88.44,26.369,88.378,26.312,88.334,26.258,88.235,26.178,88.151,26.087,88.129,26.018,88.097,25.956,88.085,25.888,88.107,25.841,88.147,25.811,88.253,25.79,88.363,25.698,88.452,25.574,88.502,25.537,88.593,25.495,88.769,25.49,88.795,25.456,88.82,25.366,88.855,25.334,88.944,25.291,88.952,25.259,88.93,25.223,88.89,25.194,88.817,25.176,88.748,25.169,88.678,25.18,88.574,25.188,88.456,25.188,88.373,24.962,88.313,24.882,88.279,24.882,88.189,24.921,88.15,24.915,88.045,24.713,88.03,24.664,88.023,24.628,88.079,24.55,88.146,24.486,88.225,24.461,88.287,24.48,88.338,24.454,88.397,24.389,88.499,24.347,88.642,24.326,88.724,24.275,88.734,24.231,88.727,24.186,88.714,24.07,88.7,24.003,88.623,23.826,88.567,23.674,88.596,23.602,88.616,23.573,88.636,23.55,88.698,23.493,88.741,23.437,88.704,23.293,88.724,23.255,88.808,23.23,88.897,23.21,88.928,23.187,88.851,23.041,88.867,22.939,88.9,22.844,88.923,22.688,88.927,22.671,88.921,22.632,88.971,22.511,89.05,22.275,89.056,22.186,89.051,22.093,89.028,21.937,88.949,21.938,89.02,21.834,89.042,21.759,89.052,21.654,88.967,21.641,88.907,21.653,88.858,21.745,88.834,21.661,88.745,21.584,88.713,21.622,88.695,21.662,88.691,21.733,88.74,22.005,88.73,22.036,88.708,22.056,88.66,22.067,88.642,22.122,88.567,21.832,88.6,21.714,88.585,21.66,88.446,21.614,88.305,21.723,88.288,21.758,88.279,21.697,88.254,21.622,88.122,21.636,88.057,21.694,88.099,21.794,88.181,22.033,88.196,22.14,88.087,22.218,87.994,22.266,87.941,22.374,87.962,22.255,88.011,22.213,88.083,22.183,88.159,22.122,88.104,22.047,88.051,22.001,87.948,21.825,87.824,21.727,87.678,21.654,87.201,21.545,87.101,21.501,86.954,21.365,86.86,21.237,86.842,21.106,86.896,20.966,86.939,20.745,86.975,20.7,86.925,20.62,86.836,20.534,86.763,20.419,86.769,20.356,86.75,20.313,86.499,20.172,86.446,20.089,86.377,20.007,86.294,20.054,86.245,20.053,86.312,19.988,86.303,19.945,86.279,19.919,86.216,19.896,85.853,19.792,85.575,19.693,85.497,19.697,85.511,19.727,85.56,19.753,85.555,19.867,85.504,19.888,85.46,19.896,85.249,19.758,85.163,19.621,85.181,19.595,85.229,19.601,85.371,19.679,85.437,19.657,85.442,19.627,85.226,19.508,84.771,19.125,84.75,19.05,84.691,18.965,84.609,18.884,84.463,18.69,84.182,18.401,84.104,18.293,83.654,18.07,83.572,18.004,83.388,17.787,83.198,17.609,82.977,17.462,82.593,17.274,82.36,17.096,82.287,16.978,82.282,16.936,82.307,16.879,82.35,16.825,82.36,16.783,82.339,16.707,82.327,16.664,82.259,16.56,82.142,16.485,81.762,16.329,81.712,16.334,81.402,16.365,81.286,16.337,81.239,16.264,81.132,15.962,81.03,15.881,80.993,15.809,80.979,15.758,80.918,15.76,80.865,15.782,80.826,15.766,80.782,15.867,80.708,15.888,80.647,15.895,80.385,15.793,80.293,15.711,80.101,15.324,80.053,15.074,80.099,14.798,80.165,14.578,80.179,14.478,80.17,14.349,80.136,14.287,80.112,14.212,80.144,14.059,80.224,13.858,80.244,13.773,80.246,13.686,80.307,13.485,80.266,13.521,80.233,13.606,80.156,13.714,80.062,13.606,80.114,13.529,80.29,13.437,80.342,13.361,80.229,12.69,80.143,12.452,80.038,12.296,79.982,12.235,79.858,11.989,79.771,11.69,79.754,11.575,79.793,11.447,79.749,11.371,79.693,11.313,79.799,11.339,79.835,11.269,79.849,11.197,79.85,10.769,79.838,10.323,79.757,10.304,79.667,10.3,79.589,10.312,79.532,10.33,79.391,10.306,79.315,10.257,79.254,10.175,79.258,10.035,78.996,9.683,78.94,9.566,78.919,9.453,78.953,9.394,79.02,9.333,79.107,9.309,79.275,9.285,79.356,9.252,79.411,9.192,79.213,9.256,78.98,9.269,78.421,9.105,78.275,8.99,78.192,8.891,78.136,8.663,78.126,8.511,78.06,8.385,77.77,8.19,77.587,8.13,77.518,8.078,77.301,8.145,77.066,8.316,76.967,8.407,76.617,8.847,76.553,8.903,76.483,9.091,76.472,9.161,76.452,9.189,76.419,9.208,76.403,9.237,76.325,9.452,76.292,9.676,76.242,9.927,76.285,9.91,76.343,9.827,76.372,9.707,76.376,9.54,76.42,9.52,76.459,9.536,76.346,9.922,76.249,10.018,76.223,10.024,76.196,10.086,76.193,10.164,76.201,10.201,76.123,10.327,76.096,10.402,75.923,10.784,75.845,11.058,75.724,11.362,75.646,11.468,75.525,11.703,75.423,11.812,75.315,11.958,75.23,12.023,75.197,12.058,74.946,12.565,74.868,12.845,74.803,12.977,74.771,13.077,74.682,13.507,74.682,13.584,74.671,13.668,74.608,13.85,74.499,14.046,74.467,14.169,74.467,14.217,74.397,14.407,74.382,14.495,74.335,14.575,74.28,14.65,74.223,14.709,74.089,14.902,74.041,14.949,73.949,15.075,73.884,15.306,73.801,15.397,73.932,15.397,73.852,15.482,73.814,15.539,73.772,15.573,73.833,15.659,73.733,15.657,73.68,15.709,73.608,15.871,73.476,16.054,73.454,16.152,73.338,16.46,73.239,17.199,73.149,17.527,73.156,17.622,73.047,17.907,72.994,18.098,72.972,18.259,72.943,18.366,72.917,18.576,72.875,18.643,72.871,18.683,72.899,18.779,72.977,18.927,73.006,19.021,72.972,19.153,72.901,19.015,72.835,18.976,72.803,19.079,72.803,19.219,72.795,19.252,72.812,19.299,72.987,19.277,72.788,19.363,72.764,19.413,72.756,19.451,72.799,19.52,72.727,19.578,72.697,19.757,72.676,19.798,72.668,19.831,72.709,20.078,72.881,20.563,72.894,20.673,72.879,20.829,72.841,20.952,72.824,21.084,72.814,21.117,72.752,21.129,72.692,21.178,72.624,21.372,72.687,21.436,72.735,21.471,72.668,21.456,72.613,21.462,72.718,21.551,72.811,21.62,73.022,21.7,73.113,21.75,72.979,21.705,72.84,21.687,72.543,21.697,72.592,21.878,72.644,21.938,72.7,21.972,72.617,21.962,72.522,21.976,72.553,22.16,72.628,22.2,72.709,22.207,72.809,22.233,72.702,22.264,72.59,22.278,72.456,22.248,72.333,22.27,72.183,22.27,72.243,22.245,72.306,22.189,72.274,22.09,72.244,22.028,72.162,21.985,72.094,21.92,72.076,21.863,72.037,21.823,72.103,21.795,72.171,21.774,72.21,21.728,72.257,21.661,72.254,21.531,72.077,21.224,72.015,21.156,71.571,20.971,71.396,20.87,71.025,20.739,70.88,20.715,70.719,20.74,70.485,20.84,70.127,21.095,70.034,21.179,69.748,21.506,69.542,21.679,69.385,21.84,69.192,21.992,69.009,22.197,68.97,22.29,68.983,22.385,69.052,22.437,69.131,22.416,69.194,22.336,69.239,22.3,69.277,22.285,69.549,22.408,69.655,22.404,69.728,22.465,69.819,22.452,70.006,22.548,70.084,22.554,70.177,22.573,70.328,22.816,70.44,22.97,70.513,23.002,70.509,23.04,70.489,23.09,70.435,23.077,70.396,23.03,70.368,22.973,70.339,22.94,70.251,22.971,70.192,22.966,70.118,22.947,69.85,22.856,69.74,22.775,69.665,22.759,69.236,22.849,68.817,23.054,68.641,23.19,68.529,23.364,68.417,23.571,68.454,23.629,68.627,23.754,68.777,23.852,68.642,23.808,68.497,23.748,68.425,23.706,68.343,23.617,68.235,23.597,68.192,23.729,68.165,23.857]],[[93.89,6.831,93.829,6.749,93.709,7.001,93.658,7.016,93.656,7.136,93.684,7.184,93.822,7.237,93.859,7.207,93.93,6.973,93.89,6.831]],[[93.734,7.356,93.638,7.262,93.597,7.319,93.614,7.358,93.655,7.38,93.692,7.411,93.734,7.356]],[[93.141,8.25,93.171,8.212,93.115,8.219,93.064,8.275,93.078,8.328,93.097,8.349,93.141,8.25]],[[93.443,7.878,93.365,7.877,93.342,7.919,93.309,7.964,93.334,8.007,93.375,8.018,93.434,7.948,93.447,7.899,93.443,7.878]],[[93.537,8.057,93.49,8.019,93.478,8.024,93.472,8.053,93.47,8.073,93.461,8.109,93.456,8.172,93.494,8.225,93.532,8.214,93.512,8.16,93.537,8.057]],[[92.788,9.137,92.744,9.131,92.717,9.165,92.713,9.205,92.739,9.231,92.762,9.244,92.786,9.241,92.809,9.173,92.788,9.137]],[[92.503,10.555,92.473,10.521,92.37,10.547,92.377,10.651,92.353,10.751,92.371,10.794,92.448,10.866,92.51,10.897,92.554,10.8,92.574,10.704,92.503,10.555]],[[92.693,11.381,92.645,11.361,92.596,11.386,92.634,11.427,92.64,11.509,92.69,11.463,92.687,11.411,92.693,11.381]],[[92.723,11.536,92.701,11.513,92.668,11.539,92.576,11.718,92.56,11.833,92.534,11.873,92.567,11.931,92.608,11.95,92.632,12.014,92.641,12.112,92.676,12.192,92.695,12.215,92.769,12.216,92.788,12.226,92.778,12.303,92.734,12.336,92.719,12.357,92.721,12.541,92.732,12.616,92.759,12.669,92.74,12.78,92.753,12.821,92.807,12.879,92.831,13.003,92.809,13.04,92.86,13.231,92.857,13.358,92.925,13.486,93.029,13.544,93.062,13.545,93.067,13.436,93.077,13.401,93.016,13.336,93.074,13.252,93.066,13.222,93.043,13.155,93.005,13.089,92.951,13.063,92.91,12.975,92.886,12.942,92.965,12.85,92.99,12.539,92.933,12.453,92.864,12.436,92.879,12.228,92.867,12.181,92.799,12.079,92.786,12.035,92.748,11.993,92.764,11.94,92.797,11.918,92.798,11.875,92.767,11.765,92.765,11.639,92.723,11.536]],[[93.017,12.037,93.062,11.899,92.982,11.959,92.955,12.002,92.996,12.032,93.017,12.037]],[[92.718,12.865,92.686,12.8,92.68,12.939,92.694,12.957,92.711,12.962,92.731,12.949,92.718,12.865]],[[72.78,11.202,72.773,11.196,72.772,11.214,72.782,11.243,72.793,11.263,72.796,11.26,72.793,11.242,72.788,11.216,72.78,11.202]],[[73.067,8.269,73.053,8.257,73.039,8.252,73.029,8.254,73.023,8.266,73.026,8.275,73.039,8.265,73.056,8.275,73.075,8.306,73.079,8.317,73.084,8.311,73.08,8.293,73.067,8.269]]]},{"n":"Iceland","c":"IS","r":3,"ml":2,"lx":-18.67,"ly":64.78,"b":[-24.476,63.407,-13.556,66.526],"p":[[[-15.543,66.229,-15.428,66.225,-15.241,66.259,-15.162,66.282,-14.97,66.36,-14.856,66.381,-14.681,66.376,-14.596,66.382,-14.594,66.374,-14.702,66.342,-14.788,66.331,-14.912,66.284,-15.03,66.178,-15.117,66.126,-15.116,66.102,-15.01,66.061,-14.894,66.038,-14.787,66.059,-14.74,66.051,-14.698,66.02,-14.674,65.99,-14.669,65.96,-14.688,65.897,-14.753,65.834,-14.839,65.781,-14.827,65.764,-14.758,65.756,-14.426,65.79,-14.392,65.787,-14.373,65.77,-14.369,65.739,-14.351,65.71,-14.318,65.684,-14.328,65.658,-14.473,65.575,-14.302,65.628,-14.167,65.642,-13.935,65.616,-13.841,65.586,-13.785,65.533,-13.705,65.551,-13.67,65.55,-13.618,65.519,-13.616,65.487,-13.654,65.441,-13.668,65.399,-13.708,65.382,-13.783,65.369,-13.805,65.355,-13.772,65.323,-13.723,65.291,-13.653,65.29,-13.641,65.275,-13.64,65.257,-13.649,65.237,-13.672,65.223,-13.707,65.215,-13.755,65.193,-13.581,65.143,-13.559,65.125,-13.556,65.098,-13.57,65.068,-13.599,65.036,-13.652,65.017,-13.777,65.014,-13.854,64.993,-13.828,64.958,-13.83,64.914,-13.853,64.862,-13.952,64.784,-14.044,64.742,-14.135,64.715,-14.297,64.724,-14.385,64.745,-14.375,64.677,-14.465,64.636,-14.448,64.601,-14.417,64.583,-14.433,64.538,-14.475,64.494,-14.547,64.446,-14.628,64.416,-14.79,64.38,-14.927,64.32,-15.022,64.296,-15.256,64.297,-15.495,64.258,-15.833,64.177,-16.06,64.111,-16.236,64.037,-16.468,63.916,-16.64,63.865,-16.74,63.852,-16.933,63.841,-17.095,63.808,-17.633,63.747,-17.816,63.713,-17.839,63.682,-17.915,63.636,-17.92,63.62,-17.886,63.607,-17.88,63.59,-17.947,63.536,-18.08,63.496,-18.143,63.497,-18.219,63.531,-18.252,63.53,-18.265,63.525,-18.266,63.514,-18.222,63.473,-18.303,63.454,-18.654,63.407,-19.25,63.442,-19.487,63.479,-19.778,63.537,-19.952,63.552,-20.198,63.556,-20.4,63.637,-20.494,63.687,-20.502,63.708,-20.491,63.732,-20.47,63.748,-20.438,63.757,-20.372,63.758,-20.363,63.765,-20.414,63.805,-20.463,63.792,-20.593,63.735,-20.651,63.737,-20.727,63.766,-20.73,63.793,-20.879,63.804,-21.008,63.838,-21.137,63.888,-21.156,63.907,-21.094,63.934,-21.106,63.94,-21.152,63.945,-21.246,63.935,-21.388,63.873,-21.449,63.858,-22.373,63.844,-22.607,63.837,-22.652,63.828,-22.693,63.869,-22.729,63.959,-22.743,64.019,-22.734,64.048,-22.701,64.083,-22.651,64.077,-22.603,64.05,-22.56,64.01,-22.51,63.991,-22.188,64.039,-22.057,64.071,-22.001,64.102,-21.935,64.154,-21.866,64.18,-21.833,64.205,-21.768,64.285,-21.723,64.322,-21.669,64.349,-21.606,64.367,-21.463,64.379,-21.557,64.398,-21.647,64.398,-21.951,64.314,-22.053,64.314,-22.049,64.327,-22.006,64.351,-21.901,64.392,-21.973,64.395,-22.001,64.413,-22.004,64.452,-21.95,64.515,-21.702,64.598,-21.617,64.61,-21.591,64.626,-21.623,64.64,-21.675,64.648,-21.924,64.563,-22.106,64.533,-22.16,64.539,-22.254,64.572,-22.284,64.587,-22.325,64.624,-22.32,64.647,-22.234,64.714,-22.248,64.727,-22.307,64.733,-22.467,64.795,-22.72,64.789,-23.347,64.824,-23.476,64.809,-23.69,64.757,-23.819,64.739,-23.879,64.751,-23.933,64.779,-23.982,64.816,-24.026,64.863,-24.007,64.896,-23.924,64.915,-23.864,64.924,-23.693,64.913,-23.485,64.946,-23.353,64.953,-23.315,64.958,-23.237,64.993,-23.198,65.002,-23.138,64.99,-23.109,64.966,-22.9,65.003,-22.828,65.022,-22.82,65.033,-22.788,65.046,-22.684,65.026,-22.6,65.026,-22.494,65.04,-22.308,65.046,-21.892,65.049,-21.83,65.079,-21.8,65.106,-21.764,65.174,-21.78,65.188,-22.04,65.125,-22.099,65.126,-22.4,65.159,-22.509,65.197,-22.473,65.227,-22.314,65.292,-22.149,65.344,-21.907,65.4,-21.85,65.422,-21.844,65.447,-22.006,65.493,-22.311,65.481,-22.39,65.535,-22.644,65.568,-22.813,65.547,-22.902,65.58,-23.122,65.535,-23.605,65.469,-23.796,65.423,-23.9,65.408,-24.019,65.445,-24.224,65.487,-24.455,65.5,-24.476,65.525,-24.341,65.601,-24.249,65.615,-24.156,65.608,-23.979,65.555,-23.857,65.538,-24.01,65.616,-24.006,65.646,-24.018,65.691,-24.065,65.71,-24.112,65.76,-24.093,65.776,-24.032,65.782,-23.909,65.766,-23.616,65.68,-23.472,65.695,-23.393,65.727,-23.285,65.75,-23.316,65.762,-23.569,65.764,-23.705,65.781,-23.773,65.806,-23.833,65.849,-23.812,65.869,-23.741,65.885,-23.525,65.88,-23.667,65.954,-23.767,65.997,-23.777,66.018,-23.771,66.043,-23.757,66.061,-23.737,66.069,-23.489,66.026,-23.434,66.024,-23.485,66.052,-23.594,66.093,-23.599,66.109,-23.553,66.122,-23.53,66.145,-23.528,66.164,-23.453,66.181,-23.377,66.182,-23.3,66.167,-23.063,66.086,-23.029,66.064,-23.017,66.034,-23.029,65.997,-23.019,65.982,-22.926,65.995,-22.852,65.979,-22.815,65.983,-22.723,66.039,-22.66,66.026,-22.622,66,-22.61,65.976,-22.604,65.944,-22.62,65.877,-22.616,65.867,-22.552,65.905,-22.442,65.908,-22.428,65.927,-22.424,65.998,-22.433,66.058,-22.445,66.07,-22.806,66.153,-22.869,66.172,-22.948,66.213,-22.932,66.233,-22.862,66.251,-22.756,66.259,-22.509,66.258,-22.484,66.266,-22.532,66.288,-22.646,66.302,-22.673,66.314,-22.686,66.338,-22.821,66.325,-22.972,66.324,-23.117,66.339,-23.12,66.357,-23.063,66.384,-22.944,66.429,-22.889,66.441,-22.724,66.433,-22.559,66.445,-22.426,66.43,-22.32,66.385,-22.17,66.307,-21.967,66.257,-21.948,66.241,-21.84,66.2,-21.625,66.09,-21.407,66.026,-21.397,66.009,-21.433,65.99,-21.517,65.968,-21.497,65.955,-21.388,65.939,-21.309,65.895,-21.303,65.876,-21.375,65.742,-21.413,65.713,-21.457,65.698,-21.658,65.724,-21.61,65.681,-21.466,65.635,-21.434,65.61,-21.455,65.585,-21.439,65.579,-21.387,65.592,-21.365,65.578,-21.374,65.536,-21.396,65.502,-21.432,65.474,-21.422,65.462,-21.365,65.466,-21.313,65.459,-21.23,65.421,-21.163,65.304,-21.13,65.267,-21.106,65.3,-21.076,65.385,-21.047,65.428,-21.021,65.43,-20.998,65.445,-20.979,65.471,-20.94,65.565,-20.804,65.636,-20.74,65.658,-20.679,65.663,-20.649,65.654,-20.548,65.579,-20.487,65.567,-20.455,65.571,-20.412,65.622,-20.357,65.719,-20.344,65.828,-20.374,65.948,-20.357,66.033,-20.292,66.084,-20.208,66.1,-20.103,66.08,-20.026,66.049,-19.875,65.93,-19.753,65.868,-19.648,65.801,-19.594,65.779,-19.49,65.768,-19.462,65.772,-19.443,65.788,-19.434,65.814,-19.456,65.985,-19.427,66.038,-19.383,66.076,-19.195,66.098,-19.093,66.122,-18.994,66.16,-18.911,66.181,-18.846,66.184,-18.778,66.169,-18.706,66.136,-18.595,66.071,-18.455,65.965,-18.277,65.885,-18.184,65.758,-18.164,65.737,-18.142,65.734,-18.118,65.751,-18.103,65.774,-18.099,65.83,-18.149,65.905,-18.315,66.093,-18.318,66.129,-18.297,66.157,-18.18,66.161,-17.907,66.143,-17.82,66.114,-17.634,65.999,-17.582,65.971,-17.55,65.964,-17.539,65.978,-17.467,66,-17.417,66.026,-17.334,66.089,-17.153,66.203,-17.115,66.206,-17.062,66.197,-16.97,66.167,-16.925,66.143,-16.838,66.125,-16.748,66.132,-16.625,66.172,-16.485,66.196,-16.437,66.253,-16.428,66.278,-16.541,66.447,-16.493,66.481,-16.249,66.523,-16.036,66.526,-15.985,66.515,-15.851,66.433,-15.76,66.392,-15.714,66.359,-15.703,66.286,-15.647,66.259,-15.543,66.229]]]},{"n":"Hungary","c":"HU","r":5,"ml":4,"lx":19.45,"ly":47.09,"b":[16.093,45.753,22.877,48.553],"p":[[[22.132,48.405,22.227,48.413,22.231,48.412,22.254,48.407,22.269,48.361,22.272,48.358,22.295,48.327,22.317,48.287,22.35,48.256,22.424,48.243,22.52,48.205,22.582,48.134,22.676,48.104,22.683,48.104,22.702,48.107,22.769,48.11,22.782,48.095,22.836,48.06,22.857,48.03,22.846,47.997,22.856,47.96,22.877,47.947,22.852,47.923,22.677,47.799,22.608,47.766,22.563,47.76,22.491,47.773,22.417,47.763,22.351,47.736,22.291,47.728,22.245,47.696,22.185,47.629,22.112,47.572,22.038,47.537,22,47.505,21.995,47.396,21.954,47.364,21.899,47.333,21.869,47.305,21.785,47.138,21.722,47.085,21.661,47.044,21.651,47.007,21.653,46.964,21.584,46.878,21.494,46.79,21.477,46.753,21.497,46.704,21.411,46.648,21.361,46.621,21.32,46.608,21.295,46.572,21.252,46.486,21.263,46.448,21.265,46.412,21.192,46.392,21.17,46.353,21.152,46.304,21.122,46.282,21.04,46.242,20.837,46.26,20.76,46.246,20.737,46.217,20.733,46.194,20.707,46.173,20.661,46.146,20.614,46.133,20.508,46.167,20.281,46.133,20.242,46.109,20.21,46.126,20.161,46.142,19.934,46.161,19.844,46.146,19.725,46.152,19.613,46.169,19.531,46.155,19.458,46.087,19.421,46.064,19.393,46.05,19.33,46.029,19.278,46.003,19.208,45.984,19.146,45.987,19.087,46.016,19.066,46.01,19.048,45.983,19.016,45.96,18.928,45.931,18.905,45.932,18.9,45.932,18.833,45.911,18.722,45.899,18.666,45.907,18.565,45.813,18.534,45.796,18.437,45.767,18.358,45.753,18.291,45.764,18.264,45.765,17.964,45.77,17.807,45.79,17.706,45.827,17.64,45.868,17.607,45.914,17.529,45.941,17.406,45.951,17.311,45.996,17.242,46.077,17.15,46.14,17.033,46.187,16.94,46.254,16.871,46.339,16.748,46.416,16.57,46.485,16.516,46.5,16.506,46.522,16.418,46.607,16.381,46.639,16.385,46.681,16.367,46.705,16.335,46.722,16.318,46.783,16.308,46.828,16.284,46.857,16.093,46.863,16.253,46.972,16.332,47.002,16.424,46.997,16.453,47.007,16.461,47.022,16.477,47.058,16.485,47.091,16.493,47.123,16.483,47.14,16.438,47.146,16.417,47.223,16.44,47.253,16.463,47.273,16.434,47.367,16.443,47.4,16.515,47.405,16.574,47.425,16.623,47.448,16.637,47.477,16.677,47.536,16.64,47.609,16.432,47.656,16.421,47.674,16.47,47.695,16.521,47.724,16.551,47.747,16.591,47.751,16.647,47.739,16.748,47.686,16.786,47.679,16.823,47.694,16.863,47.697,16.973,47.695,17.067,47.708,17.046,47.764,17.046,47.805,17.03,47.837,17.04,47.873,17.078,47.901,17.089,47.964,17.147,48.006,17.175,48.012,17.277,48.004,17.302,47.993,17.317,47.991,17.481,47.888,17.635,47.81,17.762,47.77,17.948,47.767,18.146,47.763,18.476,47.777,18.724,47.787,18.741,47.806,18.778,47.853,18.748,47.893,18.75,47.939,18.792,48,18.914,48.051,19.265,48.073,19.467,48.111,19.497,48.162,19.564,48.213,19.625,48.223,19.709,48.2,19.81,48.155,19.899,48.131,19.95,48.147,20.129,48.222,20.334,48.296,20.475,48.495,20.49,48.527,20.643,48.55,20.867,48.546,20.981,48.52,21.067,48.506,21.196,48.511,21.382,48.553,21.451,48.552,21.505,48.522,21.563,48.496,21.603,48.464,21.633,48.419,21.649,48.401,21.675,48.378,21.721,48.347,21.767,48.338,22.111,48.393,22.132,48.405]]]},{"n":"Honduras","c":"HN","r":5,"ml":4.5,"lx":-86.89,"ly":14.79,"b":[-89.363,12.979,-83.158,16.514],"p":[[[-83.158,14.993,-83.415,15.008,-83.537,14.977,-83.59,14.908,-83.635,14.876,-83.674,14.884,-83.751,14.856,-83.867,14.794,-83.972,14.771,-84.066,14.786,-84.093,14.771,-84.1,14.751,-84.114,14.731,-84.151,14.72,-84.192,14.726,-84.239,14.748,-84.264,14.739,-84.267,14.698,-84.292,14.687,-84.34,14.706,-84.394,14.692,-84.454,14.644,-84.538,14.633,-84.646,14.661,-84.73,14.713,-84.789,14.79,-84.86,14.81,-84.985,14.752,-85.037,14.686,-85.049,14.645,-85.037,14.608,-85.059,14.583,-85.117,14.571,-85.161,14.525,-85.192,14.447,-85.198,14.386,-85.179,14.343,-85.208,14.312,-85.284,14.292,-85.374,14.224,-85.477,14.109,-85.58,14.028,-85.682,13.983,-85.731,13.932,-85.728,13.876,-85.734,13.859,-85.753,13.852,-85.787,13.844,-85.984,13.966,-86.04,14.05,-86.089,14.037,-86.151,13.995,-86.238,13.899,-86.332,13.77,-86.377,13.756,-86.61,13.775,-86.734,13.763,-86.759,13.746,-86.771,13.699,-86.764,13.635,-86.73,13.407,-86.711,13.313,-86.729,13.284,-86.792,13.28,-86.874,13.267,-86.918,13.224,-86.929,13.179,-86.933,13.118,-86.959,13.054,-87.009,13.008,-87.059,12.991,-87.337,12.979,-87.333,13.085,-87.413,13.127,-87.458,13.215,-87.498,13.275,-87.485,13.311,-87.489,13.353,-87.602,13.386,-87.708,13.36,-87.769,13.377,-87.814,13.399,-87.737,13.451,-87.732,13.483,-87.756,13.506,-87.782,13.521,-87.774,13.58,-87.759,13.65,-87.715,13.813,-87.731,13.841,-87.802,13.89,-87.892,13.895,-87.991,13.88,-88.039,13.905,-88.08,13.961,-88.151,13.987,-88.276,13.943,-88.408,13.875,-88.449,13.851,-88.483,13.854,-88.498,13.905,-88.504,13.964,-88.513,13.979,-88.583,14,-88.666,14.016,-88.708,14.032,-88.747,14.072,-88.846,14.125,-88.868,14.164,-89,14.253,-89.027,14.297,-89.057,14.329,-89.121,14.37,-89.17,14.36,-89.337,14.411,-89.363,14.416,-89.339,14.461,-89.287,14.53,-89.172,14.607,-89.162,14.669,-89.192,14.789,-89.222,14.866,-89.206,14.901,-89.17,15.04,-89.143,15.072,-88.976,15.143,-88.961,15.152,-88.83,15.251,-88.684,15.36,-88.533,15.481,-88.365,15.616,-88.271,15.695,-88.228,15.729,-88.131,15.701,-88.055,15.765,-88.01,15.786,-87.907,15.863,-87.875,15.879,-87.702,15.911,-87.618,15.91,-87.545,15.832,-87.487,15.79,-87.377,15.826,-87.286,15.834,-86.907,15.762,-86.757,15.794,-86.481,15.801,-86.357,15.783,-86.181,15.885,-86.069,15.906,-85.936,15.953,-85.954,16.002,-85.986,16.024,-85.784,16.003,-85.484,15.9,-85.164,15.918,-85.048,15.974,-84.974,15.99,-84.646,15.884,-84.56,15.802,-84.492,15.794,-84.44,15.813,-84.426,15.829,-84.49,15.847,-84.52,15.873,-84.261,15.823,-83.775,15.437,-83.765,15.405,-83.973,15.52,-84.083,15.511,-84.111,15.492,-84.105,15.43,-84.095,15.401,-84.048,15.398,-84.013,15.414,-83.927,15.394,-83.871,15.353,-83.802,15.289,-83.76,15.22,-83.716,15.219,-83.672,15.261,-83.59,15.266,-83.536,15.219,-83.498,15.222,-83.551,15.294,-83.676,15.365,-83.646,15.368,-83.369,15.24,-83.291,15.079,-83.226,15.042,-83.158,14.993]],[[-86.42,16.378,-86.58,16.3,-86.63,16.302,-86.557,16.362,-86.438,16.414,-86.338,16.439,-86.256,16.428,-86.42,16.378]],[[-85.871,16.462,-85.947,16.404,-85.961,16.43,-85.924,16.483,-85.878,16.514,-85.834,16.511,-85.844,16.488,-85.871,16.462]]]},{"n":"Haiti","c":"HT","r":5,"ml":4,"lx":-72.22,"ly":19.26,"b":[-74.478,18.039,-71.645,20.094],"p":[[[-71.779,19.718,-71.757,19.688,-71.711,19.487,-71.707,19.422,-71.753,19.324,-71.746,19.286,-71.647,19.196,-71.645,19.164,-71.657,19.131,-71.742,19.046,-71.807,18.987,-71.786,18.92,-71.734,18.856,-71.727,18.803,-71.743,18.733,-71.824,18.646,-71.867,18.614,-71.987,18.61,-72,18.598,-71.94,18.513,-71.873,18.416,-71.762,18.341,-71.737,18.271,-71.764,18.204,-71.768,18.039,-71.853,18.119,-71.946,18.186,-72.002,18.212,-72.06,18.229,-72.504,18.22,-72.553,18.208,-72.592,18.187,-72.633,18.176,-72.755,18.156,-72.877,18.152,-73.16,18.206,-73.272,18.234,-73.385,18.251,-73.515,18.245,-73.644,18.229,-73.747,18.19,-73.825,18.122,-73.839,18.058,-73.885,18.042,-73.989,18.143,-74.085,18.215,-74.195,18.269,-74.419,18.346,-74.46,18.393,-74.478,18.45,-74.387,18.625,-74.284,18.657,-74.228,18.663,-74.1,18.641,-73.976,18.601,-73.862,18.575,-73.687,18.565,-73.592,18.522,-72.917,18.456,-72.789,18.435,-72.739,18.442,-72.696,18.468,-72.66,18.515,-72.618,18.551,-72.418,18.559,-72.376,18.574,-72.347,18.624,-72.348,18.675,-72.465,18.744,-72.649,18.894,-72.811,19.072,-72.741,19.131,-72.768,19.241,-72.742,19.342,-72.703,19.441,-72.863,19.526,-73.053,19.611,-73.316,19.637,-73.396,19.659,-73.438,19.722,-73.401,19.807,-73.315,19.855,-73.218,19.884,-73.118,19.904,-72.877,19.928,-72.637,19.901,-72.43,19.813,-72.22,19.745,-71.954,19.722,-71.835,19.697,-71.779,19.718]],[[-72.805,18.778,-72.822,18.707,-73.078,18.791,-73.285,18.897,-73.276,18.954,-73.171,18.967,-73.069,18.932,-72.919,18.861,-72.805,18.778]],[[-72.664,20.038,-72.623,20.014,-72.639,19.986,-72.74,20.003,-72.844,20.035,-72.878,20.027,-72.899,20.031,-72.96,20.062,-72.907,20.086,-72.851,20.094,-72.791,20.092,-72.664,20.038]]]},{"n":"Guyana","c":"GY","r":4,"ml":4,"lx":-58.94,"ly":5.12,"b":[-61.391,1.201,-56.483,8.549],"p":[[[-60.742,5.202,-60.954,5.437,-61.167,5.674,-61.377,5.907,-61.391,5.939,-61.303,6.05,-61.225,6.129,-61.159,6.174,-61.129,6.214,-61.152,6.385,-61.151,6.447,-61.182,6.513,-61.204,6.588,-61.177,6.651,-61.146,6.695,-61.105,6.711,-61.007,6.727,-60.938,6.733,-60.914,6.758,-60.873,6.787,-60.821,6.788,-60.718,6.768,-60.671,6.806,-60.586,6.857,-60.395,6.945,-60.352,7.003,-60.322,7.092,-60.325,7.134,-60.345,7.15,-60.392,7.165,-60.465,7.167,-60.523,7.144,-60.583,7.156,-60.633,7.211,-60.636,7.257,-60.607,7.321,-60.624,7.363,-60.719,7.499,-60.719,7.536,-60.649,7.597,-60.61,7.648,-60.556,7.772,-60.514,7.813,-60.381,7.828,-60.347,7.854,-60.279,7.919,-60.178,7.994,-60.032,8.054,-59.991,8.162,-59.965,8.192,-59.849,8.249,-59.829,8.279,-59.832,8.306,-60.018,8.549,-59.981,8.533,-59.837,8.374,-59.757,8.34,-59.74,8.339,-59.739,8.38,-59.666,8.363,-59.477,8.254,-59.2,8.075,-58.812,7.736,-58.701,7.607,-58.627,7.546,-58.511,7.398,-58.477,7.326,-58.481,7.038,-58.583,6.844,-58.608,6.697,-58.613,6.503,-58.673,6.391,-58.594,6.452,-58.569,6.627,-58.502,6.734,-58.415,6.851,-58.298,6.879,-58.173,6.829,-58.072,6.821,-57.983,6.786,-57.793,6.599,-57.608,6.45,-57.54,6.332,-57.344,6.272,-57.228,6.178,-57.19,6.097,-57.167,5.885,-57.205,5.565,-57.195,5.548,-57.248,5.485,-57.258,5.445,-57.292,5.374,-57.319,5.335,-57.28,5.247,-57.235,5.243,-57.218,5.232,-57.207,5.214,-57.21,5.195,-57.227,5.179,-57.269,5.157,-57.31,5.106,-57.306,5.05,-57.331,5.02,-57.412,5.005,-57.571,5.004,-57.649,5.001,-57.711,4.991,-57.752,4.954,-57.804,4.929,-57.845,4.923,-57.881,4.881,-57.917,4.82,-57.905,4.779,-57.868,4.724,-57.846,4.668,-57.875,4.577,-57.906,4.507,-57.925,4.453,-57.95,4.35,-58.011,4.236,-58.054,4.172,-58.054,4.102,-58.032,4.002,-57.908,3.857,-57.867,3.787,-57.833,3.676,-57.72,3.588,-57.649,3.517,-57.656,3.424,-57.647,3.395,-57.603,3.371,-57.55,3.353,-57.491,3.354,-57.438,3.362,-57.426,3.375,-57.304,3.377,-57.29,3.354,-57.283,3.219,-57.278,3.164,-57.249,3.142,-57.232,3.109,-57.231,3.079,-57.225,3.003,-57.207,2.963,-57.21,2.883,-57.197,2.853,-57.164,2.833,-57.121,2.776,-57.105,2.768,-57.097,2.748,-57.06,2.666,-57.042,2.641,-57.029,2.638,-57.023,2.609,-56.997,2.532,-56.979,2.513,-56.945,2.457,-56.931,2.395,-56.886,2.326,-56.841,2.277,-56.82,2.227,-56.761,2.115,-56.704,2.036,-56.627,2.016,-56.563,2.005,-56.522,1.975,-56.483,1.942,-56.525,1.927,-56.564,1.907,-56.616,1.923,-56.69,1.914,-56.766,1.892,-56.837,1.881,-56.97,1.916,-57.01,1.921,-57.038,1.936,-57.093,2.006,-57.119,2.014,-57.19,1.982,-57.276,1.959,-57.317,1.963,-57.367,1.94,-57.413,1.909,-57.5,1.774,-57.546,1.726,-57.594,1.704,-57.692,1.705,-57.796,1.7,-57.873,1.667,-57.946,1.651,-57.983,1.648,-57.995,1.574,-58.012,1.54,-58.035,1.52,-58.091,1.514,-58.142,1.517,-58.173,1.548,-58.23,1.563,-58.281,1.574,-58.314,1.592,-58.341,1.588,-58.363,1.557,-58.38,1.53,-58.396,1.482,-58.473,1.466,-58.506,1.439,-58.487,1.348,-58.496,1.312,-58.512,1.285,-58.605,1.279,-58.685,1.281,-58.73,1.248,-58.787,1.208,-58.822,1.201,-58.862,1.204,-58.917,1.249,-58.969,1.305,-59.1,1.344,-59.231,1.376,-59.317,1.465,-59.337,1.508,-59.378,1.527,-59.479,1.632,-59.536,1.7,-59.597,1.718,-59.667,1.746,-59.664,1.795,-59.669,1.842,-59.699,1.861,-59.741,1.874,-59.756,1.901,-59.752,1.962,-59.744,2.122,-59.755,2.274,-59.849,2.327,-59.89,2.363,-59.961,2.588,-59.994,2.69,-59.996,2.765,-59.972,2.99,-59.946,3.088,-59.873,3.283,-59.831,3.349,-59.829,3.399,-59.833,3.462,-59.854,3.588,-59.732,3.667,-59.679,3.7,-59.67,3.753,-59.604,3.82,-59.575,3.883,-59.551,3.934,-59.558,3.96,-59.586,3.975,-59.62,4.023,-59.691,4.16,-59.717,4.188,-59.739,4.227,-59.727,4.288,-59.7,4.354,-59.703,4.381,-59.746,4.417,-59.833,4.476,-59.906,4.48,-59.962,4.502,-60.045,4.505,-60.111,4.511,-60.149,4.533,-60.141,4.57,-60.125,4.598,-60.069,4.667,-60.032,4.741,-60.027,4.813,-60.015,4.908,-59.999,4.99,-59.991,5.083,-60.078,5.144,-60.106,5.194,-60.142,5.239,-60.182,5.239,-60.242,5.258,-60.335,5.199,-60.409,5.21,-60.46,5.188,-60.576,5.192,-60.651,5.221,-60.742,5.202]]]},{"n":"Guinea-Bissau","c":"GW","r":6,"ml":5,"lx":-14.52,"ly":12.16,"b":[-16.712,10.94,-13.674,12.68],"p":[[[-16.712,12.355,-16.657,12.364,-16.521,12.349,-16.416,12.368,-16.342,12.4,-16.242,12.443,-16.144,12.457,-15.84,12.438,-15.575,12.49,-15.378,12.589,-15.196,12.68,-14.961,12.679,-14.708,12.678,-14.349,12.676,-14.065,12.675,-13.729,12.674,-13.733,12.593,-13.674,12.479,-13.682,12.393,-13.708,12.313,-13.73,12.281,-13.76,12.262,-13.849,12.263,-13.887,12.247,-13.947,12.215,-13.949,12.178,-13.901,12.143,-13.862,12.093,-13.816,12.054,-13.738,12.01,-13.731,11.96,-13.729,11.834,-13.733,11.736,-13.953,11.665,-14.122,11.652,-14.266,11.66,-14.328,11.63,-14.452,11.556,-14.605,11.512,-14.683,11.508,-14.72,11.482,-14.779,11.406,-14.944,11.072,-14.999,10.992,-15.043,10.94,-15.094,11.011,-15.055,11.142,-15.097,11.14,-15.181,11.034,-15.222,11.031,-15.217,11.156,-15.263,11.161,-15.317,11.152,-15.393,11.217,-15.401,11.266,-15.395,11.334,-15.348,11.378,-15.355,11.396,-15.399,11.401,-15.449,11.39,-15.479,11.41,-15.429,11.499,-15.253,11.573,-15.164,11.581,-15.073,11.598,-15.122,11.662,-15.23,11.687,-15.317,11.669,-15.36,11.623,-15.413,11.615,-15.502,11.724,-15.5,11.778,-15.467,11.843,-15.416,11.872,-15.211,11.871,-15.133,11.907,-15.102,11.914,-15.072,11.947,-15.078,11.969,-15.112,11.97,-15.188,11.927,-15.435,11.944,-15.513,11.918,-15.651,11.818,-15.819,11.763,-15.942,11.787,-15.903,11.92,-15.92,11.938,-15.959,11.96,-16.138,11.917,-16.274,11.978,-16.328,12.052,-16.319,12.144,-16.255,12.206,-16.245,12.237,-16.312,12.243,-16.437,12.204,-16.712,12.355]],[[-16.115,11.059,-16.195,11.045,-16.231,11.094,-16.236,11.113,-16.195,11.13,-16.176,11.131,-16.144,11.167,-16.105,11.191,-16.087,11.199,-16.067,11.197,-16.053,11.118,-16.072,11.084,-16.115,11.059]],[[-15.896,11.082,-15.905,11.055,-15.964,11.059,-15.951,11.087,-15.963,11.095,-15.946,11.18,-15.938,11.193,-15.909,11.161,-15.905,11.148,-15.896,11.082]],[[-15.725,11.215,-15.725,11.175,-15.767,11.182,-15.78,11.195,-15.755,11.269,-15.717,11.302,-15.672,11.296,-15.658,11.286,-15.667,11.258,-15.687,11.234,-15.725,11.215]],[[-15.553,11.537,-15.563,11.514,-15.62,11.533,-15.537,11.618,-15.482,11.632,-15.484,11.568,-15.526,11.554,-15.553,11.537]],[[-15.902,11.466,-15.949,11.434,-15.997,11.449,-16.023,11.477,-16.019,11.527,-15.965,11.598,-15.915,11.589,-15.902,11.466]],[[-15.986,11.882,-16.038,11.76,-16.102,11.773,-16.147,11.846,-16.152,11.877,-16.022,11.887,-15.986,11.882]]]},{"n":"Guinea","c":"GN","r":3,"ml":3,"lx":-10.02,"ly":10.62,"b":[-15.051,7.216,-7.681,12.674],"p":[[[-10.283,8.485,-10.36,8.496,-10.394,8.481,-10.496,8.362,-10.558,8.316,-10.604,8.319,-10.653,8.33,-10.687,8.322,-10.712,8.335,-10.702,8.364,-10.677,8.401,-10.628,8.53,-10.503,8.66,-10.501,8.688,-10.552,8.764,-10.606,8.868,-10.606,8.979,-10.616,9.059,-10.727,9.082,-10.747,9.095,-10.75,9.122,-10.721,9.194,-10.688,9.261,-10.683,9.289,-10.691,9.314,-10.759,9.385,-10.865,9.516,-10.963,9.662,-11.047,9.786,-11.116,9.843,-11.181,9.925,-11.206,9.978,-11.274,9.997,-11.472,9.995,-11.71,9.994,-11.911,9.993,-11.923,9.923,-12.142,9.875,-12.278,9.93,-12.428,9.898,-12.501,9.862,-12.524,9.787,-12.558,9.705,-12.59,9.671,-12.604,9.634,-12.622,9.601,-12.652,9.562,-12.684,9.484,-12.756,9.374,-12.831,9.302,-12.959,9.263,-12.999,9.147,-13.028,9.104,-13.077,9.07,-13.13,9.048,-13.178,9.061,-13.234,9.07,-13.293,9.049,-13.303,9.078,-13.269,9.171,-13.296,9.219,-13.396,9.314,-13.406,9.361,-13.436,9.42,-13.568,9.543,-13.691,9.536,-13.657,9.639,-13.659,9.776,-13.7,9.851,-13.69,9.928,-13.713,9.923,-13.754,9.87,-13.82,9.887,-13.955,9.969,-14.022,10.048,-14.03,10.115,-14.045,10.141,-14.086,10.127,-14.17,10.129,-14.427,10.248,-14.61,10.55,-14.614,10.618,-14.587,10.735,-14.594,10.767,-14.677,10.689,-14.693,10.741,-14.757,10.862,-14.776,10.932,-14.837,10.963,-14.887,10.968,-14.925,10.945,-14.975,10.803,-15.012,10.804,-15.051,10.835,-15.043,10.94,-14.999,10.992,-14.944,11.072,-14.779,11.406,-14.72,11.482,-14.683,11.508,-14.605,11.512,-14.452,11.556,-14.328,11.63,-14.266,11.66,-14.122,11.652,-13.953,11.665,-13.733,11.736,-13.729,11.834,-13.731,11.96,-13.738,12.01,-13.816,12.054,-13.862,12.093,-13.901,12.143,-13.949,12.178,-13.947,12.215,-13.887,12.247,-13.849,12.263,-13.76,12.262,-13.73,12.281,-13.708,12.313,-13.682,12.393,-13.674,12.479,-13.733,12.593,-13.729,12.674,-13.406,12.662,-13.373,12.654,-13.228,12.64,-13.138,12.64,-13.083,12.634,-13.06,12.615,-13.064,12.581,-13.08,12.536,-13.061,12.49,-13.012,12.478,-12.986,12.492,-12.961,12.514,-12.931,12.532,-12.888,12.52,-12.797,12.452,-12.713,12.433,-12.621,12.396,-12.534,12.376,-12.457,12.378,-12.399,12.34,-12.291,12.328,-12.152,12.377,-12.042,12.398,-11.889,12.403,-11.808,12.387,-11.574,12.426,-11.457,12.418,-11.389,12.404,-11.418,12.378,-11.448,12.319,-11.475,12.247,-11.502,12.199,-11.492,12.167,-11.415,12.104,-11.305,12.015,-11.261,12.004,-11.21,12.025,-11.129,12.095,-11.066,12.171,-11.005,12.208,-10.933,12.205,-10.876,12.152,-10.806,12.034,-10.743,11.927,-10.735,11.916,-10.709,11.899,-10.677,11.899,-10.644,11.926,-10.619,11.941,-10.59,11.99,-10.466,12.139,-10.373,12.18,-10.34,12.19,-10.275,12.213,-10.167,12.177,-10.011,12.116,-9.821,12.042,-9.754,12.03,-9.715,12.042,-9.658,12.143,-9.588,12.182,-9.487,12.229,-9.405,12.252,-9.358,12.255,-9.34,12.283,-9.332,12.324,-9.341,12.366,-9.394,12.442,-9.395,12.465,-9.365,12.479,-9.3,12.49,-9.216,12.483,-9.12,12.45,-9.043,12.402,-8.999,12.346,-8.951,12.226,-8.914,12.109,-8.818,11.923,-8.82,11.807,-8.822,11.673,-8.78,11.648,-8.733,11.638,-8.711,11.618,-8.665,11.515,-8.621,11.485,-8.569,11.478,-8.471,11.412,-8.407,11.386,-8.399,11.367,-8.401,11.339,-8.425,11.305,-8.464,11.281,-8.52,11.236,-8.567,11.177,-8.664,11.036,-8.667,11.009,-8.646,10.99,-8.606,10.987,-8.564,10.997,-8.475,11.048,-8.404,11.03,-8.337,10.991,-8.313,10.95,-8.306,10.896,-8.322,10.827,-8.324,10.75,-8.302,10.618,-8.267,10.486,-8.231,10.438,-8.007,10.322,-7.986,10.278,-7.974,10.23,-7.991,10.163,-8.014,10.125,-8.078,10.067,-8.137,10.022,-8.155,9.973,-8.146,9.882,-8.146,9.675,-8.137,9.496,-8.089,9.431,-8.031,9.398,-7.963,9.404,-7.896,9.416,-7.9,9.309,-7.918,9.189,-7.839,9.152,-7.8,9.115,-7.778,9.081,-7.902,9.017,-7.938,8.98,-7.955,8.879,-7.951,8.787,-7.784,8.721,-7.72,8.643,-7.691,8.563,-7.681,8.41,-7.696,8.376,-7.739,8.375,-7.787,8.422,-7.824,8.468,-7.869,8.468,-7.953,8.478,-8.049,8.495,-8.168,8.491,-8.21,8.483,-8.237,8.456,-8.244,8.408,-8.256,8.254,-8.217,8.22,-8.141,8.181,-8.091,8.165,-8.049,8.17,-8.017,8.145,-8.01,8.079,-8.032,8.03,-8.074,7.984,-8.127,7.868,-8.118,7.824,-8.115,7.761,-8.206,7.59,-8.232,7.557,-8.352,7.591,-8.43,7.602,-8.486,7.558,-8.522,7.586,-8.564,7.625,-8.579,7.677,-8.607,7.688,-8.66,7.688,-8.708,7.659,-8.729,7.605,-8.733,7.544,-8.74,7.496,-8.769,7.467,-8.828,7.392,-8.856,7.323,-8.89,7.263,-8.938,7.266,-8.961,7.275,-8.977,7.259,-9.052,7.225,-9.118,7.216,-9.135,7.251,-9.173,7.278,-9.215,7.333,-9.263,7.378,-9.355,7.409,-9.392,7.395,-9.435,7.398,-9.464,7.416,-9.46,7.443,-9.411,7.51,-9.384,7.572,-9.369,7.64,-9.369,7.704,-9.395,7.795,-9.436,7.867,-9.446,7.908,-9.442,7.968,-9.451,8.023,-9.465,8.052,-9.471,8.107,-9.484,8.157,-9.508,8.176,-9.522,8.26,-9.518,8.346,-9.554,8.379,-9.61,8.402,-9.643,8.436,-9.664,8.474,-9.684,8.484,-9.701,8.482,-9.717,8.459,-9.736,8.454,-9.768,8.535,-9.782,8.538,-9.805,8.519,-10.064,8.43,-10.076,8.465,-10.098,8.506,-10.147,8.52,-10.233,8.489,-10.283,8.485]]]},{"n":"Guatemala","c":"GT","r":3,"ml":3,"lx":-90.5,"ly":14.98,"b":[-92.235,13.737,-88.228,17.816],"p":[[[-92.235,14.545,-92.209,14.571,-92.187,14.63,-92.16,14.691,-92.176,14.761,-92.186,14.818,-92.156,14.901,-92.159,14.964,-92.144,15.002,-92.099,15.027,-92.075,15.074,-92.204,15.238,-92.204,15.275,-92.187,15.321,-92.082,15.496,-91.957,15.703,-91.819,15.932,-91.737,16.07,-91.434,16.07,-91.234,16.071,-90.98,16.071,-90.703,16.071,-90.522,16.071,-90.447,16.073,-90.46,16.162,-90.45,16.261,-90.417,16.351,-90.417,16.391,-90.471,16.44,-90.576,16.468,-90.634,16.511,-90.634,16.565,-90.66,16.631,-90.711,16.708,-90.816,16.787,-90.976,16.868,-91.112,16.976,-91.224,17.112,-91.319,17.2,-91.392,17.236,-91.41,17.256,-91.196,17.254,-90.993,17.252,-90.992,17.447,-90.99,17.621,-90.989,17.816,-90.622,17.816,-90.184,17.816,-89.729,17.815,-89.372,17.815,-89.161,17.815,-89.171,17.572,-89.182,17.291,-89.19,17.085,-89.201,16.809,-89.212,16.527,-89.228,16.143,-89.237,15.894,-89.233,15.889,-89.114,15.901,-88.937,15.89,-88.894,15.891,-88.84,15.869,-88.798,15.863,-88.709,15.807,-88.603,15.764,-88.536,15.85,-88.572,15.901,-88.598,15.927,-88.594,15.95,-88.228,15.729,-88.271,15.695,-88.365,15.616,-88.533,15.481,-88.684,15.36,-88.83,15.251,-88.961,15.152,-88.976,15.143,-89.143,15.072,-89.17,15.04,-89.206,14.901,-89.222,14.866,-89.192,14.789,-89.162,14.669,-89.172,14.607,-89.287,14.53,-89.339,14.461,-89.363,14.416,-89.383,14.428,-89.419,14.431,-89.501,14.414,-89.541,14.41,-89.574,14.39,-89.577,14.347,-89.555,14.277,-89.547,14.241,-89.57,14.225,-89.671,14.183,-89.711,14.141,-89.749,14.077,-89.794,14.05,-89.84,14.055,-89.873,14.046,-89.943,13.997,-90.048,13.904,-90.105,13.835,-90.106,13.783,-90.095,13.737,-90.479,13.901,-90.607,13.929,-91.146,13.926,-91.377,13.99,-91.641,14.115,-91.819,14.228,-92.235,14.545]]]},{"n":"Grenada","c":"GD","r":6,"ml":4,"lx":-61.68,"ly":12.11,"b":[-61.782,12.008,-61.607,12.237],"p":[[[-61.716,12.013,-61.782,12.008,-61.756,12.045,-61.75,12.108,-61.715,12.185,-61.66,12.237,-61.607,12.223,-61.627,12.054,-61.716,12.013]]]},{"n":"Greece","c":"GR","r":3,"ml":2.7,"lx":21.73,"ly":39.49,"b":[19.646,34.934,28.232,41.744],"p":[[[27.86,36.554,27.838,36.538,27.788,36.584,27.786,36.608,27.837,36.635,27.863,36.641,27.87,36.623,27.869,36.583,27.86,36.554]],[[20.612,38.383,20.625,38.268,20.695,38.246,20.789,38.142,20.781,38.089,20.761,38.071,20.606,38.12,20.569,38.098,20.524,38.107,20.496,38.164,20.499,38.184,20.473,38.219,20.452,38.234,20.391,38.188,20.353,38.18,20.352,38.222,20.409,38.337,20.435,38.356,20.481,38.318,20.52,38.332,20.548,38.395,20.55,38.457,20.563,38.475,20.612,38.383]],[[20.888,37.805,20.994,37.708,20.909,37.732,20.819,37.665,20.704,37.743,20.635,37.823,20.62,37.855,20.692,37.93,20.759,37.853,20.84,37.841,20.888,37.805]],[[20.687,38.609,20.648,38.601,20.614,38.606,20.584,38.602,20.555,38.583,20.558,38.662,20.592,38.76,20.635,38.818,20.694,38.844,20.72,38.799,20.715,38.638,20.687,38.609]],[[20.759,38.329,20.709,38.319,20.646,38.414,20.624,38.48,20.65,38.484,20.675,38.476,20.701,38.451,20.701,38.426,20.712,38.399,20.739,38.366,20.759,38.329]],[[20.078,39.433,20.1,39.377,19.975,39.411,19.884,39.462,19.809,39.585,19.649,39.726,19.646,39.767,19.707,39.798,19.839,39.82,19.892,39.797,19.926,39.774,19.937,39.747,19.862,39.693,19.847,39.668,19.904,39.619,19.903,39.6,19.927,39.506,19.955,39.47,20.028,39.442,20.078,39.433]],[[23.415,38.959,23.472,38.85,23.525,38.813,23.636,38.77,23.688,38.765,23.878,38.687,24.099,38.671,24.128,38.648,24.155,38.588,24.2,38.541,24.211,38.504,24.188,38.463,24.22,38.339,24.276,38.22,24.36,38.163,24.464,38.145,24.563,38.148,24.588,38.124,24.579,38.02,24.537,37.98,24.502,37.97,24.473,37.981,24.446,38.005,24.417,38.017,24.359,38.019,24.318,38.06,24.212,38.118,24.193,38.152,24.189,38.204,24.144,38.243,24.103,38.317,24.064,38.337,24.042,38.374,24.04,38.39,23.886,38.401,23.759,38.401,23.651,38.443,23.617,38.553,23.553,38.582,23.505,38.613,23.465,38.656,23.364,38.735,23.252,38.801,23.144,38.845,23.029,38.873,22.936,38.84,22.881,38.848,22.87,38.871,22.986,38.916,23.146,39.003,23.258,39.031,23.313,39.035,23.415,38.959]],[[23.78,39.114,23.735,39.081,23.666,39.095,23.594,39.209,23.78,39.114]],[[23.888,39.158,23.841,39.147,23.888,39.226,23.971,39.268,23.94,39.201,23.888,39.158]],[[24.675,38.809,24.569,38.785,24.541,38.789,24.565,38.819,24.566,38.832,24.461,38.889,24.473,38.962,24.486,38.98,24.564,38.942,24.581,38.879,24.675,38.809]],[[24.774,40.615,24.646,40.579,24.516,40.647,24.517,40.687,24.586,40.769,24.623,40.793,24.719,40.786,24.774,40.73,24.786,40.704,24.769,40.659,24.774,40.615]],[[23.551,37.926,23.511,37.901,23.467,37.902,23.435,37.911,23.419,37.931,23.439,37.941,23.462,37.98,23.484,37.991,23.516,37.986,23.535,37.97,23.551,37.926]],[[23.054,36.19,23.042,36.146,22.939,36.176,22.911,36.221,22.906,36.32,22.933,36.369,22.95,36.384,22.998,36.328,23.097,36.247,23.054,36.19]],[[27.176,35.465,27.138,35.409,27.099,35.456,27.116,35.511,27.071,35.598,27.156,35.726,27.158,35.789,27.223,35.82,27.207,35.714,27.157,35.629,27.209,35.559,27.234,35.479,27.176,35.465]],[[27.02,36.959,26.92,36.945,26.938,37.025,26.889,37.087,26.967,37.052,27.016,37.01,27.04,37.002,27.035,36.976,27.02,36.959]],[[26.95,36.727,26.918,36.726,26.956,36.774,27.061,36.84,27.215,36.899,27.266,36.905,27.352,36.869,27.193,36.809,27.151,36.778,27.034,36.771,26.95,36.727]],[[25.546,36.968,25.457,36.93,25.396,36.984,25.362,37.07,25.525,37.196,25.564,37.185,25.588,37.153,25.584,37.039,25.546,36.968]],[[25.279,37.068,25.199,36.991,25.133,37,25.105,37.035,25.146,37.107,25.235,37.149,25.275,37.138,25.271,37.084,25.279,37.068]],[[25.482,36.393,25.436,36.34,25.371,36.359,25.397,36.379,25.413,36.405,25.415,36.442,25.397,36.465,25.409,36.474,25.467,36.435,25.482,36.393]],[[25.382,36.674,25.364,36.658,25.289,36.722,25.26,36.758,25.296,36.789,25.407,36.717,25.382,36.674]],[[26.824,37.811,26.947,37.778,26.982,37.782,27.04,37.77,27.055,37.709,26.978,37.7,26.845,37.645,26.788,37.657,26.721,37.705,26.613,37.71,26.581,37.724,26.639,37.781,26.743,37.81,26.824,37.811]],[[26.029,37.529,25.982,37.526,25.997,37.566,26.086,37.635,26.212,37.638,26.326,37.673,26.351,37.674,26.297,37.62,26.205,37.569,26.029,37.529]],[[25.859,36.79,25.771,36.782,25.743,36.79,25.797,36.807,25.834,36.825,25.852,36.848,25.942,36.887,26.001,36.937,26.064,36.903,25.985,36.88,25.859,36.79]],[[26.461,36.585,26.382,36.562,26.331,36.511,26.27,36.547,26.27,36.595,26.337,36.581,26.384,36.608,26.37,36.639,26.421,36.624,26.461,36.585]],[[24.356,37.577,24.289,37.528,24.277,37.601,24.32,37.678,24.379,37.683,24.401,37.649,24.356,37.577]],[[24.436,37.344,24.379,37.314,24.398,37.383,24.37,37.42,24.395,37.45,24.431,37.475,24.449,37.45,24.481,37.408,24.436,37.344]],[[24.536,36.764,24.538,36.705,24.531,36.684,24.326,36.656,24.345,36.723,24.357,36.744,24.425,36.713,24.45,36.729,24.46,36.747,24.536,36.764]],[[24.943,37.494,24.938,37.39,24.912,37.391,24.896,37.407,24.895,37.446,24.907,37.509,24.943,37.494]],[[24.992,37.76,24.962,37.692,24.884,37.771,24.799,37.824,24.767,37.871,24.714,37.899,24.7,37.962,24.763,37.988,24.79,37.99,24.855,37.914,24.956,37.905,24.948,37.858,24.98,37.797,24.992,37.76]],[[25.256,37.6,25.219,37.535,25.156,37.545,25.052,37.614,25.016,37.646,24.996,37.677,25.039,37.681,25.092,37.648,25.225,37.631,25.256,37.6]],[[24.721,36.921,24.703,36.917,24.676,36.959,24.671,36.999,24.681,37.022,24.716,37.024,24.763,36.949,24.721,36.921]],[[26.094,38.218,25.999,38.162,25.892,38.243,25.874,38.27,25.953,38.303,25.991,38.354,25.96,38.416,25.851,38.508,25.846,38.574,26.013,38.602,26.11,38.545,26.16,38.541,26.141,38.486,26.15,38.468,26.157,38.303,26.111,38.28,26.103,38.234,26.094,38.218]],[[26.41,39.329,26.393,39.27,26.531,39.172,26.578,39.11,26.596,39.049,26.584,39.031,26.532,39.064,26.489,39.075,26.503,39.031,26.547,38.994,26.469,38.973,26.39,38.974,26.161,39.026,26.108,39.081,26.245,39.164,26.273,39.198,26.176,39.194,26.072,39.096,25.906,39.139,25.855,39.179,25.844,39.2,25.91,39.288,26.026,39.285,26.088,39.304,26.165,39.332,26.165,39.374,26.348,39.383,26.41,39.329]],[[25.686,40.427,25.573,40.4,25.448,40.483,25.569,40.516,25.624,40.492,25.664,40.464,25.686,40.427]],[[25.438,39.983,25.4,39.95,25.372,39.891,25.357,39.808,25.299,39.806,25.263,39.823,25.252,39.854,25.249,39.894,25.224,39.893,25.203,39.849,25.185,39.83,25.126,39.826,25.062,39.852,25.065,39.91,25.052,39.976,25.058,40,25.234,40.005,25.286,39.956,25.348,39.985,25.374,40.016,25.449,40.035,25.438,39.983]],[[25.403,37.419,25.307,37.413,25.313,37.489,25.348,37.509,25.463,37.471,25.457,37.447,25.403,37.419]],[[24.524,37.125,24.487,37.11,24.425,37.132,24.441,37.187,24.484,37.21,24.529,37.192,24.536,37.168,24.524,37.125]],[[27.843,35.929,27.771,35.908,27.746,35.911,27.716,35.957,27.757,36.069,27.719,36.141,27.716,36.172,27.774,36.214,27.815,36.277,27.914,36.345,28.171,36.426,28.232,36.434,28.23,36.37,28.144,36.21,28.068,36.13,28.088,36.065,27.966,36.048,27.843,35.929]],[[23.852,35.535,23.921,35.528,24.013,35.529,24.034,35.535,24.093,35.594,24.166,35.595,24.198,35.537,24.124,35.511,24.109,35.496,24.123,35.484,24.179,35.46,24.255,35.469,24.258,35.423,24.275,35.386,24.313,35.364,24.354,35.359,24.445,35.366,24.535,35.381,24.627,35.41,24.721,35.425,25.003,35.41,25.104,35.347,25.297,35.339,25.476,35.306,25.57,35.328,25.73,35.349,25.756,35.326,25.735,35.184,25.745,35.143,25.791,35.123,25.837,35.133,25.893,35.179,26.028,35.215,26.168,35.215,26.286,35.31,26.32,35.315,26.299,35.269,26.281,35.159,26.256,35.095,26.244,35.045,26.166,35.019,26.047,35.014,25.83,35.025,25.611,35.007,25.206,34.959,24.8,34.934,24.745,34.951,24.744,35.014,24.735,35.058,24.709,35.089,24.583,35.115,24.464,35.16,23.994,35.222,23.884,35.246,23.704,35.233,23.638,35.235,23.593,35.257,23.562,35.295,23.548,35.416,23.57,35.535,23.609,35.566,23.627,35.53,23.673,35.514,23.715,35.55,23.715,35.605,23.737,35.656,23.771,35.634,23.793,35.556,23.852,35.535]],[[26.321,41.717,26.411,41.696,26.463,41.663,26.495,41.633,26.545,41.607,26.581,41.601,26.61,41.512,26.625,41.402,26.602,41.354,26.536,41.343,26.331,41.239,26.326,41.143,26.328,41.097,26.333,41.064,26.354,41.037,26.354,40.997,26.331,40.954,26.241,40.883,26.179,40.827,26.109,40.75,26.07,40.74,26.039,40.727,26.011,40.769,25.856,40.844,25.497,40.888,25.325,40.943,25.25,40.933,25.104,40.995,25.005,40.968,24.793,40.858,24.679,40.869,24.557,40.936,24.477,40.948,24.384,40.913,24.234,40.786,24.082,40.724,23.946,40.748,23.763,40.748,23.743,40.677,23.779,40.628,23.879,40.544,23.832,40.482,23.867,40.419,23.932,40.406,24.031,40.409,24.213,40.328,24.292,40.242,24.343,40.148,24.232,40.215,24.159,40.28,24.056,40.304,23.913,40.359,23.823,40.368,23.728,40.33,23.721,40.286,23.823,40.205,23.918,40.155,23.967,40.115,24.001,40.025,23.982,39.994,23.947,39.966,23.835,40.022,23.665,40.224,23.426,40.264,23.386,40.222,23.433,40.115,23.467,40.074,23.674,39.959,23.658,39.934,23.627,39.924,23.396,39.99,23.328,40.09,23.312,40.216,23.098,40.304,22.896,40.4,22.851,40.491,22.893,40.524,22.922,40.591,22.811,40.579,22.742,40.536,22.629,40.496,22.625,40.429,22.643,40.367,22.605,40.276,22.569,40.119,22.592,40.037,22.836,39.801,22.919,39.629,22.979,39.564,23.103,39.492,23.233,39.358,23.288,39.289,23.328,39.175,23.218,39.104,23.155,39.101,23.119,39.133,23.169,39.21,23.162,39.258,22.993,39.331,22.921,39.306,22.839,39.259,22.886,39.17,22.939,39.112,22.966,39.031,23.067,39.038,22.93,38.948,22.803,38.902,22.677,38.899,22.597,38.891,22.569,38.867,22.634,38.851,22.688,38.849,22.774,38.8,23.02,38.742,23.138,38.668,23.253,38.661,23.369,38.526,23.57,38.489,23.684,38.352,23.836,38.325,23.967,38.275,24.005,38.227,24.025,38.14,24.033,37.955,24.061,37.818,24.062,37.775,24.055,37.71,24.02,37.678,23.972,37.677,23.877,37.778,23.733,37.884,23.58,38.011,23.537,38.033,23.502,38.035,23.42,37.992,23.194,37.959,23.087,37.913,23.047,37.903,23.036,37.878,23.086,37.853,23.147,37.795,23.147,37.716,23.198,37.62,23.263,37.595,23.348,37.598,23.396,37.58,23.409,37.542,23.458,37.497,23.491,37.464,23.489,37.44,23.253,37.377,23.203,37.349,23.162,37.334,23.1,37.364,23.096,37.441,23.015,37.482,22.941,37.517,22.851,37.532,22.775,37.585,22.725,37.542,22.765,37.393,22.851,37.291,22.995,37.016,23.06,36.854,23.074,36.775,23.041,36.645,23.112,36.548,23.16,36.448,23.107,36.452,23.061,36.487,22.983,36.528,22.832,36.687,22.78,36.786,22.717,36.794,22.608,36.78,22.489,36.568,22.489,36.447,22.428,36.476,22.375,36.514,22.381,36.646,22.376,36.702,22.231,36.883,22.165,36.903,22.134,36.964,22.08,37.029,22.012,37.017,21.956,36.99,21.94,36.892,21.934,36.804,21.892,36.737,21.738,36.863,21.583,37.081,21.579,37.2,21.692,37.309,21.679,37.387,21.571,37.541,21.416,37.64,21.329,37.669,21.288,37.775,21.205,37.829,21.138,37.854,21.125,37.892,21.145,37.919,21.308,38.027,21.404,38.197,21.451,38.205,21.549,38.165,21.658,38.175,21.748,38.274,21.825,38.328,21.953,38.321,22.244,38.189,22.556,38.113,22.712,38.047,22.8,37.981,22.846,37.968,22.92,37.958,22.917,38.007,22.893,38.051,22.955,38.075,23.122,38.073,23.153,38.096,23.183,38.134,23.149,38.176,23.094,38.196,23.034,38.202,22.995,38.216,22.933,38.202,22.834,38.235,22.784,38.262,22.754,38.29,22.583,38.345,22.422,38.439,22.385,38.386,22.32,38.357,22.227,38.353,21.965,38.412,21.805,38.367,21.717,38.355,21.65,38.354,21.568,38.334,21.473,38.321,21.39,38.408,21.355,38.475,21.331,38.487,21.33,38.424,21.303,38.374,21.183,38.346,21.113,38.385,21.06,38.503,20.992,38.654,20.873,38.776,20.777,38.808,20.769,38.874,20.777,38.928,20.893,38.941,21.074,38.885,21.112,38.896,21.152,38.922,21.145,38.979,21.118,39.03,21.069,39.032,21.034,39.026,20.923,39.037,20.78,39.009,20.713,39.035,20.691,39.067,20.572,39.148,20.468,39.255,20.301,39.327,20.191,39.546,20.099,39.641,20.001,39.709,20.023,39.711,20.06,39.699,20.131,39.662,20.207,39.654,20.248,39.678,20.272,39.701,20.288,39.739,20.294,39.782,20.306,39.797,20.364,39.792,20.382,39.803,20.382,39.842,20.344,39.891,20.311,39.951,20.311,39.979,20.338,39.991,20.384,40.017,20.408,40.049,20.456,40.066,20.527,40.069,20.606,40.083,20.657,40.117,20.665,40.152,20.697,40.246,20.718,40.293,20.752,40.335,20.77,40.392,20.806,40.445,20.882,40.468,20.95,40.494,21.002,40.563,21.031,40.622,21.031,40.659,20.988,40.718,20.956,40.775,20.964,40.85,21.1,40.856,21.148,40.863,21.324,40.867,21.404,40.907,21.46,40.904,21.576,40.869,21.628,40.896,21.779,40.95,21.929,41.107,21.993,41.131,22.139,41.141,22.184,41.159,22.238,41.155,22.401,41.123,22.494,41.119,22.604,41.14,22.725,41.179,22.755,41.313,22.784,41.332,22.859,41.337,22.916,41.336,23.026,41.326,23.156,41.322,23.24,41.385,23.372,41.39,23.433,41.399,23.536,41.386,23.635,41.387,23.762,41.413,23.881,41.456,23.974,41.452,24.011,41.46,24.033,41.469,24.056,41.527,24.23,41.531,24.289,41.525,24.387,41.524,24.488,41.555,24.518,41.553,24.569,41.467,24.596,41.443,24.651,41.42,24.774,41.356,24.796,41.373,24.847,41.394,24.994,41.365,25.133,41.316,25.251,41.244,25.382,41.264,25.527,41.3,25.621,41.31,25.724,41.315,25.785,41.33,25.923,41.312,26.066,41.351,26.135,41.386,26.155,41.435,26.144,41.522,26.111,41.608,26.077,41.64,26.066,41.673,26.086,41.704,26.107,41.726,26.201,41.744,26.321,41.717]]]},{"n":"Ghana","c":"GH","r":3,"ml":2.7,"lx":-1.04,"ly":7.72,"b":[-3.244,4.762,1.187,11.167],"p":[[[-0.069,11.116,-0.005,11.056,0.009,11.021,-0.014,10.891,-0.061,10.801,-0.09,10.716,-0.086,10.673,-0.058,10.631,0.039,10.564,0.089,10.521,0.148,10.455,0.216,10.391,0.332,10.307,0.381,10.292,0.379,10.269,0.363,10.236,0.352,9.925,0.343,9.845,0.335,9.804,0.324,9.688,0.312,9.671,0.29,9.672,0.27,9.668,0.265,9.645,0.273,9.621,0.343,9.604,0.327,9.587,0.275,9.571,0.252,9.536,0.262,9.496,0.233,9.464,0.242,9.442,0.26,9.426,0.289,9.432,0.371,9.486,0.405,9.491,0.448,9.48,0.526,9.398,0.529,9.358,0.497,9.221,0.466,9.115,0.46,8.974,0.493,8.895,0.489,8.851,0.453,8.814,0.373,8.759,0.379,8.722,0.415,8.653,0.483,8.575,0.616,8.48,0.686,8.355,0.688,8.304,0.647,8.253,0.599,8.21,0.584,8.146,0.605,7.728,0.5,7.547,0.499,7.495,0.51,7.435,0.537,7.399,0.591,7.389,0.635,7.354,0.62,7.227,0.596,7.097,0.592,7.034,0.579,7.004,0.538,6.98,0.523,6.939,0.533,6.888,0.526,6.851,0.548,6.802,0.596,6.742,0.673,6.593,0.702,6.581,0.715,6.549,0.707,6.519,0.737,6.453,0.822,6.386,0.912,6.329,0.985,6.32,1.002,6.269,1.05,6.203,1.084,6.174,1.14,6.155,1.185,6.145,1.187,6.089,1.106,6.051,1.05,5.994,1.008,5.906,0.95,5.81,0.749,5.76,0.672,5.76,0.26,5.757,-0.127,5.568,-0.349,5.501,-0.485,5.394,-0.669,5.319,-0.798,5.227,-1.064,5.183,-1.502,5.038,-1.638,4.981,-1.777,4.88,-2.002,4.762,-2.09,4.764,-2.266,4.874,-2.399,4.929,-2.723,5.014,-2.965,5.046,-3.082,5.082,-3.114,5.089,-3.087,5.128,-3.019,5.131,-2.948,5.119,-2.895,5.149,-2.816,5.153,-2.795,5.185,-2.789,5.264,-2.79,5.328,-2.762,5.357,-2.755,5.433,-2.794,5.6,-2.821,5.619,-2.962,5.643,-2.973,5.676,-2.998,5.711,-3.025,5.798,-3.056,5.926,-3.106,6.086,-3.201,6.348,-3.224,6.441,-3.24,6.536,-3.244,6.649,-3.224,6.691,-3.227,6.749,-3.236,6.807,-3.169,6.941,-3.038,7.105,-3.01,7.164,-2.986,7.205,-2.982,7.264,-2.959,7.455,-2.896,7.685,-2.857,7.772,-2.83,7.819,-2.798,7.896,-2.79,7.932,-2.669,8.022,-2.613,8.047,-2.601,8.082,-2.62,8.121,-2.612,8.148,-2.583,8.161,-2.538,8.172,-2.506,8.209,-2.557,8.493,-2.598,8.776,-2.6,8.8,-2.625,8.84,-2.649,8.957,-2.69,9.025,-2.747,9.045,-2.747,9.11,-2.689,9.219,-2.674,9.283,-2.702,9.302,-2.706,9.351,-2.686,9.432,-2.696,9.481,-2.706,9.534,-2.766,9.658,-2.781,9.746,-2.75,9.797,-2.751,9.91,-2.783,10.083,-2.788,10.193,-2.767,10.238,-2.777,10.282,-2.82,10.323,-2.823,10.363,-2.787,10.402,-2.791,10.432,-2.837,10.455,-2.878,10.508,-2.915,10.592,-2.907,10.728,-2.839,10.977,-2.83,10.998,-2.752,10.997,-2.752,10.986,-2.509,10.989,-2.232,10.991,-1.901,10.995,-1.6,10.998,-1.586,11.009,-1.537,11.023,-1.233,10.997,-1.042,11.01,-0.962,11.002,-0.903,10.985,-0.772,10.995,-0.701,10.989,-0.649,10.927,-0.627,10.927,-0.598,10.954,-0.545,10.984,-0.492,11.008,-0.454,11.056,-0.43,11.093,-0.396,11.086,-0.346,11.088,-0.313,11.119,-0.299,11.167,-0.069,11.116]]]},{"n":"Germany","c":"DE","r":2,"ml":1.7,"lx":9.68,"ly":50.96,"b":[5.858,47.279,15.017,55.059],"p":[[[9.524,47.524,9.35,47.599,9.183,47.671,9.128,47.671,8.881,47.656,8.874,47.663,8.831,47.704,8.793,47.717,8.77,47.71,8.755,47.698,8.728,47.7,8.618,47.766,8.573,47.776,8.51,47.767,8.436,47.731,8.403,47.688,8.413,47.663,8.452,47.652,8.552,47.659,8.567,47.652,8.571,47.638,8.559,47.624,8.478,47.613,8.454,47.596,8.43,47.592,8.415,47.59,8.328,47.607,8.198,47.607,8.094,47.576,7.927,47.564,7.698,47.57,7.616,47.593,7.565,47.607,7.529,47.674,7.539,47.774,7.593,47.906,7.608,48.003,7.584,48.064,7.617,48.157,7.706,48.28,7.765,48.41,7.795,48.547,7.838,48.636,7.923,48.699,8.124,48.873,8.14,48.886,8.135,48.974,8.081,48.986,8.001,49.011,7.799,49.042,7.611,49.062,7.525,49.086,7.451,49.152,7.404,49.153,7.313,49.13,7.2,49.114,7.117,49.128,7.066,49.125,7.037,49.113,7.022,49.123,7.001,49.18,6.958,49.195,6.891,49.208,6.85,49.202,6.821,49.174,6.776,49.154,6.735,49.161,6.608,49.291,6.575,49.32,6.566,49.346,6.534,49.395,6.458,49.443,6.382,49.458,6.344,49.453,6.348,49.513,6.378,49.6,6.407,49.645,6.445,49.682,6.485,49.708,6.494,49.754,6.487,49.798,6.441,49.805,6.325,49.838,6.256,49.872,6.205,49.915,6.138,49.974,6.11,50.034,6.108,50.094,6.117,50.121,6.121,50.139,6.175,50.233,6.364,50.316,6.344,50.4,6.341,50.452,6.295,50.485,6.203,50.499,6.179,50.523,6.168,50.545,6.236,50.597,6.154,50.637,6.119,50.679,6.006,50.732,5.994,50.75,6.048,50.905,6.007,50.95,5.955,50.973,5.895,50.984,5.867,51.006,5.858,51.03,5.868,51.045,5.939,51.041,5.961,51.057,6.13,51.147,6.137,51.165,6.113,51.175,6.082,51.18,6.075,51.199,6.076,51.224,6.166,51.355,6.193,51.411,6.199,51.45,6.193,51.489,6.142,51.55,6.091,51.599,6.089,51.638,6.053,51.658,5.949,51.762,5.949,51.803,6.008,51.834,6.09,51.854,6.117,51.87,6.167,51.881,6.297,51.851,6.356,51.825,6.372,51.83,6.425,51.858,6.518,51.854,6.742,51.911,6.775,51.938,6.8,51.967,6.802,51.98,6.716,52.036,6.713,52.057,6.725,52.08,6.749,52.099,6.8,52.111,6.855,52.136,6.977,52.206,7.02,52.266,7.033,52.331,7.035,52.38,7.002,52.419,6.968,52.444,6.922,52.44,6.833,52.442,6.749,52.464,6.703,52.499,6.692,52.53,6.712,52.55,6.719,52.574,6.705,52.598,6.711,52.618,6.748,52.634,7.013,52.634,7.033,52.651,7.051,52.745,7.117,52.887,7.179,52.966,7.19,53,7.189,53.187,7.197,53.282,7.152,53.327,7.053,53.376,7.074,53.478,7.107,53.557,7.206,53.655,7.285,53.681,7.629,53.697,8.009,53.691,8.167,53.543,8.108,53.468,8.201,53.432,8.245,53.445,8.279,53.511,8.302,53.584,8.334,53.606,8.451,53.552,8.493,53.514,8.495,53.394,8.538,53.557,8.506,53.671,8.528,53.781,8.576,53.838,8.619,53.875,8.898,53.836,9.206,53.856,9.322,53.813,9.585,53.6,9.673,53.566,9.784,53.555,9.631,53.6,9.312,53.859,9.216,53.891,9.07,53.901,8.978,53.926,8.92,53.965,8.904,54,8.907,54.261,8.852,54.3,8.78,54.313,8.736,54.295,8.645,54.295,8.626,54.354,8.648,54.398,8.831,54.428,8.952,54.468,8.957,54.538,8.881,54.594,8.79,54.696,8.682,54.792,8.67,54.903,8.671,54.903,8.857,54.901,8.903,54.897,9.186,54.845,9.255,54.808,9.342,54.806,9.499,54.84,9.616,54.855,9.661,54.834,9.725,54.826,9.74,54.826,9.746,54.807,9.892,54.781,9.954,54.738,10.022,54.674,10.029,54.581,9.941,54.515,9.869,54.472,10.143,54.488,10.171,54.45,10.212,54.409,10.36,54.438,10.732,54.316,10.956,54.376,11.013,54.379,11.064,54.281,11.009,54.181,10.811,54.075,10.855,54.01,10.918,53.995,11.104,54.009,11.4,53.945,11.461,53.965,11.701,54.114,11.796,54.145,12.111,54.168,12.169,54.226,12.296,54.284,12.379,54.347,12.575,54.467,12.779,54.446,12.898,54.423,13.029,54.411,13.147,54.283,13.448,54.141,13.724,54.153,13.822,54.019,13.866,53.853,13.95,53.801,14.025,53.767,14.25,53.732,14.259,53.73,14.266,53.707,14.28,53.625,14.299,53.556,14.415,53.283,14.412,53.217,14.411,53.199,14.369,53.106,14.293,53.027,14.194,52.982,14.139,52.933,14.129,52.878,14.254,52.783,14.514,52.646,14.619,52.529,14.57,52.431,14.555,52.36,14.574,52.314,14.616,52.278,14.68,52.25,14.705,52.207,14.692,52.15,14.705,52.11,14.753,52.082,14.748,52.071,14.725,52.031,14.693,51.958,14.675,51.905,14.602,51.832,14.624,51.771,14.681,51.698,14.725,51.662,14.739,51.627,14.711,51.545,14.725,51.524,14.906,51.463,14.936,51.435,14.953,51.377,15.017,51.253,14.964,51.095,14.917,51.009,14.814,50.872,14.809,50.859,14.797,50.842,14.767,50.818,14.723,50.815,14.658,50.833,14.614,50.856,14.624,50.915,14.595,50.919,14.56,50.955,14.546,50.994,14.507,51.01,14.367,51.026,14.32,51.038,14.283,51.029,14.256,51.002,14.273,50.977,14.299,50.953,14.377,50.914,14.369,50.899,14.202,50.861,14.096,50.823,13.998,50.801,13.899,50.761,13.701,50.717,13.557,50.705,13.527,50.693,13.473,50.617,13.436,50.601,13.401,50.609,13.375,50.622,13.341,50.611,13.306,50.586,13.27,50.576,13.238,50.577,13.181,50.51,13.016,50.49,12.997,50.456,12.967,50.416,12.943,50.406,12.868,50.422,12.765,50.431,12.706,50.409,12.636,50.397,12.549,50.393,12.453,50.35,12.359,50.273,12.306,50.206,12.277,50.181,12.231,50.245,12.175,50.288,12.135,50.311,12.099,50.311,12.09,50.302,12.09,50.269,12.128,50.213,12.175,50.176,12.183,50.148,12.208,50.098,12.276,50.042,12.384,49.999,12.458,49.956,12.512,49.896,12.513,49.877,12.498,49.853,12.472,49.83,12.45,49.8,12.391,49.74,12.408,49.713,12.457,49.68,12.5,49.64,12.556,49.575,12.632,49.461,12.681,49.415,12.748,49.366,12.813,49.329,12.917,49.33,13.024,49.26,13.141,49.158,13.228,49.112,13.289,49.097,13.339,49.061,13.384,49.008,13.401,48.978,13.441,48.956,13.548,48.96,13.685,48.877,13.77,48.816,13.815,48.767,13.803,48.748,13.797,48.686,13.799,48.622,13.785,48.587,13.724,48.542,13.692,48.533,13.675,48.523,13.487,48.582,13.472,48.572,13.46,48.565,13.409,48.394,13.375,48.361,13.323,48.331,13.215,48.302,13.14,48.29,13.082,48.275,12.897,48.204,12.814,48.161,12.76,48.107,12.76,48.076,12.85,47.985,12.954,47.891,12.954,47.808,12.908,47.746,12.898,47.722,12.928,47.713,12.986,47.709,13.034,47.699,13.054,47.655,13.048,47.579,13.032,47.508,13.014,47.478,12.968,47.476,12.879,47.506,12.809,47.542,12.783,47.564,12.781,47.59,12.796,47.607,12.771,47.639,12.686,47.669,12.594,47.656,12.527,47.636,12.483,47.637,12.436,47.666,12.363,47.688,12.268,47.703,12.209,47.718,12.197,47.709,12.204,47.647,12.186,47.62,11.717,47.583,11.574,47.55,11.47,47.506,11.393,47.487,11.374,47.46,11.298,47.425,11.212,47.414,11.191,47.425,11.136,47.409,11.042,47.393,10.981,47.398,10.952,47.427,10.894,47.47,10.871,47.501,10.873,47.52,10.742,47.524,10.659,47.547,10.483,47.542,10.439,47.552,10.43,47.541,10.404,47.417,10.369,47.366,10.313,47.313,10.241,47.284,10.183,47.279,10.186,47.317,10.2,47.363,10.159,47.374,10.096,47.38,10.066,47.393,10.074,47.429,10.06,47.449,10.034,47.474,9.972,47.505,9.839,47.552,9.749,47.576,9.715,47.551,9.651,47.526,9.549,47.534,9.524,47.524]],[[13.709,54.383,13.734,54.315,13.707,54.281,13.595,54.338,13.482,54.337,13.415,54.25,13.364,54.246,13.19,54.326,13.162,54.365,13.156,54.397,13.181,54.509,13.177,54.544,13.231,54.583,13.24,54.638,13.337,54.697,13.423,54.699,13.45,54.65,13.491,54.615,13.636,54.577,13.658,54.56,13.671,54.535,13.603,54.488,13.58,54.464,13.602,54.425,13.709,54.383]],[[14.211,53.95,14.198,53.919,14.214,53.871,14.172,53.874,14.048,53.863,13.926,53.879,13.902,53.939,13.922,53.997,13.872,54.036,13.827,54.06,13.82,54.093,13.828,54.127,14.039,54.035,14.211,53.95]],[[11.283,54.418,11.129,54.416,11.071,54.456,11.012,54.466,11.043,54.515,11.085,54.533,11.234,54.501,11.28,54.438,11.283,54.418]],[[8.308,54.787,8.285,54.767,8.296,54.908,8.405,55.059,8.451,55.055,8.404,55.015,8.39,54.986,8.371,54.929,8.38,54.9,8.63,54.892,8.601,54.865,8.347,54.848,8.308,54.787]],[[8.588,54.713,8.549,54.688,8.454,54.691,8.4,54.714,8.418,54.739,8.468,54.757,8.51,54.76,8.573,54.749,8.588,54.713]]]},{"n":"Georgia","c":"GE","r":5,"ml":4,"lx":43.74,"ly":41.87,"b":[39.978,41.07,46.673,43.57],"p":[[[43.439,41.107,43.442,41.126,43.433,41.156,43.402,41.177,43.359,41.19,43.279,41.185,43.205,41.199,43.153,41.236,43.141,41.265,43.171,41.288,43.149,41.307,43.057,41.353,42.907,41.467,42.822,41.492,42.788,41.564,42.754,41.579,42.682,41.586,42.607,41.579,42.59,41.571,42.567,41.559,42.508,41.47,42.466,41.44,42.364,41.454,42.28,41.475,42.211,41.487,42.078,41.494,41.926,41.496,41.824,41.432,41.779,41.441,41.702,41.472,41.577,41.497,41.51,41.517,41.702,41.705,41.759,41.817,41.761,41.885,41.763,41.97,41.663,42.147,41.578,42.398,41.489,42.659,41.419,42.738,41.129,42.828,41.062,42.931,40.837,43.063,40.524,43.121,40.462,43.146,40.191,43.312,39.978,43.42,40.024,43.485,40.085,43.553,40.15,43.57,40.342,43.543,40.519,43.512,40.648,43.534,40.802,43.48,40.942,43.418,41.083,43.374,41.358,43.333,41.461,43.276,41.581,43.219,42.05,43.19,42.088,43.199,42.122,43.207,42.28,43.228,42.419,43.224,42.566,43.155,42.66,43.159,42.761,43.17,42.89,43.133,42.992,43.092,43,43.05,43.089,42.989,43.348,42.897,43.558,42.844,43.623,42.808,43.783,42.747,43.799,42.728,43.795,42.703,43.75,42.658,43.738,42.617,43.76,42.594,43.826,42.572,43.957,42.567,44.005,42.596,44.103,42.616,44.2,42.654,44.329,42.704,44.506,42.749,44.576,42.748,44.644,42.735,44.692,42.71,44.771,42.617,44.85,42.747,44.871,42.756,44.943,42.73,45.072,42.694,45.16,42.675,45.208,42.648,45.344,42.53,45.563,42.536,45.656,42.518,45.705,42.498,45.728,42.475,45.688,42.357,45.634,42.235,45.639,42.205,45.727,42.159,45.846,42.11,45.91,42.071,45.954,42.035,46.048,42.009,46.16,41.992,46.213,41.99,46.268,41.96,46.412,41.905,46.43,41.891,46.405,41.855,46.348,41.79,46.303,41.757,46.252,41.752,46.202,41.737,46.184,41.702,46.182,41.657,46.191,41.625,46.204,41.613,46.255,41.602,46.305,41.508,46.385,41.46,46.509,41.406,46.619,41.344,46.673,41.287,46.662,41.246,46.626,41.16,46.534,41.089,46.458,41.07,46.431,41.077,46.381,41.099,46.28,41.154,46.171,41.198,46.087,41.184,46.031,41.167,45.922,41.187,45.793,41.224,45.725,41.262,45.696,41.289,45.716,41.338,45.422,41.425,45.281,41.45,45.217,41.423,45.001,41.291,44.976,41.277,44.811,41.259,44.811,41.249,44.849,41.22,44.841,41.211,44.565,41.208,44.473,41.191,44.227,41.213,44.146,41.203,44.077,41.183,43.909,41.159,43.793,41.131,43.645,41.117,43.492,41.116,43.439,41.107]]]},{"n":"The Gambia","c":"GM","r":6,"ml":5,"lx":-15,"ly":13.64,"b":[-16.825,13.064,-13.827,13.812],"p":[[[-16.562,13.587,-16.309,13.597,-16.002,13.593,-15.667,13.588,-15.51,13.586,-15.427,13.727,-15.27,13.789,-15.108,13.812,-15.024,13.806,-14.936,13.785,-14.766,13.669,-14.66,13.643,-14.571,13.616,-14.507,13.56,-14.405,13.504,-14.326,13.489,-14.278,13.497,-14.199,13.519,-14.147,13.536,-13.977,13.543,-13.853,13.479,-13.827,13.408,-13.848,13.335,-14.015,13.296,-14.247,13.236,-14.439,13.269,-14.672,13.352,-14.808,13.411,-14.865,13.435,-14.95,13.473,-15.025,13.513,-15.096,13.54,-15.151,13.556,-15.192,13.535,-15.212,13.485,-15.245,13.429,-15.286,13.396,-15.482,13.376,-15.657,13.356,-15.752,13.338,-15.814,13.325,-15.834,13.156,-16.033,13.158,-16.228,13.16,-16.431,13.157,-16.649,13.154,-16.705,13.12,-16.763,13.064,-16.769,13.148,-16.825,13.341,-16.75,13.425,-16.669,13.475,-16.615,13.435,-16.598,13.357,-16.556,13.303,-16.413,13.27,-16.272,13.294,-16.185,13.283,-16.188,13.326,-16.158,13.384,-15.986,13.409,-15.804,13.425,-15.618,13.46,-15.471,13.459,-15.427,13.468,-15.438,13.483,-15.57,13.5,-15.85,13.46,-16.135,13.448,-16.352,13.343,-16.441,13.353,-16.53,13.458,-16.562,13.587]]]},{"n":"Gabon","c":"GA","r":4,"ml":3,"lx":11.84,"ly":-0.44,"b":[8.703,-3.916,14.481,2.302],"p":[[[13.294,2.162,13.289,2.092,13.209,1.92,13.172,1.789,13.163,1.648,13.185,1.535,13.223,1.455,13.247,1.367,13.228,1.305,13.19,1.279,13.216,1.248,13.274,1.241,13.372,1.268,13.523,1.315,13.721,1.382,13.851,1.419,14.066,1.396,14.181,1.37,14.24,1.323,14.303,1.121,14.334,1.09,14.386,1.004,14.43,0.901,14.439,0.849,14.434,0.811,14.391,0.756,14.342,0.674,14.324,0.624,14.283,0.587,14.231,0.551,14.088,0.537,14.066,0.515,14.025,0.428,13.95,0.354,13.915,0.284,13.885,0.191,13.891,0.075,13.875,-0.091,13.86,-0.203,13.898,-0.243,14.069,-0.27,14.103,-0.292,14.148,-0.362,14.207,-0.427,14.364,-0.469,14.425,-0.519,14.474,-0.573,14.481,-0.618,14.445,-0.799,14.411,-0.972,14.424,-1.104,14.437,-1.23,14.456,-1.413,14.447,-1.525,14.403,-1.593,14.403,-1.647,14.423,-1.712,14.384,-1.89,14.359,-1.92,14.288,-1.954,14.251,-2.001,14.24,-2.077,14.202,-2.18,14.163,-2.218,14.163,-2.266,14.2,-2.301,14.2,-2.354,14.13,-2.418,14.087,-2.467,13.994,-2.491,13.887,-2.465,13.862,-2.43,13.888,-2.375,13.879,-2.33,13.842,-2.284,13.784,-2.164,13.734,-2.138,13.706,-2.187,13.619,-2.279,13.465,-2.395,13.357,-2.405,13.159,-2.369,12.992,-2.313,12.914,-2.176,12.864,-2.063,12.794,-1.932,12.714,-1.869,12.628,-1.83,12.59,-1.827,12.469,-1.9,12.432,-1.929,12.432,-1.99,12.444,-2.048,12.463,-2.075,12.479,-2.112,12.476,-2.169,12.454,-2.246,12.446,-2.33,12.064,-2.413,11.998,-2.383,11.95,-2.345,11.892,-2.351,11.727,-2.395,11.666,-2.365,11.605,-2.343,11.578,-2.361,11.575,-2.397,11.603,-2.595,11.595,-2.671,11.557,-2.77,11.538,-2.837,11.639,-2.855,11.676,-2.887,11.711,-2.937,11.76,-2.983,11.763,-3.011,11.708,-3.063,11.689,-3.127,11.715,-3.177,11.784,-3.229,11.885,-3.283,11.934,-3.319,11.929,-3.351,11.883,-3.42,11.865,-3.479,11.833,-3.531,11.839,-3.58,11.885,-3.625,11.88,-3.666,11.849,-3.697,11.786,-3.69,11.733,-3.695,11.686,-3.682,11.537,-3.525,11.504,-3.52,11.288,-3.641,11.234,-3.691,11.19,-3.762,11.13,-3.916,11.032,-3.826,10.947,-3.662,10.849,-3.561,10.641,-3.398,10.585,-3.278,10.348,-3.013,10.006,-2.748,9.759,-2.519,9.722,-2.468,9.764,-2.474,10.002,-2.588,10.034,-2.576,10.062,-2.55,9.959,-2.49,9.861,-2.443,9.769,-2.413,9.676,-2.416,9.625,-2.367,9.591,-2.293,9.574,-2.23,9.533,-2.164,9.402,-2.028,9.371,-1.975,9.299,-1.903,9.342,-1.894,9.483,-1.962,9.495,-1.935,9.483,-1.895,9.342,-1.829,9.266,-1.825,9.248,-1.779,9.258,-1.726,9.158,-1.528,9.053,-1.379,9.036,-1.309,9.319,-1.632,9.357,-1.638,9.406,-1.635,9.523,-1.598,9.501,-1.555,9.448,-1.509,9.397,-1.53,9.331,-1.535,9.296,-1.515,9.28,-1.482,9.347,-1.325,9.318,-1.333,9.297,-1.361,9.26,-1.374,9.204,-1.382,9.065,-1.298,8.942,-1.071,8.909,-1.025,8.877,-0.946,8.844,-0.914,8.703,-0.591,8.757,-0.615,8.821,-0.708,8.946,-0.689,8.995,-0.635,9.038,-0.637,9.082,-0.624,9.137,-0.573,9.297,-0.351,9.339,-0.058,9.325,0.116,9.302,0.289,9.355,0.344,9.376,0.307,9.386,0.246,9.411,0.2,9.468,0.16,9.574,0.149,9.738,0.085,9.797,0.044,9.813,0.126,10.001,0.195,9.944,0.22,9.777,0.192,9.546,0.296,9.47,0.362,9.399,0.487,9.325,0.552,9.33,0.611,9.495,0.665,9.539,0.659,9.557,0.594,9.601,0.568,9.618,0.577,9.625,0.632,9.626,0.779,9.575,0.991,9.591,1.032,9.636,1.047,9.676,1.075,9.705,1.08,9.761,1.075,9.789,1.026,9.804,0.999,9.86,0.986,9.907,0.96,9.947,0.967,9.98,0.998,10.029,1.004,10.179,1.004,10.315,1.003,10.587,1.002,10.859,1.001,11.131,1,11.335,1,11.335,1.121,11.334,1.308,11.332,1.528,11.331,1.74,11.33,1.936,11.329,2.167,11.34,2.234,11.353,2.261,11.348,2.3,11.559,2.302,11.94,2.285,12.106,2.288,12.153,2.284,12.361,2.296,12.53,2.281,12.601,2.265,12.666,2.257,12.867,2.247,13.131,2.259,13.22,2.256,13.27,2.224,13.294,2.162]]]},{"n":"France","c":"FR","r":2,"ml":1.7,"lx":2.55,"ly":46.7,"b":[-61.794,-21.369,55.839,51.097],"p":[[[9.48,42.805,9.454,42.659,9.473,42.616,9.509,42.586,9.526,42.553,9.556,42.161,9.551,42.13,9.428,41.972,9.401,41.926,9.395,41.731,9.374,41.679,9.331,41.627,9.253,41.46,9.186,41.385,9.003,41.477,8.895,41.516,8.842,41.559,8.808,41.588,8.83,41.628,8.879,41.669,8.887,41.701,8.771,41.737,8.718,41.761,8.719,41.804,8.759,41.87,8.74,41.925,8.674,41.922,8.622,41.931,8.615,41.959,8.653,41.996,8.703,42.043,8.701,42.096,8.642,42.118,8.588,42.161,8.566,42.219,8.608,42.258,8.675,42.284,8.626,42.343,8.592,42.345,8.566,42.358,8.588,42.385,8.64,42.427,8.713,42.55,8.815,42.608,8.995,42.645,9.044,42.662,9.088,42.705,9.138,42.733,9.198,42.729,9.254,42.712,9.288,42.695,9.313,42.713,9.338,42.767,9.323,42.814,9.331,42.944,9.363,43.017,9.415,43.021,9.463,42.981,9.461,42.945,9.479,42.86,9.48,42.805]],[[7.616,47.593,7.495,47.547,7.467,47.508,7.42,47.455,7.343,47.433,7.266,47.426,7.203,47.433,7.167,47.454,7.169,47.473,7.136,47.49,7.053,47.489,6.968,47.453,6.9,47.394,6.921,47.361,6.984,47.353,7.001,47.339,7.001,47.323,6.979,47.302,6.952,47.267,6.821,47.163,6.688,47.058,6.667,47.027,6.625,47.004,6.456,46.948,6.439,46.926,6.429,46.832,6.41,46.755,6.285,46.683,6.161,46.611,6.13,46.567,6.107,46.516,6.068,46.459,6.06,46.428,6.123,46.379,6.116,46.338,6.096,46.279,6.036,46.238,5.97,46.215,5.971,46.151,6.007,46.142,6.087,46.147,6.199,46.193,6.273,46.252,6.23,46.308,6.224,46.319,6.235,46.333,6.322,46.394,6.429,46.431,6.578,46.437,6.758,46.416,6.776,46.407,6.767,46.369,6.784,46.314,6.817,46.275,6.772,46.165,6.806,46.131,6.858,46.089,6.897,46.052,6.954,46.017,7.004,45.959,7.021,45.926,6.941,45.868,6.804,45.815,6.789,45.78,6.791,45.741,6.806,45.71,6.881,45.67,6.962,45.581,7.014,45.5,7.126,45.424,7.153,45.401,7.146,45.382,7.117,45.349,7.078,45.24,7.032,45.223,6.981,45.216,6.842,45.136,6.78,45.145,6.692,45.144,6.628,45.118,6.635,45.068,6.691,45.023,6.725,44.973,6.738,44.921,6.801,44.883,6.889,44.86,6.94,44.859,6.973,44.845,6.993,44.827,7.031,44.717,7.008,44.689,6.96,44.677,6.932,44.632,6.875,44.565,6.843,44.511,6.879,44.463,6.894,44.428,6.875,44.392,6.9,44.336,6.967,44.28,7.149,44.202,7.319,44.138,7.371,44.127,7.599,44.168,7.637,44.165,7.665,44.116,7.677,44.083,7.651,44.034,7.59,43.965,7.523,43.911,7.482,43.865,7.491,43.823,7.493,43.767,7.439,43.75,7.437,43.761,7.414,43.771,7.395,43.765,7.38,43.753,7.378,43.732,7.262,43.696,7.181,43.659,6.865,43.438,6.717,43.374,6.687,43.335,6.657,43.262,6.57,43.199,6.494,43.169,6.305,43.139,6.116,43.072,6.031,43.101,5.809,43.098,5.672,43.178,5.407,43.229,5.32,43.345,5.2,43.352,5.12,43.349,5.073,43.367,5.061,43.406,5.06,43.445,4.976,43.427,4.912,43.427,4.874,43.412,4.844,43.394,4.808,43.405,4.787,43.401,4.789,43.379,4.712,43.373,4.629,43.387,4.41,43.447,4.376,43.456,4.224,43.48,4.163,43.504,4.113,43.563,4.075,43.582,4.053,43.593,3.911,43.563,3.862,43.516,3.785,43.462,3.259,43.193,3.163,43.081,3.052,42.915,3.043,42.838,3.091,42.591,3.198,42.461,3.211,42.431,3.152,42.431,3.053,42.447,2.97,42.467,2.891,42.456,2.816,42.429,2.749,42.413,2.702,42.408,2.67,42.393,2.655,42.362,2.652,42.34,2.568,42.346,2.374,42.39,2.2,42.421,2.098,42.386,2.033,42.354,1.987,42.358,1.951,42.393,1.928,42.426,1.86,42.457,1.706,42.503,1.714,42.526,1.74,42.557,1.739,42.576,1.71,42.604,1.568,42.635,1.501,42.643,1.459,42.622,1.428,42.596,1.349,42.691,1.293,42.71,1.208,42.713,1.111,42.742,1.01,42.779,0.764,42.838,0.697,42.845,0.67,42.836,0.652,42.8,0.642,42.701,0.632,42.69,0.518,42.686,0.377,42.7,0.313,42.693,0.255,42.693,0.201,42.719,-0.041,42.689,-0.081,42.704,-0.14,42.749,-0.205,42.785,-0.256,42.804,-0.299,42.825,-0.339,42.829,-0.398,42.808,-0.481,42.799,-0.55,42.802,-0.586,42.799,-0.74,42.91,-0.763,42.94,-0.839,42.948,-0.934,42.95,-1.175,43.021,-1.285,43.06,-1.302,43.082,-1.3,43.101,-1.319,43.097,-1.353,43.064,-1.371,43.038,-1.394,43.033,-1.429,43.037,-1.461,43.052,-1.48,43.071,-1.459,43.105,-1.423,43.149,-1.407,43.197,-1.411,43.24,-1.472,43.268,-1.561,43.279,-1.627,43.282,-1.713,43.307,-1.753,43.325,-1.793,43.373,-1.794,43.407,-1.631,43.438,-1.485,43.564,-1.346,44.02,-1.246,44.56,-1.171,44.662,-1.077,44.69,-1.153,44.764,-1.2,44.726,-1.22,44.687,-1.245,44.667,-1.189,45.161,-1.149,45.343,-1.081,45.532,-0.942,45.457,-0.826,45.381,-0.767,45.314,-0.691,45.093,-0.634,45.047,-0.548,45.001,-0.582,45.051,-0.641,45.09,-0.733,45.385,-0.791,45.468,-0.881,45.538,-1.17,45.686,-1.196,45.714,-1.21,45.771,-1.114,45.769,-1.032,45.741,-1.042,45.773,-1.066,45.806,-1.104,45.925,-1.136,46.205,-1.132,46.253,-1.146,46.311,-1.239,46.325,-1.313,46.327,-1.392,46.35,-1.787,46.515,-1.921,46.685,-2.059,46.81,-2.092,46.865,-2.09,46.921,-2.019,47.038,-2.082,47.112,-2.144,47.126,-2.197,47.163,-2.149,47.224,-2.108,47.263,-2.028,47.274,-1.922,47.261,-1.821,47.225,-1.743,47.216,-1.975,47.311,-2.353,47.279,-2.434,47.291,-2.503,47.312,-2.53,47.382,-2.476,47.413,-2.428,47.471,-2.483,47.512,-2.554,47.527,-2.666,47.526,-2.77,47.514,-2.797,47.537,-2.733,47.602,-2.787,47.626,-2.859,47.614,-2.964,47.601,-3.064,47.621,-3.159,47.695,-3.222,47.694,-3.265,47.685,-3.329,47.713,-3.396,47.72,-3.444,47.711,-3.508,47.753,-3.901,47.838,-4.071,47.848,-4.226,47.81,-4.312,47.823,-4.375,47.877,-4.428,47.969,-4.679,48.04,-4.629,48.086,-4.512,48.097,-4.378,48.129,-4.329,48.17,-4.435,48.218,-4.512,48.23,-4.544,48.247,-4.577,48.29,-4.531,48.31,-4.498,48.299,-4.403,48.293,-4.241,48.304,-4.302,48.347,-4.364,48.357,-4.393,48.368,-4.525,48.372,-4.585,48.358,-4.719,48.363,-4.749,48.41,-4.762,48.45,-4.721,48.54,-4.531,48.62,-4.059,48.708,-3.856,48.695,-3.715,48.71,-3.546,48.766,-3.471,48.813,-3.231,48.841,-3.003,48.791,-2.793,48.601,-2.692,48.537,-2.446,48.648,-2.079,48.645,-2.004,48.582,-1.973,48.635,-1.906,48.697,-1.852,48.669,-1.825,48.631,-1.438,48.641,-1.376,48.653,-1.48,48.698,-1.565,48.806,-1.583,49.202,-1.69,49.313,-1.813,49.49,-1.87,49.595,-1.875,49.631,-1.856,49.684,-1.705,49.681,-1.588,49.668,-1.366,49.707,-1.259,49.68,-1.265,49.598,-1.232,49.495,-1.195,49.445,-1.139,49.388,-0.959,49.393,-0.766,49.36,-0.521,49.355,-0.163,49.297,-0.011,49.33,0.136,49.402,0.417,49.448,0.439,49.473,0.278,49.463,0.129,49.508,0.109,49.558,0.127,49.602,0.187,49.703,0.616,49.863,0.924,49.91,1.246,49.998,1.407,50.089,1.514,50.205,1.548,50.231,1.593,50.252,1.552,50.294,1.579,50.739,1.61,50.819,1.672,50.885,1.768,50.936,1.913,50.991,2.446,51.067,2.525,51.097,2.536,51.05,2.575,50.989,2.601,50.955,2.579,50.912,2.597,50.876,2.669,50.811,2.759,50.751,2.84,50.712,2.862,50.716,2.922,50.727,3.023,50.767,3.107,50.779,3.155,50.749,3.182,50.732,3.235,50.663,3.25,50.591,3.273,50.532,3.316,50.507,3.477,50.499,3.595,50.477,3.627,50.457,3.667,50.325,3.689,50.306,3.719,50.322,3.748,50.344,3.789,50.347,3.858,50.339,3.95,50.336,4.044,50.321,4.175,50.246,4.17,50.222,4.144,50.178,4.135,50.144,4.158,50.13,4.192,50.094,4.184,50.053,4.15,50.024,4.137,50,4.137,49.984,4.149,49.972,4.176,49.96,4.369,49.945,4.545,49.96,4.656,50.002,4.675,50.047,4.707,50.097,4.773,50.139,4.819,50.153,4.861,50.136,4.79,49.96,4.842,49.915,4.849,49.847,4.868,49.788,4.931,49.789,5.007,49.778,5.061,49.757,5.124,49.721,5.215,49.689,5.279,49.678,5.302,49.651,5.354,49.62,5.435,49.554,5.507,49.511,5.542,49.511,5.61,49.528,5.71,49.539,5.79,49.538,5.823,49.505,5.901,49.49,5.929,49.478,5.959,49.455,6.011,49.445,6.074,49.455,6.12,49.485,6.181,49.499,6.242,49.494,6.277,49.478,6.344,49.453,6.382,49.458,6.458,49.443,6.534,49.395,6.566,49.346,6.575,49.32,6.608,49.291,6.735,49.161,6.776,49.154,6.821,49.174,6.85,49.202,6.891,49.208,6.958,49.195,7.001,49.18,7.022,49.123,7.037,49.113,7.066,49.125,7.117,49.128,7.2,49.114,7.313,49.13,7.404,49.153,7.451,49.152,7.525,49.086,7.611,49.062,7.799,49.042,8.001,49.011,8.081,48.986,8.135,48.974,8.14,48.886,8.124,48.873,7.923,48.699,7.838,48.636,7.795,48.547,7.765,48.41,7.706,48.28,7.617,48.157,7.584,48.064,7.608,48.003,7.593,47.906,7.539,47.774,7.529,47.674,7.565,47.607,7.616,47.593]],[[-1.178,45.904,-1.214,45.817,-1.28,45.897,-1.369,45.968,-1.389,46.033,-1.389,46.05,-1.285,46.003,-1.178,45.904]],[[45.18,-12.977,45.118,-12.985,45.088,-12.958,45.069,-12.896,45.088,-12.835,45.094,-12.786,45.043,-12.701,45.092,-12.653,45.135,-12.709,45.159,-12.713,45.223,-12.752,45.204,-12.824,45.209,-12.848,45.179,-12.92,45.18,-12.977]],[[55.797,-21.339,55.656,-21.369,55.558,-21.358,55.363,-21.274,55.31,-21.217,55.233,-21.058,55.25,-21.002,55.311,-20.904,55.45,-20.865,55.596,-20.88,55.662,-20.906,55.739,-21.021,55.839,-21.139,55.822,-21.278,55.797,-21.339]],[[-60.826,14.494,-60.837,14.437,-60.862,14.426,-60.899,14.474,-61.064,14.467,-61.089,14.51,-61.09,14.53,-61.011,14.602,-61.104,14.621,-61.141,14.652,-61.22,14.804,-61.213,14.849,-61.181,14.872,-61.127,14.875,-61.027,14.826,-60.953,14.756,-60.927,14.755,-60.919,14.735,-60.934,14.686,-60.889,14.645,-60.87,14.614,-60.826,14.494]],[[-61.327,16.23,-61.445,16.219,-61.522,16.228,-61.54,16.3,-61.501,16.36,-61.529,16.434,-61.511,16.478,-61.471,16.507,-61.406,16.468,-61.396,16.413,-61.355,16.363,-61.173,16.256,-61.327,16.23]],[[-61.59,16.007,-61.67,15.962,-61.71,15.976,-61.759,16.062,-61.794,16.301,-61.767,16.34,-61.748,16.355,-61.642,16.326,-61.597,16.292,-61.552,16.271,-61.575,16.227,-61.564,16.048,-61.59,16.007]],[[-61.23,15.89,-61.286,15.886,-61.311,15.895,-61.318,15.955,-61.275,15.996,-61.25,16.006,-61.212,15.96,-61.203,15.921,-61.23,15.89]],[[-54.616,2.327,-54.605,2.336,-54.568,2.343,-54.536,2.343,-54.486,2.416,-54.402,2.462,-54.257,2.714,-54.196,2.818,-54.188,2.875,-54.171,2.994,-54.203,3.138,-54.188,3.179,-54.063,3.353,-54.01,3.449,-54.006,3.531,-53.99,3.59,-54.006,3.62,-54.034,3.629,-54.082,3.706,-54.113,3.769,-54.197,3.834,-54.256,3.901,-54.351,4.054,-54.342,4.14,-54.369,4.171,-54.398,4.202,-54.396,4.241,-54.416,4.338,-54.441,4.428,-54.45,4.485,-54.426,4.583,-54.44,4.692,-54.471,4.749,-54.48,4.837,-54.473,4.915,-54.447,4.959,-54.452,5.013,-54.332,5.187,-54.24,5.288,-54.156,5.359,-54.085,5.412,-53.99,5.676,-53.92,5.769,-53.847,5.782,-53.454,5.563,-53.27,5.543,-52.899,5.425,-52.765,5.273,-52.454,5.021,-52.291,4.942,-52.289,4.876,-52.325,4.771,-52.22,4.863,-52.058,4.717,-52.012,4.646,-51.962,4.514,-51.979,4.43,-52.002,4.386,-52.003,4.352,-51.955,4.399,-51.928,4.436,-51.92,4.524,-51.88,4.634,-51.828,4.636,-51.786,4.571,-51.699,4.287,-51.666,4.229,-51.653,4.139,-51.658,4.098,-51.653,4.061,-51.683,4.04,-51.767,3.993,-51.805,3.93,-51.827,3.87,-51.879,3.829,-51.929,3.777,-51.944,3.735,-51.991,3.702,-52,3.647,-52.116,3.452,-52.163,3.365,-52.229,3.272,-52.271,3.237,-52.328,3.182,-52.357,3.118,-52.357,3.052,-52.396,2.972,-52.418,2.904,-52.456,2.864,-52.555,2.648,-52.559,2.573,-52.583,2.529,-52.653,2.426,-52.701,2.364,-52.783,2.317,-52.87,2.267,-52.903,2.212,-52.965,2.184,-53.01,2.182,-53.082,2.202,-53.18,2.211,-53.23,2.205,-53.252,2.232,-53.285,2.295,-53.334,2.34,-53.366,2.324,-53.432,2.279,-53.509,2.253,-53.564,2.262,-53.684,2.293,-53.735,2.309,-53.75,2.335,-53.768,2.355,-53.794,2.346,-53.83,2.313,-53.877,2.278,-53.946,2.233,-54.09,2.15,-54.13,2.121,-54.167,2.137,-54.228,2.153,-54.293,2.154,-54.433,2.208,-54.515,2.245,-54.55,2.293,-54.592,2.314,-54.616,2.327]]]},{"n":"Saint Pierre and Miquelon","c":"PM","r":4,"ml":5,"lx":-56.33,"ly":47.04,"b":[-56.387,46.753,-56.137,47.099],"p":[[[-56.151,46.762,-56.172,46.753,-56.243,46.767,-56.209,46.798,-56.185,46.807,-56.153,46.811,-56.137,46.802,-56.139,46.779,-56.151,46.762]],[[-56.267,46.838,-56.354,46.795,-56.385,46.819,-56.377,46.848,-56.333,46.916,-56.334,46.936,-56.387,47.068,-56.378,47.09,-56.365,47.099,-56.287,47.071,-56.278,47.035,-56.315,46.954,-56.29,46.9,-56.255,46.861,-56.267,46.838]]]},{"n":"Wallis and Futuna","c":"WF","r":4,"ml":4.7,"lx":-178.14,"ly":-14.29,"b":[-178.194,-14.325,-176.128,-13.222],"p":[[[-176.161,-13.333,-176.177,-13.341,-176.195,-13.302,-176.171,-13.243,-176.148,-13.222,-176.128,-13.268,-176.161,-13.333]],[[-178.047,-14.318,-178.103,-14.325,-178.159,-14.312,-178.194,-14.255,-178.178,-14.232,-178.142,-14.243,-178.105,-14.284,-178.044,-14.303,-178.047,-14.318]]]},{"n":"Saint Martin","c":"MF","r":6,"ml":5,"lx":-63.05,"ly":18.08,"b":[-63.123,18.069,-63.009,18.115],"p":[[[-63.011,18.069,-63.123,18.069,-63.115,18.091,-63.063,18.115,-63.025,18.113,-63.009,18.104,-63.011,18.069]]]},{"n":"Saint Barthélemy","c":"BL","r":6,"ml":5.7,"lx":-62.83,"ly":17.9,"b":[-62.875,17.875,-62.8,17.922],"p":[[[-62.832,17.876,-62.847,17.875,-62.859,17.884,-62.869,17.899,-62.875,17.914,-62.874,17.922,-62.865,17.918,-62.8,17.909,-62.807,17.898,-62.818,17.885,-62.832,17.876]]]},{"n":"French Polynesia","c":"PF","r":4,"ml":3.5,"lx":-149.46,"ly":-17.63,"b":[-151.512,-20.876,-136.294,-8.782],"p":[[[-151.467,-16.658,-151.485,-16.665,-151.504,-16.647,-151.512,-16.619,-151.506,-16.574,-151.457,-16.604,-151.438,-16.623,-151.467,-16.658]],[[-149.814,-17.545,-149.845,-17.571,-149.887,-17.553,-149.905,-17.528,-149.912,-17.501,-149.902,-17.47,-149.809,-17.474,-149.782,-17.488,-149.814,-17.545]],[[-151.41,-16.878,-151.449,-16.879,-151.485,-16.864,-151.476,-16.761,-151.467,-16.74,-151.411,-16.774,-151.365,-16.864,-151.41,-16.878]],[[-149.322,-17.69,-149.178,-17.737,-149.151,-17.812,-149.182,-17.862,-149.254,-17.85,-149.29,-17.822,-149.341,-17.732,-149.482,-17.753,-149.579,-17.735,-149.633,-17.618,-149.635,-17.564,-149.611,-17.532,-149.508,-17.496,-149.379,-17.522,-149.33,-17.589,-149.322,-17.69]],[[-139.024,-9.695,-138.874,-9.747,-138.827,-9.742,-138.875,-9.793,-139.024,-9.821,-139.074,-9.846,-139.134,-9.829,-139.166,-9.77,-139.024,-9.695]],[[-139.06,-9.931,-139.134,-10.01,-139.134,-9.926,-139.107,-9.915,-139.083,-9.915,-139.06,-9.931]],[[-138.651,-10.515,-138.688,-10.532,-138.69,-10.426,-138.643,-10.446,-138.624,-10.463,-138.632,-10.492,-138.651,-10.515]],[[-140.073,-8.91,-140.171,-8.934,-140.217,-8.93,-140.253,-8.848,-140.24,-8.798,-140.224,-8.782,-140.058,-8.801,-140.044,-8.838,-140.046,-8.874,-140.073,-8.91]],[[-140.076,-9.426,-140.097,-9.444,-140.138,-9.384,-140.144,-9.359,-140.071,-9.328,-140.031,-9.345,-140.076,-9.426]],[[-140.809,-17.857,-140.804,-17.876,-140.841,-17.873,-140.852,-17.867,-140.851,-17.831,-140.824,-17.788,-140.804,-17.752,-140.761,-17.718,-140.686,-17.684,-140.65,-17.67,-140.638,-17.678,-140.652,-17.683,-140.776,-17.754,-140.815,-17.804,-140.833,-17.838,-140.829,-17.849,-140.809,-17.857]],[[-139.556,-8.94,-139.621,-8.948,-139.632,-8.899,-139.612,-8.872,-139.584,-8.86,-139.535,-8.875,-139.508,-8.897,-139.51,-8.916,-139.556,-8.94]],[[-140.685,-18.38,-140.672,-18.416,-140.696,-18.399,-140.773,-18.364,-140.782,-18.334,-140.685,-18.38]],[[-140.83,-18.189,-140.823,-18.217,-140.86,-18.199,-140.895,-18.148,-140.959,-18.085,-140.974,-18.059,-140.925,-18.084,-140.893,-18.121,-140.83,-18.189]],[[-136.294,-18.544,-136.314,-18.566,-136.316,-18.545,-136.344,-18.535,-136.383,-18.514,-136.436,-18.489,-136.464,-18.485,-136.479,-18.471,-136.459,-18.463,-136.426,-18.474,-136.38,-18.497,-136.328,-18.519,-136.294,-18.544]],[[-136.972,-18.342,-136.971,-18.361,-137.068,-18.265,-137.03,-18.273,-136.972,-18.342]],[[-138.506,-20.857,-138.535,-20.876,-138.524,-20.851,-138.546,-20.795,-138.568,-20.787,-138.546,-20.771,-138.515,-20.813,-138.506,-20.857]],[[-142.512,-16.096,-142.53,-16.107,-142.507,-16.028,-142.481,-16.018,-142.512,-16.096]],[[-143.441,-16.62,-143.386,-16.669,-143.459,-16.635,-143.551,-16.621,-143.516,-16.612,-143.465,-16.614,-143.441,-16.62]],[[-143.571,-16.635,-143.611,-16.64,-143.707,-16.581,-143.67,-16.581,-143.615,-16.618,-143.571,-16.635]],[[-145.051,-15.856,-145.058,-15.901,-145.076,-15.858,-145.138,-15.788,-145.161,-15.757,-145.134,-15.762,-145.051,-15.856]],[[-145.487,-16.33,-145.482,-16.347,-145.503,-16.346,-145.54,-16.295,-145.553,-16.251,-145.577,-16.201,-145.609,-16.165,-145.613,-16.132,-145.614,-16.079,-145.577,-16.16,-145.542,-16.225,-145.517,-16.278,-145.487,-16.33]]]},{"n":"New Caledonia","c":"NC","r":3,"ml":4.6,"lx":165.08,"ly":-21.06,"b":[159.928,-22.661,168.139,-19.115],"p":[[[164.202,-20.246,164.315,-20.309,164.436,-20.282,164.588,-20.381,164.976,-20.681,165.112,-20.745,165.192,-20.769,165.252,-20.818,165.307,-20.887,165.381,-20.936,165.413,-20.981,165.421,-21.043,165.447,-21.081,165.582,-21.18,165.663,-21.267,165.775,-21.312,165.823,-21.364,165.885,-21.389,165.95,-21.442,166.058,-21.484,166.303,-21.637,166.493,-21.783,166.588,-21.873,166.69,-21.953,166.82,-22.017,166.942,-22.09,167.004,-22.262,166.97,-22.323,166.9,-22.353,166.835,-22.355,166.774,-22.376,166.571,-22.266,166.522,-22.249,166.468,-22.256,166.438,-22.232,166.416,-22.196,166.292,-22.155,166.177,-22.089,166.143,-22.044,166.124,-21.989,166.096,-21.957,165.933,-21.908,165.823,-21.854,165.744,-21.777,165.62,-21.724,165.428,-21.615,165.329,-21.58,165.242,-21.525,165.01,-21.327,164.927,-21.29,164.855,-21.202,164.656,-20.992,164.559,-20.906,164.455,-20.829,164.375,-20.739,164.313,-20.633,164.17,-20.48,164.152,-20.415,164.158,-20.348,164.124,-20.305,164.065,-20.279,164.037,-20.234,164.041,-20.173,164.06,-20.142,164.202,-20.246]],[[167.544,-22.623,167.513,-22.661,167.473,-22.653,167.444,-22.639,167.422,-22.619,167.443,-22.541,167.529,-22.579,167.544,-22.623]],[[159.952,-19.312,159.936,-19.333,159.928,-19.174,159.96,-19.115,159.975,-19.238,159.952,-19.312]],[[168.011,-21.43,168.058,-21.448,168.139,-21.445,168.121,-21.616,168.006,-21.643,167.967,-21.642,167.941,-21.606,167.876,-21.582,167.879,-21.524,167.815,-21.393,167.926,-21.373,167.988,-21.338,167.985,-21.37,167.995,-21.407,168.011,-21.43]],[[166.547,-20.699,166.494,-20.709,166.558,-20.617,166.56,-20.561,166.585,-20.45,166.583,-20.413,166.625,-20.418,166.671,-20.45,166.618,-20.478,166.6,-20.525,166.602,-20.585,166.623,-20.596,166.589,-20.662,166.547,-20.699]],[[167.401,-21.161,167.346,-21.169,167.273,-21.097,167.134,-21.061,167.073,-20.997,167.033,-20.923,167.112,-20.904,167.189,-20.804,167.136,-20.766,167.045,-20.759,167.056,-20.72,167.204,-20.674,167.269,-20.701,167.298,-20.733,167.293,-20.892,167.361,-20.942,167.431,-21.055,167.43,-21.087,167.401,-21.161]]]},{"n":"French Southern and Antarctic Lands","c":"TF","r":6,"ml":4,"lx":69.12,"ly":-49.3,"b":[51.659,-49.71,70.555,-46.327],"p":[[[69.185,-49.11,69.265,-49.115,69.314,-49.106,69.535,-48.974,69.593,-48.971,69.587,-49.072,69.644,-49.117,69.572,-49.129,69.436,-49.124,69.405,-49.182,69.542,-49.256,69.611,-49.266,69.667,-49.265,69.771,-49.248,69.854,-49.222,69.984,-49.16,70.061,-49.136,70.208,-49.135,70.285,-49.076,70.32,-49.059,70.406,-49.061,70.484,-49.084,70.531,-49.137,70.555,-49.201,70.537,-49.266,70.485,-49.328,70.39,-49.366,70.411,-49.411,70.386,-49.434,70.338,-49.435,70.298,-49.425,70.238,-49.372,70.166,-49.343,69.993,-49.345,69.916,-49.349,69.902,-49.389,69.861,-49.421,69.818,-49.438,69.76,-49.43,69.749,-49.448,69.78,-49.49,69.856,-49.544,69.986,-49.582,70.063,-49.589,70.073,-49.518,70.166,-49.509,70.248,-49.531,70.307,-49.583,70.259,-49.601,70.216,-49.629,70.207,-49.665,70.124,-49.704,70.075,-49.709,69.919,-49.689,69.826,-49.645,69.804,-49.614,69.747,-49.602,69.682,-49.642,69.613,-49.651,69.478,-49.617,69.353,-49.563,69.275,-49.543,69.153,-49.53,69.086,-49.653,68.993,-49.705,68.873,-49.71,68.815,-49.7,68.783,-49.651,68.791,-49.6,68.811,-49.55,68.848,-49.5,68.872,-49.444,68.862,-49.392,68.818,-49.354,68.841,-49.285,68.799,-49.232,68.814,-49.192,68.883,-49.165,68.854,-49.141,68.817,-49.135,68.79,-49.104,68.77,-49.066,68.797,-48.995,68.837,-48.926,68.832,-48.849,68.9,-48.776,68.959,-48.694,69.002,-48.661,69.057,-48.656,69.081,-48.679,69.093,-48.724,69.072,-48.753,69.123,-48.766,69.136,-48.861,69.104,-48.9,69.099,-48.938,69.033,-49.018,69.052,-49.082,69.185,-49.11]],[[69.282,-49.059,69.221,-49.067,69.202,-49.034,69.204,-48.991,69.17,-48.957,69.15,-48.919,69.167,-48.883,69.266,-48.879,69.369,-48.89,69.395,-48.951,69.321,-49.034,69.282,-49.059]],[[51.835,-46.44,51.762,-46.449,51.697,-46.428,51.659,-46.374,51.742,-46.327,51.784,-46.359,51.815,-46.395,51.835,-46.44]]]},{"n":"Åland","c":"AX","r":6,"ml":5,"lx":19.87,"ly":60.16,"b":[19.519,60.012,20.611,60.406],"p":[[[19.99,60.351,20.02,60.351,20.034,60.359,20.087,60.353,20.168,60.315,20.184,60.294,20.24,60.283,20.259,60.261,20.195,60.194,20.155,60.192,20.125,60.201,20.073,60.193,20.043,60.181,20.032,60.152,20.034,60.094,19.8,60.082,19.746,60.099,19.672,60.233,19.687,60.268,19.737,60.282,19.779,60.286,19.785,60.213,19.848,60.221,19.867,60.268,19.872,60.302,19.855,60.319,19.812,60.332,19.788,60.354,19.823,60.39,19.888,60.406,19.945,60.358,19.99,60.351]],[[19.662,60.187,19.667,60.165,19.629,60.17,19.6,60.163,19.58,60.135,19.537,60.145,19.519,60.185,19.551,60.244,19.629,60.246,19.662,60.187]],[[20.611,60.041,20.603,60.017,20.522,60.012,20.488,60.033,20.411,60.03,20.398,60.041,20.43,60.062,20.49,60.075,20.569,60.07,20.611,60.041]]]},{"n":"Finland","c":"FI","r":3,"ml":3,"lx":27.28,"ly":63.25,"b":[20.622,59.816,31.537,70.065],"p":[[[24.155,65.805,24.049,65.99,23.995,66.06,23.907,66.148,23.751,66.191,23.721,66.215,23.7,66.253,23.694,66.304,23.674,66.381,23.682,66.443,23.701,66.481,23.768,66.506,23.866,66.577,23.886,66.628,23.894,66.707,23.939,66.776,23.989,66.811,23.976,66.838,23.942,66.878,23.869,66.934,23.759,67.003,23.677,67.068,23.642,67.129,23.623,67.184,23.626,67.234,23.657,67.268,23.761,67.31,23.775,67.329,23.734,67.423,23.661,67.44,23.537,67.449,23.468,67.45,23.455,67.46,23.451,67.479,23.465,67.518,23.504,67.562,23.537,67.59,23.541,67.614,23.5,67.696,23.488,67.797,23.502,67.875,23.633,67.933,23.639,67.954,23.474,68.017,23.355,68.089,23.319,68.13,23.183,68.137,23.098,68.258,22.975,68.316,22.854,68.367,22.782,68.391,22.362,68.464,22.195,68.478,21.997,68.521,21.85,68.574,21.724,68.609,21.616,68.651,21.465,68.691,21.422,68.725,21.26,68.787,21.183,68.829,20.919,68.907,20.909,68.938,20.907,68.967,20.895,68.98,20.622,69.037,20.676,69.069,20.889,69.071,21.066,69.042,21.104,69.054,21.128,69.081,21.053,69.187,21.066,69.214,21.144,69.247,21.267,69.274,21.461,69.277,21.594,69.274,21.622,69.271,21.82,69.154,21.989,69.041,22.08,68.993,22.3,68.856,22.383,68.777,22.411,68.72,22.501,68.72,22.811,68.695,23.072,68.674,23.144,68.643,23.324,68.649,23.463,68.678,23.707,68.714,23.773,68.758,23.854,68.806,23.997,68.798,24.154,68.761,24.332,68.712,24.491,68.689,24.703,68.653,24.802,68.606,24.941,68.593,25.087,68.64,25.173,68.765,25.249,68.821,25.357,68.862,25.481,68.881,25.575,68.887,25.647,68.919,25.748,68.99,25.768,69.076,25.749,69.231,25.767,69.283,25.85,69.367,25.962,69.589,26.012,69.653,26.072,69.692,26.156,69.715,26.308,69.782,26.525,69.915,26.584,69.926,26.74,69.933,26.934,69.928,27.109,69.905,27.128,69.906,27.206,69.919,27.348,69.96,27.592,70.042,27.748,70.065,27.89,70.062,28.047,69.972,28.269,69.871,28.412,69.823,28.8,69.731,29.142,69.671,29.333,69.473,29.239,69.394,29.192,69.367,29.025,69.288,28.846,69.177,28.833,69.119,28.892,69.061,28.966,69.022,28.899,69.01,28.692,68.961,28.566,68.928,28.414,68.904,28.454,68.872,28.706,68.866,28.745,68.856,28.773,68.84,28.778,68.814,28.752,68.771,28.479,68.538,28.471,68.488,28.56,68.351,28.685,68.19,29.063,68.118,29.344,68.062,29.524,67.929,29.822,67.754,29.979,67.689,29.988,67.668,29.941,67.547,29.751,67.426,29.572,67.324,29.388,67.201,29.243,67.097,29.087,66.971,29.069,66.93,29.066,66.892,29.093,66.849,29.293,66.696,29.371,66.617,29.464,66.532,29.544,66.44,29.591,66.357,29.671,66.276,29.721,66.235,29.804,66.177,29.903,66.091,29.937,66.023,30.088,65.787,30.103,65.726,30.095,65.682,30.029,65.671,29.883,65.664,29.724,65.634,29.716,65.625,29.819,65.569,29.728,65.473,29.715,65.337,29.617,65.265,29.608,65.249,29.612,65.235,29.63,65.224,29.811,65.205,29.826,65.185,29.827,65.145,29.811,65.108,29.72,65.08,29.622,65.04,29.601,65.002,29.604,64.968,29.638,64.912,29.702,64.846,29.783,64.804,30.073,64.765,30.11,64.733,30.126,64.688,30.12,64.645,29.986,64.558,29.987,64.524,30.042,64.443,30.108,64.366,30.391,64.282,30.488,64.237,30.514,64.2,30.528,64.141,30.526,64.077,30.504,64.021,30.415,63.948,30.21,63.803,30.004,63.747,29.992,63.735,30.055,63.689,30.419,63.504,30.655,63.417,30.975,63.301,31.181,63.208,31.247,63.142,31.337,63.068,31.437,63.008,31.509,62.955,31.537,62.922,31.534,62.885,31.437,62.776,31.382,62.692,31.286,62.568,31.187,62.481,30.936,62.324,30.566,62.128,30.48,62.068,30.306,61.965,30.01,61.757,29.933,61.712,29.69,61.546,29.579,61.493,29.492,61.444,29.252,61.288,28.993,61.169,28.739,61.059,28.663,61.003,28.568,60.96,28.455,60.92,28.407,60.897,28.152,60.746,27.798,60.536,27.762,60.533,27.669,60.499,27.525,60.491,27.462,60.465,27.242,60.539,27.205,60.543,27.076,60.525,26.951,60.471,26.721,60.455,26.607,60.438,26.535,60.413,26.52,60.472,26.551,60.546,26.602,60.596,26.606,60.628,26.569,60.625,26.496,60.552,26.456,60.467,26.378,60.424,26.205,60.407,26.036,60.475,25.956,60.474,26.006,60.425,26.04,60.372,26.036,60.342,25.946,60.347,25.846,60.315,25.758,60.268,25.715,60.267,25.656,60.333,25.548,60.302,25.456,60.261,25.268,60.248,25.156,60.194,24.958,60.157,24.849,60.158,24.6,60.114,24.518,60.046,24.446,60.021,24.343,60.042,24.025,60.009,23.722,59.966,23.593,59.968,23.464,59.986,23.327,59.926,23.181,59.845,23.021,59.816,22.964,59.826,23.01,59.869,23.116,59.913,23.188,59.972,23.198,60.022,23.148,60.041,23.08,60.047,22.994,60.099,22.912,60.21,22.867,60.216,22.844,60.187,22.819,60.101,22.793,60.077,22.75,60.057,22.697,60.038,22.646,60.028,22.463,60.029,22.439,60.072,22.439,60.09,22.471,60.147,22.443,60.157,22.47,60.201,22.513,60.199,22.564,60.206,22.59,60.228,22.588,60.256,22.517,60.263,22.512,60.281,22.576,60.359,22.585,60.381,22.56,60.385,22.521,60.377,22.258,60.401,21.934,60.5,21.854,60.505,21.805,60.594,21.727,60.583,21.613,60.531,21.528,60.57,21.436,60.596,21.411,60.637,21.412,60.697,21.404,60.767,21.379,60.85,21.361,60.967,21.378,61.059,21.451,61.127,21.479,61.171,21.513,61.281,21.521,61.411,21.502,61.455,21.507,61.484,21.565,61.484,21.552,61.51,21.527,61.523,21.498,61.552,21.522,61.567,21.592,61.568,21.598,61.578,21.606,61.592,21.552,61.667,21.546,61.703,21.471,61.812,21.385,61.915,21.256,61.99,21.302,62.113,21.354,62.224,21.343,62.277,21.323,62.343,21.166,62.414,21.142,62.515,21.104,62.623,21.118,62.689,21.144,62.74,21.196,62.791,21.458,62.95,21.474,63.033,21.651,63.039,21.569,63.114,21.549,63.156,21.545,63.204,21.8,63.238,21.896,63.21,22.12,63.244,22.32,63.31,22.316,63.346,22.286,63.377,22.243,63.438,22.273,63.455,22.346,63.442,22.313,63.473,22.319,63.504,22.398,63.491,22.528,63.58,22.532,63.648,22.756,63.683,23.014,63.822,23.134,63.865,23.249,63.896,23.494,64.034,23.599,64.041,23.653,64.134,23.861,64.258,23.925,64.274,24.022,64.386,24.278,64.515,24.441,64.68,24.53,64.739,24.558,64.801,24.658,64.806,24.748,64.852,24.942,64.884,25.134,64.875,25.214,64.853,25.288,64.86,25.281,64.916,25.228,64.951,25.271,64.984,25.373,65.009,25.362,65.065,25.34,65.099,25.256,65.143,25.298,65.243,25.308,65.353,25.348,65.479,25.242,65.546,24.839,65.66,24.764,65.656,24.675,65.671,24.582,65.757,24.623,65.832,24.628,65.859,24.592,65.858,24.533,65.822,24.404,65.78,24.238,65.812,24.155,65.805]],[[21.994,60.337,21.921,60.332,21.819,60.382,21.806,60.401,21.846,60.412,21.819,60.452,21.827,60.47,21.907,60.438,21.95,60.402,21.908,60.393,21.98,60.355,21.994,60.337]],[[21.218,63.241,21.229,63.223,21.287,63.228,21.366,63.262,21.422,63.246,21.416,63.197,21.378,63.199,21.367,63.207,21.318,63.179,21.31,63.163,21.253,63.152,21.149,63.199,21.084,63.278,21.236,63.278,21.222,63.259,21.218,63.241]],[[22.175,60.371,22.302,60.348,22.355,60.356,22.416,60.303,22.313,60.27,22.306,60.229,22.346,60.203,22.361,60.166,22.258,60.166,22.209,60.197,22.188,60.237,22.141,60.265,22.077,60.286,22.108,60.315,22.126,60.356,22.175,60.371]],[[21.451,60.53,21.437,60.483,21.369,60.488,21.3,60.48,21.244,60.526,21.215,60.604,21.225,60.621,21.268,60.638,21.301,60.596,21.451,60.53]],[[21.833,60.141,21.733,60.106,21.695,60.114,21.705,60.172,21.764,60.199,21.864,60.202,21.833,60.141]],[[21.628,60.108,21.541,60.098,21.486,60.127,21.507,60.148,21.568,60.172,21.634,60.169,21.648,60.141,21.628,60.108]],[[24.848,64.991,24.699,64.958,24.579,64.979,24.577,65.043,24.651,65.074,24.786,65.086,24.971,65.055,24.998,65.039,24.892,65.026,24.848,64.991]]]},{"n":"Fiji","c":"FJ","r":6,"ml":3,"lx":177.98,"ly":-17.83,"b":[-180,-21.706,180,-12.477],"p":[[[179.999,-16.169,179.848,-16.302,179.794,-16.37,179.748,-16.446,179.619,-16.528,179.564,-16.637,179.568,-16.747,179.697,-16.632,179.841,-16.537,179.885,-16.518,179.93,-16.519,179.927,-16.552,179.906,-16.584,179.89,-16.667,179.928,-16.744,179.821,-16.737,179.715,-16.744,179.589,-16.787,179.465,-16.806,179.419,-16.807,179.375,-16.792,179.346,-16.77,179.323,-16.718,179.3,-16.71,179.202,-16.713,179.055,-16.814,179.007,-16.9,178.95,-16.904,178.884,-16.886,178.803,-16.952,178.707,-16.976,178.665,-16.92,178.638,-16.851,178.604,-16.801,178.497,-16.788,178.514,-16.726,178.542,-16.7,178.568,-16.664,178.584,-16.622,178.634,-16.649,178.686,-16.666,178.745,-16.634,178.805,-16.631,178.866,-16.54,178.961,-16.483,179.091,-16.437,179.225,-16.405,179.294,-16.399,179.359,-16.38,179.475,-16.294,179.552,-16.25,179.635,-16.223,179.715,-16.208,179.789,-16.221,179.848,-16.214,180,-16.153,179.999,-16.169]],[[178.28,-17.372,178.28,-17.416,178.309,-17.435,178.339,-17.438,178.411,-17.523,178.523,-17.596,178.592,-17.651,178.596,-17.699,178.575,-17.749,178.604,-17.839,178.618,-17.933,178.668,-18.081,178.597,-18.109,178.487,-18.112,178.461,-18.139,178.423,-18.124,178.332,-18.135,178.244,-18.184,178.16,-18.25,178.064,-18.25,177.955,-18.264,177.847,-18.255,177.771,-18.22,177.636,-18.181,177.457,-18.148,177.383,-18.121,177.321,-18.078,177.263,-17.969,177.255,-17.915,177.264,-17.863,177.316,-17.846,177.36,-17.82,177.366,-17.786,177.386,-17.762,177.411,-17.754,177.423,-17.737,177.406,-17.682,177.401,-17.632,177.504,-17.54,177.618,-17.461,177.818,-17.388,177.94,-17.395,178.128,-17.339,178.188,-17.313,178.247,-17.329,178.28,-17.372]],[[-179.975,-16.925,-180,-16.963,-180,-16.908,-180,-16.859,-180,-16.824,-180,-16.786,-179.894,-16.7,-179.861,-16.688,-179.822,-16.765,-179.868,-16.85,-179.975,-16.925]],[[180,-16.963,179.926,-17,179.897,-16.964,179.931,-16.876,180,-16.786,179.999,-16.859,180,-16.963]],[[178.488,-18.974,178.488,-19.017,178.359,-19.046,178.316,-19.01,178.288,-19.004,178.211,-19.067,178.189,-19.092,178.182,-19.112,178.162,-19.121,178.021,-19.152,177.959,-19.122,178.001,-19.101,178.052,-19.06,178.104,-19.066,178.157,-19.028,178.208,-18.97,178.282,-18.957,178.334,-18.934,178.42,-18.951,178.488,-18.974]],[[-178.988,-17.977,-179.018,-17.992,-179.039,-17.988,-179.064,-17.972,-179.079,-17.944,-179.048,-17.92,-178.999,-17.947,-178.988,-17.977]],[[-178.762,-18.234,-178.774,-18.252,-178.827,-18.222,-178.848,-18.202,-178.791,-18.186,-178.763,-18.191,-178.762,-18.234]],[[-178.251,-17.953,-178.307,-17.963,-178.357,-17.921,-178.325,-17.876,-178.28,-17.886,-178.255,-17.93,-178.251,-17.953]],[[-178.956,-17.273,-178.982,-17.307,-179.004,-17.295,-178.976,-17.237,-178.971,-17.213,-179.015,-17.182,-179.018,-17.161,-179.005,-17.148,-178.953,-17.182,-178.921,-17.208,-178.915,-17.223,-178.925,-17.249,-178.956,-17.273]],[[-178.535,-19.166,-178.546,-19.175,-178.574,-19.165,-178.596,-19.151,-178.599,-19.137,-178.589,-19.119,-178.568,-19.109,-178.557,-19.113,-178.563,-19.119,-178.576,-19.125,-178.574,-19.143,-178.557,-19.154,-178.541,-19.157,-178.535,-19.166]],[[-179.8,-18.94,-179.798,-18.97,-179.812,-18.968,-179.83,-18.956,-179.839,-18.962,-179.846,-18.971,-179.849,-18.991,-179.851,-19.003,-179.865,-18.999,-179.867,-18.978,-179.863,-18.964,-179.856,-18.943,-179.831,-18.924,-179.8,-18.94]],[[177.234,-17.147,177.183,-17.164,177.21,-17.084,177.239,-17.059,177.258,-17.054,177.287,-17.049,177.276,-17.105,177.234,-17.147]],[[179.422,-17.367,179.389,-17.394,179.373,-17.256,179.408,-17.257,179.433,-17.272,179.447,-17.306,179.422,-17.367]],[[179.349,-18.102,179.34,-18.11,179.254,-18.031,179.256,-17.999,179.272,-17.971,179.306,-17.944,179.338,-17.99,179.362,-18.065,179.349,-18.102]],[[178.828,-17.729,178.776,-17.747,178.748,-17.686,178.787,-17.624,178.831,-17.619,178.853,-17.681,178.828,-17.729]],[[-179.929,-16.503,-180,-16.54,-180,-16.489,-179.944,-16.441,-179.901,-16.432,-179.927,-16.479,-179.929,-16.503]],[[-179.956,-16.149,-180,-16.168,-180,-16.156,-180,-16.153,-179.969,-16.126,-179.945,-16.126,-179.956,-16.149]],[[180,-16.54,179.987,-16.541,179.985,-16.522,180,-16.489,180,-16.54]],[[177.121,-12.505,177.082,-12.516,177.019,-12.507,177.006,-12.491,177.026,-12.487,177.068,-12.477,177.118,-12.482,177.127,-12.493,177.121,-12.505]],[[174.63,-21.695,174.622,-21.706,174.593,-21.702,174.587,-21.68,174.604,-21.667,174.628,-21.676,174.63,-21.695]],[[-178.712,-20.668,-178.71,-20.671,-178.715,-20.67,-178.723,-20.667,-178.729,-20.66,-178.731,-20.653,-178.728,-20.645,-178.725,-20.646,-178.719,-20.652,-178.714,-20.66,-178.712,-20.668]]]},{"n":"Ethiopia","c":"ET","r":2,"ml":2,"lx":39.09,"ly":8.03,"b":[32.999,3.456,47.978,14.852],"p":[[[35.268,5.492,35.252,5.511,35.164,5.581,35.082,5.673,35.032,5.775,34.984,5.858,34.959,6.045,34.898,6.16,34.838,6.3,34.749,6.568,34.711,6.66,34.639,6.722,34.563,6.78,34.484,6.898,34.279,7.003,34.2,7.085,34.064,7.226,34.03,7.297,34.02,7.368,33.978,7.435,33.902,7.51,33.666,7.671,33.601,7.69,33.516,7.708,33.392,7.724,33.226,7.761,33.081,7.824,33.015,7.869,32.999,7.9,33.013,7.952,33.065,8.04,33.165,8.251,33.234,8.396,33.281,8.437,33.409,8.448,33.545,8.443,33.645,8.433,33.785,8.431,33.953,8.444,34.02,8.492,34.073,8.545,34.095,8.582,34.102,8.676,34.102,8.752,34.091,9.041,34.085,9.219,34.077,9.421,34.078,9.462,34.079,9.513,34.12,9.73,34.159,9.853,34.185,9.919,34.292,10.125,34.311,10.191,34.315,10.252,34.276,10.528,34.344,10.659,34.431,10.788,34.508,10.843,34.572,10.88,34.602,10.865,34.675,10.805,34.771,10.746,34.816,10.759,34.882,10.811,34.931,10.865,34.925,10.962,34.969,11.162,34.961,11.277,35.008,11.42,35.06,11.621,35.083,11.748,35.112,11.817,35.252,11.957,35.373,12.156,35.45,12.301,35.596,12.537,35.67,12.624,35.731,12.661,35.821,12.685,35.988,12.706,36.108,12.726,36.125,12.757,36.135,12.805,36.137,12.911,36.16,13.093,36.212,13.271,36.274,13.406,36.307,13.467,36.346,13.526,36.391,13.626,36.447,13.842,36.444,13.988,36.524,14.257,36.542,14.258,36.679,14.308,36.812,14.315,36.941,14.281,37.025,14.272,37.063,14.289,37.099,14.334,37.133,14.406,37.185,14.446,37.257,14.454,37.354,14.372,37.507,14.156,37.547,14.144,37.571,14.149,37.648,14.323,37.708,14.457,37.82,14.708,37.884,14.852,37.943,14.811,38.003,14.737,38.07,14.703,38.142,14.681,38.177,14.679,38.221,14.65,38.377,14.47,38.431,14.429,38.504,14.424,38.812,14.482,38.996,14.587,39.024,14.628,39.074,14.628,39.135,14.582,39.159,14.538,39.198,14.479,39.27,14.47,39.446,14.512,39.532,14.537,39.605,14.516,39.698,14.499,39.756,14.499,39.895,14.441,40.062,14.459,40.141,14.456,40.221,14.431,40.353,14.338,40.524,14.225,40.77,14.144,40.82,14.112,40.939,13.983,41.122,13.736,41.363,13.5,41.625,13.313,41.765,13.184,41.86,13.026,41.952,12.882,42.047,12.821,42.134,12.771,42.225,12.662,42.29,12.57,42.379,12.466,42.28,12.324,42.149,12.134,41.996,11.912,41.95,11.858,41.816,11.724,41.793,11.686,41.767,11.589,41.765,11.413,41.782,11.188,41.798,10.98,41.872,10.956,41.957,10.941,42.052,10.968,42.166,10.992,42.308,11.005,42.465,11.047,42.558,11.081,42.655,11.078,42.741,11.042,42.783,11.009,42.844,10.998,42.923,10.999,42.906,10.96,42.863,10.903,42.81,10.846,42.763,10.787,42.66,10.621,42.656,10.6,42.669,10.568,42.725,10.492,42.784,10.37,42.816,10.257,42.842,10.203,42.913,10.141,43.015,10.013,43.069,9.926,43.182,9.88,43.218,9.77,43.303,9.609,43.394,9.48,43.483,9.379,43.581,9.341,43.621,9.337,43.827,9.151,43.984,9.009,44.023,8.986,44.306,8.893,44.632,8.786,44.894,8.7,45.227,8.591,45.555,8.483,45.863,8.38,46.296,8.235,46.645,8.118,46.92,8.026,46.978,7.997,47.306,7.997,47.638,7.997,47.978,7.997,47.732,7.759,47.453,7.49,47.16,7.208,46.971,7.026,46.672,6.737,46.423,6.497,46.167,6.235,45.935,5.997,45.634,5.668,45.438,5.455,45.133,5.122,44.941,4.912,44.912,4.9,44.637,4.916,44.37,4.931,44.028,4.951,43.989,4.951,43.889,4.931,43.829,4.911,43.583,4.855,43.538,4.84,43.334,4.75,43.126,4.644,43.016,4.563,42.931,4.445,42.895,4.361,42.857,4.324,42.792,4.292,42.355,4.212,42.228,4.202,42.024,4.138,41.915,4.031,41.884,3.978,41.738,3.979,41.482,3.963,41.372,3.946,41.319,3.943,41.221,3.944,41.14,3.963,41.087,3.992,41.021,4.057,40.873,4.19,40.765,4.273,40.529,4.178,40.316,4.083,40.014,3.948,39.842,3.851,39.79,3.754,39.658,3.578,39.539,3.469,39.494,3.456,39.225,3.479,39.128,3.501,38.968,3.521,38.753,3.559,38.608,3.6,38.452,3.605,38.225,3.619,38.086,3.649,37.945,3.747,37.763,3.865,37.575,3.986,37.383,4.111,37.155,4.255,36.906,4.411,36.848,4.427,36.824,4.43,36.553,4.437,36.272,4.445,36.082,4.45,36.022,4.468,35.979,4.504,35.92,4.62,35.846,4.703,35.763,4.808,35.756,4.95,35.779,5.106,35.8,5.157,35.788,5.208,35.791,5.279,35.745,5.344,35.469,5.419,35.424,5.413,35.378,5.385,35.325,5.365,35.288,5.384,35.265,5.412,35.264,5.458,35.268,5.492]]]},{"n":"Estonia","c":"EE","r":6,"ml":3,"lx":25.87,"ly":58.72,"b":[21.854,57.525,28.151,59.639],"p":[[[27.352,57.528,27.327,57.525,27.187,57.538,27.033,57.579,26.966,57.609,26.9,57.609,26.82,57.589,26.533,57.531,26.462,57.544,26.298,57.601,26.215,57.663,26.03,57.786,26.015,57.815,25.991,57.838,25.794,57.869,25.721,57.914,25.66,57.92,25.571,57.943,25.34,58.039,25.283,58.048,25.269,58.032,25.273,58.009,25.258,57.996,25.229,57.997,25.175,58.032,25.111,58.063,24.911,58.005,24.839,57.989,24.776,57.985,24.459,57.908,24.363,57.866,24.323,57.871,24.332,57.91,24.464,58.106,24.488,58.262,24.536,58.283,24.55,58.305,24.529,58.354,24.392,58.386,24.337,58.381,24.287,58.328,24.236,58.29,24.115,58.266,24.011,58.307,23.768,58.361,23.706,58.433,23.692,58.506,23.563,58.576,23.509,58.659,23.531,58.716,23.647,58.754,23.681,58.787,23.534,58.782,23.504,58.79,23.497,58.82,23.432,58.921,23.49,58.96,23.515,58.999,23.468,59.032,23.48,59.07,23.517,59.108,23.494,59.196,23.641,59.242,23.783,59.275,24.083,59.292,24.054,59.372,24.175,59.376,24.38,59.473,24.584,59.456,24.878,59.522,25.444,59.521,25.521,59.559,25.507,59.598,25.509,59.639,25.616,59.628,25.794,59.635,26.461,59.554,26.625,59.554,26.852,59.472,26.975,59.451,27.336,59.45,27.893,59.414,28.002,59.47,28.013,59.484,28.066,59.453,28.133,59.403,28.151,59.374,28.128,59.358,28.061,59.343,28.046,59.328,28.016,59.302,27.938,59.297,27.898,59.278,27.85,59.193,27.758,59.052,27.622,58.945,27.513,58.886,27.464,58.841,27.434,58.787,27.427,58.733,27.531,58.435,27.53,58.381,27.506,58.326,27.488,58.27,27.502,58.221,27.571,58.138,27.644,58.014,27.673,57.935,27.722,57.905,27.769,57.884,27.779,57.871,27.777,57.857,27.753,57.841,27.542,57.799,27.515,57.764,27.492,57.725,27.4,57.667,27.372,57.613,27.354,57.55,27.352,57.528]],[[22.617,58.621,22.688,58.597,22.754,58.605,22.82,58.622,22.964,58.606,23.293,58.483,23.323,58.451,23.127,58.436,23.083,58.398,23.035,58.372,22.98,58.364,22.885,58.311,22.757,58.261,22.73,58.231,22.498,58.236,22.372,58.217,22.269,58.161,22.227,58.052,22.152,57.967,22.076,57.936,21.997,57.931,21.978,57.963,21.986,57.995,22.153,58.115,22.188,58.154,22.104,58.172,22.035,58.213,21.882,58.262,21.854,58.302,21.891,58.305,21.924,58.316,21.965,58.349,21.984,58.387,21.862,58.497,21.924,58.514,22.002,58.51,22.081,58.478,22.169,58.516,22.206,58.521,22.267,58.508,22.328,58.581,22.474,58.605,22.547,58.627,22.617,58.621]],[[22.924,58.827,22.842,58.777,22.793,58.797,22.767,58.821,22.661,58.709,22.542,58.69,22.473,58.712,22.479,58.754,22.411,58.863,22.307,58.895,22.162,58.898,22.056,58.944,22.463,58.974,22.505,59.026,22.587,59.081,22.649,59.087,22.702,59.074,22.712,59.032,22.725,59.015,22.91,58.991,22.982,58.92,23.009,58.834,22.924,58.827]],[[23.344,58.55,23.26,58.54,23.063,58.611,23.109,58.659,23.165,58.678,23.333,58.649,23.356,58.576,23.344,58.55]]]},{"n":"Eritrea","c":"ER","r":4,"ml":4,"lx":38.29,"ly":15.79,"b":[36.427,12.377,43.117,18.005],"p":[[[36.524,14.257,36.492,14.544,36.471,14.736,36.448,14.94,36.427,15.132,36.522,15.25,36.566,15.362,36.679,15.726,36.725,15.799,36.813,15.994,36.826,16.05,36.914,16.296,36.905,16.46,36.888,16.625,36.936,16.722,36.979,16.801,36.976,16.867,36.995,17.021,37.009,17.059,37.062,17.061,37.17,17.041,37.249,17.057,37.34,17.057,37.411,17.062,37.453,17.109,37.51,17.288,37.547,17.324,37.576,17.335,37.657,17.368,37.726,17.421,37.782,17.458,37.803,17.466,37.863,17.47,37.923,17.492,37.95,17.518,38.025,17.538,38.099,17.526,38.149,17.549,38.182,17.563,38.219,17.564,38.254,17.585,38.267,17.617,38.29,17.637,38.347,17.684,38.374,17.717,38.386,17.751,38.397,17.778,38.422,17.824,38.523,17.939,38.609,18.005,38.912,17.427,39.034,17.086,39.143,16.729,39.223,16.194,39.299,15.921,39.422,15.787,39.507,15.532,39.579,15.523,39.631,15.453,39.721,15.214,39.786,15.125,39.819,15.201,39.816,15.245,39.79,15.319,39.813,15.414,39.864,15.47,39.978,15.393,40.041,15.335,40.058,15.217,40.084,15.152,40.204,15.014,40.305,14.974,40.437,14.964,40.546,14.934,40.634,14.883,40.799,14.743,41.176,14.62,41.48,14.244,41.658,13.983,42.245,13.588,42.346,13.398,42.399,13.213,42.523,13.221,42.734,13.019,42.796,12.864,42.97,12.808,42.999,12.9,43.083,12.825,43.117,12.709,43.006,12.662,42.883,12.621,42.866,12.623,42.825,12.569,42.767,12.423,42.704,12.38,42.67,12.377,42.479,12.514,42.45,12.521,42.409,12.494,42.379,12.466,42.29,12.57,42.225,12.662,42.134,12.771,42.047,12.821,41.952,12.882,41.86,13.026,41.765,13.184,41.625,13.313,41.363,13.5,41.122,13.736,40.939,13.983,40.82,14.112,40.77,14.144,40.524,14.225,40.353,14.338,40.221,14.431,40.141,14.456,40.062,14.459,39.895,14.441,39.756,14.499,39.698,14.499,39.605,14.516,39.532,14.537,39.446,14.512,39.27,14.47,39.198,14.479,39.159,14.538,39.135,14.582,39.074,14.628,39.024,14.628,38.996,14.587,38.812,14.482,38.504,14.424,38.431,14.429,38.377,14.47,38.221,14.65,38.177,14.679,38.142,14.681,38.07,14.703,38.003,14.737,37.943,14.811,37.884,14.852,37.82,14.708,37.708,14.457,37.648,14.323,37.571,14.149,37.547,14.144,37.507,14.156,37.354,14.372,37.257,14.454,37.185,14.446,37.133,14.406,37.099,14.334,37.063,14.289,37.025,14.272,36.941,14.281,36.812,14.315,36.679,14.308,36.542,14.258,36.524,14.257]],[[40.076,16.082,40.11,15.986,40.012,16.023,39.996,16.043,40.039,16.081,40.048,16.104,40.076,16.082]],[[40.141,15.696,40.183,15.643,40.211,15.648,40.234,15.666,40.25,15.703,40.408,15.629,40.399,15.58,40.305,15.577,40.196,15.598,40.095,15.591,39.975,15.612,39.947,15.696,40.024,15.656,40.063,15.666,40.071,15.677,40.016,15.733,39.94,15.745,39.945,15.789,39.979,15.807,40,15.828,39.957,15.889,40.043,15.875,40.097,15.838,40.132,15.795,40.141,15.696]]]},{"n":"Equatorial Guinea","c":"GQ","r":4,"ml":4,"lx":8.99,"ly":2.33,"b":[8.434,0.96,11.335,3.758],"p":[[[8.736,3.758,8.76,3.754,8.91,3.758,8.951,3.705,8.946,3.628,8.792,3.4,8.763,3.305,8.704,3.224,8.652,3.217,8.475,3.265,8.445,3.294,8.434,3.332,8.452,3.423,8.465,3.451,8.55,3.468,8.577,3.482,8.623,3.58,8.638,3.669,8.676,3.736,8.736,3.758]],[[11.329,2.167,11.33,1.936,11.331,1.74,11.332,1.528,11.334,1.308,11.335,1.121,11.335,1,11.131,1,10.859,1.001,10.587,1.002,10.315,1.003,10.179,1.004,10.029,1.004,9.98,0.998,9.947,0.967,9.907,0.96,9.86,0.986,9.804,0.999,9.789,1.026,9.761,1.075,9.705,1.08,9.676,1.075,9.636,1.047,9.591,1.032,9.599,1.054,9.51,1.115,9.445,1.121,9.386,1.139,9.434,1.296,9.494,1.435,9.584,1.54,9.632,1.566,9.648,1.618,9.719,1.789,9.807,1.927,9.78,2.068,9.801,2.304,9.826,2.298,9.83,2.275,9.837,2.242,9.87,2.213,9.98,2.168,10.307,2.168,10.502,2.168,10.791,2.168,11.097,2.167,11.329,2.167]]]},{"n":"El Salvador","c":"SV","r":6,"ml":5,"lx":-88.89,"ly":13.69,"b":[-90.106,13.164,-87.715,14.431],"p":[[[-89.363,14.416,-89.337,14.411,-89.17,14.36,-89.121,14.37,-89.057,14.329,-89.027,14.297,-89,14.253,-88.868,14.164,-88.846,14.125,-88.747,14.072,-88.708,14.032,-88.666,14.016,-88.583,14,-88.513,13.979,-88.504,13.964,-88.498,13.905,-88.483,13.854,-88.449,13.851,-88.408,13.875,-88.276,13.943,-88.151,13.987,-88.08,13.961,-88.039,13.905,-87.991,13.88,-87.892,13.895,-87.802,13.89,-87.731,13.841,-87.715,13.813,-87.759,13.65,-87.774,13.58,-87.782,13.521,-87.756,13.506,-87.732,13.483,-87.737,13.451,-87.814,13.399,-87.838,13.386,-87.821,13.285,-87.878,13.224,-87.931,13.181,-88.023,13.169,-88.181,13.164,-88.417,13.214,-88.592,13.281,-88.686,13.281,-88.656,13.259,-88.582,13.245,-88.484,13.197,-88.512,13.184,-88.867,13.283,-89.278,13.478,-89.523,13.509,-89.804,13.56,-89.97,13.683,-90.095,13.737,-90.106,13.783,-90.105,13.835,-90.048,13.904,-89.943,13.997,-89.873,14.046,-89.84,14.055,-89.794,14.05,-89.749,14.077,-89.711,14.141,-89.671,14.183,-89.57,14.225,-89.547,14.241,-89.555,14.277,-89.577,14.347,-89.574,14.39,-89.541,14.41,-89.501,14.414,-89.419,14.431,-89.383,14.428,-89.363,14.416]]]},{"n":"Egypt","c":"EG","r":2,"ml":1.7,"lx":29.45,"ly":26.19,"b":[24.703,21.995,36.871,31.655],"p":[[[36.871,21.997,36.543,21.997,36.215,21.997,35.887,21.997,35.559,21.996,35.231,21.996,34.903,21.996,34.575,21.996,34.246,21.996,33.918,21.996,33.59,21.996,33.262,21.996,32.934,21.996,32.606,21.996,32.278,21.996,31.95,21.996,31.622,21.996,31.434,21.996,31.466,22.085,31.486,22.148,31.464,22.192,31.4,22.202,31.358,22.189,31.261,22.002,31.209,21.995,31.093,21.995,30.711,21.995,30.329,21.995,29.947,21.995,29.565,21.995,29.183,21.995,28.801,21.995,28.419,21.995,28.036,21.995,27.654,21.995,27.272,21.996,26.89,21.996,26.508,21.996,26.126,21.996,25.744,21.996,25.362,21.996,24.98,21.996,24.98,22.22,24.98,22.445,24.98,22.67,24.98,22.894,24.98,23.119,24.98,23.343,24.98,23.568,24.98,23.792,24.98,24.017,24.98,24.242,24.98,24.466,24.98,24.691,24.98,24.915,24.98,25.14,24.98,25.364,24.98,25.589,24.98,25.813,24.98,26.038,24.98,26.263,24.98,26.487,24.98,26.712,24.98,26.936,24.98,27.161,24.98,27.385,24.98,27.61,24.98,27.835,24.98,28.059,24.98,28.284,24.98,28.508,24.98,28.733,24.98,28.957,24.98,29.182,24.972,29.224,24.916,29.376,24.866,29.57,24.811,29.809,24.804,29.886,24.712,30.132,24.703,30.201,24.726,30.251,24.878,30.458,24.923,30.558,24.961,30.679,24.974,30.777,24.929,30.926,24.878,31.061,24.86,31.199,24.853,31.335,24.93,31.427,25.023,31.514,25.057,31.567,25.112,31.627,25.15,31.655,25.225,31.534,25.382,31.513,25.893,31.621,26.457,31.512,26.769,31.47,27.248,31.378,27.54,31.213,27.62,31.192,27.83,31.195,27.968,31.097,28.515,31.05,28.807,30.943,28.973,30.857,29.072,30.83,29.16,30.835,29.279,30.867,29.429,30.927,29.592,31.012,29.93,31.227,30.049,31.265,30.128,31.256,30.223,31.258,30.262,31.317,30.312,31.357,30.344,31.403,30.395,31.458,30.571,31.473,30.924,31.567,30.884,31.522,30.563,31.417,30.7,31.404,30.841,31.44,31.002,31.463,31.031,31.508,31.052,31.592,31.083,31.603,31.194,31.588,31.524,31.458,31.607,31.456,31.839,31.526,31.889,31.541,31.964,31.502,32.136,31.341,32.076,31.344,31.892,31.482,31.876,31.414,31.771,31.293,31.902,31.24,32.008,31.221,32.066,31.153,32.102,31.093,32.207,31.119,32.282,31.201,32.243,31.247,32.216,31.294,32.251,31.295,32.324,31.256,32.533,31.101,32.603,31.069,32.685,31.074,32.854,31.118,32.902,31.111,33.13,31.168,33.157,31.126,33.194,31.085,33.378,31.131,33.667,31.13,33.903,31.181,34.176,31.304,34.198,31.323,34.213,31.292,34.245,31.208,34.329,30.995,34.401,30.828,34.49,30.596,34.518,30.507,34.53,30.446,34.659,30.191,34.735,29.982,34.791,29.812,34.87,29.564,34.904,29.477,34.849,29.432,34.736,29.271,34.617,28.758,34.446,28.357,34.427,28.106,34.4,28.016,34.319,27.889,34.22,27.764,34.045,27.829,33.76,28.048,33.594,28.256,33.416,28.39,33.248,28.568,33.202,28.696,33.204,28.778,33.13,28.978,33.076,29.073,32.871,29.286,32.812,29.4,32.767,29.45,32.721,29.522,32.647,29.798,32.566,29.974,32.473,29.925,32.489,29.852,32.409,29.749,32.36,29.631,32.397,29.534,32.565,29.386,32.599,29.322,32.638,29.182,32.632,28.992,32.659,28.928,32.784,28.787,32.829,28.703,32.857,28.631,32.898,28.565,33.023,28.442,33.202,28.208,33.372,28.051,33.495,27.974,33.547,27.898,33.559,27.701,33.55,27.607,33.657,27.431,33.697,27.341,33.802,27.268,33.849,27.185,33.893,27.049,33.959,26.649,34.05,26.551,34.329,26.024,34.565,25.691,34.679,25.443,34.853,25.14,35.194,24.475,35.397,24.27,35.478,24.155,35.625,24.066,35.784,23.938,35.632,23.95,35.594,23.943,35.541,23.921,35.515,23.843,35.504,23.779,35.523,23.443,35.564,23.271,35.698,22.946,35.797,22.849,35.846,22.786,35.913,22.74,36.23,22.629,36.415,22.394,36.83,22.098,36.87,22.016,36.871,21.997]]]},{"n":"Ecuador","c":"EC","r":3,"ml":3,"lx":-78.19,"ly":-1.26,"b":[-91.654,-4.991,-75.25,1.455],"p":[[[-75.284,-0.107,-75.34,-0.142,-75.398,-0.146,-75.476,-0.157,-75.584,-0.123,-75.626,-0.123,-75.632,-0.158,-75.561,-0.2,-75.491,-0.248,-75.466,-0.322,-75.425,-0.409,-75.325,-0.507,-75.263,-0.555,-75.259,-0.59,-75.279,-0.654,-75.284,-0.707,-75.25,-0.952,-75.272,-0.967,-75.309,-0.968,-75.348,-0.967,-75.38,-0.94,-75.408,-0.924,-75.42,-0.962,-75.449,-1.071,-75.514,-1.316,-75.571,-1.531,-75.642,-1.607,-75.745,-1.728,-75.885,-1.893,-76.09,-2.133,-76.241,-2.244,-76.36,-2.331,-76.499,-2.432,-76.679,-2.563,-76.881,-2.636,-77.161,-2.738,-77.36,-2.81,-77.506,-2.86,-77.659,-2.912,-77.861,-2.982,-77.938,-3.047,-78.068,-3.207,-78.128,-3.284,-78.183,-3.35,-78.195,-3.38,-78.187,-3.4,-78.161,-3.432,-78.158,-3.465,-78.195,-3.486,-78.226,-3.489,-78.24,-3.473,-78.251,-3.436,-78.284,-3.399,-78.323,-3.388,-78.345,-3.397,-78.347,-3.431,-78.398,-3.595,-78.4,-3.674,-78.421,-3.706,-78.42,-3.777,-78.471,-3.843,-78.493,-3.902,-78.509,-3.952,-78.55,-3.987,-78.565,-4.042,-78.603,-4.157,-78.648,-4.248,-78.679,-4.326,-78.685,-4.384,-78.661,-4.425,-78.653,-4.458,-78.674,-4.518,-78.686,-4.562,-78.743,-4.593,-78.862,-4.665,-78.908,-4.714,-78.926,-4.771,-78.914,-4.819,-78.919,-4.858,-78.975,-4.873,-78.995,-4.908,-79.033,-4.969,-79.076,-4.991,-79.187,-4.958,-79.268,-4.958,-79.331,-4.928,-79.399,-4.84,-79.456,-4.766,-79.502,-4.671,-79.516,-4.539,-79.578,-4.501,-79.639,-4.455,-79.711,-4.468,-79.797,-4.476,-79.845,-4.446,-79.963,-4.39,-80.064,-4.328,-80.14,-4.296,-80.197,-4.311,-80.232,-4.349,-80.293,-4.417,-80.383,-4.464,-80.424,-4.461,-80.479,-4.43,-80.488,-4.394,-80.444,-4.336,-80.353,-4.208,-80.454,-4.205,-80.488,-4.166,-80.493,-4.119,-80.51,-4.07,-80.49,-4.01,-80.437,-3.979,-80.358,-4.003,-80.303,-4.005,-80.267,-3.949,-80.231,-3.924,-80.194,-3.906,-80.179,-3.878,-80.218,-3.788,-80.229,-3.739,-80.217,-3.711,-80.219,-3.654,-80.221,-3.613,-80.244,-3.577,-80.245,-3.522,-80.265,-3.492,-80.272,-3.461,-80.274,-3.425,-80.298,-3.406,-80.325,-3.388,-80.303,-3.375,-80.16,-3.324,-80.1,-3.274,-80.027,-3.228,-79.963,-3.158,-79.922,-3.09,-79.823,-2.777,-79.73,-2.579,-79.746,-2.485,-79.822,-2.357,-79.84,-2.168,-79.833,-2.111,-79.842,-2.067,-79.893,-2.146,-79.88,-2.424,-79.926,-2.549,-79.989,-2.579,-80.03,-2.557,-80.007,-2.354,-80.053,-2.391,-80.127,-2.528,-80.249,-2.631,-80.255,-2.665,-80.285,-2.707,-80.379,-2.668,-80.45,-2.626,-80.685,-2.397,-80.839,-2.349,-80.932,-2.269,-80.952,-2.235,-80.963,-2.189,-80.868,-2.141,-80.77,-2.077,-80.761,-1.935,-80.763,-1.823,-80.835,-1.632,-80.801,-1.383,-80.82,-1.286,-80.902,-1.079,-80.841,-0.975,-80.624,-0.899,-80.554,-0.848,-80.505,-0.684,-80.455,-0.585,-80.358,-0.625,-80.282,-0.621,-80.385,-0.584,-80.468,-0.436,-80.482,-0.368,-80.321,-0.166,-80.237,-0.113,-80.133,-0.006,-80.046,0.155,-80.025,0.41,-80.061,0.592,-80.088,0.785,-80.036,0.835,-79.904,0.86,-79.796,0.922,-79.741,0.98,-79.613,0.971,-79.465,1.06,-79.229,1.105,-78.9,1.206,-78.827,1.296,-78.86,1.455,-78.829,1.435,-78.737,1.359,-78.682,1.283,-78.588,1.237,-78.512,1.199,-78.312,1.046,-78.181,0.969,-78.037,0.899,-77.83,0.825,-77.703,0.838,-77.673,0.782,-77.649,0.724,-77.601,0.69,-77.526,0.66,-77.481,0.651,-77.468,0.637,-77.423,0.425,-77.396,0.394,-77.293,0.36,-77.166,0.348,-77.114,0.355,-77.002,0.296,-76.92,0.269,-76.829,0.248,-76.768,0.242,-76.739,0.251,-76.729,0.272,-76.679,0.268,-76.603,0.241,-76.495,0.235,-76.427,0.261,-76.418,0.304,-76.413,0.379,-76.388,0.405,-76.311,0.448,-76.271,0.439,-76.068,0.346,-76.026,0.313,-75.975,0.248,-75.88,0.151,-75.777,0.089,-75.617,0.063,-75.464,-0.038,-75.284,-0.107]],[[-80.132,-2.973,-80.151,-3.012,-80.246,-3.008,-80.273,-2.996,-80.272,-2.952,-80.25,-2.812,-80.224,-2.753,-80.146,-2.696,-80.081,-2.669,-79.997,-2.674,-79.909,-2.726,-80.013,-2.82,-80.071,-2.834,-80.093,-2.846,-80.132,-2.973]],[[-78.909,1.253,-78.966,1.245,-78.992,1.293,-78.923,1.349,-78.9,1.36,-78.909,1.253]],[[-90.335,-0.772,-90.387,-0.773,-90.542,-0.676,-90.532,-0.581,-90.47,-0.517,-90.269,-0.485,-90.185,-0.545,-90.193,-0.659,-90.261,-0.742,-90.315,-0.757,-90.335,-0.772]],[[-89.419,-0.911,-89.537,-0.952,-89.577,-0.934,-89.603,-0.913,-89.609,-0.889,-89.543,-0.827,-89.48,-0.793,-89.423,-0.722,-89.318,-0.68,-89.288,-0.69,-89.267,-0.705,-89.259,-0.728,-89.295,-0.786,-89.358,-0.826,-89.419,-0.911]],[[-91.426,-0.461,-91.526,-0.478,-91.611,-0.444,-91.647,-0.391,-91.654,-0.311,-91.647,-0.284,-91.46,-0.256,-91.399,-0.322,-91.4,-0.421,-91.426,-0.461]],[[-90.424,-1.34,-90.464,-1.342,-90.52,-1.299,-90.477,-1.221,-90.432,-1.24,-90.399,-1.262,-90.379,-1.292,-90.424,-1.34]],[[-91.272,0.025,-91.21,-0.039,-91.176,-0.223,-90.976,-0.417,-90.951,-0.525,-90.968,-0.576,-90.959,-0.595,-90.863,-0.672,-90.8,-0.752,-90.906,-0.941,-91.131,-1.02,-91.372,-1.017,-91.419,-0.997,-91.484,-0.925,-91.495,-0.861,-91.458,-0.8,-91.334,-0.706,-91.145,-0.623,-91.121,-0.559,-91.197,-0.497,-91.25,-0.374,-91.369,-0.287,-91.429,-0.023,-91.469,-0.01,-91.55,-0.047,-91.59,-0.015,-91.597,0.002,-91.509,0.062,-91.491,0.105,-91.361,0.126,-91.306,0.091,-91.272,0.025]],[[-90.574,-0.334,-90.62,-0.364,-90.809,-0.329,-90.868,-0.271,-90.82,-0.192,-90.78,-0.16,-90.668,-0.19,-90.553,-0.278,-90.574,-0.334]]]},{"n":"Dominican Republic","c":"DO","r":5,"ml":4.5,"lx":-70.65,"ly":19.1,"b":[-72,17.636,-68.339,19.914],"p":[[[-71.768,18.039,-71.764,18.204,-71.737,18.271,-71.762,18.341,-71.873,18.416,-71.94,18.513,-72,18.598,-71.987,18.61,-71.867,18.614,-71.824,18.646,-71.743,18.733,-71.727,18.803,-71.734,18.856,-71.786,18.92,-71.807,18.987,-71.742,19.046,-71.657,19.131,-71.645,19.164,-71.647,19.196,-71.746,19.286,-71.753,19.324,-71.707,19.422,-71.711,19.487,-71.757,19.688,-71.779,19.718,-71.735,19.735,-71.706,19.795,-71.667,19.849,-71.616,19.877,-71.558,19.895,-71.442,19.894,-71.281,19.847,-71.236,19.848,-71.082,19.89,-70.954,19.914,-70.834,19.887,-70.785,19.851,-70.686,19.793,-70.636,19.776,-70.479,19.777,-70.436,19.771,-70.305,19.676,-70.194,19.638,-70.129,19.636,-70.014,19.673,-69.957,19.672,-69.891,19.59,-69.878,19.473,-69.823,19.367,-69.739,19.299,-69.325,19.328,-69.232,19.272,-69.264,19.226,-69.323,19.201,-69.52,19.212,-69.606,19.206,-69.623,19.16,-69.624,19.118,-69.508,19.108,-69.395,19.086,-69.28,19.052,-69.163,19.028,-69.031,19.013,-68.901,18.988,-68.685,18.905,-68.445,18.714,-68.381,18.671,-68.339,18.612,-68.359,18.538,-68.445,18.418,-68.493,18.379,-68.564,18.355,-68.612,18.306,-68.659,18.222,-68.687,18.215,-68.721,18.218,-68.778,18.266,-68.82,18.339,-68.935,18.408,-69.072,18.399,-69.275,18.44,-69.397,18.42,-69.519,18.416,-69.645,18.436,-69.771,18.444,-69.896,18.418,-70.018,18.374,-70.063,18.346,-70.142,18.277,-70.183,18.252,-70.48,18.217,-70.565,18.268,-70.645,18.336,-70.759,18.346,-70.924,18.292,-71.028,18.273,-71.07,18.25,-71.082,18.224,-71.083,18.128,-71.106,18.07,-71.267,17.85,-71.358,17.694,-71.396,17.646,-71.439,17.636,-71.518,17.725,-71.569,17.757,-71.632,17.774,-71.658,17.821,-71.657,17.889,-71.674,17.954,-71.712,18.005,-71.768,18.039]]]},{"n":"Dominica","c":"DM","r":6,"ml":4,"lx":-61.34,"ly":15.46,"b":[-61.481,15.227,-61.251,15.633],"p":[[[-61.282,15.249,-61.375,15.227,-61.416,15.4,-61.481,15.525,-61.47,15.603,-61.458,15.633,-61.32,15.585,-61.277,15.527,-61.251,15.373,-61.282,15.249]]]},{"n":"Djibouti","c":"DJ","r":5,"ml":4,"lx":42.5,"ly":11.98,"b":[41.765,10.941,43.41,12.709],"p":[[[43.246,11.5,43.159,11.366,43.049,11.194,42.923,10.999,42.844,10.998,42.783,11.009,42.741,11.042,42.655,11.078,42.558,11.081,42.465,11.047,42.308,11.005,42.166,10.992,42.052,10.968,41.957,10.941,41.872,10.956,41.798,10.98,41.782,11.188,41.765,11.413,41.767,11.589,41.793,11.686,41.816,11.724,41.95,11.858,41.996,11.912,42.149,12.134,42.28,12.324,42.379,12.466,42.409,12.494,42.45,12.521,42.479,12.514,42.67,12.377,42.704,12.38,42.767,12.423,42.825,12.569,42.866,12.623,42.883,12.621,43.006,12.662,43.117,12.709,43.131,12.66,43.299,12.464,43.354,12.367,43.41,12.19,43.38,12.091,43.337,12.027,43.272,11.97,43.048,11.829,42.799,11.739,42.64,11.56,42.522,11.572,42.54,11.504,42.584,11.497,42.653,11.51,42.79,11.562,42.912,11.587,43.043,11.588,43.162,11.566,43.246,11.5]]]},{"n":"Greenland","c":"GL","r":3,"ml":1.7,"lx":-39.34,"ly":74.32,"b":[-72.818,59.815,-11.426,83.6],"p":[[[-29.953,83.565,-28.992,83.505,-28.484,83.435,-28.377,83.437,-27.688,83.41,-27.034,83.377,-25.947,83.29,-25.795,83.261,-25.912,83.238,-26.183,83.221,-27.572,83.193,-30.092,83.157,-31.534,83.089,-31.993,83.085,-32.033,82.983,-31.837,82.978,-31.516,82.992,-30.386,83.094,-29.964,83.11,-29.175,83.102,-28.151,83.064,-27.739,83.077,-27.002,83.067,-26.141,83.096,-25.123,83.16,-24.845,83.019,-24.47,82.877,-24.174,82.893,-23.92,82.885,-23.834,82.839,-23.695,82.819,-23.407,82.83,-22.525,82.789,-21.92,82.716,-21.692,82.683,-21.583,82.634,-21.521,82.595,-21.616,82.548,-21.994,82.463,-22.473,82.385,-23.118,82.325,-23.862,82.287,-29.579,82.161,-29.773,82.131,-29.887,82.055,-29.811,81.955,-29.544,81.94,-28.919,81.996,-27.84,82.049,-27.046,82.046,-25.149,82.001,-24.589,81.883,-24.293,81.701,-23.637,81.742,-23.496,81.773,-23.393,81.827,-23.311,81.885,-23.249,81.947,-23.18,81.989,-23.104,82.012,-22.94,82.031,-22.563,82.053,-21.576,82.075,-21.338,82.069,-21.167,81.984,-21.13,81.934,-21.118,81.87,-21.123,81.79,-21.147,81.695,-21.231,81.601,-21.504,81.438,-21.724,81.348,-21.961,81.284,-22.415,81.137,-22.573,81.098,-23.072,80.927,-23.196,80.847,-23.204,80.789,-23.118,80.778,-22.973,80.833,-22.919,80.872,-22.826,80.913,-22.089,81.02,-21.931,81.05,-21.45,81.178,-21.142,81.226,-20.89,81.276,-20.756,81.312,-20.016,81.564,-19.63,81.64,-19.225,81.64,-19.153,81.512,-18.667,81.492,-18.457,81.498,-18.118,81.467,-17.969,81.441,-17.717,81.428,-17.456,81.398,-17.226,81.43,-17.159,81.451,-16.937,81.544,-16.637,81.626,-16.359,81.729,-16.267,81.754,-16.121,81.777,-15.969,81.785,-15.556,81.834,-15.451,81.837,-15.227,81.822,-14.242,81.814,-13.704,81.789,-12.956,81.72,-12.434,81.683,-12.193,81.649,-11.841,81.578,-11.557,81.503,-11.426,81.481,-11.431,81.457,-11.529,81.424,-12.231,81.309,-12.461,81.233,-13.126,81.088,-13.451,81.038,-13.804,81.019,-14.197,81.014,-14.452,80.993,-14.49,80.973,-14.308,80.913,-14.229,80.87,-14.24,80.832,-14.431,80.776,-14.504,80.763,-15.194,80.721,-15.543,80.65,-15.998,80.642,-16.319,80.65,-16.761,80.573,-16.588,80.511,-16.429,80.484,-15.937,80.428,-15.933,80.395,-16.168,80.329,-16.489,80.252,-16.868,80.198,-17.011,80.19,-17.191,80.204,-17.357,80.201,-17.723,80.176,-18.071,80.172,-18.693,80.207,-19.029,80.248,-19.206,80.262,-19.429,80.258,-19.515,80.241,-19.867,80.145,-20.04,80.079,-20.15,80.011,-20.197,79.938,-20.181,79.858,-20.138,79.803,-20.069,79.774,-19.985,79.756,-19.839,79.746,-19.518,79.755,-19.392,79.75,-19.353,79.734,-19.284,79.683,-19.296,79.635,-19.354,79.567,-19.399,79.488,-19.431,79.398,-19.414,79.349,-19.284,79.338,-19.223,79.342,-19.152,79.325,-19.072,79.289,-19.011,79.251,-18.971,79.211,-18.992,79.178,-19.075,79.152,-19.262,79.123,-19.723,79.065,-19.77,79.047,-19.806,79.012,-19.832,78.959,-19.887,78.911,-19.973,78.868,-20.05,78.842,-20.2,78.83,-20.396,78.829,-20.616,78.804,-21.134,78.659,-21.141,78.643,-20.947,78.596,-20.956,78.555,-21.195,78.38,-21.26,78.293,-21.312,78.174,-21.397,78.074,-21.516,77.992,-21.633,77.897,-21.748,77.791,-21.73,77.709,-21.579,77.651,-21.38,77.698,-21.132,77.847,-20.863,77.912,-20.572,77.892,-20.319,77.862,-19.995,77.803,-19.724,77.767,-19.49,77.719,-19.394,77.678,-19.297,77.621,-19.296,77.585,-19.468,77.566,-19.524,77.572,-19.953,77.666,-20.162,77.69,-20.439,77.662,-20.681,77.619,-20.464,77.447,-20.232,77.368,-19.809,77.332,-19.588,77.294,-19.426,77.246,-19.3,77.222,-19.131,77.233,-18.903,77.28,-18.586,77.283,-18.443,77.259,-18.339,77.215,-18.292,77.133,-18.303,77.012,-18.337,76.921,-18.396,76.86,-18.51,76.778,-18.606,76.763,-18.74,76.768,-18.865,76.785,-18.981,76.814,-19.156,76.837,-19.509,76.861,-19.865,76.914,-20.064,76.928,-20.487,76.921,-20.942,76.887,-20.96,76.843,-21.615,76.688,-21.749,76.69,-21.931,76.743,-22.185,76.794,-22.334,76.794,-22.555,76.729,-22.609,76.704,-22.607,76.681,-22.444,76.625,-22.379,76.612,-22.295,76.601,-22.004,76.588,-21.877,76.573,-21.758,76.401,-21.569,76.294,-21.488,76.272,-21.417,76.264,-21.185,76.268,-20.887,76.304,-20.783,76.275,-20.564,76.24,-20.435,76.231,-20.279,76.232,-20.104,76.219,-19.863,76.121,-19.958,75.997,-19.807,75.897,-19.566,75.795,-19.509,75.758,-19.486,75.69,-19.48,75.645,-19.462,75.604,-19.431,75.567,-19.4,75.494,-19.366,75.386,-19.375,75.298,-19.426,75.23,-19.526,75.18,-19.676,75.149,-19.798,75.157,-19.893,75.205,-20.027,75.255,-20.199,75.308,-20.485,75.314,-20.906,75.157,-21.094,75.149,-21.247,75.133,-21.409,75.065,-21.649,75.023,-21.861,75.04,-22.233,75.12,-22.098,75.066,-21.904,75.004,-21.784,74.971,-21.695,74.964,-21.598,74.972,-21.457,74.998,-21.141,75.069,-21.057,75.079,-20.986,75.074,-20.928,75.053,-20.861,74.993,-20.786,74.892,-20.795,74.806,-20.89,74.735,-20.971,74.69,-21.038,74.67,-21.038,74.654,-20.862,74.636,-20.611,74.728,-20.532,74.843,-20.417,74.975,-20.214,75.019,-19.985,74.975,-19.8,74.852,-19.538,74.625,-19.427,74.601,-19.287,74.546,-19.225,74.479,-19.242,74.4,-19.272,74.343,-19.315,74.307,-19.369,74.284,-19.467,74.269,-19.646,74.258,-20.048,74.282,-20.256,74.283,-20.231,74.205,-20.653,74.137,-21.129,74.111,-21.581,74.163,-21.955,74.244,-21.832,74.357,-21.762,74.483,-21.943,74.566,-21.983,74.567,-21.92,74.439,-21.973,74.39,-22.177,74.33,-22.322,74.303,-22.334,74.286,-22.264,74.272,-22.217,74.246,-22.196,74.206,-22.22,74.166,-22.291,74.125,-22.329,74.091,-22.335,74.063,-22.271,74.03,-22.135,73.99,-21.988,73.971,-21.298,73.962,-21.022,73.941,-20.367,73.848,-20.338,73.82,-20.449,73.653,-20.51,73.493,-20.637,73.464,-21.326,73.457,-21.548,73.432,-21.873,73.358,-22.185,73.27,-22.347,73.269,-22.987,73.346,-23.233,73.398,-23.761,73.543,-24.158,73.764,-24.34,73.672,-24.451,73.629,-24.566,73.606,-24.677,73.602,-24.784,73.618,-24.905,73.653,-25.109,73.734,-25.351,73.814,-25.521,73.852,-25.528,73.841,-25.427,73.794,-25.281,73.74,-24.909,73.58,-24.778,73.54,-24.791,73.511,-25.026,73.486,-25.311,73.431,-25.45,73.391,-25.665,73.293,-25.74,73.278,-26.062,73.253,-26.169,73.259,-26.407,73.313,-26.765,73.348,-26.977,73.38,-27.27,73.436,-27.169,73.374,-26.604,73.279,-26.542,73.249,-26.658,73.192,-26.729,73.171,-26.863,73.167,-27.062,73.179,-27.265,73.176,-27.472,73.16,-27.562,73.138,-27.533,73.113,-27.483,73.089,-27.413,73.068,-27.348,73.068,-27.19,73.132,-27.07,73.137,-26.753,73.121,-26.433,73.171,-26.202,73.193,-26.029,73.199,-25.399,73.276,-25.268,73.362,-25.057,73.396,-24.587,73.423,-24.133,73.409,-23.899,73.398,-23.71,73.317,-23.456,73.259,-23.244,73.193,-22.996,73.172,-22.852,73.084,-22.45,72.986,-22.194,72.965,-22.036,72.918,-22.023,72.721,-22.007,72.635,-22.075,72.399,-22.28,72.345,-22.239,72.22,-22.293,72.12,-22.498,72.158,-22.707,72.224,-23.208,72.327,-23.674,72.393,-23.856,72.452,-24.069,72.499,-24.359,72.687,-24.547,72.922,-24.63,73.038,-24.789,73.044,-24.992,73.013,-25.171,72.98,-25.256,72.924,-25.861,72.847,-26.08,72.794,-26.206,72.796,-26.658,72.716,-26.477,72.678,-26.392,72.673,-26.209,72.694,-26.1,72.722,-25.688,72.797,-25.357,72.81,-25.237,72.843,-24.985,72.889,-24.813,72.902,-24.789,72.89,-24.771,72.869,-24.65,72.583,-24.701,72.506,-24.837,72.473,-25.128,72.419,-25.204,72.393,-25.118,72.347,-24.844,72.39,-24.667,72.437,-24.572,72.42,-24.417,72.348,-24.242,72.311,-23.798,72.201,-23.587,72.14,-23.291,72.081,-22.956,71.999,-22.869,71.971,-22.562,71.928,-22.497,71.914,-22.37,71.77,-22.265,71.754,-21.96,71.745,-22.013,71.689,-22.311,71.565,-22.465,71.525,-22.503,71.5,-22.489,71.457,-22.48,71.383,-22.418,71.249,-22.348,71.373,-22.299,71.432,-22.234,71.45,-22.17,71.453,-21.961,71.508,-21.752,71.478,-21.698,71.337,-21.671,71.206,-21.69,71.092,-21.667,70.916,-21.675,70.856,-21.625,70.805,-21.574,70.59,-21.523,70.526,-21.626,70.469,-21.944,70.443,-22.069,70.472,-22.384,70.462,-22.385,70.513,-22.4,70.571,-22.401,70.612,-22.422,70.649,-22.437,70.86,-22.526,70.808,-22.531,70.765,-22.555,70.721,-22.61,70.493,-22.691,70.437,-22.943,70.451,-23.191,70.442,-23.328,70.451,-23.792,70.555,-23.971,70.649,-24.13,70.791,-24.229,70.923,-24.266,71.046,-24.377,71.146,-24.562,71.224,-24.781,71.286,-25.033,71.334,-25.255,71.396,-25.446,71.471,-25.656,71.53,-25.885,71.572,-26.211,71.59,-26.689,71.583,-27.011,71.631,-27.087,71.627,-27.162,71.602,-27.107,71.533,-26.737,71.501,-26.452,71.494,-26.074,71.498,-25.843,71.48,-25.758,71.44,-25.699,71.368,-25.668,71.265,-25.742,71.184,-26.014,71.093,-26.158,71.05,-26.576,70.969,-26.718,70.95,-27.067,70.945,-27.336,70.953,-27.689,70.993,-27.889,71.002,-28.303,71.007,-28.398,70.993,-28.292,70.949,-28.116,70.925,-27.992,70.895,-27.979,70.84,-28.024,70.757,-28.07,70.699,-28.146,70.656,-28.417,70.574,-28.53,70.548,-29.037,70.462,-29.072,70.445,-28.953,70.447,-28.633,70.478,-28.541,70.477,-28.015,70.402,-27.596,70.407,-26.747,70.476,-26.677,70.474,-26.622,70.463,-26.565,70.438,-26.508,70.397,-26.577,70.357,-26.771,70.319,-27.073,70.281,-27.203,70.256,-27.328,70.217,-27.561,70.124,-27.629,70.028,-27.384,69.992,-27.274,70.038,-27.144,70.141,-27.028,70.201,-26.752,70.242,-26.416,70.221,-26.156,70.246,-25.625,70.347,-25.53,70.353,-24.749,70.295,-24.041,70.181,-23.667,70.139,-23.173,70.115,-22.284,70.126,-22.207,70.108,-22.235,70.068,-22.287,70.033,-22.435,69.986,-22.615,69.954,-22.726,69.945,-22.821,69.923,-23.034,69.901,-23.088,69.883,-23.015,69.805,-23.05,69.793,-23.237,69.791,-23.553,69.741,-23.812,69.744,-23.866,69.737,-23.817,69.718,-23.764,69.681,-23.709,69.627,-23.739,69.589,-23.856,69.566,-23.944,69.558,-24.248,69.59,-24.297,69.586,-24.252,69.562,-24.227,69.527,-24.221,69.479,-24.296,69.439,-24.451,69.407,-24.741,69.318,-24.867,69.293,-25.133,69.272,-25.189,69.261,-25.08,69.192,-25.092,69.165,-25.272,69.092,-25.544,69.046,-25.581,69.021,-25.606,68.954,-25.626,68.928,-25.698,68.89,-25.956,68.817,-26.139,68.781,-26.229,68.752,-26.341,68.702,-26.483,68.676,-26.654,68.673,-26.815,68.654,-27.081,68.602,-27.266,68.584,-27.851,68.494,-28.126,68.479,-28.365,68.447,-28.854,68.36,-29.088,68.332,-29.25,68.299,-29.426,68.289,-29.714,68.311,-29.869,68.312,-29.964,68.299,-30.051,68.272,-30.196,68.199,-30.318,68.193,-30.72,68.251,-30.712,68.225,-30.606,68.162,-30.611,68.118,-30.85,68.073,-30.979,68.061,-31.168,68.08,-31.419,68.128,-31.742,68.23,-32.137,68.385,-32.327,68.437,-32.314,68.388,-32.27,68.339,-32.195,68.292,-32.18,68.257,-32.224,68.236,-32.282,68.225,-32.355,68.225,-32.367,68.213,-32.249,68.139,-32.156,68.063,-32.165,67.991,-32.275,67.923,-32.37,67.883,-32.449,67.871,-32.918,67.701,-33.049,67.679,-33.108,67.658,-33.157,67.627,-33.294,67.486,-33.349,67.443,-33.458,67.387,-33.504,67.377,-33.518,67.354,-33.498,67.318,-33.528,67.258,-33.608,67.174,-33.881,66.942,-34.102,66.726,-34.198,66.655,-34.269,66.625,-34.314,66.636,-34.423,66.63,-34.476,66.592,-34.524,66.523,-34.576,66.471,-34.633,66.435,-35.075,66.279,-35.189,66.25,-35.291,66.269,-35.412,66.262,-35.662,66.344,-35.705,66.374,-35.867,66.441,-35.862,66.406,-35.835,66.387,-35.812,66.358,-35.756,66.324,-35.63,66.14,-35.729,66.102,-35.818,66.059,-36.044,65.987,-36.289,65.865,-36.379,65.831,-36.399,65.93,-36.389,65.96,-36.527,66.008,-36.523,65.973,-36.537,65.941,-36.637,65.812,-36.665,65.79,-36.715,65.795,-36.822,65.771,-36.932,65.783,-37.026,65.841,-37.063,65.871,-37.233,65.788,-37.316,65.79,-37.33,65.72,-37.41,65.656,-37.516,65.629,-37.664,65.631,-37.754,65.593,-37.955,65.634,-38.001,65.71,-37.842,65.814,-37.797,65.857,-37.827,65.91,-37.788,65.978,-37.484,66.195,-37.279,66.304,-37.291,66.324,-37.57,66.348,-37.814,66.385,-38.052,66.398,-38.157,66.386,-37.989,66.323,-37.752,66.262,-37.869,66.203,-37.969,66.141,-38.073,65.973,-38.14,65.904,-38.398,65.983,-38.52,66.01,-38.443,65.922,-38.216,65.838,-38.202,65.811,-38.203,65.712,-38.637,65.624,-39.089,65.611,-39.413,65.586,-39.961,65.556,-40.174,65.556,-40.192,65.523,-39.656,65.369,-39.578,65.341,-39.653,65.288,-39.763,65.255,-39.937,65.142,-40.028,65.103,-40.253,65.049,-40.668,65.109,-40.881,65.082,-41.084,65.101,-41.089,65.035,-41.028,64.988,-40.966,64.869,-40.829,64.878,-40.655,64.915,-40.521,64.825,-40.433,64.673,-40.278,64.596,-40.21,64.536,-40.182,64.48,-40.278,64.424,-40.478,64.344,-40.699,64.33,-40.686,64.267,-40.782,64.222,-40.985,64.235,-41.079,64.267,-41.178,64.281,-41.581,64.298,-41.175,64.177,-41.031,64.121,-40.966,64.154,-40.826,64.163,-40.618,64.132,-40.652,63.928,-40.561,63.762,-40.55,63.725,-40.772,63.626,-40.775,63.534,-40.907,63.508,-41.049,63.514,-41.056,63.412,-41.152,63.349,-41.135,63.309,-41.108,63.274,-41.195,63.209,-41.275,63.131,-41.388,63.062,-41.448,63.069,-41.628,63.065,-41.844,63.07,-42.02,63.16,-42.092,63.189,-42.175,63.209,-42.143,63.151,-42.094,63.117,-41.932,63.052,-41.634,62.972,-41.644,62.916,-41.723,62.891,-41.909,62.737,-41.975,62.734,-42.058,62.694,-42.316,62.707,-42.424,62.723,-42.741,62.713,-42.849,62.727,-42.942,62.72,-42.855,62.677,-42.674,62.638,-42.467,62.598,-42.153,62.568,-42.164,62.512,-42.243,62.466,-42.198,62.397,-42.233,62.348,-42.248,62.289,-42.321,62.153,-42.236,62.059,-42.143,62.014,-42.154,61.953,-42.11,61.857,-42.25,61.771,-42.365,61.775,-42.53,61.755,-42.585,61.717,-42.324,61.682,-42.347,61.617,-42.419,61.537,-42.494,61.363,-42.646,61.064,-42.717,60.767,-43.044,60.524,-43.16,60.517,-43.189,60.507,-43.348,60.52,-43.598,60.576,-43.792,60.595,-43.923,60.595,-43.94,60.567,-43.831,60.522,-43.665,60.503,-43.533,60.473,-43.296,60.445,-43.213,60.391,-43.156,60.333,-43.165,60.301,-43.165,60.263,-43.123,60.061,-43.235,59.991,-43.32,59.928,-43.617,59.937,-43.669,59.959,-43.955,60.025,-43.937,59.994,-43.73,59.904,-43.658,59.859,-43.706,59.849,-43.79,59.846,-43.907,59.815,-44.117,59.832,-44.105,59.878,-44.065,59.925,-44.162,59.917,-44.269,59.893,-44.331,59.902,-44.384,59.899,-44.413,59.923,-44.453,60.015,-44.405,60.061,-44.231,60.18,-44.176,60.244,-44.224,60.274,-44.348,60.205,-44.476,60.096,-44.533,60.029,-44.613,60.017,-44.812,60.05,-45.379,60.203,-45.362,60.296,-45.368,60.373,-45.202,60.383,-45.082,60.416,-44.975,60.457,-44.854,60.532,-44.742,60.655,-44.757,60.665,-45.083,60.507,-45.283,60.455,-45.381,60.445,-45.429,60.468,-45.59,60.519,-45.695,60.542,-45.934,60.579,-45.977,60.6,-46.047,60.616,-46.142,60.777,-46.019,60.972,-45.934,61.028,-45.88,61.094,-45.849,61.181,-45.87,61.218,-45.942,61.206,-45.976,61.176,-45.97,61.129,-46.012,61.097,-46.297,61.022,-46.582,60.962,-46.718,60.905,-46.806,60.86,-46.874,60.816,-46.98,60.82,-47.125,60.811,-47.224,60.783,-47.37,60.8,-47.465,60.843,-47.579,60.847,-47.707,60.827,-47.796,60.829,-47.789,60.8,-47.73,60.729,-47.828,60.725,-48.014,60.722,-48.108,60.742,-48.181,60.769,-48.242,60.807,-48.205,60.856,-47.906,60.946,-47.77,60.998,-47.859,61.016,-48.146,60.999,-48.194,61.013,-48.386,61.005,-48.378,61.138,-48.425,61.172,-48.428,61.187,-48.495,61.225,-48.558,61.234,-48.597,61.247,-48.922,61.277,-48.965,61.352,-48.987,61.429,-49.049,61.524,-49.205,61.549,-49.289,61.59,-49.222,61.632,-49.193,61.686,-49.265,61.71,-49.311,61.748,-49.304,61.772,-49.363,61.839,-49.38,61.89,-49.313,61.939,-49.13,61.993,-49.071,62.015,-49.04,62.039,-48.829,62.08,-49.008,62.108,-49.12,62.113,-49.202,62.099,-49.278,62.046,-49.349,62.01,-49.624,61.999,-49.664,62.017,-49.683,62.093,-49.668,62.151,-49.553,62.233,-49.685,62.273,-49.806,62.287,-49.943,62.324,-50.07,62.365,-50.179,62.411,-50.285,62.466,-50.319,62.473,-50.281,62.531,-50.259,62.578,-50.256,62.68,-50.299,62.722,-50.204,62.809,-50.076,62.904,-49.793,63.045,-50.092,62.977,-50.338,62.829,-50.39,62.822,-50.408,62.849,-50.502,62.945,-50.572,62.971,-50.604,63,-50.744,63.051,-50.804,63.091,-50.89,63.167,-51.013,63.258,-51.188,63.436,-51.469,63.642,-51.538,63.758,-51.451,63.905,-51.548,64.006,-51.28,64.053,-50.898,64.106,-50.699,64.149,-50.585,64.162,-50.342,64.17,-50.261,64.214,-50.396,64.203,-50.487,64.209,-50.492,64.229,-50.459,64.266,-50.437,64.313,-50.483,64.304,-50.721,64.223,-51.072,64.159,-51.347,64.123,-51.391,64.125,-51.487,64.103,-51.542,64.097,-51.585,64.103,-51.682,64.165,-51.708,64.205,-51.534,64.314,-51.404,64.463,-51.232,64.561,-51.11,64.573,-50.907,64.568,-50.835,64.559,-50.858,64.617,-50.849,64.645,-50.684,64.678,-50.492,64.693,-50.355,64.683,-50.269,64.615,-50.158,64.49,-50.009,64.447,-50.016,64.507,-50.093,64.585,-50.122,64.704,-50.22,64.754,-50.299,64.779,-50.517,64.767,-50.648,64.853,-50.678,64.885,-50.681,64.928,-50.812,65.052,-50.854,65.114,-50.924,65.197,-50.961,65.201,-50.914,65.097,-50.852,65.024,-50.765,64.863,-50.722,64.798,-50.78,64.746,-50.891,64.695,-50.989,64.665,-51.221,64.628,-51.171,64.708,-51.139,64.786,-51.255,64.758,-51.364,64.702,-51.401,64.623,-51.47,64.552,-51.677,64.377,-51.758,64.28,-51.835,64.232,-51.923,64.219,-51.999,64.257,-52.063,64.346,-52.093,64.416,-52.097,64.597,-52.089,64.682,-52.124,64.795,-52.235,65.061,-52.259,65.155,-52.448,65.205,-52.45,65.221,-52.5,65.275,-52.538,65.329,-52.506,65.348,-52.461,65.363,-52.18,65.442,-51.971,65.531,-51.721,65.67,-51.619,65.713,-51.253,65.746,-51.09,65.751,-51.092,65.776,-51.146,65.786,-51.394,65.779,-51.723,65.723,-51.78,65.703,-51.924,65.617,-52.035,65.569,-52.348,65.461,-52.551,65.461,-52.761,65.591,-52.995,65.566,-53.153,65.575,-53.199,65.594,-53.234,65.771,-53.106,65.977,-53.244,65.979,-53.272,65.987,-53.345,66.034,-53.392,66.048,-53.357,66.073,-53.018,66.171,-52.511,66.362,-52.293,66.438,-52.158,66.47,-52.056,66.507,-51.932,66.588,-51.891,66.623,-51.822,66.652,-51.676,66.684,-51.517,66.732,-51.259,66.841,-51.225,66.882,-51.281,66.891,-51.402,66.854,-51.648,66.754,-51.823,66.698,-52.421,66.447,-52.676,66.355,-52.814,66.297,-52.922,66.241,-53.036,66.201,-53.156,66.178,-53.413,66.16,-53.539,66.139,-53.615,66.154,-53.648,66.274,-53.623,66.344,-53.635,66.414,-53.571,66.513,-53.476,66.584,-53.436,66.622,-53.419,66.649,-53.223,66.721,-53.115,66.754,-53.038,66.827,-52.603,66.853,-52.491,66.85,-52.431,66.86,-52.387,66.881,-52.43,66.898,-52.56,66.909,-52.907,66.907,-53.227,66.919,-53.373,66.932,-53.444,66.925,-53.56,66.946,-53.687,66.986,-53.884,67.136,-53.805,67.327,-53.799,67.418,-53.548,67.498,-53.414,67.525,-53.224,67.585,-52.97,67.687,-52.666,67.75,-52.512,67.761,-52.384,67.752,-51.909,67.664,-51.665,67.646,-51.451,67.668,-51.181,67.637,-50.705,67.509,-50.613,67.528,-50.64,67.559,-51.171,67.694,-51.168,67.734,-51.032,67.744,-50.887,67.784,-50.969,67.807,-51.321,67.787,-51.423,67.754,-51.765,67.738,-51.944,67.765,-52.104,67.779,-52.345,67.837,-52.546,67.818,-52.673,67.795,-52.898,67.773,-52.98,67.758,-53.419,67.575,-53.604,67.536,-53.735,67.549,-53.643,67.668,-53.616,67.715,-53.616,67.767,-53.578,67.837,-53.353,67.971,-53.211,68.117,-53.152,68.208,-53.041,68.218,-52.89,68.205,-52.436,68.146,-52.058,68.075,-51.78,68.057,-51.597,68.055,-51.518,68.077,-51.456,68.116,-51.433,68.143,-51.415,68.198,-51.394,68.218,-51.333,68.242,-51.207,68.326,-51.169,68.385,-51.21,68.42,-51.293,68.416,-51.456,68.394,-51.478,68.384,-51.475,68.365,-51.632,68.273,-51.804,68.252,-52.199,68.221,-52.379,68.219,-52.698,68.262,-52.747,68.278,-52.78,68.31,-53.173,68.303,-53.29,68.293,-53.383,68.297,-53.337,68.352,-53.213,68.413,-53.039,68.611,-52.894,68.662,-52.605,68.709,-52.303,68.701,-51.781,68.548,-51.623,68.535,-51.479,68.547,-51.133,68.598,-51.07,68.619,-50.946,68.683,-50.801,68.791,-50.808,68.817,-51.03,68.756,-51.149,68.74,-51.249,68.74,-51.156,68.938,-51.12,69.091,-51.085,69.128,-50.792,69.117,-50.393,69.137,-50.297,69.171,-50.299,69.185,-50.459,69.206,-50.537,69.248,-50.671,69.234,-50.851,69.206,-51.077,69.209,-51.058,69.275,-50.892,69.412,-50.875,69.474,-50.811,69.599,-50.804,69.663,-50.72,69.725,-50.459,69.77,-50.349,69.796,-50.343,69.825,-50.5,69.936,-50.46,69.966,-50.337,69.994,-50.292,70.014,-50.323,70.027,-50.436,70.039,-50.61,70.015,-50.802,70.003,-50.973,70.04,-51.106,70.057,-51.19,70.052,-51.419,69.989,-51.499,69.987,-51.598,70.005,-52.255,70.059,-52.336,70.078,-52.571,70.172,-52.765,70.234,-53.023,70.302,-53.358,70.353,-53.769,70.389,-54.014,70.422,-54.136,70.468,-54.343,70.571,-54.501,70.657,-54.531,70.699,-54.438,70.752,-54.344,70.789,-54.166,70.82,-53.859,70.81,-53.694,70.796,-53.513,70.767,-53.376,70.761,-53.091,70.769,-52.802,70.751,-52.63,70.73,-52.405,70.687,-51.784,70.503,-51.524,70.439,-51.412,70.432,-50.947,70.364,-50.872,70.365,-50.682,70.397,-50.663,70.418,-50.728,70.438,-50.933,70.454,-51.173,70.529,-51.323,70.589,-51.34,70.688,-51.32,70.743,-51.283,70.768,-51.257,70.853,-51.396,70.903,-51.494,70.919,-51.753,70.992,-51.774,71.01,-51.65,71.019,-51.528,71.014,-51.267,70.977,-51.13,70.972,-51.03,70.986,-51.019,71.001,-51.178,71.043,-51.377,71.119,-51.792,71.13,-52.061,71.122,-52.234,71.148,-52.417,71.19,-52.535,71.2,-52.775,71.174,-52.897,71.171,-53.008,71.18,-53.117,71.313,-53.088,71.353,-53.002,71.37,-52.937,71.413,-52.892,71.458,-52.749,71.502,-51.967,71.599,-51.77,71.672,-51.779,71.683,-51.912,71.669,-52.082,71.637,-52.196,71.63,-52.656,71.672,-52.728,71.663,-52.915,71.602,-53.168,71.536,-53.284,71.54,-53.44,71.579,-53.465,71.607,-53.476,71.64,-53.305,71.686,-53.25,71.71,-53.139,71.775,-53.145,71.807,-53.334,71.79,-53.358,71.82,-53.355,71.871,-53.374,71.936,-53.42,72,-53.575,72.098,-53.64,72.123,-53.693,72.16,-53.81,72.293,-53.776,72.326,-53.672,72.351,-53.652,72.363,-53.901,72.342,-53.928,72.319,-53.881,72.285,-53.847,72.24,-53.828,72.183,-53.793,72.134,-53.703,72.08,-53.631,72.052,-53.514,71.976,-53.462,71.894,-53.478,71.85,-53.569,71.806,-53.715,71.758,-53.76,71.718,-53.78,71.679,-53.894,71.642,-53.964,71.656,-54.02,71.658,-53.954,71.593,-53.912,71.526,-53.963,71.459,-54.099,71.419,-54.173,71.417,-54.318,71.384,-54.689,71.367,-54.818,71.375,-55.055,71.409,-55.336,71.427,-55.448,71.472,-55.594,71.554,-55.668,71.627,-55.669,71.692,-55.63,71.739,-55.549,71.768,-55.452,71.958,-55.316,72.111,-54.971,72.268,-54.873,72.325,-54.84,72.356,-54.841,72.379,-54.896,72.394,-55.32,72.2,-55.581,72.179,-55.659,72.223,-55.636,72.3,-55.589,72.319,-55.378,72.311,-55.296,72.354,-55.428,72.42,-55.569,72.437,-55.602,72.453,-55.457,72.503,-55.122,72.5,-55.046,72.534,-54.925,72.572,-54.79,72.642,-54.74,72.7,-54.729,72.75,-54.758,72.791,-54.761,72.832,-54.738,72.873,-54.773,72.918,-54.866,72.967,-55.073,73.015,-55.134,72.961,-55.198,72.938,-55.289,72.933,-55.372,72.956,-55.46,72.964,-55.545,72.985,-55.634,72.991,-55.669,73.008,-55.691,73.054,-55.693,73.113,-55.592,73.14,-55.452,73.162,-55.359,73.203,-55.297,73.262,-55.288,73.327,-55.332,73.397,-55.446,73.46,-55.656,73.399,-55.739,73.384,-55.758,73.428,-55.787,73.46,-55.876,73.505,-55.992,73.537,-56.104,73.558,-56.109,73.591,-56.083,73.627,-56.033,73.67,-55.968,73.76,-55.897,73.752,-55.838,73.76,-55.872,73.833,-55.929,73.895,-55.997,73.931,-55.999,73.946,-56.014,73.964,-56.066,74.007,-56.124,74.039,-56.225,74.129,-56.298,74.163,-56.392,74.181,-56.493,74.182,-56.655,74.159,-56.954,74.131,-57.191,74.118,-57.231,74.125,-57.112,74.159,-56.937,74.195,-56.706,74.219,-56.639,74.278,-56.664,74.33,-56.654,74.378,-56.718,74.429,-56.656,74.458,-56.446,74.486,-56.35,74.49,-56.255,74.527,-56.522,74.614,-56.801,74.672,-56.871,74.695,-56.933,74.733,-56.986,74.787,-57.072,74.84,-57.191,74.894,-57.365,74.945,-57.813,75.04,-57.967,75.105,-58.109,75.205,-58.18,75.247,-58.253,75.279,-58.566,75.353,-58.603,75.385,-58.281,75.472,-58.25,75.507,-58.381,75.612,-58.516,75.689,-58.663,75.716,-58.881,75.73,-59.082,75.765,-59.264,75.819,-59.445,75.859,-59.717,75.896,-60.173,75.993,-60.875,76.097,-61.188,76.158,-61.375,76.18,-61.621,76.186,-62.097,76.242,-62.496,76.26,-62.743,76.252,-62.823,76.262,-63.006,76.319,-63.291,76.352,-63.439,76.339,-63.622,76.278,-63.843,76.217,-63.96,76.209,-64.135,76.265,-64.223,76.303,-64.307,76.317,-64.387,76.304,-64.543,76.253,-64.692,76.216,-64.912,76.173,-65.088,76.152,-65.313,76.146,-65.37,76.131,-65.457,76.13,-65.574,76.144,-65.683,76.173,-65.785,76.215,-65.876,76.238,-65.954,76.242,-66.134,76.22,-66.362,76.155,-66.466,76.139,-66.553,76.146,-66.66,76.166,-66.874,76.218,-66.993,76.213,-67.079,76.195,-67.055,76.152,-66.854,76.05,-66.675,75.977,-66.826,75.969,-68.149,76.067,-68.317,76.091,-68.561,76.15,-68.763,76.187,-69.108,76.281,-69.373,76.332,-69.461,76.372,-69.484,76.399,-69.4,76.436,-68.865,76.561,-68.661,76.587,-68.245,76.617,-68.147,76.636,-68.114,76.651,-68.223,76.678,-68.767,76.668,-69.252,76.686,-69.674,76.736,-69.747,76.752,-69.819,76.783,-69.888,76.827,-69.872,76.877,-69.771,76.931,-69.712,76.969,-69.694,76.989,-70.224,76.855,-70.441,76.807,-70.613,76.822,-70.734,76.844,-70.793,76.869,-70.791,76.896,-70.771,76.917,-70.735,76.929,-71.015,76.985,-71.141,77.029,-71.155,77.074,-71.055,77.121,-70.958,77.154,-70.863,77.175,-70.604,77.194,-69.657,77.229,-68.978,77.195,-68.747,77.307,-68.592,77.343,-68.136,77.38,-67.434,77.385,-66.938,77.364,-66.706,77.338,-66.389,77.28,-66.371,77.298,-66.448,77.35,-66.453,77.393,-66.325,77.468,-66.266,77.515,-66.306,77.565,-66.445,77.616,-66.691,77.681,-66.824,77.687,-66.971,77.671,-67.147,77.635,-67.515,77.543,-67.688,77.524,-67.977,77.519,-68.137,77.53,-68.292,77.544,-68.533,77.593,-68.622,77.602,-68.728,77.581,-68.854,77.529,-68.975,77.493,-69.091,77.472,-69.2,77.463,-69.351,77.467,-69.977,77.548,-70.118,77.583,-70.126,77.638,-70.318,77.69,-70.535,77.7,-70.562,77.717,-70.287,77.798,-70.081,77.831,-70.114,77.841,-70.412,77.843,-70.614,77.8,-70.729,77.793,-70.994,77.792,-71.272,77.813,-71.39,77.832,-71.512,77.875,-71.65,77.9,-72.065,77.937,-72.159,77.957,-72.247,77.99,-72.586,78.085,-72.792,78.155,-72.818,78.194,-72.581,78.279,-72.571,78.299,-72.672,78.335,-72.715,78.362,-72.68,78.4,-72.473,78.482,-72.396,78.504,-72.024,78.553,-71.651,78.623,-71.516,78.639,-71.395,78.643,-70.906,78.638,-70.754,78.656,-70.625,78.69,-70.414,78.725,-69.974,78.778,-68.993,78.857,-68.93,78.867,-68.924,78.882,-69.012,78.923,-69.031,78.943,-68.83,78.98,-68.377,79.038,-68.068,79.066,-67.868,79.068,-67.708,79.08,-67.482,79.117,-67.355,79.123,-66.584,79.138,-66.243,79.118,-66.075,79.118,-65.968,79.132,-65.826,79.174,-65.56,79.276,-65.42,79.34,-65.288,79.437,-65.117,79.589,-64.989,79.737,-64.905,79.881,-64.839,79.969,-64.792,80.001,-64.632,80.041,-64.466,80.072,-64.179,80.099,-64.205,80.112,-64.327,80.134,-64.44,80.142,-64.545,80.137,-64.735,80.104,-64.982,80.082,-65.222,80.086,-65.395,80.078,-65.553,80.048,-65.81,80.024,-65.982,80.029,-66.292,80.072,-66.448,80.08,-66.844,80.076,-66.959,80.092,-67.061,80.123,-67.141,80.166,-67.201,80.222,-67.193,80.28,-67.051,80.385,-66.996,80.413,-66.61,80.53,-66.372,80.584,-66.136,80.625,-65.963,80.649,-65.801,80.66,-65.645,80.685,-65.358,80.767,-65.062,80.836,-64.694,80.966,-64.516,81,-63.892,81.056,-63.722,81.057,-63.578,81.043,-63.442,81.014,-63.059,80.886,-63.029,80.89,-63.095,80.938,-63.235,81.083,-63.212,81.143,-62.993,81.207,-62.903,81.218,-62.672,81.214,-62.299,81.194,-62.049,81.173,-61.86,81.138,-61.636,81.116,-61.519,81.117,-61.436,81.134,-61.317,81.188,-61.162,81.281,-61.1,81.396,-61.131,81.532,-61.176,81.632,-61.236,81.695,-61.203,81.747,-61.015,81.81,-60.843,81.855,-60.432,81.92,-60.099,81.937,-59.902,81.933,-59.642,81.903,-59.282,81.884,-58.957,81.825,-58.43,81.69,-58.08,81.622,-57.79,81.592,-57.505,81.54,-57.083,81.43,-56.862,81.383,-56.731,81.366,-56.615,81.363,-56.658,81.394,-56.86,81.46,-57.168,81.532,-57.853,81.662,-58.23,81.754,-58.568,81.858,-58.817,81.92,-59.268,81.982,-59.262,82.007,-58.717,82.093,-57.717,82.168,-56.589,82.227,-56.212,82.221,-55.549,82.246,-55.486,82.283,-55.344,82.3,-54.726,82.351,-54.549,82.351,-54.277,82.326,-53.987,82.279,-53.853,82.237,-53.671,82.164,-53.582,82.062,-53.596,81.738,-53.591,81.677,-53.556,81.653,-53.43,81.688,-53.28,81.754,-53.145,81.8,-53.041,81.871,-52.969,81.967,-52.926,82.038,-53.102,82.119,-53.111,82.251,-53.023,82.322,-52.776,82.322,-51.754,82.078,-51.352,82.026,-50.894,81.895,-50.36,81.909,-49.867,81.893,-49.649,81.898,-49.541,81.918,-49.694,81.972,-50.395,82.121,-50.713,82.237,-50.936,82.383,-50.99,82.46,-50.82,82.474,-50.037,82.472,-48.861,82.405,-47.357,82.174,-46.617,82.097,-45.291,81.829,-44.891,81.788,-44.729,81.78,-44.608,81.813,-44.532,81.849,-44.527,81.897,-44.591,81.957,-44.628,82.026,-44.637,82.104,-44.547,82.26,-44.333,82.311,-44.239,82.368,-44.327,82.472,-44.577,82.543,-45.552,82.725,-45.557,82.747,-45.36,82.771,-45.067,82.785,-42.651,82.741,-42.233,82.725,-42.055,82.71,-41.977,82.689,-41.876,82.68,-41.357,82.705,-41.37,82.75,-41.434,82.779,-44.239,82.857,-44.762,82.884,-45.028,82.886,-45.303,82.865,-45.873,82.855,-46.137,82.859,-46.478,82.952,-46.169,83.064,-45.909,83.061,-45.415,83.018,-45.122,83.079,-44.657,83.129,-44.197,83.147,-43.195,83.255,-43.009,83.265,-42.776,83.259,-42.26,83.232,-42.055,83.205,-41.82,83.148,-41.683,83.13,-41.522,83.127,-41.3,83.101,-40.979,83.185,-40.689,83.275,-40.357,83.332,-39.886,83.299,-39.588,83.256,-39.316,83.204,-38.931,83.175,-38.278,82.999,-38.156,82.999,-38.099,83.014,-38.037,83.046,-38.015,83.095,-37.935,83.161,-37.993,83.185,-38.54,83.258,-38.643,83.286,-38.748,83.333,-38.75,83.371,-38.648,83.401,-38.541,83.415,-38.188,83.402,-38.071,83.412,-37.961,83.438,-37.828,83.486,-37.723,83.498,-37.487,83.499,-37.123,83.468,-36.804,83.466,-36.69,83.48,-36.672,83.51,-36.644,83.529,-36.606,83.537,-35.452,83.539,-35.166,83.546,-34.942,83.568,-34.668,83.571,-34.428,83.558,-34.132,83.529,-33.837,83.53,-33.398,83.577,-32.984,83.6,-30.703,83.593,-29.953,83.565]],[[-52.731,69.945,-52.398,69.863,-52.045,69.807,-52.011,69.782,-51.983,69.743,-51.977,69.722,-51.985,69.704,-52.007,69.686,-51.982,69.664,-51.908,69.637,-51.9,69.605,-51.988,69.55,-52.113,69.489,-52.77,69.364,-53.003,69.343,-53.578,69.257,-53.754,69.26,-53.793,69.264,-53.902,69.302,-54.051,69.337,-54.121,69.364,-54.183,69.404,-54.158,69.428,-54.047,69.437,-53.89,69.437,-53.658,69.465,-53.722,69.491,-53.783,69.506,-53.825,69.54,-53.921,69.534,-53.994,69.553,-54.133,69.565,-54.497,69.577,-54.734,69.611,-54.804,69.631,-54.866,69.665,-54.919,69.714,-54.841,69.902,-54.788,69.95,-54.665,69.966,-54.363,69.924,-54.323,69.942,-54.652,70.011,-54.774,70.053,-54.809,70.085,-54.831,70.133,-54.83,70.161,-54.816,70.189,-54.786,70.218,-54.706,70.256,-54.372,70.317,-54.007,70.296,-53.375,70.221,-53.297,70.205,-53.103,70.141,-52.731,69.945]],[[-51.014,69.552,-51.17,69.517,-51.202,69.525,-51.234,69.552,-51.315,69.674,-51.339,69.732,-51.319,69.804,-51.35,69.855,-51.209,69.914,-51.095,69.924,-50.94,69.909,-50.679,69.849,-50.698,69.829,-50.754,69.798,-50.912,69.757,-50.967,69.664,-50.978,69.618,-50.97,69.583,-51.014,69.552]],[[-53.535,71.041,-53.629,71.034,-53.898,71.085,-53.941,71.104,-53.958,71.128,-53.947,71.156,-53.862,71.207,-53.701,71.283,-53.584,71.297,-53.512,71.25,-53.441,71.186,-53.432,71.153,-53.437,71.115,-53.455,71.083,-53.488,71.056,-53.535,71.041]],[[-55.017,72.791,-55.158,72.723,-55.274,72.684,-55.524,72.568,-55.567,72.564,-55.634,72.579,-55.687,72.61,-55.781,72.617,-55.814,72.636,-55.827,72.652,-55.869,72.662,-55.936,72.668,-56.043,72.656,-56.141,72.668,-56.215,72.719,-56.078,72.753,-55.994,72.782,-55.666,72.794,-55.574,72.78,-55.516,72.781,-55.428,72.789,-55.235,72.825,-55.206,72.842,-55.033,72.821,-55.017,72.791]],[[-71.667,77.325,-72.024,77.316,-72.374,77.355,-72.495,77.386,-72.49,77.432,-72.436,77.448,-72.247,77.464,-72.089,77.467,-71.983,77.46,-71.733,77.432,-71.552,77.403,-71.433,77.394,-71.467,77.354,-71.667,77.325]],[[-44.865,82.084,-45.067,82.066,-45.491,82.172,-46.161,82.278,-46.752,82.348,-47.308,82.533,-47.351,82.599,-47.272,82.657,-46.787,82.666,-46.399,82.692,-45.411,82.578,-44.917,82.481,-44.75,82.401,-44.776,82.242,-44.865,82.084]],[[-25.432,70.921,-25.397,70.862,-25.394,70.835,-25.401,70.811,-25.421,70.795,-25.468,70.78,-25.38,70.741,-25.352,70.714,-25.346,70.693,-25.402,70.653,-25.801,70.599,-25.911,70.573,-26.05,70.509,-26.218,70.454,-26.274,70.454,-26.339,70.511,-26.605,70.553,-27.105,70.531,-27.69,70.479,-27.898,70.454,-28.003,70.467,-28.035,70.487,-28.037,70.514,-27.968,70.595,-27.94,70.615,-27.805,70.642,-27.714,70.713,-27.744,70.79,-27.709,70.897,-27.617,70.914,-27.387,70.876,-27.239,70.868,-26.976,70.863,-26.622,70.876,-26.337,70.919,-25.819,71.044,-25.727,71.042,-25.661,70.998,-25.612,70.976,-25.458,70.943,-25.432,70.921]],[[-18.665,81.846,-18.768,81.814,-19.031,81.827,-19.369,81.917,-19.594,81.991,-19.611,82.078,-19.495,82.117,-19.315,82.123,-19.067,82.049,-18.813,81.949,-18.665,81.846]],[[-17.612,79.826,-18.036,79.711,-18.662,79.72,-19.032,79.773,-19.138,79.852,-18.997,79.94,-18.547,80.011,-17.983,80.055,-17.471,80.029,-17.401,79.94,-17.612,79.826]],[[-18.997,77.974,-19.129,77.939,-19.218,78.044,-19.297,78.185,-19.315,78.344,-19.112,78.424,-19.006,78.441,-18.935,78.424,-18.953,78.353,-18.953,78.212,-18.882,78.115,-18.997,77.974]],[[-18.583,76.042,-18.697,76.016,-19.085,76.43,-19.085,76.58,-19.059,76.695,-18.882,76.704,-18.733,76.642,-18.662,76.404,-18.583,76.042]],[[-18.001,75.407,-17.921,75.302,-17.886,75.204,-17.762,75.143,-17.498,75.152,-17.392,75.037,-17.586,74.993,-18.353,75.01,-18.671,75.002,-18.891,75.072,-18.882,75.196,-18.856,75.319,-18.636,75.39,-18.45,75.328,-18.23,75.372,-18.001,75.407]],[[-17.954,77.642,-18.148,77.642,-18.22,77.668,-18.174,77.714,-17.904,77.863,-17.814,77.875,-17.681,77.859,-17.641,77.782,-17.729,77.706,-17.954,77.642]],[[-37.031,65.532,-37.187,65.531,-37.238,65.61,-37.223,65.695,-37.048,65.722,-36.953,65.663,-36.987,65.576,-37.031,65.532]],[[-46.267,60.781,-46.382,60.66,-46.496,60.687,-46.553,60.741,-46.666,60.766,-46.788,60.758,-46.79,60.78,-46.394,60.909,-46.205,60.944,-46.219,60.889,-46.254,60.842,-46.267,60.781]],[[-51.675,70.855,-51.809,70.853,-52.119,70.871,-52.144,70.882,-52.148,70.904,-52.107,70.968,-51.97,70.976,-51.807,70.942,-51.631,70.892,-51.607,70.869,-51.675,70.855]]]},{"n":"Faroe Islands","c":"FO","r":6,"ml":4,"lx":-7.06,"ly":62.19,"b":[-7.423,61.414,-6.406,62.356],"p":[[[-6.623,61.806,-6.643,61.768,-6.67,61.769,-6.764,61.815,-6.839,61.841,-6.864,61.862,-6.885,61.899,-6.842,61.904,-6.791,61.895,-6.662,61.862,-6.626,61.827,-6.623,61.806]],[[-6.699,61.445,-6.68,61.414,-6.703,61.418,-6.771,61.452,-6.888,61.535,-6.929,61.603,-6.935,61.634,-6.906,61.631,-6.882,61.603,-6.77,61.584,-6.741,61.571,-6.741,61.536,-6.704,61.496,-6.699,61.445]],[[-7.187,62.139,-7.097,62.101,-7.065,62.073,-7.117,62.047,-7.179,62.04,-7.255,62.046,-7.379,62.075,-7.423,62.14,-7.337,62.139,-7.235,62.151,-7.187,62.139]],[[-6.631,62.228,-6.656,62.094,-6.696,62.094,-6.769,62.131,-6.823,62.139,-6.841,62.119,-6.838,62.095,-6.809,62.08,-6.723,61.99,-6.714,61.964,-6.725,61.951,-6.81,61.977,-7.014,62.094,-7.172,62.286,-6.959,62.316,-6.804,62.266,-6.631,62.228]],[[-6.406,62.259,-6.454,62.187,-6.525,62.198,-6.544,62.206,-6.559,62.225,-6.552,62.278,-6.555,62.356,-6.473,62.292,-6.406,62.259]]]},{"n":"Denmark","c":"DK","r":4,"ml":3,"lx":9.02,"ly":55.97,"b":[8.121,54.629,15.137,57.737],"p":[[[12.569,55.785,12.571,55.685,12.545,55.656,12.507,55.637,12.407,55.616,12.321,55.588,12.243,55.538,12.215,55.467,12.275,55.414,12.385,55.386,12.413,55.286,12.322,55.237,12.09,55.188,12.066,55.07,12.073,54.977,12.069,54.909,12.05,54.815,11.862,54.773,11.741,54.915,11.74,54.972,11.704,55.039,11.697,55.096,11.654,55.187,11.476,55.212,11.407,55.215,11.31,55.198,11.286,55.204,11.171,55.329,11.19,55.466,11.128,55.535,11.12,55.566,11.121,55.601,11.07,55.629,11.009,55.644,10.979,55.722,11.05,55.74,11.224,55.731,11.275,55.736,11.322,55.753,11.464,55.879,11.46,55.907,11.475,55.943,11.628,55.957,11.696,55.908,11.682,55.829,11.691,55.729,11.784,55.702,11.82,55.698,11.858,55.772,11.885,55.808,11.922,55.828,11.935,55.896,11.913,55.937,11.866,55.968,12.04,56.052,12.219,56.119,12.323,56.122,12.428,56.106,12.526,56.083,12.579,56.064,12.608,56.033,12.543,55.959,12.525,55.918,12.569,55.785]],[[9.74,54.826,9.725,54.826,9.661,54.834,9.616,54.855,9.499,54.84,9.342,54.806,9.255,54.808,9.186,54.845,8.903,54.897,8.857,54.901,8.671,54.903,8.67,54.903,8.661,54.986,8.638,55.046,8.573,55.134,8.67,55.156,8.651,55.329,8.616,55.418,8.345,55.51,8.132,55.6,8.181,55.901,8.202,55.982,8.121,56.14,8.13,56.321,8.164,56.607,8.232,56.618,8.281,56.617,8.473,56.565,8.553,56.56,8.608,56.515,8.672,56.496,8.718,56.544,8.736,56.627,8.888,56.735,8.995,56.775,9.067,56.794,9.14,56.75,9.196,56.702,9.21,56.808,9.255,57.012,9.11,57.044,8.993,57.016,8.876,56.887,8.772,56.725,8.603,56.71,8.468,56.665,8.347,56.712,8.268,56.754,8.266,56.815,8.284,56.852,8.427,56.984,8.619,57.111,8.812,57.11,8.952,57.151,9.036,57.155,9.299,57.147,9.434,57.174,9.554,57.232,9.815,57.478,9.962,57.581,10.259,57.617,10.533,57.735,10.61,57.737,10.481,57.649,10.46,57.615,10.445,57.562,10.537,57.449,10.518,57.379,10.524,57.243,10.437,57.172,10.338,57.021,10.296,56.999,10.287,56.823,10.297,56.781,10.283,56.621,10.384,56.555,10.49,56.521,10.846,56.522,10.883,56.493,10.926,56.443,10.894,56.359,10.856,56.296,10.753,56.242,10.621,56.202,10.539,56.2,10.427,56.276,10.374,56.252,10.319,56.213,10.227,56.005,10.183,55.865,10.159,55.854,10.107,55.874,10.017,55.876,9.904,55.843,9.962,55.813,10.024,55.761,9.999,55.736,9.899,55.708,9.81,55.651,9.773,55.608,9.661,55.557,9.591,55.493,9.626,55.414,9.64,55.344,9.671,55.266,9.643,55.205,9.505,55.116,9.454,55.04,9.572,55.041,9.645,55.023,9.688,55,9.732,54.968,9.705,54.928,9.74,54.826]],[[10.645,55.61,10.687,55.558,10.738,55.446,10.819,55.322,10.785,55.27,10.808,55.203,10.785,55.133,10.624,55.052,10.443,55.049,10.255,55.088,9.989,55.163,9.967,55.205,9.93,55.229,9.859,55.357,9.861,55.515,9.994,55.535,10.286,55.611,10.354,55.599,10.424,55.56,10.505,55.558,10.623,55.613,10.645,55.61]],[[11.361,54.892,11.538,54.83,11.658,54.833,11.74,54.807,11.759,54.768,11.766,54.679,11.68,54.654,11.586,54.662,11.457,54.629,11.036,54.773,11.042,54.893,11.059,54.941,11.258,54.952,11.361,54.892]],[[10.734,54.751,10.69,54.745,10.629,54.826,10.622,54.851,10.692,54.903,10.738,54.962,10.857,55.052,10.925,55.158,10.951,55.156,10.921,55.062,10.765,54.8,10.734,54.751]],[[12.549,54.966,12.511,54.951,12.358,54.962,12.184,54.892,12.119,54.914,12.144,54.959,12.162,54.975,12.22,54.994,12.259,55.021,12.274,55.064,12.31,55.041,12.417,55.031,12.47,55.017,12.513,54.997,12.549,54.966]],[[12.666,55.597,12.572,55.554,12.551,55.556,12.52,55.615,12.57,55.65,12.599,55.68,12.62,55.679,12.648,55.647,12.666,55.597]],[[10.484,54.848,10.417,54.837,10.341,54.859,10.216,54.941,10.2,54.963,10.266,54.949,10.347,54.906,10.414,54.897,10.505,54.861,10.484,54.848]],[[10.061,54.886,9.957,54.872,9.904,54.897,9.806,54.906,9.771,55.06,9.781,55.069,9.83,55.058,9.999,54.986,10.058,54.908,10.061,54.886]],[[10.607,55.783,10.59,55.765,10.527,55.784,10.52,55.848,10.544,55.907,10.516,55.959,10.547,55.992,10.636,55.914,10.662,55.878,10.627,55.834,10.607,55.783]],[[11.052,57.253,11.011,57.229,10.874,57.262,10.935,57.309,11.086,57.33,11.175,57.323,11.077,57.277,11.052,57.253]],[[15.088,55.022,15.051,55.005,14.886,55.033,14.684,55.102,14.714,55.238,14.765,55.297,15.133,55.145,15.137,55.087,15.088,55.022]]]},{"n":"Czech Republic","c":"CZ","r":5,"ml":4,"lx":15.38,"ly":49.88,"b":[12.09,48.576,18.832,51.038],"p":[[[18.832,49.511,18.807,49.509,18.75,49.494,18.676,49.488,18.596,49.491,18.535,49.465,18.476,49.421,18.416,49.391,18.383,49.364,18.365,49.336,18.161,49.257,18.133,49.225,18.11,49.18,18.1,49.119,18.086,49.065,18.051,49.037,17.941,49.012,17.913,48.999,17.893,48.971,17.831,48.929,17.758,48.888,17.625,48.842,17.483,48.828,17.297,48.843,17.188,48.861,17.136,48.841,17.063,48.781,16.985,48.677,16.953,48.599,16.928,48.621,16.884,48.704,16.833,48.714,16.764,48.722,16.713,48.734,16.601,48.782,16.544,48.796,16.478,48.8,16.415,48.772,16.367,48.739,16.219,48.739,16.057,48.755,15.825,48.864,15.765,48.865,15.701,48.86,15.599,48.886,15.403,48.957,15.311,48.974,15.253,48.964,15.2,48.948,15.162,48.946,15.14,48.969,15.067,48.998,14.993,49.001,14.972,48.984,14.947,48.828,14.923,48.771,14.822,48.774,14.786,48.747,14.707,48.672,14.691,48.599,14.554,48.613,14.489,48.626,14.431,48.616,14.368,48.576,14.19,48.579,14.049,48.602,13.989,48.692,13.924,48.728,13.843,48.76,13.815,48.767,13.77,48.816,13.685,48.877,13.548,48.96,13.441,48.956,13.401,48.978,13.384,49.008,13.339,49.061,13.289,49.097,13.228,49.112,13.141,49.158,13.024,49.26,12.917,49.33,12.813,49.329,12.748,49.366,12.681,49.415,12.632,49.461,12.556,49.575,12.5,49.64,12.457,49.68,12.408,49.713,12.391,49.74,12.45,49.8,12.472,49.83,12.498,49.853,12.513,49.877,12.512,49.896,12.458,49.956,12.384,49.999,12.276,50.042,12.208,50.098,12.183,50.148,12.175,50.176,12.128,50.213,12.09,50.269,12.09,50.302,12.099,50.311,12.135,50.311,12.175,50.288,12.231,50.245,12.277,50.181,12.306,50.206,12.359,50.273,12.453,50.35,12.549,50.393,12.636,50.397,12.706,50.409,12.765,50.431,12.868,50.422,12.943,50.406,12.967,50.416,12.997,50.456,13.016,50.49,13.181,50.51,13.238,50.577,13.27,50.576,13.306,50.586,13.341,50.611,13.375,50.622,13.401,50.609,13.436,50.601,13.473,50.617,13.527,50.693,13.557,50.705,13.701,50.717,13.899,50.761,13.998,50.801,14.096,50.823,14.202,50.861,14.369,50.899,14.377,50.914,14.299,50.953,14.273,50.977,14.256,51.002,14.283,51.029,14.32,51.038,14.367,51.026,14.507,51.01,14.546,50.994,14.56,50.955,14.595,50.919,14.624,50.915,14.614,50.856,14.658,50.833,14.723,50.815,14.767,50.818,14.797,50.842,14.809,50.859,14.896,50.861,14.983,50.887,14.99,50.927,14.984,51.003,14.994,51.014,15.126,50.993,15.259,50.959,15.277,50.883,15.313,50.846,15.354,50.812,15.395,50.796,15.464,50.794,15.644,50.749,15.731,50.74,15.819,50.709,15.894,50.677,15.949,50.67,15.974,50.635,16.007,50.612,16.066,50.63,16.282,50.656,16.36,50.621,16.413,50.585,16.42,50.574,16.392,50.542,16.379,50.517,16.357,50.5,16.283,50.483,16.241,50.455,16.21,50.424,16.231,50.394,16.291,50.372,16.334,50.367,16.35,50.345,16.488,50.248,16.597,50.122,16.639,50.102,16.679,50.097,16.725,50.116,16.779,50.157,16.842,50.187,16.895,50.202,16.99,50.237,16.993,50.26,16.915,50.345,16.869,50.415,16.88,50.427,16.981,50.416,17.152,50.378,17.415,50.255,17.462,50.255,17.555,50.264,17.655,50.284,17.702,50.307,17.72,50.299,17.735,50.231,17.709,50.194,17.589,50.157,17.596,50.14,17.627,50.116,17.681,50.101,17.747,50.057,17.792,50.007,17.831,49.983,17.875,49.972,17.984,49.999,18.015,50.02,18.028,50.035,18.05,50.032,18.088,50.007,18.099,49.993,18.205,49.965,18.266,49.93,18.305,49.914,18.348,49.93,18.516,49.902,18.562,49.879,18.577,49.841,18.569,49.818,18.595,49.758,18.807,49.614,18.829,49.54,18.832,49.511]]]},{"n":"Turkish Republic of Northern Cyprus","c":"","r":6,"ml":6,"lx":33.69,"ly":35.22,"b":[32.713,35,34.556,35.662],"p":[[[34.004,35.065,33.966,35.057,33.903,35.085,33.866,35.094,33.832,35.067,33.792,35.048,33.757,35.04,33.726,35.037,33.675,35.018,33.614,35.023,33.526,35.039,33.476,35,33.464,35.005,33.456,35.101,33.424,35.141,33.384,35.163,33.326,35.154,33.248,35.157,33.191,35.173,33.078,35.146,32.986,35.116,32.92,35.088,32.869,35.089,32.784,35.116,32.72,35.145,32.713,35.171,32.772,35.16,32.88,35.181,32.926,35.278,32.942,35.39,33.123,35.358,33.308,35.342,33.459,35.336,33.608,35.354,34.063,35.474,34.192,35.546,34.272,35.57,34.411,35.629,34.556,35.662,34.463,35.594,33.942,35.292,33.908,35.202,33.931,35.14,34.004,35.065]]]},{"n":"Cyprus","c":"CY","r":5,"ml":4.5,"lx":33.08,"ly":34.91,"b":[32.301,34.57,34.05,35.183],"p":[[[32.713,35.171,32.72,35.145,32.784,35.116,32.869,35.089,32.92,35.088,32.986,35.116,33.078,35.146,33.191,35.173,33.248,35.157,33.326,35.154,33.384,35.163,33.424,35.141,33.456,35.101,33.464,35.005,33.476,35,33.526,35.039,33.614,35.023,33.675,35.018,33.726,35.037,33.757,35.04,33.792,35.048,33.832,35.067,33.866,35.094,33.903,35.085,33.966,35.057,34.004,35.065,34.024,35.046,34.05,34.988,33.937,34.971,33.822,34.966,33.759,34.973,33.699,34.97,33.514,34.806,33.415,34.751,33.297,34.718,33.176,34.698,33.116,34.696,33.062,34.675,33.025,34.637,33.024,34.6,33.008,34.57,32.942,34.576,32.914,34.635,32.867,34.661,32.75,34.648,32.693,34.649,32.506,34.706,32.449,34.729,32.414,34.778,32.317,34.953,32.301,35.083,32.391,35.05,32.475,35.09,32.556,35.156,32.652,35.183,32.713,35.171]]]},{"n":"Cuba","c":"CU","r":3,"ml":2.7,"lx":-77.98,"ly":21.33,"b":[-84.887,19.855,-74.137,23.19],"p":[[[-81.837,23.163,-81.575,23.117,-81.364,23.13,-81.262,23.157,-81.272,23.129,-81.179,23.06,-81.145,23.055,-81.008,23.09,-80.65,23.103,-80.613,23.084,-80.55,23.017,-80.459,22.975,-80.365,22.943,-80.266,22.935,-80.168,22.949,-80.075,22.942,-79.96,22.877,-79.924,22.869,-79.82,22.887,-79.851,22.827,-79.677,22.743,-79.549,22.578,-79.456,22.51,-79.358,22.449,-79.276,22.408,-79.183,22.388,-78.902,22.396,-78.835,22.391,-78.776,22.367,-78.719,22.358,-78.686,22.367,-78.143,22.109,-77.971,21.972,-77.865,21.901,-77.637,21.797,-77.545,21.775,-77.497,21.788,-77.507,21.811,-77.573,21.868,-77.583,21.889,-77.497,21.872,-77.342,21.755,-77.3,21.712,-77.222,21.672,-77.144,21.644,-77.181,21.598,-77.245,21.594,-77.366,21.613,-77.27,21.538,-77.253,21.483,-77.208,21.479,-77.141,21.539,-77.099,21.589,-76.928,21.459,-76.836,21.4,-76.86,21.365,-76.867,21.33,-76.765,21.362,-76.726,21.359,-76.689,21.34,-76.647,21.285,-76.552,21.272,-76.455,21.274,-76.259,21.227,-76.074,21.133,-75.899,21.114,-75.723,21.111,-75.634,21.061,-75.596,20.995,-75.639,20.947,-75.663,20.898,-75.597,20.838,-75.74,20.812,-75.76,20.776,-75.753,20.736,-75.725,20.715,-75.643,20.733,-75.525,20.717,-75.338,20.702,-75.213,20.714,-74.96,20.673,-74.883,20.651,-74.732,20.573,-74.662,20.522,-74.513,20.385,-74.384,20.33,-74.273,20.317,-74.234,20.326,-74.198,20.311,-74.167,20.292,-74.137,20.232,-74.154,20.169,-74.217,20.117,-74.253,20.08,-74.412,20.075,-74.635,20.058,-74.85,20.002,-74.955,19.958,-75.003,19.929,-75.116,19.901,-75.124,19.925,-75.122,19.954,-75.152,20.008,-75.177,19.959,-75.219,19.924,-75.29,19.893,-75.552,19.891,-75.657,19.932,-75.765,19.96,-76.158,19.99,-76.253,19.987,-76.516,19.957,-76.78,19.94,-76.89,19.921,-76.999,19.893,-77.212,19.894,-77.463,19.861,-77.715,19.855,-77.554,20.082,-77.213,20.3,-77.149,20.347,-77.104,20.408,-77.093,20.453,-77.108,20.492,-77.189,20.56,-77.205,20.611,-77.23,20.644,-77.348,20.672,-77.467,20.69,-77.593,20.69,-77.857,20.714,-77.997,20.715,-78.116,20.762,-78.314,20.927,-78.406,20.974,-78.454,21.011,-78.491,21.054,-78.537,21.297,-78.577,21.414,-78.636,21.516,-78.728,21.593,-78.823,21.619,-79.189,21.553,-79.274,21.563,-79.357,21.585,-79.91,21.743,-80.138,21.829,-80.231,21.872,-80.311,21.933,-80.393,22.034,-80.485,22.123,-80.485,22.087,-80.499,22.064,-80.962,22.053,-81.036,22.074,-81.083,22.098,-81.117,22.134,-81.141,22.207,-81.185,22.268,-81.2,22.203,-81.222,22.143,-81.284,22.109,-81.355,22.104,-81.441,22.184,-81.816,22.2,-81.849,22.214,-81.973,22.291,-82.078,22.388,-81.973,22.422,-81.757,22.467,-81.71,22.497,-81.683,22.535,-81.703,22.592,-81.746,22.633,-81.79,22.657,-81.839,22.672,-81.903,22.679,-82.738,22.689,-82.786,22.658,-82.861,22.595,-83.009,22.514,-83.107,22.43,-83.144,22.386,-83.189,22.355,-83.292,22.303,-83.38,22.223,-83.486,22.187,-83.544,22.209,-83.602,22.209,-83.643,22.189,-83.687,22.18,-83.901,22.17,-83.933,22.15,-83.963,22.092,-83.998,21.98,-84.031,21.943,-84.138,21.929,-84.241,21.898,-84.449,21.792,-84.503,21.776,-84.491,21.854,-84.501,21.93,-84.56,21.933,-84.627,21.92,-84.683,21.899,-84.786,21.842,-84.838,21.828,-84.887,21.857,-84.877,21.894,-84.533,22.031,-84.494,22.042,-84.433,22.031,-84.373,22.036,-84.326,22.074,-84.383,22.256,-84.361,22.379,-84.281,22.474,-84.122,22.619,-84.045,22.666,-83.258,22.968,-83.177,22.983,-82.666,23.044,-82.588,23.065,-82.351,23.154,-82.101,23.19,-81.837,23.163]],[[-77.669,21.952,-77.71,21.921,-77.755,21.966,-77.784,21.97,-77.823,21.988,-77.9,22.037,-77.919,22.088,-77.855,22.092,-77.774,22.083,-77.634,22.054,-77.646,21.996,-77.669,21.952]],[[-78.027,22.285,-78.048,22.269,-78.102,22.306,-78.18,22.322,-78.226,22.38,-78.27,22.402,-78.274,22.424,-78.201,22.438,-78.151,22.431,-78.094,22.387,-78.062,22.306,-78.027,22.285]],[[-78.63,22.552,-78.493,22.531,-78.445,22.544,-78.4,22.547,-78.351,22.539,-78.284,22.455,-78.343,22.445,-78.39,22.445,-78.425,22.46,-78.548,22.464,-78.629,22.488,-78.674,22.509,-78.696,22.534,-78.63,22.552]],[[-77.879,22.128,-77.912,22.125,-78.012,22.166,-78.042,22.201,-78.007,22.248,-77.999,22.299,-77.986,22.302,-77.97,22.241,-77.894,22.215,-77.889,22.201,-77.842,22.149,-77.879,22.128]],[[-82.562,21.572,-82.655,21.519,-82.853,21.444,-82.96,21.441,-83.067,21.469,-83.142,21.532,-83.184,21.593,-83.18,21.623,-83.113,21.574,-83.055,21.549,-83.007,21.566,-82.974,21.592,-83.083,21.791,-83.078,21.833,-82.991,21.943,-82.756,21.91,-82.715,21.89,-82.682,21.821,-82.629,21.767,-82.568,21.622,-82.562,21.572]],[[-79.35,22.664,-79.348,22.638,-79.523,22.711,-79.598,22.788,-79.628,22.805,-79.579,22.807,-79.382,22.681,-79.35,22.664]]]},{"n":"Croatia","c":"HR","r":6,"ml":4,"lx":16.37,"ly":45.81,"b":[13.517,42.433,19.401,46.535],"p":[[[13.578,45.517,13.615,45.477,13.879,45.428,13.936,45.45,13.97,45.483,13.97,45.503,13.993,45.509,14.086,45.478,14.161,45.485,14.283,45.487,14.37,45.481,14.427,45.506,14.505,45.595,14.534,45.645,14.569,45.657,14.592,45.651,14.609,45.61,14.65,45.571,14.734,45.508,14.793,45.478,14.847,45.467,14.9,45.493,14.955,45.5,15.11,45.451,15.242,45.441,15.339,45.467,15.327,45.502,15.291,45.542,15.284,45.58,15.29,45.613,15.357,45.646,15.354,45.66,15.273,45.718,15.277,45.733,15.454,45.798,15.625,45.834,15.652,45.862,15.668,45.904,15.676,45.984,15.666,46.048,15.597,46.109,15.593,46.14,15.609,46.172,15.636,46.201,15.704,46.213,15.784,46.234,15.848,46.258,15.933,46.278,16.001,46.305,16.067,46.371,16.106,46.382,16.227,46.373,16.253,46.389,16.237,46.484,16.258,46.508,16.301,46.521,16.321,46.535,16.428,46.524,16.516,46.5,16.57,46.485,16.748,46.416,16.871,46.339,16.94,46.254,17.033,46.187,17.15,46.14,17.242,46.077,17.311,45.996,17.406,45.951,17.529,45.941,17.607,45.914,17.64,45.868,17.706,45.827,17.807,45.79,17.964,45.77,18.264,45.765,18.291,45.764,18.358,45.753,18.437,45.767,18.534,45.796,18.565,45.813,18.666,45.907,18.722,45.899,18.833,45.911,18.9,45.932,18.905,45.932,18.901,45.908,18.894,45.866,18.839,45.836,18.895,45.767,18.947,45.656,18.918,45.601,18.954,45.558,19.055,45.527,19.064,45.515,19.033,45.502,19.008,45.466,19.005,45.4,19.093,45.337,19.273,45.278,19.33,45.268,19.352,45.245,19.382,45.231,19.4,45.213,19.401,45.189,19.388,45.173,19.303,45.167,19.206,45.168,19.137,45.196,19.131,45.175,19.13,45.152,19.063,45.137,19.1,44.974,19.085,44.927,19.061,44.911,19.038,44.918,19.01,44.919,18.996,44.904,19.007,44.869,18.941,44.865,18.836,44.883,18.788,44.915,18.78,44.947,18.779,44.977,18.746,45.027,18.663,45.077,18.488,45.086,18.424,45.102,18.358,45.121,18.285,45.134,18.218,45.133,18.137,45.119,17.996,45.142,17.949,45.112,17.874,45.077,17.813,45.078,17.69,45.158,17.654,45.163,17.546,45.123,17.503,45.12,17.469,45.133,17.324,45.164,17.259,45.171,17.211,45.156,17.125,45.172,16.919,45.277,16.791,45.197,16.531,45.217,16.454,45.162,16.365,45.058,16.293,45.009,16.231,45.027,16.157,45.072,16.028,45.19,15.963,45.211,15.888,45.216,15.823,45.203,15.788,45.179,15.762,45.008,15.738,44.856,15.737,44.766,15.88,44.682,16.049,44.538,16.103,44.521,16.13,44.474,16.17,44.352,16.214,44.215,16.3,44.125,16.378,44.06,16.472,44.003,16.591,43.913,16.688,43.815,16.713,43.779,16.902,43.649,17.085,43.517,17.248,43.47,17.274,43.446,17.275,43.344,17.293,43.306,17.402,43.199,17.625,43.043,17.65,43.007,17.658,42.98,17.643,42.96,17.585,42.938,17.537,42.962,17.33,43.115,17.129,43.211,16.903,43.392,16.6,43.464,16.394,43.543,16.269,43.531,16.131,43.506,16.046,43.506,15.986,43.52,15.943,43.569,15.949,43.607,15.942,43.657,15.821,43.736,15.656,43.811,15.499,43.909,15.186,44.172,15.123,44.257,15.185,44.273,15.231,44.271,15.284,44.289,15.37,44.289,15.471,44.272,15.381,44.328,15.27,44.383,14.981,44.603,14.895,44.707,14.885,44.818,14.907,44.971,14.855,45.081,14.632,45.223,14.55,45.298,14.386,45.342,14.313,45.338,14.269,45.283,14.236,45.16,14.091,44.998,14.042,44.927,13.966,44.836,13.9,44.829,13.861,44.837,13.742,44.992,13.629,45.108,13.613,45.163,13.603,45.231,13.517,45.482,13.578,45.517]],[[16.651,42.997,16.836,42.969,16.971,42.981,17.094,42.964,17.17,42.933,17.188,42.917,17.089,42.915,16.978,42.928,16.851,42.896,16.739,42.913,16.696,42.934,16.666,42.96,16.651,42.997]],[[17.194,43.126,17.124,43.115,16.679,43.123,16.55,43.144,16.406,43.197,16.376,43.214,16.521,43.229,16.656,43.214,16.697,43.175,17.061,43.144,17.194,43.126]],[[16.785,43.271,16.627,43.268,16.49,43.286,16.423,43.317,16.428,43.343,16.449,43.387,16.602,43.382,16.834,43.351,16.891,43.315,16.874,43.298,16.785,43.271]],[[15.231,44.062,15.247,44.027,15.122,44.093,15.075,44.138,15.066,44.158,15.231,44.062]],[[14.81,44.977,14.687,44.956,14.628,44.994,14.613,45.025,14.512,45.035,14.45,45.079,14.438,45.099,14.525,45.147,14.571,45.225,14.63,45.178,14.701,45.09,14.739,45.065,14.81,44.977]],[[14.831,44.759,14.857,44.715,14.763,44.755,14.678,44.77,14.66,44.8,14.672,44.824,14.691,44.848,14.754,44.845,14.764,44.821,14.831,44.759]],[[15.188,44.336,15.163,44.309,15.098,44.358,15.039,44.393,14.996,44.434,14.913,44.486,14.885,44.545,14.76,44.665,14.742,44.697,14.804,44.649,14.855,44.618,14.898,44.611,15.006,44.534,15.113,44.436,15.24,44.35,15.214,44.348,15.188,44.336]],[[15.189,43.922,15.203,43.908,15.202,43.898,15.15,43.912,15.136,43.907,14.891,44.126,14.865,44.168,14.953,44.117,15.189,43.922]],[[15.371,43.974,15.437,43.9,15.374,43.915,15.309,43.961,15.27,44.011,15.371,43.974]],[[14.488,44.66,14.48,44.621,14.42,44.67,14.389,44.758,14.312,44.9,14.303,44.94,14.342,44.98,14.34,45.02,14.286,45.145,14.331,45.165,14.358,45.167,14.369,45.081,14.394,45.031,14.467,44.97,14.453,44.869,14.468,44.725,14.483,44.693,14.488,44.66]],[[17.608,42.769,17.744,42.7,17.344,42.79,17.39,42.799,17.432,42.8,17.608,42.769]],[[18.436,42.56,18.438,42.523,18.477,42.481,18.517,42.433,18.333,42.528,18.161,42.634,17.824,42.797,17.585,42.837,17.258,42.968,17.045,43.015,17.126,43.026,17.22,43.026,17.724,42.851,17.668,42.897,17.74,42.915,17.802,42.902,17.841,42.845,17.919,42.807,18.045,42.741,18.124,42.691,18.304,42.599,18.347,42.587,18.436,42.56]]]},{"n":"Ivory Coast","c":"CI","r":3,"ml":2.5,"lx":-5.57,"ly":7.49,"b":[-8.604,4.351,-2.506,10.724],"p":[[[-3.087,5.128,-3.114,5.089,-3.246,5.114,-3.215,5.147,-3.087,5.128]],[[-7.991,10.163,-7.961,10.163,-7.884,10.186,-7.814,10.237,-7.749,10.342,-7.661,10.427,-7.562,10.421,-7.533,10.437,-7.498,10.44,-7.457,10.384,-7.415,10.341,-7.385,10.34,-7.363,10.259,-7.182,10.226,-7.105,10.204,-7.04,10.145,-7.017,10.143,-6.989,10.156,-6.968,10.176,-6.964,10.199,-6.992,10.252,-6.979,10.3,-6.95,10.342,-6.904,10.345,-6.834,10.357,-6.753,10.357,-6.693,10.349,-6.669,10.392,-6.692,10.512,-6.686,10.578,-6.676,10.634,-6.654,10.656,-6.565,10.586,-6.483,10.561,-6.424,10.559,-6.408,10.572,-6.433,10.649,-6.426,10.672,-6.404,10.685,-6.366,10.693,-6.261,10.724,-6.25,10.718,-6.231,10.598,-6.24,10.558,-6.218,10.476,-6.191,10.4,-6.193,10.369,-6.215,10.322,-6.241,10.279,-6.238,10.262,-6.197,10.232,-6.117,10.202,-6.035,10.195,-5.989,10.239,-5.941,10.275,-5.908,10.307,-5.896,10.355,-5.844,10.39,-5.694,10.433,-5.557,10.44,-5.524,10.426,-5.461,10.36,-5.382,10.314,-5.262,10.32,-5.175,10.293,-5.1,10.242,-5.049,10.128,-4.994,10.046,-4.97,9.93,-4.883,9.869,-4.814,9.841,-4.722,9.757,-4.626,9.714,-4.527,9.723,-4.48,9.679,-4.406,9.648,-4.332,9.646,-4.267,9.743,-4.181,9.782,-3.963,9.86,-3.878,9.895,-3.791,9.917,-3.581,9.924,-3.386,9.9,-3.29,9.882,-3.224,9.895,-3.161,9.849,-3.096,9.752,-3.043,9.721,-2.988,9.687,-2.948,9.611,-2.901,9.535,-2.875,9.501,-2.817,9.426,-2.767,9.425,-2.717,9.457,-2.696,9.481,-2.686,9.432,-2.706,9.351,-2.702,9.302,-2.674,9.283,-2.689,9.219,-2.747,9.11,-2.747,9.045,-2.69,9.025,-2.649,8.957,-2.625,8.84,-2.6,8.8,-2.598,8.776,-2.557,8.493,-2.506,8.209,-2.538,8.172,-2.583,8.161,-2.612,8.148,-2.62,8.121,-2.601,8.082,-2.613,8.047,-2.669,8.022,-2.79,7.932,-2.798,7.896,-2.83,7.819,-2.857,7.772,-2.896,7.685,-2.959,7.455,-2.982,7.264,-2.986,7.205,-3.01,7.164,-3.038,7.105,-3.169,6.941,-3.236,6.807,-3.227,6.749,-3.224,6.691,-3.244,6.649,-3.24,6.536,-3.224,6.441,-3.201,6.348,-3.106,6.086,-3.056,5.926,-3.025,5.798,-2.998,5.711,-2.973,5.676,-2.962,5.643,-2.821,5.619,-2.794,5.6,-2.755,5.433,-2.762,5.357,-2.79,5.328,-2.789,5.264,-2.795,5.185,-2.816,5.153,-2.895,5.149,-2.948,5.119,-3.019,5.131,-3.026,5.151,-3.064,5.158,-3.169,5.203,-3.151,5.348,-3.2,5.354,-3.238,5.335,-3.312,5.161,-3.348,5.131,-3.871,5.221,-3.984,5.293,-4.12,5.31,-4.357,5.301,-4.553,5.28,-4.609,5.236,-4.115,5.262,-4.062,5.257,-4.037,5.23,-4.662,5.173,-4.9,5.138,-4.97,5.148,-5.024,5.204,-5.282,5.21,-5.335,5.192,-5.368,5.151,-5.266,5.16,-5.105,5.162,-5.062,5.131,-5.565,5.089,-5.914,5.011,-6.062,4.953,-6.548,4.762,-6.845,4.671,-6.923,4.638,-7.058,4.545,-7.231,4.486,-7.426,4.376,-7.545,4.351,-7.572,4.386,-7.575,4.572,-7.591,4.822,-7.585,4.917,-7.569,5.006,-7.569,5.081,-7.51,5.108,-7.494,5.14,-7.485,5.236,-7.43,5.325,-7.429,5.478,-7.412,5.51,-7.4,5.551,-7.424,5.651,-7.454,5.841,-7.469,5.854,-7.483,5.846,-7.514,5.842,-7.636,5.908,-7.73,5.919,-7.797,5.975,-7.801,6.039,-7.833,6.076,-7.856,6.15,-7.889,6.235,-7.982,6.286,-8.069,6.298,-8.131,6.288,-8.204,6.291,-8.287,6.319,-8.345,6.351,-8.399,6.413,-8.45,6.463,-8.49,6.456,-8.54,6.468,-8.588,6.491,-8.604,6.508,-8.401,6.705,-8.333,6.802,-8.325,6.86,-8.325,6.92,-8.302,6.981,-8.297,7.074,-8.409,7.412,-8.437,7.516,-8.467,7.547,-8.486,7.558,-8.43,7.602,-8.352,7.591,-8.232,7.557,-8.206,7.59,-8.115,7.761,-8.118,7.824,-8.127,7.868,-8.074,7.984,-8.032,8.03,-8.01,8.079,-8.017,8.145,-8.049,8.17,-8.091,8.165,-8.141,8.181,-8.217,8.22,-8.256,8.254,-8.244,8.408,-8.237,8.456,-8.21,8.483,-8.168,8.491,-8.049,8.495,-7.953,8.478,-7.869,8.468,-7.824,8.468,-7.787,8.422,-7.739,8.375,-7.696,8.376,-7.681,8.41,-7.691,8.563,-7.72,8.643,-7.784,8.721,-7.951,8.787,-7.955,8.879,-7.938,8.98,-7.902,9.017,-7.778,9.081,-7.8,9.115,-7.839,9.152,-7.918,9.189,-7.9,9.309,-7.896,9.416,-7.963,9.404,-8.031,9.398,-8.089,9.431,-8.137,9.496,-8.146,9.675,-8.146,9.882,-8.155,9.973,-8.137,10.022,-8.078,10.067,-8.014,10.125,-7.991,10.163]]]},{"n":"Costa Rica","c":"CR","r":5,"ml":2.5,"lx":-84.08,"ly":10.07,"b":[-85.908,8.071,-82.564,11.189],"p":[[[-82.564,9.577,-82.569,9.558,-82.587,9.539,-82.611,9.519,-82.644,9.506,-82.723,9.546,-82.801,9.592,-82.844,9.571,-82.86,9.511,-82.889,9.481,-82.925,9.469,-82.94,9.449,-82.943,9.249,-82.94,9.06,-82.881,9.056,-82.783,8.99,-82.741,8.952,-82.728,8.916,-82.74,8.899,-82.812,8.857,-82.882,8.805,-82.917,8.74,-82.856,8.635,-82.843,8.564,-82.845,8.489,-82.862,8.454,-82.998,8.368,-83.027,8.338,-83.023,8.316,-82.948,8.257,-82.913,8.2,-82.883,8.131,-82.879,8.071,-82.947,8.182,-83.041,8.288,-83.123,8.353,-83.13,8.505,-83.162,8.588,-83.286,8.664,-83.391,8.718,-83.47,8.707,-83.422,8.619,-83.298,8.507,-83.29,8.464,-83.292,8.406,-83.377,8.415,-83.452,8.438,-83.544,8.446,-83.605,8.48,-83.734,8.614,-83.642,8.729,-83.614,8.804,-83.616,8.96,-83.637,9.035,-83.737,9.15,-83.896,9.276,-84.118,9.379,-84.222,9.463,-84.483,9.526,-84.582,9.568,-84.659,9.647,-84.67,9.703,-84.643,9.789,-84.715,9.899,-85.025,10.116,-85.198,10.195,-85.236,10.242,-85.263,10.257,-85.237,10.107,-85.161,10.017,-84.963,9.933,-84.908,9.885,-84.886,9.821,-85.001,9.699,-85.06,9.668,-85.077,9.602,-85.115,9.582,-85.154,9.62,-85.315,9.811,-85.625,9.902,-85.681,9.959,-85.796,10.133,-85.85,10.292,-85.831,10.398,-85.703,10.563,-85.663,10.635,-85.671,10.68,-85.667,10.745,-85.715,10.791,-85.833,10.85,-85.908,10.898,-85.887,10.921,-85.752,10.985,-85.744,11.043,-85.744,11.062,-85.722,11.066,-85.703,11.082,-85.691,11.097,-85.654,11.153,-85.621,11.184,-85.584,11.189,-85.539,11.166,-85.368,11.106,-85.179,11.04,-84.909,10.945,-84.797,11.006,-84.701,11.052,-84.634,11.046,-84.489,10.992,-84.402,10.974,-84.348,10.98,-84.256,10.901,-84.205,10.841,-84.197,10.802,-84.168,10.78,-84.096,10.776,-83.919,10.735,-83.811,10.743,-83.713,10.786,-83.659,10.837,-83.642,10.917,-83.617,10.877,-83.588,10.815,-83.575,10.735,-83.448,10.466,-83.347,10.315,-83.125,10.042,-83.029,9.991,-82.866,9.771,-82.81,9.735,-82.778,9.67,-82.61,9.616,-82.564,9.577]]]},{"n":"Democratic Republic of the Congo","c":"CD","r":2,"ml":2,"lx":23.46,"ly":-1.86,"b":[12.214,-13.454,31.274,5.312],"p":[[[30.751,-8.194,30.578,-8.22,30.328,-8.258,30.051,-8.3,29.766,-8.344,29.484,-8.387,29.216,-8.428,28.972,-8.465,28.898,-8.485,28.934,-8.59,28.918,-8.701,28.87,-8.786,28.794,-8.891,28.759,-8.933,28.681,-9.015,28.617,-9.072,28.484,-9.169,28.401,-9.225,28.4,-9.275,28.541,-9.51,28.604,-9.679,28.63,-9.831,28.629,-9.919,28.624,-10.099,28.617,-10.313,28.607,-10.397,28.646,-10.55,28.639,-10.669,28.544,-10.802,28.518,-10.933,28.47,-11.11,28.404,-11.354,28.357,-11.483,28.383,-11.567,28.407,-11.623,28.432,-11.698,28.483,-11.812,28.542,-11.879,28.575,-11.908,28.769,-12.051,28.85,-12.121,28.973,-12.258,29.064,-12.349,29.191,-12.37,29.344,-12.405,29.428,-12.431,29.486,-12.418,29.505,-12.386,29.502,-12.318,29.492,-12.267,29.508,-12.228,29.56,-12.202,29.692,-12.198,29.75,-12.164,29.795,-12.155,29.795,-12.306,29.796,-12.451,29.796,-12.626,29.796,-12.827,29.796,-12.992,29.796,-13.167,29.796,-13.37,29.795,-13.393,29.775,-13.438,29.723,-13.454,29.652,-13.414,29.648,-13.373,29.63,-13.299,29.597,-13.261,29.554,-13.249,29.481,-13.268,29.382,-13.323,29.254,-13.371,29.202,-13.398,29.112,-13.395,29.014,-13.369,28.942,-13.307,28.922,-13.215,28.859,-13.119,28.773,-12.982,28.73,-12.925,28.673,-12.861,28.615,-12.854,28.551,-12.836,28.511,-12.742,28.474,-12.623,28.451,-12.577,28.413,-12.518,28.358,-12.482,28.237,-12.435,28.069,-12.368,27.857,-12.285,27.757,-12.281,27.644,-12.267,27.574,-12.227,27.533,-12.195,27.487,-12.08,27.424,-11.945,27.238,-11.783,27.196,-11.605,27.159,-11.579,27.095,-11.594,27.046,-11.616,27.027,-11.664,26.977,-11.825,26.95,-11.899,26.931,-11.919,26.89,-11.944,26.824,-11.965,26.73,-11.976,26.596,-11.972,26.43,-11.948,26.34,-11.93,26.096,-11.903,26.026,-11.89,25.927,-11.855,25.855,-11.82,25.619,-11.744,25.512,-11.753,25.46,-11.7,25.413,-11.674,25.349,-11.623,25.321,-11.554,25.283,-11.405,25.292,-11.325,25.319,-11.237,25.289,-11.212,25.246,-11.212,25.185,-11.243,25.076,-11.26,24.877,-11.299,24.806,-11.321,24.728,-11.338,24.668,-11.353,24.519,-11.438,24.467,-11.448,24.378,-11.417,24.335,-11.371,24.378,-11.319,24.396,-11.255,24.366,-11.13,24.32,-11.072,24.187,-11.03,24.137,-11.026,24.115,-10.956,24.078,-10.892,24.003,-10.879,23.967,-10.872,23.929,-10.892,23.907,-10.943,23.901,-10.983,23.834,-11.014,23.696,-11.008,23.56,-10.979,23.464,-10.969,23.4,-10.976,23.157,-11.075,23.076,-11.088,22.815,-11.08,22.667,-11.06,22.561,-11.056,22.486,-11.087,22.393,-11.159,22.315,-11.199,22.279,-11.194,22.257,-11.164,22.226,-11.122,22.217,-11.013,22.178,-10.892,22.204,-10.829,22.28,-10.784,22.307,-10.691,22.283,-10.552,22.282,-10.453,22.302,-10.397,22.275,-10.259,22.198,-10.041,22.089,-9.863,21.949,-9.726,21.857,-9.594,21.813,-9.469,21.829,-9.168,21.872,-8.904,21.905,-8.693,21.896,-8.341,21.801,-8.112,21.78,-7.865,21.834,-7.602,21.842,-7.421,21.806,-7.329,21.782,-7.315,21.751,-7.305,21.511,-7.297,21.19,-7.285,20.911,-7.281,20.608,-7.278,20.558,-7.244,20.536,-7.183,20.537,-7.122,20.599,-6.935,20.59,-6.92,20.482,-6.916,20.19,-6.946,19.997,-6.976,19.875,-6.986,19.66,-7.037,19.528,-7.144,19.484,-7.279,19.487,-7.391,19.48,-7.472,19.419,-7.557,19.372,-7.655,19.37,-7.707,19.341,-7.967,19.143,-8.001,18.944,-8.001,18.898,-7.998,18.653,-7.936,18.563,-7.936,18.485,-7.969,18.335,-8,18.192,-8.024,18.047,-8.101,18.009,-8.108,17.913,-8.068,17.779,-8.071,17.643,-8.091,17.58,-8.099,17.536,-8.076,17.411,-7.882,17.245,-7.623,17.155,-7.461,17.122,-7.419,17.064,-7.363,16.985,-7.257,16.952,-7.157,16.966,-7.062,16.919,-6.934,16.813,-6.773,16.743,-6.618,16.709,-6.472,16.701,-6.346,16.718,-6.241,16.697,-6.164,16.64,-6.115,16.608,-6.052,16.585,-6.025,16.537,-5.966,16.431,-5.9,16.315,-5.866,16.06,-5.865,15.727,-5.864,15.425,-5.869,15.089,-5.875,14.749,-5.88,14.658,-5.889,14.399,-5.893,14.191,-5.876,14.114,-5.865,13.979,-5.857,13.765,-5.855,13.649,-5.862,13.371,-5.862,13.346,-5.863,13.303,-5.882,13.184,-5.856,13.068,-5.865,13.003,-5.836,12.861,-5.854,12.792,-5.878,12.681,-5.961,12.515,-6.004,12.453,-6,12.412,-5.986,12.315,-5.895,12.24,-5.807,12.214,-5.759,12.255,-5.746,12.386,-5.728,12.485,-5.719,12.504,-5.696,12.519,-5.425,12.522,-5.149,12.487,-5.113,12.453,-5.091,12.451,-5.071,12.503,-5.037,12.574,-4.997,12.596,-4.978,12.675,-4.905,12.83,-4.737,12.947,-4.695,13.057,-4.651,13.073,-4.635,13.087,-4.602,13.137,-4.604,13.152,-4.62,13.176,-4.656,13.22,-4.706,13.297,-4.765,13.376,-4.829,13.415,-4.837,13.478,-4.805,13.552,-4.757,13.66,-4.721,13.685,-4.689,13.699,-4.618,13.708,-4.543,13.717,-4.454,13.739,-4.442,13.778,-4.434,13.85,-4.459,13.882,-4.485,13.941,-4.485,13.978,-4.461,14.047,-4.417,14.134,-4.4,14.227,-4.358,14.316,-4.304,14.358,-4.299,14.403,-4.37,14.443,-4.419,14.45,-4.45,14.41,-4.508,14.365,-4.586,14.403,-4.682,14.412,-4.775,14.411,-4.831,14.441,-4.854,14.462,-4.865,14.494,-4.852,14.558,-4.856,14.634,-4.885,14.708,-4.882,14.779,-4.846,14.912,-4.706,15.106,-4.461,15.267,-4.308,15.395,-4.245,15.481,-4.172,15.526,-4.088,15.6,-4.031,15.755,-3.986,15.872,-3.934,15.99,-3.766,16.147,-3.464,16.191,-3.194,16.217,-3.03,16.202,-2.465,16.192,-2.279,16.215,-2.178,16.274,-2.108,16.434,-1.961,16.541,-1.84,16.622,-1.699,16.78,-1.376,16.849,-1.272,16.88,-1.226,16.975,-1.14,17.108,-1.064,17.279,-1,17.543,-0.775,17.753,-0.549,17.724,-0.278,17.773,-0.052,17.888,0.234,17.925,0.537,17.886,0.857,17.902,1.118,18.012,1.422,18.058,1.535,18.073,1.719,18.072,2.013,18.212,2.415,18.343,2.655,18.491,2.924,18.547,3.087,18.622,3.304,18.61,3.478,18.597,3.679,18.634,3.954,18.62,4.117,18.567,4.258,18.594,4.346,18.7,4.383,18.832,4.523,19.069,4.891,19.323,5.071,19.501,5.127,19.686,5.121,19.807,5.089,19.863,5.031,20.002,4.945,20.226,4.83,20.394,4.686,20.487,4.542,20.558,4.463,20.647,4.436,20.793,4.447,20.956,4.413,21.126,4.332,21.23,4.302,21.268,4.323,21.35,4.311,21.538,4.245,21.687,4.281,21.908,4.254,22.422,4.135,22.45,4.155,22.462,4.16,22.506,4.208,22.617,4.446,22.712,4.592,22.756,4.647,22.865,4.724,22.993,4.744,23.116,4.737,23.219,4.703,23.313,4.664,23.417,4.663,23.524,4.701,23.682,4.771,23.848,4.816,23.992,4.866,24.228,4.954,24.32,4.994,24.437,5.01,24.766,4.93,24.978,4.983,25.065,4.967,25.249,5.025,25.283,5.063,25.4,5.256,25.525,5.312,25.714,5.284,25.82,5.254,26.174,5.171,26.633,5.085,26.768,5.072,26.822,5.062,26.87,5.076,27.021,5.184,27.072,5.2,27.115,5.198,27.403,5.109,27.439,5.039,27.491,4.968,27.664,4.846,27.719,4.778,27.761,4.703,27.788,4.645,27.842,4.598,27.917,4.568,27.981,4.532,28.02,4.479,28.079,4.425,28.192,4.35,28.247,4.349,28.311,4.338,28.367,4.319,28.428,4.324,28.525,4.373,28.64,4.454,28.727,4.505,28.939,4.487,29.057,4.446,29.151,4.388,29.225,4.392,29.385,4.498,29.47,4.612,29.552,4.636,29.677,4.587,29.78,4.481,29.87,4.327,29.934,4.269,30.021,4.178,30.195,3.982,30.421,3.884,30.508,3.836,30.537,3.787,30.554,3.723,30.559,3.653,30.587,3.624,30.648,3.634,30.7,3.644,30.757,3.624,30.797,3.573,30.817,3.533,30.839,3.491,30.895,3.464,30.906,3.409,30.868,3.342,30.828,3.283,30.779,3.163,30.754,3.042,30.787,3.001,30.821,2.968,30.84,2.933,30.851,2.894,30.847,2.847,30.77,2.678,30.73,2.53,30.729,2.455,30.83,2.4,30.962,2.403,31.004,2.369,31.045,2.316,31.082,2.288,31.138,2.289,31.176,2.27,31.191,2.232,31.236,2.191,31.274,2.146,31.256,2.088,31.253,2.045,31.159,1.922,30.943,1.683,30.478,1.239,30.321,1.185,30.24,1.103,30.183,0.973,30.047,0.864,29.943,0.819,29.932,0.793,29.924,0.674,29.934,0.499,29.885,0.419,29.815,0.264,29.778,0.166,29.75,0.147,29.718,0.098,29.698,-0.06,29.684,-0.114,29.633,-0.442,29.648,-0.535,29.608,-0.691,29.606,-0.783,29.59,-0.887,29.562,-0.977,29.564,-1.121,29.58,-1.357,29.577,-1.388,29.538,-1.41,29.468,-1.468,29.402,-1.507,29.352,-1.518,29.268,-1.622,29.197,-1.72,29.143,-1.816,29.129,-1.86,29.141,-1.985,29.148,-2.132,29.132,-2.195,29.106,-2.233,28.99,-2.313,28.913,-2.37,28.876,-2.4,28.858,-2.447,28.891,-2.556,28.894,-2.635,28.922,-2.682,29.014,-2.72,29.014,-2.758,29.017,-2.8,29.065,-2.851,29.153,-2.955,29.224,-3.054,29.226,-3.139,29.212,-3.281,29.21,-3.363,29.217,-3.476,29.217,-3.685,29.212,-3.834,29.223,-3.911,29.331,-4.095,29.379,-4.3,29.403,-4.449,29.404,-4.497,29.368,-4.669,29.326,-4.836,29.323,-4.899,29.343,-4.983,29.42,-5.176,29.476,-5.317,29.504,-5.401,29.542,-5.5,29.594,-5.651,29.607,-5.723,29.596,-5.776,29.491,-5.965,29.48,-6.025,29.506,-6.172,29.541,-6.314,29.591,-6.394,29.71,-6.617,29.798,-6.692,29.962,-6.803,30.106,-6.915,30.162,-6.973,30.213,-7.038,30.313,-7.204,30.375,-7.339,30.407,-7.461,30.486,-7.627,30.559,-7.782,30.654,-7.971,30.721,-8.104,30.751,-8.194]]]},{"n":"Republic of the Congo","c":"CG","r":4,"ml":4,"lx":15.9,"ly":0.14,"b":[11.13,-5.004,18.622,3.687],"p":[[[11.13,-3.916,11.19,-3.762,11.234,-3.691,11.288,-3.641,11.504,-3.52,11.537,-3.525,11.686,-3.682,11.733,-3.695,11.786,-3.69,11.849,-3.697,11.88,-3.666,11.885,-3.625,11.839,-3.58,11.833,-3.531,11.865,-3.479,11.883,-3.42,11.929,-3.351,11.934,-3.319,11.885,-3.283,11.784,-3.229,11.715,-3.177,11.689,-3.127,11.708,-3.063,11.763,-3.011,11.76,-2.983,11.711,-2.937,11.676,-2.887,11.639,-2.855,11.538,-2.837,11.557,-2.77,11.595,-2.671,11.603,-2.595,11.575,-2.397,11.578,-2.361,11.605,-2.343,11.666,-2.365,11.727,-2.395,11.892,-2.351,11.95,-2.345,11.998,-2.383,12.064,-2.413,12.446,-2.33,12.454,-2.246,12.476,-2.169,12.479,-2.112,12.463,-2.075,12.444,-2.048,12.432,-1.99,12.432,-1.929,12.469,-1.9,12.59,-1.827,12.628,-1.83,12.714,-1.869,12.794,-1.932,12.864,-2.063,12.914,-2.176,12.992,-2.313,13.159,-2.369,13.357,-2.405,13.465,-2.395,13.619,-2.279,13.706,-2.187,13.734,-2.138,13.784,-2.164,13.842,-2.284,13.879,-2.33,13.888,-2.375,13.862,-2.43,13.887,-2.465,13.994,-2.491,14.087,-2.467,14.13,-2.418,14.2,-2.354,14.2,-2.301,14.163,-2.266,14.163,-2.218,14.202,-2.18,14.24,-2.077,14.251,-2.001,14.288,-1.954,14.359,-1.92,14.384,-1.89,14.423,-1.712,14.403,-1.647,14.403,-1.593,14.447,-1.525,14.456,-1.413,14.437,-1.23,14.424,-1.104,14.411,-0.972,14.445,-0.799,14.481,-0.618,14.474,-0.573,14.425,-0.519,14.364,-0.469,14.207,-0.427,14.148,-0.362,14.103,-0.292,14.069,-0.27,13.898,-0.243,13.86,-0.203,13.875,-0.091,13.891,0.075,13.885,0.191,13.915,0.284,13.95,0.354,14.025,0.428,14.066,0.515,14.088,0.537,14.231,0.551,14.283,0.587,14.324,0.624,14.342,0.674,14.391,0.756,14.434,0.811,14.439,0.849,14.43,0.901,14.386,1.004,14.334,1.09,14.303,1.121,14.24,1.323,14.181,1.37,14.066,1.396,13.851,1.419,13.721,1.382,13.523,1.315,13.372,1.268,13.274,1.241,13.216,1.248,13.19,1.279,13.228,1.305,13.247,1.367,13.223,1.455,13.185,1.535,13.163,1.648,13.172,1.789,13.209,1.92,13.289,2.092,13.294,2.162,13.533,2.16,13.773,2.157,14.034,2.159,14.287,2.16,14.484,2.155,14.579,2.199,14.669,2.132,14.713,2.117,14.728,2.122,14.763,2.075,14.875,2.08,14.893,2.069,14.902,2.012,15.006,2.014,15.058,2.001,15.1,2.002,15.16,2.036,15.204,2.024,15.282,1.982,15.339,1.945,15.417,1.957,15.6,1.95,15.742,1.915,15.882,1.817,15.975,1.76,16.059,1.676,16.09,1.691,16.12,1.714,16.136,1.724,16.135,1.796,16.088,1.918,16.07,2.022,16.08,2.107,16.116,2.168,16.177,2.205,16.183,2.262,16.183,2.27,16.252,2.407,16.32,2.543,16.401,2.701,16.469,2.832,16.46,2.897,16.466,2.993,16.48,3.101,16.477,3.165,16.496,3.209,16.543,3.37,16.57,3.463,16.611,3.505,16.673,3.535,16.764,3.536,17.003,3.557,17.225,3.598,17.298,3.617,17.438,3.685,17.492,3.687,17.538,3.662,17.807,3.584,17.88,3.554,17.907,3.558,17.948,3.552,18.011,3.551,18.072,3.56,18.111,3.551,18.161,3.5,18.194,3.505,18.237,3.543,18.318,3.581,18.474,3.623,18.5,3.604,18.554,3.51,18.61,3.478,18.622,3.304,18.547,3.087,18.491,2.924,18.343,2.655,18.212,2.415,18.072,2.013,18.073,1.719,18.058,1.535,18.012,1.422,17.902,1.118,17.886,0.857,17.925,0.537,17.888,0.234,17.773,-0.052,17.724,-0.278,17.753,-0.549,17.543,-0.775,17.279,-1,17.108,-1.064,16.975,-1.14,16.88,-1.226,16.849,-1.272,16.78,-1.376,16.622,-1.699,16.541,-1.84,16.434,-1.961,16.274,-2.108,16.215,-2.178,16.192,-2.279,16.202,-2.465,16.217,-3.03,16.191,-3.194,16.147,-3.464,15.99,-3.766,15.872,-3.934,15.755,-3.986,15.6,-4.031,15.526,-4.088,15.481,-4.172,15.395,-4.245,15.267,-4.308,15.106,-4.461,14.912,-4.706,14.779,-4.846,14.708,-4.882,14.634,-4.885,14.558,-4.856,14.494,-4.852,14.462,-4.865,14.441,-4.854,14.411,-4.831,14.412,-4.775,14.403,-4.682,14.365,-4.586,14.41,-4.508,14.45,-4.45,14.443,-4.419,14.403,-4.37,14.358,-4.299,14.316,-4.304,14.227,-4.358,14.134,-4.4,14.047,-4.417,13.978,-4.461,13.941,-4.485,13.882,-4.485,13.85,-4.459,13.778,-4.434,13.739,-4.442,13.717,-4.454,13.708,-4.543,13.699,-4.618,13.685,-4.689,13.66,-4.721,13.552,-4.757,13.478,-4.805,13.415,-4.837,13.376,-4.829,13.297,-4.765,13.22,-4.706,13.176,-4.656,13.152,-4.62,13.137,-4.604,13.087,-4.602,13.073,-4.635,13.048,-4.619,12.971,-4.552,12.881,-4.445,12.848,-4.429,12.798,-4.431,12.719,-4.47,12.642,-4.531,12.501,-4.587,12.385,-4.619,12.374,-4.658,12.347,-4.724,12.308,-4.766,12.204,-4.779,12.167,-4.838,12.078,-4.952,12.018,-5.004,12.003,-4.982,11.967,-4.954,11.893,-4.866,11.821,-4.755,11.801,-4.705,11.781,-4.677,11.778,-4.566,11.668,-4.434,11.394,-4.2,11.364,-4.131,11.13,-3.916]]]},{"n":"Comoros","c":"KM","r":6,"ml":4,"lx":43.32,"ly":-11.73,"b":[43.227,-12.368,44.527,-11.368],"p":[[[44.476,-12.082,44.527,-12.22,44.526,-12.324,44.505,-12.357,44.46,-12.335,44.377,-12.252,44.22,-12.171,44.292,-12.165,44.334,-12.173,44.379,-12.166,44.407,-12.12,44.413,-12.093,44.452,-12.071,44.476,-12.082]],[[43.789,-12.307,43.859,-12.368,43.664,-12.343,43.633,-12.288,43.631,-12.247,43.704,-12.256,43.789,-12.307]],[[43.466,-11.901,43.447,-11.915,43.355,-11.858,43.303,-11.844,43.227,-11.752,43.256,-11.432,43.281,-11.391,43.299,-11.375,43.342,-11.368,43.393,-11.409,43.379,-11.614,43.448,-11.753,43.492,-11.862,43.466,-11.901]]]},{"n":"Colombia","c":"CO","r":2,"ml":3,"lx":-73.17,"ly":3.37,"b":[-79.025,-4.236,-66.876,12.434],"p":[[[-71.32,11.862,-71.356,11.85,-71.4,11.824,-71.536,11.774,-71.719,11.727,-71.958,11.666,-72.012,11.602,-72.248,11.196,-72.446,11.114,-72.518,11.054,-72.572,10.977,-72.69,10.836,-72.739,10.727,-72.869,10.491,-72.94,10.196,-72.967,10.03,-73.007,9.789,-73.064,9.668,-73.141,9.555,-73.224,9.444,-73.296,9.322,-73.356,9.227,-73.366,9.194,-73.337,9.168,-73.193,9.194,-73.137,9.223,-73.058,9.26,-73.009,9.24,-72.96,9.135,-72.904,9.122,-72.852,9.135,-72.796,9.109,-72.726,8.848,-72.665,8.628,-72.526,8.49,-72.417,8.382,-72.39,8.287,-72.364,8.153,-72.358,8.087,-72.392,8.048,-72.446,7.966,-72.46,7.81,-72.469,7.758,-72.479,7.613,-72.472,7.524,-72.443,7.455,-72.395,7.415,-72.296,7.395,-72.208,7.37,-72.157,7.25,-72.084,7.097,-72.007,7.033,-71.893,6.99,-71.811,7.006,-71.621,7.033,-71.457,7.026,-71.218,6.985,-71.129,6.987,-71.013,6.994,-70.811,7.078,-70.737,7.09,-70.655,7.083,-70.536,7.041,-70.471,7.007,-70.387,6.973,-70.266,6.948,-70.188,6.952,-70.129,6.954,-70.095,6.938,-69.904,6.7,-69.739,6.494,-69.595,6.321,-69.439,6.135,-69.427,6.124,-69.357,6.148,-69.311,6.138,-69.268,6.1,-69.195,6.115,-69.09,6.184,-68.937,6.198,-68.736,6.157,-68.472,6.157,-68.143,6.198,-67.939,6.242,-67.859,6.29,-67.727,6.285,-67.568,6.242,-67.482,6.18,-67.472,6.12,-67.439,6.026,-67.474,5.93,-67.575,5.833,-67.631,5.709,-67.642,5.559,-67.695,5.448,-67.788,5.375,-67.825,5.27,-67.804,5.133,-67.814,4.931,-67.855,4.665,-67.855,4.507,-67.814,4.455,-67.795,4.381,-67.799,4.284,-67.783,4.198,-67.732,4.087,-67.662,3.864,-67.603,3.769,-67.551,3.734,-67.499,3.691,-67.348,3.464,-67.311,3.416,-67.322,3.374,-67.336,3.343,-67.354,3.323,-67.515,3.187,-67.835,2.893,-67.861,2.855,-67.859,2.794,-67.766,2.833,-67.667,2.8,-67.619,2.794,-67.597,2.769,-67.568,2.69,-67.535,2.677,-67.486,2.644,-67.392,2.56,-67.312,2.472,-67.253,2.429,-67.211,2.39,-67.198,2.333,-67.215,2.275,-67.165,2.143,-67.131,2.101,-67.114,2.051,-67.131,2,-67.09,1.94,-67.044,1.823,-66.988,1.68,-66.982,1.601,-66.958,1.564,-66.931,1.458,-66.884,1.358,-66.896,1.29,-66.876,1.223,-67.065,1.178,-67.082,1.185,-67.094,1.21,-67.088,1.401,-67.09,1.616,-67.119,1.704,-67.206,1.845,-67.321,2.032,-67.352,2.086,-67.4,2.117,-67.458,2.121,-67.5,2.108,-67.556,2.073,-67.609,2.035,-67.712,1.922,-67.815,1.79,-67.876,1.761,-67.936,1.748,-67.99,1.753,-68.033,1.788,-68.077,1.86,-68.13,1.956,-68.194,1.987,-68.218,1.958,-68.239,1.901,-68.256,1.846,-68.213,1.775,-68.177,1.72,-68.24,1.722,-68.443,1.722,-68.678,1.721,-68.913,1.721,-69.124,1.721,-69.32,1.721,-69.394,1.726,-69.47,1.758,-69.543,1.773,-69.581,1.771,-69.65,1.739,-69.74,1.735,-69.8,1.705,-69.849,1.709,-69.849,1.544,-69.851,1.309,-69.852,1.06,-69.798,1.078,-69.751,1.077,-69.717,1.059,-69.621,1.073,-69.568,1.066,-69.517,1.059,-69.47,1.059,-69.442,1.039,-69.403,1.042,-69.361,1.064,-69.312,1.05,-69.259,1.015,-69.224,0.963,-69.194,0.898,-69.163,0.864,-69.165,0.802,-69.166,0.753,-69.177,0.713,-69.163,0.687,-69.153,0.659,-69.156,0.643,-69.174,0.635,-69.213,0.63,-69.254,0.625,-69.283,0.627,-69.306,0.652,-69.327,0.655,-69.359,0.652,-69.392,0.667,-69.421,0.698,-69.472,0.73,-69.527,0.716,-69.565,0.7,-69.604,0.68,-69.639,0.66,-69.674,0.665,-69.719,0.65,-69.757,0.626,-69.807,0.607,-69.862,0.598,-69.925,0.589,-69.985,0.586,-70.054,0.579,-70.058,0.447,-70.066,0.189,-70.071,0.019,-70.071,-0.139,-70.044,-0.196,-69.923,-0.317,-69.828,-0.381,-69.747,-0.453,-69.667,-0.482,-69.634,-0.509,-69.612,-0.553,-69.601,-0.6,-69.592,-0.639,-69.601,-0.681,-69.621,-0.721,-69.612,-0.763,-69.583,-0.796,-69.574,-0.838,-69.555,-0.877,-69.544,-0.917,-69.519,-0.946,-69.488,-0.966,-69.449,-0.999,-69.444,-1.03,-69.449,-1.065,-69.449,-1.092,-69.411,-1.152,-69.4,-1.195,-69.418,-1.246,-69.435,-1.422,-69.479,-1.622,-69.506,-1.775,-69.552,-2.024,-69.605,-2.314,-69.669,-2.668,-69.733,-3.017,-69.794,-3.355,-69.85,-3.66,-69.911,-3.997,-69.948,-4.201,-69.966,-4.236,-70.017,-4.162,-70.095,-4.092,-70.168,-4.05,-70.198,-3.995,-70.24,-3.883,-70.298,-3.844,-70.34,-3.814,-70.379,-3.819,-70.421,-3.85,-70.486,-3.869,-70.53,-3.866,-70.706,-3.789,-70.735,-3.782,-70.622,-3.605,-70.419,-3.288,-70.29,-3.087,-70.147,-2.864,-70.074,-2.75,-70.064,-2.731,-70.065,-2.702,-70.096,-2.658,-70.165,-2.64,-70.244,-2.607,-70.295,-2.553,-70.364,-2.529,-70.418,-2.491,-70.517,-2.453,-70.576,-2.418,-70.648,-2.406,-70.705,-2.342,-70.915,-2.219,-70.969,-2.207,-71.027,-2.226,-71.113,-2.245,-71.196,-2.313,-71.3,-2.335,-71.397,-2.334,-71.447,-2.294,-71.496,-2.279,-71.559,-2.224,-71.671,-2.182,-71.753,-2.153,-71.803,-2.166,-71.867,-2.228,-71.932,-2.289,-71.984,-2.327,-72.054,-2.325,-72.137,-2.381,-72.218,-2.4,-72.301,-2.409,-72.396,-2.429,-72.501,-2.395,-72.587,-2.365,-72.625,-2.352,-72.66,-2.361,-72.714,-2.392,-72.811,-2.405,-72.887,-2.408,-72.941,-2.394,-72.99,-2.34,-73.068,-2.312,-73.154,-2.278,-73.173,-2.208,-73.16,-2.156,-73.127,-2.081,-73.145,-2.003,-73.181,-1.88,-73.197,-1.83,-73.224,-1.788,-73.266,-1.772,-73.35,-1.784,-73.44,-1.737,-73.496,-1.693,-73.525,-1.639,-73.494,-1.537,-73.521,-1.45,-73.575,-1.401,-73.61,-1.316,-73.664,-1.249,-73.736,-1.214,-73.807,-1.218,-73.863,-1.197,-73.927,-1.125,-73.987,-1.098,-74.054,-1.029,-74.181,-0.998,-74.246,-0.971,-74.284,-0.928,-74.334,-0.851,-74.329,-0.808,-74.353,-0.767,-74.375,-0.691,-74.418,-0.581,-74.465,-0.518,-74.514,-0.47,-74.555,-0.43,-74.616,-0.37,-74.692,-0.335,-74.755,-0.299,-74.78,-0.245,-74.802,-0.2,-74.837,-0.203,-74.889,-0.199,-74.945,-0.188,-75.005,-0.156,-75.055,-0.117,-75.138,-0.05,-75.184,-0.042,-75.225,-0.042,-75.284,-0.107,-75.464,-0.038,-75.617,0.063,-75.777,0.089,-75.88,0.151,-75.975,0.248,-76.026,0.313,-76.068,0.346,-76.271,0.439,-76.311,0.448,-76.388,0.405,-76.413,0.379,-76.418,0.304,-76.427,0.261,-76.495,0.235,-76.603,0.241,-76.679,0.268,-76.729,0.272,-76.739,0.251,-76.768,0.242,-76.829,0.248,-76.92,0.269,-77.002,0.296,-77.114,0.355,-77.166,0.348,-77.293,0.36,-77.396,0.394,-77.423,0.425,-77.468,0.637,-77.481,0.651,-77.526,0.66,-77.601,0.69,-77.649,0.724,-77.673,0.782,-77.703,0.838,-77.83,0.825,-78.037,0.899,-78.181,0.969,-78.312,1.046,-78.512,1.199,-78.588,1.237,-78.682,1.283,-78.737,1.359,-78.829,1.435,-78.86,1.455,-78.888,1.524,-79.025,1.624,-78.958,1.752,-78.793,1.849,-78.577,1.774,-78.55,1.924,-78.629,2.056,-78.617,2.307,-78.592,2.357,-78.535,2.424,-78.46,2.47,-78.417,2.483,-78.343,2.461,-78.296,2.51,-78.12,2.488,-78.067,2.509,-78.03,2.543,-77.987,2.569,-77.932,2.629,-77.901,2.699,-77.875,2.726,-77.814,2.716,-77.808,2.746,-77.777,2.787,-77.67,2.879,-77.671,2.919,-77.701,3.008,-77.694,3.04,-77.632,3.051,-77.559,3.076,-77.52,3.16,-77.472,3.234,-77.417,3.342,-77.357,3.349,-77.324,3.475,-77.243,3.585,-77.077,3.913,-77.127,3.906,-77.167,3.862,-77.212,3.867,-77.264,3.893,-77.248,4.041,-77.278,4.058,-77.358,3.945,-77.427,4.06,-77.434,4.131,-77.404,4.201,-77.409,4.248,-77.521,4.213,-77.516,4.256,-77.446,4.301,-77.414,4.348,-77.354,4.398,-77.328,4.475,-77.314,4.594,-77.286,4.722,-77.307,4.785,-77.339,4.839,-77.367,5.077,-77.359,5.215,-77.373,5.324,-77.402,5.416,-77.534,5.537,-77.325,5.676,-77.249,5.78,-77.345,5.995,-77.469,6.177,-77.473,6.286,-77.44,6.272,-77.398,6.275,-77.36,6.504,-77.369,6.576,-77.439,6.69,-77.526,6.693,-77.602,6.837,-77.646,6.87,-77.681,6.96,-77.804,7.137,-77.901,7.229,-77.828,7.443,-77.765,7.484,-77.744,7.537,-77.769,7.668,-77.762,7.699,-77.747,7.712,-77.732,7.711,-77.706,7.691,-77.659,7.635,-77.619,7.565,-77.587,7.543,-77.538,7.566,-77.351,7.706,-77.363,7.749,-77.346,7.837,-77.283,7.908,-77.216,7.933,-77.196,7.972,-77.212,8.034,-77.283,8.187,-77.346,8.27,-77.386,8.352,-77.407,8.427,-77.479,8.498,-77.448,8.566,-77.393,8.645,-77.374,8.658,-77.344,8.637,-77.262,8.494,-77.13,8.401,-76.992,8.25,-76.936,8.147,-76.891,8.128,-76.852,8.09,-76.869,8.063,-76.912,8.033,-76.925,7.973,-76.897,7.939,-76.867,7.918,-76.787,7.932,-76.742,8.002,-76.772,8.311,-76.819,8.465,-76.872,8.513,-76.92,8.574,-76.888,8.62,-76.802,8.641,-76.689,8.695,-76.277,8.989,-76.135,9.266,-76.027,9.366,-75.905,9.431,-75.756,9.416,-75.639,9.45,-75.604,9.538,-75.635,9.658,-75.68,9.73,-75.637,9.834,-75.593,9.993,-75.596,10.126,-75.539,10.205,-75.558,10.236,-75.642,10.172,-75.708,10.143,-75.671,10.196,-75.554,10.328,-75.493,10.528,-75.446,10.611,-75.281,10.727,-75.248,10.783,-75.123,10.87,-74.922,11.058,-74.845,11.11,-74.454,10.989,-74.33,10.997,-74.352,10.975,-74.41,10.967,-74.492,10.934,-74.516,10.863,-74.46,10.787,-74.401,10.765,-74.35,10.814,-74.3,10.952,-74.219,11.105,-74.2,11.266,-74.143,11.321,-74.059,11.341,-73.91,11.309,-73.796,11.276,-73.677,11.271,-73.313,11.296,-72.722,11.712,-72.447,11.802,-72.275,11.889,-72.165,12.06,-72.136,12.189,-72.055,12.238,-71.97,12.238,-71.931,12.27,-71.919,12.309,-71.715,12.42,-71.597,12.434,-71.494,12.432,-71.262,12.335,-71.155,12.164,-71.137,12.046,-71.284,11.918,-71.32,11.862]],[[-78.114,2.542,-78.141,2.52,-78.192,2.559,-78.21,2.609,-78.178,2.646,-78.138,2.634,-78.119,2.604,-78.114,2.542]]]},{"n":"People's Republic of China","c":"CN","r":2,"ml":1.7,"lx":106.34,"ly":32.5,"b":[73.607,18.218,134.752,53.556],"p":[[[118.183,24.496,118.15,24.436,118.091,24.446,118.089,24.489,118.077,24.501,118.093,24.541,118.104,24.552,118.171,24.519,118.183,24.496]],[[121.863,31.492,121.78,31.464,121.52,31.55,121.336,31.644,121.227,31.758,121.211,31.805,121.339,31.797,121.464,31.756,121.492,31.694,121.542,31.674,121.577,31.637,121.808,31.552,121.844,31.526,121.863,31.492]],[[122.296,29.963,122.282,29.944,122.158,30.001,122.024,30.013,121.978,30.064,121.969,30.143,122.111,30.14,122.284,30.068,122.322,30.031,122.296,29.963]],[[122.173,29.679,122.169,29.66,122.084,29.725,122.043,29.736,122.062,29.773,122.12,29.782,122.165,29.701,122.173,29.679]],[[122.404,29.892,122.394,29.846,122.368,29.853,122.332,29.935,122.351,29.955,122.402,29.95,122.404,29.892]],[[119.821,25.457,119.747,25.411,119.7,25.433,119.699,25.495,119.723,25.551,119.696,25.591,119.723,25.639,119.778,25.653,119.797,25.623,119.829,25.607,119.838,25.591,119.839,25.56,119.809,25.508,119.832,25.48,119.821,25.457]],[[110.385,21.093,110.422,21.059,110.522,21.083,110.54,21.039,110.539,21.018,110.504,20.968,110.422,21.007,110.34,20.998,110.281,21.001,110.265,21.025,110.31,21.075,110.385,21.093]],[[121.251,28.086,121.164,28.063,121.132,28.063,121.134,28.135,121.205,28.204,121.234,28.181,121.251,28.145,121.251,28.086]],[[113.555,22.804,113.564,22.758,113.486,22.828,113.463,22.832,113.426,22.859,113.404,22.903,113.465,22.905,113.521,22.852,113.555,22.804]],[[112.79,21.602,112.771,21.582,112.742,21.618,112.733,21.67,112.713,21.698,112.761,21.733,112.782,21.772,112.839,21.765,112.863,21.753,112.813,21.712,112.801,21.695,112.79,21.602]],[[112.644,21.64,112.546,21.619,112.525,21.623,112.559,21.675,112.648,21.71,112.644,21.64]],[[107.973,21.508,107.908,21.56,107.802,21.645,107.759,21.655,107.641,21.614,107.471,21.598,107.433,21.642,107.351,21.609,107.272,21.711,107.179,21.717,107.062,21.794,107.02,21.835,107.006,21.893,106.971,21.924,106.925,21.92,106.875,21.951,106.794,21.982,106.729,22,106.698,21.986,106.664,21.979,106.658,22.018,106.66,22.136,106.654,22.241,106.637,22.289,106.593,22.325,106.554,22.342,106.536,22.395,106.55,22.501,106.582,22.573,106.633,22.586,106.702,22.638,106.736,22.711,106.78,22.779,106.624,22.874,106.542,22.908,106.451,22.894,106.338,22.863,106.279,22.857,106.249,22.869,106.184,22.955,106.148,22.97,106.068,22.976,106.001,22.975,105.962,22.937,105.903,22.925,105.843,22.923,105.782,22.969,105.691,23.03,105.548,23.073,105.531,23.122,105.495,23.181,105.44,23.235,105.35,23.308,105.275,23.345,105.239,23.322,105.189,23.281,104.996,23.194,104.91,23.161,104.865,23.136,104.827,23.1,104.815,23.011,104.796,22.911,104.74,22.86,104.687,22.822,104.632,22.818,104.578,22.82,104.527,22.804,104.372,22.704,104.298,22.712,104.238,22.769,104.213,22.809,104.143,22.8,104.054,22.752,104.013,22.666,103.991,22.586,103.971,22.55,103.942,22.54,103.915,22.538,103.637,22.77,103.62,22.782,103.571,22.734,103.525,22.612,103.493,22.588,103.471,22.597,103.356,22.755,103.327,22.77,103.301,22.764,103.266,22.714,103.193,22.639,103.138,22.593,103.136,22.542,103.076,22.498,103.005,22.453,102.982,22.448,102.935,22.466,102.874,22.525,102.83,22.587,102.721,22.648,102.599,22.7,102.517,22.741,102.471,22.751,102.428,22.733,102.406,22.708,102.376,22.647,102.302,22.546,102.237,22.466,102.176,22.415,102.127,22.379,102.092,22.412,102.024,22.439,101.945,22.439,101.842,22.388,101.76,22.49,101.739,22.495,101.708,22.487,101.671,22.462,101.646,22.405,101.62,22.327,101.568,22.276,101.525,22.254,101.537,22.21,101.562,22.162,101.56,22.121,101.576,22.055,101.603,21.99,101.7,21.882,101.737,21.827,101.744,21.778,101.747,21.606,101.743,21.534,101.724,21.395,101.723,21.315,101.763,21.279,101.802,21.236,101.801,21.213,101.783,21.204,101.728,21.156,101.705,21.15,101.669,21.17,101.622,21.184,101.584,21.204,101.542,21.234,101.444,21.231,101.281,21.184,101.248,21.197,101.224,21.224,101.212,21.278,101.22,21.342,101.206,21.383,101.175,21.408,101.197,21.522,101.139,21.567,101.147,21.582,101.128,21.705,101.131,21.736,101.121,21.746,101.08,21.756,101.019,21.736,100.835,21.655,100.677,21.505,100.605,21.472,100.531,21.458,100.446,21.484,100.351,21.501,100.215,21.463,100.148,21.481,100.117,21.511,100.089,21.558,100.106,21.617,100.096,21.661,100.041,21.683,99.978,21.702,99.941,21.759,99.926,21.821,99.94,21.902,99.948,21.988,99.918,22.028,99.825,22.05,99.593,22.089,99.389,22.111,99.303,22.101,99.233,22.11,99.193,22.126,99.173,22.153,99.172,22.192,99.205,22.283,99.243,22.37,99.338,22.498,99.343,22.587,99.338,22.689,99.385,22.825,99.467,22.927,99.507,22.959,99.497,23.005,99.465,23.046,99.418,23.069,99.341,23.096,99.22,23.103,99.055,23.131,98.864,23.191,98.886,23.307,98.883,23.38,98.859,23.44,98.82,23.483,98.798,23.52,98.832,23.624,98.788,23.738,98.735,23.783,98.681,23.842,98.677,23.905,98.702,23.964,98.834,24.091,98.835,24.121,98.802,24.119,98.764,24.116,98.583,24.07,98.564,24.099,98.499,24.116,98.367,24.119,98.213,24.111,98.017,24.065,97.838,23.986,97.756,23.932,97.686,23.898,97.63,23.887,97.565,23.911,97.568,23.988,97.691,24.131,97.708,24.229,97.671,24.313,97.667,24.38,97.624,24.423,97.563,24.444,97.531,24.492,97.529,24.631,97.583,24.775,97.671,24.82,97.724,24.842,97.738,24.87,97.711,24.97,97.715,25.034,97.767,25.158,97.82,25.252,97.918,25.236,97.962,25.259,98.011,25.293,98.064,25.349,98.1,25.416,98.143,25.571,98.173,25.595,98.297,25.569,98.334,25.587,98.402,25.678,98.466,25.789,98.558,25.823,98.625,25.827,98.656,25.864,98.655,25.918,98.591,26.004,98.564,26.072,98.572,26.114,98.663,26.139,98.686,26.189,98.672,26.299,98.709,26.43,98.732,26.583,98.739,26.698,98.738,26.786,98.729,26.877,98.717,27.045,98.675,27.191,98.682,27.245,98.677,27.422,98.651,27.572,98.6,27.599,98.504,27.648,98.453,27.657,98.409,27.639,98.392,27.587,98.35,27.538,98.299,27.55,98.274,27.599,98.241,27.663,98.13,27.968,98.118,28.055,98.099,28.142,98.062,28.186,98.022,28.212,97.934,28.314,97.888,28.356,97.865,28.364,97.817,28.356,97.769,28.356,97.73,28.407,97.695,28.469,97.659,28.5,97.599,28.517,97.538,28.51,97.502,28.456,97.478,28.426,97.431,28.354,97.356,28.254,97.322,28.218,97.289,28.237,97.145,28.34,97.075,28.369,96.981,28.338,96.833,28.362,96.776,28.367,96.653,28.45,96.603,28.46,96.428,28.406,96.389,28.368,96.366,28.367,96.32,28.387,96.281,28.412,96.279,28.428,96.326,28.469,96.33,28.497,96.327,28.525,96.396,28.607,96.581,28.764,96.55,28.83,96.477,28.959,96.467,29.022,96.436,29.051,96.347,29.027,96.162,28.91,96.137,28.923,96.141,28.963,96.122,29.082,96.181,29.118,96.271,29.161,96.34,29.21,96.356,29.249,96.337,29.261,96.235,29.246,96.195,29.272,96.129,29.381,96.08,29.424,96.035,29.447,95.885,29.391,95.71,29.314,95.516,29.206,95.517,29.151,95.494,29.137,95.457,29.102,95.42,29.054,95.389,29.037,95.353,29.036,95.279,29.05,95.145,29.104,94.999,29.149,94.967,29.144,94.769,29.176,94.763,29.201,94.733,29.252,94.677,29.297,94.623,29.312,94.468,29.216,94.293,29.145,94.193,29.06,94.112,28.976,94.018,28.96,94.013,28.908,93.974,28.861,93.902,28.803,93.761,28.73,93.665,28.69,93.361,28.654,93.252,28.629,93.207,28.591,93.158,28.493,93.119,28.402,93.035,28.328,92.882,28.228,92.702,28.147,92.653,28.093,92.643,28.062,92.666,28.05,92.688,28.026,92.688,27.989,92.664,27.949,92.547,27.879,92.481,27.846,92.415,27.825,92.341,27.821,92.27,27.83,92.25,27.842,92.222,27.827,92.158,27.812,92.101,27.808,91.978,27.73,91.909,27.73,91.825,27.746,91.713,27.76,91.632,27.76,91.629,27.801,91.642,27.923,91.606,27.952,91.493,27.982,91.368,28.022,91.307,28.064,91.273,28.078,91.226,28.071,91.15,28.027,91.078,27.974,91.021,27.97,90.963,27.995,90.907,28.027,90.716,28.072,90.63,28.079,90.477,28.071,90.353,28.08,90.333,28.094,90.334,28.119,90.352,28.168,90.363,28.217,90.348,28.244,90.221,28.278,90.104,28.302,89.981,28.311,89.898,28.294,89.817,28.256,89.75,28.188,89.653,28.158,89.537,28.107,89.481,28.06,89.396,27.958,89.273,27.833,89.16,27.711,89.102,27.593,89.025,27.518,88.948,27.464,88.891,27.316,88.833,27.363,88.765,27.43,88.749,27.522,88.83,27.767,88.849,27.869,88.829,27.907,88.804,28.007,88.756,28.04,88.621,28.092,88.578,28.093,88.532,28.057,88.486,28.034,88.426,28.012,88.275,27.969,88.141,27.949,88.109,27.933,88.099,27.905,88.11,27.871,88.023,27.883,87.933,27.891,87.861,27.886,87.683,27.821,87.623,27.815,87.555,27.822,87.464,27.824,87.291,27.822,87.141,27.838,87.02,27.929,86.934,27.968,86.842,27.999,86.75,28.022,86.72,28.071,86.691,28.095,86.614,28.103,86.554,28.085,86.517,27.964,86.485,27.94,86.409,27.929,86.329,27.96,86.218,28.022,86.174,28.092,86.137,28.114,86.079,28.084,86.075,27.995,86.064,27.935,85.995,27.91,85.954,27.928,85.922,27.99,85.84,28.135,85.759,28.221,85.678,28.277,85.411,28.276,85.212,28.293,85.122,28.316,85.089,28.372,85.121,28.484,85.16,28.572,85.159,28.592,85.126,28.603,85.069,28.61,84.855,28.554,84.797,28.56,84.759,28.579,84.714,28.596,84.677,28.622,84.651,28.66,84.465,28.753,84.411,28.804,84.312,28.868,84.229,28.912,84.176,29.036,84.128,29.156,84.101,29.22,84.022,29.254,83.936,29.279,83.79,29.227,83.671,29.188,83.583,29.184,83.457,29.306,83.355,29.439,83.235,29.555,83.155,29.613,83.014,29.618,82.854,29.683,82.641,29.831,82.487,29.942,82.221,30.064,82.159,30.115,82.135,30.159,82.099,30.245,82.043,30.327,81.855,30.362,81.642,30.388,81.417,30.338,81.255,30.093,81.177,30.04,81.11,30.037,81.056,30.099,81.01,30.165,80.985,30.237,80.874,30.291,80.747,30.36,80.682,30.415,80.609,30.449,80.541,30.464,80.41,30.509,80.261,30.561,80.191,30.568,80.186,30.605,80.207,30.684,80.194,30.759,80.149,30.79,80.081,30.782,79.925,30.889,79.919,30.89,79.917,30.894,79.872,30.925,79.795,30.968,79.664,30.965,79.565,30.949,79.493,30.994,79.388,31.064,79.37,31.08,79.339,31.106,79.233,31.242,79.107,31.403,79.044,31.426,79.011,31.414,78.974,31.329,78.946,31.337,78.9,31.331,78.845,31.302,78.792,31.294,78.758,31.302,78.744,31.324,78.759,31.437,78.727,31.472,78.755,31.55,78.803,31.618,78.754,31.668,78.693,31.74,78.687,31.806,78.72,31.888,78.735,31.958,78.726,31.984,78.678,32.023,78.496,32.216,78.486,32.236,78.455,32.3,78.441,32.397,78.417,32.467,78.39,32.52,78.392,32.545,78.413,32.558,78.526,32.571,78.632,32.579,78.701,32.597,78.737,32.558,78.754,32.499,78.771,32.468,78.838,32.412,78.919,32.358,78.998,32.365,79.067,32.388,79.127,32.476,79.17,32.497,79.219,32.501,79.219,32.508,79.217,32.564,79.234,32.703,79.228,32.759,79.206,32.809,79.21,32.865,79.202,32.946,79.146,33.001,79.109,33.023,79.103,33.053,79.122,33.108,79.135,33.172,79.113,33.226,79.067,33.25,79.013,33.291,78.948,33.347,78.917,33.387,78.865,33.431,78.802,33.5,78.79,33.65,78.784,33.809,78.762,33.888,78.727,34.013,78.732,34.056,78.753,34.088,78.932,34.189,78.971,34.228,78.977,34.258,78.97,34.303,78.936,34.352,78.865,34.39,78.763,34.453,78.671,34.518,78.516,34.558,78.327,34.606,78.282,34.654,78.236,34.77,78.158,34.946,78.076,35.135,78.012,35.251,78.009,35.307,78.047,35.449,78.043,35.48,78.009,35.49,77.946,35.472,77.895,35.449,77.852,35.461,77.811,35.485,77.803,35.493,77.799,35.496,77.724,35.481,77.573,35.472,77.52,35.473,77.446,35.476,77.295,35.508,77.09,35.552,76.879,35.613,76.767,35.662,76.728,35.679,76.632,35.729,76.563,35.773,76.551,35.887,76.502,35.878,76.386,35.837,76.252,35.811,76.178,35.811,76.148,35.829,76.103,35.949,76.071,35.983,76.01,35.996,75.945,36.018,75.912,36.049,75.905,36.088,75.934,36.134,75.969,36.169,75.974,36.382,75.952,36.458,75.933,36.522,75.885,36.601,75.84,36.65,75.772,36.695,75.667,36.742,75.574,36.759,75.46,36.725,75.424,36.738,75.377,36.884,75.347,36.913,75.145,36.973,75.054,36.987,74.949,36.968,74.889,36.952,74.841,36.979,74.766,37.013,74.692,37.036,74.601,37.037,74.541,37.022,74.526,37.031,74.498,37.057,74.376,37.137,74.372,37.158,74.559,37.237,74.669,37.267,74.727,37.291,74.739,37.286,74.767,37.249,74.84,37.225,74.891,37.232,74.918,37.25,75.008,37.294,75.079,37.344,75.119,37.386,75.097,37.451,74.986,37.53,74.916,37.573,74.894,37.601,74.912,37.687,74.938,37.773,74.921,37.805,74.9,37.833,74.891,37.926,74.842,38.038,74.79,38.104,74.775,38.192,74.772,38.275,74.836,38.404,74.812,38.46,74.745,38.51,74.514,38.6,74.277,38.66,74.187,38.658,74.131,38.661,74.065,38.608,74.026,38.54,73.97,38.534,73.869,38.563,73.802,38.607,73.754,38.699,73.717,38.817,73.696,38.854,73.707,38.886,73.73,38.915,73.795,38.941,73.805,38.969,73.796,39.002,73.744,39.045,73.69,39.105,73.607,39.229,73.623,39.298,73.636,39.397,73.632,39.449,73.716,39.462,73.823,39.489,73.873,39.533,73.907,39.579,73.915,39.606,73.883,39.715,73.84,39.763,73.835,39.8,73.856,39.829,73.885,39.878,73.939,39.979,73.992,40.043,74.021,40.059,74.085,40.074,74.243,40.092,74.412,40.137,74.613,40.272,74.68,40.311,74.768,40.33,74.83,40.329,74.842,40.345,74.801,40.429,74.811,40.459,74.835,40.483,74.866,40.494,75.004,40.45,75.111,40.454,75.241,40.48,75.521,40.628,75.556,40.625,75.583,40.605,75.617,40.517,75.656,40.329,75.677,40.306,75.872,40.303,76.004,40.371,76.062,40.388,76.157,40.376,76.206,40.408,76.258,40.431,76.319,40.352,76.396,40.39,76.48,40.45,76.521,40.511,76.578,40.578,76.622,40.662,76.64,40.742,76.661,40.78,76.708,40.818,76.824,40.982,76.908,41.024,76.987,41.039,77.182,41.011,77.284,41.014,77.582,40.993,77.719,41.024,77.815,41.056,77.956,41.051,78.123,41.076,78.346,41.281,78.349,41.325,78.362,41.372,78.443,41.418,78.543,41.46,78.743,41.56,79.148,41.719,79.294,41.783,79.354,41.781,79.504,41.821,79.766,41.899,79.84,41.996,79.91,42.015,80.216,42.032,80.235,42.043,80.246,42.06,80.229,42.13,80.209,42.19,80.233,42.208,80.259,42.235,80.255,42.274,80.206,42.399,80.179,42.518,80.162,42.626,80.165,42.666,80.202,42.734,80.25,42.797,80.424,42.856,80.539,42.873,80.544,42.912,80.451,42.936,80.383,42.974,80.371,42.996,80.375,43.02,80.39,43.043,80.507,43.086,80.617,43.128,80.751,43.102,80.778,43.119,80.786,43.162,80.757,43.204,80.73,43.274,80.668,43.31,80.665,43.353,80.704,43.427,80.651,43.564,80.593,43.685,80.496,43.892,80.432,43.952,80.396,44.047,80.355,44.097,80.359,44.171,80.365,44.223,80.355,44.327,80.336,44.438,80.355,44.552,80.391,44.627,80.381,44.655,80.401,44.677,80.455,44.684,80.482,44.715,80.455,44.746,80.361,44.77,80.255,44.808,80.128,44.804,79.997,44.797,79.932,44.825,79.875,44.861,79.872,44.884,79.95,44.944,80.059,45.006,80.228,45.034,80.415,45.075,80.509,45.105,80.635,45.127,80.78,45.136,80.853,45.129,81.04,45.169,81.335,45.246,81.602,45.311,81.692,45.349,81.759,45.311,81.79,45.226,81.867,45.182,81.945,45.161,81.989,45.162,82.123,45.195,82.267,45.219,82.323,45.206,82.397,45.162,82.479,45.124,82.521,45.125,82.559,45.155,82.597,45.216,82.621,45.293,82.626,45.374,82.612,45.424,82.583,45.443,82.452,45.472,82.327,45.52,82.312,45.564,82.315,45.595,82.348,45.672,82.43,45.812,82.512,46.006,82.555,46.159,82.692,46.387,82.8,46.624,82.975,46.966,83.004,47.033,83.02,47.141,83.029,47.186,83.09,47.209,83.193,47.187,83.444,47.109,83.634,47.043,83.714,47.021,83.833,46.998,84.016,46.971,84.122,46.979,84.215,46.995,84.339,46.996,84.532,46.976,84.592,46.975,84.667,46.972,84.72,46.939,84.746,46.864,84.786,46.831,84.858,46.843,85.012,46.909,85.111,46.961,85.233,47.036,85.355,47.047,85.485,47.064,85.53,47.101,85.577,47.188,85.657,47.255,85.67,47.338,85.642,47.397,85.587,47.494,85.588,47.558,85.562,47.746,85.526,47.916,85.562,48.052,85.626,48.204,85.652,48.251,85.692,48.312,85.749,48.385,85.83,48.408,86.056,48.424,86.266,48.455,86.373,48.486,86.483,48.505,86.549,48.529,86.664,48.636,86.718,48.697,86.758,48.861,86.729,48.939,86.753,49.009,86.808,49.05,86.886,49.091,86.938,49.098,87.049,49.11,87.23,49.106,87.323,49.086,87.417,49.077,87.476,49.091,87.516,49.122,87.577,49.132,87.668,49.147,87.763,49.166,87.814,49.162,87.825,49.116,87.816,49.08,87.835,49.032,87.872,49,87.86,48.966,87.807,48.946,87.755,48.919,87.743,48.882,87.809,48.836,87.832,48.792,87.942,48.765,88.028,48.736,88.06,48.707,88.05,48.675,88.011,48.64,87.972,48.603,87.967,48.581,87.98,48.555,88.063,48.538,88.158,48.509,88.31,48.472,88.414,48.403,88.517,48.384,88.567,48.317,88.576,48.22,88.682,48.171,88.838,48.102,88.918,48.089,88.971,48.05,89.048,48.003,89.116,47.988,89.196,47.981,89.33,48.025,89.479,48.029,89.561,48.004,89.638,47.909,89.693,47.879,89.726,47.852,89.778,47.827,89.831,47.823,89.91,47.844,89.959,47.886,90.028,47.878,90.054,47.85,90.067,47.804,90.103,47.745,90.191,47.702,90.313,47.676,90.331,47.655,90.347,47.597,90.381,47.557,90.425,47.504,90.467,47.408,90.476,47.329,90.496,47.285,90.553,47.214,90.643,47.1,90.716,47.004,90.799,46.985,90.87,46.954,90.911,46.883,90.986,46.749,90.998,46.661,91.004,46.596,91.029,46.566,91.034,46.529,90.971,46.388,90.918,46.324,90.912,46.271,90.948,46.177,90.997,46.105,91.002,46.036,90.96,45.985,90.887,45.922,90.852,45.885,90.796,45.854,90.71,45.731,90.671,45.595,90.662,45.525,90.694,45.475,90.75,45.419,90.763,45.371,90.853,45.263,90.877,45.196,90.914,45.194,90.954,45.216,91.05,45.217,91.138,45.194,91.222,45.145,91.312,45.118,91.441,45.125,91.51,45.098,91.584,45.077,91.738,45.069,91.853,45.069,92.03,45.069,92.173,45.035,92.424,45.009,92.579,45.011,92.788,45.036,92.916,45.02,93.294,44.983,93.516,44.944,93.656,44.901,93.755,44.832,93.868,44.724,93.958,44.675,94.199,44.645,94.365,44.519,94.494,44.473,94.712,44.351,94.866,44.303,95.05,44.259,95.35,44.278,95.367,44.262,95.344,44.195,95.326,44.105,95.326,44.039,95.356,44.006,95.471,43.986,95.526,43.954,95.567,43.892,95.591,43.854,95.687,43.664,95.842,43.384,95.86,43.276,95.913,43.206,96.08,43.096,96.168,43.015,96.3,42.929,96.342,42.849,96.352,42.747,96.385,42.72,96.625,42.744,96.833,42.76,97.206,42.79,97.719,42.736,98.248,42.685,98.716,42.639,98.947,42.616,99.468,42.568,99.757,42.629,99.984,42.677,100.086,42.671,100.519,42.617,100.773,42.588,101.092,42.551,101.314,42.538,101.495,42.539,101.579,42.524,101.66,42.5,101.714,42.466,101.88,42.292,101.973,42.216,102.157,42.158,102.575,42.092,102.807,42.052,103.073,42.006,103.248,41.937,103.45,41.856,103.711,41.751,103.997,41.797,104.305,41.846,104.498,41.877,104.498,41.659,104.774,41.641,104.86,41.644,104.982,41.596,105.051,41.616,105.115,41.663,105.197,41.738,105.314,41.771,105.517,41.855,105.566,41.875,105.868,41.994,106.317,42.141,106.519,42.212,106.579,42.227,106.693,42.264,106.77,42.289,106.906,42.309,107.091,42.322,107.292,42.349,107.749,42.401,107.806,42.406,108.062,42.427,108.171,42.447,108.334,42.437,108.546,42.429,108.687,42.416,108.875,42.426,109.132,42.441,109.34,42.438,109.443,42.456,109.596,42.511,109.698,42.554,109.859,42.606,110.058,42.661,110.197,42.71,110.289,42.743,110.4,42.774,110.43,42.814,110.462,42.844,110.521,42.895,110.628,42.991,110.709,43.074,110.749,43.111,110.84,43.194,110.913,43.257,111.007,43.341,111.087,43.369,111.187,43.392,111.451,43.475,111.504,43.493,111.547,43.496,111.641,43.563,111.72,43.621,111.771,43.665,111.878,43.68,111.933,43.711,111.943,43.752,111.932,43.815,111.88,43.879,111.837,43.935,111.684,44.041,111.603,44.107,111.52,44.192,111.486,44.272,111.43,44.322,111.402,44.367,111.411,44.419,111.489,44.512,111.515,44.57,111.547,44.673,111.621,44.827,111.681,44.899,111.751,44.97,111.898,45.064,112.033,45.082,112.113,45.063,112.292,45.063,112.411,45.058,112.499,45.011,112.597,44.918,112.707,44.883,113.049,44.81,113.196,44.795,113.301,44.792,113.456,44.767,113.508,44.762,113.587,44.746,113.653,44.763,113.752,44.826,113.877,44.896,113.931,44.912,114.03,44.943,114.08,44.971,114.167,45.05,114.281,45.111,114.419,45.203,114.487,45.272,114.502,45.316,114.517,45.365,114.56,45.39,114.644,45.413,114.739,45.42,114.919,45.378,115.163,45.39,115.217,45.396,115.439,45.42,115.539,45.44,115.681,45.458,115.789,45.535,115.934,45.626,116.04,45.677,116.11,45.687,116.198,45.739,116.241,45.796,116.229,45.846,116.213,45.887,116.265,45.963,116.358,46.097,116.445,46.159,116.517,46.209,116.563,46.29,116.619,46.313,116.689,46.322,116.787,46.377,116.859,46.388,116.979,46.362,117.156,46.355,117.269,46.352,117.333,46.362,117.357,46.391,117.356,46.437,117.392,46.538,117.406,46.571,117.438,46.586,117.547,46.588,117.621,46.552,117.671,46.522,117.741,46.518,117.813,46.538,117.91,46.619,118.071,46.667,118.157,46.679,118.309,46.717,118.404,46.703,118.58,46.692,118.649,46.702,118.723,46.692,118.79,46.747,118.844,46.76,118.957,46.735,119.029,46.692,119.162,46.639,119.332,46.614,119.474,46.627,119.62,46.604,119.707,46.606,119.747,46.627,119.867,46.672,119.896,46.733,119.884,46.791,119.898,46.858,119.863,46.907,119.788,46.979,119.76,47.027,119.757,47.09,119.711,47.15,119.6,47.222,119.527,47.256,119.377,47.381,119.326,47.41,119.309,47.431,119.291,47.473,119.235,47.493,119.162,47.525,119.123,47.558,119.097,47.616,119.082,47.654,119.018,47.685,118.953,47.703,118.88,47.725,118.76,47.758,118.691,47.822,118.568,47.943,118.498,47.984,118.24,48,118.147,48.029,118.042,48.019,117.979,48,117.84,48,117.768,47.988,117.677,47.908,117.555,47.805,117.455,47.741,117.384,47.676,117.351,47.652,117.286,47.666,117.197,47.74,117.07,47.806,116.952,47.837,116.901,47.853,116.761,47.87,116.652,47.865,116.513,47.84,116.378,47.844,116.317,47.86,116.231,47.858,116.075,47.79,115.994,47.711,115.898,47.687,115.812,47.738,115.712,47.799,115.616,47.875,115.558,47.945,115.525,48.131,115.639,48.186,115.786,48.248,115.797,48.346,115.792,48.456,115.821,48.577,115.954,48.689,116.025,48.782,116.034,48.84,116.098,48.936,116.16,49.037,116.243,49.17,116.402,49.406,116.59,49.685,116.683,49.824,116.889,49.738,117.022,49.693,117.246,49.625,117.477,49.609,117.698,49.536,117.813,49.514,117.873,49.513,118.187,49.693,118.452,49.844,118.756,49.963,118.979,49.979,119.147,50.013,119.26,50.066,119.326,50.155,119.346,50.279,119.302,50.354,119.192,50.38,119.164,50.406,119.217,50.433,119.256,50.484,119.281,50.561,119.344,50.634,119.446,50.703,119.502,50.779,119.512,50.863,119.573,50.947,119.685,51.03,119.746,51.108,119.757,51.179,119.813,51.267,119.967,51.422,120.067,51.601,120.237,51.723,120.511,51.849,120.681,51.973,120.75,52.097,120.745,52.205,120.665,52.3,120.65,52.396,120.699,52.494,120.656,52.567,120.521,52.615,120.36,52.627,120.173,52.602,120.068,52.633,120.044,52.718,120.095,52.787,120.218,52.84,120.421,52.968,120.704,53.172,120.985,53.285,121.405,53.317,121.744,53.384,122.089,53.451,122.338,53.485,122.38,53.463,122.516,53.457,122.745,53.469,122.958,53.498,123.154,53.545,123.31,53.556,123.424,53.531,123.489,53.529,123.535,53.526,123.56,53.527,123.608,53.547,123.741,53.511,123.995,53.406,124.154,53.359,124.22,53.37,124.291,53.341,124.369,53.271,124.466,53.23,124.64,53.211,124.812,53.134,124.882,53.13,124.907,53.173,124.971,53.197,125.075,53.204,125.226,53.166,125.422,53.084,125.546,53.048,125.596,53.057,125.649,53.042,125.692,53.004,125.695,52.956,125.681,52.931,125.728,52.891,125.783,52.891,125.872,52.872,125.942,52.801,126.004,52.768,126.048,52.739,126.056,52.716,126.06,52.692,126.047,52.673,126.023,52.643,126.016,52.61,126.046,52.573,126.157,52.547,126.194,52.519,126.203,52.484,126.238,52.445,126.313,52.4,126.342,52.362,126.324,52.332,126.346,52.306,126.383,52.287,126.392,52.215,126.395,52.173,126.456,52.126,126.468,52.031,126.511,51.926,126.654,51.781,126.701,51.703,126.689,51.61,126.709,51.566,126.775,51.545,126.805,51.506,126.802,51.448,126.827,51.412,126.848,51.374,126.834,51.315,126.854,51.261,126.888,51.23,126.912,51.172,126.925,51.1,127.02,50.986,127.198,50.829,127.307,50.708,127.347,50.621,127.347,50.55,127.308,50.494,127.306,50.454,127.341,50.428,127.351,50.394,127.337,50.35,127.395,50.299,127.59,50.209,127.512,50.072,127.492,49.975,127.502,49.873,127.551,49.802,127.637,49.76,127.69,49.717,127.711,49.672,127.814,49.622,128,49.569,128.237,49.559,128.527,49.594,128.704,49.6,128.769,49.577,128.791,49.542,128.77,49.495,128.819,49.464,128.938,49.449,129.02,49.419,129.065,49.375,129.12,49.362,129.185,49.381,129.248,49.379,129.31,49.354,129.35,49.362,129.385,49.389,129.441,49.389,129.498,49.389,129.534,49.323,129.591,49.287,129.671,49.279,129.793,49.199,130.037,48.972,130.196,48.892,130.355,48.866,130.553,48.861,130.617,48.773,130.566,48.68,130.552,48.602,130.597,48.575,130.659,48.483,130.747,48.43,130.763,48.388,130.804,48.342,130.787,48.255,130.712,48.128,130.733,48.019,130.849,47.929,130.915,47.843,130.933,47.76,130.962,47.709,131.003,47.691,131.122,47.698,131.319,47.728,131.464,47.723,131.557,47.682,131.785,47.681,132.15,47.718,132.38,47.729,132.476,47.715,132.562,47.769,132.637,47.89,132.707,47.947,132.773,47.94,132.877,47.979,133.02,48.064,133.144,48.106,133.301,48.102,133.468,48.097,133.573,48.133,133.672,48.208,133.842,48.274,134.206,48.36,134.293,48.373,134.335,48.369,134.456,48.355,134.564,48.322,134.665,48.254,134.681,48.21,134.669,48.153,134.647,48.12,134.605,48.083,134.566,48.023,134.591,47.975,134.65,47.874,134.699,47.801,134.752,47.715,134.728,47.684,134.696,47.625,134.596,47.524,134.542,47.485,134.483,47.447,134.383,47.438,134.339,47.429,134.291,47.414,134.26,47.378,134.225,47.353,134.168,47.302,134.163,47.259,134.189,47.194,134.202,47.128,134.137,47.069,134.086,46.978,134.071,46.951,134.046,46.882,134.039,46.858,134.023,46.713,133.958,46.614,133.867,46.499,133.887,46.431,133.903,46.367,133.88,46.336,133.875,46.309,133.861,46.248,133.833,46.224,133.75,46.186,133.701,46.14,133.711,46.07,133.686,46.009,133.648,45.955,133.608,45.92,133.551,45.898,133.513,45.879,133.485,45.81,133.476,45.758,133.449,45.705,133.466,45.651,133.436,45.605,133.355,45.572,133.31,45.553,133.267,45.545,133.186,45.495,133.113,45.321,133.097,45.22,133.113,45.131,133.012,45.075,132.936,45.03,132.889,45.046,132.839,45.061,132.723,45.081,132.666,45.094,132.549,45.123,132.363,45.16,132.181,45.203,132.067,45.226,131.978,45.244,131.909,45.274,131.852,45.327,131.795,45.305,131.742,45.243,131.654,45.205,131.614,45.137,131.579,45.084,131.488,45.013,131.447,44.984,131.268,44.936,131.228,44.92,131.082,44.91,131.033,44.889,130.982,44.844,130.968,44.8,131.004,44.753,131.061,44.66,131.087,44.596,131.126,44.469,131.255,44.072,131.213,44.003,131.174,43.705,131.184,43.651,131.18,43.567,131.182,43.506,131.209,43.49,131.244,43.469,131.262,43.433,131.257,43.378,131.239,43.338,131.212,43.258,131.176,43.142,131.136,43.098,131.109,43.062,131.086,43.038,131.083,42.956,131.069,42.902,131.006,42.883,130.943,42.852,130.869,42.863,130.803,42.857,130.722,42.836,130.577,42.812,130.493,42.779,130.453,42.755,130.425,42.727,130.42,42.7,130.439,42.686,130.521,42.674,130.577,42.623,130.584,42.567,130.527,42.535,130.498,42.571,130.45,42.582,130.361,42.631,130.296,42.685,130.247,42.745,130.249,42.873,130.24,42.892,130.151,42.918,130.125,42.956,130.083,42.974,130.022,42.963,129.977,42.975,129.941,42.996,129.898,42.998,129.861,42.965,129.842,42.894,129.779,42.777,129.773,42.705,129.746,42.604,129.72,42.475,129.698,42.448,129.628,42.444,129.604,42.436,129.568,42.392,129.524,42.385,129.485,42.41,129.424,42.436,129.366,42.439,129.314,42.414,129.253,42.358,129.218,42.313,129.205,42.271,129.196,42.218,129.134,42.169,129.077,42.142,128.961,42.069,128.923,42.038,128.84,42.038,128.749,42.041,128.627,42.021,128.427,42.011,128.308,42.026,128.16,42.012,128.045,41.988,128.029,41.952,128.033,41.898,128.056,41.864,128.084,41.841,128.132,41.769,128.182,41.7,128.258,41.655,128.289,41.607,128.291,41.563,128.255,41.507,128.2,41.433,128.149,41.388,128.111,41.389,128.053,41.416,128.013,41.449,127.919,41.461,127.688,41.44,127.572,41.455,127.517,41.482,127.42,41.484,127.271,41.52,127.18,41.531,127.137,41.555,127.128,41.607,127.085,41.644,127.061,41.687,127.007,41.742,126.955,41.769,126.904,41.781,126.847,41.748,126.788,41.718,126.743,41.725,126.722,41.717,126.697,41.692,126.601,41.641,126.578,41.594,126.54,41.496,126.514,41.394,126.49,41.358,126.451,41.352,126.412,41.321,126.329,41.226,126.254,41.138,126.145,41.078,126.093,41.024,126.067,40.974,125.989,40.905,125.875,40.892,125.784,40.872,125.728,40.867,125.688,40.839,125.659,40.796,125.645,40.779,125.594,40.779,125.543,40.743,125.417,40.66,125.314,40.645,125.186,40.589,125.073,40.547,125.026,40.524,125.013,40.498,124.997,40.465,124.942,40.458,124.889,40.46,124.772,40.384,124.712,40.319,124.481,40.182,124.387,40.104,124.362,40.004,124.35,40.012,124.267,39.924,124.106,39.841,123.76,39.822,123.651,39.882,123.611,39.841,123.581,39.786,123.49,39.768,123.348,39.763,123.269,39.727,123.227,39.687,123.032,39.674,122.961,39.62,122.84,39.601,122.335,39.366,122.225,39.267,122.121,39.152,122.048,39.094,121.982,39.053,121.923,39.037,121.864,38.996,121.805,38.991,121.745,39.01,121.677,39.003,121.633,38.955,121.67,38.892,121.65,38.865,121.517,38.831,121.32,38.808,121.236,38.767,121.207,38.744,121.164,38.732,121.122,38.813,121.107,38.921,121.188,38.947,121.263,38.96,121.68,39.109,121.628,39.22,121.665,39.269,121.758,39.348,121.818,39.387,121.785,39.401,121.513,39.375,121.356,39.377,121.275,39.385,121.3,39.452,121.286,39.519,121.267,39.545,121.406,39.621,121.47,39.64,121.518,39.639,121.514,39.685,121.474,39.755,121.517,39.845,121.801,39.951,121.869,40.046,121.983,40.136,122.191,40.358,122.203,40.396,122.264,40.5,122.275,40.542,122.179,40.603,122.14,40.688,121.859,40.842,121.835,40.974,121.809,40.969,121.766,40.876,121.729,40.846,121.599,40.843,121.537,40.878,121.175,40.901,121.086,40.842,121.003,40.749,120.922,40.683,120.841,40.649,120.771,40.589,120.479,40.231,120.369,40.204,119.85,39.987,119.591,39.903,119.391,39.752,119.322,39.662,119.261,39.561,119.225,39.408,119.04,39.222,118.977,39.183,118.912,39.166,118.826,39.172,118.752,39.16,118.626,39.177,118.472,39.118,118.298,39.067,118.148,39.195,118.041,39.227,117.866,39.191,117.785,39.134,117.617,38.853,117.554,38.691,117.558,38.625,117.656,38.424,117.767,38.312,118.015,38.183,118.543,38.095,118.667,38.126,118.8,38.127,118.94,38.043,119.028,37.904,119.036,37.809,119.038,37.777,119.07,37.749,119.089,37.701,119.033,37.661,118.991,37.641,118.955,37.494,118.953,37.331,118.998,37.277,119.112,37.201,119.287,37.138,119.45,37.125,119.761,37.155,119.888,37.253,119.88,37.296,119.883,37.351,120.156,37.495,120.312,37.623,120.287,37.656,120.257,37.679,120.285,37.692,120.37,37.701,120.75,37.834,121.049,37.725,121.22,37.6,121.388,37.579,121.505,37.515,121.64,37.46,121.816,37.457,121.965,37.445,122.01,37.496,122.057,37.529,122.11,37.522,122.169,37.456,122.338,37.405,122.493,37.408,122.602,37.426,122.667,37.403,122.573,37.318,122.587,37.181,122.516,37.138,122.447,37.068,122.487,37.022,122.523,37.003,122.52,36.947,122.457,36.915,122.341,36.832,122.274,36.834,122.242,36.85,122.22,36.88,122.203,36.927,122.162,36.959,122.05,36.971,121.933,36.959,121.67,36.836,121.413,36.738,121.144,36.66,121.054,36.611,120.99,36.598,120.879,36.635,120.811,36.633,120.797,36.607,120.883,36.539,120.905,36.485,120.896,36.444,120.847,36.426,120.776,36.456,120.712,36.413,120.682,36.341,120.681,36.168,120.638,36.13,120.519,36.109,120.393,36.054,120.348,36.079,120.33,36.11,120.343,36.189,120.328,36.228,120.27,36.226,120.183,36.202,120.117,36.15,120.094,36.119,120.181,36.017,120.265,36.007,120.285,35.984,120.219,35.935,120.055,35.861,120.027,35.799,119.979,35.74,119.912,35.693,119.866,35.644,119.811,35.618,119.72,35.589,119.608,35.47,119.526,35.359,119.43,35.301,119.353,35.114,119.216,35.012,119.165,34.849,119.201,34.748,119.351,34.749,119.427,34.714,119.583,34.582,119.77,34.496,119.964,34.448,120.201,34.326,120.267,34.274,120.323,34.169,120.426,33.866,120.5,33.716,120.505,33.638,120.616,33.491,120.734,33.237,120.871,33.017,120.897,32.843,120.853,32.764,120.853,32.661,120.99,32.567,121.293,32.457,121.342,32.425,121.401,32.372,121.404,32.206,121.451,32.153,121.491,32.121,121.674,32.051,121.751,31.993,121.832,31.9,121.856,31.816,121.866,31.704,121.764,31.7,121.681,31.712,121.352,31.859,121.266,31.863,121.146,31.842,120.974,31.869,120.792,32.032,120.661,32.081,120.52,32.106,120.184,31.966,120.099,31.976,120.074,31.96,120.036,31.936,120.192,31.906,120.347,31.952,120.497,32.02,120.716,31.984,120.752,31.923,120.788,31.82,120.938,31.75,121.055,31.719,121.205,31.628,121.351,31.485,121.661,31.32,121.786,31.163,121.834,31.062,121.878,30.917,121.769,30.87,121.675,30.864,121.528,30.841,121.419,30.79,121.31,30.7,120.998,30.558,120.938,30.47,120.897,30.393,120.821,30.355,120.63,30.391,120.45,30.388,120.246,30.284,120.195,30.241,120.229,30.25,120.261,30.263,120.353,30.247,120.495,30.303,120.633,30.133,120.904,30.161,121.159,30.302,121.258,30.304,121.341,30.282,121.433,30.227,121.678,29.979,121.812,29.952,121.944,29.894,122.017,29.888,122.083,29.87,121.906,29.78,121.677,29.584,121.575,29.537,121.506,29.485,121.69,29.511,121.822,29.605,121.888,29.628,121.941,29.606,121.968,29.491,121.918,29.135,121.854,29.129,121.791,29.226,121.717,29.256,121.656,29.236,121.534,29.237,121.487,29.193,121.448,29.131,121.521,29.118,121.665,29.011,121.68,28.953,121.641,28.916,121.54,28.932,121.663,28.851,121.63,28.768,121.59,28.735,121.519,28.714,121.475,28.641,121.538,28.521,121.602,28.367,121.61,28.292,121.51,28.324,121.355,28.23,121.272,28.222,121.217,28.346,121.146,28.327,121.098,28.291,121.035,28.157,120.959,28.037,120.892,28.004,120.813,28.013,120.748,28.01,120.763,27.977,120.833,27.938,120.833,27.891,120.685,27.745,120.661,27.688,120.665,27.639,120.588,27.581,120.629,27.482,120.608,27.412,120.54,27.318,120.469,27.256,120.385,27.156,120.279,27.097,120.139,26.886,120.097,26.781,120.087,26.672,120.043,26.634,119.968,26.586,119.882,26.61,119.879,26.683,119.842,26.689,119.821,26.737,119.815,26.798,119.824,26.846,119.789,26.831,119.767,26.775,119.71,26.729,119.652,26.747,119.588,26.785,119.59,26.73,119.624,26.676,119.638,26.621,119.726,26.609,119.785,26.547,119.831,26.45,119.84,26.414,119.876,26.371,119.881,26.334,119.797,26.3,119.693,26.236,119.567,26.127,119.463,26.055,119.37,26.054,119.313,26.063,119.232,26.104,119.139,26.122,119.264,25.975,119.332,25.949,119.418,25.954,119.501,26.009,119.619,26.004,119.648,25.919,119.617,25.823,119.553,25.699,119.539,25.591,119.619,25.437,119.622,25.391,119.593,25.368,119.499,25.409,119.422,25.46,119.344,25.446,119.263,25.468,119.18,25.45,119.146,25.414,119.169,25.356,119.244,25.307,119.286,25.232,119.236,25.206,119.025,25.223,118.978,25.209,118.914,25.127,118.956,25.005,118.909,24.929,118.822,24.911,118.708,24.85,118.637,24.836,118.64,24.809,118.692,24.782,118.719,24.746,118.657,24.621,118.56,24.58,118.412,24.601,118.295,24.573,118.195,24.626,118.087,24.627,118.014,24.56,118.006,24.482,117.935,24.474,117.897,24.48,117.843,24.474,117.848,24.432,117.879,24.396,118.024,24.38,118.051,24.327,118.056,24.246,117.904,24.106,117.839,24.012,117.742,24.015,117.668,23.939,117.628,23.837,117.579,23.857,117.466,23.841,117.433,23.792,117.46,23.771,117.462,23.736,117.417,23.621,117.368,23.589,117.347,23.636,117.331,23.709,117.291,23.714,117.225,23.647,117.148,23.599,117.083,23.579,117.033,23.623,116.911,23.647,116.861,23.453,116.76,23.383,116.712,23.36,116.629,23.354,116.682,23.327,116.699,23.278,116.669,23.228,116.586,23.218,116.538,23.18,116.52,23.007,116.471,22.946,116.346,22.941,116.252,22.981,116.222,22.95,116.206,22.919,116.157,22.887,116.063,22.879,115.852,22.802,115.756,22.824,115.64,22.853,115.561,22.825,115.535,22.765,115.498,22.719,115.383,22.719,115.29,22.776,115.196,22.817,115.092,22.782,115.012,22.709,114.914,22.685,114.896,22.64,114.854,22.617,114.75,22.626,114.711,22.739,114.652,22.755,114.593,22.698,114.572,22.654,114.544,22.621,114.554,22.529,114.496,22.527,114.42,22.583,114.341,22.593,114.266,22.541,114.228,22.554,114.188,22.565,114.123,22.565,114.098,22.551,114.05,22.543,114.018,22.514,114.015,22.512,113.931,22.531,113.828,22.607,113.754,22.734,113.661,22.802,113.62,22.861,113.603,22.969,113.586,23.02,113.592,23.077,113.621,23.127,113.52,23.102,113.445,23.055,113.46,22.996,113.442,22.941,113.331,22.912,113.338,22.889,113.345,22.865,113.432,22.789,113.45,22.726,113.485,22.692,113.553,22.594,113.551,22.404,113.589,22.35,113.576,22.297,113.549,22.225,113.547,22.224,113.527,22.246,113.494,22.242,113.481,22.217,113.479,22.196,113.473,22.194,113.416,22.178,113.367,22.165,113.328,22.145,113.266,22.089,113.149,22.075,113.089,22.208,113.008,22.119,112.984,21.938,112.954,21.907,112.904,21.881,112.809,21.945,112.725,21.902,112.661,21.859,112.634,21.82,112.586,21.777,112.495,21.818,112.421,21.881,112.439,21.927,112.429,21.958,112.396,21.981,112.36,21.978,112.377,21.917,112.39,21.801,112.356,21.768,112.305,21.742,112.193,21.763,112.117,21.806,112.025,21.843,111.944,21.85,111.926,21.776,111.873,21.717,111.825,21.71,111.776,21.719,111.712,21.655,111.682,21.608,111.603,21.559,111.392,21.535,111.319,21.486,111.221,21.494,111.144,21.482,111.101,21.485,111.061,21.511,111.017,21.512,110.997,21.43,110.878,21.396,110.771,21.387,110.652,21.279,110.567,21.214,110.504,21.207,110.458,21.231,110.435,21.327,110.411,21.338,110.397,21.248,110.375,21.172,110.331,21.131,110.194,21.038,110.154,20.945,110.18,20.859,110.365,20.838,110.388,20.791,110.371,20.752,110.326,20.72,110.313,20.672,110.512,20.518,110.518,20.46,110.487,20.427,110.45,20.355,110.345,20.295,110.123,20.264,109.938,20.295,109.883,20.364,109.886,20.413,109.932,20.399,109.984,20.403,109.968,20.448,109.946,20.474,109.861,20.514,109.792,20.622,109.805,20.711,109.767,20.781,109.726,20.839,109.685,20.874,109.663,20.917,109.704,21.053,109.681,21.132,109.76,21.228,109.78,21.337,109.921,21.376,109.931,21.481,109.83,21.484,109.759,21.56,109.743,21.528,109.687,21.525,109.594,21.672,109.566,21.691,109.521,21.693,109.544,21.538,109.436,21.479,109.347,21.454,109.22,21.443,109.149,21.426,109.082,21.44,109.098,21.487,109.133,21.544,109.102,21.59,109.031,21.627,108.922,21.624,108.846,21.634,108.772,21.63,108.744,21.651,108.675,21.725,108.616,21.77,108.589,21.816,108.616,21.869,108.594,21.901,108.48,21.905,108.481,21.829,108.493,21.739,108.526,21.671,108.502,21.633,108.444,21.607,108.383,21.679,108.355,21.697,108.325,21.694,108.302,21.622,108.246,21.558,108.146,21.565,108.067,21.526,107.973,21.508]],[[110.889,19.992,110.938,19.948,110.971,19.883,110.998,19.765,111.014,19.655,110.913,19.586,110.822,19.558,110.641,19.291,110.603,19.207,110.572,19.172,110.563,19.135,110.566,19.099,110.519,18.97,110.478,18.813,110.451,18.748,110.4,18.698,110.334,18.673,110.291,18.67,110.252,18.656,110.156,18.57,110.049,18.505,110.066,18.476,110.067,18.448,110.02,18.416,109.968,18.422,109.816,18.397,109.76,18.348,109.703,18.259,109.681,18.247,109.59,18.226,109.519,18.218,109.4,18.281,109.341,18.3,109.183,18.325,109.03,18.368,108.922,18.416,108.702,18.535,108.676,18.75,108.638,18.866,108.636,18.908,108.65,19.265,108.666,19.304,108.694,19.338,108.791,19.418,108.903,19.481,109.063,19.614,109.179,19.674,109.277,19.761,109.22,19.757,109.177,19.768,109.219,19.843,109.263,19.883,109.315,19.904,109.418,19.889,109.514,19.904,109.584,19.97,109.651,19.984,109.906,19.963,110.083,19.993,110.172,20.054,110.213,20.056,110.344,20.039,110.392,19.976,110.388,20.018,110.394,20.059,110.418,20.055,110.588,19.976,110.589,20.072,110.598,20.098,110.652,20.138,110.679,20.137,110.745,20.059,110.809,20.014,110.889,19.992]]]},{"n":"Macau","c":"MO","r":4,"ml":4,"lx":113.56,"ly":22.13,"b":[113.479,22.196,113.548,22.246],"p":[[[113.479,22.196,113.481,22.217,113.494,22.242,113.527,22.246,113.548,22.223,113.546,22.221,113.499,22.202,113.484,22.198,113.479,22.196]]]},{"n":"Hong Kong","c":"HK","r":4,"ml":4,"lx":114.1,"ly":22.45,"b":[113.839,22.195,114.335,22.565],"p":[[[114.015,22.512,114.018,22.514,114.05,22.543,114.098,22.551,114.123,22.565,114.188,22.565,114.228,22.554,114.266,22.541,114.27,22.537,114.291,22.499,114.285,22.458,114.325,22.437,114.335,22.396,114.291,22.374,114.288,22.325,114.268,22.296,114.139,22.348,114.033,22.376,113.937,22.365,113.903,22.396,113.896,22.428,114.007,22.484,114.015,22.512]],[[114.232,22.211,114.207,22.195,114.139,22.268,114.134,22.292,114.187,22.297,114.247,22.264,114.244,22.234,114.232,22.211]],[[113.998,22.21,113.877,22.21,113.852,22.22,113.839,22.242,113.882,22.28,114.044,22.333,114.003,22.278,113.998,22.21]]]},{"n":"Chile","c":"CL","r":2,"ml":1.7,"lx":-72.32,"ly":-38.15,"b":[-109.434,-55.892,-66.436,-17.506],"p":[[[-109.28,-27.14,-109.434,-27.171,-109.429,-27.116,-109.39,-27.068,-109.276,-27.096,-109.223,-27.101,-109.28,-27.14]],[[-78.804,-33.646,-78.983,-33.668,-78.989,-33.662,-78.979,-33.644,-78.938,-33.614,-78.888,-33.576,-78.877,-33.575,-78.859,-33.578,-78.838,-33.585,-78.785,-33.61,-78.769,-33.627,-78.775,-33.642,-78.804,-33.646]],[[-70.418,-18.346,-70.377,-18.334,-70.282,-18.325,-70.184,-18.325,-70.059,-18.283,-69.926,-18.206,-69.84,-18.093,-69.803,-17.99,-69.802,-17.9,-69.842,-17.785,-69.852,-17.704,-69.806,-17.665,-69.685,-17.65,-69.586,-17.573,-69.511,-17.506,-69.495,-17.62,-69.358,-17.772,-69.313,-17.943,-69.282,-17.965,-69.094,-18.05,-69.09,-18.071,-69.118,-18.103,-69.145,-18.144,-69.126,-18.202,-69.092,-18.282,-69.081,-18.357,-69.06,-18.433,-69.039,-18.55,-69.027,-18.656,-68.979,-18.813,-68.969,-18.91,-68.968,-18.968,-68.931,-19.025,-68.858,-19.093,-68.759,-19.162,-68.681,-19.242,-68.621,-19.297,-68.548,-19.341,-68.492,-19.382,-68.47,-19.41,-68.463,-19.433,-68.487,-19.454,-68.575,-19.56,-68.698,-19.721,-68.696,-19.741,-68.578,-19.857,-68.559,-19.902,-68.561,-19.967,-68.6,-20.045,-68.727,-20.07,-68.756,-20.091,-68.759,-20.116,-68.73,-20.148,-68.735,-20.225,-68.689,-20.31,-68.712,-20.339,-68.759,-20.378,-68.761,-20.416,-68.745,-20.459,-68.696,-20.493,-68.5,-20.612,-68.484,-20.628,-68.487,-20.641,-68.563,-20.72,-68.571,-20.769,-68.569,-20.85,-68.558,-20.902,-68.534,-20.924,-68.435,-20.948,-68.314,-21.13,-68.197,-21.3,-68.199,-21.447,-68.186,-21.619,-68.112,-21.753,-68.102,-21.861,-68.077,-21.983,-67.988,-22.057,-67.954,-22.204,-67.945,-22.282,-67.95,-22.334,-67.882,-22.493,-67.874,-22.631,-67.89,-22.729,-67.889,-22.784,-67.879,-22.823,-67.821,-22.858,-67.794,-22.879,-67.707,-22.889,-67.58,-22.892,-67.362,-22.855,-67.195,-22.822,-67.009,-23.001,-67.09,-23.245,-67.219,-23.634,-67.319,-23.935,-67.336,-23.975,-67.356,-24.034,-67.572,-24.119,-67.886,-24.243,-68.047,-24.308,-68.25,-24.392,-68.3,-24.46,-68.358,-24.497,-68.423,-24.545,-68.447,-24.597,-68.507,-24.63,-68.562,-24.747,-68.562,-24.838,-68.527,-24.899,-68.466,-24.925,-68.447,-24.999,-68.428,-25.051,-68.384,-25.092,-68.395,-25.125,-68.431,-25.149,-68.496,-25.163,-68.541,-25.237,-68.592,-25.42,-68.6,-25.486,-68.542,-25.652,-68.511,-25.741,-68.427,-26.065,-68.415,-26.154,-68.53,-26.277,-68.576,-26.352,-68.592,-26.418,-68.592,-26.47,-68.581,-26.518,-68.485,-26.67,-68.373,-26.806,-68.319,-26.878,-68.319,-26.973,-68.346,-27.028,-68.405,-27.048,-68.537,-27.085,-68.592,-27.14,-68.652,-27.148,-68.71,-27.104,-68.77,-27.115,-68.846,-27.154,-68.875,-27.247,-68.942,-27.405,-68.999,-27.449,-69.042,-27.57,-69.119,-27.744,-69.155,-27.848,-69.174,-27.925,-69.251,-27.974,-69.341,-28.071,-69.41,-28.165,-69.437,-28.193,-69.489,-28.201,-69.527,-28.286,-69.657,-28.414,-69.688,-28.562,-69.735,-28.641,-69.743,-28.784,-69.815,-29.046,-69.828,-29.103,-69.9,-29.149,-69.996,-29.25,-70.027,-29.324,-69.983,-29.545,-69.928,-29.769,-69.924,-29.874,-69.945,-30.016,-69.96,-30.078,-69.924,-30.104,-69.863,-30.12,-69.844,-30.175,-69.888,-30.213,-69.907,-30.282,-69.956,-30.358,-70.102,-30.388,-70.153,-30.361,-70.17,-30.386,-70.161,-30.44,-70.194,-30.505,-70.269,-30.677,-70.319,-30.834,-70.348,-30.902,-70.336,-30.96,-70.312,-30.993,-70.309,-31.023,-70.351,-31.06,-70.388,-31.121,-70.429,-31.129,-70.473,-31.113,-70.52,-31.148,-70.529,-31.223,-70.555,-31.317,-70.566,-31.428,-70.585,-31.569,-70.526,-31.666,-70.45,-31.842,-70.394,-31.884,-70.331,-31.881,-70.282,-31.917,-70.254,-31.958,-70.291,-32.031,-70.356,-32.042,-70.364,-32.083,-70.345,-32.176,-70.32,-32.267,-70.258,-32.31,-70.23,-32.431,-70.17,-32.472,-70.177,-32.626,-70.116,-32.807,-70.052,-32.86,-70.022,-32.885,-70.042,-32.964,-70.093,-33.027,-70.104,-33.128,-70.085,-33.202,-70.02,-33.271,-69.969,-33.279,-69.896,-33.251,-69.82,-33.284,-69.809,-33.344,-69.798,-33.399,-69.839,-33.47,-69.883,-33.601,-69.894,-33.731,-69.881,-33.93,-69.862,-34.084,-69.857,-34.18,-69.852,-34.224,-69.88,-34.254,-69.946,-34.27,-70.003,-34.276,-70.052,-34.301,-70.063,-34.35,-70.101,-34.432,-70.141,-34.493,-70.211,-34.581,-70.255,-34.673,-70.29,-34.733,-70.287,-34.775,-70.312,-34.855,-70.338,-34.922,-70.393,-35.147,-70.467,-35.194,-70.525,-35.217,-70.555,-35.247,-70.532,-35.308,-70.47,-35.326,-70.449,-35.375,-70.457,-35.452,-70.416,-35.523,-70.42,-35.609,-70.38,-35.772,-70.416,-35.879,-70.404,-35.971,-70.405,-36.062,-70.457,-36.133,-70.563,-36.146,-70.622,-36.212,-70.722,-36.283,-70.733,-36.341,-70.749,-36.393,-70.79,-36.412,-70.853,-36.412,-70.905,-36.42,-70.978,-36.487,-71.056,-36.524,-71.073,-36.578,-71.066,-36.644,-71.107,-36.685,-71.159,-36.762,-71.192,-36.844,-71.159,-36.92,-71.124,-37.057,-71.118,-37.114,-71.163,-37.227,-71.2,-37.3,-71.165,-37.393,-71.135,-37.445,-71.163,-37.559,-71.187,-37.631,-71.168,-37.762,-71.096,-37.91,-71.028,-38.041,-71.018,-38.194,-71,-38.315,-70.968,-38.446,-70.9,-38.498,-70.848,-38.542,-70.859,-38.604,-70.897,-38.681,-70.952,-38.738,-71.087,-38.758,-71.197,-38.809,-71.286,-38.845,-71.353,-38.889,-71.402,-38.935,-71.426,-38.986,-71.409,-39.206,-71.42,-39.287,-71.465,-39.402,-71.508,-39.495,-71.526,-39.523,-71.531,-39.564,-71.539,-39.602,-71.587,-39.611,-71.654,-39.594,-71.693,-39.605,-71.72,-39.635,-71.697,-39.707,-71.672,-39.833,-71.638,-39.887,-71.647,-39.929,-71.66,-40.021,-71.704,-40.095,-71.764,-40.095,-71.802,-40.125,-71.818,-40.177,-71.801,-40.244,-71.723,-40.3,-71.695,-40.335,-71.709,-40.382,-71.769,-40.401,-71.805,-40.439,-71.839,-40.524,-71.884,-40.621,-71.932,-40.692,-71.941,-40.789,-71.873,-40.893,-71.881,-40.995,-71.886,-41.292,-71.892,-41.393,-71.871,-41.561,-71.898,-41.607,-71.911,-41.65,-71.844,-41.772,-71.77,-41.969,-71.75,-42.047,-71.761,-42.101,-71.861,-42.148,-71.944,-42.167,-71.993,-42.134,-72.026,-42.148,-72.064,-42.205,-72.108,-42.252,-72.125,-42.298,-72.078,-42.358,-72.053,-42.473,-72.105,-42.522,-72.144,-42.577,-72.13,-42.648,-72.114,-42.777,-72.146,-42.99,-72.102,-43.066,-72.055,-43.102,-71.899,-43.145,-71.781,-43.167,-71.751,-43.237,-71.764,-43.295,-71.82,-43.323,-71.905,-43.348,-71.905,-43.44,-71.832,-43.527,-71.751,-43.59,-71.733,-43.647,-71.737,-43.705,-71.795,-43.753,-71.716,-43.858,-71.68,-43.93,-71.716,-43.984,-71.767,-44.067,-71.812,-44.106,-71.812,-44.151,-71.831,-44.241,-71.835,-44.33,-71.82,-44.383,-71.326,-44.425,-71.213,-44.441,-71.151,-44.494,-71.16,-44.56,-71.221,-44.631,-71.261,-44.763,-71.358,-44.785,-71.455,-44.75,-71.56,-44.762,-71.652,-44.77,-71.783,-44.774,-71.957,-44.792,-72.064,-44.772,-72.073,-44.82,-72.042,-44.904,-71.812,-44.931,-71.596,-44.979,-71.531,-45.068,-71.443,-45.168,-71.354,-45.23,-71.349,-45.332,-71.49,-45.438,-71.508,-45.513,-71.693,-45.535,-71.746,-45.579,-71.773,-45.724,-71.751,-45.839,-71.68,-45.879,-71.632,-45.954,-71.684,-46.042,-71.809,-46.103,-71.876,-46.161,-71.834,-46.207,-71.778,-46.28,-71.762,-46.32,-71.731,-46.428,-71.695,-46.578,-71.7,-46.651,-71.733,-46.706,-71.856,-46.792,-71.94,-46.831,-71.957,-46.937,-71.963,-47.016,-71.954,-47.087,-71.901,-47.144,-71.905,-47.202,-71.979,-47.214,-72.042,-47.241,-72.103,-47.343,-72.283,-47.446,-72.346,-47.493,-72.342,-47.572,-72.413,-47.686,-72.472,-47.784,-72.518,-47.876,-72.509,-47.973,-72.408,-48.016,-72.328,-48.11,-72.293,-48.229,-72.355,-48.366,-72.498,-48.417,-72.583,-48.475,-72.608,-48.519,-72.586,-48.662,-72.592,-48.73,-72.614,-48.793,-72.651,-48.842,-72.728,-48.896,-72.865,-48.944,-72.982,-48.977,-73.034,-49.014,-73.095,-49.097,-73.149,-49.188,-73.135,-49.301,-73.462,-49.314,-73.484,-49.398,-73.554,-49.464,-73.576,-49.583,-73.505,-49.698,-73.47,-49.795,-73.529,-49.911,-73.508,-50.03,-73.501,-50.125,-73.387,-50.231,-73.312,-50.362,-73.274,-50.473,-73.252,-50.558,-73.222,-50.611,-73.175,-50.67,-73.153,-50.738,-73.082,-50.76,-72.956,-50.696,-72.866,-50.653,-72.804,-50.638,-72.62,-50.648,-72.51,-50.608,-72.46,-50.612,-72.393,-50.634,-72.34,-50.682,-72.301,-50.79,-72.276,-50.91,-72.307,-51.033,-72.359,-51.06,-72.377,-51.095,-72.359,-51.17,-72.302,-51.223,-72.303,-51.299,-72.366,-51.47,-72.408,-51.541,-72.335,-51.62,-72.269,-51.691,-72.137,-51.744,-72.028,-51.819,-71.953,-51.88,-71.971,-51.964,-71.919,-51.99,-71.717,-51.991,-71.415,-51.994,-70.943,-51.998,-70.483,-52.002,-69.96,-52.008,-69.713,-52.075,-69.488,-52.136,-69.206,-52.136,-68.925,-52.208,-68.715,-52.255,-68.59,-52.273,-68.461,-52.29,-68.443,-52.357,-69.007,-52.263,-69.134,-52.211,-69.241,-52.205,-69.447,-52.269,-69.561,-52.422,-69.62,-52.465,-69.763,-52.506,-69.907,-52.514,-70.391,-52.661,-70.563,-52.673,-70.68,-52.712,-70.795,-52.769,-70.839,-52.89,-70.821,-52.963,-70.952,-53.227,-70.984,-53.374,-70.985,-53.448,-70.948,-53.57,-70.996,-53.779,-71.083,-53.825,-71.298,-53.883,-71.444,-53.841,-71.694,-53.803,-71.872,-53.723,-72.101,-53.666,-72.174,-53.632,-72.377,-53.471,-72.398,-53.418,-72.413,-53.35,-72.306,-53.254,-72.249,-53.247,-72.081,-53.25,-71.942,-53.234,-71.853,-53.286,-71.828,-53.398,-71.867,-53.458,-71.903,-53.496,-71.892,-53.524,-71.791,-53.485,-71.741,-53.233,-71.4,-53.107,-71.289,-53.034,-71.18,-52.921,-71.163,-52.888,-71.155,-52.846,-71.227,-52.811,-71.388,-52.764,-71.898,-53.002,-72.129,-53.064,-72.278,-53.132,-72.458,-53.254,-72.493,-53.291,-72.531,-53.372,-72.549,-53.461,-72.727,-53.42,-72.998,-53.291,-73.053,-53.243,-72.998,-53.177,-72.916,-53.122,-72.91,-52.937,-72.889,-52.872,-72.832,-52.82,-72.728,-52.762,-72.676,-52.749,-72.632,-52.774,-72.627,-52.818,-72.453,-52.814,-72.118,-52.65,-71.979,-52.646,-71.797,-52.683,-71.591,-52.661,-71.554,-52.644,-71.511,-52.605,-71.665,-52.56,-71.812,-52.537,-72.226,-52.521,-72.315,-52.539,-72.438,-52.626,-72.478,-52.604,-72.504,-52.56,-72.645,-52.529,-72.712,-52.536,-72.777,-52.577,-72.766,-52.643,-72.802,-52.712,-72.932,-52.782,-73.02,-52.892,-73.016,-52.977,-73.023,-53.022,-73.055,-53.046,-73.122,-53.074,-73.338,-53.055,-73.46,-52.965,-73.508,-52.904,-73.645,-52.837,-73.346,-52.754,-73.241,-52.707,-73.145,-52.602,-73.073,-52.535,-73.124,-52.488,-73.184,-52.488,-73.178,-52.563,-73.244,-52.624,-73.382,-52.595,-73.586,-52.686,-73.711,-52.662,-73.915,-52.688,-74.014,-52.639,-74.036,-52.577,-74,-52.513,-74.037,-52.403,-74.094,-52.376,-74.151,-52.383,-74.177,-52.317,-74.238,-52.202,-74.266,-52.171,-74.296,-52.118,-74.265,-52.105,-74.195,-52.12,-74.134,-52.155,-74.04,-52.159,-73.834,-52.234,-73.749,-52.216,-73.703,-52.199,-73.685,-52.137,-73.684,-52.078,-73.649,-52.078,-73.532,-52.153,-73.458,-52.146,-73.327,-52.166,-73.26,-52.158,-73.137,-52.13,-72.944,-52.047,-72.843,-51.961,-72.795,-51.95,-72.735,-51.961,-72.695,-51.985,-72.695,-52.045,-72.65,-52.1,-72.588,-52.145,-72.571,-52.2,-72.583,-52.254,-72.694,-52.33,-72.714,-52.357,-72.677,-52.385,-72.631,-52.372,-72.569,-52.334,-72.533,-52.282,-72.523,-52.255,-72.519,-52.217,-72.524,-52.17,-72.614,-52.037,-72.625,-52.007,-72.625,-51.946,-72.523,-51.891,-72.494,-51.848,-72.49,-51.764,-72.543,-51.706,-72.761,-51.573,-73.127,-51.44,-73.169,-51.454,-73.197,-51.478,-73.163,-51.496,-73.115,-51.504,-72.789,-51.614,-72.705,-51.678,-72.649,-51.695,-72.583,-51.737,-72.6,-51.799,-72.928,-51.86,-73.189,-51.991,-73.383,-52.07,-73.518,-52.041,-73.582,-51.96,-73.65,-51.856,-73.753,-51.796,-73.811,-51.801,-73.858,-51.79,-73.894,-51.758,-73.973,-51.784,-74.146,-51.712,-74.197,-51.681,-74.07,-51.579,-73.93,-51.618,-73.896,-51.331,-73.94,-51.266,-74.121,-51.195,-74.21,-51.205,-74.332,-51.195,-74.414,-51.162,-74.508,-51.15,-74.587,-51.131,-74.69,-51.087,-74.815,-51.063,-74.983,-50.881,-75.055,-50.786,-75.095,-50.681,-74.837,-50.679,-74.686,-50.662,-74.649,-50.618,-74.702,-50.535,-74.776,-50.47,-74.722,-50.408,-74.644,-50.361,-74.564,-50.382,-74.366,-50.488,-74.331,-50.56,-74.19,-50.778,-74.156,-50.797,-74.139,-50.818,-73.847,-50.94,-73.807,-50.938,-73.825,-50.836,-73.741,-50.697,-73.659,-50.651,-73.618,-50.651,-73.614,-50.628,-73.693,-50.57,-73.654,-50.493,-73.68,-50.49,-73.75,-50.54,-73.892,-50.783,-73.978,-50.827,-74.097,-50.717,-74.164,-50.638,-74.197,-50.61,-74.186,-50.485,-73.95,-50.511,-74.031,-50.47,-74.306,-50.398,-74.374,-50.363,-74.425,-50.35,-74.516,-50.266,-74.63,-50.194,-74.434,-50.065,-74.334,-49.975,-74.019,-50.023,-73.959,-49.995,-74.011,-49.929,-74.073,-49.949,-74.171,-49.907,-74.324,-49.783,-74.319,-49.72,-74.291,-49.604,-74.23,-49.579,-74.102,-49.555,-73.956,-49.593,-73.892,-49.624,-73.836,-49.609,-73.892,-49.523,-73.988,-49.491,-74.094,-49.43,-74.083,-49.362,-74.049,-49.306,-74.023,-49.244,-74.006,-49.158,-74.015,-49.091,-73.985,-49.06,-73.938,-49.046,-73.935,-49.021,-74.028,-49.026,-74.061,-49.111,-74.074,-49.188,-74.14,-49.25,-74.168,-49.321,-74.185,-49.404,-74.221,-49.501,-74.302,-49.464,-74.349,-49.426,-74.367,-49.4,-74.358,-49.351,-74.38,-49.048,-74.382,-48.794,-74.341,-48.596,-74.228,-48.517,-74.176,-48.494,-74.129,-48.504,-74.057,-48.504,-74.009,-48.475,-74.172,-48.427,-74.27,-48.455,-74.343,-48.493,-74.474,-48.464,-74.499,-48.362,-74.577,-48.274,-74.591,-48.162,-74.585,-47.999,-74.4,-48.013,-74.25,-48.045,-73.854,-48.042,-73.528,-48.198,-73.384,-48.177,-73.391,-48.146,-73.501,-48.107,-73.57,-48.019,-73.61,-47.994,-73.629,-47.942,-73.635,-47.88,-73.716,-47.655,-73.748,-47.661,-73.779,-47.738,-73.847,-47.867,-73.941,-47.929,-74.085,-47.955,-74.227,-47.969,-74.351,-47.944,-74.379,-47.891,-74.376,-47.83,-74.43,-47.8,-74.569,-47.773,-74.609,-47.758,-74.655,-47.702,-74.588,-47.618,-74.534,-47.568,-74.467,-47.578,-74.404,-47.6,-74.323,-47.667,-74.243,-47.679,-74.152,-47.627,-74.134,-47.591,-74.191,-47.568,-74.243,-47.56,-74.324,-47.531,-74.483,-47.43,-74.403,-47.328,-74.216,-47.21,-74.158,-47.183,-74.208,-47.083,-74.152,-46.974,-74.209,-46.885,-74.314,-46.788,-74.454,-46.767,-74.484,-46.795,-74.489,-46.835,-74.467,-46.864,-74.48,-46.886,-74.512,-46.885,-74.691,-46.864,-74.811,-46.8,-75.006,-46.741,-75.031,-46.695,-75.053,-46.628,-74.984,-46.512,-75.019,-46.511,-75.146,-46.6,-75.337,-46.647,-75.478,-46.662,-75.54,-46.699,-75.565,-46.729,-75.528,-46.746,-75.446,-46.751,-75.386,-46.863,-75.401,-46.906,-75.43,-46.935,-75.497,-46.94,-75.635,-46.863,-75.708,-46.775,-75.706,-46.705,-75.657,-46.61,-75.437,-46.483,-75.376,-46.429,-75.247,-46.369,-75.075,-46.235,-74.924,-46.16,-74.998,-46.098,-75.075,-46.004,-75.067,-45.875,-74.763,-45.824,-74.631,-45.845,-74.463,-45.841,-74.369,-45.81,-74.301,-45.803,-74.158,-45.767,-74.096,-45.717,-74.082,-45.678,-74.083,-45.645,-74.099,-45.603,-74.123,-45.496,-74.099,-45.46,-74.038,-45.418,-73.957,-45.404,-73.92,-45.408,-73.825,-45.447,-73.844,-45.502,-73.882,-45.569,-73.96,-45.835,-74,-45.895,-74.061,-45.947,-74.02,-46.056,-74.082,-46.132,-74.357,-46.213,-74.393,-46.217,-74.372,-46.246,-74.213,-46.239,-74.09,-46.222,-73.968,-46.154,-73.929,-46.05,-73.879,-45.847,-73.813,-45.818,-73.735,-45.812,-73.695,-45.86,-73.708,-45.967,-73.708,-46.07,-73.811,-46.377,-73.935,-46.501,-73.949,-46.533,-73.944,-46.572,-73.845,-46.566,-73.77,-46.5,-73.716,-46.415,-73.662,-46.297,-73.668,-46.212,-73.652,-46.159,-73.629,-45.987,-73.592,-45.899,-73.594,-45.777,-73.662,-45.731,-73.757,-45.703,-73.78,-45.628,-73.731,-45.48,-73.55,-45.484,-73.379,-45.383,-73.266,-45.346,-73.202,-45.354,-72.978,-45.451,-72.934,-45.452,-72.941,-45.417,-72.976,-45.393,-73.064,-45.36,-73.226,-45.255,-73.445,-45.238,-73.405,-45.102,-73.362,-44.978,-73.256,-44.961,-73.078,-44.92,-72.739,-44.734,-72.68,-44.594,-72.664,-44.436,-72.828,-44.395,-73.001,-44.292,-73.141,-44.237,-73.265,-44.169,-73.241,-44.066,-73.224,-43.898,-73.069,-43.862,-72.997,-43.632,-73.101,-43.455,-73.076,-43.324,-72.94,-43.211,-72.915,-43.134,-72.878,-43.048,-72.758,-43.039,-72.755,-42.993,-72.766,-42.908,-72.845,-42.808,-72.848,-42.669,-72.774,-42.505,-72.655,-42.517,-72.632,-42.51,-72.716,-42.41,-72.785,-42.301,-72.773,-42.258,-72.707,-42.221,-72.631,-42.2,-72.548,-42.256,-72.43,-42.434,-72.412,-42.388,-72.46,-42.207,-72.499,-41.981,-72.624,-42.011,-72.738,-41.995,-72.781,-41.96,-72.824,-41.909,-72.784,-41.847,-72.744,-41.801,-72.66,-41.742,-72.486,-41.722,-72.36,-41.649,-72.318,-41.499,-72.359,-41.514,-72.428,-41.646,-72.542,-41.691,-72.601,-41.684,-72.67,-41.659,-72.805,-41.544,-72.88,-41.518,-72.953,-41.515,-73.015,-41.544,-73.174,-41.747,-73.242,-41.781,-73.521,-41.797,-73.624,-41.774,-73.735,-41.742,-73.722,-41.692,-73.688,-41.639,-73.625,-41.612,-73.624,-41.581,-73.711,-41.574,-73.811,-41.517,-73.855,-41.446,-73.876,-41.319,-73.966,-41.118,-73.984,-40.974,-73.92,-40.872,-73.784,-40.468,-73.742,-40.263,-73.669,-40.082,-73.671,-39.963,-73.482,-39.854,-73.41,-39.789,-73.25,-39.422,-73.226,-39.224,-73.481,-38.624,-73.52,-38.509,-73.533,-38.367,-73.472,-38.13,-73.465,-38.04,-73.517,-37.911,-73.662,-37.699,-73.665,-37.59,-73.603,-37.479,-73.662,-37.341,-73.634,-37.255,-73.602,-37.188,-73.375,-37.224,-73.271,-37.207,-73.216,-37.167,-73.173,-37.054,-73.151,-36.876,-73.138,-36.8,-73.118,-36.688,-73.007,-36.643,-72.968,-36.538,-72.875,-36.39,-72.778,-35.979,-72.683,-35.877,-72.587,-35.76,-72.624,-35.586,-72.562,-35.505,-72.505,-35.447,-72.455,-35.341,-72.387,-35.24,-72.224,-35.096,-72.182,-34.92,-72.056,-34.616,-72.031,-34.421,-71.992,-34.288,-72.003,-34.165,-71.927,-34.016,-71.854,-33.89,-71.831,-33.82,-71.664,-33.653,-71.636,-33.519,-71.696,-33.429,-71.697,-33.289,-71.743,-33.095,-71.636,-33.023,-71.592,-32.97,-71.452,-32.66,-71.461,-32.538,-71.421,-32.387,-71.513,-32.208,-71.526,-31.806,-71.577,-31.496,-71.662,-31.17,-71.654,-30.987,-71.706,-30.759,-71.709,-30.628,-71.669,-30.33,-71.4,-30.143,-71.348,-29.933,-71.316,-29.65,-71.327,-29.443,-71.353,-29.35,-71.486,-29.198,-71.519,-28.926,-71.494,-28.855,-71.384,-28.779,-71.307,-28.672,-71.267,-28.508,-71.186,-28.378,-71.154,-28.064,-71.087,-27.814,-71.053,-27.727,-70.946,-27.618,-70.926,-27.589,-70.909,-27.505,-70.914,-27.308,-70.898,-27.187,-70.813,-26.951,-70.803,-26.841,-70.708,-26.597,-70.687,-26.422,-70.647,-26.329,-70.662,-26.225,-70.635,-25.993,-70.7,-25.861,-70.714,-25.784,-70.633,-25.546,-70.578,-25.487,-70.49,-25.376,-70.452,-25.252,-70.445,-25.173,-70.559,-24.779,-70.574,-24.644,-70.546,-24.332,-70.507,-24.13,-70.52,-23.969,-70.507,-23.886,-70.488,-23.782,-70.41,-23.656,-70.392,-23.566,-70.42,-23.529,-70.512,-23.483,-70.588,-23.368,-70.593,-23.255,-70.569,-23.173,-70.563,-23.057,-70.45,-23.034,-70.389,-22.97,-70.332,-22.849,-70.26,-22.556,-70.229,-22.193,-70.185,-21.975,-70.155,-21.867,-70.13,-21.641,-70.088,-21.493,-70.08,-21.357,-70.088,-21.253,-70.197,-20.725,-70.194,-20.531,-70.147,-20.23,-70.148,-19.805,-70.157,-19.706,-70.198,-19.613,-70.21,-19.487,-70.276,-19.268,-70.335,-18.828,-70.336,-18.595,-70.362,-18.398,-70.418,-18.346]],[[-68.63,-52.653,-68.632,-52.95,-68.633,-53.242,-68.635,-53.515,-68.637,-53.789,-68.638,-54.053,-68.64,-54.324,-68.648,-54.628,-68.653,-54.854,-68.804,-54.854,-68.844,-54.877,-69.082,-54.91,-69.486,-54.859,-69.588,-54.813,-69.723,-54.712,-69.772,-54.739,-69.899,-54.782,-70.031,-54.816,-70.138,-54.819,-70.238,-54.778,-70.259,-54.756,-70.282,-54.752,-70.497,-54.81,-70.735,-54.751,-70.925,-54.714,-71.229,-54.694,-71.441,-54.62,-71.832,-54.626,-71.902,-54.602,-71.928,-54.529,-71.907,-54.495,-71.823,-54.474,-71.8,-54.434,-71.716,-54.444,-71.606,-54.497,-71.573,-54.495,-71.5,-54.445,-71.393,-54.4,-71.355,-54.395,-71.159,-54.451,-71.08,-54.444,-70.966,-54.42,-70.946,-54.398,-70.928,-54.36,-70.898,-54.338,-70.797,-54.327,-70.699,-54.349,-70.688,-54.415,-70.701,-54.485,-70.573,-54.504,-70.418,-54.502,-70.311,-54.529,-70.298,-54.486,-70.468,-54.373,-70.54,-54.303,-70.636,-54.262,-70.76,-54.241,-70.863,-54.11,-70.857,-53.996,-70.868,-53.884,-70.644,-53.823,-70.696,-53.727,-70.619,-53.655,-70.531,-53.627,-70.443,-53.893,-70.38,-53.987,-70.461,-54.006,-70.63,-54.006,-70.535,-54.136,-70.38,-54.181,-70.246,-54.277,-70.243,-54.348,-70.169,-54.379,-69.99,-54.381,-69.867,-54.367,-69.809,-54.321,-69.742,-54.306,-69.622,-54.364,-69.419,-54.407,-69.365,-54.438,-69.325,-54.488,-69.322,-54.543,-69.312,-54.571,-69.253,-54.557,-69.169,-54.483,-69.128,-54.458,-69.077,-54.445,-69.045,-54.428,-69.044,-54.407,-69.196,-54.354,-69.988,-54.109,-70.086,-54.011,-70.151,-53.888,-70.149,-53.761,-70.091,-53.722,-69.95,-53.672,-69.69,-53.601,-69.39,-53.499,-69.352,-53.48,-69.356,-53.416,-69.394,-53.373,-69.513,-53.342,-69.637,-53.334,-69.756,-53.337,-69.874,-53.35,-70.09,-53.418,-70.213,-53.414,-70.329,-53.378,-70.416,-53.305,-70.46,-53.206,-70.46,-53.143,-70.443,-53.086,-70.391,-53.026,-70.32,-53.001,-70.256,-53.004,-70.196,-52.99,-70.161,-52.97,-70.131,-52.943,-70.14,-52.919,-70.163,-52.899,-70.259,-52.857,-70.297,-52.817,-70.38,-52.752,-70.335,-52.734,-70.19,-52.724,-70.088,-52.769,-69.994,-52.821,-69.935,-52.821,-69.883,-52.799,-69.764,-52.731,-69.663,-52.646,-69.572,-52.549,-69.498,-52.491,-69.414,-52.486,-69.167,-52.668,-69.08,-52.674,-68.79,-52.577,-68.758,-52.582,-68.659,-52.632,-68.63,-52.653]],[[-67.08,-55.154,-67.109,-55.192,-67.173,-55.243,-67.257,-55.282,-67.34,-55.293,-67.399,-55.272,-67.429,-55.237,-67.443,-55.201,-67.463,-55.182,-67.495,-55.177,-67.535,-55.179,-67.585,-55.192,-67.691,-55.243,-67.737,-55.256,-67.768,-55.26,-68.07,-55.221,-68.1,-55.207,-68.135,-55.173,-68.174,-55.071,-68.301,-54.981,-68.107,-54.929,-67.874,-54.93,-67.425,-54.969,-67.245,-54.978,-67.107,-55.064,-67.085,-55.115,-67.08,-55.154]],[[-73.773,-43.346,-73.849,-43.367,-73.919,-43.372,-73.99,-43.357,-74.114,-43.358,-74.239,-43.319,-74.355,-43.264,-74.387,-43.232,-74.373,-43.186,-74.289,-43.079,-74.209,-42.879,-74.156,-42.591,-74.199,-42.481,-74.194,-42.436,-74.174,-42.382,-74.164,-42.325,-74.17,-42.269,-74.16,-42.216,-74.072,-42.106,-74.059,-42.056,-74.057,-42.002,-74.019,-41.891,-74.031,-41.854,-74.063,-41.823,-74.037,-41.796,-73.731,-41.877,-73.528,-41.896,-73.517,-41.981,-73.478,-42.047,-73.454,-42.166,-73.423,-42.193,-73.439,-42.278,-73.533,-42.314,-73.525,-42.393,-73.471,-42.466,-73.549,-42.493,-73.634,-42.508,-73.653,-42.529,-73.715,-42.545,-73.789,-42.586,-73.767,-42.622,-73.673,-42.704,-73.568,-42.762,-73.511,-42.847,-73.436,-42.937,-73.473,-42.993,-73.541,-43.074,-73.65,-43.127,-73.75,-43.159,-73.738,-43.291,-73.773,-43.346]],[[-74.476,-49.148,-74.467,-49.295,-74.484,-49.442,-74.522,-49.623,-74.516,-49.66,-74.471,-49.669,-74.459,-49.691,-74.472,-49.786,-74.496,-49.859,-74.543,-49.919,-74.57,-49.991,-74.595,-50.007,-74.703,-50.019,-74.763,-50.011,-74.811,-49.93,-74.825,-49.879,-74.822,-49.814,-74.88,-49.726,-74.882,-49.692,-74.859,-49.634,-74.812,-49.605,-74.805,-49.516,-74.781,-49.489,-74.727,-49.452,-74.719,-49.437,-74.724,-49.424,-74.744,-49.422,-74.96,-49.533,-74.981,-49.564,-74.991,-49.606,-74.994,-49.752,-75.032,-49.836,-75.066,-49.852,-75.167,-49.856,-75.3,-49.847,-75.451,-49.77,-75.55,-49.791,-75.57,-49.697,-75.521,-49.622,-75.337,-49.628,-75.306,-49.494,-75.364,-49.462,-75.429,-49.408,-75.467,-49.359,-75.433,-49.322,-75.327,-49.269,-75.27,-49.263,-75.217,-49.293,-75.086,-49.27,-75.094,-49.185,-75.21,-49.148,-75.184,-49.084,-75.037,-49.022,-74.949,-48.96,-74.945,-48.889,-74.981,-48.819,-74.97,-48.791,-74.896,-48.733,-74.793,-48.705,-74.747,-48.709,-74.652,-48.75,-74.567,-48.755,-74.546,-48.767,-74.531,-48.813,-74.476,-49.148]],[[-75.51,-48.763,-75.623,-48.765,-75.651,-48.586,-75.518,-48.329,-75.509,-48.231,-75.554,-48.157,-75.571,-48.096,-75.561,-48.071,-75.391,-48.02,-75.338,-48.074,-75.275,-48.218,-75.156,-48.425,-75.158,-48.623,-75.225,-48.671,-75.434,-48.721,-75.51,-48.763]],[[-74.386,-52.922,-74.369,-52.931,-74.33,-52.929,-74.275,-52.946,-74.066,-52.965,-73.879,-53.012,-73.782,-53.056,-73.654,-53.07,-73.549,-53.126,-73.505,-53.14,-73.451,-53.144,-73.31,-53.248,-73.302,-53.259,-73.143,-53.341,-73.135,-53.354,-73.226,-53.358,-73.409,-53.321,-73.501,-53.318,-73.567,-53.307,-73.583,-53.3,-73.596,-53.253,-73.617,-53.23,-73.794,-53.121,-73.867,-53.097,-73.994,-53.076,-74.139,-53.091,-74.236,-53.076,-74.27,-53.082,-74.414,-52.995,-74.558,-52.922,-74.62,-52.835,-74.712,-52.768,-74.712,-52.749,-74.67,-52.734,-74.572,-52.771,-74.475,-52.836,-74.422,-52.86,-74.386,-52.922]],[[-74.567,-48.592,-74.586,-48.616,-74.71,-48.601,-74.923,-48.626,-75.013,-48.536,-75.052,-48.391,-75.079,-48.362,-75.132,-48.279,-75.158,-48.225,-75.213,-48.142,-75.234,-48.053,-75.247,-48.027,-75.198,-47.975,-74.975,-47.923,-74.896,-47.839,-74.827,-47.85,-74.846,-48.021,-74.805,-48.078,-74.729,-48.126,-74.715,-48.146,-74.702,-48.206,-74.664,-48.299,-74.615,-48.343,-74.602,-48.37,-74.6,-48.393,-74.618,-48.425,-74.567,-48.592]],[[-72.923,-53.482,-72.896,-53.563,-72.882,-53.578,-72.809,-53.565,-72.685,-53.558,-72.482,-53.588,-72.459,-53.599,-72.373,-53.687,-72.307,-53.725,-72.205,-53.807,-72.306,-53.862,-72.366,-53.941,-72.369,-53.981,-72.409,-54.004,-72.471,-54.028,-72.563,-54.074,-72.677,-54.079,-72.789,-54.103,-72.84,-54.125,-72.871,-54.127,-72.907,-54.115,-72.946,-54.092,-72.959,-54.066,-72.882,-54.042,-72.782,-53.955,-72.764,-53.865,-72.872,-53.849,-72.936,-53.861,-72.984,-53.861,-73.039,-53.833,-73.073,-53.875,-73.086,-53.916,-73.071,-53.978,-73.081,-53.998,-73.12,-54.009,-73.211,-53.986,-73.305,-53.944,-73.312,-53.92,-73.293,-53.836,-73.295,-53.792,-73.314,-53.729,-73.325,-53.723,-73.36,-53.724,-73.471,-53.736,-73.582,-53.655,-73.642,-53.57,-73.845,-53.546,-73.687,-53.427,-73.447,-53.41,-73.366,-53.47,-73.099,-53.512,-73.115,-53.448,-73.111,-53.425,-73.074,-53.397,-73.054,-53.394,-73.022,-53.415,-72.971,-53.423,-72.947,-53.442,-72.923,-53.482]],[[-74.823,-51.63,-74.78,-51.825,-74.75,-51.852,-74.647,-51.866,-74.537,-51.965,-74.532,-51.992,-74.666,-52.16,-74.694,-52.279,-74.852,-52.271,-74.918,-52.152,-75.017,-52.038,-75.051,-51.904,-75.105,-51.789,-75.008,-51.724,-74.915,-51.738,-74.91,-51.65,-74.823,-51.63]],[[-69.703,-54.919,-68.901,-55.018,-68.654,-54.958,-68.458,-54.96,-68.4,-55.042,-68.598,-55.128,-68.613,-55.156,-68.586,-55.178,-68.382,-55.192,-68.33,-55.219,-68.283,-55.255,-68.323,-55.308,-68.327,-55.333,-68.305,-55.357,-68.153,-55.437,-68.09,-55.478,-68.058,-55.518,-68.046,-55.587,-68.048,-55.643,-68.083,-55.651,-68.157,-55.634,-68.23,-55.602,-68.293,-55.521,-68.338,-55.505,-68.467,-55.489,-68.594,-55.45,-68.694,-55.452,-68.785,-55.436,-68.867,-55.45,-68.896,-55.424,-68.931,-55.371,-68.932,-55.347,-68.889,-55.263,-68.89,-55.241,-68.913,-55.239,-69.008,-55.256,-69.047,-55.244,-69.151,-55.183,-69.193,-55.172,-69.297,-55.166,-69.356,-55.274,-69.359,-55.301,-69.299,-55.369,-69.181,-55.475,-69.241,-55.477,-69.412,-55.444,-69.456,-55.424,-69.509,-55.371,-69.61,-55.34,-69.646,-55.321,-69.656,-55.298,-69.657,-55.229,-69.68,-55.219,-69.824,-55.237,-69.854,-55.22,-69.866,-55.191,-69.887,-55.174,-69.98,-55.147,-69.988,-55.131,-69.947,-55.111,-69.921,-55.061,-69.884,-54.882,-69.703,-54.919]],[[-71.39,-54.033,-71.169,-54.113,-71.022,-54.112,-71.023,-54.162,-71.005,-54.247,-71.028,-54.281,-71.083,-54.316,-71.118,-54.366,-71.143,-54.374,-71.305,-54.314,-71.473,-54.231,-71.558,-54.246,-71.671,-54.225,-71.761,-54.23,-71.818,-54.276,-71.949,-54.301,-71.972,-54.207,-72.092,-54.119,-72.21,-54.048,-72.146,-53.939,-72.069,-53.921,-71.996,-53.885,-71.705,-53.923,-71.554,-53.956,-71.39,-54.033]],[[-73.735,-44.395,-73.785,-44.437,-73.862,-44.445,-73.983,-44.495,-73.996,-44.538,-74.002,-44.591,-73.919,-44.655,-73.877,-44.729,-73.828,-44.84,-73.792,-44.946,-73.795,-44.979,-73.786,-45.034,-73.727,-45.119,-73.722,-45.158,-73.728,-45.196,-73.752,-45.267,-73.771,-45.277,-73.83,-45.283,-73.834,-45.327,-73.849,-45.341,-74.016,-45.345,-74.099,-45.325,-74.089,-45.196,-74.195,-45.145,-74.268,-45.059,-74.35,-44.911,-74.419,-44.865,-74.499,-44.748,-74.618,-44.648,-74.481,-44.585,-74.502,-44.474,-74.422,-44.435,-74.301,-44.396,-74.212,-44.427,-74.133,-44.416,-74.097,-44.389,-74.108,-44.276,-74.083,-44.186,-73.995,-44.14,-73.9,-44.135,-73.865,-44.185,-73.818,-44.235,-73.703,-44.274,-73.704,-44.325,-73.735,-44.395]],[[-72.986,-44.78,-73.228,-44.86,-73.35,-44.833,-73.397,-44.774,-73.42,-44.725,-73.445,-44.641,-73.404,-44.596,-73.315,-44.531,-73.282,-44.49,-73.266,-44.44,-73.272,-44.394,-73.26,-44.35,-73.208,-44.335,-73.028,-44.384,-72.842,-44.458,-72.776,-44.509,-72.764,-44.549,-72.845,-44.638,-72.897,-44.712,-72.986,-44.78]],[[-75.042,-44.89,-75.067,-44.907,-75.099,-44.902,-75.124,-44.87,-75.142,-44.816,-75.107,-44.795,-75.079,-44.795,-75.048,-44.824,-75.032,-44.871,-75.042,-44.89]],[[-75.302,-50.68,-75.33,-50.772,-75.411,-50.764,-75.439,-50.741,-75.453,-50.683,-75.477,-50.654,-75.443,-50.596,-75.42,-50.53,-75.428,-50.481,-75.304,-50.484,-75.156,-50.497,-75.115,-50.51,-75.16,-50.554,-75.203,-50.581,-75.292,-50.597,-75.302,-50.68]],[[-75.107,-48.837,-75.115,-48.916,-75.263,-49.069,-75.39,-49.159,-75.506,-49.231,-75.58,-49.23,-75.641,-49.195,-75.573,-49.139,-75.488,-49.082,-75.515,-49.01,-75.54,-48.988,-75.576,-48.981,-75.638,-48.943,-75.619,-48.886,-75.583,-48.859,-75.535,-48.838,-75.49,-48.85,-75.297,-48.811,-75.236,-48.779,-75.119,-48.773,-75.107,-48.837]],[[-75.112,-47.838,-75.186,-47.851,-75.194,-47.818,-75.261,-47.764,-75.203,-47.728,-75.09,-47.691,-75.004,-47.695,-74.926,-47.723,-74.916,-47.757,-75.051,-47.8,-75.084,-47.825,-75.112,-47.838]],[[-74.669,-43.608,-74.81,-43.625,-74.843,-43.596,-74.842,-43.57,-74.818,-43.549,-74.745,-43.536,-74.697,-43.553,-74.673,-43.577,-74.665,-43.6,-74.669,-43.608]],[[-73.632,-44.821,-73.665,-44.833,-73.695,-44.831,-73.725,-44.797,-73.735,-44.752,-73.8,-44.684,-73.818,-44.652,-73.817,-44.614,-73.779,-44.559,-73.724,-44.544,-73.686,-44.546,-73.641,-44.611,-73.628,-44.681,-73.617,-44.753,-73.632,-44.821]],[[-74.559,-51.277,-74.561,-51.361,-74.593,-51.387,-74.62,-51.396,-74.691,-51.37,-74.731,-51.367,-74.797,-51.412,-74.853,-51.434,-74.937,-51.428,-75.047,-51.398,-75.146,-51.524,-75.192,-51.567,-75.289,-51.625,-75.3,-51.556,-75.238,-51.454,-75.21,-51.383,-75.154,-51.279,-75.04,-51.318,-74.881,-51.279,-74.737,-51.208,-74.612,-51.207,-74.571,-51.245,-74.559,-51.277]],[[-75.055,-50.296,-75.25,-50.376,-75.308,-50.343,-75.449,-50.343,-75.412,-50.257,-75.398,-50.193,-75.377,-50.168,-75.369,-50.113,-75.327,-50.012,-75.21,-50.045,-75.123,-50.055,-75.004,-50.089,-74.876,-50.11,-74.839,-50.197,-74.963,-50.237,-75.055,-50.296]],[[-73.811,-43.827,-73.79,-43.876,-73.834,-43.883,-73.904,-43.875,-73.938,-43.914,-73.956,-43.922,-74.118,-43.887,-74.143,-43.872,-74.14,-43.821,-73.967,-43.817,-73.857,-43.784,-73.841,-43.789,-73.811,-43.827]],[[-74.142,-51.931,-74.172,-51.942,-74.283,-51.919,-74.339,-51.898,-74.424,-51.845,-74.437,-51.791,-74.475,-51.726,-74.451,-51.725,-74.362,-51.751,-74.326,-51.77,-74.277,-51.812,-74.133,-51.871,-74.115,-51.888,-74.119,-51.911,-74.142,-51.931]],[[-74.313,-45.692,-74.368,-45.736,-74.466,-45.757,-74.562,-45.722,-74.678,-45.739,-74.69,-45.663,-74.646,-45.6,-74.558,-45.526,-74.495,-45.426,-74.502,-45.285,-74.45,-45.253,-74.422,-45.203,-74.311,-45.173,-74.285,-45.277,-74.315,-45.464,-74.24,-45.575,-74.229,-45.611,-74.244,-45.654,-74.313,-45.692]],[[-67.575,-55.89,-67.611,-55.892,-67.7,-55.873,-67.832,-55.865,-67.846,-55.857,-67.85,-55.843,-67.834,-55.828,-67.762,-55.816,-67.545,-55.826,-67.517,-55.833,-67.51,-55.844,-67.545,-55.877,-67.575,-55.89]],[[-66.472,-55.229,-66.552,-55.273,-66.611,-55.27,-66.63,-55.254,-66.637,-55.234,-66.625,-55.213,-66.6,-55.194,-66.542,-55.169,-66.523,-55.166,-66.436,-55.19,-66.472,-55.229]],[[-70.992,-54.868,-70.945,-54.931,-70.928,-54.943,-70.805,-54.968,-70.749,-54.953,-70.615,-54.946,-70.535,-54.921,-70.418,-54.909,-70.283,-55.066,-70.298,-55.114,-70.404,-55.166,-70.476,-55.177,-70.543,-55.161,-70.539,-55.135,-70.551,-55.112,-70.597,-55.082,-70.641,-55.085,-70.711,-55.107,-70.744,-55.104,-70.815,-55.08,-70.94,-55.062,-70.965,-55.04,-70.967,-55.007,-70.991,-54.99,-71.12,-54.938,-71.203,-54.893,-71.274,-54.887,-71.299,-54.892,-71.325,-54.914,-71.389,-54.934,-71.407,-54.931,-71.427,-54.914,-71.437,-54.889,-71.411,-54.839,-71.374,-54.835,-71.197,-54.844,-71.089,-54.867,-70.992,-54.868]],[[-67.289,-55.777,-67.325,-55.785,-67.352,-55.766,-67.393,-55.753,-67.56,-55.725,-67.563,-55.708,-67.546,-55.684,-67.513,-55.662,-67.449,-55.641,-67.397,-55.585,-67.374,-55.589,-67.351,-55.612,-67.31,-55.689,-67.262,-55.744,-67.267,-55.763,-67.289,-55.777]]]},{"n":"Chad","c":"TD","r":3,"ml":3,"lx":18.65,"ly":15.14,"b":[13.448,7.475,23.983,23.445],"p":[[[23.98,19.497,23.981,19.051,23.981,18.605,23.981,18.158,23.982,17.712,23.982,17.266,23.983,16.82,23.983,16.374,23.983,15.928,23.983,15.78,23.971,15.722,23.965,15.713,23.946,15.704,23.708,15.745,23.604,15.746,23.458,15.714,23.243,15.697,23.105,15.703,23.009,15.626,22.934,15.533,22.97,15.311,22.961,15.238,22.932,15.162,22.867,15.097,22.802,15.044,22.763,14.999,22.715,14.898,22.679,14.851,22.682,14.789,22.671,14.722,22.632,14.688,22.532,14.663,22.468,14.633,22.416,14.585,22.382,14.55,22.4,14.504,22.425,14.441,22.439,14.342,22.449,14.284,22.498,14.237,22.528,14.203,22.539,14.162,22.51,14.127,22.388,14.056,22.339,14.029,22.283,13.992,22.262,13.979,22.173,13.911,22.128,13.85,22.106,13.8,22.108,13.73,22.153,13.626,22.202,13.538,22.221,13.472,22.233,13.399,22.228,13.33,22.203,13.269,22.158,13.215,21.99,13.113,21.908,13.001,21.842,12.865,21.825,12.791,21.843,12.741,21.878,12.699,21.928,12.678,22.001,12.672,22.121,12.695,22.233,12.709,22.352,12.66,22.414,12.546,22.39,12.463,22.435,12.312,22.475,12.129,22.472,12.068,22.49,12.045,22.564,12.033,22.581,11.99,22.556,11.67,22.591,11.58,22.641,11.516,22.697,11.483,22.754,11.44,22.783,11.41,22.849,11.403,22.923,11.345,22.943,11.267,22.938,11.192,22.895,11.029,22.86,10.92,22.817,10.927,22.73,10.954,22.624,10.977,22.494,10.996,22.37,10.952,22.236,10.894,22.194,10.851,22.156,10.826,22.097,10.83,22.043,10.823,22.014,10.782,21.965,10.737,21.771,10.643,21.731,10.609,21.707,10.575,21.707,10.538,21.726,10.462,21.726,10.367,21.683,10.29,21.633,10.238,21.576,10.219,21.528,10.208,21.497,10.176,21.396,10.001,21.352,9.969,21.264,9.975,21.009,9.713,20.984,9.636,20.891,9.527,20.773,9.406,20.668,9.347,20.66,9.325,20.631,9.301,20.567,9.275,20.342,9.127,20.073,9.133,19.954,9.075,19.838,9.049,19.668,9.021,19.617,9.024,19.4,9.012,19.146,9.016,19.048,8.995,18.956,8.939,18.888,8.89,18.878,8.873,18.889,8.852,18.886,8.836,19.064,8.715,19.109,8.656,19.064,8.599,19.042,8.59,19.04,8.587,19.011,8.541,18.906,8.405,18.747,8.244,18.666,8.198,18.634,8.168,18.592,8.061,18.564,8.046,18.455,8.032,18.239,8.02,17.94,7.985,17.761,7.974,17.649,7.984,17.493,7.91,17.436,7.891,17.402,7.885,17.247,7.813,17.118,7.702,17.072,7.681,16.89,7.634,16.818,7.557,16.785,7.551,16.668,7.652,16.589,7.743,16.55,7.836,16.545,7.865,16.523,7.86,16.459,7.819,16.404,7.772,16.379,7.684,16.191,7.623,16.031,7.572,15.958,7.508,15.845,7.475,15.701,7.488,15.589,7.515,15.48,7.524,15.532,7.604,15.553,7.665,15.558,7.738,15.55,7.788,15.484,7.813,15.443,7.852,15.349,8.084,15.252,8.322,15.116,8.557,14.968,8.707,14.861,8.799,14.826,8.81,14.771,8.839,14.733,8.866,14.536,9.025,14.332,9.204,14.28,9.285,14.178,9.406,14.064,9.532,14.005,9.589,13.977,9.692,14.056,9.784,14.14,9.902,14.243,9.98,14.377,9.985,14.598,9.953,14.836,9.942,15.072,9.966,15.133,9.983,15.193,9.981,15.32,9.954,15.541,9.96,15.655,10.008,15.532,10.088,15.4,10.217,15.276,10.357,15.201,10.485,15.132,10.648,15.069,10.851,15.03,11.114,15.036,11.263,15.055,11.369,15.122,11.541,15.078,11.643,15.088,11.724,15.081,11.846,15.06,11.907,14.974,12.108,14.957,12.13,14.881,12.269,14.847,12.502,14.761,12.656,14.623,12.73,14.545,12.82,14.516,12.98,14.462,13.022,14.245,13.077,14.064,13.079,13.932,13.258,13.763,13.49,13.606,13.705,13.506,14.134,13.448,14.381,13.514,14.456,13.642,14.631,13.807,14.966,14.178,15.485,14.368,15.75,14.747,16.147,15.212,16.634,15.474,16.908,15.517,17.408,15.562,17.937,15.596,18.337,15.638,18.811,15.673,19.207,15.699,19.495,15.735,19.904,15.766,19.983,15.949,20.303,15.963,20.346,15.929,20.4,15.668,20.672,15.587,20.733,15.54,20.875,15.607,20.954,15.294,21.412,15.216,21.467,15.182,21.523,15.178,21.606,15.172,21.922,15.089,22.418,14.979,22.996,15.347,23.161,15.627,23.286,15.984,23.445,16.315,23.282,16.794,23.045,17.273,22.809,17.752,22.572,18.231,22.336,18.71,22.099,19.189,21.862,19.669,21.626,20.148,21.389,20.627,21.153,21.106,20.916,21.585,20.679,22.064,20.443,22.543,20.206,23.022,19.97,23.501,19.733,23.98,19.497]]]},{"n":"Central African Republic","c":"CF","r":4,"ml":4,"lx":20.91,"ly":6.99,"b":[14.431,2.27,27.403,10.996],"p":[[[24.147,8.666,24.195,8.653,24.221,8.608,24.18,8.461,24.208,8.369,24.291,8.291,24.375,8.258,24.456,8.239,24.737,8.192,24.853,8.138,25.007,7.965,25.2,7.808,25.247,7.725,25.239,7.649,25.181,7.557,25.19,7.519,25.279,7.427,25.381,7.333,25.567,7.229,25.889,7.065,26.037,6.955,26.087,6.872,26.169,6.782,26.285,6.699,26.362,6.635,26.309,6.455,26.325,6.396,26.353,6.345,26.421,6.274,26.447,6.183,26.514,6.069,26.594,6.018,26.726,5.998,26.796,5.946,26.942,5.855,27.083,5.777,27.144,5.723,27.181,5.675,27.213,5.619,27.229,5.563,27.233,5.441,27.257,5.29,27.332,5.186,27.403,5.109,27.115,5.198,27.072,5.2,27.021,5.184,26.87,5.076,26.822,5.062,26.768,5.072,26.633,5.085,26.174,5.171,25.82,5.254,25.714,5.284,25.525,5.312,25.4,5.256,25.283,5.063,25.249,5.025,25.065,4.967,24.978,4.983,24.766,4.93,24.437,5.01,24.32,4.994,24.228,4.954,23.992,4.866,23.848,4.816,23.682,4.771,23.524,4.701,23.417,4.663,23.313,4.664,23.219,4.703,23.116,4.737,22.993,4.744,22.865,4.724,22.756,4.647,22.712,4.592,22.617,4.446,22.506,4.208,22.462,4.16,22.45,4.155,22.422,4.135,21.908,4.254,21.687,4.281,21.538,4.245,21.35,4.311,21.268,4.323,21.23,4.302,21.126,4.332,20.956,4.413,20.793,4.447,20.647,4.436,20.558,4.463,20.487,4.542,20.394,4.686,20.226,4.83,20.002,4.945,19.863,5.031,19.807,5.089,19.686,5.121,19.501,5.127,19.323,5.071,19.069,4.891,18.832,4.523,18.7,4.383,18.594,4.346,18.567,4.258,18.62,4.117,18.634,3.954,18.597,3.679,18.61,3.478,18.554,3.51,18.5,3.604,18.474,3.623,18.318,3.581,18.237,3.543,18.194,3.505,18.161,3.5,18.111,3.551,18.072,3.56,18.011,3.551,17.948,3.552,17.907,3.558,17.88,3.554,17.807,3.584,17.538,3.662,17.492,3.687,17.438,3.685,17.298,3.617,17.225,3.598,17.003,3.557,16.764,3.536,16.673,3.535,16.611,3.505,16.57,3.463,16.543,3.37,16.496,3.209,16.477,3.165,16.48,3.101,16.466,2.993,16.46,2.897,16.469,2.832,16.401,2.701,16.32,2.543,16.252,2.407,16.183,2.27,16.136,2.364,16.107,2.473,16.096,2.599,16.102,2.633,16.083,2.67,16.082,2.678,16.059,2.773,16.082,2.839,16.063,2.909,16.008,2.977,15.958,3.029,15.929,3.076,15.905,3.096,15.849,3.103,15.775,3.127,15.677,3.23,15.581,3.329,15.458,3.457,15.36,3.567,15.24,3.702,15.129,3.827,15.062,3.947,15.035,4.016,15.067,4.023,15.115,4.024,15.136,4.037,15.137,4.069,15.088,4.164,15.064,4.285,15.023,4.359,14.894,4.472,14.77,4.558,14.731,4.602,14.709,4.666,14.662,5.066,14.641,5.179,14.602,5.229,14.574,5.252,14.563,5.28,14.568,5.351,14.584,5.415,14.584,5.44,14.617,5.496,14.617,5.865,14.599,5.884,14.577,5.916,14.542,5.914,14.503,5.917,14.464,5.971,14.431,6.039,14.441,6.087,14.475,6.127,14.512,6.162,14.559,6.191,14.7,6.25,14.739,6.28,14.764,6.316,14.78,6.366,14.862,6.556,14.983,6.745,15.035,6.784,15.086,6.91,15.157,7.064,15.186,7.135,15.207,7.206,15.246,7.264,15.379,7.358,15.48,7.524,15.589,7.515,15.701,7.488,15.845,7.475,15.958,7.508,16.031,7.572,16.191,7.623,16.379,7.684,16.404,7.772,16.459,7.819,16.523,7.86,16.545,7.865,16.55,7.836,16.589,7.743,16.668,7.652,16.785,7.551,16.818,7.557,16.89,7.634,17.072,7.681,17.118,7.702,17.247,7.813,17.402,7.885,17.436,7.891,17.493,7.91,17.649,7.984,17.761,7.974,17.94,7.985,18.239,8.02,18.455,8.032,18.564,8.046,18.592,8.061,18.634,8.168,18.666,8.198,18.747,8.244,18.906,8.405,19.011,8.541,19.04,8.587,19.042,8.59,19.064,8.599,19.109,8.656,19.064,8.715,18.886,8.836,18.889,8.852,18.878,8.873,18.888,8.89,18.956,8.939,19.048,8.995,19.146,9.016,19.4,9.012,19.617,9.024,19.668,9.021,19.838,9.049,19.954,9.075,20.073,9.133,20.342,9.127,20.567,9.275,20.631,9.301,20.66,9.325,20.668,9.347,20.773,9.406,20.891,9.527,20.984,9.636,21.009,9.713,21.264,9.975,21.352,9.969,21.396,10.001,21.497,10.176,21.528,10.208,21.576,10.219,21.633,10.238,21.683,10.29,21.726,10.367,21.726,10.462,21.707,10.538,21.707,10.575,21.731,10.609,21.771,10.643,21.965,10.737,22.014,10.782,22.043,10.823,22.097,10.83,22.156,10.826,22.194,10.851,22.236,10.894,22.37,10.952,22.494,10.996,22.624,10.977,22.73,10.954,22.817,10.927,22.86,10.92,22.931,10.795,22.964,10.752,23.256,10.458,23.312,10.388,23.457,10.174,23.545,10.03,23.646,9.823,23.656,9.71,23.643,9.614,23.623,9.341,23.596,9.262,23.468,9.115,23.463,9.048,23.489,8.993,23.528,8.971,23.552,8.943,23.537,8.816,23.583,8.766,23.679,8.732,23.922,8.71,24.048,8.691,24.147,8.666]]]},{"n":"Cape Verde","c":"CV","r":4,"ml":4,"lx":-23.64,"ly":15.07,"b":[-25.342,14.818,-22.682,17.194],"p":[[[-25.17,16.946,-25.267,16.926,-25.308,16.936,-25.322,17.015,-25.342,17.068,-25.337,17.091,-25.113,17.194,-25.035,17.176,-24.98,17.095,-25.017,17.049,-25.17,16.946]],[[-23.444,15.008,-23.505,14.916,-23.637,14.923,-23.705,14.961,-23.785,15.077,-23.783,15.166,-23.754,15.244,-23.759,15.311,-23.748,15.329,-23.707,15.317,-23.701,15.272,-23.58,15.161,-23.535,15.139,-23.444,15.008]],[[-24.887,16.818,-24.969,16.794,-25.02,16.797,-25.093,16.833,-25.07,16.871,-24.991,16.913,-24.936,16.922,-24.892,16.846,-24.887,16.818]],[[-22.918,16.237,-22.834,16.219,-22.803,16.226,-22.749,16.222,-22.693,16.169,-22.682,16.113,-22.71,16.043,-22.821,15.986,-22.884,15.993,-22.959,16.045,-22.916,16.148,-22.918,16.237]],[[-24.308,14.856,-24.386,14.818,-24.441,14.835,-24.492,14.874,-24.517,14.931,-24.497,14.98,-24.392,15.038,-24.329,15.019,-24.296,14.93,-24.308,14.856]],[[-24.088,16.623,-24.046,16.593,-24.033,16.572,-24.094,16.561,-24.243,16.599,-24.283,16.576,-24.322,16.493,-24.398,16.618,-24.393,16.664,-24.377,16.678,-24.271,16.645,-24.088,16.623]],[[-22.888,16.659,-22.92,16.608,-22.959,16.683,-22.981,16.701,-22.991,16.809,-22.933,16.841,-22.905,16.844,-22.904,16.732,-22.888,16.659]],[[-23.182,15.137,-23.21,15.133,-23.252,15.178,-23.242,15.241,-23.247,15.257,-23.21,15.324,-23.138,15.318,-23.119,15.268,-23.116,15.167,-23.182,15.137]]]},{"n":"Canada","c":"CA","r":2,"ml":1.7,"lx":-101.91,"ly":60.32,"b":[-141.002,41.675,-52.654,83.116],"p":[[[-132.656,54.127,-132.564,54.069,-132.344,54.106,-132.303,54.099,-132.262,54.076,-132.216,54.028,-132.166,53.955,-132.155,53.875,-132.175,53.847,-132.215,53.815,-132.565,53.688,-132.574,53.675,-132.567,53.664,-132.535,53.652,-132.464,53.653,-132.187,53.685,-132.172,53.707,-132.152,53.807,-132.114,53.86,-132.111,53.9,-132.136,53.996,-132.134,54.034,-131.941,54.042,-131.82,54.077,-131.696,54.143,-131.668,54.141,-131.685,54.023,-131.703,53.986,-131.821,53.842,-131.889,53.714,-131.922,53.588,-131.928,53.379,-131.957,53.309,-132.011,53.265,-132.347,53.189,-132.52,53.194,-132.675,53.263,-132.748,53.31,-132.693,53.368,-132.655,53.371,-132.546,53.359,-132.462,53.338,-132.425,53.337,-132.431,53.35,-132.67,53.459,-132.845,53.508,-132.898,53.563,-132.9,53.605,-132.913,53.629,-133.052,53.778,-133.079,53.837,-133.098,53.92,-133.098,54.006,-133.064,54.144,-133.048,54.159,-132.991,54.158,-132.893,54.141,-132.656,54.127]],[[-131.754,53.196,-131.652,53.103,-131.622,53.02,-131.635,52.922,-131.795,52.885,-131.88,52.915,-131.916,52.909,-131.972,52.88,-131.904,52.867,-131.81,52.819,-131.727,52.756,-131.611,52.745,-131.455,52.702,-131.573,52.623,-131.591,52.578,-131.444,52.453,-131.43,52.422,-131.383,52.416,-131.274,52.426,-131.26,52.416,-131.26,52.39,-131.327,52.318,-131.32,52.303,-131.259,52.292,-131.143,52.291,-131.116,52.219,-131.222,52.154,-131.422,52.238,-131.511,52.322,-131.562,52.4,-131.624,52.444,-131.81,52.542,-132.092,52.753,-132.165,52.783,-132.239,52.867,-132.26,52.907,-132.258,52.934,-132.23,52.948,-132.145,52.957,-132.144,52.999,-132.469,53.072,-132.505,53.087,-132.547,53.138,-132.524,53.145,-132.345,53.136,-132.154,53.16,-132.036,53.179,-131.99,53.202,-131.893,53.231,-131.853,53.23,-131.754,53.196]],[[-127.197,50.64,-126.701,50.516,-126.204,50.454,-125.839,50.381,-125.615,50.359,-125.534,50.342,-125.482,50.317,-125.42,50.255,-125.314,50.107,-125.233,50.012,-125.066,49.848,-124.935,49.732,-124.905,49.685,-124.932,49.67,-124.931,49.643,-124.831,49.53,-124.643,49.429,-124.496,49.38,-124.186,49.301,-123.996,49.224,-123.937,49.171,-123.854,49.119,-123.82,49.083,-123.752,48.951,-123.627,48.824,-123.497,48.582,-123.473,48.602,-123.458,48.674,-123.443,48.69,-123.415,48.698,-123.39,48.67,-123.366,48.606,-123.284,48.455,-123.311,48.411,-123.335,48.406,-123.446,48.427,-123.485,48.4,-123.536,48.345,-123.573,48.323,-123.595,48.334,-123.917,48.387,-124.115,48.436,-124.376,48.515,-124.689,48.597,-124.868,48.654,-125.017,48.711,-125.121,48.761,-125.14,48.803,-125.136,48.822,-124.935,48.956,-124.85,49.028,-124.817,49.083,-124.8,49.142,-124.813,49.213,-124.821,49.207,-124.839,49.139,-124.868,49.079,-124.904,49.031,-124.927,49.014,-125.168,48.991,-125.363,48.998,-125.46,48.941,-125.489,48.934,-125.543,48.953,-125.66,49.029,-125.829,49.092,-125.812,49.107,-125.702,49.139,-125.644,49.186,-125.655,49.193,-125.694,49.19,-125.728,49.2,-125.796,49.26,-125.835,49.277,-125.918,49.25,-125.952,49.248,-125.984,49.288,-125.938,49.38,-125.935,49.401,-126.02,49.368,-126.048,49.379,-126.075,49.409,-126.1,49.421,-126.169,49.415,-126.244,49.443,-126.27,49.432,-126.28,49.392,-126.304,49.382,-126.419,49.449,-126.445,49.451,-126.5,49.4,-126.519,49.397,-126.549,49.419,-126.564,49.543,-126.557,49.579,-126.542,49.59,-126.443,49.619,-126.158,49.65,-126.134,49.672,-126.348,49.661,-126.403,49.678,-126.463,49.72,-126.525,49.72,-126.558,49.733,-126.593,49.764,-126.683,49.876,-126.745,49.905,-126.849,49.923,-126.903,49.944,-126.926,49.935,-126.948,49.903,-126.977,49.883,-127.049,49.872,-127.114,49.88,-127.166,49.91,-127.196,49.949,-127.208,49.992,-127.179,50.05,-127.18,50.073,-127.192,50.1,-127.216,50.121,-127.25,50.138,-127.268,50.129,-127.272,50.096,-127.29,50.071,-127.349,50.052,-127.398,50.085,-127.43,50.131,-127.467,50.163,-127.675,50.163,-127.77,50.121,-127.816,50.118,-127.864,50.128,-127.873,50.15,-127.828,50.211,-127.839,50.293,-127.851,50.314,-127.947,50.326,-127.963,50.346,-127.906,50.445,-127.874,50.464,-127.832,50.471,-127.641,50.479,-127.578,50.465,-127.487,50.405,-127.489,50.427,-127.524,50.496,-127.529,50.537,-127.466,50.583,-127.526,50.597,-127.751,50.607,-127.75,50.578,-127.731,50.536,-127.865,50.499,-127.964,50.493,-128.058,50.498,-128.136,50.521,-128.267,50.609,-128.35,50.697,-128.346,50.744,-128.301,50.794,-128.242,50.828,-128.101,50.858,-127.918,50.861,-127.713,50.821,-127.197,50.64]],[[-130.025,55.888,-130.014,55.951,-130.023,56.015,-130.056,56.065,-130.098,56.109,-130.215,56.083,-130.413,56.123,-130.477,56.231,-130.649,56.264,-130.742,56.341,-130.93,56.379,-131.083,56.405,-131.199,56.449,-131.336,56.501,-131.472,56.557,-131.575,56.599,-131.652,56.596,-131.824,56.59,-131.833,56.685,-131.886,56.742,-131.866,56.793,-131.962,56.819,-132.104,56.857,-132.063,56.953,-132.032,57.027,-132.157,57.048,-132.338,57.079,-132.279,57.145,-132.232,57.199,-132.302,57.276,-132.442,57.407,-132.55,57.5,-132.692,57.645,-132.816,57.773,-132.917,57.877,-133.001,57.949,-133.12,58.078,-133.275,58.223,-133.423,58.337,-133.401,58.411,-133.546,58.503,-133.674,58.597,-133.821,58.705,-133.966,58.758,-134.069,58.796,-134.219,58.85,-134.297,58.898,-134.33,58.94,-134.364,58.969,-134.393,59.009,-134.41,59.056,-134.441,59.085,-134.622,59.155,-134.677,59.199,-134.802,59.25,-134.907,59.271,-134.944,59.288,-135.071,59.441,-135.051,59.496,-135.037,59.551,-135.051,59.579,-135.261,59.695,-135.368,59.743,-135.476,59.793,-135.703,59.729,-135.935,59.663,-136.097,59.638,-136.322,59.605,-136.247,59.533,-136.278,59.48,-136.348,59.456,-136.466,59.459,-136.467,59.28,-136.579,59.152,-136.813,59.15,-136.939,59.106,-137.126,59.041,-137.278,58.988,-137.439,58.903,-137.521,58.915,-137.484,58.991,-137.544,59.119,-137.593,59.226,-137.697,59.281,-137.871,59.374,-138.001,59.443,-138.187,59.542,-138.318,59.611,-138.454,59.683,-138.632,59.778,-138.705,59.901,-138.869,59.946,-139.043,59.993,-139.185,60.084,-139.137,60.173,-139.079,60.279,-139.079,60.344,-139.235,60.34,-139.468,60.334,-139.676,60.328,-139.831,60.253,-139.973,60.183,-140.197,60.238,-140.453,60.3,-140.525,60.218,-140.763,60.259,-141.002,60.3,-141.002,60.592,-141.002,60.885,-141.002,61.177,-141.002,61.469,-141.002,61.761,-141.002,62.053,-141.002,62.346,-141.002,62.638,-141.002,62.93,-141.002,63.222,-141.002,63.514,-141.002,63.807,-141.002,64.099,-141.002,64.391,-141.002,64.683,-141.002,64.976,-141.002,65.268,-141.002,65.56,-141.002,65.852,-141.002,66.144,-141.002,66.437,-141.002,66.729,-141.002,67.021,-141.002,67.313,-141.002,67.605,-141.002,67.898,-141.002,68.19,-141.002,68.482,-141.002,68.774,-141.002,69.066,-141.002,69.359,-141.002,69.651,-140.86,69.635,-140.405,69.602,-139.977,69.622,-139.182,69.516,-138.69,69.317,-138.291,69.219,-138.128,69.152,-137.869,69.093,-137.26,68.964,-137.07,68.951,-136.717,68.889,-136.499,68.897,-136.122,68.882,-135.867,68.833,-135.362,68.696,-135.259,68.684,-135.231,68.694,-135.407,68.829,-135.435,68.842,-135.638,68.892,-135.876,68.917,-135.895,68.927,-135.939,68.974,-135.925,68.993,-135.873,69.001,-135.695,69.001,-135.59,69.008,-135.576,69.027,-135.651,69.031,-135.743,69.049,-135.85,69.081,-135.91,69.111,-135.691,69.311,-135.615,69.291,-135.5,69.337,-135.293,69.308,-135.255,69.324,-135.23,69.425,-135.199,69.45,-135.141,69.468,-134.853,69.486,-134.494,69.468,-134.457,69.478,-134.491,69.545,-134.495,69.572,-134.474,69.633,-134.451,69.665,-134.409,69.682,-134.242,69.669,-134.19,69.639,-134.134,69.587,-134.077,69.558,-133.9,69.528,-133.88,69.508,-133.947,69.429,-134.018,69.388,-134.165,69.281,-134.174,69.253,-133.948,69.301,-133.694,69.368,-133.476,69.405,-133.294,69.412,-133.163,69.434,-133.084,69.471,-133.028,69.508,-132.915,69.63,-132.84,69.651,-132.527,69.643,-132.452,69.647,-132.404,69.659,-132.413,69.674,-132.479,69.693,-132.568,69.698,-132.571,69.707,-132.538,69.727,-132.488,69.738,-132.334,69.752,-132.232,69.708,-132.163,69.705,-131.934,69.753,-131.582,69.882,-131.441,69.918,-131.319,69.924,-131.216,69.901,-131.136,69.907,-131.032,69.979,-130.991,70.018,-130.926,70.052,-130.665,70.127,-130.498,70.143,-130.396,70.129,-130.275,70.098,-130.175,70.086,-130.043,70.095,-129.945,70.091,-129.898,70.106,-129.73,70.192,-129.676,70.193,-129.623,70.168,-129.538,70.105,-129.538,70.074,-129.648,69.998,-130.459,69.78,-130.709,69.686,-130.832,69.651,-130.96,69.632,-131.208,69.616,-131.306,69.597,-131.473,69.579,-131.863,69.549,-131.938,69.535,-131.989,69.518,-132.129,69.402,-132.197,69.365,-132.331,69.308,-132.481,69.273,-132.687,69.26,-132.817,69.206,-132.968,69.101,-133.089,69.029,-133.228,68.967,-133.379,68.887,-133.418,68.844,-133.373,68.788,-133.348,68.77,-133.197,68.74,-133.138,68.747,-133.192,68.777,-133.32,68.82,-133.337,68.835,-133.304,68.847,-132.706,68.815,-132.578,68.848,-132.533,68.876,-132.542,68.89,-132.704,68.896,-132.739,68.922,-132.765,68.972,-132.77,69.012,-132.755,69.042,-132.719,69.079,-132.545,69.141,-132.358,69.167,-132.214,69.202,-132.134,69.234,-131.92,69.291,-131.833,69.336,-131.787,69.371,-131.781,69.389,-131.82,69.402,-131.788,69.432,-131.632,69.459,-131.563,69.461,-131.343,69.435,-131.303,69.415,-131.325,69.361,-131.294,69.364,-131.209,69.432,-131.162,69.455,-131.113,69.459,-131.063,69.451,-131.013,69.429,-130.986,69.363,-130.982,69.253,-130.971,69.209,-130.914,69.285,-130.875,69.32,-130.661,69.481,-130.516,69.57,-130.354,69.656,-130.118,69.72,-129.572,69.827,-129.265,69.855,-129.109,69.882,-129.033,69.905,-128.984,69.933,-128.899,69.966,-128.884,69.963,-128.917,69.895,-128.939,69.875,-129.138,69.833,-129.158,69.8,-129.136,69.75,-129.102,69.717,-129.054,69.701,-128.971,69.712,-128.853,69.751,-128.706,69.81,-128.387,69.96,-128.359,69.988,-128.279,70.108,-128.096,70.161,-127.765,70.222,-127.684,70.26,-127.974,70.293,-128.034,70.315,-128.044,70.329,-127.989,70.363,-128.121,70.397,-128.17,70.418,-128.168,70.48,-128.127,70.524,-128.04,70.566,-127.991,70.574,-127.862,70.549,-127.753,70.517,-127.377,70.369,-127.226,70.296,-127.138,70.239,-126.927,70.062,-126.833,69.959,-126.759,69.853,-126.685,69.777,-126.612,69.73,-126.25,69.545,-126.064,69.467,-125.907,69.419,-125.728,69.38,-125.525,69.352,-125.387,69.349,-125.172,69.428,-125.167,69.48,-125.262,69.566,-125.357,69.626,-125.346,69.662,-125.219,69.732,-125.228,69.757,-125.201,69.829,-125.114,69.815,-125.08,69.818,-125.031,69.844,-124.968,69.894,-124.889,69.936,-124.794,69.969,-124.768,69.99,-124.863,70.006,-124.92,70.006,-124.963,70.013,-124.99,70.027,-124.952,70.042,-124.745,70.08,-124.706,70.117,-124.64,70.141,-124.555,70.151,-124.503,70.141,-124.444,70.111,-124.442,70.062,-124.467,69.983,-124.472,69.919,-124.407,69.767,-124.349,69.735,-124.125,69.69,-124.138,69.653,-124.398,69.494,-124.454,69.455,-124.481,69.425,-124.472,69.4,-124.426,69.379,-124.338,69.365,-124.112,69.359,-124.05,69.373,-123.609,69.377,-123.528,69.389,-123.46,69.42,-123.361,69.497,-123.249,69.52,-123.214,69.542,-123.144,69.632,-123.11,69.738,-123.077,69.782,-123.026,69.81,-122.957,69.819,-122.785,69.808,-122.705,69.817,-122.387,69.808,-122.07,69.816,-121.742,69.798,-121.531,69.776,-121.336,69.742,-120.962,69.66,-120.815,69.617,-120.293,69.421,-120.14,69.381,-119.853,69.342,-118.869,69.257,-118.745,69.234,-118.486,69.145,-118.307,69.093,-118.095,69.043,-117.83,69,-117.311,68.935,-117.227,68.913,-117.132,68.907,-117.026,68.916,-116.55,68.879,-116.425,68.881,-116.334,68.874,-116.223,68.847,-116.059,68.837,-116.065,68.855,-116.252,68.958,-116.243,68.974,-116.167,68.975,-115.936,68.958,-115.883,68.987,-115.806,68.987,-115.631,68.973,-115.442,68.941,-115.24,68.892,-114.994,68.85,-114.62,68.746,-114.414,68.66,-114.218,68.552,-114.111,68.477,-114.092,68.435,-114.051,68.415,-113.988,68.415,-113.964,68.399,-114.021,68.306,-114.053,68.283,-114.096,68.267,-114.275,68.248,-114.765,68.27,-114.852,68.195,-115.127,68.132,-115.176,68.104,-115.187,68.084,-115.167,68.019,-115.202,67.998,-115.427,67.924,-115.434,67.902,-115.288,67.872,-115.133,67.819,-115.011,67.806,-114.857,67.814,-114.663,67.795,-114.429,67.751,-114.267,67.731,-114.176,67.735,-114.051,67.727,-113.893,67.707,-113.682,67.7,-113.215,67.702,-113.075,67.687,-112.879,67.68,-112.503,67.682,-112.435,67.685,-112.315,67.72,-112.237,67.731,-112.101,67.732,-111.711,67.757,-111.576,67.757,-111.451,67.776,-111.291,67.815,-111.192,67.823,-111.155,67.798,-111.087,67.788,-110.99,67.791,-110.805,67.832,-110.372,67.954,-110.216,67.954,-110.102,67.992,-110.074,67.993,-110.042,67.977,-109.937,67.888,-109.904,67.874,-109.831,67.866,-109.76,67.82,-109.686,67.752,-109.63,67.733,-109.224,67.73,-109.081,67.711,-109.038,67.691,-108.994,67.637,-108.968,67.532,-108.95,67.494,-108.891,67.438,-108.852,67.422,-108.815,67.438,-108.715,67.583,-108.68,67.606,-108.613,67.598,-108.593,67.591,-108.492,67.483,-108.347,67.403,-107.989,67.256,-107.931,67.202,-107.909,67.163,-107.929,67.127,-107.991,67.095,-108.088,67.07,-108.221,67.051,-108.344,67.058,-108.459,67.09,-108.496,67.092,-108.455,67.063,-108.218,66.941,-108.158,66.893,-108.101,66.86,-108.05,66.844,-108.002,66.818,-107.958,66.781,-107.761,66.684,-107.705,66.637,-107.48,66.492,-107.374,66.435,-107.291,66.402,-107.259,66.399,-107.278,66.425,-107.564,66.619,-107.71,66.74,-107.731,66.769,-107.74,66.814,-107.746,66.961,-107.725,66.984,-107.626,67.003,-107.499,66.936,-107.451,66.927,-107.419,66.931,-107.402,66.948,-107.329,66.932,-107.2,66.883,-107.156,66.882,-107.254,66.976,-107.323,67.023,-107.348,67.055,-107.283,67.103,-107.318,67.128,-107.482,67.199,-107.567,67.273,-107.644,67.385,-107.651,67.428,-107.638,67.474,-107.65,67.511,-107.753,67.587,-107.865,67.639,-107.954,67.7,-107.972,67.732,-107.958,67.819,-107.891,67.856,-107.763,67.907,-107.729,67.959,-107.787,68.013,-107.798,68.037,-107.761,68.032,-107.509,68.059,-107.446,68.05,-107.351,68.061,-107.224,68.094,-107.125,68.108,-106.994,68.106,-106.923,68.114,-106.836,68.129,-106.791,68.145,-106.711,68.207,-106.668,68.216,-106.535,68.209,-106.459,68.196,-106.424,68.201,-106.429,68.288,-106.404,68.319,-106.271,68.383,-106.132,68.39,-106.039,68.407,-105.933,68.443,-105.857,68.475,-105.781,68.527,-105.75,68.592,-105.774,68.611,-105.932,68.637,-106.027,68.623,-106.237,68.577,-106.458,68.516,-106.543,68.461,-106.567,68.389,-106.608,68.357,-106.78,68.387,-106.854,68.387,-106.946,68.374,-107.043,68.347,-107.146,68.304,-107.298,68.296,-107.499,68.324,-107.619,68.331,-107.742,68.286,-107.734,68.252,-107.678,68.203,-107.734,68.174,-108.027,68.163,-108.105,68.169,-108.261,68.15,-108.323,68.154,-108.368,68.178,-108.687,68.277,-108.718,68.297,-108.641,68.379,-108.346,68.598,-108.313,68.611,-107.766,68.649,-107.436,68.689,-106.831,68.809,-106.713,68.819,-106.324,68.899,-106.164,68.92,-106.016,68.906,-105.798,68.865,-105.686,68.828,-105.606,68.782,-105.54,68.719,-105.457,68.578,-105.429,68.458,-105.377,68.414,-105.195,68.33,-105.101,68.298,-105.044,68.288,-104.994,68.307,-104.96,68.311,-104.937,68.303,-104.912,68.25,-104.879,68.245,-104.77,68.252,-104.653,68.23,-104.636,68.214,-104.661,68.149,-104.628,68.121,-104.487,68.063,-104.351,68.041,-104.194,68.031,-103.902,68.041,-103.657,68.069,-103.474,68.115,-103.323,68.064,-103.022,67.94,-102.842,67.853,-102.692,67.812,-102.389,67.762,-102.32,67.736,-102.21,67.733,-102.057,67.753,-101.884,67.745,-101.689,67.709,-101.555,67.693,-101.096,67.762,-101.026,67.766,-100.856,67.799,-100.746,67.809,-100.616,67.808,-100.52,67.818,-100.456,67.839,-100.213,67.839,-99.773,67.815,-99.472,67.784,-99.294,67.745,-99.147,67.724,-99.032,67.719,-98.92,67.726,-98.812,67.744,-98.697,67.78,-98.453,67.798,-98.412,67.807,-98.417,67.826,-98.468,67.856,-98.606,67.911,-98.704,67.966,-98.722,68,-98.72,68.042,-98.69,68.066,-98.632,68.073,-98.54,68.047,-98.415,67.988,-98.063,67.77,-97.978,67.739,-97.931,67.711,-97.607,67.631,-97.455,67.617,-97.274,67.666,-97.194,67.697,-97.155,67.726,-97.157,67.755,-97.14,67.796,-97.158,67.822,-97.207,67.855,-97.336,67.901,-97.547,67.961,-97.739,67.978,-97.913,67.954,-98.11,67.903,-98.193,67.923,-98.438,68.065,-98.5,68.118,-98.5,68.132,-98.386,68.115,-98.381,68.132,-98.449,68.201,-98.491,68.224,-98.633,68.331,-98.65,68.364,-98.562,68.371,-98.522,68.383,-98.469,68.382,-98.219,68.317,-98.091,68.346,-97.794,68.388,-97.911,68.45,-97.939,68.51,-97.925,68.524,-97.829,68.533,-97.64,68.482,-97.548,68.475,-97.481,68.495,-97.41,68.497,-97.336,68.479,-97.266,68.453,-97.136,68.378,-97.072,68.333,-97,68.265,-96.977,68.255,-96.628,68.25,-96.431,68.311,-96.435,68.29,-96.48,68.243,-96.725,68.061,-96.722,68.039,-96.592,68.048,-96.531,68.063,-96.494,68.085,-96.461,68.136,-96.439,68.151,-96.076,68.237,-95.97,68.249,-96.036,68.158,-96.171,67.832,-96.199,67.718,-96.228,67.679,-96.371,67.554,-96.369,67.51,-96.213,67.404,-96.185,67.376,-96.169,67.289,-96.141,67.272,-96.013,67.271,-95.879,67.298,-95.72,67.317,-95.695,67.299,-95.783,67.194,-95.778,67.185,-95.626,67.212,-95.557,67.215,-95.529,67.209,-95.416,67.156,-95.405,67.116,-95.407,67.056,-95.419,67.013,-95.457,66.989,-95.502,66.98,-95.559,66.973,-95.611,66.976,-95.769,66.967,-95.862,66.978,-95.954,67.011,-96.02,67.019,-96.095,66.994,-96.216,66.998,-96.35,67.07,-96.404,67.063,-96.423,67.052,-96.42,67.036,-96.36,66.989,-95.885,66.741,-95.813,66.69,-95.797,66.617,-95.788,66.617,-95.743,66.69,-95.772,66.726,-96.016,66.87,-96.045,66.923,-96.037,66.938,-95.972,66.952,-95.625,66.916,-95.49,66.924,-95.4,66.949,-95.354,66.981,-95.321,67.152,-95.259,67.263,-95.296,67.361,-95.39,67.518,-95.463,67.61,-95.634,67.704,-95.65,67.737,-95.461,68.021,-95.427,68.045,-95.384,68.056,-95.235,68.06,-95.126,68.083,-94.955,68.05,-94.861,68.042,-94.744,68.071,-94.485,68.19,-94.384,68.227,-94.255,68.297,-94.098,68.399,-93.928,68.474,-93.652,68.543,-93.483,68.599,-93.449,68.619,-93.606,68.624,-93.644,68.633,-93.676,68.686,-93.66,68.784,-93.663,68.838,-93.681,68.887,-93.716,68.931,-93.766,68.97,-93.811,68.993,-93.852,69,-93.881,68.997,-93.896,68.982,-93.938,68.889,-93.992,68.821,-94.065,68.785,-94.217,68.761,-94.478,68.743,-94.587,68.776,-94.6,68.803,-94.563,68.912,-94.476,68.958,-94.237,69.05,-94.084,69.123,-94.081,69.136,-94.222,69.136,-94.255,69.151,-94.285,69.242,-94.277,69.275,-94.255,69.314,-94.156,69.342,-93.854,69.376,-93.619,69.417,-93.613,69.403,-93.801,69.281,-93.82,69.253,-93.749,69.226,-93.567,69.297,-93.451,69.355,-93.431,69.375,-93.537,69.382,-93.543,69.406,-93.522,69.451,-93.532,69.481,-93.65,69.519,-93.794,69.498,-93.915,69.458,-94.015,69.447,-94.163,69.446,-94.271,69.455,-94.338,69.474,-94.419,69.517,-94.514,69.583,-94.634,69.65,-94.676,69.657,-94.713,69.649,-94.789,69.585,-94.823,69.578,-95.292,69.667,-95.491,69.718,-95.588,69.756,-95.707,69.778,-95.851,69.785,-95.965,69.803,-96.05,69.831,-96.119,69.872,-96.172,69.925,-96.269,69.992,-96.492,70.125,-96.551,70.21,-96.56,70.243,-96.546,70.327,-96.337,70.47,-96.298,70.511,-96.226,70.542,-96.123,70.561,-96.048,70.567,-95.879,70.549,-95.98,70.593,-95.988,70.617,-95.886,70.694,-95.906,70.698,-96.186,70.638,-96.258,70.642,-96.359,70.679,-96.549,70.809,-96.551,70.89,-96.491,71.002,-96.47,71.07,-96.525,71.127,-96.504,71.143,-96.445,71.159,-96.421,71.176,-96.447,71.24,-96.406,71.274,-96.271,71.339,-96.14,71.396,-96.062,71.414,-95.994,71.411,-95.924,71.393,-95.851,71.361,-95.725,71.328,-95.633,71.319,-95.564,71.337,-95.448,71.46,-95.406,71.492,-95.445,71.505,-95.674,71.504,-95.773,71.514,-95.83,71.526,-95.872,71.573,-95.838,71.598,-95.616,71.685,-95.512,71.777,-95.201,71.904,-94.887,71.963,-94.735,71.983,-94.611,71.987,-94.557,71.979,-94.491,71.916,-94.479,71.849,-94.308,71.765,-94.171,71.758,-94.086,71.771,-93.81,71.766,-93.746,71.743,-93.751,71.717,-93.782,71.674,-93.763,71.638,-93.576,71.569,-93.407,71.521,-93.256,71.461,-93.031,71.336,-92.983,71.3,-92.949,71.262,-92.89,71.122,-92.883,71.069,-92.904,70.916,-92.922,70.887,-92.981,70.852,-92.961,70.838,-92.783,70.798,-92.642,70.719,-92.567,70.693,-92.388,70.65,-92.356,70.634,-92.315,70.608,-92.214,70.493,-92.049,70.39,-92.037,70.367,-92.073,70.319,-92.047,70.303,-91.984,70.286,-91.926,70.295,-91.876,70.331,-91.82,70.342,-91.761,70.326,-91.716,70.299,-91.654,70.233,-91.564,70.178,-91.572,70.162,-91.616,70.148,-91.859,70.133,-91.995,70.143,-92.121,70.17,-92.209,70.198,-92.29,70.24,-92.321,70.235,-92.363,70.201,-92.455,70.15,-92.512,70.104,-92.446,70.083,-92.127,70.085,-92.058,70.071,-91.977,70.039,-92.069,69.984,-92.285,69.892,-92.751,69.714,-92.888,69.668,-92.855,69.655,-92.803,69.651,-92.643,69.659,-92.493,69.683,-92.312,69.673,-92.231,69.653,-92.258,69.634,-92.209,69.603,-91.912,69.531,-91.724,69.546,-91.532,69.615,-91.384,69.649,-91.202,69.645,-91.151,69.637,-91.17,69.62,-91.305,69.581,-91.427,69.538,-91.44,69.526,-91.288,69.543,-90.95,69.515,-90.786,69.509,-90.667,69.516,-90.555,69.504,-90.451,69.475,-90.416,69.457,-90.513,69.445,-90.606,69.445,-90.684,69.428,-90.749,69.392,-90.795,69.347,-90.822,69.29,-90.892,69.267,-91.005,69.277,-91.049,69.293,-91.025,69.315,-91.058,69.318,-91.148,69.303,-91.218,69.302,-91.237,69.286,-90.745,69.106,-90.587,68.947,-90.479,68.881,-90.468,68.864,-90.539,68.82,-90.543,68.786,-90.51,68.689,-90.525,68.611,-90.574,68.475,-90.528,68.432,-90.423,68.395,-90.36,68.347,-90.317,68.33,-90.285,68.292,-90.248,68.267,-90.205,68.257,-90.174,68.27,-90.157,68.306,-90.116,68.339,-90.005,68.398,-89.898,68.491,-89.879,68.522,-89.897,68.594,-89.884,68.626,-89.783,68.736,-89.751,68.812,-89.72,68.932,-89.667,69.015,-89.552,69.085,-89.352,69.227,-89.28,69.255,-89.198,69.269,-89.057,69.266,-88.954,69.22,-88.815,69.136,-88.638,69.059,-88.316,68.954,-88.224,68.915,-88.041,68.812,-87.964,68.709,-87.912,68.565,-87.866,68.478,-87.828,68.448,-87.81,68.404,-87.814,68.346,-87.828,68.3,-87.853,68.267,-87.893,68.248,-87.991,68.242,-88.111,68.251,-88.146,68.266,-88.209,68.335,-88.235,68.339,-88.347,68.288,-88.361,68.26,-88.32,68.166,-88.325,67.989,-88.314,67.95,-88.196,67.766,-87.997,67.626,-87.499,67.355,-87.471,67.325,-87.418,67.214,-87.392,67.191,-87.359,67.177,-87.32,67.173,-87.266,67.184,-87.083,67.268,-86.924,67.356,-86.813,67.402,-86.75,67.406,-86.682,67.422,-86.609,67.451,-86.561,67.482,-86.536,67.516,-86.504,67.649,-86.476,67.713,-86.398,67.8,-86.37,67.825,-85.984,68.045,-85.953,68.072,-85.789,68.328,-85.731,68.445,-85.723,68.515,-85.745,68.578,-85.734,68.63,-85.69,68.671,-85.643,68.7,-85.562,68.729,-85.518,68.77,-85.491,68.774,-85.425,68.774,-85.338,68.746,-85.275,68.741,-84.868,68.773,-84.867,68.79,-85.107,68.844,-85.104,68.871,-85.083,68.908,-85.008,68.949,-84.916,68.962,-84.895,68.989,-84.893,69.021,-84.862,69.074,-84.89,69.093,-85.114,69.166,-85.243,69.163,-85.275,69.172,-85.387,69.232,-85.428,69.318,-85.432,69.354,-85.416,69.411,-85.402,69.427,-85.409,69.452,-85.437,69.488,-85.44,69.52,-85.416,69.548,-85.43,69.581,-85.482,69.619,-85.502,69.652,-85.448,69.748,-85.446,69.778,-85.497,69.819,-85.535,69.835,-85.507,69.845,-85.415,69.85,-85.305,69.836,-85.177,69.805,-85.02,69.805,-84.834,69.835,-84.645,69.85,-84.319,69.844,-84.242,69.835,-83.917,69.745,-83.665,69.7,-83.552,69.704,-82.991,69.686,-82.746,69.695,-82.618,69.691,-82.374,69.642,-82.39,69.601,-82.496,69.532,-82.633,69.518,-82.755,69.494,-82.642,69.458,-82.31,69.41,-82.232,69.333,-82.208,69.297,-82.247,69.265,-82.228,69.249,-82.151,69.249,-81.952,69.276,-81.732,69.258,-81.412,69.198,-81.378,69.186,-81.322,69.139,-81.329,69.12,-81.611,69.003,-81.758,68.957,-81.952,68.909,-81.958,68.884,-81.687,68.879,-81.476,68.866,-81.381,68.85,-81.331,68.828,-81.264,68.781,-81.252,68.743,-81.259,68.692,-81.282,68.657,-81.527,68.556,-81.64,68.524,-81.831,68.487,-81.915,68.459,-82.006,68.463,-82.106,68.499,-82.21,68.506,-82.397,68.478,-82.499,68.479,-82.549,68.469,-82.553,68.446,-82.464,68.382,-82.413,68.357,-82.393,68.338,-82.431,68.307,-82.423,68.297,-82.393,68.285,-82.222,68.145,-82.187,68.134,-82.151,68.14,-82.078,68.18,-82.034,68.196,-82.012,68.194,-82.013,68.173,-82.092,68.051,-82.102,68.019,-82.1,67.99,-82.063,67.928,-81.976,67.862,-81.869,67.802,-81.709,67.722,-81.493,67.637,-81.412,67.595,-81.294,67.497,-81.27,67.46,-81.301,67.357,-81.387,67.189,-81.443,67.093,-81.468,67.07,-81.63,67.002,-81.722,66.986,-81.874,66.988,-81.926,66.975,-82.005,66.92,-82.113,66.825,-82.198,66.765,-82.261,66.739,-82.375,66.709,-82.554,66.621,-82.642,66.588,-82.949,66.551,-83.199,66.431,-83.298,66.392,-83.406,66.371,-83.523,66.369,-83.59,66.388,-83.628,66.461,-83.651,66.485,-83.739,66.534,-83.92,66.679,-83.998,66.729,-84.05,66.74,-84.154,66.732,-84.208,66.736,-84.324,66.782,-84.366,66.811,-84.361,66.823,-84.273,66.839,-84.31,66.863,-84.466,66.927,-84.531,66.961,-84.538,66.973,-84.693,67.017,-84.846,67.029,-85.04,66.956,-85.114,66.907,-85.111,66.891,-85.018,66.872,-84.978,66.881,-84.899,66.927,-84.857,66.941,-84.738,66.934,-84.639,66.902,-84.603,66.875,-84.59,66.857,-84.319,66.712,-84.223,66.682,-84.183,66.648,-84.153,66.59,-84.094,66.526,-83.964,66.421,-83.826,66.29,-83.798,66.238,-83.869,66.214,-83.905,66.212,-84.012,66.231,-84.293,66.292,-84.324,66.291,-84.398,66.259,-84.459,66.186,-84.478,66.179,-84.628,66.208,-84.908,66.271,-85.096,66.325,-85.192,66.37,-85.307,66.44,-85.442,66.537,-85.604,66.568,-85.792,66.533,-86.063,66.52,-86.633,66.531,-86.708,66.523,-86.737,66.511,-86.689,66.457,-86.7,66.443,-86.738,66.433,-86.747,66.417,-86.685,66.36,-86.585,66.322,-86.301,66.27,-86.113,66.225,-86.001,66.187,-85.964,66.154,-85.959,66.119,-86.012,66.048,-86.043,66.023,-86.702,65.671,-86.953,65.528,-87.081,65.441,-87.194,65.383,-87.291,65.355,-87.453,65.339,-87.678,65.335,-87.97,65.349,-88.121,65.395,-88.395,65.516,-88.587,65.588,-88.672,65.612,-88.744,65.679,-88.808,65.692,-88.946,65.703,-89.088,65.739,-89.42,65.861,-89.593,65.909,-89.749,65.936,-89.89,65.941,-89.944,65.934,-89.848,65.872,-89.89,65.869,-90.004,65.883,-90.117,65.882,-90.316,65.926,-90.513,65.921,-90.655,65.929,-90.826,65.954,-91.01,65.966,-91.305,65.965,-91.412,65.959,-91.427,65.948,-91.285,65.894,-91.041,65.83,-91.074,65.886,-91.065,65.9,-90.983,65.919,-90.597,65.885,-90.159,65.813,-90.048,65.806,-89.924,65.78,-89.788,65.737,-89.6,65.648,-89.242,65.446,-89.127,65.396,-88.974,65.348,-88.198,65.28,-87.93,65.28,-87.392,65.261,-87.108,65.225,-87.028,65.198,-87.003,65.109,-87.029,65.064,-87.183,64.927,-87.281,64.826,-87.885,64.4,-87.964,64.302,-87.998,64.244,-88.106,64.183,-88.379,64.089,-88.653,64.009,-88.818,63.992,-88.964,64.011,-89.06,64.034,-89.201,64.114,-89.209,64.105,-89.108,63.981,-89.132,63.969,-89.215,63.984,-89.404,64.04,-89.465,64.03,-89.501,64.015,-89.551,64.015,-89.616,64.031,-89.733,64.077,-89.764,64.1,-89.792,64.168,-89.811,64.181,-90.042,64.141,-90.08,64.128,-89.986,64.1,-89.954,64.081,-89.861,63.979,-89.856,63.957,-89.922,63.944,-90.142,63.982,-90.168,63.979,-90.06,63.877,-90.018,63.829,-90.013,63.804,-90.155,63.69,-90.245,63.642,-90.369,63.624,-90.446,63.636,-90.533,63.665,-90.596,63.661,-90.635,63.624,-90.707,63.597,-90.812,63.581,-90.946,63.588,-91.108,63.618,-91.539,63.726,-91.675,63.742,-91.926,63.757,-91.956,63.772,-91.954,63.787,-91.919,63.801,-91.929,63.812,-91.982,63.822,-92.038,63.813,-92.095,63.784,-92.195,63.776,-92.338,63.788,-92.55,63.83,-92.97,63.938,-93.43,64.029,-93.696,64.147,-93.597,64.041,-93.605,64.004,-93.656,63.973,-93.664,63.941,-93.56,63.865,-93.416,63.838,-93.27,63.841,-93.266,63.853,-93.327,63.872,-93.38,63.9,-93.406,63.941,-93.379,63.948,-93.25,63.927,-93.166,63.902,-92.529,63.761,-92.339,63.735,-92.196,63.708,-92.157,63.692,-92.205,63.657,-92.461,63.569,-92.465,63.555,-92.29,63.563,-92.077,63.64,-91.957,63.676,-91.842,63.698,-91.686,63.66,-91.489,63.562,-91.33,63.507,-91.103,63.476,-90.97,63.443,-90.747,63.352,-90.711,63.304,-90.691,63.111,-90.699,63.064,-90.728,63.017,-90.778,62.972,-90.871,62.946,-91.008,62.94,-91.115,62.922,-91.349,62.819,-91.449,62.804,-91.87,62.835,-92.034,62.863,-92.11,62.862,-92.152,62.839,-92.196,62.829,-92.361,62.819,-92.388,62.801,-92.378,62.772,-92.345,62.734,-92.305,62.712,-92.243,62.684,-92.149,62.665,-91.956,62.645,-91.936,62.592,-91.944,62.575,-92.008,62.541,-92.081,62.544,-92.207,62.585,-92.27,62.587,-92.324,62.565,-92.4,62.557,-92.497,62.565,-92.551,62.547,-92.562,62.503,-92.595,62.47,-92.707,62.418,-92.768,62.38,-92.766,62.35,-92.701,62.328,-92.627,62.279,-92.544,62.202,-92.528,62.168,-92.579,62.177,-92.648,62.208,-92.735,62.26,-92.866,62.306,-93.154,62.367,-93.205,62.365,-93.179,62.35,-92.988,62.286,-92.914,62.245,-92.906,62.215,-93.066,62.15,-93.07,62.128,-93.028,62.109,-93.016,62.093,-93.073,62.061,-93.167,62.034,-93.35,62.03,-93.366,62.015,-93.297,61.982,-93.273,61.961,-93.333,61.933,-93.372,61.929,-93.582,61.942,-93.527,61.872,-93.494,61.847,-93.43,61.812,-93.314,61.78,-93.312,61.767,-93.352,61.74,-93.421,61.706,-93.71,61.603,-93.913,61.481,-93.941,61.444,-93.889,61.364,-93.889,61.344,-93.942,61.308,-94.061,61.318,-94.083,61.304,-94.055,61.266,-94.05,61.211,-94.068,61.139,-94.154,61.025,-94.309,60.871,-94.427,60.731,-94.509,60.605,-94.569,60.542,-94.679,60.538,-94.762,60.498,-94.705,60.478,-94.671,60.453,-94.647,60.416,-94.67,60.301,-94.742,60.107,-94.786,59.953,-94.777,59.478,-94.788,59.268,-94.82,59.151,-94.87,59.088,-94.957,59.069,-94.847,59.05,-94.776,59.021,-94.744,58.975,-94.713,58.903,-94.673,58.87,-94.624,58.876,-94.579,58.868,-94.54,58.848,-94.419,58.746,-94.287,58.716,-94.281,58.659,-94.333,58.339,-94.332,58.297,-94.272,58.378,-94.209,58.626,-94.123,58.737,-94.056,58.76,-93.78,58.773,-93.486,58.744,-93.375,58.741,-93.278,58.756,-93.179,58.726,-93.155,58.695,-93.127,58.564,-93.1,58.49,-92.925,58.225,-92.842,58.076,-92.74,57.844,-92.702,57.778,-92.49,57.469,-92.449,57.385,-92.433,57.32,-92.44,57.275,-92.478,57.205,-92.548,57.111,-92.614,57.039,-92.675,56.99,-92.738,56.953,-92.802,56.928,-92.798,56.922,-92.725,56.934,-92.651,56.958,-92.51,57.022,-92.456,57.037,-92.303,57.046,-92.298,57.023,-92.372,56.975,-92.356,56.971,-92.249,57.009,-92.018,57.064,-91.111,57.241,-90.897,57.257,-90.592,57.224,-90.345,57.149,-90.075,57.052,-89.791,56.981,-89.342,56.915,-89.212,56.884,-88.948,56.851,-88.826,56.814,-88.68,56.725,-88.447,56.609,-88.271,56.536,-88.075,56.467,-87.878,56.342,-87.561,56.056,-87.482,56.021,-87.287,55.975,-86.919,55.915,-86.377,55.773,-86.139,55.718,-85.984,55.696,-85.831,55.657,-85.677,55.601,-85.559,55.54,-85.478,55.474,-85.407,55.431,-85.283,55.383,-85.218,55.349,-85.212,55.297,-85.362,55.095,-85.365,55.079,-85.214,55.224,-85.129,55.266,-85.061,55.286,-84.92,55.283,-84.706,55.259,-84.518,55.259,-84.356,55.283,-84.219,55.293,-84.105,55.291,-84.023,55.298,-83.972,55.314,-83.911,55.315,-83.668,55.265,-83.569,55.262,-83.214,55.215,-82.986,55.231,-82.947,55.222,-82.868,55.161,-82.801,55.156,-82.687,55.166,-82.577,55.149,-82.393,55.068,-82.308,54.998,-82.227,54.856,-82.219,54.813,-82.371,54.483,-82.418,54.356,-82.424,54.245,-82.394,54.18,-82.264,54.073,-82.24,54.045,-82.163,53.886,-82.141,53.818,-82.15,53.74,-82.191,53.611,-82.18,53.513,-82.146,53.365,-82.159,53.264,-82.219,53.211,-82.26,53.16,-82.292,53.066,-82.292,53.031,-82.26,52.961,-82.203,52.922,-82.108,52.877,-82.02,52.812,-81.859,52.651,-81.742,52.564,-81.599,52.433,-81.572,52.367,-81.612,52.324,-81.661,52.294,-81.776,52.254,-81.828,52.224,-81.815,52.217,-81.648,52.239,-81.55,52.237,-81.466,52.204,-81.398,52.142,-81.285,52.089,-81.127,52.045,-80.969,51.972,-80.706,51.798,-80.658,51.758,-80.588,51.667,-80.496,51.525,-80.448,51.432,-80.443,51.389,-80.496,51.345,-80.673,51.265,-80.851,51.125,-80.795,51.132,-80.677,51.191,-80.478,51.307,-80.368,51.33,-80.266,51.316,-80.104,51.283,-79.96,51.235,-79.836,51.173,-79.652,51.008,-79.456,50.876,-79.348,50.763,-79.381,50.835,-79.453,50.917,-79.636,51.049,-79.714,51.118,-79.731,51.15,-79.737,51.186,-79.723,51.252,-79.689,51.347,-79.643,51.414,-79.586,51.452,-79.547,51.494,-79.528,51.538,-79.498,51.57,-79.339,51.628,-79.297,51.623,-79.264,51.552,-79.226,51.537,-79.153,51.526,-79.091,51.502,-79.041,51.464,-79.005,51.425,-78.984,51.386,-78.937,51.259,-78.903,51.2,-78.898,51.272,-78.858,51.384,-78.827,51.43,-78.731,51.497,-78.736,51.527,-78.776,51.566,-78.978,51.734,-78.982,51.775,-78.928,51.799,-78.891,51.845,-78.871,51.913,-78.828,51.963,-78.702,52.033,-78.593,52.14,-78.537,52.213,-78.492,52.252,-78.448,52.261,-78.513,52.291,-78.526,52.311,-78.529,52.399,-78.557,52.492,-78.601,52.535,-78.724,52.628,-78.744,52.655,-78.766,52.76,-78.754,52.812,-78.722,52.856,-78.74,52.899,-78.854,52.976,-78.898,53.043,-78.947,53.206,-78.992,53.41,-79.043,53.56,-79.1,53.657,-79.113,53.717,-79.081,53.742,-79.04,53.818,-79.003,53.837,-78.946,53.832,-78.944,53.84,-79.032,53.881,-79.075,53.932,-79.073,53.951,-78.996,54.002,-79.01,54.024,-79.067,54.052,-79.242,54.099,-79.179,54.117,-79.139,54.157,-79.147,54.169,-79.216,54.186,-79.296,54.217,-79.356,54.263,-79.431,54.337,-79.476,54.395,-79.521,54.492,-79.598,54.602,-79.632,54.629,-79.67,54.647,-79.714,54.655,-79.712,54.672,-79.666,54.697,-78.909,54.881,-78.846,54.908,-78.475,55.011,-78.304,55.069,-78.129,55.151,-77.891,55.236,-77.775,55.291,-77.702,55.344,-77.325,55.556,-77.165,55.664,-77.073,55.756,-76.938,55.867,-76.762,55.996,-76.65,56.107,-76.604,56.2,-76.546,56.359,-76.53,56.5,-76.52,56.707,-76.526,56.892,-76.573,57.181,-76.601,57.272,-76.655,57.381,-76.786,57.599,-76.81,57.658,-76.891,57.758,-77.157,58.019,-77.489,58.195,-77.552,58.24,-77.684,58.291,-77.884,58.351,-78.014,58.399,-78.352,58.581,-78.463,58.602,-78.506,58.649,-78.515,58.682,-78.502,58.769,-78.483,58.829,-78.459,58.873,-78.431,58.902,-78.244,59.035,-78.14,59.142,-78.068,59.2,-77.988,59.246,-77.843,59.305,-77.761,59.38,-77.779,59.41,-77.845,59.444,-77.859,59.476,-77.749,59.558,-77.733,59.581,-77.748,59.658,-77.726,59.676,-77.59,59.681,-77.397,59.569,-77.349,59.579,-77.411,59.61,-77.485,59.685,-77.475,59.716,-77.332,59.797,-77.328,59.833,-77.368,59.884,-77.373,59.925,-77.289,60.022,-77.312,60.042,-77.547,60.061,-77.586,60.088,-77.572,60.101,-77.461,60.133,-77.453,60.146,-77.649,60.363,-77.681,60.427,-77.598,60.507,-77.504,60.543,-77.516,60.563,-77.639,60.567,-77.715,60.578,-77.791,60.64,-77.761,60.679,-77.734,60.697,-77.661,60.79,-77.59,60.809,-77.603,60.825,-77.872,60.786,-77.998,60.818,-78.122,60.81,-78.181,60.819,-78.16,60.852,-77.934,61.003,-77.83,61.084,-77.765,61.158,-77.731,61.206,-77.727,61.231,-77.75,61.393,-77.736,61.437,-77.649,61.479,-77.514,61.556,-77.698,61.626,-77.814,61.695,-77.89,61.729,-77.948,61.762,-78.021,61.832,-78.077,61.923,-78.137,62.107,-78.147,62.209,-78.133,62.282,-78.109,62.318,-78.068,62.355,-77.9,62.427,-77.604,62.531,-77.372,62.573,-77.205,62.55,-76.879,62.525,-76.616,62.466,-75.817,62.316,-75.676,62.25,-75.809,62.193,-75.79,62.18,-75.489,62.286,-75.409,62.307,-75.341,62.312,-75.114,62.271,-75.023,62.264,-74.908,62.23,-74.633,62.116,-74.613,62.125,-74.69,62.183,-74.646,62.211,-74.429,62.272,-74.205,62.321,-74.046,62.37,-73.878,62.434,-73.764,62.469,-73.705,62.473,-73.63,62.454,-73.428,62.369,-73.299,62.325,-73.195,62.279,-73.049,62.198,-72.992,62.18,-72.882,62.125,-72.735,62.131,-72.687,62.125,-72.671,62.114,-72.646,62.077,-72.633,62.053,-72.632,62.027,-72.666,61.955,-72.772,61.84,-72.727,61.839,-72.661,61.863,-72.574,61.907,-72.506,61.923,-72.361,61.888,-72.226,61.832,-72.178,61.802,-72.126,61.753,-72.081,61.728,-72.04,61.68,-72.043,61.665,-72.082,61.641,-72.247,61.602,-72.216,61.587,-72.023,61.612,-71.964,61.636,-71.922,61.677,-71.866,61.689,-71.638,61.617,-71.605,61.592,-71.619,61.573,-71.656,61.551,-71.756,61.527,-71.841,61.466,-71.854,61.44,-71.794,61.421,-71.645,61.413,-71.646,61.399,-71.732,61.372,-71.743,61.337,-71.552,61.213,-71.423,61.159,-71.348,61.149,-71.175,61.147,-71.035,61.126,-70.723,61.055,-70.541,61.042,-70.384,61.064,-70.279,61.069,-70.187,61.041,-70.158,61.021,-70.144,60.981,-70.146,60.922,-70.095,60.88,-69.992,60.856,-69.909,60.86,-69.8,60.907,-69.708,60.915,-69.678,60.95,-69.65,61.014,-69.624,61.05,-69.557,61.06,-69.503,61.04,-69.472,61.011,-69.414,60.922,-69.398,60.883,-69.405,60.847,-69.433,60.814,-69.49,60.78,-69.574,60.743,-69.64,60.69,-69.721,60.567,-69.751,60.487,-69.759,60.44,-69.756,60.389,-69.741,60.332,-69.708,60.286,-69.633,60.22,-69.629,60.199,-69.623,60.145,-69.63,60.122,-69.674,60.076,-69.796,60.03,-69.963,60.018,-70.509,60.015,-70.655,60.026,-70.62,59.984,-70.467,59.971,-70.327,59.971,-69.806,59.945,-69.734,59.918,-69.673,59.871,-69.63,59.822,-69.587,59.722,-69.579,59.675,-69.602,59.623,-69.656,59.565,-69.692,59.488,-69.711,59.393,-69.682,59.342,-69.4,59.338,-69.344,59.303,-69.35,59.277,-69.45,59.18,-69.46,59.152,-69.414,59.087,-69.42,59.068,-69.448,59.049,-69.475,59,-69.5,58.921,-69.532,58.869,-69.608,58.829,-69.648,58.821,-69.677,58.831,-69.753,58.94,-69.784,58.956,-69.814,58.946,-69.829,58.929,-69.829,58.905,-69.842,58.881,-69.868,58.856,-69.979,58.816,-70.16,58.789,-70.154,58.761,-70.033,58.745,-69.879,58.697,-69.79,58.689,-69.651,58.728,-69.382,58.851,-69.271,58.884,-69.173,58.897,-69.064,58.898,-68.942,58.889,-68.698,58.905,-68.637,58.893,-68.563,58.866,-68.475,58.823,-68.414,58.783,-68.381,58.744,-68.326,58.595,-68.253,58.557,-68.235,58.528,-68.229,58.485,-68.234,58.399,-68.315,58.227,-68.357,58.163,-68.468,58.076,-68.597,58.037,-68.826,58,-68.945,57.969,-69.035,57.926,-69.041,57.902,-68.781,57.976,-68.495,58.012,-68.414,58.052,-68.352,58.091,-68.289,58.178,-68.176,58.403,-68.111,58.473,-68.021,58.485,-67.981,58.461,-67.888,58.329,-67.888,58.296,-67.911,58.267,-68.064,58.139,-68.009,58.152,-67.856,58.273,-67.823,58.31,-67.805,58.365,-67.756,58.405,-67.737,58.385,-67.69,58.244,-67.688,58.14,-67.681,58.107,-67.698,58.009,-67.678,57.991,-67.632,58.076,-67.617,58.14,-67.596,58.186,-67.57,58.213,-67.382,58.3,-67.163,58.37,-67.019,58.433,-66.9,58.463,-66.722,58.491,-66.608,58.549,-66.558,58.637,-66.515,58.697,-66.48,58.731,-66.362,58.791,-66.299,58.795,-66.237,58.772,-66.168,58.727,-66.091,58.659,-66.045,58.606,-66.03,58.567,-66.017,58.431,-66.002,58.431,-65.931,58.535,-65.923,58.572,-65.928,58.611,-65.95,58.65,-66.021,58.735,-66.049,58.788,-66.043,58.821,-65.967,58.839,-65.855,58.847,-65.836,58.86,-65.918,58.896,-65.921,58.915,-65.841,58.977,-65.795,58.98,-65.704,58.971,-65.722,59.003,-65.721,59.024,-65.695,59.032,-65.544,59.012,-65.526,59.036,-65.396,59.038,-65.384,59.06,-65.496,59.091,-65.606,59.111,-65.64,59.128,-65.666,59.153,-65.7,59.213,-65.692,59.229,-65.661,59.23,-65.607,59.213,-65.578,59.245,-65.545,59.32,-65.513,59.35,-65.412,59.315,-65.407,59.33,-65.489,59.448,-65.475,59.47,-65.35,59.479,-65.274,59.464,-65.074,59.378,-65.038,59.388,-65.069,59.411,-65.171,59.462,-65.263,59.495,-65.346,59.511,-65.407,59.539,-65.475,59.617,-65.486,59.649,-65.481,59.69,-65.433,59.777,-65.406,59.795,-65.358,59.809,-65.289,59.818,-65.212,59.81,-65.054,59.753,-65.028,59.771,-65.113,59.802,-65.159,59.83,-65.181,59.867,-65.172,59.908,-65.105,59.993,-65.073,60.062,-64.931,60.252,-64.89,60.287,-64.845,60.308,-64.817,60.331,-64.706,60.336,-64.499,60.268,-64.436,60.228,-64.42,60.171,-64.528,60.095,-64.713,60.037,-64.768,60.012,-64.733,59.998,-64.559,60.043,-64.408,60.065,-64.283,60.064,-64.183,59.973,-64.169,59.847,-64.226,59.741,-64.151,59.794,-64.056,59.823,-63.979,59.754,-63.969,59.698,-63.929,59.645,-63.841,59.574,-63.75,59.513,-63.85,59.448,-63.971,59.409,-63.945,59.38,-63.781,59.349,-63.759,59.319,-63.776,59.277,-63.752,59.277,-63.637,59.341,-63.54,59.333,-63.415,59.194,-63.506,59.115,-63.646,59.079,-63.756,59.063,-63.91,59.066,-63.971,59.054,-63.941,59.027,-63.794,59.027,-63.568,59.047,-63.399,59.08,-63.326,59.082,-63.248,59.068,-63.223,59.057,-63.304,59.034,-63.31,59.026,-63.279,59.003,-63.216,58.928,-63.222,58.911,-63.282,58.867,-63.185,58.858,-63.05,58.878,-63.008,58.855,-62.926,58.765,-62.874,58.672,-63.102,58.546,-63.219,58.52,-63.39,58.453,-63.438,58.399,-63.537,58.33,-63.474,58.331,-63.296,58.441,-63.21,58.467,-63.146,58.46,-63.12,58.442,-63.132,58.411,-63.076,58.415,-62.837,58.479,-62.737,58.492,-62.608,58.496,-62.594,58.474,-62.674,58.319,-62.812,58.2,-63.063,58.127,-63.152,58.084,-63.262,58.015,-63.22,58.002,-62.981,58.093,-62.818,58.129,-62.588,58.158,-62.486,58.154,-62.306,57.972,-62.202,57.955,-62.117,57.964,-61.959,57.912,-61.899,57.861,-61.914,57.825,-61.968,57.803,-61.995,57.769,-61.931,57.669,-61.968,57.612,-62.084,57.562,-62.167,57.537,-62.254,57.529,-62.339,57.485,-62.377,57.478,-62.496,57.489,-62.455,57.462,-62.396,57.448,-62.303,57.441,-62.194,57.455,-62.088,57.453,-61.921,57.421,-61.851,57.381,-61.85,57.37,-61.886,57.348,-61.939,57.274,-61.977,57.248,-61.945,57.228,-61.861,57.198,-61.798,57.186,-61.716,57.196,-61.629,57.183,-61.334,57.011,-61.346,56.922,-61.39,56.853,-61.373,56.776,-61.372,56.681,-61.532,56.655,-62.062,56.699,-62.366,56.767,-62.382,56.788,-62.296,56.833,-62.372,56.836,-62.46,56.818,-62.497,56.802,-62.396,56.73,-62.117,56.667,-61.992,56.591,-61.855,56.584,-61.813,56.571,-61.738,56.526,-61.76,56.511,-61.899,56.505,-62.01,56.454,-61.94,56.424,-61.692,56.397,-61.515,56.39,-61.425,56.361,-61.499,56.328,-61.707,56.289,-61.713,56.231,-61.559,56.208,-61.421,56.222,-61.365,56.216,-61.324,56.076,-61.301,56.047,-61.449,56.022,-61.45,55.996,-61.351,55.974,-61.188,55.955,-61.134,55.93,-61.123,55.889,-61.089,55.866,-60.996,55.862,-60.893,55.914,-60.832,55.958,-60.743,55.941,-60.737,55.887,-60.631,55.825,-60.593,55.815,-60.562,55.727,-60.476,55.805,-60.413,55.789,-60.341,55.785,-60.365,55.709,-60.408,55.65,-60.352,55.612,-60.308,55.557,-60.192,55.481,-60.224,55.444,-60.361,55.366,-60.433,55.243,-60.45,55.2,-60.521,55.129,-60.617,55.06,-60.557,55.067,-60.341,55.194,-60.213,55.236,-59.93,55.259,-59.862,55.295,-59.759,55.31,-59.696,55.269,-59.689,55.196,-59.605,55.173,-59.518,55.197,-59.438,55.176,-59.486,55.13,-59.742,54.943,-59.816,54.867,-59.838,54.814,-59.75,54.887,-59.429,55.056,-59.394,55.081,-59.324,55.153,-59.26,55.2,-59.086,55.183,-58.997,55.149,-58.956,55.055,-58.886,54.952,-58.78,54.838,-58.5,54.783,-58.398,54.774,-58.223,54.813,-58.195,54.866,-58.058,54.882,-57.962,54.876,-57.929,54.773,-57.827,54.719,-57.725,54.674,-57.627,54.65,-57.483,54.64,-57.404,54.591,-57.404,54.57,-57.485,54.517,-57.563,54.44,-57.699,54.387,-57.889,54.384,-58.151,54.35,-58.162,54.32,-58.22,54.286,-58.359,54.253,-58.435,54.228,-58.558,54.103,-58.633,54.05,-58.719,54.039,-58.841,54.044,-58.92,54.033,-58.978,54.01,-59.013,53.976,-59.039,53.964,-59.201,53.929,-59.497,53.834,-59.653,53.831,-59.749,53.842,-59.823,53.834,-59.873,53.808,-60.014,53.762,-60.057,53.733,-60.081,53.701,-60.1,53.634,-60.117,53.61,-60.145,53.596,-60.263,53.61,-60.395,53.653,-60.37,53.607,-60.16,53.53,-60.1,53.487,-60.157,53.45,-60.29,53.391,-60.306,53.36,-60.251,53.344,-60.273,53.317,-60.346,53.289,-60.337,53.277,-60.329,53.266,-60.148,53.307,-59.987,53.393,-59.882,53.48,-59.829,53.505,-59.621,53.537,-59.498,53.575,-59.322,53.644,-59.129,53.744,-58.92,53.875,-58.652,53.978,-58.327,54.052,-58.088,54.09,-57.936,54.091,-57.928,54.104,-58.065,54.127,-58.177,54.131,-58.317,54.114,-58.361,54.154,-58.356,54.172,-58.31,54.202,-58.192,54.228,-57.615,54.191,-57.416,54.163,-57.199,53.924,-57.149,53.848,-57.135,53.792,-57.157,53.757,-57.244,53.715,-57.489,53.633,-57.524,53.611,-57.527,53.6,-57.42,53.583,-57.386,53.561,-57.332,53.469,-57.221,53.529,-57.012,53.673,-56.841,53.739,-56.697,53.758,-56.524,53.766,-56.465,53.765,-56.444,53.718,-56.354,53.624,-56.27,53.6,-56.11,53.588,-55.966,53.471,-55.911,53.391,-55.859,53.344,-55.863,53.312,-55.855,53.286,-55.817,53.246,-55.798,53.212,-55.808,53.135,-55.892,53,-55.83,52.878,-55.858,52.823,-55.873,52.736,-55.819,52.677,-55.803,52.643,-55.848,52.623,-56.167,52.575,-56.292,52.574,-56.325,52.545,-56.228,52.536,-56.053,52.537,-55.84,52.508,-55.746,52.475,-55.706,52.428,-55.716,52.392,-55.777,52.364,-55.897,52.37,-56.012,52.394,-56.005,52.37,-55.834,52.31,-55.783,52.28,-55.691,52.242,-55.673,52.19,-55.695,52.138,-56.017,51.929,-56.283,51.797,-56.549,51.681,-56.976,51.458,-57.018,51.447,-57.096,51.443,-57.299,51.478,-57.462,51.469,-57.77,51.426,-57.854,51.4,-58.023,51.322,-58.089,51.311,-58.27,51.295,-58.442,51.306,-58.51,51.295,-58.593,51.257,-58.615,51.237,-58.638,51.172,-59.055,50.879,-59.165,50.78,-59.378,50.675,-59.612,50.492,-59.815,50.418,-59.886,50.316,-60.08,50.255,-60.438,50.239,-60.608,50.221,-60.807,50.25,-60.956,50.205,-61.181,50.192,-61.29,50.202,-61.725,50.104,-61.835,50.197,-61.92,50.233,-62.165,50.239,-62.362,50.277,-62.541,50.285,-62.715,50.302,-62.83,50.301,-62.95,50.291,-63.136,50.294,-63.239,50.243,-63.587,50.258,-63.734,50.305,-63.854,50.314,-64.016,50.304,-64.17,50.269,-64.509,50.309,-64.868,50.275,-65.181,50.298,-65.269,50.32,-65.762,50.259,-65.955,50.294,-66.126,50.201,-66.242,50.22,-66.369,50.207,-66.411,50.224,-66.496,50.212,-66.55,50.161,-66.622,50.155,-66.741,50.066,-66.941,49.994,-67.234,49.602,-67.262,49.451,-67.372,49.348,-67.469,49.335,-67.549,49.332,-68.056,49.257,-68.282,49.197,-68.221,49.15,-68.295,49.114,-68.414,49.1,-68.544,49.056,-68.628,49.007,-68.669,48.94,-68.929,48.829,-69.231,48.574,-69.375,48.386,-69.55,48.251,-69.674,48.199,-69.762,48.191,-69.852,48.207,-70.001,48.271,-70.111,48.278,-70.384,48.367,-71.018,48.456,-70.923,48.422,-70.839,48.367,-70.671,48.353,-70.501,48.354,-70.145,48.244,-69.971,48.206,-69.866,48.172,-69.775,48.098,-69.84,47.953,-69.906,47.832,-69.994,47.74,-70.3,47.503,-70.448,47.423,-70.706,47.14,-70.973,47.007,-71.116,46.925,-71.268,46.796,-71.625,46.698,-71.757,46.674,-71.88,46.687,-72.028,46.607,-72.205,46.559,-72.257,46.485,-72.68,46.287,-72.843,46.262,-72.981,46.21,-73.022,46.12,-73.145,46.066,-73.18,46.025,-73.284,45.9,-73.477,45.738,-73.712,45.711,-73.798,45.655,-73.897,45.564,-74.038,45.502,-74.315,45.531,-74.248,45.493,-74,45.433,-73.974,45.345,-74.098,45.324,-74.358,45.206,-74.709,45.004,-74.762,44.999,-74.857,45.004,-74.996,44.97,-75.179,44.899,-75.401,44.772,-75.792,44.497,-75.819,44.468,-75.876,44.417,-76.02,44.363,-76.151,44.304,-76.186,44.242,-76.249,44.214,-76.465,44.058,-76.586,43.924,-76.696,43.785,-76.82,43.629,-77.073,43.627,-77.267,43.627,-77.597,43.629,-77.879,43.63,-78.215,43.631,-78.458,43.631,-78.72,43.625,-78.846,43.583,-79.002,43.527,-79.172,43.467,-79.083,43.331,-79.059,43.278,-79.066,43.106,-79.048,43.087,-79.029,43.062,-79.026,43.017,-79.012,42.997,-78.981,42.981,-78.946,42.961,-78.921,42.935,-78.915,42.909,-78.939,42.864,-79.037,42.802,-79.174,42.749,-79.446,42.651,-79.762,42.539,-80.036,42.441,-80.248,42.366,-80.683,42.3,-81.028,42.247,-81.278,42.209,-81.507,42.103,-81.761,41.987,-81.974,41.889,-82.213,41.779,-82.439,41.675,-82.69,41.675,-82.866,41.753,-83.03,41.833,-83.142,41.976,-83.15,42.142,-83.11,42.251,-83.073,42.3,-83.004,42.332,-82.868,42.385,-82.744,42.493,-82.645,42.558,-82.545,42.625,-82.488,42.74,-82.417,43.017,-82.408,43.073,-82.305,43.263,-82.19,43.474,-82.138,43.571,-82.197,43.822,-82.241,44.015,-82.281,44.192,-82.327,44.392,-82.368,44.573,-82.407,44.744,-82.447,44.916,-82.485,45.084,-82.515,45.204,-82.551,45.347,-82.76,45.448,-82.919,45.518,-83.179,45.633,-83.397,45.729,-83.593,45.817,-83.469,45.995,-83.48,46.024,-83.525,46.059,-83.616,46.117,-83.669,46.123,-83.763,46.109,-83.913,46.073,-83.978,46.085,-84.029,46.147,-84.088,46.227,-84.108,46.289,-84.115,46.371,-84.15,46.445,-84.128,46.484,-84.123,46.503,-84.125,46.527,-84.149,46.543,-84.192,46.55,-84.337,46.519,-84.402,46.516,-84.44,46.498,-84.502,46.462,-84.562,46.457,-84.666,46.543,-84.779,46.637,-84.827,46.767,-84.876,46.9,-85.07,46.98,-85.264,47.06,-85.458,47.14,-85.652,47.22,-85.846,47.3,-86.04,47.38,-86.234,47.46,-86.429,47.54,-86.496,47.567,-86.672,47.636,-86.922,47.735,-87.208,47.848,-87.494,47.962,-87.744,48.061,-87.921,48.13,-87.987,48.157,-88.161,48.225,-88.378,48.303,-88.612,48.264,-88.899,48.156,-89.063,48.094,-89.186,48.047,-89.273,48.02,-89.456,47.996,-89.551,48,-89.775,48.015,-89.901,47.995,-89.994,48.015,-90.04,48.078,-90.092,48.118,-90.32,48.099,-90.607,48.113,-90.744,48.105,-90.797,48.131,-90.84,48.201,-90.916,48.209,-91.043,48.194,-91.221,48.105,-91.387,48.059,-91.518,48.058,-91.647,48.105,-91.858,48.198,-92.005,48.302,-92.172,48.338,-92.299,48.329,-92.348,48.277,-92.415,48.277,-92.461,48.366,-92.501,48.435,-92.583,48.465,-92.733,48.532,-92.837,48.568,-92.996,48.612,-93.052,48.62,-93.155,48.625,-93.258,48.629,-93.378,48.617,-93.464,48.561,-93.564,48.537,-93.708,48.525,-93.804,48.549,-93.852,48.607,-94.055,48.659,-94.414,48.704,-94.621,48.743,-94.675,48.774,-94.705,48.808,-94.713,48.863,-94.803,49.003,-94.843,49.119,-94.86,49.259,-94.854,49.305,-94.875,49.319,-94.939,49.349,-95.155,49.37,-95.158,49.203,-95.162,48.992,-95.398,48.993,-95.824,48.993,-96.251,48.993,-96.677,48.993,-97.103,48.993,-97.53,48.993,-97.956,48.993,-98.383,48.993,-98.809,48.993,-99.235,48.993,-99.662,48.993,-100.088,48.993,-100.515,48.993,-100.941,48.993,-101.367,48.993,-101.794,48.993,-102.22,48.993,-102.646,48.993,-103.073,48.993,-103.499,48.993,-103.926,48.993,-104.352,48.993,-104.778,48.993,-105.205,48.993,-105.631,48.993,-106.057,48.993,-106.484,48.993,-106.91,48.993,-107.337,48.993,-107.763,48.993,-108.189,48.993,-108.616,48.993,-109.042,48.993,-109.469,48.993,-109.895,48.993,-110.321,48.993,-110.748,48.993,-111.174,48.993,-111.6,48.993,-112.027,48.993,-112.453,48.993,-112.88,48.993,-113.306,48.993,-113.732,48.993,-114.159,48.993,-114.585,48.993,-115.012,48.993,-115.438,48.993,-115.864,48.993,-116.291,48.993,-116.717,48.993,-117.143,48.993,-117.57,48.993,-117.996,48.993,-118.423,48.993,-118.849,48.993,-119.275,48.993,-119.702,48.993,-120.128,48.993,-120.554,48.993,-120.981,48.993,-121.407,48.993,-121.834,48.993,-122.26,48.993,-122.686,48.993,-122.789,48.993,-122.827,49.028,-122.924,49.075,-122.963,49.075,-123.002,49.061,-123.027,49.039,-123.049,48.993,-123.063,48.978,-123.077,48.98,-123.086,48.993,-123.118,49.056,-123.109,49.085,-123.077,49.118,-123.08,49.131,-123.15,49.121,-123.182,49.129,-123.196,49.148,-123.191,49.22,-123.229,49.26,-123.184,49.278,-123.067,49.292,-122.948,49.293,-122.913,49.323,-122.879,49.399,-122.964,49.329,-123.016,49.322,-123.174,49.348,-123.277,49.344,-123.291,49.359,-123.286,49.375,-123.264,49.39,-123.248,49.443,-123.223,49.59,-123.191,49.644,-123.18,49.674,-123.187,49.68,-123.325,49.578,-123.337,49.545,-123.322,49.517,-123.336,49.459,-123.399,49.442,-123.437,49.451,-123.508,49.402,-123.531,49.397,-123.859,49.483,-123.892,49.495,-123.948,49.535,-124.029,49.603,-124.054,49.662,-124.024,49.711,-123.993,49.736,-123.96,49.736,-123.923,49.718,-123.847,49.637,-123.817,49.587,-123.739,49.594,-123.613,49.658,-123.582,49.681,-123.708,49.657,-123.763,49.658,-123.818,49.685,-123.874,49.737,-123.904,49.795,-123.904,49.981,-123.885,50.017,-123.824,50.044,-123.785,50.088,-123.788,50.107,-123.825,50.144,-123.88,50.174,-123.934,50.188,-123.946,50.184,-123.863,50.103,-123.866,50.072,-123.957,49.993,-123.971,49.97,-123.972,49.892,-123.985,49.876,-124.059,49.854,-124.142,49.793,-124.281,49.772,-124.413,49.778,-124.483,49.808,-124.702,49.958,-124.782,50.02,-124.784,50.073,-124.934,50.258,-124.933,50.298,-124.986,50.356,-125.044,50.364,-125.057,50.419,-124.937,50.537,-124.863,50.637,-124.854,50.669,-124.858,50.717,-124.875,50.826,-124.86,50.872,-124.934,50.811,-124.949,50.765,-124.931,50.718,-124.943,50.666,-124.985,50.592,-125.059,50.514,-125.21,50.476,-125.476,50.497,-125.507,50.507,-125.526,50.534,-125.539,50.649,-125.556,50.635,-125.586,50.574,-125.61,50.486,-125.641,50.466,-125.698,50.465,-125.741,50.479,-125.772,50.508,-125.84,50.511,-125.965,50.487,-126.024,50.497,-126.094,50.498,-126.237,50.523,-126.404,50.53,-126.45,50.55,-126.447,50.588,-126.416,50.607,-126.239,50.624,-126.067,50.664,-125.898,50.684,-125.904,50.705,-125.981,50.711,-126.37,50.667,-126.493,50.672,-126.514,50.679,-126.517,50.724,-126.472,50.767,-126.397,50.807,-126.375,50.837,-126.418,50.85,-126.488,50.842,-126.522,50.866,-126.485,50.96,-126.517,51.057,-126.563,50.965,-126.632,50.915,-126.96,50.894,-127.014,50.867,-127.058,50.868,-127.267,50.916,-127.357,50.946,-127.441,50.989,-127.591,51.088,-127.708,51.151,-127.714,51.269,-127.689,51.343,-127.633,51.427,-127.42,51.608,-127.347,51.642,-127.281,51.654,-126.968,51.67,-126.735,51.693,-126.691,51.703,-127.034,51.717,-127.339,51.707,-127.443,51.679,-127.576,51.563,-127.61,51.514,-127.645,51.478,-127.669,51.478,-127.714,51.49,-127.729,51.506,-127.748,51.544,-127.819,51.604,-127.851,51.673,-127.869,51.775,-127.863,51.821,-127.83,51.879,-127.728,51.993,-127.859,51.99,-127.843,52.086,-127.795,52.191,-127.673,52.253,-127.55,52.298,-127.438,52.356,-127.242,52.395,-127.176,52.315,-127.008,52.291,-126.959,52.255,-126.9,52.188,-126.826,52.125,-126.739,52.065,-126.714,52.061,-126.753,52.112,-126.895,52.225,-126.901,52.265,-126.938,52.309,-127.127,52.371,-127.161,52.395,-127.194,52.458,-127.208,52.498,-127.187,52.538,-126.995,52.658,-126.951,52.721,-126.951,52.751,-126.966,52.785,-127.008,52.843,-127.019,52.842,-127.006,52.755,-127.013,52.72,-127.035,52.682,-127.066,52.653,-127.107,52.633,-127.519,52.359,-127.56,52.343,-127.713,52.319,-127.792,52.289,-127.834,52.251,-127.902,52.151,-127.995,51.951,-128.102,51.788,-128.194,51.998,-128.358,52.159,-128.037,52.318,-128.029,52.342,-128.06,52.428,-128.052,52.453,-128.021,52.491,-127.94,52.545,-127.943,52.551,-128.038,52.531,-128.184,52.408,-128.241,52.368,-128.272,52.363,-128.275,52.435,-128.197,52.623,-128.132,52.806,-128.109,52.858,-128.053,52.911,-128.106,52.907,-128.365,52.826,-128.452,52.877,-128.525,53.141,-128.652,53.244,-128.869,53.328,-129.081,53.367,-129.13,53.442,-129.172,53.534,-129.114,53.641,-129.021,53.692,-128.936,53.715,-128.855,53.705,-128.85,53.665,-128.906,53.559,-128.833,53.549,-128.542,53.421,-128.479,53.41,-128.358,53.46,-128.291,53.458,-128.133,53.418,-128.079,53.369,-127.928,53.275,-127.95,53.33,-128.115,53.446,-128.207,53.483,-128.369,53.49,-128.47,53.471,-128.512,53.477,-128.6,53.506,-128.676,53.555,-128.751,53.661,-128.768,53.71,-128.764,53.747,-128.746,53.78,-128.715,53.81,-128.653,53.832,-128.56,53.845,-128.532,53.858,-128.651,53.919,-128.705,53.919,-128.89,53.83,-128.928,53.823,-128.944,53.84,-128.959,53.841,-129.014,53.797,-129.056,53.778,-129.208,53.642,-129.232,53.576,-129.24,53.479,-129.258,53.418,-129.284,53.393,-129.462,53.347,-129.564,53.251,-129.687,53.334,-129.822,53.413,-129.912,53.551,-130.074,53.576,-130.263,53.654,-130.335,53.724,-130.233,53.867,-130.086,53.976,-130.064,54.106,-130.043,54.134,-129.791,54.166,-129.626,54.23,-129.795,54.236,-129.898,54.226,-130.084,54.181,-130.29,54.27,-130.397,54.352,-130.43,54.421,-130.393,54.48,-130.389,54.539,-130.37,54.62,-130.35,54.655,-130.307,54.7,-130.219,54.73,-130.141,54.823,-130.109,54.887,-129.949,55.081,-129.89,55.165,-129.781,55.28,-129.561,55.463,-129.63,55.452,-129.667,55.437,-129.701,55.439,-129.734,55.458,-129.765,55.498,-129.795,55.56,-129.812,55.533,-129.816,55.418,-129.838,55.319,-129.877,55.251,-129.985,55.111,-130.048,55.057,-130.092,55.108,-130.058,55.195,-129.996,55.264,-129.985,55.359,-130.044,55.472,-130.08,55.563,-130.093,55.632,-130.095,55.695,-130.085,55.752,-130.06,55.814,-130.02,55.881,-130.025,55.888]],[[-109.816,78.65,-109.641,78.592,-109.581,78.593,-109.504,78.582,-109.467,78.567,-109.362,78.493,-109.342,78.456,-109.336,78.408,-109.352,78.369,-109.391,78.337,-109.484,78.316,-109.709,78.304,-110.022,78.323,-110.293,78.298,-110.418,78.295,-110.755,78.311,-110.84,78.322,-111.027,78.368,-111.169,78.386,-111.229,78.376,-111.3,78.337,-111.435,78.287,-111.517,78.275,-111.76,78.283,-112.131,78.366,-112.558,78.342,-113,78.293,-113.173,78.284,-113.223,78.298,-113.293,78.334,-113.282,78.353,-113.15,78.408,-112.856,78.467,-112.641,78.5,-112.214,78.548,-111.709,78.575,-111.52,78.603,-111.4,78.644,-111.071,78.708,-110.878,78.735,-110.618,78.758,-110.408,78.757,-110.14,78.704,-109.941,78.678,-109.816,78.65]],[[-110.458,78.103,-109.657,78.079,-109.622,78.075,-109.619,78.057,-109.679,77.999,-109.772,77.957,-110.2,77.905,-110.751,77.857,-110.866,77.834,-110.857,77.82,-110.812,77.803,-110.719,77.781,-110.292,77.786,-110.189,77.777,-110.153,77.763,-110.131,77.742,-110.118,77.716,-110.117,77.625,-110.139,77.572,-110.198,77.525,-110.372,77.491,-110.683,77.446,-110.894,77.426,-111.06,77.433,-111.226,77.429,-111.952,77.344,-112.177,77.344,-112.373,77.364,-112.644,77.444,-112.926,77.475,-113.046,77.511,-113.164,77.53,-113.197,77.559,-113.209,77.58,-113.189,77.6,-113.137,77.618,-113.121,77.633,-113.168,77.676,-113.19,77.718,-113.271,77.778,-113.283,77.813,-113.283,77.836,-113.269,77.86,-113.215,77.904,-113.187,77.912,-113.022,77.919,-112.805,77.942,-112.305,78.007,-111.207,78.088,-110.873,78.081,-110.727,78.097,-110.458,78.103]],[[-115.551,77.363,-115.476,77.324,-115.47,77.309,-115.507,77.292,-115.624,77.266,-116.214,77.178,-116.329,77.137,-116.286,77.102,-116.073,77.03,-115.857,76.969,-115.81,76.939,-115.913,76.908,-116.11,76.918,-116.183,76.916,-116.253,76.901,-116.234,76.874,-116.016,76.785,-115.945,76.736,-115.946,76.711,-115.985,76.687,-116.076,76.654,-116.22,76.611,-116.468,76.577,-116.999,76.532,-117.017,76.496,-117.013,76.469,-117.026,76.404,-117.044,76.373,-117.108,76.322,-117.154,76.298,-117.234,76.282,-117.347,76.273,-117.492,76.273,-117.732,76.317,-117.841,76.345,-117.993,76.406,-118.02,76.447,-118.005,76.497,-117.965,76.574,-117.9,76.653,-117.808,76.734,-117.78,76.784,-117.818,76.804,-117.881,76.805,-118.076,76.772,-118.203,76.76,-118.301,76.737,-118.37,76.701,-118.409,76.662,-118.431,76.588,-118.468,76.547,-118.574,76.525,-118.732,76.526,-118.791,76.513,-118.821,76.486,-118.8,76.464,-118.644,76.418,-118.625,76.366,-118.643,76.335,-118.812,76.277,-118.851,76.258,-118.955,76.168,-118.994,76.145,-119.081,76.124,-119.168,76.127,-119.249,76.159,-119.368,76.222,-119.448,76.275,-119.489,76.32,-119.524,76.34,-119.58,76.327,-119.649,76.28,-119.651,76.244,-119.636,76.19,-119.64,76.157,-119.74,76.118,-119.725,76.1,-119.55,76.052,-119.527,76.031,-119.526,75.997,-119.538,75.982,-119.608,75.985,-119.667,75.946,-119.735,75.915,-119.913,75.859,-120.161,75.852,-120.365,75.825,-120.409,75.826,-120.458,75.87,-120.514,75.958,-120.563,76.008,-120.637,76.034,-120.729,76.134,-120.772,76.166,-120.813,76.179,-120.848,76.183,-120.9,76.163,-121.019,76.02,-121.213,75.984,-121.32,75.977,-121.428,75.981,-121.695,76.02,-121.908,76.035,-122.057,76.018,-122.303,75.96,-122.4,75.944,-122.533,75.951,-122.591,75.973,-122.64,76.009,-122.646,76.031,-122.607,76.039,-122.546,76.081,-122.548,76.097,-122.609,76.121,-122.609,76.14,-122.588,76.153,-122.593,76.162,-122.624,76.167,-122.685,76.162,-122.903,76.135,-122.878,76.165,-122.774,76.228,-122.519,76.353,-122.423,76.39,-122.365,76.401,-121.614,76.441,-121.561,76.453,-121.204,76.622,-121.102,76.661,-120.998,76.691,-120.485,76.793,-120.438,76.816,-120.358,76.887,-120.311,76.905,-120.2,76.931,-119.831,77.074,-119.495,77.177,-119.324,77.241,-119.09,77.305,-118.82,77.333,-118.005,77.381,-117.419,77.317,-117.279,77.313,-117.211,77.331,-117.149,77.361,-117.061,77.348,-116.844,77.34,-116.795,77.347,-116.704,77.38,-116.766,77.398,-117.03,77.432,-117.046,77.449,-117.04,77.465,-116.947,77.504,-116.835,77.529,-116.511,77.548,-116.363,77.543,-116.209,77.516,-116.008,77.461,-115.551,77.363]],[[-108.292,76.057,-108.166,76.054,-108.019,76.065,-107.852,76.058,-107.777,76.035,-107.723,75.995,-107.721,75.974,-107.732,75.956,-107.755,75.94,-107.97,75.84,-108.021,75.805,-107.951,75.796,-107.918,75.802,-107.703,75.878,-107.541,75.901,-107.418,75.907,-107.216,75.892,-107.136,75.879,-107.08,75.863,-107.05,75.845,-106.971,75.773,-106.914,75.68,-106.904,75.689,-106.903,75.742,-106.892,75.782,-106.693,75.81,-106.688,75.819,-106.759,75.842,-106.82,75.872,-106.862,75.93,-106.846,75.952,-106.804,75.975,-106.677,76.024,-106.529,76.053,-106.397,76.06,-105.905,76.009,-105.711,75.967,-105.633,75.945,-105.604,75.93,-105.563,75.881,-105.481,75.746,-105.481,75.702,-105.519,75.632,-105.678,75.501,-105.703,75.413,-105.863,75.192,-105.972,75.131,-106.093,75.089,-106.588,75.015,-106.961,74.94,-107.056,74.928,-107.153,74.927,-107.462,74.952,-107.82,75,-108.024,74.986,-108.227,74.952,-108.354,74.943,-108.475,74.947,-108.594,74.96,-108.751,74.992,-108.67,75.007,-108.633,75.023,-108.666,75.04,-108.831,75.065,-109.003,75.01,-109.503,74.883,-110.176,74.84,-110.387,74.814,-110.543,74.78,-110.625,74.753,-110.749,74.688,-110.941,74.639,-111.288,74.585,-111.729,74.502,-112.519,74.417,-113.017,74.402,-113.514,74.43,-113.672,74.453,-113.837,74.489,-114.175,74.574,-114.268,74.604,-114.377,74.671,-114.313,74.715,-114.132,74.766,-113.863,74.813,-113.324,74.875,-112.836,74.976,-112.663,74.994,-112.193,75.01,-111.956,75,-111.784,75.006,-111.671,75.019,-111.503,75.056,-111.258,75.128,-111.079,75.195,-111.033,75.227,-111.093,75.256,-111.181,75.26,-111.474,75.191,-111.621,75.168,-111.781,75.166,-112,75.142,-112.214,75.133,-112.256,75.134,-112.478,75.2,-112.597,75.212,-112.652,75.205,-112.703,75.187,-112.8,75.138,-112.855,75.121,-112.951,75.108,-113.34,75.093,-113.712,75.069,-113.795,75.084,-113.845,75.112,-113.855,75.129,-113.861,75.188,-113.886,75.211,-113.853,75.259,-113.811,75.296,-113.759,75.322,-113.503,75.397,-113.467,75.416,-113.589,75.412,-113.879,75.375,-113.916,75.388,-113.984,75.43,-114.017,75.434,-114.053,75.417,-114.075,75.392,-114.125,75.291,-114.168,75.24,-114.285,75.25,-114.429,75.281,-114.483,75.285,-114.514,75.275,-114.504,75.258,-114.358,75.171,-114.356,75.141,-114.452,75.088,-114.859,75,-115.02,74.976,-115.077,74.985,-115.128,75.009,-115.174,75.049,-115.28,75.102,-115.343,75.113,-115.413,75.115,-115.478,75.104,-115.537,75.081,-115.574,75.056,-115.609,75.01,-115.683,74.974,-115.729,74.968,-116.143,75.042,-116.476,75.172,-116.841,75.152,-117.005,75.156,-117.502,75.204,-117.565,75.233,-117.6,75.272,-117.597,75.293,-117.576,75.314,-117.513,75.357,-117.388,75.421,-117.336,75.442,-117.258,75.46,-117.154,75.473,-116.891,75.481,-116.213,75.483,-116.077,75.493,-115.335,75.618,-115.251,75.639,-115.142,75.679,-115.117,75.695,-115.122,75.706,-116.034,75.607,-116.426,75.585,-117.025,75.602,-117.138,75.617,-117.164,75.645,-117.039,75.718,-116.973,75.746,-116.802,75.772,-116.39,75.808,-115.838,75.841,-115.477,75.841,-115.174,75.867,-114.992,75.896,-115.602,75.895,-116.338,75.881,-116.444,75.891,-116.654,75.929,-116.665,75.958,-116.58,75.992,-116.55,76.017,-116.61,76.074,-116.591,76.096,-116.454,76.143,-116.21,76.194,-116.059,76.202,-115.768,76.184,-114.939,76.166,-114.779,76.173,-114.88,76.195,-115.025,76.211,-115.664,76.24,-115.797,76.253,-115.822,76.27,-115.832,76.296,-115.826,76.33,-115.779,76.365,-115.581,76.438,-114.998,76.497,-114.767,76.506,-114.535,76.502,-114.299,76.475,-114.194,76.451,-114.141,76.423,-114.116,76.396,-114.113,76.349,-114.101,76.331,-114.059,76.301,-113.923,76.229,-113.823,76.207,-113.363,76.248,-113.171,76.258,-112.978,76.245,-112.698,76.202,-112.334,76.072,-111.865,75.939,-111.868,75.911,-112.047,75.866,-112.081,75.847,-112.057,75.834,-111.877,75.826,-111.709,75.832,-111.549,75.822,-111.513,75.811,-111.454,75.762,-111.373,75.676,-111.276,75.613,-111.163,75.57,-111.053,75.549,-110.89,75.547,-110.726,75.56,-110.459,75.555,-109.086,75.506,-109.005,75.515,-108.947,75.542,-108.913,75.587,-108.9,75.624,-108.918,75.675,-108.945,75.699,-109.796,75.863,-109.871,75.929,-109.455,76.021,-109.425,76.043,-109.417,76.072,-109.43,76.109,-109.487,76.145,-109.71,76.212,-109.908,76.223,-110.201,76.289,-110.247,76.306,-110.285,76.333,-110.314,76.369,-110.309,76.397,-110.27,76.417,-109.982,76.485,-109.865,76.522,-109.505,76.692,-109.339,76.76,-109.22,76.792,-109.098,76.812,-108.832,76.821,-108.554,76.758,-108.492,76.754,-108.467,76.738,-108.478,76.708,-108.512,76.68,-108.612,76.63,-108.635,76.609,-108.628,76.587,-108.56,76.536,-108.539,76.503,-108.524,76.447,-108.512,76.439,-108.345,76.392,-108.194,76.33,-108.123,76.233,-108.178,76.2,-108.306,76.154,-108.382,76.116,-108.406,76.085,-108.387,76.067,-108.292,76.057]],[[-114.522,72.593,-114.458,72.58,-114.342,72.591,-114.174,72.624,-113.958,72.651,-113.692,72.673,-113.622,72.647,-113.578,72.652,-113.5,72.694,-113.486,72.722,-113.496,72.754,-113.491,72.822,-113.45,72.863,-113.292,72.95,-113.208,72.981,-113.074,72.995,-112.754,72.986,-112.454,72.937,-112.048,72.888,-111.455,72.766,-111.27,72.714,-111.25,72.669,-111.356,72.572,-111.611,72.436,-111.816,72.386,-111.895,72.356,-111.762,72.335,-111.675,72.3,-111.544,72.351,-111.447,72.408,-111.311,72.455,-111.265,72.459,-111.253,72.449,-111.277,72.425,-111.287,72.401,-111.284,72.378,-111.268,72.364,-111.185,72.357,-111.14,72.365,-110.959,72.432,-110.782,72.534,-110.513,72.6,-110.439,72.633,-110.205,72.661,-110.208,72.681,-110.197,72.759,-110.279,72.792,-110.554,72.861,-110.689,72.945,-110.661,73.008,-110.509,72.999,-110.095,72.992,-110.008,72.984,-109.61,72.876,-109.469,72.808,-109.357,72.775,-109.122,72.726,-109.043,72.687,-108.987,72.671,-108.968,72.654,-108.985,72.637,-108.994,72.596,-108.951,72.583,-108.798,72.568,-108.755,72.551,-108.698,72.499,-108.628,72.412,-108.566,72.317,-108.47,72.139,-108.276,71.9,-108.21,71.751,-108.188,71.724,-108.145,71.705,-108.021,71.677,-107.925,71.639,-107.813,71.626,-107.785,71.63,-107.757,71.663,-107.687,71.716,-107.347,71.819,-107.329,71.835,-107.369,71.859,-107.382,71.875,-107.377,71.886,-107.306,71.895,-107.543,72.025,-107.696,72.149,-107.794,72.303,-107.809,72.347,-107.824,72.443,-107.856,72.468,-107.91,72.491,-107.933,72.52,-107.924,72.557,-107.934,72.588,-107.997,72.653,-108.238,73.106,-108.237,73.15,-108.204,73.183,-108.118,73.202,-107.98,73.207,-107.936,73.217,-107.987,73.233,-108.077,73.281,-108.089,73.304,-108.029,73.349,-107.72,73.329,-107.496,73.288,-107.113,73.192,-107.074,73.197,-107.033,73.245,-106.951,73.276,-106.828,73.266,-106.482,73.196,-106.082,73.072,-105.813,73.011,-105.624,72.927,-105.496,72.849,-105.415,72.788,-105.412,72.765,-105.43,72.74,-105.411,72.709,-105.355,72.67,-105.323,72.635,-105.298,72.56,-105.247,72.464,-105.234,72.415,-104.878,71.98,-104.81,71.903,-104.767,71.868,-104.518,71.699,-104.386,71.577,-104.373,71.495,-104.355,71.472,-104.35,71.434,-104.356,71.382,-104.385,71.338,-104.437,71.3,-104.487,71.248,-104.563,71.132,-104.57,71.104,-104.515,71.064,-104.167,70.927,-103.953,70.763,-103.853,70.734,-103.585,70.631,-103.295,70.572,-103.197,70.547,-103.105,70.51,-103.077,70.509,-103.021,70.516,-103.005,70.526,-103.001,70.541,-103.083,70.619,-103.089,70.65,-103.05,70.655,-102.75,70.522,-102.589,70.469,-102.369,70.413,-101.99,70.285,-101.937,70.275,-101.732,70.286,-101.676,70.278,-101.641,70.266,-101.627,70.248,-101.618,70.172,-101.562,70.135,-101.239,70.151,-101.149,70.148,-101.091,70.136,-101.043,70.111,-100.973,70.029,-100.909,69.869,-100.906,69.812,-100.935,69.715,-100.982,69.68,-101.044,69.669,-101.216,69.68,-101.337,69.71,-101.4,69.749,-101.457,69.834,-101.484,69.85,-101.508,69.833,-101.565,69.756,-101.602,69.721,-101.648,69.699,-101.734,69.704,-101.86,69.738,-102.098,69.825,-102.182,69.846,-102.234,69.842,-102.348,69.813,-102.523,69.758,-102.596,69.718,-102.565,69.692,-102.545,69.66,-102.535,69.621,-102.541,69.592,-102.563,69.574,-102.621,69.552,-102.744,69.548,-102.92,69.565,-103.059,69.595,-103.303,69.674,-103.359,69.685,-103.435,69.668,-103.465,69.644,-103.418,69.611,-103.294,69.568,-103.142,69.497,-103.102,69.483,-103.063,69.485,-103.049,69.472,-103.032,69.433,-103.04,69.368,-103.113,69.236,-103.12,69.205,-103.09,69.212,-102.884,69.341,-102.777,69.378,-102.546,69.434,-102.447,69.476,-102.151,69.488,-102.046,69.465,-101.978,69.425,-101.976,69.407,-102.053,69.36,-102.067,69.337,-102.071,69.308,-102.064,69.281,-102.046,69.258,-101.993,69.236,-101.899,69.246,-101.873,69.24,-101.823,69.217,-101.789,69.182,-101.788,69.132,-101.857,69.024,-101.981,68.989,-102.359,68.923,-102.488,68.889,-102.738,68.865,-102.835,68.833,-102.895,68.824,-103.162,68.829,-103.468,68.809,-103.82,68.848,-104.067,68.866,-104.353,68.928,-104.46,68.912,-104.571,68.872,-105.106,68.92,-105.169,68.955,-105.148,68.978,-105.022,69.052,-105.014,69.068,-105.02,69.081,-105.262,69.094,-105.533,69.134,-105.805,69.153,-106.008,69.148,-106.141,69.162,-106.27,69.195,-106.341,69.224,-106.354,69.251,-106.356,69.281,-106.344,69.34,-106.361,69.381,-106.42,69.414,-106.54,69.443,-106.659,69.44,-106.76,69.407,-106.856,69.347,-107.033,69.181,-107.123,69.152,-107.353,69.032,-107.44,69.002,-107.863,68.954,-108.365,68.935,-108.553,68.897,-108.73,68.827,-108.946,68.76,-109.472,68.677,-109.959,68.63,-110.468,68.61,-110.848,68.578,-110.957,68.594,-111.128,68.588,-111.311,68.542,-111.518,68.533,-112.305,68.516,-112.666,68.485,-112.864,68.477,-113.02,68.481,-113.128,68.494,-113.231,68.535,-113.338,68.599,-113.555,68.768,-113.617,68.838,-113.593,68.96,-113.609,69.03,-113.681,69.182,-113.694,69.195,-114.073,69.251,-114.323,69.269,-114.699,69.273,-115.159,69.265,-115.618,69.283,-115.861,69.304,-116.102,69.337,-116.513,69.425,-116.537,69.434,-116.569,69.463,-116.609,69.512,-116.712,69.576,-116.993,69.719,-117.104,69.804,-117.122,69.826,-117.149,69.888,-117.184,69.991,-117.195,70.054,-117.163,70.092,-117.135,70.1,-116.554,70.175,-115.529,70.257,-114.592,70.312,-114.167,70.307,-113.917,70.282,-113.666,70.27,-113.211,70.264,-112.638,70.225,-112.523,70.229,-112.266,70.255,-112.19,70.276,-111.784,70.273,-111.705,70.286,-111.633,70.309,-111.726,70.352,-112.114,70.447,-113.146,70.616,-113.397,70.652,-113.757,70.691,-113.966,70.696,-114.232,70.674,-114.331,70.675,-114.593,70.642,-114.841,70.621,-115.311,70.601,-115.991,70.586,-116.086,70.591,-116.226,70.616,-116.327,70.624,-116.993,70.604,-117.587,70.63,-118.264,70.888,-118.377,70.968,-118.353,71,-118.269,71.035,-117.934,71.135,-117.814,71.158,-117.314,71.212,-116.815,71.277,-116.422,71.338,-116.228,71.359,-116.042,71.362,-115.892,71.382,-115.922,71.401,-116.045,71.423,-116.044,71.454,-115.98,71.469,-115.734,71.485,-115.472,71.466,-115.341,71.472,-115.303,71.494,-115.338,71.511,-115.587,71.546,-116.78,71.444,-117.337,71.435,-117.723,71.391,-117.936,71.392,-118.188,71.436,-118.222,71.449,-118.226,71.467,-118.148,71.526,-117.878,71.561,-117.742,71.659,-117.888,71.661,-118.372,71.64,-118.583,71.649,-118.868,71.687,-118.952,71.732,-118.988,71.764,-118.994,71.803,-118.984,71.913,-118.96,71.972,-118.945,71.986,-118.59,72.167,-118.369,72.205,-118.213,72.263,-118.207,72.287,-118.246,72.311,-118.39,72.37,-118.449,72.399,-118.481,72.428,-118.457,72.473,-118.375,72.534,-118.133,72.633,-117.552,72.831,-117.256,72.914,-116.972,72.959,-116.573,73.055,-115.552,73.213,-114.638,73.373,-114.302,73.331,-114.206,73.298,-114.164,73.27,-114.127,73.231,-114.095,73.18,-114.052,73.071,-114.046,73.015,-114.054,72.958,-114.075,72.907,-114.109,72.861,-114.178,72.805,-114.28,72.739,-114.498,72.626,-114.522,72.593]],[[-119.736,74.113,-119.729,74.108,-119.471,74.201,-119.315,74.206,-119.206,74.198,-119.171,74.186,-119.15,74.168,-119.139,74.128,-119.132,74.028,-119.118,74.016,-119.083,74.021,-119.026,74.045,-118.744,74.192,-118.625,74.233,-118.544,74.245,-118.2,74.267,-117.966,74.266,-117.707,74.252,-117.515,74.232,-117.199,74.171,-116.95,74.101,-116.722,74.027,-115.958,73.748,-115.634,73.666,-115.511,73.619,-115.456,73.585,-115.408,73.542,-115.393,73.502,-115.412,73.465,-115.447,73.439,-115.524,73.417,-115.992,73.323,-116.239,73.295,-116.483,73.253,-117.065,73.107,-117.464,73.038,-117.983,72.902,-118.962,72.684,-119.078,72.64,-119.132,72.609,-119.408,72.36,-119.513,72.303,-119.767,72.244,-120.09,72.229,-120.18,72.213,-120.194,72.127,-120.31,71.984,-120.366,71.888,-120.443,71.631,-120.461,71.605,-120.52,71.557,-120.619,71.506,-120.93,71.446,-121.16,71.415,-121.472,71.389,-121.547,71.407,-121.622,71.448,-121.701,71.451,-121.749,71.445,-122.157,71.266,-122.55,71.194,-122.72,71.128,-122.84,71.097,-122.937,71.088,-123.096,71.094,-123.211,71.123,-123.315,71.169,-123.393,71.219,-123.595,71.423,-123.682,71.493,-123.756,71.528,-123.953,71.652,-124.008,71.677,-124.76,71.835,-125.126,71.924,-125.215,71.955,-125.297,71.973,-125.767,71.961,-125.829,71.966,-125.845,71.979,-125.79,72.025,-125.768,72.054,-125.76,72.083,-125.769,72.129,-125.763,72.138,-125.584,72.183,-125.613,72.193,-125.634,72.21,-125.647,72.237,-125.627,72.255,-125.575,72.265,-125.512,72.308,-125.438,72.382,-125.383,72.424,-125.306,72.451,-125.168,72.523,-125.07,72.552,-124.987,72.588,-124.985,72.604,-125.019,72.617,-125.03,72.645,-125.015,72.731,-125.015,72.776,-125,72.813,-124.97,72.843,-124.931,72.863,-124.583,72.926,-124.565,72.944,-124.561,72.965,-124.57,72.989,-124.588,73.005,-124.643,73.019,-124.736,73.023,-124.817,73.059,-124.836,73.076,-124.804,73.126,-124.647,73.204,-124.594,73.243,-124.424,73.419,-124.114,73.527,-124.03,73.644,-123.797,73.768,-123.798,73.785,-123.873,73.828,-124.088,73.857,-124.192,73.902,-124.261,73.953,-124.575,74.248,-124.629,74.27,-124.645,74.304,-124.709,74.327,-124.696,74.348,-123.468,74.436,-122.623,74.464,-121.748,74.541,-121.504,74.545,-121.315,74.53,-121.129,74.49,-120.882,74.421,-120.554,74.353,-119.944,74.254,-119.563,74.233,-119.715,74.154,-119.737,74.13,-119.736,74.113]],[[-69.489,83.017,-68.673,82.999,-68.409,83.005,-68.107,82.961,-67.925,82.956,-67.624,82.964,-67.406,82.954,-66.592,82.944,-66.423,82.927,-66.425,82.906,-66.6,82.861,-66.836,82.818,-68.358,82.677,-68.469,82.653,-68.173,82.646,-67.736,82.652,-67.397,82.668,-66.998,82.716,-66.866,82.719,-66.612,82.742,-66.12,82.807,-65.727,82.842,-65.55,82.827,-65.4,82.802,-65.299,82.8,-65.247,82.819,-65.162,82.87,-65.113,82.889,-64.984,82.902,-64.905,82.901,-64.777,82.876,-64.635,82.819,-64.504,82.778,-64.433,82.778,-64.134,82.823,-63.984,82.829,-63.641,82.813,-63.499,82.793,-63.473,82.771,-63.564,82.749,-63.621,82.729,-63.643,82.713,-63.593,82.694,-63.385,82.653,-63.085,82.565,-63.087,82.533,-63.251,82.467,-63.247,82.45,-62.475,82.52,-61.697,82.489,-61.477,82.467,-61.392,82.442,-61.302,82.4,-61.207,82.341,-61.274,82.28,-61.615,82.184,-61.969,82.11,-62.177,82.043,-62.496,82.007,-63.592,81.846,-64.128,81.794,-64.436,81.743,-64.574,81.734,-65.226,81.744,-65.399,81.715,-65.495,81.668,-65.701,81.646,-66.005,81.629,-66.626,81.616,-66.765,81.563,-66.801,81.527,-66.861,81.499,-66.914,81.485,-68.689,81.293,-68.721,81.261,-68.543,81.248,-68.318,81.261,-65.736,81.494,-65.24,81.51,-64.78,81.493,-64.833,81.439,-65.484,81.285,-66.313,81.146,-66.727,81.041,-67.774,80.859,-68.63,80.679,-68.959,80.587,-69.4,80.423,-69.551,80.383,-69.734,80.367,-69.949,80.374,-70.144,80.398,-70.403,80.459,-70.639,80.528,-70.713,80.54,-70.668,80.506,-70.213,80.278,-70.265,80.234,-71.1,80.187,-71.47,80.146,-71.661,80.136,-71.796,80.143,-71.928,80.139,-72.056,80.123,-72.063,80.106,-71.949,80.086,-71.616,80.071,-70.877,80.122,-70.758,80.119,-70.568,80.094,-70.559,80.071,-70.758,79.998,-71.356,79.911,-71.278,79.906,-71.106,79.876,-71.11,79.848,-71.299,79.783,-71.388,79.762,-71.965,79.701,-72.216,79.687,-72.437,79.694,-73.448,79.827,-73.805,79.846,-74.144,79.88,-74.394,79.874,-74.66,79.835,-74.541,79.816,-74.051,79.778,-73.642,79.771,-73.472,79.756,-73.406,79.732,-73.229,79.644,-73.201,79.597,-73.24,79.552,-73.294,79.522,-73.362,79.504,-73.466,79.495,-73.866,79.501,-74.015,79.491,-74.189,79.465,-74.406,79.454,-74.798,79.459,-75.259,79.421,-75.503,79.414,-75.774,79.431,-76.067,79.473,-76.376,79.494,-76.899,79.512,-76.855,79.488,-76.671,79.478,-76.296,79.414,-76.116,79.326,-75.948,79.311,-75.603,79.24,-75.354,79.228,-75.094,79.204,-74.727,79.235,-74.481,79.229,-74.532,79.053,-74.641,79.036,-75.233,79.036,-75.515,79.061,-75.639,79.088,-75.912,79.118,-76.158,79.1,-76.38,79.104,-76.531,79.087,-76.771,79.087,-77.398,79.057,-77.729,79.057,-77.974,79.076,-78.258,79.082,-78.582,79.075,-78.559,79.055,-78.422,79.048,-78.222,79.015,-78.037,78.964,-77.883,78.942,-77.698,78.955,-77.51,78.978,-76.825,79.018,-76.524,79.024,-76.256,79.007,-76.077,78.985,-75.953,78.959,-75.795,78.89,-75.4,78.881,-75.099,78.858,-74.618,78.758,-74.486,78.75,-74.433,78.724,-74.535,78.659,-74.547,78.62,-74.879,78.545,-75.397,78.523,-75.966,78.53,-76.373,78.521,-76.416,78.512,-76.137,78.492,-75.488,78.404,-75.237,78.356,-75.193,78.328,-75.551,78.221,-75.866,78.01,-75.97,77.993,-76.078,77.987,-76.356,77.991,-76.708,77.938,-76.974,77.927,-77.456,77.947,-78.013,77.946,-78.056,77.912,-78.084,77.846,-78.081,77.747,-78.047,77.615,-78.076,77.519,-78.168,77.458,-78.284,77.413,-78.493,77.369,-78.708,77.342,-78.87,77.333,-79.138,77.331,-79.906,77.3,-80.282,77.301,-80.573,77.315,-80.875,77.359,-81.377,77.482,-81.519,77.51,-81.659,77.525,-81.654,77.499,-81.504,77.43,-81.378,77.385,-81.278,77.365,-81.301,77.344,-81.523,77.311,-81.767,77.296,-82.057,77.297,-82.066,77.284,-81.968,77.248,-81.84,77.214,-81.756,77.204,-81.534,77.214,-81.277,77.257,-81.117,77.27,-80.798,77.259,-80.673,77.244,-80.274,77.151,-80.219,77.147,-79.924,77.194,-79.497,77.196,-79.341,77.158,-79.281,77.085,-79.274,77.026,-79.319,76.98,-79.221,76.936,-78.979,76.893,-78.792,76.884,-78.659,76.908,-78.456,76.967,-78.37,76.981,-78.289,76.978,-78.165,76.935,-77.999,76.852,-77.983,76.755,-78.119,76.644,-78.284,76.571,-78.934,76.451,-79.131,76.404,-79.286,76.355,-79.511,76.31,-79.954,76.251,-80.187,76.24,-80.69,76.176,-80.8,76.174,-80.963,76.184,-80.997,76.215,-80.955,76.27,-80.901,76.322,-80.835,76.369,-80.832,76.409,-80.975,76.47,-81.074,76.498,-81.171,76.513,-81.365,76.504,-81.474,76.488,-81.592,76.484,-81.717,76.495,-81.823,76.521,-82.034,76.629,-82.114,76.643,-82.218,76.64,-82.311,76.655,-82.393,76.69,-82.53,76.723,-82.493,76.698,-82.357,76.636,-82.262,76.575,-82.208,76.514,-82.233,76.466,-83.389,76.439,-83.886,76.453,-83.986,76.495,-84.224,76.675,-84.275,76.357,-85.141,76.305,-85.344,76.313,-85.681,76.349,-86.116,76.435,-86.296,76.492,-86.367,76.549,-86.419,76.58,-86.454,76.585,-86.562,76.517,-86.68,76.377,-86.978,76.413,-87.354,76.448,-87.49,76.586,-87.498,76.386,-88.104,76.413,-88.396,76.405,-88.482,76.58,-88.496,76.773,-88.614,76.651,-88.563,76.547,-88.546,76.421,-88.804,76.457,-89.37,76.474,-89.57,76.492,-89.544,76.66,-89.5,76.827,-88.771,76.993,-88.556,77.072,-88.398,77.104,-88.148,77.124,-87.828,77.136,-87.61,77.127,-87.362,77.136,-87.064,77.166,-86.852,77.174,-86.812,77.185,-86.874,77.2,-87.101,77.308,-87.182,77.332,-87.265,77.343,-87.43,77.348,-87.589,77.395,-87.681,77.436,-87.78,77.493,-87.938,77.6,-88.095,77.719,-88.017,77.785,-87.757,77.836,-87.497,77.872,-87.236,77.892,-87.018,77.892,-86.755,77.864,-86.385,77.809,-86.173,77.746,-85.907,77.614,-85.731,77.509,-85.588,77.461,-84.951,77.375,-84.739,77.361,-84.487,77.368,-83.974,77.391,-83.721,77.414,-83.608,77.442,-83.55,77.483,-83.477,77.514,-83.25,77.585,-82.903,77.733,-82.71,77.85,-82.665,77.889,-82.626,77.936,-82.595,77.992,-82.704,77.962,-83.304,77.674,-83.428,77.621,-83.779,77.533,-83.928,77.518,-84.168,77.523,-84.486,77.562,-84.861,77.5,-85.088,77.515,-85.289,77.559,-85.292,77.764,-85.548,77.928,-85.265,78.011,-85.031,78.062,-84.615,78.196,-84.524,78.197,-84.223,78.176,-84.388,78.206,-84.55,78.251,-84.91,78.24,-84.783,78.528,-85.024,78.312,-85.27,78.2,-85.419,78.142,-85.586,78.11,-86.218,78.081,-86.063,78.187,-85.92,78.343,-86.071,78.285,-86.427,78.197,-86.694,78.151,-86.913,78.127,-87.339,78.133,-87.552,78.177,-87.491,78.284,-87.491,78.417,-87.361,78.479,-87.164,78.558,-86.953,78.664,-86.808,78.774,-86.242,78.824,-85.691,78.844,-85.23,78.902,-85.004,78.912,-84.787,78.885,-83.908,78.839,-83.547,78.804,-83.389,78.779,-83.271,78.77,-83.147,78.808,-82.99,78.844,-82.442,78.84,-82.291,78.847,-82.151,78.864,-81.981,78.898,-81.781,78.95,-81.75,78.976,-81.889,78.975,-82.028,78.962,-82.237,78.924,-82.439,78.904,-82.644,78.908,-83.059,78.94,-83.779,78.945,-84.146,78.96,-84.316,78.975,-84.412,78.997,-84.496,79.029,-84.568,79.071,-84.53,79.101,-84.384,79.119,-84.257,79.122,-84.053,79.099,-83.825,79.059,-83.576,79.054,-83.662,79.09,-83.978,79.163,-84.197,79.225,-84.381,79.301,-84.522,79.377,-84.836,79.495,-85.09,79.612,-85.269,79.664,-85.457,79.69,-86.031,79.722,-86.147,79.743,-86.421,79.845,-86.494,80.018,-86.615,80.124,-86.499,80.258,-86.307,80.319,-85.16,80.272,-84.675,80.279,-84.057,80.262,-83.724,80.229,-83.344,80.147,-83.004,80.055,-82.677,79.993,-82.377,79.908,-82.049,79.783,-81.856,79.723,-81.688,79.686,-81.463,79.654,-81.038,79.614,-80.668,79.601,-80.476,79.606,-80.271,79.635,-80.124,79.669,-80.287,79.679,-80.714,79.675,-81.01,79.693,-81.179,79.733,-81.359,79.788,-81.644,79.89,-81.86,79.957,-82.332,80.066,-82.681,80.175,-82.961,80.278,-82.987,80.323,-82.785,80.354,-82.536,80.376,-80.98,80.445,-80.051,80.529,-79.674,80.625,-79.629,80.648,-78.386,80.784,-77.507,80.835,-77.169,80.843,-76.863,80.865,-76.85,80.878,-77.119,80.896,-77.389,80.905,-78.004,80.905,-78.551,80.921,-78.716,80.952,-78.682,81.001,-78.629,81.043,-78.464,81.114,-78.287,81.168,-77.536,81.321,-77.031,81.386,-76.885,81.43,-77.972,81.331,-78.352,81.259,-78.734,81.151,-78.932,81.119,-79.072,81.128,-79.198,81.118,-79.309,81.089,-79.402,81.037,-79.477,80.961,-79.545,80.909,-79.607,80.882,-79.761,80.842,-80.134,80.764,-81.007,80.655,-81.301,80.627,-81.553,80.623,-82.368,80.561,-82.613,80.559,-82.884,80.578,-82.768,80.631,-82.337,80.729,-82.222,80.772,-82.498,80.763,-82.78,80.736,-83.401,80.714,-83.647,80.674,-83.885,80.602,-84.076,80.556,-84.22,80.538,-84.418,80.527,-85.146,80.521,-85.307,80.526,-85.726,80.581,-86.097,80.562,-86.25,80.566,-86.532,80.605,-86.615,80.63,-86.603,80.664,-86.44,80.728,-86.252,80.79,-85.639,80.925,-85.246,80.988,-84.68,81.042,-83.349,81.103,-83.289,81.148,-84.635,81.098,-85.781,81.035,-85.967,81.012,-86.233,80.95,-87.08,80.726,-87.33,80.67,-87.712,80.656,-88.004,80.675,-88.232,80.704,-88.625,80.77,-88.921,80.806,-89.062,80.83,-89.145,80.854,-89.212,80.882,-89.263,80.914,-89.167,80.941,-88.413,81,-87.389,80.988,-86.929,81,-86.477,81.036,-85.81,81.124,-85.083,81.247,-84.941,81.286,-85.206,81.295,-85.402,81.285,-85.875,81.241,-86.623,81.123,-87.275,81.081,-88.887,81.058,-89.398,81.025,-89.623,81.032,-89.792,81.065,-89.981,81.125,-89.947,81.173,-89.563,81.226,-89.263,81.239,-89.209,81.25,-89.636,81.302,-89.674,81.329,-89.427,81.387,-88.892,81.474,-88.622,81.501,-88.127,81.519,-87.617,81.509,-87.597,81.526,-88.101,81.559,-88.479,81.565,-88.978,81.542,-90.304,81.401,-90.416,81.405,-90.609,81.43,-90.554,81.464,-89.845,81.612,-89.822,81.635,-90.331,81.632,-90.48,81.639,-90.626,81.656,-90.834,81.64,-91.103,81.592,-91.292,81.571,-91.403,81.578,-91.684,81.636,-91.648,81.684,-91.424,81.744,-91.219,81.788,-90.942,81.827,-90.49,81.877,-90.163,81.894,-89.633,81.895,-89.381,81.917,-89.156,81.955,-88.875,82.018,-88.567,82.061,-88.063,82.096,-87.639,82.085,-87.404,82.054,-87.218,82,-87.018,81.959,-86.999,81.992,-86.834,82.033,-86.627,82.051,-86.378,82.045,-86.158,82.026,-85.875,81.976,-85.646,81.953,-85.538,81.955,-85.403,81.982,-85.045,81.983,-85.052,81.995,-85.169,82.023,-85.311,82.044,-86.581,82.187,-86.616,82.219,-86.188,82.248,-85.92,82.283,-85.794,82.292,-85.481,82.366,-85.276,82.405,-84.897,82.449,-84.745,82.437,-84.553,82.398,-84.368,82.374,-83.824,82.351,-83.591,82.326,-83.176,82.187,-83.01,82.142,-82.774,82.095,-82.634,82.077,-82.356,82.066,-82.327,82.092,-82.657,82.158,-82.747,82.196,-82.709,82.229,-82.638,82.246,-82.537,82.247,-82.277,82.218,-81.584,82.121,-80.55,82.005,-80.153,81.978,-79.909,81.936,-79.686,81.886,-79.466,81.851,-79.425,81.854,-79.629,81.932,-80.13,82.028,-81.468,82.192,-81.998,82.278,-82.254,82.336,-82.448,82.395,-82.451,82.427,-82.269,82.465,-82.023,82.494,-81.718,82.506,-81.681,82.519,-81.959,82.563,-82.123,82.602,-82.117,82.629,-81.785,82.649,-81.58,82.643,-81.189,82.594,-80.862,82.572,-80.81,82.586,-81.147,82.716,-81.178,82.745,-81.128,82.762,-81.01,82.779,-80.657,82.769,-80.076,82.706,-79.035,82.675,-78.749,82.679,-78.792,82.694,-79.207,82.733,-79.642,82.785,-79.834,82.817,-79.974,82.859,-80.141,82.894,-80.155,82.911,-79.886,82.939,-79.181,82.933,-78.525,82.891,-77.969,82.906,-77.618,82.896,-77.48,82.883,-77.226,82.837,-76.421,82.671,-76.336,82.644,-76.244,82.604,-76.146,82.55,-76.009,82.535,-75.744,82.572,-75.566,82.609,-75.643,82.644,-76.087,82.724,-76.188,82.758,-76.41,82.816,-76.908,82.919,-77.041,82.968,-77.125,83.009,-75.745,83.047,-74.414,83.013,-74.198,82.989,-74.056,82.955,-73.917,82.904,-73.703,82.852,-73.272,82.772,-72.659,82.722,-72.776,82.756,-73.235,82.844,-73.442,82.905,-73.441,82.946,-73.404,82.977,-73.331,82.999,-72.812,83.081,-72.069,83.106,-71.983,83.101,-71.406,82.975,-71.132,82.923,-70.94,82.902,-70.933,82.911,-71.198,82.97,-71.402,83.001,-71.424,83.021,-71.085,83.083,-70.871,83.098,-69.97,83.116,-69.868,83.11,-69.782,83.093,-69.569,83.025,-69.489,83.017]],[[-95.484,77.792,-95.233,77.754,-94.96,77.774,-94.667,77.776,-94.015,77.76,-93.583,77.771,-93.471,77.764,-93.301,77.74,-93.211,77.71,-93.129,77.66,-93.339,77.63,-93.52,77.474,-93.544,77.467,-93.74,77.465,-93.836,77.452,-94.409,77.474,-95.987,77.484,-96.056,77.503,-96.264,77.595,-96.277,77.631,-96.239,77.673,-96.195,77.701,-96.143,77.714,-95.684,77.782,-95.484,77.792]],[[-93.543,75.028,-93.478,74.952,-93.467,74.921,-93.463,74.856,-93.491,74.772,-93.509,74.756,-93.536,74.749,-93.548,74.728,-93.547,74.691,-93.573,74.669,-93.626,74.661,-93.985,74.644,-94.206,74.647,-94.535,74.637,-94.697,74.642,-94.804,74.66,-94.959,74.7,-95.286,74.794,-95.451,74.797,-95.865,74.83,-96.094,74.933,-96.182,74.951,-96.27,74.92,-96.294,74.927,-96.319,74.948,-96.343,74.982,-96.386,74.999,-96.56,74.99,-96.591,75.002,-96.6,75.032,-96.597,75.058,-96.566,75.099,-96.383,75.211,-96.292,75.219,-96.18,75.24,-96.118,75.301,-96.125,75.358,-95.955,75.444,-95.853,75.469,-95.671,75.529,-95.05,75.622,-94.878,75.63,-94.649,75.623,-94.427,75.593,-94.257,75.544,-93.909,75.423,-93.751,75.349,-93.667,75.274,-93.591,75.23,-93.498,75.137,-93.532,75.1,-93.552,75.051,-93.543,75.028]],[[-100.002,73.946,-99.158,73.732,-99.04,73.749,-98.785,73.761,-98.519,73.792,-98.152,73.818,-97.928,73.866,-97.832,73.879,-97.67,73.888,-97.582,73.888,-97.327,73.862,-97.225,73.844,-97.171,73.825,-97.112,73.79,-97.011,73.706,-96.997,73.675,-97.002,73.667,-97.095,73.615,-97.156,73.592,-97.284,73.571,-97.395,73.564,-97.49,73.527,-97.597,73.537,-97.626,73.502,-97.615,73.481,-97.586,73.471,-97.532,73.474,-97.47,73.488,-97.35,73.481,-97.287,73.458,-97.23,73.421,-97.273,73.387,-97.484,73.339,-97.796,73.285,-98.176,73.116,-98.376,73.045,-98.417,73.023,-98.437,73,-98.431,72.958,-98.422,72.941,-98.367,72.934,-98.181,72.993,-98.061,73.021,-97.939,73.036,-97.725,73.037,-97.636,73.028,-97.476,72.992,-97.329,72.938,-97.296,72.918,-97.31,72.898,-97.371,72.878,-97.378,72.865,-97.238,72.837,-97.083,72.763,-97.073,72.718,-97.14,72.673,-97.159,72.643,-97.128,72.628,-97.052,72.637,-96.869,72.687,-96.671,72.713,-96.592,72.71,-96.542,72.699,-96.489,72.63,-96.446,72.552,-96.44,72.487,-96.473,72.434,-96.52,72.393,-96.638,72.342,-96.746,72.323,-96.801,72.322,-96.796,72.314,-96.669,72.271,-96.616,72.237,-96.593,72.204,-96.601,72.173,-96.618,72.146,-96.766,72.046,-96.758,72.032,-96.717,72.025,-96.624,71.968,-96.613,71.834,-96.946,71.792,-97.025,71.761,-97.117,71.711,-97.222,71.673,-97.461,71.634,-97.582,71.63,-98.181,71.662,-98.242,71.681,-98.284,71.716,-98.307,71.765,-98.313,71.803,-98.303,71.831,-98.306,71.848,-98.323,71.852,-98.389,71.824,-98.459,71.773,-98.421,71.717,-98.231,71.559,-98.195,71.491,-98.19,71.462,-98.199,71.441,-98.412,71.349,-98.536,71.318,-98.663,71.302,-98.784,71.314,-98.899,71.352,-98.986,71.369,-99.167,71.367,-99.224,71.387,-99.276,71.424,-99.404,71.557,-99.581,71.652,-99.735,71.757,-100.124,71.912,-100.326,72.004,-100.594,72.152,-100.707,72.186,-100.8,72.199,-100.984,72.21,-101.026,72.229,-101.093,72.279,-101.209,72.317,-101.251,72.322,-101.319,72.313,-101.498,72.278,-101.724,72.315,-101.775,72.341,-101.804,72.385,-101.833,72.409,-101.909,72.431,-101.974,72.486,-102.402,72.595,-102.657,72.719,-102.709,72.765,-102.714,72.783,-102.687,72.843,-102.628,72.911,-102.551,72.978,-102.504,73.006,-102.336,73.064,-102.204,73.077,-102.02,73.07,-101.922,73.057,-101.835,73.018,-101.798,72.973,-101.755,72.943,-101.618,72.91,-101.544,72.883,-101.435,72.821,-101.351,72.746,-101.273,72.722,-101.088,72.713,-100.896,72.726,-100.485,72.773,-100.468,72.779,-100.443,72.807,-100.396,72.977,-100.368,72.978,-100.228,72.899,-100.188,72.89,-100.128,72.907,-100.092,72.945,-100.097,72.963,-100.184,73.055,-100.236,73.095,-100.283,73.12,-100.334,73.128,-100.446,73.121,-100.531,73.138,-100.55,73.164,-100.536,73.198,-100.489,73.234,-100.439,73.255,-100.341,73.265,-100.226,73.255,-100.067,73.211,-99.966,73.201,-99.825,73.214,-100.006,73.24,-100.258,73.34,-100.366,73.359,-100.498,73.316,-100.587,73.3,-100.755,73.278,-100.889,73.275,-101.451,73.431,-101.482,73.446,-101.523,73.486,-101.518,73.505,-101.463,73.534,-101.323,73.572,-101.115,73.596,-100.976,73.6,-100.854,73.571,-100.677,73.494,-100.522,73.449,-100.509,73.465,-100.536,73.51,-100.607,73.575,-100.658,73.593,-100.783,73.613,-100.898,73.658,-100.953,73.691,-100.982,73.727,-100.985,73.765,-100.963,73.791,-100.915,73.805,-100.484,73.844,-100.182,73.801,-99.991,73.795,-99.912,73.847,-99.94,73.857,-100.04,73.844,-100.154,73.844,-100.225,73.873,-100.227,73.889,-100.138,73.929,-100.002,73.946]],[[-84.92,65.261,-84.885,65.249,-84.842,65.256,-84.771,65.305,-84.612,65.447,-84.568,65.461,-84.501,65.458,-84.266,65.367,-84.18,65.316,-84.133,65.245,-84.085,65.218,-83.9,65.181,-83.723,65.169,-83.491,65.132,-83.407,65.104,-83.222,64.968,-83.201,64.96,-82.991,64.904,-82.668,64.78,-82.586,64.762,-82.272,64.721,-82.159,64.691,-82.05,64.644,-81.929,64.559,-81.787,64.426,-81.676,64.213,-81.667,64.171,-81.681,64.146,-81.721,64.119,-81.903,64.031,-81.887,64.016,-81.716,64.022,-81.336,64.076,-81.104,64.037,-81.024,64.031,-81.005,64.033,-80.921,64.1,-80.829,64.09,-80.694,64.025,-80.608,63.972,-80.569,63.932,-80.579,63.909,-80.668,63.901,-80.451,63.863,-80.261,63.802,-80.302,63.762,-80.504,63.674,-80.712,63.596,-80.954,63.48,-81.014,63.463,-81.046,63.462,-81.18,63.483,-81.372,63.538,-81.963,63.664,-82.146,63.691,-82.378,63.707,-82.412,63.737,-82.467,63.927,-82.571,63.961,-82.93,64,-83.034,64.023,-83.039,64.061,-83.016,64.127,-83.065,64.159,-83.186,64.158,-83.304,64.144,-83.494,64.099,-83.584,64.058,-83.617,64.013,-83.638,63.918,-83.662,63.873,-83.728,63.813,-84.022,63.66,-84.142,63.614,-84.26,63.6,-84.308,63.586,-84.387,63.529,-84.506,63.39,-84.555,63.35,-84.633,63.309,-84.796,63.247,-84.962,63.197,-85.238,63.139,-85.393,63.12,-85.496,63.139,-85.566,63.271,-85.714,63.658,-85.739,63.684,-85.769,63.7,-85.805,63.707,-86.302,63.657,-86.576,63.662,-86.847,63.575,-86.915,63.569,-87.053,63.572,-87.152,63.586,-87.177,63.595,-87.194,63.633,-87.189,63.672,-87.154,63.715,-87.032,63.83,-86.932,63.902,-86.886,63.924,-86.422,64.052,-86.309,64.094,-86.252,64.137,-86.252,64.181,-86.274,64.238,-86.354,64.377,-86.375,64.503,-86.374,64.566,-86.344,64.662,-86.228,64.896,-86.188,65.01,-86.114,65.417,-86.075,65.534,-86.017,65.64,-85.962,65.704,-85.814,65.832,-85.699,65.883,-85.555,65.919,-85.523,65.915,-85.496,65.9,-85.442,65.846,-85.241,65.796,-85.176,65.747,-85.13,65.693,-85.105,65.623,-85.13,65.592,-85.226,65.546,-85.243,65.526,-85.24,65.51,-85.056,65.437,-84.92,65.261]],[[-97.701,76.467,-97.69,76.422,-97.702,76.387,-97.737,76.363,-97.739,76.335,-97.707,76.304,-97.573,76.224,-97.531,76.182,-97.524,76.139,-97.531,76.109,-97.613,76.053,-97.65,75.979,-97.652,75.94,-97.603,75.879,-97.602,75.851,-97.694,75.803,-97.891,75.76,-97.863,75.738,-97.44,75.685,-97.408,75.673,-97.41,75.552,-97.336,75.42,-97.363,75.417,-97.465,75.459,-97.653,75.508,-97.878,75.416,-97.853,75.26,-97.705,75.191,-97.66,75.151,-97.674,75.127,-97.799,75.117,-97.843,75.122,-97.971,75.153,-98.045,75.201,-98.069,75.199,-98.092,75.176,-98.077,75.153,-97.99,75.111,-97.953,75.06,-97.992,75.046,-98.121,75.033,-98.295,75.032,-98.569,75.009,-98.704,75.006,-98.835,75.018,-99.01,75.021,-99.156,75.016,-99.245,75.026,-99.326,75.049,-99.421,75.044,-99.627,74.984,-99.947,75.003,-100.234,75.008,-100.292,75.028,-100.357,75.067,-100.483,75.188,-100.459,75.219,-100.152,75.236,-100.146,75.246,-100.364,75.29,-100.615,75.321,-100.731,75.347,-100.704,75.394,-100.712,75.406,-100.28,75.461,-99.965,75.569,-99.77,75.612,-99.756,75.633,-99.591,75.655,-99.209,75.669,-99.195,75.698,-99.915,75.681,-100.902,75.62,-101.207,75.59,-101.461,75.608,-102.541,75.514,-102.587,75.514,-102.7,75.544,-102.798,75.6,-102.728,75.639,-102.411,75.713,-102.252,75.778,-102.271,75.813,-102.145,75.875,-101.943,75.884,-101.6,75.833,-101.421,75.782,-101.261,75.758,-101.119,75.763,-100.973,75.798,-101.01,75.802,-101.259,75.784,-101.288,75.789,-101.415,75.846,-101.47,75.882,-101.506,75.918,-101.508,75.944,-101.431,75.992,-101.717,76.008,-101.823,76.041,-101.872,76.083,-101.861,76.101,-101.771,76.15,-101.529,76.217,-101.557,76.236,-101.91,76.234,-101.987,76.243,-102.138,76.285,-102.105,76.331,-101.964,76.399,-101.858,76.439,-101.788,76.451,-101.677,76.451,-101.415,76.425,-101.34,76.41,-101.139,76.345,-101.088,76.308,-101.094,76.272,-101.056,76.246,-100.9,76.207,-100.231,76.008,-100.106,75.96,-100.02,75.94,-99.865,75.924,-99.775,75.927,-99.701,75.941,-99.689,75.96,-99.978,76.029,-100.051,76.067,-100.113,76.117,-100.086,76.134,-100.002,76.139,-99.79,76.133,-99.541,76.146,-99.817,76.168,-99.998,76.196,-100.183,76.197,-100.414,76.243,-100.414,76.257,-100.358,76.271,-100.043,76.291,-99.983,76.3,-99.978,76.312,-100.082,76.343,-100.175,76.359,-100.651,76.396,-100.82,76.437,-100.874,76.457,-100.891,76.475,-100.83,76.524,-100.574,76.585,-100.388,76.614,-100.069,76.635,-99.814,76.632,-99.669,76.624,-99.329,76.521,-99.17,76.454,-98.89,76.466,-98.971,76.537,-99.024,76.615,-98.941,76.643,-98.711,76.694,-98.528,76.667,-98.289,76.599,-98.236,76.575,-97.967,76.533,-97.808,76.519,-97.726,76.496,-97.701,76.467]],[[-103.426,79.316,-103.192,79.295,-102.914,79.231,-102.652,79.095,-102.639,79.078,-102.638,79.055,-102.648,79.027,-102.683,78.991,-102.731,78.969,-102.596,78.943,-102.581,78.93,-102.593,78.901,-102.576,78.879,-102.495,78.901,-102.425,78.933,-102.407,78.954,-102.393,79.01,-102.189,79.038,-101.974,79.079,-101.873,79.088,-101.704,79.079,-101.299,78.982,-101.145,78.973,-101.088,78.962,-101.037,78.939,-101.034,78.915,-101.116,78.858,-101.147,78.824,-101.128,78.802,-100.917,78.783,-100.435,78.82,-100.015,78.729,-99.782,78.62,-99.609,78.583,-99.582,78.563,-99.631,78.545,-99.68,78.494,-99.818,78.455,-99.848,78.438,-99.774,78.393,-99.769,78.365,-99.778,78.325,-99.751,78.303,-99.562,78.279,-99.132,78.118,-99.053,78.072,-99.005,78.016,-99,77.997,-99.061,77.966,-99.128,77.877,-99.166,77.857,-99.341,77.84,-99.659,77.824,-99.956,77.794,-100.275,77.833,-100.586,77.892,-100.68,77.931,-100.758,77.978,-100.778,77.996,-100.81,78.072,-100.826,78.088,-100.958,78.13,-101.074,78.194,-101.298,78.199,-101.829,78.264,-102.057,78.28,-102.284,78.275,-102.607,78.249,-102.668,78.256,-102.723,78.275,-102.772,78.307,-102.784,78.33,-102.731,78.371,-103.677,78.32,-103.947,78.26,-104.324,78.269,-104.513,78.295,-104.764,78.352,-104.879,78.401,-104.985,78.468,-104.996,78.519,-104.91,78.553,-104.82,78.573,-104.727,78.579,-104.214,78.54,-103.764,78.52,-103.571,78.54,-103.483,78.594,-103.588,78.623,-104.021,78.635,-103.929,78.663,-103.563,78.693,-103.372,78.736,-103.408,78.752,-103.518,78.769,-104.009,78.764,-104.185,78.781,-104.195,78.796,-104.155,78.814,-103.876,78.903,-103.887,78.919,-104.007,78.948,-104.113,78.986,-104.152,78.99,-104.395,78.956,-104.736,78.826,-104.817,78.807,-104.896,78.808,-104.97,78.829,-104.97,78.856,-104.893,78.89,-104.735,78.991,-104.747,79.027,-104.901,79.051,-105.309,79.033,-105.536,79.033,-105.571,79.061,-105.58,79.114,-105.571,79.164,-105.515,79.242,-105.436,79.302,-105.388,79.324,-104.847,79.311,-103.965,79.348,-103.706,79.352,-103.426,79.316]],[[-91.886,81.133,-91.755,81.049,-91.272,80.85,-91.054,80.778,-90.683,80.688,-90.637,80.655,-90.632,80.642,-90.643,80.594,-90.537,80.576,-90.218,80.548,-89.862,80.498,-89.798,80.501,-89.674,80.531,-89.525,80.539,-89.329,80.532,-89.236,80.511,-89.167,80.48,-89.138,80.457,-89.134,80.44,-89.204,80.407,-89.197,80.394,-89.155,80.379,-89.147,80.36,-89.218,80.289,-89.198,80.263,-89.019,80.198,-88.857,80.166,-88.538,80.131,-88.329,80.134,-88.2,80.111,-88.197,80.125,-88.255,80.167,-88.381,80.225,-88.612,80.255,-88.646,80.29,-88.663,80.348,-88.644,80.387,-88.525,80.418,-88.424,80.428,-88.125,80.429,-87.96,80.416,-87.675,80.372,-87.646,80.348,-87.63,80.302,-87.618,80.207,-87.625,80.187,-87.869,80.134,-87.922,80.098,-87.861,80.088,-87.651,80.079,-87.329,80.047,-87.202,80.043,-87.076,79.967,-86.977,79.894,-87.05,79.805,-87.144,79.663,-87.22,79.63,-87.295,79.58,-87.243,79.571,-86.925,79.591,-86.861,79.598,-86.649,79.646,-86.337,79.635,-86.232,79.622,-86.18,79.605,-86.086,79.551,-86.007,79.479,-85.949,79.486,-85.804,79.573,-85.751,79.595,-85.679,79.615,-85.648,79.611,-85.501,79.53,-85.176,79.387,-85.064,79.328,-85.042,79.285,-85.181,79.234,-85.29,79.208,-86.092,79.1,-86.451,79.039,-86.629,78.991,-86.721,78.975,-86.913,78.983,-86.957,78.975,-87.016,78.899,-87.08,78.866,-87.246,78.813,-87.479,78.718,-87.617,78.676,-87.861,78.707,-87.922,78.751,-87.956,78.852,-87.961,78.893,-87.953,78.915,-87.923,78.951,-87.817,79.036,-87.829,79.045,-87.878,79.038,-88.04,78.995,-88.104,78.973,-88.164,78.933,-88.19,78.867,-88.167,78.746,-88.19,78.696,-88.254,78.672,-88.228,78.653,-88.037,78.627,-88.003,78.616,-87.982,78.595,-87.974,78.565,-87.983,78.537,-88.04,78.494,-88.148,78.477,-88.285,78.497,-88.581,78.602,-88.709,78.596,-88.742,78.584,-88.714,78.546,-88.623,78.462,-88.606,78.392,-88.648,78.334,-88.733,78.242,-88.791,78.192,-88.822,78.186,-88.97,78.184,-89.096,78.209,-89.47,78.37,-89.655,78.439,-89.926,78.573,-89.995,78.601,-90.037,78.607,-90.076,78.549,-90.001,78.496,-89.757,78.37,-89.612,78.279,-89.507,78.203,-89.49,78.172,-89.526,78.16,-89.579,78.167,-89.651,78.193,-89.873,78.238,-89.965,78.262,-90.025,78.291,-90.136,78.313,-90.297,78.328,-90.459,78.331,-90.622,78.322,-90.652,78.308,-90.469,78.269,-90.405,78.247,-90.358,78.219,-90.327,78.185,-90.387,78.163,-90.614,78.15,-90.918,78.158,-91.41,78.188,-91.899,78.237,-92.351,78.313,-92.678,78.389,-92.808,78.43,-92.848,78.46,-92.726,78.487,-92.297,78.521,-91.867,78.543,-91.935,78.562,-92.716,78.605,-92.973,78.613,-93.109,78.602,-93.267,78.608,-93.389,78.643,-93.552,78.708,-93.634,78.751,-93.623,78.768,-93.561,78.777,-93.208,78.769,-93.16,78.776,-93.336,78.808,-93.902,78.872,-94.115,78.929,-94.154,78.951,-94.17,78.973,-94.163,78.994,-93.95,79.037,-93.294,79.14,-93.068,79.155,-92.842,79.156,-92.684,79.186,-92.547,79.283,-91.868,79.317,-91.344,79.361,-91.3,79.373,-91.693,79.365,-92.248,79.373,-92.485,79.439,-92.645,79.45,-92.822,79.45,-93.028,79.429,-93.381,79.368,-93.55,79.354,-93.933,79.291,-94.04,79.295,-94.093,79.303,-94.109,79.315,-94.04,79.357,-93.94,79.386,-93.96,79.396,-94.11,79.402,-94.284,79.4,-94.405,79.391,-94.846,79.335,-95.044,79.294,-95.103,79.29,-95.317,79.355,-95.657,79.39,-95.733,79.418,-95.663,79.527,-95.563,79.55,-95.302,79.568,-94.52,79.667,-94.476,79.686,-94.402,79.736,-94.581,79.726,-94.973,79.677,-95.297,79.653,-95.552,79.653,-95.739,79.66,-95.858,79.674,-96,79.705,-96.463,79.848,-96.589,79.917,-96.607,79.978,-96.639,80.024,-96.773,80.136,-95.782,80.066,-95.394,80.053,-94.646,80.049,-94.611,80.056,-94.6,80.074,-94.613,80.103,-94.607,80.126,-94.583,80.141,-94.304,80.182,-94.263,80.195,-94.59,80.202,-95.192,80.134,-95.405,80.135,-95.646,80.231,-95.904,80.214,-96.026,80.222,-96.215,80.246,-96.308,80.267,-96.368,80.293,-96.394,80.315,-96.385,80.333,-96.334,80.353,-96.112,80.38,-96.012,80.383,-95.747,80.365,-95.549,80.367,-95.614,80.396,-95.901,80.471,-96.152,80.553,-96.133,80.691,-95.927,80.721,-95.714,80.725,-95.505,80.691,-95.226,80.686,-95.026,80.646,-94.893,80.571,-94.734,80.572,-94.485,80.558,-93.928,80.559,-94.029,80.586,-94.202,80.61,-94.596,80.641,-94.788,80.751,-95.196,80.808,-95.515,80.838,-95.509,80.863,-95.27,81.001,-94.981,81.05,-94.519,81.031,-94.216,81.057,-93.826,81.106,-93.444,81.083,-93.345,81.085,-93.287,81.1,-93.236,81.129,-93.235,81.155,-93.286,81.179,-93.407,81.209,-93.894,81.213,-94.11,81.225,-94.194,81.241,-94.219,81.265,-94.231,81.29,-94.233,81.315,-94.22,81.331,-94.179,81.339,-94.06,81.349,-93.605,81.351,-93.333,81.364,-93.035,81.346,-92.413,81.278,-92.212,81.244,-91.998,81.185,-91.886,81.133]],[[-94.295,76.912,-94.108,76.904,-93.948,76.917,-93.811,76.914,-93.608,76.874,-93.421,76.812,-93.277,76.784,-93.23,76.77,-93.212,76.755,-93.189,76.708,-93.19,76.686,-93.201,76.669,-93.264,76.626,-93.317,76.574,-93.426,76.527,-93.485,76.492,-93.535,76.448,-93.422,76.474,-92.995,76.62,-92.716,76.603,-92.297,76.616,-91.789,76.676,-91.548,76.685,-91.305,76.681,-91.124,76.662,-90.738,76.581,-90.605,76.543,-90.555,76.516,-90.543,76.496,-90.622,76.465,-90.864,76.484,-91.263,76.5,-91.336,76.511,-91.398,76.51,-91.443,76.499,-91.415,76.456,-91.334,76.446,-90.855,76.437,-89.285,76.302,-89.219,76.258,-89.237,76.239,-89.292,76.218,-89.407,76.189,-90.312,76.158,-90.827,76.186,-91.26,76.23,-91.407,76.22,-91.279,76.16,-91.02,76.142,-90.802,76.106,-90.712,76.076,-90.251,76.053,-90.176,76.03,-90.033,75.971,-89.913,75.966,-89.793,75.925,-89.695,75.854,-89.65,75.844,-89.511,75.857,-89.278,75.795,-89.205,75.762,-89.205,75.737,-89.257,75.698,-89.361,75.646,-89.625,75.584,-89.646,75.565,-89.337,75.572,-89.28,75.564,-88.917,75.454,-88.869,75.452,-88.839,75.463,-88.804,75.502,-88.82,75.539,-88.864,75.589,-88.852,75.625,-88.784,75.647,-88.715,75.659,-88.645,75.658,-88.569,75.645,-88.201,75.512,-87.73,75.576,-87.644,75.547,-87.572,75.494,-87.539,75.485,-87.365,75.591,-87.257,75.618,-86.814,75.491,-86.545,75.463,-86.437,75.436,-86.236,75.406,-85.951,75.395,-85.905,75.442,-86.069,75.502,-85.973,75.529,-85.581,75.58,-85.372,75.573,-84.987,75.645,-84.75,75.655,-84.605,75.653,-84.128,75.763,-84.014,75.78,-83.932,75.819,-83.745,75.813,-83.237,75.751,-83.093,75.756,-82.553,75.818,-82.354,75.833,-82.154,75.831,-81.647,75.795,-81.269,75.756,-81.151,75.736,-81.193,75.684,-81.174,75.669,-81.124,75.658,-81.001,75.643,-80.528,75.642,-80.322,75.629,-80.158,75.581,-80.119,75.562,-80.126,75.542,-80.287,75.49,-80.26,75.479,-80.1,75.467,-79.738,75.461,-79.66,75.45,-79.586,75.385,-79.508,75.295,-79.509,75.26,-79.634,75.199,-79.977,75.119,-80.358,75.052,-80.382,75.034,-80.261,75.002,-80.135,74.988,-80.036,74.991,-79.733,75.021,-79.664,75.021,-79.525,74.99,-79.46,74.959,-79.401,74.918,-79.508,74.88,-79.944,74.834,-80.202,74.895,-80.289,74.908,-80.348,74.903,-80.315,74.876,-80.19,74.828,-80.149,74.796,-80.192,74.78,-80.213,74.749,-80.21,74.704,-80.221,74.657,-80.263,74.584,-80.278,74.582,-81.226,74.567,-81.34,74.554,-81.607,74.502,-81.809,74.477,-81.94,74.473,-82.069,74.482,-82.415,74.535,-82.645,74.525,-82.736,74.53,-82.931,74.566,-82.978,74.583,-83.058,74.63,-83.117,74.693,-83.112,74.732,-83.087,74.788,-83.103,74.817,-83.158,74.817,-83.22,74.828,-83.407,74.885,-83.522,74.901,-83.544,74.892,-83.51,74.848,-83.487,74.834,-83.364,74.802,-83.341,74.765,-83.394,74.67,-83.412,74.655,-83.532,74.586,-83.622,74.566,-83.868,74.564,-84.245,74.515,-84.426,74.508,-84.667,74.52,-84.818,74.542,-84.916,74.568,-85.012,74.604,-85.061,74.607,-85.087,74.528,-85.133,74.517,-85.214,74.519,-85.339,74.543,-85.442,74.601,-85.474,74.6,-85.489,74.567,-85.512,74.545,-85.544,74.535,-85.808,74.499,-85.956,74.499,-86.11,74.54,-86.211,74.536,-86.341,74.513,-86.655,74.555,-86.731,74.557,-86.666,74.489,-86.77,74.479,-86.995,74.48,-87.364,74.502,-87.593,74.47,-88.006,74.489,-88.423,74.494,-88.501,74.51,-88.556,74.541,-88.558,74.57,-88.538,74.609,-88.477,74.667,-88.375,74.744,-88.34,74.785,-88.431,74.804,-88.488,74.829,-88.535,74.832,-88.682,74.802,-88.778,74.715,-88.851,74.69,-88.883,74.711,-88.908,74.764,-88.94,74.79,-88.98,74.788,-89.02,74.774,-89.058,74.747,-89.115,74.738,-89.192,74.745,-89.219,74.732,-89.197,74.698,-89.189,74.667,-89.196,74.638,-89.262,74.609,-89.45,74.568,-89.559,74.555,-89.844,74.549,-90.015,74.561,-90.362,74.61,-90.553,74.613,-90.784,74.696,-90.967,74.715,-90.958,74.745,-90.878,74.801,-90.88,74.818,-91.13,74.736,-91.164,74.71,-91.135,74.65,-91.168,74.646,-91.339,74.667,-91.508,74.651,-91.549,74.656,-91.666,74.699,-91.871,74.744,-91.962,74.793,-92.103,74.948,-92.174,75.051,-92.165,75.072,-92.06,75.101,-92.076,75.124,-92.207,75.181,-92.347,75.23,-92.389,75.263,-92.408,75.297,-92.427,75.346,-92.428,75.383,-92.411,75.406,-92.331,75.479,-92.11,75.611,-92.081,75.634,-92.069,75.658,-92.099,75.727,-92.142,75.797,-92.185,75.847,-92.307,75.915,-92.474,75.986,-92.709,76.114,-92.883,76.214,-93.092,76.354,-93.192,76.366,-93.309,76.36,-93.56,76.311,-93.644,76.289,-93.665,76.273,-93.852,76.27,-94.383,76.282,-94.585,76.297,-94.737,76.293,-94.997,76.258,-95.274,76.264,-95.447,76.363,-95.842,76.416,-95.959,76.446,-96.04,76.487,-96.013,76.513,-95.789,76.537,-95.696,76.563,-95.651,76.585,-95.873,76.566,-95.971,76.57,-96.64,76.703,-96.846,76.726,-96.881,76.738,-96.898,76.754,-96.898,76.773,-96.878,76.803,-96.679,76.766,-96.59,76.763,-96.451,76.774,-96.402,76.797,-96.433,76.811,-96.661,76.855,-96.771,76.889,-96.814,76.913,-96.77,76.948,-96.758,76.972,-96.685,76.985,-96.55,76.988,-96.377,77.005,-96.061,77.05,-95.85,77.066,-95.638,77.064,-95.126,77.017,-94.616,76.958,-94.295,76.912]],[[-96.204,78.531,-95.968,78.505,-95.561,78.517,-95.413,78.498,-95.031,78.43,-94.915,78.391,-94.888,78.36,-94.887,78.345,-95.014,78.313,-95.268,78.263,-95.329,78.225,-95.103,78.178,-94.988,78.136,-94.936,78.106,-94.934,78.076,-95.087,77.993,-95.199,77.968,-95.371,77.971,-95.452,77.963,-95.671,77.924,-96.012,77.887,-96.477,77.872,-96.603,77.849,-96.834,77.812,-96.99,77.806,-97.04,77.827,-97.064,77.859,-97.052,77.881,-97.019,77.908,-97.093,77.933,-97.427,77.982,-97.621,78.05,-97.648,78.072,-97.658,78.091,-97.227,78.103,-97.041,78.117,-96.958,78.139,-96.945,78.152,-97.027,78.157,-97.323,78.203,-97.819,78.231,-97.843,78.262,-98.05,78.326,-98.069,78.386,-98.114,78.403,-98.255,78.429,-98.276,78.438,-98.317,78.477,-98.324,78.498,-98.316,78.517,-98.06,78.558,-98.096,78.587,-98.29,78.692,-98.341,78.751,-98.333,78.774,-98.212,78.805,-98.043,78.805,-97.596,78.796,-97.382,78.783,-97.169,78.758,-96.936,78.72,-96.587,78.687,-96.475,78.665,-96.265,78.595,-96.243,78.573,-96.256,78.551,-96.204,78.531]],[[-93.171,74.161,-92.778,74.114,-92.587,74.083,-92.493,74.062,-92.314,73.992,-92.223,73.972,-91.874,74.013,-91.63,74.028,-91.088,74.009,-90.627,73.952,-90.458,73.908,-90.355,73.869,-90.381,73.825,-90.466,73.754,-90.566,73.686,-90.765,73.581,-90.934,73.528,-90.975,73.502,-91.002,73.467,-91.068,73.416,-91.249,73.304,-91.298,73.285,-91.554,73.236,-91.466,73.214,-91.426,73.195,-91.46,73.145,-91.621,73.026,-91.788,72.915,-91.905,72.849,-92.118,72.754,-92.235,72.727,-92.392,72.718,-93.341,72.802,-93.579,72.801,-94.211,72.757,-94.152,72.736,-93.92,72.703,-93.771,72.668,-93.572,72.559,-93.546,72.531,-93.534,72.499,-93.542,72.437,-93.555,72.421,-93.871,72.253,-93.973,72.13,-94.038,72.029,-94.144,72.001,-94.497,72.044,-94.611,72.042,-95.008,72.013,-95.193,72.027,-95.167,72.18,-95.193,72.345,-95.251,72.502,-95.548,72.782,-95.58,72.831,-95.602,72.884,-95.613,72.942,-95.612,72.999,-95.592,73.115,-95.589,73.174,-95.604,73.328,-95.644,73.557,-95.648,73.639,-95.645,73.671,-95.633,73.695,-95.569,73.728,-95.447,73.752,-95.386,73.755,-94.996,73.686,-94.817,73.663,-94.698,73.664,-94.691,73.671,-94.797,73.686,-94.897,73.716,-95.059,73.805,-95.134,73.881,-95.149,73.906,-95.153,73.933,-95.145,73.96,-95.121,73.985,-95.04,74.024,-94.974,74.041,-94.729,74.086,-94.483,74.113,-93.939,74.132,-93.785,74.118,-93.549,74.167,-93.41,74.179,-93.171,74.161]],[[-97.439,69.643,-97.409,69.631,-97.351,69.641,-97.306,69.673,-97.278,69.68,-97.236,69.673,-97.096,69.615,-96.989,69.554,-96.875,69.51,-96.695,69.471,-96.3,69.344,-96.184,69.259,-96.061,69.125,-95.951,69.024,-95.855,68.954,-95.751,68.898,-95.585,68.835,-95.438,68.881,-95.374,68.892,-95.32,68.873,-95.268,68.826,-95.295,68.805,-95.359,68.778,-95.466,68.747,-95.614,68.745,-95.686,68.736,-95.802,68.686,-95.895,68.627,-96.024,68.607,-96.268,68.508,-96.402,68.471,-96.599,68.461,-97.008,68.539,-97.264,68.528,-97.472,68.544,-97.705,68.626,-97.885,68.672,-98.235,68.739,-98.258,68.749,-98.273,68.772,-98.28,68.807,-98.296,68.831,-98.321,68.843,-98.376,68.842,-98.432,68.818,-98.54,68.798,-98.704,68.803,-98.775,68.817,-98.83,68.839,-98.859,68.864,-98.864,68.894,-98.879,68.916,-98.904,68.932,-98.964,68.933,-99.057,68.918,-99.094,68.899,-99.073,68.877,-99.091,68.863,-99.254,68.863,-99.318,68.876,-99.441,68.918,-99.495,68.96,-99.564,69.034,-99.557,69.054,-99.513,69.1,-99.456,69.131,-99.085,69.15,-98.912,69.168,-98.724,69.219,-98.504,69.308,-98.456,69.335,-98.45,69.354,-98.467,69.375,-98.535,69.426,-98.559,69.461,-98.537,69.478,-98.448,69.48,-98.495,69.499,-98.534,69.527,-98.548,69.545,-98.546,69.573,-98.476,69.579,-98.389,69.565,-98.222,69.485,-98.156,69.469,-98.041,69.457,-98.163,69.512,-98.289,69.629,-98.304,69.669,-98.301,69.692,-98.268,69.754,-98.239,69.78,-98.2,69.797,-98.081,69.833,-97.889,69.858,-97.791,69.862,-97.691,69.841,-97.604,69.802,-97.411,69.738,-97.383,69.712,-97.386,69.7,-97.46,69.683,-97.469,69.667,-97.439,69.643]],[[-61.105,45.945,-61.071,45.937,-60.937,45.986,-60.865,45.983,-60.868,45.949,-60.984,45.911,-61.038,45.882,-60.971,45.856,-60.972,45.838,-61.052,45.795,-61.092,45.748,-61.059,45.703,-60.93,45.748,-60.878,45.748,-60.806,45.738,-60.738,45.751,-60.699,45.773,-60.472,45.947,-60.461,45.969,-60.705,45.933,-60.733,45.957,-60.573,46.061,-60.586,46.117,-60.505,46.204,-60.431,46.256,-60.377,46.285,-60.298,46.311,-60.244,46.27,-60.226,46.196,-60.092,46.206,-59.961,46.191,-59.865,46.16,-59.85,46.141,-59.849,46.113,-59.881,46.062,-59.934,46.019,-59.828,45.965,-59.842,45.942,-60.016,45.88,-60.114,45.819,-60.205,45.743,-60.386,45.655,-60.673,45.591,-60.764,45.591,-60.872,45.611,-60.979,45.606,-61.084,45.582,-61.186,45.585,-61.236,45.573,-61.284,45.574,-61.323,45.598,-61.408,45.669,-61.45,45.716,-61.495,45.941,-61.481,46.06,-61.409,46.17,-61.302,46.244,-61.241,46.303,-60.983,46.65,-60.932,46.729,-60.87,46.797,-60.76,46.863,-60.617,46.976,-60.571,46.999,-60.489,47.01,-60.408,47.004,-60.431,46.963,-60.425,46.923,-60.332,46.768,-60.333,46.737,-60.384,46.613,-60.482,46.414,-60.508,46.303,-60.495,46.27,-60.534,46.215,-60.577,46.172,-60.745,46.093,-60.831,46.074,-60.912,46.045,-61.105,45.945]],[[-67.125,45.169,-67.171,45.182,-67.213,45.193,-67.25,45.201,-67.271,45.187,-67.291,45.168,-67.315,45.154,-67.367,45.174,-67.4,45.21,-67.453,45.248,-67.473,45.276,-67.462,45.309,-67.439,45.34,-67.428,45.378,-67.454,45.421,-67.477,45.446,-67.494,45.474,-67.488,45.501,-67.455,45.514,-67.424,45.53,-67.414,45.566,-67.433,45.603,-67.487,45.618,-67.531,45.613,-67.596,45.621,-67.658,45.644,-67.699,45.671,-67.731,45.686,-67.755,45.686,-67.785,45.702,-67.802,45.728,-67.8,45.77,-67.792,45.796,-67.775,45.818,-67.774,45.843,-67.781,45.86,-67.782,45.874,-67.778,45.892,-67.767,45.927,-67.785,45.953,-67.786,46.042,-67.79,46.209,-67.793,46.337,-67.796,46.498,-67.798,46.616,-67.8,46.78,-67.803,46.936,-67.807,47.083,-67.935,47.168,-68.097,47.275,-68.235,47.346,-68.311,47.354,-68.358,47.345,-68.377,47.316,-68.48,47.286,-68.669,47.253,-68.829,47.203,-68.887,47.203,-68.937,47.211,-69.003,47.236,-69.049,47.274,-69.064,47.338,-69.05,47.427,-69.146,47.445,-69.243,47.463,-69.302,47.402,-69.359,47.351,-69.471,47.239,-69.63,47.081,-69.718,46.995,-69.872,46.843,-70.008,46.709,-70.038,46.571,-70.067,46.441,-70.18,46.342,-70.248,46.251,-70.279,46.15,-70.304,46.057,-70.306,45.98,-70.287,45.939,-70.296,45.906,-70.333,45.868,-70.408,45.802,-70.421,45.738,-70.467,45.707,-70.596,45.644,-70.702,45.551,-70.707,45.499,-70.692,45.455,-70.69,45.428,-70.711,45.409,-70.753,45.411,-70.799,45.405,-70.838,45.366,-70.837,45.311,-70.865,45.271,-70.898,45.262,-70.926,45.291,-70.96,45.333,-71,45.337,-71.06,45.309,-71.135,45.263,-71.202,45.26,-71.327,45.29,-71.419,45.2,-71.518,45.008,-71.934,45.007,-72.35,45.007,-72.766,45.006,-73.182,45.006,-73.598,45.005,-74.014,45.005,-74.43,45.004,-74.663,45.004,-74.709,45.004,-74.566,45.042,-74.269,45.188,-74.05,45.241,-73.765,45.395,-73.558,45.425,-73.519,45.459,-73.484,45.587,-73.465,45.632,-73.369,45.758,-73.253,45.864,-73.16,46.01,-72.99,46.104,-72.733,46.182,-72.496,46.353,-72.366,46.405,-72.24,46.442,-72.187,46.512,-72.109,46.551,-71.901,46.632,-71.671,46.654,-71.439,46.721,-71.261,46.756,-71.152,46.819,-70.993,46.852,-70.519,47.033,-70.388,47.117,-70.218,47.29,-70.07,47.378,-70.017,47.471,-69.802,47.623,-69.581,47.824,-69.471,47.967,-69.306,48.047,-68.987,48.275,-68.816,48.366,-68.746,48.376,-68.552,48.457,-68.431,48.542,-68.238,48.626,-67.889,48.731,-67.561,48.856,-67.117,48.964,-66.598,49.126,-66.178,49.213,-65.883,49.226,-65.523,49.266,-65.396,49.262,-64.836,49.192,-64.568,49.105,-64.262,48.922,-64.216,48.874,-64.209,48.806,-64.371,48.839,-64.514,48.841,-64.415,48.804,-64.246,48.691,-64.254,48.55,-64.349,48.423,-64.633,48.36,-64.706,48.311,-64.765,48.228,-64.822,48.196,-64.96,48.16,-65.036,48.106,-65.259,48.021,-65.36,48.011,-65.476,48.031,-65.755,48.112,-65.927,48.189,-66.013,48.147,-66.083,48.103,-66.249,48.117,-66.324,48.098,-66.449,48.12,-66.704,48.022,-66.632,48.011,-66.429,48.067,-66.36,48.061,-66.21,47.989,-65.849,47.911,-65.756,47.86,-65.666,47.696,-65.607,47.67,-65.483,47.687,-65.344,47.768,-65.228,47.811,-65.002,47.847,-65.046,47.793,-64.874,47.797,-64.703,47.725,-64.766,47.673,-64.852,47.57,-64.912,47.369,-65.086,47.234,-65.319,47.101,-65.26,47.069,-65.192,47.05,-65.042,47.089,-64.942,47.086,-64.831,47.061,-64.866,46.958,-64.906,46.888,-64.883,46.823,-64.817,46.699,-64.726,46.671,-64.69,46.512,-64.641,46.426,-64.648,46.356,-64.557,46.311,-64.542,46.24,-64.212,46.22,-64.145,46.193,-63.916,46.166,-63.873,46.146,-63.832,46.107,-64.056,46.021,-63.875,45.959,-63.703,45.858,-63.568,45.878,-63.509,45.875,-63.358,45.811,-63.316,45.78,-63.293,45.752,-63.217,45.758,-63.108,45.782,-62.911,45.776,-62.701,45.741,-62.718,45.686,-62.75,45.648,-62.586,45.661,-62.483,45.622,-62.447,45.641,-62.422,45.665,-62.218,45.731,-61.956,45.868,-61.924,45.851,-61.912,45.799,-61.877,45.714,-61.777,45.656,-61.657,45.642,-61.492,45.687,-61.428,45.648,-61.35,45.574,-61.277,45.476,-61.282,45.441,-61.376,45.411,-61.461,45.367,-61.107,45.349,-61.071,45.33,-61.032,45.292,-61.068,45.253,-61.101,45.233,-61.165,45.256,-61.284,45.235,-61.387,45.185,-61.498,45.157,-61.569,45.154,-61.647,45.131,-61.719,45.094,-61.794,45.084,-62.027,44.994,-62.265,44.936,-62.514,44.844,-62.768,44.785,-63.032,44.715,-63.089,44.709,-63.156,44.711,-63.306,44.643,-63.381,44.652,-63.457,44.64,-63.544,44.655,-63.604,44.683,-63.558,44.611,-63.545,44.544,-63.568,44.514,-63.61,44.48,-63.761,44.486,-63.821,44.511,-63.891,44.546,-63.924,44.604,-64,44.645,-64.045,44.588,-64.045,44.545,-64.101,44.487,-64.167,44.587,-64.286,44.55,-64.339,44.445,-64.312,44.415,-64.276,44.334,-64.335,44.292,-64.378,44.304,-64.469,44.185,-64.578,44.142,-64.692,44.021,-64.826,43.929,-64.862,43.868,-65.087,43.727,-65.172,43.731,-65.235,43.727,-65.33,43.668,-65.344,43.55,-65.386,43.565,-65.429,43.561,-65.45,43.524,-65.482,43.518,-65.564,43.553,-65.662,43.534,-65.738,43.561,-65.835,43.734,-65.887,43.795,-65.978,43.815,-66.002,43.778,-66.038,43.742,-66.126,43.814,-66.193,44.08,-66.193,44.144,-66.1,44.367,-65.868,44.569,-65.942,44.576,-66.146,44.436,-66.125,44.47,-66.091,44.505,-66.022,44.562,-65.917,44.615,-65.778,44.646,-65.682,44.651,-65.616,44.68,-65.52,44.733,-65.502,44.76,-65.587,44.729,-65.728,44.697,-65.692,44.738,-65.657,44.76,-64.903,45.121,-64.751,45.18,-64.449,45.256,-64.407,45.306,-64.448,45.337,-64.331,45.309,-64.34,45.268,-64.359,45.238,-64.366,45.187,-64.354,45.138,-64.235,45.114,-64.135,45.023,-64.183,45.147,-64.093,45.217,-63.748,45.311,-63.46,45.321,-63.368,45.365,-63.614,45.394,-63.906,45.378,-64.087,45.411,-64.336,45.39,-64.6,45.41,-64.681,45.383,-64.747,45.324,-64.832,45.35,-64.873,45.355,-64.913,45.375,-64.827,45.476,-64.56,45.625,-64.397,45.756,-64.351,45.783,-64.315,45.836,-64.404,45.827,-64.482,45.806,-64.536,45.867,-64.633,45.947,-64.642,45.913,-64.594,45.814,-64.779,45.638,-64.898,45.626,-65.057,45.544,-65.282,45.473,-65.545,45.337,-65.884,45.223,-65.956,45.222,-66.11,45.317,-66.067,45.359,-66.027,45.418,-66.065,45.401,-66.09,45.376,-66.183,45.335,-66.107,45.257,-66.144,45.228,-66.252,45.189,-66.352,45.133,-66.44,45.096,-66.511,45.143,-66.707,45.083,-66.872,45.067,-66.908,45.098,-66.919,45.146,-66.977,45.157,-67.084,45.144,-67.125,45.169]],[[-55.459,51.537,-55.532,51.437,-55.583,51.389,-55.631,51.373,-55.731,51.359,-55.941,51.343,-56.031,51.328,-56.044,51.262,-56.031,51.227,-56,51.199,-55.961,51.191,-55.874,51.208,-55.841,51.205,-55.815,51.191,-55.796,51.166,-55.785,51.131,-55.785,51.087,-55.8,51.033,-55.871,50.907,-55.962,50.838,-56.078,50.781,-56.107,50.759,-56.121,50.734,-56.136,50.651,-56.196,50.585,-56.382,50.417,-56.454,50.38,-56.455,50.35,-56.484,50.271,-56.539,50.207,-56.694,50.06,-56.732,50.008,-56.75,49.967,-56.747,49.908,-56.754,49.883,-56.79,49.834,-56.839,49.788,-56.849,49.765,-56.829,49.725,-56.809,49.71,-56.807,49.673,-56.822,49.613,-56.757,49.652,-56.611,49.788,-56.501,49.87,-56.428,49.897,-56.376,49.934,-56.322,50.014,-56.247,50.09,-56.179,50.115,-56.148,50.1,-56.122,50.063,-56.127,50.015,-56.164,49.957,-56.161,49.94,-56.075,49.983,-55.927,50.018,-55.873,50.013,-55.765,49.96,-55.674,49.967,-55.53,49.997,-55.503,49.983,-55.527,49.937,-55.584,49.892,-55.718,49.829,-56.04,49.705,-56.14,49.619,-56.121,49.622,-56.052,49.658,-55.979,49.678,-55.902,49.681,-55.87,49.67,-55.882,49.646,-55.892,49.58,-56.087,49.452,-56.041,49.457,-55.815,49.515,-55.678,49.435,-55.49,49.463,-55.376,49.49,-55.379,49.473,-55.354,49.438,-55.355,49.381,-55.344,49.373,-55.29,49.392,-55.28,49.413,-55.283,49.514,-55.266,49.524,-55.23,49.508,-55.207,49.482,-55.2,49.408,-55.225,49.335,-55.259,49.267,-55.342,49.168,-55.332,49.126,-55.353,49.079,-55.335,49.078,-55.252,49.121,-55.247,49.139,-55.254,49.18,-55.245,49.2,-55.176,49.244,-55.063,49.297,-55.026,49.305,-55.01,49.293,-55.016,49.26,-54.983,49.268,-54.911,49.316,-54.844,49.345,-54.782,49.355,-54.718,49.389,-54.651,49.445,-54.579,49.491,-54.502,49.527,-54.469,49.53,-54.481,49.469,-54.465,49.401,-54.463,49.342,-54.448,49.329,-54.389,49.392,-54.356,49.415,-54.317,49.424,-54.271,49.419,-53.958,49.442,-53.862,49.426,-53.755,49.385,-53.619,49.322,-53.57,49.264,-53.56,49.192,-53.573,49.141,-53.671,49.078,-53.758,49.035,-53.809,48.993,-53.825,48.951,-53.845,48.925,-53.903,48.889,-54.161,48.788,-54.1,48.785,-53.951,48.807,-53.853,48.811,-53.848,48.797,-53.887,48.768,-53.962,48.739,-53.97,48.725,-53.966,48.707,-53.886,48.685,-53.784,48.695,-53.698,48.68,-53.706,48.656,-53.775,48.576,-53.795,48.526,-53.886,48.485,-54.068,48.419,-54.114,48.394,-54.104,48.388,-53.937,48.437,-53.853,48.449,-53.799,48.449,-53.739,48.496,-53.644,48.511,-53.552,48.482,-53.411,48.562,-53.361,48.573,-53.275,48.563,-53.22,48.578,-53.127,48.633,-53.057,48.659,-53.043,48.657,-53.028,48.635,-53.021,48.572,-53.037,48.516,-53.06,48.48,-53.136,48.402,-53.182,48.374,-53.225,48.364,-53.301,48.368,-53.334,48.356,-53.406,48.294,-53.531,48.232,-53.61,48.208,-53.56,48.174,-53.542,48.108,-53.569,48.088,-53.704,48.068,-53.71,48.057,-53.758,48.042,-53.87,48.02,-53.794,48.01,-53.653,48.026,-53.638,48.015,-53.658,47.969,-53.695,47.921,-53.862,47.799,-53.864,47.787,-53.838,47.727,-53.805,47.682,-53.765,47.65,-53.672,47.648,-53.604,47.662,-53.504,47.744,-53.283,47.998,-53.085,48.069,-52.921,48.147,-52.883,48.131,-52.866,48.113,-52.872,48.094,-52.955,48.029,-52.998,47.976,-53.111,47.812,-53.154,47.735,-53.176,47.653,-53.17,47.512,-53.158,47.488,-53.122,47.455,-53.057,47.483,-52.945,47.553,-52.873,47.619,-52.817,47.728,-52.782,47.769,-52.745,47.769,-52.711,47.745,-52.703,47.693,-52.672,47.622,-52.654,47.549,-52.669,47.47,-52.684,47.426,-52.912,47.103,-52.888,47.046,-52.882,47.011,-52.889,46.974,-52.962,46.819,-53.032,46.723,-53.07,46.681,-53.115,46.656,-53.167,46.646,-53.214,46.66,-53.255,46.698,-53.291,46.717,-53.323,46.718,-53.382,46.711,-53.536,46.633,-53.568,46.628,-53.59,46.639,-53.616,46.68,-53.595,46.888,-53.581,46.957,-53.612,47.01,-53.58,47.099,-53.578,47.133,-53.597,47.146,-53.636,47.138,-53.695,47.093,-53.774,47.012,-53.86,46.939,-54.01,46.84,-54.076,46.82,-54.102,46.825,-54.133,46.839,-54.174,46.88,-54.173,46.917,-54.155,46.967,-54.093,47.086,-53.971,47.262,-53.869,47.387,-53.85,47.44,-53.878,47.464,-53.901,47.509,-53.94,47.645,-53.989,47.756,-54.047,47.806,-54.192,47.86,-54.218,47.847,-54.234,47.772,-54.405,47.556,-54.434,47.462,-54.456,47.428,-54.488,47.404,-54.563,47.375,-54.542,47.425,-54.463,47.536,-54.474,47.547,-54.575,47.458,-54.651,47.408,-54.745,47.395,-54.802,47.399,-54.857,47.385,-55.09,47.174,-55.099,47.104,-55.14,47.046,-55.255,46.942,-55.316,46.906,-55.401,46.899,-55.479,46.917,-55.531,46.914,-55.652,46.881,-55.789,46.867,-55.845,46.874,-55.881,46.887,-55.95,46.928,-55.958,46.956,-55.954,46.973,-55.919,47.017,-55.838,47.072,-55.772,47.092,-55.61,47.12,-55.492,47.161,-55.401,47.221,-55.361,47.259,-55.191,47.449,-54.976,47.516,-54.87,47.571,-54.795,47.64,-54.785,47.665,-54.891,47.629,-54.946,47.621,-55.035,47.634,-55.075,47.658,-55.197,47.65,-55.366,47.661,-55.391,47.643,-55.413,47.55,-55.435,47.501,-55.461,47.485,-55.499,47.475,-55.576,47.465,-55.775,47.498,-55.811,47.516,-55.862,47.53,-56.081,47.5,-56.127,47.503,-56.084,47.525,-55.867,47.592,-55.844,47.788,-55.858,47.819,-55.918,47.792,-56.02,47.764,-56.09,47.772,-56.121,47.789,-56.151,47.775,-56.221,47.671,-56.263,47.658,-56.326,47.654,-56.46,47.617,-56.722,47.592,-56.774,47.565,-56.952,47.574,-57.473,47.631,-57.66,47.625,-57.884,47.66,-57.926,47.675,-58.239,47.669,-58.333,47.677,-58.327,47.72,-58.337,47.731,-58.428,47.683,-58.509,47.653,-58.613,47.626,-58.941,47.58,-59.117,47.571,-59.219,47.603,-59.26,47.634,-59.321,47.737,-59.362,47.866,-59.362,47.889,-59.341,47.934,-59.272,47.996,-58.961,48.159,-58.711,48.325,-58.605,48.411,-58.503,48.442,-58.336,48.514,-58.33,48.522,-58.492,48.513,-58.606,48.533,-58.723,48.541,-58.944,48.522,-59.167,48.522,-59.168,48.558,-59.063,48.628,-58.842,48.746,-58.819,48.747,-58.887,48.692,-58.906,48.65,-58.877,48.623,-58.843,48.605,-58.716,48.598,-58.687,48.622,-58.642,48.749,-58.546,48.897,-58.494,49.003,-58.404,49.084,-58.359,49.097,-58.319,49.081,-58.186,49.062,-58.05,48.988,-58.006,48.981,-57.991,48.988,-58.041,49.01,-58.082,49.045,-58.099,49.077,-58.049,49.18,-57.991,49.209,-57.98,49.23,-58.097,49.23,-58.191,49.259,-58.219,49.305,-58.213,49.387,-58.183,49.435,-58.107,49.5,-58.016,49.542,-57.961,49.532,-57.856,49.474,-57.791,49.49,-57.799,49.509,-57.897,49.6,-57.929,49.668,-57.926,49.701,-57.712,50.025,-57.608,50.199,-57.466,50.464,-57.433,50.506,-57.36,50.584,-57.331,50.605,-57.237,50.605,-57.18,50.615,-57.264,50.649,-57.294,50.673,-57.298,50.699,-57.275,50.725,-57.242,50.745,-57.132,50.787,-57.053,50.857,-57.006,50.94,-57.013,50.968,-57.037,50.996,-57.036,51.011,-56.976,51.028,-56.825,51.126,-56.805,51.144,-56.75,51.275,-56.682,51.333,-56.619,51.362,-56.518,51.399,-56.207,51.489,-56.026,51.568,-55.902,51.564,-55.866,51.508,-55.69,51.471,-55.66,51.511,-55.701,51.559,-55.666,51.579,-55.522,51.596,-55.496,51.59,-55.453,51.562,-55.459,51.537]],[[-86.589,71.011,-86.55,70.989,-86.321,71.017,-86.127,71.049,-85.825,71.126,-85.644,71.152,-85.095,71.152,-85.002,71.137,-85.043,71.092,-85.066,71.079,-85.048,71.059,-84.988,71.032,-84.87,71.002,-84.824,71.029,-84.79,71.093,-84.709,71.359,-84.674,71.439,-84.658,71.515,-84.66,71.586,-84.699,71.631,-84.84,71.659,-85.032,71.654,-85.131,71.661,-85.25,71.675,-85.339,71.697,-85.397,71.727,-85.512,71.817,-85.596,71.866,-85.813,71.956,-85.912,71.987,-85.862,72.022,-85.665,72.063,-85.546,72.102,-85.406,72.215,-85.322,72.233,-85.019,72.218,-84.608,72.129,-84.352,72.053,-84.284,72.044,-84.282,72.058,-84.347,72.094,-84.643,72.19,-84.778,72.259,-84.842,72.308,-84.811,72.33,-84.645,72.351,-84.623,72.377,-84.849,72.406,-84.964,72.406,-85.057,72.384,-85.156,72.383,-85.341,72.422,-85.391,72.444,-85.498,72.511,-85.554,72.569,-85.616,72.605,-85.638,72.633,-85.65,72.722,-85.645,72.774,-85.619,72.819,-85.575,72.856,-85.455,72.925,-85.388,72.945,-85.262,72.954,-84.99,72.92,-84.257,72.797,-84.274,72.836,-85.094,73.003,-85.384,73.045,-85.455,73.105,-85.018,73.335,-84.616,73.39,-84.416,73.456,-84.089,73.459,-83.782,73.417,-83.777,73.428,-83.915,73.508,-83.904,73.528,-83.73,73.576,-83.41,73.632,-83.02,73.676,-82.943,73.699,-82.843,73.715,-82.66,73.73,-82.203,73.736,-81.946,73.73,-81.605,73.696,-81.406,73.635,-81.344,73.598,-81.238,73.48,-81.152,73.314,-81.025,73.245,-80.822,73.207,-80.681,73.166,-80.603,73.121,-80.583,73.065,-80.619,72.997,-80.592,72.928,-80.501,72.857,-80.431,72.816,-80.277,72.77,-80.275,72.746,-80.323,72.717,-80.424,72.679,-80.675,72.559,-80.999,72.426,-81.229,72.312,-81.241,72.278,-80.761,72.457,-80.611,72.451,-80.605,72.426,-80.702,72.338,-80.821,72.26,-80.941,72.21,-80.919,72.191,-80.691,72.103,-80.733,72.089,-80.843,72.096,-80.888,72.088,-80.921,72.072,-80.941,72.048,-80.943,72.014,-80.925,71.971,-80.927,71.938,-80.948,71.917,-80.925,71.908,-80.858,71.911,-80.802,71.929,-80.705,71.988,-80.386,72.149,-80.182,72.209,-80.116,72.214,-79.928,72.175,-79.884,72.177,-80.091,72.301,-80.109,72.332,-80.067,72.378,-80.042,72.394,-79.927,72.428,-79.831,72.446,-79.778,72.439,-79.693,72.376,-79.654,72.332,-79.584,72.315,-79.427,72.337,-79.323,72.391,-79.194,72.356,-79,72.272,-79.018,72.188,-79.018,72.104,-79.008,72.043,-78.776,71.93,-78.614,71.881,-78.585,71.881,-78.589,71.898,-78.623,71.935,-78.711,71.972,-78.791,72.03,-78.863,72.101,-78.82,72.265,-78.699,72.351,-78.582,72.329,-78.429,72.28,-78.307,72.275,-78.116,72.28,-77.726,72.18,-77.517,72.178,-77.536,72.219,-77.694,72.238,-77.926,72.294,-78.287,72.36,-78.453,72.435,-78.484,72.471,-78.479,72.509,-78.459,72.542,-78.422,72.572,-78.35,72.6,-78.001,72.688,-77.753,72.725,-77.567,72.737,-77.255,72.736,-76.894,72.721,-76.698,72.695,-76.473,72.633,-76.189,72.572,-76.087,72.561,-75.969,72.563,-75.833,72.577,-75.704,72.572,-75.294,72.481,-75.186,72.434,-75.12,72.378,-75.071,72.323,-75.04,72.27,-75.053,72.226,-75.394,72.04,-75.543,72.008,-75.641,71.937,-75.787,71.803,-75.911,71.731,-75.923,71.717,-75.897,71.714,-75.822,71.746,-75.693,71.839,-75.6,71.918,-75.428,71.984,-75.148,72.063,-74.903,72.1,-74.695,72.097,-74.52,72.086,-74.377,72.067,-74.293,72.051,-74.266,72.038,-74.209,71.979,-74.213,71.939,-74.248,71.894,-74.316,71.843,-74.621,71.786,-74.789,71.742,-74.893,71.726,-75.205,71.709,-75.191,71.692,-74.959,71.667,-74.701,71.676,-74.707,71.647,-74.829,71.571,-74.868,71.505,-74.834,71.451,-74.841,71.407,-74.931,71.314,-75.035,71.231,-74.996,71.218,-74.759,71.338,-74.695,71.469,-74.6,71.585,-74.488,71.648,-74.404,71.673,-74.139,71.682,-73.992,71.75,-73.867,71.771,-73.814,71.771,-73.707,71.746,-73.714,71.72,-73.869,71.599,-74.197,71.404,-74.063,71.426,-73.973,71.473,-73.851,71.519,-73.713,71.588,-73.622,71.526,-73.482,71.479,-73.398,71.373,-73.262,71.322,-73.181,71.283,-73.192,71.35,-73.31,71.484,-73.278,71.538,-73.187,71.565,-72.902,71.678,-72.703,71.64,-72.581,71.607,-72.519,71.616,-72.365,71.611,-72.117,71.593,-71.875,71.561,-71.641,71.516,-71.46,71.464,-71.333,71.403,-71.256,71.362,-71.229,71.339,-71.187,71.279,-71.219,71.239,-71.397,71.147,-71.495,71.105,-71.593,71.086,-71.856,71.105,-71.938,71.094,-72.024,71.065,-72.298,70.939,-72.449,70.884,-72.598,70.849,-72.633,70.831,-72.313,70.833,-72.224,70.87,-72.15,70.941,-72.009,71.013,-71.743,71.047,-71.371,70.975,-71.186,70.978,-71.045,71.05,-70.888,71.099,-70.826,71.109,-70.792,71.103,-70.673,71.052,-70.636,71.007,-70.639,70.902,-70.655,70.871,-70.762,70.792,-71.022,70.674,-71.192,70.63,-71.38,70.606,-71.586,70.566,-71.658,70.534,-71.729,70.488,-71.8,70.457,-71.89,70.432,-71.772,70.394,-71.727,70.395,-71.684,70.418,-71.565,70.506,-71.477,70.544,-71.427,70.552,-71.375,70.548,-71.325,70.531,-71.276,70.5,-71.28,70.425,-71.429,70.128,-71.405,70.129,-71.313,70.209,-71.045,70.519,-70.98,70.581,-70.851,70.644,-70.561,70.738,-70.337,70.788,-70.085,70.83,-69.95,70.845,-69.796,70.835,-69.696,70.786,-69.56,70.777,-69.395,70.789,-69.289,70.783,-69.169,70.764,-69.066,70.728,-68.891,70.687,-68.496,70.61,-68.447,70.594,-68.401,70.565,-68.358,70.523,-68.364,70.481,-68.417,70.44,-68.483,70.415,-68.561,70.406,-68.643,70.383,-68.794,70.324,-68.843,70.314,-69.079,70.289,-69.299,70.277,-69.436,70.253,-69.699,70.189,-70.061,70.071,-70.058,70.043,-69.913,70.029,-69.796,70.047,-69.635,70.129,-69.483,70.16,-69.246,70.185,-68.919,70.207,-68.778,70.204,-68.753,70.199,-68.735,70.18,-68.723,70.146,-68.777,70.101,-68.839,70.08,-69.008,69.979,-68.897,69.953,-68.744,69.941,-68.657,69.968,-68.578,70.03,-68.489,70.065,-68.391,70.072,-68.305,70.087,-68.231,70.112,-68.21,70.128,-68.319,70.161,-68.327,70.18,-68.283,70.228,-68.204,70.281,-68.121,70.315,-68.059,70.317,-67.855,70.282,-67.716,70.22,-67.364,70.034,-67.318,69.998,-67.196,69.861,-67.173,69.799,-67.193,69.757,-67.222,69.731,-67.259,69.721,-67.337,69.721,-67.806,69.777,-68.02,69.77,-68.114,69.754,-68.189,69.731,-68.248,69.701,-68.29,69.665,-68.372,69.644,-68.67,69.644,-68.837,69.624,-69.125,69.575,-69.228,69.547,-69.251,69.512,-69.075,69.518,-68.785,69.564,-68.513,69.577,-68.058,69.476,-67.908,69.46,-67.825,69.475,-67.725,69.479,-67.361,69.473,-67.237,69.46,-67.053,69.421,-66.771,69.337,-66.717,69.312,-66.685,69.286,-66.676,69.258,-66.679,69.191,-66.707,69.168,-66.803,69.153,-67.208,69.171,-67.332,69.185,-67.484,69.167,-67.607,69.173,-67.765,69.2,-67.938,69.248,-68.198,69.203,-68.406,69.232,-68.619,69.206,-69.041,69.098,-68.993,69.079,-68.416,69.172,-68.304,69.166,-68.121,69.133,-67.833,69.066,-67.752,69.039,-67.751,68.934,-67.795,68.863,-67.883,68.784,-68.016,68.795,-68.324,68.844,-68.45,68.851,-68.543,68.843,-68.667,68.811,-68.725,68.81,-69.219,68.873,-69.33,68.876,-69.343,68.869,-69.319,68.857,-68.871,68.76,-68.541,68.749,-68.333,68.733,-68.21,68.703,-68.152,68.681,-68.148,68.616,-68.038,68.551,-67.938,68.524,-67.875,68.523,-67.766,68.547,-67.656,68.551,-67.567,68.534,-67.456,68.498,-67.321,68.488,-67.202,68.466,-67.111,68.461,-66.854,68.472,-66.743,68.458,-66.714,68.446,-66.762,68.425,-66.997,68.374,-67.033,68.326,-66.9,68.264,-66.831,68.216,-66.834,68.18,-66.905,68.098,-66.923,68.066,-66.9,68.063,-66.729,68.129,-66.702,68.121,-66.685,68.029,-66.663,68.034,-66.605,68.11,-66.631,68.211,-66.531,68.25,-66.212,68.28,-66.266,68.123,-66.275,68.041,-66.414,67.904,-66.53,67.86,-66.526,67.851,-66.444,67.834,-66.392,67.832,-66.343,67.853,-66.225,67.959,-65.986,68.069,-65.942,68.071,-65.944,68.031,-65.975,67.957,-65.864,67.923,-65.759,67.957,-65.702,67.987,-65.569,67.982,-65.509,67.968,-65.491,67.936,-65.552,67.799,-65.541,67.766,-65.401,67.675,-65.387,67.68,-65.413,67.724,-65.442,67.832,-65.415,67.879,-65.3,67.94,-65.064,68.026,-64.977,68.043,-64.922,68.032,-64.835,67.99,-64.863,67.965,-64.956,67.939,-65.026,67.892,-65.071,67.824,-65.021,67.788,-64.83,67.784,-64.638,67.84,-64.528,67.813,-64.396,67.74,-64.156,67.623,-64.019,67.655,-63.85,67.566,-64.077,67.496,-64.008,67.347,-64.303,67.353,-64.469,67.342,-64.58,67.355,-64.7,67.351,-64.589,67.316,-64.376,67.301,-64.356,67.256,-64.189,67.257,-64.063,67.266,-63.836,67.264,-63.824,67.316,-63.676,67.345,-63.592,67.378,-63.521,67.358,-63.316,67.336,-63.04,67.235,-63.162,67.174,-63.195,67.117,-63.236,67.069,-63.258,67.025,-63.307,66.994,-63.702,66.822,-63.636,66.821,-63.469,66.862,-63.144,66.924,-62.962,66.949,-62.833,66.933,-62.768,66.932,-62.71,66.954,-62.603,66.929,-62.38,66.905,-62.124,67.047,-61.969,67.019,-61.824,66.932,-61.515,66.778,-61.353,66.689,-61.3,66.649,-61.307,66.609,-61.453,66.567,-61.528,66.558,-61.724,66.638,-61.904,66.678,-62.014,66.674,-62.123,66.643,-62.089,66.626,-61.653,66.503,-61.576,66.413,-61.571,66.373,-61.863,66.313,-61.956,66.309,-62.158,66.338,-62.277,66.392,-62.375,66.411,-62.51,66.417,-62.553,66.407,-62.406,66.316,-62.42,66.289,-62.496,66.271,-62.534,66.227,-62.242,66.148,-62.024,66.068,-61.992,66.035,-62.139,66.011,-62.244,66.006,-62.468,66.017,-62.59,66.034,-62.624,66.016,-62.497,65.974,-62.448,65.946,-62.41,65.906,-62.388,65.868,-62.382,65.833,-62.486,65.804,-62.61,65.724,-62.659,65.64,-62.772,65.632,-62.817,65.648,-62.969,65.622,-63.169,65.657,-63.241,65.696,-63.459,65.853,-63.464,65.835,-63.41,65.756,-63.421,65.709,-63.652,65.674,-63.651,65.661,-63.509,65.636,-63.337,65.617,-63.364,65.543,-63.363,65.23,-63.402,65.118,-63.486,65.021,-63.607,64.928,-63.737,64.989,-63.789,65.051,-63.833,65.083,-63.896,65.109,-63.976,65.121,-64.061,65.122,-64.152,65.066,-64.25,65.114,-64.346,65.172,-64.31,65.325,-64.27,65.401,-64.286,65.4,-64.34,65.364,-64.47,65.253,-64.555,65.117,-64.665,65.169,-64.765,65.234,-64.847,65.3,-64.98,65.375,-65.108,65.464,-65.176,65.568,-65.207,65.59,-65.282,65.677,-65.311,65.702,-65.337,65.71,-65.402,65.764,-65.378,65.822,-65.277,65.891,-65.185,65.94,-65.032,65.989,-64.854,66.016,-64.773,66.079,-64.673,66.193,-64.564,66.272,-64.445,66.317,-64.504,66.325,-64.655,66.287,-64.761,66.231,-64.887,66.137,-65.004,66.078,-65.305,66.008,-65.416,65.995,-65.544,65.987,-65.826,65.997,-65.891,66.02,-65.857,66.086,-65.656,66.205,-65.688,66.213,-65.759,66.171,-65.856,66.142,-65.94,66.127,-66.064,66.133,-66.209,66.206,-66.277,66.229,-66.419,66.254,-66.477,66.28,-66.712,66.46,-66.76,66.508,-66.787,66.556,-66.863,66.595,-66.986,66.627,-67.015,66.622,-66.97,66.582,-66.969,66.547,-67.077,66.525,-67.19,66.533,-67.307,66.57,-67.318,66.52,-67.192,66.433,-67.19,66.322,-67.225,66.31,-67.311,66.304,-67.369,66.317,-67.56,66.4,-67.741,66.458,-67.868,66.49,-67.883,66.467,-67.801,66.367,-67.704,66.269,-67.547,66.187,-67.297,66.09,-67.183,66.034,-67.273,65.956,-67.35,65.93,-67.399,65.922,-67.551,65.922,-67.828,65.965,-67.958,66.014,-68.147,66.13,-68.46,66.249,-68.528,66.249,-68.749,66.2,-68.714,66.192,-68.572,66.189,-68.467,66.173,-68.217,66.079,-68.198,66.039,-68.261,65.995,-68.257,65.939,-68.187,65.871,-68.115,65.828,-67.968,65.797,-67.894,65.793,-67.866,65.774,-67.954,65.623,-67.962,65.582,-67.937,65.565,-67.906,65.563,-67.717,65.625,-67.638,65.64,-67.57,65.644,-67.49,65.626,-67.4,65.588,-67.346,65.549,-67.33,65.509,-67.303,65.483,-67.118,65.44,-67.135,65.421,-67.326,65.357,-67.337,65.347,-67.298,65.342,-67.178,65.304,-67.067,65.244,-66.999,65.173,-66.985,65.138,-66.986,65.105,-66.97,65.085,-66.912,65.081,-66.887,65.094,-66.861,65.092,-66.831,65.074,-66.8,65.02,-66.733,64.86,-66.697,64.815,-66.677,64.814,-66.667,64.974,-66.635,65,-66.518,64.972,-66.345,64.91,-66.224,64.854,-66.21,64.828,-66.302,64.778,-66.282,64.755,-66.215,64.722,-66.152,64.735,-66.108,64.791,-66.03,64.847,-65.939,64.886,-65.768,64.854,-65.627,64.771,-65.605,64.742,-65.513,64.706,-65.432,64.726,-65.275,64.632,-65.349,64.589,-65.513,64.526,-65.529,64.505,-65.49,64.51,-65.179,64.51,-65.095,64.485,-65.075,64.437,-65.213,64.303,-65.34,64.315,-65.507,64.318,-65.594,64.311,-65.58,64.294,-65.348,64.232,-65.282,64.182,-65.193,64.13,-65.15,64.087,-65.151,64.068,-65.187,64.038,-65.17,64.028,-65.011,64.009,-64.912,64.026,-64.788,64.033,-64.678,64.028,-64.67,64.01,-64.686,63.961,-64.798,63.916,-64.768,63.905,-64.637,63.918,-64.576,63.897,-64.498,63.79,-64.411,63.706,-64.482,63.687,-64.562,63.68,-64.55,63.573,-64.499,63.463,-64.498,63.358,-64.514,63.264,-64.587,63.243,-64.665,63.245,-64.696,63.269,-64.886,63.549,-64.933,63.599,-64.99,63.643,-65.192,63.764,-65.184,63.745,-65.134,63.689,-65.089,63.606,-65.031,63.44,-65.005,63.333,-65.017,63.293,-65.058,63.283,-65.069,63.263,-65.049,63.235,-64.895,63.126,-64.82,63.06,-64.767,62.992,-64.718,62.946,-64.672,62.922,-64.684,62.902,-64.752,62.887,-64.869,62.88,-64.923,62.889,-65.133,62.952,-65.163,62.933,-65.047,62.701,-65.05,62.646,-65.108,62.626,-65.18,62.649,-65.266,62.715,-65.397,62.788,-65.572,62.869,-65.74,62.932,-65.78,62.93,-65.806,62.912,-65.834,62.909,-65.864,62.921,-65.92,62.969,-65.979,63.001,-66.224,63.107,-66.249,63.108,-66.226,63.076,-66.201,63.006,-66.229,62.991,-66.293,62.997,-66.414,63.027,-66.496,63.097,-66.6,63.219,-66.655,63.265,-66.66,63.235,-66.631,63.119,-66.636,63.08,-66.697,63.07,-66.723,63.08,-66.749,63.111,-66.773,63.162,-66.831,63.201,-66.923,63.228,-66.975,63.256,-67,63.305,-67.018,63.317,-67.18,63.305,-67.261,63.341,-67.495,63.481,-67.709,63.634,-67.844,63.715,-67.893,63.734,-67.821,63.635,-67.743,63.489,-67.723,63.423,-67.759,63.42,-67.838,63.449,-68.244,63.637,-68.494,63.725,-68.633,63.741,-68.859,63.752,-68.911,63.703,-68.789,63.595,-68.671,63.514,-68.555,63.459,-68.374,63.352,-68.208,63.215,-68.141,63.172,-67.915,63.114,-67.797,63.098,-67.676,63.094,-67.665,63.073,-67.724,63.034,-67.737,63.01,-67.468,62.948,-67.367,62.914,-67.269,62.858,-67.213,62.844,-66.98,62.701,-66.922,62.678,-66.714,62.632,-66.645,62.602,-66.531,62.51,-66.459,62.463,-66.357,62.352,-66.281,62.303,-66.095,62.246,-66.016,62.23,-65.98,62.209,-66.004,62.158,-66.027,62.137,-66.133,62.102,-66.116,62.054,-66.056,61.967,-66.059,61.914,-66.124,61.893,-66.257,61.868,-66.324,61.87,-66.425,61.891,-66.551,61.926,-66.803,62.013,-67.181,62.073,-67.322,62.105,-67.369,62.134,-67.44,62.151,-68.379,62.235,-68.536,62.256,-68.634,62.281,-68.724,62.319,-69.082,62.405,-69.126,62.424,-69.366,62.572,-69.545,62.745,-69.605,62.768,-69.8,62.79,-69.962,62.776,-70.071,62.757,-70.236,62.763,-70.344,62.792,-70.571,62.869,-70.801,62.91,-71.002,62.978,-71.106,63.002,-71.096,63.02,-70.946,63.121,-70.993,63.119,-71.254,63.043,-71.347,63.066,-71.501,63.126,-71.617,63.187,-71.855,63.355,-71.992,63.416,-71.973,63.43,-71.819,63.435,-71.697,63.43,-71.614,63.444,-71.456,63.512,-71.387,63.555,-71.381,63.58,-71.513,63.587,-71.542,63.599,-71.566,63.627,-71.627,63.663,-71.725,63.706,-71.838,63.725,-72.223,63.709,-72.29,63.728,-72.289,63.757,-72.213,63.839,-72.172,63.872,-72.159,63.89,-72.174,63.893,-72.226,63.891,-72.45,63.818,-72.498,63.823,-72.586,63.901,-72.639,63.989,-72.678,64.02,-72.73,64.03,-72.913,64.117,-73.174,64.282,-73.27,64.333,-73.377,64.38,-73.455,64.399,-73.444,64.423,-73.278,64.56,-73.271,64.583,-73.413,64.574,-73.627,64.603,-73.728,64.568,-73.793,64.566,-73.868,64.585,-73.91,64.578,-73.95,64.466,-73.981,64.438,-74.026,64.423,-74.065,64.425,-74.099,64.444,-74.098,64.47,-74.13,64.608,-74.205,64.628,-74.416,64.633,-74.461,64.645,-74.512,64.67,-74.556,64.717,-74.593,64.786,-74.634,64.824,-74.681,64.831,-74.719,64.825,-74.748,64.807,-74.813,64.796,-74.916,64.792,-74.919,64.766,-74.823,64.717,-74.73,64.647,-74.64,64.557,-74.695,64.497,-74.894,64.466,-75.067,64.457,-75.215,64.469,-75.328,64.49,-75.488,64.541,-75.715,64.524,-75.767,64.392,-75.815,64.385,-76.032,64.388,-76.118,64.376,-76.407,64.303,-76.495,64.293,-76.562,64.302,-76.627,64.284,-76.724,64.242,-76.856,64.238,-77.024,64.271,-77.166,64.285,-77.283,64.28,-77.403,64.3,-77.527,64.344,-77.628,64.363,-77.76,64.36,-77.791,64.367,-77.985,64.461,-78.045,64.499,-78.175,64.618,-78.198,64.665,-78.201,64.715,-78.19,64.752,-78.145,64.808,-78.096,64.939,-78.055,64.983,-77.995,65.023,-77.876,65.073,-77.447,65.162,-77.361,65.197,-77.364,65.22,-77.461,65.328,-77.46,65.356,-77.428,65.372,-77.358,65.435,-77.327,65.453,-77.251,65.463,-77.094,65.431,-76.959,65.418,-76.779,65.414,-76.482,65.37,-76.067,65.285,-75.828,65.227,-75.648,65.141,-75.52,65.056,-75.502,65.013,-75.561,64.947,-75.591,64.928,-75.589,64.905,-75.556,64.879,-75.452,64.842,-75.427,64.856,-75.435,64.901,-75.414,64.939,-75.363,64.969,-75.357,65.009,-75.397,65.058,-75.446,65.1,-75.505,65.135,-75.773,65.257,-75.799,65.298,-75.709,65.316,-75.317,65.275,-75.166,65.284,-75.109,65.331,-75.048,65.364,-74.982,65.381,-74.85,65.389,-74.665,65.367,-74.575,65.364,-74.495,65.372,-74.391,65.398,-74.237,65.484,-74.138,65.503,-73.99,65.517,-73.878,65.519,-73.675,65.484,-73.551,65.485,-73.561,65.543,-73.643,65.653,-73.746,65.767,-73.826,65.805,-74.033,65.877,-74.276,66.013,-74.401,66.097,-74.434,66.139,-74.416,66.167,-74.375,66.208,-73.934,66.358,-73.584,66.507,-73.431,66.583,-73.357,66.636,-73.281,66.675,-73.201,66.699,-73.033,66.728,-72.985,66.765,-72.975,66.829,-72.947,66.883,-72.789,67.031,-72.668,67.07,-72.485,67.098,-72.365,67.133,-72.22,67.254,-72.234,67.284,-72.301,67.307,-72.353,67.342,-72.576,67.659,-72.725,67.812,-72.904,67.945,-73.063,68.107,-73.328,68.267,-73.331,68.309,-73.284,68.357,-73.307,68.368,-73.58,68.298,-73.644,68.295,-73.749,68.325,-73.821,68.363,-73.879,68.429,-73.873,68.464,-73.834,68.497,-73.783,68.578,-73.781,68.619,-73.798,68.659,-73.822,68.686,-73.852,68.701,-73.935,68.711,-74.073,68.715,-74.118,68.701,-73.966,68.579,-73.989,68.549,-74.183,68.535,-74.27,68.541,-74.35,68.556,-74.422,68.58,-74.648,68.708,-74.696,68.756,-74.681,68.79,-74.7,68.808,-74.746,68.797,-74.808,68.796,-74.893,68.808,-74.91,68.823,-74.752,68.892,-74.743,68.913,-74.816,68.936,-74.925,68.941,-74.954,68.961,-74.917,68.983,-74.769,69.021,-74.717,69.046,-74.805,69.064,-74.855,69.066,-74.954,69.025,-75.104,68.941,-75.213,68.909,-75.363,68.948,-75.457,68.961,-75.523,68.953,-75.623,68.888,-75.842,68.84,-76.235,68.728,-76.403,68.692,-76.585,68.699,-76.619,68.721,-76.616,68.76,-76.604,68.792,-76.582,68.816,-76.575,68.847,-76.588,68.974,-76.557,69.009,-76.495,69.03,-76.381,69.052,-76.089,69.026,-75.954,69.031,-75.859,69.06,-75.763,69.103,-75.668,69.159,-75.648,69.213,-75.749,69.3,-75.787,69.319,-76.046,69.386,-76.19,69.411,-76.316,69.422,-76.408,69.441,-76.465,69.469,-76.52,69.517,-76.525,69.549,-76.516,69.591,-76.463,69.62,-76.231,69.653,-76.234,69.662,-76.424,69.687,-76.513,69.684,-76.59,69.656,-76.687,69.591,-76.742,69.573,-76.916,69.611,-77.02,69.617,-77.09,69.635,-77.129,69.653,-77.105,69.671,-77.019,69.689,-76.869,69.745,-76.859,69.775,-76.962,69.825,-77.016,69.836,-77.232,69.855,-77.494,69.836,-77.592,69.846,-77.635,69.9,-77.663,69.966,-77.675,70.042,-77.722,70.171,-77.774,70.239,-77.843,70.247,-78.157,70.219,-78.231,70.219,-78.283,70.229,-78.491,70.316,-78.575,70.346,-78.621,70.353,-78.773,70.445,-78.831,70.463,-78.9,70.509,-78.98,70.581,-79.066,70.604,-79.16,70.575,-79.253,70.535,-79.347,70.482,-79.397,70.437,-79.405,70.401,-79.347,70.372,-79.018,70.325,-78.934,70.294,-78.863,70.242,-78.81,70.179,-78.775,70.104,-78.778,70.048,-78.819,70.01,-78.89,69.977,-79.093,69.925,-79.303,69.895,-79.515,69.888,-79.616,69.895,-80.162,69.996,-80.26,69.997,-80.387,70.01,-80.67,70.052,-80.826,70.057,-81.098,70.091,-81.56,70.111,-81.652,70.095,-81.529,70.048,-81.422,70.025,-81.329,70.024,-81.197,69.983,-81.024,69.9,-80.925,69.851,-80.843,69.792,-80.84,69.771,-80.922,69.731,-81.565,69.943,-81.958,69.869,-82.139,69.841,-82.294,69.837,-82.488,69.866,-82.925,69.968,-83.091,70.004,-83.15,70.009,-83.531,69.965,-83.859,69.963,-84.522,70.005,-84.765,70.034,-84.829,70.063,-84.909,70.078,-85.053,70.078,-85.432,70.111,-85.78,70.037,-86.198,70.105,-86.322,70.145,-86.361,70.173,-86.483,70.289,-86.5,70.35,-86.465,70.406,-86.431,70.445,-86.397,70.465,-86.624,70.401,-86.704,70.391,-86.809,70.388,-87.122,70.412,-87.172,70.4,-87.156,70.377,-87.074,70.345,-87.063,70.325,-87.238,70.31,-87.502,70.326,-87.618,70.319,-87.67,70.31,-87.789,70.258,-87.838,70.247,-87.901,70.252,-88.178,70.369,-88.402,70.442,-88.663,70.471,-88.783,70.494,-88.848,70.523,-89.208,70.76,-89.258,70.811,-89.372,70.996,-89.41,71.036,-89.456,71.062,-89.366,71.067,-89.025,71.045,-88.696,71.046,-88.517,71.031,-88.309,70.984,-88.039,70.951,-87.845,70.944,-87.534,70.957,-87.182,70.988,-87.14,71.012,-87.369,71.053,-87.572,71.108,-87.76,71.179,-87.872,71.209,-88.061,71.227,-88.59,71.24,-89.079,71.288,-89.418,71.352,-89.693,71.423,-89.805,71.462,-89.846,71.492,-89.889,71.586,-89.934,71.743,-89.977,71.848,-90.02,71.902,-90.025,71.949,-89.931,72.049,-89.664,72.158,-89.657,72.175,-89.711,72.18,-89.823,72.208,-89.859,72.248,-89.873,72.313,-89.874,72.367,-89.862,72.412,-89.817,72.468,-89.702,72.568,-89.536,72.69,-89.358,72.804,-89.327,72.842,-89.311,72.943,-89.288,73.017,-89.263,73.069,-89.225,73.108,-89.115,73.182,-88.977,73.252,-88.761,73.312,-88.743,73.335,-88.74,73.365,-88.727,73.388,-88.705,73.403,-88.17,73.595,-87.926,73.673,-87.72,73.723,-87.472,73.759,-86.769,73.834,-86.406,73.855,-85.951,73.85,-85.11,73.808,-85.009,73.779,-84.984,73.764,-84.947,73.722,-84.975,73.695,-85.204,73.604,-85.494,73.528,-85.682,73.461,-86.001,73.313,-86.086,73.26,-86.481,72.96,-86.575,72.911,-86.629,72.871,-86.668,72.763,-86.656,72.724,-86.595,72.661,-86.38,72.525,-86.323,72.461,-86.324,72.402,-86.348,72.262,-86.351,72.191,-86.341,72.123,-86.297,72.026,-86.218,71.899,-86.036,71.771,-85.75,71.641,-85.537,71.555,-85.327,71.492,-85.079,71.398,-85.023,71.353,-85.138,71.303,-85.405,71.227,-85.757,71.194,-85.945,71.163,-86.179,71.096,-86.473,71.043,-86.589,71.011]],[[-61.801,49.094,-62.22,49.079,-62.553,49.141,-62.8,49.171,-63.042,49.225,-63.566,49.399,-63.626,49.46,-63.676,49.534,-63.777,49.602,-63.885,49.658,-64.44,49.828,-64.485,49.887,-64.373,49.926,-64.244,49.944,-64.131,49.942,-63.76,49.875,-63.292,49.817,-63.089,49.773,-62.859,49.705,-62.633,49.624,-62.133,49.407,-62.043,49.39,-61.817,49.284,-61.736,49.204,-61.696,49.139,-61.746,49.106,-61.801,49.094]],[[-63.811,46.469,-63.784,46.455,-63.737,46.481,-63.681,46.562,-63.534,46.541,-63.456,46.504,-63.413,46.512,-63.369,46.508,-63.286,46.46,-63.129,46.422,-62.964,46.428,-62.712,46.45,-62.682,46.459,-62.423,46.478,-62.164,46.487,-62.074,46.466,-62.041,46.446,-62.024,46.422,-62.172,46.355,-62.32,46.278,-62.526,46.203,-62.552,46.166,-62.539,46.098,-62.543,46.029,-62.503,46.023,-62.478,46,-62.531,45.977,-62.743,45.967,-62.805,45.973,-62.878,46.001,-62.904,46.068,-62.995,46.058,-63.022,46.067,-62.895,46.124,-62.953,46.195,-63.015,46.19,-63.056,46.224,-63.053,46.27,-62.995,46.292,-62.978,46.316,-63.057,46.295,-63.117,46.253,-63.195,46.237,-63.271,46.2,-63.153,46.188,-63.213,46.16,-63.277,46.153,-63.569,46.209,-63.641,46.23,-63.732,46.289,-63.801,46.367,-63.763,46.37,-63.751,46.384,-63.759,46.398,-63.861,46.408,-64.02,46.405,-64.111,46.425,-64.107,46.562,-64.136,46.6,-64.236,46.631,-64.388,46.641,-64.403,46.692,-64.355,46.769,-64.28,46.836,-64.223,46.901,-64.157,46.955,-63.994,47.062,-63.997,46.982,-63.981,46.913,-64.088,46.775,-63.903,46.639,-63.879,46.609,-63.864,46.572,-63.876,46.539,-63.906,46.509,-63.834,46.494,-63.811,46.469]],[[-82,62.954,-81.961,62.926,-81.949,62.884,-81.964,62.828,-81.99,62.776,-82.026,62.73,-82.114,62.652,-82.388,62.519,-82.491,62.447,-82.568,62.403,-83.016,62.21,-83.071,62.2,-83.13,62.204,-83.252,62.233,-83.377,62.238,-83.699,62.16,-83.714,62.174,-83.729,62.257,-83.761,62.304,-83.903,62.402,-83.912,62.426,-83.91,62.454,-83.899,62.476,-83.739,62.569,-83.376,62.905,-83.289,62.922,-83.111,62.884,-83.026,62.872,-82.966,62.874,-82.706,62.945,-82.46,62.936,-82.235,62.977,-82.129,62.978,-82.048,62.971,-82,62.954]],[[-79.545,62.412,-79.466,62.385,-79.336,62.294,-79.286,62.248,-79.272,62.186,-79.306,62.104,-79.324,62.026,-79.372,61.968,-79.462,61.894,-79.542,61.808,-79.611,61.71,-79.669,61.644,-79.714,61.613,-79.763,61.596,-79.816,61.595,-79.896,61.63,-80.004,61.703,-80.092,61.747,-80.205,61.777,-80.265,61.818,-80.276,61.859,-80.28,61.99,-80.275,62.055,-80.26,62.109,-80.235,62.153,-80.179,62.213,-80.022,62.343,-79.927,62.393,-79.868,62.404,-79.713,62.395,-79.65,62.398,-79.598,62.413,-79.545,62.412]],[[-75.676,68.323,-75.154,68.234,-75.103,68.202,-75.078,68.173,-75.063,68.141,-75.062,68.075,-75.073,68.049,-75.124,67.985,-75.127,67.965,-75.086,67.751,-75.091,67.635,-75.127,67.537,-75.202,67.459,-75.315,67.4,-75.4,67.367,-75.78,67.284,-76.049,67.262,-76.333,67.258,-76.694,67.236,-76.859,67.24,-76.944,67.25,-77.005,67.267,-77.076,67.32,-77.157,67.408,-77.224,67.508,-77.304,67.685,-77.306,67.706,-77.229,67.85,-77.126,67.947,-76.945,68.091,-76.74,68.231,-76.688,68.254,-76.596,68.279,-76.364,68.319,-76.173,68.309,-76.088,68.314,-75.983,68.332,-75.867,68.337,-75.676,68.323]],[[-79.537,73.654,-79.367,73.641,-78.287,73.666,-78.063,73.648,-77.382,73.537,-77.207,73.5,-77.12,73.45,-77.042,73.373,-77.005,73.356,-76.759,73.31,-76.657,73.254,-76.622,73.225,-76.57,73.159,-76.458,73.122,-76.331,73.1,-76.29,73.081,-76.31,72.998,-76.255,72.959,-76.135,72.912,-76.09,72.881,-76.183,72.843,-76.401,72.821,-77.014,72.844,-77.836,72.897,-78.314,72.882,-78.554,72.858,-79.134,72.772,-79.319,72.758,-79.501,72.756,-79.821,72.826,-79.937,72.864,-79.975,72.892,-80.052,72.977,-80.114,73.078,-80.146,73.161,-80.183,73.225,-80.293,73.246,-80.618,73.271,-80.727,73.305,-80.776,73.334,-80.824,73.381,-80.823,73.429,-80.798,73.472,-80.777,73.482,-80.736,73.483,-80.827,73.535,-80.858,73.591,-80.861,73.671,-80.849,73.721,-80.823,73.743,-80.763,73.758,-80.621,73.767,-80.412,73.765,-80.12,73.707,-79.889,73.702,-79.537,73.654]],[[-80.732,52.747,-80.802,52.734,-81.01,52.761,-81.097,52.78,-81.352,52.852,-81.839,52.958,-82.005,53.01,-82.039,53.05,-81.951,53.132,-81.901,53.166,-81.847,53.186,-81.335,53.224,-81.136,53.206,-80.9,53.037,-80.765,52.923,-80.71,52.832,-80.71,52.787,-80.732,52.747]],[[-78.827,56.145,-78.877,56.131,-78.914,56.133,-78.907,56.166,-78.857,56.232,-78.828,56.29,-78.822,56.34,-78.799,56.383,-78.762,56.421,-78.725,56.439,-78.669,56.439,-78.657,56.317,-78.673,56.26,-78.71,56.213,-78.761,56.175,-78.827,56.145]],[[-78.936,56.266,-79.018,56.165,-79.084,56.068,-79.175,55.885,-79.228,55.879,-79.274,55.922,-79.142,56.136,-79.136,56.16,-79.142,56.181,-79.182,56.212,-79.222,56.176,-79.407,55.935,-79.455,55.896,-79.495,55.875,-79.527,55.871,-79.606,55.876,-79.765,55.807,-79.497,56.093,-79.495,56.115,-79.545,56.128,-79.565,56.121,-79.781,55.941,-79.905,55.871,-79.987,55.892,-80.008,55.911,-80.001,55.932,-79.79,56.114,-79.596,56.244,-79.515,56.327,-79.482,56.404,-79.468,56.46,-79.469,56.523,-79.459,56.54,-79.448,56.537,-79.435,56.513,-79.432,56.447,-79.476,56.313,-79.512,56.247,-79.554,56.192,-79.536,56.18,-79.458,56.211,-79.393,56.276,-79.339,56.376,-79.305,56.463,-79.272,56.6,-79.261,56.596,-79.246,56.568,-79.21,56.549,-79.155,56.538,-79.124,56.52,-79.1,56.474,-79.078,56.454,-78.995,56.436,-78.963,56.422,-78.94,56.371,-78.942,56.345,-78.931,56.328,-78.907,56.32,-78.936,56.266]],[[-79.978,56.207,-80.029,56.199,-80.089,56.214,-80.057,56.287,-80.005,56.318,-79.874,56.348,-79.852,56.367,-79.81,56.377,-79.749,56.377,-79.681,56.404,-79.606,56.459,-79.58,56.466,-79.633,56.387,-79.688,56.327,-79.978,56.207]],[[-89.833,77.268,-90.095,77.21,-90.228,77.212,-90.993,77.329,-91.147,77.387,-91.177,77.426,-91.185,77.482,-91.183,77.557,-91.149,77.608,-91.109,77.626,-91.019,77.644,-90.843,77.655,-90.675,77.649,-90.423,77.628,-90.172,77.595,-89.839,77.491,-89.719,77.442,-89.694,77.378,-89.695,77.339,-89.712,77.31,-89.747,77.293,-89.833,77.268]],[[-104.558,77.142,-104.711,77.124,-105.016,77.165,-105.215,77.182,-105.38,77.254,-105.556,77.353,-105.695,77.461,-105.747,77.525,-105.848,77.563,-105.883,77.627,-106.066,77.725,-106.036,77.74,-105.863,77.754,-105.588,77.736,-105.456,77.701,-105.29,77.642,-105.074,77.548,-105.007,77.507,-104.994,77.45,-104.955,77.419,-104.77,77.413,-104.542,77.338,-104.501,77.309,-104.454,77.249,-104.457,77.221,-104.493,77.162,-104.558,77.142]],[[-98.792,79.981,-98.769,79.851,-98.79,79.785,-98.841,79.737,-98.885,79.726,-98.945,79.724,-99.218,79.762,-99.302,79.784,-99.306,79.803,-99.333,79.84,-99.516,79.887,-99.857,79.879,-100,79.884,-100.057,79.898,-100.092,79.919,-100.126,80.001,-100.12,80.03,-100.079,80.081,-100.053,80.093,-99.803,80.14,-99.731,80.144,-99.425,80.126,-99.153,80.124,-99.017,80.111,-98.895,80.082,-98.823,80.037,-98.792,79.981]],[[-105.289,72.92,-105.339,72.915,-105.434,72.938,-105.573,72.989,-105.8,73.093,-106.071,73.196,-106.113,73.258,-106.18,73.304,-106.526,73.413,-106.75,73.458,-106.922,73.48,-106.95,73.51,-106.831,73.599,-106.695,73.67,-106.614,73.696,-106.362,73.719,-105.512,73.766,-105.318,73.767,-105.114,73.744,-104.835,73.647,-104.718,73.636,-104.649,73.614,-104.587,73.578,-104.555,73.541,-104.552,73.466,-104.583,73.354,-104.622,73.311,-104.791,73.168,-104.969,73.089,-105.003,73.038,-105.075,72.997,-105.201,72.947,-105.289,72.92]],[[-102.227,76.015,-102.018,75.954,-102.008,75.939,-102.047,75.928,-102.318,75.895,-102.423,75.869,-102.511,75.808,-102.58,75.78,-102.944,75.763,-103.315,75.764,-103.245,75.823,-103.042,75.919,-103.202,75.958,-103.77,75.892,-103.985,75.933,-103.801,76.037,-103.985,76.047,-104.242,76.047,-104.406,76.108,-104.351,76.182,-104.012,76.223,-103.571,76.258,-103.098,76.311,-102.728,76.307,-102.584,76.282,-102.536,76.196,-102.49,76.095,-102.426,76.086,-102.227,76.015]],[[-104.023,76.583,-103.973,76.578,-103.821,76.598,-103.723,76.601,-103.613,76.563,-103.585,76.539,-103.19,76.477,-103.051,76.45,-103.034,76.431,-103.083,76.405,-103.2,76.371,-103.311,76.348,-103.472,76.329,-104.271,76.326,-104.358,76.335,-104.408,76.365,-104.506,76.479,-104.577,76.54,-104.603,76.583,-104.586,76.606,-104.5,76.63,-104.205,76.666,-104.075,76.666,-103.992,76.657,-103.959,76.639,-103.969,76.614,-104.023,76.583]],[[-118.328,75.58,-118.614,75.515,-118.817,75.522,-119.087,75.569,-119.306,75.585,-119.383,75.601,-119.395,75.617,-119.32,75.663,-119.227,75.699,-119.003,75.77,-118.626,75.906,-118.379,75.958,-118.137,75.994,-117.889,76.076,-117.752,76.112,-117.634,76.115,-117.513,76.099,-117.499,76.077,-117.626,75.966,-117.716,75.921,-117.891,75.805,-118.227,75.611,-118.328,75.58]],[[-113.832,77.755,-114.106,77.721,-114.287,77.721,-114.608,77.769,-114.98,77.915,-115.029,77.968,-114.89,77.977,-114.79,77.993,-114.726,78.016,-114.607,78.04,-114.33,78.078,-114.297,78.063,-114.303,78.033,-114.28,78.004,-114.181,77.998,-114.087,77.978,-113.898,77.916,-113.768,77.904,-113.721,77.89,-113.697,77.869,-113.618,77.832,-113.619,77.813,-113.726,77.776,-113.832,77.755]],[[-130.927,54.479,-130.95,54.478,-130.959,54.499,-130.953,54.542,-130.922,54.615,-130.907,54.632,-130.777,54.619,-130.758,54.614,-130.753,54.6,-130.763,54.577,-130.805,54.544,-130.927,54.479]],[[-131.029,51.962,-131.047,51.96,-131.081,51.98,-131.103,52.014,-131.117,52.101,-131.107,52.137,-131.098,52.151,-131.011,52.095,-131.029,51.962]],[[-130.236,53.959,-130.267,53.923,-130.338,53.866,-130.384,53.844,-130.407,53.856,-130.47,53.862,-130.537,53.918,-130.59,53.94,-130.625,53.941,-130.642,53.921,-130.646,53.894,-130.638,53.86,-130.644,53.845,-130.664,53.848,-130.683,53.863,-130.703,53.892,-130.707,53.921,-130.696,53.951,-130.647,53.991,-130.495,54.074,-130.448,54.089,-130.397,54.086,-130.316,54.047,-130.298,54.036,-130.236,53.959]],[[-129.849,53.168,-129.869,53.165,-129.934,53.177,-130.151,53.346,-130.306,53.407,-130.411,53.491,-130.518,53.544,-130.452,53.631,-130.395,53.62,-130.195,53.55,-130.035,53.481,-129.945,53.436,-129.755,53.245,-129.769,53.217,-129.849,53.168]],[[-130.575,54.77,-130.493,54.834,-130.313,54.946,-130.214,55.026,-130.204,54.947,-130.349,54.815,-130.535,54.749,-130.575,54.77]],[[-128.552,52.94,-128.507,52.621,-128.51,52.519,-128.577,52.452,-128.624,52.34,-128.679,52.29,-128.731,52.357,-128.736,52.468,-128.749,52.556,-128.766,52.598,-128.746,52.763,-128.77,52.751,-128.831,52.679,-128.9,52.674,-129.023,52.756,-129.085,52.822,-129.095,52.892,-129.176,52.965,-129.184,52.991,-129.178,53.018,-129.111,53.091,-129.084,53.14,-129.06,53.241,-129.033,53.28,-128.97,53.274,-128.858,53.229,-128.74,53.179,-128.633,53.113,-128.552,52.94]],[[-126.092,49.354,-126.064,49.264,-126.187,49.278,-126.23,49.296,-126.231,49.339,-126.209,49.38,-126.115,49.365,-126.092,49.354]],[[-124.154,49.531,-124.14,49.51,-124.362,49.588,-124.457,49.634,-124.494,49.667,-124.518,49.686,-124.631,49.736,-124.65,49.758,-124.623,49.775,-124.547,49.765,-124.421,49.728,-124.309,49.667,-124.154,49.531]],[[-126.641,49.606,-126.68,49.601,-126.743,49.613,-126.814,49.642,-126.939,49.718,-126.951,49.736,-126.94,49.75,-126.905,49.763,-126.897,49.783,-126.926,49.838,-126.826,49.872,-126.738,49.844,-126.698,49.808,-126.65,49.746,-126.628,49.675,-126.626,49.627,-126.641,49.606]],[[-125.184,50.097,-125.195,50.044,-125.26,50.13,-125.358,50.312,-125.345,50.354,-125.301,50.414,-125.261,50.418,-125.196,50.39,-125.14,50.34,-125.126,50.32,-125.091,50.268,-125.074,50.221,-125.113,50.163,-125.184,50.097]],[[-128.937,52.51,-128.969,52.464,-129.102,52.574,-129.151,52.605,-129.25,52.722,-129.268,52.772,-129.264,52.801,-129.246,52.811,-129.215,52.804,-129.186,52.791,-128.994,52.662,-128.94,52.601,-128.937,52.51]],[[-127.925,51.474,-127.941,51.457,-127.981,51.457,-128.045,51.474,-128.092,51.511,-128.149,51.627,-128.142,51.647,-128.123,51.667,-128.032,51.708,-127.999,51.704,-127.987,51.674,-127.933,51.605,-127.916,51.585,-127.916,51.506,-127.925,51.474]],[[-128.369,52.401,-128.445,52.388,-128.42,52.441,-128.412,52.473,-128.426,52.503,-128.436,52.56,-128.44,52.696,-128.365,52.782,-128.247,52.784,-128.248,52.741,-128.298,52.548,-128.324,52.459,-128.344,52.426,-128.369,52.401]],[[-129.314,52.992,-129.329,52.984,-129.37,52.998,-129.41,53.024,-129.478,53.098,-129.5,53.129,-129.515,53.179,-129.501,53.188,-129.471,53.183,-129.451,53.175,-129.344,53.053,-129.314,52.992]],[[-129.168,53.118,-129.173,53.111,-129.277,53.111,-129.306,53.121,-129.324,53.142,-129.331,53.174,-129.314,53.212,-129.253,53.285,-129.251,53.317,-129.238,53.33,-129.195,53.293,-129.177,53.259,-129.168,53.118]],[[-123.435,48.754,-123.477,48.729,-123.5,48.732,-123.518,48.75,-123.582,48.926,-123.555,48.922,-123.468,48.867,-123.488,48.846,-123.423,48.793,-123.407,48.756,-123.435,48.754]],[[-59.788,43.94,-59.922,43.904,-60.038,43.907,-60.114,43.939,-60.117,43.953,-59.936,43.94,-59.866,43.947,-59.727,44.003,-59.788,43.94]],[[-60.962,45.49,-61.003,45.482,-61.012,45.496,-61.076,45.537,-61.082,45.558,-61.026,45.577,-60.912,45.567,-60.953,45.516,-60.962,45.49]],[[-61.914,47.285,-61.879,47.266,-61.815,47.268,-61.773,47.26,-61.834,47.223,-61.951,47.219,-62.008,47.234,-61.925,47.425,-61.827,47.469,-61.628,47.594,-61.548,47.632,-61.474,47.647,-61.396,47.638,-61.476,47.564,-61.582,47.56,-61.684,47.499,-61.751,47.431,-61.831,47.392,-61.887,47.345,-61.914,47.285]],[[-64.509,47.887,-64.534,47.814,-64.621,47.752,-64.665,47.748,-64.685,47.754,-64.66,47.794,-64.663,47.863,-64.591,47.872,-64.565,47.866,-64.509,47.887]],[[-64.476,47.959,-64.591,47.907,-64.541,47.985,-64.52,48.005,-64.5,48.014,-64.481,48.007,-64.476,47.959]],[[-66.274,44.292,-66.324,44.257,-66.312,44.292,-66.25,44.379,-66.21,44.392,-66.274,44.292]],[[-66.762,44.682,-66.897,44.629,-66.845,44.764,-66.802,44.805,-66.745,44.791,-66.753,44.71,-66.762,44.682]],[[-73.567,45.469,-73.644,45.449,-73.775,45.468,-73.92,45.442,-73.961,45.441,-73.853,45.516,-73.687,45.561,-73.522,45.701,-73.476,45.705,-73.539,45.546,-73.552,45.49,-73.567,45.469]],[[-73.695,45.585,-73.816,45.565,-73.858,45.574,-73.725,45.672,-73.572,45.694,-73.695,45.585]],[[-71.026,46.873,-71.117,46.865,-71.095,46.9,-70.971,46.961,-70.88,46.996,-70.826,46.995,-70.913,46.92,-71.026,46.873]],[[-55.536,50.72,-55.57,50.709,-55.601,50.709,-55.629,50.721,-55.634,50.74,-55.604,50.781,-55.527,50.801,-55.469,50.796,-55.473,50.776,-55.504,50.742,-55.536,50.72]],[[-54.554,49.589,-54.709,49.531,-54.744,49.508,-54.787,49.496,-54.819,49.514,-54.864,49.576,-54.855,49.597,-54.813,49.599,-54.789,49.591,-54.783,49.572,-54.764,49.562,-54.733,49.562,-54.619,49.622,-54.559,49.631,-54.538,49.62,-54.554,49.589]],[[-55.361,51.89,-55.409,51.889,-55.42,51.9,-55.4,51.938,-55.346,51.983,-55.274,51.995,-55.294,51.93,-55.361,51.89]],[[-54.094,49.744,-54.02,49.679,-53.981,49.662,-54.238,49.592,-54.269,49.587,-54.286,49.595,-54.289,49.661,-54.278,49.711,-54.259,49.719,-54.199,49.689,-54.138,49.751,-54.094,49.744]],[[-54.227,47.441,-54.276,47.407,-54.326,47.408,-54.32,47.439,-54.259,47.498,-54.227,47.54,-54.226,47.566,-54.215,47.585,-54.168,47.607,-54.128,47.647,-54.148,47.573,-54.227,47.441]],[[-64.824,62.559,-64.632,62.548,-64.515,62.552,-64.465,62.536,-64.418,62.487,-64.478,62.418,-64.546,62.391,-64.657,62.384,-64.837,62.406,-64.901,62.421,-64.956,62.458,-64.931,62.485,-64.842,62.494,-64.827,62.505,-64.85,62.525,-64.849,62.543,-64.824,62.559]],[[-68.234,60.241,-68.324,60.233,-68.365,60.254,-68.368,60.315,-68.338,60.361,-68.235,60.456,-68.142,60.562,-68.088,60.588,-67.978,60.57,-67.914,60.54,-67.848,60.489,-67.819,60.45,-67.844,60.392,-67.922,60.34,-68.012,60.305,-68.234,60.241]],[[-70.337,62.549,-70.406,62.545,-70.542,62.552,-70.687,62.573,-70.766,62.597,-70.838,62.648,-70.851,62.704,-70.986,62.788,-71.137,62.816,-71.22,62.874,-71.135,62.878,-71.014,62.865,-70.835,62.84,-70.674,62.807,-70.443,62.734,-70.367,62.666,-70.292,62.616,-70.269,62.578,-70.289,62.562,-70.337,62.549]],[[-64.833,61.366,-64.857,61.354,-64.88,61.357,-64.954,61.41,-65.054,61.432,-65.092,61.453,-65.394,61.563,-65.427,61.611,-65.432,61.65,-65.332,61.668,-65.13,61.686,-64.954,61.685,-64.79,61.662,-64.756,61.638,-64.67,61.593,-64.691,61.539,-64.696,61.471,-64.732,61.438,-64.788,61.413,-64.833,61.366]],[[-65.031,61.879,-65.008,61.87,-64.981,61.881,-64.961,61.872,-64.947,61.843,-64.924,61.824,-64.865,61.798,-64.846,61.78,-64.847,61.762,-64.897,61.733,-64.928,61.733,-65.166,61.798,-65.23,61.864,-65.235,61.898,-65.211,61.928,-65.174,61.943,-65.126,61.942,-65.068,61.926,-65.031,61.879]],[[-69.16,59.04,-69.221,58.968,-69.302,58.977,-69.331,58.962,-69.353,58.961,-69.316,59.029,-69.312,59.075,-69.33,59.121,-69.303,59.145,-69.195,59.146,-69.194,59.093,-69.181,59.073,-69.155,59.064,-69.16,59.04]],[[-64.407,60.367,-64.442,60.298,-64.558,60.323,-64.738,60.376,-64.809,60.41,-64.834,60.448,-64.836,60.501,-64.783,60.51,-64.646,60.515,-64.533,60.441,-64.5,60.43,-64.407,60.367]],[[-60.994,56.039,-60.983,56.015,-61.137,56.033,-61.191,56.048,-61.196,56.064,-61.188,56.089,-61.158,56.118,-61.087,56.141,-61.049,56.129,-60.966,56.099,-60.955,56.08,-60.994,56.039]],[[-61.744,57.555,-61.66,57.525,-61.637,57.416,-61.795,57.422,-61.975,57.495,-62.011,57.548,-62.007,57.558,-61.983,57.567,-61.937,57.554,-61.893,57.573,-61.848,57.579,-61.744,57.555]],[[-67.915,69.541,-67.94,69.535,-68.202,69.58,-68.221,69.617,-68.093,69.657,-67.989,69.679,-67.909,69.682,-67.829,69.675,-67.755,69.631,-67.845,69.592,-67.915,69.541]],[[-62.682,67.056,-62.805,67.029,-62.872,67.063,-62.825,67.072,-62.757,67.113,-62.664,67.148,-62.625,67.177,-62.47,67.19,-62.417,67.188,-62.396,67.178,-62.485,67.134,-62.682,67.056]],[[-79.063,75.926,-79.052,75.867,-79.124,75.87,-79.356,75.831,-79.545,75.826,-79.639,75.843,-79.699,75.883,-79.551,75.958,-79.382,76.011,-79.178,76.092,-79.009,76.146,-78.926,76.135,-78.845,76.106,-78.946,76.025,-79.057,75.985,-79.063,75.926]],[[-79.867,56.775,-79.894,56.757,-79.944,56.777,-79.946,56.827,-79.898,56.865,-79.861,56.864,-79.827,56.843,-79.835,56.816,-79.867,56.775]],[[-79.717,57.516,-79.732,57.508,-79.775,57.514,-79.792,57.449,-79.808,57.442,-79.838,57.483,-79.816,57.518,-79.819,57.542,-79.811,57.559,-79.768,57.599,-79.743,57.608,-79.727,57.605,-79.713,57.555,-79.717,57.516]],[[-79.518,56.657,-79.553,56.644,-79.577,56.645,-79.551,56.733,-79.582,56.765,-79.584,56.781,-79.57,56.796,-79.553,56.799,-79.511,56.771,-79.491,56.743,-79.482,56.714,-79.485,56.687,-79.497,56.667,-79.518,56.657]],[[-79.938,53.304,-79.939,53.274,-80.004,53.28,-80.039,53.297,-80.068,53.324,-80.074,53.344,-80.05,53.364,-79.975,53.352,-79.938,53.304]],[[-79.384,51.952,-79.426,51.945,-79.521,51.953,-79.597,51.978,-79.644,52.01,-79.335,52.098,-79.271,52.087,-79.27,52.071,-79.317,52.024,-79.329,51.992,-79.352,51.968,-79.384,51.952]],[[-123.372,48.886,-123.385,48.875,-123.541,48.946,-123.646,49.039,-123.689,49.095,-123.482,48.955,-123.378,48.908,-123.372,48.886]],[[-124.978,50.03,-125.002,50.021,-125.026,50.134,-124.996,50.175,-124.987,50.196,-124.991,50.217,-124.938,50.166,-124.916,50.132,-124.907,50.084,-124.908,50.071,-124.978,50.03]],[[-73.622,67.784,-74.109,67.783,-74.374,67.79,-74.481,67.805,-74.573,67.829,-74.679,67.906,-74.746,67.985,-74.749,68.018,-74.731,68.049,-74.707,68.067,-74.379,68.093,-74.111,68.061,-73.881,68.022,-73.584,68.015,-73.494,68.001,-73.459,67.99,-73.435,67.97,-73.402,67.879,-73.398,67.83,-73.407,67.793,-73.622,67.784]],[[-77.877,63.471,-77.792,63.428,-77.704,63.431,-77.655,63.396,-77.538,63.287,-77.527,63.269,-77.533,63.234,-77.594,63.188,-77.658,63.165,-77.791,63.13,-77.942,63.114,-78.024,63.139,-78.256,63.24,-78.469,63.358,-78.537,63.424,-78.507,63.451,-78.417,63.47,-78.235,63.49,-77.934,63.479,-77.877,63.471]],[[-94.527,75.749,-94.624,75.749,-94.751,75.77,-94.787,75.791,-94.815,75.821,-94.834,75.859,-94.86,75.889,-94.894,75.912,-94.901,75.931,-94.881,75.946,-94.84,75.954,-94.745,75.957,-94.538,75.996,-94.499,75.992,-94.471,75.971,-94.443,75.917,-94.414,75.885,-94.332,75.826,-94.296,75.788,-94.304,75.776,-94.33,75.766,-94.527,75.749]],[[-80.285,59.624,-80.317,59.621,-80.325,59.633,-80.299,59.674,-80.257,59.679,-80.21,59.725,-80.167,59.709,-80.183,59.683,-80.241,59.645,-80.285,59.624]],[[-80.064,59.771,-80.167,59.764,-80.122,59.823,-80.084,59.852,-80.041,59.87,-79.956,59.877,-79.899,59.853,-79.95,59.81,-80.064,59.771]],[[-96.782,72.937,-96.944,72.927,-97.093,72.997,-97.098,73.062,-97.088,73.098,-97.069,73.13,-97.015,73.157,-96.862,73.189,-96.793,73.165,-96.768,73.137,-96.744,73.126,-96.646,73.102,-96.598,73.074,-96.604,73.042,-96.635,72.992,-96.671,72.961,-96.709,72.947,-96.782,72.937]],[[-97.356,74.526,-97.656,74.466,-97.722,74.489,-97.75,74.511,-97.516,74.602,-97.417,74.627,-97.318,74.598,-97.291,74.576,-97.304,74.56,-97.356,74.526]],[[-98.27,73.869,-98.558,73.847,-98.691,73.856,-98.761,73.829,-98.817,73.817,-98.974,73.812,-99.298,73.862,-99.385,73.879,-99.417,73.895,-99.404,73.911,-99.346,73.926,-99.097,73.948,-99.005,73.965,-98.967,73.988,-98.904,74.007,-98.818,74.021,-98.585,74.035,-98.061,74.105,-97.8,74.115,-97.698,74.109,-97.667,74.09,-97.659,74.072,-97.673,74.053,-97.755,74.006,-97.861,73.968,-98.147,73.889,-98.27,73.869]],[[-90.2,69.419,-90.177,69.357,-90.267,69.273,-90.295,69.258,-90.33,69.252,-90.364,69.263,-90.465,69.329,-90.492,69.37,-90.455,69.39,-90.377,69.416,-90.322,69.429,-90.253,69.418,-90.229,69.436,-90.2,69.419]],[[-90.493,69.221,-90.574,69.209,-90.626,69.251,-90.667,69.259,-90.686,69.287,-90.772,69.293,-90.766,69.336,-90.742,69.357,-90.663,69.374,-90.6,69.368,-90.54,69.325,-90.511,69.29,-90.485,69.247,-90.493,69.221]],[[-74,62.618,-74.054,62.61,-74.254,62.622,-74.5,62.669,-74.626,62.713,-74.62,62.726,-74.564,62.733,-74.501,62.727,-74.395,62.696,-74.109,62.68,-74.017,62.663,-73.988,62.636,-74,62.618]],[[-74.881,68.349,-74.959,68.342,-75.073,68.404,-75.31,68.474,-75.4,68.525,-75.403,68.55,-75.396,68.589,-75.37,68.636,-75.287,68.688,-75.2,68.696,-75.075,68.685,-74.984,68.648,-74.885,68.545,-74.819,68.494,-74.798,68.458,-74.831,68.441,-74.828,68.424,-74.813,68.413,-74.819,68.394,-74.845,68.366,-74.881,68.349]],[[-78.532,60.729,-78.669,60.717,-78.669,60.731,-78.612,60.772,-78.4,60.808,-78.242,60.819,-78.279,60.784,-78.372,60.756,-78.532,60.729]],[[-78.983,68.193,-79.064,68.182,-79.174,68.235,-79.175,68.264,-79.153,68.335,-78.953,68.353,-78.869,68.31,-78.829,68.268,-78.983,68.193]],[[-76.678,63.394,-76.783,63.384,-76.922,63.406,-77.057,63.45,-77.365,63.588,-77.134,63.682,-76.764,63.574,-76.652,63.504,-76.678,63.394]],[[-79.431,69.788,-79.39,69.73,-79.365,69.712,-79.402,69.685,-79.553,69.631,-79.882,69.609,-80.048,69.634,-79.971,69.556,-79.954,69.523,-79.978,69.51,-80.047,69.514,-80.161,69.536,-80.227,69.562,-80.244,69.593,-80.269,69.6,-80.3,69.583,-80.33,69.587,-80.398,69.633,-80.448,69.65,-80.778,69.677,-80.795,69.689,-80.778,69.71,-80.727,69.74,-80.653,69.751,-80.466,69.737,-80.451,69.745,-80.438,69.783,-80.424,69.798,-80.295,69.794,-80.214,69.802,-80.169,69.782,-80.125,69.737,-80.062,69.746,-79.971,69.739,-79.87,69.756,-79.715,69.796,-79.594,69.81,-79.431,69.788]],[[-78.029,69.715,-77.978,69.665,-77.969,69.639,-78.04,69.608,-78.307,69.552,-78.47,69.503,-78.552,69.492,-78.662,69.503,-78.795,69.48,-78.848,69.483,-78.789,69.523,-78.579,69.639,-78.402,69.651,-78.344,69.675,-78.296,69.667,-78.267,69.687,-78.262,69.717,-78.201,69.74,-78.145,69.739,-78.029,69.715]],[[-77.642,63.992,-77.714,63.946,-77.929,63.962,-77.958,63.976,-77.966,63.993,-77.931,64.015,-77.711,64.036,-77.617,64.037,-77.569,64.03,-77.564,64.022,-77.642,63.992]],[[-83.123,66.283,-83.024,66.271,-82.948,66.272,-82.931,66.257,-83.011,66.208,-83.06,66.199,-83.148,66.234,-83.214,66.277,-83.233,66.303,-83.238,66.332,-83.222,66.336,-83.123,66.283]],[[-79.211,68.845,-79.28,68.839,-79.361,68.858,-79.39,68.89,-79.406,68.923,-79.391,68.94,-79.355,68.956,-79.305,68.992,-79.243,69.049,-79.145,69.087,-78.93,69.123,-78.9,69.135,-78.804,69.235,-78.772,69.252,-78.662,69.262,-78.65,69.275,-78.689,69.3,-78.689,69.325,-78.65,69.351,-78.597,69.371,-78.458,69.39,-78.333,69.386,-78.3,69.379,-78.272,69.361,-78.234,69.315,-78.229,69.304,-78.287,69.263,-78.439,69.199,-78.533,69.146,-78.552,69.129,-78.56,69.106,-78.596,69.079,-78.705,69.014,-78.779,68.95,-78.853,68.916,-79.054,68.883,-79.211,68.845]],[[-76.995,69.144,-77.122,69.132,-77.215,69.138,-77.276,69.162,-77.322,69.194,-77.379,69.274,-77.358,69.312,-77.352,69.379,-77.341,69.404,-77.319,69.416,-77.188,69.44,-77.109,69.437,-76.994,69.412,-76.746,69.404,-76.684,69.38,-76.669,69.366,-76.67,69.349,-76.687,69.328,-76.81,69.267,-76.869,69.225,-76.911,69.175,-76.995,69.144]],[[-86.913,70.113,-86.799,70.105,-86.691,70.115,-86.613,70.106,-86.563,70.077,-86.531,70.048,-86.515,70.017,-86.558,69.995,-86.734,69.976,-86.855,69.986,-86.984,70.011,-87.044,70,-87.191,70.019,-87.264,70.044,-87.323,70.08,-87.323,70.102,-87.168,70.127,-87.107,70.147,-86.913,70.113]],[[-83.726,65.797,-83.598,65.757,-83.469,65.735,-83.263,65.723,-83.234,65.715,-83.234,65.697,-83.264,65.668,-83.332,65.631,-83.381,65.63,-83.495,65.656,-83.537,65.669,-83.583,65.699,-83.607,65.701,-83.636,65.692,-83.644,65.679,-83.631,65.662,-83.65,65.658,-83.788,65.669,-83.809,65.678,-83.798,65.71,-83.702,65.756,-83.787,65.77,-83.814,65.788,-83.939,65.758,-84.008,65.752,-84.118,65.772,-84.13,65.877,-84.143,65.916,-84.193,65.942,-84.223,65.97,-84.271,65.991,-84.37,66.012,-84.451,66.064,-84.467,66.088,-84.456,66.106,-84.407,66.131,-84.122,66.078,-83.95,66.027,-83.787,65.966,-83.701,65.92,-83.694,65.89,-83.715,65.861,-83.765,65.831,-83.726,65.797]],[[-86.596,67.736,-86.638,67.735,-86.706,67.75,-86.861,67.81,-86.893,67.837,-86.908,67.867,-86.908,67.902,-86.895,67.938,-86.847,68.01,-86.938,68.068,-86.96,68.1,-86.949,68.119,-86.899,68.163,-86.885,68.191,-86.834,68.23,-86.702,68.306,-86.57,68.288,-86.452,68.225,-86.421,68.183,-86.43,68.139,-86.42,68.074,-86.39,67.989,-86.382,67.927,-86.396,67.889,-86.447,67.817,-86.49,67.784,-86.546,67.752,-86.596,67.736]],[[-84.675,65.575,-84.727,65.564,-84.783,65.57,-84.83,65.599,-84.869,65.651,-84.931,65.689,-85.072,65.737,-85.096,65.756,-85.136,65.821,-85.144,65.885,-85.174,65.944,-85.176,65.972,-85.15,66.015,-85.031,66.025,-84.939,66.009,-84.92,65.997,-84.889,65.972,-84.87,65.942,-84.757,65.859,-84.692,65.793,-84.603,65.657,-84.602,65.631,-84.626,65.604,-84.675,65.575]],[[-93.044,61.844,-93.085,61.842,-93.177,61.893,-93.197,61.919,-93.076,61.935,-92.993,61.89,-93,61.867,-93.044,61.844]],[[-101.226,76.579,-101.485,76.575,-101.605,76.587,-101.613,76.605,-101.509,76.628,-101.165,76.665,-100.962,76.734,-100.886,76.743,-100.622,76.752,-100.467,76.75,-100.269,76.734,-100.747,76.649,-101.226,76.579]],[[-103.003,78.146,-103.118,78.126,-103.252,78.138,-103.271,78.151,-103.274,78.166,-103.26,78.183,-103.11,78.246,-102.973,78.267,-102.892,78.271,-102.826,78.25,-102.788,78.218,-103.003,78.146]],[[-101.694,77.697,-101.831,77.687,-102.08,77.692,-102.378,77.728,-102.458,77.77,-102.475,77.837,-102.472,77.873,-102.448,77.881,-102.263,77.889,-101.918,77.9,-101.639,77.892,-101.322,77.854,-101.193,77.83,-101.128,77.813,-101.046,77.778,-101.02,77.762,-101.002,77.735,-101.398,77.729,-101.585,77.718,-101.694,77.697]],[[-89.726,76.507,-89.773,76.494,-89.924,76.501,-89.974,76.488,-90.054,76.495,-90.165,76.524,-90.294,76.579,-90.441,76.663,-90.556,76.735,-90.562,76.754,-90.525,76.788,-90.41,76.81,-90.136,76.837,-89.949,76.836,-89.775,76.782,-89.725,76.763,-89.695,76.741,-89.694,76.72,-89.709,76.701,-89.788,76.66,-89.822,76.631,-89.822,76.602,-89.805,76.561,-89.773,76.534,-89.726,76.521,-89.726,76.507]],[[-96.079,75.51,-96.156,75.477,-96.237,75.475,-96.344,75.506,-96.462,75.494,-96.622,75.431,-96.679,75.394,-96.723,75.381,-96.857,75.369,-96.915,75.38,-96.97,75.413,-97.021,75.468,-96.983,75.51,-96.856,75.538,-96.523,75.584,-96.428,75.606,-96.417,75.631,-96.397,75.647,-96.368,75.655,-96.145,75.614,-96.04,75.586,-95.96,75.554,-95.969,75.542,-96.079,75.51]],[[-95.307,74.505,-95.352,74.5,-95.442,74.506,-95.777,74.551,-95.834,74.569,-95.851,74.582,-95.774,74.599,-95.746,74.616,-95.66,74.637,-95.51,74.637,-95.353,74.586,-95.278,74.54,-95.274,74.519,-95.307,74.505]],[[-121.076,75.745,-121.154,75.741,-121.241,75.752,-121.221,75.777,-121.026,75.848,-121.015,75.868,-121.018,75.884,-121.042,75.903,-120.993,75.927,-120.914,75.938,-120.888,75.928,-120.879,75.907,-120.897,75.845,-120.921,75.814,-120.955,75.789,-121.007,75.766,-121.076,75.745]],[[-113.561,76.743,-113.712,76.711,-114.751,76.759,-114.808,76.774,-114.835,76.795,-114.647,76.851,-114.42,76.875,-113.892,76.895,-113.708,76.873,-113.585,76.847,-113.517,76.825,-113.488,76.783,-113.561,76.743]],[[-104.12,75.036,-104.309,75.031,-104.634,75.061,-104.828,75.12,-104.887,75.148,-104.882,75.16,-104.848,75.173,-104.801,75.211,-104.69,75.321,-104.649,75.35,-104.474,75.413,-104.346,75.43,-104.075,75.425,-103.917,75.392,-103.851,75.371,-103.804,75.346,-103.758,75.289,-103.746,75.252,-103.667,75.211,-103.644,75.187,-103.642,75.163,-103.664,75.139,-103.71,75.115,-103.814,75.08,-104.12,75.036]],[[-100.217,68.807,-100.249,68.775,-100.288,68.766,-100.366,68.729,-100.397,68.724,-100.443,68.748,-100.481,68.786,-100.497,68.792,-100.521,68.791,-100.573,68.766,-100.597,68.766,-100.616,68.783,-100.625,68.816,-100.625,68.865,-100.6,68.941,-100.598,68.969,-100.612,68.99,-100.601,69.009,-100.565,69.027,-100.52,69.035,-100.414,69.028,-100.33,68.998,-100.289,68.958,-100.207,68.926,-100.178,68.904,-100.217,68.807]],[[-99.995,69.014,-100.018,68.954,-100.141,68.97,-100.196,68.991,-100.242,69.04,-100.247,69.053,-100.237,69.071,-100.187,69.114,-100.153,69.129,-100.073,69.111,-100.035,69.087,-100.006,69.047,-99.995,69.014]],[[-100.308,70.496,-100.321,70.488,-100.537,70.525,-100.621,70.547,-100.648,70.563,-100.667,70.596,-100.678,70.646,-100.635,70.67,-100.538,70.669,-100.434,70.649,-100.276,70.595,-100.321,70.578,-100.323,70.542,-100.306,70.508,-100.308,70.496]],[[-95.514,69.574,-95.381,69.507,-95.382,69.474,-95.399,69.42,-95.437,69.378,-95.496,69.35,-95.579,69.336,-95.684,69.336,-95.73,69.348,-95.696,69.39,-95.67,69.402,-95.666,69.439,-95.683,69.5,-95.704,69.538,-95.764,69.56,-95.806,69.56,-95.818,69.541,-95.798,69.5,-95.812,69.447,-95.858,69.382,-95.893,69.352,-95.956,69.367,-95.986,69.392,-95.978,69.433,-95.995,69.47,-95.979,69.509,-95.936,69.567,-95.876,69.606,-95.798,69.626,-95.707,69.624,-95.602,69.602,-95.514,69.574]],[[-101.172,69.397,-101.254,69.388,-101.269,69.391,-101.262,69.418,-101.268,69.431,-101.29,69.441,-101.218,69.463,-101.207,69.48,-101.23,69.493,-101.328,69.517,-101.356,69.54,-101.351,69.559,-101.313,69.576,-101.245,69.574,-101.098,69.541,-101.031,69.495,-101.001,69.462,-101.049,69.457,-101.087,69.443,-101.127,69.415,-101.172,69.397]],[[-101.846,68.586,-101.887,68.585,-101.945,68.603,-102.266,68.664,-102.308,68.682,-102.271,68.708,-102.153,68.74,-102.074,68.774,-102.013,68.825,-101.828,68.799,-101.759,68.775,-101.733,68.753,-101.722,68.724,-101.732,68.652,-101.794,68.637,-101.846,68.586]],[[-104.541,68.406,-104.596,68.402,-104.699,68.418,-104.851,68.454,-104.965,68.492,-105.042,68.532,-105.051,68.559,-104.994,68.574,-104.907,68.582,-104.7,68.577,-104.602,68.562,-104.472,68.504,-104.445,68.471,-104.44,68.45,-104.457,68.431,-104.541,68.406]],[[-107.9,67.402,-107.95,67.318,-107.97,67.326,-108.004,67.366,-108.073,67.385,-108.152,67.429,-108.151,67.525,-108.121,67.568,-108.128,67.629,-108.049,67.665,-107.991,67.622,-107.975,67.549,-107.989,67.514,-107.932,67.476,-107.905,67.467,-107.891,67.437,-107.9,67.402]],[[-109.166,67.982,-109.054,67.972,-108.971,67.98,-108.91,67.939,-108.886,67.899,-108.894,67.884,-108.92,67.879,-109.096,67.924,-109.162,67.952,-109.184,67.975,-109.166,67.982]],[[-108.093,67.005,-107.966,66.997,-107.806,66.999,-107.833,66.921,-107.895,66.872,-107.944,66.858,-107.965,66.885,-108.06,66.947,-108.093,67.005]],[[-109.323,67.991,-109.361,67.988,-109.498,68.047,-109.469,68.098,-109.342,68.046,-109.324,68.013,-109.323,67.991]],[[-139.043,69.577,-139.126,69.539,-139.257,69.579,-139.291,69.598,-139.14,69.65,-139.073,69.648,-138.932,69.617,-138.879,69.59,-139.043,69.577]]]},{"n":"Cameroon","c":"CM","r":3,"ml":3,"lx":12.47,"ly":4.59,"b":[8.533,1.676,16.183,13.079],"p":[[[8.556,4.755,8.585,4.833,8.641,4.927,8.716,5.047,8.801,5.197,8.859,5.464,8.899,5.63,8.935,5.781,8.997,5.918,9.06,6.009,9.239,6.186,9.373,6.32,9.442,6.373,9.49,6.419,9.574,6.47,9.66,6.532,9.726,6.65,9.78,6.76,9.821,6.784,9.874,6.803,10.039,6.921,10.144,6.996,10.168,6.959,10.186,6.913,10.205,6.892,10.293,6.877,10.413,6.878,10.482,6.891,10.519,6.93,10.556,7.037,10.578,7.058,10.606,7.063,10.738,6.988,10.846,6.882,10.954,6.777,11.009,6.739,11.033,6.698,11.08,6.506,11.106,6.458,11.153,6.438,11.237,6.451,11.325,6.485,11.402,6.534,11.478,6.597,11.529,6.655,11.552,6.697,11.563,6.855,11.58,6.889,11.658,6.952,11.787,7.056,11.861,7.116,11.855,7.138,11.809,7.202,11.767,7.272,11.809,7.345,11.852,7.401,12.016,7.59,12.017,7.652,12.025,7.728,12.156,7.942,12.231,8.227,12.233,8.282,12.311,8.42,12.404,8.596,12.583,8.624,12.652,8.668,12.731,8.746,12.782,8.818,12.807,8.887,12.824,9.019,12.856,9.171,12.876,9.304,12.929,9.426,13.019,9.488,13.175,9.54,13.199,9.564,13.221,9.645,13.239,9.814,13.244,9.916,13.25,9.96,13.27,10.036,13.415,10.171,13.479,10.383,13.535,10.605,13.7,10.873,13.892,11.14,13.981,11.212,14.057,11.245,14.143,11.249,14.202,11.268,14.409,11.401,14.496,11.446,14.56,11.492,14.575,11.532,14.582,11.591,14.562,11.729,14.597,11.83,14.618,11.987,14.627,12.109,14.62,12.151,14.587,12.209,14.581,12.222,14.519,12.298,14.415,12.344,14.273,12.356,14.197,12.384,14.185,12.447,14.178,12.484,14.17,12.524,14.16,12.613,14.064,13.079,14.245,13.077,14.462,13.022,14.516,12.98,14.545,12.82,14.623,12.73,14.761,12.656,14.847,12.502,14.881,12.269,14.957,12.13,14.974,12.108,15.06,11.907,15.081,11.846,15.088,11.724,15.078,11.643,15.122,11.541,15.055,11.369,15.036,11.263,15.03,11.114,15.069,10.851,15.132,10.648,15.201,10.485,15.276,10.357,15.4,10.217,15.532,10.088,15.655,10.008,15.541,9.96,15.32,9.954,15.193,9.981,15.133,9.983,15.072,9.966,14.836,9.942,14.598,9.953,14.377,9.985,14.243,9.98,14.14,9.902,14.056,9.784,13.977,9.692,14.005,9.589,14.064,9.532,14.178,9.406,14.28,9.285,14.332,9.204,14.536,9.025,14.733,8.866,14.771,8.839,14.826,8.81,14.861,8.799,14.968,8.707,15.116,8.557,15.252,8.322,15.349,8.084,15.443,7.852,15.484,7.813,15.55,7.788,15.558,7.738,15.553,7.665,15.532,7.604,15.48,7.524,15.379,7.358,15.246,7.264,15.207,7.206,15.186,7.135,15.157,7.064,15.086,6.91,15.035,6.784,14.983,6.745,14.862,6.556,14.78,6.366,14.764,6.316,14.739,6.28,14.7,6.25,14.559,6.191,14.512,6.162,14.475,6.127,14.441,6.087,14.431,6.039,14.464,5.971,14.503,5.917,14.542,5.914,14.577,5.916,14.599,5.884,14.617,5.865,14.617,5.496,14.584,5.44,14.584,5.415,14.568,5.351,14.563,5.28,14.574,5.252,14.602,5.229,14.641,5.179,14.662,5.066,14.709,4.666,14.731,4.602,14.77,4.558,14.894,4.472,15.023,4.359,15.064,4.285,15.088,4.164,15.137,4.069,15.136,4.037,15.115,4.024,15.067,4.023,15.035,4.016,15.062,3.947,15.129,3.827,15.24,3.702,15.36,3.567,15.458,3.457,15.581,3.329,15.677,3.23,15.775,3.127,15.849,3.103,15.905,3.096,15.929,3.076,15.958,3.029,16.008,2.977,16.063,2.909,16.082,2.839,16.059,2.773,16.082,2.678,16.083,2.67,16.102,2.633,16.096,2.599,16.107,2.473,16.136,2.364,16.183,2.27,16.183,2.262,16.177,2.205,16.116,2.168,16.08,2.107,16.07,2.022,16.088,1.918,16.135,1.796,16.136,1.724,16.12,1.714,16.09,1.691,16.059,1.676,15.975,1.76,15.882,1.817,15.742,1.915,15.6,1.95,15.417,1.957,15.339,1.945,15.282,1.982,15.204,2.024,15.16,2.036,15.1,2.002,15.058,2.001,15.006,2.014,14.902,2.012,14.893,2.069,14.875,2.08,14.763,2.075,14.728,2.122,14.713,2.117,14.669,2.132,14.579,2.199,14.484,2.155,14.287,2.16,14.034,2.159,13.773,2.157,13.533,2.16,13.294,2.162,13.27,2.224,13.22,2.256,13.131,2.259,12.867,2.247,12.666,2.257,12.601,2.265,12.53,2.281,12.361,2.296,12.153,2.284,12.106,2.288,11.94,2.285,11.559,2.302,11.348,2.3,11.353,2.261,11.34,2.234,11.329,2.167,11.097,2.167,10.791,2.168,10.502,2.168,10.307,2.168,9.98,2.168,9.87,2.213,9.837,2.242,9.83,2.275,9.826,2.298,9.801,2.304,9.822,2.539,9.868,2.735,9.885,2.917,9.948,3.079,9.915,3.24,9.876,3.31,9.672,3.538,9.766,3.624,9.642,3.612,9.616,3.696,9.556,3.798,9.593,3.814,9.628,3.87,9.74,3.853,9.736,3.88,9.64,3.965,9.649,4.008,9.689,4.056,9.67,4.077,9.6,4.027,9.551,4.028,9.512,4.061,9.484,4.066,9.501,4.001,9.462,3.943,9.425,3.922,9.362,3.926,9.311,3.94,9.297,3.973,9.249,3.998,9.114,4.041,9,4.092,8.977,4.23,8.932,4.29,8.914,4.358,8.903,4.435,8.918,4.554,8.889,4.573,8.856,4.579,8.807,4.573,8.762,4.58,8.708,4.646,8.66,4.671,8.69,4.55,8.656,4.516,8.574,4.526,8.54,4.572,8.533,4.606,8.571,4.752,8.556,4.755]]]},{"n":"Cambodia","c":"KH","r":3,"ml":3,"lx":104.5,"ly":12.65,"b":[102.32,10.411,107.605,14.705],"p":[[[103.045,11.285,103.027,11.275,103.011,11.276,102.993,11.29,102.995,11.348,103.008,11.383,103.037,11.39,103.045,11.285]],[[103.318,10.719,103.281,10.68,103.223,10.76,103.223,10.782,103.318,10.719]],[[107.519,14.705,107.535,14.65,107.493,14.546,107.448,14.451,107.364,14.369,107.36,14.308,107.331,14.127,107.343,14.069,107.362,14.019,107.389,13.993,107.462,13.816,107.529,13.654,107.594,13.522,107.605,13.438,107.546,13.225,107.475,13.03,107.482,12.933,107.512,12.836,107.544,12.706,107.555,12.54,107.538,12.432,107.506,12.365,107.446,12.296,107.393,12.26,107.33,12.319,107.28,12.322,107.212,12.304,107.159,12.277,107.051,12.176,106.931,12.077,106.765,12.052,106.7,11.979,106.631,11.969,106.5,11.966,106.414,11.948,106.418,11.912,106.411,11.738,106.413,11.698,106.399,11.687,106.34,11.682,106.239,11.708,106.103,11.751,106.006,11.758,105.956,11.682,105.927,11.653,105.89,11.648,105.851,11.635,105.838,11.601,105.835,11.559,105.854,11.487,105.861,11.372,105.856,11.294,105.892,11.245,106.1,11.079,106.161,11.037,106.168,11.012,106.132,10.922,106.164,10.795,106.099,10.797,105.99,10.852,105.938,10.885,105.875,10.858,105.853,10.864,105.811,10.926,105.755,10.99,105.698,10.994,105.577,10.969,105.453,10.951,105.406,10.952,105.387,10.94,105.315,10.845,105.284,10.861,105.159,10.898,105.046,10.911,105.022,10.887,105.036,10.809,105.061,10.734,105.046,10.702,104.984,10.662,104.901,10.59,104.851,10.534,104.815,10.521,104.69,10.523,104.564,10.516,104.514,10.463,104.467,10.422,104.426,10.411,104.262,10.541,103.937,10.587,103.902,10.644,103.871,10.655,103.841,10.581,103.662,10.509,103.587,10.552,103.532,10.605,103.54,10.669,103.592,10.721,103.681,10.759,103.722,10.89,103.654,11.059,103.595,11.108,103.532,11.147,103.467,11.084,103.411,10.977,103.354,10.922,103.272,10.909,103.153,10.914,103.106,11.074,103.091,11.211,103.107,11.368,103.125,11.461,103.011,11.589,103.004,11.711,102.949,11.773,102.932,11.742,102.934,11.707,102.918,11.732,102.737,12.09,102.706,12.256,102.737,12.383,102.756,12.426,102.703,12.494,102.63,12.57,102.5,12.67,102.491,12.828,102.462,13.015,102.423,13.078,102.363,13.193,102.331,13.288,102.32,13.54,102.336,13.56,102.429,13.568,102.547,13.586,102.566,13.626,102.545,13.66,102.62,13.717,102.729,13.842,102.813,13.972,102.873,14.055,102.909,14.137,103.031,14.253,103.199,14.333,103.313,14.351,103.432,14.379,103.546,14.417,103.6,14.421,103.742,14.374,103.818,14.362,103.899,14.363,103.982,14.358,104.054,14.363,104.228,14.396,104.412,14.37,104.576,14.39,104.779,14.428,104.879,14.404,104.97,14.366,104.982,14.289,105.003,14.254,105.034,14.227,105.074,14.227,105.126,14.281,105.169,14.336,105.183,14.346,105.186,14.319,105.207,14.259,105.246,14.201,105.285,14.161,105.35,14.11,105.393,14.107,105.532,14.156,105.74,14.085,105.764,14.049,105.831,13.977,105.904,13.925,106.067,13.921,106.125,14.049,106.097,14.127,106.004,14.263,105.979,14.343,106.008,14.357,106.165,14.372,106.191,14.388,106.225,14.476,106.268,14.466,106.355,14.455,106.447,14.515,106.501,14.578,106.531,14.549,106.564,14.505,106.599,14.479,106.665,14.441,106.738,14.388,106.783,14.335,106.82,14.315,106.913,14.329,106.938,14.327,106.992,14.391,107.03,14.426,107.062,14.416,107.109,14.417,107.207,14.498,107.262,14.572,107.293,14.592,107.38,14.555,107.415,14.563,107.465,14.665,107.519,14.705]]]},{"n":"Myanmar","c":"MM","r":3,"ml":3,"lx":95.8,"ly":21.57,"b":[92.18,9.875,101.147,28.517],"p":[[[100.122,20.317,100.004,20.38,99.954,20.415,99.89,20.424,99.825,20.384,99.773,20.341,99.72,20.325,99.639,20.32,99.532,20.343,99.459,20.363,99.448,20.352,99.488,20.261,99.502,20.188,99.486,20.15,99.452,20.118,99.399,20.093,99.338,20.079,99.284,20.08,99.197,20.115,99.131,20.117,99.074,20.099,99.04,20.074,99.021,20.042,98.987,19.861,98.958,19.805,98.917,19.773,98.876,19.77,98.82,19.778,98.761,19.771,98.494,19.701,98.455,19.694,98.371,19.689,98.294,19.687,98.239,19.691,98.111,19.762,98.049,19.77,98.015,19.75,97.991,19.654,97.916,19.593,97.817,19.46,97.794,19.266,97.804,19.13,97.714,18.996,97.706,18.932,97.754,18.621,97.746,18.588,97.728,18.572,97.672,18.561,97.577,18.529,97.515,18.498,97.485,18.494,97.397,18.518,97.374,18.518,97.381,18.494,97.451,18.36,97.524,18.296,97.599,18.303,97.632,18.29,97.622,18.258,97.652,18.174,97.72,18.037,97.74,17.935,97.699,17.834,97.706,17.797,97.729,17.776,97.793,17.681,97.929,17.533,98.063,17.373,98.175,17.24,98.257,17.148,98.439,16.976,98.471,16.895,98.478,16.732,98.523,16.638,98.565,16.571,98.594,16.515,98.661,16.33,98.689,16.305,98.835,16.418,98.869,16.394,98.888,16.352,98.888,16.298,98.866,16.237,98.818,16.181,98.592,16.051,98.574,15.939,98.558,15.769,98.554,15.56,98.565,15.404,98.557,15.368,98.537,15.351,98.452,15.357,98.329,15.279,98.286,15.272,98.232,15.241,98.191,15.204,98.178,15.147,98.202,14.976,98.246,14.815,98.332,14.696,98.4,14.603,98.495,14.473,98.57,14.36,98.721,14.236,98.934,14.05,99.015,13.947,99.086,13.823,99.137,13.717,99.156,13.576,99.172,13.497,99.176,13.233,99.137,13.173,99.107,13.104,99.124,13.031,99.174,12.961,99.174,12.882,99.22,12.74,99.297,12.653,99.372,12.594,99.405,12.548,99.394,12.474,99.416,12.395,99.432,12.309,99.463,12.19,99.523,12.09,99.615,11.781,99.613,11.75,99.573,11.687,99.515,11.631,99.478,11.613,99.443,11.554,99.359,11.389,99.19,11.105,99.025,10.92,98.887,10.788,98.787,10.708,98.757,10.661,98.757,10.624,98.775,10.557,98.768,10.431,98.747,10.351,98.718,10.266,98.703,10.19,98.658,10.179,98.563,10.035,98.521,10.107,98.497,10.183,98.523,10.353,98.465,10.676,98.501,10.719,98.536,10.741,98.599,10.864,98.676,10.987,98.683,11.133,98.745,11.24,98.73,11.33,98.733,11.435,98.746,11.521,98.741,11.592,98.791,11.665,98.876,11.72,98.84,11.739,98.805,11.779,98.694,11.718,98.636,11.738,98.625,11.801,98.639,11.869,98.645,11.91,98.689,11.957,98.686,12.047,98.664,12.127,98.696,12.225,98.631,12.225,98.6,12.245,98.619,12.3,98.679,12.348,98.624,12.441,98.665,12.54,98.663,12.662,98.636,12.771,98.637,12.848,98.595,12.986,98.576,13.162,98.487,13.293,98.421,13.484,98.245,13.733,98.248,13.84,98.239,13.934,98.2,13.98,98.15,13.648,98.111,13.713,98.098,13.898,98.073,13.986,98.1,14.162,97.998,14.335,97.977,14.461,97.91,14.653,97.929,14.696,98.019,14.653,97.937,14.764,97.869,14.739,97.812,14.859,97.8,15.185,97.744,15.307,97.774,15.431,97.71,15.876,97.584,16.02,97.609,16.144,97.641,16.254,97.634,16.458,97.665,16.52,97.726,16.569,97.668,16.552,97.62,16.537,97.505,16.525,97.376,16.523,97.331,16.672,97.267,16.743,97.212,16.893,97.178,17.062,97.2,17.095,97.1,17.165,97.075,17.207,96.97,17.317,96.851,17.401,96.878,17.342,96.91,17.305,96.851,17.203,96.909,17.031,96.858,16.921,96.811,16.778,96.765,16.71,96.622,16.564,96.507,16.514,96.431,16.505,96.364,16.521,96.282,16.596,96.262,16.659,96.249,16.765,96.22,16.781,96.189,16.768,96.238,16.631,96.237,16.567,96.324,16.444,96.293,16.41,96.135,16.343,96.081,16.353,96.043,16.34,96.032,16.285,96.012,16.254,95.763,16.169,95.711,16.073,95.679,15.977,95.556,15.838,95.39,15.723,95.348,15.729,95.301,15.756,95.308,15.88,95.365,15.985,95.347,16.098,95.333,16.033,95.226,15.877,95.177,15.826,95.078,15.839,94.943,15.818,94.891,15.979,94.892,16.038,94.882,16.088,94.898,16.141,94.893,16.183,94.86,16.102,94.848,16.033,94.798,15.971,94.662,15.904,94.656,15.989,94.651,16.065,94.681,16.133,94.677,16.242,94.72,16.399,94.717,16.452,94.703,16.512,94.679,16.426,94.665,16.336,94.638,16.309,94.588,16.289,94.496,16.186,94.442,16.094,94.299,16.008,94.224,16.016,94.214,16.127,94.271,16.517,94.327,16.572,94.353,16.64,94.4,16.868,94.452,16.954,94.473,17.135,94.494,17.167,94.564,17.309,94.589,17.569,94.56,17.699,94.494,17.825,94.431,18.202,94.266,18.507,94.252,18.609,94.171,18.732,94.246,18.741,94.091,18.849,94.07,18.893,94.039,19.146,94.045,19.287,94.022,19.269,94.002,19.182,93.941,19.146,93.968,18.995,93.961,18.958,93.929,18.9,93.8,18.961,93.705,19.027,93.598,19.188,93.493,19.369,93.531,19.398,93.579,19.401,93.728,19.267,93.825,19.238,93.886,19.272,93.962,19.329,93.998,19.441,93.961,19.482,93.888,19.504,93.84,19.534,93.77,19.61,93.761,19.648,93.74,19.697,93.669,19.732,93.612,19.776,93.66,19.854,93.707,19.912,93.582,19.91,93.439,20.009,93.41,20.038,93.362,20.058,93.25,20.07,93.157,20.041,93.199,19.898,93.191,19.851,93.129,19.858,93.002,20.075,93.04,20.13,93.096,20.181,93.068,20.189,93.015,20.185,93.067,20.378,93.035,20.406,93.019,20.346,92.991,20.288,92.882,20.152,92.828,20.178,92.791,20.211,92.844,20.283,92.872,20.302,92.891,20.34,92.851,20.415,92.787,20.469,92.736,20.563,92.709,20.564,92.733,20.453,92.723,20.296,92.608,20.47,92.378,20.718,92.324,20.792,92.312,20.864,92.286,20.932,92.268,21.005,92.264,21.061,92.215,21.113,92.192,21.202,92.18,21.293,92.208,21.358,92.28,21.428,92.331,21.44,92.373,21.409,92.472,21.363,92.539,21.32,92.569,21.263,92.6,21.27,92.632,21.306,92.625,21.351,92.593,21.467,92.584,21.609,92.583,21.94,92.575,21.978,92.63,22.011,92.653,22.049,92.675,22.106,92.689,22.131,92.721,22.132,92.771,22.105,92.854,22.01,92.909,21.989,92.965,22.004,93.022,22.146,93.043,22.184,93.071,22.209,93.121,22.205,93.151,22.231,93.162,22.292,93.162,22.36,93.105,22.547,93.088,22.633,93.079,22.718,93.114,22.806,93.163,22.908,93.151,22.997,93.164,23.032,93.204,23.037,93.254,23.015,93.308,23.03,93.349,23.085,93.366,23.133,93.391,23.339,93.408,23.528,93.415,23.682,93.373,23.774,93.307,24.022,93.326,24.064,93.356,24.074,93.452,23.987,93.494,23.973,93.564,23.986,93.633,24.005,93.683,24.007,93.756,23.977,93.855,23.944,94.011,23.903,94.075,23.872,94.128,23.876,94.17,23.973,94.22,24.113,94.293,24.322,94.377,24.474,94.399,24.514,94.493,24.638,94.584,24.767,94.663,24.931,94.708,25.049,94.704,25.098,94.675,25.139,94.616,25.165,94.567,25.192,94.553,25.216,94.554,25.243,94.58,25.32,94.623,25.41,94.668,25.459,94.786,25.519,94.861,25.597,94.946,25.7,94.992,25.77,95.015,25.913,95.041,25.941,95.093,25.987,95.132,26.041,95.129,26.07,95.108,26.091,95.069,26.191,95.051,26.347,95.06,26.474,95.089,26.525,95.129,26.597,95.201,26.641,95.305,26.672,95.464,26.756,95.738,26.95,95.837,27.014,95.905,27.047,95.971,27.128,96.061,27.217,96.191,27.261,96.274,27.278,96.666,27.339,96.732,27.331,96.798,27.296,96.88,27.178,96.953,27.133,97.038,27.102,97.102,27.115,97.104,27.163,96.902,27.44,96.884,27.515,96.877,27.587,96.9,27.644,96.963,27.698,97.05,27.76,97.158,27.837,97.226,27.89,97.306,27.907,97.335,27.938,97.344,27.982,97.339,28.031,97.303,28.086,97.31,28.155,97.322,28.218,97.356,28.254,97.431,28.354,97.478,28.426,97.502,28.456,97.538,28.51,97.599,28.517,97.659,28.5,97.695,28.469,97.73,28.407,97.769,28.356,97.817,28.356,97.865,28.364,97.888,28.356,97.934,28.314,98.022,28.212,98.062,28.186,98.099,28.142,98.118,28.055,98.13,27.968,98.241,27.663,98.274,27.599,98.299,27.55,98.35,27.538,98.392,27.587,98.409,27.639,98.453,27.657,98.504,27.648,98.6,27.599,98.651,27.572,98.677,27.422,98.682,27.245,98.675,27.191,98.717,27.045,98.729,26.877,98.738,26.786,98.739,26.698,98.732,26.583,98.709,26.43,98.672,26.299,98.686,26.189,98.663,26.139,98.572,26.114,98.564,26.072,98.591,26.004,98.655,25.918,98.656,25.864,98.625,25.827,98.558,25.823,98.466,25.789,98.402,25.678,98.334,25.587,98.297,25.569,98.173,25.595,98.143,25.571,98.1,25.416,98.064,25.349,98.011,25.293,97.962,25.259,97.918,25.236,97.82,25.252,97.767,25.158,97.715,25.034,97.711,24.97,97.738,24.87,97.724,24.842,97.671,24.82,97.583,24.775,97.529,24.631,97.531,24.492,97.563,24.444,97.624,24.423,97.667,24.38,97.671,24.313,97.708,24.229,97.691,24.131,97.568,23.988,97.565,23.911,97.63,23.887,97.686,23.898,97.756,23.932,97.838,23.986,98.017,24.065,98.213,24.111,98.367,24.119,98.499,24.116,98.564,24.099,98.583,24.07,98.764,24.116,98.802,24.119,98.835,24.121,98.834,24.091,98.702,23.964,98.677,23.905,98.681,23.842,98.735,23.783,98.788,23.738,98.832,23.624,98.798,23.52,98.82,23.483,98.859,23.44,98.883,23.38,98.886,23.307,98.864,23.191,99.055,23.131,99.22,23.103,99.341,23.096,99.418,23.069,99.465,23.046,99.497,23.005,99.507,22.959,99.467,22.927,99.385,22.825,99.338,22.689,99.343,22.587,99.338,22.498,99.243,22.37,99.205,22.283,99.172,22.192,99.173,22.153,99.193,22.126,99.233,22.11,99.303,22.101,99.389,22.111,99.593,22.089,99.825,22.05,99.918,22.028,99.948,21.988,99.94,21.902,99.926,21.821,99.941,21.759,99.978,21.702,100.041,21.683,100.096,21.661,100.106,21.617,100.089,21.558,100.117,21.511,100.148,21.481,100.215,21.463,100.351,21.501,100.446,21.484,100.531,21.458,100.605,21.472,100.677,21.505,100.835,21.655,101.019,21.736,101.08,21.756,101.121,21.746,101.131,21.736,101.128,21.705,101.147,21.582,101.139,21.567,101.08,21.469,100.928,21.366,100.82,21.314,100.757,21.313,100.703,21.251,100.659,21.13,100.614,21.059,100.567,21.038,100.536,20.992,100.522,20.922,100.549,20.884,100.618,20.879,100.623,20.86,100.565,20.825,100.493,20.813,100.407,20.823,100.326,20.796,100.249,20.73,100.184,20.589,100.13,20.372,100.122,20.317]],[[98.21,10.953,98.293,10.78,98.284,10.753,98.271,10.74,98.252,10.744,98.218,10.838,98.155,10.898,98.08,10.887,98.143,10.963,98.167,10.98,98.21,10.953]],[[98.183,9.933,98.134,9.875,98.118,9.878,98.14,9.975,98.221,10.045,98.292,10.051,98.283,10.008,98.231,9.954,98.183,9.933]],[[98.222,11.478,98.216,11.456,98.209,11.457,98.187,11.472,98.201,11.567,98.239,11.645,98.278,11.758,98.3,11.783,98.308,11.723,98.284,11.594,98.263,11.524,98.222,11.478]],[[98.516,11.905,98.474,11.899,98.454,12.061,98.466,12.084,98.525,12.005,98.61,11.957,98.576,11.925,98.516,11.905]],[[98.554,11.745,98.528,11.539,98.465,11.567,98.435,11.567,98.397,11.684,98.4,11.715,98.376,11.792,98.524,11.805,98.554,11.745]],[[98.414,12.598,98.436,12.571,98.468,12.571,98.459,12.474,98.381,12.354,98.334,12.336,98.314,12.336,98.331,12.511,98.303,12.612,98.312,12.678,98.396,12.647,98.414,12.598]],[[98.137,12.15,98.125,12.145,98.108,12.148,98.075,12.164,98.037,12.232,98.057,12.28,98.071,12.292,98.105,12.288,98.122,12.279,98.128,12.261,98.118,12.223,98.12,12.191,98.137,12.15]],[[94.477,15.946,94.412,15.848,94.388,15.994,94.494,16.075,94.546,16.153,94.601,16.206,94.619,16.141,94.566,16.019,94.477,15.946]],[[97.575,16.253,97.537,16.24,97.48,16.306,97.469,16.461,97.516,16.497,97.542,16.505,97.579,16.486,97.593,16.461,97.6,16.43,97.589,16.397,97.575,16.253]],[[93.691,18.684,93.674,18.676,93.57,18.76,93.488,18.868,93.618,18.889,93.745,18.866,93.746,18.808,93.718,18.716,93.691,18.684]],[[93.492,19.893,93.513,19.755,93.445,19.806,93.42,19.878,93.413,19.95,93.492,19.893]],[[93.715,19.558,93.829,19.475,93.875,19.481,93.946,19.429,93.947,19.408,93.934,19.365,93.902,19.332,93.815,19.299,93.756,19.326,93.732,19.416,93.662,19.459,93.644,19.495,93.688,19.544,93.715,19.558]],[[98.542,10.962,98.519,10.959,98.498,10.964,98.477,10.98,98.527,11.087,98.542,10.962]],[[98.075,11.692,98.084,11.637,98.021,11.696,98.01,11.86,98.06,11.757,98.081,11.733,98.075,11.692]],[[98.066,12.39,98.06,12.354,98.002,12.279,97.952,12.322,97.939,12.346,97.99,12.394,98.045,12.387,98.06,12.398,98.066,12.39]],[[98.315,13.099,98.309,12.935,98.259,13.014,98.251,13.104,98.255,13.189,98.265,13.202,98.269,13.189,98.299,13.152,98.315,13.099]],[[94.805,15.819,94.784,15.794,94.743,15.812,94.733,15.823,94.828,15.933,94.838,15.892,94.805,15.819]],[[93.01,19.924,93.023,19.829,92.975,19.868,92.913,20,92.915,20.086,92.96,20.046,93.01,19.924]]]},{"n":"Burundi","c":"BI","r":6,"ml":4,"lx":29.92,"ly":-3.33,"b":[29.014,-4.456,30.811,-2.313],"p":[[[30.554,-2.4,30.534,-2.426,30.442,-2.613,30.424,-2.642,30.434,-2.659,30.473,-2.694,30.45,-2.753,30.441,-2.769,30.424,-2.824,30.433,-2.875,30.456,-2.893,30.515,-2.918,30.604,-2.935,30.709,-2.977,30.78,-2.985,30.797,-3.015,30.794,-3.069,30.811,-3.116,30.811,-3.201,30.79,-3.275,30.682,-3.309,30.626,-3.347,30.611,-3.366,30.625,-3.389,30.632,-3.419,30.53,-3.492,30.425,-3.589,30.4,-3.654,30.379,-3.731,30.348,-3.78,30.269,-3.85,30.187,-3.993,30.147,-4.085,29.947,-4.307,29.77,-4.418,29.718,-4.456,29.403,-4.449,29.379,-4.3,29.331,-4.095,29.223,-3.911,29.212,-3.834,29.217,-3.685,29.217,-3.476,29.21,-3.363,29.212,-3.281,29.226,-3.139,29.224,-3.054,29.153,-2.955,29.065,-2.851,29.017,-2.8,29.014,-2.758,29.014,-2.72,29.029,-2.665,29.063,-2.603,29.102,-2.596,29.198,-2.62,29.297,-2.673,29.35,-2.792,29.39,-2.809,29.464,-2.808,29.651,-2.793,29.698,-2.795,29.783,-2.766,29.868,-2.716,29.893,-2.665,29.912,-2.549,29.93,-2.34,29.973,-2.337,30.092,-2.412,30.117,-2.417,30.142,-2.414,30.183,-2.377,30.234,-2.347,30.271,-2.348,30.408,-2.313,30.482,-2.376,30.529,-2.396,30.554,-2.4]]]},{"n":"Burkina Faso","c":"BF","r":3,"ml":3,"lx":-1.36,"ly":12.67,"b":[-5.524,9.425,2.389,15.078],"p":[[[0.9,10.993,0.643,10.983,0.549,10.955,0.493,10.955,0.491,10.978,0.484,10.992,0.159,11.07,-0.069,11.116,-0.299,11.167,-0.313,11.119,-0.346,11.088,-0.396,11.086,-0.43,11.093,-0.454,11.056,-0.492,11.008,-0.545,10.984,-0.598,10.954,-0.627,10.927,-0.649,10.927,-0.701,10.989,-0.772,10.995,-0.903,10.985,-0.962,11.002,-1.042,11.01,-1.233,10.997,-1.537,11.023,-1.586,11.009,-1.6,10.998,-1.901,10.995,-2.232,10.991,-2.509,10.989,-2.752,10.986,-2.752,10.997,-2.83,10.998,-2.839,10.977,-2.907,10.728,-2.915,10.592,-2.878,10.508,-2.837,10.455,-2.791,10.432,-2.787,10.402,-2.823,10.363,-2.82,10.323,-2.777,10.282,-2.767,10.238,-2.788,10.193,-2.783,10.083,-2.751,9.91,-2.75,9.797,-2.781,9.746,-2.766,9.658,-2.706,9.534,-2.696,9.481,-2.717,9.457,-2.767,9.425,-2.817,9.426,-2.875,9.501,-2.901,9.535,-2.948,9.611,-2.988,9.687,-3.043,9.721,-3.096,9.752,-3.161,9.849,-3.224,9.895,-3.29,9.882,-3.386,9.9,-3.581,9.924,-3.791,9.917,-3.878,9.895,-3.963,9.86,-4.181,9.782,-4.267,9.743,-4.332,9.646,-4.406,9.648,-4.48,9.679,-4.527,9.723,-4.626,9.714,-4.722,9.757,-4.814,9.841,-4.883,9.869,-4.97,9.93,-4.994,10.046,-5.049,10.128,-5.1,10.242,-5.175,10.293,-5.262,10.32,-5.382,10.314,-5.461,10.36,-5.524,10.426,-5.507,10.483,-5.479,10.565,-5.476,10.644,-5.457,10.771,-5.469,10.931,-5.49,11.042,-5.424,11.089,-5.347,11.13,-5.3,11.206,-5.25,11.376,-5.229,11.522,-5.245,11.577,-5.27,11.62,-5.291,11.683,-5.302,11.76,-5.288,11.828,-5.23,11.89,-5.158,11.942,-5.106,11.968,-4.969,11.993,-4.798,12.032,-4.699,12.076,-4.627,12.12,-4.587,12.155,-4.546,12.226,-4.48,12.282,-4.429,12.338,-4.422,12.493,-4.422,12.582,-4.46,12.63,-4.481,12.672,-4.227,12.794,-4.225,12.879,-4.261,12.975,-4.31,13.052,-4.329,13.119,-4.259,13.197,-4.196,13.256,-4.151,13.306,-4.051,13.382,-3.947,13.402,-3.853,13.374,-3.576,13.194,-3.528,13.183,-3.47,13.196,-3.397,13.244,-3.302,13.281,-3.267,13.401,-3.27,13.577,-3.249,13.658,-3.198,13.673,-3.039,13.639,-2.997,13.637,-2.951,13.648,-2.917,13.679,-2.919,13.736,-2.926,13.787,-2.874,13.951,-2.779,14.074,-2.587,14.228,-2.527,14.258,-2.457,14.274,-2.113,14.168,-2.057,14.195,-1.973,14.457,-1.88,14.481,-1.768,14.486,-1.695,14.508,-1.657,14.527,-1.494,14.626,-1.205,14.762,-1.05,14.82,-1.019,14.841,-0.908,14.937,-0.76,15.048,-0.666,15.07,-0.537,15.078,-0.454,15.06,-0.432,15.029,-0.405,15.013,-0.236,15.059,0.007,14.985,0.217,14.911,0.204,14.865,0.203,14.783,0.185,14.653,0.164,14.497,0.251,14.396,0.355,14.288,0.383,14.246,0.355,14.139,0.374,14.076,0.429,13.972,0.522,13.84,0.618,13.703,0.685,13.685,0.748,13.675,0.786,13.65,0.842,13.626,0.898,13.611,0.947,13.581,0.978,13.552,1.018,13.468,1.126,13.412,1.201,13.358,1.171,13.33,1.077,13.341,0.988,13.365,0.977,13.325,0.973,13.17,0.987,13.042,1.008,13.025,1.097,13.001,1.309,12.834,1.5,12.676,1.565,12.635,1.671,12.62,1.79,12.613,1.841,12.628,1.956,12.707,2.017,12.716,2.074,12.714,2.105,12.701,2.16,12.636,2.212,12.538,2.226,12.466,2.221,12.427,2.204,12.413,2.109,12.394,2.069,12.379,2.058,12.358,2.073,12.309,2.091,12.278,2.194,12.136,2.343,11.946,2.389,11.897,2.363,11.84,2.287,11.691,2.231,11.629,1.98,11.418,1.858,11.443,1.6,11.401,1.561,11.449,1.501,11.456,1.427,11.447,1.4,11.429,1.392,11.408,1.365,11.379,1.317,11.295,1.28,11.274,1.235,11.261,1.179,11.263,1.146,11.252,1.146,11.21,1.136,11.174,1.098,11.156,1.082,11.116,1.085,11.076,1.062,11.058,1.014,11.068,0.985,11.079,0.958,11.028,0.925,10.993,0.9,10.993]]]},{"n":"Bulgaria","c":"BG","r":4,"ml":4,"lx":25.16,"ly":42.51,"b":[22.344,41.244,28.585,44.238],"p":[[[28.014,41.969,27.879,41.987,27.832,41.981,27.802,41.957,27.739,41.962,27.661,41.961,27.58,41.933,27.535,41.921,27.475,41.947,27.363,42.025,27.295,42.08,27.244,42.093,27.193,42.077,27.012,42.059,26.969,42.027,26.885,41.992,26.8,41.975,26.679,41.963,26.615,41.965,26.58,41.948,26.55,41.897,26.529,41.847,26.511,41.826,26.36,41.802,26.327,41.773,26.318,41.745,26.321,41.717,26.201,41.744,26.107,41.726,26.086,41.704,26.066,41.673,26.077,41.64,26.111,41.608,26.144,41.522,26.155,41.435,26.135,41.386,26.066,41.351,25.923,41.312,25.785,41.33,25.724,41.315,25.621,41.31,25.527,41.3,25.382,41.264,25.251,41.244,25.133,41.316,24.994,41.365,24.847,41.394,24.796,41.373,24.774,41.356,24.651,41.42,24.596,41.443,24.569,41.467,24.518,41.553,24.488,41.555,24.387,41.524,24.289,41.525,24.23,41.531,24.056,41.527,24.033,41.469,24.011,41.46,23.974,41.452,23.881,41.456,23.762,41.413,23.635,41.387,23.536,41.386,23.433,41.399,23.372,41.39,23.24,41.385,23.156,41.322,23.026,41.326,22.916,41.336,22.93,41.356,22.951,41.606,23.006,41.717,23.004,41.74,22.992,41.757,22.944,41.775,22.909,41.835,22.837,41.994,22.796,42.026,22.682,42.059,22.583,42.105,22.498,42.165,22.344,42.314,22.422,42.329,22.446,42.359,22.524,42.441,22.532,42.481,22.524,42.504,22.472,42.543,22.436,42.629,22.463,42.709,22.466,42.751,22.439,42.792,22.467,42.842,22.523,42.87,22.558,42.878,22.706,42.884,22.8,42.986,22.857,43.018,22.915,43.076,22.942,43.097,22.968,43.142,22.977,43.188,22.86,43.252,22.82,43.301,22.768,43.354,22.697,43.391,22.555,43.454,22.499,43.519,22.474,43.602,22.436,43.665,22.395,43.707,22.387,43.74,22.37,43.781,22.365,43.862,22.399,43.97,22.421,44.007,22.469,44.018,22.597,44.075,22.603,44.149,22.627,44.194,22.667,44.22,22.705,44.238,22.775,44.195,22.945,44.127,23.029,44.078,23.024,44.047,22.985,44.017,22.911,43.987,22.868,43.948,22.856,43.899,22.868,43.865,22.919,43.834,23.225,43.874,23.535,43.854,23.951,43.787,24.227,43.763,24.431,43.794,24.808,43.738,25.16,43.686,25.497,43.671,25.686,43.712,25.819,43.767,25.933,43.871,26.216,44.007,26.489,44.084,26.848,44.146,27.087,44.167,27.121,44.146,27.425,44.021,27.561,44.02,27.671,43.998,27.711,43.965,27.739,43.956,27.884,43.987,27.949,43.919,28.05,43.822,28.222,43.773,28.375,43.745,28.423,43.74,28.585,43.742,28.562,43.501,28.465,43.389,28.32,43.427,28.134,43.396,28.035,43.268,27.979,43.231,27.929,43.186,27.896,43.021,27.889,42.75,27.818,42.717,27.754,42.707,27.485,42.468,27.64,42.401,27.708,42.35,27.821,42.208,27.983,42.047,28.014,41.969]]]},{"n":"Brunei","c":"BN","r":6,"ml":4,"lx":114.55,"ly":4.45,"b":[114.064,4.024,115.327,5.022],"p":[[[115.14,4.9,115.168,4.867,115.228,4.751,115.267,4.634,115.279,4.456,115.327,4.381,115.319,4.365,115.291,4.353,115.247,4.347,115.171,4.364,115.107,4.39,115.052,4.583,115.027,4.691,115.029,4.821,115.027,4.9,115.14,4.9]],[[115.027,4.9,114.945,4.856,114.865,4.802,114.784,4.755,114.747,4.718,114.76,4.667,114.779,4.553,114.79,4.464,114.818,4.429,114.84,4.393,114.831,4.354,114.783,4.281,114.81,4.267,114.776,4.169,114.725,4.097,114.654,4.038,114.608,4.024,114.572,4.049,114.512,4.114,114.447,4.204,114.417,4.256,114.323,4.263,114.29,4.304,114.288,4.355,114.261,4.414,114.224,4.478,114.169,4.527,114.095,4.565,114.064,4.593,114.178,4.591,114.299,4.607,114.424,4.66,114.545,4.725,114.646,4.798,114.741,4.881,114.841,4.946,114.995,5.022,115.048,5.016,115.047,4.962,115.027,4.9]]]},{"n":"Brazil","c":"BR","r":2,"ml":1.7,"lx":-49.56,"ly":-12.1,"b":[-74.002,-33.742,-34.805,5.258],"p":[[[-66.876,1.223,-66.619,0.992,-66.429,0.822,-66.347,0.767,-66.302,0.752,-66.191,0.763,-66.06,0.785,-65.996,0.81,-65.926,0.863,-65.811,0.937,-65.718,0.978,-65.681,0.983,-65.645,0.97,-65.566,0.926,-65.523,0.843,-65.563,0.748,-65.556,0.688,-65.473,0.691,-65.407,0.79,-65.361,0.869,-65.264,0.932,-65.17,1.022,-65.104,1.108,-65.027,1.158,-64.91,1.22,-64.818,1.257,-64.732,1.253,-64.667,1.294,-64.584,1.37,-64.526,1.431,-64.486,1.453,-64.405,1.447,-64.304,1.455,-64.205,1.529,-64.115,1.619,-64.067,1.771,-64.035,1.904,-64.008,1.932,-63.976,1.953,-63.937,1.967,-63.844,1.977,-63.682,2.048,-63.57,2.121,-63.464,2.136,-63.433,2.156,-63.394,2.223,-63.375,2.34,-63.389,2.412,-63.585,2.434,-63.713,2.434,-63.924,2.452,-64.025,2.482,-64.047,2.502,-64.049,2.525,-64.029,2.576,-64.009,2.672,-64.038,2.802,-64.144,3.005,-64.219,3.205,-64.229,3.344,-64.227,3.491,-64.221,3.587,-64.275,3.663,-64.568,3.9,-64.669,4.012,-64.703,4.089,-64.818,4.232,-64.789,4.276,-64.722,4.274,-64.666,4.237,-64.614,4.158,-64.576,4.14,-64.526,4.14,-64.256,4.14,-64.192,4.127,-64.154,4.1,-64.122,4.067,-64.073,3.974,-64.021,3.929,-63.915,3.931,-63.747,3.933,-63.653,3.941,-63.597,3.915,-63.527,3.894,-63.38,3.943,-63.339,3.944,-63.295,3.922,-63.136,3.756,-63.045,3.686,-62.969,3.594,-62.857,3.593,-62.765,3.673,-62.74,3.94,-62.712,4.018,-62.665,4.04,-62.61,4.042,-62.544,4.084,-62.473,4.139,-62.411,4.157,-62.153,4.098,-62.082,4.126,-61.821,4.197,-61.554,4.288,-61.479,4.402,-61.368,4.433,-61.28,4.517,-61.209,4.508,-61.102,4.505,-61.036,4.519,-61.003,4.535,-60.966,4.575,-60.906,4.687,-60.833,4.729,-60.742,4.774,-60.679,4.827,-60.628,4.893,-60.604,4.949,-60.604,4.995,-60.635,5.082,-60.672,5.164,-60.712,5.192,-60.742,5.202,-60.651,5.221,-60.576,5.192,-60.46,5.188,-60.409,5.21,-60.335,5.199,-60.242,5.258,-60.182,5.239,-60.142,5.239,-60.106,5.194,-60.078,5.144,-59.991,5.083,-59.999,4.99,-60.015,4.908,-60.027,4.813,-60.032,4.741,-60.069,4.667,-60.125,4.598,-60.141,4.57,-60.149,4.533,-60.111,4.511,-60.045,4.505,-59.962,4.502,-59.906,4.48,-59.833,4.476,-59.746,4.417,-59.703,4.381,-59.7,4.354,-59.727,4.288,-59.739,4.227,-59.717,4.188,-59.691,4.16,-59.62,4.023,-59.586,3.975,-59.558,3.96,-59.551,3.934,-59.575,3.883,-59.604,3.82,-59.67,3.753,-59.679,3.7,-59.732,3.667,-59.854,3.588,-59.833,3.462,-59.829,3.399,-59.831,3.349,-59.873,3.283,-59.946,3.088,-59.972,2.99,-59.996,2.765,-59.994,2.69,-59.961,2.588,-59.89,2.363,-59.849,2.327,-59.755,2.274,-59.744,2.122,-59.752,1.962,-59.756,1.901,-59.741,1.874,-59.699,1.861,-59.669,1.842,-59.664,1.795,-59.667,1.746,-59.597,1.718,-59.536,1.7,-59.479,1.632,-59.378,1.527,-59.337,1.508,-59.317,1.465,-59.231,1.376,-59.1,1.344,-58.969,1.305,-58.917,1.249,-58.862,1.204,-58.822,1.201,-58.787,1.208,-58.73,1.248,-58.685,1.281,-58.605,1.279,-58.512,1.285,-58.496,1.312,-58.487,1.348,-58.506,1.439,-58.473,1.466,-58.396,1.482,-58.38,1.53,-58.363,1.557,-58.341,1.588,-58.314,1.592,-58.281,1.574,-58.23,1.563,-58.173,1.548,-58.142,1.517,-58.091,1.514,-58.035,1.52,-58.012,1.54,-57.995,1.574,-57.983,1.648,-57.946,1.651,-57.873,1.667,-57.796,1.7,-57.692,1.705,-57.594,1.704,-57.546,1.726,-57.5,1.774,-57.413,1.909,-57.367,1.94,-57.317,1.963,-57.276,1.959,-57.19,1.982,-57.119,2.014,-57.093,2.006,-57.038,1.936,-57.01,1.921,-56.97,1.916,-56.837,1.881,-56.766,1.892,-56.69,1.914,-56.616,1.923,-56.564,1.907,-56.525,1.927,-56.483,1.942,-56.453,1.932,-56.386,1.924,-56.227,1.885,-56.02,1.842,-55.963,1.857,-55.93,1.888,-55.922,1.977,-55.915,2.04,-55.962,2.095,-56.02,2.158,-56.074,2.237,-56.138,2.259,-56.129,2.3,-56.088,2.341,-56.045,2.364,-56.02,2.393,-55.994,2.498,-55.976,2.516,-55.957,2.52,-55.936,2.517,-55.894,2.49,-55.731,2.406,-55.659,2.419,-55.385,2.441,-55.344,2.489,-55.286,2.5,-55.188,2.548,-55.149,2.551,-55.114,2.539,-55.07,2.548,-55.006,2.593,-54.979,2.598,-54.968,2.548,-54.927,2.497,-54.876,2.45,-54.852,2.44,-54.767,2.455,-54.722,2.442,-54.703,2.398,-54.697,2.36,-54.662,2.328,-54.616,2.327,-54.592,2.314,-54.55,2.293,-54.515,2.245,-54.433,2.208,-54.293,2.154,-54.228,2.153,-54.167,2.137,-54.13,2.121,-54.09,2.15,-53.946,2.233,-53.877,2.278,-53.83,2.313,-53.794,2.346,-53.768,2.355,-53.75,2.335,-53.735,2.309,-53.684,2.293,-53.564,2.262,-53.509,2.253,-53.432,2.279,-53.366,2.324,-53.334,2.34,-53.285,2.295,-53.252,2.232,-53.23,2.205,-53.18,2.211,-53.082,2.202,-53.01,2.182,-52.965,2.184,-52.903,2.212,-52.87,2.267,-52.783,2.317,-52.701,2.364,-52.653,2.426,-52.583,2.529,-52.559,2.573,-52.555,2.648,-52.456,2.864,-52.418,2.904,-52.396,2.972,-52.357,3.052,-52.357,3.118,-52.328,3.182,-52.271,3.237,-52.229,3.272,-52.163,3.365,-52.116,3.452,-52,3.647,-51.991,3.702,-51.944,3.735,-51.929,3.777,-51.879,3.829,-51.827,3.87,-51.805,3.93,-51.767,3.993,-51.683,4.04,-51.653,4.061,-51.558,4.234,-51.547,4.311,-51.462,4.314,-51.327,4.225,-51.22,4.094,-51.076,3.672,-51.052,3.282,-50.994,3.078,-50.827,2.652,-50.817,2.573,-50.79,2.478,-50.737,2.377,-50.679,2.21,-50.677,2.179,-50.714,2.134,-50.659,2.131,-50.609,2.104,-50.576,1.999,-50.534,1.927,-50.459,1.83,-50.304,1.798,-50.188,1.786,-50.055,1.731,-49.957,1.66,-49.882,1.42,-49.906,1.269,-49.899,1.163,-49.938,1.121,-50.047,1.052,-50.071,1.015,-50.294,0.836,-50.343,0.751,-50.463,0.637,-50.582,0.421,-50.755,0.223,-50.816,0.173,-50.91,0.161,-50.967,0.13,-51.102,-0.031,-51.283,-0.085,-51.3,-0.179,-51.404,-0.393,-51.496,-0.509,-51.555,-0.549,-51.703,-0.762,-51.722,-0.855,-51.721,-1.018,-51.819,-1.118,-51.922,-1.181,-51.934,-1.32,-51.981,-1.368,-52.02,-1.399,-52.229,-1.362,-52.553,-1.514,-52.664,-1.552,-52.31,-1.56,-52.197,-1.64,-51.948,-1.587,-51.646,-1.394,-51.531,-1.354,-51.297,-1.224,-51.202,-1.137,-51.029,-1.032,-50.992,-0.986,-50.895,-0.938,-50.842,-1,-50.838,-1.039,-50.918,-1.115,-50.897,-1.164,-50.845,-1.226,-50.826,-1.311,-50.819,-1.376,-50.786,-1.49,-50.679,-1.644,-50.675,-1.695,-50.69,-1.762,-50.639,-1.817,-50.586,-1.85,-50.403,-2.016,-50.26,-1.923,-50.173,-1.896,-50.117,-1.858,-49.999,-1.832,-49.903,-1.871,-49.72,-1.926,-49.585,-1.867,-49.314,-1.732,-49.399,-1.972,-49.46,-2.192,-49.507,-2.28,-49.553,-2.52,-49.599,-2.584,-49.637,-2.657,-49.576,-2.631,-49.524,-2.597,-49.458,-2.505,-49.408,-2.344,-49.211,-1.917,-49.155,-1.879,-48.991,-1.83,-48.71,-1.488,-48.6,-1.489,-48.53,-1.567,-48.463,-1.614,-48.446,-1.52,-48.35,-1.482,-48.451,-1.436,-48.468,-1.394,-48.478,-1.324,-48.409,-1.229,-48.45,-1.146,-48.306,-1.04,-48.318,-0.961,-48.266,-0.895,-48.202,-0.828,-48.128,-0.795,-48.115,-0.737,-48.069,-0.714,-48.033,-0.705,-47.961,-0.77,-47.883,-0.693,-47.808,-0.663,-47.774,-0.677,-47.731,-0.71,-47.687,-0.725,-47.651,-0.719,-47.557,-0.67,-47.471,-0.749,-47.419,-0.766,-47.433,-0.722,-47.46,-0.681,-47.439,-0.648,-47.398,-0.627,-47.269,-0.645,-47.201,-0.68,-47.127,-0.745,-47.025,-0.75,-46.944,-0.743,-46.894,-0.78,-46.811,-0.78,-46.77,-0.837,-46.644,-0.916,-46.617,-0.971,-46.516,-0.997,-46.422,-1.03,-46.321,-1.039,-46.219,-1.031,-46.215,-1.1,-46.14,-1.118,-46.045,-1.103,-45.972,-1.187,-45.779,-1.251,-45.645,-1.348,-45.557,-1.331,-45.459,-1.356,-45.353,-1.567,-45.329,-1.717,-45.282,-1.697,-45.239,-1.629,-45.182,-1.507,-45.076,-1.466,-45.026,-1.513,-44.92,-1.589,-44.828,-1.672,-44.79,-1.725,-44.721,-1.733,-44.779,-1.799,-44.721,-1.792,-44.651,-1.746,-44.592,-1.842,-44.547,-1.946,-44.538,-2.053,-44.58,-2.114,-44.617,-2.152,-44.659,-2.228,-44.708,-2.241,-44.756,-2.266,-44.701,-2.32,-44.662,-2.373,-44.579,-2.23,-44.52,-2.19,-44.435,-2.168,-44.391,-2.27,-44.382,-2.366,-44.52,-2.405,-44.521,-2.481,-44.562,-2.524,-44.589,-2.573,-44.611,-2.677,-44.639,-2.762,-44.721,-3.142,-44.723,-3.205,-44.623,-3.138,-44.438,-2.944,-44.381,-2.738,-44.308,-2.535,-44.229,-2.471,-44.179,-2.471,-44.106,-2.493,-44.101,-2.56,-44.113,-2.599,-44.192,-2.7,-44.225,-2.755,-44.193,-2.81,-44.013,-2.642,-43.933,-2.583,-43.864,-2.595,-43.729,-2.518,-43.455,-2.502,-43.435,-2.414,-43.38,-2.376,-43.23,-2.386,-42.937,-2.465,-42.832,-2.53,-42.676,-2.59,-42.594,-2.661,-42.25,-2.792,-42,-2.806,-41.876,-2.747,-41.722,-2.809,-41.64,-2.879,-41.48,-2.917,-41.318,-2.936,-41.195,-2.886,-40.876,-2.87,-40.475,-2.796,-40.235,-2.813,-39.965,-2.862,-39.772,-2.986,-39.609,-3.056,-39.511,-3.126,-39.353,-3.197,-39.014,-3.39,-38.896,-3.502,-38.686,-3.654,-38.476,-3.717,-38.362,-3.876,-38.272,-3.948,-38.049,-4.216,-37.796,-4.404,-37.626,-4.592,-37.301,-4.713,-37.175,-4.912,-36.955,-4.937,-36.861,-4.967,-36.747,-5.051,-36.591,-5.098,-36.387,-5.084,-36.162,-5.094,-35.98,-5.054,-35.549,-5.129,-35.482,-5.166,-35.393,-5.251,-35.235,-5.567,-35.142,-5.917,-35.095,-6.185,-34.988,-6.394,-34.93,-6.785,-34.88,-6.908,-34.876,-7.003,-34.834,-7.024,-34.805,-7.288,-34.817,-7.395,-34.858,-7.533,-34.861,-7.595,-34.855,-7.634,-34.873,-7.692,-34.879,-7.747,-34.837,-7.872,-34.835,-7.971,-34.891,-8.092,-34.967,-8.408,-35.158,-8.931,-35.341,-9.231,-35.597,-9.541,-35.764,-9.703,-35.83,-9.719,-35.891,-9.687,-35.848,-9.772,-35.885,-9.848,-36.055,-10.076,-36.224,-10.225,-36.398,-10.484,-36.412,-10.49,-36.636,-10.59,-36.768,-10.672,-36.938,-10.82,-37.093,-11.055,-37.125,-11.085,-37.183,-11.068,-37.181,-11.187,-37.315,-11.376,-37.356,-11.404,-37.355,-11.35,-37.332,-11.31,-37.321,-11.267,-37.322,-11.215,-37.359,-11.253,-37.438,-11.394,-37.412,-11.497,-37.469,-11.654,-37.689,-12.1,-37.957,-12.475,-38.019,-12.591,-38.24,-12.844,-38.402,-12.966,-38.447,-12.967,-38.499,-12.957,-38.525,-12.762,-38.654,-12.645,-38.691,-12.624,-38.744,-12.749,-38.788,-12.783,-38.852,-12.79,-38.784,-12.844,-38.764,-12.907,-38.833,-13.033,-38.835,-13.147,-38.959,-13.273,-39.031,-13.365,-39.067,-13.48,-39.089,-13.588,-39.035,-13.559,-39.009,-13.581,-38.989,-13.615,-39.001,-13.665,-39.041,-13.758,-39.035,-13.991,-39.048,-14.044,-39.008,-14.101,-38.967,-14.003,-38.942,-14.031,-39.06,-14.655,-39.013,-14.936,-38.996,-15.254,-38.943,-15.564,-38.885,-15.842,-38.881,-15.864,-38.961,-16.187,-39.063,-16.504,-39.125,-16.764,-39.164,-17.044,-39.203,-17.178,-39.215,-17.316,-39.171,-17.642,-39.154,-17.704,-39.278,-17.849,-39.413,-17.92,-39.487,-17.99,-39.651,-18.252,-39.74,-18.64,-39.742,-18.846,-39.7,-19.278,-39.731,-19.454,-39.783,-19.572,-39.845,-19.649,-40.001,-19.742,-40.142,-19.968,-40.203,-20.206,-40.299,-20.293,-40.319,-20.426,-40.396,-20.569,-40.597,-20.784,-40.727,-20.846,-40.789,-20.906,-40.829,-21.031,-40.955,-21.238,-41.047,-21.506,-41.023,-21.597,-41.022,-21.611,-40.988,-21.92,-41,-21.999,-41.123,-22.084,-41.583,-22.244,-41.706,-22.31,-41.98,-22.581,-41.998,-22.645,-41.986,-22.736,-41.941,-22.788,-41.987,-22.845,-42.042,-22.947,-42.122,-22.941,-42.581,-22.941,-42.829,-22.973,-42.958,-22.967,-43.016,-22.943,-43.081,-22.903,-43.101,-22.85,-43.065,-22.771,-43.086,-22.723,-43.154,-22.725,-43.229,-22.748,-43.242,-22.795,-43.237,-22.829,-43.209,-22.878,-43.194,-22.939,-43.224,-22.991,-43.369,-22.998,-43.533,-23.046,-43.737,-23.067,-43.899,-23.101,-43.974,-23.057,-43.899,-23.035,-43.791,-23.046,-43.676,-23.009,-43.703,-22.966,-43.866,-22.911,-44.047,-22.945,-44.148,-23.011,-44.368,-23.005,-44.637,-23.055,-44.681,-23.107,-44.674,-23.207,-44.621,-23.229,-44.57,-23.274,-44.619,-23.316,-44.667,-23.335,-44.952,-23.381,-45.215,-23.576,-45.325,-23.6,-45.423,-23.685,-45.433,-23.758,-45.464,-23.803,-45.527,-23.805,-45.665,-23.765,-45.843,-23.764,-45.972,-23.796,-46.631,-24.11,-46.867,-24.236,-47.137,-24.493,-47.592,-24.781,-47.831,-24.953,-47.877,-24.997,-47.914,-25,-47.989,-25.036,-47.959,-25.065,-47.908,-25.068,-47.929,-25.168,-48.024,-25.237,-48.203,-25.417,-48.242,-25.403,-48.186,-25.31,-48.273,-25.306,-48.402,-25.272,-48.458,-25.311,-48.428,-25.403,-48.476,-25.443,-48.564,-25.447,-48.644,-25.437,-48.732,-25.369,-48.692,-25.492,-48.507,-25.521,-48.43,-25.55,-48.401,-25.597,-48.545,-25.816,-48.666,-25.844,-48.679,-25.875,-48.613,-25.875,-48.576,-25.935,-48.619,-26.179,-48.679,-26.226,-48.714,-26.227,-48.748,-26.269,-48.701,-26.348,-48.652,-26.406,-48.658,-26.519,-48.677,-26.612,-48.678,-26.703,-48.616,-26.878,-48.593,-27.058,-48.568,-27.123,-48.554,-27.196,-48.596,-27.264,-48.572,-27.373,-48.643,-27.558,-48.606,-27.825,-48.621,-28.076,-48.648,-28.207,-48.693,-28.31,-48.797,-28.443,-48.8,-28.575,-49.024,-28.699,-49.271,-28.871,-49.5,-29.075,-49.746,-29.363,-50.033,-29.801,-50.3,-30.426,-50.62,-30.898,-50.748,-31.068,-50.921,-31.258,-51.152,-31.48,-51.46,-31.702,-51.798,-31.9,-51.92,-31.99,-52.039,-32.115,-52.069,-32.063,-52.043,-31.978,-52.06,-31.913,-52.063,-31.83,-51.995,-31.815,-51.893,-31.868,-51.841,-31.832,-51.803,-31.797,-51.681,-31.775,-51.446,-31.557,-51.272,-31.477,-51.174,-31.34,-51.158,-31.267,-51.161,-31.119,-51.106,-31.081,-50.98,-31.094,-50.954,-31.052,-50.965,-31.005,-50.941,-30.904,-50.77,-30.813,-50.689,-30.704,-50.716,-30.426,-50.685,-30.413,-50.615,-30.457,-50.582,-30.439,-50.547,-30.317,-50.564,-30.254,-50.646,-30.237,-50.932,-30.374,-51.025,-30.369,-51.04,-30.261,-51.179,-30.211,-51.234,-30.121,-51.25,-30.06,-51.298,-30.035,-51.295,-30.141,-51.282,-30.244,-51.157,-30.364,-51.188,-30.412,-51.247,-30.468,-51.288,-30.591,-51.283,-30.752,-51.316,-30.703,-51.359,-30.675,-51.376,-30.847,-51.459,-30.913,-51.485,-30.978,-51.464,-31.053,-51.506,-31.104,-51.717,-31.244,-51.927,-31.339,-51.972,-31.384,-51.995,-31.49,-52.027,-31.599,-52.12,-31.695,-52.194,-31.886,-52.192,-31.968,-52.167,-32.088,-52.127,-32.168,-52.19,-32.221,-52.275,-32.324,-52.342,-32.44,-52.508,-32.875,-52.652,-33.138,-52.763,-33.266,-52.921,-33.402,-53.371,-33.742,-53.398,-33.737,-53.464,-33.71,-53.519,-33.677,-53.531,-33.655,-53.538,-33.623,-53.53,-33.5,-53.531,-33.171,-53.512,-33.109,-53.483,-33.069,-53.395,-33.01,-53.31,-32.927,-53.214,-32.821,-53.126,-32.737,-53.157,-32.68,-53.231,-32.625,-53.363,-32.581,-53.489,-32.503,-53.602,-32.403,-53.654,-32.299,-53.701,-32.186,-53.747,-32.097,-53.762,-32.057,-53.806,-32.04,-53.877,-31.995,-53.921,-31.952,-53.985,-31.928,-54.1,-31.902,-54.221,-31.855,-54.37,-31.745,-54.478,-31.623,-54.531,-31.542,-54.588,-31.485,-54.896,-31.391,-55.036,-31.279,-55.091,-31.314,-55.174,-31.28,-55.255,-31.226,-55.279,-31.184,-55.313,-31.142,-55.346,-31.093,-55.366,-31.046,-55.45,-30.964,-55.557,-30.876,-55.603,-30.851,-55.627,-30.858,-55.65,-30.892,-55.665,-30.925,-55.706,-30.947,-55.756,-30.987,-55.808,-31.037,-55.874,-31.07,-55.952,-31.081,-56.005,-31.079,-56.016,-31.06,-56.018,-30.992,-55.999,-30.837,-56.045,-30.778,-56.106,-30.714,-56.176,-30.628,-56.407,-30.447,-56.722,-30.187,-56.833,-30.107,-56.937,-30.101,-57.033,-30.11,-57.121,-30.144,-57.187,-30.265,-57.214,-30.283,-57.384,-30.281,-57.552,-30.261,-57.609,-30.188,-57.564,-30.14,-57.405,-30.034,-57.317,-29.939,-57.301,-29.857,-57.225,-29.782,-57.089,-29.716,-56.939,-29.595,-56.772,-29.418,-56.672,-29.287,-56.636,-29.203,-56.571,-29.138,-56.476,-29.092,-56.393,-28.997,-56.322,-28.852,-56.226,-28.737,-56.103,-28.652,-56.034,-28.581,-56.02,-28.525,-55.985,-28.489,-55.93,-28.473,-55.904,-28.443,-55.905,-28.4,-55.891,-28.37,-55.859,-28.354,-55.806,-28.36,-55.732,-28.387,-55.687,-28.382,-55.672,-28.345,-55.692,-28.303,-55.746,-28.255,-55.725,-28.204,-55.582,-28.121,-55.477,-28.089,-55.41,-28.038,-55.346,-27.956,-55.244,-27.899,-55.102,-27.867,-55.064,-27.836,-55.069,-27.796,-55.04,-27.768,-54.956,-27.747,-54.91,-27.709,-54.903,-27.652,-54.876,-27.599,-54.829,-27.551,-54.777,-27.533,-54.72,-27.545,-54.666,-27.527,-54.615,-27.477,-54.555,-27.454,-54.484,-27.457,-54.448,-27.446,-54.327,-27.424,-54.26,-27.382,-54.205,-27.29,-54.156,-27.254,-54.114,-27.275,-54.04,-27.244,-53.935,-27.161,-53.916,-27.16,-53.838,-27.121,-53.758,-26.978,-53.717,-26.883,-53.727,-26.805,-53.753,-26.749,-53.745,-26.667,-53.718,-26.443,-53.711,-26.352,-53.669,-26.288,-53.671,-26.225,-53.747,-26.084,-53.823,-25.96,-53.864,-25.749,-53.891,-25.669,-53.955,-25.648,-54.012,-25.578,-54.085,-25.572,-54.119,-25.545,-54.155,-25.523,-54.206,-25.53,-54.25,-25.57,-54.332,-25.572,-54.383,-25.589,-54.444,-25.625,-54.502,-25.608,-54.538,-25.576,-54.616,-25.576,-54.611,-25.433,-54.473,-25.22,-54.436,-25.121,-54.454,-25.065,-54.413,-24.867,-54.313,-24.528,-54.281,-24.306,-54.317,-24.201,-54.318,-24.128,-54.267,-24.066,-54.242,-24.047,-54.371,-23.971,-54.44,-23.902,-54.53,-23.852,-54.625,-23.812,-54.672,-23.829,-54.721,-23.852,-54.817,-23.888,-54.926,-23.951,-54.983,-23.975,-55.082,-23.998,-55.194,-24.017,-55.287,-24.004,-55.366,-23.991,-55.416,-23.951,-55.442,-23.865,-55.442,-23.793,-55.459,-23.687,-55.518,-23.627,-55.538,-23.581,-55.542,-23.525,-55.535,-23.462,-55.518,-23.416,-55.528,-23.359,-55.555,-23.32,-55.548,-23.25,-55.561,-23.154,-55.601,-23.095,-55.621,-23.025,-55.621,-22.956,-55.651,-22.886,-55.654,-22.81,-55.628,-22.741,-55.618,-22.671,-55.647,-22.622,-55.704,-22.592,-55.747,-22.513,-55.753,-22.41,-55.8,-22.354,-55.849,-22.308,-55.905,-22.308,-55.991,-22.281,-56.067,-22.284,-56.19,-22.281,-56.246,-22.265,-56.276,-22.228,-56.352,-22.179,-56.395,-22.093,-56.448,-22.076,-56.524,-22.103,-56.55,-22.136,-56.58,-22.182,-56.633,-22.235,-56.702,-22.232,-56.775,-22.261,-56.845,-22.265,-56.937,-22.271,-57.03,-22.245,-57.142,-22.215,-57.238,-22.195,-57.331,-22.215,-57.394,-22.198,-57.476,-22.189,-57.569,-22.182,-57.642,-22.129,-57.721,-22.099,-57.764,-22.109,-57.82,-22.142,-57.88,-22.136,-57.956,-22.109,-57.986,-22.046,-57.979,-22.007,-57.962,-21.967,-57.933,-21.911,-57.949,-21.851,-57.943,-21.798,-57.929,-21.752,-57.916,-21.699,-57.926,-21.65,-57.929,-21.597,-57.936,-21.547,-57.946,-21.494,-57.906,-21.418,-57.873,-21.355,-57.893,-21.302,-57.886,-21.266,-57.86,-21.206,-57.827,-21.134,-57.83,-20.998,-57.86,-20.919,-57.892,-20.897,-57.9,-20.873,-57.885,-20.842,-57.902,-20.809,-57.908,-20.776,-57.891,-20.747,-57.915,-20.69,-57.962,-20.674,-57.979,-20.657,-57.996,-20.594,-58.009,-20.522,-58.002,-20.465,-58.025,-20.416,-58.058,-20.386,-58.092,-20.333,-58.125,-20.293,-58.138,-20.237,-58.16,-20.165,-58.094,-20.151,-58.068,-20.11,-58.021,-20.055,-57.96,-20.041,-57.888,-20.02,-57.861,-19.98,-58.03,-19.833,-58.131,-19.745,-58.072,-19.625,-57.972,-19.424,-57.875,-19.229,-57.8,-19.081,-57.781,-19.054,-57.717,-19.044,-57.729,-18.967,-57.731,-18.917,-57.783,-18.914,-57.725,-18.733,-57.639,-18.475,-57.574,-18.279,-57.553,-18.246,-57.506,-18.237,-57.496,-18.215,-57.552,-18.183,-57.586,-18.122,-57.662,-17.947,-57.78,-17.672,-57.789,-17.573,-57.832,-17.512,-57.905,-17.532,-57.991,-17.513,-58.206,-17.363,-58.348,-17.282,-58.396,-17.234,-58.417,-17.081,-58.46,-16.911,-58.478,-16.701,-58.471,-16.65,-58.35,-16.491,-58.351,-16.41,-58.341,-16.34,-58.346,-16.284,-58.375,-16.284,-58.424,-16.308,-58.497,-16.327,-58.538,-16.328,-58.957,-16.313,-59.434,-16.296,-59.831,-16.282,-60.176,-16.269,-60.187,-16.132,-60.207,-15.902,-60.22,-15.739,-60.242,-15.48,-60.38,-15.318,-60.53,-15.143,-60.583,-15.098,-60.402,-15.093,-60.273,-15.089,-60.299,-14.619,-60.338,-14.571,-60.373,-14.419,-60.396,-14.333,-60.46,-14.263,-60.475,-14.185,-60.463,-14.132,-60.428,-14.1,-60.405,-14.019,-60.422,-13.938,-60.46,-13.862,-60.507,-13.79,-60.595,-13.745,-60.722,-13.664,-60.915,-13.561,-61.077,-13.49,-61.129,-13.499,-61.416,-13.527,-61.512,-13.541,-61.576,-13.525,-61.79,-13.526,-61.874,-13.47,-61.945,-13.406,-62.095,-13.242,-62.118,-13.16,-62.176,-13.134,-62.264,-13.144,-62.353,-13.132,-62.526,-13.064,-62.687,-12.994,-62.765,-12.997,-62.835,-12.954,-62.958,-12.847,-63.015,-12.806,-63.041,-12.75,-63.067,-12.669,-63.117,-12.652,-63.181,-12.666,-63.25,-12.708,-63.347,-12.68,-63.465,-12.605,-63.542,-12.547,-63.586,-12.519,-63.689,-12.478,-63.788,-12.469,-63.939,-12.53,-64.062,-12.505,-64.255,-12.483,-64.421,-12.44,-64.481,-12.326,-64.513,-12.251,-64.612,-12.204,-64.69,-12.146,-64.783,-12.059,-64.83,-12.03,-64.914,-12.006,-64.993,-11.975,-65.001,-11.92,-65.03,-11.847,-65.037,-11.829,-65.09,-11.741,-65.115,-11.735,-65.143,-11.752,-65.163,-11.765,-65.186,-11.75,-65.19,-11.71,-65.175,-11.647,-65.206,-11.581,-65.282,-11.511,-65.322,-11.439,-65.325,-11.365,-65.342,-11.315,-65.373,-11.29,-65.39,-11.246,-65.394,-11.184,-65.372,-11.11,-65.324,-11.025,-65.334,-10.893,-65.402,-10.715,-65.44,-10.586,-65.447,-10.507,-65.437,-10.449,-65.395,-10.392,-65.313,-10.253,-65.299,-10.147,-65.325,-10.027,-65.328,-9.936,-65.309,-9.873,-65.338,-9.79,-65.396,-9.712,-65.437,-9.71,-65.492,-9.732,-65.559,-9.797,-65.637,-9.809,-65.707,-9.768,-65.925,-9.785,-66.264,-9.826,-66.399,-9.868,-66.479,-9.886,-66.575,-9.9,-66.73,-9.975,-67.112,-10.269,-67.19,-10.311,-67.28,-10.317,-67.333,-10.358,-67.417,-10.39,-67.582,-10.506,-67.667,-10.599,-67.722,-10.683,-67.786,-10.686,-67.835,-10.663,-67.992,-10.674,-68.072,-10.703,-68.159,-10.785,-68.267,-10.933,-68.311,-10.975,-68.398,-11.019,-68.498,-11.055,-68.623,-11.109,-68.678,-11.113,-68.727,-11.122,-68.77,-11.098,-68.784,-11.045,-68.848,-11.011,-69.002,-10.994,-69.229,-10.956,-69.463,-10.948,-69.579,-10.952,-69.674,-10.954,-69.84,-10.933,-69.96,-10.93,-70.066,-10.982,-70.22,-11.048,-70.29,-11.064,-70.342,-11.067,-70.392,-11.059,-70.451,-11.025,-70.533,-10.947,-70.597,-10.977,-70.642,-11.01,-70.642,-10.841,-70.64,-10.586,-70.639,-10.361,-70.639,-10.182,-70.638,-9.972,-70.637,-9.824,-70.594,-9.767,-70.567,-9.705,-70.599,-9.621,-70.592,-9.543,-70.57,-9.49,-70.541,-9.437,-70.608,-9.464,-70.637,-9.478,-70.672,-9.518,-70.758,-9.572,-70.816,-9.625,-70.885,-9.669,-70.971,-9.766,-71.042,-9.819,-71.115,-9.852,-71.238,-9.966,-71.339,-9.989,-71.608,-10.006,-71.887,-10.006,-72.143,-10.005,-72.182,-10.004,-72.179,-9.91,-72.173,-9.844,-72.26,-9.774,-72.266,-9.688,-72.289,-9.629,-72.318,-9.557,-72.379,-9.51,-72.465,-9.492,-72.605,-9.452,-72.814,-9.41,-73.014,-9.407,-73.209,-9.411,-73.09,-9.266,-72.97,-9.12,-72.974,-8.993,-73.071,-8.883,-73.123,-8.814,-73.203,-8.719,-73.302,-8.654,-73.357,-8.567,-73.352,-8.514,-73.36,-8.479,-73.398,-8.459,-73.436,-8.427,-73.488,-8.392,-73.549,-8.346,-73.549,-8.299,-73.572,-8.25,-73.61,-8.192,-73.61,-8.145,-73.645,-8.073,-73.683,-8.021,-73.72,-7.986,-73.776,-7.936,-73.773,-7.896,-73.732,-7.875,-73.715,-7.829,-73.72,-7.783,-73.767,-7.754,-73.822,-7.739,-73.895,-7.655,-73.947,-7.611,-73.982,-7.585,-74.002,-7.556,-73.982,-7.536,-73.958,-7.507,-73.953,-7.46,-73.964,-7.417,-73.964,-7.379,-73.929,-7.367,-73.892,-7.373,-73.854,-7.35,-73.805,-7.341,-73.749,-7.335,-73.72,-7.309,-73.723,-7.263,-73.758,-7.173,-73.793,-7.135,-73.805,-7.08,-73.776,-6.974,-73.758,-6.906,-73.695,-6.834,-73.5,-6.679,-73.325,-6.575,-73.24,-6.564,-73.177,-6.525,-73.137,-6.466,-73.126,-6.401,-73.135,-6.344,-73.168,-6.261,-73.206,-6.156,-73.236,-6.098,-73.209,-6.029,-73.163,-5.933,-73.068,-5.79,-72.98,-5.635,-72.97,-5.59,-72.959,-5.495,-72.918,-5.303,-72.896,-5.198,-72.907,-5.158,-72.887,-5.123,-72.832,-5.094,-72.699,-5.067,-72.608,-5.01,-72.469,-4.901,-72.353,-4.786,-72.257,-4.749,-72.083,-4.642,-71.982,-4.575,-71.943,-4.553,-71.845,-4.504,-71.668,-4.487,-71.521,-4.47,-71.438,-4.438,-71.317,-4.424,-71.235,-4.388,-71.144,-4.387,-70.974,-4.35,-70.916,-4.295,-70.866,-4.23,-70.8,-4.173,-70.722,-4.159,-70.635,-4.169,-70.531,-4.168,-70.405,-4.15,-70.344,-4.194,-70.317,-4.247,-70.239,-4.301,-70.184,-4.298,-70.129,-4.287,-70.053,-4.333,-70.004,-4.327,-69.972,-4.301,-69.966,-4.236,-69.948,-4.201,-69.911,-3.997,-69.85,-3.66,-69.794,-3.355,-69.733,-3.017,-69.669,-2.668,-69.605,-2.314,-69.552,-2.024,-69.506,-1.775,-69.479,-1.622,-69.435,-1.422,-69.418,-1.246,-69.4,-1.195,-69.411,-1.152,-69.449,-1.092,-69.449,-1.065,-69.444,-1.03,-69.449,-0.999,-69.488,-0.966,-69.519,-0.946,-69.544,-0.917,-69.555,-0.877,-69.574,-0.838,-69.583,-0.796,-69.612,-0.763,-69.621,-0.721,-69.601,-0.681,-69.592,-0.639,-69.601,-0.6,-69.612,-0.553,-69.634,-0.509,-69.667,-0.482,-69.747,-0.453,-69.828,-0.381,-69.923,-0.317,-70.044,-0.196,-70.071,-0.139,-70.071,0.019,-70.066,0.189,-70.058,0.447,-70.054,0.579,-69.985,0.586,-69.925,0.589,-69.862,0.598,-69.807,0.607,-69.757,0.626,-69.719,0.65,-69.674,0.665,-69.639,0.66,-69.604,0.68,-69.565,0.7,-69.527,0.716,-69.472,0.73,-69.421,0.698,-69.392,0.667,-69.359,0.652,-69.327,0.655,-69.306,0.652,-69.283,0.627,-69.254,0.625,-69.213,0.63,-69.174,0.635,-69.156,0.643,-69.153,0.659,-69.163,0.687,-69.177,0.713,-69.166,0.753,-69.165,0.802,-69.163,0.864,-69.194,0.898,-69.224,0.963,-69.259,1.015,-69.312,1.05,-69.361,1.064,-69.403,1.042,-69.442,1.039,-69.47,1.059,-69.517,1.059,-69.568,1.066,-69.621,1.073,-69.717,1.059,-69.751,1.077,-69.798,1.078,-69.852,1.06,-69.851,1.309,-69.849,1.544,-69.849,1.709,-69.8,1.705,-69.74,1.735,-69.65,1.739,-69.581,1.771,-69.543,1.773,-69.47,1.758,-69.394,1.726,-69.32,1.721,-69.124,1.721,-68.913,1.721,-68.678,1.721,-68.443,1.722,-68.24,1.722,-68.177,1.72,-68.213,1.775,-68.256,1.846,-68.239,1.901,-68.218,1.958,-68.194,1.987,-68.13,1.956,-68.077,1.86,-68.033,1.788,-67.99,1.753,-67.936,1.748,-67.876,1.761,-67.815,1.79,-67.712,1.922,-67.609,2.035,-67.556,2.073,-67.5,2.108,-67.458,2.121,-67.4,2.117,-67.352,2.086,-67.321,2.032,-67.206,1.845,-67.119,1.704,-67.09,1.616,-67.088,1.401,-67.094,1.21,-67.082,1.185,-67.065,1.178,-66.876,1.223]],[[-49.629,-0.229,-49.535,-0.234,-49.403,-0.215,-49.314,-0.168,-49.215,-0.159,-49.117,-0.164,-48.787,-0.216,-48.588,-0.232,-48.515,-0.248,-48.444,-0.272,-48.393,-0.297,-48.38,-0.353,-48.428,-0.442,-48.464,-0.535,-48.497,-0.665,-48.523,-0.691,-48.567,-0.684,-48.54,-0.801,-48.55,-0.848,-48.571,-0.893,-48.624,-0.987,-48.705,-1.107,-48.729,-1.132,-48.79,-1.173,-48.84,-1.227,-48.829,-1.277,-48.804,-1.327,-48.834,-1.39,-48.929,-1.482,-48.986,-1.505,-49.038,-1.514,-49.087,-1.505,-49.173,-1.413,-49.182,-1.485,-49.205,-1.559,-49.234,-1.6,-49.345,-1.595,-49.407,-1.556,-49.507,-1.512,-49.526,-1.63,-49.588,-1.712,-49.651,-1.738,-49.749,-1.755,-49.805,-1.79,-49.911,-1.763,-50.01,-1.708,-50.066,-1.704,-50.109,-1.748,-50.338,-1.756,-50.443,-1.801,-50.508,-1.788,-50.602,-1.698,-50.617,-1.638,-50.673,-1.516,-50.724,-1.371,-50.76,-1.24,-50.729,-1.127,-50.668,-1.131,-50.596,-1.147,-50.581,-1.139,-50.577,-1.103,-50.593,-1.073,-50.71,-1.078,-50.783,-1.01,-50.796,-0.906,-50.781,-0.69,-50.771,-0.645,-50.72,-0.583,-50.703,-0.529,-50.716,-0.47,-50.694,-0.364,-50.646,-0.273,-50.462,-0.157,-50.248,-0.116,-49.629,-0.229]],[[-44.129,-23.142,-44.098,-23.169,-44.156,-23.167,-44.221,-23.191,-44.32,-23.212,-44.36,-23.172,-44.274,-23.116,-44.243,-23.074,-44.22,-23.083,-44.192,-23.113,-44.129,-23.142]],[[-48.584,-26.402,-48.603,-26.414,-48.666,-26.29,-48.54,-26.17,-48.498,-26.219,-48.531,-26.313,-48.568,-26.38,-48.584,-26.402]],[[-45.26,-23.889,-45.261,-23.941,-45.303,-23.915,-45.413,-23.935,-45.451,-23.896,-45.302,-23.728,-45.272,-23.752,-45.249,-23.783,-45.233,-23.825,-45.25,-23.853,-45.26,-23.889]],[[-44.499,-2.94,-44.598,-3.038,-44.565,-2.924,-44.582,-2.846,-44.569,-2.785,-44.502,-2.726,-44.481,-2.718,-44.487,-2.79,-44.483,-2.812,-44.499,-2.94]],[[-38.744,-13.097,-38.783,-13.119,-38.787,-13.055,-38.685,-12.975,-38.668,-12.88,-38.615,-12.924,-38.6,-12.972,-38.601,-12.993,-38.744,-13.097]],[[-38.904,-13.473,-38.938,-13.532,-38.978,-13.524,-38.993,-13.484,-39.022,-13.446,-39.007,-13.416,-38.98,-13.398,-38.907,-13.401,-38.904,-13.473]],[[-44.883,-1.318,-44.947,-1.366,-44.968,-1.391,-45.021,-1.372,-45.011,-1.345,-44.996,-1.348,-44.979,-1.267,-44.888,-1.277,-44.883,-1.318]],[[-49.738,0.268,-49.697,0.216,-49.839,0.007,-49.917,-0.023,-50.002,-0.029,-50.113,0.033,-50.286,0.029,-50.339,0.043,-50.345,0.134,-50.273,0.232,-50.128,0.227,-49.879,0.305,-49.738,0.268]],[[-50.299,1.939,-50.399,1.893,-50.456,1.91,-50.509,2.03,-50.491,2.129,-50.419,2.161,-50.363,2.154,-50.342,2.142,-50.292,1.98,-50.299,1.939]],[[-50.653,-0.132,-50.926,-0.327,-51.019,-0.263,-51.038,-0.226,-51.022,-0.188,-51.026,-0.172,-50.995,-0.105,-50.842,-0.05,-50.765,-0.041,-50.667,-0.058,-50.651,-0.106,-50.653,-0.132]],[[-49.444,-0.112,-49.709,-0.144,-49.83,-0.094,-49.803,-0.052,-49.712,0.015,-49.602,0.063,-49.503,0.084,-49.4,0.057,-49.372,0.001,-49.381,-0.055,-49.444,-0.112]],[[-50.426,0.139,-50.444,-0.008,-50.624,0.054,-50.61,0.205,-50.526,0.247,-50.452,0.327,-50.426,0.425,-50.425,0.558,-50.397,0.581,-50.373,0.591,-50.351,0.582,-50.343,0.382,-50.332,0.259,-50.426,0.139]],[[-50.153,0.393,-50.261,0.359,-50.282,0.391,-50.282,0.517,-50.251,0.585,-50.113,0.605,-50.099,0.625,-50.059,0.638,-50.037,0.595,-50.04,0.523,-50.153,0.393]],[[-51.833,-1.434,-51.938,-1.453,-51.802,-1.203,-51.68,-1.086,-51.678,-0.855,-51.546,-0.65,-51.424,-0.566,-51.254,-0.541,-51.161,-0.667,-51.276,-1.022,-51.31,-1.024,-51.465,-1.211,-51.638,-1.342,-51.833,-1.434]],[[-48.486,-27.767,-48.555,-27.812,-48.542,-27.575,-48.505,-27.496,-48.465,-27.436,-48.415,-27.4,-48.378,-27.451,-48.41,-27.566,-48.497,-27.707,-48.486,-27.767]]]},{"n":"Botswana","c":"BW","r":4,"ml":4,"lx":24.18,"ly":-22.1,"b":[19.977,-26.854,29.365,-17.788],"p":[[[25.259,-17.794,25.239,-17.843,25.224,-17.915,25.242,-17.969,25.282,-18.041,25.34,-18.104,25.384,-18.142,25.437,-18.235,25.489,-18.351,25.558,-18.442,25.761,-18.649,25.784,-18.724,25.812,-18.797,25.939,-18.939,25.959,-18.986,25.951,-19.082,26.082,-19.37,26.168,-19.538,26.241,-19.569,26.475,-19.749,26.678,-19.893,26.917,-19.99,27.092,-20.054,27.178,-20.101,27.221,-20.146,27.257,-20.232,27.275,-20.382,27.281,-20.479,27.469,-20.475,27.625,-20.484,27.679,-20.503,27.7,-20.531,27.695,-20.595,27.697,-20.69,27.704,-20.766,27.688,-20.848,27.677,-20.945,27.669,-21.064,27.693,-21.111,27.844,-21.262,27.907,-21.359,27.975,-21.507,28.014,-21.554,28.046,-21.573,28.182,-21.589,28.532,-21.651,28.748,-21.708,28.919,-21.766,28.991,-21.781,29.026,-21.797,29.037,-21.811,29.016,-21.94,29.023,-21.981,29.042,-22.018,29.071,-22.047,29.107,-22.066,29.237,-22.079,29.315,-22.158,29.365,-22.194,29.13,-22.213,29.013,-22.278,28.946,-22.395,28.84,-22.481,28.696,-22.535,28.543,-22.573,28.382,-22.593,28.21,-22.694,28.028,-22.874,27.935,-22.987,27.931,-23.034,27.891,-23.074,27.813,-23.108,27.769,-23.149,27.758,-23.197,27.717,-23.22,27.644,-23.218,27.593,-23.253,27.563,-23.325,27.499,-23.368,27.399,-23.384,27.313,-23.424,27.241,-23.49,27.186,-23.523,27.146,-23.524,27.086,-23.578,26.987,-23.705,26.971,-23.763,26.835,-24.241,26.761,-24.297,26.618,-24.396,26.502,-24.513,26.452,-24.583,26.397,-24.614,26.131,-24.671,26.032,-24.702,25.912,-24.747,25.882,-24.788,25.852,-24.935,25.77,-25.146,25.703,-25.302,25.659,-25.438,25.584,-25.606,25.518,-25.663,25.444,-25.714,25.346,-25.74,25.213,-25.756,25.092,-25.751,24.999,-25.754,24.869,-25.813,24.748,-25.817,24.556,-25.783,24.4,-25.75,24.331,-25.743,24.193,-25.633,24.104,-25.635,23.97,-25.626,23.894,-25.601,23.823,-25.545,23.671,-25.434,23.521,-25.344,23.389,-25.291,23.266,-25.267,23.149,-25.289,23.058,-25.312,23.022,-25.324,22.951,-25.37,22.879,-25.458,22.819,-25.595,22.796,-25.679,22.729,-25.857,22.64,-26.071,22.598,-26.133,22.549,-26.178,22.471,-26.219,22.218,-26.389,22.091,-26.58,22.011,-26.636,21.915,-26.662,21.833,-26.678,21.788,-26.71,21.738,-26.807,21.695,-26.841,21.646,-26.854,21.501,-26.843,21.455,-26.833,21.071,-26.852,20.954,-26.821,20.871,-26.809,20.74,-26.849,20.685,-26.822,20.641,-26.742,20.62,-26.581,20.627,-26.444,20.698,-26.34,20.757,-26.264,20.815,-26.165,20.823,-26.121,20.811,-26.081,20.799,-25.999,20.793,-25.916,20.711,-25.733,20.609,-25.491,20.473,-25.221,20.431,-25.147,20.345,-25.03,20.029,-24.807,19.98,-24.777,19.98,-24.752,19.98,-24.536,19.98,-24.249,19.98,-23.962,19.979,-23.676,19.979,-23.389,19.979,-23.103,19.978,-22.816,19.978,-22.529,19.978,-22.243,19.977,-22,20.205,-22,20.488,-22,20.823,-22,20.971,-22,20.979,-21.962,20.979,-21.784,20.979,-21.376,20.978,-20.968,20.977,-20.56,20.977,-20.152,20.976,-19.744,20.976,-19.336,20.975,-18.929,20.974,-18.521,20.974,-18.319,21.233,-18.307,21.53,-18.266,22.011,-18.199,22.46,-18.116,22.753,-18.067,23.1,-18.01,23.219,-18,23.252,-18.008,23.299,-18.027,23.46,-18.231,23.56,-18.386,23.581,-18.453,23.6,-18.46,23.647,-18.449,23.7,-18.424,23.864,-18.27,23.898,-18.229,24.003,-18.154,24.129,-18.078,24.244,-18.023,24.359,-17.978,24.412,-17.989,24.475,-18.029,24.531,-18.053,24.792,-17.865,24.909,-17.821,25.216,-17.788,25.259,-17.794]]]},{"n":"Bosnia and Herzegovina","c":"BA","r":5,"ml":4.5,"lx":18.07,"ly":44.09,"b":[15.737,42.56,19.584,45.277],"p":[[[19.349,44.881,19.357,44.859,19.334,44.781,19.292,44.697,19.223,44.61,19.151,44.527,19.132,44.484,19.127,44.415,19.118,44.36,19.128,44.33,19.152,44.303,19.232,44.281,19.339,44.226,19.43,44.154,19.547,44.073,19.584,44.043,19.584,44.011,19.55,43.987,19.449,43.978,19.345,43.985,19.305,43.993,19.268,43.983,19.245,43.965,19.257,43.943,19.364,43.845,19.488,43.704,19.495,43.643,19.48,43.595,19.451,43.562,19.4,43.568,19.36,43.593,19.301,43.592,19.254,43.584,19.194,43.533,19.164,43.535,19.113,43.528,19.08,43.518,19.028,43.533,18.974,43.542,18.951,43.527,18.94,43.497,18.974,43.442,19.037,43.357,19.027,43.292,18.979,43.285,18.935,43.339,18.896,43.348,18.851,43.346,18.749,43.284,18.674,43.231,18.657,43.194,18.63,43.154,18.622,43.125,18.624,43.028,18.488,43.012,18.46,42.998,18.444,42.968,18.455,42.844,18.466,42.777,18.543,42.674,18.546,42.642,18.535,42.62,18.48,42.579,18.454,42.565,18.436,42.56,18.347,42.587,18.304,42.599,18.124,42.691,18.045,42.741,17.919,42.807,17.841,42.845,17.802,42.902,17.74,42.915,17.668,42.897,17.585,42.938,17.643,42.96,17.658,42.98,17.65,43.007,17.625,43.043,17.402,43.199,17.293,43.306,17.275,43.344,17.274,43.446,17.248,43.47,17.085,43.517,16.902,43.649,16.713,43.779,16.688,43.815,16.591,43.913,16.472,44.003,16.378,44.06,16.3,44.125,16.214,44.215,16.17,44.352,16.13,44.474,16.103,44.521,16.049,44.538,15.88,44.682,15.737,44.766,15.738,44.856,15.762,45.008,15.788,45.179,15.823,45.203,15.888,45.216,15.963,45.211,16.028,45.19,16.157,45.072,16.231,45.027,16.293,45.009,16.365,45.058,16.454,45.162,16.531,45.217,16.791,45.197,16.919,45.277,17.125,45.172,17.211,45.156,17.259,45.171,17.324,45.164,17.469,45.133,17.503,45.12,17.546,45.123,17.654,45.163,17.69,45.158,17.813,45.078,17.874,45.077,17.949,45.112,17.996,45.142,18.137,45.119,18.218,45.133,18.285,45.134,18.358,45.121,18.424,45.102,18.488,45.086,18.663,45.077,18.746,45.027,18.779,44.977,18.78,44.947,18.788,44.915,18.836,44.883,18.941,44.865,19.007,44.869,19.042,44.871,19.132,44.9,19.237,44.914,19.313,44.897,19.349,44.881]]]},{"n":"Bolivia","c":"BO","r":3,"ml":3,"lx":-64.59,"ly":-16.67,"b":[-69.646,-22.892,-57.496,-9.71],"p":[[[-69.511,-17.506,-69.511,-17.505,-69.511,-17.46,-69.522,-17.389,-69.564,-17.333,-69.626,-17.294,-69.646,-17.249,-69.625,-17.2,-69.503,-17.105,-69.438,-17.088,-69.421,-17.04,-69.382,-17.001,-69.267,-16.861,-69.2,-16.768,-69.133,-16.713,-69.055,-16.674,-69.021,-16.642,-69.038,-16.543,-69.033,-16.476,-69.006,-16.434,-68.928,-16.389,-68.858,-16.355,-68.843,-16.338,-68.849,-16.313,-68.913,-16.262,-69.046,-16.218,-69.134,-16.222,-69.188,-16.183,-69.218,-16.149,-69.392,-15.737,-69.421,-15.641,-69.419,-15.603,-69.302,-15.399,-69.254,-15.333,-69.172,-15.237,-69.187,-15.199,-69.331,-15.039,-69.375,-14.963,-69.374,-14.887,-69.359,-14.795,-69.276,-14.746,-69.252,-14.671,-69.235,-14.597,-69.199,-14.573,-69.163,-14.531,-69.12,-14.47,-69.053,-14.418,-69.013,-14.377,-69.004,-14.265,-68.972,-14.234,-68.88,-14.199,-68.871,-14.17,-68.892,-14.094,-68.937,-14.015,-68.974,-13.976,-69.023,-13.78,-69.074,-13.683,-69.053,-13.644,-69.018,-13.594,-68.983,-13.496,-68.972,-13.382,-68.981,-12.963,-68.979,-12.88,-68.934,-12.822,-68.868,-12.755,-68.812,-12.73,-68.759,-12.687,-68.763,-12.608,-68.728,-12.561,-68.685,-12.502,-68.819,-12.27,-68.936,-12.067,-69.046,-11.876,-69.174,-11.654,-69.258,-11.509,-69.362,-11.328,-69.454,-11.169,-69.579,-10.952,-69.463,-10.948,-69.229,-10.956,-69.002,-10.994,-68.848,-11.011,-68.784,-11.045,-68.77,-11.098,-68.727,-11.122,-68.678,-11.113,-68.623,-11.109,-68.498,-11.055,-68.398,-11.019,-68.311,-10.975,-68.267,-10.933,-68.159,-10.785,-68.072,-10.703,-67.992,-10.674,-67.835,-10.663,-67.786,-10.686,-67.722,-10.683,-67.667,-10.599,-67.582,-10.506,-67.417,-10.39,-67.333,-10.358,-67.28,-10.317,-67.19,-10.311,-67.112,-10.269,-66.73,-9.975,-66.575,-9.9,-66.479,-9.886,-66.399,-9.868,-66.264,-9.826,-65.925,-9.785,-65.707,-9.768,-65.637,-9.809,-65.559,-9.797,-65.492,-9.732,-65.437,-9.71,-65.396,-9.712,-65.338,-9.79,-65.309,-9.873,-65.328,-9.936,-65.325,-10.027,-65.299,-10.147,-65.313,-10.253,-65.395,-10.392,-65.437,-10.449,-65.447,-10.507,-65.44,-10.586,-65.402,-10.715,-65.334,-10.893,-65.324,-11.025,-65.372,-11.11,-65.394,-11.184,-65.39,-11.246,-65.373,-11.29,-65.342,-11.315,-65.325,-11.365,-65.322,-11.439,-65.282,-11.511,-65.206,-11.581,-65.175,-11.647,-65.19,-11.71,-65.186,-11.75,-65.163,-11.765,-65.143,-11.752,-65.115,-11.735,-65.09,-11.741,-65.037,-11.829,-65.03,-11.847,-65.001,-11.92,-64.993,-11.975,-64.914,-12.006,-64.83,-12.03,-64.783,-12.059,-64.69,-12.146,-64.612,-12.204,-64.513,-12.251,-64.481,-12.326,-64.421,-12.44,-64.255,-12.483,-64.062,-12.505,-63.939,-12.53,-63.788,-12.469,-63.689,-12.478,-63.586,-12.519,-63.542,-12.547,-63.465,-12.605,-63.347,-12.68,-63.25,-12.708,-63.181,-12.666,-63.117,-12.652,-63.067,-12.669,-63.041,-12.75,-63.015,-12.806,-62.958,-12.847,-62.835,-12.954,-62.765,-12.997,-62.687,-12.994,-62.526,-13.064,-62.353,-13.132,-62.264,-13.144,-62.176,-13.134,-62.118,-13.16,-62.095,-13.242,-61.945,-13.406,-61.874,-13.47,-61.79,-13.526,-61.576,-13.525,-61.512,-13.541,-61.416,-13.527,-61.129,-13.499,-61.077,-13.49,-60.915,-13.561,-60.722,-13.664,-60.595,-13.745,-60.507,-13.79,-60.46,-13.862,-60.422,-13.938,-60.405,-14.019,-60.428,-14.1,-60.463,-14.132,-60.475,-14.185,-60.46,-14.263,-60.396,-14.333,-60.373,-14.419,-60.338,-14.571,-60.299,-14.619,-60.273,-15.089,-60.402,-15.093,-60.583,-15.098,-60.53,-15.143,-60.38,-15.318,-60.242,-15.48,-60.22,-15.739,-60.207,-15.902,-60.187,-16.132,-60.176,-16.269,-59.831,-16.282,-59.434,-16.296,-58.957,-16.313,-58.538,-16.328,-58.497,-16.327,-58.424,-16.308,-58.375,-16.284,-58.346,-16.284,-58.341,-16.34,-58.351,-16.41,-58.35,-16.491,-58.471,-16.65,-58.478,-16.701,-58.46,-16.911,-58.417,-17.081,-58.396,-17.234,-58.348,-17.282,-58.206,-17.363,-57.991,-17.513,-57.905,-17.532,-57.832,-17.512,-57.789,-17.573,-57.78,-17.672,-57.662,-17.947,-57.586,-18.122,-57.552,-18.183,-57.496,-18.215,-57.506,-18.237,-57.553,-18.246,-57.574,-18.279,-57.639,-18.475,-57.725,-18.733,-57.783,-18.914,-57.731,-18.917,-57.729,-18.967,-57.717,-19.044,-57.781,-19.054,-57.8,-19.081,-57.875,-19.229,-57.972,-19.424,-58.072,-19.625,-58.131,-19.745,-58.03,-19.833,-57.861,-19.98,-57.888,-20.02,-57.96,-20.041,-58.021,-20.055,-58.068,-20.11,-58.094,-20.151,-58.16,-20.165,-58.14,-19.999,-58.16,-19.855,-58.18,-19.818,-58.474,-19.646,-58.741,-19.49,-59.091,-19.286,-59.541,-19.292,-60.007,-19.298,-60.452,-19.389,-60.889,-19.479,-61.096,-19.521,-61.512,-19.606,-61.757,-19.645,-61.821,-19.809,-61.917,-20.055,-62.012,-20.199,-62.122,-20.35,-62.276,-20.562,-62.277,-20.821,-62.277,-21.066,-62.385,-21.412,-62.478,-21.705,-62.567,-21.989,-62.629,-22.184,-62.651,-22.234,-62.665,-22.218,-62.745,-22.16,-62.815,-22.05,-62.834,-21.999,-62.843,-21.997,-63.267,-22.001,-63.675,-22.004,-63.717,-22.028,-63.776,-22.027,-63.819,-22.005,-63.861,-22.007,-63.922,-22.029,-63.976,-22.073,-64.132,-22.366,-64.209,-22.491,-64.266,-22.603,-64.308,-22.795,-64.325,-22.828,-64.374,-22.761,-64.446,-22.585,-64.478,-22.485,-64.524,-22.372,-64.606,-22.229,-64.7,-22.186,-64.759,-22.171,-64.843,-22.144,-64.993,-22.11,-65.058,-22.103,-65.485,-22.098,-65.519,-22.095,-65.686,-22.11,-65.771,-22.1,-65.86,-22.02,-66.059,-21.879,-66.099,-21.835,-66.175,-21.806,-66.22,-21.803,-66.248,-21.83,-66.282,-21.947,-66.322,-22.053,-66.365,-22.114,-66.507,-22.158,-66.639,-22.205,-66.712,-22.216,-66.751,-22.269,-66.767,-22.343,-66.8,-22.41,-66.991,-22.51,-67.034,-22.552,-67.055,-22.651,-67.162,-22.774,-67.195,-22.822,-67.362,-22.855,-67.58,-22.892,-67.707,-22.889,-67.794,-22.879,-67.821,-22.858,-67.879,-22.823,-67.889,-22.784,-67.89,-22.729,-67.874,-22.631,-67.882,-22.493,-67.95,-22.334,-67.945,-22.282,-67.954,-22.204,-67.988,-22.057,-68.077,-21.983,-68.102,-21.861,-68.112,-21.753,-68.186,-21.619,-68.199,-21.447,-68.197,-21.3,-68.314,-21.13,-68.435,-20.948,-68.534,-20.924,-68.558,-20.902,-68.569,-20.85,-68.571,-20.769,-68.563,-20.72,-68.487,-20.641,-68.484,-20.628,-68.5,-20.612,-68.696,-20.493,-68.745,-20.459,-68.761,-20.416,-68.759,-20.378,-68.712,-20.339,-68.689,-20.31,-68.735,-20.225,-68.73,-20.148,-68.759,-20.116,-68.756,-20.091,-68.727,-20.07,-68.6,-20.045,-68.561,-19.967,-68.559,-19.902,-68.578,-19.857,-68.696,-19.741,-68.698,-19.721,-68.575,-19.56,-68.487,-19.454,-68.463,-19.433,-68.47,-19.41,-68.492,-19.382,-68.548,-19.341,-68.621,-19.297,-68.681,-19.242,-68.759,-19.162,-68.858,-19.093,-68.931,-19.025,-68.968,-18.968,-68.969,-18.91,-68.979,-18.813,-69.027,-18.656,-69.039,-18.55,-69.06,-18.433,-69.081,-18.357,-69.092,-18.282,-69.126,-18.202,-69.145,-18.144,-69.118,-18.103,-69.09,-18.071,-69.094,-18.05,-69.282,-17.965,-69.313,-17.943,-69.358,-17.772,-69.495,-17.62,-69.511,-17.506]]]},{"n":"Bhutan","c":"BT","r":5,"ml":4,"lx":90.04,"ly":27.54,"b":[88.739,26.702,92.083,28.311],"p":[[[91.632,27.76,91.626,27.737,91.598,27.677,91.579,27.611,91.595,27.558,91.658,27.494,91.743,27.443,91.851,27.439,91.951,27.458,91.991,27.45,92.045,27.365,92.083,27.291,92.031,27.214,92.003,27.147,91.992,27.1,91.999,27.079,92.031,27.041,92.068,26.975,92.073,26.915,92.05,26.875,91.998,26.855,91.944,26.861,91.899,26.86,91.842,26.853,91.754,26.831,91.672,26.802,91.518,26.807,91.456,26.867,91.427,26.867,91.287,26.79,91.134,26.803,90.856,26.778,90.74,26.772,90.62,26.78,90.56,26.797,90.448,26.851,90.346,26.89,90.242,26.854,90.206,26.848,90.123,26.755,89.943,26.724,89.764,26.702,89.711,26.714,89.61,26.719,89.606,26.741,89.609,26.762,89.586,26.779,89.545,26.796,89.475,26.803,89.384,26.827,89.332,26.849,89.148,26.816,89.041,26.865,88.919,26.932,88.858,26.961,88.835,27.066,88.814,27.099,88.766,27.134,88.739,27.176,88.76,27.218,88.882,27.297,88.891,27.316,88.948,27.464,89.025,27.518,89.102,27.593,89.16,27.711,89.273,27.833,89.396,27.958,89.481,28.06,89.537,28.107,89.653,28.158,89.75,28.188,89.817,28.256,89.898,28.294,89.981,28.311,90.104,28.302,90.221,28.278,90.348,28.244,90.363,28.217,90.352,28.168,90.334,28.119,90.333,28.094,90.353,28.08,90.477,28.071,90.63,28.079,90.716,28.072,90.907,28.027,90.963,27.995,91.021,27.97,91.078,27.974,91.15,28.027,91.226,28.071,91.273,28.078,91.307,28.064,91.368,28.022,91.493,27.982,91.606,27.952,91.642,27.923,91.629,27.801,91.632,27.76]]]},{"n":"Benin","c":"BJ","r":5,"ml":4,"lx":2.35,"ly":10.32,"b":[0.763,6.217,3.834,12.384],"p":[[[1.623,6.217,1.611,6.251,1.778,6.295,1.743,6.426,1.639,6.582,1.599,6.61,1.578,6.687,1.603,6.738,1.591,6.772,1.582,6.877,1.531,6.992,1.625,6.997,1.625,7.369,1.625,7.726,1.625,8.03,1.625,8.271,1.607,8.559,1.604,8.771,1.6,9.05,1.566,9.137,1.424,9.285,1.386,9.362,1.379,9.463,1.347,9.568,1.345,9.75,1.343,9.963,1.33,9.997,1.176,10.098,0.958,10.242,0.792,10.352,0.78,10.36,0.763,10.387,0.788,10.71,0.822,10.753,0.875,10.886,0.9,10.993,0.925,10.993,0.958,11.028,0.985,11.079,1.014,11.068,1.062,11.058,1.085,11.076,1.082,11.116,1.098,11.156,1.136,11.174,1.146,11.21,1.146,11.252,1.179,11.263,1.235,11.261,1.28,11.274,1.317,11.295,1.365,11.379,1.392,11.408,1.4,11.429,1.427,11.447,1.501,11.456,1.561,11.449,1.6,11.401,1.858,11.443,1.98,11.418,2.231,11.629,2.287,11.691,2.363,11.84,2.389,11.897,2.413,11.999,2.363,12.188,2.366,12.222,2.469,12.263,2.598,12.294,2.648,12.297,2.681,12.313,2.729,12.354,2.805,12.384,2.85,12.374,2.878,12.368,3.15,12.118,3.267,11.992,3.299,11.927,3.36,11.88,3.45,11.852,3.532,11.787,3.595,11.696,3.554,11.632,3.491,11.499,3.488,11.395,3.639,11.177,3.656,11.155,3.695,11.12,3.716,11.08,3.734,10.972,3.745,10.85,3.757,10.769,3.83,10.654,3.834,10.607,3.784,10.436,3.772,10.418,3.758,10.413,3.68,10.428,3.647,10.409,3.604,10.351,3.578,10.292,3.577,10.268,3.646,10.16,3.602,10.005,3.557,9.907,3.477,9.852,3.405,9.839,3.354,9.813,3.325,9.778,3.329,9.667,3.223,9.566,3.165,9.495,3.136,9.452,3.148,9.321,3.11,9.188,3.045,9.084,2.898,9.061,2.775,9.049,2.733,8.783,2.735,8.614,2.724,8.442,2.703,8.372,2.712,8.273,2.702,8.05,2.686,7.874,2.708,7.827,2.72,7.723,2.719,7.616,2.751,7.542,2.785,7.477,2.784,7.443,2.766,7.423,2.75,7.395,2.751,7.143,2.757,7.068,2.748,7.02,2.721,6.98,2.732,6.853,2.753,6.772,2.775,6.712,2.754,6.662,2.736,6.596,2.708,6.428,2.706,6.369,2.287,6.328,1.818,6.261,1.623,6.217]]]},{"n":"Belize","c":"BZ","r":6,"ml":5,"lx":-88.71,"ly":17.2,"b":[-89.237,15.889,-87.789,18.482],"p":[[[-89.161,17.815,-89.162,17.902,-89.134,17.971,-89.05,18,-88.943,17.94,-88.898,17.915,-88.857,17.929,-88.806,17.966,-88.744,18.072,-88.586,18.291,-88.523,18.446,-88.461,18.477,-88.372,18.482,-88.296,18.472,-88.349,18.359,-88.296,18.344,-88.247,18.355,-88.13,18.351,-88.085,18.226,-88.097,18.122,-88.207,17.846,-88.221,17.751,-88.272,17.61,-88.203,17.517,-88.267,17.393,-88.289,17.313,-88.294,17.192,-88.262,16.963,-88.313,16.633,-88.405,16.489,-88.461,16.434,-88.562,16.29,-88.695,16.248,-88.879,16.017,-88.912,15.956,-88.894,15.891,-88.937,15.89,-89.114,15.901,-89.233,15.889,-89.237,15.894,-89.228,16.143,-89.212,16.527,-89.201,16.809,-89.19,17.085,-89.182,17.291,-89.171,17.572,-89.161,17.815]],[[-87.951,17.925,-87.998,17.906,-87.959,17.964,-87.953,18.001,-87.898,18.155,-87.859,18.154,-87.849,18.14,-87.951,17.925]],[[-87.853,17.423,-87.93,17.283,-87.935,17.323,-87.903,17.426,-87.859,17.463,-87.833,17.501,-87.826,17.546,-87.789,17.524,-87.798,17.48,-87.853,17.423]]]},{"n":"Belgium","c":"BE","r":2,"ml":4,"lx":4.8,"ly":50.79,"b":[2.525,49.511,6.364,51.491],"p":[[[4.226,51.386,4.304,51.362,4.374,51.356,4.404,51.367,4.385,51.428,4.441,51.46,4.503,51.475,4.532,51.449,4.589,51.422,4.634,51.422,4.756,51.491,4.784,51.477,4.811,51.453,4.816,51.433,4.821,51.412,4.848,51.403,4.944,51.408,4.993,51.445,5.031,51.469,5.059,51.453,5.073,51.407,5.1,51.346,5.214,51.279,5.311,51.26,5.43,51.273,5.477,51.285,5.509,51.275,5.54,51.239,5.609,51.198,5.752,51.169,5.796,51.153,5.827,51.126,5.818,51.086,5.75,50.989,5.741,50.96,5.75,50.95,5.737,50.932,5.648,50.867,5.639,50.844,5.669,50.806,5.694,50.775,5.747,50.76,5.797,50.755,5.892,50.753,5.994,50.75,6.006,50.732,6.119,50.679,6.154,50.637,6.236,50.597,6.168,50.545,6.179,50.523,6.203,50.499,6.295,50.485,6.341,50.452,6.344,50.4,6.364,50.316,6.175,50.233,6.121,50.139,6.117,50.121,6.11,50.124,6.089,50.155,6.055,50.154,5.976,50.167,5.867,50.083,5.817,50.013,5.788,49.961,5.744,49.92,5.735,49.876,5.741,49.857,5.726,49.833,5.725,49.808,5.788,49.759,5.804,49.732,5.88,49.645,5.857,49.613,5.838,49.578,5.815,49.554,5.79,49.538,5.71,49.539,5.61,49.528,5.542,49.511,5.507,49.511,5.435,49.554,5.354,49.62,5.302,49.651,5.279,49.678,5.215,49.689,5.124,49.721,5.061,49.757,5.007,49.778,4.931,49.789,4.868,49.788,4.849,49.847,4.842,49.915,4.79,49.96,4.861,50.136,4.819,50.153,4.773,50.139,4.707,50.097,4.675,50.047,4.656,50.002,4.545,49.96,4.369,49.945,4.176,49.96,4.149,49.972,4.137,49.984,4.137,50,4.15,50.024,4.184,50.053,4.192,50.094,4.158,50.13,4.135,50.144,4.144,50.178,4.17,50.222,4.175,50.246,4.044,50.321,3.95,50.336,3.858,50.339,3.789,50.347,3.748,50.344,3.719,50.322,3.689,50.306,3.667,50.325,3.627,50.457,3.595,50.477,3.477,50.499,3.316,50.507,3.273,50.532,3.25,50.591,3.235,50.663,3.182,50.732,3.155,50.749,3.107,50.779,3.023,50.767,2.922,50.727,2.862,50.716,2.84,50.712,2.759,50.751,2.669,50.811,2.597,50.876,2.579,50.912,2.601,50.955,2.575,50.989,2.536,51.05,2.525,51.097,2.96,51.265,3.225,51.352,3.35,51.378,3.38,51.291,3.403,51.264,3.433,51.246,3.472,51.242,3.517,51.264,3.58,51.286,3.682,51.276,3.756,51.255,3.782,51.233,3.831,51.213,3.902,51.208,4.04,51.247,4.173,51.307,4.211,51.349,4.226,51.386]]]},{"n":"Belarus","c":"BY","r":4,"ml":3,"lx":28.42,"ly":53.82,"b":[23.175,51.265,32.71,56.146],"p":[[[31.763,52.101,31.574,52.108,31.346,52.105,31.218,52.05,31.168,52.063,31.079,52.077,30.981,52.046,30.846,51.953,30.755,51.895,30.667,51.814,30.639,51.77,30.584,51.689,30.533,51.596,30.561,51.531,30.602,51.471,30.612,51.406,30.633,51.355,30.577,51.318,30.545,51.265,30.45,51.274,30.333,51.326,30.309,51.4,30.22,51.451,30.161,51.478,30.064,51.482,29.909,51.458,29.706,51.44,29.553,51.435,29.47,51.408,29.346,51.383,29.299,51.413,29.23,51.497,29.174,51.581,29.136,51.617,29.102,51.628,29.061,51.625,29.013,51.599,28.978,51.572,28.928,51.562,28.85,51.54,28.793,51.51,28.731,51.433,28.69,51.439,28.648,51.457,28.599,51.543,28.532,51.562,28.425,51.564,28.292,51.582,28.184,51.608,28.144,51.602,28.08,51.565,28.011,51.56,27.859,51.592,27.829,51.577,27.789,51.529,27.741,51.483,27.7,51.478,27.677,51.49,27.69,51.572,27.601,51.602,27.452,51.606,27.348,51.594,27.296,51.597,27.27,51.614,27.142,51.752,27.074,51.761,26.953,51.754,26.773,51.771,26.567,51.802,26.453,51.813,26.394,51.844,26.267,51.855,25.925,51.914,25.786,51.924,25.58,51.925,25.267,51.938,25.067,51.931,24.974,51.911,24.866,51.899,24.685,51.888,24.611,51.89,24.495,51.883,24.362,51.868,24.324,51.838,24.28,51.775,24.127,51.665,23.978,51.591,23.951,51.585,23.864,51.624,23.792,51.637,23.707,51.641,23.647,51.629,23.609,51.61,23.614,51.525,23.605,51.518,23.54,51.619,23.545,51.71,23.581,51.762,23.626,51.809,23.607,51.88,23.651,51.973,23.652,52.04,23.633,52.07,23.598,52.103,23.501,52.14,23.458,52.17,23.327,52.208,23.197,52.257,23.175,52.287,23.181,52.307,23.204,52.338,23.303,52.428,23.411,52.516,23.48,52.552,23.845,52.664,23.901,52.704,23.915,52.77,23.916,52.819,23.909,52.905,23.887,53.028,23.859,53.112,23.789,53.271,23.599,53.599,23.485,53.94,23.559,53.92,23.734,53.912,23.873,53.936,23.944,53.939,24.008,53.932,24.104,53.945,24.191,53.95,24.237,53.92,24.318,53.893,24.479,53.932,24.621,53.98,24.768,53.975,24.789,53.998,24.826,54.119,24.87,54.145,25.046,54.133,25.111,54.155,25.179,54.214,25.284,54.251,25.371,54.251,25.461,54.293,25.506,54.265,25.527,54.215,25.497,54.175,25.51,54.16,25.573,54.14,25.681,54.14,25.749,54.157,25.765,54.18,25.765,54.221,25.748,54.26,25.703,54.293,25.617,54.31,25.558,54.311,25.547,54.332,25.568,54.377,25.62,54.46,25.685,54.536,25.725,54.564,25.732,54.59,25.724,54.636,25.722,54.718,25.781,54.833,25.859,54.919,25.964,54.947,26.093,54.962,26.175,55.003,26.216,55.05,26.231,55.09,26.251,55.125,26.292,55.14,26.601,55.13,26.648,55.204,26.675,55.225,26.734,55.247,26.776,55.273,26.76,55.293,26.681,55.306,26.495,55.318,26.458,55.342,26.47,55.372,26.519,55.448,26.567,55.546,26.591,55.623,26.594,55.668,26.62,55.68,26.772,55.694,26.822,55.709,26.953,55.813,27.053,55.831,27.309,55.804,27.427,55.806,27.459,55.804,27.577,55.799,27.589,55.809,27.642,55.912,27.694,55.942,27.896,56.076,28.032,56.133,28.118,56.146,28.148,56.143,28.284,56.056,28.316,56.053,28.392,56.087,28.407,56.089,28.564,56.092,28.637,56.062,28.691,56.003,28.741,55.955,28.795,55.943,28.947,56.002,29.032,56.022,29.087,56.021,29.283,55.968,29.375,55.939,29.396,55.912,29.398,55.881,29.373,55.835,29.353,55.784,29.413,55.725,29.482,55.685,29.63,55.751,29.685,55.77,29.744,55.77,29.824,55.795,29.882,55.832,29.937,55.845,30.043,55.836,30.234,55.845,30.456,55.787,30.475,55.769,30.587,55.7,30.626,55.666,30.662,55.655,30.722,55.622,30.801,55.601,30.856,55.608,30.882,55.596,30.907,55.57,30.909,55.525,30.901,55.397,30.862,55.36,30.821,55.33,30.811,55.307,30.814,55.279,30.877,55.223,30.959,55.138,30.978,55.088,30.978,55.05,30.867,54.941,30.83,54.915,30.804,54.861,30.791,54.806,30.799,54.783,30.984,54.696,31.121,54.648,31.152,54.625,31.155,54.611,31.082,54.517,31.075,54.492,31.185,54.453,31.246,54.392,31.299,54.292,31.404,54.196,31.628,54.111,31.792,54.056,31.826,54.031,31.838,54.001,31.825,53.935,31.783,53.855,31.754,53.81,31.821,53.792,31.992,53.797,32.2,53.781,32.45,53.693,32.451,53.653,32.425,53.617,32.442,53.579,32.47,53.547,32.686,53.448,32.706,53.419,32.71,53.371,32.704,53.336,32.644,53.329,32.578,53.312,32.469,53.27,32.426,53.211,32.251,53.128,32.142,53.091,32.055,53.089,31.85,53.106,31.777,53.147,31.747,53.184,31.668,53.201,31.563,53.202,31.418,53.196,31.388,53.185,31.365,53.139,31.303,53.061,31.259,53.017,31.295,52.99,31.353,52.933,31.443,52.862,31.535,52.798,31.565,52.759,31.563,52.731,31.519,52.699,31.526,52.633,31.616,52.546,31.586,52.532,31.577,52.426,31.577,52.312,31.602,52.285,31.65,52.262,31.691,52.221,31.759,52.126,31.763,52.101]]]},{"n":"Barbados","c":"BB","r":5,"ml":4.5,"lx":-59.57,"ly":13.16,"b":[-59.647,13.062,-59.428,13.318],"p":[[[-59.493,13.082,-59.522,13.062,-59.611,13.102,-59.643,13.15,-59.647,13.303,-59.592,13.318,-59.488,13.197,-59.428,13.153,-59.493,13.082]]]},{"n":"Bangladesh","c":"BD","r":3,"ml":3,"lx":89.68,"ly":24.21,"b":[88.023,20.79,92.632,26.572],"p":[[[89.051,22.093,89.056,22.186,89.05,22.275,88.971,22.511,88.921,22.632,88.927,22.671,88.923,22.688,88.9,22.844,88.867,22.939,88.851,23.041,88.928,23.187,88.897,23.21,88.808,23.23,88.724,23.255,88.704,23.293,88.741,23.437,88.698,23.493,88.636,23.55,88.616,23.573,88.596,23.602,88.567,23.674,88.623,23.826,88.7,24.003,88.714,24.07,88.727,24.186,88.734,24.231,88.724,24.275,88.642,24.326,88.499,24.347,88.397,24.389,88.338,24.454,88.287,24.48,88.225,24.461,88.146,24.486,88.079,24.55,88.023,24.628,88.03,24.664,88.045,24.713,88.15,24.915,88.189,24.921,88.279,24.882,88.313,24.882,88.373,24.962,88.456,25.188,88.574,25.188,88.678,25.18,88.748,25.169,88.817,25.176,88.89,25.194,88.93,25.223,88.952,25.259,88.944,25.291,88.855,25.334,88.82,25.366,88.795,25.456,88.769,25.49,88.593,25.495,88.502,25.537,88.452,25.574,88.363,25.698,88.253,25.79,88.147,25.811,88.107,25.841,88.085,25.888,88.097,25.956,88.129,26.018,88.151,26.087,88.235,26.178,88.334,26.258,88.378,26.312,88.44,26.369,88.448,26.401,88.437,26.437,88.386,26.472,88.351,26.483,88.346,26.505,88.37,26.564,88.418,26.572,88.518,26.518,88.62,26.431,88.681,26.353,88.683,26.292,88.722,26.282,88.762,26.279,88.828,26.252,88.896,26.26,88.941,26.245,88.97,26.251,88.982,26.286,88.948,26.338,88.924,26.375,88.952,26.412,88.983,26.42,89.019,26.41,89.067,26.377,89.102,26.308,89.108,26.202,89.186,26.106,89.289,26.038,89.37,26.006,89.467,25.984,89.55,26.005,89.591,26.072,89.573,26.132,89.586,26.186,89.619,26.216,89.671,26.214,89.71,26.171,89.823,25.941,89.8,25.84,89.825,25.56,89.796,25.376,89.801,25.336,89.814,25.305,89.833,25.293,89.866,25.293,90.004,25.258,90.12,25.22,90.25,25.185,90.439,25.158,90.555,25.167,90.613,25.168,90.73,25.159,91.038,25.174,91.293,25.178,91.397,25.152,91.48,25.142,91.763,25.161,92.05,25.169,92.205,25.111,92.373,25.015,92.468,24.944,92.485,24.903,92.475,24.869,92.443,24.849,92.385,24.849,92.251,24.895,92.228,24.881,92.231,24.786,92.227,24.771,92.198,24.686,92.117,24.494,92.102,24.408,92.085,24.386,92.064,24.374,92.001,24.371,91.952,24.357,91.931,24.326,91.899,24.261,91.877,24.195,91.846,24.175,91.772,24.211,91.727,24.205,91.669,24.19,91.611,24.153,91.571,24.107,91.526,24.091,91.393,24.1,91.367,24.094,91.35,24.06,91.336,24.019,91.232,23.92,91.192,23.763,91.16,23.661,91.166,23.581,91.254,23.374,91.315,23.104,91.339,23.077,91.359,23.068,91.369,23.075,91.367,23.13,91.371,23.198,91.399,23.214,91.436,23.2,91.471,23.141,91.511,23.034,91.554,22.992,91.62,22.98,91.695,23.005,91.751,23.054,91.774,23.106,91.758,23.21,91.754,23.287,91.79,23.361,91.919,23.471,91.938,23.505,91.929,23.598,91.93,23.686,91.979,23.692,92.044,23.678,92.127,23.721,92.152,23.722,92.187,23.676,92.246,23.684,92.289,23.492,92.334,23.324,92.334,23.242,92.341,23.07,92.362,22.929,92.393,22.897,92.43,22.822,92.464,22.734,92.491,22.685,92.51,22.526,92.532,22.41,92.561,22.048,92.575,21.978,92.583,21.94,92.584,21.609,92.593,21.467,92.625,21.351,92.632,21.306,92.6,21.27,92.569,21.263,92.539,21.32,92.472,21.363,92.373,21.409,92.331,21.44,92.28,21.428,92.208,21.358,92.18,21.293,92.192,21.202,92.215,21.113,92.264,21.061,92.268,21.005,92.286,20.932,92.312,20.864,92.324,20.792,92.308,20.79,92.248,20.884,92.195,20.984,92.056,21.175,92.011,21.516,92.008,21.685,91.913,21.883,91.85,22.157,91.825,22.229,91.858,22.317,91.863,22.35,91.845,22.343,91.797,22.297,91.734,22.407,91.693,22.505,91.53,22.708,91.482,22.797,91.48,22.885,91.41,22.797,91.314,22.735,91.216,22.642,91.151,22.614,90.946,22.597,90.827,22.721,90.656,23.025,90.634,23.094,90.656,23.273,90.616,23.442,90.616,23.532,90.604,23.591,90.573,23.578,90.562,23.537,90.568,23.474,90.556,23.422,90.408,23.432,90.269,23.456,90.392,23.367,90.523,23.346,90.591,23.266,90.599,23.204,90.595,23.134,90.528,23.085,90.466,23.054,90.478,22.987,90.552,22.905,90.462,22.882,90.437,22.828,90.435,22.752,90.481,22.685,90.498,22.635,90.487,22.589,90.532,22.539,90.596,22.436,90.616,22.362,90.589,22.258,90.553,22.218,90.494,22.179,90.356,22.048,90.288,21.899,90.231,21.83,90.159,21.817,90.131,21.847,90.071,21.887,90.07,21.96,90.088,22.017,90.21,22.157,90.143,22.138,90.069,22.098,89.954,22.023,89.918,22.116,89.894,22.203,89.894,22.308,89.985,22.466,89.882,22.388,89.853,22.289,89.866,22.173,89.853,22.091,89.812,21.983,89.757,21.919,89.668,21.878,89.628,21.814,89.569,21.767,89.567,21.861,89.547,21.984,89.483,22.276,89.469,22.213,89.503,22.032,89.501,21.914,89.452,21.821,89.354,21.721,89.279,21.707,89.234,21.722,89.167,21.784,89.094,21.873,89.082,22.015,89.051,22.093]],[[91.151,22.175,91.045,22.105,91.079,22.52,91.158,22.365,91.178,22.283,91.151,22.175]],[[91.557,22.382,91.51,22.353,91.467,22.378,91.411,22.476,91.439,22.599,91.456,22.617,91.484,22.577,91.523,22.491,91.548,22.425,91.557,22.382]],[[90.778,22.089,90.604,22.054,90.515,22.065,90.68,22.327,90.675,22.445,90.649,22.541,90.565,22.618,90.56,22.673,90.523,22.748,90.503,22.835,90.596,22.864,90.672,22.813,90.683,22.785,90.699,22.714,90.737,22.639,90.868,22.485,90.866,22.391,90.83,22.16,90.778,22.089]],[[91.874,21.832,91.838,21.75,91.82,21.81,91.835,21.885,91.851,21.927,91.861,21.927,91.883,21.884,91.874,21.832]],[[91.949,21.508,91.889,21.503,91.859,21.533,91.873,21.574,91.857,21.709,91.908,21.723,91.934,21.722,91.949,21.683,91.962,21.61,91.949,21.508]],[[90.642,22.963,90.66,22.92,90.604,22.946,90.562,22.975,90.536,23.015,90.58,23.035,90.642,22.963]]]},{"n":"Bahrain","c":"BH","r":4,"ml":4,"lx":50.55,"ly":26.06,"b":[50.452,25.807,50.617,26.246],"p":[[[50.607,25.883,50.575,25.807,50.544,25.833,50.466,25.966,50.489,26.058,50.452,26.191,50.47,26.229,50.564,26.246,50.586,26.241,50.558,26.198,50.61,26.124,50.617,26.002,50.607,25.883]]]},{"n":"The Bahamas","c":"BS","r":4,"ml":4,"lx":-77.15,"ly":26.4,"b":[-78.986,20.937,-72.747,26.94],"p":[[[-77.658,24.249,-77.656,24.227,-77.755,24.163,-77.683,24.118,-77.615,24.216,-77.562,24.137,-77.532,23.988,-77.537,23.962,-77.532,23.939,-77.521,23.911,-77.519,23.869,-77.574,23.739,-77.771,23.753,-77.776,23.862,-77.806,23.884,-77.852,24.04,-77.914,24.091,-78,24.22,-77.95,24.253,-77.884,24.242,-77.85,24.258,-77.757,24.27,-77.701,24.288,-77.658,24.249]],[[-77.226,25.904,-77.246,25.895,-77.333,25.996,-77.403,26.025,-77.294,26.096,-77.247,26.156,-77.248,26.289,-77.221,26.362,-77.23,26.425,-77.206,26.489,-77.239,26.561,-77.33,26.618,-77.511,26.846,-77.796,26.901,-77.944,26.904,-77.863,26.94,-77.788,26.936,-77.672,26.914,-77.534,26.903,-77.449,26.836,-77.369,26.748,-77.296,26.712,-77.266,26.689,-77.269,26.663,-77.257,26.639,-77.162,26.597,-77.066,26.53,-77.038,26.333,-77.167,26.24,-77.191,25.955,-77.226,25.904]],[[-73.027,21.192,-73.059,21.119,-73.165,20.979,-73.401,20.944,-73.661,20.937,-73.681,20.976,-73.687,21.009,-73.668,21.062,-73.67,21.082,-73.68,21.103,-73.585,21.126,-73.523,21.191,-73.425,21.202,-73.302,21.156,-73.235,21.154,-73.137,21.205,-73.058,21.313,-73.012,21.3,-73.027,21.192]],[[-77.744,24.707,-77.746,24.586,-77.735,24.496,-77.745,24.463,-77.853,24.403,-77.881,24.369,-77.983,24.335,-78.045,24.287,-78.076,24.365,-78.136,24.412,-78.146,24.493,-78.192,24.466,-78.258,24.483,-78.367,24.544,-78.435,24.628,-78.339,24.642,-78.319,24.59,-78.243,24.654,-78.26,24.687,-78.274,24.692,-78.299,24.754,-78.184,24.917,-78.159,25.022,-78.211,25.191,-78.163,25.202,-78.033,25.143,-77.975,25.085,-77.973,25.005,-77.919,24.943,-77.84,24.794,-77.744,24.707]],[[-78.493,26.729,-78.372,26.698,-78.307,26.702,-78.268,26.723,-78.089,26.714,-77.944,26.744,-77.922,26.691,-77.926,26.663,-78.234,26.637,-78.516,26.559,-78.671,26.507,-78.744,26.501,-78.799,26.528,-78.986,26.69,-78.936,26.673,-78.798,26.582,-78.712,26.599,-78.633,26.659,-78.621,26.705,-78.633,26.726,-78.597,26.798,-78.493,26.729]],[[-77.348,25.014,-77.46,24.993,-77.541,25.014,-77.562,25.03,-77.527,25.058,-77.451,25.081,-77.329,25.083,-77.276,25.056,-77.269,25.044,-77.348,25.014]],[[-74.058,22.723,-74.035,22.706,-74.099,22.665,-74.242,22.715,-74.275,22.712,-74.303,22.764,-74.314,22.804,-74.307,22.84,-74.221,22.812,-74.175,22.76,-74.058,22.723]],[[-74.429,24.068,-74.509,23.96,-74.551,23.969,-74.527,24.105,-74.472,24.127,-74.45,24.125,-74.429,24.068]],[[-74.207,22.214,-74.277,22.184,-74.261,22.236,-74.127,22.323,-74.052,22.401,-74.01,22.428,-73.995,22.449,-73.936,22.478,-73.906,22.527,-73.915,22.568,-73.976,22.635,-73.975,22.682,-73.954,22.716,-73.85,22.731,-73.877,22.681,-73.837,22.538,-73.975,22.361,-74.093,22.306,-74.207,22.214]],[[-76.649,25.487,-76.484,25.375,-76.344,25.332,-76.192,25.191,-76.127,25.141,-76.115,25.095,-76.141,24.886,-76.175,24.76,-76.17,24.649,-76.205,24.682,-76.241,24.754,-76.3,24.796,-76.32,24.818,-76.214,24.822,-76.204,24.936,-76.153,25.026,-76.16,25.119,-76.284,25.222,-76.369,25.313,-76.5,25.342,-76.621,25.432,-76.693,25.443,-76.781,25.427,-76.749,25.481,-76.727,25.552,-76.711,25.565,-76.649,25.487]],[[-75.665,23.45,-75.706,23.444,-75.781,23.471,-75.956,23.592,-76.037,23.603,-76.01,23.671,-75.949,23.647,-75.808,23.543,-75.754,23.49,-75.665,23.45]],[[-74.84,22.894,-74.847,22.869,-74.973,23.069,-75.132,23.117,-75.223,23.165,-75.204,23.193,-75.141,23.205,-75.131,23.268,-75.158,23.336,-75.241,23.475,-75.288,23.568,-75.31,23.59,-75.316,23.668,-75.217,23.547,-75.175,23.439,-75.109,23.333,-75.064,23.15,-74.937,23.088,-74.846,23,-74.84,22.894]],[[-75.308,24.2,-75.302,24.149,-75.369,24.159,-75.468,24.14,-75.503,24.139,-75.481,24.174,-75.412,24.221,-75.409,24.266,-75.494,24.33,-75.593,24.491,-75.639,24.529,-75.661,24.59,-75.744,24.655,-75.727,24.689,-75.71,24.698,-75.654,24.681,-75.526,24.45,-75.518,24.427,-75.308,24.2]],[[-73.041,22.429,-72.979,22.415,-72.945,22.416,-72.831,22.386,-72.763,22.344,-72.747,22.327,-72.784,22.291,-72.889,22.36,-72.981,22.369,-73.11,22.368,-73.162,22.381,-73.127,22.455,-73.041,22.429]],[[-72.916,21.507,-73.049,21.458,-73.063,21.515,-72.995,21.562,-72.916,21.507]]]},{"n":"Azerbaijan","c":"AZ","r":5,"ml":4,"lx":47.21,"ly":40.4,"b":[44.768,38.399,50.366,41.891],"p":[[[44.817,39.65,44.783,39.685,44.768,39.704,44.867,39.719,45.032,39.765,45.076,39.743,45.125,39.696,45.149,39.657,45.153,39.583,45.173,39.571,45.253,39.595,45.288,39.566,45.35,39.53,45.457,39.494,45.611,39.55,45.687,39.564,45.75,39.563,45.784,39.546,45.796,39.488,45.784,39.417,45.766,39.378,45.799,39.35,45.925,39.282,45.977,39.244,45.952,39.178,46.046,39.018,46.077,38.955,46.114,38.878,45.922,38.908,45.575,38.973,45.48,39.006,45.389,39.096,45.336,39.139,45.256,39.195,45.191,39.216,45.141,39.254,45.113,39.312,45.072,39.363,45,39.424,44.838,39.629,44.817,39.65]],[[48.869,38.435,48.84,38.437,48.636,38.399,48.593,38.411,48.417,38.586,48.381,38.606,48.306,38.613,48.261,38.642,48.225,38.689,48.205,38.724,48.023,38.819,47.996,38.854,47.993,38.884,48.019,38.912,48.05,38.935,48.139,38.959,48.242,38.979,48.275,38.994,48.292,39.019,48.291,39.059,48.274,39.099,48.125,39.172,48.109,39.203,48.104,39.241,48.113,39.281,48.136,39.312,48.257,39.355,48.322,39.399,48.282,39.448,48.151,39.561,47.996,39.684,47.892,39.685,47.773,39.649,47.582,39.543,47.476,39.498,47.338,39.424,47.188,39.341,47.065,39.253,46.989,39.18,46.853,39.148,46.783,39.087,46.555,38.904,46.491,38.907,46.487,38.997,46.49,39.069,46.475,39.111,46.401,39.168,46.4,39.192,46.42,39.207,46.477,39.198,46.55,39.201,46.585,39.224,46.507,39.299,46.437,39.349,46.378,39.382,46.365,39.402,46.365,39.417,46.378,39.434,46.478,39.475,46.488,39.513,46.481,39.555,46.322,39.617,46.202,39.594,46.095,39.664,46.026,39.719,45.94,39.777,45.863,39.808,45.79,39.881,45.662,39.956,45.58,39.978,45.581,39.989,45.596,40.003,45.63,40.014,45.858,40.011,45.886,40.025,45.9,40.057,45.931,40.105,45.968,40.175,45.965,40.234,45.736,40.329,45.57,40.417,45.454,40.532,45.376,40.638,45.379,40.674,45.401,40.707,45.579,40.804,45.591,40.83,45.588,40.847,45.524,40.897,45.444,40.948,45.419,40.986,45.369,41.005,45.273,41.006,45.106,41.069,45.071,41.076,45.063,41.088,45.071,41.101,45.19,41.126,45.189,41.147,45.152,41.175,45.085,41.195,45.023,41.246,45.001,41.291,45.217,41.423,45.281,41.45,45.422,41.425,45.716,41.338,45.696,41.289,45.725,41.262,45.793,41.224,45.922,41.187,46.031,41.167,46.087,41.184,46.171,41.198,46.28,41.154,46.381,41.099,46.431,41.077,46.458,41.07,46.534,41.089,46.626,41.16,46.662,41.246,46.673,41.287,46.619,41.344,46.509,41.406,46.385,41.46,46.305,41.508,46.255,41.602,46.204,41.613,46.191,41.625,46.182,41.657,46.184,41.702,46.202,41.737,46.252,41.752,46.303,41.757,46.348,41.79,46.405,41.855,46.43,41.891,46.538,41.87,46.552,41.812,46.571,41.8,46.616,41.807,46.69,41.831,46.749,41.813,46.826,41.743,46.931,41.67,46.988,41.621,47.01,41.588,47.064,41.555,47.143,41.516,47.205,41.456,47.261,41.315,47.318,41.282,47.521,41.229,47.592,41.218,47.791,41.199,47.861,41.213,47.964,41.334,48.056,41.459,48.142,41.485,48.298,41.545,48.391,41.602,48.431,41.663,48.519,41.779,48.573,41.844,48.665,41.787,48.824,41.63,49.051,41.374,49.107,41.302,49.143,41.218,49.175,41.116,49.226,41.026,49.457,40.8,49.556,40.716,49.718,40.608,49.776,40.584,49.852,40.577,49.991,40.577,50.119,40.535,50.183,40.505,50.248,40.462,50.307,40.412,50.366,40.279,50.143,40.323,49.919,40.316,49.792,40.288,49.669,40.249,49.551,40.194,49.477,40.087,49.415,39.84,49.324,39.608,49.328,39.501,49.367,39.398,49.363,39.35,49.321,39.329,49.269,39.285,49.2,39.073,49.165,39.03,49.121,39.004,49.109,39.029,49.111,39.085,49.013,39.134,48.962,39.079,48.926,38.962,48.854,38.839,48.851,38.815,48.869,38.435],[45.552,40.616,45.562,40.649,45.534,40.664,45.504,40.665,45.479,40.648,45.479,40.607,45.514,40.6,45.552,40.616]],[[45.024,41.027,45.002,41.016,44.969,41.027,44.959,41.053,44.961,41.079,44.994,41.086,45.021,41.078,45.029,41.054,45.024,41.027]]]},{"n":"Austria","c":"AT","r":4,"ml":3,"lx":14.13,"ly":47.52,"b":[9.524,46.4,17.147,49.001],"p":[[[9.528,47.271,9.609,47.392,9.626,47.467,9.554,47.511,9.524,47.524,9.549,47.534,9.651,47.526,9.715,47.551,9.749,47.576,9.839,47.552,9.972,47.505,10.034,47.474,10.06,47.449,10.074,47.429,10.066,47.393,10.096,47.38,10.159,47.374,10.2,47.363,10.186,47.317,10.183,47.279,10.241,47.284,10.313,47.313,10.369,47.366,10.404,47.417,10.43,47.541,10.439,47.552,10.483,47.542,10.659,47.547,10.742,47.524,10.873,47.52,10.871,47.501,10.894,47.47,10.952,47.427,10.981,47.398,11.042,47.393,11.136,47.409,11.191,47.425,11.212,47.414,11.298,47.425,11.374,47.46,11.393,47.487,11.47,47.506,11.574,47.55,11.717,47.583,12.186,47.62,12.204,47.647,12.197,47.709,12.209,47.718,12.268,47.703,12.363,47.688,12.436,47.666,12.483,47.637,12.527,47.636,12.594,47.656,12.686,47.669,12.771,47.639,12.796,47.607,12.781,47.59,12.783,47.564,12.809,47.542,12.879,47.506,12.968,47.476,13.014,47.478,13.032,47.508,13.048,47.579,13.054,47.655,13.034,47.699,12.986,47.709,12.928,47.713,12.898,47.722,12.908,47.746,12.954,47.808,12.954,47.891,12.85,47.985,12.76,48.076,12.76,48.107,12.814,48.161,12.897,48.204,13.082,48.275,13.14,48.29,13.215,48.302,13.323,48.331,13.375,48.361,13.409,48.394,13.46,48.565,13.472,48.572,13.487,48.582,13.675,48.523,13.692,48.533,13.724,48.542,13.785,48.587,13.799,48.622,13.797,48.686,13.803,48.748,13.815,48.767,13.843,48.76,13.924,48.728,13.989,48.692,14.049,48.602,14.19,48.579,14.368,48.576,14.431,48.616,14.489,48.626,14.554,48.613,14.691,48.599,14.707,48.672,14.786,48.747,14.822,48.774,14.923,48.771,14.947,48.828,14.972,48.984,14.993,49.001,15.067,48.998,15.14,48.969,15.162,48.946,15.2,48.948,15.253,48.964,15.311,48.974,15.403,48.957,15.599,48.886,15.701,48.86,15.765,48.865,15.825,48.864,16.057,48.755,16.219,48.739,16.367,48.739,16.415,48.772,16.478,48.8,16.544,48.796,16.601,48.782,16.713,48.734,16.764,48.722,16.833,48.714,16.884,48.704,16.928,48.621,16.953,48.599,16.949,48.589,16.943,48.551,16.904,48.504,16.863,48.441,16.865,48.387,16.973,48.198,17.068,48.083,17.086,48.04,17.147,48.006,17.089,47.964,17.078,47.901,17.04,47.873,17.03,47.837,17.046,47.805,17.046,47.764,17.067,47.708,16.973,47.695,16.863,47.697,16.823,47.694,16.786,47.679,16.748,47.686,16.647,47.739,16.591,47.751,16.551,47.747,16.521,47.724,16.47,47.695,16.421,47.674,16.432,47.656,16.64,47.609,16.677,47.536,16.637,47.477,16.623,47.448,16.574,47.425,16.515,47.405,16.443,47.4,16.434,47.367,16.463,47.273,16.44,47.253,16.417,47.223,16.438,47.146,16.483,47.14,16.493,47.123,16.485,47.091,16.477,47.058,16.461,47.022,16.453,47.007,16.424,46.997,16.332,47.002,16.253,46.972,16.093,46.863,16.037,46.845,15.977,46.801,15.98,46.706,15.972,46.697,15.958,46.678,15.767,46.711,15.76,46.711,15.633,46.698,15.545,46.655,15.439,46.63,15.217,46.643,15.001,46.626,14.949,46.613,14.893,46.606,14.841,46.58,14.811,46.545,14.757,46.499,14.68,46.463,14.597,46.436,14.577,46.413,14.55,46.4,14.504,46.417,14.466,46.416,14.42,46.428,14.267,46.441,14.1,46.462,14.02,46.482,13.929,46.498,13.831,46.511,13.744,46.514,13.7,46.52,13.49,46.556,13.352,46.558,13.169,46.573,12.806,46.626,12.7,46.647,12.599,46.654,12.479,46.673,12.388,46.703,12.33,46.76,12.268,46.836,12.154,46.935,12.131,46.985,12.166,47.028,12.201,47.061,12.197,47.075,12.169,47.082,11.97,47.04,11.776,46.986,11.699,46.985,11.625,46.997,11.528,46.997,11.433,46.983,11.244,46.976,11.134,46.936,11.063,46.859,11.025,46.797,10.993,46.777,10.927,46.769,10.829,46.775,10.76,46.793,10.689,46.846,10.58,46.854,10.479,46.855,10.453,46.865,10.455,46.899,10.415,46.964,10.349,46.985,10.18,46.862,10.133,46.852,9.997,46.885,9.878,46.938,9.865,46.976,9.845,47.007,9.745,47.037,9.62,47.057,9.58,47.057,9.596,47.076,9.611,47.107,9.601,47.132,9.572,47.158,9.556,47.185,9.551,47.212,9.542,47.234,9.537,47.255,9.528,47.271]]]},{"n":"Australia","c":"AU","r":2,"ml":1.7,"lx":134.05,"ly":-24.13,"b":[112.908,-54.749,158.959,-10.052],"p":[[[143.179,-11.954,143.153,-12.076,143.105,-12.17,143.099,-12.226,143.11,-12.304,143.191,-12.361,143.254,-12.398,143.29,-12.499,143.402,-12.64,143.398,-12.736,143.458,-12.856,143.512,-13.095,143.529,-13.304,143.587,-13.444,143.548,-13.741,143.589,-13.863,143.643,-13.964,143.707,-14.165,143.756,-14.349,143.822,-14.401,143.962,-14.463,144.106,-14.395,144.21,-14.302,144.322,-14.279,144.473,-14.232,144.586,-14.355,144.648,-14.492,144.916,-14.674,145.064,-14.791,145.18,-14.857,145.288,-14.943,145.277,-15.029,145.252,-15.097,145.276,-15.204,145.293,-15.327,145.272,-15.477,145.35,-15.702,145.375,-15.881,145.458,-16.056,145.452,-16.237,145.436,-16.305,145.426,-16.406,145.49,-16.532,145.55,-16.625,145.638,-16.726,145.755,-16.879,145.838,-16.91,145.912,-16.912,145.902,-17.07,146.05,-17.381,146.126,-17.635,146.074,-17.977,146.023,-18.176,146.032,-18.273,146.223,-18.51,146.333,-18.554,146.312,-18.667,146.297,-18.841,146.383,-18.977,146.481,-19.079,146.587,-19.139,146.692,-19.187,146.829,-19.236,147.003,-19.256,147.093,-19.333,147.139,-19.393,147.278,-19.414,147.342,-19.403,147.419,-19.378,147.471,-19.419,147.51,-19.474,147.586,-19.623,147.742,-19.77,147.853,-19.795,147.916,-19.869,148.004,-19.89,148.081,-19.899,148.19,-19.956,148.367,-20.087,148.527,-20.109,148.6,-20.145,148.759,-20.29,148.821,-20.366,148.885,-20.481,148.805,-20.492,148.73,-20.468,148.684,-20.58,148.789,-20.736,148.912,-20.845,149.061,-20.961,149.205,-21.125,149.241,-21.25,149.28,-21.3,149.329,-21.476,149.454,-21.579,149.46,-21.765,149.524,-22.024,149.596,-22.258,149.645,-22.328,149.704,-22.441,149.772,-22.426,149.822,-22.39,149.92,-22.501,149.974,-22.551,150.006,-22.522,149.942,-22.308,149.981,-22.184,150.021,-22.168,150.076,-22.164,150.143,-22.265,150.235,-22.373,150.405,-22.469,150.541,-22.559,150.58,-22.556,150.564,-22.486,150.569,-22.384,150.623,-22.367,150.672,-22.418,150.764,-22.576,150.783,-22.903,150.783,-23.177,150.843,-23.458,150.931,-23.532,150.989,-23.602,151.088,-23.696,151.154,-23.784,151.236,-23.825,151.501,-24.012,151.575,-24.034,151.691,-24.038,151.832,-24.123,151.903,-24.201,152.055,-24.494,152.13,-24.598,152.282,-24.699,152.353,-24.733,152.456,-24.802,152.493,-24.904,152.502,-24.964,152.563,-25.072,152.654,-25.202,152.789,-25.274,152.913,-25.432,152.921,-25.689,152.985,-25.816,153.028,-25.87,153.125,-25.923,153.165,-25.964,153.084,-26.304,153.162,-26.983,153.117,-27.194,153.198,-27.405,153.386,-27.769,153.428,-27.898,153.455,-28.048,153.576,-28.241,153.569,-28.533,153.617,-28.673,153.605,-28.854,153.462,-29.05,153.348,-29.29,153.347,-29.497,153.272,-29.892,153.224,-29.999,153.188,-30.164,153.031,-30.563,153.024,-30.72,153.048,-30.907,153.022,-31.087,152.982,-31.209,152.944,-31.435,152.786,-31.786,152.559,-32.046,152.545,-32.243,152.517,-32.33,152.47,-32.439,152.331,-32.558,152.247,-32.609,152.216,-32.678,152.137,-32.678,152.135,-32.7,152.188,-32.722,152.164,-32.757,151.954,-32.82,151.813,-32.901,151.668,-33.099,151.608,-33.202,151.53,-33.301,151.484,-33.347,151.463,-33.397,151.432,-33.522,151.358,-33.544,151.292,-33.581,151.323,-33.699,151.288,-33.835,151.28,-33.927,151.245,-33.985,151.202,-33.964,151.168,-33.973,151.125,-34.005,151.191,-34.015,151.232,-34.03,151.09,-34.162,150.96,-34.297,150.927,-34.387,150.871,-34.499,150.822,-34.749,150.781,-34.892,150.809,-34.994,150.805,-35.013,150.775,-35.02,150.756,-35.007,150.697,-35.042,150.681,-35.077,150.706,-35.12,150.722,-35.135,150.715,-35.155,150.69,-35.178,150.634,-35.178,150.567,-35.214,150.374,-35.584,150.292,-35.682,150.195,-35.834,150.158,-35.971,150.129,-36.12,150.095,-36.373,150.063,-36.55,149.988,-36.723,149.96,-36.846,149.951,-37.08,149.986,-37.258,149.963,-37.353,149.962,-37.444,149.933,-37.529,149.809,-37.548,149.709,-37.617,149.565,-37.73,149.481,-37.771,149.298,-37.802,148.944,-37.788,148.263,-37.831,148.131,-37.856,147.877,-37.934,147.631,-38.056,147.396,-38.219,146.857,-38.663,146.436,-38.712,146.356,-38.712,146.293,-38.7,146.217,-38.727,146.216,-38.783,146.286,-38.84,146.337,-38.894,146.427,-38.82,146.467,-38.84,146.482,-38.978,146.484,-39.065,146.457,-39.112,146.4,-39.146,146.34,-39.124,146.332,-39.077,146.254,-38.964,146.158,-38.866,146.07,-38.834,146.018,-38.867,145.935,-38.902,145.866,-38.776,145.791,-38.667,145.692,-38.656,145.606,-38.657,145.535,-38.61,145.397,-38.535,145.424,-38.477,145.463,-38.416,145.542,-38.394,145.518,-38.311,145.476,-38.244,145.366,-38.226,145.293,-38.238,145.249,-38.291,145.191,-38.384,144.96,-38.501,144.847,-38.436,144.718,-38.34,144.78,-38.347,144.911,-38.344,145.02,-38.258,145.067,-38.205,145.12,-38.091,145.05,-38.011,144.985,-37.952,144.891,-37.9,144.538,-38.077,144.465,-38.103,144.396,-38.137,144.518,-38.166,144.589,-38.158,144.665,-38.21,144.544,-38.284,144.448,-38.304,144.329,-38.348,144.102,-38.462,143.812,-38.699,143.687,-38.767,143.539,-38.821,143.338,-38.758,143.226,-38.743,143.083,-38.646,142.84,-38.581,142.612,-38.452,142.456,-38.386,142.345,-38.372,142.188,-38.399,141.925,-38.284,141.725,-38.271,141.594,-38.388,141.492,-38.38,141.424,-38.363,141.214,-38.172,141.011,-38.077,140.627,-38.028,140.39,-37.897,140.212,-37.642,139.875,-37.352,139.784,-37.246,139.742,-37.142,139.738,-37.06,139.784,-36.903,139.847,-36.748,139.857,-36.662,139.729,-36.371,139.549,-36.097,139.466,-36.01,139.245,-35.827,139.038,-35.689,138.985,-35.618,138.969,-35.581,139.067,-35.598,139.113,-35.542,139.178,-35.523,139.231,-35.598,139.289,-35.611,139.292,-35.486,139.325,-35.427,139.303,-35.399,139.283,-35.375,139.193,-35.347,139.094,-35.39,139.018,-35.443,138.915,-35.489,138.875,-35.537,138.771,-35.538,138.73,-35.551,138.522,-35.642,138.389,-35.645,138.184,-35.613,138.252,-35.487,138.333,-35.412,138.4,-35.326,138.511,-35.024,138.49,-34.764,138.436,-34.656,138.264,-34.44,138.186,-34.307,138.089,-34.17,138.041,-34.25,138.012,-34.334,137.919,-34.456,137.874,-34.727,137.692,-35.143,137.566,-35.148,137.46,-35.131,137.272,-35.179,137.144,-35.236,137.03,-35.237,136.967,-35.255,136.884,-35.24,137.014,-34.916,137.128,-34.925,137.252,-34.912,137.308,-34.917,137.391,-34.913,137.454,-34.764,137.493,-34.598,137.469,-34.49,137.459,-34.379,137.484,-34.252,137.494,-34.161,137.65,-33.859,137.781,-33.703,137.932,-33.579,137.914,-33.461,137.866,-33.314,137.852,-33.201,137.924,-33.165,137.993,-33.094,137.913,-32.771,137.863,-32.674,137.783,-32.578,137.782,-32.702,137.791,-32.823,137.68,-32.978,137.536,-33.089,137.442,-33.194,137.354,-33.43,137.237,-33.629,137.13,-33.703,137.034,-33.72,136.937,-33.75,136.783,-33.83,136.636,-33.897,136.526,-33.984,136.431,-34.03,136.121,-34.429,135.98,-34.562,135.951,-34.616,135.891,-34.661,135.903,-34.724,135.951,-34.767,135.999,-34.944,135.97,-34.982,135.919,-34.962,135.792,-34.863,135.713,-34.899,135.648,-34.94,135.481,-34.758,135.412,-34.716,135.324,-34.643,135.231,-34.58,135.191,-34.573,135.123,-34.586,135.13,-34.537,135.176,-34.497,135.217,-34.487,135.292,-34.546,135.379,-34.598,135.427,-34.602,135.45,-34.581,135.368,-34.376,135.312,-34.196,135.286,-34.142,135.219,-33.96,135.185,-33.907,135.042,-33.778,134.889,-33.626,134.847,-33.445,134.791,-33.328,134.719,-33.255,134.608,-33.19,134.301,-33.165,134.174,-32.979,134.1,-32.749,134.158,-32.733,134.227,-32.731,134.249,-32.659,134.234,-32.549,133.93,-32.412,133.787,-32.269,133.665,-32.207,133.551,-32.183,133.401,-32.188,133.212,-32.184,132.757,-31.956,132.649,-31.949,132.324,-32.02,132.215,-32.007,131.721,-31.696,131.393,-31.549,131.285,-31.521,131.144,-31.496,131.029,-31.532,130.948,-31.566,130.783,-31.604,130.13,-31.579,129.569,-31.627,129.188,-31.66,128.946,-31.703,128.546,-31.888,128.068,-32.067,127.678,-32.151,127.32,-32.264,127.084,-32.297,126.779,-32.311,126.137,-32.257,125.917,-32.297,125.567,-32.506,125.464,-32.557,125.267,-32.614,124.759,-32.883,124.525,-32.94,124.373,-32.958,124.244,-33.015,124.126,-33.129,123.967,-33.446,123.868,-33.596,123.65,-33.836,123.507,-33.916,123.365,-33.905,123.208,-33.988,123.068,-33.901,122.956,-33.884,122.778,-33.891,122.151,-33.992,122.061,-33.874,121.946,-33.857,121.73,-33.862,121.405,-33.827,120.815,-33.871,120.531,-33.92,120.418,-33.963,120.209,-33.935,119.854,-33.975,119.729,-34.042,119.635,-34.101,119.451,-34.368,119.248,-34.456,119.081,-34.459,118.895,-34.48,118.52,-34.737,118.136,-34.987,118.006,-35.013,117.863,-35.055,117.675,-35.075,117.582,-35.098,117.144,-35.034,116.865,-35.027,116.517,-34.988,116.217,-34.866,115.987,-34.795,115.726,-34.526,115.565,-34.426,115.278,-34.304,115.195,-34.308,115.128,-34.342,115.009,-34.256,115.006,-34.145,114.973,-34.051,114.976,-33.804,114.994,-33.515,115.099,-33.58,115.182,-33.643,115.359,-33.64,115.515,-33.531,115.604,-33.372,115.683,-33.193,115.671,-33.002,115.619,-32.667,115.654,-32.597,115.708,-32.568,115.725,-32.401,115.738,-31.888,115.698,-31.695,115.455,-31.303,115.294,-30.962,115.177,-30.808,115.078,-30.56,114.995,-30.216,114.969,-30.042,114.942,-29.722,114.971,-29.54,114.959,-29.434,114.857,-29.143,114.628,-28.872,114.591,-28.772,114.592,-28.666,114.537,-28.543,114.354,-28.295,114.165,-28.081,114.133,-27.976,114.098,-27.544,114.028,-27.347,113.709,-26.848,113.333,-26.417,113.231,-26.241,113.185,-26.182,113.211,-26.174,113.253,-26.197,113.3,-26.24,113.323,-26.244,113.345,-26.208,113.343,-26.126,113.356,-26.08,113.389,-26.106,113.427,-26.198,113.547,-26.437,113.582,-26.558,113.734,-26.595,113.78,-26.563,113.836,-26.501,113.853,-26.332,113.776,-26.256,113.706,-26.224,113.589,-26.099,113.513,-25.898,113.395,-25.713,113.397,-25.647,113.451,-25.599,113.539,-25.625,113.621,-25.732,113.713,-25.831,113.698,-26.004,113.684,-26.052,113.692,-26.092,113.724,-26.13,113.766,-26.16,113.812,-26.116,113.854,-26.014,113.88,-26.028,113.942,-26.259,113.992,-26.321,114.09,-26.394,114.176,-26.337,114.216,-26.289,114.203,-26.126,114.229,-25.969,114.214,-25.852,113.993,-25.545,113.792,-25.166,113.671,-24.977,113.569,-24.693,113.504,-24.595,113.418,-24.436,113.413,-24.254,113.421,-24.132,113.49,-23.87,113.553,-23.733,113.757,-23.418,113.767,-23.283,113.765,-23.18,113.795,-23.024,113.795,-22.915,113.768,-22.813,113.683,-22.638,113.795,-22.332,113.958,-21.939,114.023,-21.881,114.124,-21.829,114.143,-21.91,114.093,-22.181,114.164,-22.323,114.142,-22.483,114.205,-22.456,114.304,-22.425,114.378,-22.342,114.417,-22.261,114.603,-21.942,114.709,-21.823,114.859,-21.736,115.162,-21.631,115.456,-21.492,115.596,-21.358,115.771,-21.242,115.894,-21.117,116.011,-21.03,116.606,-20.713,116.707,-20.654,116.836,-20.647,116.995,-20.658,117.139,-20.641,117.293,-20.713,117.406,-20.721,117.684,-20.643,117.832,-20.573,118.087,-20.419,118.199,-20.375,118.458,-20.327,118.751,-20.262,119.104,-19.995,119.359,-20.012,119.586,-20.038,119.768,-19.958,120.196,-19.909,120.434,-19.842,120.878,-19.665,120.998,-19.604,121.18,-19.478,121.338,-19.32,121.494,-19.106,121.589,-18.915,121.631,-18.817,121.722,-18.66,121.785,-18.536,121.834,-18.477,122.006,-18.394,122.262,-18.159,122.345,-18.112,122.361,-18.037,122.306,-17.995,122.237,-17.969,122.191,-17.72,122.147,-17.549,122.143,-17.428,122.16,-17.314,122.261,-17.136,122.333,-17.059,122.432,-16.97,122.523,-16.943,122.598,-16.865,122.72,-16.788,122.772,-16.71,122.848,-16.552,122.917,-16.433,122.971,-16.437,123.074,-16.715,123.142,-16.863,123.266,-17.037,123.383,-17.293,123.479,-17.41,123.525,-17.486,123.563,-17.521,123.571,-17.472,123.562,-17.415,123.608,-17.22,123.586,-17.083,123.594,-17.03,123.618,-17.008,123.664,-17.023,123.754,-17.1,123.799,-17.127,123.831,-17.121,123.829,-16.997,123.874,-16.919,123.856,-16.865,123.778,-16.868,123.745,-16.801,123.68,-16.724,123.607,-16.668,123.518,-16.541,123.49,-16.491,123.525,-16.468,123.581,-16.471,123.626,-16.416,123.646,-16.343,123.607,-16.224,123.647,-16.18,123.729,-16.192,123.859,-16.382,123.915,-16.364,123.961,-16.287,124.044,-16.265,124.13,-16.279,124.186,-16.334,124.3,-16.388,124.453,-16.382,124.53,-16.395,124.692,-16.386,124.772,-16.403,124.757,-16.373,124.669,-16.339,124.57,-16.332,124.454,-16.335,124.405,-16.299,124.388,-16.203,124.416,-16.133,124.435,-16.104,124.51,-16.116,124.577,-16.114,124.585,-16.02,124.609,-15.937,124.649,-15.87,124.648,-15.805,124.607,-15.823,124.504,-15.972,124.455,-15.851,124.382,-15.758,124.397,-15.626,124.44,-15.494,124.506,-15.475,124.562,-15.496,124.644,-15.419,124.691,-15.36,124.68,-15.311,124.693,-15.274,124.75,-15.285,124.972,-15.404,125.016,-15.467,125.063,-15.442,125.078,-15.375,125.073,-15.307,125.024,-15.317,124.909,-15.31,124.883,-15.272,124.893,-15.241,124.839,-15.161,124.914,-15.11,124.979,-15.107,125.023,-15.072,125.024,-15.024,125.038,-15.004,125.073,-15.032,125.189,-15.045,125.302,-15.107,125.356,-15.12,125.376,-15.087,125.384,-15.016,125.243,-14.945,125.239,-14.875,125.18,-14.794,125.179,-14.715,125.267,-14.648,125.285,-14.584,125.335,-14.558,125.436,-14.557,125.504,-14.502,125.58,-14.483,125.598,-14.362,125.597,-14.278,125.628,-14.257,125.705,-14.291,125.681,-14.388,125.681,-14.48,125.662,-14.529,125.691,-14.525,125.708,-14.505,125.738,-14.444,125.82,-14.469,125.84,-14.534,125.85,-14.597,125.891,-14.618,125.946,-14.52,126.021,-14.495,126.017,-14.371,126.045,-14.283,126.054,-14.217,126.101,-14.184,126.111,-14.114,126.073,-14.066,126.054,-13.977,126.119,-13.958,126.184,-14.002,126.228,-14.113,126.258,-14.164,126.299,-14.136,126.323,-14.062,126.403,-14.019,126.482,-14.079,126.57,-14.161,126.679,-14.089,126.781,-13.955,126.764,-13.873,126.776,-13.788,126.903,-13.744,127.006,-13.777,127.099,-13.867,127.293,-13.935,127.458,-14.031,127.531,-14.095,127.673,-14.195,127.763,-14.299,127.888,-14.485,128.18,-14.712,128.199,-14.752,128.16,-14.827,128.124,-14.924,128.08,-15.088,128.069,-15.329,128.112,-15.312,128.155,-15.226,128.202,-15.243,128.255,-15.299,128.259,-15.246,128.227,-15.214,128.173,-15.102,128.175,-15.043,128.218,-14.996,128.285,-14.939,128.358,-14.902,128.403,-14.869,128.41,-14.829,128.477,-14.788,128.576,-14.775,128.636,-14.781,129.058,-14.884,129.165,-14.988,129.175,-15.115,129.216,-15.16,129.238,-15.08,129.234,-14.906,129.268,-14.871,129.381,-14.898,129.459,-14.933,129.567,-15.047,129.588,-15.103,129.635,-15.14,129.65,-15.087,129.628,-15.012,129.613,-14.926,129.637,-14.851,129.763,-14.845,129.849,-14.829,129.808,-14.8,129.754,-14.79,129.663,-14.721,129.605,-14.647,129.699,-14.575,129.698,-14.557,129.608,-14.56,129.484,-14.49,129.379,-14.392,129.459,-14.213,129.62,-14.038,129.71,-13.98,129.718,-13.921,129.762,-13.812,129.789,-13.72,129.797,-13.648,129.839,-13.573,129.938,-13.502,130.073,-13.476,130.136,-13.448,130.199,-13.383,130.26,-13.302,130.135,-13.146,130.145,-13.059,130.168,-12.957,130.318,-12.883,130.4,-12.688,130.454,-12.659,130.572,-12.664,130.617,-12.647,130.61,-12.491,130.623,-12.431,130.672,-12.407,130.736,-12.428,130.777,-12.495,130.867,-12.558,130.898,-12.524,130.883,-12.455,130.874,-12.367,130.957,-12.348,131.023,-12.343,131.03,-12.271,131.02,-12.214,131.046,-12.19,131.22,-12.178,131.265,-12.119,131.292,-12.068,131.314,-12.096,131.342,-12.21,131.438,-12.277,131.726,-12.278,131.888,-12.232,131.957,-12.259,132.064,-12.281,132.182,-12.227,132.253,-12.186,132.372,-12.239,132.411,-12.295,132.442,-12.176,132.511,-12.135,132.584,-12.11,132.676,-12.13,132.713,-12.123,132.63,-12.035,132.635,-11.955,132.63,-11.836,132.645,-11.727,132.674,-11.649,132.475,-11.492,132.278,-11.468,132.134,-11.501,132.073,-11.475,131.945,-11.349,131.822,-11.302,131.812,-11.271,131.962,-11.181,132.019,-11.196,132.106,-11.281,132.155,-11.311,132.198,-11.305,132.225,-11.239,132.263,-11.204,132.334,-11.224,132.557,-11.367,132.683,-11.506,132.747,-11.469,132.857,-11.391,132.961,-11.407,133.025,-11.453,133.114,-11.622,133.185,-11.706,133.356,-11.728,133.443,-11.76,133.533,-11.816,133.654,-11.811,133.904,-11.832,134.139,-11.94,134.237,-12.008,134.351,-12.026,134.417,-12.053,134.538,-12.061,134.73,-11.984,134.816,-12.055,134.855,-12.103,135.03,-12.194,135.218,-12.222,135.352,-12.129,135.549,-12.061,135.686,-11.956,135.788,-11.907,135.885,-11.822,135.922,-11.826,135.844,-11.905,135.834,-11.951,135.896,-11.97,135.889,-11.993,135.804,-12.055,135.703,-12.152,135.704,-12.21,135.744,-12.242,135.791,-12.228,135.857,-12.179,135.938,-12.152,136.008,-12.191,136.031,-12.331,136.082,-12.422,136.193,-12.435,136.261,-12.434,136.329,-12.306,136.292,-12.196,136.25,-12.173,136.27,-12.132,136.443,-11.951,136.54,-11.958,136.61,-12.134,136.719,-12.226,136.836,-12.219,136.897,-12.244,136.947,-12.35,136.537,-12.784,136.518,-12.833,136.573,-12.912,136.594,-13.004,136.461,-13.225,136.412,-13.236,136.365,-13.176,136.294,-13.138,136.232,-13.165,136.166,-13.181,135.927,-13.304,135.929,-13.622,135.99,-13.81,135.954,-13.935,135.883,-14.153,135.806,-14.234,135.745,-14.287,135.539,-14.585,135.473,-14.657,135.405,-14.758,135.428,-14.856,135.453,-14.923,135.531,-15,135.833,-15.16,135.97,-15.27,136.205,-15.403,136.259,-15.495,136.291,-15.57,136.462,-15.655,136.584,-15.707,136.619,-15.693,136.644,-15.676,136.675,-15.675,136.705,-15.685,136.7,-15.752,136.687,-15.788,136.698,-15.835,136.785,-15.894,136.923,-15.892,137.002,-15.878,137.09,-15.941,137.169,-15.982,137.299,-16.066,137.526,-16.167,137.704,-16.233,137.913,-16.477,138.072,-16.617,138.245,-16.718,138.506,-16.79,138.626,-16.778,138.82,-16.861,139.01,-16.899,139.11,-17.014,139.145,-17.101,139.154,-17.168,139.248,-17.329,139.441,-17.381,139.69,-17.541,139.895,-17.611,139.946,-17.654,140.036,-17.703,140.21,-17.704,140.511,-17.625,140.648,-17.544,140.83,-17.414,140.916,-17.193,140.966,-17.015,141.219,-16.646,141.291,-16.463,141.356,-16.221,141.412,-16.07,141.393,-15.905,141.452,-15.605,141.581,-15.195,141.625,-15.057,141.604,-14.853,141.523,-14.47,141.559,-14.338,141.594,-14.153,141.535,-14.019,141.481,-13.927,141.473,-13.798,141.534,-13.554,141.589,-13.425,141.645,-13.259,141.614,-12.943,141.735,-12.833,141.782,-12.779,141.876,-12.778,141.92,-12.803,141.93,-12.74,141.893,-12.681,141.878,-12.613,141.852,-12.579,141.795,-12.567,141.747,-12.529,141.678,-12.491,141.689,-12.351,141.806,-12.08,141.871,-11.976,141.913,-12.019,141.961,-12.054,141.968,-11.976,141.952,-11.896,142.041,-11.632,142.139,-11.273,142.168,-10.947,142.326,-10.884,142.407,-10.802,142.456,-10.707,142.545,-10.707,142.605,-10.748,142.565,-10.819,142.553,-10.874,142.723,-11.01,142.78,-11.115,142.803,-11.214,142.837,-11.307,142.853,-11.432,142.851,-11.632,142.873,-11.821,142.934,-11.881,142.988,-11.919,143.066,-11.924,143.179,-11.954]],[[153.077,-25.751,153.052,-25.778,153.007,-25.729,152.977,-25.551,152.999,-25.448,153.052,-25.354,153.061,-25.302,153.038,-25.193,153.189,-25.071,153.228,-25.006,153.242,-24.923,153.186,-24.833,153.144,-24.815,153.181,-24.765,153.223,-24.74,153.257,-24.729,153.282,-24.738,153.298,-24.915,153.359,-24.978,153.35,-25.063,153.141,-25.513,153.084,-25.683,153.077,-25.751]],[[139.508,-16.573,139.431,-16.661,139.392,-16.649,139.354,-16.697,139.283,-16.719,139.239,-16.719,139.16,-16.742,139.148,-16.714,139.163,-16.626,139.229,-16.528,139.293,-16.467,139.459,-16.438,139.588,-16.395,139.604,-16.403,139.698,-16.515,139.56,-16.529,139.508,-16.573]],[[136.715,-13.804,136.758,-13.845,136.804,-13.842,136.845,-13.751,136.871,-13.764,136.891,-13.787,136.906,-13.827,136.843,-13.897,136.815,-13.907,136.788,-13.946,136.745,-14.073,136.75,-14.115,136.787,-14.158,136.885,-14.197,136.934,-14.179,136.951,-14.184,136.931,-14.246,136.894,-14.293,136.763,-14.273,136.65,-14.28,136.461,-14.235,136.363,-14.229,136.335,-14.212,136.392,-14.175,136.428,-14.126,136.411,-14.011,136.425,-13.865,136.534,-13.794,136.583,-13.721,136.656,-13.676,136.702,-13.682,136.696,-13.726,136.715,-13.804]],[[130.459,-11.679,130.542,-11.703,130.58,-11.737,130.603,-11.773,130.606,-11.817,130.503,-11.836,130.317,-11.772,130.131,-11.825,130.077,-11.825,130.043,-11.787,130.072,-11.681,130.139,-11.697,130.198,-11.658,130.187,-11.541,130.153,-11.478,130.251,-11.361,130.295,-11.337,130.339,-11.337,130.377,-11.42,130.386,-11.51,130.433,-11.592,130.459,-11.679]],[[130.619,-11.376,130.752,-11.384,130.913,-11.309,130.987,-11.34,131.023,-11.334,131.141,-11.263,131.217,-11.243,131.268,-11.19,131.321,-11.247,131.437,-11.313,131.473,-11.383,131.522,-11.415,131.539,-11.437,131.468,-11.51,131.459,-11.588,131.383,-11.583,131.292,-11.711,130.951,-11.926,130.645,-11.742,130.512,-11.618,130.423,-11.446,130.405,-11.305,130.369,-11.215,130.385,-11.192,130.403,-11.18,130.427,-11.183,130.519,-11.279,130.56,-11.306,130.619,-11.376]],[[113.183,-26.053,113.156,-26.095,112.964,-25.783,112.908,-25.57,112.947,-25.532,112.982,-25.52,113.096,-25.815,113.132,-25.883,113.132,-25.952,113.148,-25.974,113.183,-26.053]],[[137.596,-35.739,137.836,-35.762,137.929,-35.726,138.047,-35.755,138.123,-35.852,138.067,-35.901,138.012,-35.908,137.836,-35.868,137.671,-35.898,137.622,-35.938,137.59,-36.027,137.448,-36.075,137.382,-36.021,137.21,-35.982,137.148,-36.039,137.026,-36.024,136.913,-36.047,136.755,-36.033,136.589,-35.935,136.541,-35.89,136.579,-35.809,136.639,-35.749,137.092,-35.664,137.334,-35.592,137.53,-35.605,137.585,-35.62,137.635,-35.656,137.598,-35.722,137.596,-35.739]],[[145.487,-38.355,145.336,-38.421,145.28,-38.391,145.286,-38.341,145.295,-38.319,145.427,-38.314,145.487,-38.355]],[[145.314,-38.491,145.349,-38.538,145.355,-38.557,145.271,-38.52,145.128,-38.528,145.218,-38.459,145.288,-38.472,145.314,-38.491]],[[149.044,-20.292,149.02,-20.303,148.987,-20.302,148.939,-20.284,148.981,-20.154,149.004,-20.221,149.045,-20.278,149.044,-20.292]],[[148.936,-20.15,148.913,-20.154,148.887,-20.144,148.906,-20.102,148.932,-20.069,148.968,-20.044,148.956,-20.135,148.936,-20.15]],[[151.147,-23.491,151.181,-23.516,151.212,-23.513,151.24,-23.53,151.229,-23.595,151.274,-23.668,151.296,-23.72,151.262,-23.762,151.238,-23.776,151.184,-23.741,151.033,-23.53,151.06,-23.461,151.147,-23.491]],[[150.517,-22.323,150.488,-22.325,150.462,-22.308,150.485,-22.268,150.488,-22.211,150.521,-22.228,150.549,-22.307,150.517,-22.323]],[[149.928,-22.193,149.894,-22.223,149.87,-22.15,149.875,-22.074,149.912,-22.049,149.928,-22.149,149.928,-22.193]],[[153.539,-27.436,153.453,-27.712,153.427,-27.706,153.396,-27.665,153.401,-27.506,153.435,-27.405,153.522,-27.422,153.539,-27.436]],[[153.442,-27.316,153.421,-27.331,153.377,-27.235,153.365,-27.139,153.38,-27.049,153.432,-27.03,153.467,-27.038,153.426,-27.201,153.442,-27.316]],[[142.275,-10.705,142.191,-10.762,142.137,-10.732,142.125,-10.668,142.131,-10.641,142.198,-10.592,142.275,-10.705]],[[142.339,-10.192,142.279,-10.254,142.216,-10.236,142.195,-10.199,142.219,-10.149,142.299,-10.14,142.339,-10.192]],[[142.168,-10.154,142.142,-10.181,142.098,-10.122,142.149,-10.052,142.192,-10.085,142.168,-10.154]],[[146.278,-18.231,146.299,-18.326,146.342,-18.4,146.327,-18.449,146.299,-18.485,146.236,-18.451,146.191,-18.363,146.116,-18.292,146.099,-18.252,146.187,-18.255,146.231,-18.241,146.249,-18.226,146.278,-18.231]],[[136.599,-11.379,136.527,-11.439,136.522,-11.394,136.56,-11.358,136.649,-11.212,136.688,-11.178,136.711,-11.158,136.727,-11.105,136.732,-11.025,136.78,-11.012,136.741,-11.195,136.599,-11.379]],[[136.339,-11.602,136.18,-11.677,136.267,-11.576,136.449,-11.487,136.479,-11.466,136.471,-11.509,136.379,-11.583,136.339,-11.602]],[[137.094,-15.778,137.051,-15.824,136.996,-15.776,136.985,-15.726,136.943,-15.712,136.963,-15.666,136.986,-15.652,137.01,-15.595,137.065,-15.663,137.071,-15.738,137.094,-15.778]],[[136.863,-15.62,136.847,-15.627,136.846,-15.544,136.877,-15.503,136.89,-15.589,136.863,-15.62]],[[136.591,-15.628,136.531,-15.632,136.514,-15.627,136.503,-15.583,136.523,-15.543,136.586,-15.534,136.612,-15.544,136.591,-15.628]],[[139.459,-17.115,139.422,-17.132,139.408,-17.091,139.459,-17.049,139.493,-16.99,139.56,-17.042,139.571,-17.094,139.459,-17.115]],[[136.237,-13.825,136.214,-13.836,136.123,-13.817,136.122,-13.781,136.134,-13.753,136.16,-13.737,136.215,-13.665,136.257,-13.707,136.275,-13.791,136.237,-13.825]],[[132.593,-11.303,132.574,-11.318,132.494,-11.164,132.516,-11.116,132.484,-11.037,132.538,-11.028,132.579,-10.969,132.593,-10.998,132.597,-11.106,132.629,-11.169,132.593,-11.303]],[[125.199,-14.579,125.135,-14.642,125.091,-14.592,125.117,-14.492,125.16,-14.456,125.198,-14.475,125.194,-14.553,125.199,-14.579]],[[124.597,-15.402,124.56,-15.43,124.524,-15.421,124.524,-15.382,124.483,-15.34,124.504,-15.292,124.519,-15.267,124.551,-15.27,124.565,-15.311,124.605,-15.357,124.597,-15.402]],[[115.446,-20.788,115.388,-20.866,115.318,-20.851,115.309,-20.811,115.354,-20.746,115.435,-20.668,115.458,-20.716,115.446,-20.788]],[[145.043,-40.787,145.159,-40.791,145.224,-40.765,145.283,-40.77,145.349,-40.826,145.429,-40.858,145.485,-40.852,145.533,-40.864,145.576,-40.904,145.686,-40.939,145.734,-40.962,145.775,-40.997,145.821,-41.025,146.111,-41.118,146.317,-41.163,146.574,-41.142,146.651,-41.116,146.723,-41.078,146.786,-41.114,146.848,-41.168,146.836,-41.109,146.857,-41.058,146.919,-41.018,146.99,-40.992,147.106,-40.994,147.219,-40.983,147.269,-40.96,147.321,-40.956,147.388,-40.986,147.455,-41.002,147.501,-40.964,147.579,-40.876,147.622,-40.845,147.818,-40.872,147.873,-40.873,147.969,-40.78,148.033,-40.781,148.215,-40.855,148.293,-40.947,148.285,-41.115,148.292,-41.175,148.306,-41.233,148.312,-41.35,148.29,-41.465,148.287,-41.555,148.297,-41.646,148.288,-41.816,148.316,-41.928,148.302,-42.004,148.301,-42.04,148.328,-42.074,148.341,-42.111,148.331,-42.159,148.343,-42.215,148.331,-42.262,148.29,-42.255,148.277,-42.219,148.285,-42.173,148.277,-42.136,148.256,-42.103,148.183,-42.065,148.204,-42.042,148.242,-42.022,148.214,-41.97,148.167,-42.012,148.141,-42.07,148.156,-42.088,148.128,-42.104,148.067,-42.17,148.023,-42.259,148.005,-42.345,148.009,-42.436,147.974,-42.506,147.924,-42.572,147.912,-42.658,147.915,-42.816,147.958,-42.96,147.981,-43.157,147.945,-43.182,147.839,-43.195,147.786,-43.22,147.699,-43.123,147.648,-43.021,147.687,-42.98,147.774,-43.003,147.8,-42.98,147.807,-42.954,147.8,-42.928,147.693,-42.872,147.574,-42.846,147.536,-42.878,147.549,-42.975,147.537,-42.996,147.452,-43.033,147.408,-42.894,147.298,-42.791,147.302,-42.841,147.348,-42.927,147.343,-42.964,147.325,-43.013,147.281,-43.032,147.26,-43.071,147.26,-43.126,147.245,-43.216,147.173,-43.256,146.997,-43.156,146.985,-43.19,146.988,-43.219,147.077,-43.276,147.036,-43.319,147.005,-43.37,146.955,-43.502,146.874,-43.612,146.834,-43.619,146.699,-43.602,146.549,-43.509,146.413,-43.52,146.187,-43.513,146.043,-43.547,146.013,-43.445,145.982,-43.408,145.994,-43.376,146.109,-43.354,146.226,-43.355,146.208,-43.316,146.176,-43.302,146.125,-43.311,145.975,-43.277,145.873,-43.292,145.803,-43.244,145.682,-43.076,145.61,-42.998,145.567,-42.968,145.518,-42.951,145.488,-42.927,145.268,-42.544,145.237,-42.456,145.199,-42.231,145.373,-42.338,145.435,-42.407,145.468,-42.493,145.527,-42.388,145.517,-42.354,145.36,-42.228,145.34,-42.191,145.331,-42.147,145.294,-42.191,145.235,-42.197,145.259,-42.107,145.238,-42.02,145.055,-41.827,144.916,-41.644,144.778,-41.419,144.766,-41.39,144.764,-41.342,144.698,-41.191,144.662,-41.079,144.646,-40.981,144.71,-40.783,144.719,-40.672,144.819,-40.722,145.043,-40.787]],[[143.928,-40.116,143.899,-40.12,143.876,-40.064,143.888,-39.984,143.839,-39.904,143.865,-39.824,143.862,-39.738,143.879,-39.7,143.939,-39.658,143.949,-39.584,144.001,-39.58,144.091,-39.638,144.121,-39.785,144.106,-39.874,144.141,-39.954,144.112,-40.022,144.035,-40.078,143.928,-40.116]],[[148,-39.758,148.178,-39.938,148.27,-39.967,148.297,-39.986,148.29,-40.065,148.251,-40.1,148.323,-40.144,148.314,-40.174,148.299,-40.172,148.21,-40.234,148.106,-40.262,148.074,-40.241,148.047,-40.213,148.025,-40.172,147.891,-40.015,147.906,-39.971,147.876,-39.905,147.812,-39.91,147.767,-39.87,147.839,-39.832,147.933,-39.726,148,-39.758]],[[147.356,-43.397,147.309,-43.501,147.231,-43.483,147.154,-43.5,147.105,-43.431,147.105,-43.413,147.163,-43.43,147.185,-43.408,147.198,-43.379,147.22,-43.371,147.234,-43.33,147.284,-43.279,147.313,-43.28,147.342,-43.346,147.356,-43.397]],[[148.104,-42.71,148.048,-42.719,148.03,-42.715,148.031,-42.663,148.023,-42.64,148.073,-42.593,148.143,-42.616,148.17,-42.652,148.101,-42.681,148.104,-42.71]],[[147.435,-43.241,147.372,-43.241,147.349,-43.232,147.338,-43.183,147.296,-43.162,147.319,-43.145,147.327,-43.115,147.353,-43.08,147.397,-43.118,147.435,-43.241]],[[144.784,-40.507,144.748,-40.589,144.71,-40.485,144.751,-40.47,144.783,-40.435,144.791,-40.44,144.784,-40.507]],[[148.326,-40.307,148.421,-40.367,148.474,-40.432,148.404,-40.487,148.353,-40.497,148.319,-40.435,148.214,-40.458,148.103,-40.452,148.02,-40.404,148.01,-40.381,148.059,-40.357,148.198,-40.358,148.326,-40.307]],[[148.237,-40.515,148.188,-40.593,148.127,-40.544,148.117,-40.521,148.193,-40.503,148.218,-40.505,148.237,-40.515]],[[158.879,-54.71,158.845,-54.749,158.836,-54.704,158.897,-54.506,158.959,-54.472,158.946,-54.575,158.879,-54.71]]]},{"n":"Australian Indian Ocean Territories","c":"AU","r":5,"ml":5,"lx":105.67,"ly":-10.49,"b":[96.826,-12.2,105.725,-10.431],"p":[[[105.725,-10.493,105.697,-10.564,105.644,-10.525,105.584,-10.512,105.596,-10.46,105.646,-10.452,105.67,-10.449,105.705,-10.431,105.725,-10.493]],[[96.84,-12.182,96.852,-12.187,96.867,-12.181,96.874,-12.188,96.85,-12.197,96.835,-12.18,96.828,-12.151,96.826,-12.126,96.833,-12.126,96.833,-12.136,96.834,-12.144,96.839,-12.16,96.836,-12.171,96.84,-12.182]],[[96.918,-12.194,96.907,-12.2,96.897,-12.196,96.894,-12.193,96.893,-12.187,96.904,-12.187,96.913,-12.182,96.919,-12.173,96.921,-12.162,96.925,-12.173,96.924,-12.185,96.918,-12.194]]]},{"n":"Heard Island and McDonald Islands","c":"HM","r":5,"ml":4.5,"lx":73.51,"ly":-53.1,"b":[73.251,-53.185,73.838,-52.966],"p":[[[73.707,-53.137,73.588,-53.185,73.465,-53.184,73.413,-53.147,73.336,-53.03,73.285,-53.021,73.254,-52.989,73.251,-52.976,73.305,-52.966,73.388,-53,73.586,-53.027,73.731,-53.091,73.838,-53.113,73.795,-53.13,73.707,-53.137]]]},{"n":"Norfolk Island","c":"NF","r":5,"ml":4.5,"lx":167.95,"ly":-29.03,"b":[167.906,-29.096,167.99,-29.014],"p":[[[167.939,-29.018,167.96,-29.028,167.978,-29.034,167.99,-29.042,167.989,-29.059,167.979,-29.076,167.968,-29.083,167.964,-29.085,167.962,-29.088,167.961,-29.092,167.961,-29.096,167.955,-29.082,167.944,-29.073,167.934,-29.072,167.927,-29.083,167.92,-29.083,167.918,-29.072,167.914,-29.062,167.912,-29.053,167.916,-29.045,167.924,-29.036,167.925,-29.029,167.919,-29.025,167.906,-29.028,167.921,-29.014,167.939,-29.018]]]},{"n":"Ashmore and Cartier Islands","c":"AU","r":5,"ml":4.5,"lx":123.59,"ly":-12.43,"b":[123.572,-12.436,123.595,-12.424],"p":[[[123.595,-12.426,123.595,-12.436,123.573,-12.434,123.572,-12.424,123.595,-12.426]]]},{"n":"Armenia","c":"AM","r":6,"ml":5,"lx":44.8,"ly":40.46,"b":[43.439,38.869,46.585,41.291],"p":[[[44.768,39.704,44.734,39.746,44.56,39.888,44.4,39.996,44.289,40.04,44.178,40.036,44.005,40.014,43.942,40.023,43.792,40.07,43.666,40.126,43.683,40.15,43.71,40.167,43.678,40.239,43.608,40.357,43.616,40.393,43.594,40.444,43.569,40.482,43.668,40.574,43.713,40.648,43.723,40.72,43.696,40.794,43.632,40.929,43.592,40.968,43.517,41.005,43.455,41.065,43.439,41.107,43.492,41.116,43.645,41.117,43.793,41.131,43.909,41.159,44.077,41.183,44.146,41.203,44.227,41.213,44.473,41.191,44.565,41.208,44.841,41.211,44.849,41.22,44.811,41.249,44.811,41.259,44.976,41.277,45.001,41.291,45.023,41.246,45.085,41.195,45.152,41.175,45.189,41.147,45.19,41.126,45.071,41.101,45.063,41.088,45.071,41.076,45.106,41.069,45.273,41.006,45.369,41.005,45.419,40.986,45.444,40.948,45.524,40.897,45.588,40.847,45.591,40.83,45.579,40.804,45.401,40.707,45.379,40.674,45.376,40.638,45.454,40.532,45.57,40.417,45.736,40.329,45.965,40.234,45.968,40.175,45.931,40.105,45.9,40.057,45.886,40.025,45.858,40.011,45.63,40.014,45.596,40.003,45.581,39.989,45.58,39.978,45.662,39.956,45.79,39.881,45.863,39.808,45.94,39.777,46.026,39.719,46.095,39.664,46.202,39.594,46.322,39.617,46.481,39.555,46.488,39.513,46.478,39.475,46.378,39.434,46.365,39.417,46.365,39.402,46.378,39.382,46.437,39.349,46.507,39.299,46.585,39.224,46.55,39.201,46.477,39.198,46.42,39.207,46.4,39.192,46.401,39.168,46.475,39.111,46.49,39.069,46.487,38.997,46.491,38.907,46.318,38.913,46.17,38.869,46.114,38.878,46.077,38.955,46.046,39.018,45.952,39.178,45.977,39.244,45.925,39.282,45.799,39.35,45.766,39.378,45.784,39.417,45.796,39.488,45.784,39.546,45.75,39.563,45.687,39.564,45.611,39.55,45.457,39.494,45.35,39.53,45.288,39.566,45.253,39.595,45.173,39.571,45.153,39.583,45.149,39.657,45.125,39.696,45.076,39.743,45.032,39.765,44.867,39.719,44.768,39.704],[45.024,41.027,45.029,41.054,45.021,41.078,44.994,41.086,44.961,41.079,44.959,41.053,44.969,41.027,45.002,41.016,45.024,41.027]],[[45.552,40.616,45.514,40.6,45.479,40.607,45.479,40.648,45.504,40.665,45.534,40.664,45.562,40.649,45.552,40.616]]]},{"n":"Argentina","c":"AR","r":2,"ml":2,"lx":-64.17,"ly":-33.5,"b":[-73.576,-55.032,-53.669,-21.803],"p":[[[-57.609,-30.188,-57.646,-30.227,-57.651,-30.295,-57.713,-30.384,-57.831,-30.495,-57.873,-30.591,-57.819,-30.712,-57.811,-30.859,-57.834,-30.917,-57.886,-30.937,-57.898,-30.975,-57.87,-31.031,-57.868,-31.104,-57.893,-31.195,-57.948,-31.299,-58.033,-31.417,-58.054,-31.495,-58.01,-31.534,-57.988,-31.576,-57.989,-31.621,-58.007,-31.685,-58.042,-31.769,-58.096,-31.832,-58.167,-31.873,-58.189,-31.924,-58.16,-31.987,-58.156,-32.052,-58.177,-32.119,-58.165,-32.185,-58.12,-32.249,-58.123,-32.322,-58.201,-32.472,-58.22,-32.564,-58.171,-32.959,-58.201,-33.015,-58.25,-33.078,-58.309,-33.083,-58.376,-33.072,-58.424,-33.112,-58.455,-33.286,-58.547,-33.663,-58.531,-33.753,-58.457,-33.898,-58.429,-33.991,-58.409,-34.061,-58.392,-34.193,-58.435,-34.253,-58.475,-34.263,-58.525,-34.296,-58.466,-34.457,-58.419,-34.532,-58.283,-34.683,-57.764,-34.895,-57.548,-35.019,-57.304,-35.188,-57.171,-35.362,-57.159,-35.506,-57.354,-35.72,-57.375,-35.9,-57.335,-36.027,-57.265,-36.144,-57.076,-36.297,-56.937,-36.353,-56.749,-36.346,-56.717,-36.389,-56.698,-36.426,-56.668,-36.735,-56.672,-36.851,-56.727,-36.958,-57.088,-37.446,-57.396,-37.745,-57.507,-37.909,-57.547,-38.086,-57.646,-38.17,-58.179,-38.436,-59.007,-38.673,-59.676,-38.797,-59.828,-38.838,-60.904,-38.974,-61.112,-38.993,-61.383,-38.981,-61.603,-38.999,-61.848,-38.962,-62.067,-38.919,-62.189,-38.813,-62.335,-38.8,-62.374,-38.853,-62.304,-38.988,-62.338,-39.151,-62.295,-39.243,-62.209,-39.262,-62.126,-39.31,-62.054,-39.374,-62.179,-39.38,-62.131,-39.432,-62.077,-39.462,-62.083,-39.568,-62.132,-39.825,-62.254,-39.88,-62.287,-39.895,-62.324,-39.951,-62.402,-40.197,-62.427,-40.356,-62.394,-40.459,-62.246,-40.675,-62.302,-40.815,-62.395,-40.891,-62.798,-41.047,-62.959,-41.11,-63.213,-41.152,-63.622,-41.16,-63.773,-41.15,-64.123,-41.008,-64.383,-40.922,-64.621,-40.854,-64.853,-40.814,-64.82,-40.793,-64.804,-40.757,-64.869,-40.736,-64.917,-40.731,-65.069,-40.805,-65.133,-40.881,-65.152,-40.947,-65.155,-41.106,-65.128,-41.239,-65.018,-41.567,-65.007,-41.745,-65.059,-41.97,-64.986,-42.102,-64.898,-42.162,-64.7,-42.221,-64.622,-42.261,-64.538,-42.255,-64.512,-42.27,-64.524,-42.299,-64.574,-42.356,-64.571,-42.416,-64.42,-42.434,-64.265,-42.422,-64.101,-42.395,-64.062,-42.353,-64.061,-42.266,-64.253,-42.251,-64.229,-42.218,-64.083,-42.183,-63.893,-42.125,-63.796,-42.114,-63.729,-42.153,-63.685,-42.189,-63.63,-42.283,-63.596,-42.407,-63.594,-42.556,-63.617,-42.696,-63.644,-42.746,-63.692,-42.805,-64.035,-42.881,-64.131,-42.861,-64.22,-42.756,-64.248,-42.646,-64.324,-42.572,-64.488,-42.513,-64.65,-42.531,-64.812,-42.633,-64.971,-42.666,-65.027,-42.759,-64.629,-42.909,-64.442,-42.951,-64.38,-42.949,-64.319,-42.969,-64.376,-43.025,-64.432,-43.059,-64.715,-43.136,-64.84,-43.189,-64.986,-43.294,-65.19,-43.522,-65.252,-43.572,-65.284,-43.63,-65.305,-43.787,-65.239,-44.049,-65.308,-44.158,-65.266,-44.28,-65.29,-44.361,-65.361,-44.477,-65.648,-44.661,-65.698,-44.796,-65.599,-44.876,-65.606,-44.945,-65.639,-45.008,-65.758,-45.007,-66.19,-44.965,-66.348,-45.034,-66.494,-45.118,-66.533,-45.158,-66.585,-45.183,-66.882,-45.228,-66.941,-45.257,-67.258,-45.577,-67.393,-45.776,-67.557,-45.97,-67.6,-46.053,-67.609,-46.167,-67.586,-46.27,-67.563,-46.345,-67.506,-46.443,-67.387,-46.554,-66.777,-47.006,-66.65,-47.045,-65.999,-47.094,-65.854,-47.157,-65.769,-47.257,-65.738,-47.345,-65.775,-47.568,-65.814,-47.638,-65.886,-47.702,-66.041,-47.783,-66.225,-47.827,-66.172,-47.858,-66.097,-47.853,-65.934,-47.827,-65.864,-47.853,-65.81,-47.941,-65.912,-47.977,-65.943,-48.019,-66.017,-48.084,-66.393,-48.342,-66.596,-48.42,-66.783,-48.523,-67.033,-48.628,-67.131,-48.688,-67.263,-48.814,-67.466,-48.952,-67.685,-49.247,-67.694,-49.304,-67.662,-49.342,-67.783,-49.859,-67.826,-49.92,-67.914,-49.984,-68.146,-50.091,-68.257,-50.105,-68.405,-50.043,-68.488,-49.978,-68.569,-49.867,-68.668,-49.753,-68.673,-49.793,-68.638,-49.863,-68.662,-49.936,-68.913,-49.969,-68.98,-50.003,-68.753,-49.988,-68.598,-50.009,-68.533,-50.036,-68.474,-50.091,-68.422,-50.158,-68.465,-50.195,-68.589,-50.225,-68.75,-50.281,-68.939,-50.382,-69.045,-50.499,-69.09,-50.583,-69.141,-50.753,-69.155,-50.864,-69.235,-50.951,-69.359,-51.028,-69.352,-51.046,-69.268,-51.006,-69.201,-50.994,-69.144,-51.097,-69.066,-51.304,-69.03,-51.446,-69.035,-51.489,-69.058,-51.547,-69.218,-51.561,-69.361,-51.559,-69.465,-51.584,-69.409,-51.61,-69.313,-51.601,-69.18,-51.662,-69.033,-51.636,-68.965,-51.677,-68.917,-51.715,-68.691,-52.013,-68.494,-52.198,-68.394,-52.307,-68.443,-52.357,-68.461,-52.29,-68.59,-52.273,-68.715,-52.255,-68.925,-52.208,-69.206,-52.136,-69.488,-52.136,-69.713,-52.075,-69.96,-52.008,-70.483,-52.002,-70.943,-51.998,-71.415,-51.994,-71.717,-51.991,-71.919,-51.99,-71.971,-51.964,-71.953,-51.88,-72.028,-51.819,-72.137,-51.744,-72.269,-51.691,-72.335,-51.62,-72.408,-51.541,-72.366,-51.47,-72.303,-51.299,-72.302,-51.223,-72.359,-51.17,-72.377,-51.095,-72.359,-51.06,-72.307,-51.033,-72.276,-50.91,-72.301,-50.79,-72.34,-50.682,-72.393,-50.634,-72.46,-50.612,-72.51,-50.608,-72.62,-50.648,-72.804,-50.638,-72.866,-50.653,-72.956,-50.696,-73.082,-50.76,-73.153,-50.738,-73.175,-50.67,-73.222,-50.611,-73.252,-50.558,-73.274,-50.473,-73.312,-50.362,-73.387,-50.231,-73.501,-50.125,-73.508,-50.03,-73.529,-49.911,-73.47,-49.795,-73.505,-49.698,-73.576,-49.583,-73.554,-49.464,-73.484,-49.398,-73.462,-49.314,-73.135,-49.301,-73.149,-49.188,-73.095,-49.097,-73.034,-49.014,-72.982,-48.977,-72.865,-48.944,-72.728,-48.896,-72.651,-48.842,-72.614,-48.793,-72.592,-48.73,-72.586,-48.662,-72.608,-48.519,-72.583,-48.475,-72.498,-48.417,-72.355,-48.366,-72.293,-48.229,-72.328,-48.11,-72.408,-48.016,-72.509,-47.973,-72.518,-47.876,-72.472,-47.784,-72.413,-47.686,-72.342,-47.572,-72.346,-47.493,-72.283,-47.446,-72.103,-47.343,-72.042,-47.241,-71.979,-47.214,-71.905,-47.202,-71.901,-47.144,-71.954,-47.087,-71.963,-47.016,-71.957,-46.937,-71.94,-46.831,-71.856,-46.792,-71.733,-46.706,-71.7,-46.651,-71.695,-46.578,-71.731,-46.428,-71.762,-46.32,-71.778,-46.28,-71.834,-46.207,-71.876,-46.161,-71.809,-46.103,-71.684,-46.042,-71.632,-45.954,-71.68,-45.879,-71.751,-45.839,-71.773,-45.724,-71.746,-45.579,-71.693,-45.535,-71.508,-45.513,-71.49,-45.438,-71.349,-45.332,-71.354,-45.23,-71.443,-45.168,-71.531,-45.068,-71.596,-44.979,-71.812,-44.931,-72.042,-44.904,-72.073,-44.82,-72.064,-44.772,-71.957,-44.792,-71.783,-44.774,-71.652,-44.77,-71.56,-44.762,-71.455,-44.75,-71.358,-44.785,-71.261,-44.763,-71.221,-44.631,-71.16,-44.56,-71.151,-44.494,-71.213,-44.441,-71.326,-44.425,-71.82,-44.383,-71.835,-44.33,-71.831,-44.241,-71.812,-44.151,-71.812,-44.106,-71.767,-44.067,-71.716,-43.984,-71.68,-43.93,-71.716,-43.858,-71.795,-43.753,-71.737,-43.705,-71.733,-43.647,-71.751,-43.59,-71.832,-43.527,-71.905,-43.44,-71.905,-43.348,-71.82,-43.323,-71.764,-43.295,-71.751,-43.237,-71.781,-43.167,-71.899,-43.145,-72.055,-43.102,-72.102,-43.066,-72.146,-42.99,-72.114,-42.777,-72.13,-42.648,-72.144,-42.577,-72.105,-42.522,-72.053,-42.473,-72.078,-42.358,-72.125,-42.298,-72.108,-42.252,-72.064,-42.205,-72.026,-42.148,-71.993,-42.134,-71.944,-42.167,-71.861,-42.148,-71.761,-42.101,-71.75,-42.047,-71.77,-41.969,-71.844,-41.772,-71.911,-41.65,-71.898,-41.607,-71.871,-41.561,-71.892,-41.393,-71.886,-41.292,-71.881,-40.995,-71.873,-40.893,-71.941,-40.789,-71.932,-40.692,-71.884,-40.621,-71.839,-40.524,-71.805,-40.439,-71.769,-40.401,-71.709,-40.382,-71.695,-40.335,-71.723,-40.3,-71.801,-40.244,-71.818,-40.177,-71.802,-40.125,-71.764,-40.095,-71.704,-40.095,-71.66,-40.021,-71.647,-39.929,-71.638,-39.887,-71.672,-39.833,-71.697,-39.707,-71.72,-39.635,-71.693,-39.605,-71.654,-39.594,-71.587,-39.611,-71.539,-39.602,-71.531,-39.564,-71.526,-39.523,-71.508,-39.495,-71.465,-39.402,-71.42,-39.287,-71.409,-39.206,-71.426,-38.986,-71.402,-38.935,-71.353,-38.889,-71.286,-38.845,-71.197,-38.809,-71.087,-38.758,-70.952,-38.738,-70.897,-38.681,-70.859,-38.604,-70.848,-38.542,-70.9,-38.498,-70.968,-38.446,-71,-38.315,-71.018,-38.194,-71.028,-38.041,-71.096,-37.91,-71.168,-37.762,-71.187,-37.631,-71.163,-37.559,-71.135,-37.445,-71.165,-37.393,-71.2,-37.3,-71.163,-37.227,-71.118,-37.114,-71.124,-37.057,-71.159,-36.92,-71.192,-36.844,-71.159,-36.762,-71.107,-36.685,-71.066,-36.644,-71.073,-36.578,-71.056,-36.524,-70.978,-36.487,-70.905,-36.42,-70.853,-36.412,-70.79,-36.412,-70.749,-36.393,-70.733,-36.341,-70.722,-36.283,-70.622,-36.212,-70.563,-36.146,-70.457,-36.133,-70.405,-36.062,-70.404,-35.971,-70.416,-35.879,-70.38,-35.772,-70.42,-35.609,-70.416,-35.523,-70.457,-35.452,-70.449,-35.375,-70.47,-35.326,-70.532,-35.308,-70.555,-35.247,-70.525,-35.217,-70.467,-35.194,-70.393,-35.147,-70.338,-34.922,-70.312,-34.855,-70.287,-34.775,-70.29,-34.733,-70.255,-34.673,-70.211,-34.581,-70.141,-34.493,-70.101,-34.432,-70.063,-34.35,-70.052,-34.301,-70.003,-34.276,-69.946,-34.27,-69.88,-34.254,-69.852,-34.224,-69.857,-34.18,-69.862,-34.084,-69.881,-33.93,-69.894,-33.731,-69.883,-33.601,-69.839,-33.47,-69.798,-33.399,-69.809,-33.344,-69.82,-33.284,-69.896,-33.251,-69.969,-33.279,-70.02,-33.271,-70.085,-33.202,-70.104,-33.128,-70.093,-33.027,-70.042,-32.964,-70.022,-32.885,-70.052,-32.86,-70.116,-32.807,-70.177,-32.626,-70.17,-32.472,-70.23,-32.431,-70.258,-32.31,-70.32,-32.267,-70.345,-32.176,-70.364,-32.083,-70.356,-32.042,-70.291,-32.031,-70.254,-31.958,-70.282,-31.917,-70.331,-31.881,-70.394,-31.884,-70.45,-31.842,-70.526,-31.666,-70.585,-31.569,-70.566,-31.428,-70.555,-31.317,-70.529,-31.223,-70.52,-31.148,-70.473,-31.113,-70.429,-31.129,-70.388,-31.121,-70.351,-31.06,-70.309,-31.023,-70.312,-30.993,-70.336,-30.96,-70.348,-30.902,-70.319,-30.834,-70.269,-30.677,-70.194,-30.505,-70.161,-30.44,-70.17,-30.386,-70.153,-30.361,-70.102,-30.388,-69.956,-30.358,-69.907,-30.282,-69.888,-30.213,-69.844,-30.175,-69.863,-30.12,-69.924,-30.104,-69.96,-30.078,-69.945,-30.016,-69.924,-29.874,-69.928,-29.769,-69.983,-29.545,-70.027,-29.324,-69.996,-29.25,-69.9,-29.149,-69.828,-29.103,-69.815,-29.046,-69.743,-28.784,-69.735,-28.641,-69.688,-28.562,-69.657,-28.414,-69.527,-28.286,-69.489,-28.201,-69.437,-28.193,-69.41,-28.165,-69.341,-28.071,-69.251,-27.974,-69.174,-27.925,-69.155,-27.848,-69.119,-27.744,-69.042,-27.57,-68.999,-27.449,-68.942,-27.405,-68.875,-27.247,-68.846,-27.154,-68.77,-27.115,-68.71,-27.104,-68.652,-27.148,-68.592,-27.14,-68.537,-27.085,-68.405,-27.048,-68.346,-27.028,-68.319,-26.973,-68.319,-26.878,-68.373,-26.806,-68.485,-26.67,-68.581,-26.518,-68.592,-26.47,-68.592,-26.418,-68.576,-26.352,-68.53,-26.277,-68.415,-26.154,-68.427,-26.065,-68.511,-25.741,-68.542,-25.652,-68.6,-25.486,-68.592,-25.42,-68.541,-25.237,-68.496,-25.163,-68.431,-25.149,-68.395,-25.125,-68.384,-25.092,-68.428,-25.051,-68.447,-24.999,-68.466,-24.925,-68.527,-24.899,-68.562,-24.838,-68.562,-24.747,-68.507,-24.63,-68.447,-24.597,-68.423,-24.545,-68.358,-24.497,-68.3,-24.46,-68.25,-24.392,-68.047,-24.308,-67.886,-24.243,-67.572,-24.119,-67.356,-24.034,-67.336,-23.975,-67.319,-23.935,-67.219,-23.634,-67.09,-23.245,-67.009,-23.001,-67.195,-22.822,-67.162,-22.774,-67.055,-22.651,-67.034,-22.552,-66.991,-22.51,-66.8,-22.41,-66.767,-22.343,-66.751,-22.269,-66.712,-22.216,-66.639,-22.205,-66.507,-22.158,-66.365,-22.114,-66.322,-22.053,-66.282,-21.947,-66.248,-21.83,-66.22,-21.803,-66.175,-21.806,-66.099,-21.835,-66.059,-21.879,-65.86,-22.02,-65.771,-22.1,-65.686,-22.11,-65.519,-22.095,-65.485,-22.098,-65.058,-22.103,-64.993,-22.11,-64.843,-22.144,-64.759,-22.171,-64.7,-22.186,-64.606,-22.229,-64.524,-22.372,-64.478,-22.485,-64.446,-22.585,-64.374,-22.761,-64.325,-22.828,-64.308,-22.795,-64.266,-22.603,-64.209,-22.491,-64.132,-22.366,-63.976,-22.073,-63.922,-22.029,-63.861,-22.007,-63.819,-22.005,-63.776,-22.027,-63.717,-22.028,-63.675,-22.004,-63.267,-22.001,-62.843,-21.997,-62.834,-21.999,-62.815,-22.05,-62.745,-22.16,-62.665,-22.218,-62.651,-22.234,-62.626,-22.262,-62.626,-22.29,-62.542,-22.35,-62.373,-22.439,-62.214,-22.612,-62.067,-22.869,-61.928,-23.059,-61.799,-23.182,-61.679,-23.269,-61.571,-23.319,-61.513,-23.36,-61.506,-23.392,-61.404,-23.458,-61.208,-23.557,-61.085,-23.656,-61.033,-23.756,-60.84,-23.858,-60.505,-23.964,-60.262,-24.014,-60.11,-24.009,-59.892,-24.094,-59.609,-24.267,-59.435,-24.387,-59.373,-24.454,-59.187,-24.562,-58.724,-24.787,-58.52,-24.843,-58.423,-24.894,-58.365,-24.959,-58.309,-24.979,-58.253,-24.954,-58.136,-24.977,-57.96,-25.049,-57.822,-25.136,-57.644,-25.328,-57.587,-25.405,-57.563,-25.474,-57.572,-25.534,-57.626,-25.599,-57.725,-25.667,-57.755,-25.697,-57.757,-25.726,-57.782,-25.784,-57.865,-25.907,-57.886,-25.964,-57.891,-26.007,-57.943,-26.053,-58.082,-26.139,-58.111,-26.18,-58.118,-26.225,-58.136,-26.251,-58.155,-26.263,-58.181,-26.307,-58.203,-26.381,-58.205,-26.477,-58.188,-26.593,-58.191,-26.63,-58.222,-26.65,-58.239,-26.677,-58.246,-26.731,-58.272,-26.771,-58.318,-26.796,-58.335,-26.825,-58.323,-26.858,-58.356,-26.89,-58.436,-26.922,-58.485,-26.968,-58.503,-27.029,-58.548,-27.084,-58.619,-27.132,-58.642,-27.196,-58.605,-27.314,-58.168,-27.273,-57.812,-27.317,-57.391,-27.43,-57.112,-27.47,-56.974,-27.436,-56.872,-27.441,-56.805,-27.485,-56.716,-27.494,-56.603,-27.468,-56.511,-27.488,-56.437,-27.554,-56.371,-27.537,-56.311,-27.439,-56.242,-27.367,-56.164,-27.321,-56.067,-27.308,-55.951,-27.326,-55.859,-27.362,-55.79,-27.416,-55.715,-27.415,-55.633,-27.357,-55.594,-27.288,-55.597,-27.208,-55.565,-27.15,-55.497,-27.115,-55.451,-27.068,-55.427,-27.009,-55.346,-26.973,-55.208,-26.96,-55.136,-26.931,-55.13,-26.886,-55.089,-26.845,-55.014,-26.807,-54.962,-26.759,-54.934,-26.703,-54.889,-26.667,-54.825,-26.652,-54.755,-26.533,-54.678,-26.309,-54.632,-26.006,-54.616,-25.576,-54.538,-25.576,-54.502,-25.608,-54.444,-25.625,-54.383,-25.589,-54.332,-25.572,-54.25,-25.57,-54.206,-25.53,-54.155,-25.523,-54.119,-25.545,-54.085,-25.572,-54.012,-25.578,-53.955,-25.648,-53.891,-25.669,-53.864,-25.749,-53.823,-25.96,-53.747,-26.084,-53.671,-26.225,-53.669,-26.288,-53.711,-26.352,-53.718,-26.443,-53.745,-26.667,-53.753,-26.749,-53.727,-26.805,-53.717,-26.883,-53.758,-26.978,-53.838,-27.121,-53.916,-27.16,-53.935,-27.161,-54.04,-27.244,-54.114,-27.275,-54.156,-27.254,-54.205,-27.29,-54.26,-27.382,-54.327,-27.424,-54.448,-27.446,-54.484,-27.457,-54.555,-27.454,-54.615,-27.477,-54.666,-27.527,-54.72,-27.545,-54.777,-27.533,-54.829,-27.551,-54.876,-27.599,-54.903,-27.652,-54.91,-27.709,-54.956,-27.747,-55.04,-27.768,-55.069,-27.796,-55.064,-27.836,-55.102,-27.867,-55.244,-27.899,-55.346,-27.956,-55.41,-28.038,-55.477,-28.089,-55.582,-28.121,-55.725,-28.204,-55.746,-28.255,-55.692,-28.303,-55.672,-28.345,-55.687,-28.382,-55.732,-28.387,-55.806,-28.36,-55.859,-28.354,-55.891,-28.37,-55.905,-28.4,-55.904,-28.443,-55.93,-28.473,-55.985,-28.489,-56.02,-28.525,-56.034,-28.581,-56.103,-28.652,-56.226,-28.737,-56.322,-28.852,-56.393,-28.997,-56.476,-29.092,-56.571,-29.138,-56.636,-29.203,-56.672,-29.287,-56.772,-29.418,-56.939,-29.595,-57.089,-29.716,-57.225,-29.782,-57.301,-29.857,-57.317,-29.939,-57.405,-30.034,-57.564,-30.14,-57.609,-30.188]],[[-68.653,-54.854,-68.648,-54.628,-68.64,-54.324,-68.638,-54.053,-68.637,-53.789,-68.635,-53.515,-68.633,-53.242,-68.632,-52.95,-68.63,-52.653,-68.571,-52.695,-68.339,-52.9,-68.278,-52.984,-68.24,-53.082,-68.333,-53.02,-68.431,-53.055,-68.479,-53.114,-68.521,-53.177,-68.521,-53.222,-68.489,-53.261,-68.393,-53.295,-68.161,-53.306,-68.144,-53.319,-68.008,-53.564,-67.94,-53.619,-67.861,-53.662,-67.678,-53.787,-67.503,-53.922,-67.294,-54.05,-67.069,-54.148,-66.865,-54.223,-66.67,-54.314,-66.462,-54.441,-66.236,-54.533,-65.993,-54.599,-65.747,-54.653,-65.369,-54.632,-65.252,-54.638,-65.179,-54.678,-65.252,-54.789,-65.346,-54.878,-65.471,-54.915,-65.603,-54.928,-65.723,-54.926,-65.842,-54.91,-65.954,-54.919,-66.061,-54.957,-66.172,-54.975,-66.287,-54.978,-66.399,-55.009,-66.511,-55.032,-66.628,-55.013,-66.93,-54.925,-67.127,-54.904,-67.793,-54.869,-68.007,-54.848,-68.22,-54.818,-68.332,-54.816,-68.491,-54.836,-68.619,-54.834,-68.653,-54.854]],[[-64.549,-54.716,-64.439,-54.739,-64.221,-54.722,-64.105,-54.722,-64.055,-54.73,-64.032,-54.742,-63.882,-54.723,-63.815,-54.725,-63.833,-54.768,-63.971,-54.811,-64.028,-54.793,-64.323,-54.796,-64.453,-54.84,-64.509,-54.84,-64.637,-54.903,-64.731,-54.863,-64.757,-54.827,-64.689,-54.775,-64.625,-54.774,-64.581,-54.753,-64.549,-54.716]],[[-61.876,-39.172,-61.866,-39.235,-61.918,-39.227,-62.042,-39.167,-62.083,-39.11,-62.093,-39.086,-61.967,-39.112,-61.907,-39.136,-61.876,-39.172]]]},{"n":"Antigua and Barbuda","c":"AG","r":6,"ml":5,"lx":-61.79,"ly":17.35,"b":[-61.887,16.997,-61.686,17.714],"p":[[[-61.716,17.037,-61.748,16.997,-61.86,17.013,-61.882,17.063,-61.887,17.098,-61.817,17.169,-61.739,17.138,-61.708,17.105,-61.686,17.098,-61.686,17.07,-61.695,17.049,-61.716,17.037]],[[-61.747,17.575,-61.762,17.549,-61.844,17.596,-61.869,17.685,-61.866,17.704,-61.852,17.714,-61.82,17.697,-61.777,17.69,-61.75,17.661,-61.747,17.575]]]},{"n":"Angola","c":"AO","r":3,"ml":3,"lx":17.98,"ly":-12.18,"b":[11.743,-18.02,24.047,-4.429],"p":[[[13.073,-4.635,13.057,-4.651,12.947,-4.695,12.83,-4.737,12.675,-4.905,12.596,-4.978,12.574,-4.997,12.503,-5.037,12.451,-5.071,12.453,-5.091,12.487,-5.113,12.522,-5.149,12.519,-5.425,12.504,-5.696,12.485,-5.719,12.386,-5.728,12.255,-5.746,12.214,-5.759,12.199,-5.732,12.155,-5.633,12.18,-5.539,12.207,-5.468,12.177,-5.325,12.111,-5.197,12.04,-5.035,12.018,-5.004,12.078,-4.952,12.167,-4.838,12.204,-4.779,12.308,-4.766,12.347,-4.724,12.374,-4.658,12.385,-4.619,12.501,-4.587,12.642,-4.531,12.719,-4.47,12.798,-4.431,12.848,-4.429,12.881,-4.445,12.971,-4.552,13.048,-4.619,13.073,-4.635]],[[23.967,-10.872,23.988,-11.003,24.01,-11.185,24.026,-11.316,24.041,-11.374,24.047,-11.405,24.029,-11.439,24.015,-11.518,23.987,-11.587,23.971,-11.636,23.984,-11.725,23.973,-11.853,23.962,-11.988,23.959,-12.118,23.996,-12.351,23.991,-12.422,23.945,-12.544,23.909,-12.636,23.887,-12.743,23.882,-12.799,23.968,-12.957,23.963,-12.988,23.897,-12.998,23.843,-13.001,23.636,-13.001,23.339,-13.001,23.042,-13.001,22.744,-13.001,22.471,-13.001,22.21,-13.001,21.979,-13.001,21.979,-13.157,21.979,-13.478,21.979,-13.799,21.979,-14.12,21.979,-14.441,21.979,-14.761,21.98,-15.082,21.98,-15.403,21.98,-15.724,21.98,-15.956,22.04,-16.263,22.151,-16.597,22.194,-16.628,22.305,-16.69,22.459,-16.815,22.546,-16.91,22.722,-17.075,22.956,-17.286,23.182,-17.474,23.381,-17.641,23.068,-17.699,22.624,-17.782,22.324,-17.837,21.961,-17.905,21.718,-17.948,21.417,-18.001,21.369,-18,21.288,-17.963,21.113,-17.956,20.908,-18.006,20.746,-18.02,20.625,-17.997,20.508,-17.953,20.393,-17.887,20.194,-17.864,19.912,-17.881,19.639,-17.869,19.377,-17.825,19.189,-17.808,19.076,-17.818,18.955,-17.804,18.826,-17.766,18.718,-17.703,18.588,-17.57,18.487,-17.443,18.46,-17.425,18.428,-17.405,18.396,-17.399,18.109,-17.396,17.835,-17.393,17.679,-17.393,17.296,-17.392,16.914,-17.391,16.531,-17.391,16.148,-17.39,15.766,-17.39,15.383,-17.389,15.001,-17.389,14.618,-17.388,14.415,-17.388,14.226,-17.398,14.017,-17.409,13.987,-17.404,13.938,-17.389,13.904,-17.361,13.792,-17.288,13.694,-17.233,13.562,-17.141,13.476,-17.04,13.404,-17.008,13.276,-16.99,13.179,-16.972,13.101,-16.968,12.963,-17.015,12.859,-17.063,12.785,-17.108,12.657,-17.161,12.548,-17.213,12.359,-17.206,12.318,-17.213,12.213,-17.21,12.114,-17.165,12.014,-17.169,11.903,-17.227,11.743,-17.249,11.78,-16.871,11.819,-16.704,11.82,-16.504,11.797,-15.986,11.769,-15.915,11.751,-15.832,11.85,-15.768,11.9,-15.72,11.968,-15.634,12.016,-15.514,12.073,-15.248,12.28,-14.637,12.379,-14.039,12.504,-13.755,12.55,-13.438,12.898,-13.028,12.983,-12.776,13.163,-12.652,13.417,-12.52,13.598,-12.286,13.686,-12.124,13.785,-11.813,13.784,-11.488,13.847,-11.054,13.834,-10.93,13.739,-10.757,13.721,-10.634,13.633,-10.512,13.539,-10.421,13.495,-10.257,13.332,-9.999,13.288,-9.827,13.209,-9.703,13.197,-9.551,13.156,-9.39,13.076,-9.23,12.999,-9.048,12.999,-8.991,13.047,-8.922,13.093,-8.9,13.077,-8.934,13.047,-8.975,13.054,-9.007,13.359,-8.687,13.378,-8.625,13.368,-8.555,13.366,-8.469,13.379,-8.37,13.091,-7.78,12.862,-7.232,12.823,-6.955,12.521,-6.59,12.402,-6.353,12.334,-6.187,12.283,-6.124,12.303,-6.093,12.38,-6.084,12.554,-6.046,12.791,-6.004,13.01,-5.908,13.068,-5.865,13.184,-5.856,13.303,-5.882,13.346,-5.863,13.371,-5.862,13.649,-5.862,13.765,-5.855,13.979,-5.857,14.114,-5.865,14.191,-5.876,14.399,-5.893,14.658,-5.889,14.749,-5.88,15.089,-5.875,15.425,-5.869,15.727,-5.864,16.06,-5.865,16.315,-5.866,16.431,-5.9,16.537,-5.966,16.585,-6.025,16.608,-6.052,16.64,-6.115,16.697,-6.164,16.718,-6.241,16.701,-6.346,16.709,-6.472,16.743,-6.618,16.813,-6.773,16.919,-6.934,16.966,-7.062,16.952,-7.157,16.985,-7.257,17.064,-7.363,17.122,-7.419,17.155,-7.461,17.245,-7.623,17.411,-7.882,17.536,-8.076,17.58,-8.099,17.643,-8.091,17.779,-8.071,17.913,-8.068,18.009,-8.108,18.047,-8.101,18.192,-8.024,18.335,-8,18.485,-7.969,18.563,-7.936,18.653,-7.936,18.898,-7.998,18.944,-8.001,19.143,-8.001,19.341,-7.967,19.37,-7.707,19.372,-7.655,19.419,-7.557,19.48,-7.472,19.487,-7.391,19.484,-7.279,19.528,-7.144,19.66,-7.037,19.875,-6.986,19.997,-6.976,20.19,-6.946,20.482,-6.916,20.59,-6.92,20.599,-6.935,20.537,-7.122,20.536,-7.183,20.558,-7.244,20.608,-7.278,20.911,-7.281,21.19,-7.285,21.511,-7.297,21.751,-7.305,21.782,-7.315,21.806,-7.329,21.842,-7.421,21.834,-7.602,21.78,-7.865,21.801,-8.112,21.896,-8.341,21.905,-8.693,21.872,-8.904,21.829,-9.168,21.813,-9.469,21.857,-9.594,21.949,-9.726,22.089,-9.863,22.198,-10.041,22.275,-10.259,22.302,-10.397,22.282,-10.453,22.283,-10.552,22.307,-10.691,22.28,-10.784,22.204,-10.829,22.178,-10.892,22.217,-11.013,22.226,-11.122,22.257,-11.164,22.279,-11.194,22.315,-11.199,22.393,-11.159,22.486,-11.087,22.561,-11.056,22.667,-11.06,22.815,-11.08,23.076,-11.088,23.157,-11.075,23.4,-10.976,23.464,-10.969,23.56,-10.979,23.696,-11.008,23.834,-11.014,23.901,-10.983,23.907,-10.943,23.929,-10.892,23.967,-10.872]]]},{"n":"Andorra","c":"AD","r":6,"ml":5,"lx":1.54,"ly":42.55,"b":[1.415,42.434,1.74,42.643],"p":[[[1.706,42.503,1.679,42.497,1.586,42.456,1.534,42.442,1.486,42.434,1.449,42.437,1.428,42.461,1.43,42.498,1.422,42.531,1.415,42.548,1.428,42.596,1.459,42.622,1.501,42.643,1.568,42.635,1.71,42.604,1.739,42.576,1.74,42.557,1.714,42.526,1.706,42.503]]]},{"n":"Algeria","c":"DZ","r":3,"ml":2.5,"lx":2.81,"ly":27.4,"b":[-8.683,18.987,11.968,37.092],"p":[[[8.577,36.937,8.598,36.884,8.601,36.834,8.507,36.788,8.444,36.761,8.37,36.633,8.231,36.545,8.208,36.519,8.209,36.495,8.303,36.456,8.334,36.418,8.349,36.368,8.307,36.189,8.28,36.051,8.246,35.871,8.247,35.802,8.283,35.719,8.318,35.655,8.329,35.582,8.316,35.403,8.36,35.3,8.394,35.204,8.312,35.085,8.277,34.979,8.255,34.829,8.246,34.734,8.193,34.646,8.123,34.564,8.046,34.513,7.949,34.469,7.838,34.41,7.749,34.254,7.554,34.125,7.514,34.081,7.496,33.977,7.5,33.832,7.534,33.718,7.628,33.549,7.709,33.362,7.731,33.269,7.763,33.233,7.877,33.172,8.076,33.089,8.113,33.055,8.211,32.927,8.304,32.696,8.333,32.544,8.515,32.422,8.683,32.31,8.844,32.212,9.019,32.105,9.044,32.072,9.102,31.846,9.16,31.621,9.224,31.374,9.288,31.125,9.363,30.833,9.406,30.667,9.458,30.465,9.519,30.229,9.421,30.179,9.31,30.115,9.391,29.994,9.546,29.796,9.64,29.636,9.673,29.567,9.746,29.369,9.805,29.177,9.821,29.115,9.843,28.967,9.816,28.56,9.858,28.043,9.916,27.786,9.825,27.553,9.748,27.331,9.753,27.219,9.795,27.045,9.837,26.916,9.894,26.848,9.883,26.631,9.859,26.552,9.685,26.438,9.491,26.334,9.438,26.246,9.422,26.147,9.448,26.067,9.581,25.89,9.781,25.624,10.001,25.332,10.019,25.259,10.028,25.051,10.12,24.79,10.219,24.676,10.256,24.591,10.326,24.53,10.396,24.486,10.439,24.48,10.686,24.551,11.108,24.434,11.508,24.314,11.537,24.291,11.624,24.14,11.767,23.893,11.873,23.695,11.968,23.518,11.45,23.213,10.932,22.907,10.414,22.602,9.896,22.297,9.379,21.991,8.861,21.686,8.343,21.381,7.825,21.076,7.482,20.873,7.263,20.694,6.989,20.471,6.731,20.248,6.527,20.073,6.263,19.846,6.131,19.732,5.837,19.479,5.748,19.434,5.359,19.36,5.001,19.291,4.671,19.228,4.446,19.185,4.228,19.143,3.91,19.084,3.683,19.042,3.439,18.996,3.401,18.988,3.356,18.987,3.323,18.988,3.256,19.013,3.174,19.073,3.12,19.103,3.106,19.15,3.138,19.212,3.177,19.268,3.192,19.312,3.22,19.345,3.254,19.373,3.256,19.411,3.227,19.474,3.202,19.56,3.203,19.718,3.203,19.771,3.204,19.79,3.13,19.85,2.992,19.917,2.866,19.956,2.808,19.969,2.668,19.993,2.474,20.035,2.406,20.064,2.281,20.21,2.219,20.248,1.929,20.273,1.832,20.297,1.753,20.332,1.685,20.378,1.647,20.459,1.636,20.524,1.611,20.556,1.29,20.714,1.209,20.767,1.166,20.817,1.164,20.891,1.173,20.982,1.159,21.063,1.146,21.102,0.999,21.198,0.672,21.412,0.344,21.626,0.017,21.84,-0.311,22.054,-0.638,22.268,-0.965,22.482,-1.293,22.697,-1.62,22.911,-1.948,23.125,-2.275,23.339,-2.603,23.553,-2.93,23.767,-3.258,23.981,-3.585,24.195,-3.913,24.409,-4.24,24.624,-4.517,24.804,-4.823,24.996,-5.05,25.135,-5.275,25.275,-5.517,25.424,-5.675,25.516,-5.863,25.627,-6.051,25.738,-6.239,25.848,-6.427,25.959,-6.615,26.069,-6.803,26.18,-6.991,26.291,-7.179,26.401,-7.367,26.512,-7.555,26.622,-7.743,26.733,-7.931,26.844,-8.119,26.954,-8.307,27.065,-8.495,27.175,-8.683,27.286,-8.683,27.49,-8.683,27.656,-8.683,27.9,-8.683,28.112,-8.683,28.324,-8.683,28.469,-8.683,28.621,-8.678,28.689,-8.66,28.719,-8.558,28.768,-8.399,28.88,-8.34,28.93,-8.265,28.981,-7.999,29.132,-7.944,29.175,-7.685,29.35,-7.625,29.375,-7.486,29.392,-7.428,29.425,-7.35,29.495,-7.235,29.575,-7.16,29.613,-7.142,29.62,-7.095,29.625,-6.856,29.602,-6.755,29.584,-6.635,29.569,-6.598,29.579,-6.566,29.604,-6.521,29.66,-6.511,29.726,-6.508,29.784,-6.501,29.809,-6.48,29.82,-6.428,29.816,-6.358,29.808,-6.215,29.811,-6.167,29.819,-6.004,29.831,-5.775,29.869,-5.593,29.918,-5.449,29.957,-5.294,30.059,-5.18,30.166,-5.062,30.326,-4.968,30.465,-4.779,30.552,-4.62,30.605,-4.529,30.626,-4.323,30.699,-4.149,30.81,-3.985,30.914,-3.86,30.927,-3.702,30.944,-3.667,30.964,-3.627,31.001,-3.625,31.066,-3.673,31.111,-3.73,31.135,-3.771,31.162,-3.812,31.167,-3.833,31.198,-3.821,31.255,-3.815,31.309,-3.789,31.362,-3.796,31.437,-3.837,31.512,-3.85,31.566,-3.847,31.62,-3.827,31.662,-3.768,31.69,-3.7,31.7,-3.605,31.687,-3.44,31.705,-3.017,31.834,-2.988,31.874,-2.961,31.964,-2.931,32.043,-2.887,32.069,-2.863,32.075,-2.723,32.096,-2.523,32.126,-2.448,32.13,-2.231,32.121,-2.073,32.115,-1.817,32.105,-1.635,32.1,-1.477,32.095,-1.275,32.089,-1.226,32.107,-1.226,32.165,-1.262,32.271,-1.24,32.338,-1.163,32.399,-1.066,32.468,-1.111,32.552,-1.188,32.608,-1.296,32.676,-1.352,32.703,-1.45,32.785,-1.51,32.878,-1.551,33.074,-1.625,33.183,-1.679,33.319,-1.631,33.567,-1.703,33.717,-1.714,33.782,-1.715,33.858,-1.693,33.99,-1.707,34.176,-1.792,34.368,-1.752,34.433,-1.733,34.467,-1.739,34.496,-1.817,34.557,-1.85,34.607,-1.832,34.655,-1.792,34.723,-1.796,34.752,-1.921,34.836,-2.132,34.971,-2.191,35.03,-2.22,35.104,-2.018,35.085,-1.913,35.094,-1.674,35.183,-1.484,35.303,-1.336,35.364,-1.205,35.496,-1.088,35.579,-0.917,35.668,-0.426,35.862,-0.351,35.863,-0.189,35.819,-0.048,35.833,0.048,35.901,0.152,36.063,0.312,36.162,0.515,36.262,0.791,36.357,0.972,36.444,1.257,36.52,1.975,36.568,2.343,36.61,2.593,36.601,2.846,36.739,2.973,36.784,3.521,36.795,3.779,36.896,4.758,36.896,4.878,36.862,4.995,36.808,5.196,36.677,5.295,36.648,5.425,36.675,5.725,36.8,6.065,36.864,6.249,36.938,6.328,37.046,6.487,37.086,6.576,37.003,6.928,36.919,7.143,36.943,7.238,36.969,7.204,37.092,7.432,37.059,7.608,37,7.792,36.88,7.91,36.856,8.127,36.91,8.577,36.937]]]},{"n":"Albania","c":"AL","r":6,"ml":5,"lx":20.11,"ly":40.65,"b":[19.281,39.654,21.031,42.648],"p":[[[19.342,41.869,19.346,41.919,19.361,41.998,19.352,42.024,19.361,42.069,19.331,42.129,19.281,42.173,19.329,42.249,19.4,42.342,19.465,42.415,19.545,42.492,19.597,42.565,19.654,42.629,19.703,42.648,19.728,42.635,19.741,42.607,19.738,42.525,19.754,42.497,19.788,42.476,19.86,42.486,19.939,42.507,20.046,42.55,20.064,42.547,20.104,42.525,20.186,42.426,20.241,42.339,20.348,42.309,20.408,42.275,20.485,42.223,20.523,42.171,20.575,42.013,20.581,41.917,20.566,41.874,20.553,41.862,20.505,41.706,20.517,41.627,20.516,41.575,20.476,41.554,20.449,41.521,20.492,41.391,20.487,41.336,20.489,41.273,20.568,41.128,20.614,41.083,20.656,41.062,20.709,40.928,20.741,40.905,20.87,40.918,20.933,40.903,20.959,40.872,20.964,40.85,20.956,40.775,20.988,40.718,21.031,40.659,21.031,40.622,21.002,40.563,20.95,40.494,20.882,40.468,20.806,40.445,20.77,40.392,20.752,40.335,20.718,40.293,20.697,40.246,20.665,40.152,20.657,40.117,20.606,40.083,20.527,40.069,20.456,40.066,20.408,40.049,20.384,40.017,20.338,39.991,20.311,39.979,20.311,39.951,20.344,39.891,20.382,39.842,20.382,39.803,20.364,39.792,20.306,39.797,20.294,39.782,20.288,39.739,20.272,39.701,20.248,39.678,20.207,39.654,20.131,39.662,20.06,39.699,20.023,39.711,20.001,39.709,19.996,39.801,19.965,39.872,19.852,40.044,19.485,40.21,19.398,40.285,19.36,40.348,19.322,40.407,19.359,40.409,19.395,40.394,19.441,40.376,19.459,40.405,19.439,40.47,19.345,40.622,19.338,40.664,19.384,40.791,19.461,40.933,19.456,41.106,19.48,41.236,19.453,41.321,19.441,41.425,19.497,41.563,19.546,41.597,19.576,41.64,19.578,41.788,19.468,41.856,19.342,41.869]]]},{"n":"Afghanistan","c":"AF","r":3,"ml":3,"lx":66.5,"ly":34.16,"b":[60.486,29.392,74.891,38.456],"p":[[[66.522,37.348,66.828,37.371,67.069,37.335,67.196,37.235,67.32,37.21,67.442,37.258,67.517,37.267,67.546,37.236,67.607,37.223,67.7,37.227,67.753,37.2,67.759,37.172,67.766,37.14,67.834,37.064,67.958,36.972,68.068,36.95,68.212,37.022,68.261,37.013,68.285,37.036,68.3,37.088,68.387,37.138,68.546,37.183,68.637,37.224,68.669,37.258,68.723,37.268,68.782,37.258,68.824,37.271,68.838,37.303,68.855,37.317,68.885,37.328,68.912,37.334,68.96,37.325,69.05,37.267,69.18,37.158,69.265,37.108,69.304,37.117,69.354,37.15,69.414,37.208,69.43,37.291,69.399,37.399,69.42,37.487,69.492,37.553,69.626,37.594,69.821,37.61,69.941,37.6,69.985,37.566,70.045,37.547,70.12,37.544,70.189,37.582,70.251,37.664,70.255,37.765,70.199,37.886,70.215,37.924,70.239,37.941,70.313,37.985,70.418,38.075,70.519,38.192,70.616,38.334,70.736,38.423,70.879,38.456,71.052,38.418,71.256,38.307,71.333,38.17,71.283,38.008,71.279,37.918,71.32,37.902,71.39,37.906,71.488,37.932,71.552,37.933,71.582,37.91,71.58,37.864,71.546,37.796,71.505,37.603,71.48,37.436,71.455,37.272,71.433,37.128,71.472,37.015,71.531,36.845,71.597,36.733,71.666,36.697,71.734,36.684,71.802,36.694,71.942,36.766,72.154,36.901,72.359,36.983,72.657,37.029,72.757,37.173,72.896,37.268,73.211,37.408,73.383,37.462,73.481,37.472,73.605,37.446,73.633,37.437,73.657,37.43,73.721,37.419,73.734,37.376,73.717,37.329,73.649,37.291,73.628,37.262,73.654,37.239,73.75,37.232,73.949,37.283,74.078,37.316,74.167,37.329,74.204,37.372,74.26,37.415,74.349,37.419,74.445,37.396,74.524,37.382,74.659,37.394,74.731,37.357,74.83,37.286,74.875,37.242,74.891,37.232,74.84,37.225,74.767,37.249,74.739,37.286,74.727,37.291,74.669,37.267,74.559,37.237,74.372,37.158,74.376,37.137,74.498,37.057,74.526,37.031,74.541,37.022,74.431,36.984,74.195,36.897,74.039,36.826,74.002,36.823,73.908,36.853,73.769,36.888,73.732,36.888,73.411,36.882,73.117,36.869,72.994,36.852,72.766,36.835,72.623,36.83,72.531,36.802,72.431,36.766,72.327,36.742,72.25,36.735,72.157,36.701,72.096,36.634,71.921,36.534,71.822,36.486,71.773,36.432,71.716,36.427,71.621,36.436,71.546,36.378,71.463,36.293,71.313,36.171,71.233,36.122,71.185,36.042,71.22,36.001,71.343,35.939,71.398,35.88,71.428,35.834,71.484,35.715,71.519,35.598,71.572,35.547,71.587,35.461,71.601,35.408,71.572,35.37,71.546,35.329,71.546,35.289,71.577,35.248,71.605,35.212,71.621,35.183,71.602,35.151,71.546,35.101,71.517,35.051,71.455,34.967,71.358,34.91,71.294,34.868,71.226,34.78,71.113,34.682,71.066,34.6,71.016,34.555,70.966,34.53,70.979,34.486,71.023,34.431,71.096,34.369,71.092,34.273,71.089,34.204,71.091,34.12,71.052,34.05,70.848,33.982,70.654,33.952,70.416,33.95,70.326,33.961,70.254,33.976,69.995,34.052,69.89,34.007,69.868,33.898,70.057,33.72,70.134,33.621,70.22,33.455,70.284,33.369,70.261,33.289,70.09,33.198,69.92,33.113,69.704,33.095,69.568,33.064,69.502,33.02,69.453,32.833,69.405,32.764,69.405,32.683,69.359,32.59,69.29,32.531,69.241,32.434,69.257,32.249,69.279,31.937,69.187,31.838,69.083,31.738,68.973,31.667,68.869,31.634,68.782,31.646,68.714,31.708,68.673,31.76,68.598,31.803,68.521,31.794,68.443,31.754,68.32,31.768,68.214,31.807,68.161,31.803,68.13,31.763,68.017,31.678,67.74,31.548,67.627,31.539,67.578,31.506,67.598,31.453,67.647,31.41,67.733,31.379,67.738,31.344,67.662,31.313,67.596,31.278,67.453,31.235,67.287,31.218,67.116,31.243,67.028,31.3,66.924,31.306,66.829,31.264,66.731,31.195,66.624,31.046,66.596,31.02,66.567,30.997,66.497,30.965,66.397,30.912,66.347,30.803,66.287,30.608,66.301,30.503,66.305,30.321,66.282,30.193,66.238,30.11,66.247,30.044,66.313,29.969,66.287,29.92,66.231,29.866,66.177,29.836,65.962,29.779,65.666,29.701,65.471,29.652,65.18,29.578,65.096,29.559,64.919,29.553,64.827,29.564,64.704,29.567,64.521,29.565,64.394,29.544,64.266,29.507,64.172,29.46,64.118,29.414,64.099,29.392,63.971,29.43,63.568,29.498,62.477,29.408,62.373,29.425,62.001,29.53,61.521,29.666,61.224,29.749,60.843,29.859,61.104,30.128,61.332,30.364,61.559,30.599,61.784,30.832,61.811,30.913,61.814,31.073,61.755,31.285,61.66,31.382,61.346,31.422,61.111,31.451,60.854,31.483,60.821,31.495,60.792,31.661,60.804,31.734,60.788,31.877,60.79,31.987,60.827,32.168,60.829,32.249,60.71,32.6,60.645,32.794,60.577,32.995,60.562,33.059,60.561,33.138,60.718,33.324,60.767,33.364,60.859,33.456,60.917,33.505,60.907,33.539,60.806,33.559,60.655,33.56,60.574,33.588,60.511,33.639,60.486,33.712,60.527,33.842,60.486,34.095,60.57,34.22,60.643,34.307,60.889,34.319,60.804,34.418,60.763,34.475,60.736,34.492,60.726,34.518,60.739,34.545,60.802,34.555,60.845,34.588,60.915,34.634,60.951,34.654,60.958,34.71,60.991,34.75,61.04,34.799,61.08,34.856,61.07,34.922,61.107,35.001,61.123,35.051,61.15,35.094,61.126,35.157,61.107,35.209,61.1,35.272,61.14,35.289,61.189,35.312,61.199,35.362,61.226,35.424,61.246,35.474,61.279,35.514,61.282,35.553,61.262,35.62,61.345,35.629,61.378,35.593,61.422,35.546,61.543,35.458,61.621,35.432,61.72,35.419,61.841,35.431,61.938,35.448,61.984,35.444,62.09,35.38,62.213,35.29,62.253,35.25,62.271,35.189,62.308,35.171,62.387,35.231,62.463,35.251,62.533,35.24,62.611,35.233,62.688,35.255,62.723,35.271,62.858,35.35,62.98,35.409,63.057,35.446,63.084,35.568,63.119,35.638,63.17,35.678,63.151,35.728,63.13,35.767,63.109,35.819,63.13,35.846,63.179,35.858,63.302,35.858,63.517,35.913,63.697,35.968,63.863,36.012,63.938,36.02,64.01,36.012,64.042,36.025,64.051,36.068,64.092,36.113,64.184,36.149,64.358,36.226,64.511,36.341,64.566,36.428,64.603,36.555,64.674,36.75,64.753,36.965,64.782,37.059,64.816,37.132,64.952,37.194,65.09,37.238,65.304,37.247,65.555,37.251,65.608,37.368,65.641,37.468,65.683,37.519,65.744,37.561,65.765,37.569,65.901,37.508,66.108,37.415,66.35,37.368,66.472,37.345,66.522,37.348]]]},{"n":"Siachen Glacier","c":"","r":5,"ml":6.5,"lx":77.13,"ly":35.34,"b":[76.767,35.11,77.799,35.662],"p":[[[77.049,35.11,77.004,35.196,76.979,35.246,76.928,35.347,76.882,35.436,76.813,35.572,76.767,35.662,76.879,35.613,77.09,35.552,77.295,35.508,77.446,35.476,77.52,35.473,77.573,35.472,77.724,35.481,77.799,35.496,77.697,35.443,77.572,35.379,77.423,35.303,77.293,35.236,77.169,35.172,77.049,35.11]]]},{"n":"Antarctica","c":"AQ","r":4,"ml":4,"lx":35.89,"ly":-79.84,"b":[-180,-89.999,180,-60.521],"p":[[[-45.718,-60.521,-45.5,-60.546,-45.386,-60.583,-45.357,-60.624,-45.228,-60.64,-45.211,-60.648,-45.186,-60.672,-45.173,-60.699,-45.174,-60.733,-45.398,-60.65,-45.709,-60.645,-45.78,-60.586,-45.937,-60.62,-45.955,-60.597,-45.956,-60.568,-45.935,-60.527,-45.834,-60.543,-45.718,-60.521]],[[-90.536,-68.798,-90.547,-68.798,-90.567,-68.798,-90.581,-68.799,-90.595,-68.8,-90.609,-68.802,-90.617,-68.803,-90.62,-68.803,-90.624,-68.801,-90.628,-68.797,-90.638,-68.788,-90.647,-68.78,-90.652,-68.771,-90.648,-68.76,-90.637,-68.754,-90.637,-68.747,-90.637,-68.738,-90.631,-68.73,-90.628,-68.721,-90.629,-68.716,-90.626,-68.714,-90.617,-68.712,-90.6,-68.712,-90.59,-68.713,-90.571,-68.714,-90.562,-68.715,-90.548,-68.718,-90.535,-68.728,-90.525,-68.738,-90.525,-68.743,-90.523,-68.749,-90.521,-68.756,-90.519,-68.764,-90.515,-68.771,-90.518,-68.783,-90.526,-68.795,-90.533,-68.797,-90.536,-68.798]],[[-57.021,-63.373,-56.927,-63.506,-56.782,-63.572,-56.835,-63.631,-56.974,-63.625,-57.119,-63.638,-57.152,-63.572,-57.098,-63.524,-57.152,-63.479,-57.284,-63.491,-57.461,-63.514,-57.581,-63.547,-57.737,-63.617,-57.857,-63.657,-58.263,-63.763,-58.532,-63.915,-58.723,-64.077,-58.839,-64.187,-59.005,-64.195,-59.047,-64.234,-58.977,-64.266,-58.923,-64.279,-58.799,-64.293,-58.819,-64.339,-58.905,-64.352,-58.895,-64.389,-58.806,-64.445,-58.786,-64.524,-58.892,-64.537,-59.051,-64.451,-59.229,-64.444,-59.37,-64.404,-59.461,-64.346,-59.547,-64.359,-59.612,-64.44,-59.573,-64.531,-59.646,-64.584,-59.734,-64.559,-59.765,-64.451,-59.85,-64.434,-59.963,-64.431,-60.242,-64.547,-60.341,-64.551,-60.394,-64.609,-60.556,-64.677,-60.66,-64.729,-60.915,-64.907,-61.06,-64.981,-61.332,-65.024,-61.439,-65.018,-61.503,-65,-61.603,-64.988,-61.703,-64.987,-61.736,-65.033,-61.577,-65.186,-61.663,-65.239,-61.856,-65.235,-61.948,-65.192,-62.025,-65.233,-62.085,-65.273,-62.145,-65.332,-62.054,-65.457,-61.903,-65.513,-61.796,-65.523,-61.756,-65.569,-61.991,-65.589,-62.151,-65.699,-62.222,-65.775,-62.305,-65.84,-62.293,-65.916,-62.169,-66.031,-62.005,-66.113,-61.839,-66.12,-61.625,-66.095,-61.575,-66.071,-61.359,-66.059,-61.266,-65.98,-61.198,-65.975,-61.138,-65.989,-61.039,-65.992,-60.988,-65.94,-60.913,-65.921,-60.813,-65.934,-60.618,-65.933,-60.565,-65.979,-60.625,-66.032,-60.744,-66.105,-60.856,-66.065,-60.956,-66.072,-61.009,-66.111,-60.903,-66.191,-60.942,-66.264,-61.028,-66.337,-61.134,-66.29,-61.15,-66.212,-61.293,-66.165,-61.432,-66.145,-61.526,-66.226,-61.676,-66.25,-61.696,-66.343,-61.756,-66.429,-61.842,-66.403,-61.875,-66.296,-62.117,-66.209,-62.241,-66.197,-62.494,-66.219,-62.583,-66.217,-62.682,-66.237,-62.755,-66.31,-62.65,-66.364,-62.615,-66.436,-62.618,-66.49,-62.638,-66.511,-62.655,-66.556,-62.543,-66.621,-62.537,-66.707,-62.629,-66.706,-62.705,-66.68,-62.997,-66.453,-63.18,-66.353,-63.258,-66.264,-63.449,-66.244,-63.587,-66.242,-63.753,-66.278,-63.688,-66.32,-63.654,-66.383,-63.756,-66.409,-63.88,-66.506,-63.964,-66.589,-64.015,-66.607,-64.078,-66.654,-63.809,-66.761,-63.769,-66.803,-63.755,-66.873,-63.84,-66.912,-64.043,-66.927,-64.401,-66.853,-64.554,-66.852,-64.607,-66.8,-64.686,-66.806,-64.735,-66.894,-64.793,-66.972,-64.878,-67.025,-64.854,-67.105,-64.785,-67.124,-64.839,-67.156,-64.951,-67.183,-65.027,-67.214,-64.858,-67.243,-64.826,-67.269,-64.819,-67.307,-65.08,-67.335,-65.249,-67.342,-65.35,-67.311,-65.443,-67.326,-65.503,-67.377,-65.523,-67.445,-65.504,-67.528,-65.471,-67.588,-65.447,-67.61,-65.418,-67.66,-65.574,-67.788,-65.589,-67.816,-65.6,-67.876,-65.528,-67.93,-65.469,-68.009,-65.551,-68.048,-65.64,-68.131,-65.546,-68.147,-65.387,-68.15,-65.218,-68.14,-64.959,-68.068,-64.885,-68.056,-64.853,-68.083,-64.829,-68.127,-64.896,-68.168,-65.365,-68.287,-65.452,-68.337,-65.331,-68.364,-65.09,-68.37,-64.996,-68.408,-65.055,-68.449,-65.14,-68.489,-65.242,-68.583,-65.158,-68.618,-64.898,-68.673,-64.429,-68.746,-64.078,-68.771,-64.157,-68.687,-64.169,-68.583,-63.924,-68.498,-63.796,-68.47,-63.217,-68.419,-63.057,-68.421,-62.933,-68.443,-62.98,-68.486,-63.115,-68.471,-63.348,-68.499,-63.707,-68.592,-63.773,-68.632,-63.747,-68.705,-63.443,-68.764,-63.344,-68.81,-63.478,-68.951,-63.456,-69.042,-63.301,-69.141,-63.094,-69.253,-62.994,-69.329,-62.84,-69.372,-62.587,-69.477,-62.451,-69.584,-62.407,-69.827,-62.202,-70.028,-61.961,-70.12,-61.935,-70.2,-62.014,-70.279,-62.218,-70.233,-62.331,-70.279,-62.378,-70.365,-62.232,-70.424,-62.001,-70.497,-61.505,-70.491,-61.491,-70.57,-61.605,-70.617,-61.696,-70.676,-61.809,-70.709,-61.994,-70.729,-62.04,-70.801,-61.961,-70.901,-61.702,-70.857,-61.513,-70.851,-61.313,-70.868,-61.252,-71.002,-61.017,-71.167,-60.962,-71.245,-61.003,-71.319,-61.148,-71.342,-61.237,-71.401,-61.369,-71.452,-61.516,-71.479,-61.79,-71.616,-61.91,-71.631,-61.959,-71.658,-61.725,-71.673,-61.563,-71.675,-61.214,-71.564,-61.081,-71.589,-60.995,-71.661,-60.949,-71.747,-61.035,-71.82,-61.645,-71.863,-61.939,-71.904,-62.257,-72.018,-61.894,-72.071,-61.628,-72.053,-61.493,-72.073,-61.31,-72.113,-61.107,-72.092,-60.952,-72.05,-60.833,-72.052,-60.719,-72.073,-60.704,-72.144,-60.691,-72.27,-60.665,-72.362,-60.73,-72.426,-61.048,-72.471,-61.28,-72.468,-61.286,-72.601,-60.939,-72.7,-60.724,-72.647,-60.532,-72.673,-60.532,-72.832,-60.385,-73.007,-60.254,-73.017,-60.149,-72.938,-60.01,-72.938,-59.957,-73.031,-60.016,-73.189,-60.122,-73.275,-60.404,-73.24,-60.561,-73.211,-60.687,-73.271,-60.896,-73.32,-61.081,-73.328,-61.242,-73.25,-61.428,-73.191,-61.726,-73.161,-62.008,-73.148,-61.915,-73.216,-61.788,-73.255,-61.738,-73.375,-61.637,-73.5,-61.405,-73.467,-61.08,-73.539,-60.879,-73.612,-60.79,-73.712,-60.903,-73.871,-61.088,-73.929,-61.203,-73.957,-61.404,-73.896,-61.545,-73.896,-61.692,-73.924,-61.742,-73.996,-61.838,-74.032,-61.319,-74.036,-61.161,-74.056,-61.042,-74.122,-61.227,-74.208,-61.571,-74.195,-61.718,-74.228,-61.843,-74.29,-61.332,-74.329,-61.121,-74.307,-60.784,-74.241,-60.704,-74.307,-60.838,-74.373,-61.011,-74.478,-61.37,-74.512,-61.64,-74.514,-61.995,-74.476,-62.089,-74.453,-62.235,-74.441,-62.226,-74.506,-62.133,-74.55,-61.894,-74.713,-61.855,-74.777,-61.928,-74.863,-62.138,-74.926,-62.372,-74.952,-62.567,-74.896,-62.708,-74.737,-62.887,-74.691,-63.072,-74.678,-63.178,-74.684,-63.168,-74.765,-63.125,-74.85,-63.198,-74.909,-63.357,-74.878,-63.559,-74.906,-63.751,-74.952,-63.925,-75.004,-63.571,-75.03,-63.337,-75.035,-63.173,-75.115,-63.231,-75.154,-63.551,-75.171,-63.858,-75.206,-64.28,-75.293,-63.972,-75.329,-63.678,-75.328,-63.475,-75.336,-63.304,-75.352,-63.258,-75.399,-63.363,-75.451,-64.053,-75.58,-64.778,-75.738,-65.044,-75.787,-65.322,-75.815,-65.966,-75.952,-66.37,-76.013,-67.518,-76.11,-69.304,-76.351,-69.915,-76.522,-70.096,-76.654,-70.21,-76.674,-70.551,-76.718,-70.895,-76.739,-71.799,-76.753,-72.722,-76.689,-73.472,-76.675,-73.88,-76.697,-75.268,-76.581,-75.444,-76.587,-75.659,-76.608,-75.831,-76.608,-75.963,-76.592,-76.244,-76.585,-77.19,-76.63,-77.287,-76.702,-77.168,-76.834,-76.824,-76.993,-76.249,-77.275,-75.937,-77.334,-75.748,-77.398,-75.387,-77.474,-74.581,-77.478,-73.478,-77.536,-72.852,-77.59,-72.875,-77.694,-73.252,-77.894,-73.485,-77.971,-74.042,-78.109,-74.812,-78.178,-75.398,-78.158,-76.438,-78.044,-77.742,-77.94,-79.679,-77.843,-80.104,-77.797,-80.602,-77.752,-80.889,-77.798,-81.103,-77.842,-81.581,-77.846,-81.441,-77.886,-79.51,-78.154,-77.858,-78.351,-77.665,-78.401,-77.433,-78.435,-77.452,-78.56,-77.545,-78.66,-77.869,-78.746,-78.712,-78.752,-79.767,-78.821,-80.292,-78.823,-80.816,-78.754,-81.929,-78.559,-82.608,-78.412,-83.083,-78.247,-83.412,-78.115,-83.779,-77.984,-83.752,-78.066,-83.688,-78.148,-83.508,-78.248,-83.246,-78.357,-83.227,-78.402,-83.355,-78.408,-83.544,-78.355,-83.706,-78.404,-83.763,-78.461,-83.697,-78.537,-83.595,-78.611,-83.26,-78.774,-82.971,-78.817,-82.589,-78.916,-81.661,-79.1,-81.503,-79.163,-81.222,-79.298,-81.163,-79.4,-80.892,-79.502,-80.705,-79.517,-80.535,-79.513,-80.479,-79.426,-80.489,-79.321,-80.416,-79.295,-80.151,-79.268,-79.456,-79.304,-76.499,-79.326,-76.218,-79.387,-76.105,-79.465,-76.032,-79.627,-76.344,-79.821,-76.558,-79.904,-76.904,-79.955,-77.222,-79.994,-77.702,-80.01,-78.692,-79.995,-79.66,-79.997,-78.907,-80.09,-78.176,-80.167,-77.16,-80.153,-76.757,-80.131,-76.407,-80.095,-76.26,-80.16,-75.986,-80.295,-75.822,-80.338,-75.709,-80.383,-75.555,-80.531,-75.495,-80.617,-75.345,-80.719,-75.237,-80.803,-75.076,-80.86,-74.807,-80.887,-74.511,-80.838,-73.938,-80.816,-73.383,-80.894,-73.029,-80.917,-72.553,-80.853,-72.174,-80.764,-71.38,-80.682,-71.231,-80.647,-71.018,-80.619,-70.688,-80.626,-70.56,-80.647,-70.392,-80.735,-70.239,-80.857,-70.012,-80.918,-69.772,-80.962,-69.634,-80.966,-69.182,-81.005,-68.59,-80.968,-68.327,-81.004,-68.285,-81.074,-68.144,-81.13,-67.965,-81.148,-65.574,-81.461,-64.75,-81.522,-63.478,-81.553,-62.49,-81.557,-62.354,-81.577,-62.165,-81.636,-62.542,-81.678,-62.946,-81.684,-63.554,-81.667,-63.769,-81.676,-64.233,-81.66,-64.476,-81.672,-64.696,-81.652,-65.022,-81.696,-65.62,-81.729,-65.487,-81.775,-65.264,-81.786,-64.811,-81.803,-64.19,-81.795,-64.137,-81.869,-64.706,-81.887,-65.916,-81.902,-66.042,-81.914,-66.134,-81.953,-65.953,-81.971,-65.844,-81.993,-65.787,-82.046,-65.913,-82.183,-65.714,-82.279,-65.572,-82.294,-65.424,-82.28,-65.17,-82.318,-64.92,-82.371,-64.397,-82.374,-63.773,-82.304,-63.466,-82.307,-62.645,-82.263,-61.902,-82.271,-60.859,-82.187,-60.687,-82.189,-60.528,-82.2,-60.817,-82.276,-62.095,-82.467,-62.553,-82.503,-62.736,-82.527,-62.631,-82.621,-62.466,-82.718,-62.129,-82.822,-61.917,-82.977,-61.709,-83.01,-61.313,-82.939,-61.218,-82.992,-61.2,-83.098,-61.303,-83.184,-61.436,-83.232,-61.531,-83.279,-61.59,-83.341,-61.425,-83.396,-60.983,-83.428,-60.397,-83.441,-59.854,-83.442,-59.516,-83.458,-58.29,-83.121,-57.798,-82.959,-57.557,-82.89,-57.354,-82.84,-56.318,-82.633,-56.075,-82.57,-55.801,-82.478,-55.295,-82.465,-54.601,-82.316,-53.986,-82.201,-53.74,-82.178,-53.558,-82.169,-53.339,-82.145,-52.799,-82.154,-52.415,-82.135,-51.731,-82.062,-51.21,-82.015,-50.653,-81.975,-50.029,-81.968,-48.361,-81.892,-47.887,-81.925,-47.36,-82.004,-47.02,-82.003,-46.567,-81.979,-46.258,-81.947,-46.119,-82.04,-46.046,-82.159,-46.199,-82.271,-46.448,-82.34,-46.517,-82.455,-46.175,-82.512,-45.789,-82.495,-45.044,-82.438,-44.455,-82.366,-44.292,-82.318,-44.064,-82.331,-43.669,-82.27,-43.18,-82.017,-42.565,-81.762,-42.046,-81.598,-41.712,-81.408,-41.434,-81.298,-41.126,-81.215,-40.915,-81.172,-40.441,-81.165,-39.762,-81.032,-38.772,-80.882,-38.011,-80.954,-37.209,-81.064,-36.812,-80.975,-36.5,-80.96,-36.234,-80.921,-35.966,-80.891,-35.776,-80.813,-35.521,-80.746,-35.327,-80.651,-34.35,-80.603,-33.329,-80.54,-33.191,-80.519,-33.057,-80.532,-32.706,-80.514,-32.256,-80.461,-31.634,-80.445,-31.312,-80.45,-31.015,-80.308,-30.425,-80.28,-29.797,-80.223,-29.531,-80.182,-29.329,-80.172,-24.24,-80.062,-24.02,-80.009,-23.574,-79.965,-23.407,-79.859,-24.088,-79.815,-24.3,-79.771,-24.534,-79.758,-24.67,-79.775,-25.259,-79.762,-29.949,-79.599,-30.049,-79.585,-30.211,-79.485,-30.178,-79.304,-30.316,-79.163,-30.645,-79.124,-30.985,-79.128,-31.413,-79.145,-32.542,-79.222,-32.994,-79.229,-34.197,-79.11,-34.995,-78.978,-35.516,-78.933,-35.89,-78.844,-36.239,-78.774,-36.266,-78.616,-36.181,-78.468,-35.509,-78.041,-35.088,-77.837,-34.808,-77.821,-34.551,-77.729,-34.29,-77.522,-34.076,-77.425,-33.591,-77.311,-33.377,-77.282,-32.614,-77.141,-32.405,-77.136,-32.063,-77.16,-31.676,-77.033,-30.489,-76.762,-30.222,-76.66,-29.892,-76.598,-28.934,-76.37,-28.079,-76.258,-27.653,-76.226,-27.135,-76.157,-26.56,-76.055,-26.059,-75.957,-24.27,-75.767,-23.197,-75.718,-22.465,-75.661,-21.948,-75.694,-21.434,-75.683,-20.989,-75.634,-20.783,-75.594,-20.487,-75.492,-19.493,-75.54,-18.851,-75.47,-18.585,-75.463,-18.305,-75.431,-18.415,-75.396,-18.517,-75.39,-18.617,-75.342,-18.749,-75.242,-18.617,-75.115,-18.517,-75.052,-18.221,-74.975,-18.068,-74.863,-17.923,-74.699,-17.436,-74.379,-17.299,-74.334,-16.989,-74.32,-16.727,-74.328,-16.43,-74.324,-15.673,-74.407,-15.531,-74.376,-15.29,-74.281,-15.089,-74.163,-14.659,-73.989,-14.574,-73.937,-14.611,-73.852,-15.26,-73.889,-15.749,-73.946,-16.22,-73.916,-16.282,-73.867,-16.181,-73.83,-16.003,-73.816,-15.936,-73.758,-16.097,-73.709,-16.388,-73.681,-16.519,-73.644,-16.507,-73.556,-16.435,-73.426,-16.279,-73.388,-16.149,-73.334,-15.803,-73.152,-15.596,-73.097,-15.007,-73.047,-14.321,-73.123,-14.165,-73.102,-14,-73.001,-14.168,-72.843,-14.298,-72.785,-14.298,-72.733,-13.939,-72.756,-13.603,-72.792,-13.209,-72.785,-12.747,-72.629,-12.095,-72.498,-11.777,-72.444,-11.497,-72.413,-11.346,-72.282,-11.121,-72.032,-10.958,-71.902,-10.961,-71.822,-11.009,-71.758,-11.179,-71.777,-11.333,-71.786,-11.697,-71.719,-12.148,-71.614,-12.285,-71.495,-12.351,-71.39,-12.208,-71.332,-12.074,-71.297,-11.926,-71.289,-11.663,-71.331,-11.328,-71.44,-11.16,-71.481,-10.97,-71.56,-10.825,-71.553,-10.659,-71.443,-10.52,-71.296,-10.407,-71.25,-10.231,-71.201,-10.033,-71.131,-10.122,-71.061,-10.331,-71.024,-10.36,-70.982,-10.271,-70.936,-10.099,-70.926,-9.888,-71.027,-9.599,-71.095,-9.402,-71.118,-9.231,-71.174,-8.966,-71.361,-8.646,-71.673,-8.498,-71.675,-8.216,-71.647,-7.916,-71.635,-7.714,-71.546,-7.669,-71.324,-7.59,-71.224,-7.618,-71.121,-7.757,-71.017,-7.873,-70.94,-7.855,-70.885,-7.753,-70.843,-7.62,-70.829,-7.388,-70.787,-7.032,-70.835,-6.838,-70.845,-6.548,-70.817,-6.245,-70.756,-5.936,-70.713,-5.695,-70.745,-5.588,-70.857,-5.709,-70.968,-5.904,-71.052,-6.08,-71.154,-6.127,-71.266,-6.117,-71.326,-5.95,-71.342,-4.45,-71.328,-4.253,-71.338,-3.995,-71.339,-3.713,-71.375,-3.24,-71.36,-2.812,-71.321,-2.61,-71.321,-2.261,-71.357,-2.015,-71.433,-1.501,-71.412,-1.354,-71.387,-1.216,-71.284,-1.068,-71.266,-0.896,-71.349,-0.84,-71.54,-0.76,-71.63,-0.543,-71.713,-0.327,-71.642,-0.185,-71.559,0.154,-71.398,0.538,-71.274,0.835,-71.202,1.552,-71.08,1.909,-71.004,2.609,-70.9,3.507,-70.844,5.113,-70.656,5.644,-70.636,6.508,-70.586,6.951,-70.535,7.401,-70.494,7.677,-70.356,8.307,-70.462,8.523,-70.474,8.817,-70.391,9.142,-70.184,9.613,-70.269,9.886,-70.403,10.218,-70.508,10.969,-70.688,11.204,-70.729,11.701,-70.767,11.834,-70.737,12.068,-70.617,12.309,-70.443,12.462,-70.37,12.682,-70.309,12.929,-70.213,12.865,-70.162,12.723,-70.144,12.595,-70.117,12.626,-70.066,13.066,-70.054,13.298,-70.23,13.533,-70.287,13.823,-70.343,14.492,-70.3,15.064,-70.295,15.563,-70.331,15.807,-70.324,16.025,-70.193,16.381,-70.145,16.585,-70.204,16.709,-70.397,17.167,-70.451,18.125,-70.54,18.232,-70.518,18.351,-70.416,18.433,-70.29,18.627,-70.269,18.877,-70.201,19.009,-70.212,19.196,-70.293,19.132,-70.492,19.027,-70.674,19.153,-70.821,19.265,-70.902,19.409,-70.917,19.652,-70.921,19.944,-70.91,20.128,-70.918,21.071,-70.843,21.186,-70.681,21.337,-70.495,21.705,-70.258,21.849,-70.277,21.962,-70.3,22.216,-70.417,22.366,-70.475,22.396,-70.561,22.234,-70.643,22.278,-70.696,22.445,-70.74,22.979,-70.81,23.15,-70.796,23.407,-70.723,23.665,-70.575,23.804,-70.405,24.024,-70.413,24.236,-70.449,24.386,-70.537,24.386,-70.704,24.588,-70.82,24.757,-70.892,25.187,-70.971,25.65,-70.991,25.974,-71.037,26.499,-71.02,26.754,-70.967,26.918,-70.954,27.207,-70.911,27.509,-70.813,27.698,-70.772,28.386,-70.682,28.912,-70.583,29.464,-70.406,30.003,-70.3,30.834,-70.246,31.063,-70.225,31.379,-70.226,32.16,-70.1,32.457,-70.026,32.621,-70.001,32.81,-69.909,32.912,-69.734,32.989,-69.624,32.976,-69.517,32.903,-69.379,32.738,-69.255,32.568,-69.074,32.642,-68.869,32.776,-68.783,33.121,-68.689,33.466,-68.671,33.854,-68.683,34.193,-68.702,34.219,-68.791,34.074,-68.885,33.885,-68.979,33.772,-69.02,33.814,-69.099,34.059,-69.111,34.596,-69.095,34.75,-69.168,35.131,-69.487,35.225,-69.637,35.357,-69.681,35.568,-69.66,36.018,-69.662,36.331,-69.639,36.586,-69.638,36.719,-69.652,36.856,-69.726,37.115,-69.81,37.375,-69.748,37.56,-69.718,37.787,-69.726,38.144,-69.824,38.499,-70.056,38.886,-70.172,38.912,-70.098,38.859,-70.006,39.019,-69.924,39.211,-69.786,39.487,-69.608,39.705,-69.426,39.762,-69.173,39.864,-68.967,40.042,-68.868,40.216,-68.805,40.484,-68.739,40.817,-68.724,41.133,-68.575,41.356,-68.515,41.825,-68.433,42.409,-68.352,42.82,-68.123,42.961,-68.095,43.171,-68.06,43.554,-68.046,44.178,-67.972,44.373,-67.961,44.7,-67.904,44.99,-67.769,45.197,-67.731,45.569,-67.736,45.888,-67.66,46.154,-67.657,46.399,-67.618,46.437,-67.533,46.32,-67.477,46.317,-67.402,46.454,-67.304,46.56,-67.268,46.884,-67.275,47.154,-67.357,47.352,-67.362,47.403,-67.409,47.231,-67.468,47.117,-67.573,47.314,-67.665,47.49,-67.728,47.704,-67.716,47.959,-67.66,48.21,-67.699,48.322,-67.785,48.322,-67.917,48.375,-67.988,48.551,-67.926,48.648,-67.794,48.62,-67.625,48.63,-67.521,49.053,-67.352,49.219,-67.227,48.923,-67.2,48.714,-67.217,48.598,-67.171,48.465,-67.043,48.83,-66.938,49.247,-66.942,49.489,-67.031,50.006,-67.175,50.293,-67.172,50.553,-67.194,50.606,-67.15,50.509,-66.939,50.521,-66.82,50.306,-66.753,50.244,-66.603,50.332,-66.445,50.588,-66.356,50.937,-66.315,51.688,-66.072,51.885,-66.02,52.378,-65.969,52.955,-65.946,53.672,-65.859,54.948,-65.916,55.29,-65.954,55.504,-66.003,55.71,-66.08,55.974,-66.209,56.362,-66.373,56.859,-66.423,57,-66.475,57.185,-66.613,56.987,-66.704,56.824,-66.713,56.51,-66.659,56.295,-66.603,56.146,-66.626,56.292,-66.721,56.453,-66.78,56.48,-66.859,56.391,-66.974,55.803,-67.199,56.155,-67.265,56.366,-67.212,56.562,-67.116,56.76,-67.073,56.892,-67.056,57.361,-67.053,57.627,-67.014,57.828,-67.041,58.027,-67.103,58.317,-67.163,58.737,-67.23,59.251,-67.485,59.65,-67.459,59.868,-67.403,60.482,-67.385,61.012,-67.5,61.309,-67.54,62.174,-67.575,62.688,-67.648,63.018,-67.562,63.238,-67.527,63.699,-67.508,63.931,-67.526,64.574,-67.62,65.708,-67.716,66.488,-67.766,67.175,-67.768,67.502,-67.81,68.099,-67.854,68.328,-67.89,68.9,-67.862,69.167,-67.825,69.416,-67.743,69.559,-67.763,69.656,-67.865,69.603,-68.041,69.704,-68.161,69.789,-68.279,69.907,-68.379,69.982,-68.464,69.928,-68.535,69.762,-68.599,69.534,-68.737,69.547,-68.857,69.646,-68.932,69.531,-69.024,69.615,-69.154,69.629,-69.232,69.549,-69.294,69.372,-69.331,69.065,-69.337,68.906,-69.373,68.88,-69.47,68.959,-69.54,69.136,-69.578,69.188,-69.655,69.162,-69.77,69.083,-69.867,68.921,-69.912,68.744,-69.921,68.415,-69.902,68.178,-69.837,68.027,-69.894,67.917,-69.953,67.575,-70.088,67.417,-70.177,67.268,-70.273,67.659,-70.326,67.941,-70.423,68.559,-70.412,68.757,-70.37,69.021,-70.325,69.162,-70.334,69.25,-70.431,69.197,-70.585,69.188,-70.705,68.873,-71.035,68.768,-71.091,68.624,-71.181,68.448,-71.252,68.311,-71.287,68.037,-71.391,67.873,-71.58,67.694,-71.737,67.432,-72.003,67.281,-72.291,67.215,-72.461,67.113,-72.641,66.892,-72.949,66.746,-73,66.498,-73.125,66.569,-73.209,66.765,-73.217,67.003,-73.236,67.322,-73.3,67.749,-73.168,67.971,-73.086,68.016,-72.918,67.971,-72.751,68.107,-72.651,68.42,-72.515,69.157,-72.419,69.309,-72.409,69.555,-72.375,69.77,-72.254,69.962,-72.133,70.294,-72.055,70.573,-71.931,70.616,-71.842,70.732,-71.822,71.079,-71.737,71.168,-71.672,71.277,-71.624,71.349,-71.514,71.379,-71.309,71.465,-71.155,71.634,-70.949,71.771,-70.801,71.905,-70.707,72.263,-70.657,72.418,-70.599,72.622,-70.472,72.76,-70.396,72.744,-70.239,72.822,-70.096,73.041,-70.01,73.325,-69.849,73.676,-69.826,73.942,-69.743,74.227,-69.8,74.571,-69.88,75.148,-69.855,75.424,-69.893,75.636,-69.849,75.821,-69.725,75.891,-69.576,76.112,-69.487,76.36,-69.49,76.77,-69.34,77.192,-69.206,77.541,-69.174,77.817,-69.069,78.015,-68.892,78.229,-68.756,78.489,-68.626,78.563,-68.394,78.726,-68.278,79.035,-68.175,79.288,-68.119,80.363,-67.947,81.187,-67.831,82.017,-67.69,82.273,-67.692,82.607,-67.613,83.158,-67.611,83.304,-67.603,83.494,-67.441,83.904,-67.292,84.161,-67.244,84.485,-67.114,84.748,-67.102,85.117,-67.126,85.429,-67.161,85.711,-67.161,86.118,-67.055,86.75,-67.037,86.947,-66.986,87.085,-66.94,87.98,-66.788,88.314,-66.817,88.789,-66.792,89.077,-66.799,89.352,-66.818,89.698,-66.823,90.293,-66.77,90.547,-66.734,91.022,-66.603,91.546,-66.572,91.777,-66.537,92.073,-66.508,92.312,-66.559,92.486,-66.604,92.592,-66.614,92.731,-66.624,93.075,-66.571,93.358,-66.585,93.722,-66.643,93.964,-66.69,94.089,-66.689,94.313,-66.647,94.587,-66.544,94.84,-66.501,95.084,-66.527,95.248,-66.571,95.541,-66.631,95.991,-66.621,96.424,-66.6,96.789,-66.551,97.101,-66.499,97.388,-66.579,97.72,-66.607,98.258,-66.467,98.462,-66.499,98.603,-66.535,98.72,-66.553,98.859,-66.671,99.37,-66.648,99.824,-66.549,100.212,-66.474,100.591,-66.425,100.889,-66.358,101.327,-66.1,101.321,-66.021,101.381,-65.973,101.474,-65.951,102.174,-65.954,102.392,-65.933,102.674,-65.865,103.167,-65.917,103.639,-65.999,103.763,-65.99,103.951,-65.988,104.289,-66.039,104.667,-66.137,105,-66.164,106.387,-66.411,107.171,-66.47,107.566,-66.552,107.667,-66.58,107.785,-66.664,107.992,-66.672,108.158,-66.639,108.376,-66.766,108.91,-66.862,109.463,-66.909,109.824,-66.834,110.437,-66.621,110.622,-66.524,110.587,-66.312,110.907,-66.077,111.453,-65.961,112.13,-65.9,112.548,-65.848,113.099,-65.8,113.368,-65.849,113.502,-65.886,113.71,-65.93,113.954,-66.06,114.337,-66.36,114.619,-66.468,114.87,-66.477,115.082,-66.493,115.31,-66.561,115.635,-66.771,115.442,-66.958,115.274,-67.028,114.571,-67.108,114.26,-67.172,113.991,-67.212,113.913,-67.368,114.027,-67.441,114.319,-67.406,114.658,-67.388,114.926,-67.357,115.172,-67.308,115.384,-67.238,115.885,-67.202,116.215,-67.143,116.509,-67.108,116.713,-67.047,116.924,-67.055,117.132,-67.114,117.298,-67.109,117.745,-67.129,117.952,-67.085,118.139,-67.082,118.326,-67.115,118.519,-67.161,118.714,-67.172,118.965,-67.145,119.318,-67.071,119.768,-66.992,120.187,-66.966,120.289,-66.967,120.375,-66.984,119.954,-67.076,119.281,-67.199,118.922,-67.32,119.133,-67.371,120.4,-67.236,120.979,-67.136,121.488,-67.091,121.613,-67.057,122.033,-66.902,122.183,-66.859,122.633,-66.805,123.222,-66.745,123.667,-66.677,123.969,-66.608,124.196,-66.601,124.371,-66.652,124.598,-66.708,124.822,-66.695,125.095,-66.641,125.286,-66.516,125.398,-66.424,125.603,-66.393,125.866,-66.364,126.077,-66.396,126.424,-66.462,126.665,-66.498,126.874,-66.759,127.365,-66.99,127.541,-67.051,127.968,-67.028,128.431,-67.119,128.628,-67.107,128.816,-67.08,128.982,-67.098,129.237,-67.042,129.5,-66.753,129.741,-66.469,129.976,-66.345,130.121,-66.292,130.301,-66.268,130.579,-66.209,130.952,-66.191,131.232,-66.216,131.831,-66.236,132.32,-66.165,132.874,-66.178,133.148,-66.095,133.445,-66.081,133.843,-66.154,133.958,-66.204,134.179,-66.277,134.232,-66.348,134.289,-66.477,134.398,-66.48,134.77,-66.353,134.971,-66.33,135.352,-66.127,135.555,-66.18,136.009,-66.267,136.194,-66.292,136.553,-66.439,136.74,-66.408,136.889,-66.34,137.336,-66.346,137.754,-66.406,137.926,-66.457,138.14,-66.544,138.271,-66.564,138.376,-66.54,139.242,-66.574,139.613,-66.638,139.9,-66.715,140.902,-66.752,141.286,-66.832,141.517,-66.794,141.973,-66.807,142.159,-66.874,142.327,-66.948,142.688,-67.013,142.888,-67,143.169,-66.949,143.448,-66.877,143.73,-66.877,143.863,-66.939,143.911,-67.091,144.118,-67.088,144.348,-67.018,144.551,-67.036,144.621,-67.141,144.515,-67.283,144.26,-67.479,144.154,-67.644,143.942,-67.794,143.977,-67.865,144.189,-67.9,144.404,-67.794,144.879,-67.721,145.128,-67.626,145.556,-67.591,145.975,-67.624,146.276,-67.751,146.828,-67.965,146.852,-68.041,146.897,-68.12,146.878,-68.191,146.798,-68.274,147.094,-68.369,147.354,-68.384,147.569,-68.375,148.456,-68.467,148.881,-68.431,149.263,-68.431,149.717,-68.418,150.066,-68.42,150.342,-68.436,150.672,-68.403,150.936,-68.358,151.068,-68.385,151.121,-68.623,151.139,-68.764,151.289,-68.817,151.448,-68.764,151.562,-68.694,152.265,-68.726,152.546,-68.73,152.814,-68.768,153.082,-68.857,153.34,-68.818,153.496,-68.764,153.705,-68.729,153.767,-68.641,153.792,-68.493,153.785,-68.35,153.908,-68.323,154.032,-68.35,154.2,-68.418,154.576,-68.634,154.866,-68.774,154.988,-68.841,155.164,-68.895,155.52,-69.024,156.011,-69.078,156.489,-69.183,157.046,-69.176,157.481,-69.309,157.776,-69.205,157.933,-69.181,158.158,-69.209,158.433,-69.299,158.647,-69.32,159.386,-69.468,159.784,-69.522,159.931,-69.631,160.126,-69.734,160.126,-69.84,160.21,-69.975,160.652,-70.081,160.827,-70.182,161.037,-70.317,161.425,-70.827,161.625,-70.916,161.916,-70.907,162.189,-71.04,162.278,-71.022,162.287,-70.969,162.04,-70.625,162.022,-70.44,162.216,-70.334,162.675,-70.305,163.026,-70.501,163.349,-70.621,163.567,-70.642,163.998,-70.637,164.403,-70.51,164.716,-70.557,165.209,-70.571,165.854,-70.645,166.132,-70.633,166.627,-70.664,167.229,-70.771,167.569,-70.81,167.64,-70.854,167.799,-70.925,167.878,-71.013,167.966,-71.092,168.173,-71.183,168.383,-71.197,168.798,-71.275,169.664,-71.511,169.977,-71.581,170.162,-71.63,170.25,-71.569,170.277,-71.444,170.436,-71.419,170.603,-71.604,170.78,-71.745,170.859,-71.869,170.675,-71.969,170.409,-71.948,170.224,-71.948,170.03,-72.116,169.954,-72.403,170.127,-72.398,170.259,-72.371,170.286,-72.477,170.206,-72.565,170.048,-72.601,169.775,-72.534,169.44,-72.487,169.072,-72.469,168.719,-72.384,168.576,-72.379,168.428,-72.383,168.622,-72.473,168.82,-72.552,169.27,-72.621,169.829,-72.729,169.845,-72.795,169.712,-72.877,169.545,-73.05,169.033,-73.2,168.736,-73.091,168.381,-73.066,168.204,-73.13,167.853,-73.122,167.156,-73.147,166.883,-73.011,166.453,-72.936,166.467,-72.997,166.834,-73.224,167.226,-73.276,167.616,-73.337,167.709,-73.394,167.534,-73.447,167.296,-73.44,166.996,-73.544,166.429,-73.527,166.159,-73.534,166.001,-73.577,165.86,-73.593,165.971,-73.631,166.106,-73.735,165.913,-73.823,165.734,-73.867,165.549,-73.846,165.347,-73.879,165.25,-73.782,165.245,-73.571,165.129,-73.383,165.005,-73.375,164.813,-73.397,164.75,-73.559,164.888,-73.838,164.98,-73.926,164.906,-74.003,164.776,-74.029,165.037,-74.263,165.263,-74.426,165.4,-74.479,165.409,-74.559,165.303,-74.594,165.001,-74.563,164.853,-74.578,164.689,-74.568,164.411,-74.533,164.174,-74.523,163.936,-74.567,163.735,-74.564,163.557,-74.417,163.398,-74.382,163.266,-74.426,163.167,-74.602,162.961,-74.656,162.752,-74.736,162.604,-74.823,162.534,-75.167,162.41,-75.238,162.226,-75.235,162.088,-75.262,161.91,-75.234,161.68,-75.218,160.911,-75.335,161.033,-75.396,161.227,-75.386,161.904,-75.404,162.19,-75.467,162.239,-75.622,162.352,-75.687,162.578,-75.758,162.754,-75.793,162.816,-75.846,162.745,-75.952,162.648,-76.049,162.437,-76.155,162.498,-76.208,162.728,-76.225,162.825,-76.464,162.675,-76.569,162.745,-76.658,162.763,-76.746,162.61,-76.829,162.489,-76.869,162.45,-76.956,162.679,-77.007,162.85,-77.024,163.087,-77.032,163.25,-77.126,163.458,-77.269,163.608,-77.388,163.619,-77.582,163.766,-77.7,164.045,-77.775,164.036,-77.853,164.232,-77.877,164.421,-77.883,164.492,-77.954,164.43,-78.042,164.108,-78.147,163.978,-78.224,164.297,-78.236,164.628,-78.316,165.051,-78.226,165.274,-78.129,165.418,-78.042,165.524,-78.064,165.534,-78.154,165.663,-78.306,166.209,-78.452,166.51,-78.497,166.801,-78.522,167.058,-78.518,167.13,-78.606,167.049,-78.686,166.85,-78.68,166.525,-78.695,166.287,-78.628,166.117,-78.571,164.635,-78.603,164.301,-78.63,163.902,-78.717,163.503,-78.759,162.895,-78.845,162.639,-78.898,161.975,-78.694,161.757,-78.545,161.669,-78.536,161.51,-78.571,161.502,-78.677,161.813,-78.907,161.951,-78.968,161.951,-79.03,161.864,-79.061,161.737,-79.06,161.546,-79.015,161.191,-78.979,160.874,-79.05,160.483,-79.201,160.567,-79.302,160.67,-79.359,160.646,-79.427,160.209,-79.554,160.068,-79.561,159.976,-79.586,160.084,-79.633,160.323,-79.636,160.346,-79.692,160.265,-79.737,159.872,-79.79,159.896,-79.859,160.111,-79.892,160.387,-79.879,160.558,-79.93,160.559,-80.011,160.382,-80.054,160.179,-80.088,158.767,-80.293,158.56,-80.349,158.574,-80.423,159.065,-80.443,160.542,-80.425,160.637,-80.45,160.603,-80.508,160.521,-80.583,160.601,-80.637,160.823,-80.674,160.83,-80.73,160.72,-80.753,160.503,-80.779,160.26,-80.787,160.276,-80.847,160.607,-80.901,160.717,-80.907,160.728,-81.113,160.717,-81.2,160.54,-81.242,160.479,-81.27,160.47,-81.341,160.908,-81.39,161.582,-81.61,161.73,-81.61,161.996,-81.653,162.425,-81.765,162.577,-81.832,162.821,-81.866,162.854,-81.921,163.004,-81.969,163.602,-82.121,163.68,-82.187,162.427,-82.314,161.167,-82.408,161.283,-82.49,162.644,-82.482,163.012,-82.535,163.175,-82.519,163.269,-82.463,164.001,-82.397,164.747,-82.354,164.98,-82.385,165.982,-82.63,166.446,-82.722,166.742,-82.757,166.957,-82.765,167.116,-82.801,167.271,-82.879,167.233,-82.952,167.297,-82.986,167.404,-82.999,167.602,-83.047,167.828,-83.031,168.092,-82.975,168.276,-82.987,168.607,-83.065,168.482,-83.127,168.408,-83.155,168.32,-83.211,168.24,-83.23,167.973,-83.243,167.825,-83.243,167.674,-83.231,167.658,-83.272,167.843,-83.316,168.11,-83.362,169.838,-83.399,170.332,-83.479,170.817,-83.436,171.036,-83.448,171.221,-83.475,171.289,-83.555,171.537,-83.581,171.917,-83.644,172.45,-83.675,172.874,-83.673,173.397,-83.759,173.662,-83.761,173.822,-83.81,175.011,-83.839,175.187,-83.877,175.323,-83.94,175.606,-83.968,175.911,-83.973,177.581,-84.075,178.209,-84.13,178.353,-84.127,178.496,-84.136,178.944,-84.181,179.403,-84.206,179.62,-84.268,180,-84.352,180,-89.999,178.594,-89.999,177.188,-89.999,175.781,-89.999,174.375,-89.999,172.969,-89.999,171.563,-89.999,170.156,-89.999,168.75,-89.999,167.344,-89.999,165.938,-89.999,164.531,-89.999,163.125,-89.999,161.719,-89.999,160.313,-89.999,158.906,-89.999,157.5,-89.999,156.094,-89.999,154.688,-89.999,153.281,-89.999,151.875,-89.999,150.469,-89.999,149.063,-89.999,147.656,-89.999,146.25,-89.999,144.844,-89.999,143.438,-89.999,142.031,-89.999,140.625,-89.999,139.219,-89.999,137.813,-89.999,136.406,-89.999,135,-89.999,133.594,-89.999,132.188,-89.999,130.781,-89.999,129.375,-89.999,127.969,-89.999,126.562,-89.999,125.156,-89.999,123.75,-89.999,122.344,-89.999,120.938,-89.999,119.531,-89.999,118.125,-89.999,116.719,-89.999,115.313,-89.999,113.906,-89.999,112.5,-89.999,111.094,-89.999,109.688,-89.999,108.281,-89.999,106.875,-89.999,105.469,-89.999,104.063,-89.999,102.656,-89.999,101.25,-89.999,99.844,-89.999,98.438,-89.999,97.031,-89.999,95.625,-89.999,94.219,-89.999,92.813,-89.999,91.406,-89.999,90,-89.999,88.594,-89.999,87.188,-89.999,85.781,-89.999,84.375,-89.999,82.969,-89.999,81.563,-89.999,80.156,-89.999,78.75,-89.999,77.344,-89.999,75.938,-89.999,74.531,-89.999,73.125,-89.999,71.719,-89.999,70.313,-89.999,68.906,-89.999,67.5,-89.999,66.094,-89.999,64.688,-89.999,63.281,-89.999,61.875,-89.999,60.469,-89.999,59.063,-89.999,57.656,-89.999,56.25,-89.999,54.844,-89.999,53.438,-89.999,52.031,-89.999,50.625,-89.999,49.219,-89.999,47.813,-89.999,46.406,-89.999,45,-89.999,43.594,-89.999,42.188,-89.999,40.781,-89.999,39.375,-89.999,37.969,-89.999,36.563,-89.999,35.156,-89.999,33.75,-89.999,32.344,-89.999,30.938,-89.999,29.531,-89.999,28.125,-89.999,26.719,-89.999,25.313,-89.999,23.906,-89.999,22.5,-89.999,21.094,-89.999,19.688,-89.999,18.281,-89.999,16.875,-89.999,15.469,-89.999,14.063,-89.999,12.656,-89.999,11.25,-89.999,9.844,-89.999,8.438,-89.999,7.031,-89.999,5.625,-89.999,4.219,-89.999,2.813,-89.999,1.406,-89.999,0,-89.999,-1.406,-89.999,-2.812,-89.999,-4.219,-89.999,-5.625,-89.999,-7.031,-89.999,-8.438,-89.999,-9.844,-89.999,-11.25,-89.999,-12.656,-89.999,-14.062,-89.999,-15.469,-89.999,-16.875,-89.999,-18.281,-89.999,-19.687,-89.999,-21.094,-89.999,-22.5,-89.999,-23.906,-89.999,-25.313,-89.999,-26.719,-89.999,-28.125,-89.999,-29.531,-89.999,-30.937,-89.999,-32.344,-89.999,-33.75,-89.999,-35.156,-89.999,-36.562,-89.999,-37.969,-89.999,-39.375,-89.999,-40.781,-89.999,-42.187,-89.999,-43.594,-89.999,-45,-89.999,-46.406,-89.999,-47.812,-89.999,-49.219,-89.999,-50.625,-89.999,-52.031,-89.999,-53.438,-89.999,-54.844,-89.999,-56.25,-89.999,-57.656,-89.999,-59.062,-89.999,-60.469,-89.999,-61.875,-89.999,-63.281,-89.999,-64.687,-89.999,-66.094,-89.999,-67.5,-89.999,-68.906,-89.999,-70.312,-89.999,-71.719,-89.999,-73.125,-89.999,-74.531,-89.999,-75.937,-89.999,-77.344,-89.999,-78.75,-89.999,-80.156,-89.999,-81.562,-89.999,-82.969,-89.999,-84.375,-89.999,-85.781,-89.999,-87.187,-89.999,-88.594,-89.999,-90,-89.999,-91.406,-89.999,-92.812,-89.999,-94.219,-89.999,-95.625,-89.999,-97.031,-89.999,-98.437,-89.999,-99.844,-89.999,-101.25,-89.999,-102.656,-89.999,-104.062,-89.999,-105.469,-89.999,-106.875,-89.999,-108.281,-89.999,-109.687,-89.999,-111.094,-89.999,-112.5,-89.999,-113.906,-89.999,-115.312,-89.999,-116.719,-89.999,-118.125,-89.999,-119.531,-89.999,-120.937,-89.999,-122.344,-89.999,-123.75,-89.999,-125.156,-89.999,-126.562,-89.999,-127.969,-89.999,-129.375,-89.999,-130.781,-89.999,-132.187,-89.999,-133.594,-89.999,-135,-89.999,-136.406,-89.999,-137.812,-89.999,-139.219,-89.999,-140.625,-89.999,-142.031,-89.999,-143.437,-89.999,-144.844,-89.999,-146.25,-89.999,-147.656,-89.999,-149.062,-89.999,-150.469,-89.999,-151.875,-89.999,-153.281,-89.999,-154.688,-89.999,-156.094,-89.999,-157.5,-89.999,-158.906,-89.999,-160.312,-89.999,-161.719,-89.999,-163.125,-89.999,-164.531,-89.999,-165.938,-89.999,-167.344,-89.999,-168.75,-89.999,-170.156,-89.999,-171.562,-89.999,-172.969,-89.999,-174.375,-89.999,-175.781,-89.999,-177.187,-89.999,-178.594,-89.999,-180,-89.999,-180,-89.583,-180,-89.293,-180,-88.587,-180,-87.881,-180,-87.175,-180,-86.469,-180,-85.763,-180,-85.058,-180,-84.352,-178.39,-84.337,-178.069,-84.352,-177.73,-84.395,-176.986,-84.399,-176.289,-84.418,-176.107,-84.475,-175.875,-84.51,-175.381,-84.48,-174.987,-84.465,-174.663,-84.463,-171.704,-84.542,-168.668,-84.684,-168.049,-84.729,-167.492,-84.834,-166.911,-84.819,-163.464,-84.901,-162.933,-84.901,-160.821,-84.987,-157.127,-85.186,-156.81,-85.192,-156.459,-85.186,-156.643,-85.079,-156.988,-84.982,-157.454,-84.912,-157.15,-84.891,-156.49,-84.889,-156.621,-84.84,-156.986,-84.811,-158.303,-84.778,-163.569,-84.529,-163.685,-84.513,-163.759,-84.493,-163.897,-84.47,-164.114,-84.445,-164.917,-84.431,-165.135,-84.41,-165.24,-84.381,-165.185,-84.37,-165.125,-84.375,-163.899,-84.353,-163.765,-84.324,-163.758,-84.305,-163.821,-84.291,-164.032,-84.274,-164.528,-84.191,-164.685,-84.155,-164.603,-84.097,-164.503,-84.072,-164.124,-84.054,-164.011,-84.016,-164.083,-83.946,-164.951,-83.806,-165.536,-83.757,-165.922,-83.79,-166.649,-83.792,-167.553,-83.811,-167.801,-83.791,-168.053,-83.735,-168.347,-83.637,-168.497,-83.611,-168.785,-83.529,-169.168,-83.45,-171.188,-83.256,-171.539,-83.204,-174.066,-82.9,-174.172,-82.848,-174.236,-82.793,-173.071,-82.916,-172.852,-82.917,-172.593,-82.884,-172.392,-82.893,-172.124,-82.862,-171.821,-82.847,-171.031,-82.943,-169.441,-83.096,-169.016,-83.15,-168.79,-83.188,-168.604,-83.202,-168.418,-83.229,-168.191,-83.213,-168.055,-83.227,-167.724,-83.217,-166.217,-83.201,-165.619,-83.216,-164.916,-83.29,-164.644,-83.412,-164.446,-83.468,-164.058,-83.425,-163.733,-83.373,-163.111,-83.329,-162.912,-83.347,-162.574,-83.411,-162.197,-83.519,-160.595,-83.49,-159.924,-83.495,-159.444,-83.543,-157.699,-83.381,-157.428,-83.346,-157.028,-83.234,-157.356,-83.198,-157.589,-83.187,-157.679,-83.129,-157.522,-83.107,-157.018,-83.075,-156.037,-83.027,-155.459,-82.981,-155.15,-82.858,-153.822,-82.669,-153.399,-82.586,-153.01,-82.45,-153.883,-82.177,-154.717,-81.941,-154.451,-81.868,-154.188,-81.811,-154.061,-81.765,-153.957,-81.7,-154.232,-81.623,-154.485,-81.566,-154.908,-81.51,-156.493,-81.377,-157.033,-81.319,-156.815,-81.231,-156.528,-81.162,-155.921,-81.133,-152.035,-81.029,-148.123,-80.901,-148.023,-80.836,-148.543,-80.76,-148.984,-80.742,-149.147,-80.719,-149.207,-80.67,-149.214,-80.604,-149.264,-80.593,-149.429,-80.586,-150.133,-80.51,-150.282,-80.48,-150.516,-80.409,-150.575,-80.354,-150.435,-80.211,-150.221,-80.15,-149.845,-80.118,-149.578,-80.106,-148.766,-80.108,-148.448,-80.091,-148.317,-80.071,-148.34,-80.003,-148.43,-79.971,-148.433,-79.929,-148.296,-79.907,-148.129,-79.908,-148.083,-79.857,-148.177,-79.776,-148.417,-79.731,-149.051,-79.657,-150.491,-79.546,-151.048,-79.46,-151.368,-79.393,-151.636,-79.318,-151.904,-79.281,-152.091,-79.242,-152.053,-79.193,-152.138,-79.116,-152.244,-79.103,-152.701,-79.135,-153.518,-79.117,-154.518,-79.047,-155.21,-78.965,-156.115,-78.745,-156.469,-78.635,-156.208,-78.559,-155.92,-78.51,-154.716,-78.398,-154.538,-78.359,-154.293,-78.259,-154.695,-78.217,-155.037,-78.221,-155.342,-78.192,-156.569,-78.186,-157.267,-78.2,-157.848,-78.074,-158.286,-77.951,-158.407,-77.888,-158.5,-77.778,-158.351,-77.615,-158.23,-77.498,-158.246,-77.354,-158.214,-77.157,-158.003,-77.091,-157.842,-77.079,-157.465,-77.231,-157.139,-77.242,-156.668,-77.213,-156.368,-77.135,-156.211,-77.106,-155.92,-77.098,-155.359,-77.133,-155.102,-77.12,-154.815,-77.127,-153.91,-77.227,-153.713,-77.274,-153.606,-77.31,-153.573,-77.363,-153.461,-77.416,-153.077,-77.442,-151.998,-77.413,-151.719,-77.426,-150.956,-77.574,-150.306,-77.731,-150.084,-77.771,-149.718,-77.797,-149.588,-77.774,-149.474,-77.715,-149.126,-77.643,-148.34,-77.551,-148.156,-77.462,-148.26,-77.413,-148.559,-77.361,-148.744,-77.343,-148.844,-77.284,-148.839,-77.202,-148.777,-77.125,-148.572,-77.105,-148.196,-77.211,-147.73,-77.31,-147.566,-77.325,-147.442,-77.321,-147.207,-77.286,-146.928,-77.26,-146.391,-77.472,-146.074,-77.487,-145.677,-77.488,-145.601,-77.455,-145.65,-77.398,-145.714,-77.338,-145.794,-77.33,-145.808,-77.273,-145.635,-77.221,-145.516,-77.199,-145.563,-77.162,-145.753,-77.103,-145.864,-77.094,-145.967,-77.069,-145.934,-77.029,-145.806,-77.012,-145.629,-76.954,-145.686,-76.884,-145.676,-76.797,-145.75,-76.749,-146.166,-76.658,-146.777,-76.507,-147.34,-76.438,-148.601,-76.493,-149.046,-76.458,-149.34,-76.419,-149.654,-76.365,-149.285,-76.311,-148.895,-76.272,-148.78,-76.238,-148.632,-76.168,-148.459,-76.118,-148.32,-76.104,-147.86,-76.131,-146.817,-76.318,-146.597,-76.338,-145.886,-76.424,-145.687,-76.429,-145.442,-76.409,-145.642,-76.326,-145.86,-76.267,-146.383,-76.1,-146.323,-76.02,-145.988,-75.889,-145.106,-75.879,-144.721,-75.832,-144.221,-75.731,-143.574,-75.564,-143.022,-75.543,-142.33,-75.491,-142.094,-75.53,-141.506,-75.69,-141.135,-75.746,-141.009,-75.751,-140.874,-75.746,-141.223,-75.546,-140.999,-75.52,-140.709,-75.498,-140.471,-75.447,-140.294,-75.406,-139.691,-75.213,-139.149,-75.16,-137.618,-75.076,-137.09,-75.153,-136.65,-75.162,-136.55,-75.139,-136.462,-75.036,-136.228,-74.836,-136.03,-74.765,-135.362,-74.69,-134.84,-74.694,-134.465,-74.776,-134.117,-74.83,-133.796,-74.855,-133.475,-74.852,-132.992,-74.806,-132.351,-74.789,-132.049,-74.766,-131.707,-74.811,-130.857,-74.826,-130.196,-74.891,-129.791,-74.891,-129.238,-74.829,-128.941,-74.82,-127.863,-74.719,-127.02,-74.698,-126.384,-74.743,-125.353,-74.715,-124.312,-74.736,-123.889,-74.773,-121.544,-74.75,-119.677,-74.655,-119.422,-74.622,-119.022,-74.518,-118.803,-74.422,-118.656,-74.393,-118.342,-74.382,-117.806,-74.403,-117.068,-74.473,-116.433,-74.447,-115.223,-74.487,-115.105,-74.455,-114.991,-74.275,-114.791,-73.989,-114.624,-73.903,-114.346,-73.925,-113.508,-74.089,-113.489,-74.158,-113.575,-74.208,-113.714,-74.228,-113.753,-74.367,-113.641,-74.406,-113.454,-74.394,-113.333,-74.454,-113.597,-74.559,-113.783,-74.618,-113.903,-74.644,-113.985,-74.843,-114.097,-74.909,-114.11,-74.982,-113.932,-74.982,-113.753,-74.952,-113.593,-74.944,-113.092,-74.892,-112.17,-74.832,-111.868,-74.801,-111.696,-74.792,-111.584,-74.751,-111.739,-74.653,-111.789,-74.572,-111.696,-74.504,-111.722,-74.387,-111.806,-74.27,-111.63,-74.181,-111.467,-74.201,-111.18,-74.188,-111.02,-74.23,-110.77,-74.269,-110.534,-74.289,-110.307,-74.367,-110.23,-74.536,-110.3,-74.711,-110.532,-74.836,-110.968,-74.951,-111.463,-75.133,-111.359,-75.22,-111.104,-75.191,-109.99,-75.199,-109.272,-75.185,-108.822,-75.207,-108.254,-75.253,-107.805,-75.322,-107.267,-75.334,-106.932,-75.309,-106.619,-75.344,-105.399,-75.198,-104.902,-75.115,-104.618,-75.156,-104.16,-75.121,-103.901,-75.153,-103.425,-75.101,-103.121,-75.095,-102.771,-75.117,-101.708,-75.127,-101.628,-75.222,-101.304,-75.366,-101.039,-75.422,-100.706,-75.398,-100.463,-75.353,-100.083,-75.37,-99.531,-75.309,-98.98,-75.327,-98.752,-75.317,-98.646,-75.277,-98.558,-75.19,-98.727,-75.141,-99.208,-75.079,-99.652,-74.949,-99.849,-74.922,-100.164,-74.938,-100.313,-74.914,-100.473,-74.872,-100.265,-74.823,-100.013,-74.662,-100.119,-74.515,-100.238,-74.484,-100.531,-74.489,-100.882,-74.541,-101.023,-74.505,-101.252,-74.486,-101.343,-74.35,-101.587,-74.096,-101.715,-74.024,-102.105,-73.958,-102.441,-73.926,-102.766,-73.884,-102.863,-73.784,-102.8,-73.646,-102.411,-73.616,-102.037,-73.631,-101.828,-73.655,-101.587,-73.667,-101.311,-73.695,-101.13,-73.735,-100.985,-73.757,-100.718,-73.758,-99.781,-73.72,-99.656,-73.694,-99.541,-73.645,-99.343,-73.634,-99.162,-73.641,-98.896,-73.611,-99.2,-73.571,-99.528,-73.495,-100.021,-73.403,-100.436,-73.353,-101.189,-73.318,-101.574,-73.33,-101.816,-73.311,-102.675,-73.321,-102.909,-73.285,-103.076,-73.185,-103.308,-72.945,-103.375,-72.819,-103.217,-72.772,-103.11,-72.721,-102.856,-72.716,-102.485,-72.736,-102.363,-72.76,-102.272,-72.835,-102.363,-72.911,-102.482,-72.951,-102.409,-72.987,-102.029,-72.998,-101.842,-73.021,-101.681,-73.03,-101.332,-72.995,-100.821,-72.981,-100.564,-73.016,-100.259,-73.041,-99.811,-73,-98.209,-73.022,-98.012,-73.033,-97.819,-73.102,-97.651,-73.144,-97.476,-73.126,-96.956,-73.206,-96.676,-73.269,-96.394,-73.301,-96.152,-73.309,-95.881,-73.294,-95.529,-73.241,-95.237,-73.22,-95.03,-73.239,-94.586,-73.25,-94.246,-73.313,-93.985,-73.287,-93.706,-73.215,-92.828,-73.165,-92.241,-73.178,-91.169,-73.307,-90.921,-73.319,-90.431,-73.243,-90.274,-73.119,-90.295,-72.978,-90.152,-72.945,-90.035,-72.96,-89.818,-72.863,-89.522,-72.871,-89.341,-72.89,-89.229,-72.826,-89.127,-72.693,-88.78,-72.683,-88.527,-72.702,-88.194,-72.787,-88.195,-72.859,-88.332,-72.934,-88.561,-73.121,-88.419,-73.229,-88.205,-73.22,-87.936,-73.241,-87.608,-73.195,-87.401,-73.192,-87.038,-73.354,-86.791,-73.364,-86.602,-73.354,-85.981,-73.208,-85.801,-73.192,-85.582,-73.259,-85.261,-73.413,-84.981,-73.502,-84.571,-73.557,-84.214,-73.573,-83.796,-73.645,-83.565,-73.706,-83.042,-73.707,-82.815,-73.732,-82.183,-73.857,-81.606,-73.796,-81.309,-73.738,-81.163,-73.632,-81.236,-73.474,-81.262,-73.315,-81.176,-73.249,-81.024,-73.236,-80.336,-73.414,-80.38,-73.308,-80.439,-73.225,-80.614,-73.083,-80.588,-72.978,-80.442,-72.945,-80.152,-73,-79.808,-73.028,-79.522,-73.09,-78.964,-73.312,-78.786,-73.507,-78.408,-73.556,-78.144,-73.547,-77.846,-73.515,-77.444,-73.488,-77.136,-73.496,-76.85,-73.46,-76.765,-73.566,-77.033,-73.718,-77.135,-73.818,-77.049,-73.844,-76.887,-73.821,-76.755,-73.789,-76.291,-73.805,-75.916,-73.736,-75.595,-73.711,-75.293,-73.639,-75.044,-73.645,-74.855,-73.658,-74.594,-73.715,-74.345,-73.684,-74.197,-73.696,-73.996,-73.7,-72.929,-73.448,-72.687,-73.452,-72.381,-73.438,-71.994,-73.379,-71.698,-73.353,-71.453,-73.354,-71.017,-73.263,-70.323,-73.274,-69.969,-73.226,-69.282,-73.17,-68.821,-73.105,-68,-72.936,-67.667,-72.835,-67.307,-72.611,-67.08,-72.388,-66.828,-72.09,-66.952,-71.897,-67.084,-71.812,-67.196,-71.719,-67.46,-71.527,-67.53,-71.285,-67.505,-71.058,-67.598,-70.845,-67.692,-70.686,-67.888,-70.422,-68.126,-70.25,-68.403,-70.02,-68.404,-69.809,-68.47,-69.644,-68.638,-69.526,-68.708,-69.432,-68.58,-69.413,-68.462,-69.384,-68.141,-69.348,-67.372,-69.412,-67.304,-69.318,-67.11,-69.248,-66.975,-69.161,-67.021,-69.029,-67.188,-68.974,-67.391,-68.861,-67.299,-68.771,-67.134,-68.771,-67.054,-68.671,-67.117,-68.575,-67.041,-68.453,-66.894,-68.298,-66.793,-68.24,-66.978,-68.147,-67.15,-68.025,-67.107,-67.93,-67.021,-67.831,-66.915,-67.693,-66.77,-67.593,-66.677,-67.56,-66.705,-67.527,-66.923,-67.492,-67.124,-67.485,-67.487,-67.547,-67.545,-67.535,-67.565,-67.503,-67.586,-67.435,-67.55,-67.269,-67.493,-67.113,-67.44,-67.091,-67.299,-67.071,-67.16,-66.952,-67.034,-66.945,-66.955,-66.985,-66.929,-67.144,-66.886,-67.18,-66.902,-67.256,-66.836,-67.282,-66.757,-67.233,-66.61,-67.209,-66.552,-67.263,-66.499,-67.289,-66.472,-67.243,-66.491,-67.114,-66.515,-67.062,-66.533,-66.979,-66.502,-66.94,-66.465,-66.875,-66.527,-66.741,-66.504,-66.69,-66.371,-66.609,-66.307,-66.592,-66.182,-66.592,-65.954,-66.646,-65.847,-66.65,-65.766,-66.625,-65.718,-66.573,-65.678,-66.403,-65.776,-66.343,-65.775,-66.288,-65.717,-66.254,-65.617,-66.135,-65.465,-66.129,-65.316,-66.14,-65.172,-66.117,-65.222,-66.068,-65.267,-65.994,-65.105,-65.958,-64.999,-65.946,-64.722,-65.993,-64.614,-66.019,-64.514,-65.96,-64.547,-65.9,-64.653,-65.867,-64.673,-65.814,-64.647,-65.748,-64.475,-65.781,-64.435,-65.768,-64.39,-65.708,-64.417,-65.68,-64.439,-65.641,-64.213,-65.633,-64.18,-65.617,-64.132,-65.571,-64.066,-65.554,-63.862,-65.556,-63.818,-65.532,-63.798,-65.48,-63.908,-65.467,-64.051,-65.417,-64.071,-65.278,-64.038,-65.179,-63.912,-65.093,-63.76,-65.033,-63.482,-65.085,-63.264,-65.073,-63.178,-65.126,-63.059,-65.139,-63.033,-65.08,-63.086,-65.028,-63.12,-64.942,-62.775,-64.842,-62.665,-64.858,-62.528,-64.833,-62.576,-64.756,-62.503,-64.656,-62.404,-64.643,-62.338,-64.729,-62.243,-64.747,-62.14,-64.727,-61.883,-64.625,-61.756,-64.61,-61.632,-64.605,-61.5,-64.546,-61.47,-64.476,-61.396,-64.427,-61.174,-64.362,-61.082,-64.315,-60.887,-64.15,-60.922,-64.108,-60.864,-64.073,-60.277,-63.924,-59.99,-63.91,-59.51,-63.821,-59.218,-63.714,-59.036,-63.67,-58.872,-63.552,-58.674,-63.534,-58.216,-63.451,-57.868,-63.319,-57.39,-63.226,-57.168,-63.235,-57.077,-63.262,-57.021,-63.373]],[[-45.223,-78.811,-45.092,-78.814,-44.566,-78.804,-44.041,-78.807,-43.722,-78.818,-43.627,-78.846,-43.544,-78.902,-43.451,-78.99,-43.364,-79.085,-43.267,-79.163,-43.211,-79.3,-43.119,-79.35,-42.965,-79.477,-42.945,-79.579,-42.99,-79.801,-43.066,-79.891,-43.267,-79.979,-43.496,-79.969,-43.6,-79.974,-43.704,-79.99,-43.742,-80.003,-43.758,-80.021,-43.49,-80.096,-43.459,-80.123,-43.454,-80.155,-43.489,-80.178,-43.528,-80.191,-49.187,-80.643,-49.41,-80.667,-49.63,-80.712,-49.701,-80.753,-49.773,-80.784,-54.162,-80.87,-54.202,-80.864,-54.241,-80.847,-54.351,-80.76,-54.372,-80.624,-54.347,-80.569,-54.13,-80.517,-54.045,-80.487,-53.676,-80.284,-53.482,-80.189,-53.394,-80.109,-53.346,-80.114,-53.176,-80.161,-53.053,-80.175,-52.807,-80.156,-52.567,-80.099,-52.461,-80.067,-52.357,-80.078,-52.338,-80.126,-52.297,-80.141,-51.711,-79.99,-51.184,-79.82,-50.664,-79.627,-50.402,-79.512,-50.339,-79.479,-50.295,-79.43,-50.331,-79.381,-50.379,-79.338,-50.42,-79.321,-50.464,-79.313,-50.733,-79.283,-50.649,-79.233,-50.573,-79.172,-50.52,-79.104,-50.502,-79.022,-50.514,-78.98,-50.502,-78.95,-50.38,-78.923,-50.298,-78.882,-50.242,-78.833,-50.335,-78.818,-50.377,-78.78,-50.294,-78.696,-50.22,-78.605,-50.142,-78.557,-49.94,-78.462,-49.354,-78.222,-49.144,-78.094,-49.081,-78.047,-47.692,-77.84,-47.463,-77.819,-47.03,-77.791,-46.826,-77.785,-46.258,-77.805,-45.993,-77.827,-45.53,-77.881,-44.852,-77.988,-44.594,-78.035,-44.34,-78.093,-44.094,-78.167,-43.854,-78.258,-43.809,-78.287,-43.785,-78.336,-43.777,-78.385,-43.788,-78.433,-43.852,-78.53,-43.947,-78.598,-45.068,-78.661,-45.213,-78.687,-45.295,-78.74,-45.352,-78.791,-45.223,-78.811]],[[-59.734,-80.344,-59.774,-80.558,-59.771,-80.657,-59.825,-80.733,-59.926,-80.774,-60.125,-80.841,-60.268,-80.881,-60.583,-80.948,-62.023,-80.889,-62.671,-80.834,-62.94,-80.766,-62.986,-80.735,-63.068,-80.627,-63.144,-80.595,-63.715,-80.617,-64.065,-80.65,-64.126,-80.668,-64.22,-80.734,-64.268,-80.749,-65.203,-80.607,-66.184,-80.442,-66.591,-80.358,-66.734,-80.319,-66.771,-80.294,-66.681,-80.26,-66.588,-80.239,-66.483,-80.224,-66.377,-80.222,-66.296,-80.235,-66.217,-80.258,-66.168,-80.346,-66.115,-80.361,-65.98,-80.384,-62.519,-80.373,-62.232,-80.369,-61.633,-80.344,-61.313,-80.307,-61.194,-80.257,-61.485,-80.244,-61.597,-80.206,-61.694,-80.134,-61.717,-80.069,-61.684,-80.02,-61.302,-79.996,-61.246,-79.978,-61.346,-79.951,-61.343,-79.887,-61.115,-79.862,-61.026,-79.809,-60.579,-79.741,-59.873,-79.777,-59.706,-79.875,-59.752,-79.938,-59.786,-80.001,-59.788,-80.101,-59.498,-80.115,-59.408,-80.151,-59.322,-80.196,-59.427,-80.198,-59.53,-80.208,-59.612,-80.255,-59.683,-80.315,-59.734,-80.344]],[[-70.051,-69.189,-70.079,-69.311,-69.913,-69.267,-69.855,-69.277,-69.708,-69.321,-69.417,-69.583,-69.353,-69.666,-69.234,-69.909,-69.091,-70.09,-68.731,-70.408,-68.554,-70.581,-68.459,-70.683,-68.451,-70.818,-68.336,-70.856,-68.314,-70.912,-68.278,-71.097,-68.252,-71.313,-68.228,-71.725,-68.241,-71.822,-68.394,-71.975,-68.461,-72.085,-68.543,-72.158,-68.64,-72.21,-69.149,-72.427,-69.209,-72.534,-70.063,-72.626,-70.543,-72.664,-70.731,-72.623,-70.923,-72.613,-71.159,-72.627,-71.846,-72.639,-72.367,-72.67,-72.433,-72.658,-72.48,-72.617,-72.531,-72.59,-72.67,-72.596,-72.78,-72.581,-72.888,-72.547,-73.007,-72.484,-73.057,-72.448,-73.086,-72.408,-72.855,-72.304,-72.737,-72.281,-72.618,-72.275,-72.376,-72.296,-72.135,-72.331,-71.605,-72.359,-70.873,-72.366,-70.671,-72.356,-70.428,-72.323,-70.206,-72.228,-70.314,-72.191,-70.424,-72.168,-70.533,-72.163,-70.641,-72.17,-70.945,-72.229,-71.178,-72.264,-71.413,-72.284,-71.661,-72.25,-71.892,-72.153,-71.898,-72.121,-71.107,-72.047,-71.034,-72.035,-70.891,-71.987,-70.845,-71.946,-70.821,-71.907,-71.355,-71.836,-71.465,-71.838,-71.574,-71.851,-71.816,-71.822,-72.046,-71.74,-72.259,-71.641,-72.337,-71.632,-72.412,-71.662,-72.928,-71.922,-72.972,-71.924,-73.167,-71.905,-73.41,-71.853,-73.633,-71.835,-73.776,-71.849,-73.83,-71.87,-73.691,-71.929,-73.572,-71.981,-73.537,-72.022,-73.899,-72.152,-73.996,-72.17,-74.153,-72.159,-74.209,-72.142,-74.322,-72.073,-74.429,-72.056,-74.663,-72.07,-74.786,-72.064,-74.908,-72.033,-75.024,-71.988,-75.13,-71.964,-75.259,-71.914,-75.353,-71.878,-75.383,-71.828,-75.373,-71.78,-75.331,-71.752,-75.325,-71.726,-75.353,-71.68,-75.335,-71.645,-75.293,-71.615,-75.1,-71.555,-74.863,-71.543,-74.636,-71.617,-74.487,-71.642,-74.419,-71.643,-74.392,-71.638,-74.373,-71.618,-74.38,-71.579,-74.42,-71.507,-74.425,-71.457,-74.375,-71.415,-74.308,-71.4,-74.236,-71.388,-74.187,-71.383,-74.041,-71.41,-73.937,-71.438,-73.724,-71.517,-73.545,-71.573,-73.479,-71.579,-73.427,-71.559,-73.38,-71.528,-73.592,-71.448,-73.617,-71.397,-73.604,-71.351,-73.474,-71.325,-73.397,-71.321,-73.02,-71.369,-72.821,-71.384,-72.622,-71.388,-72.212,-71.335,-72.43,-71.275,-72.905,-71.223,-72.995,-71.187,-73.06,-71.127,-72.71,-71.073,-72.356,-71.075,-71.719,-71.145,-71.504,-71.112,-71.308,-71.011,-71.194,-70.985,-70.741,-70.993,-70.381,-70.946,-70.323,-70.951,-70.268,-70.965,-69.916,-71.134,-69.87,-71.126,-69.835,-71.093,-69.823,-71.034,-69.823,-70.973,-69.83,-70.914,-69.876,-70.876,-69.933,-70.88,-69.993,-70.897,-70.094,-70.883,-70.196,-70.851,-70.299,-70.836,-70.661,-70.818,-70.917,-70.786,-71.049,-70.762,-71.173,-70.713,-71.19,-70.66,-71.061,-70.537,-70.562,-70.404,-70.328,-70.361,-70.09,-70.351,-69.975,-70.36,-69.702,-70.415,-69.66,-70.412,-69.618,-70.398,-69.883,-70.305,-70.118,-70.234,-70.234,-70.18,-70.328,-70.16,-70.72,-70.139,-70.926,-70.192,-71.024,-70.201,-71.121,-70.196,-71.696,-70.068,-71.729,-70.054,-71.81,-70.005,-71.854,-69.969,-71.879,-69.909,-71.868,-69.847,-71.852,-69.807,-71.767,-69.649,-71.718,-69.524,-71.743,-69.423,-71.834,-69.367,-71.963,-69.329,-72.081,-69.267,-72.114,-69.225,-72.135,-69.177,-72.138,-69.115,-72.109,-69.06,-72.058,-69.001,-71.99,-68.971,-71.869,-68.941,-71.392,-68.874,-70.417,-68.789,-70.312,-68.832,-70.154,-68.923,-70.105,-68.959,-70.053,-69.14,-70.051,-69.189]],[[-98.091,-71.912,-98.176,-72.018,-98.168,-72.123,-97.923,-72.117,-97.816,-71.919,-97.585,-71.883,-97.473,-72,-97.582,-72.095,-97.526,-72.149,-97.46,-72.188,-97.345,-72.189,-97.242,-72.132,-97.196,-72.091,-97.155,-72.045,-97.089,-71.944,-96.869,-71.851,-96.383,-71.836,-96.125,-71.896,-96.298,-72.045,-96.715,-72.132,-96.979,-72.222,-96.89,-72.247,-96.799,-72.259,-96.718,-72.255,-96.482,-72.208,-95.906,-72.122,-95.685,-72.057,-95.609,-72.068,-95.61,-72.175,-95.531,-72.249,-95.575,-72.41,-95.826,-72.439,-96.078,-72.454,-96.014,-72.525,-96.03,-72.554,-96.052,-72.577,-96.693,-72.548,-96.804,-72.558,-96.915,-72.578,-97.028,-72.574,-97.25,-72.521,-97.366,-72.522,-97.596,-72.548,-97.828,-72.557,-98.163,-72.556,-98.408,-72.548,-98.641,-72.49,-98.882,-72.473,-99.149,-72.472,-99.434,-72.407,-99.672,-72.38,-100.014,-72.312,-100.104,-72.287,-100.195,-72.273,-100.357,-72.278,-101.602,-72.176,-101.785,-72.178,-101.903,-72.19,-102.022,-72.185,-102.265,-72.135,-102.314,-72.081,-102.288,-72.032,-102.237,-72.009,-102.128,-71.985,-100.401,-71.866,-100.219,-71.833,-100.085,-71.837,-99.985,-71.939,-99.833,-72.046,-99.784,-72.044,-99.735,-72.033,-99.563,-71.945,-99.254,-71.972,-99.082,-71.933,-98.965,-71.854,-98.615,-71.764,-98.394,-71.782,-98.189,-71.82,-98.091,-71.912]],[[-120.556,-73.756,-120.378,-73.856,-120.312,-73.922,-120.272,-73.989,-120.99,-74.157,-121.019,-74.173,-121.054,-74.26,-121.036,-74.279,-121.005,-74.293,-121.002,-74.326,-121.062,-74.337,-122.287,-74.403,-122.859,-74.343,-122.938,-74.302,-122.956,-74.24,-122.891,-74.227,-122.765,-74.219,-122.794,-74.19,-122.875,-74.141,-122.881,-74.099,-122.71,-73.994,-122.625,-73.966,-122.951,-73.867,-122.992,-73.844,-123.035,-73.838,-123.191,-73.849,-123.346,-73.843,-123.292,-73.803,-123.249,-73.739,-123.112,-73.682,-123.012,-73.673,-122.91,-73.68,-122.436,-73.682,-121.967,-73.712,-121.497,-73.733,-120.722,-73.752,-120.556,-73.756]],[[-126.33,-73.286,-126.065,-73.315,-125.976,-73.357,-125.857,-73.388,-125.736,-73.406,-125.627,-73.453,-125.561,-73.536,-125.504,-73.562,-125.326,-73.618,-125.264,-73.666,-125.276,-73.691,-125.612,-73.711,-125.723,-73.703,-125.828,-73.718,-125.86,-73.749,-125.857,-73.78,-125.799,-73.802,-125.674,-73.822,-125.552,-73.82,-125.327,-73.796,-125.224,-73.801,-125.109,-73.826,-124.993,-73.83,-124.694,-73.75,-124.617,-73.735,-124.54,-73.74,-124.129,-73.834,-124.042,-73.88,-124.101,-73.907,-124.152,-73.944,-124.129,-73.971,-123.932,-74.008,-123.851,-74.057,-123.8,-74.076,-123.811,-74.117,-123.839,-74.168,-123.837,-74.226,-123.937,-74.256,-123.982,-74.256,-124.199,-74.226,-124.873,-74.208,-125.09,-74.182,-125.421,-74.07,-125.549,-74.063,-125.683,-74.035,-125.887,-73.955,-126.244,-73.891,-126.472,-73.812,-126.466,-73.746,-126.496,-73.7,-126.538,-73.68,-126.583,-73.67,-126.711,-73.654,-126.838,-73.657,-126.902,-73.677,-127.006,-73.726,-127.122,-73.734,-127.212,-73.724,-127.232,-73.713,-127.233,-73.586,-127.332,-73.567,-127.414,-73.516,-127.429,-73.447,-127.394,-73.382,-127.268,-73.304,-127.124,-73.294,-126.978,-73.308,-126.83,-73.291,-126.597,-73.279,-126.33,-73.286]],[[-159.053,-79.807,-160.302,-79.845,-160.807,-79.812,-161.866,-79.704,-163.317,-79.505,-163.712,-79.442,-163.971,-79.389,-164.226,-79.321,-164.282,-79.246,-164.245,-79.138,-164.2,-79.051,-164.126,-78.995,-163.815,-78.929,-163.66,-78.856,-163.345,-78.78,-163.256,-78.722,-163.124,-78.719,-162.873,-78.725,-162.622,-78.742,-162.39,-78.76,-162.161,-78.793,-161.643,-78.901,-161.283,-79.007,-160.764,-79.132,-160.25,-79.271,-159.964,-79.324,-159.684,-79.402,-159.419,-79.508,-159.366,-79.545,-159.256,-79.591,-159.19,-79.637,-159.119,-79.675,-159.051,-79.695,-158.997,-79.735,-159.01,-79.78,-159.053,-79.807]],[[167.084,-77.322,167.461,-77.394,168.451,-77.386,169.276,-77.455,169.353,-77.525,169.117,-77.561,168.755,-77.653,168.519,-77.681,168.323,-77.683,167.918,-77.644,167.392,-77.649,167.279,-77.703,167.025,-77.756,166.729,-77.851,166.65,-77.774,166.533,-77.7,166.237,-77.547,166.217,-77.525,166.378,-77.494,166.458,-77.444,166.626,-77.377,166.607,-77.336,166.469,-77.289,166.413,-77.252,166.506,-77.189,166.716,-77.162,166.987,-77.187,167.107,-77.271,167.085,-77.307,167.084,-77.322]],[[-31.119,-79.798,-30.985,-79.818,-30.841,-79.771,-30.861,-79.726,-30.78,-79.647,-30.66,-79.733,-29.871,-79.823,-29.614,-79.91,-29.721,-79.93,-29.8,-79.926,-30.029,-79.936,-30.422,-80.011,-30.844,-79.938,-31.594,-79.888,-31.824,-79.85,-32,-79.732,-31.68,-79.634,-31.605,-79.645,-31.119,-79.798]],[[-33.934,-79.32,-34.05,-79.357,-36.481,-79.294,-36.601,-79.283,-36.566,-79.209,-36.238,-79.196,-36.048,-79.181,-35.79,-79.149,-35.597,-79.092,-35.535,-79.09,-34.392,-79.223,-33.995,-79.279,-33.947,-79.305,-33.934,-79.32]],[[-70.334,-79.68,-70.553,-79.683,-70.984,-79.674,-71.414,-79.64,-71.526,-79.624,-71.687,-79.568,-71.735,-79.539,-71.778,-79.501,-71.784,-79.444,-71.667,-79.246,-71.454,-79.129,-71.254,-79.06,-70.626,-78.902,-70.544,-78.884,-69.972,-78.809,-69.748,-78.769,-69.398,-78.686,-67.479,-78.362,-67.038,-78.316,-66.84,-78.35,-66.728,-78.384,-66.787,-78.422,-67.046,-78.514,-67.166,-78.57,-67.481,-78.682,-68.157,-78.871,-68.638,-79.013,-69.251,-79.21,-69.394,-79.28,-69.686,-79.443,-69.634,-79.518,-69.732,-79.618,-70.116,-79.666,-70.334,-79.68]],[[-57.846,-64.054,-57.809,-64.068,-57.774,-64.062,-57.741,-64.048,-57.71,-64.015,-57.593,-63.967,-57.48,-63.962,-57.517,-64.011,-57.249,-64.097,-57.273,-64.166,-57.222,-64.221,-57.328,-64.238,-57.414,-64.296,-57.338,-64.318,-57.295,-64.367,-57.388,-64.379,-57.581,-64.35,-57.683,-64.357,-57.671,-64.311,-57.703,-64.293,-57.823,-64.302,-57.871,-64.401,-57.91,-64.41,-57.952,-64.394,-57.921,-64.331,-57.971,-64.32,-58.022,-64.322,-58.169,-64.369,-58.214,-64.37,-58.304,-64.315,-58.02,-64.242,-58.138,-64.206,-58.162,-64.161,-58.147,-64.097,-58.25,-64.107,-58.352,-64.131,-58.398,-64.135,-58.438,-64.113,-58.425,-64.068,-58.342,-63.994,-58.275,-63.916,-58.146,-63.878,-58.07,-63.847,-57.971,-63.835,-57.926,-63.806,-57.831,-63.804,-57.779,-63.868,-57.781,-63.907,-57.827,-63.949,-57.846,-64.054]],[[-55.528,-63.174,-55.466,-63.2,-55.216,-63.199,-55.156,-63.205,-55.106,-63.249,-55.075,-63.324,-55.157,-63.353,-55.594,-63.336,-55.75,-63.297,-55.83,-63.298,-56.009,-63.342,-56.083,-63.383,-56.379,-63.437,-56.463,-63.418,-56.499,-63.358,-56.505,-63.334,-56.475,-63.318,-56.461,-63.302,-56.466,-63.283,-56.385,-63.234,-56.042,-63.157,-55.59,-63.128,-55.529,-63.157,-55.528,-63.174]],[[-57.978,-61.912,-57.849,-61.94,-57.738,-61.921,-57.677,-61.942,-57.637,-61.998,-57.64,-62.02,-57.807,-62.012,-57.963,-62.078,-58.148,-62.063,-58.172,-62.118,-58.133,-62.146,-58.183,-62.17,-58.341,-62.119,-58.467,-62.137,-58.507,-62.226,-58.562,-62.244,-58.594,-62.248,-58.644,-62.225,-58.746,-62.218,-58.755,-62.206,-58.819,-62.171,-59.004,-62.21,-58.955,-62.164,-58.71,-62.045,-58.684,-62.008,-58.399,-61.938,-58.265,-61.953,-57.978,-61.912]],[[-63.181,-64.47,-63.277,-64.573,-63.131,-64.572,-63.032,-64.535,-62.928,-64.519,-62.837,-64.572,-63.026,-64.611,-63.203,-64.68,-63.275,-64.717,-63.355,-64.734,-63.458,-64.727,-63.558,-64.734,-63.647,-64.803,-63.74,-64.834,-63.77,-64.808,-63.804,-64.792,-64.007,-64.769,-64.099,-64.733,-64.184,-64.71,-64.272,-64.698,-64.226,-64.635,-64.171,-64.582,-63.867,-64.51,-63.897,-64.487,-63.916,-64.457,-63.674,-64.421,-63.668,-64.384,-63.683,-64.343,-63.606,-64.314,-63.534,-64.273,-63.486,-64.261,-63.334,-64.266,-63.23,-64.324,-63.271,-64.381,-63.181,-64.47]],[[-62.326,-64.424,-62.396,-64.465,-62.455,-64.472,-62.508,-64.454,-62.58,-64.514,-62.727,-64.496,-62.782,-64.479,-62.747,-64.472,-62.721,-64.445,-62.643,-64.392,-62.504,-64.253,-62.48,-64.211,-62.59,-64.14,-62.611,-64.116,-62.586,-64.076,-62.545,-64.046,-62.451,-64.012,-62.329,-64.013,-62.268,-64.04,-62.269,-64.09,-62.058,-64.138,-62.094,-64.235,-62.174,-64.296,-62.186,-64.369,-62.304,-64.401,-62.326,-64.424]],[[-67.988,-67.474,-68.093,-67.539,-68.175,-67.558,-68.25,-67.54,-68.325,-67.532,-68.381,-67.555,-68.439,-67.656,-68.507,-67.707,-68.58,-67.733,-68.622,-67.723,-68.664,-67.723,-68.734,-67.746,-68.818,-67.753,-68.901,-67.744,-68.982,-67.68,-69.098,-67.603,-69.12,-67.578,-69.138,-67.515,-69.133,-67.453,-69.082,-67.403,-68.82,-67.234,-68.734,-67.157,-68.656,-67.07,-68.575,-66.993,-68.417,-66.853,-68.336,-66.802,-67.938,-66.657,-67.831,-66.624,-67.711,-66.633,-67.681,-66.709,-67.741,-66.746,-67.932,-66.845,-67.969,-66.982,-67.968,-67.032,-67.949,-67.045,-67.876,-67.062,-67.828,-67.082,-67.761,-67.123,-67.688,-67.147,-67.848,-67.219,-67.956,-67.255,-68.03,-67.3,-68.175,-67.344,-68.235,-67.372,-68.144,-67.382,-68.007,-67.418,-67.969,-67.45,-67.988,-67.474]],[[-73.707,-70.635,-73.55,-70.723,-73.695,-70.794,-74.205,-70.924,-74.505,-70.973,-74.806,-71.012,-76.176,-71.132,-76.271,-71.133,-76.364,-71.117,-76.421,-71.09,-76.512,-70.991,-76.5,-70.941,-76.378,-70.894,-76.249,-70.864,-76.035,-70.836,-75.21,-70.773,-75.127,-70.752,-75.06,-70.706,-75.038,-70.651,-75.007,-70.609,-74.954,-70.59,-74.898,-70.591,-74.79,-70.631,-74.59,-70.792,-74.527,-70.77,-74.469,-70.727,-74.456,-70.587,-74.401,-70.576,-74.225,-70.615,-74.115,-70.655,-74.113,-70.577,-74.038,-70.553,-73.958,-70.561,-73.879,-70.578,-73.707,-70.635]],[[-74.987,-69.728,-74.81,-69.752,-74.55,-69.861,-74.465,-69.917,-74.438,-69.95,-74.46,-69.972,-74.578,-69.998,-74.672,-70.132,-74.849,-70.179,-75.268,-70.149,-75.727,-70.096,-75.764,-70.085,-75.804,-70.038,-75.813,-69.984,-75.76,-69.916,-75.681,-69.882,-75.34,-69.84,-75.314,-69.817,-75.265,-69.749,-75.179,-69.735,-75.136,-69.741,-74.987,-69.728]],[[-74.354,-73.098,-74.499,-73.229,-74.522,-73.244,-74.668,-73.275,-74.615,-73.311,-74.575,-73.328,-74.551,-73.369,-74.467,-73.427,-74.366,-73.464,-74.452,-73.565,-74.575,-73.611,-75.901,-73.333,-76.003,-73.288,-76.053,-73.255,-76.09,-73.203,-76.096,-73.15,-76.062,-73.109,-76.018,-73.085,-75.898,-73.056,-75.775,-73.054,-75.506,-73.109,-75.468,-73.101,-75.417,-73.052,-75.276,-73.05,-75.244,-73.009,-75.439,-72.994,-75.6,-72.953,-75.702,-72.911,-75.731,-72.879,-75.377,-72.82,-74.474,-72.894,-74.336,-72.919,-74.276,-72.951,-74.224,-72.997,-74.354,-73.098]],[[-91.161,-73.182,-91.344,-73.207,-91.511,-73.196,-91.45,-72.968,-91.357,-72.909,-91.382,-72.868,-91.551,-72.754,-91.67,-72.624,-91.612,-72.594,-91.304,-72.547,-90.947,-72.556,-90.807,-72.611,-90.763,-72.681,-90.78,-72.732,-90.895,-72.824,-90.776,-72.854,-90.751,-72.917,-90.776,-72.993,-90.893,-73.084,-90.998,-73.137,-91.161,-73.182]],[[167.643,-78.141,167.517,-78.216,167.377,-78.249,166.936,-78.222,166.626,-78.284,166.285,-78.306,166.122,-78.275,166.051,-78.213,166.013,-78.131,166.013,-78.102,166.111,-78.09,166.567,-78.148,166.76,-78.198,166.864,-78.196,167.138,-78.13,167.364,-78.046,167.422,-78.006,167.498,-77.992,167.594,-78.022,167.64,-78.112,167.643,-78.141]],[[100.981,-65.678,100.547,-65.701,100.512,-65.675,100.351,-65.673,100.293,-65.651,100.27,-65.603,100.324,-65.521,100.409,-65.466,100.545,-65.409,100.607,-65.396,100.883,-65.378,101.079,-65.403,101.221,-65.472,101.259,-65.528,101.238,-65.565,100.981,-65.678]],[[26.857,-70.381,26.793,-70.419,26.609,-70.412,26.47,-70.448,26.358,-70.434,26.005,-70.373,25.964,-70.295,25.954,-70.261,25.983,-70.2,26.301,-70.072,26.426,-70.061,26.605,-70.078,26.686,-70.114,26.737,-70.186,26.875,-70.33,26.857,-70.381]],[[-5.894,-70.552,-6.156,-70.612,-6.181,-70.586,-6.266,-70.55,-6.438,-70.453,-6.244,-70.446,-6.068,-70.405,-5.972,-70.421,-5.95,-70.432,-5.894,-70.552]],[[-3.28,-70.534,-3.442,-70.535,-3.49,-70.508,-3.497,-70.488,-3.287,-70.344,-3.173,-70.307,-2.95,-70.28,-2.805,-70.288,-2.714,-70.32,-2.684,-70.376,-2.683,-70.462,-2.738,-70.507,-3.28,-70.534]],[[-66.174,-80.078,-66.267,-80.081,-66.319,-80.075,-66.367,-80.054,-66.41,-79.973,-66.904,-79.909,-66.962,-79.873,-66.994,-79.793,-67.077,-79.762,-67.719,-79.62,-67.771,-79.589,-67.809,-79.546,-67.688,-79.528,-67.438,-79.56,-66.979,-79.569,-66.881,-79.582,-66.785,-79.608,-66.274,-79.612,-66.014,-79.624,-65.87,-79.738,-65.579,-79.771,-65.54,-79.837,-65.504,-79.954,-65.899,-80.041,-65.989,-80.054,-66.174,-80.078]],[[-67.262,-79.453,-67.434,-79.501,-68.161,-79.479,-68.408,-79.464,-68.549,-79.437,-68.422,-79.333,-68.324,-79.298,-68.233,-79.285,-68.033,-79.227,-67.714,-79.214,-67.475,-79.223,-67.069,-79.268,-67.173,-79.312,-67.24,-79.328,-67.305,-79.394,-67.262,-79.453]],[[-55.165,-61.22,-55.297,-61.249,-55.347,-61.212,-55.369,-61.146,-55.44,-61.106,-55.387,-61.073,-54.671,-61.117,-54.71,-61.14,-55.058,-61.169,-55.165,-61.22]],[[-55.873,-63.536,-55.957,-63.58,-56.178,-63.513,-56.235,-63.469,-56.21,-63.437,-55.858,-63.407,-55.762,-63.422,-55.719,-63.492,-55.873,-63.536]],[[-57.24,-64.567,-57.326,-64.571,-57.433,-64.54,-57.448,-64.488,-57.446,-64.46,-57.366,-64.439,-57.315,-64.435,-57.023,-64.352,-56.895,-64.333,-56.952,-64.382,-56.945,-64.427,-56.991,-64.468,-57.24,-64.567]],[[-56.06,-63.079,-56.259,-63.173,-56.354,-63.169,-56.546,-63.098,-56.601,-63.062,-56.614,-63.045,-56.489,-62.982,-56.14,-63.005,-56.062,-63.013,-56.058,-63.019,-56.051,-63.055,-56.06,-63.079]],[[-57.374,-63.807,-57.36,-63.825,-57.161,-63.816,-57.104,-63.841,-57.218,-63.876,-57.248,-63.868,-57.344,-63.879,-57.616,-63.854,-57.683,-63.813,-57.439,-63.791,-57.374,-63.807]],[[-61.998,-69.722,-62.085,-69.729,-62.172,-69.637,-62.216,-69.495,-62.496,-69.288,-62.568,-69.18,-62.516,-69.155,-62.442,-69.146,-62.239,-69.176,-62.118,-69.215,-61.978,-69.3,-61.816,-69.376,-61.784,-69.442,-61.807,-69.515,-61.911,-69.533,-61.908,-69.588,-61.97,-69.691,-61.998,-69.722]],[[-60.625,-62.56,-60.576,-62.573,-60.14,-62.549,-60.003,-62.618,-59.85,-62.615,-60.221,-62.745,-60.322,-62.708,-60.354,-62.679,-60.378,-62.617,-60.62,-62.633,-60.697,-62.621,-60.796,-62.662,-60.995,-62.679,-61.063,-62.679,-61.15,-62.634,-61.152,-62.589,-60.975,-62.592,-60.838,-62.534,-60.799,-62.475,-60.732,-62.491,-60.625,-62.56]],[[-71.985,-69.698,-72.202,-69.74,-72.345,-69.707,-72.777,-69.645,-72.957,-69.529,-72.937,-69.469,-72.857,-69.433,-72.726,-69.413,-72.464,-69.452,-72.331,-69.492,-71.985,-69.698]],[[-95.027,-72.665,-95.219,-72.67,-95.273,-72.647,-95.216,-72.599,-94.753,-72.517,-94.566,-72.468,-94.538,-72.476,-94.514,-72.492,-94.434,-72.589,-94.426,-72.613,-95.027,-72.665]],[[-104.538,-73.166,-104.66,-73.212,-104.881,-73.201,-105.053,-73.126,-105.123,-73.026,-105.132,-72.992,-105.085,-72.966,-104.972,-72.941,-104.538,-73.166]],[[-119.549,-74.11,-119.758,-74.122,-119.821,-74.12,-119.887,-74.097,-119.905,-74.082,-119.797,-74.029,-119.694,-74.006,-119.662,-73.989,-119.803,-73.815,-119.669,-73.809,-119.516,-73.775,-119.216,-73.778,-118.959,-73.809,-118.91,-73.834,-118.877,-73.878,-118.99,-73.967,-119.059,-73.998,-119.449,-74.076,-119.549,-74.11]],[[-127.366,-74.623,-127.518,-74.641,-127.817,-74.575,-127.915,-74.543,-128,-74.489,-128.07,-74.478,-128.096,-74.466,-128.133,-74.327,-128.043,-74.312,-127.853,-74.332,-127.486,-74.405,-127.23,-74.425,-127.145,-74.48,-127.232,-74.578,-127.366,-74.623]],[[-161.994,-83.119,-162.305,-83.142,-163.047,-83.097,-163.242,-83.06,-163.348,-83.022,-163.552,-82.988,-163.602,-82.969,-163.602,-82.927,-163.634,-82.902,-163.704,-82.879,-163.735,-82.857,-163.796,-82.843,-162.798,-82.865,-162.411,-82.899,-162.34,-82.923,-161.635,-83.027,-161.828,-83.043,-161.994,-83.119]],[[-160.467,-81.589,-160.571,-81.598,-163.253,-81.482,-163.766,-81.445,-163.89,-81.424,-163.939,-81.404,-163.951,-81.391,-163.93,-81.352,-163.869,-81.324,-163.201,-81.281,-162.456,-81.313,-161.559,-81.397,-160.938,-81.463,-160.617,-81.522,-160.485,-81.567,-160.467,-81.589]],[[163.976,-74.833,163.845,-74.833,163.763,-74.803,163.737,-74.734,163.742,-74.712,164.002,-74.629,164.208,-74.608,164.098,-74.732,164.059,-74.753,163.976,-74.833]],[[162.968,-75.567,162.788,-75.696,162.662,-75.692,162.591,-75.669,162.72,-75.597,162.842,-75.566,162.917,-75.557,162.968,-75.567]],[[169.844,-73.605,169.709,-73.625,169.522,-73.562,169.479,-73.539,169.659,-73.418,169.645,-73.379,169.672,-73.346,169.74,-73.32,169.783,-73.324,169.887,-73.459,169.96,-73.514,169.859,-73.568,169.844,-73.605]],[[164.834,-67.54,164.746,-67.569,164.692,-67.56,164.639,-67.5,164.696,-67.408,164.675,-67.289,164.684,-67.259,164.825,-67.326,164.85,-67.364,164.907,-67.419,164.919,-67.447,164.86,-67.504,164.834,-67.54]],[[163.302,-66.821,163.284,-66.882,163.235,-66.868,163.164,-66.819,163.09,-66.701,163.156,-66.688,163.238,-66.709,163.271,-66.768,163.299,-66.798,163.302,-66.821]],[[162.611,-66.477,162.557,-66.525,162.511,-66.52,162.303,-66.4,162.326,-66.347,162.297,-66.304,162.302,-66.265,162.311,-66.251,162.563,-66.433,162.611,-66.477]],[[96.613,-66.036,96.727,-66.061,96.932,-66.058,97.006,-66.097,97.019,-66.139,97.016,-66.164,96.934,-66.201,96.395,-66.225,96.307,-66.186,96.399,-66.08,96.5,-66.046,96.613,-66.036]],[[100.265,-66.217,100.133,-66.229,100.082,-66.203,100.076,-66.188,100.174,-66.131,100.291,-66.112,100.282,-66.18,100.265,-66.217]],[[98.846,-66.47,98.752,-66.482,98.655,-66.453,98.605,-66.4,98.596,-66.383,98.749,-66.369,98.95,-66.421,98.846,-66.47]],[[103.397,-65.445,103.337,-65.469,103.176,-65.455,103.138,-65.435,103.124,-65.338,103.113,-65.312,103.054,-65.285,102.789,-65.236,102.76,-65.168,102.796,-65.136,102.893,-65.13,103.137,-65.191,103.191,-65.237,103.182,-65.308,103.186,-65.331,103.261,-65.377,103.379,-65.426,103.397,-65.445]],[[92.601,-65.808,92.471,-65.822,92.333,-65.807,92.263,-65.76,92.248,-65.74,92.301,-65.707,92.496,-65.702,92.634,-65.731,92.665,-65.76,92.67,-65.775,92.601,-65.808]],[[85.822,-66.953,85.65,-66.98,85.622,-66.965,85.617,-66.951,85.359,-66.854,85.314,-66.776,85.34,-66.723,85.553,-66.729,85.806,-66.775,85.938,-66.894,85.822,-66.953]],[[86.542,-66.767,86.427,-66.792,86.337,-66.788,86.232,-66.733,86.278,-66.697,86.383,-66.675,86.521,-66.687,86.557,-66.706,86.652,-66.718,86.542,-66.767]],[[85.329,-66.612,85.222,-66.643,85.136,-66.637,85.08,-66.604,85.069,-66.584,85.121,-66.519,85.165,-66.522,85.194,-66.556,85.329,-66.612]],[[69.918,-71.918,69.792,-72.047,69.744,-72.044,69.693,-71.968,69.737,-71.922,69.796,-71.894,69.895,-71.908,69.918,-71.918]],[[68.462,-72.3,68.409,-72.3,68.436,-72.26,68.566,-72.19,68.667,-72.103,68.729,-72.089,68.84,-72.165,68.817,-72.229,68.67,-72.276,68.462,-72.3]],[[72.002,-70.633,71.929,-70.633,71.841,-70.622,71.726,-70.549,71.659,-70.497,71.637,-70.444,71.647,-70.336,71.705,-70.284,71.797,-70.264,71.838,-70.312,71.851,-70.368,71.88,-70.406,72,-70.457,72.056,-70.501,72.073,-70.525,72.097,-70.575,72.078,-70.609,72.002,-70.633]],[[48.546,-66.784,48.378,-66.807,48.304,-66.798,48.296,-66.774,48.294,-66.75,48.301,-66.724,48.358,-66.704,48.638,-66.701,48.751,-66.72,48.782,-66.731,48.786,-66.768,48.775,-66.778,48.546,-66.784]],[[16.223,-70.008,16.159,-70.072,15.845,-69.982,15.663,-69.955,15.614,-69.939,15.571,-69.885,15.563,-69.863,15.597,-69.828,15.699,-69.773,15.91,-69.728,16.247,-69.705,16.573,-69.723,16.625,-69.75,16.315,-69.844,16.223,-70.008]],[[3.037,-70.597,2.698,-70.624,2.623,-70.593,2.585,-70.535,2.631,-70.5,3.072,-70.382,3.193,-70.393,3.231,-70.403,3.26,-70.449,3.221,-70.519,3.171,-70.554,3.037,-70.597]],[[-2.955,-71.214,-3.061,-71.237,-3.201,-71.23,-3.309,-71.201,-3.386,-71.143,-3.404,-71.12,-3.392,-71.081,-3.399,-71.062,-3.263,-71.052,-3.213,-71.076,-3.191,-71.095,-2.955,-71.214]],[[1.299,-70.255,1.212,-70.381,1.156,-70.378,1.105,-70.304,0.99,-70.224,0.953,-70.169,0.95,-70.094,1.027,-70.05,1.315,-70.023,1.412,-70.041,1.461,-70.136,1.299,-70.255]],[[-2.533,-70.768,-2.423,-70.8,-2.256,-70.796,-2.092,-70.821,-2.119,-70.855,-2.213,-70.902,-2.293,-70.998,-2.369,-71.044,-2.607,-71.141,-2.783,-71.167,-2.825,-71.113,-2.822,-71.057,-2.805,-71.015,-2.801,-70.982,-2.963,-70.94,-2.975,-70.883,-3.007,-70.851,-3.489,-70.736,-3.575,-70.703,-3.537,-70.683,-3.04,-70.674,-2.75,-70.694,-2.533,-70.768]],[[4.526,-70.479,4.365,-70.503,4.18,-70.451,4.13,-70.417,4.076,-70.325,4.07,-70.29,4.112,-70.267,4.256,-70.241,4.495,-70.251,4.586,-70.294,4.618,-70.369,4.59,-70.433,4.526,-70.479]],[[-20.607,-73.887,-20.654,-74.105,-20.641,-74.151,-20.6,-74.197,-20.423,-74.317,-20.411,-74.408,-20.417,-74.443,-20.489,-74.493,-20.737,-74.481,-20.818,-74.455,-20.846,-74.438,-20.977,-74.225,-21.051,-74.176,-21.167,-74.133,-21.61,-74.092,-22.035,-74.107,-21.93,-74.057,-21.288,-73.989,-21.126,-73.94,-21.025,-73.88,-20.979,-73.79,-20.867,-73.677,-20.69,-73.625,-20.58,-73.619,-20.521,-73.712,-20.521,-73.798,-20.607,-73.887]],[[-16.104,-72.679,-16.175,-72.703,-16.318,-72.702,-16.453,-72.652,-16.51,-72.582,-16.517,-72.531,-16.455,-72.474,-16.356,-72.459,-16.303,-72.478,-16.173,-72.6,-16.104,-72.679]],[[-12.509,-72.173,-12.588,-72.196,-12.72,-72.188,-12.888,-72.137,-12.944,-72.099,-12.963,-72.064,-12.915,-72.015,-12.875,-72.001,-12.789,-72.007,-12.637,-72.071,-12.535,-72.14,-12.509,-72.173]],[[-32.343,-79.674,-32.515,-79.683,-32.583,-79.658,-32.501,-79.592,-32.377,-79.535,-32.15,-79.53,-31.933,-79.568,-31.957,-79.604,-32.001,-79.607,-32.343,-79.674]],[[-54.071,-61.299,-54.115,-61.308,-54.184,-61.27,-54.192,-61.247,-54.122,-61.202,-54.05,-61.142,-54.024,-61.135,-54.041,-61.255,-54.071,-61.299]],[[-58.838,-62.303,-59.059,-62.348,-59.175,-62.302,-59.202,-62.283,-59.064,-62.239,-58.991,-62.249,-58.962,-62.264,-58.879,-62.268,-58.838,-62.303]],[[-61.158,-69.976,-61.309,-69.978,-61.378,-69.95,-61.404,-69.933,-61.386,-69.893,-61.327,-69.856,-61.152,-69.883,-61.108,-69.955,-61.158,-69.976]],[[-60.656,-68.768,-60.693,-68.795,-60.82,-68.778,-60.894,-68.759,-61.015,-68.71,-60.947,-68.681,-60.814,-68.688,-60.705,-68.722,-60.656,-68.768]],[[-60.741,-70.711,-60.826,-70.711,-60.896,-70.69,-60.958,-70.629,-60.976,-70.599,-60.942,-70.533,-60.884,-70.518,-60.554,-70.509,-60.452,-70.544,-60.449,-70.603,-60.488,-70.647,-60.741,-70.711]],[[-60.552,-71.053,-60.652,-71.059,-60.79,-71.041,-60.906,-71.007,-60.946,-70.967,-60.889,-70.934,-60.783,-70.914,-60.613,-70.92,-60.533,-70.962,-60.516,-71,-60.536,-71.041,-60.552,-71.053]],[[-59.389,-62.444,-59.525,-62.451,-59.619,-62.395,-59.661,-62.354,-59.479,-62.352,-59.396,-62.367,-59.353,-62.413,-59.389,-62.444]],[[-60.505,-62.967,-60.555,-62.978,-60.62,-62.969,-60.618,-62.987,-60.564,-63.009,-60.622,-63.018,-60.693,-62.996,-60.74,-62.949,-60.706,-62.906,-60.637,-62.895,-60.505,-62.967]],[[-60.653,-63.867,-60.778,-63.902,-60.852,-63.891,-60.972,-63.849,-60.81,-63.837,-60.797,-63.717,-60.715,-63.669,-60.562,-63.696,-60.656,-63.759,-60.689,-63.808,-60.655,-63.85,-60.653,-63.867]],[[-61.952,-64.077,-62.044,-64.08,-62.021,-64.027,-61.936,-63.99,-61.798,-63.967,-61.886,-64.027,-61.911,-64.054,-61.952,-64.077]],[[-63.316,-64.861,-63.474,-64.907,-63.558,-64.906,-63.459,-64.796,-63.367,-64.792,-63.219,-64.73,-63.177,-64.739,-63.257,-64.791,-63.316,-64.861]],[[-62.615,-63.069,-62.655,-63.074,-62.639,-63.032,-62.527,-62.924,-62.317,-62.874,-62.344,-62.918,-62.411,-62.972,-62.615,-63.069]],[[-65.845,-65.842,-66.064,-65.881,-66.175,-65.867,-66.181,-65.826,-66.153,-65.774,-66.05,-65.745,-66.067,-65.666,-66,-65.633,-65.968,-65.571,-65.834,-65.527,-65.637,-65.548,-65.668,-65.626,-65.67,-65.653,-65.784,-65.674,-65.814,-65.687,-65.841,-65.738,-65.836,-65.814,-65.845,-65.842]],[[-66.595,-66.201,-66.819,-66.313,-66.85,-66.305,-66.868,-66.294,-66.867,-66.275,-66.792,-66.234,-66.779,-66.111,-66.631,-66.067,-66.575,-66.082,-66.623,-66.134,-66.593,-66.179,-66.595,-66.201]],[[-67.362,-66.895,-67.409,-66.902,-67.521,-66.897,-67.593,-66.876,-67.5,-66.804,-67.511,-66.756,-67.426,-66.737,-67.332,-66.754,-67.269,-66.815,-67.257,-66.841,-67.362,-66.895]],[[-67.349,-67.766,-67.545,-67.785,-67.693,-67.763,-67.69,-67.688,-67.731,-67.679,-67.743,-67.661,-67.557,-67.604,-67.418,-67.591,-67.247,-67.599,-67.175,-67.625,-67.149,-67.65,-67.28,-67.712,-67.3,-67.737,-67.349,-67.766]],[[-71.695,-70.265,-71.648,-70.295,-71.432,-70.267,-71.355,-70.298,-71.34,-70.317,-71.438,-70.392,-71.551,-70.439,-71.685,-70.442,-71.782,-70.319,-71.795,-70.288,-71.695,-70.265]],[[-73.878,-73.357,-73.975,-73.376,-74.038,-73.366,-74.147,-73.315,-74.134,-73.277,-74.084,-73.249,-74.049,-73.22,-73.832,-73.113,-73.674,-73.1,-73.542,-73.124,-73.682,-73.225,-73.721,-73.296,-73.878,-73.357]],[[-93.796,-72.92,-93.966,-72.92,-94.078,-72.884,-94.113,-72.86,-94.047,-72.823,-94.004,-72.82,-93.8,-72.882,-93.756,-72.908,-93.796,-72.92]],[[-116.739,-74.165,-117.23,-74.193,-117.363,-74.161,-117.398,-74.122,-117.376,-74.083,-116.381,-73.866,-116.203,-73.896,-116.155,-73.91,-116.451,-74.018,-116.585,-74.056,-116.609,-74.069,-116.534,-74.083,-116.514,-74.096,-116.571,-74.126,-116.739,-74.165]],[[-132.391,-74.442,-132.546,-74.498,-132.857,-74.462,-132.832,-74.422,-132.552,-74.387,-132.362,-74.41,-132.391,-74.442]],[[-131.067,-74.584,-131.179,-74.605,-131.598,-74.554,-131.841,-74.543,-131.952,-74.514,-132.025,-74.489,-132.049,-74.464,-132.163,-74.426,-131.938,-74.349,-131.763,-74.324,-131.594,-74.33,-131.56,-74.367,-131.234,-74.414,-130.981,-74.414,-130.957,-74.456,-130.967,-74.515,-131.067,-74.584]],[[-147.588,-76.65,-147.579,-76.663,-147.73,-76.653,-147.954,-76.597,-148.001,-76.577,-147.9,-76.558,-147.77,-76.577,-147.649,-76.611,-147.588,-76.65]],[[-145.238,-75.711,-145.348,-75.716,-145.541,-75.693,-146.043,-75.612,-146.151,-75.574,-146.076,-75.533,-145.896,-75.505,-145.761,-75.514,-145.417,-75.588,-145.315,-75.641,-145.252,-75.683,-145.238,-75.711]],[[-146.607,-76.961,-146.981,-77.006,-147.079,-76.993,-147.044,-76.93,-147.101,-76.887,-147.116,-76.866,-147.087,-76.837,-146.867,-76.837,-146.244,-76.883,-146.164,-76.949,-146.607,-76.961]],[[-148.596,-77.007,-149.015,-77.019,-149.245,-76.993,-149.303,-76.916,-149.238,-76.9,-148.704,-76.936,-148.509,-76.955,-148.44,-76.977,-148.474,-76.998,-148.596,-77.007]],[[-149.333,-76.717,-148.928,-76.73,-148.663,-76.721,-148.384,-76.744,-148.321,-76.772,-148.371,-76.795,-148.67,-76.802,-148.815,-76.841,-148.984,-76.845,-149.238,-76.818,-149.469,-76.757,-149.333,-76.717]],[[-149.218,-77.336,-148.929,-77.387,-149.439,-77.371,-149.662,-77.301,-149.519,-77.275,-149.375,-77.28,-149.249,-77.315,-149.218,-77.336]],[[-150.233,-76.776,-150.655,-76.789,-150.83,-76.762,-150.874,-76.737,-150.838,-76.714,-150.177,-76.691,-150.104,-76.719,-150.085,-76.735,-150.233,-76.776]],[[-146.69,-76.246,-146.894,-76.261,-147.151,-76.197,-147.345,-76.147,-147.408,-76.105,-147.421,-76.09,-147.418,-76.073,-147.361,-76.063,-146.949,-76.098,-146.69,-76.246]],[[-146.79,-76.633,-146.908,-76.714,-147.221,-76.671,-147.355,-76.619,-147.279,-76.553,-147.135,-76.532,-146.947,-76.555,-146.878,-76.563,-146.79,-76.633]],[[-150.397,-77.369,-150.475,-77.374,-151.344,-77.296,-151.512,-77.273,-151.218,-77.226,-151.022,-77.22,-150.499,-77.335,-150.356,-77.349,-150.397,-77.369]],[[-153.93,-80.033,-154.114,-80.036,-154.349,-80.026,-154.529,-80,-154.942,-79.966,-155.045,-79.9,-155.525,-79.846,-155.751,-79.83,-155.674,-79.766,-155.162,-79.851,-154.535,-79.936,-154.025,-79.988,-153.93,-80.033]],[[-149.231,-77.121,-149.293,-77.137,-149.729,-77.129,-149.817,-77.114,-149.856,-77.099,-150.462,-77.076,-150.736,-77.004,-150.789,-76.982,-150.68,-76.948,-150.476,-76.926,-150.393,-76.899,-149.871,-76.875,-149.79,-76.889,-149.743,-76.927,-149.506,-77.002,-149.442,-77.049,-149.416,-77.079,-149.288,-77.093,-149.231,-77.121]],[[-157.987,-82.105,-158.076,-82.112,-158.154,-82.058,-158.545,-81.949,-158.773,-81.875,-158.926,-81.819,-158.989,-81.779,-158.914,-81.78,-158.347,-81.9,-158.261,-81.947,-157.835,-82.031,-157.987,-82.105]]]},{"n":"Sint Maarten","c":"SX","r":6,"ml":5,"lx":-63.07,"ly":18.04,"b":[-63.125,18.019,-63.011,18.069],"p":[[[-63.123,18.069,-63.011,18.069,-63.012,18.045,-63.023,18.019,-63.09,18.041,-63.125,18.064,-63.123,18.069]]]},{"n":"Tuvalu","c":"TV","r":6,"ml":5,"lx":179.21,"ly":-8.51,"b":[179.196,-8.535,179.217,-8.466],"p":[[[179.214,-8.524,179.201,-8.535,179.196,-8.535,179.201,-8.512,179.198,-8.489,179.199,-8.47,179.203,-8.466,179.212,-8.488,179.217,-8.515,179.214,-8.524]]]}]} \ No newline at end of file diff --git a/src/maps/data/lakes_50m.json b/src/maps/data/lakes_50m.json new file mode 100644 index 0000000..9f59147 --- /dev/null +++ b/src/maps/data/lakes_50m.json @@ -0,0 +1 @@ +{"f":[{"n":"Mälaren","z":2,"b":[16.044,59.268,17.98,59.621],"p":[[[17.98,59.329,17.876,59.271,17.571,59.268,17.475,59.292,17.371,59.295,17.305,59.272,17.175,59.356,17.066,59.373,16.914,59.446,16.742,59.431,16.61,59.454,16.144,59.448,16.044,59.478,16.252,59.493,16.473,59.519,16.574,59.612,16.647,59.559,16.752,59.543,16.841,59.588,16.978,59.551,17.063,59.569,17.372,59.496,17.391,59.584,17.534,59.539,17.687,59.542,17.672,59.595,17.76,59.621,17.786,59.598,17.809,59.553,17.773,59.414,17.829,59.379,17.964,59.359,17.98,59.329]]]},{"n":"Ladoga","z":1,"b":[29.825,59.907,32.944,61.775],"p":[[[29.837,61.226,29.86,61.239,29.865,61.251,29.855,61.271,29.891,61.295,29.949,61.289,30.029,61.254,30.079,61.243,30.094,61.259,30.089,61.271,30.09,61.299,30.13,61.313,30.174,61.314,30.193,61.327,30.196,61.348,30.183,61.361,30.179,61.373,30.201,61.389,30.21,61.418,30.217,61.431,30.237,61.446,30.252,61.459,30.252,61.473,30.235,61.493,30.239,61.515,30.285,61.517,30.466,61.487,30.481,61.49,30.486,61.515,30.513,61.525,30.551,61.535,30.606,61.541,30.617,61.56,30.593,61.579,30.594,61.606,30.758,61.733,30.852,61.775,30.888,61.765,30.896,61.754,30.906,61.742,31.026,61.714,31.057,61.689,31.071,61.645,31.098,61.62,31.161,61.619,31.172,61.63,31.238,61.643,31.258,61.641,31.286,61.615,31.308,61.605,31.328,61.617,31.334,61.627,31.365,61.627,31.486,61.551,31.533,61.497,31.576,61.437,31.604,61.423,31.62,61.427,31.617,61.442,31.62,61.461,31.637,61.463,31.698,61.415,31.762,61.384,31.871,61.347,31.899,61.324,32.36,61.181,32.527,61.118,32.563,61.079,32.581,61.045,32.581,61.014,32.593,60.998,32.633,60.987,32.641,60.969,32.765,60.886,32.808,60.826,32.812,60.756,32.844,60.712,32.903,60.694,32.937,60.67,32.944,60.643,32.907,60.586,32.841,60.497,32.816,60.482,32.782,60.488,32.722,60.521,32.642,60.537,32.6,60.533,32.596,60.511,32.618,60.476,32.664,60.427,32.677,60.392,32.656,60.372,32.595,60.355,32.59,60.33,32.614,60.286,32.612,60.245,32.584,60.209,32.5,60.177,32.362,60.148,32.26,60.154,32.196,60.195,32.057,60.227,31.843,60.248,31.699,60.236,31.599,60.17,31.591,60.156,31.597,60.144,31.606,60.142,31.606,60.134,31.548,60.072,31.541,60.035,31.565,59.998,31.567,59.967,31.547,59.94,31.51,59.92,31.455,59.907,31.401,59.911,31.349,59.933,31.274,59.938,31.176,59.926,31.106,59.928,31.064,59.943,31.043,59.951,31.063,59.963,31.145,60.006,31.143,60.032,31.106,60.056,31.095,60.084,31.115,60.135,31.109,60.146,31.019,60.193,30.957,60.264,30.849,60.443,30.832,60.458,30.817,60.459,30.803,60.447,30.776,60.448,30.774,60.492,30.759,60.525,30.724,60.562,30.663,60.593,30.534,60.63,30.524,60.655,30.504,60.704,30.504,60.732,30.525,60.739,30.533,60.766,30.528,60.812,30.502,60.843,30.456,60.86,30.441,60.874,30.436,60.903,30.409,60.928,30.293,61.005,30.125,61.087,29.857,61.188,29.825,61.216,29.837,61.226]]]},{"n":"Albert","z":1,"b":[30.387,1.021,31.475,2.401],"p":[[[31.472,2.385,31.396,2.28,31.384,2.261,31.387,2.2,31.4,1.896,31.387,1.857,31.369,1.836,31.343,1.834,31.322,1.803,31.305,1.742,31.268,1.688,31.166,1.605,31.129,1.588,31.04,1.564,31.014,1.546,30.969,1.527,30.893,1.448,30.785,1.309,30.727,1.224,30.719,1.191,30.594,1.041,30.569,1.029,30.539,1.021,30.507,1.041,30.501,1.05,30.504,1.121,30.492,1.173,30.492,1.203,30.502,1.212,30.495,1.23,30.478,1.239,30.468,1.258,30.45,1.259,30.414,1.255,30.4,1.256,30.387,1.297,30.441,1.382,30.479,1.467,30.529,1.532,30.591,1.579,30.674,1.676,30.714,1.702,30.868,1.859,30.936,1.915,30.982,1.932,31.179,2.073,31.259,2.153,31.303,2.226,31.344,2.291,31.431,2.388,31.475,2.401,31.472,2.385]]]},{"n":"Erie","z":1,"b":[-83.456,41.398,-78.872,43.106],"p":[[[-83.105,42.286,-83.076,42.275,-83.105,42.215,-83.117,42.12,-83.108,42.071,-83.061,42.05,-82.937,42.006,-82.834,41.997,-82.69,42.036,-82.645,42.037,-82.604,42.025,-82.567,42,-82.512,41.932,-82.496,41.944,-82.49,42.005,-82.462,42.058,-82.412,42.103,-82.311,42.158,-82.16,42.223,-82.044,42.257,-81.938,42.269,-81.929,42.279,-81.874,42.315,-81.866,42.308,-81.887,42.274,-81.887,42.257,-81.867,42.258,-81.841,42.295,-81.809,42.368,-81.73,42.447,-81.495,42.591,-81.404,42.63,-81.252,42.654,-81.039,42.662,-80.827,42.643,-80.617,42.596,-80.473,42.574,-80.397,42.577,-80.094,42.546,-80.062,42.554,-80.105,42.572,-80.305,42.59,-80.368,42.608,-80.446,42.604,-80.451,42.613,-80.396,42.676,-80.365,42.683,-80.326,42.707,-80.286,42.738,-80.22,42.774,-80.099,42.802,-79.923,42.822,-79.747,42.858,-79.699,42.862,-79.661,42.851,-79.594,42.854,-79.386,42.881,-79.264,42.88,-79.176,42.873,-79.122,42.858,-79.057,42.862,-78.936,42.907,-78.922,42.924,-78.947,42.952,-79.011,42.991,-79.04,43.021,-79.035,43.044,-79.043,43.064,-79.073,43.093,-79.066,43.106,-78.908,43.07,-78.895,43.058,-78.9,43.033,-78.924,42.998,-78.92,42.948,-78.886,42.886,-78.872,42.836,-78.878,42.799,-78.918,42.764,-78.994,42.731,-79.063,42.68,-79.125,42.611,-79.209,42.555,-79.315,42.512,-79.416,42.457,-79.512,42.391,-79.704,42.3,-80.076,42.146,-80.125,42.128,-80.168,42.109,-80.252,42.075,-80.334,42.041,-80.869,41.893,-81.229,41.77,-81.447,41.671,-81.637,41.548,-81.723,41.511,-81.817,41.501,-81.966,41.508,-82.121,41.493,-82.282,41.457,-82.387,41.427,-82.434,41.405,-82.48,41.398,-82.524,41.407,-82.643,41.467,-82.651,41.462,-82.673,41.454,-82.743,41.464,-82.822,41.465,-82.891,41.443,-82.96,41.44,-83.027,41.455,-82.971,41.476,-82.7,41.521,-82.703,41.529,-82.801,41.556,-82.831,41.58,-82.856,41.578,-82.875,41.55,-82.911,41.538,-82.963,41.542,-83.016,41.563,-83.069,41.601,-83.243,41.662,-83.291,41.693,-83.352,41.714,-83.443,41.725,-83.456,41.742,-83.441,41.766,-83.362,41.88,-83.316,41.923,-83.297,41.935,-83.273,41.935,-83.262,41.946,-83.265,41.971,-83.256,41.986,-83.205,42.027,-83.177,42.049,-83.188,42.083,-83.175,42.146,-83.142,42.228,-83.105,42.286],[-78.99,42.992,-78.961,42.988,-78.93,43.022,-78.914,43.042,-78.916,43.053,-78.988,43.064,-79.014,43.052,-79.019,43.024,-79.012,43.003,-78.99,42.992],[-82.638,41.747,-82.627,41.771,-82.643,41.815,-82.654,41.827,-82.674,41.806,-82.685,41.757,-82.685,41.736,-82.638,41.747]]]},{"n":"Chad","z":1,"b":[14.259,12.877,14.743,13.239],"p":[[[14.742,13.078,14.743,13.062,14.716,13.047,14.694,13.024,14.686,12.993,14.661,12.982,14.62,12.992,14.592,12.99,14.516,12.98,14.475,12.952,14.407,12.899,14.347,12.877,14.295,12.884,14.275,12.903,14.259,12.986,14.259,13.004,14.282,13.014,14.289,13.029,14.307,13.065,14.311,13.082,14.312,13.087,14.33,13.116,14.33,13.135,14.342,13.162,14.37,13.185,14.415,13.209,14.496,13.239,14.554,13.238,14.591,13.214,14.653,13.208,14.668,13.195,14.707,13.137,14.742,13.078]]]},{"n":"Malawi","z":1,"b":[33.905,-14.403,35.284,-9.495],"p":[[[35.26,-14.277,35.284,-14.365,35.273,-14.391,35.258,-14.403,35.236,-14.401,35.193,-14.353,35.13,-14.289,35.048,-14.177,35.014,-14.157,34.958,-14.148,34.947,-14.134,34.942,-14.071,34.93,-14.042,34.906,-14.021,34.881,-14.012,34.857,-14.014,34.849,-14.035,34.858,-14.074,34.85,-14.112,34.825,-14.15,34.816,-14.178,34.825,-14.197,34.819,-14.217,34.77,-14.254,34.729,-14.268,34.707,-14.262,34.701,-14.237,34.678,-14.214,34.637,-14.195,34.6,-14.159,34.567,-14.106,34.547,-14.048,34.538,-13.985,34.546,-13.938,34.571,-13.908,34.596,-13.846,34.622,-13.751,34.605,-13.669,34.547,-13.6,34.498,-13.567,34.458,-13.572,34.41,-13.53,34.321,-13.379,34.313,-13.346,34.319,-13.308,34.338,-13.265,34.344,-13.158,34.33,-12.945,34.32,-12.921,34.299,-12.857,34.275,-12.825,34.267,-12.791,34.273,-12.756,34.263,-12.73,34.236,-12.713,34.214,-12.684,34.196,-12.642,34.197,-12.481,34.18,-12.425,34.15,-12.393,34.107,-12.384,34.074,-12.353,34.037,-12.25,34.033,-12.209,34.059,-12.049,34.1,-11.976,34.164,-11.925,34.194,-11.88,34.192,-11.84,34.222,-11.788,34.318,-11.679,34.323,-11.653,34.289,-11.425,34.262,-11.312,34.23,-11.228,34.224,-11.167,34.241,-11.131,34.245,-11.099,34.234,-11.073,34.203,-10.614,34.19,-10.59,34.195,-10.567,34.218,-10.544,34.229,-10.517,34.227,-10.485,34.238,-10.461,34.26,-10.448,34.248,-10.4,34.204,-10.319,34.155,-10.256,34.057,-10.157,34.016,-10.098,33.985,-10.024,33.941,-9.874,33.919,-9.843,33.907,-9.802,33.905,-9.751,33.916,-9.712,33.954,-9.658,33.959,-9.627,33.95,-9.565,33.962,-9.532,33.996,-9.495,34.089,-9.538,34.321,-9.732,34.328,-9.757,34.476,-9.949,34.524,-10.03,34.524,-10.073,34.57,-10.241,34.58,-10.32,34.57,-10.38,34.572,-10.428,34.59,-10.496,34.584,-10.525,34.637,-10.626,34.662,-10.71,34.667,-10.792,34.652,-10.873,34.606,-10.99,34.598,-11.037,34.608,-11.08,34.638,-11.127,34.688,-11.177,34.726,-11.238,34.752,-11.309,34.774,-11.342,34.801,-11.341,34.851,-11.352,34.891,-11.394,34.937,-11.463,34.959,-11.577,34.957,-11.735,34.934,-11.871,34.891,-11.985,34.857,-12.048,34.832,-12.058,34.788,-12.094,34.726,-12.157,34.7,-12.2,34.711,-12.224,34.714,-12.279,34.711,-12.364,34.702,-12.403,34.694,-12.422,34.791,-12.643,34.808,-12.767,34.772,-12.87,34.77,-12.963,34.8,-13.044,34.81,-13.142,34.8,-13.258,34.812,-13.332,34.846,-13.366,34.862,-13.46,34.859,-13.616,34.868,-13.701,34.888,-13.717,35.03,-13.73,35.056,-13.743,35.097,-13.825,35.18,-14.064,35.178,-14.088,35.26,-14.277],[34.622,-12.067,34.642,-12.014,34.624,-11.985,34.591,-11.971,34.554,-11.982,34.542,-12.019,34.58,-12.066,34.622,-12.067],[34.662,-12.101,34.684,-12.119,34.719,-12.111,34.746,-12.088,34.756,-12.059,34.756,-12.031,34.739,-12.013,34.715,-12.003,34.68,-12.009,34.667,-12.048,34.662,-12.101]]]},{"n":"Nyanza","z":1,"b":[31.664,-2.985,34.813,0.472],"p":[[[34.718,-0.092,34.813,-0.276,34.798,-0.296,34.703,-0.325,34.577,-0.33,34.478,-0.353,34.458,-0.369,34.475,-0.464,34.456,-0.495,34.425,-0.502,34.376,-0.459,34.329,-0.459,34.275,-0.445,34.217,-0.442,34.175,-0.47,34.079,-0.579,34.06,-0.645,34.068,-0.725,34.106,-0.798,34.163,-0.879,34.144,-0.94,34.121,-0.975,34.07,-0.997,34.063,-1.034,33.932,-1.188,33.903,-1.247,33.907,-1.278,33.952,-1.321,33.942,-1.34,33.844,-1.342,33.829,-1.363,33.829,-1.39,33.816,-1.406,33.822,-1.425,33.915,-1.495,33.921,-1.514,33.889,-1.513,33.791,-1.485,33.73,-1.48,33.686,-1.488,33.668,-1.506,33.646,-1.571,33.667,-1.604,33.675,-1.638,33.602,-1.691,33.593,-1.761,33.546,-1.8,33.489,-1.822,33.402,-1.812,33.366,-1.83,33.331,-1.864,33.298,-1.922,33.45,-1.915,33.478,-1.928,33.491,-1.953,33.521,-1.98,33.524,-1.996,33.412,-1.997,33.374,-2.032,33.3,-2.023,33.233,-2.072,33.226,-2.096,33.276,-2.124,33.517,-2.162,33.585,-2.168,33.753,-2.111,33.794,-2.123,33.81,-2.157,33.809,-2.192,33.767,-2.238,33.594,-2.358,33.51,-2.43,33.432,-2.46,33.442,-2.502,33.437,-2.522,33.416,-2.531,33.21,-2.489,33.067,-2.41,32.999,-2.402,32.945,-2.421,32.908,-2.449,32.892,-2.484,32.884,-2.664,32.947,-2.818,32.963,-2.849,32.892,-2.862,32.78,-2.971,32.76,-2.985,32.773,-2.91,32.764,-2.867,32.848,-2.797,32.862,-2.757,32.814,-2.647,32.824,-2.544,32.811,-2.515,32.751,-2.514,32.703,-2.502,32.685,-2.491,32.671,-2.456,32.644,-2.439,32.548,-2.508,32.535,-2.494,32.535,-2.462,32.521,-2.454,32.44,-2.444,32.392,-2.422,32.357,-2.394,32.32,-2.304,32.283,-2.282,32.24,-2.283,32.211,-2.31,32.174,-2.366,32.114,-2.377,32.096,-2.394,32.141,-2.486,32.15,-2.525,32.13,-2.537,32.031,-2.516,31.986,-2.547,31.969,-2.608,31.979,-2.677,32.003,-2.757,31.971,-2.752,31.918,-2.674,31.898,-2.681,31.885,-2.7,31.848,-2.705,31.819,-2.798,31.801,-2.791,31.784,-2.75,31.782,-2.693,31.815,-2.621,31.77,-2.597,31.743,-2.567,31.735,-2.53,31.786,-2.465,31.781,-2.44,31.767,-2.399,31.739,-2.357,31.702,-2.328,31.675,-2.277,31.675,-2.08,31.664,-2.016,31.664,-1.932,31.763,-1.492,31.811,-1.419,31.826,-1.359,31.865,-1.092,31.861,-1.044,31.849,-1.017,31.829,-1.013,31.804,-1.022,31.783,-1.01,31.768,-0.936,31.756,-0.888,31.732,-0.837,31.723,-0.79,31.818,-0.568,31.867,-0.481,31.994,-0.321,32.001,-0.255,31.976,-0.212,31.94,-0.18,31.933,-0.158,31.95,-0.132,32.033,-0.09,32.055,-0.057,32.159,-0.04,32.209,0.009,32.258,-0.005,32.287,0.021,32.351,0.015,32.384,0.066,32.413,0.089,32.438,0.093,32.449,0.056,32.486,0.053,32.533,0.07,32.564,0.099,32.588,0.193,32.615,0.235,32.647,0.253,32.681,0.189,32.711,0.179,32.676,0.137,32.671,0.117,32.692,0.108,32.735,0.108,32.779,0.119,32.853,0.169,32.879,0.169,32.895,0.112,32.915,0.096,32.958,0.108,33.021,0.147,33.07,0.193,33.076,0.239,33.137,0.252,33.201,0.388,33.187,0.428,33.245,0.451,33.273,0.463,33.312,0.451,33.364,0.472,33.368,0.453,33.332,0.369,33.354,0.349,33.461,0.334,33.484,0.315,33.485,0.293,33.441,0.271,33.448,0.244,33.483,0.218,33.533,0.207,33.58,0.222,33.64,0.3,33.68,0.316,33.713,0.308,33.728,0.23,33.745,0.208,33.861,0.185,33.945,0.218,33.975,0.213,33.974,0.089,33.982,0.025,34.003,-0.024,34.029,-0.062,34.059,-0.081,34.115,-0.079,34.125,-0.091,34.108,-0.133,34.117,-0.163,34.226,-0.259,34.285,-0.35,34.31,-0.356,34.334,-0.337,34.353,-0.314,34.37,-0.243,34.42,-0.196,34.538,-0.144,34.718,-0.092],[32.054,-2.328,32.06,-2.265,32.045,-2.243,31.975,-2.266,31.9,-2.284,31.876,-2.264,31.84,-2.216,31.818,-2.192,31.791,-2.234,31.783,-2.321,31.843,-2.381,31.848,-2.454,31.9,-2.443,31.917,-2.372,31.966,-2.366,32.038,-2.351,32.054,-2.328],[32.298,-0.392,32.319,-0.337,32.269,-0.29,32.194,-0.332,32.179,-0.311,32.182,-0.235,32.147,-0.232,32.086,-0.231,32.065,-0.254,32.081,-0.29,32.104,-0.322,32.13,-0.337,32.208,-0.363,32.205,-0.397,32.166,-0.442,32.145,-0.487,32.132,-0.525,32.272,-0.472,32.298,-0.392],[33.175,-2.159,33.183,-2.116,33.14,-2.069,33.12,-2.011,33.097,-1.965,33.027,-1.973,32.972,-1.973,32.949,-1.902,32.884,-1.904,32.83,-1.937,32.847,-1.979,32.851,-2.061,32.899,-2.085,32.989,-2.105,33.059,-2.135,33.125,-2.158,33.175,-2.159],[33.372,0.216,33.388,0.272,33.318,0.298,33.224,0.277,33.221,0.233,33.218,0.19,33.18,0.175,33.184,0.15,33.264,0.144,33.273,0.121,33.339,0.08,33.325,0.142,33.317,0.184,33.372,0.216]]]},{"n":"Baikal","z":1,"b":[103.713,51.482,109.955,55.779],"p":[[[109.864,55.539,109.857,55.446,109.825,55.402,109.791,55.33,109.781,55.28,109.75,55.185,109.744,55.14,109.768,55.083,109.765,55.04,109.716,54.932,109.702,54.902,109.684,54.85,109.646,54.807,109.633,54.772,109.607,54.676,109.544,54.629,109.523,54.559,109.49,54.38,109.496,54.262,109.554,54.174,109.556,54.154,109.538,54.116,109.5,54.061,109.426,53.998,109.244,53.869,109.211,53.828,109.198,53.793,109.204,53.737,109.204,53.679,109.199,53.66,109.178,53.638,109.03,53.639,109.009,53.668,109.034,53.76,109.061,53.804,109.061,53.831,109.033,53.858,108.994,53.869,108.944,53.862,108.891,53.842,108.836,53.807,108.619,53.582,108.532,53.513,108.545,53.508,108.646,53.548,108.732,53.571,108.842,53.587,108.921,53.566,108.968,53.508,108.989,53.45,108.982,53.393,108.931,53.344,108.768,53.281,108.729,53.278,108.703,53.283,108.676,53.304,108.661,53.302,108.392,53.163,108.302,53.109,108.297,53.093,108.33,53.056,108.335,53.038,108.288,52.989,108.234,52.946,108.007,52.837,107.948,52.78,107.763,52.714,107.674,52.701,107.579,52.701,107.387,52.648,107.099,52.542,106.938,52.475,106.905,52.447,106.843,52.356,106.806,52.329,106.752,52.317,106.693,52.338,106.599,52.371,106.497,52.383,106.39,52.355,106.314,52.323,106.269,52.282,106.258,52.231,106.281,52.169,106.261,52.103,106.187,51.956,106.134,51.916,106.065,51.863,105.967,51.774,105.798,51.701,105.435,51.614,105.065,51.557,104.903,51.522,104.826,51.49,104.655,51.482,104.39,51.5,104.156,51.535,103.817,51.634,103.745,51.669,103.713,51.694,103.723,51.711,103.743,51.722,103.791,51.732,104.133,51.769,104.343,51.783,104.537,51.784,104.663,51.806,104.721,51.849,104.812,51.87,104.867,51.87,104.935,51.87,105.053,51.882,105.165,51.906,105.243,51.938,105.286,51.979,105.345,52.009,105.419,52.03,105.57,52.108,105.692,52.216,105.86,52.387,106.06,52.512,106.292,52.592,106.452,52.656,106.537,52.706,106.579,52.742,106.702,52.832,106.757,52.869,106.783,52.906,106.881,52.973,106.897,53.003,106.873,53.029,106.774,53.022,106.78,53.045,106.852,53.103,107.072,53.241,107.232,53.316,107.535,53.439,107.547,53.453,107.551,53.502,107.576,53.548,107.627,53.611,107.789,53.711,108.062,53.849,108.224,53.976,108.275,54.036,108.298,54.078,108.362,54.173,108.408,54.21,108.473,54.277,108.519,54.35,108.546,54.429,108.593,54.492,108.723,54.601,108.786,54.672,108.827,54.739,108.847,54.8,108.882,54.853,108.933,54.898,108.944,54.931,108.917,54.951,108.953,54.983,109.053,55.028,109.11,55.099,109.171,55.194,109.198,55.261,109.213,55.405,109.191,55.464,109.193,55.536,109.239,55.575,109.313,55.608,109.507,55.731,109.576,55.771,109.667,55.779,109.76,55.774,109.847,55.755,109.955,55.672,109.861,55.575,109.864,55.539],[107.767,53.267,107.795,53.393,107.744,53.399,107.584,53.283,107.353,53.21,107.091,53.133,106.995,53.107,106.96,53.078,106.963,53.023,107.314,53.068,107.438,53.103,107.639,53.22,107.767,53.267]]]},{"n":"Winnipeg","z":1,"b":[-99.24,50.398,-96.288,54.413],"p":[[[-96.373,50.729,-96.359,50.691,-96.294,50.624,-96.392,50.629,-96.452,50.656,-96.542,50.728,-96.566,50.695,-96.588,50.664,-96.594,50.599,-96.608,50.552,-96.576,50.468,-96.596,50.442,-96.683,50.398,-96.761,50.398,-96.866,50.418,-96.931,50.461,-96.962,50.57,-96.976,50.644,-96.983,50.683,-96.956,50.797,-96.954,50.844,-96.967,50.927,-96.938,51.06,-96.927,51.082,-96.841,51.128,-96.832,51.157,-96.774,51.221,-96.693,51.255,-96.627,51.324,-96.631,51.343,-96.708,51.364,-96.725,51.366,-96.908,51.206,-96.937,51.193,-96.979,51.204,-96.982,51.251,-96.914,51.331,-96.742,51.554,-96.736,51.593,-96.751,51.612,-96.785,51.611,-96.797,51.626,-96.817,51.676,-96.826,51.71,-96.844,51.722,-96.881,51.724,-97.065,51.665,-97.106,51.642,-97.127,51.608,-97.124,51.536,-97.141,51.502,-97.22,51.45,-97.253,51.444,-97.276,51.491,-97.289,51.593,-97.277,51.685,-97.24,51.766,-97.25,51.865,-97.307,51.981,-97.357,52.04,-97.446,52.027,-97.496,52.001,-97.55,51.925,-97.573,51.924,-97.6,51.988,-97.526,52.061,-97.495,52.108,-97.497,52.133,-97.531,52.137,-97.68,52.138,-97.743,52.135,-97.774,52.109,-97.729,52.088,-97.735,52.045,-97.792,51.914,-97.823,51.889,-97.963,51.927,-98.036,51.981,-98.045,52.052,-98.071,52.1,-98.139,52.162,-98.143,52.211,-98.188,52.28,-98.429,52.382,-98.454,52.416,-98.469,52.486,-98.553,52.556,-98.809,52.725,-98.826,52.767,-98.82,52.783,-98.896,52.856,-98.904,52.901,-98.895,52.916,-98.773,52.965,-98.501,53.018,-98.46,53.044,-98.525,53.065,-98.696,53.082,-98.796,53.076,-98.903,53.06,-98.99,53.048,-99.05,53.073,-99.186,53.175,-99.23,53.185,-99.24,53.21,-99.202,53.353,-99.085,53.48,-99.042,53.557,-98.994,53.677,-98.983,53.718,-99.001,53.754,-98.929,53.867,-98.811,53.897,-98.731,53.877,-98.613,53.843,-98.456,53.852,-98.299,53.834,-98.009,53.71,-97.963,53.695,-97.91,53.7,-97.9,53.738,-98.076,53.823,-98.204,53.874,-98.261,53.912,-98.299,53.965,-98.297,54.018,-98.254,54.069,-98.174,54.218,-98.021,54.41,-98.003,54.413,-97.932,54.375,-97.932,54.353,-97.99,54.322,-97.983,54.316,-97.844,54.317,-97.82,54.3,-97.864,54.265,-98.083,54.202,-98.172,54.079,-98.213,54.025,-98.192,53.994,-98.061,53.931,-98.046,53.94,-98.033,53.977,-98.001,54.002,-97.94,54.029,-97.811,54.042,-97.734,54.081,-97.729,54.06,-97.762,54.013,-97.972,53.98,-97.977,53.969,-97.938,53.89,-97.832,53.736,-97.82,53.695,-97.828,53.641,-97.791,53.581,-97.707,53.515,-97.652,53.4,-97.582,53.291,-97.475,53.201,-97.437,53.119,-97.352,53.026,-97.347,53.006,-97.376,52.98,-97.424,52.965,-97.398,52.879,-97.333,52.784,-97.198,52.673,-97.112,52.478,-97.071,52.413,-97.002,52.269,-97.007,52.257,-97.019,52.204,-96.986,52.133,-96.958,52.077,-96.859,51.993,-96.835,51.942,-96.854,51.892,-96.844,51.866,-96.769,51.855,-96.707,51.815,-96.689,51.791,-96.706,51.773,-96.757,51.763,-96.722,51.704,-96.601,51.596,-96.551,51.53,-96.546,51.476,-96.495,51.413,-96.344,51.309,-96.29,51.251,-96.288,51.208,-96.337,51.179,-96.349,51.153,-96.394,51.088,-96.403,51.02,-96.367,50.857,-96.357,50.76,-96.373,50.729],[-97.938,52.318,-97.887,52.358,-97.944,52.531,-97.995,52.542,-98.046,52.46,-98.043,52.398,-97.978,52.315,-97.938,52.318],[-96.632,51.148,-96.598,51.188,-96.709,51.199,-96.737,51.137,-96.782,51.092,-96.842,51.066,-96.845,51.027,-96.762,51.01,-96.68,51.041,-96.632,51.148]]]},{"n":"Great Slave","z":1,"b":[-117.668,60.838,-108.919,62.952],"p":[[[-109.022,62.807,-108.919,62.788,-108.928,62.768,-108.952,62.752,-108.993,62.739,-108.987,62.712,-109.006,62.69,-109.252,62.597,-109.292,62.603,-109.263,62.657,-109.301,62.717,-109.396,62.739,-109.691,62.777,-109.747,62.798,-109.944,62.814,-110.284,62.827,-110.469,62.828,-110.498,62.819,-110.603,62.832,-110.662,62.829,-110.701,62.81,-110.756,62.763,-110.967,62.727,-111.269,62.661,-111.294,62.634,-111.272,62.617,-110.836,62.666,-110.536,62.66,-110.514,62.67,-110.411,62.685,-109.864,62.673,-109.768,62.668,-109.735,62.65,-109.836,62.607,-109.958,62.576,-110.101,62.557,-110.187,62.554,-110.263,62.571,-110.33,62.569,-110.455,62.542,-110.554,62.538,-110.764,62.546,-110.78,62.538,-110.683,62.516,-110.647,62.482,-110.684,62.42,-110.705,62.395,-110.75,62.387,-110.95,62.403,-111.06,62.381,-111.1,62.357,-111.236,62.302,-111.294,62.281,-111.302,62.258,-111.307,62.233,-111.388,62.192,-111.587,62.129,-111.591,62.098,-111.634,62.071,-111.638,62.059,-111.509,62.065,-111.502,62.049,-111.62,61.95,-111.843,61.857,-112.09,61.736,-112.204,61.645,-112.208,61.624,-112.329,61.577,-112.676,61.463,-112.893,61.41,-112.969,61.39,-113.001,61.405,-112.99,61.426,-113.007,61.449,-113.091,61.484,-113.148,61.466,-113.359,61.445,-113.398,61.416,-113.515,61.378,-113.602,61.335,-113.677,61.301,-113.699,61.276,-113.704,61.246,-113.682,61.214,-113.691,61.189,-113.729,61.172,-113.725,61.163,-113.648,61.157,-113.633,61.145,-113.63,61.106,-113.664,61.072,-113.729,61.028,-113.832,60.997,-113.973,60.979,-114.086,60.982,-114.173,61.006,-114.338,60.988,-114.581,60.929,-114.727,60.907,-114.773,60.923,-114.913,60.91,-115.333,60.845,-115.473,60.838,-115.744,60.857,-115.877,60.848,-116.057,60.868,-116.425,60.953,-116.502,60.994,-116.528,61.029,-116.612,61.017,-116.664,61.021,-116.719,61.039,-117.089,61.051,-117.226,61.075,-117.278,61.097,-117.438,61.195,-117.505,61.24,-117.557,61.259,-117.629,61.285,-117.668,61.308,-117.627,61.307,-117.473,61.268,-117.206,61.19,-117.067,61.15,-116.98,61.169,-116.794,61.206,-116.738,61.224,-116.726,61.241,-116.797,61.298,-116.784,61.321,-116.763,61.329,-116.594,61.324,-116.511,61.309,-116.45,61.285,-116.367,61.273,-116.199,61.266,-116.121,61.228,-115.95,61.183,-115.939,61.193,-115.955,61.218,-115.988,61.235,-115.98,61.245,-115.87,61.284,-115.858,61.302,-115.905,61.327,-116.011,61.359,-116.039,61.381,-115.956,61.406,-115.918,61.423,-115.881,61.409,-115.827,61.411,-115.717,61.44,-115.711,61.45,-115.744,61.468,-115.752,61.481,-115.745,61.511,-115.708,61.568,-115.679,61.607,-115.658,61.641,-115.571,61.687,-115.566,61.723,-115.557,61.74,-115.508,61.757,-115.337,61.778,-115.215,61.773,-115.197,61.756,-115.19,61.742,-115.162,61.736,-114.898,61.767,-114.845,61.768,-114.721,61.812,-114.648,61.857,-114.636,61.869,-114.651,61.923,-114.667,61.935,-114.724,61.937,-114.762,61.956,-114.781,61.994,-114.848,62.032,-114.942,62.055,-115.033,62.099,-115.053,62.119,-115.038,62.145,-115.039,62.154,-115.045,62.174,-115.167,62.244,-115.208,62.247,-115.29,62.237,-115.307,62.254,-115.259,62.299,-115.254,62.329,-115.291,62.345,-115.309,62.371,-115.307,62.408,-115.294,62.44,-115.271,62.467,-115.289,62.482,-115.37,62.48,-115.701,62.485,-115.756,62.539,-115.792,62.565,-115.809,62.599,-115.867,62.633,-115.992,62.682,-116.038,62.726,-116.004,62.766,-115.913,62.764,-115.763,62.72,-115.738,62.696,-115.754,62.667,-115.724,62.66,-115.672,62.664,-115.289,62.607,-115.246,62.591,-115.217,62.569,-115.202,62.543,-115.165,62.519,-115.108,62.498,-114.902,62.466,-114.77,62.412,-114.714,62.376,-114.625,62.359,-114.437,62.373,-114.373,62.388,-114.351,62.408,-114.322,62.416,-114.284,62.413,-114.227,62.385,-114.225,62.373,-114.247,62.326,-114.224,62.312,-114.155,62.279,-114.136,62.277,-114.085,62.283,-114.023,62.268,-113.892,62.206,-113.861,62.176,-113.756,62.103,-113.574,62.019,-113.493,62.015,-113.309,62.005,-113.114,62.029,-113.037,62.019,-113.005,62.044,-112.923,62.073,-112.906,62.07,-112.892,62.037,-112.529,62.074,-112.302,62.134,-112.082,62.23,-111.962,62.29,-111.78,62.368,-111.7,62.42,-111.643,62.496,-111.593,62.541,-111.551,62.556,-111.512,62.595,-111.478,62.661,-111.393,62.722,-111.259,62.779,-111.081,62.835,-110.673,62.927,-110.525,62.945,-110.056,62.952,-109.983,62.944,-109.89,62.924,-109.705,62.911,-109.638,62.895,-109.348,62.867,-109.185,62.847,-109.046,62.829,-109.022,62.807],[-111.745,62.081,-111.668,62.123,-111.706,62.131,-111.828,62.113,-111.985,62.03,-112.071,61.974,-112.306,61.916,-112.524,61.875,-112.696,61.868,-112.875,61.84,-112.971,61.832,-113.011,61.809,-112.92,61.762,-112.754,61.764,-112.741,61.721,-112.73,61.65,-112.69,61.605,-112.596,61.584,-112.455,61.605,-112.331,61.65,-112.248,61.756,-112.137,61.837,-111.927,61.916,-111.787,62.002,-111.745,62.081]]]},{"n":"Ontario","z":1,"b":[-79.774,43.198,-75.776,44.495],"p":[[[-75.787,44.484,-75.776,44.459,-75.791,44.43,-75.832,44.397,-76.238,44.183,-76.352,44.106,-76.31,44.063,-76.306,44.043,-76.281,44.046,-76.198,44.083,-76.173,44.079,-76.161,44.069,-76.154,44.045,-76.192,44.003,-76.175,43.994,-76.102,44.003,-76.071,43.998,-76.099,43.967,-76.136,43.947,-76.143,43.927,-76.198,43.874,-76.223,43.864,-76.236,43.871,-76.233,43.895,-76.285,43.869,-76.288,43.849,-76.258,43.829,-76.235,43.799,-76.218,43.758,-76.21,43.67,-76.199,43.653,-76.195,43.645,-76.2,43.635,-76.205,43.614,-76.194,43.596,-76.203,43.575,-76.232,43.55,-76.29,43.537,-76.376,43.535,-76.473,43.507,-76.58,43.454,-76.653,43.405,-76.693,43.361,-76.722,43.343,-76.741,43.35,-76.885,43.314,-76.937,43.29,-76.95,43.268,-76.963,43.264,-76.976,43.277,-77.079,43.288,-77.271,43.295,-77.421,43.288,-77.529,43.267,-77.625,43.279,-77.707,43.323,-77.838,43.357,-78.175,43.395,-78.457,43.388,-78.689,43.361,-79.053,43.285,-79.213,43.242,-79.291,43.207,-79.338,43.198,-79.401,43.209,-79.528,43.21,-79.638,43.235,-79.728,43.253,-79.773,43.301,-79.774,43.32,-79.656,43.453,-79.61,43.513,-79.596,43.552,-79.569,43.578,-79.499,43.612,-79.48,43.636,-79.446,43.647,-79.396,43.645,-79.303,43.69,-79.165,43.78,-79.061,43.835,-78.989,43.855,-78.793,43.876,-78.669,43.902,-78.581,43.912,-78.528,43.905,-78.446,43.916,-78.335,43.944,-78.186,43.966,-78,43.979,-77.881,43.994,-77.829,44.011,-77.726,44.009,-77.709,44.033,-77.682,44.045,-77.583,44.038,-77.549,44.025,-77.544,44.014,-77.568,44.008,-77.554,43.99,-77.503,43.963,-77.421,43.954,-77.325,43.972,-77.259,43.964,-77.276,43.952,-77.311,43.952,-77.315,43.947,-77.289,43.923,-77.267,43.922,-77.19,43.948,-77.185,43.938,-77.219,43.9,-77.208,43.876,-77.151,43.866,-77.054,43.883,-76.934,43.93,-76.879,43.943,-76.87,43.965,-76.91,43.965,-77.013,43.957,-77.042,43.943,-77.044,43.952,-77.024,43.987,-76.93,44.038,-76.888,44.068,-76.863,44.1,-76.89,44.099,-76.969,44.066,-77.04,44.049,-77.103,44.046,-77.117,44.079,-77.083,44.148,-77.115,44.173,-77.271,44.139,-77.29,44.127,-77.29,44.118,-77.273,44.111,-77.275,44.104,-77.296,44.098,-77.332,44.104,-77.331,44.119,-77.314,44.143,-77.333,44.153,-77.381,44.146,-77.446,44.131,-77.586,44.084,-77.584,44.094,-77.547,44.124,-77.398,44.161,-77.326,44.178,-77.28,44.176,-77.251,44.189,-77.225,44.189,-77.203,44.175,-77.148,44.181,-77.059,44.205,-77.02,44.197,-77.033,44.156,-77.01,44.153,-76.951,44.186,-76.913,44.198,-76.896,44.188,-77.021,44.133,-77.055,44.106,-77.062,44.091,-77.041,44.071,-76.992,44.074,-76.787,44.182,-76.688,44.218,-76.589,44.238,-76.49,44.244,-76.408,44.262,-76.341,44.295,-76.057,44.367,-75.942,44.411,-75.831,44.49,-75.805,44.495,-75.787,44.484],[-76.688,44.138,-76.64,44.181,-76.643,44.199,-76.761,44.172,-76.799,44.155,-76.809,44.14,-76.787,44.13,-76.731,44.127,-76.688,44.138],[-76.275,44.009,-76.277,43.987,-76.262,43.99,-76.231,44.018,-76.231,44.029,-76.26,44.022,-76.275,44.009],[-76.237,44.233,-76.227,44.254,-76.279,44.247,-76.329,44.239,-76.43,44.227,-76.451,44.202,-76.483,44.179,-76.492,44.162,-76.477,44.153,-76.474,44.141,-76.485,44.128,-76.469,44.125,-76.423,44.122,-76.365,44.156,-76.353,44.176,-76.33,44.208,-76.237,44.233],[-76.081,44.295,-76.075,44.306,-76.119,44.306,-76.145,44.292,-76.15,44.281,-76.143,44.269,-76.111,44.277,-76.081,44.295]]]},{"n":"Great Bear","z":1,"b":[-125.09,64.827,-117.514,67.029],"p":[[[-117.842,66.424,-117.676,66.456,-117.542,66.483,-117.514,66.46,-117.55,66.424,-117.613,66.385,-117.632,66.366,-117.674,66.336,-117.673,66.3,-117.68,66.253,-117.728,66.226,-117.781,66.207,-117.813,66.187,-117.872,66.15,-117.923,66.139,-117.987,66.105,-117.971,66.084,-117.907,66.067,-117.924,66.044,-118.073,66.036,-118.116,66.011,-118.134,65.962,-118.199,65.905,-118.209,65.856,-118.157,65.831,-118.064,65.831,-117.874,65.847,-117.846,65.84,-117.901,65.8,-117.903,65.764,-117.943,65.723,-118.034,65.693,-118.148,65.682,-118.264,65.67,-118.379,65.664,-118.387,65.68,-118.453,65.683,-118.569,65.644,-118.715,65.694,-118.834,65.755,-118.944,65.784,-119.276,65.796,-119.504,65.793,-119.619,65.782,-119.705,65.752,-119.698,65.714,-119.892,65.641,-119.974,65.617,-119.988,65.586,-119.762,65.493,-119.541,65.408,-119.509,65.368,-119.556,65.344,-119.717,65.342,-119.885,65.325,-119.99,65.267,-120.095,65.203,-120.159,65.176,-120.466,65.073,-120.861,64.956,-120.952,64.935,-121.073,64.9,-121.133,64.861,-121.23,64.827,-121.367,64.834,-121.471,64.879,-121.48,64.933,-121.397,64.969,-121.32,64.974,-121.214,64.993,-121.089,65.004,-121.003,65.038,-120.991,65.082,-120.55,65.289,-120.406,65.411,-120.368,65.509,-120.383,65.557,-120.466,65.585,-120.663,65.605,-120.978,65.553,-121.182,65.496,-121.368,65.435,-121.548,65.364,-121.546,65.342,-121.569,65.297,-121.606,65.229,-121.612,65.175,-121.616,65.128,-121.615,65.093,-121.663,65.074,-121.727,65.061,-121.77,65.037,-121.929,64.996,-122.04,64.961,-122.558,65.023,-122.891,65.068,-123.371,65.114,-123.446,65.116,-123.472,65.152,-123.444,65.185,-123.344,65.2,-123.188,65.216,-122.853,65.278,-122.767,65.342,-122.769,65.409,-122.887,65.487,-122.786,65.531,-122.589,65.588,-122.374,65.657,-122.206,65.753,-122.164,65.817,-122.531,65.944,-122.549,65.986,-122.387,66.007,-122.067,65.977,-121.772,65.948,-121.475,65.957,-121.293,66.001,-121.249,66.054,-121.278,66.092,-121.533,66.122,-121.695,66.178,-122.11,66.234,-122.436,66.251,-122.745,66.251,-123.016,66.255,-123.02,66.221,-122.993,66.173,-123.04,66.126,-123.092,66.1,-123.098,66.073,-123.116,66.054,-123.154,66.064,-123.198,66.104,-123.396,66.135,-123.569,66.139,-123.77,66.125,-124.051,66.13,-124.261,66.157,-124.499,66.153,-124.59,66.11,-124.574,66.056,-124.59,66.016,-124.642,66.007,-124.676,66.025,-124.732,66.04,-124.785,65.995,-124.817,65.941,-124.898,65.966,-124.963,66.01,-125.02,66.057,-125.09,66.157,-125.063,66.248,-124.806,66.28,-124.59,66.274,-124.525,66.276,-124.404,66.309,-123.56,66.427,-123.177,66.517,-123.146,66.511,-123.143,66.483,-123.088,66.473,-122.987,66.495,-122.844,66.533,-122.529,66.56,-122.523,66.599,-122.411,66.599,-122.344,66.583,-121.98,66.636,-121.606,66.709,-121.528,66.719,-121.455,66.74,-121.233,66.832,-120.692,66.907,-120.593,66.93,-120.223,66.978,-120.103,67.001,-119.878,67.029,-119.785,67.004,-119.755,66.99,-119.671,66.992,-119.443,66.966,-119.211,66.933,-119.097,66.909,-119.252,66.829,-119.568,66.746,-119.762,66.696,-119.993,66.662,-120.084,66.654,-120.095,66.634,-120.13,66.6,-120.209,66.568,-120.33,66.537,-120.34,66.527,-120.314,66.493,-120.325,66.47,-120.411,66.456,-120.478,66.422,-120.445,66.395,-120.23,66.392,-120.016,66.38,-119.962,66.356,-119.768,66.327,-119.722,66.311,-119.592,66.307,-119.521,66.316,-119.489,66.338,-119.105,66.326,-118.756,66.331,-118.605,66.348,-118.337,66.399,-118.247,66.46,-118.192,66.515,-118.004,66.601,-117.838,66.639,-117.726,66.659,-117.696,66.658,-117.683,66.642,-117.699,66.621,-117.691,66.596,-117.734,66.551,-117.841,66.487,-117.864,66.461,-117.862,66.438,-117.842,66.424]]]},{"n":"Saint Clair","z":1,"b":[-83.105,42.275,-82.414,42.697],"p":[[[-83.076,42.275,-83.105,42.286,-83.063,42.323,-83.017,42.347,-82.926,42.375,-82.902,42.394,-82.883,42.442,-82.87,42.519,-82.841,42.568,-82.796,42.589,-82.784,42.613,-82.804,42.639,-82.787,42.665,-82.733,42.69,-82.687,42.697,-82.65,42.687,-82.633,42.661,-82.63,42.595,-82.639,42.574,-82.645,42.558,-82.634,42.542,-82.613,42.525,-82.481,42.5,-82.453,42.495,-82.432,42.498,-82.418,42.472,-82.414,42.411,-82.428,42.367,-82.462,42.339,-82.516,42.322,-82.59,42.315,-82.867,42.335,-82.971,42.327,-83.014,42.312,-83.047,42.297,-83.076,42.275]]]},{"n":"Vänern","z":1.7,"b":[12.313,58.372,14.091,59.39],"p":[[[14.038,59.083,14.066,59.097,14.083,59.1,14.089,59.092,14.091,59.079,14.083,59.064,14.072,59.028,14.056,59.017,14.05,59.012,14.039,59.004,14.012,59.004,13.993,58.986,13.995,58.945,13.987,58.898,13.975,58.865,13.954,58.844,13.924,58.823,13.89,58.8,13.859,58.772,13.83,58.744,13.8,58.722,13.737,58.704,13.631,58.684,13.517,58.663,13.437,58.642,13.358,58.617,13.3,58.561,13.24,58.515,13.195,58.511,13.172,58.512,13.148,58.521,13.137,58.541,13.149,58.559,13.149,58.573,13.148,58.597,13.115,58.608,13.041,58.604,12.952,58.572,12.913,58.546,12.869,58.504,12.798,58.486,12.728,58.452,12.679,58.421,12.65,58.397,12.624,58.382,12.602,58.376,12.58,58.372,12.562,58.373,12.551,58.379,12.554,58.394,12.573,58.419,12.571,58.432,12.554,58.43,12.507,58.397,12.375,58.383,12.335,58.374,12.313,58.395,12.329,58.434,12.488,58.513,12.607,58.617,12.611,58.641,12.563,58.65,12.525,58.661,12.504,58.695,12.501,58.728,12.5,58.764,12.471,58.783,12.448,58.783,12.425,58.801,12.432,58.813,12.459,58.817,12.474,58.84,12.472,58.877,12.464,58.921,12.469,58.935,12.482,58.931,12.497,58.92,12.51,58.901,12.52,58.867,12.526,58.846,12.535,58.832,12.557,58.819,12.576,58.826,12.6,58.846,12.618,58.887,12.647,58.931,12.676,58.987,12.708,59.036,12.746,59.065,12.784,59.076,12.818,59.082,12.869,59.073,12.945,59.048,13.041,59.014,13.062,58.977,13.088,58.947,13.122,58.921,13.16,58.905,13.195,58.899,13.236,58.905,13.244,58.93,13.221,58.993,13.177,59.097,13.123,59.198,13.103,59.255,13.103,59.274,13.108,59.284,13.118,59.296,13.13,59.319,13.143,59.342,13.155,59.353,13.173,59.348,13.191,59.325,13.218,59.311,13.246,59.31,13.252,59.315,13.251,59.325,13.249,59.336,13.251,59.345,13.261,59.352,13.278,59.357,13.316,59.361,13.378,59.364,13.461,59.37,13.548,59.381,13.628,59.39,13.68,59.39,13.72,59.378,13.738,59.359,13.744,59.341,13.752,59.331,13.766,59.331,13.8,59.337,13.83,59.341,13.867,59.335,13.904,59.328,13.933,59.33,13.962,59.334,13.997,59.329,14.028,59.32,14.046,59.314,14.052,59.305,14.056,59.29,14.054,59.268,14.047,59.241,14.027,59.204,13.994,59.157,13.946,59.113,13.931,59.09,13.945,59.067,13.973,59.066,14.005,59.068,14.038,59.083]]]},{"n":"Okeechobee","z":1.7,"b":[-81.081,26.691,-80.621,27.182],"p":[[[-80.629,26.887,-80.668,26.826,-80.711,26.742,-80.743,26.697,-80.784,26.691,-80.827,26.707,-80.858,26.723,-80.896,26.752,-80.932,26.81,-80.979,26.856,-81.027,26.876,-81.071,26.917,-81.081,26.956,-81.055,26.991,-80.997,27.033,-80.942,27.077,-80.87,27.147,-80.803,27.182,-80.759,27.182,-80.711,27.133,-80.69,27.11,-80.671,27.087,-80.654,27.054,-80.639,27.008,-80.621,26.949,-80.629,26.887]]]},{"n":"Nicaragua","z":1.7,"b":[-85.914,11.035,-84.769,12.109],"p":[[[-84.814,11.26,-84.792,11.206,-84.781,11.157,-84.769,11.118,-84.803,11.065,-84.842,11.049,-84.897,11.035,-84.94,11.052,-85.005,11.057,-85.11,11.081,-85.26,11.123,-85.411,11.174,-85.517,11.216,-85.584,11.25,-85.64,11.283,-85.684,11.317,-85.719,11.353,-85.754,11.406,-85.798,11.483,-85.834,11.563,-85.854,11.626,-85.864,11.664,-85.877,11.683,-85.89,11.691,-85.899,11.702,-85.903,11.721,-85.901,11.745,-85.901,11.795,-85.908,11.874,-85.914,11.953,-85.912,12.003,-85.894,12.035,-85.871,12.098,-85.835,12.109,-85.798,12.107,-85.751,12.094,-85.676,12.064,-85.588,12.016,-85.513,11.959,-85.441,11.902,-85.362,11.855,-85.259,11.783,-85.132,11.663,-85.009,11.527,-84.916,11.414,-84.857,11.329,-84.814,11.26]]]},{"n":"Tana","z":1.7,"b":[37.002,11.632,37.592,12.303],"p":[[[37.286,11.794,37.253,11.82,37.211,11.822,37.169,11.817,37.146,11.833,37.113,11.855,37.096,11.84,37.078,11.829,37.058,11.834,37.04,11.852,37.024,11.88,37.002,11.907,37.006,11.972,37.008,12.029,37.017,12.087,37.028,12.165,37.048,12.21,37.072,12.243,37.096,12.266,37.126,12.27,37.164,12.26,37.195,12.231,37.219,12.23,37.269,12.224,37.333,12.272,37.377,12.288,37.413,12.291,37.46,12.303,37.496,12.286,37.529,12.259,37.555,12.223,37.566,12.164,37.592,12.117,37.585,12.084,37.583,12.049,37.577,12.018,37.56,11.986,37.55,11.942,37.513,11.91,37.501,11.876,37.496,11.835,37.44,11.753,37.387,11.632,37.361,11.635,37.342,11.632,37.327,11.636,37.32,11.648,37.301,11.668,37.322,11.709,37.309,11.741,37.289,11.771,37.286,11.794]]]},{"n":"Titicaca","z":1.7,"b":[-70.021,-16.589,-68.586,-15.264],"p":[[[-69.366,-16.223,-69.4,-16.2,-69.436,-16.187,-69.483,-16.182,-69.487,-16.161,-69.449,-16.112,-69.439,-16.059,-69.447,-16.029,-69.489,-16.012,-69.558,-15.991,-69.627,-15.967,-69.668,-15.943,-69.691,-15.922,-69.717,-15.907,-69.748,-15.887,-69.782,-15.855,-69.812,-15.828,-69.834,-15.819,-69.846,-15.827,-69.854,-15.841,-69.852,-15.861,-69.842,-15.884,-69.833,-15.905,-69.835,-15.916,-69.845,-15.92,-69.859,-15.92,-69.876,-15.914,-69.895,-15.9,-69.923,-15.88,-69.961,-15.86,-69.994,-15.842,-70.011,-15.828,-70.016,-15.807,-70.021,-15.771,-70.017,-15.733,-70.002,-15.706,-69.979,-15.68,-69.956,-15.646,-69.921,-15.634,-69.866,-15.66,-69.818,-15.692,-69.798,-15.693,-69.818,-15.638,-69.869,-15.533,-69.929,-15.427,-69.938,-15.399,-69.914,-15.374,-69.855,-15.344,-69.844,-15.324,-69.833,-15.31,-69.799,-15.299,-69.761,-15.287,-69.742,-15.269,-69.687,-15.264,-69.625,-15.344,-69.541,-15.372,-69.449,-15.474,-69.405,-15.491,-69.361,-15.511,-69.299,-15.621,-69.27,-15.628,-69.224,-15.66,-69.19,-15.679,-69.17,-15.697,-69.147,-15.725,-69.103,-15.765,-69.039,-15.796,-69.014,-15.848,-69.014,-15.91,-68.995,-15.926,-68.904,-15.943,-68.831,-15.948,-68.796,-15.972,-68.809,-15.994,-68.826,-16.017,-68.853,-16.049,-68.889,-16.084,-68.871,-16.136,-68.829,-16.19,-68.764,-16.202,-68.694,-16.219,-68.635,-16.221,-68.593,-16.239,-68.594,-16.272,-68.586,-16.288,-68.588,-16.302,-68.612,-16.312,-68.655,-16.318,-68.693,-16.326,-68.705,-16.339,-68.712,-16.359,-68.739,-16.383,-68.787,-16.407,-68.86,-16.415,-68.906,-16.424,-68.928,-16.448,-68.897,-16.465,-68.868,-16.5,-68.858,-16.524,-68.856,-16.547,-68.862,-16.569,-68.871,-16.589,-68.904,-16.589,-68.942,-16.582,-69.006,-16.551,-69.049,-16.525,-69.076,-16.521,-69.098,-16.483,-69.052,-16.417,-69.039,-16.364,-69.043,-16.318,-69.044,-16.281,-69.005,-16.252,-68.946,-16.223,-68.905,-16.241,-68.885,-16.241,-68.891,-16.206,-68.947,-16.176,-69.015,-16.111,-69.077,-16.081,-69.103,-16.087,-69.107,-16.132,-69.121,-16.178,-69.136,-16.232,-69.178,-16.25,-69.222,-16.254,-69.269,-16.251,-69.324,-16.243,-69.366,-16.223]]]},{"n":"Cedar","z":1.7,"b":[-100.891,53.052,-99.33,54.252],"p":[[[-99.349,53.199,-99.33,53.159,-99.369,53.15,-99.389,53.131,-99.396,53.1,-99.395,53.085,-99.403,53.074,-99.416,53.065,-99.436,53.058,-99.459,53.053,-99.487,53.052,-99.518,53.057,-99.539,53.067,-99.54,53.083,-99.538,53.097,-99.552,53.105,-99.569,53.112,-99.579,53.123,-99.587,53.141,-99.605,53.163,-99.624,53.18,-99.638,53.188,-99.644,53.183,-99.645,53.17,-99.647,53.155,-99.657,53.145,-99.685,53.14,-99.73,53.14,-99.798,53.137,-99.876,53.102,-99.972,53.098,-100.048,53.117,-100.125,53.131,-100.2,53.152,-100.274,53.185,-100.346,53.231,-100.412,53.27,-100.47,53.289,-100.542,53.307,-100.646,53.347,-100.709,53.375,-100.771,53.402,-100.785,53.409,-100.842,53.426,-100.876,53.449,-100.891,53.471,-100.881,53.488,-100.855,53.501,-100.779,53.504,-100.707,53.5,-100.641,53.495,-100.604,53.498,-100.593,53.507,-100.592,53.519,-100.606,53.535,-100.632,53.554,-100.634,53.569,-100.591,53.572,-100.51,53.577,-100.39,53.583,-100.323,53.593,-100.313,53.64,-100.324,53.669,-100.352,53.712,-100.383,53.758,-100.432,53.79,-100.413,53.822,-100.33,53.865,-100.347,53.898,-100.393,53.892,-100.474,53.896,-100.498,53.905,-100.506,53.917,-100.483,53.95,-100.387,53.954,-100.331,53.958,-100.303,53.973,-100.3,53.99,-100.303,54.008,-100.306,54.031,-100.302,54.061,-100.306,54.098,-100.328,54.14,-100.351,54.177,-100.358,54.202,-100.35,54.215,-100.335,54.222,-100.31,54.221,-100.278,54.213,-100.244,54.211,-100.216,54.222,-100.191,54.24,-100.163,54.252,-100.122,54.246,-100.064,54.219,-99.994,54.192,-99.927,54.188,-99.882,54.192,-99.869,54.186,-99.928,54.154,-100.056,54.097,-100.18,54.042,-100.226,54.015,-100.209,54.008,-100.178,54.004,-100.14,53.993,-100.105,53.971,-100.094,53.941,-100.113,53.911,-100.14,53.883,-100.151,53.861,-100.141,53.846,-100.118,53.837,-100.06,53.847,-99.963,53.876,-99.845,53.927,-99.737,53.989,-99.66,54.034,-99.617,54.043,-99.604,54.027,-99.602,54.011,-99.617,53.989,-99.649,53.958,-99.69,53.918,-99.729,53.87,-99.776,53.835,-99.833,53.827,-99.883,53.831,-99.908,53.827,-99.914,53.81,-99.915,53.785,-99.926,53.76,-99.955,53.747,-99.995,53.735,-100.033,53.714,-100.076,53.694,-100.125,53.686,-100.17,53.679,-100.201,53.664,-100.215,53.646,-100.219,53.632,-100.185,53.611,-100.103,53.576,-100.019,53.533,-99.976,53.491,-99.96,53.456,-99.94,53.43,-99.929,53.407,-99.938,53.381,-99.948,53.355,-99.94,53.332,-99.925,53.311,-99.914,53.288,-99.906,53.268,-99.894,53.255,-99.872,53.248,-99.839,53.245,-99.799,53.253,-99.759,53.273,-99.693,53.299,-99.589,53.32,-99.491,53.333,-99.437,53.337,-99.421,53.329,-99.411,53.311,-99.407,53.285,-99.405,53.257,-99.375,53.223,-99.349,53.199]]]},{"n":"Onega","z":1.7,"b":[33.941,60.867,36.428,62.916],"p":[[[36.419,61.392,36.428,61.373,36.425,61.349,36.408,61.319,36.396,61.302,36.383,61.285,36.38,61.281,36.374,61.271,36.371,61.257,36.378,61.242,36.388,61.225,36.395,61.206,36.387,61.184,36.364,61.16,36.325,61.135,36.279,61.114,36.224,61.096,36.163,61.083,36.099,61.063,36.037,61.03,35.976,60.994,35.915,60.967,35.88,60.948,35.884,60.931,35.904,60.917,35.91,60.902,35.899,60.89,35.88,60.878,35.855,60.871,35.829,60.867,35.801,60.872,35.772,60.886,35.719,60.905,35.635,60.918,35.551,60.938,35.499,60.97,35.473,61.002,35.45,61.023,35.426,61.033,35.4,61.04,35.363,61.038,35.316,61.028,35.255,61.015,35.184,61.005,35.133,61.007,35.117,61.022,35.093,61.045,35.022,61.064,34.942,61.07,34.896,61.047,34.869,61.027,34.846,61.027,34.829,61.039,34.815,61.055,34.802,61.074,34.796,61.096,34.797,61.123,34.804,61.15,34.819,61.174,34.838,61.194,34.887,61.206,34.974,61.212,35.075,61.207,35.157,61.194,35.207,61.177,35.231,61.165,35.208,61.159,35.143,61.156,35.078,61.151,35.053,61.141,35.062,61.126,35.08,61.108,35.108,61.092,35.142,61.077,35.192,61.069,35.259,61.067,35.345,61.073,35.441,61.063,35.527,61.023,35.583,61.034,35.615,61.076,35.632,61.108,35.629,61.15,35.611,61.197,35.578,61.246,35.537,61.292,35.495,61.329,35.457,61.356,35.398,61.386,35.304,61.43,35.154,61.485,34.962,61.543,34.772,61.601,34.63,61.654,34.531,61.704,34.448,61.753,34.388,61.796,34.345,61.831,34.323,61.854,34.315,61.869,34.336,61.864,34.385,61.843,34.445,61.819,34.494,61.808,34.531,61.812,34.562,61.824,34.583,61.846,34.594,61.874,34.594,61.906,34.586,61.938,34.566,61.969,34.537,61.999,34.477,62.039,34.381,62.093,34.294,62.142,34.255,62.169,34.255,62.186,34.262,62.21,34.265,62.238,34.257,62.262,34.223,62.294,34.165,62.339,34.098,62.379,34.042,62.397,34.003,62.401,33.978,62.408,33.962,62.425,33.947,62.453,33.941,62.482,33.944,62.5,33.959,62.51,33.979,62.516,34.003,62.516,34.027,62.512,34.054,62.495,34.084,62.463,34.137,62.414,34.218,62.352,34.292,62.297,34.328,62.264,34.329,62.246,34.321,62.227,34.343,62.199,34.417,62.158,34.514,62.12,34.591,62.096,34.636,62.095,34.653,62.109,34.644,62.136,34.632,62.182,34.647,62.213,34.658,62.245,34.615,62.304,34.609,62.339,34.633,62.348,34.682,62.339,34.696,62.365,34.66,62.406,34.592,62.458,34.547,62.498,34.527,62.524,34.52,62.542,34.532,62.546,34.557,62.54,34.595,62.519,34.641,62.489,34.696,62.443,34.758,62.383,34.811,62.332,34.841,62.31,34.852,62.294,34.859,62.26,34.871,62.219,34.892,62.187,34.928,62.16,34.976,62.118,35.029,62.078,35.075,62.054,35.115,62.054,35.15,62.071,35.165,62.081,35.168,62.097,35.144,62.127,35.094,62.172,35.052,62.212,35.046,62.229,35.096,62.218,35.188,62.19,35.284,62.162,35.341,62.149,35.387,62.158,35.455,62.181,35.518,62.21,35.554,62.236,35.564,62.26,35.567,62.287,35.559,62.319,35.54,62.354,35.498,62.404,35.433,62.469,35.352,62.523,35.271,62.545,35.201,62.55,35.147,62.559,35.093,62.58,35.024,62.613,34.967,62.639,34.943,62.643,34.868,62.611,34.839,62.591,34.776,62.622,34.739,62.621,34.704,62.643,34.658,62.68,34.621,62.712,34.583,62.756,34.536,62.818,34.48,62.874,34.489,62.904,34.533,62.916,34.605,62.873,34.703,62.844,34.788,62.831,34.855,62.826,34.91,62.815,34.958,62.797,35.006,62.773,35.083,62.741,35.202,62.705,35.326,62.666,35.416,62.627,35.464,62.604,35.488,62.606,35.495,62.619,35.504,62.625,35.519,62.617,35.544,62.599,35.585,62.576,35.641,62.556,35.698,62.538,35.74,62.52,35.771,62.496,35.803,62.461,35.822,62.412,35.823,62.352,35.8,62.281,35.764,62.205,35.73,62.139,35.674,62.079,35.689,62.04,35.75,61.999,35.818,61.951,35.843,61.902,35.871,61.855,35.926,61.806,35.985,61.756,36.026,61.704,36.061,61.654,36.109,61.611,36.159,61.571,36.2,61.531,36.247,61.493,36.312,61.459,36.371,61.432,36.406,61.411,36.419,61.392],[35.326,61.935,35.347,61.998,35.339,62.033,35.308,62.077,35.252,62.08,35.196,61.988,35.171,61.906,35.16,61.859,35.202,61.839,35.274,61.87,35.326,61.935]]]},{"n":"Great Salt","z":1.7,"b":[-113.014,40.699,-111.951,41.667],"p":[[[-111.974,41.002,-111.951,40.968,-111.961,40.942,-112.053,40.902,-112.097,40.881,-112.19,40.769,-112.267,40.74,-112.332,40.699,-112.37,40.701,-112.394,40.757,-112.436,40.816,-112.446,40.875,-112.478,40.943,-112.493,40.938,-112.508,40.919,-112.522,40.878,-112.546,40.839,-112.58,40.849,-112.632,40.927,-112.69,40.98,-112.728,41.019,-112.751,41.059,-112.768,41.114,-112.788,41.174,-112.814,41.224,-112.846,41.261,-112.881,41.289,-112.918,41.328,-112.961,41.377,-112.985,41.427,-112.997,41.489,-112.998,41.534,-112.995,41.56,-112.997,41.576,-113.007,41.582,-113.014,41.597,-112.999,41.612,-112.966,41.614,-112.909,41.637,-112.844,41.657,-112.8,41.667,-112.77,41.666,-112.742,41.659,-112.719,41.637,-112.707,41.603,-112.734,41.563,-112.738,41.537,-112.708,41.496,-112.666,41.466,-112.604,41.431,-112.554,41.43,-112.527,41.421,-112.512,41.381,-112.489,41.309,-112.458,41.247,-112.437,41.219,-112.422,41.217,-112.406,41.217,-112.39,41.223,-112.377,41.232,-112.366,41.248,-112.381,41.324,-112.388,41.385,-112.402,41.432,-112.39,41.447,-112.374,41.457,-112.33,41.443,-112.29,41.41,-112.243,41.393,-112.171,41.397,-112.092,41.408,-112.064,41.4,-112.063,41.388,-112.076,41.347,-112.114,41.336,-112.179,41.342,-112.241,41.345,-112.27,41.332,-112.287,41.317,-112.304,41.283,-112.284,41.268,-112.243,41.222,-112.212,41.192,-112.181,41.163,-112.152,41.134,-112.083,41.065,-111.974,41.002],[-112.171,40.875,-112.156,40.956,-112.198,41.056,-112.236,41.069,-112.263,41.058,-112.252,41.02,-112.25,40.964,-112.216,40.924,-112.171,40.875]]]},{"n":"Michigan","z":1,"b":[-88.028,41.638,-84.757,46.109],"p":[[[-84.763,45.792,-84.772,45.795,-84.806,45.775,-84.866,45.769,-84.952,45.775,-84.981,45.767,-84.955,45.743,-84.977,45.707,-85.047,45.656,-85.088,45.605,-85.099,45.553,-85.088,45.507,-85.055,45.467,-85.006,45.44,-84.943,45.429,-84.93,45.413,-84.967,45.393,-85.033,45.381,-85.127,45.378,-85.223,45.356,-85.319,45.314,-85.376,45.277,-85.391,45.244,-85.394,45.196,-85.382,45.078,-85.389,45.033,-85.43,44.949,-85.485,44.84,-85.549,44.772,-85.561,44.781,-85.551,44.815,-85.497,44.924,-85.489,44.97,-85.494,44.994,-85.513,44.997,-85.534,44.976,-85.558,44.931,-85.563,44.907,-85.549,44.903,-85.559,44.873,-85.593,44.816,-85.623,44.804,-85.647,44.838,-85.645,44.882,-85.605,44.97,-85.619,44.987,-85.596,45.042,-85.585,45.082,-85.597,45.123,-85.588,45.161,-85.556,45.196,-85.566,45.207,-85.617,45.193,-85.689,45.128,-85.784,45.012,-85.858,44.962,-85.91,44.976,-85.953,44.967,-85.985,44.933,-86.02,44.916,-86.058,44.916,-86.08,44.876,-86.088,44.794,-86.126,44.748,-86.237,44.72,-86.259,44.699,-86.261,44.669,-86.243,44.63,-86.242,44.551,-86.259,44.434,-86.296,44.331,-86.353,44.242,-86.415,44.169,-86.519,44.074,-86.521,44.055,-86.47,43.98,-86.449,43.924,-86.438,43.813,-86.441,43.794,-86.541,43.657,-86.542,43.637,-86.435,43.407,-86.34,43.242,-86.329,43.224,-86.301,43.158,-86.248,43.029,-86.222,42.9,-86.22,42.773,-86.233,42.65,-86.26,42.531,-86.3,42.418,-86.352,42.309,-86.407,42.224,-86.52,42.082,-86.575,41.988,-86.632,41.911,-86.692,41.853,-86.798,41.789,-87.073,41.669,-87.159,41.646,-87.253,41.638,-87.356,41.643,-87.412,41.656,-87.425,41.679,-87.447,41.681,-87.477,41.695,-87.515,41.721,-87.557,41.766,-87.602,41.859,-87.651,42,-87.695,42.084,-87.733,42.112,-87.767,42.154,-87.797,42.211,-87.812,42.341,-87.812,42.543,-87.802,42.68,-87.781,42.753,-87.787,42.809,-87.819,42.848,-87.838,42.896,-87.846,42.953,-87.862,42.992,-87.888,43.012,-87.893,43.041,-87.879,43.078,-87.879,43.103,-87.893,43.117,-87.896,43.137,-87.889,43.165,-87.902,43.245,-87.898,43.294,-87.881,43.347,-87.824,43.443,-87.806,43.49,-87.801,43.538,-87.774,43.599,-87.7,43.724,-87.698,43.752,-87.723,43.809,-87.729,43.851,-87.724,43.907,-87.7,43.977,-87.658,44.059,-87.603,44.125,-87.535,44.174,-87.512,44.23,-87.536,44.293,-87.527,44.386,-87.487,44.509,-87.441,44.601,-87.391,44.662,-87.356,44.72,-87.337,44.776,-87.346,44.801,-87.383,44.827,-87.42,44.866,-87.48,44.874,-87.555,44.849,-87.566,44.85,-87.604,44.847,-87.677,44.79,-87.741,44.692,-87.788,44.652,-87.851,44.624,-87.898,44.593,-87.929,44.559,-87.969,44.552,-88.019,44.571,-88.028,44.618,-87.998,44.692,-87.952,44.764,-87.857,44.88,-87.854,44.908,-87.83,44.942,-87.683,44.993,-87.627,45.032,-87.608,45.083,-87.602,45.157,-87.593,45.178,-87.571,45.187,-87.506,45.249,-87.399,45.365,-87.302,45.484,-87.216,45.607,-87.154,45.677,-87.114,45.692,-87.085,45.73,-87.039,45.845,-87.005,45.897,-86.982,45.917,-86.971,45.905,-86.993,45.842,-86.993,45.79,-86.974,45.723,-86.938,45.7,-86.884,45.721,-86.837,45.765,-86.799,45.832,-86.754,45.859,-86.703,45.848,-86.661,45.852,-86.627,45.873,-86.563,45.883,-86.552,45.857,-86.559,45.806,-86.58,45.782,-86.612,45.784,-86.65,45.757,-86.691,45.701,-86.69,45.657,-86.644,45.625,-86.622,45.623,-86.622,45.652,-86.601,45.682,-86.534,45.736,-86.527,45.753,-86.4,45.801,-86.351,45.831,-86.338,45.866,-86.272,45.937,-86.209,45.959,-86.123,45.969,-86.04,45.963,-85.959,45.941,-85.914,45.941,-85.903,45.964,-85.834,45.98,-85.708,45.988,-85.615,46.02,-85.555,46.078,-85.479,46.108,-85.386,46.109,-85.26,46.088,-85.101,46.046,-84.972,45.989,-84.802,45.874,-84.757,45.863,-84.763,45.792],[-85.492,45.76,-85.539,45.775,-85.573,45.738,-85.608,45.656,-85.612,45.609,-85.586,45.598,-85.552,45.603,-85.509,45.624,-85.492,45.663,-85.501,45.75,-85.492,45.76],[-86.052,45.156,-86.06,45.147,-86.038,45.103,-86.016,45.089,-85.988,45.088,-85.976,45.102,-85.98,45.132,-85.998,45.15,-86.052,45.156],[-86.926,45.415,-86.938,45.426,-86.967,45.368,-86.947,45.346,-86.892,45.342,-86.862,45.352,-86.857,45.394,-86.862,45.409,-86.878,45.414,-86.926,45.415],[-87.049,45.303,-87.088,45.279,-87.122,45.225,-87.166,45.19,-87.221,45.174,-87.259,45.138,-87.279,45.081,-87.316,45.031,-87.396,44.954,-87.403,44.933,-87.398,44.903,-87.382,44.864,-87.355,44.839,-87.316,44.827,-87.28,44.835,-87.188,44.922,-87.179,44.941,-87.191,44.95,-87.179,44.983,-87.141,45.041,-87.114,45.068,-87.096,45.067,-87.078,45.078,-87.059,45.102,-87.062,45.125,-87.086,45.145,-87.084,45.154,-87.057,45.151,-87.047,45.172,-87.054,45.218,-87.042,45.237,-87.01,45.228,-86.992,45.241,-86.986,45.275,-87.005,45.296,-87.049,45.303]]]},{"n":"Superior","z":1,"b":[-92.107,46.436,-84.344,49.001],"p":[[[-84.432,46.522,-84.344,46.531,-84.358,46.505,-84.387,46.502,-84.424,46.491,-84.467,46.464,-84.528,46.436,-84.592,46.443,-84.635,46.482,-84.697,46.491,-84.779,46.468,-84.851,46.469,-84.914,46.494,-85.034,46.506,-85.049,46.524,-85.046,46.543,-85.027,46.563,-85.017,46.611,-85.015,46.686,-84.998,46.74,-84.968,46.771,-85.015,46.783,-85.138,46.778,-85.281,46.752,-85.442,46.707,-85.616,46.691,-85.804,46.704,-85.947,46.702,-86.105,46.679,-86.132,46.687,-86.231,46.656,-86.403,46.588,-86.531,46.524,-86.637,46.453,-86.654,46.452,-86.679,46.468,-86.733,46.49,-86.79,46.491,-86.851,46.47,-86.914,46.483,-86.977,46.53,-87.034,46.543,-87.086,46.523,-87.171,46.514,-87.358,46.524,-87.378,46.54,-87.394,46.59,-87.415,46.612,-87.451,46.627,-87.509,46.683,-87.637,46.827,-87.673,46.844,-87.778,46.888,-87.877,46.908,-88.129,46.935,-88.184,46.955,-88.216,46.952,-88.383,46.874,-88.39,46.857,-88.4,46.841,-88.427,46.814,-88.449,46.802,-88.463,46.814,-88.476,46.837,-88.474,46.886,-88.456,46.963,-88.466,47.013,-88.504,47.035,-88.521,47.063,-88.515,47.096,-88.543,47.122,-88.604,47.141,-88.633,47.167,-88.628,47.199,-88.634,47.22,-88.65,47.232,-88.716,47.207,-88.83,47.146,-88.907,47.091,-88.948,47.043,-89.009,47.016,-89.09,47.009,-89.155,46.989,-89.269,46.918,-89.355,46.882,-89.497,46.855,-89.695,46.839,-89.861,46.793,-89.993,46.718,-90.154,46.654,-90.341,46.602,-90.469,46.584,-90.536,46.599,-90.617,46.631,-90.737,46.689,-90.754,46.684,-90.745,46.672,-90.748,46.662,-90.77,46.645,-90.923,46.603,-90.944,46.619,-90.886,46.677,-90.864,46.721,-90.865,46.761,-90.84,46.81,-90.788,46.868,-90.768,46.908,-90.782,46.929,-90.84,46.962,-90.887,46.965,-90.946,46.954,-91.127,46.868,-91.145,46.874,-91.203,46.888,-91.218,46.886,-91.357,46.811,-91.499,46.766,-91.836,46.702,-91.921,46.694,-91.987,46.699,-92.073,46.736,-92.107,46.762,-92.104,46.789,-92.065,46.819,-91.645,47.05,-91.386,47.215,-90.957,47.524,-90.847,47.585,-90.597,47.688,-90.197,47.786,-90,47.841,-89.829,47.901,-89.72,47.947,-89.672,47.979,-89.634,47.994,-89.605,47.992,-89.547,48.013,-89.45,48.092,-89.438,48.099,-89.345,48.121,-89.315,48.159,-89.297,48.204,-89.252,48.28,-89.221,48.311,-89.214,48.345,-89.229,48.382,-89.225,48.418,-89.201,48.451,-89.086,48.498,-88.775,48.583,-88.764,48.572,-88.818,48.506,-88.876,48.381,-88.897,48.378,-88.916,48.363,-88.933,48.335,-88.888,48.337,-88.783,48.368,-88.734,48.384,-88.722,48.415,-88.706,48.476,-88.683,48.51,-88.654,48.517,-88.635,48.546,-88.625,48.599,-88.602,48.63,-88.565,48.64,-88.55,48.658,-88.552,48.684,-88.542,48.753,-88.535,48.786,-88.503,48.833,-88.458,48.848,-88.395,48.845,-88.352,48.821,-88.329,48.776,-88.323,48.738,-88.334,48.709,-88.394,48.656,-88.567,48.528,-88.585,48.492,-88.569,48.474,-88.559,48.447,-88.539,48.443,-88.501,48.467,-88.484,48.488,-88.489,48.507,-88.476,48.531,-88.444,48.56,-88.418,48.569,-88.4,48.557,-88.369,48.562,-88.325,48.584,-88.248,48.596,-88.233,48.597,-88.138,48.683,-88.109,48.733,-88.109,48.776,-88.147,48.821,-88.224,48.867,-88.247,48.917,-88.249,48.942,-88.264,48.974,-88.248,48.986,-88.2,48.977,-88.161,48.982,-88.128,48.998,-88.07,49.001,-88.023,48.999,-87.966,48.959,-87.761,48.92,-87.685,48.899,-87.592,48.878,-87.568,48.871,-87.552,48.848,-87.517,48.839,-87.464,48.842,-87.407,48.835,-87.311,48.799,-87.224,48.776,-87.085,48.779,-87.009,48.789,-86.888,48.772,-86.82,48.773,-86.752,48.801,-86.693,48.808,-86.643,48.795,-86.621,48.777,-86.581,48.753,-86.459,48.765,-86.432,48.758,-86.396,48.731,-86.39,48.714,-86.371,48.688,-86.325,48.637,-86.314,48.619,-86.283,48.598,-86.257,48.548,-86.237,48.471,-86.216,48.423,-86.191,48.406,-86.181,48.385,-86.159,48.304,-86.104,48.21,-86.022,48.119,-85.914,48.03,-85.778,47.97,-85.615,47.941,-85.47,47.934,-85.342,47.95,-85.174,47.959,-84.964,47.96,-84.855,47.95,-84.847,47.928,-84.867,47.896,-84.915,47.855,-84.92,47.828,-84.922,47.793,-84.961,47.746,-84.981,47.718,-84.975,47.698,-84.986,47.672,-85.014,47.639,-84.998,47.595,-84.936,47.54,-84.86,47.496,-84.771,47.462,-84.716,47.427,-84.693,47.39,-84.668,47.371,-84.641,47.367,-84.616,47.35,-84.594,47.32,-84.618,47.267,-84.688,47.191,-84.72,47.142,-84.712,47.12,-84.772,47.037,-84.775,47.004,-84.75,46.981,-84.656,46.949,-84.625,46.922,-84.6,46.918,-84.582,46.937,-84.545,46.946,-84.488,46.944,-84.435,46.931,-84.385,46.906,-84.367,46.88,-84.382,46.853,-84.408,46.844,-84.485,46.848,-84.528,46.834,-84.553,46.806,-84.561,46.73,-84.54,46.697,-84.528,46.697,-84.51,46.713,-84.488,46.744,-84.464,46.747,-84.438,46.723,-84.466,46.674,-84.548,46.6,-84.569,46.577,-84.568,46.543,-84.501,46.483,-84.475,46.489,-84.459,46.504,-84.432,46.522],[-90.756,46.965,-90.766,46.951,-90.761,46.937,-90.723,46.92,-90.708,46.927,-90.713,46.941,-90.738,46.962,-90.756,46.965],[-90.653,46.828,-90.589,46.855,-90.596,46.869,-90.619,46.87,-90.752,46.814,-90.782,46.791,-90.792,46.772,-90.768,46.77,-90.711,46.783,-90.68,46.798,-90.678,46.813,-90.653,46.828],[-90.547,46.919,-90.535,46.954,-90.543,46.963,-90.576,46.957,-90.65,46.937,-90.652,46.922,-90.58,46.914,-90.547,46.919],[-90.41,47.044,-90.41,47.064,-90.422,47.071,-90.447,47.066,-90.464,47.048,-90.472,47.018,-90.459,47.01,-90.427,47.026,-90.41,47.044],[-88.057,48.837,-88.087,48.808,-88.076,48.741,-88.057,48.717,-88.031,48.718,-87.998,48.743,-87.941,48.757,-87.859,48.758,-87.816,48.768,-87.812,48.785,-87.782,48.795,-87.77,48.811,-87.765,48.841,-87.813,48.859,-87.915,48.865,-87.996,48.857,-88.057,48.837],[-88.911,47.936,-88.625,48.042,-88.593,48.072,-88.597,48.087,-88.581,48.111,-88.485,48.167,-88.499,48.176,-88.588,48.158,-88.756,48.104,-89.147,47.953,-89.193,47.929,-89.2,47.917,-89.169,47.917,-89.169,47.902,-89.199,47.872,-89.195,47.852,-89.157,47.841,-89.082,47.851,-88.97,47.881,-88.93,47.896,-88.924,47.905,-88.96,47.909,-88.955,47.926,-88.911,47.936],[-87.79,47.411,-87.741,47.434,-87.793,47.467,-87.878,47.484,-87.994,47.484,-88.11,47.473,-88.32,47.421,-88.394,47.389,-88.562,47.272,-88.609,47.217,-88.616,47.174,-88.574,47.143,-88.434,47.122,-88.427,47.137,-88.433,47.143,-88.454,47.152,-88.417,47.191,-88.405,47.175,-88.409,47.131,-88.438,47.1,-88.468,47.077,-88.469,47.048,-88.439,47.011,-88.402,47.019,-88.358,47.073,-88.237,47.159,-88.24,47.178,-88.222,47.211,-88.016,47.321,-87.975,47.355,-87.983,47.368,-88.027,47.386,-88.021,47.392,-87.79,47.411],[-87.69,48.75,-87.622,48.757,-87.626,48.78,-87.667,48.821,-87.702,48.841,-87.731,48.84,-87.748,48.835,-87.753,48.824,-87.731,48.794,-87.73,48.778,-87.744,48.764,-87.73,48.755,-87.69,48.75],[-85.606,47.742,-85.596,47.757,-85.641,47.782,-85.765,47.807,-85.827,47.804,-85.883,47.791,-85.931,47.766,-85.951,47.747,-85.942,47.734,-85.859,47.719,-85.666,47.733,-85.606,47.742]]]},{"n":"Huron","z":1,"b":[-84.763,43.017,-79.678,46.563],"p":[[[-84.344,46.531,-84.236,46.557,-84.165,46.563,-84.118,46.539,-84.087,46.507,-84.084,46.473,-84.106,46.43,-84.109,46.393,-84.092,46.361,-84.034,46.344,-83.934,46.342,-83.882,46.333,-83.863,46.306,-83.843,46.302,-83.661,46.29,-83.593,46.278,-83.572,46.26,-83.459,46.238,-83.025,46.181,-82.895,46.188,-82.719,46.207,-82.664,46.176,-82.638,46.172,-82.598,46.177,-82.35,46.193,-82.318,46.163,-82.196,46.134,-82.116,46.132,-82.012,46.129,-81.817,46.115,-81.769,46.104,-81.766,46.071,-81.765,46.058,-81.75,46.057,-81.727,46.073,-81.689,46.079,-81.662,46.093,-81.646,46.115,-81.615,46.119,-81.545,46.09,-81.542,46.075,-81.64,46.04,-81.629,46.025,-81.624,46.017,-81.597,46.012,-81.595,45.996,-81.627,45.978,-81.597,45.967,-81.502,45.993,-81.449,45.994,-81.304,45.974,-81.237,45.974,-81.197,45.991,-81.191,46.006,-81.174,46.016,-81.162,46.012,-81.158,45.992,-81.16,45.955,-81.097,45.942,-80.968,45.952,-80.89,45.951,-80.818,45.951,-80.805,45.942,-80.787,45.919,-80.732,45.877,-80.713,45.858,-80.696,45.816,-80.656,45.79,-80.643,45.767,-80.641,45.747,-80.629,45.727,-80.593,45.682,-80.546,45.65,-80.52,45.623,-80.511,45.59,-80.486,45.577,-80.444,45.584,-80.427,45.598,-80.429,45.625,-80.414,45.62,-80.389,45.574,-80.392,45.501,-80.385,45.458,-80.35,45.399,-80.302,45.383,-80.271,45.36,-80.177,45.344,-80.168,45.348,-80.185,45.387,-80.176,45.397,-80.145,45.399,-80.092,45.393,-80.057,45.375,-80.041,45.342,-80.05,45.301,-80.111,45.259,-80.121,45.223,-80.108,45.201,-80.092,45.197,-80.075,45.211,-80.049,45.204,-80.015,45.176,-80.006,45.153,-80.023,45.136,-80.083,45.126,-80.086,45.109,-80.022,45.072,-79.972,45.022,-79.946,45.017,-79.932,45.001,-79.929,44.974,-79.902,44.957,-79.828,44.936,-79.8,44.885,-79.785,44.854,-79.772,44.819,-79.75,44.815,-79.733,44.832,-79.732,44.863,-79.696,44.875,-79.692,44.859,-79.713,44.821,-79.709,44.791,-79.678,44.769,-79.717,44.761,-79.827,44.768,-79.876,44.799,-79.891,44.809,-79.925,44.813,-79.935,44.824,-79.92,44.855,-79.93,44.863,-79.997,44.856,-80.065,44.834,-80.108,44.807,-80.108,44.775,-80.079,44.743,-80.021,44.71,-79.997,44.652,-80.008,44.572,-80.037,44.517,-80.085,44.489,-80.226,44.51,-80.46,44.578,-80.593,44.639,-80.65,44.721,-80.67,44.727,-80.79,44.7,-80.854,44.666,-80.91,44.618,-80.925,44.633,-80.901,44.712,-80.891,44.761,-80.895,44.781,-80.919,44.797,-80.963,44.809,-81.022,44.802,-81.11,44.765,-81.13,44.77,-81.128,44.782,-81.036,44.843,-81.006,44.876,-81.006,44.907,-80.993,44.934,-80.966,44.959,-80.973,44.97,-81.012,44.967,-81.032,44.957,-81.074,44.914,-81.134,44.918,-81.141,44.928,-81.125,44.953,-81.138,44.968,-81.18,44.974,-81.193,45.003,-81.207,45.009,-81.26,45.028,-81.264,45.081,-81.282,45.124,-81.313,45.156,-81.316,45.189,-81.285,45.241,-81.296,45.249,-81.4,45.256,-81.595,45.262,-81.697,45.251,-81.706,45.223,-81.683,45.203,-81.629,45.189,-81.59,45.166,-81.577,45.145,-81.541,45.125,-81.499,45.081,-81.451,45.015,-81.415,44.987,-81.377,44.984,-81.355,44.978,-81.358,44.911,-81.341,44.87,-81.3,44.832,-81.278,44.79,-81.281,44.716,-81.278,44.644,-81.298,44.597,-81.352,44.542,-81.387,44.497,-81.402,44.461,-81.431,44.436,-81.474,44.425,-81.519,44.399,-81.565,44.36,-81.595,44.311,-81.609,44.253,-81.647,44.192,-81.708,44.131,-81.732,44.06,-81.718,43.982,-81.717,43.642,-81.705,43.558,-81.713,43.474,-81.739,43.392,-81.785,43.327,-81.848,43.28,-81.911,43.246,-81.974,43.223,-82.033,43.186,-82.088,43.133,-82.181,43.085,-82.312,43.043,-82.417,43.017,-82.432,43.039,-82.461,43.081,-82.516,43.28,-82.597,43.634,-82.663,43.854,-82.716,43.939,-82.788,44.004,-82.878,44.048,-82.947,44.067,-82.995,44.061,-83.069,44.025,-83.209,43.994,-83.267,43.973,-83.299,43.945,-83.338,43.929,-83.383,43.924,-83.387,43.917,-83.353,43.906,-83.353,43.879,-83.447,43.758,-83.469,43.731,-83.526,43.721,-83.573,43.697,-83.622,43.636,-83.668,43.621,-83.735,43.627,-83.848,43.664,-83.916,43.701,-83.938,43.738,-83.94,43.782,-83.92,43.834,-83.922,43.875,-83.879,43.963,-83.821,43.993,-83.738,44.001,-83.692,44.014,-83.683,44.035,-83.657,44.05,-83.614,44.058,-83.577,44.113,-83.548,44.215,-83.512,44.271,-83.487,44.281,-83.464,44.277,-83.463,44.265,-83.366,44.33,-83.331,44.375,-83.328,44.421,-83.319,44.556,-83.308,44.644,-83.294,44.683,-83.3,44.785,-83.328,44.869,-83.436,44.961,-83.452,44.992,-83.448,45.025,-83.426,45.059,-83.378,45.067,-83.293,45.04,-83.279,45.047,-83.302,45.062,-83.316,45.093,-83.32,45.139,-83.331,45.164,-83.349,45.169,-83.396,45.221,-83.403,45.248,-83.397,45.274,-83.415,45.299,-83.479,45.339,-83.594,45.379,-83.869,45.456,-83.918,45.486,-83.986,45.504,-84.073,45.509,-84.128,45.532,-84.151,45.574,-84.189,45.611,-84.242,45.644,-84.326,45.666,-84.441,45.676,-84.563,45.712,-84.691,45.774,-84.763,45.792,-84.757,45.863,-84.727,45.861,-84.713,45.871,-84.713,45.882,-84.725,45.895,-84.717,45.944,-84.725,45.958,-84.675,46.033,-84.632,46.053,-84.581,46.046,-84.55,46.029,-84.537,46.004,-84.521,45.995,-84.49,45.994,-84.473,45.99,-84.463,45.992,-84.447,46.006,-84.341,46,-83.979,45.962,-83.929,45.968,-83.901,45.981,-83.894,46.001,-83.917,46.021,-84.012,46.061,-84.041,46.088,-84.048,46.114,-84.035,46.137,-84.055,46.159,-84.109,46.178,-84.227,46.184,-84.248,46.203,-84.238,46.222,-84.204,46.247,-84.202,46.283,-84.213,46.304,-84.229,46.366,-84.255,46.408,-84.307,46.469,-84.327,46.491,-84.358,46.505,-84.344,46.531],[-83.345,45.993,-83.392,45.988,-83.437,45.963,-83.467,45.936,-83.48,45.907,-83.453,45.884,-83.387,45.869,-83.332,45.88,-83.269,45.947,-83.271,45.961,-83.296,45.977,-83.345,45.993],[-83.529,46.024,-83.58,46.08,-83.608,46.097,-83.684,46.106,-83.699,46.096,-83.68,46.07,-83.683,46.057,-83.715,46.038,-83.775,46.011,-83.82,46.005,-83.851,46.019,-83.871,46.017,-83.875,45.991,-83.866,45.979,-83.823,45.956,-83.731,45.943,-83.588,45.941,-83.518,45.958,-83.494,45.991,-83.503,46.013,-83.529,46.024],[-83.829,46.148,-83.808,46.167,-83.795,46.214,-83.822,46.253,-83.891,46.284,-83.971,46.303,-84.061,46.31,-84.097,46.288,-84.078,46.236,-84.039,46.177,-83.979,46.109,-83.93,46.092,-83.907,46.097,-83.887,46.119,-83.876,46.148,-83.856,46.157,-83.829,46.148],[-84.169,46.324,-84.188,46.323,-84.196,46.303,-84.185,46.279,-84.135,46.24,-84.121,46.246,-84.116,46.261,-84.12,46.285,-84.137,46.306,-84.169,46.324],[-84.133,46.341,-84.133,46.362,-84.164,46.41,-84.165,46.457,-84.137,46.503,-84.142,46.531,-84.18,46.542,-84.216,46.536,-84.274,46.488,-84.235,46.445,-84.209,46.384,-84.189,46.36,-84.163,46.346,-84.133,46.341],[-84.429,45.807,-84.557,45.82,-84.563,45.804,-84.487,45.754,-84.441,45.734,-84.412,45.736,-84.39,45.747,-84.375,45.765,-84.38,45.788,-84.417,45.814,-84.429,45.807],[-82.962,45.833,-82.937,45.811,-82.736,45.797,-82.626,45.772,-82.51,45.727,-82.409,45.698,-82.323,45.684,-82.256,45.66,-82.208,45.627,-82.091,45.591,-82.076,45.57,-82.055,45.561,-82.029,45.566,-81.821,45.692,-81.76,45.698,-81.76,45.685,-81.865,45.611,-81.902,45.603,-81.922,45.604,-81.977,45.57,-81.977,45.561,-81.946,45.566,-81.914,45.557,-81.881,45.535,-81.828,45.547,-81.755,45.591,-81.686,45.657,-81.593,45.792,-81.601,45.801,-81.685,45.801,-81.699,45.815,-81.673,45.845,-81.665,45.872,-81.675,45.896,-81.698,45.894,-81.735,45.864,-81.763,45.821,-81.796,45.74,-81.804,45.743,-81.838,45.788,-81.842,45.823,-81.828,45.862,-81.841,45.885,-81.879,45.892,-81.897,45.91,-81.895,45.939,-81.909,45.962,-81.939,45.979,-81.987,45.977,-82.055,45.955,-82.106,45.919,-82.143,45.869,-82.175,45.871,-82.205,45.924,-82.236,45.946,-82.268,45.936,-82.288,45.945,-82.295,45.973,-82.332,45.982,-82.401,45.971,-82.444,45.955,-82.463,45.934,-82.481,45.93,-82.498,45.944,-82.527,45.939,-82.569,45.914,-82.58,45.896,-82.556,45.87,-82.56,45.86,-82.52,45.849,-82.515,45.833,-82.531,45.806,-82.554,45.807,-82.583,45.837,-82.608,45.848,-82.627,45.839,-82.645,45.843,-82.66,45.86,-82.691,45.863,-82.737,45.852,-82.765,45.858,-82.774,45.88,-82.793,45.898,-82.823,45.91,-82.832,45.936,-82.821,45.977,-82.845,45.99,-82.905,45.978,-82.945,45.963,-82.965,45.945,-82.996,45.943,-83.035,45.957,-83.069,45.955,-83.097,45.935,-83.125,45.938,-83.151,45.963,-83.176,45.969,-83.201,45.955,-83.216,45.933,-83.222,45.902,-83.16,45.875,-82.962,45.833],[-81.713,45.526,-81.735,45.531,-81.829,45.498,-81.837,45.484,-81.833,45.473,-81.815,45.463,-81.788,45.466,-81.727,45.5,-81.713,45.526],[-80.173,44.796,-80.174,44.809,-80.158,44.848,-80.171,44.865,-80.216,44.872,-80.236,44.865,-80.233,44.843,-80.216,44.823,-80.173,44.796]]]},{"n":"Laguna Madre","z":2,"b":[-97.833,23.915,-97.427,25.447],"p":[[[-97.427,25.354,-97.456,25.273,-97.537,25.082,-97.612,24.782,-97.687,24.476,-97.72,24.255,-97.758,23.915,-97.765,23.917,-97.775,24.082,-97.759,24.159,-97.75,24.264,-97.741,24.391,-97.768,24.436,-97.805,24.444,-97.828,24.477,-97.833,24.517,-97.824,24.544,-97.8,24.546,-97.742,24.502,-97.725,24.501,-97.689,24.663,-97.7,24.709,-97.703,24.845,-97.643,24.908,-97.655,24.937,-97.707,24.966,-97.729,24.994,-97.699,25.159,-97.7,25.201,-97.706,25.225,-97.754,25.257,-97.721,25.288,-97.68,25.346,-97.654,25.365,-97.643,25.364,-97.611,25.326,-97.576,25.312,-97.543,25.312,-97.531,25.327,-97.518,25.435,-97.508,25.447,-97.473,25.44,-97.438,25.406,-97.427,25.354]]]},{"n":"Wollaston","z":2,"b":[-103.832,57.695,-102.791,58.552],"p":[[[-102.837,58.33,-102.862,58.318,-102.911,58.323,-102.948,58.306,-102.982,58.275,-103.001,58.243,-102.983,58.22,-102.939,58.198,-102.941,58.173,-103.056,58.124,-103.102,58.134,-103.231,58.081,-103.259,58.053,-103.281,57.984,-103.281,57.897,-103.269,57.87,-103.245,57.851,-103.218,57.844,-103.211,57.828,-103.269,57.738,-103.33,57.697,-103.36,57.695,-103.347,57.729,-103.306,57.778,-103.314,57.828,-103.359,57.861,-103.399,57.868,-103.469,57.877,-103.559,57.869,-103.728,57.796,-103.794,57.777,-103.829,57.779,-103.832,57.794,-103.796,57.823,-103.656,57.919,-103.67,57.939,-103.777,57.932,-103.803,57.947,-103.793,57.964,-103.697,58.019,-103.673,58.037,-103.672,58.054,-103.682,58.078,-103.709,58.102,-103.706,58.127,-103.654,58.173,-103.619,58.234,-103.613,58.267,-103.619,58.283,-103.662,58.284,-103.627,58.348,-103.61,58.409,-103.581,58.431,-103.55,58.455,-103.495,58.462,-103.384,58.432,-103.239,58.472,-103.223,58.485,-103.221,58.525,-103.205,58.544,-103.185,58.552,-103.141,58.533,-102.952,58.533,-102.946,58.503,-102.921,58.477,-102.803,58.412,-102.791,58.381,-102.837,58.33]]]},{"n":"Athabasca","z":2,"b":[-111.301,58.505,-106.018,59.6],"p":[[[-109.233,59.127,-109.279,59.127,-109.453,59.083,-109.886,59.006,-110.074,58.949,-110.276,58.86,-110.336,58.826,-110.403,58.768,-110.44,58.716,-110.435,58.699,-110.37,58.676,-110.376,58.644,-110.411,58.631,-110.673,58.63,-110.747,58.614,-110.777,58.583,-110.787,58.546,-110.812,58.512,-110.839,58.511,-110.877,58.629,-110.903,58.631,-110.944,58.621,-110.99,58.594,-111.036,58.522,-111.075,58.505,-111.093,58.527,-111.107,58.547,-111.11,58.572,-111.098,58.616,-111.262,58.703,-111.295,58.729,-111.301,58.756,-111.28,58.788,-111.194,58.788,-111.15,58.775,-111.161,58.739,-111.125,58.735,-111.019,58.764,-110.872,58.837,-110.612,59.041,-110.483,59.11,-110.352,59.156,-110.035,59.297,-109.875,59.357,-109.835,59.389,-109.777,59.413,-109.799,59.433,-109.801,59.445,-109.705,59.504,-109.623,59.543,-109.547,59.57,-109.398,59.597,-109.26,59.6,-109.172,59.587,-109.036,59.498,-108.828,59.51,-108.795,59.504,-108.863,59.406,-108.857,59.387,-108.748,59.387,-108.538,59.435,-108.219,59.473,-107.953,59.434,-107.558,59.396,-107.239,59.348,-106.816,59.327,-106.609,59.32,-106.021,59.29,-106.018,59.271,-106.042,59.252,-106.71,59.263,-107.205,59.284,-107.452,59.304,-107.589,59.292,-107.606,59.3,-107.626,59.331,-107.708,59.34,-107.734,59.336,-107.757,59.32,-107.786,59.288,-107.819,59.229,-107.837,59.216,-107.958,59.204,-108.211,59.161,-108.75,59.12,-108.942,59.115,-109.103,59.098,-109.163,59.102,-109.233,59.127]]]},{"n":"Mistassini","z":2,"b":[-74.22,50.038,-72.69,51.397],"p":[[[-72.9,51.146,-72.988,51.104,-73.114,50.997,-73.135,50.989,-73.254,50.913,-73.371,50.845,-73.459,50.793,-73.458,50.814,-73.394,50.893,-73.262,50.977,-73.189,51.069,-73.175,51.071,-73.151,51.07,-73.132,51.09,-73.129,51.106,-73.143,51.145,-73.151,51.156,-73.173,51.158,-73.293,51.077,-73.396,50.994,-73.573,50.835,-73.599,50.825,-73.631,50.824,-73.771,50.641,-73.8,50.595,-73.817,50.499,-73.81,50.452,-73.799,50.412,-73.788,50.39,-73.807,50.388,-73.832,50.413,-73.843,50.406,-73.837,50.341,-73.819,50.301,-73.8,50.236,-73.818,50.211,-73.904,50.162,-74.008,50.12,-74.034,50.091,-74.076,50.043,-74.152,50.038,-74.22,50.058,-74.2,50.076,-74.098,50.092,-74.013,50.178,-73.879,50.218,-73.828,50.249,-73.836,50.276,-73.88,50.297,-73.896,50.327,-73.899,50.352,-73.878,50.381,-73.839,50.639,-73.887,50.59,-73.916,50.544,-73.926,50.514,-73.917,50.489,-73.919,50.474,-73.932,50.458,-74.002,50.478,-74.105,50.399,-74.132,50.403,-74.104,50.448,-73.954,50.722,-73.845,50.888,-73.856,50.898,-73.911,50.897,-73.916,50.91,-73.89,50.954,-73.905,50.963,-73.892,50.981,-73.641,51.078,-73.378,51.24,-73.202,51.32,-73.067,51.39,-72.986,51.397,-72.984,51.378,-73.067,51.317,-73.04,51.309,-72.89,51.354,-72.811,51.361,-72.792,51.353,-72.8,51.335,-72.851,51.305,-73.067,51.206,-73.092,51.178,-73.072,51.16,-73.041,51.154,-72.811,51.244,-72.723,51.254,-72.69,51.241,-72.719,51.217,-72.9,51.146]]]},{"n":"IJsselmeer","z":2,"b":[5.014,52.252,5.87,53.065],"p":[[[5.867,52.563,5.87,52.485,5.795,52.428,5.495,52.252,5.316,52.292,5.04,52.344,5.014,52.364,5.036,52.405,5.062,52.431,5.069,52.451,5.07,52.52,5.06,52.566,5.066,52.624,5.138,52.627,5.242,52.67,5.247,52.73,5.165,52.786,5.118,52.836,5.094,52.886,5.068,52.923,5.152,52.967,5.269,53.027,5.377,53.065,5.398,53.042,5.413,52.903,5.42,52.874,5.455,52.857,5.547,52.846,5.657,52.851,5.667,52.817,5.625,52.782,5.618,52.758,5.613,52.684,5.619,52.658,5.653,52.63,5.835,52.609,5.867,52.563],[5.801,52.466,5.834,52.502,5.829,52.535,5.797,52.565,5.736,52.582,5.658,52.588,5.539,52.558,5.377,52.49,5.169,52.379,5.184,52.358,5.242,52.334,5.424,52.298,5.499,52.302,5.548,52.324,5.621,52.376,5.741,52.429,5.801,52.466]]]},{"n":"Rybinsk","z":2,"b":[37.151,58.016,39.005,59.21],"p":[[[38.681,58.108,38.612,58.113,38.383,58.122,38.357,58.1,38.437,58.036,38.437,58.018,38.415,58.016,38.369,58.032,38.31,58.041,38.281,58.054,38.259,58.071,38.25,58.103,38.251,58.199,38.221,58.237,38.149,58.249,37.944,58.301,37.897,58.292,37.507,58.535,37.417,58.613,37.317,58.657,37.155,58.695,37.155,58.707,37.169,58.726,37.151,58.849,37.308,58.76,37.372,58.731,37.423,58.727,37.41,58.689,37.41,58.67,37.459,58.634,37.538,58.613,37.565,58.553,37.609,58.545,37.661,58.557,37.667,58.572,37.621,58.626,37.632,58.636,37.715,58.618,37.768,58.618,37.995,58.55,38.074,58.542,38.074,58.592,38.103,58.619,38.096,58.634,37.988,58.672,37.984,58.692,37.945,58.72,37.942,58.735,37.961,58.744,38.024,58.746,37.996,58.862,37.918,58.858,37.869,58.897,37.828,58.914,37.777,58.926,37.714,58.928,37.642,58.919,37.581,58.899,37.501,58.845,37.471,58.857,37.466,58.875,37.484,58.927,37.499,58.944,37.609,58.989,37.673,59.025,37.696,59.064,37.676,59.102,37.64,59.138,37.645,59.157,37.843,59.155,37.94,59.174,37.987,59.177,38.002,59.16,38.007,59.144,38.027,59.128,38.116,59.103,38.159,59.102,38.18,59.113,38.18,59.132,38.176,59.148,38.124,59.203,38.143,59.21,38.183,59.203,38.294,59.161,38.415,59.173,38.407,59.152,38.332,59.093,38.239,59.063,38.171,59.065,37.91,59.103,37.866,59.094,37.858,59.075,37.92,59.042,38.047,58.994,38.119,58.955,38.427,58.747,38.61,58.637,38.753,58.547,38.989,58.496,39.004,58.476,38.97,58.441,39,58.387,39.005,58.358,38.871,58.331,38.83,58.296,38.812,58.251,38.819,58.135,38.805,58.122,38.779,58.135,38.692,58.207,38.638,58.22,38.619,58.201,38.698,58.133,38.704,58.113,38.681,58.108]]]},{"n":"Issyk-Kul","z":2,"b":[76.215,42.168,78.303,42.734],"p":[[[78.303,42.588,78.292,42.564,78.158,42.55,78.014,42.487,77.871,42.428,77.852,42.402,77.854,42.367,77.716,42.24,77.595,42.2,77.515,42.182,77.137,42.168,77.023,42.173,76.921,42.19,76.578,42.291,76.432,42.32,76.329,42.33,76.263,42.328,76.227,42.389,76.215,42.435,76.226,42.461,76.453,42.489,76.747,42.565,76.965,42.612,77.19,42.647,77.337,42.67,77.535,42.651,77.684,42.702,78.064,42.727,78.249,42.734,78.151,42.672,78.119,42.642,78.136,42.608,78.303,42.588]]]},{"n":"Woods","z":2,"b":[-95.315,48.863,-94.041,49.993],"p":[[[-94.052,49.373,-94.041,49.358,-94.047,49.268,-94.065,49.238,-94.092,49.219,-94.119,49.206,-94.129,49.21,-94.115,49.231,-94.109,49.263,-94.182,49.33,-94.221,49.394,-94.278,49.407,-94.251,49.457,-94.254,49.471,-94.365,49.483,-94.474,49.472,-94.646,49.445,-94.745,49.409,-94.77,49.393,-94.79,49.353,-94.778,49.34,-94.64,49.354,-94.609,49.345,-94.577,49.298,-94.522,49.267,-94.464,49.232,-94.354,49.266,-94.303,49.253,-94.272,49.227,-94.267,49.194,-94.24,49.181,-94.099,49.167,-94.074,49.155,-94.117,49.126,-94.197,49.096,-94.265,49.114,-94.31,49.104,-94.324,49.071,-94.537,48.945,-94.713,48.863,-94.818,48.868,-94.916,48.926,-95.003,48.964,-95.037,48.959,-95.179,48.897,-95.274,48.886,-95.313,48.928,-95.315,48.97,-95.267,49.029,-95.266,49.06,-95.277,49.105,-95.261,49.137,-95.199,49.166,-95.085,49.176,-94.983,49.195,-94.936,49.24,-94.941,49.321,-95.009,49.342,-95.014,49.368,-94.941,49.4,-94.909,49.437,-94.822,49.512,-94.691,49.561,-94.645,49.603,-94.65,49.621,-94.688,49.626,-94.745,49.616,-94.796,49.599,-94.911,49.526,-94.985,49.497,-95.107,49.463,-95.139,49.472,-95.116,49.543,-95.152,49.622,-95.099,49.639,-95.008,49.635,-94.962,49.621,-94.973,49.584,-94.958,49.575,-94.926,49.576,-94.888,49.585,-94.84,49.613,-94.87,49.663,-94.823,49.69,-94.588,49.724,-94.56,49.764,-94.576,49.807,-94.678,49.953,-94.659,49.974,-94.615,49.993,-94.588,49.988,-94.539,49.931,-94.53,49.853,-94.496,49.804,-94.451,49.738,-94.346,49.724,-94.312,49.685,-94.332,49.639,-94.314,49.6,-94.362,49.576,-94.368,49.563,-94.302,49.515,-94.173,49.497,-94.134,49.448,-94.144,49.427,-94.052,49.373],[-94.702,49.191,-94.787,49.192,-94.784,49.17,-94.731,49.092,-94.657,49.09,-94.631,49.056,-94.594,49.036,-94.528,49.041,-94.506,49.118,-94.446,49.179,-94.448,49.202,-94.515,49.215,-94.621,49.191,-94.702,49.191]]]},{"n":"Nipigon","z":2,"b":[-89.092,49.389,-88.03,50.291],"p":[[[-88.03,49.961,-88.106,49.934,-88.145,49.912,-88.143,49.895,-88.108,49.868,-88.129,49.785,-88.112,49.711,-88.134,49.635,-88.166,49.58,-88.159,49.507,-88.18,49.466,-88.305,49.459,-88.368,49.434,-88.406,49.389,-88.439,49.394,-88.467,49.443,-88.494,49.463,-88.513,49.514,-88.542,49.549,-88.552,49.544,-88.547,49.488,-88.563,49.462,-88.622,49.428,-88.668,49.433,-88.706,49.442,-88.773,49.458,-88.786,49.488,-88.759,49.527,-88.71,49.587,-88.61,49.606,-88.59,49.608,-88.587,49.628,-88.607,49.647,-88.688,49.633,-88.744,49.631,-88.786,49.567,-88.823,49.54,-88.869,49.521,-88.909,49.51,-88.96,49.501,-89,49.529,-89.019,49.568,-89.016,49.583,-88.96,49.56,-88.92,49.56,-88.907,49.579,-88.906,49.618,-88.929,49.694,-88.93,49.755,-88.956,49.756,-89.008,49.714,-89.045,49.726,-89.092,49.776,-89.087,49.804,-89.069,49.824,-89.004,49.787,-88.949,49.81,-88.941,49.82,-88.95,49.855,-88.945,49.88,-88.888,49.933,-88.905,50.023,-88.893,50.057,-88.769,50.213,-88.705,50.254,-88.639,50.277,-88.551,50.239,-88.465,50.238,-88.356,50.162,-88.315,50.148,-88.288,50.15,-88.278,50.159,-88.288,50.178,-88.34,50.226,-88.415,50.265,-88.421,50.289,-88.384,50.291,-88.344,50.275,-88.232,50.213,-88.199,50.188,-88.171,50.135,-88.172,50.113,-88.204,50.111,-88.215,50.096,-88.222,50.059,-88.218,50.027,-88.235,49.991,-88.213,49.993,-88.169,50.028,-88.133,50.035,-88.084,50.001,-88.033,49.979,-88.03,49.961],[-88.554,50.022,-88.554,50.036,-88.59,50.047,-88.616,50.047,-88.63,50.044,-88.633,50.017,-88.639,49.985,-88.654,49.95,-88.688,49.927,-88.719,49.9,-88.747,49.879,-88.748,49.868,-88.716,49.85,-88.676,49.85,-88.621,49.82,-88.588,49.805,-88.582,49.816,-88.59,49.836,-88.611,49.862,-88.636,49.897,-88.636,49.916,-88.625,49.951,-88.588,49.983,-88.567,49.997,-88.554,50.022],[-88.385,49.647,-88.374,49.689,-88.393,49.7,-88.422,49.685,-88.489,49.66,-88.49,49.612,-88.399,49.617,-88.385,49.647]]]},{"n":"Reindeer","z":2,"b":[-103.298,56.277,-101.533,58.107],"p":[[[-101.724,57.91,-101.678,57.918,-101.544,57.92,-101.533,57.88,-101.543,57.816,-101.63,57.757,-101.735,57.647,-101.828,57.579,-101.961,57.517,-102.048,57.484,-102.106,57.427,-102.094,57.368,-101.888,57.305,-101.836,57.27,-101.864,57.25,-101.957,57.239,-102.032,57.191,-102.048,57.114,-101.999,57.054,-101.962,57.025,-102.041,56.965,-102.127,56.915,-102.177,56.864,-102.313,56.838,-102.34,56.81,-102.356,56.771,-102.398,56.697,-102.437,56.658,-102.439,56.627,-102.465,56.608,-102.544,56.588,-102.601,56.564,-102.885,56.473,-102.927,56.441,-102.938,56.402,-102.977,56.369,-103.044,56.38,-103.087,56.419,-103.137,56.414,-103.201,56.358,-103.232,56.309,-103.241,56.278,-103.272,56.277,-103.288,56.304,-103.271,56.387,-103.261,56.437,-103.257,56.475,-103.298,56.496,-103.293,56.543,-103.241,56.574,-103.192,56.555,-103.153,56.62,-103.129,56.764,-103.106,56.853,-103.069,56.849,-103.08,56.677,-103.051,56.569,-102.978,56.562,-102.862,56.683,-102.794,56.709,-102.732,56.716,-102.692,56.742,-102.677,56.773,-102.625,56.787,-102.57,56.83,-102.544,56.93,-102.554,56.998,-102.584,57.046,-102.676,57.083,-102.742,57.101,-102.722,57.154,-102.693,57.21,-102.721,57.245,-102.789,57.283,-102.852,57.299,-102.919,57.295,-102.933,57.364,-102.89,57.451,-102.791,57.504,-102.694,57.564,-102.674,57.597,-102.693,57.637,-102.692,57.688,-102.64,57.702,-102.523,57.73,-102.44,57.799,-102.205,57.931,-102.149,57.993,-102.151,58.038,-102.052,58.107,-101.991,58.088,-102.013,58.003,-101.971,57.963,-101.913,57.991,-101.868,58.054,-101.819,58.065,-101.803,58.02,-101.847,57.898,-101.846,57.866,-101.815,57.843,-101.733,57.873,-101.724,57.91]]]},{"n":"Oneida","z":3,"b":[-76.096,43.177,-75.733,43.263],"p":[[[-75.75,43.226,-75.733,43.206,-75.8,43.181,-75.959,43.177,-76.096,43.238,-76.096,43.254,-76.06,43.263,-75.861,43.248,-75.75,43.226]]]},{"n":"Iliamna","z":3,"b":[-155.914,59.335,-153.971,59.791],"p":[[[-153.971,59.769,-154.109,59.718,-154.23,59.687,-154.371,59.665,-154.534,59.628,-154.563,59.592,-154.555,59.548,-154.579,59.503,-154.583,59.474,-154.614,59.451,-154.685,59.443,-154.907,59.432,-155.743,59.335,-155.881,59.377,-155.914,59.456,-155.846,59.538,-155.26,59.662,-154.993,59.705,-154.328,59.791,-154.19,59.781,-154.074,59.78,-153.971,59.769]]]},{"n":"","z":3,"b":[-156.884,57.633,-155.961,58.086],"p":[[[-155.961,57.87,-156.021,57.837,-156.021,57.812,-155.996,57.793,-155.993,57.763,-156.04,57.705,-156.13,57.64,-156.159,57.633,-156.127,57.689,-156.115,57.749,-156.161,57.797,-156.358,57.836,-156.605,57.879,-156.678,57.888,-156.832,57.931,-156.884,57.999,-156.817,58.067,-156.638,58.086,-156.279,58.006,-155.976,57.909,-155.961,57.87]]]},{"n":"Tahoe","z":3,"b":[-120.139,38.945,-119.918,39.254],"p":[[[-119.943,39.025,-119.966,38.957,-120.033,38.945,-120.081,38.971,-120.113,39.06,-120.139,39.091,-120.132,39.143,-120.059,39.221,-119.986,39.242,-119.962,39.254,-119.936,39.254,-119.918,39.207,-119.943,39.025]]]},{"n":"","z":3,"b":[35.197,-14.739,35.324,-14.51],"p":[[[35.324,-14.619,35.293,-14.684,35.27,-14.733,35.249,-14.739,35.197,-14.682,35.198,-14.58,35.208,-14.531,35.24,-14.51,35.268,-14.517,35.298,-14.563,35.324,-14.619]]]},{"n":"Zaysan","z":3,"b":[83.027,47.699,84.516,49.726],"p":[[[84.516,47.93,84.427,47.857,84.394,47.816,84.357,47.732,84.296,47.699,84.242,47.706,83.994,47.843,83.766,47.951,83.625,48.013,83.445,48.049,83.259,48.095,83.083,48.178,83.027,48.212,83.039,48.25,83.182,48.249,83.275,48.261,83.435,48.27,83.498,48.305,83.533,48.375,83.461,48.432,83.392,48.5,83.387,48.554,83.417,48.597,83.423,48.743,83.471,48.858,83.589,48.962,83.747,49.039,83.872,49.063,84.036,49.111,84.121,49.159,84.1,49.238,83.972,49.329,83.849,49.417,83.724,49.44,83.66,49.443,83.608,49.486,83.539,49.532,83.479,49.538,83.426,49.573,83.365,49.626,83.364,49.644,83.391,49.652,83.608,49.606,83.74,49.631,83.89,49.72,83.964,49.726,83.961,49.703,83.818,49.634,83.797,49.597,83.757,49.564,83.729,49.535,83.823,49.484,84.288,49.238,84.361,49.161,84.329,49.125,84.149,49.086,83.864,48.994,83.777,48.975,83.609,48.91,83.514,48.813,83.487,48.718,83.517,48.617,83.613,48.437,83.774,48.41,83.919,48.397,83.907,48.374,83.682,48.353,83.596,48.303,83.581,48.267,83.634,48.242,83.74,48.198,83.881,48.152,84.051,48.083,84.167,48.056,84.334,48.023,84.468,47.969,84.516,47.93]]]},{"n":"Winnebago","z":3,"b":[-88.519,43.808,-88.321,44.196],"p":[[[-88.325,44.068,-88.334,43.938,-88.383,43.866,-88.431,43.808,-88.465,43.813,-88.492,43.939,-88.519,44.029,-88.484,44.11,-88.418,44.195,-88.358,44.196,-88.321,44.18,-88.325,44.068]]]},{"n":"Nettilling","z":3,"b":[-71.438,65.954,-69.221,67.028],"p":[[[-71.438,66.596,-71.421,66.628,-71.373,66.663,-71.331,66.726,-71.209,66.862,-71.194,66.945,-71.227,67.001,-71.174,67.028,-71.001,67.02,-70.826,66.994,-70.788,66.967,-70.737,66.923,-70.63,66.868,-70.611,66.832,-70.569,66.794,-70.463,66.753,-70.353,66.737,-70.288,66.748,-70.211,66.731,-69.945,66.688,-69.736,66.628,-69.621,66.602,-69.49,66.581,-69.466,66.547,-69.527,66.503,-69.646,66.476,-69.646,66.442,-69.553,66.432,-69.423,66.429,-69.402,66.432,-69.347,66.449,-69.253,66.451,-69.221,66.406,-69.34,66.335,-69.471,66.29,-69.511,66.268,-69.53,66.238,-69.557,66.212,-69.585,66.203,-69.636,66.219,-69.648,66.246,-69.684,66.243,-69.732,66.22,-69.742,66.205,-69.77,66.203,-69.89,66.254,-69.975,66.258,-70.185,66.22,-70.376,66.195,-70.519,66.265,-70.582,66.289,-70.619,66.282,-70.656,66.252,-70.677,66.226,-70.71,66.204,-70.851,66.156,-71.104,65.991,-71.226,65.954,-71.311,65.969,-71.321,65.987,-71.221,66.024,-71.179,66.083,-71.194,66.151,-71.145,66.215,-71.15,66.256,-71.147,66.29,-71.127,66.331,-71.163,66.372,-71.231,66.418,-71.306,66.515,-71.438,66.596]]]},{"n":"Turkana","z":3,"b":[35.847,2.405,36.697,4.654],"p":[[[36.685,2.538,36.633,2.425,36.571,2.405,36.535,2.448,36.424,2.734,36.331,2.874,36.242,2.966,36.2,2.974,36.154,3.038,36.113,3.167,36.038,3.264,35.966,3.368,35.937,3.482,35.915,3.519,35.872,3.598,35.847,3.775,35.857,3.876,35.888,4.038,35.919,4.27,35.926,4.385,35.95,4.553,35.995,4.566,36.045,4.539,36.074,4.555,36.098,4.623,36.132,4.653,36.155,4.654,36.174,4.632,36.197,4.431,36.215,4.148,36.213,4.054,36.226,3.88,36.221,3.473,36.311,3.207,36.427,3.011,36.538,2.924,36.652,2.858,36.697,2.727,36.685,2.538]]]},{"n":"Dubawnt","z":3,"b":[-102.174,62.664,-100.644,63.546],"p":[[[-101.935,63.221,-101.807,63.267,-101.749,63.337,-101.688,63.37,-101.635,63.375,-101.611,63.382,-101.583,63.397,-101.584,63.428,-101.548,63.456,-101.457,63.443,-101.396,63.418,-101.365,63.397,-101.319,63.394,-101.293,63.415,-101.332,63.467,-101.276,63.491,-101.137,63.43,-101.081,63.439,-101.061,63.46,-101.008,63.463,-100.902,63.53,-100.856,63.52,-100.799,63.54,-100.706,63.546,-100.644,63.468,-100.673,63.373,-100.77,63.319,-100.823,63.297,-100.864,63.294,-100.917,63.301,-100.983,63.303,-101.041,63.292,-101.06,63.268,-101.064,63.243,-101.113,63.171,-101.104,63.102,-101.061,63.06,-101.007,63.052,-100.884,63.007,-100.884,62.956,-100.982,62.911,-101.028,62.896,-101.059,62.858,-101.023,62.803,-101.028,62.761,-101.138,62.713,-101.29,62.666,-101.382,62.683,-101.392,62.721,-101.354,62.766,-101.322,62.828,-101.33,62.891,-101.381,62.928,-101.414,62.911,-101.434,62.845,-101.473,62.791,-101.499,62.74,-101.512,62.675,-101.529,62.664,-101.554,62.684,-101.553,62.723,-101.59,62.776,-101.673,62.797,-101.755,62.801,-101.823,62.881,-101.916,62.877,-101.989,62.887,-102.035,62.925,-102.048,63.004,-102.091,63.066,-102.097,63.076,-102.127,63.089,-102.174,63.122,-102.16,63.165,-102.079,63.194,-101.935,63.221]]]},{"n":"Baker","z":3,"b":[-96.273,63.996,-94.225,64.325],"p":[[[-94.626,64.159,-94.556,64.149,-94.456,64.14,-94.346,64.135,-94.247,64.124,-94.225,64.076,-94.312,64.012,-94.455,63.996,-94.624,64.031,-94.86,64.066,-95.299,64.079,-95.502,64.058,-95.671,64.056,-95.784,64.056,-95.839,64.084,-95.946,64.127,-96.227,64.193,-96.273,64.301,-96.203,64.325,-95.744,64.302,-95.251,64.274,-95.037,64.239,-94.917,64.225,-94.793,64.217,-94.65,64.197,-94.626,64.159]]]},{"n":"Vistula Lagoon","z":3,"b":[19.717,54.434,20.405,54.71],"p":[[[19.764,54.434,19.717,54.482,19.879,54.589,19.942,54.647,19.958,54.682,19.988,54.71,20.033,54.702,20.084,54.673,20.208,54.684,20.405,54.682,20.102,54.583,19.929,54.517,19.769,54.438,19.764,54.434]]]},{"n":"Vistula Lagoon","z":3,"b":[19.251,54.268,19.764,54.482],"p":[[[19.717,54.482,19.764,54.434,19.738,54.412,19.658,54.374,19.586,54.348,19.481,54.3,19.347,54.268,19.251,54.311,19.279,54.338,19.503,54.381,19.616,54.426,19.717,54.482]]]},{"n":"Champlain","z":3,"b":[-73.451,43.757,-73.088,45.078],"p":[[[-73.373,44.148,-73.405,44.054,-73.369,43.876,-73.356,43.781,-73.368,43.757,-73.386,43.81,-73.403,43.866,-73.43,44.005,-73.447,44.031,-73.451,44.083,-73.444,44.193,-73.354,44.281,-73.377,44.393,-73.4,44.431,-73.444,44.638,-73.4,44.775,-73.383,44.936,-73.347,45.018,-73.324,45.029,-73.308,44.956,-73.296,44.914,-73.285,44.913,-73.255,44.942,-73.204,45.022,-73.147,45.073,-73.104,45.078,-73.088,45.046,-73.111,45.009,-73.163,44.985,-73.193,44.957,-73.173,44.919,-73.176,44.874,-73.194,44.707,-73.214,44.586,-73.227,44.563,-73.263,44.542,-73.256,44.507,-73.229,44.493,-73.228,44.445,-73.236,44.433,-73.248,44.437,-73.271,44.397,-73.288,44.32,-73.326,44.234,-73.373,44.148]]]},{"n":"Salton Sea","z":3,"b":[-116.062,33.138,-115.612,33.522],"p":[[[-115.612,33.274,-115.616,33.223,-115.698,33.138,-115.789,33.147,-115.911,33.303,-116.062,33.433,-116.053,33.502,-116.01,33.522,-115.948,33.519,-115.884,33.452,-115.66,33.321,-115.612,33.274]]]},{"n":"Martre","z":3,"b":[-118.628,63.113,-117.306,63.57],"p":[[[-117.319,63.209,-117.306,63.185,-117.36,63.146,-117.554,63.113,-117.738,63.113,-117.785,63.133,-118.107,63.223,-118.137,63.25,-118.164,63.261,-118.201,63.283,-118.315,63.291,-118.467,63.31,-118.627,63.337,-118.628,63.375,-118.564,63.428,-118.344,63.513,-118.1,63.57,-118.001,63.544,-117.915,63.466,-117.764,63.404,-117.63,63.384,-117.575,63.372,-117.57,63.358,-117.47,63.305,-117.435,63.298,-117.339,63.224,-117.319,63.209]]]},{"n":"Llanquihue","z":3,"b":[-72.994,-41.294,-72.599,-40.974],"p":[[[-72.967,-41.285,-72.989,-41.228,-72.994,-41.15,-72.974,-41.087,-72.912,-41.021,-72.849,-40.974,-72.776,-40.989,-72.703,-41.026,-72.639,-41.1,-72.599,-41.184,-72.629,-41.214,-72.727,-41.2,-72.876,-41.294,-72.967,-41.285]]]},{"n":"Hongze Lake","z":3,"b":[117.809,32.885,118.861,33.645],"p":[[[118.605,33.172,118.574,33.141,118.519,33.036,118.492,32.998,118.431,32.973,118.267,32.925,118.227,32.897,118.183,32.885,118.171,32.906,118.192,32.941,118.181,32.971,118.142,32.986,118.088,32.958,118.066,32.936,118.05,32.964,118.084,33.005,118.13,33.028,118.226,32.991,118.24,33.008,118.234,33.046,118.182,33.139,118.102,33.144,117.979,33.142,117.915,33.126,117.892,33.065,117.838,33.038,117.809,33.048,117.851,33.062,117.874,33.093,117.886,33.157,117.912,33.181,117.992,33.187,118.114,33.215,118.175,33.21,118.227,33.143,118.275,33.045,118.34,32.994,118.407,33.01,118.417,33.038,118.44,33.068,118.46,33.099,118.47,33.128,118.454,33.16,118.322,33.182,118.249,33.221,118.233,33.288,118.273,33.267,118.318,33.268,118.392,33.338,118.446,33.407,118.49,33.401,118.57,33.372,118.607,33.376,118.547,33.437,118.488,33.54,118.493,33.623,118.544,33.645,118.571,33.62,118.608,33.515,118.664,33.463,118.774,33.443,118.861,33.357,118.861,33.339,118.834,33.304,118.778,33.176,118.716,33.089,118.681,33.077,118.678,33.124,118.653,33.174,118.605,33.172]]]},{"n":"Khövsgöl Nuur","z":3,"b":[100.179,50.494,100.787,51.61],"p":[[[100.787,51.397,100.77,51.293,100.749,51.222,100.73,51.138,100.685,51.019,100.688,50.983,100.507,50.809,100.472,50.739,100.445,50.659,100.473,50.625,100.473,50.603,100.444,50.568,100.37,50.538,100.193,50.494,100.179,50.531,100.194,50.594,100.228,50.654,100.248,50.743,100.296,51.109,100.284,51.25,100.356,51.352,100.402,51.392,100.388,51.441,100.38,51.466,100.491,51.556,100.525,51.597,100.559,51.61,100.752,51.447,100.787,51.397]]]},{"n":"Uvs","z":3,"b":[92.235,50.005,93.362,50.658],"p":[[[92.316,50.302,92.235,50.413,92.348,50.475,92.546,50.54,92.789,50.617,92.893,50.658,92.965,50.638,92.99,50.608,92.978,50.54,93.076,50.428,93.227,50.364,93.259,50.358,93.254,50.327,93.325,50.308,93.362,50.282,93.301,50.255,93.216,50.251,93.095,50.198,93.018,50.145,92.913,50.056,92.722,50.01,92.601,50.005,92.498,50.053,92.382,50.142,92.337,50.228,92.316,50.302]]]},{"n":"Mweru","z":3,"b":[28.324,-9.505,29.128,-8.513],"p":[[[29.128,-8.662,29.107,-8.74,29.021,-8.922,28.891,-9.092,28.769,-9.272,28.696,-9.396,28.592,-9.407,28.521,-9.404,28.504,-9.441,28.51,-9.485,28.474,-9.505,28.377,-9.503,28.333,-9.45,28.324,-9.364,28.326,-9.237,28.367,-9.101,28.643,-8.768,28.867,-8.513,28.954,-8.521,29.078,-8.6,29.128,-8.662]]]},{"n":"Khanka","z":3,"b":[132.005,44.529,132.81,45.329],"p":[[[132.81,45.052,132.743,44.943,132.614,44.711,132.46,44.613,132.366,44.529,132.276,44.569,132.238,44.606,132.241,44.63,132.169,44.674,132.069,44.743,132.084,44.851,132.109,44.969,132.015,45.074,132.017,45.127,132.005,45.214,132.071,45.287,132.23,45.328,132.402,45.329,132.579,45.296,132.725,45.23,132.808,45.149,132.81,45.052]]]},{"n":"Tonlé Sap","z":3,"b":[103.699,12.426,104.591,13.25],"p":[[[104.57,12.426,104.532,12.458,104.464,12.506,104.401,12.508,104.258,12.564,104.14,12.671,104.105,12.789,104.008,12.804,103.895,12.867,103.806,12.947,103.755,13.039,103.724,13.121,103.699,13.207,103.731,13.25,103.842,13.235,103.981,13.179,104.1,13.036,104.199,12.845,104.235,12.764,104.268,12.739,104.37,12.747,104.44,12.724,104.468,12.64,104.468,12.571,104.471,12.559,104.498,12.54,104.555,12.514,104.558,12.486,104.565,12.463,104.591,12.432,104.57,12.426]]]},{"n":"Qinghai","z":3,"b":[99.65,36.557,100.753,37.218],"p":[[[100.753,36.811,100.746,36.781,100.723,36.745,100.723,36.673,100.704,36.589,100.628,36.557,100.523,36.58,100.431,36.641,100.228,36.645,99.958,36.679,99.827,36.739,99.7,36.853,99.65,36.911,99.681,36.947,99.762,36.948,99.816,36.955,99.834,36.988,99.804,37.067,99.787,37.149,99.836,37.182,99.956,37.218,100.104,37.202,100.275,37.15,100.448,37.025,100.586,36.903,100.753,36.811]]]},{"n":"Yarroto Vtoroe","z":3,"b":[70.93,67.988,71.381,68.103],"p":[[[71.239,67.99,71.133,68,71.065,67.996,70.971,68.012,70.93,68.065,70.964,68.098,71.093,68.103,71.277,68.059,71.381,68.025,71.381,67.997,71.343,67.988,71.239,67.99]]]},{"n":"Tharthar","z":3,"b":[43.066,33.706,43.493,34.329],"p":[[[43.261,33.728,43.196,33.792,43.106,33.915,43.074,34.113,43.066,34.258,43.084,34.315,43.116,34.329,43.239,34.141,43.362,34.043,43.467,33.95,43.493,33.888,43.476,33.807,43.478,33.729,43.42,33.706,43.332,33.717,43.261,33.728]]]},{"n":"Milh","z":3,"b":[43.423,32.531,43.878,33.079],"p":[[[43.878,32.688,43.863,32.612,43.793,32.535,43.739,32.531,43.668,32.553,43.629,32.58,43.577,32.596,43.482,32.642,43.438,32.715,43.423,32.81,43.432,32.869,43.451,32.879,43.475,32.919,43.469,33.011,43.508,33.06,43.55,33.079,43.583,33.039,43.72,32.808,43.821,32.732,43.878,32.688]]]},{"n":"Dead Sea","z":3,"b":[35.402,31.325,35.585,31.766],"p":[[[35.585,31.724,35.567,31.574,35.545,31.359,35.521,31.327,35.423,31.325,35.403,31.348,35.402,31.492,35.421,31.619,35.463,31.71,35.522,31.756,35.559,31.766,35.585,31.724]]]},{"n":"Pihkva","z":3,"b":[27.497,57.86,28.169,58.215],"p":[[[27.497,58.184,27.569,58.215,27.592,58.175,27.714,58.164,27.865,58.173,27.973,58.133,28.097,58.044,28.149,57.975,28.169,57.915,28.102,57.86,27.992,57.868,27.911,57.896,27.875,57.92,27.882,57.937,27.803,57.99,27.678,58.019,27.624,58.014,27.497,58.184]]]},{"n":"Saint-Jean","z":4,"b":[-72.331,48.444,-71.774,48.755],"p":[[[-72.055,48.454,-72.133,48.469,-72.165,48.481,-72.242,48.575,-72.306,48.619,-72.331,48.669,-72.291,48.733,-72.158,48.755,-72,48.724,-71.907,48.681,-71.846,48.659,-71.776,48.61,-71.774,48.561,-71.781,48.518,-71.823,48.475,-71.924,48.444,-72.055,48.454]]]},{"n":"Keuka","z":4,"b":[-77.203,42.425,-77.058,42.648],"p":[[[-77.058,42.644,-77.107,42.521,-77.191,42.425,-77.203,42.437,-77.17,42.512,-77.148,42.568,-77.113,42.569,-77.077,42.648,-77.058,42.644]]]},{"n":"Seneca","z":4,"b":[-76.97,42.416,-76.876,42.878],"p":[[[-76.879,42.416,-76.91,42.459,-76.918,42.591,-76.954,42.73,-76.97,42.878,-76.954,42.87,-76.882,42.683,-76.883,42.537,-76.876,42.444,-76.879,42.416]]]},{"n":"Cayuga","z":4,"b":[-76.773,42.489,-76.52,42.943],"p":[[[-76.52,42.501,-76.528,42.489,-76.586,42.547,-76.667,42.603,-76.773,42.778,-76.751,42.925,-76.735,42.943,-76.701,42.758,-76.665,42.668,-76.603,42.594,-76.52,42.501]]]},{"n":"Kainji","z":4,"b":[4.4,9.865,4.685,10.644],"p":[[[4.685,10.365,4.679,10.344,4.598,10.196,4.6,10.113,4.617,10.003,4.659,9.885,4.626,9.865,4.594,9.867,4.566,9.903,4.537,10.028,4.496,10.066,4.474,10.07,4.468,10.098,4.495,10.126,4.51,10.148,4.506,10.174,4.47,10.186,4.464,10.196,4.485,10.229,4.465,10.306,4.4,10.375,4.4,10.403,4.438,10.418,4.451,10.438,4.459,10.46,4.47,10.627,4.492,10.644,4.567,10.603,4.606,10.617,4.656,10.607,4.68,10.585,4.65,10.517,4.667,10.41,4.685,10.365]]]},{"n":"Urmia","z":4,"b":[45.015,37.135,45.858,38.264],"p":[[[45.666,37.742,45.747,37.701,45.749,37.663,45.755,37.578,45.858,37.426,45.831,37.341,45.703,37.303,45.663,37.288,45.613,37.212,45.536,37.135,45.449,37.149,45.365,37.227,45.292,37.438,45.257,37.615,45.253,37.685,45.234,37.764,45.087,37.885,45.08,37.988,45.155,38.045,45.188,38.108,45.171,38.144,45.092,38.14,45.015,38.144,45.026,38.193,45.106,38.264,45.357,38.264,45.458,38.173,45.487,38.073,45.447,37.932,45.436,37.806,45.483,37.754,45.546,37.754,45.666,37.742]]]},{"n":"Cree","z":4,"b":[-107.091,57.224,-106.087,57.713],"p":[[[-106.258,57.696,-106.181,57.681,-106.11,57.655,-106.114,57.595,-106.106,57.56,-106.087,57.535,-106.097,57.507,-106.095,57.465,-106.14,57.414,-106.193,57.398,-106.254,57.374,-106.374,57.369,-106.541,57.326,-106.608,57.337,-106.698,57.327,-106.732,57.342,-106.764,57.356,-106.804,57.356,-106.884,57.321,-106.933,57.287,-106.97,57.254,-107.035,57.229,-107.079,57.224,-107.091,57.236,-107.073,57.326,-107.083,57.361,-107.06,57.386,-107.028,57.426,-107.064,57.457,-107.069,57.476,-107.018,57.477,-106.924,57.462,-106.831,57.49,-106.655,57.597,-106.422,57.649,-106.316,57.705,-106.28,57.713,-106.258,57.696]]]},{"n":"","z":4,"b":[-99.764,62.807,-99.142,63.187],"p":[[[-99.606,62.959,-99.558,63.004,-99.348,63.173,-99.298,63.187,-99.303,63.155,-99.332,63.1,-99.304,63.048,-99.234,63.025,-99.159,62.991,-99.142,62.929,-99.24,62.873,-99.386,62.817,-99.573,62.807,-99.73,62.819,-99.762,62.848,-99.764,62.873,-99.606,62.959]]]},{"n":"Kasba","z":4,"b":[-102.6,59.929,-101.849,60.649],"p":[[[-102.549,60.322,-102.485,60.314,-102.379,60.368,-102.315,60.424,-102.367,60.443,-102.437,60.5,-102.434,60.585,-102.352,60.633,-102.261,60.649,-102.215,60.615,-102.177,60.561,-102.158,60.512,-102.108,60.457,-101.998,60.375,-101.886,60.238,-101.849,60.112,-101.878,60.037,-101.885,59.983,-101.983,60.01,-101.988,59.965,-102.023,59.929,-102.085,60.019,-102.275,60.211,-102.382,60.25,-102.477,60.236,-102.588,60.246,-102.6,60.309,-102.549,60.322]]]},{"n":"Sarygamysh","z":4,"b":[57.053,41.573,57.865,42.335],"p":[[[57.865,41.773,57.818,41.7,57.685,41.636,57.527,41.594,57.43,41.579,57.346,41.573,57.247,41.653,57.174,41.735,57.116,41.731,57.062,41.725,57.053,41.773,57.094,41.968,57.183,42.185,57.273,42.303,57.392,42.335,57.55,42.261,57.627,42.15,57.613,42.105,57.632,42.066,57.682,42.01,57.7,41.913,57.845,41.838,57.865,41.773]]]},{"n":"Chany","z":4,"b":[76.734,54.614,78.049,55.057],"p":[[[76.747,54.832,76.869,54.883,77.067,54.919,77.134,54.923,77.164,54.887,77.245,54.856,77.294,54.89,77.307,54.925,77.327,54.968,77.43,55.02,77.57,55.053,77.636,55.057,77.667,55.054,77.696,55.047,77.724,55.025,77.878,55.009,78.033,54.975,78.049,54.927,77.938,54.891,77.807,54.863,77.777,54.859,77.694,54.832,77.643,54.803,77.636,54.769,77.693,54.741,77.73,54.692,77.741,54.661,77.694,54.634,77.6,54.614,77.565,54.624,77.574,54.647,77.522,54.665,77.435,54.684,77.354,54.694,77.245,54.723,77.052,54.732,77.017,54.73,76.943,54.715,76.826,54.701,76.734,54.717,76.753,54.787,76.747,54.832]]]},{"n":"Alakol","z":4,"b":[81.399,45.764,82.133,46.456],"p":[[[82.133,45.764,81.749,45.872,81.555,45.965,81.427,46.055,81.404,46.151,81.41,46.235,81.399,46.305,81.443,46.392,81.53,46.452,81.65,46.456,81.761,46.431,81.841,46.396,81.912,46.351,81.976,46.279,82.025,46.185,82.038,46.08,82.064,45.944,82.133,45.764]]]},{"n":"","z":4,"b":[-90.58,30.179,-90.425,30.348],"p":[[[-90.425,30.274,-90.467,30.198,-90.537,30.179,-90.574,30.197,-90.58,30.235,-90.557,30.284,-90.493,30.348,-90.433,30.326,-90.425,30.274]]]},{"n":"Wiyâshâkimî","z":4,"b":[-74.825,55.935,-73.985,56.336],"p":[[[-73.99,56.026,-74.07,55.975,-74.184,55.957,-74.228,55.946,-74.221,55.935,-74.249,55.938,-74.276,55.951,-74.231,55.985,-74.336,56.024,-74.564,56.067,-74.732,56.091,-74.8,56.137,-74.825,56.214,-74.819,56.273,-74.771,56.308,-74.664,56.336,-74.477,56.318,-74.312,56.262,-74.248,56.161,-74.161,56.144,-74.054,56.137,-73.985,56.097,-73.99,56.026]]]},{"n":"Nipissing","z":4,"b":[-80.447,46.161,-79.436,46.358],"p":[[[-80.447,46.282,-80.409,46.29,-80.346,46.286,-80.314,46.264,-80.253,46.249,-80.147,46.258,-80.118,46.277,-80.143,46.302,-80.125,46.324,-80.053,46.346,-79.856,46.34,-79.779,46.348,-79.653,46.358,-79.494,46.311,-79.436,46.235,-79.487,46.184,-79.573,46.166,-79.772,46.213,-79.876,46.193,-79.986,46.161,-80.006,46.166,-79.991,46.199,-80.015,46.232,-80.063,46.244,-80.293,46.225,-80.447,46.282]]]},{"n":"Hulun","z":4,"b":[116.988,48.561,118.035,49.351],"p":[[[117.229,48.585,117.114,48.561,117.089,48.579,117.105,48.629,117.096,48.705,117.014,48.786,116.988,48.841,117.059,48.907,117.225,49.003,117.389,49.139,117.529,49.27,117.636,49.342,117.69,49.351,117.707,49.322,117.688,49.274,117.686,49.22,117.716,49.18,117.749,49.174,117.776,49.161,117.812,49.125,117.899,49.11,118.007,49.105,118.035,49.071,117.964,49.035,117.875,49.033,117.824,49.059,117.772,49.059,117.733,49.03,117.716,48.986,117.516,48.846,117.328,48.694,117.31,48.636,117.229,48.585]]]},{"n":"Bangweulu","z":4,"b":[29.496,-11.416,30.019,-10.878],"p":[[[29.995,-11.091,29.937,-11.216,29.917,-11.301,29.837,-11.38,29.752,-11.416,29.669,-11.415,29.588,-11.394,29.562,-11.298,29.55,-11.248,29.511,-11.23,29.496,-11.195,29.52,-11.172,29.565,-11.157,29.741,-10.931,29.772,-10.933,29.847,-10.887,29.927,-10.878,30,-10.938,30.019,-11.014,29.995,-11.091]]]},{"n":"Inari","z":4,"b":[27.162,68.715,28.76,69.419],"p":[[[28.447,69.326,28.53,69.336,28.632,69.364,28.677,69.395,28.76,69.419,28.749,69.394,28.696,69.363,28.554,69.299,28.532,69.257,28.566,69.222,28.54,69.192,28.448,69.153,28.442,69.116,28.4,69.082,28.251,69.064,28.091,69.007,28.092,68.981,28.264,68.922,28.329,68.893,28.356,68.872,28.336,68.857,28.295,68.852,28.079,68.876,27.989,68.88,27.891,68.861,27.919,68.828,27.978,68.816,27.992,68.788,27.87,68.742,27.84,68.749,27.836,68.782,27.779,68.792,27.7,68.759,27.655,68.732,27.566,68.749,27.464,68.761,27.434,68.76,27.366,68.751,27.282,68.715,27.26,68.737,27.359,68.771,27.428,68.802,27.417,68.841,27.328,68.884,27.271,68.891,27.172,68.906,27.162,68.923,27.242,68.946,27.484,69.004,27.526,69.031,27.56,69.062,27.745,69.129,27.941,69.178,27.984,69.162,28.088,69.162,28.084,69.19,28.049,69.203,28.011,69.227,27.991,69.254,28.031,69.288,28.101,69.299,28.115,69.271,28.138,69.237,28.232,69.249,28.309,69.284,28.323,69.305,28.345,69.323,28.447,69.326]]]},{"n":"Oulujärvi","z":4,"b":[26.78,64.12,28.188,64.53],"p":[[[26.78,64.391,26.815,64.415,26.831,64.44,26.814,64.469,26.827,64.505,26.866,64.53,27.112,64.523,27.214,64.497,27.217,64.414,27.262,64.369,27.365,64.33,27.469,64.339,27.578,64.38,27.727,64.397,27.925,64.381,27.913,64.333,27.93,64.307,27.883,64.309,27.801,64.304,27.738,64.282,27.765,64.245,27.859,64.233,27.94,64.205,28.058,64.178,28.157,64.16,28.186,64.142,28.188,64.131,28.164,64.121,28.073,64.121,28.009,64.128,27.929,64.145,27.653,64.225,27.562,64.274,27.506,64.27,27.481,64.25,27.421,64.23,27.323,64.209,27.282,64.173,27.288,64.131,27.258,64.12,27.2,64.158,27.073,64.201,26.942,64.238,26.921,64.278,26.793,64.362,26.78,64.391]]]},{"n":"Peipus","z":4,"b":[26.966,58.184,27.821,59.007],"p":[[[27.569,58.215,27.497,58.184,27.473,58.217,27.425,58.303,27.324,58.402,27.237,58.452,27.157,58.648,26.966,58.806,26.974,58.884,27.107,58.954,27.298,58.995,27.524,59.007,27.683,58.98,27.748,58.912,27.814,58.571,27.821,58.485,27.798,58.447,27.748,58.43,27.583,58.306,27.561,58.257,27.569,58.215]]]},{"n":"Kivu","z":4,"b":[28.85,-2.448,29.358,-1.605],"p":[[[29.198,-1.644,29.249,-1.736,29.289,-1.86,29.358,-2.016,29.338,-2.058,29.283,-2.095,29.256,-2.13,29.24,-2.162,29.149,-2.277,29.112,-2.303,29.077,-2.345,29.047,-2.355,28.995,-2.36,28.91,-2.433,28.85,-2.448,28.857,-2.371,28.917,-2.288,28.903,-2.24,28.876,-2.228,28.872,-2.187,28.879,-2.153,28.916,-2.082,28.909,-2.008,28.988,-1.838,29.033,-1.647,29.071,-1.605,29.198,-1.644]]]},{"n":"Edward","z":4,"b":[29.327,-0.64,29.887,-0.12],"p":[[[29.419,-0.63,29.366,-0.64,29.335,-0.577,29.327,-0.53,29.36,-0.452,29.417,-0.339,29.496,-0.18,29.597,-0.134,29.644,-0.12,29.783,-0.121,29.887,-0.172,29.868,-0.303,29.642,-0.507,29.51,-0.612,29.452,-0.607,29.419,-0.63]]]},{"n":"Tai","z":4,"b":[119.939,30.936,120.602,31.532],"p":[[[120.602,31.087,120.394,30.971,120.236,30.936,120.103,30.973,119.978,31.091,119.939,31.253,119.987,31.392,120.108,31.492,120.219,31.532,120.254,31.51,120.256,31.495,120.251,31.475,120.271,31.445,120.325,31.442,120.38,31.429,120.372,31.386,120.331,31.35,120.326,31.32,120.357,31.27,120.391,31.227,120.431,31.2,120.44,31.153,120.395,31.1,120.423,31.083,120.461,31.081,120.54,31.127,120.602,31.087]]]},{"n":"","z":4,"b":[-76.547,56.127,-75.935,56.55],"p":[[[-76.547,56.164,-76.525,56.184,-76.504,56.213,-76.417,56.306,-76.397,56.345,-76.42,56.365,-76.425,56.389,-76.391,56.499,-76.366,56.55,-76.341,56.531,-76.33,56.504,-76.305,56.468,-76.245,56.423,-76.234,56.397,-76.275,56.379,-76.205,56.322,-76.116,56.277,-76.025,56.237,-75.939,56.191,-75.935,56.174,-75.953,56.145,-76.147,56.127,-76.446,56.139,-76.547,56.164]]]},{"n":"Mar Chiquita","z":4,"b":[-62.979,-30.917,-62.23,-30.521],"p":[[[-62.23,-30.776,-62.408,-30.841,-62.571,-30.863,-62.727,-30.917,-62.81,-30.847,-62.86,-30.801,-62.931,-30.751,-62.979,-30.692,-62.931,-30.649,-62.827,-30.624,-62.722,-30.584,-62.688,-30.548,-62.644,-30.521,-62.426,-30.559,-62.413,-30.543,-62.374,-30.548,-62.288,-30.589,-62.233,-30.671,-62.23,-30.776]]]},{"n":"Kremenchuk","z":4,"b":[31.888,48.994,33.194,49.57],"p":[[[33.167,49.019,33.163,48.994,33.076,49.004,33.066,49.021,33.043,49.063,32.981,49.083,32.863,49.087,32.77,49.116,32.718,49.14,32.621,49.199,32.408,49.302,32.294,49.346,32.17,49.404,31.9,49.513,31.888,49.554,31.955,49.57,32.114,49.541,32.184,49.521,32.35,49.469,32.461,49.426,32.632,49.382,32.682,49.401,32.659,49.464,32.665,49.537,32.712,49.559,32.754,49.485,32.861,49.408,33.015,49.281,33.104,49.2,33.194,49.119,33.194,49.094,33.167,49.019]]]},{"n":"La Grande 3","z":4,"b":[-76.124,53.465,-74.433,53.897],"p":[[[-76.088,53.666,-76.1,53.737,-76.036,53.737,-75.93,53.766,-75.902,53.791,-75.912,53.813,-75.85,53.855,-75.552,53.892,-75.292,53.897,-75.268,53.878,-75.241,53.843,-75.184,53.82,-74.799,53.893,-74.734,53.896,-74.725,53.857,-74.82,53.838,-74.889,53.794,-75.049,53.753,-75.194,53.721,-75.27,53.709,-75.265,53.684,-75.162,53.677,-74.943,53.672,-74.771,53.654,-74.593,53.667,-74.456,53.701,-74.433,53.685,-74.461,53.663,-74.679,53.609,-74.807,53.605,-74.935,53.578,-75.027,53.576,-74.998,53.622,-75.034,53.639,-75.12,53.623,-75.238,53.619,-75.478,53.631,-75.633,53.638,-75.658,53.62,-75.614,53.602,-75.601,53.582,-75.632,53.538,-75.716,53.497,-75.934,53.465,-76.102,53.475,-76.124,53.523,-76.064,53.569,-76.024,53.591,-75.945,53.624,-75.907,53.653,-75.945,53.686,-76.018,53.672,-76.088,53.666]]]},{"n":"Robert-Bourassa","z":4,"b":[-77.685,53.493,-76.088,54.188],"p":[[[-76.1,53.737,-76.088,53.666,-76.36,53.63,-76.385,53.596,-76.515,53.557,-76.839,53.499,-76.912,53.501,-77.187,53.493,-77.223,53.515,-77.29,53.523,-77.397,53.521,-77.488,53.56,-77.493,53.607,-77.465,53.638,-77.511,53.651,-77.621,53.659,-77.685,53.679,-77.612,53.722,-77.42,53.798,-77.341,53.811,-77.288,53.791,-77.016,53.821,-76.902,53.847,-76.766,53.91,-76.731,53.977,-76.846,54.031,-76.888,54.057,-76.868,54.072,-76.84,54.088,-76.66,54.026,-76.618,54.033,-76.602,54.055,-76.561,54.056,-76.509,54.048,-76.471,54.059,-76.472,54.082,-76.533,54.115,-76.573,54.14,-76.557,54.157,-76.454,54.178,-76.368,54.188,-76.329,54.162,-76.264,54.136,-76.202,54.139,-76.191,54.153,-76.15,54.143,-76.105,54.116,-76.14,54.108,-76.183,54.103,-76.255,54.073,-76.304,53.963,-76.349,53.946,-76.376,53.927,-76.433,53.898,-76.487,53.851,-76.596,53.788,-76.703,53.75,-76.75,53.723,-76.696,53.699,-76.554,53.712,-76.143,53.737,-76.1,53.737]]]},{"n":"","z":4,"b":[-157.044,57.443,-156.548,57.785],"p":[[[-156.548,57.766,-156.604,57.696,-156.777,57.566,-156.783,57.533,-156.768,57.507,-156.796,57.464,-156.834,57.443,-156.927,57.473,-157.019,57.512,-157.044,57.548,-157.006,57.566,-156.954,57.569,-156.893,57.58,-156.842,57.611,-156.837,57.653,-156.716,57.725,-156.683,57.755,-156.59,57.785,-156.548,57.766]]]},{"n":"Enriquillo","z":4,"b":[-71.794,18.421,-71.509,18.551],"p":[[[-71.509,18.47,-71.537,18.434,-71.607,18.421,-71.794,18.494,-71.777,18.531,-71.683,18.551,-71.584,18.514,-71.509,18.47]]]},{"n":"","z":4,"b":[-160.134,66.43,-159.663,66.544],"p":[[[-159.665,66.474,-159.663,66.433,-159.801,66.43,-159.945,66.442,-159.992,66.469,-160.134,66.52,-160.115,66.544,-159.922,66.535,-159.729,66.512,-159.665,66.474]]]},{"n":"","z":4,"b":[-115.896,60.109,-115.062,60.35],"p":[[[-115.062,60.329,-115.148,60.214,-115.157,60.195,-115.219,60.183,-115.286,60.16,-115.295,60.131,-115.294,60.109,-115.477,60.138,-115.605,60.173,-115.744,60.163,-115.879,60.163,-115.896,60.201,-115.804,60.254,-115.65,60.254,-115.531,60.262,-115.349,60.295,-115.221,60.337,-115.11,60.35,-115.062,60.329]]]},{"n":"Sakami","z":4,"b":[-76.938,52.968,-76.497,53.485],"p":[[[-76.637,53.26,-76.648,53.242,-76.665,53.221,-76.692,53.212,-76.685,53.19,-76.688,53.17,-76.784,53.147,-76.843,53.095,-76.839,53.034,-76.799,52.981,-76.855,52.968,-76.899,52.995,-76.938,53.119,-76.904,53.165,-76.881,53.216,-76.89,53.244,-76.892,53.291,-76.873,53.324,-76.892,53.345,-76.844,53.394,-76.72,53.469,-76.703,53.444,-76.703,53.419,-76.661,53.417,-76.613,53.447,-76.564,53.485,-76.515,53.468,-76.497,53.414,-76.523,53.379,-76.637,53.361,-76.672,53.34,-76.676,53.314,-76.656,53.298,-76.608,53.306,-76.589,53.289,-76.637,53.26]]]},{"n":"Argentino","z":4,"b":[-73.3,-50.62,-71.982,-49.982],"p":[[[-71.982,-50.224,-71.999,-50.244,-72.113,-50.264,-72.274,-50.302,-72.441,-50.307,-72.595,-50.293,-72.672,-50.278,-72.794,-50.291,-72.973,-50.357,-73.004,-50.419,-72.962,-50.471,-72.858,-50.47,-72.795,-50.46,-72.782,-50.483,-72.85,-50.538,-72.921,-50.612,-72.955,-50.62,-73.01,-50.515,-73.109,-50.365,-73.168,-50.332,-73.223,-50.335,-73.274,-50.346,-73.285,-50.331,-73.214,-50.27,-73.028,-50.289,-72.972,-50.261,-72.983,-50.226,-73.044,-50.199,-73.163,-50.148,-73.232,-50.165,-73.291,-50.211,-73.3,-50.185,-73.278,-50.121,-73.286,-50.07,-73.251,-50.033,-73.205,-50.001,-73.188,-49.982,-73.137,-50.031,-73.062,-50.133,-72.947,-50.18,-72.8,-50.196,-72.731,-50.186,-72.44,-50.173,-72.116,-50.132,-72.039,-50.162,-71.982,-50.224]]]},{"n":"Namtso","z":4,"b":[90.289,30.524,91.014,30.924],"p":[[[90.346,30.524,90.314,30.573,90.292,30.698,90.289,30.847,90.353,30.88,90.476,30.855,90.579,30.829,90.651,30.84,90.676,30.871,90.712,30.895,90.866,30.885,90.944,30.924,90.975,30.923,91.012,30.888,91.014,30.839,90.933,30.793,90.843,30.718,90.714,30.641,90.596,30.596,90.346,30.524]]]},{"n":"Tuz","z":4,"b":[33.158,38.523,33.674,39.127],"p":[[[33.458,38.854,33.525,38.812,33.597,38.799,33.674,38.712,33.617,38.578,33.533,38.523,33.423,38.559,33.326,38.611,33.236,38.667,33.158,38.772,33.219,38.876,33.293,38.967,33.309,39.057,33.305,39.127,33.35,39.12,33.424,39.056,33.454,38.992,33.44,38.938,33.458,38.854]]]},{"n":"Scott","z":4,"b":[-106.407,59.805,-105.704,60.228],"p":[[[-105.959,59.998,-106.019,59.994,-106.073,59.959,-106.081,59.892,-106.055,59.845,-106.104,59.809,-106.167,59.808,-106.195,59.836,-106.206,59.86,-106.237,59.862,-106.259,59.827,-106.278,59.813,-106.338,59.805,-106.375,59.831,-106.326,59.935,-106.332,60.022,-106.398,60.043,-106.407,60.064,-106.374,60.087,-106.308,60.101,-106.276,60.087,-106.263,60.061,-106.228,60.035,-106.195,60.021,-106.18,60.035,-106.153,60.097,-106.122,60.144,-106.019,60.192,-105.873,60.228,-105.826,60.214,-105.801,60.203,-105.704,60.184,-105.77,60.131,-105.914,60.117,-106.028,60.111,-106.069,60.083,-106.046,60.05,-105.939,60.059,-105.88,60.038,-105.875,60.006,-105.959,59.998]]]},{"n":"","z":4,"b":[27.34,60.938,27.849,61.078],"p":[[[27.535,60.938,27.429,60.969,27.342,61.001,27.34,61.021,27.367,61.033,27.448,60.994,27.579,60.984,27.692,61.012,27.704,61.056,27.724,61.078,27.777,61.068,27.81,61.044,27.849,60.984,27.744,60.95,27.535,60.938]]]},{"n":"Rikkavesi","z":4,"b":[29.422,62.682,29.822,63.051],"p":[[[29.472,62.869,29.521,62.833,29.562,62.834,29.583,62.888,29.537,62.976,29.494,63.029,29.522,63.051,29.595,62.981,29.747,62.876,29.822,62.795,29.812,62.732,29.742,62.697,29.666,62.682,29.623,62.74,29.541,62.774,29.458,62.833,29.422,62.883,29.44,62.894,29.472,62.869]]]},{"n":"","z":4,"b":[29.188,62.641,29.522,62.805],"p":[[[29.229,62.705,29.219,62.772,29.293,62.805,29.345,62.745,29.39,62.721,29.465,62.734,29.522,62.706,29.454,62.641,29.339,62.658,29.219,62.65,29.188,62.684,29.229,62.705]]]},{"n":"Nerpichye","z":4,"b":[159.808,69.228,160.48,69.378],"p":[[[160.48,69.25,160.433,69.228,160.256,69.238,159.965,69.282,159.808,69.347,159.844,69.378,159.998,69.357,160.17,69.327,160.325,69.296,160.48,69.25]]]},{"n":"Yarroto Pervoe","z":4,"b":[71.298,67.87,71.786,68.037],"p":[[[71.359,67.87,71.298,67.889,71.303,67.906,71.37,67.947,71.552,68.004,71.708,68.037,71.786,67.993,71.776,67.95,71.65,67.895,71.359,67.87]]]},{"n":"Ozero Neyto","z":4,"b":[70.128,70.011,70.512,70.136],"p":[[[70.489,70.092,70.512,70.055,70.371,70.015,70.231,70.011,70.128,70.076,70.174,70.136,70.368,70.117,70.489,70.092]]]},{"n":"Ozero Yambuto","z":4,"b":[70.749,69.953,71.058,70.139],"p":[[[70.851,69.969,70.749,70.033,70.754,70.067,70.79,70.096,70.837,70.136,70.941,70.139,70.939,70.1,70.943,70.054,71.058,69.988,71.026,69.953,70.851,69.969]]]},{"n":"","z":4,"b":[70.344,70.123,70.821,70.217],"p":[[[70.563,70.123,70.372,70.153,70.344,70.2,70.77,70.217,70.821,70.191,70.793,70.166,70.696,70.136,70.563,70.123]]]},{"n":"Vygozero","z":4,"b":[34.214,62.919,35.413,63.851],"p":[[[35.013,63.506,35.145,63.482,35.249,63.492,35.374,63.492,35.413,63.448,35.382,63.434,35.193,63.404,35.172,63.381,35.099,63.383,35.049,63.399,34.977,63.429,34.872,63.464,34.829,63.434,34.885,63.294,34.953,63.181,34.957,63.119,34.946,63.033,34.91,62.951,34.863,62.919,34.861,62.941,34.867,62.976,34.864,63.01,34.873,63.04,34.899,63.055,34.891,63.107,34.858,63.172,34.853,63.225,34.835,63.276,34.804,63.326,34.702,63.399,34.552,63.459,34.517,63.484,34.525,63.502,34.453,63.623,34.358,63.697,34.308,63.692,34.266,63.656,34.215,63.613,34.214,63.676,34.284,63.777,34.347,63.851,34.428,63.848,34.563,63.816,34.644,63.789,34.616,63.766,34.857,63.721,34.861,63.688,34.99,63.585,34.984,63.564,34.953,63.54,35.013,63.506]]]},{"n":"Segozero","z":4,"b":[33.257,63.189,34.066,63.487],"p":[[[33.441,63.444,33.586,63.487,33.637,63.476,33.657,63.433,33.697,63.409,33.847,63.438,33.933,63.418,33.991,63.297,34.066,63.229,34.017,63.201,33.897,63.189,33.703,63.19,33.572,63.203,33.499,63.216,33.307,63.284,33.274,63.316,33.274,63.341,33.283,63.382,33.257,63.423,33.355,63.424,33.441,63.444]]]},{"n":"Koitere","z":4,"b":[30.481,62.767,30.847,63.05],"p":[[[30.604,62.876,30.587,62.937,30.615,62.966,30.676,62.973,30.704,63.012,30.755,63.05,30.831,63.027,30.847,62.954,30.742,62.864,30.689,62.847,30.659,62.808,30.667,62.784,30.659,62.767,30.634,62.775,30.502,62.779,30.481,62.788,30.539,62.82,30.604,62.876]]]},{"n":"Pielinen","z":4,"b":[29.042,62.722,30.33,63.573],"p":[[[29.994,63.293,30.004,63.271,29.966,63.265,29.959,63.227,29.994,63.14,30.138,62.973,30.33,62.829,30.317,62.809,30.229,62.804,30.143,62.722,30.09,62.729,30.155,62.79,30.216,62.868,30.137,62.929,30.065,62.944,30.033,62.959,29.854,63.087,29.814,63.145,29.594,63.158,29.528,63.179,29.393,63.183,29.299,63.256,29.238,63.323,29.123,63.407,29.042,63.475,29.071,63.508,29.077,63.564,29.092,63.573,29.187,63.514,29.309,63.461,29.372,63.44,29.527,63.377,29.722,63.315,29.784,63.294,29.831,63.291,29.915,63.315,29.994,63.293]]]},{"n":"Pyaozero","z":4,"b":[30.651,65.905,31.335,66.27],"p":[[[31.17,66.107,31.211,66.059,31.24,66.004,31.277,65.955,31.335,65.918,31.295,65.905,31.155,65.914,30.95,65.964,30.651,66.052,30.704,66.135,30.722,66.186,30.789,66.23,30.899,66.263,31.033,66.27,31.103,66.245,31.174,66.138,31.17,66.107]]]},{"n":"Topozero","z":4,"b":[31.507,65.388,32.712,65.89],"p":[[[32.517,65.532,32.664,65.477,32.688,65.452,32.712,65.416,32.706,65.388,32.668,65.389,32.635,65.418,32.481,65.469,32.274,65.506,32.109,65.546,31.996,65.569,31.946,65.599,31.943,65.622,31.895,65.636,31.845,65.644,31.733,65.67,31.574,65.735,31.507,65.795,31.693,65.856,31.983,65.89,32.051,65.876,32.118,65.834,32.139,65.784,32.204,65.764,32.329,65.7,32.36,65.667,32.363,65.629,32.377,65.577,32.425,65.542,32.517,65.532]]]},{"n":"","z":4,"b":[-72.841,81.67,-68.968,81.94],"p":[[[-72.841,81.68,-72.618,81.705,-72.347,81.728,-69.582,81.918,-69.285,81.933,-68.968,81.94,-69.476,81.887,-70.016,81.84,-72.22,81.698,-72.714,81.67,-72.736,81.677,-72.841,81.68]]]},{"n":"","z":4,"b":[-89.522,52.854,-88.952,53.006],"p":[[[-89.522,52.883,-89.471,52.897,-89.434,52.918,-89.422,52.948,-89.283,53.006,-89.199,52.988,-89.123,52.966,-89.004,52.985,-88.952,52.988,-88.983,52.94,-89.044,52.87,-89.151,52.854,-89.362,52.866,-89.478,52.868,-89.522,52.883]]]},{"n":"Bosten","z":4,"b":[86.728,41.825,87.375,42.108],"p":[[[87.375,41.851,87.259,41.894,87.102,41.901,86.894,41.87,86.753,41.825,86.728,41.85,86.735,41.903,86.795,41.975,86.829,42.073,86.889,42.108,87.008,42.084,87.315,42.017,87.375,41.908,87.375,41.851]]]},{"n":"","z":4,"b":[30.85,61.921,31.053,62.083],"p":[[[30.868,61.974,30.868,62.009,30.853,62.049,30.85,62.083,30.926,62.083,30.971,62.065,31.053,61.97,31.02,61.929,30.939,61.921,30.885,61.941,30.868,61.974]]]},{"n":"Ilmen","z":4,"b":[30.857,58.142,31.688,58.477],"p":[[[31.531,58.23,31.446,58.206,31.394,58.142,31.156,58.171,30.876,58.214,30.857,58.239,30.955,58.284,31.019,58.32,31.08,58.332,31.17,58.359,31.252,58.425,31.298,58.477,31.374,58.429,31.43,58.417,31.511,58.389,31.518,58.373,31.637,58.388,31.653,58.369,31.646,58.351,31.647,58.308,31.688,58.202,31.631,58.207,31.531,58.23]]]},{"n":"Poyang","z":4,"b":[115.965,28.804,116.709,29.751],"p":[[[116.706,29.136,116.709,29.102,116.672,29.098,116.635,29.102,116.618,29.115,116.581,29.128,116.521,29.143,116.496,29.122,116.477,29.043,116.503,28.953,116.53,28.894,116.537,28.845,116.512,28.804,116.468,28.805,116.441,28.86,116.418,28.88,116.409,28.905,116.348,28.906,116.329,28.961,116.097,29.106,116.064,29.149,116.047,29.19,115.982,29.279,115.965,29.311,116.002,29.394,116.026,29.453,116.091,29.555,116.117,29.653,116.1,29.688,116.082,29.703,116.113,29.726,116.18,29.751,116.198,29.751,116.23,29.698,116.235,29.666,116.207,29.645,116.199,29.604,116.181,29.573,116.159,29.519,116.145,29.454,116.11,29.427,116.115,29.402,116.143,29.386,116.188,29.382,116.196,29.362,116.153,29.334,116.137,29.309,116.153,29.253,116.31,29.232,116.343,29.234,116.368,29.223,116.358,29.186,116.362,29.154,116.407,29.176,116.44,29.236,116.469,29.25,116.54,29.24,116.604,29.237,116.64,29.237,116.664,29.226,116.661,29.19,116.674,29.159,116.706,29.136]]]},{"n":"Daguan Lake","z":4,"b":[116.257,29.964,116.52,30.094],"p":[[[116.461,30.055,116.496,30.02,116.52,29.988,116.498,29.964,116.388,29.966,116.29,29.994,116.259,30.048,116.257,30.094,116.281,30.079,116.298,30.086,116.327,30.087,116.386,30.069,116.442,30.077,116.461,30.055]]]},{"n":"Longgan","z":4,"b":[115.977,29.881,116.26,30.061],"p":[[[116.195,30.038,116.242,30.032,116.253,30.01,116.26,29.976,116.254,29.935,116.211,29.908,116.131,29.898,116.044,29.881,115.977,29.952,115.985,30.016,116.073,30,116.118,30,116.129,30.026,116.15,30.061,116.195,30.038]]]},{"n":"Liangzi","z":4,"b":[114.35,30.085,114.642,30.366],"p":[[[114.35,30.196,114.371,30.211,114.395,30.236,114.406,30.268,114.444,30.285,114.496,30.289,114.501,30.311,114.474,30.333,114.458,30.364,114.501,30.366,114.539,30.345,114.603,30.3,114.623,30.258,114.642,30.133,114.623,30.096,114.581,30.137,114.542,30.188,114.509,30.194,114.474,30.158,114.445,30.085,114.408,30.106,114.365,30.165,114.35,30.196]]]},{"n":"Siling","z":4,"b":[88.566,31.596,89.323,31.918],"p":[[[89.318,31.794,89.287,31.788,89.253,31.749,89.211,31.679,89.138,31.602,89.058,31.596,88.989,31.609,88.946,31.625,88.937,31.658,88.948,31.7,88.927,31.728,88.85,31.726,88.764,31.712,88.685,31.729,88.608,31.796,88.566,31.844,88.608,31.88,88.704,31.867,88.759,31.834,88.789,31.829,88.814,31.841,88.864,31.887,88.968,31.918,89.12,31.913,89.266,31.872,89.323,31.823,89.318,31.794]]]},{"n":"Khar-Us","z":4,"b":[91.976,47.769,92.802,48.361],"p":[[[92.787,48.178,92.622,48.098,92.442,48.03,92.354,47.964,92.28,47.851,92.2,47.769,92.084,47.816,91.991,47.933,91.976,48.01,92.105,48.054,92.249,48.13,92.25,48.263,92.287,48.352,92.363,48.361,92.425,48.36,92.483,48.339,92.402,48.237,92.405,48.156,92.48,48.138,92.544,48.138,92.643,48.193,92.645,48.271,92.638,48.33,92.733,48.31,92.802,48.253,92.787,48.178]]]},{"n":"Taupo","z":4,"b":[175.749,-38.948,176.083,-38.677],"p":[[[176.066,-38.812,176.003,-38.865,175.927,-38.902,175.851,-38.931,175.795,-38.948,175.755,-38.943,175.8,-38.845,175.782,-38.797,175.749,-38.773,175.763,-38.727,175.809,-38.677,175.944,-38.677,176.057,-38.721,176.083,-38.758,176.066,-38.812]]]},{"n":"Geneva","z":4,"b":[6.133,46.205,6.874,46.529],"p":[[[6.874,46.432,6.866,46.403,6.845,46.385,6.665,46.402,6.469,46.389,6.336,46.361,6.157,46.205,6.133,46.205,6.158,46.302,6.344,46.458,6.588,46.529,6.819,46.466,6.874,46.432]]]},{"n":"Neagh","z":4,"b":[-6.585,54.499,-6.272,54.723],"p":[[[-6.488,54.723,-6.459,54.715,-6.318,54.709,-6.272,54.66,-6.282,54.604,-6.349,54.525,-6.354,54.502,-6.427,54.499,-6.562,54.515,-6.585,54.56,-6.541,54.623,-6.511,54.698,-6.488,54.723]]]},{"n":"Vättern","z":4,"b":[14.121,57.796,14.968,58.854],"p":[[[14.933,58.784,14.941,58.756,14.929,58.7,14.9,58.635,14.891,58.592,14.963,58.539,14.946,58.511,14.827,58.462,14.686,58.395,14.622,58.299,14.582,58.202,14.543,58.138,14.499,58.085,14.339,57.933,14.245,57.809,14.159,57.796,14.121,57.925,14.213,58.15,14.391,58.406,14.507,58.561,14.632,58.68,14.726,58.731,14.817,58.791,14.932,58.854,14.968,58.849,14.962,58.835,14.933,58.784]]]},{"n":"Van","z":4,"b":[42.353,38.326,43.611,38.988],"p":[[[42.989,38.346,42.923,38.39,42.912,38.426,42.882,38.477,42.856,38.484,42.781,38.459,42.683,38.457,42.586,38.482,42.538,38.49,42.353,38.5,42.365,38.549,42.434,38.61,42.458,38.643,42.458,38.692,42.526,38.745,42.723,38.782,42.845,38.794,42.935,38.794,43.079,38.809,43.163,38.846,43.159,38.869,43.126,38.882,43.088,38.881,43.075,38.916,43.152,38.948,43.285,38.964,43.436,38.988,43.56,38.978,43.611,38.947,43.531,38.898,43.383,38.836,43.292,38.813,43.21,38.716,43.186,38.603,43.287,38.548,43.318,38.489,43.235,38.407,43.148,38.337,43.088,38.326,42.989,38.346]]]},{"n":"Yellowstone","z":4,"b":[-110.555,44.319,-110.199,44.567],"p":[[[-110.199,44.335,-110.235,44.322,-110.273,44.319,-110.292,44.336,-110.328,44.356,-110.341,44.383,-110.394,44.391,-110.39,44.425,-110.426,44.447,-110.489,44.437,-110.531,44.414,-110.555,44.447,-110.529,44.471,-110.49,44.473,-110.45,44.48,-110.418,44.503,-110.381,44.556,-110.327,44.567,-110.272,44.544,-110.264,44.447,-110.202,44.391,-110.199,44.335]]]},{"n":"Teshekpuk","z":4,"b":[-154.257,70.48,-152.94,70.737],"p":[[[-152.94,70.569,-153.275,70.518,-153.656,70.48,-153.719,70.494,-153.794,70.487,-153.926,70.512,-154.019,70.543,-154.101,70.598,-154.174,70.634,-154.237,70.639,-154.257,70.661,-154.161,70.678,-154.035,70.684,-153.975,70.679,-153.938,70.698,-153.812,70.725,-153.611,70.737,-153.014,70.603,-152.94,70.569]]]},{"n":"Flathead","z":4,"b":[-114.254,47.711,-114.005,48.091],"p":[[[-114.156,47.933,-114.18,48.022,-114.199,48.04,-114.193,48.069,-114.134,48.091,-114.046,48.015,-114.024,47.934,-114.005,47.785,-114.021,47.756,-114.056,47.711,-114.118,47.718,-114.15,47.714,-114.163,47.74,-114.126,47.784,-114.148,47.812,-114.193,47.826,-114.25,47.837,-114.254,47.858,-114.194,47.891,-114.156,47.933]]]},{"n":"Mead","z":4,"b":[-114.816,35.831,-113.633,36.519],"p":[[[-113.636,35.831,-113.67,35.843,-113.726,35.922,-113.795,36.01,-113.895,36.079,-113.952,36.13,-113.989,36.165,-114.039,36.163,-114.11,36.032,-114.208,36.006,-114.294,36.03,-114.321,36.076,-114.373,36.1,-114.406,36.091,-114.451,36.079,-114.504,36.106,-114.64,36.106,-114.714,36.024,-114.741,36.014,-114.769,36.02,-114.812,36.075,-114.816,36.121,-114.653,36.154,-114.512,36.194,-114.432,36.237,-114.413,36.348,-114.387,36.483,-114.364,36.515,-114.336,36.519,-114.335,36.444,-114.359,36.335,-114.37,36.209,-114.295,36.11,-114.226,36.05,-114.159,36.077,-114.097,36.152,-114.03,36.245,-113.996,36.244,-113.924,36.176,-113.858,36.116,-113.777,36.048,-113.699,35.958,-113.633,35.859,-113.636,35.831]]]},{"n":"Claire","z":4,"b":[-112.447,58.303,-111.717,58.825],"p":[[[-111.817,58.56,-111.809,58.517,-111.831,58.484,-111.838,58.46,-111.843,58.425,-111.831,58.394,-111.789,58.331,-111.809,58.303,-111.898,58.306,-112.08,58.347,-112.189,58.413,-112.237,58.46,-112.208,58.499,-112.211,58.55,-112.255,58.576,-112.315,58.563,-112.393,58.578,-112.447,58.618,-112.431,58.683,-112.337,58.778,-112.154,58.825,-112.041,58.819,-112.037,58.787,-112.113,58.778,-112.119,58.736,-111.985,58.713,-111.902,58.726,-111.844,58.721,-111.802,58.697,-111.766,58.685,-111.731,58.686,-111.717,58.659,-111.817,58.56]]]},{"n":"Smallwood","z":4,"b":[-66.621,53.421,-63.104,55.102],"p":[[[-63.176,54.129,-63.184,54.109,-63.203,54.074,-63.104,54.004,-63.137,53.973,-63.2,53.956,-63.247,53.966,-63.305,53.99,-63.417,54.006,-63.489,54.012,-63.555,54.006,-63.568,53.982,-63.529,53.955,-63.529,53.929,-63.652,53.925,-63.703,53.918,-63.77,53.902,-63.764,53.879,-63.827,53.858,-64.009,53.862,-64.124,53.896,-64.138,53.96,-64.176,54.012,-64.495,54.079,-64.525,54.11,-64.564,54.125,-64.652,54.114,-64.688,54.081,-64.642,54.031,-64.658,53.996,-64.694,53.961,-64.751,53.934,-64.836,53.931,-64.922,53.916,-65.014,53.888,-65.123,53.84,-65.177,53.818,-65.263,53.796,-65.298,53.76,-65.295,53.724,-65.191,53.674,-65.19,53.639,-65.148,53.564,-65.164,53.54,-65.128,53.5,-64.973,53.455,-65.02,53.421,-65.234,53.455,-65.529,53.49,-65.583,53.542,-65.48,53.549,-65.388,53.559,-65.335,53.587,-65.317,53.605,-65.326,53.636,-65.364,53.675,-65.381,53.715,-65.363,53.756,-65.328,53.821,-65.338,53.879,-65.407,53.904,-65.453,53.914,-65.507,53.95,-65.517,53.997,-65.583,54.061,-65.728,54.087,-65.826,54.135,-66.112,54.347,-66.254,54.426,-66.276,54.478,-66.364,54.541,-66.44,54.562,-66.429,54.508,-66.349,54.424,-66.377,54.375,-66.406,54.338,-66.387,54.263,-66.331,54.179,-66.324,54.154,-66.382,54.164,-66.491,54.273,-66.546,54.363,-66.577,54.424,-66.608,54.488,-66.616,54.521,-66.615,54.582,-66.621,54.623,-66.535,54.628,-66.508,54.654,-66.529,54.77,-66.509,54.802,-66.555,54.883,-66.6,54.922,-66.595,54.949,-66.567,54.958,-66.574,54.991,-66.598,55.029,-66.584,55.049,-66.553,55.054,-66.541,55.073,-66.556,55.102,-66.532,55.1,-66.449,55.052,-66.345,54.979,-66.349,54.952,-66.352,54.937,-66.351,54.914,-66.38,54.904,-66.42,54.901,-66.432,54.893,-66.428,54.881,-66.399,54.853,-66.412,54.838,-66.423,54.833,-66.429,54.813,-66.36,54.715,-66.354,54.687,-66.2,54.577,-66.191,54.525,-66.191,54.476,-66.023,54.346,-65.994,54.34,-65.938,54.271,-65.909,54.285,-65.872,54.281,-65.791,54.202,-65.713,54.163,-65.682,54.176,-65.503,54.055,-65.4,54.007,-65.345,54.043,-65.192,53.993,-65.103,54.012,-65.178,54.075,-65.13,54.083,-64.864,54.053,-64.816,54.076,-64.941,54.152,-65.011,54.224,-65.072,54.34,-65.165,54.435,-65.305,54.46,-65.302,54.475,-65.279,54.523,-65.271,54.608,-65.224,54.65,-65.157,54.656,-65.135,54.638,-65.218,54.557,-65.187,54.528,-65.14,54.511,-65.103,54.473,-65.059,54.396,-64.96,54.303,-64.98,54.289,-64.97,54.236,-64.92,54.171,-64.883,54.148,-64.879,54.16,-64.855,54.167,-64.763,54.144,-64.68,54.143,-64.617,54.175,-64.583,54.186,-64.51,54.189,-64.431,54.187,-64.333,54.166,-64.219,54.178,-64.234,54.211,-64.274,54.231,-64.284,54.256,-64.325,54.273,-64.407,54.256,-64.464,54.253,-64.48,54.273,-64.425,54.332,-64.4,54.511,-64.366,54.615,-64.378,54.679,-64.416,54.737,-64.397,54.743,-64.32,54.696,-64.296,54.659,-64.329,54.566,-64.32,54.517,-64.202,54.406,-64.138,54.35,-64.029,54.285,-63.947,54.243,-63.87,54.169,-63.748,54.096,-63.584,54.078,-63.465,54.098,-63.373,54.14,-63.319,54.143,-63.228,54.137,-63.176,54.129]]]},{"n":"Taymyr","z":4,"b":[99.507,73.838,105.333,75.16],"p":[[[101.023,73.86,100.949,73.894,101.026,73.936,101.104,73.992,101.062,74.095,100.968,74.196,100.9,74.256,100.768,74.292,100.607,74.296,100.5,74.308,100.412,74.337,100.218,74.347,100.079,74.343,99.957,74.331,99.71,74.34,99.585,74.36,99.507,74.4,99.607,74.42,99.83,74.407,99.972,74.409,100.171,74.44,100.232,74.482,100.351,74.529,100.495,74.577,100.501,74.631,100.434,74.722,100.318,74.8,100.097,74.861,100.132,74.896,100.266,74.936,100.419,74.948,100.463,75.007,100.285,75.079,100.042,75.103,100.027,75.125,100.033,75.148,100.109,75.16,100.192,75.15,100.301,75.127,100.435,75.087,100.537,75.073,100.612,75.036,100.616,74.982,100.544,74.932,100.534,74.873,100.573,74.823,100.707,74.757,100.841,74.72,101.059,74.681,101.806,74.641,102.484,74.641,103.253,74.676,103.493,74.71,103.762,74.763,104.032,74.795,104.234,74.814,104.209,74.859,104.028,74.885,103.878,74.928,103.912,74.985,103.991,75.009,104.047,75.021,104.117,75.055,104.129,75.088,104.146,75.112,104.264,75.121,104.467,75.093,104.628,75.012,104.864,74.915,105.307,74.855,105.333,74.814,105.232,74.785,104.973,74.772,104.508,74.74,103.999,74.651,103.92,74.633,103.843,74.629,103.742,74.633,103.569,74.593,103.204,74.568,102.833,74.54,102.464,74.516,102.041,74.502,101.986,74.413,102.073,74.322,102.19,74.24,102.161,74.233,102.059,74.231,101.979,74.236,101.848,74.258,101.691,74.29,101.426,74.356,101.313,74.404,101.331,74.439,101.366,74.479,101.213,74.491,101.049,74.484,100.856,74.463,100.882,74.425,100.981,74.412,101.103,74.387,101.1,74.33,101.156,74.246,101.277,74.143,101.439,74.043,101.529,73.953,101.553,73.878,101.533,73.841,101.435,73.838,101.377,73.844,101.125,73.849,101.023,73.86]]]},{"n":"Izabal","z":4.7,"b":[-89.388,15.382,-88.843,15.765],"p":[[[-88.878,15.712,-88.925,15.678,-88.97,15.636,-88.997,15.573,-89.022,15.489,-89.079,15.444,-89.182,15.417,-89.238,15.41,-89.278,15.395,-89.301,15.382,-89.326,15.412,-89.366,15.46,-89.388,15.488,-89.372,15.505,-89.317,15.522,-89.247,15.549,-89.183,15.575,-89.105,15.602,-88.971,15.688,-88.862,15.764,-88.843,15.765,-88.852,15.741,-88.878,15.712]]]},{"n":"Mogotoyevo","z":4.7,"b":[148.916,71.906,149.318,72.163],"p":[[[149.318,72.037,149.311,72.008,149.251,71.969,149.187,71.921,149.113,71.906,149.01,71.925,148.942,71.979,148.916,72.034,148.93,72.081,148.961,72.129,148.987,72.153,149.021,72.163,149.079,72.162,149.155,72.151,149.224,72.127,149.283,72.082,149.318,72.037]]]},{"n":"Ayakum","z":4.7,"b":[89.082,37.485,89.645,37.633],"p":[[[89.611,37.495,89.514,37.485,89.365,37.504,89.228,37.528,89.145,37.538,89.097,37.56,89.082,37.587,89.147,37.598,89.277,37.615,89.396,37.633,89.487,37.628,89.565,37.613,89.618,37.594,89.645,37.575,89.639,37.553,89.64,37.527,89.645,37.51,89.611,37.495]]]},{"n":"Storsjön","z":4.7,"b":[13.972,62.826,14.671,63.325],"p":[[[14.084,63.259,14.054,63.296,14.026,63.319,14.063,63.325,14.22,63.305,14.394,63.262,14.446,63.264,14.51,63.265,14.612,63.205,14.671,63.139,14.569,63.162,14.504,63.165,14.455,63.153,14.427,63.127,14.422,63.09,14.444,63.028,14.469,62.952,14.474,62.887,14.461,62.836,14.444,62.827,14.427,62.826,14.39,62.874,14.345,62.925,14.341,62.953,14.368,62.956,14.39,62.953,14.413,62.954,14.43,62.98,14.398,63.028,14.357,63.087,14.208,63.152,14.114,63.178,14.057,63.194,13.981,63.246,13.972,63.263,14.005,63.259,14.061,63.236,14.083,63.242,14.084,63.259]]]},{"n":"St. Joseph","z":4.7,"b":[-91.367,50.918,-90.427,51.215],"p":[[[-91.345,50.969,-91.302,51.035,-91.274,51.069,-91.214,51.066,-91.091,51.038,-90.976,51.011,-90.911,51.037,-90.844,51.073,-90.795,51.094,-90.745,51.12,-90.663,51.176,-90.604,51.215,-90.52,51.192,-90.472,51.17,-90.431,51.14,-90.427,51.119,-90.528,51.068,-90.747,51.014,-90.937,50.954,-91.12,50.946,-91.238,50.934,-91.318,50.918,-91.367,50.93,-91.345,50.969]]]},{"n":"Rukwa","z":4.7,"b":[31.794,-8.09,32.53,-7.585],"p":[[[32.514,-8.039,32.53,-8.07,32.521,-8.09,32.363,-8.033,32.171,-7.963,32.052,-7.971,31.96,-7.986,31.921,-7.959,31.873,-7.884,31.823,-7.783,31.794,-7.701,31.8,-7.653,31.834,-7.619,31.897,-7.585,32.026,-7.633,32.254,-7.826,32.448,-7.989,32.514,-8.039]]]},{"n":"Nam Ngum Dam","z":4.7,"b":[102.506,18.45,102.894,18.759],"p":[[[102.879,18.527,102.826,18.527,102.719,18.497,102.675,18.465,102.646,18.45,102.615,18.476,102.571,18.56,102.546,18.615,102.527,18.669,102.506,18.746,102.527,18.759,102.561,18.735,102.582,18.696,102.622,18.681,102.638,18.673,102.659,18.662,102.704,18.657,102.747,18.65,102.753,18.599,102.791,18.569,102.851,18.55,102.885,18.544,102.894,18.541,102.879,18.527]]]},{"n":"Gouin","z":4.7,"b":[-75.414,48.288,-74.1,48.802],"p":[[[-74.531,48.461,-74.545,48.439,-74.575,48.425,-74.596,48.418,-74.621,48.42,-74.638,48.414,-74.651,48.4,-74.677,48.364,-74.686,48.357,-74.692,48.382,-74.706,48.42,-74.722,48.434,-74.756,48.449,-74.816,48.512,-74.867,48.56,-74.911,48.575,-74.945,48.576,-74.964,48.556,-74.969,48.519,-74.96,48.499,-74.942,48.487,-74.945,48.454,-74.954,48.38,-74.96,48.364,-74.986,48.365,-75.023,48.359,-75.059,48.347,-75.082,48.341,-75.13,48.288,-75.14,48.289,-75.13,48.33,-75.115,48.414,-75.099,48.478,-75.073,48.495,-75.042,48.514,-75.008,48.564,-75.018,48.612,-75.07,48.596,-75.095,48.577,-75.129,48.555,-75.165,48.526,-75.182,48.479,-75.181,48.438,-75.174,48.414,-75.213,48.406,-75.269,48.408,-75.3,48.424,-75.336,48.437,-75.363,48.429,-75.387,48.444,-75.414,48.438,-75.389,48.483,-75.35,48.532,-75.302,48.58,-75.282,48.597,-75.25,48.598,-75.219,48.592,-75.181,48.602,-75.146,48.636,-75.109,48.675,-75.061,48.706,-75.042,48.712,-74.97,48.694,-74.921,48.71,-74.812,48.757,-74.762,48.773,-74.723,48.785,-74.63,48.801,-74.558,48.802,-74.543,48.778,-74.522,48.756,-74.544,48.704,-74.58,48.64,-74.555,48.608,-74.507,48.594,-74.485,48.58,-74.456,48.581,-74.412,48.589,-74.397,48.579,-74.406,48.547,-74.37,48.486,-74.314,48.449,-74.263,48.439,-74.223,48.426,-74.174,48.398,-74.118,48.388,-74.1,48.372,-74.151,48.36,-74.237,48.347,-74.312,48.377,-74.395,48.44,-74.463,48.471,-74.51,48.48,-74.531,48.461]]]},{"n":"Gaoyou","z":4.7,"b":[118.882,32.686,119.407,33.201],"p":[[[119.407,32.794,119.392,32.751,119.35,32.749,119.3,32.748,119.253,32.736,119.201,32.691,119.173,32.686,119.165,32.706,119.147,32.753,119.125,32.798,119.092,32.836,119.079,32.855,119.084,32.866,119.13,32.877,119.188,32.878,119.229,32.882,119.242,32.896,119.241,32.919,119.253,32.943,119.251,32.978,119.245,33.013,119.264,33.034,119.296,33.041,119.314,33.054,119.303,33.085,119.269,33.121,119.237,33.154,119.215,33.156,119.18,33.148,119.168,33.118,119.164,33.09,119.154,33.065,119.099,33.027,119.035,33.031,118.966,33.038,118.907,33.058,118.882,33.077,118.957,33.071,119.059,33.058,119.102,33.069,119.135,33.144,119.17,33.186,119.226,33.2,119.254,33.201,119.304,33.164,119.349,33.078,119.346,33.024,119.372,33,119.397,32.916,119.407,32.794]]]},{"n":"Pyasino","z":4.7,"b":[87.349,69.478,88.161,70.097],"p":[[[88.161,69.498,88.146,69.483,88.108,69.482,88.04,69.479,87.937,69.478,87.875,69.483,87.814,69.493,87.728,69.513,87.695,69.541,87.705,69.587,87.721,69.624,87.685,69.705,87.609,69.809,87.472,69.871,87.373,69.876,87.349,69.889,87.372,69.92,87.395,69.953,87.41,70.004,87.494,70.024,87.647,70.071,87.81,70.097,87.925,70.09,87.969,70.061,87.939,70.025,87.771,69.982,87.657,69.96,87.66,69.921,87.682,69.895,87.71,69.878,87.785,69.841,87.881,69.784,87.93,69.756,87.996,69.687,88.025,69.614,88.015,69.59,88.039,69.563,88.114,69.529,88.161,69.498]]]},{"n":"","z":4.7,"b":[26.276,61.558,27.045,62.037],"p":[[[27.037,61.662,26.998,61.664,26.938,61.67,26.863,61.696,26.795,61.703,26.743,61.694,26.77,61.666,26.837,61.648,26.879,61.626,26.884,61.594,26.886,61.581,26.89,61.569,26.87,61.564,26.834,61.573,26.811,61.571,26.796,61.558,26.778,61.576,26.758,61.612,26.665,61.681,26.554,61.758,26.468,61.791,26.406,61.809,26.35,61.834,26.29,61.854,26.276,61.866,26.305,61.9,26.351,61.935,26.412,61.976,26.442,61.962,26.428,61.945,26.391,61.905,26.376,61.87,26.418,61.863,26.464,61.875,26.491,61.89,26.524,61.9,26.54,61.915,26.527,61.928,26.531,61.939,26.575,61.937,26.603,61.917,26.631,61.886,26.668,61.868,26.684,61.872,26.686,61.881,26.692,61.888,26.713,61.886,26.744,61.871,26.774,61.861,26.796,61.871,26.81,61.887,26.809,61.902,26.764,61.913,26.698,61.931,26.644,61.968,26.591,62.022,26.588,62.037,26.64,62.011,26.705,61.993,26.755,61.975,26.819,61.948,26.873,61.902,26.881,61.871,26.877,61.855,26.894,61.797,26.9,61.753,26.927,61.73,26.983,61.712,27.028,61.704,27.043,61.688,27.045,61.671,27.037,61.662]]]},{"n":"Hjälmaren","z":4.7,"b":[15.344,59.149,16.406,59.317],"p":[[[16.404,59.309,16.406,59.305,16.356,59.288,16.31,59.275,16.13,59.24,15.921,59.194,15.892,59.176,15.881,59.162,15.816,59.149,15.701,59.151,15.624,59.167,15.62,59.192,15.59,59.225,15.545,59.246,15.52,59.255,15.481,59.261,15.344,59.27,15.354,59.283,15.5,59.302,15.613,59.317,15.728,59.298,15.822,59.295,15.857,59.304,15.99,59.286,16.187,59.288,16.271,59.3,16.334,59.303,16.404,59.309]]]},{"n":"","z":4.7,"b":[26.232,62.952,26.741,63.365],"p":[[[26.63,63.114,26.592,63.089,26.603,63.035,26.648,62.982,26.65,62.961,26.629,62.952,26.538,62.981,26.462,63.015,26.468,63.024,26.469,63.039,26.462,63.065,26.454,63.095,26.427,63.123,26.353,63.168,26.272,63.216,26.235,63.248,26.232,63.264,26.257,63.26,26.304,63.226,26.36,63.187,26.429,63.164,26.491,63.17,26.565,63.196,26.586,63.234,26.511,63.276,26.449,63.302,26.42,63.329,26.423,63.354,26.459,63.365,26.524,63.341,26.623,63.321,26.697,63.322,26.717,63.314,26.685,63.304,26.649,63.293,26.692,63.278,26.733,63.257,26.741,63.227,26.714,63.191,26.663,63.164,26.644,63.138,26.63,63.114]]]},{"n":"Bienville","z":4.7,"b":[-73.604,54.903,-72.225,55.281],"p":[[[-73.182,55.098,-73.154,55.113,-73.154,55.125,-73.067,55.147,-72.923,55.18,-72.832,55.191,-72.732,55.2,-72.647,55.213,-72.589,55.216,-72.517,55.221,-72.418,55.241,-72.305,55.272,-72.225,55.281,-72.25,55.262,-72.281,55.226,-72.286,55.193,-72.29,55.166,-72.358,55.126,-72.473,55.083,-72.629,55.049,-72.706,55.01,-72.789,54.998,-72.918,54.974,-73.045,54.946,-73.11,54.947,-73.149,54.964,-73.213,54.968,-73.313,54.947,-73.413,54.921,-73.555,54.903,-73.604,54.927,-73.511,54.942,-73.428,54.97,-73.409,54.987,-73.433,55.006,-73.485,55.022,-73.514,55.034,-73.511,55.048,-73.491,55.056,-73.434,55.051,-73.365,55.048,-73.272,55.07,-73.206,55.089,-73.182,55.098]]]},{"n":"Beloe","z":4.7,"b":[37.231,60.077,38.004,60.329],"p":[[[37.232,60.164,37.231,60.186,37.286,60.233,37.4,60.289,37.542,60.319,37.737,60.329,37.898,60.301,37.981,60.239,38.004,60.161,37.999,60.103,37.968,60.086,37.823,60.079,37.715,60.077,37.469,60.088,37.397,60.098,37.28,60.131,37.232,60.164]]]},{"n":"Brokopondo","z":4.7,"b":[-55.335,4.462,-54.796,4.999],"p":[[[-54.894,4.783,-54.925,4.747,-54.941,4.725,-54.938,4.699,-54.924,4.673,-54.892,4.665,-54.865,4.655,-54.864,4.626,-54.874,4.601,-54.9,4.594,-54.932,4.581,-54.952,4.596,-54.983,4.609,-55.013,4.599,-55.048,4.57,-55.064,4.533,-55.087,4.489,-55.123,4.462,-55.149,4.471,-55.19,4.484,-55.22,4.492,-55.23,4.514,-55.26,4.525,-55.299,4.524,-55.335,4.528,-55.329,4.574,-55.303,4.609,-55.279,4.621,-55.273,4.663,-55.284,4.699,-55.235,4.779,-55.173,4.879,-55.15,4.964,-55.127,4.999,-55.093,4.999,-55.02,4.971,-54.968,4.918,-54.938,4.904,-54.885,4.908,-54.84,4.912,-54.807,4.908,-54.796,4.881,-54.814,4.836,-54.838,4.789,-54.862,4.787,-54.894,4.783]]]},{"n":"Manyara","z":4.7,"b":[35.742,-3.757,35.882,-3.416],"p":[[[35.744,-3.636,35.762,-3.553,35.792,-3.48,35.822,-3.432,35.848,-3.416,35.869,-3.424,35.882,-3.452,35.869,-3.576,35.838,-3.701,35.806,-3.737,35.775,-3.757,35.753,-3.75,35.742,-3.707,35.744,-3.636]]]},{"n":"Abaya","z":4.7,"b":[37.696,6.014,38.143,6.589],"p":[[[38.055,6.578,38.102,6.576,38.138,6.577,38.143,6.56,38.129,6.519,38.116,6.473,38.107,6.449,38.087,6.423,38.058,6.38,38.014,6.307,37.944,6.202,37.875,6.108,37.843,6.073,37.802,6.049,37.739,6.014,37.701,6.015,37.696,6.044,37.709,6.088,37.745,6.136,37.79,6.172,37.826,6.189,37.865,6.22,37.887,6.263,37.875,6.36,37.863,6.455,37.869,6.491,37.936,6.548,38.017,6.589,38.055,6.578]]]},{"n":"Akkajaure","z":4.7,"b":[17.131,67.419,18.428,67.792],"p":[[[18.184,67.478,17.838,67.586,17.632,67.612,17.575,67.62,17.475,67.644,17.339,67.654,17.246,67.674,17.22,67.687,17.154,67.735,17.131,67.792,17.303,67.753,17.435,67.714,18.116,67.561,18.203,67.533,18.396,67.462,18.428,67.419,18.184,67.478]]]},{"n":"Balaton","z":4.7,"b":[17.269,46.721,18.164,47.055],"p":[[[17.586,46.766,17.476,46.724,17.277,46.721,17.269,46.742,17.271,46.76,17.309,46.763,17.383,46.779,17.479,46.8,17.665,46.852,17.824,46.911,17.881,46.943,17.955,46.987,18.039,47.037,18.078,47.055,18.13,47.039,18.164,47.011,18.143,46.963,17.881,46.866,17.672,46.791,17.586,46.766]]]},{"n":"Canandaigua","z":4.7,"b":[-77.364,42.689,-77.245,42.895],"p":[[[-77.262,42.815,-77.309,42.768,-77.354,42.689,-77.364,42.704,-77.361,42.734,-77.334,42.785,-77.291,42.836,-77.271,42.895,-77.245,42.876,-77.262,42.815]]]},{"n":"Owasco","z":4.7,"b":[-76.549,42.769,-76.468,42.922],"p":[[[-76.479,42.77,-76.527,42.836,-76.549,42.897,-76.536,42.922,-76.469,42.781,-76.468,42.769,-76.479,42.77]]]},{"n":"Skaneateles","z":4.7,"b":[-76.428,42.787,-76.272,42.96],"p":[[[-76.272,42.787,-76.362,42.85,-76.428,42.91,-76.425,42.96,-76.372,42.887,-76.293,42.833,-76.272,42.787]]]},{"n":"","z":4.7,"b":[-164.054,60.167,-163.468,60.439],"p":[[[-163.468,60.414,-163.529,60.393,-163.557,60.362,-163.577,60.342,-163.557,60.27,-163.629,60.214,-163.77,60.167,-163.775,60.178,-163.795,60.211,-163.784,60.259,-163.775,60.286,-163.793,60.298,-163.836,60.297,-163.937,60.285,-164.042,60.275,-164.054,60.295,-164.018,60.302,-163.901,60.333,-163.85,60.351,-163.881,60.369,-163.939,60.389,-163.893,60.408,-163.866,60.43,-163.852,60.433,-163.797,60.416,-163.734,60.431,-163.721,60.439,-163.66,60.433,-163.499,60.425,-163.468,60.414]]]},{"n":"Bear","z":4.7,"b":[-111.407,41.871,-111.282,42.152],"p":[[[-111.288,42.033,-111.308,41.949,-111.327,41.889,-111.349,41.871,-111.373,41.883,-111.394,41.937,-111.407,41.988,-111.403,42.062,-111.393,42.131,-111.344,42.152,-111.313,42.148,-111.285,42.109,-111.282,42.059,-111.288,42.033]]]},{"n":"Utah","z":4.7,"b":[-111.914,40.064,-111.68,40.377],"p":[[[-111.742,40.19,-111.827,40.158,-111.848,40.095,-111.867,40.07,-111.892,40.064,-111.894,40.081,-111.89,40.117,-111.899,40.154,-111.914,40.176,-111.852,40.252,-111.86,40.323,-111.881,40.377,-111.793,40.369,-111.763,40.337,-111.733,40.285,-111.701,40.238,-111.68,40.225,-111.69,40.206,-111.742,40.19]]]},{"n":"","z":4.7,"b":[-126.414,66.754,-126.129,67.021],"p":[[[-126.414,66.938,-126.399,66.983,-126.356,67.011,-126.29,67.021,-126.218,67.01,-126.158,66.979,-126.129,66.941,-126.149,66.909,-126.183,66.883,-126.191,66.855,-126.177,66.817,-126.189,66.796,-126.236,66.775,-126.275,66.754,-126.314,66.76,-126.369,66.784,-126.408,66.857,-126.414,66.938]]]},{"n":"Leech","z":4.7,"b":[-94.621,47.062,-94.245,47.285],"p":[[[-94.245,47.191,-94.26,47.15,-94.302,47.097,-94.346,47.075,-94.395,47.067,-94.433,47.077,-94.461,47.109,-94.519,47.143,-94.566,47.12,-94.57,47.075,-94.578,47.062,-94.586,47.079,-94.605,47.123,-94.621,47.166,-94.611,47.206,-94.589,47.202,-94.502,47.221,-94.437,47.232,-94.379,47.241,-94.296,47.285,-94.281,47.27,-94.297,47.221,-94.308,47.183,-94.278,47.186,-94.245,47.191]]]},{"n":"Mille Lacs","z":4.7,"b":[-90.688,48.785,-90.288,48.974],"p":[[[-90.292,48.935,-90.288,48.916,-90.314,48.878,-90.34,48.821,-90.375,48.786,-90.44,48.785,-90.458,48.798,-90.525,48.806,-90.601,48.811,-90.634,48.833,-90.632,48.867,-90.65,48.917,-90.684,48.92,-90.688,48.944,-90.666,48.967,-90.626,48.974,-90.591,48.96,-90.561,48.926,-90.546,48.895,-90.48,48.885,-90.4,48.886,-90.361,48.893,-90.324,48.916,-90.292,48.935]]]},{"n":"Baskatong","z":4.7,"b":[-76.033,46.715,-75.688,46.997],"p":[[[-75.694,46.897,-75.688,46.872,-75.698,46.825,-75.732,46.768,-75.751,46.718,-75.795,46.715,-75.864,46.755,-75.943,46.759,-76.017,46.747,-76.033,46.759,-76.01,46.778,-75.971,46.819,-75.958,46.859,-75.955,46.875,-75.89,46.889,-75.831,46.902,-75.779,46.964,-75.737,46.997,-75.727,46.974,-75.694,46.897]]]},{"n":"Ranco","z":4.7,"b":[-72.558,-40.302,-72.263,-40.128],"p":[[[-72.265,-40.26,-72.263,-40.292,-72.273,-40.302,-72.331,-40.299,-72.457,-40.293,-72.547,-40.272,-72.558,-40.235,-72.552,-40.201,-72.529,-40.172,-72.425,-40.135,-72.33,-40.128,-72.299,-40.15,-72.273,-40.166,-72.267,-40.192,-72.27,-40.221,-72.265,-40.26]]]},{"n":"Trout","z":4.7,"b":[-121.586,60.4,-121.091,60.771],"p":[[[-121.256,60.448,-121.355,60.408,-121.408,60.4,-121.429,60.414,-121.456,60.443,-121.497,60.459,-121.586,60.438,-121.566,60.464,-121.484,60.517,-121.389,60.56,-121.316,60.588,-121.301,60.623,-121.314,60.7,-121.336,60.771,-121.276,60.762,-121.176,60.722,-121.108,60.665,-121.091,60.592,-121.149,60.513,-121.256,60.448]]]},{"n":"Viedma","z":4.7,"b":[-73.03,-49.784,-72.101,-49.44],"p":[[[-72.306,-49.761,-72.49,-49.691,-72.621,-49.641,-72.71,-49.639,-72.753,-49.657,-72.825,-49.659,-72.943,-49.668,-73.029,-49.673,-73.03,-49.662,-73.002,-49.639,-72.973,-49.616,-72.969,-49.588,-72.991,-49.552,-73.018,-49.508,-72.985,-49.481,-72.93,-49.44,-72.878,-49.44,-72.814,-49.478,-72.52,-49.552,-72.261,-49.611,-72.211,-49.626,-72.132,-49.668,-72.101,-49.709,-72.101,-49.736,-72.114,-49.759,-72.136,-49.775,-72.184,-49.784,-72.306,-49.761]]]},{"n":"Tumba","z":4.7,"b":[17.811,-0.989,18.129,-0.652],"p":[[[18.036,-0.957,18.014,-0.931,17.982,-0.892,17.941,-0.852,17.917,-0.816,17.918,-0.771,17.901,-0.736,17.843,-0.713,17.817,-0.695,17.811,-0.672,17.833,-0.669,17.901,-0.665,17.983,-0.652,18.045,-0.657,18.089,-0.68,18.114,-0.708,18.121,-0.731,18.111,-0.782,18.107,-0.799,18.117,-0.86,18.126,-0.931,18.129,-0.981,18.102,-0.989,18.077,-0.971,18.057,-0.956,18.036,-0.957]]]},{"n":"","z":4.7,"b":[30.727,63.613,31.199,63.922],"p":[[[30.768,63.834,30.795,63.879,30.797,63.922,30.833,63.918,30.873,63.903,30.882,63.887,30.896,63.868,31.023,63.836,31.167,63.815,31.199,63.786,31.191,63.775,31.17,63.771,31.14,63.783,31.1,63.79,31.08,63.78,31.088,63.754,31.121,63.733,31.138,63.709,31.134,63.698,31.114,63.681,31.122,63.654,31.144,63.629,31.129,63.613,31.068,63.64,30.999,63.668,30.949,63.683,30.912,63.708,30.919,63.738,30.916,63.767,30.883,63.79,30.749,63.805,30.727,63.825,30.768,63.834]]]},{"n":"Po","z":4.7,"b":[116.314,30.078,116.54,30.245],"p":[[[116.424,30.108,116.401,30.118,116.387,30.143,116.373,30.161,116.351,30.175,116.317,30.203,116.314,30.23,116.335,30.234,116.357,30.245,116.371,30.244,116.39,30.224,116.443,30.207,116.472,30.204,116.514,30.208,116.539,30.207,116.54,30.18,116.527,30.149,116.525,30.116,116.526,30.089,116.506,30.078,116.494,30.085,116.468,30.104,116.424,30.108]]]},{"n":"Buir","z":4.7,"b":[117.529,47.666,117.869,47.961],"p":[[[117.581,47.686,117.558,47.71,117.529,47.739,117.531,47.776,117.57,47.835,117.638,47.901,117.709,47.948,117.764,47.961,117.801,47.95,117.841,47.91,117.869,47.865,117.856,47.825,117.806,47.772,117.729,47.718,117.655,47.68,117.606,47.666,117.586,47.671,117.581,47.686]]]},{"n":"Ang Laren","z":4.7,"b":[82.837,31.445,83.341,31.652],"p":[[[83.23,31.464,83.201,31.464,83.094,31.492,82.991,31.515,82.913,31.517,82.857,31.541,82.837,31.564,82.894,31.575,82.928,31.596,82.934,31.612,82.974,31.627,83.06,31.641,83.1,31.652,83.142,31.597,83.209,31.557,83.29,31.508,83.341,31.462,83.333,31.445,83.296,31.446,83.23,31.464]]]},{"n":"Tangra Yumco","z":4.7,"b":[86.396,30.795,86.777,31.367],"p":[[[86.777,31.166,86.764,31.136,86.704,31.123,86.628,31.084,86.607,31.034,86.565,30.943,86.523,30.858,86.494,30.811,86.457,30.795,86.419,30.807,86.397,30.84,86.396,30.871,86.412,30.894,86.445,30.952,86.516,31.058,86.607,31.221,86.654,31.367,86.742,31.342,86.765,31.256,86.777,31.166]]]},{"n":"Kulundinskoe","z":4.7,"b":[79.387,52.83,79.809,53.143],"p":[[[79.77,52.967,79.744,52.932,79.731,52.9,79.736,52.873,79.75,52.86,79.734,52.842,79.691,52.83,79.603,52.837,79.517,52.858,79.451,52.899,79.4,52.944,79.387,52.974,79.387,53.007,79.399,53.041,79.427,53.076,79.472,53.102,79.594,53.143,79.638,53.137,79.695,53.118,79.738,53.092,79.767,53.048,79.791,53.012,79.809,52.996,79.77,52.967]]]},{"n":"Siletiteniz","z":4.7,"b":[72.923,53.07,73.424,53.398],"p":[[[73.387,53.07,73.325,53.113,73.197,53.205,73.088,53.269,73.014,53.271,72.958,53.25,72.933,53.26,72.923,53.281,72.957,53.309,72.981,53.344,72.987,53.368,73.031,53.384,73.109,53.395,73.204,53.398,73.321,53.382,73.405,53.361,73.424,53.337,73.421,53.255,73.419,53.154,73.408,53.108,73.403,53.088,73.407,53.072,73.387,53.07]]]},{"n":"Ubinskoye","z":4.7,"b":[79.823,55.385,80.328,55.564],"p":[[[80.328,55.54,80.308,55.494,80.208,55.452,80.1,55.413,79.987,55.39,79.898,55.385,79.847,55.397,79.823,55.423,79.823,55.452,79.881,55.479,79.984,55.516,80.086,55.55,80.186,55.564,80.269,55.561,80.305,55.563,80.328,55.54]]]},{"n":"","z":4.7,"b":[38.647,61.195,38.917,61.467],"p":[[[38.651,61.344,38.675,61.39,38.708,61.421,38.797,61.452,38.894,61.467,38.917,61.445,38.884,61.381,38.858,61.323,38.875,61.289,38.895,61.255,38.886,61.229,38.837,61.207,38.761,61.195,38.697,61.199,38.663,61.217,38.647,61.273,38.651,61.344]]]},{"n":"","z":4.7,"b":[27.886,66.034,29.089,66.338],"p":[[[28.869,66.311,28.949,66.306,29.03,66.31,29.089,66.299,29.053,66.284,29.03,66.27,29.007,66.243,28.941,66.22,28.791,66.162,28.701,66.106,28.626,66.063,28.523,66.049,28.364,66.034,28.07,66.034,28.043,66.048,28.049,66.066,28.047,66.09,28.034,66.108,28.025,66.119,27.952,66.144,27.886,66.168,27.94,66.172,28.03,66.162,28.086,66.145,28.118,66.125,28.128,66.097,28.138,66.074,28.177,66.072,28.32,66.1,28.433,66.125,28.506,66.136,28.54,66.167,28.561,66.186,28.703,66.212,28.833,66.233,28.836,66.242,28.835,66.262,28.78,66.3,28.727,66.331,28.755,66.338,28.807,66.335,28.869,66.311]]]},{"n":"Umbozero","z":4.7,"b":[34.194,67.543,34.582,67.887],"p":[[[34.194,67.871,34.216,67.882,34.28,67.887,34.335,67.858,34.363,67.805,34.449,67.734,34.551,67.645,34.582,67.593,34.534,67.552,34.409,67.543,34.318,67.545,34.317,67.572,34.313,67.6,34.315,67.655,34.319,67.701,34.301,67.724,34.254,67.75,34.237,67.771,34.248,67.793,34.218,67.836,34.194,67.871]]]},{"n":"Kovdozero","z":4.7,"b":[31.554,66.425,32.397,66.912],"p":[[[32.315,66.502,32.3,66.486,32.304,66.481,32.325,66.466,32.366,66.458,32.381,66.454,32.397,66.445,32.39,66.438,32.355,66.425,32.224,66.453,32.12,66.471,31.991,66.471,31.978,66.49,31.983,66.499,32.107,66.519,32.135,66.538,32.119,66.564,32.028,66.634,31.963,66.7,31.901,66.741,31.736,66.75,31.577,66.758,31.561,66.769,31.554,66.783,31.576,66.799,31.609,66.813,31.621,66.822,31.708,66.823,31.764,66.838,31.765,66.858,31.701,66.873,31.68,66.885,31.746,66.912,31.861,66.886,31.948,66.841,32.087,66.845,32.218,66.835,32.239,66.816,32.203,66.779,32.18,66.761,32.196,66.745,32.249,66.731,32.31,66.706,32.329,66.69,32.339,66.674,32.311,66.652,32.255,66.624,32.22,66.601,32.242,66.566,32.306,66.525,32.315,66.502]]]},{"n":"Tekapo","z":4.7,"b":[170.5,-44.003,170.656,-43.534],"p":[[[170.656,-43.658,170.653,-43.681,170.618,-43.737,170.576,-43.851,170.575,-43.935,170.51,-44.003,170.508,-43.967,170.509,-43.896,170.512,-43.853,170.53,-43.816,170.5,-43.596,170.519,-43.534,170.531,-43.579,170.543,-43.675,170.559,-43.707,170.579,-43.718,170.6,-43.712,170.632,-43.678,170.656,-43.658]]]},{"n":"Bustakh","z":4.7,"b":[141.586,72.465,142.194,72.608],"p":[[[142.194,72.562,142.153,72.549,142.057,72.518,141.96,72.484,141.829,72.466,141.69,72.465,141.639,72.476,141.637,72.5,141.623,72.521,141.607,72.541,141.586,72.57,141.622,72.579,141.794,72.593,141.965,72.608,142.127,72.595,142.194,72.562]]]},{"n":"Yamdrok","z":4.7,"b":[90.386,28.786,91.003,29.211],"p":[[[90.931,28.923,90.89,28.934,90.867,28.927,90.856,28.928,90.835,28.933,90.808,28.938,90.768,28.914,90.752,28.875,90.739,28.866,90.715,28.883,90.67,28.864,90.631,28.838,90.59,28.797,90.566,28.787,90.535,28.786,90.508,28.804,90.479,28.835,90.454,28.86,90.447,28.878,90.456,28.887,90.474,28.885,90.492,28.87,90.51,28.859,90.53,28.861,90.561,28.874,90.594,28.903,90.621,28.939,90.647,28.975,90.672,29.011,90.664,29.047,90.659,29.089,90.654,29.126,90.63,29.151,90.59,29.167,90.534,29.165,90.477,29.145,90.424,29.111,90.388,29.093,90.386,29.107,90.424,29.138,90.503,29.188,90.562,29.211,90.633,29.192,90.692,29.161,90.729,29.13,90.726,29.109,90.719,29.094,90.721,29.072,90.73,29.052,90.768,29.012,90.788,28.991,90.829,28.989,90.844,29.013,90.876,29.024,90.919,29.021,90.963,29.003,90.992,28.978,90.988,28.951,91.003,28.904,90.993,28.892,90.956,28.912,90.931,28.923]]]},{"n":"Ozero Tenis","z":4.7,"b":[71.625,56.058,72.009,56.211],"p":[[[72.003,56.202,72.009,56.177,71.989,56.143,71.947,56.109,71.869,56.106,71.817,56.095,71.778,56.087,71.75,56.064,71.718,56.058,71.676,56.067,71.662,56.115,71.625,56.178,71.642,56.191,71.721,56.197,71.82,56.189,71.862,56.192,71.88,56.206,71.941,56.211,72.003,56.202]]]},{"n":"Ozero Chernoye","z":4.7,"b":[67.296,55.713,67.679,55.869],"p":[[[67.679,55.801,67.64,55.792,67.621,55.78,67.608,55.765,67.594,55.75,67.504,55.713,67.396,55.718,67.298,55.745,67.296,55.777,67.321,55.815,67.348,55.851,67.351,55.868,67.382,55.869,67.433,55.835,67.483,55.806,67.536,55.806,67.595,55.821,67.668,55.817,67.679,55.801]]]},{"n":"","z":4.7,"b":[32.704,65.762,33.087,66.044],"p":[[[32.91,65.975,32.931,65.957,32.974,65.94,33.055,65.93,33.087,65.909,33.081,65.879,33.076,65.852,33.072,65.833,32.946,65.794,32.8,65.762,32.743,65.769,32.736,65.787,32.734,65.805,32.704,65.83,32.778,65.858,32.842,65.876,32.849,65.894,32.838,65.913,32.795,65.929,32.745,65.948,32.751,65.975,32.796,66.007,32.811,66.033,32.836,66.044,32.898,66.043,32.891,66.023,32.863,65.998,32.871,65.983,32.91,65.975]]]},{"n":"Bolshoe Morskoe","z":4.7,"b":[158.546,69.99,158.934,70.138],"p":[[[158.934,70.092,158.913,70.084,158.883,70.073,158.845,70.036,158.819,70,158.792,69.99,158.752,69.994,158.642,70.029,158.573,70.066,158.546,70.099,158.558,70.124,158.61,70.138,158.709,70.138,158.854,70.122,158.925,70.119,158.934,70.092]]]},{"n":"Chukochye","z":4.7,"b":[160.025,69.352,160.445,69.539],"p":[[[160.025,69.456,160.066,69.468,160.099,69.485,160.068,69.514,160.055,69.538,160.104,69.539,160.22,69.519,160.325,69.489,160.367,69.462,160.413,69.441,160.445,69.43,160.424,69.42,160.3,69.417,160.206,69.411,160.249,69.386,160.278,69.359,160.248,69.352,160.152,69.369,160.056,69.394,160.033,69.41,160.047,69.428,160.044,69.446,160.025,69.456]]]},{"n":"Shijiu","z":4.7,"b":[118.723,31.393,118.964,31.554],"p":[[[118.78,31.409,118.768,31.419,118.747,31.459,118.743,31.492,118.724,31.522,118.723,31.542,118.758,31.545,118.867,31.546,118.941,31.554,118.964,31.503,118.96,31.465,118.915,31.435,118.85,31.393,118.815,31.402,118.78,31.409]]]},{"n":"Hong","z":4.7,"b":[113.23,29.749,113.454,29.953],"p":[[[113.451,29.94,113.454,29.914,113.426,29.876,113.391,29.826,113.369,29.785,113.34,29.76,113.312,29.749,113.275,29.78,113.237,29.848,113.23,29.893,113.249,29.904,113.302,29.888,113.331,29.894,113.362,29.949,113.406,29.953,113.451,29.94]]]},{"n":"Võrtsjärv","z":4.7,"b":[25.928,58.143,26.143,58.396],"p":[[[26.143,58.327,26.141,58.269,26.11,58.21,26.085,58.143,26.051,58.146,26.004,58.219,25.928,58.305,25.941,58.345,25.966,58.382,26.019,58.396,26.077,58.391,26.121,58.374,26.143,58.327]]]},{"n":"Chao","z":4.7,"b":[117.297,31.445,117.941,31.709],"p":[[[117.941,31.521,117.912,31.522,117.83,31.57,117.784,31.579,117.706,31.524,117.623,31.455,117.563,31.445,117.468,31.467,117.382,31.536,117.317,31.592,117.297,31.65,117.315,31.697,117.366,31.709,117.413,31.694,117.434,31.651,117.448,31.61,117.487,31.593,117.543,31.591,117.594,31.609,117.652,31.654,117.733,31.66,117.81,31.621,117.919,31.551,117.941,31.521]]]},{"n":"","z":4.7,"b":[12.899,63.427,13.358,63.726],"p":[[[12.899,63.652,12.93,63.686,13.012,63.712,13.117,63.726,13.209,63.724,13.273,63.711,13.3,63.695,13.276,63.686,13.16,63.69,13.11,63.671,13.085,63.65,13.097,63.608,13.119,63.554,13.213,63.486,13.358,63.451,13.335,63.427,13.243,63.452,13.179,63.471,13.113,63.497,13.01,63.544,12.936,63.6,12.899,63.652]]]},{"n":"Rossignol","z":4.7,"b":[-65.199,44.149,-64.964,44.272],"p":[[[-64.966,44.252,-64.964,44.238,-65.001,44.182,-65.025,44.164,-65.099,44.149,-65.176,44.155,-65.198,44.181,-65.199,44.208,-65.189,44.235,-65.167,44.257,-65.134,44.27,-65.104,44.272,-65.095,44.247,-65.08,44.228,-65.053,44.224,-65.01,44.23,-64.966,44.252]]]},{"n":"Foshay Lake","z":4.7,"b":[-66.19,45.856,-65.92,46.099],"p":[[[-66.106,45.871,-66.15,45.856,-66.19,45.881,-66.131,45.91,-66.077,45.953,-66.048,46.01,-66.027,46.05,-66.002,46.066,-65.97,46.082,-65.958,46.091,-65.942,46.099,-65.92,46.093,-65.92,46.064,-65.93,46.034,-65.95,46.006,-65.979,45.969,-65.989,45.942,-66.041,45.904,-66.106,45.871]]]},{"n":"Goose","z":4.7,"b":[-120.489,41.785,-120.323,42.107],"p":[[[-120.324,41.987,-120.35,41.923,-120.378,41.832,-120.396,41.792,-120.443,41.785,-120.466,41.803,-120.484,41.869,-120.489,41.988,-120.475,42.065,-120.449,42.088,-120.411,42.107,-120.375,42.09,-120.344,42.066,-120.323,42.04,-120.324,41.987]]]},{"n":"Pyramid","z":4.7,"b":[-119.696,39.868,-119.432,40.209],"p":[[[-119.432,39.903,-119.433,39.886,-119.446,39.868,-119.487,39.878,-119.547,39.927,-119.636,40.028,-119.696,40.123,-119.684,40.15,-119.641,40.191,-119.577,40.209,-119.534,40.181,-119.515,40.082,-119.483,39.974,-119.454,39.933,-119.432,39.903]]]},{"n":"Sevan","z":4.7,"b":[44.972,40.149,45.659,40.614],"p":[[[44.99,40.515,44.972,40.542,44.989,40.582,44.986,40.612,45.015,40.614,45.095,40.59,45.242,40.51,45.311,40.488,45.373,40.479,45.437,40.435,45.537,40.365,45.602,40.318,45.636,40.279,45.656,40.243,45.659,40.215,45.651,40.195,45.608,40.172,45.543,40.156,45.481,40.156,45.389,40.151,45.313,40.149,45.276,40.164,45.248,40.197,45.218,40.296,45.183,40.385,45.153,40.404,45.125,40.428,45.095,40.461,45.04,40.49,44.99,40.515]]]},{"n":"","z":4.7,"b":[114.674,30.211,114.846,30.345],"p":[[[114.839,30.287,114.812,30.283,114.783,30.278,114.753,30.283,114.739,30.279,114.743,30.263,114.76,30.238,114.781,30.221,114.778,30.215,114.762,30.211,114.735,30.212,114.704,30.212,114.679,30.228,114.679,30.25,114.68,30.277,114.674,30.3,114.713,30.33,114.764,30.345,114.784,30.344,114.806,30.345,114.834,30.327,114.846,30.302,114.839,30.287]]]},{"n":"","z":4.7,"b":[114.584,30.608,114.786,30.751],"p":[[[114.601,30.662,114.622,30.656,114.64,30.66,114.649,30.674,114.646,30.715,114.674,30.724,114.703,30.735,114.719,30.751,114.746,30.738,114.781,30.701,114.786,30.663,114.756,30.65,114.696,30.613,114.622,30.608,114.584,30.65,114.586,30.663,114.601,30.662]]]},{"n":"Nansi","z":4.7,"b":[117.128,34.498,117.326,34.725],"p":[[[117.326,34.587,117.295,34.549,117.25,34.502,117.21,34.498,117.174,34.543,117.141,34.612,117.128,34.663,117.139,34.725,117.179,34.719,117.266,34.665,117.318,34.621,117.326,34.587]]]},{"n":"","z":4.7,"b":[114.482,30.339,114.756,30.512],"p":[[[114.601,30.344,114.553,30.384,114.499,30.418,114.482,30.431,114.495,30.439,114.533,30.435,114.575,30.405,114.601,30.399,114.631,30.42,114.633,30.44,114.629,30.456,114.607,30.483,114.609,30.496,114.64,30.486,114.666,30.49,114.678,30.497,114.701,30.512,114.739,30.494,114.756,30.477,114.752,30.463,114.748,30.443,114.753,30.429,114.727,30.42,114.683,30.397,114.655,30.346,114.623,30.339,114.601,30.344]]]},{"n":"Futou Lake","z":4.7,"b":[114.143,29.901,114.301,30.141],"p":[[[114.301,30.025,114.289,30.002,114.252,29.972,114.244,29.938,114.264,29.915,114.255,29.906,114.218,29.901,114.171,29.921,114.143,29.988,114.147,30.083,114.177,30.114,114.218,30.141,114.249,30.141,114.246,30.115,114.237,30.084,114.253,30.045,114.273,30.044,114.294,30.043,114.301,30.025]]]},{"n":"","z":4.7,"b":[113.941,29.862,114.158,30.027],"p":[[[114.158,29.872,114.137,29.862,114.114,29.883,114.087,29.934,114.063,29.927,114.029,29.898,113.99,29.879,113.969,29.88,113.963,29.892,113.984,29.916,114.003,29.942,113.996,29.959,113.97,29.963,113.944,29.966,113.941,29.981,114.019,30.001,114.074,30.027,114.101,30.022,114.13,29.935,114.152,29.89,114.158,29.872]]]},{"n":"Millerton","z":4.7,"b":[-119.693,37,-119.598,37.057],"p":[[[-119.603,37.057,-119.598,37.051,-119.603,37.031,-119.641,37.008,-119.693,37,-119.672,37.028,-119.623,37.046,-119.603,37.057]]]},{"n":"Bluenose","z":4.7,"b":[-119.875,68.246,-119.47,68.755],"p":[[[-119.875,68.558,-119.848,68.583,-119.806,68.619,-119.809,68.642,-119.825,68.661,-119.786,68.684,-119.744,68.719,-119.72,68.746,-119.691,68.755,-119.665,68.754,-119.663,68.744,-119.625,68.715,-119.613,68.698,-119.572,68.672,-119.526,68.658,-119.486,68.633,-119.47,68.608,-119.487,68.595,-119.552,68.594,-119.595,68.574,-119.621,68.532,-119.65,68.471,-119.642,68.44,-119.59,68.396,-119.511,68.361,-119.525,68.332,-119.646,68.289,-119.734,68.255,-119.817,68.246,-119.848,68.254,-119.844,68.321,-119.833,68.421,-119.835,68.484,-119.859,68.529,-119.875,68.558]]]},{"n":"Pipmuacan","z":4.7,"b":[-70.963,49.333,-69.783,49.887],"p":[[[-70.08,49.44,-70.137,49.491,-70.14,49.53,-70.137,49.565,-70.157,49.592,-70.195,49.617,-70.242,49.635,-70.283,49.641,-70.324,49.624,-70.351,49.59,-70.335,49.54,-70.329,49.523,-70.354,49.504,-70.406,49.519,-70.431,49.532,-70.45,49.537,-70.467,49.532,-70.487,49.499,-70.505,49.461,-70.508,49.427,-70.516,49.411,-70.539,49.439,-70.571,49.473,-70.639,49.466,-70.677,49.448,-70.701,49.427,-70.736,49.442,-70.756,49.488,-70.789,49.536,-70.816,49.543,-70.834,49.542,-70.855,49.525,-70.892,49.457,-70.94,49.408,-70.963,49.406,-70.921,49.497,-70.859,49.599,-70.834,49.625,-70.826,49.616,-70.792,49.596,-70.744,49.56,-70.716,49.525,-70.667,49.531,-70.585,49.572,-70.514,49.63,-70.471,49.672,-70.442,49.684,-70.411,49.682,-70.34,49.67,-70.305,49.701,-70.307,49.748,-70.358,49.832,-70.377,49.887,-70.339,49.884,-70.317,49.864,-70.289,49.813,-70.259,49.75,-70.236,49.699,-70.206,49.682,-70.171,49.684,-70.135,49.708,-70.1,49.69,-70.073,49.643,-70.017,49.597,-69.951,49.555,-69.899,49.545,-69.844,49.472,-69.787,49.37,-69.783,49.339,-69.825,49.333,-69.856,49.342,-69.866,49.366,-69.889,49.407,-69.904,49.451,-69.902,49.481,-69.917,49.504,-69.946,49.518,-69.982,49.517,-70.029,49.498,-70.064,49.475,-70.069,49.459,-70.08,49.44]]]},{"n":"Grand Lake","z":4.7,"b":[-58.013,48.643,-56.928,49.336],"p":[[[-57.035,49.265,-57.011,49.297,-56.99,49.332,-56.963,49.336,-56.944,49.304,-56.928,49.264,-56.97,49.227,-57.025,49.213,-57.053,49.215,-57.13,49.166,-57.24,49.056,-57.435,48.907,-57.513,48.858,-57.578,48.786,-57.76,48.672,-57.859,48.643,-57.947,48.658,-58.013,48.663,-57.965,48.685,-57.78,48.774,-57.665,48.859,-57.562,48.894,-57.371,49.011,-57.27,49.099,-57.262,49.147,-57.262,49.181,-57.221,49.189,-57.115,49.237,-57.035,49.265]]]},{"n":"","z":4.7,"b":[-126.762,67.256,-126.191,67.578],"p":[[[-126.357,67.271,-126.419,67.256,-126.495,67.263,-126.514,67.32,-126.536,67.398,-126.548,67.442,-126.584,67.471,-126.6,67.495,-126.613,67.522,-126.691,67.535,-126.762,67.56,-126.73,67.573,-126.614,67.578,-126.494,67.552,-126.398,67.488,-126.298,67.444,-126.231,67.434,-126.205,67.419,-126.198,67.403,-126.208,67.382,-126.208,67.358,-126.191,67.33,-126.206,67.304,-126.357,67.271]]]},{"n":"Colville","z":4.7,"b":[-126.241,67.059,-125.73,67.313],"p":[[[-125.751,67.313,-125.73,67.297,-125.756,67.238,-125.793,67.177,-125.828,67.124,-125.918,67.079,-126.061,67.059,-126.166,67.065,-126.222,67.085,-126.224,67.104,-126.196,67.115,-126.184,67.138,-126.191,67.169,-126.216,67.182,-126.241,67.199,-126.235,67.215,-126.033,67.254,-125.793,67.305,-125.751,67.313]]]},{"n":"Hottah","z":4.7,"b":[-118.832,64.781,-118.146,65.291],"p":[[[-118.666,64.999,-118.682,65.03,-118.681,65.061,-118.677,65.081,-118.698,65.109,-118.767,65.158,-118.832,65.192,-118.825,65.217,-118.733,65.245,-118.659,65.262,-118.552,65.279,-118.445,65.291,-118.423,65.281,-118.395,65.261,-118.365,65.245,-118.303,65.227,-118.265,65.199,-118.246,65.136,-118.279,65.09,-118.282,65.069,-118.275,65.043,-118.248,65.014,-118.237,64.986,-118.248,64.956,-118.276,64.929,-118.325,64.903,-118.319,64.873,-118.239,64.845,-118.156,64.852,-118.149,64.843,-118.146,64.795,-118.176,64.786,-118.333,64.781,-118.488,64.841,-118.578,64.884,-118.636,64.928,-118.664,64.97,-118.666,64.999]]]},{"n":"Big Trout","z":4.7,"b":[-90.357,53.679,-89.609,53.886],"p":[[[-90.357,53.766,-90.344,53.789,-90.321,53.835,-90.29,53.868,-90.252,53.87,-90.227,53.856,-90.201,53.841,-90.171,53.838,-90.145,53.854,-90.101,53.871,-90.046,53.881,-90.002,53.886,-89.928,53.863,-89.862,53.834,-89.847,53.815,-89.729,53.791,-89.609,53.772,-89.678,53.733,-89.814,53.69,-89.939,53.679,-90.034,53.685,-90.103,53.717,-90.167,53.747,-90.275,53.757,-90.357,53.766]]]},{"n":"Trout","z":4.7,"b":[-93.481,51.033,-93.072,51.302],"p":[[[-93.165,51.071,-93.182,51.055,-93.2,51.039,-93.235,51.033,-93.32,51.075,-93.429,51.154,-93.481,51.208,-93.48,51.237,-93.455,51.264,-93.406,51.292,-93.37,51.302,-93.344,51.286,-93.283,51.279,-93.215,51.299,-93.175,51.295,-93.141,51.273,-93.123,51.241,-93.101,51.207,-93.072,51.173,-93.074,51.159,-93.137,51.123,-93.165,51.071]]]},{"n":"Island","z":4.7,"b":[-94.949,53.71,-94.002,53.989],"p":[[[-94.236,53.734,-94.351,53.73,-94.446,53.72,-94.518,53.71,-94.553,53.713,-94.682,53.733,-94.855,53.786,-94.933,53.827,-94.949,53.859,-94.938,53.902,-94.933,53.933,-94.889,53.946,-94.851,53.956,-94.822,53.975,-94.754,53.989,-94.634,53.986,-94.601,53.972,-94.621,53.945,-94.686,53.915,-94.689,53.897,-94.654,53.876,-94.59,53.858,-94.477,53.864,-94.346,53.874,-94.229,53.883,-94.131,53.892,-94.048,53.882,-94.002,53.847,-94.014,53.798,-94.103,53.759,-94.18,53.742,-94.236,53.734]]]},{"n":"Naococane","z":4.7,"b":[-70.982,52.61,-70.397,53.06],"p":[[[-70.509,52.996,-70.486,52.983,-70.437,52.976,-70.397,52.974,-70.442,52.941,-70.476,52.901,-70.484,52.872,-70.464,52.863,-70.467,52.841,-70.474,52.813,-70.468,52.746,-70.444,52.677,-70.422,52.639,-70.433,52.61,-70.487,52.63,-70.522,52.682,-70.519,52.725,-70.522,52.761,-70.521,52.785,-70.538,52.8,-70.577,52.803,-70.592,52.789,-70.603,52.759,-70.64,52.736,-70.656,52.751,-70.703,52.775,-70.762,52.787,-70.78,52.809,-70.777,52.863,-70.783,52.899,-70.817,52.909,-70.847,52.878,-70.899,52.848,-70.94,52.817,-70.965,52.819,-70.981,52.841,-70.982,52.872,-70.952,52.907,-70.924,52.938,-70.899,52.94,-70.838,52.943,-70.768,52.956,-70.75,52.963,-70.741,52.981,-70.748,52.996,-70.771,53.008,-70.828,53.015,-70.831,53.028,-70.833,53.043,-70.82,53.06,-70.75,53.038,-70.691,52.998,-70.652,52.99,-70.599,52.988,-70.548,53.007,-70.527,53.007,-70.509,52.996]]]},{"n":"Ronge","z":4.7,"b":[-105.258,54.925,-104.369,55.307],"p":[[[-104.396,55.16,-104.455,55.138,-104.507,55.133,-104.591,55.154,-104.637,55.148,-104.69,55.126,-104.735,55.09,-104.79,55.034,-104.822,54.994,-104.901,54.977,-105.026,54.947,-105.128,54.925,-105.194,54.936,-105.237,54.957,-105.245,55.002,-105.247,55.048,-105.258,55.074,-105.233,55.117,-105.218,55.156,-105.224,55.179,-105.229,55.2,-105.237,55.219,-105.205,55.248,-105.138,55.283,-105.081,55.302,-105.06,55.298,-104.955,55.28,-104.833,55.306,-104.808,55.301,-104.798,55.292,-104.755,55.302,-104.714,55.296,-104.674,55.291,-104.621,55.307,-104.59,55.302,-104.574,55.281,-104.519,55.257,-104.418,55.254,-104.4,55.247,-104.369,55.182,-104.396,55.16]]]},{"n":"Mille Lacs","z":4.7,"b":[-93.795,46.136,-93.514,46.36],"p":[[[-93.534,46.249,-93.521,46.21,-93.514,46.177,-93.54,46.15,-93.6,46.136,-93.668,46.137,-93.726,46.158,-93.775,46.2,-93.795,46.255,-93.758,46.308,-93.679,46.345,-93.607,46.36,-93.569,46.357,-93.552,46.341,-93.542,46.3,-93.538,46.269,-93.534,46.249]]]},{"n":"Winnipesaukee","z":4.7,"b":[-71.443,43.532,-71.214,43.725],"p":[[[-71.293,43.635,-71.262,43.609,-71.216,43.587,-71.214,43.569,-71.232,43.54,-71.277,43.532,-71.371,43.598,-71.443,43.628,-71.436,43.656,-71.43,43.679,-71.409,43.695,-71.394,43.711,-71.399,43.725,-71.374,43.722,-71.331,43.7,-71.306,43.667,-71.293,43.635]]]},{"n":"Simcoe","z":4.7,"b":[-79.584,44.227,-79.163,44.698],"p":[[[-79.448,44.31,-79.479,44.27,-79.48,44.236,-79.499,44.227,-79.519,44.274,-79.537,44.353,-79.574,44.393,-79.584,44.405,-79.567,44.431,-79.503,44.463,-79.445,44.51,-79.413,44.562,-79.393,44.603,-79.387,44.641,-79.373,44.698,-79.354,44.69,-79.341,44.636,-79.265,44.563,-79.211,44.535,-79.177,44.491,-79.163,44.45,-79.172,44.419,-79.202,44.384,-79.342,44.343,-79.448,44.31]]]},{"n":"Gods","z":4.7,"b":[-94.768,54.412,-93.645,54.85],"p":[[[-93.923,54.835,-93.777,54.808,-93.69,54.792,-93.645,54.788,-93.647,54.779,-93.702,54.758,-93.835,54.758,-93.912,54.744,-93.934,54.709,-93.989,54.671,-94.157,54.636,-94.291,54.618,-94.34,54.607,-94.414,54.586,-94.45,54.542,-94.465,54.521,-94.495,54.502,-94.551,54.48,-94.675,54.44,-94.763,54.412,-94.767,54.427,-94.768,54.454,-94.758,54.476,-94.735,54.5,-94.65,54.541,-94.558,54.623,-94.508,54.669,-94.489,54.724,-94.476,54.786,-94.477,54.801,-94.437,54.805,-94.369,54.819,-94.272,54.842,-94.11,54.85,-94.012,54.847,-93.923,54.835]]]},{"n":"Hoh Xil Hu","z":4.7,"b":[90.95,35.528,91.316,35.653],"p":[[[91.316,35.595,91.303,35.574,91.286,35.544,91.268,35.528,91.165,35.547,91.043,35.565,90.979,35.609,90.95,35.634,90.953,35.645,90.993,35.653,91.033,35.65,91.115,35.637,91.199,35.634,91.222,35.646,91.271,35.629,91.316,35.595]]]},{"n":"Hammar","z":4.7,"b":[46.712,30.663,47.608,30.917],"p":[[[47.608,30.689,47.573,30.664,47.491,30.663,47.407,30.678,47.245,30.711,47.104,30.74,47.04,30.742,46.953,30.733,46.879,30.727,46.852,30.722,46.822,30.725,46.807,30.738,46.804,30.764,46.802,30.786,46.792,30.803,46.765,30.817,46.75,30.823,46.719,30.837,46.712,30.867,46.748,30.903,46.801,30.913,46.818,30.915,46.861,30.917,46.897,30.9,46.926,30.892,46.983,30.857,47.063,30.828,47.103,30.826,47.146,30.814,47.194,30.8,47.29,30.772,47.357,30.782,47.429,30.812,47.472,30.814,47.5,30.809,47.504,30.797,47.53,30.768,47.59,30.713,47.608,30.689]]]},{"n":"Malheur","z":4.7,"b":[-118.948,43.24,-118.612,43.425],"p":[[[-118.68,43.29,-118.721,43.291,-118.766,43.258,-118.821,43.24,-118.864,43.263,-118.903,43.303,-118.935,43.354,-118.948,43.394,-118.929,43.414,-118.791,43.425,-118.664,43.405,-118.633,43.344,-118.612,43.298,-118.623,43.282,-118.643,43.281,-118.654,43.284,-118.68,43.29]]]},{"n":"","z":4.7,"b":[-56.712,48.182,-56.466,48.363],"p":[[[-56.483,48.3,-56.586,48.232,-56.712,48.182,-56.689,48.219,-56.639,48.266,-56.604,48.3,-56.587,48.321,-56.56,48.352,-56.524,48.363,-56.494,48.346,-56.466,48.329,-56.483,48.3]]]},{"n":"","z":4.7,"b":[-95.918,55.419,-95.6,55.615],"p":[[[-95.6,55.615,-95.635,55.576,-95.683,55.52,-95.714,55.477,-95.769,55.442,-95.861,55.421,-95.918,55.419,-95.913,55.436,-95.906,55.461,-95.896,55.487,-95.817,55.526,-95.795,55.569,-95.699,55.596,-95.6,55.615]]]},{"n":"","z":4.7,"b":[-165.898,65.08,-165.376,65.205],"p":[[[-165.382,65.205,-165.376,65.193,-165.403,65.171,-165.482,65.134,-165.597,65.097,-165.708,65.08,-165.792,65.091,-165.888,65.138,-165.898,65.165,-165.852,65.182,-165.82,65.191,-165.655,65.188,-165.455,65.196,-165.382,65.205]]]},{"n":"","z":4.7,"b":[-163.936,61.055,-163.547,61.231],"p":[[[-163.798,61.144,-163.783,61.129,-163.781,61.106,-163.801,61.076,-163.816,61.055,-163.842,61.055,-163.858,61.077,-163.897,61.115,-163.936,61.145,-163.931,61.169,-163.889,61.196,-163.826,61.217,-163.76,61.219,-163.633,61.226,-163.547,61.231,-163.591,61.211,-163.628,61.198,-163.709,61.174,-163.798,61.144]]]},{"n":"","z":4.7,"b":[-162.768,60.76,-162.502,60.843],"p":[[[-162.508,60.795,-162.502,60.772,-162.527,60.763,-162.573,60.76,-162.666,60.777,-162.75,60.801,-162.768,60.818,-162.755,60.832,-162.714,60.842,-162.655,60.843,-162.573,60.826,-162.508,60.795]]]},{"n":"Tikchik","z":4.7,"b":[-159.058,59.164,-158.863,59.337],"p":[[[-158.863,59.201,-158.875,59.166,-158.913,59.164,-158.941,59.18,-158.951,59.208,-158.959,59.246,-158.998,59.292,-159.058,59.337,-158.945,59.314,-158.878,59.252,-158.865,59.221,-158.863,59.201]]]},{"n":"Tikchik","z":4.7,"b":[-159.132,59.896,-158.223,60.016],"p":[[[-158.223,59.943,-158.257,59.935,-158.381,59.949,-158.535,59.961,-158.705,59.934,-158.826,59.911,-158.971,59.896,-159.024,59.896,-159.092,59.9,-159.108,59.913,-159.12,59.934,-159.132,59.951,-159.1,59.952,-159,59.948,-158.759,59.98,-158.542,60.016,-158.486,60.012,-158.423,59.983,-158.307,59.98,-158.242,59.963,-158.223,59.943]]]},{"n":"Tikchik","z":4.7,"b":[-159.159,60.19,-158.774,60.283],"p":[[[-159.159,60.196,-159.132,60.22,-159.058,60.242,-158.919,60.273,-158.853,60.283,-158.874,60.264,-158.831,60.246,-158.778,60.23,-158.774,60.216,-158.774,60.201,-158.781,60.19,-158.913,60.201,-159.083,60.212,-159.149,60.196,-159.159,60.196]]]},{"n":"Tikchik","z":4.7,"b":[-159.133,59.425,-158.582,59.653],"p":[[[-158.708,59.564,-158.825,59.543,-158.835,59.518,-158.804,59.491,-158.663,59.465,-158.605,59.443,-158.582,59.425,-158.633,59.427,-158.757,59.438,-158.805,59.445,-158.869,59.481,-158.992,59.516,-159.046,59.547,-159.083,59.594,-159.133,59.651,-159.094,59.653,-158.945,59.586,-158.892,59.59,-158.794,59.603,-158.647,59.596,-158.708,59.564]]]},{"n":"Khyargas Nuur","z":4.7,"b":[92.832,48.991,93.799,49.322],"p":[[[93.799,49.129,93.779,49.103,93.678,49.07,93.536,49.034,93.45,49.009,93.397,48.991,93.322,49.037,93.241,49.097,93.179,49.112,93.104,49.139,92.975,49.187,92.865,49.225,92.832,49.253,92.835,49.282,92.864,49.306,92.94,49.315,93.059,49.322,93.26,49.292,93.421,49.226,93.471,49.205,93.516,49.213,93.56,49.206,93.654,49.215,93.723,49.19,93.786,49.158,93.799,49.129]]]},{"n":"","z":4.7,"b":[85.352,30.759,85.889,31.045],"p":[[[85.888,30.929,85.851,30.886,85.845,30.827,85.827,30.783,85.802,30.759,85.783,30.759,85.786,30.789,85.757,30.84,85.692,30.87,85.64,30.869,85.6,30.855,85.568,30.842,85.513,30.846,85.438,30.871,85.377,30.906,85.352,30.944,85.361,30.992,85.402,31.035,85.476,31.045,85.561,31.028,85.647,31.025,85.725,31.028,85.81,31.005,85.875,30.978,85.889,30.952,85.888,30.929]]]},{"n":"Assal","z":4.7,"b":[42.396,11.629,42.447,11.712],"p":[[[42.447,11.677,42.441,11.64,42.417,11.629,42.399,11.645,42.396,11.672,42.399,11.693,42.411,11.712,42.436,11.697,42.447,11.677]]]},{"n":"Rainy","z":4.7,"b":[-93.613,48.351,-92.435,48.934],"p":[[[-92.918,48.433,-92.995,48.451,-93.05,48.48,-93.071,48.504,-93.056,48.513,-92.982,48.491,-92.865,48.462,-92.788,48.455,-92.745,48.477,-92.704,48.505,-92.698,48.517,-92.813,48.54,-92.936,48.565,-92.95,48.576,-92.948,48.586,-93.001,48.593,-93.079,48.591,-93.109,48.581,-93.1,48.564,-93.117,48.553,-93.162,48.558,-93.196,48.579,-93.279,48.607,-93.339,48.6,-93.382,48.616,-93.37,48.638,-93.37,48.657,-93.401,48.681,-93.403,48.705,-93.378,48.71,-93.352,48.717,-93.365,48.748,-93.417,48.785,-93.496,48.813,-93.579,48.826,-93.613,48.834,-93.583,48.843,-93.491,48.839,-93.434,48.846,-93.439,48.869,-93.45,48.887,-93.463,48.903,-93.408,48.915,-93.345,48.929,-93.322,48.934,-93.294,48.885,-93.262,48.825,-93.227,48.805,-93.203,48.791,-93.201,48.767,-93.2,48.746,-93.229,48.718,-93.265,48.686,-93.261,48.674,-93.245,48.667,-93.223,48.67,-93.171,48.701,-93.124,48.728,-93.135,48.706,-93.149,48.675,-93.119,48.668,-93.015,48.683,-92.979,48.688,-92.949,48.681,-92.896,48.683,-92.793,48.695,-92.779,48.678,-92.853,48.646,-92.87,48.635,-92.868,48.625,-92.866,48.615,-92.808,48.604,-92.732,48.588,-92.685,48.591,-92.655,48.6,-92.641,48.587,-92.603,48.565,-92.585,48.534,-92.583,48.51,-92.57,48.5,-92.6,48.485,-92.567,48.468,-92.491,48.456,-92.47,48.44,-92.449,48.424,-92.435,48.391,-92.441,48.357,-92.456,48.351,-92.478,48.366,-92.52,48.399,-92.6,48.424,-92.764,48.43,-92.918,48.433]]]},{"n":"Williston","z":4.7,"b":[-124.836,55.176,-122.222,56.757],"p":[[[-122.502,56.084,-122.603,56.09,-122.7,56.077,-122.819,56.061,-122.925,56.058,-123.012,56.04,-123.139,55.974,-123.429,55.985,-123.716,56.015,-123.782,55.995,-123.79,55.942,-123.774,55.887,-123.76,55.863,-123.742,55.854,-123.715,55.848,-123.687,55.802,-123.657,55.752,-123.614,55.728,-123.536,55.678,-123.449,55.623,-123.387,55.572,-123.303,55.49,-123.232,55.425,-123.177,55.402,-123.169,55.393,-123.169,55.358,-123.173,55.33,-123.169,55.291,-123.159,55.235,-123.147,55.191,-123.144,55.176,-123.172,55.199,-123.237,55.3,-123.325,55.434,-123.415,55.534,-123.506,55.613,-123.638,55.684,-123.809,55.794,-123.883,55.813,-123.921,55.829,-123.996,55.893,-124.028,55.906,-124.04,55.913,-124.049,55.922,-124.043,55.943,-124.027,55.972,-124.033,56,-124.1,56.014,-124.21,56.014,-124.385,56.064,-124.508,56.114,-124.482,56.115,-124.354,56.081,-124.215,56.053,-124.166,56.067,-124.158,56.087,-124.238,56.146,-124.345,56.21,-124.481,56.361,-124.834,56.713,-124.836,56.757,-124.759,56.742,-124.661,56.658,-124.562,56.566,-124.459,56.471,-124.397,56.401,-124.375,56.368,-124.359,56.345,-124.312,56.31,-124.249,56.263,-124.194,56.24,-124.142,56.237,-124.073,56.255,-124.053,56.223,-124.073,56.154,-124.038,56.1,-123.97,56.061,-123.788,56.072,-123.606,56.061,-123.458,56.049,-123.238,56.029,-123.118,56.048,-123.091,56.076,-122.971,56.116,-122.832,56.13,-122.757,56.139,-122.638,56.159,-122.566,56.158,-122.423,56.128,-122.264,56.084,-122.223,56.058,-122.222,56.021,-122.293,56.023,-122.345,56.029,-122.435,56.055,-122.502,56.084]]]},{"n":"Split","z":4.7,"b":[-96.572,56.063,-95.842,56.302],"p":[[[-96.049,56.136,-96.081,56.118,-96.088,56.1,-96.073,56.085,-96.086,56.079,-96.173,56.085,-96.245,56.092,-96.358,56.094,-96.452,56.087,-96.553,56.063,-96.572,56.086,-96.504,56.15,-96.371,56.165,-96.279,56.177,-96.205,56.22,-96.054,56.273,-95.906,56.302,-95.842,56.291,-95.865,56.265,-95.933,56.246,-96.036,56.222,-96.105,56.203,-96.075,56.182,-96.044,56.156,-96.049,56.136]]]},{"n":"Fort Peck","z":4.7,"b":[-108.196,47.39,-106.217,48.027],"p":[[[-106.217,47.585,-106.251,47.55,-106.269,47.548,-106.252,47.6,-106.251,47.644,-106.285,47.697,-106.318,47.755,-106.336,47.785,-106.36,47.813,-106.384,47.848,-106.42,47.876,-106.454,47.879,-106.48,47.858,-106.521,47.828,-106.55,47.785,-106.556,47.75,-106.599,47.753,-106.683,47.739,-106.768,47.724,-106.813,47.716,-106.838,47.694,-106.85,47.649,-106.862,47.631,-106.879,47.649,-106.906,47.672,-106.944,47.667,-106.993,47.644,-107.015,47.645,-107.03,47.666,-107.183,47.685,-107.306,47.685,-107.333,47.685,-107.386,47.654,-107.429,47.635,-107.507,47.624,-107.612,47.623,-107.719,47.563,-107.846,47.468,-107.911,47.39,-107.924,47.407,-107.909,47.471,-107.916,47.509,-107.93,47.542,-107.949,47.558,-107.982,47.569,-108.044,47.58,-108.17,47.584,-108.196,47.602,-108.173,47.613,-108.045,47.614,-107.959,47.601,-107.931,47.588,-107.889,47.538,-107.841,47.526,-107.779,47.57,-107.712,47.613,-107.685,47.646,-107.688,47.671,-107.714,47.688,-107.605,47.676,-107.455,47.662,-107.422,47.68,-107.38,47.716,-107.336,47.728,-107.295,47.722,-107.231,47.724,-107.121,47.725,-106.99,47.716,-106.93,47.725,-106.938,47.746,-106.933,47.764,-106.881,47.778,-106.821,47.783,-106.769,47.788,-106.738,47.806,-106.709,47.822,-106.655,47.841,-106.596,47.86,-106.582,47.879,-106.566,47.907,-106.574,47.936,-106.59,47.958,-106.538,47.983,-106.433,48.027,-106.406,48.021,-106.366,48.015,-106.325,47.985,-106.331,47.95,-106.358,47.925,-106.345,47.879,-106.285,47.837,-106.253,47.815,-106.267,47.793,-106.253,47.769,-106.241,47.739,-106.232,47.691,-106.217,47.628,-106.217,47.585]]]},{"n":"Sakakawea","z":4.7,"b":[-103.772,47.464,-101.14,48.18],"p":[[[-102.147,47.568,-102.237,47.589,-102.333,47.586,-102.409,47.564,-102.429,47.562,-102.42,47.59,-102.361,47.625,-102.299,47.657,-102.308,47.705,-102.336,47.757,-102.425,47.764,-102.561,47.785,-102.616,47.819,-102.653,47.916,-102.622,47.956,-102.614,47.992,-102.657,48.035,-102.761,48.084,-102.901,48.12,-103.059,48.125,-103.186,48.097,-103.238,48.063,-103.339,48.03,-103.459,48.016,-103.52,48.027,-103.551,48.072,-103.583,48.097,-103.665,48.063,-103.75,48.017,-103.772,48.027,-103.759,48.074,-103.721,48.102,-103.666,48.142,-103.575,48.153,-103.524,48.108,-103.482,48.081,-103.45,48.067,-103.353,48.076,-103.211,48.145,-103.118,48.171,-103.03,48.18,-102.916,48.171,-102.79,48.145,-102.675,48.105,-102.598,48.053,-102.557,47.968,-102.579,47.911,-102.577,47.867,-102.575,47.844,-102.527,47.822,-102.462,47.814,-102.408,47.806,-102.384,47.82,-102.4,47.877,-102.415,47.938,-102.375,47.952,-102.311,47.964,-102.273,47.978,-102.258,47.978,-102.265,47.961,-102.279,47.938,-102.287,47.925,-102.294,47.912,-102.276,47.856,-102.253,47.793,-102.24,47.766,-102.187,47.748,-102.169,47.729,-102.212,47.677,-102.16,47.624,-102.093,47.604,-102.021,47.585,-101.884,47.561,-101.718,47.567,-101.625,47.58,-101.601,47.586,-101.589,47.599,-101.453,47.608,-101.276,47.628,-101.203,47.645,-101.163,47.634,-101.14,47.606,-101.184,47.585,-101.247,47.567,-101.328,47.533,-101.4,47.506,-101.59,47.515,-101.679,47.498,-101.772,47.488,-101.883,47.467,-101.917,47.464,-101.951,47.488,-102.032,47.529,-102.096,47.553,-102.147,47.568]]]},{"n":"Teslin","z":4.7,"b":[-133.245,59.604,-132.089,60.449],"p":[[[-132.095,59.607,-132.125,59.604,-132.159,59.626,-132.213,59.688,-132.316,59.835,-132.399,59.922,-132.61,60.05,-132.834,60.135,-132.958,60.201,-133.012,60.251,-133.187,60.382,-133.245,60.449,-133.138,60.427,-132.98,60.297,-132.798,60.192,-132.702,60.153,-132.589,60.123,-132.522,60.098,-132.461,60.055,-132.364,59.986,-132.323,59.951,-132.277,59.902,-132.23,59.849,-132.225,59.838,-132.214,59.816,-132.202,59.77,-132.123,59.694,-132.089,59.644,-132.095,59.607]]]},{"n":"Arrow Lakes","z":4.7,"b":[-118.156,49.336,-117.907,49.802],"p":[[[-117.907,49.342,-117.957,49.336,-118.063,49.412,-118.096,49.45,-118.139,49.521,-118.156,49.601,-118.138,49.703,-118.128,49.752,-118.092,49.802,-118.084,49.774,-118.109,49.583,-118.087,49.515,-118.048,49.457,-117.976,49.406,-117.911,49.375,-117.907,49.342]]]},{"n":"Pend Oreille","z":4.7,"b":[-116.569,47.964,-116.215,48.302],"p":[[[-116.215,48.16,-116.306,48.139,-116.41,48.107,-116.441,48.075,-116.446,48.026,-116.456,47.984,-116.489,47.97,-116.529,47.964,-116.553,47.971,-116.541,47.995,-116.522,48.044,-116.498,48.115,-116.458,48.168,-116.408,48.191,-116.374,48.208,-116.381,48.229,-116.4,48.247,-116.431,48.254,-116.469,48.263,-116.525,48.242,-116.569,48.25,-116.53,48.284,-116.499,48.301,-116.433,48.302,-116.362,48.284,-116.302,48.236,-116.235,48.183,-116.215,48.16]]]},{"n":"Chelan","z":4.7,"b":[-120.628,47.852,-120.011,48.279],"p":[[[-120.011,47.852,-120.05,47.857,-120.118,47.88,-120.172,47.914,-120.205,47.962,-120.333,48.04,-120.53,48.162,-120.625,48.257,-120.628,48.279,-120.535,48.204,-120.32,48.064,-120.163,47.958,-120.069,47.884,-120.011,47.852]]]},{"n":"Black","z":4.7,"b":[-105.868,58.919,-104.948,59.296],"p":[[[-105.394,59.128,-105.554,59.099,-105.66,59.055,-105.733,58.98,-105.785,58.922,-105.835,58.919,-105.868,58.939,-105.855,58.978,-105.806,59.031,-105.55,59.15,-105.215,59.296,-105.119,59.286,-105.012,59.263,-104.948,59.237,-104.975,59.2,-105.012,59.173,-105.216,59.146,-105.303,59.138,-105.394,59.128]]]},{"n":"Louise","z":4.7,"b":[-146.704,62.282,-146.473,62.508],"p":[[[-146.473,62.332,-146.478,62.312,-146.515,62.282,-146.564,62.293,-146.653,62.362,-146.704,62.411,-146.703,62.445,-146.69,62.477,-146.684,62.504,-146.661,62.508,-146.648,62.484,-146.634,62.46,-146.61,62.414,-146.54,62.363,-146.473,62.332]]]},{"n":"Red","z":4.7,"b":[-95.271,47.889,-94.777,48.065],"p":[[[-94.97,48.032,-94.871,48.004,-94.783,47.97,-94.777,47.936,-94.792,47.91,-94.897,47.892,-95.111,47.889,-95.248,47.907,-95.271,47.942,-95.251,47.989,-95.187,48.033,-95.086,48.065,-95.011,48.06,-94.97,48.032]]]},{"n":"Red","z":4.7,"b":[-95.031,48.064,-94.558,48.203],"p":[[[-95.02,48.125,-94.975,48.152,-94.84,48.184,-94.69,48.203,-94.612,48.199,-94.561,48.178,-94.558,48.141,-94.6,48.106,-94.663,48.083,-94.75,48.066,-94.894,48.064,-95.008,48.072,-95.031,48.094,-95.02,48.125]]]},{"n":"Yathkyed","z":4.7,"b":[-98.458,62.422,-97.309,62.979],"p":[[[-97.969,62.548,-97.995,62.566,-98.051,62.607,-98.132,62.664,-98.206,62.69,-98.227,62.67,-98.315,62.618,-98.35,62.616,-98.348,62.665,-98.356,62.708,-98.387,62.73,-98.419,62.762,-98.447,62.804,-98.458,62.829,-98.432,62.828,-98.409,62.816,-98.356,62.811,-98.263,62.812,-98.207,62.81,-98.202,62.829,-98.194,62.851,-98.145,62.855,-98.099,62.842,-98.055,62.849,-97.983,62.864,-97.874,62.867,-97.764,62.864,-97.714,62.891,-97.68,62.947,-97.63,62.979,-97.617,62.967,-97.618,62.924,-97.638,62.891,-97.662,62.864,-97.647,62.828,-97.635,62.781,-97.631,62.716,-97.613,62.663,-97.561,62.638,-97.518,62.603,-97.509,62.568,-97.475,62.534,-97.427,62.502,-97.384,62.485,-97.36,62.484,-97.358,62.496,-97.36,62.507,-97.349,62.508,-97.326,62.501,-97.309,62.485,-97.318,62.461,-97.377,62.432,-97.445,62.422,-97.528,62.453,-97.626,62.492,-97.702,62.507,-97.78,62.509,-97.842,62.5,-97.876,62.488,-97.889,62.47,-97.913,62.459,-97.965,62.474,-98.023,62.504,-98.048,62.527,-98.016,62.537,-97.969,62.548]]]},{"n":"Amadjuak","z":4.7,"b":[-72.053,64.581,-70.378,65.408],"p":[[[-70.647,64.686,-70.637,64.711,-70.694,64.739,-70.804,64.774,-70.917,64.792,-70.999,64.787,-71.052,64.768,-71.097,64.707,-71.225,64.643,-71.373,64.636,-71.446,64.626,-71.499,64.609,-71.563,64.613,-71.657,64.632,-71.797,64.681,-71.899,64.728,-71.919,64.751,-71.898,64.778,-71.928,64.806,-71.982,64.835,-72.001,64.867,-72.019,64.889,-72.036,64.9,-72.053,64.91,-72.039,64.925,-71.995,64.942,-71.956,64.948,-71.959,64.931,-71.963,64.913,-71.938,64.907,-71.851,64.918,-71.819,64.91,-71.872,64.873,-71.873,64.844,-71.818,64.824,-71.748,64.819,-71.708,64.829,-71.725,64.845,-71.75,64.861,-71.584,64.884,-71.512,64.906,-71.504,64.921,-71.534,64.947,-71.638,64.985,-71.624,65.016,-71.583,65.04,-71.555,65.051,-71.546,65.082,-71.57,65.129,-71.642,65.189,-71.691,65.242,-71.676,65.278,-71.665,65.308,-71.717,65.353,-71.765,65.397,-71.737,65.408,-71.645,65.367,-71.542,65.31,-71.483,65.289,-71.429,65.286,-71.357,65.28,-71.3,65.266,-71.283,65.251,-71.27,65.234,-71.231,65.211,-71.093,65.163,-70.937,65.124,-70.909,65.111,-70.875,65.101,-70.771,65.07,-70.695,65.024,-70.667,64.985,-70.648,64.959,-70.636,64.934,-70.633,64.908,-70.532,64.832,-70.414,64.744,-70.384,64.717,-70.378,64.707,-70.429,64.686,-70.48,64.664,-70.477,64.655,-70.473,64.64,-70.467,64.621,-70.419,64.605,-70.385,64.59,-70.416,64.581,-70.509,64.6,-70.627,64.619,-70.721,64.622,-70.729,64.639,-70.682,64.661,-70.669,64.671,-70.647,64.686]]]},{"n":"Amistad","z":4.7,"b":[-101.312,29.458,-100.918,29.648],"p":[[[-100.918,29.502,-100.92,29.495,-100.936,29.486,-100.978,29.48,-101.039,29.46,-101.1,29.458,-101.151,29.467,-101.19,29.492,-101.233,29.503,-101.272,29.498,-101.287,29.506,-101.275,29.526,-101.261,29.562,-101.269,29.578,-101.28,29.585,-101.302,29.584,-101.312,29.603,-101.31,29.622,-101.305,29.634,-101.275,29.648,-101.214,29.603,-101.181,29.579,-101.142,29.558,-101.108,29.528,-101.072,29.51,-101.035,29.526,-101.004,29.548,-100.993,29.535,-100.977,29.557,-100.951,29.61,-100.944,29.606,-100.956,29.565,-100.951,29.529,-100.933,29.507,-100.918,29.502]]]},{"n":"Cabonga","z":4.7,"b":[-76.823,47.056,-76.327,47.691],"p":[[[-76.479,47.138,-76.435,47.122,-76.394,47.122,-76.359,47.119,-76.327,47.102,-76.331,47.083,-76.363,47.068,-76.393,47.056,-76.443,47.064,-76.505,47.092,-76.535,47.109,-76.553,47.121,-76.582,47.178,-76.603,47.241,-76.625,47.269,-76.651,47.278,-76.668,47.266,-76.654,47.222,-76.639,47.185,-76.669,47.2,-76.707,47.242,-76.715,47.311,-76.741,47.355,-76.781,47.357,-76.789,47.385,-76.778,47.416,-76.767,47.449,-76.779,47.476,-76.823,47.498,-76.819,47.517,-76.748,47.558,-76.658,47.611,-76.562,47.667,-76.522,47.691,-76.516,47.673,-76.595,47.624,-76.699,47.57,-76.722,47.54,-76.715,47.524,-76.656,47.552,-76.605,47.572,-76.618,47.543,-76.633,47.51,-76.556,47.498,-76.459,47.476,-76.408,47.446,-76.422,47.4,-76.475,47.331,-76.489,47.284,-76.494,47.25,-76.502,47.222,-76.517,47.201,-76.512,47.171,-76.479,47.138]]]},{"n":"Chapala","z":4.7,"b":[-103.372,20.124,-102.7,20.329],"p":[[[-102.711,20.194,-102.7,20.182,-102.713,20.162,-102.744,20.139,-102.786,20.124,-102.852,20.139,-102.934,20.168,-103.008,20.183,-103.089,20.181,-103.17,20.179,-103.255,20.2,-103.34,20.231,-103.372,20.26,-103.272,20.281,-103.105,20.308,-103.002,20.329,-102.894,20.319,-102.77,20.198,-102.744,20.198,-102.711,20.194]]]},{"n":"Managua","z":4.7,"b":[-86.589,12.172,-86.102,12.48],"p":[[[-86.104,12.312,-86.102,12.271,-86.105,12.208,-86.175,12.185,-86.261,12.172,-86.315,12.177,-86.307,12.206,-86.297,12.242,-86.304,12.265,-86.323,12.28,-86.387,12.274,-86.47,12.274,-86.529,12.303,-86.572,12.343,-86.589,12.38,-86.589,12.405,-86.546,12.41,-86.468,12.428,-86.4,12.464,-86.339,12.48,-86.294,12.474,-86.236,12.383,-86.161,12.315,-86.119,12.329,-86.104,12.312]]]},{"n":"Itaipu","z":4.7,"b":[-54.795,-25.44,-54.183,-24.066],"p":[[[-54.183,-24.708,-54.227,-24.726,-54.292,-24.751,-54.312,-24.783,-54.308,-24.819,-54.257,-24.878,-54.221,-24.92,-54.251,-24.916,-54.314,-24.892,-54.357,-24.892,-54.375,-24.92,-54.393,-24.946,-54.387,-24.982,-54.337,-25.033,-54.318,-25.079,-54.332,-25.115,-54.348,-25.139,-54.377,-25.151,-54.377,-25.18,-54.288,-25.216,-54.201,-25.24,-54.268,-25.261,-54.34,-25.291,-54.383,-25.303,-54.433,-25.309,-54.432,-25.335,-54.402,-25.371,-54.424,-25.389,-54.491,-25.387,-54.527,-25.396,-54.554,-25.419,-54.582,-25.44,-54.611,-25.433,-54.634,-25.417,-54.634,-25.302,-54.608,-25.267,-54.547,-25.235,-54.519,-25.19,-54.527,-25.155,-54.559,-25.138,-54.58,-25.128,-54.576,-25.112,-54.624,-25.101,-54.721,-25.088,-54.754,-25.063,-54.712,-25.043,-54.61,-25.045,-54.528,-25.053,-54.51,-25.016,-54.506,-24.965,-54.526,-24.935,-54.67,-24.905,-54.795,-24.88,-54.77,-24.868,-54.7,-24.873,-54.612,-24.884,-54.531,-24.877,-54.474,-24.848,-54.466,-24.804,-54.524,-24.75,-54.49,-24.712,-54.4,-24.69,-54.386,-24.659,-54.387,-24.62,-54.419,-24.579,-54.42,-24.542,-54.388,-24.509,-54.4,-24.454,-54.382,-24.416,-54.332,-24.404,-54.312,-24.368,-54.313,-24.322,-54.348,-24.264,-54.368,-24.211,-54.357,-24.165,-54.35,-24.122,-54.323,-24.092,-54.267,-24.066,-54.283,-24.144,-54.271,-24.225,-54.252,-24.286,-54.258,-24.468,-54.259,-24.642,-54.216,-24.686,-54.183,-24.708]]]},{"n":"Gatun","z":4.7,"b":[-80.076,9.016,-79.671,9.32],"p":[[[-79.671,9.144,-79.688,9.124,-79.691,9.104,-79.8,9.073,-79.83,9.046,-79.847,9.045,-79.855,9.055,-79.834,9.08,-79.811,9.105,-79.836,9.118,-79.875,9.139,-79.9,9.154,-79.912,9.121,-79.926,9.078,-79.947,9.068,-79.953,9.043,-79.96,9.023,-79.973,9.024,-79.993,9.039,-80.012,9.06,-80.021,9.056,-80.019,9.037,-80.03,9.026,-80.058,9.021,-80.076,9.016,-80.068,9.041,-80.013,9.097,-79.953,9.192,-79.923,9.267,-79.857,9.294,-79.822,9.32,-79.809,9.316,-79.805,9.285,-79.799,9.254,-79.822,9.238,-79.844,9.222,-79.838,9.207,-79.816,9.185,-79.808,9.172,-79.776,9.148,-79.739,9.13,-79.717,9.127,-79.686,9.134,-79.671,9.144]]]},{"n":"Kariba","z":4.7,"b":[27.018,-17.963,28.988,-16.495],"p":[[[28.988,-16.611,28.968,-16.626,28.943,-16.658,28.931,-16.689,28.901,-16.682,28.867,-16.676,28.854,-16.724,28.829,-16.77,28.79,-16.79,28.762,-16.808,28.725,-16.798,28.697,-16.774,28.7,-16.747,28.69,-16.71,28.655,-16.7,28.613,-16.716,28.586,-16.728,28.555,-16.739,28.509,-16.759,28.474,-16.793,28.459,-16.834,28.461,-16.866,28.454,-16.865,28.419,-16.837,28.382,-16.795,28.341,-16.788,28.242,-16.818,28.15,-16.839,28.117,-16.861,28.101,-16.896,28.084,-16.947,28.089,-16.978,28.102,-16.993,28.096,-17.017,28.076,-17.046,28.047,-17.065,27.999,-17.051,27.959,-17.04,27.943,-17.051,27.914,-17.079,27.871,-17.118,27.841,-17.146,27.813,-17.156,27.75,-17.175,27.705,-17.207,27.683,-17.23,27.668,-17.254,27.654,-17.294,27.611,-17.381,27.576,-17.451,27.565,-17.455,27.549,-17.474,27.502,-17.508,27.455,-17.533,27.44,-17.553,27.45,-17.558,27.462,-17.559,27.453,-17.568,27.407,-17.58,27.353,-17.602,27.336,-17.618,27.287,-17.673,27.228,-17.754,27.195,-17.801,27.164,-17.844,27.116,-17.912,27.077,-17.956,27.051,-17.963,27.021,-17.958,27.018,-17.927,27.04,-17.904,27.065,-17.874,27.094,-17.848,27.096,-17.807,27.107,-17.771,27.172,-17.689,27.255,-17.61,27.292,-17.581,27.318,-17.564,27.338,-17.55,27.354,-17.529,27.371,-17.504,27.372,-17.479,27.344,-17.46,27.371,-17.389,27.431,-17.3,27.472,-17.247,27.55,-17.152,27.609,-17.073,27.641,-17.068,27.669,-17.075,27.688,-17.066,27.686,-17.042,27.704,-16.998,27.717,-16.959,27.705,-16.94,27.718,-16.928,27.753,-16.923,27.8,-16.917,27.831,-16.901,27.817,-16.869,27.789,-16.836,27.794,-16.819,27.826,-16.811,27.858,-16.793,27.877,-16.773,27.899,-16.769,27.917,-16.782,27.942,-16.797,27.975,-16.819,28.009,-16.824,28.042,-16.818,28.073,-16.79,28.106,-16.755,28.136,-16.736,28.168,-16.707,28.179,-16.679,28.175,-16.655,28.206,-16.647,28.246,-16.63,28.283,-16.601,28.298,-16.568,28.29,-16.543,28.314,-16.549,28.35,-16.56,28.373,-16.573,28.393,-16.586,28.441,-16.558,28.497,-16.54,28.53,-16.546,28.552,-16.536,28.56,-16.521,28.579,-16.506,28.623,-16.495,28.681,-16.508,28.761,-16.532,28.854,-16.572,28.899,-16.569,28.943,-16.576,28.967,-16.597,28.988,-16.611]]]},{"n":"Kyoga","z":4.7,"b":[32.537,1.223,33.487,1.731],"p":[[[33.487,1.643,33.443,1.641,33.334,1.623,33.227,1.585,33.161,1.542,33.126,1.503,33.14,1.483,33.206,1.477,33.258,1.47,33.251,1.454,33.208,1.43,33.197,1.399,33.243,1.386,33.282,1.402,33.303,1.419,33.317,1.42,33.321,1.398,33.326,1.356,33.305,1.305,33.271,1.253,33.26,1.223,33.23,1.254,33.167,1.289,33.123,1.313,33.019,1.382,32.899,1.43,32.869,1.432,32.864,1.419,32.868,1.405,32.892,1.381,32.893,1.311,32.86,1.332,32.824,1.388,32.786,1.402,32.764,1.394,32.752,1.357,32.742,1.34,32.661,1.376,32.58,1.411,32.554,1.437,32.537,1.497,32.545,1.522,32.611,1.514,32.736,1.545,32.801,1.581,32.803,1.572,32.797,1.556,32.811,1.549,32.862,1.556,32.938,1.563,32.982,1.553,32.999,1.547,33.022,1.56,33.067,1.601,33.101,1.634,33.113,1.624,33.129,1.616,33.163,1.639,33.206,1.66,33.237,1.67,33.269,1.664,33.31,1.656,33.357,1.677,33.414,1.731,33.446,1.728,33.457,1.684,33.471,1.671,33.483,1.653,33.487,1.643]]]},{"n":"Kubenskoe","z":4.7,"b":[39.053,59.48,39.798,59.869],"p":[[[39.267,59.783,39.305,59.774,39.348,59.766,39.454,59.711,39.511,59.707,39.582,59.665,39.707,59.634,39.78,59.638,39.744,59.61,39.721,59.578,39.764,59.546,39.798,59.525,39.734,59.48,39.553,59.546,39.402,59.624,39.194,59.747,39.053,59.839,39.054,59.868,39.093,59.869,39.139,59.842,39.192,59.809,39.23,59.794,39.267,59.783]]]},{"n":"Constance","z":4.7,"b":[9.113,47.506,9.731,47.782],"p":[[[9.731,47.527,9.719,47.515,9.685,47.506,9.554,47.511,9.452,47.514,9.25,47.598,9.183,47.671,9.113,47.772,9.132,47.782,9.231,47.733,9.373,47.677,9.514,47.625,9.644,47.566,9.731,47.527]]]},{"n":"","z":4.7,"b":[22.579,61.202,23.293,61.46],"p":[[[23.241,61.417,23.185,61.407,23.1,61.392,23.046,61.386,22.987,61.369,22.847,61.324,22.702,61.263,22.659,61.219,22.613,61.202,22.579,61.211,22.632,61.234,22.663,61.268,22.718,61.3,22.841,61.341,22.975,61.395,23.053,61.429,23.137,61.446,23.26,61.46,23.293,61.452,23.249,61.433,23.244,61.424,23.241,61.417]]]},{"n":"Dead Sea","z":4.7,"b":[35.377,31.096,35.522,31.232],"p":[[[35.522,31.177,35.517,31.154,35.508,31.125,35.498,31.105,35.459,31.096,35.417,31.104,35.391,31.135,35.377,31.178,35.382,31.216,35.401,31.231,35.446,31.232,35.494,31.205,35.522,31.177]]]},{"n":"Sea of Galilee","z":4.7,"b":[35.534,32.72,35.636,32.9],"p":[[[35.632,32.782,35.616,32.749,35.577,32.72,35.551,32.75,35.534,32.803,35.539,32.856,35.565,32.892,35.599,32.9,35.623,32.878,35.636,32.834,35.632,32.782]]]},{"n":"Argyle","z":4.7,"b":[128.53,-16.788,129.032,-16.076],"p":[[[129.032,-16.143,129.015,-16.161,129.001,-16.19,128.97,-16.228,128.944,-16.271,128.958,-16.302,128.951,-16.319,128.938,-16.349,128.928,-16.37,128.909,-16.394,128.895,-16.424,128.878,-16.436,128.864,-16.428,128.851,-16.422,128.827,-16.422,128.802,-16.45,128.792,-16.508,128.798,-16.554,128.794,-16.593,128.788,-16.637,128.784,-16.694,128.757,-16.778,128.734,-16.788,128.72,-16.759,128.696,-16.717,128.663,-16.681,128.626,-16.66,128.611,-16.645,128.597,-16.633,128.583,-16.615,128.587,-16.602,128.591,-16.572,128.584,-16.542,128.537,-16.536,128.53,-16.508,128.568,-16.444,128.62,-16.314,128.635,-16.25,128.659,-16.236,128.678,-16.223,128.671,-16.195,128.645,-16.159,128.658,-16.144,128.694,-16.137,128.737,-16.134,128.774,-16.087,128.805,-16.076,128.809,-16.087,128.778,-16.126,128.784,-16.149,128.791,-16.171,128.812,-16.171,128.844,-16.158,128.891,-16.139,128.946,-16.117,128.991,-16.117,129.022,-16.128,129.032,-16.143]]]},{"n":"Ngoring","z":4.7,"b":[97.547,34.788,97.887,35.088],"p":[[[97.887,34.838,97.861,34.823,97.801,34.827,97.751,34.828,97.717,34.826,97.695,34.831,97.689,34.812,97.683,34.792,97.648,34.788,97.603,34.798,97.575,34.825,97.547,34.876,97.555,34.902,97.583,34.929,97.625,34.953,97.644,34.978,97.636,35.003,97.651,35.028,97.687,35.053,97.735,35.08,97.763,35.088,97.794,35.033,97.797,34.982,97.801,34.932,97.847,34.882,97.887,34.838]]]},{"n":"Gyaring","z":4.7,"b":[97.062,34.826,97.468,35.023],"p":[[[97.468,34.976,97.46,34.961,97.413,34.916,97.36,34.865,97.288,34.835,97.23,34.826,97.084,34.907,97.072,34.931,97.062,34.979,97.067,34.997,97.085,35,97.118,35.008,97.231,35.023,97.351,35.02,97.419,34.997,97.468,34.976]]]},{"n":"Toktogul","z":4.7,"b":[72.635,41.663,73.3,41.9],"p":[[[73.291,41.759,73.266,41.755,73.202,41.745,73.107,41.757,72.972,41.755,72.839,41.74,72.679,41.663,72.654,41.667,72.635,41.687,72.658,41.701,72.689,41.728,72.709,41.767,72.688,41.822,72.658,41.874,72.685,41.9,72.817,41.898,72.94,41.878,73.011,41.855,73.086,41.828,73.24,41.801,73.3,41.776,73.291,41.759]]]},{"n":"Rakshastal","z":4.7,"b":[81.126,30.608,81.309,30.863],"p":[[[81.309,30.636,81.29,30.617,81.255,30.608,81.212,30.611,81.158,30.634,81.126,30.662,81.15,30.688,81.185,30.721,81.184,30.804,81.199,30.852,81.234,30.863,81.275,30.839,81.294,30.817,81.296,30.789,81.252,30.753,81.279,30.709,81.309,30.662,81.309,30.636]]]},{"n":"Manasarovar","z":4.7,"b":[81.373,30.591,81.584,30.778],"p":[[[81.584,30.7,81.552,30.654,81.494,30.61,81.434,30.591,81.392,30.61,81.373,30.659,81.376,30.758,81.409,30.778,81.46,30.774,81.524,30.757,81.574,30.733,81.584,30.7]]]},{"n":"Volta","z":4.7,"b":[-1.715,6.338,0.32,9.117],"p":[[[0.272,6.675,0.299,6.719,0.317,6.738,0.309,6.698,0.276,6.648,0.23,6.616,0.192,6.607,0.172,6.615,0.16,6.618,0.152,6.611,0.154,6.597,0.151,6.57,0.147,6.538,0.14,6.505,0.133,6.45,0.138,6.366,0.103,6.338,0.081,6.353,0.073,6.404,0.041,6.405,-0.011,6.379,-0.033,6.408,-0.06,6.452,-0.099,6.49,-0.175,6.533,-0.255,6.585,-0.324,6.622,-0.428,6.668,-0.55,6.717,-0.634,6.753,-0.674,6.802,-0.684,6.834,-0.66,6.836,-0.616,6.807,-0.58,6.785,-0.551,6.811,-0.518,6.794,-0.491,6.737,-0.467,6.723,-0.442,6.727,-0.419,6.731,-0.409,6.766,-0.406,6.816,-0.393,6.828,-0.368,6.773,-0.335,6.716,-0.305,6.717,-0.271,6.722,-0.228,6.734,-0.194,6.741,-0.175,6.724,-0.163,6.686,-0.144,6.648,-0.096,6.629,-0.057,6.63,-0.041,6.677,-0.026,6.72,-0.017,6.733,-0.023,6.748,-0.049,6.769,-0.067,6.789,-0.071,6.809,-0.066,6.825,-0.042,6.847,-0.011,6.853,0.012,6.845,0.03,6.85,0.077,6.87,0.112,6.897,0.104,6.91,0.059,6.914,0.042,6.946,0.087,6.974,0.12,6.971,0.123,6.983,0.099,7.004,0.088,7.023,0.093,7.037,0.101,7.053,0.098,7.08,0.086,7.121,0.085,7.147,0.066,7.151,0.045,7.164,0.028,7.176,0.01,7.187,0.007,7.209,0.015,7.211,0.035,7.2,0.063,7.195,0.083,7.194,0.099,7.193,0.118,7.203,0.142,7.222,0.153,7.241,0.16,7.252,0.174,7.252,0.184,7.266,0.184,7.287,0.172,7.304,0.156,7.309,0.135,7.313,0.102,7.343,0.068,7.393,0.05,7.432,0.051,7.449,0.047,7.46,0.046,7.478,-0.002,7.5,-0.063,7.508,-0.058,7.466,-0.066,7.444,-0.107,7.458,-0.128,7.472,-0.136,7.496,-0.157,7.51,-0.207,7.512,-0.242,7.517,-0.232,7.532,-0.208,7.546,-0.169,7.55,-0.134,7.553,-0.105,7.551,-0.071,7.549,-0.027,7.546,0.012,7.543,-0.018,7.592,-0.064,7.641,-0.11,7.645,-0.182,7.647,-0.242,7.639,-0.336,7.618,-0.432,7.594,-0.475,7.589,-0.498,7.597,-0.531,7.576,-0.603,7.554,-0.654,7.551,-0.611,7.567,-0.543,7.6,-0.483,7.625,-0.422,7.649,-0.357,7.664,-0.294,7.667,-0.262,7.684,-0.256,7.706,-0.254,7.72,-0.21,7.714,-0.161,7.721,-0.125,7.74,-0.098,7.746,-0.1,7.755,-0.134,7.767,-0.157,7.779,-0.159,7.794,-0.152,7.813,-0.152,7.841,-0.17,7.89,-0.186,7.927,-0.192,7.955,-0.224,7.977,-0.285,8.015,-0.315,8.04,-0.344,8.038,-0.351,8.002,-0.384,7.98,-0.417,7.972,-0.394,8,-0.379,8.048,-0.415,8.079,-0.446,8.091,-0.498,8.057,-0.527,8.062,-0.526,8.128,-0.543,8.154,-0.561,8.134,-0.591,8.1,-0.624,8.085,-0.643,8.087,-0.643,8.106,-0.693,8.11,-0.756,8.102,-0.761,8.118,-0.748,8.135,-0.721,8.135,-0.655,8.142,-0.604,8.159,-0.6,8.173,-0.621,8.191,-0.676,8.239,-0.724,8.28,-0.754,8.297,-0.795,8.315,-0.801,8.334,-0.784,8.366,-0.838,8.447,-0.912,8.555,-0.965,8.604,-0.999,8.63,-1.041,8.646,-1.087,8.64,-1.128,8.632,-1.141,8.595,-1.153,8.551,-1.18,8.604,-1.219,8.658,-1.254,8.668,-1.304,8.697,-1.351,8.724,-1.405,8.739,-1.443,8.731,-1.488,8.697,-1.545,8.664,-1.605,8.644,-1.664,8.629,-1.715,8.63,-1.663,8.673,-1.591,8.695,-1.519,8.731,-1.466,8.761,-1.412,8.781,-1.376,8.805,-1.355,8.792,-1.294,8.75,-1.269,8.726,-1.126,8.705,-1.062,8.722,-1.067,8.75,-1.096,8.784,-1.134,8.813,-1.145,8.874,-1.174,8.945,-1.179,8.979,-1.178,9.027,-1.174,9.074,-1.152,9.117,-1.13,9.076,-1.127,9.028,-1.125,8.981,-1.121,8.929,-1.106,8.897,-1.075,8.853,-1.045,8.843,-1.026,8.844,-0.997,8.775,-0.931,8.653,-0.873,8.586,-0.843,8.579,-0.817,8.6,-0.818,8.65,-0.823,8.707,-0.809,8.743,-0.791,8.761,-0.782,8.756,-0.795,8.721,-0.806,8.689,-0.77,8.656,-0.722,8.639,-0.705,8.643,-0.707,8.625,-0.757,8.578,-0.805,8.52,-0.797,8.497,-0.782,8.469,-0.721,8.402,-0.66,8.357,-0.627,8.329,-0.594,8.288,-0.566,8.275,-0.554,8.251,-0.527,8.232,-0.485,8.209,-0.449,8.209,-0.394,8.221,-0.355,8.243,-0.318,8.244,-0.286,8.253,-0.245,8.245,-0.202,8.213,-0.193,8.186,-0.257,8.185,-0.321,8.177,-0.33,8.158,-0.308,8.142,-0.263,8.108,-0.19,8.053,-0.128,8.011,-0.103,7.976,-0.082,7.876,-0.052,7.811,-0.024,7.793,0.002,7.793,0.019,7.789,0.055,7.792,0.07,7.821,0.058,7.863,0.056,7.886,0.059,7.92,0.073,7.969,0.079,8.003,0.062,8.041,0.07,8.081,0.099,8.093,0.129,8.086,0.154,8.078,0.17,8.086,0.165,8.115,0.152,8.161,0.123,8.21,0.105,8.242,0.137,8.254,0.169,8.289,0.198,8.338,0.217,8.363,0.161,8.381,0.092,8.403,0.081,8.429,0.093,8.461,0.091,8.5,0.085,8.548,0.106,8.583,0.135,8.598,0.15,8.633,0.16,8.7,0.184,8.742,0.198,8.738,0.185,8.69,0.168,8.625,0.152,8.592,0.128,8.574,0.121,8.518,0.113,8.457,0.103,8.434,0.115,8.416,0.178,8.399,0.242,8.379,0.261,8.355,0.28,8.341,0.294,8.334,0.284,8.327,0.242,8.322,0.214,8.314,0.217,8.296,0.19,8.251,0.184,8.206,0.201,8.166,0.231,8.125,0.268,8.106,0.267,8.094,0.24,8.088,0.198,8.079,0.161,8.06,0.146,8.021,0.121,7.972,0.108,7.939,0.124,7.913,0.146,7.894,0.168,7.863,0.221,7.883,0.258,7.905,0.251,7.876,0.252,7.863,0.248,7.849,0.237,7.834,0.248,7.803,0.278,7.762,0.279,7.699,0.281,7.617,0.282,7.575,0.259,7.581,0.24,7.578,0.228,7.556,0.229,7.536,0.242,7.511,0.25,7.491,0.285,7.515,0.32,7.542,0.313,7.506,0.302,7.47,0.297,7.453,0.295,7.416,0.287,7.382,0.276,7.353,0.277,7.339,0.277,7.33,0.292,7.303,0.294,7.278,0.276,7.258,0.272,7.234,0.279,7.218,0.281,7.201,0.279,7.182,0.276,7.172,0.271,7.138,0.289,7.106,0.305,7.099,0.3,7.071,0.281,7.047,0.273,7.001,0.263,6.948,0.24,6.896,0.237,6.768,0.254,6.673,0.272,6.675],[0.13,6.679,0.181,6.726,0.159,6.767,0.115,6.796,0.062,6.808,0.017,6.779,0.011,6.718,0.049,6.695,0.09,6.659,0.13,6.679]]]},{"n":"South Aral Sea","z":1,"b":[58.221,44.425,59.303,46.002],"p":[[[58.312,45.062,58.372,45.131,58.417,45.291,58.549,45.408,58.574,45.532,58.638,45.59,58.653,45.722,58.712,45.835,58.838,45.834,58.814,45.882,58.849,45.918,59.06,45.895,59.079,45.943,59.179,45.926,59.24,45.994,59.303,46.002,59.244,45.892,59.105,45.897,58.914,45.795,58.869,45.596,58.766,45.563,58.672,45.46,58.629,45.264,58.682,45.13,58.547,45.008,58.588,44.99,58.567,44.915,58.624,44.818,58.563,44.772,58.524,44.569,58.483,44.504,58.364,44.425,58.26,44.431,58.235,44.461,58.237,44.524,58.321,44.603,58.221,44.847,58.312,45.062]]]},{"n":"North Aral Sea","z":1,"b":[59.988,46.086,61.528,46.784],"p":[[[60.783,46.086,60.752,46.118,60.825,46.201,60.722,46.374,60.58,46.293,60.348,46.403,60.133,46.357,59.988,46.438,60.002,46.572,60.287,46.7,60.368,46.617,60.401,46.509,60.41,46.504,60.42,46.508,60.453,46.618,60.459,46.623,60.471,46.622,60.58,46.563,60.741,46.627,60.746,46.639,60.745,46.653,60.526,46.751,60.525,46.777,60.58,46.784,60.811,46.747,60.776,46.581,60.892,46.473,61.134,46.45,61.256,46.493,61.264,46.5,61.265,46.513,61.203,46.577,61.297,46.611,61.315,46.648,61.528,46.677,61.45,46.584,61.37,46.528,61.283,46.448,61.3,46.385,61.217,46.237,60.977,46.17,60.862,46.09,60.783,46.086]]]},{"n":"Barsakelmes","z":5,"b":[59.504,46.083,59.813,46.322],"p":[[[59.729,46.083,59.592,46.209,59.504,46.319,59.731,46.322,59.785,46.284,59.813,46.124,59.729,46.083]]]},{"n":"Balkhash","z":1,"b":[73.437,44.989,79.245,46.818],"p":[[[78.991,46.749,79.019,46.768,79.022,46.795,79.052,46.808,79.084,46.81,79.122,46.799,79.177,46.764,79.222,46.72,79.245,46.68,79.245,46.645,79.236,46.634,79.217,46.61,79.157,46.603,79.095,46.582,79.068,46.526,79.023,46.447,78.971,46.406,78.89,46.37,78.787,46.385,78.711,46.396,78.625,46.38,78.544,46.401,78.465,46.392,78.438,46.377,78.435,46.352,78.462,46.313,78.445,46.297,78.409,46.299,78.367,46.339,78.317,46.35,78.233,46.337,78.161,46.319,78.128,46.319,78.075,46.328,78.018,46.343,77.865,46.429,77.817,46.439,77.781,46.423,77.726,46.407,77.652,46.415,77.563,46.435,77.429,46.5,77.355,46.502,77.282,46.473,77.165,46.489,77.104,46.454,77.004,46.464,76.853,46.492,76.742,46.532,76.464,46.542,76.238,46.555,76.176,46.532,76.033,46.512,75.909,46.536,75.867,46.536,75.715,46.516,75.611,46.507,75.558,46.53,75.515,46.567,75.492,46.596,75.49,46.631,75.508,46.636,75.548,46.634,75.562,46.646,75.56,46.653,75.553,46.673,75.509,46.678,75.463,46.671,75.417,46.641,75.404,46.61,75.432,46.542,75.448,46.498,75.406,46.471,75.35,46.47,75.33,46.513,75.304,46.519,75.278,46.515,75.256,46.474,75.241,46.448,75.212,46.427,75.152,46.438,75.121,46.446,75.093,46.461,75.067,46.451,75.013,46.417,74.911,46.405,74.888,46.377,74.857,46.294,74.815,46.216,74.794,46.13,74.771,46.108,74.599,46.022,74.55,46.006,74.37,46.02,74.278,46.004,74.258,45.977,74.253,45.942,74.303,45.79,74.289,45.765,74.251,45.761,74.243,45.752,74.245,45.74,74.311,45.695,74.313,45.671,74.289,45.651,74.231,45.615,74.101,45.537,74.089,45.509,74.083,45.352,74.087,45.344,74.128,45.337,74.151,45.317,74.156,45.271,74.143,45.103,74.136,45.07,74.101,45.059,74.094,45.04,74.12,45,74.1,44.989,74.056,44.994,74.017,45.013,74.01,45.034,74.016,45.056,73.993,45.112,74.008,45.147,73.999,45.189,73.981,45.204,73.877,45.239,73.84,45.264,73.663,45.436,73.615,45.513,73.465,45.582,73.437,45.609,73.444,45.657,73.494,45.687,73.532,45.734,73.519,45.763,73.451,45.78,73.44,45.806,73.463,45.826,73.488,45.831,73.56,45.816,73.633,45.907,73.715,45.983,73.737,46.013,73.663,46.062,73.654,46.093,73.646,46.15,73.679,46.183,73.752,46.203,73.775,46.206,73.829,46.185,73.898,46.208,73.982,46.186,74.021,46.205,74.027,46.222,73.95,46.288,74.023,46.369,74.094,46.428,74.28,46.47,74.375,46.518,74.43,46.578,74.524,46.623,74.553,46.69,74.625,46.752,74.804,46.781,74.939,46.817,74.974,46.814,75.089,46.814,75.163,46.775,75.201,46.759,75.338,46.73,75.427,46.738,75.826,46.818,75.951,46.795,76.203,46.78,76.298,46.753,76.336,46.729,76.405,46.662,76.45,46.641,76.496,46.636,76.6,46.665,76.668,46.673,76.7,46.667,76.793,46.631,76.859,46.644,77.082,46.625,77.175,46.556,77.226,46.567,77.266,46.611,77.37,46.623,77.602,46.64,77.645,46.656,77.69,46.659,77.86,46.648,78.136,46.595,78.169,46.58,78.297,46.469,78.324,46.482,78.348,46.512,78.359,46.623,78.368,46.643,78.397,46.658,78.433,46.652,78.452,46.618,78.476,46.625,78.504,46.665,78.551,46.707,78.581,46.723,78.63,46.747,78.761,46.724,78.784,46.735,78.787,46.755,78.793,46.78,78.823,46.795,78.868,46.799,78.93,46.788,78.991,46.749]]]},{"n":"Tanganyika","z":1,"b":[29.058,-8.791,31.198,-3.338],"p":[[[30.806,-8.578,30.799,-8.566,30.711,-8.534,30.681,-8.514,30.672,-8.503,30.654,-8.507,30.62,-8.544,30.555,-8.58,30.534,-8.579,30.56,-8.556,30.569,-8.537,30.568,-8.514,30.543,-8.504,30.495,-8.507,30.464,-8.498,30.448,-8.449,30.461,-8.382,30.478,-8.355,30.508,-8.332,30.549,-8.313,30.573,-8.262,30.581,-8.181,30.567,-8.115,30.532,-8.064,30.499,-8.033,30.468,-8.021,30.442,-7.997,30.396,-7.928,30.367,-7.903,30.294,-7.873,30.277,-7.848,30.265,-7.768,30.221,-7.67,30.204,-7.587,30.214,-7.558,30.208,-7.474,30.147,-7.299,30.09,-7.214,30.013,-7.145,29.928,-7.095,29.836,-7.063,29.752,-6.995,29.616,-6.822,29.537,-6.754,29.511,-6.72,29.48,-6.633,29.484,-6.538,29.476,-6.515,29.454,-6.517,29.436,-6.509,29.424,-6.491,29.311,-6.191,29.289,-6.176,29.27,-6.131,29.25,-6.109,29.215,-6.088,29.193,-6.038,29.184,-5.958,29.19,-5.915,29.214,-5.892,29.33,-5.799,29.34,-5.766,29.341,-5.746,29.333,-5.662,29.342,-5.635,29.371,-5.63,29.372,-5.616,29.226,-5.371,29.157,-5.236,29.117,-5.124,29.102,-5.054,29.112,-5.026,29.114,-4.714,29.124,-4.586,29.147,-4.529,29.169,-4.504,29.203,-4.511,29.202,-4.437,29.254,-4.101,29.245,-4.065,29.228,-4.071,29.218,-4.118,29.145,-4.335,29.133,-4.343,29.063,-4.317,29.058,-4.286,29.089,-4.245,29.099,-4.206,29.088,-4.169,29.086,-4.092,29.103,-3.896,29.117,-3.852,29.117,-3.816,29.103,-3.788,29.11,-3.75,29.138,-3.703,29.147,-3.617,29.139,-3.49,29.142,-3.408,29.158,-3.37,29.175,-3.346,29.196,-3.338,29.298,-3.355,29.332,-3.39,29.331,-3.462,29.335,-3.663,29.346,-3.794,29.364,-3.856,29.434,-3.977,29.433,-4.001,29.523,-4.203,29.579,-4.3,29.629,-4.366,29.653,-4.42,29.641,-4.489,29.607,-4.789,29.614,-4.881,29.6,-4.896,29.608,-4.907,29.637,-4.913,29.684,-4.939,29.749,-4.984,29.792,-5.041,29.813,-5.109,29.816,-5.152,29.803,-5.169,29.8,-5.257,29.788,-5.263,29.774,-5.354,29.752,-5.405,29.758,-5.467,29.812,-5.528,29.865,-5.63,29.924,-5.771,29.951,-5.861,29.948,-5.899,29.916,-5.938,29.856,-5.98,29.817,-5.997,29.798,-5.99,29.775,-6.001,29.748,-6.03,29.729,-6.095,29.717,-6.196,29.722,-6.244,29.748,-6.288,29.82,-6.37,29.943,-6.479,29.95,-6.498,29.965,-6.5,30.023,-6.469,30.094,-6.457,30.137,-6.465,30.166,-6.483,30.182,-6.51,30.299,-6.616,30.356,-6.69,30.398,-6.778,30.455,-6.856,30.528,-6.923,30.563,-6.993,30.561,-7.065,30.543,-7.104,30.521,-7.107,30.513,-7.116,30.522,-7.166,30.577,-7.276,30.599,-7.376,30.589,-7.464,30.604,-7.542,30.645,-7.61,30.725,-7.701,30.825,-7.911,30.906,-8.038,30.913,-8.075,30.925,-8.098,30.957,-8.121,30.962,-8.139,30.948,-8.162,30.95,-8.19,30.964,-8.238,31.015,-8.303,31.102,-8.385,31.143,-8.451,31.138,-8.501,31.151,-8.551,31.184,-8.602,31.198,-8.652,31.189,-8.73,31.179,-8.742,31.166,-8.739,31.15,-8.72,31.133,-8.725,31.113,-8.754,31.084,-8.776,31.045,-8.791,31.022,-8.787,30.971,-8.745,30.806,-8.578]]]},{"n":"Lagoa Mirim","z":1.7,"b":[-53.624,-33.557,-52.591,-32.199],"p":[[[-52.598,-32.585,-52.617,-32.638,-52.659,-32.698,-52.694,-32.757,-52.731,-32.812,-52.773,-32.876,-52.808,-32.907,-52.847,-32.917,-52.895,-32.91,-52.939,-32.861,-52.976,-32.822,-53.015,-32.799,-53.068,-32.808,-53.131,-32.846,-53.173,-32.956,-53.228,-33.055,-53.292,-33.081,-53.326,-33.085,-53.352,-33.095,-53.377,-33.12,-53.409,-33.159,-53.437,-33.235,-53.453,-33.351,-53.437,-33.473,-53.454,-33.532,-53.491,-33.547,-53.508,-33.557,-53.527,-33.553,-53.544,-33.539,-53.557,-33.515,-53.567,-33.483,-53.573,-33.433,-53.577,-33.368,-53.586,-33.299,-53.604,-33.242,-53.62,-33.191,-53.624,-33.136,-53.6,-33.079,-53.546,-33.024,-53.473,-32.959,-53.406,-32.954,-53.354,-32.926,-53.323,-32.883,-53.313,-32.834,-53.313,-32.793,-53.302,-32.761,-53.265,-32.733,-53.217,-32.707,-53.178,-32.679,-53.139,-32.649,-53.092,-32.62,-53.03,-32.598,-52.991,-32.534,-52.959,-32.468,-52.877,-32.432,-52.81,-32.391,-52.804,-32.348,-52.792,-32.311,-52.78,-32.263,-52.745,-32.217,-52.72,-32.199,-52.701,-32.204,-52.69,-32.237,-52.69,-32.348,-52.653,-32.394,-52.625,-32.454,-52.593,-32.499,-52.591,-32.54,-52.598,-32.585]]]},{"n":"Manitoba","z":2,"b":[-99.549,50.202,-97.951,51.781],"p":[[[-97.959,50.357,-98.016,50.304,-98.072,50.274,-98.151,50.24,-98.242,50.214,-98.33,50.202,-98.402,50.204,-98.455,50.211,-98.519,50.242,-98.551,50.287,-98.549,50.421,-98.568,50.488,-98.608,50.568,-98.716,50.737,-98.815,50.864,-98.853,50.946,-98.941,51.008,-98.991,51.076,-99.006,51.114,-99.008,51.14,-98.988,51.152,-98.941,51.115,-98.881,51.037,-98.867,51.028,-98.847,51.039,-98.829,51.072,-98.836,51.1,-98.856,51.109,-98.897,51.188,-98.953,51.202,-98.979,51.223,-99.026,51.267,-99.077,51.339,-99.138,51.503,-99.163,51.543,-99.258,51.563,-99.32,51.617,-99.37,51.635,-99.426,51.627,-99.527,51.546,-99.542,51.578,-99.538,51.66,-99.549,51.699,-99.545,51.714,-99.491,51.758,-99.443,51.777,-99.39,51.781,-99.338,51.759,-99.292,51.716,-99.239,51.646,-99.217,51.636,-99.181,51.653,-99.166,51.675,-99.177,51.762,-99.166,51.777,-99.105,51.736,-99.064,51.686,-98.999,51.511,-98.956,51.433,-98.918,51.395,-98.907,51.404,-98.901,51.428,-98.911,51.513,-98.972,51.675,-98.929,51.702,-98.894,51.709,-98.865,51.707,-98.833,51.684,-98.79,51.639,-98.758,51.588,-98.752,51.552,-98.779,51.466,-98.778,51.41,-98.754,51.363,-98.705,51.326,-98.591,51.269,-98.572,51.243,-98.583,51.218,-98.633,51.165,-98.669,51.104,-98.718,51.088,-98.724,51.059,-98.754,51.016,-98.76,50.995,-98.74,50.935,-98.72,50.912,-98.5,50.86,-98.41,50.823,-98.244,50.719,-98.174,50.66,-98.071,50.552,-97.971,50.47,-97.951,50.42,-97.959,50.357]]]},{"n":"Winnipegosis","z":2,"b":[-101.102,51.59,-99.62,53.171],"p":[[[-99.674,51.613,-99.775,51.59,-99.858,51.643,-99.935,51.717,-99.95,51.763,-99.899,51.841,-99.953,51.999,-99.984,52.04,-100,52.044,-100.005,52.016,-99.984,51.812,-99.999,51.744,-100.011,51.731,-100.025,51.727,-100.049,51.756,-100.113,51.891,-100.159,52.116,-100.165,52.254,-100.156,52.306,-100.136,52.351,-100.027,52.464,-99.992,52.567,-100.045,52.649,-100.062,52.763,-100.096,52.796,-100.241,52.865,-100.268,52.872,-100.292,52.859,-100.326,52.833,-100.343,52.807,-100.326,52.794,-100.279,52.782,-100.242,52.64,-100.261,52.621,-100.275,52.623,-100.301,52.651,-100.339,52.672,-100.358,52.713,-100.418,52.773,-100.44,52.924,-100.453,52.953,-100.523,52.967,-100.564,52.996,-100.6,52.998,-100.665,52.98,-100.667,52.959,-100.621,52.886,-100.678,52.778,-100.746,52.752,-100.823,52.753,-100.839,52.767,-100.825,52.799,-100.833,52.813,-100.915,52.814,-100.941,52.845,-100.993,52.88,-100.984,52.931,-100.994,52.987,-101.007,53.008,-101.102,53.081,-101.091,53.1,-101.051,53.124,-100.964,53.147,-100.895,53.098,-100.82,53.091,-100.726,53.098,-100.644,53.115,-100.59,53.138,-100.543,53.17,-100.515,53.171,-100.335,53.107,-100.105,53.054,-99.844,52.975,-99.79,52.945,-99.757,52.913,-99.699,52.915,-99.685,52.91,-99.678,52.9,-99.705,52.812,-99.662,52.61,-99.661,52.539,-99.675,52.473,-99.713,52.407,-99.839,52.243,-99.827,52.152,-99.809,52.095,-99.749,52.028,-99.744,52.005,-99.744,51.973,-99.756,51.939,-99.798,51.876,-99.803,51.83,-99.781,51.761,-99.746,51.752,-99.666,51.854,-99.647,51.842,-99.642,51.72,-99.62,51.651,-99.624,51.629,-99.645,51.617,-99.674,51.613]]]},{"n":"Manicouagan","z":2,"b":[-69.25,50.716,-68.197,51.883],"p":[[[-68.255,51.399,-68.217,51.345,-68.204,51.306,-68.223,51.265,-68.313,51.192,-68.352,51.149,-68.447,51.12,-68.529,51.075,-68.642,50.991,-68.74,50.716,-68.744,50.776,-68.735,50.842,-68.743,50.922,-68.782,51.004,-68.868,51.094,-68.995,51.18,-69.148,51.274,-69.176,51.324,-69.161,51.391,-69.134,51.518,-69.155,51.58,-69.25,51.682,-69.232,51.7,-69.202,51.684,-69.138,51.656,-69.118,51.661,-69.099,51.709,-69.092,51.813,-69.063,51.838,-69.043,51.675,-69.01,51.633,-68.979,51.631,-68.935,51.648,-68.856,51.721,-68.808,51.683,-68.768,51.684,-68.7,51.691,-68.636,51.71,-68.596,51.756,-68.535,51.755,-68.501,51.811,-68.457,51.883,-68.437,51.874,-68.429,51.859,-68.443,51.818,-68.483,51.702,-68.461,51.672,-68.319,51.642,-68.252,51.686,-68.206,51.707,-68.197,51.692,-68.269,51.615,-68.267,51.572,-68.291,51.537,-68.286,51.507,-68.251,51.465,-68.255,51.399],[-68.538,51.393,-68.58,51.457,-68.562,51.48,-68.539,51.473,-68.473,51.402,-68.436,51.376,-68.391,51.368,-68.407,51.442,-68.396,51.503,-68.405,51.532,-68.486,51.591,-68.605,51.631,-68.721,51.632,-68.783,51.62,-68.844,51.6,-68.959,51.536,-69.054,51.472,-69.101,51.41,-69.092,51.368,-69.063,51.332,-69.046,51.302,-69.015,51.28,-68.924,51.242,-68.865,51.198,-68.758,51.165,-68.683,51.156,-68.596,51.168,-68.537,51.2,-68.468,51.214,-68.427,51.232,-68.4,51.256,-68.403,51.285,-68.492,51.349,-68.538,51.393]]]},{"n":"Syvash","z":2,"b":[33.696,45.255,35.439,46.267],"p":[[[35.399,45.255,35.276,45.302,35.089,45.358,35.062,45.379,35.043,45.402,35.04,45.429,35.08,45.499,35.052,45.565,35.024,45.604,35,45.631,34.882,45.687,34.834,45.725,34.762,45.743,34.743,45.786,34.683,45.804,34.676,45.819,34.645,45.837,34.495,45.785,34.495,45.803,34.618,45.915,34.619,45.935,34.608,45.944,34.568,45.952,34.525,45.907,34.497,45.899,34.46,45.917,34.412,45.89,34.39,45.888,34.38,45.901,34.388,45.95,34.382,45.979,34.363,46,34.271,46.027,34.227,46.021,34.19,46.03,34.182,46.016,34.187,45.977,34.187,45.956,34.164,45.944,34.14,45.963,34.133,46.01,34.121,46.026,34.072,46.059,33.998,46.072,33.978,46.052,33.96,46.04,33.924,46.039,33.885,46.06,33.831,46.143,33.815,46.142,33.771,46.133,33.708,46.182,33.696,46.2,33.704,46.215,33.728,46.228,33.778,46.234,33.848,46.231,33.922,46.213,34.03,46.157,34.078,46.142,34.107,46.144,34.103,46.182,34.112,46.193,34.13,46.19,34.147,46.191,34.164,46.203,34.158,46.248,34.159,46.267,34.175,46.266,34.236,46.214,34.27,46.191,34.298,46.181,34.312,46.18,34.329,46.19,34.544,46.161,34.555,46.138,34.548,46.115,34.492,46.113,34.469,46.101,34.429,46.022,34.443,45.999,34.479,45.983,34.523,45.988,34.588,46.021,34.64,46.153,34.667,46.173,34.716,46.165,34.754,46.131,34.755,46.113,34.718,46.09,34.713,46.053,34.74,46.035,34.789,46.024,34.816,45.959,34.84,45.843,34.909,45.793,35.032,45.638,35.182,45.472,35.3,45.364,35.426,45.295,35.439,45.275,35.399,45.255]]]},{"n":"Saimaa","z":2,"b":[26.899,61.068,29.982,63.584],"p":[[[29.518,62.123,29.492,62.089,29.47,62.08,29.492,62.046,29.552,62.003,29.615,61.983,29.638,62.005,29.685,62.004,29.757,61.949,29.775,61.906,29.777,61.889,29.733,61.874,29.727,61.848,29.694,61.834,29.66,61.826,29.61,61.802,29.531,61.778,29.477,61.742,29.426,61.734,29.29,61.649,29.266,61.615,29.115,61.6,29.098,61.578,28.941,61.626,28.73,61.671,28.629,61.642,28.608,61.617,28.76,61.61,28.79,61.59,28.722,61.581,28.68,61.558,28.679,61.524,28.693,61.484,28.646,61.476,28.582,61.473,28.568,61.441,28.464,61.399,28.406,61.37,28.345,61.349,28.36,61.331,28.477,61.332,28.641,61.352,28.663,61.368,28.714,61.352,28.82,61.297,28.867,61.247,28.788,61.204,28.61,61.162,28.359,61.093,28.292,61.08,27.988,61.08,27.947,61.068,27.919,61.08,27.892,61.114,27.882,61.157,27.966,61.188,28.082,61.179,28.172,61.153,28.197,61.163,28.174,61.188,28.072,61.233,27.862,61.252,27.678,61.232,27.625,61.214,27.519,61.23,27.406,61.259,27.432,61.27,27.435,61.296,27.368,61.326,27.303,61.329,27.27,61.324,27.265,61.379,27.301,61.433,27.274,61.468,27.298,61.484,27.296,61.557,27.276,61.653,27.329,61.693,27.327,61.632,27.339,61.597,27.359,61.531,27.459,61.523,27.476,61.539,27.472,61.55,27.471,61.599,27.488,61.618,27.499,61.6,27.515,61.577,27.593,61.581,27.665,61.607,27.678,61.636,27.758,61.656,27.773,61.685,27.705,61.731,27.623,61.764,27.605,61.781,27.626,61.801,27.816,61.728,27.866,61.696,27.918,61.657,27.921,61.627,27.946,61.601,28,61.549,28.054,61.511,28.104,61.522,28.129,61.549,28.147,61.551,28.185,61.544,28.234,61.621,28.248,61.716,28.262,61.744,28.306,61.756,28.339,61.749,28.369,61.751,28.442,61.768,28.539,61.841,28.672,61.826,28.807,61.828,28.84,61.846,28.838,61.863,28.737,61.891,28.697,61.921,28.579,61.993,28.427,62.052,28.403,62.069,28.297,62.097,28.168,62.096,28.086,62.1,28.032,62.108,27.926,62.133,27.899,62.157,27.902,62.244,27.885,62.306,27.877,62.329,27.852,62.354,27.844,62.376,27.8,62.429,27.755,62.429,27.679,62.412,27.597,62.418,27.547,62.412,27.489,62.443,27.459,62.479,27.419,62.496,27.367,62.482,27.297,62.477,27.236,62.507,27.178,62.553,27.159,62.577,27.191,62.572,27.295,62.518,27.339,62.519,27.352,62.544,27.373,62.562,27.398,62.576,27.6,62.494,27.647,62.498,27.67,62.513,27.669,62.528,27.608,62.601,27.619,62.71,27.648,62.735,27.666,62.76,27.65,62.816,27.667,62.874,27.684,62.912,27.652,62.925,27.621,62.916,27.588,62.9,27.564,62.877,27.56,62.851,27.494,62.863,27.415,62.902,27.391,62.941,27.33,63.001,27.176,63.084,27.195,63.099,27.207,63.122,27.193,63.169,27.197,63.229,27.193,63.289,27.213,63.309,27.246,63.308,27.239,63.332,27.19,63.388,27.175,63.438,27.135,63.496,27.068,63.523,27.038,63.517,27.02,63.501,26.959,63.524,26.899,63.566,26.899,63.584,27.113,63.546,27.183,63.518,27.234,63.457,27.234,63.427,27.211,63.417,27.25,63.376,27.297,63.356,27.338,63.346,27.363,63.316,27.347,63.279,27.337,63.213,27.264,63.146,27.358,63.068,27.44,63.023,27.559,63.001,27.605,63.015,27.677,62.979,27.723,62.954,27.77,62.946,27.774,62.996,27.736,63.059,27.767,63.1,27.81,63.092,27.848,63.071,27.851,63.032,27.888,63.019,28.006,63.04,28.106,63.009,28.279,63.031,28.352,63.04,28.35,63.026,28.308,63.003,28.262,62.984,28.192,62.964,28.154,62.933,28.187,62.857,28.144,62.825,28.149,62.804,28.181,62.772,28.211,62.73,28.239,62.706,28.268,62.689,28.264,62.663,28.653,62.527,28.799,62.456,28.898,62.39,28.81,62.382,28.659,62.42,28.529,62.474,28.496,62.501,28.565,62.499,28.555,62.523,28.482,62.555,28.424,62.575,28.367,62.587,28.34,62.577,28.274,62.565,28.211,62.508,28.191,62.448,28.169,62.434,28.103,62.442,28.106,62.476,28.138,62.538,28.057,62.69,27.979,62.732,27.849,62.719,27.766,62.68,27.755,62.652,27.801,62.627,27.758,62.583,27.846,62.524,27.915,62.43,28.04,62.409,28.042,62.379,27.991,62.337,27.963,62.298,27.97,62.259,28.067,62.244,28.144,62.252,28.276,62.256,28.277,62.276,28.324,62.287,28.358,62.27,28.406,62.237,28.506,62.22,28.566,62.222,28.608,62.279,28.63,62.294,28.67,62.306,28.699,62.328,28.668,62.368,28.666,62.387,28.693,62.383,28.765,62.348,28.835,62.267,28.87,62.235,28.852,62.212,28.837,62.185,28.786,62.177,28.826,62.134,28.976,62.142,29.139,62.169,29.198,62.177,29.33,62.166,29.345,62.186,29.353,62.219,29.319,62.242,29.298,62.275,29.298,62.313,29.278,62.349,29.265,62.394,29.297,62.426,29.3,62.476,29.261,62.551,29.279,62.599,29.335,62.6,29.352,62.571,29.364,62.53,29.422,62.523,29.509,62.467,29.626,62.419,29.65,62.427,29.612,62.462,29.589,62.5,29.593,62.527,29.616,62.559,29.633,62.597,29.697,62.609,29.776,62.577,29.825,62.518,29.885,62.463,29.926,62.434,29.835,62.389,29.802,62.362,29.793,62.335,29.768,62.313,29.688,62.316,29.641,62.31,29.62,62.294,29.599,62.27,29.648,62.223,29.727,62.199,29.791,62.2,29.874,62.185,29.96,62.164,29.982,62.133,29.911,62.115,29.708,62.151,29.688,62.138,29.607,62.145,29.518,62.123],[27.849,62.951,27.842,62.911,27.886,62.834,28.011,62.775,28.072,62.746,28.109,62.755,28.109,62.788,28.099,62.852,28.075,62.935,28.062,62.976,27.991,62.978,27.986,62.977,27.903,62.973,27.849,62.951],[27.637,61.273,27.677,61.31,27.659,61.355,27.629,61.383,27.556,61.397,27.509,61.409,27.418,61.437,27.36,61.422,27.328,61.4,27.368,61.369,27.475,61.315,27.588,61.266,27.637,61.273],[27.959,61.48,27.969,61.503,27.946,61.527,27.923,61.526,27.859,61.519,27.794,61.538,27.732,61.547,27.712,61.534,27.7,61.488,27.666,61.483,27.59,61.486,27.548,61.491,27.52,61.485,27.564,61.456,27.662,61.435,27.712,61.403,27.75,61.401,27.798,61.405,27.914,61.442,27.959,61.48],[28.496,61.478,28.445,61.516,28.459,61.558,28.493,61.602,28.536,61.622,28.515,61.699,28.476,61.723,28.388,61.712,28.328,61.73,28.277,61.71,28.281,61.665,28.286,61.623,28.308,61.582,28.243,61.539,28.24,61.523,28.255,61.497,28.245,61.473,28.226,61.443,28.24,61.394,28.307,61.408,28.416,61.416,28.495,61.444,28.496,61.478],[29.477,61.967,29.442,62.038,29.363,62.105,29.322,62.13,29.232,62.126,29.187,62.122,29.102,62.118,29.019,62.125,28.939,62.105,28.809,62.082,28.703,62.082,28.691,62.036,28.714,61.995,28.828,61.963,28.944,61.873,28.99,61.802,29.137,61.775,29.255,61.807,29.309,61.87,29.327,61.944,29.342,61.971,29.403,61.975,29.445,61.948,29.474,61.951,29.477,61.967]]]},{"n":"Zaysan","z":3,"b":[83.027,47.699,84.516,49.726],"p":[[[84.516,47.93,84.427,47.857,84.394,47.816,84.357,47.732,84.296,47.699,84.242,47.706,83.994,47.843,83.766,47.951,83.625,48.013,83.445,48.049,83.259,48.095,83.083,48.178,83.027,48.212,83.039,48.25,83.182,48.249,83.275,48.261,83.435,48.27,83.498,48.305,83.533,48.375,83.461,48.432,83.392,48.5,83.387,48.554,83.417,48.597,83.423,48.743,83.471,48.858,83.589,48.962,83.747,49.039,83.872,49.063,84.036,49.111,84.121,49.159,84.1,49.238,83.972,49.329,83.849,49.417,83.724,49.44,83.66,49.443,83.608,49.486,83.539,49.532,83.479,49.538,83.426,49.573,83.365,49.626,83.364,49.644,83.391,49.652,83.608,49.606,83.74,49.631,83.89,49.72,83.964,49.726,83.961,49.703,83.818,49.634,83.797,49.597,83.757,49.564,83.729,49.535,83.823,49.484,84.288,49.238,84.361,49.161,84.329,49.125,84.149,49.086,83.864,48.994,83.777,48.975,83.609,48.91,83.514,48.813,83.487,48.718,83.517,48.617,83.613,48.437,83.774,48.41,83.919,48.397,83.907,48.374,83.682,48.353,83.596,48.303,83.581,48.267,83.634,48.242,83.74,48.198,83.881,48.152,84.051,48.083,84.167,48.056,84.334,48.023,84.468,47.969,84.516,47.93]]]},{"n":"Churchill","z":3,"b":[-109.158,55.552,-108.074,56.813],"p":[[[-108.561,56.51,-108.634,56.488,-108.692,56.49,-108.715,56.521,-108.721,56.611,-108.736,56.668,-108.787,56.69,-108.859,56.709,-108.87,56.748,-108.837,56.784,-108.765,56.813,-108.703,56.792,-108.634,56.715,-108.571,56.696,-108.53,56.704,-108.495,56.693,-108.425,56.629,-108.412,56.464,-108.324,56.441,-108.218,56.435,-108.134,56.426,-108.074,56.382,-108.111,56.308,-108.189,56.271,-108.191,56.253,-108.174,56.234,-108.179,56.143,-108.122,56.063,-108.135,56.038,-108.178,56.015,-108.219,55.966,-108.247,55.915,-108.3,55.858,-108.486,55.765,-108.572,55.698,-108.605,55.648,-108.598,55.585,-108.627,55.552,-108.665,55.569,-108.687,55.628,-108.678,55.695,-108.688,55.749,-108.716,55.842,-108.759,55.903,-108.918,55.933,-109.034,55.93,-109.1,55.981,-109.158,56.068,-109.137,56.108,-109.086,56.122,-108.875,56.08,-108.671,55.979,-108.618,55.887,-108.6,55.848,-108.52,55.879,-108.467,55.936,-108.467,56.033,-108.433,56.104,-108.352,56.156,-108.273,56.191,-108.268,56.244,-108.315,56.29,-108.345,56.284,-108.376,56.285,-108.489,56.329,-108.539,56.366,-108.534,56.428,-108.53,56.479,-108.561,56.51]]]},{"n":"General Carrera/Buenos Aires","z":3,"b":[-72.904,-47.011,-71.166,-46.297],"p":[[[-71.166,-46.527,-71.207,-46.563,-71.376,-46.575,-71.628,-46.538,-71.782,-46.499,-71.998,-46.521,-72.258,-46.586,-72.554,-46.744,-72.774,-46.873,-72.815,-46.909,-72.826,-46.951,-72.86,-46.997,-72.904,-47.011,-72.87,-46.911,-72.789,-46.785,-72.707,-46.727,-72.661,-46.664,-72.695,-46.592,-72.724,-46.512,-72.692,-46.499,-72.645,-46.564,-72.585,-46.605,-72.478,-46.596,-72.237,-46.513,-71.988,-46.437,-71.872,-46.382,-71.88,-46.341,-71.872,-46.324,-71.847,-46.316,-71.755,-46.33,-71.626,-46.297,-71.605,-46.313,-71.578,-46.361,-71.442,-46.408,-71.313,-46.436,-71.221,-46.482,-71.166,-46.527]]]},{"n":"Päijänne","z":3,"b":[25.188,60.961,27.092,63.216],"p":[[[26.95,62.852,26.986,62.858,27.092,62.842,27.084,62.833,27.018,62.832,27.018,62.8,26.957,62.794,26.88,62.796,26.916,62.744,26.983,62.686,26.952,62.652,26.999,62.613,26.984,62.603,26.896,62.624,26.853,62.606,26.819,62.564,26.825,62.526,26.851,62.497,26.886,62.495,26.93,62.482,26.909,62.456,26.778,62.479,26.736,62.494,26.724,62.553,26.69,62.596,26.662,62.583,26.665,62.551,26.629,62.479,26.58,62.412,26.5,62.391,26.426,62.366,26.432,62.329,26.42,62.318,26.379,62.346,26.384,62.381,26.407,62.418,26.465,62.427,26.508,62.424,26.51,62.453,26.558,62.49,26.526,62.539,26.383,62.567,26.273,62.551,26.239,62.522,26.238,62.481,26.254,62.437,26.288,62.407,26.312,62.368,26.277,62.34,26.173,62.358,26.128,62.329,26.128,62.28,26.089,62.295,26.05,62.348,25.992,62.373,25.956,62.356,25.979,62.311,26.002,62.277,25.974,62.254,25.946,62.21,25.951,62.145,25.923,62.149,25.898,62.2,25.876,62.206,25.826,62.148,25.821,62.072,25.878,62.021,25.858,62.001,25.79,61.992,25.774,61.967,25.703,61.964,25.641,61.95,25.632,61.916,25.651,61.903,25.701,61.885,25.682,61.869,25.615,61.842,25.588,61.793,25.622,61.774,25.683,61.802,25.721,61.813,25.723,61.778,25.68,61.712,25.682,61.638,25.706,61.601,25.63,61.602,25.586,61.591,25.583,61.541,25.573,61.494,25.641,61.467,25.607,61.442,25.543,61.423,25.557,61.393,25.547,61.335,25.58,61.308,25.759,61.278,25.977,61.255,26.013,61.235,26.027,61.212,26.082,61.196,26.118,61.211,26.106,61.246,26.163,61.31,26.275,61.374,26.278,61.36,26.238,61.317,26.255,61.284,26.257,61.247,26.234,61.242,26.212,61.263,26.173,61.271,26.148,61.256,26.175,61.218,26.197,61.175,26.184,61.148,26.241,61.102,26.216,61.081,26.178,61.059,26.175,61.041,26.15,61.003,26.095,60.964,26.025,60.961,26.071,61.004,26.102,61.101,26.026,61.17,25.961,61.19,25.913,61.184,25.871,61.168,25.788,61.215,25.67,61.273,25.621,61.263,25.645,61.235,25.657,61.206,25.583,61.166,25.531,61.115,25.564,61.08,25.603,61.055,25.592,61.024,25.549,61.027,25.518,61.062,25.48,61.088,25.454,61.073,25.405,61.058,25.356,61.076,25.397,61.116,25.459,61.156,25.5,61.229,25.465,61.28,25.442,61.273,25.431,61.25,25.406,61.251,25.394,61.284,25.356,61.315,25.303,61.334,25.271,61.352,25.255,61.366,25.263,61.388,25.194,61.475,25.188,61.532,25.233,61.546,25.27,61.568,25.338,61.58,25.398,61.584,25.418,61.626,25.403,61.67,25.358,61.697,25.228,61.759,25.209,61.794,25.237,61.796,25.306,61.777,25.389,61.794,25.444,61.844,25.393,61.872,25.314,61.878,25.317,61.902,25.423,61.921,25.51,61.931,25.506,61.95,25.543,61.989,25.625,62.005,25.671,62.059,25.586,62.101,25.498,62.111,25.472,62.142,25.55,62.143,25.668,62.136,25.713,62.162,25.716,62.194,25.735,62.219,25.761,62.235,25.824,62.252,25.881,62.292,25.907,62.33,25.922,62.385,25.897,62.441,25.865,62.443,25.841,62.433,25.825,62.448,25.815,62.491,25.763,62.534,25.69,62.543,25.644,62.589,25.632,62.627,25.781,62.652,25.795,62.705,25.827,62.725,25.894,62.72,25.888,62.756,25.831,62.783,25.818,62.794,25.857,62.825,25.866,62.875,25.934,62.913,25.87,62.987,25.832,63.014,25.737,63.046,25.617,63.071,25.541,63.03,25.522,62.966,25.495,62.957,25.436,62.975,25.368,63.007,25.384,63.028,25.444,63.052,25.449,63.098,25.44,63.128,25.505,63.145,25.589,63.211,25.623,63.216,25.621,63.182,25.681,63.137,25.792,63.092,25.822,63.103,25.837,63.13,25.903,63.111,25.932,63.082,25.991,63.043,26.084,62.993,26.079,62.981,26.015,62.991,26.014,62.972,26.077,62.93,26.22,62.854,26.364,62.768,26.371,62.725,26.302,62.729,26.203,62.781,26.103,62.826,26.067,62.819,26.038,62.796,26.013,62.752,26.007,62.71,25.98,62.706,26.02,62.665,26.029,62.632,25.913,62.648,25.821,62.613,25.834,62.535,25.938,62.462,26.024,62.477,26.056,62.521,26.096,62.519,26.103,62.488,26.069,62.454,26.084,62.421,26.16,62.388,26.185,62.46,26.178,62.55,26.217,62.572,26.342,62.608,26.359,62.633,26.451,62.633,26.469,62.704,26.387,62.789,26.358,62.826,26.359,62.854,26.447,62.797,26.502,62.75,26.563,62.706,26.69,62.637,26.785,62.609,26.813,62.623,26.827,62.659,26.822,62.697,26.75,62.742,26.689,62.787,26.671,62.817,26.595,62.872,26.568,62.9,26.59,62.901,26.68,62.846,26.73,62.836,26.743,62.866,26.713,62.929,26.729,62.957,26.762,62.959,26.765,62.98,26.917,63.007,26.996,63.031,27.033,63.019,26.978,62.998,26.931,62.941,26.948,62.916,26.93,62.913,26.894,62.917,26.889,62.895,26.927,62.859,26.95,62.852]]]},{"n":"Srednee Kuyto","z":3,"b":[30.362,64.864,31.955,65.183],"p":[[[30.362,65.149,30.397,65.159,30.467,65.124,30.646,65.074,30.804,65.105,30.903,65.122,30.93,65.157,30.974,65.183,31.346,65.181,31.576,65.137,31.616,65.112,31.628,65.065,31.661,65.032,31.829,64.96,31.942,64.897,31.955,64.864,31.905,64.88,31.752,64.935,31.664,64.945,31.545,65.003,31.51,65.04,31.51,65.067,31.482,65.096,31.373,65.116,31.246,65.117,31.191,65.102,31.181,65.078,31.129,65.078,31.019,65.092,30.986,65.082,30.906,65.074,30.839,65.043,30.814,64.982,30.743,64.944,30.661,64.94,30.54,64.931,30.556,64.95,30.606,64.977,30.721,64.995,30.709,65.021,30.533,65.063,30.404,65.112,30.362,65.149]]]},{"n":"Kakhovka","z":4,"b":[33.54,46.822,35.302,47.842],"p":[[[35.302,47.492,35.269,47.45,35.213,47.45,35.049,47.428,34.961,47.406,34.808,47.463,34.592,47.52,34.419,47.487,34.19,47.42,34.177,47.389,34.183,47.346,34.173,47.325,34.097,47.316,33.904,47.159,33.718,46.941,33.7,46.899,33.687,46.861,33.559,46.822,33.54,46.831,33.576,46.883,33.63,47.034,33.655,47.05,33.667,47.042,33.705,47.061,33.761,47.119,33.777,47.163,33.835,47.212,33.933,47.282,33.963,47.331,33.965,47.359,33.947,47.451,33.936,47.541,33.984,47.556,34.067,47.558,34.148,47.542,34.211,47.536,34.24,47.575,34.266,47.59,34.316,47.567,34.38,47.558,34.436,47.569,34.487,47.593,34.731,47.578,34.997,47.588,35.088,47.676,35.086,47.824,35.106,47.842,35.13,47.84,35.242,47.647,35.302,47.492]]]},{"n":"","z":4,"b":[-105.063,21.796,-105.042,21.809],"p":[[[-105.042,21.801,-105.042,21.796,-105.063,21.809,-105.042,21.801]]]},{"n":"Kuybyshev","z":4,"b":[48.378,53.27,51.937,55.833],"p":[[[48.387,54.168,48.378,54.231,48.399,54.337,48.405,54.452,48.416,54.597,48.551,54.652,48.745,54.675,48.808,54.733,48.832,54.804,48.864,54.871,48.859,55.012,48.977,55.074,49.159,55.163,49.172,55.293,49.104,55.353,49.048,55.443,49.059,55.483,49.12,55.476,49.189,55.381,49.233,55.338,49.303,55.323,49.358,55.339,49.328,55.379,49.332,55.452,49.329,55.518,49.367,55.544,49.368,55.524,49.371,55.465,49.455,55.404,49.583,55.39,49.788,55.383,50,55.415,50.09,55.463,50.225,55.438,50.379,55.431,50.481,55.46,50.675,55.451,50.834,55.48,51.126,55.501,51.255,55.526,51.331,55.499,51.353,55.496,51.414,55.494,51.469,55.511,51.467,55.59,51.398,55.685,51.414,55.774,51.448,55.812,51.48,55.833,51.472,55.795,51.461,55.701,51.521,55.632,51.635,55.646,51.763,55.72,51.875,55.704,51.937,55.699,51.886,55.671,51.8,55.684,51.748,55.675,51.728,55.643,51.714,55.593,51.615,55.565,51.55,55.558,51.524,55.503,51.41,55.441,51.206,55.489,51.174,55.463,51.198,55.441,51.219,55.4,51.184,55.357,51.13,55.393,51.071,55.415,50.877,55.452,50.764,55.429,50.734,55.407,49.892,55.294,49.811,55.244,49.692,55.191,49.523,55.134,49.461,55.114,49.452,55.074,49.446,55.052,49.396,55.016,49.32,55.014,49.224,55.023,49.136,54.998,49.008,54.97,48.94,54.902,48.969,54.828,49.03,54.809,49.098,54.8,49.081,54.776,48.986,54.728,48.976,54.667,48.973,54.641,48.943,54.617,48.896,54.582,48.909,54.55,48.895,54.528,48.863,54.501,48.864,54.462,48.787,54.447,48.631,54.435,48.53,54.394,48.499,54.334,48.549,54.241,48.659,54.171,48.756,54.15,48.798,54.131,48.809,54.099,48.848,54.045,49.02,54.015,49.275,54.071,49.395,54.12,49.563,54.179,49.727,54.225,49.742,54.191,49.666,54.159,49.537,54.127,49.441,54.038,49.309,53.973,49.218,53.934,49.219,53.878,49.27,53.818,49.32,53.791,49.351,53.77,49.372,53.73,49.369,53.695,49.345,53.698,49.31,53.722,49.279,53.728,49.19,53.807,49.127,53.825,49.091,53.794,49.072,53.713,49.063,53.607,49.087,53.534,49.247,53.502,49.561,53.499,49.882,53.486,49.905,53.457,49.852,53.426,49.495,53.436,49.316,53.402,49.258,53.352,49.23,53.284,49.094,53.27,48.801,53.281,48.759,53.324,48.925,53.309,49.137,53.309,49.206,53.338,49.205,53.368,49.106,53.405,48.992,53.44,49.009,53.46,48.956,53.535,48.865,53.648,48.877,53.754,48.903,53.828,48.883,53.866,48.813,53.939,48.661,54.038,48.567,54.098,48.468,54.124,48.387,54.168]]]},{"n":"Mead","z":4,"b":[-114.816,35.831,-113.633,36.519],"p":[[[-113.636,35.831,-113.67,35.843,-113.726,35.922,-113.795,36.01,-113.895,36.079,-113.952,36.13,-113.989,36.165,-114.039,36.163,-114.11,36.032,-114.208,36.006,-114.294,36.03,-114.321,36.076,-114.373,36.1,-114.406,36.091,-114.451,36.079,-114.504,36.106,-114.64,36.106,-114.714,36.024,-114.741,36.014,-114.769,36.02,-114.812,36.075,-114.816,36.121,-114.653,36.154,-114.512,36.194,-114.432,36.237,-114.413,36.348,-114.387,36.483,-114.364,36.515,-114.336,36.519,-114.335,36.444,-114.359,36.335,-114.37,36.209,-114.295,36.11,-114.226,36.05,-114.159,36.077,-114.097,36.152,-114.03,36.245,-113.996,36.244,-113.924,36.176,-113.858,36.116,-113.777,36.048,-113.699,35.958,-113.633,35.859,-113.636,35.831]]]},{"n":"Powell","z":4,"b":[-111.53,36.919,-110.366,37.906],"p":[[[-110.368,37.898,-110.366,37.871,-110.406,37.81,-110.433,37.711,-110.561,37.582,-110.673,37.45,-110.697,37.393,-110.776,37.31,-110.836,37.225,-110.792,37.193,-110.65,37.208,-110.676,37.175,-110.792,37.17,-110.996,37.114,-111.112,37.076,-111.189,37.023,-111.307,37.016,-111.382,36.977,-111.455,36.919,-111.473,36.932,-111.53,37.029,-111.439,37.059,-111.386,37.055,-111.339,37.062,-111.304,37.099,-111.256,37.146,-111.211,37.119,-111.183,37.136,-111.163,37.165,-111.073,37.139,-110.963,37.151,-110.887,37.264,-110.858,37.335,-110.799,37.352,-110.736,37.431,-110.755,37.526,-110.733,37.535,-110.681,37.531,-110.596,37.592,-110.555,37.631,-110.515,37.673,-110.447,37.769,-110.435,37.871,-110.404,37.906,-110.368,37.898]]]},{"n":"Southern Indian","z":4,"b":[-99.686,56.641,-97.995,57.654],"p":[[[-98.295,57.493,-98.201,57.405,-98.098,57.392,-98.024,57.399,-97.995,57.387,-98.028,57.362,-98.109,57.317,-98.19,57.215,-98.226,57.136,-98.25,57.112,-98.392,57.094,-98.561,57.061,-98.685,56.979,-98.779,56.887,-98.855,56.841,-98.939,56.841,-99.002,56.862,-99.068,56.846,-99.218,56.736,-99.342,56.733,-99.47,56.733,-99.553,56.711,-99.544,56.683,-99.549,56.654,-99.571,56.641,-99.643,56.681,-99.686,56.73,-99.663,56.753,-99.546,56.776,-99.407,56.903,-99.378,56.96,-99.329,56.988,-99.226,56.951,-99.155,56.956,-99.124,57.005,-99.069,57.049,-99.015,57.085,-99.011,57.05,-99.037,56.975,-99.006,56.927,-98.905,56.925,-98.873,56.946,-98.88,57.004,-98.857,57.06,-98.775,57.073,-98.695,57.117,-98.661,57.18,-98.651,57.219,-98.668,57.262,-98.632,57.276,-98.542,57.275,-98.508,57.299,-98.518,57.35,-98.537,57.456,-98.593,57.529,-98.596,57.585,-98.556,57.611,-98.54,57.596,-98.544,57.566,-98.528,57.543,-98.472,57.549,-98.397,57.589,-98.293,57.638,-98.206,57.654,-98.208,57.625,-98.224,57.58,-98.271,57.53,-98.295,57.493]]]},{"n":"Sobradinho","z":4,"b":[-43.16,-11.257,-40.818,-9.004],"p":[[[-40.92,-9.257,-40.818,-9.406,-40.818,-9.438,-40.84,-9.455,-41.097,-9.5,-41.138,-9.577,-41.147,-9.664,-41.188,-9.702,-41.266,-9.71,-41.35,-9.736,-41.464,-9.794,-41.541,-9.801,-41.561,-9.789,-41.649,-9.819,-41.818,-9.82,-41.866,-9.77,-41.935,-9.787,-41.999,-9.833,-42.075,-9.888,-42.171,-10.024,-42.167,-10.081,-42.071,-10.115,-41.986,-10.152,-41.916,-10.237,-41.874,-10.3,-41.917,-10.29,-41.938,-10.253,-41.995,-10.196,-42.119,-10.151,-42.201,-10.145,-42.243,-10.114,-42.278,-10.107,-42.335,-10.098,-42.363,-10.192,-42.252,-10.462,-42.157,-10.668,-42.196,-10.746,-42.24,-10.896,-42.269,-10.931,-42.3,-10.949,-42.336,-11.001,-42.351,-10.999,-42.295,-10.84,-42.237,-10.66,-42.259,-10.548,-42.317,-10.495,-42.37,-10.427,-42.415,-10.415,-42.482,-10.467,-42.538,-10.55,-42.611,-10.675,-42.725,-10.842,-42.734,-10.875,-42.74,-11.044,-42.764,-11.142,-42.812,-11.187,-42.825,-11.251,-42.863,-11.249,-42.902,-11.257,-42.934,-11.244,-42.978,-11.174,-43.017,-11.138,-43.072,-11.109,-43.15,-11.143,-43.16,-11.115,-43.107,-10.996,-43.087,-10.94,-43.051,-10.916,-42.968,-10.832,-42.893,-10.774,-42.924,-10.713,-42.947,-10.69,-42.994,-10.664,-42.991,-10.623,-42.955,-10.594,-42.904,-10.651,-42.839,-10.741,-42.78,-10.744,-42.745,-10.706,-42.748,-10.615,-42.79,-10.561,-42.851,-10.498,-42.848,-10.462,-42.816,-10.462,-42.733,-10.517,-42.668,-10.538,-42.626,-10.509,-42.614,-10.48,-42.679,-10.398,-42.685,-10.354,-42.673,-10.288,-42.642,-10.301,-42.558,-10.352,-42.497,-10.331,-42.494,-10.267,-42.53,-10.212,-42.596,-10.093,-42.791,-9.998,-42.816,-9.962,-42.791,-9.945,-42.753,-9.956,-42.734,-9.916,-42.71,-9.954,-42.691,-9.967,-42.675,-9.955,-42.639,-9.973,-42.606,-9.999,-42.593,-10.031,-42.567,-10.071,-42.547,-10.078,-42.508,-10.069,-42.482,-10.044,-42.519,-9.983,-42.51,-9.948,-42.472,-9.959,-42.419,-9.995,-42.361,-9.985,-42.341,-9.949,-42.333,-9.906,-42.303,-9.872,-42.287,-9.834,-42.305,-9.772,-42.354,-9.708,-42.318,-9.696,-42.221,-9.696,-42.195,-9.613,-42.141,-9.607,-42.09,-9.592,-42.077,-9.546,-42.048,-9.511,-41.998,-9.517,-41.979,-9.573,-41.924,-9.618,-41.74,-9.565,-41.592,-9.591,-41.503,-9.586,-41.457,-9.596,-41.446,-9.591,-41.365,-9.622,-41.328,-9.608,-41.305,-9.591,-41.264,-9.594,-41.224,-9.511,-41.224,-9.432,-41.264,-9.362,-41.245,-9.346,-41.186,-9.366,-41.175,-9.352,-41.191,-9.309,-41.184,-9.283,-41.132,-9.291,-41.064,-9.282,-41.042,-9.242,-41.029,-9.194,-41.047,-9.149,-41.041,-9.125,-41.038,-9.058,-41.027,-9.004,-40.976,-9.018,-40.959,-9.038,-40.935,-9.057,-40.953,-9.174,-40.92,-9.257],[-42.803,-10.961,-42.849,-10.935,-42.839,-10.905,-42.804,-10.87,-42.772,-10.843,-42.783,-10.831,-42.847,-10.846,-42.891,-10.902,-42.991,-10.943,-42.996,-10.961,-42.899,-10.945,-42.879,-10.975,-42.918,-11.038,-42.931,-11.079,-42.91,-11.111,-42.879,-11.148,-42.849,-11.143,-42.807,-11.083,-42.803,-10.961]]]},{"n":"Toba","z":4.7,"b":[98.526,2.344,99.143,2.876],"p":[[[99.143,2.412,99.143,2.397,99.093,2.37,98.999,2.349,98.908,2.344,98.846,2.353,98.818,2.38,98.754,2.467,98.689,2.555,98.711,2.577,98.784,2.515,98.885,2.476,98.927,2.483,98.949,2.504,98.919,2.558,98.826,2.671,98.76,2.729,98.723,2.724,98.69,2.688,98.662,2.644,98.641,2.65,98.581,2.732,98.526,2.824,98.53,2.859,98.572,2.876,98.64,2.873,98.707,2.854,98.792,2.799,98.87,2.731,98.918,2.665,98.979,2.575,99.042,2.483,99.102,2.437,99.143,2.412]]]},{"n":"","z":4.7,"b":[25.891,61.542,26.57,61.841],"p":[[[26.57,61.623,26.567,61.616,26.545,61.609,26.473,61.612,26.346,61.655,26.24,61.718,26.183,61.73,26.121,61.716,26.077,61.704,26.072,61.679,26.085,61.657,26.103,61.648,26.123,61.657,26.144,61.669,26.167,61.653,26.183,61.629,26.172,61.6,26.158,61.565,26.148,61.546,26.114,61.542,26.059,61.555,26.007,61.59,25.958,61.619,25.9,61.647,25.891,61.675,25.931,61.691,25.955,61.717,25.966,61.743,25.953,61.767,25.936,61.786,25.944,61.796,26.032,61.772,26.126,61.767,26.176,61.798,26.219,61.841,26.237,61.841,26.246,61.822,26.265,61.785,26.278,61.751,26.279,61.733,26.295,61.716,26.329,61.714,26.354,61.715,26.406,61.717,26.438,61.735,26.466,61.735,26.507,61.718,26.543,61.688,26.563,61.648,26.57,61.623]]]},{"n":"Pukaki","z":4.7,"b":[170.131,-44.173,170.248,-43.704],"p":[[[170.248,-44.117,170.23,-44.173,170.204,-44.171,170.178,-44.17,170.178,-44.065,170.153,-43.914,170.131,-43.809,170.136,-43.766,170.152,-43.746,170.176,-43.723,170.198,-43.704,170.21,-43.714,170.2,-43.782,170.184,-43.831,170.213,-43.968,170.243,-44.075,170.248,-44.117]]]},{"n":"Ōhau","z":4.7,"b":[169.86,-44.295,169.995,-43.973],"p":[[[169.995,-44.287,169.945,-44.295,169.893,-44.277,169.875,-44.248,169.865,-44.224,169.87,-44.181,169.874,-44.087,169.86,-43.987,169.881,-43.973,169.885,-43.998,169.891,-44.039,169.912,-44.051,169.948,-44.008,169.98,-43.974,169.982,-44.017,169.925,-44.091,169.913,-44.172,169.921,-44.221,169.947,-44.255,169.962,-44.269,169.995,-44.287]]]},{"n":"Abitibi","z":4.7,"b":[-80.18,48.449,-79.226,48.935],"p":[[[-80.18,48.812,-80.153,48.835,-80.123,48.877,-80.105,48.913,-80.055,48.919,-79.952,48.935,-79.889,48.924,-79.865,48.89,-79.836,48.875,-79.836,48.853,-79.857,48.807,-79.855,48.765,-79.832,48.73,-79.77,48.717,-79.701,48.723,-79.654,48.739,-79.617,48.77,-79.576,48.797,-79.499,48.761,-79.413,48.715,-79.324,48.705,-79.259,48.69,-79.248,48.665,-79.257,48.599,-79.243,48.544,-79.233,48.503,-79.226,48.472,-79.247,48.456,-79.283,48.449,-79.313,48.452,-79.311,48.475,-79.312,48.498,-79.311,48.532,-79.295,48.578,-79.297,48.608,-79.319,48.631,-79.373,48.645,-79.439,48.648,-79.478,48.642,-79.483,48.63,-79.485,48.618,-79.511,48.613,-79.602,48.62,-79.701,48.641,-79.753,48.644,-79.804,48.632,-79.867,48.633,-79.916,48.659,-79.972,48.699,-79.981,48.717,-80,48.734,-80.017,48.748,-80.06,48.783,-80.091,48.775,-80.148,48.79,-80.18,48.812]]]},{"n":"Fort Peck","z":4.7,"b":[-108.196,47.39,-106.217,48.027],"p":[[[-106.217,47.585,-106.251,47.55,-106.269,47.548,-106.252,47.6,-106.251,47.644,-106.285,47.697,-106.318,47.755,-106.336,47.785,-106.36,47.813,-106.384,47.848,-106.42,47.876,-106.454,47.879,-106.48,47.858,-106.521,47.828,-106.55,47.785,-106.556,47.75,-106.599,47.753,-106.683,47.739,-106.768,47.724,-106.813,47.716,-106.838,47.694,-106.85,47.649,-106.862,47.631,-106.879,47.649,-106.906,47.672,-106.944,47.667,-106.993,47.644,-107.015,47.645,-107.03,47.666,-107.183,47.685,-107.306,47.685,-107.333,47.685,-107.386,47.654,-107.429,47.635,-107.507,47.624,-107.612,47.623,-107.719,47.563,-107.846,47.468,-107.911,47.39,-107.924,47.407,-107.909,47.471,-107.916,47.509,-107.93,47.542,-107.949,47.558,-107.982,47.569,-108.044,47.58,-108.17,47.584,-108.196,47.602,-108.173,47.613,-108.045,47.614,-107.959,47.601,-107.931,47.588,-107.889,47.538,-107.841,47.526,-107.779,47.57,-107.712,47.613,-107.685,47.646,-107.688,47.671,-107.714,47.688,-107.605,47.676,-107.455,47.662,-107.422,47.68,-107.38,47.716,-107.336,47.728,-107.295,47.722,-107.231,47.724,-107.121,47.725,-106.99,47.716,-106.93,47.725,-106.938,47.746,-106.933,47.764,-106.881,47.778,-106.821,47.783,-106.769,47.788,-106.738,47.806,-106.709,47.822,-106.655,47.841,-106.596,47.86,-106.582,47.879,-106.566,47.907,-106.574,47.936,-106.59,47.958,-106.538,47.983,-106.433,48.027,-106.406,48.021,-106.366,48.015,-106.325,47.985,-106.331,47.95,-106.358,47.925,-106.345,47.879,-106.285,47.837,-106.253,47.815,-106.267,47.793,-106.253,47.769,-106.241,47.739,-106.232,47.691,-106.217,47.628,-106.217,47.585]]]},{"n":"Oahe","z":4.7,"b":[-101.188,44.419,-100.255,46.714],"p":[[[-100.289,45.017,-100.341,45.006,-100.37,44.988,-100.382,44.951,-100.392,44.896,-100.427,44.847,-100.476,44.794,-100.532,44.763,-100.573,44.753,-100.625,44.767,-100.636,44.75,-100.614,44.669,-100.595,44.601,-100.559,44.584,-100.516,44.593,-100.488,44.573,-100.504,44.522,-100.477,44.499,-100.418,44.492,-100.397,44.47,-100.388,44.451,-100.394,44.434,-100.412,44.419,-100.478,44.443,-100.567,44.446,-100.61,44.47,-100.637,44.503,-100.62,44.531,-100.633,44.575,-100.671,44.627,-100.676,44.676,-100.682,44.714,-100.724,44.732,-100.774,44.747,-100.803,44.761,-100.848,44.764,-100.946,44.747,-101.072,44.732,-101.163,44.725,-101.188,44.727,-101.158,44.747,-101.071,44.764,-100.995,44.777,-100.976,44.798,-100.96,44.818,-100.908,44.82,-100.861,44.831,-100.805,44.834,-100.767,44.85,-100.778,44.882,-100.737,44.883,-100.67,44.867,-100.604,44.828,-100.559,44.811,-100.54,44.839,-100.5,44.87,-100.457,44.872,-100.441,44.897,-100.443,44.939,-100.447,44.999,-100.401,45.052,-100.347,45.102,-100.335,45.157,-100.323,45.215,-100.317,45.26,-100.345,45.305,-100.407,45.348,-100.543,45.353,-100.612,45.355,-100.528,45.377,-100.4,45.383,-100.344,45.39,-100.355,45.422,-100.378,45.447,-100.434,45.504,-100.543,45.583,-100.627,45.647,-100.637,45.662,-100.606,45.664,-100.531,45.636,-100.448,45.644,-100.4,45.687,-100.39,45.71,-100.398,45.747,-100.414,45.808,-100.478,45.89,-100.579,46.008,-100.659,46.12,-100.668,46.162,-100.641,46.223,-100.612,46.25,-100.594,46.314,-100.594,46.392,-100.606,46.489,-100.621,46.537,-100.634,46.58,-100.713,46.641,-100.767,46.673,-100.786,46.696,-100.777,46.714,-100.714,46.705,-100.667,46.669,-100.585,46.617,-100.564,46.559,-100.543,46.485,-100.527,46.386,-100.552,46.279,-100.574,46.187,-100.557,46.14,-100.559,46.096,-100.535,46.059,-100.493,45.988,-100.432,45.923,-100.377,45.895,-100.35,45.854,-100.342,45.767,-100.345,45.685,-100.354,45.645,-100.398,45.618,-100.408,45.564,-100.376,45.524,-100.307,45.469,-100.281,45.427,-100.279,45.369,-100.271,45.296,-100.255,45.242,-100.273,45.16,-100.286,45.088,-100.265,45.071,-100.262,45.044,-100.289,45.017]]]},{"n":"Teslin","z":4.7,"b":[-133.245,59.604,-132.089,60.449],"p":[[[-132.095,59.607,-132.125,59.604,-132.159,59.626,-132.213,59.688,-132.316,59.835,-132.399,59.922,-132.61,60.05,-132.834,60.135,-132.958,60.201,-133.012,60.251,-133.187,60.382,-133.245,60.449,-133.138,60.427,-132.98,60.297,-132.798,60.192,-132.702,60.153,-132.589,60.123,-132.522,60.098,-132.461,60.055,-132.364,59.986,-132.323,59.951,-132.277,59.902,-132.23,59.849,-132.225,59.838,-132.214,59.816,-132.202,59.77,-132.123,59.694,-132.089,59.644,-132.095,59.607]]]},{"n":"Arrow Lakes","z":4.7,"b":[-118.156,49.336,-117.907,49.802],"p":[[[-117.907,49.342,-117.957,49.336,-118.063,49.412,-118.096,49.45,-118.139,49.521,-118.156,49.601,-118.138,49.703,-118.128,49.752,-118.092,49.802,-118.084,49.774,-118.109,49.583,-118.087,49.515,-118.048,49.457,-117.976,49.406,-117.911,49.375,-117.907,49.342]]]},{"n":"Nasser","z":4.7,"b":[31.091,21.609,33.007,23.988],"p":[[[32.234,22.674,32.167,22.68,32.127,22.688,32.096,22.683,32.056,22.665,32.02,22.634,31.986,22.6,31.962,22.573,31.953,22.535,31.936,22.481,31.897,22.441,31.855,22.415,31.831,22.381,31.826,22.351,31.791,22.358,31.746,22.363,31.714,22.351,31.694,22.334,31.679,22.327,31.664,22.322,31.645,22.312,31.64,22.297,31.648,22.273,31.625,22.242,31.552,22.209,31.487,22.159,31.441,22.102,31.412,22.057,31.391,21.984,31.355,21.881,31.319,21.812,31.295,21.785,31.257,21.764,31.203,21.709,31.156,21.649,31.124,21.612,31.091,21.609,31.112,21.671,31.169,21.749,31.236,21.848,31.303,21.932,31.336,22.01,31.372,22.103,31.45,22.21,31.593,22.339,31.684,22.43,31.704,22.453,31.74,22.47,31.785,22.483,31.752,22.535,31.679,22.604,31.676,22.626,31.729,22.603,31.794,22.569,31.824,22.565,31.841,22.583,31.864,22.614,31.884,22.639,31.911,22.661,31.965,22.698,32.024,22.734,32.061,22.752,32.088,22.764,32.132,22.781,32.167,22.788,32.191,22.779,32.222,22.769,32.292,22.723,32.337,22.691,32.413,22.694,32.501,22.758,32.526,22.795,32.542,22.829,32.562,22.899,32.582,22.929,32.614,22.968,32.636,23.004,32.667,23.078,32.7,23.151,32.739,23.229,32.788,23.273,32.819,23.278,32.833,23.293,32.843,23.318,32.856,23.342,32.851,23.382,32.853,23.438,32.841,23.483,32.787,23.503,32.743,23.516,32.706,23.493,32.661,23.472,32.622,23.477,32.599,23.496,32.65,23.534,32.707,23.57,32.697,23.587,32.676,23.594,32.661,23.58,32.646,23.567,32.624,23.587,32.655,23.633,32.739,23.624,32.798,23.584,32.834,23.619,32.854,23.677,32.843,23.708,32.848,23.733,32.87,23.76,32.873,23.792,32.863,23.829,32.828,23.873,32.823,23.91,32.879,23.988,32.933,23.941,32.945,23.791,32.986,23.695,32.981,23.655,32.951,23.643,32.921,23.634,32.909,23.61,32.911,23.584,32.924,23.567,32.941,23.563,32.951,23.555,32.988,23.53,33.005,23.509,32.951,23.509,32.922,23.485,32.944,23.43,32.979,23.396,32.99,23.371,32.995,23.337,33.006,23.3,33.007,23.26,33.007,23.237,32.995,23.231,32.957,23.241,32.921,23.249,32.929,23.204,32.911,23.161,32.865,23.156,32.853,23.142,32.855,23.117,32.869,23.088,32.907,23.051,32.944,23.004,32.99,22.955,32.972,22.935,32.859,22.978,32.785,23.039,32.744,23.033,32.739,23.008,32.722,22.972,32.705,22.947,32.69,22.938,32.66,22.922,32.632,22.906,32.618,22.896,32.611,22.869,32.607,22.823,32.586,22.783,32.516,22.725,32.429,22.651,32.368,22.604,32.332,22.581,32.328,22.55,32.297,22.531,32.254,22.536,32.248,22.547,32.27,22.564,32.299,22.592,32.296,22.612,32.279,22.623,32.256,22.642,32.238,22.659,32.234,22.674]]]},{"n":"","z":4.7,"b":[23.773,61.031,24.702,61.721],"p":[[[24.608,61.654,24.558,61.64,24.493,61.614,24.456,61.59,24.507,61.552,24.469,61.528,24.342,61.52,24.265,61.496,24.194,61.457,24.172,61.423,24.169,61.404,24.153,61.376,24.128,61.344,24.158,61.328,24.24,61.315,24.279,61.293,24.245,61.289,24.161,61.28,24.08,61.26,24.024,61.247,24.014,61.231,24.041,61.207,24.092,61.185,24.176,61.183,24.242,61.195,24.296,61.187,24.355,61.162,24.372,61.145,24.354,61.142,24.33,61.139,24.337,61.12,24.396,61.071,24.439,61.031,24.414,61.036,24.352,61.073,24.277,61.093,24.03,61.154,23.875,61.183,23.781,61.214,23.773,61.283,23.803,61.26,23.845,61.228,23.919,61.211,23.952,61.224,23.981,61.256,24.021,61.282,24.035,61.312,24.017,61.369,24.043,61.423,24.171,61.484,24.254,61.529,24.306,61.553,24.358,61.587,24.333,61.607,24.309,61.621,24.321,61.633,24.368,61.629,24.422,61.621,24.454,61.626,24.481,61.647,24.497,61.668,24.491,61.679,24.524,61.687,24.604,61.703,24.691,61.721,24.702,61.702,24.674,61.668,24.659,61.656,24.635,61.656,24.608,61.654]]]},{"n":"Pyhäjärvi","z":4.7,"b":[23.349,61.262,24.728,62.266],"p":[[[23.511,61.419,23.507,61.391,23.528,61.369,23.568,61.348,23.646,61.331,23.707,61.317,23.712,61.304,23.717,61.287,23.725,61.27,23.717,61.262,23.675,61.276,23.572,61.296,23.485,61.331,23.44,61.371,23.38,61.382,23.349,61.39,23.365,61.403,23.416,61.418,23.509,61.451,23.639,61.491,23.709,61.511,23.677,61.531,23.638,61.556,23.627,61.589,23.599,61.623,23.57,61.641,23.603,61.647,23.632,61.652,23.627,61.665,23.603,61.688,23.621,61.712,23.667,61.728,23.667,61.745,23.68,61.777,23.692,61.81,23.703,61.825,23.725,61.836,23.708,61.865,23.751,61.877,23.838,61.87,23.861,61.879,23.859,61.894,23.865,61.911,23.951,61.944,24.032,61.974,24.025,61.987,24.016,62,24.015,62.038,24.003,62.076,23.974,62.09,23.873,62.095,23.738,62.103,23.681,62.116,23.684,62.125,23.673,62.137,23.649,62.166,23.638,62.197,23.645,62.215,23.675,62.227,23.699,62.227,23.691,62.213,23.683,62.191,23.701,62.164,23.75,62.159,23.81,62.155,23.855,62.14,23.885,62.142,23.906,62.154,23.915,62.188,23.928,62.221,23.968,62.233,23.999,62.232,23.979,62.208,23.953,62.178,23.954,62.16,23.98,62.16,24.034,62.154,24.122,62.151,24.181,62.157,24.146,62.143,24.123,62.123,24.117,62.092,24.126,62.057,24.181,62.045,24.22,62.045,24.257,62.08,24.285,62.117,24.301,62.12,24.321,62.115,24.335,62.103,24.319,62.073,24.304,62.042,24.324,62.03,24.375,62.029,24.498,62.027,24.565,62.045,24.524,62.081,24.459,62.099,24.384,62.119,24.343,62.146,24.363,62.156,24.438,62.141,24.533,62.106,24.595,62.09,24.611,62.109,24.599,62.146,24.582,62.18,24.585,62.197,24.601,62.201,24.615,62.205,24.613,62.227,24.611,62.251,24.63,62.263,24.662,62.266,24.674,62.229,24.682,62.194,24.722,62.198,24.728,62.181,24.709,62.134,24.686,62.095,24.63,62.043,24.634,61.986,24.684,61.949,24.706,61.921,24.678,61.929,24.599,61.965,24.549,61.993,24.482,62.001,24.417,61.995,24.417,61.987,24.377,61.99,24.294,61.996,24.181,62.01,24.094,62.02,24.119,61.988,24.105,61.951,24.053,61.927,24.004,61.907,23.962,61.894,23.954,61.88,23.852,61.835,23.758,61.786,23.776,61.756,23.786,61.729,23.767,61.703,23.743,61.673,23.739,61.651,23.82,61.646,23.834,61.636,23.772,61.618,23.784,61.612,23.798,61.599,23.791,61.56,23.776,61.521,23.73,61.495,23.611,61.46,23.511,61.419]]]},{"n":"Volta","z":4.7,"b":[-1.715,6.338,0.32,9.117],"p":[[[0.272,6.675,0.299,6.719,0.317,6.738,0.309,6.698,0.276,6.648,0.23,6.616,0.192,6.607,0.172,6.615,0.16,6.618,0.152,6.611,0.154,6.597,0.151,6.57,0.147,6.538,0.14,6.505,0.133,6.45,0.138,6.366,0.103,6.338,0.081,6.353,0.073,6.404,0.041,6.405,-0.011,6.379,-0.033,6.408,-0.06,6.452,-0.099,6.49,-0.175,6.533,-0.255,6.585,-0.324,6.622,-0.428,6.668,-0.55,6.717,-0.634,6.753,-0.674,6.802,-0.684,6.834,-0.66,6.836,-0.616,6.807,-0.58,6.785,-0.551,6.811,-0.518,6.794,-0.491,6.737,-0.467,6.723,-0.442,6.727,-0.419,6.731,-0.409,6.766,-0.406,6.816,-0.393,6.828,-0.368,6.773,-0.335,6.716,-0.305,6.717,-0.271,6.722,-0.228,6.734,-0.194,6.741,-0.175,6.724,-0.163,6.686,-0.144,6.648,-0.096,6.629,-0.057,6.63,-0.041,6.677,-0.026,6.72,-0.017,6.733,-0.023,6.748,-0.049,6.769,-0.067,6.789,-0.071,6.809,-0.066,6.825,-0.042,6.847,-0.011,6.853,0.012,6.845,0.03,6.85,0.077,6.87,0.112,6.897,0.104,6.91,0.059,6.914,0.042,6.946,0.087,6.974,0.12,6.971,0.123,6.983,0.099,7.004,0.088,7.023,0.093,7.037,0.101,7.053,0.098,7.08,0.086,7.121,0.085,7.147,0.066,7.151,0.045,7.164,0.028,7.176,0.01,7.187,0.007,7.209,0.015,7.211,0.035,7.2,0.063,7.195,0.083,7.194,0.099,7.193,0.118,7.203,0.142,7.222,0.153,7.241,0.16,7.252,0.174,7.252,0.184,7.266,0.184,7.287,0.172,7.304,0.156,7.309,0.135,7.313,0.102,7.343,0.068,7.393,0.05,7.432,0.051,7.449,0.047,7.46,0.046,7.478,-0.002,7.5,-0.063,7.508,-0.058,7.466,-0.066,7.444,-0.107,7.458,-0.128,7.472,-0.136,7.496,-0.157,7.51,-0.207,7.512,-0.242,7.517,-0.232,7.532,-0.208,7.546,-0.169,7.55,-0.134,7.553,-0.105,7.551,-0.071,7.549,-0.027,7.546,0.012,7.543,-0.018,7.592,-0.064,7.641,-0.11,7.645,-0.182,7.647,-0.242,7.639,-0.336,7.618,-0.432,7.594,-0.475,7.589,-0.498,7.597,-0.531,7.576,-0.603,7.554,-0.654,7.551,-0.611,7.567,-0.543,7.6,-0.483,7.625,-0.422,7.649,-0.357,7.664,-0.294,7.667,-0.262,7.684,-0.256,7.706,-0.254,7.72,-0.21,7.714,-0.161,7.721,-0.125,7.74,-0.098,7.746,-0.1,7.755,-0.134,7.767,-0.157,7.779,-0.159,7.794,-0.152,7.813,-0.152,7.841,-0.17,7.89,-0.186,7.927,-0.192,7.955,-0.224,7.977,-0.285,8.015,-0.315,8.04,-0.344,8.038,-0.351,8.002,-0.384,7.98,-0.417,7.972,-0.394,8,-0.379,8.048,-0.415,8.079,-0.446,8.091,-0.498,8.057,-0.527,8.062,-0.526,8.128,-0.543,8.154,-0.561,8.134,-0.591,8.1,-0.624,8.085,-0.643,8.087,-0.643,8.106,-0.693,8.11,-0.756,8.102,-0.761,8.118,-0.748,8.135,-0.721,8.135,-0.655,8.142,-0.604,8.159,-0.6,8.173,-0.621,8.191,-0.676,8.239,-0.724,8.28,-0.754,8.297,-0.795,8.315,-0.801,8.334,-0.784,8.366,-0.838,8.447,-0.912,8.555,-0.965,8.604,-0.999,8.63,-1.041,8.646,-1.087,8.64,-1.128,8.632,-1.141,8.595,-1.153,8.551,-1.18,8.604,-1.219,8.658,-1.254,8.668,-1.304,8.697,-1.351,8.724,-1.405,8.739,-1.443,8.731,-1.488,8.697,-1.545,8.664,-1.605,8.644,-1.664,8.629,-1.715,8.63,-1.663,8.673,-1.591,8.695,-1.519,8.731,-1.466,8.761,-1.412,8.781,-1.376,8.805,-1.355,8.792,-1.294,8.75,-1.269,8.726,-1.126,8.705,-1.062,8.722,-1.067,8.75,-1.096,8.784,-1.134,8.813,-1.145,8.874,-1.174,8.945,-1.179,8.979,-1.178,9.027,-1.174,9.074,-1.152,9.117,-1.13,9.076,-1.127,9.028,-1.125,8.981,-1.121,8.929,-1.106,8.897,-1.075,8.853,-1.045,8.843,-1.026,8.844,-0.997,8.775,-0.931,8.653,-0.873,8.586,-0.843,8.579,-0.817,8.6,-0.818,8.65,-0.823,8.707,-0.809,8.743,-0.791,8.761,-0.782,8.756,-0.795,8.721,-0.806,8.689,-0.77,8.656,-0.722,8.639,-0.705,8.643,-0.707,8.625,-0.757,8.578,-0.805,8.52,-0.797,8.497,-0.782,8.469,-0.721,8.402,-0.66,8.357,-0.627,8.329,-0.594,8.288,-0.566,8.275,-0.554,8.251,-0.527,8.232,-0.485,8.209,-0.449,8.209,-0.394,8.221,-0.355,8.243,-0.318,8.244,-0.286,8.253,-0.245,8.245,-0.202,8.213,-0.193,8.186,-0.257,8.185,-0.321,8.177,-0.33,8.158,-0.308,8.142,-0.263,8.108,-0.19,8.053,-0.128,8.011,-0.103,7.976,-0.082,7.876,-0.052,7.811,-0.024,7.793,0.002,7.793,0.019,7.789,0.055,7.792,0.07,7.821,0.058,7.863,0.056,7.886,0.059,7.92,0.073,7.969,0.079,8.003,0.062,8.041,0.07,8.081,0.099,8.093,0.129,8.086,0.154,8.078,0.17,8.086,0.165,8.115,0.152,8.161,0.123,8.21,0.105,8.242,0.137,8.254,0.169,8.289,0.198,8.338,0.217,8.363,0.161,8.381,0.092,8.403,0.081,8.429,0.093,8.461,0.091,8.5,0.085,8.548,0.106,8.583,0.135,8.598,0.15,8.633,0.16,8.7,0.184,8.742,0.198,8.738,0.185,8.69,0.168,8.625,0.152,8.592,0.128,8.574,0.121,8.518,0.113,8.457,0.103,8.434,0.115,8.416,0.178,8.399,0.242,8.379,0.261,8.355,0.28,8.341,0.294,8.334,0.284,8.327,0.242,8.322,0.214,8.314,0.217,8.296,0.19,8.251,0.184,8.206,0.201,8.166,0.231,8.125,0.268,8.106,0.267,8.094,0.24,8.088,0.198,8.079,0.161,8.06,0.146,8.021,0.121,7.972,0.108,7.939,0.124,7.913,0.146,7.894,0.168,7.863,0.221,7.883,0.258,7.905,0.251,7.876,0.252,7.863,0.248,7.849,0.237,7.834,0.248,7.803,0.278,7.762,0.279,7.699,0.281,7.617,0.282,7.575,0.259,7.581,0.24,7.578,0.228,7.556,0.229,7.536,0.242,7.511,0.25,7.491,0.285,7.515,0.32,7.542,0.313,7.506,0.302,7.47,0.297,7.453,0.295,7.416,0.287,7.382,0.276,7.353,0.277,7.339,0.277,7.33,0.292,7.303,0.294,7.278,0.276,7.258,0.272,7.234,0.279,7.218,0.281,7.201,0.279,7.182,0.276,7.172,0.271,7.138,0.289,7.106,0.305,7.099,0.3,7.071,0.281,7.047,0.273,7.001,0.263,6.948,0.24,6.896,0.237,6.768,0.254,6.673,0.272,6.675],[0.13,6.679,0.181,6.726,0.159,6.767,0.115,6.796,0.062,6.808,0.017,6.779,0.011,6.718,0.049,6.695,0.09,6.659,0.13,6.679]]]},{"n":"Bratsk","z":5,"b":[101.275,53.107,103.743,56.349],"p":[[[103.533,53.636,103.725,53.602,103.737,53.554,103.743,53.496,103.726,53.484,103.686,53.512,103.609,53.552,103.49,53.563,103.44,53.519,103.425,53.473,103.44,53.223,103.374,53.107,103.343,53.228,103.343,53.572,103.275,53.677,103.096,53.742,102.868,53.79,102.927,53.82,103.117,53.774,103.208,53.806,103.119,53.938,103.003,54.068,102.987,54.156,103.017,54.283,103.106,54.415,103.173,54.424,103.218,54.487,103.186,54.627,103.089,54.713,103.033,54.782,103.07,54.921,103.226,55.046,103.282,55.159,103.272,55.302,103.179,55.486,103.149,55.578,103.139,55.685,103.074,55.789,102.99,55.854,102.961,55.958,102.921,56.063,102.852,56.104,102.797,56.108,102.717,56.143,102.593,56.156,102.53,56.158,102.506,56.156,102.494,56.127,102.48,56.108,102.439,56.091,102.393,56.088,102.309,56.11,102.236,56.103,102.18,56.068,101.986,56.026,102,55.961,101.978,55.903,101.906,55.834,101.913,55.783,101.99,55.731,102.085,55.718,102.094,55.696,102.051,55.663,102.102,55.628,102.332,55.59,102.386,55.544,102.366,55.485,102.315,55.436,102.311,55.401,102.327,55.353,102.342,55.32,102.39,55.302,102.43,55.285,102.39,55.262,102.344,55.232,102.32,55.195,102.285,55.143,102.233,55.101,102.186,55.07,102.118,55.053,102.142,55.08,102.184,55.118,102.208,55.146,102.259,55.183,102.274,55.222,102.33,55.271,102.31,55.301,102.284,55.345,102.26,55.409,102.268,55.445,102.307,55.495,102.3,55.53,102.217,55.514,102.072,55.457,101.789,55.414,101.761,55.436,101.757,55.469,101.979,55.523,101.97,55.563,101.91,55.59,101.806,55.654,101.788,55.772,101.741,55.807,101.504,55.889,101.411,55.923,101.322,55.946,101.275,55.982,101.352,55.987,101.483,55.968,101.613,55.969,101.633,55.977,101.706,55.973,101.73,56.014,101.732,56.07,101.699,56.095,101.672,56.135,101.706,56.175,101.697,56.218,101.71,56.264,101.767,56.287,101.803,56.291,101.867,56.236,101.931,56.242,101.964,56.227,101.922,56.167,101.945,56.125,102.054,56.156,102.255,56.227,102.26,56.255,102.462,56.253,102.474,56.311,102.522,56.349,102.582,56.332,102.588,56.298,102.626,56.253,102.727,56.25,102.799,56.262,102.823,56.235,102.872,56.199,102.941,56.197,103.001,56.223,103.023,56.214,103.009,56.144,103.02,56.045,103.063,55.92,103.143,55.808,103.247,55.714,103.249,55.672,103.226,55.558,103.334,55.358,103.364,55.219,103.348,55.132,103.304,55.02,103.185,54.878,103.154,54.801,103.216,54.719,103.279,54.578,103.286,54.482,103.349,54.323,103.425,54.205,103.463,54.16,103.397,54.17,103.318,54.303,103.261,54.342,103.205,54.311,103.144,54.239,103.103,54.147,103.16,54.001,103.298,53.874,103.3,53.841,103.33,53.779,103.435,53.679,103.533,53.636]]]},{"n":"Khantayskoye","z":5,"b":[89.313,68.24,92.331,68.509],"p":[[[91.937,68.489,92.104,68.509,92.222,68.46,92.331,68.426,92.114,68.408,91.898,68.398,91.541,68.397,91.105,68.368,90.92,68.311,90.533,68.287,89.935,68.24,89.807,68.263,89.772,68.305,89.667,68.315,89.556,68.31,89.472,68.313,89.399,68.33,89.313,68.335,89.33,68.351,89.315,68.388,89.523,68.399,90.232,68.369,90.515,68.39,90.658,68.407,90.869,68.405,91.092,68.443,91.483,68.44,91.937,68.489]]]},{"n":"Keta","z":5,"b":[89.547,68.7,91.713,68.862],"p":[[[91.306,68.793,90.843,68.755,90.544,68.71,90.356,68.7,90.05,68.702,90.006,68.707,89.989,68.727,89.547,68.819,89.582,68.848,89.737,68.856,89.999,68.837,90.223,68.787,90.445,68.758,90.684,68.779,90.787,68.799,90.918,68.816,91.192,68.832,91.335,68.831,91.452,68.836,91.713,68.862,91.484,68.787,91.306,68.793]]]},{"n":"Lama","z":5,"b":[88.825,69.323,91.559,69.625],"p":[[[89.469,69.378,89.225,69.349,88.958,69.323,88.825,69.339,88.845,69.371,89.123,69.425,89.346,69.459,89.546,69.495,89.901,69.603,90.205,69.625,90.523,69.577,91.08,69.488,91.436,69.51,91.559,69.418,90.988,69.459,90.66,69.453,90.408,69.519,90.099,69.539,89.761,69.489,89.582,69.444,89.469,69.378]]]},{"n":"Uddjaure","z":5,"b":[17.346,65.852,18.009,66.201],"p":[[[17.967,65.852,17.955,65.864,17.926,65.88,17.882,65.89,17.815,65.866,17.74,65.857,17.693,65.875,17.691,65.905,17.704,65.931,17.736,65.949,17.72,65.979,17.637,66.012,17.591,66.014,17.577,66.005,17.546,66.012,17.517,66.05,17.46,66.066,17.401,66.051,17.36,66.047,17.346,66.08,17.395,66.1,17.471,66.113,17.547,66.108,17.595,66.085,17.672,66.067,17.733,66.083,17.723,66.115,17.615,66.168,17.621,66.193,17.676,66.201,17.777,66.187,17.835,66.155,17.968,66.106,18.009,66.051,17.964,66.048,17.945,65.997,17.949,65.959,17.948,65.934,17.964,65.912,18.001,65.882,17.967,65.852]]]},{"n":"Storavan","z":5,"b":[17.967,65.616,18.509,65.882],"p":[[[18.001,65.882,18.031,65.857,18.14,65.778,18.286,65.776,18.37,65.752,18.381,65.73,18.424,65.7,18.509,65.66,18.378,65.648,18.255,65.677,18.173,65.664,18.151,65.644,18.161,65.621,18.131,65.616,18.093,65.629,18.083,65.649,18.071,65.715,18.036,65.774,17.967,65.852,18.001,65.882]]]},{"n":"","z":5,"b":[13.71,63.754,15.824,64.66],"p":[[[15.755,63.758,15.642,63.808,15.576,63.784,15.542,63.76,15.536,63.798,15.52,63.85,15.303,63.932,15.263,63.961,15.39,63.926,15.417,63.939,15.419,63.959,15.376,63.997,15.316,64.031,15.319,64.11,15.24,64.157,15.125,64.245,15.037,64.285,14.328,64.388,14.192,64.447,14.093,64.519,13.961,64.563,13.71,64.636,13.802,64.66,14.096,64.557,14.195,64.481,14.356,64.42,14.493,64.417,14.664,64.383,14.851,64.347,14.94,64.344,14.991,64.336,15.013,64.334,15.048,64.338,15.111,64.341,15.21,64.322,15.261,64.262,15.322,64.163,15.419,64.075,15.394,64.051,15.395,64.019,15.471,63.967,15.578,63.868,15.745,63.83,15.824,63.754,15.755,63.758]]]},{"n":"","z":5,"b":[13.198,64.671,13.635,64.785],"p":[[[13.29,64.738,13.399,64.785,13.527,64.727,13.635,64.686,13.6,64.675,13.368,64.693,13.234,64.671,13.198,64.684,13.29,64.738]]]},{"n":"Habbaniyah","z":5,"b":[43.304,33.19,43.595,33.435],"p":[[[43.595,33.306,43.566,33.219,43.483,33.19,43.434,33.199,43.402,33.257,43.304,33.309,43.309,33.349,43.382,33.435,43.428,33.39,43.477,33.338,43.554,33.348,43.595,33.306]]]},{"n":"Skadar","z":5,"b":[19.099,42.079,19.481,42.29],"p":[[[19.293,42.265,19.364,42.29,19.392,42.228,19.451,42.157,19.481,42.108,19.467,42.079,19.394,42.088,19.216,42.158,19.099,42.278,19.175,42.277,19.293,42.265]]]},{"n":"Aylmer","z":5,"b":[-109.639,63.945,-107.932,64.413],"p":[[[-107.932,64.036,-108.084,64.045,-108.161,64.038,-108.252,64.022,-108.372,63.983,-108.459,63.945,-108.661,63.988,-108.749,64.036,-108.858,64.065,-109.114,64.076,-109.394,64.02,-109.571,64.018,-109.639,64.051,-109.363,64.066,-109.154,64.104,-109.029,64.103,-108.634,64.17,-108.598,64.195,-108.601,64.227,-108.623,64.276,-108.602,64.331,-108.541,64.388,-108.467,64.413,-108.432,64.398,-108.438,64.314,-108.419,64.267,-108.361,64.278,-108.285,64.317,-108.247,64.316,-108.261,64.277,-108.335,64.209,-108.358,64.181,-108.336,64.156,-108.263,64.142,-108.257,64.16,-108.186,64.177,-108.102,64.137,-107.945,64.094,-107.932,64.036]]]},{"n":"Clinton-Colden","z":5,"b":[-107.945,63.776,-107.088,64.131],"p":[[[-107.945,64.094,-107.837,64.089,-107.752,64.071,-107.675,64.081,-107.589,64.112,-107.464,64.128,-107.323,64.131,-107.238,64.1,-107.233,64.067,-107.24,64.037,-107.24,64.022,-107.269,63.983,-107.222,63.926,-107.104,63.924,-107.088,63.912,-107.106,63.894,-107.2,63.824,-107.274,63.776,-107.502,63.789,-107.529,63.798,-107.557,63.833,-107.632,63.877,-107.614,63.937,-107.622,63.97,-107.668,63.985,-107.789,63.981,-107.805,63.956,-107.836,63.983,-107.932,64.036,-107.945,64.094]]]},{"n":"Francis Case","z":5,"b":[-99.471,43.059,-98.56,43.991],"p":[[[-99.147,43.433,-99.04,43.358,-98.905,43.285,-98.831,43.206,-98.706,43.135,-98.571,43.102,-98.56,43.06,-98.666,43.059,-98.747,43.102,-98.9,43.159,-98.93,43.226,-99.069,43.31,-99.178,43.374,-99.315,43.468,-99.344,43.539,-99.383,43.606,-99.471,43.669,-99.441,43.752,-99.365,43.843,-99.352,43.888,-99.374,43.947,-99.364,43.991,-99.322,43.957,-99.303,43.903,-99.315,43.841,-99.37,43.757,-99.411,43.703,-99.35,43.643,-99.244,43.477,-99.147,43.433]]]},{"n":"Falcon International","z":5,"b":[-99.464,26.562,-99.113,27.057],"p":[[[-99.184,26.76,-99.15,26.751,-99.119,26.749,-99.113,26.727,-99.149,26.652,-99.15,26.592,-99.172,26.564,-99.2,26.562,-99.243,26.584,-99.255,26.67,-99.283,26.76,-99.331,26.803,-99.394,26.795,-99.386,26.807,-99.364,26.821,-99.368,26.886,-99.401,26.954,-99.464,27.025,-99.457,27.057,-99.415,27.044,-99.369,27.002,-99.277,26.921,-99.218,26.91,-99.218,26.805,-99.184,26.76]]]},{"n":"Île-à-la-Crosse","z":5,"b":[-108.044,55.211,-107.666,55.921],"p":[[[-107.789,55.211,-107.843,55.303,-107.963,55.42,-107.999,55.471,-108.044,55.508,-108.036,55.554,-107.985,55.575,-107.85,55.528,-107.806,55.553,-107.851,55.618,-107.857,55.661,-107.805,55.665,-107.769,55.698,-107.767,55.802,-107.766,55.894,-107.737,55.921,-107.666,55.744,-107.706,55.582,-107.74,55.48,-107.74,55.408,-107.792,55.367,-107.769,55.267,-107.789,55.211]]]},{"n":"Wholdaia","z":5,"b":[-104.643,60.341,-103.647,61.026],"p":[[[-103.836,60.554,-103.907,60.505,-103.935,60.477,-103.949,60.421,-104.117,60.355,-104.187,60.341,-104.152,60.425,-104.135,60.533,-104.229,60.587,-104.332,60.633,-104.395,60.685,-104.468,60.703,-104.494,60.726,-104.39,60.809,-104.343,60.871,-104.38,60.901,-104.425,60.885,-104.44,60.852,-104.505,60.818,-104.588,60.813,-104.637,60.835,-104.643,60.856,-104.53,60.86,-104.509,60.883,-104.45,60.973,-104.449,61.001,-104.419,61.022,-104.335,61.026,-104.297,61.005,-104.281,60.981,-104.213,60.938,-104.212,60.903,-104.23,60.846,-104.219,60.814,-104.141,60.826,-104.139,60.805,-104.219,60.757,-104.217,60.738,-104.124,60.752,-104.073,60.781,-104.02,60.793,-103.983,60.785,-103.851,60.824,-103.769,60.866,-103.752,60.909,-103.742,60.945,-103.716,60.952,-103.647,60.902,-103.667,60.835,-103.813,60.785,-103.97,60.707,-104.082,60.693,-104.137,60.668,-104.123,60.624,-104.042,60.602,-103.937,60.614,-103.879,60.641,-103.859,60.677,-103.81,60.71,-103.746,60.701,-103.736,60.642,-103.836,60.554]]]},{"n":"Tehek","z":5,"b":[-96.032,64.762,-95.249,65.042],"p":[[[-95.249,65.003,-95.39,64.972,-95.463,64.921,-95.465,64.859,-95.456,64.814,-95.459,64.789,-95.487,64.766,-95.55,64.762,-95.593,64.782,-95.62,64.806,-95.625,64.826,-95.636,64.846,-95.712,64.874,-95.799,64.923,-95.858,64.915,-95.87,64.871,-95.881,64.851,-95.993,64.891,-96.032,64.923,-96.003,64.974,-95.931,65.003,-95.855,65.011,-95.726,65.022,-95.632,65.039,-95.503,65.042,-95.25,65.034,-95.249,65.003]]]},{"n":"Caniapiscau","z":5,"b":[-70.141,53.946,-69.269,54.379],"p":[[[-69.351,54.083,-69.269,54.047,-69.32,54.025,-69.412,54.036,-69.502,54.019,-69.595,53.998,-69.722,53.961,-69.782,53.947,-69.834,53.946,-69.869,53.955,-69.856,53.977,-69.827,53.999,-69.741,54.024,-69.703,54.038,-69.707,54.069,-69.758,54.078,-69.847,54.101,-69.983,54.139,-70.055,54.134,-70.129,54.11,-70.141,54.129,-70.084,54.178,-70.011,54.223,-69.98,54.237,-69.926,54.252,-69.879,54.261,-69.869,54.289,-69.88,54.311,-69.927,54.32,-69.979,54.318,-70.001,54.325,-70.029,54.337,-70.071,54.337,-70.082,54.343,-70.041,54.372,-69.971,54.365,-69.901,54.379,-69.858,54.368,-69.804,54.312,-69.793,54.283,-69.682,54.268,-69.687,54.24,-69.671,54.225,-69.664,54.21,-69.704,54.172,-69.697,54.153,-69.588,54.107,-69.477,54.092,-69.433,54.111,-69.406,54.121,-69.351,54.083]]]},{"n":"Bermen","z":5,"b":[-69.243,53.428,-68.68,54.042],"p":[[[-68.68,53.798,-68.705,53.775,-68.726,53.689,-68.707,53.61,-68.716,53.583,-68.777,53.577,-68.853,53.594,-68.869,53.576,-68.869,53.523,-68.9,53.483,-68.935,53.469,-68.976,53.456,-69.055,53.428,-69.085,53.444,-69.045,53.492,-68.984,53.559,-68.947,53.609,-68.868,53.645,-68.827,53.676,-68.837,53.717,-68.866,53.737,-68.85,53.755,-68.805,53.765,-68.781,53.775,-68.767,53.796,-68.814,53.846,-68.907,53.876,-68.967,53.908,-69.008,53.94,-69.061,53.958,-69.225,53.949,-69.243,53.961,-69.231,53.979,-69.2,53.989,-69.171,53.99,-69.154,54,-69.16,54.023,-69.146,54.04,-69.116,54.023,-69.062,54.007,-69.022,54.028,-69.005,54.042,-68.979,54.011,-68.949,54,-68.903,53.985,-68.864,53.952,-68.846,53.911,-68.733,53.836,-68.685,53.812,-68.68,53.798]]]},{"n":"Caniapiscau","z":5,"b":[-70.517,54.359,-69.766,54.742],"p":[[[-70.233,54.444,-70.274,54.436,-70.312,54.408,-70.419,54.359,-70.497,54.407,-70.517,54.441,-70.46,54.432,-70.424,54.41,-70.357,54.429,-70.214,54.479,-70.095,54.473,-69.97,54.49,-69.932,54.516,-69.912,54.552,-69.912,54.617,-69.92,54.654,-69.96,54.684,-70.029,54.676,-70.008,54.722,-69.983,54.742,-69.935,54.732,-69.9,54.704,-69.86,54.622,-69.84,54.556,-69.839,54.525,-69.774,54.492,-69.766,54.466,-69.882,54.456,-69.949,54.425,-69.988,54.425,-70.015,54.438,-70.155,54.436,-70.233,54.444]]]},{"n":"Kentucky","z":5,"b":[-88.283,35.945,-87.91,37.022],"p":[[[-88.057,36.669,-88.023,36.528,-88.007,36.427,-87.956,36.303,-87.91,36.187,-87.934,36.074,-87.975,35.945,-88.041,35.949,-87.983,36.099,-87.991,36.234,-88.039,36.307,-88.066,36.314,-88.092,36.26,-88.116,36.293,-88.119,36.355,-88.086,36.447,-88.084,36.555,-88.126,36.655,-88.272,36.951,-88.283,37.022,-88.207,36.971,-88.057,36.669]]]},{"n":"Strom Thurmond","z":5,"b":[-82.589,33.641,-82.188,34.017],"p":[[[-82.411,33.954,-82.381,33.927,-82.327,33.847,-82.247,33.774,-82.188,33.694,-82.208,33.664,-82.332,33.672,-82.468,33.641,-82.575,33.643,-82.541,33.662,-82.439,33.671,-82.275,33.725,-82.308,33.776,-82.419,33.846,-82.491,33.898,-82.521,33.893,-82.555,33.879,-82.579,33.865,-82.553,33.937,-82.577,33.956,-82.589,34.017,-82.546,34.009,-82.485,33.966,-82.441,33.938,-82.411,33.954]]]},{"n":"Kinbasket","z":5,"b":[-119.128,52.06,-118.18,52.739],"p":[[[-118.402,52.172,-118.287,52.159,-118.18,52.178,-118.218,52.129,-118.322,52.103,-118.366,52.09,-118.392,52.069,-118.448,52.06,-118.499,52.069,-118.527,52.102,-118.551,52.241,-118.642,52.332,-118.751,52.404,-118.89,52.549,-119.128,52.739,-119.058,52.727,-118.931,52.618,-118.677,52.411,-118.565,52.303,-118.402,52.172]]]},{"n":"George","z":5,"b":[-81.684,29.198,-81.529,29.505],"p":[[[-81.684,29.368,-81.683,29.505,-81.64,29.414,-81.54,29.287,-81.529,29.224,-81.595,29.198,-81.64,29.258,-81.663,29.314,-81.684,29.368]]]},{"n":"Havasu","z":5,"b":[-114.405,34.242,-114.082,34.54],"p":[[[-114.359,34.438,-114.405,34.488,-114.377,34.54,-114.327,34.478,-114.221,34.421,-114.082,34.318,-114.134,34.242,-114.164,34.253,-114.189,34.318,-114.261,34.368,-114.359,34.438]]]},{"n":"Shasta","z":5,"b":[-122.426,40.721,-122.095,40.871],"p":[[[-122.232,40.871,-122.242,40.806,-122.229,40.793,-122.116,40.8,-122.095,40.783,-122.102,40.766,-122.151,40.746,-122.241,40.754,-122.364,40.724,-122.411,40.721,-122.426,40.744,-122.417,40.768,-122.395,40.829,-122.362,40.82,-122.351,40.784,-122.301,40.804,-122.259,40.861,-122.232,40.871]]]},{"n":"Point","z":5,"b":[-114.442,64.952,-112.217,65.677],"p":[[[-114.098,65.378,-114.185,65.446,-114.442,65.477,-114.405,65.561,-114.381,65.616,-114.397,65.657,-114.229,65.677,-114.173,65.638,-114.183,65.611,-114.298,65.6,-114.287,65.555,-114.222,65.522,-114.169,65.496,-114.077,65.476,-114.001,65.448,-113.937,65.444,-113.93,65.425,-113.91,65.402,-113.84,65.368,-113.621,65.416,-113.517,65.385,-113.401,65.382,-113.271,65.401,-113.113,65.385,-113.125,65.361,-113.224,65.356,-113.316,65.332,-113.312,65.318,-113.272,65.306,-113.125,65.292,-112.871,65.242,-112.678,65.203,-112.55,65.207,-112.462,65.22,-112.427,65.221,-112.294,65.22,-112.217,65.225,-112.222,65.192,-112.307,65.183,-112.346,65.128,-112.29,65.038,-112.242,64.966,-112.288,64.952,-112.318,64.96,-112.35,65.013,-112.417,65.104,-112.457,65.153,-112.528,65.16,-112.65,65.16,-112.822,65.19,-112.884,65.203,-112.939,65.213,-112.997,65.196,-113.063,65.216,-113.324,65.26,-113.507,65.272,-113.618,65.253,-113.787,65.244,-113.95,65.335,-114.055,65.361,-114.098,65.378]]]},{"n":"Upper Klamath","z":5,"b":[-122.06,42.251,-121.806,42.604],"p":[[[-121.941,42.604,-121.948,42.499,-121.921,42.475,-121.828,42.409,-121.806,42.289,-121.809,42.251,-121.842,42.256,-121.922,42.332,-121.921,42.36,-121.957,42.396,-122.042,42.428,-122.06,42.475,-122.017,42.523,-121.965,42.59,-121.941,42.604]]]},{"n":"Harney","z":5,"b":[-119.205,43.16,-119.005,43.308],"p":[[[-119.195,43.229,-119.205,43.256,-119.183,43.282,-119.133,43.308,-119.019,43.259,-119.005,43.239,-119.023,43.197,-119.081,43.16,-119.163,43.184,-119.195,43.229]]]},{"n":"Chiputneticook Lakes","z":5,"b":[-67.863,45.589,-67.449,45.759],"p":[[[-67.708,45.692,-67.6,45.637,-67.533,45.643,-67.48,45.683,-67.461,45.642,-67.449,45.607,-67.512,45.589,-67.625,45.602,-67.779,45.67,-67.863,45.74,-67.787,45.759,-67.708,45.692]]]},{"n":"Tucuruí Dam","z":5,"b":[-49.865,-5.388,-48.937,-3.835],"p":[[[-49.709,-4.422,-49.81,-4.409,-49.826,-4.391,-49.865,-4.321,-49.859,-4.298,-49.815,-4.26,-49.816,-4.194,-49.809,-4.175,-49.753,-4.115,-49.749,-4.058,-49.719,-4.028,-49.716,-3.958,-49.715,-3.855,-49.687,-3.835,-49.626,-3.848,-49.592,-3.958,-49.543,-4.125,-49.524,-4.226,-49.524,-4.257,-49.499,-4.283,-49.448,-4.36,-49.429,-4.454,-49.435,-4.49,-49.35,-4.602,-49.298,-4.679,-49.316,-4.725,-49.283,-4.8,-49.29,-4.836,-49.347,-4.881,-49.338,-4.945,-49.281,-5.165,-49.176,-5.275,-48.962,-5.276,-48.937,-5.303,-48.971,-5.333,-49.17,-5.388,-49.281,-5.301,-49.351,-5.183,-49.415,-5.044,-49.532,-4.846,-49.549,-4.74,-49.592,-4.652,-49.677,-4.617,-49.713,-4.593,-49.675,-4.546,-49.653,-4.484,-49.675,-4.437,-49.709,-4.422]]]},{"n":"Três Marias Dam","z":5,"b":[-45.516,-19.051,-44.903,-18.201],"p":[[[-44.972,-18.782,-44.942,-18.794,-44.932,-18.824,-44.903,-18.841,-44.92,-18.857,-44.967,-18.85,-45.044,-18.828,-45.118,-18.83,-45.129,-18.849,-45.121,-18.887,-45.077,-19.005,-45.089,-19.051,-45.112,-19.029,-45.156,-18.948,-45.199,-18.957,-45.217,-18.962,-45.31,-18.974,-45.397,-19.007,-45.396,-18.939,-45.386,-18.923,-45.251,-18.909,-45.209,-18.882,-45.195,-18.831,-45.214,-18.753,-45.236,-18.758,-45.247,-18.838,-45.281,-18.853,-45.34,-18.829,-45.34,-18.801,-45.29,-18.764,-45.258,-18.678,-45.255,-18.623,-45.283,-18.625,-45.359,-18.66,-45.372,-18.636,-45.349,-18.621,-45.331,-18.603,-45.298,-18.541,-45.318,-18.501,-45.367,-18.488,-45.4,-18.524,-45.429,-18.556,-45.476,-18.564,-45.516,-18.586,-45.507,-18.545,-45.479,-18.474,-45.434,-18.433,-45.355,-18.395,-45.353,-18.359,-45.328,-18.303,-45.329,-18.273,-45.379,-18.268,-45.36,-18.239,-45.287,-18.201,-45.262,-18.204,-45.23,-18.231,-45.253,-18.296,-45.285,-18.414,-45.259,-18.474,-45.212,-18.489,-45.192,-18.514,-45.185,-18.569,-45.181,-18.607,-45.148,-18.598,-45.133,-18.608,-45.136,-18.64,-45.156,-18.686,-45.152,-18.735,-45.097,-18.775,-45.036,-18.759,-45.017,-18.723,-45,-18.739,-44.989,-18.769,-44.972,-18.782]]]},{"n":"Engineer Souza Dias Dam","z":5,"b":[-51.438,-20.529,-50.582,-19.668],"p":[[[-51.416,-20.426,-51.423,-20.397,-51.413,-20.373,-51.396,-20.355,-51.438,-20.284,-51.427,-20.257,-51.381,-20.277,-51.314,-20.281,-51.272,-20.255,-51.181,-20.257,-51.156,-20.24,-51.172,-20.185,-51.15,-20.147,-51.104,-20.179,-51.078,-20.175,-51.05,-20.1,-51.049,-20.013,-51.051,-19.781,-51.03,-19.718,-50.997,-19.668,-50.97,-19.727,-50.955,-19.912,-50.949,-19.981,-50.807,-19.932,-50.668,-19.841,-50.582,-19.82,-50.669,-19.947,-50.792,-19.995,-50.844,-20.032,-50.936,-20.09,-50.985,-20.17,-50.978,-20.232,-50.934,-20.265,-50.913,-20.319,-50.983,-20.307,-51.036,-20.329,-51.09,-20.338,-51.112,-20.337,-51.148,-20.351,-51.207,-20.368,-51.245,-20.392,-51.177,-20.458,-51.065,-20.513,-51.066,-20.529,-51.197,-20.499,-51.305,-20.441,-51.363,-20.452,-51.397,-20.441,-51.416,-20.426]]]},{"n":"","z":5,"b":[-72.609,-40.704,-72.365,-40.617],"p":[[[-72.609,-40.658,-72.541,-40.617,-72.391,-40.622,-72.365,-40.639,-72.37,-40.663,-72.464,-40.704,-72.57,-40.69,-72.609,-40.658]]]},{"n":"Colhué Huapí","z":5,"b":[-68.976,-45.657,-68.566,-45.264],"p":[[[-68.676,-45.422,-68.664,-45.461,-68.641,-45.512,-68.601,-45.55,-68.574,-45.568,-68.566,-45.607,-68.599,-45.645,-68.708,-45.657,-68.848,-45.644,-68.923,-45.529,-68.966,-45.447,-68.976,-45.324,-68.97,-45.282,-68.934,-45.264,-68.875,-45.294,-68.859,-45.388,-68.836,-45.413,-68.802,-45.413,-68.766,-45.365,-68.702,-45.369,-68.676,-45.422]]]},{"n":"","z":5,"b":[8.881,47.656,9.128,47.752],"p":[[[9.128,47.671,8.881,47.656,8.948,47.686,8.981,47.702,8.977,47.73,8.999,47.752,9.094,47.716,9.128,47.671]]]},{"n":"","z":5,"b":[36.768,62.179,37.035,62.466],"p":[[[36.867,62.198,36.827,62.223,36.776,62.239,36.768,62.26,36.784,62.291,36.802,62.364,36.813,62.43,36.847,62.459,36.909,62.466,37.015,62.428,37.035,62.368,37.012,62.295,37.025,62.262,36.983,62.211,36.898,62.179,36.867,62.198]]]},{"n":"","z":5,"b":[38.844,60.394,39.188,60.777],"p":[[[38.909,60.586,38.892,60.585,38.89,60.664,38.844,60.734,38.866,60.777,38.938,60.767,39.085,60.718,39.173,60.68,39.185,60.617,39.188,60.496,39.15,60.442,39.096,60.394,39.031,60.395,39.028,60.411,39.042,60.438,39.069,60.475,39.054,60.523,39.008,60.573,38.958,60.595,38.909,60.586]]]},{"n":"","z":5,"b":[31.346,64.405,32.044,64.557],"p":[[[31.545,64.435,31.346,64.465,31.37,64.5,31.473,64.482,31.594,64.477,31.648,64.475,31.743,64.477,31.79,64.495,31.783,64.524,31.765,64.557,31.821,64.553,31.898,64.515,32.015,64.495,32.044,64.46,32.002,64.405,31.873,64.416,31.545,64.435]]]},{"n":"Imandra","z":5,"b":[31.899,67.415,32.406,67.544],"p":[[[32.406,67.457,32.342,67.442,32.246,67.415,32.125,67.434,31.899,67.512,31.944,67.544,32.071,67.543,32.216,67.539,32.342,67.487,32.406,67.457]]]},{"n":"","z":5,"b":[34.995,67.707,35.297,68.1],"p":[[[35.159,67.811,35.136,67.782,35.126,67.751,35.08,67.707,34.995,67.801,35.055,67.827,35.129,67.875,35.158,67.906,35.138,67.926,35.092,67.94,35.099,67.979,35.221,68.059,35.247,68.1,35.291,68.078,35.297,68.006,35.243,67.956,35.256,67.923,35.26,67.88,35.222,67.845,35.159,67.811]]]},{"n":"Chardara Reservoir","z":5,"b":[67.933,41.048,68.456,41.255],"p":[[[67.991,41.255,68.162,41.2,68.243,41.189,68.393,41.161,68.456,41.107,68.403,41.078,68.28,41.052,68.135,41.048,68.024,41.099,67.933,41.225,67.991,41.255]]]},{"n":"Vilyuy","z":5,"b":[109.558,61.783,112.48,63.625],"p":[[[111.12,62.62,111.166,62.672,111.194,62.707,111.158,62.74,111.165,62.792,111.154,62.808,111.139,62.848,110.839,62.912,110.648,62.982,110.431,62.989,110.433,63.046,110.494,63.099,110.484,63.136,110.373,63.139,110.23,63.155,110.109,63.172,110.039,63.208,110.094,63.222,110.2,63.217,110.291,63.218,110.374,63.242,110.393,63.292,110.389,63.35,110.274,63.4,110.162,63.439,110.002,63.429,109.848,63.382,109.75,63.405,109.648,63.424,109.587,63.455,109.746,63.466,109.899,63.456,109.946,63.48,110.038,63.558,110.056,63.594,110.096,63.625,110.136,63.621,110.146,63.6,110.113,63.573,110.127,63.536,110.247,63.485,110.431,63.459,110.482,63.46,110.563,63.422,110.61,63.388,110.568,63.336,110.474,63.25,110.5,63.233,110.546,63.198,110.668,63.199,110.691,63.217,110.744,63.239,110.795,63.219,110.843,63.19,110.918,63.178,110.939,63.159,110.914,63.146,110.886,63.125,110.728,63.09,110.719,63.063,110.772,63.036,110.865,63.019,110.944,62.997,111.015,63.046,111.086,63.094,111.044,63.269,111.05,63.293,111.042,63.311,111.076,63.349,111.103,63.415,111.143,63.424,111.17,63.353,111.162,63.308,111.17,63.295,111.144,63.219,111.16,63.176,111.211,63.138,111.273,63.119,111.334,63.116,111.473,63.13,111.558,63.165,111.584,63.218,111.536,63.288,111.596,63.283,111.652,63.283,111.69,63.289,111.798,63.266,111.9,63.219,111.977,63.119,111.985,63.097,111.83,63.003,111.829,62.96,111.924,62.913,111.998,62.9,112.06,62.914,112.12,62.917,112.155,62.97,112.115,63.017,112.108,63.054,112.12,63.13,112.078,63.235,112.047,63.315,111.908,63.37,111.816,63.383,111.91,63.412,112.079,63.409,112.125,63.432,112.171,63.424,112.139,63.381,112.135,63.315,112.148,63.249,112.234,63.126,112.44,63.075,112.48,63.02,112.4,63,112.38,62.92,112.368,62.842,112.278,62.79,112.175,62.765,112.106,62.783,112.049,62.818,111.915,62.849,111.846,62.873,111.777,62.862,111.729,62.846,111.693,62.864,111.631,62.863,111.581,62.858,111.488,62.873,111.435,62.894,111.399,62.919,111.408,62.94,111.47,62.944,111.521,62.953,111.561,62.968,111.633,62.973,111.657,63.041,111.774,63.07,111.839,63.121,111.844,63.16,111.786,63.196,111.709,63.187,111.64,63.143,111.592,63.077,111.518,63.047,111.416,63.028,111.318,63.028,111.207,63.005,111.191,62.94,111.293,62.883,111.345,62.847,111.376,62.805,111.388,62.723,111.424,62.653,111.39,62.611,111.317,62.55,111.359,62.494,111.358,62.472,111.294,62.46,111.293,62.431,111.328,62.397,111.489,62.402,111.605,62.39,111.64,62.361,111.666,62.331,111.663,62.311,111.504,62.349,111.39,62.327,111.372,62.283,111.604,62.214,111.551,62.187,111.483,62.196,111.423,62.219,111.35,62.223,111.286,62.191,111.296,62.131,111.399,62.058,111.251,61.99,111.046,61.99,110.9,62.011,110.618,62.064,110.554,62.079,110.417,62.093,110.35,62.11,110.303,62.134,110.16,62.124,110.016,62.078,109.827,61.994,109.665,61.911,109.635,61.836,109.601,61.783,109.56,61.799,109.568,61.878,109.558,61.9,109.604,61.942,109.637,62.007,109.627,62.053,109.714,62.039,109.816,62.055,109.932,62.103,110.145,62.204,110.433,62.194,110.606,62.131,110.674,62.121,110.757,62.099,110.81,62.111,110.887,62.084,110.937,62.086,110.947,62.083,110.973,62.061,111.088,62.051,111.156,62.11,111.123,62.164,111.12,62.221,111.209,62.256,111.243,62.309,111.235,62.375,111.209,62.431,111.137,62.483,111.012,62.504,110.916,62.488,110.875,62.468,110.859,62.478,110.852,62.499,110.784,62.528,110.851,62.546,110.979,62.54,111.051,62.544,111.09,62.57,111.12,62.62]]]},{"n":"","z":5,"b":[139.658,53.34,139.922,53.556],"p":[[[139.881,53.556,139.922,53.527,139.855,53.46,139.809,53.4,139.823,53.361,139.816,53.34,139.749,53.348,139.672,53.39,139.658,53.456,139.748,53.524,139.881,53.556]]]},{"n":"Krasnoye","z":5,"b":[174.207,64.433,174.804,64.702],"p":[[[174.429,64.569,174.575,64.581,174.605,64.615,174.607,64.652,174.713,64.669,174.804,64.65,174.766,64.568,174.671,64.481,174.541,64.433,174.338,64.437,174.207,64.513,174.273,64.608,174.352,64.676,174.373,64.702,174.378,64.66,174.352,64.594,174.429,64.569]]]},{"n":"Biwa","z":5,"b":[135.889,35,136.268,35.466],"p":[[[136.058,35.387,136.083,35.443,136.137,35.466,136.182,35.453,136.225,35.407,136.268,35.347,136.214,35.264,135.981,35.098,135.922,35.026,135.889,35,135.892,35.093,135.969,35.203,136.032,35.304,136.058,35.387]]]},{"n":"Aishihik","z":5,"b":[-137.522,61.227,-136.989,61.652],"p":[[[-137.148,61.503,-137.096,61.457,-137.075,61.386,-137.042,61.31,-136.992,61.273,-136.989,61.235,-137,61.227,-137.012,61.235,-137.033,61.262,-137.083,61.292,-137.168,61.45,-137.332,61.542,-137.522,61.636,-137.442,61.652,-137.331,61.612,-137.263,61.577,-137.148,61.503]]]},{"n":"","z":5,"b":[-156.409,58.515,-155.435,58.729],"p":[[[-155.453,58.637,-155.576,58.617,-155.655,58.602,-155.646,58.576,-155.496,58.548,-155.473,58.53,-155.52,58.515,-155.653,58.52,-155.748,58.555,-155.841,58.597,-156.028,58.6,-156.162,58.605,-156.323,58.629,-156.38,58.665,-156.409,58.691,-156.343,58.709,-156.198,58.712,-156.197,58.724,-156.153,58.729,-156.09,58.713,-156.037,58.7,-156.006,58.688,-155.838,58.683,-155.56,58.663,-155.435,58.66,-155.453,58.637]]]},{"n":"","z":5,"b":[-163.031,61.041,-162.713,61.109],"p":[[[-162.838,61.041,-163.031,61.083,-163.002,61.109,-162.801,61.081,-162.713,61.077,-162.838,61.041]]]},{"n":"Clark","z":5,"b":[-154.752,60.04,-153.627,60.436],"p":[[[-154.473,60.222,-154.312,60.279,-154.089,60.34,-153.689,60.431,-153.627,60.436,-153.659,60.404,-154.056,60.301,-154.26,60.23,-154.549,60.113,-154.72,60.04,-154.752,60.07,-154.642,60.143,-154.573,60.183,-154.541,60.189,-154.473,60.222]]]},{"n":"","z":5,"b":[-151.166,60.048,-150.654,60.305],"p":[[[-151.166,60.227,-151.149,60.253,-151.065,60.305,-150.894,60.272,-150.717,60.159,-150.654,60.121,-150.681,60.048,-150.716,60.052,-150.767,60.082,-150.831,60.128,-150.937,60.168,-151.021,60.206,-151.06,60.218,-151.166,60.227]]]},{"n":"","z":5,"b":[-160.131,61.34,-159.934,61.422],"p":[[[-160.002,61.35,-160.089,61.34,-160.131,61.358,-160.103,61.406,-160.038,61.422,-159.971,61.402,-159.934,61.367,-160.002,61.35]]]},{"n":"","z":5,"b":[-162.397,60.821,-162.204,60.99],"p":[[[-162.307,60.951,-162.297,60.918,-162.265,60.89,-162.204,60.853,-162.242,60.827,-162.275,60.821,-162.37,60.83,-162.396,60.899,-162.397,60.95,-162.362,60.99,-162.307,60.951]]]},{"n":"","z":5,"b":[-163.973,61.472,-163.638,61.648],"p":[[[-163.706,61.562,-163.638,61.519,-163.699,61.472,-163.729,61.523,-163.875,61.578,-163.959,61.619,-163.973,61.645,-163.931,61.648,-163.783,61.602,-163.706,61.562]]]},{"n":"","z":5,"b":[-163.619,62.574,-163.41,62.71],"p":[[[-163.459,62.671,-163.5,62.652,-163.524,62.574,-163.582,62.6,-163.619,62.675,-163.565,62.686,-163.456,62.71,-163.41,62.679,-163.459,62.671]]]},{"n":"Wheeler","z":5,"b":[-87.7,34.565,-86.899,34.851],"p":[[[-87.661,34.814,-87.542,34.851,-87.383,34.851,-87.268,34.808,-87.006,34.683,-86.935,34.61,-86.899,34.575,-86.959,34.565,-87.323,34.767,-87.512,34.792,-87.637,34.765,-87.7,34.773,-87.661,34.814]]]},{"n":"Üüreg","z":5,"b":[90.891,50.08,91.131,50.214],"p":[[[91.052,50.208,91.117,50.175,91.131,50.13,91.078,50.099,90.961,50.08,90.891,50.136,90.942,50.214,91.052,50.208]]]},{"n":"","z":5,"b":[90.006,33.325,90.401,33.702],"p":[[[90.401,33.526,90.394,33.436,90.322,33.363,90.247,33.325,90.101,33.377,90.033,33.385,90.006,33.437,90.047,33.445,90.186,33.416,90.29,33.475,90.282,33.569,90.289,33.638,90.319,33.702,90.401,33.526]]]},{"n":"","z":5,"b":[89.71,33.256,89.97,33.523],"p":[[[89.71,33.469,89.771,33.469,89.832,33.48,89.876,33.505,89.935,33.523,89.97,33.48,89.919,33.376,89.89,33.329,89.83,33.256,89.806,33.286,89.786,33.417,89.734,33.447,89.71,33.469]]]},{"n":"","z":5,"b":[75.936,31.9,76.171,32.153],"p":[[[76.169,31.9,76.082,31.909,75.975,31.972,75.938,32.061,75.936,32.139,75.983,32.153,76.095,32.073,76.171,31.962,76.169,31.9]]]},{"n":"","z":5,"b":[91.424,31.943,91.532,32.133],"p":[[[91.469,31.947,91.437,31.991,91.424,32.062,91.455,32.112,91.511,32.133,91.532,32.08,91.529,31.966,91.518,31.943,91.469,31.947]]]},{"n":"","z":5,"b":[87.415,31.847,87.596,31.969],"p":[[[87.415,31.852,87.424,31.912,87.49,31.954,87.546,31.969,87.591,31.948,87.596,31.903,87.53,31.847,87.415,31.852]]]},{"n":"Pangong Tso","z":5,"b":[78.443,33.677,78.976,33.974],"p":[[[78.443,33.939,78.49,33.974,78.658,33.816,78.758,33.747,78.961,33.741,78.976,33.723,78.914,33.697,78.734,33.677,78.553,33.851,78.443,33.939]]]},{"n":"Tengiz","z":5,"b":[68.69,50.179,69.617,50.719],"p":[[[69.288,50.688,69.371,50.719,69.516,50.693,69.617,50.676,69.613,50.615,69.5,50.591,69.39,50.599,69.335,50.604,69.275,50.588,69.109,50.46,69.047,50.426,69.036,50.397,69.057,50.364,69.237,50.354,69.259,50.311,69.256,50.254,69.2,50.254,69.148,50.241,69.074,50.201,68.938,50.179,68.778,50.228,68.69,50.266,68.719,50.33,68.759,50.438,68.779,50.513,68.861,50.571,68.993,50.564,69.091,50.564,69.227,50.616,69.288,50.688]]]},{"n":"Kushmurun","z":5,"b":[64.501,52.516,64.982,52.908],"p":[[[64.574,52.605,64.685,52.687,64.79,52.768,64.884,52.856,64.949,52.908,64.982,52.883,64.967,52.731,64.89,52.672,64.794,52.624,64.765,52.613,64.722,52.594,64.649,52.553,64.515,52.516,64.501,52.535,64.574,52.605]]]},{"n":"Femund","z":5,"b":[11.742,61.929,11.797,62.052],"p":[[[11.797,61.939,11.753,61.929,11.742,62.008,11.778,62.052,11.797,61.939]]]},{"n":"Femund","z":5,"b":[11.721,61.931,11.967,62.421],"p":[[[11.846,62.212,11.893,62.3,11.892,62.4,11.922,62.421,11.967,62.407,11.95,62.31,11.929,62.205,11.9,62.144,11.885,62.089,11.902,62.026,11.927,61.931,11.883,61.931,11.843,62.046,11.721,62.103,11.736,62.138,11.766,62.154,11.846,62.212]]]},{"n":"","z":5,"b":[-77.026,51.288,-76.554,51.416],"p":[[[-76.677,51.373,-76.554,51.364,-76.563,51.344,-76.751,51.288,-77.026,51.329,-76.983,51.343,-76.903,51.355,-76.862,51.38,-76.84,51.404,-76.819,51.416,-76.762,51.391,-76.677,51.373]]]},{"n":"","z":5,"b":[-91.647,51.071,-91.39,51.166],"p":[[[-91.418,51.12,-91.39,51.071,-91.542,51.085,-91.591,51.118,-91.647,51.154,-91.609,51.166,-91.504,51.162,-91.441,51.163,-91.417,51.143,-91.418,51.12]]]},{"n":"","z":5,"b":[-91.991,51.894,-91.895,51.982],"p":[[[-91.97,51.958,-91.95,51.982,-91.895,51.952,-91.91,51.911,-91.932,51.894,-91.968,51.924,-91.991,51.937,-91.97,51.958]]]},{"n":"","z":5,"b":[-78.827,51.034,-78.55,51.152],"p":[[[-78.616,51.077,-78.55,51.034,-78.813,51.097,-78.827,51.152,-78.794,51.146,-78.72,51.109,-78.616,51.077]]]},{"n":"","z":5,"b":[-78.813,47.57,-78.571,47.691],"p":[[[-78.628,47.597,-78.696,47.577,-78.763,47.57,-78.787,47.595,-78.798,47.626,-78.813,47.656,-78.792,47.691,-78.724,47.684,-78.612,47.681,-78.571,47.656,-78.584,47.625,-78.604,47.608,-78.628,47.597]]]},{"n":"Dozois","z":5,"b":[-77.322,47.347,-76.935,47.614],"p":[[[-77.273,47.591,-77.235,47.55,-77.177,47.547,-77.129,47.561,-77.102,47.545,-77.065,47.508,-76.963,47.502,-76.935,47.488,-76.966,47.442,-77.119,47.477,-77.185,47.456,-77.179,47.412,-77.165,47.389,-77.181,47.369,-77.259,47.347,-77.289,47.386,-77.27,47.463,-77.304,47.513,-77.322,47.573,-77.304,47.607,-77.288,47.614,-77.273,47.591]]]},{"n":"","z":5,"b":[-108.09,64.697,-107.979,64.749],"p":[[[-108.09,64.715,-108.085,64.737,-108.044,64.749,-108.016,64.745,-107.979,64.72,-107.987,64.71,-108.063,64.697,-108.09,64.715]]]},{"n":"","z":5,"b":[-108.533,64.559,-108.18,64.686],"p":[[[-108.189,64.646,-108.209,64.634,-108.24,64.625,-108.481,64.559,-108.533,64.562,-108.494,64.598,-108.329,64.654,-108.24,64.684,-108.18,64.686,-108.189,64.646]]]},{"n":"","z":5,"b":[-109.1,64.552,-108.821,64.727],"p":[[[-108.929,64.6,-108.989,64.613,-109.056,64.594,-109.09,64.587,-109.1,64.603,-109.098,64.656,-109.061,64.686,-109.071,64.711,-109.062,64.727,-108.93,64.706,-108.893,64.664,-108.852,64.606,-108.821,64.552,-108.854,64.561,-108.929,64.6]]]},{"n":"","z":5,"b":[-109.362,64.536,-109.149,64.758],"p":[[[-109.251,64.594,-109.282,64.608,-109.34,64.655,-109.362,64.702,-109.36,64.734,-109.347,64.758,-109.291,64.758,-109.24,64.74,-109.256,64.727,-109.284,64.702,-109.272,64.662,-109.149,64.58,-109.16,64.547,-109.224,64.536,-109.273,64.536,-109.235,64.565,-109.251,64.594]]]},{"n":"","z":5,"b":[-79.367,71.089,-78.671,71.178],"p":[[[-78.866,71.16,-78.792,71.146,-78.671,71.126,-78.671,71.112,-78.699,71.1,-78.896,71.089,-79.211,71.097,-79.339,71.122,-79.367,71.149,-79.326,71.17,-79.277,71.175,-79.09,71.178,-78.88,71.173,-78.866,71.16]]]},{"n":"","z":5,"b":[-104.862,69.718,-104.358,69.858],"p":[[[-104.511,69.718,-104.646,69.733,-104.862,69.856,-104.762,69.858,-104.482,69.84,-104.422,69.83,-104.358,69.815,-104.388,69.756,-104.511,69.718]]]},{"n":"","z":5,"b":[-98.552,67.192,-98.229,67.421],"p":[[[-98.382,67.384,-98.302,67.35,-98.229,67.332,-98.272,67.289,-98.421,67.192,-98.491,67.206,-98.546,67.243,-98.552,67.272,-98.523,67.351,-98.486,67.421,-98.435,67.414,-98.382,67.384]]]},{"n":"","z":5,"b":[-96.285,55.212,-95.676,55.401],"p":[[[-96.285,55.238,-96.205,55.271,-96.055,55.287,-95.853,55.361,-95.789,55.4,-95.702,55.401,-95.676,55.364,-95.733,55.308,-95.83,55.28,-96.106,55.217,-96.229,55.212,-96.285,55.238]]]},{"n":"","z":5,"b":[-102.378,54.449,-102.106,54.738],"p":[[[-102.363,54.578,-102.336,54.655,-102.287,54.715,-102.197,54.738,-102.116,54.708,-102.106,54.598,-102.128,54.494,-102.169,54.455,-102.256,54.449,-102.34,54.475,-102.378,54.516,-102.363,54.578]]]},{"n":"","z":5,"b":[-105.812,54.055,-105.553,54.509],"p":[[[-105.812,54.234,-105.728,54.414,-105.714,54.492,-105.683,54.509,-105.643,54.488,-105.568,54.459,-105.553,54.39,-105.57,54.327,-105.636,54.268,-105.673,54.222,-105.707,54.15,-105.714,54.086,-105.755,54.055,-105.805,54.078,-105.81,54.161,-105.812,54.234]]]},{"n":"","z":5,"b":[-71.125,53.407,-70.843,53.596],"p":[[[-70.932,53.519,-70.935,53.483,-70.904,53.458,-70.861,53.452,-70.843,53.427,-70.865,53.407,-70.98,53.431,-71.048,53.501,-71.125,53.565,-71.117,53.591,-71.019,53.596,-70.95,53.565,-70.932,53.519]]]},{"n":"","z":5,"b":[-61.297,52.315,-60.6,52.619],"p":[[[-60.904,52.55,-60.724,52.444,-60.708,52.409,-60.657,52.358,-60.604,52.329,-60.6,52.32,-60.648,52.315,-60.739,52.378,-60.79,52.424,-60.917,52.477,-60.968,52.516,-61.15,52.589,-61.297,52.603,-61.139,52.619,-60.904,52.55]]]},{"n":"","z":5,"b":[-112.343,64.638,-111.887,64.899],"p":[[[-111.996,64.66,-111.988,64.69,-112.151,64.78,-112.326,64.86,-112.343,64.899,-112.114,64.848,-111.887,64.701,-111.927,64.659,-111.966,64.638,-111.996,64.66]]]},{"n":"","z":5,"b":[-83.925,48.305,-83.429,48.506],"p":[[[-83.611,48.378,-83.688,48.339,-83.781,48.305,-83.832,48.315,-83.925,48.307,-83.854,48.337,-83.721,48.421,-83.64,48.427,-83.618,48.426,-83.573,48.424,-83.429,48.506,-83.468,48.434,-83.611,48.378]]]},{"n":"","z":5,"b":[-107.063,55.806,-106.88,55.927],"p":[[[-107.063,55.82,-107.059,55.855,-107.011,55.89,-106.947,55.921,-106.897,55.927,-106.88,55.908,-106.984,55.832,-106.973,55.815,-107.009,55.806,-107.063,55.82]]]},{"n":"","z":5,"b":[-106.551,55.631,-106.33,55.76],"p":[[[-106.472,55.631,-106.514,55.639,-106.524,55.657,-106.551,55.668,-106.539,55.689,-106.441,55.728,-106.344,55.76,-106.33,55.74,-106.359,55.691,-106.341,55.662,-106.386,55.648,-106.472,55.631]]]},{"n":"","z":5,"b":[-102.774,62.41,-102.551,62.762],"p":[[[-102.597,62.657,-102.58,62.605,-102.56,62.552,-102.551,62.499,-102.576,62.45,-102.67,62.418,-102.737,62.41,-102.677,62.56,-102.692,62.602,-102.769,62.669,-102.774,62.704,-102.694,62.762,-102.66,62.76,-102.597,62.657]]]},{"n":"","z":5,"b":[-103.478,61.366,-103.144,61.677],"p":[[[-103.447,61.366,-103.469,61.385,-103.478,61.431,-103.456,61.513,-103.342,61.562,-103.233,61.642,-103.21,61.677,-103.144,61.653,-103.191,61.588,-103.296,61.521,-103.392,61.395,-103.447,61.366]]]},{"n":"","z":5,"b":[-103.836,58.545,-103.6,58.702],"p":[[[-103.836,58.609,-103.798,58.669,-103.786,58.691,-103.663,58.702,-103.621,58.678,-103.6,58.661,-103.623,58.607,-103.668,58.545,-103.771,58.574,-103.819,58.583,-103.836,58.609]]]},{"n":"","z":5,"b":[-69.274,56.055,-68.795,56.65],"p":[[[-69.153,56.507,-69.123,56.332,-69.077,56.272,-68.808,56.089,-68.795,56.055,-69.121,56.248,-69.189,56.326,-69.195,56.427,-69.274,56.504,-69.219,56.563,-69.128,56.65,-69.136,56.606,-69.153,56.507]]]},{"n":"","z":5,"b":[-89.089,66.483,-88.792,66.633],"p":[[[-88.93,66.577,-89.089,66.483,-89.075,66.556,-88.96,66.607,-88.792,66.633,-88.82,66.596,-88.93,66.577]]]},{"n":"","z":5,"b":[-88.711,66.579,-88.091,66.714],"p":[[[-88.202,66.633,-88.091,66.605,-88.213,66.581,-88.284,66.634,-88.396,66.657,-88.547,66.64,-88.682,66.579,-88.71,66.597,-88.711,66.632,-88.658,66.665,-88.615,66.711,-88.567,66.714,-88.463,66.701,-88.341,66.705,-88.202,66.633]]]},{"n":"Kumpupintil","z":5.7,"b":[122.608,-23.685,122.947,-23.255],"p":[[[122.823,-23.685,122.768,-23.661,122.641,-23.538,122.608,-23.469,122.697,-23.322,122.703,-23.274,122.758,-23.264,122.879,-23.255,122.932,-23.277,122.947,-23.434,122.926,-23.591,122.876,-23.662,122.823,-23.685]]]},{"n":"Mackay","z":5.7,"b":[128.317,-22.678,129.219,-22.002],"p":[[[128.601,-22.648,128.385,-22.598,128.317,-22.491,128.341,-22.404,128.429,-22.369,128.478,-22.345,128.599,-22.309,128.794,-22.22,128.908,-22.12,128.934,-22.064,129,-22.009,129.135,-22.002,129.216,-22.035,129.201,-22.085,129.072,-22.174,129.06,-22.271,129.142,-22.335,129.219,-22.366,129.215,-22.417,129.158,-22.448,129.099,-22.498,129.026,-22.563,128.899,-22.654,128.82,-22.678,128.601,-22.648]]]},{"n":"Gairdner","z":5.7,"b":[135.418,-32.326,136.312,-31.063],"p":[[[135.896,-32.326,135.89,-32.241,135.921,-32.15,136.045,-31.966,136.052,-31.902,135.992,-31.835,135.852,-31.743,135.812,-31.668,135.788,-31.617,135.749,-31.593,135.715,-31.597,135.691,-31.606,135.637,-31.59,135.611,-31.514,135.634,-31.445,135.638,-31.402,135.624,-31.382,135.595,-31.366,135.562,-31.354,135.522,-31.315,135.49,-31.28,135.439,-31.232,135.418,-31.096,135.429,-31.063,135.493,-31.084,135.605,-31.189,135.777,-31.238,135.886,-31.224,135.955,-31.225,136.08,-31.254,136.106,-31.305,136.104,-31.353,136.11,-31.415,136.145,-31.505,136.203,-31.577,136.275,-31.845,136.312,-31.926,136.253,-32.148,136.256,-32.211,136.238,-32.24,136.185,-32.253,136.123,-32.232,136.056,-32.209,135.975,-32.23,135.916,-32.318,135.896,-32.326]]]},{"n":"Torrens","z":5.7,"b":[137.212,-31.897,138.133,-30.179],"p":[[[137.83,-31.717,137.775,-31.864,137.748,-31.897,137.71,-31.877,137.613,-31.808,137.601,-31.778,137.672,-31.732,137.703,-31.664,137.733,-31.326,137.703,-31.273,137.652,-31.241,137.557,-31.156,137.531,-31.106,137.565,-31.082,137.632,-31.063,137.659,-31.027,137.662,-30.999,137.642,-30.832,137.621,-30.765,137.626,-30.673,137.595,-30.633,137.427,-30.629,137.395,-30.573,137.383,-30.471,137.328,-30.365,137.256,-30.288,137.212,-30.223,137.227,-30.179,137.323,-30.217,137.421,-30.307,137.482,-30.383,137.602,-30.427,137.746,-30.494,137.84,-30.597,137.928,-30.717,138.002,-30.874,138.03,-30.951,138.02,-31.003,138.026,-31.06,138.133,-31.259,138.111,-31.393,137.83,-31.717]]]},{"n":"Barlee","z":6,"b":[119.129,-29.58,120.006,-28.882],"p":[[[119.798,-29.505,119.786,-29.58,119.728,-29.58,119.636,-29.55,119.662,-29.51,119.693,-29.452,119.666,-29.387,119.636,-29.338,119.665,-29.277,119.668,-29.21,119.629,-29.186,119.58,-29.188,119.548,-29.216,119.542,-29.379,119.517,-29.505,119.442,-29.554,119.382,-29.542,119.437,-29.236,119.38,-29.214,119.33,-29.235,119.204,-29.351,119.142,-29.28,119.129,-29.114,119.226,-29.091,119.314,-29.012,119.398,-28.994,119.548,-29.057,119.65,-29.027,119.715,-28.936,119.764,-28.888,119.857,-28.899,119.938,-28.882,120.006,-28.928,119.993,-28.981,119.946,-28.985,119.877,-28.943,119.718,-29.067,119.683,-29.12,119.751,-29.297,119.79,-29.373,119.823,-29.413,119.798,-29.505]]]},{"n":"Frome","z":5.7,"b":[139.488,-31.126,140.044,-30.321],"p":[[[139.938,-30.964,139.92,-31.027,139.876,-31.084,139.835,-31.126,139.8,-31.124,139.744,-31.08,139.659,-31.033,139.613,-31.011,139.519,-30.925,139.488,-30.877,139.55,-30.774,139.719,-30.566,139.896,-30.374,139.951,-30.321,140.011,-30.341,140.044,-30.423,140.026,-30.524,139.982,-30.854,139.938,-30.964]]]},{"n":"Eyre South","z":6,"b":[137.111,-29.468,137.72,-29.131],"p":[[[137.661,-29.348,137.605,-29.371,137.522,-29.395,137.407,-29.444,137.374,-29.454,137.28,-29.449,137.189,-29.468,137.111,-29.449,137.119,-29.346,137.2,-29.245,137.331,-29.222,137.589,-29.159,137.698,-29.131,137.72,-29.197,137.661,-29.348]]]},{"n":"Eyre North","z":5.6,"b":[136.785,-29.1,138.014,-27.859],"p":[[[137.914,-29.1,137.777,-29.038,137.619,-29.051,137.478,-29.012,137.407,-29.029,137.344,-29.025,137.301,-28.988,137.239,-28.892,137.146,-28.887,137.125,-28.918,137.12,-28.978,137.075,-29.014,137.018,-29.015,136.949,-28.953,136.894,-28.832,136.917,-28.756,136.922,-28.689,136.883,-28.45,136.907,-28.426,136.966,-28.394,136.99,-28.314,136.969,-28.199,136.913,-28.103,136.889,-28.064,136.83,-28.044,136.785,-28.019,136.8,-27.967,136.891,-27.911,137.033,-27.859,137.328,-27.974,137.453,-28.087,137.566,-28.116,137.672,-28.12,137.702,-28.143,137.707,-28.196,137.72,-28.28,137.774,-28.35,137.733,-28.39,137.712,-28.537,137.68,-28.562,137.676,-28.645,137.786,-28.84,137.905,-28.985,138.014,-29.051,138.01,-29.091,137.914,-29.1]]]}]} \ No newline at end of file diff --git a/src/maps/data/places_110m.json b/src/maps/data/places_110m.json new file mode 100644 index 0000000..a042065 --- /dev/null +++ b/src/maps/data/places_110m.json @@ -0,0 +1 @@ +{"f":[{"n":"Vatican City","a":"Vatican","i":"VA","p":832,"la":41.903,"lo":12.453,"k":1,"z":7,"s":8},{"n":"San Marino","a":"San Marino","i":"SM","p":29579,"la":43.936,"lo":12.442,"k":1,"z":6.1,"s":7},{"n":"Vaduz","a":"Liechtenstein","i":"LI","p":36281,"la":47.134,"lo":9.517,"k":1,"z":6.7,"s":7},{"n":"Lobamba","a":"eSwatini","i":"SZ","p":9782,"la":-26.467,"lo":31.2,"k":0,"z":6,"s":6},{"n":"Luxembourg","a":"Luxembourg","i":"LU","p":107260,"la":49.612,"lo":6.13,"k":1,"z":6,"s":6},{"n":"Palikir","a":"Federated States of Micronesia","i":"FM","p":4645,"la":6.917,"lo":158.15,"k":1,"z":6,"s":6},{"n":"Majuro","a":"Marshall Islands","i":"MH","p":25400,"la":7.103,"lo":171.38,"k":1,"z":6,"s":6},{"n":"Funafuti","a":"Tuvalu","i":"TV","p":4749,"la":-8.517,"lo":179.217,"k":1,"z":6,"s":6},{"n":"Melekeok","a":"Palau","i":"PW","p":7026,"la":7.487,"lo":134.627,"k":1,"z":6,"s":6},{"n":"Bir Lehlou","a":"Western Sahara","i":"EH","p":500,"la":26.119,"lo":-9.653,"k":0,"z":6,"s":6},{"n":"Monaco","a":"Monaco","i":"MC","p":36371,"la":43.74,"lo":7.407,"k":1,"z":5.1,"s":6},{"n":"Tarawa","a":"Kiribati","i":"KI","p":28802,"la":1.338,"lo":173.018,"k":1,"z":6,"s":6},{"n":"Moroni","a":"Comoros","i":"KM","p":128698,"la":-11.704,"lo":43.24,"k":1,"z":6,"s":6},{"n":"Andorra","a":"Andorra","i":"AD","p":53998,"la":42.511,"lo":1.527,"k":1,"z":6,"s":6},{"n":"Port-of-Spain","a":"Trinidad and Tobago","i":"TT","p":294934,"la":10.652,"lo":-61.517,"k":1,"z":5.1,"s":4},{"n":"Kigali","a":"Rwanda","i":"RW","p":860000,"la":-1.952,"lo":30.059,"k":1,"z":5.1,"s":4},{"n":"Mbabane","a":"eSwatini","i":"SZ","p":90138,"la":-26.317,"lo":31.133,"k":1,"z":5.6,"s":4},{"n":"Juba","a":"South Sudan","i":"SS","p":111975,"la":4.83,"lo":31.58,"k":0,"z":5.1,"s":4},{"n":"The Hague","a":"Netherlands","i":"NL","p":1406000,"la":52.08,"lo":4.27,"k":0,"z":5.6,"s":4},{"n":"Ljubljana","a":"Slovenia","i":"SI","p":314807,"la":46.055,"lo":14.515,"k":1,"z":5.6,"s":4},{"n":"Bratislava","a":"Slovakia","i":"SK","p":423737,"la":48.15,"lo":17.117,"k":1,"z":5.1,"s":4},{"n":"Doha","a":"Qatar","i":"QA","p":1450000,"la":25.287,"lo":51.533,"k":1,"z":5.6,"s":4},{"n":"Podgorica","a":"Montenegro","i":"ME","p":145850,"la":42.466,"lo":19.266,"k":1,"z":5.6,"s":4},{"n":"Sri Jayawardenepura Kotte","a":"Sri Lanka","i":"LK","p":115826,"la":6.9,"lo":79.95,"k":0,"z":5.6,"s":4},{"n":"Baguio","a":"Philippines","i":"PH","p":447824,"la":16.43,"lo":120.57,"k":0,"z":5.6,"s":4},{"n":"Dodoma","a":"Tanzania","i":"TZ","p":218269,"la":-6.183,"lo":35.75,"k":0,"z":5.6,"s":4},{"n":"Bern","a":"Switzerland","i":"CH","p":275329,"la":46.917,"lo":7.467,"k":1,"z":5.6,"s":4},{"n":"Laayoune","a":"Morocco","i":"MA","p":188084,"la":27.15,"lo":-13.2,"k":0,"z":5.6,"s":4},{"n":"Pristina","a":"Kosovo","i":"-99","p":465186,"la":42.667,"lo":21.166,"k":1,"z":5.6,"s":4},{"n":"Roseau","a":"Dominica","i":"DM","p":23336,"la":15.301,"lo":-61.387,"k":1,"z":5.1,"s":4},{"n":"Djibouti","a":"Djibouti","i":"DJ","p":923000,"la":11.595,"lo":43.148,"k":1,"z":5.6,"s":4},{"n":"Putrajaya","a":"Malaysia","i":"MY","p":67964,"la":2.933,"lo":101.695,"k":0,"z":5.6,"s":4},{"n":"Kyoto","a":"Japan","i":"JP","p":1805000,"la":35.032,"lo":135.748,"k":0,"z":5.1,"s":4},{"n":"Banjul","a":"The Gambia","i":"GM","p":43094,"la":13.454,"lo":-16.592,"k":1,"z":5.6,"s":4},{"n":"Skopje","a":"North Macedonia","i":"MK","p":494087,"la":42,"lo":21.433,"k":1,"z":5.6,"s":4},{"n":"Bridgetown","a":"Barbados","i":"BB","p":191152,"la":13.102,"lo":-59.617,"k":1,"z":5.6,"s":4},{"n":"Porto-Novo","a":"Benin","i":"BJ","p":300000,"la":6.483,"lo":2.617,"k":0,"z":5.6,"s":4},{"n":"Bujumbura","a":"Burundi","i":"BI","p":331700,"la":-3.376,"lo":29.36,"k":1,"z":5.6,"s":4},{"n":"Kingstown","a":"Saint Vincent and the Grenadines","i":"VC","p":49485,"la":13.156,"lo":-61.22,"k":1,"z":5.1,"s":4},{"n":"Castries","a":"Saint Lucia","i":"LC","p":37963,"la":14.008,"lo":-60.993,"k":1,"z":5.6,"s":4},{"n":"Basseterre","a":"Saint Kitts and Nevis","i":"KN","p":21887,"la":17.302,"lo":-62.717,"k":1,"z":5.6,"s":4},{"n":"Port Louis","a":"Mauritius","i":"MU","p":595491,"la":-20.167,"lo":57.5,"k":1,"z":5.6,"s":4},{"n":"Saint George's","a":"Grenada","i":"GD","p":33734,"la":12.053,"lo":-61.742,"k":1,"z":5.1,"s":4},{"n":"Manama","a":"Bahrain","i":"BH","p":563920,"la":26.236,"lo":50.583,"k":1,"z":5.6,"s":4},{"n":"Saint John's","a":"Antigua and Barbuda","i":"AG","p":35499,"la":17.118,"lo":-61.85,"k":1,"z":5.1,"s":4},{"n":"Montevideo","a":"Uruguay","i":"UY","p":1513000,"la":-34.905,"lo":-56.187,"k":1,"z":4,"s":3},{"n":"Lomé","a":"Togo","i":"TG","p":1452000,"la":6.134,"lo":1.221,"k":1,"z":5,"s":3},{"n":"Tunis","a":"Tunisia","i":"TN","p":2412500,"la":36.803,"lo":10.18,"k":1,"z":5,"s":3},{"n":"Abu Dhabi","a":"United Arab Emirates","i":"AE","p":603492,"la":24.467,"lo":54.367,"k":1,"z":5,"s":3},{"n":"Ashgabat","a":"Turkmenistan","i":"TM","p":727700,"la":37.95,"lo":58.383,"k":1,"z":5,"s":3},{"n":"Lusaka","a":"Zambia","i":"ZM","p":1328000,"la":-15.415,"lo":28.281,"k":1,"z":4,"s":3},{"n":"Harare","a":"Zimbabwe","i":"ZW","p":1572000,"la":-17.816,"lo":31.043,"k":1,"z":4,"s":3},{"n":"Dili","a":"East Timor","i":"TL","p":234331,"la":-8.559,"lo":125.579,"k":1,"z":5,"s":3},{"n":"Port Vila","a":"Vanuatu","i":"VU","p":44040,"la":-17.733,"lo":168.317,"k":1,"z":4,"s":3},{"n":"Tegucigalpa","a":"Honduras","i":"HN","p":946000,"la":14.104,"lo":-87.219,"k":1,"z":5,"s":3},{"n":"Georgetown","a":"Guyana","i":"GY","p":264350,"la":6.802,"lo":-58.167,"k":1,"z":4,"s":3},{"n":"Reykjavík","a":"Iceland","i":"IS","p":166212,"la":64.143,"lo":-21.937,"k":1,"z":3.7,"s":3},{"n":"Port-au-Prince","a":"Haiti","i":"HT","p":1998000,"la":18.543,"lo":-72.338,"k":1,"z":5,"s":3},{"n":"Kampala","a":"Uganda","i":"UG","p":1420000,"la":0.319,"lo":32.581,"k":1,"z":5,"s":3},{"n":"Paramaribo","a":"Suriname","i":"SR","p":254169,"la":5.835,"lo":-55.167,"k":1,"z":5,"s":3},{"n":"Niamey","a":"Niger","i":"NE","p":915000,"la":13.519,"lo":2.115,"k":1,"z":5,"s":3},{"n":"Dushanbe","a":"Tajikistan","i":"TJ","p":1086244,"la":38.56,"lo":68.774,"k":1,"z":5,"s":3},{"n":"Asunción","a":"Paraguay","i":"PY","p":1870000,"la":-25.291,"lo":-57.626,"k":1,"z":4,"s":3},{"n":"Managua","a":"Nicaragua","i":"NI","p":920000,"la":12.155,"lo":-86.27,"k":1,"z":5,"s":3},{"n":"Freetown","a":"Sierra Leone","i":"SL","p":827000,"la":8.472,"lo":-13.236,"k":1,"z":5,"s":3},{"n":"Islamabad","a":"Pakistan","i":"PK","p":780000,"la":33.689,"lo":73.081,"k":1,"z":5,"s":3},{"n":"Kathmandu","a":"Nepal","i":"NP","p":895000,"la":27.719,"lo":85.315,"k":1,"z":5,"s":3},{"n":"Bloemfontein","a":"South Africa","i":"ZA","p":463064,"la":-29.12,"lo":26.23,"k":1,"z":5,"s":3},{"n":"Pretoria","a":"South Africa","i":"ZA","p":1338000,"la":-25.705,"lo":28.227,"k":1,"z":5,"s":3},{"n":"Port Moresby","a":"Papua New Guinea","i":"PG","p":283733,"la":-9.465,"lo":147.193,"k":1,"z":4,"s":3},{"n":"Honiara","a":"Solomon Islands","i":"SB","p":76328,"la":-9.438,"lo":159.95,"k":1,"z":5,"s":3},{"n":"Panama City","a":"Panama","i":"PA","p":1281000,"la":8.97,"lo":-79.535,"k":1,"z":5,"s":3},{"n":"Rabat","a":"Morocco","i":"MA","p":1705000,"la":34.025,"lo":-6.836,"k":1,"z":5,"s":3},{"n":"Chișinău","a":"Moldova","i":"MD","p":688134,"la":47.005,"lo":28.858,"k":1,"z":5,"s":3},{"n":"Maputo","a":"Mozambique","i":"MZ","p":1446000,"la":-25.953,"lo":32.587,"k":1,"z":5,"s":3},{"n":"Mogadishu","a":"Somalia","i":"SO","p":1100000,"la":2.069,"lo":45.365,"k":1,"z":4,"s":3},{"n":"Muscat","a":"Oman","i":"OM","p":734697,"la":23.585,"lo":58.378,"k":1,"z":5,"s":3},{"n":"Colombo","a":"Sri Lanka","i":"LK","p":217000,"la":6.932,"lo":79.858,"k":1,"z":5,"s":3},{"n":"Ulaanbaatar","a":"Mongolia","i":"MN","p":885000,"la":47.919,"lo":106.915,"k":1,"z":4,"s":3},{"n":"Windhoek","a":"Namibia","i":"NA","p":268132,"la":-22.57,"lo":17.084,"k":1,"z":5,"s":3},{"n":"Abuja","a":"Nigeria","i":"NG","p":1576000,"la":9.055,"lo":7.49,"k":1,"z":4,"s":3},{"n":"Bissau","a":"Guinea Bissau","i":"GW","p":403339,"la":11.865,"lo":-15.598,"k":1,"z":5,"s":3},{"n":"Amman","a":"Jordan","i":"JO","p":1060000,"la":31.952,"lo":35.931,"k":1,"z":5,"s":3},{"n":"Vilnius","a":"Lithuania","i":"LT","p":542366,"la":54.683,"lo":25.317,"k":1,"z":5,"s":3},{"n":"Riga","a":"Latvia","i":"LV","p":742572,"la":56.95,"lo":24.1,"k":1,"z":4,"s":3},{"n":"Bishkek","a":"Kyrgyzstan","i":"KG","p":837000,"la":42.875,"lo":74.583,"k":1,"z":5,"s":3},{"n":"Maseru","a":"Lesotho","i":"LS","p":361324,"la":-29.317,"lo":27.483,"k":1,"z":5,"s":3},{"n":"Antananarivo","a":"Madagascar","i":"MG","p":1697000,"la":-18.915,"lo":47.515,"k":1,"z":5,"s":3},{"n":"Quito","a":"Ecuador","i":"EC","p":1701000,"la":-0.213,"lo":-78.502,"k":1,"z":4,"s":3},{"n":"San José","a":"Costa Rica","i":"CR","p":1284000,"la":9.93,"lo":-84.079,"k":1,"z":5,"s":3},{"n":"San Salvador","a":"El Salvador","i":"SV","p":1433000,"la":13.703,"lo":-89.216,"k":1,"z":5,"s":3},{"n":"Kingston","a":"Jamaica","i":"JM","p":937700,"la":17.977,"lo":-76.767,"k":1,"z":5,"s":3},{"n":"N'Djamena","a":"Chad","i":"TD","p":989000,"la":12.115,"lo":15.047,"k":1,"z":5,"s":3},{"n":"Malabo","a":"Equatorial Guinea","i":"GQ","p":155963,"la":3.75,"lo":8.783,"k":1,"z":5,"s":3},{"n":"Asmara","a":"Eritrea","i":"ER","p":620802,"la":15.333,"lo":38.933,"k":1,"z":5,"s":3},{"n":"Zagreb","a":"Croatia","i":"HR","p":722526,"la":45.8,"lo":16,"k":1,"z":5,"s":3},{"n":"Tallinn","a":"Estonia","i":"EE","p":394024,"la":59.434,"lo":24.728,"k":1,"z":5,"s":3},{"n":"Lilongwe","a":"Malawi","i":"MW","p":646750,"la":-13.983,"lo":33.783,"k":1,"z":5,"s":3},{"n":"Guatemala City","a":"Guatemala","i":"GT","p":1024000,"la":14.623,"lo":-90.529,"k":1,"z":5,"s":3},{"n":"Libreville","a":"Gabon","i":"GA","p":578156,"la":0.385,"lo":9.458,"k":1,"z":5,"s":3},{"n":"Suva","a":"Fiji","i":"FJ","p":175399,"la":-18.133,"lo":178.442,"k":1,"z":5,"s":3},{"n":"Valparaíso","a":"Chile","i":"CL","p":854000,"la":-33.048,"lo":-71.617,"k":0,"z":5,"s":3},{"n":"Nouakchott","a":"Mauritania","i":"MR","p":742144,"la":18.086,"lo":-15.975,"k":1,"z":5,"s":3},{"n":"Bamako","a":"Mali","i":"ML","p":1494000,"la":12.652,"lo":-8.002,"k":1,"z":4,"s":3},{"n":"Beirut","a":"Lebanon","i":"LB","p":1846000,"la":33.874,"lo":35.508,"k":1,"z":5,"s":3},{"n":"Tbilisi","a":"Georgia","i":"GE","p":1100000,"la":41.727,"lo":44.789,"k":1,"z":5,"s":3},{"n":"Nur-Sultan","a":"Kazakhstan","i":"KZ","p":345604,"la":51.181,"lo":71.428,"k":1,"z":4,"s":3},{"n":"Vientiane","a":"Laos","i":"LA","p":754000,"la":17.967,"lo":102.6,"k":1,"z":5,"s":3},{"n":"Brazzaville","a":"Congo (Brazzaville)","i":"CG","p":1355000,"la":-4.257,"lo":15.283,"k":1,"z":5,"s":3},{"n":"Conakry","a":"Guinea","i":"GN","p":1494000,"la":9.533,"lo":-13.682,"k":1,"z":5,"s":3},{"n":"Yamoussoukro","a":"Ivory Coast","i":"CI","p":206499,"la":6.818,"lo":-5.276,"k":1,"z":5,"s":3},{"n":"Ottawa","a":"Canada","i":"CA","p":1145000,"la":45.419,"lo":-75.702,"k":1,"z":3,"s":3},{"n":"Belgrade","a":"Serbia","i":"RS","p":1099000,"la":44.821,"lo":20.466,"k":1,"z":5,"s":3},{"n":"Bandar Seri Begawan","a":"Brunei","i":"BN","p":296500,"la":4.883,"lo":114.933,"k":1,"z":5,"s":3},{"n":"Sucre","a":"Bolivia","i":"BO","p":224838,"la":-19.041,"lo":-65.26,"k":1,"z":4.7,"s":3},{"n":"Belmopan","a":"Belize","i":"BZ","p":15220,"la":17.252,"lo":-88.767,"k":1,"z":5,"s":3},{"n":"Bangui","a":"Central African Republic","i":"CF","p":831925,"la":4.367,"lo":18.558,"k":1,"z":5,"s":3},{"n":"Yaoundé","a":"Cameroon","i":"CM","p":1611000,"la":3.869,"lo":11.515,"k":1,"z":5,"s":3},{"n":"Tirana","a":"Albania","i":"AL","p":895350,"la":41.328,"lo":19.819,"k":1,"z":5,"s":3},{"n":"Yerevan","a":"Armenia","i":"AM","p":1102000,"la":40.183,"lo":44.512,"k":1,"z":5,"s":3},{"n":"Baku","a":"Azerbaijan","i":"AZ","p":2122300,"la":40.397,"lo":49.86,"k":1,"z":5,"s":3},{"n":"Phnom Penh","a":"Cambodia","i":"KH","p":1466000,"la":11.552,"lo":104.915,"k":1,"z":5,"s":3},{"n":"La Paz","a":"Bolivia","i":"BO","p":1590000,"la":-16.496,"lo":-68.152,"k":1,"z":4,"s":3},{"n":"Cotonou","a":"Benin","i":"BJ","p":762000,"la":6.363,"lo":2.404,"k":1,"z":5,"s":3},{"n":"Sofia","a":"Bulgaria","i":"BG","p":1185000,"la":42.685,"lo":23.315,"k":1,"z":5,"s":3},{"n":"Minsk","a":"Belarus","i":"BY","p":1805000,"la":53.902,"lo":27.565,"k":1,"z":5,"s":3},{"n":"Thimphu","a":"Bhutan","i":"BT","p":98676,"la":27.473,"lo":89.639,"k":1,"z":5,"s":3},{"n":"Gaborone","a":"Botswana","i":"BW","p":208411,"la":-24.646,"lo":25.912,"k":1,"z":5,"s":3},{"n":"Canberra","a":"Australia","i":"AU","p":327700,"la":-35.283,"lo":149.129,"k":1,"z":4,"s":3},{"n":"Ouagadougou","a":"Burkina Faso","i":"BF","p":1149000,"la":12.372,"lo":-1.527,"k":1,"z":5,"s":3},{"n":"Sarajevo","a":"Bosnia and Herzegovina","i":"BA","p":696731,"la":43.85,"lo":18.383,"k":1,"z":5,"s":3},{"n":"Naypyidaw","a":"Myanmar","i":"MM","p":930000,"la":19.769,"lo":96.117,"k":1,"z":5,"s":3},{"n":"Nuku'alofa","a":"Tonga","i":"TO","p":42620,"la":-21.139,"lo":-175.221,"k":1,"z":5,"s":3},{"n":"Hargeisa","a":"Somaliland","i":"-99","p":477876,"la":9.56,"lo":44.065,"k":1,"z":5,"s":3},{"n":"Victoria","a":"Seychelles","i":"SC","p":33576,"la":-4.617,"lo":55.45,"k":1,"z":5,"s":3},{"n":"São Tomé","a":"Sao Tome and Principe","i":"ST","p":88219,"la":0.337,"lo":6.73,"k":1,"z":5,"s":3},{"n":"Apia","a":"Samoa","i":"WS","p":61916,"la":-13.836,"lo":-171.769,"k":1,"z":5,"s":3},{"n":"Valletta","a":"Malta","i":"MT","p":368250,"la":35.9,"lo":14.515,"k":1,"z":5,"s":3},{"n":"Malé","a":"Maldives","i":"MV","p":112927,"la":4.172,"lo":73.509,"k":1,"z":5,"s":3},{"n":"Jerusalem","a":"Israel","i":"IL","p":1029300,"la":31.778,"lo":35.207,"k":1,"z":3.7,"s":3},{"n":"Praia","a":"Cape Verde","i":"CV","p":113364,"la":14.917,"lo":-23.517,"k":1,"z":5,"s":3},{"n":"Nassau","a":"The Bahamas","i":"BS","p":227940,"la":25.083,"lo":-77.35,"k":1,"z":5,"s":3},{"n":"Nicosia","a":"Cyprus","i":"CY","p":224300,"la":35.167,"lo":33.367,"k":1,"z":5,"s":3},{"n":"Wellington","a":"New Zealand","i":"NZ","p":393400,"la":-41.292,"lo":174.777,"k":1,"z":4,"s":3},{"n":"Hanoi","a":"Vietnam","i":"VN","p":4378000,"la":21.035,"lo":105.848,"k":1,"z":4,"s":2},{"n":"Ankara","a":"Turkey","i":"TR","p":3716000,"la":39.929,"lo":32.862,"k":1,"z":4,"s":2},{"n":"Budapest","a":"Hungary","i":"HU","p":1679000,"la":47.502,"lo":19.081,"k":1,"z":4.7,"s":2},{"n":"Sanaa","a":"Yemen","i":"YE","p":2008000,"la":15.357,"lo":44.205,"k":1,"z":4,"s":2},{"n":"Bucharest","a":"Romania","i":"RO","p":1942000,"la":44.435,"lo":26.098,"k":1,"z":4,"s":2},{"n":"Damascus","a":"Syria","i":"SY","p":2466000,"la":33.502,"lo":36.298,"k":1,"z":4.7,"s":2},{"n":"Lisbon","a":"Portugal","i":"PT","p":2812000,"la":38.725,"lo":-9.147,"k":1,"z":4,"s":2},{"n":"Khartoum","a":"Sudan","i":"SD","p":4754000,"la":15.59,"lo":32.532,"k":1,"z":4,"s":2},{"n":"Oslo","a":"Norway","i":"NO","p":835000,"la":59.919,"lo":10.748,"k":1,"z":4,"s":2},{"n":"Warsaw","a":"Poland","i":"PL","p":1707000,"la":52.231,"lo":21.005,"k":1,"z":4,"s":2},{"n":"Pyongyang","a":"North Korea","i":"KP","p":3300000,"la":39.021,"lo":125.753,"k":1,"z":4.7,"s":2},{"n":"Dar es Salaam","a":"Tanzania","i":"TZ","p":2930000,"la":-6.798,"lo":39.266,"k":1,"z":4,"s":2},{"n":"Dublin","a":"Ireland","i":"IE","p":1059000,"la":53.347,"lo":-6.257,"k":1,"z":4,"s":2},{"n":"Monrovia","a":"Liberia","i":"LR","p":1041000,"la":6.315,"lo":-10.8,"k":1,"z":4,"s":2},{"n":"Kuala Lumpur","a":"Malaysia","i":"MY","p":1448000,"la":3.14,"lo":101.689,"k":1,"z":4,"s":2},{"n":"Havana","a":"Cuba","i":"CU","p":2174000,"la":23.134,"lo":-82.366,"k":1,"z":4,"s":2},{"n":"Prague","a":"Czechia","i":"CZ","p":1162000,"la":50.087,"lo":14.423,"k":1,"z":4.7,"s":2},{"n":"Kuwait City","a":"Kuwait","i":"KW","p":2063000,"la":29.372,"lo":47.976,"k":1,"z":4,"s":2},{"n":"Santo Domingo","a":"Dominican Republic","i":"DO","p":2154000,"la":18.471,"lo":-69.93,"k":1,"z":4,"s":2},{"n":"Accra","a":"Ghana","i":"GH","p":2121000,"la":5.552,"lo":-0.219,"k":1,"z":4,"s":2},{"n":"Tripoli","a":"Libya","i":"LY","p":2189000,"la":32.893,"lo":13.18,"k":1,"z":4,"s":2},{"n":"Tel Aviv","a":"Israel","i":"IL","p":3112000,"la":32.082,"lo":34.768,"k":0,"z":4,"s":2},{"n":"Helsinki","a":"Finland","i":"FI","p":1115000,"la":60.164,"lo":24.932,"k":1,"z":4,"s":2},{"n":"København","a":"Denmark","i":"DK","p":1085000,"la":55.681,"lo":12.562,"k":1,"z":4,"s":2},{"n":"Abidjan","a":"Ivory Coast","i":"CI","p":3802000,"la":5.323,"lo":-4.02,"k":1,"z":4,"s":2},{"n":"Brasília","a":"Brazil","i":"BR","p":3716996,"la":-15.781,"lo":-47.918,"k":1,"z":3.7,"s":2},{"n":"Brussels","a":"Belgium","i":"BE","p":1743000,"la":50.835,"lo":4.331,"k":1,"z":4.7,"s":2},{"n":"Dhaka","a":"Bangladesh","i":"BD","p":12797394,"la":23.725,"lo":90.407,"k":1,"z":4,"s":2},{"n":"Luanda","a":"Angola","i":"AO","p":5172900,"la":-8.836,"lo":13.232,"k":1,"z":4,"s":2},{"n":"Algiers","a":"Algeria","i":"DZ","p":3354000,"la":36.765,"lo":3.049,"k":1,"z":4,"s":2},{"n":"Yangon","a":"Myanmar","i":"MM","p":4088000,"la":16.785,"lo":96.165,"k":0,"z":4,"s":2},{"n":"San Francisco","a":"United States of America","i":"US","p":3450000,"la":37.784,"lo":-122.4,"k":0,"z":2.7,"s":1},{"n":"Denver","a":"United States of America","i":"US","p":2313000,"la":39.741,"lo":-104.986,"k":0,"z":3.7,"s":1},{"n":"Houston","a":"United States of America","i":"US","p":4459000,"la":29.741,"lo":-95.348,"k":0,"z":3,"s":1},{"n":"Miami","a":"United States of America","i":"US","p":5585000,"la":25.79,"lo":-80.226,"k":0,"z":2.1,"s":1},{"n":"Atlanta","a":"United States of America","i":"US","p":4506000,"la":33.739,"lo":-84.368,"k":0,"z":3,"s":1},{"n":"Chicago","a":"United States of America","i":"US","p":8990000,"la":41.848,"lo":-87.635,"k":0,"z":3,"s":1},{"n":"Caracas","a":"Venezuela","i":"VE","p":2985000,"la":10.503,"lo":-66.919,"k":1,"z":3,"s":1},{"n":"Kyiv","a":"Ukraine","i":"UA","p":2709000,"la":50.435,"lo":30.515,"k":1,"z":3.7,"s":1},{"n":"Dubai","a":"United Arab Emirates","i":"AE","p":1379000,"la":25.215,"lo":55.287,"k":0,"z":3.7,"s":1},{"n":"Tashkent","a":"Uzbekistan","i":"UZ","p":2184000,"la":41.304,"lo":69.269,"k":1,"z":3.7,"s":1},{"n":"Madrid","a":"Spain","i":"ES","p":5567000,"la":40.402,"lo":-3.685,"k":1,"z":3,"s":1},{"n":"Geneva","a":"Switzerland","i":"CH","p":1240000,"la":46.21,"lo":6.14,"k":0,"z":3.7,"s":1},{"n":"Stockholm","a":"Sweden","i":"SE","p":1264000,"la":59.324,"lo":18.066,"k":1,"z":3,"s":1},{"n":"Bangkok","a":"Thailand","i":"TH","p":6704000,"la":13.752,"lo":100.515,"k":1,"z":3,"s":1},{"n":"Lima","a":"Peru","i":"PE","p":8012000,"la":-12.046,"lo":-77.052,"k":1,"z":3,"s":1},{"n":"Dakar","a":"Senegal","i":"SN","p":2604000,"la":14.718,"lo":-17.475,"k":1,"z":3,"s":1},{"n":"Johannesburg","a":"South Africa","i":"ZA","p":3435000,"la":-26.168,"lo":28.028,"k":0,"z":3,"s":1},{"n":"Amsterdam","a":"Netherlands","i":"NL","p":1031000,"la":52.352,"lo":4.915,"k":1,"z":3.7,"s":1},{"n":"Casablanca","a":"Morocco","i":"MA","p":3181000,"la":33.602,"lo":-7.618,"k":0,"z":3.7,"s":1},{"n":"Seoul","a":"South Korea","i":"KR","p":9796000,"la":37.568,"lo":126.998,"k":1,"z":3,"s":1},{"n":"Manila","a":"Philippines","i":"PH","p":11100000,"la":14.606,"lo":120.98,"k":1,"z":2.7,"s":1},{"n":"Monterrey","a":"Mexico","i":"MX","p":3712000,"la":25.672,"lo":-100.332,"k":0,"z":3.7,"s":1},{"n":"Berlin","a":"Germany","i":"DE","p":3406000,"la":52.524,"lo":13.4,"k":1,"z":2.7,"s":1},{"n":"Ürümqi","a":"China","i":"CN","p":3575000,"la":43.807,"lo":87.573,"k":0,"z":3.7,"s":1},{"n":"Chengdu","a":"China","i":"CN","p":4123000,"la":30.672,"lo":104.068,"k":0,"z":3,"s":1},{"n":"Ōsaka","a":"Japan","i":"JP","p":11294000,"la":34.691,"lo":135.504,"k":0,"z":3,"s":1},{"n":"Kinshasa","a":"Congo (Kinshasa)","i":"CD","p":7843000,"la":-4.328,"lo":15.313,"k":1,"z":3,"s":1},{"n":"New Delhi","a":"India","i":"IN","p":317797,"la":28.6,"lo":77.2,"k":1,"z":2.1,"s":1},{"n":"Bengaluru","a":"India","i":"IN","p":6787000,"la":12.972,"lo":77.558,"k":0,"z":3.7,"s":1},{"n":"Athens","a":"Greece","i":"GR","p":3242000,"la":37.985,"lo":23.731,"k":1,"z":3.7,"s":1},{"n":"Baghdad","a":"Iraq","i":"IQ","p":5054000,"la":33.341,"lo":44.392,"k":1,"z":3.7,"s":1},{"n":"Addis Ababa","a":"Ethiopia","i":"ET","p":3100000,"la":9.035,"lo":38.698,"k":1,"z":3,"s":1},{"n":"Tehran","a":"Iran","i":"IR","p":7873000,"la":35.674,"lo":51.422,"k":1,"z":3,"s":1},{"n":"Vancouver","a":"Canada","i":"CA","p":2313328,"la":49.275,"lo":-123.124,"k":0,"z":2.1,"s":1},{"n":"Toronto","a":"Canada","i":"CA","p":5213000,"la":43.665,"lo":-79.389,"k":0,"z":2.1,"s":1},{"n":"Buenos Aires","a":"Argentina","i":"AR","p":12795000,"la":-34.611,"lo":-58.433,"k":1,"z":2.1,"s":1},{"n":"Kabul","a":"Afghanistan","i":"AF","p":3277000,"la":34.519,"lo":69.181,"k":1,"z":3.7,"s":1},{"n":"Vienna","a":"Austria","i":"AT","p":2400000,"la":48.202,"lo":16.365,"k":1,"z":3.7,"s":1},{"n":"Melbourne","a":"Australia","i":"AU","p":4170000,"la":-37.818,"lo":144.973,"k":0,"z":3,"s":1},{"n":"Taipei","a":"Taiwan","i":"TW","p":6900273,"la":25.036,"lo":121.568,"k":1,"z":3.7,"s":1},{"n":"Auckland","a":"New Zealand","i":"NZ","p":1377200,"la":-36.848,"lo":174.763,"k":0,"z":3,"s":1},{"n":"Los Angeles","a":"United States of America","i":"US","p":12500000,"la":34.049,"lo":-118.232,"k":0,"z":2,"s":0},{"n":"Washington, D.C.","a":"United States of America","i":"US","p":4338000,"la":38.901,"lo":-77.011,"k":1,"z":2.1,"s":0},{"n":"New York","a":"United States of America","i":"US","p":19040000,"la":40.722,"lo":-73.996,"k":0,"z":1.7,"s":0},{"n":"London","a":"United Kingdom","i":"GB","p":8567000,"la":51.502,"lo":-0.119,"k":1,"z":1.7,"s":0},{"n":"Istanbul","a":"Turkey","i":"TR","p":10061000,"la":41.018,"lo":28.974,"k":0,"z":3,"s":0},{"n":"Riyadh","a":"Saudi Arabia","i":"SA","p":4465000,"la":24.634,"lo":46.72,"k":1,"z":3.7,"s":0},{"n":"Cape Town","a":"South Africa","i":"ZA","p":3215000,"la":-33.918,"lo":18.433,"k":1,"z":1.7,"s":0},{"n":"Moscow","a":"Russia","i":"RU","p":10452000,"la":55.754,"lo":37.614,"k":1,"z":1.7,"s":0},{"n":"Mexico City","a":"Mexico","i":"MX","p":19028000,"la":19.444,"lo":-99.133,"k":1,"z":2.1,"s":0},{"n":"Lagos","a":"Nigeria","i":"NG","p":9466000,"la":6.445,"lo":3.39,"k":0,"z":2,"s":0},{"n":"Rome","a":"Italy","i":"IT","p":3339000,"la":41.898,"lo":12.481,"k":1,"z":2.7,"s":0},{"n":"Beijing","a":"China","i":"CN","p":11106000,"la":39.902,"lo":116.394,"k":1,"z":1.7,"s":0},{"n":"Nairobi","a":"Kenya","i":"KE","p":3010000,"la":-1.281,"lo":36.815,"k":1,"z":2,"s":0},{"n":"Jakarta","a":"Indonesia","i":"ID","p":9125000,"la":-6.172,"lo":106.827,"k":1,"z":2.1,"s":0},{"n":"Bogota","a":"Colombia","i":"CO","p":7772000,"la":4.598,"lo":-74.085,"k":1,"z":2.1,"s":0},{"n":"Cairo","a":"Egypt","i":"EG","p":11893000,"la":30.052,"lo":31.248,"k":1,"z":1.7,"s":0},{"n":"Shanghai","a":"China","i":"CN","p":14987000,"la":31.218,"lo":121.435,"k":0,"z":2,"s":0},{"n":"Tokyo","a":"Japan","i":"JP","p":35676000,"la":35.687,"lo":139.749,"k":1,"z":1.7,"s":0},{"n":"Mumbai","a":"India","i":"IN","p":18978000,"la":19.068,"lo":72.876,"k":0,"z":1.7,"s":0},{"n":"Paris","a":"France","i":"FR","p":9904000,"la":48.858,"lo":2.353,"k":1,"z":1.7,"s":0},{"n":"Santiago","a":"Chile","i":"CL","p":5720000,"la":-33.44,"lo":-70.651,"k":1,"z":2,"s":0},{"n":"Kolkata","a":"India","i":"IN","p":14787000,"la":22.57,"lo":88.369,"k":0,"z":3.7,"s":0},{"n":"Rio de Janeiro","a":"Brazil","i":"BR","p":11748000,"la":-22.907,"lo":-43.212,"k":0,"z":1.7,"s":0},{"n":"São Paulo","a":"Brazil","i":"BR","p":18845000,"la":-23.557,"lo":-46.627,"k":0,"z":3,"s":0},{"n":"Sydney","a":"Australia","i":"AU","p":4630000,"la":-33.871,"lo":151.213,"k":0,"z":1.7,"s":0},{"n":"Singapore","a":"Singapore","i":"SG","p":5183700,"la":1.295,"lo":103.854,"k":1,"z":2.1,"s":0},{"n":"Hong Kong","a":"Hong Kong S.A.R.","i":"HK","p":7206000,"la":22.307,"lo":114.183,"k":0,"z":3,"s":0}]} \ No newline at end of file diff --git a/src/maps/data/places_50m.json b/src/maps/data/places_50m.json new file mode 100644 index 0000000..ba16935 --- /dev/null +++ b/src/maps/data/places_50m.json @@ -0,0 +1 @@ +{"f":[{"n":"Bombo","a":"Uganda","i":"UG","p":75000,"la":0.583,"lo":32.533,"k":0,"z":7,"s":10},{"n":"Fort Portal","a":"Uganda","i":"UG","p":42670,"la":0.671,"lo":30.275,"k":0,"z":7,"s":10},{"n":"Potenza","a":"Italy","i":"IT","p":69060,"la":40.642,"lo":15.799,"k":0,"z":7,"s":10},{"n":"Campobasso","a":"Italy","i":"IT","p":50762,"la":41.563,"lo":14.656,"k":0,"z":7,"s":10},{"n":"Aosta","a":"Italy","i":"IT","p":34062,"la":45.737,"lo":7.315,"k":0,"z":7,"s":10},{"n":"Mariehamn","a":"Aland","i":"AX","p":10682,"la":60.097,"lo":19.949,"k":0,"z":7,"s":10},{"n":"Ramallah","a":"Palestine","i":"PS","p":24599,"la":31.903,"lo":35.206,"k":0,"z":9,"s":10},{"n":"Vatican City","a":"Vatican","i":"VA","p":832,"la":41.903,"lo":12.453,"k":1,"z":7,"s":8},{"n":"Poitier","a":"France","i":"FR","p":85960,"la":46.583,"lo":0.333,"k":0,"z":6.7,"s":8},{"n":"Clermont-Ferrand","a":"France","i":"FR","p":233050,"la":45.78,"lo":3.08,"k":0,"z":6,"s":8},{"n":"Besançon","a":"France","i":"FR","p":128426,"la":47.23,"lo":6.03,"k":0,"z":7,"s":8},{"n":"Artigas Base","a":"Antarctica","i":"AQ","p":60,"la":-62.174,"lo":-58.864,"k":0,"z":7,"s":8},{"n":"Chipata","a":"Zambia","i":"ZM","p":85963,"la":-13.63,"lo":32.64,"k":0,"z":6.7,"s":7},{"n":"Jinja","a":"Uganda","i":"UG","p":301619,"la":0.44,"lo":33.2,"k":0,"z":6.7,"s":7},{"n":"Arua","a":"Uganda","i":"UG","p":250000,"la":3.02,"lo":30.9,"k":0,"z":6.7,"s":7},{"n":"Mbale","a":"Uganda","i":"UG","p":402368,"la":1.09,"lo":34.17,"k":0,"z":6.7,"s":7},{"n":"Moroto","a":"Uganda","i":"UG","p":371,"la":2.54,"lo":34.64,"k":0,"z":6.7,"s":7},{"n":"Masaka","a":"Uganda","i":"UG","p":65373,"la":-0.33,"lo":31.73,"k":0,"z":6.7,"s":7},{"n":"Mbarara","a":"Uganda","i":"UG","p":83700,"la":-0.6,"lo":30.65,"k":0,"z":6.7,"s":7},{"n":"Otjiwarongo","a":"Namibia","i":"NA","p":24815,"la":-20.46,"lo":16.64,"k":0,"z":6.7,"s":7},{"n":"Bologna","a":"Italy","i":"IT","p":488172,"la":44.5,"lo":11.34,"k":0,"z":6.1,"s":7},{"n":"Cagliari","a":"Italy","i":"IT","p":291511,"la":39.222,"lo":9.104,"k":0,"z":6.1,"s":7},{"n":"Catanzaro","a":"Italy","i":"IT","p":95251,"la":38.9,"lo":16.6,"k":0,"z":6.7,"s":7},{"n":"Bari","a":"Italy","i":"IT","p":500577,"la":41.114,"lo":16.873,"k":0,"z":5.1,"s":7},{"n":"L'Aquila","a":"Italy","i":"IT","p":68503,"la":42.35,"lo":13.39,"k":0,"z":6.7,"s":7},{"n":"Ancona","a":"Italy","i":"IT","p":100507,"la":43.6,"lo":13.5,"k":0,"z":6.1,"s":7},{"n":"Perugia","a":"Italy","i":"IT","p":149125,"la":43.11,"lo":12.39,"k":0,"z":6.7,"s":7},{"n":"Trieste","a":"Italy","i":"IT","p":216035,"la":45.65,"lo":13.8,"k":0,"z":6.7,"s":7},{"n":"Trento","a":"Italy","i":"IT","p":107808,"la":46.08,"lo":11.12,"k":0,"z":6.1,"s":7},{"n":"Fort-de-France","a":"France","i":"MQ","p":253995,"la":14.61,"lo":-61.08,"k":0,"z":6.7,"s":7},{"n":"Gifu","a":"Japan","i":"JP","p":412895,"la":35.423,"lo":136.763,"k":0,"z":6.1,"s":7},{"n":"Saint-Laurent-du-Maroni","a":"France","i":"GF","p":24287,"la":5.498,"lo":-54.033,"k":0,"z":6.7,"s":7},{"n":"Caen","a":"France","i":"FR","p":190099,"la":49.184,"lo":-0.35,"k":0,"z":6.7,"s":7},{"n":"Nantes","a":"France","i":"FR","p":438537,"la":47.21,"lo":-1.59,"k":0,"z":5.1,"s":7},{"n":"Ajaccio","a":"France","i":"FR","p":54364,"la":41.927,"lo":8.728,"k":0,"z":6.1,"s":7},{"n":"Montpellier","a":"France","i":"FR","p":327254,"la":43.61,"lo":3.87,"k":0,"z":6.1,"s":7},{"n":"Dijon","a":"France","i":"FR","p":169946,"la":47.33,"lo":5.03,"k":0,"z":6,"s":7},{"n":"Orléans","a":"France","i":"FR","p":217301,"la":47.9,"lo":1.9,"k":0,"z":6.1,"s":7},{"n":"Rouen","a":"France","i":"FR","p":532559,"la":49.43,"lo":1.08,"k":0,"z":6,"s":7},{"n":"Reims","a":"France","i":"FR","p":196565,"la":49.25,"lo":4.03,"k":0,"z":6.1,"s":7},{"n":"Amiens","a":"France","i":"FR","p":143086,"la":49.9,"lo":2.3,"k":0,"z":6.7,"s":7},{"n":"Nancy","a":"France","i":"FR","p":268976,"la":48.684,"lo":6.2,"k":0,"z":5.1,"s":7},{"n":"Basse-terre","a":"France","i":"GP","p":307,"la":16.01,"lo":-61.705,"k":0,"z":6.7,"s":7},{"n":"Novi Sad","a":"Serbia","i":"RS","p":225457,"la":45.25,"lo":19.85,"k":0,"z":6.1,"s":7},{"n":"Banja Luka","a":"Bosnia and Herzegovina","i":"BA","p":221738,"la":44.78,"lo":17.18,"k":0,"z":6.1,"s":7},{"n":"George Town","a":"Cayman Islands","i":"KY","p":4268,"la":19.295,"lo":-81.372,"k":0,"z":6.7,"s":7},{"n":"Grand Turk","a":"Turks and Caicos Islands","i":"TC","p":5801,"la":21.466,"lo":-71.136,"k":0,"z":6.7,"s":7},{"n":"Douglas","a":"Isle of Man","i":"IM","p":35854,"la":54.149,"lo":-4.48,"k":0,"z":6.7,"s":7},{"n":"San Marino","a":"San Marino","i":"SM","p":29579,"la":43.936,"lo":12.442,"k":1,"z":6.1,"s":7},{"n":"Willemstad","a":"Curacao","i":"CW","p":146813,"la":12.112,"lo":-68.872,"k":0,"z":6.7,"s":7},{"n":"Oranjestad","a":"Aruba","i":"AW","p":68775,"la":12.522,"lo":-70.026,"k":0,"z":6.7,"s":7},{"n":"Vaduz","a":"Liechtenstein","i":"LI","p":36281,"la":47.134,"lo":9.517,"k":1,"z":6.7,"s":7},{"n":"Capitan Arturo Prat Station","a":"Antarctica","i":"AQ","p":41,"la":-62.5,"lo":-59.683,"k":0,"z":6.1,"s":7},{"n":"Marambio Base","a":"Antarctica","i":"AQ","p":150,"la":-64.233,"lo":-56.65,"k":0,"z":5,"s":7},{"n":"Zucchelli Station","a":"Antarctica","i":"AQ","p":80,"la":-74.619,"lo":164.219,"k":0,"z":6.7,"s":7},{"n":"Rothera Station","a":"Antarctica","i":"AQ","p":130,"la":-67.565,"lo":-68.124,"k":0,"z":4,"s":7},{"n":"Palmer Station","a":"Antarctica","i":"AQ","p":46,"la":-64.762,"lo":-64.047,"k":0,"z":4,"s":7},{"n":"Base Presidente Montalva","a":"Antarctica","i":"AQ","p":150,"la":-62.183,"lo":-58.908,"k":0,"z":5,"s":7},{"n":"Carlini Base","a":"Antarctica","i":"AQ","p":60,"la":-62.224,"lo":-58.65,"k":0,"z":6.1,"s":7},{"n":"King Sejong Station","a":"Antarctica","i":"AQ","p":90,"la":-62.225,"lo":-58.772,"k":0,"z":6.1,"s":7},{"n":"Great Wall Station","a":"Antarctica","i":"AQ","p":40,"la":-62.216,"lo":-58.967,"k":0,"z":5,"s":7},{"n":"Escudero Base","a":"Antarctica","i":"AQ","p":20,"la":-62.195,"lo":-58.953,"k":0,"z":6.1,"s":7},{"n":"Elephant Island","a":"Antarctica","i":"AQ","p":6,"la":-62,"lo":-58,"k":0,"z":6.1,"s":7},{"n":"Scott Base","a":"Antarctica","i":"AQ","p":85,"la":-77.847,"lo":166.749,"k":0,"z":6.7,"s":7},{"n":"McMurdo Station","a":"Antarctica","i":"AQ","p":1000,"la":-77.732,"lo":166.869,"k":0,"z":3,"s":7},{"n":"Zhongshan Station","a":"Antarctica","i":"AQ","p":60,"la":-69.434,"lo":76.339,"k":0,"z":6.1,"s":7},{"n":"Vostok Station","a":"Antarctica","i":"AQ","p":25,"la":-78.466,"lo":106.8,"k":0,"z":6.7,"s":7},{"n":"Peter I Island","a":"Antarctica","i":"AQ","p":1,"la":-68.795,"lo":-90.584,"k":0,"z":6.1,"s":7},{"n":"Mirny Station","a":"Antarctica","i":"AQ","p":169,"la":-66.576,"lo":93.006,"k":0,"z":3,"s":7},{"n":"Mawson Station","a":"Antarctica","i":"AQ","p":60,"la":-67.615,"lo":62.875,"k":0,"z":6.1,"s":7},{"n":"Davis Station","a":"Antarctica","i":"AQ","p":70,"la":-68.778,"lo":78.141,"k":0,"z":6.1,"s":7},{"n":"Concordia Research Station","a":"Antarctica","i":"AQ","p":30,"la":-74.666,"lo":124.167,"k":0,"z":6.7,"s":7},{"n":"Casey Station","a":"Antarctica","i":"AQ","p":200,"la":-66.286,"lo":110.536,"k":0,"z":3,"s":7},{"n":"Amundsen–Scott South Pole Station","a":"Antarctica","i":"AQ","p":200,"la":-90,"lo":176.994,"k":0,"z":5,"s":7},{"n":"Wasa Station","a":"Antarctica","i":"AQ","p":10,"la":-73.05,"lo":-13.417,"k":0,"z":6.1,"s":7},{"n":"Troll Station","a":"Antarctica","i":"AQ","p":44,"la":-72.016,"lo":2.533,"k":0,"z":6.1,"s":7},{"n":"Svea Station","a":"Antarctica","i":"AQ","p":10,"la":-74.583,"lo":-11.217,"k":0,"z":6.1,"s":7},{"n":"Novolazarevskaya Station","a":"Antarctica","i":"AQ","p":70,"la":-71.301,"lo":-11.85,"k":0,"z":6.1,"s":7},{"n":"Neumayer Station III","a":"Antarctica","i":"AQ","p":40,"la":-70.89,"lo":-7.838,"k":0,"z":6.1,"s":7},{"n":"Maitri Station","a":"Antarctica","i":"AQ","p":65,"la":-70.782,"lo":11.73,"k":0,"z":6.1,"s":7},{"n":"Halley Station","a":"Antarctica","i":"AQ","p":70,"la":-76.092,"lo":-26.474,"k":0,"z":6.1,"s":7},{"n":"Belgrano II Base","a":"Antarctica","i":"AQ","p":100,"la":-77.866,"lo":-34.617,"k":0,"z":6.1,"s":7},{"n":"Sobral Base","a":"Antarctica","i":"AQ","p":40,"la":-81.179,"lo":-40.502,"k":0,"z":6.1,"s":7},{"n":"Aboa Station","a":"Antarctica","i":"AQ","p":18,"la":-75.05,"lo":-13.417,"k":0,"z":6.1,"s":7},{"n":"San Martín Base","a":"Antarctica","i":"AQ","p":20,"la":-68.116,"lo":-67.1,"k":0,"z":6.1,"s":7},{"n":"Gen. O'Higgins Base","a":"Antarctica","i":"AQ","p":44,"la":-63.328,"lo":-57.89,"k":0,"z":6.1,"s":7},{"n":"Esperanza Base","a":"Antarctica","i":"AQ","p":250,"la":-63.375,"lo":-57.026,"k":0,"z":3,"s":7},{"n":"Orcadas Station","a":"Antarctica","i":"AQ","p":45,"la":-60.733,"lo":-44.733,"k":0,"z":6.1,"s":7},{"n":"Signy Research Station","a":"Antarctica","i":"AQ","p":8,"la":-60.631,"lo":-45.601,"k":0,"z":6.1,"s":7},{"n":"Dumont d'Urville Station","a":"Antarctica","i":"AQ","p":120,"la":-66.775,"lo":139.964,"k":0,"z":4,"s":7},{"n":"Showa Station","a":"Antarctica","i":"AQ","p":60,"la":-69.03,"lo":39.746,"k":0,"z":6.1,"s":7},{"n":"Gibraltar","a":"Gibraltar","i":"GI","p":187083,"la":36.128,"lo":-5.346,"k":0,"z":5.1,"s":6},{"n":"Lobamba","a":"eSwatini","i":"SZ","p":9782,"la":-26.467,"lo":31.2,"k":0,"z":6,"s":6},{"n":"Edinburgh","a":"United Kingdom","i":"GB","p":504966,"la":55.948,"lo":-3.219,"k":0,"z":5.1,"s":6},{"n":"Cardiff","a":"United Kingdom","i":"GB","p":861400,"la":51.483,"lo":-3.168,"k":0,"z":5.1,"s":6},{"n":"Longyearbyen","a":"Svalbard and Jan Mayen Islands","i":"SJ","p":1232,"la":78.221,"lo":15.621,"k":0,"z":6,"s":6},{"n":"Luxembourg","a":"Luxembourg","i":"LU","p":107260,"la":49.612,"lo":6.13,"k":1,"z":6,"s":6},{"n":"Turin","a":"Italy","i":"IT","p":1652000,"la":45.072,"lo":7.668,"k":0,"z":6,"s":6},{"n":"Nouméa","a":"New Caledonia","i":"NC","p":93060,"la":-22.263,"lo":166.444,"k":0,"z":6,"s":6},{"n":"Matsuyama","a":"Japan","i":"JP","p":533541,"la":33.846,"lo":132.766,"k":0,"z":6,"s":6},{"n":"Rennes","a":"France","i":"FR","p":209375,"la":48.1,"lo":-1.67,"k":0,"z":6,"s":6},{"n":"Toulouse","a":"France","i":"FR","p":847000,"la":43.622,"lo":1.448,"k":0,"z":6,"s":6},{"n":"Limoges","a":"France","i":"FR","p":152199,"la":45.83,"lo":1.25,"k":0,"z":6.7,"s":6},{"n":"Lille","a":"France","i":"FR","p":1044000,"la":50.652,"lo":3.078,"k":0,"z":6,"s":6},{"n":"Strasbourg","a":"France","i":"FR","p":439972,"la":48.58,"lo":7.75,"k":0,"z":6,"s":6},{"n":"Batumi","a":"Georgia","i":"GE","p":155542,"la":41.6,"lo":41.63,"k":0,"z":6,"s":6},{"n":"Funchal","a":"Portugal","i":"PT","p":204767,"la":32.65,"lo":-16.88,"k":0,"z":6,"s":6},{"n":"El Fasher","a":"Sudan","i":"SD","p":252609,"la":13.63,"lo":25.35,"k":0,"z":6,"s":6},{"n":"Keetmanshoop","a":"Namibia","i":"NA","p":18039,"la":-26.574,"lo":18.13,"k":0,"z":6,"s":6},{"n":"Swakopmund","a":"Namibia","i":"NA","p":29491,"la":-22.669,"lo":14.535,"k":0,"z":6,"s":6},{"n":"Genoa","a":"Italy","i":"IT","p":647497,"la":44.41,"lo":8.93,"k":0,"z":5.1,"s":6},{"n":"Sukhumi","a":"Georgia","i":"GE","p":81546,"la":43.02,"lo":41.02,"k":0,"z":6,"s":6},{"n":"Palikir","a":"Federated States of Micronesia","i":"FM","p":4645,"la":6.917,"lo":158.15,"k":1,"z":6,"s":6},{"n":"Majuro","a":"Marshall Islands","i":"MH","p":25400,"la":7.103,"lo":171.38,"k":1,"z":6,"s":6},{"n":"Agana","a":"Guam","i":"GU","p":122411,"la":13.47,"lo":144.75,"k":0,"z":6,"s":6},{"n":"Funafuti","a":"Tuvalu","i":"TV","p":4749,"la":-8.517,"lo":179.217,"k":1,"z":6,"s":6},{"n":"Melekeok","a":"Palau","i":"PW","p":7026,"la":7.487,"lo":134.627,"k":1,"z":6,"s":6},{"n":"Bir Lehlou","a":"Western Sahara","i":"EH","p":500,"la":26.119,"lo":-9.653,"k":0,"z":6,"s":6},{"n":"Monaco","a":"Monaco","i":"MC","p":36371,"la":43.74,"lo":7.407,"k":1,"z":5.1,"s":6},{"n":"Tarawa","a":"Kiribati","i":"KI","p":28802,"la":1.338,"lo":173.018,"k":1,"z":6,"s":6},{"n":"Moroni","a":"Comoros","i":"KM","p":128698,"la":-11.704,"lo":43.24,"k":1,"z":6,"s":6},{"n":"Macau","a":"Macau S.A.R","i":"MO","p":568700,"la":22.189,"lo":113.544,"k":0,"z":6,"s":6},{"n":"Andorra","a":"Andorra","i":"AD","p":53998,"la":42.511,"lo":1.527,"k":1,"z":6,"s":6},{"n":"San Bernardino","a":"United States of America","i":"US","p":1745000,"la":34.122,"lo":-117.302,"k":0,"z":5.6,"s":4},{"n":"Bridgeport","a":"United States of America","i":"US","p":1018000,"la":41.182,"lo":-73.202,"k":0,"z":5.6,"s":4},{"n":"Rochester","a":"United States of America","i":"US","p":755000,"la":43.172,"lo":-77.622,"k":0,"z":5.1,"s":4},{"n":"Manchester","a":"United Kingdom","i":"GB","p":2230000,"la":53.475,"lo":-2.249,"k":0,"z":5,"s":4},{"n":"Gujranwala","a":"Pakistan","i":"PK","p":1513000,"la":32.162,"lo":74.183,"k":0,"z":5.6,"s":4},{"n":"Incheon","a":"South Korea","i":"KR","p":2550000,"la":37.478,"lo":126.64,"k":0,"z":5.6,"s":4},{"n":"Benin City","a":"Nigeria","i":"NG","p":1190000,"la":6.342,"lo":5.618,"k":0,"z":5.6,"s":4},{"n":"Xiamen","a":"China","i":"CN","p":2519000,"la":24.452,"lo":118.078,"k":0,"z":5.6,"s":4},{"n":"Nanchong","a":"China","i":"CN","p":2174000,"la":30.782,"lo":106.128,"k":0,"z":5.6,"s":4},{"n":"Neijiang","a":"China","i":"CN","p":1466000,"la":29.582,"lo":105.048,"k":0,"z":5.6,"s":4},{"n":"Nanyang","a":"China","i":"CN","p":1944000,"la":33.002,"lo":112.528,"k":0,"z":5.6,"s":4},{"n":"Jinxi","a":"China","i":"CN","p":2426000,"la":40.752,"lo":120.828,"k":0,"z":5.6,"s":4},{"n":"Yantai","a":"China","i":"CN","p":2116000,"la":37.532,"lo":121.398,"k":0,"z":5.6,"s":4},{"n":"Zaozhuang","a":"China","i":"CN","p":2145000,"la":34.882,"lo":117.568,"k":0,"z":5.1,"s":4},{"n":"Suzhou","a":"China","i":"CN","p":1650000,"la":31.302,"lo":120.618,"k":0,"z":5.6,"s":4},{"n":"Xuzhou","a":"China","i":"CN","p":2091000,"la":34.282,"lo":117.178,"k":0,"z":5.6,"s":4},{"n":"Wuxi","a":"China","i":"CN","p":1749000,"la":31.582,"lo":120.298,"k":0,"z":5.6,"s":4},{"n":"Jilin","a":"China","i":"CN","p":2396000,"la":43.852,"lo":126.548,"k":0,"z":5.6,"s":4},{"n":"Chandigarh","a":"India","i":"IN","p":979000,"la":30.722,"lo":76.778,"k":0,"z":5.6,"s":4},{"n":"Jammu","a":"India","i":"IN","p":791000,"la":32.714,"lo":74.845,"k":0,"z":5.6,"s":4},{"n":"Sholapur","a":"India","i":"IN","p":1057000,"la":17.672,"lo":75.898,"k":0,"z":5.6,"s":4},{"n":"Aurangabad","a":"India","i":"IN","p":1113000,"la":19.862,"lo":75.329,"k":0,"z":5.6,"s":4},{"n":"Nasik","a":"India","i":"IN","p":1473000,"la":20.002,"lo":73.778,"k":0,"z":5.6,"s":4},{"n":"Dispur","a":"India","i":"IN","p":16140,"la":26.144,"lo":91.767,"k":0,"z":5.6,"s":4},{"n":"Jullundur","a":"India","i":"IN","p":855000,"la":31.337,"lo":75.567,"k":0,"z":5.6,"s":4},{"n":"Allahabad","a":"India","i":"IN","p":1201000,"la":25.457,"lo":81.838,"k":0,"z":5.6,"s":4},{"n":"Moradabad","a":"India","i":"IN","p":787000,"la":28.844,"lo":78.755,"k":0,"z":5.6,"s":4},{"n":"Ghaziabad","a":"India","i":"IN","p":1341000,"la":28.662,"lo":77.406,"k":0,"z":5.6,"s":4},{"n":"Agra","a":"India","i":"IN","p":1592000,"la":27.172,"lo":78.013,"k":0,"z":5.6,"s":4},{"n":"Aligarh","a":"India","i":"IN","p":805000,"la":27.894,"lo":78.06,"k":0,"z":5.6,"s":4},{"n":"Meerut","a":"India","i":"IN","p":1398000,"la":29.002,"lo":77.698,"k":0,"z":5.6,"s":4},{"n":"Dhanbad","a":"India","i":"IN","p":1246000,"la":23.802,"lo":86.418,"k":0,"z":5.6,"s":4},{"n":"Gwalior","a":"India","i":"IN","p":978000,"la":26.2,"lo":78.169,"k":0,"z":5.6,"s":4},{"n":"Vadodara","a":"India","i":"IN","p":1756000,"la":22.312,"lo":73.178,"k":0,"z":5.6,"s":4},{"n":"Rajkot","a":"India","i":"IN","p":1260000,"la":22.312,"lo":70.798,"k":0,"z":5.6,"s":4},{"n":"Durazno","a":"Uruguay","i":"UY","p":34037,"la":-33.41,"lo":-56.51,"k":0,"z":5.6,"s":4},{"n":"International Falls","a":"United States of America","i":"US","p":15240,"la":48.601,"lo":-93.411,"k":0,"z":5.6,"s":4},{"n":"St. Paul","a":"United States of America","i":"US","p":734854,"la":44.944,"lo":-93.085,"k":0,"z":5.6,"s":4},{"n":"Billings","a":"United States of America","i":"US","p":104552,"la":45.788,"lo":-108.54,"k":0,"z":5.6,"s":4},{"n":"Great Falls","a":"United States of America","i":"US","p":66558,"la":47.5,"lo":-111.3,"k":0,"z":5.6,"s":4},{"n":"Missoula","a":"United States of America","i":"US","p":72856,"la":46.872,"lo":-113.993,"k":0,"z":5.6,"s":4},{"n":"Minot","a":"United States of America","i":"US","p":39439,"la":48.232,"lo":-101.296,"k":0,"z":5.6,"s":4},{"n":"Fargo","a":"United States of America","i":"US","p":162842,"la":46.877,"lo":-96.789,"k":0,"z":5.1,"s":4},{"n":"Hilo","a":"United States of America","i":"US","p":52391,"la":19.706,"lo":-155.082,"k":0,"z":5.1,"s":4},{"n":"Olympia","a":"United States of America","i":"US","p":156984,"la":47.038,"lo":-122.899,"k":0,"z":5.6,"s":4},{"n":"Spokane","a":"United States of America","i":"US","p":347705,"la":47.67,"lo":-117.42,"k":0,"z":5.1,"s":4},{"n":"Vancouver","a":"United States of America","i":"US","p":525802,"la":45.63,"lo":-122.64,"k":0,"z":5.6,"s":4},{"n":"Flagstaff","a":"United States of America","i":"US","p":63993,"la":35.198,"lo":-111.651,"k":0,"z":5.1,"s":4},{"n":"Tucson","a":"United States of America","i":"US","p":823000,"la":32.207,"lo":-110.892,"k":0,"z":5.1,"s":4},{"n":"Santa Barbara","a":"United States of America","i":"US","p":181632,"la":34.411,"lo":-119.717,"k":0,"z":5.6,"s":4},{"n":"Fresno","a":"United States of America","i":"US","p":616353,"la":36.748,"lo":-119.773,"k":0,"z":5.1,"s":4},{"n":"Eureka","a":"United States of America","i":"US","p":42398,"la":40.793,"lo":-124.15,"k":0,"z":5,"s":4},{"n":"Colorado Springs","a":"United States of America","i":"US","p":493654,"la":38.863,"lo":-104.792,"k":0,"z":5.6,"s":4},{"n":"Reno","a":"United States of America","i":"US","p":328585,"la":39.53,"lo":-119.82,"k":0,"z":5.1,"s":4},{"n":"Elko","a":"United States of America","i":"US","p":19252,"la":40.833,"lo":-115.762,"k":0,"z":5.6,"s":4},{"n":"Albuquerque","a":"United States of America","i":"US","p":898642,"la":35.105,"lo":-106.641,"k":0,"z":5.1,"s":4},{"n":"Salem","a":"United States of America","i":"US","p":229010,"la":44.928,"lo":-123.024,"k":0,"z":5.6,"s":4},{"n":"Casper","a":"United States of America","i":"US","p":60791,"la":42.847,"lo":-106.323,"k":0,"z":5.6,"s":4},{"n":"Topeka","a":"United States of America","i":"US","p":132091,"la":39.05,"lo":-95.67,"k":0,"z":5.1,"s":4},{"n":"Kansas City","a":"United States of America","i":"US","p":1469000,"la":39.109,"lo":-94.606,"k":0,"z":5.1,"s":4},{"n":"Tulsa","a":"United States of America","i":"US","p":946962,"la":36.12,"lo":-95.93,"k":0,"z":5.6,"s":4},{"n":"Sioux Falls","a":"United States of America","i":"US","p":155724,"la":43.55,"lo":-96.73,"k":0,"z":5.6,"s":4},{"n":"Shreveport","a":"United States of America","i":"US","p":248053,"la":32.5,"lo":-93.77,"k":0,"z":5.6,"s":4},{"n":"Baton Rouge","a":"United States of America","i":"US","p":422072,"la":30.458,"lo":-91.14,"k":0,"z":5.6,"s":4},{"n":"Ft. Worth","a":"United States of America","i":"US","p":1440454,"la":32.74,"lo":-97.34,"k":0,"z":5.1,"s":4},{"n":"Corpus Christi","a":"United States of America","i":"US","p":277454,"la":27.743,"lo":-97.402,"k":0,"z":5.6,"s":4},{"n":"Austin","a":"United States of America","i":"US","p":1161000,"la":30.269,"lo":-97.745,"k":0,"z":5.1,"s":4},{"n":"Amarillo","a":"United States of America","i":"US","p":181766,"la":35.23,"lo":-101.83,"k":0,"z":5.6,"s":4},{"n":"El Paso","a":"United States of America","i":"US","p":753000,"la":31.782,"lo":-106.512,"k":0,"z":5.1,"s":4},{"n":"Laredo","a":"United States of America","i":"US","p":434768,"la":27.506,"lo":-99.507,"k":0,"z":5.6,"s":4},{"n":"Mérida","a":"Venezuela","i":"VE","p":345489,"la":8.57,"lo":-71.183,"k":0,"z":5.6,"s":4},{"n":"Burlington","a":"United States of America","i":"US","p":93807,"la":44.476,"lo":-73.212,"k":0,"z":5.6,"s":4},{"n":"Montgomery","a":"United States of America","i":"US","p":198325,"la":32.362,"lo":-86.279,"k":0,"z":5.6,"s":4},{"n":"Tallahassee","a":"United States of America","i":"US","p":221222,"la":30.45,"lo":-84.28,"k":0,"z":5.6,"s":4},{"n":"Orlando","a":"United States of America","i":"US","p":1350000,"la":28.512,"lo":-81.382,"k":0,"z":5.1,"s":4},{"n":"Jacksonville","a":"United States of America","i":"US","p":988000,"la":30.332,"lo":-81.672,"k":0,"z":5.1,"s":4},{"n":"Savannah","a":"United States of America","i":"US","p":180187,"la":32.021,"lo":-81.11,"k":0,"z":5.1,"s":4},{"n":"Columbia","a":"United States of America","i":"US","p":398093,"la":33.997,"lo":-81.031,"k":0,"z":5.6,"s":4},{"n":"Indianapolis","a":"United States of America","i":"US","p":1436000,"la":39.752,"lo":-86.172,"k":0,"z":5.1,"s":4},{"n":"Wilmington","a":"United States of America","i":"US","p":161560,"la":34.226,"lo":-77.945,"k":0,"z":5.1,"s":4},{"n":"Knoxville","a":"United States of America","i":"US","p":655400,"la":35.97,"lo":-83.92,"k":0,"z":5.6,"s":4},{"n":"Richmond","a":"United States of America","i":"US","p":912000,"la":37.552,"lo":-77.452,"k":0,"z":5,"s":4},{"n":"Charleston","a":"United States of America","i":"US","p":123799,"la":38.35,"lo":-81.633,"k":0,"z":5.1,"s":4},{"n":"Baltimore","a":"United States of America","i":"US","p":2255000,"la":39.282,"lo":-76.615,"k":0,"z":5.6,"s":4},{"n":"Syracuse","a":"United States of America","i":"US","p":662577,"la":43.05,"lo":-76.15,"k":0,"z":5.6,"s":4},{"n":"Puerto Ayacucho","a":"Venezuela","i":"VE","p":52526,"la":5.664,"lo":-67.624,"k":0,"z":5.6,"s":4},{"n":"Port-of-Spain","a":"Trinidad and Tobago","i":"TT","p":294934,"la":10.652,"lo":-61.517,"k":1,"z":5.1,"s":4},{"n":"Augusta","a":"United States of America","i":"US","p":24042,"la":44.311,"lo":-69.78,"k":0,"z":5.6,"s":4},{"n":"Sault Ste. Marie","a":"United States of America","i":"US","p":83805,"la":46.495,"lo":-84.345,"k":0,"z":5.1,"s":4},{"n":"Atakpamé","a":"Togo","i":"TG","p":80683,"la":7.53,"lo":1.12,"k":0,"z":5.6,"s":4},{"n":"Sousse","a":"Tunisia","i":"TN","p":327004,"la":35.83,"lo":10.625,"k":0,"z":5.6,"s":4},{"n":"Taizz","a":"Yemen","i":"YE","p":751000,"la":13.606,"lo":44.037,"k":0,"z":5.1,"s":4},{"n":"Sitka","a":"United States of America","i":"US","p":8931,"la":57.06,"lo":-135.328,"k":0,"z":5.1,"s":4},{"n":"Lviv","a":"Ukraine","i":"UA","p":803880,"la":49.835,"lo":24.03,"k":0,"z":5.1,"s":4},{"n":"Odessa","a":"Ukraine","i":"UA","p":991000,"la":46.492,"lo":30.708,"k":0,"z":5.1,"s":4},{"n":"Zhytomyr","a":"Ukraine","i":"UA","p":282192,"la":50.246,"lo":28.662,"k":0,"z":5.6,"s":4},{"n":"Dnipro","a":"Ukraine","i":"UA","p":1050000,"la":48.482,"lo":34.998,"k":0,"z":5.1,"s":4},{"n":"Donetsk","a":"Ukraine","i":"UA","p":988000,"la":48.002,"lo":37.828,"k":0,"z":5,"s":4},{"n":"Kharkiv","a":"Ukraine","i":"UA","p":1461000,"la":50.002,"lo":36.248,"k":0,"z":5.1,"s":4},{"n":"Türkmenbaşy","a":"Turkmenistan","i":"TM","p":68292,"la":40.023,"lo":52.97,"k":0,"z":5.6,"s":4},{"n":"Bukhara","a":"Uzbekistan","i":"UZ","p":319476,"la":39.78,"lo":64.43,"k":0,"z":5.1,"s":4},{"n":"Nukus","a":"Uzbekistan","i":"UZ","p":230006,"la":42.47,"lo":59.615,"k":0,"z":5.6,"s":4},{"n":"Türkmenabat","a":"Turkmenistan","i":"TM","p":234817,"la":39.11,"lo":63.58,"k":0,"z":5.6,"s":4},{"n":"Mary","a":"Turkmenistan","i":"TM","p":178708,"la":37.6,"lo":61.833,"k":0,"z":5.1,"s":4},{"n":"Andijan","a":"Uzbekistan","i":"UZ","p":650000,"la":40.79,"lo":72.34,"k":0,"z":5.1,"s":4},{"n":"Haiphong","a":"Vietnam","i":"VN","p":1969000,"la":20.832,"lo":106.678,"k":0,"z":5.6,"s":4},{"n":"Da Nang","a":"Vietnam","i":"VN","p":1000000,"la":16.06,"lo":108.25,"k":0,"z":5.6,"s":4},{"n":"Kabwe","a":"Zambia","i":"ZM","p":188979,"la":-14.44,"lo":28.45,"k":0,"z":5.6,"s":4},{"n":"Mufulira","a":"Zambia","i":"ZM","p":153624,"la":-12.55,"lo":28.26,"k":0,"z":5.6,"s":4},{"n":"Kitwe","a":"Zambia","i":"ZM","p":404901,"la":-12.81,"lo":28.22,"k":0,"z":5.6,"s":4},{"n":"Livingstone","a":"Zambia","i":"ZM","p":165480,"la":-17.86,"lo":25.86,"k":0,"z":5.6,"s":4},{"n":"Chitungwiza","a":"Zimbabwe","i":"ZW","p":340360,"la":-18,"lo":31.1,"k":0,"z":5.6,"s":4},{"n":"Douala","a":"Cameroon","i":"CM","p":1906000,"la":4.062,"lo":9.708,"k":0,"z":5.6,"s":4},{"n":"Birmingham","a":"United Kingdom","i":"GB","p":2285000,"la":52.477,"lo":-1.922,"k":0,"z":5,"s":4},{"n":"Belfast","a":"United Kingdom","i":"GB","p":450406,"la":54.594,"lo":-5.928,"k":0,"z":5,"s":4},{"n":"İzmir","a":"Turkey","i":"TR","p":2587000,"la":38.438,"lo":27.15,"k":0,"z":5.1,"s":4},{"n":"Bursa","a":"Turkey","i":"TR","p":1492000,"la":40.202,"lo":29.068,"k":0,"z":5.1,"s":4},{"n":"Samsun","a":"Turkey","i":"TR","p":609339,"la":41.28,"lo":36.344,"k":0,"z":5.6,"s":4},{"n":"Konya","a":"Turkey","i":"TR","p":919000,"la":37.877,"lo":32.473,"k":0,"z":5.6,"s":4},{"n":"Adana","a":"Turkey","i":"TR","p":1293000,"la":36.997,"lo":35.318,"k":0,"z":5.1,"s":4},{"n":"Gulu","a":"Uganda","i":"UG","p":146858,"la":2.78,"lo":32.28,"k":0,"z":5.6,"s":4},{"n":"Kigali","a":"Rwanda","i":"RW","p":860000,"la":-1.952,"lo":30.059,"k":1,"z":5.1,"s":4},{"n":"Cottica","a":"Suriname","i":"SR","p":29210,"la":3.85,"lo":-54.233,"k":0,"z":5.6,"s":4},{"n":"Córdoba","a":"Spain","i":"ES","p":321376,"la":37.88,"lo":-4.77,"k":0,"z":5.6,"s":4},{"n":"Niamey","a":"Niger","i":"NE","p":232555,"la":13.492,"lo":7.096,"k":0,"z":5.6,"s":4},{"n":"Tahoua","a":"Niger","i":"NE","p":115956,"la":14.9,"lo":5.26,"k":0,"z":5.6,"s":4},{"n":"Constanța","a":"Romania","i":"RO","p":303399,"la":44.203,"lo":28.61,"k":0,"z":5.6,"s":4},{"n":"Luleå","a":"Sweden","i":"SE","p":48638,"la":65.597,"lo":22.158,"k":0,"z":5.1,"s":4},{"n":"Sundsvall","a":"Sweden","i":"SE","p":73389,"la":62.4,"lo":17.317,"k":0,"z":5.1,"s":4},{"n":"Iași","a":"Romania","i":"RO","p":325914,"la":47.168,"lo":27.575,"k":0,"z":5.6,"s":4},{"n":"Surat Thani","a":"Thailand","i":"TH","p":157627,"la":9.15,"lo":99.34,"k":0,"z":5.6,"s":4},{"n":"Chiang Mai","a":"Thailand","i":"TH","p":397211,"la":18.8,"lo":98.98,"k":0,"z":5.6,"s":4},{"n":"Nakhon Ratchasima","a":"Thailand","i":"TH","p":334984,"la":15,"lo":102.1,"k":0,"z":5.6,"s":4},{"n":"Mbabane","a":"eSwatini","i":"SZ","p":90138,"la":-26.317,"lo":31.133,"k":1,"z":5.6,"s":4},{"n":"Piura","a":"Peru","i":"PE","p":396932,"la":-5.21,"lo":-80.63,"k":0,"z":5.6,"s":4},{"n":"Arequipa","a":"Peru","i":"PE","p":815000,"la":-16.418,"lo":-71.532,"k":0,"z":5.6,"s":4},{"n":"Chimbote","a":"Peru","i":"PE","p":349846,"la":-9.07,"lo":-78.57,"k":0,"z":5.6,"s":4},{"n":"Pucallpa","a":"Peru","i":"PE","p":310750,"la":-8.369,"lo":-74.535,"k":0,"z":5.1,"s":4},{"n":"Iquitos","a":"Peru","i":"PE","p":458729,"la":-3.75,"lo":-73.25,"k":0,"z":5.1,"s":4},{"n":"Huancayo","a":"Peru","i":"PE","p":412733,"la":-12.08,"lo":-75.2,"k":0,"z":5.6,"s":4},{"n":"Ciudad del Este","a":"Paraguay","i":"PY","p":320872,"la":-25.517,"lo":-54.616,"k":0,"z":5.6,"s":4},{"n":"Ponta Delgada","a":"Portugal","i":"PT","p":61526,"la":37.748,"lo":-25.667,"k":0,"z":5.6,"s":4},{"n":"Vigo","a":"Spain","i":"ES","p":378952,"la":42.22,"lo":-8.73,"k":0,"z":5.6,"s":4},{"n":"Bilbao","a":"Spain","i":"ES","p":875552,"la":43.25,"lo":-2.93,"k":0,"z":5,"s":4},{"n":"Kaolack","a":"Senegal","i":"SN","p":277812,"la":14.15,"lo":-16.1,"k":0,"z":5.6,"s":4},{"n":"Kaédi","a":"Senegal","i":"SN","p":21656,"la":16.15,"lo":-13.5,"k":0,"z":5.6,"s":4},{"n":"Geneina","a":"Sudan","i":"SD","p":162981,"la":13.45,"lo":22.44,"k":0,"z":5.6,"s":4},{"n":"Medina","a":"Saudi Arabia","i":"SA","p":1010000,"la":24.502,"lo":39.578,"k":0,"z":5.1,"s":4},{"n":"Tabuk","a":"Saudi Arabia","i":"SA","p":547957,"la":28.384,"lo":36.555,"k":0,"z":5.6,"s":4},{"n":"Juba","a":"South Sudan","i":"SS","p":111975,"la":4.83,"lo":31.58,"k":0,"z":5.1,"s":4},{"n":"Malakal","a":"South Sudan","i":"SS","p":160765,"la":9.537,"lo":31.656,"k":0,"z":5.6,"s":4},{"n":"Omdurman","a":"Sudan","i":"SD","p":2395159,"la":15.617,"lo":32.48,"k":0,"z":5.6,"s":4},{"n":"Al-Ubayyid","a":"Sudan","i":"SD","p":393311,"la":13.183,"lo":30.217,"k":0,"z":5.6,"s":4},{"n":"The Hague","a":"Netherlands","i":"NL","p":1406000,"la":52.08,"lo":4.27,"k":0,"z":5.6,"s":4},{"n":"Kristiansand","a":"Norway","i":"NO","p":63814,"la":58.146,"lo":7.975,"k":0,"z":5.6,"s":4},{"n":"Ljubljana","a":"Slovenia","i":"SI","p":314807,"la":46.055,"lo":14.515,"k":1,"z":5.6,"s":4},{"n":"Bratislava","a":"Slovakia","i":"SK","p":423737,"la":48.15,"lo":17.117,"k":1,"z":5.1,"s":4},{"n":"Hammerfest","a":"Norway","i":"NO","p":10000,"la":70.661,"lo":23.688,"k":0,"z":5.1,"s":4},{"n":"Doha","a":"Qatar","i":"QA","p":1450000,"la":25.287,"lo":51.533,"k":1,"z":5.6,"s":4},{"n":"Quetta","a":"Pakistan","i":"PK","p":768000,"la":30.222,"lo":67.023,"k":0,"z":5.6,"s":4},{"n":"Larkana","a":"Pakistan","i":"PK","p":364033,"la":27.562,"lo":68.207,"k":0,"z":5.6,"s":4},{"n":"Springbok","a":"South Africa","i":"ZA","p":10438,"la":-29.667,"lo":17.883,"k":0,"z":5.6,"s":4},{"n":"Upington","a":"South Africa","i":"ZA","p":71373,"la":-28.46,"lo":21.23,"k":0,"z":5.6,"s":4},{"n":"Worcester","a":"South Africa","i":"ZA","p":127597,"la":-33.64,"lo":19.44,"k":0,"z":5.6,"s":4},{"n":"George","a":"South Africa","i":"ZA","p":174582,"la":-33.95,"lo":22.45,"k":0,"z":5.6,"s":4},{"n":"Tete","a":"Mozambique","i":"MZ","p":129316,"la":-16.17,"lo":33.58,"k":0,"z":5.6,"s":4},{"n":"Pemba","a":"Mozambique","i":"MZ","p":110643,"la":-12.983,"lo":40.532,"k":0,"z":5.6,"s":4},{"n":"Nampula","a":"Mozambique","i":"MZ","p":388526,"la":-15.136,"lo":39.293,"k":0,"z":5.6,"s":4},{"n":"Welkom","a":"South Africa","i":"ZA","p":431944,"la":-27.97,"lo":26.73,"k":0,"z":5.1,"s":4},{"n":"Xai-Xai","a":"Mozambique","i":"MZ","p":128805,"la":-25.04,"lo":33.64,"k":0,"z":5.6,"s":4},{"n":"Goroka","a":"Papua New Guinea","i":"PG","p":39699,"la":-6.083,"lo":145.385,"k":0,"z":5.6,"s":4},{"n":"Mt. Hagen","a":"Papua New Guinea","i":"PG","p":59064,"la":-5.863,"lo":144.217,"k":0,"z":5.6,"s":4},{"n":"Rabaul","a":"Papua New Guinea","i":"PG","p":8074,"la":-4.205,"lo":152.143,"k":0,"z":5.6,"s":4},{"n":"Lae","a":"Papua New Guinea","i":"PG","p":131052,"la":-6.733,"lo":146.99,"k":0,"z":5.6,"s":4},{"n":"David","a":"Panama","i":"PA","p":110037,"la":8.433,"lo":-82.433,"k":0,"z":5.6,"s":4},{"n":"Oujda","a":"Morocco","i":"MA","p":409391,"la":34.69,"lo":-1.91,"k":0,"z":5.6,"s":4},{"n":"Safi","a":"Morocco","i":"MA","p":353476,"la":32.32,"lo":-9.24,"k":0,"z":5.6,"s":4},{"n":"Podgorica","a":"Montenegro","i":"ME","p":145850,"la":42.466,"lo":19.266,"k":1,"z":5.6,"s":4},{"n":"Quelimane","a":"Mozambique","i":"MZ","p":188964,"la":-17.88,"lo":36.89,"k":0,"z":5.6,"s":4},{"n":"East London","a":"South Africa","i":"ZA","p":338627,"la":-32.97,"lo":27.87,"k":0,"z":5.6,"s":4},{"n":"Middelburg","a":"South Africa","i":"ZA","p":18164,"la":-31.5,"lo":25.01,"k":0,"z":5.1,"s":4},{"n":"Nalchik","a":"Russia","i":"RU","p":307866,"la":43.498,"lo":43.618,"k":0,"z":5.6,"s":4},{"n":"Stavropol","a":"Russia","i":"RU","p":363064,"la":45.05,"lo":41.98,"k":0,"z":5.6,"s":4},{"n":"Ugolnye Kopi","a":"Russia","i":"RU","p":3367,"la":64.733,"lo":177.7,"k":0,"z":5.6,"s":4},{"n":"Kaliningrad","a":"Russia","i":"RU","p":434954,"la":54.7,"lo":20.497,"k":0,"z":5,"s":4},{"n":"Pskov","a":"Russia","i":"RU","p":201990,"la":57.83,"lo":28.33,"k":0,"z":5.1,"s":4},{"n":"Bryansk","a":"Russia","i":"RU","p":427236,"la":53.26,"lo":34.43,"k":0,"z":5.1,"s":4},{"n":"Smolensk","a":"Russia","i":"RU","p":320991,"la":54.783,"lo":32.047,"k":0,"z":5.1,"s":4},{"n":"Petrozavodsk","a":"Russia","i":"RU","p":265025,"la":61.781,"lo":34.361,"k":0,"z":5.1,"s":4},{"n":"Tver","a":"Russia","i":"RU","p":400212,"la":56.86,"lo":35.89,"k":0,"z":5.6,"s":4},{"n":"Vologda","a":"Russia","i":"RU","p":294889,"la":59.21,"lo":39.92,"k":0,"z":5.1,"s":4},{"n":"Yaroslavl","a":"Russia","i":"RU","p":606730,"la":57.62,"lo":39.87,"k":0,"z":5.1,"s":4},{"n":"Rostov","a":"Russia","i":"RU","p":1052000,"la":47.237,"lo":39.711,"k":0,"z":5.1,"s":4},{"n":"Sochi","a":"Russia","i":"RU","p":327608,"la":43.59,"lo":39.73,"k":0,"z":5.1,"s":4},{"n":"Krasnodar","a":"Russia","i":"RU","p":649851,"la":45.02,"lo":39,"k":0,"z":5.1,"s":4},{"n":"Penza","a":"Russia","i":"RU","p":512602,"la":53.18,"lo":45,"k":0,"z":5.1,"s":4},{"n":"Ryazan","a":"Russia","i":"RU","p":520173,"la":54.62,"lo":39.72,"k":0,"z":5.6,"s":4},{"n":"Voronezh","a":"Russia","i":"RU","p":844000,"la":51.665,"lo":39.187,"k":0,"z":5.1,"s":4},{"n":"Magnitogorsk","a":"Russia","i":"RU","p":413351,"la":53.423,"lo":58.98,"k":0,"z":5.1,"s":4},{"n":"Chelyabinsk","a":"Russia","i":"RU","p":1091000,"la":55.157,"lo":61.437,"k":0,"z":5.1,"s":4},{"n":"Vorkuta","a":"Russia","i":"RU","p":80039,"la":67.5,"lo":64.01,"k":0,"z":5.6,"s":4},{"n":"Kirov","a":"Russia","i":"RU","p":457437,"la":58.59,"lo":49.67,"k":0,"z":5.1,"s":4},{"n":"Nizhny Tagil","a":"Russia","i":"RU","p":381116,"la":57.92,"lo":59.975,"k":0,"z":5.1,"s":4},{"n":"Astrakhan","a":"Russia","i":"RU","p":502533,"la":46.349,"lo":48.055,"k":0,"z":5.1,"s":4},{"n":"Orenburg","a":"Russia","i":"RU","p":550204,"la":51.78,"lo":55.11,"k":0,"z":5.1,"s":4},{"n":"Saratov","a":"Russia","i":"RU","p":843000,"la":51.582,"lo":46.028,"k":0,"z":5.1,"s":4},{"n":"Ulyanovsk","a":"Russia","i":"RU","p":640680,"la":54.33,"lo":48.41,"k":0,"z":5.6,"s":4},{"n":"Omsk","a":"Russia","i":"RU","p":1135000,"la":54.992,"lo":73.398,"k":0,"z":3.7,"s":4},{"n":"Tyumen","a":"Russia","i":"RU","p":519119,"la":57.14,"lo":65.53,"k":0,"z":5.6,"s":4},{"n":"Novokuznetsk","a":"Russia","i":"RU","p":539616,"la":53.75,"lo":87.115,"k":0,"z":5.1,"s":4},{"n":"Kemerovo","a":"Russia","i":"RU","p":477090,"la":55.34,"lo":86.09,"k":0,"z":5.1,"s":4},{"n":"Grozny","a":"Russia","i":"RU","p":226100,"la":43.319,"lo":45.699,"k":0,"z":5,"s":4},{"n":"Kandy","a":"Sri Lanka","i":"LK","p":111701,"la":7.28,"lo":80.67,"k":0,"z":5.6,"s":4},{"n":"Sri Jayawardenepura Kotte","a":"Sri Lanka","i":"LK","p":115826,"la":6.9,"lo":79.95,"k":0,"z":5.6,"s":4},{"n":"Daejeon","a":"South Korea","i":"KR","p":1468000,"la":36.337,"lo":127.423,"k":0,"z":5.1,"s":4},{"n":"Gwangju","a":"South Korea","i":"KR","p":1440000,"la":35.173,"lo":126.908,"k":0,"z":5.6,"s":4},{"n":"Busan","a":"South Korea","i":"KR","p":3480000,"la":35.097,"lo":129.008,"k":0,"z":5.6,"s":4},{"n":"Zamboanga","a":"Philippines","i":"PH","p":773000,"la":6.922,"lo":122.078,"k":0,"z":5.6,"s":4},{"n":"Laoag","a":"Philippines","i":"PH","p":207048,"la":18.199,"lo":120.594,"k":0,"z":5.6,"s":4},{"n":"Baguio","a":"Philippines","i":"PH","p":447824,"la":16.43,"lo":120.57,"k":0,"z":5.6,"s":4},{"n":"General Santos","a":"Philippines","i":"PH","p":950530,"la":6.111,"lo":125.175,"k":0,"z":5.6,"s":4},{"n":"Ust-Ilimsk","a":"Russia","i":"RU","p":100271,"la":57.99,"lo":102.633,"k":0,"z":5.6,"s":4},{"n":"Angarsk","a":"Russia","i":"RU","p":243158,"la":52.56,"lo":103.92,"k":0,"z":5.6,"s":4},{"n":"Abakan","a":"Russia","i":"RU","p":167289,"la":53.704,"lo":91.445,"k":0,"z":5.6,"s":4},{"n":"Norilsk","a":"Russia","i":"RU","p":165873,"la":69.34,"lo":88.225,"k":0,"z":5.6,"s":4},{"n":"Khatanga","a":"Russia","i":"RU","p":3205,"la":72.041,"lo":102.465,"k":0,"z":5.6,"s":4},{"n":"Kyzyl","a":"Russia","i":"RU","p":108240,"la":51.707,"lo":94.383,"k":0,"z":5.6,"s":4},{"n":"Ulan-Ude","a":"Russia","i":"RU","p":360278,"la":51.825,"lo":107.625,"k":0,"z":5.6,"s":4},{"n":"Blagoveshchensk","a":"Russia","i":"RU","p":221296,"la":50.267,"lo":127.533,"k":0,"z":5.6,"s":4},{"n":"Bukachacha","a":"Russia","i":"RU","p":3372,"la":52.983,"lo":116.917,"k":0,"z":5.6,"s":4},{"n":"Dalnegorsk","a":"Russia","i":"RU","p":8123,"la":44.537,"lo":135.517,"k":0,"z":5.6,"s":4},{"n":"Ambarchik","a":"Russia","i":"RU","p":0,"la":69.651,"lo":162.334,"k":0,"z":5.6,"s":4},{"n":"Batagay","a":"Russia","i":"RU","p":4266,"la":67.656,"lo":134.635,"k":0,"z":5.6,"s":4},{"n":"Chokurdakh","a":"Russia","i":"RU","p":2506,"la":70.618,"lo":147.895,"k":0,"z":5.6,"s":4},{"n":"Ust-Nera","a":"Russia","i":"RU","p":9148,"la":64.567,"lo":143.2,"k":0,"z":5.6,"s":4},{"n":"Lensk","a":"Russia","i":"RU","p":25589,"la":60.725,"lo":114.947,"k":0,"z":5.6,"s":4},{"n":"Aldan","a":"Russia","i":"RU","p":24426,"la":58.603,"lo":125.389,"k":0,"z":5.6,"s":4},{"n":"Mirny","a":"Russia","i":"RU","p":40308,"la":62.54,"lo":113.961,"k":0,"z":5.1,"s":4},{"n":"Zhigansk","a":"Russia","i":"RU","p":3237,"la":66.77,"lo":123.371,"k":0,"z":5.1,"s":4},{"n":"Okhotsk","a":"Russia","i":"RU","p":5570,"la":59.383,"lo":143.217,"k":0,"z":5.1,"s":4},{"n":"Khabarovsk","a":"Russia","i":"RU","p":579000,"la":48.455,"lo":135.12,"k":0,"z":5.6,"s":4},{"n":"Okha","a":"Russia","i":"RU","p":26560,"la":53.574,"lo":142.948,"k":0,"z":5.6,"s":4},{"n":"Yuzhno Sakhalinsk","a":"Russia","i":"RU","p":176484,"la":46.965,"lo":142.74,"k":0,"z":5.1,"s":4},{"n":"Mexicali","a":"Mexico","i":"MX","p":885000,"la":32.652,"lo":-115.482,"k":0,"z":5.6,"s":4},{"n":"La Paz","a":"Mexico","i":"MX","p":189767,"la":24.14,"lo":-110.32,"k":0,"z":5.6,"s":4},{"n":"Torreón","a":"Mexico","i":"MX","p":1144000,"la":25.572,"lo":-103.422,"k":0,"z":5.6,"s":4},{"n":"Culiacán","a":"Mexico","i":"MX","p":809000,"la":24.832,"lo":-107.382,"k":0,"z":5.6,"s":4},{"n":"Nogales","a":"Mexico","i":"MX","p":178097,"la":31.305,"lo":-110.945,"k":0,"z":5.6,"s":4},{"n":"Hermosillo","a":"Mexico","i":"MX","p":595811,"la":29.099,"lo":-110.954,"k":0,"z":5.6,"s":4},{"n":"Guaymas","a":"Mexico","i":"MX","p":103449,"la":27.93,"lo":-110.89,"k":0,"z":5.6,"s":4},{"n":"San Luis Potosí","a":"Mexico","i":"MX","p":992000,"la":22.172,"lo":-101.002,"k":0,"z":5.6,"s":4},{"n":"Matamoros","a":"Mexico","i":"MX","p":538785,"la":25.88,"lo":-97.5,"k":0,"z":5.6,"s":4},{"n":"Nuevo Laredo","a":"Mexico","i":"MX","p":349550,"la":27.5,"lo":-99.55,"k":0,"z":5.6,"s":4},{"n":"Colima","a":"Mexico","i":"MX","p":223287,"la":19.23,"lo":-103.72,"k":0,"z":5.6,"s":4},{"n":"Campeche","a":"Mexico","i":"MX","p":205212,"la":19.83,"lo":-90.5,"k":0,"z":5.6,"s":4},{"n":"Oaxaca","a":"Mexico","i":"MX","p":530728,"la":17.083,"lo":-96.67,"k":0,"z":5.1,"s":4},{"n":"León","a":"Mexico","i":"MX","p":1488000,"la":21.152,"lo":-101.702,"k":0,"z":5.6,"s":4},{"n":"Maiduguri","a":"Nigeria","i":"NG","p":896000,"la":11.852,"lo":13.158,"k":0,"z":5.6,"s":4},{"n":"Port Harcourt","a":"Nigeria","i":"NG","p":1020000,"la":4.812,"lo":7.008,"k":0,"z":5.6,"s":4},{"n":"Makurdi","a":"Nigeria","i":"NG","p":292645,"la":7.73,"lo":8.53,"k":0,"z":5.6,"s":4},{"n":"Ibadan","a":"Nigeria","i":"NG","p":2628000,"la":7.382,"lo":3.928,"k":0,"z":5.1,"s":4},{"n":"Ogbomosho","a":"Nigeria","i":"NG","p":951000,"la":8.132,"lo":4.238,"k":0,"z":5.6,"s":4},{"n":"Warri","a":"Nigeria","i":"NG","p":830106,"la":5.52,"lo":5.76,"k":0,"z":5.6,"s":4},{"n":"Kaduna","a":"Nigeria","i":"NG","p":1442000,"la":10.522,"lo":7.438,"k":0,"z":5.6,"s":4},{"n":"Gdańsk","a":"Poland","i":"PL","p":740000,"la":54.36,"lo":18.64,"k":0,"z":5,"s":4},{"n":"Kraków","a":"Poland","i":"PL","p":756000,"la":50.062,"lo":19.958,"k":0,"z":5.1,"s":4},{"n":"Dalandzadgad","a":"Mongolia","i":"MN","p":15093,"la":43.584,"lo":104.44,"k":0,"z":5.6,"s":4},{"n":"Wonsan","a":"North Korea","i":"KP","p":329207,"la":39.16,"lo":127.431,"k":0,"z":5.6,"s":4},{"n":"Sinuiju","a":"North Korea","i":"KP","p":288112,"la":40.086,"lo":124.421,"k":0,"z":5.6,"s":4},{"n":"Dund-Us","a":"Mongolia","i":"MN","p":30500,"la":48.017,"lo":91.633,"k":0,"z":5.6,"s":4},{"n":"Choibalsan","a":"Mongolia","i":"MN","p":33376,"la":48.067,"lo":114.506,"k":0,"z":5.6,"s":4},{"n":"Lüderitz","a":"Namibia","i":"NA","p":15137,"la":-26.648,"lo":15.159,"k":0,"z":5.6,"s":4},{"n":"Walvis Bay","a":"Namibia","i":"NA","p":52058,"la":-22.958,"lo":14.505,"k":0,"z":5.6,"s":4},{"n":"Mwanza","a":"Tanzania","i":"TZ","p":493944,"la":-2.52,"lo":32.93,"k":0,"z":5.6,"s":4},{"n":"Morogoro","a":"Tanzania","i":"TZ","p":250902,"la":-6.82,"lo":37.66,"k":0,"z":5.6,"s":4},{"n":"Dodoma","a":"Tanzania","i":"TZ","p":218269,"la":-6.183,"lo":35.75,"k":0,"z":5.6,"s":4},{"n":"Arusha","a":"Tanzania","i":"TZ","p":341136,"la":-3.36,"lo":36.67,"k":0,"z":5.6,"s":4},{"n":"Bern","a":"Switzerland","i":"CH","p":275329,"la":46.917,"lo":7.467,"k":1,"z":5.6,"s":4},{"n":"Malmö","a":"Sweden","i":"SE","p":269349,"la":55.599,"lo":13.023,"k":0,"z":5.1,"s":4},{"n":"Laayoune","a":"Morocco","i":"MA","p":188084,"la":27.15,"lo":-13.2,"k":0,"z":5.6,"s":4},{"n":"Ternate","a":"Indonesia","i":"ID","p":187521,"la":0.793,"lo":127.363,"k":0,"z":5.6,"s":4},{"n":"Ambon","a":"Indonesia","i":"ID","p":355596,"la":-3.717,"lo":128.2,"k":0,"z":5.6,"s":4},{"n":"Raba","a":"Indonesia","i":"ID","p":106101,"la":-8.45,"lo":118.767,"k":0,"z":5.6,"s":4},{"n":"Jayapura","a":"Indonesia","i":"ID","p":169341,"la":-2.533,"lo":140.7,"k":0,"z":5.6,"s":4},{"n":"Florence","a":"Italy","i":"IT","p":1500000,"la":43.78,"lo":11.25,"k":0,"z":5.6,"s":4},{"n":"Catania","a":"Italy","i":"IT","p":674542,"la":37.5,"lo":15.08,"k":0,"z":5.6,"s":4},{"n":"Pristina","a":"Kosovo","i":"-99","p":465186,"la":42.667,"lo":21.166,"k":1,"z":5.6,"s":4},{"n":"Meru","a":"Kenya","i":"KE","p":47226,"la":0.06,"lo":37.64,"k":0,"z":5.6,"s":4},{"n":"Eldoret","a":"Kenya","i":"KE","p":353381,"la":0.52,"lo":35.27,"k":0,"z":5.6,"s":4},{"n":"Banda Aceh","a":"Indonesia","i":"ID","p":513698,"la":5.55,"lo":95.32,"k":0,"z":5.6,"s":4},{"n":"George Town","a":"Malaysia","i":"MY","p":2500000,"la":5.414,"lo":100.329,"k":0,"z":5.1,"s":4},{"n":"Zhangye","a":"China","i":"CN","p":230728,"la":38.93,"lo":100.45,"k":0,"z":5.6,"s":4},{"n":"Wuwei","a":"China","i":"CN","p":493092,"la":37.928,"lo":102.641,"k":0,"z":5.6,"s":4},{"n":"Dunhuang","a":"China","i":"CN","p":140094,"la":40.143,"lo":94.662,"k":0,"z":5,"s":4},{"n":"Tianshui","a":"China","i":"CN","p":1225000,"la":34.602,"lo":105.918,"k":0,"z":5.6,"s":4},{"n":"Dulan","a":"China","i":"CN","p":100,"la":36.167,"lo":98.267,"k":0,"z":5.6,"s":4},{"n":"Golmud","a":"China","i":"CN","p":107092,"la":36.417,"lo":94.883,"k":0,"z":5,"s":4},{"n":"Yulin","a":"China","i":"CN","p":1127000,"la":22.632,"lo":110.148,"k":0,"z":5.6,"s":4},{"n":"Bose","a":"China","i":"CN","p":175282,"la":23.9,"lo":106.613,"k":0,"z":5.6,"s":4},{"n":"Wuzhou","a":"China","i":"CN","p":442315,"la":23.48,"lo":111.32,"k":0,"z":5.6,"s":4},{"n":"Liupanshui","a":"China","i":"CN","p":1221000,"la":26.596,"lo":104.831,"k":0,"z":5.6,"s":4},{"n":"Quanzhou","a":"China","i":"CN","p":1463000,"la":24.902,"lo":118.578,"k":0,"z":5.6,"s":4},{"n":"Hefei","a":"China","i":"CN","p":2035000,"la":31.852,"lo":117.278,"k":0,"z":5.6,"s":4},{"n":"Suzhou","a":"China","i":"CN","p":1964000,"la":33.638,"lo":116.977,"k":0,"z":5.6,"s":4},{"n":"Zhanjiang","a":"China","i":"CN","p":1590000,"la":21.202,"lo":110.378,"k":0,"z":5.6,"s":4},{"n":"Shaoguan","a":"China","i":"CN","p":720266,"la":24.8,"lo":113.58,"k":0,"z":5.1,"s":4},{"n":"Balikpapan","a":"Indonesia","i":"ID","p":445905,"la":-1.25,"lo":116.83,"k":0,"z":5.6,"s":4},{"n":"Kuching","a":"Malaysia","i":"MY","p":570407,"la":1.53,"lo":110.33,"k":0,"z":5.6,"s":4},{"n":"Antsiranana","a":"Madagascar","i":"MG","p":82937,"la":-12.277,"lo":49.312,"k":0,"z":5.6,"s":4},{"n":"Fianarantsoa","a":"Madagascar","i":"MG","p":184184,"la":-21.433,"lo":47.083,"k":0,"z":5.6,"s":4},{"n":"Mahajanga","a":"Madagascar","i":"MG","p":154657,"la":-15.67,"lo":46.345,"k":0,"z":5.6,"s":4},{"n":"Toliara","a":"Madagascar","i":"MG","p":115319,"la":-23.357,"lo":43.69,"k":0,"z":5.1,"s":4},{"n":"Surakarta","a":"Indonesia","i":"ID","p":555308,"la":-7.565,"lo":110.825,"k":0,"z":5.6,"s":4},{"n":"Bandar Lampung","a":"Indonesia","i":"ID","p":865000,"la":-5.428,"lo":105.268,"k":0,"z":5.6,"s":4},{"n":"Tanjung Pandan","a":"Indonesia","i":"ID","p":62374,"la":-2.75,"lo":107.65,"k":0,"z":5.6,"s":4},{"n":"Malang","a":"Indonesia","i":"ID","p":805000,"la":-7.978,"lo":112.608,"k":0,"z":5.6,"s":4},{"n":"Kupang","a":"Indonesia","i":"ID","p":282396,"la":-10.179,"lo":123.583,"k":0,"z":5.6,"s":4},{"n":"Parepare","a":"Indonesia","i":"ID","p":87776,"la":-4.017,"lo":119.633,"k":0,"z":5.6,"s":4},{"n":"Cuenca","a":"Ecuador","i":"EC","p":286878,"la":-2.9,"lo":-79,"k":0,"z":5.6,"s":4},{"n":"Santa Cruz","a":"Ecuador","i":"EC","p":11262,"la":-0.533,"lo":-90.35,"k":0,"z":5.6,"s":4},{"n":"Limón","a":"Costa Rica","i":"CR","p":85001,"la":10,"lo":-83.033,"k":0,"z":5.6,"s":4},{"n":"Santiago de Cuba","a":"Cuba","i":"CU","p":555865,"la":20.025,"lo":-75.821,"k":0,"z":5.1,"s":4},{"n":"Santiago","a":"Dominican Republic","i":"DO","p":1550753,"la":19.5,"lo":-70.67,"k":0,"z":5.6,"s":4},{"n":"Manizales","a":"Colombia","i":"CO","p":375848,"la":5.06,"lo":-75.52,"k":0,"z":5.6,"s":4},{"n":"Pasto","a":"Colombia","i":"CO","p":382236,"la":1.214,"lo":-77.281,"k":0,"z":5.6,"s":4},{"n":"Barranquilla","a":"Colombia","i":"CO","p":1798000,"la":10.962,"lo":-74.802,"k":0,"z":5.6,"s":4},{"n":"Roseau","a":"Dominica","i":"DM","p":23336,"la":15.301,"lo":-61.387,"k":1,"z":5.1,"s":4},{"n":"Mbandaka","a":"Congo (Kinshasa)","i":"CD","p":274996,"la":0.04,"lo":18.26,"k":0,"z":5.6,"s":4},{"n":"Moundou","a":"Chad","i":"TD","p":156705,"la":8.55,"lo":16.09,"k":0,"z":5.6,"s":4},{"n":"Suez","a":"Egypt","i":"EG","p":508335,"la":30.005,"lo":32.55,"k":0,"z":5.6,"s":4},{"n":"Bur Said","a":"Egypt","i":"EG","p":623864,"la":31.26,"lo":32.29,"k":0,"z":5.6,"s":4},{"n":"El Faiyum","a":"Egypt","i":"EG","p":316772,"la":29.31,"lo":30.84,"k":0,"z":5.6,"s":4},{"n":"Aswan","a":"Egypt","i":"EG","p":313442,"la":24.088,"lo":32.899,"k":0,"z":5.6,"s":4},{"n":"Asyut","a":"Egypt","i":"EG","p":420585,"la":27.19,"lo":31.18,"k":0,"z":5.6,"s":4},{"n":"Kisangani","a":"Congo (Kinshasa)","i":"CD","p":578470,"la":0.52,"lo":25.22,"k":0,"z":5.6,"s":4},{"n":"Assab","a":"Eritrea","i":"ER","p":105496,"la":13.01,"lo":42.73,"k":0,"z":5.6,"s":4},{"n":"Djibouti","a":"Djibouti","i":"DJ","p":923000,"la":11.595,"lo":43.148,"k":1,"z":5.6,"s":4},{"n":"Dresden","a":"Germany","i":"DE","p":617515,"la":51.05,"lo":13.75,"k":0,"z":5,"s":4},{"n":"Xigaze","a":"China","i":"CN","p":80000,"la":29.25,"lo":88.883,"k":0,"z":5,"s":4},{"n":"Shache","a":"China","i":"CN","p":851374,"la":38.426,"lo":77.25,"k":0,"z":5.6,"s":4},{"n":"Yining","a":"China","i":"CN","p":542507,"la":43.9,"lo":81.35,"k":0,"z":5.1,"s":4},{"n":"Altay","a":"China","i":"CN","p":142000,"la":47.867,"lo":88.117,"k":0,"z":5.6,"s":4},{"n":"Putrajaya","a":"Malaysia","i":"MY","p":67964,"la":2.933,"lo":101.695,"k":0,"z":5.6,"s":4},{"n":"Shizuishan","a":"China","i":"CN","p":136570,"la":39.233,"lo":106.769,"k":0,"z":5.6,"s":4},{"n":"Yulin","a":"China","i":"CN","p":155960,"la":38.283,"lo":109.733,"k":0,"z":5.6,"s":4},{"n":"Ankang","a":"China","i":"CN","p":1100000,"la":32.68,"lo":109.02,"k":0,"z":5.6,"s":4},{"n":"Houma","a":"China","i":"CN","p":102400,"la":35.62,"lo":111.21,"k":0,"z":5.1,"s":4},{"n":"Yueyang","a":"China","i":"CN","p":826000,"la":29.382,"lo":113.098,"k":0,"z":5.6,"s":4},{"n":"Hengyang","a":"China","i":"CN","p":1016000,"la":26.882,"lo":112.588,"k":0,"z":5.6,"s":4},{"n":"Mianyang","a":"China","i":"CN","p":1396000,"la":31.465,"lo":104.69,"k":0,"z":5.6,"s":4},{"n":"Xichang","a":"China","i":"CN","p":379993,"la":27.88,"lo":102.3,"k":0,"z":5,"s":4},{"n":"Baoshan","a":"China","i":"CN","p":1000000,"la":25.12,"lo":99.15,"k":0,"z":5.6,"s":4},{"n":"Gejiu","a":"China","i":"CN","p":149105,"la":23.38,"lo":103.15,"k":0,"z":5.1,"s":4},{"n":"Shijiazhuang","a":"China","i":"CN","p":2417000,"la":38.052,"lo":114.478,"k":0,"z":5.6,"s":4},{"n":"Handan","a":"China","i":"CN","p":1631000,"la":36.582,"lo":114.478,"k":0,"z":5.1,"s":4},{"n":"Anshan","a":"China","i":"CN","p":1639000,"la":41.117,"lo":122.938,"k":0,"z":5.6,"s":4},{"n":"Dalian","a":"China","i":"CN","p":3167000,"la":38.925,"lo":121.628,"k":0,"z":5.1,"s":4},{"n":"Qingdao","a":"China","i":"CN","p":2866000,"la":36.092,"lo":120.328,"k":0,"z":5,"s":4},{"n":"Linyi","a":"China","i":"CN","p":2082000,"la":35.082,"lo":118.328,"k":0,"z":5.6,"s":4},{"n":"Huaiyin","a":"China","i":"CN","p":1264000,"la":33.585,"lo":119.066,"k":0,"z":5.6,"s":4},{"n":"Wenzhou","a":"China","i":"CN","p":2350000,"la":28.022,"lo":120.648,"k":0,"z":5.1,"s":4},{"n":"Ningbo","a":"China","i":"CN","p":1923000,"la":29.882,"lo":121.548,"k":0,"z":5.1,"s":4},{"n":"Fukuoka","a":"Japan","i":"JP","p":2792000,"la":33.597,"lo":130.408,"k":0,"z":5.6,"s":4},{"n":"Miyazaki","a":"Japan","i":"JP","p":324384,"la":31.918,"lo":131.418,"k":0,"z":5.6,"s":4},{"n":"Naha","a":"Japan","i":"JP","p":905238,"la":26.207,"lo":127.673,"k":0,"z":5.1,"s":4},{"n":"Kōchi","a":"Japan","i":"JP","p":335570,"la":33.562,"lo":133.538,"k":0,"z":5.6,"s":4},{"n":"Gorontalo","a":"Indonesia","i":"ID","p":365497,"la":0.55,"lo":123.07,"k":0,"z":5.6,"s":4},{"n":"Tongliao","a":"China","i":"CN","p":884000,"la":43.622,"lo":122.268,"k":0,"z":5.6,"s":4},{"n":"Hohhot","a":"China","i":"CN","p":1726000,"la":40.822,"lo":111.658,"k":0,"z":5.1,"s":4},{"n":"Chifeng","a":"China","i":"CN","p":1277000,"la":42.272,"lo":118.948,"k":0,"z":5.6,"s":4},{"n":"Ulan Hot","a":"China","i":"CN","p":241894,"la":46.08,"lo":122.08,"k":0,"z":5.6,"s":4},{"n":"Hailar","a":"China","i":"CN","p":231171,"la":49.2,"lo":119.7,"k":0,"z":5.6,"s":4},{"n":"Jiamusi","a":"China","i":"CN","p":1020000,"la":46.832,"lo":130.348,"k":0,"z":5.1,"s":4},{"n":"Bei'an","a":"China","i":"CN","p":154936,"la":48.246,"lo":126.529,"k":0,"z":5.6,"s":4},{"n":"Daqing","a":"China","i":"CN","p":1693000,"la":46.582,"lo":124.998,"k":0,"z":5.1,"s":4},{"n":"Jixi","a":"China","i":"CN","p":965000,"la":45.302,"lo":130.968,"k":0,"z":5.6,"s":4},{"n":"Nagoya","a":"Japan","i":"JP","p":3230000,"la":35.157,"lo":136.913,"k":0,"z":5.1,"s":4},{"n":"Nagano","a":"Japan","i":"JP","p":594311,"la":36.65,"lo":138.17,"k":0,"z":5.6,"s":4},{"n":"Kushiro","a":"Japan","i":"JP","p":198566,"la":42.975,"lo":144.375,"k":0,"z":5.1,"s":4},{"n":"Hakodate","a":"Japan","i":"JP","p":302984,"la":41.795,"lo":140.74,"k":0,"z":5.6,"s":4},{"n":"Kyoto","a":"Japan","i":"JP","p":1805000,"la":35.032,"lo":135.748,"k":0,"z":5.1,"s":4},{"n":"Sendai","a":"Japan","i":"JP","p":2250000,"la":38.268,"lo":140.87,"k":0,"z":5.1,"s":4},{"n":"Sakata","a":"Japan","i":"JP","p":100446,"la":38.92,"lo":139.85,"k":0,"z":5.6,"s":4},{"n":"Bandundu","a":"Congo (Kinshasa)","i":"CD","p":118211,"la":-3.31,"lo":17.38,"k":0,"z":5.6,"s":4},{"n":"Kananga","a":"Congo (Kinshasa)","i":"CD","p":765000,"la":-5.888,"lo":22.398,"k":0,"z":5.6,"s":4},{"n":"Kasongo","a":"Congo (Kinshasa)","i":"CD","p":63000,"la":-4.45,"lo":26.66,"k":0,"z":5.6,"s":4},{"n":"Mbuji-Mayi","a":"Congo (Kinshasa)","i":"CD","p":1295000,"la":-6.148,"lo":23.598,"k":0,"z":5.6,"s":4},{"n":"Kalemie","a":"Congo (Kinshasa)","i":"CD","p":206257,"la":-5.933,"lo":29.2,"k":0,"z":5.6,"s":4},{"n":"Butembo","a":"Congo (Kinshasa)","i":"CD","p":286403,"la":0.13,"lo":29.28,"k":0,"z":5.6,"s":4},{"n":"Goma","a":"Congo (Kinshasa)","i":"CD","p":144124,"la":-1.679,"lo":29.222,"k":0,"z":5.1,"s":4},{"n":"Mzuzu","a":"Malawi","i":"MW","p":110201,"la":-11.46,"lo":34.02,"k":0,"z":5.6,"s":4},{"n":"Blantyre","a":"Malawi","i":"MW","p":584877,"la":-15.79,"lo":34.99,"k":0,"z":5.6,"s":4},{"n":"Quetzaltenango","a":"Guatemala","i":"GT","p":575263,"la":14.83,"lo":-91.52,"k":0,"z":5.6,"s":4},{"n":"Banjul","a":"The Gambia","i":"GM","p":43094,"la":13.454,"lo":-16.592,"k":1,"z":5.6,"s":4},{"n":"Faridabad","a":"India","i":"IN","p":1394000,"la":28.435,"lo":77.315,"k":0,"z":5.6,"s":4},{"n":"Srinagar","a":"India","i":"IN","p":1140000,"la":34.069,"lo":74.828,"k":0,"z":5.6,"s":4},{"n":"Vijayawada","a":"India","i":"IN","p":1137000,"la":16.522,"lo":80.628,"k":0,"z":5.6,"s":4},{"n":"Thiruvananthapuram","a":"India","i":"IN","p":954000,"la":8.516,"lo":76.925,"k":0,"z":5.6,"s":4},{"n":"Kochi","a":"India","i":"IN","p":1519000,"la":10.017,"lo":76.222,"k":0,"z":5.6,"s":4},{"n":"Cuttack","a":"India","i":"IN","p":580000,"la":20.47,"lo":85.89,"k":0,"z":5.6,"s":4},{"n":"Hubballi","a":"India","i":"IN","p":890000,"la":15.362,"lo":75.123,"k":0,"z":5.6,"s":4},{"n":"Mangaluru","a":"India","i":"IN","p":776632,"la":12.9,"lo":74.85,"k":0,"z":5.6,"s":4},{"n":"Mysuru","a":"India","i":"IN","p":887000,"la":12.312,"lo":76.658,"k":0,"z":5.6,"s":4},{"n":"Kalaburagi","a":"India","i":"IN","p":483615,"la":17.35,"lo":76.82,"k":0,"z":5.6,"s":4},{"n":"Kolhapur","a":"India","i":"IN","p":750000,"la":16.7,"lo":74.22,"k":0,"z":5.1,"s":4},{"n":"Nanded","a":"India","i":"IN","p":623708,"la":19.197,"lo":77.308,"k":0,"z":5.6,"s":4},{"n":"Akola","a":"India","i":"IN","p":503502,"la":20.71,"lo":77.01,"k":0,"z":5.6,"s":4},{"n":"Guwahati","a":"India","i":"IN","p":983000,"la":26.162,"lo":91.768,"k":0,"z":5.6,"s":4},{"n":"Kayes","a":"Congo (Brazzaville)","i":"CG","p":62521,"la":-4.18,"lo":13.28,"k":0,"z":5.6,"s":4},{"n":"Franceville","a":"Gabon","i":"GA","p":42967,"la":-1.633,"lo":13.583,"k":0,"z":5.6,"s":4},{"n":"Bordeaux","a":"France","i":"FR","p":803000,"la":44.852,"lo":-0.597,"k":0,"z":5,"s":4},{"n":"Marseille","a":"France","i":"FR","p":1400000,"la":43.292,"lo":5.373,"k":0,"z":5,"s":4},{"n":"Le Havre","a":"France","i":"FR","p":242124,"la":49.505,"lo":0.105,"k":0,"z":5,"s":4},{"n":"Gao","a":"Mali","i":"ML","p":116967,"la":16.267,"lo":-0.05,"k":0,"z":5.1,"s":4},{"n":"Coihaique","a":"Chile","i":"CL","p":45787,"la":-45.57,"lo":-72.07,"k":0,"z":5.6,"s":4},{"n":"Arica","a":"Chile","i":"CL","p":185999,"la":-18.5,"lo":-70.29,"k":0,"z":5.6,"s":4},{"n":"Copiapó","a":"Chile","i":"CL","p":129280,"la":-27.372,"lo":-70.33,"k":0,"z":5.6,"s":4},{"n":"La Serena","a":"Chile","i":"CL","p":154521,"la":-29.9,"lo":-71.25,"k":0,"z":5.6,"s":4},{"n":"Los Angeles","a":"Chile","i":"CL","p":145239,"la":-37.48,"lo":-72.347,"k":0,"z":5.6,"s":4},{"n":"Narsarsuaq","a":"Greenland","i":"GL","p":145,"la":61.167,"lo":-45.417,"k":0,"z":5.1,"s":4},{"n":"Sisimiut","a":"Greenland","i":"GL","p":5227,"la":66.95,"lo":-53.667,"k":0,"z":5.1,"s":4},{"n":"Upernavik","a":"Greenland","i":"GL","p":1129,"la":72.709,"lo":-56.142,"k":0,"z":5.1,"s":4},{"n":"Qaanaaq","a":"Greenland","i":"GL","p":616,"la":77.467,"lo":-69.23,"k":0,"z":5.6,"s":4},{"n":"Nouadhibou","a":"Mauritania","i":"MR","p":86738,"la":20.9,"lo":-17.056,"k":0,"z":5.6,"s":4},{"n":"Kayes","a":"Mali","i":"ML","p":77207,"la":14.45,"lo":-11.44,"k":0,"z":5.6,"s":4},{"n":"Ayoun el Atrous","a":"Mauritania","i":"MR","p":1423,"la":16.667,"lo":-9.617,"k":0,"z":5.6,"s":4},{"n":"Ségou","a":"Mali","i":"ML","p":122952,"la":13.44,"lo":-6.26,"k":0,"z":5.6,"s":4},{"n":"Skopje","a":"North Macedonia","i":"MK","p":494087,"la":42,"lo":21.433,"k":1,"z":5.6,"s":4},{"n":"Al Jawf","a":"Libya","i":"LY","p":24132,"la":24.2,"lo":23.29,"k":0,"z":5.6,"s":4},{"n":"Tmassa","a":"Libya","i":"LY","p":500,"la":26.367,"lo":15.8,"k":0,"z":5.6,"s":4},{"n":"Misrata","a":"Libya","i":"LY","p":386120,"la":32.38,"lo":15.1,"k":0,"z":5.6,"s":4},{"n":"Zuwara","a":"Libya","i":"LY","p":180310,"la":32.934,"lo":12.079,"k":0,"z":5.6,"s":4},{"n":"Kirkuk","a":"Iraq","i":"IQ","p":601433,"la":35.472,"lo":44.392,"k":0,"z":5.1,"s":4},{"n":"Mosul","a":"Iraq","i":"IQ","p":1316000,"la":36.347,"lo":43.143,"k":0,"z":5.6,"s":4},{"n":"Najaf","a":"Iraq","i":"IQ","p":665552,"la":32,"lo":44.335,"k":0,"z":5.6,"s":4},{"n":"Bahir Dar","a":"Ethiopia","i":"ET","p":206748,"la":11.6,"lo":37.383,"k":0,"z":5.6,"s":4},{"n":"Mekele","a":"Ethiopia","i":"ET","p":95856,"la":13.5,"lo":39.47,"k":0,"z":5.6,"s":4},{"n":"Dire Dawa","a":"Ethiopia","i":"ET","p":252279,"la":9.59,"lo":41.86,"k":0,"z":5.6,"s":4},{"n":"Rovaniemi","a":"Finland","i":"FI","p":34781,"la":66.5,"lo":25.716,"k":0,"z":5.1,"s":4},{"n":"Vaasa","a":"Finland","i":"FI","p":57014,"la":63.1,"lo":21.6,"k":0,"z":5.1,"s":4},{"n":"Tampere","a":"Finland","i":"FI","p":259279,"la":61.5,"lo":23.75,"k":0,"z":5.1,"s":4},{"n":"Aqtobe","a":"Kazakhstan","i":"KZ","p":262457,"la":50.28,"lo":57.17,"k":0,"z":5.6,"s":4},{"n":"Rudniy","a":"Kazakhstan","i":"KZ","p":124000,"la":52.953,"lo":63.13,"k":0,"z":5.6,"s":4},{"n":"Qyzylorda","a":"Kazakhstan","i":"KZ","p":300000,"la":44.8,"lo":65.465,"k":0,"z":5.6,"s":4},{"n":"Atyrau","a":"Kazakhstan","i":"KZ","p":182753,"la":47.113,"lo":51.92,"k":0,"z":5,"s":4},{"n":"Ekibastuz","a":"Kazakhstan","i":"KZ","p":127868,"la":51.73,"lo":75.32,"k":0,"z":5.6,"s":4},{"n":"Pavlodar","a":"Kazakhstan","i":"KZ","p":329002,"la":52.3,"lo":76.95,"k":0,"z":5.6,"s":4},{"n":"Semey","a":"Kazakhstan","i":"KZ","p":311353,"la":50.435,"lo":80.275,"k":0,"z":5.6,"s":4},{"n":"Oskemen","a":"Kazakhstan","i":"KZ","p":319067,"la":49.99,"lo":82.615,"k":0,"z":5.6,"s":4},{"n":"Yazd","a":"Iran","i":"IR","p":477905,"la":31.92,"lo":54.37,"k":0,"z":5.6,"s":4},{"n":"Ahvaz","a":"Iran","i":"IR","p":996000,"la":31.282,"lo":48.718,"k":0,"z":5.1,"s":4},{"n":"Basra","a":"Iraq","i":"IQ","p":870000,"la":30.515,"lo":47.812,"k":0,"z":5.1,"s":4},{"n":"Bandar-e-Abbas","a":"Iran","i":"IR","p":461499,"la":27.204,"lo":56.272,"k":0,"z":5.6,"s":4},{"n":"Hamadan","a":"Iran","i":"IR","p":528256,"la":34.796,"lo":48.515,"k":0,"z":5.6,"s":4},{"n":"Tabriz","a":"Iran","i":"IR","p":1413000,"la":38.088,"lo":46.299,"k":0,"z":5.1,"s":4},{"n":"Ludhiana","a":"India","i":"IN","p":1649000,"la":30.93,"lo":75.87,"k":0,"z":5.6,"s":4},{"n":"Kota","a":"India","i":"IN","p":827000,"la":25.182,"lo":75.833,"k":0,"z":5.6,"s":4},{"n":"Jodhpur","a":"India","i":"IN","p":995000,"la":26.294,"lo":73.015,"k":0,"z":5.6,"s":4},{"n":"Shymkent","a":"Kazakhstan","i":"KZ","p":465392,"la":42.32,"lo":69.595,"k":0,"z":5.6,"s":4},{"n":"Taraz","a":"Kazakhstan","i":"KZ","p":358153,"la":42.9,"lo":71.365,"k":0,"z":5.6,"s":4},{"n":"Lucknow","a":"India","i":"IN","p":2695000,"la":26.857,"lo":80.913,"k":0,"z":5.6,"s":4},{"n":"Saharanpur","a":"India","i":"IN","p":484873,"la":29.97,"lo":77.55,"k":0,"z":5.6,"s":4},{"n":"Ranchi","a":"India","i":"IN","p":1044000,"la":23.372,"lo":85.328,"k":0,"z":5.6,"s":4},{"n":"Bhagalpur","a":"India","i":"IN","p":361548,"la":25.23,"lo":86.98,"k":0,"z":5.6,"s":4},{"n":"Raipur","a":"India","i":"IN","p":875000,"la":21.237,"lo":81.633,"k":0,"z":5.6,"s":4},{"n":"Jabalpur","a":"India","i":"IN","p":1285000,"la":23.177,"lo":79.953,"k":0,"z":5.6,"s":4},{"n":"Indore","a":"India","i":"IN","p":2026000,"la":22.717,"lo":75.863,"k":0,"z":5.6,"s":4},{"n":"Puducherry","a":"India","i":"IN","p":227411,"la":11.935,"lo":79.83,"k":0,"z":5.1,"s":4},{"n":"Salem","a":"India","i":"IN","p":873000,"la":11.672,"lo":78.178,"k":0,"z":5.6,"s":4},{"n":"Tiruchirappalli","a":"India","i":"IN","p":951000,"la":10.801,"lo":78.688,"k":0,"z":5.6,"s":4},{"n":"Pointe-Noire","a":"Congo (Brazzaville)","i":"CG","p":659084,"la":-4.77,"lo":11.88,"k":0,"z":5.6,"s":4},{"n":"Kankan","a":"Guinea","i":"GN","p":114009,"la":10.39,"lo":-9.31,"k":0,"z":5.1,"s":4},{"n":"Nzérékoré","a":"Guinea","i":"GN","p":168121,"la":7.76,"lo":-8.83,"k":0,"z":5.1,"s":4},{"n":"Bouaké","a":"Ivory Coast","i":"CI","p":567481,"la":7.69,"lo":-5.03,"k":0,"z":5.6,"s":4},{"n":"St.-Denis","a":"France","i":"RE","p":190047,"la":-20.879,"lo":55.448,"k":0,"z":5.6,"s":4},{"n":"Rio Branco","a":"Brazil","i":"BR","p":257642,"la":-9.967,"lo":-67.8,"k":0,"z":5.6,"s":4},{"n":"São Luís","a":"Brazil","i":"BR","p":1038000,"la":-2.514,"lo":-44.268,"k":0,"z":5.6,"s":4},{"n":"Porto Velho","a":"Brazil","i":"BR","p":306180,"la":-8.75,"lo":-63.9,"k":0,"z":5.1,"s":4},{"n":"Alvorada","a":"Brazil","i":"BR","p":10232,"la":-12.47,"lo":-49.082,"k":0,"z":5.6,"s":4},{"n":"Corumbá","a":"Brazil","i":"BR","p":96520,"la":-19.016,"lo":-57.65,"k":0,"z":5.6,"s":4},{"n":"Belo Horizonte","a":"Brazil","i":"BR","p":5575000,"la":-19.913,"lo":-43.917,"k":0,"z":5.1,"s":4},{"n":"Montes Claros","a":"Brazil","i":"BR","p":332379,"la":-16.72,"lo":-43.86,"k":0,"z":5.6,"s":4},{"n":"Uberlândia","a":"Brazil","i":"BR","p":563536,"la":-18.9,"lo":-48.28,"k":0,"z":5.6,"s":4},{"n":"Colíder","a":"Brazil","i":"BR","p":27139,"la":-10.817,"lo":-55.451,"k":0,"z":5.6,"s":4},{"n":"Alta Floresta","a":"Brazil","i":"BR","p":40466,"la":-9.9,"lo":-55.91,"k":0,"z":5.6,"s":4},{"n":"Cuiabá","a":"Brazil","i":"BR","p":806000,"la":-15.568,"lo":-56.087,"k":0,"z":5.1,"s":4},{"n":"Pelotas","a":"Brazil","i":"BR","p":320674,"la":-31.75,"lo":-52.33,"k":0,"z":5.6,"s":4},{"n":"Caxias do Sul","a":"Brazil","i":"BR","p":381270,"la":-29.18,"lo":-51.17,"k":0,"z":5.6,"s":4},{"n":"Ponta Grossa","a":"Brazil","i":"BR","p":292177,"la":-25.09,"lo":-50.16,"k":0,"z":5.6,"s":4},{"n":"Teresina","a":"Brazil","i":"BR","p":907000,"la":-5.093,"lo":-42.782,"k":0,"z":5.6,"s":4},{"n":"Maceió","a":"Brazil","i":"BR","p":1186000,"la":-9.618,"lo":-35.732,"k":0,"z":5.6,"s":4},{"n":"Vitória da Conquista","a":"Brazil","i":"BR","p":308204,"la":-14.85,"lo":-40.84,"k":0,"z":5.6,"s":4},{"n":"Barreiras","a":"Brazil","i":"BR","p":158292,"la":-12.14,"lo":-45,"k":0,"z":5.6,"s":4},{"n":"Vila Velha","a":"Brazil","i":"BR","p":1209091,"la":-20.338,"lo":-40.29,"k":0,"z":5.6,"s":4},{"n":"Natal","a":"Brazil","i":"BR","p":1088000,"la":-5.778,"lo":-35.242,"k":0,"z":5.6,"s":4},{"n":"Thompson","a":"Canada","i":"CA","p":13727,"la":55.75,"lo":-97.867,"k":0,"z":5.6,"s":4},{"n":"Brandon","a":"Canada","i":"CA","p":28418,"la":49.833,"lo":-99.95,"k":0,"z":5.6,"s":4},{"n":"Fort Smith","a":"Canada","i":"CA","p":518,"la":60,"lo":-111.883,"k":0,"z":5.1,"s":4},{"n":"Fort McMurray","a":"Canada","i":"CA","p":21863,"la":56.733,"lo":-111.383,"k":0,"z":5.6,"s":4},{"n":"Peace River","a":"Canada","i":"CA","p":5340,"la":56.233,"lo":-117.283,"k":0,"z":5.6,"s":4},{"n":"Fort St. John","a":"Canada","i":"CA","p":18776,"la":56.25,"lo":-120.833,"k":0,"z":5.1,"s":4},{"n":"Iqaluit","a":"Canada","i":"CA","p":6124,"la":63.75,"lo":-68.5,"k":0,"z":5.1,"s":4},{"n":"Cambridge Bay","a":"Canada","i":"CA","p":1477,"la":69.117,"lo":-105.033,"k":0,"z":5.6,"s":4},{"n":"Kugluktuk","a":"Canada","i":"CA","p":1302,"la":67.799,"lo":-115.125,"k":0,"z":5.6,"s":4},{"n":"Chesterfield Inlet","a":"Canada","i":"CA","p":374,"la":63.338,"lo":-90.7,"k":0,"z":5.6,"s":4},{"n":"Arviat","a":"Canada","i":"CA","p":1868,"la":61.109,"lo":-94.059,"k":0,"z":5.6,"s":4},{"n":"Taloyoak","a":"Canada","i":"CA","p":774,"la":69.533,"lo":-93.533,"k":0,"z":5.6,"s":4},{"n":"Igloolik","a":"Canada","i":"CA","p":1612,"la":69.257,"lo":-81.794,"k":0,"z":5.6,"s":4},{"n":"Dawson City","a":"Canada","i":"CA","p":1319,"la":64.067,"lo":-139.417,"k":0,"z":5.1,"s":4},{"n":"Timmins","a":"Canada","i":"CA","p":34974,"la":48.467,"lo":-81.333,"k":0,"z":5.6,"s":4},{"n":"North Bay","a":"Canada","i":"CA","p":50170,"la":46.3,"lo":-79.45,"k":0,"z":5.6,"s":4},{"n":"Kuujjuarapik","a":"Canada","i":"CA","p":1243,"la":55.281,"lo":-77.766,"k":0,"z":5.6,"s":4},{"n":"Kuujjuaq","a":"Canada","i":"CA","p":1273,"la":58.1,"lo":-68.4,"k":0,"z":5.6,"s":4},{"n":"Sydney","a":"Canada","i":"CA","p":37538,"la":46.066,"lo":-60.18,"k":0,"z":5.6,"s":4},{"n":"Labrador City","a":"Canada","i":"CA","p":10313,"la":52.941,"lo":-66.916,"k":0,"z":5.6,"s":4},{"n":"Ebolowa","a":"Cameroon","i":"CM","p":87875,"la":2.9,"lo":11.15,"k":0,"z":5.6,"s":4},{"n":"Bambari","a":"Central African Republic","i":"CF","p":62098,"la":5.762,"lo":20.667,"k":0,"z":5.6,"s":4},{"n":"Venice","a":"Italy","i":"IT","p":270816,"la":45.439,"lo":12.335,"k":0,"z":5.1,"s":4},{"n":"El Calafate","a":"Argentina","i":"AR","p":8000,"la":-50.333,"lo":-72.3,"k":0,"z":5.6,"s":4},{"n":"San Juan","a":"Argentina","i":"AR","p":447048,"la":-31.55,"lo":-68.52,"k":0,"z":5.6,"s":4},{"n":"Rawson","a":"Argentina","i":"AR","p":26335,"la":-43.3,"lo":-65.1,"k":0,"z":5.1,"s":4},{"n":"Neuquén","a":"Argentina","i":"AR","p":242092,"la":-38.95,"lo":-68.06,"k":0,"z":5.1,"s":4},{"n":"Trinidad","a":"Bolivia","i":"BO","p":84259,"la":-14.833,"lo":-64.9,"k":0,"z":5.6,"s":4},{"n":"Santa Rosa","a":"Argentina","i":"AR","p":111424,"la":-36.62,"lo":-64.3,"k":0,"z":5.1,"s":4},{"n":"Bariloche","a":"Argentina","i":"AR","p":95394,"la":-41.15,"lo":-71.3,"k":0,"z":5.6,"s":4},{"n":"Salta","a":"Argentina","i":"AR","p":512686,"la":-24.783,"lo":-65.417,"k":0,"z":5.6,"s":4},{"n":"Tucumán","a":"Argentina","i":"AR","p":830000,"la":-26.814,"lo":-65.219,"k":0,"z":5.6,"s":4},{"n":"Formosa","a":"Argentina","i":"AR","p":221383,"la":-26.173,"lo":-58.183,"k":0,"z":5.6,"s":4},{"n":"Santa Fe","a":"Argentina","i":"AR","p":489505,"la":-31.624,"lo":-60.69,"k":0,"z":5.6,"s":4},{"n":"Rosario","a":"Argentina","i":"AR","p":1203000,"la":-32.949,"lo":-60.668,"k":0,"z":5.1,"s":4},{"n":"Campinas","a":"Brazil","i":"BR","p":2791000,"la":-22.908,"lo":-47.056,"k":0,"z":5.6,"s":4},{"n":"Sorocaba","a":"Brazil","i":"BR","p":563281,"la":-23.49,"lo":-47.47,"k":0,"z":5.6,"s":4},{"n":"Ribeirão Preto","a":"Brazil","i":"BR","p":551267,"la":-21.17,"lo":-47.83,"k":0,"z":5.6,"s":4},{"n":"Petrolina","a":"Brazil","i":"BR","p":260985,"la":-9.38,"lo":-40.51,"k":0,"z":5.6,"s":4},{"n":"Bamenda","a":"Cameroon","i":"CM","p":445299,"la":5.96,"lo":10.15,"k":0,"z":5.6,"s":4},{"n":"Garoua","a":"Cameroon","i":"CM","p":436899,"la":9.3,"lo":13.39,"k":0,"z":5.6,"s":4},{"n":"Herat","a":"Afghanistan","i":"AF","p":481009,"la":34.33,"lo":62.17,"k":0,"z":5.1,"s":4},{"n":"Mazar-i-Sharif","a":"Afghanistan","i":"AF","p":458151,"la":36.7,"lo":67.1,"k":0,"z":5.1,"s":4},{"n":"Battambang","a":"Cambodia","i":"KH","p":154773,"la":13.1,"lo":103.2,"k":0,"z":5.6,"s":4},{"n":"Siem Reap","a":"Cambodia","i":"KH","p":109398,"la":13.367,"lo":103.85,"k":0,"z":5.6,"s":4},{"n":"Malanje","a":"Angola","i":"AO","p":125856,"la":-9.54,"lo":16.34,"k":0,"z":5.6,"s":4},{"n":"Benguela","a":"Angola","i":"AO","p":151226,"la":-12.578,"lo":13.407,"k":0,"z":5.6,"s":4},{"n":"Lubango","a":"Angola","i":"AO","p":125632,"la":-14.91,"lo":13.49,"k":0,"z":5.6,"s":4},{"n":"Moçâmedes","a":"Angola","i":"AO","p":132900,"la":-15.19,"lo":12.16,"k":0,"z":5.6,"s":4},{"n":"Tarija","a":"Bolivia","i":"BO","p":159269,"la":-21.517,"lo":-64.75,"k":0,"z":5.6,"s":4},{"n":"Bridgetown","a":"Barbados","i":"BB","p":191152,"la":13.102,"lo":-59.617,"k":1,"z":5.6,"s":4},{"n":"Annaba","a":"Algeria","i":"DZ","p":503524,"la":36.92,"lo":7.76,"k":0,"z":5.6,"s":4},{"n":"Parakou","a":"Benin","i":"BJ","p":188853,"la":9.34,"lo":2.62,"k":0,"z":5.6,"s":4},{"n":"Porto-Novo","a":"Benin","i":"BJ","p":300000,"la":6.483,"lo":2.617,"k":0,"z":5.6,"s":4},{"n":"Constantine","a":"Algeria","i":"DZ","p":605179,"la":36.36,"lo":6.6,"k":0,"z":5.6,"s":4},{"n":"Brest","a":"Belarus","i":"BY","p":300715,"la":52.1,"lo":23.7,"k":0,"z":5.1,"s":4},{"n":"Khulna","a":"Bangladesh","i":"BD","p":1553000,"la":22.842,"lo":89.558,"k":0,"z":5.6,"s":4},{"n":"Francistown","a":"Botswana","i":"BW","p":89979,"la":-21.17,"lo":27.5,"k":0,"z":5.6,"s":4},{"n":"Mahalapye","a":"Botswana","i":"BW","p":50744,"la":-23.1,"lo":26.82,"k":0,"z":5.6,"s":4},{"n":"Serowe","a":"Botswana","i":"BW","p":48573,"la":-22.39,"lo":26.71,"k":0,"z":5.6,"s":4},{"n":"Katherine","a":"Australia","i":"AU","p":10141,"la":-14.467,"lo":132.267,"k":0,"z":5.6,"s":4},{"n":"Busselton","a":"Australia","i":"AU","p":10214,"la":-33.656,"lo":115.349,"k":0,"z":5.6,"s":4},{"n":"Mandurah","a":"Australia","i":"AU","p":73356,"la":-32.523,"lo":115.747,"k":0,"z":5.6,"s":4},{"n":"Broome","a":"Australia","i":"AU","p":13218,"la":-17.962,"lo":122.231,"k":0,"z":5,"s":4},{"n":"Kalgoorlie","a":"Australia","i":"AU","p":36852,"la":-30.735,"lo":121.46,"k":0,"z":5.1,"s":4},{"n":"Albany","a":"Australia","i":"AU","p":26445,"la":-35.017,"lo":117.892,"k":0,"z":5,"s":4},{"n":"Port Hedland","a":"Australia","i":"AU","p":14288,"la":-20.31,"lo":118.606,"k":0,"z":5.1,"s":4},{"n":"Karratha","a":"Australia","i":"AU","p":16796,"la":-20.73,"lo":116.87,"k":0,"z":5.6,"s":4},{"n":"Geraldton","a":"Australia","i":"AU","p":27258,"la":-28.767,"lo":114.6,"k":0,"z":5,"s":4},{"n":"Griffith","a":"Australia","i":"AU","p":15455,"la":-34.29,"lo":146.04,"k":0,"z":5,"s":4},{"n":"Orange","a":"Australia","i":"AU","p":39329,"la":-33.28,"lo":149.1,"k":0,"z":5.6,"s":4},{"n":"Dubbo","a":"Australia","i":"AU","p":31574,"la":-32.26,"lo":148.597,"k":0,"z":5,"s":4},{"n":"Armidale","a":"Australia","i":"AU","p":22673,"la":-30.512,"lo":151.667,"k":0,"z":5.6,"s":4},{"n":"Broken Hill","a":"Australia","i":"AU","p":17395,"la":-31.95,"lo":141.433,"k":0,"z":5.1,"s":4},{"n":"Port Lincoln","a":"Australia","i":"AU","p":13630,"la":-34.733,"lo":135.867,"k":0,"z":5.6,"s":4},{"n":"Whyalla","a":"Australia","i":"AU","p":22559,"la":-33.025,"lo":137.561,"k":0,"z":5,"s":4},{"n":"Portland","a":"Australia","i":"AU","p":11808,"la":-38.34,"lo":141.59,"k":0,"z":5,"s":4},{"n":"Bendigo","a":"Australia","i":"AU","p":81657,"la":-36.76,"lo":144.28,"k":0,"z":5.6,"s":4},{"n":"Wangaratta","a":"Australia","i":"AU","p":14022,"la":-36.36,"lo":146.3,"k":0,"z":5.6,"s":4},{"n":"Windorah","a":"Australia","i":"AU","p":158,"la":-25.433,"lo":142.65,"k":0,"z":5,"s":4},{"n":"Mount Isa","a":"Australia","i":"AU","p":33200,"la":-20.724,"lo":139.49,"k":0,"z":5.1,"s":4},{"n":"Rockhampton","a":"Australia","i":"AU","p":65850,"la":-23.364,"lo":150.52,"k":0,"z":5.6,"s":4},{"n":"Cairns","a":"Australia","i":"AU","p":154225,"la":-16.888,"lo":145.763,"k":0,"z":5,"s":4},{"n":"Gold Coast","a":"Australia","i":"AU","p":527660,"la":-28.082,"lo":153.448,"k":0,"z":5,"s":4},{"n":"Devonport","a":"Australia","i":"AU","p":19317,"la":-41.193,"lo":146.331,"k":0,"z":5.6,"s":4},{"n":"Bobo Dioulasso","a":"Burkina Faso","i":"BF","p":360106,"la":11.18,"lo":-4.29,"k":0,"z":5.6,"s":4},{"n":"Rajshahi","a":"Bangladesh","i":"BD","p":810000,"la":24.377,"lo":88.603,"k":0,"z":5.6,"s":4},{"n":"Mandalay","a":"Myanmar","i":"MM","p":1300000,"la":21.972,"lo":96.083,"k":0,"z":5.6,"s":4},{"n":"Sittwe","a":"Myanmar","i":"MM","p":179032,"la":20.14,"lo":92.88,"k":0,"z":5.6,"s":4},{"n":"Bujumbura","a":"Burundi","i":"BI","p":331700,"la":-3.376,"lo":29.36,"k":1,"z":5.6,"s":4},{"n":"Pago Pago","a":"American Samoa","i":"AS","p":12576,"la":-14.277,"lo":-170.707,"k":0,"z":5.6,"s":4},{"n":"Kingstown","a":"Saint Vincent and the Grenadines","i":"VC","p":49485,"la":13.156,"lo":-61.22,"k":1,"z":5.1,"s":4},{"n":"Castries","a":"Saint Lucia","i":"LC","p":37963,"la":14.008,"lo":-60.993,"k":1,"z":5.6,"s":4},{"n":"Basseterre","a":"Saint Kitts and Nevis","i":"KN","p":21887,"la":17.302,"lo":-62.717,"k":1,"z":5.6,"s":4},{"n":"Las Palmas","a":"Spain","i":"ES","p":378495,"la":28.1,"lo":-15.43,"k":0,"z":5.1,"s":4},{"n":"Berbera","a":"Somaliland","i":"-99","p":242344,"la":10.436,"lo":45.016,"k":0,"z":5.6,"s":4},{"n":"Port Louis","a":"Mauritius","i":"MU","p":595491,"la":-20.167,"lo":57.5,"k":1,"z":5.6,"s":4},{"n":"Gaza City","a":"Palestine","i":"PS","p":505700,"la":31.53,"lo":34.445,"k":0,"z":5.6,"s":4},{"n":"Saint George's","a":"Grenada","i":"GD","p":33734,"la":12.053,"lo":-61.742,"k":1,"z":5.1,"s":4},{"n":"Papeete","a":"French Polynesia","i":"PF","p":131695,"la":-17.533,"lo":-149.567,"k":0,"z":5.6,"s":4},{"n":"Manama","a":"Bahrain","i":"BH","p":563920,"la":26.236,"lo":50.583,"k":1,"z":5.6,"s":4},{"n":"Freeport","a":"The Bahamas","i":"BS","p":25383,"la":26.533,"lo":-78.7,"k":0,"z":5.6,"s":4},{"n":"Saint John's","a":"Antigua and Barbuda","i":"AG","p":35499,"la":17.118,"lo":-61.85,"k":1,"z":5.1,"s":4},{"n":"Taichung","a":"Taiwan","i":"TW","p":2629323,"la":24.152,"lo":120.682,"k":0,"z":5.6,"s":4},{"n":"Napier","a":"New Zealand","i":"NZ","p":57800,"la":-39.49,"lo":176.926,"k":0,"z":5,"s":4},{"n":"Manukau","a":"New Zealand","i":"NZ","p":375600,"la":-37,"lo":174.885,"k":0,"z":5.6,"s":4},{"n":"Hamilton","a":"New Zealand","i":"NZ","p":148200,"la":-37.778,"lo":175.29,"k":0,"z":5,"s":4},{"n":"Blenheim","a":"New Zealand","i":"NZ","p":30300,"la":-41.514,"lo":173.955,"k":0,"z":5,"s":4},{"n":"Dunedin","a":"New Zealand","i":"NZ","p":117700,"la":-45.885,"lo":170.491,"k":0,"z":5,"s":4},{"n":"Leh","a":"India","i":"IN","p":30870,"la":34.162,"lo":77.572,"k":0,"z":5.6,"s":4},{"n":"Amaravati","a":"India","i":"IN","p":5800000,"la":16.534,"lo":80.524,"k":0,"z":5.6,"s":4},{"n":"Kozhikode","a":"India","i":"IN","p":953000,"la":11.252,"lo":75.768,"k":0,"z":5,"s":3},{"n":"Bhubaneswar","a":"India","i":"IN","p":844000,"la":20.272,"lo":85.825,"k":0,"z":5,"s":3},{"n":"Jamshedpur","a":"India","i":"IN","p":1300000,"la":22.789,"lo":86.196,"k":0,"z":5,"s":3},{"n":"Montevideo","a":"Uruguay","i":"UY","p":1513000,"la":-34.905,"lo":-56.187,"k":1,"z":4,"s":3},{"n":"Helena","a":"United States of America","i":"US","p":38725,"la":46.593,"lo":-112.035,"k":0,"z":5,"s":3},{"n":"Bismarck","a":"United States of America","i":"US","p":63871,"la":46.808,"lo":-100.783,"k":0,"z":5,"s":3},{"n":"Boise","a":"United States of America","i":"US","p":338071,"la":43.609,"lo":-116.227,"k":0,"z":5,"s":3},{"n":"San Jose","a":"United States of America","i":"US","p":1668000,"la":37.331,"lo":-121.888,"k":0,"z":5,"s":3},{"n":"Sacramento","a":"United States of America","i":"US","p":1604000,"la":38.577,"lo":-121.472,"k":0,"z":5,"s":3},{"n":"Las Vegas","a":"United States of America","i":"US","p":1823000,"la":36.165,"lo":-115.151,"k":0,"z":5,"s":3},{"n":"Santa Fe","a":"United States of America","i":"US","p":92681,"la":35.687,"lo":-105.937,"k":0,"z":5,"s":3},{"n":"Portland","a":"United States of America","i":"US","p":1875000,"la":45.522,"lo":-122.682,"k":0,"z":4,"s":3},{"n":"Salt Lake City","a":"United States of America","i":"US","p":966000,"la":40.777,"lo":-111.932,"k":0,"z":4,"s":3},{"n":"Cheyenne","a":"United States of America","i":"US","p":72927,"la":41.14,"lo":-104.82,"k":0,"z":5,"s":3},{"n":"Des Moines","a":"United States of America","i":"US","p":380655,"la":41.58,"lo":-93.62,"k":0,"z":5,"s":3},{"n":"Omaha","a":"United States of America","i":"US","p":877110,"la":41.24,"lo":-96.01,"k":0,"z":5,"s":3},{"n":"Oklahoma City","a":"United States of America","i":"US","p":788000,"la":35.472,"lo":-97.521,"k":0,"z":5,"s":3},{"n":"Pierre","a":"United States of America","i":"US","p":13879,"la":44.368,"lo":-100.351,"k":0,"z":5,"s":3},{"n":"San Antonio","a":"United States of America","i":"US","p":1473000,"la":29.42,"lo":-98.493,"k":0,"z":5,"s":3},{"n":"San Cristóbal","a":"Venezuela","i":"VE","p":438798,"la":7.77,"lo":-72.25,"k":0,"z":5,"s":3},{"n":"Valencia","a":"Venezuela","i":"VE","p":1770000,"la":10.232,"lo":-67.982,"k":0,"z":5,"s":3},{"n":"Jackson","a":"United States of America","i":"US","p":250902,"la":32.299,"lo":-90.185,"k":0,"z":5,"s":3},{"n":"Raleigh","a":"United States of America","i":"US","p":1163515,"la":35.773,"lo":-78.647,"k":0,"z":5,"s":3},{"n":"Cleveland","a":"United States of America","i":"US","p":1890000,"la":41.472,"lo":-81.697,"k":0,"z":5,"s":3},{"n":"Cincinnati","a":"United States of America","i":"US","p":1636000,"la":39.164,"lo":-84.459,"k":0,"z":5,"s":3},{"n":"Nashville","a":"United States of America","i":"US","p":877000,"la":36.172,"lo":-86.782,"k":0,"z":5,"s":3},{"n":"Memphis","a":"United States of America","i":"US","p":1081000,"la":35.145,"lo":-90.029,"k":0,"z":5,"s":3},{"n":"Norfolk","a":"United States of America","i":"US","p":1047869,"la":36.85,"lo":-76.28,"k":0,"z":5.7,"s":3},{"n":"Milwaukee","a":"United States of America","i":"US","p":1388000,"la":43.03,"lo":-87.917,"k":0,"z":5,"s":3},{"n":"Buffalo","a":"United States of America","i":"US","p":1016000,"la":42.882,"lo":-78.882,"k":0,"z":5,"s":3},{"n":"Pittsburgh","a":"United States of America","i":"US","p":1838000,"la":40.432,"lo":-80.002,"k":0,"z":5,"s":3},{"n":"Ciudad Guayana","a":"Venezuela","i":"VE","p":746535,"la":8.37,"lo":-62.62,"k":0,"z":5,"s":3},{"n":"Lomé","a":"Togo","i":"TG","p":1452000,"la":6.134,"lo":1.221,"k":1,"z":5,"s":3},{"n":"Tunis","a":"Tunisia","i":"TN","p":2412500,"la":36.803,"lo":10.18,"k":1,"z":5,"s":3},{"n":"Kodiak","a":"United States of America","i":"US","p":9461,"la":57.79,"lo":-152.407,"k":0,"z":5,"s":3},{"n":"Cold Bay","a":"United States of America","i":"US","p":200,"la":55.2,"lo":-162.715,"k":0,"z":5,"s":3},{"n":"Bethel","a":"United States of America","i":"US","p":6228,"la":60.793,"lo":-161.756,"k":0,"z":5,"s":3},{"n":"Point Hope","a":"United States of America","i":"US","p":461,"la":68.348,"lo":-166.808,"k":0,"z":5,"s":3},{"n":"Utqiaġvik","a":"United States of America","i":"US","p":4336,"la":71.291,"lo":-156.789,"k":0,"z":5,"s":3},{"n":"Nome","a":"United States of America","i":"US","p":3485,"la":64.506,"lo":-165.406,"k":0,"z":4,"s":3},{"n":"Valdez","a":"United States of America","i":"US","p":4036,"la":61.136,"lo":-146.348,"k":0,"z":5,"s":3},{"n":"Juneau","a":"United States of America","i":"US","p":30711,"la":58.303,"lo":-134.401,"k":0,"z":4,"s":3},{"n":"Fairbanks","a":"United States of America","i":"US","p":56993,"la":64.837,"lo":-147.711,"k":0,"z":4,"s":3},{"n":"Prudhoe Bay","a":"United States of America","i":"US","p":2500,"la":70.292,"lo":-148.669,"k":0,"z":3.7,"s":3},{"n":"Sevastopol","a":"Russia","i":"RU","p":379200,"la":44.604,"lo":33.524,"k":0,"z":4,"s":3},{"n":"Abu Dhabi","a":"United Arab Emirates","i":"AE","p":603492,"la":24.467,"lo":54.367,"k":1,"z":5,"s":3},{"n":"Ashgabat","a":"Turkmenistan","i":"TM","p":727700,"la":37.95,"lo":58.383,"k":1,"z":5,"s":3},{"n":"Samarkand","a":"Uzbekistan","i":"UZ","p":708000,"la":39.67,"lo":66.945,"k":0,"z":5,"s":3},{"n":"Lusaka","a":"Zambia","i":"ZM","p":1328000,"la":-15.415,"lo":28.281,"k":1,"z":4,"s":3},{"n":"Harare","a":"Zimbabwe","i":"ZW","p":1572000,"la":-17.816,"lo":31.043,"k":1,"z":4,"s":3},{"n":"Bulawayo","a":"Zimbabwe","i":"ZW","p":699385,"la":-20.17,"lo":28.58,"k":0,"z":5,"s":3},{"n":"Dili","a":"East Timor","i":"TL","p":234331,"la":-8.559,"lo":125.579,"k":1,"z":5,"s":3},{"n":"Port Vila","a":"Vanuatu","i":"VU","p":44040,"la":-17.733,"lo":168.317,"k":1,"z":4,"s":3},{"n":"Tegucigalpa","a":"Honduras","i":"HN","p":946000,"la":14.104,"lo":-87.219,"k":1,"z":5,"s":3},{"n":"Georgetown","a":"Guyana","i":"GY","p":264350,"la":6.802,"lo":-58.167,"k":1,"z":4,"s":3},{"n":"Reykjavík","a":"Iceland","i":"IS","p":166212,"la":64.143,"lo":-21.937,"k":1,"z":3.7,"s":3},{"n":"Port-au-Prince","a":"Haiti","i":"HT","p":1998000,"la":18.543,"lo":-72.338,"k":1,"z":5,"s":3},{"n":"Glasgow","a":"United Kingdom","i":"GB","p":1160000,"la":55.862,"lo":-4.239,"k":0,"z":4,"s":3},{"n":"Kampala","a":"Uganda","i":"UG","p":1420000,"la":0.319,"lo":32.581,"k":1,"z":5,"s":3},{"n":"Aden","a":"Yemen","i":"YE","p":1000000,"la":12.78,"lo":45.009,"k":0,"z":5,"s":3},{"n":"Paramaribo","a":"Suriname","i":"SR","p":254169,"la":5.835,"lo":-55.167,"k":1,"z":5,"s":3},{"n":"Seville","a":"Spain","i":"ES","p":1212045,"la":37.405,"lo":-5.98,"k":0,"z":5,"s":3},{"n":"Zinder","a":"Niger","i":"NE","p":230358,"la":13.8,"lo":8.983,"k":0,"z":5,"s":3},{"n":"Niamey","a":"Niger","i":"NE","p":915000,"la":13.519,"lo":2.115,"k":1,"z":5,"s":3},{"n":"Port Sudan","a":"Sudan","i":"SD","p":489725,"la":19.616,"lo":37.216,"k":0,"z":5,"s":3},{"n":"Dushanbe","a":"Tajikistan","i":"TJ","p":1086244,"la":38.56,"lo":68.774,"k":1,"z":5,"s":3},{"n":"Cusco","a":"Peru","i":"PE","p":361182,"la":-13.525,"lo":-71.972,"k":0,"z":4,"s":3},{"n":"Tacna","a":"Peru","i":"PE","p":280098,"la":-18,"lo":-70.25,"k":0,"z":5,"s":3},{"n":"Trujillo","a":"Peru","i":"PE","p":765171,"la":-8.12,"lo":-79.02,"k":0,"z":4,"s":3},{"n":"Ica","a":"Peru","i":"PE","p":279420,"la":-14.068,"lo":-75.725,"k":0,"z":5,"s":3},{"n":"Asunción","a":"Paraguay","i":"PY","p":1870000,"la":-25.291,"lo":-57.626,"k":1,"z":4,"s":3},{"n":"Managua","a":"Nicaragua","i":"NI","p":920000,"la":12.155,"lo":-86.27,"k":1,"z":5,"s":3},{"n":"Freetown","a":"Sierra Leone","i":"SL","p":827000,"la":8.472,"lo":-13.236,"k":1,"z":5,"s":3},{"n":"Agadez","a":"Niger","i":"NE","p":117770,"la":16.996,"lo":7.983,"k":0,"z":5,"s":3},{"n":"Nyala","a":"Sudan","i":"SD","p":392373,"la":12.06,"lo":24.89,"k":0,"z":5,"s":3},{"n":"Wau","a":"South Sudan","i":"SS","p":127384,"la":7.7,"lo":27.99,"k":0,"z":5,"s":3},{"n":"Dongola","a":"Sudan","i":"SD","p":26404,"la":19.167,"lo":30.483,"k":0,"z":5,"s":3},{"n":"Kassala","a":"Sudan","i":"SD","p":401477,"la":15.46,"lo":36.39,"k":0,"z":5,"s":3},{"n":"Tromsø","a":"Norway","i":"NO","p":52436,"la":69.635,"lo":18.992,"k":0,"z":3.7,"s":3},{"n":"Trondheim","a":"Norway","i":"NO","p":147139,"la":63.417,"lo":10.417,"k":0,"z":4,"s":3},{"n":"Bergen","a":"Norway","i":"NO","p":213585,"la":60.391,"lo":5.325,"k":0,"z":5,"s":3},{"n":"Islamabad","a":"Pakistan","i":"PK","p":780000,"la":33.689,"lo":73.081,"k":1,"z":5,"s":3},{"n":"Multan","a":"Pakistan","i":"PK","p":1522000,"la":30.202,"lo":71.453,"k":0,"z":5,"s":3},{"n":"Hyderabad","a":"Pakistan","i":"PK","p":1459000,"la":25.382,"lo":68.373,"k":0,"z":5,"s":3},{"n":"Peshawar","a":"Pakistan","i":"PK","p":1303000,"la":34.007,"lo":71.533,"k":0,"z":5,"s":3},{"n":"Kathmandu","a":"Nepal","i":"NP","p":895000,"la":27.719,"lo":85.315,"k":1,"z":5,"s":3},{"n":"Nacala","a":"Mozambique","i":"MZ","p":224795,"la":-14.519,"lo":40.715,"k":0,"z":5,"s":3},{"n":"Bloemfontein","a":"South Africa","i":"ZA","p":463064,"la":-29.12,"lo":26.23,"k":1,"z":5,"s":3},{"n":"Pretoria","a":"South Africa","i":"ZA","p":1338000,"la":-25.705,"lo":28.227,"k":1,"z":5,"s":3},{"n":"Port Moresby","a":"Papua New Guinea","i":"PG","p":283733,"la":-9.465,"lo":147.193,"k":1,"z":4,"s":3},{"n":"Honiara","a":"Solomon Islands","i":"SB","p":76328,"la":-9.438,"lo":159.95,"k":1,"z":5,"s":3},{"n":"Panama City","a":"Panama","i":"PA","p":1281000,"la":8.97,"lo":-79.535,"k":1,"z":5,"s":3},{"n":"Fez","a":"Morocco","i":"MA","p":1002000,"la":34.057,"lo":-5.002,"k":0,"z":5,"s":3},{"n":"Rabat","a":"Morocco","i":"MA","p":1705000,"la":34.025,"lo":-6.836,"k":1,"z":5,"s":3},{"n":"Marrakesh","a":"Morocco","i":"MA","p":872000,"la":31.632,"lo":-8.002,"k":0,"z":5,"s":3},{"n":"Chișinău","a":"Moldova","i":"MD","p":688134,"la":47.005,"lo":28.858,"k":1,"z":5,"s":3},{"n":"Beira","a":"Mozambique","i":"MZ","p":530604,"la":-19.82,"lo":34.87,"k":0,"z":5,"s":3},{"n":"Port Elizabeth","a":"South Africa","i":"ZA","p":1021000,"la":-33.968,"lo":25.598,"k":0,"z":5,"s":3},{"n":"Maputo","a":"Mozambique","i":"MZ","p":1446000,"la":-25.953,"lo":32.587,"k":1,"z":5,"s":3},{"n":"Tomsk","a":"Russia","i":"RU","p":535479,"la":56.495,"lo":84.975,"k":0,"z":4,"s":3},{"n":"Anadyr","a":"Russia","i":"RU","p":10332,"la":64.737,"lo":177.475,"k":0,"z":5,"s":3},{"n":"Murmansk","a":"Russia","i":"RU","p":319263,"la":68.97,"lo":33.1,"k":0,"z":3.7,"s":3},{"n":"Archangel","a":"Russia","i":"RU","p":356051,"la":64.575,"lo":40.545,"k":0,"z":4,"s":3},{"n":"Nizhny Novgorod","a":"Russia","i":"RU","p":1278000,"la":56.335,"lo":43.998,"k":0,"z":5,"s":3},{"n":"Volgograd","a":"Russia","i":"RU","p":984000,"la":48.712,"lo":44.498,"k":0,"z":4,"s":3},{"n":"Ufa","a":"Russia","i":"RU","p":1018000,"la":54.792,"lo":56.038,"k":0,"z":5,"s":3},{"n":"Yekaterinburg","a":"Russia","i":"RU","p":1313000,"la":56.852,"lo":60.598,"k":0,"z":5,"s":3},{"n":"Samara","a":"Russia","i":"RU","p":1137000,"la":53.197,"lo":50.149,"k":0,"z":3.7,"s":3},{"n":"Kazan","a":"Russia","i":"RU","p":1115000,"la":55.752,"lo":49.124,"k":0,"z":5,"s":3},{"n":"Surgut","a":"Russia","i":"RU","p":400000,"la":61.26,"lo":73.425,"k":0,"z":4,"s":3},{"n":"Barnaul","a":"Russia","i":"RU","p":599579,"la":53.355,"lo":83.745,"k":0,"z":5,"s":3},{"n":"Novosibirsk","a":"Russia","i":"RU","p":1389000,"la":55.032,"lo":82.958,"k":0,"z":5,"s":3},{"n":"Mogadishu","a":"Somalia","i":"SO","p":1100000,"la":2.069,"lo":45.365,"k":1,"z":4,"s":3},{"n":"Muscat","a":"Oman","i":"OM","p":734697,"la":23.585,"lo":58.378,"k":1,"z":5,"s":3},{"n":"Colombo","a":"Sri Lanka","i":"LK","p":217000,"la":6.932,"lo":79.858,"k":1,"z":5,"s":3},{"n":"Cebu","a":"Philippines","i":"PH","p":815000,"la":10.322,"lo":123.898,"k":0,"z":4,"s":3},{"n":"Iloilo","a":"Philippines","i":"PH","p":387681,"la":10.705,"lo":122.545,"k":0,"z":5,"s":3},{"n":"Davao","a":"Philippines","i":"PH","p":1402000,"la":7.112,"lo":125.628,"k":0,"z":5,"s":3},{"n":"Bratsk","a":"Russia","i":"RU","p":246348,"la":56.157,"lo":101.615,"k":0,"z":5,"s":3},{"n":"Irkutsk","a":"Russia","i":"RU","p":586695,"la":52.285,"lo":104.284,"k":0,"z":3.7,"s":3},{"n":"Krasnoyarsk","a":"Russia","i":"RU","p":925000,"la":56.016,"lo":92.864,"k":0,"z":5,"s":3},{"n":"Dikson","a":"Russia","i":"RU","p":1113,"la":73.507,"lo":80.545,"k":0,"z":5,"s":3},{"n":"Chita","a":"Russia","i":"RU","p":308500,"la":52.055,"lo":113.465,"k":0,"z":5,"s":3},{"n":"Vladivostok","a":"Russia","i":"RU","p":587022,"la":43.13,"lo":131.91,"k":0,"z":5,"s":3},{"n":"Nizhneyansk","a":"Russia","i":"RU","p":400,"la":71.433,"lo":136.067,"k":0,"z":5,"s":3},{"n":"Yakutsk","a":"Russia","i":"RU","p":235600,"la":62.035,"lo":129.735,"k":0,"z":4,"s":3},{"n":"Tiksi","a":"Russia","i":"RU","p":5700,"la":71.627,"lo":128.835,"k":0,"z":4,"s":3},{"n":"Magadan","a":"Russia","i":"RU","p":95282,"la":59.575,"lo":150.81,"k":0,"z":3.7,"s":3},{"n":"Tijuana","a":"Mexico","i":"MX","p":1553000,"la":32.502,"lo":-117.082,"k":0,"z":5,"s":3},{"n":"Chihuahua","a":"Mexico","i":"MX","p":793000,"la":28.647,"lo":-106.087,"k":0,"z":4,"s":3},{"n":"Mazatlán","a":"Mexico","i":"MX","p":368204,"la":23.221,"lo":-106.42,"k":0,"z":5,"s":3},{"n":"Tampico","a":"Mexico","i":"MX","p":859419,"la":22.3,"lo":-97.87,"k":0,"z":5,"s":3},{"n":"Acapulco","a":"Mexico","i":"MX","p":715584,"la":16.85,"lo":-99.916,"k":0,"z":5,"s":3},{"n":"Veracruz","a":"Mexico","i":"MX","p":578963,"la":19.177,"lo":-96.16,"k":0,"z":5,"s":3},{"n":"Tuxtla Gutiérrez","a":"Mexico","i":"MX","p":481128,"la":16.75,"lo":-93.15,"k":0,"z":5,"s":3},{"n":"Cancún","a":"Mexico","i":"MX","p":542043,"la":21.17,"lo":-86.83,"k":0,"z":4,"s":3},{"n":"Mérida","a":"Mexico","i":"MX","p":965000,"la":20.969,"lo":-89.619,"k":0,"z":5,"s":3},{"n":"Enugu","a":"Nigeria","i":"NG","p":688862,"la":6.45,"lo":7.5,"k":0,"z":5,"s":3},{"n":"Sokoto","a":"Nigeria","i":"NG","p":732178,"la":13.06,"lo":5.24,"k":0,"z":5,"s":3},{"n":"Perm","a":"Russia","i":"RU","p":997000,"la":58.002,"lo":56.248,"k":0,"z":4,"s":3},{"n":"Erdenet","a":"Mongolia","i":"MN","p":79647,"la":49.053,"lo":104.118,"k":0,"z":5,"s":3},{"n":"Ulaanbaatar","a":"Mongolia","i":"MN","p":885000,"la":47.919,"lo":106.915,"k":1,"z":4,"s":3},{"n":"Mbeya","a":"Tanzania","i":"TZ","p":291649,"la":-8.89,"lo":33.43,"k":0,"z":5,"s":3},{"n":"Windhoek","a":"Namibia","i":"NA","p":268132,"la":-22.57,"lo":17.084,"k":1,"z":5,"s":3},{"n":"Grootfontein","a":"Namibia","i":"NA","p":24099,"la":-19.567,"lo":18.117,"k":0,"z":5,"s":3},{"n":"Zanzibar","a":"Tanzania","i":"TZ","p":403658,"la":-6.16,"lo":39.2,"k":0,"z":5,"s":3},{"n":"Valencia","a":"Spain","i":"ES","p":808000,"la":39.487,"lo":-0.402,"k":0,"z":4.7,"s":3},{"n":"Palana","a":"Russia","i":"RU","p":3671,"la":59.084,"lo":159.95,"k":0,"z":5,"s":3},{"n":"Petropavlovsk-Kamchatsky","a":"Russia","i":"RU","p":187282,"la":53.062,"lo":158.623,"k":0,"z":5,"s":3},{"n":"Abuja","a":"Nigeria","i":"NG","p":1576000,"la":9.055,"lo":7.49,"k":1,"z":4,"s":3},{"n":"Padang","a":"Indonesia","i":"ID","p":855000,"la":-0.958,"lo":100.358,"k":0,"z":5,"s":3},{"n":"Bissau","a":"Guinea Bissau","i":"GW","p":403339,"la":11.865,"lo":-15.598,"k":1,"z":5,"s":3},{"n":"Palermo","a":"Italy","i":"IT","p":863000,"la":38.127,"lo":13.348,"k":0,"z":5,"s":3},{"n":"Amman","a":"Jordan","i":"JO","p":1060000,"la":31.952,"lo":35.931,"k":1,"z":5,"s":3},{"n":"Vilnius","a":"Lithuania","i":"LT","p":542366,"la":54.683,"lo":25.317,"k":1,"z":5,"s":3},{"n":"Riga","a":"Latvia","i":"LV","p":742572,"la":56.95,"lo":24.1,"k":1,"z":4,"s":3},{"n":"Bishkek","a":"Kyrgyzstan","i":"KG","p":837000,"la":42.875,"lo":74.583,"k":1,"z":5,"s":3},{"n":"Jiayuguan","a":"China","i":"CN","p":148279,"la":39.82,"lo":98.3,"k":0,"z":5,"s":3},{"n":"Xining","a":"China","i":"CN","p":1048000,"la":36.622,"lo":101.768,"k":0,"z":4.7,"s":3},{"n":"Guilin","a":"China","i":"CN","p":987000,"la":25.282,"lo":110.278,"k":0,"z":5,"s":3},{"n":"Huainan","a":"China","i":"CN","p":1451000,"la":32.632,"lo":116.978,"k":0,"z":5,"s":3},{"n":"Shantou","a":"China","i":"CN","p":1601000,"la":23.372,"lo":116.668,"k":0,"z":5,"s":3},{"n":"Tarakan","a":"Indonesia","i":"ID","p":193069,"la":3.3,"lo":117.633,"k":0,"z":5,"s":3},{"n":"Mombasa","a":"Kenya","i":"KE","p":882000,"la":-4.038,"lo":39.688,"k":0,"z":5,"s":3},{"n":"Maseru","a":"Lesotho","i":"LS","p":361324,"la":-29.317,"lo":27.483,"k":1,"z":5,"s":3},{"n":"Antananarivo","a":"Madagascar","i":"MG","p":1697000,"la":-18.915,"lo":47.515,"k":1,"z":5,"s":3},{"n":"Semarang","a":"Indonesia","i":"ID","p":1396000,"la":-6.965,"lo":110.418,"k":0,"z":5,"s":3},{"n":"Palembang","a":"Indonesia","i":"ID","p":1749000,"la":-2.978,"lo":104.748,"k":0,"z":5,"s":3},{"n":"Bandjarmasin","a":"Indonesia","i":"ID","p":603576,"la":-3.33,"lo":114.58,"k":0,"z":5,"s":3},{"n":"Makassar","a":"Indonesia","i":"ID","p":1262000,"la":-5.138,"lo":119.43,"k":0,"z":5,"s":3},{"n":"Lyon","a":"France","i":"FR","p":1423000,"la":45.772,"lo":4.828,"k":0,"z":4.7,"s":3},{"n":"Quito","a":"Ecuador","i":"EC","p":1701000,"la":-0.213,"lo":-78.502,"k":1,"z":4,"s":3},{"n":"San José","a":"Costa Rica","i":"CR","p":1284000,"la":9.93,"lo":-84.079,"k":1,"z":5,"s":3},{"n":"San Salvador","a":"El Salvador","i":"SV","p":1433000,"la":13.703,"lo":-89.216,"k":1,"z":5,"s":3},{"n":"Kingston","a":"Jamaica","i":"JM","p":937700,"la":17.977,"lo":-76.767,"k":1,"z":5,"s":3},{"n":"Cartagena","a":"Colombia","i":"CO","p":887000,"la":10.402,"lo":-75.516,"k":0,"z":5,"s":3},{"n":"Mitú","a":"Colombia","i":"CO","p":5917,"la":1.198,"lo":-70.174,"k":0,"z":5,"s":3},{"n":"Bumba","a":"Congo (Kinshasa)","i":"CD","p":160539,"la":2.19,"lo":22.46,"k":0,"z":5,"s":3},{"n":"N'Djamena","a":"Chad","i":"TD","p":989000,"la":12.115,"lo":15.047,"k":1,"z":5,"s":3},{"n":"Abéché","a":"Chad","i":"TD","p":158317,"la":13.84,"lo":20.83,"k":0,"z":5,"s":3},{"n":"Malabo","a":"Equatorial Guinea","i":"GQ","p":155963,"la":3.75,"lo":8.783,"k":1,"z":5,"s":3},{"n":"Luxor","a":"Egypt","i":"EG","p":609248,"la":25.7,"lo":32.65,"k":0,"z":5,"s":3},{"n":"Asmara","a":"Eritrea","i":"ER","p":620802,"la":15.333,"lo":38.933,"k":1,"z":5,"s":3},{"n":"Zagreb","a":"Croatia","i":"HR","p":722526,"la":45.8,"lo":16,"k":1,"z":5,"s":3},{"n":"Tallinn","a":"Estonia","i":"EE","p":394024,"la":59.434,"lo":24.728,"k":1,"z":5,"s":3},{"n":"Lhasa","a":"China","i":"CN","p":219599,"la":29.645,"lo":91.1,"k":0,"z":4,"s":3},{"n":"Hami","a":"China","i":"CN","p":580000,"la":42.827,"lo":93.515,"k":0,"z":5,"s":3},{"n":"Hotan","a":"China","i":"CN","p":408894,"la":37.1,"lo":79.927,"k":0,"z":4,"s":3},{"n":"Kashgar","a":"China","i":"CN","p":506640,"la":39.476,"lo":75.97,"k":0,"z":5,"s":3},{"n":"Yinchuan","a":"China","i":"CN","p":991000,"la":38.47,"lo":106.271,"k":0,"z":5,"s":3},{"n":"Pingxiang","a":"China","i":"CN","p":961000,"la":27.622,"lo":113.848,"k":0,"z":5,"s":3},{"n":"Nagasaki","a":"Japan","i":"JP","p":435455,"la":32.765,"lo":129.885,"k":0,"z":5,"s":3},{"n":"Qiqihar","a":"China","i":"CN","p":1641000,"la":47.347,"lo":123.988,"k":0,"z":4,"s":3},{"n":"Kikwit","a":"Congo (Kinshasa)","i":"CD","p":637736,"la":-5.03,"lo":18.85,"k":0,"z":5,"s":3},{"n":"Matadi","a":"Congo (Kinshasa)","i":"CD","p":245862,"la":-5.817,"lo":13.45,"k":0,"z":5,"s":3},{"n":"Kolwezi","a":"Congo (Kinshasa)","i":"CD","p":418000,"la":-10.717,"lo":25.472,"k":0,"z":5,"s":3},{"n":"Lubumbashi","a":"Congo (Kinshasa)","i":"CD","p":1352000,"la":-11.678,"lo":27.478,"k":0,"z":5,"s":3},{"n":"Lilongwe","a":"Malawi","i":"MW","p":646750,"la":-13.983,"lo":33.783,"k":1,"z":5,"s":3},{"n":"Guatemala City","a":"Guatemala","i":"GT","p":1024000,"la":14.623,"lo":-90.529,"k":1,"z":5,"s":3},{"n":"Cayenne","a":"France","i":"GF","p":61550,"la":4.933,"lo":-52.33,"k":0,"z":4,"s":3},{"n":"Libreville","a":"Gabon","i":"GA","p":578156,"la":0.385,"lo":9.458,"k":1,"z":5,"s":3},{"n":"Vishakhapatnam","a":"India","i":"IN","p":1529000,"la":17.683,"lo":83.22,"k":0,"z":5,"s":3},{"n":"Suva","a":"Fiji","i":"FJ","p":175399,"la":-18.133,"lo":178.442,"k":1,"z":5,"s":3},{"n":"Port-Gentil","a":"Gabon","i":"GA","p":116836,"la":-0.72,"lo":8.78,"k":0,"z":5,"s":3},{"n":"Timbuktu","a":"Mali","i":"ML","p":68872,"la":16.767,"lo":-3.017,"k":0,"z":5,"s":3},{"n":"Punta Arenas","a":"Chile","i":"CL","p":117430,"la":-53.165,"lo":-70.94,"k":0,"z":3,"s":3},{"n":"Iquique","a":"Chile","i":"CL","p":227499,"la":-20.25,"lo":-70.13,"k":0,"z":5,"s":3},{"n":"Antofagasta","a":"Chile","i":"CL","p":309832,"la":-23.65,"lo":-70.4,"k":0,"z":4,"s":3},{"n":"Valparaíso","a":"Chile","i":"CL","p":854000,"la":-33.048,"lo":-71.617,"k":0,"z":5,"s":3},{"n":"Valdivia","a":"Chile","i":"CL","p":159599,"la":-39.822,"lo":-73.233,"k":0,"z":4,"s":3},{"n":"Concepción","a":"Chile","i":"CL","p":889725,"la":-36.83,"lo":-73.05,"k":0,"z":5,"s":3},{"n":"Puerto Montt","a":"Chile","i":"CL","p":174629,"la":-41.47,"lo":-72.93,"k":0,"z":5,"s":3},{"n":"Nuuk","a":"Greenland","i":"GL","p":14798,"la":64.198,"lo":-51.733,"k":0,"z":4,"s":3},{"n":"Nouakchott","a":"Mauritania","i":"MR","p":742144,"la":18.086,"lo":-15.975,"k":1,"z":5,"s":3},{"n":"Bamako","a":"Mali","i":"ML","p":1494000,"la":12.652,"lo":-8.002,"k":1,"z":4,"s":3},{"n":"Atar","a":"Mauritania","i":"MR","p":44265,"la":20.517,"lo":-13.05,"k":0,"z":5,"s":3},{"n":"Djenné","a":"Mali","i":"ML","p":32944,"la":13.9,"lo":-4.55,"k":0,"z":5,"s":3},{"n":"Sabha","a":"Libya","i":"LY","p":100249,"la":27.033,"lo":14.433,"k":0,"z":5,"s":3},{"n":"Banghazi","a":"Libya","i":"LY","p":1180000,"la":32.119,"lo":20.065,"k":0,"z":4.7,"s":3},{"n":"Thessaloniki","a":"Greece","i":"GR","p":828000,"la":40.698,"lo":22.883,"k":0,"z":5,"s":3},{"n":"Beirut","a":"Lebanon","i":"LB","p":1846000,"la":33.874,"lo":35.508,"k":1,"z":5,"s":3},{"n":"Tbilisi","a":"Georgia","i":"GE","p":1100000,"la":41.727,"lo":44.789,"k":1,"z":5,"s":3},{"n":"Gondar","a":"Ethiopia","i":"ET","p":156230,"la":12.61,"lo":37.46,"k":0,"z":5,"s":3},{"n":"Nur-Sultan","a":"Kazakhstan","i":"KZ","p":345604,"la":51.181,"lo":71.428,"k":1,"z":4,"s":3},{"n":"Qaraghandy","a":"Kazakhstan","i":"KZ","p":451800,"la":49.885,"lo":73.115,"k":0,"z":5,"s":3},{"n":"Almaty","a":"Kazakhstan","i":"KZ","p":1209000,"la":43.327,"lo":76.913,"k":0,"z":5,"s":3},{"n":"Isfahan","a":"Iran","i":"IR","p":1628000,"la":32.702,"lo":51.698,"k":0,"z":5,"s":3},{"n":"Shiraz","a":"Iran","i":"IR","p":1240000,"la":29.632,"lo":52.568,"k":0,"z":5,"s":3},{"n":"Amritsar","a":"India","i":"IN","p":1212000,"la":31.642,"lo":74.868,"k":0,"z":5,"s":3},{"n":"Varanasi","a":"India","i":"IN","p":1352000,"la":25.332,"lo":82.998,"k":0,"z":5,"s":3},{"n":"Asansol","a":"India","i":"IN","p":1328000,"la":23.685,"lo":86.981,"k":0,"z":5,"s":3},{"n":"Bhilai","a":"India","i":"IN","p":1097000,"la":21.211,"lo":81.372,"k":0,"z":5,"s":3},{"n":"Bhopal","a":"India","i":"IN","p":1727000,"la":23.252,"lo":77.408,"k":0,"z":5,"s":3},{"n":"Madurai","a":"India","i":"IN","p":1294000,"la":9.922,"lo":78.118,"k":0,"z":5,"s":3},{"n":"Coimbatore","a":"India","i":"IN","p":1696000,"la":11.002,"lo":76.948,"k":0,"z":5,"s":3},{"n":"Vientiane","a":"Laos","i":"LA","p":754000,"la":17.967,"lo":102.6,"k":1,"z":5,"s":3},{"n":"Brazzaville","a":"Congo (Brazzaville)","i":"CG","p":1355000,"la":-4.257,"lo":15.283,"k":1,"z":5,"s":3},{"n":"Conakry","a":"Guinea","i":"GN","p":1494000,"la":9.533,"lo":-13.682,"k":1,"z":5,"s":3},{"n":"Yamoussoukro","a":"Ivory Coast","i":"CI","p":206499,"la":6.818,"lo":-5.276,"k":1,"z":5,"s":3},{"n":"Cruzeiro do Sul","a":"Brazil","i":"BR","p":56862,"la":-7.63,"lo":-72.67,"k":0,"z":5,"s":3},{"n":"Leticia","a":"Colombia","i":"CO","p":59575,"la":-4.201,"lo":-69.955,"k":0,"z":5,"s":3},{"n":"Manaus","a":"Brazil","i":"BR","p":1753000,"la":-3.098,"lo":-60.002,"k":0,"z":4,"s":3},{"n":"Caxias","a":"Brazil","i":"BR","p":134640,"la":-4.833,"lo":-43.35,"k":0,"z":5,"s":3},{"n":"Santarém","a":"Brazil","i":"BR","p":230428,"la":-2.433,"lo":-54.7,"k":0,"z":5,"s":3},{"n":"Marabá","a":"Brazil","i":"BR","p":166182,"la":-5.35,"lo":-49.116,"k":0,"z":5,"s":3},{"n":"Vilhena","a":"Brazil","i":"BR","p":63231,"la":-12.717,"lo":-60.117,"k":0,"z":5,"s":3},{"n":"Ji-Paraná","a":"Brazil","i":"BR","p":65016,"la":-10.833,"lo":-61.967,"k":0,"z":5,"s":3},{"n":"Campo Grande","a":"Brazil","i":"BR","p":778000,"la":-20.448,"lo":-54.619,"k":0,"z":5,"s":3},{"n":"Florianópolis","a":"Brazil","i":"BR","p":1023000,"la":-27.578,"lo":-48.522,"k":0,"z":5,"s":3},{"n":"Feira de Santana","a":"Brazil","i":"BR","p":481911,"la":-12.25,"lo":-38.97,"k":0,"z":5,"s":3},{"n":"Winnipeg","a":"Canada","i":"CA","p":632063,"la":49.883,"lo":-97.166,"k":0,"z":4,"s":3},{"n":"Churchill","a":"Canada","i":"CA","p":1000,"la":58.766,"lo":-94.166,"k":0,"z":4,"s":3},{"n":"Regina","a":"Canada","i":"CA","p":176183,"la":50.45,"lo":-104.617,"k":0,"z":5,"s":3},{"n":"Saskatoon","a":"Canada","i":"CA","p":198958,"la":52.17,"lo":-106.67,"k":0,"z":5,"s":3},{"n":"Calgary","a":"Canada","i":"CA","p":1110000,"la":51.085,"lo":-114.082,"k":0,"z":4,"s":3},{"n":"Prince Rupert","a":"Canada","i":"CA","p":14708,"la":54.317,"lo":-130.33,"k":0,"z":4,"s":3},{"n":"Victoria","a":"Canada","i":"CA","p":289625,"la":48.433,"lo":-123.35,"k":0,"z":5,"s":3},{"n":"Arctic Bay","a":"Canada","i":"CA","p":604,"la":73.033,"lo":-85.167,"k":0,"z":5,"s":3},{"n":"Resolute","a":"Canada","i":"CA","p":250,"la":74.683,"lo":-94.9,"k":0,"z":5,"s":3},{"n":"Naujaat","a":"Canada","i":"CA","p":1000,"la":66.529,"lo":-86.283,"k":0,"z":4,"s":3},{"n":"Yellowknife","a":"Canada","i":"CA","p":19234,"la":62.442,"lo":-114.397,"k":0,"z":4,"s":3},{"n":"Fort Good Hope","a":"Canada","i":"CA","p":597,"la":66.267,"lo":-128.633,"k":0,"z":5,"s":3},{"n":"Whitehorse","a":"Canada","i":"CA","p":23276,"la":60.717,"lo":-135.05,"k":0,"z":4,"s":3},{"n":"Boa Vista","a":"Brazil","i":"BR","p":235150,"la":2.816,"lo":-60.666,"k":0,"z":5,"s":3},{"n":"Macapá","a":"Brazil","i":"BR","p":499166,"la":0.033,"lo":-51.05,"k":0,"z":5,"s":3},{"n":"Ottawa","a":"Canada","i":"CA","p":1145000,"la":45.419,"lo":-75.702,"k":1,"z":3,"s":3},{"n":"Fort Severn","a":"Canada","i":"CA","p":125,"la":55.983,"lo":-87.65,"k":0,"z":5,"s":3},{"n":"Thunder Bay","a":"Canada","i":"CA","p":99334,"la":48.446,"lo":-89.275,"k":0,"z":5,"s":3},{"n":"Québec","a":"Canada","i":"CA","p":624177,"la":46.84,"lo":-71.246,"k":0,"z":4,"s":3},{"n":"Halifax","a":"Canada","i":"CA","p":359111,"la":44.65,"lo":-63.6,"k":0,"z":4,"s":3},{"n":"St. John's","a":"Canada","i":"CA","p":131469,"la":47.585,"lo":-52.681,"k":0,"z":4,"s":3},{"n":"Nain","a":"Canada","i":"CA","p":1151,"la":56.547,"lo":-61.686,"k":0,"z":5,"s":3},{"n":"Charlottetown","a":"Canada","i":"CA","p":42402,"la":46.249,"lo":-63.131,"k":0,"z":5,"s":3},{"n":"N'Délé","a":"Central African Republic","i":"CF","p":11764,"la":8.409,"lo":20.653,"k":0,"z":5,"s":3},{"n":"Belgrade","a":"Serbia","i":"RS","p":1099000,"la":44.821,"lo":20.466,"k":1,"z":5,"s":3},{"n":"Obo","a":"Central African Republic","i":"CF","p":12887,"la":5.4,"lo":26.5,"k":0,"z":5,"s":3},{"n":"Bandar Seri Begawan","a":"Brunei","i":"BN","p":296500,"la":4.883,"lo":114.933,"k":1,"z":5,"s":3},{"n":"Puerto Deseado","a":"Argentina","i":"AR","p":3305,"la":-47.75,"lo":-65.9,"k":0,"z":5,"s":3},{"n":"Río Gallegos","a":"Argentina","i":"AR","p":85700,"la":-51.633,"lo":-69.217,"k":0,"z":5,"s":3},{"n":"Comodoro Rivadavia","a":"Argentina","i":"AR","p":140850,"la":-45.87,"lo":-67.5,"k":0,"z":5,"s":3},{"n":"Mendoza","a":"Argentina","i":"AR","p":893000,"la":-32.881,"lo":-68.819,"k":0,"z":5,"s":3},{"n":"Sucre","a":"Bolivia","i":"BO","p":224838,"la":-19.041,"lo":-65.26,"k":1,"z":4.7,"s":3},{"n":"Riberalta","a":"Bolivia","i":"BO","p":74014,"la":-10.983,"lo":-66.1,"k":0,"z":5,"s":3},{"n":"Bahía Blanca","a":"Argentina","i":"AR","p":281536,"la":-38.74,"lo":-62.265,"k":0,"z":5,"s":3},{"n":"Mar del Plata","a":"Argentina","i":"AR","p":555897,"la":-38,"lo":-57.58,"k":0,"z":5,"s":3},{"n":"Córdoba","a":"Argentina","i":"AR","p":1452000,"la":-31.398,"lo":-64.184,"k":0,"z":4,"s":3},{"n":"Posadas","a":"Argentina","i":"AR","p":357119,"la":-27.358,"lo":-55.885,"k":0,"z":5,"s":3},{"n":"Belmopan","a":"Belize","i":"BZ","p":15220,"la":17.252,"lo":-88.767,"k":1,"z":5,"s":3},{"n":"Bangui","a":"Central African Republic","i":"CF","p":831925,"la":4.367,"lo":18.558,"k":1,"z":5,"s":3},{"n":"Maroua","a":"Cameroon","i":"CM","p":319941,"la":10.596,"lo":14.325,"k":0,"z":5,"s":3},{"n":"Yaoundé","a":"Cameroon","i":"CM","p":1611000,"la":3.869,"lo":11.515,"k":1,"z":5,"s":3},{"n":"Tirana","a":"Albania","i":"AL","p":895350,"la":41.328,"lo":19.819,"k":1,"z":5,"s":3},{"n":"Yerevan","a":"Armenia","i":"AM","p":1102000,"la":40.183,"lo":44.512,"k":1,"z":5,"s":3},{"n":"Baku","a":"Azerbaijan","i":"AZ","p":2122300,"la":40.397,"lo":49.86,"k":1,"z":5,"s":3},{"n":"Kandahar","a":"Afghanistan","i":"AF","p":715542,"la":31.61,"lo":65.695,"k":0,"z":5,"s":3},{"n":"Phnom Penh","a":"Cambodia","i":"KH","p":1466000,"la":11.552,"lo":104.915,"k":1,"z":5,"s":3},{"n":"Menongue","a":"Angola","i":"AO","p":13030,"la":-14.667,"lo":17.7,"k":0,"z":5,"s":3},{"n":"Huambo","a":"Angola","i":"AO","p":1100000,"la":-12.748,"lo":15.758,"k":0,"z":5,"s":3},{"n":"La Paz","a":"Bolivia","i":"BO","p":1590000,"la":-16.496,"lo":-68.152,"k":1,"z":4,"s":3},{"n":"Santa Cruz","a":"Bolivia","i":"BO","p":2102998,"la":-17.752,"lo":-63.228,"k":0,"z":5,"s":3},{"n":"Oran","a":"Algeria","i":"DZ","p":798000,"la":35.712,"lo":-0.622,"k":0,"z":5,"s":3},{"n":"Cotonou","a":"Benin","i":"BJ","p":762000,"la":6.363,"lo":2.404,"k":1,"z":5,"s":3},{"n":"Tamanrasset","a":"Algeria","i":"DZ","p":76000,"la":22.785,"lo":5.523,"k":0,"z":5,"s":3},{"n":"Ghardaia","a":"Algeria","i":"DZ","p":125480,"la":32.49,"lo":3.67,"k":0,"z":5,"s":3},{"n":"Sofia","a":"Bulgaria","i":"BG","p":1185000,"la":42.685,"lo":23.315,"k":1,"z":5,"s":3},{"n":"Minsk","a":"Belarus","i":"BY","p":1805000,"la":53.902,"lo":27.565,"k":1,"z":5,"s":3},{"n":"Thimphu","a":"Bhutan","i":"BT","p":98676,"la":27.473,"lo":89.639,"k":1,"z":5,"s":3},{"n":"Gaborone","a":"Botswana","i":"BW","p":208411,"la":-24.646,"lo":25.912,"k":1,"z":5,"s":3},{"n":"Darwin","a":"Australia","i":"AU","p":93080,"la":-12.425,"lo":130.85,"k":0,"z":3.7,"s":3},{"n":"Alice Springs","a":"Australia","i":"AU","p":27710,"la":-23.701,"lo":133.88,"k":0,"z":5,"s":3},{"n":"Canberra","a":"Australia","i":"AU","p":327700,"la":-35.283,"lo":149.129,"k":1,"z":4,"s":3},{"n":"Newcastle","a":"Australia","i":"AU","p":540796,"la":-32.93,"lo":151.77,"k":0,"z":5,"s":3},{"n":"Adelaide","a":"Australia","i":"AU","p":1145000,"la":-34.933,"lo":138.598,"k":0,"z":3.7,"s":3},{"n":"Townsville","a":"Australia","i":"AU","p":138954,"la":-19.25,"lo":146.77,"k":0,"z":5,"s":3},{"n":"Brisbane","a":"Australia","i":"AU","p":1860000,"la":-27.453,"lo":153.033,"k":0,"z":4,"s":3},{"n":"Hobart","a":"Australia","i":"AU","p":80870,"la":-42.85,"lo":147.295,"k":0,"z":5,"s":3},{"n":"Ouagadougou","a":"Burkina Faso","i":"BF","p":1149000,"la":12.372,"lo":-1.527,"k":1,"z":5,"s":3},{"n":"Sarajevo","a":"Bosnia and Herzegovina","i":"BA","p":696731,"la":43.85,"lo":18.383,"k":1,"z":5,"s":3},{"n":"Naypyidaw","a":"Myanmar","i":"MM","p":930000,"la":19.769,"lo":96.117,"k":1,"z":5,"s":3},{"n":"San Juan","a":"Puerto Rico","i":"PR","p":2478905,"la":18.44,"lo":-66.13,"k":0,"z":5,"s":3},{"n":"Stanley","a":"Falkland Islands","i":"FK","p":2213,"la":-51.7,"lo":-57.85,"k":0,"z":5,"s":3},{"n":"Hamilton","a":"Bermuda","i":"BM","p":52320,"la":32.294,"lo":-64.784,"k":1,"z":5,"s":3},{"n":"Nuku'alofa","a":"Tonga","i":"TO","p":42620,"la":-21.139,"lo":-175.221,"k":1,"z":5,"s":3},{"n":"Hargeisa","a":"Somaliland","i":"-99","p":477876,"la":9.56,"lo":44.065,"k":1,"z":5,"s":3},{"n":"Victoria","a":"Seychelles","i":"SC","p":33576,"la":-4.617,"lo":55.45,"k":1,"z":5,"s":3},{"n":"São Tomé","a":"Sao Tome and Principe","i":"ST","p":88219,"la":0.337,"lo":6.73,"k":1,"z":5,"s":3},{"n":"Apia","a":"Samoa","i":"WS","p":61916,"la":-13.836,"lo":-171.769,"k":1,"z":5,"s":3},{"n":"Valletta","a":"Malta","i":"MT","p":368250,"la":35.9,"lo":14.515,"k":1,"z":5,"s":3},{"n":"Malé","a":"Maldives","i":"MV","p":112927,"la":4.172,"lo":73.509,"k":1,"z":5,"s":3},{"n":"Jerusalem","a":"Israel","i":"IL","p":1029300,"la":31.778,"lo":35.207,"k":1,"z":3.7,"s":3},{"n":"Praia","a":"Cape Verde","i":"CV","p":113364,"la":14.917,"lo":-23.517,"k":1,"z":5,"s":3},{"n":"Nassau","a":"The Bahamas","i":"BS","p":227940,"la":25.083,"lo":-77.35,"k":1,"z":5,"s":3},{"n":"Nicosia","a":"Cyprus","i":"CY","p":224300,"la":35.167,"lo":33.367,"k":1,"z":5,"s":3},{"n":"Kaohsiung","a":"Taiwan","i":"TW","p":2769072,"la":22.633,"lo":120.267,"k":0,"z":5,"s":3},{"n":"Wellington","a":"New Zealand","i":"NZ","p":393400,"la":-41.292,"lo":174.777,"k":1,"z":4,"s":3},{"n":"Christchurch","a":"New Zealand","i":"NZ","p":363200,"la":-43.535,"lo":172.63,"k":0,"z":4,"s":3},{"n":"Shenzhen","a":"China","i":"CN","p":7581000,"la":22.548,"lo":114.061,"k":0,"z":4.7,"s":2},{"n":"Zibo","a":"China","i":"CN","p":3061000,"la":36.821,"lo":117.996,"k":0,"z":4.7,"s":2},{"n":"Minneapolis","a":"United States of America","i":"US","p":2616000,"la":44.982,"lo":-93.254,"k":0,"z":4,"s":2},{"n":"Honolulu","a":"United States of America","i":"US","p":786000,"la":21.303,"lo":-157.858,"k":0,"z":4,"s":2},{"n":"Seattle","a":"United States of America","i":"US","p":3074000,"la":47.6,"lo":-122.315,"k":0,"z":4,"s":2},{"n":"Phoenix","a":"United States of America","i":"US","p":3551000,"la":33.448,"lo":-112.067,"k":0,"z":4,"s":2},{"n":"San Diego","a":"United States of America","i":"US","p":2916000,"la":32.719,"lo":-117.151,"k":0,"z":4,"s":2},{"n":"St. Louis","a":"United States of America","i":"US","p":2199000,"la":38.637,"lo":-90.242,"k":0,"z":4,"s":2},{"n":"New Orleans","a":"United States of America","i":"US","p":785000,"la":29.969,"lo":-90.088,"k":0,"z":4,"s":2},{"n":"Dallas","a":"United States of America","i":"US","p":4798000,"la":32.772,"lo":-96.795,"k":0,"z":4,"s":2},{"n":"Maracaibo","a":"Venezuela","i":"VE","p":2072000,"la":10.732,"lo":-71.662,"k":0,"z":4,"s":2},{"n":"Boston","a":"United States of America","i":"US","p":4467000,"la":42.332,"lo":-71.072,"k":0,"z":4,"s":2},{"n":"Tampa","a":"United States of America","i":"US","p":2314000,"la":27.949,"lo":-82.461,"k":0,"z":5,"s":2},{"n":"Philadelphia","a":"United States of America","i":"US","p":5492000,"la":39.946,"lo":-75.18,"k":0,"z":5,"s":2},{"n":"Detroit","a":"United States of America","i":"US","p":4101000,"la":42.334,"lo":-83.051,"k":0,"z":4,"s":2},{"n":"Anchorage","a":"United States of America","i":"US","p":260283,"la":61.214,"lo":-149.887,"k":0,"z":3,"s":2},{"n":"Hanoi","a":"Vietnam","i":"VN","p":4378000,"la":21.035,"lo":105.848,"k":1,"z":4,"s":2},{"n":"Ho Chi Minh City","a":"Vietnam","i":"VN","p":5314000,"la":10.762,"lo":106.703,"k":0,"z":4,"s":2},{"n":"Ankara","a":"Turkey","i":"TR","p":3716000,"la":39.929,"lo":32.862,"k":1,"z":4,"s":2},{"n":"Budapest","a":"Hungary","i":"HU","p":1679000,"la":47.502,"lo":19.081,"k":1,"z":4.7,"s":2},{"n":"Sanaa","a":"Yemen","i":"YE","p":2008000,"la":15.357,"lo":44.205,"k":1,"z":4,"s":2},{"n":"Barcelona","a":"Spain","i":"ES","p":4920000,"la":41.385,"lo":2.181,"k":0,"z":4,"s":2},{"n":"Bucharest","a":"Romania","i":"RO","p":1942000,"la":44.435,"lo":26.098,"k":1,"z":4,"s":2},{"n":"Aleppo","a":"Syria","i":"SY","p":2738000,"la":36.232,"lo":37.168,"k":0,"z":4.7,"s":2},{"n":"Damascus","a":"Syria","i":"SY","p":2466000,"la":33.502,"lo":36.298,"k":1,"z":4.7,"s":2},{"n":"Zürich","a":"Switzerland","i":"CH","p":1108000,"la":47.382,"lo":8.548,"k":0,"z":4.7,"s":2},{"n":"Lisbon","a":"Portugal","i":"PT","p":2812000,"la":38.725,"lo":-9.147,"k":1,"z":4,"s":2},{"n":"Khartoum","a":"Sudan","i":"SD","p":4754000,"la":15.59,"lo":32.532,"k":1,"z":4,"s":2},{"n":"Jeddah","a":"Saudi Arabia","i":"SA","p":3012000,"la":21.519,"lo":39.217,"k":0,"z":4,"s":2},{"n":"Makkah","a":"Saudi Arabia","i":"SA","p":1385000,"la":21.432,"lo":39.818,"k":0,"z":3.7,"s":2},{"n":"Oslo","a":"Norway","i":"NO","p":835000,"la":59.919,"lo":10.748,"k":1,"z":4,"s":2},{"n":"Lahore","a":"Pakistan","i":"PK","p":6577000,"la":31.562,"lo":74.348,"k":0,"z":4,"s":2},{"n":"Karachi","a":"Pakistan","i":"PK","p":12130000,"la":24.872,"lo":66.988,"k":0,"z":4,"s":2},{"n":"Durban","a":"South Africa","i":"ZA","p":2729000,"la":-29.861,"lo":31.011,"k":0,"z":4,"s":2},{"n":"St. Petersburg","a":"Russia","i":"RU","p":4553000,"la":59.941,"lo":30.314,"k":0,"z":3.7,"s":2},{"n":"Guadalajara","a":"Mexico","i":"MX","p":4198000,"la":20.672,"lo":-103.332,"k":0,"z":4,"s":2},{"n":"Puebla","a":"Mexico","i":"MX","p":2195000,"la":19.033,"lo":-98.204,"k":0,"z":4,"s":2},{"n":"Kano","a":"Nigeria","i":"NG","p":3140000,"la":12.002,"lo":8.518,"k":0,"z":4.7,"s":2},{"n":"Warsaw","a":"Poland","i":"PL","p":1707000,"la":52.231,"lo":21.005,"k":1,"z":4,"s":2},{"n":"Pyongyang","a":"North Korea","i":"KP","p":3300000,"la":39.021,"lo":125.753,"k":1,"z":4.7,"s":2},{"n":"Dar es Salaam","a":"Tanzania","i":"TZ","p":2930000,"la":-6.798,"lo":39.266,"k":1,"z":4,"s":2},{"n":"Medan","a":"Indonesia","i":"ID","p":2115000,"la":3.582,"lo":98.648,"k":0,"z":4,"s":2},{"n":"Dublin","a":"Ireland","i":"IE","p":1059000,"la":53.347,"lo":-6.257,"k":1,"z":4,"s":2},{"n":"Monrovia","a":"Liberia","i":"LR","p":1041000,"la":6.315,"lo":-10.8,"k":1,"z":4,"s":2},{"n":"Naples","a":"Italy","i":"IT","p":2250000,"la":40.842,"lo":14.243,"k":0,"z":4.7,"s":2},{"n":"Milan","a":"Italy","i":"IT","p":2945000,"la":45.472,"lo":9.203,"k":0,"z":4.7,"s":2},{"n":"Kuala Lumpur","a":"Malaysia","i":"MY","p":1448000,"la":3.14,"lo":101.689,"k":1,"z":4,"s":2},{"n":"Lanzhou","a":"China","i":"CN","p":2561000,"la":36.058,"lo":103.79,"k":0,"z":4,"s":2},{"n":"Nanning","a":"China","i":"CN","p":2167000,"la":22.822,"lo":108.318,"k":0,"z":4,"s":2},{"n":"Guiyang","a":"China","i":"CN","p":3662000,"la":26.582,"lo":106.718,"k":0,"z":4.7,"s":2},{"n":"Chongqing","a":"China","i":"CN","p":6461000,"la":29.567,"lo":106.593,"k":0,"z":4,"s":2},{"n":"Fuzhou","a":"China","i":"CN","p":2606000,"la":26.082,"lo":119.298,"k":0,"z":4.7,"s":2},{"n":"Guangzhou","a":"China","i":"CN","p":8829000,"la":23.125,"lo":113.262,"k":0,"z":4,"s":2},{"n":"Dongguan","a":"China","i":"CN","p":4528000,"la":23.022,"lo":113.75,"k":0,"z":5.6,"s":2},{"n":"Bandung","a":"Indonesia","i":"ID","p":2394000,"la":-6.948,"lo":107.568,"k":0,"z":4.7,"s":2},{"n":"Surabaya","a":"Indonesia","i":"ID","p":2845000,"la":-7.247,"lo":112.749,"k":0,"z":4,"s":2},{"n":"Guayaquil","a":"Ecuador","i":"EC","p":2514000,"la":-2.218,"lo":-79.922,"k":0,"z":4.7,"s":2},{"n":"Medellín","a":"Colombia","i":"CO","p":3297000,"la":6.277,"lo":-75.577,"k":0,"z":4,"s":2},{"n":"Cali","a":"Colombia","i":"CO","p":2254000,"la":3.402,"lo":-76.502,"k":0,"z":4,"s":2},{"n":"Havana","a":"Cuba","i":"CU","p":2174000,"la":23.134,"lo":-82.366,"k":1,"z":4,"s":2},{"n":"Alexandria","a":"Egypt","i":"EG","p":4165000,"la":31.202,"lo":29.948,"k":0,"z":4,"s":2},{"n":"Frankfurt","a":"Germany","i":"DE","p":2895000,"la":50.1,"lo":8.675,"k":0,"z":4,"s":2},{"n":"Hamburg","a":"Germany","i":"DE","p":1757000,"la":53.552,"lo":9.998,"k":0,"z":5,"s":2},{"n":"Munich","a":"Germany","i":"DE","p":1275000,"la":48.132,"lo":11.573,"k":0,"z":4.7,"s":2},{"n":"Prague","a":"Czechia","i":"CZ","p":1162000,"la":50.087,"lo":14.423,"k":1,"z":4.7,"s":2},{"n":"Kuwait City","a":"Kuwait","i":"KW","p":2063000,"la":29.372,"lo":47.976,"k":1,"z":4,"s":2},{"n":"Xian","a":"China","i":"CN","p":4009000,"la":34.277,"lo":108.893,"k":0,"z":3.7,"s":2},{"n":"Taiyuan","a":"China","i":"CN","p":2913000,"la":37.877,"lo":112.543,"k":0,"z":4,"s":2},{"n":"Wuhan","a":"China","i":"CN","p":7243000,"la":30.582,"lo":114.268,"k":0,"z":4,"s":2},{"n":"Changsha","a":"China","i":"CN","p":2604000,"la":28.202,"lo":112.968,"k":0,"z":4.7,"s":2},{"n":"Kunming","a":"China","i":"CN","p":2931000,"la":25.043,"lo":102.697,"k":0,"z":4,"s":2},{"n":"Zhengzhou","a":"China","i":"CN","p":2636000,"la":34.757,"lo":113.663,"k":0,"z":4,"s":2},{"n":"Shenyeng","a":"China","i":"CN","p":4787000,"la":41.807,"lo":123.448,"k":0,"z":4,"s":2},{"n":"Jinan","a":"China","i":"CN","p":2798000,"la":36.677,"lo":116.993,"k":0,"z":4,"s":2},{"n":"Tianjin","a":"China","i":"CN","p":7180000,"la":39.083,"lo":117.197,"k":0,"z":4,"s":2},{"n":"Nanchang","a":"China","i":"CN","p":2350000,"la":28.682,"lo":115.878,"k":0,"z":4.7,"s":2},{"n":"Nanjing","a":"China","i":"CN","p":3679000,"la":32.052,"lo":118.778,"k":0,"z":4,"s":2},{"n":"Hangzhou","a":"China","i":"CN","p":3007000,"la":30.252,"lo":120.168,"k":0,"z":4,"s":2},{"n":"Hiroshima","a":"Japan","i":"JP","p":2045000,"la":34.39,"lo":132.441,"k":0,"z":4,"s":2},{"n":"Changchun","a":"China","i":"CN","p":3183000,"la":43.867,"lo":125.338,"k":0,"z":4,"s":2},{"n":"Baotou","a":"China","i":"CN","p":2036000,"la":40.654,"lo":109.82,"k":0,"z":4,"s":2},{"n":"Harbin","a":"China","i":"CN","p":3621000,"la":45.752,"lo":126.648,"k":0,"z":3.7,"s":2},{"n":"Sapporo","a":"Japan","i":"JP","p":2544000,"la":43.077,"lo":141.338,"k":0,"z":4,"s":2},{"n":"Santo Domingo","a":"Dominican Republic","i":"DO","p":2154000,"la":18.471,"lo":-69.93,"k":1,"z":4,"s":2},{"n":"Accra","a":"Ghana","i":"GH","p":2121000,"la":5.552,"lo":-0.219,"k":1,"z":4,"s":2},{"n":"Delhi","a":"India","i":"IN","p":15926000,"la":28.672,"lo":77.228,"k":0,"z":4,"s":2},{"n":"Hyderabad","a":"India","i":"IN","p":6376000,"la":17.402,"lo":78.478,"k":0,"z":4,"s":2},{"n":"Pune","a":"India","i":"IN","p":4672000,"la":18.516,"lo":73.852,"k":0,"z":4,"s":2},{"n":"Nagpur","a":"India","i":"IN","p":2454000,"la":21.172,"lo":79.088,"k":0,"z":4,"s":2},{"n":"Tripoli","a":"Libya","i":"LY","p":2189000,"la":32.893,"lo":13.18,"k":1,"z":4,"s":2},{"n":"Tel Aviv","a":"Israel","i":"IL","p":3112000,"la":32.082,"lo":34.768,"k":0,"z":4,"s":2},{"n":"Helsinki","a":"Finland","i":"FI","p":1115000,"la":60.164,"lo":24.932,"k":1,"z":4,"s":2},{"n":"Mashhad","a":"Iran","i":"IR","p":2469000,"la":36.272,"lo":59.568,"k":0,"z":4,"s":2},{"n":"Jaipur","a":"India","i":"IN","p":2917000,"la":26.923,"lo":75.808,"k":0,"z":4,"s":2},{"n":"Kanpur","a":"India","i":"IN","p":3162000,"la":26.462,"lo":80.318,"k":0,"z":4,"s":2},{"n":"Patna","a":"India","i":"IN","p":2158000,"la":25.627,"lo":85.128,"k":0,"z":4.7,"s":2},{"n":"Chennai","a":"India","i":"IN","p":7163000,"la":13.092,"lo":80.278,"k":0,"z":4,"s":2},{"n":"Ahmedabad","a":"India","i":"IN","p":5375000,"la":23.032,"lo":72.578,"k":0,"z":4.7,"s":2},{"n":"Surat","a":"India","i":"IN","p":3842000,"la":21.202,"lo":72.838,"k":0,"z":4,"s":2},{"n":"København","a":"Denmark","i":"DK","p":1085000,"la":55.681,"lo":12.562,"k":1,"z":4,"s":2},{"n":"Abidjan","a":"Ivory Coast","i":"CI","p":3802000,"la":5.323,"lo":-4.02,"k":1,"z":4,"s":2},{"n":"Belém","a":"Brazil","i":"BR","p":2167000,"la":-1.448,"lo":-48.482,"k":0,"z":4,"s":2},{"n":"Brasília","a":"Brazil","i":"BR","p":3716996,"la":-15.781,"lo":-47.918,"k":1,"z":3.7,"s":2},{"n":"Porto Alegre","a":"Brazil","i":"BR","p":3917000,"la":-30.048,"lo":-51.202,"k":0,"z":4,"s":2},{"n":"Curitiba","a":"Brazil","i":"BR","p":3084000,"la":-25.418,"lo":-49.322,"k":0,"z":4,"s":2},{"n":"Fortaleza","a":"Brazil","i":"BR","p":3602319,"la":-3.748,"lo":-38.582,"k":0,"z":4,"s":2},{"n":"Salvador","a":"Brazil","i":"BR","p":3484000,"la":-12.968,"lo":-38.482,"k":0,"z":3.7,"s":2},{"n":"Edmonton","a":"Canada","i":"CA","p":1058000,"la":53.552,"lo":-113.502,"k":0,"z":4,"s":2},{"n":"Montréal","a":"Canada","i":"CA","p":3678000,"la":45.502,"lo":-73.585,"k":0,"z":4,"s":2},{"n":"Goiânia","a":"Brazil","i":"BR","p":2022000,"la":-16.718,"lo":-49.302,"k":0,"z":4.7,"s":2},{"n":"Recife","a":"Brazil","i":"BR","p":3651000,"la":-8.061,"lo":-34.909,"k":0,"z":3.7,"s":2},{"n":"Brussels","a":"Belgium","i":"BE","p":1743000,"la":50.835,"lo":4.331,"k":1,"z":4.7,"s":2},{"n":"Dhaka","a":"Bangladesh","i":"BD","p":12797394,"la":23.725,"lo":90.407,"k":1,"z":4,"s":2},{"n":"Luanda","a":"Angola","i":"AO","p":5172900,"la":-8.836,"lo":13.232,"k":1,"z":4,"s":2},{"n":"Algiers","a":"Algeria","i":"DZ","p":3354000,"la":36.765,"lo":3.049,"k":1,"z":4,"s":2},{"n":"Chattogram","a":"Bangladesh","i":"BD","p":4529000,"la":22.332,"lo":91.798,"k":0,"z":4,"s":2},{"n":"Perth","a":"Australia","i":"AU","p":1532000,"la":-31.953,"lo":115.838,"k":0,"z":3.7,"s":2},{"n":"Yangon","a":"Myanmar","i":"MM","p":4088000,"la":16.785,"lo":96.165,"k":0,"z":4,"s":2},{"n":"San Francisco","a":"United States of America","i":"US","p":3450000,"la":37.784,"lo":-122.4,"k":0,"z":2.7,"s":1},{"n":"Denver","a":"United States of America","i":"US","p":2313000,"la":39.741,"lo":-104.986,"k":0,"z":3.7,"s":1},{"n":"Houston","a":"United States of America","i":"US","p":4459000,"la":29.741,"lo":-95.348,"k":0,"z":3,"s":1},{"n":"Miami","a":"United States of America","i":"US","p":5585000,"la":25.79,"lo":-80.226,"k":0,"z":2.1,"s":1},{"n":"Atlanta","a":"United States of America","i":"US","p":4506000,"la":33.739,"lo":-84.368,"k":0,"z":3,"s":1},{"n":"Chicago","a":"United States of America","i":"US","p":8990000,"la":41.848,"lo":-87.635,"k":0,"z":3,"s":1},{"n":"Caracas","a":"Venezuela","i":"VE","p":2985000,"la":10.503,"lo":-66.919,"k":1,"z":3,"s":1},{"n":"Kyiv","a":"Ukraine","i":"UA","p":2709000,"la":50.435,"lo":30.515,"k":1,"z":3.7,"s":1},{"n":"Dubai","a":"United Arab Emirates","i":"AE","p":1379000,"la":25.215,"lo":55.287,"k":0,"z":3.7,"s":1},{"n":"Tashkent","a":"Uzbekistan","i":"UZ","p":2184000,"la":41.304,"lo":69.269,"k":1,"z":3.7,"s":1},{"n":"Madrid","a":"Spain","i":"ES","p":5567000,"la":40.402,"lo":-3.685,"k":1,"z":3,"s":1},{"n":"Geneva","a":"Switzerland","i":"CH","p":1240000,"la":46.21,"lo":6.14,"k":0,"z":3.7,"s":1},{"n":"Stockholm","a":"Sweden","i":"SE","p":1264000,"la":59.324,"lo":18.066,"k":1,"z":3,"s":1},{"n":"Bangkok","a":"Thailand","i":"TH","p":6704000,"la":13.752,"lo":100.515,"k":1,"z":3,"s":1},{"n":"Lima","a":"Peru","i":"PE","p":8012000,"la":-12.046,"lo":-77.052,"k":1,"z":3,"s":1},{"n":"Dakar","a":"Senegal","i":"SN","p":2604000,"la":14.718,"lo":-17.475,"k":1,"z":3,"s":1},{"n":"Johannesburg","a":"South Africa","i":"ZA","p":3435000,"la":-26.168,"lo":28.028,"k":0,"z":3,"s":1},{"n":"Amsterdam","a":"Netherlands","i":"NL","p":1031000,"la":52.352,"lo":4.915,"k":1,"z":3.7,"s":1},{"n":"Casablanca","a":"Morocco","i":"MA","p":3181000,"la":33.602,"lo":-7.618,"k":0,"z":3.7,"s":1},{"n":"Seoul","a":"South Korea","i":"KR","p":9796000,"la":37.568,"lo":126.998,"k":1,"z":3,"s":1},{"n":"Manila","a":"Philippines","i":"PH","p":11100000,"la":14.606,"lo":120.98,"k":1,"z":2.7,"s":1},{"n":"Monterrey","a":"Mexico","i":"MX","p":3712000,"la":25.672,"lo":-100.332,"k":0,"z":3.7,"s":1},{"n":"Berlin","a":"Germany","i":"DE","p":3406000,"la":52.524,"lo":13.4,"k":1,"z":2.7,"s":1},{"n":"Ürümqi","a":"China","i":"CN","p":3575000,"la":43.807,"lo":87.573,"k":0,"z":3.7,"s":1},{"n":"Chengdu","a":"China","i":"CN","p":4123000,"la":30.672,"lo":104.068,"k":0,"z":3,"s":1},{"n":"Ōsaka","a":"Japan","i":"JP","p":11294000,"la":34.691,"lo":135.504,"k":0,"z":3,"s":1},{"n":"Kinshasa","a":"Congo (Kinshasa)","i":"CD","p":7843000,"la":-4.328,"lo":15.313,"k":1,"z":3,"s":1},{"n":"New Delhi","a":"India","i":"IN","p":317797,"la":28.6,"lo":77.2,"k":1,"z":2.1,"s":1},{"n":"Bengaluru","a":"India","i":"IN","p":6787000,"la":12.972,"lo":77.558,"k":0,"z":3.7,"s":1},{"n":"Athens","a":"Greece","i":"GR","p":3242000,"la":37.985,"lo":23.731,"k":1,"z":3.7,"s":1},{"n":"Baghdad","a":"Iraq","i":"IQ","p":5054000,"la":33.341,"lo":44.392,"k":1,"z":3.7,"s":1},{"n":"Addis Ababa","a":"Ethiopia","i":"ET","p":3100000,"la":9.035,"lo":38.698,"k":1,"z":3,"s":1},{"n":"Tehran","a":"Iran","i":"IR","p":7873000,"la":35.674,"lo":51.422,"k":1,"z":3,"s":1},{"n":"Vancouver","a":"Canada","i":"CA","p":2313328,"la":49.275,"lo":-123.124,"k":0,"z":2.1,"s":1},{"n":"Toronto","a":"Canada","i":"CA","p":5213000,"la":43.665,"lo":-79.389,"k":0,"z":2.1,"s":1},{"n":"Buenos Aires","a":"Argentina","i":"AR","p":12795000,"la":-34.611,"lo":-58.433,"k":1,"z":2.1,"s":1},{"n":"Kabul","a":"Afghanistan","i":"AF","p":3277000,"la":34.519,"lo":69.181,"k":1,"z":3.7,"s":1},{"n":"Vienna","a":"Austria","i":"AT","p":2400000,"la":48.202,"lo":16.365,"k":1,"z":3.7,"s":1},{"n":"Melbourne","a":"Australia","i":"AU","p":4170000,"la":-37.818,"lo":144.973,"k":0,"z":3,"s":1},{"n":"Taipei","a":"Taiwan","i":"TW","p":6900273,"la":25.036,"lo":121.568,"k":1,"z":3.7,"s":1},{"n":"Auckland","a":"New Zealand","i":"NZ","p":1377200,"la":-36.848,"lo":174.763,"k":0,"z":3,"s":1},{"n":"Los Angeles","a":"United States of America","i":"US","p":12500000,"la":34.049,"lo":-118.232,"k":0,"z":2,"s":0},{"n":"Washington, D.C.","a":"United States of America","i":"US","p":4338000,"la":38.901,"lo":-77.011,"k":1,"z":2.1,"s":0},{"n":"New York","a":"United States of America","i":"US","p":19040000,"la":40.722,"lo":-73.996,"k":0,"z":1.7,"s":0},{"n":"London","a":"United Kingdom","i":"GB","p":8567000,"la":51.502,"lo":-0.119,"k":1,"z":1.7,"s":0},{"n":"Istanbul","a":"Turkey","i":"TR","p":10061000,"la":41.018,"lo":28.974,"k":0,"z":3,"s":0},{"n":"Riyadh","a":"Saudi Arabia","i":"SA","p":4465000,"la":24.634,"lo":46.72,"k":1,"z":3.7,"s":0},{"n":"Cape Town","a":"South Africa","i":"ZA","p":3215000,"la":-33.918,"lo":18.433,"k":1,"z":1.7,"s":0},{"n":"Moscow","a":"Russia","i":"RU","p":10452000,"la":55.754,"lo":37.614,"k":1,"z":1.7,"s":0},{"n":"Mexico City","a":"Mexico","i":"MX","p":19028000,"la":19.444,"lo":-99.133,"k":1,"z":2.1,"s":0},{"n":"Lagos","a":"Nigeria","i":"NG","p":9466000,"la":6.445,"lo":3.39,"k":0,"z":2,"s":0},{"n":"Rome","a":"Italy","i":"IT","p":3339000,"la":41.898,"lo":12.481,"k":1,"z":2.7,"s":0},{"n":"Beijing","a":"China","i":"CN","p":11106000,"la":39.902,"lo":116.394,"k":1,"z":1.7,"s":0},{"n":"Nairobi","a":"Kenya","i":"KE","p":3010000,"la":-1.281,"lo":36.815,"k":1,"z":2,"s":0},{"n":"Jakarta","a":"Indonesia","i":"ID","p":9125000,"la":-6.172,"lo":106.827,"k":1,"z":2.1,"s":0},{"n":"Bogota","a":"Colombia","i":"CO","p":7772000,"la":4.598,"lo":-74.085,"k":1,"z":2.1,"s":0},{"n":"Cairo","a":"Egypt","i":"EG","p":11893000,"la":30.052,"lo":31.248,"k":1,"z":1.7,"s":0},{"n":"Shanghai","a":"China","i":"CN","p":14987000,"la":31.218,"lo":121.435,"k":0,"z":2,"s":0},{"n":"Tokyo","a":"Japan","i":"JP","p":35676000,"la":35.687,"lo":139.749,"k":1,"z":1.7,"s":0},{"n":"Mumbai","a":"India","i":"IN","p":18978000,"la":19.068,"lo":72.876,"k":0,"z":1.7,"s":0},{"n":"Paris","a":"France","i":"FR","p":9904000,"la":48.858,"lo":2.353,"k":1,"z":1.7,"s":0},{"n":"Santiago","a":"Chile","i":"CL","p":5720000,"la":-33.44,"lo":-70.651,"k":1,"z":2,"s":0},{"n":"Kolkata","a":"India","i":"IN","p":14787000,"la":22.57,"lo":88.369,"k":0,"z":3.7,"s":0},{"n":"Rio de Janeiro","a":"Brazil","i":"BR","p":11748000,"la":-22.907,"lo":-43.212,"k":0,"z":1.7,"s":0},{"n":"São Paulo","a":"Brazil","i":"BR","p":18845000,"la":-23.557,"lo":-46.627,"k":0,"z":3,"s":0},{"n":"Sydney","a":"Australia","i":"AU","p":4630000,"la":-33.871,"lo":151.213,"k":0,"z":1.7,"s":0},{"n":"Singapore","a":"Singapore","i":"SG","p":5183700,"la":1.295,"lo":103.854,"k":1,"z":2.1,"s":0},{"n":"Hong Kong","a":"Hong Kong S.A.R.","i":"HK","p":7206000,"la":22.307,"lo":114.183,"k":0,"z":3,"s":0}]} \ No newline at end of file diff --git a/src/maps/data/rivers_50m.json b/src/maps/data/rivers_50m.json new file mode 100644 index 0000000..669bdd0 --- /dev/null +++ b/src/maps/data/rivers_50m.json @@ -0,0 +1 @@ +{"f":[{"n":"Kama","z":5,"b":[49.248,55.113,51.937,55.701],"l":[[51.937,55.701,51.881,55.686,51.82,55.697,51.748,55.694,51.663,55.608,51.579,55.599,51.513,55.583,51.509,55.529,51.486,55.496,51.369,55.468,51.213,55.503,51.135,55.483,51.079,55.468,50.98,55.466,50.834,55.456,50.688,55.42,50.412,55.401,50.078,55.381,49.822,55.335,49.532,55.261,49.382,55.172,49.248,55.113]]},{"n":"Kama","z":5,"b":[51.936,55.695,56.686,60.5],"l":[[53.694,58.206,53.677,58.273,53.626,58.357,53.552,58.394,53.465,58.419,53.429,58.435,53.439,58.485,53.471,58.511,53.492,58.587,53.381,58.672,53.3,58.78,53.247,58.825,53.201,58.93,53.239,59.011,53.219,59.099,53.15,59.128,53.063,59.178,53.003,59.209,52.964,59.282,52.912,59.34,52.866,59.402,52.815,59.494,52.689,59.582,52.615,59.608,52.609,59.68,52.639,59.71,52.687,59.734,52.767,59.724,52.803,59.758,52.866,59.798,52.92,59.896,52.975,59.956,53.035,59.973,53.126,59.99,53.221,60.015,53.322,60.052,53.401,60.056,53.451,60.074,53.492,60.124,53.526,60.139,53.595,60.138,53.732,60.184,53.825,60.21,53.897,60.259,53.947,60.292,53.986,60.323,54.038,60.337,54.105,60.336,54.155,60.357,54.267,60.361,54.313,60.337,54.391,60.277,54.521,60.239,54.849,60.245,55.035,60.226,55.146,60.203,55.25,60.221,55.358,60.251,55.571,60.311,55.664,60.417,55.715,60.485,55.873,60.5,55.979,60.459,56.11,60.424,56.137,60.392,56.135,60.385,56.165,60.334,56.156,60.287,56.15,60.234,56.152,60.205,56.168,60.155,56.236,60.159,56.277,60.14,56.281,60.107,56.299,60.052,56.294,60.001,56.311,59.945,56.341,59.93,56.364,59.923,56.385,59.917,56.434,59.883,56.471,59.85,56.55,59.811,56.591,59.766,56.623,59.691,56.661,59.625,56.683,59.562,56.686,59.452,56.678,59.389,56.612,59.349,56.575,59.333,56.427,59.304,56.342,59.279,56.24,59.25,56.209,59.197,56.179,59.098,56.224,58.997,56.252,58.885,56.268,58.81,56.145,58.696,56.089,58.655,56.095,58.572,56.177,58.554,56.251,58.522,56.393,58.443,56.341,58.261,56.334,58.146,56.333,58.136,56.331,58.11,56.328,58.096,56.322,58.078,56.308,58.059,56.201,58.028,56.066,58.03,55.891,57.949,55.832,57.946,55.794,58.022,55.726,58.076,55.661,58.069,55.633,58.015,55.475,57.896,55.299,57.877,55.247,57.874,55.212,57.832,55.267,57.796,55.347,57.763,55.4,57.68,55.449,57.59,55.525,57.577,55.544,57.532,55.456,57.33,55.355,57.313,55.248,57.306,55.136,57.342,55.081,57.341,55.043,57.305,55.016,57.237,54.979,57.114,54.922,57.091,54.85,57.086,54.781,57.055,54.67,57.042,54.539,57.055,54.453,57.042,54.379,56.998,54.313,56.984,54.222,56.979,54.167,56.942,54.15,56.87,54.136,56.82,54.088,56.771,54.046,56.753,53.996,56.736,53.949,56.74,53.917,56.777,53.91,56.812,53.885,56.828,53.831,56.839,53.797,56.834,53.746,56.805,53.743,56.794,53.75,56.777,53.752,56.759,53.747,56.715,53.757,56.674,53.774,56.661,53.781,56.654,53.792,56.613,53.8,56.528,53.814,56.49,53.842,56.458,53.854,56.419,53.866,56.401,53.9,56.393,53.975,56.36,54.035,56.343,54.102,56.278,54.157,56.24,54.185,56.188,54.117,56.158,54.099,56.144,54.083,56.123,54.047,56.058,54.006,56.029,53.949,56.022,53.888,56.032,53.834,56.051,53.774,56.051,53.739,56.034,53.663,55.993,53.643,55.982,53.565,55.97,53.543,55.959,53.54,55.951,53.539,55.94,53.54,55.918,53.539,55.914,53.536,55.911,53.526,55.904,53.499,55.893,53.286,55.852,53.063,55.87,52.992,55.9,52.917,55.926,52.842,55.93,52.76,55.954,52.708,55.965,52.609,55.928,52.53,55.863,52.468,55.852,52.377,55.87,52.332,55.871,52.306,55.842,52.316,55.803,52.344,55.753,52.352,55.729,52.335,55.705,52.277,55.696,52.094,55.695,52.023,55.722,51.936,55.701]]},{"n":"Abay","z":3,"b":[37.113,11.632,37.387,11.906],"l":[[37.113,11.855,37.15,11.894,37.197,11.906,37.264,11.903,37.32,11.834,37.387,11.632]]},{"n":"Al Furat","z":3,"b":[38.066,35.863,38.561,36.342],"l":[[38.561,35.863,38.365,35.903,38.246,35.951,38.139,36.02,38.079,36.074,38.066,36.114,38.092,36.167,38.157,36.232,38.188,36.284,38.184,36.342]]},{"n":"Alabama","z":5,"b":[-86.522,32.811,-86.2,33.588],"l":[[-86.522,33.032,-86.521,33.014,-86.461,32.836,-86.445,32.811],[-86.2,33.588,-86.212,33.56,-86.33,33.444]]},{"n":"Albany","z":5,"b":[-91.97,50.998,-90.417,51.988],"l":[[-91.312,51.063,-91.075,50.998,-90.875,51.014,-90.634,51.087,-90.489,51.116,-90.417,51.093],[-91.598,51.162,-91.418,51.068],[-91.781,51.335,-91.886,51.458],[-91.915,51.762,-91.881,51.723,-91.885,51.67,-91.917,51.59,-91.923,51.534,-91.893,51.487],[-91.929,51.892,-91.97,51.988]]},{"n":"Albert Nile","z":2,"b":[31.356,2.193,31.475,2.4],"l":[[31.367,2.193,31.356,2.254,31.383,2.306,31.475,2.4]]},{"n":"Amur","z":3,"b":[136.77,49.759,140.114,51.764],"l":[[137.07,50.454,137.101,50.298,137.057,50.216,136.946,50.155,136.872,50.098,136.834,50.045,136.771,49.915,136.77,49.85,136.81,49.759],[140.114,51.764,139.95,51.64,139.902,51.597,139.858,51.579]]},{"n":"Angara","z":2.1,"b":[101.777,51.816,106.497,57.943],"l":[[106.497,52.383,106.438,52.415,106.352,52.404,106.221,52.36,106.069,52.264,105.896,52.117,105.725,51.998,105.556,51.908,105.361,51.847,105.141,51.816,104.99,51.819,104.866,51.872],[103.374,53.107,103.403,53.282,103.404,53.425,103.384,53.594,103.279,53.797,103.087,54.032,103.058,54.218,103.19,54.355,103.251,54.481,103.24,54.598,103.192,54.696,103.107,54.774,103.133,54.892,103.268,55.05,103.329,55.18,103.316,55.282,103.278,55.379,103.215,55.47,103.186,55.575,103.189,55.692,103.151,55.777,103.071,55.828,103.024,55.895,103.009,55.98,102.965,56.057,102.892,56.127,102.751,56.173,102.544,56.195,102.304,56.168,102.031,56.092,101.863,56.072,101.8,56.107,101.777,56.166,101.803,56.291],[102.674,57.943,102.674,57.855,102.651,57.789,102.606,57.717,102.534,57.638,102.434,57.551,102.371,57.446,102.345,57.321,102.338,57.195,102.358,57.004]]},{"n":"Atbara","z":5,"b":[35.895,14.62,35.924,14.935],"l":[[35.924,14.935,35.9,14.863,35.895,14.62]]},{"n":"Athabasca","z":4.7,"b":[-111.28,58.505,-111.016,58.788],"l":[[-111.016,58.691,-111.075,58.505],[-111.28,58.788,-111.208,58.72,-111.142,58.696,-111.016,58.691]]},{"n":"Back","z":5,"b":[-108.533,64.562,-96.995,66.151],"l":[[-108.533,64.562,-108.18,64.686],[-108.089,64.715,-107.979,64.72],[-98.656,65.928,-98.677,66.003,-98.716,66.023,-98.782,66.014,-98.874,66.032,-98.992,66.076,-99.127,66.062,-99.279,65.989,-99.467,65.932,-99.692,65.892,-99.852,65.886,-99.948,65.912,-100.115,65.909,-100.353,65.876,-100.582,65.871,-100.8,65.895,-100.973,65.898,-101.099,65.879,-101.314,65.889,-101.77,65.947],[-97.267,65.974,-96.995,66.151]]},{"n":"Biya","z":5,"b":[87.392,51.364,87.756,51.762],"l":[[87.756,51.364,87.686,51.636,87.595,51.735,87.392,51.762]]},{"n":"Chattahoochee","z":5,"b":[-85.125,30.721,-83.815,34.364],"l":[[-84.966,30.923,-84.887,30.759,-84.884,30.721],[-85.067,31.635,-85.125,31.777,-85.124,31.881,-85.064,32.051],[-83.815,34.364,-84.076,34.167]]},{"n":"Churchill","z":5,"b":[-64.713,53.125,-64.678,53.382],"l":[[-64.682,53.125,-64.713,53.275,-64.712,53.339,-64.678,53.382]]},{"n":"Colorado","z":4.7,"b":[-114.741,34.253,-105.821,40.185],"l":[[-114.377,34.54,-114.371,34.488,-114.308,34.433,-114.159,34.355,-114.125,34.317,-114.125,34.314,-114.126,34.287,-114.164,34.253],[-114.591,35.353,-114.649,35.476,-114.662,35.545,-114.645,35.631],[-114.741,36.014,-114.716,36.085,-114.669,36.122,-114.593,36.148,-114.505,36.156,-114.406,36.148,-114.332,36.116,-114.282,36.06,-114.232,36.032,-114.183,36.03,-114.119,36.077,-114.062,36.175,-114.005,36.195,-113.949,36.16,-113.76,36.007,-113.636,35.831],[-110.368,37.898,-110.399,37.883,-110.421,37.832,-110.43,37.772,-110.47,37.702,-110.54,37.626,-110.654,37.521,-110.692,37.483,-110.698,37.452,-110.722,37.406,-110.781,37.336,-110.916,37.157,-111.019,37.127,-111.301,37.054,-111.431,37.003,-111.473,36.932],[-105.821,40.185,-105.861,40.165]]},{"n":"Columbia","z":3,"b":[-118.527,48.232,-117.517,52.102],"l":[[-118.072,48.664,-118.174,48.512,-118.198,48.404,-118.168,48.232],[-118.092,49.802,-118.13,49.664,-118.128,49.574,-118.097,49.484,-118.014,49.404,-117.907,49.342],[-117.906,50.682,-117.917,50.465,-117.892,50.339,-117.837,50.231,-117.827,50.159,-117.88,50.103],[-117.517,51.659,-117.873,51.87],[-118.527,52.102,-118.366,52.09]]},{"n":"Coppermine","z":5,"b":[-114.366,64.638,-111.956,65.64],"l":[[-112.343,64.899,-112.05,64.776,-111.956,64.711,-111.966,64.638],[-114.173,65.639,-114.305,65.64,-114.355,65.609,-114.366,65.544,-114.29,65.496,-114.127,65.465,-114.034,65.425,-114.011,65.378,-113.787,65.336,-113.36,65.298,-113.025,65.252,-112.781,65.199,-112.6,65.177,-112.483,65.186,-112.389,65.138,-112.317,65.032,-112.29,64.974,-112.318,64.96]]},{"n":"Dalälven","z":5,"b":[13.212,60.122,17.416,61.663],"l":[[16.434,60.156,16.323,60.122],[17.416,60.51,17.389,60.461,17.246,60.402,16.978,60.315,16.832,60.252,16.807,60.211,16.721,60.184,16.499,60.163],[14.551,60.998,14.632,60.922,14.729,60.881,14.868,60.848,14.946,60.806,14.973,60.728],[13.717,61.414,13.672,61.468,13.591,61.502,13.459,61.533,13.348,61.577,13.212,61.663]]},{"n":"Dniester","z":5,"b":[30.259,46.101,30.478,46.32],"l":[[30.478,46.101,30.311,46.233,30.259,46.288,30.269,46.32]]},{"n":"Dnieper","z":4,"b":[30.478,46.831,35.149,51.217],"l":[[33.54,46.831,33.606,46.869,33.662,46.957,33.739,47.05,33.859,47.149,33.976,47.307,33.998,47.411,34.155,47.49,34.448,47.542,34.712,47.553,34.946,47.521,35.088,47.528,35.138,47.573,35.149,47.657,35.106,47.842],[33.194,49.094,32.884,49.225,32.603,49.302,31.886,49.555],[31.44,50,31.48,49.933,31.486,49.871,31.467,49.754],[30.508,51.217,30.478,50.904,30.479,50.749,30.515,50.597]]},{"n":"Don","z":5,"b":[42.11,47.602,43.985,49.507],"l":[[42.11,47.602,42.303,47.661,42.484,47.728,42.663,47.794,42.832,47.838,42.881,47.885,42.927,47.961,43.042,48.186,43.132,48.294,43.237,48.389,43.459,48.539,43.528,48.614,43.522,48.653,43.481,48.699,43.465,48.737,43.474,48.769,43.522,48.821,43.606,48.892,43.69,48.929,43.773,48.931,43.844,48.949,43.896,48.968,43.923,49.01,43.938,49.072,43.985,49.136,43.956,49.186,43.905,49.204,43.785,49.203,43.749,49.207,43.708,49.25,43.7,49.328,43.659,49.402,43.602,49.486,43.549,49.507]]},{"n":"Blue Nile","z":3,"b":[34.38,11.296,34.778,11.783],"l":[[34.38,11.783,34.409,11.639,34.509,11.517,34.778,11.296]]},{"n":"Ertis","z":2.1,"b":[83.364,47.732,84.357,49.644],"l":[[83.364,49.644,83.603,49.52,83.731,49.471,83.826,49.454,83.959,49.379,84.132,49.245,84.204,49.162,84.176,49.129,84.012,49.07,83.714,48.984,83.533,48.889,83.468,48.784,83.45,48.668,83.479,48.543,83.524,48.455,83.587,48.404,83.589,48.359,83.531,48.318,83.507,48.273,83.516,48.222,83.642,48.137,83.887,48.018,84.072,47.902,84.198,47.791,84.285,47.734,84.357,47.732]]},{"n":"Firat","z":3,"b":[38.74,38.813,38.813,39.078],"l":[[38.768,38.813,38.813,38.875,38.74,39.078]]},{"n":"Gan","z":5,"b":[115.982,29.279,116.198,29.751],"l":[[115.982,29.279,116.034,29.319,116.088,29.437,116.198,29.751]]},{"n":"Grande","z":4.7,"b":[-50.994,-21.465,-44.387,-19.82],"l":[[-44.387,-21.465,-44.616,-21.318],[-45.205,-21.093,-45.252,-21.086,-45.276,-21.067,-45.336,-21.035,-45.443,-21.011,-45.577,-20.918,-45.737,-20.754,-45.847,-20.686,-45.906,-20.714,-45.951,-20.714,-45.981,-20.686,-46.078,-20.671,-46.326,-20.668],[-47.073,-20.284,-46.741,-20.48,-46.619,-20.564,-46.587,-20.623],[-50.994,-20.102,-50.582,-19.82],[-49.385,-19.98,-50.365,-19.858]]},{"n":"Han","z":5,"b":[111,32.568,111.478,32.741],"l":[[111.478,32.568,111,32.741]]},{"n":"Huang","z":3,"b":[97.072,34.835,97.763,35.088],"l":[[97.288,34.835,97.248,34.889,97.194,34.913,97.072,34.931],[97.763,35.088,97.714,34.962,97.659,34.909,97.546,34.876]]},{"n":"Indus","z":3,"b":[72.707,34.086,72.848,34.623],"l":[[72.782,34.623,72.835,34.371,72.848,34.258,72.838,34.198,72.802,34.148,72.707,34.086]]},{"n":"Jordan","z":5,"b":[35.423,31.325,35.599,32.9],"l":[[35.423,31.325,35.465,31.562,35.499,31.672,35.559,31.766],[35.599,32.9,35.577,32.72]]},{"n":"Kemijoki","z":5,"b":[27.359,66.467,27.519,66.765],"l":[[27.385,66.765,27.519,66.668,27.513,66.621,27.414,66.592,27.363,66.55,27.359,66.467]]},{"n":"Klamath","z":4.7,"b":[-121.921,42.251,-121.809,42.475],"l":[[-121.921,42.475,-121.916,42.419,-121.809,42.251]]},{"n":"Krishna","z":5,"b":[78.997,16.23,79.313,16.579],"l":[[78.997,16.23,79.157,16.235,79.211,16.301,79.213,16.429,79.238,16.515,79.313,16.579]]},{"n":"La Grande Rivière","z":4,"b":[-77.42,53.427,-70.843,53.798],"l":[[-70.843,53.427,-70.948,53.439,-70.991,53.468,-71.007,53.518,-71.041,53.555,-71.117,53.591],[-72.034,53.598,-71.926,53.572,-71.871,53.584,-71.817,53.646],[-74.433,53.685,-74.697,53.631,-74.852,53.628,-74.965,53.648,-75.239,53.656,-75.617,53.674,-75.802,53.664,-75.895,53.71,-76.136,53.699,-76.664,53.668,-77.04,53.677,-77.196,53.699,-77.311,53.732,-77.42,53.798]]},{"n":"Mackenzie","z":2.1,"b":[-117.668,61.048,-113.704,61.339],"l":[[-117.668,61.308,-117.221,61.15,-116.826,61.082,-116.335,61.051,-115.996,61.048,-115.81,61.073,-115.502,61.157,-115.072,61.299,-114.569,61.339,-113.704,61.246]]},{"n":"Madison","z":2,"b":[-111.309,44.757,-111.1,44.868],"l":[[-111.1,44.757,-111.118,44.762,-111.189,44.771,-111.242,44.8,-111.301,44.863,-111.309,44.868]]},{"n":"Mahäna Nadï","z":5,"b":[83.3,21.52,83.858,21.736],"l":[[83.858,21.52,83.713,21.673,83.636,21.728,83.578,21.736,83.528,21.717,83.488,21.669,83.425,21.658,83.3,21.695]]},{"n":"Mississippi","z":4.7,"b":[-94.637,47.406,-94.094,47.464],"l":[[-94.326,47.406,-94.202,47.457,-94.144,47.464,-94.094,47.433],[-94.507,47.456,-94.637,47.44]]},{"n":"Missouri","z":2,"b":[-111.697,43.06,-98.56,48.151],"l":[[-99.365,43.991,-99.328,43.894,-99.353,43.818,-99.43,43.729,-99.442,43.674,-99.39,43.653,-99.347,43.599,-99.313,43.515,-99.196,43.415,-98.997,43.299,-98.892,43.225,-98.882,43.192,-98.798,43.146,-98.56,43.06],[-111.516,46.362,-111.54,46.469,-111.585,46.544,-111.697,46.662],[-100.777,46.713,-100.625,46.608,-100.569,46.513,-100.556,46.393,-100.571,46.287,-100.614,46.196,-100.557,46.041,-100.4,45.822,-100.369,45.705,-100.369,45.674,-100.402,45.643,-100.433,45.613,-100.438,45.561,-100.433,45.535,-100.406,45.505,-100.342,45.448,-100.297,45.316,-100.302,45.134,-100.321,45.053,-100.348,45.037,-100.375,45.033,-100.402,44.993,-100.41,44.918,-100.444,44.854,-100.505,44.8,-100.583,44.783,-100.68,44.802,-100.691,44.795,-100.697,44.776,-100.659,44.735,-100.635,44.68,-100.633,44.62,-100.609,44.578,-100.561,44.555,-100.556,44.53,-100.562,44.504,-100.536,44.476,-100.418,44.45,-100.394,44.434],[-101.4,47.506,-101.428,47.552,-101.549,47.556,-101.772,47.533,-101.976,47.547,-102.161,47.598,-102.269,47.669,-102.299,47.76,-102.345,47.799,-102.409,47.786,-102.491,47.794,-102.59,47.824,-102.62,47.881,-102.58,47.965,-102.614,48.038,-102.724,48.1,-102.873,48.137,-103.06,48.151,-103.179,48.136,-103.23,48.093,-103.308,48.059,-103.414,48.036,-103.496,48.054,-103.555,48.114,-103.631,48.115,-103.772,48.027],[-106.406,48.02,-106.519,47.886,-106.6,47.822,-106.879,47.707,-107.04,47.694,-107.262,47.706,-107.385,47.694,-107.41,47.659,-107.473,47.645,-107.575,47.653,-107.689,47.608,-107.813,47.512,-107.888,47.502,-107.94,47.581,-108.02,47.598,-108.196,47.602]]},{"n":"Black Volta","z":4.7,"b":[-1.713,8.631,-0.976,8.762],"l":[[-1.713,8.631,-1.57,8.682,-1.502,8.714,-1.437,8.756,-1.376,8.762,-1.3,8.714,-1.177,8.672,-0.976,8.662]]},{"n":"Murray","z":4,"b":[146.028,-36.108,147.272,-36.015],"l":[[147.043,-36.108,147.1,-36.05,147.157,-36.036,147.272,-36.049],[146.028,-36.015,146.216,-36.056]]},{"n":"White Volta","z":5,"b":[-1.155,8.661,-0.974,9.117],"l":[[-0.974,8.661,-1.111,8.856,-1.155,8.97,-1.152,9.117]]},{"n":"Naryn","z":4.7,"b":[72.654,41.667,73.3,41.79],"l":[[73.3,41.776,72.932,41.79,72.771,41.762,72.654,41.667]]},{"n":"Nelson","z":5,"b":[-98.015,50.979,-94.779,56.394],"l":[[-96.598,50.979,-96.595,51.308,-96.631,51.482,-96.703,51.611,-96.952,51.86,-97.377,52.23,-97.643,52.532,-97.75,52.766,-97.831,53.042,-97.914,53.519,-97.869,53.714,-97.906,53.776,-97.981,53.837,-98.015,53.886,-98.009,53.925,-98.005,53.966,-97.987,53.988,-97.914,54.003,-97.798,54.026,-97.731,54.082],[-97.901,54.705,-97.791,54.571],[-97.946,54.948,-97.254,55.236],[-96.553,56.063,-96.504,56.107,-96.421,56.129,-96.285,56.141,-96.196,56.171,-96.152,56.219,-96.058,56.255,-95.841,56.291],[-95.431,56.339,-94.779,56.394]]},{"n":"Neva","z":4.7,"b":[31.043,59.951,31.414,60.235],"l":[[31.043,59.951,31.181,59.983,31.274,60.054,31.414,60.235]]},{"n":"Niger","z":3,"b":[-4.304,9.865,4.657,15.507],"l":[[4.626,9.865,4.529,10.239,4.537,10.425,4.657,10.609],[-3.98,15.281,-4.166,15.365,-4.247,15.421,-4.304,15.507]]},{"n":"Nile","z":2,"b":[31.091,21.609,32.914,23.988],"l":[[32.879,23.988,32.909,23.808,32.903,23.696,32.872,23.591,32.878,23.481,32.903,23.415,32.914,23.332,32.855,23.208,32.68,23.009,32.587,22.876,32.576,22.806,32.514,22.74,32.418,22.672,32.326,22.653,32.232,22.718,32.137,22.733,32.042,22.699,31.961,22.63,31.894,22.526,31.735,22.395,31.544,22.252,31.4,22.093,31.362,21.98,31.28,21.845,31.091,21.609]]},{"n":"Ob","z":4,"b":[81.327,53.886,83.025,54.845],"l":[[83.025,54.845,82.312,54.491,82.004,54.358,81.862,54.33,81.741,54.269,81.639,54.176,81.522,54.104,81.391,54.052,81.327,53.991,81.334,53.886]]},{"n":"Orange","z":5,"b":[24.731,-30.664,25.972,-29.979],"l":[[25.972,-30.545,25.902,-30.548,25.841,-30.581,25.764,-30.648,25.67,-30.664,25.506,-30.612],[24.731,-29.979,24.978,-30.263]]},{"n":"Ord","z":5,"b":[128.708,-16.788,128.756,-16.133],"l":[[128.737,-16.133,128.708,-16.434,128.714,-16.575,128.747,-16.638,128.756,-16.699,128.734,-16.788]]},{"n":"Ottawa","z":5,"b":[-79.559,46.827,-79.166,47.577],"l":[[-79.166,46.827,-79.365,47.022,-79.435,47.134,-79.442,47.228,-79.479,47.319,-79.547,47.407,-79.559,47.483,-79.496,47.577]]},{"n":"Panama Canal","z":5,"b":[-79.923,9.104,-79.691,9.267],"l":[[-79.691,9.104,-79.756,9.112,-79.814,9.153,-79.923,9.267]]},{"n":"Paranaíba","z":4,"b":[-50.499,-19.02,-47.447,-18.171],"l":[[-50.49,-19.02,-50.499,-18.953,-50.458,-18.877,-50.368,-18.769,-50.247,-18.69,-50.094,-18.641,-49.96,-18.616,-49.789,-18.615],[-48.227,-18.34,-48.952,-18.326,-49.108,-18.388],[-47.447,-18.171,-47.786,-18.379,-47.929,-18.445,-48.017,-18.434]]},{"n":"Paraná","z":4,"b":[-54.611,-25.433,-50.994,-19.668],"l":[[-54.611,-25.433,-54.473,-25.22,-54.436,-25.121,-54.454,-25.065,-54.413,-24.867,-54.313,-24.528,-54.281,-24.306,-54.317,-24.201,-54.318,-24.128,-54.267,-24.066],[-51.936,-21.423,-51.955,-21.469,-51.99,-21.494,-52.048,-21.511,-52.073,-21.554,-52.065,-21.623,-52.123,-21.718,-52.246,-21.84,-52.339,-21.957,-52.403,-22.069,-52.566,-22.207,-52.961,-22.455],[-50.997,-19.668,-50.994,-20.102,-51.064,-20.223,-51.165,-20.308,-51.397,-20.442]]},{"n":"Peace","z":2.1,"b":[-124.836,56,-113.714,58.71],"l":[[-122.222,56.021,-122.482,56.112,-122.691,56.124,-122.934,56.086,-123.088,56.046,-123.152,56.005,-123.323,56,-123.601,56.03,-123.797,56.033,-123.912,56.011,-124.083,56.085,-124.309,56.256,-124.454,56.389,-124.517,56.483,-124.621,56.587,-124.836,56.757],[-113.823,58.71,-113.714,58.669]]},{"n":"Pit","z":5,"b":[-122.369,40.763,-122.095,40.784],"l":[[-122.095,40.784,-122.369,40.763]]},{"n":"R. des Outaouais","z":5,"b":[-78.813,47.478,-76.935,47.83],"l":[[-77.457,47.556,-77.471,47.566,-77.484,47.595,-77.53,47.596,-77.592,47.576,-77.613,47.582,-77.615,47.597,-77.544,47.671],[-77.322,47.573,-77.301,47.562,-77.265,47.527,-77.216,47.507,-77.158,47.531,-77.114,47.526,-77.086,47.492,-77.037,47.478,-76.935,47.488],[-78.724,47.684,-78.744,47.672,-78.813,47.656],[-77.527,47.744,-77.528,47.766,-77.544,47.82,-77.567,47.83]]},{"n":"Rainy","z":5,"b":[-93.382,48.616,-91.319,48.912],"l":[[-93.382,48.616,-92.779,48.678],[-91.319,48.828,-91.44,48.903,-91.518,48.912,-91.63,48.865]]},{"n":"Rhein","z":4,"b":[8.881,47.511,9.554,47.671],"l":[[9.128,47.671,8.881,47.656],[9.183,47.671,9.554,47.511]]},{"n":"Rhône","z":5,"b":[6.133,46.205,6.866,46.437],"l":[[6.133,46.205,6.235,46.333,6.322,46.394,6.429,46.431,6.578,46.437,6.866,46.403]]},{"n":"Rio Grande","z":4,"b":[-107.293,26.564,-99.172,33.356],"l":[[-99.457,27.057,-99.444,27.037,-99.302,26.885,-99.23,26.762,-99.172,26.564],[-101.039,29.46,-101.304,29.634],[-107.293,32.91,-107.281,33.114],[-107.181,33.173,-107.17,33.356]]},{"n":"Rupert","z":5,"b":[-76.819,51.338,-74.818,51.446],"l":[[-76.819,51.416,-76.781,51.354,-76.73,51.338,-76.553,51.364],[-75.204,51.446,-75.086,51.442,-75,51.419,-74.948,51.374,-74.895,51.367,-74.818,51.412]]},{"n":"Río Grande de Santiago","z":5,"b":[-105.104,20.198,-102.77,21.822],"l":[[-102.78,20.31,-102.77,20.198],[-103.709,20.938,-103.594,20.971],[-105.104,21.822,-105.06,21.807,-105.034,21.779]]},{"n":"Sacramento","z":4.7,"b":[-122.411,40.721,-122.369,40.763],"l":[[-122.369,40.763,-122.411,40.721]]},{"n":"San Joaquin","z":5,"b":[-119.698,37,-119.601,37.062],"l":[[-119.698,37,-119.601,37.062]]},{"n":"San Juan","z":4.7,"b":[-85.871,11.118,-84.77,12.098],"l":[[-84.77,11.118,-84.978,11.207,-85.253,11.453,-85.871,12.098]]},{"n":"Saskatchewan","z":4,"b":[-103.891,53.159,-97.914,53.982],"l":[[-99.23,53.185,-98.416,53.354,-98.082,53.436,-97.914,53.519],[-100.323,53.593,-100.163,53.503,-100.066,53.405,-99.976,53.268,-99.852,53.212,-99.693,53.237,-99.543,53.227,-99.33,53.159],[-103.421,53.652,-103.58,53.629,-103.697,53.586,-103.891,53.481],[-102.437,53.982,-102.381,53.966]]},{"n":"Sassandra","z":5,"b":[-7.057,6.264,-6.989,7.041],"l":[[-7.043,7.041,-7.057,6.911,-7.039,6.825,-6.993,6.739,-6.989,6.586,-7.026,6.365,-7.008,6.264]]},{"n":"Savannah","z":5,"b":[-82.589,33.664,-82.208,34.018],"l":[[-82.589,34.018,-82.352,33.838,-82.257,33.749,-82.208,33.664]]},{"n":"Semliki","z":5,"b":[29.452,-0.607,31.367,2.193],"l":[[29.452,-0.607,29.597,-0.134],[31.367,2.193,31.159,1.922,30.943,1.683,30.478,1.239]]},{"n":"Shire","z":3,"b":[35.257,-14.758,35.278,-14.504],"l":[[35.257,-14.504,35.278,-14.758]]},{"n":"Shishhid Gol","z":4,"b":[99.31,51.349,99.427,51.41],"l":[[99.427,51.349,99.31,51.41]]},{"n":"Snake","z":4,"b":[-113.472,42.654,-111.048,43.338],"l":[[-113.316,42.654,-113.472,42.675],[-112.864,42.813,-112.64,43.023],[-111.048,43.207,-111.093,43.234,-111.186,43.338]]},{"n":"South Saskatchewan","z":5,"b":[-108.006,50.636,-106.69,51.262],"l":[[-106.853,51.262,-106.69,51.13,-106.691,51.072,-106.803,51.045,-106.901,50.963,-106.986,50.824,-107.159,50.723,-107.42,50.66,-107.665,50.636,-108.006,50.658]]},{"n":"Suez Canal","z":5,"b":[32.36,30.181,32.561,30.424],"l":[[32.561,30.181,32.36,30.424]]},{"n":"Sukhona","z":4,"b":[39.053,59.525,39.799,59.868],"l":[[39.053,59.868,39.5,59.616,39.686,59.53,39.799,59.525]]},{"n":"Sutlej","z":5,"b":[76.435,30.792,81.295,31.422],"l":[[81.295,30.792,81.199,30.852],[76.765,31.409,76.744,31.334,76.715,31.298,76.671,31.278,76.596,31.306,76.435,31.422]]},{"n":"Svir’","z":4.7,"b":[31.414,60.235,32.821,60.655],"l":[[31.414,60.235,31.595,60.408,31.798,60.52,32.086,60.629,32.356,60.655,32.611,60.598,32.711,60.544,32.821,60.483]]},{"n":"Syr Darya","z":4.7,"b":[61.562,40.273,70.355,46.11],"l":[[70.355,40.435,70.319,40.371,70.186,40.331,69.827,40.273],[68.456,41.107,68.173,41.13,68.05,41.165,68.003,41.212,67.991,41.255],[61.76,46.021,61.562,46.11]]},{"n":"São Francisco","z":4,"b":[-45.336,-19.051,-40.818,-9.406],"l":[[-45.089,-19.051,-45.176,-18.801,-45.202,-18.591,-45.249,-18.501,-45.324,-18.459,-45.336,-18.38,-45.262,-18.203],[-43.15,-11.143,-43.057,-10.988,-42.945,-10.888,-42.781,-10.791,-42.633,-10.638,-42.5,-10.427,-42.439,-10.273,-42.45,-10.175,-42.404,-10.09,-42.301,-10.019,-42.209,-9.911,-42.128,-9.768,-41.91,-9.696,-41.554,-9.694,-41.335,-9.676,-41.253,-9.643,-41.175,-9.568,-41.102,-9.453,-41.003,-9.406,-40.818,-9.438]]},{"n":"Tajo","z":4.7,"b":[-5.996,39.797,-2.577,40.724],"l":[[-5.996,39.852,-5.851,39.807,-5.656,39.797,-5.215,39.812],[-2.734,40.528,-2.577,40.724]]},{"n":"Tennessee","z":4.7,"b":[-88.283,34.575,-86.899,37.022],"l":[[-86.899,34.575,-87.242,34.754,-87.4,34.819,-87.486,34.828,-87.572,34.818,-87.7,34.773],[-87.975,35.945,-88.004,35.962,-87.992,36.011,-87.949,36.1,-87.959,36.216,-88.023,36.359,-88.056,36.484,-88.057,36.59,-88.114,36.738,-88.283,37.022]]},{"n":"Teslin","z":3,"b":[-133.245,59.607,-132.095,60.449],"l":[[-132.095,59.607,-132.353,59.921,-132.545,60.069,-132.759,60.156,-132.96,60.262,-133.245,60.449]]},{"n":"Thelon","z":4.7,"b":[-104.834,63.071,-94.225,64.761],"l":[[-104.834,63.071,-104.729,63.128,-104.697,63.178,-104.706,63.27],[-96.273,64.301,-95.448,64.158,-95.049,64.158,-94.811,64.126,-94.602,64.062,-94.429,64.041,-94.225,64.076],[-101.811,64.411,-101.536,64.493],[-97.103,64.761,-97.163,64.715,-97.292,64.716,-97.511,64.75,-97.773,64.741,-98.08,64.687,-98.264,64.666,-98.326,64.679,-98.382,64.652,-98.433,64.585,-98.493,64.555,-98.563,64.562,-98.687,64.539,-98.864,64.487,-99.003,64.491,-99.104,64.552,-99.293,64.583,-99.571,64.584,-99.789,64.603,-99.949,64.639,-100.088,64.633,-100.208,64.586,-100.4,64.573,-100.795,64.604]]},{"n":"Tocantins","z":4,"b":[-49.687,-5.335,-48.937,-3.835],"l":[[-49.687,-3.835,-49.639,-4.211,-49.546,-4.546,-49.392,-4.966,-49.28,-5.218,-49.21,-5.303,-49.115,-5.335,-48.937,-5.303]]},{"n":"Tugaloo","z":5,"b":[-83.166,34.366,-82.819,34.599],"l":[[-83.166,34.599,-83.053,34.511,-82.976,34.476,-82.897,34.466,-82.848,34.437,-82.819,34.366]]},{"n":"Ural","z":4,"b":[58.631,51.695,58.835,52.197],"l":[[58.835,52.197,58.782,52.071,58.767,51.976,58.773,51.87,58.74,51.786,58.631,51.695]]},{"n":"Vaal","z":4,"b":[25.622,-27.699,28.497,-26.867],"l":[[26.091,-27.603,26.06,-27.69,26.037,-27.699,26.012,-27.659,25.955,-27.624,25.865,-27.594,25.771,-27.601,25.622,-27.668],[28.497,-26.991,28.446,-26.973,28.124,-26.867]]},{"n":"Verkhniy Yenisey","z":4,"b":[90.939,53.735,92.408,55.503],"l":[[91.511,53.735,91.454,53.83,91.466,53.904,91.528,53.987,91.556,54.046,91.551,54.079,91.484,54.167,91.358,54.311,91.286,54.415,91.269,54.479,91.179,54.55,91.016,54.628,90.939,54.722,90.947,54.832,90.988,54.926,91.062,55.004,91.351,55.087,91.855,55.176,92.182,55.291,92.408,55.503]]},{"n":"Victoria Nile","z":2,"b":[31.768,-0.936,33.259,1.666],"l":[[33.187,0.428,33.259,0.379,33.232,0.294,33.129,0.159,33.018,-0.052,32.897,-0.338,32.744,-0.545,32.56,-0.675,32.292,-0.789,31.768,-0.936],[32.545,1.522,32.614,1.454,32.685,1.427,32.78,1.422,32.835,1.405,32.893,1.311],[32.241,1.666,32.475,1.552]]},{"n":"Vilyuy","z":5,"b":[109.587,62.857,112.48,63.48],"l":[[109.587,63.455,109.814,63.426,109.939,63.435,110.038,63.473,110.142,63.48,110.251,63.457,110.345,63.423,110.423,63.378,110.449,63.313,110.421,63.227,110.474,63.177,110.61,63.163,110.644,63.128,110.577,63.073,110.665,63.008,110.909,62.935,111.057,62.935,111.107,63.01,111.234,63.057,111.436,63.076,111.576,63.126,111.656,63.208,111.741,63.239,111.831,63.218,111.886,63.176,111.907,63.112,111.869,63.059,111.772,63.019,111.777,62.965,111.885,62.896,111.995,62.86,112.107,62.857,112.208,62.906,112.296,63.007,112.375,63.048,112.48,63.021]]},{"n":"Vistula","z":4.7,"b":[18.81,49.925,18.937,49.932],"l":[[18.81,49.925,18.937,49.932]]},{"n":"Volga","z":3,"b":[36.375,50.222,49.905,58.288],"l":[[45.664,50.222,45.831,50.357,45.836,50.457,45.733,50.567,45.68,50.647,45.675,50.696,45.744,50.742,45.885,50.782,45.981,50.824,46.03,50.866,46.044,50.925,46.023,51,45.968,51.066,45.879,51.124,45.836,51.188,45.837,51.26,45.87,51.322,45.935,51.375,45.971,51.423,45.979,51.465,46.033,51.506,46.131,51.547,46.218,51.614,46.294,51.708,46.365,51.746,46.477,51.773,46.566,51.752,46.68,51.744,46.775,51.748],[49.059,55.483,49.285,55.234,49.248,55.113,49.023,55.036,48.9,54.91,48.878,54.736,48.782,54.617,48.611,54.554,48.507,54.489,48.47,54.422,48.453,54.344,48.458,54.257,48.585,54.135,48.833,53.978,48.964,53.827,48.978,53.681,49.006,53.579,49.048,53.522,49.104,53.483,49.209,53.461,49.567,53.467,49.905,53.457],[43.432,56.675,43.344,56.733,43.286,56.814,43.231,56.937,43.175,57.024,43.119,57.075,43.089,57.119,43.082,57.156,43.104,57.213,43.178,57.329,42.874,57.41,42.724,57.431,42.576,57.415],[37.151,56.753,36.882,56.783,36.736,56.75,36.624,56.664,36.52,56.643,36.375,56.706],[37.761,57.291,37.944,57.312,38.079,57.382,38.301,57.572],[41.343,57.493,41.206,57.517,41.099,57.588,40.976,57.714,40.889,57.784,40.838,57.799,40.706,57.79,40.494,57.755,40.293,57.695,40.007,57.567],[38.437,58.018,38.326,58.083,38.319,58.15,38.408,58.288,38.704,58.113]]},{"n":"Volta","z":4.7,"b":[-0.976,6.338,0.231,8.662],"l":[[-0.976,8.662,-0.726,8.362,-0.526,8.198,-0.295,8.068,-0.153,7.945,-0.101,7.828,-0.054,7.759,-0.011,7.738,0.039,7.648,0.098,7.49,0.155,7.394,0.209,7.36,0.231,7.245,0.218,7.05,0.195,6.922,0.16,6.863,0.161,6.802,0.197,6.74,0.194,6.688,0.134,6.635,0.108,6.551,0.103,6.338]]},{"n":"Vorma","z":5,"b":[11.157,59.389,11.203,59.911],"l":[[11.17,59.389,11.201,59.451,11.203,59.498],[11.2,59.67,11.194,59.72,11.159,59.831,11.157,59.911]]},{"n":"Vuoksi","z":5,"b":[29.596,60.236,31.412,60.717],"l":[[30.478,60.574,30.298,60.654,30.117,60.676,29.874,60.669,29.714,60.678,29.596,60.717],[30.534,60.63,30.658,60.649,30.793,60.635,30.979,60.593,31.157,60.489,31.412,60.236]]},{"n":"Waikato","z":5,"b":[175.802,-38.939,176.059,-38.694],"l":[[175.802,-38.939,175.83,-38.917,175.965,-38.821,176.033,-38.758,176.059,-38.694]]},{"n":"Winnipeg","z":5,"b":[-96.598,48.863,-94.508,50.979],"l":[[-94.615,49.993,-94.508,49.707,-94.549,49.574,-94.703,49.496,-94.802,49.425,-94.847,49.361,-94.86,49.259,-94.843,49.119,-94.803,49.003,-94.713,48.863],[-96.026,50.283,-95.989,50.405],[-96.598,50.979,-96.467,50.774,-96.392,50.685,-96.294,50.624]]},{"n":"Yuan","z":5,"b":[111.993,28.838,113.071,29.389],"l":[[112.905,29.031,112.801,28.927,112.693,28.874,112.545,28.838,112.351,28.861,111.993,28.985],[112.994,29.147,113.071,29.389]]},{"n":"Zambezi","z":3,"b":[27.021,-17.958,32.708,-15.565],"l":[[27.021,-17.958,27.131,-17.849,27.271,-17.643,27.438,-17.512,27.637,-17.262,27.757,-17.06,27.932,-16.896,28.164,-16.77,28.4,-16.663,28.761,-16.532],[32.708,-15.579,32.497,-15.565,32.346,-15.589,32.184,-15.647,31.924,-15.667,31.566,-15.65,31.329,-15.662,31.213,-15.704,31.065,-15.704,30.796,-15.642]]},{"n":"","z":5,"b":[-111.016,58.544,-103.665,59.361],"l":[[-103.785,58.691,-103.665,58.544],[-111.016,58.691,-110.802,58.744,-110.637,58.82,-110.448,58.937,-110.257,59.031,-110.064,59.103,-109.775,59.183,-109.389,59.272,-109.105,59.305,-108.92,59.282,-108.518,59.298,-107.898,59.351,-107.511,59.361,-107.358,59.329,-106.966,59.303,-106.018,59.271],[-105.55,59.15,-105.249,59.194,-105.115,59.2,-105.012,59.173]]},{"n":"","z":4.7,"b":[-103.447,61.366,-95.447,64.158],"l":[[-103.22,61.681,-103.192,61.643,-103.224,61.609,-103.306,61.567,-103.372,61.5,-103.447,61.366],[-102.919,62.319,-102.909,62.281,-102.862,62.26,-102.777,62.241,-102.727,62.215,-102.714,62.182,-102.783,62.13,-102.936,62.061,-103.049,61.979,-103.158,61.836],[-100.552,62.355,-100.074,62.227,-99.841,62.213,-99.617,62.3],[-101.163,62.367,-100.735,62.356],[-101.831,62.52,-101.847,62.451,-101.826,62.39,-101.775,62.314,-101.654,62.295,-101.366,62.35],[-98.35,62.616,-98.24,62.717,-98.082,62.755,-97.841,62.764,-97.697,62.822,-97.63,62.979],[-101.529,62.664,-101.537,62.791,-101.581,62.862,-101.66,62.919,-101.783,62.941,-102.035,62.925],[-102.66,62.76,-102.68,62.694,-102.665,62.642,-102.622,62.583,-102.634,62.517,-102.735,62.411],[-97.459,63.157,-97.287,63.514],[-95.878,63.656,-96.301,63.613,-96.569,63.608,-96.952,63.637],[-95.497,64.051,-95.447,64.158]]},{"n":"Abay","z":3,"b":[36.981,11.036,37.18,11.855],"l":[[37.179,11.036,37.022,11.192,36.981,11.287,37.015,11.416,37.05,11.58,37.097,11.656,37.167,11.7,37.18,11.755,37.113,11.855]]},{"n":"Blue Nile","z":3,"b":[35.353,9.884,38.514,11.634],"l":[[37.387,11.634,37.393,11.571,37.45,11.534,37.56,11.504,37.68,11.421,37.812,11.288,38.019,11.186,38.302,11.116,38.462,11.037,38.5,10.949,38.514,10.772,38.503,10.505,38.478,10.314,38.439,10.199,38.376,10.136,38.289,10.123,38.11,10.048,37.839,9.911,37.66,9.884,37.574,9.966,37.49,10.021,37.409,10.049,37.354,10.052,37.328,10.032,37.308,10.04,37.296,10.076,37.263,10.105,37.212,10.125,37.189,10.164,37.194,10.224,37.159,10.266,37.046,10.303,36.677,10.36,36.541,10.404,36.514,10.453,36.483,10.469,36.45,10.454,36.395,10.375,36.32,10.232,36.207,10.155,36.055,10.143,35.957,10.111,35.912,10.058,35.849,10.036,35.768,10.044,35.726,10.03,35.724,10.009,35.72,9.998,35.706,9.996,35.628,10.017,35.369,10.14,35.353,10.155]]},{"n":"Al Furat","z":3,"b":[38.026,34.428,40.993,36.836],"l":[[38.56,35.863,38.916,35.913,39.227,35.881,39.613,35.784,39.833,35.683,39.889,35.579,40.004,35.473,40.179,35.363,40.263,35.298,40.257,35.277,40.294,35.253,40.377,35.225,40.424,35.167,40.438,35.079,40.475,35.016,40.535,34.978,40.584,34.918,40.62,34.837,40.711,34.752,40.855,34.663,40.928,34.581,40.93,34.506,40.946,34.458,40.993,34.428],[38.026,36.836,38.056,36.741,38.103,36.694,38.174,36.663,38.231,36.618,38.272,36.558,38.266,36.482,38.184,36.342]]},{"n":"Alabama","z":5,"b":[-88.016,30.694,-85.183,34.259],"l":[[-86.446,32.812,-86.268,32.622,-86.243,32.527,-86.31,32.462,-86.328,32.42,-86.339,32.409,-86.374,32.418,-86.423,32.408,-86.482,32.368,-86.543,32.363,-86.605,32.394,-86.652,32.398,-86.684,32.373,-86.72,32.369,-86.759,32.384,-86.786,32.374,-86.795,32.354,-86.81,32.353,-86.824,32.362,-86.84,32.41,-86.855,32.42,-86.871,32.42,-86.911,32.381,-86.95,32.373,-86.989,32.398,-87.025,32.404,-87.057,32.391,-87.058,32.371,-87.03,32.342,-87.05,32.317,-87.119,32.297,-87.139,32.26,-87.11,32.207,-87.133,32.176,-87.208,32.167,-87.246,32.138,-87.25,32.089,-87.285,32.086,-87.354,32.127,-87.409,32.11,-87.453,32.036,-87.452,31.98,-87.407,31.942,-87.418,31.916,-87.484,31.901,-87.494,31.868,-87.447,31.816,-87.456,31.765,-87.521,31.715,-87.543,31.66,-87.522,31.601,-87.535,31.559,-87.583,31.535,-87.602,31.499,-87.591,31.451,-87.63,31.413,-87.718,31.383,-87.791,31.316,-87.848,31.214,-87.893,31.159,-87.921,31.152,-87.941,31.14,-87.945,31.118,-87.951,30.919,-87.971,30.829,-88.005,30.8,-88.016,30.76,-88.011,30.694],[-86.33,33.444,-86.366,33.418,-86.376,33.382,-86.373,33.327,-86.401,33.276,-86.462,33.228,-86.484,33.189,-86.468,33.158,-86.476,33.115,-86.517,33.061,-86.522,33.032],[-85.183,34.259,-85.501,34.209,-85.657,34.205,-85.703,34.203,-85.727,34.192,-85.733,34.174,-85.707,34.154,-85.705,34.134,-85.734,34.112,-85.771,34.107,-85.813,34.113,-85.838,34.1,-85.845,34.076,-85.863,34.058,-85.881,34.058,-85.898,34.05,-85.901,34.032,-85.922,34.01,-85.95,34.01,-85.965,33.992,-85.967,33.967,-85.984,33.944,-86.033,33.928,-86.055,33.885,-86.046,33.811,-86.082,33.753,-86.161,33.712,-86.191,33.667,-86.194,33.615,-86.2,33.588]]},{"n":"Albany","z":5,"b":[-92.299,51.063,-81.828,52.224],"l":[[-91.418,51.069,-91.312,51.063],[-90.416,51.092,-89.862,51.198,-89.593,51.278,-89.423,51.368,-89.293,51.419,-89.205,51.43,-89.141,51.461,-89.104,51.51,-89.056,51.515,-88.998,51.474,-88.912,51.483,-88.799,51.541,-88.689,51.57,-88.583,51.57,-88.51,51.55,-88.469,51.511,-88.329,51.493,-88.088,51.494,-87.853,51.434,-87.624,51.314,-87.448,51.26,-87.324,51.27,-87.198,51.309,-87.067,51.378,-86.941,51.412,-86.82,51.413,-86.702,51.442,-86.529,51.528,-86.439,51.582,-86.4,51.635,-86.384,51.705,-86.352,51.75,-86.306,51.77,-86.247,51.775,-86.177,51.766,-86.095,51.731,-86.001,51.669,-85.841,51.646,-85.616,51.663,-85.4,51.605,-85.194,51.471,-85.078,51.378,-85.053,51.325,-84.906,51.251,-84.638,51.158,-84.455,51.14,-84.358,51.198,-84.078,51.296,-83.615,51.433,-83.314,51.572,-83.176,51.713,-83.035,51.821,-82.893,51.896,-82.758,51.936,-82.631,51.943,-82.505,51.985,-82.381,52.063,-82.253,52.111,-82.055,52.138,-81.883,52.194,-81.828,52.224],[-91.782,51.336,-91.724,51.277,-91.684,51.258,-91.659,51.244,-91.646,51.215,-91.641,51.19,-91.598,51.161],[-91.893,51.487,-91.886,51.458],[-91.929,51.892,-91.915,51.762],[-92.299,51.974,-92.237,51.948,-92.155,51.951,-91.97,51.988]]},{"n":"Albert Nile","z":2,"b":[31.382,2.401,32.016,3.614],"l":[[32.016,3.614,31.935,3.528,31.885,3.508,31.839,3.526,31.785,3.523,31.724,3.499,31.624,3.363,31.485,3.117,31.404,2.951,31.382,2.864,31.388,2.804,31.421,2.77,31.434,2.719,31.427,2.651,31.45,2.588,31.503,2.53,31.515,2.476,31.475,2.401]]},{"n":"Aldan","z":4,"b":[123.063,56.528,135.834,63.49],"l":[[123.566,56.528,123.205,56.561,123.101,56.558,123.066,56.58,123.063,56.625,123.086,56.68,123.134,56.746,123.17,56.837,123.193,56.953,123.294,57.091,123.474,57.248,123.555,57.341,123.537,57.369,123.552,57.407,123.598,57.454,123.59,57.485,123.528,57.5,123.505,57.539,123.521,57.603,123.546,57.651,123.58,57.682,123.63,57.685,123.697,57.658,123.757,57.669,123.81,57.718,123.804,57.789,123.739,57.882,123.719,57.977,123.746,58.074,123.824,58.154,123.955,58.214,123.985,58.271,123.989,58.291,124.021,58.315,124.101,58.328,124.229,58.332,124.298,58.366,124.31,58.43,124.348,58.503,124.445,58.628,124.578,58.72,124.654,58.749,124.716,58.744,124.759,58.761,124.782,58.8,124.845,58.824,124.945,58.832,125.032,58.86,125.104,58.908,125.144,58.95,125.152,58.988,125.205,59.016,125.304,59.035,125.339,59.065,125.31,59.107,125.309,59.142,125.336,59.17,125.338,59.206,125.315,59.249,125.314,59.286,125.335,59.316,125.406,59.314,125.527,59.281,125.636,59.226,125.735,59.15,125.877,59.092,126.064,59.053,126.193,59.007,126.265,58.955,126.471,58.896,126.809,58.832,127.008,58.78,127.068,58.742,127.14,58.747,127.221,58.795,127.32,58.811,127.436,58.795,127.521,58.805,127.577,58.843,127.657,58.85,127.761,58.825,127.815,58.792,127.818,58.75,127.855,58.716,127.928,58.689,128.004,58.712,128.082,58.784,128.157,58.818,128.229,58.813,128.287,58.792,128.329,58.754,128.377,58.766,128.432,58.826,128.478,58.817,128.515,58.739,128.569,58.694,128.64,58.684,128.688,58.648,128.712,58.588,128.816,58.545,128.999,58.518,129.17,58.518,129.326,58.545,129.489,58.538,129.658,58.5,129.837,58.524,130.026,58.612,130.234,58.645,130.461,58.622,130.586,58.628,130.608,58.662,130.609,58.714,130.588,58.785,130.602,58.835,130.65,58.865,130.782,58.9,130.998,58.938,131.141,58.937,131.21,58.899,131.266,58.904,131.307,58.952,131.362,58.965,131.431,58.945,131.526,58.947,131.648,58.97,131.768,59.01,131.887,59.067,131.948,59.116,131.95,59.157,132.003,59.172,132.106,59.161,132.209,59.177,132.366,59.242,132.638,59.372,132.806,59.412,132.96,59.406,133.058,59.459,133.101,59.573,133.179,59.635,133.291,59.646,133.53,59.79,133.896,60.069,134.198,60.267,134.554,60.444,134.705,60.46,134.839,60.44,135.007,60.389,135.104,60.392,135.13,60.446,135.089,60.506,134.982,60.57,134.927,60.629,134.924,60.685,135.019,60.754,135.211,60.836,135.362,60.946,135.472,61.084,135.506,61.187,135.462,61.256,135.462,61.313,135.504,61.357,135.478,61.394,135.383,61.424,135.383,61.47,135.476,61.532,135.526,61.58,135.533,61.614,135.485,61.632,135.383,61.633,135.293,61.662,135.215,61.718,135.264,61.761,135.438,61.791,135.53,61.843,135.539,61.917,135.599,61.982,135.709,62.036,135.738,62.094,135.685,62.156,135.715,62.212,135.826,62.261,135.834,62.315,135.739,62.376,135.651,62.473,135.572,62.608,135.489,62.677,135.404,62.679,135.325,62.661,135.252,62.622,135.11,62.617,134.898,62.647,134.638,62.718,134.332,62.83,134.184,62.911,134.194,62.962,134.146,62.989,134.039,62.992,133.972,63.007,133.946,63.035,133.946,63.074,133.803,63.132,133.441,63.091,133.224,63.092,133.151,63.135,133.146,63.177,133.209,63.219,133.236,63.274,133.23,63.345,132.974,63.356,132.469,63.31,132.018,63.318,131.619,63.382,131.343,63.386,131.19,63.331,131.007,63.313,130.689,63.341,130.431,63.339,130.105,63.377,129.384,63.49]]},{"n":"Allegheny","z":3,"b":[-79.991,40.448,-77.86,42.144],"l":[[-77.86,41.832,-77.999,41.781,-78.083,41.771,-78.197,41.79,-78.33,41.878,-78.367,41.942,-78.351,42.013,-78.42,42.059,-78.573,42.081,-78.674,42.11,-78.723,42.144,-78.797,42.128,-78.895,42.061,-78.955,41.985,-78.977,41.899,-79.059,41.849,-79.201,41.834,-79.308,41.789,-79.381,41.714,-79.417,41.65,-79.416,41.571,-79.452,41.506,-79.513,41.48,-79.61,41.472,-79.699,41.447,-79.78,41.405,-79.81,41.374,-79.79,41.352,-79.798,41.328,-79.834,41.3,-79.816,41.264,-79.746,41.221,-79.682,41.155,-79.624,41.066,-79.604,41.012,-79.62,40.992,-79.587,40.966,-79.523,40.939,-79.515,40.906,-79.543,40.824,-79.698,40.637,-79.83,40.519,-79.946,40.456,-79.991,40.448]]},{"n":"Amazonas","z":2,"b":[-73.489,-4.445,-52.664,-1.552],"l":[[-52.664,-1.552,-52.874,-1.611,-53,-1.668,-53.145,-1.764,-53.358,-1.838,-53.639,-1.892,-53.843,-1.958,-53.971,-2.038,-54.045,-2.141,-54.064,-2.267,-54.187,-2.358,-54.468,-2.44,-54.525,-2.44,-54.564,-2.441,-54.639,-2.419,-54.676,-2.359,-54.675,-2.253,-54.694,-2.184,-54.734,-2.152,-54.794,-2.12,-54.873,-2.089,-54.957,-2.104,-55.046,-2.167,-55.15,-2.18,-55.269,-2.145,-55.375,-2.083,-55.458,-1.987,-55.515,-1.949,-55.555,-1.924,-55.606,-1.918,-55.669,-1.927,-55.736,-1.964,-55.84,-1.99,-55.981,-2.006,-56.129,-2.08,-56.284,-2.212,-56.401,-2.336,-56.48,-2.454,-56.549,-2.527,-56.607,-2.556,-56.676,-2.575,-56.756,-2.582,-56.847,-2.567,-56.951,-2.528,-57.05,-2.523,-57.143,-2.551,-57.244,-2.526,-57.353,-2.448,-57.44,-2.405,-57.506,-2.396,-57.605,-2.481,-57.74,-2.662,-57.883,-2.784,-58.035,-2.848,-58.135,-2.959,-58.184,-3.117,-58.235,-3.2,-58.268,-3.214,-58.316,-3.212,-58.38,-3.202,-58.453,-3.182,-58.549,-3.205,-58.642,-3.304,-58.689,-3.342,-58.805,-3.347,-58.89,-3.323,-58.982,-3.271,-59.091,-3.239,-59.217,-3.228,-59.381,-3.184,-59.584,-3.109,-59.735,-3.078,-59.837,-3.092,-59.897,-3.128,-59.906,-3.171,-59.925,-3.206,-59.957,-3.237,-60.105,-3.306,-60.203,-3.325,-60.279,-3.302,-60.411,-3.3,-60.598,-3.319,-60.747,-3.398,-60.858,-3.538,-61.036,-3.61,-61.28,-3.614,-61.468,-3.676,-61.598,-3.796,-61.717,-3.883,-61.824,-3.936,-61.893,-3.947,-62.011,-3.896,-62.277,-3.762,-62.367,-3.718,-62.393,-3.716,-62.423,-3.731,-62.449,-3.802,-62.543,-3.851,-62.705,-3.878,-62.863,-3.938,-63.018,-4.032,-63.097,-4.078,-63.152,-4.072,-63.177,-4.035,-63.216,-3.974,-63.283,-3.923,-63.45,-3.871,-63.573,-3.854,-63.654,-3.872,-63.747,-3.868,-63.854,-3.843,-64.035,-3.765,-64.291,-3.632,-64.458,-3.524,-64.576,-3.398,-64.79,-3.222,-64.905,-3.128,-65.07,-2.971,-65.18,-2.835,-65.288,-2.747,-65.395,-2.707,-65.459,-2.663,-65.48,-2.615,-65.558,-2.594,-65.761,-2.603,-65.901,-2.493,-65.977,-2.466,-66.038,-2.485,-66.096,-2.477,-66.153,-2.442,-66.21,-2.46,-66.269,-2.532,-66.334,-2.537,-66.405,-2.475,-66.471,-2.439,-66.531,-2.429,-66.573,-2.478,-66.596,-2.588,-66.641,-2.659,-66.706,-2.692,-66.801,-2.715,-66.925,-2.727,-67.077,-2.695,-67.258,-2.619,-67.384,-2.623,-67.455,-2.707,-67.572,-2.779,-67.736,-2.838,-67.846,-2.928,-67.932,-3.108,-67.927,-3.223,-67.946,-3.283,-67.988,-3.326,-68.095,-3.334,-68.266,-3.307,-68.596,-3.345,-69.084,-3.449,-69.352,-3.553,-69.399,-3.655,-69.416,-3.764,-69.404,-3.879,-69.425,-3.974,-69.478,-4.049,-69.481,-4.111,-69.434,-4.161,-69.459,-4.223,-69.556,-4.299,-69.685,-4.335,-69.847,-4.331,-69.948,-4.285,-69.966,-4.236,-70.017,-4.162,-70.095,-4.092,-70.168,-4.05,-70.198,-3.995,-70.24,-3.883,-70.298,-3.844,-70.339,-3.814,-70.379,-3.819,-70.421,-3.85,-70.486,-3.869,-70.53,-3.866,-70.557,-3.87,-70.9,-3.96,-71.058,-3.985,-71.138,-3.963,-71.193,-3.903,-71.283,-3.869,-71.408,-3.862,-71.521,-3.826,-71.623,-3.761,-71.689,-3.65,-71.718,-3.492,-71.752,-3.395,-71.789,-3.359,-71.914,-3.384,-72.126,-3.468,-72.293,-3.503,-72.476,-3.481,-72.747,-3.457,-72.891,-3.467,-72.997,-3.504,-73.094,-3.562,-73.18,-3.643,-73.211,-3.783,-73.189,-3.984,-73.192,-4.099,-73.221,-4.13,-73.268,-4.136,-73.334,-4.117,-73.366,-4.133,-73.364,-4.183,-73.385,-4.231,-73.431,-4.277,-73.463,-4.336,-73.489,-4.445]]},{"n":"Amu Darya","z":4.7,"b":[58.316,36.95,68.212,44.426],"l":[[68.212,37.022,68.068,36.95,67.958,36.972,67.834,37.064,67.766,37.14,67.753,37.2,67.7,37.227,67.607,37.223,67.547,37.236,67.517,37.267,67.442,37.258,67.32,37.21,67.195,37.235,67.069,37.335,66.828,37.371,66.472,37.345,66.108,37.415,65.737,37.582,65.48,37.68,65.337,37.712,65.22,37.797,65.131,37.938,64.907,38.088,64.549,38.247,64.272,38.432,64.076,38.642,63.953,38.8,63.904,38.904,63.724,39.049,63.413,39.232,63.198,39.33,63.081,39.344,63.014,39.4,62.998,39.5,62.897,39.608,62.71,39.723,62.573,39.845,62.442,40.036,62.375,40.332,62.298,40.468,62.188,40.541,62.095,40.683,62.018,40.894,61.954,41.031,61.903,41.094,61.8,41.163,61.645,41.24,61.497,41.276,61.357,41.272,61.263,41.3,61.215,41.36,61.031,41.487,60.71,41.68,60.527,41.825,60.482,41.919,60.42,41.978,60.343,42,60.291,42.04,60.266,42.1,60.203,42.164,60.102,42.232,59.95,42.291,59.745,42.341,59.587,42.416,59.475,42.516,59.387,42.574,59.322,42.591,59.3,42.632,59.319,42.697,59.298,42.744,59.236,42.772,59.21,42.808,59.222,42.852,59.204,42.904,59.156,42.966,59.139,43.026,59.153,43.084,59.141,43.152,59.103,43.23,59.093,43.288,59.097,43.294],[59.099,43.292,59.097,43.294],[59.097,43.294,59.112,43.326,59.098,43.379,59.052,43.446,59.036,43.498,59.05,43.533,59.106,43.561,59.204,43.581,59.255,43.633,59.26,43.716,59.211,43.805,59.107,43.901,59.082,43.971,59.134,44.016,59.255,44.066,59.541,44.148],[59.097,43.294,59.005,43.423,58.993,43.526,59.032,43.643,59.019,43.732,58.952,43.792,58.831,43.839,58.655,43.872,58.57,43.948,58.575,44.067,58.512,44.202,58.316,44.426]]},{"n":"Amur","z":3,"b":[127.492,47.681,140.84,53.22],"l":[[136.81,49.759,136.843,49.729,136.844,49.678,136.825,49.595,136.721,49.52,136.53,49.452,136.394,49.367,136.311,49.265,136.271,49.171,136.275,49.086,136.162,48.987,135.818,48.817,135.745,48.764,135.669,48.755,135.565,48.771,135.445,48.768,135.307,48.746,135.185,48.705,135.077,48.645,135.026,48.571,135.032,48.482,134.965,48.421,134.823,48.388,134.709,48.383,134.623,48.406,134.487,48.403,134.206,48.36,133.842,48.274,133.672,48.208,133.573,48.133,133.468,48.097,133.301,48.102,133.144,48.106,133.02,48.064,132.877,47.979,132.773,47.94,132.707,47.947,132.637,47.89,132.562,47.769,132.476,47.715,132.38,47.729,132.15,47.718,131.785,47.681,131.557,47.682,131.464,47.723,131.319,47.728,131.122,47.698,131.003,47.691,130.962,47.709,130.933,47.76,130.915,47.843,130.849,47.929,130.733,48.019,130.712,48.128,130.787,48.255,130.804,48.341,130.763,48.388,130.747,48.43,130.659,48.483,130.597,48.575,130.552,48.603,130.566,48.68,130.617,48.773,130.553,48.861,130.355,48.866,130.196,48.892,130.037,48.972,129.793,49.199,129.671,49.278,129.591,49.287,129.534,49.323,129.498,49.389,129.441,49.389,129.385,49.389,129.35,49.362,129.31,49.354,129.248,49.379,129.185,49.381,129.12,49.362,129.065,49.375,129.02,49.419,128.938,49.449,128.819,49.464,128.77,49.495,128.791,49.542,128.769,49.577,128.704,49.6,128.527,49.594,128.237,49.559,128,49.569,127.814,49.622,127.711,49.672,127.69,49.717,127.637,49.76,127.551,49.802,127.502,49.873,127.492,49.975,127.512,50.072,127.59,50.209],[139.858,51.579,139.806,51.563,139.705,51.558,139.602,51.544,139.495,51.505,139.449,51.449,139.378,51.405,139.283,51.374,139.159,51.363,139.008,51.375,138.918,51.35,138.89,51.291,138.825,51.248,138.675,51.207,138.068,50.856,137.708,50.714,137.391,50.665,137.187,50.607,137.095,50.54,137.054,50.493,137.07,50.454],[140.114,51.764,140.147,51.786,140.206,51.814,140.218,51.875,140.182,51.964,140.215,52.046,140.317,52.121,140.385,52.226,140.42,52.362,140.405,52.468,140.338,52.545,140.262,52.592,140.178,52.61,140.125,52.657,140.104,52.734,139.996,52.822,139.801,52.922,139.757,53.032,139.863,53.153,140.003,53.215,140.176,53.22,140.343,53.188,140.504,53.119,140.614,53.091,140.707,53.111,140.783,53.105,140.84,53.088]]},{"n":"Anadyr’","z":5,"b":[168.574,64.684,174.549,67.099],"l":[[170.966,67.099,171.345,66.974,171.448,66.826,171.508,66.766,171.559,66.745,171.377,66.686,170.962,66.587,170.659,66.486,170.468,66.383,170.248,66.304,169.998,66.249,169.857,66.206,169.824,66.175,169.81,66.071,169.815,65.893,169.766,65.791,169.612,65.755,169.123,65.647,168.935,65.582,168.883,65.526,168.786,65.475,168.643,65.428,168.574,65.37,168.581,65.302,168.654,65.245,168.795,65.201,168.814,65.14,168.711,65.065,168.646,64.995,168.618,64.931,168.704,64.866,168.902,64.8,169.146,64.781,169.434,64.808,169.755,64.795,170.107,64.742,170.4,64.72,170.632,64.729,170.733,64.777,170.76,64.823,170.776,64.859,170.788,64.912,170.832,64.928,170.891,64.926,170.977,64.959,171.09,65.026,171.125,65.076,171.079,65.107,171.067,65.162,171.089,65.24,171.261,65.281,171.583,65.284,171.793,65.252,171.892,65.186,172.022,65.152,172.185,65.148,172.294,65.169,172.352,65.214,172.498,65.266,172.732,65.325,172.889,65.392,172.968,65.467,173.053,65.516,173.145,65.54,173.234,65.544,173.322,65.529,173.37,65.502,173.38,65.464,173.456,65.415,173.6,65.354,173.676,65.31,173.685,65.282,173.668,65.242,173.606,65.164,173.55,65.124,173.543,65.09,173.567,65.048,173.648,65.021,173.787,65.007,173.91,64.951,174.017,64.855,174.164,64.778,174.442,64.692,174.549,64.684]]},{"n":"Angara","z":2.1,"b":[92.998,51.87,104.867,58.973],"l":[[104.867,51.87,104.437,52.165,104.29,52.289,104.281,52.339,104.205,52.399,104.059,52.47,103.973,52.54,103.945,52.61,103.877,52.674,103.77,52.733,103.71,52.799,103.698,52.873,103.617,52.934,103.469,52.982,103.39,53.032,103.374,53.107],[101.803,56.291,101.817,56.561,101.77,56.687,101.668,56.76,101.561,56.801,101.449,56.808,101.391,56.848,101.387,56.919,101.461,56.97,101.612,57.002,101.778,56.988,101.959,56.929,102.113,56.898,102.239,56.894,102.316,56.92,102.358,57.004],[102.674,57.943,102.795,58.122,102.837,58.245,102.84,58.373,102.78,58.524,102.659,58.698,102.48,58.83,102.462,58.836],[102.489,58.829,102.462,58.836],[102.462,58.836,102.101,58.933],[102.462,58.836,102.243,58.919,102.101,58.933],[102.101,58.933,101.842,58.956,101.276,58.941,100.98,58.899,100.954,58.83,100.817,58.758,100.568,58.681,100.437,58.627,100.424,58.598,100.434,58.555,100.465,58.5,100.441,58.439,100.361,58.371,100.251,58.346,100.11,58.364,99.986,58.412,99.881,58.489,99.754,58.536,99.603,58.553,99.472,58.594,99.361,58.66,99.241,58.701,99.113,58.717,98.962,58.701,98.788,58.652,98.691,58.6,98.67,58.547,98.628,58.506,98.565,58.476,98.322,58.446,97.899,58.413,97.572,58.408,97.339,58.432,97.171,58.43,97.066,58.404,96.984,58.393,96.923,58.399,96.85,58.432,96.765,58.491,96.514,58.527,96.098,58.541,95.777,58.522,95.55,58.472,95.418,58.425,95.383,58.381,95.235,58.307,94.975,58.203,94.599,58.14,94.108,58.12,93.73,58.129,93.467,58.168,93.279,58.161,93.166,58.11,93.08,58.086,92.998,58.1],[102.101,58.933,101.948,58.973]]},{"n":"Araguaia","z":4,"b":[-53.211,-17.855,-48.159,-5.303],"l":[[-53.115,-17.855,-53.211,-17.409,-53.202,-17.302,-53.158,-17.209,-53.078,-17.095,-53.033,-16.995,-53.024,-16.91,-52.943,-16.814,-52.789,-16.705,-52.68,-16.576,-52.615,-16.427,-52.545,-16.329,-52.469,-16.282,-52.434,-16.218,-52.441,-16.136,-52.417,-16.082,-52.362,-16.058,-52.308,-16.009,-52.257,-15.936,-52.141,-15.872,-51.962,-15.819,-51.847,-15.747,-51.799,-15.656,-51.779,-15.593,-51.788,-15.557,-51.773,-15.523,-51.732,-15.49,-51.696,-15.403,-51.665,-15.261,-51.59,-15.142,-51.471,-15.044,-51.372,-15.024,-51.286,-15.003,-51.246,-15.004,-51.189,-14.977,-51.114,-14.838,-50.978,-14.463,-50.941,-14.201,-50.909,-14.107,-50.868,-14.095,-50.852,-13.995,-50.861,-13.807,-50.8,-13.612,-50.667,-13.41,-50.598,-13.249,-50.595,-13.131,-50.566,-13.015,-50.484,-12.844,-50.566,-12.82,-50.614,-12.764,-50.655,-12.667,-50.66,-12.558,-50.629,-12.437,-50.632,-12.289,-50.668,-12.112,-50.675,-11.99,-50.653,-11.922,-50.661,-11.854,-50.717,-11.749,-50.666,-11.652,-50.671,-11.574,-50.717,-11.483,-50.708,-11.346,-50.645,-11.163,-50.61,-11.001,-50.6,-10.79,-50.599,-10.69,-50.551,-10.567,-50.455,-10.388,-50.403,-10.258,-50.395,-10.177,-50.337,-10.034,-50.175,-9.73,-50.084,-9.422,-49.979,-9.22,-49.83,-9.022,-49.708,-8.895,-49.613,-8.839,-49.502,-8.704,-49.374,-8.489,-49.283,-8.301,-49.227,-8.14,-49.19,-7.995,-49.172,-7.867,-49.22,-7.752,-49.334,-7.649,-49.34,-7.51,-49.24,-7.335,-49.196,-7.167,-49.21,-7.006,-49.08,-6.856,-48.805,-6.718,-48.653,-6.596,-48.622,-6.489,-48.58,-6.414,-48.527,-6.372,-48.471,-6.353,-48.41,-6.358,-48.391,-6.309,-48.414,-6.207,-48.39,-6.142,-48.319,-6.112,-48.297,-6.067,-48.321,-6.007,-48.307,-5.966,-48.254,-5.945,-48.245,-5.889,-48.28,-5.796,-48.259,-5.724,-48.181,-5.672,-48.159,-5.619,-48.193,-5.565,-48.231,-5.533,-48.275,-5.523,-48.317,-5.486,-48.357,-5.424,-48.422,-5.398,-48.511,-5.408,-48.598,-5.399,-48.723,-5.355,-48.937,-5.303]]},{"n":"Ergun","z":3,"b":[117.873,49.513,121.405,53.317],"l":[[121.405,53.317,120.985,53.285,120.704,53.172,120.421,52.968,120.218,52.84,120.095,52.787,120.044,52.718,120.068,52.633,120.173,52.602,120.36,52.627,120.521,52.615,120.656,52.567,120.699,52.494,120.65,52.396,120.665,52.3,120.744,52.205,120.75,52.097,120.682,51.973,120.511,51.849,120.237,51.723,120.067,51.601,119.967,51.422,119.813,51.267,119.757,51.179,119.746,51.108,119.685,51.03,119.573,50.947,119.512,50.863,119.502,50.779,119.446,50.703,119.344,50.634,119.281,50.561,119.256,50.484,119.217,50.433,119.164,50.406,119.192,50.38,119.302,50.354,119.346,50.279,119.326,50.155,119.26,50.066,119.147,50.013,118.98,49.979,118.756,49.963,118.452,49.845,117.873,49.513]]},{"n":"Arkansas","z":4,"b":[-106.381,33.797,-91.077,39.222],"l":[[-91.077,33.797,-91.122,33.824,-91.15,33.864,-91.167,33.922,-91.217,33.955,-91.299,33.962,-91.323,33.974,-91.33,33.991,-91.343,34.004,-91.387,34.005,-91.419,34.026,-91.439,34.066,-91.488,34.091,-91.564,34.1,-91.682,34.153,-91.844,34.249,-91.921,34.271,-91.939,34.267,-91.973,34.275,-92.017,34.316,-92.109,34.448,-92.155,34.566,-92.155,34.673,-92.225,34.756,-92.365,34.818,-92.447,34.877,-92.469,34.934,-92.506,34.972,-92.558,34.993,-92.577,35.035,-92.562,35.096,-92.595,35.119,-92.713,35.095,-92.831,35.161,-92.93,35.184,-93.05,35.185,-93.193,35.244,-93.358,35.363,-93.521,35.415,-93.682,35.399,-93.797,35.421,-93.864,35.479,-93.958,35.494,-94.078,35.467,-94.139,35.434,-94.141,35.396,-94.162,35.373,-94.202,35.367,-94.268,35.385,-94.358,35.43,-94.43,35.415,-94.484,35.342,-94.529,35.314,-94.567,35.331,-94.602,35.325,-94.635,35.297,-94.666,35.3,-94.694,35.333,-94.792,35.375,-94.959,35.425,-95.074,35.488,-95.138,35.565,-95.19,35.603,-95.229,35.605,-95.245,35.637,-95.236,35.699,-95.251,35.747,-95.29,35.78,-95.388,35.795,-95.545,35.79,-95.63,35.816,-95.643,35.875,-95.721,35.925,-95.864,35.967,-95.958,36.029,-96.004,36.11,-96.084,36.152,-96.199,36.153,-96.26,36.174,-96.266,36.213,-96.316,36.256,-96.409,36.303,-96.477,36.322,-96.521,36.312,-96.542,36.332,-96.54,36.382,-96.578,36.415,-96.656,36.431,-96.711,36.473,-96.742,36.539,-96.767,36.542,-96.806,36.532,-96.865,36.494,-96.921,36.469,-96.96,36.472,-96.986,36.486,-96.999,36.513,-96.979,36.542,-96.944,36.567,-96.94,36.579,-96.957,36.591,-97,36.595,-97.041,36.619,-97.053,36.664,-96.993,36.706,-96.862,36.745,-96.836,36.766,-96.838,36.782,-96.877,36.797,-96.913,36.866,-96.932,36.981,-96.986,37.058,-97.074,37.098,-97.135,37.189,-97.167,37.332,-97.237,37.499,-97.344,37.69,-97.695,37.925,-98.289,38.205,-98.633,38.346,-98.728,38.349,-98.854,38.308,-99.011,38.223,-99.218,38.055,-99.474,37.805,-99.698,37.697,-99.987,37.748,-100.776,37.935,-101.127,37.971,-101.304,37.918,-101.627,37.95,-102.095,38.066,-102.559,38.118,-103.017,38.106,-103.32,38.081,-103.467,38.042,-103.594,38.048,-103.699,38.099,-103.833,38.136,-104.075,38.174,-104.237,38.238,-104.411,38.26,-104.649,38.261,-104.826,38.298,-104.94,38.371,-105.093,38.425,-105.283,38.459,-105.47,38.451,-105.654,38.4,-105.832,38.425,-106.005,38.527,-106.089,38.611,-106.087,38.677,-106.135,38.802,-106.286,39.078,-106.381,39.222]]},{"n":"Atbara","z":5,"b":[33.952,14.935,35.958,17.699],"l":[[35.583,16,35.352,16.313,35.273,16.445,35.268,16.503,35.19,16.632,35.036,16.834,34.955,16.963,34.945,17.02,34.767,17.138,34.421,17.319,34.228,17.453,34.188,17.541,34.122,17.606,33.952,17.699],[35.586,16.004,35.613,15.716,35.714,15.469,35.9,15.169,35.95,15.045,35.958,14.999,35.924,14.935]]},{"n":"Athabasca","z":4.7,"b":[-118.084,52.171,-111.07,59.005],"l":[[-117.575,52.171,-117.543,52.209,-117.552,52.271,-117.645,52.456,-117.674,52.537,-117.727,52.578,-117.814,52.606,-117.864,52.647,-117.878,52.701,-117.926,52.757,-118.007,52.815,-118.045,52.872,-118.039,52.929,-118.052,52.977,-118.084,53.018,-118.036,53.113,-117.907,53.263,-117.679,53.415,-117.354,53.57,-117.184,53.676,-117.17,53.735,-117.173,53.775,-117.193,53.796,-117.172,53.839,-117.108,53.907,-116.924,54.01,-116.617,54.151,-116.414,54.228,-116.315,54.243,-116.133,54.224,-115.868,54.172,-115.615,54.149,-115.373,54.155,-115.176,54.2,-115.021,54.285,-114.859,54.331,-114.689,54.34,-114.536,54.39,-114.399,54.481,-114.332,54.565,-114.336,54.642,-114.265,54.804,-114.049,55.174],[-114.042,55.168,-113.76,55.228,-113.599,55.172,-113.466,55.018,-113.413,54.901,-113.44,54.822,-113.41,54.771,-113.324,54.748,-113.259,54.773,-113.215,54.846,-113.077,54.909,-112.846,54.961,-112.733,55,-112.739,55.028,-112.772,55.05,-112.834,55.066,-112.85,55.116,-112.82,55.198,-112.774,55.256,-112.712,55.291,-112.708,55.385,-112.761,55.537,-112.74,55.648,-112.645,55.716,-112.614,55.789,-112.648,55.868,-112.658,55.947,-112.645,56.025,-112.61,56.098,-112.555,56.165,-112.58,56.256,-112.683,56.371,-112.65,56.464,-112.482,56.536,-112.257,56.573,-111.974,56.574,-111.728,56.615,-111.518,56.694,-111.423,56.798,-111.442,56.926,-111.488,57.023,-111.559,57.09,-111.612,57.169,-111.647,57.261,-111.657,57.337,-111.644,57.397,-111.57,57.513,-111.435,57.684,-111.381,57.808,-111.407,57.885,-111.399,57.951,-111.357,58.004,-111.348,58.048,-111.372,58.081,-111.377,58.121,-111.365,58.167,-111.391,58.207,-111.455,58.241,-111.497,58.294,-111.518,58.365,-111.514,58.412,-111.487,58.434,-111.372,58.448,-111.17,58.454,-111.07,58.468,-111.075,58.505],[-111.28,58.788,-111.2,58.892,-111.233,58.946,-111.411,59.005]]},{"n":"Ayeyarwady","z":3,"b":[94.74,16.183,97.519,25.701],"l":[[97.519,25.701,97.508,25.69,97.515,25.668,97.506,25.59,97.449,25.417,97.394,25.337,97.34,25.35,97.292,25.31,97.249,25.216,97.25,25.152,97.293,25.12,97.29,25.1,97.239,25.091,97.216,25.064,97.22,25.019,97.196,24.981,97.117,24.933,97.055,24.835,97.055,24.755,97.095,24.659,97.117,24.552,97.12,24.432,97.142,24.347,97.184,24.296,97.198,24.252,97.183,24.214,97.114,24.178,96.991,24.145,96.892,24.169,96.819,24.25,96.737,24.307,96.647,24.341,96.561,24.344,96.479,24.317,96.402,24.251,96.331,24.148,96.291,24.059,96.28,23.946,96.085,23.653,96.006,23.359,95.979,22.966,95.949,22.754,95.917,22.723,95.923,22.663,95.968,22.574,95.984,22.495,95.97,22.427,95.976,22.377,96,22.347,96.02,22.23,96.034,22.026,96.009,21.907,95.946,21.873,95.879,21.875,95.811,21.913,95.71,21.928,95.578,21.921,95.481,21.871,95.418,21.779,95.373,21.666,95.344,21.531,95.315,21.458,95.269,21.438,95.173,21.345,95.061,21.287,94.9,21.232,94.82,21.146,94.82,21.03,94.794,20.933,94.74,20.854,94.761,20.702,94.857,20.477,94.907,20.305,94.912,20.184,94.956,20.071,95.081,19.91,95.14,19.845,95.169,19.71,95.188,19.485,95.177,19.277,95.136,19.087,95.14,18.945,95.189,18.852,95.18,18.771,95.113,18.702,95.076,18.641,95.067,18.588,95.143,18.479,95.303,18.314,95.4,18.181,95.433,18.083,95.452,17.956,95.455,17.856,95.456,17.803,95.472,17.744,95.485,17.698,95.539,17.642,95.561,17.586,95.552,17.529,95.58,17.48,95.645,17.439,95.655,17.39,95.611,17.332,95.603,17.254,95.632,17.158,95.625,17.086,95.582,17.039,95.574,16.977,95.6,16.902,95.543,16.838,95.481,16.814,95.404,16.784,95.331,16.721,95.322,16.647,95.236,16.52,94.992,16.251,94.952,16.21,94.893,16.183]]},{"n":"Back","z":5,"b":[-108.828,64.562,-95.797,66.617],"l":[[-108.828,64.562,-108.533,64.562],[-108.18,64.686,-108.09,64.715],[-107.979,64.72,-107.849,64.755,-107.775,64.803,-107.712,64.877,-107.605,64.953,-107.454,65.032,-107.374,65.108,-107.365,65.18,-107.377,65.244,-107.408,65.298,-107.362,65.366,-107.239,65.447,-107.103,65.478,-106.954,65.458,-106.802,65.399,-106.648,65.301,-106.462,65.249,-106.244,65.244,-106.127,65.229,-106.111,65.205,-105.897,65.164,-105.275,65.077,-105.005,65.071,-104.648,65.112,-104.112,65.197,-103.709,65.283,-103.436,65.369,-103.277,65.445,-103.231,65.509,-103.159,65.556,-103.061,65.584,-102.812,65.601,-102.413,65.605,-102.178,65.63,-102.107,65.676,-102.096,65.724,-102.144,65.772,-102.068,65.834,-101.77,65.947],[-98.656,65.927,-98.6,65.884,-98.541,65.887,-98.461,65.92,-98.356,65.929,-98.225,65.914,-98.145,65.926,-98.116,65.966,-98.06,65.999,-97.979,66.024,-97.858,66.013,-97.698,65.964,-97.53,65.949,-97.267,65.974],[-96.995,66.152,-96.654,66.092,-96.475,66.093,-96.344,66.137,-96.192,66.206,-96.018,66.302,-95.896,66.389,-95.826,66.466,-95.799,66.527,-95.797,66.617]]},{"n":"Bafing","z":4,"b":[-12.088,10.522,-10.215,13.778],"l":[[-12.088,10.59,-12.006,10.529,-11.93,10.522,-11.835,10.549,-11.775,10.619,-11.752,10.731,-11.728,10.792,-11.691,10.809,-11.621,10.812,-11.602,10.864,-11.611,10.965,-11.58,11.073,-11.509,11.188,-11.443,11.251,-11.385,11.263,-11.303,11.317,-11.198,11.413,-11.128,11.498,-11.092,11.574,-11.059,11.6,-11.029,11.575,-10.965,11.574,-10.869,11.595,-10.744,11.734,-10.59,11.99,-10.466,12.139,-10.373,12.18,-10.341,12.231,-10.37,12.292,-10.345,12.395,-10.267,12.54,-10.238,12.636,-10.26,12.684,-10.252,12.725,-10.215,12.759,-10.223,12.797,-10.274,12.838,-10.294,12.883,-10.282,12.932,-10.292,12.957,-10.323,12.96,-10.352,13.021,-10.379,13.138,-10.43,13.212,-10.505,13.241,-10.609,13.256,-10.742,13.255,-10.804,13.297,-10.796,13.382,-10.772,13.427,-10.738,13.438,-10.734,13.454,-10.753,13.503,-10.833,13.64,-10.867,13.735,-10.873,13.751,-10.892,13.778]]},{"n":"Bahr el Zeraf","z":5,"b":[30.536,7.767,31.139,9.426],"l":[[30.536,7.767,30.621,8.031,30.646,8.212,30.641,8.4,30.669,8.516,30.731,8.56,30.784,8.641,30.829,8.758,30.84,8.873,30.819,8.986,30.891,9.138,31.139,9.426]]},{"n":"Mountain Nile","z":2,"b":[31.148,3.614,32.016,6.83],"l":[[32.016,3.614,31.711,3.944,31.588,4.101,31.544,4.194,31.517,4.326,31.507,4.496,31.525,4.606,31.57,4.656,31.635,4.817,31.719,5.087,31.77,5.3,31.788,5.455,31.789,5.589,31.774,5.702,31.743,5.802,31.673,5.93,31.46,6.357,31.329,6.582,31.148,6.83]]},{"n":"White Nile","z":2,"b":[30.2,6.83,31.148,9.531],"l":[[31.148,6.83,30.922,6.983,30.77,7.153,30.617,7.39,30.547,7.521,30.56,7.545,30.549,7.582,30.512,7.632,30.508,7.693,30.536,7.765,30.508,7.88,30.426,8.037,30.366,8.124,30.329,8.141,30.323,8.227,30.348,8.383,30.339,8.485,30.296,8.534,30.284,8.59,30.301,8.654,30.289,8.704,30.248,8.742,30.218,8.818,30.2,8.933,30.233,9.043,30.316,9.147,30.35,9.237,30.333,9.311,30.37,9.395,30.46,9.489,30.618,9.531,30.846,9.52,31.001,9.494,31.125,9.433]]},{"n":"Bamingui","z":5,"b":[18.886,7.538,19.733,8.836],"l":[[19.733,7.538,19.627,7.56,19.592,7.601,19.592,7.666,19.524,7.725,19.389,7.778,19.331,7.886,19.349,8.049,19.285,8.247,19.064,8.599,19.109,8.656,19.064,8.715,18.886,8.836]]},{"n":"Barito","z":5,"b":[114.067,-3.377,114.93,0.565],"l":[[114.838,0.565,114.871,0.458,114.853,0.416,114.792,0.403,114.736,0.355,114.659,0.229,114.659,0.183,114.637,0.159,114.592,0.142,114.522,0.145,114.425,0.167,114.349,0.174,114.295,0.166,114.259,0.143,114.239,0.106,114.219,0.094,114.192,0.041,114.221,-0.004,114.276,-0.045,114.249,-0.11,114.139,-0.198,114.079,-0.287,114.067,-0.377,114.14,-0.503,114.297,-0.664,114.427,-0.737,114.533,-0.72,114.586,-0.688,114.587,-0.641,114.639,-0.618,114.741,-0.619,114.824,-0.662,114.887,-0.747,114.923,-0.825,114.93,-0.894,114.904,-0.945,114.846,-0.975,114.836,-1.025,114.874,-1.094,114.864,-1.163,114.806,-1.231,114.807,-1.28,114.865,-1.31,114.886,-1.386,114.861,-1.566,114.796,-1.771,114.791,-1.843,114.824,-1.852,114.853,-1.891,114.877,-1.962,114.876,-2.037,114.848,-2.116,114.853,-2.222,114.889,-2.353,114.865,-2.505,114.779,-2.678,114.752,-2.803,114.782,-2.881,114.787,-2.946,114.766,-2.998,114.716,-3.052,114.636,-3.109,114.584,-3.19,114.526,-3.377]]},{"n":"Barwon","z":3,"b":[147.415,-30.125,150.47,-28.558],"l":[[150.47,-28.65,150.341,-28.564,150.15,-28.558,149.853,-28.604,149.677,-28.612,149.595,-28.565,149.512,-28.579,149.467,-28.608,149.433,-28.657,149.335,-28.723,149.174,-28.807,149.081,-28.875,149.058,-28.928,148.993,-28.986,148.887,-29.05,148.796,-29.188,148.72,-29.4,148.641,-29.528,148.558,-29.571,148.429,-29.688,148.254,-29.879,147.978,-30.013,147.415,-30.125]]},{"n":"Benue","z":4,"b":[6.776,7.788,11.924,9.497],"l":[[11.924,9.491,11.783,9.497,11.574,9.409,11.249,9.23,11.038,9.061,10.941,8.901,10.858,8.795,10.788,8.745,10.636,8.707,10.401,8.684,10.218,8.59,10.086,8.427,9.977,8.327,9.847,8.272,9.53,8.121,9.395,8.039,9.337,7.976,9.141,7.892,8.807,7.788,8.437,7.799,7.829,7.99,7.422,8.019,7.156,7.968,6.776,7.793]]},{"n":"Biya","z":5,"b":[85.056,51.762,87.392,52.633],"l":[[87.392,51.762,87.177,51.849,87.107,51.976,87.112,52.171,87.07,52.293,86.982,52.341,86.929,52.387,86.911,52.429,86.868,52.454,86.798,52.46,86.688,52.507,86.537,52.595,86.431,52.633,86.372,52.619,86.334,52.586,86.318,52.534,86.247,52.54,86.121,52.603,86.019,52.617,85.942,52.58,85.855,52.574,85.758,52.599,85.604,52.6,85.392,52.578,85.228,52.534,85.056,52.434]]},{"n":"Borcea","z":2.1,"b":[27.267,44.138,27.935,44.744],"l":[[27.267,44.138,27.769,44.363,27.93,44.486,27.935,44.592,27.922,44.67,27.877,44.744]]},{"n":"Braco Menor","z":4,"b":[-50.484,-12.846,-49.889,-9.73],"l":[[-50.484,-12.846,-50.392,-12.617,-50.296,-12.49,-50.164,-12.388,-50.105,-12.313,-50.12,-12.266,-50.114,-12.209,-50.06,-12.093,-50.036,-12.065,-49.999,-12.058,-49.948,-12.071,-49.914,-12.034,-49.897,-11.945,-49.904,-11.873,-49.935,-11.816,-49.931,-11.693,-49.892,-11.504,-49.889,-11.399,-49.92,-11.376,-49.933,-11.316,-49.926,-11.217,-49.939,-11.111,-49.974,-10.998,-49.978,-10.772,-49.954,-10.435,-49.963,-10.191,-50.006,-10.04,-50.048,-9.96,-50.09,-9.952,-50.126,-9.923,-50.158,-9.873,-50.174,-9.819,-50.175,-9.73]]},{"n":"Brahmaputra","z":2.1,"b":[89.613,23.459,95.444,28.003],"l":[[89.739,24.33,89.714,24.135,89.708,23.962,89.744,23.859,89.868,23.72,89.994,23.621,90.074,23.573,90.267,23.459],[89.739,24.33,89.809,24.172,89.923,24.082,90.116,23.971,90.214,23.869,90.218,23.776,90.244,23.735,90.291,23.745,90.355,23.709,90.476,23.583,90.562,23.537],[95.396,28.003,95.444,27.884,95.369,27.806,95.189,27.729,95.116,27.698,95.104,27.683,95.109,27.678,95.122,27.676,95.137,27.666,95.139,27.644,95.084,27.615,94.972,27.586,94.878,27.543,94.801,27.487,94.725,27.413,94.652,27.322,94.63,27.278,94.597,27.234,94.567,27.189,94.559,27.131,94.518,27.072,94.443,27.014,94.335,26.952,94.193,26.885,94.12,26.867,94.023,26.867,93.752,26.732,93.403,26.739,93.245,26.718,93.161,26.671,93.009,26.636,92.787,26.612,92.656,26.58,92.615,26.538,92.544,26.521,92.442,26.529,92.312,26.495,92.155,26.418,92.071,26.361,92.062,26.323,91.966,26.27,91.644,26.164,91.547,26.148,91.436,26.151,91.25,26.183,91.152,26.205,91.052,26.192,90.917,26.151,90.76,26.155,90.58,26.204,90.356,26.174,90.088,26.065,89.89,25.901,89.762,25.682,89.699,25.49,89.701,25.325,89.68,25.168,89.621,25.026,89.613,24.948,89.62,24.895,89.729,24.649,89.75,24.411,89.739,24.33]]},{"n":"Branco","z":5,"b":[-63.78,-1.389,-60.525,3.717],"l":[[-63.78,2.846,-63.703,3.001,-63.699,3.075,-63.741,3.12,-63.761,3.163,-63.757,3.227,-63.779,3.464,-63.743,3.597,-63.658,3.705,-63.541,3.717,-63.392,3.634,-63.289,3.615,-63.231,3.66,-63.189,3.635,-63.161,3.539,-63.111,3.459,-63.04,3.393,-62.865,3.304,-62.586,3.194,-62.383,3.142,-62.256,3.148,-62.165,3.177,-62.11,3.227,-62.047,3.251,-61.975,3.247,-61.901,3.269,-61.825,3.316,-61.72,3.333,-61.585,3.321,-61.5,3.324,-61.446,3.355,-61.353,3.359,-61.299,3.382,-61.253,3.425,-61.095,3.452,-60.825,3.465,-60.666,3.455,-60.616,3.421,-60.569,3.318,-60.525,3.146,-60.576,2.947,-60.722,2.722,-60.801,2.54,-60.813,2.404,-60.871,2.284,-60.974,2.182,-61.03,2.113,-61.039,2.079,-61.023,2.03,-60.983,1.967,-61.026,1.864,-61.154,1.722,-61.256,1.471,-61.334,1.114,-61.46,0.815,-61.634,0.575,-61.755,0.25,-61.825,-0.161,-61.847,-0.419,-61.812,-0.576,-61.868,-0.792,-61.878,-0.956,-61.861,-1.141,-61.863,-1.27,-61.881,-1.389]]},{"n":"Bratul Chillia","z":2.1,"b":[28.761,45.18,29.706,45.42],"l":[[29.706,45.26,29.652,45.314,29.568,45.371,29.404,45.42,29.224,45.403,29.027,45.321,28.894,45.29,28.824,45.311,28.782,45.31,28.767,45.286,28.77,45.267,28.791,45.252,28.788,45.241,28.761,45.234,28.781,45.218,28.883,45.18]]},{"n":"Bratul Sfintu Gheorghe","z":2.1,"b":[28.883,44.915,29.605,45.18],"l":[[28.883,45.18,29.075,45.1,29.147,45.084,29.175,45.089,29.205,45.08,29.276,45.007,29.348,44.97,29.42,44.97,29.495,44.95,29.605,44.915]]},{"n":"Bratul Sulina","z":2.1,"b":[28.883,45.152,29.679,45.18],"l":[[28.883,45.18,29.679,45.152]]},{"n":"Brazos","z":5,"b":[-100.501,28.898,-95.378,33.584],"l":[[-100.501,32.905,-100.441,32.957,-100.319,32.994,-100.115,33.034,-99.998,33.091,-99.968,33.165,-99.964,33.248,-99.987,33.34,-99.891,33.435,-99.679,33.532,-99.494,33.582,-99.337,33.584,-99.247,33.556,-99.225,33.497,-99.184,33.468,-99.126,33.466,-99.081,33.438,-99.05,33.383,-99.022,33.361,-98.996,33.373,-98.973,33.355,-98.953,33.307,-98.882,33.246,-98.7,33.136,-98.712,33.082,-98.693,33.039,-98.647,32.99,-98.608,32.987,-98.577,33.032,-98.519,33.033,-98.433,32.992,-98.406,32.952,-98.437,32.915,-98.435,32.879,-98.399,32.846,-98.34,32.837,-98.258,32.853,-98.201,32.836,-98.169,32.784,-98.176,32.752,-98.194,32.739,-98.186,32.719,-98.156,32.704,-97.973,32.649,-97.85,32.574,-97.788,32.479,-97.716,32.416,-97.636,32.384,-97.623,32.358,-97.676,32.337,-97.696,32.303,-97.682,32.258,-97.632,32.213,-97.546,32.168,-97.498,32.157,-97.474,32.168,-97.464,32.163,-97.462,32.152,-97.466,32.09,-97.417,32.003,-97.315,31.892,-97.263,31.809,-97.261,31.752,-97.193,31.663,-97.06,31.541,-96.971,31.421,-96.904,31.244,-96.654,30.791,-96.512,30.61,-96.394,30.551,-96.319,30.485,-96.288,30.412,-96.242,30.376,-96.181,30.38,-96.131,30.362,-96.093,30.325,-96.099,30.258,-96.149,30.164,-96.153,30.092,-96.11,30.042,-96.078,29.931,-96.058,29.758,-96.03,29.666,-95.979,29.647,-95.905,29.632,-95.758,29.601,-95.656,29.559,-95.599,29.505,-95.572,29.418,-95.573,29.298,-95.589,29.206,-95.619,29.141,-95.57,29.074,-95.44,29.004,-95.378,28.947,-95.388,28.898]]},{"n":"Busira","z":5,"b":[18.951,-0.294,20.754,-0.105],"l":[[20.754,-0.282,20.612,-0.294,20.503,-0.28,20.38,-0.242,20.297,-0.234,20.254,-0.254,20.221,-0.227,20.196,-0.154,20.156,-0.13,20.101,-0.155,20.028,-0.147,19.937,-0.105,19.864,-0.105,19.809,-0.148,19.713,-0.15,19.577,-0.112,19.411,-0.114,19.214,-0.155,19.099,-0.199,19.064,-0.247,19.023,-0.258,18.951,-0.219]]},{"n":"Bykovskaya Protoka","z":2,"b":[126.657,72.294,127.726,72.524],"l":[[126.69,72.294,126.657,72.423,126.674,72.485,126.729,72.524,126.984,72.515,127.666,72.43,127.726,72.413]]},{"n":"Bénoué","z":4,"b":[11.924,7.777,13.999,9.506],"l":[[13.524,7.777,13.674,7.855,13.717,7.922,13.705,8.006,13.736,8.071,13.85,8.138,13.96,8.24,13.998,8.312,13.999,8.388,13.977,8.453,13.93,8.506,13.927,8.597,13.966,8.726,13.925,8.833,13.801,8.917,13.681,9.054,13.565,9.244,13.475,9.314,13.412,9.264,13.319,9.226,13.197,9.199,13.084,9.223,12.927,9.335,12.821,9.375,12.717,9.351,12.581,9.277,12.456,9.307,12.342,9.443,12.195,9.506,11.924,9.491]]},{"n":"Bío-Bío","z":4.7,"b":[-73.133,-38.77,-71.127,-36.795],"l":[[-71.241,-38.77,-71.127,-38.691,-71.127,-38.602,-71.241,-38.413,-71.304,-38.353,-71.329,-38.28,-71.336,-38.172,-71.401,-38.081,-71.526,-38.007,-71.62,-37.92,-71.684,-37.82,-71.754,-37.754,-71.83,-37.721,-71.918,-37.714,-72.02,-37.732,-72.09,-37.719,-72.148,-37.652,-72.256,-37.593,-72.355,-37.568,-72.482,-37.557,-72.576,-37.531,-72.636,-37.489,-72.691,-37.406,-72.741,-37.281,-72.807,-37.205,-72.89,-37.178,-72.941,-37.115,-72.958,-37.016,-72.999,-36.927,-73.062,-36.846,-73.112,-36.803,-73.133,-36.795]]},{"n":"Caquetá","z":4.7,"b":[-76.536,-1.53,-69.462,1.33],"l":[[-76.533,1.33,-76.536,1.139,-76.508,1.057,-76.422,1.002,-76.263,0.98,-76.145,0.988,-76.015,1.019,-75.92,0.996,-75.859,0.918,-75.69,0.844,-75.413,0.772,-75.252,0.676,-75.209,0.556,-75.139,0.49,-75.044,0.476,-74.988,0.42,-74.971,0.321,-74.893,0.241,-74.753,0.181,-74.678,0.1,-74.669,-0.003,-74.638,-0.07,-74.585,-0.101,-74.527,-0.108,-74.462,-0.091,-74.404,-0.093,-74.353,-0.113,-74.306,-0.149,-74.262,-0.202,-74.166,-0.263,-74.016,-0.334,-73.885,-0.374,-73.771,-0.383,-73.693,-0.406,-73.629,-0.463,-73.517,-0.515,-73.438,-0.528,-73.356,-0.518,-73.288,-0.536,-73.233,-0.582,-73.154,-0.585,-73.05,-0.546,-72.969,-0.545,-72.913,-0.583,-72.851,-0.591,-72.784,-0.57,-72.704,-0.591,-72.611,-0.654,-72.529,-0.653,-72.458,-0.589,-72.398,-0.575,-72.349,-0.611,-72.286,-0.607,-72.209,-0.563,-72.139,-0.593,-72.076,-0.697,-71.99,-0.765,-71.883,-0.798,-71.803,-0.847,-71.75,-0.914,-71.677,-0.947,-71.584,-0.945,-71.543,-0.978,-71.557,-1.044,-71.55,-1.087,-71.523,-1.107,-71.477,-1.111,-71.379,-1.093,-71.227,-1.061,-71.13,-1.095,-71.036,-1.184,-70.945,-1.24,-70.858,-1.263,-70.778,-1.305,-70.706,-1.367,-70.623,-1.41,-70.529,-1.435,-70.44,-1.438,-70.356,-1.419,-70.256,-1.441,-70.141,-1.503,-70.067,-1.53,-70.033,-1.52,-69.975,-1.435,-69.893,-1.275,-69.801,-1.215,-69.7,-1.255,-69.6,-1.256,-69.524,-1.226,-69.462,-1.231]]},{"n":"Yangtze","z":2,"b":[103.851,28.621,113.128,31.061],"l":[[103.851,28.668,103.942,28.627,104.094,28.621,104.335,28.636,104.492,28.679,104.564,28.749,104.62,28.779,104.65,28.795,104.753,28.817,104.833,28.816,104.893,28.79,104.939,28.793,104.985,28.79,105.012,28.778,105.043,28.746,105.091,28.724,105.128,28.732,105.153,28.769,105.209,28.786,105.296,28.785,105.351,28.805,105.373,28.848,105.424,28.877,105.503,28.892,105.547,28.915,105.577,28.922,105.619,28.906,105.673,28.898,105.741,28.918,105.785,28.906,105.803,28.862,105.829,28.839,105.861,28.837,105.882,28.852,105.89,28.884,105.876,28.915,105.838,28.943,105.834,28.98,105.865,29.025,105.946,29.058,106.076,29.08,106.146,29.125,106.157,29.195,106.253,29.274,106.434,29.363,106.518,29.426,106.505,29.462,106.523,29.516,106.572,29.589,106.618,29.612,106.66,29.586,106.721,29.578,106.802,29.588,106.894,29.65,106.998,29.763,107.084,29.787,107.15,29.721,107.237,29.699,107.345,29.718,107.419,29.771,107.458,29.856,107.53,29.89,107.634,29.873,107.842,30.043,108.308,30.58,108.391,30.692,108.418,30.755,108.417,30.808,108.443,30.853,108.496,30.892,108.757,30.936,109.46,31.01,109.545,31.034,109.598,31.031,109.646,31.008,109.742,31.016,109.884,31.054,109.984,31.061,110.043,31.037,110.143,31.032,110.285,31.045,110.474,31.02,110.828,30.922,110.971,30.852,111.045,30.835,111.098,30.847,111.159,30.829,111.228,30.781,111.309,30.673,111.399,30.507,111.465,30.418,111.507,30.407,111.52,30.384,111.507,30.351,111.512,30.318,111.538,30.287,111.571,30.287,111.612,30.311,111.736,30.389,111.818,30.426,111.86,30.421,111.911,30.387,111.969,30.323,112.058,30.293,112.179,30.295,112.247,30.278,112.263,30.242,112.252,30.191,112.215,30.125,112.252,30.068,112.363,30.018,112.406,29.927,112.381,29.793,112.41,29.738,112.492,29.762,112.544,29.792,112.569,29.784,112.593,29.766,112.604,29.752,112.629,29.752,112.632,29.765,112.657,29.788,112.707,29.799,112.739,29.786,112.732,29.772,112.745,29.754,112.782,29.744,112.819,29.737,112.885,29.76,112.942,29.74,112.955,29.691,112.94,29.669,112.917,29.6,112.924,29.53,112.951,29.508,112.989,29.505,113.017,29.494,113.031,29.481,113.053,29.469,113.097,29.456,113.128,29.463]]},{"n":"Chari","z":5,"b":[14.176,8.816,18.886,12.98],"l":[[18.886,8.836,18.842,8.816,18.76,8.844,18.625,8.914,18.476,9.041,18.231,9.319,17.791,9.722,17.623,9.913,17.582,10.026,17.521,10.129,17.372,10.296,17.316,10.35,17.248,10.388,17.17,10.41,17.104,10.456,17.051,10.525,17.004,10.564,16.963,10.573,16.876,10.548,16.742,10.489,16.611,10.481,16.483,10.522,16.34,10.598,16.18,10.708,16.017,10.773,15.849,10.793,15.711,10.834,15.603,10.895,15.521,10.957,15.466,11.019,15.433,11.105,15.423,11.214,15.386,11.344,15.322,11.496,15.301,11.638,15.322,11.773,15.283,11.895,15.184,12.007,15.075,12.085],[15.075,12.085,14.95,12.043,14.843,12.073,14.692,12.144,14.584,12.219,14.519,12.298,14.415,12.344,14.273,12.357,14.197,12.384,14.176,12.471],[15.075,12.085,14.957,12.13,14.881,12.269,14.847,12.502,14.761,12.656,14.623,12.73,14.545,12.82,14.516,12.98]]},{"n":"Chattahoochee","z":5,"b":[-85.185,29.745,-83.636,34.716],"l":[[-84.886,30.721,-84.893,30.668,-84.961,30.574,-85.08,30.272,-85.117,30.17,-85.113,30.08,-85.043,29.995,-85.016,29.909,-85.033,29.822,-85.024,29.772,-84.969,29.745],[-85.067,31.635,-85.055,31.573,-85.093,31.296,-85.067,31.118,-84.966,30.923],[-84.076,34.167,-84.105,34.091,-84.153,34.042,-84.23,33.995,-84.292,33.98,-84.339,33.998,-84.43,33.926,-84.566,33.765,-84.729,33.622,-84.921,33.496,-85.037,33.396,-85.077,33.323,-85.089,33.252,-85.072,33.184,-85.096,33.107,-85.161,33.023,-85.185,32.937,-85.157,32.804,-85.015,32.526,-84.974,32.409,-84.986,32.361,-84.969,32.32,-84.922,32.286,-84.93,32.247,-84.993,32.202,-85.035,32.147,-85.065,32.051],[-83.746,34.716,-83.691,34.7,-83.661,34.659,-83.636,34.587,-83.672,34.504,-83.817,34.364]]},{"n":"Chattooga","z":5,"b":[-83.356,34.708,-83.121,35.076],"l":[[-83.121,35.076,-83.121,35.001,-83.17,34.933,-83.317,34.806,-83.356,34.708]]},{"n":"Chenab","z":5,"b":[70.612,29.023,77.436,33.371],"l":[[72.139,31.183,72.139,31.116,72.103,31.048,71.995,30.909,71.771,30.614,71.679,30.583,71.577,30.508,71.435,30.38,71.316,30.206,71.221,29.986,71.173,29.829,71.172,29.736,71.131,29.649,71.049,29.571,71.017,29.505,71.036,29.452,71.035,29.392,71.014,29.326,70.987,29.275,70.955,29.238,70.912,29.221,70.857,29.223,70.797,29.178,70.732,29.084,70.684,29.037,70.612,29.023],[77.436,32.293,77.134,32.457,76.94,32.593,76.767,32.671,76.552,32.736,76.437,32.842,76.423,32.989,76.338,33.122,76.182,33.243,76.02,33.323,75.854,33.363,75.759,33.371,75.737,33.347,75.746,33.305,75.788,33.246,75.802,33.202,75.788,33.171,75.677,33.152,75.468,33.144,75.308,33.174,75.198,33.241,75.122,33.246,75.08,33.191,74.999,33.17,74.879,33.186,74.809,33.123,74.789,32.982,74.583,32.789,74.188,32.542,73.844,32.371,73.548,32.275,73.335,32.146,73.139,31.902,72.974,31.758,72.854,31.699,72.722,31.677,72.582,31.622,72.435,31.533,72.334,31.457,72.279,31.395,72.247,31.328,72.238,31.256,72.21,31.211,72.139,31.183]]},{"n":"Chixoy","z":4.7,"b":[-91.303,15.017,-90.417,16.391],"l":[[-91.278,15.017,-91.264,15.157,-91.303,15.237,-91.296,15.27,-91.257,15.285,-91.047,15.308,-90.881,15.288,-90.677,15.236,-90.555,15.225,-90.492,15.286,-90.483,15.316,-90.534,15.347,-90.647,15.378,-90.676,15.427,-90.624,15.493,-90.611,15.558,-90.64,15.622,-90.686,15.66,-90.749,15.673,-90.79,15.697,-90.809,15.733,-90.789,15.817,-90.732,15.951,-90.637,16.019,-90.506,16.023,-90.447,16.071,-90.46,16.162,-90.45,16.261,-90.417,16.351,-90.417,16.391]]},{"n":"Chubut","z":5,"b":[-71.22,-43.846,-65.047,-42.617],"l":[[-71.22,-43.582,-71.092,-43.608,-71.013,-43.562,-70.906,-43.396],[-70.899,-43.393,-70.811,-43.096,-70.744,-42.982,-70.669,-42.953,-70.626,-42.904,-70.615,-42.835,-70.579,-42.766,-70.518,-42.698,-70.461,-42.651,-70.408,-42.625,-70.244,-42.617,-69.97,-42.628,-69.784,-42.643,-69.686,-42.664,-69.508,-42.821,-69.252,-43.114,-69.134,-43.305,-69.155,-43.391,-69.153,-43.447,-69.117,-43.486,-68.976,-43.582,-68.901,-43.668,-68.844,-43.774,-68.765,-43.818,-68.665,-43.798,-68.547,-43.806,-68.41,-43.842,-68.102,-43.824,-67.358,-43.714,-67.308,-43.714,-67.264,-43.742,-67.225,-43.797,-67.16,-43.832,-67.068,-43.846,-66.879,-43.838,-66.523,-43.8,-66.493,-43.795,-66.458,-43.768,-66.457,-43.745,-66.465,-43.706,-66.444,-43.677,-66.356,-43.619,-66.277,-43.577,-66.107,-43.521,-65.982,-43.459,-65.902,-43.392,-65.793,-43.353,-65.656,-43.344,-65.551,-43.322,-65.479,-43.288,-65.344,-43.286,-65.047,-43.333]]},{"n":"Chulyshman","z":5,"b":[87.735,50.505,89.656,51.364],"l":[[89.656,50.505,89.403,50.598,89.194,50.622,88.946,50.608,88.76,50.656,88.636,50.764,88.512,50.831,88.387,50.857,88.22,50.929,88.01,51.048,87.861,51.151,87.772,51.238,87.735,51.302,87.756,51.364]]},{"n":"Churchill","z":5,"b":[-64.682,52.886,-60.337,53.546],"l":[[-64.622,52.891,-64.682,53.125],[-64.678,53.382,-64.577,53.49,-64.449,53.532,-64.26,53.546,-64.067,53.538,-63.867,53.508,-63.638,53.425,-63.378,53.291,-63.034,53.21,-62.606,53.181,-62.352,53.145,-62.272,53.103,-62.214,53.042,-62.179,52.963,-62.036,52.911,-61.785,52.886,-61.65,52.906,-61.631,52.968,-61.578,52.996,-61.448,52.986,-60.981,53.198,-60.724,53.268,-60.558,53.25,-60.337,53.277]]},{"n":"Coco","z":5,"b":[-86.844,13.42,-83.158,15.008],"l":[[-86.844,13.42,-86.639,13.513,-86.492,13.61,-86.411,13.636,-86.346,13.622,-86.282,13.572,-86.217,13.487,-86.148,13.461,-86.075,13.493,-86.004,13.505,-85.935,13.496,-85.856,13.581,-85.769,13.759,-85.728,13.876,-85.731,13.932,-85.682,13.983,-85.58,14.028,-85.477,14.109,-85.374,14.224,-85.284,14.292,-85.208,14.312,-85.179,14.343,-85.198,14.386,-85.192,14.447,-85.161,14.525,-85.117,14.571,-85.059,14.583,-85.037,14.608,-85.049,14.645,-85.037,14.686,-84.985,14.752,-84.86,14.81,-84.789,14.79,-84.73,14.713,-84.646,14.661,-84.538,14.633,-84.454,14.644,-84.394,14.692,-84.34,14.706,-84.292,14.687,-84.267,14.698,-84.264,14.739,-84.239,14.748,-84.192,14.726,-84.151,14.72,-84.114,14.731,-84.1,14.751,-84.093,14.771,-84.066,14.786,-83.972,14.771,-83.867,14.794,-83.751,14.856,-83.674,14.884,-83.635,14.876,-83.59,14.908,-83.537,14.977,-83.415,15.008,-83.158,14.993]]},{"n":"Colorado","z":4.7,"b":[-115.199,-39.88,-62.254,40.404],"l":[[-64.97,-38.804,-64.596,-38.841,-64.39,-38.878,-64.228,-38.925,-63.989,-39.038,-63.674,-39.217,-63.456,-39.316,-63.336,-39.335,-63.214,-39.38,-63.091,-39.451,-62.918,-39.499,-62.697,-39.524,-62.517,-39.608,-62.309,-39.822,-62.279,-39.852,-62.254,-39.88],[-70.302,-35.288,-70.223,-35.591,-70.166,-35.722,-70.072,-35.813,-69.865,-35.86,-69.762,-35.964,-69.694,-36.14,-69.671,-36.26,-69.692,-36.324,-69.693,-36.407,-69.676,-36.508,-69.694,-36.596,-69.748,-36.67,-69.785,-36.78,-69.806,-36.925,-69.797,-37.02,-69.758,-37.064,-69.699,-37.106,-69.621,-37.146,-69.463,-37.174,-69.226,-37.19,-69.076,-37.24,-69.016,-37.323,-68.87,-37.383,-68.641,-37.422,-68.501,-37.465,-68.452,-37.514,-68.273,-37.558,-67.963,-37.598,-67.783,-37.659,-67.735,-37.74,-67.751,-37.82,-67.83,-37.901,-67.866,-37.969,-67.858,-38.022,-67.824,-38.056,-67.765,-38.069,-67.69,-38.122,-67.599,-38.215,-67.46,-38.252,-67.271,-38.232,-67.15,-38.269,-67.097,-38.362,-66.993,-38.436,-66.763,-38.516,-66.653,-38.564,-66.603,-38.61,-66.576,-38.671,-66.52,-38.71,-66.435,-38.726,-66.068,-38.74,-65.893,-38.761,-65.757,-38.799,-65.658,-38.807,-65.595,-38.786,-65.514,-38.791,-65.416,-38.823,-65.27,-38.83,-64.978,-38.804],[-114.721,32.724,-114.788,32.565,-114.839,32.504,-114.897,32.49,-114.959,32.42,-115.025,32.294,-115.093,32.213,-115.164,32.176,-115.199,32.124,-115.196,32.058,-115.187,32.044,-115.173,32.045,-115.153,32.036,-115.126,31.988,-115.093,31.965,-115.035,31.966,-115.009,31.946,-114.981,31.918,-114.934,31.901],[-114.164,34.253,-114.329,34.142,-114.418,34.051,-114.509,33.904,-114.495,33.785,-114.499,33.686,-114.522,33.608,-114.556,33.536,-114.603,33.47,-114.651,33.431,-114.702,33.418,-114.719,33.331,-114.704,33.17,-114.648,33.072,-114.551,33.037,-114.491,32.984,-114.469,32.912,-114.478,32.842,-114.516,32.773,-114.582,32.735,-114.721,32.724],[-114.591,35.353,-114.611,34.998,-114.611,34.907,-114.557,34.795,-114.398,34.59,-114.379,34.541],[-114.741,36.014,-114.732,35.984,-114.687,35.917,-114.684,35.814,-114.677,35.73,-114.65,35.684,-114.645,35.631],[-112.876,36.28,-113.206,36.124,-113.296,36.048,-113.325,35.995,-113.323,35.835,-113.356,35.764,-113.422,35.762,-113.579,35.801,-113.636,35.831],[-111.473,36.932,-111.591,36.85,-111.683,36.748,-111.788,36.599,-111.852,36.488,-111.874,36.414,-111.865,36.32,-111.826,36.206,-111.834,36.126,-111.891,36.081,-111.995,36.073,-112.147,36.102,-112.256,36.151,-112.322,36.221,-112.377,36.246,-112.42,36.226,-112.459,36.229,-112.495,36.254,-112.499,36.288,-112.471,36.332,-112.467,36.366,-112.489,36.389,-112.533,36.401,-112.598,36.402,-112.693,36.372,-112.881,36.28],[-105.861,40.165,-105.985,40.127,-106.126,40.093,-106.285,40.091,-106.401,40.062,-106.482,40.019,-106.595,39.923,-106.638,39.894,-106.676,39.89,-106.726,39.92,-106.793,39.924,-106.877,39.902,-106.969,39.828,-107.071,39.701,-107.132,39.644,-107.235,39.602,-107.541,39.579,-107.734,39.579,-107.847,39.557,-107.944,39.512,-108.106,39.41,-108.242,39.315,-108.314,39.178,-108.434,39.099,-108.558,39.09,-108.801,39.184,-108.867,39.201,-108.952,39.198,-109.051,39.141,-109.196,39.033,-109.265,38.935,-109.306,38.812,-109.363,38.734,-109.434,38.702,-109.499,38.651,-109.556,38.581,-109.596,38.554,-109.614,38.561,-109.635,38.546,-109.648,38.485,-109.67,38.453,-109.7,38.449,-109.723,38.414,-109.739,38.35,-109.785,38.279,-109.897,38.165,-110.131,37.962,-110.248,37.895,-110.368,37.898],[-105.846,40.404,-105.816,40.215,-105.821,40.185]]},{"n":"Columbia","z":3,"b":[-123.221,45.591,-115.865,52.102],"l":[[-122.15,45.594,-122.203,45.591,-122.561,45.625,-122.65,45.627,-122.726,45.674,-122.746,45.75,-122.843,45.978,-122.942,46.115,-123.042,46.16,-123.121,46.179,-123.221,46.154],[-118.168,48.232,-118.232,48.086,-118.284,48.035,-118.346,48.032,-118.368,47.996,-118.349,47.927,-118.351,47.879,-118.375,47.849,-118.433,47.857,-118.525,47.903,-118.665,47.927,-118.854,47.93,-118.983,47.982,-119.052,48.082,-119.228,48.101,-119.511,48.04,-119.681,48.03,-119.74,48.073,-119.801,48.085,-119.864,48.066,-119.887,48.029,-119.868,47.975,-119.87,47.929,-119.894,47.891,-119.969,47.813,-120.04,47.783,-120.14,47.763,-120.214,47.693,-120.262,47.575,-120.273,47.487,-120.248,47.429,-120.194,47.382,-120.068,47.329,-120.068,47.275,-120.049,47.245,-120.01,47.219,-119.997,47.176,-120.011,47.117,-119.992,46.985,-119.939,46.78,-119.838,46.674,-119.687,46.667,-119.577,46.681,-119.506,46.716,-119.417,46.679,-119.311,46.569,-119.256,46.456,-119.253,46.341,-119.201,46.269,-119.108,46.237,-119.085,46.233,-119.057,46.22,-119.036,46.206,-118.968,46.132,-118.948,46.11,-118.932,46.095,-118.925,46.08,-118.931,46.063,-118.959,46.038,-118.99,46.019,-119.05,45.981,-119.177,45.945,-119.429,45.934,-119.934,45.838,-120.156,45.762,-120.351,45.725,-120.716,45.69,-121.045,45.639,-121.184,45.644,-121.243,45.687,-121.402,45.712,-121.663,45.718,-121.882,45.689,-122.085,45.603,-122.15,45.594],[-117.907,49.342,-117.721,49.331,-117.676,49.275,-117.711,49.172,-117.701,49.098,-117.648,49.055,-117.682,49.015,-117.717,48.975,-117.908,48.858,-118.021,48.766,-118.074,48.663],[-117.88,50.103,-117.917,50.092,-117.925,50.064,-117.915,50.016,-117.95,49.971,-118.029,49.929,-118.074,49.881,-118.092,49.802],[-115.865,50.287,-116.081,50.591,-116.224,50.758,-116.365,50.89,-116.545,51.01,-116.762,51.12,-116.931,51.244,-117.052,51.383,-117.183,51.471,-117.324,51.507,-117.425,51.558,-117.517,51.659],[-117.873,51.87,-118.366,52.09],[-118.527,52.102,-118.581,52.052,-118.595,51.999,-118.589,51.925,-118.611,51.858,-118.661,51.796,-118.681,51.742,-118.672,51.696,-118.599,51.59,-118.463,51.426,-118.359,51.323,-118.289,51.281,-118.237,51.214,-118.205,51.12,-118.195,51.049,-118.208,51,-118.138,50.903,-117.906,50.683]]},{"n":"Congo","z":2.1,"b":[13.068,-8.272,27.049,2.178],"l":[[26.441,-8.272,26.654,-8.223,26.742,-8.193,26.776,-8.165,26.794,-8.124,26.794,-8.069,26.838,-8.011,26.928,-7.949,26.973,-7.9,26.973,-7.864,26.929,-7.804,26.839,-7.72,26.865,-7.529,27.009,-7.229,27.049,-6.991,26.984,-6.815,26.961,-6.664,26.98,-6.537,26.97,-6.422,26.931,-6.319,26.906,-6.173,26.895,-5.982,26.9,-5.818,26.93,-5.61],[24.292,0.782,24.178,0.924,24.005,1.022,23.734,1.123,23.461,1.342,23.184,1.679,22.968,1.873,22.735,1.95,22.557,2.101,22.396,2.161,22.194,2.178,21.969,2.178,21.721,2.158,21.499,2.113,21.304,2.041,20.944,1.995,20.418,1.972,19.902,1.864,19.143,1.574,18.714,1.223,18.526,1.052,18.436,0.944,18.343,0.692,18.199,0.1,18.184,0.063,18.109,-0.124,17.9,-0.391,17.753,-0.549,17.543,-0.775,17.279,-1,17.108,-1.064,16.975,-1.14,16.88,-1.226,16.849,-1.272,16.78,-1.376,16.622,-1.699,16.541,-1.84,16.434,-1.961,16.274,-2.108,16.215,-2.178,16.192,-2.279,16.202,-2.465,16.217,-3.03,16.191,-3.194,16.147,-3.464,15.99,-3.766,15.872,-3.934,15.755,-3.986,15.6,-4.031,15.526,-4.088,15.481,-4.172,15.395,-4.245,15.267,-4.308,15.106,-4.461,14.912,-4.706,14.779,-4.846,14.708,-4.882,14.634,-4.885,14.558,-4.856,14.494,-4.852,14.418,-4.883,14.234,-4.912,14.133,-4.945,14.054,-4.991,14.005,-5.052,13.985,-5.129,13.938,-5.205,13.863,-5.278,13.832,-5.301,13.809,-5.303,13.777,-5.279,13.752,-5.279,13.735,-5.303,13.692,-5.31,13.623,-5.3,13.574,-5.316,13.543,-5.359,13.55,-5.414,13.595,-5.482,13.591,-5.513,13.565,-5.531,13.553,-5.576,13.564,-5.655,13.512,-5.743,13.399,-5.841,13.303,-5.882,13.184,-5.856,13.068,-5.865]]},{"n":"Copper","z":5,"b":[-145.419,60.669,-143.658,62.699],"l":[[-143.658,62.474,-143.675,62.557,-143.713,62.605,-143.776,62.646,-143.907,62.674,-144.203,62.699,-144.458,62.663,-144.733,62.568,-145.114,62.403,-145.336,62.286,-145.399,62.219,-145.419,62.157,-145.384,62.073,-145.166,61.916,-144.936,61.81,-144.621,61.701,-144.455,61.617,-144.427,61.526,-144.486,61.453,-144.58,61.407,-144.729,61.366,-144.828,61.309,-144.877,61.238,-144.873,61.151,-144.815,61.049,-144.716,60.951,-144.574,60.859,-144.534,60.833,-144.523,60.815,-144.532,60.788,-144.616,60.714,-144.638,60.69,-144.691,60.669]]},{"n":"Coppermine","z":5,"b":[-116.326,64.552,-111.212,67.819],"l":[[-111.212,64.583,-111.351,64.552,-111.456,64.562,-111.572,64.604,-111.715,64.628,-111.966,64.638],[-112.318,64.96,-112.343,64.899],[-114.173,65.638,-114.168,65.684,-114.231,65.728,-114.36,65.787,-114.43,65.864,-114.439,65.958,-114.403,66.035,-114.321,66.096,-114.374,66.233,-114.56,66.444,-114.751,66.586,-114.948,66.658,-115.265,66.728,-115.921,66.831,-116.157,66.81,-116.266,66.832,-116.326,66.89,-116.293,66.972,-116.166,67.08,-116.134,67.156,-116.196,67.201,-116.074,67.224,-115.768,67.226,-115.6,67.236,-115.571,67.253,-115.595,67.276,-115.67,67.303,-115.713,67.341,-115.724,67.389,-115.694,67.44,-115.625,67.495,-115.593,67.536,-115.598,67.563,-115.553,67.615,-115.458,67.691,-115.335,67.747,-115.133,67.819]]},{"n":"Cubango","z":5,"b":[16.145,-18.06,21.503,-12.634],"l":[[16.145,-12.654,16.192,-12.634,16.234,-12.66,16.288,-12.725,16.328,-12.821,16.365,-13.01,16.431,-13.436,16.414,-13.711,16.335,-13.977,16.288,-14.196,16.272,-14.368,16.294,-14.493,16.354,-14.573,16.446,-14.641,16.568,-14.699,16.646,-14.821,16.679,-15.005,16.75,-15.166,16.859,-15.305,16.944,-15.395,17.005,-15.436,17.12,-15.474,17.288,-15.508,17.379,-15.555,17.393,-15.615,17.462,-15.714,17.586,-15.853,17.664,-16.019,17.696,-16.212,17.767,-16.418,17.876,-16.637,18.095,-16.953,18.588,-17.57,18.718,-17.703,18.826,-17.766,18.955,-17.804,19.076,-17.818,19.189,-17.808,19.377,-17.825,19.639,-17.869,19.912,-17.881,20.194,-17.864,20.393,-17.887,20.508,-17.953,20.625,-17.997,20.746,-18.02,20.908,-18.006,21.113,-17.956,21.288,-17.963,21.503,-18.06]]},{"n":"Culuene","z":4.7,"b":[-54.609,-14.984,-52.877,-11.444],"l":[[-54.609,-14.984,-54.357,-14.897,-54.251,-14.837,-54.207,-14.775,-54.136,-14.758,-54.04,-14.786,-53.967,-14.79,-53.894,-14.761,-53.659,-14.592,-53.512,-14.346,-53.374,-13.968,-53.269,-13.736,-53.197,-13.651,-53.16,-13.583,-53.159,-13.532,-53.08,-13.344,-52.922,-13.019,-52.877,-12.807,-52.945,-12.71,-52.97,-12.644,-52.953,-12.611,-53.004,-12.517,-53.125,-12.361,-53.213,-12.268,-53.27,-12.235,-53.329,-12.161,-53.391,-12.047,-53.459,-11.966,-53.532,-11.918,-53.579,-11.846,-53.599,-11.75,-53.567,-11.637,-53.444,-11.444]]},{"n":"Dadu","z":5,"b":[100.07,29.236,103.719,33.006],"l":[[100.07,33.006,100.165,32.896,100.24,32.839,100.371,32.779,100.509,32.627,100.616,32.546,100.739,32.486,100.801,32.427,100.802,32.37,100.842,32.326,100.921,32.295,100.97,32.212,100.999,32.009,101.214,31.837,101.422,31.8,101.694,31.841,101.879,31.823,101.977,31.748,102.039,31.646,102.064,31.518,102.055,31.432,102.011,31.389,102.004,31.35,102.034,31.316,102.01,31.258,101.932,31.176,101.887,31.105,101.876,30.985,101.882,30.925,101.928,30.846,102.013,30.748,102.058,30.655,102.065,30.568,102.102,30.446,102.169,30.291,102.195,30.175,102.179,30.097,102.185,30.032,102.214,29.979,102.213,29.84,102.182,29.614,102.206,29.439,102.285,29.316,102.397,29.278,102.616,29.35,102.76,29.313,102.818,29.279,102.839,29.237,102.877,29.237,102.931,29.281,102.979,29.288,103.022,29.258,103.061,29.258,103.096,29.288,103.151,29.289,103.225,29.26,103.278,29.261,103.309,29.293,103.359,29.311,103.427,29.316,103.506,29.292,103.594,29.237,103.644,29.236,103.655,29.289,103.639,29.321,103.596,29.333,103.569,29.36,103.559,29.4,103.595,29.455,103.642,29.495,103.677,29.525,103.719,29.537]]},{"n":"Daly","z":5,"b":[130.26,-14.706,131.844,-13.302],"l":[[131.844,-14.706,131.75,-14.669,131.706,-14.601,131.681,-14.491,131.566,-14.345,131.257,-14.073,131.2,-13.981,131.186,-13.927,131.195,-13.879,131.174,-13.849,131.123,-13.836,131.037,-13.855,130.916,-13.904,130.827,-13.914,130.771,-13.883,130.721,-13.819,130.678,-13.721,130.637,-13.671,130.598,-13.669,130.546,-13.618,130.482,-13.516,130.409,-13.456,130.327,-13.438,130.305,-13.401,130.324,-13.353,130.325,-13.33,130.293,-13.309,130.26,-13.302]]},{"n":"Dalälven","z":5,"b":[12.095,60.122,17.457,62.157],"l":[[16.433,60.156,16.499,60.163],[17.417,60.51,17.457,60.642],[14.973,60.728,15.047,60.711,15.057,60.681,15.029,60.632,15.045,60.591,15.107,60.557,15.206,60.54,15.343,60.541,15.478,60.505,15.611,60.432,15.754,60.401,15.904,60.411,16.006,60.39,16.058,60.336,16.073,60.28,16.052,60.22,16.112,60.173,16.323,60.122],[13.718,61.413,13.724,61.334,13.74,61.3,13.768,61.284,13.949,61.268,14.034,61.237,14.094,61.183,14.231,61.116,14.552,60.997],[12.095,62.15,12.212,62.157,12.291,62.12,12.41,61.999,13.212,61.663]]},{"n":"Damietta Branch","z":2,"b":[31.078,30.151,31.839,31.526],"l":[[31.163,30.151,31.078,30.325,31.107,30.419,31.21,30.508,31.25,30.623,31.226,30.766,31.224,30.869,31.245,30.932,31.31,31.019,31.42,31.128,31.509,31.186,31.579,31.192,31.671,31.278,31.839,31.526]]},{"n":"Danube","z":2.1,"b":[17.206,43.671,28.761,48.061],"l":[[17.206,48.061,17.481,47.888,17.635,47.81,17.762,47.77,18.146,47.763,18.754,47.788,18.795,47.809,19,47.819,19.071,47.807,19.103,47.779,19.123,47.731,19.124,47.709,19.073,47.552,19.06,47.481,19.03,47.413,18.959,47.365,18.907,47.26,18.884,47.151,18.928,47.083,18.935,47.033,18.94,47,18.936,46.885,18.945,46.804,18.983,46.74,18.966,46.685,18.894,46.638,18.874,46.586,18.905,46.528,18.919,46.423,18.916,46.271,18.859,46.151,18.748,46.063,18.727,46.014,18.759,45.935,18.839,45.836,18.894,45.767,18.947,45.656,18.918,45.601,18.954,45.558,19.055,45.527,19.064,45.515,19.033,45.502,19.008,45.466,19.005,45.4,19.093,45.337,19.273,45.278,19.475,45.243,19.699,45.233,19.828,45.241,19.863,45.269,19.907,45.259,19.959,45.213,20.045,45.186,20.163,45.179,20.253,45.15,20.282,45.126,20.315,45.099,20.339,45.038,20.326,44.969,20.353,44.911,20.421,44.864,20.5,44.843,20.591,44.847,20.636,44.828,20.634,44.786,20.666,44.741,20.731,44.693,20.913,44.708,21.36,44.827,21.523,44.79,21.597,44.755,21.636,44.71,21.74,44.681,21.909,44.666,22.027,44.62,22.093,44.542,22.201,44.561,22.351,44.676,22.498,44.706,22.642,44.651,22.721,44.606,22.734,44.57,22.701,44.556,22.62,44.562,22.554,44.54,22.502,44.49,22.495,44.435,22.531,44.378,22.582,44.338,22.648,44.316,22.683,44.286,22.688,44.248,22.775,44.195,22.945,44.127,23.028,44.078,23.024,44.047,22.985,44.017,22.911,43.987,22.868,43.948,22.856,43.899,22.868,43.865,22.919,43.834,23.225,43.874,23.535,43.854,23.951,43.787,24.227,43.763,24.431,43.794,25.16,43.686,25.497,43.671,25.686,43.712,25.819,43.767,25.933,43.871,26.216,44.007,26.489,44.084,26.848,44.146,27.087,44.167,27.222,44.143,27.267,44.138,27.338,44.133,27.412,44.138,27.594,44.179,27.86,44.263,28.022,44.343,28.08,44.419,28.083,44.476,28.032,44.513,28.013,44.558,28.025,44.611,27.992,44.664,27.907,44.719,27.877,44.744,27.85,44.768,27.83,44.796,27.831,44.83,27.863,44.865,27.895,44.968,27.926,45.139,27.965,45.269,28.011,45.358,28.079,45.414,28.212,45.45,28.318,45.347,28.451,45.292,28.761,45.234]]},{"n":"Darling","z":3,"b":[141.918,-34.117,147.415,-29.928],"l":[[147.415,-30.125,147.262,-30.122,147.201,-30.107,147.182,-30.078,147.048,-30.027,146.799,-29.953,146.656,-29.928,146.616,-29.95,146.46,-29.962,146.188,-29.963,146.043,-29.983,146.026,-30.023,145.909,-30.103,145.692,-30.224,145.565,-30.316,145.528,-30.378,145.467,-30.409,145.338,-30.409,145.272,-30.464,145.219,-30.481,145.157,-30.479,145.11,-30.51,145.075,-30.575,145.008,-30.626,144.906,-30.662,144.813,-30.733,144.728,-30.838,144.622,-30.9,144.494,-30.921,144.411,-30.965,144.372,-31.033,144.27,-31.102,144.107,-31.173,144.022,-31.25,144.017,-31.333,143.904,-31.403,143.683,-31.458,143.548,-31.514,143.5,-31.57,143.454,-31.59,143.411,-31.575,143.373,-31.609,143.341,-31.692,143.298,-31.737,143.245,-31.745,143.197,-31.779,143.156,-31.839,143.104,-31.881,143.04,-31.903,143.005,-31.956,142.999,-32.039,142.978,-32.09,142.926,-32.116,142.538,-32.333,142.404,-32.505,142.388,-32.804,142.397,-32.951,142.375,-33.048,142.326,-33.143,142.368,-33.239,142.501,-33.335,142.57,-33.4,142.575,-33.435,142.535,-33.512,142.449,-33.632,142.312,-33.723,142.124,-33.787,142.002,-33.867,141.944,-33.964,141.918,-34.038,141.926,-34.117]]},{"n":"Zapadnaya Dvina","z":4.7,"b":[24.054,55.069,32.318,57.066],"l":[[32.078,56.908,32.062,56.877,32.097,56.861,32.177,56.85,32.242,56.821,32.318,56.75,32.273,56.609,32.233,56.552,32.184,56.531,32.135,56.478,32.087,56.395,32.073,56.315,32.092,56.24,32.068,56.194,32.001,56.177,31.974,56.144,31.986,56.095,31.875,56.069,31.641,56.066,31.479,56.039,31.39,55.989,31.379,55.889,31.449,55.738,31.415,55.655,31.278,55.639,31.171,55.601,31.056,55.509,30.643,55.436,30.409,55.369,30.214,55.285,30.167,55.235,30.148,55.195,30.087,55.183,29.925,55.204,29.815,55.205,29.757,55.187,29.726,55.161,29.722,55.129,29.687,55.116,29.62,55.122,29.555,55.107,29.491,55.069,29.452,55.071,29.438,55.112,29.406,55.132,29.355,55.133,29.316,55.155,29.288,55.2,29.202,55.258,29.057,55.329,29.001,55.38,28.993,55.409,28.919,55.455,28.724,55.524,28.504,55.566,28.26,55.582,28.087,55.62,27.986,55.681,27.928,55.74,27.914,55.796,27.881,55.817,27.855,55.819,27.764,55.808,27.604,55.798,27.459,55.803,27.349,55.831,27.015,55.899,26.809,55.907,26.621,55.88,26.489,55.899,26.414,55.965,26.364,56.039,26.34,56.121,26.294,56.178,26.227,56.21,26.184,56.264,26.166,56.342,26.064,56.418,25.879,56.493,25.779,56.553,25.762,56.598,25.71,56.609,25.623,56.587,25.534,56.59,25.444,56.617,25.358,56.619,25.276,56.597,25.174,56.594,25.053,56.612,24.891,56.668,24.689,56.764,24.505,56.827,24.341,56.858,24.22,56.894,24.144,56.936,24.091,56.985,24.054,57.066]]},{"n":"Desaguadero","z":5,"b":[-67.362,-38.804,-64.97,-32.397],"l":[[-67.362,-32.397,-67.235,-32.67,-67.201,-32.8,-67.218,-32.877,-67.208,-33.044,-67.171,-33.301,-67.051,-33.588,-66.848,-33.905,-66.75,-34.094,-66.756,-34.154,-66.775,-34.196,-66.806,-34.218,-66.819,-34.268,-66.815,-34.346,-66.794,-34.442,-66.757,-34.558,-66.697,-34.675,-66.614,-34.794,-66.555,-34.921,-66.522,-35.056,-66.534,-35.346,-66.592,-35.79,-66.679,-36.078,-66.793,-36.209,-66.833,-36.395,-66.797,-36.635,-66.795,-36.8,-66.828,-36.889,-66.828,-36.99,-66.794,-37.103,-66.796,-37.173,-66.832,-37.201,-66.784,-37.251,-66.651,-37.325,-66.542,-37.407,-66.458,-37.498,-66.371,-37.544,-66.279,-37.547,-66.211,-37.606,-66.168,-37.722,-66.067,-37.799,-65.91,-37.838,-65.822,-37.915,-65.802,-38.029,-65.818,-38.097,-65.87,-38.117,-65.912,-38.161,-65.946,-38.228,-65.912,-38.282,-65.81,-38.324,-65.743,-38.37,-65.712,-38.422,-65.63,-38.489,-65.494,-38.571,-65.313,-38.66,-64.97,-38.804]]},{"n":"Dicle","z":4,"b":[39.643,37.062,42.377,38.416],"l":[[39.643,38.416,40.018,38.348,40.024,38.306,40.068,38.269,40.15,38.223,40.178,38.179,40.153,38.136,40.171,38.106,40.235,38.091,40.262,38.023,40.252,37.903,40.427,37.832,40.969,37.8,41.155,37.757,41.358,37.744,41.64,37.746,41.8,37.709,41.84,37.632,41.854,37.556,41.841,37.481,41.882,37.441,41.979,37.435,42.094,37.395,42.226,37.322,42.313,37.23,42.377,37.062]]},{"n":"Digul","z":5,"b":[139.177,-7.252,140.742,-4.856],"l":[[140.683,-4.856,140.731,-4.915,140.742,-4.967,140.734,-5.03,140.697,-5.109,140.597,-5.253,140.319,-5.536,140.223,-5.673,140.217,-5.758,140.239,-5.828,140.289,-5.883,140.309,-5.961,140.295,-6.116,140.264,-6.153,140.266,-6.216,140.289,-6.319,140.314,-6.375,140.338,-6.382,140.346,-6.427,140.337,-6.51,140.349,-6.573,140.382,-6.616,140.372,-6.675,140.321,-6.75,140.265,-6.802,140.205,-6.831,140.154,-6.879,140.111,-6.947,140.09,-7.009,140.092,-7.066,140.062,-7.114,139.999,-7.153,139.918,-7.146,139.85,-7.105,139.798,-7.105,139.74,-7.138,139.707,-7.204,139.652,-7.222,139.626,-7.193,139.589,-7.2,139.558,-7.231,139.521,-7.252,139.489,-7.244,139.468,-7.215,139.458,-7.164,139.421,-7.16,139.388,-7.185,139.355,-7.204,139.237,-7.177,139.177,-7.19]]},{"n":"Dihang","z":2.1,"b":[94.356,28.003,95.418,29.876],"l":[[94.356,29.31,94.526,29.439,94.641,29.484,94.759,29.489,94.844,29.521,94.897,29.58,94.919,29.636,94.909,29.689,94.959,29.728,95.069,29.752,95.125,29.793,95.128,29.851,95.171,29.876,95.255,29.867,95.294,29.85,95.29,29.824,95.312,29.801,95.36,29.781,95.395,29.691,95.418,29.531,95.385,29.41,95.298,29.329,95.191,29.258,95.064,29.198,94.944,29.09,94.83,28.933,94.809,28.846,94.88,28.829,94.961,28.751,95.052,28.61,95.097,28.506,95.094,28.439,95.068,28.376,95.018,28.316,95.003,28.258,95.022,28.202,95.088,28.172,95.199,28.169,95.291,28.127,95.396,28.003]]},{"n":"Dnepre","z":4,"b":[33.391,54.917,33.891,55.874],"l":[[33.735,55.874,33.818,55.827,33.826,55.802,33.788,55.784,33.803,55.764,33.872,55.742,33.891,55.713,33.845,55.657,33.768,55.57,33.702,55.546,33.622,55.556,33.585,55.498,33.59,55.372,33.57,55.287,33.524,55.243,33.473,55.145,33.391,54.917]]},{"n":"Dniester","z":5,"b":[22.847,46.09,30.493,49.565],"l":[[30.478,46.101,30.493,46.09],[22.847,49.267,23.082,49.461,23.388,49.554,23.607,49.565,23.841,49.524,24.019,49.481,24.139,49.436,24.227,49.372,24.281,49.29,24.36,49.245,24.466,49.237,24.594,49.182,24.745,49.08,24.851,49.028,24.912,49.026,24.987,48.998,25.122,48.918,25.155,48.878,25.185,48.882,25.211,48.885,25.23,48.864,25.251,48.842,25.302,48.842,25.339,48.832,25.398,48.845,25.521,48.809,25.668,48.689,25.731,48.659,25.781,48.66,25.827,48.649,25.894,48.608,25.958,48.602,26.005,48.614,26.037,48.617,26.072,48.572,26.093,48.563,26.13,48.572,26.169,48.547,26.253,48.529,26.398,48.535,26.498,48.517,26.555,48.474,26.591,48.466,26.607,48.493,26.639,48.504,26.668,48.517,26.694,48.539,26.72,48.55,26.759,48.562,26.784,48.565,26.804,48.579,26.835,48.583,26.884,48.558,26.997,48.56,27.272,48.601,27.433,48.584,27.48,48.51,27.574,48.465,27.714,48.449,27.82,48.416,27.891,48.365,27.963,48.334,28.038,48.321,28.08,48.296,28.088,48.257,28.159,48.238,28.291,48.239,28.347,48.213,28.327,48.161,28.341,48.144,28.388,48.162,28.423,48.147,28.442,48.109,28.463,48.091,28.496,48.096,28.539,48.104,28.574,48.069,28.572,48.028,28.604,48.009,28.653,48.017,28.766,47.987,28.934,47.898,29.01,47.796,28.994,47.68,29.017,47.583,29.079,47.504,29.103,47.45,29.089,47.421,29.103,47.392,29.12,47.369,29.116,47.341,29.104,47.31,29.154,47.235,29.145,47.181,29.132,47.153,29.137,47.136,29.172,47.136,29.215,47.143,29.265,47.146,29.314,47.116,29.359,47.066,29.36,47.029,29.317,47.005,29.3,46.984,29.311,46.967,29.424,46.955,29.472,46.912,29.499,46.829,29.537,46.801,29.589,46.827,29.635,46.813,29.677,46.756,29.69,46.715,29.674,46.689,29.693,46.665,29.748,46.641,29.763,46.62,29.77,46.601,29.805,46.581,29.898,46.553,30.131,46.423,30.22,46.426,30.254,46.401,30.269,46.32]]},{"n":"Dnieper","z":4,"b":[30.034,46.616,35.194,55.001],"l":[[32.578,46.616,32.645,46.65,32.934,46.717,33.067,46.759,33.237,46.777,33.357,46.786,33.451,46.809,33.54,46.831],[35.105,47.84,35.13,47.972,35.118,48.038,35.077,48.081,35.087,48.108,35.149,48.12,35.185,48.15,35.194,48.199,35.17,48.28,35.113,48.394,34.959,48.477,34.819,48.492,34.708,48.53,34.41,48.64,34.225,48.736,34.065,48.808,33.717,48.942,33.508,49.008,33.363,49.065,33.194,49.094],[31.888,49.554,31.691,49.636,31.462,49.757],[31.44,50,31.244,49.979,31.071,50.023,30.856,50.126,30.707,50.222,30.625,50.312,30.567,50.417,30.515,50.597],[33.391,54.917,33.109,54.986,32.978,55.001,32.903,54.986,32.823,54.958,32.738,54.916,32.687,54.871,32.67,54.822,32.611,54.763,32.512,54.694,32.382,54.695,32.221,54.765,32.006,54.777,31.598,54.707,30.974,54.655,30.718,54.613,30.622,54.564,30.552,54.544,30.509,54.553,30.463,54.537,30.413,54.495,30.36,54.425,30.304,54.326,30.318,54.185,30.405,54.001,30.407,53.899,30.324,53.879,30.275,53.802,30.261,53.666,30.284,53.523,30.371,53.297,30.279,53.25,30.26,53.206,30.284,53.148,30.251,53.12,30.162,53.124,30.091,53.101,30.038,53.051,30.034,52.954,30.081,52.809,30.143,52.71,30.222,52.658,30.258,52.606,30.252,52.555,30.279,52.507,30.342,52.462,30.365,52.43,30.351,52.409,30.397,52.38,30.503,52.342,30.546,52.306,30.525,52.272,30.584,52.197,30.722,52.08,30.796,51.996,30.805,51.944,30.773,51.894,30.699,51.844,30.625,51.766,30.551,51.658,30.547,51.545,30.612,51.427,30.611,51.33,30.508,51.217]]},{"n":"Don","z":5,"b":[38.306,47.106,43.549,54.064],"l":[[42.11,47.602,42.015,47.56,41.853,47.564,41.755,47.59,41.593,47.599,41.407,47.599,41.295,47.564,41.184,47.547,41.073,47.549,41.009,47.567,40.993,47.601,40.97,47.61,40.926,47.586,40.588,47.491,40.412,47.401,40.283,47.284,40.112,47.222,39.9,47.216,39.689,47.187,39.378,47.111,39.293,47.106],[43.549,49.507,43.379,49.487,43.316,49.456,43.302,49.408,43.252,49.365,43.165,49.329,43.095,49.324,43.042,49.351,43.016,49.38,43.017,49.412,42.957,49.442,42.835,49.472,42.797,49.516,42.77,49.573,42.731,49.582,42.647,49.567,42.446,49.579,42.297,49.599,42.006,49.61,41.833,49.597,41.713,49.616,41.467,49.613,41.38,49.588,41.314,49.587,41.27,49.609,41.261,49.63,41.288,49.648,41.279,49.683,41.235,49.734,41.14,49.77,40.997,49.791,40.921,49.827,40.914,49.879,40.89,49.922,40.849,49.956,40.778,49.964,40.677,49.945,40.644,49.953,40.621,49.977,40.596,50.027,40.502,50.12,40.432,50.139,40.386,50.083,40.322,50.065,40.198,50.096,40.04,50.103,40.001,50.133,40.03,50.186,40.027,50.238,39.99,50.289,40.002,50.346,40.064,50.408,40.036,50.475,39.92,50.547,39.907,50.617,39.879,50.65,39.794,50.692,39.757,50.733,39.768,50.775,39.794,50.797,39.814,50.833,39.789,50.874,39.775,50.919,39.778,50.952,39.745,50.962,39.677,50.955,39.513,50.96,39.266,50.987,39.143,51.024,39.144,51.071,39.182,51.111,39.256,51.142,39.262,51.173,39.199,51.203,39.177,51.238,39.16,51.294,39.131,51.364,39.052,51.43,39.052,51.477,39.059,51.507,39.037,51.54,39.018,51.568,39.012,51.597,39.038,51.626,39.046,51.657,39.035,51.69,39.06,51.757,39.122,51.859,39.14,51.934,39.133,52.012],[39.133,52.019,39.152,52.052,39.164,52.084,39.154,52.125,39.096,52.179,38.99,52.248,38.958,52.289,38.958,52.327,38.905,52.438,38.931,52.576,38.933,52.665,38.911,52.703,38.964,52.794,39.093,52.938,39.155,53.118,39.152,53.333,39.112,53.449,39.056,53.519,39.021,53.561,38.998,53.576,38.871,53.618,38.641,53.687,38.531,53.754,38.541,53.819,38.529,53.861,38.478,53.889,38.306,54.064]]},{"n":"Danube","z":2.1,"b":[8.22,47.931,17.206,49.027],"l":[[8.22,48.047,8.49,47.931,8.696,47.936,8.836,47.976,8.979,48.053,9.132,48.086,9.295,48.078,9.421,48.116,9.509,48.202,9.589,48.244,9.661,48.241,9.784,48.289,9.959,48.388,10.127,48.453,10.289,48.483,10.385,48.516,10.415,48.552,10.524,48.607,10.712,48.679,11.025,48.735,11.684,48.794,11.766,48.871,11.85,48.906,11.964,48.925,12.024,48.958,12.033,49.027,12.242,49.018,12.361,48.992,12.507,48.924],[17.206,48.061,17.165,48.117,17.111,48.146,17.029,48.167,16.878,48.172,16.659,48.161,16.486,48.213,16.36,48.327,16.209,48.38,16.034,48.369,15.861,48.379,15.693,48.411,15.573,48.424,15.502,48.417,15.431,48.371,15.36,48.286,15.265,48.229,15.147,48.198,15.036,48.198,14.932,48.227,14.829,48.227,14.726,48.197,14.583,48.219,14.4,48.292,14.243,48.328,14.114,48.329,14.037,48.352,14.012,48.399,13.872,48.462,13.487,48.582,13.316,48.602,13.2,48.636,13.081,48.691,13.005,48.748,12.972,48.806,12.909,48.836,12.816,48.836,12.704,48.86,12.507,48.931]]},{"n":"Double Mountain Fork Brazos","z":5,"b":[-101.667,32.905,-100.501,33.54],"l":[[-101.667,33.54,-101.426,33.4,-101.323,33.266,-101.253,33.201,-101.18,33.16,-101.107,33.14,-100.995,33.14,-100.832,33.103,-100.709,33.044,-100.501,32.905]]},{"n":"Drava","z":5,"b":[13.822,45.558,18.954,47.402],"l":[[13.822,47.09,14.367,47.147,14.507,47.209,14.62,47.224,14.752,47.214,14.861,47.245,14.946,47.319,15.062,47.371,15.145,47.4,15.208,47.402,15.303,47.394,15.346,47.347,15.352,47.316,15.323,47.3,15.316,47.253,15.329,47.174,15.361,47.121,15.413,47.095,15.476,46.997,15.56,46.831,15.582,46.767,15.63,46.736,15.701,46.723,16.042,46.663,16.211,46.57,16.321,46.535,16.428,46.524,16.57,46.485,16.748,46.416,16.871,46.339,16.94,46.254,17.033,46.187,17.15,46.14,17.242,46.077,17.311,45.996,17.406,45.951,17.529,45.941,17.607,45.914,17.64,45.868,17.706,45.827,17.807,45.79,17.964,45.77,18.283,45.765,18.551,45.627,18.712,45.574,18.954,45.558]]},{"n":"Duero","z":5,"b":[-8.675,41.062,-2.357,41.928],"l":[[-2.837,41.928,-2.542,41.85,-2.444,41.797,-2.445,41.743,-2.424,41.688,-2.357,41.605,-2.498,41.521,-2.617,41.505,-2.76,41.528,-2.866,41.529,-2.935,41.508,-3.038,41.524,-3.176,41.578,-3.411,41.633,-3.743,41.688,-3.941,41.693,-4.002,41.649,-4.146,41.631,-4.372,41.64,-4.682,41.592,-5.075,41.489,-5.307,41.464,-5.378,41.516,-5.518,41.527,-5.726,41.498,-5.838,41.492,-5.888,41.474,-5.971,41.49,-6.092,41.543,-6.178,41.55,-6.244,41.516,-6.289,41.455,-6.403,41.375,-6.566,41.304,-6.69,41.214,-6.776,41.108,-6.883,41.062,-7.011,41.079,-7.085,41.113,-7.103,41.166,-7.244,41.189,-7.651,41.181,-8.235,41.092,-8.425,41.084,-8.555,41.128,-8.675,41.154]]},{"n":"Ebro","z":4.7,"b":[-4.189,40.722,0.891,43.011],"l":[[-4.189,43.011,-4.102,43.001,-4.055,42.953,-4.017,42.861,-3.963,42.825,-3.89,42.844,-3.822,42.842,-3.757,42.817,-3.709,42.833,-3.678,42.888,-3.627,42.898,-3.522,42.845,-3.451,42.807,-3.337,42.792,-3.155,42.787,-3.011,42.755,-2.905,42.695,-2.843,42.644,-2.823,42.602,-2.756,42.56,-2.641,42.518,-2.563,42.507,-2.522,42.527,-2.395,42.514,-2.183,42.466,-1.994,42.391,-1.748,42.236,-1.405,41.978,-1.11,41.802,-0.748,41.622,-0.526,41.479,-0.444,41.374,-0.301,41.309,-0.097,41.284,0.04,41.306,0.111,41.375,0.167,41.411,0.208,41.413,0.263,41.399,0.331,41.368,0.362,41.328,0.357,41.28,0.364,41.256,0.395,41.254,0.525,41.249,0.59,41.22,0.632,41.165,0.652,41.119,0.652,41.083,0.614,41.038,0.538,40.982,0.517,40.904,0.551,40.802,0.651,40.745,0.891,40.722]]},{"n":"White Nile","z":2,"b":[31.125,9.354,32.796,15.635],"l":[[31.125,9.433,31.346,9.366,31.453,9.354,31.518,9.376,31.581,9.445,31.64,9.562,31.746,9.657,31.899,9.731,32.036,9.834,32.158,9.968,32.235,10.072,32.267,10.148,32.27,10.246,32.244,10.369,32.201,10.435,32.139,10.446,32.108,10.471,32.106,10.51,32.24,10.656,32.511,10.908,32.645,11.103,32.642,11.24,32.673,11.414,32.737,11.624,32.764,11.795,32.749,11.995,32.73,12.19,32.743,12.333,32.783,12.489,32.791,12.652,32.765,12.822,32.767,12.939,32.796,13.004,32.752,13.117,32.635,13.277,32.586,13.424,32.604,13.559,32.587,13.638,32.535,13.662,32.5,13.704,32.481,13.764,32.439,13.825,32.373,13.886,32.295,14.032,32.205,14.264,32.172,14.465,32.196,14.637,32.262,14.783,32.37,14.902,32.44,15.13,32.49,15.635]]},{"n":"Blue Nile","z":3,"b":[32.49,10.155,35.353,15.635],"l":[[35.353,10.155,35.343,10.187,35.293,10.411,35.242,10.517,35.147,10.573,35.182,10.974,35.161,11.14,35.097,11.202,34.993,11.249,34.778,11.296],[34.38,11.783,34.324,12.048,34.317,12.194,34.341,12.308,34.301,12.403,34.199,12.478,34.157,12.534,34.176,12.572,34.175,12.637,34.154,12.729,34.117,12.804,34.065,12.863,34.04,12.872,34,12.902,33.981,12.958,33.961,13.144,33.93,13.258,33.89,13.3,33.828,13.325,33.744,13.334,33.692,13.381,33.672,13.466,33.639,13.53,33.593,13.573,33.582,13.619,33.607,13.669,33.602,13.708,33.578,13.734,33.581,13.756,33.597,13.772,33.599,13.793,33.58,13.819,33.574,13.839,33.581,13.856,33.596,13.867,33.604,13.887,33.591,13.937,33.609,14.011,33.657,14.109,33.566,14.344,33.337,14.716,33.211,14.956,33.189,15.064,33.134,15.16,33.046,15.244,32.949,15.304,32.842,15.339,32.735,15.423,32.628,15.553,32.554,15.622,32.49,15.635]]},{"n":"Elbe","z":4.7,"b":[9.784,50.031,15.889,53.555],"l":[[15.583,50.693,15.615,50.614,15.653,50.576,15.706,50.553,15.734,50.522,15.736,50.484,15.782,50.44,15.864,50.394,15.884,50.372,15.889,50.328,15.845,50.272,15.819,50.197,15.817,50.107,15.677,50.052,15.4,50.031,15.236,50.035,15.159,50.075,15.088,50.152,14.969,50.179,14.779,50.181,14.631,50.222,14.473,50.343,14.388,50.438,14.332,50.46,14.277,50.442,14.241,50.454,14.222,50.498,14.178,50.52,14.109,50.52,14.062,50.535,14.039,50.567,14.04,50.589,14.063,50.602,14.067,50.619,14.07,50.641,14.084,50.661,14.16,50.686,14.203,50.75,14.212,50.851,14.136,50.918,13.977,50.95,13.874,50.991,13.828,51.041,13.744,51.079,13.622,51.104,13.522,51.141,13.443,51.19,13.398,51.235,13.386,51.278,13.345,51.312,13.275,51.339,13.226,51.382,13.184,51.474],[13.191,51.474,12.934,51.623,12.842,51.711,12.829,51.79,12.742,51.843,12.58,51.871,12.382,51.881,12.147,51.874,11.995,51.909,11.925,51.986,11.843,52.031,11.749,52.046,11.691,52.072,11.668,52.109,11.668,52.155,11.692,52.21,11.757,52.273,11.917,52.378,11.978,52.438,12.003,52.562,12.014,52.77,11.921,52.918,11.724,53.006,11.555,53.054,11.414,53.062,11.297,53.093,11.203,53.147,11.139,53.166,11.106,53.151,10.994,53.2,10.804,53.312,10.682,53.369,10.601,53.372,10.413,53.417,10.305,53.426,10.215,53.413,10.082,53.44],[10.082,53.44,9.819,53.54,9.784,53.555,9.9,53.552,9.988,53.538,10.048,53.516,10.082,53.44]]},{"n":"Ertis","z":2.1,"b":[74.994,47.727,85.267,53.986],"l":[[85.267,47.994,84.999,47.928,84.88,47.88,84.822,47.828,84.729,47.783,84.6,47.744,84.491,47.727,84.357,47.732],[83.364,49.644,83.128,49.682,82.991,49.748,82.872,49.856,82.696,49.947,82.462,50.021,82.321,50.101,82.273,50.185,82.15,50.219,81.953,50.202,81.775,50.228,81.614,50.295,81.482,50.325,81.379,50.319,81.248,50.338,81.089,50.382,80.963,50.392,80.87,50.367,80.691,50.36,80.426,50.373,80.242,50.41,80.139,50.473,80.019,50.496,79.882,50.479,79.702,50.517,79.479,50.608,79.313,50.662,79.203,50.678,79.116,50.667,79.05,50.63,78.9,50.651,78.545,50.767,78.348,50.858,78.245,50.923,78.171,50.993,78.106,51.094,78.05,51.225,77.96,51.335,77.837,51.424,77.691,51.499,77.521,51.56,77.441,51.651,77.451,51.771,77.334,51.886,77.089,51.997,76.96,52.108,76.948,52.219,76.9,52.303,76.814,52.36,76.78,52.411,76.796,52.455,76.794,52.502,76.775,52.552,76.734,52.589,76.673,52.612,76.654,52.64,76.678,52.672,76.679,52.704,76.659,52.736,76.55,52.816,76.514,52.827,76.494,52.83,76.479,52.85,76.485,52.885,76.461,52.899,76.314,52.905,76.26,52.93,76.242,52.98,76.144,53.015,75.967,53.034,75.855,53.078,75.806,53.146,75.727,53.202,75.619,53.246,75.568,53.282,75.567,53.304,75.545,53.322,75.521,53.333,75.503,53.344,75.506,53.361,75.503,53.397,75.457,53.455,75.357,53.52,75.203,53.59,75.101,53.654,75.05,53.71,75.04,53.789,75.07,53.891,75.062,53.953,74.994,53.986]]},{"n":"Irtysh","z":2.1,"b":[85.267,46.99,90.325,48.019],"l":[[90.325,47.65,90.284,47.491,90.253,47.43,90.201,47.408,90.132,47.401,90.064,47.356,89.973,47.269,89.872,47.213,89.763,47.187,89.655,47.126,89.546,47.03,89.406,46.99,89.233,47.005,89.032,47.061,88.804,47.159,88.515,47.233,88.166,47.284,87.847,47.348,87.559,47.427,87.387,47.495,87.331,47.555,87.266,47.58,87.19,47.572,87.1,47.591,86.944,47.66,86.607,47.832,86.433,47.878,86.31,47.853,86.154,47.876,85.966,47.945,85.792,47.98,85.634,47.982,85.544,47.994,85.521,48.016,85.449,48.019,85.267,47.994]]},{"n":"Euphrates","z":3,"b":[40.993,30.902,47.468,34.509],"l":[[45.463,31.305,45.541,31.229,45.685,31.184,45.922,31.146,46.121,31.086,46.363,30.965,46.483,30.903,46.58,30.902,46.692,30.942,46.886,30.959,47.16,30.953,47.331,30.967,47.399,31,47.442,31.004,47.468,30.968],[40.993,34.428,41.119,34.388,41.177,34.393,41.208,34.43,41.276,34.443,41.38,34.43,41.43,34.436,41.448,34.44,41.471,34.449,41.502,34.436,41.591,34.448,41.717,34.497,41.827,34.509,41.921,34.484,41.99,34.446,42.055,34.369,42.122,34.302,42.17,34.288,42.22,34.306,42.248,34.297,42.263,34.29,42.286,34.292,42.303,34.334,42.33,34.346,42.366,34.329,42.366,34.302,42.331,34.264,42.332,34.231,42.369,34.205,42.383,34.165,42.375,34.114,42.417,34.059,42.447,34.041,42.51,34.001,42.551,33.944,42.541,33.888,42.563,33.87,42.617,33.891,42.673,33.89,42.73,33.865,42.748,33.82,42.724,33.752,42.767,33.671,42.877,33.577,42.965,33.521,43.032,33.503,43.079,33.505,43.106,33.526,43.155,33.515,43.228,33.471,43.288,33.455,43.334,33.466,43.454,33.442,43.536,33.418,43.65,33.384,43.776,33.311,43.832,33.226,43.872,33.199,43.905,33.199,43.948,33.195,44.029,33.16,44.122,33.053,44.254,32.828,44.263,32.793,44.264,32.727,44.237,32.624,44.255,32.496,44.321,32.349,44.355,32.235,44.356,32.157,44.387,32.072,44.448,31.981,44.477,31.878,44.475,31.762,44.548,31.648,44.77,31.479,45.042,31.441,45.158,31.403,45.209,31.352,45.293,31.321,45.467,31.305]]},{"n":"Ferenc Csatorna","z":5,"b":[18.839,45.836,18.92,46.267],"l":[[18.92,46.267,18.909,45.95,18.905,45.932,18.894,45.866,18.839,45.836]]},{"n":"Finlay","z":5,"b":[-127.235,56.757,-124.836,57.674],"l":[[-127.235,56.815,-126.926,56.916,-126.915,57.082,-126.822,57.19,-126.645,57.296,-126.551,57.415,-126.542,57.546,-126.518,57.627,-126.478,57.659,-126.403,57.674,-126.294,57.674,-126.167,57.622,-126.022,57.516,-125.845,57.439,-125.634,57.391,-125.393,57.261,-125.12,57.049,-124.946,56.897,-124.836,56.757]]},{"n":"Firat","z":3,"b":[37.848,36.836,41.455,40.18],"l":[[38.768,38.813,38.555,38.768,38.45,38.714,38.383,38.635,38.355,38.581,38.367,38.551,38.452,38.508,38.61,38.45,38.719,38.426,38.78,38.435,38.847,38.413,38.921,38.358,38.997,38.334,39.075,38.34,39.115,38.306,39.115,38.23,39.149,38.19,39.218,38.183,39.248,38.154,39.239,38.102,39.171,38.034,39.042,37.949,38.978,37.887,38.978,37.849,38.956,37.82,38.949,37.797,38.924,37.745,38.841,37.713,38.829,37.69,38.838,37.666,38.813,37.645,38.701,37.631,38.608,37.593,38.533,37.532,38.439,37.495,38.328,37.483,38.25,37.461,38.207,37.429,38.137,37.421,38.039,37.437,37.966,37.425,37.92,37.384,37.855,37.292,37.848,37.217,37.862,37.116,37.897,37.061,37.951,37.054,37.991,36.996,38.026,36.836],[41.455,40.18,41.328,40.016,40.985,39.944,40.77,39.928,40.569,39.945,40.396,39.942,40.251,39.919,40.169,39.895,40.15,39.869,40.17,39.824,40.23,39.76,40.24,39.694,40.203,39.626,40.163,39.584,40.122,39.568,39.939,39.595,39.613,39.666,39.415,39.683,39.343,39.648,39.196,39.619,38.976,39.594,38.74,39.536,38.488,39.445,38.426,39.342,38.555,39.229,38.643,39.17,38.689,39.167,38.718,39.144,38.74,39.078]]},{"n":"Fitzroy","z":5,"b":[123.563,-18.492,127.152,-17.383],"l":[[127.152,-17.383,126.998,-17.454,126.903,-17.478,126.775,-17.478,126.4,-17.458,126.237,-17.48,126.162,-17.538,126.124,-17.582,126.124,-17.613,126.098,-17.633,126.045,-17.641,125.985,-17.707,125.918,-17.83,125.792,-17.988,125.511,-18.277,125.139,-18.452,124.923,-18.492,124.736,-18.456,124.581,-18.353,124.456,-18.183,124.351,-18.09,124.267,-18.075,124.178,-18.085,124.087,-18.119,124,-18.116,123.918,-18.075,123.817,-17.967,123.698,-17.793,123.622,-17.701,123.588,-17.691,123.57,-17.661,123.568,-17.588,123.563,-17.521]]},{"n":"Fraser","z":4.7,"b":[-123.08,49.131,-118.319,54.245],"l":[[-118.319,52.522,-118.572,52.762,-118.601,52.825,-118.721,52.891,-118.941,52.983,-119.175,53.023,-119.425,53.009,-119.669,53.056,-119.906,53.161,-120.244,53.346,-120.681,53.609,-121.024,53.778,-121.396,53.889,-121.584,53.93,-121.649,53.963,-121.654,54.002,-121.738,54.061,-121.903,54.141,-122.089,54.201,-122.295,54.24,-122.435,54.245,-122.509,54.215,-122.551,54.174,-122.56,54.121,-122.611,54.046,-122.704,53.947,-122.744,53.816,-122.733,53.652,-122.708,53.534,-122.67,53.461,-122.709,53.399,-122.827,53.348,-122.804,53.266,-122.64,53.153,-122.53,53.042,-122.475,52.932,-122.457,52.798,-122.475,52.642,-122.436,52.508,-122.34,52.395,-122.285,52.259,-122.27,52.097,-122.272,51.99,-122.29,51.937,-122.321,51.902,-122.365,51.884,-122.392,51.846,-122.408,51.758,-122.268,51.519,-122.225,51.407,-122.232,51.345,-122.143,51.221,-121.955,51.038,-121.858,50.901,-121.85,50.812,-121.866,50.766,-121.906,50.763,-121.924,50.74,-121.92,50.698,-121.86,50.615,-121.744,50.491,-121.628,50.302,-121.51,50.047,-121.449,49.791,-121.445,49.532,-121.49,49.387,-121.582,49.354,-121.652,49.307,-121.698,49.246,-121.874,49.19,-122.179,49.139,-122.445,49.141,-122.673,49.197,-122.85,49.207,-123.041,49.151,-123.08,49.131]]},{"n":"Gan","z":5,"b":[114.412,24.707,116.016,29.279],"l":[[114.412,24.707,114.48,24.708,114.53,24.735,114.614,24.816,114.73,24.882,114.762,24.953,114.748,25.051,114.783,25.113,114.865,25.137,114.899,25.196,114.885,25.29,114.93,25.43,115.033,25.614,115.09,25.746,115.101,25.826,115.068,25.864,114.992,25.86,114.94,25.882,114.913,25.93,114.909,25.992,114.938,26.105,114.904,26.125,114.908,26.153,114.939,26.194,114.946,26.237,114.931,26.283,114.862,26.369,114.738,26.497,114.717,26.614,114.797,26.722,114.879,26.789,114.961,26.813,114.995,26.849,114.98,26.897,114.988,26.942,115.021,26.985,115.021,27.032,114.99,27.083,115.009,27.138,115.076,27.198,115.098,27.284,115.075,27.396,115.13,27.512,115.264,27.634,115.369,27.785,115.445,27.966,115.577,28.11,115.765,28.218,115.848,28.317,115.818,28.452,115.816,28.585,115.869,28.716,115.975,28.888,116.016,29.05,115.982,29.279]]},{"n":"Ganges","z":3,"b":[78.022,21.721,90.435,30.888],"l":[[89.483,22.276,89.425,22.496,89.372,22.581,89.3,22.619],[89.3,22.619,89.261,22.574,89.255,22.445,89.286,22.308,89.354,22.16,89.379,22.058,89.352,21.972,89.351,21.886,89.336,21.788,89.354,21.721],[88.44,22.986,88.385,22.868,88.347,22.735,88.344,22.629,88.29,22.549,88.185,22.496,88.118,22.41,88.087,22.218],[88.44,22.986,88.518,22.975,88.598,22.975,88.682,22.954,88.756,22.915,88.82,22.858,88.843,22.803,88.823,22.748,88.845,22.7,88.91,22.659,88.971,22.511,89.05,22.275,89.056,22.186,89.051,22.093],[90.074,23.573,90.096,23.34,90.127,23.252,90.181,23.206,90.228,23.178,90.244,23.138,90.265,23.116,90.299,23.088,90.302,23.055,90.315,23.016,90.362,22.961,90.361,22.876,90.394,22.775,90.435,22.752],[89.109,23.992,89.219,23.808,89.302,23.733,89.381,23.694,89.447,23.64,89.5,23.572,89.52,23.525,89.509,23.499,89.527,23.483,89.575,23.477,89.598,23.443,89.598,23.383,89.615,23.337,89.632,23.301,89.638,23.252,89.66,23.223,89.709,23.192,89.719,23.133,89.765,23.02,89.926,22.84,90.008,22.685,90.014,22.563,89.985,22.466],[89.109,23.992,89.2,23.933,89.281,23.931,89.38,23.901,89.497,23.844,89.604,23.823,89.749,23.848],[88.397,24.389,88.51,24.261,88.534,24.208,88.527,24.166,88.554,24.129,88.616,24.098,88.643,24.059,88.636,24.012,88.69,23.968,88.805,23.928,88.864,23.856,88.868,23.752,88.837,23.666,88.772,23.598,88.764,23.531,88.811,23.463,88.853,23.441,88.885,23.423,88.906,23.39,88.926,23.363,88.989,23.333,89.002,23.263,89.028,23.186,89.089,23.113,89.113,23.04,89.1,22.97,89.113,22.893,89.154,22.809,89.208,22.749,89.268,22.708,89.276,22.703,89.271,22.679,89.3,22.619],[88.397,24.389,88.499,24.347,88.642,24.326,88.724,24.275,88.742,24.193,88.77,24.144,88.806,24.127,88.858,24.131,88.926,24.155,89.01,24.109,89.109,23.992],[87.92,24.825,87.94,24.666,88.019,24.543,88.175,24.389,88.238,24.234,88.209,24.079,88.205,23.945,88.227,23.831,88.215,23.74,88.183,23.7,88.214,23.637,88.272,23.587,88.304,23.543,88.345,23.53,88.361,23.461,88.352,23.333,88.375,23.255,88.43,23.227,88.467,23.186,88.486,23.134,88.459,23.027,88.44,22.986],[87.92,24.825,87.946,24.753,87.999,24.661,88.079,24.55,88.145,24.486,88.225,24.461,88.287,24.48,88.338,24.454,88.397,24.389],[79.841,30.888,79.82,30.837,79.829,30.787,79.859,30.722,79.865,30.679,79.834,30.648,79.745,30.543,79.667,30.522,79.571,30.55,79.47,30.516,79.313,30.372,79.232,30.301,79.159,30.285,79.067,30.3,78.939,30.292,78.777,30.26,78.669,30.201,78.614,30.116,78.52,30.09,78.387,30.124,78.281,30.093,78.203,29.998,78.17,29.889,78.183,29.768,78.139,29.673,78.04,29.606,78.022,29.492,78.086,29.332,78.106,29.181,78.084,29.04,78.098,28.882,78.176,28.621,78.242,28.522,78.271,28.443,78.284,28.352,78.337,28.253,78.43,28.146,78.603,28.026,78.855,27.893,79.025,27.825,79.112,27.82,79.167,27.786,79.192,27.723,79.255,27.68,79.358,27.658,79.446,27.62,79.519,27.568,79.591,27.466,79.662,27.316,79.733,27.217,79.838,27.145,79.922,27.126,79.998,27.025,80.094,26.837,80.238,26.648,80.43,26.459,80.544,26.317,80.58,26.222,80.652,26.138,80.76,26.066,80.869,26.038,80.978,26.053,81.123,25.989,81.303,25.844,81.395,25.746,81.398,25.692,81.458,25.646,81.573,25.606,81.649,25.564,81.686,25.518,81.756,25.499,81.857,25.507,81.907,25.487,81.903,25.413,82.042,25.312,82.12,25.285,82.183,25.296,82.215,25.272,82.217,25.259,82.226,25.243,82.239,25.241,82.254,25.246,82.274,25.262,82.3,25.26,82.318,25.23,82.345,25.227,82.38,25.249,82.436,25.235,82.513,25.184,82.552,25.184,82.563,25.203,82.585,25.218,82.62,25.22,82.786,25.144,82.879,25.142,82.965,25.188,83.017,25.239,83.033,25.295,83.074,25.33,83.139,25.344,83.159,25.377,83.135,25.427,83.139,25.472,83.17,25.51,83.242,25.519,83.353,25.498,83.44,25.466,83.489,25.444,83.513,25.462,83.513,25.497,83.518,25.534,83.6,25.584,83.67,25.607,83.713,25.59,83.752,25.542,83.803,25.519,83.866,25.522,83.959,25.577,84.082,25.683,84.171,25.722,84.226,25.694,84.284,25.701,84.343,25.744,84.428,25.747,84.54,25.711,84.634,25.708,84.711,25.738,84.827,25.747,84.981,25.734,85.119,25.683,85.239,25.592,85.338,25.544,85.412,25.535,85.456,25.538,85.501,25.545,85.555,25.544,85.618,25.494,85.697,25.474,85.786,25.495,85.88,25.462,85.979,25.377,86.098,25.324,86.237,25.303,86.371,25.341,86.5,25.437,86.571,25.448,86.584,25.373,86.617,25.315,86.668,25.276,86.743,25.28,86.841,25.327,86.965,25.32,87.115,25.26,87.219,25.267,87.275,25.34,87.35,25.383,87.444,25.394,87.518,25.366,87.572,25.3,87.644,25.257,87.734,25.237,87.779,25.193,87.78,25.124,87.824,25.064,87.911,25.013,87.942,24.956,87.919,24.893,87.92,24.825]]},{"n":"Ariège","z":5,"b":[1.454,42.627,1.825,43.415],"l":[[1.809,42.627,1.825,42.693,1.791,42.732,1.675,42.783,1.616,42.856,1.594,42.97,1.587,43.149,1.54,43.298,1.454,43.415]]},{"n":"Genala","z":5,"b":[38.724,4.324,41.966,6.605],"l":[[38.774,6.605,38.724,6.541,38.76,6.453,38.92,6.253,38.991,6.126,39.11,6.054,39.357,5.981,39.388,5.969,39.407,5.952,39.478,5.795,39.559,5.724,39.654,5.709,39.701,5.658,39.699,5.572,39.736,5.526,39.81,5.521,39.88,5.488,39.946,5.426,40.031,5.385,40.135,5.365,40.273,5.37,40.443,5.4,40.627,5.487,40.824,5.629,40.954,5.702,41.016,5.708,41.073,5.693,41.124,5.66,41.196,5.526,41.288,5.291,41.394,5.09,41.515,4.922,41.579,4.801,41.587,4.728,41.685,4.599,41.966,4.324]]},{"n":"Gharraf Canal","z":4,"b":[45.809,30.964,46.405,32.502],"l":[[45.809,32.502,45.901,32.431,45.928,32.377,45.923,32.316,45.943,32.264,45.989,32.219,45.998,32.133,45.97,32.005,45.986,31.922,46.046,31.883,46.089,31.773,46.116,31.592,46.154,31.457,46.203,31.367,46.276,31.282,46.371,31.202,46.405,31.112,46.366,30.964]]},{"n":"Gila","z":5,"b":[-114.582,32.657,-108.114,33.577],"l":[[-111.249,33.104,-111.455,33.04,-111.648,33.054,-111.899,33.126,-112.122,33.222,-112.32,33.342,-112.511,33.372,-112.697,33.311,-112.767,33.238,-112.72,33.154,-112.704,33.087,-112.717,33.036,-112.809,33.03,-112.98,33.067,-113.084,33.067,-113.12,33.03,-113.197,33.001,-113.317,32.979,-113.525,32.906,-113.824,32.782,-114.005,32.725,-114.068,32.735,-114.13,32.725,-114.19,32.696,-114.261,32.709,-114.344,32.763,-114.428,32.776,-114.582,32.735],[-108.204,33.577,-108.114,33.413,-108.246,33.176,-108.348,33.091,-108.464,33.079,-108.555,32.992,-108.62,32.828,-108.71,32.718,-108.825,32.659,-108.944,32.657,-109.067,32.711,-109.156,32.786,-109.209,32.881,-109.264,32.94,-109.321,32.962,-109.397,32.943,-109.492,32.884,-109.591,32.853,-109.695,32.85,-109.846,32.932,-110.042,33.099,-110.226,33.186,-110.398,33.193,-110.554,33.15,-110.694,33.056,-110.814,33.038,-110.916,33.095,-111.039,33.121,-111.256,33.112]]},{"n":"Glåma","z":5,"b":[10.624,60.525,12.028,62.687],"l":[[11.484,62.687,11.188,62.496,11.085,62.443,10.984,62.391,10.773,62.309,10.655,62.245,10.632,62.201,10.624,62.157,10.634,62.112,10.699,62.062,10.821,62.006,10.888,61.95,10.901,61.893,10.883,61.835,10.835,61.776,10.863,61.7,10.968,61.607,11.033,61.526,11.072,61.423,11.247,61.317,11.318,61.233,11.344,61.134,11.402,61.029,11.493,60.919,11.579,60.849,11.661,60.818,11.737,60.763,11.806,60.685,11.881,60.633,11.962,60.609,12.009,60.579,12.028,60.525]]},{"n":"Godävari","z":4.7,"b":[73.652,16.334,82.339,20.036],"l":[[81.739,16.942,81.833,16.669,81.856,16.553,81.839,16.503,81.8,16.465,81.736,16.441,81.706,16.4,81.712,16.334],[81.739,16.942,81.888,16.777,82.034,16.713,82.339,16.707],[73.652,20.013,73.889,19.99,73.99,19.999,74.035,20.031,74.084,20.036,74.163,20.003,74.422,19.918,74.589,19.835,74.749,19.726,74.917,19.652,75.093,19.613,75.197,19.579,75.247,19.533,75.473,19.425,75.591,19.38,75.678,19.362,75.769,19.361,75.846,19.376,75.905,19.363,75.917,19.326,75.941,19.314,75.987,19.333,76.011,19.326,76.027,19.31,76.092,19.291,76.319,19.257,76.347,19.241,76.353,19.202,76.337,19.14,76.347,19.124,76.368,19.12,76.396,19.111,76.427,19.077,76.473,19.073,76.535,19.099,76.582,19.086,76.614,19.035,76.657,19.002,76.71,18.986,76.748,18.996,76.77,19.034,76.856,19.07,77.005,19.105,77.093,19.108,77.12,19.081,77.196,19.089,77.319,19.134,77.405,19.12,77.453,19.048,77.579,18.966,77.784,18.873,77.968,18.87,78.133,18.958,78.235,18.984,78.275,18.948,78.319,18.946,78.367,18.976,78.429,18.98,78.504,18.956,78.575,18.966,78.643,19.012,78.701,19.027,78.751,19.014,78.835,19.026,78.953,19.063,79.05,19.033,79.125,18.937,79.201,18.871,79.317,18.816,79.401,18.843,79.496,18.809,79.631,18.723,79.727,18.69,79.785,18.711,79.822,18.762,79.874,18.802,79.925,18.807,80.029,18.744,80.142,18.702,80.244,18.709,80.319,18.686,80.366,18.632,80.39,18.573,80.389,18.509,80.408,18.463,80.447,18.434,80.473,18.388,80.485,18.324,80.541,18.261,80.688,18.166,80.84,17.996,80.892,17.865,80.895,17.716,80.986,17.622,81.165,17.582,81.267,17.543,81.291,17.506,81.352,17.482,81.45,17.472,81.528,17.448,81.587,17.41,81.647,17.279,81.739,16.942]]},{"n":"Grande","z":3,"b":[-66.323,-22.155,-44.058,-15.86],"l":[[-44.45,-22.155,-44.304,-22.047,-44.208,-21.948,-44.112,-21.822,-44.062,-21.734,-44.058,-21.684,-44.068,-21.658,-44.093,-21.657,-44.127,-21.613,-44.171,-21.524,-44.241,-21.476,-44.387,-21.465],[-44.616,-21.318,-44.686,-21.236,-44.739,-21.206,-44.799,-21.201,-44.857,-21.176,-44.915,-21.13,-44.971,-21.125,-45.026,-21.161,-45.076,-21.175,-45.122,-21.169,-45.164,-21.14,-45.205,-21.093],[-46.326,-20.668,-46.587,-20.623],[-47.073,-20.284,-47.329,-20.118,-47.428,-20.036,-47.46,-20.001,-47.518,-19.994,-47.594,-20.019,-47.656,-20.023,-47.696,-19.991,-47.75,-19.977,-47.819,-19.981,-47.86,-20.012,-47.862,-20.056,-47.909,-20.082,-47.991,-20.083,-48.051,-20.12,-48.109,-20.117,-48.178,-20.083,-48.253,-20.093,-48.361,-20.139,-48.716,-20.144,-48.812,-20.159,-48.858,-20.233,-48.887,-20.37,-48.918,-20.429,-48.952,-20.41,-48.978,-20.338,-48.995,-20.214,-49.04,-20.187,-49.113,-20.259,-49.166,-20.295,-49.2,-20.294,-49.239,-20.246,-49.284,-20.149,-49.29,-20.072,-49.259,-20.017,-49.278,-19.987,-49.385,-19.98],[-50.364,-19.859,-50.434,-19.8,-50.489,-19.791,-50.584,-19.821],[-63.757,-19.028,-63.619,-19.071,-63.565,-19.049,-63.548,-18.977,-63.497,-18.928,-63.411,-18.904,-63.31,-18.847,-63.195,-18.756,-63.074,-18.615,-62.949,-18.425,-62.858,-18.197,-62.773,-17.799,-62.786,-17.598,-62.775,-17.465,-62.743,-17.334,-62.738,-17.243,-62.762,-17.192,-62.823,-17.151,-62.922,-17.121,-63,-17.031,-63.059,-16.88,-63.259,-16.732,-63.601,-16.585,-63.804,-16.449,-63.869,-16.322,-63.965,-16.237,-64.092,-16.192,-64.162,-16.148,-64.176,-16.104,-64.303,-16.027,-64.664,-15.86],[-66.323,-17.577,-66.019,-17.862,-65.96,-17.952,-65.916,-17.978,-65.868,-17.972,-65.793,-18.009,-65.642,-18.129,-65.39,-18.371,-65.287,-18.493,-65.25,-18.575,-65.19,-18.626,-65.108,-18.645,-65.051,-18.63,-65.018,-18.581,-64.956,-18.538,-64.863,-18.503,-64.756,-18.53,-64.635,-18.621,-64.529,-18.651,-64.437,-18.618,-64.342,-18.686,-64.242,-18.855,-64.16,-18.954,-64.097,-18.985,-64.05,-19.042,-64.021,-19.126,-63.985,-19.165,-63.94,-19.158,-63.906,-19.122,-63.884,-19.058,-63.842,-19.027,-63.751,-19.028]]},{"n":"Great Pee Dee","z":5,"b":[-80.481,33.405,-79.106,36.232],"l":[[-80.464,36.232,-80.456,36.206,-80.481,36.093,-80.459,36.011,-80.392,35.943,-80.365,35.893,-80.378,35.86,-80.406,35.843,-80.447,35.841,-80.465,35.817,-80.46,35.773,-80.423,35.742,-80.354,35.724,-80.27,35.652,-80.122,35.466,-80.071,35.401,-80.055,35.312,-80.058,35.176,-80.027,35.109,-79.963,35.113,-79.915,35.096,-79.884,35.06,-79.884,34.976,-79.915,34.846,-79.891,34.738,-79.812,34.652,-79.781,34.58,-79.8,34.523,-79.791,34.505,-79.754,34.503,-79.722,34.465,-79.676,34.359,-79.634,34.3,-79.596,34.288,-79.551,34.195,-79.471,33.934,-79.334,33.786,-79.246,33.715,-79.162,33.673,-79.115,33.629,-79.106,33.583,-79.123,33.529,-79.19,33.437,-79.226,33.405]]},{"n":"Guaporé","z":3,"b":[-65.447,-11.913,-65.031,-10.392],"l":[[-65.031,-11.913,-65.037,-11.829,-65.09,-11.741,-65.115,-11.735,-65.143,-11.752,-65.163,-11.765,-65.186,-11.75,-65.19,-11.71,-65.175,-11.647,-65.206,-11.581,-65.282,-11.511,-65.322,-11.439,-65.325,-11.365,-65.342,-11.315,-65.373,-11.29,-65.39,-11.246,-65.394,-11.184,-65.372,-11.11,-65.324,-11.025,-65.334,-10.893,-65.402,-10.715,-65.44,-10.586,-65.447,-10.507,-65.437,-10.449,-65.395,-10.392]]},{"n":"Göta älv","z":5,"b":[11.729,57.764,12.335,58.374],"l":[[12.335,58.374,12.171,58.24,12.112,58.14,12.105,58.031,12.029,57.928,11.81,57.781,11.729,57.764]]},{"n":"Hailar","z":3,"b":[117.873,49.192,122.287,49.849],"l":[[117.873,49.513,117.912,49.429,117.969,49.374,118.057,49.319,118.175,49.332,118.323,49.412,118.44,49.437,118.524,49.405,118.583,49.347,118.617,49.262,118.747,49.212,119.086,49.192,119.232,49.199,119.337,49.226,119.45,49.275,119.71,49.275,120.118,49.225,120.518,49.256,120.911,49.368,121.216,49.473,121.433,49.572,121.626,49.635,121.878,49.678,122.287,49.849]]},{"n":"Han","z":5,"b":[106.398,30.407,114.288,33.234],"l":[[111.478,32.568,111.602,32.468,111.642,32.407,111.639,32.354,111.685,32.265,111.78,32.141,111.919,32.065,112.101,32.036,112.189,31.966,112.181,31.853,112.225,31.771,112.322,31.719,112.39,31.657,112.432,31.586,112.44,31.496,112.417,31.388,112.434,31.308,112.521,31.23,112.556,31.219,112.572,31.183,112.58,31.118,112.569,31.096,112.539,31.077,112.523,31.039,112.556,30.939,112.599,30.875,112.634,30.848,112.632,30.815,112.622,30.787,112.605,30.713,112.627,30.67,112.689,30.658,112.713,30.627,112.707,30.601,112.716,30.575,112.754,30.572,112.797,30.564,112.849,30.519,112.886,30.515,112.973,30.531,113.073,30.475,113.21,30.43,113.381,30.407,113.512,30.443,113.602,30.538,113.672,30.597,113.769,30.626,113.801,30.622,113.838,30.63,113.86,30.644,113.9,30.637,113.926,30.644,113.939,30.66,113.964,30.657,114.009,30.615,114.096,30.586,114.288,30.572],[106.398,33.068,106.676,33.142,106.832,33.134,106.931,33.102,107.026,33.042,107.189,33.063,107.42,33.165,107.596,33.2,107.718,33.171,107.807,33.178,107.861,33.222,107.905,33.234,107.939,33.216,107.956,33.175,107.956,33.112,108.036,33.062,108.195,33.024,108.266,32.964,108.247,32.881,108.288,32.811,108.388,32.753,108.411,32.734,108.411,32.709,108.425,32.665,108.512,32.55,108.651,32.506,108.718,32.528,108.759,32.599,108.782,32.614,108.798,32.617,108.819,32.609,108.836,32.603,108.855,32.608,108.922,32.636,109.063,32.735,109.175,32.775,109.257,32.757,109.325,32.778,109.377,32.838,109.487,32.886,109.716,32.934,109.739,32.937,109.767,32.934,110.14,32.825,110.305,32.807,110.367,32.841,110.475,32.843,110.628,32.814,110.715,32.815,110.736,32.846,110.808,32.848,110.931,32.821,110.967,32.788,110.969,32.771,111,32.741]]},{"n":"Amur","z":3,"b":[121.405,50.209,127.59,53.556],"l":[[127.59,50.209,127.395,50.299,127.337,50.35,127.351,50.394,127.341,50.428,127.306,50.454,127.308,50.494,127.347,50.55,127.347,50.621,127.307,50.708,127.198,50.829,127.02,50.986,126.925,51.1,126.912,51.172,126.888,51.23,126.854,51.261,126.834,51.315,126.848,51.374,126.827,51.412,126.802,51.448,126.805,51.506,126.775,51.545,126.709,51.566,126.689,51.61,126.701,51.703,126.654,51.781,126.511,51.926,126.468,52.031,126.456,52.126,126.395,52.173,126.391,52.214,126.384,52.286,126.346,52.306,126.324,52.332,126.342,52.362,126.313,52.4,126.238,52.445,126.203,52.484,126.194,52.519,126.157,52.547,126.046,52.573,126.016,52.61,126.023,52.643,126.047,52.673,126.06,52.692,126.056,52.716,126.048,52.739,126.004,52.768,125.942,52.801,125.872,52.872,125.783,52.891,125.728,52.891,125.681,52.931,125.695,52.956,125.692,53.004,125.649,53.042,125.596,53.057,125.546,53.048,125.422,53.084,125.226,53.166,125.075,53.204,124.971,53.197,124.907,53.173,124.882,53.13,124.812,53.134,124.64,53.211,124.466,53.23,124.369,53.271,124.291,53.341,124.22,53.37,124.154,53.359,123.995,53.406,123.741,53.511,123.608,53.547,123.56,53.527,123.535,53.526,123.489,53.529,123.424,53.531,123.31,53.556,123.154,53.545,122.958,53.498,122.745,53.469,122.516,53.457,122.38,53.463,122.338,53.485,122.089,53.451,121.405,53.317]]},{"n":"Helmand","z":5,"b":[61.604,30.192,68.489,34.572],"l":[[61.604,31.204,61.754,31.014,61.817,30.802,61.842,30.506,62.031,30.31,62.383,30.216,62.685,30.192,62.937,30.238,63.194,30.342,63.457,30.503,63.663,30.57,63.812,30.543,63.908,30.538,63.949,30.557,64.002,30.617,64.065,30.719,64.12,30.872,64.166,31.075,64.236,31.239,64.328,31.366,64.371,31.488,64.366,31.578,64.391,31.657,64.455,31.728,64.64,31.847,64.801,31.999,64.932,32.181,65.064,32.289,65.195,32.322,65.332,32.486,65.475,32.782,65.62,32.964,65.842,33.065,65.937,33.121,66.017,33.202,66.114,33.326,66.193,33.393,66.255,33.402,66.309,33.441,66.356,33.509,66.452,33.585,66.599,33.668,66.728,33.716,66.84,33.731,66.959,33.792,67.086,33.898,67.172,33.952,67.216,33.953,67.233,33.989,67.222,34.057,67.229,34.109,67.266,34.157,67.498,34.306,67.734,34.395,68.212,34.513,68.307,34.493,68.376,34.507,68.489,34.572]]},{"n":"Holston","z":4.7,"b":[-83.974,35.87,-81.42,37.009],"l":[[-81.42,37.009,-81.695,36.932,-81.875,36.853,-82.136,36.762,-82.589,36.61,-82.665,36.522,-82.936,36.401,-83.026,36.37,-83.193,36.324,-83.387,36.256,-83.454,36.2,-83.747,36.076,-83.9,35.978,-83.974,35.915,-83.969,35.87]]},{"n":"Red","z":5,"b":[100.183,20.289,106.518,25.443],"l":[[106.518,20.289,106.387,20.348,106.329,20.356,106.288,20.34,106.27,20.347,106.276,20.377,106.264,20.398,106.234,20.408,106.196,20.466,106.149,20.572,106.109,20.623,106.076,20.619,106.023,20.665,105.951,20.761,105.909,20.861,105.897,20.964,105.836,21.051,105.725,21.122,105.626,21.162,105.538,21.17,105.477,21.203,105.426,21.292,105.325,21.273,105.275,21.301,105.241,21.37,105.216,21.389,105.197,21.358,105.118,21.43,104.977,21.604,104.692,21.875,104.052,22.424,103.479,22.902,103.161,23.121,102.906,23.24,102.614,23.353,102.119,23.514,101.892,23.699,101.745,23.857,101.601,24.049,101.507,24.208,101.463,24.335,101.41,24.434,101.349,24.505,101.292,24.54,101.238,24.538,101.118,24.655,100.931,24.891,100.812,25.003,100.76,24.993,100.642,25.021,100.459,25.088,100.351,25.15,100.303,25.235,100.183,25.443]]},{"n":"Hongshui","z":4,"b":[106.944,23.662,109.529,25.225],"l":[[106.944,25.225,106.963,25.223,106.974,25.2,106.982,25.134,107.019,25.061,107.063,25.029,107.156,25.009,107.277,24.875,107.315,24.796,107.312,24.729,107.362,24.603,107.467,24.417,107.542,24.318,107.586,24.308,107.608,24.281,107.608,24.236,107.578,24.189,107.516,24.139,107.494,24.087,107.512,24.031,107.547,23.986,107.601,23.954,107.643,23.945,107.674,23.961,107.733,23.905,107.821,23.776,107.902,23.722,107.975,23.744,108.039,23.782,108.094,23.836,108.131,23.857,108.152,23.843,108.223,23.889,108.343,23.995,108.453,24.035,108.552,24.007,108.631,23.96,108.691,23.891,108.758,23.85,108.833,23.836,108.865,23.806,108.855,23.758,108.873,23.71,108.919,23.662,108.999,23.663,109.112,23.712,109.192,23.723,109.238,23.695,109.281,23.688,109.322,23.7,109.369,23.746,109.423,23.825,109.469,23.847,109.529,23.796]]},{"n":"Huang","z":3,"b":[96.164,33.356,114.831,40.875],"l":[[97.288,34.835,97.547,34.876],[97.763,35.088,97.934,35.082,98.028,35.034,98.101,34.94,98.197,34.874,98.318,34.835,98.382,34.798,98.396,34.747,98.4,34.674,98.384,34.631,98.348,34.593,98.376,34.531,98.469,34.445,98.563,34.39,98.661,34.367,98.713,34.38,98.72,34.43,98.752,34.461,98.809,34.473,98.892,34.462,99.001,34.428,99.058,34.301,99.061,34.081,99.123,33.915,99.245,33.803,99.324,33.748,99.359,33.751,99.381,33.771,99.391,33.809,99.439,33.812,99.526,33.781,99.62,33.775,99.721,33.791,99.868,33.744,100.062,33.631,100.191,33.604,100.256,33.662,100.278,33.709,100.33,33.757,100.434,33.799,100.587,33.883,100.689,33.926,100.74,33.928,100.825,33.878,100.942,33.777,101.009,33.764,101.026,33.839,101.123,33.827,101.301,33.727,101.481,33.678,101.663,33.679,101.809,33.611,101.919,33.473,102.005,33.388,102.066,33.356,102.193,33.371,102.385,33.434,102.482,33.484,102.482,33.521,102.45,33.557,102.387,33.592,102.353,33.638,102.33,33.725,102.316,33.745,102.285,33.8,102.206,33.897,102.103,33.959,101.976,33.986,101.838,34.047,101.687,34.143,101.517,34.219,101.328,34.273,101.21,34.35,101.163,34.45,101.087,34.513,100.982,34.539,100.912,34.593,100.878,34.677,100.815,34.708,100.721,34.686,100.63,34.708,100.54,34.773,100.455,34.813,100.376,34.827,100.307,34.936,100.22,35.241,100.259,35.282,100.236,35.338,100.166,35.424,100.172,35.543,100.255,35.695,100.334,35.774,100.409,35.777,100.546,35.869,100.744,36.05,100.898,36.142,101.009,36.145,101.071,36.135,101.085,36.112,101.176,36.089,101.343,36.065,101.495,36.086,101.63,36.151,101.763,36.152,101.893,36.086,101.993,35.997,102.064,35.886,102.185,35.845,102.356,35.873,102.61,35.86,102.948,35.806,103.177,35.812,103.299,35.878,103.317,35.931,103.232,35.97,103.256,36.04,103.388,36.139,103.56,36.158,103.771,36.096,103.916,36.068,103.995,36.075,104.031,36.104,104.023,36.156,104.072,36.229,104.179,36.322,104.352,36.428,104.591,36.547,104.713,36.628,104.72,36.671,104.69,36.728,104.625,36.798,104.528,36.85,104.4,36.884,104.327,36.946,104.308,37.035,104.314,37.093,104.353,37.174,104.446,37.197,104.52,37.241,104.576,37.307,104.692,37.359,104.957,37.415,104.998,37.461,105.152,37.483,105.433,37.495,105.649,37.522,105.8,37.565,105.905,37.661,105.964,37.81,106.037,37.916,106.125,37.977,106.196,38.064,106.251,38.177,106.338,38.282,106.457,38.378,106.531,38.498,106.559,38.64,106.64,38.78,106.774,38.918,106.851,39.017,106.871,39.075,106.849,39.183,106.783,39.34,106.759,39.483,106.776,39.611,106.768,39.76,106.718,40.016,107.043,40.286,107.149,40.396,107.144,40.434,107.174,40.5,107.239,40.592,107.38,40.671,107.598,40.738,107.72,40.793,107.745,40.837,107.862,40.865,108.07,40.875,108.19,40.863,108.224,40.83,108.29,40.815,108.389,40.816,108.476,40.778,108.553,40.7,108.651,40.632,108.77,40.573,108.884,40.547,108.992,40.554,109.134,40.535,109.309,40.49,109.45,40.489,109.555,40.53,109.635,40.532,109.687,40.494,109.836,40.491,110.08,40.524,110.303,40.484,110.505,40.373,110.675,40.302,110.811,40.271,110.919,40.266,110.996,40.286,111.125,40.235,111.307,40.11,111.407,40.013,111.426,39.942,111.416,39.867,111.378,39.789,111.385,39.718,111.437,39.653,111.433,39.576,111.373,39.488,111.284,39.43,111.212,39.414,111.168,39.4,111.156,39.37,111.155,39.347,111.192,39.297,111.199,39.24,111.145,39.125,111.081,39.044,111.009,38.997,110.98,38.956,110.996,38.92,110.97,38.832,110.904,38.692,110.878,38.6,110.891,38.557,110.798,38.443,110.599,38.258,110.503,38.113,110.511,38.008,110.585,37.881,110.725,37.732,110.782,37.614,110.758,37.525,110.719,37.465,110.664,37.434,110.644,37.384,110.658,37.316,110.61,37.217,110.5,37.087,110.427,37.021,110.391,37.019,110.384,37.005,110.407,36.98,110.408,36.919,110.387,36.823,110.393,36.763,110.425,36.739,110.428,36.717,110.403,36.698,110.417,36.648,110.471,36.566,110.486,36.429,110.46,36.237,110.49,36.006,110.576,35.733,110.579,35.534,110.459,35.343,110.366,35.234,110.316,35.135,110.278,35.008,110.262,34.868,110.27,34.637,110.465,34.597,110.619,34.604,110.795,34.645,110.89,34.683,110.97,34.724,111.027,34.746,111.101,34.795,111.243,34.83,111.453,34.852,111.589,34.899,111.651,34.972,111.736,35.03,111.844,35.074,112.118,35.022,112.559,34.872,112.857,34.809,113.012,34.831,113.156,34.875,113.288,34.94,113.399,34.974,113.489,34.978,113.617,34.954,113.784,34.902,113.9,34.895,113.965,34.931,114.156,34.936,114.473,34.91,114.681,34.928,114.798,34.978,114.831,35.02],[96.164,35.13,96.748,35.053,97.072,34.931]]},{"n":"Yellow","z":3,"b":[114.838,35.02,119.036,37.824],"l":[[114.838,35.02,114.862,35.061,114.866,35.145,114.905,35.229,115.014,35.33,115.136,35.406,115.272,35.459,115.34,35.503,115.341,35.538,115.382,35.597,115.463,35.681,115.596,35.758,115.781,35.83,115.871,35.88,115.866,35.91,115.883,35.939,115.924,35.966,115.966,35.976,116.036,36.071,116.078,36.11,116.116,36.143,116.227,36.201,116.451,36.339,116.581,36.44,116.593,36.49,116.644,36.561,116.735,36.656,116.862,36.726,117.024,36.773,117.151,36.862,117.24,36.994,117.352,37.082,117.485,37.127,117.571,37.175,117.608,37.228,117.693,37.256,117.825,37.26,117.97,37.29,118.128,37.347,118.238,37.431,118.298,37.541,118.385,37.604,118.498,37.619,118.645,37.678,118.826,37.78,118.946,37.824,119.036,37.809]]},{"n":"Hudson","z":5,"b":[-74.178,41.25,-73.617,44.1],"l":[[-74.074,44.1,-74.064,44.016,-74.08,43.982,-74.119,43.972,-74.152,43.942,-74.176,43.894,-74.178,43.86,-74.146,43.832,-73.971,43.735,-73.888,43.661,-73.839,43.576,-73.83,43.506,-73.862,43.451,-73.867,43.383,-73.846,43.302,-73.781,43.274,-73.671,43.297,-73.617,43.232,-73.622,43,-73.728,42.714,-73.767,42.544,-73.773,42.394,-73.818,42.265,-73.901,42.159,-73.944,42.02,-73.948,41.765,-73.984,41.574,-73.984,41.46,-73.96,41.331,-73.969,41.281,-73.97,41.25]]},{"n":"Ideriyn","z":2.1,"b":[97.413,47.999,100.7,49.263],"l":[[98.007,47.999,97.617,48.075,97.452,48.141,97.413,48.202,97.444,48.279,97.454,48.357,97.443,48.435,97.529,48.541,97.711,48.673,98.05,48.736,98.547,48.73,98.923,48.697,99.179,48.638,99.588,48.725,100.152,48.958,100.473,49.076,100.551,49.077,100.613,49.098,100.659,49.139,100.686,49.185,100.7,49.263]]},{"n":"Indaiá","z":4,"b":[-46.499,-20.352,-45.089,-19.051],"l":[[-46.499,-20.321,-46.322,-20.352,-46.097,-20.348,-46.002,-20.337,-45.961,-20.318,-45.947,-20.296,-45.961,-20.273,-45.943,-20.231,-45.896,-20.173,-45.819,-20.14,-45.714,-20.133,-45.64,-20.068,-45.598,-19.945,-45.554,-19.87,-45.506,-19.841,-45.492,-19.8,-45.513,-19.746,-45.496,-19.688,-45.443,-19.626,-45.395,-19.524,-45.352,-19.383,-45.281,-19.284,-45.182,-19.227,-45.121,-19.161,-45.089,-19.051]]},{"n":"Indigirka","z":4.7,"b":[141.885,62.507,150.098,71.227],"l":[[144.713,62.766,144.441,63.009,144.316,63.091,144.249,63.092,143.974,63.178,143.492,63.351,143.055,63.47,142.663,63.536,142.446,63.599,142.402,63.658,142.3,63.729,142.058,63.851,141.915,63.954,141.885,64.037,141.921,64.136,142.012,64.215,142.159,64.276,142.366,64.293,142.634,64.269,142.789,64.303,142.831,64.396,142.84,64.459,142.862,64.501,142.917,64.532,143.151,64.579,143.267,64.633,143.265,64.694,143.295,64.748,143.356,64.793,143.338,64.871,143.243,64.983,143.161,65.055,143.092,65.088,143.085,65.134,143.14,65.195,143.157,65.24,143.161,65.277,143.169,65.312,143.218,65.344,143.23,65.371,143.208,65.389,143.134,65.404,142.814,65.424,142.637,65.461,142.603,65.515,142.618,65.592,142.68,65.692,142.811,65.789,143.011,65.884,143.147,66.017,143.219,66.189,143.223,66.309,143.158,66.377,143.106,66.466,143.068,66.574,142.987,66.65,142.865,66.693,142.754,66.813,142.654,67.01,142.615,67.16,142.635,67.265,142.599,67.314,142.574,67.341,142.578,67.363,142.618,67.395,142.636,67.425,142.655,67.452,142.845,67.503,143.337,67.6,143.647,67.637,143.776,67.611,144.015,67.604,144.362,67.614,144.574,67.626,144.65,67.639,144.695,67.661,144.708,67.692,144.671,67.714,144.582,67.726,144.527,67.75,144.507,67.785,144.594,67.814,144.885,67.847,145.025,67.871,145.059,67.908,145.067,67.954,145.093,67.98,145.167,67.985,145.238,67.995,145.282,68.023,145.279,68.047,145.278,68.082,145.313,68.111,145.346,68.142,145.358,68.179,145.423,68.2,145.539,68.192,145.644,68.218,145.653,68.267,145.715,68.293,145.809,68.288,145.856,68.304,145.869,68.351,145.913,68.383,145.901,68.431,145.969,68.498,146.071,68.547,146.206,68.578,146.358,68.578,146.528,68.547,146.646,68.545,146.711,68.571,146.809,68.581,146.94,68.573,147.022,68.624,147.056,68.735,147.144,68.801,147.286,68.821,147.346,68.855,147.325,68.903,147.327,68.93,147.354,68.939,147.422,68.934,147.483,68.942,147.492,68.977,147.463,69.003,147.444,69.072,147.488,69.147,147.471,69.221,147.392,69.295,147.434,69.352,147.597,69.392,147.638,69.44,147.598,69.508,147.575,69.557,147.575,69.586,147.568,69.599,147.538,69.619,147.445,69.642,147.429,69.656,147.423,69.668,147.435,69.677,147.463,69.695,147.474,69.73,147.451,69.773,147.466,69.847,147.519,69.953,147.519,70.004,147.496,70.023,147.453,70.039,147.39,70.067,147.355,70.111,147.352,70.144,147.385,70.167,147.393,70.195,147.365,70.225,147.379,70.272,147.489,70.336,147.538,70.38,147.525,70.402,147.538,70.425,147.576,70.449,147.657,70.481,147.783,70.522,147.853,70.56,147.867,70.595,148.08,70.635,148.492,70.679,148.709,70.73,148.732,70.787,148.941,70.837,149.333,70.881,149.561,70.925,149.622,70.97,149.646,71.002,149.662,71.03,149.691,71.046,149.848,71.083,149.913,71.111,149.893,71.131,149.892,71.152,149.91,71.174,149.984,71.209,150.098,71.227],[144.712,62.768,145.009,62.507]]},{"n":"Indragi","z":5,"b":[100.55,-0.675,103.405,-0.331],"l":[[100.55,-0.566,100.66,-0.572,100.724,-0.602,100.779,-0.657,100.84,-0.675,100.937,-0.647,101.007,-0.659,101.043,-0.638,101.07,-0.59,101.115,-0.586,101.179,-0.624,101.259,-0.636,101.356,-0.622,101.424,-0.627,101.483,-0.632,101.534,-0.611,101.631,-0.51,101.77,-0.486,101.953,-0.539,102.097,-0.535,102.256,-0.442,102.372,-0.356,102.521,-0.378,102.742,-0.481,102.843,-0.506,102.937,-0.479,103.105,-0.374,103.229,-0.331,103.346,-0.36,103.405,-0.362]]},{"n":"Indus","z":3,"b":[67.454,23.848,79.735,35.836],"l":[[67.86,23.903,67.925,24.001],[67.646,23.92,67.711,23.957,67.714,24.001,67.713,24.066],[67.925,24.001,68,24.007,68.017,23.991,68.025,23.934,68.042,23.892,68.037,23.848],[67.925,24.001,67.924,24.056,67.801,24.059,67.76,24.053,67.713,24.066],[67.713,24.066,67.661,24.105],[79.735,32.444,79.43,32.745],[79.437,32.738,79.341,32.881,79.22,32.996,79.044,33.133,78.907,33.189,78.809,33.165,78.64,33.208,78.245,33.398,78.183,33.445,78.152,33.493,78.15,33.542,78.077,33.62,77.931,33.727,77.792,33.857,77.66,34.011,77.53,34.106,77.339,34.16,76.967,34.288,76.782,34.386,76.661,34.498,76.529,34.583,76.388,34.639,76.285,34.708,76.222,34.789,76.196,34.861,76.209,34.924,76.142,35.008,75.995,35.113,75.902,35.21,75.863,35.297,75.791,35.335,75.688,35.321,75.568,35.357,75.431,35.443,75.353,35.511,75.333,35.561,75.264,35.59,75.145,35.598,75.001,35.632,74.83,35.694,74.74,35.753,74.73,35.812,74.701,35.836,74.654,35.826,74.631,35.748,74.632,35.601,74.575,35.5,74.461,35.445,74.342,35.415,74.22,35.408,74.058,35.436,73.857,35.497,73.645,35.531,73.423,35.538,73.302,35.471,73.28,35.33,73.175,35.204,72.986,35.093,72.885,35.004,72.872,34.939,72.895,34.886,72.955,34.845,72.945,34.809,72.864,34.777,72.814,34.726,72.782,34.623],[72.707,34.086,72.652,34.032,72.592,34.015,72.509,34.019,72.411,34,72.297,33.959,72.239,33.895,72.236,33.809,72.197,33.772,72.124,33.784,72.071,33.779,72.038,33.757,71.986,33.665,71.914,33.504,71.839,33.406,71.762,33.372,71.722,33.274,71.72,33.111,71.651,32.994,71.516,32.923,71.425,32.838,71.355,32.689,71.344,32.484,71.273,32.311,71.07,31.999,70.861,31.569,70.801,31.3,70.829,30.92,70.77,30.811,70.775,30.716,70.824,30.6,70.83,30.485,70.793,30.372,70.792,30.212,70.827,30.004,70.792,29.825,70.688,29.676,70.657,29.55,70.699,29.45,70.702,29.353,70.666,29.261,70.651,29.176,70.657,29.098,70.612,29.023,70.515,28.952,70.443,28.916,70.397,28.915,70.287,28.835,70.115,28.677,69.962,28.576,69.829,28.531,69.715,28.444,69.621,28.315,69.535,28.246,69.458,28.237,69.409,28.212,69.389,28.172,69.356,28.157,69.311,28.165,69.249,28.141,69.171,28.085,69.107,28.058,69.059,28.061,69.026,28.04,69.008,27.995,68.98,27.975,68.942,27.981,68.929,27.955,68.943,27.897,68.928,27.853,68.883,27.823,68.884,27.792,68.932,27.761,68.95,27.737,68.938,27.719,68.826,27.678,68.762,27.698,68.705,27.759,68.669,27.771,68.654,27.731,68.573,27.688,68.426,27.641,68.337,27.603,68.307,27.572,68.3,27.511,68.318,27.42,68.297,27.371,68.239,27.366,68.194,27.342,68.162,27.3,68.16,27.263,68.188,27.231,68.175,27.208,68.121,27.193,68.099,27.163,68.109,27.117,68.063,27.062,67.963,26.998,67.911,26.91,67.905,26.742,67.891,26.487,67.9,26.369,67.926,26.302,67.982,26.243,68.068,26.192,68.117,26.14,68.13,26.088,68.202,25.999,68.332,25.873,68.379,25.795,68.341,25.765,68.348,25.728,68.4,25.684,68.398,25.621,68.342,25.539,68.323,25.444,68.34,25.334,68.337,25.262,68.313,25.226,68.319,25.192,68.353,25.158,68.352,25.139,68.314,25.132,68.286,25.089,68.269,25.009,68.245,24.979,68.214,24.997,68.183,24.973,68.155,24.907,68.116,24.873,68.068,24.87,68.025,24.76,67.986,24.544,67.936,24.43,67.874,24.418,67.85,24.396,67.863,24.363,67.849,24.344,67.806,24.339,67.796,24.318,67.818,24.282,67.787,24.239,67.702,24.191,67.659,24.146,67.659,24.104,67.627,24.078,67.532,24.061,67.454,24.04]]},{"n":"Irrawaddy Delta","z":2.1,"b":[94.656,15.839,96.189,17.908],"l":[[94.703,16.512,94.728,16.673,94.744,16.783,94.742,16.908,94.795,17.035,94.903,17.164,94.954,17.257,94.948,17.315,94.977,17.352,95.042,17.371,95.12,17.472,95.21,17.656,95.304,17.766,95.455,17.856,95.482,17.88,95.52,17.908,95.575,17.904,95.633,17.81,95.694,17.625,95.783,17.438,95.899,17.251,95.956,17.13,95.952,17.074,96.042,16.969,96.09,16.847,96.189,16.768],[95.481,16.814,95.323,16.328,95.237,16.133,95.133,16.025,95.099,15.958,95.078,15.839],[95.6,16.902,95.649,16.768,95.719,16.709,95.829,16.653,95.872,16.593,95.847,16.529,95.845,16.478,95.867,16.44,95.91,16.412,95.994,16.39,96.043,16.34],[95.472,17.744,95.406,17.718,95.356,17.666,95.296,17.554,95.291,17.502,95.323,17.468,95.34,17.431,95.335,17.379,95.317,17.348,95.276,17.323,95.263,17.302,95.253,17.224,95.248,17.178,95.213,17.144,95.186,17.078,95.166,16.979,95.175,16.903,95.213,16.852,95.222,16.796,95.176,16.674,95.145,16.61,95.107,16.574,95.061,16.566,95.014,16.503,94.968,16.388,94.907,16.288,94.794,16.162,94.748,16.08,94.656,15.989]]},{"n":"Irtysh","z":2.1,"b":[68.215,53.986,74.994,61.124],"l":[[74.994,53.986,74.918,54.003,74.904,54.028,74.924,54.066,74.872,54.128,74.745,54.214,74.66,54.291,74.617,54.36,74.558,54.4,74.484,54.41,74.4,54.449,74.305,54.517,74.125,54.586,73.728,54.694],[73.722,54.694,73.539,54.805,73.434,54.845,73.347,54.85,73.318,54.888,73.346,54.96,73.308,55.009,73.203,55.036,73.157,55.097,73.17,55.193,73.128,55.242,73.029,55.245,72.993,55.266,73.02,55.305,73.163,55.389,73.424,55.518,73.662,55.618,73.878,55.689,74.081,55.729,74.272,55.739,74.469,55.833,74.672,56.011,74.709,56.058,74.709,56.092,74.676,56.126,74.67,56.148,74.69,56.162,74.699,56.203,74.681,56.264,74.698,56.286,74.729,56.289,74.737,56.317,74.721,56.337,74.74,56.371,74.749,56.396,74.735,56.419,74.706,56.427,74.678,56.478,74.68,56.563,74.656,56.607,74.606,56.609,74.59,56.627,74.609,56.663,74.595,56.693,74.577,56.717,74.585,56.734,74.597,56.757,74.552,56.845,74.511,56.893,74.456,56.923,74.387,56.936,74.366,56.95,74.334,56.988,74.181,57.017,74.117,57.053,74.142,57.096,74.119,57.133,74.049,57.163,73.965,57.174,73.869,57.166,73.77,57.179,73.669,57.213,73.635,57.239,73.633,57.258,73.599,57.272,73.504,57.265,73.441,57.278,73.436,57.306,73.371,57.327,73.247,57.342,73.18,57.362,73.171,57.388,73.113,57.408,73.007,57.422,72.949,57.441,72.903,57.458,72.867,57.447,72.78,57.442,72.625,57.472,72.529,57.502,72.492,57.52,72.45,57.534,72.413,57.529,72.389,57.536,72.334,57.565,72.231,57.579,71.951,57.581,71.855,57.608,71.82,57.63,71.751,57.643,71.606,57.635,71.527,57.645,71.515,57.674,71.473,57.699,71.4,57.719,71.312,57.721,71.209,57.704,71.108,57.71,71.007,57.74,70.964,57.764,70.92,57.765,70.873,57.761,70.785,57.777,70.683,57.822,70.615,57.821,70.597,57.823,70.569,57.851,70.526,57.859,70.468,57.847,70.425,57.861,70.398,57.902,70.396,57.928,70.373,57.957,70.294,57.977,70.225,57.977,70.176,57.954,70.137,57.959,70.107,57.99,70.053,58.009,69.974,58.015,69.877,58.001,69.763,57.967,69.649,57.96,69.536,57.98,69.48,58,69.427,58.017,69.316,57.995,69.239,58,69.197,58.032,69.14,58.032,69.07,58,69,58,68.931,58.032,68.871,58.037,68.818,58.014,68.73,58.037,68.605,58.108,68.464,58.149,68.226,58.168],[68.861,61.124,69.011,61.019,69.161,60.976,69.362,60.959,69.516,60.885,69.623,60.755,69.724,60.683,69.802,60.668,69.83,60.65,69.836,60.621,69.816,60.604,69.782,60.567,69.769,60.512,69.801,60.463,69.804,60.419,69.778,60.38,69.785,60.354,69.825,60.342,69.854,60.311,69.874,60.259,69.862,60.239,69.821,60.237,69.77,60.207,69.729,60.17,69.721,60.126,69.738,60.095,69.781,60.078,69.854,60.058,69.888,60.005,69.866,59.943,69.865,59.901,69.888,59.866,69.871,59.835,69.82,59.839,69.777,59.817,69.757,59.766,69.699,59.744,69.601,59.75,69.498,59.727,69.334,59.65,69.284,59.577,69.223,59.555,69.133,59.561,69.075,59.536,69.047,59.481,68.997,59.452,68.925,59.451,68.911,59.43,68.929,59.402,68.905,59.354,68.831,59.292,68.799,59.24,68.81,59.198,68.843,59.173,68.882,59.161,68.882,59.138,68.862,59.12,68.854,59.084,68.873,59.057,68.856,59.021,68.812,59.013,68.801,59.002,68.821,58.967,68.812,58.935,68.762,58.926,68.749,58.9,68.75,58.877,68.737,58.859,68.697,58.853,68.689,58.827,68.705,58.788,68.671,58.764,68.627,58.77,68.569,58.753,68.556,58.679,68.564,58.607,68.566,58.562,68.549,58.521,68.469,58.512,68.399,58.5,68.355,58.47,68.331,58.431,68.328,58.383,68.283,58.355,68.24,58.342,68.219,58.328,68.215,58.29,68.233,58.246,68.222,58.166]]},{"n":"Ishikari","z":5,"b":[141.358,43.13,143.066,43.895],"l":[[143.066,43.7,142.885,43.81,142.722,43.878,142.632,43.895,142.56,43.883,142.481,43.849,142.396,43.792,142.254,43.738,141.943,43.648,141.909,43.623,141.884,43.564,141.861,43.428,141.71,43.329,141.644,43.254,141.613,43.169,141.567,43.13,141.508,43.135,141.448,43.163,141.358,43.238]]},{"n":"Ismailiya Canal","z":5,"b":[31.238,30.121,32.303,30.593],"l":[[31.238,30.121,31.282,30.129,31.324,30.18,31.378,30.278,31.483,30.379,31.64,30.484,31.865,30.551,32.303,30.593]]},{"n":"Japurá","z":4.7,"b":[-69.462,-3.222,-64.774,-1.231],"l":[[-69.462,-1.231,-69.452,-1.299,-69.447,-1.448,-69.339,-1.605,-69.128,-1.754,-68.914,-1.818,-68.699,-1.797,-68.458,-1.748,-68.191,-1.672,-67.964,-1.639,-67.776,-1.651,-67.629,-1.685,-67.525,-1.742,-67.361,-1.798,-67.138,-1.854,-66.805,-1.823,-66.363,-1.706,-65.981,-1.716,-65.658,-1.853,-65.445,-1.905,-65.344,-1.869,-65.268,-1.904,-65.193,-2.063,-65.227,-2.138,-65.18,-2.266,-65.066,-2.472,-65.009,-2.606,-65.011,-2.667,-64.979,-2.709,-64.914,-2.733,-64.889,-2.777,-64.903,-2.842,-64.875,-2.927,-64.802,-3.031,-64.774,-3.109,-64.79,-3.222]]},{"n":"Jequitinhonha","z":5,"b":[-43.665,-18.294,-38.885,-15.821],"l":[[-43.409,-18.294,-43.532,-18.104,-43.559,-18.012,-43.515,-17.923,-43.476,-17.839,-43.494,-17.818,-43.558,-17.832,-43.611,-17.786,-43.655,-17.68,-43.665,-17.608,-43.642,-17.57,-43.57,-17.505,-43.488,-17.496,-43.365,-17.507,-43.299,-17.476,-43.233,-17.418,-43.213,-17.339,-43.181,-17.304,-43.118,-17.279,-43.048,-17.212,-42.972,-17.105,-42.905,-17.046,-42.846,-17.035,-42.777,-16.96,-42.698,-16.819,-42.555,-16.716,-42.347,-16.651,-42.178,-16.651,-42.046,-16.717,-41.919,-16.703,-41.796,-16.61,-41.687,-16.562,-41.548,-16.56,-41.332,-16.524,-41.236,-16.486,-41.187,-16.435,-41.124,-16.416,-41.044,-16.43,-40.908,-16.355,-40.715,-16.19,-40.52,-16.116,-40.324,-16.134,-40.17,-16.125,-40.059,-16.092,-39.99,-16.056,-39.963,-16.019,-39.84,-15.986,-39.513,-15.942,-39.402,-16.025,-39.323,-16.04,-39.237,-16.015,-39.164,-15.955,-39.104,-15.86,-39.021,-15.821,-38.949,-15.825,-38.885,-15.842]]},{"n":"Yangtze","z":2,"b":[98.542,25.981,103.859,31.69],"l":[[99.399,28.12,99.431,27.842,99.494,27.642,99.6,27.426,99.727,27.246,99.874,27.101,99.954,26.994,99.967,26.922,99.996,26.904,100.041,26.94,100.063,27.012,100.063,27.12,100.109,27.221,100.199,27.314,100.232,27.399,100.206,27.474,100.219,27.554,100.27,27.64,100.296,27.721,100.298,27.795,100.33,27.834,100.39,27.837,100.427,27.754,100.442,27.584,100.472,27.484,100.519,27.454,100.545,27.393,100.55,27.301,100.523,27.238,100.463,27.202,100.45,27.169,100.481,27.139,100.486,27.05,100.463,26.901,100.437,26.817,100.408,26.797,100.411,26.691,100.444,26.499,100.449,26.378,100.425,26.326,100.428,26.279,100.459,26.236,100.582,26.203,100.798,26.178,100.961,26.211,101.071,26.301,101.2,26.357,101.349,26.381,101.447,26.442,101.495,26.541,101.59,26.593,101.734,26.599,101.841,26.554,101.912,26.46,101.94,26.363,101.925,26.261,101.899,26.193,101.863,26.157,101.847,26.098,101.852,26.016,101.912,25.981,102.027,25.994,102.126,26.057,102.21,26.171,102.331,26.266,102.489,26.341,102.583,26.37,102.612,26.353,102.622,26.324,102.614,26.281,102.635,26.252,102.686,26.237,102.753,26.263,102.836,26.331,102.905,26.365,102.961,26.365,102.99,26.395,102.994,26.455,103.011,26.498,103.042,26.523,103.018,26.633,102.94,26.828,102.897,27.013,102.89,27.189,102.903,27.313,102.938,27.385,102.969,27.412,102.998,27.395,103.046,27.398,103.147,27.434,103.2,27.534,103.237,27.572,103.276,27.583,103.293,27.601,103.287,27.627,103.326,27.67,103.41,27.73,103.479,27.814,103.535,27.922,103.532,27.992,103.471,28.026,103.444,28.065,103.451,28.111,103.484,28.161,103.589,28.25,103.626,28.264,103.663,28.257,103.702,28.229,103.762,28.243,103.841,28.298,103.859,28.377,103.816,28.48,103.809,28.565,103.851,28.668],[98.542,31.69,98.799,31.438,98.855,31.323,98.797,31.261,98.738,31.252,98.679,31.295,98.638,31.28,98.615,31.206,98.653,31.125,98.753,31.036,98.799,30.963,98.791,30.908,98.831,30.842,98.917,30.766,98.947,30.711,98.919,30.68,98.943,30.477,99.018,30.102,99.039,29.849,99.004,29.717,99.014,29.551,99.096,29.25,99.146,28.641,99.221,28.36,99.399,28.126]]},{"n":"Jordan","z":5,"b":[35.401,31.23,35.864,33.596],"l":[[35.424,31.325,35.401,31.23],[35.577,32.72,35.551,32.396,35.572,32.238,35.535,32.103,35.532,31.985,35.559,31.766],[35.864,33.596,35.696,33.431,35.619,33.244,35.595,33.111,35.599,32.9]]},{"n":"Jubba","z":5,"b":[41.966,-0.251,42.751,4.324],"l":[[41.966,4.324,42.029,4.291,42.056,4.255,42.067,4.206,42.121,4.173,42.219,4.155,42.342,4.078,42.49,3.943,42.542,3.795,42.499,3.635,42.497,3.522,42.536,3.456,42.533,3.398,42.488,3.35,42.473,3.28,42.487,3.188,42.516,3.13,42.56,3.107,42.575,3.07,42.562,3.02,42.502,2.942,42.393,2.838,42.346,2.749,42.36,2.677,42.345,2.614,42.302,2.559,42.295,2.503,42.322,2.444,42.316,2.388,42.274,2.333,42.229,2.319,42.179,2.348,42.145,2.345,42.126,2.312,42.144,2.28,42.2,2.252,42.205,2.221,42.157,2.188,42.177,2.135,42.265,2.061,42.297,1.993,42.275,1.929,42.306,1.835,42.389,1.711,42.426,1.617,42.416,1.553,42.424,1.513,42.425,1.462,42.462,1.375,42.561,1.219,42.605,1.07,42.595,0.929,42.626,0.808,42.698,0.705,42.738,0.581,42.751,0.362,42.73,0.135,42.685,0.015,42.608,-0.072,42.585,-0.148,42.634,-0.251]]},{"n":"Juruena","z":4.7,"b":[-59.077,-14.532,-58.622,-10.975],"l":[[-59.07,-14.532,-59.036,-14.373,-59.038,-14.249,-59.077,-14.038,-59.041,-13.979,-59.026,-13.821,-59.019,-13.544,-58.997,-13.367,-58.96,-13.289,-58.933,-12.998,-58.915,-12.494,-58.863,-12.181,-58.778,-12.058,-58.731,-11.92,-58.724,-11.769,-58.699,-11.657,-58.654,-11.586,-58.628,-11.503,-58.622,-11.407,-58.633,-11.364,-58.669,-11.33,-58.68,-11.234,-58.659,-10.975]]},{"n":"Kadéï","z":5,"b":[14.243,1.654,16.263,5.865],"l":[[14.617,5.865,14.617,5.496,14.493,5.287,14.463,5.182,14.485,5.111,14.437,5.03,14.319,4.941,14.254,4.797,14.243,4.598,14.297,4.43,14.417,4.292,14.498,4.23,14.539,4.229,14.57,4.211,14.574,4.189,14.623,4.172,14.68,4.205,14.754,4.228,14.827,4.23,14.873,4.203,14.894,4.147,14.957,4.118,15.064,4.116,15.147,4.091,15.236,4.019,15.368,4.055,15.451,4.046,15.531,4.005,15.578,3.931,15.593,3.825,15.651,3.756,15.755,3.724,15.881,3.65,16.032,3.535,16.108,3.4,16.11,3.246,16.158,3.103,16.252,2.971,16.263,2.924,16.245,2.884,16.174,2.824,16.118,2.716,16.095,2.599,16.107,2.473,16.136,2.364,16.183,2.27,16.177,2.205,16.116,2.168,16.08,2.107,16.07,2.022,16.088,1.918,16.135,1.796,16.136,1.715,16.098,1.654]]},{"n":"Kafue","z":5,"b":[25.947,-15.953,28.973,-11.626],"l":[[27.157,-11.626,27.143,-11.749,27.183,-11.852,27.317,-12.039,27.366,-12.154,27.445,-12.255,27.568,-12.379,27.655,-12.429,27.706,-12.405,27.821,-12.42,28.002,-12.474,28.136,-12.585,28.223,-12.755,28.24,-12.943,28.185,-13.152,28.125,-13.284,28.06,-13.339,27.964,-13.368,27.837,-13.372,27.76,-13.408,27.733,-13.476,27.688,-13.516,27.626,-13.526,27.603,-13.573,27.616,-13.659,27.561,-13.769,27.436,-13.903,27.386,-14.012,27.412,-14.095,27.41,-14.165,27.38,-14.22,27.317,-14.279,27.222,-14.343,27.12,-14.383,26.959,-14.409,26.656,-14.44,26.5,-14.517,26.39,-14.648,26.292,-14.691,26.206,-14.644,26.151,-14.647,26.127,-14.701,26.131,-14.736,26.163,-14.754,26.179,-14.789,26.178,-14.841,26.112,-14.881,25.983,-14.907,25.947,-14.947,26.006,-14.999,26.015,-15.054,25.977,-15.111,25.967,-15.288,25.987,-15.585,26.048,-15.755,26.15,-15.797,26.285,-15.776,26.453,-15.692,26.568,-15.664,26.63,-15.69,26.748,-15.667,26.921,-15.595,27.08,-15.606,27.227,-15.698,27.373,-15.721,27.517,-15.673,27.573,-15.673,27.625,-15.685,27.643,-15.683,27.653,-15.665,27.675,-15.648,27.71,-15.646,27.894,-15.66,28.037,-15.704,28.17,-15.78,28.275,-15.81,28.352,-15.793,28.482,-15.826,28.663,-15.91,28.802,-15.953,28.973,-15.95]]},{"n":"Kagera","z":2,"b":[29.593,-3.365,31.768,-0.936],"l":[[29.593,-2.984,29.827,-3.047,29.901,-3.091,29.895,-3.137,29.909,-3.16,29.943,-3.16,29.976,-3.184,30.021,-3.256,29.99,-3.315,29.99,-3.345,30.01,-3.365,30.105,-3.315,30.274,-3.196,30.391,-3.088,30.455,-2.991,30.47,-2.92,30.433,-2.875,30.424,-2.824,30.441,-2.769,30.53,-2.679,30.691,-2.554,30.774,-2.463,30.779,-2.404,30.798,-2.363,30.829,-2.339,30.855,-2.265,30.877,-2.143,30.865,-2.044,30.819,-1.967,30.807,-1.851,30.828,-1.694,30.813,-1.563,30.762,-1.459,30.711,-1.397,30.632,-1.367,30.508,-1.208,30.47,-1.131,30.477,-1.083,30.52,-1.063,30.599,-1.07,30.673,-1.051,30.742,-1.008,30.809,-0.995,30.874,-1.013,30.942,-1.063,31.012,-1.143,31.125,-1.2,31.28,-1.234,31.409,-1.239,31.512,-1.213,31.573,-1.157,31.594,-1.072,31.645,-1.006,31.768,-0.936]]},{"n":"Kamchatka","z":5,"b":[157.632,54.081,162.461,56.398],"l":[[157.632,54.29,157.694,54.131,157.748,54.081,157.817,54.086,157.884,54.115,157.982,54.194,158.124,54.364,158.205,54.434,158.271,54.461,158.314,54.505,158.333,54.565,158.456,54.642,158.684,54.737,158.857,54.866,158.975,55.031,159.101,55.145,159.233,55.209,159.31,55.261,159.329,55.301,159.329,55.328,159.309,55.341,159.315,55.352,159.336,55.378,159.343,55.391,159.36,55.41,159.405,55.433,159.488,55.451,159.608,55.462,159.665,55.483,159.659,55.511,159.677,55.542,159.722,55.575,159.74,55.642,159.733,55.741,159.697,55.815,159.632,55.863,159.633,55.875,159.641,55.887,159.652,55.905,159.647,55.941,159.693,55.986,159.79,56.042,159.868,56.116,159.926,56.208,160.022,56.271,160.156,56.304,160.247,56.346,160.295,56.398,160.684,56.386,161.778,56.271,161.906,56.235,162.044,56.224,162.176,56.261,162.373,56.264,162.461,56.235]]},{"n":"Kapuas","z":5,"b":[109.15,-0.352,113.465,1.201],"l":[[113.465,1.201,113.233,0.965,112.954,0.855,112.794,0.823,112.66,0.834,112.536,0.802,112.421,0.728,112.252,0.672,112.028,0.636,111.95,0.599,111.945,0.546,111.917,0.503,111.84,0.462,111.821,0.43,111.827,0.408,111.802,0.375,111.736,0.33,111.681,0.321,111.645,0.311,111.632,0.272,111.618,0.242,111.58,0.242,111.556,0.221,111.542,0.158,111.501,0.104,111.465,0.098,111.404,0.106,111.367,0.089,111.329,0.098,111.304,0.102,111.282,0.082,111.263,0.088,111.232,0.106,111.206,0.089,111.156,0.079,111.077,0.092,111.024,0.082,110.997,0.048,110.933,0.023,110.833,0.008,110.772,0.029,110.732,0.046,110.696,0.033,110.653,0.043,110.613,0.06,110.58,0.06,110.568,0.034,110.544,0.02,110.517,0.023,110.485,-0.002,110.465,-0.038,110.418,-0.066,110.335,-0.081,110.177,-0.056,110.098,-0.07,110.007,-0.095,109.947,-0.185,109.81,-0.269,109.596,-0.346,109.522,-0.352,109.453,-0.337,109.403,-0.304,109.336,-0.267,109.219,-0.231,109.15,-0.186]]},{"n":"Kasai","z":3,"b":[16.191,-11.249,22.307,-3.052],"l":[[22.237,-11.249,22.217,-11.013,22.178,-10.892,22.203,-10.829,22.28,-10.784,22.307,-10.691,22.283,-10.552,22.282,-10.453,22.302,-10.397,22.275,-10.259,22.198,-10.041,22.089,-9.863,21.949,-9.726,21.857,-9.594,21.813,-9.469,21.829,-9.168,21.905,-8.693,21.896,-8.341,21.801,-8.112,21.78,-7.865,21.834,-7.602,21.842,-7.421,21.785,-7.275,21.692,-7.109,21.547,-6.964,21.206,-6.692,20.939,-6.535,20.783,-6.366,20.648,-6.134,20.58,-5.993,20.579,-5.943,20.638,-5.876,20.755,-5.793,20.835,-5.712,20.898,-5.596,21.038,-5.339,21.075,-5.102,21.055,-4.8,20.966,-4.611,20.808,-4.537,20.688,-4.455,20.606,-4.365,20.522,-4.309,20.464,-4.295,20.403,-4.3,20.375,-4.319,20.355,-4.362,20.323,-4.39,20.285,-4.39,20.145,-4.315,19.904,-4.162,19.742,-4.081,19.66,-4.072,19.41,-3.959,18.991,-3.743,18.74,-3.626,18.616,-3.601,18.51,-3.599,18.414,-3.551,18.292,-3.457,18.13,-3.412,17.928,-3.415,17.796,-3.391,17.735,-3.34,17.674,-3.313,17.613,-3.31,17.427,-3.234,17.355,-3.2,17.06,-3.059,16.978,-3.052,16.885,-3.073,16.745,-3.086,16.519,-3.07,16.364,-3.073,16.281,-3.097,16.231,-3.139,16.191,-3.194]]},{"n":"Katherine","z":5,"b":[131.844,-14.706,133.227,-13.298],"l":[[133.134,-13.298,133.2,-13.421,133.224,-13.507,133.227,-13.595,133.21,-13.664,133.173,-13.712,133.133,-13.736,133.069,-13.733,132.966,-13.705,132.9,-13.73,132.835,-13.801,132.781,-13.914,132.741,-14.069,132.695,-14.172,132.645,-14.223,132.568,-14.266,132.466,-14.302,132.302,-14.414,132.076,-14.602,131.933,-14.698,131.844,-14.706]]},{"n":"Kayan","z":5,"b":[114.762,1.76,117.569,2.929],"l":[[114.762,1.76,114.876,1.885,114.956,1.933,115.085,1.949,115.261,2.011,115.36,2.031,115.44,2.028,115.49,2.01,115.51,1.976,115.552,1.969,115.617,1.988,115.694,2.061,115.781,2.188,115.845,2.204,115.886,2.11,115.959,2.066,116.063,2.073,116.141,2.146,116.194,2.286,116.326,2.466,116.536,2.685,116.656,2.798,116.686,2.805,116.719,2.782,116.756,2.73,116.812,2.736,116.887,2.801,116.982,2.81,117.099,2.765,117.168,2.755,117.191,2.779,117.229,2.787,117.284,2.776,117.342,2.809,117.402,2.885,117.46,2.922,117.512,2.929,117.569,2.929]]},{"n":"Kem","z":5,"b":[31.57,63.766,34.803,65.111],"l":[[31.57,63.869,31.714,63.819,31.834,63.766,31.944,63.767,32.236,63.853,32.294,63.903,32.357,63.984,32.378,64.037,32.357,64.062,32.36,64.109,32.388,64.177,32.339,64.249,32.215,64.325,32.185,64.377,32.25,64.404,32.302,64.445,32.34,64.499,32.303,64.57,32.193,64.656,32.144,64.723,32.149,64.748],[32.044,64.46,32.2,64.384],[32.149,64.748,32.154,64.771,32.259,64.82,32.459,64.872,32.589,64.93,32.65,64.995,32.742,65.016,32.865,64.994,32.952,65.016,33.001,65.081,33.066,65.111,33.186,65.104,34.005,64.975,34.386,64.944,34.709,64.98,34.803,64.986],[31.955,64.864,32.149,64.748]]},{"n":"Kemijoki","z":5,"b":[24.628,65.859,28.955,68.105],"l":[[27.359,66.467,27.324,66.406,27.271,66.373,27.19,66.349,26.901,66.356,26.403,66.395,26.066,66.441,25.889,66.493,25.709,66.494,25.525,66.444,25.4,66.363,25.303,66.193,24.946,66.118,24.798,66.048,24.711,65.912,24.628,65.859],[28.781,68.105,28.93,68.012,28.955,67.961,28.878,67.901,28.851,67.813,28.803,67.751,28.724,67.686,28.553,67.59,28.289,67.462,28.187,67.369,28.245,67.309,28.227,67.287,28.132,67.304,28.085,67.293,28.086,67.255,28.063,67.231,27.992,67.214,27.608,67.158,27.5,67.106,27.54,67.039,27.516,66.945,27.385,66.765]]},{"n":"Khatanga","z":5,"b":[102.078,71.952,105.144,72.777],"l":[[105.144,72.777,104.86,72.75,104.616,72.671,104.429,72.646,104.2,72.576,104.17,72.52,103.89,72.462,103.36,72.4,103.078,72.349,103.044,72.307,102.886,72.214,102.602,72.071,102.364,71.987,102.078,71.952]]},{"n":"Kheta","z":5,"b":[92.22,69.844,102.078,71.952],"l":[[92.22,69.987,92.861,69.948,93.228,69.926,93.62,69.888,93.733,69.853,94.006,69.844,94.439,69.862,94.819,69.933,95.145,70.056,95.405,70.131,95.598,70.155,95.744,70.211,95.843,70.298,95.876,70.356,95.844,70.385,95.834,70.439,95.846,70.517,95.737,70.557,95.384,70.565,95.325,70.631,95.158,70.67,94.726,70.652,94.388,70.677,94.145,70.747,94.139,70.831,94.371,70.93,94.867,70.945,96.007,70.846,97.365,70.813,97.835,70.825,97.869,70.87,97.947,70.908,98.069,70.939,98.141,70.983,98.164,71.04,98.18,71.12,98.305,71.191,98.704,71.158,98.97,71.151,99.102,71.171,99.265,71.248,99.513,71.433],[102.078,71.952,101.584,71.816,101.081,71.752,100.405,71.713,99.91,71.659,99.597,71.591,99.54,71.515,99.515,71.435]]},{"n":"Kibali","z":3,"b":[28.586,2.333,30.439,3.598],"l":[[30.371,2.333,30.439,2.663,30.363,2.825,30.278,2.878,30.159,2.876,30.008,2.924,29.825,3.024,29.68,3.081,29.571,3.094,29.509,3.15,29.495,3.249,29.44,3.3,29.347,3.303,29.245,3.381,29.135,3.535,29.033,3.598,28.938,3.568,28.814,3.564,28.586,3.596]]},{"n":"Klamath","z":4.7,"b":[-124.093,41.225,-121.785,42.251],"l":[[-123.78,41.277,-123.837,41.302,-123.884,41.362,-123.938,41.466,-123.991,41.523,-124.068,41.539,-124.093,41.54],[-121.809,42.251,-121.785,42.215,-121.8,42.181,-121.845,42.137,-121.901,42.126,-121.966,42.149,-122.052,42.119,-122.157,42.036,-122.257,41.991,-122.353,41.986,-122.447,41.951,-122.54,41.887,-122.637,41.867,-122.738,41.889,-122.812,41.892,-122.882,41.866,-122.999,41.826,-123.1,41.83,-123.223,41.866,-123.342,41.811,-123.458,41.665,-123.511,41.532,-123.503,41.411,-123.535,41.315,-123.609,41.244,-123.679,41.225,-123.78,41.277]]},{"n":"Kokemäenjoki","z":5,"b":[21.598,61.211,22.579,61.578],"l":[[22.579,61.211,22.399,61.293,22.341,61.302,22.345,61.265,22.301,61.256,22.21,61.276,22.053,61.344,21.716,61.519,21.655,61.545,21.598,61.578]]},{"n":"Kolyma","z":4,"b":[145.216,61.454,160.856,68.789],"l":[[145.216,61.856,145.606,61.899,145.801,61.877,145.943,61.839,146.096,61.78,146.23,61.697,146.344,61.59,146.486,61.509,146.657,61.455,146.812,61.454,146.95,61.508,147.052,61.522,147.119,61.497,147.225,61.52,147.371,61.59,147.433,61.708,147.412,61.873,147.355,61.985,147.26,62.045,147.255,62.14,147.342,62.271,147.464,62.331,147.624,62.32,147.718,62.299,147.747,62.266,147.816,62.243,147.924,62.229,148.025,62.18,148.12,62.095,148.244,62.075,148.397,62.119,148.524,62.107,148.625,62.037,148.663,61.98,148.622,61.913,148.727,61.785,148.747,61.729,148.718,61.704,148.722,61.68,148.758,61.659,148.859,61.666,149.027,61.7,149.2,61.699,149.379,61.66,149.501,61.674,149.566,61.741,149.601,61.802,149.606,61.859,149.656,61.897,149.752,61.917,149.81,61.943,149.83,61.976,149.915,62.02,150.064,62.075,150.19,62.092,150.292,62.072,150.429,62.088,150.601,62.141,150.714,62.197,150.766,62.257,150.8,62.341,150.817,62.449,150.804,62.533,150.763,62.593,150.777,62.639,150.845,62.672,151.056,62.665,151.41,62.619,151.618,62.559,151.682,62.486,151.742,62.454,151.798,62.462,151.865,62.499,151.942,62.565,151.992,62.556,152.015,62.471,152.084,62.427,152.201,62.425,152.271,62.435,152.293,62.458,152.292,62.488,152.27,62.525,152.326,62.608,152.462,62.736,152.505,62.823,152.456,62.87,152.461,62.929,152.521,63,152.543,63.063,152.53,63.118,152.543,63.162,152.584,63.194,152.598,63.244,152.583,63.312,152.65,63.361,152.798,63.394,152.908,63.456,152.982,63.549,153.158,63.67,153.437,63.819,153.664,63.902,153.841,63.917,154.057,63.96,154.315,64.033,154.468,64.088,154.517,64.127,154.542,64.168,154.542,64.213,154.498,64.224,154.41,64.203,154.372,64.227,154.383,64.295,154.317,64.354,154.175,64.406,154.08,64.512,154.031,64.671,153.835,64.734,153.493,64.701,153.222,64.716,153.022,64.779,152.818,64.813,152.611,64.817,152.487,64.839,152.447,64.879,152.327,64.925,152.127,64.98,151.964,65.042,151.837,65.113,151.768,65.164,151.758,65.195,151.703,65.22,151.606,65.237,151.549,65.265,151.534,65.303,151.497,65.331,151.439,65.349,151.343,65.415,151.21,65.532,151.145,65.603,151.15,65.628,151.059,65.69,150.873,65.788,150.768,65.887,150.744,65.986,150.755,66.04,150.777,66.061,150.785,66.088,150.773,66.129,150.798,66.149,150.944,66.139,151.037,66.162,151.08,66.223,151.105,66.235,151.148,66.235,151.254,66.211,151.374,66.216,151.509,66.25,151.596,66.256,151.634,66.232,151.654,66.241,151.654,66.285,151.692,66.332,151.77,66.382,151.798,66.421,151.778,66.447,151.921,66.503,152.4,66.647,152.439,66.673,152.434,66.702,152.385,66.732,152.392,66.751,152.455,66.758,152.538,66.806,152.64,66.896,152.768,66.964,152.922,67.012,153.007,67.061,153.025,67.109,153.18,67.151,153.473,67.186,153.625,67.214,153.635,67.234,153.6,67.278,153.52,67.346,153.535,67.407,153.644,67.463,154.088,67.534,154.865,67.62,155.354,67.646,155.555,67.614,155.804,67.61,156.101,67.636,156.217,67.657,156.247,67.671,156.25,67.692,156.282,67.717,156.348,67.736,156.354,67.753,156.336,67.775,156.2,67.819,156.132,67.874,156.132,67.918,156.146,67.944,156.256,67.961,156.502,67.937,156.667,67.939,156.793,67.982,157.432,68.093,157.654,68.175,157.671,68.265,157.772,68.36,157.956,68.46,158.019,68.545,157.963,68.616,158.015,68.656,158.175,68.666,158.318,68.705,158.446,68.771,158.549,68.789,158.667,68.741,159.059,68.774,159.157,68.783,159.19,68.768,159.192,68.735,159.179,68.694,159.215,68.67,159.395,68.632,160.127,68.54,160.559,68.512,160.679,68.542,160.856,68.538]]},{"n":"Konqi","z":5,"b":[86.372,40.633,89.387,41.315],"l":[[89.387,40.633,88.92,40.672,88.715,40.69,88.422,40.744,88.286,40.815,88.127,40.86,87.943,40.877,87.787,40.91,87.659,40.958,87.529,40.974,87.397,40.957,87.258,40.971,87.114,41.016,87.025,41.069,86.991,41.13,86.899,41.205,86.749,41.294,86.598,41.315,86.372,41.243]]},{"n":"Koukourou","z":5,"b":[19.733,7.026,20.935,7.538],"l":[[20.935,7.343,20.829,7.26,20.767,7.234,20.713,7.239,20.673,7.212,20.647,7.154,20.597,7.105,20.487,7.046,20.383,7.026,20.304,7.034,20.213,7.061,20.106,7.117,19.982,7.203,19.874,7.319,19.733,7.538]]},{"n":"Koyukuk","z":5,"b":[-157.769,64.85,-150.726,68.043],"l":[[-150.821,68.043,-150.892,67.854,-150.988,67.747,-151.027,67.678,-151.041,67.611,-150.991,67.548,-150.878,67.49,-150.789,67.426,-150.726,67.357,-150.726,67.279,-150.791,67.193,-151.032,67.092,-151.451,66.975,-151.619,66.918,-151.676,66.857,-151.709,66.739,-151.779,66.658,-151.885,66.615,-152.032,66.58,-152.219,66.555,-152.396,66.552,-152.562,66.571,-152.734,66.551,-152.914,66.492,-153.074,66.46,-153.214,66.456,-153.296,66.438,-153.33,66.39,-153.416,66.411,-153.6,66.382,-153.909,66.308,-154.134,66.195,-154.276,66.043,-154.454,65.923,-154.668,65.834,-154.847,65.794,-154.991,65.802,-155.055,65.833,-155.088,65.852,-155.222,65.883,-155.272,65.91,-155.31,65.93,-155.363,65.921,-155.39,65.936,-155.393,65.962,-155.434,65.994,-155.521,65.998,-155.653,65.975,-155.769,65.98,-155.869,66.013,-156.01,66.022,-156.192,66.008,-156.219,65.987,-156.225,65.963,-156.25,65.947,-156.328,65.93,-156.348,65.911,-156.334,65.886,-156.349,65.833,-156.481,65.755,-156.503,65.726,-156.488,65.696,-156.475,65.674,-156.49,65.654,-156.471,65.63,-156.426,65.621,-156.391,65.617,-156.369,65.577,-156.346,65.559,-156.336,65.534,-156.348,65.516,-156.382,65.504,-156.388,65.487,-156.391,65.467,-156.426,65.455,-156.498,65.457,-156.57,65.458,-156.638,65.469,-156.629,65.491,-156.673,65.517,-156.77,65.549,-156.858,65.564,-156.935,65.564,-157.028,65.539,-157.137,65.49,-157.291,65.475,-157.492,65.492,-157.609,65.491,-157.643,65.47,-157.684,65.415,-157.676,65.396,-157.643,65.389,-157.624,65.364,-157.621,65.323,-157.642,65.299,-157.627,65.26,-157.652,65.216,-157.662,65.164,-157.647,65.084,-157.595,65.055,-157.506,65.078,-157.432,65.075,-157.375,65.044,-157.397,65.027,-157.499,65.026,-157.545,65.003,-157.535,64.961,-157.582,64.921,-157.74,64.867,-157.769,64.85]]},{"n":"Krishna","z":5,"b":[73.796,15.782,80.892,17.992],"l":[[79.313,16.579,79.657,16.69,79.818,16.703,79.912,16.655,79.993,16.678,80.063,16.773,80.121,16.768,80.169,16.664,80.262,16.607,80.401,16.597,80.524,16.553,80.63,16.475,80.717,16.393,80.784,16.307,80.834,16.201,80.883,16.014,80.892,15.971,80.892,15.866,80.878,15.82,80.865,15.782],[73.796,17.992,73.898,17.931,73.962,17.854,74.054,17.686,74.118,17.633,74.127,17.579,74.103,17.507,74.121,17.423,74.18,17.329,74.204,17.255,74.193,17.203,74.232,17.157,74.322,17.119,74.369,17.083,74.376,17.049,74.396,17.034,74.429,17.038,74.457,17.016,74.492,16.942,74.537,16.858,74.587,16.808,74.659,16.766,74.672,16.733,74.628,16.711,74.623,16.667,74.66,16.602,74.754,16.596,74.907,16.649,75.013,16.634,75.075,16.552,75.126,16.54,75.167,16.598,75.231,16.621,75.317,16.608,75.383,16.58,75.429,16.536,75.449,16.492,75.443,16.448,75.49,16.43,75.591,16.441,75.677,16.422,75.749,16.373,75.838,16.338,75.943,16.317,76.016,16.279,76.059,16.225,76.124,16.188,76.213,16.167,76.405,16.227,76.699,16.368,76.882,16.47,76.954,16.534,77.057,16.528,77.259,16.415,77.616,16.349,77.845,16.232,78.066,16.044,78.193,15.956,78.227,15.97,78.244,15.992,78.244,16.023,78.283,16.05,78.361,16.072,78.498,16.071,78.693,16.047,78.805,16.064,78.833,16.122,78.884,16.171,78.997,16.23]]},{"n":"Kuskokwim","z":5,"b":[-161.947,60.685,-154.721,63.079],"l":[[-154.721,63.079,-154.844,63.02,-154.926,63.006,-155.01,63.018,-155.125,63.005,-155.271,62.965,-155.368,62.962,-155.416,62.996,-155.492,63,-155.595,62.972,-155.619,62.949,-155.564,62.931,-155.605,62.886,-155.741,62.815,-155.817,62.766,-155.831,62.739,-155.823,62.72,-155.794,62.71,-155.775,62.678,-155.767,62.624,-155.776,62.586,-155.814,62.554,-156.025,62.466,-156.142,62.38,-156.236,62.268,-156.256,62.163,-156.203,62.065,-156.203,62.007,-156.256,61.988,-156.298,61.951,-156.327,61.895,-156.519,61.817,-156.873,61.715,-157.244,61.725,-157.632,61.848,-157.885,61.892,-158.004,61.856,-158.081,61.85,-158.118,61.873,-158.182,61.821,-158.273,61.693,-158.393,61.61,-158.544,61.573,-158.71,61.566,-158.892,61.59,-159.187,61.594,-159.801,61.573,-160.215,61.543,-160.415,61.504,-160.538,61.445,-160.604,61.388,-160.615,61.334,-160.652,61.306,-160.716,61.302,-160.755,61.28,-160.77,61.239,-160.846,61.187,-160.984,61.123,-161.068,61.058,-161.1,60.992,-161.175,60.938,-161.292,60.897,-161.386,60.885,-161.457,60.903,-161.487,60.893,-161.476,60.854,-161.483,60.827,-161.508,60.811,-161.555,60.816,-161.624,60.844,-161.661,60.838,-161.666,60.799,-161.694,60.78,-161.745,60.781,-161.762,60.768,-161.746,60.74,-161.774,60.721,-161.881,60.704,-161.947,60.685]]},{"n":"Kwango","z":5,"b":[16.508,-11.256,18.797,-3.2],"l":[[18.797,-11.256,18.707,-10.879,18.632,-10.731,18.622,-10.657,18.652,-10.609,18.61,-10.526,18.497,-10.41,18.48,-10.363,18.487,-10.339,18.496,-10.317,18.481,-10.281,18.495,-10.232,18.536,-10.17,18.588,-10.117,18.65,-10.072,18.677,-9.997,18.67,-9.891,18.648,-9.805,18.611,-9.74,18.546,-9.681,18.455,-9.63,18.361,-9.522,18.267,-9.357,18.189,-9.254,18.128,-9.211,18.091,-9.204,18.056,-9.191,18.043,-9.152,18.05,-9.115,18.042,-9.011,18.014,-8.875,17.938,-8.715,17.816,-8.531,17.708,-8.414,17.613,-8.363,17.559,-8.272,17.536,-8.076,17.245,-7.623,17.155,-7.461,17.122,-7.419,17.064,-7.363,16.985,-7.257,16.952,-7.157,16.966,-7.062,16.919,-6.934,16.813,-6.773,16.743,-6.618,16.709,-6.472,16.701,-6.346,16.718,-6.241,16.697,-6.164,16.64,-6.115,16.602,-6.04,16.577,-5.89,16.515,-5.391,16.508,-5.158,16.536,-5.023,16.536,-4.841,16.508,-4.611,16.508,-4.46,16.535,-4.386,16.536,-4.315,16.513,-4.245,16.587,-4.18,16.758,-4.119,16.87,-4.062,16.921,-4.011,16.993,-3.991,17.086,-4.003,17.183,-3.92,17.286,-3.742,17.322,-3.61,17.291,-3.525,17.298,-3.454,17.365,-3.369,17.376,-3.295,17.355,-3.2]]},{"n":"Kyzyl-Khem","z":4,"b":[95.78,51.295,98,51.519],"l":[[98,51.514,97.879,51.519,97.703,51.48,97.298,51.36,96.524,51.346,96.235,51.356,96.174,51.383,96.099,51.383,95.965,51.342,95.78,51.295]]},{"n":"La Grande Rivière","z":4,"b":[-78.946,53.406,-70.362,53.916],"l":[[-70.48,53.406,-70.42,53.408,-70.387,53.435,-70.362,53.487,-70.473,53.492,-70.845,53.428],[-71.117,53.591,-71.277,53.664,-71.452,53.678,-71.816,53.646],[-72.036,53.598,-72.094,53.602,-72.102,53.616,-72.078,53.64,-72.082,53.655,-72.113,53.659,-72.28,53.621,-72.35,53.63,-72.389,53.675,-72.503,53.691,-72.692,53.679,-72.727,53.704,-72.852,53.765,-73.143,53.866,-73.328,53.916,-73.407,53.914,-73.536,53.877,-73.714,53.805,-73.962,53.748,-74.439,53.684],[-77.42,53.798,-77.626,53.82,-77.826,53.806,-78.088,53.764,-78.39,53.762,-78.901,53.817,-78.924,53.823,-78.946,53.832]]},{"n":"Mekong","z":2.1,"b":[96.941,17.821,102.233,31.966],"l":[[96.941,31.966,97.055,31.692,97.121,31.577,97.175,31.528,97.207,31.463,97.215,31.381,97.204,31.325,97.173,31.296,97.168,31.233,97.188,31.134,97.243,31.05,97.332,30.979,97.377,30.89,97.378,30.785,97.495,30.618,97.846,30.278,97.963,30.162,98.037,30.046,98.109,29.89,98.161,29.81,98.194,29.806,98.242,29.763,98.307,29.68,98.363,29.568,98.411,29.427,98.484,29.307,98.582,29.209,98.619,29.126,98.595,29.058,98.593,29.009,98.624,28.963,98.67,28.772,98.727,28.632,98.809,28.478,98.863,28.304,98.889,28.109,98.938,27.959,99.012,27.856,99.036,27.768,99.01,27.695,99.005,27.623,99.028,27.516,99.088,27.377,99.104,27.308,99.094,27.265,99.11,27.215,99.151,27.16,99.175,27.071,99.182,26.948,99.165,26.801,99.124,26.632,99.114,26.531,99.135,26.498,99.141,26.38,99.134,26.176,99.139,26.021,99.157,25.918,99.213,25.792,99.31,25.644,99.341,25.521,99.308,25.423,99.341,25.315,99.44,25.198,99.525,25.129,99.599,25.109,99.7,25.017,99.828,24.853,99.939,24.762,100.031,24.743,100.087,24.718,100.106,24.687,100.14,24.69,100.187,24.727,100.251,24.747,100.332,24.75,100.418,24.682,100.511,24.545,100.548,24.388,100.529,24.211,100.485,24.108,100.416,24.078,100.388,24.026,100.401,23.953,100.356,23.854,100.252,23.73,100.185,23.62,100.139,23.473,100.067,23.278,100.106,23.149,100.233,23.023,100.313,22.887,100.347,22.744,100.415,22.636,100.517,22.563,100.567,22.502,100.565,22.451,100.588,22.398,100.636,22.343,100.647,22.306,100.622,22.288,100.632,22.268,100.677,22.247,100.705,22.205,100.715,22.141,100.77,22.047,100.869,21.923,100.984,21.845,101.115,21.813,101.166,21.789,101.134,21.774,101.128,21.705,101.147,21.582,101.08,21.469,100.928,21.366,100.82,21.314,100.757,21.313,100.703,21.251,100.659,21.13,100.614,21.059,100.567,21.038,100.536,20.992,100.522,20.922,100.549,20.884,100.618,20.879,100.623,20.86,100.565,20.825,100.493,20.813,100.407,20.823,100.326,20.796,100.249,20.73,100.184,20.589,100.13,20.372,100.115,20.258,100.14,20.245,100.174,20.273,100.218,20.34,100.266,20.377,100.318,20.386,100.373,20.34,100.432,20.241,100.492,20.184,100.553,20.17,100.589,20.073,100.597,19.893,100.653,19.825,100.755,19.87,100.857,19.878,100.959,19.849,101.037,19.848,101.091,19.875,101.153,19.872,101.223,19.838,101.333,19.822,101.484,19.823,101.699,19.89,101.979,20.022,102.137,20.087,102.173,20.083,102.205,20.058,102.233,20.011,102.141,19.903,101.93,19.734,101.823,19.604,101.822,19.513,101.842,19.451,101.885,19.419,101.886,19.372,101.845,19.31,101.819,19.181,101.808,18.984,101.816,18.855,101.841,18.794,101.826,18.698,101.77,18.568,101.658,18.422,101.488,18.262,101.408,18.119,101.419,17.992,101.459,17.902,101.564,17.821]]},{"n":"Lek","z":4,"b":[4.085,51.896,5.331,51.994],"l":[[5.331,51.963,5.161,51.994,4.999,51.977,4.787,51.925,4.596,51.898,4.426,51.896,4.28,51.919,4.085,51.994]]},{"n":"Lena","z":2,"b":[105.07,53.845,130.051,72.294],"l":[[120.648,60.403,120.343,60.396,120.184,60.35,120.069,60.262,119.874,60.229,119.599,60.252,119.355,60.191,119.142,60.046,118.959,59.966,118.805,59.949,118.678,59.917,118.575,59.87,118.51,59.824,118.482,59.778,118.42,59.745,118.323,59.725,117.998,59.805,117.447,59.986,117.129,60.134,117.046,60.249,116.964,60.331,116.884,60.381,116.669,60.439,116.321,60.505,116.031,60.598,115.798,60.717,115.503,60.775,115.146,60.773,114.893,60.745,114.743,60.692,114.549,60.567,114.311,60.369,114.137,60.26,114.029,60.241,113.823,60.125,113.519,59.912,113.24,59.779,112.983,59.728,112.836,59.674,112.798,59.618,112.745,59.578,112.643,59.545,112.543,59.363,112.394,59.274,112.161,59.215,112.01,59.197,111.942,59.218,111.846,59.187,111.723,59.104,111.549,59.019,111.323,58.93,111.202,58.847,111.186,58.769,111.117,58.739,110.993,58.758,110.949,58.746,110.929,58.71,110.805,58.659,110.56,58.662,110.282,58.645,109.97,58.606,109.798,58.539,109.764,58.445,109.694,58.359,109.587,58.279,109.498,58.231,109.427,58.215,109.341,58.225,109.239,58.261,109.154,58.275,109.085,58.264,108.94,58.21,108.721,58.113,108.606,58.044,108.594,58.001,108.553,57.972,108.482,57.956,108.409,57.907,108.332,57.825,108.229,57.785,108.1,57.787,108.034,57.759,108.029,57.702,107.957,57.659,107.904,57.635,107.894,57.61,107.918,57.57,107.872,57.523,107.697,57.438,107.619,57.408,107.593,57.386,107.584,57.357,107.565,57.343,107.519,57.346,107.487,57.329,107.471,57.292,107.415,57.271,107.318,57.264,107.251,57.276,107.213,57.308,107.149,57.326,107.059,57.328,106.989,57.31,106.937,57.27,106.918,57.232,106.93,57.196,106.912,57.146,106.865,57.083,106.747,57.043,106.558,57.026,106.425,57.028,106.348,57.051,106.256,57.011,106.15,56.909,105.986,56.833,105.764,56.783,105.672,56.742,105.712,56.711,105.831,56.696,106.031,56.698,106.173,56.671,106.256,56.617,106.292,56.56,106.281,56.501,106.25,56.451,106.201,56.411,106.058,56.366,105.822,56.316,105.695,56.266,105.676,56.217,105.718,56.152,105.821,56.07,105.852,55.976,105.811,55.869,105.731,55.755,105.613,55.633,105.52,55.49,105.45,55.326,105.331,55.197,105.163,55.103,105.076,55.008,105.07,54.913,105.108,54.85,105.19,54.82,105.234,54.754,105.241,54.649,105.224,54.55,105.185,54.456,105.221,54.384,105.332,54.334,105.457,54.238,105.665,54.022,105.806,54.006,105.873,53.973,105.913,53.916,105.974,53.878,106.056,53.858,106.305,53.857,106.927,53.884,106.996,53.85,107.053,53.845,107.119,53.858,107.159,53.894,107.171,53.952,107.234,53.999,107.347,54.036,107.456,54.105,107.562,54.205,107.657,54.265,107.783,54.294,108.021,54.246,108.107,54.2,108.118,54.138,108.088,54.082,107.98,54.004],[126.69,72.294,126.897,72.05,127.02,71.946,127.127,71.899,127.168,71.845,127.142,71.784,127.162,71.683,127.229,71.542,127.258,71.412,127.25,71.294,127.317,71.184,127.459,71.082,127.507,70.999,127.46,70.934,127.472,70.879,127.414,70.742,127.091,70.606,126.789,70.541,126.509,70.545,126.231,70.527,125.954,70.487,125.801,70.449,125.77,70.414,125.833,70.367,125.99,70.309,126.019,70.235,125.919,70.145,125.773,70.069,125.485,69.974,125.201,69.904,125.079,69.826,125.023,69.716,124.902,69.58,124.722,69.4,124.622,69.336,124.251,69.181,124.094,69.097,124.026,69.03,123.985,68.926,123.971,68.785,123.927,68.634,123.853,68.474,123.774,68.349,123.689,68.259,123.535,68.173,123.31,68.092,123.15,67.929,123.057,67.685,123.095,67.455,123.263,67.237,123.398,66.997,123.5,66.737,123.588,66.581,123.697,66.503,123.738,66.461,123.876,66.198,124.028,66.022,124.211,65.873,124.285,65.733,124.251,65.602,124.268,65.504,124.337,65.441,124.374,65.379,124.377,65.316,124.408,65.252,124.466,65.184,124.593,65.098,124.79,64.992,124.954,64.931,125.085,64.916,125.22,64.879,125.359,64.818,125.454,64.747,125.503,64.665,125.744,64.575,126.175,64.478,126.423,64.403,126.506,64.378,126.853,64.224,127.292,63.965,127.586,63.843,127.88,63.772,128.078,63.697,128.178,63.618,128.291,63.561,128.417,63.527,128.641,63.516,128.963,63.528,129.188,63.523,129.38,63.49,129.586,63.305,129.637,63.199,129.603,63.11,129.622,63.034,129.695,62.971,129.764,62.858,129.828,62.696,129.909,62.569,130.009,62.479,130.051,62.396,130.037,62.322,129.997,62.27,129.934,62.24,129.892,62.16,129.873,62.031,129.788,61.893,129.638,61.748,129.476,61.631,129.216,61.499,128.751,61.323,128.481,61.266,128.249,61.269,127.893,61.228,127.413,61.142,127.047,61.103,126.796,61.112,126.554,61.101,126.322,61.071,126.097,61.014,125.881,60.931,125.578,60.857,124.995,60.758,124.893,60.71,124.73,60.688,124.473,60.675,124.279,60.646,124.147,60.599,123.552,60.576,122.297,60.575,121.653,60.598,121.425,60.643,121.162,60.622,120.864,60.537,120.7,60.473,120.654,60.407]]},{"n":"Liao","z":5,"b":[122.14,40.688,123.831,43.483],"l":[[123.554,43.483,123.614,43.399,123.604,43.29,123.545,43.128,123.534,43.019,123.571,42.963,123.583,42.877,123.568,42.76,123.611,42.66,123.712,42.576,123.774,42.504,123.796,42.442,123.831,42.406,123.771,42.327,123.513,42.203,123.35,42.149,123.283,42.166,123.177,42.145,123.033,42.085,122.893,41.957,122.758,41.762,122.677,41.579,122.636,41.325,122.457,41.214,122.401,41.133,122.44,41.055,122.409,41.003,122.326,40.963,122.294,40.898,122.236,40.852,122.152,40.823,122.147,40.78,122.14,40.688]]},{"n":"Liard","z":5,"b":[-131.373,59.311,-121.221,61.864],"l":[[-131.373,61.204,-131.32,61.072,-131.246,61.043,-131.132,61.075,-131.044,61.082,-130.951,61.054,-130.619,60.862,-130.374,60.765,-130.105,60.698,-129.845,60.582,-129.593,60.417,-129.374,60.309,-129.185,60.26,-129.028,60.186,-128.904,60.087,-128.791,60.035,-128.691,60.029,-128.61,60.003,-128.547,59.956,-128.397,59.909,-128.161,59.863,-127.991,59.883,-127.888,59.971,-127.772,60.005,-127.644,59.986,-127.54,59.952,-127.462,59.902,-127.446,59.843,-127.494,59.773,-127.463,59.738,-127.354,59.738,-127.259,59.712,-127.138,59.636,-126.987,59.66,-126.835,59.65,-126.63,59.614,-126.404,59.55,-126.156,59.457,-125.979,59.425,-125.872,59.455,-125.81,59.456,-125.672,59.384,-125.447,59.322,-125.232,59.311,-125.028,59.349,-124.88,59.413,-124.789,59.503,-124.72,59.553,-124.682,59.616,-124.646,59.665,-124.554,59.729,-124.414,59.724,-124.225,59.651,-124.16,59.619,-124.146,59.561,-124.081,59.558,-124.048,59.594,-123.931,59.71,-123.86,59.853,-123.835,60.025,-123.713,60.165,-123.494,60.274,-123.42,60.366,-123.49,60.443,-123.534,60.523,-123.553,60.605,-123.552,60.666,-123.53,60.704,-123.468,60.735,-123.437,60.801,-123.395,60.893,-123.346,60.947,-123.326,60.974,-123.277,61.022,-123.182,61.078,-123.136,61.097,-123.003,61.101,-122.886,61.135,-122.753,61.218,-122.405,61.307,-121.842,61.403,-121.521,61.482,-121.442,61.544,-121.414,61.596,-121.434,61.64,-121.384,61.685,-121.263,61.731,-121.221,61.777,-121.28,61.864]]},{"n":"Limpopo","z":5,"b":[26.962,-25.189,33.631,-22.146],"l":[[26.962,-23.737,27.086,-23.578,27.146,-23.524,27.186,-23.523,27.241,-23.49,27.313,-23.424,27.399,-23.384,27.499,-23.368,27.563,-23.325,27.593,-23.253,27.644,-23.218,27.717,-23.22,27.758,-23.197,27.769,-23.149,27.813,-23.108,27.891,-23.074,27.931,-23.034,27.935,-22.987,28.028,-22.874,28.21,-22.694,28.382,-22.593,28.543,-22.573,28.696,-22.535,28.84,-22.481,28.946,-22.395,29.014,-22.278,29.13,-22.213,29.377,-22.193,29.663,-22.146,29.902,-22.184,30.19,-22.291,30.46,-22.329,30.712,-22.298,30.916,-22.291,31.073,-22.308,31.197,-22.345,31.288,-22.402,31.393,-22.437,31.512,-22.451,31.63,-22.506,31.749,-22.6,31.877,-22.74,32.015,-22.924,32.119,-23.036,32.188,-23.076,32.231,-23.133,32.247,-23.205,32.302,-23.302,32.396,-23.424,32.488,-23.636,32.577,-23.937,32.673,-24.14,32.825,-24.297,32.871,-24.404,32.906,-24.44,32.943,-24.441,33.01,-24.485,33.108,-24.572,33.183,-24.614,33.234,-24.609,33.264,-24.624,33.273,-24.657,33.329,-24.699,33.431,-24.751,33.485,-24.772,33.51,-24.771,33.526,-24.757,33.537,-24.745,33.552,-24.739,33.568,-24.751,33.597,-24.823,33.631,-24.949,33.614,-25.041,33.545,-25.098,33.515,-25.142,33.53,-25.189]]},{"n":"Lom","z":5,"b":[13.126,4.933,14.705,6.698],"l":[[14.665,6.698,14.705,6.661,14.687,6.585,14.596,6.393,14.471,6.302,14.431,6.222,14.432,6.124,14.417,6.042,14.386,5.977,14.314,5.926,14.201,5.89,14.13,5.814,14.1,5.699,14.067,5.64,14.029,5.637,13.991,5.607,13.951,5.549,13.91,5.522,13.866,5.528,13.848,5.499,13.858,5.435,13.801,5.372,13.678,5.311,13.595,5.299,13.528,5.358,13.466,5.381,13.423,5.362,13.378,5.307,13.364,5.234,13.381,5.141,13.359,5.056,13.299,4.978,13.233,4.938,13.126,4.933]]},{"n":"Nizhnyaya Tunguska","z":4.7,"b":[87.961,57.8,108.603,65.969],"l":[[105.689,58.068,105.809,58,105.877,57.982,105.962,57.999,106.199,58.028,106.319,58.011,106.402,57.958,106.534,57.908,106.89,57.826,107.057,57.804,107.173,57.805,107.238,57.828,107.317,57.827,107.409,57.8,107.504,57.816,107.6,57.874,107.687,57.902,107.762,57.899,107.892,57.961,108.075,58.089,108.195,58.152,108.251,58.149,108.342,58.211,108.467,58.34,108.506,58.435,108.461,58.497,108.458,58.547,108.498,58.585,108.484,58.644,108.416,58.721,108.406,58.781,108.452,58.822,108.466,58.903,108.448,59.025,108.392,59.126,108.252,59.247,108.134,59.395,108.074,59.446,108.033,59.449,108.023,59.485,108.045,59.553,108.018,59.61,107.941,59.657,107.963,59.726,108.084,59.818,108.099,59.893,108.091,59.936,108.104,60.002,108.094,60.051,108.062,60.085,108.062,60.123,108.038,60.205,107.897,60.246,107.819,60.292,107.803,60.343,107.811,60.391,107.842,60.437,107.841,60.493,107.872,60.524,107.895,60.554,107.893,60.593,107.866,60.624,107.867,60.679,107.891,60.721,107.956,60.84,107.98,60.914,107.965,60.942,107.93,60.956,107.875,60.955,107.854,60.995,107.866,61.074,107.909,61.117,107.982,61.126,108.007,61.145,107.985,61.175,107.959,61.231,107.967,61.269,108.029,61.327,108.036,61.379,107.988,61.423,107.978,61.467,108.006,61.511,108.008,61.552,108.053,61.597,108.085,61.635,108.072,61.674,108.04,61.7,108.013,61.729,108,61.752,108.014,61.815,108.034,61.879,108.048,61.929,108.089,61.971,108.223,62.034,108.245,62.085,108.221,62.135,108.225,62.191,108.279,62.256,108.29,62.333,108.256,62.421,108.225,62.468,108.196,62.471,108.179,62.451,108.173,62.406,108.14,62.373,108.08,62.35,108.021,62.355,107.962,62.388,107.922,62.442,107.95,62.527,108.062,62.583,108.407,62.641,108.587,62.681,108.603,62.704,108.595,62.729,108.563,62.755,108.445,62.763,108.239,62.753,108.17,62.779,108.237,62.841,108.366,62.878,108.555,62.887,108.592,62.919,108.552,62.948,108.471,62.955,108.369,62.961,108.317,62.974,108.188,62.973,108.119,62.99,108.112,63.026,108.144,63.064,108.216,63.102,108.218,63.143,108.151,63.188,108.078,63.203,108,63.189,107.949,63.148,107.927,63.078,107.887,63.054,107.829,63.078,107.805,63.111,107.815,63.153,107.772,63.166,107.676,63.15,107.611,63.159,107.576,63.192,107.525,63.21,107.455,63.214,107.422,63.236,107.372,63.272,107.278,63.304,106.829,63.334,106.809,63.35,106.839,63.38,106.918,63.424,106.925,63.471,106.859,63.519,106.741,63.538,106.569,63.526,106.441,63.495,106.357,63.444,106.266,63.437,106.172,63.445,106.099,63.42,106.07,63.375,106.026,63.367,105.818,63.392,105.646,63.381,105.51,63.333,105.385,63.327,105.273,63.362,105.183,63.408,105.117,63.464,104.775,63.517,104.157,63.568,103.807,63.625,103.725,63.687,103.705,63.734,103.637,63.785,103.487,63.82,103.271,63.899,103.03,64.032,102.831,64.103,102.674,64.111,102.539,64.098,102.373,64.047,101.994,64.094,101.784,64.14,101.617,64.203,101.432,64.202,101.228,64.137,101.023,64.13,100.818,64.181,100.671,64.2,100.581,64.187,100.487,64.211,100.389,64.272,100.278,64.294,100.153,64.277,100.037,64.232,99.929,64.158,99.443,64.132,98.578,64.155,98.124,64.154,98.083,64.13,98.097,64.101,98.168,64.066,98.189,64.03,98.159,63.993,98.073,63.96,97.932,63.933,97.906,63.889,97.998,63.828,98.03,63.785,98.003,63.761,97.78,63.775,97.361,63.825,96.993,63.837,96.675,63.809,96.476,63.818,96.397,63.865,96.271,63.892,96.1,63.901,95.887,63.945,95.632,64.025,95.429,64.059,95.278,64.046,95.078,64.079,94.829,64.16,94.519,64.191,94.147,64.172,93.714,64.232,93.22,64.373,92.831,64.438,92.548,64.427,92.382,64.404,92.334,64.368,92.252,64.366,92.08,64.412],[87.961,65.776,88.132,65.83,88.228,65.844,88.307,65.835,88.386,65.855,88.466,65.904,88.616,65.942,88.836,65.969,89.211,65.904,89.74,65.748,90.018,65.656,90.045,65.627,90.035,65.582,89.989,65.521,89.974,65.399,89.992,65.216,90.046,65.116,90.137,65.099,90.212,65.066,90.274,65.017,90.304,64.956,90.302,64.883,90.337,64.831,90.406,64.8,90.514,64.782,90.66,64.776,90.824,64.718,91.007,64.608,91.345,64.518,92.081,64.411]]},{"n":"Lualaba","z":2.1,"b":[25.413,-11.743,26.441,-8.272],"l":[[26.249,-11.743,26.166,-11.672,26.119,-11.658,26.08,-11.678,26.03,-11.68,25.938,-11.653,25.886,-11.623,25.84,-11.547,25.782,-11.417,25.715,-11.331,25.639,-11.29,25.602,-11.247,25.604,-11.2,25.59,-11.169,25.56,-11.153,25.552,-11.114,25.566,-11.054,25.611,-11.005,25.684,-10.969,25.726,-10.878,25.735,-10.731,25.696,-10.607,25.61,-10.507,25.541,-10.467,25.49,-10.486,25.449,-10.485,25.417,-10.462,25.414,-10.407,25.441,-10.319,25.44,-10.23,25.413,-10.137,25.441,-10.036,25.525,-9.926,25.547,-9.811,25.506,-9.691,25.489,-9.587,25.502,-9.455,25.561,-9.266,25.611,-9.229,25.67,-9.278,25.729,-9.276,25.786,-9.221,25.843,-9.197,25.901,-9.204,25.966,-9.137,26.037,-8.996,26.058,-8.881,26.031,-8.792,26.086,-8.705,26.223,-8.621,26.284,-8.544,26.268,-8.474,26.282,-8.42,26.326,-8.383,26.346,-8.347,26.341,-8.313,26.364,-8.29,26.441,-8.272]]},{"n":"Congo","z":2.1,"b":[24.292,-5.61,27.014,0.782],"l":[[26.93,-5.61,27.003,-5.378,27.014,-5.286,26.988,-5.256,26.959,-5.123,26.927,-4.885,26.879,-4.731,26.813,-4.662,26.652,-4.55,26.395,-4.397,26.199,-4.25,26.065,-4.11,26.012,-3.958,26.04,-3.796,26.013,-3.649,25.93,-3.519,25.904,-3.363,25.935,-3.18,25.949,-3.023,25.946,-2.89,25.927,-2.804,25.876,-2.745],[25.876,-2.752,25.788,-2.457,25.785,-2.113,25.837,-1.623,25.818,-1.286,25.682,-1.012,25.534,-0.816,25.465,-0.682,25.423,-0.545,25.443,-0.386,25.525,-0.204,25.554,-0.036,25.529,0.121,25.489,0.235,25.409,0.346,25.255,0.459,25.136,0.507,24.934,0.539,24.791,0.577,24.69,0.628,24.584,0.705,24.472,0.753,24.292,0.782]]},{"n":"Mackenzie","z":2.1,"b":[-135.638,61.248,-117.668,69.324],"l":[[-117.668,61.308,-117.706,61.367,-117.86,61.415,-118.143,61.47,-118.323,61.455,-118.402,61.369,-118.7,61.3,-119.22,61.248,-119.773,61.304,-120.36,61.468,-120.673,61.567,-120.714,61.603,-120.718,61.648,-120.686,61.704,-120.684,61.752,-120.713,61.792,-120.868,61.821,-121.148,61.839,-121.28,61.864,-121.363,61.88,-121.589,61.974,-122.235,62.118,-122.603,62.167,-122.906,62.17,-123.139,62.204,-123.388,62.31,-123.402,62.326,-123.356,62.405,-123.25,62.548,-123.204,62.709,-123.218,62.889,-123.254,63.024,-123.311,63.113,-123.419,63.199,-123.577,63.28,-123.683,63.379,-123.736,63.493,-123.83,63.63,-123.966,63.79,-124.084,63.897,-124.183,63.952,-124.266,63.978,-124.333,63.977,-124.39,63.999,-124.438,64.046,-124.451,64.104,-124.429,64.174,-124.448,64.232,-124.506,64.278,-124.584,64.311,-124.682,64.333,-124.789,64.405,-124.906,64.526,-124.947,64.615,-124.912,64.671,-124.943,64.707,-125.039,64.724,-125.109,64.764,-125.153,64.826,-125.375,64.875,-125.619,64.895,-125.775,64.908,-126.027,64.956,-126.182,65.049,-126.459,65.177,-126.646,65.238,-126.929,65.291,-127.061,65.295,-127.337,65.375,-128.03,65.608,-128.401,65.62,-128.588,65.643,-128.715,65.681,-128.75,65.739,-128.817,65.791,-128.97,65.867,-129.065,65.95,-129.102,66.04,-129.093,66.109,-129.038,66.155,-128.923,66.202,-128.748,66.251,-128.666,66.292,-128.678,66.327,-128.745,66.358,-128.867,66.385,-129.089,66.48,-129.411,66.644,-129.699,66.75,-129.955,66.797,-130.118,66.86,-130.189,66.941,-130.236,67.046,-130.26,67.174,-130.306,67.258,-130.375,67.296,-130.496,67.325,-130.705,67.34,-130.759,67.354,-130.808,67.374,-130.882,67.42,-130.956,67.461,-131.16,67.489,-131.437,67.473,-131.907,67.403,-132.569,67.278,-133.02,67.245,-133.26,67.304,-133.433,67.371,-133.539,67.444,-133.64,67.479,-133.737,67.477,-133.887,67.526,-134.091,67.625,-134.199,67.718,-134.212,67.803,-134.277,67.889,-134.394,67.976,-134.448,68.073,-134.44,68.177,-134.409,68.231,-134.366,68.292,-134.33,68.314,-134.272,68.318,-134.199,68.345,-134.125,68.392,-134.099,68.429,-134.075,68.517,-134.103,68.576,-134.244,68.706],[-134.244,68.706,-134.391,68.706,-134.539,68.77,-134.836,68.964],[-134.244,68.706,-134.545,68.919,-134.626,69.017,-134.586,69.071,-134.503,69.111,-134.313,69.154,-134.245,69.192,-134.174,69.253],[-134.836,68.964,-135.034,68.908,-135.131,68.903,-135.195,68.93,-135.322,68.936,-135.606,68.911,-135.638,68.892],[-134.836,68.964,-135.037,69.062,-135.076,69.11,-135.02,69.139,-135.039,69.181,-135.183,69.263,-135.255,69.324]]},{"n":"Madeira","z":3,"b":[-65.422,-10.392,-58.689,-3.342],"l":[[-65.395,-10.392,-65.313,-10.253,-65.299,-10.147,-65.325,-10.027,-65.328,-9.936,-65.309,-9.873,-65.338,-9.79,-65.414,-9.689,-65.422,-9.611,-65.361,-9.556,-65.233,-9.546,-65.038,-9.581,-64.914,-9.583,-64.863,-9.554,-64.833,-9.499,-64.827,-9.416,-64.802,-9.372,-64.759,-9.367,-64.701,-9.32,-64.627,-9.231,-64.534,-9.185,-64.423,-9.181,-64.341,-9.138,-64.287,-9.056,-64.216,-9.002,-64.127,-8.976,-64.08,-8.937,-64.075,-8.884,-64.035,-8.832,-63.96,-8.783,-63.919,-8.729,-63.913,-8.672,-63.849,-8.619,-63.728,-8.572,-63.636,-8.556,-63.571,-8.571,-63.547,-8.555,-63.562,-8.507,-63.545,-8.466,-63.495,-8.432,-63.438,-8.358,-63.343,-8.186,-63.245,-8.215,-63.131,-8.177,-62.888,-8.034,-62.867,-7.945,-62.9,-7.803,-62.98,-7.577,-62.98,-7.351,-62.898,-7.123,-62.824,-7.004,-62.757,-6.994,-62.688,-6.934,-62.615,-6.823,-62.529,-6.762,-62.429,-6.752,-62.367,-6.723,-62.344,-6.675,-62.344,-6.634,-62.367,-6.598,-62.348,-6.538,-62.288,-6.454,-62.25,-6.367,-62.234,-6.278,-62.184,-6.23,-62.126,-6.222,-62.092,-6.218,-62.058,-6.22,-61.905,-6.236,-61.844,-6.21,-61.827,-6.147,-61.78,-6.086,-61.706,-6.026,-61.668,-5.973,-61.668,-5.927,-61.648,-5.901,-61.609,-5.896,-61.599,-5.881,-61.62,-5.856,-61.549,-5.838,-61.385,-5.827,-61.289,-5.781,-61.258,-5.699,-61.117,-5.615,-60.865,-5.529,-60.734,-5.448,-60.725,-5.37,-60.647,-5.293,-60.529,-5.23,-60.459,-5.199,-60.425,-5.176,-60.388,-5.147,-60.362,-5.103,-60.26,-4.977,-60.171,-4.913,-60.105,-4.917,-60.037,-4.897,-59.966,-4.851,-59.931,-4.791,-59.931,-4.714,-59.914,-4.671,-59.883,-4.595,-59.786,-4.432,-59.699,-4.367,-59.648,-4.364,-59.603,-4.37,-59.574,-4.366,-59.548,-4.357,-59.521,-4.342,-59.489,-4.318,-59.427,-4.25,-59.362,-4.156,-59.328,-4.045,-59.253,-3.952,-59.138,-3.875,-59.083,-3.798,-59.09,-3.72,-58.997,-3.596,-58.689,-3.342]]},{"n":"Madison","z":2,"b":[-111.719,44.653,-110.744,46.36],"l":[[-110.744,44.727,-110.859,44.668,-110.933,44.653,-111.004,44.662,-111.054,44.689,-111.091,44.747,-111.1,44.757],[-111.309,44.868,-111.329,44.867,-111.406,44.843,-111.477,44.852,-111.553,44.892,-111.626,44.988,-111.694,45.141,-111.719,45.263,-111.701,45.354,-111.646,45.458,-111.553,45.573,-111.493,45.722,-111.464,45.904,-111.43,46.025,-111.39,46.085,-111.406,46.177,-111.516,46.36]]},{"n":"Madre de Dios","z":5,"b":[-72.165,-12.661,-65.395,-10.392],"l":[[-72.165,-12.182,-72.094,-12.063,-72.093,-11.96,-72.139,-11.835,-72.117,-11.742,-72.028,-11.683,-71.931,-11.659,-71.776,-11.675,-71.604,-11.688,-71.533,-11.716,-71.503,-11.765,-71.399,-11.848,-71.221,-11.965,-71.086,-12.08,-70.997,-12.192,-70.91,-12.259,-70.825,-12.278,-70.754,-12.33,-70.695,-12.412,-70.59,-12.489,-70.438,-12.561,-70.319,-12.598,-70.232,-12.6,-70.18,-12.589,-70.155,-12.565,-70.115,-12.568,-70.026,-12.586,-69.768,-12.655,-69.578,-12.661,-69.455,-12.603,-69.391,-12.559,-69.344,-12.554,-69.309,-12.568,-69.268,-12.582,-69.235,-12.575,-69.215,-12.552,-69.187,-12.548,-69.156,-12.573,-69.107,-12.574,-69.063,-12.543,-69.008,-12.534,-68.949,-12.556,-68.859,-12.548,-68.678,-12.495,-68.606,-12.429,-68.556,-12.409,-68.503,-12.414,-68.455,-12.373,-68.412,-12.286,-68.354,-12.22,-68.282,-12.176,-68.239,-12.12,-68.223,-12.053,-68.16,-12.005,-68.051,-11.974,-67.976,-11.897,-67.936,-11.773,-67.881,-11.704,-67.811,-11.688,-67.749,-11.635,-67.695,-11.546,-67.641,-11.494,-67.588,-11.481,-67.519,-11.498,-67.436,-11.545,-67.36,-11.551,-67.293,-11.514,-67.239,-11.463,-67.198,-11.397,-67.127,-11.36,-66.976,-11.344,-66.96,-11.294,-66.916,-11.276,-66.838,-11.274,-66.803,-11.254,-66.786,-11.222,-66.749,-11.23,-66.673,-11.205,-66.567,-11.132,-66.48,-11.095,-66.413,-11.095,-66.37,-11.062,-66.343,-11.026,-66.315,-11.018,-66.276,-11.007,-66.214,-10.95,-66.158,-10.947,-66.118,-10.957,-66.079,-10.937,-66.065,-10.893,-66.061,-10.861,-66.092,-10.848,-66.084,-10.828,-66.036,-10.802,-65.983,-10.795,-65.924,-10.806,-65.902,-10.78,-65.897,-10.725,-65.874,-10.696,-65.82,-10.697,-65.78,-10.667,-65.768,-10.598,-65.74,-10.566,-65.696,-10.57,-65.604,-10.527,-65.395,-10.392]]},{"n":"Magdalena","z":4,"b":[-76.507,1.896,-73.765,11.11],"l":[[-76.507,1.943,-76.281,1.896,-76.179,1.898,-76.097,1.948,-75.791,2.118,-75.69,2.198,-75.692,2.246,-75.662,2.297,-75.599,2.352,-75.572,2.401,-75.583,2.446,-75.518,2.57,-75.376,2.774,-75.292,2.981,-75.266,3.19,-75.159,3.461,-74.971,3.793,-74.858,4.038,-74.82,4.194,-74.824,4.274,-74.869,4.278,-74.861,4.422,-74.771,4.848,-74.756,5.189,-74.723,5.367,-74.668,5.498,-74.646,5.615,-74.657,5.72,-74.641,5.829,-74.596,5.944,-74.576,6.055,-74.579,6.162,-74.531,6.265,-74.433,6.363,-74.385,6.465,-74.386,6.573,-74.317,6.667,-74.178,6.749,-74.053,6.848,-73.943,6.962,-73.9,7.078,-73.925,7.195,-73.907,7.385,-73.846,7.648,-73.83,7.846,-73.858,7.979,-73.852,8.071,-73.79,8.15],[-73.796,8.15,-73.765,8.329,-73.784,8.535,-73.843,8.827,-73.942,8.992,-74.083,9.03,-74.223,9.104,-74.361,9.213,-74.456,9.261,-74.507,9.249,-74.559,9.278,-74.611,9.347,-74.663,9.391,-74.742,9.418,-74.786,9.441,-74.795,9.494,-74.786,9.586,-74.806,9.691,-74.854,9.81,-74.86,9.909,-74.823,9.986,-74.84,10.06,-74.912,10.13,-74.892,10.275,-74.781,10.495,-74.727,10.684,-74.728,10.841,-74.761,10.972,-74.845,11.11]]},{"n":"Mahäna Nadï","z":5,"b":[80.625,20.053,86.245,21.863],"l":[[80.625,20.599,80.63,20.715,80.665,20.765,80.763,20.798,80.889,20.97,81.006,21.046,81.156,21.083,81.271,21.22,81.35,21.456,81.428,21.588,81.543,21.632,81.641,21.59,81.681,21.622,81.694,21.713,81.745,21.764,81.834,21.776,81.92,21.809,82.003,21.863,82.114,21.848,82.255,21.762,82.376,21.728,82.479,21.745,82.723,21.739,83.3,21.695],[83.857,21.52,83.949,21.454,83.943,21.337,83.83,21.055,83.832,20.962,83.866,20.907,83.932,20.86,84.046,20.846,84.21,20.867,84.31,20.847,84.346,20.787,84.444,20.73,84.603,20.674,84.708,20.615,84.757,20.552,84.922,20.48,85.344,20.357,85.665,20.443,85.832,20.445,85.952,20.393,86.048,20.311,86.159,20.143,86.177,20.094,86.245,20.053]]},{"n":"Malyy Yenisey","z":4,"b":[94.465,51.295,95.78,51.734],"l":[[95.78,51.295,95.558,51.476,95.23,51.585,94.465,51.734]]},{"n":"Mamberamo","z":5,"b":[137.815,-3.202,138.702,-1.484],"l":[[138.702,-3.191,138.668,-3.202,138.623,-3.198,138.598,-3.158,138.592,-3.027,138.557,-2.949,138.494,-2.925,138.453,-2.854,138.434,-2.736,138.407,-2.654,138.356,-2.587,138.221,-2.419,138.123,-2.342,138.018,-2.3,137.923,-2.223,137.838,-2.109,137.815,-2.027,137.852,-1.977,137.858,-1.909,137.834,-1.822,137.838,-1.769,137.852,-1.735,137.844,-1.699,137.849,-1.649,137.911,-1.484]]},{"n":"Mamoré","z":3,"b":[-65.304,-15.86,-64.664,-11.913],"l":[[-64.664,-15.86,-64.72,-15.799,-64.769,-15.676,-64.828,-15.469,-64.89,-15.323,-64.954,-15.237,-64.998,-15.157,-65.021,-15.083,-65.02,-15.025,-64.996,-14.981,-65.017,-14.892,-65.082,-14.756,-65.087,-14.717,-65.066,-14.697,-65.025,-14.615,-64.978,-14.446,-64.976,-14.327,-65.019,-14.257,-65.038,-14.199,-65.034,-14.153,-65.062,-14.086,-65.121,-13.997,-65.167,-13.952,-65.199,-13.949,-65.211,-13.917,-65.201,-13.855,-65.232,-13.781,-65.304,-13.694,-65.302,-13.533,-65.226,-13.299,-65.191,-13.204,-65.183,-13.144,-65.137,-13.112,-65.118,-13.053,-65.099,-13.022,-65.056,-12.998,-65.029,-12.937,-65.017,-12.839,-65.053,-12.694,-65.137,-12.501,-65.169,-12.39,-65.15,-12.361,-65.151,-12.335,-65.171,-12.311,-65.169,-12.292,-65.142,-12.268,-65.145,-12.235,-65.173,-12.165,-65.155,-12.126,-65.122,-12.111,-65.114,-12.081,-65.126,-12.027,-65.114,-11.986,-65.061,-11.955,-65.031,-11.913]]},{"n":"Mananantanana","z":5,"b":[45.653,-22.011,47.167,-21.275],"l":[[47.167,-22.011,47.164,-21.973,47.101,-21.939,46.976,-21.896,46.905,-21.852,46.879,-21.786,46.73,-21.736,46.675,-21.653,46.665,-21.543,46.637,-21.513,46.596,-21.473,46.574,-21.464,46.493,-21.38,46.345,-21.302,46.186,-21.275,46.016,-21.301,45.898,-21.348,45.829,-21.416,45.76,-21.444,45.653,-21.428]]},{"n":"Mangoky","z":5,"b":[43.542,-21.89,45.653,-21.316],"l":[[45.653,-21.428,45.514,-21.44,45.425,-21.501,45.293,-21.672,44.806,-21.724,44.565,-21.77,44.409,-21.828,44.302,-21.845,44.244,-21.818,44.166,-21.829,44.066,-21.876,43.986,-21.89,43.914,-21.872,43.895,-21.861,43.874,-21.838,43.785,-21.637,43.697,-21.501,43.542,-21.316]]},{"n":"Damqogkanbab","z":2.1,"b":[82.4,29.126,87.66,30.411],"l":[[82.4,30.411,82.723,30.365,82.825,30.371,82.886,30.336,82.939,30.261,83.164,30.126,83.761,29.833,83.925,29.761,83.983,29.711,83.987,29.659,84.014,29.617,84.063,29.586,84.189,29.555,84.392,29.525,84.501,29.452,84.517,29.335,84.634,29.254,84.854,29.209,85.014,29.221,85.115,29.291,85.226,29.313,85.409,29.274,85.592,29.204,85.867,29.195,86.295,29.225,86.734,29.209,87.184,29.148,87.472,29.126,87.66,29.152]]},{"n":"Marañón","z":4.7,"b":[-78.716,-9.913,-73.489,-4.433],"l":[[-76.617,-9.913,-76.711,-9.64,-76.765,-9.473,-76.766,-9.365,-76.733,-9.26,-76.835,-9.117,-77.073,-8.937,-77.224,-8.769,-77.29,-8.612,-77.346,-8.522,-77.392,-8.496,-77.437,-8.424,-77.483,-8.304,-77.534,-8.215,-77.592,-8.158,-77.621,-8.057,-77.622,-7.91,-77.719,-7.628,-77.909,-7.21,-78.01,-6.936,-78.02,-6.805,-78.062,-6.67,-78.138,-6.529,-78.219,-6.432,-78.306,-6.378,-78.374,-6.301,-78.45,-6.15,-78.568,-6.089,-78.643,-6.017,-78.716,-5.915,-78.703,-5.762,-78.603,-5.561,-78.528,-5.446,-78.479,-5.417,-78.456,-5.355,-78.459,-5.259,-78.434,-5.198,-78.382,-5.171,-78.366,-5.114,-78.387,-5.027,-78.382,-4.964,-78.35,-4.924,-78.335,-4.871,-78.336,-4.804,-78.305,-4.762,-78.242,-4.744,-78.207,-4.708,-78.202,-4.653,-78.163,-4.632,-78.091,-4.646,-77.958,-4.598,-77.763,-4.488,-77.623,-4.433,-77.538,-4.434,-77.473,-4.45,-77.43,-4.481,-77.408,-4.526,-77.41,-4.584,-77.387,-4.617,-77.342,-4.623,-77.333,-4.642,-77.344,-4.651,-77.338,-4.683,-77.307,-4.729,-77.282,-4.732,-77.263,-4.69,-77.169,-4.708,-77.002,-4.784,-76.832,-4.822,-76.659,-4.823,-76.566,-4.84,-76.552,-4.873,-76.511,-4.901,-76.442,-4.926,-76.352,-4.935,-76.24,-4.928,-76.173,-4.944,-76.137,-4.999,-75.841,-5.039,-75.723,-5.043,-75.686,-5.025,-75.654,-5.031,-75.629,-5.062,-75.559,-5.006,-75.445,-4.865,-75.259,-4.78,-75.002,-4.751,-74.847,-4.691,-74.796,-4.6,-74.722,-4.529,-74.625,-4.478,-74.549,-4.485,-74.492,-4.549,-74.413,-4.606,-74.312,-4.654,-74.247,-4.638,-74.218,-4.556,-74.177,-4.502,-74.125,-4.477,-74.053,-4.474,-73.962,-4.491,-73.888,-4.531,-73.829,-4.592,-73.724,-4.579,-73.489,-4.445]]},{"n":"Mekong","z":2.1,"b":[101.564,10.001,106.136,18.423],"l":[[104.951,11.591,104.951,11.508,104.976,11.472,105.026,11.453,105.041,11.428,105.023,11.395,105.044,11.212,105.105,10.879,105.311,10.535,105.831,10.001],[104.951,11.591,105.181,11.446,105.266,11.356,105.282,11.274,105.266,11.18,105.218,11.075,105.199,10.978,105.21,10.89,105.249,10.825,105.315,10.785,105.358,10.723,105.377,10.641,105.49,10.529,105.697,10.39,105.879,10.299,105.931,10.284,106.136,10.222],[101.564,17.821,101.687,17.889,101.744,17.953,101.775,18.033,101.819,18.065,101.875,18.046,101.947,18.081,102.035,18.17,102.101,18.211,102.148,18.204,102.232,18.149,102.352,18.046,102.459,17.985,102.553,17.965,102.598,17.927,102.596,17.87,102.617,17.833,102.661,17.818,102.68,17.824,102.675,17.852,102.718,17.892,102.807,17.946,102.899,17.977,102.991,17.986,103.051,18.029,103.091,18.138],[103.09,18.137,103.091,18.138],[103.091,18.138,103.092,18.141],[103.091,18.138,103.149,18.222,103.2,18.259,103.263,18.278,103.28,18.305,103.249,18.339,103.252,18.373,103.288,18.408,103.367,18.423,103.488,18.418,103.63,18.383,103.792,18.316,103.899,18.295,103.95,18.319,104.049,18.217,104.196,17.988,104.323,17.816,104.428,17.699,104.539,17.609,104.656,17.547,104.74,17.462,104.816,17.3,104.759,17.077,104.744,16.884,104.751,16.648,104.819,16.466,104.95,16.34,105.026,16.238,105.047,16.16,105.149,16.094,105.331,16.038,105.406,15.987,105.376,15.942,105.373,15.89,105.399,15.83,105.462,15.78,105.562,15.741,105.622,15.7,105.641,15.657,105.639,15.586,105.616,15.488,105.574,15.413,105.513,15.361,105.52,15.331,105.595,15.324,105.666,15.273,105.782,15.121,105.868,15.033,105.907,14.944,105.926,14.826,105.915,14.713,105.873,14.606,105.86,14.495,105.873,14.38,105.864,14.267,105.832,14.158,105.855,14.07,105.934,14.005,105.981,13.948,105.996,13.9,105.996,13.852,105.982,13.803,105.991,13.752,106.021,13.698,106.005,13.609,105.94,13.486,105.918,13.405,105.949,13.35,106.023,13.196,106.025,13.061,105.981,12.893,105.978,12.683,106.018,12.428,105.985,12.289,105.88,12.266,105.763,12.264,105.636,12.284,105.543,12.204,105.486,12.025,105.389,11.936,105.252,11.939,105.168,11.92,105.137,11.88,105.09,11.855,105.025,11.845,105,11.822,105.013,11.786,105.002,11.747,104.965,11.706,104.948,11.662,104.951,11.591]]},{"n":"Mendoza","z":5,"b":[-70.136,-32.355,-69.418,-31.283],"l":[[-70.136,-32.238,-70.043,-32.334,-69.97,-32.355,-69.889,-32.332,-69.833,-32.281,-69.803,-32.202,-69.796,-32.144,-69.81,-32.106,-69.787,-32.033,-69.698,-31.871,-69.589,-31.782,-69.521,-31.693,-69.458,-31.574,-69.427,-31.455,-69.429,-31.304,-69.418,-31.283]]},{"n":"Meta","z":5,"b":[-73.917,3.958,-67.423,6.29],"l":[[-73.872,4.67,-73.909,4.608,-73.917,4.541,-73.908,4.447,-73.886,4.365,-73.831,4.258,-73.741,4.193,-73.719,4.142,-73.733,4.083,-73.571,4.022,-73.233,3.96,-73.034,3.958,-72.976,4.014,-72.927,4.1,-72.885,4.215,-72.836,4.282,-72.748,4.313,-72.479,4.332,-72.373,4.356,-72.34,4.392,-72.28,4.42,-72.193,4.44,-72.116,4.438,-72.047,4.412,-71.962,4.443,-71.86,4.533,-71.661,4.637,-71.364,4.756,-71.174,4.844,-71.09,4.901,-71.027,4.977,-70.982,5.071,-70.894,5.167,-70.762,5.265,-70.687,5.336,-70.671,5.382,-70.52,5.465,-70.234,5.585,-70.03,5.743,-69.907,5.939,-69.768,6.043,-69.611,6.056,-69.507,6.077,-69.432,6.122,-69.357,6.148,-69.311,6.138,-69.268,6.1,-69.195,6.115,-69.09,6.184,-68.937,6.198,-68.736,6.157,-68.472,6.157,-68.143,6.198,-67.939,6.242,-67.859,6.29,-67.727,6.285,-67.423,6.202]]},{"n":"Min","z":5,"b":[103.719,28.779,104.62,29.537],"l":[[103.719,29.537,103.767,29.517,103.832,29.386,103.866,29.316,103.857,29.29,103.926,29.224,104.145,29.062,104.251,28.996,104.291,28.948,104.302,28.896,104.38,28.849,104.62,28.779]]},{"n":"Mississippi","z":2,"b":[-95.026,28.981,-89.016,47.482],"l":[[-89.376,28.981,-89.257,29.155,-89.016,29.203],[-89.261,29.155,-89.289,29.178],[-89.289,29.178,-89.181,29.336],[-89.289,29.178,-89.433,29.294,-89.519,29.347,-89.616,29.387,-89.666,29.416,-89.67,29.444,-89.714,29.486,-89.758,29.506,-89.869,29.608,-89.973,29.689,-90.007,29.754,-90.012,29.811,-89.987,29.86,-89.958,29.876,-89.932,29.878,-89.923,29.888,-89.923,29.903,-89.933,29.914,-90.041,29.932,-90.25,29.943,-90.39,29.976,-90.464,30.032,-90.582,30.048,-90.744,30.023,-90.849,30.04,-90.878,30.075,-90.897,30.098,-90.944,30.127,-90.99,30.126,-91.021,30.147,-91.038,30.181,-91.074,30.204,-91.11,30.209,-91.133,30.227,-91.13,30.248,-91.148,30.268,-91.179,30.285,-91.191,30.307,-91.187,30.328,-91.195,30.352,-91.226,30.368,-91.235,30.402,-91.218,30.479,-91.226,30.505,-91.258,30.534,-91.257,30.565,-91.276,30.615,-91.338,30.706,-91.388,30.744,-91.423,30.753,-91.529,30.756,-91.557,30.78,-91.547,30.815,-91.523,30.854,-91.526,30.871,-91.546,30.908,-91.618,30.938,-91.646,30.952,-91.66,30.972,-91.657,30.996,-91.653,31.03,-91.611,31.065,-91.596,31.11,-91.631,31.218,-91.618,31.277,-91.559,31.288,-91.529,31.345,-91.529,31.449,-91.506,31.519,-91.459,31.555,-91.415,31.64,-91.373,31.774,-91.286,31.884,-91.157,31.971,-91.093,32.046,-91.089,32.104,-91.075,32.131,-91.048,32.146,-91.005,32.186,-90.948,32.24,-90.938,32.277,-90.94,32.303,-90.945,32.331,-90.99,32.381,-91.059,32.424,-91.104,32.49,-91.101,32.541,-91.116,32.586,-91.15,32.625,-91.142,32.661,-91.094,32.694,-91.092,32.726,-91.136,32.756,-91.136,32.809,-91.093,32.885,-91.096,32.938,-91.144,32.967,-91.153,33.041,-91.107,33.218,-91.117,33.241,-91.163,33.294,-91.168,33.359,-91.141,33.439,-91.154,33.5,-91.193,33.523,-91.203,33.547,-91.215,33.583,-91.173,33.617,-91.17,33.647,-91.207,33.676,-91.184,33.716,-91.1,33.767,-91.077,33.797,-91.053,33.828,-91.038,33.901,-91.05,33.945,-91.07,33.974,-91.06,33.989,-91.043,34.003,-90.943,34.03,-90.915,34.049,-90.909,34.072,-90.94,34.129,-90.91,34.189,-90.817,34.252,-90.768,34.308,-90.762,34.356,-90.718,34.392,-90.635,34.414,-90.584,34.504,-90.564,34.661,-90.539,34.728,-90.497,34.738,-90.475,34.76,-90.468,34.805,-90.416,34.852,-90.32,34.899,-90.282,34.945,-90.293,34.97,-90.294,35.004,-90.256,35.046,-90.142,35.114,-90.093,35.204,-90.108,35.314,-90.066,35.414,-89.967,35.503,-89.927,35.592,-89.943,35.679,-89.895,35.751,-89.78,35.806,-89.764,35.828,-89.768,35.845,-89.77,35.864,-89.769,35.872,-89.764,35.889,-89.738,35.899,-89.704,35.907,-89.689,35.921,-89.689,35.944,-89.709,35.983,-89.698,36.03,-89.642,36.105,-89.634,36.168,-89.675,36.221,-89.654,36.248,-89.628,36.255,-89.595,36.259,-89.584,36.272,-89.584,36.288,-89.612,36.322,-89.604,36.352,-89.555,36.373,-89.541,36.427,-89.56,36.513,-89.555,36.541,-89.55,36.553,-89.536,36.555,-89.52,36.555,-89.497,36.529,-89.485,36.498,-89.471,36.489,-89.452,36.498,-89.397,36.588,-89.351,36.611,-89.331,36.616,-89.28,36.6,-89.256,36.594,-89.203,36.626,-89.156,36.716,-89.13,36.811,-89.122,36.912,-89.154,36.992,-89.225,37.053,-89.272,37.08,-89.281,37.078,-89.292,37.071,-89.296,37.052,-89.293,37.038,-89.286,37.023,-89.29,37.013,-89.301,37.012,-89.352,37.036,-89.414,37.102,-89.487,37.213,-89.503,37.301,-89.462,37.366,-89.462,37.434,-89.503,37.506,-89.523,37.576,-89.523,37.645,-89.623,37.747,-89.823,37.884,-89.97,37.969,-90.064,38.001,-90.174,38.069,-90.299,38.174,-90.366,38.254,-90.374,38.31,-90.317,38.458,-90.151,38.781,-90.132,38.819,-90.127,38.833,-90.134,38.847,-90.15,38.86,-90.22,38.898,-90.354,38.93,-90.478,38.948,-90.577,38.91,-90.664,38.987,-90.737,39.182,-90.919,39.387,-91.21,39.603,-91.379,39.749,-91.426,39.827,-91.467,39.943,-91.502,40.098,-91.5,40.226,-91.442,40.379,-91.391,40.397,-91.381,40.448,-91.394,40.537,-91.339,40.604,-91.215,40.65,-91.102,40.75,-91,40.905,-90.954,41.016,-90.964,41.083,-91.001,41.146,-91.067,41.206,-91.082,41.28,-91.048,41.369,-90.939,41.426,-90.754,41.45,-90.587,41.495,-90.437,41.56,-90.348,41.632,-90.319,41.71,-90.276,41.77,-90.218,41.813,-90.177,41.884,-90.152,41.982,-90.149,42.054,-90.168,42.1,-90.233,42.154,-90.347,42.215,-90.41,42.272,-90.425,42.326,-90.485,42.383,-90.59,42.445,-90.657,42.521,-90.688,42.61,-90.789,42.677,-90.96,42.72,-91.07,42.789,-91.118,42.882,-91.148,42.986,-91.161,43.102,-91.148,43.2,-91.107,43.278,-91.117,43.331,-91.178,43.359,-91.219,43.395,-91.238,43.44,-91.278,43.797,-91.32,43.936,-91.383,43.991,-91.515,44.054,-91.716,44.126,-91.841,44.194,-91.912,44.289,-92.055,44.4,-92.149,44.445,-92.24,44.462,-92.313,44.498,-92.367,44.552,-92.442,44.587,-92.537,44.601,-92.644,44.645,-92.82,44.758,-92.963,44.79,-93.03,44.838,-93.07,44.912,-93.119,44.937,-93.175,44.913,-93.232,44.963,-93.291,45.087,-93.349,45.167,-93.408,45.202,-93.936,45.389,-94.063,45.439,-94.133,45.496,-94.189,45.577,-94.212,45.632,-94.202,45.662,-94.238,45.714,-94.32,45.789,-94.371,45.853,-94.389,45.906,-94.386,45.972,-94.363,46.051,-94.366,46.125,-94.394,46.194,-94.333,46.284,-94.183,46.396,-94.091,46.482,-94.057,46.543,-93.959,46.569,-93.798,46.559,-93.692,46.586,-93.641,46.651,-93.572,46.698,-93.486,46.725,-93.404,46.807,-93.325,46.944,-93.309,47.023,-93.357,47.044,-93.402,47.09,-93.445,47.16,-93.509,47.211,-93.593,47.243,-93.676,47.249,-93.759,47.228,-93.798,47.242,-93.795,47.29,-93.829,47.312,-93.901,47.306,-93.976,47.336,-94.094,47.433],[-95.026,47.156,-94.968,47.318,-94.877,47.451,-94.794,47.482,-94.637,47.44],[-94.508,47.457,-94.326,47.406]]},{"n":"Missouri","z":2,"b":[-111.989,38.579,-90.133,48.15],"l":[[-98.56,43.06,-98.143,42.852,-97.965,42.806,-97.881,42.85,-97.698,42.875,-97.417,42.881,-97.239,42.861,-97.164,42.813,-97.02,42.76,-96.805,42.701,-96.676,42.638,-96.633,42.569,-96.558,42.525,-96.451,42.506,-96.384,42.418,-96.356,42.26,-96.295,42.133,-96.201,42.036,-96.154,41.965,-96.154,41.92,-96.131,41.872,-96.087,41.821,-96.069,41.734,-96.078,41.611,-96.059,41.546,-96.011,41.538,-95.991,41.52,-95.997,41.494,-95.982,41.474,-95.944,41.461,-95.933,41.425,-95.95,41.367,-95.936,41.331,-95.892,41.316,-95.884,41.288,-95.911,41.245,-95.907,41.212,-95.871,41.188,-95.847,41.08,-95.834,40.889,-95.838,40.778,-95.859,40.745,-95.826,40.676,-95.74,40.571,-95.685,40.466,-95.662,40.363,-95.607,40.295,-95.521,40.265,-95.464,40.198,-95.434,40.096,-95.362,40.01,-95.116,39.872,-95.056,39.871,-94.984,39.891,-94.93,39.861,-94.893,39.782,-94.899,39.741,-94.946,39.739,-95.005,39.692,-95.075,39.597,-95.058,39.509,-94.953,39.426,-94.882,39.343,-94.847,39.259,-94.778,39.194,-94.622,39.124,-94.504,39.124,-94.421,39.151,-94.334,39.204,-94.239,39.207,-94.136,39.159,-94.028,39.154,-93.915,39.19,-93.773,39.211,-93.604,39.218,-93.513,39.237,-93.501,39.27,-93.469,39.271,-93.418,39.24,-93.343,39.27,-93.243,39.361,-93.108,39.359,-92.939,39.265,-92.877,39.192,-92.922,39.141,-92.933,39.087,-92.909,39.03,-92.816,38.993,-92.654,38.976,-92.503,38.89,-92.293,38.659,-92.124,38.586,-91.978,38.598,-91.801,38.67,-91.619,38.706,-91.433,38.707,-91.238,38.667,-91.034,38.585,-90.872,38.579,-90.752,38.648,-90.661,38.681,-90.598,38.681,-90.513,38.732,-90.404,38.835,-90.316,38.86,-90.256,38.845,-90.133,38.819],[-100.394,44.436,-100.372,44.415,-100.345,44.374,-100.225,44.329,-100.019,44.286,-99.922,44.245,-99.934,44.205,-99.921,44.171,-99.883,44.142,-99.804,44.122,-99.685,44.113,-99.647,44.132,-99.691,44.179,-99.684,44.205,-99.626,44.208,-99.587,44.178,-99.568,44.115,-99.51,44.06,-99.364,43.991],[-111.699,46.663,-111.758,46.671,-111.814,46.711,-111.885,46.786,-111.913,46.837,-111.898,46.863,-111.919,46.916,-111.976,46.997,-111.989,47.057,-111.959,47.096,-111.616,47.342,-111.454,47.415,-111.348,47.412,-111.293,47.439,-111.29,47.498,-111.237,47.541,-111.137,47.567,-111.037,47.632,-110.938,47.736,-110.856,47.784,-110.792,47.777,-110.617,47.843,-110.331,47.984,-110.167,48.043,-110.124,48.021,-110.089,47.968,-110.062,47.885,-109.995,47.816,-109.886,47.762,-109.609,47.755,-109.164,47.794,-108.935,47.795,-108.922,47.758,-108.874,47.718,-108.788,47.677,-108.607,47.643,-108.191,47.602],[-101.4,47.506,-101.423,47.484,-101.407,47.431,-101.362,47.339,-101.248,47.282,-101.066,47.26,-100.979,47.231,-100.987,47.196,-100.967,47.148,-100.917,47.087,-100.902,47.034,-100.921,46.99,-100.892,46.904,-100.777,46.714],[-106.406,48.021,-106.367,48.058,-106.221,48.058,-105.956,48.033,-105.729,48.042,-105.542,48.086,-105.34,48.1,-105.125,48.083,-104.978,48.095,-104.899,48.136,-104.75,48.15,-104.529,48.137,-104.277,48.093,-103.996,48.02,-103.835,47.994,-103.772,48.027]]},{"n":"Mitchell","z":5,"b":[141.581,-16.484,143.469,-15.195],"l":[[143.469,-16.484,143.138,-16.38,142.75,-16.104,142.565,-16.001,142.452,-15.981,142.365,-15.947,142.271,-15.874,142.043,-15.473,141.877,-15.306,141.751,-15.245,141.581,-15.195]]},{"n":"Black Volta","z":4.7,"b":[-5.263,8.16,-1.713,12.738],"l":[[-5.263,11.139,-5.185,11.124,-5.126,11.154,-5.059,11.224,-4.99,11.264,-4.879,11.282,-4.715,11.406,-4.639,11.432,-4.596,11.402,-4.539,11.413,-4.469,11.467,-4.382,11.507,-4.277,11.533,-4.124,11.697,-3.922,11.998,-3.82,12.176,-3.815,12.232,-3.784,12.296,-3.725,12.366,-3.666,12.401,-3.607,12.402,-3.569,12.477,-3.55,12.628,-3.511,12.715,-3.45,12.738,-3.406,12.72,-3.379,12.663,-3.336,12.645,-3.278,12.666,-3.249,12.64,-3.248,12.567,-3.208,12.5,-3.126,12.44,-3.023,12.397,-2.899,12.372,-2.849,12.339,-2.875,12.298,-2.864,12.235,-2.816,12.151,-2.824,12.098,-2.886,12.075,-2.896,12.019,-2.853,11.927,-2.854,11.861,-2.901,11.819,-2.883,11.779,-2.802,11.743,-2.79,11.606,-2.849,11.369,-2.858,11.242,-2.817,11.226,-2.804,11.165,-2.83,11.008,-2.907,10.728,-2.915,10.592,-2.878,10.508,-2.837,10.455,-2.791,10.432,-2.787,10.402,-2.823,10.363,-2.82,10.323,-2.777,10.282,-2.767,10.238,-2.788,10.193,-2.783,10.083,-2.751,9.91,-2.75,9.797,-2.781,9.746,-2.766,9.658,-2.706,9.534,-2.686,9.432,-2.706,9.351,-2.702,9.302,-2.674,9.283,-2.689,9.219,-2.747,9.11,-2.747,9.045,-2.69,9.025,-2.649,8.957,-2.625,8.84,-2.557,8.731,-2.446,8.631,-2.364,8.514,-2.311,8.38,-2.247,8.274,-2.17,8.196,-2.087,8.16,-2,8.166,-1.935,8.192,-1.892,8.237,-1.845,8.255,-1.793,8.244,-1.768,8.26,-1.77,8.304,-1.797,8.328,-1.851,8.333,-1.887,8.357,-1.904,8.398,-1.899,8.47,-1.871,8.573,-1.821,8.625,-1.713,8.631]]},{"n":"Murray","z":3,"b":[139.289,-36.967,148.197,-33.999],"l":[[148.005,-36.967,148.162,-36.73,148.197,-36.625,148.147,-36.546,148.034,-36.181,147.975,-36.046,147.931,-36.019,147.762,-35.956,147.674,-35.954,147.596,-35.982,147.538,-35.984,147.502,-35.959,147.458,-35.954,147.407,-35.971,147.371,-36.004,147.35,-36.052,147.323,-36.07,147.272,-36.049],[147.045,-36.108,146.896,-36.1,146.761,-36.067,146.59,-36.005,146.47,-35.994,146.403,-36.033,146.331,-36.054,146.216,-36.056],[146.028,-36.015,145.815,-35.977,145.692,-35.931,145.588,-35.863,145.432,-35.835,145.224,-35.846,145.083,-35.862,145.008,-35.884,144.962,-35.923,144.943,-35.978,144.944,-36.029,144.965,-36.075,144.916,-36.107,144.799,-36.126,144.668,-36.088,144.524,-35.994,144.427,-35.905,144.378,-35.82,144.29,-35.73,144.163,-35.634,143.971,-35.528,143.714,-35.411,143.585,-35.327,143.582,-35.275,143.54,-35.234,143.459,-35.203,143.404,-35.168,143.377,-35.127,143.363,-35.032,143.361,-34.882,143.325,-34.788,143.221,-34.733,142.965,-34.693,142.857,-34.655,142.812,-34.607,142.774,-34.588,142.742,-34.598,142.705,-34.649,142.663,-34.741,142.619,-34.783,142.571,-34.775,142.507,-34.712,142.426,-34.593,142.388,-34.485,142.394,-34.387,142.373,-34.333,142.328,-34.322,142.287,-34.286,142.252,-34.223,142.158,-34.173,141.926,-34.117,141.761,-34.112,141.666,-34.137,141.586,-34.191,141.47,-34.183,141.318,-34.112,141.192,-34.073,141.094,-34.066,141.026,-34.043,140.997,-34.016,140.948,-33.999,140.914,-34.011,140.806,-34.062,140.763,-34.096,140.784,-34.113,140.787,-34.145,140.773,-34.189,140.715,-34.246,140.613,-34.315,140.565,-34.375,140.571,-34.426,140.533,-34.433,140.452,-34.394,140.392,-34.324,140.352,-34.223,140.248,-34.175,140.08,-34.181,139.979,-34.176,139.944,-34.16,139.933,-34.136,139.93,-34.103,139.886,-34.074,139.801,-34.057,139.753,-34.051,139.721,-34.077,139.657,-34.148,139.607,-34.383,139.603,-34.544,139.645,-34.631,139.635,-34.695,139.576,-34.734,139.57,-34.787,139.617,-34.852,139.592,-34.878,139.495,-34.864,139.4,-34.884,139.307,-34.939,139.289,-34.981,139.347,-35.011,139.355,-35.048,139.313,-35.093,139.299,-35.133,139.31,-35.168,139.351,-35.207,139.423,-35.25,139.435,-35.297,139.374,-35.365,139.303,-35.399]]},{"n":"N. Fork Kuskokwim","z":5,"b":[-154.721,63.079,-151.674,64.326],"l":[[-151.674,64.301,-152.207,64.326,-152.399,64.235,-152.499,64.161,-152.568,64.072,-152.597,64.008,-152.585,63.97,-152.791,63.884,-153.215,63.751,-153.455,63.649,-153.513,63.577,-153.605,63.515,-153.733,63.464,-153.85,63.403,-153.959,63.332,-154.009,63.286,-154.001,63.265,-154.178,63.211,-154.721,63.079]]},{"n":"White Volta","z":5,"b":[-2.135,9.117,-0.336,14.01],"l":[[-2.083,14.01,-2.055,13.681,-2.108,13.566,-2.104,13.473,-2.063,13.365,-2.069,13.253,-2.123,13.138,-2.135,13.068,-2.105,13.042,-1.989,13.038,-1.787,13.055,-1.628,13.032,-1.511,12.966,-1.411,12.946,-1.327,12.97,-1.262,12.962,-1.216,12.921,-1.183,12.836,-1.163,12.708,-1.141,12.651,-1.115,12.667,-1.096,12.658,-1.083,12.624,-1.092,12.333,-1.045,12.153,-0.94,11.975,-0.858,11.867,-0.798,11.829,-0.771,11.754,-0.778,11.64,-0.724,11.557,-0.609,11.505,-0.534,11.454,-0.499,11.405,-0.492,11.37,-0.512,11.35,-0.483,11.291,-0.406,11.194,-0.373,11.081,-0.384,10.954,-0.374,10.867,-0.344,10.82,-0.339,10.784,-0.359,10.76,-0.361,10.72,-0.336,10.638,-0.608,10.546,-0.7,10.546,-0.703,10.608,-0.724,10.64,-0.765,10.642,-0.817,10.604,-0.88,10.526,-0.937,10.499,-0.988,10.522,-1.027,10.522,-1.054,10.497,-1.067,10.445,-1.067,10.367,-1.05,10.303,-1.018,10.253,-1.024,10.206,-1.068,10.161,-1.07,10.117,-1.031,10.074,-1,9.961,-0.977,9.777,-1.021,9.696,-1.13,9.715,-1.237,9.686,-1.342,9.608,-1.385,9.523,-1.366,9.431,-1.328,9.359,-1.27,9.307,-1.242,9.254,-1.246,9.199,-1.223,9.158,-1.152,9.117]]},{"n":"Nanpan","z":4,"b":[103.053,23.802,106.944,25.879],"l":[[103.922,25.879,103.855,25.801,103.836,25.725,103.846,25.574,103.883,25.341,103.867,25.219,103.81,25.129,103.713,25.048,103.488,24.939,103.448,24.935,103.416,24.955,103.392,25,103.326,25.013,103.219,24.994,103.165,24.944,103.165,24.863,103.132,24.774,103.064,24.677,103.053,24.609,103.098,24.57,103.105,24.52,103.074,24.458,103.08,24.321,103.146,24.004,103.169,23.87,103.195,23.818,103.232,23.802,103.262,23.81,103.288,23.84,103.376,23.863,103.527,23.88,103.61,23.916,103.626,23.97,103.619,24.02,103.59,24.065,103.591,24.11,103.623,24.153,103.674,24.185,103.746,24.204,103.825,24.27,103.912,24.385,103.987,24.442,104.052,24.441,104.151,24.472,104.353,24.562,104.537,24.741,104.681,24.659,104.8,24.673,104.941,24.756,105.056,24.85,105.144,24.956,105.208,24.991,105.247,24.956,105.308,24.937,105.39,24.935,105.45,24.905,105.489,24.847,105.583,24.79,105.731,24.735,105.833,24.712,105.888,24.723,105.942,24.707,105.994,24.664,106.065,24.689,106.154,24.783,106.188,24.862,106.166,24.928,106.231,24.979,106.381,25.017,106.509,25.07,106.614,25.139,106.742,25.189,106.874,25.215,106.944,25.225]]},{"n":"Narmada","z":5,"b":[73.112,21.75,81.574,23.129],"l":[[81.574,22.754,81.28,22.877,81.096,22.956,81.027,22.969,81.01,22.943,80.973,22.936,80.917,22.947,80.877,22.979,80.854,23.03,80.832,23.04,80.812,23.01,80.732,22.985,80.59,22.964,80.509,22.925,80.487,22.87,80.484,22.788,80.498,22.678,80.486,22.611,80.448,22.586,80.411,22.584,80.373,22.602,80.333,22.658,80.29,22.75,80.217,22.79,80.06,22.775,79.963,22.852,79.933,22.934,79.938,23.047,79.834,23.112,79.622,23.129,79.419,23.122,79.224,23.091,79.116,23.061,79.096,23.03,78.997,23.024,78.82,23.04,78.672,23.038,78.553,23.016,78.442,22.97,78.338,22.899,78.257,22.868,78.2,22.878,78.158,22.859,78.133,22.812,78.075,22.794,77.984,22.803,77.919,22.825,77.88,22.857,77.79,22.825,77.65,22.729,77.457,22.639,77.086,22.514,76.884,22.458,76.803,22.407,76.776,22.342,76.714,22.289,76.615,22.248,76.53,22.254,76.458,22.309,76.306,22.299,76.071,22.225,75.608,22.146,74.567,22.021,74.466,22.035,74.387,22.012,74.295,21.957,74.202,21.932,74.108,21.938,73.985,21.914,73.832,21.862,73.703,21.872,73.596,21.944,73.518,21.981,73.469,21.985,73.417,21.961,73.363,21.911,73.311,21.897,73.258,21.919,73.235,21.908,73.242,21.866,73.218,21.844,73.163,21.844,73.135,21.821,73.137,21.783,73.112,21.75]]},{"n":"Naryn","z":4.7,"b":[70.355,40.435,78.019,41.88],"l":[[72.654,41.667,72.349,41.497,72.222,41.376,72.171,41.246,72.053,41.117,71.87,40.988,71.597,40.907,71.234,40.872,71.101,40.842,71.062,40.822,71.026,40.809,70.965,40.796,70.863,40.756,70.698,40.661,70.355,40.435],[77.737,41.88,77.978,41.869,78.019,41.836,77.901,41.75,77.763,41.624,77.673,41.567,77.586,41.536,77.278,41.518,76.75,41.51,76.388,41.492,76.092,41.449,75.882,41.459,75.747,41.442,75.617,41.4,75.478,41.397,75.33,41.431,75.066,41.446,74.686,41.443,74.436,41.424,74.317,41.39,74.18,41.4,74.025,41.456,73.946,41.519,73.942,41.591,73.922,41.649,73.886,41.696,73.797,41.74,73.655,41.783,73.513,41.797,73.298,41.776]]},{"n":"Nederrijn","z":4,"b":[5.331,51.872,6.039,51.981],"l":[[6.039,51.872,5.922,51.961,5.747,51.981,5.331,51.963]]},{"n":"Negro","z":3,"b":[-70.336,-41.047,-59.897,2.833],"l":[[-68.014,-38.975,-67.757,-39.052,-67.598,-39.074,-67.453,-39.069,-67.314,-39.085,-67.18,-39.123,-66.987,-39.129,-66.735,-39.104,-66.531,-39.1,-66.373,-39.117,-66.224,-39.154,-66.084,-39.212,-65.931,-39.248,-65.765,-39.261,-65.629,-39.331,-65.521,-39.457,-65.434,-39.593,-65.366,-39.74,-65.234,-39.83,-65.037,-39.862,-64.899,-39.91,-64.82,-39.975,-64.728,-40.025,-64.622,-40.062,-64.503,-40.078,-64.371,-40.074,-64.172,-40.166,-63.905,-40.354,-63.738,-40.455,-63.67,-40.469,-63.574,-40.534,-63.388,-40.709,-63.174,-40.743,-63.028,-40.826,-62.86,-40.948,-62.798,-41.047],[-70.336,2.058,-69.967,2.11,-69.683,2.159,-69.526,2.204,-69.401,2.262,-69.252,2.271,-69.079,2.23,-68.932,2.211,-68.81,2.215,-68.701,2.264,-68.603,2.36,-68.497,2.379,-68.385,2.321,-68.277,2.372,-68.175,2.534,-68.113,2.606,-68.093,2.588,-68.068,2.608,-68.038,2.665,-67.955,2.73,-67.859,2.794,-67.766,2.833,-67.667,2.8,-67.619,2.794,-67.597,2.769,-67.568,2.69,-67.535,2.677,-67.486,2.644,-67.392,2.56,-67.312,2.472,-67.253,2.429,-67.211,2.39,-67.198,2.333,-67.215,2.275,-67.165,2.143,-67.131,2.101,-67.114,2.051,-67.131,2,-67.09,1.94,-66.988,1.68,-66.982,1.601,-66.958,1.564,-66.931,1.458,-66.884,1.358,-66.896,1.29,-66.876,1.223,-66.86,1.022,-66.877,0.938,-66.921,0.903,-67.008,0.887,-67.137,0.889,-67.231,0.768,-67.32,0.403,-67.277,0.156,-67.201,0.008,-67.077,-0.122,-66.984,-0.18,-66.922,-0.164,-66.772,-0.216,-66.535,-0.335,-66.373,-0.377,-66.285,-0.343,-66.172,-0.324,-66.034,-0.322,-65.942,-0.303,-65.896,-0.268,-65.84,-0.264,-65.772,-0.291,-65.681,-0.301,-65.566,-0.292,-65.447,-0.316,-65.261,-0.401,-65.005,-0.457,-64.798,-0.449,-64.43,-0.367,-64.152,-0.314,-63.972,-0.304,-63.796,-0.32,-63.633,-0.382,-63.481,-0.492,-63.31,-0.585,-63.025,-0.702,-62.879,-0.874,-62.73,-0.985,-62.432,-1.146,-61.933,-1.377,-61.881,-1.389,-61.799,-1.389,-61.717,-1.401,-61.62,-1.449,-61.538,-1.53,-61.471,-1.642,-61.437,-1.736,-61.435,-1.814,-61.355,-1.926,-61.197,-2.074,-61.142,-2.15,-61.092,-2.218,-61.04,-2.36,-60.903,-2.553,-60.571,-2.922,-60.451,-3.028,-60.36,-3.065,-60.26,-3.069,-60.158,-3.096,-60.052,-3.145,-59.976,-3.158,-59.897,-3.128]]},{"n":"Nelson","z":5,"b":[-98.012,54.082,-92.802,56.928],"l":[[-97.731,54.082,-97.595,54.241,-97.569,54.319,-97.615,54.373,-97.622,54.429,-97.591,54.487,-97.629,54.53,-97.791,54.571],[-97.901,54.705,-97.924,54.774,-97.957,54.807,-98.008,54.826,-98.012,54.863,-97.946,54.948],[-97.254,55.236,-97.074,55.249,-96.954,55.302,-96.833,55.4,-96.768,55.483,-96.76,55.551,-96.704,55.671,-96.602,55.843,-96.552,55.962,-96.553,56.063],[-95.842,56.291,-95.431,56.339],[-94.779,56.394,-94.344,56.421,-93.996,56.549,-93.591,56.788,-93.262,56.909,-92.882,56.915,-92.802,56.928]]},{"n":"Neuquén","z":4,"b":[-70.803,-38.975,-68.014,-36.529],"l":[[-70.803,-36.529,-70.7,-36.909,-70.648,-37.077,-70.648,-37.149,-70.681,-37.182,-70.698,-37.227,-70.699,-37.282,-70.675,-37.325,-70.626,-37.355,-70.51,-37.375,-70.325,-37.387,-70.199,-37.42,-70.13,-37.474,-70.077,-37.539,-70.04,-37.615,-69.991,-37.658,-69.931,-37.67,-69.886,-37.767,-69.857,-37.947,-69.809,-38.078,-69.74,-38.159,-69.7,-38.245,-69.687,-38.337,-69.604,-38.435,-69.452,-38.538,-69.325,-38.592,-69.224,-38.597,-69.102,-38.543,-68.961,-38.43,-68.849,-38.371,-68.769,-38.366,-68.603,-38.447,-68.351,-38.614,-68.173,-38.766,-68.014,-38.975]]},{"n":"Neva","z":4.7,"b":[30.157,59.811,31.043,59.951],"l":[[31.043,59.951,31.014,59.933,30.982,59.882,30.906,59.843,30.795,59.811,30.652,59.835,30.477,59.917,30.347,59.947,30.22,59.916,30.157,59.904]]},{"n":"Niagara","z":3,"b":[-79.066,43.106,-79.059,43.278],"l":[[-79.059,43.278,-79.066,43.106]]},{"n":"Niger","z":3,"b":[-10.816,4.291,6.776,17.057],"l":[[6.266,4.991,6.224,4.919,6.131,4.844,6.078,4.71,6.055,4.42,6.077,4.291],[6.266,4.991,6.271,4.432],[6.455,5.363,6.374,5.346,6.302,5.316,6.24,5.274,6.21,5.235,6.21,5.201,6.17,5.164,6.09,5.124,5.992,5.13,5.876,5.182,5.738,5.191,5.476,5.154],[6.455,5.363,6.394,5.301,6.349,5.256,6.33,5.221,6.333,5.196,6.358,5.18,6.354,5.153,6.321,5.114,6.292,5.095,6.266,5.095,6.257,5.061,6.266,4.991],[6.557,5.542,6.606,5.386,6.587,5.301,6.522,5.235,6.474,5.156,6.443,5.065,6.461,5.003,6.526,4.97,6.573,4.908,6.599,4.816,6.643,4.729,6.709,4.607,6.715,4.501,6.71,4.452,6.71,4.425,6.715,4.342],[-10.732,9.09,-10.665,9.154,-10.622,9.235,-10.58,9.355,-10.583,9.453,-10.656,9.566,-10.642,9.75,-10.684,9.915,-10.779,10.121,-10.816,10.248,-10.796,10.294,-10.747,10.322,-10.668,10.331,-10.634,10.359,-10.644,10.403,-10.613,10.426,-10.541,10.426,-10.481,10.452,-10.433,10.503,-10.363,10.524,-10.271,10.513,-10.218,10.491,-10.199,10.478,-10.174,10.472,-10.151,10.482,-10.053,10.569,-9.97,10.608,-9.901,10.602,-9.866,10.625,-9.848,10.642,-9.801,10.662,-9.658,10.678,-9.535,10.781,-9.433,10.969,-9.343,11.065,-9.268,11.067,-9.199,11.097,-9.136,11.154,-9.117,11.239,-9.14,11.352,-9.089,11.419,-8.962,11.438,-8.852,11.505,-8.759,11.62,-8.651,11.719,-8.527,11.801,-8.433,11.905,-8.367,12.031,-8.281,12.144,-8.175,12.243,-8.098,12.375,-8.051,12.539,-7.964,12.627,-7.837,12.636,-7.666,12.747,-7.453,12.958,-7.279,13.104,-7.078,13.224,-6.949,13.325,-6.788,13.362,-6.551,13.368,-6.367,13.409,-6.237,13.484,-6.146,13.572,-6.094,13.672,-6.021,13.71,-5.902,13.719,-5.785,13.745,-5.595,13.887,-5.435,13.962,-5.305,13.969,-5.214,14.008,-5.164,14.079,-4.996,14.155,-4.712,14.236,-4.549,14.266,-4.487,14.274,-4.414,14.313,-4.201,14.551,-4.117,14.772,-4.042,14.885,-3.948,14.964,-3.921,15.073,-3.979,15.281,-3.954,15.29,-3.914,15.307,-3.852,15.354,-3.82,15.395,-3.818,15.432,-3.853,15.456,-3.927,15.467,-3.923,15.504,-3.796,15.603,-3.787,15.62,-3.861,15.642,-3.849,15.66,-3.812,15.674,-3.748,15.684,-3.714,15.732,-3.708,15.818,-3.682,15.872,-3.635,15.894,-3.598,15.939,-3.57,16.005,-3.503,16.047],[4.626,9.865,4.644,9.755,4.631,9.669,4.593,9.571,4.586,9.473,4.612,9.374,4.654,9.312,4.713,9.287,4.75,9.243,4.764,9.18,4.83,9.161,4.947,9.185,5.057,9.15,5.159,9.055,5.292,8.989,5.456,8.949,5.604,8.883,5.802,8.742,6.02,8.754,6.159,8.742,6.291,8.71,6.385,8.628,6.438,8.497,6.521,8.377,6.633,8.268,6.716,8.144,6.771,8.006,6.776,7.793,6.71,7.362,6.664,6.973,6.66,6.743,6.691,6.44,6.741,6.202,6.707,5.978,6.556,5.541,6.495,5.464,6.48,5.422,6.49,5.388,6.455,5.363],[-4.304,15.507,-4.338,15.621,-4.317,15.683,-4.252,15.732,-4.208,15.793,-4.185,15.865,-4.138,15.892,-4.066,15.873,-3.961,15.925,-3.824,16.047,-3.689,16.113,-3.556,16.123,-3.467,16.161],[-3.503,16.047,-3.467,16.161],[-3.467,16.161,-3.424,16.226,-3.379,16.261,-3.333,16.266,-3.31,16.292,-3.31,16.337,-3.263,16.386,-3.222,16.408],[-3.222,16.408,-3.189,16.3,-3.187,16.253,-3.206,16.225,-3.234,16.207,-3.271,16.199,-3.302,16.163,-3.329,16.101,-3.396,16.062,-3.503,16.047],[-3.222,16.408,-3.167,16.438,-3.106,16.511,-3.08,16.606,-3.028,16.65,-2.948,16.643,-2.743,16.69,-2.411,16.792,-2.197,16.837,-2.101,16.826,-2.043,16.835,-2.023,16.864,-1.897,16.915,-1.664,16.988,-1.448,17.035,-1.25,17.057,-1.118,17.048,-1.052,17.009,-0.965,16.996,-0.857,17.008,-0.77,16.993,-0.705,16.95,-0.601,16.938,-0.457,16.955,-0.346,16.873,-0.269,16.691,-0.203,16.577,-0.148,16.533,-0.089,16.426,0.003,16.17,0.144,15.983,0.259,15.872,0.397,15.775,0.471,15.679,0.481,15.585,0.548,15.51,0.672,15.456,0.739,15.41,0.749,15.371,0.727,15.283,0.675,15.144,0.657,15.046,0.675,14.988,0.722,14.948,0.799,14.926,0.855,14.861,0.891,14.751,0.965,14.63,1.076,14.498,1.208,14.381,1.362,14.28,1.463,14.146,1.512,13.98,1.581,13.847,1.671,13.747,1.812,13.651,2.004,13.558,2.125,13.476,2.174,13.402,2.236,13.343,2.31,13.299,2.355,13.176,2.37,12.976,2.415,12.818,2.49,12.704,2.521,12.627,2.508,12.59,2.515,12.552,2.54,12.529,2.571,12.53,2.597,12.552,2.615,12.552,2.626,12.531,2.653,12.522,2.692,12.53,2.718,12.519,2.736,12.468,2.768,12.432,2.815,12.419,2.844,12.399,3.15,12.118,3.267,11.992,3.299,11.927,3.36,11.88,3.45,11.852,3.532,11.787,3.595,11.696,3.695,11.62,3.851,11.57,4.022,11.484,4.133,11.391,4.241,11.261,4.368,11.17,4.513,11.115,4.62,11.045,4.688,10.957,4.705,10.837,4.656,10.607]]},{"n":"Nile","z":2,"b":[30.317,15.635,33.969,30.121],"l":[[32.49,15.635,32.535,16.04,32.629,16.256,32.785,16.418,32.929,16.508,33.061,16.526,33.165,16.566,33.241,16.628,33.359,16.686,33.519,16.738,33.635,16.825,33.706,16.944,33.732,17.029,33.712,17.079,33.715,17.151,33.74,17.246,33.81,17.379,33.925,17.55,33.952,17.699,33.969,17.792,33.942,18.107,33.9,18.278,33.845,18.305,33.784,18.364,33.718,18.455,33.682,18.555,33.675,18.662,33.645,18.724,33.59,18.741,33.567,18.847,33.574,19.043,33.539,19.173,33.463,19.237,33.413,19.308,33.38,19.423,33.138,19.503,33.018,19.511,32.938,19.474,32.838,19.383,32.717,19.238,32.62,19.161,32.546,19.152,32.468,19.083,32.385,18.954,32.306,18.89,32.23,18.891,32.153,18.865,32.076,18.812,32.039,18.743,32.044,18.661,32.005,18.606,31.921,18.58,31.8,18.452,31.642,18.223,31.474,18.077,31.296,18.015,31.146,18.002,31.023,18.036,30.911,18.087,30.809,18.154,30.744,18.257,30.717,18.397,30.674,18.499,30.616,18.562,30.562,18.66,30.511,18.794,30.486,18.988,30.488,19.243,30.458,19.471,30.396,19.672,30.35,19.786,30.348,19.822,30.37,19.857,30.5,19.914,30.575,19.967,30.594,20.016,30.6,20.098,30.591,20.214,30.571,20.291,30.539,20.329,30.49,20.351,30.422,20.358,30.366,20.417,30.321,20.527,30.317,20.616,30.352,20.683,30.363,20.735,30.37,20.774,30.397,20.805,30.51,20.84,30.576,20.9,30.595,20.985,30.632,21.044,30.687,21.076,30.709,21.121,30.699,21.177,30.763,21.249,30.903,21.337,30.97,21.399,30.965,21.436,30.994,21.493,31.091,21.609],[32.879,23.988,32.913,24.328,32.913,24.454,32.889,24.478,32.892,24.54,32.92,24.639,32.921,24.77,32.895,24.933,32.871,25.037,32.848,25.082,32.767,25.149,32.629,25.239,32.54,25.35,32.502,25.483,32.493,25.565,32.523,25.612,32.704,25.754,32.764,25.879,32.763,26.034,32.734,26.129,32.678,26.163,32.553,26.143,32.362,26.067,32.229,26.077,32.154,26.171,32.094,26.189,32.041,26.19,32.015,26.2,31.997,26.247,31.966,26.297,31.871,26.375,31.82,26.445,31.812,26.509,31.782,26.542,31.73,26.543,31.706,26.564,31.712,26.605,31.695,26.632,31.655,26.644,31.553,26.762,31.369,27.01,31.341,27.057,31.304,27.099,31.278,27.123,31.245,27.144,30.969,27.356,30.868,27.5,30.889,27.619,30.884,27.768,30.877,27.902,30.863,27.985,30.837,28.038,30.807,28.085,30.774,28.12,30.744,28.165,30.74,28.23,30.762,28.309,30.811,28.401,30.836,28.492,30.837,28.582,30.858,28.682,30.899,28.791,30.995,28.943,31.146,29.136,31.219,29.282,31.215,29.38,31.236,29.489,31.281,29.609,31.287,29.783,31.255,29.999,31.254,30.08,31.238,30.121]]},{"n":"Nmai","z":3,"b":[97.519,25.62,98.373,28.425],"l":[[97.983,28.425,98.192,28.25,98.297,28.132,98.335,28.008,98.342,27.84,98.302,27.73,98.215,27.679,98.154,27.674,98.118,27.715,98.08,27.726,98.04,27.707,98.042,27.629,98.087,27.494,98.172,27.329,98.296,27.133,98.362,26.974,98.373,26.85,98.354,26.701,98.281,26.443,98.158,26.03,98.096,25.867,98.055,25.815,97.976,25.74,97.86,25.641,97.775,25.62,97.72,25.678,97.641,25.706,97.558,25.703,97.519,25.701]]},{"n":"North Saskatchewan","z":4,"b":[-117.132,51.993,-105.065,54.041],"l":[[-117.132,52.014,-117.011,52.077,-116.937,52.072,-116.838,51.993,-116.534,52.022,-116.438,52.079,-116.448,52.176,-116.39,52.276,-116.262,52.379,-116.053,52.437,-115.764,52.453,-115.576,52.44,-115.488,52.399,-115.398,52.387,-115.308,52.404,-115.211,52.393,-115.108,52.355,-115.029,52.345,-114.975,52.363,-114.984,52.463,-115.057,52.644,-115.111,52.74,-115.146,52.752,-115.164,52.785,-115.166,52.838,-115.184,52.875,-115.218,52.895,-115.227,52.94,-115.211,53.01,-115.159,53.05,-115.071,53.061,-115.014,53.083,-114.974,53.131,-114.841,53.276,-114.729,53.321,-114.596,53.314,-114.483,53.338,-114.388,53.392,-114.262,53.396,-114.104,53.348,-113.947,53.333,-113.789,53.35,-113.691,53.389,-113.651,53.451,-113.569,53.5,-113.445,53.536,-113.306,53.628,-113.153,53.777,-113.024,53.876,-112.919,53.926,-112.856,53.975,-112.834,54.023,-112.732,54.032,-112.55,54.003,-112.41,54.006,-112.313,54.041,-112.193,54.022,-112.05,53.948,-111.92,53.908,-111.802,53.902,-111.737,53.87,-111.726,53.812,-111.578,53.774,-111.292,53.755,-111.094,53.772,-110.986,53.825,-110.858,53.851,-110.712,53.849,-110.563,53.801,-110.411,53.707,-110.302,53.668,-110.235,53.686,-110.123,53.661,-109.967,53.593,-109.826,53.564,-109.702,53.573,-109.613,53.549,-109.558,53.492,-109.535,53.452,-109.543,53.428,-109.482,53.409,-109.352,53.393,-109.236,53.36,-109.135,53.308,-109.022,53.216,-108.897,53.082,-108.71,52.971,-108.459,52.882,-108.343,52.819,-108.362,52.781,-108.361,52.755,-108.328,52.734],[-108.334,52.734,-108.103,52.611,-107.985,52.567,-107.904,52.56,-107.747,52.502,-107.513,52.394,-107.357,52.338,-107.281,52.334,-107.18,52.343,-107.057,52.366,-106.962,52.441,-106.895,52.568,-106.691,52.764,-106.352,53.029,-106.15,53.171,-106.085,53.191,-106.034,53.185,-105.996,53.154,-105.88,53.163,-105.684,53.211,-105.456,53.236,-105.065,53.24]]},{"n":"Nu","z":4,"b":[96.596,20.951,99.089,30.448],"l":[[96.596,30.448,96.675,30.443,96.707,30.406,96.717,30.333,96.854,30.245,97.116,30.142,97.29,30.032,97.375,29.918,97.424,29.817,97.436,29.729,97.477,29.635,97.546,29.536,97.66,29.447,97.819,29.367,97.949,29.226,98.052,29.023,98.163,28.87,98.343,28.715,98.367,28.577,98.391,28.522,98.423,28.502,98.451,28.402,98.474,28.22,98.536,28.088,98.635,28.003,98.682,27.913,98.676,27.817,98.709,27.722,98.816,27.581,98.869,27.341,98.889,27.059,98.895,26.656,98.885,26.362,98.857,26.177,98.856,25.931,98.882,25.625,98.882,25.372,98.856,25.173,98.871,24.991,98.927,24.828,98.98,24.723,99.03,24.677,99.036,24.594,98.998,24.475,99.016,24.383,99.089,24.317,99.058,24.248,98.922,24.175,98.764,24.116,98.583,24.07,98.473,24.026,98.432,23.984,98.42,23.933,98.437,23.872,98.496,23.783,98.598,23.667,98.638,23.563,98.617,23.472,98.635,23.417,98.692,23.398,98.721,23.357,98.723,23.294,98.654,23.248,98.514,23.22,98.474,23.173,98.534,23.109,98.548,23.068,98.516,23.051,98.504,23.024,98.513,22.988,98.554,22.953,98.628,22.918,98.629,22.871,98.558,22.812,98.553,22.706,98.614,22.555,98.626,22.432,98.588,22.336,98.587,22.251,98.625,22.177,98.676,22.115,98.742,22.065,98.718,21.995,98.605,21.905,98.574,21.79,98.624,21.651,98.629,21.564,98.589,21.531,98.595,21.483,98.647,21.421,98.681,21.337,98.696,21.231,98.726,21.145,98.773,21.077,98.724,21.02,98.508,20.951]]},{"n":"Salween","z":4,"b":[91.518,30.437,96.596,31.943],"l":[[91.518,31.943,91.546,31.815,91.6,31.747,91.691,31.696,91.755,31.607,91.794,31.478,91.863,31.418,91.963,31.425,92.032,31.396,92.071,31.33,92.134,31.299,92.221,31.304,92.282,31.347,92.319,31.429,92.428,31.486,92.703,31.533,92.869,31.563,92.958,31.56,93.027,31.534,93.156,31.529,93.347,31.544,93.524,31.536,93.687,31.504,93.819,31.501,93.922,31.525,94.106,31.523,94.371,31.495,94.528,31.468,94.575,31.443,94.576,31.387,94.529,31.301,94.531,31.251,94.583,31.237,94.629,31.196,94.669,31.129,94.71,31.091,94.752,31.08,94.821,31.102,94.919,31.158,95.039,31.186,95.181,31.188,95.301,31.171,95.399,31.136,95.486,31.074,95.563,30.985,95.667,30.957,95.799,30.99,95.962,30.978,96.157,30.921,96.283,30.873,96.34,30.835,96.411,30.721,96.497,30.529,96.554,30.437,96.596,30.448]]},{"n":"Ob","z":4,"b":[70.354,52.332,85.056,61.304],"l":[[85.056,52.434,84.589,52.422,84.347,52.393,84.175,52.342,84.003,52.332,83.831,52.362,83.728,52.414,83.694,52.489,83.683,52.592,83.699,52.789,83.658,52.961,83.658,53.04,83.685,53.083,83.817,53.109,83.86,53.175,83.849,53.199,83.838,53.353,83.712,53.419,83.493,53.429,83.332,53.457,83.228,53.503,83.128,53.505,83.03,53.464,82.897,53.437,82.728,53.425,82.581,53.446,82.454,53.499,82.388,53.545,82.38,53.585,82.33,53.631,82.237,53.684,82.114,53.718,81.96,53.734,81.867,53.758,81.836,53.789,81.727,53.8,81.542,53.788,81.42,53.808,81.334,53.886],[70.354,61.301,70.784,61.304,70.979,61.298,71.081,61.283,71.154,61.251,71.199,61.202,71.257,61.195,71.329,61.23,71.447,61.253,71.695,61.269,72.044,61.255,72.223,61.264,72.347,61.293,72.466,61.288,72.581,61.251,72.678,61.243,72.757,61.263,72.907,61.263,73.237,61.235,73.566,61.196,73.71,61.196,73.779,61.221,73.877,61.214,74.002,61.177,74.118,61.175,74.223,61.207,74.314,61.201,74.385,61.168,74.429,61.168,74.449,61.189,74.515,61.213,74.645,61.215,74.75,61.202,74.868,61.16,75.333,61.124,75.535,61.082,75.627,61.024,75.714,61.01,75.794,61.04,75.887,61.05,75.994,61.04,76.135,61,76.308,60.93,76.435,60.905,76.517,60.925,76.626,60.896,76.764,60.82,76.96,60.745,77.215,60.674,77.399,60.649,77.512,60.669,77.602,60.662,77.672,60.627,77.714,60.586,77.729,60.538,77.807,60.483,77.949,60.422,78.111,60.396,78.293,60.405,78.341,60.371,78.345,60.332,78.389,60.304,78.437,60.292,78.481,60.244,78.563,60.196,78.683,60.15,78.785,60.133,78.836,60.141,78.871,60.116,78.876,60.084,78.909,60.065,78.962,60.055,78.979,60.019,78.959,59.981,78.952,59.933,78.979,59.884,79.039,59.841,79.13,59.805,79.166,59.766,79.147,59.724,79.148,59.689,79.183,59.647,79.283,59.558,79.388,59.517,79.531,59.495,79.623,59.46,79.665,59.412,79.779,59.402,79.967,59.429,80.093,59.424,80.158,59.385,80.172,59.349,80.133,59.317,80.152,59.289,80.228,59.265,80.295,59.273,80.354,59.312,80.411,59.33,80.467,59.328,80.502,59.299,80.516,59.243,80.616,59.18,80.802,59.108,80.973,59.091,81.128,59.13,81.256,59.118,81.358,59.056,81.411,59.004,81.415,58.963,81.444,58.945,81.478,58.937,81.496,58.914,81.505,58.886,81.535,58.848,81.568,58.831,81.573,58.797,81.542,58.745,81.554,58.718,81.648,58.701,81.704,58.707,81.735,58.723,81.775,58.736,81.858,58.702,81.902,58.66,81.908,58.61,81.965,58.585,82.073,58.584,82.172,58.549,82.262,58.48,82.339,58.439,82.403,58.427,82.434,58.404,82.435,58.369,82.463,58.359,82.519,58.376,82.575,58.363,82.631,58.322,82.734,58.305,82.884,58.313,82.928,58.283,82.934,58.24,82.961,58.193,83.031,58.15,83.097,58.128,83.16,58.127,83.224,58.057,83.289,57.92,83.351,57.826,83.41,57.776,83.55,57.742,83.7,57.73,83.777,57.699,83.816,57.641,83.833,57.58,83.907,57.506,83.943,57.441,83.94,57.384,83.958,57.339,83.997,57.306,84.105,57.277,84.28,57.253,84.344,57.21,84.351,57.163,84.376,57.137,84.406,57.109,84.414,57.067,84.407,57.011,84.436,56.911,84.401,56.792,84.308,56.69,84.144,56.565,84.064,56.458,84.066,56.369,84.054,56.314,84.027,56.293,84.017,56.264,84.024,56.227,83.994,56.18,83.925,56.121,83.887,56.038,83.879,55.929,83.819,55.833,83.708,55.748,83.65,55.68,83.645,55.629,83.536,55.561,83.323,55.475,83.156,55.436,83.033,55.444,82.944,55.433,82.888,55.404,82.869,55.34,82.887,55.243,82.877,55.173,82.837,55.129,82.879,55.065,83.002,54.981,83.053,54.915,83.025,54.845]]},{"n":"Ob","z":4,"b":[68.857,61.124,70.354,61.301],"l":[[68.857,61.124,69.01,61.198,69.104,61.213,69.189,61.196,69.29,61.215,69.408,61.272,69.578,61.284,69.8,61.251,70.021,61.252,70.354,61.301]]},{"n":"Ob","z":2.1,"b":[65.14,61.089,69.013,66.814],"l":[[68.857,61.124,68.786,61.089,68.589,61.095,68.465,61.125,68.348,61.181,68.309,61.256,68.348,61.351,68.353,61.416,68.325,61.451,68.134,61.519,67.779,61.62,67.586,61.712,67.553,61.794,67.455,61.919,67.292,62.087,67.043,62.2,66.707,62.258,66.511,62.318,66.455,62.38,66.39,62.42,66.281,62.447,65.988,62.482,65.68,62.622,65.258,62.879,65.163,62.996,65.14,63.072,65.149,63.131,65.211,63.206,65.335,63.296,65.472,63.343,65.527,63.395,65.524,63.456,65.557,63.496,65.701,63.532,65.767,63.584,65.757,63.652,65.732,63.707,65.709,63.734,65.721,63.769,65.788,63.792,65.805,63.809,65.796,63.843,65.781,63.878,65.803,63.899,65.805,63.95,65.789,64.031,65.806,64.076,65.858,64.085,65.885,64.101,65.889,64.126,65.93,64.136,66.008,64.131,66.031,64.154,65.998,64.206,65.953,64.242,65.895,64.262,65.885,64.289,65.923,64.325,65.92,64.37,65.876,64.426,65.8,64.482,65.69,64.537,65.685,64.587,65.706,64.615,65.688,64.659,65.554,64.709,65.501,64.756,65.529,64.799,65.533,64.842,65.512,64.885,65.543,64.93,65.565,64.982,65.524,65.016,65.408,65.022,65.301,65.053,65.257,65.099,65.277,65.176,65.362,65.283,65.455,65.362,65.557,65.411,65.606,65.468,65.605,65.532,65.625,65.604,65.667,65.685,65.736,65.764,65.831,65.842,65.877,65.933,65.871,66.035,65.83,66.094,65.821,66.137,65.839,66.157,65.883,66.196,65.957,66.22,66.097,66.278,66.3,66.411,66.451,66.571,66.663,66.627,66.936,66.578,67.153,66.583,67.315,66.641],[67.315,66.641,67.573,66.629,67.927,66.547,68.197,66.538,68.385,66.6,68.497,66.656,68.533,66.707,68.655,66.752],[67.315,66.641,67.56,66.759,67.692,66.783,67.869,66.773,68.007,66.784,68.105,66.814,68.261,66.807,68.655,66.752],[68.655,66.752,68.907,66.799,69.013,66.788]]},{"n":"Oder","z":4,"b":[14.129,49.637,18.317,53.639],"l":[[17.618,49.739,17.847,49.654,17.946,49.637,18.016,49.672,18.142,49.73,18.225,49.795,18.307,49.886,18.317,49.972,18.253,50.055,18.231,50.137,18.252,50.22,18.185,50.321,18.032,50.441,17.958,50.532,17.964,50.593,17.938,50.656,17.88,50.722,17.762,50.78,17.45,50.882,17.361,50.928,17.3,50.975,17.269,51.023,17.194,51.068,17.015,51.131,16.869,51.229,16.738,51.253,16.573,51.236,16.475,51.245,16.442,51.279,16.44,51.315,16.467,51.354,16.474,51.421,16.462,51.518,16.408,51.595,16.31,51.652,16.223,51.677,16.146,51.669,16.02,51.691,15.847,51.743,15.769,51.811,15.787,51.894,15.758,51.961,15.682,52.011,15.424,52.046,14.761,52.077,14.705,52.11,14.692,52.15,14.705,52.207,14.68,52.25,14.616,52.278,14.574,52.314,14.555,52.36,14.57,52.431,14.619,52.528,14.514,52.646,14.254,52.783,14.129,52.878,14.139,52.933,14.194,52.982,14.293,53.027,14.369,53.106,14.446,53.275,14.581,53.445,14.618,53.526,14.61,53.584,14.584,53.639]]},{"n":"Ohio","z":3,"b":[-89.165,36.992,-79.991,40.686],"l":[[-85.186,38.694,-85.211,38.704,-85.247,38.727,-85.307,38.737,-85.405,38.73,-85.445,38.683,-85.425,38.597,-85.459,38.53,-85.546,38.484,-85.609,38.423,-85.646,38.347,-85.706,38.301,-85.788,38.285,-85.855,38.215,-85.906,38.091,-85.961,38.014,-86.021,37.984,-86.103,37.992,-86.208,38.037,-86.268,38.083,-86.284,38.13,-86.315,38.164,-86.343,38.179,-86.361,38.179,-86.363,38.172,-86.355,38.159,-86.365,38.145,-86.43,38.14,-86.457,38.12,-86.448,38.086,-86.463,38.062,-86.5,38.047,-86.52,38.015,-86.522,37.964,-86.549,37.917,-86.599,37.875,-86.673,37.889,-86.77,37.96,-86.869,37.976,-86.969,37.938,-87.044,37.886,-87.094,37.821,-87.141,37.805,-87.209,37.855,-87.372,37.923,-87.457,37.938,-87.518,37.922,-87.561,37.929,-87.576,37.948,-87.587,37.956,-87.599,37.946,-87.601,37.887,-87.615,37.849,-87.643,37.833,-87.665,37.846,-87.679,37.886,-87.714,37.9,-87.77,37.885,-87.825,37.891,-87.874,37.916,-87.892,37.913,-87.906,37.902,-87.902,37.855,-87.912,37.818,-87.938,37.794,-87.975,37.788,-88.022,37.8,-88.042,37.789,-88.073,37.771,-88.129,37.7,-88.14,37.621,-88.108,37.534,-88.191,37.465,-88.389,37.411,-88.495,37.364,-88.507,37.324,-88.493,37.262,-88.453,37.178,-88.446,37.12,-88.472,37.088,-88.614,37.11,-88.873,37.186,-89.046,37.183,-89.135,37.102,-89.165,37.042,-89.154,36.992],[-79.991,40.448,-80.149,40.522,-80.223,40.584,-80.265,40.661,-80.344,40.686,-80.519,40.647,-80.638,40.603,-80.659,40.563,-80.623,40.512,-80.604,40.445,-80.602,40.364,-80.668,40.185,-80.804,39.909,-80.872,39.736,-80.874,39.664,-80.963,39.571,-81.139,39.456,-81.263,39.387,-81.333,39.363,-81.392,39.365,-81.44,39.393,-81.493,39.374,-81.552,39.308,-81.606,39.274,-81.657,39.272,-81.701,39.229,-81.737,39.147,-81.771,39.099,-81.803,39.086,-81.805,39.044,-81.778,38.973,-81.78,38.947,-81.795,38.946,-81.809,38.95,-81.844,38.934,-81.882,38.894,-81.897,38.894,-81.907,38.9,-81.919,38.952,-81.95,38.989,-82.001,39.011,-82.072,38.963,-82.163,38.845,-82.204,38.768,-82.188,38.715,-82.18,38.638,-82.202,38.606,-82.252,38.594,-82.291,38.552,-82.318,38.48,-82.387,38.434,-82.498,38.415,-82.621,38.451,-82.758,38.541,-82.845,38.628,-82.88,38.71,-83,38.714,-83.203,38.639,-83.357,38.627,-83.461,38.68,-83.538,38.7,-83.616,38.683,-83.69,38.65,-83.752,38.669,-83.828,38.731,-83.948,38.774,-84.113,38.798,-84.227,38.865,-84.29,38.975,-84.358,39.05,-84.43,39.091,-84.509,39.103,-84.595,39.086,-84.661,39.094,-84.707,39.129,-84.766,39.125,-84.837,39.083,-84.861,39.04,-84.835,38.996,-84.837,38.961,-84.864,38.933,-84.854,38.913,-84.807,38.9,-84.79,38.868,-84.801,38.819,-84.901,38.77,-85.094,38.714,-85.136,38.697,-85.17,38.69,-85.186,38.694]]},{"n":"Okavango","z":5,"b":[21.503,-21.265,24.739,-18.06],"l":[[24.739,-21.265,24.69,-21.185,24.597,-21.117,24.444,-21.036,24.367,-20.944,24.367,-20.84,24.4,-20.705,24.467,-20.54,24.495,-20.44,24.484,-20.406,24.453,-20.391,24.403,-20.393,24.323,-20.345,24.212,-20.248,24.098,-20.228,23.981,-20.287,23.881,-20.282,23.799,-20.212,23.698,-20.16,23.577,-20.126,23.46,-20.119,23.404,-20.118,23.37,-20.103,23.368,-20.08,23.392,-20.048,23.545,-19.886,23.595,-19.735,23.579,-19.683],[23.354,-19.556,23.435,-19.622,23.579,-19.683],[23.528,-19.522,23.579,-19.683],[23.237,-19.338,23.354,-19.556],[23.237,-19.338,23.292,-19.387,23.37,-19.405,23.438,-19.441,23.526,-19.521],[22.527,-18.942,22.542,-19.072,22.635,-19.2,22.771,-19.368,22.892,-19.473,22.998,-19.517,23.124,-19.545,23.354,-19.556],[22.527,-18.942,22.58,-19.04,22.663,-19.082,22.774,-19.068,22.894,-19.078,23.023,-19.111,23.129,-19.19,23.212,-19.316,23.237,-19.338],[21.503,-18.06,21.637,-18.097,21.738,-18.185,21.85,-18.339,21.937,-18.417,22,-18.422,22.088,-18.518,22.2,-18.705,22.317,-18.822,22.439,-18.87,22.527,-18.942]]},{"n":"Olenekskaya Protoka","z":2,"b":[122.752,72.294,126.69,72.907],"l":[[126.69,72.294,126.475,72.353,126.336,72.35,126.203,72.308,126.019,72.304,125.786,72.34,125.424,72.436,124.932,72.591,124.614,72.665,124.469,72.657,124.221,72.691,123.871,72.767,123.607,72.793,123.43,72.768,123.189,72.773,122.752,72.907]]},{"n":"Olenëk","z":5,"b":[104.957,67.124,123.743,72.979],"l":[[104.957,67.568,105.339,67.514,105.468,67.57,105.562,67.584,105.665,67.573,105.693,67.536,105.707,67.512,105.751,67.486,105.814,67.477,105.895,67.483,105.962,67.505,106.013,67.542,106.026,67.57,106.113,67.594,106.252,67.557,106.283,67.526,106.248,67.486,106.217,67.44,106.285,67.375,106.402,67.351,106.567,67.368,106.872,67.34,107.315,67.269,107.618,67.244,107.78,67.266,108.068,67.239,108.482,67.164,108.851,67.124,109.292,67.13,109.337,67.141,109.371,67.167,109.448,67.175,109.576,67.159,109.773,67.166,110.038,67.197,110.128,67.235,110.145,67.261,110.168,67.278,110.231,67.28,110.27,67.286,110.286,67.296,110.277,67.31,110.261,67.331,110.257,67.354,110.319,67.391,110.447,67.426,110.639,67.46,110.808,67.508,110.954,67.569,110.956,67.602,110.941,67.618,110.924,67.637,110.935,67.664,110.931,67.697,110.893,67.747,110.908,67.786,111.054,67.848,111.195,67.894,111.331,67.922,111.447,67.924,111.543,67.898,111.586,67.866,111.599,67.846,111.641,67.825,111.776,67.858,111.964,67.863,112.204,67.839,112.336,67.838,112.359,67.861,112.35,67.884,112.31,67.908,112.285,67.947,112.274,68.002,112.191,68.048,112.037,68.084,111.98,68.121,112.021,68.161,112.108,68.199,112.205,68.211,112.237,68.229,112.234,68.26,112.192,68.281,112.192,68.305,112.239,68.344,112.254,68.377,112.239,68.407,112.194,68.441,112.186,68.495,112.241,68.547,112.323,68.553,112.434,68.514,112.532,68.5,112.617,68.511,112.843,68.487,113.209,68.427,113.542,68.432,113.839,68.502,114.048,68.518,114.166,68.481,114.339,68.469,114.565,68.483,114.693,68.481,114.721,68.464,114.694,68.442,114.612,68.415,114.642,68.39,114.783,68.368,115.064,68.405,115.483,68.501,115.705,68.543,115.727,68.529,115.73,68.51,115.713,68.485,115.639,68.45,115.51,68.404,115.582,68.347,115.855,68.279,115.947,68.238,115.858,68.223,115.811,68.194,115.804,68.151,115.872,68.124,116.016,68.114,116.07,68.075,116.102,68.007,116.177,67.895,116.203,67.813,116.179,67.761,116.258,67.74,116.44,67.751,116.527,67.781,116.555,67.827,116.638,67.862,116.887,67.877,116.994,67.897,116.958,67.924,116.965,67.958,117.012,68.001,117.122,68.031,117.292,68.049,117.462,68.094,117.633,68.169,117.594,68.234,117.345,68.291,117.224,68.338,117.231,68.374,117.203,68.404,117.141,68.426,117.098,68.459,117.075,68.504,117.139,68.549,117.292,68.596,117.434,68.573,117.568,68.479,117.741,68.469,117.955,68.545,118.07,68.615,118.087,68.68,118.052,68.714,117.965,68.716,117.959,68.733,118.035,68.763,118.193,68.761,118.434,68.724,118.573,68.731,118.609,68.783,118.689,68.823,118.811,68.851,118.822,68.886,118.72,68.929,118.637,68.947,118.573,68.941,118.525,68.919,118.493,68.882,118.293,68.902,117.961,68.962,117.912,69.015,117.939,69.052,118.228,69.092,118.357,69.12,118.371,69.159,118.33,69.248,118.34,69.284,118.358,69.323,118.389,69.346,118.375,69.382,118.296,69.415,118.262,69.456,118.272,69.503,118.227,69.551,118.177,69.575,118.187,69.629,118.299,69.647,118.741,69.696,119.026,69.752,119.156,69.814,119.248,69.876,119.303,69.938,119.321,70.007,119.303,70.082,119.195,70.164,118.996,70.254,118.909,70.314,118.934,70.342,118.986,70.343,119.067,70.314,119.208,70.312,119.409,70.337,119.558,70.387,119.653,70.462,119.751,70.511,119.852,70.534,120.15,70.533,120.896,70.497,121.727,70.512,122.124,70.534,122.364,70.57,122.511,70.606,122.549,70.64,122.524,70.672,122.382,70.695,122.307,70.734,122.297,70.79,122.322,70.841,122.381,70.887,122.386,70.923,122.338,70.949,122.319,70.971,122.33,70.986,122.373,71.005,122.45,71.026,122.48,71.061,122.495,71.09,122.501,71.141,122.545,71.193,122.625,71.225,122.648,71.26,122.615,71.298,122.739,71.379,123.02,71.503,123.274,71.587,123.499,71.632,123.612,71.668,123.613,71.696,123.657,71.721,123.743,71.744,123.738,71.79,123.643,71.859,123.631,71.935,123.702,72.02,123.424,72.146,122.799,72.316,122.232,72.502,121.722,72.705,121.355,72.799,121.133,72.782,120.967,72.799,120.857,72.85,120.745,72.878,120.573,72.889,120.199,72.894,119.885,72.933,119.75,72.979]]},{"n":"Omo","z":5,"b":[35.912,4.539,37.6,9.151],"l":[[37.032,9.151,37.031,9.052,37.067,8.997,37.14,8.953,37.184,8.907,37.204,8.838,37.157,8.741,37.149,8.692,37.166,8.658,37.41,8.364,37.518,8.261,37.574,8.228,37.6,8.112,37.59,7.877,37.527,7.649,37.411,7.426,37.377,7.286,37.427,7.23,37.475,7.202,37.521,7.202,37.524,7.184,37.483,7.147,37.452,7.072,37.43,6.958,37.396,6.893,37.35,6.877,37.3,6.821,37.247,6.724,37.165,6.659,37.054,6.626,36.983,6.633,36.954,6.681,36.878,6.706,36.754,6.71,36.579,6.635,36.355,6.481,36.209,6.415,36.143,6.435,36.085,6.419,36.036,6.367,36.004,6.306,35.983,6.198,35.955,6.172,35.96,6.119,35.989,6.028,35.978,5.84,35.927,5.553,35.912,5.391,35.933,5.353,35.984,5.344,36.066,5.365,36.113,5.41,36.128,5.481,36.155,5.501,36.196,5.47,36.202,5.418,36.175,5.344,36.171,5.285,36.192,5.24,36.167,5.208,36.095,5.187,36.066,5.165,36.08,5.141,36.072,5.114,36.043,5.084,36.048,5.043,36.087,4.99,36.074,4.914,36.009,4.812,35.994,4.706,36.045,4.539]]},{"n":"Orange","z":4,"b":[16.448,-30.679,29.057,-28.031],"l":[[25.506,-30.612,25.344,-30.56,25.212,-30.473,24.978,-30.263],[24.731,-29.979,24.489,-29.818,24.392,-29.736,24.358,-29.678,24.296,-29.643,24.207,-29.632,24.103,-29.586,23.984,-29.506,23.923,-29.441,23.92,-29.393,23.882,-29.347,23.808,-29.304,23.736,-29.23,23.628,-29.074,23.577,-29.166,23.539,-29.205,23.498,-29.222,23.452,-29.219,23.412,-29.203,23.376,-29.211,23.348,-29.245,23.28,-29.288,23.163,-29.332,23.1,-29.37,23.088,-29.39,23.056,-29.412,23.014,-29.431,23.002,-29.486,22.981,-29.512,22.924,-29.522,22.892,-29.529,22.87,-29.537,22.852,-29.558,22.863,-29.624,22.85,-29.661,22.812,-29.669,22.741,-29.613,22.638,-29.493,22.507,-29.403,22.35,-29.343,22.266,-29.268,22.257,-29.177,22.209,-29.061,22.122,-28.918,22.056,-28.854,22.012,-28.867,21.963,-28.858,21.908,-28.825,21.835,-28.721,21.745,-28.544,21.662,-28.44,21.586,-28.408,21.486,-28.4,21.362,-28.417,21.236,-28.494,21.107,-28.629,20.97,-28.714,20.827,-28.748,20.704,-28.76,20.602,-28.747,20.485,-28.692,20.352,-28.593,20.21,-28.514,20.057,-28.453,19.878,-28.449,19.671,-28.504,19.54,-28.575,19.483,-28.662,19.407,-28.714,19.313,-28.733,19.271,-28.778,19.282,-28.848,19.246,-28.902,19.162,-28.939,19.026,-28.928,18.839,-28.869,18.6,-28.855,18.311,-28.886,18.103,-28.872,17.976,-28.811,17.842,-28.777,17.699,-28.768,17.617,-28.743,17.448,-28.698,17.416,-28.621,17.396,-28.563,17.348,-28.501,17.343,-28.452,17.38,-28.414,17.386,-28.353,17.359,-28.269,17.312,-28.229,17.246,-28.231,17.205,-28.199,17.188,-28.132,17.149,-28.082,17.056,-28.031,16.933,-28.07,16.875,-28.128,16.841,-28.219,16.81,-28.265,16.795,-28.341,16.787,-28.395,16.756,-28.452,16.723,-28.476,16.689,-28.465,16.626,-28.488,16.487,-28.573,16.448,-28.618],[29.057,-29.082,29.017,-29.218,28.958,-29.277,28.868,-29.304,28.809,-29.359,28.766,-29.481,28.721,-29.55,28.725,-29.586,28.762,-29.612,28.769,-29.643,28.744,-29.678,28.746,-29.732,28.773,-29.802,28.753,-29.851,28.686,-29.877,28.666,-29.91,28.693,-29.948,28.686,-29.984,28.645,-30.018,28.567,-30.038,28.454,-30.045,28.374,-30.032,28.325,-30.001,28.209,-30.006,28.025,-30.049,27.925,-30.101,27.906,-30.164,27.876,-30.213,27.835,-30.25,27.792,-30.264,27.744,-30.257,27.713,-30.282,27.698,-30.339,27.657,-30.374,27.591,-30.386,27.51,-30.371,27.431,-30.339,27.409,-30.325,27.39,-30.326,27.388,-30.335,27.389,-30.353,27.377,-30.372,27.345,-30.386,27.338,-30.402,27.337,-30.416,27.334,-30.427,27.31,-30.431,27.295,-30.445,27.283,-30.476,27.226,-30.499,27.082,-30.522,27.041,-30.541,27.028,-30.573,26.988,-30.609,26.897,-30.654,26.796,-30.678,26.684,-30.679,26.581,-30.65,26.488,-30.59,26.401,-30.565,26.32,-30.574,26.247,-30.556,26.18,-30.509,26.103,-30.5,25.972,-30.545]]},{"n":"Ord","z":5,"b":[127.578,-17.706,128.891,-15.298],"l":[[127.578,-17.623,127.929,-17.463,128.157,-17.523,128.258,-17.58,128.309,-17.654,128.356,-17.696,128.398,-17.706,128.502,-17.653,128.668,-17.537,128.789,-17.432,128.867,-17.335,128.891,-17.259,128.862,-17.204,128.856,-17.151,128.873,-17.102,128.837,-17.048,128.746,-16.99,128.697,-16.931,128.689,-16.871,128.698,-16.828,128.734,-16.788],[128.737,-16.134,128.703,-16.084,128.701,-16.032,128.744,-15.929,128.747,-15.802,128.712,-15.651,128.612,-15.563,128.447,-15.538,128.346,-15.495,128.289,-15.402,128.28,-15.35,128.255,-15.298]]},{"n":"Orinoco","z":3,"b":[-67.855,2.153,-61.099,9.788],"l":[[-63.436,2.374,-63.641,2.312,-63.732,2.258,-63.779,2.191,-63.849,2.156,-63.991,2.153,-64.279,2.265,-64.408,2.292,-64.475,2.271,-64.68,2.326,-65.022,2.456,-65.196,2.587,-65.202,2.718,-65.246,2.818,-65.328,2.886,-65.393,2.966,-65.442,3.059,-65.49,3.117,-65.537,3.142,-65.613,3.152,-65.772,3.142,-65.942,3.162,-66.011,3.13,-66.036,3.052,-66.129,3.024,-66.29,3.045,-66.413,3.091,-66.5,3.161,-66.556,3.237,-66.581,3.318,-66.645,3.42,-66.801,3.606,-66.946,3.706,-67.002,3.792,-67.015,3.896,-67.116,3.948,-67.302,3.946,-67.48,3.981,-67.732,4.087,-67.783,4.198,-67.799,4.284,-67.795,4.381,-67.814,4.455,-67.855,4.507,-67.855,4.665,-67.814,4.931,-67.804,5.133,-67.825,5.27,-67.788,5.375,-67.695,5.448,-67.642,5.559,-67.631,5.709,-67.575,5.833,-67.474,5.93,-67.439,6.026,-67.472,6.12,-67.423,6.202,-67.4,6.24,-67.225,6.386,-67.119,6.557,-67.062,6.851,-67.071,7.033,-66.959,7.151,-66.73,7.266,-66.563,7.396,-66.461,7.542,-66.297,7.629,-66.072,7.657,-65.912,7.714,-65.769,7.841,-65.641,7.892,-65.518,7.901,-65.358,7.883,-65.221,7.857,-65.105,7.823,-65.004,7.771,-64.928,7.71,-64.904,7.681,-64.887,7.668,-64.873,7.666,-64.854,7.668,-64.838,7.67,-64.787,7.685,-64.742,7.709,-64.59,7.76,-64.51,7.824,-64.538,7.892,-64.545,7.943,-64.531,7.978,-64.481,7.999,-64.395,8.006,-64.331,8.028,-64.288,8.065,-64.222,8.057,-64.155,8.005,-64.114,7.981,-64.089,7.975,-64.05,7.971,-63.993,7.974,-63.924,8.013,-63.849,8.092,-63.727,8.136,-63.558,8.145,-63.399,8.191,-63.249,8.273,-63.101,8.304,-62.953,8.284,-62.791,8.334,-62.615,8.454,-62.429,8.553,-62.235,8.633,-62.152,8.642],[-62.152,8.642,-62.092,8.648,-62.002,8.6,-61.88,8.582,-61.65,8.603,-61.619,8.597,-61.395,8.774,-61.099,8.941],[-62.063,9.036,-62.077,8.975,-62.065,8.928,-62.025,8.896,-62.039,8.828,-62.137,8.673,-62.152,8.642],[-61.736,9.631,-61.743,9.603,-61.849,9.289,-61.924,9.152,-62.044,9.055,-62.063,9.036],[-62.3,9.788,-62.309,9.748,-62.326,9.721,-62.414,9.562,-62.388,9.421,-62.277,9.245,-62.18,9.134,-62.098,9.089,-62.063,9.036]]},{"n":"Ottawa","z":5,"b":[-79.166,45.42,-74.315,46.827],"l":[[-76.903,45.892,-76.824,45.88,-76.754,45.798,-76.667,45.643,-76.553,45.542,-76.413,45.496,-76.27,45.484,-76.123,45.505,-76.001,45.484,-75.904,45.42,-75.626,45.451,-75.167,45.575,-74.864,45.642,-74.717,45.651,-74.577,45.632,-74.378,45.561,-74.315,45.531],[-77.371,46.063,-77.272,45.954,-77.155,45.912,-76.902,45.892],[-77.683,46.187,-77.373,46.064],[-77.683,46.187,-77.682,46.187],[-77.684,46.187,-77.683,46.187],[-79.166,46.827,-79.015,46.607,-78.903,46.486,-78.781,46.393,-78.46,46.307,-77.683,46.187]]},{"n":"Ouham","z":5,"b":[15.323,6.264,18.285,9.316],"l":[[15.323,6.462,15.398,6.342,15.496,6.325,15.714,6.393,15.79,6.461,15.846,6.486,15.908,6.49,15.96,6.437,16.001,6.328,16.075,6.27,16.182,6.264,16.276,6.302,16.358,6.383,16.404,6.448,16.414,6.497,16.474,6.56,16.584,6.637,16.663,6.671,16.709,6.662,16.774,6.609,16.857,6.514,16.933,6.472,17.001,6.486,17.082,6.475,17.174,6.441,17.281,6.448,17.401,6.497,17.509,6.52,17.605,6.515,17.709,6.558,17.821,6.65,17.9,6.772,17.948,6.926,18.002,7.023,18.062,7.062,18.127,7.139,18.197,7.254,18.249,7.313,18.285,7.314,18.275,7.362,18.22,7.457,18.145,7.49,18.051,7.46,17.984,7.469,17.945,7.517,17.877,7.548,17.782,7.561,17.749,7.611,17.778,7.698,17.768,7.826,17.718,7.995,17.699,8.127,17.709,8.224,17.77,8.369,17.882,8.56,17.945,8.744,17.96,8.92,17.992,9.011,18.04,9.018,18.098,9.056,18.164,9.126,18.207,9.2,18.235,9.316]]},{"n":"Pamir","z":4.7,"b":[72.657,37.029,73.605,37.472],"l":[[73.605,37.446,73.481,37.472,73.383,37.462,73.211,37.409,72.896,37.268,72.757,37.173,72.657,37.029]]},{"n":"Panama Canal","z":5,"b":[-79.927,8.924,-79.552,9.361],"l":[[-79.691,9.104,-79.566,8.968,-79.552,8.924],[-79.923,9.267,-79.927,9.304,-79.915,9.361]]},{"n":"Panj","z":4.7,"b":[68.212,36.684,72.657,38.456],"l":[[72.657,37.029,72.359,36.983,72.154,36.901,71.942,36.766,71.802,36.694,71.734,36.684,71.666,36.697,71.598,36.733,71.531,36.845,71.433,37.128,71.505,37.603,71.546,37.796,71.58,37.864,71.582,37.91,71.552,37.933,71.488,37.932,71.39,37.906,71.32,37.902,71.279,37.918,71.283,38.008,71.333,38.17,71.256,38.307,71.052,38.418,70.879,38.456,70.736,38.423,70.616,38.334,70.519,38.192,70.418,38.075,70.313,37.985,70.239,37.941,70.215,37.924,70.199,37.886,70.255,37.765,70.251,37.664,70.189,37.582,70.12,37.543,70.045,37.547,69.985,37.566,69.941,37.6,69.821,37.61,69.626,37.594,69.492,37.553,69.42,37.487,69.399,37.399,69.43,37.291,69.415,37.208,69.354,37.15,69.304,37.117,69.265,37.108,69.18,37.158,69.05,37.267,68.96,37.325,68.912,37.334,68.885,37.328,68.855,37.317,68.838,37.303,68.824,37.271,68.782,37.258,68.723,37.268,68.669,37.258,68.637,37.224,68.547,37.183,68.387,37.137,68.3,37.088,68.285,37.036,68.261,37.013,68.212,37.022]]},{"n":"Paraiba","z":5,"b":[-46.021,-23.396,-41.022,-21.582],"l":[[-45.049,-23.049,-45.505,-23.309,-45.606,-23.351,-45.709,-23.361,-45.85,-23.354,-45.934,-23.365,-45.961,-23.393,-45.99,-23.396,-46.021,-23.375,-46.004,-23.309,-45.941,-23.197,-45.861,-23.127,-45.765,-23.096,-45.666,-23.038,-45.565,-22.952,-45.446,-22.893,-45.31,-22.86,-45.167,-22.773,-45.016,-22.631,-44.863,-22.547,-44.706,-22.521,-44.651,-22.524,-44.593,-22.538,-44.522,-22.517,-44.362,-22.435,-44.256,-22.426,-44.205,-22.49,-44.151,-22.508,-44.094,-22.48,-44.04,-22.477,-43.989,-22.498,-43.852,-22.438,-43.629,-22.299,-43.425,-22.196,-43.144,-22.097],[-43.138,-22.097,-42.28,-21.714,-41.991,-21.6,-41.855,-21.582,-41.775,-21.619,-41.684,-21.631,-41.581,-21.618,-41.467,-21.646,-41.341,-21.714,-41.213,-21.714,-41.022,-21.611]]},{"n":"Paranaíba","z":4,"b":[-49.789,-19.171,-46.314,-18.083],"l":[[-46.314,-19.171,-46.561,-18.852,-46.517,-18.793,-46.514,-18.765,-46.538,-18.748,-46.54,-18.71,-46.519,-18.652,-46.535,-18.564,-46.586,-18.448,-46.697,-18.355,-46.867,-18.284,-46.992,-18.258,-47.071,-18.277,-47.152,-18.228,-47.235,-18.112,-47.319,-18.083,-47.447,-18.171],[-48.018,-18.434,-48.227,-18.34],[-49.108,-18.388,-49.179,-18.405,-49.247,-18.468,-49.337,-18.585,-49.417,-18.603,-49.488,-18.522,-49.553,-18.51,-49.612,-18.567,-49.678,-18.6,-49.789,-18.615]]},{"n":"Paraná","z":2.1,"b":[-60.757,-34.038,-50.49,-14.319],"l":[[-59.182,-33.806,-59.082,-33.822,-58.934,-33.884,-58.737,-33.994,-58.591,-34.038,-58.429,-33.991],[-60.39,-33.173,-60.282,-33.259,-59.854,-33.534,-59.548,-33.684,-59.362,-33.709,-59.287,-33.742,-59.241,-33.779,-59.182,-33.806],[-60.39,-33.173,-60.111,-33.249,-59.947,-33.268,-59.823,-33.25,-59.664,-33.315,-59.472,-33.463,-59.328,-33.604,-59.182,-33.806],[-60.391,-33.173,-60.39,-33.173],[-58.605,-27.314,-58.816,-27.438,-58.878,-27.512,-58.85,-27.611,-58.824,-27.718,-58.831,-27.826,-58.862,-27.971,-58.93,-28.076,-59.035,-28.142,-59.087,-28.288,-59.087,-28.514,-59.122,-28.735,-59.192,-28.948,-59.194,-28.981,-59.205,-29.032,-59.227,-29.058,-59.256,-29.083,-59.304,-29.104,-59.44,-29.17,-59.529,-29.25,-59.565,-29.335,-59.605,-29.495,-59.65,-29.73,-59.654,-29.898,-59.615,-29.999,-59.614,-30.11,-59.651,-30.233,-59.656,-30.336,-59.629,-30.421,-59.625,-30.526,-59.637,-30.604,-59.638,-30.669,-59.654,-30.717,-59.675,-30.76,-59.738,-30.83,-60.064,-31.27,-60.302,-31.554,-60.448,-31.666,-60.581,-31.699,-60.666,-31.768,-60.702,-31.871,-60.706,-31.959,-60.676,-32.033,-60.678,-32.136,-60.726,-32.335,-60.757,-32.517,-60.744,-32.645,-60.698,-32.78,-60.631,-32.916,-60.541,-33.053,-60.39,-33.173],[-54.611,-25.433,-54.632,-26.006,-54.678,-26.309,-54.755,-26.533,-54.826,-26.652,-54.889,-26.667,-54.934,-26.703,-54.962,-26.759,-55.014,-26.807,-55.089,-26.845,-55.13,-26.886,-55.136,-26.931,-55.208,-26.96,-55.346,-26.973,-55.427,-27.009,-55.451,-27.068,-55.497,-27.115,-55.565,-27.15,-55.597,-27.208,-55.594,-27.288,-55.633,-27.357,-55.715,-27.415,-55.79,-27.416,-55.859,-27.362,-55.951,-27.326,-56.067,-27.308,-56.164,-27.321,-56.242,-27.367,-56.311,-27.439,-56.37,-27.537,-56.437,-27.554,-56.511,-27.488,-56.603,-27.468,-56.716,-27.494,-56.805,-27.485,-56.872,-27.441,-56.974,-27.436,-57.112,-27.47,-57.391,-27.43,-57.812,-27.317,-58.168,-27.273,-58.605,-27.314],[-53.786,-23.333,-53.908,-23.407,-53.996,-23.57,-54.05,-23.823,-54.125,-23.978,-54.267,-24.066,-54.067,-24.004,-53.99,-23.838,-53.916,-23.577,-53.858,-23.423,-53.786,-23.333],[-52.961,-22.455,-53.116,-22.64,-53.272,-22.752,-53.481,-22.852,-53.62,-23.001,-53.69,-23.198,-53.786,-23.333],[-51.397,-20.441,-51.534,-20.558,-51.593,-20.666,-51.619,-20.807,-51.692,-20.944,-51.811,-21.077,-51.866,-21.183,-51.857,-21.26,-51.873,-21.33,-51.936,-21.423],[-50.49,-19.02,-50.532,-19.087,-50.578,-19.112,-50.642,-19.118,-50.713,-19.161,-50.792,-19.238,-50.844,-19.311,-50.869,-19.377,-50.871,-19.424,-50.864,-19.438,-50.881,-19.459,-50.924,-19.464,-50.964,-19.511,-50.997,-19.668],[-55.075,-14.319,-55.432,-14.357,-55.8,-14.549,-56.006,-14.635,-56.173,-14.68,-56.316,-14.778,-56.436,-14.928,-56.497,-15.037,-56.497,-15.104,-56.444,-15.171,-56.337,-15.239,-56.275,-15.328,-56.258,-15.437,-56.205,-15.531,-56.117,-15.609,-56.089,-15.68,-56.121,-15.746,-56.133,-15.801,-56.122,-15.845,-56.07,-15.896,-55.977,-15.952,-55.917,-16.009,-55.89,-16.069,-55.905,-16.13,-55.963,-16.195,-55.977,-16.254,-55.947,-16.309,-55.965,-16.362,-56.031,-16.413,-56.134,-16.453,-56.273,-16.483,-56.382,-16.565,-56.461,-16.699,-56.529,-16.785,-56.586,-16.822,-56.62,-16.872,-56.633,-16.936,-56.627,-16.993,-56.59,-17.067,-56.79,-17.386,-56.888,-17.518,-56.98,-17.595,-57.08,-17.734,-57.198,-17.812,-57.37,-17.876,-57.432,-18.045,-57.386,-18.321,-57.407,-18.497,-57.496,-18.573,-57.549,-18.664,-57.567,-18.769,-57.607,-18.838,-57.668,-18.873,-57.688,-18.919,-57.668,-18.978,-57.588,-19.019,-57.448,-19.042,-57.365,-19.07,-57.342,-19.103,-57.304,-19.233,-57.3,-19.297,-57.309,-19.351,-57.345,-19.414,-57.408,-19.485,-57.427,-19.529,-57.403,-19.546,-57.413,-19.587,-57.458,-19.652,-57.522,-19.697,-57.606,-19.721,-57.649,-19.752,-57.649,-19.789,-57.711,-19.861,-57.835,-19.97,-57.962,-20.059,-58.094,-20.13,-58.149,-20.177,-58.129,-20.202,-58.129,-20.226,-58.148,-20.25,-58.115,-20.304,-58.032,-20.39,-57.988,-20.498,-57.984,-20.628,-57.966,-20.687,-57.934,-20.675,-57.904,-20.685,-57.875,-20.715,-57.881,-20.746,-57.921,-20.778,-57.921,-20.803,-57.881,-20.822,-57.874,-20.85,-57.902,-20.887,-57.891,-20.918,-57.843,-20.942,-57.822,-21.009,-57.831,-21.119,-57.852,-21.199,-57.887,-21.25,-57.893,-21.289,-57.868,-21.317,-57.88,-21.375,-57.927,-21.464,-57.937,-21.553,-57.91,-21.643,-57.918,-21.789,-57.981,-22.09,-57.97,-22.215,-57.931,-22.29,-57.862,-22.356,-57.814,-22.455,-57.788,-22.589,-57.817,-22.671,-57.899,-22.701,-57.903,-22.751,-57.829,-22.822,-57.792,-22.898,-57.79,-22.98,-57.713,-23.084,-57.561,-23.21,-57.476,-23.348,-57.459,-23.499,-57.407,-23.613,-57.32,-23.692,-57.293,-23.779,-57.327,-23.875,-57.299,-23.972,-57.208,-24.069,-57.172,-24.132,-57.19,-24.162,-57.17,-24.223,-57.112,-24.318,-57.08,-24.403,-57.073,-24.478,-57.084,-24.547,-57.13,-24.64,-57.124,-24.724,-57.154,-24.787,-57.217,-24.857,-57.237,-24.915,-57.212,-24.961,-57.221,-24.984,-57.264,-24.983,-57.32,-25.015,-57.388,-25.08,-57.448,-25.112,-57.499,-25.11,-57.561,-25.154,-57.635,-25.245,-57.644,-25.328,-57.587,-25.405,-57.563,-25.474,-57.572,-25.534,-57.626,-25.599,-57.725,-25.667,-57.755,-25.697,-57.757,-25.726,-57.782,-25.784,-57.865,-25.907,-57.886,-25.964,-57.891,-26.007,-57.943,-26.053,-58.082,-26.139,-58.111,-26.18,-58.118,-26.225,-58.136,-26.251,-58.155,-26.263,-58.181,-26.307,-58.203,-26.381,-58.205,-26.477,-58.188,-26.593,-58.191,-26.63,-58.222,-26.65,-58.239,-26.677,-58.246,-26.731,-58.272,-26.771,-58.318,-26.796,-58.335,-26.825,-58.323,-26.858,-58.356,-26.89,-58.436,-26.922,-58.485,-26.968,-58.503,-27.03,-58.548,-27.084,-58.619,-27.132,-58.642,-27.196,-58.605,-27.314]]},{"n":"Peace","z":2.1,"b":[-122.222,55.934,-111.411,59.152],"l":[[-122.222,56.021,-122.17,55.972,-122.118,55.962,-122.048,55.974,-121.951,56.017,-121.826,56.091,-121.631,56.165,-121.365,56.24,-121.039,56.238,-120.654,56.16,-120.389,56.121,-120.244,56.122,-119.735,56.18,-119.492,56.231,-119.298,56.294,-119.145,56.316,-119.032,56.296,-118.978,56.267,-118.984,56.23,-118.963,56.188,-118.917,56.14,-118.907,56.1,-118.917,56.07,-118.869,56.017,-118.716,55.956,-118.449,55.934,-118.068,55.949,-117.739,56.023,-117.462,56.153,-117.273,56.288,-117.171,56.427,-117.128,56.536,-117.143,56.616,-117.177,56.669,-117.228,56.705,-117.23,56.733,-117.253,56.755,-117.291,56.787,-117.319,56.843,-117.295,56.925,-117.219,57.033,-117.209,57.083,-117.213,57.117,-117.195,57.137,-117.17,57.148,-117.121,57.18,-117.097,57.229,-117.106,57.268,-117.133,57.309,-117.127,57.357,-117.083,57.417,-117.091,57.477,-117.105,57.507,-117.095,57.53,-117.058,57.558,-117.054,57.6,-117.076,57.622,-117.094,57.664,-117.088,57.692,-117.093,57.719,-117.106,57.752,-117.098,57.774,-117.107,57.795,-117.1,57.823,-117.078,57.845,-117.083,57.886,-117.133,57.921,-117.148,57.971,-117.116,58.024,-117.053,58.067,-116.956,58.099,-116.832,58.112,-116.68,58.107,-116.592,58.115,-116.569,58.137,-116.573,58.162,-116.604,58.189,-116.612,58.216,-116.598,58.242,-116.616,58.261,-116.642,58.278,-116.644,58.292,-116.6,58.312,-116.544,58.31,-116.495,58.306,-116.496,58.278,-116.482,58.263,-116.455,58.259,-116.384,58.293,-116.271,58.363,-116.144,58.416,-116.003,58.45,-115.767,58.439,-115.435,58.382,-115.169,58.359,-114.968,58.37,-114.694,58.444,-114.347,58.58,-114.086,58.664,-113.823,58.71],[-113.714,58.669,-113.271,58.779,-113.127,58.832,-113.134,58.865,-113.101,58.889,-113.026,58.903,-112.944,58.958,-112.853,59.054,-112.769,59.118,-112.692,59.152,-112.528,59.143,-112.277,59.092,-112.102,59.078,-112.001,59.102,-111.944,59.099,-111.931,59.07,-111.962,59.035,-112.037,58.994,-112.059,58.963,-112.03,58.941,-111.966,58.94,-111.868,58.961,-111.76,58.953,-111.642,58.918,-111.539,58.927,-111.411,59.005]]},{"n":"Pechora","z":4,"b":[52.126,61.699,59.287,68.27],"l":[[59.287,62.058,59.175,62.022,59.076,62.022,58.89,62.057,58.777,61.996,58.639,62.008,58.438,62.073,58.234,62.067,58.027,61.988,57.909,61.913,57.879,61.84,57.828,61.806,57.756,61.812,57.617,61.783,57.412,61.72,57.229,61.699,57.009,61.728,56.962,61.763,56.882,61.797,56.857,61.803,56.842,61.816,56.851,61.846,56.836,61.903,56.796,61.984,56.729,62.049,56.666,62.082,56.641,62.1,56.631,62.13,56.63,62.16,56.644,62.178,56.7,62.234,56.726,62.344,56.706,62.489,56.625,62.595,56.483,62.661,56.36,62.701,56.257,62.715,56.213,62.753,56.227,62.817,56.201,62.884,56.103,62.99,55.936,63.16,55.941,63.191,55.956,63.209,55.993,63.22,56.056,63.196,56.174,63.222,56.292,63.29,56.37,63.305,56.419,63.313,56.441,63.35,56.445,63.383,56.47,63.416,56.519,63.427,56.541,63.442,56.53,63.464,56.478,63.502,56.467,63.539,56.504,63.566,56.556,63.589,56.639,63.661,56.712,63.763,56.944,63.839,57.336,63.89,57.47,63.912,57.507,63.942,57.511,63.986,57.525,64.017,57.584,64.052,57.603,64.09,57.577,64.123,57.581,64.16,57.559,64.197,57.565,64.231,57.579,64.258,57.624,64.29,57.638,64.325,57.673,64.34,57.716,64.373,57.73,64.397,57.773,64.411,57.783,64.443,57.787,64.474,57.793,64.496,57.805,64.535,57.807,64.579,57.828,64.651,57.879,64.74,57.844,64.806,57.722,64.847,57.608,64.908,57.503,64.988,57.4,65.029,57.301,65.034,57.256,65.06,57.265,65.108,57.203,65.137,57.068,65.147,57.016,65.241,57.047,65.421,56.967,65.568,56.777,65.683,56.744,65.789,56.777,65.847,56.834,65.912,56.837,65.971,56.806,66.003,56.736,66.033,56.621,66.122,56.491,66.257,56.283,66.325,55.998,66.326,55.763,66.302,55.412,66.2,55.264,66.142,55.171,66.065,55.133,65.968,55.093,65.91,55.029,65.878,54.836,65.752,54.711,65.702,54.588,65.686,54.456,65.625,54.315,65.516,54.103,65.458,53.822,65.449,53.588,65.417,53.403,65.361,53.194,65.333,52.959,65.332,52.67,65.357,52.327,65.408,52.153,65.466,52.15,65.532,52.179,65.629,52.239,65.759,52.349,65.869,52.507,65.96,52.567,66.02,52.528,66.049,52.507,66.111,52.506,66.206,52.487,66.287,52.451,66.354,52.454,66.404,52.496,66.438,52.504,66.474,52.478,66.514,52.471,66.556,52.484,66.599,52.451,66.651,52.373,66.71,52.354,66.831,52.392,67.014,52.381,67.115,52.287,67.145,52.169,67.26,52.132,67.318,52.136,67.358,52.17,67.396,52.234,67.43,52.231,67.456,52.16,67.473,52.126,67.507,52.128,67.557,52.247,67.614,52.483,67.678,52.627,67.699],[52.627,67.699,52.743,67.755,53.107,67.864],[52.627,67.699,52.68,67.678,52.813,67.673,53.025,67.684,53.184,67.734,53.291,67.824,53.514,67.911,53.852,67.995,54.067,68.081,54.207,68.209,54.233,68.266],[53.107,67.864,53.215,67.933,53.085,67.965,53.027,68.011,53.043,68.069,53.093,68.118,53.179,68.157,53.227,68.196,53.242,68.242,53.262,68.27],[53.914,68.231,53.749,68.175,53.672,68.125,53.621,68.057,53.495,67.987,53.192,67.88,53.107,67.864]]},{"n":"Pend Oreille","z":5,"b":[-117.682,48.178,-116.569,49.027],"l":[[-117.682,49.015,-117.414,49.027,-117.337,49.007,-117.324,48.961,-117.339,48.905,-117.383,48.84,-117.391,48.753,-117.363,48.645,-117.325,48.569,-117.278,48.527,-117.249,48.447,-117.239,48.33,-117.122,48.241,-116.901,48.182,-116.761,48.178,-116.705,48.23,-116.65,48.254,-116.569,48.25]]},{"n":"Pilcomayo","z":5,"b":[-66.444,-25.328,-57.644,-19.12],"l":[[-66.444,-19.37,-66.387,-19.308,-66.373,-19.268,-66.384,-19.23,-66.359,-19.196,-66.297,-19.165,-66.207,-19.206,-66.089,-19.318,-65.98,-19.366,-65.827,-19.342,-65.778,-19.25,-65.702,-19.186,-65.582,-19.12,-65.465,-19.143,-65.35,-19.254,-65.252,-19.328,-65.171,-19.362,-65.078,-19.377,-64.973,-19.373,-64.84,-19.545,-64.679,-19.894,-64.59,-20.157,-64.572,-20.336,-64.499,-20.441,-64.371,-20.472,-64.274,-20.586,-64.209,-20.785,-64.132,-20.899,-64.044,-20.929,-63.975,-21.002,-63.922,-21.117,-63.843,-21.176,-63.736,-21.178,-63.552,-21.242,-63.293,-21.367,-63.122,-21.466,-63.041,-21.539,-62.977,-21.633,-62.908,-21.806,-62.815,-22.05,-62.745,-22.16,-62.665,-22.218,-62.626,-22.262,-62.626,-22.29,-62.542,-22.35,-62.372,-22.439,-62.214,-22.612,-62.067,-22.869,-61.928,-23.059,-61.799,-23.182,-61.68,-23.269,-61.571,-23.319,-61.513,-23.36,-61.506,-23.392,-61.404,-23.457,-61.208,-23.557,-61.085,-23.656,-61.033,-23.756,-60.84,-23.858,-60.505,-23.964,-60.262,-24.014,-60.11,-24.009,-59.892,-24.094,-59.609,-24.267,-59.435,-24.387,-59.373,-24.454,-59.187,-24.562,-58.724,-24.787,-58.52,-24.843,-58.423,-24.894,-58.365,-24.959,-58.309,-24.979,-58.253,-24.954,-58.136,-24.977,-57.96,-25.049,-57.822,-25.136,-57.644,-25.328]]},{"n":"Pit","z":5,"b":[-122.095,40.784,-120.429,41.709],"l":[[-120.429,41.709,-120.457,41.558,-120.514,41.498,-120.679,41.451,-120.971,41.397,-121.115,41.289,-121.112,41.128,-121.13,41.023,-121.171,40.974,-121.223,40.977,-121.284,41.032,-121.347,41.042,-121.411,41.007,-121.496,41,-121.6,41.02,-121.689,41.015,-121.763,40.984,-121.829,40.986,-121.888,41.02,-121.932,41.02,-121.963,40.986,-121.97,40.947,-121.952,40.901,-121.981,40.854,-122.095,40.784]]},{"n":"Po","z":5,"b":[7.11,44.665,12.523,45.22],"l":[[7.11,44.706,7.326,44.665,7.424,44.687,7.502,44.795,7.619,44.849,7.665,44.925,7.677,45.041,7.731,45.129,7.824,45.19,7.976,45.22,8.185,45.219,8.36,45.199,8.5,45.161,8.594,45.113,8.644,45.054,8.808,45.054,9.224,45.141,9.462,45.1,9.505,45.093,9.55,45.116,9.607,45.129,9.652,45.114,9.699,45.086,9.733,45.083,9.754,45.104,9.782,45.103,9.816,45.08,9.846,45.085,9.87,45.118,9.909,45.133,9.96,45.13,10.008,45.106,10.05,45.06,10.154,45.026,10.317,45.003,10.43,44.972,10.491,44.935,10.557,44.928,10.628,44.953,10.666,44.986,10.671,45.028,10.74,45.057,10.871,45.072,11.02,45.075,11.188,45.065,11.312,45.029,11.392,44.968,11.479,44.927,11.574,44.906,11.684,44.918,11.869,44.985,12.085,45.02,12.199,45.013,12.283,44.976,12.377,44.961,12.523,44.968]]},{"n":"Porcupine","z":5,"b":[-145.453,65.65,-137.491,67.654],"l":[[-139.811,65.65,-139.728,65.748,-139.697,65.811,-139.687,65.903,-139.698,65.933,-139.671,65.959,-139.609,65.989,-139.488,66.002,-139.306,65.998,-139.166,66.016,-139.066,66.056,-138.923,66.177,-138.736,66.38,-138.554,66.511,-138.379,66.57,-138.249,66.642,-138.163,66.727,-138.068,66.768,-137.963,66.767,-137.886,66.787,-137.839,66.83,-137.821,66.862,-137.826,66.886,-137.801,66.902,-137.682,66.928,-137.603,66.986,-137.558,67.001,-137.521,66.992,-137.499,67.003,-137.491,67.032,-137.526,67.054,-137.604,67.07,-137.612,67.102,-137.551,67.152,-137.561,67.178,-137.639,67.18,-137.664,67.2,-137.701,67.237,-137.788,67.297,-137.816,67.339,-137.783,67.361,-137.773,67.395,-137.786,67.442,-137.857,67.469,-137.988,67.475,-138.103,67.505,-138.202,67.558,-138.335,67.581,-138.599,67.58,-138.629,67.594,-138.637,67.612,-138.622,67.634,-138.633,67.648,-138.67,67.654,-139.287,67.548,-139.668,67.575,-139.826,67.574,-139.887,67.554,-139.912,67.535,-139.9,67.517,-139.995,67.539,-140.109,67.538,-140.27,67.514,-140.352,67.492,-140.356,67.471,-140.511,67.454,-140.817,67.443,-141.103,67.409,-141.501,67.328,-141.617,67.29,-141.665,67.252,-141.683,67.201,-141.709,67.171,-141.742,67.162,-142.055,67.164,-142.111,67.168,-142.132,67.178,-142.137,67.197,-142.148,67.211,-142.181,67.22,-142.32,67.168,-142.565,67.054,-142.861,66.998,-143.209,67,-143.457,66.988,-143.606,66.961,-143.668,66.936,-143.644,66.912,-143.643,66.894,-143.664,66.884,-143.781,66.902,-143.851,66.885,-143.921,66.835,-144.041,66.797,-144.212,66.773,-144.32,66.773,-144.363,66.796,-144.39,66.796,-144.403,66.771,-144.465,66.759,-144.575,66.758,-144.686,66.729,-144.797,66.672,-144.911,66.649,-145.027,66.66,-145.197,66.645,-145.453,66.587]]},{"n":"Potomac","z":5,"b":[-78.325,38.889,-77.03,39.686],"l":[[-78.325,39.639,-78.181,39.686,-78.098,39.678,-78.027,39.631,-77.956,39.609,-77.883,39.611,-77.855,39.6,-77.872,39.576,-77.853,39.548,-77.798,39.517,-77.78,39.488,-77.797,39.461,-77.792,39.442,-77.762,39.429,-77.745,39.4,-77.739,39.354,-77.669,39.31,-77.534,39.266,-77.479,39.221,-77.503,39.175,-77.506,39.143,-77.481,39.113,-77.301,39.053,-77.19,38.969,-77.102,38.936,-77.053,38.915,-77.03,38.889]]},{"n":"Purari","z":5,"b":[143.492,-7.841,145.302,-5.938],"l":[[143.492,-5.938,143.639,-6.294,143.638,-6.387,143.682,-6.457,143.769,-6.536,143.878,-6.589,144.01,-6.617,144.124,-6.597,144.221,-6.528,144.301,-6.491,144.363,-6.485,144.494,-6.57,144.757,-6.801,144.781,-6.83,144.797,-6.859,144.802,-6.897,144.831,-6.914,144.884,-6.905,144.926,-6.91,144.959,-6.929,144.97,-6.952,144.966,-6.974,144.984,-6.992,145.042,-6.988,145.083,-7,145.106,-7.026,145.127,-7.03,145.153,-7.019,145.191,-7.027,145.265,-7.078,145.302,-7.166,145.301,-7.291,145.257,-7.361,145.17,-7.374,145.118,-7.411,145.101,-7.473,145.127,-7.538,145.195,-7.608,145.215,-7.687,145.186,-7.787,145.194,-7.841]]},{"n":"Purús","z":5,"b":[-72.426,-10.907,-61.406,-3.676],"l":[[-72.426,-10.845,-72.349,-10.907,-72.221,-10.901,-71.912,-10.824,-71.801,-10.718,-71.689,-10.662,-71.539,-10.621,-71.38,-10.543,-71.214,-10.429,-71.136,-10.356,-71.147,-10.323,-71.126,-10.292,-71.074,-10.263,-71.037,-10.18,-71.013,-10.043,-70.929,-9.912,-70.786,-9.788,-70.708,-9.696,-70.696,-9.635,-70.659,-9.595,-70.596,-9.575,-70.55,-9.514,-70.52,-9.412,-70.455,-9.33,-70.353,-9.266,-70.298,-9.211,-70.291,-9.166,-70.225,-9.123,-70.1,-9.083,-69.994,-9.077,-69.907,-9.104,-69.847,-9.1,-69.815,-9.065,-69.764,-9.047,-69.658,-9.046,-69.616,-9.059,-69.593,-9.045,-69.57,-9.013,-69.541,-9,-69.492,-9.004,-69.419,-8.964,-69.323,-8.874,-69.188,-8.798,-69.014,-8.738,-68.923,-8.731,-68.889,-8.748,-68.867,-8.77,-68.823,-8.782,-68.772,-8.771,-68.711,-8.795,-68.634,-8.883,-68.593,-8.953,-68.588,-9.006,-68.563,-9.028,-68.518,-9.018,-68.488,-9.029,-68.472,-9.06,-68.407,-9.065,-68.292,-9.046,-68.218,-9.046,-68.183,-9.066,-68.105,-9.044,-67.924,-8.947,-67.518,-8.777,-67.378,-8.682,-67.368,-8.603,-67.379,-8.574,-67.398,-8.561,-67.405,-8.535,-67.393,-8.487,-67.4,-8.459,-67.412,-8.443,-67.41,-8.425,-67.355,-8.38,-67.347,-8.343,-67.364,-8.316,-67.358,-8.295,-67.342,-8.254,-67.264,-8.149,-67.233,-8.069,-67.249,-8.014,-67.201,-7.939,-67.09,-7.843,-67.048,-7.798,-67.045,-7.775,-67.025,-7.76,-66.999,-7.76,-66.978,-7.742,-66.968,-7.701,-66.923,-7.672,-66.843,-7.653,-66.789,-7.658,-66.761,-7.685,-66.719,-7.673,-66.662,-7.621,-66.605,-7.606,-66.547,-7.626,-66.511,-7.617,-66.499,-7.577,-66.453,-7.541,-66.374,-7.509,-66.335,-7.523,-66.329,-7.562,-66.298,-7.578,-66.24,-7.567,-66.195,-7.587,-66.165,-7.638,-66.122,-7.658,-66.069,-7.657,-66.044,-7.684,-66.019,-7.696,-65.957,-7.675,-65.911,-7.68,-65.886,-7.702,-65.856,-7.701,-65.835,-7.683,-65.789,-7.684,-65.741,-7.698,-65.673,-7.691,-65.648,-7.662,-65.62,-7.646,-65.578,-7.647,-65.543,-7.632,-65.522,-7.599,-65.487,-7.592,-65.45,-7.596,-65.429,-7.582,-65.431,-7.561,-65.41,-7.549,-65.385,-7.552,-65.373,-7.535,-65.38,-7.514,-65.367,-7.493,-65.302,-7.45,-65.23,-7.411,-65.193,-7.408,-65.167,-7.426,-65.152,-7.4,-65.147,-7.327,-65.121,-7.311,-65.098,-7.307,-65.035,-7.325,-64.99,-7.312,-64.975,-7.268,-64.957,-7.257,-64.933,-7.25,-64.871,-7.276,-64.79,-7.252,-64.69,-7.178,-64.645,-7.131,-64.656,-7.11,-64.646,-7.096,-64.616,-7.087,-64.612,-7.075,-64.609,-7.063,-64.584,-7.049,-64.585,-7.02,-64.636,-6.929,-64.635,-6.872,-64.58,-6.848,-64.565,-6.825,-64.589,-6.803,-64.595,-6.77,-64.586,-6.725,-64.552,-6.702,-64.495,-6.7,-64.462,-6.679,-64.454,-6.638,-64.415,-6.6,-64.345,-6.567,-64.298,-6.474,-64.261,-6.246,-64.304,-6.025,-64.357,-5.934,-64.433,-5.901,-64.454,-5.867,-64.418,-5.833,-64.397,-5.787,-64.392,-5.728,-64.368,-5.695,-64.326,-5.689,-64.311,-5.665,-64.323,-5.622,-64.292,-5.598,-64.218,-5.592,-64.181,-5.603,-64.182,-5.631,-64.163,-5.647,-64.124,-5.652,-64.095,-5.638,-64.075,-5.605,-64.019,-5.589,-63.927,-5.59,-63.857,-5.624,-63.811,-5.69,-63.744,-5.716,-63.657,-5.702,-63.63,-5.721,-63.635,-5.757,-63.624,-5.788,-63.59,-5.811,-63.542,-5.8,-63.48,-5.755,-63.427,-5.742,-63.383,-5.762,-63.304,-5.733,-63.191,-5.655,-63.158,-5.662,-63.144,-5.683,-63.122,-5.716,-63.072,-5.728,-63.052,-5.707,-63.063,-5.622,-63.047,-5.573,-63.002,-5.56,-62.995,-5.531,-62.98,-5.473,-62.957,-5.46,-62.936,-5.437,-62.901,-5.437,-62.856,-5.43,-62.83,-5.392,-62.829,-5.276,-62.839,-5.208,-62.861,-5.187,-62.843,-5.153,-62.785,-5.106,-62.763,-5.061,-62.778,-5.02,-62.748,-4.968,-62.7,-4.944,-62.658,-4.951,-62.617,-4.937,-62.601,-4.913,-62.589,-4.875,-62.551,-4.857,-62.51,-4.862,-62.473,-4.837,-62.462,-4.808,-62.476,-4.776,-62.453,-4.744,-62.393,-4.713,-62.313,-4.704,-62.214,-4.716,-62.115,-4.664,-62.079,-4.631,-62.033,-4.59,-62.016,-4.538,-62.04,-4.487,-62.023,-4.444,-61.992,-4.429,-61.921,-4.415,-61.875,-4.38,-61.854,-4.324,-61.79,-4.28,-61.681,-4.248,-61.564,-4.146,-61.441,-3.973,-61.406,-3.832,-61.468,-3.676]]},{"n":"Pánuco","z":5,"b":[-98.59,21.965,-97.782,22.279],"l":[[-98.59,21.981,-98.471,21.965,-98.425,21.98,-98.414,22.019,-98.376,22.041,-98.311,22.046,-98.258,22.066,-98.217,22.102,-98.178,22.099,-98.139,22.056,-98.097,22.047,-98.051,22.07,-98.047,22.107,-98.051,22.123,-98.015,22.178,-97.966,22.209,-97.871,22.219,-97.782,22.279]]},{"n":"Quan","z":4,"b":[109.529,23.405,110.089,23.796],"l":[[109.529,23.796,109.543,23.729,109.575,23.7,109.629,23.688,109.651,23.665,109.641,23.629,109.663,23.575,109.717,23.502,109.796,23.454,109.9,23.43,109.963,23.431,109.986,23.455,110.021,23.452,110.089,23.405]]},{"n":"R. des Outaouais","z":5,"b":[-79.496,47.488,-76.819,47.938],"l":[[-76.819,47.517,-76.891,47.539,-76.92,47.532,-76.935,47.488],[-77.322,47.573,-77.361,47.578,-77.431,47.557,-77.457,47.556],[-78.813,47.656,-78.987,47.554,-79.068,47.537,-79.113,47.572,-79.225,47.586,-79.496,47.577],[-77.544,47.671,-77.527,47.744],[-77.567,47.83,-77.621,47.818,-77.729,47.733,-77.834,47.728,-77.933,47.79,-78.004,47.784,-78.047,47.709,-78.127,47.688,-78.246,47.722,-78.342,47.764,-78.414,47.815,-78.449,47.861,-78.445,47.903,-78.485,47.929,-78.57,47.938,-78.641,47.925,-78.698,47.888,-78.726,47.823,-78.716,47.709,-78.724,47.684]]},{"n":"Rainy","z":5,"b":[-94.713,48.525,-90.503,49.329],"l":[[-93.377,48.617,-93.464,48.561,-93.564,48.537,-93.708,48.525,-93.804,48.549,-93.852,48.607,-94.055,48.659,-94.414,48.704,-94.621,48.743,-94.675,48.774,-94.705,48.809,-94.713,48.863],[-91.629,48.865,-91.659,48.792,-91.757,48.762,-91.935,48.751,-92.06,48.76,-92.132,48.787,-92.321,48.77,-92.779,48.678],[-90.503,49.329,-90.613,49.219,-90.59,49.169,-90.601,49.13,-90.637,49.086,-90.756,49.054,-90.957,49.032,-91.123,48.973,-91.319,48.828]]},{"n":"Rhein","z":4,"b":[8.751,46.676,9.626,47.671],"l":[[8.751,46.676,9.046,46.758,9.416,46.835],[9.409,46.835,9.517,46.907,9.534,46.963,9.496,47.028,9.479,47.098,9.484,47.173,9.528,47.271,9.609,47.392,9.626,47.467,9.554,47.511],[9.128,47.671,9.183,47.671]]},{"n":"Rhein","z":4,"b":[7.529,47.564,8.881,48.965],"l":[[8.881,47.656,8.7,47.686,8.62,47.662,8.58,47.595,8.482,47.576,8.328,47.607,8.198,47.607,8.094,47.576,7.927,47.564,7.698,47.57,7.565,47.607,7.529,47.674,7.539,47.774,7.593,47.906,7.608,48.003,7.584,48.064,7.617,48.157,7.706,48.28,7.765,48.41,7.795,48.547,7.838,48.636,7.923,48.699,8.23,48.965]]},{"n":"Rhine","z":4,"b":[4.985,48.965,8.498,51.888],"l":[[8.23,48.965,8.417,49.214,8.486,49.33,8.498,49.397,8.489,49.436,8.458,49.447,8.422,49.505,8.381,49.611,8.389,49.69,8.431,49.728,8.437,49.767,8.401,49.815,8.357,49.911,8.305,49.981,8.243,50.023,8.129,50.026,7.962,49.991,7.803,50.042,7.654,50.181,7.607,50.222,7.611,50.266,7.629,50.324,7.608,50.367,7.557,50.4,7.444,50.433,7.344,50.486,7.257,50.559,7.204,50.622,7.185,50.673,7.125,50.734,7.025,50.805,6.995,50.853,6.995,50.904,6.973,50.938,6.968,50.998,6.938,51.04,6.881,51.064,6.851,51.097,6.847,51.139,6.816,51.169,6.758,51.187,6.76,51.227,6.741,51.268,6.69,51.303,6.698,51.364,6.707,51.422,6.698,51.458,6.693,51.487,6.687,51.524,6.624,51.559,6.593,51.592,6.596,51.637,6.357,51.775,6.212,51.837,6.08,51.869,6.039,51.872,5.975,51.877,5.898,51.86,5.78,51.863,5.622,51.888,5.492,51.878,5.389,51.833,5.25,51.813,4.985,51.824]]},{"n":"Rhône","z":5,"b":[4.371,43.401,8.25,46.482],"l":[[4.617,43.71,4.514,43.702,4.467,43.683,4.44,43.65,4.436,43.627,4.444,43.613,4.434,43.591,4.379,43.56,4.371,43.528,4.41,43.447],[6.133,46.205,5.902,46.123,5.814,46.013,5.793,45.849,5.747,45.737,5.676,45.676,5.573,45.704,5.437,45.821,5.35,45.847,5.285,45.812,5.191,45.799,5.058,45.817,4.952,45.808,4.871,45.771,4.824,45.713,4.809,45.633,4.819,45.582,4.853,45.561,4.841,45.513,4.784,45.439,4.79,45.289,4.858,45.063,4.864,44.911,4.806,44.834,4.781,44.761,4.786,44.693,4.772,44.637,4.721,44.573,4.674,44.35,4.675,44.228,4.711,44.133,4.759,44.071,4.82,44.04,4.843,44.012,4.821,43.973,4.69,43.906,4.638,43.828,4.619,43.715,4.631,43.639,4.695,43.579,4.724,43.554,4.735,43.508,4.743,43.449,4.787,43.401],[8.25,46.482,8.068,46.374,7.955,46.335,7.799,46.325,7.653,46.327,7.471,46.277,7.205,46.178,7.089,46.152,7.02,46.197,6.866,46.403]]},{"n":"Rio Grande","z":4,"b":[-107.427,25.871,-97.146,37.809],"l":[[-99.172,26.564,-99.108,26.447,-99.015,26.399,-98.873,26.381,-98.765,26.34,-98.691,26.276,-98.598,26.238,-98.486,26.225,-98.378,26.182,-98.275,26.111,-98.083,26.064,-97.801,26.042,-97.587,25.984,-97.44,25.891,-97.376,25.872,-97.358,25.871,-97.35,25.885,-97.339,25.911,-97.282,25.942,-97.146,25.961],[-101.039,29.46,-101.016,29.401,-100.924,29.315,-100.755,29.183,-100.659,29.069,-100.636,28.973,-100.55,28.821,-100.399,28.614,-100.332,28.503,-100.348,28.486,-100.336,28.428,-100.296,28.328,-100.221,28.243,-100.112,28.173,-100.001,28.048,-99.89,27.867,-99.754,27.73,-99.595,27.636,-99.505,27.548,-99.484,27.467,-99.486,27.398,-99.51,27.34,-99.5,27.286,-99.455,27.234,-99.44,27.17,-99.458,27.082,-99.457,27.057],[-106.566,31.82,-106.436,31.764,-106.347,31.679,-106.256,31.545,-106.148,31.451,-106.024,31.398,-105.813,31.241,-105.514,30.981,-105.276,30.807,-105.098,30.721,-104.979,30.646,-104.918,30.583,-104.836,30.448,-104.681,30.134,-104.681,29.991,-104.622,29.854,-104.504,29.678,-104.401,29.574,-104.312,29.542,-104.216,29.48,-104.111,29.386,-103.99,29.323,-103.853,29.291,-103.664,29.207,-103.423,29.071,-103.258,29.001,-103.168,28.998,-103.09,29.042,-103.023,29.132,-102.957,29.19,-102.892,29.216,-102.866,29.258,-102.878,29.315,-102.834,29.444,-102.734,29.644,-102.615,29.752,-102.476,29.769,-102.386,29.807,-102.343,29.865,-102.269,29.871,-102.163,29.825,-101.991,29.796,-101.752,29.782,-101.612,29.787,-101.569,29.809,-101.546,29.808,-101.545,29.784,-101.509,29.773,-101.44,29.777,-101.38,29.743,-101.304,29.634],[-107.293,32.91,-107.24,32.743,-107.172,32.674,-107.07,32.649,-106.975,32.584,-106.889,32.479,-106.846,32.399,-106.847,32.346,-106.777,32.195,-106.566,31.82],[-107.181,33.173,-107.28,33.116],[-107.427,37.776,-107.067,37.744,-106.924,37.809,-106.814,37.792,-106.688,37.716,-106.542,37.678,-106.374,37.678,-106.196,37.647,-106.007,37.584,-105.879,37.525,-105.811,37.469,-105.763,37.406,-105.735,37.335,-105.732,37.24,-105.752,37.119,-105.743,36.98,-105.703,36.823,-105.695,36.651,-105.719,36.464,-105.761,36.339,-105.85,36.244,-106.006,36.133,-106.073,36.048,-106.101,35.951,-106.166,35.855,-106.268,35.76,-106.343,35.648,-106.392,35.519,-106.487,35.376,-106.628,35.218,-106.692,35.089,-106.682,34.989,-106.716,34.781,-106.796,34.465,-106.85,34.295,-106.878,34.269,-106.886,34.147,-106.874,33.928,-106.928,33.749,-107.048,33.61,-107.123,33.494,-107.17,33.356]]},{"n":"Rosetta Branch","z":2,"b":[30.395,30.121,31.238,31.458],"l":[[31.238,30.121,31.212,30.139,31.163,30.151,31.053,30.192,30.972,30.245,30.935,30.306,30.896,30.335,30.855,30.354,30.858,30.384,30.848,30.471,30.786,30.666,30.77,30.78,30.785,30.811,30.772,30.844,30.762,30.866,30.763,30.944,30.747,31.011,30.715,31.065,30.656,31.118,30.569,31.17,30.523,31.229,30.52,31.295,30.488,31.357,30.395,31.458]]},{"n":"Ruki","z":5,"b":[18.184,-0.219,18.951,0.082],"l":[[18.951,-0.219,18.804,-0.121,18.662,-0.069,18.475,-0.031,18.36,0.012,18.318,0.062,18.272,0.082,18.184,0.063]]},{"n":"Rupert","z":5,"b":[-78.731,51.07,-73.641,51.546],"l":[[-74.818,51.412,-74.59,51.48,-74.491,51.49,-74.445,51.467,-74.389,51.462,-74.337,51.46,-74.297,51.435,-74.256,51.405,-74.169,51.376,-74.073,51.371,-73.966,51.387,-73.891,51.349,-73.847,51.257,-73.837,51.175,-73.861,51.103,-73.815,51.07,-73.641,51.078],[-75.204,51.446,-75.229,51.485,-75.226,51.508,-75.242,51.527,-75.327,51.54,-75.481,51.546,-75.822,51.5,-76.553,51.364],[-78.731,51.497,-78.69,51.487,-78.457,51.397,-78.157,51.362,-77.713,51.354,-77.304,51.378,-76.931,51.435,-76.821,51.415]]},{"n":"Ruvuma","z":5,"b":[35.199,-11.716,40.464,-10.464],"l":[[35.599,-10.807,35.358,-10.712,35.26,-10.696,35.224,-10.726,35.204,-10.776,35.199,-10.879,35.368,-11.406,35.451,-11.59,35.504,-11.605,35.564,-11.602,35.631,-11.582,35.705,-11.532,35.785,-11.453,35.911,-11.455,36.082,-11.537,36.175,-11.609,36.191,-11.671,36.306,-11.706,36.519,-11.716,36.674,-11.684,36.771,-11.61,36.873,-11.571,36.979,-11.567,37.059,-11.592,37.114,-11.647,37.218,-11.687,37.373,-11.71,37.542,-11.675,37.725,-11.581,37.829,-11.482,37.855,-11.379,37.885,-11.317,37.92,-11.295,38.017,-11.282,38.177,-11.279,38.315,-11.311,38.492,-11.413,38.795,-11.229,38.988,-11.167,39.171,-11.167,39.322,-11.123,39.439,-11.035,39.563,-10.979,39.694,-10.955,39.817,-10.912,39.989,-10.821,40.464,-10.464]]},{"n":"Río Grande de Santiago","z":5,"b":[-105.457,20.31,-102.78,21.822],"l":[[-102.78,20.31,-102.79,20.368,-102.866,20.398,-103.07,20.443,-103.121,20.478,-103.153,20.525,-103.147,20.587,-103.19,20.663,-103.293,20.735,-103.341,20.795,-103.332,20.841,-103.351,20.91,-103.398,21.002,-103.44,21.051,-103.478,21.059,-103.521,21.04,-103.592,20.972],[-103.709,20.938,-103.842,20.989,-103.885,21.027,-103.882,21.068,-103.93,21.118,-104.079,21.205],[-104.079,21.206,-104.18,21.2,-104.253,21.22,-104.33,21.263,-104.364,21.298,-104.442,21.433,-104.526,21.502,-104.634,21.708,-104.664,21.765,-104.724,21.817,-104.779,21.822,-104.892,21.767,-105,21.729,-105.021,21.739,-105.033,21.777],[-105.104,21.822,-105.176,21.804,-105.207,21.795,-105.246,21.756,-105.339,21.705,-105.411,21.691,-105.457,21.672]]},{"n":"S. Branch Potomac","z":5,"b":[-79.571,38.415,-78.325,39.639],"l":[[-79.571,38.415,-79.385,38.634,-79.253,38.901,-79.163,38.999,-79.071,39.017,-78.956,39.124,-78.818,39.32,-78.705,39.447,-78.575,39.533,-78.496,39.533,-78.46,39.556,-78.443,39.601,-78.407,39.628,-78.325,39.639]]},{"n":"Sacramento","z":4.7,"b":[-122.432,38.08,-121.504,40.721],"l":[[-121.581,38.659,-121.514,38.618,-121.504,38.58,-121.528,38.532,-121.531,38.475,-121.514,38.41,-121.519,38.366,-121.546,38.342,-121.544,38.311,-121.512,38.273,-121.544,38.228,-121.572,38.195,-121.689,38.148,-121.708,38.128,-121.725,38.101,-121.749,38.08],[-122.411,40.721,-122.432,40.648,-122.374,40.574,-122.244,40.474,-122.182,40.401,-122.188,40.354,-122.177,40.327,-122.15,40.319,-122.153,40.282,-122.187,40.216,-122.184,40.175,-122.144,40.159,-122.104,40.067,-122.064,39.9,-122.017,39.794,-121.964,39.75,-121.956,39.632,-121.993,39.44,-122.005,39.317,-121.992,39.262,-121.941,39.186,-121.853,39.091,-121.805,39.002,-121.797,38.919,-121.762,38.851,-121.701,38.796,-121.649,38.769,-121.606,38.772,-121.584,38.744,-121.581,38.659]]},{"n":"Saint John","z":5,"b":[-70.264,45.335,-66.053,47.354],"l":[[-70.264,46.183,-70.208,46.315,-70.161,46.374,-70.075,46.42,-70.014,46.494,-69.96,46.514,-69.893,46.504,-69.862,46.518,-69.867,46.558,-69.729,46.7,-69.449,46.943,-69.261,47.08,-69.167,47.113,-69.096,47.122,-69.05,47.108,-68.961,47.135,-68.829,47.203,-68.669,47.253,-68.48,47.286,-68.377,47.316,-68.358,47.345,-68.311,47.354,-68.235,47.346,-68.097,47.275,-67.794,47.074,-67.711,46.984,-67.698,46.868,-67.725,46.696,-67.702,46.584,-67.628,46.532,-67.599,46.492,-67.615,46.462,-67.598,46.407,-67.546,46.327,-67.528,46.227,-67.545,46.109,-67.497,46.03,-67.386,45.993,-67.292,45.977,-67.214,45.985,-67.123,45.964,-67.02,45.914,-66.935,45.912,-66.867,45.959,-66.796,45.982,-66.684,45.985,-66.524,45.899,-66.394,45.858,-66.24,45.834,-66.131,45.757,-66.065,45.628,-66.053,45.569,-66.081,45.52,-66.179,45.435,-66.218,45.379,-66.218,45.355,-66.183,45.335]]},{"n":"Salado","z":5,"b":[-65.865,-31.959,-60.608,-24.208],"l":[[-65.865,-24.208,-65.82,-24.417,-65.855,-24.53,-65.831,-24.642,-65.76,-24.791,-65.649,-24.925,-65.498,-25.046,-65.424,-25.136,-65.427,-25.194,-65.382,-25.235,-65.29,-25.258,-65.205,-25.228,-65.126,-25.144,-65.049,-25.119,-64.974,-25.151,-64.923,-25.202,-64.896,-25.271,-64.83,-25.321,-64.722,-25.351,-64.601,-25.329,-64.465,-25.257,-64.368,-25.185,-64.309,-25.114,-64.253,-25.086,-64.2,-25.102,-64.117,-25.227,-64.006,-25.463,-63.951,-25.613,-63.954,-25.677,-63.897,-25.795,-63.781,-25.966,-63.738,-26.092,-63.769,-26.172,-63.754,-26.306,-63.692,-26.493,-63.676,-26.658,-63.707,-26.801,-63.712,-26.937,-63.691,-27.066,-63.646,-27.206,-63.576,-27.356,-63.546,-27.477,-63.559,-27.617,-63.572,-27.663,-63.529,-27.772,-63.435,-27.958,-63.321,-28.103,-63.188,-28.209,-63.116,-28.296,-63.105,-28.365,-63.058,-28.417,-62.976,-28.452,-62.883,-28.521,-62.78,-28.624,-62.695,-28.779,-62.627,-28.987,-62.504,-29.155,-62.326,-29.283,-62.166,-29.358,-62.023,-29.382,-61.896,-29.363,-61.785,-29.302,-61.703,-29.299,-61.649,-29.355,-61.508,-29.422,-61.281,-29.5,-61.064,-29.597,-60.859,-29.714,-60.742,-29.799,-60.714,-29.852,-60.706,-29.96,-60.717,-30.123,-60.705,-30.223,-60.67,-30.26,-60.658,-30.345,-60.669,-30.477,-60.655,-30.576,-60.618,-30.642,-60.608,-30.695,-60.625,-30.733,-60.679,-30.764,-60.771,-30.787,-60.821,-30.831,-60.835,-30.926,-60.807,-31.07,-60.828,-31.172,-60.889,-31.28,-60.889,-31.341,-60.828,-31.357,-60.787,-31.447,-60.766,-31.61,-60.777,-31.733,-60.819,-31.816,-60.811,-31.882,-60.706,-31.959]]},{"n":"Salween","z":4,"b":[97.332,16.552,98.679,20.951],"l":[[98.508,20.951,98.502,20.828,98.538,20.712,98.613,20.561,98.66,20.414,98.679,20.272,98.552,20.149,98.141,19.99,97.943,19.961,97.865,19.929,97.841,19.884,97.81,19.858,97.771,19.85,97.696,19.718,97.531,19.331,97.457,18.852,97.44,18.831,97.406,18.828,97.353,18.843,97.332,18.778,97.341,18.634,97.381,18.494,97.451,18.36,97.524,18.296,97.599,18.303,97.632,18.29,97.622,18.258,97.652,18.174,97.72,18.037,97.74,17.935,97.699,17.834,97.755,17.571,97.749,17.444,97.7,17.364,97.659,17.251,97.626,17.107,97.609,16.947,97.606,16.772,97.622,16.652,97.668,16.552]]},{"n":"San Joaquin","z":5,"b":[-121.525,36.805,-118.737,38.056],"l":[[-119.698,37,-119.78,36.897,-119.94,36.843,-120.205,36.805,-120.393,36.866,-120.504,37.024,-120.605,37.118,-120.695,37.148,-120.856,37.285,-121.204,37.652],[-118.737,37.096,-118.808,37.203,-119.108,37.412,-119.153,37.441,-119.188,37.456,-119.223,37.438,-119.246,37.405,-119.323,37.253,-119.404,37.167,-119.488,37.149,-119.548,37.12,-119.601,37.062],[-121.204,37.651,-121.255,37.703,-121.282,37.78,-121.303,37.9,-121.347,37.971,-121.445,38.004,-121.468,38.017,-121.496,38.042,-121.525,38.056]]},{"n":"San Juan","z":4.7,"b":[-86.105,-32.389,-67.362,12.208],"l":[[-69.418,-31.283,-69.397,-31.277,-69.185,-31.269,-69.078,-31.295,-69.029,-31.353,-69.004,-31.415,-69.002,-31.481,-68.944,-31.519,-68.828,-31.529,-68.693,-31.515,-68.539,-31.477,-68.451,-31.472,-68.418,-31.512,-68.367,-31.573,-68.357,-31.654,-68.37,-31.775,-68.349,-31.883,-68.294,-31.979,-68.28,-32.044,-68.27,-32.072,-68.226,-32.084,-68.089,-32.074,-67.974,-32.112,-67.879,-32.197,-67.744,-32.25,-67.567,-32.27,-67.449,-32.308,-67.362,-32.389],[-84.769,11.118,-84.488,11.044,-84.348,10.98,-84.256,10.901,-84.205,10.841,-84.197,10.802,-84.168,10.78,-84.096,10.776,-83.919,10.735,-83.811,10.743,-83.713,10.786,-83.659,10.837,-83.641,10.915],[-86.105,12.208,-85.871,12.098]]},{"n":"Sanaga","z":5,"b":[9.766,3.624,13.126,4.993],"l":[[13.126,4.933,12.994,4.984,12.901,4.993,12.712,4.96,12.631,4.936,12.548,4.877,12.463,4.782,12.385,4.733,12.28,4.725,12.131,4.575,12.058,4.544,12.012,4.583,11.907,4.542,11.742,4.423,11.596,4.399,11.468,4.469,11.296,4.412,11.079,4.227,10.859,4.108,10.636,4.053,10.502,4.047,10.459,4.088,10.392,4.045,10.304,3.919,10.235,3.859,10.186,3.865,10.116,3.813,10.024,3.703,9.938,3.643,9.82,3.628,9.766,3.624]]},{"n":"Sangha","z":5,"b":[16.09,-1.272,17.237,1.654],"l":[[16.098,1.654,16.09,1.617,16.15,1.487,16.212,1.426,16.281,1.419,16.357,1.366,16.438,1.268,16.501,1.221,16.554,1.184,16.601,1.058,16.664,0.716,16.73,0.524,16.797,0.482,16.86,0.475,16.905,0.456,16.923,0.431,16.964,0.41,16.991,0.408,17.036,0.394,17.074,0.362,17.096,0.316,17.103,0.256,17.131,0.221,17.18,0.211,17.19,0.169,17.161,0.095,17.156,0,17.177,-0.115,17.174,-0.214,17.148,-0.298,17.146,-0.346,17.169,-0.36,17.173,-0.391,17.159,-0.44,17.17,-0.471,17.207,-0.485,17.229,-0.556,17.237,-0.682,17.228,-0.744,17.181,-0.773,17.166,-0.842,17.103,-0.92,16.99,-1.009,16.913,-1.102,16.849,-1.272]]},{"n":"Santa Cruz","z":4.7,"b":[-71.982,-50.24,-68.98,-50.003],"l":[[-71.982,-50.224,-71.362,-50.209,-71.112,-50.215,-71.024,-50.236,-70.937,-50.232,-70.852,-50.202,-70.671,-50.204,-70.395,-50.24,-70.239,-50.24,-70.204,-50.205,-70.154,-50.188,-70.06,-50.187,-69.83,-50.187,-69.722,-50.158,-69.659,-50.101,-69.516,-50.086,-69.294,-50.115,-69.141,-50.101,-69.013,-50.017,-68.98,-50.003]]},{"n":"Saskatchewan","z":4,"b":[-105.065,53.157,-99.23,54.071],"l":[[-99.33,53.159,-99.23,53.185],[-105.065,53.24,-104.901,53.164,-104.819,53.157,-104.765,53.193,-104.571,53.233,-104.238,53.276,-104.027,53.344,-103.894,53.482],[-103.421,53.653,-103.129,53.752,-103.045,53.809,-103.072,53.855,-103.05,53.922,-102.978,54.009,-102.9,54.059,-102.813,54.071,-102.686,54.053,-102.434,53.982],[-102.381,53.966,-102.188,53.904,-102.04,53.884,-101.872,53.885,-101.732,53.904,-101.62,53.942,-101.548,53.981,-101.516,54.021,-101.46,53.994,-101.381,53.903,-101.265,53.862,-101.112,53.874,-101.043,53.859,-101.056,53.817,-101.026,53.761,-100.953,53.69,-100.782,53.66,-100.514,53.67,-100.366,53.654,-100.323,53.591]]},{"n":"Sassandra","z":5,"b":[-7.255,4.953,-6.062,9.714],"l":[[-7.008,6.264,-6.953,6.185,-6.776,6.054,-6.701,5.973,-6.731,5.945,-6.733,5.902,-6.708,5.845,-6.676,5.81,-6.637,5.797,-6.592,5.736,-6.543,5.628,-6.481,5.538,-6.373,5.43,-6.274,5.32,-6.232,5.226,-6.215,5.111,-6.171,5.03,-6.062,4.953],[-7.236,8.945,-7.201,8.828,-7.199,8.747,-7.22,8.665,-7.201,8.549,-7.142,8.398,-7.103,8.172,-7.072,7.719,-7.064,7.643,-7.042,7.611,-7.003,7.596,-6.989,7.553,-7,7.481,-7.026,7.426,-7.066,7.386,-7.075,7.285,-7.042,7.042],[-7.167,9.714,-7.214,9.605,-7.23,9.511,-7.23,9.336,-7.204,9.193,-7.212,9.126,-7.244,9.088,-7.255,9.04,-7.236,8.952]]},{"n":"Savannah","z":5,"b":[-82.819,32.03,-80.872,34.366],"l":[[-82.208,33.664,-82.006,33.523,-81.936,33.447,-81.933,33.39,-81.909,33.349,-81.864,33.325,-81.821,33.274,-81.78,33.195,-81.699,33.127,-81.578,33.069,-81.49,32.936,-81.434,32.728,-81.377,32.607,-81.29,32.557,-81.171,32.38,-81.133,32.275,-81.135,32.182,-81.075,32.11,-80.872,32.03],[-82.819,34.366,-82.589,34.017]]},{"n":"Seine","z":4,"b":[0.439,47.578,4.708,49.494],"l":[[4.708,47.578,4.584,47.74,4.554,47.812,4.574,47.85,4.565,47.891,4.506,47.958,4.408,48.082,4.322,48.145,4.214,48.19,4.096,48.284,3.969,48.427,3.858,48.517,3.763,48.551,3.561,48.526,3.252,48.44,3.038,48.393,2.922,48.385,2.819,48.409,2.687,48.492,2.573,48.533,2.499,48.582,2.428,48.65,2.405,48.699,2.43,48.728,2.43,48.766,2.404,48.811,2.352,48.834,2.296,48.852,2.27,48.875,2.25,48.908,2.217,48.912,2.173,48.904,2.144,48.928,2.138,48.948,2.111,48.958,2.062,48.954,2.008,48.961,1.976,48.996,1.908,49.006,1.803,48.99,1.718,48.992,1.675,49.017,1.668,49.047,1.635,49.06,1.585,49.052,1.497,49.084,1.397,49.16,1.354,49.211,1.314,49.219,1.279,49.212,1.244,49.221,1.238,49.244,1.228,49.282,1.189,49.307,1.092,49.312,1.056,49.328,1.043,49.355,1.023,49.381,0.972,49.378,0.93,49.39,0.897,49.424,0.853,49.415,0.828,49.426,0.785,49.471,0.719,49.48,0.652,49.46,0.585,49.453,0.493,49.494,0.439,49.473]]},{"n":"Selenge (Selenga)","z":2.1,"b":[100.7,49.263,107.554,52.383],"l":[[100.7,49.263,101.061,49.346,101.164,49.41,101.256,49.439,101.37,49.455,101.467,49.445,101.548,49.407,101.696,49.391,101.911,49.397,102.061,49.385,102.148,49.356,102.321,49.355,102.581,49.382,102.738,49.385,102.791,49.363,102.832,49.363,102.86,49.385,102.976,49.377,103.182,49.338,103.444,49.361,103.765,49.446,104.012,49.484,104.187,49.475,104.342,49.513,104.477,49.596,104.763,49.692,105.198,49.803,105.6,49.958,106.155,50.254,106.083,50.375,106.115,50.446,106.226,50.508,106.271,50.559,106.248,50.6,106.255,50.644,106.293,50.69,106.301,50.751,106.277,50.826,106.277,50.885,106.3,50.929,106.394,50.974,106.556,51.021,106.679,51.091,106.763,51.184,106.851,51.252,106.992,51.317,107.063,51.424,107.148,51.478,107.269,51.514,107.391,51.609,107.512,51.763,107.554,51.853,107.516,51.88,107.502,51.932,107.513,52.009,107.5,52.074,107.462,52.125,107.35,52.145,107.162,52.132,107,52.104,106.862,52.059,106.753,52.055,106.672,52.092,106.615,52.145],[106.615,52.145,106.583,52.214,106.614,52.266,106.752,52.316],[106.615,52.145,106.499,52.234,106.477,52.29,106.497,52.383]]},{"n":"Semliki","z":5,"b":[29.367,-1.22,30.478,1.239],"l":[[29.838,-1.2,29.782,-1.141,29.697,-1.149,29.564,-1.204,29.476,-1.22,29.433,-1.199,29.397,-1.158,29.367,-1.098,29.377,-0.952,29.452,-0.607],[29.597,-0.134,29.638,0.113,29.614,0.234,29.537,0.31,29.534,0.386,29.604,0.464,29.661,0.559,29.705,0.671,29.834,0.773,30.047,0.864,30.183,0.973,30.24,1.103,30.321,1.185,30.478,1.239]]},{"n":"Setit","z":5,"b":[35.865,13.875,37.817,14.62],"l":[[37.817,13.875,37.7,13.91,37.629,13.97,37.535,14.117,37.354,14.372,37.257,14.454,37.185,14.446,37.133,14.406,37.099,14.334,37.064,14.289,37.025,14.272,36.941,14.281,36.812,14.315,36.679,14.308,36.542,14.258,36.427,14.25,36.334,14.282,36.22,14.279,36.085,14.242,35.977,14.258,35.895,14.326,35.865,14.425,35.895,14.62]]},{"n":"Shatt al Arab","z":3,"b":[47.468,29.962,48.547,30.968],"l":[[47.468,30.968,47.582,30.831,47.647,30.775,47.702,30.754,47.736,30.704,47.749,30.625,47.785,30.561,47.845,30.512,47.938,30.478,48.066,30.458,48.148,30.417,48.182,30.355,48.226,30.321,48.279,30.316,48.331,30.285,48.383,30.23,48.401,30.188,48.388,30.16,48.399,30.11,48.435,30.038,48.479,30.004,48.547,29.962]]},{"n":"Shiquan","z":3,"b":[79.735,31.4,81.64,32.553],"l":[[81.64,31.45,81.549,31.404,81.5,31.4,81.446,31.432,81.293,31.535,81.225,31.602,81.191,31.667,81.187,31.743,81.213,31.828,81.268,31.9,81.352,31.957,81.366,32.002,81.31,32.033,81.282,32.08,81.283,32.142,81.252,32.224,81.188,32.325,81.029,32.42,80.775,32.509,80.535,32.553,80.309,32.552,80.08,32.525,79.735,32.444]]},{"n":"Shire","z":3,"b":[34.743,-17.705,35.293,-14.401],"l":[[35.278,-14.758,35.293,-14.921,35.251,-15.057,35.107,-15.303,34.96,-15.372,34.874,-15.445,34.802,-15.544,34.758,-15.624,34.743,-15.685,34.751,-15.784,34.795,-15.991,34.889,-16.13,34.925,-16.225,34.934,-16.323,34.975,-16.392,35.049,-16.434,35.134,-16.516,35.23,-16.639,35.281,-16.808,35.291,-17.129,35.291,-17.705],[35.257,-14.504,35.236,-14.401]]},{"n":"Shishhid Gol","z":4,"b":[98,50.552,99.491,51.514],"l":[[98.809,50.627,98.985,50.557,99.063,50.552,99.121,50.61,99.207,50.751,99.322,50.809,99.403,50.898,99.478,51.048,99.491,51.149,99.444,51.2,99.466,51.329,99.424,51.349],[99.31,51.41,99.004,51.473,98.764,51.474,98.487,51.437,98.262,51.442,98,51.514]]},{"n":"Skeena","z":5,"b":[-129.626,54.228,-127.384,57.079],"l":[[-128.57,57.079,-128.284,56.907,-127.851,56.717,-127.621,56.574,-127.448,56.417,-127.384,56.322,-127.428,56.289,-127.543,56.278,-127.728,56.291,-127.858,56.274,-127.931,56.228,-127.967,56.169,-127.965,56.096,-127.939,56.03,-127.89,55.972,-127.889,55.904,-127.936,55.829,-127.891,55.779,-127.753,55.756,-127.703,55.721,-127.739,55.675,-127.72,55.594,-127.645,55.478,-127.627,55.405,-127.665,55.374,-127.685,55.33,-127.685,55.274,-127.717,55.218,-127.781,55.162,-127.903,55.126,-128.085,55.11,-128.225,55.073,-128.323,55.018,-128.374,54.967,-128.377,54.923,-128.351,54.87,-128.294,54.809,-128.317,54.725,-128.47,54.567,-129.484,54.242,-129.542,54.228,-129.578,54.228,-129.626,54.23]]},{"n":"Slave","z":2.1,"b":[-113.704,59.005,-111.384,61.253],"l":[[-111.411,59.005,-111.384,59.153,-111.408,59.213,-111.474,59.234,-111.473,59.262,-111.404,59.296,-111.394,59.346,-111.442,59.411,-111.453,59.49,-111.425,59.582,-111.45,59.683,-111.569,59.848,-111.631,59.922,-111.7,59.966,-111.799,60.005,-111.923,60.026,-112.07,60.029,-112.206,60.097,-112.33,60.232,-112.465,60.317,-112.523,60.332,-112.56,60.346,-112.551,60.362,-112.51,60.369,-112.456,60.381,-112.449,60.41,-112.461,60.438,-112.495,60.466,-112.497,60.49,-112.511,60.52,-112.547,60.543,-112.734,60.591,-112.838,60.643,-112.857,60.701,-112.898,60.739,-112.933,60.743,-112.957,60.754,-112.969,60.786,-112.978,60.801,-113.001,60.802,-113.061,60.777,-113.13,60.769,-113.186,60.784,-113.226,60.832,-113.247,60.911,-113.219,60.974,-113.142,61.02,-113.134,61.054,-113.151,61.069,-113.181,61.087,-113.192,61.12,-113.16,61.158,-113.164,61.172,-113.177,61.182,-113.211,61.188,-113.223,61.207,-113.214,61.226,-113.215,61.24,-113.252,61.249,-113.319,61.253,-113.704,61.246]]},{"n":"Snake","z":4,"b":[-119.036,42.522,-110.631,46.715],"l":[[-113.474,42.675,-113.624,42.578,-113.785,42.538,-114.008,42.522,-114.284,42.554,-114.614,42.635,-114.813,42.707,-114.882,42.771,-114.913,42.825,-114.908,42.871,-114.961,42.9,-115.072,42.913,-115.144,42.941,-115.176,42.985,-115.257,42.989,-115.386,42.953,-115.569,42.956,-115.806,42.999,-115.916,42.995,-115.974,42.978,-116.079,43.019,-116.257,43.129,-116.359,43.212,-116.386,43.269,-116.439,43.304,-116.516,43.316,-116.606,43.364,-116.707,43.448,-116.767,43.514,-116.785,43.562,-116.865,43.617,-117.006,43.679,-117.064,43.734,-117.024,43.806,-116.958,43.964,-116.944,44.038,-116.961,44.079,-116.95,44.119,-116.911,44.157,-116.912,44.199,-116.953,44.245,-117.029,44.276,-117.14,44.29,-117.197,44.344,-117.202,44.436,-117.119,44.578,-116.949,44.77,-116.856,44.906,-116.84,44.985,-116.737,45.169,-116.549,45.457,-116.477,45.641,-116.521,45.722,-116.617,45.799,-116.765,45.873,-116.869,45.958,-116.93,46.056,-116.948,46.124,-116.922,46.164,-116.943,46.232,-117.01,46.33,-117.039,46.397,-117.029,46.433,-117.07,46.449,-117.162,46.445,-117.214,46.468,-117.224,46.517,-117.284,46.582,-117.393,46.664,-117.503,46.709,-117.615,46.715,-117.702,46.698,-117.764,46.655,-117.875,46.618,-118.035,46.586,-118.205,46.577,-118.386,46.59,-118.546,46.527,-118.688,46.387,-118.827,46.291,-119.036,46.206],[-112.864,42.812,-113.022,42.686,-113.135,42.646,-113.316,42.654],[-111.188,43.339,-111.348,43.448,-111.409,43.51,-111.425,43.561,-111.586,43.635,-111.894,43.73,-112.043,43.708,-112.033,43.568,-112.072,43.446,-112.159,43.343,-112.312,43.225,-112.639,43.024],[-110.631,43.837,-110.727,43.62,-110.78,43.535,-110.822,43.512,-110.837,43.478,-110.825,43.435,-110.792,43.402,-110.738,43.378,-110.725,43.336,-110.754,43.274,-110.837,43.226,-110.983,43.19,-111.048,43.207]]},{"n":"Sungari","z":5,"b":[124.675,45.389,132.562,47.769],"l":[[124.675,45.45,124.814,45.446,124.875,45.466,124.902,45.511,124.947,45.523,125.01,45.503,125.052,45.466,125.072,45.413,125.156,45.389,125.302,45.395,125.394,45.42,125.432,45.463,125.549,45.488,125.745,45.495,125.888,45.55,125.979,45.652,126.107,45.702,126.273,45.698,126.415,45.711,126.534,45.741,126.63,45.797,126.704,45.876,126.802,45.943,126.923,45.997,127.083,45.998,127.284,45.945,127.416,45.922,127.479,45.93,127.527,45.961,127.559,46.014,127.64,46.011,127.772,45.953,127.91,45.922,128.056,45.918,128.187,45.932,128.303,45.963,128.419,45.959,128.533,45.921,128.648,45.92,128.762,45.955,128.86,45.948,128.941,45.899,129.036,45.899,129.145,45.947,129.247,46.042,129.342,46.183,129.432,46.277,129.515,46.321,129.577,46.382,129.617,46.458,129.702,46.525,129.831,46.582,129.915,46.649,129.953,46.724,130.003,46.764,130.065,46.767,130.104,46.784,130.12,46.816,130.179,46.833,130.332,46.835],[132.562,47.769,132.414,47.567,132.3,47.48,132.142,47.4,132.044,47.334,132.007,47.28,131.705,47.237,131.137,47.205,130.81,47.151,130.722,47.074,130.645,47.027,130.578,47.01,130.531,46.97,130.503,46.908,130.449,46.867,130.33,46.837]]},{"n":"Soroksari Duna","z":5,"b":[18.935,47.033,19.098,47.481],"l":[[19.06,47.481,19.091,47.439,19.098,47.418,19.094,47.394,19.052,47.354,18.974,47.275,18.949,47.199,18.973,47.145,18.976,47.101,18.935,47.033]]},{"n":"South Saskatchewan","z":5,"b":[-116.422,49.904,-105.065,53.24],"l":[[-106.853,51.263,-107.015,51.388,-107.067,51.464,-107.062,51.533,-106.993,51.608,-106.859,51.687,-106.776,51.748,-106.744,51.792,-106.738,51.853,-106.758,51.932,-106.755,51.976,-106.727,51.986,-106.713,52.012,-106.713,52.055,-106.574,52.198,-106.294,52.441,-106.148,52.616,-106.135,52.722,-106.103,52.808,-106.054,52.873,-105.903,52.932,-105.651,52.983,-105.497,53.046,-105.442,53.122,-105.373,53.153,-105.289,53.139,-105.201,53.159,-105.065,53.24],[-116.422,51.667,-116.357,51.651,-116.319,51.612,-116.271,51.51,-115.86,51.273,-115.658,51.194,-115.53,51.194,-115.414,51.159,-115.312,51.09,-115.1,51.103,-114.781,51.198,-114.48,51.196,-114.197,51.094,-114.041,50.999,-114.01,50.909,-113.94,50.85,-113.832,50.825,-113.581,50.799,-113.447,50.813,-113.31,50.848,-113.19,50.838,-113.088,50.783,-113.003,50.767,-112.933,50.788,-112.809,50.789,-112.632,50.77,-112.524,50.729,-112.485,50.665,-112.444,50.631,-112.4,50.626,-112.357,50.574,-112.316,50.475,-112.263,50.422,-112.197,50.415,-112.168,50.393,-112.173,50.355,-112.151,50.303,-112.101,50.238,-112.031,50.205,-111.941,50.204,-111.825,50.176,-111.684,50.121,-111.609,50.065,-111.6,50.01,-111.624,49.968,-111.643,49.937,-111.607,49.904,-111.523,49.909,-111.422,49.934,-111.342,50.001,-111.264,50.041,-111.188,50.055,-110.775,50.046,-110.66,50.059,-110.681,50.095,-110.664,50.12,-110.609,50.134,-110.58,50.155,-110.577,50.185,-110.594,50.211,-110.632,50.235,-110.632,50.299,-110.594,50.404,-110.529,50.448,-110.436,50.433,-110.391,50.463,-110.393,50.537,-110.344,50.59,-110.245,50.62,-110.158,50.661,-110.084,50.714,-110.054,50.754,-110.068,50.781,-110.054,50.81,-110.013,50.838,-109.998,50.863,-110.011,50.884,-109.977,50.909,-109.897,50.938,-109.634,50.97,-109.188,51.004,-108.925,51.002,-108.846,50.965,-108.73,50.939,-108.576,50.925,-108.441,50.858,-108.326,50.737,-108.203,50.672,-108.006,50.658]]},{"n":"Sprague","z":4.7,"b":[-121.921,42.4,-120.882,42.583],"l":[[-120.882,42.4,-121.048,42.441,-121.437,42.482,-121.604,42.525,-121.679,42.583,-121.776,42.579,-121.889,42.512,-121.921,42.475]]},{"n":"St. Clair","z":3,"b":[-82.645,42.558,-82.417,43.017],"l":[[-82.417,43.017,-82.488,42.74,-82.545,42.625,-82.645,42.558]]},{"n":"St. Lawrence","z":2.1,"b":[-75.792,44.497,-74.709,45.004],"l":[[-74.709,45.004,-74.762,44.999,-74.857,45.004,-74.996,44.97,-75.179,44.899,-75.401,44.772,-75.792,44.497]]},{"n":"Stikine","z":4.7,"b":[-132.358,56.626,-127.596,58.148],"l":[[-128.13,57.292,-127.939,57.37,-127.833,57.394,-127.705,57.389,-127.601,57.432,-127.596,57.49,-127.656,57.576,-127.664,57.639,-127.62,57.678,-127.723,57.706,-127.974,57.722,-128.128,57.762,-128.186,57.826,-128.286,57.89,-128.428,57.956,-128.615,57.976,-128.846,57.952,-129.04,57.949,-129.196,57.966,-129.332,57.96,-129.449,57.931,-129.733,57.975,-130.185,58.093,-130.475,58.148,-130.605,58.14,-130.76,58.105,-130.939,58.041,-131.055,57.988,-131.108,57.945,-131.19,57.909,-131.302,57.882,-131.418,57.821,-131.598,57.679,-131.728,57.553,-131.767,57.467,-131.757,57.379,-131.775,57.333,-131.821,57.329,-131.841,57.306,-131.833,57.263,-131.794,57.214,-131.725,57.159,-131.71,57.116,-131.751,57.086,-131.775,56.978,-131.781,56.792,-131.801,56.753,-131.834,56.715,-131.881,56.703,-132.075,56.711,-132.206,56.696,-132.311,56.639,-132.358,56.626]]},{"n":"Suez Canal","z":5,"b":[32.303,29.974,32.566,31.256],"l":[[32.561,30.182,32.566,29.974],[32.36,30.424,32.303,30.593,32.334,30.689,32.337,30.859,32.324,31.256]]},{"n":"Sukhona","z":4,"b":[38.656,59.302,46.34,60.76],"l":[[39.798,59.525,40.099,59.476,40.215,59.447,40.248,59.421,40.262,59.38,40.26,59.323,40.35,59.302,40.532,59.318,40.684,59.352,40.808,59.404,40.917,59.418,41.011,59.396,41.155,59.391,41.349,59.403,41.558,59.43,41.784,59.473,41.9,59.512,41.906,59.546,41.954,59.584,42.044,59.628,42.169,59.657,42.328,59.67,42.462,59.7,42.57,59.746,42.653,59.816,42.711,59.91,42.853,59.982,43.191,60.057,43.731,60.19,44.032,60.292,44.276,60.407,44.492,60.474,44.679,60.493,44.835,60.526,44.962,60.573,45.156,60.6,45.418,60.605,45.606,60.632,45.72,60.68,45.831,60.697,45.939,60.684,46.051,60.699,46.166,60.742,46.252,60.76,46.34,60.749],[39.053,59.868,38.928,59.939,38.656,59.93]]},{"n":"Susitna","z":5,"b":[-150.57,61.307,-146.684,62.859],"l":[[-146.684,62.505,-146.829,62.576,-146.879,62.615,-146.884,62.646,-146.959,62.676,-147.103,62.705,-147.228,62.702,-147.335,62.668,-147.493,62.689,-147.705,62.765,-147.89,62.812,-148.125,62.838,-148.51,62.849,-148.68,62.839,-148.763,62.811,-148.867,62.813,-148.993,62.844,-149.188,62.859,-149.453,62.855,-149.694,62.803,-149.912,62.702,-150.044,62.619,-150.113,62.519,-150.159,62.434,-150.162,62.355,-150.136,62.255,-150.138,62.193,-150.168,62.169,-150.161,62.14,-150.114,62.106,-150.094,62.049,-150.099,61.968,-150.172,61.843,-150.313,61.676,-150.425,61.577,-150.509,61.547,-150.559,61.488,-150.57,61.364,-150.567,61.307]]},{"n":"Sutlej","z":5,"b":[71.014,29.326,81.376,31.837],"l":[[81.376,30.758,81.294,30.792],[81.199,30.852,80.874,31.054,80.731,31.121,80.628,31.121,80.534,31.066,80.474,31.053,80.418,31.064,80.296,31.125,80.107,31.235,79.98,31.336,79.917,31.426,79.795,31.481,79.616,31.5,79.456,31.55,79.315,31.632,79.171,31.67,79.023,31.664,78.922,31.697,78.869,31.769,78.811,31.816,78.749,31.837,78.652,31.804,78.521,31.718,78.452,31.657,78.445,31.619,78.405,31.6,78.33,31.6,78.284,31.581,78.269,31.541,78.153,31.532,77.937,31.553,77.757,31.512,77.611,31.407,77.495,31.356,77.411,31.358,77.353,31.338,77.323,31.297,77.27,31.267,77.196,31.25,77.09,31.273,76.952,31.337,76.883,31.38,76.884,31.403,76.854,31.413,76.765,31.409],[76.435,31.422,76.384,31.407,76.37,31.387,76.375,31.355,76.535,31.144,76.567,31.053,76.526,31.003,76.36,30.989,76.069,31.011,75.791,31.013,75.527,30.994,75.305,31.035,75.124,31.134,74.984,31.177,74.886,31.165,74.728,31.092,74.51,30.96,74.38,30.893,74.339,30.894,74.216,30.769,74.009,30.52,73.806,30.363,73.505,30.268,73.382,30.229,73.325,30.194,73.295,30.151,73.268,30.139,73.245,30.16,73.158,30.124,73.008,30.03,72.858,29.96,72.71,29.915,72.631,29.876,72.623,29.843,72.554,29.829,72.426,29.835,72.309,29.794,72.202,29.704,72.153,29.64,72.162,29.599,72.121,29.557,72.03,29.513,71.905,29.487,71.747,29.478,71.65,29.452,71.615,29.409,71.565,29.382,71.5,29.372,71.418,29.382,71.32,29.413,71.209,29.413,71.014,29.326]]},{"n":"Severnaya Dvina","z":4,"b":[40.408,60.749,46.617,64.755],"l":[[46.34,60.749,46.456,60.843,46.512,60.91,46.547,60.982,46.565,61.044,46.565,61.096,46.58,61.13,46.61,61.145,46.617,61.186,46.591,61.287,46.255,61.472,45.945,61.595,45.551,61.717,45.291,61.84,45.167,61.964,45.091,62.061,45.063,62.13,44.975,62.206,44.826,62.29,44.562,62.347,44.184,62.377,43.907,62.442,43.732,62.543,43.404,62.67,42.924,62.824,42.657,62.934,42.605,63.002,42.487,63.045,42.212,63.073,42.077,63.141,42.041,63.195,42.057,63.258,42.016,63.329,41.916,63.408,41.872,63.483,41.885,63.553,41.806,63.605,41.634,63.639,41.515,63.681,41.448,63.732,41.47,63.785,41.58,63.84,41.633,63.88,41.628,63.904,41.658,63.944,41.724,63.999,41.752,64.052,41.742,64.102,41.777,64.137,41.857,64.157,41.884,64.192,41.856,64.244,41.654,64.295,41.277,64.345,40.949,64.416,40.53,64.555,40.45,64.63,40.408,64.755]]},{"n":"Svir’","z":4.7,"b":[32.816,60.477,34.846,61.027],"l":[[32.816,60.482,32.863,60.477,32.996,60.511,33.14,60.576,33.29,60.68,33.438,60.738,33.583,60.752,33.713,60.811,33.828,60.915,33.918,60.964,34.017,60.958,34.086,60.926,34.293,60.943,34.846,61.027]]},{"n":"Syr Darya","z":4.7,"b":[60.878,40.225,69.827,46.137],"l":[[69.827,40.273,69.684,40.302,69.563,40.286,69.414,40.234,69.299,40.225,69.218,40.259,69.152,40.338,69.101,40.461,69.087,40.539,69.094,40.577,69.082,40.599,69.02,40.658,68.817,40.797,68.702,40.898,68.675,40.962,68.61,41.022,68.456,41.107],[67.991,41.255,68.008,41.285,68.069,41.336,68.087,41.463,68.051,41.658,68.067,41.834,68.134,41.991,68.186,42.081,68.201,42.099,68.207,42.132,68.192,42.15,68.188,42.182,68.255,42.237,68.285,42.302,68.277,42.377,68.253,42.443,68.211,42.501,68.208,42.554,68.243,42.603,68.255,42.663,68.245,42.735,68.229,42.769,68.224,42.794,68.2,42.825,68.135,42.866,68.119,42.908,68.129,42.947,68.089,42.991,67.929,43.031,67.838,43.121,67.818,43.26,67.789,43.341,67.751,43.365,67.72,43.358,67.693,43.331,67.66,43.343,67.646,43.389,67.605,43.418,67.548,43.421,67.514,43.459,67.504,43.534,67.444,43.631,67.333,43.751,67.259,43.811,67.222,43.812,67.195,43.839,67.176,43.894,67.107,43.949,66.988,44.003,66.902,44.009,66.85,43.967,66.771,43.959,66.665,43.985,66.603,43.989,66.561,44.009,66.533,44.102,66.494,44.152,66.442,44.158,66.418,44.181,66.422,44.218,66.382,44.268,66.299,44.33,66.263,44.371,66.188,44.425,66.019,44.457,65.975,44.509,65.957,44.54,65.916,44.584,65.833,44.64,65.794,44.687,65.791,44.723,65.738,44.748,65.616,44.756,65.493,44.816,65.37,44.927,65.23,44.993,65.072,45.014,64.964,45.012,64.904,44.989,64.784,45.002,64.602,45.052,64.466,45.113,64.375,45.187,64.334,45.234,64.318,45.27,64.261,45.281,64.221,45.322,64.198,45.394,64.112,45.446,63.964,45.48,63.883,45.511,63.869,45.539,63.877,45.567,63.907,45.595,63.876,45.632,63.784,45.68,63.752,45.713,63.7,45.718,63.336,45.635,63.124,45.633,63.005,45.7,62.9,45.719,62.809,45.691,62.733,45.691,62.673,45.72,62.634,45.759,62.616,45.809,62.565,45.813,62.483,45.772,62.376,45.758,62.246,45.772,62.127,45.751,62.019,45.696,61.897,45.705,61.76,45.776,61.703,45.833,61.725,45.875,61.763,45.903,61.817,45.918,61.823,45.949,61.76,46.021],[61.562,46.11,61.264,46.051,61.093,46.058,60.878,46.137]]},{"n":"São Francisco","z":4,"b":[-45.262,-18.204,-36.412,-8.559],"l":[[-45.262,-18.204,-45.242,-18.126,-45.221,-18.103,-45.19,-18.109,-45.16,-18.036,-45.13,-17.886,-45.086,-17.797,-45.028,-17.77,-45.008,-17.717,-45.027,-17.637,-45.007,-17.514,-44.95,-17.347,-44.888,-17.219,-44.823,-17.13,-44.87,-16.94,-45.028,-16.649,-45.089,-16.483,-45.052,-16.444,-45.038,-16.316,-45.049,-15.991,-44.922,-15.959,-44.861,-15.917,-44.823,-15.854,-44.744,-15.795,-44.563,-15.711,-44.271,-15.458,-44.154,-15.311,-44.116,-15.185,-44.054,-15.057,-43.971,-14.926,-43.906,-14.752,-43.861,-14.534,-43.8,-14.37,-43.724,-14.258,-43.654,-14.062,-43.591,-13.781,-43.532,-13.613,-43.479,-13.556,-43.454,-13.408,-43.457,-13.17,-43.443,-13.008,-43.411,-12.923,-43.342,-12.82,-43.238,-12.698,-43.209,-12.559,-43.256,-12.403,-43.265,-12.291,-43.235,-12.222,-43.254,-12.13,-43.323,-12.013,-43.348,-11.931,-43.329,-11.885,-43.336,-11.805,-43.37,-11.691,-43.362,-11.614,-43.314,-11.574,-43.255,-11.451,-43.15,-11.143],[-40.818,-9.438,-40.671,-9.474,-40.552,-9.457,-40.413,-9.397,-40.32,-9.293,-40.273,-9.145,-40.212,-9.102,-40.148,-9.095,-40.078,-9.088,-40.005,-9.058,-39.949,-9.022,-39.909,-8.98,-39.888,-8.924,-39.885,-8.853,-39.835,-8.809,-39.738,-8.791,-39.688,-8.749,-39.683,-8.683,-39.601,-8.62,-39.442,-8.559,-39.327,-8.566,-39.257,-8.64,-39.093,-8.718,-38.835,-8.8,-38.705,-8.88,-38.668,-8.896,-38.619,-8.896,-38.584,-8.866,-38.544,-8.831,-38.505,-8.835,-38.484,-8.878,-38.482,-8.96,-38.433,-9.004,-38.336,-9.01,-38.294,-9.043,-38.308,-9.102,-38.283,-9.204,-38.221,-9.348,-37.938,-9.538,-37.183,-9.893,-37.019,-9.957,-36.956,-10.014,-36.94,-10.084,-36.88,-10.154,-36.777,-10.224,-36.699,-10.257,-36.647,-10.255,-36.607,-10.293,-36.58,-10.371,-36.538,-10.411,-36.483,-10.411,-36.443,-10.432,-36.412,-10.49]]},{"n":"Sénégal","z":4,"b":[-16.535,13.778,-10.892,16.679],"l":[[-10.892,13.778,-10.914,13.789,-10.952,13.801,-11.011,13.856,-11.062,13.972,-11.129,14.045,-11.213,14.074,-11.246,14.105,-11.227,14.138,-11.23,14.192,-11.253,14.267,-11.3,14.343,-11.371,14.418,-11.477,14.49,-11.618,14.557,-11.74,14.598,-11.843,14.612,-11.936,14.647,-12.061,14.729,-12.303,14.817,-12.409,14.889,-12.46,14.975,-12.544,15.039,-12.66,15.082,-12.735,15.131,-12.77,15.187,-12.813,15.224,-12.859,15.243,-12.863,15.262,-12.852,15.29,-12.863,15.34,-12.931,15.453,-12.994,15.505,-13.049,15.497,-13.079,15.51,-13.098,15.535,-13.105,15.572,-13.142,15.603,-13.206,15.617,-13.258,15.7,-13.297,15.854,-13.348,15.973,-13.41,16.059,-13.454,16.091,-13.487,16.097,-13.498,16.11,-13.507,16.135,-13.556,16.144,-13.624,16.118,-13.685,16.127,-13.715,16.169,-13.757,16.173,-13.81,16.138,-13.868,16.148,-13.933,16.203,-13.968,16.257,-13.975,16.311,-14.086,16.419,-14.3,16.58,-14.534,16.656,-14.787,16.646,-14.929,16.654,-14.96,16.679,-14.991,16.677,-15.022,16.647,-15.055,16.641,-15.091,16.657,-15.113,16.645,-15.121,16.604,-15.211,16.583,-15.38,16.582,-15.517,16.557,-15.621,16.507,-15.768,16.485,-15.959,16.492,-16.074,16.51,-16.113,16.54,-16.168,16.547,-16.239,16.531,-16.302,16.451,-16.358,16.307,-16.404,16.225,-16.441,16.205,-16.48,16.097,-16.502,15.917,-16.535,15.838]]},{"n":"Tajo","z":4.7,"b":[-7.118,39.682,-1.906,40.811],"l":[[-5.996,39.852,-6.326,39.764,-6.533,39.746,-6.726,39.77,-6.896,39.757,-7.118,39.682],[-2.733,40.528,-2.961,40.275,-3.1,40.156,-3.226,40.087,-3.381,40.048,-3.565,40.038,-3.739,39.995,-3.901,39.92,-4.059,39.881,-4.213,39.879,-4.318,39.839,-4.428,39.867,-4.628,39.948,-4.757,39.982,-4.816,39.968,-4.889,39.921,-4.978,39.84,-5.071,39.803,-5.217,39.812],[-1.906,40.601,-2.06,40.745,-2.144,40.797,-2.242,40.811,-2.577,40.724]]},{"n":"Tanana","z":5,"b":[-152.048,62.027,-141.395,65.165],"l":[[-142.022,62.027,-142.071,62.066,-142.087,62.109,-142.085,62.171,-142.051,62.217,-141.951,62.264,-141.817,62.306,-141.743,62.355,-141.685,62.424,-141.602,62.48,-141.494,62.523,-141.425,62.564,-141.395,62.604,-141.413,62.673,-141.479,62.771,-141.552,62.829,-141.633,62.847,-141.656,62.872,-141.622,62.905,-141.738,62.983,-142.004,63.105,-142.192,63.165,-142.302,63.165,-142.42,63.186,-142.544,63.227,-142.632,63.279,-142.686,63.34,-142.744,63.368,-142.807,63.362,-142.879,63.378,-142.961,63.417,-143.038,63.43,-143.149,63.411,-143.617,63.404,-143.789,63.42,-143.822,63.457,-143.827,63.499,-143.803,63.544,-143.827,63.593,-143.899,63.645,-144.101,63.688,-144.433,63.722,-144.678,63.791,-144.835,63.894,-144.895,63.958,-144.887,64,-144.915,64.034,-145.032,64.068,-145.158,64.084,-145.294,64.083,-145.439,64.108,-145.594,64.16,-145.726,64.18,-145.834,64.168,-146.029,64.192,-146.309,64.254,-146.505,64.278,-146.672,64.256,-146.934,64.383,-147.021,64.456,-147.021,64.516,-147.08,64.582,-147.197,64.653,-147.322,64.711,-147.456,64.754,-147.626,64.782,-147.832,64.796,-147.993,64.783,-148.163,64.726,-148.566,64.62,-148.801,64.583,-149.116,64.582,-149.138,64.594,-149.14,64.647,-149.123,64.741,-149.131,64.783,-149.156,64.804,-149.156,64.824,-149.129,64.853,-149.139,64.878,-149.187,64.901,-149.307,64.911,-149.499,64.909,-149.676,64.877,-149.839,64.814,-150.082,64.832,-150.404,64.93,-150.674,64.977,-150.89,64.973,-151.09,64.947,-151.273,64.9,-151.415,64.905,-151.517,64.962,-151.553,64.995,-151.544,65.025,-151.565,65.034,-151.623,65.016,-151.675,65.029,-151.697,65.085,-151.793,65.127,-152.048,65.165]]},{"n":"Tapajós","z":4.7,"b":[-58.659,-10.975,-54.639,-2.385],"l":[[-58.659,-10.975,-58.591,-10.664,-58.513,-10.521,-58.404,-10.442,-58.316,-10.277,-58.248,-10.024,-58.228,-9.802,-58.257,-9.609,-58.352,-9.475,-58.512,-9.4,-58.609,-9.303,-58.644,-9.182,-58.64,-9.069,-58.599,-8.963,-58.515,-8.858,-58.387,-8.754,-58.358,-8.691,-58.428,-8.668,-58.427,-8.523,-58.353,-8.256,-58.329,-8.051,-58.354,-7.906,-58.33,-7.78,-58.259,-7.673,-58.224,-7.575,-58.224,-7.485,-58.202,-7.415,-58.136,-7.34,-58.146,-7.263,-58.228,-7.142,-58.386,-6.949,-58.455,-6.794,-58.437,-6.676,-58.375,-6.57,-58.269,-6.478,-58.144,-6.413,-58.002,-6.378,-57.872,-6.318,-57.754,-6.235,-57.681,-6.127,-57.652,-5.996,-57.492,-5.82,-57.2,-5.598,-57.042,-5.435,-57.019,-5.329,-56.97,-5.247,-56.894,-5.188,-56.821,-5.059,-56.75,-4.862,-56.686,-4.745,-56.63,-4.708,-56.56,-4.7,-56.475,-4.722,-56.397,-4.695,-56.326,-4.62,-56.283,-4.533,-56.268,-4.435,-56.155,-4.333,-56.008,-4.26,-55.945,-4.229,-55.786,-4.128,-55.678,-4.03,-55.567,-3.892,-55.394,-3.627,-55.233,-3.279,-55.145,-2.982,-55.077,-2.62,-54.992,-2.423,-54.89,-2.392,-54.798,-2.385,-54.639,-2.419]]},{"n":"Tarim","z":5,"b":[80.888,40.467,86.753,41.826],"l":[[80.888,40.467,81.026,40.519,81.129,40.535,81.242,40.533,81.343,40.574,81.429,40.659,81.546,40.707,81.693,40.719,81.81,40.753,81.896,40.808,82.121,40.886,82.486,40.987,82.749,41.004,82.912,40.939,83.082,40.928,83.26,40.971,83.455,40.998,83.666,41.009,83.793,41.001,83.835,40.975,83.896,40.998,83.975,41.07,84.091,41.131,84.246,41.182,84.477,41.195,84.784,41.169,84.984,41.129,85.077,41.077,85.218,41.028,85.407,40.984,85.617,40.993,85.848,41.054,86.038,41.071,86.185,41.042,86.335,41.037,86.486,41.058,86.505,41.1,86.392,41.163,86.345,41.206,86.372,41.243,86.382,41.278,86.368,41.285,86.334,41.275,86.266,41.297,86.163,41.352,86.061,41.382,85.959,41.388,85.877,41.426,85.814,41.496,85.786,41.571,85.795,41.651,85.829,41.707,85.888,41.738,85.973,41.753,86.085,41.752,86.165,41.772,86.212,41.815,86.305,41.819,86.443,41.786,86.573,41.783,86.753,41.826]]},{"n":"Taritatu","z":5,"b":[138.702,-3.711,140.295,-3.191],"l":[[140.295,-3.711,139.942,-3.629,139.789,-3.552,139.684,-3.528,139.575,-3.531,139.522,-3.493,139.487,-3.471,139.416,-3.481,139.384,-3.473,139.335,-3.439,139.21,-3.443,139.122,-3.403,139.07,-3.318,138.958,-3.254,138.755,-3.204,138.702,-3.191]]},{"n":"Taz","z":5,"b":[78.59,62.637,85.617,67.578],"l":[[84.22,62.637,84.445,62.811,84.582,62.886,84.833,62.966,85.245,63.077,85.506,63.189,85.617,63.303,85.609,63.357,85.482,63.352,85.389,63.391,85.329,63.475,85.184,63.537,84.955,63.576,84.806,63.62,84.738,63.667,84.636,63.696,84.5,63.707,84.323,63.682,84.105,63.623,83.915,63.588,83.753,63.58,83.706,63.595,83.774,63.634,83.765,63.681,83.681,63.736,83.602,63.758,83.529,63.746,83.496,63.751,83.502,63.771,83.459,63.807,83.323,63.884,83.198,63.924,83.109,63.926,83.015,63.902,82.985,63.875,83.018,63.844,83,63.81,82.931,63.775,82.898,63.766,82.902,63.785,82.862,63.784,82.778,63.763,82.686,63.763,82.585,63.784,82.523,63.784,82.502,63.763,82.459,63.767,82.397,63.797,82.38,63.82,82.409,63.835,82.349,63.866,82.2,63.912,82.077,63.967,81.98,64.03,81.885,64.114,81.791,64.22,81.754,64.288,81.773,64.317,81.75,64.35,81.683,64.387,81.669,64.41,81.706,64.42,81.713,64.437,81.687,64.462,81.439,64.543,81.388,64.571,81.441,64.581,81.488,64.606,81.528,64.646,81.537,64.674,81.516,64.691,81.522,64.699,81.553,64.699,81.535,64.732,81.47,64.796,81.468,64.846,81.53,64.881,81.528,64.909,81.463,64.929,81.43,64.979,81.43,65.057,81.528,65.112,81.724,65.144,81.89,65.192,82.025,65.258,82.101,65.309,82.116,65.346,82.088,65.372,82.017,65.388,81.99,65.42,82.005,65.47,82.042,65.488,82.099,65.475,82.12,65.478,82.104,65.497,82.123,65.524,82.176,65.56,82.399,65.651,82.422,65.703,82.337,65.756,82.303,65.815,82.32,65.88,82.279,65.941,82.179,65.999,82.126,66.04,82.119,66.064,82.148,66.109,82.214,66.176,82.212,66.213,82.142,66.219,82.096,66.245,82.073,66.293,82.121,66.336,82.241,66.373,82.272,66.406,82.214,66.433,82.216,66.466,82.277,66.506,82.294,66.538,82.268,66.562,82.262,66.602,82.276,66.656,82.156,66.756,81.9,66.9,81.527,67.011,81.037,67.087,80.725,67.12,80.592,67.11,80.535,67.091,80.552,67.063,80.49,67.073,80.348,67.118,80.252,67.135,80.176,67.117,79.635,67.213,79.451,67.259,79.444,67.286,79.409,67.296,79.347,67.29,79.131,67.36,78.59,67.578]]},{"n":"Tagus","z":4.7,"b":[-8.792,39.078,-7.118,39.682],"l":[[-7.118,39.682,-7.454,39.681,-7.653,39.634,-7.828,39.541,-7.982,39.492,-8.185,39.485,-8.376,39.468,-8.521,39.371,-8.661,39.249,-8.742,39.115,-8.792,39.078]]},{"n":"Tennessee","z":4.7,"b":[-88.472,34.403,-83.969,37.088],"l":[[-87.7,34.773,-87.814,34.763,-87.965,34.844,-88.084,34.933,-88.191,35.012,-88.306,35.126,-88.311,35.185,-88.282,35.239,-88.218,35.287,-88.188,35.329,-88.19,35.366,-88.139,35.391,-88.034,35.404,-87.994,35.418,-88.018,35.434,-88.015,35.464,-87.986,35.511,-87.987,35.579,-88.02,35.67,-88.01,35.767,-87.958,35.868,-87.943,35.925,-87.975,35.945],[-83.969,35.87,-84.229,35.767,-84.344,35.736,-84.4,35.743,-84.457,35.775,-84.542,35.863,-84.598,35.803,-84.63,35.789,-84.656,35.803,-84.701,35.749,-84.766,35.629,-84.82,35.566,-84.862,35.559,-84.89,35.528,-84.902,35.472,-84.924,35.45,-84.957,35.461,-85.013,35.391,-85.092,35.242,-85.182,35.132,-85.285,35.062,-85.347,35.05,-85.367,35.095,-85.392,35.096,-85.424,35.055,-85.46,35.05,-85.501,35.081,-85.559,35.08,-85.635,35.049,-85.68,35.01,-85.694,34.963,-85.839,34.803,-86.115,34.528,-86.294,34.403,-86.378,34.427,-86.462,34.478,-86.546,34.557,-86.666,34.591,-86.899,34.575],[-88.283,37.022,-88.472,37.088]]},{"n":"Teslin","z":3,"b":[-134.913,59.072,-131.208,61.57],"l":[[-131.208,59.087,-131.393,59.072,-131.513,59.084,-131.689,59.135,-131.792,59.141,-131.861,59.178,-131.93,59.248,-131.997,59.364,-132.095,59.607],[-133.245,60.449,-134.288,61.089,-134.647,61.352,-134.667,61.45,-134.737,61.517,-134.913,61.57]]},{"n":"Thames","z":5,"b":[-2.022,51.392,0.425,51.763],"l":[[-2.022,51.686,-1.939,51.648,-1.824,51.652,-1.562,51.7,-1.434,51.705,-1.369,51.725,-1.324,51.763,-1.282,51.759,-1.242,51.713,-1.239,51.682,-1.22,51.65,-1.17,51.628,-1.124,51.587,-1.11,51.525,-1.069,51.485,-0.999,51.465,-0.944,51.479,-0.903,51.527,-0.841,51.554,-0.76,51.56,-0.715,51.549,-0.706,51.521,-0.677,51.504,-0.604,51.494,-0.503,51.416,-0.433,51.392,-0.36,51.398,-0.32,51.419,-0.314,51.455,-0.245,51.48,-0.049,51.501,0.385,51.453,0.425,51.466]]},{"n":"Thelon","z":4.7,"b":[-105.942,62.356,-93.656,64.761],"l":[[-105.942,62.356,-105.664,62.41,-105.305,62.439,-104.772,62.461,-104.586,62.486,-104.544,62.5,-104.55,62.517,-104.598,62.551,-104.676,62.577,-104.734,62.589,-104.757,62.605,-104.735,62.622,-104.743,62.659,-104.763,62.682,-104.772,62.709,-104.744,62.729,-104.761,62.762,-104.819,62.808,-104.801,62.864,-104.769,62.906,-104.767,62.937,-104.817,62.948,-104.846,62.983,-104.834,63.071],[-104.706,63.27,-104.681,63.352,-104.612,63.44,-104.489,63.562,-104.452,63.63,-104.501,63.644,-104.468,63.691,-104.354,63.772,-104.214,63.836,-104.05,63.884,-103.901,63.974,-103.766,64.108,-103.696,64.193,-103.692,64.227,-103.532,64.269,-103.217,64.317,-102.966,64.328,-102.779,64.302,-102.676,64.263,-102.657,64.211,-102.626,64.178,-102.585,64.165,-102.409,64.184,-102.098,64.235,-101.91,64.298,-101.811,64.411],[-94.225,64.076,-93.766,63.994,-93.656,63.973],[-101.536,64.493,-100.795,64.604],[-97.103,64.761,-96.728,64.697,-96.58,64.655,-96.536,64.614,-96.504,64.554,-96.484,64.474,-96.423,64.401,-96.273,64.301]]},{"n":"Tigris","z":4,"b":[42.377,30.999,47.441,37.062],"l":[[42.377,37.062,42.434,36.99,42.466,36.967,42.489,36.96,42.509,36.941,42.513,36.876,42.559,36.832,42.713,36.785,42.759,36.755,42.774,36.733,42.761,36.719,42.858,36.708,42.895,36.691,42.901,36.662,42.886,36.646,42.85,36.644,42.811,36.609,42.771,36.54,42.844,36.478,43.031,36.425,43.169,36.334,43.258,36.205,43.317,36.076,43.346,35.946,43.343,35.858,43.308,35.812,43.297,35.765,43.312,35.718,43.299,35.682,43.261,35.657,43.248,35.586,43.262,35.47,43.348,35.316,43.505,35.126,43.568,35.006,43.538,34.957,43.584,34.832,43.708,34.63,43.785,34.453,43.815,34.3,43.854,34.194,43.904,34.135,43.96,34.098,44.055,34.075,44.232,34.05,44.329,34.019,44.403,33.974,44.427,33.909,44.402,33.825,44.398,33.769,44.417,33.739,44.4,33.697,44.348,33.641,44.318,33.586,44.31,33.531,44.324,33.451,44.361,33.347,44.397,33.293,44.431,33.291,44.48,33.235,44.544,33.127,44.675,33.024,44.871,32.925,44.981,32.887,45.027,32.891,45.041,32.879,45.041,32.856,45.044,32.832,45.08,32.803,45.14,32.785,45.225,32.778,45.26,32.752,45.259,32.714,45.298,32.663,45.384,32.604,45.522,32.556,45.809,32.502,46.03,32.619,46.137,32.632,46.201,32.588,46.344,32.542,46.563,32.494,46.692,32.443,46.73,32.386,46.735,32.321,46.706,32.248,46.699,32.191,46.713,32.149,46.755,32.112,46.827,32.079,46.888,32.014,46.94,31.917,47.008,31.868,47.093,31.865,47.139,31.826,47.146,31.749,47.221,31.627,47.363,31.459,47.434,31.286,47.441,30.999]]},{"n":"Tisa","z":5,"b":[20.061,45.126,20.308,46.261],"l":[[20.179,46.261,20.087,46.166,20.066,46.138,20.064,46.117,20.079,46.076,20.074,46.049,20.061,46.029,20.08,46.001,20.104,45.969,20.124,45.922,20.122,45.89,20.098,45.873,20.105,45.834,20.14,45.778,20.136,45.742,20.11,45.724,20.085,45.689,20.07,45.632,20.105,45.563,20.188,45.483,20.223,45.431,20.209,45.407,20.231,45.349,20.289,45.256,20.308,45.192,20.282,45.126]]},{"n":"Tisza","z":5,"b":[20.066,46.261,24.338,48.415],"l":[[21.346,48.026,21.182,48.006,21.103,47.944,21.057,47.835,20.871,47.69,20.546,47.51,20.4,47.382,20.434,47.306,20.426,47.27,20.377,47.272,20.306,47.242,20.214,47.178,20.175,47.119,20.189,47.066,20.218,47.038,20.192,47.01,20.155,46.98,20.144,46.939,20.125,46.912,20.066,46.874,20.08,46.811,20.166,46.724,20.192,46.653,20.157,46.598,20.163,46.5,20.207,46.36,20.217,46.283,20.179,46.261],[24.256,48.322,24.338,48.294,24.333,48.234,24.234,48.083,24.17,47.955,24.002,47.936,23.709,47.983,23.477,48.049,23.307,48.136,23.146,48.155,22.996,48.106,22.885,48.091,22.815,48.111,22.702,48.107,22.545,48.078,22.42,48.095,22.325,48.159,22.276,48.244,22.273,48.35,22.254,48.407,22.219,48.415,22.133,48.374,21.997,48.284,21.869,48.222,21.75,48.186,21.637,48.17,21.53,48.175,21.465,48.146,21.443,48.082,21.409,48.045,21.339,48.026]]},{"n":"Tobol","z":4.7,"b":[60.91,51.442,68.218,58.168],"l":[[60.91,51.669,61.015,51.492,61.363,51.442,61.516,51.454,61.589,51.513,61.606,51.636,61.565,51.822,61.555,51.949,61.575,52.016,61.56,52.086,61.509,52.158,61.547,52.253,61.674,52.37,61.735,52.442,61.777,52.466,61.835,52.483,62.047,52.487,62.298,52.529,62.589,52.609,62.767,52.696,62.834,52.79,63.024,52.891,63.337,52.999,63.587,53.146,63.864,53.423,63.876,53.567,63.846,53.671,63.778,53.782,63.752,53.887,63.767,53.984,63.798,54.041,63.844,54.056,63.858,54.079,63.859,54.118,63.877,54.153,63.964,54.203,64.007,54.252,64.063,54.303,64.199,54.347,64.59,54.402,64.809,54.48,64.858,54.582,64.916,54.652,64.983,54.689,65.001,54.718,64.971,54.739,64.974,54.773,65.009,54.821,65.008,54.863,64.972,54.899,64.958,54.936,64.966,54.973,65.044,55.064,65.193,55.21,65.255,55.312,65.23,55.37,65.264,55.411,65.357,55.437,65.448,55.501,65.58,55.654,65.611,55.788,65.641,55.841,65.682,55.86,65.695,55.899,65.68,55.96,65.758,56.027,65.928,56.102,65.973,56.152,65.965,56.191,65.997,56.251,66.185,56.308,66.256,56.362,66.26,56.384,66.278,56.425,66.337,56.481,66.349,56.531,66.311,56.574,66.329,56.632,66.402,56.704,66.436,56.766,66.433,56.818,66.465,56.851,66.533,56.862,66.562,56.884,66.552,56.917,66.577,56.961,66.637,57.018,66.731,57.071,66.934,57.158,66.96,57.186,66.963,57.211,66.944,57.235,66.953,57.258,66.993,57.28,67.004,57.297,67.043,57.32,67.076,57.339,67.1,57.375,67.08,57.432,67.097,57.464,67.149,57.471,67.16,57.5,67.129,57.55,67.091,57.58,67.099,57.612,67.115,57.635,67.148,57.678,67.153,57.735,67.208,57.786,67.313,57.831,67.534,57.864,67.871,57.886,68.053,57.91,68.082,57.938,68.083,57.97,68.058,58.006,68.089,58.06,68.218,58.168]]},{"n":"Tocantins","z":4,"b":[-49.717,-15.926,-47.397,-2.652],"l":[[-49.179,-15.926,-49.258,-15.768,-49.35,-15.725,-49.437,-15.634,-49.55,-15.479,-49.592,-15.348,-49.564,-15.242,-49.514,-15.165,-49.444,-15.118,-49.356,-15.011,-49.252,-14.845,-49.155,-14.746,-49.064,-14.715,-49.009,-14.629,-48.992,-14.486,-48.994,-14.387,-49.014,-14.33,-49.008,-14.282,-48.973,-14.243,-48.796,-14.125,-48.713,-14.111,-48.653,-14.148,-48.598,-14.154,-48.546,-14.129,-48.384,-13.996,-48.342,-13.922,-48.348,-13.855,-48.325,-13.828,-48.273,-13.84,-48.216,-13.82,-48.155,-13.767,-48.116,-13.691,-48.1,-13.591,-48.123,-13.516,-48.185,-13.467,-48.198,-13.329,-48.164,-13.103,-48.181,-12.897,-48.284,-12.619,-48.238,-12.512,-48.235,-12.466,-48.259,-12.445,-48.266,-12.413,-48.256,-12.37,-48.307,-12.309,-48.42,-12.23,-48.524,-12.029,-48.646,-11.636,-48.648,-11.536,-48.598,-11.467,-48.516,-11.384,-48.489,-11.257,-48.541,-11.11,-48.526,-10.979,-48.443,-10.863,-48.417,-10.732,-48.448,-10.586,-48.454,-10.43,-48.437,-10.262,-48.405,-10.14,-48.357,-10.065,-48.345,-10.001,-48.367,-9.951,-48.365,-9.908,-48.339,-9.874,-48.346,-9.799,-48.387,-9.685,-48.398,-9.553,-48.379,-9.402,-48.307,-9.19,-48.182,-8.917,-48.127,-8.722,-48.141,-8.607,-48.134,-8.522,-48.106,-8.467,-48.113,-8.369,-48.154,-8.229,-48.162,-8.142,-48.137,-8.108,-48.087,-8.085,-48.011,-8.072,-47.963,-8.003,-47.941,-7.877,-47.862,-7.779,-47.724,-7.709,-47.647,-7.627,-47.63,-7.532,-47.589,-7.459,-47.522,-7.407,-47.495,-7.356,-47.507,-7.306,-47.546,-7.288,-47.612,-7.302,-47.669,-7.28,-47.717,-7.222,-47.716,-7.185,-47.665,-7.167,-47.606,-7.115,-47.537,-7.027,-47.47,-6.796,-47.406,-6.421,-47.397,-6.072,-47.445,-5.75,-47.486,-5.561,-47.52,-5.506,-47.612,-5.455,-47.763,-5.407,-47.854,-5.351,-47.885,-5.286,-47.953,-5.256,-48.059,-5.263,-48.169,-5.241,-48.282,-5.189,-48.378,-5.17,-48.458,-5.181,-48.524,-5.22,-48.574,-5.288,-48.631,-5.33,-48.723,-5.355],[-49.687,-3.835,-49.649,-3.361,-49.66,-3.091,-49.708,-2.867,-49.717,-2.736,-49.67,-2.676,-49.635,-2.652]]},{"n":"Tom’","z":5,"b":[84.436,53.435,89.771,56.911],"l":[[89.771,53.646,89.395,53.435,89.176,53.441,89.08,53.483,89.033,53.564,88.975,53.621,88.907,53.654,88.744,53.677,88.487,53.689,88.316,53.684,88.232,53.661,88.115,53.678,87.967,53.733,87.788,53.777,87.579,53.808,87.401,53.81,87.254,53.781,87.179,53.798,87.175,53.861,87.249,53.937,87.402,54.028,87.491,54.105,87.517,54.171,87.512,54.232,87.477,54.288,87.484,54.344,87.536,54.399,87.54,54.475,87.497,54.571,87.4,54.635,87.173,54.681,87.087,54.716,87.069,54.75,87.051,54.83,87.057,54.856,87.103,54.896,87.121,54.945,87.112,55.002,87.04,55.026,86.842,55.067,86.801,55.103,86.715,55.14,86.594,55.143,86.436,55.109,86.371,55.164,86.338,55.236,86.262,55.332,86.166,55.385,86.048,55.395,85.967,55.434,85.924,55.503,85.833,55.571,85.696,55.639,85.537,55.67,85.358,55.663,85.194,55.68,85.045,55.719,84.967,55.766,84.959,55.822,84.977,55.866,84.989,55.917,84.984,55.953,84.958,55.995,84.947,56.049,84.95,56.141,84.916,56.199,84.906,56.235,84.911,56.295,84.898,56.343,84.9,56.378,84.943,56.402,84.947,56.448,84.912,56.516,84.856,56.567,84.782,56.6,84.717,56.663,84.662,56.755,84.597,56.807,84.523,56.819,84.474,56.846,84.436,56.911]]},{"n":"Tongtian","z":2,"b":[92.993,31.69,98.542,34.741],"l":[[92.993,34.1,93.095,34.195,93.262,34.222,93.529,34.215,93.707,34.239,93.838,34.321,94.01,34.501,94.117,34.567,94.215,34.578,94.338,34.629,94.485,34.719,94.661,34.741,94.865,34.694,95.018,34.624,95.119,34.532,95.212,34.486,95.296,34.486,95.398,34.442,95.518,34.355,95.575,34.293,95.57,34.257,95.596,34.229,95.652,34.21,95.757,34.111,95.91,33.933,96.023,33.824,96.095,33.784,96.153,33.77,96.196,33.781,96.222,33.769,96.231,33.735,96.276,33.725,96.359,33.739,96.418,33.715,96.454,33.654,96.529,33.604,96.644,33.566,96.702,33.516,96.704,33.454,96.723,33.406,96.757,33.373,96.781,33.364,96.794,33.381,96.808,33.371,96.822,33.333,96.873,33.32,96.962,33.334,97.011,33.31,97.02,33.246,97.085,33.144,97.207,33.002,97.291,32.923,97.337,32.906,97.378,32.848,97.413,32.749,97.624,32.614,98.012,32.442,98.209,32.33,98.216,32.278,98.278,32.185,98.395,32.051,98.446,31.941,98.432,31.856,98.454,31.782,98.542,31.69]]},{"n":"Trofimovskaya Protoka","z":4.7,"b":[126.444,72.353,128.258,73.267],"l":[[126.475,72.353,126.444,72.508,126.519,72.594,126.712,72.685,126.811,72.762,126.814,72.827,126.901,72.898,127.072,72.975,127.318,73.032,127.641,73.069,127.911,73.132,128.258,73.267]]},{"n":"Tshuapa","z":5,"b":[20.754,-3.177,24.617,-0.191],"l":[[24.594,-3.177,24.617,-2.995,24.583,-2.902,24.499,-2.839,24.44,-2.769,24.388,-2.656,24.369,-2.477,24.271,-2.309,24.088,-2.092,23.971,-1.828,23.92,-1.519,23.848,-1.402,23.769,-1.361,23.708,-1.331,23.669,-1.273,23.488,-1.211,23.166,-1.146,22.96,-1.064,22.871,-0.965,22.771,-0.905,22.661,-0.885,22.557,-0.851,22.458,-0.802,22.397,-0.754,22.375,-0.706,22.273,-0.651,22.001,-0.559,21.785,-0.486,21.683,-0.438,21.624,-0.389,21.575,-0.371,21.535,-0.384,21.475,-0.367,21.393,-0.317,21.303,-0.295,21.204,-0.3,21.127,-0.285,21.072,-0.249,21.029,-0.261,21.009,-0.249,20.978,-0.205,20.936,-0.191,20.877,-0.197,20.821,-0.222,20.796,-0.269,20.754,-0.282]]},{"n":"Tugaloo","z":5,"b":[-83.356,34.6,-83.167,34.708],"l":[[-83.356,34.708,-83.167,34.6]]},{"n":"Tumatskaya Protoka","z":4.7,"b":[126.298,72.353,126.698,73.335],"l":[[126.475,72.353,126.375,72.435,126.309,72.486,126.298,72.522,126.298,72.585,126.31,72.675,126.333,72.735,126.367,72.766,126.372,72.819,126.349,72.895,126.416,72.954,126.575,72.995,126.669,73.036,126.698,73.078,126.676,73.132,126.565,73.233,126.553,73.335]]},{"n":"Tuotuo","z":2,"b":[90.795,34.091,92.993,34.321],"l":[[90.795,34.3,91.152,34.321,91.647,34.281,91.853,34.235,91.936,34.169,92.037,34.165,92.157,34.222,92.309,34.24,92.493,34.22,92.62,34.18,92.688,34.118,92.79,34.091,92.993,34.1]]},{"n":"Ubangi","z":3,"b":[17.724,-0.549,18.622,3.679],"l":[[18.597,3.679,18.622,3.304,18.491,2.924,18.212,2.415,18.072,2.013,18.073,1.719,18.058,1.535,18.012,1.422,17.902,1.118,17.886,0.857,17.925,0.537,17.888,0.234,17.773,-0.052,17.724,-0.278,17.753,-0.549]]},{"n":"Ucayali","z":2,"b":[-75.28,-15.336,-71.457,-4.445],"l":[[-71.669,-15.336,-71.658,-15.217,-71.637,-15.173,-71.604,-15.164,-71.597,-15.115,-71.617,-15.027,-71.596,-14.936,-71.501,-14.792,-71.457,-14.693,-71.457,-14.568,-71.489,-14.384,-71.519,-14.264,-71.549,-14.207,-71.6,-14.163,-71.672,-14.13,-71.738,-14.04,-71.799,-13.892,-71.914,-13.81,-72.081,-13.793,-72.232,-13.738,-72.364,-13.644,-72.549,-13.547,-72.786,-13.446,-72.952,-13.411,-73.049,-13.442,-73.128,-13.449,-73.221,-13.422,-73.478,-13.175,-73.562,-13.061,-73.558,-12.997,-73.584,-12.897,-73.64,-12.76,-73.716,-12.645,-73.811,-12.551,-73.916,-12.392,-74.031,-12.168,-74.055,-12.007,-73.989,-11.909,-74.031,-11.718,-74.183,-11.433,-74.258,-11.256,-74.257,-11.188,-74.204,-11.154,-74.101,-11.155,-73.983,-11.188,-73.851,-11.252,-73.763,-11.28,-73.719,-11.272,-73.719,-11.195,-73.763,-11.048,-73.774,-10.912,-73.753,-10.787,-73.804,-10.684,-73.927,-10.603,-74.003,-10.509,-74.03,-10.403,-74.033,-10.31,-74.013,-10.233,-74.017,-10.205,-74.038,-10.188,-74.051,-10.173,-74.041,-10.128,-74.017,-10.098,-74.004,-10.074,-74.01,-10.059,-74.029,-10.051,-74.058,-10.015,-74.071,-9.961,-74.057,-9.933,-74.015,-9.933,-73.984,-9.908,-73.963,-9.859,-74.001,-9.833,-74.098,-9.829,-74.151,-9.767,-74.16,-9.648,-74.18,-9.559,-74.212,-9.501,-74.247,-9.487,-74.268,-9.489,-74.281,-9.475,-74.291,-9.453,-74.315,-9.415,-74.379,-9.367,-74.405,-9.293,-74.392,-9.192,-74.412,-9.139,-74.447,-9.115,-74.459,-9.089,-74.454,-9.062,-74.458,-9.041,-74.475,-9.029,-74.539,-8.849,-74.508,-8.732,-74.405,-8.62,-74.331,-8.57,-74.286,-8.583,-74.266,-8.553,-74.269,-8.479,-74.298,-8.457,-74.352,-8.486,-74.4,-8.486,-74.441,-8.455,-74.473,-8.447,-74.496,-8.446,-74.506,-8.419,-74.507,-8.321,-74.532,-8.26,-74.582,-8.237,-74.599,-8.207,-74.584,-8.17,-74.594,-8.153,-74.628,-8.157,-74.639,-8.131,-74.625,-8.076,-74.634,-8.03,-74.665,-7.992,-74.706,-7.977,-74.724,-7.975,-74.743,-7.962,-74.728,-7.943,-74.737,-7.917,-74.816,-7.883,-74.86,-7.885,-74.869,-7.924,-74.899,-7.922,-74.95,-7.879,-74.965,-7.84,-74.946,-7.806,-74.962,-7.751,-75.013,-7.676,-75.013,-7.652,-74.99,-7.647,-74.979,-7.623,-75.015,-7.558,-75.016,-7.497,-74.983,-7.438,-75.019,-7.365,-75.123,-7.279,-75.196,-7.24,-75.238,-7.25,-75.255,-7.237,-75.247,-7.203,-75.269,-7.183,-75.28,-7.159,-75.265,-7.129,-75.151,-7.05,-75.136,-7.012,-75.142,-6.988,-75.166,-6.96,-75.15,-6.902,-75.091,-6.827,-75.084,-6.748,-75.13,-6.665,-75.128,-6.61,-75.106,-6.587,-75.125,-6.536,-75.163,-6.496,-75.178,-6.454,-75.152,-6.419,-75.151,-6.345,-75.187,-6.236,-75.18,-6.157,-75.129,-6.106,-75.1,-6.106,-75.081,-6.112,-75.062,-6.101,-75.052,-6.076,-75.018,-6.063,-74.962,-6.075,-74.888,-6.051,-74.8,-5.98,-74.754,-5.985,-74.722,-5.987,-74.697,-5.975,-74.694,-5.942,-74.647,-5.863,-74.555,-5.758,-74.484,-5.727,-74.434,-5.769,-74.4,-5.774,-74.381,-5.742,-74.358,-5.738,-74.335,-5.749,-74.307,-5.739,-74.285,-5.669,-74.244,-5.626,-74.192,-5.603,-74.177,-5.579,-74.186,-5.556,-74.183,-5.533,-74.165,-5.513,-74.167,-5.484,-74.175,-5.47,-74.173,-5.455,-74.159,-5.435,-74.156,-5.413,-74.162,-5.394,-74.148,-5.373,-74.107,-5.345,-74.089,-5.305,-74.099,-5.216,-74.086,-5.125,-74.049,-5.032,-74.027,-5.021,-74,-5.022,-73.963,-5.063,-73.9,-5.058,-73.818,-4.994,-73.774,-4.938,-73.765,-4.895,-73.74,-4.882,-73.714,-4.887,-73.704,-4.863,-73.724,-4.827,-73.702,-4.771,-73.635,-4.711,-73.594,-4.701,-73.563,-4.696,-73.556,-4.676,-73.56,-4.644,-73.547,-4.606,-73.49,-4.553,-73.489,-4.521,-73.506,-4.51,-73.512,-4.489,-73.489,-4.445]]},{"n":"Uele","z":3,"b":[18.567,3.402,28.586,5.127],"l":[[28.586,3.596,28.43,3.647,28.348,3.65,28.29,3.622,28.239,3.637,28.194,3.695,28.141,3.729,28.08,3.74,27.978,3.72,27.835,3.67,27.696,3.644,27.56,3.643,27.431,3.696,27.309,3.803,27.228,3.851,27.185,3.84,27.17,3.789,27.18,3.698,27.153,3.617,27.089,3.544,27.04,3.517,27.007,3.536,26.954,3.535,26.879,3.515,26.833,3.584,26.816,3.741,26.785,3.864,26.74,3.952,26.697,4.002,26.656,4.012,26.561,3.954,26.335,3.765,26.235,3.68,26.157,3.669,26.068,3.703,25.988,3.715,25.916,3.705,25.859,3.657,25.816,3.569,25.762,3.543,25.696,3.577,25.619,3.576,25.53,3.539,25.428,3.524,25.313,3.531,25.226,3.506,25.167,3.45,25.102,3.415,25.031,3.402,24.979,3.417,24.949,3.462,24.839,3.482,24.649,3.477,24.461,3.533,24.276,3.651,24.081,3.709,23.877,3.708,23.733,3.745,23.651,3.818,23.545,3.845,23.415,3.824,23.3,3.836,23.199,3.88,23.049,3.885,22.85,3.851,22.668,3.909,22.422,4.135,21.908,4.254,21.687,4.281,21.538,4.245,21.35,4.311,21.268,4.323,21.23,4.302,21.126,4.332,20.956,4.413,20.793,4.447,20.647,4.436,20.558,4.463,20.486,4.542,20.394,4.686,20.226,4.83,20.002,4.945,19.862,5.031,19.807,5.089,19.686,5.121,19.501,5.127,19.323,5.071,19.069,4.891,18.832,4.523,18.7,4.383,18.594,4.346,18.567,4.258,18.62,4.117,18.634,3.954,18.597,3.679]]},{"n":"Ural","z":4,"b":[51.11,46.944,59.426,54.52],"l":[[58.631,51.695,58.628,51.619,58.659,51.561,58.723,51.496,58.751,51.425,58.744,51.347,58.713,51.287,58.657,51.247,58.527,51.201,58.325,51.149,58.209,51.106,58.181,51.072,58.045,51.069,57.801,51.096,57.536,51.165,57.248,51.275,56.906,51.353,56.51,51.399,56.224,51.472,56.049,51.573,55.877,51.648,55.709,51.696,55.456,51.735,55.119,51.766,54.65,51.726,54.047,51.613,53.667,51.56,53.511,51.566,53.406,51.551,53.35,51.514,53.227,51.485,53.038,51.464,52.903,51.467,52.82,51.495,52.728,51.498,52.626,51.478,52.554,51.442,52.512,51.391,52.428,51.383,52.301,51.418,52.225,51.421,52.199,51.392,52.128,51.382,52.012,51.391,51.941,51.378,51.916,51.343,51.769,51.286,51.499,51.205,51.395,51.148,51.364,51.075,51.316,50.998,51.221,50.904,51.183,50.826,51.201,50.765,51.189,50.708,51.148,50.657,51.136,50.595,51.154,50.523,51.15,50.478,51.121,50.46,51.116,50.436,51.133,50.406,51.131,50.365,51.11,50.312,51.156,50.253,51.269,50.187,51.329,50.122,51.341,50.027,51.32,49.955,51.347,49.913,51.416,49.875,51.471,49.802,51.512,49.692,51.558,49.631,51.594,49.614,51.608,49.585,51.636,49.546,51.707,49.488,51.739,49.449,51.744,49.42,51.76,49.404,51.828,49.374,51.848,49.352,51.847,49.336,51.859,49.323,51.884,49.314,51.916,49.275,51.948,49.206,51.936,49.138,51.881,49.071,51.85,49.001,51.841,48.927,51.854,48.895,51.88,48.869,51.876,48.815,51.804,48.623,51.776,48.586,51.73,48.56,51.667,48.547,51.625,48.515,51.606,48.469,51.603,48.443,51.61,48.424,51.635,48.405,51.633,48.386,51.602,48.37,51.589,48.318,51.592,48.229,51.584,48.181,51.569,48.167,51.582,48.119,51.64,48.016,51.654,47.882,51.623,47.716,51.674,47.579,51.806,47.47,51.879,47.397,51.895,47.36,51.892,47.331,51.872,47.31,51.877,47.293,51.908,47.28,51.927,47.25,51.934,47.204,51.884,47.121,51.724,46.944],[59.426,54.52,59.307,54.403,59.194,54.325,59.162,54.278,59.172,54.236,59.15,54.175,59.095,54.093,59.102,54.012,59.173,53.932,59.19,53.863,59.156,53.805,59.169,53.746,59.229,53.686,59.254,53.64,59.243,53.61,59.182,53.573,59.072,53.531,59.021,53.464,59.029,53.374,59.067,53.307,59.136,53.265,59.13,53.217,59.049,53.165,58.974,53.071,58.905,52.936,58.904,52.811,58.971,52.697,58.996,52.62,58.986,52.6,58.99,52.569,59.01,52.546,59.058,52.527,59.083,52.513,59.08,52.47,59.062,52.42,58.834,52.196]]},{"n":"Uruguay","z":5,"b":[-58.201,-32.472,-49.737,-27.081],"l":[[-49.737,-28.511,-49.839,-28.465,-49.938,-28.448,-50.069,-28.444,-50.141,-28.453,-50.155,-28.474,-50.233,-28.469,-50.446,-28.422,-50.578,-28.386,-50.736,-28.236,-50.966,-27.959,-51.117,-27.808,-51.19,-27.781,-51.287,-27.716,-51.406,-27.613,-51.488,-27.564,-51.53,-27.569,-51.582,-27.548,-51.645,-27.5,-51.702,-27.488,-51.754,-27.512,-51.8,-27.512,-51.873,-27.509,-51.913,-27.473,-51.984,-27.377,-52.06,-27.314,-52.139,-27.283,-52.192,-27.281,-52.229,-27.293,-52.265,-27.277,-52.304,-27.283,-52.348,-27.276,-52.415,-27.236,-52.511,-27.231,-52.636,-27.259,-52.742,-27.246,-52.826,-27.19,-52.9,-27.177,-52.953,-27.179,-52.994,-27.163,-53.031,-27.137,-53.071,-27.142,-53.104,-27.154,-53.134,-27.14,-53.184,-27.15,-53.227,-27.174,-53.263,-27.163,-53.285,-27.129,-53.305,-27.098,-53.348,-27.081,-53.398,-27.082,-53.457,-27.103,-53.49,-27.133,-53.498,-27.172,-53.54,-27.198,-53.616,-27.21,-53.66,-27.2,-53.672,-27.169,-53.718,-27.158,-53.8,-27.165,-53.852,-27.156,-53.935,-27.161,-54.04,-27.244,-54.114,-27.275,-54.156,-27.254,-54.205,-27.29,-54.26,-27.382,-54.327,-27.423,-54.448,-27.446,-54.484,-27.457,-54.555,-27.454,-54.615,-27.477,-54.666,-27.527,-54.72,-27.545,-54.777,-27.533,-54.829,-27.551,-54.876,-27.599,-54.903,-27.652,-54.91,-27.709,-54.956,-27.747,-55.04,-27.768,-55.069,-27.796,-55.064,-27.836,-55.102,-27.867,-55.244,-27.899,-55.347,-27.956,-55.41,-28.038,-55.477,-28.089,-55.582,-28.121,-55.726,-28.204,-55.746,-28.255,-55.692,-28.303,-55.672,-28.345,-55.687,-28.382,-55.732,-28.387,-55.806,-28.36,-55.859,-28.354,-55.891,-28.37,-55.905,-28.4,-55.904,-28.443,-55.93,-28.473,-55.985,-28.489,-56.02,-28.525,-56.034,-28.581,-56.103,-28.652,-56.226,-28.737,-56.322,-28.852,-56.393,-28.997,-56.476,-29.092,-56.571,-29.138,-56.636,-29.203,-56.672,-29.287,-56.772,-29.418,-56.939,-29.595,-57.089,-29.716,-57.225,-29.782,-57.301,-29.857,-57.318,-29.939,-57.405,-30.034,-57.564,-30.14,-57.646,-30.227,-57.651,-30.295,-57.713,-30.384,-57.831,-30.495,-57.873,-30.591,-57.819,-30.712,-57.811,-30.859,-57.834,-30.917,-57.886,-30.937,-57.898,-30.975,-57.87,-31.031,-57.868,-31.104,-57.893,-31.195,-57.948,-31.299,-58.033,-31.417,-58.054,-31.495,-58.01,-31.534,-57.988,-31.576,-57.989,-31.621,-58.007,-31.685,-58.042,-31.769,-58.096,-31.832,-58.168,-31.873,-58.189,-31.924,-58.16,-31.987,-58.156,-32.052,-58.177,-32.119,-58.165,-32.185,-58.12,-32.249,-58.123,-32.322,-58.201,-32.472]]},{"n":"Usumacinta","z":4.7,"b":[-92.71,16.391,-90.417,18.665],"l":[[-90.417,16.391,-90.471,16.44,-90.576,16.468,-90.634,16.511,-90.634,16.565,-90.66,16.631,-90.711,16.708,-90.816,16.787,-90.976,16.868,-91.112,16.976,-91.224,17.112,-91.319,17.2,-91.392,17.236,-91.417,17.264,-91.417,17.287,-91.4,17.314,-91.414,17.35,-91.466,17.384,-91.496,17.42,-91.504,17.458,-91.488,17.481,-91.449,17.488,-91.45,17.515,-91.49,17.563,-91.473,17.589,-91.398,17.594,-91.366,17.627,-91.378,17.686,-91.4,17.711,-91.433,17.701,-91.446,17.715,-91.44,17.751,-91.456,17.779,-91.494,17.798,-91.524,17.794,-91.546,17.766,-91.567,17.773,-91.587,17.813,-91.613,17.818,-91.645,17.787,-91.652,17.764,-91.64,17.75,-91.641,17.736,-91.69,17.732,-91.732,17.762,-91.79,17.844,-91.85,17.878,-91.912,17.862,-91.993,17.887,-92.093,17.954,-92.137,18.013,-92.127,18.062,-92.144,18.122,-92.189,18.191,-92.212,18.204],[-92.212,18.204,-92.296,18.255,-92.464,18.316,-92.572,18.335,-92.621,18.312,-92.652,18.333,-92.67,18.431,-92.675,18.481,-92.679,18.543,-92.688,18.578,-92.71,18.612],[-92.212,18.204,-92.263,18.361,-92.31,18.426,-92.371,18.467,-92.422,18.531,-92.485,18.665]]},{"n":"Vaal","z":4,"b":[23.628,-29.073,30.21,-26.383],"l":[[25.622,-27.668,25.366,-27.758,25.223,-27.855,25.11,-27.991,24.99,-28.07,24.864,-28.093,24.774,-28.149,24.719,-28.238,24.694,-28.336,24.699,-28.443,24.676,-28.496,24.631,-28.503,24.605,-28.53,24.601,-28.562,24.582,-28.581,24.555,-28.564,24.516,-28.554,24.468,-28.572,24.424,-28.556,24.402,-28.509,24.359,-28.463,24.297,-28.432,24.216,-28.473,24.115,-28.587,24.04,-28.723,23.992,-28.882,23.883,-28.989,23.628,-29.073],[28.124,-26.867,28,-26.717,27.909,-26.694,27.81,-26.75,27.716,-26.768,27.627,-26.748,27.557,-26.772,27.506,-26.84,27.437,-26.871,27.351,-26.865,27.276,-26.887,27.212,-26.936,27.118,-26.937,26.994,-26.889,26.943,-26.89,26.921,-26.933,26.887,-26.948,26.825,-26.935,26.698,-26.99,26.507,-27.115,26.45,-27.177,26.45,-27.217,26.467,-27.235,26.477,-27.279,26.459,-27.328,26.466,-27.362,26.432,-27.398,26.333,-27.407,26.228,-27.46,26.091,-27.603],[30.135,-26.383,30.21,-26.479,30.188,-26.56,30.049,-26.709,29.839,-26.811,29.697,-26.845,29.552,-26.846,29.473,-26.867,29.461,-26.909,29.415,-26.924,29.336,-26.911,29.269,-26.933,29.215,-26.99,29.156,-27.025,29.104,-27.042,29.062,-27.044,28.992,-27.007,28.933,-27.006,28.859,-27.028,28.78,-27.018,28.694,-26.977,28.665,-26.976,28.656,-26.993,28.627,-27.019,28.565,-27.045,28.531,-27.043,28.524,-27.014,28.496,-26.99]]},{"n":"Verde","z":5,"b":[-100.344,21.664,-98.59,22.117],"l":[[-100.344,22.117,-100.171,22.027,-99.993,21.945,-99.902,21.864,-99.84,21.756,-99.782,21.703,-99.73,21.705,-99.69,21.692,-99.665,21.664,-99.621,21.673,-99.559,21.719,-99.46,21.719,-99.324,21.672,-99.191,21.699,-99.06,21.799,-98.952,21.851,-98.868,21.855,-98.807,21.89,-98.769,21.957,-98.728,21.988,-98.662,21.983,-98.59,21.981]]},{"n":"Verkhniy Yenisey","z":4,"b":[91.367,51.566,94.465,58.1],"l":[[94.465,51.734,94.07,51.604,93.646,51.566,92.769,51.581,92.35,51.701,92.152,51.796,92.035,51.905,92.068,52.012,92.251,52.117,92.377,52.214,92.444,52.303,92.438,52.378,92.358,52.437,92.314,52.5,92.307,52.565,92.172,52.597,91.91,52.594,91.769,52.616,91.748,52.662,91.672,52.677,91.541,52.662,91.439,52.674,91.367,52.714,91.378,52.797,91.472,52.924,91.501,53.012,91.465,53.06,91.454,53.102,91.468,53.136,91.538,53.16,91.664,53.173,91.788,53.23,91.909,53.329,91.955,53.398,91.924,53.437,91.847,53.479,91.725,53.522,91.634,53.57,91.575,53.621,91.559,53.66,91.586,53.687,91.578,53.709,91.511,53.735],[92.998,58.1,92.989,58.049,93.028,58.029,93.113,58.024,93.176,57.98,93.22,57.897,93.305,57.637,93.305,57.516,93.262,57.419,93.293,57.332,93.398,57.255,93.453,57.183,93.457,57.114,93.438,57.063,93.396,57.028,93.383,56.993,93.457,56.861,93.509,56.823,93.58,56.802,93.657,56.726,93.741,56.596,93.735,56.48,93.64,56.38,93.55,56.317,93.465,56.292,93.418,56.262,93.408,56.227,93.345,56.195,93.228,56.167,93.162,56.131,93.148,56.085,92.935,56.035,92.523,55.979,92.3,55.923,92.266,55.865,92.301,55.813,92.406,55.768,92.47,55.716,92.493,55.658,92.48,55.598,92.408,55.503]]},{"n":"Victoria Nile","z":2,"b":[31.366,0.428,33.187,2.333],"l":[[32.893,1.311,33.011,0.809,33.085,0.589,33.187,0.428],[32.475,1.552,32.545,1.522],[31.366,2.193,31.387,2.2,31.751,2.324,31.948,2.333,32.087,2.265,32.187,2.236,32.247,2.245,32.295,2.199,32.333,2.1,32.349,2.005,32.344,1.914,32.282,1.83,32.164,1.754,32.104,1.699,32.104,1.665,32.138,1.652,32.241,1.666]]},{"n":"Vilyuy","z":5,"b":[103.516,61.939,126.423,66.008],"l":[[126.423,64.403,126.421,64.285,126.31,64.18,126.094,64.057,125.808,63.964,125.454,63.901,125.115,63.894,124.793,63.944,124.621,63.986,124.599,64.021,124.345,64.051,123.616,64.091,123.225,63.944,123.035,63.907,122.917,63.931,122.607,63.925,122.106,63.891,121.795,63.852,121.675,63.808,121.503,63.789,121.28,63.795,121.141,63.777,121.085,63.736,121.023,63.726,120.955,63.747,120.869,63.729,120.766,63.674,120.658,63.648,120.545,63.652,120.482,63.611,120.469,63.524,120.329,63.509,120.063,63.564,119.674,63.566,119.163,63.516,118.891,63.481,118.858,63.461,118.859,63.43,118.894,63.388,118.833,63.349,118.678,63.314,118.408,63.285,118.024,63.264,117.769,63.221,117.642,63.156,117.544,63.049,117.476,62.902,117.457,62.813,117.487,62.784,117.481,62.754,117.439,62.722,117.419,62.661,117.421,62.573,117.456,62.516,117.524,62.491,117.549,62.445,117.53,62.379,117.573,62.306,117.679,62.228,117.709,62.161,117.665,62.106,117.619,62.088,117.57,62.106,117.522,62.09,117.473,62.039,117.38,61.992,117.244,61.95,117.044,61.939,116.782,61.961,116.55,62,116.35,62.056,116.216,62.104,116.148,62.143,116.126,62.19,116.151,62.245,116.054,62.301,115.835,62.358,115.764,62.421,115.735,62.467,115.735,62.505,115.763,62.559,115.717,62.584,115.596,62.579,115.494,62.592,115.41,62.621,115.352,62.656,115.32,62.698,115.323,62.738,115.362,62.776,115.338,62.802,115.251,62.815,115.229,62.851,115.273,62.907,115.281,62.952,115.255,62.984,115.198,63.015,115.11,63.045,115.029,63.043,114.955,63.009,114.865,63.001,114.759,63.02,114.683,63.046,114.638,63.079,114.631,63.114,114.664,63.149,114.671,63.173,114.654,63.185,114.344,63.145,114.207,63.148,114.133,63.186,114.059,63.195,113.984,63.175,113.937,63.179,113.917,63.206,113.855,63.228,113.752,63.246,113.638,63.215,113.515,63.136,113.497,63.097,113.455,63.061,113.378,63.057,113.355,63.04,113.363,62.996,113.375,62.96,113.389,62.934,113.432,62.905,113.431,62.873,113.385,62.839,113.339,62.829,113.292,62.842,113.254,62.88,113.225,62.941,113.157,62.978,113.051,62.99,112.992,63.013,112.98,63.047,112.926,63.056,112.83,63.039,112.783,63.009,112.766,62.981,112.709,62.96,112.48,63.02],[103.516,65.994,103.677,66.008,103.769,65.993,103.885,65.935,104.057,65.803,104.268,65.731,104.576,65.674,104.811,65.647,104.973,65.65,105.152,65.683,105.348,65.744,105.495,65.765,105.593,65.744,105.711,65.685,105.85,65.587,105.918,65.515,105.916,65.47,105.932,65.437,105.966,65.419,106.021,65.422,106.097,65.447,106.216,65.448,106.378,65.424,106.54,65.435,106.703,65.48,106.782,65.517,106.821,65.555,106.935,65.575,107.415,65.618,107.704,65.592,107.814,65.568,107.84,65.538,107.845,65.507,107.829,65.475,107.841,65.458,107.858,65.446,107.856,65.427,107.88,65.385,107.957,65.317,108.064,65.296,108.203,65.323,108.349,65.314,108.594,65.236,108.628,65.213,108.638,65.194,108.623,65.179,108.667,65.151,108.771,65.11,108.883,65.105,109.004,65.139,109.106,65.125,109.19,65.065,109.299,65.019,109.432,64.987,109.487,64.939,109.463,64.875,109.418,64.829,109.351,64.8,109.317,64.763,109.315,64.721,109.392,64.675,109.547,64.626,109.555,64.581,109.416,64.541,109.367,64.484,109.407,64.411,109.408,64.36,109.368,64.331,109.296,64.321,109.19,64.329,109.114,64.29,109.069,64.203,109.125,64.165,109.283,64.176,109.421,64.167,109.54,64.137,109.546,64.103,109.438,64.066,109.405,64.026,109.447,63.984,109.463,63.95,109.451,63.924,109.477,63.895,109.542,63.863,109.545,63.818,109.486,63.761,109.386,63.707,109.284,63.685,109.245,63.655,109.271,63.626,109.348,63.569,109.411,63.528,109.587,63.455]]},{"n":"Vistula","z":4.7,"b":[18.2,49.644,21.961,54.349],"l":[[18.879,49.644,18.788,49.817,18.771,49.888,18.81,49.925],[18.937,49.932,19.068,49.952,19.141,49.987,19.2,50.042,19.348,50.046,19.586,49.999,19.852,50.006,20.291,50.096,20.522,50.137,20.645,50.188,20.738,50.261,20.883,50.307,21.082,50.326,21.289,50.386,21.505,50.485,21.67,50.586,21.784,50.689,21.824,50.846,21.79,51.057,21.784,51.188,21.805,51.239,21.856,51.288,21.935,51.335,21.961,51.386,21.933,51.442,21.897,51.473,21.854,51.478,21.83,51.501,21.824,51.542,21.732,51.591,21.553,51.647,21.452,51.705,21.43,51.763,21.371,51.82,21.276,51.877,21.241,51.918,21.265,51.944,21.259,51.993,21.221,52.067,21.126,52.17,20.974,52.302,20.842,52.384,20.731,52.416,20.524,52.419,20.222,52.392,19.964,52.421,19.64,52.549,19.202,52.644,18.959,52.76,18.766,52.928,18.538,53.038,18.274,53.089,18.2,53.174,18.315,53.293,18.463,53.383,18.645,53.444,18.739,53.519,18.745,53.608,18.777,53.674,18.835,53.716,18.871,53.777,18.884,53.858,18.873,53.947,18.839,54.043,18.853,54.129,18.916,54.205,18.949,54.272,18.976,54.349]]},{"n":"Volga","z":3,"b":[32.598,45.657,50.148,58.113],"l":[[47.842,46.735,48.114,46.683,48.22,46.605,48.25,46.484,48.314,46.401,48.411,46.356,48.501,46.272,48.684,46.086],[45.664,50.222,45.49,50.122,45.414,50.056,45.379,49.991,45.361,49.916,45.36,49.832,45.305,49.737,45.198,49.632,45.107,49.513,45.048,49.408,45.011,49.351,44.996,49.313,44.99,49.294,44.987,49.259,44.976,49.195,44.842,49.013,44.588,48.727,44.544,48.665,44.528,48.598,44.596,48.545,44.865,48.466,45.043,48.434,45.129,48.451,45.228,48.447,45.337,48.42,45.411,48.424,45.449,48.461,45.559,48.451,45.733,48.399,45.798,48.383,45.831,48.369,45.865,48.353,45.909,48.322,46.078,48.218,46.155,48.11,46.147,47.994,46.193,47.918,46.295,47.881,46.35,47.843,46.359,47.802,46.409,47.773,46.498,47.756,46.616,47.673,46.764,47.525,46.875,47.444,46.948,47.431,47.009,47.403,47.057,47.359,47.082,47.316,47.085,47.252,47.086,47.214,47.212,47.144,47.463,47.029,47.585,46.942,47.577,46.884,47.612,46.855,47.69,46.855,47.8,46.773,47.942,46.609,48.002,46.506,47.981,46.464,47.98,46.418,47.998,46.37,47.916,46.25,47.735,46.057,47.641,45.929,47.636,45.866,47.61,45.834,47.578,45.813,47.561,45.79,47.563,45.761,47.587,45.737,47.631,45.694,47.65,45.657],[49.905,53.457,50.042,53.455,50.108,53.405,50.148,53.306,50.129,53.236,50.048,53.193,49.957,53.185,49.856,53.213,49.717,53.203,49.543,53.156,49.428,53.152,49.372,53.193,49.269,53.219,49.119,53.23,48.919,53.209,48.667,53.155,48.524,53.105,48.491,53.057,48.503,52.995,48.561,52.919,48.505,52.816,48.334,52.688,48.177,52.506,48.035,52.271,47.901,52.125,47.775,52.066,47.644,52.043,47.507,52.055,47.273,51.981,47.114,51.919,46.971,51.83,46.843,51.773,46.775,51.748],[43.432,56.675,43.657,56.496,43.837,56.404,44.047,56.337,44.167,56.274,44.198,56.214,44.258,56.18,44.348,56.172,44.451,56.14,44.566,56.086,44.776,56.082,45.082,56.131,45.281,56.137,45.373,56.102,45.443,56.104,45.489,56.143,45.56,56.16,45.655,56.153,45.737,56.166,45.806,56.201,45.872,56.199,45.936,56.161,46.025,56.172,46.138,56.232,46.294,56.287,46.492,56.336,46.68,56.309,46.857,56.205,47.166,56.151,47.829,56.145,47.886,56.122,47.927,56.073,47.972,55.992,48.067,55.925,48.212,55.872,48.46,55.825,48.809,55.784,49.003,55.693,49.059,55.483],[37.148,56.752,37.392,56.862,37.477,56.935,37.486,57.008,37.558,57.106,37.761,57.291],[32.598,57.25,32.681,57.079,32.675,57.009,32.692,56.972,32.729,56.945,32.851,56.932,33.057,56.933,33.282,56.899,33.528,56.829,33.675,56.765,33.722,56.707,33.816,56.641,33.956,56.565,33.98,56.509,33.886,56.473,33.844,56.445,33.857,56.423,34.05,56.357,34.424,56.245,34.668,56.209,34.784,56.247,34.862,56.29,34.903,56.337,34.909,56.369,34.88,56.385,34.891,56.416,34.94,56.459,34.956,56.503,34.937,56.546,35.001,56.578,35.223,56.613,35.334,56.703,35.422,56.742,35.523,56.76,35.607,56.791,35.672,56.835,35.773,56.861,35.908,56.867,36.066,56.846,36.245,56.799,36.344,56.758,36.375,56.706],[41.344,57.493,41.715,57.448,41.889,57.451,41.989,57.489,42.173,57.485,42.576,57.415],[40.007,57.567,39.853,57.669,39.798,57.725,39.79,57.77,39.697,57.831,39.517,57.909,39.246,57.99,38.704,58.113],[38.301,57.572,38.446,57.761,38.48,57.872,38.437,58.018]]},{"n":"Volta","z":4.7,"b":[0.099,5.76,0.672,6.338],"l":[[0.103,6.338,0.099,6.188,0.157,6.114,0.275,6.065,0.38,6.042,0.47,6.046,0.544,6.021,0.6,5.968,0.639,5.897,0.672,5.76]]},{"n":"Vorma","z":5,"b":[10.945,59.171,12.055,60.525],"l":[[11.17,59.389,11.141,59.3,11.083,59.249,10.945,59.171],[11.2,59.67,11.179,59.632,11.127,59.604,11.127,59.562,11.184,59.513,11.203,59.498],[12.028,60.525,12.055,60.492,12.048,60.411,12.017,60.27,11.925,60.212,11.774,60.236,11.641,60.228,11.527,60.186,11.385,60.102,11.192,59.956,11.157,59.911]]},{"n":"Vuoksi","z":5,"b":[28.788,60.574,30.534,61.204],"l":[[30.478,60.574,30.504,60.587,30.511,60.616,30.534,60.63],[28.788,61.204,28.822,61.129,29.095,60.952,29.288,60.849,29.596,60.717]]},{"n":"Vychegda","z":4.7,"b":[46.591,61.287,55.398,62.51],"l":[[53.691,61.65,53.653,61.686,53.651,61.713,53.673,61.744,53.635,61.775,53.539,61.807,53.438,61.809,53.333,61.782,53.25,61.79,53.19,61.833,53.034,61.865,52.658,61.897,52.583,61.931,52.452,61.939,52.241,61.933,52.076,61.915,51.956,61.883,51.894,61.854,51.889,61.828,51.779,61.823,51.564,61.84,51.307,61.815,51.007,61.748,50.877,61.719,50.803,61.751,50.78,61.836,50.766,61.875,50.762,61.966,50.7,62.005,50.546,62.017,50.313,62.001,50.242,62.041,50.247,62.077,50.298,62.13,50.343,62.167,50.331,62.208,50.259,62.221,50.081,62.218,49.866,62.174,49.611,62.088,49.379,62.074,49.168,62.131,48.902,62.098,48.581,61.977,48.4,61.879,48.359,61.804,48.235,61.709,48.028,61.594,47.802,61.495,47.557,61.411,47.222,61.347,46.591,61.287],[55.398,62.362,55.183,62.438,55.015,62.438,54.908,62.462,54.805,62.51,54.727,62.506,54.676,62.45,54.568,62.422,54.402,62.422,54.297,62.397,54.252,62.347,54.223,62.298,54.211,62.252,54.236,62.165,54.298,62.036,54.394,61.944,54.524,61.889,54.662,61.858,54.809,61.848,54.891,61.797,54.908,61.703,54.905,61.648,54.882,61.631,54.126,61.532,53.829,61.539,53.684,61.65]]},{"n":"Waal","z":4,"b":[3.979,51.719,4.985,51.848],"l":[[4.985,51.824,4.725,51.757,4.581,51.719,4.418,51.74,4.074,51.841,3.979,51.848]]},{"n":"Wabash","z":5,"b":[-88.095,37.789,-84.639,40.872],"l":[[-84.755,40.359,-84.647,40.356,-84.639,40.371,-84.724,40.422,-84.757,40.44,-84.767,40.483,-84.759,40.525,-84.794,40.567,-84.886,40.588,-85.007,40.65,-85.156,40.752,-85.317,40.826,-85.49,40.872,-85.677,40.863,-85.877,40.801,-86.113,40.763,-86.383,40.75,-86.618,40.66,-86.818,40.494,-86.955,40.412,-87.029,40.415,-87.117,40.39,-87.218,40.337,-87.404,40.214,-87.445,40.127,-87.438,40.018,-87.419,39.941,-87.388,39.895,-87.378,39.808,-87.388,39.682,-87.413,39.568,-87.452,39.467,-87.507,39.392,-87.579,39.344,-87.611,39.291,-87.603,39.234,-87.615,39.193,-87.647,39.169,-87.632,39.107,-87.568,39.007,-87.53,38.909,-87.518,38.812,-87.529,38.742,-87.584,38.679,-87.636,38.543,-87.681,38.488,-87.735,38.47,-87.784,38.419,-87.827,38.336,-87.873,38.295,-87.921,38.298,-87.957,38.282,-87.98,38.249,-87.977,38.214,-87.949,38.178,-87.958,38.144,-88.006,38.113,-88.005,38.098,-87.99,38.089,-87.996,38.074,-88.026,38.06,-88.045,38.013,-88.043,37.937,-88.059,37.898,-88.091,37.897,-88.095,37.876,-88.042,37.789]]},{"n":"Waikato","z":5,"b":[174.768,-39.072,176.326,-37.287],"l":[[175.744,-39.072,175.807,-39.014,175.817,-38.983,175.803,-38.967,175.802,-38.939],[176.059,-38.694,176.125,-38.628,176.183,-38.603,176.256,-38.595,176.304,-38.567,176.326,-38.519,176.303,-38.498,176.234,-38.506,176.167,-38.482,176.101,-38.428,176.003,-38.405,175.873,-38.415,175.778,-38.394,175.72,-38.341,175.679,-38.274,175.656,-38.192,175.654,-38.107,175.673,-38.019,175.655,-37.963,175.573,-37.928,175.451,-37.892,175.353,-37.828,175.239,-37.725,175.178,-37.625,175.169,-37.527,175.138,-37.43,175.083,-37.334,175,-37.287,174.833,-37.292,174.768,-37.339]]},{"n":"Waitaki","z":5,"b":[170.223,-44.911,170.999,-44.003],"l":[[170.51,-44.003,170.387,-44.216,170.332,-44.288,170.279,-44.32,170.257,-44.361,170.243,-44.388,170.223,-44.49,170.283,-44.591,170.491,-44.739,170.558,-44.795,170.655,-44.833,170.882,-44.891,170.999,-44.911]]},{"n":"Weir","z":3,"b":[150.47,-29.139,151.863,-28.639],"l":[[151.863,-28.739,151.75,-28.77,151.673,-28.824,151.554,-28.955,151.528,-29.035,151.478,-29.087,151.393,-29.139,151.335,-29.104,151.303,-28.982,151.231,-28.898,151.118,-28.851,151.058,-28.805,151.05,-28.758,150.982,-28.71,150.856,-28.66,150.712,-28.639,150.47,-28.65]]},{"n":"Willamette","z":5,"b":[-123.22,43.422,-122.095,45.674],"l":[[-122.095,43.422,-122.219,43.485,-122.371,43.515,-122.424,43.574,-122.427,43.672,-122.458,43.746,-122.517,43.795,-122.684,43.888,-122.96,44.024,-123.119,44.15,-123.184,44.323,-123.22,44.513,-123.189,44.599,-123.103,44.644,-123.077,44.68,-123.112,44.707,-123.116,44.747,-123.09,44.802,-123.098,44.836,-123.14,44.849,-123.124,44.887,-123.05,44.947,-123.024,44.999,-123.046,45.042,-123.036,45.081,-122.993,45.117,-122.987,45.157,-123.018,45.202,-123.016,45.238,-122.981,45.267,-122.902,45.288,-122.718,45.306,-122.621,45.368,-122.603,45.427,-122.634,45.502,-122.677,45.56,-122.732,45.603,-122.745,45.649,-122.726,45.674]]},{"n":"Winnipeg","z":5,"b":[-96.295,49.993,-94.615,50.625],"l":[[-94.615,49.993,-94.651,50.024,-94.648,50.062,-94.639,50.101,-94.668,50.135,-94.745,50.143,-94.809,50.15,-94.931,50.131,-94.999,50.141,-95.013,50.18,-95.073,50.22,-95.179,50.261,-95.207,50.34,-95.276,50.366,-95.398,50.369,-95.506,50.323,-95.6,50.228,-95.726,50.17,-95.883,50.148,-95.987,50.148,-96.036,50.168,-96.052,50.205,-96.026,50.283],[-95.989,50.404,-96.005,50.451,-96.081,50.506,-96.295,50.625]]},{"n":"Xar Moron","z":5,"b":[117.172,42.592,120.759,43.423],"l":[[117.209,42.592,117.172,42.613,117.179,42.645,117.216,42.696,117.23,42.748,117.214,42.827,117.189,42.886,117.194,42.935,117.222,42.994,117.347,43.063,117.57,43.142,117.746,43.185,117.874,43.192,118.016,43.22,118.172,43.271,118.471,43.276,118.911,43.237,119.22,43.229,119.484,43.262,120.759,43.423]]},{"n":"Xi","z":4,"b":[111.301,22.178,113.552,23.481],"l":[[113.035,22.795,113.095,22.73,113.166,22.576,113.214,22.483,113.264,22.363,113.416,22.178],[113.035,22.795,113.299,22.698,113.395,22.651,113.444,22.599,113.492,22.591,113.552,22.587],[112.98,23.041,113.445,23.055],[112.805,23.148,112.819,23.082,112.891,22.918,112.963,22.822,113.035,22.795],[112.805,23.148,112.912,23.113,112.974,23.052,113.024,22.954,113.09,22.907,113.213,22.912,113.338,22.889],[111.301,23.481,111.433,23.449,111.499,23.375,111.542,23.249,111.707,23.161,112.137,23.09,112.271,23.142,112.353,23.132,112.427,23.076,112.498,23.078,112.563,23.136,112.643,23.165,112.736,23.164,112.805,23.148]]},{"n":"Xiliao","z":5,"b":[120.759,43.423,123.554,43.815],"l":[[120.759,43.423,121.059,43.509,121.349,43.517,121.729,43.474,122.03,43.521,122.251,43.656,122.449,43.748,122.624,43.795,122.769,43.815,122.886,43.808,123.017,43.765,123.162,43.687,123.269,43.647,123.338,43.643,123.405,43.628,123.468,43.6,123.513,43.561,123.554,43.483]]},{"n":"Xingu","z":4.7,"b":[-53.444,-11.444,-51.641,-1.64],"l":[[-53.444,-11.444,-53.305,-11.298,-53.273,-11.175,-53.302,-11.025,-53.266,-10.917,-53.166,-10.852,-53.101,-10.706,-53.072,-10.48,-53.032,-10.343,-52.982,-10.296,-52.972,-10.25,-53.001,-10.206,-53.011,-10.155,-53.002,-10.095,-52.945,-10.005,-52.839,-9.884,-52.755,-9.808,-52.69,-9.775,-52.636,-9.715,-52.59,-9.628,-52.48,-9.596,-52.304,-9.62,-52.205,-9.585,-52.182,-9.491,-52.133,-9.4,-52.058,-9.311,-52.036,-9.237,-52.067,-9.176,-52.083,-9.085,-52.085,-8.965,-52.132,-8.878,-52.222,-8.824,-52.292,-8.746,-52.342,-8.643,-52.413,-8.581,-52.505,-8.558,-52.572,-8.522,-52.612,-8.473,-52.637,-8.4,-52.645,-8.303,-52.688,-8.167,-52.765,-7.993,-52.742,-7.726,-52.618,-7.365,-52.513,-7.131,-52.427,-7.022,-52.31,-6.954,-52.164,-6.925,-52.064,-6.863,-52.011,-6.767,-52.011,-6.677,-52.063,-6.594,-52.132,-6.548,-52.216,-6.539,-52.301,-6.483,-52.387,-6.381,-52.435,-6.29,-52.45,-6.172,-52.512,-6.089,-52.534,-6.009,-52.536,-5.902,-52.58,-5.754,-52.667,-5.565,-52.75,-5.445,-52.829,-5.395,-52.882,-5.298,-52.908,-5.156,-52.856,-4.933,-52.775,-4.745,-52.725,-4.629,-52.679,-4.441,-52.719,-4.37,-52.714,-4.301,-52.665,-4.233,-52.634,-4.114,-52.613,-3.861],[-52.609,-3.855,-52.345,-3.595,-52.233,-3.438,-52.184,-3.299,-52.109,-3.275,-52.009,-3.367,-51.952,-3.441,-51.939,-3.499,-51.905,-3.543,-51.849,-3.574,-51.787,-3.56,-51.719,-3.502,-51.693,-3.445,-51.709,-3.388,-51.691,-3.313,-51.641,-3.218,-51.657,-3.15,-51.738,-3.108,-51.836,-3.021,-51.952,-2.887,-52.003,-2.765,-51.989,-2.655,-52.039,-2.489,-52.092,-2.386,-52.153,-2.267,-52.221,-2.041,-52.251,-1.746,-52.231,-1.683,-52.197,-1.64]]},{"n":"Xun","z":4,"b":[110.089,23.397,111.301,23.555],"l":[[110.089,23.408,110.201,23.545,110.334,23.555,110.622,23.45,110.745,23.466,110.814,23.447,110.868,23.397,110.997,23.399,111.301,23.481]]},{"n":"Yadkin","z":5,"b":[-81.576,36.047,-80.464,36.259],"l":[[-81.576,36.074,-81.522,36.047,-81.465,36.055,-81.349,36.106,-80.899,36.225,-80.674,36.259,-80.49,36.242,-80.464,36.232]]},{"n":"Yamuna","z":5,"b":[77.074,25.289,81.903,30.915],"l":[[78.34,30.915,78.072,30.738,77.987,30.568,77.942,30.518,77.91,30.532,77.815,30.511,77.655,30.456,77.574,30.391,77.57,30.278,77.528,30.203,77.443,30.127,77.301,30.027,77.187,29.871,77.101,29.661,77.074,29.538,77.105,29.502,77.107,29.469,77.094,29.45,77.093,29.427,77.121,29.401,77.145,29.34,77.137,29.191,77.156,29.066,77.199,28.965,77.221,28.858,77.222,28.745,77.249,28.647,77.327,28.52,77.455,28.425,77.489,28.374,77.474,28.335,77.484,28.282,77.521,28.215,77.521,28.147,77.486,28.08,77.496,27.985,77.552,27.86,77.607,27.797,77.662,27.797,77.681,27.749,77.664,27.653,77.668,27.601,77.693,27.592,77.7,27.57,77.69,27.533,77.711,27.467,77.765,27.371,77.821,27.305,77.88,27.269,77.92,27.263,77.942,27.286,77.966,27.29,77.994,27.274,78.012,27.245,78.02,27.202,78.068,27.183,78.156,27.188,78.203,27.165,78.21,27.113,78.249,27.096,78.292,27.106,78.309,27.095,78.313,27.079,78.327,27.062,78.383,27.058,78.437,27.024,78.488,26.961,78.508,26.953,78.522,26.96,78.544,26.958,78.609,26.91,78.663,26.894,78.707,26.911,78.779,26.88,78.879,26.801,78.948,26.762,78.986,26.762,79.024,26.721,79.064,26.639,79.136,26.58,79.24,26.542,79.263,26.511,79.262,26.485,79.251,26.472,79.266,26.446,79.28,26.433,79.311,26.428,79.402,26.436,79.473,26.416,79.494,26.372,79.518,26.359,79.536,26.355,79.545,26.335,79.538,26.306,79.592,26.244,79.712,26.156,79.804,26.108,79.868,26.099,79.912,26.112,79.932,26.125,79.947,26.125,79.956,26.095,79.989,26.072,80.05,26.071,80.08,26.053,80.08,26.017,80.113,25.979,80.211,25.92,80.442,25.889,80.524,25.87,80.535,25.852,80.523,25.835,80.489,25.819,80.524,25.781,80.63,25.723,80.717,25.702,80.784,25.72,80.844,25.717,80.897,25.696,80.93,25.668,80.944,25.632,80.969,25.617,81.005,25.623,81.022,25.605,81.019,25.563,81.059,25.531,81.142,25.507,81.181,25.466,81.176,25.407,81.222,25.367,81.318,25.345,81.377,25.319,81.399,25.289,81.472,25.295,81.598,25.337,81.694,25.351,81.762,25.337,81.804,25.352,81.82,25.395,81.847,25.416,81.903,25.415]]},{"n":"Yangtze","z":2,"b":[113.128,29.463,120.074,32.235],"l":[[113.128,29.463,113.166,29.484,113.21,29.538,113.53,29.832,113.696,29.949,113.76,29.939,113.86,29.972,113.995,30.05,114.011,30.124,113.909,30.196,113.885,30.246,113.94,30.276,113.99,30.279,114.032,30.253,114.116,30.328,114.288,30.572,114.303,30.593,114.396,30.664,114.472,30.661,114.558,30.603,114.662,30.582,114.784,30.598,114.841,30.565,114.833,30.482,114.885,30.423,114.997,30.387,115.058,30.336,115.067,30.269,115.102,30.229,115.16,30.216,115.252,30.128,115.376,29.966,115.504,29.879,115.638,29.867,115.751,29.828,115.843,29.764,115.983,29.746,116.171,29.775,116.212,29.785],[116.198,29.751,116.212,29.785],[116.212,29.785,116.327,29.815,116.451,29.868,116.544,29.931,116.606,30.006,116.683,30.061,116.776,30.097,116.847,30.168,116.896,30.272,116.913,30.349,116.9,30.399,116.972,30.446,117.13,30.491,117.212,30.552,117.217,30.629,117.247,30.674,117.327,30.695,117.605,30.776,117.725,30.886,117.78,31.052,117.844,31.124,117.916,31.104,117.968,31.13,117.999,31.202,118.089,31.255,118.24,31.29,118.312,31.341,118.305,31.406,118.333,31.51,118.395,31.653,118.515,31.841,118.692,32.074,118.961,32.203,119.324,32.227,119.53,32.229,119.643,32.235,119.771,32.207,119.867,32.1,119.941,32.04,120.074,31.96]]},{"n":"Yarkant","z":5,"b":[75.941,35.521,80.888,40.467],"l":[[77.525,35.521,77.638,35.582,77.683,35.631,77.708,35.715,77.585,35.857,77.597,35.971,77.704,36.105,77.701,36.196,77.588,36.243,77.511,36.308,77.471,36.389,77.358,36.437,77.172,36.452,77.017,36.44,76.891,36.402,76.783,36.388,76.69,36.396,76.52,36.43,76.464,36.468,76.435,36.524,76.341,36.586,76.182,36.655,76.058,36.756,75.969,36.889,75.947,37.011,75.992,37.122,75.988,37.212,75.941,37.272,75.952,37.293,75.97,37.303,76.006,37.296,76.056,37.286,76.142,37.311,76.214,37.382,76.229,37.523,76.188,37.732,76.216,37.875,76.314,37.95,76.402,37.989,76.479,37.993,76.523,37.978,76.533,37.947,76.597,37.919,76.716,37.895,76.927,37.997,77.23,38.225,77.368,38.4,77.343,38.522,77.39,38.655,77.512,38.799,77.568,38.905,77.56,38.973,77.579,39.04,77.625,39.106,77.67,39.141,77.713,39.146,77.802,39.204,77.937,39.316,78.121,39.425,78.354,39.531,78.607,39.597,78.882,39.622,79.101,39.66,79.265,39.713,79.597,39.862,80.097,40.108,80.377,40.274,80.437,40.361,80.572,40.42,80.888,40.467]]},{"n":"Brahmaputra","z":2.1,"b":[87.66,29.013,94.356,29.381],"l":[[87.66,29.152,87.747,29.288,87.823,29.349,87.917,29.38,88.015,29.381,88.118,29.352,88.182,29.347,88.208,29.368,88.257,29.366,88.327,29.343,88.43,29.336,88.631,29.351,89.404,29.366,89.699,29.359,89.774,29.335,89.862,29.334,89.963,29.356,90.082,29.36,90.218,29.344,90.321,29.315,90.392,29.274,90.506,29.278,90.662,29.329,90.746,29.34,90.759,29.31,90.786,29.308,90.828,29.332,90.897,29.337,91.038,29.317,92.076,29.281,92.465,29.231,92.551,29.155,92.695,29.101,92.895,29.069,93.023,29.073,93.078,29.11,93.117,29.107,93.138,29.061,93.183,29.029,93.25,29.013,93.331,29.05,93.427,29.14,93.568,29.178,93.755,29.163,93.932,29.171,94.097,29.202,94.224,29.24,94.356,29.31]]},{"n":"Yenisey","z":4,"b":[83.11,58.1,92.998,70.11],"l":[[83.11,70.11,83.307,70.049,83.464,69.976,83.551,69.891,83.791,69.79,84.183,69.67,84.487,69.647,84.702,69.719,84.906,69.766,85.098,69.788,85.413,69.721,85.85,69.567,85.953,69.49,85.963,69.382,85.992,69.294,86.025,69.187,86.051,69.119,86.012,69.031,85.955,68.933,85.992,68.841,86.193,68.74,86.248,68.7,86.287,68.666,86.33,68.599,86.353,68.529,86.345,68.456,86.401,68.381,86.524,68.302,86.536,68.214,86.437,68.114,86.436,68.016,86.48,67.928,86.451,67.84,86.347,67.748,86.286,67.66,86.288,67.573,86.353,67.488,86.447,67.437,86.568,67.42,86.618,67.359,86.598,67.255,86.618,67.184,86.679,67.146,86.698,67.093,86.677,67.025,86.688,66.942,86.73,66.845,86.63,66.747,86.388,66.647,86.286,66.585,86.326,66.558,86.477,66.564,86.741,66.601,87.032,66.557,87.35,66.431,87.458,66.387,87.505,66.348,87.514,66.32,87.479,66.289,87.454,66.273,87.316,66.225,87.275,66.19,87.312,66.163,87.474,66.135,87.762,66.105,87.914,66.062,87.93,66.004,87.896,65.962,87.81,65.936,87.765,65.91,87.763,65.884,87.772,65.854,87.793,65.818,87.85,65.791,87.944,65.773,88.003,65.74,88.029,65.694,87.98,65.457,87.848,64.999,87.822,64.878,87.795,64.817,87.768,64.787,87.7,64.737,87.631,64.679,87.564,64.595,87.54,64.516,87.544,64.442,87.577,64.373,87.579,64.271,87.55,64.137,87.558,64.039,87.602,63.975,87.551,63.886,87.405,63.77,87.347,63.678,87.377,63.607,87.36,63.528,87.293,63.439,87.266,63.382,87.277,63.357,87.442,63.299,87.76,63.207,88.024,63.093,88.34,62.888,88.434,62.854,88.48,62.803,88.51,62.724,88.595,62.635,88.736,62.535,88.857,62.472,88.961,62.443,89.014,62.388,89.016,62.307,89.083,62.179,89.212,62.004,89.325,61.894,89.47,61.827,89.706,61.751,89.786,61.688,89.789,61.613,89.843,61.586,89.95,61.606,90.052,61.601,90.15,61.573,90.174,61.537,90.124,61.494,90.001,61.45,89.806,61.405,89.685,61.337,89.637,61.244,89.659,61.11,89.751,60.935,89.847,60.815,89.947,60.751,90.006,60.657,90.025,60.535,90.056,60.459,90.12,60.417,90.166,60.381,90.172,60.307,90.309,60.238,90.495,60.115,90.698,59.927,90.783,59.782,90.804,59.745,90.858,59.707,90.884,59.657,90.882,59.597,90.99,59.498,91.207,59.361,91.325,59.267,91.345,59.217,91.431,59.163,91.582,59.106,91.678,59.051,91.719,58.997,91.733,58.953,91.718,58.919,91.769,58.901,91.885,58.898,91.977,58.876,92.045,58.835,92.046,58.795,91.982,58.757,91.982,58.727,92.048,58.707,92.084,58.649,92.091,58.555,92.139,58.498,92.228,58.48,92.275,58.451,92.282,58.412,92.32,58.38,92.39,58.357,92.428,58.326,92.435,58.287,92.473,58.245,92.539,58.199,92.679,58.157,92.998,58.1]]},{"n":"Yuan","z":5,"b":[107.501,26.164,113.128,29.463],"l":[[107.505,26.338,107.501,26.239,107.519,26.191,107.558,26.164,107.61,26.167,107.708,26.218,107.78,26.358,107.8,26.432,107.793,26.485,107.826,26.53,107.9,26.564,107.959,26.617,108.005,26.688,108.072,26.753,108.159,26.812,108.251,26.839,108.348,26.835,108.396,26.817,108.396,26.785,108.453,26.725,108.568,26.636,108.645,26.618,108.683,26.67,108.718,26.696,108.748,26.695,108.762,26.679,108.76,26.649,108.795,26.614,108.869,26.575,108.961,26.573,109.073,26.608,109.194,26.679,109.39,26.838,109.438,26.894,109.444,26.954,109.424,27.038,109.441,27.08,109.494,27.079,109.556,27.107,109.628,27.165,109.667,27.177,109.675,27.144,109.692,27.128,109.719,27.131,109.74,27.152,109.755,27.19,109.823,27.189,109.943,27.147,110.051,27.19,110.217,27.423,110.264,27.504,110.284,27.566,110.277,27.611,110.299,27.653,110.351,27.694,110.367,27.729,110.347,27.758,110.348,27.791,110.371,27.828,110.347,27.863,110.275,27.896,110.251,27.927,110.275,27.955,110.252,27.981,110.182,28.005,110.135,28.036,110.113,28.076,110.128,28.128,110.179,28.193,110.195,28.242,110.175,28.275,110.182,28.289,110.216,28.286,110.272,28.32,110.351,28.393,110.419,28.486,110.477,28.602,110.633,28.69,110.887,28.751,111.04,28.803,111.094,28.845,111.149,28.838,111.207,28.781,111.279,28.762,111.405,28.791,111.522,28.986,111.559,29.005,111.593,28.99,111.626,28.996,111.655,29.013,111.679,29.013,111.705,28.992,111.744,28.988,111.796,29.01,111.865,29.012,111.993,28.985],[112.905,29.031,112.994,29.147],[113.069,29.388,113.128,29.463]]},{"n":"Yukon","z":3,"b":[-165.244,61.269,-134.87,66.611],"l":[[-134.916,61.568,-134.87,61.628,-134.885,61.685,-134.946,61.759,-134.964,61.816,-134.941,61.856,-134.94,61.889,-134.961,61.912,-135.121,61.957,-135.419,62.024,-135.629,62.049,-135.75,62.032,-135.928,62.046,-136.307,62.121,-136.362,62.145,-136.378,62.197,-136.354,62.275,-136.583,62.428,-137.356,62.781,-137.452,62.809,-137.733,62.82,-138.398,62.801,-139.236,62.97,-139.418,63.011,-139.532,63.071,-139.564,63.161,-139.545,63.234,-139.472,63.288,-139.506,63.353,-139.646,63.427,-139.73,63.513,-139.76,63.609,-139.759,63.674,-139.728,63.707,-139.723,63.75,-139.744,63.805,-139.743,63.865,-139.721,63.932,-139.653,63.983,-139.541,64.017,-139.482,64.057,-139.476,64.102,-139.511,64.158,-139.587,64.227,-139.849,64.297,-140.296,64.367,-140.453,64.405,-140.477,64.426,-140.47,64.453,-140.465,64.498,-140.479,64.526,-140.545,64.518,-140.589,64.528,-140.61,64.558,-140.649,64.572,-140.705,64.57,-140.73,64.585,-140.724,64.616,-140.746,64.627,-140.796,64.62,-140.882,64.641,-140.933,64.674,-141.006,64.689,-141.065,64.726,-141.059,64.749,-141.093,64.775,-141.164,64.802,-141.176,64.833,-141.128,64.867,-141.129,64.888,-141.179,64.895,-141.198,64.913,-141.187,64.943,-141.23,64.954,-141.327,64.945,-141.373,64.966,-141.369,65.041,-141.404,65.086,-141.533,65.129,-141.768,65.183,-141.924,65.241,-142.002,65.3,-142.147,65.335,-142.359,65.344,-142.502,65.363,-142.576,65.392,-142.655,65.393,-142.738,65.366,-142.929,65.355,-143.227,65.359,-143.447,65.383,-143.589,65.427,-143.673,65.472,-143.698,65.518,-143.781,65.554,-143.922,65.579,-144.023,65.613,-144.085,65.655,-144.087,65.722,-144.03,65.812,-144.017,65.876,-144.048,65.914,-144.251,66.027,-144.624,66.215,-144.832,66.335,-144.874,66.386,-145.003,66.453,-145.219,66.537,-145.453,66.587,-145.828,66.611],[-163.259,62.016,-163.241,61.94,-163.242,61.886,-163.289,61.847,-163.388,61.829,-163.539,61.832,-163.644,61.816,-163.705,61.783,-163.774,61.777,-163.836,61.797,-163.845,61.835,-163.86,61.851,-163.898,61.853,-163.959,61.839,-164.002,61.851,-164.026,61.891,-164.087,61.879,-164.183,61.816,-164.264,61.789,-164.331,61.798,-164.429,61.782,-164.56,61.742,-164.646,61.744,-164.746,61.762,-164.831,61.739,-164.879,61.698,-164.869,61.665,-164.766,61.629,-164.743,61.591,-164.778,61.551,-164.888,61.507,-165.007,61.48,-165.091,61.441,-165.188,61.392,-165.206,61.33,-165.201,61.286,-165.244,61.269],[-163.259,62.016,-163.434,62.061,-163.731,62.092,-163.904,62.134,-163.953,62.184,-163.948,62.248,-163.889,62.325,-163.859,62.392,-163.861,62.448,-163.93,62.532,-164.067,62.643,-164.222,62.71,-164.482,62.745,-164.522,62.737,-164.589,62.709],[-145.828,66.611,-146.664,66.508,-147.015,66.445,-147.159,66.386,-147.323,66.352,-147.509,66.344,-147.596,66.321,-147.585,66.284,-147.746,66.264,-148.079,66.261,-148.364,66.225,-148.601,66.155,-148.729,66.104,-148.764,66.067,-148.79,66.062,-148.812,66.085,-148.873,66.09,-148.971,66.077,-149.014,66.056,-149,66.029,-149.054,66.007,-149.175,65.991,-149.251,65.957,-149.284,65.905,-149.438,65.889,-149.714,65.91,-149.92,65.89,-150.058,65.83,-150.048,65.798,-149.89,65.795,-149.827,65.755,-149.858,65.679,-149.93,65.65,-150.041,65.669,-150.125,65.641,-150.183,65.565,-150.324,65.52,-150.549,65.505,-150.792,65.446,-151.054,65.343,-151.401,65.26,-152.048,65.165,-152.349,65.124,-152.534,65.134,-152.699,65.182,-152.857,65.201,-153.007,65.191,-153.126,65.166,-153.215,65.124,-153.331,65.112,-153.476,65.129,-153.74,65.084,-154.122,64.976,-154.402,64.927,-154.581,64.936,-154.848,64.902,-155.134,64.867,-155.381,64.79,-155.575,64.756,-155.754,64.764,-155.998,64.721,-156.202,64.713,-156.425,64.68,-156.733,64.664,-156.76,64.696,-156.809,64.718,-156.917,64.73,-156.993,64.755,-157.036,64.795,-157.271,64.826,-157.396,64.82,-157.51,64.844,-157.769,64.85,-157.842,64.869,-157.913,64.858,-158.003,64.801,-158.169,64.681,-158.286,64.643,-158.501,64.464,-158.678,64.343,-158.691,64.311,-158.659,64.223,-158.674,64.138,-158.734,64.058,-158.826,63.994,-159.01,63.925,-159.149,63.884,-159.203,63.848,-159.217,63.802,-159.256,63.767,-159.318,63.743,-159.386,63.679,-159.458,63.575,-159.479,63.494,-159.448,63.437,-159.502,63.372,-159.642,63.301,-159.725,63.214,-159.753,63.111,-159.826,63.035,-159.945,62.987,-160.019,62.916,-160.049,62.822,-160.09,62.756,-160.144,62.717,-160.17,62.66,-160.17,62.586,-160.133,62.523,-160.058,62.471,-160.006,62.415,-159.975,62.371,-159.963,62.321,-159.956,62.28,-159.913,62.253,-159.805,62.236,-159.744,62.214,-159.73,62.188,-159.742,62.154,-159.782,62.114,-159.852,62.086,-159.952,62.071,-160.032,62.033,-160.093,61.973,-160.336,61.941,-160.762,61.936,-161.067,61.907,-161.253,61.853,-161.339,61.805,-161.326,61.764,-161.346,61.72,-161.401,61.673,-161.528,61.637,-161.754,61.617,-161.826,61.601,-161.92,61.576,-161.956,61.597,-161.963,61.656,-162.023,61.745,-162.135,61.863,-162.249,61.923,-162.367,61.924,-162.44,61.948,-162.47,61.994,-162.527,62.006,-162.613,61.984,-162.668,61.957,-162.691,61.923,-162.74,61.918,-162.814,61.942,-162.888,61.94,-162.962,61.913,-163.043,61.908,-163.132,61.923,-163.204,61.959,-163.259,62.016],[-145.828,66.611,-145.833,66.611],[-145.825,66.611,-145.828,66.611]]},{"n":"Za","z":2.1,"b":[94.084,31.966,96.941,33.205],"l":[[94.084,33.156,94.448,33.163,94.507,33.204,94.61,33.205,94.777,33.181,94.916,33.14,95.026,33.083,95.089,33.025,95.106,32.966,95.225,32.915,95.447,32.87,95.581,32.815,95.627,32.748,95.728,32.672,95.882,32.587,96.011,32.552,96.113,32.567,96.183,32.562,96.239,32.526,96.381,32.433,96.455,32.343,96.507,32.225,96.585,32.155,96.69,32.133,96.77,32.085,96.824,32.012,96.874,31.973,96.941,31.966]]},{"n":"Zambezi","z":3,"b":[22.544,-18.842,36.498,-11.101],"l":[[36.039,-18.337,36.105,-18.424,36.14,-18.536,36.145,-18.62,36.12,-18.677,36.129,-18.72,36.15,-18.75,36.153,-18.795,36.125,-18.842],[36.039,-18.337,36.498,-18.576],[28.761,-16.532,28.833,-16.424,28.857,-16.306,28.857,-16.142,28.876,-16.036,28.913,-15.988,28.973,-15.95,29.051,-15.901,29.288,-15.777,29.487,-15.697,29.73,-15.645,30.796,-15.642],[32.707,-15.58,32.925,-15.586,33.028,-15.634,33.089,-15.725,33.203,-15.835,33.372,-15.963,33.564,-16.147,33.885,-16.508,33.984,-16.599,34.205,-16.697,34.582,-16.834,34.795,-16.923,34.844,-16.964,34.897,-17.059,34.981,-17.28,35.096,-17.521,35.187,-17.636,35.291,-17.705,35.389,-17.8,35.48,-17.92,35.602,-18.013,35.755,-18.08,35.846,-18.146,35.877,-18.211,35.941,-18.275,36.039,-18.337],[24.266,-11.379,24.327,-11.311,24.334,-11.239,24.308,-11.141,24.226,-11.101,24.022,-11.126,23.75,-11.332,23.599,-11.423,23.477,-11.467,23.387,-11.521,23.33,-11.586,23.173,-11.686,22.915,-11.823,22.76,-11.952,22.707,-12.074,22.65,-12.166,22.588,-12.227,22.553,-12.346,22.544,-12.522,22.581,-12.69,22.662,-12.849,22.698,-12.965,22.687,-13.038,22.753,-13.132,22.964,-13.305,23.023,-13.355,23.074,-13.458,23.137,-13.63,23.186,-13.726,23.221,-13.747,23.228,-13.785,23.207,-13.84,23.207,-13.909,23.228,-13.993,23.225,-14.066,23.197,-14.131,23.195,-14.216,23.218,-14.324,23.189,-14.443,23.107,-14.573,23.034,-14.784,22.969,-15.077,22.982,-15.278,23.073,-15.387,23.125,-15.482,23.136,-15.564,23.127,-15.625,23.099,-15.666,23.12,-15.737,23.189,-15.837,23.246,-16.013,23.292,-16.264,23.492,-16.549,23.846,-16.868,24.077,-17.14,24.238,-17.478,24.733,-17.518,24.932,-17.543,25.002,-17.569,25.092,-17.634,25.259,-17.794,25.452,-17.845,25.557,-17.85,25.64,-17.824,25.742,-17.858,25.863,-17.952,25.996,-17.97,26.14,-17.912,26.333,-17.929,26.578,-18.023,26.78,-18.042,27.021,-17.958]]},{"n":"","z":5,"b":[170.001,-44.361,170.257,-44.286],"l":[[170.001,-44.286,170.11,-44.291,170.257,-44.361]]},{"n":"","z":5,"b":[170.204,-44.32,170.279,-44.171],"l":[[170.204,-44.171,170.279,-44.32]]},{"n":"","z":5,"b":[-106.018,58.455,-103.55,59.271],"l":[[-103.55,58.455,-103.665,58.545],[-103.786,58.691,-103.959,58.772,-103.999,58.809,-103.962,58.829,-103.95,58.868,-103.963,58.927,-104.144,58.979,-104.495,59.023,-104.792,59.037,-104.92,59.08,-104.951,59.142,-105.012,59.173],[-105.55,59.15,-105.572,59.221,-105.689,59.251,-106.018,59.271]]},{"n":"","z":5,"b":[-138.515,59.166,-137,61.227],"l":[[-137,61.227,-137.061,60.991,-137.069,60.952,-137.09,60.91,-137.177,60.874,-137.348,60.824,-137.511,60.797,-137.664,60.794,-137.768,60.757,-137.822,60.687,-137.845,60.607,-137.836,60.517,-137.878,60.412,-137.972,60.293,-137.997,60.231,-137.967,60.178,-137.863,60.067,-137.851,60.027,-137.863,59.984,-137.966,59.927,-137.991,59.881,-137.936,59.846,-137.929,59.811,-137.969,59.776,-137.972,59.738,-137.92,59.678,-137.83,59.587,-137.817,59.544,-137.824,59.51,-137.968,59.461,-137.987,59.452,-138.021,59.439,-138.061,59.437,-138.184,59.401,-138.233,59.372,-138.26,59.32,-138.25,59.291,-138.229,59.275,-138.209,59.266,-138.198,59.252,-138.185,59.224,-138.182,59.202,-138.189,59.19,-138.202,59.18,-138.221,59.174,-138.239,59.176,-138.438,59.183,-138.482,59.177,-138.515,59.166]]},{"n":"","z":5,"b":[-125.619,64.895,-123.472,65.152],"l":[[-123.472,65.152,-123.737,65.062,-124.205,65.026,-124.966,65.014,-125.41,64.982,-125.619,64.895]]},{"n":"","z":5,"b":[168.633,-46.566,168.957,-45.327],"l":[[168.775,-45.327,168.724,-45.461,168.683,-45.516,168.633,-45.537,168.637,-45.57,168.692,-45.614,168.712,-45.669,168.694,-45.736,168.701,-45.789,168.747,-45.847,168.922,-46.028,168.957,-46.107,168.929,-46.209,168.799,-46.448,168.767,-46.566]]},{"n":"","z":4.7,"b":[-73.177,46.044,-72.991,46.177],"l":[[-72.991,46.177,-73.079,46.16,-73.146,46.124,-73.177,46.071,-73.164,46.044]]},{"n":"","z":4.7,"b":[-103.759,60.952,-95.492,64.05],"l":[[-103.447,61.366,-103.468,61.283,-103.55,61.212,-103.699,61.126,-103.759,61.05,-103.716,60.952],[-103.158,61.836,-103.236,61.773,-103.251,61.734,-103.22,61.681],[-99.618,62.301,-98.574,62.252,-98.209,62.252,-98.175,62.285,-98.206,62.38,-98.35,62.616],[-101.366,62.35,-101.318,62.345],[-100.735,62.356,-100.552,62.355],[-102.737,62.41,-102.919,62.319],[-101.831,62.52,-101.529,62.664],[-102.035,62.925,-102.118,62.915,-102.181,62.884,-102.25,62.828,-102.379,62.79,-102.66,62.76],[-97.63,62.979,-97.61,63.047,-97.567,63.092,-97.459,63.157],[-97.288,63.514,-97.235,63.573,-97.182,63.593,-97.111,63.593,-97.084,63.604,-97.071,63.635,-97.014,63.64,-96.952,63.637],[-95.878,63.656,-95.811,63.669,-95.799,63.694,-95.809,63.719,-95.799,63.746,-95.723,63.766,-95.677,63.798,-95.677,63.846,-95.631,63.898,-95.537,63.956,-95.492,64.001,-95.497,64.05]]},{"n":"","z":4.7,"b":[105.931,9.949,106.658,10.33],"l":[[106.464,10.298,106.385,10.33,106.138,10.306],[106.138,10.306,105.931,10.284],[106.138,10.306,106.184,10.253,106.222,10.245,106.292,10.262,106.33,10.247,106.36,10.211,106.408,10.117,106.468,10.05,106.519,10.015,106.579,9.979,106.658,9.949]]},{"n":"","z":4.7,"b":[-101.318,62.345,-101.163,62.367],"l":[[-101.163,62.367,-101.318,62.345]]},{"n":"Garonne","z":5,"b":[-0.567,43.415,1.454,45.001],"l":[[1.454,43.415,1.453,43.417,1.409,43.514,1.409,43.588,1.361,43.689,1.22,43.881,1.082,44.031,0.861,44.126,0.509,44.215,0.323,44.295,0.304,44.366,0.255,44.422,0.137,44.484,0.003,44.561,-0.102,44.587,-0.224,44.588,-0.343,44.636,-0.534,44.796,-0.562,44.83,-0.567,44.86,-0.542,44.898,-0.536,44.952,-0.548,45.001]]},{"n":"Loire","z":4.7,"b":[-1.743,44.775,4.256,47.894],"l":[[3.083,46.985,3.022,47.149,2.916,47.287,2.893,47.363,2.917,47.423,2.883,47.499,2.789,47.59,2.614,47.691,2.36,47.804,2.145,47.871,1.971,47.894,1.845,47.893,1.769,47.866,1.608,47.764,1.364,47.587,1.158,47.474,0.99,47.425,0.815,47.396,0.635,47.386,0.428,47.341,0.207,47.244,0.075,47.218,-0.038,47.249,-0.216,47.364,-0.418,47.402,-0.731,47.371,-1.056,47.375,-1.249,47.336,-1.417,47.258,-1.559,47.216,-1.633,47.202,-1.743,47.216],[3.083,46.985,3.168,46.977,3.249,46.952,3.329,46.846,3.49,46.829,3.625,46.74,3.724,46.639,3.749,46.558,3.86,46.507,3.984,46.46,3.99,46.379,4.039,46.238,4.101,46.166,4.101,46.063,4.033,45.977,4.052,45.925,4.157,45.852,4.213,45.745,4.219,45.628,4.256,45.477,4.2,45.368,4.132,45.307,4.083,45.238,3.953,45.199,3.91,45.116,3.922,45.037,3.904,44.972,3.928,44.871,4.07,44.81,4.169,44.775,4.2,44.823]]}]} \ No newline at end of file diff --git a/src/maps/data/states_50m.json b/src/maps/data/states_50m.json new file mode 100644 index 0000000..64dcd18 --- /dev/null +++ b/src/maps/data/states_50m.json @@ -0,0 +1 @@ +{"f":[{"z":3,"b":[-53.935,-29.356,-49.699,-27.081],"l":[-53.935,-27.161,-53.852,-27.156,-53.8,-27.165,-53.719,-27.158,-53.671,-27.169,-53.66,-27.201,-53.616,-27.21,-53.54,-27.198,-53.498,-27.172,-53.49,-27.133,-53.457,-27.103,-53.398,-27.082,-53.347,-27.081,-53.305,-27.098,-53.263,-27.163,-53.227,-27.174,-53.184,-27.149,-53.134,-27.14,-53.104,-27.154,-53.071,-27.142,-53.03,-27.138,-52.994,-27.163,-52.953,-27.179,-52.9,-27.177,-52.826,-27.189,-52.742,-27.246,-52.636,-27.259,-52.511,-27.23,-52.415,-27.236,-52.348,-27.276,-52.304,-27.283,-52.265,-27.277,-52.229,-27.293,-52.192,-27.281,-52.139,-27.283,-52.06,-27.313,-51.984,-27.377,-51.913,-27.473,-51.873,-27.509,-51.754,-27.512,-51.702,-27.488,-51.645,-27.5,-51.582,-27.547,-51.53,-27.569,-51.488,-27.564,-51.406,-27.613,-51.287,-27.716,-51.19,-27.781,-51.117,-27.808,-50.966,-27.959,-50.736,-28.236,-50.578,-28.386,-50.447,-28.422,-50.233,-28.469,-50.155,-28.474,-50.141,-28.453,-50.069,-28.444,-49.939,-28.448,-49.839,-28.465,-49.737,-28.511,-49.708,-28.53,-49.699,-28.597,-49.797,-28.624,-49.846,-28.695,-49.954,-28.794,-49.945,-28.95,-50.007,-29.102,-50.07,-29.093,-50.164,-29.195,-50.168,-29.285,-50.052,-29.356,-50.034,-29.32,-50.079,-29.244,-49.954,-29.195,-49.869,-29.218,-49.815,-29.271,-49.713,-29.325]},{"z":3,"b":[-38.221,-9.356,-35.15,-8.834],"l":[-38.221,-9.348,-38.155,-9.267,-38.092,-9.186,-37.967,-9.142,-37.815,-8.977,-37.806,-8.896,-37.748,-8.861,-37.681,-8.977,-37.628,-8.981,-37.521,-8.945,-37.471,-9.003,-37.391,-9.044,-37.181,-9.24,-36.998,-9.307,-36.94,-9.356,-36.905,-9.289,-36.833,-9.262,-36.579,-9.294,-36.44,-9.209,-36.346,-9.2,-36.266,-9.142,-36.239,-9.088,-36.123,-9.017,-36.119,-8.968,-36.016,-8.896,-35.971,-8.905,-35.882,-8.874,-35.802,-8.869,-35.721,-8.919,-35.61,-8.865,-35.471,-8.834,-35.391,-8.883,-35.302,-8.887,-35.15,-8.914]},{"z":3,"b":[-40.538,-7.856,-38.699,-7.347],"l":[-38.699,-7.619,-38.82,-7.664,-38.864,-7.704,-38.967,-7.847,-38.998,-7.82,-39.074,-7.856,-39.11,-7.753,-39.244,-7.682,-39.306,-7.624,-39.351,-7.548,-39.534,-7.477,-39.655,-7.374,-39.846,-7.347,-39.954,-7.361,-40.065,-7.405,-40.172,-7.419,-40.262,-7.392,-40.538,-7.392]},{"z":3,"b":[-38.699,-8.285,-34.81,-7.271],"l":[-34.81,-7.567,-34.98,-7.512,-35.065,-7.41,-35.271,-7.383,-35.373,-7.459,-35.498,-7.454,-35.556,-7.655,-35.681,-7.713,-35.713,-7.709,-35.837,-7.744,-35.887,-7.732,-35.931,-7.838,-35.967,-7.816,-36.074,-7.825,-36.123,-7.78,-36.168,-7.825,-36.217,-7.78,-36.266,-7.825,-36.338,-7.811,-36.422,-7.825,-36.449,-7.91,-36.57,-7.923,-36.659,-8.012,-36.628,-8.084,-36.784,-8.222,-36.833,-8.227,-36.963,-8.285,-37.061,-8.231,-37.128,-8.164,-37.15,-7.977,-37.213,-7.959,-37.342,-7.999,-37.226,-7.816,-37.168,-7.762,-37.213,-7.646,-37.199,-7.575,-37.016,-7.508,-37.016,-7.401,-37.15,-7.347,-37.253,-7.271,-37.346,-7.298,-37.382,-7.352,-37.436,-7.347,-37.552,-7.477,-37.708,-7.548,-37.793,-7.637,-37.896,-7.686,-37.976,-7.771,-38.057,-7.758,-38.088,-7.82,-38.15,-7.771,-38.168,-7.807,-38.226,-7.811,-38.257,-7.852,-38.302,-7.829,-38.304,-7.782,-38.355,-7.7,-38.418,-7.749,-38.449,-7.736,-38.525,-7.767,-38.588,-7.74,-38.592,-7.691,-38.646,-7.677,-38.699,-7.619]},{"z":3,"b":[-41.369,-8.713,-40.53,-7.392],"l":[-40.538,-7.392,-40.646,-7.401,-40.69,-7.427,-40.69,-7.512,-40.619,-7.637,-40.659,-7.762,-40.547,-7.834,-40.53,-7.905,-40.543,-8.03,-40.592,-8.124,-40.748,-8.244,-40.833,-8.365,-40.887,-8.352,-40.922,-8.432,-41.016,-8.419,-41.083,-8.526,-41.159,-8.548,-41.213,-8.633,-41.369,-8.713]},{"z":3,"b":[-46.271,-14.869,-45.913,-12.798],"l":[-46.025,-14.869,-45.931,-14.749,-45.913,-14.691,-45.963,-14.535,-45.963,-14.468,-45.922,-14.352,-46.012,-14.298,-46.038,-14.253,-46.128,-14.182,-46.172,-14.075,-46.222,-13.999,-46.262,-13.869,-46.271,-13.655,-46.186,-13.405,-46.097,-13.352,-46.083,-13.253,-46.177,-13.218,-46.181,-13.16,-46.15,-13.026,-46.074,-12.977,-46.088,-12.914,-46.235,-12.798]},{"z":3,"b":[-48.28,-16.044,-47.302,-15.49],"l":[-47.302,-16.039,-47.855,-16.044,-48.181,-16.039,-48.253,-16.03,-48.235,-15.959,-48.28,-15.838,-48.204,-15.736,-48.239,-15.691,-48.181,-15.49,-47.766,-15.49,-47.422,-15.499,-47.413,-15.535,-47.306,-15.588,-47.306,-15.709,-47.351,-15.834,-47.36,-15.986,-47.302,-16.039]},{"z":3,"b":[-53.056,-19.511,-50.964,-18.017],"l":[-50.964,-19.511,-51.034,-19.369,-51.137,-19.285,-51.311,-19.253,-51.405,-19.169,-51.543,-19.137,-51.641,-19.128,-51.842,-19.048,-51.913,-18.99,-52.061,-18.945,-52.097,-18.896,-52.271,-18.811,-52.342,-18.816,-52.498,-18.704,-52.614,-18.722,-52.762,-18.709,-52.869,-18.682,-52.891,-18.637,-52.847,-18.535,-52.788,-18.463,-52.784,-18.392,-52.909,-18.347,-52.989,-18.387,-53.034,-18.356,-53.056,-18.294,-53.043,-18.097,-53.052,-18.017]},{"z":3,"b":[-50.965,-19.513,-46.025,-14.713],"l":[-46.025,-14.869,-46.079,-14.923,-46.19,-14.936,-46.297,-14.91,-46.315,-14.847,-46.387,-14.771,-46.516,-14.713,-46.583,-14.794,-46.556,-14.869,-46.579,-14.914,-46.547,-15.039,-46.637,-15.07,-46.735,-15.017,-46.833,-15.012,-46.891,-15.048,-46.931,-15.227,-46.891,-15.24,-46.855,-15.325,-46.936,-15.432,-46.931,-15.544,-46.869,-15.593,-46.833,-15.843,-46.864,-15.883,-47.079,-15.936,-47.128,-15.923,-47.23,-16.035,-47.302,-16.039,-47.338,-16.146,-47.324,-16.227,-47.351,-16.303,-47.431,-16.41,-47.454,-16.494,-47.405,-16.57,-47.266,-16.66,-47.159,-16.919,-47.15,-16.977,-47.23,-17.026,-47.213,-17.075,-47.311,-17.142,-47.422,-17.271,-47.454,-17.347,-47.498,-17.329,-47.53,-17.454,-47.458,-17.526,-47.409,-17.499,-47.288,-17.548,-47.271,-17.669,-47.329,-17.744,-47.355,-17.825,-47.347,-17.878,-47.28,-18.061,-47.319,-18.083,-47.447,-18.171,-47.786,-18.379,-47.929,-18.445,-48.018,-18.434,-48.227,-18.34,-48.954,-18.326,-49.108,-18.388,-49.179,-18.405,-49.247,-18.468,-49.248,-18.469,-49.248,-18.47,-49.249,-18.471,-49.25,-18.472,-49.251,-18.473,-49.252,-18.475,-49.254,-18.476,-49.255,-18.478,-49.257,-18.481,-49.258,-18.483,-49.26,-18.485,-49.262,-18.488,-49.264,-18.491,-49.267,-18.494,-49.269,-18.497,-49.271,-18.5,-49.274,-18.503,-49.276,-18.506,-49.279,-18.509,-49.281,-18.513,-49.284,-18.516,-49.287,-18.52,-49.289,-18.523,-49.292,-18.527,-49.295,-18.53,-49.297,-18.534,-49.3,-18.537,-49.303,-18.541,-49.305,-18.544,-49.308,-18.547,-49.31,-18.55,-49.313,-18.554,-49.315,-18.557,-49.317,-18.56,-49.32,-18.563,-49.322,-18.565,-49.324,-18.568,-49.326,-18.57,-49.327,-18.573,-49.329,-18.575,-49.33,-18.577,-49.332,-18.579,-49.333,-18.58,-49.334,-18.582,-49.335,-18.583,-49.336,-18.584,-49.337,-18.585,-49.417,-18.603,-49.488,-18.522,-49.553,-18.51,-49.612,-18.567,-49.678,-18.6,-49.789,-18.615,-49.96,-18.616,-50.094,-18.641,-50.247,-18.69,-50.368,-18.769,-50.458,-18.877,-50.499,-18.953,-50.49,-19.02,-50.532,-19.087,-50.578,-19.112,-50.642,-19.118,-50.713,-19.161,-50.792,-19.238,-50.844,-19.311,-50.869,-19.377,-50.871,-19.424,-50.864,-19.438,-50.881,-19.459,-50.924,-19.464,-50.964,-19.511,-50.965,-19.513]},{"z":3,"b":[-44.864,-23.294,-44.168,-22.405],"l":[-44.815,-23.294,-44.864,-23.204,-44.806,-23.137,-44.797,-22.99,-44.726,-22.936,-44.588,-22.878,-44.472,-22.852,-44.36,-22.856,-44.275,-22.82,-44.248,-22.749,-44.168,-22.7,-44.217,-22.593,-44.538,-22.619,-44.637,-22.602,-44.65,-22.561,-44.771,-22.423,-44.833,-22.405]},{"z":3,"b":[-46.57,-12.798,-45.757,-10.329],"l":[-46.235,-12.798,-46.235,-12.713,-46.159,-12.602,-46.159,-12.503,-46.315,-12.423,-46.347,-12.343,-46.338,-12.128,-46.324,-12.093,-46.248,-12.048,-46.213,-11.999,-46.079,-11.927,-46.101,-11.865,-46.159,-11.834,-46.248,-11.843,-46.275,-11.767,-46.244,-11.727,-46.105,-11.664,-46.083,-11.602,-46.19,-11.544,-46.427,-11.499,-46.489,-11.414,-46.552,-11.378,-46.57,-11.316,-46.53,-11.236,-46.463,-11.177,-46.369,-10.968,-46.248,-10.914,-46.235,-10.883,-46.302,-10.758,-46.088,-10.584,-46.038,-10.57,-45.86,-10.468,-45.757,-10.329]},{"z":3,"b":[-45.945,-10.329,-45.757,-10.316],"l":[-45.757,-10.329,-45.945,-10.316]},{"z":3,"b":[-50.485,-13.383,-46.235,-12.388],"l":[-50.485,-12.843,-50.392,-12.617,-50.296,-12.49,-50.164,-12.388,-50.137,-12.477,-50.204,-12.57,-50.199,-12.655,-50.275,-12.789,-50.271,-12.892,-50.239,-12.932,-50.052,-13.021,-49.972,-13.026,-49.913,-13.07,-49.686,-13.177,-49.521,-13.2,-49.382,-13.249,-49.36,-13.133,-49.297,-13.003,-49.137,-12.736,-49.016,-12.655,-48.998,-12.749,-48.927,-12.785,-48.824,-12.874,-48.797,-12.928,-48.802,-12.999,-48.766,-13.097,-48.762,-13.209,-48.775,-13.352,-48.739,-13.383,-48.686,-13.338,-48.655,-13.177,-48.588,-13.155,-48.48,-13.195,-48.369,-13.186,-48.226,-13.093,-48.164,-13.103,-48.175,-13.181,-48.074,-13.195,-48.003,-13.249,-47.936,-13.244,-47.855,-13.294,-47.681,-13.356,-47.659,-13.32,-47.722,-13.151,-47.641,-13.115,-47.556,-13.244,-47.44,-13.249,-47.293,-13.2,-47.092,-13.097,-46.972,-13.07,-46.829,-13.07,-46.753,-13.035,-46.646,-12.923,-46.592,-12.892,-46.436,-12.847,-46.302,-12.834,-46.235,-12.798]},{"z":3,"b":[-38.244,-11.557,-37.388,-9.512],"l":[-37.388,-11.497,-37.574,-11.539,-37.632,-11.521,-37.659,-11.557,-37.797,-11.526,-37.851,-11.441,-37.967,-11.401,-38.007,-11.356,-37.98,-11.24,-38.061,-11.169,-38.11,-11.03,-38.177,-10.981,-38.235,-10.896,-38.244,-10.825,-38.19,-10.718,-38.11,-10.709,-38.012,-10.753,-37.971,-10.753,-37.815,-10.677,-37.784,-10.628,-37.824,-10.548,-37.838,-10.423,-37.784,-10.307,-37.78,-10.079,-37.9,-9.95,-37.9,-9.914,-37.963,-9.874,-38.03,-9.727,-37.994,-9.646,-38.043,-9.606,-37.976,-9.512]},{"z":3,"b":[-53.663,-26.7,-48.581,-25.972],"l":[-48.581,-25.985,-48.65,-25.972,-48.918,-25.977,-48.945,-26.008,-49.11,-25.994,-49.213,-26.03,-49.297,-26.106,-49.454,-26.169,-49.489,-26.222,-49.597,-26.222,-49.722,-26.16,-49.753,-26.124,-49.882,-26.039,-49.949,-26.012,-50.19,-26.053,-50.315,-26.053,-50.369,-26.086,-50.458,-26.026,-50.543,-26.026,-50.646,-26.07,-50.735,-26.231,-50.771,-26.222,-50.9,-26.28,-50.945,-26.244,-51.074,-26.236,-51.248,-26.347,-51.288,-26.423,-51.28,-26.499,-51.239,-26.606,-51.284,-26.651,-51.422,-26.7,-51.503,-26.602,-51.873,-26.602,-52.007,-26.584,-52.199,-26.45,-52.458,-26.432,-52.543,-26.401,-52.641,-26.401,-52.672,-26.378,-52.815,-26.338,-52.913,-26.365,-52.994,-26.352,-53.123,-26.369,-53.28,-26.262,-53.355,-26.24,-53.458,-26.289,-53.516,-26.289,-53.663,-26.258]},{"z":3,"b":[-63.394,-1.449,-58.869,2.224],"l":[-63.394,2.224,-63.306,2.166,-63.15,2.18,-63.123,2.113,-63.003,2.019,-62.847,2.019,-62.708,1.948,-62.69,1.912,-62.73,1.831,-62.713,1.738,-62.789,1.604,-62.722,1.497,-62.623,1.416,-62.614,1.363,-62.516,1.059,-62.512,0.961,-62.445,0.805,-62.534,0.733,-62.539,0.675,-62.485,0.541,-62.489,0.488,-62.53,0.43,-62.534,0.314,-62.57,0.247,-62.565,0.175,-62.525,0.095,-62.579,0.019,-62.57,-0.048,-62.512,-0.124,-62.476,-0.222,-62.405,-0.271,-62.373,-0.343,-62.369,-0.468,-62.32,-0.521,-62.311,-0.642,-62.378,-0.718,-62.476,-0.695,-62.507,-0.771,-62.418,-0.829,-62.315,-0.945,-62.244,-0.981,-62.204,-1.044,-62.141,-1.066,-62.03,-1.146,-61.936,-1.244,-61.878,-1.369,-61.881,-1.389,-61.799,-1.389,-61.717,-1.401,-61.62,-1.449,-61.619,-1.276,-61.574,-1.142,-61.561,-1.021,-61.579,-0.945,-61.534,-0.727,-61.458,-0.637,-61.244,-0.548,-61.222,-0.499,-61.105,-0.494,-61.061,-0.53,-60.931,-0.557,-60.905,-0.611,-60.815,-0.686,-60.735,-0.852,-60.641,-0.861,-60.516,-0.829,-60.467,-0.74,-60.338,-0.704,-60.306,-0.673,-60.315,-0.619,-60.391,-0.521,-60.387,-0.463,-60.315,-0.307,-60.311,-0.236,-60.253,-0.151,-60.217,-0.044,-60.168,0.006,-60.128,0.131,-60.025,0.224,-59.771,0.229,-58.869,0.224]},{"z":3,"b":[-58.974,0.224,-58.869,1.386],"l":[-58.974,1.386,-58.869,0.224]},{"z":3,"b":[-73.797,-9.838,-66.828,-7.117],"l":[-73.797,-7.117,-72.664,-7.593,-70.373,-8.155,-69.806,-8.454,-66.829,-9.838,-66.828,-9.838]},{"z":3,"b":[-61.61,-13.663,-59.824,-8.767],"l":[-61.61,-8.767,-61.507,-8.847,-61.485,-8.914,-61.53,-8.999,-61.556,-9.128,-61.53,-9.227,-61.592,-9.24,-61.61,-9.32,-61.547,-9.41,-61.552,-9.463,-61.48,-9.637,-61.521,-9.704,-61.565,-9.727,-61.521,-9.861,-61.534,-9.994,-61.57,-10.061,-61.565,-10.262,-61.472,-10.436,-61.503,-10.686,-61.472,-10.758,-61.521,-10.789,-61.507,-10.878,-61.521,-10.954,-61.472,-10.999,-60.44,-11.003,-60.44,-11.039,-60.382,-11.102,-60.28,-11.079,-60.177,-11.119,-60.07,-11.115,-60.003,-11.146,-59.909,-11.383,-60.025,-11.535,-60.101,-11.602,-60.101,-11.744,-60.061,-11.905,-59.985,-11.919,-59.931,-12.052,-59.887,-12.128,-59.891,-12.244,-59.824,-12.387,-59.909,-12.619,-60.003,-12.731,-60.043,-12.874,-60.083,-12.928,-60.195,-12.972,-60.271,-13.07,-60.275,-13.137,-60.351,-13.271,-60.382,-13.419,-60.659,-13.602,-60.725,-13.663]},{"z":3,"b":[-38.605,-6.396,-37.158,-4.799],"l":[-38.525,-6.383,-38.605,-6.396,-38.579,-6.262,-38.512,-6.182,-38.494,-6.128,-38.413,-6.061,-38.369,-6.093,-38.302,-6.075,-38.128,-5.878,-38.079,-5.762,-38.079,-5.686,-38.047,-5.615,-37.913,-5.463,-37.721,-5.061,-37.596,-4.959,-37.543,-4.932,-37.158,-4.799]},{"z":3,"b":[-38.525,-6.977,-34.92,-6.026],"l":[-38.525,-6.383,-38.436,-6.414,-38.284,-6.503,-38.239,-6.486,-38.132,-6.521,-38.056,-6.445,-38.003,-6.432,-37.842,-6.343,-37.793,-6.294,-37.757,-6.177,-37.641,-6.128,-37.476,-6.084,-37.422,-6.097,-37.262,-6.026,-37.177,-6.052,-37.177,-6.115,-37.396,-6.392,-37.431,-6.517,-37.507,-6.552,-37.516,-6.682,-37.338,-6.7,-37.271,-6.74,-37.217,-6.825,-37.155,-6.785,-37.043,-6.753,-36.989,-6.709,-36.945,-6.749,-36.793,-6.767,-36.744,-6.825,-36.771,-6.932,-36.713,-6.977,-36.659,-6.932,-36.574,-6.927,-36.503,-6.785,-36.53,-6.736,-36.534,-6.637,-36.445,-6.624,-36.516,-6.481,-36.494,-6.378,-36.382,-6.302,-36.293,-6.307,-36.253,-6.414,-35.927,-6.468,-35.721,-6.445,-35.668,-6.427,-35.467,-6.463,-35.306,-6.53,-35.257,-6.508,-35.047,-6.535,-34.92,-6.481]},{"z":3,"b":[-41.864,-21.276,-40.967,-20.776],"l":[-41.864,-20.776,-41.744,-20.82,-41.713,-20.977,-41.73,-21.044,-41.713,-21.111,-41.44,-21.2,-41.391,-21.186,-41.266,-21.231,-41.07,-21.213,-40.967,-21.276]},{"z":3,"b":[-45.757,-10.896,-41.369,-8.713],"l":[-45.757,-10.329,-45.605,-10.334,-45.516,-10.414,-45.472,-10.486,-45.44,-10.619,-45.315,-10.78,-45.043,-10.896,-44.931,-10.861,-44.851,-10.878,-44.784,-10.856,-44.753,-10.785,-44.646,-10.736,-44.619,-10.686,-44.538,-10.628,-44.418,-10.588,-44.311,-10.602,-44.217,-10.628,-44.119,-10.588,-43.994,-10.454,-43.913,-10.432,-43.802,-10.204,-43.762,-10.169,-43.753,-10.106,-43.704,-10.035,-43.713,-9.95,-43.681,-9.802,-43.726,-9.749,-43.784,-9.566,-43.829,-9.499,-43.811,-9.427,-43.695,-9.445,-43.659,-9.365,-43.605,-9.338,-43.538,-9.361,-43.449,-9.302,-43.415,-9.336,-43.378,-9.414,-43.186,-9.419,-43.128,-9.374,-43.043,-9.396,-43.03,-9.436,-42.937,-9.474,-42.931,-9.517,-42.851,-9.548,-42.753,-9.521,-42.619,-9.566,-42.574,-9.486,-42.489,-9.499,-42.431,-9.41,-42.311,-9.307,-42.15,-9.294,-42.043,-9.209,-41.918,-9.213,-41.851,-9.253,-41.797,-9.173,-41.735,-9.133,-41.735,-8.981,-41.61,-8.963,-41.556,-8.972,-41.498,-8.936,-41.467,-8.865,-41.4,-8.785,-41.369,-8.713]},{"z":3,"b":[-41.449,-7.392,-40.418,-2.869],"l":[-40.538,-7.392,-40.534,-7.325,-40.583,-7.195,-40.512,-7.003,-40.44,-6.905,-40.418,-6.811,-40.48,-6.736,-40.614,-6.709,-40.708,-6.66,-40.722,-6.57,-40.771,-6.494,-40.797,-6.378,-40.806,-6.253,-40.891,-6.012,-40.891,-5.905,-40.922,-5.682,-40.918,-5.602,-40.949,-5.419,-41.025,-5.365,-41.07,-5.307,-41.061,-5.169,-41.114,-5.066,-41.123,-5.008,-41.186,-4.914,-41.181,-4.811,-41.226,-4.727,-41.244,-4.615,-41.235,-4.539,-41.141,-4.427,-41.074,-4.325,-41.119,-4.177,-41.181,-4.124,-41.23,-4.044,-41.284,-3.811,-41.355,-3.709,-41.382,-3.593,-41.449,-3.436,-41.427,-3.32,-41.378,-3.271,-41.257,-3.079,-41.273,-2.869]},{"z":3,"b":[-38.73,-7.619,-38.525,-6.383],"l":[-38.699,-7.619,-38.637,-7.535,-38.632,-7.459,-38.583,-7.427,-38.534,-7.294,-38.547,-7.236,-38.601,-7.222,-38.677,-7.16,-38.686,-7.03,-38.73,-7.003,-38.726,-6.892,-38.65,-6.838,-38.619,-6.758,-38.65,-6.691,-38.579,-6.481,-38.525,-6.383]},{"z":3,"b":[-54.793,-1.177,-51.888,2.511],"l":[-54.682,2.511,-54.735,2.416,-54.717,2.265,-54.788,2.131,-54.753,2.081,-54.793,2.01,-54.762,1.97,-54.766,1.898,-54.735,1.773,-54.579,1.782,-54.498,1.747,-54.364,1.76,-54.204,1.657,-54.172,1.657,-54.083,1.541,-54.079,1.506,-54.007,1.523,-53.922,1.461,-53.891,1.407,-53.757,1.394,-53.739,1.434,-53.659,1.421,-53.65,1.363,-53.556,1.367,-53.538,1.242,-53.44,1.26,-53.409,1.184,-53.467,1.153,-53.418,0.943,-53.266,0.782,-53.132,0.751,-53.114,0.72,-53.141,0.537,-53.128,0.385,-53.043,0.247,-53.007,0.135,-53.016,0.055,-52.976,-0.017,-52.913,-0.191,-52.829,-0.182,-52.699,-0.302,-52.628,-0.396,-52.632,-0.557,-52.605,-0.611,-52.525,-0.646,-52.507,-0.731,-52.516,-0.874,-52.4,-0.869,-52.369,-0.923,-52.391,-0.954,-52.36,-1.07,-52.239,-1.146,-52.132,-1.151,-52.052,-1.177,-51.985,-1.142,-51.888,-1.16]},{"z":3,"b":[-58.869,-7.338,-56.391,0.224],"l":[-58.869,0.224,-58.851,-0.084,-58.864,-0.173,-58.855,-0.352,-58.739,-0.432,-58.748,-0.624,-58.704,-0.695,-58.628,-0.767,-58.57,-0.762,-58.44,-0.861,-58.391,-1.048,-58.315,-1.119,-58.253,-1.128,-58.159,-1.235,-58.083,-1.298,-57.98,-1.347,-57.9,-1.423,-57.829,-1.445,-57.655,-1.588,-57.565,-1.624,-57.449,-1.691,-57.338,-1.731,-57.275,-1.713,-57.248,-1.758,-57.168,-1.771,-57.088,-1.811,-57.007,-1.936,-56.824,-2.035,-56.744,-2.052,-56.748,-2.177,-56.637,-2.222,-56.498,-2.16,-56.485,-2.249,-56.401,-2.336,-56.391,-2.392,-56.757,-3.186,-58.293,-6.499,-58.375,-6.57,-58.437,-6.676,-58.455,-6.794,-58.386,-6.949,-58.228,-7.142,-58.146,-7.263,-58.137,-7.338]},{"z":3,"b":[-58.137,-9.843,-50.235,-7.338],"l":[-50.235,-9.843,-51.297,-9.789,-55.092,-9.566,-56.463,-9.468,-56.489,-9.468,-56.677,-9.378,-56.771,-9.396,-56.838,-9.271,-56.927,-9.244,-57.047,-9.231,-57.074,-9.186,-57.083,-9.079,-57.101,-9.052,-57.297,-8.959,-57.48,-8.794,-57.579,-8.758,-57.641,-8.615,-57.65,-8.499,-57.681,-8.436,-57.641,-8.231,-57.78,-8.048,-57.896,-7.691,-57.949,-7.619,-57.98,-7.53,-58.061,-7.41,-58.137,-7.338]},{"z":3,"b":[-41.882,-20.776,-40.217,-17.892],"l":[-41.864,-20.776,-41.882,-20.758,-41.811,-20.646,-41.851,-20.624,-41.797,-20.535,-41.797,-20.428,-41.855,-20.369,-41.717,-20.209,-41.405,-20.213,-41.364,-20.186,-41.306,-19.954,-41.186,-19.865,-41.159,-19.66,-41.03,-19.548,-41.047,-19.49,-40.958,-19.472,-40.927,-19.294,-40.945,-19.146,-41.052,-19.035,-41.025,-18.981,-41.105,-18.887,-41.163,-18.905,-41.235,-18.852,-41.195,-18.807,-41.092,-18.829,-40.931,-18.807,-40.927,-18.695,-41.03,-18.637,-41.003,-18.454,-41.025,-18.405,-41.128,-18.334,-41.141,-18.298,-41.061,-18.177,-41.016,-18.173,-40.913,-18.102,-40.838,-18.151,-40.766,-18.137,-40.775,-18.093,-40.909,-17.972,-40.788,-17.972,-40.726,-17.945,-40.668,-17.954,-40.534,-17.901,-40.476,-17.928,-40.427,-17.892,-40.217,-17.972]},{"z":3,"b":[-40.217,-17.977,-40.208,-17.972],"l":[-40.217,-17.972,-40.208,-17.977]},{"z":3,"b":[-61.61,-8.767,-58.137,-7.338],"l":[-61.61,-8.767,-58.547,-8.749,-58.472,-8.691,-58.427,-8.523,-58.397,-8.416,-58.354,-8.276,-58.329,-8.051,-58.354,-7.906,-58.33,-7.78,-58.259,-7.673,-58.224,-7.575,-58.224,-7.485,-58.202,-7.415,-58.137,-7.338]},{"z":3,"b":[-53.065,-18.021,-53.052,-18.017],"l":[-53.052,-18.017,-53.065,-18.021]},{"z":3,"b":[-40.208,-18.325,-39.672,-17.977],"l":[-40.208,-17.977,-40.186,-18.008,-39.672,-18.325]},{"z":3,"b":[-50.717,-12.843,-50.237,-9.846],"l":[-50.485,-12.843,-50.566,-12.82,-50.614,-12.763,-50.655,-12.667,-50.66,-12.558,-50.629,-12.437,-50.632,-12.289,-50.668,-12.112,-50.675,-11.99,-50.653,-11.922,-50.661,-11.854,-50.717,-11.749,-50.666,-11.652,-50.671,-11.574,-50.717,-11.483,-50.708,-11.346,-50.645,-11.163,-50.61,-11.001,-50.599,-10.69,-50.551,-10.567,-50.455,-10.388,-50.403,-10.258,-50.395,-10.177,-50.337,-10.034,-50.237,-9.846]},{"z":3,"b":[-48.73,-5.356,-46.006,-1.146],"l":[-48.722,-5.356,-48.73,-5.338,-48.253,-4.954,-47.802,-4.597,-47.655,-4.606,-47.583,-4.548,-47.489,-4.423,-47.458,-4.338,-47.382,-4.276,-47.333,-4.164,-47.311,-4.066,-47.288,-4.079,-47.065,-3.843,-47.025,-3.597,-47.03,-3.57,-46.967,-3.526,-46.945,-3.396,-46.86,-3.329,-46.824,-3.329,-46.744,-3.191,-46.668,-3.093,-46.637,-2.986,-46.65,-2.914,-46.574,-2.847,-46.668,-2.74,-46.601,-2.664,-46.507,-2.615,-46.485,-2.548,-46.427,-2.512,-46.436,-2.41,-46.409,-2.387,-46.413,-2.24,-46.378,-2.253,-46.28,-2.142,-46.213,-1.927,-46.217,-1.807,-46.302,-1.802,-46.315,-1.731,-46.244,-1.722,-46.199,-1.677,-46.181,-1.575,-46.199,-1.486,-46.123,-1.347,-46.159,-1.316,-46.141,-1.24,-46.006,-1.146]},{"z":3,"b":[-45.98,-10.316,-41.815,-2.679],"l":[-45.945,-10.316,-45.936,-10.213,-45.954,-10.182,-45.9,-10.084,-45.9,-10.026,-45.86,-9.999,-45.86,-9.834,-45.838,-9.535,-45.797,-9.463,-45.829,-9.374,-45.896,-9.329,-45.905,-9.177,-45.936,-9.119,-45.94,-9.012,-45.98,-8.927,-45.927,-8.789,-45.82,-8.7,-45.753,-8.561,-45.735,-8.432,-45.659,-8.311,-45.655,-8.253,-45.579,-8.151,-45.543,-7.869,-45.472,-7.673,-45.297,-7.557,-45.025,-7.494,-44.909,-7.445,-44.815,-7.365,-44.713,-7.396,-44.579,-7.249,-44.449,-7.146,-44.306,-7.111,-44.257,-7.008,-44.168,-6.923,-44.163,-6.887,-44.092,-6.807,-44.061,-6.829,-44.038,-6.767,-43.954,-6.762,-43.815,-6.709,-43.677,-6.7,-43.561,-6.753,-43.463,-6.843,-43.302,-6.802,-43.199,-6.753,-43.132,-6.78,-42.98,-6.744,-42.918,-6.669,-42.913,-6.615,-42.855,-6.481,-42.869,-6.436,-42.851,-6.253,-42.963,-6.186,-43.003,-6.124,-43.074,-6.057,-43.11,-5.771,-43.083,-5.713,-43.101,-5.624,-43.047,-5.597,-42.913,-5.392,-42.833,-5.311,-42.806,-5.16,-42.833,-5.097,-42.855,-4.936,-42.905,-4.829,-42.954,-4.776,-42.931,-4.731,-42.949,-4.66,-42.913,-4.637,-42.864,-4.499,-42.896,-4.405,-42.954,-4.392,-42.989,-4.24,-42.891,-4.142,-42.851,-4.039,-42.735,-3.923,-42.663,-3.789,-42.677,-3.7,-42.628,-3.615,-42.574,-3.57,-42.503,-3.45,-42.454,-3.477,-42.364,-3.45,-42.217,-3.432,-42.101,-3.302,-42.105,-3.262,-42.003,-3.231,-41.922,-3.111,-41.842,-3.035,-41.815,-2.936,-41.869,-2.852,-41.831,-2.679]},{"z":3,"b":[-41.369,-9.472,-38.216,-8.559],"l":[-41.369,-8.713,-41.324,-8.736,-41.159,-8.709,-41.105,-8.718,-41.101,-8.776,-41.004,-8.781,-40.98,-8.816,-40.891,-8.856,-40.891,-9.035,-40.855,-9.093,-40.851,-9.151,-40.704,-9.218,-40.69,-9.343,-40.744,-9.423,-40.686,-9.472,-40.565,-9.46,-40.413,-9.397,-40.32,-9.293,-40.273,-9.145,-40.212,-9.102,-40.078,-9.088,-40.005,-9.058,-39.949,-9.022,-39.909,-8.98,-39.888,-8.924,-39.885,-8.853,-39.835,-8.809,-39.738,-8.791,-39.688,-8.749,-39.683,-8.683,-39.601,-8.62,-39.442,-8.559,-39.333,-8.566,-39.257,-8.64,-39.098,-8.715,-38.835,-8.8,-38.705,-8.88,-38.668,-8.896,-38.619,-8.896,-38.577,-8.861,-38.544,-8.831,-38.505,-8.835,-38.484,-8.878,-38.482,-8.96,-38.433,-9.004,-38.336,-9.01,-38.294,-9.043,-38.308,-9.102,-38.283,-9.204,-38.221,-9.348,-38.216,-9.351]},{"z":3,"b":[-46.074,-17.977,-39.864,-14.24],"l":[-40.208,-17.977,-40.23,-17.919,-40.186,-17.816,-40.208,-17.767,-40.284,-17.718,-40.382,-17.624,-40.413,-17.557,-40.48,-17.557,-40.498,-17.419,-40.61,-17.392,-40.605,-17.311,-40.565,-17.262,-40.574,-17.133,-40.525,-16.932,-40.471,-16.869,-40.297,-16.878,-40.244,-16.834,-40.284,-16.744,-40.275,-16.615,-40.253,-16.566,-40.137,-16.544,-40.132,-16.503,-40.056,-16.396,-39.94,-16.311,-39.864,-16.111,-39.936,-15.999,-40.003,-15.994,-40.132,-15.892,-40.168,-15.896,-40.235,-15.82,-40.329,-15.825,-40.476,-15.776,-40.534,-15.798,-40.574,-15.758,-40.668,-15.718,-40.762,-15.74,-40.829,-15.682,-40.891,-15.695,-40.945,-15.673,-41.03,-15.736,-41.159,-15.78,-41.333,-15.718,-41.36,-15.494,-41.802,-15.111,-41.94,-15.173,-42.088,-15.182,-42.177,-15.106,-42.262,-15.106,-42.431,-15.035,-42.579,-14.932,-42.632,-14.941,-42.891,-14.749,-42.954,-14.677,-43.23,-14.637,-43.387,-14.7,-43.449,-14.78,-43.494,-14.789,-43.708,-14.736,-43.824,-14.695,-43.864,-14.66,-43.879,-14.625,-43.879,-14.624,-43.879,-14.623,-43.879,-14.622,-43.879,-14.621,-43.878,-14.62,-43.878,-14.618,-43.878,-14.617,-43.878,-14.615,-43.877,-14.613,-43.877,-14.611,-43.876,-14.609,-43.876,-14.607,-43.875,-14.605,-43.875,-14.603,-43.874,-14.6,-43.874,-14.598,-43.873,-14.595,-43.873,-14.593,-43.872,-14.59,-43.872,-14.588,-43.871,-14.585,-43.871,-14.582,-43.87,-14.579,-43.87,-14.577,-43.869,-14.574,-43.868,-14.571,-43.868,-14.569,-43.867,-14.566,-43.867,-14.564,-43.866,-14.561,-43.866,-14.559,-43.865,-14.556,-43.865,-14.554,-43.864,-14.552,-43.864,-14.549,-43.864,-14.547,-43.863,-14.546,-43.863,-14.544,-43.862,-14.542,-43.862,-14.541,-43.862,-14.539,-43.862,-14.538,-43.861,-14.537,-43.861,-14.536,-43.861,-14.535,-43.861,-14.534,-43.8,-14.37,-43.781,-14.343,-43.838,-14.316,-44.159,-14.271,-44.213,-14.24,-44.32,-14.244,-44.36,-14.271,-44.561,-14.347,-44.838,-14.517,-44.882,-14.597,-45.101,-14.718,-45.226,-14.74,-45.32,-14.856,-45.463,-14.941,-45.552,-14.936,-45.655,-15.044,-45.713,-15.124,-45.766,-15.146,-45.927,-15.128,-45.967,-15.186,-46.074,-15.249,-45.976,-14.986,-46.025,-14.869]},{"z":3,"b":[-50.994,-22.892,-44.833,-19.791],"l":[-44.833,-22.405,-44.922,-22.45,-45.056,-22.468,-45.244,-22.561,-45.271,-22.606,-45.409,-22.646,-45.471,-22.612,-45.53,-22.651,-45.583,-22.615,-45.63,-22.623,-45.672,-22.619,-45.802,-22.7,-45.744,-22.727,-45.744,-22.798,-45.793,-22.852,-45.86,-22.861,-45.909,-22.82,-46.021,-22.874,-46.146,-22.892,-46.155,-22.847,-46.288,-22.883,-46.36,-22.843,-46.369,-22.749,-46.472,-22.682,-46.396,-22.615,-46.454,-22.517,-46.543,-22.472,-46.547,-22.441,-46.641,-22.41,-46.699,-22.32,-46.663,-22.204,-46.601,-22.133,-46.659,-22.053,-46.619,-21.986,-46.659,-21.905,-46.668,-21.811,-46.628,-21.771,-46.605,-21.682,-46.561,-21.673,-46.494,-21.526,-46.507,-21.459,-46.605,-21.428,-46.655,-21.374,-46.686,-21.396,-46.815,-21.361,-46.896,-21.405,-47.003,-21.401,-46.994,-21.343,-47.056,-21.195,-47.123,-21.133,-47.146,-20.981,-47.222,-20.914,-47.213,-20.807,-47.11,-20.633,-47.155,-20.526,-47.253,-20.477,-47.293,-20.423,-47.266,-20.285,-47.235,-20.204,-47.244,-20.174,-47.329,-20.118,-47.428,-20.036,-47.46,-20.001,-47.518,-19.994,-47.594,-20.019,-47.656,-20.023,-47.696,-19.991,-47.75,-19.977,-47.819,-19.981,-47.86,-20.012,-47.862,-20.056,-47.909,-20.082,-47.991,-20.083,-48.051,-20.12,-48.109,-20.117,-48.178,-20.083,-48.253,-20.093,-48.364,-20.138,-48.716,-20.144,-48.812,-20.159,-48.858,-20.233,-48.887,-20.37,-48.918,-20.429,-48.952,-20.41,-48.978,-20.338,-48.995,-20.214,-49.04,-20.187,-49.113,-20.259,-49.166,-20.295,-49.2,-20.294,-49.239,-20.246,-49.284,-20.149,-49.288,-20.074,-49.259,-20.017,-49.278,-19.987,-49.377,-19.978,-50.364,-19.859,-50.434,-19.8,-50.489,-19.791,-50.584,-19.821,-50.994,-20.102]},{"z":3,"b":[-53.119,-22.642,-50.994,-20.102],"l":[-50.994,-20.102,-51.064,-20.223,-51.161,-20.307,-51.397,-20.442,-51.534,-20.558,-51.593,-20.666,-51.619,-20.807,-51.692,-20.944,-51.811,-21.077,-51.866,-21.183,-51.857,-21.26,-51.873,-21.33,-51.936,-21.423,-51.955,-21.469,-51.99,-21.494,-52.048,-21.511,-52.073,-21.554,-52.065,-21.623,-52.123,-21.718,-52.246,-21.84,-52.339,-21.957,-52.403,-22.069,-52.566,-22.207,-52.961,-22.455,-53.116,-22.64,-53.119,-22.642]},{"z":3,"b":[-53.116,-22.794,-51.003,-22.521],"l":[-51.003,-22.794,-51.181,-22.74,-51.355,-22.651,-51.547,-22.686,-51.641,-22.651,-51.695,-22.664,-51.744,-22.619,-51.873,-22.611,-51.985,-22.548,-52.065,-22.521,-52.141,-22.544,-52.163,-22.602,-52.217,-22.642,-52.262,-22.602,-52.315,-22.619,-52.445,-22.602,-52.521,-22.615,-52.614,-22.57,-52.695,-22.602,-52.949,-22.57,-53.045,-22.636,-53.116,-22.64]},{"z":3,"b":[-51.003,-25.297,-48.082,-22.794],"l":[-48.082,-25.297,-48.092,-25.236,-48.186,-25.191,-48.23,-25.012,-48.266,-25.035,-48.413,-24.959,-48.512,-25.084,-48.565,-25.057,-48.543,-24.811,-48.489,-24.744,-48.614,-24.682,-48.78,-24.695,-48.829,-24.664,-49.025,-24.669,-49.092,-24.686,-49.217,-24.691,-49.297,-24.664,-49.311,-24.53,-49.239,-24.419,-49.226,-24.338,-49.275,-24.316,-49.338,-24.213,-49.333,-24.142,-49.409,-24.084,-49.601,-23.865,-49.57,-23.834,-49.552,-23.713,-49.605,-23.642,-49.655,-23.508,-49.614,-23.41,-49.637,-23.258,-49.73,-23.106,-49.909,-23.035,-49.913,-22.986,-49.958,-22.963,-49.972,-22.914,-50.123,-22.941,-50.293,-22.954,-50.369,-22.914,-50.431,-22.945,-50.628,-22.923,-50.726,-22.95,-50.784,-22.945,-50.882,-22.82,-51.003,-22.794]},{"z":3,"b":[-57.753,-18.03,-53.065,-17.186],"l":[-53.065,-18.021,-53.146,-18.03,-53.306,-17.999,-53.494,-18.012,-53.605,-17.994,-53.726,-18.008,-53.869,-17.945,-53.976,-17.932,-53.989,-17.901,-53.86,-17.691,-53.753,-17.642,-53.681,-17.258,-53.744,-17.253,-53.82,-17.307,-53.949,-17.459,-54.038,-17.512,-54.083,-17.588,-54.146,-17.628,-54.235,-17.637,-54.32,-17.673,-54.391,-17.673,-54.422,-17.579,-54.525,-17.512,-54.699,-17.517,-54.78,-17.557,-54.811,-17.602,-54.9,-17.651,-55.074,-17.682,-55.181,-17.686,-55.338,-17.584,-55.534,-17.503,-55.61,-17.392,-55.788,-17.352,-55.833,-17.311,-55.985,-17.267,-56.025,-17.213,-56.101,-17.186,-56.248,-17.218,-56.329,-17.285,-56.436,-17.32,-56.489,-17.302,-56.668,-17.32,-56.745,-17.317,-56.79,-17.386,-56.888,-17.518,-56.98,-17.595,-57.08,-17.734,-57.198,-17.812,-57.37,-17.876,-57.419,-17.882,-57.472,-17.901,-57.494,-17.869,-57.601,-17.816,-57.632,-17.74,-57.753,-17.735]},{"z":2,"b":[138.002,-25.999,138.002,-16.557],"l":[138.002,-16.557,138.002,-25.999]},{"z":2,"b":[129.002,-25.999,129.002,-14.871],"l":[129.002,-25.999,129.002,-14.871]},{"z":2,"b":[129.002,-25.999,138.002,-25.999],"l":[138.002,-25.999,129.002,-25.999]},{"z":2,"b":[138.002,-28.999,141.002,-25.999],"l":[141.002,-28.999,141.002,-25.999,138.002,-25.999]},{"z":2,"b":[129.002,-31.693,129.002,-25.999],"l":[129.002,-31.693,129.002,-25.999]},{"z":2,"b":[140.966,-38.072,140.997,-34.005],"l":[140.997,-34.016,140.968,-34.005,140.966,-38.072]},{"z":2,"b":[141.002,-29.139,153.516,-28.144],"l":[141.002,-28.999,148.974,-28.999,148.993,-28.986,148.994,-28.986,148.994,-28.985,148.995,-28.984,148.996,-28.984,148.997,-28.983,148.998,-28.982,148.999,-28.981,149,-28.98,149.001,-28.979,149.003,-28.978,149.004,-28.976,149.006,-28.975,149.007,-28.973,149.009,-28.972,149.011,-28.97,149.012,-28.969,149.014,-28.967,149.016,-28.966,149.018,-28.964,149.02,-28.962,149.022,-28.96,149.024,-28.959,149.026,-28.957,149.028,-28.955,149.029,-28.953,149.031,-28.952,149.033,-28.95,149.035,-28.948,149.037,-28.947,149.039,-28.945,149.041,-28.943,149.042,-28.942,149.044,-28.94,149.046,-28.939,149.047,-28.937,149.049,-28.936,149.05,-28.935,149.051,-28.934,149.052,-28.933,149.054,-28.932,149.055,-28.931,149.055,-28.93,149.056,-28.929,149.057,-28.929,149.057,-28.928,149.058,-28.928,149.081,-28.875,149.174,-28.807,149.335,-28.723,149.433,-28.657,149.467,-28.608,149.512,-28.579,149.596,-28.566,149.677,-28.612,149.853,-28.604,150.15,-28.558,150.341,-28.564,150.47,-28.65,150.712,-28.639,150.856,-28.66,150.983,-28.71,151.05,-28.758,151.058,-28.805,151.118,-28.851,151.231,-28.898,151.303,-28.982,151.335,-29.104,151.393,-29.139,151.478,-29.087,151.528,-29.035,151.554,-28.955,151.73,-28.887,151.761,-28.945,151.824,-28.972,151.855,-28.919,151.935,-28.923,152.024,-28.874,152.069,-28.704,152.007,-28.637,151.966,-28.535,152.163,-28.436,152.216,-28.436,152.292,-28.387,152.413,-28.343,152.493,-28.262,152.57,-28.323,152.609,-28.294,152.752,-28.356,152.846,-28.325,153.118,-28.352,153.194,-28.258,153.382,-28.236,153.516,-28.144]},{"z":2,"b":[148.77,-35.914,149.404,-35.146],"l":[148.77,-35.494,148.774,-35.637,148.792,-35.709,148.85,-35.749,148.89,-35.745,148.899,-35.82,148.949,-35.887,149.051,-35.914,149.105,-35.829,149.087,-35.611,149.127,-35.584,149.149,-35.508,149.14,-35.441,149.185,-35.383,149.279,-35.347,149.368,-35.356,149.404,-35.329,149.257,-35.271,149.194,-35.218,149.19,-35.178,149.105,-35.146,148.828,-35.311,148.77,-35.494]},{"z":2,"b":[141.002,-34.043,141.026,-28.999],"l":[141.002,-28.999,141.026,-34.043]},{"z":2,"b":[140.997,-37.501,149.944,-34.016],"l":[149.944,-37.501,148.216,-36.803,148.133,-36.777,148.162,-36.73,148.197,-36.625,148.147,-36.546,148.034,-36.181,147.975,-36.046,147.931,-36.019,147.762,-35.956,147.674,-35.954,147.591,-35.982,147.538,-35.984,147.502,-35.959,147.458,-35.954,147.407,-35.971,147.407,-35.972,147.406,-35.972,147.405,-35.973,147.404,-35.974,147.404,-35.975,147.403,-35.975,147.402,-35.976,147.401,-35.977,147.4,-35.977,147.4,-35.978,147.399,-35.979,147.398,-35.98,147.397,-35.981,147.396,-35.982,147.395,-35.983,147.394,-35.984,147.393,-35.985,147.392,-35.986,147.39,-35.987,147.389,-35.987,147.388,-35.988,147.387,-35.989,147.386,-35.99,147.385,-35.991,147.384,-35.992,147.383,-35.993,147.382,-35.994,147.381,-35.995,147.38,-35.996,147.379,-35.997,147.378,-35.998,147.377,-35.998,147.377,-35.999,147.376,-36,147.375,-36,147.375,-36.001,147.374,-36.002,147.373,-36.002,147.373,-36.003,147.372,-36.003,147.372,-36.004,147.371,-36.004,147.35,-36.052,147.323,-36.069,147.272,-36.049,147.157,-36.036,147.1,-36.05,147.047,-36.106,146.896,-36.1,146.746,-36.061,146.59,-36.005,146.47,-35.994,146.403,-36.033,146.325,-36.054,146.216,-36.057,146.038,-36.017,145.815,-35.977,145.692,-35.931,145.588,-35.863,145.432,-35.835,145.431,-35.835,145.43,-35.835,145.429,-35.835,145.426,-35.835,145.424,-35.835,145.421,-35.835,145.418,-35.835,145.415,-35.836,145.411,-35.836,145.407,-35.836,145.402,-35.836,145.397,-35.837,145.393,-35.837,145.387,-35.837,145.382,-35.837,145.377,-35.838,145.371,-35.838,145.365,-35.838,145.359,-35.839,145.353,-35.839,145.347,-35.839,145.341,-35.84,145.335,-35.84,145.328,-35.84,145.322,-35.841,145.316,-35.841,145.31,-35.841,145.304,-35.842,145.298,-35.842,145.292,-35.842,145.286,-35.842,145.28,-35.843,145.275,-35.843,145.269,-35.843,145.264,-35.844,145.259,-35.844,145.255,-35.844,145.25,-35.844,145.246,-35.845,145.242,-35.845,145.239,-35.845,145.236,-35.845,145.233,-35.845,145.23,-35.845,145.228,-35.846,145.227,-35.846,145.225,-35.846,145.224,-35.846,145.223,-35.846,145.222,-35.846,145.22,-35.846,145.219,-35.846,145.217,-35.847,145.215,-35.847,145.212,-35.847,145.21,-35.847,145.207,-35.848,145.204,-35.848,145.201,-35.848,145.197,-35.849,145.194,-35.849,145.19,-35.85,145.187,-35.85,145.183,-35.851,145.179,-35.851,145.175,-35.851,145.171,-35.852,145.166,-35.852,145.162,-35.853,145.158,-35.853,145.154,-35.854,145.15,-35.854,145.145,-35.855,145.141,-35.855,145.137,-35.856,145.133,-35.856,145.129,-35.857,145.125,-35.857,145.121,-35.858,145.117,-35.858,145.114,-35.859,145.11,-35.859,145.107,-35.859,145.104,-35.86,145.101,-35.86,145.098,-35.86,145.095,-35.861,145.093,-35.861,145.091,-35.861,145.089,-35.861,145.087,-35.862,145.086,-35.862,145.085,-35.862,145.084,-35.862,145.083,-35.862,145.008,-35.884,144.962,-35.923,144.943,-35.978,144.944,-36.029,144.965,-36.075,144.916,-36.107,144.799,-36.126,144.668,-36.088,144.524,-35.994,144.427,-35.905,144.378,-35.82,144.29,-35.73,144.163,-35.634,143.971,-35.528,143.714,-35.411,143.585,-35.327,143.584,-35.279,143.54,-35.234,143.459,-35.203,143.404,-35.168,143.377,-35.127,143.363,-35.032,143.361,-34.882,143.325,-34.788,143.221,-34.733,142.965,-34.691,142.857,-34.655,142.812,-34.607,142.774,-34.588,142.742,-34.598,142.705,-34.649,142.663,-34.741,142.619,-34.783,142.571,-34.775,142.507,-34.712,142.426,-34.593,142.388,-34.485,142.394,-34.387,142.373,-34.333,142.328,-34.322,142.287,-34.286,142.252,-34.223,142.158,-34.173,141.926,-34.117,141.761,-34.112,141.666,-34.137,141.586,-34.191,141.47,-34.183,141.318,-34.112,141.192,-34.073,141.094,-34.066,141.026,-34.043,140.997,-34.016]},{"z":2,"b":[150.612,-35.19,150.705,-35.12],"l":[150.705,-35.12,150.636,-35.146,150.612,-35.19]},{"z":3,"b":[-66.829,-9.925,-66.628,-9.838],"l":[-66.829,-9.838,-66.628,-9.925]},{"z":2,"b":[-84.325,34.988,-81.659,36.611],"l":[-84.325,34.988,-84.287,35.201,-84.261,35.25,-84.232,35.261,-84.129,35.261,-84.062,35.286,-84.004,35.414,-83.871,35.515,-83.812,35.54,-83.723,35.56,-83.511,35.584,-83.284,35.682,-83.12,35.775,-82.99,35.8,-82.949,35.835,-82.92,35.879,-82.902,35.939,-82.888,35.952,-82.813,35.947,-82.756,36,-82.7,36.03,-82.645,36.047,-82.611,36.047,-82.598,36.032,-82.589,35.987,-82.577,35.972,-82.553,35.971,-82.521,35.981,-82.478,36.01,-82.368,36.1,-82.295,36.134,-82.182,36.154,-82.101,36.107,-82.051,36.123,-81.908,36.306,-81.851,36.34,-81.808,36.353,-81.734,36.355,-81.733,36.406,-81.708,36.482,-81.704,36.534,-81.659,36.611]},{"z":2,"b":[-75.192,39.983,-74.699,41.357],"l":[-74.699,41.357,-74.778,41.325,-74.812,41.302,-74.841,41.269,-74.912,41.155,-75.032,41.052,-75.123,40.999,-75.123,40.972,-75.075,40.884,-75.076,40.856,-75.112,40.802,-75.175,40.776,-75.186,40.744,-75.192,40.69,-75.189,40.596,-75.171,40.577,-75.097,40.543,-75.034,40.42,-74.976,40.406,-74.735,40.155,-74.909,40.08,-75.024,40.017,-75.074,39.983]},{"z":2,"b":[-72.553,42.73,-71.51,45.008],"l":[-71.518,45.008,-71.533,44.988,-71.51,44.908,-71.621,44.772,-71.618,44.728,-71.568,44.608,-71.571,44.579,-71.588,44.565,-71.609,44.514,-71.638,44.482,-71.683,44.45,-71.826,44.374,-71.868,44.355,-72.002,44.329,-72.031,44.301,-72.062,44.116,-72.106,44.013,-72.115,43.965,-72.174,43.884,-72.222,43.791,-72.297,43.715,-72.338,43.622,-72.362,43.587,-72.385,43.529,-72.407,43.332,-72.438,43.225,-72.474,43.039,-72.496,42.992,-72.519,42.967,-72.55,42.887,-72.553,42.856,-72.54,42.83,-72.484,42.766,-72.467,42.73]},{"z":2,"b":[-82.6,38.42,-82.589,38.432],"l":[-82.589,38.42,-82.6,38.432]},{"z":2,"b":[-82.613,38.42,-82.589,38.448],"l":[-82.613,38.448,-82.589,38.42]},{"z":2,"b":[-82.62,37.54,-81.965,38.42],"l":[-81.965,37.54,-82.103,37.571,-82.28,37.687,-82.394,37.828,-82.43,37.894,-82.472,37.938,-82.482,37.984,-82.589,38.1,-82.618,38.15,-82.62,38.182,-82.585,38.243,-82.578,38.272,-82.589,38.42]},{"z":2,"b":[-83.668,36.605,-81.965,37.54],"l":[-81.965,37.54,-82.343,37.297,-82.517,37.209,-82.682,37.138,-82.695,37.126,-82.732,37.048,-82.832,36.979,-82.895,36.906,-83.044,36.846,-83.15,36.762,-83.417,36.681,-83.566,36.653,-83.668,36.605]},{"z":2,"b":[-89.838,30.193,-89.521,31.001],"l":[-89.735,31.001,-89.765,30.886,-89.835,30.731,-89.838,30.69,-89.82,30.64,-89.82,30.611,-89.794,30.569,-89.737,30.527,-89.686,30.463,-89.641,30.349,-89.629,30.296,-89.592,30.223,-89.572,30.209,-89.545,30.201,-89.521,30.193]},{"z":2,"b":[-97.226,45.943,-96.556,48.994],"l":[-96.556,45.943,-96.57,46.019,-96.561,46.138,-96.592,46.228,-96.599,46.272,-96.617,46.327,-96.731,46.473,-96.758,46.59,-96.787,46.648,-96.796,46.797,-96.793,46.839,-96.77,46.92,-96.818,46.974,-96.825,47.019,-96.848,47.373,-96.863,47.424,-96.859,47.588,-96.946,47.762,-96.988,47.82,-97.009,47.892,-97.007,47.926,-97.079,48.043,-97.126,48.173,-97.122,48.286,-97.137,48.371,-97.131,48.437,-97.144,48.537,-97.129,48.593,-97.128,48.681,-97.226,48.994]},{"z":2,"b":[-94.484,33.554,-94.047,33.648],"l":[-94.047,33.554,-94.099,33.577,-94.192,33.589,-94.238,33.581,-94.295,33.587,-94.333,33.565,-94.358,33.561,-94.378,33.566,-94.39,33.586,-94.432,33.6,-94.484,33.648]},{"z":2,"b":[-75.785,39.722,-75.421,39.843],"l":[-75.421,39.815,-75.51,39.843,-75.635,39.839,-75.677,39.827,-75.709,39.803,-75.785,39.722]},{"z":2,"b":[-81.087,35.066,-81.045,35.156],"l":[-81.087,35.156,-81.057,35.146,-81.047,35.131,-81.051,35.089,-81.045,35.066]},{"z":2,"b":[-82.778,35.084,-82.356,35.211],"l":[-82.778,35.087,-82.744,35.084,-82.651,35.128,-82.464,35.178,-82.393,35.211,-82.356,35.202]},{"z":2,"b":[-120.001,48.993,-114.051,60.001],"l":[-120,60.001,-120,59.616,-120,59.231,-120,58.847,-120,58.462,-120,58.077,-120,57.692,-120,57.307,-120.001,56.922,-120.001,56.538,-120.001,56.153,-120.001,55.768,-120.001,55.383,-120.001,54.998,-120.001,54.613,-120.001,54.229,-120.001,53.844,-119.973,53.797,-119.917,53.77,-119.904,53.723,-119.885,53.708,-119.812,53.699,-119.735,53.634,-119.743,53.615,-119.887,53.615,-119.918,53.612,-119.921,53.602,-119.896,53.567,-119.894,53.516,-119.855,53.509,-119.82,53.491,-119.723,53.4,-119.68,53.371,-119.648,53.363,-119.592,53.372,-119.435,53.362,-119.4,53.353,-119.309,53.242,-119.261,53.209,-119.046,53.144,-119.022,53.139,-119.016,53.204,-118.993,53.236,-118.914,53.212,-118.804,53.153,-118.774,53.122,-118.766,53.065,-118.731,53.058,-118.678,53.026,-118.668,52.972,-118.631,52.909,-118.605,52.895,-118.502,52.888,-118.477,52.879,-118.428,52.836,-118.413,52.784,-118.358,52.742,-118.33,52.709,-118.319,52.674,-118.35,52.623,-118.288,52.562,-118.266,52.518,-118.225,52.483,-118.247,52.441,-118.243,52.409,-118.221,52.386,-118.191,52.384,-118.06,52.413,-118.044,52.427,-118.029,52.466,-118.002,52.486,-117.883,52.43,-117.758,52.395,-117.732,52.383,-117.735,52.353,-117.822,52.272,-117.788,52.233,-117.751,52.208,-117.587,52.142,-117.385,52.144,-117.348,52.159,-117.32,52.156,-117.245,52.047,-117.077,51.926,-117.037,51.886,-116.939,51.736,-116.913,51.714,-116.83,51.715,-116.815,51.722,-116.808,51.753,-116.791,51.771,-116.758,51.789,-116.718,51.8,-116.674,51.791,-116.634,51.758,-116.567,51.651,-116.502,51.615,-116.458,51.574,-116.411,51.544,-116.377,51.497,-116.31,51.448,-116.279,51.348,-116.25,51.323,-116.176,51.297,-116.135,51.266,-116.054,51.237,-116.042,51.23,-116.035,51.218,-116.014,51.151,-115.998,51.13,-115.94,51.102,-115.794,51.07,-115.619,50.962,-115.596,50.936,-115.588,50.909,-115.64,50.869,-115.634,50.855,-115.433,50.756,-115.399,50.731,-115.386,50.726,-115.376,50.727,-115.36,50.729,-115.347,50.729,-115.335,50.72,-115.311,50.697,-115.298,50.673,-115.308,50.656,-115.311,50.642,-115.305,50.635,-115.267,50.594,-115.222,50.563,-115.206,50.554,-115.189,50.557,-115.128,50.585,-115.091,50.585,-115.048,50.577,-114.993,50.545,-114.789,50.359,-114.716,50.129,-114.668,50.061,-114.652,50.02,-114.651,49.985,-114.678,49.955,-114.685,49.934,-114.638,49.797,-114.643,49.73,-114.686,49.643,-114.737,49.604,-114.734,49.587,-114.717,49.569,-114.681,49.556,-114.602,49.551,-114.577,49.544,-114.576,49.519,-114.59,49.452,-114.588,49.42,-114.565,49.394,-114.48,49.333,-114.426,49.274,-114.386,49.252,-114.378,49.232,-114.355,49.211,-114.19,49.168,-114.164,49.147,-114.133,49.094,-114.057,49.038,-114.051,49.015,-114.063,48.993]},{"z":2,"b":[-136.445,60.001,-123.819,68.895],"l":[-123.819,60.001,-123.842,60.02,-123.879,60.04,-123.994,60.04,-124.011,60.046,-124.013,60.06,-123.992,60.099,-123.997,60.127,-124.103,60.225,-124.185,60.321,-124.205,60.353,-124.225,60.453,-124.249,60.473,-124.403,60.494,-124.469,60.567,-124.599,60.662,-124.611,60.682,-124.611,60.701,-124.505,60.788,-124.506,60.822,-124.583,60.953,-124.616,60.96,-124.807,60.961,-124.837,60.948,-124.86,60.924,-124.88,60.86,-125.152,60.844,-125.23,60.826,-125.322,60.792,-125.379,60.792,-125.707,60.84,-125.864,60.89,-125.899,60.89,-125.931,60.883,-125.957,60.865,-126.003,60.809,-126.064,60.816,-126.099,60.829,-126.115,60.863,-126.162,60.866,-126.223,60.856,-126.235,60.844,-126.235,60.809,-126.255,60.794,-126.297,60.785,-126.35,60.781,-126.52,60.8,-126.665,60.762,-126.767,60.775,-126.832,60.764,-126.859,60.769,-126.89,60.796,-126.888,60.833,-126.912,60.86,-126.918,60.884,-126.909,60.95,-126.944,61.052,-126.964,61.063,-127.055,61.051,-127.093,61.071,-127.096,61.081,-127.082,61.094,-127.027,61.126,-127.015,61.198,-127.021,61.237,-127.073,61.37,-127.116,61.404,-127.167,61.465,-127.229,61.494,-127.299,61.512,-127.468,61.511,-127.609,61.533,-127.656,61.552,-127.713,61.588,-127.807,61.619,-127.994,61.711,-128.019,61.78,-128.047,61.813,-128.094,61.841,-128.206,61.865,-128.232,61.884,-128.258,61.928,-128.366,61.991,-128.4,62.034,-128.561,62.117,-128.616,62.122,-128.679,62.112,-128.776,62.069,-128.814,62.064,-128.841,62.073,-128.88,62.111,-128.907,62.119,-129.014,62.132,-129.136,62.126,-129.209,62.136,-129.255,62.153,-129.269,62.166,-129.262,62.184,-129.239,62.202,-129.233,62.22,-129.244,62.236,-129.293,62.294,-129.291,62.322,-129.239,62.373,-129.249,62.389,-129.294,62.411,-129.296,62.425,-129.287,62.441,-129.2,62.47,-129.189,62.49,-129.211,62.513,-129.449,62.58,-129.49,62.599,-129.495,62.619,-129.525,62.674,-129.544,62.688,-129.62,62.712,-129.636,62.728,-129.642,62.756,-129.658,62.776,-129.719,62.832,-129.729,62.865,-129.714,62.895,-129.714,62.931,-129.617,63.037,-129.612,63.059,-129.63,63.068,-129.734,63.07,-129.827,63.092,-129.87,63.157,-129.906,63.187,-129.979,63.216,-130.06,63.261,-130.134,63.276,-130.14,63.293,-130.102,63.319,-129.948,63.376,-129.905,63.406,-129.845,63.486,-129.855,63.522,-129.888,63.555,-129.946,63.586,-129.976,63.616,-130.063,63.636,-130.082,63.65,-130.098,63.679,-130.111,63.695,-130.142,63.698,-130.244,63.678,-130.285,63.686,-130.303,63.704,-130.298,63.721,-130.26,63.737,-130.145,63.762,-130.121,63.773,-130.118,63.793,-130.14,63.808,-130.283,63.823,-130.364,63.85,-130.513,63.92,-130.661,63.951,-130.758,63.993,-130.792,64.031,-130.847,64.049,-130.872,64.081,-130.93,64.125,-130.941,64.141,-130.913,64.168,-130.913,64.193,-130.931,64.219,-130.982,64.259,-131.012,64.273,-131.026,64.298,-131.022,64.324,-131.107,64.401,-131.265,64.447,-131.359,64.458,-131.399,64.451,-131.45,64.416,-131.513,64.393,-131.586,64.38,-131.764,64.381,-131.797,64.388,-131.803,64.407,-131.79,64.426,-131.73,64.489,-131.716,64.515,-131.729,64.534,-131.752,64.545,-131.893,64.577,-132.052,64.684,-132.106,64.702,-132.321,64.763,-132.402,64.777,-132.554,64.78,-132.588,64.794,-132.6,64.809,-132.601,64.826,-132.584,64.842,-132.505,64.884,-132.497,64.913,-132.497,64.957,-132.481,64.97,-132.381,65.022,-132.354,65.039,-132.353,65.057,-132.381,65.075,-132.516,65.094,-132.538,65.109,-132.54,65.17,-132.579,65.185,-132.61,65.181,-132.708,65.167,-132.752,65.173,-132.768,65.185,-132.773,65.203,-132.762,65.225,-132.597,65.28,-132.566,65.3,-132.542,65.341,-132.346,65.443,-132.268,65.538,-132.195,65.598,-132.196,65.628,-132.295,65.716,-132.378,65.767,-132.489,65.82,-132.533,65.834,-132.555,65.857,-132.539,65.886,-132.341,65.955,-132.343,65.979,-132.371,65.994,-132.483,65.989,-132.577,66.024,-132.614,66.029,-132.714,65.998,-132.766,65.971,-132.821,65.928,-132.903,65.914,-132.948,65.917,-132.968,65.934,-132.94,65.976,-132.916,66.002,-132.918,66.018,-132.938,66.029,-133.041,66.028,-133.229,66.006,-133.412,65.956,-133.5,65.955,-133.589,65.963,-133.622,65.975,-133.624,66.01,-133.645,66.049,-133.676,66.074,-133.677,66.096,-133.66,66.112,-133.594,66.15,-133.569,66.177,-133.567,66.208,-133.571,66.249,-133.583,66.285,-133.612,66.295,-133.651,66.3,-133.76,66.304,-133.79,66.304,-133.8,66.326,-133.79,66.348,-133.76,66.416,-133.739,66.44,-133.706,66.444,-133.674,66.446,-133.654,66.453,-133.652,66.472,-133.658,66.509,-133.649,66.536,-133.635,66.566,-133.652,66.586,-133.726,66.631,-133.764,66.651,-133.769,66.669,-133.771,66.684,-133.799,66.711,-133.806,66.733,-133.798,66.752,-133.783,66.785,-133.794,66.819,-133.848,66.862,-133.924,66.903,-134.031,66.944,-134.053,66.973,-134.056,67.004,-134.118,67.004,-134.405,67.004,-134.692,67.005,-134.979,67.005,-135.265,67.005,-135.552,67.005,-135.839,67.005,-136.126,67.005,-136.169,67.015,-136.195,67.036,-136.223,67.088,-136.224,67.135,-136.221,67.163,-136.218,67.181,-136.15,67.226,-136.108,67.288,-136.105,67.304,-136.129,67.354,-136.141,67.375,-136.159,67.394,-136.178,67.423,-136.178,67.45,-136.169,67.514,-136.208,67.577,-136.233,67.6,-136.265,67.616,-136.401,67.666,-136.434,67.703,-136.445,67.747,-136.445,68.038,-136.445,68.329,-136.445,68.62,-136.444,68.895]},{"z":2,"b":[-69.054,47.295,-68.379,47.589],"l":[-68.379,47.589,-68.394,47.563,-68.43,47.528,-68.496,47.48,-68.628,47.421,-69.054,47.295]},{"z":2,"b":[-67.613,47.855,-66.704,48.022],"l":[-67.613,47.999,-67.609,47.969,-67.579,47.94,-67.365,47.855,-67.307,47.887,-67.187,47.894,-67.058,47.919,-66.952,47.899,-66.925,47.964,-66.843,47.998,-66.704,48.022]},{"z":2,"b":[-124.228,42.001,-120.001,42.001],"l":[-120.001,42.001,-120.264,42.001,-120.528,42.001,-120.792,42.001,-121.055,42.001,-121.319,42.001,-121.583,42.001,-121.846,42.001,-122.11,42.001,-122.374,42.001,-122.637,42.001,-122.901,42.001,-123.165,42.001,-123.428,42.001,-123.692,42.001,-123.956,42.001,-124.228,42.001]},{"z":2,"b":[-116.049,44.397,-111.051,48.993],"l":[-111.051,44.499,-111.117,44.525,-111.161,44.556,-111.198,44.591,-111.265,44.688,-111.304,44.723,-111.337,44.745,-111.373,44.746,-111.416,44.734,-111.448,44.711,-111.459,44.671,-111.491,44.624,-111.489,44.585,-111.475,44.565,-111.479,44.554,-111.5,44.545,-111.659,44.56,-111.797,44.54,-111.892,44.555,-112.074,44.548,-112.231,44.561,-112.285,44.554,-112.32,44.533,-112.364,44.48,-112.375,44.47,-112.393,44.468,-112.648,44.497,-112.716,44.498,-112.787,44.463,-112.81,44.438,-112.816,44.406,-112.831,44.397,-112.854,44.398,-112.91,44.419,-112.97,44.454,-113,44.48,-113.006,44.539,-113.054,44.605,-113.063,44.665,-113.093,44.715,-113.141,44.765,-113.187,44.799,-113.234,44.81,-113.32,44.803,-113.339,44.808,-113.432,44.874,-113.461,44.904,-113.469,44.93,-113.438,44.994,-113.447,45.029,-113.471,45.073,-113.504,45.109,-113.557,45.14,-113.607,45.209,-113.697,45.305,-113.722,45.379,-113.756,45.441,-113.766,45.503,-113.8,45.54,-113.797,45.588,-113.802,45.601,-113.813,45.607,-113.873,45.634,-113.943,45.682,-113.978,45.692,-113.992,45.693,-114.001,45.681,-114.006,45.658,-114.117,45.579,-114.21,45.542,-114.256,45.506,-114.298,45.488,-114.319,45.486,-114.351,45.497,-114.448,45.551,-114.529,45.576,-114.537,45.587,-114.533,45.633,-114.496,45.686,-114.534,45.782,-114.523,45.815,-114.493,45.839,-114.405,45.874,-114.397,45.891,-114.416,45.982,-114.465,46.016,-114.478,46.036,-114.464,46.11,-114.485,46.158,-114.447,46.199,-114.444,46.271,-114.413,46.344,-114.38,46.453,-114.387,46.5,-114.346,46.551,-114.332,46.601,-114.33,46.638,-114.337,46.659,-114.351,46.668,-114.381,46.668,-114.519,46.648,-114.563,46.65,-114.602,46.66,-114.624,46.678,-114.631,46.719,-114.662,46.746,-114.678,46.75,-114.74,46.734,-114.754,46.735,-114.761,46.742,-114.786,46.777,-114.887,46.845,-114.936,46.923,-115.034,46.991,-115.127,47.085,-115.307,47.231,-115.349,47.261,-115.486,47.312,-115.593,47.385,-115.702,47.438,-115.713,47.447,-115.714,47.452,-115.708,47.457,-115.677,47.471,-115.67,47.498,-115.711,47.553,-115.691,47.607,-115.718,47.681,-115.734,47.706,-115.811,47.776,-115.862,47.842,-115.941,47.904,-116.049,48.005,-116.049,48.252,-116.049,48.499,-116.048,48.747,-116.048,48.993]},{"z":2,"b":[-111.051,42.001,-111.05,44.499],"l":[-111.051,44.499,-111.051,44.343,-111.051,44.187,-111.051,44.031,-111.051,43.874,-111.051,43.718,-111.051,43.562,-111.051,43.406,-111.051,43.25,-111.051,43.094,-111.051,42.938,-111.051,42.782,-111.051,42.625,-111.051,42.469,-111.05,42.313,-111.05,42.157,-111.05,42.001]},{"z":2,"b":[-111.051,44.499,-104.037,45.001],"l":[-111.051,44.499,-111.051,44.624,-111.051,44.75,-111.051,44.876,-111.051,45.001,-110.832,45.001,-110.613,45.001,-110.393,45.001,-110.174,45.001,-109.955,45.001,-109.736,45.001,-109.517,45.001,-109.297,45.001,-109.078,45.001,-108.859,45.001,-108.64,45.001,-108.421,45.001,-108.202,45.001,-107.982,45.001,-107.763,45.001,-107.544,45.001,-107.325,45.001,-107.106,45.001,-106.887,45.001,-106.667,45.001,-106.448,45.001,-106.229,45.001,-106.01,45.001,-105.791,45.001,-105.571,45.001,-105.352,45.001,-105.133,45.001,-104.914,45.001,-104.695,45.001,-104.476,45.001,-104.256,45.001,-104.037,45.001]},{"z":2,"b":[-120.001,42.001,-117.017,42.001],"l":[-117.017,42.001,-117.39,42.001,-117.763,42.001,-118.136,42.001,-118.509,42.001,-118.882,42.001,-119.255,42.001,-119.628,42.001,-120.001,42.001]},{"z":2,"b":[-117.017,42.001,-114.043,42.001],"l":[-117.017,42.001,-116.645,42.001,-116.273,42.001,-115.901,42.001,-115.53,42.001,-115.158,42.001,-114.786,42.001,-114.414,42.001,-114.043,42.001]},{"z":2,"b":[-114.043,42.001,-111.05,42.001],"l":[-111.05,42.001,-111.237,42.001,-111.424,42.001,-111.611,42.001,-111.798,42.001,-111.985,42.001,-112.172,42.001,-112.359,42.001,-112.546,42.001,-112.733,42.001,-112.92,42.001,-113.107,42.001,-113.295,42.001,-113.482,42.001,-113.669,42.001,-113.856,42.001,-114.043,42.001]},{"z":2,"b":[-111.05,41.001,-109.046,42.001],"l":[-111.05,42.001,-111.05,41.876,-111.05,41.751,-111.05,41.626,-111.05,41.501,-111.05,41.376,-111.05,41.251,-111.05,41.126,-111.05,41.001,-110.8,41.001,-110.549,41.001,-110.299,41.001,-110.048,41.001,-109.798,41.001,-109.547,41.001,-109.297,41.001,-109.046,41.001]},{"z":2,"b":[-114.043,37.004,-114.04,42.001],"l":[-114.043,42.001,-114.043,41.845,-114.042,41.689,-114.042,41.533,-114.042,41.376,-114.042,41.22,-114.042,41.064,-114.042,40.908,-114.042,40.752,-114.042,40.596,-114.042,40.44,-114.042,40.283,-114.042,40.127,-114.042,39.971,-114.042,39.815,-114.042,39.659,-114.041,39.503,-114.041,39.346,-114.041,39.19,-114.041,39.034,-114.041,38.878,-114.041,38.722,-114.041,38.566,-114.041,38.41,-114.041,38.253,-114.041,38.097,-114.041,37.941,-114.041,37.785,-114.041,37.629,-114.04,37.473,-114.04,37.316,-114.04,37.16,-114.04,37.004]},{"z":2,"b":[-114.04,37.001,-109.047,37.004],"l":[-114.04,37.004,-113.728,37.004,-113.416,37.004,-113.104,37.004,-112.792,37.003,-112.48,37.003,-112.168,37.003,-111.856,37.003,-111.543,37.002,-111.231,37.002,-110.919,37.002,-110.607,37.002,-110.295,37.002,-109.983,37.001,-109.671,37.001,-109.359,37.001,-109.047,37.001]},{"z":2,"b":[-109.048,31.328,-109.047,37.001],"l":[-109.047,37.001,-109.047,36.646,-109.047,36.292,-109.047,35.937,-109.047,35.583,-109.047,35.228,-109.047,34.874,-109.047,34.519,-109.047,34.165,-109.047,33.81,-109.047,33.456,-109.047,33.101,-109.047,32.747,-109.047,32.392,-109.047,32.038,-109.047,31.683,-109.048,31.328]},{"z":2,"b":[-109.046,41.001,-104.022,41.001],"l":[-109.046,41.001,-108.732,41.001,-108.418,41.001,-108.104,41.001,-107.79,41.001,-107.476,41.001,-107.162,41.001,-106.848,41.001,-106.534,41.001,-106.22,41.001,-105.906,41.001,-105.592,41.001,-105.278,41.001,-104.964,41.001,-104.65,41.001,-104.336,41.001,-104.022,41.001]},{"z":2,"b":[-109.047,37.001,-109.046,41.001],"l":[-109.046,41.001,-109.046,40.751,-109.046,40.501,-109.046,40.251,-109.046,40.001,-109.046,39.751,-109.046,39.501,-109.047,39.251,-109.047,39.001,-109.047,38.751,-109.047,38.501,-109.047,38.251,-109.047,38.001,-109.047,37.751,-109.047,37.501,-109.047,37.251,-109.047,37.001]},{"z":2,"b":[-109.047,37.001,-103.001,37.001],"l":[-103.001,37.001,-103.379,37.001,-103.757,37.001,-104.134,37.001,-104.512,37.001,-104.89,37.001,-105.268,37.001,-105.646,37.001,-106.024,37.001,-106.402,37.001,-106.779,37.001,-107.157,37.001,-107.535,37.001,-107.913,37.001,-108.291,37.001,-108.669,37.001,-109.047,37.001]},{"z":2,"b":[-104.034,45.943,-104.011,48.993],"l":[-104.011,45.943,-104.012,46.134,-104.014,46.324,-104.015,46.515,-104.017,46.706,-104.018,46.896,-104.019,47.087,-104.021,47.278,-104.022,47.469,-104.024,47.659,-104.025,47.85,-104.026,48.041,-104.028,48.231,-104.029,48.422,-104.031,48.613,-104.032,48.803,-104.034,48.993]},{"z":2,"b":[-104.037,45.001,-104.005,45.943],"l":[-104.037,45.001,-104.029,45.001,-104.021,45.001,-104.013,45.001,-104.005,45.001,-104.005,45.004,-104.005,45.007,-104.005,45.01,-104.005,45.013,-104.006,45.246,-104.008,45.478,-104.01,45.711,-104.011,45.943]},{"z":2,"b":[-104.037,43.001,-104.029,45.001],"l":[-104.029,43.001,-104.03,43.251,-104.031,43.501,-104.032,43.751,-104.033,44.001,-104.034,44.251,-104.035,44.501,-104.036,44.751,-104.037,45.001]},{"z":2,"b":[-104.011,45.943,-96.556,45.943],"l":[-96.556,45.943,-96.789,45.943,-97.022,45.943,-97.255,45.943,-97.488,45.943,-97.721,45.943,-97.954,45.943,-98.187,45.943,-98.42,45.943,-98.653,45.943,-98.886,45.943,-99.119,45.943,-99.352,45.943,-99.585,45.943,-99.818,45.943,-100.051,45.943,-100.284,45.943,-100.517,45.943,-100.75,45.943,-100.983,45.943,-101.216,45.943,-101.449,45.943,-101.682,45.943,-101.914,45.943,-102.147,45.943,-102.38,45.943,-102.613,45.943,-102.846,45.943,-103.079,45.943,-103.312,45.943,-103.545,45.943,-103.778,45.943,-104.011,45.943]},{"z":2,"b":[-104.029,41.001,-104.022,43.001],"l":[-104.022,41.001,-104.023,41.251,-104.023,41.501,-104.024,41.751,-104.025,42.001,-104.026,42.251,-104.027,42.501,-104.028,42.751,-104.029,43.001]},{"z":2,"b":[-104.022,40.001,-102.024,41.001],"l":[-102.024,40.001,-102.025,40.126,-102.025,40.251,-102.025,40.376,-102.025,40.501,-102.025,40.626,-102.025,40.751,-102.025,40.876,-102.025,41.001,-102.275,41.001,-102.524,41.001,-102.774,41.001,-103.023,41.001,-103.273,41.001,-103.522,41.001,-103.772,41.001,-104.022,41.001]},{"z":2,"b":[-102.024,37.001,-102.013,40.001],"l":[-102.013,37.001,-102.014,37.188,-102.014,37.376,-102.015,37.563,-102.016,37.751,-102.016,37.938,-102.017,38.126,-102.018,38.313,-102.019,38.501,-102.019,38.688,-102.02,38.876,-102.021,39.064,-102.022,39.251,-102.022,39.439,-102.023,39.626,-102.024,39.814,-102.024,40.001]},{"z":2,"b":[-102.024,40.001,-95.347,40.001],"l":[-95.347,40.001,-95.527,40.001,-95.737,40.001,-95.946,40.001,-96.156,40.001,-96.365,40.001,-96.575,40.001,-96.784,40.001,-96.994,40.001,-97.204,40.001,-97.413,40.001,-97.623,40.001,-97.832,40.001,-98.042,40.001,-98.252,40.001,-98.461,40.001,-98.671,40.001,-98.88,40.001,-99.09,40.001,-99.3,40.001,-99.509,40.001,-99.719,40.001,-99.928,40.001,-100.138,40.001,-100.348,40.001,-100.557,40.001,-100.767,40.001,-100.976,40.001,-101.186,40.001,-101.396,40.001,-101.605,40.001,-101.815,40.001,-102.024,40.001]},{"z":2,"b":[-103.001,36.501,-103,37.001],"l":[-103,36.501,-103,36.626,-103.001,36.751,-103.001,36.876,-103.001,37.001]},{"z":2,"b":[-103.001,37.001,-102.013,37.001],"l":[-102.013,37.001,-102.26,37.001,-102.507,37.001,-102.754,37.001,-103.001,37.001]},{"z":2,"b":[-103,33.648,-94.484,36.501],"l":[-94.484,33.648,-94.709,33.699,-94.766,33.732,-94.869,33.768,-94.962,33.843,-95.146,33.937,-95.216,33.959,-95.238,33.956,-95.276,33.911,-95.374,33.874,-95.427,33.873,-95.523,33.887,-95.542,33.898,-95.556,33.926,-95.594,33.945,-95.623,33.93,-95.763,33.891,-95.784,33.865,-95.863,33.863,-95.942,33.883,-96.139,33.822,-96.207,33.774,-96.279,33.758,-96.315,33.711,-96.337,33.718,-96.411,33.775,-96.487,33.792,-96.592,33.849,-96.595,33.899,-96.606,33.906,-96.625,33.911,-96.649,33.906,-96.694,33.864,-96.72,33.853,-96.746,33.854,-96.848,33.883,-96.89,33.935,-96.911,33.95,-96.934,33.953,-96.978,33.938,-97.002,33.882,-97.022,33.867,-97.064,33.857,-97.061,33.829,-97.099,33.75,-97.119,33.735,-97.145,33.738,-97.172,33.757,-97.192,33.785,-97.201,33.808,-97.186,33.87,-97.195,33.891,-97.215,33.897,-97.265,33.868,-97.308,33.869,-97.364,33.84,-97.398,33.831,-97.43,33.839,-97.482,33.886,-97.571,33.916,-97.606,33.972,-97.622,33.988,-97.641,33.992,-97.66,33.99,-97.692,33.972,-97.795,33.899,-97.842,33.876,-97.883,33.871,-97.922,33.878,-97.946,33.897,-97.951,33.971,-97.974,33.994,-98.069,34.04,-98.09,34.084,-98.099,34.145,-98.214,34.135,-98.356,34.142,-98.42,34.088,-98.454,34.078,-98.494,34.089,-98.608,34.161,-98.73,34.147,-98.799,34.155,-98.969,34.209,-99.102,34.213,-99.158,34.223,-99.187,34.235,-99.2,34.318,-99.253,34.388,-99.27,34.407,-99.36,34.456,-99.375,34.458,-99.385,34.447,-99.405,34.392,-99.432,34.389,-99.536,34.405,-99.632,34.388,-99.686,34.416,-99.867,34.543,-99.941,34.576,-99.999,34.587,-100,34.706,-100,34.826,-100,34.946,-100,35.065,-100,35.185,-100.001,35.305,-100.001,35.424,-100.001,35.544,-100.001,35.663,-100.001,35.783,-100.001,35.903,-100.002,36.022,-100.002,36.142,-100.002,36.262,-100.002,36.381,-100.002,36.501,-100.19,36.501,-100.377,36.501,-100.565,36.501,-100.752,36.501,-100.939,36.501,-101.127,36.501,-101.314,36.501,-101.501,36.501,-101.689,36.501,-101.876,36.501,-102.063,36.501,-102.251,36.501,-102.438,36.501,-102.626,36.501,-102.813,36.501,-103,36.501]},{"z":2,"b":[-102.013,37.001,-94.618,37.001],"l":[-94.618,37.001,-94.849,37.001,-95.08,37.001,-95.311,37.001,-95.542,37.001,-95.774,37.001,-96.005,37.001,-96.236,37.001,-96.467,37.001,-96.698,37.001,-96.929,37.001,-97.16,37.001,-97.391,37.001,-97.622,37.001,-97.853,37.001,-98.084,37.001,-98.315,37.001,-98.547,37.001,-98.778,37.001,-99.009,37.001,-99.24,37.001,-99.471,37.001,-99.702,37.001,-99.933,37.001,-100.164,37.001,-100.395,37.001,-100.626,37.001,-100.857,37.001,-101.088,37.001,-101.32,37.001,-101.551,37.001,-101.782,37.001,-102.013,37.001]},{"z":2,"b":[-96.627,42.511,-96.454,43.501],"l":[-96.454,43.501,-96.599,43.498,-96.581,43.435,-96.542,43.373,-96.535,43.338,-96.543,43.309,-96.57,43.283,-96.563,43.243,-96.555,43.232,-96.49,43.206,-96.468,43.17,-96.461,43.13,-96.463,43.099,-96.502,43.043,-96.513,42.969,-96.563,42.859,-96.616,42.791,-96.627,42.757,-96.622,42.73,-96.539,42.659,-96.534,42.63,-96.506,42.594,-96.481,42.511]},{"z":2,"b":[-94.625,37.001,-94.618,39.086],"l":[-94.625,39.086,-94.624,38.956,-94.624,38.825,-94.623,38.695,-94.623,38.565,-94.623,38.434,-94.622,38.304,-94.622,38.174,-94.621,38.043,-94.621,37.913,-94.62,37.783,-94.62,37.652,-94.62,37.522,-94.619,37.392,-94.619,37.261,-94.618,37.131,-94.618,37.001]},{"z":2,"b":[-94.625,39.086,-94.622,39.124],"l":[-94.622,39.124,-94.625,39.086]},{"z":2,"b":[-94.618,36.501,-94.618,37.001],"l":[-94.618,37.001,-94.618,36.876,-94.618,36.751,-94.618,36.626,-94.618,36.501]},{"z":2,"b":[-95.765,40.379,-91.442,40.625],"l":[-95.765,40.602,-95.515,40.603,-95.263,40.604,-95.011,40.606,-94.759,40.607,-94.506,40.609,-94.254,40.61,-94.002,40.612,-93.75,40.613,-93.497,40.615,-93.245,40.616,-92.993,40.617,-92.741,40.619,-92.488,40.62,-92.236,40.622,-91.984,40.623,-91.732,40.625,-91.682,40.552,-91.627,40.531,-91.617,40.507,-91.545,40.469,-91.53,40.432,-91.442,40.379]},{"z":2,"b":[-94.047,33.012,-94.041,33.554],"l":[-94.041,33.012,-94.043,33.148,-94.044,33.283,-94.046,33.419,-94.047,33.554]},{"z":2,"b":[-94.618,33.648,-94.439,36.501],"l":[-94.618,36.501,-94.596,36.362,-94.574,36.222,-94.551,36.083,-94.529,35.944,-94.506,35.805,-94.484,35.666,-94.462,35.526,-94.439,35.387,-94.442,35.279,-94.445,35.17,-94.448,35.061,-94.45,34.953,-94.453,34.844,-94.456,34.735,-94.459,34.627,-94.462,34.518,-94.464,34.409,-94.467,34.3,-94.47,34.192,-94.473,34.083,-94.476,33.974,-94.479,33.866,-94.481,33.757,-94.484,33.648]},{"z":2,"b":[-94.618,35.993,-89.705,36.501],"l":[-94.618,36.501,-94.34,36.501,-94.061,36.501,-93.783,36.501,-93.504,36.501,-93.226,36.501,-92.947,36.501,-92.669,36.501,-92.39,36.501,-92.112,36.501,-91.833,36.501,-91.555,36.501,-91.276,36.501,-90.998,36.501,-90.719,36.501,-90.441,36.501,-90.162,36.501,-90.118,36.423,-90.074,36.372,-90.067,36.334,-90.076,36.297,-90.149,36.216,-90.216,36.178,-90.253,36.138,-90.303,36.099,-90.381,35.993,-90.217,35.995,-90.052,35.997,-89.888,35.999,-89.705,36.002]},{"z":2,"b":[-80.521,40.647,-80.519,42.324],"l":[-80.519,40.647,-80.52,40.753,-80.52,40.858,-80.52,40.962,-80.52,41.067,-80.52,41.172,-80.52,41.277,-80.52,41.382,-80.52,41.487,-80.52,41.591,-80.52,41.696,-80.52,41.801,-80.52,41.906,-80.52,42.011,-80.52,42.116,-80.52,42.22,-80.521,42.324]},{"z":2,"b":[-80.52,39.723,-79.477,40.647],"l":[-79.477,39.723,-79.608,39.723,-79.738,39.723,-79.868,39.723,-79.998,39.723,-80.129,39.723,-80.259,39.723,-80.389,39.723,-80.519,39.723,-80.52,39.838,-80.52,39.954,-80.52,40.069,-80.52,40.185,-80.52,40.301,-80.52,40.416,-80.52,40.532,-80.519,40.647]},{"z":2,"b":[-94.043,29.977,-93.531,33.012],"l":[-93.794,29.977,-93.727,30.077,-93.711,30.113,-93.721,30.283,-93.75,30.345,-93.747,30.38,-93.715,30.473,-93.72,30.558,-93.665,30.661,-93.625,30.714,-93.579,30.824,-93.569,30.895,-93.549,30.948,-93.563,31.005,-93.531,31.046,-93.551,31.104,-93.558,31.18,-93.597,31.21,-93.666,31.323,-93.664,31.372,-93.736,31.478,-93.738,31.514,-93.807,31.569,-93.821,31.604,-93.827,31.75,-93.846,31.793,-93.905,31.877,-94.043,31.999,-94.043,32.126,-94.043,32.252,-94.043,32.379,-94.042,32.506,-94.042,32.632,-94.042,32.759,-94.042,32.885,-94.041,33.012]},{"z":2,"b":[-94.041,33.012,-91.15,33.016],"l":[-94.041,33.012,-93.862,33.012,-93.682,33.012,-93.502,33.013,-93.323,33.013,-93.143,33.013,-92.963,33.013,-92.784,33.013,-92.604,33.014,-92.424,33.014,-92.245,33.014,-92.065,33.014,-91.885,33.015,-91.706,33.015,-91.526,33.015,-91.346,33.015,-91.15,33.016]},{"z":2,"b":[-90.294,34.999,-88.173,35.001],"l":[-90.294,35.001,-90.039,35.001,-89.773,35.001,-89.507,35,-89.242,35,-88.976,35,-88.71,34.999,-88.445,34.999,-88.173,34.999]},{"z":2,"b":[-87.607,30.378,-85.007,31.002],"l":[-85.007,31.002,-85.33,31.001,-85.653,31.001,-85.977,31.001,-86.3,31.001,-86.624,31.001,-86.947,31.001,-87.27,31.001,-87.594,31.001,-87.607,30.929,-87.601,30.861,-87.566,30.796,-87.422,30.672,-87.408,30.641,-87.434,30.546,-87.429,30.478,-87.445,30.443,-87.48,30.412,-87.49,30.378]},{"z":2,"b":[-83.097,35.001,-83.096,35.002],"l":[-83.097,35.002,-83.096,35.001]},{"z":2,"b":[-85.624,34.988,-84.325,35.001],"l":[-84.325,34.988,-84.649,34.991,-84.974,34.994,-85.299,34.998,-85.624,35.001]},{"z":2,"b":[-84.325,34.988,-83.115,35.001],"l":[-83.115,35.001,-83.417,34.998,-83.72,34.995,-84.022,34.991,-84.325,34.988]},{"z":2,"b":[-83.115,35.001,-82.778,35.087],"l":[-82.778,35.087,-82.937,35.044,-83.097,35.002,-83.115,35.001]},{"z":2,"b":[-81.045,33.877,-78.564,35.114],"l":[-81.045,35.066,-80.934,35.114,-80.882,35.058,-80.793,34.963,-80.789,34.907,-80.784,34.829,-80.643,34.825,-80.501,34.821,-80.36,34.817,-80.219,34.813,-80.078,34.809,-79.936,34.805,-79.795,34.801,-79.654,34.797,-79.519,34.683,-79.383,34.569,-79.248,34.454,-79.113,34.34,-78.978,34.226,-78.843,34.112,-78.708,33.998,-78.564,33.877]},{"z":2,"b":[-82.356,35.156,-81.087,35.202],"l":[-82.356,35.202,-82.197,35.196,-82.039,35.19,-81.88,35.184,-81.722,35.179,-81.563,35.173,-81.405,35.167,-81.246,35.161,-81.087,35.156]},{"z":2,"b":[-71.801,41.707,-71.234,42.017],"l":[-71.801,42.012,-71.726,42.013,-71.623,42.014,-71.524,42.015,-71.463,42.016,-71.387,42.017,-71.384,41.972,-71.379,41.902,-71.338,41.891,-71.339,41.835,-71.341,41.798,-71.305,41.775,-71.268,41.751,-71.234,41.707]},{"z":2,"b":[-73.481,42.008,-71.801,42.056],"l":[-73.481,42.056,-73.396,42.053,-73.312,42.05,-73.228,42.048,-73.144,42.045,-73.06,42.042,-72.976,42.039,-72.892,42.037,-72.808,42.034,-72.806,42.008,-72.763,42.011,-72.758,42.034,-72.638,42.033,-72.519,42.031,-72.399,42.03,-72.28,42.028,-72.16,42.027,-72.041,42.026,-71.921,42.024,-71.802,42.023,-71.801,42.012]},{"z":2,"b":[-71.842,41.336,-71.796,42.012],"l":[-71.801,42.012,-71.8,41.958,-71.8,41.883,-71.799,41.829,-71.798,41.753,-71.797,41.668,-71.797,41.589,-71.796,41.52,-71.799,41.479,-71.805,41.417,-71.83,41.393,-71.842,41.336]},{"z":2,"b":[-81.971,37.208,-77.727,39.452],"l":[-81.965,37.54,-81.958,37.508,-81.971,37.473,-81.939,37.433,-81.913,37.369,-81.876,37.33,-81.829,37.299,-81.752,37.271,-81.661,37.222,-81.589,37.208,-81.565,37.21,-81.486,37.257,-81.419,37.282,-81.35,37.334,-81.227,37.263,-81.178,37.263,-81.057,37.294,-80.944,37.302,-80.915,37.311,-80.873,37.341,-80.872,37.399,-80.862,37.415,-80.843,37.422,-80.821,37.423,-80.77,37.402,-80.72,37.401,-80.507,37.469,-80.491,37.464,-80.465,37.433,-80.371,37.473,-80.316,37.51,-80.292,37.566,-80.246,37.621,-80.255,37.645,-80.285,37.677,-80.246,37.751,-80.188,37.824,-79.972,38.046,-79.903,38.181,-79.813,38.267,-79.783,38.336,-79.715,38.404,-79.669,38.544,-79.64,38.572,-79.598,38.577,-79.555,38.559,-79.525,38.533,-79.505,38.491,-79.396,38.454,-79.304,38.45,-79.24,38.477,-79.203,38.526,-79.153,38.618,-79.104,38.673,-79.073,38.742,-79.041,38.79,-78.992,38.82,-78.869,38.818,-78.805,38.86,-78.757,38.908,-78.727,38.924,-78.693,38.922,-78.581,38.999,-78.435,39.161,-78.404,39.263,-78.366,39.336,-78.351,39.411,-78.322,39.452,-78.193,39.37,-78.118,39.323,-77.949,39.216,-77.836,39.145,-77.792,39.227,-77.727,39.346]},{"z":2,"b":[-79.477,39.722,-75.785,39.723],"l":[-79.477,39.723,-79.247,39.723,-79.016,39.723,-78.785,39.723,-78.554,39.723,-78.324,39.723,-78.093,39.723,-77.862,39.723,-77.631,39.723,-77.4,39.723,-77.169,39.723,-76.939,39.723,-76.708,39.722,-76.477,39.722,-76.246,39.722,-76.016,39.722,-75.785,39.722]},{"z":2,"b":[-79.488,39.211,-79.477,39.723],"l":[-79.477,39.723,-79.48,39.595,-79.483,39.467,-79.485,39.339,-79.488,39.211]},{"z":2,"b":[-75.785,38.455,-75.038,39.722],"l":[-75.785,39.722,-75.775,39.564,-75.765,39.406,-75.756,39.247,-75.746,39.089,-75.736,38.93,-75.726,38.772,-75.717,38.613,-75.707,38.455,-75.705,38.455,-75.702,38.455,-75.7,38.455,-75.698,38.455,-75.696,38.455,-75.693,38.455,-75.691,38.455,-75.689,38.455,-75.588,38.455,-75.366,38.455,-75.145,38.455,-75.038,38.456]},{"z":2,"b":[-75.659,37.954,-75.224,38.041],"l":[-75.659,37.954,-75.62,37.999,-75.469,38.015,-75.376,38.025,-75.252,38.037,-75.224,38.041]},{"z":2,"b":[-77.122,38.807,-76.931,39.012],"l":[-77.02,38.807,-76.931,38.888,-76.989,38.952,-77.042,39.012,-77.122,38.944]},{"z":2,"b":[-79.763,41.357,-74.699,42.539],"l":[-74.699,41.357,-74.72,41.395,-74.757,41.424,-74.935,41.474,-74.981,41.508,-75.023,41.553,-75.051,41.608,-75.066,41.667,-75.067,41.713,-75.054,41.765,-75.095,41.799,-75.096,41.832,-75.122,41.853,-75.239,41.892,-75.274,41.947,-75.351,41.998,-75.489,41.998,-75.627,41.999,-75.765,41.999,-75.903,41.999,-76.041,41.999,-76.179,41.999,-76.316,41.999,-76.454,41.999,-76.592,41.999,-76.73,41.999,-76.868,41.999,-77.006,41.999,-77.144,41.999,-77.281,42,-77.419,42,-77.557,42,-77.695,42,-77.833,42,-77.971,42,-78.109,42,-78.246,42,-78.384,42,-78.522,42,-78.66,42,-78.798,42,-78.936,42,-79.074,42.001,-79.212,42.001,-79.349,42.001,-79.487,42.001,-79.625,42.001,-79.763,42.001,-79.763,42.136,-79.763,42.27,-79.763,42.405,-79.762,42.539]},{"z":2,"b":[-73.507,42.056,-73.253,42.752],"l":[-73.253,42.752,-73.285,42.668,-73.317,42.584,-73.349,42.5,-73.38,42.416,-73.412,42.332,-73.444,42.248,-73.476,42.164,-73.507,42.08,-73.481,42.056]},{"z":2,"b":[-73.723,40.992,-73.481,42.056],"l":[-73.481,42.056,-73.489,41.961,-73.497,41.866,-73.505,41.771,-73.513,41.676,-73.521,41.581,-73.529,41.486,-73.537,41.391,-73.545,41.296,-73.514,41.257,-73.484,41.219,-73.522,41.201,-73.58,41.173,-73.642,41.143,-73.723,41.105,-73.682,41.055,-73.63,40.992]},{"z":2,"b":[-74.699,40.992,-73.91,41.357],"l":[-74.699,41.357,-74.6,41.312,-74.501,41.266,-74.402,41.22,-74.303,41.175,-74.204,41.129,-74.105,41.083,-74.006,41.037,-73.91,40.992]},{"z":2,"b":[-73.253,42.73,-72.467,42.752],"l":[-72.467,42.73,-72.565,42.733,-72.663,42.736,-72.762,42.739,-72.86,42.741,-72.958,42.744,-73.057,42.747,-73.155,42.749,-73.253,42.752]},{"z":2,"b":[-73.426,42.752,-73.247,45.005],"l":[-73.253,42.752,-73.28,42.813,-73.267,42.865,-73.265,42.935,-73.262,43.027,-73.259,43.128,-73.255,43.265,-73.253,43.357,-73.25,43.44,-73.247,43.553,-73.306,43.616,-73.33,43.627,-73.349,43.62,-73.391,43.579,-73.405,43.579,-73.416,43.59,-73.413,43.628,-73.376,43.762,-73.375,43.804,-73.383,43.876,-73.425,44.038,-73.426,44.075,-73.41,44.131,-73.381,44.187,-73.332,44.245,-73.321,44.269,-73.325,44.368,-73.31,44.46,-73.372,44.597,-73.349,44.775,-73.366,44.86,-73.345,44.939,-73.352,45.005]},{"z":2,"b":[-72.467,42.702,-70.806,42.882],"l":[-70.806,42.877,-70.923,42.882,-70.974,42.872,-71.076,42.825,-71.139,42.808,-71.242,42.73,-71.33,42.702,-71.472,42.706,-71.614,42.709,-71.756,42.713,-71.898,42.716,-72.04,42.72,-72.183,42.723,-72.325,42.727,-72.467,42.73]},{"z":2,"b":[-71.085,43.07,-70.733,45.294],"l":[-70.733,43.07,-70.813,43.164,-70.829,43.239,-70.92,43.328,-70.956,43.389,-70.968,43.458,-70.962,43.532,-70.97,43.643,-70.978,43.754,-70.985,43.865,-70.993,43.976,-71.001,44.086,-71.008,44.197,-71.016,44.308,-71.024,44.419,-71.031,44.53,-71.039,44.641,-71.047,44.751,-71.054,44.862,-71.062,44.973,-71.07,45.084,-71.077,45.195,-71.085,45.294]},{"z":2,"b":[-96.851,43.501,-96.454,45.943],"l":[-96.454,43.501,-96.454,43.726,-96.454,43.95,-96.454,44.175,-96.454,44.399,-96.454,44.624,-96.454,44.848,-96.454,45.073,-96.454,45.297,-96.53,45.372,-96.696,45.432,-96.738,45.468,-96.815,45.557,-96.845,45.596,-96.851,45.626,-96.814,45.659,-96.663,45.756,-96.615,45.8,-96.591,45.843,-96.556,45.943]},{"z":2,"b":[-96.454,43.501,-91.245,43.502],"l":[-91.245,43.502,-91.575,43.502,-91.901,43.502,-92.226,43.502,-92.551,43.502,-92.876,43.502,-93.201,43.502,-93.527,43.502,-93.852,43.502,-94.177,43.502,-94.502,43.501,-94.828,43.501,-95.153,43.501,-95.478,43.501,-95.803,43.501,-96.128,43.501,-96.454,43.501]},{"z":2,"b":[-84.796,41.701,-83.122,41.95],"l":[-84.796,41.701,-84.463,41.711,-84.13,41.72,-83.797,41.73,-83.464,41.739,-83.304,41.838,-83.122,41.95]},{"z":2,"b":[-89.958,47.287,-89.498,47.998],"l":[-89.958,47.287,-89.843,47.464,-89.728,47.642,-89.614,47.819,-89.498,47.998]},{"z":2,"b":[-87.221,41.761,-87.039,42.493],"l":[-87.221,41.761,-87.176,41.944,-87.13,42.127,-87.085,42.31,-87.039,42.493]},{"z":2,"b":[-90.651,42.493,-87.039,42.513],"l":[-90.651,42.513,-90.191,42.51,-89.741,42.508,-89.291,42.505,-88.84,42.503,-88.39,42.5,-87.94,42.498,-87.489,42.495,-87.039,42.493]},{"z":2,"b":[-84.822,39.092,-84.796,41.701],"l":[-84.822,39.092,-84.82,39.424,-84.817,39.749,-84.813,40.075,-84.81,40.4,-84.806,40.725,-84.803,41.051,-84.8,41.376,-84.796,41.701]},{"z":2,"b":[-87.221,41.701,-84.796,41.761],"l":[-87.221,41.761,-86.918,41.761,-86.615,41.76,-86.312,41.76,-86.009,41.76,-85.706,41.76,-85.403,41.76,-85.1,41.76,-84.796,41.76,-84.796,41.701]},{"z":2,"b":[-90.413,42.493,-86.265,47.287],"l":[-87.039,42.493,-87.079,42.782,-87.114,43.031,-87.155,43.327,-87.145,43.571,-87.142,43.658,-87.083,43.89,-87.032,44.091,-86.919,44.362,-86.822,44.591,-86.715,44.846,-86.536,45.041,-86.401,45.133,-86.265,45.227,-86.482,45.323,-86.774,45.452,-86.942,45.452,-87.115,45.452,-87.244,45.29,-87.42,45.184,-87.46,45.068,-87.654,45.122,-87.694,45.161,-87.719,45.201,-87.721,45.243,-87.663,45.345,-87.658,45.362,-87.664,45.371,-87.696,45.383,-87.769,45.364,-87.842,45.361,-87.865,45.368,-87.875,45.385,-87.874,45.413,-87.859,45.445,-87.815,45.506,-87.815,45.563,-87.795,45.596,-87.814,45.657,-87.809,45.686,-87.84,45.718,-87.923,45.76,-88.116,45.816,-88.096,45.878,-88.104,45.905,-88.152,45.945,-88.355,45.992,-88.493,46.014,-88.63,46.014,-88.727,46.031,-88.837,46.037,-88.908,46.065,-88.978,46.093,-89.049,46.121,-89.119,46.149,-89.244,46.173,-89.369,46.197,-89.494,46.221,-89.619,46.245,-89.744,46.269,-89.869,46.294,-89.994,46.318,-90.119,46.342,-90.147,46.384,-90.174,46.426,-90.202,46.468,-90.23,46.51,-90.288,46.527,-90.318,46.537,-90.341,46.558,-90.392,46.549,-90.413,46.585,-90.356,46.673,-90.299,46.761,-90.242,46.848,-90.185,46.936,-90.128,47.024,-90.071,47.112,-90.015,47.199,-89.958,47.287]},{"z":2,"b":[-91.656,31.001,-89.735,31.002],"l":[-91.656,31.002,-91.417,31.001,-91.177,31.001,-90.937,31.001,-90.696,31.001,-90.456,31.001,-90.216,31.001,-89.976,31.001,-89.735,31.001]},{"z":2,"b":[-89.557,36.501,-89.487,36.503],"l":[-89.487,36.503,-89.557,36.501]},{"z":2,"b":[-120.001,34.991,-114.611,42.001],"l":[-114.611,34.991,-114.882,35.203,-115.136,35.401,-115.39,35.599,-115.644,35.797,-115.898,35.995,-116.152,36.194,-116.406,36.392,-116.66,36.59,-116.843,36.728,-117.026,36.866,-117.21,37.003,-117.393,37.141,-117.576,37.279,-117.76,37.417,-117.943,37.555,-118.126,37.692,-118.36,37.856,-118.595,38.019,-118.829,38.183,-119.063,38.346,-119.297,38.51,-119.532,38.673,-119.766,38.837,-120,39,-120,39.188,-120,39.375,-120,39.563,-120,39.75,-120,39.938,-120,40.125,-120,40.313,-120,40.5,-120.001,40.688,-120.001,40.875,-120.001,41.063,-120.001,41.251,-120.001,41.438,-120.001,41.626,-120.001,41.813,-120.001,42.001]},{"z":2,"b":[-68.379,47.589,-67.613,47.999],"l":[-67.613,47.999,-67.739,47.999,-67.913,47.999,-68.023,47.999,-68.114,47.999,-68.114,47.929,-68.252,47.929,-68.378,47.929,-68.378,47.838,-68.379,47.765,-68.379,47.692,-68.379,47.589]},{"z":2,"b":[-139.057,60.001,-123.819,60.002],"l":[-123.819,60.001,-124.771,60.001,-125.723,60.001,-126.674,60.001,-127.626,60.001,-128.578,60.001,-129.53,60.001,-130.481,60.001,-131.433,60.001,-132.385,60.001,-133.337,60.001,-134.288,60.001,-135.24,60.001,-136.192,60.001,-137.144,60.001,-138.095,60.001,-139.057,60.002]},{"z":2,"b":[-123.819,60.001,-120,60.001],"l":[-120,60.001,-120.477,60.001,-120.955,60.001,-121.432,60.001,-121.91,60.001,-122.387,60.001,-122.864,60.001,-123.342,60.001,-123.819,60.001]},{"z":2,"b":[-64.315,45.836,-64.031,46.013],"l":[-64.031,46.013,-64.051,45.978,-64.16,45.965,-64.216,45.901,-64.273,45.85,-64.315,45.836]},{"z":2,"b":[-67.761,51.443,-57.1,60.306],"l":[-64.614,60.306,-64.816,60.267,-64.847,60.251,-64.847,60.238,-64.789,60.196,-64.645,60.161,-64.622,60.141,-64.65,60.11,-64.711,60.091,-64.782,60.064,-64.88,60.05,-64.887,60.035,-64.868,60.009,-64.834,59.986,-64.682,59.943,-64.665,59.933,-64.661,59.922,-64.666,59.906,-64.762,59.864,-64.791,59.816,-64.818,59.54,-64.776,59.488,-64.715,59.466,-64.445,59.528,-64.385,59.515,-64.379,59.475,-64.426,59.456,-64.503,59.399,-64.519,59.356,-64.519,59.226,-64.505,59.166,-64.478,59.116,-64.442,59.091,-64.332,59.044,-64.324,59.02,-64.358,59.002,-64.394,58.998,-64.77,59.049,-64.802,59.04,-64.833,59.02,-64.851,58.984,-64.848,58.949,-64.809,58.929,-64.618,58.911,-64.294,58.866,-64.236,58.786,-64.192,58.769,-64.155,58.771,-63.991,58.811,-63.727,58.862,-63.666,58.854,-63.592,58.83,-63.535,58.799,-63.504,58.771,-63.497,58.753,-63.506,58.743,-63.553,58.732,-63.961,58.684,-64.031,58.666,-64.071,58.642,-64.085,58.621,-64.086,58.599,-64.062,58.555,-64.039,58.545,-63.882,58.555,-63.841,58.505,-63.837,58.483,-63.848,58.46,-64.023,58.399,-64.098,58.359,-64.371,58.169,-64.395,58.144,-64.4,58.121,-64.387,58.099,-64.227,58.018,-64.134,57.875,-64.079,57.813,-64.045,57.785,-63.986,57.803,-63.961,57.802,-63.921,57.783,-63.872,57.731,-63.842,57.713,-63.793,57.706,-63.716,57.696,-63.618,57.726,-63.601,57.723,-63.598,57.692,-63.615,57.669,-63.744,57.581,-63.759,57.553,-63.762,57.513,-63.742,57.396,-63.8,57.319,-63.793,57.279,-63.76,57.241,-63.758,57.228,-63.792,57.13,-63.836,57.083,-63.876,57.023,-63.903,56.889,-63.95,56.863,-64.004,56.821,-64.03,56.787,-64.101,56.738,-64.116,56.707,-64.112,56.691,-63.955,56.527,-63.926,56.475,-63.955,56.439,-64.053,56.43,-64.136,56.395,-64.119,56.334,-64.121,56.299,-64.115,56.289,-64.084,56.275,-63.913,56.229,-63.899,56.212,-63.992,56.145,-64.004,56.097,-63.968,56.095,-63.886,56.088,-63.836,56.063,-63.578,56.026,-63.558,55.992,-63.586,55.976,-63.661,55.948,-63.698,55.923,-63.708,55.883,-63.701,55.861,-63.524,55.706,-63.415,55.596,-63.363,55.565,-63.305,55.542,-63.154,55.514,-63.151,55.484,-63.159,55.44,-63.151,55.404,-63.112,55.34,-63.161,55.307,-63.429,55.21,-63.511,55.168,-63.534,55.14,-63.551,55.086,-63.48,54.987,-63.476,54.962,-63.491,54.918,-63.57,54.776,-63.658,54.644,-63.699,54.615,-63.751,54.612,-63.91,54.634,-64.341,54.726,-64.815,54.734,-65.122,54.719,-65.676,54.745,-65.742,54.769,-65.797,54.805,-65.868,54.898,-65.884,54.907,-66.083,54.942,-66.157,54.968,-66.24,55.008,-66.618,55.256,-66.733,55.319,-66.774,55.328,-66.8,55.311,-66.797,55.294,-66.725,55.213,-66.719,55.193,-66.747,55.129,-66.693,54.998,-66.71,54.948,-66.657,54.847,-66.651,54.799,-66.724,54.748,-66.867,54.781,-67.167,54.937,-67.302,55.064,-67.339,55.075,-67.408,55.072,-67.426,55.059,-67.437,55.036,-67.416,54.984,-67.372,54.917,-67.29,54.826,-67.205,54.699,-67.199,54.682,-67.147,54.653,-67.15,54.617,-67.236,54.586,-67.286,54.519,-67.315,54.517,-67.537,54.573,-67.579,54.565,-67.601,54.553,-67.62,54.507,-67.61,54.452,-67.582,54.392,-67.515,54.282,-67.508,54.253,-67.511,54.236,-67.681,54.141,-67.731,54.1,-67.761,54.061,-67.757,54.034,-67.531,53.839,-67.471,53.796,-67.456,53.73,-67.477,53.635,-67.474,53.613,-67.457,53.601,-67.165,53.543,-67.128,53.503,-67.076,53.417,-67.026,53.393,-66.98,53.384,-66.944,53.355,-66.938,53.329,-66.965,53.183,-66.97,53.073,-67.028,52.98,-67.054,52.92,-67.07,52.833,-67.068,52.755,-67.029,52.72,-66.951,52.698,-66.897,52.694,-66.856,52.698,-66.815,52.687,-66.688,52.727,-66.671,52.749,-66.631,52.91,-66.615,52.932,-66.521,52.958,-66.49,53.005,-66.468,53.019,-66.428,53.011,-66.39,52.992,-66.328,52.934,-66.3,52.897,-66.31,52.861,-66.341,52.84,-66.357,52.806,-66.366,52.741,-66.417,52.688,-66.426,52.661,-66.394,52.418,-66.402,52.398,-66.463,52.341,-66.462,52.295,-66.439,52.229,-66.407,52.184,-66.379,52.165,-66.345,52.155,-66.327,52.189,-66.296,52.295,-66.28,52.301,-66.149,52.23,-66.097,52.21,-66.036,52.109,-65.979,52.081,-65.883,52.054,-65.827,52.052,-65.806,52.064,-65.781,52.104,-65.691,52.094,-65.631,52.073,-65.589,52.073,-65.552,52.094,-65.474,52.196,-65.452,52.203,-65.398,52.203,-65.268,52.185,-65.137,52.155,-65.036,52.113,-64.932,52.055,-64.765,51.905,-64.658,51.849,-64.627,51.825,-64.639,51.791,-64.693,51.763,-64.702,51.745,-64.697,51.72,-64.598,51.6,-64.506,51.609,-64.291,51.729,-64.289,51.769,-64.326,51.811,-64.335,51.842,-64.321,51.93,-64.271,52.083,-64.26,52.1,-64.16,52.148,-64.131,52.187,-64.103,52.378,-64.183,52.554,-64.187,52.626,-64.178,52.815,-64.139,52.854,-64.008,52.878,-63.973,52.895,-63.917,53.066,-63.905,53.077,-63.799,53.104,-63.723,53.109,-63.672,53.101,-63.606,53.035,-63.555,52.964,-63.549,52.94,-63.567,52.826,-63.563,52.797,-63.55,52.774,-63.41,52.689,-63.423,52.67,-63.523,52.649,-63.811,52.611,-63.899,52.589,-63.955,52.566,-64.018,52.525,-64.059,52.485,-64.066,52.466,-64.062,52.458,-63.951,52.429,-63.943,52.418,-63.935,52.381,-63.884,52.345,-63.766,52.298,-63.684,52.138,-63.668,52.094,-63.664,52.06,-63.704,52.048,-63.786,52.051,-63.792,52.044,-63.783,52.02,-63.757,52.011,-63.607,52.001,-63.2,52.001,-62.793,52.001,-62.387,52.001,-61.98,52.001,-61.574,52.001,-61.167,52.001,-60.76,52.001,-60.354,52.001,-59.947,52.001,-59.541,52.001,-59.134,52.001,-58.727,52.001,-58.321,52.001,-57.914,52.001,-57.508,52.001,-57.101,52.001,-57.101,51.711,-57.1,51.443]},{"z":2,"b":[-120.682,60.001,-101.987,78.687],"l":[-102.002,60.001,-102.002,60.242,-102.002,60.482,-102.002,60.723,-102.003,60.963,-102.003,61.204,-102.003,61.444,-102.003,61.685,-102.004,61.925,-102.004,62.166,-102.004,62.406,-102.004,62.647,-102.005,62.887,-102.005,63.128,-102.005,63.368,-102.005,63.609,-102.006,63.849,-101.987,64.08,-101.999,64.228,-102.435,64.263,-102.871,64.297,-103.307,64.332,-103.743,64.366,-104.178,64.401,-104.614,64.435,-105.05,64.469,-105.486,64.504,-105.922,64.538,-106.358,64.573,-106.794,64.607,-107.229,64.642,-107.665,64.676,-108.101,64.71,-108.537,64.745,-108.973,64.779,-109.215,64.813,-109.516,64.903,-109.805,65.051,-110.093,65.2,-110.381,65.349,-110.669,65.497,-111.128,65.498,-111.587,65.5,-112.045,65.501,-112.504,65.502,-112.759,65.58,-113.015,65.659,-113.27,65.737,-113.526,65.815,-113.781,65.893,-114.037,65.971,-114.292,66.049,-114.547,66.127,-114.803,66.206,-115.058,66.284,-115.314,66.362,-115.569,66.44,-115.825,66.518,-116.08,66.596,-116.336,66.674,-116.591,66.753,-116.846,66.831,-117.102,66.909,-117.357,66.987,-117.613,67.065,-117.868,67.143,-118.124,67.221,-118.379,67.3,-118.634,67.378,-118.89,67.456,-119.145,67.534,-119.401,67.612,-119.656,67.69,-119.912,67.768,-120.167,67.847,-120.422,67.925,-120.678,68.003,-120.678,68.209,-120.679,68.414,-120.679,68.62,-120.679,68.826,-120.68,69.031,-120.68,69.237,-120.682,69.567,-120.681,69.649,-120.203,69.649,-119.725,69.649,-119.246,69.649,-118.768,69.649,-118.29,69.649,-117.812,69.649,-117.334,69.649,-116.856,69.65,-116.65,69.65,-116.694,69.695,-116.821,69.76,-116.948,69.825,-117.075,69.889,-117.055,70.002,-116.596,70.002,-116.068,70.002,-115.539,70.002,-115.011,70.002,-114.483,70.002,-113.955,70.001,-113.427,70.001,-112.881,70.001,-112.878,69.912,-112.875,69.833,-112.762,69.834,-112.65,69.834,-112.5,69.912,-112.5,70.001,-112.188,70.001,-111.875,70.001,-111.563,70.001,-111.25,70.001,-110.938,70.001,-110.625,70.001,-110.313,70.001,-110,70.001,-110,70.544,-110,71.087,-110.001,71.63,-110.001,72.173,-110.001,72.716,-110.002,72.982,-110.001,73.259,-110.002,73.802,-110.002,74.345,-110.003,74.851,-110.002,74.888,-110.002,75.43,-110.003,75.973,-110.003,76.244,-110.003,76.48,-110.003,76.516,-110.003,77.059,-110.003,77.602,-110.004,77.929,-110.004,78.09,-110.004,78.145,-110.004,78.322,-110.004,78.687]},{"z":2,"b":[-110,60.001,-102.002,60.001],"l":[-102.002,60.001,-102.502,60.001,-103.001,60.001,-103.501,60.001,-104.001,60.001,-104.501,60.001,-105.001,60.001,-105.501,60.001,-106.001,60.001,-106.501,60.001,-107.001,60.001,-107.501,60.001,-108,60.001,-108.5,60.001,-109,60.001,-109.5,60.001,-110,60.001]},{"z":2,"b":[-120,60.001,-110,60.001],"l":[-120,60.001,-119.375,60.001,-118.75,60.001,-118.125,60.001,-117.5,60.001,-116.875,60.001,-116.25,60.001,-115.625,60.001,-115,60.001,-114.375,60.001,-113.75,60.001,-113.125,60.001,-112.5,60.001,-111.875,60.001,-111.25,60.001,-110.625,60.001,-110,60.001]},{"z":2,"b":[-110,48.993,-109.999,60.001],"l":[-110,60.001,-110,59.657,-110,59.313,-110,58.969,-110,58.625,-110,58.281,-110,57.937,-110,57.593,-110,57.249,-110,56.905,-110,56.561,-110,56.217,-110,55.873,-110,55.529,-110,55.185,-110,54.842,-110,54.498,-110,54.154,-110,53.81,-110,53.466,-110,53.122,-110,52.778,-110,52.434,-110,52.09,-110,51.746,-110,51.402,-110,51.058,-110,50.714,-109.999,50.37,-109.999,50.026,-109.999,49.682,-109.999,49.338,-109.999,48.993]},{"z":2,"b":[-95.16,49.37,-88.948,56.851],"l":[-88.948,56.851,-89.205,56.695,-89.464,56.535,-89.722,56.376,-89.981,56.216,-90.33,55.991,-90.68,55.767,-91.029,55.542,-91.378,55.317,-91.684,55.108,-91.989,54.899,-92.295,54.69,-92.6,54.481,-92.855,54.298,-93.11,54.114,-93.365,53.931,-93.62,53.748,-94.004,53.517,-94.387,53.285,-94.77,53.054,-95.154,52.822,-95.155,52.391,-95.156,51.96,-95.157,51.528,-95.158,51.097,-95.159,50.665,-95.159,50.234,-95.16,49.802,-95.155,49.37]},{"z":2,"b":[-102.002,60.001,-94.772,60.002],"l":[-102.002,60.001,-101.552,60.001,-101.102,60.001,-100.652,60.001,-100.202,60.001,-99.752,60.001,-99.302,60.001,-98.852,60.001,-98.402,60.001,-97.952,60.001,-97.502,60.001,-97.052,60.001,-96.603,60.001,-96.153,60.001,-95.703,60.001,-95.253,60.001,-94.772,60.002]},{"z":2,"b":[-102.002,48.993,-101.367,60.001],"l":[-102.002,60.001,-102.002,59.74,-102.002,59.478,-102.002,59.217,-102.002,58.955,-102.001,58.694,-102.001,58.432,-102.001,58.171,-102.001,57.909,-102.001,57.648,-102.001,57.386,-102.001,57.125,-102.001,56.863,-102.001,56.602,-102.001,56.34,-102.001,56.079,-102.001,55.818,-101.961,55.391,-101.921,54.965,-101.882,54.538,-101.842,54.112,-101.802,53.685,-101.762,53.259,-101.723,52.832,-101.683,52.406,-101.643,51.979,-101.603,51.553,-101.563,51.126,-101.524,50.7,-101.484,50.273,-101.444,49.847,-101.404,49.421,-101.367,48.993]},{"z":2,"b":[-123.221,45.591,-116.896,46.179],"l":[-116.896,46.002,-117.144,46.002,-117.41,46.002,-117.675,46.002,-117.941,46.002,-118.206,46.001,-118.472,46.001,-118.737,46.001,-119.002,46.001,-119.02,46,-119.05,45.981,-119.177,45.945,-119.429,45.934,-119.934,45.838,-120.156,45.762,-120.351,45.725,-120.716,45.69,-121.045,45.639,-121.184,45.644,-121.243,45.687,-121.402,45.712,-121.663,45.718,-121.882,45.689,-122.085,45.603,-122.15,45.594,-122.203,45.591,-122.561,45.625,-122.65,45.627,-122.726,45.674,-122.746,45.75,-122.843,45.978,-122.942,46.115,-123.042,46.16,-123.121,46.179,-123.221,46.154]},{"z":2,"b":[-117.039,46.002,-116.896,48.993],"l":[-116.896,46.002,-116.93,46.056,-116.948,46.124,-116.922,46.164,-116.943,46.232,-117.01,46.33,-117.039,46.397,-117.032,46.42,-117.025,46.429,-117.026,46.589,-117.027,46.75,-117.028,46.91,-117.028,47.07,-117.029,47.231,-117.03,47.391,-117.031,47.551,-117.032,47.712,-117.032,47.872,-117.033,48.032,-117.034,48.192,-117.035,48.353,-117.036,48.513,-117.037,48.673,-117.037,48.834,-117.039,48.993]},{"z":2,"b":[-117.202,42.001,-116.477,46.002],"l":[-117.017,42.001,-117.017,42.224,-117.018,42.447,-117.018,42.67,-117.018,42.893,-117.019,43.117,-117.019,43.34,-117.02,43.563,-117.02,43.808,-117.02,43.813,-116.958,43.964,-116.944,44.038,-116.961,44.079,-116.95,44.119,-116.911,44.157,-116.912,44.199,-116.953,44.245,-117.029,44.276,-117.14,44.29,-117.197,44.344,-117.202,44.436,-117.119,44.578,-116.949,44.77,-116.856,44.906,-116.84,44.985,-116.737,45.169,-116.549,45.457,-116.477,45.641,-116.521,45.722,-116.617,45.799,-116.765,45.873,-116.869,45.958,-116.896,46.002]},{"z":2,"b":[-114.725,32.715,-114.125,34.991],"l":[-114.611,34.991,-114.611,34.907,-114.557,34.795,-114.398,34.59,-114.379,34.541,-114.377,34.54,-114.371,34.488,-114.308,34.433,-114.159,34.355,-114.125,34.317,-114.125,34.314,-114.126,34.287,-114.164,34.253,-114.329,34.142,-114.418,34.051,-114.509,33.904,-114.495,33.785,-114.499,33.686,-114.522,33.608,-114.556,33.536,-114.603,33.47,-114.651,33.431,-114.702,33.418,-114.719,33.331,-114.704,33.17,-114.648,33.072,-114.551,33.037,-114.491,32.984,-114.469,32.912,-114.478,32.842,-114.516,32.773,-114.582,32.735,-114.721,32.724,-114.725,32.715]},{"z":2,"b":[-114.741,34.991,-114.04,37.004],"l":[-114.611,34.991,-114.611,34.998,-114.591,35.353,-114.649,35.476,-114.662,35.545,-114.645,35.631,-114.65,35.684,-114.677,35.73,-114.684,35.814,-114.687,35.917,-114.732,35.984,-114.741,36.014,-114.716,36.085,-114.669,36.122,-114.593,36.148,-114.505,36.156,-114.406,36.148,-114.332,36.116,-114.282,36.06,-114.232,36.032,-114.183,36.03,-114.119,36.077,-114.062,36.175,-114.043,36.182,-114.041,36.612,-114.04,37.004]},{"z":2,"b":[-106.668,31.768,-103,36.501],"l":[-103,36.501,-103.042,36.501,-103.044,36.219,-103.045,35.938,-103.047,35.657,-103.048,35.376,-103.049,35.095,-103.051,34.814,-103.052,34.533,-103.054,34.252,-103.055,33.971,-103.057,33.689,-103.058,33.408,-103.06,33.127,-103.061,32.846,-103.063,32.565,-103.064,32.284,-103.066,32.003,-103.29,32.003,-103.515,32.003,-103.74,32.002,-103.965,32.002,-104.19,32.002,-104.414,32.002,-104.639,32.002,-104.864,32.002,-105.089,32.002,-105.313,32.002,-105.538,32.002,-105.763,32.002,-105.988,32.002,-106.213,32.001,-106.437,32.001,-106.662,32.001,-106.668,32.001,-106.566,31.82,-106.445,31.768]},{"z":2,"b":[-95.347,39.124,-94.622,40.001],"l":[-95.347,40.001,-95.116,39.872,-95.056,39.871,-94.984,39.891,-94.93,39.861,-94.893,39.782,-94.899,39.741,-94.946,39.739,-95.005,39.692,-95.075,39.597,-95.058,39.509,-94.953,39.426,-94.882,39.343,-94.847,39.259,-94.778,39.194,-94.622,39.124]},{"z":2,"b":[-95.765,40.001,-95.347,40.602],"l":[-95.765,40.602,-95.74,40.571,-95.685,40.466,-95.662,40.363,-95.607,40.295,-95.521,40.265,-95.464,40.198,-95.434,40.096,-95.362,40.01,-95.347,40.001]},{"z":2,"b":[-96.481,40.602,-95.765,42.511],"l":[-96.481,42.511,-96.451,42.506,-96.384,42.418,-96.356,42.26,-96.295,42.133,-96.201,42.036,-96.154,41.965,-96.154,41.92,-96.131,41.872,-96.087,41.821,-96.069,41.734,-96.078,41.611,-96.059,41.546,-96.011,41.538,-95.991,41.52,-95.997,41.494,-95.982,41.474,-95.944,41.461,-95.933,41.425,-95.95,41.367,-95.936,41.331,-95.892,41.316,-95.884,41.288,-95.911,41.245,-95.907,41.212,-95.871,41.188,-95.847,41.08,-95.834,40.889,-95.838,40.778,-95.859,40.745,-95.826,40.676,-95.765,40.602]},{"z":2,"b":[-104.029,42.511,-96.481,43.001],"l":[-96.481,42.511,-96.558,42.525,-96.633,42.569,-96.676,42.638,-96.805,42.701,-97.02,42.76,-97.164,42.813,-97.239,42.861,-97.417,42.881,-97.698,42.875,-97.881,42.85,-97.965,42.806,-98.143,42.852,-98.442,43.001,-98.837,43.001,-99.183,43.001,-99.529,43.001,-99.875,43.001,-100.222,43.001,-100.568,43.001,-100.914,43.001,-101.26,43.001,-101.606,43.001,-101.952,43.001,-102.298,43.001,-102.644,43.001,-102.991,43.001,-103.337,43.001,-103.683,43.001,-104.029,43.001]},{"z":2,"b":[-91.656,31.002,-90.938,33.016],"l":[-91.15,33.016,-91.144,32.967,-91.096,32.938,-91.093,32.885,-91.136,32.809,-91.136,32.756,-91.092,32.726,-91.094,32.694,-91.142,32.661,-91.15,32.625,-91.116,32.586,-91.101,32.541,-91.104,32.49,-91.059,32.424,-90.99,32.381,-90.945,32.331,-90.94,32.303,-90.938,32.277,-90.948,32.24,-91.005,32.186,-91.048,32.146,-91.075,32.131,-91.089,32.104,-91.093,32.046,-91.157,31.971,-91.286,31.884,-91.373,31.774,-91.415,31.64,-91.459,31.555,-91.506,31.519,-91.529,31.449,-91.529,31.345,-91.559,31.288,-91.618,31.277,-91.631,31.218,-91.596,31.11,-91.611,31.065,-91.653,31.03,-91.656,31.002]},{"z":2,"b":[-91.215,33.016,-90.282,35.001],"l":[-90.294,35.001,-90.293,34.97,-90.282,34.945,-90.32,34.899,-90.416,34.852,-90.468,34.805,-90.475,34.76,-90.497,34.738,-90.539,34.728,-90.564,34.661,-90.584,34.504,-90.635,34.414,-90.718,34.392,-90.762,34.356,-90.768,34.308,-90.817,34.252,-90.91,34.189,-90.94,34.129,-90.909,34.072,-90.915,34.049,-90.943,34.03,-91.043,34.003,-91.06,33.989,-91.07,33.974,-91.05,33.945,-91.038,33.901,-91.053,33.828,-91.077,33.797,-91.1,33.767,-91.184,33.716,-91.207,33.676,-91.17,33.647,-91.173,33.617,-91.215,33.583,-91.203,33.547,-91.193,33.523,-91.154,33.5,-91.141,33.439,-91.168,33.359,-91.163,33.294,-91.117,33.241,-91.107,33.218,-91.153,33.041,-91.15,33.016]},{"z":2,"b":[-90.294,35.001,-89.689,36.002],"l":[-89.705,36.002,-89.709,35.983,-89.689,35.944,-89.689,35.921,-89.704,35.907,-89.738,35.899,-89.764,35.889,-89.769,35.872,-89.77,35.864,-89.768,35.845,-89.764,35.828,-89.78,35.806,-89.895,35.751,-89.944,35.679,-89.927,35.592,-89.967,35.503,-90.066,35.414,-90.108,35.314,-90.093,35.204,-90.142,35.114,-90.256,35.046,-90.294,35.004,-90.294,35.001]},{"z":2,"b":[-89.705,36.002,-89.541,36.501],"l":[-89.557,36.501,-89.541,36.427,-89.555,36.373,-89.604,36.352,-89.612,36.322,-89.584,36.288,-89.584,36.272,-89.595,36.259,-89.628,36.255,-89.654,36.248,-89.675,36.221,-89.634,36.168,-89.642,36.105,-89.698,36.03,-89.705,36.002]},{"z":2,"b":[-89.56,36.501,-89.487,36.555],"l":[-89.487,36.503,-89.497,36.529,-89.52,36.555,-89.536,36.555,-89.55,36.553,-89.555,36.541,-89.56,36.513,-89.557,36.501]},{"z":2,"b":[-89.487,36.489,-89.45,36.503],"l":[-89.45,36.5,-89.452,36.498,-89.471,36.489,-89.485,36.498,-89.487,36.503]},{"z":2,"b":[-89.45,36.5,-89.122,36.992],"l":[-89.154,36.992,-89.122,36.912,-89.13,36.811,-89.156,36.716,-89.203,36.626,-89.256,36.594,-89.28,36.6,-89.331,36.616,-89.351,36.611,-89.397,36.588,-89.45,36.5]},{"z":2,"b":[-91.502,36.992,-89.154,40.379],"l":[-91.442,40.379,-91.5,40.226,-91.502,40.098,-91.467,39.943,-91.426,39.827,-91.379,39.749,-91.21,39.603,-90.919,39.387,-90.737,39.182,-90.664,38.987,-90.577,38.91,-90.478,38.948,-90.354,38.93,-90.22,38.898,-90.15,38.86,-90.134,38.847,-90.127,38.833,-90.132,38.819,-90.151,38.781,-90.317,38.458,-90.374,38.31,-90.366,38.254,-90.299,38.174,-90.174,38.069,-90.064,38.001,-89.97,37.969,-89.823,37.884,-89.623,37.747,-89.523,37.645,-89.523,37.576,-89.503,37.506,-89.462,37.434,-89.462,37.366,-89.503,37.301,-89.487,37.213,-89.414,37.102,-89.352,37.036,-89.301,37.012,-89.29,37.013,-89.286,37.023,-89.293,37.038,-89.296,37.052,-89.292,37.071,-89.281,37.078,-89.272,37.08,-89.225,37.053,-89.154,36.992]},{"z":2,"b":[-91.442,40.379,-90.149,42.513],"l":[-90.651,42.513,-90.59,42.445,-90.485,42.383,-90.425,42.326,-90.41,42.272,-90.347,42.215,-90.233,42.154,-90.168,42.1,-90.149,42.054,-90.152,41.982,-90.177,41.884,-90.218,41.813,-90.276,41.77,-90.319,41.71,-90.348,41.632,-90.437,41.56,-90.587,41.495,-90.754,41.45,-90.939,41.426,-91.048,41.369,-91.082,41.28,-91.067,41.206,-91.001,41.146,-90.964,41.083,-90.954,41.016,-91,40.905,-91.102,40.75,-91.215,40.65,-91.339,40.604,-91.394,40.537,-91.381,40.448,-91.391,40.397,-91.442,40.379]},{"z":2,"b":[-91.245,42.513,-90.651,43.502],"l":[-91.245,43.502,-91.238,43.44,-91.219,43.395,-91.178,43.359,-91.117,43.331,-91.107,43.278,-91.148,43.2,-91.161,43.102,-91.148,42.986,-91.118,42.882,-91.07,42.789,-90.96,42.72,-90.789,42.677,-90.688,42.61,-90.657,42.521,-90.651,42.513]},{"z":2,"b":[-92.897,43.502,-89.958,47.309],"l":[-91.245,43.502,-91.278,43.797,-91.32,43.936,-91.383,43.991,-91.515,44.054,-91.716,44.126,-91.841,44.194,-91.912,44.289,-92.055,44.4,-92.149,44.445,-92.24,44.462,-92.313,44.498,-92.367,44.552,-92.442,44.587,-92.537,44.601,-92.644,44.645,-92.77,44.726,-92.773,44.732,-92.799,44.79,-92.785,44.823,-92.763,44.934,-92.765,44.969,-92.793,45.071,-92.759,45.111,-92.766,45.236,-92.754,45.278,-92.686,45.381,-92.683,45.433,-92.707,45.494,-92.757,45.543,-92.892,45.595,-92.897,45.659,-92.875,45.706,-92.786,45.796,-92.738,45.875,-92.694,45.909,-92.425,46.028,-92.367,46.036,-92.325,46.07,-92.293,46.084,-92.292,46.228,-92.292,46.373,-92.292,46.517,-92.292,46.661,-92.224,46.671,-92.195,46.707,-92.125,46.763,-92.093,46.762,-91.965,46.73,-91.841,46.779,-91.717,46.827,-91.593,46.876,-91.468,46.925,-91.265,47.021,-91.061,47.117,-90.858,47.213,-90.654,47.309,-90.48,47.304,-90.306,47.298,-90.132,47.293,-89.958,47.287]},{"z":2,"b":[-89.165,36.992,-88.042,37.789],"l":[-88.042,37.789,-88.073,37.771,-88.129,37.7,-88.14,37.621,-88.108,37.534,-88.191,37.465,-88.389,37.411,-88.495,37.364,-88.507,37.324,-88.493,37.262,-88.453,37.178,-88.446,37.12,-88.472,37.088,-88.614,37.11,-88.873,37.186,-89.046,37.183,-89.135,37.102,-89.165,37.042,-89.154,36.992]},{"z":2,"b":[-88.042,37.788,-84.79,39.092],"l":[-84.822,39.092,-84.837,39.083,-84.861,39.04,-84.835,38.996,-84.837,38.961,-84.864,38.933,-84.854,38.913,-84.807,38.9,-84.79,38.868,-84.801,38.819,-84.901,38.77,-85.094,38.714,-85.136,38.697,-85.17,38.69,-85.186,38.694,-85.211,38.704,-85.247,38.727,-85.307,38.737,-85.405,38.73,-85.445,38.683,-85.425,38.597,-85.459,38.53,-85.546,38.484,-85.609,38.423,-85.646,38.347,-85.706,38.301,-85.788,38.285,-85.855,38.215,-85.906,38.091,-85.961,38.014,-86.021,37.984,-86.103,37.992,-86.208,38.037,-86.268,38.083,-86.284,38.13,-86.315,38.164,-86.343,38.179,-86.361,38.179,-86.363,38.172,-86.355,38.159,-86.365,38.145,-86.43,38.14,-86.457,38.12,-86.448,38.086,-86.463,38.062,-86.5,38.048,-86.52,38.015,-86.522,37.964,-86.549,37.917,-86.599,37.875,-86.673,37.889,-86.77,37.96,-86.869,37.976,-86.969,37.938,-87.044,37.886,-87.094,37.821,-87.141,37.805,-87.209,37.855,-87.372,37.923,-87.457,37.938,-87.518,37.922,-87.561,37.929,-87.576,37.948,-87.587,37.956,-87.599,37.946,-87.601,37.887,-87.615,37.849,-87.643,37.833,-87.665,37.846,-87.679,37.886,-87.714,37.9,-87.77,37.885,-87.825,37.891,-87.874,37.916,-87.892,37.913,-87.906,37.902,-87.902,37.855,-87.912,37.818,-87.938,37.794,-87.975,37.788,-88.022,37.8,-88.042,37.789]},{"z":2,"b":[-84.822,38.448,-82.613,39.129],"l":[-82.613,38.448,-82.621,38.451,-82.758,38.541,-82.845,38.628,-82.88,38.71,-83,38.714,-83.203,38.639,-83.357,38.627,-83.461,38.68,-83.538,38.7,-83.616,38.683,-83.69,38.65,-83.752,38.669,-83.828,38.731,-83.948,38.774,-84.113,38.798,-84.227,38.865,-84.29,38.975,-84.358,39.05,-84.43,39.091,-84.509,39.103,-84.595,39.086,-84.661,39.094,-84.707,39.129,-84.766,39.125,-84.822,39.092]},{"z":2,"b":[-82.613,38.415,-80.519,40.647],"l":[-80.519,40.647,-80.638,40.603,-80.659,40.563,-80.623,40.512,-80.604,40.445,-80.602,40.364,-80.668,40.185,-80.804,39.909,-80.872,39.736,-80.874,39.664,-80.963,39.571,-81.139,39.456,-81.263,39.387,-81.333,39.363,-81.392,39.365,-81.44,39.393,-81.493,39.374,-81.552,39.308,-81.606,39.274,-81.657,39.272,-81.701,39.229,-81.737,39.147,-81.771,39.099,-81.803,39.086,-81.805,39.044,-81.778,38.973,-81.78,38.947,-81.795,38.946,-81.809,38.95,-81.844,38.934,-81.882,38.894,-81.897,38.894,-81.907,38.9,-81.919,38.952,-81.95,38.989,-82.001,39.011,-82.072,38.963,-82.163,38.845,-82.204,38.768,-82.188,38.715,-82.18,38.638,-82.202,38.606,-82.252,38.594,-82.291,38.552,-82.318,38.48,-82.387,38.434,-82.498,38.415,-82.613,38.448]},{"z":2,"b":[-88.095,37.789,-87.221,41.761],"l":[-88.042,37.789,-88.095,37.876,-88.091,37.897,-88.059,37.898,-88.042,37.937,-88.045,38.013,-88.026,38.06,-87.996,38.074,-87.99,38.089,-88.005,38.098,-88.006,38.113,-87.958,38.144,-87.949,38.178,-87.977,38.214,-87.98,38.249,-87.957,38.282,-87.921,38.298,-87.873,38.295,-87.827,38.336,-87.784,38.419,-87.735,38.47,-87.681,38.488,-87.636,38.543,-87.584,38.679,-87.529,38.742,-87.518,38.812,-87.53,38.909,-87.568,39.007,-87.632,39.107,-87.647,39.169,-87.615,39.193,-87.603,39.234,-87.611,39.291,-87.579,39.344,-87.533,39.375,-87.533,39.379,-87.531,39.662,-87.53,39.962,-87.528,40.262,-87.527,40.562,-87.525,40.861,-87.524,41.161,-87.523,41.461,-87.521,41.761,-87.371,41.761,-87.221,41.761]},{"z":2,"b":[-88.486,30.371,-88.085,34.999],"l":[-88.173,34.999,-88.085,34.933,-88.088,34.909,-88.089,34.893,-88.096,34.846,-88.12,34.661,-88.144,34.477,-88.169,34.292,-88.193,34.107,-88.218,33.923,-88.242,33.738,-88.267,33.553,-88.291,33.368,-88.315,33.184,-88.34,32.999,-88.364,32.814,-88.389,32.629,-88.413,32.445,-88.437,32.26,-88.462,32.075,-88.486,31.891,-88.476,31.703,-88.465,31.515,-88.455,31.327,-88.444,31.14,-88.434,30.952,-88.423,30.764,-88.413,30.577,-88.4,30.371]},{"z":2,"b":[-88.203,34.999,-85.624,35.025],"l":[-85.624,35.001,-85.784,35.002,-85.944,35.004,-86.105,35.005,-86.265,35.007,-86.426,35.008,-86.586,35.01,-86.747,35.011,-86.907,35.013,-87.067,35.014,-87.228,35.016,-87.388,35.017,-87.549,35.019,-87.709,35.02,-87.87,35.022,-88.03,35.023,-88.19,35.025,-88.203,35.024,-88.191,35.012,-88.173,34.999]},{"z":2,"b":[-89.45,36.5,-83.668,36.693],"l":[-83.668,36.605,-83.675,36.603,-83.683,36.6,-83.691,36.598,-83.699,36.596,-83.707,36.594,-83.715,36.591,-83.723,36.589,-83.73,36.587,-84.078,36.597,-84.425,36.606,-84.772,36.616,-85.119,36.626,-85.466,36.636,-85.813,36.645,-86.16,36.655,-86.508,36.665,-86.84,36.66,-87.173,36.654,-87.506,36.649,-87.839,36.644,-87.869,36.675,-87.956,36.682,-88.091,36.693,-88.096,36.693,-88.057,36.59,-88.056,36.501,-88.059,36.5,-88.216,36.5,-88.388,36.5,-88.561,36.5,-88.734,36.5,-88.906,36.5,-89.079,36.5,-89.252,36.5,-89.45,36.5]},{"z":2,"b":[-83.668,36.598,-81.659,36.619],"l":[-81.659,36.611,-81.801,36.615,-81.905,36.619,-81.953,36.598,-82.162,36.599,-82.329,36.6,-82.612,36.601,-82.956,36.602,-83.441,36.604,-83.668,36.605]},{"z":2,"b":[-81.659,36.551,-75.857,36.611],"l":[-75.857,36.551,-75.889,36.551,-75.966,36.551,-76.215,36.553,-76.577,36.556,-76.938,36.559,-77.3,36.561,-77.662,36.564,-78.024,36.567,-78.386,36.57,-78.747,36.573,-79.109,36.575,-79.471,36.578,-79.833,36.581,-80.194,36.584,-80.556,36.587,-80.918,36.589,-81.28,36.592,-81.642,36.595,-81.659,36.611]},{"z":2,"b":[-85.007,30.371,-81.504,31.002],"l":[-85.007,31.002,-84.966,30.923,-84.887,30.759,-84.884,30.721,-84.79,30.715,-84.631,30.706,-84.472,30.696,-84.312,30.687,-84.153,30.678,-83.993,30.668,-83.834,30.659,-83.675,30.65,-83.515,30.64,-83.356,30.631,-83.196,30.622,-83.037,30.612,-82.877,30.603,-82.718,30.594,-82.559,30.584,-82.399,30.575,-82.24,30.566,-82.239,30.533,-82.213,30.481,-82.206,30.422,-82.194,30.393,-82.163,30.376,-82.112,30.371,-82.062,30.405,-82.03,30.478,-82.022,30.56,-82.041,30.653,-82.038,30.732,-82.013,30.778,-81.983,30.789,-81.957,30.814,-81.936,30.82,-81.88,30.806,-81.673,30.74,-81.504,30.731]},{"z":2,"b":[-85.624,31.002,-84.922,35.001],"l":[-85.007,31.002,-85.067,31.118,-85.093,31.296,-85.055,31.573,-85.067,31.635,-85.125,31.777,-85.124,31.881,-85.064,32.051,-85.065,32.051,-85.035,32.147,-84.993,32.202,-84.93,32.247,-84.922,32.286,-84.968,32.32,-84.986,32.361,-84.974,32.409,-85.015,32.526,-85.157,32.804,-85.176,32.898,-85.201,32.956,-85.227,33.084,-85.254,33.212,-85.28,33.34,-85.307,33.467,-85.333,33.595,-85.359,33.723,-85.386,33.851,-85.412,33.979,-85.439,34.106,-85.465,34.234,-85.492,34.362,-85.518,34.49,-85.544,34.617,-85.571,34.745,-85.597,34.873,-85.624,35.001]},{"z":2,"b":[-83.356,32.03,-80.872,35.001],"l":[-83.115,35.001,-83.121,35.001,-83.17,34.933,-83.317,34.806,-83.356,34.708,-83.167,34.6,-83.166,34.599,-83.053,34.511,-82.976,34.476,-82.897,34.466,-82.848,34.437,-82.819,34.366,-82.589,34.017,-82.589,34.018,-82.352,33.838,-82.257,33.749,-82.208,33.664,-82.006,33.523,-81.936,33.447,-81.933,33.39,-81.909,33.349,-81.864,33.325,-81.821,33.274,-81.78,33.195,-81.699,33.127,-81.578,33.069,-81.49,32.936,-81.434,32.728,-81.377,32.607,-81.29,32.557,-81.172,32.38,-81.133,32.275,-81.135,32.182,-81.075,32.11,-80.872,32.03]},{"z":2,"b":[-77.122,38.807,-77.02,38.944],"l":[-77.122,38.944,-77.102,38.936,-77.053,38.915,-77.03,38.889,-77.02,38.807]},{"z":2,"b":[-77.727,38.944,-77.122,39.346],"l":[-77.727,39.346,-77.669,39.31,-77.534,39.266,-77.479,39.221,-77.503,39.175,-77.506,39.143,-77.481,39.113,-77.301,39.053,-77.19,38.969,-77.122,38.944]},{"z":2,"b":[-79.488,39.211,-77.727,39.686],"l":[-79.488,39.211,-79.36,39.285,-79.294,39.312,-79.075,39.476,-79.047,39.477,-78.971,39.454,-78.815,39.57,-78.796,39.613,-78.778,39.627,-78.758,39.625,-78.746,39.614,-78.753,39.59,-78.742,39.578,-78.678,39.55,-78.587,39.534,-78.575,39.533,-78.496,39.533,-78.46,39.556,-78.443,39.601,-78.407,39.628,-78.325,39.639,-78.181,39.686,-78.098,39.678,-78.027,39.631,-77.956,39.609,-77.883,39.611,-77.855,39.6,-77.872,39.576,-77.853,39.548,-77.798,39.517,-77.78,39.488,-77.797,39.461,-77.792,39.442,-77.762,39.429,-77.745,39.4,-77.739,39.354,-77.727,39.346]},{"z":2,"b":[-79.559,45.214,-74.34,51.545],"l":[-79.521,51.545,-79.52,51.226,-79.52,50.966,-79.52,50.919,-79.519,50.742,-79.519,50.5,-79.518,50.258,-79.518,50.017,-79.517,49.775,-79.517,49.533,-79.516,49.291,-79.516,49.049,-79.515,48.807,-79.515,48.565,-79.514,48.323,-79.514,48.082,-79.513,47.84,-79.513,47.556,-79.513,47.551,-79.559,47.483,-79.547,47.407,-79.479,47.319,-79.442,47.228,-79.435,47.134,-79.365,47.022,-79.166,46.827,-79.015,46.607,-78.903,46.486,-78.781,46.393,-78.46,46.307,-77.683,46.187,-77.373,46.064,-77.371,46.063,-77.293,45.977,-77.282,45.962,-77.267,45.929,-77.233,45.904,-77.101,45.833,-77.031,45.807,-76.977,45.798,-76.941,45.801,-76.926,45.82,-76.913,45.876,-76.89,45.89,-76.872,45.887,-76.824,45.88,-76.754,45.798,-76.667,45.643,-76.553,45.542,-76.413,45.496,-76.27,45.484,-76.123,45.505,-76.001,45.484,-75.904,45.42,-75.626,45.451,-75.167,45.575,-74.864,45.642,-74.717,45.651,-74.577,45.632,-74.397,45.568,-74.415,45.509,-74.453,45.394,-74.478,45.318,-74.413,45.269,-74.34,45.214]},{"z":4.7,"b":[47.949,52.78,50.256,54.516],"l":[50.077,54.516,50.119,54.437,50.206,54.418,50.256,54.305,50.197,54.038,49.979,53.966,49.957,53.907,49.827,53.887,49.624,53.815,49.414,53.868,49.299,53.84,49.029,53.857,48.874,53.716,48.491,53.771,48.456,53.666,48.331,53.624,48.376,53.471,48.057,53.476,47.949,53.359,48.035,53.315,48.144,53.162,48.149,53.059,48.222,52.999,48.39,52.97,48.522,52.978,48.593,52.919,48.54,52.83,48.437,52.78]},{"z":4.7,"b":[45.792,52.616,46.983,54.005],"l":[46.831,52.616,46.852,52.726,46.983,52.771,46.935,52.91,46.913,53.079,46.954,53.191,46.906,53.335,46.795,53.408,46.494,53.511,46.468,53.654,46.193,53.85,46.212,53.909,45.957,54.005,45.792,54.004]},{"z":4.7,"b":[50.072,56.1,51.436,56.67],"l":[50.072,56.623,50.35,56.67,50.497,56.511,50.586,56.367,50.706,56.33,50.851,56.39,50.908,56.296,50.837,56.248,51.073,56.131,51.224,56.1,51.436,56.144]},{"z":4.7,"b":[51.399,55.858,53.632,56.538],"l":[51.436,56.144,51.501,56.082,51.399,55.994,51.442,55.925,51.633,55.952,51.829,55.918,51.938,55.974,52.064,55.896,52.168,55.899,52.242,55.965,52.195,56.077,52.439,56.033,52.473,56.074,52.655,56.018,52.794,56.095,52.839,56.163,52.711,56.23,52.562,56.259,52.733,56.378,52.908,56.416,52.934,56.538,53.08,56.527,52.945,56.366,52.958,56.252,53.273,56.085,53.336,56.157,53.26,56.254,53.427,56.277,53.551,56.241,53.564,56.181,53.405,56.023,53.276,55.935,53.289,55.858,53.632,55.907]},{"z":4.7,"b":[47.242,54.678,48.412,55.829],"l":[47.242,54.678,47.37,54.753,47.379,54.832,47.487,54.858,47.729,54.831,47.803,54.945,47.996,54.968,48.075,55.108,48.017,55.276,47.913,55.316,47.763,55.214,47.703,55.328,47.834,55.342,47.974,55.5,48.101,55.545,48.025,55.63,48.205,55.694,48.412,55.829]},{"z":4.7,"b":[48.401,55.829,50.18,56.623],"l":[48.412,55.829,48.401,55.888,48.694,55.965,48.69,56.068,48.835,56.072,49.016,56.233,49.037,56.358,49.276,56.353,49.558,56.441,49.578,56.505,49.765,56.536,49.885,56.442,50.025,56.46,50.18,56.549,50.072,56.623]},{"z":4.7,"b":[50.077,54.33,52.539,54.642],"l":[52.539,54.377,52.482,54.467,52.223,54.445,52.099,54.388,51.933,54.436,51.936,54.528,51.668,54.567,51.513,54.639,51.391,54.601,51.296,54.642,51.169,54.637,51.093,54.554,51.004,54.539,50.948,54.348,50.697,54.427,50.519,54.33,50.384,54.486,50.077,54.516]},{"z":4.7,"b":[47.242,54.51,50.077,54.884],"l":[50.077,54.516,49.975,54.51,49.746,54.574,49.616,54.562,49.476,54.775,49.291,54.884,49.118,54.795,49.014,54.793,48.828,54.648,48.641,54.66,48.47,54.634,48.29,54.738,48.086,54.766,47.85,54.704,47.754,54.731,47.637,54.565,47.366,54.527,47.351,54.593,47.242,54.678]},{"z":4.7,"b":[42.094,52.403,43.237,53.812],"l":[42.134,53.812,42.094,53.54,42.312,53.44,42.354,53.368,42.748,53.022,42.905,52.97,43.095,52.822,43.237,52.661,43.042,52.561,43.011,52.482,43.08,52.403]},{"z":4.7,"b":[84.41,56.044,88.624,56.833],"l":[88.624,56.833,88.552,56.787,88.419,56.829,88.144,56.719,87.779,56.548,87.62,56.64,87.397,56.616,87.167,56.676,87.13,56.54,86.913,56.556,86.707,56.632,86.532,56.569,86.394,56.549,86.313,56.627,86.217,56.624,86.112,56.497,85.784,56.406,85.791,56.361,85.606,56.324,85.688,56.239,85.379,56.213,85.254,56.245,85.121,56.216,85.009,56.133,84.893,56.17,84.41,56.044]},{"z":4.7,"b":[38.895,53.342,40.16,53.588],"l":[38.895,53.565,39.041,53.55,39.162,53.395,39.415,53.414,39.524,53.462,39.512,53.525,39.74,53.588,39.907,53.499,39.918,53.416,40.16,53.342]},{"z":4.7,"b":[40.16,53.342,42.134,53.823],"l":[40.16,53.342,40.317,53.356,40.502,53.461,40.738,53.56,40.84,53.487,41.021,53.49,41.174,53.434,41.354,53.556,41.349,53.741,41.4,53.783,41.748,53.823,42.134,53.812]},{"z":4.7,"b":[37.727,52.017,38.038,53.009],"l":[37.747,52.017,37.778,52.161,37.948,52.246,38.038,52.345,37.911,52.473,37.783,52.508,37.727,52.597,37.813,52.648,37.777,52.719,37.871,52.772,37.79,52.845,37.839,52.891,37.841,53.009]},{"z":4.7,"b":[37.841,52.974,38.895,53.565],"l":[37.841,53.009,37.912,53.073,38.108,53.059,38.307,52.974,38.473,53.041,38.595,53.049,38.633,53.123,38.595,53.24,38.477,53.31,38.762,53.382,38.697,53.5,38.895,53.565]},{"z":4.7,"b":[75.159,58.65,84.26,61.047],"l":[84.26,60.856,83.997,60.826,83.616,60.982,83.504,61.047,83.144,61.028,82.407,60.723,82.362,60.601,82.139,60.532,81.83,60.645,81.498,60.615,81.141,60.637,81.017,60.757,80.723,60.797,80.414,60.761,80.165,60.664,79.853,60.692,79.503,60.687,79.36,60.664,79.285,60.726,79.314,60.792,79.169,60.837,78.806,60.78,78.675,60.828,78.474,60.777,78.164,60.803,77.941,60.749,77.702,60.825,77.408,60.811,77.136,60.856,77.068,60.726,76.97,60.649,77.054,60.578,77.013,60.527,76.78,60.452,76.749,60.347,76.848,60.266,76.699,60.117,76.758,60.057,76.741,59.758,76.656,59.693,76.645,59.585,76.44,59.545,76.172,59.543,76.007,59.428,75.895,59.426,75.843,59.323,75.628,59.238,75.687,59.04,75.642,58.982,75.373,58.786,75.159,58.65]},{"z":4.7,"b":[39.953,51.954,40.697,53.342],"l":[40.16,53.342,40.107,53.276,40.141,53.072,40.07,52.925,40.08,52.804,39.979,52.769,39.953,52.654,40.067,52.561,40.203,52.512,40.331,52.403,40.477,52.374,40.697,52.27,40.616,52.129,40.632,52.066,40.507,52.022,40.504,51.954]},{"z":4.7,"b":[46.341,58.075,47.602,59.611],"l":[47.114,59.611,47.056,59.379,47.252,59.355,47.239,59.22,47.571,59.06,47.602,58.907,47.452,58.921,47.282,58.893,47.313,58.807,47.26,58.756,47.12,58.775,46.947,58.607,46.687,58.567,46.554,58.488,46.541,58.397,46.424,58.347,46.407,58.209,46.341,58.075]},{"z":4.7,"b":[43.727,57.431,46.341,58.08],"l":[46.341,58.075,45.817,58.047,45.706,57.974,45.557,57.939,45.462,58.035,45.294,58.041,45.155,58.08,44.863,58.075,44.793,57.955,44.865,57.88,44.728,57.704,44.612,57.707,44.483,57.646,44.303,57.517,43.995,57.44,43.846,57.431,43.727,57.466]},{"z":4.7,"b":[35.931,53.618,37.259,54.836],"l":[37.259,54.836,37.208,54.688,37.089,54.549,36.805,54.61,36.848,54.472,36.965,54.396,36.891,54.283,36.714,54.317,36.461,54.288,36.354,54.223,36.237,54.229,36.201,54.098,36.111,53.961,35.989,53.916,35.931,53.786,36.01,53.618]},{"z":4.7,"b":[40.926,58.579,47.114,59.633],"l":[40.926,58.579,41.218,58.74,41.146,58.785,41.245,58.902,41.522,59.037,41.687,59.08,42.012,59.118,42.09,59.23,41.959,59.278,42.058,59.427,42.188,59.391,42.197,59.303,42.632,59.308,42.671,59.228,42.8,59.17,43.026,59.208,43.151,59.261,43.294,59.218,43.371,59.28,43.498,59.294,43.582,59.188,43.742,59.26,43.96,59.205,44.103,59.23,44.207,59.172,44.391,59.153,44.991,59.122,45.091,59.184,45.366,59.198,45.918,59.184,46.004,59.282,46.182,59.345,46.356,59.347,46.406,59.633,46.757,59.607,46.875,59.632,47.114,59.611]},{"z":4.7,"b":[35.37,55.194,36.904,55.319],"l":[35.37,55.239,35.465,55.22,35.579,55.29,35.755,55.287,35.856,55.238,36.238,55.194,36.423,55.319,36.586,55.309,36.904,55.229]},{"z":4.7,"b":[37.12,54.836,37.259,54.991],"l":[37.12,54.991,37.259,54.836]},{"z":4.7,"b":[36.904,54.991,37.12,55.229],"l":[36.904,55.229,37.084,55.096,37.12,54.991]},{"z":4.7,"b":[42.467,54.476,46.056,55.177],"l":[42.467,54.871,42.636,54.787,43.098,54.799,43.155,54.903,43.369,54.95,43.61,54.901,43.689,54.831,43.949,54.807,43.939,54.742,44.091,54.734,44.107,54.646,44.376,54.59,44.54,54.53,44.597,54.58,44.74,54.516,44.917,54.476,45.114,54.494,45.029,54.568,45.091,54.659,45.041,54.714,45.124,54.796,45.209,54.757,45.266,54.873,45.424,54.882,45.439,55.033,45.605,55.164,45.71,55.177,45.74,55.077,45.815,55,45.954,54.997,46.056,55.042]},{"z":4.7,"b":[46.056,54.772,46.441,55.042],"l":[46.056,55.042,46.415,54.895,46.441,54.772]},{"z":4.7,"b":[36.01,53.009,37.841,53.618],"l":[36.01,53.618,36.098,53.556,36.22,53.57,36.398,53.429,36.535,53.391,36.667,53.416,36.807,53.297,37.088,53.28,37.197,53.251,37.327,53.303,37.568,53.289,37.561,53.169,37.787,53.089,37.841,53.009]},{"z":4.7,"b":[42.206,53.847,42.699,54.871],"l":[42.308,53.847,42.437,54.012,42.319,54.099,42.535,54.083,42.596,54.177,42.463,54.254,42.273,54.171,42.206,54.231,42.381,54.264,42.527,54.367,42.625,54.373,42.557,54.492,42.699,54.582,42.524,54.723,42.419,54.736,42.391,54.807,42.467,54.871]},{"z":4.7,"b":[88.365,52.168,89.26,55.446],"l":[88.609,55.446,88.396,55.279,88.55,55.083,88.547,55.023,88.66,54.903,88.748,54.89,88.753,54.805,88.613,54.697,88.603,54.522,88.519,54.416,88.561,54.288,88.798,54.407,88.955,54.369,89.004,54.29,89.129,54.35,89.219,54.243,89.187,54.147,89.047,54.117,88.981,54.065,89.073,53.953,89.26,53.835,89.181,53.781,89.018,53.742,89.011,53.679,88.834,53.585,88.895,53.387,89.051,53.249,89.049,53.13,88.895,52.987,89.118,52.92,89.236,52.81,89.043,52.678,89.115,52.626,88.945,52.488,88.676,52.352,88.688,52.257,88.365,52.168]},{"z":4.7,"b":[35.104,53.272,36.01,53.644],"l":[35.104,53.311,35.205,53.272,35.356,53.284,35.422,53.404,35.637,53.543,35.766,53.519,35.911,53.644,36.01,53.618]},{"z":4.7,"b":[42.134,53.812,42.308,53.847],"l":[42.134,53.812,42.308,53.847]},{"z":4.7,"b":[42.308,53.664,45.792,54.017],"l":[42.308,53.847,42.513,53.737,42.631,53.808,42.94,53.801,43.17,53.839,43.106,53.909,43.182,54.001,43.31,54.017,43.425,53.989,43.528,54.015,43.752,53.963,43.933,53.862,43.877,53.81,44.099,53.736,44.206,53.76,44.314,53.701,44.428,53.723,44.534,53.664,44.729,53.7,44.615,53.831,44.617,53.892,44.883,53.969,45.176,53.979,45.339,53.885,45.646,53.93,45.792,54.004]},{"z":4.7,"b":[45.792,54.004,46.734,54.772],"l":[45.792,54.004,45.879,54.031,46.018,54.189,46.379,54.231,46.488,54.284,46.719,54.33,46.734,54.384,46.543,54.406,46.482,54.499,46.533,54.553,46.516,54.653,46.444,54.688,46.441,54.772]},{"z":4.7,"b":[46.441,54.632,47.242,54.772],"l":[46.441,54.772,46.869,54.632,47.063,54.69,47.242,54.678]},{"z":4.7,"b":[42.588,56.42,43.727,57.466],"l":[43.727,57.466,43.689,57.279,43.539,57.262,43.514,57.118,43.67,57.043,43.501,56.906,43.339,56.861,42.978,56.842,42.83,56.802,42.787,56.669,42.902,56.646,42.845,56.552,42.882,56.472,42.648,56.481,42.588,56.42]},{"z":4.7,"b":[46.341,56.966,47.762,58.075],"l":[46.341,58.075,46.43,58.02,47.354,58.026,47.452,57.987,47.703,57.958,47.762,57.876,47.572,57.81,47.521,57.586,47.471,57.529,47.249,57.567,47.054,57.498,46.75,57.517,46.695,57.32,46.79,57.281,46.742,57.205,46.763,57.03,46.715,56.966]},{"z":4.7,"b":[38.675,54.245,40.206,55.309],"l":[38.675,54.245,38.737,54.345,38.909,54.398,38.798,54.451,38.82,54.602,39.216,54.632,39.328,54.748,39.398,54.876,39.488,54.941,39.685,55.021,39.813,54.997,39.888,55.081,39.827,55.183,40.001,55.2,40.206,55.309]},{"z":4.7,"b":[41.783,54.871,42.467,55.119],"l":[41.783,55.119,41.886,55.059,42.167,54.949,42.44,55.013,42.467,54.871]},{"z":4.7,"b":[38.33,55.309,40.206,56.697],"l":[40.206,55.309,40.137,55.434,40.141,55.517,40.037,55.546,40.085,55.613,39.9,55.734,39.823,55.833,39.555,55.776,39.358,55.761,39.31,55.831,39.123,55.83,39.016,55.925,38.839,55.901,38.729,55.953,38.606,55.959,38.562,56.083,38.403,56.264,38.412,56.369,38.33,56.562,38.374,56.697]},{"z":4.7,"b":[38.672,53.565,38.909,54.245],"l":[38.895,53.565,38.909,53.753,38.837,53.76,38.795,53.872,38.841,53.919,38.714,54.001,38.694,54.075,38.764,54.135,38.672,54.177,38.675,54.245]},{"z":4.7,"b":[41.783,55.119,42.906,56.42],"l":[42.588,56.42,42.761,56.306,42.755,56.22,42.906,56.152,42.686,56.122,42.694,56.003,42.565,55.969,42.486,55.852,42.38,55.777,42.281,55.77,42.056,55.613,42.068,55.481,42.117,55.422,41.933,55.33,41.859,55.241,41.87,55.158,41.783,55.119]},{"z":4.7,"b":[40.206,55.109,41.783,55.34],"l":[41.783,55.119,41.658,55.109,41.497,55.157,41.412,55.131,41.21,55.221,41.047,55.183,40.965,55.26,40.729,55.204,40.612,55.207,40.517,55.328,40.345,55.34,40.206,55.309]},{"z":4.7,"b":[37.626,58.49,40.926,58.954],"l":[37.626,58.49,37.717,58.622,37.941,58.641,38.048,58.736,38.478,58.706,38.55,58.777,38.916,58.744,39,58.846,38.94,58.944,39.119,58.954,39.5,58.889,39.62,58.895,39.969,58.759,40.161,58.559,40.293,58.544,40.565,58.591,40.926,58.579]},{"z":4.7,"b":[39.417,56.368,42.588,56.822],"l":[39.417,56.772,39.679,56.822,39.859,56.78,39.879,56.646,39.943,56.53,40.156,56.434,40.491,56.584,40.715,56.598,40.934,56.502,41.053,56.475,41.934,56.517,41.958,56.391,42.066,56.368,42.289,56.471,42.469,56.421,42.588,56.42]},{"z":4.7,"b":[38.257,56.697,38.374,56.77],"l":[38.374,56.697,38.257,56.77]},{"z":4.7,"b":[37.35,56.77,38.306,58.49],"l":[38.257,56.77,38.154,56.832,38.199,56.894,38.25,57.129,38.143,57.197,38.306,57.276,38.27,57.371,38.148,57.337,37.984,57.359,37.794,57.443,37.81,57.608,37.391,57.828,37.367,57.977,37.484,57.981,37.549,58.053,37.673,58.083,37.562,58.165,37.35,58.239,37.449,58.408,37.644,58.429,37.626,58.49]},{"z":4.7,"b":[39.387,56.772,40.445,57.293],"l":[40.445,57.276,40.327,57.293,40.193,57.213,40.064,57.219,39.842,57.177,39.844,57.129,39.52,57.027,39.527,56.956,39.387,56.835,39.417,56.772]},{"z":4.7,"b":[38.374,56.538,39.417,56.772],"l":[39.417,56.772,39.31,56.684,39.337,56.609,39.272,56.538,39.121,56.594,39,56.543,38.847,56.598,38.629,56.581,38.374,56.697]},{"z":4.7,"b":[40.445,57.276,43.727,57.75],"l":[43.727,57.466,43.542,57.567,43.312,57.517,43.264,57.424,42.973,57.388,42.853,57.394,42.851,57.667,42.726,57.702,42.65,57.635,42.404,57.633,42.336,57.579,42.186,57.56,42.026,57.589,41.886,57.686,41.63,57.75,41.567,57.704,41.604,57.524,41.544,57.459,41.434,57.472,41.27,57.426,41.17,57.435,41.073,57.348,40.571,57.316,40.445,57.276]},{"z":4.7,"b":[40.385,57.276,41.164,58.579],"l":[40.445,57.276,40.448,57.441,40.385,57.498,40.499,57.58,40.663,57.581,40.623,57.655,40.626,57.94,40.744,58.052,40.885,58.126,40.889,58.198,41.164,58.361,41.065,58.411,41.034,58.522,40.926,58.579]},{"z":4.7,"b":[53.785,56.359,54.352,58.44],"l":[53.785,58.44,53.886,58.319,53.81,58.228,54.087,57.994,54.156,57.706,54.096,57.607,54.168,57.559,54.188,57.463,54.158,57.314,54.314,57.291,54.295,57.084,54.341,57.012,54.099,56.966,54.088,56.771,53.869,56.713,53.959,56.632,54.168,56.534,54.245,56.432,54.352,56.359]},{"z":4.7,"b":[51.774,60.242,59.349,61.682],"l":[59.349,61.682,59.114,61.615,59.023,61.552,58.692,61.503,57.204,61.511,57.086,61.488,56.702,61.525,56.545,61.435,56.391,61.42,56.257,61.206,55.841,61.25,55.69,61.086,55.316,61.123,55.214,61.019,55.062,61.009,54.98,60.864,53.853,60.98,53.791,60.848,53.354,60.893,53.349,61.037,52.876,61.094,52.79,60.949,52.442,60.98,52.355,60.844,51.89,60.876,51.774,60.606,52.134,60.547,52.138,60.473,52.324,60.432,52.298,60.242]},{"z":4.7,"b":[52.298,58.44,53.922,60.242],"l":[52.298,60.242,52.454,60.201,53.358,60.166,53.457,60.205,53.608,60.143,53.707,60.001,53.658,59.824,53.592,59.709,53.418,59.678,53.354,59.501,53.206,59.364,53.335,59.185,53.434,59.144,53.877,59.093,53.767,59.022,53.726,58.917,53.821,58.821,53.922,58.783,53.75,58.697,53.705,58.595,53.785,58.44]},{"z":4.7,"b":[51.12,56.144,53.785,58.537],"l":[53.785,58.44,53.498,58.45,53.34,58.389,53.165,58.456,53.13,58.537,52.921,58.534,52.87,58.411,52.329,58.45,52.167,58.49,51.971,58.464,51.796,58.361,51.791,58.281,51.673,58.166,51.766,58.141,51.793,58.043,51.885,57.986,51.852,57.842,51.924,57.817,51.862,57.647,51.748,57.546,51.64,57.544,51.601,57.457,51.397,57.478,51.181,57.447,51.12,57.265,51.193,57.203,51.227,57.067,51.359,56.927,51.54,56.883,51.4,56.767,51.379,56.68,51.206,56.67,51.145,56.515,51.273,56.44,51.32,56.329,51.42,56.284,51.436,56.144]},{"z":4.7,"b":[46.715,56.623,50.101,57.291],"l":[46.715,56.966,46.823,56.93,46.951,56.945,47.161,56.901,47.174,56.814,47.509,56.893,47.77,57.06,47.91,56.983,48.202,57.028,48.293,57.163,48.451,57.128,48.569,57.172,48.822,57.164,48.961,57.089,49.039,57.222,49.035,57.288,49.213,57.291,49.15,57.15,49.285,57.039,49.411,57.025,49.529,57.073,49.7,57.073,49.742,56.9,50.017,56.864,50.101,56.792,50.072,56.623]},{"z":4.7,"b":[48.419,59.208,52.298,60.854],"l":[48.419,60.854,48.538,60.564,48.519,60.476,48.755,60.361,48.497,60.218,48.456,60.115,48.493,59.989,48.508,59.715,48.562,59.699,49.073,59.66,49.039,59.5,49.151,59.506,49.363,59.416,49.525,59.397,49.527,59.235,49.664,59.208,49.795,59.233,49.834,59.638,50.04,59.682,49.999,59.777,50.212,59.754,50.386,59.84,50.83,59.871,51.075,60.061,51.307,60.028,51.421,59.943,51.521,59.946,51.802,60.115,51.9,60.244,52.061,60.317,52.298,60.242]},{"z":4.7,"b":[35.174,55.948,38.257,56.944],"l":[38.257,56.77,38.015,56.774,37.974,56.854,37.656,56.944,37.553,56.906,37.499,56.785,37.231,56.773,37.157,56.683,37.135,56.56,37.055,56.518,36.833,56.583,36.537,56.475,36.173,56.408,36.096,56.347,35.983,56.387,35.783,56.392,35.645,56.474,35.458,56.448,35.548,56.371,35.484,56.242,35.379,56.174,35.317,56.054,35.21,56.019,35.174,55.948]},{"z":4.7,"b":[36.904,54.991,37.843,55.904],"l":[37.12,54.991,37.216,55.084,37.425,55.184,37.463,55.379,37.566,55.358,37.659,55.569,37.813,55.649,37.843,55.763,37.801,55.838,37.611,55.904,37.431,55.878,37.348,55.744,37.414,55.66,37.211,55.602,37.175,55.448,36.914,55.426,37.014,55.301,36.904,55.229]},{"z":4.7,"b":[34.73,58.525,36.173,59.103],"l":[36.173,58.525,36.126,58.695,35.836,58.745,35.751,58.827,35.398,58.844,35.266,58.765,35.125,58.815,35.131,58.937,34.831,58.991,34.879,59.043,34.73,59.103]},{"z":4.7,"b":[31,56.931,36.173,58.525],"l":[36.173,58.525,35.941,58.455,35.935,58.423,35.552,58.448,35.394,58.431,35.279,58.359,35.269,58.271,35.035,58.2,34.786,58.242,34.684,58.197,34.651,58.081,34.488,58.146,34.142,58.208,33.869,58.144,33.719,58.158,33.545,58.047,33.686,58.003,33.701,57.927,33.581,57.858,33.537,57.72,33.325,57.655,33.309,57.566,33.216,57.454,33.046,57.437,32.807,57.46,32.668,57.346,32.272,57.195,32.133,57.184,31.967,57.117,31.722,57.106,31.678,57.013,31.546,56.94,31.307,56.969,31.118,56.931,31,56.965]},{"z":4.7,"b":[46.349,59.611,47.114,60.854],"l":[47.043,60.854,47.084,60.819,46.959,60.66,47.049,60.575,46.862,60.507,46.807,60.318,46.75,60.261,46.441,60.271,46.349,60.214,46.404,60.107,46.608,60.067,47.036,60.1,46.923,59.844,46.935,59.789,47.113,59.753,47.114,59.611]},{"z":4.7,"b":[36.173,58.49,37.626,58.866],"l":[37.626,58.49,37.188,58.771,37.122,58.866,36.864,58.843,36.816,58.758,36.669,58.685,36.521,58.558,36.324,58.5,36.173,58.525]},{"z":4.7,"b":[37.259,54.245,38.675,54.836],"l":[37.259,54.836,37.461,54.822,37.564,54.737,37.758,54.736,37.75,54.82,37.939,54.832,38.039,54.714,38.219,54.66,38.462,54.64,38.49,54.58,38.39,54.469,38.514,54.364,38.521,54.289,38.675,54.245]},{"z":4.7,"b":[46.214,55.829,48.412,56.335],"l":[46.214,56.05,46.402,56.119,46.658,56.12,47.056,56.288,47.277,56.335,47.385,56.33,47.437,56.188,47.529,56.154,47.829,56.145,47.905,56.114,48.024,55.937,48.285,55.846,48.412,55.829]},{"z":4.7,"b":[45.651,56.05,46.715,56.966],"l":[46.715,56.966,46.601,56.92,46.336,56.952,46.313,56.907,45.959,56.872,45.859,56.629,45.651,56.591,45.663,56.461,45.789,56.425,45.949,56.433,45.946,56.325,45.874,56.259,46.068,56.182,46.092,56.056,46.214,56.05]},{"z":4.7,"b":[45.92,55.042,46.384,56.05],"l":[46.214,56.05,46.099,55.941,46.094,55.808,45.92,55.688,46.048,55.617,46.14,55.524,46.244,55.524,46.384,55.463,46.254,55.385,46.229,55.273,46.098,55.204,46.056,55.042]},{"z":4.7,"b":[78.483,62.178,86.028,72.395],"l":[78.483,72.395,78.697,72.18,79.419,72.043,79.695,72.058,80.218,71.942,80.339,71.89,80.036,71.831,80.119,71.675,79.868,71.602,79.383,71.615,79.333,71.481,79.143,71.422,79.245,71.317,79.565,71.277,79.954,71.134,80.403,71.091,80.55,71.044,80.552,70.9,80.68,70.805,80.608,70.71,80.753,70.635,80.569,70.461,80.177,70.43,80.095,70.353,79.63,70.221,79.505,70.115,79.208,70.053,79.171,69.978,78.913,69.861,79.116,69.732,79.585,69.588,79.644,69.491,79.854,69.355,80.26,69.356,80.734,69.308,80.967,69.209,81.324,69.286,81.611,69.286,81.626,69.41,81.783,69.429,81.929,69.349,81.772,69.233,81.893,69.183,82.257,69.157,82.497,69.111,82.333,69.062,82.481,68.995,82.546,68.84,83.015,68.684,82.867,68.628,82.536,68.611,82.71,68.414,82.681,68.283,82.55,68.179,82.395,68.177,82.354,67.944,82.059,67.948,81.731,67.908,81.882,67.799,82.1,67.724,82.11,67.609,82.387,67.524,82.256,67.409,82.353,67.328,82.117,67.244,82.796,66.977,83.042,66.891,83.162,66.818,83.293,66.663,83.078,66.579,83.091,66.459,83.262,66.389,83.525,66.173,83.381,66.14,83.296,66.058,83.521,65.908,83.518,65.822,84.031,65.793,84.168,65.703,84.319,65.684,84.291,65.613,84.43,65.551,84.55,65.433,84.586,65.311,84.523,65.213,84.388,65.175,84.313,65.098,84.387,64.994,84.279,64.94,84.47,64.89,84.82,64.931,84.968,64.891,85.184,64.751,85.61,64.828,85.859,64.756,85.812,64.656,85.916,64.588,85.845,64.501,85.951,64.329,86.028,64.28,85.934,64.123,85.948,64.053,85.534,63.937,85.373,63.808,85.333,63.701,85.097,63.534,85.276,63.522,85.378,63.467,85.41,63.353,85.641,63.374,85.469,63.149,85.584,63.054,85.518,62.933,85.324,62.869,85.225,62.801,85.09,62.638,84.958,62.593,84.861,62.451,84.746,62.41,84.436,62.178]},{"z":4.7,"b":[62.018,62.178,84.436,65.717],"l":[84.436,62.178,84.289,62.276,84.237,62.377,84.096,62.375,83.798,62.56,83.533,62.525,83.311,62.457,83.034,62.53,83.01,62.595,82.741,62.68,82.733,62.757,82.288,62.775,82.073,62.8,81.84,62.692,81.6,62.773,81.349,62.821,81.195,62.95,81.075,63.117,80.584,63.083,80.574,63.019,80.073,62.826,79.915,62.792,79.82,62.604,78.793,62.615,78.379,62.568,77.967,62.478,77.889,62.557,77.327,62.71,77.132,62.813,77.026,62.975,76.623,63.038,76.289,62.95,76.24,63.019,75.803,63.114,75.7,63.091,75.364,63.108,74.915,63.032,74.507,63.043,74.241,63.172,73.368,63.207,73.284,63.315,73.104,63.436,72.907,63.393,72.655,63.28,72.161,63.299,71.964,63.197,71.805,63.219,71.608,63.185,71.569,63.551,71.388,63.675,70.914,63.692,70.813,63.822,70.638,63.903,70.57,64.015,70.755,64.1,70.769,64.194,70.672,64.237,70.473,64.252,70.307,64.344,70,64.316,69.825,64.341,69.693,64.4,69.39,64.389,69.244,64.466,69.015,64.472,68.859,64.436,68.99,64.33,68.811,64.221,68.425,64.296,68.179,64.176,67.936,64.118,67.809,64.032,67.679,64.079,67.281,64.081,67.171,64.163,66.819,64.226,66.796,64.321,66.892,64.383,66.936,64.487,66.682,64.557,66.434,64.515,66.146,64.586,65.883,64.569,65.4,64.571,65.176,64.446,64.995,64.466,64.761,64.427,64.622,64.439,64.444,64.356,64.016,64.357,63.896,64.287,63.611,64.268,63.417,64.324,63.503,64.371,63.467,64.451,63.215,64.526,62.699,64.492,62.532,64.56,62.6,64.645,62.803,64.726,62.799,64.847,62.679,64.878,62.801,65.051,62.817,65.292,62.517,65.339,62.374,65.449,62.411,65.515,62.278,65.623,62.018,65.717]},{"z":4.7,"b":[163.137,61.815,174.468,64.931],"l":[174.454,61.815,174.468,61.979,174.344,62.05,174.117,62.074,174.112,62.189,174.015,62.269,174.093,62.34,174.08,62.447,173.872,62.441,173.798,62.518,173.504,62.534,173.027,62.375,172.853,62.331,172.705,62.402,172.417,62.432,172.319,62.404,172.055,62.467,171.47,62.312,171.218,62.369,170.901,62.279,170.382,62.311,170.262,62.285,170.159,62.342,170.086,62.492,169.986,62.545,170.01,62.677,169.842,62.655,169.657,62.709,169.514,62.814,169.127,62.973,168.939,62.944,168.51,63.054,168.544,63.126,168.796,63.303,169.099,63.44,169.205,63.516,169.509,63.584,169.582,63.643,169.549,63.811,169.364,63.88,169.344,63.95,169.079,63.959,168.874,64.021,168.778,64.185,168.638,64.211,168.499,64.317,168.198,64.339,168.026,64.286,167.815,64.351,167.714,64.341,167.498,64.481,167.414,64.482,167.237,64.624,167,64.55,166.807,64.533,166.406,64.601,166.199,64.609,166.035,64.559,165.743,64.679,165.684,64.754,165.481,64.743,165.329,64.775,165.263,64.851,165.02,64.837,165.004,64.69,164.882,64.686,164.798,64.772,164.216,64.925,163.873,64.865,163.52,64.931,163.345,64.818,163.137,64.77]},{"z":4.7,"b":[157.692,66.359,162.847,69.677],"l":[158.529,66.359,158.368,66.459,158.47,66.488,158.637,66.619,158.716,66.731,158.905,66.803,158.703,67.055,158.299,67.148,157.958,67.258,157.793,67.358,157.876,67.413,157.692,67.545,157.806,67.705,158.329,67.754,158.27,67.823,158.093,67.84,158.277,68.076,158.633,68.142,158.934,68.144,159.111,68.191,159.693,68.239,159.85,68.277,160.997,68.301,161.226,68.384,161.406,68.412,161.711,68.373,162.178,68.355,162.418,68.309,162.535,68.363,162.581,68.501,162.723,68.605,162.606,68.714,162.594,68.796,162.755,68.802,162.847,68.869,162.554,68.991,162.524,69.118,162.774,69.203,162.521,69.325,162.315,69.547,162.432,69.677]},{"z":4.6,"b":[75.222,11.96,75.313,12.076],"l":[75.313,11.96,75.262,12.076,75.222,12.022]},{"z":4.6,"b":[79.705,10.81,79.847,10.986],"l":[79.847,10.81,79.767,10.889,79.705,10.91,79.737,10.986,79.847,10.98]},{"z":4.6,"b":[79.686,11.833,79.85,12],"l":[79.811,11.833,79.727,11.842,79.686,12,79.791,11.917,79.85,11.955]},{"z":4.6,"b":[74.893,11.667,76.416,12.756],"l":[76.416,11.667,76.389,11.744,76.317,11.747,76.177,11.857,76.115,11.857,76.056,11.964,75.961,11.929,75.846,11.951,75.776,12.01,75.761,12.069,75.629,12.102,75.537,12.169,75.426,12.292,75.389,12.295,75.312,12.469,75.102,12.679,74.893,12.756]},{"z":4.6,"b":[76.241,8.285,77.399,11.667],"l":[76.416,11.667,76.339,11.591,76.241,11.604,76.256,11.478,76.409,11.437,76.495,11.385,76.534,11.313,76.466,11.205,76.641,11.209,76.719,11.139,76.69,10.933,76.82,10.876,76.89,10.78,76.876,10.678,76.821,10.633,76.809,10.437,76.842,10.311,76.928,10.236,76.986,10.231,77.065,10.296,77.191,10.351,77.277,10.21,77.224,10.058,77.26,9.962,77.226,9.859,77.235,9.765,77.188,9.613,77.263,9.557,77.341,9.58,77.399,9.5,77.312,9.301,77.255,9.122,77.165,9.001,77.203,8.894,77.249,8.859,77.235,8.784,77.179,8.695,77.204,8.612,77.279,8.526,77.166,8.337,77.11,8.285]},{"z":4.6,"b":[77.257,15.953,77.621,18.308],"l":[77.551,18.308,77.6,18.115,77.563,18.071,77.621,17.987,77.62,17.926,77.5,17.812,77.525,17.746,77.464,17.713,77.412,17.631,77.493,17.578,77.583,17.579,77.599,17.487,77.374,17.223,77.465,17.043,77.433,16.935,77.411,16.721,77.432,16.594,77.371,16.524,77.257,16.449,77.294,16.407,77.533,16.361,77.491,16.262,77.484,16.002,77.446,15.953]},{"z":4.6,"b":[76.416,11.626,78.23,12.873],"l":[78.23,12.76,78.056,12.846,78.002,12.807,77.895,12.873,77.801,12.857,77.765,12.719,77.609,12.673,77.569,12.56,77.619,12.467,77.561,12.34,77.472,12.274,77.492,12.205,77.668,12.204,77.769,12.12,77.671,11.971,77.49,11.926,77.434,11.812,77.354,11.784,77.259,11.817,77.122,11.793,77.083,11.757,76.997,11.826,76.908,11.8,76.821,11.626,76.751,11.642,76.605,11.631,76.532,11.709,76.465,11.715,76.416,11.667]},{"z":4.6,"b":[73.68,15.614,74.101,15.772],"l":[74.101,15.64,73.99,15.614,73.943,15.632,73.908,15.721,73.813,15.772,73.68,15.713]},{"z":4.6,"b":[74.091,14.888,74.304,15.64],"l":[74.101,15.64,74.227,15.63,74.258,15.519,74.237,15.472,74.304,15.289,74.241,15.238,74.293,15.188,74.26,15.098,74.241,14.956,74.091,14.888]},{"z":4.6,"b":[77.503,18.308,80.27,19.866],"l":[80.27,18.714,80.091,18.697,80.002,18.786,79.96,18.867,79.934,19.053,79.862,19.081,79.944,19.169,79.931,19.212,79.965,19.402,79.931,19.483,79.865,19.509,79.796,19.594,79.735,19.603,79.539,19.534,79.42,19.536,79.266,19.59,79.198,19.462,79.059,19.547,79.024,19.542,78.873,19.65,78.848,19.725,78.78,19.777,78.712,19.774,78.534,19.825,78.478,19.799,78.383,19.866,78.322,19.863,78.384,19.685,78.333,19.643,78.303,19.483,78.24,19.46,78.174,19.375,78.168,19.264,78.064,19.264,77.992,19.323,77.92,19.332,77.855,19.228,77.834,19.121,77.752,19.046,77.912,18.835,77.839,18.81,77.781,18.704,77.703,18.669,77.68,18.57,77.592,18.548,77.503,18.382,77.551,18.308]},{"z":4.6,"b":[74.101,15.64,77.551,18.46],"l":[77.551,18.308,77.461,18.282,77.347,18.352,77.302,18.46,77.233,18.411,77.231,18.334,77.118,18.254,77.088,18.197,76.95,18.18,76.893,18.144,76.885,17.974,76.839,17.884,76.741,17.898,76.756,17.826,76.676,17.707,76.593,17.771,76.513,17.773,76.391,17.637,76.322,17.618,76.344,17.352,76.21,17.389,76.095,17.378,75.936,17.326,75.87,17.416,75.784,17.375,75.613,17.472,75.553,17.366,75.622,17.221,75.605,17.158,75.647,17.091,75.617,16.975,75.554,17.015,75.453,16.964,75.371,16.984,75.256,16.959,75.243,16.902,75.167,16.868,75.043,16.953,74.96,16.951,74.922,16.889,74.845,16.852,74.867,16.776,74.685,16.708,74.666,16.606,74.557,16.552,74.525,16.624,74.43,16.644,74.353,16.544,74.292,16.528,74.366,16.293,74.452,16.254,74.47,16.11,74.416,16.096,74.429,15.994,74.354,15.873,74.331,15.762,74.224,15.726,74.186,15.766,74.101,15.64]},{"z":4.6,"b":[74.129,21.075,80.679,22.023],"l":[74.129,21.952,74.205,21.925,74.284,21.945,74.408,22.023,74.486,21.946,74.512,21.727,74.708,21.611,74.894,21.612,75.048,21.554,75.13,21.438,75.312,21.381,75.535,21.372,75.814,21.383,75.878,21.4,76.083,21.368,76.143,21.193,76.109,21.153,76.187,21.086,76.287,21.075,76.437,21.108,76.498,21.18,76.614,21.202,76.654,21.251,76.633,21.341,76.73,21.413,76.786,21.502,76.794,21.577,76.917,21.618,77.029,21.694,77.116,21.724,77.219,21.701,77.333,21.774,77.461,21.77,77.552,21.697,77.555,21.542,77.457,21.549,77.433,21.499,77.482,21.424,77.578,21.384,77.713,21.38,77.788,21.423,77.871,21.398,78.029,21.438,78.193,21.551,78.413,21.606,78.428,21.508,78.514,21.527,78.696,21.475,78.904,21.509,78.922,21.572,79.216,21.655,79.24,21.713,79.413,21.69,79.533,21.619,79.552,21.55,79.65,21.582,79.798,21.588,79.84,21.534,80.005,21.548,80.187,21.639,80.31,21.58,80.418,21.447,80.626,21.326,80.679,21.351]},{"z":4.6,"b":[80.27,18.714,80.894,21.351],"l":[80.679,21.351,80.626,21.243,80.485,21.168,80.435,20.999,80.481,20.932,80.552,20.919,80.573,20.698,80.503,20.633,80.622,20.601,80.622,20.326,80.424,20.234,80.426,20.146,80.531,20.135,80.557,20.048,80.531,19.921,80.425,19.893,80.538,19.811,80.566,19.74,80.676,19.677,80.713,19.593,80.831,19.547,80.894,19.484,80.832,19.345,80.717,19.263,80.617,19.319,80.576,19.392,80.397,19.234,80.367,19.117,80.272,18.979,80.344,18.851,80.275,18.777,80.27,18.714]},{"z":4.7,"b":[41.713,44.976,45.712,46.24],"l":[45.706,44.976,45.712,45.014,45.586,45.158,45.353,45.243,44.706,45.506,44.421,45.527,44.207,45.61,44.148,45.653,44.097,45.774,43.878,45.945,43.742,45.979,43.568,45.981,43.393,46.016,43.237,46.112,42.991,46.18,42.905,46.24,42.835,46.156,42.671,46.088,42.533,45.998,42.317,45.981,42.34,46.107,42.177,46.099,42.15,45.949,41.713,45.996]},{"z":4.6,"b":[83.337,22.539,84.352,24.14],"l":[83.97,22.539,83.996,22.593,84.191,22.701,84.228,22.783,84.352,22.886,84.292,22.997,84.115,22.999,84.089,23.085,84.029,23.146,84.039,23.309,84.015,23.383,83.947,23.37,83.952,23.475,84.015,23.554,83.991,23.632,83.906,23.565,83.781,23.586,83.721,23.676,83.735,23.738,83.697,23.819,83.557,23.936,83.522,24.066,83.442,24.122,83.337,24.14]},{"z":4.7,"b":[84.26,56.833,89.391,60.856],"l":[88.624,56.833,88.721,57.055,88.53,57.096,88.648,57.212,88.739,57.233,88.861,57.432,89.077,57.515,89.167,57.616,89.38,57.637,89.354,57.796,89.391,57.878,89.328,57.95,88.851,57.961,88.687,58.037,88.156,58.107,88.014,58.268,87.918,58.5,87.932,58.526,88.383,58.909,88.797,59.01,88.826,59.035,88.62,59.199,88.586,59.299,87.936,59.266,87.878,59.289,87.554,59.641,87.492,59.674,87.214,59.685,87.084,59.883,86.625,59.95,85.968,59.956,85.496,59.892,84.692,59.905,84.531,59.975,84.611,60.059,84.632,60.206,84.781,60.356,84.708,60.459,84.355,60.791,84.26,60.856]},{"z":4.7,"b":[84.26,60.856,85.956,62.178],"l":[84.26,60.856,84.648,61.001,85.671,61.289,85.644,61.388,85.771,61.443,85.95,61.465,85.956,61.55,85.854,61.595,85.722,61.575,85.48,61.628,85.386,61.698,85.273,61.683,84.919,61.797,84.681,61.815,84.52,61.945,84.551,62.001,84.436,62.178]},{"z":4.7,"b":[88.609,52.183,91.846,55.446],"l":[90.477,52.183,90.7,52.458,91.038,52.66,91.242,52.747,91.396,52.868,91.408,52.947,91.351,53.077,91.66,53.158,91.841,53.288,91.846,53.352,91.744,53.447,91.47,53.514,91.472,53.567,91.333,53.758,91.327,53.817,91.465,53.966,91.464,54.057,91.21,54.338,91.159,54.488,90.855,54.627,90.915,54.791,90.84,54.839,90.715,54.831,90.685,54.905,90.458,54.871,90.436,55.016,90.262,55.05,90.129,55.105,89.904,55.086,89.658,55.003,89.55,55.046,89.201,54.99,89.147,55.076,88.966,55.185,88.95,55.289,88.76,55.344,88.609,55.446]},{"z":4.7,"b":[88.517,55.446,89.443,56.833],"l":[88.609,55.446,88.682,55.516,89.062,55.693,89.17,55.704,89.363,55.773,89.443,55.876,89.33,55.902,89.199,56.135,89.056,56.21,89.041,56.305,88.913,56.31,88.861,56.366,88.734,56.376,88.592,56.444,88.625,56.517,88.537,56.545,88.517,56.627,88.661,56.636,88.721,56.732,88.624,56.833]},{"z":4.7,"b":[37.773,60.65,47.043,61.522],"l":[47.043,60.854,46.753,60.907,46.737,60.986,46.527,61.051,46.285,60.968,45.878,61.016,45.767,61.055,45.792,61.154,45.723,61.179,45.332,61.189,45.229,60.904,44.455,60.947,44.206,60.972,44.158,61.082,43.776,61.021,43.715,60.885,43.536,60.853,43.511,60.767,43.15,60.806,43.049,60.74,42.823,60.855,42.65,60.863,42.395,60.769,42.024,60.836,41.801,60.899,41.519,60.899,41.394,60.858,41.412,60.791,41.34,60.676,41.059,60.65,40.819,60.711,40.757,60.774,40.534,60.725,39.923,60.652,39.764,60.737,39.648,60.746,39.338,60.684,39.068,60.714,38.948,60.803,38.789,60.713,38.633,60.723,38.574,60.799,38.43,60.805,38.418,60.874,38.213,60.924,38.166,61.133,37.963,61.16,37.857,61.285,37.773,61.522]},{"z":4.7,"b":[95.653,53.639,108.32,64.224],"l":[108.027,64.224,108.224,64.203,108.32,64.039,108.263,63.996,107.68,63.989,107.55,63.857,107.372,63.866,107.202,63.957,107.053,63.892,106.764,63.996,106.68,63.931,106.793,63.842,106.629,63.697,106.691,63.636,106.517,63.483,106.725,63.375,106.661,63.297,106.438,63.32,106.368,63.272,106.403,63.113,106.126,63.048,106.225,62.894,106.435,62.847,106.525,62.741,106.495,62.692,106.726,62.588,106.714,62.488,106.479,62.329,106.397,62.314,106.443,62.185,106.375,62.103,106.404,62.031,106.218,62.003,105.902,61.835,105.941,61.685,105.823,61.631,105.718,61.65,105.55,61.566,105.367,61.53,105.253,61.42,104.957,61.398,104.904,61.368,105.005,61.198,104.837,61.176,104.635,61.036,104.571,60.955,104.629,60.89,104.46,60.839,104.594,60.735,104.523,60.645,104.676,60.4,104.889,60.302,105.221,60.276,105.321,60.295,105.476,60.254,105.424,60.123,105.485,60.022,105.377,59.822,105.186,59.858,105.035,59.836,105.005,59.766,104.799,59.691,104.883,59.525,104.986,59.461,105.225,59.429,105.196,59.303,105.288,59.264,105.353,59.108,104.948,58.959,104.759,58.82,104.872,58.719,104.787,58.666,104.458,58.602,104.374,58.713,104.22,58.758,103.949,58.774,103.89,58.903,103.693,58.923,103.375,59.034,103.246,59.18,103.239,59.284,103.083,59.264,102.999,59.315,102.759,59.199,102.588,59.203,102.455,59.153,102.407,59.067,102.439,58.944,102.537,58.897,102.565,58.797,102.52,58.728,102.311,58.642,102.162,58.516,101.774,58.453,101.678,58.407,101.531,58.225,101.298,58.219,101.208,58.142,101.137,57.985,100.971,57.87,100.846,57.845,100.885,57.77,100.801,57.673,100.955,57.555,100.911,57.452,100.716,57.413,100.638,57.477,100.419,57.542,100.275,57.805,100.129,57.867,100.079,58.045,100.017,58.081,99.794,58.063,99.703,58.086,99.498,57.929,99.411,57.796,99.08,57.77,98.962,57.723,98.768,57.791,98.546,57.791,97.987,57.822,97.934,57.811,97.355,57.061,97.489,56.964,97.469,56.827,97.547,56.793,97.765,56.788,97.762,56.559,97.868,56.555,97.882,56.427,97.841,56.393,97.591,56.387,97.565,56.19,97.44,56.147,97.352,56.051,97.125,56.102,96.986,56.018,96.96,55.861,96.76,55.746,96.753,55.651,96.82,55.558,96.918,55.33,96.72,55.265,96.685,55.085,96.586,55.019,96.712,54.927,96.673,54.806,96.565,54.697,96.552,54.58,96.495,54.511,96.351,54.562,96.221,54.543,96.039,54.558,95.981,54.452,95.897,54.389,95.717,54.388,95.653,54.282,95.678,54.234,95.919,54.156,96.079,54.063,96.126,53.972,96.406,53.783,96.505,53.684,96.684,53.639]},{"z":4.7,"b":[45.442,60.854,50.467,66.119],"l":[49.006,66.119,48.989,65.269,49.021,65.238,49.567,65.292,49.663,65.255,50.063,64.89,50.16,64.847,50.467,64.792,50.348,64.538,50.273,64.513,49.627,64.592,49.45,64.463,48.95,64.491,48.84,64.357,48.518,64.356,48.408,64.224,48.196,64.239,48.116,64.348,46.611,64.327,46.323,64.265,45.825,64.284,45.442,64.188,45.593,64.064,45.884,63.964,46.11,63.946,46.436,63.888,46.819,63.747,46.97,63.64,47.026,63.531,46.937,63.352,47.091,63.246,47.366,63.167,47.555,63.148,47.516,63.037,47.4,62.955,47.083,62.84,47.238,62.636,47.235,62.351,47.386,62.32,47.413,62.193,47.621,62.185,47.65,62.297,47.751,62.322,48.291,62.322,48.237,62.473,48.366,62.729,48.629,62.706,48.714,62.816,48.875,62.829,49.609,62.77,49.369,62.149,49.213,62.091,49.052,61.691,49.097,61.658,49.417,61.618,49.461,61.586,49.302,61.176,49.098,61.175,48.889,61.141,48.509,61.048,48.446,60.984,48.419,60.854]},{"z":4.7,"b":[47.043,60.854,48.419,61.059],"l":[48.419,60.854,47.984,60.896,47.965,60.999,47.568,61.059,47.245,61.056,47.104,60.956,47.043,60.854]},{"z":4.7,"b":[108.027,58.223,119.132,64.282],"l":[119.108,58.223,119.07,58.341,119.132,58.472,118.98,58.543,118.821,58.574,118.893,58.816,118.795,58.935,118.84,59.016,118.707,59.064,118.689,59.191,118.84,59.3,118.734,59.42,118.407,59.512,118.336,59.596,118.058,59.583,117.906,59.444,117.779,59.528,117.598,59.472,117.352,59.497,117.089,59.588,117.19,59.646,117.228,59.798,117.065,59.908,117.296,60.019,117.077,60.029,116.954,60.169,116.722,60.266,116.582,60.36,116.075,60.407,115.999,60.457,115.842,60.468,115.686,60.526,115.395,60.487,115.112,60.395,115.007,60.269,114.836,60.19,114.697,60.219,114.54,60.102,114.545,59.985,114.309,59.879,114.161,59.748,114.03,59.681,113.849,59.682,113.779,59.609,113.592,59.565,113.605,59.499,113.435,59.402,113.465,59.266,113.264,59.157,113.152,59.164,112.728,59.083,112.554,59.033,112.612,58.954,112.451,58.924,112.465,59.168,112.631,59.31,112.435,59.326,112.218,59.5,111.933,59.272,111.776,59.276,111.625,59.207,111.47,59.267,111.316,59.257,111.164,59.189,110.713,59.259,110.58,59.191,110.589,59.131,110.471,59.034,110.287,58.985,110.119,58.98,109.636,59.066,109.499,59.294,109.268,59.316,109.269,59.449,109.517,59.63,109.504,59.764,109.65,59.871,109.764,60.027,109.694,60.129,109.741,60.233,109.917,60.451,110.116,60.587,110.057,60.674,110.268,60.702,110.24,60.831,110.458,60.988,110.517,61.091,110.481,61.159,110.169,61.164,110.094,61.263,109.792,61.325,109.846,61.424,109.851,61.557,109.62,61.718,109.598,61.87,109.819,62.008,110,62.164,109.996,62.278,109.918,62.406,109.557,62.431,109.306,62.484,109.512,62.777,109.614,62.874,109.465,62.95,109.453,63.149,109.215,63.442,109.053,63.549,108.857,63.537,108.547,63.599,108.16,63.563,108.112,63.617,108.265,63.673,108.279,63.796,108.687,63.795,108.766,63.86,108.735,63.977,108.495,64.116,108.533,64.228,108.468,64.282,108.027,64.224]},{"z":4.7,"b":[105.54,64.182,112.579,73.713],"l":[108.027,64.224,107.856,64.182,107.57,64.299,107.368,64.251,106.877,64.414,106.395,64.443,106.204,64.403,106.132,64.481,105.803,64.53,105.708,64.653,105.873,64.68,105.832,64.76,105.946,64.884,106.123,64.905,105.973,65.012,106.282,65.137,106.454,65.233,106.451,65.313,106.635,65.393,106.836,65.378,106.94,65.511,106.713,65.549,106.631,65.635,106.44,65.662,106.478,65.845,106.471,66.007,106.33,66.154,106.171,66.159,106.103,66.378,106.322,66.479,106.249,66.581,106.247,66.702,106.047,66.789,106.044,66.901,105.687,66.939,105.54,67.023,105.894,67.044,106.055,67.167,106.317,67.209,106.492,67.284,106.685,67.254,106.802,67.348,106.833,67.673,106.875,67.84,106.896,68.143,106.885,68.865,106.153,69.391,106.427,69.564,106.604,69.517,106.839,69.518,107.124,69.588,108.007,69.705,108.149,69.825,108.291,69.857,108.871,69.84,108.941,69.789,109.237,69.776,109.36,69.838,109.509,70.033,109.358,70.069,109.277,70.207,109.535,70.264,109.602,70.378,109.995,70.417,110.117,70.478,110.103,70.632,110.489,70.726,110.539,70.801,110.903,70.808,111.335,70.855,111.522,70.939,111.953,70.982,112.027,71.042,112.277,71.013,112.579,71.114,112.513,71.268,111.997,71.414,111.996,72.133,111.817,72.132,111.643,72.274,111.085,72.37,111.281,72.478,111.21,72.545,110.881,72.58,110.702,72.65,110.611,72.781,110.627,72.895,110.837,72.945,110.881,73.023,110.755,73.175,110.571,73.253,110.523,73.404,110.818,73.559,111.073,73.583,111.111,73.641,110.827,73.713]},{"z":4.7,"b":[110.801,73.759,111.303,73.883],"l":[110.801,73.759,111.303,73.883]},{"z":4.7,"b":[49.006,66.119,65.48,68.43],"l":[65.48,68.43,64.745,68.404,64.523,68.344,64.53,68.2,64.325,68.134,63.829,68.017,63.944,67.884,63.69,67.762,63.445,67.771,63.161,67.691,62.871,67.58,62.821,67.388,61.851,67.126,61.695,67.03,61.432,67,60.082,66.99,59.918,67.002,58.79,67,53.772,66.999,52.246,67.089,51.836,66.995,51.811,66.954,51.495,66.907,51.51,66.783,49.006,66.119]},{"z":4.7,"b":[44.214,65.808,49.006,66.408],"l":[49.006,66.119,48.156,66.13,47.753,65.941,47.764,65.861,47.661,65.808,47.407,65.836,47.11,65.821,46.347,66.028,45.832,66.088,45.741,66.068,44.578,66.307,44.476,66.37,44.214,66.408]},{"z":4.7,"b":[62.018,65.717,66.206,68.43],"l":[62.018,65.717,62.504,65.831,62.839,65.869,62.807,65.937,62.87,66.079,63.066,66.232,63.241,66.313,63.306,66.4,63.809,66.547,63.969,66.646,64.316,66.668,64.567,66.794,64.999,66.865,65.116,66.908,65.09,67.054,65.19,67.145,65.422,67.215,65.708,67.335,65.937,67.393,66.11,67.489,66.026,67.576,65.836,67.555,65.831,67.643,66.206,67.693,66.022,67.794,66.083,67.934,65.437,67.92,65.29,68.012,65.33,68.085,65.273,68.216,65.48,68.43]},{"z":4.7,"b":[64.545,68.43,65.654,69.271],"l":[65.48,68.43,65.654,68.556,65.517,68.588,65.326,68.73,65.315,68.803,65.064,68.814,64.904,68.871,64.554,68.871,64.545,69.032,64.99,69.205,65.038,69.271]},{"z":4.6,"b":[107.238,37.068,107.658,37.853],"l":[107.658,37.853,107.607,37.766,107.509,37.764,107.436,37.663,107.317,37.59,107.322,37.528,107.238,37.301,107.325,37.164,107.285,37.068]},{"z":4.6,"b":[110.237,34.616,111.225,39.385],"l":[111.11,39.385,111.225,39.311,111.118,39.067,110.995,39.004,110.98,38.817,110.872,38.621,110.899,38.545,110.86,38.468,110.774,38.426,110.671,38.317,110.58,38.287,110.511,38.202,110.515,37.955,110.589,37.919,110.653,37.813,110.753,37.746,110.789,37.561,110.746,37.48,110.63,37.397,110.687,37.357,110.665,37.282,110.46,37.051,110.389,37.022,110.398,36.781,110.483,36.593,110.496,36.484,110.459,36.349,110.473,36.264,110.447,36.142,110.511,35.887,110.56,35.849,110.576,35.713,110.62,35.597,110.425,35.294,110.335,35.198,110.26,34.945,110.237,34.801,110.252,34.663,110.336,34.616]},{"z":4.6,"b":[109.207,29.066,113.902,30.096],"l":[109.207,29.104,109.269,29.242,109.338,29.29,109.432,29.534,109.472,29.568,109.7,29.656,109.783,29.735,110.104,29.785,110.225,29.737,110.362,29.65,110.447,29.727,110.526,29.725,110.591,29.771,110.501,29.863,110.463,30.014,110.543,30.055,110.716,30.03,110.742,30.096,110.944,30.064,111.246,30.032,111.415,29.926,111.585,29.904,111.769,29.92,111.924,29.856,112.126,29.707,112.17,29.649,112.363,29.572,112.444,29.641,112.648,29.627,112.786,29.737,112.886,29.792,112.946,29.709,112.908,29.601,112.928,29.495,113.008,29.518,113.146,29.472,113.186,29.532,113.416,29.765,113.52,29.842,113.536,29.686,113.635,29.685,113.673,29.545,113.729,29.437,113.588,29.294,113.641,29.222,113.655,29.124,113.711,29.073,113.773,29.125,113.902,29.066]},{"z":4.6,"b":[113.562,25.448,114.227,29.066],"l":[113.902,29.066,113.905,28.956,114.012,28.913,114.143,28.789,114.113,28.616,114.057,28.564,114.177,28.507,114.227,28.409,114.184,28.293,114.092,28.258,114.028,28.184,114.014,28.106,113.916,28.021,113.737,27.982,113.706,27.938,113.701,27.819,113.611,27.699,113.562,27.591,113.6,27.368,113.667,27.33,113.783,27.371,113.851,27.346,113.824,27.229,113.754,27.142,113.892,26.945,113.832,26.827,113.865,26.656,114.085,26.577,114.068,26.454,114.015,26.385,113.999,26.283,113.939,26.209,114.063,26.178,114.162,26.221,114.214,26.17,114.041,26.069,114.018,25.935,113.907,25.75,113.918,25.66,113.982,25.582,113.928,25.448]},{"z":4.6,"b":[106.382,28.2,109.258,29.246],"l":[109.258,28.506,109.155,28.436,109.095,28.205,108.943,28.2,108.728,28.242,108.784,28.426,108.73,28.475,108.583,28.374,108.582,28.601,108.33,28.689,108.367,28.777,108.341,28.92,108.286,29.077,108.055,29.065,107.875,28.984,107.79,29.04,107.783,29.137,107.557,29.187,107.496,29.246,107.43,29.183,107.424,29.077,107.5,28.993,107.437,28.877,107.297,28.808,107.161,28.881,107.031,28.888,106.973,28.805,106.889,28.801,106.835,28.731,106.829,28.609,106.762,28.591,106.75,28.502,106.562,28.511,106.626,28.637,106.562,28.767,106.489,28.541,106.382,28.572]},{"z":4.6,"b":[108.791,26.039,109.507,28.506],"l":[109.258,28.506,109.264,28.322,109.354,28.265,109.299,28.095,109.363,28.03,109.31,27.97,109.328,27.808,109.413,27.739,109.447,27.65,109.418,27.56,109.318,27.501,109.277,27.439,109.206,27.445,109.015,27.262,108.9,27.189,108.897,27.147,108.791,27.083,108.874,27.018,108.95,27.027,109.008,27.094,109.095,27.134,109.156,27.08,109.231,27.152,109.438,27.131,109.499,27.062,109.507,26.976,109.444,26.901,109.505,26.825,109.397,26.755,109.294,26.727,109.377,26.646,109.403,26.537,109.315,26.426,109.297,26.313,109.399,26.27,109.428,26.099,109.457,26.039]},{"z":4.6,"b":[108.381,29.104,110.175,31.738],"l":[109.594,31.738,109.659,31.718,109.724,31.636,109.733,31.561,109.929,31.519,110.108,31.398,110.147,31.331,110.175,31.189,110.108,31.122,110.138,31,110.128,30.906,110.066,30.828,109.997,30.826,109.871,30.889,109.749,30.835,109.64,30.718,109.394,30.584,109.248,30.597,109.179,30.567,108.965,30.607,108.868,30.551,108.71,30.523,108.67,30.574,108.573,30.498,108.433,30.487,108.381,30.4,108.532,30.315,108.53,30.113,108.55,30.009,108.538,29.885,108.4,29.831,108.447,29.736,108.513,29.722,108.613,29.858,108.658,29.817,108.696,29.698,108.872,29.639,108.872,29.502,108.921,29.349,109.014,29.307,109.077,29.214,109.207,29.104]},{"z":4.6,"b":[109.207,28.506,109.285,29.104],"l":[109.207,29.104,109.275,29.049,109.246,28.931,109.247,28.786,109.281,28.729,109.213,28.629,109.285,28.573,109.258,28.506]},{"z":4.6,"b":[105.499,32.158,108.518,32.907],"l":[105.499,32.907,105.568,32.73,105.633,32.707,105.724,32.76,105.813,32.762,105.875,32.845,106.082,32.874,106.057,32.714,106.115,32.649,106.229,32.592,106.364,32.639,106.604,32.676,106.713,32.738,107.065,32.708,107.096,32.669,107.07,32.523,107.202,32.453,107.284,32.453,107.371,32.52,107.448,32.522,107.46,32.409,107.659,32.389,107.773,32.311,107.844,32.228,107.96,32.158,108.052,32.227,108.237,32.263,108.479,32.25,108.518,32.198]},{"z":4.6,"b":[108.518,31.705,109.594,32.198],"l":[108.518,32.198,108.789,32.045,109.056,31.941,109.189,31.84,109.253,31.744,109.336,31.705,109.594,31.738]},{"z":4.6,"b":[105.297,27.678,106.382,28.754],"l":[106.382,28.572,106.339,28.48,106.269,28.557,106.189,28.585,105.984,28.754,105.892,28.609,105.683,28.583,105.621,28.482,105.656,28.441,105.667,28.318,105.847,28.269,105.906,28.151,106.009,28.14,106.109,28.175,106.199,28.124,106.215,28.056,106.299,28,106.345,27.835,106.248,27.775,106.104,27.788,106.039,27.757,105.876,27.745,105.647,27.678,105.524,27.775,105.361,27.76,105.297,27.722]},{"z":4.6,"b":[97.361,32.403,101.778,34.005],"l":[97.716,32.544,97.664,32.56,97.431,32.7,97.384,32.779,97.361,32.897,97.469,32.983,97.504,33.046,97.476,33.12,97.501,33.194,97.616,33.328,97.724,33.406,97.539,33.454,97.477,33.562,97.391,33.611,97.384,33.87,97.61,33.931,97.696,34.005,97.745,33.865,97.819,33.864,98.035,33.96,98.212,33.939,98.302,33.846,98.458,33.84,98.646,33.676,98.633,33.608,98.736,33.49,98.773,33.303,98.852,33.175,99.101,33.072,99.225,32.999,99.289,32.887,99.509,32.835,99.699,32.745,99.729,32.758,99.769,32.922,99.841,32.957,100.047,32.935,100.137,32.848,100.159,32.782,100.119,32.671,100.223,32.637,100.255,32.726,100.399,32.758,100.498,32.669,100.561,32.563,100.473,32.485,100.531,32.403,100.603,32.451,100.643,32.594,100.697,32.686,100.902,32.63,101.049,32.676,101.171,32.688,101.237,32.808,101.232,32.858,101.131,32.938,101.164,33.128,101.22,33.171,101.445,33.236,101.644,33.128,101.727,33.268,101.665,33.32,101.691,33.42,101.778,33.53]},{"z":4.6,"b":[80.679,21.351,82.801,23.999],"l":[82.801,23.999,82.768,23.929,82.658,23.885,82.651,23.845,82.532,23.775,82.077,23.801,81.937,23.841,81.778,23.802,81.682,23.885,81.622,23.834,81.686,23.69,81.591,23.561,81.644,23.482,81.745,23.543,81.907,23.505,81.991,23.401,82.098,23.372,82.19,23.289,82.129,23.096,82.071,23.1,81.946,23.043,81.94,22.946,81.789,22.853,81.766,22.664,81.67,22.609,81.642,22.551,81.54,22.52,81.418,22.437,81.373,22.493,81.235,22.444,81.178,22.478,81.123,22.401,81.119,22.295,81.024,22.194,80.987,22.108,80.919,22.097,80.835,21.951,80.838,21.846,80.805,21.774,80.731,21.714,80.721,21.473,80.679,21.351]},{"z":4.6,"b":[105.291,28.572,108.518,32.198],"l":[108.518,32.198,108.389,32.184,108.419,32.1,108.345,32.074,108.268,31.973,108.287,31.922,108.501,31.791,108.516,31.708,108.407,31.566,108.323,31.499,108.223,31.468,108.194,31.331,108.033,31.222,108.007,31.039,107.932,30.949,107.93,30.873,107.85,30.81,107.769,30.827,107.736,30.883,107.621,30.837,107.508,30.822,107.457,30.761,107.478,30.61,107.372,30.5,107.229,30.238,107.033,30.058,106.77,30.037,106.717,30.059,106.618,30.269,106.532,30.338,106.449,30.324,106.385,30.264,106.239,30.22,106.125,30.342,105.999,30.385,105.792,30.423,105.709,30.274,105.557,30.148,105.739,30.033,105.71,29.868,105.558,29.804,105.551,29.748,105.412,29.703,105.291,29.585,105.315,29.47,105.405,29.43,105.422,29.334,105.506,29.291,105.645,29.27,105.701,29.224,105.745,28.995,105.795,28.937,105.878,28.92,106.008,28.973,106.26,28.869,106.257,28.794,106.298,28.672,106.382,28.572]},{"z":4.6,"b":[76.837,28.433,77.327,28.882],"l":[77.327,28.52,77.27,28.51,77.194,28.433,77.101,28.476,77.071,28.529,76.873,28.519,76.837,28.587,76.947,28.696,76.937,28.815,77.068,28.882,77.201,28.877]},{"z":4.6,"b":[76.815,30.681,76.827,30.789],"l":[76.815,30.789,76.827,30.681]},{"z":4.7,"b":[59.485,58.614,65.935,61.993],"l":[59.485,61.993,59.652,61.936,59.779,61.952,59.984,61.903,59.988,61.747,60.057,61.721,60.431,61.747,60.759,61.704,61.165,61.683,61.9,61.434,61.933,61.404,62.42,61.305,62.62,61.211,62.86,61.001,62.76,60.859,62.881,60.64,63.091,60.46,63.22,60.301,63.197,60.242,63.438,60.159,63.589,60.155,63.694,60.001,63.796,59.792,63.825,59.477,63.98,59.406,64.121,59.391,64.324,59.411,65.002,59.338,65.185,59.223,65.382,58.919,65.669,58.663,65.935,58.614]},{"z":4.7,"b":[59.232,61.993,62.018,65.717],"l":[59.485,61.993,59.404,62.108,59.507,62.307,59.565,62.335,59.644,62.51,59.527,62.541,59.394,62.731,59.483,62.892,59.278,62.97,59.232,63.089,59.33,63.274,59.292,63.342,59.494,63.611,59.521,63.791,59.584,63.938,59.761,63.996,59.838,64.08,59.588,64.233,59.633,64.344,59.589,64.467,59.484,64.497,59.704,64.668,59.648,64.772,59.75,64.859,59.889,64.901,60.147,65.065,60.305,65.071,60.677,64.912,60.831,65.051,61.003,65.06,61.107,65.161,61.241,65.192,61.263,65.307,61.511,65.5,61.882,65.702,62.018,65.717]},{"z":4.7,"b":[57.219,56.329,59.468,61.682],"l":[57.408,56.329,57.317,56.373,57.373,56.552,57.358,56.681,57.219,56.853,57.323,56.92,57.56,56.888,57.825,56.974,57.869,57.058,58.03,57.099,58.039,57.237,57.938,57.332,57.995,57.477,58.268,57.67,58.436,57.676,58.458,57.588,58.58,57.594,58.83,57.726,58.864,57.827,58.756,57.848,58.605,57.996,58.678,58.107,58.868,58.205,58.971,58.2,59.203,58.305,59.281,58.413,59.456,58.493,59.389,58.59,59.388,58.703,59.186,58.711,59.083,58.769,59.066,58.884,59.181,58.953,59.091,59.022,59.175,59.147,59.003,59.231,58.67,59.293,58.508,59.44,58.311,59.468,58.403,59.552,58.451,59.705,58.582,59.716,58.675,59.859,58.805,59.863,58.849,59.926,58.989,59.928,58.962,60.005,59.188,60.278,59.169,60.374,59.239,60.48,59.385,60.574,59.468,60.808,59.458,60.953,59.373,60.97,59.358,61.143,59.261,61.222,59.317,61.377,59.406,61.418,59.428,61.507,59.349,61.682]},{"z":4.7,"b":[59.345,61.682,59.485,61.993],"l":[59.349,61.682,59.389,61.744,59.345,61.857,59.485,61.993]},{"z":4.7,"b":[53.632,55.907,54.352,56.359],"l":[53.632,55.907,53.777,55.97,53.838,56.039,54.047,56.058,54.117,56.158,54.346,56.253,54.352,56.359]},{"z":4.7,"b":[54.352,56.108,57.408,56.515],"l":[54.352,56.359,54.43,56.343,54.532,56.515,54.918,56.392,54.968,56.32,55.448,56.341,55.548,56.4,55.876,56.437,56.073,56.289,56.275,56.305,56.402,56.387,56.655,56.22,56.73,56.224,56.864,56.108,57.213,56.182,57.274,56.305,57.408,56.329]},{"z":4.7,"b":[57.408,56.063,59.026,56.329],"l":[57.408,56.329,57.411,56.235,57.537,56.1,57.802,56.151,57.907,56.115,58.079,56.141,58.308,56.105,58.543,56.163,58.747,56.063,58.944,56.066,59.026,56.169]},{"z":4.7,"b":[53.141,54.046,54.177,55.907],"l":[53.47,54.046,53.348,54.382,53.426,54.499,53.414,54.552,53.515,54.616,53.607,54.728,53.579,54.822,53.422,54.982,53.269,55.012,53.141,55.099,53.147,55.147,53.327,55.166,53.418,55.222,53.591,55.212,53.723,55.338,53.891,55.381,53.997,55.544,54.175,55.624,54.177,55.707,53.976,55.853,53.759,55.866,53.632,55.907]},{"z":4.6,"b":[110.975,31.395,115.374,33.266],"l":[115.374,31.418,115.286,31.395,115.219,31.447,115.213,31.546,115.129,31.595,115.077,31.516,114.999,31.478,114.767,31.49,114.638,31.568,114.548,31.573,114.556,31.738,114.495,31.759,114.362,31.734,114.267,31.809,114.144,31.848,114.085,31.784,113.972,31.747,113.96,31.832,113.862,31.863,113.794,31.973,113.752,32.138,113.782,32.194,113.783,32.359,113.729,32.419,113.616,32.386,113.528,32.297,113.436,32.289,113.183,32.413,113.066,32.417,112.751,32.35,112.534,32.377,112.286,32.351,112.145,32.395,112.053,32.455,111.779,32.52,111.711,32.6,111.629,32.626,111.532,32.611,111.507,32.682,111.363,32.825,111.21,32.93,111.216,32.976,111.109,33.148,111.003,33.208,110.975,33.266]},{"z":4.6,"b":[115.374,29.789,116.147,31.418],"l":[115.374,31.418,115.55,31.183,115.669,31.217,115.768,31.144,115.877,31.129,115.946,31.034,116.036,30.989,115.859,30.861,115.862,30.785,115.775,30.755,115.768,30.688,115.833,30.594,115.947,30.488,115.877,30.39,115.912,30.316,116.04,30.221,116.081,30.047,116.078,29.969,116.126,29.905,116.147,29.789]},{"z":4.6,"b":[116.147,29.408,118.175,30.051],"l":[116.147,29.789,116.236,29.781,116.39,29.876,116.493,29.885,116.65,30.051,116.791,30.022,116.887,29.92,116.798,29.756,116.675,29.708,116.648,29.627,116.749,29.545,116.888,29.561,116.955,29.654,117.079,29.71,117.107,29.786,117.067,29.841,117.125,29.911,117.226,29.907,117.274,29.829,117.406,29.831,117.402,29.773,117.527,29.622,117.649,29.606,117.708,29.556,117.899,29.549,117.986,29.572,118.113,29.519,118.175,29.408]},{"z":4.6,"b":[114.88,31.418,116.641,34.567],"l":[116.151,34.567,116.157,34.447,116.401,34.275,116.552,34.287,116.575,34.192,116.543,34.121,116.641,33.953,116.445,33.845,116.421,33.791,116.182,33.718,116.08,33.778,115.971,33.905,115.961,34,115.841,34.007,115.733,34.066,115.654,34.048,115.586,33.98,115.561,33.898,115.63,33.839,115.574,33.75,115.635,33.592,115.443,33.548,115.346,33.453,115.316,33.374,115.345,33.251,115.251,33.121,115.143,33.084,114.969,33.124,114.887,33.083,114.88,32.993,114.925,32.958,115.185,32.859,115.199,32.591,115.367,32.56,115.457,32.507,115.565,32.402,115.647,32.402,115.694,32.492,115.79,32.471,115.888,32.394,115.915,32.25,115.928,32.025,115.901,31.794,115.663,31.784,115.532,31.736,115.39,31.512,115.374,31.418]},{"z":4.6,"b":[110.336,34.584,113.721,36.356],"l":[113.721,36.356,113.673,36.221,113.687,36.056,113.628,35.953,113.585,35.687,113.61,35.646,113.491,35.525,113.328,35.463,113.13,35.352,113.02,35.329,112.903,35.241,112.808,35.242,112.682,35.311,112.526,35.22,112.335,35.219,112.058,35.234,112.061,35.108,111.829,35.083,111.622,34.947,111.558,34.863,111.394,34.823,111.331,34.841,111.026,34.747,110.884,34.666,110.766,34.661,110.709,34.621,110.531,34.584,110.336,34.616]},{"z":4.6,"b":[113.902,29.066,116.147,29.864],"l":[113.902,29.066,114.063,29.203,114.247,29.249,114.243,29.309,114.308,29.355,114.47,29.318,114.709,29.39,114.818,29.394,114.895,29.52,114.973,29.56,115.272,29.615,115.417,29.712,115.486,29.864,115.705,29.861,115.856,29.739,115.999,29.744,116.147,29.789]},{"z":4.6,"b":[110.328,33.266,111.016,34.616],"l":[110.336,34.616,110.328,34.48,110.374,34.404,110.479,34.326,110.434,34.261,110.607,34.17,110.594,34.065,110.642,33.98,110.594,33.88,110.749,33.803,110.792,33.705,110.968,33.599,111.011,33.5,111.016,33.374,110.975,33.266]},{"z":4.6,"b":[109.487,31.738,110.975,33.266],"l":[110.975,33.266,110.9,33.205,110.823,33.199,110.711,33.108,110.604,33.154,110.547,33.245,110.432,33.173,110.234,33.156,110.094,33.213,109.96,33.214,109.875,33.243,109.66,33.264,109.516,33.237,109.491,33.145,109.757,33.08,109.781,33.045,109.74,32.93,109.836,32.896,109.899,32.914,110.096,32.841,110.155,32.73,110.168,32.622,110.074,32.617,110.001,32.552,109.88,32.592,109.635,32.591,109.579,32.541,109.561,32.405,109.487,32.336,109.548,32.234,109.619,32.189,109.666,32.043,109.61,31.927,109.594,31.738]},{"z":4.6,"b":[108.547,-7.665,108.895,-6.814],"l":[108.799,-7.665,108.734,-7.605,108.742,-7.537,108.705,-7.391,108.547,-7.279,108.595,-7.142,108.695,-7.14,108.789,-7.061,108.801,-6.935,108.895,-6.814]},{"z":4.6,"b":[106.401,-7.007,106.763,-6.301],"l":[106.401,-7.007,106.451,-6.836,106.506,-6.783,106.427,-6.697,106.405,-6.524,106.434,-6.439,106.421,-6.37,106.56,-6.355,106.631,-6.384,106.739,-6.386,106.763,-6.301]},{"z":4.6,"b":[121.068,-3.02,121.598,-2.873],"l":[121.598,-2.973,121.475,-3.02,121.278,-2.879,121.164,-2.873,121.068,-2.918]},{"z":4.6,"b":[110.043,-8.201,110.844,-7.569],"l":[110.844,-8.201,110.799,-8.122,110.787,-7.874,110.753,-7.833,110.579,-7.821,110.507,-7.783,110.441,-7.569,110.301,-7.669,110.137,-7.677,110.117,-7.762,110.043,-7.889]},{"z":4.6,"b":[119.412,-3.513,119.873,-1.955],"l":[119.467,-3.513,119.453,-3.281,119.412,-3.215,119.639,-3.121,119.543,-2.907,119.56,-2.746,119.661,-2.758,119.786,-2.695,119.739,-2.565,119.707,-2.354,119.624,-2.294,119.646,-2.176,119.753,-2.082,119.825,-2.046,119.873,-1.955]},{"z":4.6,"b":[119.873,-2.973,121.787,-1.86],"l":[119.873,-1.955,120.048,-1.861,120.157,-1.86,120.292,-1.9,120.448,-1.866,120.553,-2.025,120.625,-2.104,120.804,-2.239,121.35,-2.348,121.449,-2.388,121.638,-2.558,121.742,-2.61,121.787,-2.67,121.772,-2.801,121.598,-2.973]},{"z":4.6,"b":[77.201,28.52,77.338,28.877],"l":[77.327,28.52,77.338,28.626,77.307,28.721,77.227,28.784,77.201,28.877]},{"z":4.6,"b":[110.944,-8.226,111.673,-6.734],"l":[110.944,-8.226,110.965,-8.117,111.099,-8.009,111.228,-7.938,111.308,-7.838,111.265,-7.732,111.191,-7.707,111.217,-7.613,111.185,-7.529,111.197,-7.309,111.22,-7.251,111.444,-7.311,111.46,-7.274,111.608,-7.154,111.598,-6.961,111.629,-6.8,111.673,-6.734]},{"z":4.6,"b":[78.169,23.999,82.808,26.851],"l":[82.801,23.999,82.679,24.167,82.733,24.214,82.767,24.386,82.734,24.42,82.734,24.552,82.808,24.566,82.772,24.668,82.692,24.72,82.624,24.692,82.427,24.719,82.424,24.648,82.303,24.638,82.319,24.702,82.033,24.859,81.881,24.922,81.817,25.036,81.585,25.08,81.537,25.2,81.361,25.157,81.243,25.17,81.239,25.098,81.185,24.973,81.098,24.909,80.999,24.966,80.893,24.984,80.779,24.971,80.859,25.188,80.806,25.212,80.718,25.082,80.647,25.07,80.58,25.148,80.515,25.041,80.458,25.015,80.288,25.016,80.265,25.064,80.422,25.181,80.395,25.247,80.326,25.282,80.284,25.414,80.191,25.413,80.125,25.351,80.015,25.331,79.852,25.248,79.863,25.153,79.661,25.148,79.542,25.177,79.468,25.104,79.339,25.147,79.425,25.257,79.295,25.259,79.269,25.164,79.109,25.195,79.024,25.175,78.983,25.237,78.885,25.266,78.955,25.386,78.919,25.505,78.853,25.434,78.749,25.426,78.728,25.346,78.597,25.414,78.516,25.266,78.61,25.136,78.64,24.964,78.732,24.815,78.715,24.685,78.75,24.63,78.879,24.613,78.955,24.393,78.826,24.23,78.747,24.279,78.656,24.295,78.489,24.411,78.386,24.301,78.33,24.342,78.35,24.408,78.278,24.453,78.229,24.533,78.265,24.583,78.261,24.693,78.169,24.865,78.337,25.023,78.366,25.186,78.262,25.412,78.33,25.465,78.371,25.553,78.558,25.576,78.715,25.626,78.754,25.674,78.74,25.79,78.808,25.842,78.954,26.11,78.963,26.278,79.061,26.355,79.028,26.498,78.974,26.572,78.936,26.694,78.842,26.712,78.693,26.798,78.564,26.758,78.371,26.851,78.2,26.824]},{"z":4.6,"b":[77.281,27.809,77.529,28.52],"l":[77.281,27.809,77.424,27.891,77.48,27.898,77.526,27.974,77.468,28.047,77.529,28.253,77.467,28.315,77.497,28.393,77.327,28.52]},{"z":4.6,"b":[77.065,28.877,77.585,30.383],"l":[77.201,28.877,77.196,28.998,77.141,29.13,77.131,29.281,77.15,29.403,77.065,29.57,77.088,29.659,77.18,29.804,77.23,29.977,77.285,30.038,77.57,30.278,77.585,30.383]},{"z":4.6,"b":[83.337,24.14,83.523,24.53],"l":[83.523,24.53,83.402,24.498,83.454,24.362,83.337,24.14]},{"z":4.6,"b":[82.801,23.911,83.337,24.14],"l":[83.337,24.14,83.206,23.944,83.089,23.911,82.964,23.914,82.801,23.999]},{"z":4.6,"b":[83.523,24.307,87.784,25.324],"l":[87.784,25.214,87.726,25.252,87.599,25.267,87.523,25.324,87.407,25.181,87.292,25.174,87.266,25.072,87.186,25.062,87.117,24.971,87.119,24.851,87.078,24.745,87.05,24.597,86.925,24.614,86.89,24.532,86.755,24.588,86.702,24.548,86.586,24.561,86.494,24.48,86.439,24.34,86.276,24.435,86.29,24.552,86.155,24.556,86.098,24.69,86.033,24.712,85.912,24.698,85.831,24.771,85.746,24.77,85.647,24.649,85.662,24.569,85.578,24.558,85.526,24.502,85.391,24.509,85.281,24.487,84.961,24.348,84.889,24.373,84.798,24.497,84.588,24.394,84.519,24.308,84.472,24.307,84.312,24.451,84.305,24.531,84.096,24.504,84.044,24.624,83.918,24.544,83.788,24.537,83.696,24.506,83.523,24.53]},{"z":4.6,"b":[83.323,24.53,84.606,27.378],"l":[83.523,24.53,83.531,24.704,83.509,24.762,83.36,24.863,83.36,25.009,83.323,25.068,83.334,25.173,83.384,25.236,83.775,25.445,83.904,25.529,84.143,25.736,84.213,25.728,84.239,25.648,84.324,25.659,84.315,25.742,84.373,25.765,84.487,25.701,84.601,25.747,84.606,25.8,84.532,25.862,84.42,25.89,84.374,25.94,84.261,25.931,84.15,26.066,84.065,26.104,83.993,26.241,84.187,26.293,84.178,26.404,84.026,26.469,83.955,26.454,83.92,26.533,84.054,26.565,84.099,26.65,84.383,26.642,84.306,26.744,84.246,26.757,84.241,26.874,84.149,26.876,84.071,26.924,84.023,27.06,83.938,27.164,83.942,27.222,83.829,27.378]},{"z":4.6,"b":[76.768,30.383,77.585,30.908],"l":[77.585,30.383,77.303,30.46,77.148,30.539,77.15,30.664,77.115,30.722,77.002,30.777,76.906,30.899,76.768,30.908]},{"z":4.7,"b":[33.571,44.398,33.852,44.852],"l":[33.73,44.398,33.852,44.432,33.806,44.527,33.713,44.582,33.709,44.666,33.611,44.721,33.675,44.792,33.571,44.852]},{"z":4.6,"b":[103.592,-4.927,103.765,-4.805],"l":[103.592,-4.927,103.765,-4.805]},{"z":4.6,"b":[101.027,-2.48,101.336,-2.297],"l":[101.027,-2.48,101.171,-2.367,101.336,-2.297]},{"z":4.6,"b":[103.765,-4.947,105.828,-3.756],"l":[105.828,-4.158,105.709,-4.15,105.513,-3.868,105.31,-3.756,105.266,-3.886,105.129,-3.956,105.111,-4.06,105.009,-4.169,104.84,-4.221,104.674,-4.297,104.527,-4.32,104.397,-4.405,104.309,-4.44,104.293,-4.495,104.309,-4.628,104.265,-4.708,104.337,-4.814,104.298,-4.892,104.21,-4.893,104.081,-4.947,103.889,-4.934,103.855,-4.854,103.765,-4.805]},{"z":4.6,"b":[102.067,-4.805,103.765,-2.782],"l":[103.765,-4.805,103.723,-4.757,103.694,-4.639,103.622,-4.505,103.616,-4.418,103.391,-4.335,103.186,-4.243,103.142,-4.047,103.107,-4.02,102.981,-4.035,102.908,-3.991,102.845,-4,102.768,-3.961,102.63,-3.814,102.702,-3.791,102.897,-3.631,102.993,-3.584,103.017,-3.485,102.975,-3.396,102.87,-3.376,102.766,-3.279,102.631,-3.373,102.563,-3.344,102.462,-3.25,102.489,-3.151,102.431,-3.088,102.241,-3.059,102.224,-2.954,102.067,-2.782]},{"z":4.6,"b":[101.336,-2.782,102.067,-2.236],"l":[102.067,-2.782,101.963,-2.735,101.9,-2.74,101.737,-2.636,101.564,-2.45,101.517,-2.305,101.436,-2.236,101.336,-2.297]},{"z":4.6,"b":[101.114,-2.297,101.841,-1.038],"l":[101.336,-2.297,101.26,-2.081,101.142,-1.92,101.114,-1.705,101.226,-1.728,101.476,-1.702,101.519,-1.64,101.675,-1.5,101.678,-1.289,101.794,-1.239,101.841,-1.166,101.783,-1.132,101.77,-1.038]},{"z":4.6,"b":[123.075,0.306,123.5,0.941],"l":[123.075,0.941,123.205,0.837,123.247,0.733,123.325,0.664,123.439,0.611,123.481,0.557,123.5,0.43,123.457,0.306]},{"z":4.6,"b":[102.067,-2.782,104.532,-1.694],"l":[104.532,-1.734,104.428,-1.694,104.376,-1.714,104.322,-1.804,104.126,-1.831,103.958,-1.791,103.886,-1.795,103.535,-1.968,103.524,-2.225,103.46,-2.238,103.463,-2.352,103.419,-2.394,103.304,-2.285,103.272,-2.209,103.216,-2.186,103.17,-2.286,103.205,-2.361,103.123,-2.398,102.862,-2.354,102.864,-2.462,102.792,-2.568,102.619,-2.672,102.596,-2.71,102.487,-2.73,102.398,-2.71,102.26,-2.64,102.233,-2.684,102.067,-2.782]},{"z":4.6,"b":[100.163,-1.038,101.77,0.758],"l":[101.77,-1.038,101.674,-0.967,101.525,-0.932,101.434,-0.847,101.331,-0.787,101.236,-0.703,101.04,-0.474,101,-0.401,100.856,-0.322,100.79,-0.206,100.749,-0.064,100.846,-0.03,100.847,0.186,100.51,0.444,100.369,0.411,100.265,0.45,100.163,0.648,100.207,0.711,100.173,0.758]},{"z":4.6,"b":[100.071,0.758,100.441,2.555],"l":[100.173,0.758,100.21,0.866,100.21,1.054,100.225,1.146,100.148,1.225,100.13,1.3,100.071,1.359,100.105,1.415,100.35,1.525,100.441,1.599,100.435,1.667,100.376,1.814,100.304,1.881,100.319,2.046,100.313,2.177,100.279,2.355,100.314,2.555]},{"z":4.6,"b":[121.16,0.483,122.197,1.028],"l":[122.197,1.028,122.046,0.944,121.993,0.935,121.84,0.97,121.508,0.847,121.378,0.861,121.303,0.826,121.235,0.725,121.16,0.686,121.257,0.587,121.334,0.576,121.333,0.483]},{"z":4.6,"b":[101.77,-1.085,103.525,-0.739],"l":[103.525,-0.739,103.1,-0.772,102.917,-0.763,102.804,-0.793,102.682,-0.904,102.541,-1.085,102.452,-1.068,102.356,-1.01,102.261,-0.88,102.189,-0.856,102.013,-0.88,101.935,-0.927,101.882,-1,101.77,-1.038]},{"z":4.6,"b":[121.598,-3.246,122.349,-2.973],"l":[121.598,-2.973,121.781,-2.986,122.058,-3.093,122.207,-3.111,122.349,-3.246]},{"z":4.6,"b":[119.494,-1.955,119.873,-0.834],"l":[119.873,-1.955,119.823,-1.872,119.846,-1.792,119.822,-1.729,119.725,-1.613,119.693,-1.461,119.642,-1.394,119.494,-1.329,119.527,-1.224,119.602,-1.151,119.572,-1.052,119.566,-0.834]},{"z":4.6,"b":[106.763,-6.382,106.989,-6.027],"l":[106.763,-6.301,106.924,-6.382,106.985,-6.235,106.989,-6.027]},{"z":4.6,"b":[106.684,-6.301,106.763,-6.059],"l":[106.763,-6.301,106.702,-6.21,106.684,-6.102,106.731,-6.059]},{"z":4.6,"b":[120.883,31.012,121.341,31.491],"l":[120.909,31.012,120.883,31.14,121.035,31.144,121.059,31.248,121.126,31.277,121.136,31.374,121.187,31.458,121.341,31.491]},{"z":4.6,"b":[116.377,31.133,119.63,34.64],"l":[119.63,31.133,119.363,31.192,119.317,31.267,119.163,31.287,119.075,31.239,118.816,31.226,118.756,31.281,118.751,31.358,118.827,31.398,118.881,31.522,118.86,31.627,118.681,31.647,118.684,31.701,118.477,31.79,118.481,31.859,118.352,31.947,118.383,32.054,118.499,32.138,118.514,32.2,118.65,32.238,118.684,32.339,118.668,32.453,118.612,32.478,118.552,32.574,118.752,32.613,118.891,32.596,119.081,32.446,119.199,32.594,119.19,32.711,119.11,32.834,119.009,32.91,118.993,32.962,118.857,32.972,118.798,32.865,118.748,32.839,118.72,32.732,118.399,32.731,118.3,32.777,118.239,32.924,118.248,33.013,118.218,33.18,118.081,33.15,117.996,33.168,117.932,33.235,118.122,33.611,118.177,33.693,118.124,33.766,117.926,33.734,117.752,33.724,117.746,33.882,117.688,33.888,117.651,33.982,117.572,33.99,117.503,34.05,117.392,34.031,117.321,34.079,117.159,34.091,117.034,34.156,116.97,34.275,116.949,34.389,116.802,34.4,116.77,34.452,116.625,34.485,116.428,34.64,116.377,34.64]},{"z":4.6,"b":[119.63,30.764,120.909,31.176],"l":[119.63,31.133,119.74,31.176,119.932,31.15,119.943,31.087,120.129,30.943,120.213,30.921,120.362,30.939,120.447,30.89,120.491,30.764,120.578,30.846,120.709,30.891,120.692,30.977,120.909,31.012]},{"z":4.6,"b":[120.909,30.691,121.275,31.012],"l":[120.909,31.012,120.969,31.009,120.981,30.832,121.104,30.834,121.256,30.744,121.275,30.691]},{"z":4.6,"b":[118.001,28.289,118.476,29.408],"l":[118.446,28.289,118.476,28.332,118.458,28.517,118.416,28.604,118.426,28.695,118.372,28.799,118.303,28.836,118.229,28.955,118.064,29.051,118.001,29.144,118.052,29.275,118.132,29.298,118.175,29.408]},{"z":4.6,"b":[118.175,29.408,119.63,31.133],"l":[118.175,29.408,118.373,29.453,118.47,29.524,118.557,29.625,118.648,29.658,118.724,29.73,118.741,29.828,118.82,29.88,118.894,29.983,118.853,30.159,118.909,30.222,118.881,30.324,118.935,30.352,119.058,30.312,119.211,30.314,119.363,30.404,119.321,30.518,119.242,30.551,119.351,30.675,119.443,30.639,119.501,30.769,119.575,30.847,119.63,31.133]},{"z":4.6,"b":[115.843,24.917,118.446,28.289],"l":[115.888,24.917,115.888,25,115.843,25.195,115.908,25.238,115.979,25.351,116,25.497,116.045,25.575,116.028,25.636,116.124,25.764,116.135,25.866,116.18,25.907,116.323,25.955,116.397,26.061,116.419,26.153,116.399,26.288,116.502,26.407,116.594,26.395,116.619,26.492,116.542,26.563,116.505,26.689,116.545,26.868,116.67,26.987,116.923,27.021,117.04,27.11,117.154,27.276,117.101,27.347,117.125,27.43,117.112,27.568,117.225,27.723,117.287,27.773,117.284,27.857,117.544,27.967,117.605,27.866,117.746,27.813,117.823,27.937,117.946,27.973,118.061,27.979,118.131,28.041,118.368,28.1,118.349,28.22,118.446,28.289]},{"z":4.6,"b":[118.446,27.206,120.419,28.326],"l":[118.446,28.289,118.499,28.262,118.735,28.326,118.797,28.213,118.766,28.178,118.748,27.973,118.82,27.91,118.889,27.722,118.923,27.55,119.005,27.48,119.186,27.42,119.266,27.424,119.39,27.513,119.467,27.525,119.512,27.637,119.616,27.666,119.687,27.511,119.693,27.411,119.77,27.316,120.064,27.344,120.148,27.4,120.249,27.41,120.344,27.38,120.406,27.297,120.419,27.206]},{"z":4.6,"b":[81.759,17.909,84.778,19.161],"l":[81.759,17.909,81.924,18,82.042,18.043,82.132,18.039,82.24,17.993,82.33,18.044,82.353,18.155,82.31,18.195,82.361,18.292,82.368,18.414,82.449,18.522,82.503,18.511,82.511,18.422,82.597,18.325,82.587,18.244,82.781,18.358,82.78,18.417,82.878,18.409,82.904,18.355,83.039,18.39,83.037,18.469,83.084,18.525,83.028,18.641,83.127,18.758,83.202,18.742,83.394,18.869,83.327,18.93,83.341,19.005,83.424,18.964,83.49,19.052,83.573,19.07,83.613,19.13,83.706,19.001,83.802,19,83.854,18.86,83.902,18.806,84.071,18.818,84.153,18.788,84.318,18.809,84.463,18.946,84.525,19.033,84.641,19.101,84.695,19.161,84.778,19.116]},{"z":4.6,"b":[76.719,12.76,78.568,15.976],"l":[78.23,12.76,78.286,12.843,78.416,12.948,78.516,13.089,78.568,13.275,78.381,13.323,78.35,13.376,78.375,13.58,78.184,13.602,78.068,13.699,78.097,13.809,78.033,13.885,77.921,13.83,77.879,13.917,77.735,13.824,77.593,13.767,77.499,13.704,77.443,13.702,77.384,13.847,77.292,13.848,77.152,13.904,77.103,13.85,77.133,13.785,77.032,13.746,76.959,13.782,77.01,13.905,76.936,14.011,76.943,14.061,76.873,14.137,76.974,14.156,76.995,14.066,77.112,14.005,77.289,14.011,77.332,13.908,77.388,13.963,77.328,14.052,77.369,14.163,77.468,14.192,77.461,14.29,77.384,14.262,77.278,14.296,77.257,14.334,77.111,14.33,77.045,14.232,76.908,14.258,76.862,14.347,76.932,14.458,76.83,14.479,76.753,14.566,76.762,14.764,76.8,14.795,76.822,14.921,76.719,14.99,76.764,15.094,76.948,15.022,77.08,15.037,77.128,15.132,77.105,15.308,77.026,15.351,77,15.614,77.085,15.667,77.025,15.781,77.034,15.874,77.144,15.961,77.226,15.976,77.446,15.953]},{"z":4.6,"b":[77.446,15.833,81.776,17.909],"l":[77.446,15.953,77.641,15.901,77.85,15.894,77.888,15.919,78.087,15.833,78.235,15.95,78.256,16.006,78.4,16.083,78.5,16.094,78.734,16.03,78.839,16.067,78.839,16.125,79.022,16.247,79.161,16.219,79.215,16.244,79.233,16.331,79.213,16.493,79.266,16.57,79.406,16.587,79.504,16.642,79.721,16.689,79.752,16.724,79.882,16.703,79.958,16.63,80.043,16.739,80.073,16.823,80.002,16.871,80.078,16.973,80.22,17.033,80.281,16.997,80.351,16.862,80.506,16.792,80.586,16.934,80.474,16.937,80.384,16.976,80.502,17.108,80.591,17.14,80.67,17.08,80.83,17.046,80.893,17.156,80.99,17.218,81.144,17.25,81.214,17.324,81.408,17.371,81.47,17.42,81.535,17.638,81.629,17.763,81.776,17.833,81.759,17.909]},{"z":4.6,"b":[114.311,-3.434,115.68,-1.361],"l":[115.678,-1.426,115.68,-1.361,115.427,-1.434,115.404,-1.474,115.338,-1.765,115.358,-1.904,115.308,-1.992,115.33,-2.037,115.131,-2.208,114.905,-2.263,114.879,-2.326,114.908,-2.419,114.822,-2.562,114.785,-2.689,114.675,-2.759,114.631,-2.822,114.504,-2.881,114.471,-3.004,114.311,-3.434]},{"z":4.6,"b":[78.23,12.633,80.26,13.468],"l":[80.26,13.468,80.098,13.464,79.962,13.417,79.957,13.366,79.816,13.295,79.785,13.224,79.697,13.236,79.668,13.291,79.572,13.284,79.544,13.324,79.45,13.331,79.42,13.22,79.315,13.132,79.251,13.148,79.162,13.045,79.078,13.036,78.944,13.093,78.736,13.061,78.645,12.992,78.583,12.778,78.419,12.633,78.353,12.642,78.23,12.76]},{"z":4.6,"b":[80.278,13.513,80.288,13.527],"l":[80.288,13.527,80.278,13.513]},{"z":4.6,"b":[99.151,0.252,100.173,0.868],"l":[99.151,0.252,99.342,0.474,99.584,0.563,99.685,0.501,99.814,0.468,99.908,0.504,99.915,0.569,99.818,0.709,99.794,0.78,99.71,0.849,99.826,0.868,100.173,0.758]},{"z":4.6,"b":[97.784,2.189,98.268,4.295],"l":[98.101,2.189,98.128,2.327,98.063,2.444,98.025,2.576,98.069,2.677,98.057,2.812,97.923,2.909,97.896,3.083,97.936,3.141,97.864,3.257,97.99,3.33,97.907,3.433,97.914,3.484,97.844,3.58,97.784,3.745,97.893,3.848,97.89,3.915,98.001,3.98,98.063,4.24,98.188,4.295,98.268,4.288]},{"z":4.7,"b":[162.265,61.658,163.434,64.77],"l":[163.137,64.77,163.246,64.727,163.167,64.559,163.345,64.487,163.434,64.373,163.256,64.321,163.167,64.19,162.942,64.2,162.794,64.148,162.841,64.017,162.929,63.95,162.888,63.877,162.726,63.837,162.755,63.738,162.979,63.621,162.957,63.491,162.884,63.436,162.695,63.393,162.744,63.182,162.38,63.119,162.269,62.935,162.548,62.845,162.673,62.71,162.819,62.697,162.709,62.601,162.768,62.503,162.736,62.297,162.664,62.262,162.404,62.253,162.265,62.129,162.512,62.103,162.48,61.963,162.311,61.934,162.468,61.82,162.431,61.764,162.51,61.658]},{"z":4.6,"b":[72.947,20.04,73.166,20.127],"l":[73.166,20.115,73.161,20.047,72.997,20.04,72.947,20.127]},{"z":4.7,"b":[144.719,62.047,158.529,66.359],"l":[144.745,62.047,144.719,62.082,144.903,62.16,144.993,62.244,145.177,62.286,145.175,62.428,145.124,62.49,145.48,62.548,145.326,62.754,145.366,62.91,145.252,63.18,145.364,63.331,145.445,63.515,145.633,63.66,145.572,63.836,146.109,63.942,146.291,64.061,146.377,64.197,146.499,64.209,146.996,64.147,147.23,64.056,147.307,64.132,147.61,64.17,147.603,64.047,147.955,63.944,148.115,64.051,148.254,64.255,148.001,64.363,148.023,64.431,148.161,64.458,148.291,64.549,148.436,64.524,148.631,64.419,149.27,64.446,149.402,64.437,149.59,64.525,149.816,64.567,150.068,64.502,150.024,64.37,150.224,64.213,150.633,64.171,150.631,64.331,150.966,64.332,151.123,64.371,151.301,64.361,151.48,64.436,152.016,64.504,152.136,64.44,152.169,64.355,152.414,64.407,152.331,64.51,152.748,64.686,152.502,64.945,152.645,65.022,152.62,65.169,152.688,65.233,153.032,65.275,153.521,65.408,153.401,65.504,153.531,65.706,153.454,65.788,153.505,65.842,153.698,65.886,154.041,65.869,154.25,65.83,154.374,65.921,154.359,66.033,154.22,66.069,154.395,66.205,154.548,66.238,154.802,66.185,154.885,66.134,155.17,66.209,155.32,66.142,155.752,66.177,155.958,66.096,156.36,66.067,156.488,66.146,156.659,66.193,156.762,66.105,156.973,66.033,156.927,65.965,157.124,65.915,157.373,65.926,157.382,66,157.718,66.112,158.124,66.156,158.485,66.118,158.393,66.255,158.529,66.359]},{"z":4.7,"b":[158.529,64.649,163.137,66.359],"l":[158.529,66.359,158.776,66.284,159.166,66.234,159.203,66.164,159.052,66.048,158.934,65.903,158.922,65.743,159.377,65.658,159.834,65.529,159.997,65.523,160.17,65.451,160.174,65.376,160.418,65.251,160.537,65.162,160.688,65.149,161.01,65.17,161.281,65.141,161.417,65.069,161.645,65.013,161.784,64.838,162.293,64.742,162.608,64.734,162.736,64.661,162.997,64.649,163.117,64.693,163.137,64.77]},{"z":4.6,"b":[115.427,39.442,117.429,41.031],"l":[117.357,40.257,117.251,40.338,117.238,40.45,117.184,40.497,117.243,40.57,117.412,40.597,117.429,40.669,117.303,40.662,117.199,40.69,117.058,40.677,116.91,40.749,116.676,40.94,116.653,41.031,116.438,40.9,116.459,40.792,116.291,40.74,116.207,40.75,116.13,40.658,115.995,40.596,115.874,40.61,115.728,40.54,115.945,40.294,115.82,40.152,115.578,40.096,115.427,39.963,115.496,39.924,115.511,39.838,115.437,39.749,115.463,39.643,115.618,39.604,115.756,39.509,115.945,39.575,116.001,39.558,116.21,39.572,116.25,39.506,116.387,39.442,116.428,39.521,116.604,39.611,116.793,39.602]},{"z":4.6,"b":[116.793,39.602,116.876,39.686],"l":[116.793,39.602,116.876,39.686]},{"z":4.6,"b":[117.239,40.093,117.372,40.257],"l":[117.239,40.093,117.311,40.11,117.372,40.201,117.357,40.257]},{"z":4.6,"b":[113.454,36.356,114.541,40.743],"l":[113.721,36.356,113.631,36.438,113.535,36.483,113.547,36.549,113.459,36.636,113.454,36.737,113.608,36.769,113.679,36.872,113.776,36.891,113.754,37.115,113.858,37.209,113.887,37.296,114.053,37.501,114.096,37.6,114.1,37.698,114.026,37.751,113.987,37.839,113.891,37.949,113.791,38.153,113.558,38.243,113.518,38.378,113.534,38.537,113.609,38.644,113.669,38.66,113.825,38.81,113.76,38.909,113.853,39.072,114.013,39.119,114.089,39.074,114.216,39.069,114.394,39.169,114.42,39.3,114.541,39.531,114.402,39.658,114.383,39.861,114.233,39.876,114.086,39.917,113.936,40.016,113.996,40.132,114.085,40.185,114.45,40.294,114.434,40.361,114.286,40.379,114.258,40.55,114.152,40.674,114.154,40.743]},{"z":4.6,"b":[117.357,39.219,118.01,40.257],"l":[118.01,39.219,118.008,39.343,117.897,39.399,117.893,39.565,117.631,39.576,117.623,39.666,117.533,39.75,117.501,39.989,117.766,39.964,117.75,40.063,117.451,40.227,117.357,40.257]},{"z":4.6,"b":[115.276,36.158,117.837,38.271],"l":[115.46,36.158,115.449,36.249,115.343,36.352,115.276,36.474,115.342,36.6,115.478,36.754,115.626,36.794,115.773,36.946,115.852,37.057,115.907,37.206,115.975,37.322,116.115,37.368,116.256,37.387,116.22,37.462,116.35,37.621,116.411,37.485,116.609,37.639,116.744,37.799,116.84,37.834,117.421,37.847,117.569,38.049,117.687,38.073,117.78,38.164,117.837,38.271]},{"z":4.6,"b":[113.721,36.057,115.46,36.359],"l":[115.46,36.158,115.35,36.082,115.245,36.161,115.106,36.185,115.034,36.101,114.931,36.057,114.893,36.12,114.707,36.153,114.548,36.139,114.343,36.245,114.156,36.247,114.063,36.277,113.989,36.359,113.808,36.339,113.721,36.356]},{"z":4.6,"b":[116.151,34.567,116.377,34.64],"l":[116.377,34.64,116.267,34.577,116.151,34.567]},{"z":4.6,"b":[114.834,34.567,116.151,36.158],"l":[116.151,34.567,116.044,34.6,115.816,34.571,115.707,34.6,115.646,34.569,115.542,34.578,115.436,34.676,115.429,34.78,115.318,34.843,115.216,34.836,115.175,34.944,114.998,35.008,114.834,35.016,114.864,35.189,114.942,35.219,115.068,35.38,115.227,35.416,115.345,35.496,115.395,35.616,115.504,35.723,115.655,35.746,115.761,35.849,115.874,35.866,115.863,35.925,115.966,35.986,116.034,35.966,116.077,36.103,115.927,36.022,115.829,36.031,115.739,35.969,115.53,35.911,115.468,35.913,115.416,35.824,115.318,35.792,115.334,35.931,115.424,35.995,115.46,36.158]},{"z":4.6,"b":[81.394,17.807,83.996,22.539],"l":[81.394,17.807,81.444,17.88,81.543,18.262,81.644,18.292,81.741,18.358,81.792,18.438,81.913,18.545,81.904,18.622,81.969,18.686,82.139,18.747,82.169,18.879,82.224,18.922,82.225,19.015,82.145,19.244,82.169,19.387,82.029,19.518,82.029,19.776,81.824,19.913,81.868,20.047,81.94,20.105,82.046,20.018,82.096,20.051,82.228,19.959,82.29,19.836,82.381,19.886,82.682,19.823,82.695,20.001,82.611,19.988,82.416,20.067,82.434,20.159,82.422,20.428,82.34,20.556,82.367,20.657,82.342,20.861,82.454,20.847,82.562,20.954,82.668,21.155,82.965,21.179,83.078,21.115,83.198,21.173,83.279,21.359,83.361,21.337,83.381,21.4,83.328,21.499,83.352,21.577,83.444,21.719,83.473,21.807,83.557,21.835,83.571,21.923,83.515,21.964,83.546,22.102,83.605,22.196,83.733,22.262,83.841,22.349,83.946,22.371,83.996,22.441,83.97,22.539]},{"z":4.6,"b":[83.97,21.986,86.687,22.557],"l":[83.97,22.539,84.067,22.5,84.148,22.387,84.311,22.324,84.421,22.331,84.53,22.423,84.734,22.427,84.801,22.45,84.908,22.422,85.066,22.503,85.1,22.394,85.088,22.28,84.985,22.113,85.109,22.108,85.232,22.035,85.324,22.127,85.425,22.164,85.551,22.108,85.679,22.076,85.792,22.129,85.785,22.012,85.834,21.986,85.916,22.022,85.974,22.105,85.965,22.245,85.987,22.394,85.939,22.501,86.04,22.557,86.102,22.5,86.268,22.422,86.366,22.33,86.487,22.337,86.687,22.218]},{"z":4.6,"b":[116.377,34.399,119.296,35.106],"l":[116.377,34.64,116.42,34.834,116.47,34.898,116.632,34.938,116.828,34.958,116.941,34.88,116.993,34.774,117.061,34.709,117.182,34.45,117.235,34.461,117.274,34.564,117.337,34.589,117.469,34.475,117.591,34.472,117.653,34.526,117.773,34.527,117.79,34.651,117.915,34.677,118.05,34.651,118.165,34.399,118.391,34.444,118.423,34.578,118.477,34.689,118.596,34.707,118.681,34.684,118.759,34.749,118.777,34.834,118.842,34.935,118.867,35.036,119.076,35.05,119.176,35.106,119.296,35.071]},{"z":4.6,"b":[81.394,17.797,81.759,17.909],"l":[81.759,17.909,81.578,17.797,81.394,17.807]},{"z":4.6,"b":[25.013,-28.068,27.583,-26.738],"l":[25.013,-28.068,25.053,-28.058,25.129,-27.929,25.225,-27.878,25.241,-27.823,25.421,-27.71,25.581,-27.655,25.64,-27.673,25.799,-27.581,25.874,-27.621,26,-27.639,26.229,-27.489,26.25,-27.426,26.45,-27.353,26.486,-27.254,26.411,-27.177,26.49,-27.139,26.645,-26.999,26.914,-26.89,27.044,-26.929,27.178,-26.924,27.308,-26.862,27.408,-26.894,27.48,-26.874,27.519,-26.781,27.583,-26.738]},{"z":4.6,"b":[27.583,-26.894,28.256,-26.671],"l":[27.583,-26.738,27.644,-26.764,27.882,-26.724,27.97,-26.671,28.044,-26.818,28.256,-26.894]},{"z":4.6,"b":[29.016,-31.078,30.196,-29.979],"l":[30.196,-31.078,30.126,-30.934,30.064,-30.859,29.974,-30.832,29.794,-30.709,29.602,-30.688,29.476,-30.62,29.32,-30.655,29.22,-30.618,29.163,-30.555,29.2,-30.436,29.296,-30.374,29.298,-30.318,29.175,-30.286,29.102,-30.174,29.116,-30.078,29.016,-29.979]},{"z":4.6,"b":[25.467,-30.681,27.378,-30.312],"l":[27.378,-30.312,27.298,-30.488,27.132,-30.502,27.01,-30.533,26.922,-30.655,26.781,-30.643,26.703,-30.675,26.535,-30.651,26.442,-30.56,26.311,-30.582,26.205,-30.525,26.06,-30.514,25.725,-30.681,25.634,-30.657,25.611,-30.606,25.467,-30.613]},{"z":4.6,"b":[28.859,-28.773,29.771,-27.452],"l":[28.859,-28.773,28.973,-28.575,29.189,-28.535,29.235,-28.44,29.407,-28.357,29.488,-28.268,29.589,-28.25,29.675,-28.121,29.624,-28.048,29.722,-27.811,29.683,-27.668,29.766,-27.605,29.716,-27.518,29.771,-27.452]},{"z":4.6,"b":[29.771,-27.452,31.27,-27.236],"l":[29.771,-27.452,29.929,-27.364,30.133,-27.379,30.281,-27.302,30.423,-27.275,30.444,-27.309,30.575,-27.311,30.662,-27.277,30.833,-27.334,30.902,-27.311,30.98,-27.355,31.151,-27.324,31.27,-27.236]},{"z":4.6,"b":[92.995,24.033,93.312,24.399],"l":[93.312,24.033,93.249,24.057,93.106,24.045,92.995,24.095,93.018,24.399]},{"z":4.6,"b":[92.246,23.683,92.325,24.238],"l":[92.246,23.683,92.271,23.829,92.319,23.851,92.325,24.188,92.298,24.238]},{"z":4.6,"b":[118.855,39.986,119.85,41.273],"l":[119.85,39.986,119.738,40.1,119.743,40.205,119.635,40.209,119.58,40.373,119.6,40.477,119.564,40.54,119.294,40.531,119.19,40.62,119.191,40.7,119.068,40.651,118.855,40.821,118.965,41.075,119.048,41.094,119.164,41.176,119.231,41.273]},{"z":4.6,"b":[93.449,25.206,94.655,25.684],"l":[94.655,25.445,94.553,25.49,94.565,25.684,94.325,25.512,94.209,25.495,94.027,25.554,93.803,25.544,93.809,25.446,93.679,25.328,93.605,25.206,93.449,25.302]},{"z":4.6,"b":[123.696,40.859,125.766,43.472],"l":[125.704,40.859,125.576,40.913,125.692,41.004,125.766,41.139,125.738,41.245,125.651,41.286,125.47,41.575,125.45,41.68,125.306,41.68,125.328,41.747,125.286,41.826,125.295,41.957,125.458,42.105,125.481,42.151,125.284,42.234,125.269,42.309,125.179,42.333,125.209,42.406,125.102,42.494,125.096,42.577,125.019,42.647,124.942,42.804,124.86,42.888,124.888,43.087,124.766,43.106,124.673,42.999,124.52,42.872,124.376,42.986,124.419,43.082,124.296,43.157,124.282,43.226,124.006,43.31,123.807,43.454,123.749,43.472,123.696,43.353]},{"z":4.7,"b":[144.745,59.365,147.159,62.058],"l":[144.745,62.047,144.968,62.057,145.156,61.968,145.272,61.992,145.514,61.964,145.743,62.058,145.891,62.025,146.219,61.903,146.361,61.82,146.505,61.662,146.583,61.636,146.613,61.443,146.768,61.403,146.683,61.313,146.747,61.223,146.747,61.115,146.678,61.081,146.701,60.959,146.546,60.958,146.464,60.709,146.32,60.659,146.069,60.739,145.62,60.557,145.51,60.464,145.63,60.392,145.571,60.335,145.793,60.198,146.1,60.224,146.374,60.271,146.452,60.142,146.68,60.095,146.657,59.982,146.787,59.901,146.989,59.948,147.141,59.849,147.159,59.647,147.075,59.411,146.887,59.365]},{"z":4.7,"b":[133.972,46.637,139.012,48.445],"l":[139.012,47.396,138.856,47.466,138.755,47.438,138.629,47.451,138.595,47.548,138.618,47.629,138.517,47.669,138.61,47.726,138.707,47.735,138.826,47.861,138.581,48.031,138.722,48.136,138.71,48.183,138.558,48.181,138.48,48.303,138.489,48.402,138.306,48.445,138.179,48.379,138.145,48.316,137.995,48.297,137.843,48.209,137.579,48.228,137.449,48.202,137.315,48.084,137.417,47.978,137.597,47.91,137.601,47.784,137.787,47.697,137.896,47.724,138.048,47.668,138.087,47.517,138.006,47.455,137.755,47.329,137.598,47.3,137.452,47.198,137.328,47.214,137.094,47.297,137.013,47.236,136.934,47.057,136.974,47.012,136.835,46.944,136.728,46.841,136.577,46.777,136.451,46.81,136.384,46.769,136.264,46.765,136.062,46.85,135.942,46.79,135.85,46.811,135.746,46.949,135.526,46.953,135.42,46.911,135.363,46.95,135.433,47.019,135.205,47.153,135.078,47.193,135.015,47.128,134.851,47.154,134.653,47.092,134.549,46.967,134.443,46.942,134.412,46.866,134.465,46.786,134.412,46.678,134.199,46.644,133.972,46.637]},{"z":4.7,"b":[131.396,48.371,134.987,49.486],"l":[131.396,49.244,131.463,49.206,131.612,49.288,131.767,49.31,131.966,49.305,132.068,49.466,132.12,49.486,132.398,49.393,132.382,49.295,132.639,49.29,132.732,49.315,132.815,49.235,133.033,49.219,133.201,49.141,133.309,48.826,133.77,48.759,133.824,48.683,133.935,48.64,134.105,48.633,134.269,48.716,134.392,48.743,134.482,48.659,134.746,48.569,134.862,48.625,134.987,48.595,134.977,48.507,134.884,48.424,134.769,48.374,134.598,48.416,134.323,48.371]},{"z":4.7,"b":[130.862,55.679,144.745,62.573],"l":[130.923,55.679,130.932,55.749,130.862,55.889,130.924,55.942,130.964,56.129,131.093,56.206,131.088,56.345,131.184,56.468,131.394,56.479,131.502,56.55,131.67,56.55,131.676,56.648,131.797,56.717,131.751,56.843,131.445,57.036,131.474,57.079,131.179,57.137,131.183,57.235,131.509,57.251,131.599,57.296,131.565,57.365,131.617,57.453,131.729,57.5,131.693,57.561,131.975,57.586,132.035,57.691,131.888,57.809,131.757,57.853,131.806,57.906,131.61,57.995,131.477,58.168,131.705,58.174,132.039,58.02,132.017,58.19,132.196,58.248,132.207,58.35,132.129,58.489,132.372,58.524,132.564,58.64,132.599,58.891,132.808,58.88,132.871,58.987,132.862,59.064,133.126,59.204,133.384,59.24,133.543,59.294,133.625,59.232,133.767,59.223,134.047,59.256,134.219,59.209,134.555,59.181,134.725,59.211,134.752,59.153,135.209,59.122,135.26,59.22,135.422,59.387,135.619,59.495,135.791,59.534,136.053,59.438,136.759,59.355,136.836,59.398,137.182,59.446,137.349,59.549,137.456,59.551,137.458,59.647,137.616,59.74,137.833,59.761,138.044,59.741,138.174,59.68,138.253,59.713,138.174,59.837,138.263,59.938,138.2,60.231,138.315,60.318,138.327,60.406,138.243,60.493,138.404,60.611,138.429,60.702,138.284,60.782,138.186,60.895,138.205,60.952,138.346,61,138.383,61.1,138.703,61.193,138.651,61.282,138.719,61.337,138.941,61.321,139.12,61.449,139.247,61.433,139.435,61.491,139.548,61.482,139.667,61.641,139.919,61.802,139.988,61.972,140.114,61.971,140.256,62.038,140.3,62.119,140.294,62.274,140.404,62.422,140.635,62.425,140.705,62.573,140.881,62.499,141.055,62.482,141.067,62.417,141.302,62.431,141.545,62.179,141.838,62.041,142.008,62.027,142.187,62.081,142.29,61.963,142.438,61.87,142.653,61.962,142.826,61.949,142.928,61.901,143.085,62.018,143.126,62.101,143.362,62.024,143.62,62.016,143.571,61.948,143.954,61.957,143.927,61.766,144.002,61.727,144.245,61.76,144.54,61.856,144.586,61.978,144.745,62.047]},{"z":4.7,"b":[70.43,55.285,75.105,58.581],"l":[75.105,58.581,74.671,58.354,74.614,58.294,74.484,58.269,74.206,58.134,73.087,58.146,72.861,58.011,72.476,58.028,72.149,58.026,71.989,58.123,71.269,58.076,71.221,58.166,71.245,58.341,71.302,58.385,71.12,58.453,71.084,58.518,70.865,58.535,70.436,57.934,70.43,57.785,70.565,57.705,70.58,57.584,70.675,57.511,70.436,57.409,70.491,57.304,70.634,57.265,70.729,57.194,70.8,57.319,71.08,57.344,71.144,57.252,71.371,57.064,71.652,56.923,71.673,56.838,71.557,56.732,71.4,56.725,71.357,56.671,71.169,56.676,71.029,56.527,70.855,56.456,70.927,56.347,70.77,56.296,70.85,56.236,70.828,56.16,70.909,56.112,70.847,55.963,70.586,55.838,70.549,55.676,70.747,55.59,70.676,55.505,70.544,55.487,70.471,55.418,70.469,55.285]},{"z":4.7,"b":[61.98,54.163,63.329,56.134],"l":[63.329,54.163,63.25,54.247,63.128,54.306,63.13,54.448,63.238,54.478,63.295,54.564,63.192,54.686,62.903,54.694,62.711,54.626,62.424,54.723,62.379,54.672,62.147,54.644,62.025,54.717,62.123,54.815,61.98,54.934,61.989,55.035,62.244,55.079,62.165,55.242,62.049,55.311,62.234,55.341,62.304,55.403,62.579,55.517,62.516,55.564,62.517,55.738,62.343,55.905,62.305,55.979,62.132,56.065,62.072,56.134]},{"z":4.7,"b":[65.935,58.589,75.159,59.989],"l":[65.935,58.614,66.252,58.614,66.38,58.589,66.741,58.67,66.786,58.72,66.774,58.93,66.951,58.973,67.071,58.925,67.264,59.02,67.564,59.093,67.742,59.212,68.195,59.242,68.358,59.283,68.543,59.551,68.844,59.621,69.295,59.783,69.078,59.893,69.133,59.945,69.306,59.989,69.501,59.989,69.616,59.905,70.155,59.9,70.248,59.84,70.399,59.817,70.798,59.863,71.017,59.823,71.193,59.905,71.296,59.799,71.777,59.648,71.882,59.573,72,59.566,72.355,59.34,72.437,59.303,72.454,59.215,72.657,59.107,72.863,59.157,72.976,59.099,72.967,59,73.095,58.946,73.032,58.863,73.444,58.818,73.621,58.854,73.756,58.809,74,58.8,74.1,58.739,74.743,58.748,74.899,58.696,74.981,58.718,75.159,58.65]},{"z":4.7,"b":[75.105,58.581,75.159,58.65],"l":[75.159,58.65,75.105,58.581]},{"z":4.7,"b":[64.967,55.33,68.772,56.768],"l":[68.772,55.33,68.591,55.424,68.487,55.401,68.317,55.555,68.174,55.596,68.189,55.7,67.912,55.665,67.825,55.721,67.591,55.711,67.447,55.735,67.299,55.897,66.989,55.989,66.802,55.967,66.761,56.045,66.463,55.95,66.298,56.058,66.186,56.068,66.028,55.99,65.861,56.065,65.708,56.195,65.667,56.274,65.117,56.325,65.088,56.448,64.967,56.501,65.016,56.649,64.995,56.768]},{"z":4.7,"b":[64.839,56.768,66.197,58.614],"l":[64.995,56.768,65.14,56.77,65.227,56.86,65.101,56.892,64.994,57.042,64.992,57.221,64.918,57.296,64.842,57.446,64.937,57.549,64.839,57.641,65.428,57.834,65.54,57.78,65.702,57.901,66.197,58.033,66.002,58.297,65.935,58.614]},{"z":4.6,"b":[109.76,21.56,112.047,24.736],"l":[111.997,24.736,111.932,24.698,111.933,24.601,112.006,24.553,111.991,24.466,112.047,24.381,111.976,24.277,111.869,24.232,111.899,23.991,111.808,23.909,111.793,23.849,111.658,23.842,111.614,23.76,111.601,23.658,111.471,23.613,111.469,23.558,111.391,23.431,111.363,23.273,111.37,23.139,111.411,23.053,111.351,22.908,111.277,22.819,111.181,22.749,111.066,22.741,111.024,22.639,110.755,22.58,110.697,22.369,110.775,22.284,110.671,22.268,110.642,22.183,110.467,22.151,110.414,22.198,110.334,22.19,110.355,22.101,110.356,21.889,110.216,21.875,110.142,21.897,109.967,21.867,109.917,21.687,109.76,21.56]},{"z":4.6,"b":[134.255,-4.255,135.265,-2.838],"l":[134.873,-4.255,135.265,-3.76,134.258,-3.399,134.255,-3.308,134.332,-3.19,134.451,-3.078,134.445,-3.024,134.67,-2.838]},{"z":4.6,"b":[104.54,24.631,109.457,26.039],"l":[104.54,24.742,104.729,24.631,104.852,24.706,104.877,24.755,105.012,24.797,105.1,24.954,105.206,25.004,105.289,24.933,105.381,24.957,105.444,24.924,105.509,24.817,105.675,24.79,105.788,24.716,105.916,24.729,106.001,24.648,106.176,24.782,106.199,24.877,106.156,24.96,106.307,24.981,106.587,25.104,106.667,25.174,106.886,25.197,106.909,25.248,106.996,25.255,107.015,25.353,106.975,25.44,107.022,25.498,107.244,25.559,107.398,25.387,107.424,25.303,107.499,25.214,107.642,25.268,107.776,25.153,107.862,25.146,107.967,25.209,108.068,25.213,108.129,25.269,108.132,25.386,108.293,25.539,108.405,25.515,108.568,25.416,108.695,25.602,108.768,25.599,108.774,25.522,108.941,25.549,109.024,25.595,109.044,25.709,108.953,25.691,108.953,25.776,109.085,25.805,109.149,25.751,109.3,25.743,109.399,25.999,109.457,26.039]},{"z":4.6,"b":[109.457,24.643,111.997,26.382],"l":[109.457,26.039,109.603,26.053,109.672,25.985,109.677,25.889,109.772,25.908,109.781,26.024,109.838,26.045,109.912,26.181,110.062,26.15,110.034,26.036,110.138,26.052,110.247,25.978,110.342,26.113,110.485,26.172,110.537,26.225,110.577,26.351,110.689,26.332,110.724,26.274,110.879,26.275,110.935,26.382,111.123,26.306,111.252,26.28,111.257,26.21,111.202,25.93,111.256,25.869,111.408,25.908,111.411,25.803,111.324,25.73,111.305,25.639,111.316,25.503,111.254,25.399,111.164,25.365,111.114,25.239,110.993,25.151,110.955,25.025,111.006,24.935,111.079,24.944,111.126,25.044,111.274,25.148,111.427,25.106,111.462,25.019,111.43,24.943,111.474,24.793,111.428,24.681,111.517,24.643,111.616,24.707,111.667,24.78,111.797,24.76,111.926,24.775,111.997,24.736]},{"z":4.6,"b":[26.397,-25.317,28.294,-24.614],"l":[28.294,-25.317,28.231,-25.226,28.128,-25.2,28.124,-25.11,28.222,-25.078,28.098,-25.009,27.966,-24.985,27.906,-25.003,27.668,-24.971,27.587,-24.997,27.546,-25.093,27.488,-25.056,27.367,-25.059,27.289,-25.007,27.183,-24.991,27.055,-24.85,27.023,-24.724,26.944,-24.79,26.768,-24.866,26.626,-24.827,26.455,-24.812,26.41,-24.741,26.397,-24.614]},{"z":4.6,"b":[27.191,-26.738,28.294,-25.317],"l":[28.294,-25.317,28.235,-25.353,28.104,-25.372,28.004,-25.361,27.949,-25.41,28.033,-25.472,27.964,-25.593,27.943,-25.807,27.876,-25.853,27.728,-25.878,27.641,-25.804,27.459,-25.885,27.445,-26.007,27.395,-26.123,27.29,-26.146,27.324,-26.287,27.191,-26.382,27.23,-26.504,27.243,-26.64,27.335,-26.616,27.569,-26.65,27.583,-26.738]},{"z":4.6,"b":[28.355,-25.411,31.856,-23.963],"l":[31.856,-23.963,31.78,-23.965,31.712,-24.05,31.446,-24.023,31.413,-24.054,31.248,-24.101,31.203,-24.035,31.111,-24.097,31.051,-24.19,31.059,-24.321,31.195,-24.311,31.163,-24.379,31.26,-24.508,31.221,-24.557,30.975,-24.582,30.765,-24.465,30.734,-24.582,30.673,-24.618,30.72,-24.704,30.659,-24.819,30.492,-24.948,30.421,-24.851,30.35,-24.877,30.184,-24.855,30.185,-25.022,30.124,-25.123,30.035,-25.072,30.007,-25.15,30.004,-25.332,29.886,-25.344,29.77,-25.395,29.671,-25.37,29.528,-25.411,29.353,-25.404,29.268,-25.375,29.211,-25.408,29.166,-25.304,29.1,-25.318,29.038,-25.269,29.043,-25.142,28.969,-25.072,28.898,-24.956,28.732,-24.943,28.612,-25.002,28.629,-25.052,28.459,-25.117,28.355,-25.189,28.392,-25.224,28.606,-25.158]},{"z":4.6,"b":[28.294,-25.322,28.663,-25.158],"l":[28.606,-25.158,28.663,-25.247,28.527,-25.27,28.433,-25.322,28.294,-25.317]},{"z":4.6,"b":[28.256,-27.502,29.771,-26.894],"l":[28.256,-26.894,28.386,-26.933,28.426,-26.983,28.641,-27.01,28.652,-26.956,28.823,-27.039,28.959,-26.995,29.057,-27.03,29.126,-27.154,29.178,-27.138,29.442,-27.253,29.485,-27.342,29.615,-27.502,29.771,-27.452]},{"z":4.6,"b":[22.745,-33.98,24.182,-31.79],"l":[23.636,-33.98,23.687,-33.872,23.544,-33.796,23.367,-33.781,23.437,-33.712,23.618,-33.644,23.563,-33.496,23.437,-33.433,23.262,-33.399,23.029,-33.39,22.75,-33.402,22.745,-33.328,22.916,-33.096,22.916,-33.014,23.001,-32.889,23.195,-32.808,23.327,-32.808,23.37,-32.768,23.278,-32.668,23.237,-32.505,23.243,-32.425,23.325,-32.355,23.602,-32.325,23.674,-32.239,23.757,-32.196,23.885,-32.228,24.029,-32.175,24.09,-32.125,24.122,-32.003,24.182,-31.952,24.13,-31.858,24.147,-31.79]},{"z":4.7,"b":[42.489,51.608,43.08,52.403],"l":[42.489,51.608,42.49,51.693,42.643,51.804,42.694,51.966,42.806,52.012,42.751,52.078,42.752,52.174,42.996,52.365,43.08,52.403]},{"z":4.7,"b":[43.08,52.316,46.831,52.695],"l":[43.08,52.403,43.193,52.462,43.353,52.393,43.532,52.426,43.747,52.433,44.044,52.35,44.347,52.316,44.422,52.416,44.539,52.432,44.5,52.521,44.873,52.434,44.91,52.378,45.072,52.377,45.14,52.421,45.264,52.392,45.431,52.4,45.528,52.481,45.644,52.519,45.717,52.471,45.981,52.407,45.971,52.486,46.147,52.591,46.276,52.619,46.289,52.672,46.516,52.695,46.713,52.628,46.831,52.616]},{"z":4.7,"b":[53.47,51.575,58.857,54.046],"l":[53.47,54.046,53.598,53.883,53.656,53.752,53.878,53.658,54.108,53.417,54.247,53.375,54.302,53.41,54.52,53.36,54.559,53.27,54.664,53.236,54.802,53.271,54.927,53.22,54.955,53.11,55.162,52.823,55.264,52.862,55.407,52.837,55.412,52.658,55.545,52.542,55.546,52.385,55.778,52.344,55.889,52.399,55.919,52.499,56.078,52.604,56.365,52.523,56.445,52.42,56.451,52.309,56.194,52.189,56.217,52.132,56.495,52.162,56.627,52.118,56.685,52.045,56.608,51.829,56.748,51.773,56.715,51.721,56.78,51.592,56.89,51.625,56.876,51.699,56.999,51.728,57.036,51.66,57.207,51.575,57.374,51.639,57.432,51.726,57.621,51.759,57.653,51.863,57.784,51.826,57.882,51.846,57.981,51.786,58.092,51.795,58.236,51.738,58.494,51.762,58.576,51.793,58.639,52.016,58.694,52.049,58.606,52.181,58.691,52.265,58.857,52.263,58.828,52.433]},{"z":4.7,"b":[42.873,50.368,47.447,51.231],"l":[47.447,50.368,47.277,50.372,47.184,50.512,47.008,50.48,46.878,50.524,46.83,50.633,46.708,50.691,46.583,50.701,46.483,50.617,46.461,50.547,46.295,50.567,46.255,50.529,46.065,50.544,46.068,50.652,45.89,50.759,45.811,50.774,45.678,50.729,45.668,50.594,45.495,50.613,45.241,50.586,45.191,50.697,45.293,50.781,45.308,50.877,45.215,50.942,45.202,51.04,45.042,51.126,44.66,51.173,44.21,51.194,44.182,51.117,44.004,51.087,43.864,51.113,43.682,51.113,43.642,51.066,43.33,51.018,43.142,51.122,42.873,51.231]},{"z":4.7,"b":[50.796,51.729,52.539,54.377],"l":[50.796,51.729,51.415,52.103,51.453,52.219,51.413,52.35,51.563,52.447,51.471,52.569,51.545,52.681,51.732,52.668,51.772,52.783,51.715,52.84,51.862,52.926,52.061,53,52.069,53.154,52.128,53.174,52.143,53.297,52.193,53.375,52.079,53.501,52.253,53.654,52.413,53.927,52.378,53.966,52.476,54.077,52.339,54.174,52.271,54.327,52.506,54.322,52.539,54.377]},{"z":4.7,"b":[52.539,53.974,53.47,54.377],"l":[52.539,54.377,52.771,54.347,53.001,54.271,52.951,54.196,53.141,54.076,53.258,54.071,53.384,53.974,53.47,54.046]},{"z":4.7,"b":[59.026,56.134,62.072,56.376],"l":[59.026,56.169,59.135,56.206,59.122,56.317,59.194,56.352,59.349,56.318,59.701,56.306,59.789,56.233,59.921,56.265,60.24,56.209,60.315,56.238,60.585,56.214,60.751,56.237,60.909,56.188,61.15,56.217,61.084,56.326,61.15,56.376,61.267,56.331,61.385,56.367,61.537,56.297,61.642,56.319,61.816,56.239,61.864,56.181,62.072,56.134]},{"z":4.7,"b":[85.079,52.612,87.175,54.487],"l":[85.079,54.469,85.16,54.487,85.435,54.212,85.577,54.232,85.811,54.126,85.944,54.04,86.117,54,86.232,53.887,86.292,53.876,86.406,53.751,86.455,53.614,86.569,53.538,86.723,53.484,86.934,53.453,86.983,53.407,86.705,53.196,86.943,53.08,87.077,53.075,87.026,52.921,86.944,52.862,87.074,52.717,87.175,52.706,87.173,52.612]},{"z":4.7,"b":[58.828,51.975,60.149,52.495],"l":[60.064,51.975,59.914,52.1,59.975,52.182,60.14,52.267,60.149,52.421,59.946,52.428,59.781,52.492,59.509,52.435,59.398,52.495,59.249,52.487,59.175,52.277,58.828,52.433]},{"z":4.7,"b":[57.137,52.433,59.975,56.169],"l":[58.828,52.433,58.777,52.525,58.812,52.587,58.724,52.645,58.778,52.704,58.749,52.835,58.9,53.066,58.891,53.188,58.829,53.2,58.865,53.32,58.868,53.502,58.821,53.596,58.907,53.666,58.873,53.758,58.906,53.862,59.014,53.958,59.162,53.993,59.263,54.14,59.344,54.185,59.544,54.196,59.694,54.129,59.801,54.242,59.667,54.34,59.746,54.447,59.679,54.498,59.86,54.603,59.975,54.801,59.903,54.867,59.806,54.846,59.614,54.867,59.404,54.703,59.228,54.611,59.061,54.632,58.825,54.572,58.614,54.47,58.364,54.547,58.206,54.515,57.971,54.393,57.743,54.501,57.742,54.583,57.594,54.629,57.497,54.701,57.386,54.72,57.165,54.819,57.243,54.957,57.137,55.092,57.234,55.15,57.264,55.262,57.518,55.334,57.675,55.3,58.03,55.275,58.144,55.209,57.965,55.159,58.089,55.02,58.147,55.117,58.288,55.171,58.364,55.084,58.62,54.95,58.816,55.025,58.674,55.188,58.797,55.203,58.872,55.323,58.981,55.303,59.159,55.332,59.201,55.45,59.565,55.5,59.447,55.632,59.255,55.614,59.296,55.78,59.165,55.79,59.228,55.911,59.178,55.99,59.269,56.048,59.188,56.127,59.026,56.169]},{"z":4.7,"b":[84.41,54.469,85.079,56.044],"l":[84.41,56.044,84.529,56.042,84.567,55.961,84.506,55.868,84.606,55.841,84.566,55.709,84.709,55.662,84.771,55.54,84.731,55.391,84.875,55.389,84.823,55.246,84.916,55.165,84.883,54.985,85.076,54.868,84.955,54.744,85.043,54.662,84.948,54.604,85.079,54.469]},{"z":4.7,"b":[77.785,53.327,85.079,54.469],"l":[85.079,54.469,84.693,54.337,84.532,54.17,84.266,54.216,84.003,54.144,83.555,54.123,83.492,54.086,83.499,53.991,83.385,53.949,83.295,54.083,83.183,54.062,82.733,53.787,82.486,53.469,82.256,53.591,82.214,53.663,82.104,53.662,81.968,53.719,81.846,53.71,81.756,53.778,81.732,53.854,81.838,53.895,81.855,53.969,81.582,54.017,81.474,54.072,81.247,54.101,81.112,54.152,81.104,54.213,80.994,54.265,80.925,54.396,80.802,54.33,80.655,54.204,80.525,54.17,80.297,54.07,80.128,54.075,80.044,54.001,79.866,53.932,79.862,53.858,79.721,53.841,79.572,53.733,79.442,53.693,79.304,53.716,79.245,53.68,79.028,53.661,78.886,53.682,78.603,53.616,78.627,53.529,78.377,53.488,78.26,53.495,78.204,53.612,77.925,53.565,77.93,53.44,77.785,53.327]},{"z":4.7,"b":[75.097,54.113,76.309,57.25],"l":[76.109,57.25,76.196,56.969,76.249,56.913,76.284,56.726,76.009,56.548,76.108,56.328,76.309,56.224,76.305,56.183,76.006,56.179,75.939,56.129,75.776,56.144,75.696,56.068,75.566,56.054,75.318,55.896,75.342,55.764,75.134,55.699,75.227,55.652,75.178,55.569,75.311,55.5,75.191,55.462,75.097,55.382,75.268,55.25,75.255,55.137,75.394,55.037,75.363,54.908,75.53,54.891,75.64,54.812,75.691,54.713,75.766,54.706,75.83,54.602,75.697,54.544,75.683,54.482,75.691,54.113]},{"z":4.7,"b":[76.109,55.692,84.41,57.25],"l":[76.109,57.25,76.802,57.21,76.919,57.188,78.444,57.161,78.448,57.122,79.59,56.929,80.249,56.455,80.31,56.439,81.122,56.533,81.215,56.508,81.426,56.373,81.557,56.258,81.944,56.339,82.146,56.335,82.342,56.356,82.427,56.394,82.828,56.441,82.856,56.533,82.996,56.508,83.096,56.549,83.252,56.456,83.154,56.382,83.137,56.276,83.214,56.212,83.128,56.132,83.353,55.969,83.407,55.893,83.293,55.851,83.223,55.737,83.316,55.692,83.403,55.735,83.568,55.693,83.783,55.882,83.91,55.913,83.951,56.005,84.197,56.055,84.329,55.988,84.41,56.044]},{"z":4.7,"b":[62.072,56.134,64.995,56.841],"l":[62.072,56.134,62.083,56.291,62.253,56.363,62.333,56.469,62.542,56.578,62.59,56.524,63.001,56.598,63.098,56.569,63.359,56.563,63.448,56.61,63.766,56.508,64.173,56.468,64.237,56.519,64.219,56.601,64.357,56.653,64.337,56.753,64.484,56.76,64.542,56.808,64.826,56.841,64.995,56.768]},{"z":4.7,"b":[75.08,57.25,76.109,58.581],"l":[75.105,58.581,75.303,58.487,75.304,58.451,75.08,58.354,75.222,58.222,75.093,58.11,75.543,57.941,75.581,57.675,75.727,57.599,75.859,57.399,75.992,57.385,76.109,57.25]},{"z":4.6,"b":[113.928,24.5,115.888,25.448],"l":[113.928,25.448,114.012,25.435,114.026,25.268,114.115,25.308,114.279,25.292,114.384,25.335,114.429,25.386,114.569,25.407,114.58,25.36,114.707,25.29,114.683,25.163,114.729,25.119,114.548,25.054,114.414,24.98,114.384,24.885,114.302,24.759,114.192,24.695,114.289,24.596,114.425,24.5,114.498,24.556,114.642,24.585,114.712,24.555,114.74,24.625,114.863,24.588,114.945,24.67,115.047,24.707,115.139,24.684,115.24,24.746,115.421,24.785,115.541,24.685,115.599,24.609,115.706,24.546,115.799,24.573,115.749,24.748,115.787,24.861,115.888,24.917]},{"z":4.6,"b":[115.888,23.616,117.175,24.917],"l":[115.888,24.917,116.036,24.894,116.115,24.846,116.372,24.841,116.53,24.615,116.604,24.656,116.745,24.671,116.749,24.551,116.882,24.394,116.968,24.185,116.925,24.078,117.037,23.738,117.175,23.616]},{"z":4.6,"b":[74.323,23.036,78.2,26.824],"l":[78.2,26.824,78.101,26.795,78.079,26.683,78.013,26.696,77.903,26.664,77.831,26.576,77.715,26.506,77.617,26.483,77.431,26.374,77.323,26.352,77.21,26.242,77.125,26.238,77.082,26.185,76.908,26.099,76.727,25.916,76.652,25.923,76.546,25.848,76.478,25.72,76.551,25.46,76.652,25.379,76.78,25.332,76.913,25.309,77.081,25.353,77.202,25.356,77.255,25.434,77.359,25.407,77.363,25.262,77.399,25.212,77.341,25.114,77.187,25.128,76.999,25.088,76.861,25.034,76.883,24.897,76.781,24.837,76.848,24.76,76.967,24.737,77.037,24.67,77.035,24.538,76.943,24.5,76.881,24.562,76.806,24.556,76.815,24.36,76.892,24.241,76.87,24.182,76.767,24.16,76.573,24.273,76.543,24.215,76.456,24.265,76.23,24.278,76.133,24.329,76.117,24.134,75.999,24.051,75.998,23.984,75.899,23.918,75.776,23.883,75.7,23.793,75.597,23.818,75.578,23.876,75.49,23.94,75.539,24.041,75.622,23.995,75.73,24.001,75.805,24.119,75.748,24.19,75.795,24.252,75.734,24.424,75.792,24.494,75.875,24.514,75.869,24.599,75.759,24.777,75.649,24.738,75.448,24.703,75.204,24.725,75.172,24.763,75.318,24.955,75.235,25.04,75.133,25.04,75.084,24.889,74.981,24.863,74.876,24.919,74.815,24.917,74.838,24.797,74.974,24.786,74.965,24.694,74.87,24.666,74.807,24.736,74.712,24.524,74.847,24.436,74.766,24.359,74.759,24.257,74.87,24.236,74.893,24.134,74.968,24.033,74.908,23.897,74.907,23.791,74.939,23.687,74.823,23.5,74.648,23.431,74.57,23.368,74.536,23.251,74.658,23.226,74.716,23.179,74.612,23.113,74.476,23.062,74.323,23.036]},{"z":4.6,"b":[111.997,24.736,113.928,25.506],"l":[111.997,24.736,112.066,24.797,112.145,24.794,112.165,24.912,112.121,24.949,112.183,25.153,112.214,25.2,112.458,25.164,112.663,25.113,112.714,25,112.808,24.943,112.989,24.964,112.959,25.056,112.969,25.181,112.889,25.241,112.917,25.325,113.09,25.414,113.217,25.506,113.422,25.384,113.523,25.386,113.582,25.326,113.714,25.361,113.828,25.357,113.928,25.448]},{"z":4.6,"b":[74.466,27.679,77.281,29.947],"l":[74.517,29.947,74.547,29.861,74.466,29.799,74.506,29.737,74.603,29.726,74.575,29.59,74.602,29.47,74.537,29.441,74.613,29.333,74.759,29.364,74.821,29.403,74.907,29.378,74.97,29.284,75.107,29.238,75.25,29.256,75.328,29.296,75.392,29.253,75.393,29.106,75.452,29.024,75.515,28.996,75.486,28.927,75.594,28.643,75.699,28.521,75.887,28.404,76.026,28.262,76.032,28.188,75.962,28.139,75.993,28.051,75.932,27.918,76,27.843,76.215,27.842,76.18,27.91,76.163,28.058,76.267,28.077,76.368,28.156,76.453,28.163,76.553,27.971,76.658,28.017,76.647,28.086,76.857,28.229,76.941,28.123,76.921,28.034,76.941,27.852,76.901,27.734,76.973,27.679,77.041,27.807,77.281,27.809]},{"z":4.6,"b":[77.281,26.824,78.223,27.809],"l":[77.281,27.809,77.346,27.542,77.46,27.385,77.606,27.324,77.657,27.203,77.591,27.131,77.513,27.109,77.628,26.973,77.461,26.904,77.511,26.833,77.705,26.93,77.977,26.904,78.107,26.947,78.223,26.937,78.2,26.824]},{"z":4.6,"b":[76.704,30.677,76.827,30.789],"l":[76.815,30.789,76.704,30.781,76.776,30.677,76.827,30.681]},{"z":4.6,"b":[73.898,29.947,74.517,30.222],"l":[74.517,29.947,73.916,29.98,73.898,30.035,73.97,30.137,73.933,30.222]},{"z":4.6,"b":[75.602,30.908,76.768,32.517],"l":[75.847,32.517,75.912,32.431,75.693,32.272,75.623,32.259,75.639,32.147,75.602,32.069,75.707,32.05,75.88,31.972,75.964,31.849,75.923,31.806,76.071,31.564,76.158,31.339,76.275,31.31,76.374,31.419,76.434,31.318,76.561,31.277,76.645,30.999,76.768,30.908]},{"z":4.6,"b":[76.768,30.789,76.833,30.908],"l":[76.768,30.908,76.833,30.842,76.815,30.789]},{"z":4.6,"b":[74.517,29.562,76.905,30.681],"l":[76.827,30.681,76.887,30.634,76.905,30.384,76.753,30.431,76.714,30.337,76.606,30.274,76.625,30.143,76.531,30.084,76.43,30.14,76.267,30.114,76.178,29.955,76.226,29.869,75.957,29.739,75.887,29.749,75.786,29.831,75.718,29.778,75.604,29.765,75.446,29.806,75.312,29.656,75.295,29.575,75.235,29.562,75.172,29.688,75.238,29.749,75.188,29.835,75.105,29.821,75.073,29.891,74.996,29.873,74.903,29.955,74.823,29.985,74.711,29.968,74.651,29.914,74.517,29.947]},{"z":4.7,"b":[43.634,47.477,45.301,48.255],"l":[45.301,48.098,45.137,48.112,44.963,48.209,44.933,48.135,44.766,48.059,44.662,48.089,44.547,48.063,44.54,48.197,44.306,48.255,44.314,48.094,44.448,48.05,44.467,47.99,44.389,47.868,44.083,47.909,44.032,47.816,43.951,47.756,43.822,47.75,43.705,47.683,43.634,47.57,43.732,47.477]},{"z":4.6,"b":[28.256,-26.894,28.976,-25.115],"l":[28.256,-26.894,28.304,-26.829,28.408,-26.759,28.402,-26.659,28.506,-26.613,28.529,-26.565,28.644,-26.508,28.767,-26.516,28.796,-26.427,28.864,-26.401,28.807,-26.308,28.718,-26.338,28.532,-26.221,28.501,-26.08,28.581,-26.059,28.634,-25.973,28.778,-26.012,28.87,-26.005,28.881,-25.896,28.941,-25.87,28.976,-25.674,28.92,-25.552,28.804,-25.542,28.859,-25.632,28.764,-25.706,28.67,-25.664,28.628,-25.527,28.629,-25.333,28.783,-25.228,28.752,-25.115,28.606,-25.158]},{"z":4.7,"b":[45.301,45.546,47.528,48.098],"l":[47.528,45.546,47.47,45.635,47.354,45.684,47.296,45.646,46.922,45.578,46.906,45.637,47.04,45.966,47.17,46.078,47.089,46.118,46.846,46.076,46.856,46.244,46.504,46.274,46.531,46.338,46.664,46.429,46.94,46.432,46.93,46.556,47.022,46.635,47.15,46.668,47.213,46.742,47.126,46.851,46.988,46.863,46.912,46.98,46.801,47.004,46.726,47.089,46.511,47.406,46.526,47.435,46.788,47.557,46.685,47.628,46.449,47.478,46.297,47.437,46.116,47.502,45.843,47.719,45.707,47.908,45.741,48,45.677,48.032,45.547,47.997,45.405,48.092,45.301,48.098]},{"z":4.7,"b":[44.987,48.098,46.644,48.869],"l":[45.301,48.098,45.15,48.238,44.987,48.268,45.042,48.395,45.172,48.439,45.375,48.464,45.301,48.558,45.609,48.593,45.76,48.666,45.948,48.836,46.057,48.869,46.298,48.84,46.644,48.659]},{"z":4.7,"b":[46.831,52.57,48.437,52.81],"l":[46.831,52.616,46.917,52.637,47.062,52.57,47.435,52.57,47.531,52.611,47.731,52.587,47.777,52.68,47.917,52.675,47.998,52.754,48.252,52.81,48.437,52.78]},{"z":4.7,"b":[48.351,51.729,50.794,52.78],"l":[48.437,52.78,48.351,52.669,48.524,52.682,48.7,52.639,48.73,52.568,48.936,52.475,49.159,52.486,49.344,52.469,49.478,52.331,49.628,52.366,49.812,52.193,49.977,52.184,50.221,52.13,50.236,52.035,50.517,51.991,50.732,51.914,50.716,51.841,50.794,51.729]},{"z":4.6,"b":[93.018,24.399,93.449,25.302],"l":[93.449,25.302,93.394,25.235,93.189,24.811,93.124,24.81,93.087,24.707,93.046,24.445,93.018,24.399]},{"z":4.6,"b":[92.298,24.133,93.018,24.471],"l":[93.018,24.399,92.829,24.377,92.746,24.471,92.701,24.362,92.631,24.295,92.624,24.239,92.489,24.133,92.416,24.239,92.298,24.238]},{"z":4.7,"b":[29.418,59.61,30.756,60.266],"l":[29.697,59.95,29.68,59.869,29.85,59.828,30.113,59.663,30.259,59.61,30.502,59.631,30.556,59.686,30.748,59.726,30.756,59.777,30.512,59.862,30.527,59.97,30.374,60.036,30.39,60.123,30.179,60.148,29.633,60.266,29.418,60.169]},{"z":4.6,"b":[93.336,25.302,95.176,27.033],"l":[95.176,27.033,95.031,26.933,94.917,26.951,94.799,26.814,94.69,26.741,94.487,26.677,94.358,26.498,94.207,26.484,94.177,26.372,94.008,26.184,93.973,26.04,93.989,25.946,93.904,25.857,93.752,25.959,93.709,25.87,93.581,25.763,93.524,25.689,93.336,25.538,93.459,25.381,93.449,25.302]},{"z":4.7,"b":[34.73,59.103,35.686,61.188],"l":[35.604,61.188,35.686,61.121,35.489,60.959,35.44,60.862,35.281,60.85,35.154,60.787,35.259,60.585,35.231,60.341,35.147,60.269,35.157,60.067,35.238,59.985,35.404,59.974,35.382,59.875,35.424,59.701,35.497,59.647,35.496,59.554,35.324,59.507,35.449,59.428,35.301,59.301,35.135,59.254,34.901,59.238,34.73,59.103]},{"z":4.7,"b":[29.975,58.445,34.73,59.422],"l":[34.73,59.103,34.568,59.108,34.247,59.198,34.1,59.145,33.855,59.198,33.716,59.302,33.52,59.373,33.368,59.38,33.246,59.326,32.955,59.422,32.732,59.267,32.706,59.142,32.584,59.181,32.291,59.207,32.124,59.394,31.882,59.375,31.827,59.314,31.563,59.368,31.452,59.092,31.258,58.981,31.16,59.073,30.981,59.06,30.962,58.975,30.728,58.829,30.687,58.746,30.413,58.711,30.28,58.754,30.101,58.73,30.088,58.64,30.167,58.533,29.975,58.445]},{"z":4.7,"b":[35.604,61.188,37.773,61.594],"l":[37.773,61.522,37.278,61.501,36.996,61.594,36.873,61.545,36.578,61.529,36.467,61.407,35.604,61.188]},{"z":4.7,"b":[29.277,60.646,35.604,61.32],"l":[35.604,61.188,35.392,61.162,35.258,61.254,34.822,61.278,34.741,61.32,34.544,61.27,34.457,61.168,34.216,61.242,33.535,61.221,33.452,61.134,33.585,61.071,33.696,61.122,33.867,61.071,33.752,60.944,33.406,60.989,33.423,60.936,33.253,60.899,33.078,60.738,32.91,60.724,32.703,60.646,29.87,61.176,29.576,61.171,29.491,61.239,29.277,61.305]},{"z":4.7,"b":[29.362,66.356,33.05,66.684],"l":[33.05,66.576,32.594,66.582,32.531,66.489,32.363,66.467,32.329,66.359,31.902,66.356,31.785,66.503,31.652,66.537,31.555,66.655,31.073,66.684,29.362,66.627]},{"z":4.7,"b":[33.046,66.574,33.184,66.576],"l":[33.184,66.574,33.046,66.576]},{"z":4.7,"b":[35.882,61.522,37.927,63.971],"l":[36.523,63.971,36.555,63.764,36.375,63.645,35.945,63.62,35.882,63.573,36.058,63.46,36.22,63.416,36.262,63.236,36.32,63.161,36.259,63.032,36.501,62.826,36.76,62.718,37.081,62.656,37.352,62.684,37.505,62.645,37.588,62.473,37.573,62.361,37.43,62.237,37.719,62.203,37.808,61.998,37.927,61.968,37.857,61.698,37.762,61.648,37.773,61.522]},{"z":4.6,"b":[103.596,24.742,105.297,27.722],"l":[105.297,27.722,105.197,27.399,105.083,27.413,104.871,27.31,104.783,27.332,104.594,27.314,104.444,27.347,104.344,27.441,104.174,27.272,103.981,27.4,103.911,27.39,103.859,27.315,103.695,27.152,103.596,27.076,103.701,27.052,103.778,26.956,103.778,26.88,103.717,26.799,103.775,26.732,103.758,26.629,103.829,26.547,104.02,26.514,104.153,26.651,104.218,26.627,104.359,26.648,104.43,26.713,104.466,26.613,104.559,26.585,104.627,26.498,104.676,26.379,104.545,26.27,104.505,26.147,104.499,26.031,104.393,25.948,104.421,25.857,104.306,25.665,104.422,25.579,104.433,25.507,104.554,25.485,104.566,25.381,104.669,25.297,104.72,25.211,104.689,25.101,104.707,25.017,104.546,24.813,104.54,24.742]},{"z":4.6,"b":[104.495,23.152,106.194,24.742],"l":[104.54,24.742,104.495,24.698,104.502,24.578,104.599,24.403,104.667,24.34,104.729,24.364,104.753,24.468,104.961,24.416,105.061,24.431,105.176,24.31,105.17,24.167,105.253,24.086,105.32,24.118,105.447,24.038,105.532,24.126,105.608,24.14,105.641,24.082,105.788,24.021,105.872,24.025,105.971,24.088,106.157,23.974,106.194,23.869,106.134,23.797,106.152,23.75,106.128,23.541,105.983,23.469,105.848,23.519,105.702,23.359,105.629,23.347,105.619,23.285,105.512,23.152]},{"z":4.7,"b":[45.706,44.752,46.888,45.001],"l":[45.706,44.976,45.854,45.001,46.434,44.937,46.603,44.885,46.888,44.752]},{"z":4.6,"b":[99.099,26.035,105.297,29.224],"l":[99.109,29.224,99.121,28.965,99.099,28.856,99.118,28.709,99.169,28.602,99.163,28.438,99.275,28.277,99.382,28.185,99.416,28.255,99.389,28.334,99.393,28.543,99.503,28.579,99.534,28.681,99.629,28.815,99.705,28.845,99.732,28.745,99.828,28.673,99.846,28.604,99.934,28.569,100.001,28.456,100.143,28.342,100.162,28.236,100.047,28.187,100.059,28.066,100.202,27.899,100.296,27.853,100.349,27.747,100.41,27.829,100.533,27.816,100.625,27.897,100.661,27.875,100.872,27.562,100.867,27.495,100.941,27.435,100.992,27.331,101.004,27.198,101.173,27.191,101.141,27.045,101.342,26.871,101.371,26.775,101.503,26.766,101.495,26.706,101.412,26.57,101.544,26.495,101.644,26.349,101.58,26.307,101.595,26.209,101.677,26.228,101.764,26.176,101.776,26.107,101.85,26.035,101.922,26.099,102.113,26.084,102.252,26.229,102.358,26.263,102.401,26.312,102.568,26.379,102.626,26.345,102.61,26.26,102.72,26.238,102.878,26.365,102.951,26.345,103.003,26.4,102.996,26.485,103.058,26.557,102.975,26.831,102.901,26.925,102.878,27.032,102.914,27.112,102.889,27.291,102.977,27.389,103.121,27.412,103.218,27.567,103.415,27.743,103.509,27.865,103.514,27.971,103.435,28.068,103.459,28.135,103.572,28.243,103.666,28.263,103.709,28.216,103.88,28.326,103.795,28.531,103.837,28.605,104.057,28.63,104.414,28.64,104.44,28.605,104.25,28.531,104.268,28.334,104.377,28.282,104.445,28.1,104.334,28.047,104.375,27.962,104.482,27.891,104.605,27.846,104.702,27.883,104.897,27.901,104.958,27.957,104.966,28.048,105.056,28.088,105.172,28.062,105.203,28.002,105.282,28.001,105.228,27.856,105.297,27.722]},{"z":4.7,"b":[45.509,42.474,46.586,44.015],"l":[45.509,44.015,45.71,43.955,45.803,43.978,46.045,43.934,46.016,43.876,46.22,43.863,46.257,43.948,46.349,43.998,46.462,43.962,46.452,43.887,46.586,43.872,46.585,43.772,46.483,43.672,46.439,43.541,46.338,43.458,46.36,43.361,46.423,43.315,46.403,43.114,46.534,42.961,46.425,42.877,46.336,42.904,46.123,42.843,46.17,42.73,46.095,42.715,45.948,42.575,45.821,42.487,45.727,42.474]},{"z":4.7,"b":[45.124,44.015,45.706,44.976],"l":[45.509,44.015,45.506,44.19,45.124,44.202,45.131,44.268,45.404,44.318,45.444,44.402,45.398,44.444,45.237,44.46,45.271,44.591,45.435,44.617,45.614,44.766,45.621,44.899,45.706,44.976]},{"z":4.7,"b":[33.501,53.311,35.104,54.026],"l":[33.501,54.016,33.682,54.026,33.78,53.982,33.86,53.879,34.031,53.862,34.199,53.892,34.385,53.719,34.49,53.666,34.488,53.544,34.582,53.428,34.673,53.39,35.017,53.381,35.104,53.311]},{"z":4.7,"b":[34.805,52.353,35.314,53.311],"l":[35.104,53.311,35.122,53.186,35.182,53.102,35.314,53.066,35.157,52.969,34.967,52.97,34.805,52.868,34.869,52.784,34.948,52.783,35.092,52.698,35.011,52.614,34.99,52.511,34.833,52.424,34.897,52.353]},{"z":4.7,"b":[33.501,54.016,35.37,55.239],"l":[35.37,55.239,35.284,55.112,35.144,55.022,35.002,54.887,34.866,54.884,34.921,54.772,34.817,54.709,34.5,54.632,34.416,54.451,34.326,54.43,34.108,54.55,33.927,54.572,33.729,54.562,33.643,54.485,33.617,54.381,33.684,54.321,33.562,54.156,33.501,54.016]},{"z":4.7,"b":[32.706,53.42,33.501,54.016],"l":[33.501,54.016,33.475,53.972,33.287,53.954,33.289,53.822,32.899,53.569,32.908,53.493,32.706,53.42]},{"z":4.7,"b":[31.467,55.632,35.174,56.075],"l":[31.467,55.729,31.586,55.747,31.755,55.714,31.894,55.77,32.094,55.674,32.722,55.69,32.939,55.632,33.188,55.653,33.225,55.698,33.372,55.703,33.505,55.755,33.67,55.865,33.66,55.946,33.838,55.949,33.917,56.073,34.201,56.075,34.631,55.918,35.031,55.984,35.174,55.948]},{"z":4.7,"b":[35.174,55.239,35.397,55.948],"l":[35.174,55.948,35.354,55.779,35.316,55.727,35.397,55.617,35.303,55.571,35.301,55.299,35.37,55.239]},{"z":4.6,"b":[77.585,28.737,80.073,30.395],"l":[80.073,28.821,80.001,28.737,79.844,28.805,79.841,28.855,79.743,28.885,79.699,28.85,79.526,28.878,79.444,28.85,79.371,28.97,79.296,28.972,79.171,29.051,79.056,29.152,78.95,29.127,78.868,29.215,78.722,29.284,78.907,29.388,78.643,29.538,78.479,29.722,78.381,29.762,78.228,29.947,78.155,29.925,78.189,29.707,78.107,29.655,77.983,29.719,77.831,29.66,77.784,29.742,77.747,29.878,77.746,29.977,77.835,30.131,77.929,30.244,77.751,30.326,77.662,30.395,77.585,30.383]},{"z":4.6,"b":[77.585,30.383,78.946,31.29],"l":[77.585,30.383,77.813,30.521,77.706,30.791,77.789,30.931,77.806,31.051,77.941,31.173,78.04,31.172,78.284,31.29,78.369,31.29,78.501,31.22,78.582,31.231,78.793,31.207,78.87,31.122,78.946,31.138]},{"z":4.6,"b":[87.768,25.214,88.297,26.539],"l":[87.784,25.214,87.838,25.278,87.768,25.436,87.956,25.534,88.043,25.547,88.046,25.669,87.911,25.765,87.805,25.874,87.831,26.011,87.979,26.146,88.08,26.193,88.18,26.286,88.297,26.336,88.282,26.428,88.195,26.501,88.087,26.539]},{"z":4.6,"b":[110.899,-2.909,113.776,0.559],"l":[113.776,0.559,113.647,0.41,113.623,0.311,113.532,0.261,113.416,0.275,113.325,0.245,113.453,0.066,113.479,-0.048,113.429,-0.19,113.326,-0.252,113.249,-0.361,113.301,-0.423,113.256,-0.551,113.161,-0.47,113.084,-0.518,113.004,-0.519,112.755,-0.636,112.578,-0.704,112.497,-0.702,112.38,-0.755,112.119,-0.735,112.059,-0.796,111.969,-0.823,111.651,-1.015,111.587,-1.102,111.577,-1.187,111.416,-1.269,111.327,-1.373,111.242,-1.421,111.134,-1.53,111.012,-1.526,110.92,-1.613,111.017,-1.66,111.025,-1.867,110.995,-1.985,111.012,-2.127,111.138,-2.405,111.138,-2.733,111.067,-2.765,110.899,-2.909]},{"z":4.6,"b":[86.687,21.614,87.477,22.218],"l":[87.477,21.614,87.409,21.786,87.275,21.814,87.232,21.848,87.191,21.956,87.113,21.971,87.059,21.907,86.994,21.972,86.987,22.067,86.834,22.122,86.687,22.218]},{"z":4.6,"b":[85.829,22.218,87.895,25.214],"l":[86.687,22.218,86.78,22.287,86.79,22.416,86.718,22.469,86.749,22.545,86.611,22.604,86.587,22.681,86.399,22.785,86.415,22.943,86.45,23.004,86.142,23.028,86.02,23.153,85.888,23.163,85.83,23.261,85.895,23.416,85.829,23.483,85.996,23.512,86.056,23.603,86.135,23.559,86.121,23.501,86.214,23.449,86.336,23.508,86.348,23.568,86.459,23.639,86.646,23.697,86.8,23.726,86.81,23.818,86.877,23.913,87.08,23.815,87.166,23.81,87.241,23.851,87.302,24.057,87.338,24.002,87.489,24.047,87.487,24.124,87.572,24.118,87.662,24.186,87.631,24.269,87.721,24.318,87.8,24.501,87.791,24.562,87.877,24.589,87.895,24.67,87.853,24.758,87.877,24.939,87.844,25.042,87.781,25.09,87.784,25.214]},{"z":4.6,"b":[89.658,26.187,89.888,26.722],"l":[89.658,26.187,89.727,26.206,89.782,26.309,89.829,26.328,89.888,26.533,89.859,26.722]},{"z":4.6,"b":[88.001,27.085,88.756,27.248],"l":[88.756,27.149,88.664,27.173,88.518,27.176,88.448,27.114,88.357,27.085,88.233,27.137,88.118,27.138,88.001,27.248]},{"z":4.6,"b":[92.139,24.238,92.298,24.545],"l":[92.298,24.238,92.22,24.264,92.258,24.396,92.222,24.5,92.139,24.545]},{"z":4.6,"b":[89.809,25.005,92.806,26.089],"l":[92.385,25.005,92.565,25.152,92.806,25.228,92.79,25.349,92.588,25.499,92.593,25.587,92.382,25.764,92.167,25.69,92.165,25.809,92.215,25.919,92.184,25.993,92.261,26.055,92.164,26.088,91.922,26.038,91.825,26.089,91.752,26.048,91.718,25.968,91.582,26.027,91.475,25.896,91.361,25.843,91.251,25.738,91.208,25.736,91.179,25.87,91.097,25.84,90.975,25.944,90.753,25.962,90.644,25.931,90.618,25.97,90.485,26.012,90.136,25.96,89.974,25.802,89.942,25.691,90.002,25.587,89.897,25.541,89.875,25.46,89.809,25.441]},{"z":4.7,"b":[98.634,52.105,99.25,52.954],"l":[98.94,52.105,98.932,52.192,98.809,52.196,98.799,52.274,98.646,52.309,98.634,52.413,98.806,52.514,98.805,52.611,98.937,52.673,98.938,52.798,98.856,52.847,98.929,52.918,99.22,52.865,99.25,52.954]},{"z":4.7,"b":[99.25,51.132,116.17,57.24],"l":[99.25,52.954,99.366,53.054,99.379,53.113,99.536,53.105,99.69,53.132,99.805,53.19,99.878,53.299,100.08,53.3,100.26,53.375,100.401,53.234,100.363,53.137,100.606,52.944,100.827,52.856,100.852,52.811,101.031,52.769,101.32,52.641,101.436,52.631,101.476,52.571,101.688,52.461,101.743,52.402,101.91,52.317,101.963,52.222,102.046,52.212,102.188,52.276,102.32,52.231,102.395,52.259,102.542,52.228,102.574,52.149,102.673,52.098,102.777,51.985,102.945,51.907,103.065,51.883,103.222,51.788,103.181,51.621,103.335,51.434,103.576,51.479,103.69,51.46,103.819,51.388,103.814,51.132,104.098,51.218,104.212,51.281,104.494,51.309,104.609,51.268,104.67,51.399,104.629,51.443,104.65,51.622,104.899,51.669,105.123,51.731,105.461,51.863,105.752,52.031,105.992,52.221,106.198,52.44,108.041,53.193,108.061,53.205,108.423,53.783,109.007,54.425,109.022,54.494,108.671,54.547,108.64,54.601,108.716,54.785,108.69,55.062,108.782,55.17,108.773,55.237,108.651,55.313,108.622,55.456,108.771,55.623,108.854,55.866,108.96,55.941,109.18,55.913,109.105,56.022,108.991,56.04,108.929,56.148,108.799,56.257,108.571,56.294,108.557,56.346,108.719,56.499,108.699,56.583,108.93,56.588,109.143,56.676,109.314,56.672,109.41,56.735,109.665,56.694,109.78,56.711,109.887,56.891,109.989,56.969,110.248,56.925,110.399,56.921,110.532,56.847,110.662,56.833,110.753,56.874,110.883,56.834,110.998,56.858,111.134,56.815,111.363,56.895,111.337,56.95,111.462,57.102,111.62,57.095,111.628,57.016,111.854,56.913,111.916,56.945,112.11,56.944,112.235,56.878,112.444,56.863,112.662,56.96,112.762,56.753,112.886,56.697,113.356,56.626,113.547,56.672,113.803,56.602,113.87,56.533,114.01,56.545,114.127,56.676,114.289,56.692,114.498,56.671,114.795,56.71,114.851,56.837,115.05,56.991,115.086,57.119,115.45,57.097,115.698,57.137,115.852,57.24,116.156,57.229,116.17,57.141,116.048,57.082,115.805,57.031,115.711,56.964]},{"z":4.7,"b":[41.656,45.996,44.334,47.575],"l":[43.732,47.477,43.799,47.371,43.802,47.283,43.939,47.269,43.953,47.365,44.11,47.414,44.117,47.533,44.228,47.575,44.334,47.475,44.328,47.111,44.262,46.9,43.93,46.723,43.817,46.589,43.928,46.521,43.918,46.416,43.786,46.407,43.646,46.216,43.559,46.169,43.356,46.172,43.08,46.36,42.912,46.435,42.787,46.389,42.597,46.411,42.409,46.525,42.181,46.578,42.034,46.543,42.1,46.407,42.202,46.314,41.995,46.301,41.915,46.185,41.84,46.153,41.726,46.214,41.656,46.105,41.713,45.996]},{"z":4.7,"b":[41.162,45.947,41.713,45.996],"l":[41.713,45.996,41.658,45.976,41.42,45.995,41.375,45.947,41.162,45.954]},{"z":4.7,"b":[40.081,49.576,41.362,50.226],"l":[41.362,50.226,41.288,50.179,41.341,50.075,41.179,50.016,41.056,49.916,41.029,49.794,40.885,49.69,40.612,49.648,40.411,49.597,40.17,49.624,40.081,49.576]},{"z":4.7,"b":[41.362,47.45,43.732,50.226],"l":[41.362,50.226,41.492,50.084,41.595,50.045,41.617,49.962,41.876,49.9,42.115,49.75,42.178,49.668,42.072,49.502,42.053,49.402,42.092,49.289,42.037,49.267,42.095,49.141,42.344,49.152,42.411,49.047,42.644,48.977,42.74,48.786,42.651,48.705,42.701,48.624,42.634,48.552,42.442,48.505,42.277,48.498,42.165,48.464,42.074,48.392,42.147,48.287,42.051,48.23,42.096,48.154,42.03,48.029,42.283,48.016,42.343,48.045,42.492,48.014,42.728,47.854,42.785,47.731,42.955,47.51,43.054,47.573,43.263,47.45,43.482,47.484,43.732,47.477]},{"z":4.7,"b":[42.489,51.231,42.873,51.608],"l":[42.489,51.608,42.734,51.479,42.861,51.381,42.873,51.231]},{"z":4.7,"b":[41.168,50.226,42.873,51.239],"l":[42.873,51.231,42.715,51.239,42.573,51.169,42.255,51.132,42.067,51.16,41.977,51.203,41.855,51.189,41.737,51.026,41.444,50.917,41.37,50.831,41.168,50.781,41.314,50.725,41.4,50.625,41.526,50.599,41.424,50.482,41.509,50.392,41.362,50.226]},{"z":4.7,"b":[38.299,51.92,40.504,52.098],"l":[38.299,51.961,38.529,52.024,38.698,52.098,38.762,52.052,38.88,52.093,38.994,52.001,39.182,52.098,39.447,52.082,39.497,51.973,39.834,51.92,39.913,51.98,40.088,51.985,40.295,51.956,40.504,51.954]},{"z":4.7,"b":[40.504,51.6,42.489,51.954],"l":[40.504,51.954,40.735,51.94,40.938,51.89,40.892,51.743,41.066,51.71,41.268,51.735,41.311,51.794,41.556,51.767,41.952,51.656,42.248,51.694,42.274,51.6,42.403,51.633,42.489,51.608]},{"z":4.7,"b":[35.313,50.906,38.292,51.42],"l":[38.292,51.292,38.027,51.366,38.046,51.42,37.829,51.42,37.667,51.366,37.539,51.394,37.379,51.385,37.249,51.331,37.144,51.343,37.054,51.247,36.93,51.181,36.856,51.186,36.618,51.08,36.314,51.103,35.976,51.158,36,51.058,35.877,51.012,35.817,50.945,35.546,50.906,35.512,50.947,35.313,50.959]},{"z":4.7,"b":[38.292,49.856,39.234,51.292],"l":[38.292,51.292,38.446,51.257,38.501,51.189,38.396,51.157,38.397,51.077,38.516,51.062,38.618,50.998,38.738,51.074,38.846,51.036,38.874,50.911,38.737,50.901,38.724,50.79,38.966,50.647,39.072,50.636,39.098,50.506,38.99,50.433,39.085,50.349,39.099,50.173,39.234,50.056,39.175,49.856]},{"z":4.7,"b":[37.747,51.879,38.299,52.017],"l":[37.747,52.017,37.821,51.912,37.997,51.879,38.101,51.927,38.192,51.912,38.299,51.961]},{"z":4.7,"b":[38.207,51.292,38.41,51.961],"l":[38.299,51.961,38.41,51.92,38.41,51.688,38.22,51.708,38.207,51.629,38.276,51.526,38.323,51.341,38.292,51.292]},{"z":4.7,"b":[34.372,51.799,34.917,52.353],"l":[34.897,52.353,34.917,52.322,34.805,52.185,34.715,52.216,34.672,52.065,34.531,52.07,34.407,52.013,34.372,51.799]},{"z":4.7,"b":[34.897,51.936,37.747,52.414],"l":[34.897,52.353,35.124,52.335,35.241,52.39,35.396,52.414,35.572,52.387,35.637,52.332,35.893,52.274,36.001,52.313,36.146,52.319,36.357,52.366,36.552,52.215,36.729,52.176,36.82,52.12,37.158,52.072,37.354,51.936,37.481,52.009,37.652,51.971,37.747,52.017]},{"z":4.7,"b":[130.41,49.244,134.956,55.704],"l":[130.923,55.679,131.087,55.618,131.301,55.618,131.599,55.652,131.804,55.614,131.962,55.658,132.126,55.645,132.24,55.704,132.538,55.676,132.663,55.561,132.644,55.491,132.908,55.359,132.733,55.349,132.581,55.196,132.355,55.191,132.367,55.056,132.087,55.05,131.971,54.998,131.994,54.909,131.612,54.781,131.532,54.722,131.399,54.719,131.199,54.613,131.22,54.527,131.114,54.323,130.944,54.323,130.774,54.288,130.507,54.12,130.484,53.993,130.41,53.948,130.447,53.882,130.635,53.887,130.84,53.828,130.921,53.768,131.097,53.813,131.421,53.751,131.444,53.627,131.498,53.555,131.483,53.476,131.532,53.319,131.465,53.213,131.8,53.241,131.894,53.128,131.991,53.134,132.112,53.223,132.398,53.244,132.594,53.23,132.893,53.235,132.951,53.289,133.153,53.291,133.21,53.421,133.573,53.546,133.709,53.477,134.011,53.433,134.215,53.472,134.223,53.515,134.451,53.538,134.555,53.621,134.748,53.539,134.761,53.475,134.924,53.417,134.897,53.315,134.956,53.254,134.859,53.184,134.812,53.066,134.659,52.939,134.631,52.801,134.706,52.766,134.782,52.647,134.659,52.575,134.663,52.471,134.606,52.418,134.224,52.489,134.056,52.5,134.003,52.539,133.659,52.572,133.369,52.683,133.28,52.664,133.238,52.567,133.246,52.456,133.42,52.251,133.322,52.198,132.907,52.159,132.59,52.082,132.525,51.963,132.404,51.963,132.423,51.86,132.279,51.793,132.216,51.817,131.959,51.737,131.864,51.755,131.728,51.681,131.431,51.688,131.395,51.607,131.484,51.476,131.488,51.345,131.29,51.364,131.169,51.249,131.065,51.246,130.919,51.024,130.816,51.02,130.772,50.942,130.841,50.881,130.688,50.746,130.645,50.657,130.954,50.65,131.011,50.568,130.866,50.457,130.989,50.382,131.134,50.419,131.158,50.345,131.29,50.366,131.325,50.269,131.442,50.186,131.295,49.99,131.445,49.964,131.499,49.901,131.469,49.73,131.384,49.659,131.5,49.611,131.494,49.411,131.396,49.244]},{"z":4.7,"b":[130.554,48.86,131.396,49.25],"l":[131.396,49.244,131.182,49.25,131.007,49.051,130.983,48.972,130.829,48.983,130.554,48.86]},{"z":4.7,"b":[119.898,55.466,130.923,57.032],"l":[119.898,56.889,120.031,56.899,120.303,57.004,120.656,57.032,121.019,57.031,121.053,56.99,121.407,56.976,121.511,56.922,121.602,56.813,121.608,56.741,122.16,56.738,122.283,56.651,122.467,56.602,122.589,56.49,122.781,56.498,122.919,56.551,123.132,56.457,123.453,56.406,123.915,56.377,124.06,56.231,124.206,56.165,124.243,56.081,124.36,56.057,124.448,55.905,124.635,55.902,124.958,55.826,125.248,55.88,125.437,55.837,125.546,55.736,125.758,55.796,126.039,55.728,126.047,55.589,126.234,55.591,126.434,55.555,126.551,55.633,126.843,55.606,126.954,55.687,127.188,55.691,127.33,55.656,127.487,55.555,127.573,55.58,127.675,55.683,127.794,55.629,128.29,55.648,128.502,55.516,128.663,55.521,128.772,55.469,128.975,55.466,129.089,55.533,129.047,55.654,129.444,55.7,129.653,55.745,129.892,55.734,129.995,55.693,130.742,55.719,130.923,55.679]},{"z":4.7,"b":[123.162,43.436,131.253,46.272],"l":[131.253,43.469,131.149,43.436,130.995,43.508,130.917,43.45,130.589,43.628,130.414,43.669,130.333,43.928,130.28,43.964,130.104,43.845,130.033,43.856,130,43.984,129.913,44.022,129.802,43.965,129.752,43.874,129.535,43.87,129.249,43.786,129.216,43.738,129.229,43.608,129.015,43.54,128.883,43.556,128.764,43.709,128.747,43.802,128.632,43.912,128.529,44.08,128.446,44.154,128.451,44.316,128.419,44.45,128.359,44.502,128.207,44.44,128.155,44.353,128.043,44.341,128.052,44.126,127.85,44.056,127.744,44.086,127.715,44.161,127.61,44.22,127.618,44.272,127.493,44.411,127.559,44.574,127.375,44.649,127.233,44.616,127.147,44.655,127.045,44.598,127.032,44.731,126.978,44.826,127.077,44.934,126.953,45.134,126.789,45.149,126.559,45.247,126.421,45.228,126.15,45.141,125.841,45.235,125.705,45.36,125.699,45.511,125.521,45.471,125.432,45.477,125.307,45.417,125.104,45.395,125.028,45.498,124.872,45.449,124.612,45.448,124.563,45.415,124.396,45.451,124.332,45.547,124.224,45.636,124.141,45.632,124.018,45.782,124.062,45.872,123.979,45.978,124.034,46.018,123.919,46.272,123.356,46.235,123.258,46.265,123.162,46.22]},{"z":4.7,"b":[119.67,53.43,122.127,56.889],"l":[119.898,56.889,119.67,56.771,119.787,56.7,119.809,56.612,119.96,56.493,120.257,56.459,120.276,56.386,120.533,56.285,120.527,56.232,120.344,56.22,120.062,56.138,120.055,56.078,120.26,55.916,120.466,55.872,120.532,55.914,120.986,56.02,121.217,56.013,121.276,55.867,121.347,55.813,121.307,55.742,121.363,55.507,121.439,55.481,121.634,55.535,121.693,55.6,121.883,55.606,121.984,55.476,121.939,55.214,121.95,55.007,121.667,54.821,121.659,54.728,121.826,54.784,121.922,54.733,121.895,54.646,121.936,54.566,122.127,54.468,122.115,54.407,121.902,54.369,121.866,54.419,121.731,54.426,121.655,54.355,121.753,54.272,121.754,54.152,121.641,54.113,121.643,54.012,121.783,53.973,121.919,53.831,121.947,53.605,122.018,53.521,122.025,53.43]},{"z":4.7,"b":[115.711,56.726,119.108,58.398],"l":[115.711,56.964,115.86,56.932,115.957,56.865,116.122,56.813,116.362,56.847,116.629,56.749,116.815,56.726,117.012,56.806,117.141,56.809,117.18,56.866,117.571,56.851,117.576,56.935,117.673,56.978,117.677,57.066,117.601,57.15,117.723,57.187,117.794,57.287,117.597,57.321,117.427,57.264,117.357,57.337,117.321,57.457,117.257,57.509,117.282,57.617,117.14,57.668,117.173,57.736,117.128,57.842,117.313,57.847,117.368,57.934,117.375,58.061,117.438,58.152,117.588,58.184,117.471,58.304,117.684,58.398,118.142,58.386,118.317,58.353,118.405,58.262,118.63,58.175,118.784,58.213,119.108,58.223]},{"z":4.7,"b":[119.047,56.889,119.898,58.223],"l":[119.108,58.223,119.154,58.146,119.135,58.007,119.047,57.728,119.165,57.669,119.141,57.539,119.41,57.615,119.487,57.591,119.41,57.497,119.442,57.371,119.546,57.34,119.683,57.157,119.622,57.101,119.645,57.002,119.772,56.983,119.898,56.889]},{"z":4.7,"b":[90.477,51.781,96.684,53.639],"l":[90.477,52.183,90.582,52.112,90.744,52.063,90.839,51.912,91.101,51.943,91.18,51.89,91.543,51.901,91.616,51.869,91.894,51.853,91.987,51.794,92.43,51.802,92.505,51.781,92.673,51.84,92.844,51.832,92.901,51.895,93.075,51.945,93.36,52.129,93.678,52.224,93.768,52.371,93.774,52.446,93.989,52.585,94.139,52.761,94.141,52.832,94.29,52.9,94.512,52.911,94.604,53.055,94.604,53.176,94.69,53.224,94.594,53.316,94.74,53.383,94.976,53.405,95.041,53.376,95.26,53.456,95.381,53.372,95.622,53.449,95.768,53.52,96.048,53.454,96.186,53.515,96.241,53.613,96.418,53.543,96.642,53.548,96.684,53.639]},{"z":4.7,"b":[96.684,52.954,99.25,53.729],"l":[96.684,53.639,96.743,53.711,96.875,53.729,97.135,53.613,97.328,53.596,97.448,53.461,97.586,53.438,97.699,53.379,97.972,53.352,98.031,53.253,98.28,53.226,98.303,53.104,98.615,53.108,98.698,53.155,99.056,53.104,98.989,53.034,99.25,52.954]},{"z":4.7,"b":[107.745,49.967,116.927,56.964],"l":[115.711,56.964,115.589,56.902,115.571,56.732,115.434,56.69,115.49,56.62,115.688,56.595,115.673,56.54,115.801,56.466,115.684,56.425,115.688,56.327,115.774,56.206,115.765,56.086,115.889,55.965,115.844,55.857,115.845,55.705,115.892,55.641,115.895,55.525,116.072,55.415,116.178,55.391,116.376,55.409,116.458,55.34,116.476,55.253,116.716,55.145,116.774,55.148,116.893,54.977,116.927,54.826,116.884,54.738,116.774,54.69,116.777,54.627,116.673,54.548,116.458,54.517,116.19,54.538,115.934,54.504,115.629,54.442,115.526,54.404,115.491,54.329,115.269,54.257,115.164,54.162,115.023,54.098,114.966,54.023,114.86,53.981,114.588,53.927,114.501,53.845,114.378,53.844,114.326,53.79,113.983,53.744,113.927,53.681,113.793,53.63,113.878,53.562,113.842,53.382,113.936,53.358,113.978,53.24,114.201,53.187,114.132,53.1,114.325,53.068,114.233,52.961,114.235,52.822,114.088,52.695,113.965,52.633,113.744,52.574,113.573,52.453,113.236,52.469,112.95,52.343,112.68,52.331,112.545,52.347,112.405,52.208,112.185,52.288,112.062,52.277,111.796,52.09,111.724,52.006,111.586,51.922,111.454,51.878,111.125,51.818,110.845,51.66,110.862,51.595,110.677,51.504,110.423,51.523,110.389,51.577,110.049,51.616,109.629,51.449,109.247,51.366,109.047,51.5,108.942,51.44,108.757,51.449,108.649,51.501,108.532,51.423,108.385,51.237,108.555,51.157,108.484,51.057,108.353,51.055,108.308,51.011,108.25,50.844,108.067,50.753,108.187,50.658,108.472,50.59,108.554,50.465,108.315,50.403,108.213,50.411,108.132,50.318,107.99,50.335,107.926,50.215,107.77,50.147,107.796,50.026,107.745,49.967]},{"z":4.7,"b":[88.736,51.553,90.477,52.192],"l":[88.736,51.559,89.033,51.6,89.233,51.645,89.223,51.56,89.421,51.605,89.581,51.553,89.73,51.582,89.943,51.75,90.105,51.768,90.028,51.984,90.119,52.044,90.124,52.129,90.359,52.192,90.477,52.183]},{"z":4.6,"b":[80.323,35.246,89.678,36.449],"l":[89.678,36.082,89.224,36.264,88.958,36.287,88.828,36.374,88.712,36.368,88.522,36.414,88.459,36.449,88,36.428,87.925,36.383,87.571,36.334,87.346,36.374,87.194,36.304,87.001,36.274,86.787,36.26,86.714,36.231,86.459,36.219,86.4,36.17,86.212,36.116,86.087,35.876,85.93,35.762,85.717,35.759,85.62,35.675,85.389,35.757,85.26,35.73,85.15,35.75,84.924,35.689,84.778,35.6,84.563,35.559,84.215,35.389,84.133,35.378,83.984,35.416,83.871,35.375,83.565,35.361,83.453,35.394,83.246,35.421,83.099,35.413,82.972,35.49,82.978,35.601,82.936,35.674,82.785,35.685,82.71,35.65,82.428,35.69,82.319,35.634,82.289,35.545,82.086,35.448,82.002,35.319,81.711,35.246,81.545,35.252,81.462,35.31,81.357,35.332,81.21,35.317,81.061,35.389,80.979,35.293,80.858,35.347,80.766,35.343,80.656,35.384,80.486,35.404,80.323,35.507]},{"z":4.6,"b":[89.435,31.548,97.716,36.082],"l":[89.678,36.082,89.515,36.053,89.435,35.988,89.485,35.88,89.581,35.84,89.789,35.827,89.789,35.776,89.715,35.642,89.69,35.509,89.73,35.444,89.484,35.342,89.455,35.213,89.572,35.073,89.584,34.941,89.788,34.926,89.825,34.846,89.724,34.74,89.791,34.554,89.796,34.408,89.84,34.383,89.787,34.199,89.637,34.093,89.742,33.91,89.877,33.823,89.975,33.631,90.141,33.578,90.203,33.5,90.206,33.402,90.323,33.289,90.436,33.282,90.66,33.156,90.804,33.141,90.95,33.239,91.128,33.254,91.189,33.337,91.362,33.337,91.376,33.271,91.543,33.08,91.72,32.983,91.813,32.968,91.977,32.849,92.086,32.886,92.196,32.869,92.221,32.745,92.466,32.769,92.65,32.74,92.727,32.761,92.85,32.729,93.006,32.735,93.031,32.664,93.219,32.66,93.345,32.578,93.443,32.563,93.51,32.516,93.597,32.565,93.734,32.572,93.877,32.495,94.111,32.478,94.178,32.522,94.317,32.541,94.582,32.672,94.722,32.593,94.755,32.535,94.852,32.5,94.939,32.428,94.951,32.337,95.062,32.261,95.259,32.244,95.382,32.17,95.425,32.095,95.354,31.954,95.407,31.811,95.497,31.742,95.593,31.762,95.725,31.746,95.774,31.698,95.849,31.714,96.094,31.701,96.167,31.548,96.215,31.607,96.213,31.735,96.148,31.78,96.183,31.871,96.278,31.909,96.483,31.753,96.664,31.721,96.737,31.68,96.795,31.715,96.753,31.839,96.786,31.91,96.754,31.974,96.915,31.99,97.018,32.023,97.124,32.009,97.255,32.076,97.254,32.204,97.36,32.26,97.393,32.385,97.326,32.424,97.361,32.5,97.562,32.484,97.631,32.444,97.716,32.544]},{"z":4.6,"b":[92.772,39.153,96.366,42.723],"l":[96.366,42.723,96.091,42.587,96.044,42.5,96.018,42.139,96.036,41.995,95.864,41.855,95.772,41.83,95.492,41.856,95.058,41.789,94.581,41.585,94.011,41.104,93.76,40.823,93.669,40.683,93.577,40.588,93.286,40.469,93.037,40.49,92.926,40.423,92.92,40.118,92.777,39.906,92.772,39.811,92.948,39.452,92.936,39.153]},{"z":4.6,"b":[89.678,36.01,92.936,39.153],"l":[92.936,39.153,92.333,39.05,92.123,38.941,91.881,38.875,91.629,38.826,91.492,38.817,91.294,38.745,91.012,38.699,90.654,38.674,90.636,38.624,90.478,38.533,90.312,38.466,90.151,38.433,90.193,38.326,90.439,37.996,90.41,37.847,90.436,37.779,90.834,37.608,90.957,37.519,91.074,37.488,91.152,37.323,91.235,37.195,91.316,37.119,91.288,37.014,91.191,37.001,91.055,36.945,90.891,36.939,90.808,36.911,90.712,36.822,90.688,36.698,90.71,36.636,90.805,36.559,91.007,36.503,91.061,36.314,91.136,36.141,91.111,36.074,90.915,36.013,90.829,36.01,90.623,36.111,90.292,36.116,90.166,36.129,90.014,36.253,89.972,36.105,89.867,36.066,89.678,36.082]},{"z":4.6,"b":[75.348,33.197,76.74,34.573],"l":[76.74,33.197,76.647,33.352,76.488,33.502,76.246,33.574,76.236,33.729,76.166,33.873,76.024,33.982,75.88,33.964,75.742,34.066,75.663,34.199,75.585,34.21,75.469,34.28,75.375,34.371,75.348,34.573]},{"z":4.6,"b":[76.74,32.504,78.381,33.197],"l":[78.381,32.528,78.27,32.504,78.304,32.596,78.375,32.649,78.367,32.765,78.254,32.698,78.115,32.668,78.009,32.61,77.939,32.695,77.929,32.762,77.858,32.873,77.721,33.013,77.539,32.918,77.349,32.867,77.136,32.927,76.956,33.025,76.74,33.197]},{"z":4.6,"b":[113.776,0.559,114.211,1.463],"l":[113.776,0.559,113.918,0.736,113.948,0.862,113.863,0.902,113.864,0.976,114.018,1.079,114.192,1.214,114.211,1.463]},{"z":4.6,"b":[115.607,-2.363,116.573,-1.426],"l":[116.573,-2.363,116.39,-2.314,116.043,-2.288,115.994,-2.271,115.874,-2.351,115.814,-2.184,115.829,-2.047,115.749,-2.001,115.741,-1.784,115.689,-1.636,115.607,-1.467,115.678,-1.426]},{"z":4.6,"b":[113.776,-1.426,115.805,0.735],"l":[115.678,-1.426,115.72,-1.426,115.802,-1.294,115.805,-1.085,115.74,-1.104,115.629,-0.978,115.518,-0.938,115.46,-0.886,115.415,-0.774,115.381,-0.582,115.33,-0.511,115.274,-0.311,115.277,-0.22,115.347,0.002,115.275,0.031,115.187,0.005,115.086,-0.118,115.003,-0.178,114.967,-0.098,115.018,0.058,114.953,0.284,114.968,0.364,115.076,0.533,115.057,0.637,114.976,0.735,114.735,0.673,114.663,0.621,114.378,0.589,114.184,0.65,114.036,0.647,113.97,0.604,113.776,0.559]},{"z":4.6,"b":[73.166,20.115,74.292,21.952],"l":[74.129,21.952,74.061,21.94,73.918,21.867,73.809,21.836,73.829,21.651,73.752,21.611,73.821,21.508,73.996,21.555,74.192,21.546,74.292,21.514,74.109,21.452,74.053,21.466,73.96,21.394,73.919,21.292,73.8,21.281,73.777,21.217,73.695,21.123,73.868,20.975,73.903,20.884,73.909,20.776,73.811,20.677,73.792,20.606,73.7,20.562,73.476,20.679,73.404,20.67,73.459,20.538,73.361,20.369,73.394,20.27,73.364,20.178,73.272,20.193,73.243,20.139,73.166,20.115]},{"z":4.6,"b":[72.708,20.073,72.947,20.127],"l":[72.947,20.127,72.708,20.073]},{"z":4.6,"b":[72.897,20.115,73.166,20.33],"l":[73.166,20.115,73.042,20.158,73.047,20.221,73.112,20.255,73.05,20.33,72.897,20.222,72.947,20.127]},{"z":4.6,"b":[72.818,20.379,72.875,20.468],"l":[72.818,20.379,72.875,20.404,72.847,20.468]},{"z":4.6,"b":[70.967,20.729,71.006,20.747],"l":[71.006,20.737,70.983,20.747,70.967,20.729]},{"z":4.6,"b":[71.057,23.036,74.323,24.708],"l":[71.057,24.693,71.251,24.637,71.378,24.647,71.546,24.688,71.652,24.658,71.751,24.678,71.948,24.663,72.051,24.708,72.099,24.657,72.262,24.608,72.275,24.568,72.501,24.496,72.556,24.507,72.668,24.461,72.747,24.381,72.897,24.361,72.959,24.39,73,24.493,73.095,24.504,73.137,24.336,73.079,24.204,73.246,24.016,73.354,24.105,73.424,23.941,73.364,23.889,73.362,23.784,73.511,23.684,73.621,23.583,73.619,23.452,73.711,23.415,73.812,23.435,73.904,23.333,73.96,23.359,74.106,23.248,74.109,23.191,74.233,23.156,74.323,23.036]},{"z":4.6,"b":[74.026,21.952,74.458,23.036],"l":[74.323,23.036,74.365,22.875,74.427,22.872,74.458,22.804,74.376,22.651,74.273,22.61,74.161,22.517,74.063,22.521,74.026,22.48,74.113,22.411,74.222,22.409,74.224,22.349,74.056,22.294,74.071,22.198,74.142,22.063,74.129,21.952]},{"z":4.7,"b":[121.174,46.22,126.056,53.317],"l":[121.413,53.317,121.607,53.241,121.8,53.024,121.692,52.908,121.503,52.775,121.174,52.595,121.491,52.445,121.692,52.389,121.831,52.269,122.018,52.283,122.174,52.48,122.339,52.454,122.492,52.295,122.731,52.249,122.775,52.2,122.638,52.095,122.754,51.847,122.714,51.702,122.836,51.568,122.839,51.511,122.956,51.386,123.088,51.312,123.272,51.267,123.438,51.273,123.57,51.204,123.789,51.233,123.866,51.313,124.004,51.302,124.162,51.328,124.351,51.279,124.502,51.361,124.666,51.335,124.821,51.366,125.061,51.534,125.128,51.616,125.333,51.583,125.487,51.507,125.577,51.389,125.719,51.254,125.843,51.206,125.871,51.145,125.984,51.104,126.056,50.98,126.001,50.902,125.824,50.767,125.809,50.562,125.531,50.393,125.418,50.195,125.264,50.106,125.295,50.014,125.188,49.942,125.235,49.848,125.22,49.668,125.131,49.637,125.236,49.554,125.267,49.438,125.223,49.22,125.158,49.151,124.864,49.175,124.822,49.061,124.622,48.742,124.616,48.668,124.538,48.572,124.521,48.369,124.562,48.236,124.52,48.129,124.426,48.174,124.327,48.332,124.301,48.492,124.234,48.531,124.078,48.448,123.727,48.191,123.578,48.054,123.293,47.946,123.207,47.814,123.095,47.755,122.797,47.644,122.565,47.531,122.495,47.41,122.405,47.332,122.574,47.131,122.797,47.034,122.996,46.84,123.187,46.762,123.33,46.833,123.38,46.898,123.489,46.817,123.609,46.775,123.536,46.692,123.397,46.598,123.292,46.586,123.094,46.622,123.02,46.612,122.989,46.407,123.162,46.22]},{"z":4.6,"b":[24.147,-31.79,25.522,-30.613],"l":[25.467,-30.613,25.483,-30.756,25.522,-30.802,25.507,-30.934,25.446,-31.095,25.35,-31.228,25.215,-31.202,24.968,-31.284,24.765,-31.389,24.559,-31.402,24.513,-31.553,24.5,-31.705,24.335,-31.719,24.154,-31.758,24.147,-31.79]},{"z":4.6,"b":[17.767,-32.937,24.147,-30.471],"l":[24.147,-31.79,23.963,-31.727,23.863,-31.8,23.809,-31.728,23.699,-31.667,23.401,-31.681,23.293,-31.751,23.181,-31.89,23.07,-31.977,23.027,-31.886,22.943,-31.856,22.756,-31.834,22.606,-31.777,22.477,-31.653,22.291,-31.57,22.144,-31.854,22.089,-31.893,22.101,-32.057,21.971,-32.196,21.853,-32.243,21.716,-32.212,21.553,-32.241,21.423,-32.349,21.305,-32.377,21.192,-32.459,21.118,-32.607,20.865,-32.677,20.823,-32.716,20.784,-32.862,20.705,-32.918,20.546,-32.902,20.421,-32.937,20.209,-32.764,20.123,-32.596,20.082,-32.412,20.213,-32.284,20.139,-32.19,19.925,-32.357,19.801,-32.373,19.704,-32.438,19.61,-32.458,19.534,-32.638,19.473,-32.583,19.488,-32.466,19.425,-32.31,19.493,-32.232,19.471,-32.078,19.319,-32.022,19.211,-31.914,19.029,-31.893,19.072,-31.748,19.067,-31.577,19.017,-31.495,18.997,-31.29,18.937,-31.197,18.905,-31.01,18.928,-30.942,18.939,-30.724,18.843,-30.695,18.735,-30.577,18.561,-30.471,18.464,-30.526,18.411,-30.5,18.336,-30.654,18.247,-30.783,18.157,-30.822,18.024,-30.775,17.892,-30.934,17.767,-31.156]},{"z":4.6,"b":[22.621,-28.1,25.027,-25.987],"l":[22.621,-26.112,22.705,-26.13,22.702,-26.41,22.815,-26.448,22.851,-26.398,22.915,-26.155,22.986,-26.161,23.057,-26.1,23.099,-25.987,23.374,-26.016,23.351,-26.08,23.229,-26.059,23.222,-26.12,23.303,-26.144,23.222,-26.268,23.284,-26.343,23.271,-26.479,23.458,-26.529,23.581,-26.615,23.494,-26.737,23.846,-26.887,23.927,-26.866,23.988,-26.912,24.024,-27.016,23.962,-27.1,23.894,-27.258,24.085,-27.359,24.036,-27.475,24.043,-27.585,24.116,-27.628,24.112,-27.743,24.408,-27.684,24.445,-27.783,24.363,-27.899,24.447,-27.977,24.438,-28.038,24.52,-28.1,24.611,-28,24.621,-27.904,24.691,-27.826,24.671,-27.672,24.796,-27.611,24.815,-27.687,24.972,-27.685,25.027,-27.708,24.842,-27.906,24.932,-28.097,25.013,-28.068]},{"z":4.6,"b":[24.341,-30.613,25.467,-28.068],"l":[25.013,-28.068,25.01,-28.115,24.916,-28.317,24.845,-28.519,24.874,-28.65,24.655,-29.069,24.341,-29.649,24.409,-29.764,24.658,-29.896,24.67,-29.95,24.784,-29.988,24.907,-30.201,25.029,-30.283,25.043,-30.33,25.185,-30.456,25.467,-30.613]},{"z":4.6,"b":[95.176,26.655,95.246,27.033],"l":[95.246,26.655,95.235,26.791,95.202,26.867,95.225,26.974,95.176,27.033]},{"z":4.6,"b":[94.959,27.033,96.007,27.993],"l":[95.176,27.033,95.261,27.049,95.424,27.141,95.493,27.25,95.546,27.274,95.644,27.23,95.8,27.286,95.904,27.276,96.007,27.365,95.978,27.45,95.88,27.463,95.887,27.557,95.804,27.619,95.776,27.717,95.901,27.865,95.964,27.993,95.717,27.99,95.544,27.902,95.408,27.871,95.313,27.896,94.959,27.773]},{"z":4.6,"b":[75.805,32.517,76.74,33.231],"l":[76.74,33.197,76.626,33.189,76.559,33.231,76.397,33.198,76.294,33.148,76.216,33.051,76.101,33.013,75.972,32.911,75.826,32.938,75.805,32.899,75.909,32.773,75.923,32.617,75.847,32.517]},{"z":4.6,"b":[75.298,32.301,75.847,32.517],"l":[75.847,32.517,75.743,32.464,75.683,32.4,75.612,32.395,75.502,32.301,75.39,32.335,75.298,32.332]},{"z":4.6,"b":[97.716,29.224,99.109,32.544],"l":[97.716,32.544,98.014,32.465,98.081,32.405,98.206,32.356,98.22,32.252,98.31,32.129,98.453,31.984,98.421,31.878,98.445,31.804,98.559,31.677,98.592,31.599,98.885,31.354,98.768,31.23,98.683,31.316,98.611,31.256,98.62,31.188,98.803,30.991,98.775,30.902,98.954,30.748,98.905,30.664,98.933,30.491,98.961,30.46,98.987,30.144,99.034,30.055,99.056,29.915,99.005,29.82,98.987,29.651,99.042,29.562,99.063,29.308,99.109,29.224]},{"z":4.6,"b":[98.099,28.14,99.109,29.224],"l":[99.109,29.224,98.99,29.202,99.006,29.073,98.932,28.986,98.909,28.912,98.946,28.842,98.873,28.812,98.82,28.886,98.785,29,98.665,28.974,98.639,28.879,98.678,28.757,98.589,28.67,98.64,28.424,98.576,28.318,98.49,28.248,98.41,28.251,98.3,28.355,98.23,28.203,98.099,28.14]},{"z":4.7,"b":[88.736,49.903,89.869,51.559],"l":[88.736,51.559,88.955,51.438,88.946,51.228,89.032,51.184,89.052,51.065,89.384,50.858,89.674,50.579,89.78,50.567,89.869,50.485,89.844,50.43,89.508,50.362,89.39,50.319,89.336,50.21,89.501,50.207,89.553,50.052,89.644,49.903]},{"z":4.7,"b":[27.67,58.44,29.975,59.014],"l":[27.67,58.982,27.873,59.014,28.131,58.99,28.272,58.856,28.552,58.865,28.757,58.827,28.946,58.82,28.975,58.78,29.232,58.706,29.449,58.595,29.539,58.48,29.786,58.44,29.975,58.445]},{"z":4.7,"b":[87.173,52.168,88.418,52.612],"l":[87.173,52.612,87.334,52.598,87.37,52.518,87.579,52.487,87.636,52.45,87.924,52.548,88.201,52.416,88.413,52.455,88.418,52.351,88.363,52.299,88.365,52.168]},{"z":4.7,"b":[87.839,51.316,88.736,52.168],"l":[88.365,52.168,88.121,52.098,87.839,51.811,88.086,51.714,87.967,51.598,87.862,51.549,88.106,51.432,88.132,51.386,88.364,51.32,88.508,51.316,88.665,51.424,88.659,51.535,88.736,51.559]},{"z":4.6,"b":[92.936,33.512,103.04,39.322],"l":[101.778,33.53,101.613,33.512,101.577,33.631,101.496,33.705,101.4,33.645,101.16,33.664,101.166,33.756,101.129,33.851,101.009,33.881,100.948,33.93,100.792,34.155,100.815,34.295,100.935,34.386,101.036,34.339,101.256,34.3,101.324,34.26,101.612,34.191,101.647,34.136,101.78,34.068,101.863,34.141,102.012,34.178,102.077,34.267,102.212,34.352,102.15,34.495,102.013,34.536,101.932,34.591,101.914,34.661,101.793,34.632,101.755,34.704,101.923,34.787,101.913,34.845,102.009,34.942,102.267,35.056,102.376,35.204,102.318,35.275,102.289,35.407,102.426,35.435,102.535,35.536,102.702,35.524,102.721,35.619,102.68,35.757,102.7,35.839,102.838,35.861,102.976,35.833,103,35.991,102.933,36.086,103.04,36.216,102.894,36.309,102.819,36.326,102.77,36.498,102.698,36.609,102.611,36.657,102.615,36.729,102.675,36.802,102.46,36.956,102.496,37.089,102.551,37.164,102.427,37.278,102.246,37.381,102.142,37.407,102.033,37.479,102.067,37.548,101.996,37.615,101.937,37.731,101.766,37.525,101.647,37.612,101.293,37.799,101.05,37.964,100.636,38.105,100.554,38.252,100.497,38.289,100.358,38.307,100.141,38.486,100.083,38.395,100.156,38.303,100.079,38.278,99.8,38.366,99.557,38.515,99.496,38.601,99.099,38.89,99.095,38.958,98.962,38.982,98.902,39.036,98.782,39.068,98.602,38.948,98.445,38.957,98.324,39.023,98.263,38.932,98.069,38.802,98.021,38.836,97.698,38.981,97.675,39.005,97.363,39.15,97.137,39.203,97.026,39.196,96.963,39.103,96.991,38.997,96.971,38.864,96.981,38.744,96.942,38.639,96.971,38.567,96.945,38.358,96.845,38.356,96.51,38.47,96.503,38.549,96.315,38.627,96.055,38.679,96.008,38.752,95.892,38.784,95.788,38.862,95.722,38.875,95.566,39.015,95.461,39.047,95.305,39.183,95.057,39.147,94.912,39.198,94.725,39.237,94.631,39.295,94.479,39.318,94.117,39.322,93.909,39.286,93.722,39.307,93.502,39.272,93.405,39.217,93.276,39.189,93.14,39.199,93.036,39.152,92.936,39.153]},{"z":4.7,"b":[84.014,50.629,87.173,52.668],"l":[84.067,50.629,84.183,50.661,84.542,50.691,84.6,50.858,84.528,50.881,84.471,50.973,84.252,51.002,84.018,51.064,84.014,51.138,84.172,51.144,84.27,51.275,84.394,51.266,84.705,51.395,84.937,51.43,85.008,51.489,85.239,51.471,85.313,51.569,85.428,51.56,85.537,51.7,85.67,51.754,85.74,51.861,85.877,51.919,85.834,51.971,85.94,52.075,86.15,52.07,86.265,52.012,86.44,52.023,86.505,52.12,86.754,52.109,86.814,52.145,86.806,52.292,86.618,52.372,86.841,52.648,86.952,52.635,87.117,52.668,87.173,52.612]},{"z":4.6,"b":[105.499,32.885,108.704,37.068],"l":[107.285,37.068,107.3,36.907,107.501,36.891,107.549,36.84,107.695,36.825,107.887,36.755,108.042,36.598,108.356,36.546,108.441,36.461,108.607,36.432,108.704,36.359,108.65,36.226,108.7,36.126,108.677,36.005,108.493,35.881,108.523,35.776,108.51,35.701,108.604,35.548,108.617,35.393,108.493,35.272,108.277,35.263,108.155,35.291,107.979,35.223,107.923,35.267,107.742,35.318,107.67,35.228,107.83,34.977,107.71,34.951,107.57,34.965,107.497,34.926,107.321,34.942,107.211,34.892,107.052,35.038,106.915,35.089,106.565,35.08,106.491,35.03,106.501,34.926,106.55,34.863,106.492,34.741,106.318,34.583,106.368,34.52,106.498,34.52,106.614,34.459,106.674,34.385,106.656,34.254,106.554,34.281,106.587,34.137,106.514,34.107,106.43,33.942,106.48,33.868,106.455,33.803,106.482,33.701,106.559,33.599,106.496,33.544,106.393,33.619,106.171,33.562,106.073,33.618,105.993,33.611,105.952,33.553,105.832,33.498,105.785,33.406,105.718,33.389,105.747,33.294,105.913,33.234,105.911,33.032,105.886,32.978,105.639,32.885,105.499,32.907]},{"z":4.6,"b":[101.778,32.608,105.499,34.296],"l":[105.499,32.907,105.431,32.911,105.386,32.823,105.435,32.774,105.27,32.642,105.147,32.608,105.032,32.639,104.898,32.612,104.841,32.64,104.645,32.658,104.558,32.688,104.406,32.809,104.289,32.848,104.321,32.953,104.378,32.959,104.406,33.063,104.328,33.14,104.281,33.272,104.293,33.364,104.175,33.49,104.156,33.624,104.093,33.669,103.915,33.683,103.745,33.676,103.659,33.711,103.523,33.714,103.559,33.807,103.351,33.756,103.272,33.765,103.184,33.822,103.193,33.884,103.142,33.962,103.152,34.109,103.12,34.169,103.001,34.214,102.931,34.296,102.782,34.274,102.598,34.165,102.618,34.084,102.441,34.06,102.372,33.976,102.319,33.987,102.171,33.942,102.343,33.725,102.356,33.609,102.482,33.54,102.482,33.465,102.279,33.378,102.131,33.285,102.099,33.222,102.004,33.219,101.864,33.123,101.832,33.269,101.923,33.406,101.884,33.546,101.778,33.53]},{"z":4.6,"b":[97.191,37.39,104.359,42.787],"l":[104.359,37.401,104.262,37.39,104.109,37.462,103.864,37.626,103.679,37.775,103.438,37.842,103.387,37.999,103.388,38.102,103.522,38.14,103.471,38.436,103.794,38.591,103.98,38.759,104.043,38.874,104.172,38.96,104.225,39.091,104.06,39.308,104.024,39.441,103.749,39.425,103.464,39.357,103.284,39.293,102.965,39.119,102.831,39.13,102.593,39.178,102.393,39.237,101.825,39.064,102.023,38.893,102.02,38.857,101.831,38.69,101.58,38.688,101.414,38.749,101.32,38.758,101.328,38.826,101.248,38.87,101.18,39.021,101.049,39.001,100.889,39.105,100.852,39.167,100.833,39.38,100.79,39.408,100.543,39.409,100.455,39.496,100.307,39.55,100.287,39.627,100.189,39.695,100.043,39.728,99.686,39.876,99.431,39.879,99.533,40.001,99.627,40.069,99.899,40.205,100.014,40.402,100.14,40.518,100.204,40.617,100.176,40.735,100.075,40.859,100.011,40.896,99.665,40.901,99.496,40.843,99.157,40.836,98.826,40.727,98.646,40.568,98.515,40.534,98.235,40.542,98.327,40.856,98.294,40.917,98.204,40.951,97.951,41.12,97.708,41.349,97.648,41.456,97.68,41.502,97.847,41.619,97.191,42.787]},{"z":4.6,"b":[104.359,35.261,107.285,37.44],"l":[104.359,37.401,104.465,37.44,104.69,37.412,104.722,37.339,104.621,37.249,104.709,37.215,104.765,37.25,104.855,37.218,104.922,37.097,105.18,36.972,105.322,36.781,105.22,36.693,105.269,36.55,105.393,36.384,105.446,36.255,105.437,36.107,105.351,36.058,105.318,35.933,105.432,35.756,105.687,35.66,105.829,35.494,105.936,35.525,106.065,35.488,106.112,35.425,106.211,35.395,106.313,35.274,106.378,35.261,106.465,35.332,106.495,35.553,106.429,35.699,106.47,35.727,106.767,35.707,106.913,35.789,106.843,35.882,106.915,35.907,106.945,36.077,106.814,36.212,106.745,36.207,106.604,36.278,106.497,36.268,106.495,36.437,106.44,36.515,106.494,36.56,106.502,36.707,106.602,36.726,106.649,36.834,106.575,36.931,106.634,36.998,106.644,37.182,106.791,37.19,106.933,37.107,107.188,37.117,107.285,37.068]},{"z":4.6,"b":[107.658,37.633,111.137,39.565],"l":[111.11,39.385,111.041,39.423,111.137,39.565,111.013,39.565,110.846,39.465,110.685,39.264,110.603,39.267,110.494,39.374,110.338,39.318,110.225,39.424,110.109,39.428,110.177,39.285,109.963,39.181,109.821,39.054,109.674,39.011,109.635,38.906,109.538,38.785,109.424,38.769,109.313,38.62,109.16,38.542,108.986,38.335,108.953,38.199,109.028,38.111,109.001,37.988,108.954,37.926,108.863,37.988,108.791,37.942,108.764,37.682,108.496,37.669,108.333,37.634,108.039,37.633,107.991,37.726,107.885,37.802,107.658,37.853]},{"z":4.6,"b":[104.359,37.401,107.658,39.361],"l":[107.658,37.853,107.405,37.922,107.324,38.066,107.163,38.139,107.061,38.115,106.829,38.15,106.47,38.291,106.645,38.459,106.704,38.637,106.932,38.921,106.978,39.036,106.912,39.069,106.808,39.199,106.789,39.361,106.614,39.358,106.497,39.286,106.347,39.294,106.276,39.269,106.283,39.157,106.113,39.121,106.06,38.979,105.897,38.736,105.841,38.571,105.828,38.227,105.756,38.136,105.825,38.005,105.788,37.805,105.659,37.74,105.534,37.725,105.354,37.753,105.085,37.661,104.944,37.546,104.647,37.507,104.522,37.53,104.425,37.499,104.359,37.401]},{"z":4.6,"b":[113.881,40.743,119.231,42.591],"l":[119.231,41.273,119.153,41.312,118.728,41.347,118.478,41.355,118.37,41.314,118.314,41.57,118.244,41.592,118.159,41.719,118.278,41.753,118.322,41.864,118.268,42.074,118.134,42.022,118.086,42.182,117.989,42.222,118.04,42.289,118.024,42.383,117.8,42.569,117.743,42.591,117.594,42.552,117.45,42.549,117.412,42.458,117.244,42.482,117.056,42.463,116.887,42.395,116.827,42.308,116.879,42.22,116.858,42.02,116.6,41.924,116.466,41.948,116.378,42.01,116.207,41.884,116.13,41.873,116.038,41.801,115.937,41.927,115.859,41.927,115.528,41.772,115.34,41.721,115.347,41.624,115.226,41.586,115.097,41.616,114.889,41.61,114.876,41.812,114.931,41.839,114.92,41.937,114.805,42.178,114.752,42.14,114.596,42.141,114.481,42.075,114.42,41.956,114.256,41.86,114.214,41.767,114.197,41.594,114.027,41.531,113.928,41.418,113.909,41.294,113.985,41.241,113.964,41.153,113.881,41.099,113.908,41.028,114.019,40.924,114.06,40.814,114.154,40.743]},{"z":4.6,"b":[111.11,39.385,114.154,40.743],"l":[114.154,40.743,114.027,40.635,114.076,40.55,114.044,40.488,113.808,40.434,113.677,40.429,113.518,40.344,113.334,40.319,113.23,40.417,113.086,40.404,112.961,40.356,112.844,40.226,112.734,40.173,112.596,40.238,112.399,40.288,112.279,40.229,112.165,40.059,112.099,40.005,111.921,39.689,111.761,39.598,111.675,39.636,111.49,39.652,111.425,39.625,111.421,39.522,111.343,39.444,111.199,39.427,111.11,39.385]},{"z":4.6,"b":[121.666,43.353,123.696,46.22],"l":[123.162,46.22,123.069,46.109,122.799,46.089,122.801,45.913,122.773,45.788,122.647,45.725,122.498,45.825,122.399,45.949,122.24,45.813,121.996,45.98,121.774,46.01,121.815,45.914,121.666,45.716,121.785,45.676,121.934,45.69,121.964,45.568,122.027,45.485,122.136,45.449,122.234,45.287,122.192,45.201,122.011,45.118,122.038,44.934,122.023,44.847,122.046,44.695,122.088,44.62,122.282,44.429,122.254,44.246,122.377,44.22,122.715,44.336,122.955,44.447,123.045,44.513,123.124,44.494,123.117,44.406,123.168,44.367,123.258,44.204,123.359,44.151,123.322,44.042,123.522,43.697,123.49,43.591,123.361,43.54,123.378,43.453,123.606,43.365,123.696,43.353]},{"z":4.6,"b":[119.231,41.273,123.696,43.353],"l":[123.696,43.353,123.612,43.081,123.354,43.012,123.182,42.944,123.131,42.826,122.893,42.745,122.728,42.741,122.476,42.85,122.367,42.799,122.426,42.722,122.4,42.658,122.299,42.636,122.045,42.721,121.918,42.647,121.878,42.54,121.724,42.458,121.646,42.454,121.544,42.535,121.369,42.5,121.034,42.26,120.913,42.297,120.735,42.228,120.628,42.16,120.509,42.149,120.469,42.038,120.34,41.966,120.141,41.789,120.108,41.707,120.026,41.734,120.03,41.861,119.96,41.972,119.892,41.999,119.808,42.12,119.821,42.209,119.629,42.251,119.563,42.372,119.336,42.289,119.245,42.192,119.366,42.1,119.322,41.965,119.331,41.868,119.299,41.777,119.316,41.658,119.39,41.473,119.362,41.375,119.231,41.273]},{"z":4.7,"b":[44.839,42.691,45.172,43.584],"l":[44.839,43.584,45.047,43.494,45.121,43.204,45.169,43.127,45.137,43.075,45.161,42.946,45.137,42.835,45.172,42.792,45.076,42.691]},{"z":4.7,"b":[44.839,43.661,45.509,44.015],"l":[44.839,43.661,45,43.705,45.08,43.788,45.083,43.953,45.19,43.952,45.237,43.88,45.432,43.879,45.459,44,45.509,44.015]},{"z":4.7,"b":[44.839,43.584,44.839,43.661],"l":[44.839,43.661,44.839,43.584]},{"z":4.7,"b":[44.499,42.74,44.937,43.62],"l":[44.839,43.584,44.768,43.62,44.616,43.61,44.522,43.564,44.499,43.392,44.572,43.392,44.575,43.24,44.713,43.218,44.818,43.163,44.915,43.071,44.937,42.92,44.852,42.836,44.628,42.883,44.639,42.74]},{"z":4.7,"b":[43.386,42.887,44.433,43.819],"l":[44.285,43.819,44.328,43.664,44.433,43.654,44.38,43.49,44.181,43.357,44.108,43.431,44.036,43.365,44.084,43.274,43.898,43.254,43.848,43.31,43.735,43.11,43.566,42.939,43.437,42.95,43.386,42.887]},{"z":4.7,"b":[44.285,43.661,44.839,43.83],"l":[44.285,43.819,44.399,43.795,44.694,43.83,44.803,43.802,44.839,43.661]},{"z":4.7,"b":[42.363,43.235,42.683,43.807],"l":[42.683,43.807,42.671,43.696,42.46,43.615,42.479,43.5,42.405,43.395,42.404,43.284,42.363,43.235]},{"z":4.7,"b":[42.683,43.807,44.285,43.994],"l":[42.683,43.807,42.852,43.817,42.975,43.891,43.111,43.896,43.175,43.948,43.266,43.91,43.3,43.824,43.502,43.867,43.729,43.839,43.803,43.941,43.906,43.994,44.259,43.925,44.285,43.819]},{"z":4.7,"b":[38.728,43.751,40.625,45.193],"l":[39.752,45.127,39.642,45.193,39.54,45.156,39.324,45.001,39.042,44.966,38.921,45.017,38.728,45.012,38.76,44.939,39.064,44.81,39.553,44.807,39.632,44.858,39.489,44.967,39.495,45.031,39.639,45.034,39.75,44.935,39.884,45.007,40.02,44.724,39.915,44.699,39.915,44.583,39.874,44.473,39.793,44.357,39.908,44.325,39.971,44.256,40.078,44.244,39.988,44.078,39.905,44.077,39.8,44.19,39.718,44.118,39.754,44.061,39.73,43.918,39.931,43.898,40.279,43.751,40.336,43.769,40.455,43.981,40.383,44.033,40.366,44.198,40.437,44.284,40.408,44.343,40.406,44.499,40.327,44.564,40.394,44.725,40.554,44.72,40.625,44.767,40.528,44.915,40.416,45,40.174,45.11,39.964,45.075,39.752,45.127]},{"z":4.7,"b":[41.729,43.807,42.683,44.495],"l":[41.729,44.459,41.905,44.495,41.945,44.403,42.038,44.364,42.23,44.344,42.346,44.306,42.499,44.299,42.555,44.245,42.303,44.129,42.306,44.058,42.378,43.971,42.497,44.03,42.66,43.917,42.683,43.807]},{"z":4.7,"b":[40.841,44.459,41.729,45.954],"l":[41.162,45.954,41.261,45.872,41.274,45.754,41.172,45.702,40.87,45.696,40.841,45.539,40.925,45.43,41.008,45.385,41.038,45.218,41.26,45.228,41.36,45.212,41.347,45.122,41.469,44.987,41.642,44.981,41.651,44.897,41.538,44.736,41.417,44.708,41.567,44.546,41.702,44.501,41.729,44.459]},{"z":4.7,"b":[40.69,43.523,41.729,44.459],"l":[41.729,44.459,41.602,44.367,41.704,44.308,41.711,44.243,41.633,44.122,41.538,44.085,41.449,43.994,41.282,44.012,41.256,44.081,40.985,44.116,40.951,43.989,40.782,43.927,40.724,43.81,40.723,43.631,40.69,43.523]},{"z":4.7,"b":[30.907,55.578,31.467,55.769],"l":[31.467,55.729,31.187,55.737,31.047,55.769,30.907,55.578]},{"z":4.7,"b":[38.666,45.954,41.162,46.88],"l":[38.666,46.88,38.69,46.821,38.883,46.808,38.849,46.713,38.884,46.635,39.099,46.601,39.18,46.688,39.444,46.809,39.577,46.778,39.817,46.785,40.023,46.765,40.132,46.722,40.045,46.616,40.059,46.542,40.214,46.525,40.284,46.403,40.234,46.358,40.339,46.28,40.735,46.276,40.757,46.233,40.954,46.198,41,46.127,41.126,46.068,41.162,45.954]},{"z":4.7,"b":[29.658,56.953,31,58.445],"l":[29.975,58.445,29.658,58.244,29.679,58.077,29.82,58.083,29.95,57.944,30.168,57.912,30.53,57.887,30.52,57.742,30.472,57.682,30.609,57.617,30.493,57.488,30.516,57.318,30.773,57.258,30.647,57.191,30.645,56.982,30.8,56.953,31,56.965]},{"z":4.7,"b":[30.817,55.729,31.498,56.965],"l":[31,56.965,30.896,56.897,30.89,56.808,30.817,56.72,30.913,56.656,31.08,56.623,31.017,56.519,31.215,56.435,31.231,56.346,31.492,56.301,31.474,56.119,31.498,56.029,31.345,55.964,31.413,55.89,31.467,55.729]},{"z":4.6,"b":[80.27,17.776,81.394,18.714],"l":[81.394,17.807,81.245,17.804,81.148,17.837,81.045,17.776,80.986,17.84,80.979,17.964,80.938,18.086,80.852,18.196,80.772,18.233,80.696,18.412,80.583,18.541,80.503,18.6,80.409,18.589,80.32,18.642,80.27,18.714]},{"z":3,"b":[-50.237,-9.846,-48.159,-5.364],"l":[-50.237,-9.846,-50.235,-9.843,-50.175,-9.73,-50.084,-9.422,-49.979,-9.22,-49.83,-9.022,-49.708,-8.895,-49.613,-8.839,-49.503,-8.709,-49.379,-8.498,-49.283,-8.301,-49.227,-8.14,-49.19,-7.995,-49.172,-7.867,-49.22,-7.752,-49.334,-7.649,-49.34,-7.51,-49.24,-7.335,-49.195,-7.169,-49.21,-7.006,-49.08,-6.856,-48.805,-6.718,-48.653,-6.596,-48.622,-6.489,-48.58,-6.414,-48.527,-6.372,-48.471,-6.353,-48.41,-6.358,-48.391,-6.309,-48.414,-6.207,-48.39,-6.142,-48.319,-6.112,-48.297,-6.067,-48.321,-6.007,-48.307,-5.966,-48.254,-5.945,-48.245,-5.889,-48.28,-5.796,-48.259,-5.724,-48.181,-5.672,-48.159,-5.619,-48.193,-5.565,-48.231,-5.533,-48.275,-5.522,-48.317,-5.486,-48.357,-5.424,-48.422,-5.398,-48.511,-5.408,-48.598,-5.399,-48.699,-5.364]},{"z":3,"b":[-48.723,-10.316,-45.945,-5.17],"l":[-48.699,-5.364,-48.723,-5.355,-48.631,-5.33,-48.574,-5.288,-48.524,-5.22,-48.458,-5.181,-48.378,-5.17,-48.282,-5.189,-48.169,-5.241,-48.059,-5.263,-47.953,-5.256,-47.885,-5.286,-47.854,-5.351,-47.763,-5.407,-47.612,-5.455,-47.52,-5.506,-47.486,-5.561,-47.445,-5.75,-47.397,-6.072,-47.406,-6.421,-47.47,-6.796,-47.537,-7.027,-47.606,-7.115,-47.665,-7.167,-47.716,-7.185,-47.717,-7.222,-47.669,-7.28,-47.612,-7.302,-47.546,-7.288,-47.507,-7.306,-47.495,-7.356,-47.498,-7.45,-47.458,-7.535,-47.413,-7.53,-47.347,-7.66,-47.303,-7.663,-47.28,-7.731,-47.15,-7.856,-47.079,-7.977,-47.016,-8.044,-46.985,-8.039,-46.869,-7.959,-46.744,-7.923,-46.574,-7.905,-46.489,-7.963,-46.467,-8.07,-46.503,-8.173,-46.507,-8.276,-46.543,-8.32,-46.503,-8.396,-46.659,-8.396,-46.793,-8.436,-46.838,-8.486,-46.882,-8.584,-46.922,-8.744,-46.905,-8.829,-47.065,-8.977,-47.083,-9.035,-47.038,-9.066,-46.94,-9.07,-46.847,-9.169,-46.829,-9.307,-46.753,-9.41,-46.561,-9.499,-46.534,-9.557,-46.601,-9.651,-46.65,-9.664,-46.668,-9.744,-46.507,-9.856,-46.463,-9.95,-46.472,-10.003,-46.347,-10.169,-46.284,-10.186,-46.19,-10.177,-46.088,-10.209,-46.034,-10.271,-45.945,-10.316]},{"z":3,"b":[-50.998,-20.102,-50.965,-19.513],"l":[-50.965,-19.513,-50.998,-19.664,-50.994,-20.102]},{"z":3,"b":[-54.243,-24.047,-53.119,-22.642],"l":[-53.119,-22.642,-53.272,-22.752,-53.481,-22.852,-53.62,-23.001,-53.677,-23.174,-53.771,-23.322,-53.908,-23.407,-53.996,-23.57,-54.05,-23.823,-54.125,-23.978,-54.243,-24.047]},{"z":3,"b":[-44.833,-22.405,-41.864,-20.776],"l":[-44.833,-22.405,-44.73,-22.361,-44.663,-22.369,-44.601,-22.316,-44.534,-22.303,-44.445,-22.253,-44.293,-22.24,-44.217,-22.249,-44.092,-22.173,-43.896,-22.106,-43.815,-22.066,-43.565,-22.066,-43.463,-22.057,-43.342,-22.003,-43.132,-22.026,-43.063,-22.063,-42.28,-21.713,-42.284,-21.642,-42.369,-21.633,-42.364,-21.593,-42.302,-21.486,-42.297,-21.405,-42.222,-21.338,-42.181,-21.164,-42.097,-21.003,-42.141,-20.963,-42.097,-20.923,-41.998,-20.932,-41.963,-20.91,-41.927,-20.829,-41.864,-20.776]},{"z":3,"b":[-38.216,-9.513,-37.975,-9.351],"l":[-38.216,-9.351,-37.976,-9.512,-37.975,-9.513]},{"z":3,"b":[-37.975,-10.492,-36.411,-9.513],"l":[-37.975,-9.513,-37.938,-9.538,-37.426,-9.778,-37.196,-9.886,-37.019,-9.957,-36.956,-10.014,-36.94,-10.084,-36.88,-10.154,-36.777,-10.224,-36.699,-10.257,-36.648,-10.254,-36.607,-10.293,-36.58,-10.371,-36.538,-10.411,-36.483,-10.411,-36.482,-10.411,-36.481,-10.411,-36.48,-10.412,-36.479,-10.412,-36.478,-10.413,-36.477,-10.414,-36.476,-10.414,-36.475,-10.415,-36.474,-10.415,-36.473,-10.416,-36.472,-10.416,-36.471,-10.417,-36.47,-10.417,-36.469,-10.418,-36.467,-10.419,-36.466,-10.419,-36.465,-10.42,-36.464,-10.421,-36.463,-10.421,-36.462,-10.422,-36.46,-10.423,-36.459,-10.423,-36.458,-10.424,-36.457,-10.424,-36.456,-10.425,-36.455,-10.426,-36.454,-10.426,-36.453,-10.427,-36.452,-10.427,-36.451,-10.428,-36.45,-10.428,-36.449,-10.429,-36.448,-10.429,-36.447,-10.43,-36.446,-10.43,-36.446,-10.431,-36.445,-10.431,-36.444,-10.431,-36.444,-10.432,-36.443,-10.432,-36.411,-10.492]},{"z":3,"b":[-66.828,-9.838,-61.61,-7.999],"l":[-66.828,-9.838,-66.744,-9.749,-66.695,-9.749,-66.597,-9.664,-66.503,-9.633,-66.405,-9.526,-66.396,-9.405,-66.105,-9.419,-65.998,-9.401,-65.931,-9.414,-65.766,-9.566,-65.677,-9.535,-65.632,-9.45,-65.525,-9.414,-65.445,-9.445,-65.418,-9.392,-65.222,-9.258,-65.199,-9.267,-65.172,-9.374,-65.092,-9.436,-65.039,-9.401,-64.905,-9.213,-64.927,-9.119,-64.887,-9.061,-64.78,-8.986,-64.69,-9.021,-64.597,-9.026,-64.476,-8.95,-64.418,-8.972,-64.382,-8.941,-64.217,-8.95,-64.119,-8.936,-64.128,-8.686,-64.074,-8.713,-63.998,-8.682,-63.922,-8.535,-63.994,-8.441,-63.918,-8.334,-63.753,-8.285,-63.726,-8.191,-63.592,-8.164,-63.588,-8.079,-63.539,-7.999,-62.905,-8.008,-62.829,-8.017,-62.722,-8.061,-62.677,-8.115,-62.637,-8.222,-62.556,-8.289,-62.539,-8.361,-62.454,-8.347,-62.373,-8.383,-62.306,-8.57,-62.172,-8.611,-62.132,-8.767,-61.998,-8.811,-61.918,-8.874,-61.873,-8.856,-61.838,-8.744,-61.762,-8.727,-61.717,-8.686,-61.61,-8.722,-61.61,-8.767]},{"z":3,"b":[-53.211,-18.017,-50.485,-12.843],"l":[-53.052,-18.017,-53.115,-17.855,-53.211,-17.409,-53.203,-17.304,-53.158,-17.209,-53.078,-17.095,-53.033,-16.995,-53.024,-16.91,-52.943,-16.814,-52.789,-16.705,-52.68,-16.576,-52.614,-16.428,-52.681,-16.361,-52.668,-16.289,-52.534,-16.227,-52.538,-16.169,-52.417,-16.082,-52.362,-16.058,-52.308,-16.009,-52.257,-15.936,-52.142,-15.873,-51.962,-15.819,-51.847,-15.747,-51.799,-15.656,-51.779,-15.593,-51.788,-15.557,-51.773,-15.523,-51.732,-15.49,-51.696,-15.403,-51.665,-15.261,-51.59,-15.142,-51.471,-15.044,-51.286,-15.003,-51.246,-15.004,-51.189,-14.977,-51.114,-14.838,-50.978,-14.463,-50.941,-14.201,-50.909,-14.107,-50.868,-14.095,-50.852,-13.995,-50.861,-13.806,-50.8,-13.612,-50.667,-13.41,-50.598,-13.249,-50.595,-13.131,-50.566,-13.015,-50.485,-12.843]},{"z":4.6,"b":[78.954,34.326,80.327,35.523],"l":[80.327,35.503,80.276,35.523,80.249,35.479,80.258,35.408,80.207,35.313,80.102,35.173,80.017,35.096,80.039,35.047,80.036,34.98,79.99,34.963,79.906,34.946,79.879,34.902,79.906,34.855,79.948,34.768,79.96,34.658,79.897,34.58,79.825,34.466,79.768,34.424,79.671,34.439,79.56,34.474,79.458,34.471,79.343,34.431,79.202,34.441,79.049,34.389,78.954,34.326]},{"z":4.6,"b":[92.073,26.916,94.959,27.773],"l":[92.073,26.916,92.204,26.916,92.468,26.966,92.621,27.038,92.717,27.052,92.91,27.031,92.966,26.974,93.094,26.959,93.415,26.995,93.503,26.974,93.684,26.982,93.84,27.109,93.897,27.243,94.127,27.453,94.208,27.538,94.233,27.629,94.289,27.622,94.414,27.583,94.667,27.652,94.959,27.773]},{"z":4.6,"b":[116.744,39.686,117.239,40.093],"l":[116.876,39.686,116.922,39.774,116.781,39.89,116.744,39.959,116.829,40.037,116.988,40.034,117.239,40.093]},{"z":4.6,"b":[116.681,38.559,117.558,39.602],"l":[116.793,39.602,116.791,39.493,116.853,39.341,116.826,39.217,116.892,39.103,116.712,39.012,116.681,38.916,116.711,38.821,116.881,38.699,117.021,38.708,117.095,38.612,117.189,38.606,117.271,38.559,117.558,38.625]},{"z":4.6,"b":[116.876,39.618,117.239,40.093],"l":[117.239,40.093,117.148,39.948,117.203,39.762,117.153,39.618,116.876,39.686]}]} \ No newline at end of file diff --git a/src/maps/map_data.js b/src/maps/map_data.js new file mode 100644 index 0000000..d646834 --- /dev/null +++ b/src/maps/map_data.js @@ -0,0 +1,80 @@ +const fs = require("fs"); +const path = require("path"); + +const DATA_DIR = path.join(__dirname, "data"); +const loaded = new Map(); + +const load = (name) => { + if (loaded.has(name)) return loaded.get(name); + let data = { f: [] }; + try { data = JSON.parse(fs.readFileSync(path.join(DATA_DIR, `${name}.json`), "utf8")); } catch (_) {} + loaded.set(name, data); + return data; +}; + +const countries = (detail) => load(detail === "high" ? "countries_50m" : "countries_110m").f; +const coast = () => load("coast_50m").f; +const lakes = () => load("lakes_50m").f; +const rivers = () => load("rivers_50m").f; +const states = () => load("states_50m").f; +const places = (detail) => load(detail === "high" ? "places_50m" : "places_110m").f; + +const fold = (s) => String(s || "").normalize("NFD").replace(/[̀-ͯ]/g, "").toLowerCase().trim(); + +let placeIndex = null; +const buildPlaceIndex = () => { + if (placeIndex) return placeIndex; + const list = []; + for (const p of places("high")) { + list.push({ kind: "place", name: p.n, country: p.a, iso: p.i, pop: p.p || 0, capital: !!p.k, lat: p.la, lng: p.lo, key: fold(p.n) }); + } + for (const c of countries("low")) { + list.push({ kind: "country", name: c.n, country: "", iso: c.c, pop: Number.MAX_SAFE_INTEGER, capital: false, lat: c.ly, lng: c.lx, bbox: c.b, key: fold(c.n) }); + } + for (const c of countries("high")) { + if (list.some((e) => e.kind === "country" && e.key === fold(c.n))) continue; + list.push({ kind: "country", name: c.n, country: "", iso: c.c, pop: Number.MAX_SAFE_INTEGER, capital: false, lat: c.ly, lng: c.lx, bbox: c.b, key: fold(c.n) }); + } + placeIndex = list; + return list; +}; + +const searchPlaces = (query, limit = 5) => { + const q = fold(query); + if (!q) return []; + const index = buildPlaceIndex(); + const scored = []; + for (const e of index) { + let score = 0; + if (e.key === q) score = 3; + else if (e.key.startsWith(q)) score = 2; + else if (e.key.includes(q)) score = 1; + else if (e.iso && fold(e.iso) === q) score = 2; + if (!score) continue; + scored.push({ e, score }); + } + scored.sort((a, b) => b.score - a.score || b.e.pop - a.e.pop || a.e.key.localeCompare(b.e.key)); + return scored.slice(0, limit).map(({ e }) => ({ ...e, key: undefined })); +}; + +const toRad = (d) => d * Math.PI / 180; +const distanceKm = (lat1, lng1, lat2, lng2) => { + const dLat = toRad(lat2 - lat1); + const dLng = toRad(lng2 - lng1); + const a = Math.sin(dLat / 2) ** 2 + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLng / 2) ** 2; + return 6371 * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); +}; + +const nearestPlace = (lat, lng, maxKm = 300) => { + if (typeof lat !== "number" || typeof lng !== "number" || !isFinite(lat) || !isFinite(lng)) return null; + let best = null; + for (const p of places("high")) { + if (Math.abs(p.la - lat) > 4) continue; + const d = distanceKm(lat, lng, p.la, p.lo); + if (d > maxKm) continue; + if (!best || d < best.km) best = { name: p.n, country: p.a, iso: p.i, km: Math.round(d), lat: p.la, lng: p.lo }; + } + return best; +}; + +module.exports = { load, countries, coast, lakes, rivers, states, places, searchPlaces, nearestPlace, distanceKm, fold }; diff --git a/src/maps/map_renderer.js b/src/maps/map_renderer.js index 0a63299..427799a 100644 --- a/src/maps/map_renderer.js +++ b/src/maps/map_renderer.js @@ -1,223 +1,608 @@ -const { execFileSync } = require("child_process"); -const path = require("path"); -const fs = require("fs"); -const crypto = require("crypto"); +const data = require("./map_data"); -const BASE_MAP = path.join(__dirname, "..", "client", "assets", "images", "worldmap-z2.png"); -const CACHE_DIR = path.join(__dirname, "cache"); -const TILES_DIR = path.join(__dirname, "tiles"); const MAP_W = 1024; -const MAP_H = 1024; +const MAP_H = 640; +const TILE = 256; +const MIN_ZOOM = 2; +const MAX_ZOOM = 9; +const TILE_MAX_ZOOM = 6; +const TILE_PATH = "/maptiles"; +const TILE_ENV = process.env.OASIS_MAP_TILES || ""; +const CRISP_ZOOM = 9; +const MAX_LAT = 85.0511; +const GRID_COLS = 24; +const GRID_ROWS = 15; +const CLUSTER_PX = 28; +const EDGE = 6; +const POPUP_W = 320; +const POPUP_H = 400; +const POPUP_GAP = 10; -const latLngToPx = (lat, lng) => { - const latRad = lat * Math.PI / 180; - const x = Math.round((lng + 180) / 360 * MAP_W); - const y = Math.round((1 - Math.log(Math.tan(latRad) + 1 / Math.cos(latRad)) / Math.PI) / 2 * MAP_H); - return { x: Math.max(12, Math.min(MAP_W - 12, x)), y: Math.max(12, Math.min(MAP_H - 12, y)) }; -}; +const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v)); +const num = (v) => { const n = typeof v === "number" ? v : parseFloat(v); return isFinite(n) ? n : null; }; +const r1 = (v) => Math.round(v * 10) / 10; +const r4 = (v) => Math.round(v * 10000) / 10000; +const esc = (s) => String(s == null ? "" : s).replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); -const pxToLatLng = (px, py) => { - const lng = px / MAP_W * 360 - 180; - const n = Math.PI - 2 * Math.PI * py / MAP_H; - const lat = 180 / Math.PI * Math.atan(0.5 * (Math.exp(n) - Math.exp(-n))); - return { lat: Math.round(lat * 100) / 100, lng: Math.round(lng * 100) / 100 }; -}; - -const getMaxTileZoom = () => { - try { - const dirs = fs.readdirSync(TILES_DIR).filter(d => /^\d+$/.test(d) && fs.existsSync(path.join(TILES_DIR, d, '0_0.png'))); - return dirs.length ? Math.max(...dirs.map(Number)) : 0; - } catch (_) { return 0; } -}; - -const getViewportBounds = (centerLat, centerLng, zoom) => { - const effectiveZ = Math.min(zoom, getMaxTileZoom()); - const n = Math.pow(2, effectiveZ); - const tileSize = 256; - const worldPx = n * tileSize; - const scale = Math.pow(2, zoom - effectiveZ); - const vw = MAP_W / scale; - const vh = MAP_H / scale; - - const latRad = centerLat * Math.PI / 180; - const cx = (centerLng + 180) / 360 * worldPx; - const cy = (1 - Math.log(Math.tan(latRad) + 1 / Math.cos(latRad)) / Math.PI) / 2 * worldPx; - - const x0 = cx - vw / 2; - const y0 = cy - vh / 2; - const x1 = cx + vw / 2; - const y1 = cy + vh / 2; - - const pxToLng = (px) => px / worldPx * 360 - 180; - const pxToLat = (py) => { const nn = Math.PI - 2 * Math.PI * py / worldPx; return 180 / Math.PI * Math.atan(0.5 * (Math.exp(nn) - Math.exp(-nn))); }; +const worldPx = (zoom) => TILE * Math.pow(2, zoom); +const project = (lat, lng, zoom) => { + const w = worldPx(zoom); + const la = clamp(lat, -MAX_LAT, MAX_LAT) * Math.PI / 180; return { - latMin: Math.max(-85, pxToLat(Math.min(y1, worldPx - 1))), - latMax: Math.min(85, pxToLat(Math.max(y0, 0))), - lngMin: Math.max(-180, pxToLng(Math.max(x0, 0))), - lngMax: Math.min(180, pxToLng(Math.min(x1, worldPx - 1))) + x: (lng + 180) / 360 * w, + y: (1 - Math.log(Math.tan(Math.PI / 4 + la / 2)) / Math.PI) / 2 * w }; }; -const renderMapWithPins = (markers, mainIdx) => { - const pins = (Array.isArray(markers) ? markers : []) - .filter((m) => m && typeof m.lat === "number" && typeof m.lng === "number") - .map((m, i) => ({ ...latLngToPx(m.lat, m.lng), main: i === (mainIdx || 0) })); +const unproject = (x, y, zoom) => { + const w = worldPx(zoom); + const n = Math.PI - 2 * Math.PI * y / w; + return { lat: 180 / Math.PI * Math.atan(0.5 * (Math.exp(n) - Math.exp(-n))), lng: x / w * 360 - 180 }; +}; - const hash = crypto.createHash("md5") - .update(pins.map((p) => `${p.x},${p.y},${p.main}`).join(";")) - .digest("hex") - .slice(0, 12); +const latLngToPx = (lat, lng) => { + const p = project(lat, lng, MIN_ZOOM); + return { x: Math.round(p.x), y: Math.round(p.y) }; +}; - const outFile = path.join(CACHE_DIR, `map_${hash}.png`); +const pxToLatLng = (px, py) => { + const p = unproject(px, py, MIN_ZOOM); + return { lat: Math.round(p.lat * 100) / 100, lng: Math.round(p.lng * 100) / 100 }; +}; - if (fs.existsSync(outFile)) return `map_${hash}.png`; +const normalizeZoom = (z) => clamp(Math.round(num(z) == null ? MIN_ZOOM : num(z)), MIN_ZOOM, MAX_ZOOM); - const script = ` -from PIL import Image, ImageDraw -import sys, json +const makeView = (centerLat, centerLng, zoom) => { + const z = normalizeZoom(zoom); + const w = worldPx(z); + const c = project(num(centerLat) == null ? 0 : num(centerLat), num(centerLng) == null ? 0 : num(centerLng), z); + const cx = w <= MAP_W ? w / 2 : clamp(c.x, MAP_W / 2, w - MAP_W / 2); + const cy = w <= MAP_H ? w / 2 : clamp(c.y, MAP_H / 2, w - MAP_H / 2); + const center = unproject(cx, cy, z); + return { zoom: z, lat: r4(center.lat), lng: r4(center.lng), x0: cx - MAP_W / 2, y0: cy - MAP_H / 2, world: w }; +}; -pins = json.loads(sys.argv[1]) -im = Image.open(sys.argv[2]).copy() -draw = ImageDraw.Draw(im) +const toScreen = (view, lat, lng) => { + const p = project(lat, lng, view.zoom); + return { x: p.x - view.x0, y: p.y - view.y0 }; +}; -for p in pins: - x, y, main = p['x'], p['y'], p.get('main', False) - sw = 3 if main else 2 - sh = 18 if main else 13 - clr = '#e74c3c' if main else '#3498db' - dark = '#c0392b' if main else '#2980b9' - draw.polygon([(x, y + 2), (x - sw, y - sh + sw * 2), (x + sw, y - sh + sw * 2)], fill=clr) - draw.ellipse([x - sw - 1, y - sh - sw, x + sw + 1, y - sh + sw], fill=dark, outline='white', width=1) +const fromScreen = (view, x, y) => { + const p = unproject(view.x0 + x, view.y0 + y, view.zoom); + return { lat: r4(clamp(p.lat, -MAX_LAT, MAX_LAT)), lng: r4(clamp(p.lng, -180, 180)) }; +}; -im.save(sys.argv[3], optimize=True) -`; +const getViewportBounds = (centerLat, centerLng, zoom) => { + const view = makeView(centerLat, centerLng, zoom); + const nw = fromScreen(view, 0, 0); + const se = fromScreen(view, MAP_W, MAP_H); + return { latMin: se.lat, latMax: nw.lat, lngMin: nw.lng, lngMax: se.lng }; +}; - try { - fs.mkdirSync(CACHE_DIR, { recursive: true }); - execFileSync("python3", [ - "-c", script, - JSON.stringify(pins), - BASE_MAP, - outFile - ], { timeout: 10000 }); - } catch (e) { - return null; +const fitView = (points, opts = {}) => { + const pts = (Array.isArray(points) ? points : []).filter((p) => p && num(p.lat) != null && num(p.lng) != null); + const single = normalizeZoom(opts.singleZoom || 8); + if (!pts.length) return { zoom: MIN_ZOOM, lat: 10, lng: 0 }; + if (pts.length === 1) return { zoom: single, lat: num(pts[0].lat), lng: num(pts[0].lng) }; + let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity; + for (const p of pts) { + const q = project(num(p.lat), num(p.lng), 0); + x0 = Math.min(x0, q.x); x1 = Math.max(x1, q.x); y0 = Math.min(y0, q.y); y1 = Math.max(y1, q.y); } + const pad = opts.pad == null ? 70 : opts.pad; + const dx = Math.max(x1 - x0, 1e-6); + const dy = Math.max(y1 - y0, 1e-6); + const z = Math.floor(Math.log2(Math.min((MAP_W - 2 * pad) / dx, (MAP_H - 2 * pad) / dy))); + const c = unproject((x0 + x1) / 2, (y0 + y1) / 2, 0); + return { zoom: clamp(z, MIN_ZOOM, Math.min(MAX_ZOOM, opts.maxZoom || MAX_ZOOM)), lat: r4(c.lat), lng: r4(c.lng) }; +}; - return `map_${hash}.png`; +const parseView = (raw) => { + const s = Array.isArray(raw) ? raw[raw.length - 1] : raw; + const m = /^\s*(\d{1,2})\/(-?\d+(?:\.\d+)?)\/(-?\d+(?:\.\d+)?)\s*$/.exec(String(s || "")); + if (!m) return null; + return { zoom: normalizeZoom(m[1]), lat: clamp(parseFloat(m[2]), -MAX_LAT, MAX_LAT), lng: clamp(parseFloat(m[3]), -180, 180) }; +}; + +const viewParam = (v) => `${normalizeZoom(v.zoom)}/${r4(v.lat)}/${r4(v.lng)}`; + +const parsePick = (raw) => { + const s = Array.isArray(raw) ? raw[raw.length - 1] : raw; + const m = /^\s*(-?\d+(?:\.\d+)?),(-?\d+(?:\.\d+)?)\s*$/.exec(String(s || "")); + if (!m) return null; + const lat = parseFloat(m[1]), lng = parseFloat(m[2]); + if (Math.abs(lat) > 90 || Math.abs(lng) > 180) return null; + return { lat: r4(lat), lng: r4(lng) }; +}; + +const resolveView = (params = {}, pins = [], opts = {}) => { + const explicit = parseView(params.view); + if (explicit) return makeView(explicit.lat, explicit.lng, explicit.zoom); + const clat = num(Array.isArray(params.clat) ? params.clat[0] : params.clat); + const clng = num(Array.isArray(params.clng) ? params.clng[0] : params.clng); + const zoomParam = num(Array.isArray(params.zoom) ? params.zoom[0] : params.zoom); + if (clat != null && clng != null) return makeView(clat, clng, zoomParam == null ? (opts.singleZoom || 8) : zoomParam); + const fit = fitView(pins, opts); + if (zoomParam != null && zoomParam >= MIN_ZOOM) return makeView(opts.focusLat != null ? opts.focusLat : fit.lat, opts.focusLng != null ? opts.focusLng : fit.lng, zoomParam); + return makeView(fit.lat, fit.lng, fit.zoom); +}; + +const metersPerPx = (view) => 156543.03392 * Math.cos(view.lat * Math.PI / 180) / Math.pow(2, view.zoom); + +const bboxHit = (b, vb) => b[0] <= vb[2] && b[2] >= vb[0] && b[1] <= vb[3] && b[3] >= vb[1]; + +const clipPolygon = (pts, x0, y0, x1, y1) => { + let out = pts; + const passes = [ + (p) => p.x >= x0, (p, q) => ({ x: x0, y: p.y + (q.y - p.y) * (x0 - p.x) / (q.x - p.x) }), + (p) => p.x <= x1, (p, q) => ({ x: x1, y: p.y + (q.y - p.y) * (x1 - p.x) / (q.x - p.x) }), + (p) => p.y >= y0, (p, q) => ({ x: p.x + (q.x - p.x) * (y0 - p.y) / (q.y - p.y), y: y0 }), + (p) => p.y <= y1, (p, q) => ({ x: p.x + (q.x - p.x) * (y1 - p.y) / (q.y - p.y), y: y1 }) + ]; + for (let i = 0; i < passes.length && out.length; i += 2) { + const inside = passes[i], cross = passes[i + 1]; + const res = []; + let prev = out[out.length - 1]; + for (const cur of out) { + const ci = inside(cur), pi = inside(prev); + if (ci) { + if (!pi) res.push(cross(prev, cur)); + res.push(cur); + } else if (pi) { + res.push(cross(prev, cur)); + } + prev = cur; + } + out = res; + } + return out; +}; + +const clipSegment = (a, b, x0, y0, x1, y1) => { + let t0 = 0, t1 = 1; + const dx = b.x - a.x, dy = b.y - a.y; + const checks = [[-dx, a.x - x0], [dx, x1 - a.x], [-dy, a.y - y0], [dy, y1 - a.y]]; + for (const [p, q] of checks) { + if (p === 0) { if (q < 0) return null; continue; } + const r = q / p; + if (p < 0) { if (r > t1) return null; if (r > t0) t0 = r; } + else { if (r < t0) return null; if (r < t1) t1 = r; } + } + return [{ x: a.x + t0 * dx, y: a.y + t0 * dy }, { x: a.x + t1 * dx, y: a.y + t1 * dy }]; +}; + +const simplify = (pts, tol) => { + if (pts.length <= 2) return pts; + const keep = new Uint8Array(pts.length); + keep[0] = 1; keep[pts.length - 1] = 1; + const stack = [[0, pts.length - 1]]; + const tol2 = tol * tol; + while (stack.length) { + const [a, b] = stack.pop(); + if (b - a < 2) continue; + const ax = pts[a].x, ay = pts[a].y, bx = pts[b].x, by = pts[b].y; + const dx = bx - ax, dy = by - ay; + const len2 = dx * dx + dy * dy; + let best = -1, bestD = 0; + for (let i = a + 1; i < b; i++) { + let d; + if (len2 === 0) { d = (pts[i].x - ax) ** 2 + (pts[i].y - ay) ** 2; } + else { + const t = clamp(((pts[i].x - ax) * dx + (pts[i].y - ay) * dy) / len2, 0, 1); + d = (pts[i].x - ax - t * dx) ** 2 + (pts[i].y - ay - t * dy) ** 2; + } + if (d > bestD) { bestD = d; best = i; } + } + if (best >= 0 && bestD > tol2) { keep[best] = 1; stack.push([a, best], [best, b]); } + } + const out = []; + for (let i = 0; i < pts.length; i++) if (keep[i]) out.push(pts[i]); + return out; +}; + +const projectFlat = (flat, view, tol) => { + const out = []; + let lx = NaN, ly = NaN; + for (let i = 0; i < flat.length; i += 2) { + const p = toScreen(view, flat[i + 1], flat[i]); + if (Math.abs(p.x - lx) + Math.abs(p.y - ly) < tol) continue; + out.push(p); + lx = p.x; ly = p.y; + } + return simplify(out, tol); +}; + +const pathOf = (pts, close) => { + if (pts.length < 2) return ""; + let d = `M${r1(pts[0].x)} ${r1(pts[0].y)}`; + for (let i = 1; i < pts.length; i++) d += `L${r1(pts[i].x)} ${r1(pts[i].y)}`; + return close ? d + "Z" : d; +}; + +const polygonPath = (rings, view, tol) => { + let d = ""; + for (const ring of rings) { + const pts = projectFlat(ring, view, tol); + if (pts.length < 3) continue; + const clipped = clipPolygon(pts, -EDGE, -EDGE, MAP_W + EDGE, MAP_H + EDGE); + if (clipped.length >= 3) d += pathOf(clipped, true); + } + return d; +}; + +const linePath = (flat, view, tol) => { + const pts = projectFlat(flat, view, tol); + let d = ""; + let run = []; + const flush = () => { if (run.length >= 2) d += pathOf(run, false); run = []; }; + for (let i = 1; i < pts.length; i++) { + const seg = clipSegment(pts[i - 1], pts[i], -EDGE, -EDGE, MAP_W + EDGE, MAP_H + EDGE); + if (!seg) { flush(); continue; } + if (!run.length || Math.abs(run[run.length - 1].x - seg[0].x) + Math.abs(run[run.length - 1].y - seg[0].y) > 0.05) { flush(); run.push(seg[0]); } + run.push(seg[1]); + } + flush(); + return d; +}; + +const viewBoxLatLng = (view) => { + const nw = fromScreen(view, -EDGE, -EDGE); + const se = fromScreen(view, MAP_W + EDGE, MAP_H + EDGE); + return [nw.lng, se.lat, se.lng, nw.lat]; +}; + +const baseCache = new Map(); +const cacheGet = (key) => baseCache.get(key); +const cacheSet = (key, val) => { + if (baseCache.size >= 64) baseCache.delete(baseCache.keys().next().value); + baseCache.set(key, val); +}; + +const tileZoom = (zoom, thumb) => clamp(Math.floor(zoom) - (thumb ? 2 : 0), 0, TILE_MAX_ZOOM); + +const tileCoverage = (view, opts = {}) => { + const tz = tileZoom(view.zoom, !!opts.thumb); + const size = TILE * Math.pow(2, view.zoom - tz); + const n = Math.pow(2, tz); + const tx0 = Math.max(0, Math.floor(view.x0 / size)), tx1 = Math.min(n - 1, Math.ceil((view.x0 + MAP_W) / size) - 1); + const ty0 = Math.max(0, Math.floor(view.y0 / size)), ty1 = Math.min(n - 1, Math.ceil((view.y0 + MAP_H) / size) - 1); + const out = []; + for (let ty = ty0; ty <= ty1; ty++) { + for (let tx = tx0; tx <= tx1; tx++) out.push({ z: tz, x: tx, y: ty, sx: tx * size - view.x0, sy: ty * size - view.y0, size }); + } + return out; +}; + +const tileHref = (base, t) => `${base}/${t.z}/${t.x}_${t.y}.jpg`; + +const tileBaseOf = (opts) => opts.tileBase || TILE_ENV || `${opts.publicBase || ""}${TILE_PATH}`; + +const renderRaster = (view, opts) => { + const tiles = tileCoverage(view, { thumb: !!opts.thumb }); + if (!tiles.length) return ""; + const base = tileBaseOf(opts); + const clipId = `${opts.id || "map"}-clip`; + let out = ``; + for (const t of tiles) { + out += ``; + } + return out + ""; +}; + +const graticuleStep = (zoom) => zoom <= 2 ? 30 : zoom === 3 ? 15 : zoom === 4 ? 10 : 0; + +const renderGraticule = (view, vb) => { + const step = graticuleStep(view.zoom); + if (!step) return ""; + let d = ""; + for (let lng = Math.ceil(vb[0] / step) * step; lng <= vb[2]; lng += step) { + const x = r1(toScreen(view, 0, lng).x); + d += `M${x} 0L${x} ${MAP_H}`; + } + for (let lat = Math.ceil(vb[1] / step) * step; lat <= vb[3]; lat += step) { + if (Math.abs(lat) >= 85) continue; + const y = r1(toScreen(view, lat, 0).y); + d += `M0 ${y}L${MAP_W} ${y}`; + } + return d ? `` : ""; +}; + +const renderBase = (view, detail) => { + const key = `${view.zoom}|${Math.round(view.x0)}|${Math.round(view.y0)}|${detail}`; + const hit = cacheGet(key); + if (hit) return hit; + const z = view.zoom; + const tol = detail === "low" ? 4 : z <= 3 ? 1.1 : 0.7; + const vb = viewBoxLatLng(view); + const high = z >= 5; + let out = detail === "low" ? "" : renderGraticule(view, vb); + let borders = ""; + for (const c of data.countries(high ? "high" : "low")) { + if (!bboxHit(c.b, vb)) continue; + for (const poly of c.p) { + const d = polygonPath(poly, view, tol); + if (d) borders += ``; + } + } + out += `${borders}`; + if (high && detail !== "low") { + let st = ""; + for (const s of data.states()) { + if (s.z > z || !bboxHit(s.b, vb)) continue; + const d = linePath(s.l, view, tol); + if (d) st += ``; + } + if (st) out += `${st}`; + } + if (z >= CRISP_ZOOM && detail !== "low") { + let co = ""; + for (const c of data.coast()) { + if (!bboxHit(c.b, vb)) continue; + const d = linePath(c.l, view, tol); + if (d) co += ``; + } + for (const l of data.lakes()) { + if (l.z > z || !bboxHit(l.b, vb)) continue; + for (const poly of l.p) { + const d = polygonPath(poly, view, tol); + if (d) co += ``; + } + } + if (co) out += `${co}`; + } + cacheSet(key, out); + return out; +}; + +const textWidth = (s, size) => String(s || "").length * size * 0.58; + +const collides = (boxes, b) => boxes.some((o) => b.x0 < o.x1 && b.x1 > o.x0 && b.y0 < o.y1 && b.y1 > o.y0); + +const renderLabels = (view, boxes) => { + const z = view.zoom; + let out = ""; + if (z >= 3 && z <= 7) { + const list = data.countries(z >= 5 ? "high" : "low").filter((c) => c.ml <= z && c.n).sort((a, b) => a.r - b.r); + for (const c of list) { + const p = toScreen(view, c.ly, c.lx); + if (p.x < 20 || p.x > MAP_W - 20 || p.y < 10 || p.y > MAP_H - 10) continue; + const w = textWidth(c.n, 14); + const b = { x0: p.x - w / 2 - 4, x1: p.x + w / 2 + 4, y0: p.y - 10, y1: p.y + 6 }; + if (collides(boxes, b)) continue; + boxes.push(b); + out += `${esc(c.n)}`; + } + } + if (z >= 2) { + const list = data.places(z >= 5 ? "high" : "low").filter((p) => p.z <= z).sort((a, b) => (b.k - a.k) || (b.p - a.p)); + for (const pl of list) { + const p = toScreen(view, pl.la, pl.lo); + if (p.x < 4 || p.x > MAP_W - 4 || p.y < 4 || p.y > MAP_H - 4) continue; + const w = textWidth(pl.n, 12); + const dot = { x0: p.x - 5, x1: p.x + 5, y0: p.y - 5, y1: p.y + 5 }; + if (collides(boxes, dot)) continue; + const lb = { x0: p.x + 5, x1: p.x + 9 + w, y0: p.y - 8, y1: p.y + 8 }; + const withLabel = !collides(boxes, lb) && lb.x1 <= MAP_W; + boxes.push(dot); + if (withLabel) boxes.push(lb); + const cls = pl.k ? "map-place-dot map-place-capital" : "map-place-dot"; + out += ``; + if (withLabel) out += `${esc(pl.n)}`; + } + } + return out ? `${out}` : ""; +}; + +const niceScale = (view) => { + const mpp = metersPerPx(view); + const target = 150 * mpp; + const pow = Math.pow(10, Math.floor(Math.log10(target))); + const nice = [1, 2, 5, 10].map((n) => n * pow).filter((n) => n <= target).pop() || pow; + const px = nice / mpp; + const label = nice >= 1000 ? `${Math.round(nice / 1000)} km` : `${Math.round(nice)} m`; + return { px, label }; +}; + +const renderScale = (view) => { + const { px, label } = niceScale(view); + const x = 14, y = MAP_H - 16; + return `${esc(label)} · z${view.zoom}`; +}; + +const renderAttribution = () => `Natural Earth`; + +const truncate = (s, n) => { + const line = String(s || "").split(/\r?\n/)[0].trim(); + return line.length > n ? line.slice(0, n - 1) + "…" : line; +}; + +const pinPath = (s) => `M0 0C${-7 * s} ${-9 * s} ${-11 * s} ${-14 * s} ${-11 * s} ${-21 * s}A${11 * s} ${11 * s} 0 1 1 ${11 * s} ${-21 * s}C${11 * s} ${-14 * s} ${7 * s} ${-9 * s} 0 0Z`; + +const clusterPins = (placed, view) => { + if (view.zoom >= MAX_ZOOM) return placed.map((p) => ({ ...p, members: [p] })); + const clusters = []; + for (const p of placed) { + const near = clusters.find((c) => Math.abs(c.x - p.x) <= CLUSTER_PX && Math.abs(c.y - p.y) <= CLUSTER_PX); + if (near) { + near.members.push(p); + near.x = near.members.reduce((s, m) => s + m.x, 0) / near.members.length; + near.y = near.members.reduce((s, m) => s + m.y, 0) / near.members.length; + } else { + clusters.push({ x: p.x, y: p.y, members: [p] }); + } + } + return clusters; +}; + +const renderPins = (view, pins, opts, boxes, hit = {}) => { + const placed = []; + pins.forEach((m, i) => { + const lat = num(m.lat), lng = num(m.lng); + if (lat == null || lng == null) return; + const p = toScreen(view, lat, lng); + if (p.x < -20 || p.x > MAP_W + 20 || p.y < -10 || p.y > MAP_H + 50) return; + placed.push({ ...m, lat, lng, x: p.x, y: p.y, idx: i }); + }); + if (!placed.length) return ""; + const clusters = clusterPins(placed.filter((p) => !p.pick), view); + for (const p of placed.filter((p) => p.pick)) clusters.push({ x: p.x, y: p.y, members: [p], pick: true }); + clusters.sort((a, b) => a.y - b.y); + const quiet = !!opts.popup; + let out = ""; + for (const c of clusters) { + const x = r1(c.x), y = r1(c.y); + if (c.members.length > 1) { + const n = c.members.length; + const center = fromScreen(view, c.x, c.y); + if (c.members.some((m) => m.focus)) hit.focus = { x: c.x, y: c.y, top: 20 }; + let gap = Infinity; + for (let i = 0; i < n; i++) for (let j = i + 1; j < n; j++) gap = Math.min(gap, Math.max(Math.abs(c.members[i].x - c.members[j].x), Math.abs(c.members[i].y - c.members[j].y))); + const splitZoom = gap > 0 ? view.zoom + Math.ceil(Math.log2((CLUSTER_PX + 1) / gap)) : MAX_ZOOM; + const href = opts.viewHref ? opts.viewHref({ zoom: clamp(Math.max(splitZoom, view.zoom + 1), MIN_ZOOM, MAX_ZOOM), lat: center.lat, lng: center.lng }) : null; + const title = opts.clusterTitle ? `${n} ${opts.clusterTitle}` : String(n); + const glyph = `${esc(title)}${n}`; + out += href ? `${glyph}` : glyph; + boxes.push({ x0: c.x - 20, x1: c.x + 20, y0: c.y - 20, y1: c.y + 20 }); + continue; + } + const m = c.members[0]; + if (m.pick) { + out += ``; + continue; + } + const s = m.main ? 1.25 : 1; + const cls = ["map-pin", m.main ? "map-pin-main" : "", m.focus ? "map-pin-focus" : ""].filter(Boolean).join(" "); + let g = ``; + if (m.title) g += `${esc(m.title)}`; + g += ``; + const silent = quiet && m.focus; + if (silent) hit.focus = { x: c.x, y: c.y, top: 32 * s }; + const label = silent ? "" : truncate(m.label, 28); + if (label) { + const w = textWidth(label, 12) + 12; + const ly = -21 * s - 11 * s - 16; + g += `${esc(label)}`; + boxes.push({ x0: c.x - w / 2, x1: c.x + w / 2, y0: c.y + ly - 12, y1: c.y }); + } else { + boxes.push({ x0: c.x - 12, x1: c.x + 12, y0: c.y - 34, y1: c.y }); + } + if (m.image && !silent) { + const size = 38; + g += ``; + boxes.push({ x0: c.x + 13 * s, x1: c.x + 13 * s + size + 4, y0: c.y - 21 * s - size / 2 - 2, y1: c.y - 21 * s + size / 2 + 2 }); + } + g += ""; + out += m.href ? `${g}` : g; + } + return `${out}`; +}; + +const popupPlacement = (at) => { + const roomAbove = at.y - at.top - POPUP_GAP; + const roomBelow = MAP_H - at.y - POPUP_GAP; + const below = roomAbove < POPUP_H && roomBelow > roomAbove; + const h = Math.min(POPUP_H, Math.max(below ? roomBelow : roomAbove, 60)); + const x = clamp(at.x - POPUP_W / 2, 4, MAP_W - POPUP_W - 4); + const y = below ? at.y + POPUP_GAP : at.y - at.top - POPUP_GAP - h; + return { x, y, w: POPUP_W, h, below, tipX: clamp(at.x, x + 14, x + POPUP_W - 14) }; +}; + +const renderPopup = (at, html) => { + if (!at || !html) return ""; + const p = popupPlacement(at); + const pointer = p.below + ? `M${r1(p.tipX - 8)} ${r1(p.y)}L${r1(p.tipX)} ${r1(p.y - 8)}L${r1(p.tipX + 8)} ${r1(p.y)}Z` + : `M${r1(p.tipX - 8)} ${r1(p.y + p.h)}L${r1(p.tipX)} ${r1(p.y + p.h + 8)}L${r1(p.tipX + 8)} ${r1(p.y + p.h)}Z`; + return `` + + `` + + `
${html}
`; +}; + +const renderGrid = (view, opts) => { + const pick = opts.pick; + const legacy = opts.clickUrl; + if (!pick && !legacy) return ""; + const cells = []; + for (let gy = 0; gy < GRID_ROWS; gy++) { + for (let gx = 0; gx < GRID_COLS; gx++) { + const c = fromScreen(view, (gx + 0.5) / GRID_COLS * MAP_W, (gy + 0.5) / GRID_ROWS * MAP_H); + const val = `${c.lat},${c.lng}`; + if (pick) { + cells.push(``); + } else { + const latParam = opts.latParam || "lat", lngParam = opts.lngParam || "lng"; + cells.push(``); + } + } + } + return `
${cells.join("")}
`; +}; + +const renderControls = (view, opts) => { + const nav = opts.nav; + if (!nav) return ""; + const t = opts.text || {}; + const el = (target, label, cls, title) => { + const value = target ? viewParam(target) : ""; + if (nav.form) { + return ``; + } + return `${label}`; + }; + const pan = (dx, dy) => { + const c = fromScreen(view, MAP_W / 2 + dx * MAP_W * 0.4, MAP_H / 2 + dy * MAP_H * 0.4); + return { zoom: view.zoom, lat: c.lat, lng: c.lng }; + }; + const zoomIn = view.zoom < MAX_ZOOM ? el({ zoom: view.zoom + 1, lat: view.lat, lng: view.lng }, "+", "map-ctrl-in", t.zoomIn || "+") : `+`; + const zoomOut = view.zoom > MIN_ZOOM ? el({ zoom: view.zoom - 1, lat: view.lat, lng: view.lng }, "−", "map-ctrl-out", t.zoomOut || "−") : `−`; + const fit = el(null, "⌖", "map-ctrl-fit", t.fit || "⌖"); + return `
${zoomIn}${zoomOut}
` + + `
` + + `${el(pan(0, -1), "↑", "map-ctrl-up", t.pan || "↑")}` + + `${el(pan(-1, 0), "←", "map-ctrl-left", t.pan || "←")}${fit}${el(pan(1, 0), "→", "map-ctrl-right", t.pan || "→")}` + + `${el(pan(0, 1), "↓", "map-ctrl-down", t.pan || "↓")}` + + `
`; +}; + +const renderMapHtml = (view, pins, opts = {}) => { + const detail = opts.thumb ? "low" : "normal"; + const boxes = []; + const pinList = Array.isArray(pins) ? pins : []; + const grid = renderGrid(view, opts); + const hit = {}; + const pinsSvg = renderPins(view, pinList, opts, boxes, hit) + renderPopup(hit.focus, opts.popup); + const labels = opts.thumb ? "" : renderLabels(view, boxes); + const extras = opts.thumb ? "" : renderScale(view) + renderAttribution(); + const base = `` + renderRaster(view, opts) + renderBase(view, detail); + const svgOpen = ``; + let html = `
`; + if (grid || opts.nav) { + html += svgOpen + base + labels + extras + ""; + html += grid; + html += `${pinsSvg}`; + } else { + html += svgOpen + base + labels + pinsSvg + extras + ""; + } + html += renderControls(view, opts); + html += "
"; + return html; +}; + +const renderMapWithPins = (markers, mainIdx) => { + const pins = (Array.isArray(markers) ? markers : []).map((m, i) => ({ ...m, main: i === (mainIdx || 0) })); + const fit = fitView(pins); + return renderMapHtml(makeView(fit.lat, fit.lng, fit.zoom), pins, {}); }; const renderZoomedMapWithPins = (centerLat, centerLng, zoom, markers, mainIdx) => { - const maxZ = getMaxTileZoom(); - if (!maxZ || zoom <= 2) return renderMapWithPins(markers, mainIdx); - - const effectiveZ = Math.min(zoom, maxZ); - const scale = Math.pow(2, zoom - effectiveZ); - const n = Math.pow(2, effectiveZ); - const tileSize = 256; - const worldPx = n * tileSize; - - const latRad = centerLat * Math.PI / 180; - const cx = (centerLng + 180) / 360 * worldPx; - const cy = (1 - Math.log(Math.tan(latRad) + 1 / Math.cos(latRad)) / Math.PI) / 2 * worldPx; - - const vw = MAP_W / scale; - const vh = MAP_H / scale; - const x0 = cx - vw / 2; - const y0 = cy - vh / 2; - - const pinData = (Array.isArray(markers) ? markers : []) - .filter((m) => m && typeof m.lat === "number" && typeof m.lng === "number") - .map((m, i) => { - const latR = m.lat * Math.PI / 180; - const wx = (m.lng + 180) / 360 * worldPx; - const wy = (1 - Math.log(Math.tan(latR) + 1 / Math.cos(latR)) / Math.PI) / 2 * worldPx; - return { px: (wx - x0) * scale, py: (wy - y0) * scale, main: i === (mainIdx || 0) }; - }); - - const hashInput = `z${zoom}_${Math.round(centerLat * 100)}_${Math.round(centerLng * 100)}_` + pinData.map(p => `${Math.round(p.px)},${Math.round(p.py)},${p.main}`).join(";"); - const hash = crypto.createHash("md5").update(hashInput).digest("hex").slice(0, 12); - const outFile = path.join(CACHE_DIR, `map_${hash}.png`); - - if (fs.existsSync(outFile)) return `map_${hash}.png`; - - const txMin = Math.max(0, Math.floor(x0 / tileSize)); - const txMax = Math.min(n - 1, Math.floor((x0 + vw) / tileSize)); - const tyMin = Math.max(0, Math.floor(y0 / tileSize)); - const tyMax = Math.min(n - 1, Math.floor((y0 + vh) / tileSize)); - - const tiles = []; - for (let tx = txMin; tx <= txMax; tx++) { - for (let ty = tyMin; ty <= tyMax; ty++) { - const tp = path.join(TILES_DIR, String(effectiveZ), `${tx}_${ty}.png`); - tiles.push({ tx, ty, tp, exists: fs.existsSync(tp) }); - } - } - - const script = ` -from PIL import Image, ImageDraw -import sys, json, os - -args = json.loads(sys.argv[1]) -out_file = sys.argv[2] - -tile_size = 256 -tx_min = args['txMin'] -ty_min = args['tyMin'] -tx_max = args['txMax'] -ty_max = args['tyMax'] -x0 = args['x0'] -y0 = args['y0'] -vw = args['vw'] -vh = args['vh'] -scale = args['scale'] -pins = args['pins'] -tiles = args['tiles'] - -canvas_w = (tx_max - tx_min + 1) * tile_size -canvas_h = (ty_max - ty_min + 1) * tile_size -canvas = Image.new('RGB', (canvas_w, canvas_h), (170, 211, 223)) - -for t in tiles: - if t['exists']: - try: - tile = Image.open(t['tp']).convert('RGB') - canvas.paste(tile, ((t['tx'] - tx_min) * tile_size, (t['ty'] - ty_min) * tile_size)) - except: - pass - -crop_x = x0 - tx_min * tile_size -crop_y = y0 - ty_min * tile_size -cropped = canvas.crop((int(crop_x), int(crop_y), int(crop_x + vw), int(crop_y + vh))) -result = cropped.resize((1024, 1024), Image.LANCZOS) - -draw = ImageDraw.Draw(result) -for p in pins: - px, py, main = p['px'], p['py'], p.get('main', False) - if -20 <= px <= 1044 and -20 <= py <= 1044: - sw = 3 if main else 2 - sh = 18 if main else 13 - clr = '#e74c3c' if main else '#3498db' - dark = '#c0392b' if main else '#2980b9' - draw.polygon([(px, py + 2), (px - sw, py - sh + sw * 2), (px + sw, py - sh + sw * 2)], fill=clr) - draw.ellipse([px - sw - 1, py - sh - sw, px + sw + 1, py - sh + sw], fill=dark, outline='white', width=1) - -result.save(out_file, optimize=True) -`; - - try { - fs.mkdirSync(CACHE_DIR, { recursive: true }); - execFileSync("python3", [ - "-c", script, - JSON.stringify({ txMin, tyMin, txMax, tyMax, x0, y0, vw, vh, scale, pins: pinData, tiles }), - outFile - ], { timeout: 15000 }); - } catch (e) { - return renderMapWithPins(markers, mainIdx); - } - - return `map_${hash}.png`; + const pins = (Array.isArray(markers) ? markers : []).map((m, i) => ({ ...m, main: i === (mainIdx || 0) })); + return renderMapHtml(makeView(centerLat, centerLng, zoom), pins, {}); }; -module.exports = { renderMapWithPins, renderZoomedMapWithPins, getViewportBounds, getMaxTileZoom, latLngToPx, pxToLatLng, MAP_W, MAP_H }; +module.exports = { + MAP_W, MAP_H, MIN_ZOOM, MAX_ZOOM, TILE_MAX_ZOOM, TILE_PATH, + project, unproject, latLngToPx, pxToLatLng, makeView, toScreen, fromScreen, fitView, resolveView, + parseView, viewParam, parsePick, getViewportBounds, metersPerPx, clusterPins, niceScale, tileCoverage, popupPlacement, + renderMapHtml, renderMapWithPins, renderZoomedMapWithPins +}; diff --git a/src/models/activity_model.js b/src/models/activity_model.js index 6b51862..b4770b6 100644 --- a/src/models/activity_model.js +++ b/src/models/activity_model.js @@ -1,8 +1,10 @@ const pull = require('../server/node_modules/pull-stream'); +const longText = require('../backend/long_text'); const ssbRef = require('../server/node_modules/ssb-ref'); const { getConfig } = require('../configs/config-manager.js'); const { buildVoteTally } = require('../backend/vote_tally'); const { buildValidatedTombstoneSet } = require('./tombstone_validator'); +const { versionGroups, versionEntry } = require('../backend/dedupe'); const logLimit = getConfig().ssbLogStream?.limit || 1000; const safeFeedId = (v) => { @@ -57,6 +59,8 @@ function inferType(c = {}) { } const HIDDEN_ENVELOPE_TYPES = new Set([ + 'textChunk', + 'aiExchange', 'tribe-keys-distrib', 'tribe-keys', 'tribe-invite-msg', @@ -67,7 +71,6 @@ const HIDDEN_ENVELOPE_TYPES = new Set([ 'larpJoinHouse', 'larpLeaveLarp', 'larpTestAttempt', - 'larpHousePost', 'larpHouseInvite', 'larpHouseInviteRedeem', 'larpHouseTribeAnchor', @@ -75,19 +78,29 @@ const HIDDEN_ENVELOPE_TYPES = new Set([ 'larpAutoInvite', 'karmaScore', 'pubAvailability', + 'pam', 'calendarReminderSent', 'taskReminderSent', + 'ubiClaim', 'ubiClaimResult', + 'ubiRefuse', 'ubiAllocation', + 'bankClaim', 'courts-key' ]); -module.exports = ({ cooler, tribeCrypto, tribesModel, padsModel }) => { +module.exports = ({ cooler, tribeCrypto, tribesModel, padsModel, industryModel, larpModel }) => { let ssb; const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb }; let _feedCache = null; let _feedCacheInflight = null; + const logTip = async () => { + try { + const ssbClient = await openSsb(); + return await require('./typed_log').logTip(ssbClient); + } catch (_) { return null; } + }; const FEED_CACHE_MS = 15 * 1000; const buildAccessibleTribeIds = async () => { @@ -95,14 +108,14 @@ module.exports = ({ cooler, tribeCrypto, tribesModel, padsModel }) => { if (!tribesModel) return set; try { const list = await tribesModel.listAll(); - for (const t of list) { - if (!t || !t.id) continue; + await Promise.all(list.map(async (t) => { + if (!t || !t.id) return; set.add(t.id); try { const chain = await tribesModel.getChainIds(t.id); for (const cid of chain) set.add(cid); } catch (_) {} - } + })); } catch (_) {} return set; }; @@ -143,6 +156,92 @@ module.exports = ({ cooler, tribeCrypto, tribesModel, padsModel }) => { return t.length > max ? t.slice(0, max - 1) + '…' : t; }; + const CHAT_THREAD_LIMIT = 6; + const buildChatThreads = async (ssbClient, idToAction, rootOf) => { + const replacedChatIds = new Set(); + for (const a of idToAction.values()) { + if (a.type !== 'chat') continue; + const rep = a.content && a.content.replaces; + if (rep) replacedChatIds.add(rep); + } + const stateByRoot = new Map(); + for (const a of idToAction.values()) { + if (a.type !== 'chat') continue; + const root = rootOf(a.id); + const isTip = !replacedChatIds.has(a.id); + const prev = stateByRoot.get(root); + if (prev && !((isTip && !prev.isTip) || (isTip === prev.isTip && (a.ts || 0) >= prev.ts))) continue; + const cc = a.content || {}; + stateByRoot.set(root, { ts: a.ts || 0, isTip, owner: cc.author || a.author || null, status: String(cc.status || '').toUpperCase(), tribeId: cc.tribeId || null, title: String(cc.title || ''), description: String(cc.description || ''), members: Array.isArray(cc.members) ? cc.members.length : 0 }); + } + const msgsByRoot = new Map(); + for (const a of idToAction.values()) { + const isPoll = a.type === 'poll'; + if (a.type !== 'chatMessage' && !isPoll) continue; + const c = a.content || {}; + if (c.tribeId || c.encryptedText || c.encryptedQuestion) continue; + const root = c.chatId; + if (!root) continue; + if (isPoll) { + if (!(typeof c.question === 'string' && c.question.trim())) continue; + } else if (!(typeof c.text === 'string' && c.text) && !c.image) continue; + if (!msgsByRoot.has(root)) msgsByRoot.set(root, []); + msgsByRoot.get(root).push(isPoll ? { ...a, content: { chatId: root, text: c.question.trim(), poll: true } } : a); + } + const missingRoots = [...msgsByRoot.keys()].filter(root => !stateByRoot.has(root)); + const rootMsgs = new Map(await Promise.all(missingRoots.map(async root => [root, await getMsg(ssbClient, root)]))); + for (const root of missingRoots) { + const val = rootMsgs.get(root); + const cc = val && val.content; + if (cc && typeof cc === 'object' && cc.type === 'chat') { + stateByRoot.set(root, { ts: 0, owner: cc.author || val.author || null, status: String(cc.status || '').toUpperCase(), tribeId: cc.tribeId || null, title: String(cc.title || ''), description: String(cc.description || ''), members: Array.isArray(cc.members) ? cc.members.length : 0 }); + } else { + stateByRoot.set(root, null); + } + } + const items = []; + for (const [root, msgs] of msgsByRoot.entries()) { + const info = stateByRoot.get(root); + if (!info || info.tribeId || info.status !== 'OPEN') continue; + const asc = msgs.slice().sort((x, y) => (x.ts || 0) - (y.ts || 0)); + const last = asc[asc.length - 1]; + items.push({ + id: `chatthread:${root}`, + type: 'chatThread', + author: last.author, + ts: last.ts || 0, + content: { + type: 'chatThread', + chatRoot: root, + chatAuthor: info.owner || null, + title: info.title || '', + description: info.description || '', + members: Math.max(info.members || 0, new Set(asc.map(m => m.author)).size), + messageCount: asc.length, + replies: (() => { + const byId = new Map(asc.map(m => [m.id, m])); + return asc.slice(-CHAT_THREAD_LIMIT).map(m => { + const quotedId = (m.content && typeof m.content.replyTo === 'string') ? m.content.replyTo : null; + const quoted = quotedId ? byId.get(quotedId) : null; + return { + id: m.id, + author: m.author, + ts: m.ts || 0, + text: (m.content && m.content.text) || '', + poll: !!(m.content && m.content.poll), + image: (m.content && m.content.image) || null, + mimeType: (m.content && m.content.mimeType) || '', + replyTo: quotedId, + reply: quoted ? { id: quoted.id, author: quoted.author, text: String((quoted.content && quoted.content.text) || '').slice(0, 140) } : null + }; + }); + })() + } + }); + } + return items; + }; + return { invalidateCache() { _feedCache = null; @@ -156,16 +255,18 @@ module.exports = ({ cooler, tribeCrypto, tribesModel, padsModel }) => { if (entry && now - entry.ts < FEED_CACHE_MS) return entry.value; if (!_feedCacheInflight) _feedCacheInflight = new Map(); if (_feedCacheInflight.has(cacheKey)) return _feedCacheInflight.get(cacheKey); + const tipNow = await logTip(); + if (entry && tipNow && entry.tip === tipNow) { entry.ts = now; return entry.value; } const promise = (async () => { const ssbClient = await openSsb(); const userId = ssbClient.id; - const results = await new Promise((resolve, reject) => { + const results = (await new Promise((resolve, reject) => { pull( - ssbClient.createLogStream({ reverse: true, limit: logLimit }), + ssbClient.createLogStream({}), pull.collect((err, msgs) => err ? reject(err) : resolve(msgs)) ); - }); + })).reverse(); if (typeof ssbClient.createUserStream === 'function') { const ownResults = await new Promise((resolve) => { @@ -181,12 +282,42 @@ module.exports = ({ cooler, tribeCrypto, tribesModel, padsModel }) => { } const tombstoned = buildValidatedTombstoneSet(results); + const chunkLookup = longText.lookupIn(longText.indexChunks(results)); const parentOf = new Map(); const idToAction = new Map(); const rawById = new Map(); const voteTally = buildVoteTally(results); + const pollTally = new Map(); + for (const m of results) { + const c = m && m.value && m.value.content; + if (!c || c.type !== 'pollVote' || typeof c.target !== 'string') continue; + if (!Array.isArray(c.choices)) continue; + const entry = pollTally.get(c.target) || { counts: {}, voters: new Map() }; + entry.voters.set(m.value.author, { ts: m.value.timestamp || 0, choices: c.choices }); + pollTally.set(c.target, entry); + } + for (const entry of pollTally.values()) { + entry.counts = {}; + for (const v of entry.voters.values()) { + for (const choice of v.choices) entry.counts[choice] = (entry.counts[choice] || 0) + 1; + } + } const fpIdx = tribeCrypto ? tribeCrypto.buildFingerprintIndex() : null; const accessibleTribeIds = await buildAccessibleTribeIds(); + let larpWall = null; + if (larpModel && results.some(m => m && m.value && m.value.content && m.value.content.type === 'larpHousePost')) { + try { + const s = await openSsb(); + larpWall = { + mine: await larpModel.getUserHouse(s.id).catch(() => null), + governing: typeof larpModel.getGoverningHouseKey === 'function' ? larpModel.getGoverningHouseKey() : null, + members: await larpModel.listAllMemberships().catch(() => new Map()) + }; + } catch (_) { larpWall = null; } + } + const larpPostVisible = (author, house) => !!larpWall && !!house + && (larpWall.members.get(author) || 'academia') === house + && (house === 'academia' || house === larpWall.governing || house === larpWall.mine); for (const msg of results) { const k = msg.key; @@ -194,7 +325,9 @@ module.exports = ({ cooler, tribeCrypto, tribesModel, padsModel }) => { let c = v?.content; if (!c) continue; if (typeof c === 'string' && c.endsWith('.box')) continue; + if (c.private === true) continue; if (c.type && HIDDEN_ENVELOPE_TYPES.has(c.type)) continue; + if (c.type === 'larpHousePost' && !larpPostVisible(v.author, String(c.house || '').toLowerCase())) continue; if (typeof c.type === 'string' && /e2ee/i.test(c.type)) continue; if (c.type === 'contact') continue; if (tribeCrypto && tribeCrypto.isTribeMsg(c)) { @@ -224,6 +357,8 @@ module.exports = ({ cooler, tribeCrypto, tribesModel, padsModel }) => { } catch (_) {} } if (!isContentSane(c)) continue; + if ((c.type === 'post' || c.type === 'forum') && longText.hasChunks(c)) c = longText.resolveField(c, 'text', v?.author, chunkLookup); + if (c.type === 'about' && !c.name && !c.description && !c.image) continue; const ts = v?.timestamp || Number(c?.timestamp || 0) || (c?.updatedAt ? Date.parse(c.updatedAt) : 0) || 0; idToAction.set(k, { id: k, author: v?.author, ts, type: inferType(c), content: c }); rawById.set(k, msg); @@ -232,17 +367,23 @@ module.exports = ({ cooler, tribeCrypto, tribesModel, padsModel }) => { const forgedParentOf = new Map(); const forgedFeedContent = new Map(); + const vg = versionGroups(Array.from(idToAction.values()).map(a => versionEntry({ key: a.id, value: { ...(rawById.get(a.id) || {}).value, content: a.content } }))); for (const [child, parent] of Array.from(parentOf.entries())) { const ca = idToAction.get(child); - const pa = idToAction.get(parent); - if (!pa) { parentOf.delete(child); continue; } - if (!ca || String(ca.author) !== String(pa.author)) { + if (vg.isOrphan(child)) { + parentOf.delete(child); + if (vg.isForeignCopy(child)) { idToAction.delete(child); rawById.delete(child); } + continue; + } + if (!ca || vg.isForeign(child)) { forgedParentOf.set(child, parent); if (ca && ca.type === 'feed') forgedFeedContent.set(child, ca.content || {}); parentOf.delete(child); idToAction.delete(child); rawById.delete(child); + continue; } + if (vg.isForeign(parent)) parentOf.set(child, vg.rootOf(child)); } const replacedIds = new Set(parentOf.values()); @@ -256,7 +397,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel, padsModel }) => { const target = String(c.target || ''); const author = v.author; if (!target || !author) continue; - const key = `${target}${author}`; + const key = `${target}\u0000${author}`; const prev = aiHelpfulSeen.get(key); const curTs = v.timestamp || 0; if (!prev || curTs > prev.ts) aiHelpfulSeen.set(key, { target, ts: curTs, helpful: c.helpful !== false }); @@ -298,6 +439,10 @@ module.exports = ({ cooler, tribeCrypto, tribesModel, padsModel }) => { } const fetchedTargetCache = new Map(); + const missingTargets = [...new Set([...idToAction.values()].filter(a => a.type === 'spread').map(a => (a.content || {}).vote?.link || '').filter(link => link && !rawById.get(link)))]; + for (const [link, got] of await Promise.all(missingTargets.map(async link => [link, await getMsg(ssbClient, link)]))) { + fetchedTargetCache.set(link, got ? { key: link, value: got } : null); + } for (const a of idToAction.values()) { if (a.type !== 'spread') continue; @@ -457,7 +602,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel, padsModel }) => { const v = msg && msg.value; const c = v && v.content; if (!c || typeof c !== 'object') continue; - const isOpinion = c.type === 'feedOpinion' && typeof c.target === 'string'; + const isOpinion = typeof c.type === 'string' && /Opinion$/.test(c.type) && c.type !== 'pollOpinion' && typeof c.target === 'string'; const isVoteAction = c.type === 'feed-action' && c.action === 'vote' && (typeof c.root === 'string' || typeof c.target === 'string'); if (!isOpinion && !isVoteAction) continue; const target = isOpinion ? c.target : String(c.root || c.target); @@ -487,21 +632,19 @@ module.exports = ({ cooler, tribeCrypto, tribesModel, padsModel }) => { } const latest = []; + const docUrls = [...new Set([...idToAction.values()].filter(a => a.type === 'document').map(a => (a.content || {}).url))]; + const docBlobOk = new Map(await Promise.all(docUrls.map(async url => [url, await hasBlob(ssbClient, url)]))); for (const a of idToAction.values()) { if (tombstoned.has(a.id)) continue; if (a.type === 'tribe' && parentOf.has(a.id)) continue; const c = a.content || {}; - if (c.root && tombstoned.has(c.root)) continue; + if (c.root && tombstoned.has(c.root) && !c.replaces) continue; if (a.type === 'vote' && tombstoned.has(c.vote?.link)) continue; if (a.type === 'spread' && (c.spreadTargetId || c.vote?.link) && tombstoned.has(c.spreadTargetId || c.vote?.link)) continue; if (c.key && tombstoned.has(c.key)) continue; if (c.branch && tombstoned.has(c.branch)) continue; if (c.target && tombstoned.has(c.target)) continue; - if (a.type === 'document') { - const url = c.url; - const ok = await hasBlob(ssbClient, url); - if (!ok) continue; - } + if (a.type === 'document' && !docBlobOk.get(c.url)) continue; if (a.type === 'forum' && c.root) { const rootId = typeof c.root === 'string' ? c.root : (c.root?.key || c.root?.id || ''); const rootAction = idToAction.get(rootId); @@ -511,7 +654,25 @@ module.exports = ({ cooler, tribeCrypto, tribesModel, padsModel }) => { const actionRoot = rootOf(a.id); const extra = a.type === 'aiExchange' ? { helpfulVotes: aiHelpfulCounts.get(a.id) || 0 } : {}; const voteAgg = a.type === 'votes' ? voteTally.get(a.id) : null; + if (a.type === 'poll') { + const tally = pollTally.get(a.id) || pollTally.get(actionRoot); + a.pollCounts = tally ? tally.counts : {}; + a.pollVoters = tally ? tally.voters.size : 0; + } let content = voteAgg ? { ...c, ...voteAgg } : a.content; + if (a.type !== 'feed' && a.type !== 'votes' && (feedOpinionsByRoot.get(actionRoot) || []).length) { + const base = a.content || {}; + const opinions = { ...(base.opinions || {}) }; + const voters = Array.isArray(base.opinions_inhabitants) ? base.opinions_inhabitants.slice() : []; + const voterSet = new Set(voters); + for (const op of feedOpinionsByRoot.get(actionRoot)) { + if (!op.author || voterSet.has(op.author)) continue; + voterSet.add(op.author); + voters.push(op.author); + if (op.category) opinions[op.category] = (Number(opinions[op.category]) || 0) + 1; + } + content = { ...content, opinions, opinions_inhabitants: voters }; + } if (a.type === 'feed') { const base = a.content || {}; const opinions = { ...(base.opinions || {}) }; @@ -541,6 +702,8 @@ module.exports = ({ cooler, tribeCrypto, tribesModel, padsModel }) => { let deduped = latest.filter(a => !a.tipId || a.tipId === a.id || (a.type === 'tribe' && !parentOf.has(a.id))); const mediaTypes = new Set(['image','video','audio','document','bookmark','map']); + const textTypes = new Set(['feed', 'post']); + const REPEAT_WINDOW_MS = 10 * 60 * 1000; const perAuthorUnique = new Set(); const byKey = new Map(); const norm = s => String(s || '').trim().toLowerCase(); @@ -580,6 +743,19 @@ module.exports = ({ cooler, tribeCrypto, tribesModel, padsModel }) => { } else if (prevHasImage === newHasImage && effTs > prev.__effTs) { byKey.set(key, { ...a, __effTs: effTs, __hasImage: newHasImage }); } + } else if (textTypes.has(a.type)) { + const t = norm(c.text); + const key = t ? `${a.type}:${a.author}:${t}` : null; + const prev = key ? byKey.get(key) : null; + if (!key) { + byKey.set(`id:${a.id}`, { ...a, __effTs: effTs }); + } else if (!prev) { + byKey.set(key, { ...a, __effTs: effTs }); + } else if (Math.abs(effTs - prev.__effTs) <= REPEAT_WINDOW_MS) { + if (effTs > prev.__effTs) byKey.set(key, { ...a, __effTs: effTs }); + } else { + byKey.set(`id:${a.id}`, { ...a, __effTs: effTs }); + } } else if (a.type === 'tribe') { const t = norm(c.title); if (t) { @@ -598,11 +774,37 @@ module.exports = ({ cooler, tribeCrypto, tribesModel, padsModel }) => { deduped = Array.from(byKey.values()).map(x => { delete x.__effTs; delete x.__hasImage; return x }); + if (industryModel && deduped.some(a => a.type === 'industryBuild')) { + const builds = await industryModel.listAllBuilds().catch(() => []); + const buildStatusById = new Map(); + for (const b of (builds || [])) buildStatusById.set(b.id || b.key, String(b.status || 'PROPOSED').toUpperCase()); + deduped = deduped.filter(a => { + if (a.type !== 'industryBuild') return true; + const st = buildStatusById.get(a.id); + if (st === undefined || st === 'PROPOSED' || st === 'REJECTED') return false; + a.content = { ...a.content, buildStatus: st }; + return true; + }); + } + + if (industryModel && deduped.some(a => a.type === 'industryBlueprint')) { + const bps = await industryModel.listAllBlueprints().catch(() => []); + const bpById = new Map((bps || []).map(b => [b.id || b.key, b])); + for (const a of deduped) { + if (a.type !== 'industryBlueprint') continue; + const b = bpById.get(a.id); + if (b && b.estTotal != null) a.content = { ...a.content, estTotal: b.estTotal }; + } + } + const tribeInternalTypes = new Set(['tribe-content', 'tribeParliamentCandidature', 'tribeParliamentTerm', 'tribeParliamentProposal', 'tribeParliamentRule', 'tribeParliamentLaw', 'tribeParliamentRevocation']); - const hiddenTypes = new Set(['padEntry', 'chatMessage', 'calendarDate', 'calendarNote', 'calendarReminderSent', 'taskReminderSent', 'feed-action', 'pubBalance', 'pubAvailability', 'log', 'gameScore']); + const hiddenTypes = new Set(['wallet', 'bankWallet', 'padEntry', 'roomMember', 'chatMessage', 'calendarDate', 'calendarNote', 'calendarReminderSent', 'taskReminderSent', 'feed-action', 'pubBalance', 'pubAvailability', 'log', 'logPublic', 'gameScore', 'pollVote', 'pollClose', 'pollOpinion', 'curriculum', 'schoolLesson', 'schoolEnroll', 'schoolCertificate', 'schoolProgress', 'schoolExam', 'schoolExamResult', 'school-invite', 'schoolExamQuestion', 'schoolLessonMedia']); + const chatThreadItems = await buildChatThreads(ssbClient, idToAction, rootOf, deduped); + deduped = deduped.concat(chatThreadItems); const isAllowedTribeActivity = (a) => { if (tribeInternalTypes.has(a.type)) return false; const c = a.content || {}; + if (a.type === 'poll' && c.chatId) return false; if (c.tribeId) return false; if (a.type === 'tribe') { const isInitial = !c.replaces; @@ -613,25 +815,31 @@ module.exports = ({ cooler, tribeCrypto, tribesModel, padsModel }) => { } return true; }; - const itemVisible = (a) => { - if (hiddenTypes.has(a.type)) return false; + const itemVisible = (a, opts = {}) => { + if (hiddenTypes.has(a.type) && !(opts.allowHidden && opts.allowHidden.has(a.type))) return false; const c = a.content || {}; if (c.encryptedPayload) return false; if (a.type === 'pad' && c.status !== 'OPEN') return false; + if (a.type === 'room' && (c.status !== 'OPEN' || c.encrypted === true)) return false; if (a.type === 'chat' && c.status !== 'OPEN') return false; if (a.type === 'calendar' && c.status !== 'OPEN') return false; if (a.type === 'event' && String(c.isPublic || '').toLowerCase() === 'private' && c.organizer !== userId && !(Array.isArray(c.attendees) && c.attendees.includes(userId))) return false; if (a.type === 'task' && String(c.isPublic || '').toUpperCase() === 'PRIVATE' && a.author !== userId && !(Array.isArray(c.assignees) && c.assignees.includes(userId))) return false; if (a.type === 'forum' && c.isPrivate === true && a.author !== userId) return false; if (a.type === 'job' && String(c.visibility || '').toUpperCase() === 'HIDDEN' && a.author !== userId && !(Array.isArray(c.subscribers) && c.subscribers.includes(userId))) return false; + if (a.type === 'schoolCourse' && c.encryptedPayload) return false; + if (a.type === 'schoolCourse' && String(c.visibility || '').toUpperCase() === 'INVITE' && a.author !== userId && !(Array.isArray(c.invited) && c.invited.includes(userId))) return false; + if (a.type === 'housing' && String(c.visibility || '').toUpperCase() === 'HIDDEN' && a.author !== userId) return false; if (a.type === 'market' && String(c.visibility || '').toUpperCase() === 'HIDDEN' && c.seller !== userId) return false; + if (a.type === 'market' && c.shopProductId) return false; if (a.type === 'shop' && String(c.visibility || '').toUpperCase() === 'CLOSED' && a.author !== userId) return false; if (a.type === 'curriculum' && String(c.visibility || '').toUpperCase() === 'HIDDEN' && a.author !== userId) return false; if (a.type === 'shopProduct' && c.shopVisibility && String(c.shopVisibility).toUpperCase() === 'CLOSED' && a.author !== userId) return false; + if (a.type === 'transfer' && String(c.status || '').toUpperCase() === 'UNCONFIRMED' && a.author !== userId && c.from !== userId && c.to !== userId) return false; return true; }; - const isVisible = (a) => { - if (!itemVisible(a)) return false; + const isVisible = (a, opts) => { + if (!itemVisible(a, opts)) return false; if (a.type === 'post' || a.type === 'opinion') { const c = a.content || {}; const ref = (typeof c.root === 'string' && c.root) @@ -650,7 +858,14 @@ module.exports = ({ cooler, tribeCrypto, tribesModel, padsModel }) => { if (filter === 'mine') out = deduped.filter(a => a.author === userId && isAllowedTribeActivity(a) && isVisible(a)); else if (filter === 'recent') { const cutoff = Date.now() - 24 * 60 * 60 * 1000; out = deduped.filter(a => (a.ts || 0) >= cutoff && isAllowedTribeActivity(a) && isVisible(a)) } else if (filter === 'all') out = deduped.filter(a => isAllowedTribeActivity(a) && isVisible(a)); - else if (filter === 'banking') out = deduped.filter(a => a.type === 'bankWallet' || a.type === 'bankClaim' || a.type === 'ubiClaim'); + else if (filter === 'top') { + const visible = deduped.filter(a => isAllowedTribeActivity(a) && isVisible(a)); + const perAuthor = new Map(); + for (const a of visible) perAuthor.set(a.author, (perAuthor.get(a.author) || 0) + 1); + out = visible + .map(a => ({ ...a, authorActions: perAuthor.get(a.author) || 0 })) + .sort((x, y) => (y.authorActions - x.authorActions) || ((y.ts || 0) - (x.ts || 0))); + } else if (filter === 'tribe') out = deduped.filter(a => a.type === 'tribe' || String(a.type || '').startsWith('tribe')); else if (filter === 'spread') out = deduped.filter(a => a.type === 'spread'); else if (filter === 'parliament') @@ -664,18 +879,30 @@ module.exports = ({ cooler, tribeCrypto, tribesModel, padsModel }) => { }); else if (filter === 'task') out = deduped.filter(a => a.type === 'task' || a.type === 'taskAssignment'); + else if (filter === 'votes') + out = deduped.filter(a => (a.type === 'votes' || a.type === 'poll') && isAllowedTribeActivity(a) && isVisible(a)); + else if (filter === 'industry') + out = deduped.filter(a => ['industry', 'industryBuild', 'industryBlueprint', 'industryAllocation'].includes(a.type) && isVisible(a)); else if (filter === 'pad') out = deduped.filter(a => a.type === 'pad' && (a.content || {}).status === 'OPEN'); - else if (filter === 'chat') out = deduped.filter(a => a.type === 'chat' && (a.content || {}).status === 'OPEN'); + else if (filter === 'room') out = deduped.filter(a => a.type === 'room' && (a.content || {}).status === 'OPEN' && (a.content || {}).encrypted !== true); + else if (filter === 'wiki') out = deduped.filter(a => a.type === 'wikiPage' && isVisible(a)); + else if (filter === 'emergency') out = deduped.filter(a => (a.type === 'emergency' || a.type === 'emergencyUpdate') && isVisible(a)); + else if (filter === 'campaign') out = deduped.filter(a => (a.type === 'campaign' || a.type === 'campaignUpdate') && isVisible(a)); + else if (filter === 'mailing') out = deduped.filter(a => a.type === 'mailingList' && isVisible(a)); + else if (filter === 'logistics') out = deduped.filter(a => a.type === 'logisticsRoute' && isVisible(a)); + else if (filter === 'podcast') out = deduped.filter(a => (a.type === 'podcast' || a.type === 'podcastEpisode') && isVisible(a)); + else if (filter === 'chat') out = deduped.filter(a => (a.type === 'chat' || a.type === 'chatThread') && isAllowedTribeActivity(a) && isVisible(a)); else if (filter === 'calendar') out = deduped.filter(a => a.type === 'calendar' && (a.content || {}).status === 'OPEN'); + else if (filter === 'transfer') out = deduped.filter(a => a.type === 'transfer' && isVisible(a)); else out = deduped.filter(a => a.type === filter); - out.sort((a, b) => (b.ts || 0) - (a.ts || 0)); + if (filter !== 'top') out.sort((a, b) => (b.ts || 0) - (a.ts || 0)); return out; })(); _feedCacheInflight.set(cacheKey, promise); try { const value = await promise; - _feedCache.set(cacheKey, { value, ts: Date.now() }); + _feedCache.set(cacheKey, { value, ts: Date.now(), tip: tipNow }); return value; } finally { _feedCacheInflight.delete(cacheKey); diff --git a/src/models/agenda_model.js b/src/models/agenda_model.js index 863b8a5..e1b3f67 100644 --- a/src/models/agenda_model.js +++ b/src/models/agenda_model.js @@ -3,9 +3,11 @@ const path = require('path'); const pull = require('../server/node_modules/pull-stream'); const moment = require('../server/node_modules/moment'); -const agendaConfigPath = path.join(__dirname, '../configs/agenda-config.json'); +const agendaConfigPath = require('../configs/state-manager').statePath('agenda-config.json'); const { getConfig } = require('../configs/config-manager.js'); const { buildValidatedTombstoneSet } = require('./tombstone_validator'); +const { readTyped } = require('./typed_log'); +const { versionGroups, versionEntry } = require('../backend/dedupe'); const logLimit = getConfig().ssbLogStream?.limit || 1000; function readAgendaConfig() { @@ -19,137 +21,117 @@ function writeAgendaConfig(cfg) { fs.writeFileSync(agendaConfigPath, JSON.stringify(cfg, null, 2)); } -module.exports = ({ cooler, calendarsModel, eventsModel, tasksModel, marketModel, jobsModel, projectsModel }) => { +module.exports = ({ cooler, campaignsModel = null, logisticsModel = null, calendarsModel, eventsModel, tasksModel, marketModel, jobsModel, projectsModel, industryModel, housingModel, schoolModel }) => { let ssb; const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb; }; const STATUS_ORDER = ['FOR SALE', 'OPEN', 'RESERVED', 'CLOSED', 'SOLD']; const sIdx = s => STATUS_ORDER.indexOf(String(s || '').toUpperCase()); - const fetchItems = (targetType) => - new Promise((resolve, reject) => { - openSsb().then((ssbClient) => { - pull( - ssbClient.createLogStream({ limit: logLimit }), - pull.collect((err, msgs) => { - if (err) return reject(err); + const fetchItems = async (targetType) => { + const ssbClient = await openSsb(); + const msgs = await readTyped(ssbClient, [targetType, 'tombstone'], { limit: logLimit }); - const tomb = buildValidatedTombstoneSet(msgs); - const nodes = new Map(); - const parent = new Map(); - const child = new Map(); + const tomb = buildValidatedTombstoneSet(msgs); + const nodes = new Map(); + const versions = []; - for (const m of msgs) { - const k = m.key; - const v = m.value; - const c = v?.content; - if (!c) continue; - if (c.type === 'tombstone') continue; - if (c.type !== targetType) continue; - if (c.encryptedPayload) continue; - nodes.set(k, { key: k, ts: v.timestamp || 0, author: v.author, content: c }); - if (c.replaces) parent.set(k, c.replaces); - } + for (const m of msgs) { + const k = m.key; + const v = m.value; + const c = v?.content; + if (!c) continue; + if (c.type === 'tombstone') continue; + if (c.type !== targetType) continue; + if (c.encryptedPayload) continue; + nodes.set(k, { key: k, ts: v.timestamp || 0, author: v.author, content: c }); + versions.push(versionEntry(m)); + } - for (const [k, p] of Array.from(parent.entries())) { - const cn = nodes.get(k); - const pn = nodes.get(p); - if (!pn) { parent.delete(k); continue; } - if (!cn || String(cn.author) !== String(pn.author)) { parent.delete(k); nodes.delete(k); } - } - for (const [k, p] of parent.entries()) child.set(p, k); + const vg = versionGroups(versions); + const groups = new Map(); + for (const r of vg.roots) groups.set(r, new Set(vg.groups.get(r).filter(id => !vg.isForeign(id)))); - const rootOf = (id) => { let cur = id; while (parent.has(cur) && nodes.has(parent.get(cur))) cur = parent.get(cur); return cur; }; + const statusOrder = ['FOR SALE', 'OPEN', 'RESERVED', 'CLOSED', 'SOLD']; + const sIdx = s => statusOrder.indexOf(String(s || '').toUpperCase()); - const groups = new Map(); - for (const id of nodes.keys()) { - const r = rootOf(id); - if (!groups.has(r)) groups.set(r, new Set()); - groups.get(r).add(id); - } + const out = []; - const statusOrder = ['FOR SALE', 'OPEN', 'RESERVED', 'CLOSED', 'SOLD']; - const sIdx = s => statusOrder.indexOf(String(s || '').toUpperCase()); + for (const [root, ids] of groups.entries()) { + const items = Array.from(ids).map(id => nodes.get(id)).filter(n => n && !tomb.has(n.key)); + if (!items.length) continue; - const out = []; + const tip = nodes.get(vg.tipOf(root)); + if (tomb.has(tip.key)) continue; - for (const [root, ids] of groups.entries()) { - const items = Array.from(ids).map(id => nodes.get(id)).filter(n => n && !tomb.has(n.key)); - if (!items.length) continue; + if (targetType === 'market') { + let chosen = items[0]; + for (const n of items) { + const a = sIdx(n.content.status); + const b = sIdx(chosen.content.status); + if (a > b || (a === b && n.ts > chosen.ts)) chosen = n; + } + const c = chosen.content; + let status = c.status; + if (c.deadline) { + const dl = moment(c.deadline); + if (dl.isValid() && dl.isBefore(moment()) && String(status).toUpperCase() !== 'SOLD') status = 'DISCARDED'; + } + if (status === 'FOR SALE' && (c.stock || 0) === 0) continue; - let tipId = Array.from(ids).find(id => !child.has(id)); - let tip = tipId ? nodes.get(tipId) : items.reduce((a, b) => a.ts > b.ts ? a : b); + out.push({ + ...c, + status, + author: chosen.author, + id: chosen.key, + tipId: chosen.key, + createdAt: c.createdAt || chosen.ts + }); + continue; + } - if (targetType === 'market') { - let chosen = items[0]; - for (const n of items) { - const a = sIdx(n.content.status); - const b = sIdx(chosen.content.status); - if (a > b || (a === b && n.ts > chosen.ts)) chosen = n; - } - const c = chosen.content; - let status = c.status; - if (c.deadline) { - const dl = moment(c.deadline); - if (dl.isValid() && dl.isBefore(moment()) && String(status).toUpperCase() !== 'SOLD') status = 'DISCARDED'; - } - if (status === 'FOR SALE' && (c.stock || 0) === 0) continue; + if (targetType === 'job') { + const latest = items.sort((a, b) => b.ts - a.ts)[0]; + const withSubsNode = items + .filter(n => Array.isArray(n.content.subscribers)) + .sort((a, b) => b.ts - a.ts)[0]; + const subscribers = withSubsNode ? withSubsNode.content.subscribers : []; + const latestWithStatus = items + .filter(n => typeof n.content.status !== 'undefined') + .sort((a, b) => b.ts - a.ts)[0]; + const resolvedStatus = latestWithStatus + ? latestWithStatus.content.status + : latest.content.status; - out.push({ - ...c, - status, - author: chosen.author, - id: chosen.key, - tipId: chosen.key, - createdAt: c.createdAt || chosen.ts - }); - continue; - } + const c = { ...latest.content, status: resolvedStatus, subscribers }; - if (targetType === 'job') { - const latest = items.sort((a, b) => b.ts - a.ts)[0]; - const withSubsNode = items - .filter(n => Array.isArray(n.content.subscribers)) - .sort((a, b) => b.ts - a.ts)[0]; - const subscribers = withSubsNode ? withSubsNode.content.subscribers : []; - const latestWithStatus = items - .filter(n => typeof n.content.status !== 'undefined') - .sort((a, b) => b.ts - a.ts)[0]; - const resolvedStatus = latestWithStatus - ? latestWithStatus.content.status - : latest.content.status; + out.push({ + ...c, + author: latest.author, + id: latest.key, + tipId: latest.key, + createdAt: c.createdAt || latest.ts + }); + continue; + } - const c = { ...latest.content, status: resolvedStatus, subscribers }; + out.push({ + ...tip.content, + author: tip.author, + id: tip.key, + tipId: tip.key, + createdAt: tip.content.createdAt || tip.ts + }); + } - out.push({ - ...c, - author: latest.author, - id: latest.key, - tipId: latest.key, - createdAt: c.createdAt || latest.ts - }); - continue; - } - - out.push({ - ...tip.content, - author: tip.author, - id: tip.key, - tipId: tip.key, - createdAt: tip.content.createdAt || tip.ts - }); - } - - resolve(out); - }) - ); - }).catch(reject); - }); + return out; + }; return { async listAgenda(filter = 'all') { const agendaConfig = readAgendaConfig(); const discardedItems = agendaConfig.discardedItems || []; + const removedItems = agendaConfig.removedItems || []; const ssbClient = await openSsb(); const userId = ssbClient.id; @@ -184,7 +166,27 @@ module.exports = ({ cooler, calendarsModel, eventsModel, tasksModel, marketModel ? projectsModel.listProjects('all').then(normalize).catch(() => []) : fetchItems('project'); - const [tasksAll, eventsAll, transfersAll, tribesAll, marketAll, reportsAll, jobsAll, projectsAll, calendarsAll] = await Promise.all([ + const industryViaModel = industryModel && typeof industryModel.listMyBuilds === 'function' + ? industryModel.listMyBuilds().then(normalize).catch(() => []) + : []; + + const housingViaModel = housingModel && typeof housingModel.listHousing === 'function' + ? housingModel.listHousing('ALL', userId).then(normalize).catch(() => []) + : []; + + const schoolViaModel = schoolModel && typeof schoolModel.listCourses === 'function' + ? schoolModel.listCourses('ALL', userId, {}).then(normalize).catch(() => []) + : []; + + const campaignsViaModel = campaignsModel && typeof campaignsModel.listAll === 'function' + ? campaignsModel.listAll({ filter: 'all' }).catch(() => []) + : []; + + const logisticsViaModel = logisticsModel && typeof logisticsModel.listAll === 'function' + ? logisticsModel.listAll({ filter: 'all' }).catch(() => []) + : []; + + const [tasksAll, eventsAll, transfersAll, tribesAll, marketAll, reportsAll, jobsAll, projectsAll, calendarsAll, industryAll, housingAll, schoolAll, campaignsAll, logisticsAll] = await Promise.all([ tasksViaModel, eventsViaModel, fetchItems('transfer'), @@ -193,7 +195,12 @@ module.exports = ({ cooler, calendarsModel, eventsModel, tasksModel, marketModel fetchItems('report'), jobsViaModel, projectsViaModel, - calendarsViaModel + calendarsViaModel, + industryViaModel, + housingViaModel, + schoolViaModel, + campaignsViaModel, + logisticsViaModel ]); const tasks = tasksAll.filter(c => Array.isArray(c.assignees) && c.assignees.includes(userId)).map(t => ({ ...t, type: 'task' })); @@ -205,22 +212,36 @@ module.exports = ({ cooler, calendarsModel, eventsModel, tasksModel, marketModel ).map(m => ({ ...m, type: 'market' })); const reports = reportsAll.filter(c => c.author === userId || (Array.isArray(c.confirmations) && c.confirmations.includes(userId))).map(r => ({ ...r, type: 'report' })); const jobs = jobsAll.filter(c => c.author === userId || (Array.isArray(c.subscribers) && c.subscribers.includes(userId))).map(j => ({ ...j, type: 'job', title: j.title })); - const projects = projectsAll.map(p => ({ ...p, type: 'project' })); + const projects = projectsAll.filter(p => p.author === userId || (Array.isArray(p.followers) && p.followers.includes(userId)) || (Array.isArray(p.backers) && p.backers.some(b => b && b.userId === userId)) || (Array.isArray(p.bounties) && p.bounties.some(b => b && b.claimedBy === userId))).map(p => ({ ...p, type: 'project' })); + const industryBuilds = industryAll.map(b => ({ ...b, type: 'industry', title: b.title })); + const schoolCourses = schoolAll + .filter(c => c.author === userId || (Array.isArray(c.students) && c.students.includes(userId)) || (Array.isArray(c.pending) && c.pending.some(pn => pn.author === userId))) + .map(c => ({ ...c, type: 'schoolCourse', title: c.title, date: c.startDate || c.createdAt, teaching: c.author === userId })); + const campaigns = (Array.isArray(campaignsAll) ? campaignsAll : []) + .filter(c => c && c.deadline && (c.author === userId || (Array.isArray(c.signers) && c.signers.includes(userId)))) + .map(c => ({ ...c, type: 'campaign', date: c.deadline, status: c.closed ? 'CLOSED' : 'OPEN' })); + const routes = (Array.isArray(logisticsAll) ? logisticsAll : []) + .filter(r => r && r.date && (r.author === userId || !!r.myBooking)) + .map(r => ({ ...r, type: 'logisticsRoute', status: r.status === 'CLOSED' ? 'CLOSED' : 'OPEN' })); + const housingPlaces = housingAll + .filter(h => h.author === userId || (Array.isArray(h.requests) && h.requests.includes(userId))) + .map(h => ({ ...h, type: 'housing', date: h.availableFrom || h.createdAt, requested: Array.isArray(h.requests) && h.requests.includes(userId) })); const myCalendars = calendarsAll .filter(c => c.author === userId || (Array.isArray(c.participants) && c.participants.includes(userId))); - const calendars = myCalendars.map(c => ({ ...c, type: 'calendar' })); + const calendars = myCalendars.map(c => ({ ...c, id: c.rootId || c.id || c.key, type: 'calendar' })); const calendarDates = []; if (calendarsModel && typeof calendarsModel.getDatesForCalendar === 'function') { - for (const cal of myCalendars) { + const datesByCal = await Promise.all(myCalendars.map(cal => calendarsModel.getDatesForCalendar(cal.rootId || cal.id || cal.key).catch(() => []))); + for (const [ci, cal] of myCalendars.entries()) { try { - const dates = await calendarsModel.getDatesForCalendar(cal.id || cal.key); + const dates = datesByCal[ci]; for (const d of (dates || [])) { calendarDates.push({ type: 'calendarDate', id: d.key || d.id, title: d.label || cal.title || 'Calendar date', calendarTitle: cal.title || '', - calendarId: cal.id || cal.key, + calendarId: cal.rootId || cal.id || cal.key, date: d.date || d.createdAt, label: d.label || '', author: cal.author, @@ -241,9 +262,14 @@ module.exports = ({ cooler, calendarsModel, eventsModel, tasksModel, marketModel ...reports, ...jobs, ...projects, + ...industryBuilds, + ...housingPlaces, + ...schoolCourses, + ...campaigns, + ...routes, ...calendars, ...calendarDates - ]; + ].filter(i => !removedItems.includes(i.id)); let filtered; if (filter === 'discarded') { @@ -260,6 +286,11 @@ module.exports = ({ cooler, calendarsModel, eventsModel, tasksModel, marketModel else if (filter === 'closed') filtered = filtered.filter(i => String(i.status).toUpperCase() === 'CLOSED'); else if (filter === 'jobs') filtered = filtered.filter(i => i.type === 'job'); else if (filter === 'projects') filtered = filtered.filter(i => i.type === 'project'); + else if (filter === 'industry') filtered = filtered.filter(i => i.type === 'industry'); + else if (filter === 'housing') filtered = filtered.filter(i => i.type === 'housing'); + else if (filter === 'school') filtered = filtered.filter(i => i.type === 'schoolCourse'); + else if (filter === 'campaigns') filtered = filtered.filter(i => i.type === 'campaign'); + else if (filter === 'logistics') filtered = filtered.filter(i => i.type === 'logisticsRoute'); else if (filter === 'calendars') filtered = filtered.filter(i => i.type === 'calendar' || i.type === 'calendarDate'); else if (filter === 'today') { const startOfDay = new Date(); startOfDay.setHours(0, 0, 0, 0); @@ -313,6 +344,11 @@ module.exports = ({ cooler, calendarsModel, eventsModel, tasksModel, marketModel reports: mainItems.filter(i => i.type === 'report').length, jobs: mainItems.filter(i => i.type === 'job').length, projects: mainItems.filter(i => i.type === 'project').length, + industry: mainItems.filter(i => i.type === 'industry').length, + housing: mainItems.filter(i => i.type === 'housing').length, + school: mainItems.filter(i => i.type === 'schoolCourse').length, + campaigns: mainItems.filter(i => i.type === 'campaign').length, + logistics: mainItems.filter(i => i.type === 'logisticsRoute').length, calendars: mainItems.filter(i => i.type === 'calendar' || i.type === 'calendarDate').length, today: mainItems.filter(i => { const d = itemTs(i); return d >= startOfDay.getTime() && d <= endOfDay.getTime(); }).length, upcoming: mainItems.filter(i => itemTs(i) > now).length, @@ -334,6 +370,14 @@ module.exports = ({ cooler, calendarsModel, eventsModel, tasksModel, marketModel const agendaConfig = readAgendaConfig(); agendaConfig.discardedItems = agendaConfig.discardedItems.filter(id => id !== itemId); writeAgendaConfig(agendaConfig); + }, + + async removeItem(itemId) { + const agendaConfig = readAgendaConfig(); + agendaConfig.discardedItems = (agendaConfig.discardedItems || []).filter(id => id !== itemId); + agendaConfig.removedItems = agendaConfig.removedItems || []; + if (!agendaConfig.removedItems.includes(itemId)) agendaConfig.removedItems.push(itemId); + writeAgendaConfig(agendaConfig); } }; }; diff --git a/src/models/audios_model.js b/src/models/audios_model.js index 13f66d6..cf65107 100644 --- a/src/models/audios_model.js +++ b/src/models/audios_model.js @@ -1,6 +1,7 @@ const pull = require("../server/node_modules/pull-stream"); const { buildValidatedTombstoneSet } = require('./tombstone_validator'); -const { dedupeBy, norm } = require('../backend/dedupe'); +const { dedupeBy, norm, versionGroups } = require('../backend/dedupe'); +const { readTyped, memoIndex } = require('./typed_log'); const { getConfig } = require("../configs/config-manager.js"); const categories = require("../backend/opinion_categories"); @@ -31,10 +32,9 @@ module.exports = ({ cooler }) => { return ssb; }; - const getAllMessages = async (ssbClient) => - new Promise((resolve, reject) => { - pull(ssbClient.createLogStream({ limit: logLimit }), pull.collect((err, msgs) => (err ? reject(err) : resolve(msgs)))); - }); + const AUDIO_TYPES = ["audio", "audioOpinion", "tombstone"]; + + const getAllMessages = async (ssbClient) => readTyped(ssbClient, AUDIO_TYPES, { limit: logLimit }); const getMsg = async (ssbClient, key) => new Promise((resolve, reject) => { @@ -44,9 +44,6 @@ module.exports = ({ cooler }) => { const buildIndex = (messages) => { const tomb = buildValidatedTombstoneSet(messages); const nodes = new Map(); - const parent = new Map(); - const child = new Map(); - const strictChild = new Map(); const opinionMsgs = []; for (const m of messages) { @@ -62,41 +59,16 @@ module.exports = ({ cooler }) => { const ts = v.timestamp || m.timestamp || 0; let sizeBytes = 0; try { sizeBytes = Buffer.byteLength(JSON.stringify(v), 'utf8'); } catch (_) { sizeBytes = 0; } - nodes.set(k, { key: k, ts, c, sizeBytes, author: v.author }); - - if (c.replaces) { - parent.set(k, c.replaces); - child.set(c.replaces, k); - } + nodes.set(k, { key: k, ts, seq: v.sequence || 0, c, sizeBytes, author: v.author }); } - for (const [k, node] of nodes) { - const t = node.c.replaces; - if (t) { const orig = nodes.get(t); if (orig && orig.author === node.author) strictChild.set(t, k); } - } - - const rootOf = (id) => { - let cur = id, g = 0; - while (parent.has(cur) && nodes.has(parent.get(cur)) && g++ < 100000) cur = parent.get(cur); - return cur; - }; - - const contentTipOf = (root) => { - let cur = root, g = 0; - while (strictChild.has(cur) && g++ < 100000) cur = strictChild.get(cur); - const n = nodes.get(cur), rn = nodes.get(root); - return (n && rn && n.author === rn.author) ? cur : root; - }; - - const roots = new Set(); - for (const id of nodes.keys()) roots.add(rootOf(id)); + const vg = versionGroups([...nodes.values()].map(n => ({ key: n.key, author: n.author, seq: n.seq, ts: n.ts, replaces: n.c.replaces || null, claimedAuthor: typeof n.c.author === "string" ? n.c.author : null }))); + const rootOf = vg.rootOf; + const contentTipOf = vg.tipOf; const opinionsByRoot = new Map(); for (const op of opinionMsgs) { if (!nodes.has(op.target)) continue; const r = rootOf(op.target); if (!opinionsByRoot.has(r)) opinionsByRoot.set(r, []); opinionsByRoot.get(r).push(op); } - const forward = new Map(); - for (const [newId, oldId] of parent.entries()) forward.set(oldId, newId); - const resolveGroup = (root) => { const contentTip = contentTipOf(root); const contentNode = nodes.get(contentTip) || nodes.get(root); @@ -113,9 +85,9 @@ module.exports = ({ cooler }) => { }; const tipByRoot = new Map(); - for (const r of roots) tipByRoot.set(r, contentTipOf(r)); + for (const r of vg.roots) tipByRoot.set(r, contentTipOf(r)); - return { tomb, nodes, parent, child, rootOf, tipByRoot, forward, resolveGroup }; + return { tomb, nodes, rootOf, tipByRoot, resolveGroup }; }; const buildAudio = (node, rootId, viewerId, agg) => { @@ -132,9 +104,10 @@ module.exports = ({ cooler }) => { createdAt: c.createdAt || new Date(node.ts).toISOString(), updatedAt: c.updatedAt || null, tags: tagsArr, - author: c.author, + author: node.author, title: c.title || "", description: c.description || "", + license: c.license || "", mapUrl: c.mapUrl || "", opinions, opinions_inhabitants: voters, @@ -163,16 +136,12 @@ module.exports = ({ cooler }) => { const messages = await getAllMessages(ssbClient); const idx = buildIndex(messages); - let tip = id; - while (idx.forward.has(tip)) tip = idx.forward.get(tip); - if (idx.tomb.has(tip)) throw new Error("Audio not found"); - - let root = tip; - while (idx.parent.has(root)) root = idx.parent.get(root); + const root = idx.rootOf(id); + if (idx.tomb.has(idx.tipByRoot.get(root) || id)) throw new Error("Audio not found"); return root; }, - async createAudio(blobMarkdown, tagsRaw, title, description, mapUrl) { + async createAudio(blobMarkdown, tagsRaw, title, description, mapUrl, license) { const ssbClient = await openSsb(); const blobId = parseBlobId(blobMarkdown); const tags = normalizeTags(tagsRaw) || []; @@ -189,7 +158,8 @@ module.exports = ({ cooler }) => { description: description || "", mapUrl: mapUrl || "", opinions: {}, - opinions_inhabitants: [] + opinions_inhabitants: [], + ...(license ? { license } : {}) }; return new Promise((resolve, reject) => { @@ -251,7 +221,7 @@ module.exports = ({ cooler }) => { }); }, - async updateAudioById(id, blobMarkdown, tagsRaw, title, description, mapUrl) { + async updateAudioById(id, blobMarkdown, tagsRaw, title, description, mapUrl, license) { const ssbClient = await openSsb(); const userId = ssbClient.id; const tipId = await this.resolveCurrentId(id); @@ -275,7 +245,8 @@ module.exports = ({ cooler }) => { description: description !== undefined ? description || "" : oldMsg.content.description || "", mapUrl: mapUrl !== undefined ? mapUrl || "" : oldMsg.content.mapUrl || "", createdAt: oldMsg.content.createdAt, - updatedAt: now + updatedAt: now, + ...(license !== undefined ? { license } : {}) }; const tombstone = { type: "tombstone", target: tipId, deletedAt: now, author: userId }; @@ -358,11 +329,11 @@ module.exports = ({ cooler }) => { const ssbClient = await openSsb(); const viewer = viewerId || ssbClient.id; const messages = await getAllMessages(ssbClient); - const idx = buildIndex(messages); + const idx = memoIndex('audios', messages, buildIndex); const rootId = idx.rootOf(id); const agg = idx.resolveGroup(rootId); - if (!agg.contentNode || idx.tomb.has(agg.contentTip)) throw new Error("Audio not found"); + if (!idx.tipByRoot.has(rootId) || !agg.contentNode || idx.tomb.has(agg.contentTip)) throw new Error("Audio not found"); return buildAudio(agg.contentNode, rootId, viewer, agg); }, diff --git a/src/models/backup_model.js b/src/models/backup_model.js new file mode 100644 index 0000000..175e028 --- /dev/null +++ b/src/models/backup_model.js @@ -0,0 +1,779 @@ +const fs = require('fs'); +const os = require('os'); +const path = require('path'); +const zlib = require('zlib'); +const crypto = require('crypto'); +const { pipeline } = require('stream'); +const pull = require('../server/node_modules/pull-stream'); +const toPull = require('../server/node_modules/stream-to-pull-stream'); + +const KEY_MAGIC = Buffer.from('OASIS1'); +const BACKUP_MAGIC = Buffer.from('OASISBK1'); +const SNAPSHOT_MAGIC = Buffer.from('OASISSN1'); +const SNAPSHOT_MAX_BYTES = 2 * 1024 * 1024 * 1024; +const SALT_LEN = 16; +const IV_LEN = 12; +const TAG_LEN = 16; +const REC_META = 1; +const REC_MSG = 2; +const REC_BLOB = 3; +const REC_FILE = 4; +const REC_MAX_LEN = 256 * 1024 * 1024; + +const stateRoot = () => { + try { return path.join(require('../configs/state-manager').ssbDir(), 'oasis'); } catch (_) { return null; } +}; + +const NOT_BACKED_UP = new Set(['oasis-config.json', 'oasis-server-config.json', 'rooms']); +const listStateFiles = (dir, base = dir) => { + const out = []; + let entries = []; + try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (_) { return out; } + for (const entry of entries) { + if (dir === base && NOT_BACKED_UP.has(entry.name)) continue; + const full = path.join(dir, entry.name); + if (entry.isDirectory()) out.push(...listStateFiles(full, base)); + else if (entry.isFile() && !entry.name.endsWith('.migrated') && !/^snapshot(-recent)?\.oasissn/.test(entry.name)) out.push({ rel: path.relative(base, full), full }); + } + return out; +}; +const BLOB_RE = /&[A-Za-z0-9+/=]{44}\.sha256/g; +const DEFAULT_REMINDER_DAYS = 30; +const REMINDER_OPTIONS = [7, 30, 90, 0]; + +const MODULE_GROUPS = [ + { key: 'feed', test: (t) => /^(post|feed|about|contact|pub|vote|subscription)$/.test(t) }, + { key: 'media', test: (t) => /^(audio|video|image|document|bookmark|torrent|file)/i.test(t) }, + { key: 'blogs', test: (t) => /^blog/i.test(t) }, + { key: 'forum', test: (t) => /^forum/i.test(t) }, + { key: 'chats', test: (t) => /^chat/i.test(t) }, + { key: 'pads', test: (t) => /^pad/i.test(t) }, + { key: 'rooms', test: (t) => /^room/i.test(t) }, + { key: 'wiki', test: (t) => /^wiki/i.test(t) }, + { key: 'maps', test: (t) => /^map/i.test(t) }, + { key: 'tribes', test: (t) => /^tribe/i.test(t) }, + { key: 'school', test: (t) => /^school/i.test(t) }, + { key: 'parliament', test: (t) => /^parliament/i.test(t) }, + { key: 'courts', test: (t) => /^courts/i.test(t) }, + { key: 'emergencies', test: (t) => /^emergency/i.test(t) }, + { key: 'votes', test: (t) => /^(votes?|poll)/i.test(t) }, + { key: 'events', test: (t) => /^event/i.test(t) }, + { key: 'calendars', test: (t) => /^calendar/i.test(t) }, + { key: 'tasks', test: (t) => /^task/i.test(t) }, + { key: 'reports', test: (t) => /^report/i.test(t) }, + { key: 'mailing', test: (t) => /^mailing/i.test(t) }, + { key: 'campaigns', test: (t) => /^campaign/i.test(t) }, + { key: 'market', test: (t) => /^market/i.test(t) }, + { key: 'shops', test: (t) => /^shop/i.test(t) }, + { key: 'jobs', test: (t) => /^(job|curriculum)/i.test(t) }, + { key: 'housing', test: (t) => /^housing/i.test(t) }, + { key: 'projects', test: (t) => /^project/i.test(t) }, + { key: 'industry', test: (t) => /^industry/i.test(t) }, + { key: 'banking', test: (t) => /^(bank|ubi|wallet)/i.test(t) }, + { key: 'transfers', test: (t) => /^transfer/i.test(t) }, + { key: 'logistics', test: (t) => /^logistics/i.test(t) }, + { key: 'podcasts', test: (t) => /^podcast/i.test(t) }, + { key: 'games', test: (t) => /^(game|larp|pixelia|melody)/i.test(t) }, + { key: 'logs', test: (t) => /^log/i.test(t) } +]; +const MODULE_KEYS = [...MODULE_GROUPS.map(g => g.key), 'inbox', 'other']; + +const moduleOf = (content) => { + if (typeof content === 'string') return 'inbox'; + const t = content && typeof content === 'object' ? String(content.type || '') : ''; + if (!t) return 'other'; + const g = MODULE_GROUPS.find(x => x.test(t)); + return g ? g.key : 'other'; +}; + +const normalizePassword = (password) => { + if (password && typeof password === 'object' && password.password) password = password.password; + return String(password || ''); +}; +const deriveKey = (password, salt) => crypto.scryptSync(password, salt, 32); + +const encryptBuffer = (plaintext, password) => { + const salt = crypto.randomBytes(SALT_LEN); + const iv = crypto.randomBytes(IV_LEN); + const cipher = crypto.createCipheriv('aes-256-gcm', deriveKey(password, salt), iv); + const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]); + return Buffer.concat([KEY_MAGIC, salt, iv, cipher.getAuthTag(), ciphertext]); +}; + +const decryptBuffer = (data, password) => { + if (!Buffer.isBuffer(data) || data.length < KEY_MAGIC.length + SALT_LEN + IV_LEN + TAG_LEN) throw new Error('Unrecognized or corrupt backup file.'); + if (!data.slice(0, KEY_MAGIC.length).equals(KEY_MAGIC)) throw new Error('Unrecognized or corrupt backup file.'); + let o = KEY_MAGIC.length; + const salt = data.slice(o, o += SALT_LEN); + const iv = data.slice(o, o += IV_LEN); + const tag = data.slice(o, o += TAG_LEN); + const decipher = crypto.createDecipheriv('aes-256-gcm', deriveKey(password, salt), iv); + decipher.setAuthTag(tag); + return Buffer.concat([decipher.update(data.slice(o)), decipher.final()]); +}; + +const ssbDirOf = () => require('../server/ssb_config').path; +const secretPathOf = () => path.join(ssbDirOf(), 'secret'); + +const stateFile = () => require('../configs/state-manager').statePath('oasis-backup.json'); +const readState = () => { + try { return JSON.parse(fs.readFileSync(stateFile(), 'utf8')) || {}; } catch (_) { return {}; } +}; +const writeState = (next) => { + try { + const p = stateFile(); + fs.mkdirSync(path.dirname(p), { recursive: true }); + fs.writeFileSync(p, JSON.stringify(next, null, 2), { mode: 0o600 }); + } catch (_) {} + return next; +}; + + +const reminderState = () => { + const b = readState(); + const days = b.reminderDays === undefined ? DEFAULT_REMINDER_DAYS : Number(b.reminderDays); + const lastAt = b.lastAt || null; + const lastTs = lastAt ? Date.parse(lastAt) || 0 : 0; + const overdue = days > 0 && (!lastTs || Date.now() - lastTs > days * 86400000); + return { lastAt, reminderDays: days, overdue, daysSince: lastTs ? Math.floor((Date.now() - lastTs) / 86400000) : null }; +}; +const setReminderDays = (days) => { + const n = Number(days); + return writeState({ ...readState(), reminderDays: REMINDER_OPTIONS.includes(n) ? n : DEFAULT_REMINDER_DAYS }); +}; +const markBackup = () => writeState({ ...readState(), lastAt: new Date().toISOString() }); + +const blobRefsOf = (value) => { + let json; + try { json = JSON.stringify(value); } catch (_) { return []; } + return Array.from(new Set(json.match(BLOB_RE) || [])); +}; + +const record = (type, payload) => { + const head = Buffer.alloc(5); + head.writeUInt8(type, 0); + head.writeUInt32BE(payload.length, 1); + return Buffer.concat([head, payload]); +}; + +const writeChunk = (stream, chunk) => new Promise((resolve, reject) => { + const ok = stream.write(chunk, (err) => err ? reject(err) : null); + if (ok) resolve(); + else stream.once('drain', resolve); +}); + +module.exports = ({ cooler }) => { + let ssb; + let restoreJob = null; + const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb; }; + + const readLog = async (ssbClient) => new Promise((resolve, reject) => { + pull(ssbClient.createLogStream({ reverse: false }), pull.collect((err, arr) => err ? reject(err) : resolve(arr || []))); + }); + + const listBlobs = async (ssbClient) => { + if (!ssbClient.blobs || typeof ssbClient.blobs.ls !== 'function') return new Map(); + return new Promise((resolve) => { + pull(ssbClient.blobs.ls({ long: true }), pull.collect((err, arr) => { + if (err) return resolve(new Map()); + const out = new Map(); + for (const b of arr || []) { + if (!b) continue; + if (typeof b === 'string') out.set(b, 0); + else if (b.id) out.set(b.id, Number(b.size) || 0); + } + resolve(out); + })); + }); + }; + + const readBlob = (ssbClient, id) => new Promise((resolve) => { + pull(ssbClient.blobs.get(id), pull.collect((e, chunks) => { + if (e || !chunks || !chunks.length) return resolve(null); + resolve(Buffer.concat(chunks.map(c => Buffer.isBuffer(c) ? c : Buffer.from(c)))); + })); + }); + + const normalizeOptions = (opts = {}) => { + const scope = String(opts.scope || 'all').toLowerCase() === 'mine' ? 'mine' : 'all'; + const withBlobs = opts.blobs === undefined ? true : !(opts.blobs === false || String(opts.blobs) === '0' || String(opts.blobs) === 'false' || String(opts.blobs) === 'off'); + const rawMods = Array.isArray(opts.modules) ? opts.modules : (opts.modules ? String(opts.modules).split(',') : []); + const modules = rawMods.map(m => String(m || '').trim().toLowerCase()).filter(m => MODULE_KEYS.includes(m)); + const sinceTs = opts.since ? (Date.parse(opts.since) || 0) : 0; + return { scope, withBlobs, modules, sinceTs }; + }; + + const selectMessages = async (ssbClient, options) => { + const me = ssbClient.id; + const all = await readLog(ssbClient); + const picked = []; + const blobIds = new Set(); + for (const m of all) { + if (!m || !m.value) continue; + if (options.scope === 'mine' && m.value.author !== me) continue; + if (options.sinceTs && Number(m.value.timestamp || m.timestamp || 0) < options.sinceTs) continue; + if (options.modules.length && !options.modules.includes(moduleOf(m.value.content))) continue; + picked.push(m); + if (options.withBlobs) for (const ref of blobRefsOf(m.value.content)) blobIds.add(ref); + } + if (options.withBlobs && options.scope !== 'mine' && !options.sinceTs && !options.modules.length) { + const sizes = await listBlobs(ssbClient); + for (const id of sizes.keys()) blobIds.add(id); + } + return { picked, blobIds }; + }; + + return { + MODULE_KEYS, + REMINDER_OPTIONS, + moduleOf, + encryptBuffer, + decryptBuffer, + + async exportKeys(password) { + const pw = normalizePassword(password); + if (pw.length < 32) throw new Error('Password too short'); + const secretPath = secretPathOf(); + if (!fs.existsSync(secretPath)) throw new Error(".ssb/secret file doesn't exist"); + const original = fs.readFileSync(secretPath); + const encrypted = encryptBuffer(original, pw); + if (!decryptBuffer(encrypted, pw).equals(original)) throw new Error('Backup verification failed; nothing was exported.'); + return { filename: 'oasis.enc', data: encrypted }; + }, + + async importKeys({ filePath, password }) { + const pw = normalizePassword(password); + if (!fs.existsSync(filePath)) throw new Error('Encrypted file not found.'); + let decrypted; + try { decrypted = decryptBuffer(fs.readFileSync(filePath), pw); } catch (_) { throw new Error('Wrong password or corrupt backup file.'); } + let keys = null; + try { keys = JSON.parse(decrypted.toString('utf8').split('\n').filter(l => !l.trim().startsWith('#')).join('\n')); } catch (_) { keys = null; } + const ssbKeys = require('../server/node_modules/ssb-keys'); + const probe = Buffer.from('oasis-keys-check'); + const valid = (() => { + try { + if (!keys || keys.curve !== 'ed25519' || typeof keys.public !== 'string' || typeof keys.private !== 'string' || keys.id !== '@' + keys.public) return false; + return ssbKeys.verify(keys, ssbKeys.sign(keys, probe), probe) === true; + } catch (_) { return false; } + })(); + if (!valid) throw new Error('The file does not contain a valid Oasis identity.'); + const ssbDir = ssbDirOf(); + fs.mkdirSync(ssbDir, { recursive: true }); + const secretPath = path.join(ssbDir, 'secret'); + if (fs.existsSync(secretPath)) { + const bakPath = path.join(ssbDir, 'secret.bak-' + new Date().toISOString().replace(/[:.]/g, '-')); + fs.copyFileSync(secretPath, bakPath); + try { fs.chmodSync(bakPath, 0o600); } catch (_) {} + } + const tmpPath = path.join(ssbDir, 'secret.tmp-' + process.pid); + fs.writeFileSync(tmpPath, decrypted, { mode: 0o600 }); + fs.renameSync(tmpPath, secretPath); + try { fs.unlinkSync(filePath); } catch (_) {} + let id = ''; + try { id = JSON.parse(decrypted.toString('utf8').split('\n').filter(l => !l.trim().startsWith('#')).join('\n')).id || ''; } catch (_) {} + return { path: secretPath, id }; + }, + + recoveryKit() { + const secretPath = secretPathOf(); + if (!fs.existsSync(secretPath)) throw new Error(".ssb/secret file doesn't exist"); + const raw = fs.readFileSync(secretPath, 'utf8'); + const clean = raw.split('\n').filter(l => !l.trim().startsWith('#')).join('\n').trim(); + let id = ''; + try { id = JSON.parse(clean).id || ''; } catch (_) {} + return { secret: clean, id, createdAt: new Date().toISOString() }; + }, + + async estimate(opts = {}) { + const ssbClient = await openSsb(); + const options = normalizeOptions(opts); + const { picked, blobIds } = await selectMessages(ssbClient, options); + let bytes = 0; + for (const m of picked) bytes += Buffer.byteLength(JSON.stringify({ key: m.key, value: m.value, timestamp: m.timestamp }), 'utf8') + 5; + let blobBytes = 0; + let blobsFound = 0; + if (options.withBlobs && blobIds.size) { + const sizes = await listBlobs(ssbClient); + for (const id of blobIds) { + if (!sizes.has(id)) continue; + blobsFound += 1; + blobBytes += sizes.get(id) || 0; + } + } + return { options, messages: picked.length, blobs: blobsFound, blobsReferenced: blobIds.size, messageBytes: bytes, blobBytes, bytes: bytes + blobBytes }; + }, + + async createBackup(opts = {}, password, outPath) { + const pw = normalizePassword(password); + if (pw.length < 32) throw new Error('Password too short'); + const ssbClient = await openSsb(); + const options = normalizeOptions(opts); + const { picked, blobIds } = await selectMessages(ssbClient, options); + const salt = crypto.randomBytes(SALT_LEN); + const iv = crypto.randomBytes(IV_LEN); + const cipher = crypto.createCipheriv('aes-256-gcm', deriveKey(pw, salt), iv); + const gzip = zlib.createGzip(); + const out = fs.createWriteStream(outPath); + const done = new Promise((resolve, reject) => pipeline(gzip, cipher, out, (err) => err ? reject(err) : resolve())); + out.write(Buffer.concat([BACKUP_MAGIC, salt, iv])); + const meta = { version: 1, createdAt: new Date().toISOString(), author: ssbClient.id, options, messages: picked.length }; + await writeChunk(gzip, record(REC_META, Buffer.from(JSON.stringify(meta), 'utf8'))); + for (const m of picked) { + await writeChunk(gzip, record(REC_MSG, Buffer.from(JSON.stringify({ key: m.key, value: m.value, timestamp: m.timestamp }), 'utf8'))); + } + let files = 0; + const root = stateRoot(); + if (root) { + for (const f of listStateFiles(root)) { + let data = null; + try { data = fs.readFileSync(f.full); } catch (_) { continue; } + const head = Buffer.from(JSON.stringify({ rel: f.rel.split(path.sep).join('/'), size: data.length }), 'utf8'); + const lenBuf = Buffer.alloc(4); + lenBuf.writeUInt32BE(head.length, 0); + await writeChunk(gzip, record(REC_FILE, Buffer.concat([lenBuf, head, data]))); + files += 1; + } + } + let blobs = 0; + if (options.withBlobs) { + for (const id of blobIds) { + const data = await readBlob(ssbClient, id); + if (!data) continue; + const head = Buffer.from(JSON.stringify({ id, size: data.length }), 'utf8'); + const lenBuf = Buffer.alloc(4); + lenBuf.writeUInt32BE(head.length, 0); + await writeChunk(gzip, record(REC_BLOB, Buffer.concat([lenBuf, head, data]))); + blobs += 1; + } + } + gzip.end(); + await done; + fs.appendFileSync(outPath, cipher.getAuthTag()); + markBackup(); + return { path: outPath, messages: picked.length, blobs, bytes: fs.statSync(outPath).size, filename: `oasis-backup-${new Date().toISOString().slice(0, 10)}.oasisbk` }; + }, + + async createSnapshot(outPath, { sinceMs = 0, now = Date.now() } = {}) { + const ssbClient = await openSsb(); + const all = await readLog(ssbClient); + const byAuthor = new Map(); + for (const m of all) { + if (!m || !m.value || !m.value.author || m.value.content === undefined || m.value.content === null) continue; + const list = byAuthor.get(m.value.author) || []; + list.push(m); + byAuthor.set(m.value.author, list); + } + const latest = (list) => list.reduce((t, m) => Math.max(t, Number(m.timestamp) || 0, Number(m.value.timestamp) || 0), 0); + const picked = [...byAuthor.values()] + .filter(list => !(sinceMs > 0) || latest(list) >= now - sinceMs) + .sort((a, b) => latest(b) - latest(a)) + .flatMap(list => list.sort((a, b) => (a.value.sequence || 0) - (b.value.sequence || 0))); + const gzip = zlib.createGzip(); + const out = fs.createWriteStream(outPath); + const done = new Promise((resolve, reject) => pipeline(gzip, out, (err) => err ? reject(err) : resolve())); + out.write(SNAPSHOT_MAGIC); + const meta = { version: 1, kind: 'snapshot', createdAt: new Date().toISOString(), author: ssbClient.id, messages: picked.length, feeds: new Set(picked.map(m => m.value.author)).size, boxed: picked.filter(m => typeof m.value.content === 'string').length, sinceMs: sinceMs > 0 ? sinceMs : null }; + await writeChunk(gzip, record(REC_META, Buffer.from(JSON.stringify(meta), 'utf8'))); + for (const m of picked) { + await writeChunk(gzip, record(REC_MSG, Buffer.from(JSON.stringify({ key: m.key, value: m.value, timestamp: m.timestamp }), 'utf8'))); + } + gzip.end(); + await done; + return { path: outPath, messages: picked.length, feeds: meta.feeds, bytes: fs.statSync(outPath).size, createdAt: meta.createdAt }; + }, + + isSnapshotFile(filePath) { + try { + const fd = fs.openSync(filePath, 'r'); + const head = Buffer.alloc(SNAPSHOT_MAGIC.length); + fs.readSync(fd, head, 0, head.length, 0); + fs.closeSync(fd); + return head.equals(SNAPSHOT_MAGIC); + } catch (_) { return false; } + }, + + async fetchSnapshot({ address, connect = null, tier = 'full' }, outPath) { + const ssbClient = await openSsb(); + const doConnect = connect || ((addr, cb) => ssbClient.conn.connect(addr, cb)); + const discard = () => { try { fs.unlinkSync(outPath); } catch (_) {} return null; }; + try { + const rpc = await new Promise((resolve, reject) => doConnect(address, (err, r) => err ? reject(err) : resolve(r))); + if (!rpc || !rpc.snapshot || typeof rpc.snapshot.get !== 'function') return null; + const info = await new Promise((resolve) => rpc.snapshot.info({ tier }, (err, i) => resolve(err ? null : i))); + if (!info || !(info.bytes > 0) || info.bytes > SNAPSHOT_MAX_BYTES) return null; + const out = fs.createWriteStream(outPath); + let written = 0; + await new Promise((resolve, reject) => { + pull( + rpc.snapshot.get({ tier }), + pull.map(c => Buffer.isBuffer(c) ? c : Buffer.from(c)), + pull.through(c => { written += c.length; }), + toPull.sink(out, (err) => err ? reject(err) : resolve()) + ); + }); + if (written > SNAPSHOT_MAX_BYTES || !this.isSnapshotFile(outPath)) return discard(); + return { address, tier, path: outPath, bytes: written, createdAt: info.createdAt }; + } catch (_) { return discard(); } + }, + + startBootstrap({ address, connect = null, tiers = ['recent', 'full'] }) { + if (restoreJob && restoreJob.running) return restoreJob; + const empty = () => ({ messages: 0, skipped: 0, failed: 0, forked: 0, blobs: 0, blobsSkipped: 0, percent: 0 }); + const job = { running: true, source: 'snapshot', phase: null, tier: null, tiers: [], address, startedAt: new Date().toISOString(), finishedAt: null, progress: empty(), result: null, error: null }; + restoreJob = job; + const total = { ...empty(), files: 0, forks: [], errors: [], meta: null }; + const run = async () => { + for (const tier of tiers) { + const tmp = path.join(os.tmpdir(), `oasis-snapshot-${tier}-${Date.now()}-${Math.random().toString(36).slice(2)}.oasissn`); + job.tier = tier; + job.phase = 'download'; + try { + const got = await this.fetchSnapshot({ address, connect, tier }, tmp); + if (!got) continue; + job.phase = 'restore'; + const res = await this.restoreBackup({ filePath: tmp, password: '', stateFiles: false, onProgress: (p) => { job.progress = { ...total, ...p, messages: total.messages + p.messages, skipped: total.skipped + p.skipped, failed: total.failed + p.failed, forked: total.forked + p.forked }; } }); + for (const k of ['messages', 'skipped', 'failed', 'forked', 'blobs', 'blobsSkipped', 'files']) total[k] += Number(res[k]) || 0; + total.forks.push(...(res.forks || [])); + total.errors.push(...(res.errors || [])); + total.meta = res.meta || total.meta; + job.tiers.push({ tier, messages: res.messages, skipped: res.skipped, bytes: got.bytes }); + } finally { try { fs.unlinkSync(tmp); } catch (_) {} } + } + if (!job.tiers.length) throw new Error('snapshot not available'); + total.percent = 100; + return total; + }; + job.promise = run() + .then((res) => { job.result = res; job.progress = { ...job.progress, ...res }; return res; }) + .catch((err) => { job.error = err && err.message ? err.message : String(err); return null; }) + .finally(() => { job.running = false; job.phase = null; job.finishedAt = new Date().toISOString(); }); + return job; + }, + + async readBackup(filePath, password, onRecord) { + const size = fs.statSync(filePath).size; + const snapshot = this.isSnapshotFile(filePath); + const pw = snapshot ? '' : normalizePassword(password); + const headLen = snapshot ? SNAPSHOT_MAGIC.length : BACKUP_MAGIC.length + SALT_LEN + IV_LEN; + const tailLen = snapshot ? 0 : TAG_LEN; + if (size < headLen + tailLen) throw new Error('Unrecognized or corrupt backup file.'); + const fd = fs.openSync(filePath, 'r'); + const head = Buffer.alloc(headLen); + fs.readSync(fd, head, 0, headLen, 0); + const tag = Buffer.alloc(TAG_LEN); + if (!snapshot) fs.readSync(fd, tag, 0, TAG_LEN, size - TAG_LEN); + fs.closeSync(fd); + if (!snapshot && !head.slice(0, BACKUP_MAGIC.length).equals(BACKUP_MAGIC)) throw new Error('Unrecognized or corrupt backup file.'); + let decipher = null; + if (!snapshot) { + const salt = head.slice(BACKUP_MAGIC.length, BACKUP_MAGIC.length + SALT_LEN); + const iv = head.slice(BACKUP_MAGIC.length + SALT_LEN, headLen); + decipher = crypto.createDecipheriv('aes-256-gcm', deriveKey(pw, salt), iv); + decipher.setAuthTag(tag); + } + const gunzip = zlib.createGunzip(); + const total = size - headLen - tailLen; + const source = fs.createReadStream(filePath, { start: headLen, end: size - tailLen - 1 }); + let streamError = null; + if (decipher) pipeline(source, decipher, gunzip, (err) => { if (err) streamError = err; }); + else pipeline(source, gunzip, (err) => { if (err) streamError = err; }); + const chunks = []; + let pendingLen = 0; + let meta = null; + const compact = () => { + if (chunks.length < 2) return; + const all = Buffer.concat(chunks, pendingLen); + chunks.length = 0; + chunks.push(all); + }; + const nextRecord = () => { + if (pendingLen < 5) return null; + if (chunks[0].length < 5) compact(); + const type = chunks[0].readUInt8(0); + const len = chunks[0].readUInt32BE(1); + if (len > REC_MAX_LEN) throw new Error('Backup record too large.'); + if (pendingLen < 5 + len) return null; + compact(); + const all = chunks[0]; + chunks.length = 0; + pendingLen = all.length - 5 - len; + if (pendingLen) chunks.push(all.slice(5 + len)); + return { type, payload: all.slice(5, 5 + len) }; + }; + const handle = async (type, payload) => { + const read = Math.min(total, source.bytesRead || 0); + if (type === REC_META) { meta = JSON.parse(payload.toString('utf8')); await onRecord({ type: 'meta', meta, read, total }); } + else if (type === REC_MSG) await onRecord({ type: 'msg', msg: JSON.parse(payload.toString('utf8')), read, total }); + else if (type === REC_BLOB) { + const hl = payload.readUInt32BE(0); + const header = JSON.parse(payload.slice(4, 4 + hl).toString('utf8')); + await onRecord({ type: 'blob', id: header.id, data: payload.slice(4 + hl), read, total }); + } + else if (type === REC_FILE) { + const hl = payload.readUInt32BE(0); + const header = JSON.parse(payload.slice(4, 4 + hl).toString('utf8')); + await onRecord({ type: 'file', rel: header.rel, data: payload.slice(4 + hl), read, total }); + } + }; + try { + for await (const chunk of gunzip) { + chunks.push(chunk); + pendingLen += chunk.length; + let rec; + while ((rec = nextRecord())) await handle(rec.type, rec.payload); + } + } catch (e) { + source.destroy(); + if (streamError || /unsupported state|auth|bad decrypt|incorrect header|invalid/i.test(String(e && e.message))) throw new Error('Wrong password or corrupt backup file.'); + throw e; + } + if (streamError) throw new Error('Wrong password or corrupt backup file.'); + return meta; + }, + + async restoreBackup({ filePath, password, onProgress, stateFiles = true }) { + const ssbClient = await openSsb(); + let validate = null; + try { validate = require('../server/node_modules/ssb-validate'); } catch (_) {} + const seqKey = (v) => `${v.author}::${v.sequence}`; + const existing = new Map(); + for (const m of await readLog(ssbClient)) if (m && m.value) existing.set(seqKey(m.value), m.key); + const stats = { messages: 0, skipped: 0, failed: 0, forked: 0, blobs: 0, blobsSkipped: 0, files: 0, percent: 0, forks: [], errors: [], meta: null }; + const forkAuthors = new Map(); + const errorCounts = new Map(); + const noteError = (err) => { + const reason = String(err && err.message ? err.message : err || 'unknown error').replace(/, on feed:@[^\s]+/g, '').replace(/in state:\{.*?\}/g, '').trim(); + errorCounts.set(reason, (errorCounts.get(reason) || 0) + 1); + }; + const addMsg = (value) => new Promise((resolve) => { + if (typeof ssbClient.add !== 'function') return resolve(new Error('add is not available')); + ssbClient.add(value, (err) => resolve(err || null)); + }); + const addBlob = (data) => new Promise((resolve) => { + pull(pull.values([data]), ssbClient.blobs.add((err, ref) => resolve(err ? null : ref))); + }); + const getByKey = (key) => new Promise((resolve) => { + if (!key || typeof ssbClient.get !== 'function') return resolve(false); + try { ssbClient.get(key, (err, value) => resolve(!err && !!value)); } catch (_) { resolve(false); } + }); + const storedByKey = async (msg) => { + if (await getByKey(msg.key)) return true; + let computed = null; + try { computed = validate ? validate.id(msg.value) : null; } catch (_) { computed = null; } + return computed && computed !== msg.key ? getByKey(computed) : false; + }; + const supportsLs = !!(ssbClient.blobs && typeof ssbClient.blobs.ls === 'function'); + const present = supportsLs ? await listBlobs(ssbClient) : new Map(); + const hasBlob = (id) => supportsLs ? Promise.resolve(present.has(id)) : new Promise((resolve) => ssbClient.blobs.has(id, (err, has) => resolve(!err && !!has))); + const deferred = []; + const report = () => { if (typeof onProgress === 'function') onProgress({ ...stats }); }; + const sameMessage = (storedKey, msg) => { + if (storedKey === msg.key) return true; + try { return storedKey === validate.id(msg.value); } catch (_) { return false; } + }; + const tryAdd = async (msg) => { + const k = seqKey(msg.value); + if (existing.has(k)) { + if (sameMessage(existing.get(k), msg)) { stats.skipped += 1; return true; } + stats.forked += 1; + forkAuthors.set(msg.value.author, (forkAuthors.get(msg.value.author) || 0) + 1); + return true; + } + const err = await addMsg(msg.value); + if (err) { + if (!(await storedByKey(msg))) return err; + stats.skipped += 1; + existing.set(k, msg.key); + return true; + } + stats.messages += 1; + existing.set(k, msg.key); + return true; + }; + const meta = await this.readBackup(filePath, password, async (rec) => { + if (rec.total) stats.percent = Math.min(100, (rec.read / rec.total) * 100); + if (rec.type === 'meta') { stats.meta = rec.meta; return; } + if (rec.type === 'msg') { + if (!rec.msg || !rec.msg.value || !rec.msg.value.author) { stats.failed += 1; noteError('malformed message'); report(); return; } + const res = await tryAdd(rec.msg); + if (res !== true) deferred.push({ msg: rec.msg, err: res }); + report(); + return; + } + if (rec.type === 'file') { + if (!stateFiles) { stats.skipped += 1; report(); return; } + const root = stateRoot(); + const rel = typeof rec.rel === 'string' ? rec.rel.replace(/\\/g, '/') : ''; + const parts = rel.split('/').filter(Boolean); + const safe = !!root && parts.length > 0 && !NOT_BACKED_UP.has(parts[0]) && !path.isAbsolute(rel) && parts.every(p => p !== '.' && p !== '..' && !/[\0]/.test(p)); + const rootAbs = root ? path.resolve(root) : ''; + const target = safe ? path.resolve(rootAbs, ...parts) : ''; + if (!safe || !target.startsWith(rootAbs + path.sep)) { stats.failed += 1; noteError('state file rejected'); report(); return; } + try { + fs.mkdirSync(path.dirname(target), { recursive: true }); + if (fs.existsSync(target)) { try { fs.copyFileSync(target, `${target}.before-restore`); } catch (_) {} } + fs.writeFileSync(target, rec.data); + stats.files += 1; + } catch (_) { stats.failed += 1; noteError('state file could not be written'); } + report(); + return; + } + if (rec.type === 'blob') { + if (await hasBlob(rec.id)) { stats.blobsSkipped += 1; report(); return; } + const ref = await addBlob(rec.data); + if (ref) { stats.blobs += 1; present.set(ref, rec.data.length); } else { stats.failed += 1; noteError('blob could not be stored'); } + report(); + } + }); + deferred.sort((a, b) => a.msg.value.author === b.msg.value.author ? (a.msg.value.sequence || 0) - (b.msg.value.sequence || 0) : String(a.msg.value.author).localeCompare(String(b.msg.value.author))); + let pending = deferred; + while (pending.length) { + const next = []; + for (const item of pending) { + const res = await tryAdd(item.msg); + if (res !== true) next.push({ msg: item.msg, err: res }); + } + report(); + if (next.length === pending.length) { pending = next; break; } + pending = next; + } + for (const item of pending) { stats.failed += 1; noteError(item.err); } + stats.percent = 100; + stats.forks = Array.from(forkAuthors.entries()).map(([author, count]) => ({ author, count, mine: author === ssbClient.id })); + stats.errors = Array.from(errorCounts.entries()).sort((a, b) => b[1] - a[1]).slice(0, 5).map(([reason, count]) => ({ reason, count })); + try { fs.unlinkSync(filePath); } catch (_) {} + report(); + return { ...stats, meta: stats.meta || meta }; + }, + + startRestore({ filePath, password, source = 'file' }) { + if (restoreJob && restoreJob.running) return restoreJob; + const job = { running: true, source, startedAt: new Date().toISOString(), finishedAt: null, progress: { messages: 0, skipped: 0, failed: 0, forked: 0, blobs: 0, blobsSkipped: 0, percent: 0 }, result: null, error: null }; + restoreJob = job; + job.promise = this.restoreBackup({ filePath, password, onProgress: (p) => { job.progress = p; } }) + .then((res) => { job.result = res; return res; }) + .catch((err) => { job.error = err && err.message ? err.message : String(err); try { fs.unlinkSync(filePath); } catch (_) {} return null; }) + .finally(() => { job.running = false; job.finishedAt = new Date().toISOString(); }); + return job; + }, + + restoreStatus() { + if (!restoreJob) return null; + const { promise, ...rest } = restoreJob; + return rest; + }, + + reminderState() { return reminderState(); }, + + setReminderDays(days) { return setReminderDays(days); }, + + markBackup() { return markBackup(); }, + + async rebuildIndexes() { + const started = Date.now(); + const ssbClient = await openSsb(); + const indexDir = path.join(require('../configs/state-manager').ssbDir(), 'db2', 'indexes'); + const measure = (dir) => { + let files = 0, bytes = 0; + const walk = (d) => { + let entries = []; + try { entries = fs.readdirSync(d, { withFileTypes: true }); } catch (_) { return; } + for (const entry of entries) { + const full = path.join(d, entry.name); + if (entry.isDirectory()) { walk(full); continue; } + try { bytes += fs.statSync(full).size; files += 1; } catch (_) {} + } + }; + walk(dir); + return { files, bytes }; + }; + const before = measure(indexDir); + let error = null; + try { + await new Promise((resolve, reject) => { + try { ssbClient.rebuild((err) => err ? reject(err) : resolve()); } catch (e) { reject(e); } + }); + } catch (e) { error = e && e.message ? e.message : String(e); } + const after = measure(indexDir); + let totalMessages = 0; + try { totalMessages = (await readLog(ssbClient)).length; } catch (_) {} + return { + checkedAt: new Date().toISOString(), + tookMs: Date.now() - started, + ok: !error, + error, + totalMessages, + indexes: { path: indexDir, files: after.files, bytes: after.bytes, deltaBytes: after.bytes - before.bytes } + }; + }, + async verify({ author = null } = {}) { + const started = Date.now(); + const ssbClient = await openSsb(); + const me = author || ssbClient.id; + const all = await readLog(ssbClient); + let validate = null; + try { validate = require('../server/node_modules/ssb-validate'); } catch (_) {} + const own = all.filter(m => m && m.value && m.value.author === me).sort((a, b) => (a.value.sequence || 0) - (b.value.sequence || 0)); + const feed = { messages: own.length, lastSequence: own.length ? own[own.length - 1].value.sequence : 0, gaps: [], brokenLinks: [], badHashes: [], badSignatures: [], duplicates: [] }; + let expected = 1; + let prevKey = null; + const seen = new Set(); + for (const m of own) { + const seq = Number(m.value.sequence) || 0; + if (seen.has(seq)) feed.duplicates.push(seq); + seen.add(seq); + if (seq !== expected) { feed.gaps.push({ expected, found: seq }); expected = seq; } + if (prevKey && m.value.previous !== prevKey) feed.brokenLinks.push(seq); + if (validate) { + try { if (validate.id(m.value) !== m.key) feed.badHashes.push(seq); } catch (_) { feed.badHashes.push(seq); } + try { if (validate.checkInvalidOOO(m.value, null)) feed.badSignatures.push(seq); } catch (_) { feed.badSignatures.push(seq); } + } + prevKey = m.key; + expected = seq + 1; + } + feed.ok = !feed.gaps.length && !feed.brokenLinks.length && !feed.badHashes.length && !feed.badSignatures.length && !feed.duplicates.length; + const bySeq = new Map(); + for (const m of all) { + if (!m || !m.value || !m.value.author) continue; + const k = `${m.value.author}::${m.value.sequence}`; + if (!bySeq.has(k)) bySeq.set(k, new Set()); + bySeq.get(k).add(m.key); + } + const forkMap = new Map(); + for (const [k, keys] of bySeq) { + if (keys.size < 2) continue; + const [feedId, seq] = k.split('::'); + if (!forkMap.has(feedId)) forkMap.set(feedId, []); + forkMap.get(feedId).push({ sequence: Number(seq), keys: Array.from(keys) }); + } + const forks = Array.from(forkMap.entries()).map(([feedId, points]) => ({ author: feedId, mine: feedId === me, points: points.sort((a, b) => a.sequence - b.sequence) })); + const referenced = new Set(); + const referencedOwn = new Set(); + for (const m of all) { + if (!m || !m.value) continue; + for (const ref of blobRefsOf(m.value.content)) { + referenced.add(ref); + if (m.value.author === me) referencedOwn.add(ref); + } + } + const present = await listBlobs(ssbClient); + const orphan = []; + let orphanBytes = 0; + for (const [id, size] of present) if (!referenced.has(id)) { orphan.push(id); orphanBytes += size || 0; } + const missing = []; + const missingOwn = []; + for (const id of referenced) { + if (!present.size || present.has(id)) continue; + missing.push(id); + if (referencedOwn.has(id)) missingOwn.push(id); + } + return { + author: me, checkedAt: new Date().toISOString(), tookMs: Date.now() - started, totalMessages: all.length, + feed, forks, forkCount: forks.length, + blobs: { present: present.size, referenced: referenced.size, orphan: orphan.length, orphanBytes, missing: missing.length, missingOwn: missingOwn.length, orphanIds: orphan.slice(0, 50), missingIds: missing.slice(0, 50), missingOwnIds: missingOwn.slice(0, 50), listed: present.size > 0 || referenced.size === 0 } + }; + } + }; +}; + +Object.assign(module.exports, { reminderState, setReminderDays, markBackup, REMINDER_OPTIONS, MODULE_KEYS }); diff --git a/src/models/banking_model.js b/src/models/banking_model.js index 4837168..f5092e2 100644 --- a/src/models/banking_model.js +++ b/src/models/banking_model.js @@ -2,7 +2,8 @@ const crypto = require("crypto"); const fs = require("fs"); const path = require("path"); const pull = require("../server/node_modules/pull-stream"); -const { getConfig } = require("../configs/config-manager.js"); +const { readTyped, CONTENT_TYPES, requestScope } = require("./typed_log"); +const { getConfig, defaultInvite } = require("../configs/config-manager.js"); const { config } = require("../server/SSB_server.js"); const sharedState = require("../configs/shared-state.js"); @@ -19,12 +20,17 @@ const DEFAULT_RULES = { graceDays: 30 }; -const STORAGE_DIR = process.env.OASIS_BANKING_DIR || path.join(__dirname, "..", "configs"); -const EPOCHS_PATH = path.join(STORAGE_DIR, "banking-epochs.json"); -const TRANSFERS_PATH = path.join(STORAGE_DIR, "banking-allocations.json"); -const ADDR_PATH = path.join(STORAGE_DIR, "wallet-addresses.json"); -const ECO_HISTORY_PATH = path.join(STORAGE_DIR, "banking-eco-history.json"); -const ECO_HISTORY_MAX = 500; +const { statePath, stateDir } = require("../configs/state-manager"); +const stateFile = (name) => process.env.OASIS_BANKING_DIR ? path.join(process.env.OASIS_BANKING_DIR, name) : statePath(name); +const STORAGE_DIR = process.env.OASIS_BANKING_DIR || stateDir("banking-epochs.json"); +const EPOCHS_PATH = stateFile("banking-epochs.json"); +const TRANSFERS_PATH = stateFile("banking-allocations.json"); +const ADDR_PATH = stateFile("wallet-addresses.json"); +const BOOK_PATH = stateFile("banking-address-book.json"); +const ECO_HISTORY_PATH = stateFile("banking-eco-history.json"); +const UBI_PAID_PATH = stateFile("banking-ubi-paid.json"); +const REBALANCE_PENDING_PATH = stateFile("banking-rebalance-pending.json"); +const FUNDS_HISTORY_PATH = stateFile("banking-funds-history.json"); const ECO_HISTORY_MIN_GAP_MS = 5 * 60 * 1000; const ECOIN_PER_GRAM_CO2 = 0.1; @@ -42,6 +48,8 @@ function ensureStoreFiles() { if (!fs.existsSync(TRANSFERS_PATH)) fs.writeFileSync(TRANSFERS_PATH, "[]"); if (!fs.existsSync(ADDR_PATH)) fs.writeFileSync(ADDR_PATH, "{}"); if (!fs.existsSync(ECO_HISTORY_PATH)) fs.writeFileSync(ECO_HISTORY_PATH, "[]"); + if (!fs.existsSync(UBI_PAID_PATH)) fs.writeFileSync(UBI_PAID_PATH, "{}"); + if (!fs.existsSync(FUNDS_HISTORY_PATH)) fs.writeFileSync(FUNDS_HISTORY_PATH, "[]"); } function readEcoHistory() { @@ -49,6 +57,59 @@ function readEcoHistory() { try { return JSON.parse(fs.readFileSync(ECO_HISTORY_PATH, "utf8")) || []; } catch (_) { return []; } } +const ECO_HISTORY_TIERS = [ + { olderThanMs: 2 * ONE_DAY_MS, keepEveryMs: 60 * 60 * 1000 }, + { olderThanMs: 60 * ONE_DAY_MS, keepEveryMs: ONE_DAY_MS } +]; +const ECO_HISTORY_HARD_MAX = 20000; + +function readFundsHistory() { + try { return JSON.parse(fs.readFileSync(FUNDS_HISTORY_PATH, "utf8")) || []; } catch (_) { return []; } +} + +function appendFundsHistory(sample) { + ensureStoreFiles(); + let arr = readFundsHistory(); + if (!Array.isArray(arr)) arr = []; + const last = arr[arr.length - 1]; + if (last && Number(sample.ts) - Number(last.ts) < ECO_HISTORY_MIN_GAP_MS) return arr; + arr.push(sample); + arr = compactEcoHistory(arr); + writeJson(FUNDS_HISTORY_PATH, arr); + return arr; +} + +function compactEcoHistory(arr) { + const now = Date.now(); + const out = []; + let lastKeptTs = -Infinity; + for (const s of arr) { + const ts = Number(s.ts) || 0; + const age = now - ts; + const tier = ECO_HISTORY_TIERS.slice().reverse().find(t => age > t.olderThanMs); + if (!tier || ts - lastKeptTs >= tier.keepEveryMs) { out.push(s); lastKeptTs = ts; } + } + return out.length > ECO_HISTORY_HARD_MAX ? out.slice(out.length - ECO_HISTORY_HARD_MAX) : out; +} + +const RANGE_MS = { today: ONE_DAY_MS, week: 7 * ONE_DAY_MS, month: 30 * ONE_DAY_MS, year: 365 * ONE_DAY_MS, "5y": 5 * 365 * ONE_DAY_MS, all: Infinity }; +const normalizeRange = (r) => (Object.prototype.hasOwnProperty.call(RANGE_MS, String(r || "")) ? String(r) : "today"); +const filterByRange = (arr, range, key = "ts") => { + const span = RANGE_MS[normalizeRange(range)]; + if (!Number.isFinite(span)) return arr; + const from = Date.now() - span; + return arr.filter(x => Number(x[key]) >= from); +}; +const RANGE_ORDER = ["today", "week", "month", "year", "5y", "all"]; +const widenRange = (arr, range, key = "ts") => { + const start = Math.max(0, RANGE_ORDER.indexOf(normalizeRange(range))); + for (let i = start; i < RANGE_ORDER.length; i++) { + const r = RANGE_ORDER[i]; + if (filterByRange(arr, r, key).length >= 2) return r; + } + return normalizeRange(range); +}; + function appendEcoHistory(sample) { ensureStoreFiles(); let arr = readEcoHistory(); @@ -56,7 +117,7 @@ function appendEcoHistory(sample) { const last = arr[arr.length - 1]; if (last && Number(sample.ts) - Number(last.ts) < ECO_HISTORY_MIN_GAP_MS) return arr; arr.push(sample); - if (arr.length > ECO_HISTORY_MAX) arr = arr.slice(arr.length - ECO_HISTORY_MAX); + arr = compactEcoHistory(arr); try { fs.writeFileSync(ECO_HISTORY_PATH, JSON.stringify(arr)); } catch (_) {} return arr; } @@ -133,7 +194,52 @@ function writeJson(p, v) { fs.writeFileSync(p, JSON.stringify(v, null, 2)); } -async function rpcCall(method, params, kind = "user") { +const ubiPaidKey = (epochId, claimantId) => `${epochId}:${claimantId}`; + +function readUbiPaidLedger() { + ensureStoreFiles(); + const raw = readJson(UBI_PAID_PATH, {}); + return raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {}; +} + +function reserveUbiPayment(key) { + const ledger = readUbiPaidLedger(); + if (ledger[key]) return false; + ledger[key] = { startedAt: new Date().toISOString() }; + writeJson(UBI_PAID_PATH, ledger); + return true; +} + +function settleUbiPayment(key, patch) { + const ledger = readUbiPaidLedger(); + ledger[key] = { ...(ledger[key] || {}), ...patch }; + writeJson(UBI_PAID_PATH, ledger); +} + +function releaseUbiPayment(key) { + const ledger = readUbiPaidLedger(); + delete ledger[key]; + writeJson(UBI_PAID_PATH, ledger); +} + +function ubiAddressTaken(epochId, address, claimantId) { + const ledger = readUbiPaidLedger(); + const own = ubiPaidKey(epochId, claimantId); + return Object.keys(ledger).some(k => k !== own && k.startsWith(`${epochId}:`) && ledger[k] && ledger[k].address === address); +} + +const rpcFailureLog = new Map(); +function logRpcFailure(method, kind, reason) { + const key = `${kind}:${method}:${reason}`; + const now = Date.now(); + if (now - (rpcFailureLog.get(key) || 0) < 5 * 60 * 1000) return; + rpcFailureLog.set(key, now); + if (kind === "pub") console.warn(`[ECOin RPC] ${kind} ${method} failed: ${reason}`); +} + +const RPC_TIMEOUT_MS = { sendtoaddress: 120000, listtransactions: 30000, gettransaction: 15000 }; +const ENGINE_RPC_TIMEOUT_MS = 20000; +async function rpcCall(method, params, kind = "user", timeoutMs = 0) { const cfg = getWalletCfg(kind); if (!cfg?.url) { return null; @@ -145,7 +251,7 @@ async function rpcCall(method, params, kind = "user") { headers.authorization = "Basic " + Buffer.from(`${cfg.user}:${cfg.pass}`).toString("base64"); } const controller = new AbortController(); - const timer = setTimeout(() => controller.abort(), 1500); + const timer = setTimeout(() => controller.abort(), timeoutMs || RPC_TIMEOUT_MS[method] || 1500); try { const res = await fetch(cfg.url, { method: "POST", @@ -159,23 +265,78 @@ async function rpcCall(method, params, kind = "user") { signal: controller.signal, }); if (!res.ok) { + logRpcFailure(method, kind, `HTTP ${res.status}`); return null; } const data = await res.json(); if (data.error) { + logRpcFailure(method, kind, data.error.message || JSON.stringify(data.error)); return null; } return data.result; } catch (err) { + logRpcFailure(method, kind, err && err.name === "AbortError" ? "timeout" : (err && err.message) || String(err)); return null; } finally { clearTimeout(timer); } } -async function safeGetBalance(kind = "user") { +const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + +async function listWalletSends(kind = "pub") { + for (const account of ["*", ""]) { + const list = await rpcCall("listtransactions", [account, 1000, 0], kind); + if (Array.isArray(list)) return list.filter(t => t && t.category === "send"); + } + return null; +} + +const sameAmount = (a, b) => Math.abs(Math.abs(Number(a)) - Math.abs(Number(b))) <= 0.000001; + +function sendMatches(t, { address, amount, comment, sinceMs, amountIfNoComment }) { + if (!t || t.address !== address || typeof t.txid !== "string") return false; + const at = Number(t.time || t.timereceived || 0) * 1000; + if (sinceMs && at && at < sinceMs - 5 * 60 * 1000) return false; + if (amount !== undefined && amount !== null && !sameAmount(t.amount, amount)) return false; + if (comment) { + if (typeof t.comment === "string") { if (t.comment !== comment) return false; } + else if (amountIfNoComment !== undefined && amountIfNoComment !== null && !sameAmount(t.amount, amountIfNoComment)) return false; + } + return true; +} + +async function findWalletSend(match, kind = "pub", sends = null) { + const list = sends || await listWalletSends(kind); + if (!list) return undefined; + const hit = list.find(t => sendMatches(t, match) && !(match.exclude && match.exclude.has(t.txid))); + return hit ? hit.txid : null; +} + +async function sendVerified(address, amount, comment, kind = "pub") { + const sinceMs = Date.now(); + const txid = await rpcCall("sendtoaddress", [address, amount, comment], kind); + if (typeof txid === "string" && txid) return { txid }; + await sleep(3000); + const found = await findWalletSend({ address, amount, comment, sinceMs }, kind); + if (typeof found === "string") return { txid: found, recovered: true }; + return { txid: null, unknown: found === undefined }; +} + +async function tryGetBalance(kind = "user", timeoutMs = 0) { try { - const r = await rpcCall("getbalance", [], kind); + const r = await rpcCall("getbalance", [], kind, timeoutMs); + if (r === null || r === undefined) return null; + const n = Number(r); + return Number.isFinite(n) ? n : null; + } catch { + return null; + } +} + +async function safeGetBalance(kind = "user", timeoutMs = 0) { + try { + const r = await rpcCall("getbalance", [], kind, timeoutMs); return Number(r) || 0; } catch { return 0; @@ -193,6 +354,35 @@ function writeAddrMap(m) { writeJson(ADDR_PATH, m || {}); } +function readAddressBook() { + const raw = readJson(BOOK_PATH, []); + return Array.isArray(raw) ? raw : []; +} + +function listAddressBook() { + return readAddressBook().map(e => ({ entryId: e.id, id: e.userId || null, address: e.address, label: e.label || "", source: "book", createdAt: e.createdAt })); +} + +function addAddressBookEntry({ label, address, userId }) { + if (!isValidEcoinAddress(address)) return { status: "invalid" }; + const book = readAddressBook(); + const cleanLabel = String(label || "").trim().slice(0, 80); + const cleanUser = /^@[A-Za-z0-9+/]+={0,2}\.ed25519$/.test(String(userId || "")) ? String(userId) : null; + const dup = book.find(e => e.address === address && (e.label || "") === cleanLabel && (e.userId || null) === cleanUser); + if (dup) return { status: "exists" }; + book.push({ id: crypto.randomBytes(6).toString("hex"), label: cleanLabel, address, userId: cleanUser, createdAt: new Date().toISOString() }); + writeJson(BOOK_PATH, book); + return { status: "added" }; +} + +function removeAddressBookEntry(entryId) { + const book = readAddressBook(); + const next = book.filter(e => e.id !== String(entryId || "")); + if (next.length === book.length) return { status: "not_found" }; + writeJson(BOOK_PATH, next); + return { status: "deleted" }; +} + function getLogLimit() { return getConfig().ssbLogStream?.limit || 1000; } @@ -201,6 +391,11 @@ function isValidEcoinAddress(addr) { return typeof addr === "string" && /^E[1-9A-HJ-NP-Za-km-z]{32,34}$/.test(addr); } +function hasWalletCredentials(cfg = null) { + const w = (cfg || getConfig() || {}).wallet || {}; + return !!(String(w.url || "").trim() && String(w.user || "").trim() && String(w.pass || "").trim()); +} + function getWalletCfg(kind) { const cfg = getConfig() || {}; if (kind === "pub") { @@ -211,14 +406,32 @@ function getWalletCfg(kind) { } function isPubNode() { - const pubId = (getConfig() || {}).walletPub?.pubId || ""; - const myId = config?.keys?.id || ""; - return !!pubId && !!myId && pubId === myId; + const cfg = getConfig() || {}; + return config?.pub === true && !!(cfg.wallet && cfg.wallet.url) && !!config?.keys?.id; } -function getConfiguredPubId() { - return (getConfig() || {}).walletPub?.pubId || ""; +function getDefaultPubId() { + const m = String((defaultInvite() || {}).code || "").match(/(@[A-Za-z0-9+/]+={0,2}\.ed25519)/); + return m ? m[1] : ""; } +const PUB_DISCOVERY_TTL_MS = 60 * 1000; +const PUB_ANNOUNCE_MAX_AGE_MS = 3 * 24 * 60 * 60 * 1000; +const PUB_ANNOUNCE_REFRESH_MS = 12 * 60 * 60 * 1000; +const CLAIMANT_MIN_AGE_MS = 30 * 24 * 60 * 60 * 1000; +const REBALANCE_MIN_CLAIMANTS = 3; +const REBALANCE_PAYOUT_RATIO = 0.5; +const REBALANCE_TOLERANCE = 0.05; +const REBALANCE_SETTLE_MS = 6 * 60 * 60 * 1000; +const REBALANCE_CONCEPT = "OASIS UBI Rebalance"; +const UBI_PAYMENT_CONCEPT = "UBI - "; +const UBI_PAYMENT_PREFIXES = ["UBI - ", "OASIS UBI Payment"]; +const UBI_SEND_COMMENT = "OASIS UBI Payment"; +const UBI_TAKEOVER_AFTER_MS = 3 * 24 * 60 * 60 * 1000; +const UBI_TAKEOVER_MARGIN_MS = 12 * 60 * 60 * 1000; +const UBI_MAX_ATTEMPTS = 6; +const UBI_RETRY_AFTER_MS = 60 * 60 * 1000; +const isUbiPayoutConcept = (c) => UBI_PAYMENT_PREFIXES.some(p => String(c || "").startsWith(p)); +const TXID_RE = /\b([0-9a-f]{64})\b/i; function resolveUserId(maybeId) { const s = String(maybeId || "").trim(); @@ -226,7 +439,7 @@ function resolveUserId(maybeId) { return config?.keys?.id || ""; } -let FEED_SRC = "none"; + module.exports = ({ services } = {}) => { const transfersRepo = { @@ -260,38 +473,56 @@ module.exports = ({ services } = {}) => { return ssbInstance; } + const karma = require("./karma_model")({ services, ranges: { normalizeRange, widenRange, filterByRange } }); + const { listAllActions, fetchUserActions, scoreFromActions, getCarbonGramsForUser, getUserEngagementScore, getLastKarmaScore, listKarmaHistory } = karma; + + const BANKING_TYPES = ["wallet", "karmaScore", "ubiClaim", "ubiClaimResult", "ubiAllocation", "ubiRefuse", "pubAvailability", "tombstone"]; + async function scanLogStream() { const ssb = await openSsb(); if (!ssb) return []; - return new Promise((resolve, reject) => - pull( - ssb.createLogStream({ limit: getLogLimit(), reverse: true }), - pull.collect((err, arr) => err ? reject(err) : resolve(arr)) - ) - ); + return readTyped(ssb, BANKING_TYPES, { limit: getLogLimit(), withWindow: true }); + } + + const claimsInFlight = new Set(); + + const isWalletMessage = (c) => !!c && typeof c.address === "string" && isValidEcoinAddress(c.address) && + ((c.type === "wallet" && c.coin === "ECO") || c.type === "bankWallet"); + const isWalletUnset = (c) => !!c && typeof c.address === "string" && c.address === "" && + ((c.type === "wallet" && c.coin === "ECO") || c.type === "bankWallet"); + + async function scanWalletMessages() { + const ssb = await openSsb(); + if (!ssb) return []; + const msgs = await readTyped(ssb, ["wallet", "bankWallet"], { limit: Math.max(getLogLimit(), 20000) }); + return msgs.slice().sort((a, b) => (((b.value && b.value.timestamp) || 0) - ((a.value && a.value.timestamp) || 0)) || (((b.value && b.value.sequence) || 0) - ((a.value && a.value.sequence) || 0))); } async function getWalletFromSSB(userId) { - const msgs = await scanLogStream(); + const msgs = await scanWalletMessages(); for (const m of msgs) { const v = m.value || {}; - const c = v.content || {}; - if (v.author === userId && c && c.type === "wallet" && c.coin === "ECO" && typeof c.address === "string") { - return c.address; - } + if (v.author !== userId) continue; + if (isWalletMessage(v.content)) return v.content.address; + if (isWalletUnset(v.content)) return null; } return null; } + async function hasPublishedAddress(userId) { + return !!(await getWalletFromSSB(userId)); + } + async function scanAllWalletsSSB() { const latest = {}; - const msgs = await scanLogStream(); + const decided = new Set(); + const msgs = await scanWalletMessages(); for (const m of msgs) { const v = m.value || {}; const c = v.content || {}; - if (c && c.type === "wallet" && c.coin === "ECO" && typeof c.address === "string") { - if (!latest[v.author]) latest[v.author] = c.address; - } + if (!v.author || decided.has(v.author)) continue; + if (isWalletMessage(c)) { latest[v.author] = c.address; decided.add(v.author); } + else if (isWalletUnset(c)) decided.add(v.author); } return latest; } @@ -347,6 +578,7 @@ module.exports = ({ services } = {}) => { if (ssbAll[userId]) { m[userId] = "__removed__"; writeAddrMap(m); + if (idsEqual(userId, config.keys.id)) { try { await publishSelfAddress(""); } catch (_) {} } return { status: "deleted" }; } if (hadLocal) { @@ -379,235 +611,6 @@ module.exports = ({ services } = {}) => { return strip(A) === strip(B); } - function inferType(c = {}) { - if (c.vote) return "vote"; - if (c.votes) return "votes"; - if (c.address && c.coin === "ECO" && c.type === "wallet") return "bankWallet"; - if (c.type === "ubiClaimResult" && c.txid && c.epochId) return "ubiClaimResult"; - if (typeof c.amount !== "undefined" && c.epochId && c.allocationId) return "bankClaim"; - if (typeof c.item_type !== "undefined" && typeof c.status !== "undefined") return "market"; - if (typeof c.goal !== "undefined" && typeof c.progress !== "undefined") return "project"; - if (typeof c.members !== "undefined" && typeof c.isAnonymous !== "undefined") return "tribe"; - if (typeof c.date !== "undefined" && typeof c.location !== "undefined") return "event"; - if (typeof c.priority !== "undefined" && typeof c.status !== "undefined" && c.title) return "task"; - if (typeof c.confirmations !== "undefined" && typeof c.severity !== "undefined") return "report"; - if (typeof c.job_type !== "undefined" && typeof c.status !== "undefined") return "job"; - if (typeof c.url !== "undefined" && typeof c.mimeType !== "undefined" && c.type === "audio") return "audio"; - if (typeof c.url !== "undefined" && typeof c.mimeType !== "undefined" && c.type === "video") return "video"; - if (typeof c.url !== "undefined" && c.title && c.key) return "document"; - if (typeof c.text !== "undefined" && typeof c.refeeds !== "undefined") return "feed"; - if (typeof c.text !== "undefined" && typeof c.contentWarning !== "undefined") return "post"; - if (typeof c.contact !== "undefined") return "contact"; - if (typeof c.about !== "undefined") return "about"; - if (typeof c.concept !== "undefined" && typeof c.amount !== "undefined" && c.status) return "transfer"; - return ""; - } - - function normalizeType(a) { - const t = a.type || a.content?.type || inferType(a.content) || ""; - return String(t).toLowerCase(); - } - - function priorityBump(p) { - const s = String(p || "").toUpperCase(); - if (s === "HIGH") return 3; - if (s === "MEDIUM") return 1; - return 0; - } - - function severityBump(s) { - const x = String(s || "").toUpperCase(); - if (x === "CRITICAL") return 6; - if (x === "HIGH") return 4; - if (x === "MEDIUM") return 2; - return 0; - } - - function scoreMarket(c) { - const st = String(c.status || "").toUpperCase(); - let s = 5; - if (st === "SOLD") s += 8; - else if (st === "ACTIVE") s += 3; - const bids = Array.isArray(c.auctions_poll) ? c.auctions_poll.length : 0; - s += Math.min(10, bids); - return s; - } - - function scoreProject(c) { - const st = String(c.status || "ACTIVE").toUpperCase(); - const prog = Number(c.progress || 0); - let s = 8 + Math.min(10, prog / 10); - if (st === "FUNDED") s += 10; - return s; - } - - function calculateOpinionScore(content) { - const cats = content?.opinions || {}; - let s = 0; - for (const k in cats) { - if (!Object.prototype.hasOwnProperty.call(cats, k)) continue; - if (k === "interesting" || k === "inspiring") s += 5; - else if (k === "boring" || k === "spam" || k === "propaganda") s -= 3; - else s += 1; - } - return s; - } - - async function listAllActions() { - if (services?.feed?.listAll) { - const arr = await services.feed.listAll(); - FEED_SRC = "services.feed.listAll"; - return normalizeFeedArray(arr); - } - if (services?.activity?.list) { - const arr = await services.activity.list(); - FEED_SRC = "services.activity.list"; - return normalizeFeedArray(arr); - } - if (typeof global.listFeed === "function") { - const arr = await global.listFeed("all"); - FEED_SRC = "global.listFeed('all')"; - return normalizeFeedArray(arr); - } - const ssb = await openSsb(); - if (!ssb || !ssb.createLogStream) { - FEED_SRC = "none"; - return []; - } - const msgs = await scanLogStream(); - FEED_SRC = "ssb.createLogStream"; - return msgs.map(m => { - const v = m.value || {}; - const c = v.content || {}; - return { - id: v.key || m.key, - author: v.author, - type: (c.type || "").toLowerCase(), - value: v, - content: c - }; - }); - } - - function normalizeFeedArray(arr) { - if (!Array.isArray(arr)) return []; - return arr.map(x => { - const value = x.value || {}; - const content = x.content || value.content || {}; - const author = x.author || value.author || content.author || null; - const type = (content.type || "").toLowerCase(); - return { id: x.id || value.key || x.key, author, type, value, content }; - }); - } - -async function publishKarmaScore(userId, karmaScore) { - const ssb = await openSsb(); - if (!ssb || !ssb.publish) return false; - const timestamp = new Date().toISOString(); - const content = { type: "karmaScore", karmaScore, userId, timestamp }; - return new Promise((resolve, reject) => { - ssb.publish(content, (err, msg) => err ? reject(err) : resolve(msg)); - }); -} - -async function fetchUserActions(userId) { - const me = resolveUserId(userId); - const actions = await listAllActions(); - const authored = actions.filter(a => - (a.author && a.author === me) || (a.value?.author && a.value.author === me) - ); - if (authored.length) return authored; - return actions.filter(a => { - const c = a.content || {}; - const fields = [c.author, c.organizer, c.seller, c.about, c.contact]; - return fields.some(f => f && f === me); - }); -} - -function scoreFromActions(actions) { - let score = 0; - const nowMs = Date.now(); - for (const action of actions) { - const t = normalizeType(action); - const c = action.content || {}; - const rawType = String(c.type || "").toLowerCase(); - const ts = action.value?.timestamp; - const ageDays = ts ? (nowMs - ts) / 86400000 : Infinity; - const decay = ageDays <= 30 ? 1.0 : ageDays <= 90 ? 0.5 : 0.25; - if (t === "post") score += 10 * decay; - else if (t === "comment") score += 5 * decay; - else if (t === "like") score += 2 * decay; - else if (t === "image") score += 8 * decay; - else if (t === "video") score += 12 * decay; - else if (t === "audio") score += 8 * decay; - else if (t === "document") score += 6 * decay; - else if (t === "bookmark") score += 2 * decay; - else if (t === "feed") score += 6 * decay; - else if (t === "forum") score += (c.root ? 5 : 10) * decay; - else if (t === "vote") score += (3 + calculateOpinionScore(c)) * decay; - else if (t === "votes") score += Math.min(10, Number(c.totalVotes || 0)) * decay; - else if (t === "market") score += scoreMarket(c) * decay; - else if (t === "project") score += scoreProject(c) * decay; - else if (t === "tribe") score += (6 + Math.min(10, Array.isArray(c.members) ? c.members.length * 0.5 : 0)) * decay; - else if (t === "event") score += (4 + Math.min(10, Array.isArray(c.attendees) ? c.attendees.length : 0)) * decay; - else if (t === "task") score += (3 + priorityBump(c.priority)) * decay; - else if (t === "report") score += (4 + (Array.isArray(c.confirmations) ? c.confirmations.length : 0) + severityBump(c.severity)) * decay; - else if (t === "curriculum") score += 5 * decay; - else if (t === "aiexchange") score += (Array.isArray(c.ctx) ? Math.min(10, c.ctx.length) : 0) * decay; - else if (t === "job") score += (4 + (Array.isArray(c.subscribers) ? c.subscribers.length : 0)) * decay; - else if (t === "bankclaim") score += Math.min(20, Math.log(1 + Math.max(0, Number(c.amount) || 0)) * 5) * decay; - else if (t === "bankwallet") score += 2 * decay; - else if (t === "transfer") score += 1 * decay; - else if (t === "about") score += (1 + ((c.visibilityPrefs && c.visibilityPrefs.fediverseHandle) ? 8 : 0)) * decay; - else if (t === "contact") score += 1 * decay; - else if (t === "pub") score += 1 * decay; - else if (t === "parliamentcandidature" || rawType === "parliamentcandidature") score += 12 * decay; - else if (t === "parliamentterm" || rawType === "parliamentterm") score += 25 * decay; - else if (t === "parliamentproposal" || rawType === "parliamentproposal") score += 8 * decay; - else if (t === "parliamentlaw" || rawType === "parliamentlaw") score += 16 * decay; - else if (t === "parliamentrevocation" || rawType === "parliamentrevocation") score += 10 * decay; - else if (t === "courts_case" || t === "courtscase" || rawType === "courts_case") score += 4 * decay; - else if (t === "courts_evidence" || t === "courtsevidence" || rawType === "courts_evidence") score += 3 * decay; - else if (t === "courts_answer" || t === "courtsanswer" || rawType === "courts_answer") score += 4 * decay; - else if (t === "courts_verdict" || t === "courtsverdict" || rawType === "courts_verdict") score += 10 * decay; - else if (t === "courts_settlement" || t === "courtssettlement" || rawType === "courts_settlement") score += 8 * decay; - else if (t === "courts_nomination" || t === "courtsnomination" || rawType === "courts_nomination") score += 6 * decay; - else if (t === "courts_nom_vote" || t === "courtsnomvote" || rawType === "courts_nom_vote") score += 3 * decay; - else if (t === "courts_public_pref" || t === "courtspublicpref" || rawType === "courts_public_pref") score += 1 * decay; - else if (t === "courts_mediators" || t === "courtsmediators" || rawType === "courts_mediators") score += 6 * decay; - else if (t === "courts_open_support" || t === "courtsopensupport" || rawType === "courts_open_support") score += 2 * decay; - else if (t === "courts_verdict_vote" || t === "courtsverdictvote" || rawType === "courts_verdict_vote") score += 3 * decay; - else if (t === "courts_judge_assign" || t === "courtsjudgeassign" || rawType === "courts_judge_assign") score += 5 * decay; - else if (t === "larpjoinhouse") score += 15 * decay; - else if (t === "larphousepost") score += 6 * decay; - else if (t === "larptestattempt") score += 3 * decay; - else if (t === "torrent") score += 6 * decay; - else if (t === "shop" || t === "shopproduct") score += 6 * decay; - else if (t === "shop-purchase") score += 2 * decay; - else if (t === "pad" || t === "padentry") score += 3 * decay; - else if (t === "calendar" || t === "calendarnote" || t === "calendardate") score += 3 * decay; - else if (t === "chat") score += 1 * decay; - else if (t === "gamescore") score += 2 * decay; - else if (t === "pixelia") score += 2 * decay; - } - return Math.max(0, Math.round(score)); -} - -async function getCarbonGramsForUser(userId) { - const ssb = await openSsb(); - if (!ssb || !userId) return 0; - return new Promise((resolve) => { - let bytes = 0; - pull( - ssb.createUserStream({ id: userId }), - pull.drain( - (m) => { try { bytes += Buffer.byteLength(JSON.stringify(m && m.value), 'utf8'); } catch (_) {} }, - () => resolve((bytes / (1024 * 1024)) * 0.095) - ) - ); - }); -} - async function getBytesForUser(userId) { const ssb = await openSsb(); if (!ssb || !userId) return 0; @@ -624,7 +627,7 @@ async function getBytesForUser(userId) { } let _ecoTaxStatsCache = null; -const ECO_TAX_STATS_TTL_MS = 60 * 1000; +const ECO_TAX_STATS_TTL_MS = 10 * 60 * 1000; async function calculateEcoTaxStatsInternal() { const ssb = await (async () => { @@ -702,12 +705,14 @@ async function calculateEcoTaxStatsInternal() { }); } +let _ecoTaxStatsPending = null; async function calculateEcoTaxStats() { const now = Date.now(); if (_ecoTaxStatsCache && (now - _ecoTaxStatsCache.ts) < ECO_TAX_STATS_TTL_MS) { return _ecoTaxStatsCache.value; } - const value = await calculateEcoTaxStatsInternal().catch(() => null); + if (!_ecoTaxStatsPending) _ecoTaxStatsPending = calculateEcoTaxStatsInternal().catch(() => null).finally(() => { _ecoTaxStatsPending = null; }); + const value = await _ecoTaxStatsPending; if (value) _ecoTaxStatsCache = { ts: now, value }; return value || { totalBlocks: 0, totalBytes: 0, totalGramsCO2: 0, @@ -736,7 +741,7 @@ async function getUserFirstBlockTs(userId) { pull( ssb.createUserStream({ id: userId, limit: 1 }), pull.drain( - (m) => { if (m && m.value && m.value.timestamp && !first) first = Number(m.value.timestamp); }, + (m) => { if (m && m.value && !first) first = Number(m.timestamp) || 0; }, () => resolve(first || 0) ) ); @@ -754,82 +759,68 @@ async function getUserArchTax(userId) { return archTaxFromDays(ageDays); } -async function getUserEngagementScore(userId) { - const ssb = await openSsb(); +let _industryModel = null; +function industryModel() { + if (_industryModel) return _industryModel; + if (!services || !services.cooler) return null; + try { _industryModel = require("./industry_model")({ cooler: services.cooler }); } catch (_) { _industryModel = null; } + return _industryModel; +} + +async function getIndustryBalance(userId) { const uid = resolveUserId(userId); - const actions = await fetchUserActions(uid); - const rawKarma = scoreFromActions(actions); - const carbonGrams = await getCarbonGramsForUser(uid).catch(() => 0); - const karmaScore = Math.max(0, Math.round(rawKarma - carbonGrams)); - - const prev = await getLastKarmaScore(uid); - const lastPublishedTimestamp = await getLastPublishedTimestamp(uid); - - const isSelf = idsEqual(uid, ssb.id); - const hasSSB = !!(ssb && ssb.publish); - - const changed = (prev === null) || (karmaScore !== prev); - const nowMs = Date.now(); - const lastMs = lastPublishedTimestamp ? new Date(lastPublishedTimestamp).getTime() : 0; - const cooldownOk = (nowMs - lastMs) >= 24 * 60 * 60 * 1000; - - if (isSelf && hasSSB && changed && cooldownOk) { - await publishKarmaScore(uid, karmaScore); + const industry = industryModel(); + if (!industry) return { received: 0, sent: 0, net: 0, networkTotal: 0 }; + const builds = await industry.listAllBuilds().catch(() => []); + let production = 0; + let earned = 0; + for (const b of (builds || [])) { + const est = parseFloat(b && b.estTotal); + if (Number.isFinite(est) && est > 0) production += est; + const mine = parseFloat(b && b.points && b.points[uid]); + if (Number.isFinite(mine) && mine > 0) earned += mine; } - return karmaScore; + return { + received: Number(earned.toFixed(6)), + sent: 0, + net: Number(earned.toFixed(6)), + networkTotal: Number(production.toFixed(6)) + }; } -async function getLastKarmaScore(userId) { +async function getSchoolBalance(userId) { + const uid = resolveUserId(userId); const ssb = await openSsb(); - if (!ssb) return null; - return new Promise((resolve) => { - const source = ssb.messagesByType - ? ssb.messagesByType({ type: "karmaScore", reverse: true }) - : ssb.createLogStream && ssb.createLogStream({ reverse: true }); - if (!source) return resolve(null); - pull( - source, - pull.filter(msg => { - const v = msg.value || msg; - const c = v.content || {}; - return c && c.type === "karmaScore" && c.userId === userId; - }), - pull.take(1), - pull.collect((err, arr) => { - if (err || !arr || !arr.length) return resolve(null); - const v = arr[0].value || arr[0]; - const c = v.content || {}; - resolve(Number(c.karmaScore) || 0); - }) - ); - }); -} - -async function getLastPublishedTimestamp(userId) { - const ssb = await openSsb(); - if (!ssb) return new Date(0).toISOString(); - const fallback = new Date(0).toISOString(); - return new Promise((resolve) => { - const source = ssb.messagesByType - ? ssb.messagesByType({ type: "karmaScore", reverse: true }) - : ssb.createLogStream && ssb.createLogStream({ reverse: true }); - if (!source) return resolve(fallback); - pull( - source, - pull.filter(msg => { - const v = msg.value || msg; - const c = v.content || {}; - return c && c.type === "karmaScore" && c.userId === userId; - }), - pull.take(1), - pull.collect((err, arr) => { - if (err || !arr || !arr.length) return resolve(fallback); - const v = arr[0].value || arr[0]; - const c = v.content || {}; - resolve(c.timestamp || fallback); - }) - ); + const collect = (type) => new Promise((resolve) => { + if (!ssb.messagesByType) return resolve([]); + pull(ssb.messagesByType({ type }), pull.collect((err, msgs) => resolve(err ? [] : msgs))); }); + const [transfers, confirms] = await Promise.all([collect("transfer"), collect("transferConfirm")]); + const confirmsByTarget = new Map(); + for (const m of confirms) { + const c = m.value && m.value.content; + if (!c || !c.target) continue; + if (!confirmsByTarget.has(c.target)) confirmsByTarget.set(c.target, new Set()); + if (m.value.author) confirmsByTarget.get(c.target).add(m.value.author); + } + const monthKey = new Date().toISOString().slice(0, 7); + let received = 0; + let lifetime = 0; + for (const m of transfers) { + const c = m.value && m.value.content; + if (!c || c.to !== uid) continue; + const tags = Array.isArray(c.tags) ? c.tags.map(t => String(t).toUpperCase()) : []; + if (!tags.includes("SCHOOL")) continue; + const signatures = new Set(Array.isArray(c.confirmedBy) ? c.confirmedBy : []); + if (c.from) signatures.add(c.from); + for (const a of (confirmsByTarget.get(m.key) || [])) signatures.add(a); + if (signatures.size < 2) continue; + const amount = parseFloat(c.amount); + if (!Number.isFinite(amount) || amount <= 0) continue; + lifetime += amount; + if (String(c.createdAt || "").slice(0, 7) === monthKey) received += amount; + } + return { received: Number(received.toFixed(6)), lifetime: Number(lifetime.toFixed(6)), net: Number(received.toFixed(6)) }; } function computePoolVars(pubBal, rules) { @@ -841,10 +832,11 @@ async function getLastPublishedTimestamp(userId) { } async function computeEpoch({ epochId, userId, rules = DEFAULT_RULES }) { - const pubBal = await safeGetBalance("pub"); + const pubBal = await safeGetBalance("pub", ENGINE_RPC_TIMEOUT_MS); const pv = computePoolVars(pubBal, rules); const addresses = await listAddressesMerged(); - const eligible = addresses.filter(a => a.address && isValidEcoinAddress(a.address)); + const pubIds = await knownPubIds(); + const eligible = addresses.filter(a => a.address && isValidEcoinAddress(a.address) && a.id !== config.keys.id && !pubIds.has(a.id)); const capUser = rules.caps?.cap_user_epoch ?? DEFAULT_RULES.caps.cap_user_epoch; const wMin = rules.caps?.w_min ?? DEFAULT_RULES.caps.w_min; const wMax = rules.caps?.w_max ?? DEFAULT_RULES.caps.w_max; @@ -899,7 +891,10 @@ async function getLastPublishedTimestamp(userId) { from: config.keys.id, to: a.user, amount: a.amount, - concept: `UBI ${eid}`, + ecoTax: a.ecoTax, + archTax: a.archTax, + totalTax: a.totalTax, + concept: `${UBI_PAYMENT_CONCEPT}${eid}`, status: "UNCLAIMED", createdAt: new Date().toISOString(), deadline: new Date(Date.now() + ((rules.graceDays ?? DEFAULT_RULES.graceDays) * 86400000)).toISOString(), @@ -924,25 +919,78 @@ async function getLastPublishedTimestamp(userId) { if (claimerId && allocation.to !== claimerId) throw new Error("This allocation is not for you."); const addr = await getUserAddress(allocation.to); if (!addr || !isValidEcoinAddress(addr)) throw new Error("No valid ECOin address registered."); - const txid = await rpcCall("sendtoaddress", [addr, allocation.amount, `UBI ${allocation.concept || "claim"}`], "pub"); - if (!txid) throw new Error("RPC sendtoaddress failed. Check PUB wallet configuration."); + const epochId = String((allocation.tags || []).find(t => String(t).startsWith("epoch:")) || "").replace(/^epoch:/, "") || epochIdNow(); + const paidKey = ubiPaidKey(epochId, allocation.to); + const pubBal = await safeGetBalance("pub", ENGINE_RPC_TIMEOUT_MS); + if (computePoolVars(pubBal, DEFAULT_RULES).available < Number(allocation.amount)) throw new Error("The PUB wallet is at its reserve."); + if (ubiAddressTaken(epochId, addr, allocation.to)) throw new Error("This ECOin address was already paid this epoch."); + if (!reserveUbiPayment(paidKey)) throw new Error("Invalid allocation or already claimed."); + settleUbiPayment(paidKey, { attempts: 1, lastAttemptAt: new Date().toISOString(), amount: allocation.amount, address: addr }); + const sent = await sendVerified(addr, allocation.amount, UBI_SEND_COMMENT, "pub"); + const txid = sent.txid; + if (!txid) { + if (sent.unknown) settleUbiPayment(paidKey, { error: "the wallet did not answer" }); + else releaseUbiPayment(paidKey); + throw new Error("RPC sendtoaddress failed. Check PUB wallet configuration."); + } + settleUbiPayment(paidKey, { txid, paidAt: new Date().toISOString() }); await transfersRepo.markClosed(transferId, txid); + console.log(`[UBI] paid ${allocation.amount} ECO to ${String(allocation.to).slice(0, 12)}… (${epochId}) tx ${txid}`); + try { await publishUbiClaimResult(transferId, epochId, txid, allocation.to, allocation.amount, addr); } catch (_) {} + try { await publishUbiTransfer({ to: allocation.to, amount: allocation.amount, epochId, txid }); } catch (_) {} return { txid }; } + async function publishUbiTransfer({ to, amount, epochId, txid }) { + const ssb = await openSsb(); + if (!ssb || !ssb.publish) return; + const now = new Date().toISOString(); + const content = { + type: "transfer", + from: config.keys.id, + to, + concept: `${UBI_PAYMENT_CONCEPT}${epochId}`, + amount: Number(amount).toFixed(6), + category: "ECONOMIC", + createdAt: now, + updatedAt: now, + deadline: null, + confirmedBy: [config.keys.id, to], + status: "CLOSED", + tags: ["UBI", `epoch:${epochId}`], + opinions: {}, + opinions_inhabitants: [], + txid + }; + const published = await new Promise((resolve, reject) => ssb.publish(content, (err, msg) => err ? reject(err) : resolve(msg))); + if (services && typeof services.notifyUbiPaid === "function") { + try { await services.notifyUbiPaid({ to, amount: Number(amount).toFixed(6), epochId, txid, transferKey: published && published.key ? published.key : null }); } catch (_) {} + } + return published; + } + async function claimUBI(userId) { const uid = resolveUserId(userId); const epochId = epochIdNow(); - const pubId = getConfiguredPubId(); - if (!pubId) throw new Error("no_pub_configured"); - const alreadyClaimed = await hasClaimedThisMonth(uid); - if (alreadyClaimed) throw new Error("already_claimed"); - const ssb = await openSsb(); - if (!ssb || !ssb.publish) throw new Error("ssb_unavailable"); - const now = new Date().toISOString(); - const claimContent = { type: "ubiClaim", pubId, epochId, claimedAt: now }; - await new Promise((resolve, reject) => ssb.publish(claimContent, (err, res) => err ? reject(err) : resolve(res))); - return { status: "claimed_pending", epochId }; + const lockKey = ubiPaidKey(epochId, uid); + if (isPubNode()) throw new Error("pub"); + if (claimsInFlight.has(lockKey)) throw new Error("already_claimed"); + claimsInFlight.add(lockKey); + try { + const pubId = await resolvePubId(); + if (!pubId) throw new Error("no_pub_configured"); + const alreadyClaimed = await hasClaimedThisMonth(uid); + if (alreadyClaimed) throw new Error("already_claimed"); + if (await hasRefusedThisMonth(uid)) throw new Error("already_refused"); + const ssb = await openSsb(); + if (!ssb || !ssb.publish) throw new Error("ssb_unavailable"); + const now = new Date().toISOString(); + const claimContent = { type: "ubiClaim", pubId, epochId, claimedAt: now }; + await new Promise((resolve, reject) => ssb.publish(claimContent, (err, res) => err ? reject(err) : resolve(res))); + return { status: "claimed_pending", epochId }; + } finally { + claimsInFlight.delete(lockKey); + } } async function updateAllocationStatus(allocationId, status, txid) { @@ -960,13 +1008,107 @@ async function getLastPublishedTimestamp(userId) { } } - async function hasClaimedThisMonth(userId) { + async function hasRefusedThisMonth(userId) { const epochId = epochIdNow(); const msgs = await scanLogStream(); + return msgs.some(m => m.value && m.value.author === userId && m.value.content && m.value.content.type === "ubiRefuse" && m.value.content.epochId === epochId); + } + + async function refuseUBI(userId) { + const uid = resolveUserId(userId); + const epochId = epochIdNow(); + if (await hasClaimedThisMonth(uid)) throw new Error("already_claimed"); + if (await hasRefusedThisMonth(uid)) throw new Error("already_refused"); + const pubId = await resolvePubId(); + const ssb = await openSsb(); + if (!ssb || !ssb.publish) throw new Error("ssb_unavailable"); + const content = { type: "ubiRefuse", pubId, epochId, refusedAt: new Date().toISOString() }; + await new Promise((resolve, reject) => ssb.publish(content, (err, res) => err ? reject(err) : resolve(res))); + return { status: "refused", epochId }; + } + + async function claimAvailability(userId) { + const uid = resolveUserId(userId); + if (isPubNode()) return { available: false, reason: "pub" }; + const pub = await discoverUbiPub(); + if (!pub.pubId || !pub.available) return { available: false, reason: "no_pub" }; + const address = await getUserAddress(uid); + const cfg = getWalletCfg("user") || {}; + if (!address || !isValidEcoinAddress(address) || !hasWalletCredentials()) return { available: false, reason: "no_wallet" }; + if (await hasClaimedThisMonth(uid)) return { available: false, reason: "claimed" }; + if (await hasRefusedThisMonth(uid)) return { available: false, reason: "refused" }; + return { available: true, epochId: epochIdNow(), pubId: pub.pubId }; + } + + function pubIdsFrom(msgs) { + const ids = new Set(); + const defaultPubId = getDefaultPubId(); + if (defaultPubId) ids.add(defaultPubId); + try { if (isPubNode() && config.keys && config.keys.id) ids.add(config.keys.id); } catch (_) {} + for (const m of msgs || []) { + const v = m.value || {}; + const c = v.content || {}; + if (c.type === "pubAvailability" && c.coin === "ECO" && v.author) ids.add(v.author); + } + return ids; + } + + async function trustedPubIds() { + const ids = pubIdsFrom([]); + const me = config.keys && config.keys.id; + const ssb = await openSsb(); + if (!ssb || !me) return ids; + const msgs = await readTyped(ssb, ["pub"], { limit: Math.max(getLogLimit(), 20000) }).catch(() => []); for (const m of msgs) { const v = m.value || {}; const c = v.content || {}; - if (c.type === "ubiClaimResult" && c.userId === userId && c.epochId === epochId) return true; + const key = c.address && typeof c.address.key === "string" ? c.address.key : ""; + if (c.type !== "pub" || v.author !== me || !/^@[A-Za-z0-9+/]+={0,2}\.ed25519$/.test(key) || ids.has(key)) continue; + if (await isFollowing(ssb, me, key)) ids.add(key); + } + if (!isPubNode()) return ids; + const announcers = await scanUbiAnnouncements().catch(() => new Map()); + const contacts = await readTyped(ssb, ["contact"], { limit: Math.max(getLogLimit(), 20000) }).catch(() => []); + const latest = new Map(); + for (const m of contacts) { + const v = m.value || {}; + const c = v.content || {}; + if (c.type !== "contact" || v.author !== me || !announcers.has(c.contact)) continue; + const prev = latest.get(c.contact); + if (!prev || (Number(v.sequence) || 0) > prev.seq) latest.set(c.contact, { seq: Number(v.sequence) || 0, c }); + } + for (const [id, { c }] of latest) if (c.following === true && c.pub !== true && c.blocking !== true) ids.add(id); + return ids; + } + + function claimAddressees(msgs) { + const addressed = new Set(); + for (const m of msgs || []) { + const v = m.value || {}; + const c = v.content || {}; + if (c.type === "ubiClaim" && v.author && c.pubId) addressed.add(`${c.epochId}:${v.author}:${c.pubId}`); + } + return addressed; + } + + const resultHonoured = (v, c, trusted, addressed) => !!v.author && (trusted.has(v.author) || addressed.has(`${c.epochId}:${c.userId}:${v.author}`)); + + async function knownPubIds() { + const ids = pubIdsFrom([]); + try { for (const id of (await scanUbiAnnouncements()).keys()) ids.add(id); } catch (_) {} + try { for (const id of await trustedPubIds()) ids.add(id); } catch (_) {} + return ids; + } + + async function hasClaimedThisMonth(userId) { + const epochId = epochIdNow(); + const msgs = await scanLogStream(); + const trusted = await trustedPubIds(); + const addressed = claimAddressees(msgs); + for (const m of msgs) { + const v = m.value || {}; + const c = v.content || {}; + if (c.type === "ubiClaimResult" && c.userId === userId && c.epochId === epochId && resultHonoured(v, c, trusted, addressed)) return true; if (c.type === "ubiClaim" && v.author === userId && c.epochId === epochId) return true; } return false; @@ -974,13 +1116,15 @@ async function getLastPublishedTimestamp(userId) { async function getUbiClaimHistory(userId) { const msgs = await scanLogStream(); + const trusted = await trustedPubIds(); + const addressed = claimAddressees(msgs); let lastClaimedDate = null; let totalClaimed = 0; let claimCount = 0; for (const m of msgs) { const v = m.value || {}; const c = v.content || {}; - if (c.type === "ubiClaimResult" && c.userId === userId) { + if (c.type === "ubiClaimResult" && c.userId === userId && resultHonoured(v, c, trusted, addressed)) { totalClaimed += Number(c.amount) || 0; claimCount += 1; const d = c.processedAt || null; @@ -991,57 +1135,368 @@ async function getLastPublishedTimestamp(userId) { } async function getUbiAllocationsFromSSB() { - const pubId = getConfiguredPubId(); + const pubId = isPubNode() ? config.keys.id : await resolvePubId(); if (!pubId) return []; const msgs = await scanLogStream(); + const pubIds = pubIdsFrom(msgs); + const trusted = await trustedPubIds(); + const addressed = claimAddressees(msgs); + const paid = new Map(); + for (const m of msgs) { + const v = m.value || {}; + const c = v.content || {}; + if (c.type === "ubiClaimResult" && c.txid && c.epochId && c.userId && resultHonoured(v, c, trusted, addressed)) paid.set(ubiPaidKey(c.epochId, c.userId), c.txid); + } const out = []; + const seen = new Map(); for (const m of msgs) { const v = m.value || {}; const c = v.content || {}; if (v.author === pubId && c && c.type === "ubiAllocation") { - out.push({ + if (!c.to || c.to === pubId || pubIds.has(c.to)) continue; + const dupKey = ubiPaidKey(c.epochId, c.to); + const prev = seen.get(dupKey); + if (prev && String(prev.createdAt || "") >= String(c.createdAt || "")) continue; + const txid = paid.get(dupKey) || null; + if (prev) out.splice(out.indexOf(prev), 1); + const entry = { id: c.allocationId, from: pubId, to: c.to, amount: c.amount, concept: c.concept, epochId: c.epochId, - status: c.status || "UNCLAIMED", + status: txid ? "CLOSED" : (c.status || "UNCLAIMED"), + txid, createdAt: c.createdAt || new Date().toISOString() - }); + }; + seen.set(dupKey, entry); + out.push(entry); } } return out; } - async function publishPubAvailability() { - if (!isPubNode()) return; - const balance = await safeGetBalance("pub"); - const floor = Math.max(1, DEFAULT_RULES?.caps?.floor_user ?? 1); - const available = Number(balance) >= floor; - const ssb = await openSsb(); - if (!ssb || !ssb.publish) return; - const content = { type: "pubAvailability", available, coin: "ECO", timestamp: Date.now() }; - await new Promise((resolve, reject) => ssb.publish(content, (err, res) => err ? reject(err) : resolve(res))); - return available; - } - - async function getPubAvailabilityFromSSB() { - const pubId = getConfiguredPubId(); - if (!pubId) return false; - const msgs = await scanLogStream(); - let latest = null; + let lastAnnounce = null; + async function loadLastAnnounce(ssb) { + if (lastAnnounce) return lastAnnounce; + const msgs = await readTyped(ssb, ["pubAvailability"], { limit: Math.max(getLogLimit(), 5000) }); + let best = { available: null, timestamp: 0 }; for (const m of msgs) { const v = m.value || {}; const c = v.content || {}; - if (v.author === pubId && c && c.type === "pubAvailability" && c.coin === "ECO") { - if (!latest || (Number(c.timestamp) || 0) > (Number(latest.timestamp) || 0)) latest = c; - } + if (v.author !== config.keys.id || !c || c.type !== "pubAvailability") continue; + const ts = Number(c.timestamp) || 0; + if (ts > best.timestamp) best = { available: !!c.available, timestamp: ts, balance: Number(c.balance) || 0 }; } - return !!(latest && latest.available); + lastAnnounce = best; + return lastAnnounce; } - async function listBanking(filter = "overview", userId) { + async function scanUbiAnnouncements() { + const ssb = await openSsb(); + if (!ssb) return new Map(); + const msgs = await readTyped(ssb, ["pubAvailability"], { limit: Math.max(getLogLimit(), 5000) }); + const latestByPub = new Map(); + for (const m of msgs) { + const v = m.value || {}; + const c = v.content || {}; + if (!c || c.type !== "pubAvailability" || c.coin !== "ECO") continue; + const ts = Number(c.timestamp) || Number(v.timestamp) || 0; + const prev = latestByPub.get(v.author); + if (!prev || ts > prev.timestamp) latestByPub.set(v.author, { pubId: v.author, available: !!c.available, timestamp: ts, balance: Number(c.balance) || 0, balanceKnown: c.balance !== undefined && c.balance !== null && Number.isFinite(Number(c.balance)), pool: Number(c.pool) || 0, address: isValidEcoinAddress(c.address) ? String(c.address) : null }); + } + return latestByPub; + } + + async function networkPubIds() { + const trusted = await trustedPubIds(); + const ids = new Set(trusted); + const ssb = await openSsb(); + if (!ssb) return ids; + const contacts = await readTyped(ssb, ["contact"], { limit: Math.max(getLogLimit(), 20000) }).catch(() => []); + const latest = new Map(); + for (const m of contacts) { + const v = m.value || {}; + const c = v.content || {}; + if (c.type !== "contact" || !trusted.has(v.author) || typeof c.contact !== "string") continue; + const k = `${v.author} ${c.contact}`; + const seq = Number(v.sequence) || 0; + const prev = latest.get(k); + if (!prev || seq > prev.seq) latest.set(k, { seq, contact: c.contact, federated: c.following === true && c.pub !== true && c.blocking !== true }); + } + for (const { contact, federated } of latest.values()) if (federated) ids.add(contact); + return ids; + } + + async function listNetworkUbiPubs() { + const latestByPub = await scanUbiAnnouncements(); + const known = await networkPubIds(); + const out = []; + for (const cand of latestByPub.values()) { + if (!known.has(cand.pubId) || Date.now() - cand.timestamp > PUB_ANNOUNCE_MAX_AGE_MS || !isValidEcoinAddress(cand.address) || !cand.balanceKnown) continue; + out.push(cand); + } + return out; + } + + async function listUbiPubsDetailed() { + const pubs = await listNetworkUbiPubs(); + if (!pubs.length) return []; + const ssb = await openSsb(); + const { transfers } = ssb ? await readUbiLedger(ssb) : { transfers: [] }; + const defaultPubId = getDefaultPubId(); + const connected = (await discoverUbiPub()).pubId; + const me = isPubNode() && config.keys ? config.keys.id : null; + return pubs.map(p => { + const payouts = transfers.filter(t => t.author === p.pubId && isUbiPayoutConcept(t.concept)); + const lastPayoutAt = payouts.reduce((acc, t) => Math.max(acc, Date.parse(t.createdAt || "") || 0), 0); + const paidOut = payouts.reduce((acc, t) => acc + (Number(t.amount) || 0), 0); + const address = p.address || null; + return { ...p, address, lastPayoutAt, paidOut: Number(paidOut.toFixed(6)), payouts: payouts.length, isDefault: p.pubId === defaultPubId, isConnected: p.pubId === connected, isThisNode: !!me && p.pubId === me }; + }).sort((a, b) => (Number(b.isThisNode || b.isConnected) - Number(a.isThisNode || a.isConnected)) || (b.balance - a.balance)); + } + + async function listUbiRebalanceSeries(range = "all", explicit = true) { + const ssb = await openSsb(); + if (!ssb) return { points: [], hasAnyData: false, range: normalizeRange(range), bucket: "month" }; + const { transfers } = await readUbiLedger(ssb); + const known = await networkPubIds(); + const events = transfers + .filter(t => String(t.concept || "").startsWith(REBALANCE_CONCEPT) && known.has(t.author)) + .map(t => ({ ts: Date.parse(t.createdAt || "") || 0, rebalanced: Number(t.amount) || 0 })) + .filter(e => e.ts > 0) + .sort((a, b) => a.ts - b.ts); + return bucketEvents(events, range, explicit, ["rebalanced"]); + } + + async function listUbiCharts(range = "all", explicit = true) { + const ssb = await openSsb(); + if (!ssb) return { monthly: [], pool: [] }; + const { transfers } = await readUbiLedger(ssb); + const byMonth = new Map(); + for (const t of transfers) { + if (!isUbiPayoutConcept(t.concept)) continue; + const month = (String(t.concept).match(/(\d{4}-\d{2})/) || [])[1] || String(t.createdAt || "").slice(0, 7); + if (!month) continue; + const cur = byMonth.get(month) || { month, amount: 0, count: 0 }; + cur.amount += Number(t.amount) || 0; + cur.count += 1; + byMonth.set(month, cur); + } + const monthly = Array.from(byMonth.values()).sort((a, b) => a.month.localeCompare(b.month)).map(m => ({ ...m, amount: Number(m.amount.toFixed(6)) })); + const msgs = await readTyped(ssb, ["pubAvailability"], { limit: Math.max(getLogLimit(), 5000) }); + const trusted = await trustedPubIds(); + const events = msgs + .filter(m => m.value && m.value.content && m.value.content.type === "pubAvailability" && m.value.content.coin === "ECO" && trusted.has(m.value.author) && Number.isFinite(Number(m.value.content.balance))) + .map(m => ({ ts: Number(m.value.content.timestamp) || Number(m.value.timestamp) || 0, pubId: m.value.author, balance: Number(m.value.content.balance) })) + .filter(x => x.ts > 0) + .sort((a, b) => a.ts - b.ts); + const latestByPub = new Map(); + const pool = []; + for (const e of events) { + latestByPub.set(e.pubId, e.balance); + let total = 0; + for (const v of latestByPub.values()) total += v; + const last = pool[pool.length - 1]; + if (last && last.ts === e.ts) last.balance = Number(total.toFixed(6)); + else pool.push({ ts: e.ts, balance: Number(total.toFixed(6)) }); + } + const fullPool = pool.some(p => p.balance > 0) ? pool : []; + const monthTs = (m) => Date.parse(`${m}-01T00:00:00Z`) || 0; + const monthlyTs = monthly.map(m => ({ ...m, ts: monthTs(m.month) })); + const poolRange = explicit ? normalizeRange(range) : widenRange(fullPool, range); + const monthlyRange = explicit ? normalizeRange(range) : widenRange(monthlyTs, range); + return { + monthly: filterByRange(monthlyTs, monthlyRange).slice(-120), + pool: filterByRange(fullPool, poolRange).slice(-120), + hasMonthly: monthlyTs.length >= 2, + hasPool: fullPool.length >= 2, + poolRange, + monthlyRange, + pubCount: latestByPub.size + }; + } + + function bucketEvents(events, range, explicit, fields) { + const effective = explicit ? normalizeRange(range) : widenRange(events, range); + const inRange = filterByRange(events, effective); + const bucketMs = effective === "today" ? 60 * 60 * 1000 : (effective === "week" || effective === "month") ? ONE_DAY_MS : 0; + const bucketOf = (ts) => { + if (bucketMs) return Math.floor(ts / bucketMs) * bucketMs; + const d = new Date(ts); + return Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), 1); + }; + const buckets = new Map(); + for (const e of inRange) { + const b = bucketOf(e.ts); + const cur = buckets.get(b) || fields.reduce((acc, f) => ({ ...acc, [f]: 0 }), { ts: b }); + for (const f of fields) cur[f] += Number(e[f]) || 0; + buckets.set(b, cur); + } + let points = Array.from(buckets.values()).sort((a, b) => a.ts - b.ts) + .map(p => fields.reduce((acc, f) => ({ ...acc, [f]: Number(Number(p[f]).toFixed(6)) }), { ts: p.ts })) + .slice(-240); + if (points.length === 1) { + const span = RANGE_MS[effective]; + const baseTs = Number.isFinite(span) ? Date.now() - span : points[0].ts - (bucketMs || 30 * ONE_DAY_MS); + points = [fields.reduce((acc, f) => ({ ...acc, [f]: 0 }), { ts: Math.min(baseTs, points[0].ts - 1) }), ...points]; + } + return { points, range: effective, bucket: bucketMs === 60 * 60 * 1000 ? "hour" : bucketMs ? "day" : "month", hasAnyData: events.length >= 1 }; + } + + async function listUbiPaymentsSeries(range = "all", explicit = true) { + const ssb = await openSsb(); + if (!ssb) return { points: [], hasAnyData: false, range: normalizeRange(range), bucket: "month" }; + const { transfers } = await readUbiLedger(ssb); + const events = transfers + .filter(t => isUbiPayoutConcept(t.concept)) + .map(t => ({ ts: Date.parse(t.createdAt || "") || 0, distributed: Number(t.amount) || 0 })) + .filter(e => e.ts > 0) + .sort((a, b) => a.ts - b.ts); + return bucketEvents(events, range, explicit, ["distributed"]); + } + + async function listWealthSeries(range = "all", explicit = true) { + const ssb = await openSsb(); + if (!ssb) return { points: [], totals: { distributed: 0, taxes: 0 } }; + const { transfers } = await readUbiLedger(ssb); + const allocs = await readTyped(ssb, ["ubiAllocation"], { limit: Math.max(getLogLimit(), 20000) }); + const events = []; + for (const t of transfers) { + if (!isUbiPayoutConcept(t.concept)) continue; + const ts = Date.parse(t.createdAt || "") || 0; + if (ts) events.push({ ts, distributed: Number(t.amount) || 0, taxes: 0 }); + } + for (const m of allocs) { + const c = m.value && m.value.content; + if (!c || c.type !== "ubiAllocation") continue; + const ts = Date.parse(c.createdAt || "") || Number(m.value.timestamp) || 0; + if (ts) events.push({ ts, distributed: 0, taxes: Number(c.totalTax) || 0 }); + } + events.sort((a, b) => a.ts - b.ts); + const totals = events.reduce((acc, e) => ({ distributed: acc.distributed + e.distributed, taxes: acc.taxes + e.taxes }), { distributed: 0, taxes: 0 }); + totals.distributed = Number(totals.distributed.toFixed(6)); + totals.taxes = Number(totals.taxes.toFixed(6)); + return { ...bucketEvents(events, range, explicit, ["distributed", "taxes"]), totals }; + } + + async function listNetworkEpochs() { + const ssb = await openSsb(); + if (!ssb) return []; + const byEpoch = new Map(); + const bump = (id, amount, to) => { + if (!id) return; + const cur = byEpoch.get(id) || { id, pool: 0, recipients: new Set() }; + cur.pool += Number(amount) || 0; + if (to) cur.recipients.add(to); + byEpoch.set(id, cur); + }; + const allocs = await getUbiAllocationsFromSSB().catch(() => []); + for (const a of allocs) bump(String(a.epochId || ""), a.amount, a.to); + if (!byEpoch.size) { + const { transfers } = await readUbiLedger(ssb); + for (const t of transfers) { + if (!isUbiPayoutConcept(t.concept)) continue; + const tag = (t.tags || []).find(x => /^epoch:\d{4}-\d{2}$/i.test(String(x))); + const id = tag ? String(tag).slice(6) : ((String(t.concept || "").match(/(\d{4}-\d{2})/) || [])[1] || ""); + bump(id, t.amount, t.to); + } + } + return Array.from(byEpoch.values()) + .map(e => ({ id: e.id, pool: Number(e.pool.toFixed(6)), recipients: e.recipients.size })) + .sort((a, b) => String(b.id).localeCompare(String(a.id))); + } + + async function sampleUserFunds() { + if (!hasWalletCredentials()) return false; + const hist = readFundsHistory(); + const last = Array.isArray(hist) && hist.length ? hist[hist.length - 1] : null; + if (last && Date.now() - Number(last.ts) < ECO_HISTORY_MIN_GAP_MS) return false; + try { + const balance = Number(await rpcCall("getbalance", [], "user")); + if (!Number.isFinite(balance)) return false; + appendFundsHistory({ ts: Date.now(), balance }); + return true; + } catch (_) { return false; } + } + + function listFundsHistory(range = "all", explicit = true) { + const all = readFundsHistory() + .map(s => ({ ts: Number(s.ts) || 0, balance: Number(s.balance) || 0 })) + .filter(x => x.ts > 0) + .sort((a, b) => a.ts - b.ts); + const effective = explicit ? normalizeRange(range) : widenRange(all, range); + return { points: filterByRange(all, effective).slice(-240), hasAny: all.length >= 2, range: effective }; + } + + async function listUbiPubs() { + const latestByPub = await scanUbiAnnouncements(); + const trusted = await trustedPubIds(); + const out = []; + for (const cand of latestByPub.values()) { + if (!trusted.has(cand.pubId) || Date.now() - cand.timestamp > PUB_ANNOUNCE_MAX_AGE_MS) continue; + out.push(cand); + } + return out.sort((a, b) => b.timestamp - a.timestamp); + } + + async function publishPubAvailability() { + if (!isPubNode()) return; + const raw = await tryGetBalance("pub", ENGINE_RPC_TIMEOUT_MS); + const balance = raw === null ? 0 : raw; + const floor = Math.max(1, DEFAULT_RULES?.caps?.floor_user ?? 1); + const pv = computePoolVars(balance, DEFAULT_RULES); + const available = raw !== null && pv.available >= floor; + const ssb = await openSsb(); + if (!ssb || !ssb.publish) return; + const last = await loadLastAnnounce(ssb); + const stale = Date.now() - last.timestamp > PUB_ANNOUNCE_REFRESH_MS; + const announcedBalance = Number(pv.available.toFixed(6)); + const balanceMoved = raw !== null && Math.abs(announcedBalance - (Number(last.balance) || 0)) > Math.max(1, (Number(last.balance) || 0) * 0.2); + if (last.available === available && !stale && !balanceMoved) return available; + const address = await getAnyWalletAddress().catch(() => null); + const content = { type: "pubAvailability", available, coin: "ECO", ...(raw === null ? {} : { balance: announcedBalance, pool: Number(pv.pool.toFixed(6)) }), address: isValidEcoinAddress(address) ? address : undefined, timestamp: Date.now() }; + await new Promise((resolve, reject) => ssb.publish(content, (err, res) => err ? reject(err) : resolve(res))); + lastAnnounce = { available, timestamp: content.timestamp, balance: raw === null ? 0 : announcedBalance }; + console.log(`[UBI] announced ${available ? "available" : "unavailable"} (PUB wallet ${Number(balance).toFixed(6)} ECO)`); + return available; + } + + let discoveredPub = { pubId: "", available: false, timestamp: 0, checkedAt: 0 }; + async function discoverUbiPub() { + if (Date.now() - discoveredPub.checkedAt < PUB_DISCOVERY_TTL_MS) return discoveredPub; + const latestByPub = await scanUbiAnnouncements(); + const trusted = await trustedPubIds(); + const defaultPubId = getDefaultPubId(); + const fresh = (cand) => cand && Date.now() - cand.timestamp <= PUB_ANNOUNCE_MAX_AGE_MS; + const defaultCand = latestByPub.get(defaultPubId) || null; + let best = null; + if (fresh(defaultCand) && defaultCand.available) best = defaultCand; + else { + for (const cand of latestByPub.values()) { + if (!trusted.has(cand.pubId) || !fresh(cand) || !cand.available) continue; + if (!best || cand.timestamp > best.timestamp) best = cand; + } + } + if (!best && defaultPubId) best = { pubId: defaultPubId, available: false, timestamp: (defaultCand && defaultCand.timestamp) || 0 }; + discoveredPub = { pubId: (best && best.pubId) || "", available: !!(best && best.available), timestamp: (best && best.timestamp) || 0, balance: (best && best.balance) || 0, pool: (best && best.pool) || 0, checkedAt: Date.now() }; + return discoveredPub; + } + + async function resolvePubId() { + return (await discoverUbiPub()).pubId; + } + + async function getPubAvailabilityFromSSB() { + const pubId = await resolvePubId(); + if (!pubId) return false; + return (await discoverUbiPub()).available; + } + + async function listBanking(filter = "overview", userId, opts = {}) { + const explicitRange = !!opts.range; + const range = normalizeRange(opts.range); const uid = resolveUserId(userId); const epochId = epochIdNow(); let pubBalance = 0; @@ -1050,8 +1505,7 @@ async function getLastPublishedTimestamp(userId) { if (isPubNode()) { pubBalance = await safeGetBalance("pub"); const floor = Math.max(1, DEFAULT_RULES?.caps?.floor_user ?? 1); - ubiAvailable = Number(pubBalance) >= floor; - try { await publishPubAvailability(); } catch (_) {} + ubiAvailable = computePoolVars(Number(pubBalance) || 0, DEFAULT_RULES).available >= floor; const all = await transfersRepo.listByTag("UBI"); allocations = all.map(t => ({ id: t.id, concept: t.concept, from: t.from, to: t.to, amount: t.amount, status: t.status, @@ -1061,37 +1515,82 @@ async function getLastPublishedTimestamp(userId) { ubiAvailable = await getPubAvailabilityFromSSB(); allocations = await getUbiAllocationsFromSSB(); } - const userBalance = await safeGetBalance("user"); - const epochs = await epochsRepo.list(); + let userBalance = 0; + if (hasWalletCredentials()) { + try { + userBalance = Number(await rpcCall("getbalance", [], "user")) || 0; + appendFundsHistory({ ts: Date.now(), balance: userBalance }); + } catch (_) { userBalance = 0; } + } + const industryBal = await getIndustryBalance(uid).catch(() => ({ received: 0, sent: 0, net: 0, networkTotal: 0 })); + const schoolBal = await getSchoolBalance(uid).catch(() => ({ received: 0, lifetime: 0, net: 0 })); + let epochs = await epochsRepo.list(); + if (!epochs.length) epochs = await listNetworkEpochs().catch(() => []); let computed = null; - try { computed = await computeEpoch({ epochId, userId: uid, rules: DEFAULT_RULES }); } catch {} + if (filter === "overview" || isPubNode()) { + try { computed = await computeEpoch({ epochId, userId: uid, rules: DEFAULT_RULES }); } catch {} + } const pv = computePoolVars(pubBalance, DEFAULT_RULES); const actions = await fetchUserActions(uid); const rawScore = scoreFromActions(actions); const carbonGramsForScore = await getCarbonGramsForUser(uid).catch(() => 0); const engagementScore = Math.max(0, Math.round(rawScore - carbonGramsForScore)); - const poolForEpoch = computed?.epoch?.pool || pv.pool || 0; + let poolForEpoch = computed?.epoch?.pool || pv.pool || 0; + if (!isPubNode()) { + const info = await discoverUbiPub(); + poolForEpoch = Number(info.pool) || computePoolVars((Number(info.balance) || 0) + (DEFAULT_RULES.reserveMin || 0), DEFAULT_RULES).pool || 0; + } const futureUBI = Number(((engagementScore / 100) * poolForEpoch).toFixed(6)); const addresses = await listAddressesMerged(); const alreadyClaimed = await hasClaimedThisMonth(uid); - const pubId = getConfiguredPubId(); + const pubId = isPubNode() ? config.keys.id : await resolvePubId(); + const pubLastSeen = isPubNode() ? (lastAnnounce ? lastAnnounce.timestamp : 0) : (await discoverUbiPub()).timestamp; const userAddress = await getUserAddress(uid); const userWalletCfg = getWalletCfg("user") || {}; - const hasValidWallet = !!(userAddress && isValidEcoinAddress(userAddress) && userWalletCfg.url); + const hasValidWallet = !!(userAddress && isValidEcoinAddress(userAddress) && hasWalletCredentials()); + const addressPublished = await hasPublishedAddress(uid).catch(() => false); const summary = { userBalance, + industryBalance: industryBal.net, + schoolBalance: schoolBal.net, + schoolLifetime: schoolBal.lifetime, + industryNetworkTotal: industryBal.networkTotal, + industryReceived: industryBal.received, + industrySent: industryBal.sent, epochId, pool: poolForEpoch, weightsSum: computed?.epoch?.weightsSum || 0, userEngagementScore: engagementScore, futureUBI, alreadyClaimed, + alreadyRefused: await hasRefusedThisMonth(uid).catch(() => false), pubId, + wealthTotals: (await listWealthSeries("all").catch(() => ({ totals: { distributed: 0, taxes: 0 } }))).totals, + pubLastSeen, + pubBalance: isPubNode() ? pubBalance : null, + addressPublished, hasValidWallet, ubiAvailability: ubiAvailable ? "OK" : "NO_FUNDS" }; const exchange = await calculateEcoinValue(); - const exchangeHistory = readEcoHistory(); + if (filter === 'exchange') { + const pubs = await listUbiPubs().catch(() => []); + exchange.pubsSupply = Number(pubs.reduce((acc, p) => acc + (Number(p.balance) || 0), 0).toFixed(6)); + const lastSample = readEcoHistory().filter(x => Number(x.currentSupply) > 0).pop(); + const generated = Number(exchange.currentSupply) > 0 ? Number(exchange.currentSupply) : (lastSample ? Number(lastSample.currentSupply) : 0); + exchange.existingSupply = generated > 0 ? Number(generated.toFixed(6)) : null; + exchange.holdingSupply = generated > 0 ? Number(Math.max(0, generated - exchange.pubsSupply).toFixed(6)) : null; + } + const fullHistory = readEcoHistory(); + const valueRange = explicitRange ? range : widenRange(fullHistory, range); + let exchangeHistory = filterByRange(fullHistory, valueRange); + if (exchangeHistory.length === 1 && fullHistory.length > 1) { + const firstTs = Number(exchangeHistory[0].ts) || 0; + const before = fullHistory.filter(x => Number(x.ts) < firstTs).pop(); + const span = RANGE_MS[normalizeRange(valueRange)]; + if (before) exchangeHistory = [{ ...before, ts: Number.isFinite(span) ? Math.max(Number(before.ts), Date.now() - span) : Number(before.ts) }, ...exchangeHistory]; + } + const valueHasAnyData = Array.isArray(fullHistory) && fullHistory.length >= 2; let taxStats = null; let userEcoinTax = 0; if (filter === 'taxes' || filter === 'exchange' || filter === 'overview') { @@ -1104,8 +1603,15 @@ async function getLastPublishedTimestamp(userId) { try { userArchTax = await getUserArchTax(uid); } catch (_) { userArchTax = 0; } } const userTotalTax = (userEcoinTax || 0) + (userArchTax || 0); + const ubiPubs = filter === 'ubi' ? await listUbiPubsDetailed().catch(() => []) : []; + const ubiCharts = filter === 'ubi' ? await listUbiCharts(range, explicitRange).catch(() => null) : null; + const karmaHistory = filter === 'overview' ? await listKarmaHistory(uid, range, explicitRange).catch(() => ({ points: [], hasAny: false, range })) : { points: [], hasAny: false, range }; + const fundsHistory = filter === 'overview' ? listFundsHistory(range, explicitRange) : { points: [], hasAny: false, range }; + const wealth = filter === 'exchange' || filter === 'overview' ? await listWealthSeries(range, explicitRange).catch(() => ({ points: [], totals: { distributed: 0, taxes: 0 } })) : { points: [], totals: { distributed: 0, taxes: 0 } }; + const ubiPayments = filter === 'ubi' || filter === 'overview' ? await listUbiPaymentsSeries(range, explicitRange).catch(() => ({ points: [], hasAnyData: false })) : { points: [], hasAnyData: false }; + const ubiRebalances = filter === 'ubi' ? await listUbiRebalanceSeries(range, explicitRange).catch(() => ({ points: [], hasAnyData: false })) : { points: [], hasAnyData: false }; return { - summary, allocations, epochs, rules: DEFAULT_RULES, taxRules, addresses, exchange, exchangeHistory, taxStats, + summary, allocations, epochs, rules: DEFAULT_RULES, taxRules, addresses, exchange, exchangeHistory, taxStats, ubiPubs, ubiCharts, karmaHistory, fundsHistory, wealth, ubiPayments, ubiRebalances, range, valueRange, valueHasAnyData, userEcoinTax: Number((userEcoinTax || 0).toFixed(6)), userArchTax: Number((userArchTax || 0).toFixed(6)), userTotalTax: Number(userTotalTax.toFixed(6)) @@ -1121,17 +1627,27 @@ async function getLastPublishedTimestamp(userId) { async function getEpochById(id) { const existing = await epochsRepo.get(id); if (existing) return existing; - const all = await transfersRepo.listAll(); - const filtered = all.filter(t => (t.tags || []).includes(`epoch:${id}`)); - const pool = filtered.reduce((s, t) => s + Number(t.amount || 0), 0); - return { id, pool, weightsSum: 0, rules: DEFAULT_RULES, hash: "-" }; + const allocations = await listEpochAllocations(id).catch(() => []); + if (!allocations.length) return null; + const pool = allocations.reduce((s, t) => s + Number(t.amount || 0), 0); + return { id, pool: Number(pool.toFixed(6)), recipients: allocations.length, rules: DEFAULT_RULES }; } async function listEpochAllocations(id) { const all = await transfersRepo.listAll(); - return all.filter(t => (t.tags || []).includes(`epoch:${id}`)).map(t => ({ + const local = all.filter(t => (t.tags || []).includes(`epoch:${id}`)).map(t => ({ id: t.id, concept: t.concept, from: t.from, to: t.to, amount: t.amount, status: t.status, createdAt: t.createdAt || new Date().toISOString(), txid: t.txid })); + if (local.length) return local; + const published = await getUbiAllocationsFromSSB().catch(() => []); + const fromNetwork = published.filter(a => String(a.epochId || "") === String(id)); + if (fromNetwork.length) return fromNetwork; + const ssb = await openSsb(); + if (!ssb) return []; + const { transfers } = await readUbiLedger(ssb); + return transfers + .filter(t => isUbiPayoutConcept(t.concept) && (String(t.concept || "").includes(id) || (t.tags || []).includes(`epoch:${id}`))) + .map(t => ({ id: t.key, concept: t.concept, from: t.from, to: t.to, amount: t.amount, status: t.status || "UNCONFIRMED", createdAt: t.createdAt || new Date().toISOString(), txid: t.txid })); } let genesisTimeCache = null; @@ -1156,6 +1672,63 @@ async function getLastPublishedTimestamp(userId) { } catch (_) { return 0; } } + async function getRecentBlockSeconds(blocks, window = 720) { + if (!blocks || blocks <= window + 1) return 0; + try { + const hCur = await rpcCall("getblockhash", [blocks]); + const hOld = await rpcCall("getblockhash", [blocks - window]); + if (!hCur || !hOld) return 0; + const bCur = await rpcCall("getblock", [hCur]); + const bOld = await rpcCall("getblock", [hOld]); + const elapsed = (bCur?.time || 0) - (bOld?.time || 0); + return elapsed > 0 ? elapsed / window : 0; + } catch (_) { return 0; } + } + + function observedIssuancePerHour(currentSupply) { + const hist = readEcoHistory(); + if (!Array.isArray(hist) || hist.length < 2 || !(currentSupply > 0)) return null; + const now = Date.now(); + const windowStart = now - 30 * ONE_DAY_MS; + const base = hist.find(s => Number(s.ts) >= windowStart && Number(s.currentSupply) > 0) || hist.find(s => Number(s.currentSupply) > 0); + if (!base) return null; + const spanMs = now - Number(base.ts); + if (spanMs < 6 * 60 * 60 * 1000) return null; + const minted = currentSupply - Number(base.currentSupply); + if (!Number.isFinite(minted) || minted < 0) return null; + return minted / (spanMs / (3600 * 1000)); + } + + function observedInflationFromHistory(currentSupply) { + const hist = readEcoHistory(); + if (!Array.isArray(hist) || hist.length < 2 || !(currentSupply > 0)) return null; + const now = Date.now(); + const minSpanMs = 6 * 60 * 60 * 1000; + const oldEnough = (s) => Number(s.currentSupply) > 0 && now - Number(s.ts) >= minSpanMs; + const base = hist.find(s => Number(s.ts) >= now - 7 * ONE_DAY_MS && oldEnough(s)) + || hist.find(s => Number(s.ts) >= now - 30 * ONE_DAY_MS && oldEnough(s)) + || hist.find(oldEnough); + if (!base) return null; + const spanMs = now - Number(base.ts); + const growth = (currentSupply - Number(base.currentSupply)) / Number(base.currentSupply); + return growth * (365 * ONE_DAY_MS / spanMs) * 100; + } + + async function ubiPerHourPerInhabitant() { + const pool = isPubNode() + ? computePoolVars(await safeGetBalance("pub"), DEFAULT_RULES).pool + : (Number((await discoverUbiPub()).pool) || 0); + const pubIds = await knownPubIds(); + const addresses = await listAddressesMerged().catch(() => []); + const eligible = addresses.filter(a => a.address && isValidEcoinAddress(a.address) && !pubIds.has(a.id) && !(isPubNode() && a.id === config.keys.id)).length; + const cap = DEFAULT_RULES.caps.cap_user_epoch; + const floor = DEFAULT_RULES.caps.floor_user ?? 1; + const perInhabitant = pool > 0 && eligible > 0 ? Math.max(floor, Math.min(cap, pool / eligible)) : 0; + const now = new Date(); + const hoursInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate() * 24; + return perInhabitant / hoursInMonth; + } + async function calculateEcoinValue() { const totalSupply = 25500000; let circulatingSupply = 0; @@ -1170,12 +1743,18 @@ async function getLastPublishedTimestamp(userId) { const mining = await rpcCall("getmininginfo", []); blockValueEco = (mining?.blockvalue || 0) / 1e8; } catch (_) {} - const avgSec = await getAvgBlockSeconds(blocks); - const ecoValuePerHour = avgSec > 0 ? (3600 / avgSec) * blockValueEco : 0; + const recentSec = await getRecentBlockSeconds(blocks); + const avgSec = recentSec > 0 ? recentSec : await getAvgBlockSeconds(blocks); + const nominalPerHour = avgSec > 0 ? (3600 / avgSec) * blockValueEco : 0; + const observedPerHour = isSynced ? observedIssuancePerHour(circulatingSupply) : null; + const issuancePerHour = observedPerHour !== null ? observedPerHour : nominalPerHour; + const ecoValuePerHour = await ubiPerHourPerInhabitant().catch(() => 0); const maturity = totalSupply > 0 ? circulatingSupply / totalSupply : 0; const ecoTimeMs = maturity * 3600 * 1000; - const annualIssuance = ecoValuePerHour * 24 * 365; - const inflationFactor = circulatingSupply > 0 ? (annualIssuance / circulatingSupply) * 100 : 0; + const annualIssuance = issuancePerHour * 24 * 365; + const inflationIssuance = circulatingSupply > 0 ? (annualIssuance / circulatingSupply) * 100 : 0; + const inflationObserved = isSynced ? observedInflationFromHistory(circulatingSupply) : null; + const inflationFactor = inflationObserved !== null && Number.isFinite(inflationObserved) ? inflationObserved : inflationIssuance; const inflationMonthly = inflationFactor / 12; const result = { ecoValue: Number(ecoValuePerHour.toFixed(6)), @@ -1183,6 +1762,8 @@ async function getLastPublishedTimestamp(userId) { totalSupply, inflationFactor: Number(inflationFactor.toFixed(2)), inflationMonthly: Number(inflationMonthly.toFixed(2)), + inflationIssuance: Number(inflationIssuance.toFixed(2)), + inflationObserved: inflationObserved === null ? null : Number(inflationObserved.toFixed(2)), currentSupply: circulatingSupply, isSynced }; @@ -1190,6 +1771,7 @@ async function getLastPublishedTimestamp(userId) { appendEcoHistory({ ts: Date.now(), ecoValue: result.ecoValue, + ecoTimeMs: result.ecoTimeMs, currentSupply: result.currentSupply, inflationFactor: result.inflationFactor, blockValueEco: Number(blockValueEco.toFixed(8)), @@ -1200,15 +1782,25 @@ async function getLastPublishedTimestamp(userId) { return result; } + const perRequest = (key, fn) => { + const store = requestScope.getStore(); + if (!store) return fn(); + const memo = store.bankingMemo || (store.bankingMemo = new Map()); + if (!memo.has(key)) memo.set(key, fn()); + return memo.get(key); + }; + async function getBankingData(userId) { - const ecoValue = await calculateEcoinValue(); + const ecoValue = await perRequest("ecoinValue", () => calculateEcoinValue()); const karmaScore = await getUserEngagementScore(userId); + const received = await karma.getKarmaBreakdown(userId); + const karmaReceived = { positive: received.positive, negative: received.negative }; let estimatedUBI = 0; try { - const pubBal = isPubNode() ? await safeGetBalance("pub") : 0; + const pubBal = isPubNode() ? await perRequest("pubBalance", () => safeGetBalance("pub")) : 0; const pv = computePoolVars(pubBal, DEFAULT_RULES); const pool = pv.pool || 0; - const addresses = await listAddressesMerged(); + const addresses = await perRequest("addresses", () => listAddressesMerged()); const eligible = addresses.filter(a => a.address && isValidEcoinAddress(a.address)); const wMin = DEFAULT_RULES.caps?.w_min ?? 0.2; const wMax = DEFAULT_RULES.caps?.w_max ?? 6; @@ -1233,6 +1825,7 @@ async function getLastPublishedTimestamp(userId) { return { ecoValue, karmaScore, + karmaReceived, estimatedUBI, estimatedUBIBeforeTax: Number(estimatedUBIBeforeTax.toFixed(6)), userEcoinTax: Number(userEcoinTax.toFixed(6)), @@ -1264,6 +1857,9 @@ async function getLastPublishedTimestamp(userId) { allocationId: allocation.id, to: allocation.to, amount: allocation.amount, + ecoTax: Number(allocation.ecoTax) || 0, + archTax: Number(allocation.archTax) || 0, + totalTax: Number(allocation.totalTax) || 0, concept: allocation.concept, epochId: epochTag ? epochTag.slice(6) : "", status: "UNCLAIMED", @@ -1279,87 +1875,367 @@ async function getLastPublishedTimestamp(userId) { return new Promise((resolve, reject) => ssb.publish(content, (err, res) => err ? reject(err) : resolve(res))); } - async function publishUbiClaimResult(allocationId, epochId, txid, userId, amount) { + async function publishUbiClaimResult(allocationId, epochId, txid, userId, amount, address) { const ssb = await openSsb(); if (!ssb) return; - const content = { type: "ubiClaimResult", allocationId, epochId, txid, userId, amount, processedAt: new Date().toISOString() }; + const content = { type: "ubiClaimResult", allocationId, epochId, txid, userId, amount, ...(address ? { address: String(address) } : {}), processedAt: new Date().toISOString() }; return new Promise((resolve, reject) => ssb.publish(content, (err, res) => err ? reject(err) : resolve(res))); } + async function isEligibleClaimant(userId) { + if ((isPubNode() && userId === config.keys.id) || (await knownPubIds()).has(userId)) return { ok: false, reason: "a PUB cannot claim the UBI" }; + const addr = await getUserAddress(userId); + if (!addr || !isValidEcoinAddress(addr)) return { ok: false, reason: "no published ECOin address" }; + const firstTs = await getUserFirstBlockTs(userId).catch(() => 0); + if (!firstTs || Date.now() - firstTs < CLAIMANT_MIN_AGE_MS) return { ok: false, reason: "feed younger than 30 days" }; + const score = await getUserEngagementScore(userId).catch(() => 0); + if (!(Number(score) > 0)) return { ok: false, reason: "no activity" }; + return { ok: true, address: addr, score: Number(score) }; + } + + function ubiAmountFor({ pool, userW, totalW, score, ecoTax, archTax }) { + const floorUbi = Math.max(1, DEFAULT_RULES.caps.floor_user ?? 1); + const capUser = DEFAULT_RULES.caps.cap_user_epoch; + const gross = Math.max(floorUbi, Math.min(pool * userW / totalW, capUser)); + const surplus = Math.max(0, gross - floorUbi); + const tax = (Number(ecoTax) || 0) + (Number(archTax) || 0); + return Number((floorUbi + Math.max(0, surplus - tax)).toFixed(6)); + } + + const previousEpochId = () => { + const d = new Date(); + d.setUTCDate(1); + d.setUTCMonth(d.getUTCMonth() - 1); + return `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, "0")}`; + }; + + const epochStartMs = (eid) => { + const m = /^(\d{4})-(\d{2})$/.exec(String(eid || "")); + return m ? Date.UTC(Number(m[1]), Number(m[2]) - 1, 1) : 0; + }; + + function claimPaidByMe(claim, me, defaultPubId, nowMs) { + const addressed = claim.pubId || defaultPubId; + const age = nowMs - (Number(claim._ts) || nowMs); + if (addressed === me) return me === defaultPubId || age < UBI_TAKEOVER_AFTER_MS; + if (!defaultPubId || me !== defaultPubId) return false; + const eid = claim.epochId || ""; + return age >= UBI_TAKEOVER_AFTER_MS + UBI_TAKEOVER_MARGIN_MS && (eid === epochIdNow() || eid === previousEpochId()); + } + async function processPendingClaims() { if (!isPubNode()) return; const ssb = await openSsb(); if (!ssb) return; const claims = []; const results = []; + const pubIds = await knownPubIds(); + const trusted = await trustedPubIds(); + const addressed = new Set(); + const seenClaims = new Set(); await new Promise((resolve, reject) => { pull(ssb.messagesByType({ type: "ubiClaim", reverse: false }), pull.drain(msg => { if (msg.value?.content?.type === "ubiClaim") { - claims.push({ ...msg.value.content, _author: msg.value.author }); + if (msg.value.content.pubId) addressed.add(`${msg.value.content.epochId}:${msg.value.author}:${msg.value.content.pubId}`); + const key = ubiPaidKey(msg.value.content.epochId || "", msg.value.author); + if (seenClaims.has(key)) return; + seenClaims.add(key); + claims.push({ ...msg.value.content, _author: msg.value.author, _ts: Number(msg.value.timestamp) || Date.parse(msg.value.content.claimedAt || "") || 0 }); } }, err => err ? reject(err) : resolve())); }); await new Promise((resolve, reject) => { pull(ssb.messagesByType({ type: "ubiClaimResult", reverse: false }), - pull.drain(msg => { if (msg.value?.content?.type === "ubiClaimResult") results.push(msg.value.content); }, + pull.drain(msg => { if (msg.value?.content?.type === "ubiClaimResult" && resultHonoured(msg.value, msg.value.content, trusted, addressed)) results.push(msg.value.content); }, err => err ? reject(err) : resolve())); }); const processedEpochUser = new Set(results.map(r => `${r.epochId}:${r.userId}`)); + const paidAddressBy = new Map(); + for (const r of results) if (r.address && r.epochId && r.userId) paidAddressBy.set(`${r.epochId}:${r.address}`, r.userId); + const refused = new Set(); + await new Promise((resolve, reject) => { + pull(ssb.messagesByType({ type: "ubiRefuse", reverse: false }), + pull.drain(msg => { const c = msg.value?.content; if (c && c.type === "ubiRefuse") refused.add(`${c.epochId}:${msg.value.author}`); }, + err => err ? reject(err) : resolve())); + }); const epochId = epochIdNow(); + const me = config.keys.id; + const defaultPubId = getDefaultPubId(); + const nowMs = Date.now(); + const attributed = new Set(results.map(r => r.txid).filter(Boolean)); + for (const e of Object.values(readUbiPaidLedger())) if (e && e.txid) attributed.add(e.txid); + let sends = null; + const walletSends = async () => { if (!sends) sends = await listWalletSends("pub"); return sends; }; + + const settlePaid = async ({ claim, claimantId, claimEpoch, paidKey, txid, amount, address, recovered }) => { + settleUbiPayment(paidKey, { txid, amount, address, paidAt: new Date().toISOString(), error: undefined }); + attributed.add(txid); + processedEpochUser.add(paidKey); + if (sends) sends.push({ category: "send", address, amount: -Number(amount), txid, comment: UBI_SEND_COMMENT, time: Math.floor(Date.now() / 1000) }); + try { + const open = (await transfersRepo.listByTag(`epoch:${claimEpoch}`)).find(a => a.to === claimantId && (a.status === "UNCLAIMED" || a.status === "UNCONFIRMED")); + if (open) await transfersRepo.markClosed(open.id, txid); + } catch (_) {} + console.log(`[UBI] ${recovered ? "found payment of" : "paid"} ${amount} ECO to ${claimantId.slice(0, 12)}… (${claimEpoch}) tx ${txid}`); + const allocationId = claim.allocationId || `claim:${claimEpoch}:${claimantId}`; + await publishUbiClaimResult(allocationId, claimEpoch, txid, claimantId, amount, address); + await publishBankClaim({ amount, epochId: claimEpoch, allocationId, txid }); + await publishUbiTransfer({ to: claimantId, amount, epochId: claimEpoch, txid }); + }; + for (const claim of claims) { const claimantId = claim._author; - if (!claimantId) continue; + if (!claimantId || claimantId === me || pubIds.has(claimantId)) continue; const claimEpoch = claim.epochId || epochId; - if (processedEpochUser.has(`${claimEpoch}:${claimantId}`)) continue; + if (!/^\d{4}-\d{2}$/.test(String(claimEpoch)) || (claimEpoch !== epochIdNow() && claimEpoch !== previousEpochId())) continue; + const paidKey = ubiPaidKey(claimEpoch, claimantId); + if (processedEpochUser.has(paidKey)) continue; + if (!claimPaidByMe(claim, me, defaultPubId, nowMs)) continue; + const ledgerEntry = readUbiPaidLedger()[paidKey]; + if (ledgerEntry && ledgerEntry.txid) continue; + if (refused.has(`${claimEpoch}:${claimantId}`)) { console.warn(`[UBI] claim ${claimEpoch} by ${claimantId.slice(0, 12)}… skipped: refused by the inhabitant`); continue; } try { - const addr = await getUserAddress(claimantId); - if (!addr || !isValidEcoinAddress(addr)) continue; - const pubBal = await safeGetBalance("pub"); - if (pubBal <= 0) continue; + let attempts = 0; + if (ledgerEntry) { + attempts = Number(ledgerEntry.attempts) || 1; + const lastAt = Date.parse(ledgerEntry.lastAttemptAt || ledgerEntry.startedAt || "") || 0; + if (attempts >= UBI_MAX_ATTEMPTS) continue; + if (claimEpoch !== epochIdNow() && claimEpoch !== previousEpochId()) continue; + if (nowMs - lastAt < UBI_RETRY_AFTER_MS) continue; + const prevAddress = ledgerEntry.address || await getUserAddress(claimantId).catch(() => null); + if (prevAddress && ledgerEntry.amount) { + const list = await walletSends(); + if (!list) { console.warn(`[UBI] claim ${claimEpoch} by ${claimantId.slice(0, 12)}… waiting: the wallet does not answer, the previous attempt cannot be checked`); continue; } + const found = await findWalletSend({ address: prevAddress, amount: ledgerEntry.amount, sinceMs: lastAt || epochStartMs(claimEpoch), exclude: attributed }, "pub", list); + if (found) { await settlePaid({ claim, claimantId, claimEpoch, paidKey, txid: found, amount: ledgerEntry.amount, address: prevAddress, recovered: true }); continue; } + } + } + const eligibility = await isEligibleClaimant(claimantId); + if (!eligibility.ok) { console.warn(`[UBI] claim ${claimEpoch} by ${claimantId.slice(0, 12)}… skipped: ${eligibility.reason}`); continue; } + const addr = eligibility.address; + const pubBal = await safeGetBalance("pub", ENGINE_RPC_TIMEOUT_MS); + if (pubBal <= 0) { console.warn(`[UBI] claim ${claimEpoch} by ${claimantId.slice(0, 12)}… skipped: PUB wallet balance is 0`); continue; } const pv = computePoolVars(pubBal, DEFAULT_RULES); const addresses = await listAddressesMerged(); - const eligible = addresses.filter(a => a.address && isValidEcoinAddress(a.address)); - const karmaScore = await getUserEngagementScore(claimantId); + const eligible = addresses.filter(a => a.address && isValidEcoinAddress(a.address) && a.id !== me && !pubIds.has(a.id)); + const karmaScore = eligibility.score; const wMin = DEFAULT_RULES.caps.w_min; const wMax = DEFAULT_RULES.caps.w_max; - const capUser = DEFAULT_RULES.caps.cap_user_epoch; const userW = clamp(1 + karmaScore / 100, wMin, wMax); const totalW = eligible.reduce((acc) => acc + clamp(1, wMin, wMax), 0) || 1; - const amount = Number(Math.max(1, Math.min(pv.pool * userW / totalW, capUser)).toFixed(6)); - const txid = await rpcCall("sendtoaddress", [addr, amount, `UBI ${claimEpoch}`], "pub"); - if (!txid) continue; - await publishUbiClaimResult(claim.allocationId || `claim:${claimEpoch}:${claimantId}`, claimEpoch, txid, claimantId, amount); - await publishBankClaim({ amount, epochId: claimEpoch, allocationId: claim.allocationId || `claim:${claimEpoch}:${claimantId}`, txid }); - const now = new Date().toISOString(); - await new Promise((resolve, reject) => ssb.publish({ - type: "transfer", - from: config.keys.id, - to: claimantId, - concept: `UBI ${claimEpoch} ${claimantId}`, - amount: String(amount), - createdAt: now, - updatedAt: now, - deadline: null, - confirmedBy: [config.keys.id], - status: "UNCONFIRMED", - tags: ["UBI"], - opinions: {}, - opinions_inhabitants: [], - txid - }, (err, msg) => err ? reject(err) : resolve(msg))); + const ecoTax = await getUserEcoinTax(claimantId).catch(() => 0); + const archTax = await getUserArchTax(claimantId).catch(() => 0); + const amount = ubiAmountFor({ pool: pv.pool, userW, totalW, score: karmaScore, ecoTax, archTax }); + const list = await walletSends(); + if (!list) { console.warn(`[UBI] claim ${claimEpoch} by ${claimantId.slice(0, 12)}… waiting: the wallet does not answer`); continue; } + const earlier = await findWalletSend({ address: addr, comment: UBI_SEND_COMMENT, amountIfNoComment: amount, sinceMs: epochStartMs(claimEpoch), exclude: attributed }, "pub", list); + if (earlier) { await settlePaid({ claim, claimantId, claimEpoch, paidKey, txid: earlier, amount, address: addr, recovered: true }); continue; } + if (pv.available < amount) { console.warn(`[UBI] claim ${claimEpoch} by ${claimantId.slice(0, 12)}… skipped: the PUB wallet is down to its reserve`); continue; } + if (ubiAddressTaken(claimEpoch, addr, claimantId) || (paidAddressBy.has(`${claimEpoch}:${addr}`) && paidAddressBy.get(`${claimEpoch}:${addr}`) !== claimantId)) { console.warn(`[UBI] claim ${claimEpoch} by ${claimantId.slice(0, 12)}… skipped: this ECOin address was already paid this epoch`); continue; } + if (ledgerEntry) settleUbiPayment(paidKey, { attempts: attempts + 1, lastAttemptAt: new Date().toISOString(), amount, address: addr, error: undefined }); + else { + if (!reserveUbiPayment(paidKey)) continue; + settleUbiPayment(paidKey, { attempts: 1, lastAttemptAt: new Date().toISOString(), amount, address: addr }); + } + processedEpochUser.add(paidKey); + const sent = await sendVerified(addr, amount, UBI_SEND_COMMENT, "pub"); + if (!sent.txid) { + settleUbiPayment(paidKey, { error: sent.unknown ? "the wallet did not answer" : "sendtoaddress failed" }); + console.warn(`[UBI] claim ${claimEpoch} by ${claimantId.slice(0, 12)}… not paid yet: ${sent.unknown ? "the wallet did not answer, it will be checked again" : "sendtoaddress failed, it will be retried"}`); + continue; + } + await settlePaid({ claim, claimantId, claimEpoch, paidKey, txid: sent.txid, amount, address: addr, recovered: sent.recovered }); + } catch (err) { console.warn(`[UBI] claim by ${String(claimantId).slice(0, 12)}… failed after payment: ${(err && err.message) || err}`); } + } + } + + async function readUbiLedger(ssb) { + const msgs = await readTyped(ssb, ["transfer", "transferConfirm", "ubiClaim"], { limit: Math.max(getLogLimit(), 20000) }); + const transfers = []; + const confirms = new Map(); + const claims = []; + for (const m of msgs) { + const v = m.value || {}; + const c = v.content || {}; + if (!c || typeof c !== "object") continue; + if (c.type === "transfer" && Array.isArray(c.tags) && c.tags.some(t => String(t).toUpperCase() === "UBI")) transfers.push({ key: m.key, author: v.author, ...c }); + else if (c.type === "transferConfirm" && c.target) { if (!confirms.has(c.target)) confirms.set(c.target, new Set()); confirms.get(c.target).add(v.author); } + else if (c.type === "ubiClaim") claims.push({ key: m.key, author: v.author, pubId: c.pubId, epochId: c.epochId }); + } + return { transfers, confirms, claims }; + } + + function isFollowing(ssb, source, dest) { + return new Promise((resolve) => { + if (!ssb || !ssb.friends || typeof ssb.friends.isFollowing !== "function") return resolve(false); + ssb.friends.isFollowing({ source, dest }, (err, val) => resolve(!err && !!val)); + }); + } + + async function rebalanceUbiPools() { + if (!isPubNode()) return []; + const ssb = await openSsb(); + if (!ssb || !ssb.publish) return []; + const me = config.keys.id; + const epochId = epochIdNow(); + const pubs = (await listUbiPubs()).filter(p => p.pubId !== me && p.balanceKnown); + const { transfers, confirms, claims } = await readUbiLedger(ssb); + const confirmedBy = (t) => confirms.get(t.key) || new Set(); + const sum = (list) => list.reduce((acc, t) => acc + (Number(t.amount) || 0), 0); + const createdOf = (t) => Date.parse(t.createdAt || "") || 0; + const isRebalance = (t) => String(t.concept || "").startsWith(REBALANCE_CONCEPT); + const epochOf = (t) => { + const tag = (Array.isArray(t.tags) ? t.tags : []).find(x => String(x).startsWith("epoch:")); + return tag ? String(tag).slice(6) : ((String(t.concept || "").match(/(\d{4}-\d{2})\s*$/) || [])[1] || ""); + }; + const eligibleCache = new Map(); + const eligible = async (id) => { + if (!eligibleCache.has(id)) eligibleCache.set(id, !!(await isEligibleClaimant(id)).ok); + return eligibleCache.get(id); + }; + const servedBy = async (pubId) => { + const epochs = new Set([previousEpochId(), epochId]); + const people = new Set(); + for (const c of claims) { + if (!epochs.has(c.epochId) || people.has(c.author) || !(c.pubId === pubId || transfers.some(t => t.author === pubId && t.to === c.author && isUbiPayoutConcept(t.concept) && epochOf(t) === c.epochId))) continue; + if (await eligible(c.author)) people.add(c.author); + } + return people; + }; + const now = Date.now(); + const balanceOf = (p) => { + const announcedAt = Number(p.timestamp) || 0; + const inflows = transfers.filter(t => isRebalance(t) && t.to === p.pubId && createdOf(t) > announcedAt - REBALANCE_SETTLE_MS); + const outflows = transfers.filter(t => isRebalance(t) && t.author === p.pubId && createdOf(t) > announcedAt); + const payouts = transfers.filter(t => t.author === p.pubId && isUbiPayoutConcept(t.concept) && createdOf(t) > announcedAt); + return Math.max(0, (Number(p.balance) || 0) + sum(inflows) - sum(outflows) - sum(payouts)); + }; + const sent = []; + const recorded = new Set(transfers.map(t => t.txid).filter(Boolean)); + const pendingAll = () => { const raw = readJson(REBALANCE_PENDING_PATH, {}); return raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {}; }; + const setPending = (key, value) => { const all = pendingAll(); if (value) all[key] = value; else delete all[key]; writeJson(REBALANCE_PENDING_PATH, all); }; + let left = 0; + const record = async (pub, amount, txid, served, ep = epochId, at = null) => { + const when = at || new Date().toISOString(); + const content = { type: "transfer", from: me, to: pub.pubId, concept: `${REBALANCE_CONCEPT} · ${ep}`, amount: Number(amount).toFixed(6), category: "ECONOMIC", createdAt: when, updatedAt: when, deadline: null, confirmedBy: [me], status: "UNCONFIRMED", tags: ["UBI", "REBALANCE", `epoch:${ep}`], opinions: {}, opinions_inhabitants: [], txid }; + await new Promise((resolve, reject) => ssb.publish(content, (err, msg) => err ? reject(err) : resolve(msg))); + recorded.add(txid); + transfers.push({ ...content, author: me }); + left -= Number(amount); + console.log(`[UBI] rebalanced ${amount} ECO to PUB ${pub.pubId.slice(0, 12)}…${served ? ` (${served} claimants)` : ""} tx ${txid}`); + sent.push({ pubId: pub.pubId, amount: Number(amount), txid }); + }; + for (const [key, pending] of Object.entries(pendingAll())) { + const cut = key.indexOf(":"); + const ep = key.slice(0, cut); + const pubId = key.slice(cut + 1); + const list = await listWalletSends("pub"); + if (!list) { console.warn(`[UBI] rebalance to ${pubId.slice(0, 12)}… waiting: the wallet does not answer, the previous attempt cannot be checked`); continue; } + const found = await findWalletSend({ address: pending.address, amount: pending.amount, comment: REBALANCE_CONCEPT, sinceMs: Date.parse(pending.startedAt || "") || 0, exclude: recorded }, "pub", list); + if (found) await record(pubs.find(p => p.pubId === pubId) || { pubId }, pending.amount, found, 0, ep, pending.startedAt || null); + setPending(key, null); + } + if (!pubs.length) return sent; + const myBalance = await tryGetBalance("pub", ENGINE_RPC_TIMEOUT_MS); + if (myBalance === null) return sent; + const mine = computePoolVars(myBalance, DEFAULT_RULES).available; + const balances = new Map([[me, mine], ...pubs.map(p => [p.pubId, balanceOf(p)])]); + const average = Array.from(balances.values()).reduce((a, b) => a + b, 0) / balances.size; + const tolerance = Math.max(1, average * REBALANCE_TOLERANCE); + const myExcess = mine - average; + if (myExcess < tolerance) return sent; + const totalExcess = Array.from(balances.values()).reduce((acc, b) => acc + Math.max(0, b - average), 0); + left = myExcess; + const poorer = pubs.map(p => ({ pub: p, gap: average - balances.get(p.pubId) })).filter(x => x.gap >= tolerance).sort((a, b) => b.gap - a.gap); + for (const { pub, gap } of poorer) { + if (left < 1) break; + const tag = pub.pubId.slice(0, 12); + try { + const pendingKey = `${epochId}:${pub.pubId}`; + if (Object.keys(pendingAll()).some(k => k.endsWith(`:${pub.pubId}`))) { console.warn(`[UBI] rebalance to ${tag}… waiting: an earlier transfer is still being checked`); continue; } + if (!(await isFollowing(ssb, me, pub.pubId))) { console.warn(`[UBI] rebalance to ${tag}… skipped: this PUB does not follow it`); continue; } + const address = pub.address || await getUserAddress(pub.pubId).catch(() => null); + if (!address || !isValidEcoinAddress(address)) { console.warn(`[UBI] rebalance to ${tag}… skipped: no ECOin address`); continue; } + const served = await servedBy(pub.pubId); + if (served.size < REBALANCE_MIN_CLAIMANTS) { console.warn(`[UBI] rebalance to ${tag}… skipped: ${served.size} eligible claimant(s), ${REBALANCE_MIN_CLAIMANTS} required`); continue; } + const receivedBefore = sum(transfers.filter(t => t.author === me && t.to === pub.pubId && isRebalance(t))); + const paidOut = sum(transfers.filter(t => t.author === pub.pubId && isUbiPayoutConcept(t.concept) && confirmedBy(t).has(t.to))); + if (receivedBefore > 0 && paidOut < receivedBefore * REBALANCE_PAYOUT_RATIO) { console.warn(`[UBI] rebalance to ${tag}… skipped: paid out ${paidOut.toFixed(6)} of ${receivedBefore.toFixed(6)} received`); continue; } + const amount = Number(Math.min(left, gap * myExcess / totalExcess).toFixed(6)); + if (!(amount >= 1)) { console.warn(`[UBI] rebalance to ${tag}… skipped: nothing needed`); continue; } + setPending(pendingKey, { address, amount, startedAt: new Date().toISOString() }); + const result = await sendVerified(address, amount, REBALANCE_CONCEPT, "pub"); + if (!result.txid) { + if (!result.unknown) setPending(pendingKey, null); + console.warn(`[UBI] rebalance to ${tag}… not sent: ${result.unknown ? "the wallet did not answer, it will be checked next time" : "sendtoaddress failed"}`); + continue; + } + await record(pub, amount, result.txid, served.size); + setPending(pendingKey, null); + } catch (err) { console.warn(`[UBI] rebalance to ${tag}… failed: ${(err && err.message) || err}`); } + } + return sent; + } + + async function confirmIncomingTransfers() { + if (!services || !services.transfers || typeof services.transfers.confirmTransferById !== "function" || typeof services.transfers.listAll !== "function") return []; + const kind = isPubNode() ? "pub" : "user"; + const cfg = getWalletCfg(kind) || {}; + if (!cfg.url) return []; + const ssb = await openSsb(); + if (!ssb) return []; + const me = config.keys.id; + const msgs = await readTyped(ssb, ["transfer", "transferConfirm"], { limit: Math.max(getLogLimit(), 20000) }); + const txidOf = (t) => (String(t.txid || "").match(TXID_RE) ? String(t.txid) : (String(t.concept || "").match(TXID_RE) || [])[1] || "").toLowerCase(); + const signed = new Map(); + const confirmedByMe = new Set(); + for (const m of msgs) { + const v = m.value || {}; + const c = v.content || {}; + if (c.type === "transferConfirm" && c.target && v.author === me) confirmedByMe.add(c.target); + else if (c.type === "transfer" && c.to === me && c.from === v.author) signed.set(m.key, c); + } + const used = new Set(); + for (const k of confirmedByMe) { const txid = signed.has(k) ? txidOf(signed.get(k)) : ""; if (txid) used.add(txid); } + const claimed = new Map(); + const incoming = []; + for (const t of (await services.transfers.listAll("all")) || []) { + if (t.to !== me || t.from === me || t.from !== t.author) continue; + const txid = txidOf(t); + if (!txid) continue; + if (t.status === "CLOSED" || (Array.isArray(t.confirmedBy) && t.confirmedBy.includes(me))) { used.add(txid); continue; } + if (t.status !== "UNCONFIRMED") continue; + claimed.set(txid, (claimed.get(txid) || 0) + 1); + incoming.push({ ...t, txid }); + } + const done = []; + for (const t of incoming) { + if (claimed.get(t.txid) !== 1 || used.has(t.txid)) continue; + const amount = Number(t.amount); + if (!Number.isFinite(amount) || amount <= 0) continue; + const tx = await rpcCall("gettransaction", [t.txid], kind); + if (!tx || !(Number(tx.confirmations) >= 1) || !Array.isArray(tx.details)) continue; + const address = signed.has(t.id) && typeof signed.get(t.id).address === "string" ? signed.get(t.id).address : ""; + if (!tx.details.some(d => d && d.category === "receive" && Number(d.amount) >= amount - 0.000001 && (!address || d.address === address))) continue; + try { + await services.transfers.confirmTransferById(t.id); + used.add(t.txid); + console.log(`[WALLET] confirmed incoming ${amount.toFixed(6)} ECO from ${String(t.from).slice(0, 12)}… (${t.concept}) tx ${t.txid}`); + done.push(t.id); } catch (_) {} } + return done; } return { DEFAULT_RULES, isPubNode, - getConfiguredPubId, computeEpoch, executeEpoch, getUserEngagementScore, + getKarmaReceived: async (userId) => { const r = karma.receivedBreakdownFor(String(userId || config.keys.id), await listAllActions()); return { positive: r.positive, negative: r.negative }; }, publishBankClaim, publishUbiAllocation, publishUbiClaim, @@ -1367,6 +2243,8 @@ async function getLastPublishedTimestamp(userId) { publishPubAvailability, getPubAvailabilityFromSSB, hasClaimedThisMonth, + hasWalletCredentials, + sampleUserFunds, getUbiClaimHistory, claimUBI, processPendingClaims, @@ -1378,10 +2256,34 @@ async function getLastPublishedTimestamp(userId) { listEpochAllocations, addAddress, removeAddress, + listAddressBook, + addAddressBookEntry, + removeAddressBookEntry, ensureSelfAddressPublished, getUserAddress, setUserAddress, listAddressesMerged, + scanAllWalletsSSB, + hasPublishedAddress, + discoverUbiPub, + resolvePubId, + listUbiPubs, + listUbiPubsDetailed, + listUbiCharts, + listKarmaHistory, + listWealthSeries, + listUbiPaymentsSeries, + listNetworkEpochs, + normalizeRange, + filterByRange, + compactEcoHistory, + isEligibleClaimant, + ubiAmountFor, + rebalanceUbiPools, + confirmIncomingTransfers, + refuseUBI, + hasRefusedThisMonth, + claimAvailability, calculateEcoinValue, calculateEcoTaxStats, getUserEcoinTax, diff --git a/src/models/blobcache_model.js b/src/models/blobcache_model.js new file mode 100644 index 0000000..0bfb001 --- /dev/null +++ b/src/models/blobcache_model.js @@ -0,0 +1,161 @@ +const fs = require('fs'); +const pull = require('../server/node_modules/pull-stream'); + +const BLOB_RE = /&[A-Za-z0-9+/=]{44}\.sha256/g; +const DEFAULT_MAX_MB = 0; +const PROTECT_MS = 24 * 60 * 60 * 1000; +const FLUSH_MS = 60 * 1000; + +const pinsPath = () => { + try { return require('../configs/state-manager').statePath('blob-pins.json'); } catch (_) { return null; } +}; + +const accessPath = () => { + try { return require('../configs/state-manager').statePath('blob-access.json'); } catch (_) { return null; } +}; + +module.exports = ({ cooler } = {}) => { + const openSsb = async () => cooler.open(); + let access = null; + let dirty = false; + let flushTimer = null; + + const loadAccess = () => { + if (access) return access; + access = {}; + const p = accessPath(); + if (p) { try { access = JSON.parse(fs.readFileSync(p, 'utf8')) || {}; } catch (_) { access = {}; } } + return access; + }; + + const flush = () => { + if (!dirty) return; + const p = accessPath(); + if (!p) return; + try { fs.writeFileSync(p, JSON.stringify(loadAccess())); dirty = false; } catch (_) {} + }; + + const normalizeId = (raw) => { + let id = String(raw || '').trim(); + try { id = decodeURIComponent(id); } catch (_) {} + if (!id) return null; + if (!id.startsWith('&')) id = `&${id}`; + return /^&[A-Za-z0-9+/=]{44}\.sha256$/.test(id) ? id : null; + }; + + const touch = (raw) => { + const id = normalizeId(raw); + if (!id) return; + loadAccess()[id] = Date.now(); + dirty = true; + if (!flushTimer) { + flushTimer = setTimeout(() => { flushTimer = null; flush(); }, FLUSH_MS); + if (typeof flushTimer.unref === 'function') flushTimer.unref(); + } + }; + + const listBlobs = async (ssb) => new Promise((resolve) => { + if (!ssb.blobs || typeof ssb.blobs.ls !== 'function') return resolve([]); + pull(ssb.blobs.ls({ long: true }), pull.collect((err, arr) => { + if (err) return resolve([]); + resolve((arr || []).filter(b => b && b.id).map(b => ({ id: b.id, size: Number(b.size) || 0, ts: Number(b.ts) || 0 }))); + })); + }); + + const loadPins = () => { + const p = pinsPath(); + if (!p) return []; + try { const arr = JSON.parse(fs.readFileSync(p, 'utf8')); return Array.isArray(arr) ? arr : []; } catch (_) { return []; } + }; + + const pin = (ids) => { + const p = pinsPath(); + if (!p) return; + const set = new Set(loadPins()); + for (const raw of (Array.isArray(ids) ? ids : [ids])) { const id = normalizeId(raw); if (id) set.add(id); } + try { fs.writeFileSync(p, JSON.stringify(Array.from(set))); } catch (_) {} + }; + + const getContent = (ssb, key) => new Promise((resolve) => { + try { ssb.get(key, (err, v) => resolve(!err && v && v.content && typeof v.content === 'object' ? v.content : null)); } catch (_) { resolve(null); } + }); + + const ownBlobIds = async (ssb) => { + const own = new Set(); + const msgs = await new Promise((resolve) => { + pull(ssb.createUserStream({ id: ssb.id, reverse: false }), pull.collect((err, arr) => resolve(err ? [] : (arr || [])))); + }); + const spreads = new Map(); + const tombstoned = new Set(); + for (const m of msgs) { + const content = m && m.value && m.value.content; + if (!content || typeof content !== 'object') continue; + if (content.type === 'spread' && typeof content.link === 'string') spreads.set(m.key, content.link); + else if (content.type === 'tombstone' && typeof content.target === 'string') tombstoned.add(content.target); + for (const id of (JSON.stringify(content).match(BLOB_RE) || [])) own.add(id); + } + if (ssb.private && typeof ssb.private.read === 'function') { + const boxed = await new Promise((resolve) => { + pull(ssb.private.read({ reverse: false }), pull.collect((err, arr) => resolve(err ? [] : (arr || [])))); + }); + for (const m of boxed) { + const content = m && m.value && m.value.content; + if (content && typeof content === 'object') for (const id of (JSON.stringify(content).match(BLOB_RE) || [])) own.add(id); + } + } + for (const id of loadPins()) own.add(id); + for (const [key, link] of spreads) { + if (tombstoned.has(key)) continue; + const seeded = await getContent(ssb, link); + if (!seeded || (seeded.type !== 'torrent' && seeded.type !== 'file')) continue; + for (const id of (JSON.stringify(seeded).match(BLOB_RE) || [])) own.add(id); + } + return own; + }; + + const usage = async () => { + const ssb = await openSsb(); + const blobs = await listBlobs(ssb); + return { count: blobs.length, bytes: blobs.reduce((n, b) => n + b.size, 0) }; + }; + + const collect = async ({ maxBytes, now = Date.now(), protectMs = PROTECT_MS } = {}) => { + const limit = Number(maxBytes); + const ssb = await openSsb(); + const blobs = await listBlobs(ssb); + const total = blobs.reduce((n, b) => n + b.size, 0); + const result = { before: total, after: total, deleted: 0, freed: 0, kept: blobs.length }; + if (!(limit > 0) || total <= limit) return result; + const own = await ownBlobIds(ssb); + const seen = loadAccess(); + const candidates = blobs + .filter(b => !own.has(b.id)) + .map(b => ({ ...b, last: Math.max(Number(seen[b.id]) || 0, b.ts) })) + .filter(b => now - b.last > protectMs) + .sort((a, b) => a.last - b.last); + let current = total; + for (const b of candidates) { + if (current <= limit) break; + const removed = await new Promise((resolve) => ssb.blobs.rm(b.id, (err) => resolve(!err))); + if (!removed) continue; + current -= b.size; + result.deleted += 1; + result.freed += b.size; + delete seen[b.id]; + dirty = true; + } + result.after = current; + result.kept = blobs.length - result.deleted; + flush(); + return result; + }; + + const maxBytesFor = (cfg, { isPublic = false } = {}) => { + const mb = Number(cfg && cfg.blobCache && (isPublic ? cfg.blobCache.pubMaxMB : cfg.blobCache.maxMB)); + if (!Number.isFinite(mb) || mb <= 0) return 0; + return Math.round(mb * 1024 * 1024); + }; + + return { touch, usage, collect, ownBlobIds, maxBytesFor, flush, pin, DEFAULT_MAX_MB }; +}; +module.exports.DEFAULT_MAX_MB = DEFAULT_MAX_MB; diff --git a/src/models/blockchain_model.js b/src/models/blockchain_model.js index 39fe868..b305269 100644 --- a/src/models/blockchain_model.js +++ b/src/models/blockchain_model.js @@ -2,6 +2,8 @@ const pull = require('../server/node_modules/pull-stream'); const config = require('../server/ssb_config'); const { getConfig } = require('../configs/config-manager.js'); const { buildValidatedTombstoneSet } = require('./tombstone_validator'); +const { readContentWindow, requestScope, logTip } = require('./typed_log'); +const { versionGroups, versionEntry } = require('../backend/dedupe'); const logLimit = getConfig().ssbLogStream?.limit || 1000; module.exports = ({ cooler, tribeCrypto, tribesModel }) => { @@ -11,6 +13,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { if (!ssb) ssb = await cooler.open(); return ssb; }; + const nameByFeedId = new Map(); const HIDDEN_ENVELOPE_TYPES = new Set([ 'tribe-keys-distrib', @@ -19,7 +22,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { ]); const isHiddenBoxedContent = (rawContent) => - typeof rawContent === 'string' && rawContent.endsWith('.box'); + typeof rawContent === 'string' && /\.box\d*$/.test(rawContent); const buildAccessibleTribeIds = async () => { const set = new Set(); @@ -41,16 +44,10 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { const hasBlob = async (ssbClient, url) => new Promise(resolve => ssbClient.blobs.has(url, (err, has) => resolve(!err && has))); - const isClosedSold = s => - String(s || '').toUpperCase() === 'SOLD' || String(s || '').toUpperCase() === 'CLOSED'; - - const projectRank = (status) => { - const S = String(status || '').toUpperCase(); - if (S === 'COMPLETED') return 3; - if (S === 'ACTIVE') return 2; - if (S === 'PAUSED') return 1; - if (S === 'CANCELLED') return 0; - return -1; + const isReplacedBlock = (vg, tombstoned, block) => { + const c = block.content; + const rootDeleted = !c.replaces && c?.type === 'forum' && c.root && tombstoned.has(c.root); + return vg.tipOf(block.id) !== block.id || vg.isForeignCopy(block.id) || tombstoned.has(block.id) || !!rootDeleted; }; const safeDecode = (s) => { @@ -127,115 +124,83 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { return nameByFeedId; }; + let windowCache = null; + const readWindow = async (ssbClient) => { + const tip = await logTip(ssbClient).catch(() => null); + if (tip && windowCache && windowCache.tip === tip) return windowCache.msgs; + const msgs = await readContentWindow(ssbClient, logLimit); + if (tip) windowCache = { tip, msgs }; + return msgs; + }; + + async function buildBlocks(showLogs, me) { + const ssbClient = await openSsb(); + + const results = await readWindow(ssbClient); + + const tombstoned = buildValidatedTombstoneSet(results); + const idToBlock = new Map(); + const versions = []; + + const fpIdx = tribeCrypto ? tribeCrypto.buildFingerprintIndex() : null; + const accessibleTribeIds = await buildAccessibleTribeIds(); + for (const msg of results) { + const k = msg.key; + let c = msg.value?.content; + const author = msg.value?.author; + if (isHiddenBoxedContent(c) || msg.value?.private === true) continue; + if (showLogs && typeof c === 'string' && author === me) { + try { + const dec = ssbClient.private.unbox({ key: k, value: msg.value, timestamp: msg.timestamp || msg.value?.timestamp || 0 }); + c = dec?.value?.content; + } catch { c = null; } + } + if (!c?.type) continue; + if (HIDDEN_ENVELOPE_TYPES.has(c.type)) continue; + if (tribeCrypto && tribeCrypto.isTribeMsg(c)) { + const r = fpIdx ? tribeCrypto.unwrapMsg(c, fpIdx) : null; + if (!r || !r.body) continue; + const inner = r.body; + const innerType = inner.k === 'tribe' ? 'tribe' : (inner.k === 'tribe-content' ? `tribe-content:${inner.contentType || ''}` : inner.k || 'tribe-msg'); + c = { type: innerType, _decrypted: true, _rootId: r.rootId, ...inner }; + } else if (c.tribeId && !accessibleTribeIds.has(c.tribeId)) { + continue; + } + + if (c.type === 'about') { + const aboutId = String(c.about || author || '').trim(); + const nm = typeof c.name === 'string' ? c.name.trim() : ''; + if (aboutId && nm && !nameByFeedId.has(aboutId)) nameByFeedId.set(aboutId, nm); + } + + if (c.type === 'tombstone' && c.target) { + idToBlock.set(k, { id: k, author, ts: msg.value.timestamp, type: c.type, content: c, size: Buffer.byteLength(JSON.stringify(msg.value), 'utf8') }); + continue; + } + versions.push(versionEntry({ key: k, value: { ...msg.value, content: c } })); + idToBlock.set(k, { id: k, author, ts: msg.value.timestamp, type: c.type, content: c, size: Buffer.byteLength(JSON.stringify(msg.value), 'utf8') }); + } + + const vg = versionGroups(versions); + + const blockData = Array.from(idToBlock.values()).map(block => ({ + ...block, + isTombstoned: tombstoned.has(block.id), + isReplaced: isReplacedBlock(vg, tombstoned, block) + })); + + return blockData; + } + return { async listBlockchain(filter = 'all', userId, search = {}) { - const ssbClient = await openSsb(); - - const results = await new Promise((resolve, reject) => - pull( - ssbClient.createLogStream({ reverse: true, limit: logLimit }), - pull.collect((err, msgs) => err ? reject(err) : resolve(msgs)) - ) - ); - - const tombstoned = buildValidatedTombstoneSet(results); - const idToBlock = new Map(); - const referencedAsReplaces = new Set(); - - const nameByFeedId = new Map(); - const showLogs = (filter === 'logs' || filter === 'LOGS'); const me = userId || config.keys.id; - const fpIdx = tribeCrypto ? tribeCrypto.buildFingerprintIndex() : null; - const accessibleTribeIds = await buildAccessibleTribeIds(); - for (const msg of results) { - const k = msg.key; - let c = msg.value?.content; - const author = msg.value?.author; - if (isHiddenBoxedContent(c)) continue; - if (showLogs && typeof c === 'string' && author === me) { - try { - const dec = ssbClient.private.unbox({ key: k, value: msg.value, timestamp: msg.timestamp || msg.value?.timestamp || 0 }); - c = dec?.value?.content; - } catch { c = null; } - } - if (!c?.type) continue; - if (HIDDEN_ENVELOPE_TYPES.has(c.type)) continue; - if (tribeCrypto && tribeCrypto.isTribeMsg(c)) { - const r = fpIdx ? tribeCrypto.unwrapMsg(c, fpIdx) : null; - if (!r || !r.body) continue; - const inner = r.body; - const innerType = inner.k === 'tribe' ? 'tribe' : (inner.k === 'tribe-content' ? `tribe-content:${inner.contentType || ''}` : inner.k || 'tribe-msg'); - c = { type: innerType, _decrypted: true, _rootId: r.rootId, ...inner }; - } else if (c.tribeId && !accessibleTribeIds.has(c.tribeId)) { - continue; - } - - if (c.type === 'about') { - const aboutId = String(c.about || author || '').trim(); - const nm = typeof c.name === 'string' ? c.name.trim() : ''; - if (aboutId && nm && !nameByFeedId.has(aboutId)) nameByFeedId.set(aboutId, nm); - } - - if (c.type === 'tombstone' && c.target) { - idToBlock.set(k, { id: k, author, ts: msg.value.timestamp, type: c.type, content: c, size: Buffer.byteLength(JSON.stringify(msg.value), 'utf8') }); - continue; - } - if (c.replaces) referencedAsReplaces.add(c.replaces); - idToBlock.set(k, { id: k, author, ts: msg.value.timestamp, type: c.type, content: c, size: Buffer.byteLength(JSON.stringify(msg.value), 'utf8') }); - } - - const tipBlocks = []; - for (const [id, block] of idToBlock.entries()) { - if (!referencedAsReplaces.has(id) && block.content.replaces) tipBlocks.push(block); - } - for (const [id, block] of idToBlock.entries()) { - if (!block.content.replaces && !referencedAsReplaces.has(id)) tipBlocks.push(block); - } - - const groups = {}; - for (const block of tipBlocks) { - const ancestor = block.content.replaces || block.id; - if (!groups[ancestor]) groups[ancestor] = []; - groups[ancestor].push(block); - } - - const liveTipIds = new Set(); - for (const groupBlocks of Object.values(groups)) { - let best = groupBlocks[0]; - for (const block of groupBlocks) { - if (block.type === 'market') { - if (isClosedSold(block.content.status) && !isClosedSold(best.content.status)) { - best = block; - } else if ((block.content.status === best.content.status) && block.ts > best.ts) { - best = block; - } - } else if (block.type === 'project') { - const br = projectRank(best.content.status); - const cr = projectRank(block.content.status); - if (cr > br || (cr === br && block.ts > best.ts)) best = block; - } else if (block.type === 'job' || block.type === 'forum') { - if (block.ts > best.ts) best = block; - } else { - if (block.ts > best.ts) best = block; - } - } - liveTipIds.add(best.id); - } - - const blockData = Array.from(idToBlock.values()).map(block => { - const c = block.content; - const rootDeleted = c?.type === 'forum' && c.root && tombstoned.has(c.root); - return { - ...block, - isTombstoned: tombstoned.has(block.id), - isReplaced: c.replaces - ? (!liveTipIds.has(block.id) || tombstoned.has(block.id)) - : referencedAsReplaces.has(block.id) || tombstoned.has(block.id) || rootDeleted - }; - }); - - let filtered = blockData; + const store = requestScope.getStore(); + const memoKey = `blockchainBase:${showLogs}:${me}`; + if (store && !store[memoKey]) store[memoKey] = buildBlocks(showLogs, me); + const blockData = await (store ? store[memoKey] : buildBlocks(showLogs, me)); + let filtered = blockData.slice(); if (filter === 'RECENT' || filter === 'recent') { const now = Date.now(); @@ -267,7 +232,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { const needsNameSearch = !!authorQ && !authorQ.toLowerCase().includes('.ed25519'); if (needsNameSearch) { - aboutIndex = await buildNameIndexFromAbout(ssbClient, 10000); + aboutIndex = await buildNameIndexFromAbout(await openSsb(), 10000); for (const [fid, nm] of aboutIndex.entries()) { if (!nameByFeedId.has(fid)) nameByFeedId.set(fid, nm); } @@ -293,17 +258,12 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { async getBlockById(id, userId) { const ssbClient = await openSsb(); - const results = await new Promise((resolve, reject) => - pull( - ssbClient.createLogStream({ reverse: true, limit: logLimit }), - pull.collect((err, msgs) => err ? reject(err) : resolve(msgs)) - ) - ); + const results = await readWindow(ssbClient); const me = userId || config.keys.id; const tombstoned = buildValidatedTombstoneSet(results); const idToBlock = new Map(); - const referencedAsReplaces = new Set(); + const versions = []; const fpIdx = tribeCrypto ? tribeCrypto.buildFingerprintIndex() : null; const accessibleTribeIds = await buildAccessibleTribeIds(); @@ -311,7 +271,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { const k = msg.key; let c = msg.value?.content; const author = msg.value?.author; - if (isHiddenBoxedContent(c)) continue; + if (isHiddenBoxedContent(c) || msg.value?.private === true) continue; if (typeof c === 'string' && author === me) { try { const dec = ssbClient.private.unbox({ key: k, value: msg.value, timestamp: msg.timestamp || msg.value?.timestamp || 0 }); @@ -333,48 +293,10 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { idToBlock.set(k, { id: k, author, ts: msg.value.timestamp, type: c.type, content: c, size: Buffer.byteLength(JSON.stringify(msg.value), 'utf8') }); continue; } - if (c.replaces) referencedAsReplaces.add(c.replaces); + versions.push(versionEntry({ key: k, value: { ...msg.value, content: c } })); idToBlock.set(k, { id: k, author, ts: msg.value.timestamp, type: c.type, content: c, size: Buffer.byteLength(JSON.stringify(msg.value), 'utf8') }); } - const tipBlocks = []; - for (const [bid, block] of idToBlock.entries()) { - if (!referencedAsReplaces.has(bid) && block.content.replaces) tipBlocks.push(block); - } - for (const [bid, block] of idToBlock.entries()) { - if (!block.content.replaces && !referencedAsReplaces.has(bid)) tipBlocks.push(block); - } - - const groups = {}; - for (const block of tipBlocks) { - const ancestor = block.content.replaces || block.id; - if (!groups[ancestor]) groups[ancestor] = []; - groups[ancestor].push(block); - } - - const liveTipIds = new Set(); - for (const groupBlocks of Object.values(groups)) { - let best = groupBlocks[0]; - for (const block of groupBlocks) { - if (block.type === 'market') { - if (isClosedSold(block.content.status) && !isClosedSold(best.content.status)) { - best = block; - } else if ((block.content.status === best.content.status) && block.ts > best.ts) { - best = block; - } - } else if (block.type === 'project') { - const br = projectRank(best.content.status); - const cr = projectRank(block.content.status); - if (cr > br || (cr === br && block.ts > best.ts)) best = block; - } else if (block.type === 'job' || block.type === 'forum') { - if (block.ts > best.ts) best = block; - } else { - if (block.ts > best.ts) best = block; - } - } - liveTipIds.add(best.id); - } - const block = idToBlock.get(id); if (!block) return null; @@ -383,12 +305,8 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { if (!valid) return null; } - const c = block.content; - const rootDeleted = c?.type === 'forum' && c.root && tombstoned.has(c.root); const isTombstoned = tombstoned.has(block.id); - const isReplaced = c.replaces - ? (!liveTipIds.has(block.id) || tombstoned.has(block.id)) - : referencedAsReplaces.has(block.id) || tombstoned.has(block.id) || rootDeleted; + const isReplaced = isReplacedBlock(versionGroups(versions), tombstoned, block); return { ...block, isTombstoned, isReplaced }; } diff --git a/src/models/blog_model.js b/src/models/blog_model.js new file mode 100644 index 0000000..c0fa690 --- /dev/null +++ b/src/models/blog_model.js @@ -0,0 +1,183 @@ +const pull = require('../server/node_modules/pull-stream'); +const { getConfig } = require('../configs/config-manager.js'); +const { buildValidatedTombstoneSet } = require('./tombstone_validator'); +const { readTyped, memoIndex } = require('./typed_log'); +const longText = require('../backend/long_text'); +const logLimit = getConfig().ssbLogStream?.limit || 1000; + +const OPINION_TYPE = 'blogOpinion'; + +module.exports = ({ cooler, isPublic = false }) => { + let ssb; + const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb; }; + + const BLOG_TYPES = ['post', OPINION_TYPE, 'tombstone', longText.CHUNK_TYPE]; + + const getAllMessages = async (ssbClient) => readTyped(ssbClient, BLOG_TYPES, { limit: logLimit }); + + const isRootPost = (c) => + !!c && c.type === 'post' && typeof c.text === 'string' && + !c.root && !c.fork && !c.branch && !c.about && !c.target; + + const buildIndex = (messages) => { + const tomb = buildValidatedTombstoneSet(messages); + const chunks = longText.indexChunks(messages); + const posts = new Map(); + const commentsByRoot = new Map(); + const opinionsByRoot = new Map(); + + for (const m of messages) { + const v = m && m.value; + const c = v && v.content; + if (!c || typeof c !== 'object') continue; + if (tomb.has(m.key)) continue; + + if (c.type === 'post' && typeof c.text === 'string') { + if (isRootPost(c)) { + posts.set(m.key, { key: m.key, author: v.author, ts: v.timestamp || m.timestamp || 0, c }); + } else if (typeof c.root === 'string') { + commentsByRoot.set(c.root, (commentsByRoot.get(c.root) || 0) + 1); + } + continue; + } + + if (c.type === OPINION_TYPE && typeof c.target === 'string') { + const entry = opinionsByRoot.get(c.target) || { counts: {}, voters: [] }; + if (!entry.voters.includes(v.author)) { + entry.voters.push(v.author); + entry.counts[c.category] = (entry.counts[c.category] || 0) + 1; + } + opinionsByRoot.set(c.target, entry); + } + } + + return { tomb, posts, commentsByRoot, opinionsByRoot, chunks }; + }; + + const buildBlog = (node, idx) => { + const c = node.c || {}; + const op = idx.opinionsByRoot.get(node.key) || { counts: {}, voters: [] }; + return { + id: node.key, + key: node.key, + author: node.author, + subject: typeof c.contentWarning === 'string' ? c.contentWarning : '', + text: longText.joinText(c.text || '', c.chunks, longText.lookupIn(idx.chunks), node.author), + mentions: Array.isArray(c.mentions) ? c.mentions : [], + allowComments: c.allowComments !== false, + tags: Array.isArray(c.tags) ? c.tags.map(t => String(t || '').trim().replace(/^#+/, '')).filter(Boolean) : [], + createdAt: new Date(node.ts).toISOString(), + commentCount: idx.commentsByRoot.get(node.key) || 0, + opinions: op.counts, + opinions_inhabitants: op.voters + }; + }; + + const collect = async () => { + const ssbClient = await openSsb(); + const messages = await getAllMessages(ssbClient); + const idx = memoIndex('blog', messages, buildIndex); + const list = []; + for (const node of idx.posts.values()) list.push(buildBlog(node, idx)); + return { list, idx, viewerId: ssbClient.id }; + }; + + const opinionScore = (blog) => + Object.values(blog.opinions || {}).reduce((n, v) => n + (Number(v) || 0), 0); + + return { + type: 'post', + + async listAll(filter = 'ALL', opts = {}) { + const { list, viewerId } = await collect(); + const f = String(filter || 'ALL').toUpperCase(); + const favorites = Array.isArray(opts.favorites) ? new Set(opts.favorites.map(String)) : null; + let out = list.slice(); + + if (f === 'MINE') out = out.filter(b => String(b.author) === String(viewerId)); + else if (f === 'FAVORITES') out = favorites ? out.filter(b => favorites.has(String(b.id))) : []; + else if (f === 'RECENT') out = out.filter(b => (Date.parse(b.createdAt || '') || 0) >= Date.now() - 86400000); + + const q = String(opts.q || '').trim().toLowerCase(); + if (q) out = out.filter(b => + b.text.toLowerCase().includes(q) || b.subject.toLowerCase().includes(q) || + b.tags.some(t => t.toLowerCase().includes(q.replace(/^#/, '')))); + + if (f === 'TOP') { + out.sort((a, b) => (opinionScore(b) + b.commentCount) - (opinionScore(a) + a.commentCount) || + new Date(b.createdAt) - new Date(a.createdAt)); + } else { + out.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)); + } + + return out; + }, + + async getBlogById(id) { + const { list } = await collect(); + const blog = list.find(b => b.id === id); + if (!blog) throw new Error('Blog not found'); + return blog; + }, + + async createBlog({ text, subject = '', mentions = [], allowComments = true, tags = [] } = {}) { + if (isPublic) throw new Error('Not available in public mode'); + const body = String(text || '').trim(); + if (!body) throw new Error('Blog text is required'); + const ssbClient = await openSsb(); + const publish = (c) => new Promise((res, rej) => ssbClient.publish(c, (err, msg) => err ? rej(err) : res(msg))); + const content = await longText.chunkContent({ + type: 'post', + text: body, + allowComments: allowComments !== false, + ...(Array.isArray(tags) && tags.length ? { tags: Array.from(new Set(tags.map(t => String(t || '').trim().replace(/^#+/, '')).filter(Boolean))) } : {}), + ...(Array.isArray(mentions) && mentions.length ? { mentions } : {}), + ...(String(subject || '').trim() ? { contentWarning: String(subject).trim() } : {}) + }, 'text', { publish }); + return publish(content); + }, + + async deleteBlogById(id) { + if (isPublic) throw new Error('Not available in public mode'); + const ssbClient = await openSsb(); + const userId = ssbClient.id; + const msg = await new Promise((res, rej) => ssbClient.get(id, (err, m) => (err || !m || !m.content) ? rej(new Error('Blog not found')) : res(m))); + if (!isRootPost(msg.content)) throw new Error('Blog not found'); + if (msg.author !== userId) throw new Error('Not the author'); + const content = { type: 'tombstone', target: id, deletedAt: new Date().toISOString(), author: userId }; + return new Promise((res, rej) => ssbClient.publish(content, (err, result) => err ? rej(err) : res(result))); + }, + + async createOpinion(id, category) { + const categories = require('../backend/opinion_categories'); + if (!categories.includes(category)) throw new Error('Invalid opinion category'); + const ssbClient = await openSsb(); + const userId = ssbClient.id; + const blog = await this.getBlogById(id); + if (blog.opinions_inhabitants.includes(userId)) throw new Error('Already opined'); + const content = { type: OPINION_TYPE, target: blog.id, category, createdAt: new Date().toISOString() }; + return new Promise((res, rej) => ssbClient.publish(content, (err, result) => err ? rej(err) : res(result))); + }, + + async resolveRootId(id) { + const ssbClient = await openSsb(); + const msg = await new Promise((res) => ssbClient.get(id, (err, m) => res(err ? null : m))); + const c = msg && (msg.content || (msg.value && msg.value.content)); + if (c && c.type === 'post' && typeof c.root === 'string') return c.root; + return id; + }, + + async blogHrefFor(id) { + const ssbClient = await openSsb(); + const msg = await new Promise((res) => ssbClient.get(id, (err, m) => res(err ? null : m))); + const c = msg && (msg.content || (msg.value && msg.value.content)); + if (!c || c.type !== 'post' || typeof c.text !== 'string') return null; + if (c.private === true || Array.isArray(c.recps)) return null; + if (typeof c.root === 'string') { + return `/blogs/${encodeURIComponent(c.root)}#${encodeURIComponent(id)}`; + } + if (c.fork || c.branch || c.about || c.target) return null; + return `/blogs/${encodeURIComponent(id)}`; + } + }; +}; diff --git a/src/models/bookmarking_model.js b/src/models/bookmarking_model.js index 144fb96..df6c3ac 100644 --- a/src/models/bookmarking_model.js +++ b/src/models/bookmarking_model.js @@ -3,7 +3,8 @@ const moment = require("../server/node_modules/moment"); const { getConfig } = require("../configs/config-manager.js"); const categories = require("../backend/opinion_categories"); const { buildValidatedTombstoneSet } = require('./tombstone_validator'); -const { dedupeBy, norm } = require('../backend/dedupe'); +const { readTyped, memoIndex } = require('./typed_log'); +const { dedupeBy, norm, versionGroups } = require('../backend/dedupe'); const logLimit = getConfig().ssbLogStream?.limit || 1000; @@ -36,13 +37,9 @@ module.exports = ({ cooler }) => { return ssb; }; - const getAllMessages = async (ssbClient) => - new Promise((resolve, reject) => { - pull( - ssbClient.createLogStream({ limit: logLimit }), - pull.collect((err, msgs) => (err ? reject(err) : resolve(msgs))) - ); - }); + const BOOKMARK_TYPES = ["bookmark", "bookmarkOpinion", "tombstone"]; + + const getAllMessages = async (ssbClient) => readTyped(ssbClient, BOOKMARK_TYPES, { limit: logLimit }); const getMsg = async (ssbClient, key) => new Promise((resolve, reject) => { @@ -52,9 +49,6 @@ module.exports = ({ cooler }) => { const buildIndex = (messages) => { const tomb = buildValidatedTombstoneSet(messages); const nodes = new Map(); - const parent = new Map(); - const child = new Map(); - const strictChild = new Map(); const opinionMsgs = []; for (const m of messages) { @@ -70,48 +64,16 @@ module.exports = ({ cooler }) => { const ts = v.timestamp || m.timestamp || 0; let sizeBytes = 0; try { sizeBytes = Buffer.byteLength(JSON.stringify(v), "utf8"); } catch (_) { sizeBytes = 0; } - nodes.set(k, { key: k, ts, c, sizeBytes, author: v.author }); - - if (c.replaces) { - parent.set(k, c.replaces); - child.set(c.replaces, k); - } + nodes.set(k, { key: k, ts, seq: v.sequence || 0, c, sizeBytes, author: v.author }); } - for (const [k, t] of Array.from(parent.entries())) { - const cn = nodes.get(k); - const pn = nodes.get(t); - if (!pn) { parent.delete(k); if (child.get(t) === k) child.delete(t); continue; } - if (!cn || String(cn.author) !== String(pn.author)) { parent.delete(k); if (child.get(t) === k) child.delete(t); nodes.delete(k); } - } - - for (const [k, node] of nodes) { - const t = node.c.replaces; - if (t) { const orig = nodes.get(t); if (orig && orig.author === node.author) strictChild.set(t, k); } - } - - const rootOf = (id) => { - let cur = id, g = 0; - while (parent.has(cur) && nodes.has(parent.get(cur)) && g++ < 100000) cur = parent.get(cur); - return cur; - }; - - const contentTipOf = (root) => { - let cur = root, g = 0; - while (strictChild.has(cur) && g++ < 100000) cur = strictChild.get(cur); - const n = nodes.get(cur), rn = nodes.get(root); - return (n && rn && n.author === rn.author) ? cur : root; - }; - - const roots = new Set(); - for (const id of nodes.keys()) roots.add(rootOf(id)); + const vg = versionGroups([...nodes.values()].map(n => ({ key: n.key, author: n.author, seq: n.seq, ts: n.ts, replaces: n.c.replaces || null, claimedAuthor: typeof n.c.author === "string" ? n.c.author : null }))); + const rootOf = vg.rootOf; + const contentTipOf = vg.tipOf; const opinionsByRoot = new Map(); for (const op of opinionMsgs) { if (!nodes.has(op.target)) continue; const r = rootOf(op.target); if (!opinionsByRoot.has(r)) opinionsByRoot.set(r, []); opinionsByRoot.get(r).push(op); } - const forward = new Map(); - for (const [newId, oldId] of parent.entries()) forward.set(oldId, newId); - const resolveGroup = (root) => { const contentTip = contentTipOf(root); const contentNode = nodes.get(contentTip) || nodes.get(root); @@ -128,9 +90,9 @@ module.exports = ({ cooler }) => { }; const tipByRoot = new Map(); - for (const r of roots) tipByRoot.set(r, contentTipOf(r)); + for (const r of vg.roots) tipByRoot.set(r, contentTipOf(r)); - return { tomb, nodes, parent, child, rootOf, tipByRoot, forward, resolveGroup }; + return { tomb, nodes, rootOf, tipByRoot, resolveGroup }; }; const buildBookmark = (node, rootId, viewerId, agg) => { @@ -145,7 +107,6 @@ module.exports = ({ cooler }) => { updatedAt: c.updatedAt || null, lastVisit: c.lastVisit || null, tags: safeArr(c.tags), - category: c.category || "", description: c.description || "", opinions, opinions_inhabitants: voters, @@ -173,16 +134,12 @@ module.exports = ({ cooler }) => { const messages = await getAllMessages(ssbClient); const idx = buildIndex(messages); - let tip = id; - while (idx.forward.has(tip)) tip = idx.forward.get(tip); - if (idx.tomb.has(tip)) throw new Error("Bookmark not found"); - - let root = tip; - while (idx.parent.has(root)) root = idx.parent.get(root); + const root = idx.rootOf(id); + if (idx.tomb.has(idx.tipByRoot.get(root) || id)) throw new Error("Bookmark not found"); return root; }, - async createBookmark(url, tagsRaw, description, category, lastVisit) { + async createBookmark(url, tagsRaw, description, lastVisit) { const ssbClient = await openSsb(); const now = new Date().toISOString(); @@ -195,7 +152,6 @@ module.exports = ({ cooler }) => { url: u, tags: normalizeTags(tagsRaw), description: description || "", - category: category || "", createdAt: now, updatedAt: now, lastVisit: coerceLastVisit(lastVisit), @@ -231,7 +187,6 @@ module.exports = ({ cooler }) => { url, tags: updatedData.tags !== undefined ? normalizeTags(updatedData.tags) : safeArr(oldMsg.content.tags), description: updatedData.description !== undefined ? updatedData.description || "" : oldMsg.content.description || "", - category: updatedData.category !== undefined ? updatedData.category || "" : oldMsg.content.category || "", lastVisit: coerceLastVisit(updatedData.lastVisit || oldMsg.content.lastVisit), createdAt: oldMsg.content.createdAt, updatedAt: now @@ -296,11 +251,10 @@ module.exports = ({ cooler }) => { if (q) { list = list.filter((b) => { const url = String(b.url || "").toLowerCase(); - const cat = String(b.category || "").toLowerCase(); const desc = String(b.description || "").toLowerCase(); const tags = safeArr(b.tags).join(" ").toLowerCase(); const author = String(b.author || "").toLowerCase(); - return url.includes(q) || cat.includes(q) || desc.includes(q) || tags.includes(q) || author.includes(q); + return url.includes(q) || desc.includes(q) || tags.includes(q) || author.includes(q); }); } @@ -322,10 +276,10 @@ module.exports = ({ cooler }) => { const viewer = viewerId || ssbClient.id; const messages = await getAllMessages(ssbClient); - const idx = buildIndex(messages); + const idx = memoIndex('bookmarking', messages, buildIndex); const rootId = idx.rootOf(id); const agg = idx.resolveGroup(rootId); - if (!agg.contentNode || idx.tomb.has(agg.contentTip)) throw new Error("Bookmark not found"); + if (!idx.tipByRoot.has(rootId) || !agg.contentNode || idx.tomb.has(agg.contentTip)) throw new Error("Bookmark not found"); return buildBookmark(agg.contentNode, rootId, viewer, agg); }, diff --git a/src/models/calendars_model.js b/src/models/calendars_model.js index 5eb49ce..e21f286 100644 --- a/src/models/calendars_model.js +++ b/src/models/calendars_model.js @@ -2,6 +2,7 @@ const pull = require("../server/node_modules/pull-stream") const crypto = require("crypto") const { getConfig } = require("../configs/config-manager.js") const { buildValidatedTombstoneSet } = require('./tombstone_validator') +const { readTyped, memoIndex } = require('./typed_log') const { collabContent, openInviteOf } = require('../backend/collab_content') const calCollab = collabContent({ membersField: 'participants', undecField: 'encrypted', contentFields: ['title', 'deadline', 'status'], listFields: ['tags', 'invites'] }) const logLimit = getConfig().ssbLogStream?.limit || 1000 @@ -14,25 +15,40 @@ const normalizeTags = (raw) => { return String(raw).split(",").map(t => t.trim()).filter(Boolean) } const hasAnyInterval = (w, m, y) => !!(w || m || y) -const expandRecurrence = (firstDate, deadline, weekly, monthly, yearly) => { - const start = new Date(firstDate) - const out = [start] - if (!deadline || !hasAnyInterval(weekly, monthly, yearly)) return out - const end = new Date(deadline).getTime() - const seen = new Set([start.getTime()]) - const walk = (mutate) => { - const n = new Date(start) - mutate(n) - while (n.getTime() <= end) { - const t = n.getTime() - if (!seen.has(t)) { seen.add(t); out.push(new Date(n)) } - mutate(n) - } - } - if (weekly) walk((d) => d.setDate(d.getDate() + 7)) - if (monthly) walk((d) => d.setMonth(d.getMonth() + 1)) - if (yearly) walk((d) => d.setFullYear(d.getFullYear() + 1)) - return out.sort((a, b) => a.getTime() - b.getTime()) +const { expandRecurrence, truthy } = require('./recurrence') +const MAX_OCCURRENCES_PER_DATE = 520 +const MAX_DATES_PER_VIEW = 5000 + +const ts = (v) => { + const t = new Date(v).getTime() + return Number.isFinite(t) ? t : null +} + +const boundedOccurrences = (date, until, weekly, monthly, yearly) => { + const start = ts(date) + const end = ts(until) + if (start === null || end === null) return expandRecurrence(date, until, weekly, monthly, yearly).slice(0, MAX_OCCURRENCES_PER_DATE) + const limit = new Date(start) + if (truthy(weekly)) limit.setUTCDate(limit.getUTCDate() + 7 * MAX_OCCURRENCES_PER_DATE) + else if (truthy(monthly)) limit.setUTCMonth(limit.getUTCMonth() + MAX_OCCURRENCES_PER_DATE) + else limit.setUTCFullYear(limit.getUTCFullYear() + MAX_OCCURRENCES_PER_DATE) + const stop = Number.isFinite(limit.getTime()) ? Math.min(end, limit.getTime()) : end + return expandRecurrence(date, new Date(stop).toISOString(), weekly, monthly, yearly).slice(0, MAX_OCCURRENCES_PER_DATE) +} + +const assertCalendarDates = ({ deadline, date, until }) => { + const now = Date.now() + const dl = deadline ? ts(deadline) : null + const dt = date ? ts(date) : null + const un = until ? ts(until) : null + if (deadline && dl === null) throw new Error("Deadline is not a valid date") + if (date && dt === null) throw new Error("Date is not a valid date") + if (until && un === null) throw new Error("The recurrence end is not a valid date") + if (dl !== null && dl <= now) throw new Error("Deadline must be in the future") + if (dt !== null && dt <= now) throw new Error("Date must be in the future") + if (dt !== null && dl !== null && dt > dl) throw new Error("Date cannot be later than the deadline") + if (un !== null && dt !== null && un < dt) throw new Error("The recurrence cannot end before it starts") + if (un !== null && dl !== null && un > dl) throw new Error("The recurrence cannot end after the deadline") } module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) => { @@ -47,6 +63,7 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) return (tribeCrypto && tribeCrypto.getKeys(rid)) || [] } const lookupGen = (rid) => ((ownCrypto && ownCrypto.getGen(rid)) || (tribeCrypto && tribeCrypto.getGen(rid)) || 0) + const keyProofFor = (keyHex, id) => { try { return crypto.createHmac("sha256", Buffer.from(keyHex, "hex")).update(String(id), "utf8").digest("hex") } catch (_) { return null } } const rotateCalendarKey = async (rootId, remainingMembers) => { if (!ownCrypto || !tribeCrypto || !rootId) return @@ -54,11 +71,10 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) if (!existing) return const newKey = ownCrypto.generateTribeKey() const newGen = ownCrypto.addNewKey(rootId, newKey) - if (!Array.isArray(remainingMembers) || !remainingMembers.length) return const ssbClient = await openSsb() const ssbKeys = require("../server/node_modules/ssb-keys") const memberKeys = {} - for (const m of remainingMembers) { + for (const m of new Set([ssbClient.id, ...(Array.isArray(remainingMembers) ? remainingMembers : [])])) { try { memberKeys[m] = tribeCrypto.boxKeyForMember(newKey, m, ssbKeys) } catch (_) {} } if (Object.keys(memberKeys).length) { @@ -75,9 +91,12 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) const ssbKeys = require("../server/node_modules/ssb-keys") const config = require("../server/ssb_config") const msgs = await readAll(ssbClient) + const authorOf = new Map() + for (const m of msgs) if (m && m.key && m.value) authorOf.set(m.key, m.value.author) for (const m of msgs) { const c = m.value && m.value.content if (!c || c.type !== "tribe-keys") continue + if (!c.tribeId || !authorOf.has(c.tribeId) || (m.value.author !== authorOf.get(c.tribeId) && m.value.author !== ssbClient.id)) continue const memberKeys = c.memberKeys if (!memberKeys || typeof memberKeys !== "object") continue const boxed = memberKeys[ssbClient.id] @@ -91,12 +110,45 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) } catch (_) {} } - const readAll = async (ssbClient) => - new Promise((resolve, reject) => - pull(ssbClient.createLogStream({ limit: logLimit }), pull.collect((err, msgs) => err ? reject(err) : resolve(msgs))) - ) + const CALENDAR_TYPES = ["calendar", "calendarDate", "calendarNote", "calendarParticipant", "calendarReminderSent", "tribe-keys", "tombstone"] + + const readAll = async (ssbClient) => unwrapForIndex(await readTyped(ssbClient, CALENDAR_TYPES, { limit: logLimit, withWindow: true })) + + const ensureMemberKeys = async (ssbClient, messages, cals) => { + if (!tribeCrypto || !ownCrypto) return + const leavesByRoot = new Map() + for (const m of messages) { + const c = m.value && m.value.content + if (!c || c.type !== "calendarParticipant" || c.on !== false || !c.target) continue + if (!leavesByRoot.has(c.target)) leavesByRoot.set(c.target, []) + leavesByRoot.get(c.target).push({ member: m.value.author, ts: Number(m.timestamp || m.value.timestamp || 0) }) + } + const ssbKeys = require("../server/node_modules/ssb-keys") + for (const cal of (Array.isArray(cals) ? cals : [])) { + if (!cal || cal.tribeId || cal.encrypted || cal.author !== ssbClient.id) continue + const rootId = cal.rootId + if (!rootId || !lookupKey(rootId)) continue + const members = Array.isArray(cal.participants) ? cal.participants : [] + const plan = ownCrypto.keyPlan({ rootId, ownerId: cal.author, gen: lookupGen(rootId), members, messages, leaves: leavesByRoot.get(rootId) || [] }) + if (plan.rotate) { + await rotateCalendarKey(rootId, members) + continue + } + if (!plan.missing.length) continue + const key = lookupKey(rootId) + const memberKeys = {} + for (const m of plan.missing) { + try { memberKeys[m] = tribeCrypto.boxKeyForMember(key, m, ssbKeys) } catch (_) {} + } + if (!Object.keys(memberKeys).length) continue + await new Promise((resolve) => { + ssbClient.publish({ type: "tribe-keys", tribeId: rootId, generation: lookupGen(rootId) || 1, memberKeys }, () => resolve()) + }) + } + } const tribeHelpers = tribeCrypto ? tribeCrypto.createHelpers(tribesModel) : null + const unwrapForIndex = (msgs) => tribeHelpers ? tribeHelpers.unwrapMessagesForKind(msgs, CALENDAR_TYPES) : msgs const encryptIfTribe = tribeHelpers ? tribeHelpers.encryptIfTribe : async (c) => c const decryptIfTribe = tribeHelpers ? tribeHelpers.decryptIfTribe : async (c) => c const assertReadable = tribeHelpers ? tribeHelpers.assertReadable : () => {} @@ -108,6 +160,12 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) return tribeCrypto.encryptContent(content, [key], true) } + const decryptScoped = async (content, rootId) => { + if (!content) return content + if (content.tribeId || (tribeCrypto && tribeCrypto.isTribeMsg(content))) return await decryptIfTribe(content) + return decryptCalendarRoot(content, rootId) + } + const decryptCalendarRoot = (content, rootId) => { if (!content || !content.encryptedPayload) return content if (!tribeCrypto) return content @@ -178,9 +236,9 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) const c = v.content if (!c) continue if (c.type === "tombstone" && c.target) { tombRequests.push({ target: c.target, author: v.author }); continue } - if (c.type === "calendarParticipant" && c.target) { participantMsgs.push({ target: c.target, author: v.author, on: c.on !== false, ts: v.timestamp || m.timestamp || 0 }); continue } + if (c.type === "calendarParticipant" && c.target) { participantMsgs.push({ target: c.target, author: v.author, on: c.on !== false, keyProof: typeof c.keyProof === "string" ? c.keyProof : "", code: typeof c.code === "string" ? c.code : "", ts: v.timestamp || m.timestamp || 0 }); continue } if (c.type === "calendar") { - nodes.set(k, { key: k, ts: v.timestamp || m.timestamp || 0, c, author: v.author }) + nodes.set(k, { key: k, ts: v.timestamp || m.timestamp || 0, seq: v.sequence || 0, c, author: v.author }) authorByKey.set(k, v.author) if (c.replaces) { parent.set(k, c.replaces); child.set(c.replaces, k) } } @@ -200,30 +258,31 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) const rootOf = (id) => { let cur = id; while (parent.has(cur)) cur = parent.get(cur); return cur } const tipOf = (id) => { let cur = id; while (child.has(cur)) cur = child.get(cur); return cur } - const contentTipOf = (root) => { - const rn = nodes.get(root) - if (!rn) return root - let cur = root - let best = root - const seen = new Set() - while (child.has(cur) && !seen.has(cur)) { - seen.add(cur) - const next = child.get(cur) - const n = nodes.get(next) - if (!n) break - if (n.author === rn.author && !tomb.has(next)) best = next - cur = next - } - return best + const ownerTip = new Map() + for (const [k, n] of nodes.entries()) { + const r = rootOf(k) + const rn = nodes.get(r) + if (!rn || n.author !== rn.author || tomb.has(k)) continue + const prev = ownerTip.has(r) ? nodes.get(ownerTip.get(r)) : null + if (!prev || n.seq > prev.seq || (n.seq === prev.seq && n.ts >= prev.ts)) ownerTip.set(r, k) } + const contentTipOf = (root) => ownerTip.get(root) || root const roots = new Set() for (const id of nodes.keys()) roots.add(rootOf(id)) + const joinAllowed = (root, pm) => { + const rn = nodes.get(root) + if (!rn) return false + if (pm.author === rn.author || rn.c.tribeId) return true + if (!rn.c.encryptedPayload && String(rn.c.status || "OPEN").toUpperCase() === "OPEN") return true + return !!pm.keyProof && lookupKeys(root).some(k => keyProofFor(k, pm.author) === pm.keyProof) + } const participantsByRoot = new Map() for (const pm of participantMsgs) { if (!nodes.has(pm.target)) continue const r = rootOf(pm.target) + if (pm.on && !joinAllowed(r, pm)) continue if (!participantsByRoot.has(r)) participantsByRoot.set(r, new Map()) const m2 = participantsByRoot.get(r) const prev = m2.get(pm.author) @@ -259,17 +318,30 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) status: c.status || "OPEN", deadline: undec ? "" : (c.deadline || ""), tags: Array.isArray(c.tags) ? c.tags : [], - author: c.author || node.author, + mapUrl: typeof c.mapUrl === "string" ? c.mapUrl : "", + author: node.author || c.author, participants: Array.isArray(participants) ? participants : (Array.isArray(c.participants) ? c.participants : []), invites: Array.isArray(c.invites) ? c.invites : [], createdAt: c.createdAt || new Date(node.ts).toISOString(), updatedAt: c.updatedAt || null, tribeId: c.tribeId || null, - encrypted: !!undec + encrypted: !!undec, + contentEncrypted: !!(c.encryptedPayload || c._decrypted === true) } } + const calendarMembers = async (cal) => { + const set = new Set([cal.author, ...(Array.isArray(cal.participants) ? cal.participants : [])].filter(Boolean)) + if (cal.tribeId && tribesModel) { + try { + const t = await tribesModel.getTribeById(cal.tribeId) + for (const m of (t && Array.isArray(t.members) ? t.members : [])) set.add(m) + } catch (_) {} + } + return set + } + const isClosed = (calendar) => { if (calendar.status === "CLOSED") return true if (!calendar.deadline) return false @@ -305,17 +377,11 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) try { const ssbClient = await openSsb() const messages = await readAll(ssbClient) - const live = new Set() const tomb = buildValidatedTombstoneSet(messages) - for (const m of messages) { - const c = m.value && m.value.content - if (!c) continue - if (c.type === "calendar") live.add(m.key) - } const all = ownCrypto.getAllRootIds() let removed = 0 for (const rid of all) { - if (!live.has(rid) || tomb.has(rid)) { + if (tomb.has(rid)) { try { ownCrypto.dropKey(rid); removed += 1 } catch (_) {} } } @@ -327,11 +393,8 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) const ssbClient = await openSsb() const messages = await readAll(ssbClient) const idx = buildIndex(messages) - let tip = id - while (idx.child.has(tip)) tip = idx.child.get(tip) - if (idx.tomb.has(tip)) throw new Error("Not found") - let root = tip - while (idx.parent.has(root)) root = idx.parent.get(root) + const root = idx.rootOf(id) + if (idx.tomb.has(idx.contentTipOf(root))) throw new Error("Not found") return root }, @@ -339,20 +402,23 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) const ssbClient = await openSsb() const messages = await readAll(ssbClient) const idx = buildIndex(messages) - let tip = id - while (idx.child.has(tip)) tip = idx.child.get(tip) + const tip = idx.contentTipOf(idx.rootOf(id)) if (idx.tomb.has(tip)) throw new Error("Not found") return tip }, - async createCalendar({ title, status, deadline, tags, firstDate, firstDateLabel, firstNote, intervalWeekly, intervalMonthly, intervalYearly, tribeId }) { + async createCalendar({ title, status, deadline, tags, firstDate, firstDateLabel, firstNote, intervalWeekly, intervalMonthly, intervalYearly, intervalDeadline, mapUrl, tribeId }) { const ssbClient = await openSsb() const userId = ssbClient.id const now = new Date().toISOString() const validStatus = ["OPEN", "CLOSED"].includes(String(status).toUpperCase()) ? String(status).toUpperCase() : "OPEN" - if (deadline && new Date(deadline).getTime() <= Date.now()) throw new Error("Deadline must be in the future") - if (!firstDate || new Date(firstDate).getTime() <= Date.now()) throw new Error("First date must be in the future") + if (!firstDate) throw new Error("First date must be in the future") + assertCalendarDates({ + deadline, + date: firstDate, + until: hasAnyInterval(intervalWeekly, intervalMonthly, intervalYearly) ? intervalDeadline : "" + }) let plainContent = { type: "calendar", @@ -360,6 +426,7 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) status: validStatus, deadline: deadline || "", tags: normalizeTags(tags), + mapUrl: safeText(mapUrl), author: userId, participants: [userId], invites: [], @@ -406,6 +473,7 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) status: validStatus, deadline: dec.deadline || "", tags: Array.isArray(dec.tags) ? dec.tags : [], + mapUrl: dec.mapUrl || "", author: userId, participants: [userId], invites: [{ code: pubCode, ek, salt: inviteSalt, gen: 1, public: true }], @@ -430,7 +498,7 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) ...(intervalWeekly ? { intervalWeekly: true } : {}), ...(intervalMonthly ? { intervalMonthly: true } : {}), ...(intervalYearly ? { intervalYearly: true } : {}), - ...(deadline && hasAnyInterval(intervalWeekly, intervalMonthly, intervalYearly) ? { intervalDeadline: deadline } : {}), + ...(hasAnyInterval(intervalWeekly, intervalMonthly, intervalYearly) && (intervalDeadline || deadline) ? { intervalDeadline: intervalDeadline || deadline } : {}), ...(tribeId ? { tribeId } : {}) } if (tribeId) dateContent = await encryptIfTribe(dateContent) @@ -468,26 +536,32 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) ssbClient.get(tipId, (err, it) => err ? reject(err) : resolve(it)) }) if (!item || !item.content) throw new Error("Calendar not found") - const oldDec = item.content.tribeId - ? await decryptIfTribe(item.content) - : decryptCalendarRoot(item.content, rootId) + const oldDec = await decryptScoped(item.content, rootId) assertReadable(oldDec, "Calendar") - if ((oldDec.author || item.content.author) !== userId) throw new Error("Not the author") + if (item.author !== userId) throw new Error("Not the author") + const nextDeadline = data.deadline !== undefined ? data.deadline : (oldDec.deadline || "") + if (nextDeadline !== (oldDec.deadline || "")) assertCalendarDates({ deadline: nextDeadline }) let updated = { type: "calendar", title: data.title !== undefined ? safeText(data.title) : (oldDec.title || ""), status: data.status !== undefined ? (["OPEN","CLOSED"].includes(String(data.status).toUpperCase()) ? String(data.status).toUpperCase() : oldDec.status) : (oldDec.status || "OPEN"), - deadline: data.deadline !== undefined ? data.deadline : (oldDec.deadline || ""), + deadline: nextDeadline, tags: data.tags !== undefined ? normalizeTags(data.tags) : (Array.isArray(oldDec.tags) ? oldDec.tags : []), + mapUrl: data.mapUrl !== undefined ? safeText(data.mapUrl) : (oldDec.mapUrl || ""), author: oldDec.author || userId, participants: oldDec.participants || [userId], invites: Array.isArray(oldDec.invites) ? oldDec.invites : [], - ...(item.content.tribeId ? { tribeId: item.content.tribeId } : {}), + ...(oldDec.tribeId ? { tribeId: oldDec.tribeId } : {}), createdAt: oldDec.createdAt, updatedAt: new Date().toISOString(), replaces: tipId } - if (item.content.tribeId) updated = await encryptIfTribe(updated) + const closing = !oldDec.tribeId && String(oldDec.status || "OPEN").toUpperCase() === "OPEN" && updated.status === "CLOSED" + if (closing) { + updated.invites = updated.invites.filter(inv => !(inv && typeof inv === "object" && inv.public === true)) + try { await rotateCalendarKey(rootId, (updated.participants || []).filter(p => p !== userId)) } catch (_) {} + } + if (updated.tribeId) updated = await encryptIfTribe(updated) else updated = encryptStandalone(updated, rootId) const result = await new Promise((resolve, reject) => ssbClient.publish(updated, (e, res) => e ? reject(e) : resolve(res))) const tombstone = { type: "tombstone", target: tipId, deletedAt: new Date().toISOString(), author: userId } @@ -501,10 +575,10 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) const userId = ssbClient.id const item = await new Promise((resolve, reject) => ssbClient.get(tipId, (e, it) => e ? reject(e) : resolve(it))) if (!item || !item.content) throw new Error("Calendar not found") - const dec = await decryptIfTribe(item.content) + const rootId = await this.resolveRootId(id) + const dec = await decryptScoped(item.content, rootId) assertReadable(dec, "Calendar") - const contentAuthor = (dec && dec.author) || (typeof item.content === 'object' && item.content.author) - if (contentAuthor !== userId) throw new Error("Not the author") + if (item.author !== userId) throw new Error("Not the author") const tombstone = { type: "tombstone", target: tipId, deletedAt: new Date().toISOString(), author: userId } return new Promise((resolve, reject) => ssbClient.publish(tombstone, e => e ? reject(e) : resolve())) }, @@ -535,9 +609,7 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) const participants = Array.isArray(cal.participants) ? cal.participants : [] if (!participants.includes(userId)) return const content = { type: "calendarParticipant", target: rootId, on: false, createdAt: new Date().toISOString() } - const result = await new Promise((resolve, reject) => ssbClient.publish(content, (e, res) => e ? reject(e) : resolve(res))) - try { await rotateCalendarKey(rootId, participants.filter(p => p !== userId)) } catch (_) {} - return result + return new Promise((resolve, reject) => ssbClient.publish(content, (e, res) => e ? reject(e) : resolve(res))) }, async findCalendarByLinkText(linkSubstring) { @@ -559,7 +631,7 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) async getCalendarById(id) { const ssbClient = await openSsb() const messages = await readAll(ssbClient) - const idx = buildIndex(messages) + const idx = memoIndex('calendars', messages, buildIndex) await decryptIndexNodes(idx) let root = id while (idx.parent.has(root)) root = idx.parent.get(root) @@ -577,6 +649,7 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) const ssbClient = await openSsb() const uid = viewerId || ssbClient.id let list = calCollab.visibleThenCollapsed(await collectCalendars(), uid) + try { await ensureMemberKeys(ssbClient, await readAll(ssbClient), list) } catch (_) {} if (filter === "mine") list = list.filter(c => c.author === uid) else if (filter === "recent") { const now = Date.now() @@ -594,10 +667,19 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) const cal = await this.getCalendarById(rootId) if (!cal) throw new Error("Calendar not found") if (cal.status === "CLOSED" && userId !== cal.author) throw new Error("Only the author can add dates to a CLOSED calendar") - if (!date || new Date(date).getTime() <= Date.now()) throw new Error("Date must be in the future") - + if (!date) throw new Error("Date must be in the future") const hasInterval = hasAnyInterval(intervalWeekly, intervalMonthly, intervalYearly) + assertCalendarDates({ + deadline: cal.deadline || "", + date, + until: hasInterval ? intervalDeadline : "" + }) + const ruleDeadline = hasInterval ? (intervalDeadline || cal.deadline || "") : "" + if (!(await calendarMembers(cal)).has(userId)) { + if (cal.tribeId || cal.status !== "OPEN") throw new Error("Only participants can add dates") + await this.joinCalendar(rootId) + } let dateContent = { type: "calendarDate", calendarId: rootId, @@ -623,6 +705,8 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) const rootId = await this.resolveRootId(calendarId) const cal = await this.getCalendarById(rootId) const calDeadline = cal && cal.deadline ? cal.deadline : "" + const pubKey = cal ? tryDecryptPublicInviteKey(cal.invites) : null + const members = cal ? await calendarMembers(cal) : new Set() const ssbClient = await openSsb() const messages = await readAll(ssbClient) const authorByKey = new Map() @@ -642,6 +726,7 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) const c = v.content if (!c || c.type !== "calendarDate") continue if (c.calendarId !== rootId) continue + if (!members.has(v.author)) continue let dec = c if (c.encryptedPayload && tribeCrypto) { if (c.tribeId && tribesModel) { @@ -649,25 +734,24 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) dec = r && !r._undecryptable ? r : c if (r && r._undecryptable) continue } else { - const keys = lookupKeys(c.calendarId) - if (keys && keys.length) { - const r = tribeCrypto.decryptContent(c, keys.map(k => [k])) - dec = r && !r._undecryptable ? r : c - if (r && r._undecryptable) continue - } + const keys = [...(lookupKeys(c.calendarId) || []), ...(pubKey ? [pubKey] : [])] + if (!keys.length) continue + const r = tribeCrypto.decryptContent(c, keys.map(k => [k])) + if (!r || r._undecryptable) continue + dec = r } } const baseEntry = { key: m.key, calendarId: dec.calendarId || c.calendarId, label: dec.label || "", - author: dec.author || v.author, + author: v.author || dec.author, createdAt: dec.createdAt || new Date(v.timestamp || 0).toISOString() } const hasInterval = !!(dec.intervalWeekly || dec.intervalMonthly || dec.intervalYearly) const ruleDeadline = dec.intervalDeadline || calDeadline if (hasInterval && ruleDeadline) { - const occurrences = expandRecurrence(dec.date, ruleDeadline, dec.intervalWeekly, dec.intervalMonthly, dec.intervalYearly) + const occurrences = boundedOccurrences(dec.date, ruleDeadline, dec.intervalWeekly, dec.intervalMonthly, dec.intervalYearly) for (const occ of occurrences) { dates.push({ ...baseEntry, date: occ.toISOString() }) } @@ -675,8 +759,10 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) dates.push({ ...baseEntry, date: dec.date }) } } - dates.sort((a, b) => new Date(a.date) - new Date(b.date)) - return dates + const owner = cal ? cal.author : null + const kept = [...dates.filter(d => d.author === owner), ...dates.filter(d => d.author !== owner)].slice(0, MAX_DATES_PER_VIEW) + kept.sort((a, b) => new Date(a.date) - new Date(b.date)) + return kept }, async deleteDate(dateId, calendarId) { @@ -754,8 +840,7 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) const userId = ssbClient.id const item = await new Promise((resolve, reject) => ssbClient.get(noteId, (e, it) => e ? reject(e) : resolve(it))) if (!item || !item.content) throw new Error("Note not found") - const dec = await decryptIfTribe(item.content) - if ((dec.author || item.content.author) !== userId) throw new Error("Not the author") + if (item.author !== userId) throw new Error("Not the author") return new Promise((resolve, reject) => { ssbClient.publish({ type: "tombstone", target: noteId, deletedAt: new Date().toISOString(), author: userId }, (e, msg) => e ? reject(e) : resolve(msg)) }) @@ -776,15 +861,23 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) } } const notes = [] + let pubKey for (const m of messages) { const v = m.value || {} const c = v.content if (!c || c.type !== "calendarNote") continue if (tombstoned.has(m.key)) continue - if (c.calendarId !== rootId || c.dateId !== dateId) continue + if (c.calendarId !== rootId || (dateId != null && c.dateId !== dateId)) continue let dec = c - if (c.encryptedPayload && tribeCrypto && tribesModel) { - const r = await tribeCrypto.decryptFromTribe(c, tribesModel) + if (c.encryptedPayload) { + let r = await decryptScoped(c, rootId) + if ((!r || r._undecryptable) && !c.tribeId && tribeCrypto) { + if (pubKey === undefined) { + const cal = await this.getCalendarById(rootId).catch(() => null) + pubKey = cal ? tryDecryptPublicInviteKey(cal.invites) : null + } + if (pubKey) r = tribeCrypto.decryptContent(c, [[pubKey]]) + } if (r && !r._undecryptable) dec = r else continue } @@ -793,7 +886,7 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) calendarId: dec.calendarId || c.calendarId, dateId: dec.dateId || c.dateId, text: dec.text || "", - author: dec.author || v.author, + author: v.author || dec.author, createdAt: dec.createdAt || new Date(v.timestamp || 0).toISOString() }) } @@ -826,33 +919,36 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) } } - const calendarDeadlines = new Map() + const calendarInfo = new Map() const dueByCalendar = new Map() - for (const m of messages) { - if (tombstoned.has(m.key)) continue + const dated = messages.filter(m => !tombstoned.has(m.key) && ((m.value || {}).content || {}).type === "calendarDate") + const decoded = await Promise.all(dated.map(async (m) => { + const c = m.value.content + if (!(c.encryptedPayload && tribeCrypto && tribesModel)) return c + const r = await tribeCrypto.decryptFromTribe(c, tribesModel) + return !r || r._undecryptable ? null : r + })) + for (let i = 0; i < dated.length; i++) { + const m = dated[i] const v = m.value || {} const c = v.content - if (!c || c.type !== "calendarDate") continue - let dec = c - if (c.encryptedPayload && tribeCrypto && tribesModel) { - const r = await tribeCrypto.decryptFromTribe(c, tribesModel) - if (!r || r._undecryptable) continue - dec = r - } - if (!dec.date) continue + const dec = decoded[i] + if (!dec || !dec.date) continue const calId = c.calendarId - let calDeadline = calendarDeadlines.get(calId) - if (calDeadline === undefined) { + let info = calendarInfo.get(calId) + if (info === undefined) { try { const cc = await this.getCalendarById(calId) - calDeadline = (cc && cc.deadline) || "" - } catch (_) { calDeadline = "" } - calendarDeadlines.set(calId, calDeadline) + info = cc ? { deadline: cc.deadline || "", members: await calendarMembers(cc) } : null + } catch (_) { info = null } + calendarInfo.set(calId, info) } + if (!info || !info.members.has(v.author)) continue + const calDeadline = info.deadline const hasInterval = !!(dec.intervalWeekly || dec.intervalMonthly || dec.intervalYearly) const ruleDeadline = dec.intervalDeadline || calDeadline const occurrences = (hasInterval && ruleDeadline) - ? expandRecurrence(dec.date, ruleDeadline, dec.intervalWeekly, dec.intervalMonthly, dec.intervalYearly) + ? boundedOccurrences(dec.date, ruleDeadline, dec.intervalWeekly, dec.intervalMonthly, dec.intervalYearly) : [new Date(dec.date)] for (const occ of occurrences) { if (occ.getTime() > now) continue @@ -916,18 +1012,19 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) if (cal.author !== userId) throw new Error("Only the author can generate invites") const code = crypto.randomBytes(INVITE_CODE_BYTES).toString("hex") let invite = code - const pubFlag = opts.public ? { public: true } : {} - if (tribeCrypto && !cal.tribeId) { + const calKey = !cal.tribeId ? lookupKey(cal.rootId) : null + if (tribeCrypto && calKey) { const inviteSalt = tribeCrypto.generateInviteSalt() - const ekChain = tribeCrypto.encryptChainForInvite([cal.rootId], code, inviteSalt) - if (ekChain) invite = { code, ekChain, salt: inviteSalt, gen: lookupGen(cal.rootId), ...pubFlag } + const ek = tribeCrypto.encryptForInvite(calKey, code, inviteSalt) + invite = opts.public + ? { code, ek, salt: inviteSalt, gen: lookupGen(cal.rootId), public: true } + : { ch: tribeCrypto.hashInviteCode(code, inviteSalt), ek, salt: inviteSalt, gen: lookupGen(cal.rootId) } } if (opts.public && typeof invite !== "object") invite = { code, public: true } const tipId = await this.resolveCurrentId(calendarId) const item = await new Promise((resolve, reject) => ssbClient.get(tipId, (e, it) => e ? reject(e) : resolve(it))) - const dec = item.content.tribeId - ? await decryptIfTribe(item.content) - : decryptCalendarRoot(item.content, cal.rootId) + const dec = await decryptScoped(item.content, cal.rootId) + assertReadable(dec, "Calendar") const invites = [...(Array.isArray(dec.invites) ? dec.invites : []), invite] let updated = { type: "calendar", @@ -935,15 +1032,16 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) status: dec.status || "OPEN", deadline: dec.deadline || "", tags: Array.isArray(dec.tags) ? dec.tags : [], + mapUrl: dec.mapUrl || "", author: dec.author, participants: Array.isArray(dec.participants) ? dec.participants : [userId], invites, - ...(item.content.tribeId ? { tribeId: item.content.tribeId } : {}), + ...(dec.tribeId ? { tribeId: dec.tribeId } : {}), createdAt: dec.createdAt, updatedAt: new Date().toISOString(), replaces: tipId } - if (item.content.tribeId) updated = await encryptIfTribe(updated) + if (updated.tribeId) updated = await encryptIfTribe(updated) else updated = encryptStandalone(updated, cal.rootId) await new Promise((resolve, reject) => ssbClient.publish(updated, (e, res) => e ? reject(e) : resolve(res))) const tombstone = { type: "tombstone", target: tipId, deletedAt: new Date().toISOString(), author: userId } @@ -971,9 +1069,8 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) if (cal.author !== userId) throw new Error("Only the author can remove invites") const tipId = await this.resolveCurrentId(calendarId) const item = await new Promise((resolve, reject) => ssbClient.get(tipId, (e, it) => e ? reject(e) : resolve(it))) - const dec = item.content.tribeId - ? await decryptIfTribe(item.content) - : decryptCalendarRoot(item.content, cal.rootId) + const dec = await decryptScoped(item.content, cal.rootId) + assertReadable(dec, "Calendar") const invites = (Array.isArray(dec.invites) ? dec.invites : []).filter(inv => !(typeof inv === "object" && inv.public === true)) let updated = { type: "calendar", @@ -981,19 +1078,21 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) status: dec.status || "OPEN", deadline: dec.deadline || "", tags: Array.isArray(dec.tags) ? dec.tags : [], + mapUrl: dec.mapUrl || "", author: dec.author, participants: Array.isArray(dec.participants) ? dec.participants : [userId], invites, - ...(item.content.tribeId ? { tribeId: item.content.tribeId } : {}), + ...(dec.tribeId ? { tribeId: dec.tribeId } : {}), createdAt: dec.createdAt, updatedAt: new Date().toISOString(), replaces: tipId } - if (item.content.tribeId) updated = await encryptIfTribe(updated) + if (updated.tribeId) updated = await encryptIfTribe(updated) else updated = encryptStandalone(updated, cal.rootId) await new Promise((resolve, reject) => ssbClient.publish(updated, (e, res) => e ? reject(e) : resolve(res))) const tombstone = { type: "tombstone", target: tipId, deletedAt: new Date().toISOString(), author: userId } await new Promise((resolve, reject) => ssbClient.publish(tombstone, e => e ? reject(e) : resolve())) + if (!dec.tribeId) await rotateCalendarKey(cal.rootId, Array.isArray(cal.participants) ? cal.participants : []) }, async joinByInvite(code) { @@ -1006,16 +1105,26 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) const invs = Array.isArray(cal.invites) ? cal.invites : [] for (const inv of invs) { if (typeof inv === "string" && inv === code) { matched = cal; matchedInvite = inv; break } - if (typeof inv === "object" && inv.code === code) { matched = cal; matchedInvite = inv; break } + if (typeof inv === "object" && inv && (inv.code === code || (inv.ch && tribeCrypto && inv.ch === tribeCrypto.hashInviteCode(code, inv.salt)))) { matched = cal; matchedInvite = inv; break } } if (matched) break } if (!matched) throw new Error("Invalid or expired invite code") if (matched.participants.includes(userId)) throw new Error("Already a participant") + if (typeof matchedInvite === "object" && matchedInvite.ch && tribeCrypto && matchedInvite.ek) { + let probeKey = null + try { probeKey = tribeCrypto.decryptFromInvite(matchedInvite.ek, code, matchedInvite.salt) } catch (_) {} + const messages = await readAll(await openSsb()) + const spent = messages.some(m => { + const c = m.value && m.value.content + return c && c.type === "calendarParticipant" && c.on !== false && c.target === matched.rootId && c.code === matchedInvite.ch && m.value.author !== userId && (!probeKey || keyProofFor(probeKey, m.value.author) === c.keyProof) + }) + if (spent) throw new Error("Invite already used") + } let calKey = null if (tribeCrypto && typeof matchedInvite === "object") { if (matchedInvite.ekChain) { - const chain = tribeCrypto.decryptChainFromInvite(matchedInvite.ekChain, code, matchedInvite.salt) + const chain = tribeCrypto.decryptChainFromInvite(matchedInvite.ekChain, code, matchedInvite.salt, 3) if (Array.isArray(chain) && chain.length) { for (const entry of chain) { if (Array.isArray(entry.keys) && entry.keys.length) { @@ -1033,9 +1142,8 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) } const tipId = await this.resolveCurrentId(matched.rootId) const item = await new Promise((resolve, reject) => ssbClient.get(tipId, (e, it) => e ? reject(e) : resolve(it))) - const dec = item.content.tribeId - ? await decryptIfTribe(item.content) - : decryptCalendarRoot(item.content, matched.rootId) + const dec = await decryptScoped(item.content, matched.rootId) + assertReadable(dec, "Calendar") const isPublicInvite = typeof matchedInvite === "object" && matchedInvite.public === true const invites = isPublicInvite ? (Array.isArray(dec.invites) ? dec.invites : []) @@ -1049,15 +1157,16 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) status: dec.status || "OPEN", deadline: dec.deadline || "", tags: Array.isArray(dec.tags) ? dec.tags : [], + mapUrl: dec.mapUrl || "", author: dec.author, participants: [...(Array.isArray(dec.participants) ? dec.participants : []), userId], invites, - ...(item.content.tribeId ? { tribeId: item.content.tribeId } : {}), + ...(dec.tribeId ? { tribeId: dec.tribeId } : {}), createdAt: dec.createdAt, updatedAt: new Date().toISOString(), replaces: tipId } - if (item.content.tribeId) updated = await encryptIfTribe(updated) + if (updated.tribeId) updated = await encryptIfTribe(updated) else updated = encryptStandalone(updated, matched.rootId) await new Promise((resolve, reject) => ssbClient.publish(updated, (e, res) => e ? reject(e) : resolve(res))) const tombstone = { type: "tombstone", target: tipId, deletedAt: new Date().toISOString(), author: userId } @@ -1077,7 +1186,7 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) } } catch (_) {} } - await new Promise((resolve, reject) => ssbClient.publish({ type: "calendarParticipant", target: matched.rootId, on: true, createdAt: new Date().toISOString() }, (e, res) => e ? reject(e) : resolve(res))) + await new Promise((resolve, reject) => ssbClient.publish({ type: "calendarParticipant", target: matched.rootId, on: true, ...(calKey ? { keyProof: keyProofFor(calKey, userId) } : {}), ...(matchedInvite && typeof matchedInvite === "object" && matchedInvite.ch ? { code: matchedInvite.ch } : {}), createdAt: new Date().toISOString() }, (e, res) => e ? reject(e) : resolve(res))) return matched.rootId } } diff --git a/src/models/campaigns_model.js b/src/models/campaigns_model.js new file mode 100644 index 0000000..add3e31 --- /dev/null +++ b/src/models/campaigns_model.js @@ -0,0 +1,312 @@ +const { readTyped, memoIndex } = require('./typed_log'); +const categories = require('../backend/opinion_categories'); +const { buildValidatedTombstoneSet } = require('./tombstone_validator'); +const { getConfig } = require('../configs/config-manager.js'); +const logLimit = getConfig().ssbLogStream?.limit || 1000; + +const CAMPAIGN_TYPE = 'campaign'; +const SIGNATURE_TYPE = 'campaignSignature'; +const UPDATE_TYPE = 'campaignUpdate'; +const OPINION_TYPE = 'campaignOpinion'; +const PROPOSAL_TYPE = 'parliamentProposal'; +const TYPES = [CAMPAIGN_TYPE, SIGNATURE_TYPE, UPDATE_TYPE, OPINION_TYPE, PROPOSAL_TYPE, 'tombstone']; +const CATEGORIES = ['ENVIRONMENT', 'RIGHTS', 'HEALTH', 'EDUCATION', 'INFRASTRUCTURE', 'CULTURE', 'ECONOMY', 'OTHER']; +const STATUSES = ['OPEN', 'ACHIEVED', 'CLOSED']; +const RECENT_MS = 24 * 60 * 60 * 1000; + +const safeText = (v) => String(v == null ? '' : v).trim(); +const normU = (v) => String(v || '').trim().toUpperCase(); +const normalizeCategory = (v) => (CATEGORIES.includes(normU(v)) ? normU(v) : 'OTHER'); +const normalizeTags = (raw) => { + if (!raw) return []; + const arr = Array.isArray(raw) ? raw : String(raw).split(','); + return Array.from(new Set(arr.map(t => String(t || '').trim()).filter(Boolean))); +}; +const toIso = (v) => { const t = Date.parse(v || ''); return Number.isFinite(t) ? new Date(t).toISOString() : ''; }; +const MEDIA_RE = /!?\[(image|video):[^\]]*\]\((&[^)]+)\)/; +const mediaOf = (text) => { const m = safeText(text).match(MEDIA_RE); return m ? { kind: m[1], blobId: m[2] } : null; }; +const goalOf = (v) => { const n = Math.floor(Number(v)); return Number.isFinite(n) && n > 0 ? n : 0; }; + +module.exports = ({ cooler }) => { + let ssb; + const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb; }; + const publish = (ssbClient, content) => new Promise((resolve, reject) => ssbClient.publish(content, (err, res) => err ? reject(err) : resolve(res))); + + const buildIndex = (messages) => { + const tomb = buildValidatedTombstoneSet(messages); + const nodes = new Map(); + const parentOf = new Map(); + const signatures = []; + const updates = []; + const opinions = []; + const proposalByCampaign = new Map(); + for (const m of messages) { + const v = m.value || {}; + const c = v.content; + if (!c || typeof c !== 'object') continue; + if (c.type === CAMPAIGN_TYPE) { + nodes.set(m.key, { key: m.key, author: v.author, ts: v.timestamp || m.timestamp || 0, c }); + if (typeof c.replaces === 'string') parentOf.set(m.key, c.replaces); + } else if (c.type === SIGNATURE_TYPE && typeof c.target === 'string') { + signatures.push({ key: m.key, target: c.target, author: v.author, ts: v.timestamp || 0, text: safeText(c.text), createdAt: c.createdAt || new Date(v.timestamp || 0).toISOString() }); + } else if (c.type === PROPOSAL_TYPE) { + const legacy = typeof c.campaignId === 'string' && c.campaignId ? null : String(c.description || '').match(/signatures: \/campaigns\/(%[^\s]+\.sha256)\s*$/); + const ref = typeof c.campaignId === 'string' && c.campaignId ? c.campaignId : (legacy ? legacy[1] : ''); + if (ref && !proposalByCampaign.has(ref)) proposalByCampaign.set(ref, m.key); + } else if (c.type === OPINION_TYPE && typeof c.target === 'string') { + opinions.push({ target: c.target, author: v.author, category: String(c.category || ''), ts: v.timestamp || 0 }); + } else if (c.type === UPDATE_TYPE && typeof c.target === 'string') { + updates.push({ key: m.key, target: c.target, author: v.author, ts: v.timestamp || 0, text: safeText(c.text), createdAt: c.createdAt || new Date(v.timestamp || 0).toISOString(), replaces: typeof c.replaces === 'string' ? c.replaces : null }); + } + } + const updateParent = new Map(updates.filter(u => u.replaces).map(u => [u.key, u.replaces])); + const updateRootOf = (key) => { + const seen = new Set(); + let cur = key; + while (updateParent.has(cur) && !seen.has(cur)) { seen.add(cur); cur = updateParent.get(cur); } + return cur; + }; + const updateVersions = new Map(); + for (const u of updates) { + const uroot = updateRootOf(u.key); + if (tomb.has(uroot)) continue; + if (!updateVersions.has(uroot)) updateVersions.set(uroot, []); + updateVersions.get(uroot).push(u); + } + const updateItems = []; + for (const [uroot, versions] of updateVersions) { + versions.sort((a, b) => a.ts - b.ts); + const first = versions[0]; + const own = versions.filter(v => v.author === first.author); + const tip = own[own.length - 1]; + updateItems.push({ id: uroot, key: uroot, tipId: tip.key, target: first.target, author: first.author, ts: first.ts, text: tip.text, createdAt: first.createdAt, updatedAt: tip.createdAt, edited: tip.key !== uroot }); + } + const rootOf = (key) => { + const seen = new Set(); + let cur = key; + while (parentOf.has(cur) && !seen.has(cur)) { seen.add(cur); cur = parentOf.get(cur); } + return cur; + }; + const versionsByRoot = new Map(); + for (const node of nodes.values()) { + const root = rootOf(node.key); + if (tomb.has(root)) continue; + if (!versionsByRoot.has(root)) versionsByRoot.set(root, []); + versionsByRoot.get(root).push(node); + } + const sigByRoot = new Map(); + for (const s of signatures) { + if (!nodes.has(s.target)) continue; + const r = rootOf(s.target); + if (!sigByRoot.has(r)) sigByRoot.set(r, new Map()); + const prev = sigByRoot.get(r).get(s.author); + if (!prev || s.ts < prev.ts) sigByRoot.get(r).set(s.author, s); + } + const opinionsByRoot = new Map(); + for (const op of opinions) { + if (!nodes.has(op.target)) continue; + const r = rootOf(op.target); + if (!opinionsByRoot.has(r)) opinionsByRoot.set(r, []); + opinionsByRoot.get(r).push(op); + } + const campaigns = new Map(); + const now = Date.now(); + for (const [root, versions] of versionsByRoot) { + versions.sort((a, b) => a.ts - b.ts); + const first = versions[0]; + const own = versions.filter(v => v.author === first.author); + const tip = own[own.length - 1]; + const c = tip.c; + const sigs = Array.from((sigByRoot.get(root) || new Map()).values()).sort((a, b) => a.ts - b.ts); + const goal = goalOf(c.goal); + const deadline = toIso(c.deadline); + const deadlineTs = Date.parse(deadline) || 0; + const expired = !!deadlineTs && deadlineTs < now; + const achieved = goal > 0 && sigs.length >= goal; + const manual = normU(c.status) === 'CLOSED'; + const status = manual ? 'CLOSED' : (achieved ? 'ACHIEVED' : (expired ? 'CLOSED' : 'OPEN')); + const thread = updateItems.filter(u => nodes.has(u.target) && rootOf(u.target) === root && u.author === first.author).sort((a, b) => a.ts - b.ts); + const ops = {}; + const voters = []; + for (const op of (opinionsByRoot.get(root) || []).sort((a, b) => a.ts - b.ts)) { + if (voters.includes(op.author) || !categories.includes(op.category)) continue; + voters.push(op.author); + ops[op.category] = (ops[op.category] || 0) + 1; + } + campaigns.set(root, { + id: root, rootId: root, tipId: tip.key, + title: safeText(c.title), text: safeText(c.text), media: mediaOf(c.text), category: normalizeCategory(c.category), + goal, deadline, deadlineTs, expired, tags: normalizeTags(c.tags), mapUrl: safeText(c.mapUrl), + status, achieved, closed: status === 'CLOSED', proposalId: safeText(c.proposalId) || proposalByCampaign.get(root) || '', + author: first.author, createdAt: first.c.createdAt || new Date(first.ts).toISOString(), + updatedAt: c.updatedAt || new Date(tip.ts).toISOString(), ts: tip.ts, + signatures: sigs, signatureCount: sigs.length, signers: sigs.map(s => s.author), + progress: goal > 0 ? Math.min(100, Math.round((sigs.length / goal) * 100)) : 0, + updates: thread, + opinions: ops, opinions_inhabitants: voters, opinionCount: voters.length, + lastActivityTs: Math.max(tip.ts, ...sigs.map(s => s.ts), ...thread.map(u => u.ts)) + }); + } + return { campaigns, rootOf, nodes, updatesById: new Map(updateItems.map(u => [u.id, u])), updateRootOf }; + }; + + const load = async () => { + const ssbClient = await openSsb(); + const idx = memoIndex('campaigns', await readTyped(ssbClient, TYPES, { limit: logLimit }), buildIndex); + return { ssbClient, idx }; + }; + + const find = (idx, id) => { + const key = String(id || ''); + if (idx.campaigns.has(key)) return idx.campaigns.get(key); + if (idx.nodes.has(key)) return idx.campaigns.get(idx.rootOf(key)) || null; + return null; + }; + + const decorate = (cp, me) => ({ + ...cp, + isOwner: cp.author === me, + signed: cp.signers.includes(me), + canSign: !cp.closed && !cp.signers.includes(me), + canElevate: cp.author === me && cp.achieved && !cp.proposalId + }); + + const versionOf = (cp, me, data = {}) => ({ + type: CAMPAIGN_TYPE, + title: safeText(data.title) || cp.title, + text: data.text !== undefined ? safeText(data.text) : cp.text, + category: data.category !== undefined ? normalizeCategory(data.category) : cp.category, + goal: data.goal !== undefined && goalOf(data.goal) ? goalOf(data.goal) : cp.goal, + deadline: data.deadline !== undefined ? toIso(data.deadline) : cp.deadline, + tags: data.tags !== undefined ? normalizeTags(data.tags) : cp.tags, + mapUrl: data.mapUrl !== undefined ? safeText(data.mapUrl) : cp.mapUrl, + status: data.status !== undefined ? (normU(data.status) === 'CLOSED' ? 'CLOSED' : 'OPEN') : (cp.status === 'CLOSED' && !cp.expired ? 'CLOSED' : 'OPEN'), + proposalId: data.proposalId !== undefined ? safeText(data.proposalId) : cp.proposalId, + author: me, + createdAt: cp.createdAt, + updatedAt: new Date().toISOString(), + replaces: cp.tipId + }); + + return { + CATEGORIES, + STATUSES, + + async createCampaign({ title, text, category, goal, deadline, tags, mapUrl }) { + const ssbClient = await openSsb(); + const t = safeText(title); + if (!t) throw new Error('Title is required'); + const g = goalOf(goal); + if (!g) throw new Error('A signature goal is required'); + const now = new Date().toISOString(); + return publish(ssbClient, { + type: CAMPAIGN_TYPE, title: t, text: safeText(text), category: normalizeCategory(category), goal: g, + deadline: toIso(deadline), tags: normalizeTags(tags), mapUrl: safeText(mapUrl), status: 'OPEN', + author: ssbClient.id, createdAt: now, updatedAt: now + }); + }, + + async updateCampaign(id, data = {}) { + const { ssbClient, idx } = await load(); + const cp = find(idx, id); + if (!cp) throw new Error('Campaign not found'); + if (cp.author !== ssbClient.id) throw new Error('Only the promoter can update this campaign'); + const res = await publish(ssbClient, versionOf(cp, ssbClient.id, data)); + return { key: res.key, rootId: cp.id }; + }, + + async closeCampaign(id) { return this.updateCampaign(id, { status: 'CLOSED' }); }, + async reopenCampaign(id) { return this.updateCampaign(id, { status: 'OPEN' }); }, + + async deleteCampaign(id) { + const { ssbClient, idx } = await load(); + const cp = find(idx, id); + if (!cp) throw new Error('Campaign not found'); + if (cp.author !== ssbClient.id) throw new Error('Only the promoter can delete this campaign'); + await publish(ssbClient, { type: 'tombstone', target: cp.id, deletedAt: new Date().toISOString(), author: ssbClient.id }); + return { key: cp.id }; + }, + + async sign(id, text = '') { + const { ssbClient, idx } = await load(); + const cp = find(idx, id); + if (!cp) throw new Error('Campaign not found'); + if (cp.closed) throw new Error('This campaign is closed'); + if (cp.signers.includes(ssbClient.id)) throw new Error('You already signed this campaign'); + return publish(ssbClient, { type: SIGNATURE_TYPE, target: cp.id, text: safeText(text).slice(0, 500), createdAt: new Date().toISOString() }); + }, + + async addUpdate(id, text) { + const { ssbClient, idx } = await load(); + const cp = find(idx, id); + if (!cp) throw new Error('Campaign not found'); + if (cp.author !== ssbClient.id) throw new Error('Only the promoter can post updates'); + const body = safeText(text); + if (!body) throw new Error('Text is required'); + return publish(ssbClient, { type: UPDATE_TYPE, target: cp.id, text: body, createdAt: new Date().toISOString() }); + }, + + async editUpdate(updateId, text) { + const { ssbClient, idx } = await load(); + const update = idx.updatesById.get(idx.updateRootOf(String(updateId || ''))); + if (!update) throw new Error('Update not found'); + if (String(update.author) !== String(ssbClient.id)) throw new Error('Only the promoter can edit an update'); + const body = safeText(text); + if (!body) throw new Error('Text is required'); + const res = await publish(ssbClient, { type: UPDATE_TYPE, target: update.target, text: body, createdAt: new Date().toISOString(), replaces: update.tipId }); + return { key: res.key, rootId: update.id, campaignId: update.target }; + }, + + async deleteUpdate(updateId) { + const { ssbClient, idx } = await load(); + const update = idx.updatesById.get(idx.updateRootOf(String(updateId || ''))); + if (!update) throw new Error('Update not found'); + if (String(update.author) !== String(ssbClient.id)) throw new Error('Only the promoter can delete an update'); + await publish(ssbClient, { type: 'tombstone', target: update.id, deletedAt: new Date().toISOString(), author: ssbClient.id }); + return { key: update.id, campaignId: update.target }; + }, + + async createOpinion(id, category) { + if (!categories.includes(category)) throw new Error('Invalid voting category'); + const { ssbClient, idx } = await load(); + const cp = find(idx, id); + if (!cp) throw new Error('Campaign not found'); + if (cp.opinions_inhabitants.includes(ssbClient.id)) throw new Error('Already voted'); + return publish(ssbClient, { type: OPINION_TYPE, target: cp.id, category, createdAt: new Date().toISOString() }); + }, + + async recordProposal(id, proposalId) { + return this.updateCampaign(id, { proposalId: safeText(proposalId) }); + }, + + async getCampaignById(id, viewerId = null) { + const { ssbClient, idx } = await load(); + const cp = find(idx, id); + return cp ? decorate(cp, viewerId || ssbClient.id) : null; + }, + + async listAll({ filter = 'all', q = '', viewerId = null } = {}) { + const { ssbClient, idx } = await load(); + const me = viewerId || ssbClient.id; + const f = String(filter || 'all').toLowerCase(); + const needle = String(q || '').trim().toLowerCase(); + let out = Array.from(idx.campaigns.values()).map(cp => decorate(cp, me)); + if (f === 'mine') out = out.filter(cp => cp.isOwner); + else if (f === 'signed') out = out.filter(cp => cp.signed); + else if (f === 'recent') out = out.filter(cp => cp.lastActivityTs >= Date.now() - RECENT_MS); + else if (f === 'top') out = out.filter(cp => cp.signatureCount > 0); + else if (STATUSES.includes(f.toUpperCase())) out = out.filter(cp => cp.status === f.toUpperCase()); + else if (CATEGORIES.includes(f.toUpperCase())) out = out.filter(cp => cp.category === f.toUpperCase()); + if (needle) out = out.filter(cp => [cp.title, cp.text, cp.category, ...cp.tags].some(v => String(v || '').toLowerCase().includes(needle))); + if (f === 'top') out.sort((a, b) => b.signatureCount - a.signatureCount || b.lastActivityTs - a.lastActivityTs); + else out.sort((a, b) => b.lastActivityTs - a.lastActivityTs); + return out; + }, + + async resolveRootId(id) { + const { idx } = await load(); + const cp = find(idx, id); + return cp ? cp.id : String(id || ''); + } + }; +}; diff --git a/src/models/chats_model.js b/src/models/chats_model.js index f4abc89..5f60594 100644 --- a/src/models/chats_model.js +++ b/src/models/chats_model.js @@ -2,6 +2,7 @@ const pull = require("../server/node_modules/pull-stream") const crypto = require("crypto") const { getConfig } = require("../configs/config-manager.js") const { buildValidatedTombstoneSet } = require('./tombstone_validator') +const { readTyped, memoIndex } = require("./typed_log") const { collabContent, openInviteOf } = require('../backend/collab_content') const chatCollab = collabContent({ membersField: 'members', undecField: 'undecryptable', contentFields: ['title', 'description', 'image', 'category', 'status'], listFields: ['tags', 'invites'] }) const logLimit = getConfig().ssbLogStream?.limit || 1000 @@ -16,13 +17,37 @@ const normalizeTags = (raw) => { const INVITE_CODE_BYTES = 16 const VALID_STATUS = ["OPEN", "INVITE-ONLY", "CLOSED"] +const REACTION_EMOJIS = ["up", "heart", "laugh", "down"] + +const DEFAULT_MESSAGES_PER_HOUR = 60 module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { let ssb const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb } + const MESSAGES_PER_HOUR = (() => { + try { + const raw = getConfig()?.chats?.messagesPerHour + const n = parseInt(raw, 10) + return Number.isFinite(n) && n > 0 ? n : DEFAULT_MESSAGES_PER_HOUR + } catch (_) { + return DEFAULT_MESSAGES_PER_HOUR + } + })() + const ownCrypto = chatCrypto || tribeCrypto const lookupKey = (rid) => (ownCrypto && ownCrypto.getKey(rid)) || (tribeCrypto && tribeCrypto.getKey(rid)) || null + const hashCode = (code, salt) => tribeCrypto ? tribeCrypto.hashInviteCode(code, salt) : null + const inviteMatches = (inv, token) => { + if (!token) return false + if (typeof inv === "string") return inv === token + if (!inv || typeof inv !== "object") return false + if (typeof inv.code === "string" && inv.code === token) return true + if (typeof inv.ch === "string" && (inv.ch === token || hashCode(token, inv.salt) === inv.ch)) return true + return false + } + const inviteToken = (inv) => (inv && typeof inv === "object" && typeof inv.ch === "string") ? inv.ch : (typeof inv === "string" ? inv : (inv && inv.code) || "") + const keyProofFor = (keyHex, id) => { try { return crypto.createHmac("sha256", Buffer.from(keyHex, "hex")).update(String(id), "utf8").digest("hex") } catch (_) { return null } } const lookupKeys = (rid) => { const a = (ownCrypto && ownCrypto.getKeys(rid)) || [] if (a.length) return a @@ -36,11 +61,10 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { if (!existing) return const newKey = ownCrypto.generateTribeKey() const newGen = ownCrypto.addNewKey(rootId, newKey) - if (!Array.isArray(remainingMembers) || !remainingMembers.length) return const ssbClient = await openSsb() const ssbKeys = require("../server/node_modules/ssb-keys") const memberKeys = {} - for (const m of remainingMembers) { + for (const m of new Set([ssbClient.id, ...(Array.isArray(remainingMembers) ? remainingMembers : [])])) { try { memberKeys[m] = tribeCrypto.boxKeyForMember(newKey, m, ssbKeys) } catch (_) {} } if (Object.keys(memberKeys).length) { @@ -57,9 +81,15 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { const ssbKeys = require("../server/node_modules/ssb-keys") const config = require("../server/ssb_config") const msgs = await readAll(ssbClient) + const idx = buildIndex(msgs) + const trusted = (root, author) => { + const n = idx.nodes.get(root) + return !!author && !!n && (author === n.author || author === ssbClient.id) + } for (const m of msgs) { const c = m.value && m.value.content if (!c || c.type !== "tribe-keys") continue + if (!trusted(c.tribeId, m.value.author)) continue const memberKeys = c.memberKeys if (!memberKeys || typeof memberKeys !== "object") continue const boxed = memberKeys[ssbClient.id] @@ -86,10 +116,9 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { return ks.length ? ks[0] : null } - const readAll = async (ssbClient) => - new Promise((resolve, reject) => - pull(ssbClient.createLogStream({ limit: logLimit }), pull.collect((err, msgs) => err ? reject(err) : resolve(msgs))) - ) + const CHAT_TYPES = ["chat", "chatMessage", "chatMember", "chatReaction", "chatPin", "tribe-keys", "tombstone"] + + const readAll = async (ssbClient) => readTyped(ssbClient, CHAT_TYPES, { limit: logLimit }) const buildIndex = (messages) => { const tomb = new Set() @@ -100,6 +129,8 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { const authorByKey = new Map() const tombRequests = [] const memberMsgs = [] + const reactMsgs = [] + const pinMsgs = [] for (const m of messages) { const k = m.key @@ -108,11 +139,19 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { if (!c) continue if (c.type === "tombstone" && c.target) { tombRequests.push({ target: c.target, author: v.author }); continue } if (c.type === "chatMember" && c.target && c.member) { - memberMsgs.push({ target: c.target, member: c.member, on: c.on !== false, code: typeof c.code === "string" ? c.code : "", author: v.author, ts: v.timestamp || m.timestamp || 0 }) + memberMsgs.push({ target: c.target, member: c.member, on: c.on !== false, code: typeof c.code === "string" ? c.code : "", keyProof: typeof c.keyProof === "string" ? c.keyProof : "", author: v.author, ts: v.timestamp || m.timestamp || 0 }) + continue + } + if (c.type === "chatReaction" && c.target && REACTION_EMOJIS.includes(c.emoji)) { + reactMsgs.push({ target: c.target, emoji: c.emoji, on: c.on !== false, author: v.author, ts: v.timestamp || m.timestamp || 0 }) + continue + } + if (c.type === "chatPin" && c.target) { + pinMsgs.push({ target: c.target, on: c.on !== false, author: v.author, ts: v.timestamp || m.timestamp || 0 }) continue } if (c.type === "chat") { - nodes.set(k, { key: k, ts: v.timestamp || m.timestamp || 0, c, author: v.author }) + nodes.set(k, { key: k, ts: v.timestamp || m.timestamp || 0, seq: v.sequence || 0, c, author: v.author }) authorByKey.set(k, v.author) if (c.replaces) { parent.set(k, c.replaces); child.set(c.replaces, k) } } else if (c.type === "chatMessage") { @@ -121,24 +160,28 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { } } - const strictParent = new Map() - const strictChild = new Map() - for (const [c1, p1] of parent.entries()) { - const childNode = nodes.get(c1) - const parentNode = nodes.get(p1) - if (childNode && parentNode && childNode.author === parentNode.author) { - strictParent.set(c1, p1) - strictChild.set(p1, c1) - } - } - for (const t of tombRequests) { const targetAuthor = authorByKey.get(t.target) if (targetAuthor && t.author === targetAuthor) tomb.add(t.target) } - const rootOf = (id) => { let cur = id; while (strictParent.has(cur)) cur = strictParent.get(cur); return cur } - const tipOf = (id) => { let cur = id; while (strictChild.has(cur)) cur = strictChild.get(cur); return cur } + const rootMemo = new Map() + const rootOf = (id) => { + if (rootMemo.has(id)) return rootMemo.get(id) + let cur = id, g = 0 + while (parent.has(cur) && nodes.has(parent.get(cur)) && g++ < 100000) cur = parent.get(cur) + rootMemo.set(id, cur) + return cur + } + const ownerTip = new Map() + for (const [k, n] of nodes) { + const r = rootOf(k) + const rn = nodes.get(r) + if (!rn || n.author !== rn.author) continue + const prev = ownerTip.has(r) ? nodes.get(ownerTip.get(r)) : null + if (!prev || n.seq > prev.seq || (n.seq === prev.seq && n.ts >= prev.ts)) ownerTip.set(r, k) + } + const tipOf = (id) => { const r = rootOf(id); return ownerTip.get(r) || r } const roots = new Set() for (const id of nodes.keys()) roots.add(rootOf(id)) @@ -146,25 +189,72 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { for (const r of roots) tipByRoot.set(r, tipOf(r)) const memberByRoot = new Map() + const invitesOf = (root) => { + const out = [] + for (const id of [root, tipOf(root)]) { const n = nodes.get(id); if (n && Array.isArray(n.c.invites)) out.push(...n.c.invites) } + return out + } + const sealedRoot = (root) => { const oc = (nodes.get(root) || {}).c || {}; return !!(oc.encryptedPayload || oc.encrypted === true) } + const selfJoinAllowed = (root, mm) => { + if (!sealedRoot(root)) return true + const invites = invitesOf(root) + if (invites.some(inv => inv && typeof inv === "object" && inv.public === true)) return true + if (mm.keyProof && lookupKeys(root).some(k => keyProofFor(k, mm.member) === mm.keyProof)) return true + const tribeChat = !!((nodes.get(root) || {}).c || {}).tribeId + return !!mm.code && invites.some(inv => tribeChat ? inviteMatches(inv, mm.code) : (typeof inv === "string" ? inv === mm.code : !!(inv && typeof inv === "object" && typeof inv.code === "string" && inv.code === mm.code))) + } const consumedByRoot = new Map() + const codeClaimsByRoot = new Map() for (const mm of memberMsgs) { if (!nodes.has(mm.target)) continue const r = rootOf(mm.target) - if (!memberByRoot.has(r)) memberByRoot.set(r, new Map()) - const perMember = memberByRoot.get(r) - const prev = perMember.get(mm.member) - if (!prev || mm.ts >= prev.ts) perMember.set(mm.member, { on: mm.on, ts: mm.ts }) + const ownerAuthor = nodes.get(r) && nodes.get(r).author + const self = mm.member === mm.author + if (!self && mm.author !== ownerAuthor) continue + if (mm.on && mm.code) { + if (!codeClaimsByRoot.has(r)) codeClaimsByRoot.set(r, []) + codeClaimsByRoot.get(r).push({ code: mm.code, member: mm.member, keyProof: mm.keyProof, byOwner: mm.author === ownerAuthor }) + } + if (self && mm.author !== ownerAuthor && mm.on && !selfJoinAllowed(r, mm)) continue if (mm.on && mm.code) { if (!consumedByRoot.has(r)) consumedByRoot.set(r, new Set()) consumedByRoot.get(r).add(mm.code) } + if (!memberByRoot.has(r)) memberByRoot.set(r, new Map()) + const perMember = memberByRoot.get(r) + const prev = perMember.get(mm.member) + if (!prev || mm.ts >= prev.ts) perMember.set(mm.member, { on: mm.on, ts: mm.ts }) } const isCodeConsumed = (rootId, code) => !!(code && consumedByRoot.has(rootId) && consumedByRoot.get(rootId).has(code)) + const codeClaims = (rootId, codes) => (codeClaimsByRoot.get(rootId) || []).filter(c => codes.includes(c.code)) const rawRootOf = (id) => { let cur = id, g = 0; while (parent.has(cur) && g++ < 100000) cur = parent.get(cur); return cur } - return { tomb, nodes, parent: strictParent, child: strictChild, rawParent: parent, rawChild: child, rawRootOf, rootOf, tipOf, tipByRoot, msgNodes, memberByRoot, consumedByRoot, isCodeConsumed } + for (const [f, fNode] of nodes) { + const r = rootOf(f) + if (!nodes.has(r) || fNode.author === nodes.get(r).author) continue + const opened = decryptChatContent(fNode.c, r) + if (opened.undecryptable || !Array.isArray(opened.c && opened.c.members)) continue + if (!memberByRoot.has(r)) memberByRoot.set(r, new Map()) + const perMember = memberByRoot.get(r) + const prev = perMember.get(fNode.author) + if (!prev || fNode.ts >= prev.ts) perMember.set(fNode.author, { on: opened.c.members.includes(fNode.author), ts: fNode.ts }) + } + + const reactionByKey = new Map() + for (const r of reactMsgs) { + const rk = `${r.target}|${r.author}|${r.emoji}` + const prev = reactionByKey.get(rk) + if (!prev || r.ts >= prev.ts) reactionByKey.set(rk, r) + } + const pinByTarget = new Map() + for (const pm of pinMsgs) { + const prev = pinByTarget.get(pm.target) + if (!prev || pm.ts >= prev.ts) pinByTarget.set(pm.target, pm) + } + + return { tomb, nodes, rawRootOf, rootOf, tipOf, tipByRoot, msgNodes, memberByRoot, consumedByRoot, isCodeConsumed, codeClaims, reactionByKey, pinByTarget } } const resolveKeyChainSets = (chatRootId) => { @@ -236,13 +326,13 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { const { c, undecryptable } = decryptChatContent(rawC, rootId) let ownerContent = c - let ownerAuthor = c.author || node.author + let ownerAuthor = node.author if (idx) { const ownerNode = idx.nodes.get(rootId) if (ownerNode) { const decOwner = decryptChatContent(ownerNode.c || {}, rootId) ownerContent = decOwner.c - ownerAuthor = decOwner.c.author || ownerNode.author + ownerAuthor = ownerNode.author } } const memberToggles = idx && idx.memberByRoot ? idx.memberByRoot.get(rootId) : null @@ -263,7 +353,7 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { tags: safeArr(c.tags), members, invites, - author: c.author || node.author, + author: node.author, createdAt: c.createdAt || new Date(node.ts).toISOString(), updatedAt: c.updatedAt || null, encrypted: !!c.encrypted, @@ -277,11 +367,15 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { if (c.type !== "chatMessage") return null let text = c.text || "" + let image = c.image || null + let mimeType = typeof c.mimeType === "string" ? c.mimeType : null if (tribeCrypto && c.encryptedText) { const candidateKeys = [...tribeKeys, ...lookupKeys(chatRootId)] for (const keyHex of candidateKeys) { try { text = tribeCrypto.decryptWithKey(c.encryptedText, keyHex) + if (c.encryptedImage) image = tribeCrypto.decryptWithKey(c.encryptedImage, keyHex) + if (c.encryptedMime) mimeType = tribeCrypto.decryptWithKey(c.encryptedMime, keyHex) break } catch (_) {} } @@ -291,8 +385,10 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { key: node.key, chatId: c.chatId || "", text, - image: c.image || null, - author: c.author || node.author, + image, + mimeType, + replyTo: typeof c.replyTo === "string" ? c.replyTo : null, + author: node.author, createdAt: c.createdAt || new Date(node.ts).toISOString() } } @@ -304,28 +400,27 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { }) const ensureMemberKeys = async (ssbClient, messages, chats) => { - if (!tribeCrypto) return - const distributed = new Map() + if (!tribeCrypto || !ownCrypto) return + const leavesByRoot = new Map() for (const m of messages) { const c = m.value && m.value.content - if (!c || c.type !== "tribe-keys" || !c.tribeId) continue - const mk = c.memberKeys - if (!mk || typeof mk !== "object") continue - if (!distributed.has(c.tribeId)) distributed.set(c.tribeId, new Set()) - for (const id of Object.keys(mk)) distributed.get(c.tribeId).add(id) + if (!c || c.type !== "chatMember" || c.on !== false || !c.target || !c.member) continue + if (!leavesByRoot.has(c.target)) leavesByRoot.set(c.target, []) + leavesByRoot.get(c.target).push({ member: c.member, author: m.value.author, ts: Number(m.timestamp || m.value.timestamp || 0) }) } const ssbKeys = require("../server/node_modules/ssb-keys") for (const chat of safeArr(chats)) { - if (!chat || chat.undecryptable) continue + if (!chat || chat.undecryptable || chat.author !== ssbClient.id) continue const rootId = chat.rootId - if (!rootId) continue + if (!rootId || !lookupKey(rootId)) continue + const members = safeArr(chat.members) + const leaves = (leavesByRoot.get(rootId) || []).filter(l => l.author === l.member || l.author === chat.author) + const plan = ownCrypto.keyPlan({ rootId, ownerId: chat.author, gen: lookupGen(rootId), members, messages, leaves }) + if (plan.rotate) { await rotateChatKey(rootId, members); continue } + if (!plan.missing.length) continue const key = lookupKey(rootId) - if (!key) continue - const have = distributed.get(rootId) || new Set() - const missing = safeArr(chat.members).filter(m => m && m !== ssbClient.id && !have.has(m)) - if (!missing.length) continue const memberKeys = {} - for (const m of missing) { + for (const m of plan.missing) { try { memberKeys[m] = tribeCrypto.boxKeyForMember(key, m, ssbKeys) } catch (_) {} } if (!Object.keys(memberKeys).length) continue @@ -339,9 +434,22 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { new Promise((resolve, reject) => { const content = { type: "chatMember", target: rootId, member, on: !!on, createdAt: new Date().toISOString() } if (typeof code === "string" && code) content.code = code + const proofKey = on && member === ssbClient.id ? lookupKey(rootId) : null + if (proofKey) content.keyProof = keyProofFor(proofKey, member) ssbClient.publish(content, (e, res) => e ? reject(e) : resolve(res)) }) + const decryptTribeChats = async (idx) => { + if (!tribeCrypto || !tribesModel || typeof tribeCrypto.decryptFromTribe !== "function") return idx + for (const [k, n] of idx.nodes) { + const c = n.c || {} + if (!c.encryptedPayload || !c.tribeId) continue + const dec = await tribeCrypto.decryptFromTribe(c, tribesModel).catch(() => null) + if (dec && !dec._undecryptable) idx.nodes.set(k, { ...n, c: dec }) + } + return idx + } + const collectChats = (idx) => { const out = [] for (const [rootId, tipId] of idx.tipByRoot.entries()) { @@ -357,15 +465,21 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { return { type: "chat", + async encryptionKeyFor(chatRootId, tribeId = null) { + if (!tribeCrypto) return null + if (tribeId) { + const k = await getTribeFirstKeyFor(tribeId) + if (k) return k + } + return lookupKey(chatRootId) || null + }, + async resolveRootId(id) { const ssbClient = await openSsb() const messages = await readAll(ssbClient) const idx = buildIndex(messages) - let tip = id - while (idx.child.has(tip)) tip = idx.child.get(tip) - if (idx.tomb.has(tip)) throw new Error("Not found") - let root = tip - while (idx.parent.has(root)) root = idx.parent.get(root) + const root = idx.rootOf(id) + if (idx.tomb.has(idx.tipOf(root))) throw new Error("Not found") return root }, @@ -373,13 +487,12 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { const ssbClient = await openSsb() const messages = await readAll(ssbClient) const idx = buildIndex(messages) - let tip = id - while (idx.child.has(tip)) tip = idx.child.get(tip) + const tip = idx.tipOf(idx.rootOf(id)) if (idx.tomb.has(tip)) throw new Error("Not found") return tip }, - async createChat(title, description, image, category, status, tagsRaw, tribeId) { + async createChat(title, description, image, category, status, tagsRaw, tribeId, extra = {}) { const ssbClient = await openSsb() const userId = ssbClient.id const blobId = image ? String(image).trim() || null : null @@ -400,7 +513,8 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { author: userId, createdAt: now, updatedAt: now, - ...(tribeId ? { tribeId } : {}) + ...(tribeId ? { tribeId } : {}), + ...(extra && typeof extra === "object" && typeof extra.courseId === "string" && extra.courseId ? { courseId: extra.courseId } : {}) } if (!tribeCrypto) { @@ -456,13 +570,11 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { ssbClient.get(tipId, (err, item) => err || !item?.content ? reject(new Error("Chat not found")) : resolve(item)) }) const c = item.content - const rawAuthor = c.author || (c.encryptedPayload ? null : undefined) - if (rawAuthor && rawAuthor !== userId) throw new Error("Not the author") + if (item.author !== userId) throw new Error("Not the author") const messages = await readAll(ssbClient) const idx = buildIndex(messages) - let rootId = tipId - while (idx.parent.has(rootId)) rootId = idx.parent.get(rootId) + const rootId = idx.rootOf(tipId) const node = { key: tipId, c, author: item.author, ts: item.timestamp || 0 } const chat = buildChat(node, rootId, idx) if (!chat) throw new Error("Invalid chat") @@ -522,7 +634,7 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { return new Promise((resolve, reject) => { ssbClient.get(tipId, (err, item) => { if (err || !item?.content) return reject(new Error("Chat not found")) - if (item.content.author && item.content.author !== userId) return reject(new Error("Not the author")) + if (item.author !== userId) return reject(new Error("Not the author")) ssbClient.publish({ type: "tombstone", target: tipId, deletedAt: new Date().toISOString(), author: userId }, (e) => e ? reject(e) : resolve()) }) }) @@ -533,11 +645,9 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { const userId = ssbClient.id const messages = await readAll(ssbClient) const idx = buildIndex(messages) - let tip = id - while (idx.child.has(tip)) tip = idx.child.get(tip) + const root = idx.rootOf(id) + const tip = idx.tipOf(root) if (idx.tomb.has(tip)) throw new Error("Not found") - let root = tip - while (idx.parent.has(root)) root = idx.parent.get(root) const node = idx.nodes.get(tip) if (!node) throw new Error("Not found") @@ -570,18 +680,15 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { async getChatById(id) { const ssbClient = await openSsb() const messages = await readAll(ssbClient) - const idx = buildIndex(messages) + const idx = await decryptTribeChats(memoIndex('chats', messages, buildIndex)) - let tip = id - while (idx.child.has(tip)) tip = idx.child.get(tip) + const root = idx.rootOf(id) + const tip = idx.tipOf(root) if (idx.tomb.has(tip)) return null const node = idx.nodes.get(tip) if (!node || node.c.type !== "chat") return null - let root = tip - while (idx.parent.has(root)) root = idx.parent.get(root) - const chat = buildChat(node, root, idx) if (!chat) return null return chatCollab.fold(chat, collectChats(idx)) @@ -591,13 +698,25 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { const ssbClient = await openSsb() const uid = viewerId || ssbClient.id const messages = await readAll(ssbClient) - const idx = buildIndex(messages) + const idx = await decryptTribeChats(buildIndex(messages)) const now = Date.now() let list = chatCollab.visibleThenCollapsed(collectChats(idx), uid) + const lastMsgAt = new Map() + const lastMineAt = new Map() + for (const node of idx.msgNodes.values()) { + const cid = node.c && node.c.chatId + if (!cid) continue + const r = idx.rawRootOf(cid) || cid + const t = node.ts || 0 + if (t > (lastMsgAt.get(r) || 0)) lastMsgAt.set(r, t) + if (node.author === uid && t > (lastMineAt.get(r) || 0)) lastMineAt.set(r, t) + } + list = list.map(c => ({ ...c, lastMsgAt: lastMsgAt.get(c.rootId) || 0, lastMineAt: lastMineAt.get(c.rootId) || 0 })) + if (filter === "mine") list = list.filter(c => c.author === uid) - else if (filter === "recent") list = list.filter(c => new Date(c.createdAt).getTime() >= now - 86400000) + else if (filter === "recent") list = list.filter(c => Math.max(new Date(c.createdAt).getTime() || 0, Number(c.lastMsgAt || 0)) >= now - 86400000) else if (filter === "open") list = list.filter(c => c.status === "OPEN" || c.status === "INVITE-ONLY") else if (filter === "closed") list = list.filter(c => c.status === "CLOSED") @@ -631,13 +750,16 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { if (tribeCrypto) { const inviteSalt = tribeCrypto.generateInviteSalt() const ekChain = tribeCrypto.encryptChainForInvite([chat.rootId], code, inviteSalt) + const ident = opts.public ? { code, ...pubFlag } : { ch: hashCode(code, inviteSalt) } if (ekChain) { - invite = { code, ekChain, salt: inviteSalt, gen: lookupGen(chat.rootId), ...pubFlag } + invite = { ...ident, ekChain, salt: inviteSalt, gen: lookupGen(chat.rootId) } } else { const chatKey = lookupKey(chat.rootId) if (chatKey) { const ek = tribeCrypto.encryptForInvite(chatKey, code, inviteSalt) - invite = { code, ek, salt: inviteSalt, gen: lookupGen(chat.rootId), ...pubFlag } + invite = { ...ident, ek, salt: inviteSalt, gen: lookupGen(chat.rootId) } + } else { + invite = { ...ident, salt: inviteSalt } } } } @@ -668,6 +790,7 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { if (chat.author !== userId) throw new Error("Only the author can remove invites") const invites = (Array.isArray(chat.invites) ? chat.invites : []).filter(inv => !(typeof inv === "object" && inv.public === true)) await this.updateChatById(chatId, { invites, members: chat.members, status: chat.status, title: chat.title, description: chat.description, image: chat.image, category: chat.category, tags: chat.tags }) + if (!chat.tribeId) await rotateChatKey(chat.rootId, chat.members) }, async joinByInvite(code) { @@ -687,12 +810,7 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { if (!chat || !chat.invites.length) continue for (const inv of chat.invites) { - if (typeof inv === "string" && inv === code) { - matchedChat = chat; matchedInvite = inv; break - } - if (typeof inv === "object" && inv.code === code) { - matchedChat = chat; matchedInvite = inv; break - } + if (inviteMatches(inv, code)) { matchedChat = chat; matchedInvite = inv; break } } if (matchedChat) break } @@ -701,29 +819,40 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { if (matchedChat.members.includes(userId)) throw new Error("Already a participant") const isPublic = typeof matchedInvite === "object" && matchedInvite.public === true - if (!isPublic && idx.isCodeConsumed(matchedChat.rootId, code)) throw new Error("Invite already used") + const token = inviteToken(matchedInvite) || code let chatKey = null + let chain = null if (tribeCrypto && typeof matchedInvite === "object") { if (matchedInvite.ekChain) { - const chain = tribeCrypto.decryptChainFromInvite(matchedInvite.ekChain, code, matchedInvite.salt) - if (Array.isArray(chain) && chain.length) { - for (const entry of chain) { - if (Array.isArray(entry.keys) && entry.keys.length) { - tribeCrypto.setKeys(entry.rootId, entry.keys, entry.gen || entry.keys.length) - } else if (entry.key) { - tribeCrypto.setKey(entry.rootId, entry.key, entry.gen || 1) - } - } - chatKey = chain[0].key - } + chain = tribeCrypto.decryptChainFromInvite(matchedInvite.ekChain, code, matchedInvite.salt, 3) + if (Array.isArray(chain) && chain.length) chatKey = chain[0].key + else chain = null } else if (matchedInvite.ek) { chatKey = tribeCrypto.decryptFromInvite(matchedInvite.ek, code, matchedInvite.salt) - ownCrypto.setKey(matchedChat.rootId, chatKey, matchedInvite.gen || 1) } } - await publishMemberToggle(ssbClient, matchedChat.rootId, userId, true, isPublic ? undefined : code) + if (!isPublic) { + if (idx.isCodeConsumed(matchedChat.rootId, token) || idx.isCodeConsumed(matchedChat.rootId, code)) throw new Error("Invite already used") + const claims = idx.codeClaims(matchedChat.rootId, [token, code]).filter(c => c.member !== userId) + const spent = chatKey ? claims.some(c => c.byOwner || keyProofFor(chatKey, c.member) === c.keyProof) : claims.length > 0 + if (spent) throw new Error("Invite already used") + } + + if (chain) { + for (const entry of chain) { + if (Array.isArray(entry.keys) && entry.keys.length) { + tribeCrypto.setKeys(entry.rootId, entry.keys, entry.gen || entry.keys.length) + } else if (entry.key) { + tribeCrypto.setKey(entry.rootId, entry.key, entry.gen || 1) + } + } + } else if (chatKey) { + ownCrypto.setKey(matchedChat.rootId, chatKey, matchedInvite.gen || 1) + } + + await publishMemberToggle(ssbClient, matchedChat.rootId, userId, true, isPublic ? undefined : token) if (tribeCrypto && chatKey) { try { @@ -767,9 +896,7 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { const chat = await this.getChatById(chatId) if (!chat) throw new Error("Chat not found") if (chat.author === userId) throw new Error("Author cannot leave their own chat") - const members = chat.members.filter(m => m !== userId) await publishMemberToggle(ssbClient, chat.rootId, userId, false) - try { await rotateChatKey(chat.rootId, members) } catch (_) {} }, async ingestKeys() { await ingestOwnTribeKeys() }, @@ -779,17 +906,11 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { try { const ssbClient = await openSsb() const messages = await readAll(ssbClient) - const live = new Set() - for (const m of messages) { - const c = m.value && m.value.content - if (!c) continue - if (c.type === "chat") live.add(m.key) - } const tomb = buildValidatedTombstoneSet(messages) const all = ownCrypto.getAllRootIds() let removed = 0 for (const rid of all) { - if (!live.has(rid) || tomb.has(rid)) { + if (tomb.has(rid)) { try { ownCrypto.dropKey(rid); removed += 1 } catch (_) {} } } @@ -797,7 +918,7 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { } catch (_) { return 0 } }, - async sendMessage(chatId, text, image = null) { + async sendMessage(chatId, text, image = null, replyTo = null, mimeType = null) { const ssbClient = await openSsb() const userId = ssbClient.id const chat = await this.getChatById(chatId) @@ -814,7 +935,12 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { const c = m.value?.content return c?.type === "chatMessage" && c?.chatId === chat.rootId && m.value?.author === userId && (m.value?.timestamp || 0) >= oneHourAgo }).length - if (recentCount >= 3) throw new Error("Rate limit: max 3 messages per hour") + if (recentCount >= MESSAGES_PER_HOUR) { + const err = new Error(`Rate limit: max ${MESSAGES_PER_HOUR} messages per hour`) + err.code = "CHAT_RATE_LIMIT" + err.retryAfterMinutes = 60 + throw err + } const now = new Date().toISOString() let content = { @@ -824,6 +950,14 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { createdAt: now } if (image) content.image = image + if (image && mimeType) content.mimeType = String(mimeType) + if (replyTo) { + const idxAll = buildIndex(messages) + const target = idxAll.msgNodes.get(replyTo) + if (target && (target.c.chatId === chat.rootId || idxAll.rawRootOf(target.c.chatId) === idxAll.rawRootOf(chat.rootId))) { + content.replyTo = replyTo + } + } const chatIsEncrypted = !!(chat.tribeId) || !!lookupKey(chat.rootId) if (chatIsEncrypted && tribeCrypto) { @@ -832,6 +966,14 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { if (!encKey) encKey = lookupKey(chat.rootId) if (!encKey) throw new Error(`Missing chat key for ${chat.rootId} — cannot send message`) content.encryptedText = tribeCrypto.encryptWithKey(safeText(text), encKey) + if (content.image) { + content.encryptedImage = tribeCrypto.encryptWithKey(String(content.image), encKey) + delete content.image + if (content.mimeType) { + content.encryptedMime = tribeCrypto.encryptWithKey(String(content.mimeType), encKey) + delete content.mimeType + } + } if (chat.tribeId) content.tribeId = chat.tribeId } else { content.text = safeText(text) @@ -862,10 +1004,74 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { if (msg) result.push(msg) } + const viewerId = ssbClient.id + let chatAuthor = null + if (chatNode) chatAuthor = chatNode.author + const reactsByTarget = new Map() + for (const r of idx.reactionByKey.values()) { + if (!r.on) continue + if (!reactsByTarget.has(r.target)) reactsByTarget.set(r.target, []) + reactsByTarget.get(r.target).push(r) + } + const byKey = new Map(result.map(m => [m.key, m])) + for (const m of result) { + const counts = {} + const mine = {} + for (const e of REACTION_EMOJIS) { counts[e] = 0; mine[e] = false } + for (const r of reactsByTarget.get(m.key) || []) { + counts[r.emoji] += 1 + if (r.author === viewerId) mine[r.emoji] = true + } + m.reactions = { counts, mine } + const pin = idx.pinByTarget.get(m.key) + m.pinned = !!(pin && pin.on && chatAuthor && pin.author === chatAuthor) + const quoted = m.replyTo ? byKey.get(m.replyTo) : null + m.reply = quoted ? { author: quoted.author, text: String(quoted.text || "").slice(0, 120) } : null + } + result.sort((a, b) => new Date(a.createdAt) - new Date(b.createdAt)) return result }, + async toggleReaction(chatId, targetKey, emoji) { + if (!REACTION_EMOJIS.includes(emoji)) throw new Error("Invalid reaction") + const ssbClient = await openSsb() + const userId = ssbClient.id + const chat = await this.getChatById(chatId) + if (!chat) throw new Error("Chat not found") + if (chat.status === "CLOSED") throw new Error("Chat is closed") + if (!chat.members.includes(userId)) { + if (chat.status === "OPEN") await this.joinChat(chatId) + else throw new Error("Not a participant") + } + const messages = await readAll(ssbClient) + const idx = buildIndex(messages) + const target = idx.msgNodes.get(targetKey) + if (!target || (target.c.chatId !== chat.rootId && idx.rawRootOf(target.c.chatId) !== idx.rawRootOf(chat.rootId))) throw new Error("Message not found") + const prev = idx.reactionByKey.get(`${targetKey}|${userId}|${emoji}`) + const on = !(prev && prev.on) + return new Promise((resolve, reject) => { + ssbClient.publish({ type: "chatReaction", chatId: chat.rootId, target: targetKey, emoji, on, author: userId, createdAt: new Date().toISOString() }, (e, res) => e ? reject(e) : resolve(res)) + }) + }, + + async togglePin(chatId, targetKey) { + const ssbClient = await openSsb() + const userId = ssbClient.id + const chat = await this.getChatById(chatId) + if (!chat) throw new Error("Chat not found") + if (chat.author !== userId) throw new Error("Only the chat author can pin") + const messages = await readAll(ssbClient) + const idx = buildIndex(messages) + const target = idx.msgNodes.get(targetKey) + if (!target || (target.c.chatId !== chat.rootId && idx.rawRootOf(target.c.chatId) !== idx.rawRootOf(chat.rootId))) throw new Error("Message not found") + const prev = idx.pinByTarget.get(targetKey) + const on = !(prev && prev.on && prev.author === userId) + return new Promise((resolve, reject) => { + ssbClient.publish({ type: "chatPin", chatId: chat.rootId, target: targetKey, on, author: userId, createdAt: new Date().toISOString() }, (e, res) => e ? reject(e) : resolve(res)) + }) + }, + async getParticipants(chatRootId) { const chat = await this.getChatById(chatRootId) if (!chat) return [] diff --git a/src/models/content_visibility.js b/src/models/content_visibility.js new file mode 100644 index 0000000..6afd0b7 --- /dev/null +++ b/src/models/content_visibility.js @@ -0,0 +1,30 @@ +const up = (v) => String(v == null ? '' : v).toUpperCase(); +const arr = (v) => (Array.isArray(v) ? v : []); +const isPrivateFlag = (v) => { + if (v === false) return true; + const s = String(v == null ? '' : v).toLowerCase(); + return s === 'private' || s === 'false' || s === '0'; +}; + +const isContentVisibleTo = (type, c, viewer, author = null) => { + if (!c || typeof c !== 'object') return false; + if (c.encrypted === true || c.encryptedPayload || c.encryptedQuestion || c.encryptedText) return false; + const owner = String(author || c.author || '') === String(viewer); + switch (type) { + case 'task': return !isPrivateFlag(c.isPublic) || owner || arr(c.assignees).includes(viewer); + case 'event': return !isPrivateFlag(c.isPublic) || owner || arr(c.attendees).includes(viewer); + case 'forum': return !(c.isPrivate === true || c.isPrivate === 'true' || c.isPrivate === 'on') || owner; + case 'job': return up(c.visibility) !== 'HIDDEN' || owner || arr(c.subscribers).includes(viewer); + case 'housing': + case 'market': return up(c.visibility) !== 'HIDDEN' || owner; + case 'shop': return up(c.visibility) !== 'CLOSED' || owner; + case 'schoolCourse': return up(c.visibility) !== 'INVITE' || owner || arr(c.students).includes(viewer) || arr(c.invited).includes(viewer); + case 'tribe': return c.isAnonymous === false || owner || arr(c.members).includes(viewer); + case 'poll': return !c.chatId && !c.tribeId; + case 'room': return !c.tribeId && c.encrypted !== true; + case 'curriculum': return up(c.visibility) !== 'HIDDEN' || owner; + default: return !c.tribeId; + } +}; + +module.exports = { isContentVisibleTo }; diff --git a/src/models/courts_model.js b/src/models/courts_model.js index 13b8fde..91f946c 100644 --- a/src/models/courts_model.js +++ b/src/models/courts_model.js @@ -2,6 +2,9 @@ const pull = require('../server/node_modules/pull-stream'); const moment = require('../server/node_modules/moment'); const { getConfig } = require('../configs/config-manager.js'); const { buildValidatedTombstoneSet } = require('./tombstone_validator'); +const { readTyped } = require('./typed_log'); +const { buildVoteResults } = require('../backend/vote_tally'); +const { versionGroups } = require('../backend/dedupe'); const logLimit = getConfig().ssbLogStream?.limit || 1000; const CASE_ANSWER_DAYS = 7; @@ -16,6 +19,13 @@ const EVIDENCE_FIELDS = ['text', 'link', 'imageUrl']; const ANSWER_FIELDS = ['stance', 'text']; const VERDICT_FIELDS = ['result', 'orders']; const SETTLEMENT_FIELDS = ['terms']; +const COURTS_TYPES = [ + 'courtsCase', 'courtsMediators', 'courtsJudge', 'courtsVoteLink', 'courtsVisibility', + 'courtsAnswer', 'courtsEvidence', 'courtsVerdict', 'courtsSettlementProposal', + 'courtsSettlementAccepted', 'courtsSupport', 'courtsNomination', 'courtsNominationVote', + 'courts-key', 'tombstone' +]; +const VOTE_LOG_TYPES = ['votes', 'votesVote', 'tombstone']; module.exports = ({ cooler, services = {}, tribeCrypto }) => { let ssb; @@ -73,12 +83,7 @@ module.exports = ({ cooler, services = {}, tribeCrypto }) => { async function readLog() { const ssbClient = await openSsb(); - return new Promise((resolve, reject) => { - pull( - ssbClient.createLogStream({ limit: logLimit }), - pull.collect((err, arr) => (err ? reject(err) : resolve(arr))) - ); - }); + return readTyped(ssbClient, COURTS_TYPES, { limit: logLimit }); } async function listByType(type) { @@ -86,13 +91,19 @@ module.exports = ({ cooler, services = {}, tribeCrypto }) => { const tomb = buildValidatedTombstoneSet(msgs); const rep = new Map(); const map = new Map(); + const authorOf = new Map(); + for (const m of msgs) { + const c = m.value?.content || m.content; + if (c && c.type === type) authorOf.set(m.key || m.id, m.value?.author); + } for (const m of msgs) { const k = m.key || m.id; const c = m.value?.content || m.content; if (!c) continue; if (c.type === type) { + if (c.replaces && authorOf.has(c.replaces) && authorOf.get(c.replaces) !== m.value?.author) continue; if (c.replaces) rep.set(c.replaces, k); - map.set(k, { id: k, ...c }); + map.set(k, { ...c, id: k }); } } for (const oldId of rep.keys()) map.delete(oldId); @@ -252,7 +263,7 @@ if (c.type === type) { myPublicPreference = c.publicPrefRespondent; } const rootId = c.rootCaseId || c.id; - const tally = c.voteId ? await voteTally(c.voteId) : null; + const tally = c.voteId ? await voteTally(c.voteId, [c.accuser, c.respondentId]) : null; rows.push({ ...c, respondent: c.respondentId || c.respondent, @@ -284,27 +295,24 @@ if (c.type === type) { for (const m of msgs) { const c = m.value?.content || m.content; if (!c || c.type !== 'courtsCase') continue; - nodes.set(m.key || m.id, { key: m.key || m.id, author: m.value?.author, c }); + nodes.set(m.key || m.id, { key: m.key || m.id, author: m.value?.author, seq: m.value?.sequence || 0, ts: m.value?.timestamp || m.timestamp || 0, c }); } - const strictNext = new Map(), strictPrev = new Map(); - for (const [k, n] of nodes) { - const t = n.c.replaces; - if (t) { const o = nodes.get(t); if (o && o.author === n.author) { strictNext.set(t, k); strictPrev.set(k, t); } } - } - return { nodes, strictNext, strictPrev }; + const vg = versionGroups([...nodes.values()].map(n => ({ key: n.key, author: n.author, seq: n.seq, ts: n.ts, replaces: typeof n.c.replaces === 'string' ? n.c.replaces : null, claimedAuthor: typeof n.c.accuser === 'string' ? n.c.accuser : null }))); + return { nodes, vg }; } function strictCaseBase(msgs, id, idxIn) { const idx = idxIn || collectCaseNodes(msgs); - const { nodes, strictNext, strictPrev } = idx; + const { nodes, vg } = idx; const tomb = buildValidatedTombstoneSet(msgs); let startKey = nodes.has(id) ? id : null; if (!startKey) { for (const [k, n] of nodes) { if (String(n.c.rootCaseId || '') === id) { startKey = k; break; } } } if (!startKey) return null; - let root = startKey, g = 0; while (strictPrev.has(root) && g++ < 100000) root = strictPrev.get(root); - let tip = root; g = 0; while (strictNext.has(tip) && g++ < 100000) tip = strictNext.get(tip); + const root = vg.rootOf(startKey); + if (vg.isForeignCopy(root)) return null; + const tip = vg.tipOf(root); if (tomb.has(tip) || tomb.has(root)) return null; const node = nodes.get(tip); if (!node) return null; @@ -329,6 +337,7 @@ if (c.type === type) { let medATs = 0, medRTs = 0, judgeId = '', judgeTs = 0, voteId = String(base.voteId || ''), voteTs = 0; let prefA = base.publicPrefAccuser, prefATs = 0, prefR = base.publicPrefRespondent, prefRTs = 0; let answered = false, settled = false, verdict = null, verdictTs = 0, verdictAt = null, settledAt = null; + const verdictClaims = []; for (const m of msgs) { const c = m.value?.content || m.content; if (!c) continue; @@ -351,20 +360,20 @@ if (c.type === type) { answered = true; } else if (c.type === 'courtsSettlementAccepted' && matchC(c) && isParty(author)) { settled = true; settledAt = c.createdAt || new Date(ts).toISOString(); - } else if (c.type === 'courtsVerdict' && matchC(c) && ts >= verdictTs) { - verdict = author; verdictTs = ts; verdictAt = c.createdAt || new Date(ts).toISOString(); + } else if (c.type === 'courtsVerdict' && matchC(c)) { + verdictClaims.push({ author, ts, at: c.createdAt || new Date(ts).toISOString() }); } } - let verdictValid = false; - if (verdict) { - if (method === 'JUDGE') verdictValid = (verdict === judgeId); - else if (method === 'MEDIATION') verdictValid = (medA.includes(verdict) || medR.includes(verdict)); - else if (method === 'DICTATOR') verdictValid = (verdict === dictatorId); + const legit = await verdictAuthors({ method, judgeId, medA, medR, dictatorId, openedAt: base.openedAt || base.createdAt, accuser, respondentId }); + for (const v of verdictClaims) { + if (!legit.has(v.author) || v.ts < verdictTs) continue; + verdict = v.author; verdictTs = v.ts; verdictAt = v.at; } + const verdictValid = !!verdict; let voteDecided = false; if ((method === 'POPULAR' || method === 'KARMATOCRACY') && voteId) { - try { const t = await voteTally(voteId); if (t && t.closed && t.total > 0) voteDecided = true; } catch (_) {} + try { const t = await voteTally(voteId, [accuser, respondentId]); if (t && t.closed && t.total > 0 && (t.yes >= t.needed || t.no >= t.needed)) voteDecided = true; } catch (_) {} } base.mediatorsAccuser = medA; @@ -380,6 +389,33 @@ if (c.type === type) { return base; } + async function isRulingDictator(feedId, at) { + if (!feedId || !services.parliament || !services.parliament.listTerms) return false; + const atMs = new Date(at).getTime(); + if (!Number.isFinite(atMs)) return false; + try { + const terms = await services.parliament.listTerms('all'); + return (terms || []).some(t => + String(t.method || '').toUpperCase() === 'DICTATORSHIP' && + t.powerType === 'inhabitant' && + String(t.powerId || '') === String(feedId) && + new Date(t.startAt).getTime() <= atMs && + atMs < new Date(t.endAt).getTime() + ); + } catch (_) { return false; } + } + + async function verdictAuthors({ method, judgeId, medA, medR, dictatorId, openedAt, accuser, respondentId }) { + const out = new Set(); + if (method === 'JUDGE' && judgeId) out.add(String(judgeId)); + else if (method === 'MEDIATION') for (const x of [...ensureArray(medA), ...ensureArray(medR)]) out.add(String(x)); + else if (method === 'DICTATOR' && dictatorId && await isRulingDictator(dictatorId, openedAt)) out.add(String(dictatorId)); + out.delete(String(accuser || '')); + out.delete(String(respondentId || '')); + out.delete(''); + return out; + } + async function getCaseById(caseId) { const id = String(caseId || '').trim(); if (!id) return null; @@ -649,17 +685,22 @@ if (c.type === type) { return map; } - async function voteTally(voteId) { - if (!voteId || !services.votes || !services.votes.getVoteById) return null; + async function voteTally(voteId, parties) { + if (!voteId) return null; try { - const v = await services.votes.getVoteById(voteId); - const vm = v && v.votes ? v.votes : {}; + const ssbClient = await openSsb(); + const msgs = await readTyped(ssbClient, VOTE_LOG_TYPES, { limit: logLimit }); + const r = buildVoteResults(msgs).get(voteId); + if (!r) return null; + const tomb = buildValidatedTombstoneSet(msgs); + if (tomb.has(r.rootId) || tomb.has(voteId)) return null; + if (Array.isArray(parties) && !parties.map(x => String(x || '')).includes(String(r.creator || ''))) return null; + const vm = r.votes || {}; const yes = Number(vm.YES ?? vm.Yes ?? vm.yes ?? 0); const no = Number(vm.NO ?? vm.No ?? vm.no ?? 0); - const sum = Object.values(vm).reduce((s, n) => s + Number(n || 0), 0); - const total = Number(v.totalVotes ?? v.total ?? sum); - const deadline = v.deadline || v.endAt || v.expiresAt || null; - const closed = v.status === 'CLOSED' || (deadline && moment(deadline).isBefore(moment())); + const total = Number(r.totalVotes || 0); + const deadline = r.deadline || null; + const closed = !!(deadline && moment(deadline).isBefore(moment())); const needed = Math.floor(total / 2) + 1; return { yes, no, total, needed, closed, deadline }; } catch { return null; } @@ -868,8 +909,24 @@ if (c.type === type) { .filter(matchCase) .map(tryDecrypt) .sort((a, b) => new Date(a.createdAt || 0) - new Date(b.createdAt || 0)); + const legit = await verdictAuthors({ + method: String(base.method || '').toUpperCase(), + judgeId, + medA: ma, + medR: mr, + dictatorId, + openedAt: base.openedAt || base.createdAt, + accuser: accuserId, + respondentId + }); + const legitVerdictKeys = new Set(); + for (const m of await readLog()) { + const c = m.value?.content; + if (c && c.type === 'courtsVerdict' && legit.has(String(m.value?.author || ''))) legitVerdictKeys.add(m.key || m.id); + } const verdicts = verdictsAll .filter(matchCase) + .filter(v => legitVerdictKeys.has(v.id)) .map(tryDecrypt) .sort((a, b) => new Date(a.createdAt || 0) - new Date(b.createdAt || 0)); const verdict = verdicts.length ? verdicts[verdicts.length - 1] : null; @@ -884,7 +941,7 @@ if (c.type === type) { const hasVerdict = !!verdict; const supportsMap = await countSupportsByCase(); const supportCount = supportsMap.get(String(caseRootId)) || supportsMap.get(String(id)) || 0; - const tally = base.voteId ? await voteTally(base.voteId) : null; + const tally = base.voteId ? await voteTally(base.voteId, [accuserId, respondentId]) : null; return { ...base, id, @@ -919,12 +976,7 @@ if (c.type === type) { const ssbKeys = require('../server/node_modules/ssb-keys'); const ssbConfig = require('../server/ssb_config'); const ssbClient = await openSsb(); - const msgs = await new Promise((res, rej) => { - pull( - ssbClient.createLogStream({ limit: logLimit }), - pull.collect((err, arr) => (err ? rej(err) : res(arr))) - ); - }); + const msgs = await readLog(); for (const m of msgs) { const c = m.value?.content; if (!c || c.type !== 'courts-key') continue; diff --git a/src/models/crypto.js b/src/models/crypto.js index bdd9321..65b3dec 100644 --- a/src/models/crypto.js +++ b/src/models/crypto.js @@ -12,13 +12,17 @@ const SENSITIVE_FIELDS = [ const ENVELOPE_PRESERVE = new Set([ 'type', 'tribeId', 'contentType', 'replaces', 'target', 'author', 'createdAt', 'updatedAt', 'encryptedPayload', - 'mapId', 'calendarId', 'dateId', 'padId', 'roomId', 'parentId', + 'mapId', 'calendarId', 'dateId', 'padId', 'roomId', 'parentId', 'courseId', 'members', 'invites', 'participants', '_decrypted', '_undecryptable' ]); const INVITE_SALT_LEGACY = 'SolarNET.HuB'; -const INVITE_SCRYPT = { N: 131072, r: 8, p: 1, maxmem: 256 * 1024 * 1024 }; +const INVITE_SCRYPT = { N: 16384, r: 8, p: 1, maxmem: 64 * 1024 * 1024 }; +const INVITE_SCRYPT_LEGACY = { N: 131072, r: 8, p: 1, maxmem: 512 * 1024 * 1024 }; +const INVITE_CACHE_MAX = 512; +const UNSAFE_KEYS = new Set(['__proto__', 'constructor', 'prototype']); +const INVITE_FAIL_TTL_MS = 10 * 60 * 1000; const FP_INFO = Buffer.from('v1-fp', 'utf8'); const ENVELOPE_TYPE = 'tribe-msg'; @@ -27,7 +31,7 @@ const KEY_DISTRIB_TYPE = 'tribe-keys-distrib'; const KEY_DISTRIB_BATCH = 7; module.exports = (configPath, namespace = 'tribes') => { - const keysDir = path.join(configPath, 'keys'); + const keysDir = require('../configs/state-manager').keysDir(configPath); try { fs.mkdirSync(keysDir, { recursive: true, mode: 0o700 }); } catch (_) {} const keyringPath = path.join(keysDir, `${namespace}-keys.json`); @@ -62,6 +66,8 @@ module.exports = (configPath, namespace = 'tribes') => { }; const generateTribeKey = () => crypto.randomBytes(32).toString('hex'); + const HEX_KEY = /^[0-9a-f]{64}$/i; + const validKey = (k) => typeof k === 'string' && HEX_KEY.test(k); const getKey = (rid) => { const e = keyring[rid]; @@ -81,15 +87,17 @@ module.exports = (configPath, namespace = 'tribes') => { const getAllRootIds = () => Object.keys(keyring); const setKey = (rid, kHex, gen) => { + if (!validKey(kHex)) return; keyring[rid] = { keys: [kHex], gen: gen || 1 }; saveKeyring(); }; const setKeys = (rid, ks, topGen) => { - if (!Array.isArray(ks) || !ks.length) return; + if (!Array.isArray(ks)) return; const seen = new Set(); const dedup = []; - for (const k of ks) if (k && !seen.has(k)) { seen.add(k); dedup.push(k); } + for (const k of ks) if (validKey(k) && !seen.has(k)) { seen.add(k); dedup.push(k); } + if (!dedup.length) return; keyring[rid] = { keys: dedup, gen: topGen || dedup.length }; saveKeyring(); }; @@ -97,8 +105,9 @@ module.exports = (configPath, namespace = 'tribes') => { const mergeKeys = (rid, incoming, topGen) => { const e = keyring[rid] || { keys: [], gen: 0 }; const seen = new Set(e.keys); - const merged = [...e.keys]; - for (const k of incoming) if (k && !seen.has(k)) { seen.add(k); merged.push(k); } + const fresh = []; + for (const k of (Array.isArray(incoming) ? incoming : [])) if (validKey(k) && !seen.has(k)) { seen.add(k); fresh.push(k); } + const merged = [...fresh, ...e.keys]; keyring[rid] = { keys: merged, gen: Math.max(e.gen || 0, topGen || merged.length) }; saveKeyring(); return keyring[rid].gen; @@ -106,6 +115,7 @@ module.exports = (configPath, namespace = 'tribes') => { const addNewKey = (rid, kHex) => { const e = keyring[rid] || { keys: [], gen: 0 }; + if (!validKey(kHex)) return e.gen; if (e.keys.includes(kHex)) return e.gen; e.keys.unshift(kHex); e.gen = (e.gen || 0) + 1; @@ -114,6 +124,27 @@ module.exports = (configPath, namespace = 'tribes') => { return e.gen; }; + const keyPlan = ({ rootId, ownerId, gen, members, messages, leaves }) => { + const stamp = (m) => Number(m.timestamp || (m.value && m.value.timestamp) || 0); + const g = Number(gen) || 0; + let genStart = g > 1 ? null : 0; + const lastGot = new Map(); + for (const m of (Array.isArray(messages) ? messages : [])) { + const c = m && m.value && m.value.content; + if (!c || c.type !== 'tribe-keys' || c.tribeId !== rootId || m.value.author !== ownerId) continue; + if (!c.memberKeys || typeof c.memberKeys !== 'object') continue; + const t = stamp(m); + for (const id of Object.keys(c.memberKeys)) lastGot.set(id, Math.max(lastGot.get(id) || 0, t)); + if (g > 1 && Number(c.generation) === g) genStart = genStart === null ? t : Math.min(genStart, t); + } + if (genStart === null) genStart = 0; + const current = new Set((Array.isArray(members) ? members : []).filter(Boolean)); + const gone = (Array.isArray(leaves) ? leaves : []).filter((l) => l && l.member && l.member !== ownerId); + const rotate = g > 0 && gone.some((l) => !current.has(l.member) && l.ts > genStart); + const missing = rotate ? [] : [...current].filter((id) => id !== ownerId && (!lastGot.has(id) || gone.some((l) => l.member === id && l.ts > lastGot.get(id)))); + return { rotate, missing }; + }; + const dropKey = (rid) => { if (keyring[rid]) { delete keyring[rid]; @@ -121,8 +152,15 @@ module.exports = (configPath, namespace = 'tribes') => { } }; - const fingerprint = (kHex) => - crypto.createHmac('sha256', Buffer.from(kHex, 'hex')).update(FP_INFO).digest('hex').slice(0, 32); + const fingerprints = new Map(); + const fingerprint = (kHex) => { + let fp = fingerprints.get(kHex); + if (!fp) { + fp = crypto.createHmac('sha256', Buffer.from(kHex, 'hex')).update(FP_INFO).digest('hex').slice(0, 32); + fingerprints.set(kHex, fp); + } + return fp; + }; const buildFingerprintIndex = () => { const m = new Map(); @@ -200,9 +238,9 @@ module.exports = (configPath, namespace = 'tribes') => { const generateInviteSalt = () => crypto.randomBytes(16).toString('hex'); - const deriveInviteKey = (code, salt) => { + const deriveInviteKey = (code, salt, params = INVITE_SCRYPT) => { const s = (salt === undefined || salt === null || salt === '') ? INVITE_SALT_LEGACY : salt; - return crypto.scryptSync(code, s, 32, INVITE_SCRYPT); + return crypto.scryptSync(code, s, 32, params); }; const hashInviteCode = (code, salt) => { @@ -218,22 +256,76 @@ module.exports = (configPath, namespace = 'tribes') => { return encryptWithKey(tribeKeyHex, derived.toString('hex'), inviteAad(inviteCode, salt)); }; - const decryptFromInvite = (encryptedKey, inviteCode, salt) => { - const derived = deriveInviteKey(inviteCode, salt); - return decryptWithKey(encryptedKey, derived.toString('hex'), inviteAad(inviteCode, salt)); + const derivedCache = new Map(); + const failedCache = new Map(); + const remember = (map, k, v, max) => { + map.delete(k); + map.set(k, v); + while (map.size > max) map.delete(map.keys().next().value); }; + const derivedKeyName = (code, salt, params) => `${params.N}|${salt == null ? '' : salt}|${code}`; + const openWithDerived = (payload, code, salt, params, aad) => { + const name = derivedKeyName(code, salt, params); + const cached = derivedCache.get(name); + const derived = cached || deriveInviteKey(code, salt, params); + const out = decryptWithKey(payload, derived.toString('hex'), aad); + if (out && !cached) remember(derivedCache, name, derived, INVITE_CACHE_MAX); + return out; + }; + const failureName = (payload, code, salt) => crypto.createHash('sha256').update(`${String(payload)}|${code}|${salt == null ? '' : salt}`).digest('hex'); + const recentlyFailed = (name) => { + const at = failedCache.get(name); + return !!at && Date.now() - at < INVITE_FAIL_TTL_MS; + }; + + const decryptFromInvite = (encryptedKey, inviteCode, salt) => { + const failure = failureName(encryptedKey, inviteCode, salt); + if (recentlyFailed(failure)) throw new Error('Unsupported state or unable to authenticate data'); + const aad = inviteAad(inviteCode, salt); + try { + const out = openWithDerived(encryptedKey, inviteCode, salt, INVITE_SCRYPT, aad); + if (out) return out; + } catch (_) {} + for (let attempt = 0; attempt < 3; attempt++) { + try { + const out = openWithDerived(encryptedKey, inviteCode, salt, INVITE_SCRYPT_LEGACY, aad); + if (out) return out; + } catch (_) {} + } + remember(failedCache, failure, Date.now(), INVITE_CACHE_MAX); + throw new Error('Unsupported state or unable to authenticate data'); + }; + + const INVITE_ENCRYPT_ATTEMPTS = 3; const encryptChainForInvite = (ancestryRootIds, code, salt) => { const chain = ancestryRootIds.map(rid => ({ rootId: rid, keys: getKeys(rid), gen: getGen(rid) })); if (chain.some(e => !Array.isArray(e.keys) || !e.keys.length)) return null; - const k = deriveInviteKey(code, salt); - return encryptWithKey(JSON.stringify(chain), k.toString('hex'), inviteAad(code, salt)); + const plain = JSON.stringify(chain); + const aad = inviteAad(code, salt); + for (let attempt = 0; attempt < INVITE_ENCRYPT_ATTEMPTS; attempt++) { + const k = deriveInviteKey(code, salt); + const payload = encryptWithKey(plain, k.toString('hex'), aad); + if (!payload) continue; + const readBack = decryptChainFromInvite(payload, code, salt); + if (readBack && JSON.stringify(readBack.map(e => ({ rootId: e.rootId, keys: e.keys, gen: e.gen }))) + === JSON.stringify(chain.map(e => ({ rootId: e.rootId, keys: e.keys, gen: e.gen })))) { + return payload; + } + } + throw new Error('Could not produce a verifiable invite on this machine'); }; - const decryptChainFromInvite = (encryptedPayload, code, salt) => { - const k = deriveInviteKey(code, salt); + const decryptChainOnce = (encryptedPayload, code, salt) => { + let json = null + try { + json = openWithDerived(encryptedPayload, code, salt, INVITE_SCRYPT, inviteAad(code, salt)); + } catch (_) { + try { + json = openWithDerived(encryptedPayload, code, salt, INVITE_SCRYPT_LEGACY, inviteAad(code, salt)); + } catch (_) { return null; } + } try { - const json = decryptWithKey(encryptedPayload, k.toString('hex'), inviteAad(code, salt)); const parsed = JSON.parse(json); if (Array.isArray(parsed) && parsed.every(e => e && e.rootId && Array.isArray(e.keys) && e.keys.length)) { return parsed.map(e => ({ @@ -247,6 +339,18 @@ module.exports = (configPath, namespace = 'tribes') => { return null; }; + const decryptChainFromInvite = (encryptedPayload, code, salt, attempts = 1) => { + const failure = failureName(encryptedPayload, code, salt); + if (recentlyFailed(failure)) return null; + const tries = Math.max(1, Number(attempts) || 1); + for (let i = 0; i < tries; i++) { + const chain = decryptChainOnce(encryptedPayload, code, salt); + if (chain) return chain; + } + remember(failedCache, failure, Date.now(), INVITE_CACHE_MAX); + return null; + }; + const inviteMatchesCode = (inv, code) => { if (!inv || typeof inv !== 'object' || !inv.codeHash) return false; return inv.codeHash === hashInviteCode(code, inv.salt); @@ -343,7 +447,9 @@ module.exports = (configPath, namespace = 'tribes') => { const payload = JSON.parse(plaintext); const result = { ...content }; delete result.encryptedPayload; - Object.assign(result, payload); + if (payload && typeof payload === 'object' && !Array.isArray(payload)) { + for (const [k, v] of Object.entries(payload)) if (!UNSAFE_KEYS.has(k) && !(k in envelope)) result[k] = v; + } return result; } catch (_) {} } @@ -512,7 +618,7 @@ module.exports = (configPath, namespace = 'tribes') => { ENVELOPE_TYPE, ENVELOPE_VERSION, KEY_DISTRIB_TYPE, KEY_DISTRIB_BATCH, loadKeyring, saveKeyring, generateTribeKey, getKey, getKeys, getGen, getAllRootIds, - setKey, setKeys, mergeKeys, addNewKey, dropKey, + setKey, setKeys, mergeKeys, addNewKey, dropKey, keyPlan, fingerprint, buildFingerprintIndex, isTribeMsg, wrapMsg, unwrapMsg, encryptWithKey, decryptWithKey, diff --git a/src/models/cv_model.js b/src/models/cv_model.js index 8e66e54..173ac0b 100644 --- a/src/models/cv_model.js +++ b/src/models/cv_model.js @@ -1,8 +1,11 @@ const pull = require('../server/node_modules/pull-stream'); const { getConfig } = require('../configs/config-manager.js'); const { buildValidatedTombstoneSet } = require('./tombstone_validator'); +const { readTyped } = require('./typed_log'); const logLimit = getConfig().ssbLogStream?.limit || 1000; +const CV_TYPES = ['curriculum', 'tombstone']; + const extractBlobId = str => { if (!str || typeof str !== 'string') return null; const match = str.match(/\(([^)]+\.sha256)\)/); @@ -24,7 +27,7 @@ module.exports = ({ cooler }) => { return { type: 'curriculum', - async createCV(data, photoBlobId) { + async createCV(data, photoBlobId, pdfBlobId = null) { const ssbClient = await openSsb(); const userId = ssbClient.id; const content = { @@ -33,6 +36,7 @@ module.exports = ({ cooler }) => { name: data.name, description: data.description, photo: extractBlobId(photoBlobId) || null, + pdf: extractBlobId(pdfBlobId) || null, contact: userId, personalSkills: parseCSV(data.personalSkills), personalExperiences: data.personalExperiences || '', @@ -47,6 +51,8 @@ module.exports = ({ cooler }) => { status: data.status || 'LOOKING FOR WORK', preferences: data.preferences || 'REMOTE WORKING', visibility: String(data.visibility || 'PUBLIC').toUpperCase() === 'HIDDEN' ? 'HIDDEN' : 'PUBLIC', + aiManaged: data.aiManaged === undefined ? true : (data.aiManaged === true || data.aiManaged === '1' || data.aiManaged === 'true'), + matchThreshold: Math.min(100, Math.max(0, parseInt(data.matchThreshold, 10) || 80)), createdAt: new Date().toISOString() }; return new Promise((resolve, reject) => { @@ -54,7 +60,7 @@ module.exports = ({ cooler }) => { }); }, - async updateCV(id, data, photoBlobId) { + async updateCV(id, data, photoBlobId, pdfBlobId = null) { const ssbClient = await openSsb(); const userId = ssbClient.id; @@ -85,7 +91,8 @@ module.exports = ({ cooler }) => { author: userId, name: data.name, description: data.description, - photo: extractBlobId(photoBlobId) || null, + photo: extractBlobId(photoBlobId) || old.content.photo || null, + pdf: extractBlobId(pdfBlobId) || old.content.pdf || null, contact: userId, personalSkills: parseCSV(data.personalSkills), personalExperiences: data.personalExperiences || '', @@ -102,6 +109,12 @@ module.exports = ({ cooler }) => { visibility: data.visibility !== undefined ? (String(data.visibility).toUpperCase() === 'HIDDEN' ? 'HIDDEN' : 'PUBLIC') : (old.content.visibility || 'PUBLIC'), + aiManaged: data.aiManaged !== undefined + ? (data.aiManaged === true || data.aiManaged === '1' || data.aiManaged === 'true') + : (old.content.aiManaged !== false), + matchThreshold: data.matchThreshold !== undefined + ? Math.min(100, Math.max(0, parseInt(data.matchThreshold, 10) || 80)) + : (Number(old.content.matchThreshold) || 80), createdAt: old.content.createdAt, updatedAt: new Date().toISOString() }; @@ -143,34 +156,27 @@ module.exports = ({ cooler }) => { const userId = ssbClient.id; const authorId = targetUserId || userId; - return new Promise((resolve, reject) => { - pull( - ssbClient.createLogStream({ limit: logLimit }), - pull.collect((err, msgs) => { - if (err) return reject(err); + const msgs = await readTyped(ssbClient, CV_TYPES, { limit: logLimit }); - const tombstoned = buildValidatedTombstoneSet(msgs); + const tombstoned = buildValidatedTombstoneSet(msgs); - const cvMsgs = msgs - .filter(m => - m.value?.content?.type === 'curriculum' && - m.value.author === authorId && - !tombstoned.has(m.key) - ) - .sort((a, b) => b.value.timestamp - a.value.timestamp); + const cvMsgs = msgs + .filter(m => + m.value?.content?.type === 'curriculum' && + m.value.author === authorId && + !tombstoned.has(m.key) + ) + .sort((a, b) => b.value.timestamp - a.value.timestamp); - if (!cvMsgs.length) { - return resolve(null); - } + if (!cvMsgs.length) { + return null; + } - const latest = cvMsgs[0]; - const c = latest.value.content; - const visibility = String(c.visibility || 'PUBLIC').toUpperCase() === 'HIDDEN' ? 'HIDDEN' : 'PUBLIC'; - if (visibility === 'HIDDEN' && authorId !== userId) return resolve(null); - resolve({ id: latest.key, ...c, visibility }); - }) - ); - }); + const latest = cvMsgs[0]; + const c = latest.value.content; + const visibility = String(c.visibility || 'PUBLIC').toUpperCase() === 'HIDDEN' ? 'HIDDEN' : 'PUBLIC'; + if (visibility === 'HIDDEN' && authorId !== userId) return null; + return { id: latest.key, ...c, visibility }; } }; }; diff --git a/src/models/data_model.js b/src/models/data_model.js new file mode 100644 index 0000000..f51ca0b --- /dev/null +++ b/src/models/data_model.js @@ -0,0 +1,496 @@ +const pull = require('../server/node_modules/pull-stream'); +const { getConfig } = require('../configs/config-manager.js'); +const { buildValidatedTombstoneSet } = require('./tombstone_validator'); +const { readTyped } = require('./typed_log'); +const { isContentVisibleTo } = require('./content_visibility'); +const { versionGroups, versionEntry } = require('../backend/dedupe'); + +const logLimit = getConfig().ssbLogStream?.limit || 1000; + +const norm = (v) => String(v == null ? '' : v).trim().toLowerCase(); + +const KINDS = { + inhabitants: { type: 'curriculum', href: (id, c) => `/author/${encodeURIComponent(c.author)}` }, + jobs: { type: 'job', href: (id) => `/jobs/${encodeURIComponent(id)}` }, + projects: { type: 'project', href: (id) => `/projects/${encodeURIComponent(id)}` }, + events: { type: 'event', href: (id) => `/events/${encodeURIComponent(id)}` }, + tribes: { type: 'tribe', href: (id) => `/tribe/${encodeURIComponent(id)}` }, + market: { type: 'market', href: (id) => `/market/${encodeURIComponent(id)}` }, + housing: { type: 'housing', href: (id) => `/housing/${encodeURIComponent(id)}` }, + industry: { type: 'industry', href: (id) => `/industry/${encodeURIComponent(id)}` }, + tasks: { type: 'task', href: (id) => `/tasks/${encodeURIComponent(id)}` }, + reports: { type: 'report', href: (id) => `/reports/${encodeURIComponent(id)}` }, + votes: { type: 'poll', href: (id) => `/polls/${encodeURIComponent(id)}` }, + audios: { type: 'audio', href: (id) => `/audios/${encodeURIComponent(id)}` }, + videos: { type: 'video', href: (id) => `/videos/${encodeURIComponent(id)}` }, + images: { type: 'image', href: (id) => `/images/${encodeURIComponent(id)}` }, + documents: { type: 'document', href: (id) => `/documents/${encodeURIComponent(id)}` }, + bookmarks: { type: 'bookmark', href: (id) => `/bookmarks/${encodeURIComponent(id)}` }, + torrents: { type: 'torrent', href: (id) => `/torrents/${encodeURIComponent(id)}` }, + files: { type: 'file', href: (id) => `/files/${encodeURIComponent(id)}` }, + chats: { type: 'chat', href: (id) => `/chats/${encodeURIComponent(id)}` }, + pads: { type: 'pad', href: (id) => `/pads/${encodeURIComponent(id)}` }, + rooms: { type: 'room', href: (id) => `/rooms/${encodeURIComponent(id)}` }, + maps: { type: 'map', href: (id) => `/maps/${encodeURIComponent(id)}` }, + calendars: { type: 'calendar', href: (id) => `/calendars/${encodeURIComponent(id)}` }, + forum: { type: 'forum', href: (id) => `/forum/${encodeURIComponent(id)}` }, + school: { type: 'schoolCourse', href: (id) => `/school/course/${encodeURIComponent(id)}` }, + wiki: { type: 'wikiPage', href: (id) => `/wiki/${encodeURIComponent(id)}` }, + emergencies: { type: 'emergency', href: (id) => `/emergencies/${encodeURIComponent(id)}` }, + mailing: { type: 'mailingList', href: (id) => `/mailing/${encodeURIComponent(id)}` }, + logistics: { type: 'logisticsRoute', href: (id) => `/logistics/${encodeURIComponent(id)}` }, + podcasts: { type: 'podcast', href: (id) => `/podcasts/${encodeURIComponent(id)}` }, + campaigns: { type: 'campaign', href: (id) => `/campaigns/${encodeURIComponent(id)}` }, + blogs: { type: 'post', href: (id) => `/blogs/${encodeURIComponent(id)}` } +}; + +const isRootBlogPost = (c) => + !!c && c.type === 'post' && typeof c.text === 'string' && + !c.root && !c.fork && !c.branch && !c.about && !c.target && + c.private !== true && !Array.isArray(c.recps); + +const KIND_BY_TYPE = Object.fromEntries(Object.entries(KINDS).map(([k, v]) => [v.type, k])); + +const cvSkills = (c) => [ + ...(c.personalSkills || []), + ...(c.oasisSkills || []), + ...(c.educationalSkills || []), + ...(c.professionalSkills || []) +]; + +const PLACEHOLDERS = new Set(['unknown', 'n/a', 'na', 'none', '-', 'other']); + +const TITLE_STOPWORDS = new Set([ + 'the', 'this', 'that', 'and', 'for', 'with', 'from', 'into', 'about', 'new', 'all', 'not', 'are', 'was', 'you', 'your', 'our', + 'los', 'las', 'del', 'una', 'unos', 'unas', 'este', 'esta', 'esto', 'que', 'con', 'para', 'por', 'sin', 'sobre', 'bajo', 'mas', 'más' +]); + +const titleTerms = (c) => String(c.title || c.name || c.question || c.concept || '') + .toLowerCase() + .split(/[^\w\u0080-\uFFFF]+/) + .filter(w => w.length >= 3 && !TITLE_STOPWORDS.has(w) && !/^\d+$/.test(w)); + +const termsOf = (kind, c) => { + const out = []; + if (kind === 'inhabitants') { + out.push(...cvSkills(c)); + if (c.languages) out.push(...String(c.languages).split(/[,;]/)); + } else if (kind === 'jobs') { + out.push(...(c.tasks || []), ...(c.tags || []), c.job_type, c.location); + } else if (kind === 'industry') { + out.push(...(c.tags || []), c.sector, ...(c.skills || [])); + } else { + out.push(...(c.tags || [])); + if (c.category) out.push(c.category); + } + return Array.from(new Set(out.map(norm).filter(t => t && !PLACEHOLDERS.has(t)))); +}; + +const DESC_TERMS_MAX = 40; +const descTerms = (c) => String(c.description || c.text || c.body || '') + .toLowerCase() + .split(/[^\w\u0080-\uFFFF]+/) + .filter(w => w.length >= 4 && !TITLE_STOPWORDS.has(w) && !/^\d+$/.test(w)) + .slice(0, DESC_TERMS_MAX); + +const FAV_KIND = Object.fromEntries(Object.keys(KINDS).map(k => [k, k === 'votes' ? 'polls' : k])); + +const ALIKE_MIN_AFFINITY = 0.3; +const RATED_MIN_OPINIONS = 3; +const REASON_ORDER = ['related', 'content', 'mutual', 'following', 'supportsYou', 'tribe', 'alike', 'cv', 'pinned', 'rated', 'near']; + +const opinionsTotalOf = (c) => { + const ops = c && typeof c.opinions === 'object' && c.opinions ? c.opinions : {}; + let total = 0; + for (const v of Object.values(ops)) total += Number(v) || 0; + return total; +}; + +const excerpt = (s) => String(s || '').replace(/!?\[[^\]]*\]\([^)]*\)/g, ' ').replace(/[#*_>`~|]/g, ' ').replace(/\s+/g, ' ').trim().slice(0, 90); +const titleOf = (kind, c, author) => { + if (kind === 'inhabitants') return c.name || ''; + return c.title || c.name || c.question || c.concept || c.subject || c.contentWarning || excerpt(typeof c.text === 'string' ? c.text : '') || excerpt(typeof c.description === 'string' ? c.description : '') || ''; +}; + +const jaccard = (a, b) => { + if (!a.size || !b.size) return { score: 0, common: [] }; + const common = [...b].filter(x => a.has(x)); + const union = a.size + b.size - common.length; + return { score: union > 0 ? common.length / union : 0, common }; +}; + +module.exports = ({ cooler, favoriteIdsFor = null }) => { + let ssb; + const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb; }; + + const DATA_TYPES = [...Object.values(KINDS).map(v => v.type), 'tombstone', 'contact', 'about']; + + const getAllMessages = async (ssbClient) => readTyped(ssbClient, DATA_TYPES, { limit: logLimit }); + + const buildGraph = async () => { + const ssbClient = await openSsb(); + const viewerId = ssbClient.id; + const messages = await getAllMessages(ssbClient); + const tomb = buildValidatedTombstoneSet(messages); + + const candidates = new Map(); + const versions = []; + const follows = new Map(); + const followers = new Map(); + const aboutTerms = new Set(); + for (const m of messages) { + const v = m && m.value; + const c = v && v.content; + if (!c || typeof c !== 'object' || !c.type) continue; + if (c.type === 'contact') { + if (typeof c.contact !== 'string' || typeof c.following !== 'boolean') continue; + if (v.author === viewerId) follows.set(c.contact, c.following); + else if (c.contact === viewerId) followers.set(v.author, c.following); + continue; + } + if (c.type === 'about') { + if (v.author === viewerId && c.about === viewerId && typeof c.description === 'string') for (const t of descTerms(c)) aboutTerms.add(norm(t)); + continue; + } + if (c.tribeId && c.type !== 'tribe') continue; + if (c.type === 'post' && !isRootBlogPost(c)) continue; + const kind = KIND_BY_TYPE[c.type]; + if (!kind) continue; + versions.push(versionEntry(m)); + if (tomb.has(m.key)) continue; + if (!isContentVisibleTo(c.type, c, viewerId, v.author)) continue; + candidates.set(m.key, { key: m.key, author: v.author, ts: v.timestamp || m.timestamp || 0, kind, c }); + } + + const vg = versionGroups(versions); + const latestByRoot = new Map(); + for (const root of vg.roots) { + const tip = candidates.get(vg.tipOf(root)); + if (!tip) continue; + if (tip.kind !== 'tribes') { latestByRoot.set(root, { ...tip, key: root }); continue; } + const members = new Set(Array.isArray(tip.c.members) ? tip.c.members : []); + const lastByAuthor = new Map(); + for (const k of vg.groups.get(root)) { + const n = candidates.get(k); + if (!n || n.author === tip.author || n.ts < tip.ts) continue; + const prev = lastByAuthor.get(n.author); + if (!prev || n.ts >= prev.ts) lastByAuthor.set(n.author, n); + } + for (const [a, n] of lastByAuthor) { if (Array.isArray(n.c.members) && n.c.members.includes(a)) members.add(a); else members.delete(a); } + latestByRoot.set(root, { ...tip, key: root, c: { ...tip.c, members: [...members] } }); + } + + const nodes = []; + const byAuthorCv = new Map(); + for (const node of latestByRoot.values()) { + const coreTerms = termsOf(node.kind, node.c); + const extra = node.kind === 'inhabitants' ? [] : [...titleTerms(node.c), ...descTerms(node.c)].map(norm).filter(t => t && !PLACEHOLDERS.has(t)); + const terms = Array.from(new Set([...coreTerms, ...extra])); + if (!terms.length) continue; + const entry = { + id: node.key, + kind: node.kind, + ctype: KINDS[node.kind] ? KINDS[node.kind].type : '', + author: node.author, + title: titleOf(node.kind, node.c, node.author), + terms, + termSet: new Set(terms), + coreTermSet: new Set(coreTerms), + ts: node.ts, + createdAt: node.c.createdAt || new Date(node.ts).toISOString(), + href: KINDS[node.kind].href(node.key, node.c), + members: node.kind === 'tribes' && Array.isArray(node.c.members) ? node.c.members : null, + location: norm(node.c.location || ''), + opinionsTotal: opinionsTotalOf(node.c) + }; + if (node.kind === 'inhabitants') { + const prev = byAuthorCv.get(entry.author); + if (prev && prev.ts >= entry.ts) continue; + byAuthorCv.set(entry.author, entry); + continue; + } + nodes.push(entry); + } + for (const cv of byAuthorCv.values()) nodes.push(cv); + + const following = new Set([...follows.entries()].filter(([, on]) => on).map(([id]) => id)); + const supporters = new Set([...followers.entries()].filter(([, on]) => on).map(([id]) => id)); + return { viewerId, nodes, cvByAuthor: byAuthorCv, following, supporters, aboutTerms }; + }; + + const favoriteIds = async () => { + if (typeof favoriteIdsFor !== 'function') return new Set(); + const out = new Set(); + await Promise.all(Object.values(FAV_KIND).map(async (k) => { + try { for (const id of await favoriteIdsFor(k)) out.add(String(id)); } catch (_) {} + })); + return out; + }; + + const diversify = (items) => { + const leaders = new Map(); + for (const it of items) if (!leaders.has(it.kind)) leaders.set(it.kind, it); + const lead = new Set(leaders.values()); + return [...leaders.values()].sort((x, y) => y.score - x.score || y.ts - x.ts) + .concat(items.filter(it => !lead.has(it))); + }; + + const MAX_ENTITIES = 400; + const MAX_PAIRS = 300; + + const strip = (n) => ({ + id: n.id, kind: n.kind, ctype: n.ctype, author: n.author, title: n.title, + href: n.href, createdAt: n.createdAt, ts: n.ts + }); + + const buildProfile = async (graph) => { + const { viewerId, nodes, cvByAuthor, aboutTerms } = graph; + const use = nodes.slice(0, MAX_ENTITIES); + const favs = await favoriteIds(); + + const myTermSet = new Set(); + const cvTerms = new Set(); + const contentTerms = new Set(); + const mineCv = cvByAuthor.get(viewerId); + if (mineCv) for (const t of mineCv.terms) { myTermSet.add(t); cvTerms.add(t); } + for (const t of aboutTerms) if (t && !PLACEHOLDERS.has(t)) myTermSet.add(t); + const tribeMates = new Set(); + const pinnedTerms = new Set(); + let myLocation = mineCv ? mineCv.location : ''; + for (const n of use) { + if (String(n.author) === String(viewerId)) { + for (const t of n.terms) { myTermSet.add(t); contentTerms.add(t); } + if (!myLocation && n.location) myLocation = n.location; + } + if (favs.has(String(n.id))) for (const t of n.terms) { myTermSet.add(t); pinnedTerms.add(t); } + if (n.members && (String(n.author) === String(viewerId) || n.members.includes(viewerId))) for (const m of n.members) tribeMates.add(m); + } + tribeMates.delete(viewerId); + + const df = new Map(); + for (const n of use) for (const t of n.termSet) df.set(t, (df.get(t) || 0) + 1); + const total = use.length || 1; + const weightOf = (t) => Math.log(1 + total / (df.get(t) || 1)); + return { use, myTermSet, cvTerms, contentTerms, pinnedTerms, tribeMates, myLocation, df, weightOf, total }; + }; + + const affinitiesFor = (viewerId, { use, myTermSet, pinnedTerms, tribeMates, weightOf }) => { + const termsByAuthor = new Map(); + for (const n of use) { + if (String(n.author) === String(viewerId)) continue; + if (!termsByAuthor.has(n.author)) termsByAuthor.set(n.author, new Set()); + const set = termsByAuthor.get(n.author); + for (const t of n.termSet) set.add(t); + } + let myW = 0; + for (const t of myTermSet) myW += weightOf(t); + const byAuthor = new Map(); + for (const [author, terms] of termsByAuthor) { + const common = [...terms].filter(t => myTermSet.has(t)); + let commonW = 0; + for (const t of common) commonW += weightOf(t); + let theirW = 0; + for (const t of terms) theirW += weightOf(t); + const floor = Math.min(myW, theirW); + const score = floor > 0 ? Math.min(1, commonW / floor) : 0; + common.sort((x, y) => weightOf(y) - weightOf(x) || x.localeCompare(y)); + const reasons = []; + if (tribeMates.has(String(author))) reasons.push('tribe'); + if (common.some(t => pinnedTerms.has(t))) reasons.push('pinned'); + byAuthor.set(author, { score, common, reasons }); + } + for (const m of tribeMates) if (!byAuthor.has(m)) byAuthor.set(m, { score: 0, common: [], reasons: ['tribe'] }); + return byAuthor; + }; + + return { + KINDS: Object.keys(KINDS), + + async authorAffinities() { + const graph = await buildGraph(); + const profile = await buildProfile(graph); + return { viewerId: graph.viewerId, byAuthor: affinitiesFor(graph.viewerId, profile), hasProfile: profile.myTermSet.size > 0 }; + }, + + async listMatches(filter = 'ALL', opts = {}) { + const graph = await buildGraph(); + const { viewerId, following, supporters } = graph; + const profile = await buildProfile(graph); + const { use, myTermSet, cvTerms, contentTerms, pinnedTerms, tribeMates, myLocation, df, weightOf, total } = profile; + const affinity = affinitiesFor(viewerId, profile); + const f = String(filter || 'ALL').toUpperCase(); + + const expanded = new Set(); + for (const n of use) { + if (String(n.author) === String(viewerId)) continue; + if (![...n.termSet].some(t => myTermSet.has(t))) continue; + for (const t of n.termSet) if (!myTermSet.has(t)) expanded.add(t); + } + + let out = []; + for (const n of use) { + if (!myTermSet.size) break; + if (String(n.author) === String(viewerId)) continue; + const common = [...n.termSet].filter(t => myTermSet.has(t)); + let commonW = 0; + for (const t of common) commonW += weightOf(t); + let indirectW = 0; + for (const t of n.termSet) if (!myTermSet.has(t) && expanded.has(t)) indirectW += weightOf(t); + let itemW = 0; + for (const t of n.termSet) itemW += weightOf(t); + const direct = itemW > 0 ? Math.min(1, commonW / itemW) : 0; + const indirect = itemW > 0 ? Math.min(1, indirectW / itemW) : 0; + let dfSum = 0; + for (const t of n.termSet) dfSum += (df.get(t) || 1) / total; + const connectivity = n.termSet.size ? dfSum / n.termSet.size : 0; + const base = 0.8 * direct + 0.15 * indirect + 0.05 * connectivity; + const ageDays = Math.max(0, (Date.now() - (n.ts || 0)) / 86400000); + const recency = 0.8 + 0.2 * Math.exp(-ageDays / 60); + const social = (following.has(String(n.author)) ? 0.1 : 0) + (tribeMates.has(String(n.author)) ? 0.05 : 0); + const score = Math.min(1, base * recency + social); + common.sort((x, y) => weightOf(y) - weightOf(x) || x.localeCompare(y)); + const author = String(n.author); + const flags = new Set(); + if (!common.length && indirect > 0) flags.add('related'); + if (common.some(t => contentTerms.has(t))) flags.add('content'); + if (following.has(author) && supporters.has(author)) flags.add('mutual'); + else if (following.has(author)) flags.add('following'); + else if (supporters.has(author)) flags.add('supportsYou'); + if (tribeMates.has(author)) flags.add('tribe'); + const aff = affinity.get(n.author); + if (aff && aff.score >= ALIKE_MIN_AFFINITY) flags.add('alike'); + if (common.some(t => cvTerms.has(t))) flags.add('cv'); + if (common.some(t => pinnedTerms.has(t))) flags.add('pinned'); + if (n.opinionsTotal >= RATED_MIN_OPINIONS) flags.add('rated'); + if (myLocation && n.location && n.location === myLocation) flags.add('near'); + const reasons = REASON_ORDER.filter(r => flags.has(r)); + out.push({ ...strip(n), score, common, connections: common.length, reasons }); + } + + if (opts.hidden && (opts.hidden.authors || opts.hidden.ids)) { + const ha = opts.hidden.authors instanceof Set ? opts.hidden.authors : new Set(); + const hi = opts.hidden.ids instanceof Set ? opts.hidden.ids : new Set(); + out = out.filter(s => !ha.has(String(s.author)) && !hi.has(String(s.id))); + } + const anyMatches = out.length > 0; + const kindsAvail = {}; + for (const n of use) { + if (String(n.author) !== String(viewerId)) kindsAvail[n.kind] = true; + } + + const kindsMatched = {}; + for (const s of out) kindsMatched[s.kind] = true; + + if (f === 'RECENT') { + const cutoff = Date.now() - 24 * 60 * 60 * 1000; + out = out.filter(s => s.ts >= cutoff); + } else if (f !== 'ALL' && f !== 'TOP') { + const kind = f.toLowerCase(); + if (KINDS[kind]) out = out.filter(s => s.kind === kind); + } + + const q = norm(opts.q); + if (q) out = out.filter(s => norm(s.title).includes(q) || s.common.some(t => t.includes(q))); + + const present = new Set(); + for (const s of out) for (const r of s.reasons) present.add(r); + const reasonsAvail = REASON_ORDER.filter(r => present.has(r)); + const reason = String(opts.reason || '').trim(); + if (reason && REASON_ORDER.includes(reason)) out = out.filter(s => s.reasons.includes(reason)); + + if (f === 'RECENT') out.sort((x, y) => y.ts - x.ts || y.score - x.score); + else out.sort((x, y) => y.score - x.score || y.ts - x.ts); + if (f === 'ALL' || f === 'TOP') out = diversify(out); + + return { + matches: out.slice(0, MAX_PAIRS), + total: out.length, + hasProfile: myTermSet.size > 0, + myTerms: [...myTermSet], + kindsAvail, + kindsMatched, + anyMatches, + reasonsAvail, + reason: reasonsAvail.includes(reason) ? reason : '' + }; + }, + + async jobMatchesFor(viewerId, { minScore = 0.8 } = {}) { + const { nodes, cvByAuthor } = await buildGraph(); + const mine = cvByAuthor.get(viewerId); + if (!mine) return []; + return nodes + .filter(n => n.kind === 'jobs' && String(n.author) !== String(viewerId)) + .map(n => { + const { score, common } = jaccard(mine.termSet, n.coreTermSet || n.termSet); + return { id: n.id, title: n.title, author: n.author, href: n.href, score, common }; + }) + .filter(m => m.score >= minScore) + .sort((a, b) => b.score - a.score); + }, + + async cohesion() { + const { cvByAuthor, nodes } = await buildGraph(); + const use = nodes.slice(0, MAX_ENTITIES); + let comparisons = 0; + let sum = 0; + let connectedPairs = 0; + const linkedEntities = new Set(); + for (let i = 0; i < use.length; i++) { + for (let j = i + 1; j < use.length; j++) { + const a = use[i], b = use[j]; + if (a.kind === 'inhabitants' && b.kind === 'inhabitants' && a.author === b.author) continue; + const { score } = jaccard(a.termSet, b.termSet); + comparisons += 1; + sum += score; + if (score > 0) { connectedPairs += 1; linkedEntities.add(a.id); linkedEntities.add(b.id); } + } + } + const coefficient = comparisons > 0 ? sum / comparisons : 0; + + const people = [...cvByAuthor.values()]; + const linked = new Set(); + let cvComparisons = 0; + let cvSum = 0; + for (let i = 0; i < people.length; i++) { + for (let j = i + 1; j < people.length; j++) { + const { score } = jaccard(people[i].termSet, people[j].termSet); + cvComparisons += 1; + cvSum += score; + if (score > 0) { linked.add(people[i].author); linked.add(people[j].author); } + } + } + const cvCoefficient = cvComparisons > 0 ? cvSum / cvComparisons : 0; + + const skillSet = new Set(); + for (const p of people) for (const t of p.terms) skillSet.add(t); + + const termCount = new Map(); + for (const n of nodes) for (const t of n.terms) termCount.set(t, (termCount.get(t) || 0) + 1); + const topTerms = [...termCount.entries()] + .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])) + .slice(0, 12) + .map(([term, count]) => ({ term, count })); + + const perKind = {}; + for (const n of nodes) perKind[n.kind] = (perKind[n.kind] || 0) + 1; + + return { + coefficient, + percent: Math.round(coefficient * 1000) / 10, + comparisons, + pairs: connectedPairs, + entities: use.length, + distinctTerms: termCount.size, + topTerms, + perKind, + people: people.length, + skills: skillSet.size, + connected: linkedEntities.size, + isolated: Math.max(0, use.length - linkedEntities.size), + cvCoefficient, + cvPercent: Math.round(cvCoefficient * 1000) / 10 + }; + } + }; +}; diff --git a/src/models/dev_model.js b/src/models/dev_model.js new file mode 100644 index 0000000..8b18406 --- /dev/null +++ b/src/models/dev_model.js @@ -0,0 +1,340 @@ +const fs = require('fs'); +const path = require('path'); +const { getConfig } = require('../configs/config-manager.js'); + +const ROOT = path.resolve(__dirname, '..', '..'); + +const ALLOWED_ROOTS = ['src', 'docs', 'scripts', 'test']; +const ALLOWED_ROOT_FILES = ['README.md', 'LICENSE', 'install.sh', 'oasis.sh']; +const DENIED_SEGMENTS = new Set(['node_modules', '.git', '.ssb', 'packages', 'configs', 'embeddings', 'tiles', 'cache', 'results']); +const DENIED_NAMES = new Set(['secret', '.env', '.npmrc', 'package-lock.json']); +const DENIED_EXTENSIONS = new Set(['.gguf', '.jks', '.keystore', '.enc', '.pem', '.key', '.p12', '.crt']); +const VIEWABLE_EXTENSIONS = new Set(['.js', '.mjs', '.json', '.css', '.md', '.sh', '.html', '.txt', '.yml', '.yaml']); + +const STATS_TTL = 60 * 1000; +let statsCache = null; +let statsCacheAt = 0; + +const MAX_VIEW_BYTES = 2 * 1024 * 1024; +const PAGE_LINES = 500; +const SEARCH_MAX = 200; +const SEARCH_SNIPPET = 200; + +const extOf = (name) => { + const i = String(name || '').lastIndexOf('.'); + return i > 0 ? String(name).slice(i).toLowerCase() : ''; +}; + +const isDeniedEntry = (name) => { + const n = String(name || ''); + if (!n || n === '.' || n === '..') return true; + if (n.toLowerCase().startsWith('.git')) return true; + if (DENIED_SEGMENTS.has(n)) return true; + if (DENIED_NAMES.has(n)) return true; + if (DENIED_EXTENSIONS.has(extOf(n))) return true; + return false; +}; + +const PLAIN_TEXT_NAME = /^[A-Z][A-Z0-9_-]*$/; + +const isViewable = (name) => (VIEWABLE_EXTENSIONS.has(extOf(name)) || PLAIN_TEXT_NAME.test(String(name || ''))) && !isDeniedEntry(name); + +function realRoot() { + try { return fs.realpathSync(ROOT); } catch (_) { return ROOT; } +} + +function splitPath(rel) { + const raw = String(rel || ''); + for (let i = 0; i < raw.length; i++) { if (raw.charCodeAt(i) < 32) throw new Error('Invalid path'); } + return raw.replace(/^[/\\]+/, '').split(/[/\\]+/).filter(s => s && s !== '.'); +} + +function resolveSafe(rel) { + const segments = splitPath(rel); + if (segments.some(s => s === '..')) throw new Error('Invalid path'); + if (segments.some(isDeniedEntry)) throw new Error('Path not available'); + if (segments.length) { + const head = segments[0]; + const isRootFile = segments.length === 1 && ALLOWED_ROOT_FILES.includes(head); + if (!ALLOWED_ROOTS.includes(head) && !isRootFile) throw new Error('Path not available'); + } + const target = path.resolve(ROOT, segments.join(path.sep)); + let real; + try { real = fs.realpathSync(target); } catch (_) { throw new Error('Not found'); } + const base = realRoot(); + if (real !== base && !real.startsWith(base + path.sep)) throw new Error('Path not available'); + return { abs: real, rel: segments.join('/'), segments }; +} + +function parentsOf(segments) { + const out = []; + let acc = ''; + for (const s of segments) { + acc = acc ? `${acc}/${s}` : s; + out.push({ name: s, path: acc }); + } + return out; +} + +function rootListing() { + const dirs = []; + const files = []; + for (const name of ALLOWED_ROOTS) { + const abs = path.join(ROOT, name); + if (fs.existsSync(abs) && fs.statSync(abs).isDirectory()) dirs.push({ name, path: name }); + } + for (const name of ALLOWED_ROOT_FILES) { + const abs = path.join(ROOT, name); + if (!fs.existsSync(abs) || !fs.statSync(abs).isFile()) continue; + files.push({ name, path: name, size: fs.statSync(abs).size, ext: extOf(name), viewable: isViewable(name) }); + } + return { path: '', parents: [], dirs, files, hidden: 0 }; +} + +function walkFiles(relDir, onFile) { + let entries; + try { entries = fs.readdirSync(path.resolve(ROOT, relDir), { withFileTypes: true }); } catch (_) { return true; } + for (const e of entries) { + if (isDeniedEntry(e.name)) continue; + const rel = relDir ? `${relDir}/${e.name}` : e.name; + if (e.isSymbolicLink()) continue; + if (e.isDirectory()) { if (walkFiles(rel, onFile) === false) return false; continue; } + if (!e.isFile()) continue; + if (onFile(rel, e.name) === false) return false; + } + return true; +} + +const MODULE_ALIASES = { + ai: { model: 'src/AI/ai_service.mjs', view: 'src/views/AI_view.js' }, + aiNav: { model: 'src/AI/routes_index.js', view: 'src/views/AI_view.js', paths: ['/ai/ask'] }, + docs: { paths: ['/documents'] }, + blogs: { model: 'src/models/blog_model.js', view: 'src/views/blog_view.js', paths: ['/blogs'] }, + polls: { model: 'src/models/polls_model.js', view: 'src/views/polls_view.js', paths: ['/polls'] }, + data: { model: 'src/models/data_model.js', view: 'src/views/data_view.js', paths: ['/data'] }, + invites: { model: 'src/models/main_models.js', view: 'src/views/invites_view.js' }, + graphos: { model: 'src/models/main_models.js', view: 'src/views/graphos_view.js' } +}; + +const GROUPS = { + translations: ['src/client/assets/translations'], + styles: ['src/client/assets/styles', 'src/client/assets/themes'], + tests: ['test'] +}; + +module.exports = { + ROOT, + PAGE_LINES, + GROUPS, + + listGroup(key) { + const roots = GROUPS[String(key || '')]; + if (!roots) throw new Error('Not found'); + const dirs = []; + const files = []; + for (const rel of roots) { + let listing; + try { listing = module.exports.listTree(rel); } catch (_) { continue; } + dirs.push(...listing.dirs); + files.push(...listing.files); + } + dirs.sort((a, b) => a.name.localeCompare(b.name)); + files.sort((a, b) => a.name.localeCompare(b.name)); + return { group: String(key), path: '', parents: [], dirs, files, hidden: 0 }; + }, + + listTree(relPath) { + const segments = splitPath(relPath); + if (!segments.length) return rootListing(); + const target = resolveSafe(relPath); + const stat = fs.statSync(target.abs); + if (!stat.isDirectory()) throw new Error('Not a directory'); + const entries = fs.readdirSync(target.abs, { withFileTypes: true }); + const dirs = []; + const files = []; + let hidden = 0; + for (const e of entries) { + if (isDeniedEntry(e.name) || e.isSymbolicLink()) { hidden++; continue; } + const rel = `${target.rel}/${e.name}`; + if (e.isDirectory()) { dirs.push({ name: e.name, path: rel }); continue; } + if (!e.isFile()) { hidden++; continue; } + let size = 0; + try { size = fs.statSync(path.join(target.abs, e.name)).size; } catch (_) { size = 0; } + files.push({ name: e.name, path: rel, size, ext: extOf(e.name), viewable: isViewable(e.name) }); + } + dirs.sort((a, b) => a.name.localeCompare(b.name)); + files.sort((a, b) => a.name.localeCompare(b.name)); + return { path: target.rel, parents: parentsOf(target.segments), dirs, files, hidden }; + }, + + readFile(relPath, opts = {}) { + const target = resolveSafe(relPath); + const stat = fs.statSync(target.abs); + if (!stat.isFile()) throw new Error('Not a file'); + if (!isViewable(path.basename(target.abs))) throw new Error('This file type cannot be displayed'); + if (stat.size > MAX_VIEW_BYTES) throw new Error('This file is too large to be displayed'); + const all = fs.readFileSync(target.abs, 'utf8').split('\n'); + const totalLines = all.length; + const requested = parseInt(opts.from, 10); + const from = Math.min(Math.max(Number.isFinite(requested) ? requested : 1, 1), Math.max(1, totalLines)); + const to = Math.min(from + PAGE_LINES - 1, totalLines); + const lines = []; + for (let n = from; n <= to; n++) lines.push({ n, text: all[n - 1] }); + return { + path: target.rel, + parents: parentsOf(target.segments), + name: path.basename(target.abs), + size: stat.size, + totalLines, + from, + to, + pageLines: PAGE_LINES, + hasPrev: from > 1, + hasNext: to < totalLines, + lines + }; + }, + + rawFile(relPath) { + const target = resolveSafe(relPath); + const stat = fs.statSync(target.abs); + if (!stat.isFile()) throw new Error('Not a file'); + if (!isViewable(path.basename(target.abs))) throw new Error('This file type cannot be displayed'); + if (stat.size > MAX_VIEW_BYTES) throw new Error('This file is too large to be displayed'); + return { name: path.basename(target.abs), content: fs.readFileSync(target.abs, 'utf8') }; + }, + + searchCode(query, opts = {}) { + const q = String(query || '').trim(); + if (q.length < 2) return { query: q, results: [], truncated: false, scanned: 0 }; + const wanted = String(opts.ext || '').trim().toLowerCase(); + const max = Math.min(Math.max(parseInt(opts.max, 10) || SEARCH_MAX, 1), SEARCH_MAX); + const needle = q.toLowerCase(); + const results = []; + let scanned = 0; + let truncated = false; + for (const root of ALLOWED_ROOTS) { + if (truncated) break; + walkFiles(root, (rel, name) => { + if (!isViewable(name)) return true; + if (wanted && extOf(name) !== wanted) return true; + let stat; + try { stat = fs.statSync(path.resolve(ROOT, rel)); } catch (_) { return true; } + if (stat.size > MAX_VIEW_BYTES) return true; + scanned++; + let content; + try { content = fs.readFileSync(path.resolve(ROOT, rel), 'utf8'); } catch (_) { return true; } + if (content.toLowerCase().indexOf(needle) < 0) return true; + const lines = content.split('\n'); + for (let i = 0; i < lines.length; i++) { + if (lines[i].toLowerCase().indexOf(needle) < 0) continue; + results.push({ path: rel, line: i + 1, text: lines[i].trim().slice(0, SEARCH_SNIPPET) }); + if (results.length >= max) { truncated = true; return false; } + } + return true; + }); + } + return { query: q, results, truncated, scanned }; + }, + + moduleMap() { + const cfg = getConfig(); + const mods = Object.keys((cfg && cfg.modules) || {}).map(k => k.replace(/Mod$/, '')).sort(); + let backend = ''; + try { backend = fs.readFileSync(path.join(ROOT, 'src', 'backend', 'backend.js'), 'utf8'); } catch (_) { backend = ''; } + const modelFiles = fs.existsSync(path.join(ROOT, 'src', 'models')) ? fs.readdirSync(path.join(ROOT, 'src', 'models')) : []; + const viewFiles = fs.existsSync(path.join(ROOT, 'src', 'views')) ? fs.readdirSync(path.join(ROOT, 'src', 'views')) : []; + const stem = (s) => String(s || '').toLowerCase().replace(/s$/, ''); + const pick = (files, name, suffixes) => { + for (const suf of suffixes) { + const exact = files.find(f => f.toLowerCase() === `${name.toLowerCase()}${suf}`); + if (exact) return exact; + } + const singular = name.replace(/s$/, ''); + for (const suf of suffixes) { + const short = files.find(f => f.toLowerCase() === `${singular.toLowerCase()}${suf}`); + if (short) return short; + } + const target = stem(name); + return files.find(f => { + const suf = suffixes.find(x => f.toLowerCase().endsWith(x)); + if (!suf) return false; + const base = stem(f.slice(0, f.length - suf.length)); + return base.startsWith(target) || target.startsWith(base); + }) || null; + }; + const testsIndex = new Map(); + const modsDir = path.join(ROOT, 'test', 'mods'); + const indexTests = (dir, prefix) => { + let entries; + try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (_) { return; } + for (const e of entries) { + if (!e.isDirectory()) continue; + const rel = `${prefix}/${e.name}`; + if (!testsIndex.has(e.name)) testsIndex.set(e.name, rel); + indexTests(path.join(dir, e.name), rel); + } + }; + indexTests(modsDir, 'test/mods'); + return mods.map(name => { + const alias = MODULE_ALIASES[name] || {}; + const routePaths = Array.isArray(alias.paths) && alias.paths.length ? alias.paths : [`/${name}`]; + let routes = 0; + for (const routePath of routePaths) { + const escaped = routePath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const re = new RegExp(`\\.(get|post)\\(\\s*["'\`]${escaped}(?=["'/?])`, 'g'); + routes += backend ? (backend.match(re) || []).length : 0; + } + const model = alias.model !== undefined ? alias.model : (pick(modelFiles, name, ['_model.js', '_models.js']) ? `src/models/${pick(modelFiles, name, ['_model.js', '_models.js'])}` : null); + const view = alias.view !== undefined ? alias.view : (pick(viewFiles, name, ['_view.js', '_views.js']) ? `src/views/${pick(viewFiles, name, ['_view.js', '_views.js'])}` : null); + return { + name, + enabled: (cfg.modules || {})[`${name}Mod`] !== 'off', + model, + view, + routes, + tests: testsIndex.get(name) || testsIndex.get(name.replace(/s$/, '')) || (() => { + const target = stem(name); + for (const [dir, rel] of testsIndex) { + const base = stem(dir); + if (base.startsWith(target) || target.startsWith(base)) return rel; + } + return null; + })() + }; + }); + }, + + projectStats() { + const now = Date.now(); + if (statsCache && now - statsCacheAt < STATS_TTL) return statsCache; + let files = 0; + let lines = 0; + let bytes = 0; + for (const root of ALLOWED_ROOTS) { + walkFiles(root, (rel, name) => { + if (!isViewable(name)) return true; + try { + const abs = path.resolve(ROOT, rel); + const stat = fs.statSync(abs); + if (stat.size > MAX_VIEW_BYTES) return true; + files++; + bytes += stat.size; + lines += fs.readFileSync(abs, 'utf8').split('\n').length; + } catch (_) {} + return true; + }); + } + const cfg = getConfig(); + const modules = Object.keys((cfg && cfg.modules) || {}).length; + const translations = path.join(ROOT, 'src', 'client', 'assets', 'translations'); + const languages = fs.existsSync(translations) ? fs.readdirSync(translations).filter(f => /^oasis_[a-z]{2}\.js$/.test(f)).length : 0; + const testsDir = path.join(ROOT, 'test', 'mods'); + const testMods = fs.existsSync(testsDir) ? fs.readdirSync(testsDir).length : 0; + let version = ''; + try { version = String(JSON.parse(fs.readFileSync(path.join(ROOT, 'src', 'server', 'package.json'), 'utf8')).version || ''); } catch (_) { version = ''; } + statsCache = { files, lines, bytes, modules, languages, testMods, version, node: process.version }; + statsCacheAt = now; + return statsCache; + } +}; diff --git a/src/models/documents_model.js b/src/models/documents_model.js index ada7c64..b044b9d 100644 --- a/src/models/documents_model.js +++ b/src/models/documents_model.js @@ -2,8 +2,9 @@ const pull = require("../server/node_modules/pull-stream"); const { getConfig } = require("../configs/config-manager.js"); const categories = require("../backend/opinion_categories"); const { buildValidatedTombstoneSet } = require('./tombstone_validator'); -const { dedupeBy, norm } = require('../backend/dedupe'); -const mediaFavorites = require("../backend/media-favorites"); +const { dedupeBy, norm, versionGroups } = require('../backend/dedupe'); +const { readTyped, memoIndex } = require("./typed_log"); +const contentFavorites = require("../backend/content_favorites"); const logLimit = getConfig().ssbLogStream?.limit || 1000; @@ -34,20 +35,13 @@ module.exports = ({ cooler }) => { return ssb; }; - const getAllMessages = async (ssbClient) => - new Promise((resolve, reject) => { - pull( - ssbClient.createLogStream({ limit: logLimit }), - pull.collect((err, msgs) => (err ? reject(err) : resolve(msgs))) - ); - }); + const DOCUMENT_TYPES = ["document", "documentOpinion", "tombstone"]; + + const getAllMessages = async (ssbClient) => readTyped(ssbClient, DOCUMENT_TYPES, { limit: logLimit }); const buildIndex = (messages) => { const tomb = buildValidatedTombstoneSet(messages); const nodes = new Map(); - const parent = new Map(); - const child = new Map(); - const strictChild = new Map(); const opinionMsgs = []; for (const m of messages) { @@ -63,50 +57,16 @@ module.exports = ({ cooler }) => { const ts = v.timestamp || m.timestamp || 0; let sizeBytes = 0; try { sizeBytes = Buffer.byteLength(JSON.stringify(v), "utf8"); } catch (_) { sizeBytes = 0; } - nodes.set(k, { key: k, ts, c, sizeBytes, author: v.author }); - - if (c.replaces) { - parent.set(k, c.replaces); - child.set(c.replaces, k); - } + nodes.set(k, { key: k, ts, seq: v.sequence || 0, c, sizeBytes, author: v.author }); } - for (const [k, t] of Array.from(parent.entries())) { - const orig = nodes.get(t); - const node = nodes.get(k); - if (orig && node && orig.author === node.author) continue; - parent.delete(k); - if (child.get(t) === k) child.delete(t); - if (orig) nodes.delete(k); - } - - for (const [k, node] of nodes) { - const t = node.c.replaces; - if (t) { const orig = nodes.get(t); if (orig && orig.author === node.author) strictChild.set(t, k); } - } - - const rootOf = (id) => { - let cur = id, g = 0; - while (parent.has(cur) && nodes.has(parent.get(cur)) && g++ < 100000) cur = parent.get(cur); - return cur; - }; - - const contentTipOf = (root) => { - let cur = root, g = 0; - while (strictChild.has(cur) && g++ < 100000) cur = strictChild.get(cur); - const n = nodes.get(cur), rn = nodes.get(root); - return (n && rn && n.author === rn.author) ? cur : root; - }; - - const roots = new Set(); - for (const id of nodes.keys()) roots.add(rootOf(id)); + const vg = versionGroups([...nodes.values()].map(n => ({ key: n.key, author: n.author, seq: n.seq, ts: n.ts, replaces: n.c.replaces || null, claimedAuthor: typeof n.c.author === "string" ? n.c.author : null }))); + const rootOf = vg.rootOf; + const contentTipOf = vg.tipOf; const opinionsByRoot = new Map(); for (const op of opinionMsgs) { if (!nodes.has(op.target)) continue; const r = rootOf(op.target); if (!opinionsByRoot.has(r)) opinionsByRoot.set(r, []); opinionsByRoot.get(r).push(op); } - const forward = new Map(); - for (const [newId, oldId] of parent.entries()) forward.set(oldId, newId); - const resolveGroup = (root) => { const contentTip = contentTipOf(root); const contentNode = nodes.get(contentTip) || nodes.get(root); @@ -123,9 +83,9 @@ module.exports = ({ cooler }) => { }; const tipByRoot = new Map(); - for (const r of roots) tipByRoot.set(r, contentTipOf(r)); + for (const r of vg.roots) tipByRoot.set(r, contentTipOf(r)); - return { tomb, nodes, parent, child, rootOf, tipByRoot, forward, resolveGroup }; + return { tomb, nodes, rootOf, tipByRoot, resolveGroup }; }; const pickDoc = (node, rootId, agg) => { @@ -140,6 +100,7 @@ module.exports = ({ cooler }) => { author: node.author, title: c.title || "", description: c.description || "", + license: c.license || "", opinions: agg ? agg.opinions : (c.opinions || {}), opinions_inhabitants: agg ? agg.voters : safeArr(c.opinions_inhabitants), sizeBytes: node.sizeBytes || 0 @@ -154,7 +115,7 @@ module.exports = ({ cooler }) => { const favoritesSetForDocuments = async () => { try { - return await mediaFavorites.getFavoriteSet("documents"); + return await contentFavorites.getFavoriteSet("documents"); } catch { return new Set(); } @@ -178,16 +139,12 @@ module.exports = ({ cooler }) => { const messages = await getAllMessages(ssbClient); const idx = buildIndex(messages); - let tip = id; - while (idx.forward.has(tip)) tip = idx.forward.get(tip); - if (idx.tomb.has(tip)) throw new Error("Document not found"); - - let root = tip; - while (idx.parent.has(root)) root = idx.parent.get(root); + const root = idx.rootOf(id); + if (idx.tomb.has(idx.tipByRoot.get(root) || id)) throw new Error("Document not found"); return root; }, - async createDocument(blobMarkdown, tagsRaw, title, description) { + async createDocument(blobMarkdown, tagsRaw, title, description, license) { const ssbClient = await openSsb(); const blobId = parseBlobId(blobMarkdown); if (!blobId) throw new Error("Missing document blob"); @@ -203,7 +160,8 @@ module.exports = ({ cooler }) => { title: title || "", description: description || "", opinions: {}, - opinions_inhabitants: [] + opinions_inhabitants: [], + ...(license ? { license } : {}) }; return new Promise((resolve, reject) => { @@ -211,7 +169,7 @@ module.exports = ({ cooler }) => { }); }, - async updateDocumentById(id, blobMarkdown, tagsRaw, title, description) { + async updateDocumentById(id, blobMarkdown, tagsRaw, title, description, license) { const ssbClient = await openSsb(); const tipId = await this.resolveCurrentId(id); @@ -238,7 +196,8 @@ module.exports = ({ cooler }) => { tags, title: title !== undefined ? (title || "") : oldMsg.content.title || "", description: description !== undefined ? (description || "") : oldMsg.content.description || "", - updatedAt + updatedAt, + ...(license !== undefined ? { license } : {}) }; const tombstone = { type: "tombstone", target: tipId, deletedAt: updatedAt, author: userId }; @@ -327,12 +286,12 @@ module.exports = ({ cooler }) => { async getDocumentById(id) { const ssbClient = await openSsb(); const messages = await getAllMessages(ssbClient); - const idx = buildIndex(messages); + const idx = memoIndex('documents', messages, buildIndex); const favorites = await favoritesSetForDocuments(); const rootId = idx.rootOf(id); const agg = idx.resolveGroup(rootId); - if (!agg.contentNode || idx.tomb.has(agg.contentTip)) throw new Error("Document not found"); + if (!idx.tipByRoot.has(rootId) || !agg.contentNode || idx.tomb.has(agg.contentTip)) throw new Error("Document not found"); const c = agg.contentNode.c || {}; return { @@ -345,6 +304,7 @@ module.exports = ({ cooler }) => { author: agg.contentNode.author, title: c.title || "", description: c.description || "", + license: c.license || "", opinions: agg.opinions, opinions_inhabitants: agg.voters, isFavorite: favorites.has(rootId || agg.contentTip) diff --git a/src/models/emergencies_model.js b/src/models/emergencies_model.js new file mode 100644 index 0000000..91929b3 --- /dev/null +++ b/src/models/emergencies_model.js @@ -0,0 +1,297 @@ +const { readTyped, memoIndex } = require('./typed_log'); +const { buildValidatedTombstoneSet } = require('./tombstone_validator'); +const { getConfig } = require('../configs/config-manager.js'); +const logLimit = getConfig().ssbLogStream?.limit || 1000; + +const EMERGENCY_TYPES = ['emergency', 'emergencyConfirm', 'emergencyUpdate', 'tombstone']; +const CATEGORIES = ['WEATHER', 'INFRASTRUCTURE', 'HEALTH', 'SECURITY', 'LOST', 'NEIGHBORHOOD']; +const SEVERITY_THRESHOLDS = { LOW: 2, MEDIUM: 5, HIGH: 10 }; +const EXPIRES_OPTIONS = { '1d': 1, '3d': 3, '7d': 7, '30d': 30 }; +const MEDIA_RE = /!?\[(image|video):[^\]]*\]\((&[^)]+)\)/; +const RECENT_MS = 24 * 60 * 60 * 1000; + +const normU = (v) => String(v == null ? '' : v).trim().toUpperCase(); +const safeText = (v) => String(v == null ? '' : v).trim(); +const normalizeTags = (raw) => { + if (!raw) return []; + const arr = Array.isArray(raw) ? raw : String(raw).split(','); + return Array.from(new Set(arr.map(t => String(t || '').trim()).filter(Boolean))); +}; +const normalizeCategory = (v) => (CATEGORIES.includes(normU(v)) ? normU(v) : 'NEIGHBORHOOD'); +const severityOf = (confirmations) => { + const n = Number(confirmations) || 0; + if (n >= SEVERITY_THRESHOLDS.HIGH) return 'HIGH'; + if (n >= SEVERITY_THRESHOLDS.MEDIUM) return 'MEDIUM'; + if (n >= SEVERITY_THRESHOLDS.LOW) return 'LOW'; + return 'UNVERIFIED'; +}; +const expiresAtFrom = (expiresIn, from = Date.now()) => { + const days = EXPIRES_OPTIONS[String(expiresIn || '').toLowerCase()]; + return days ? new Date(from + days * 24 * 60 * 60 * 1000).toISOString() : null; +}; + +module.exports = ({ cooler }) => { + let ssb; + const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb; }; + + const readAll = async (ssbClient) => readTyped(ssbClient, EMERGENCY_TYPES, { limit: logLimit }); + + const buildIndex = (messages) => { + const tomb = buildValidatedTombstoneSet(messages); + const nodes = new Map(); + const parentOf = new Map(); + const confirms = []; + const updates = []; + for (const m of messages) { + const v = m.value || {}; + const c = v.content; + if (!c || typeof c !== 'object') continue; + if (c.type === 'emergency') { + nodes.set(m.key, { key: m.key, author: v.author, ts: v.timestamp || m.timestamp || 0, c }); + if (typeof c.replaces === 'string') parentOf.set(m.key, c.replaces); + } else if (c.type === 'emergencyConfirm' && typeof c.target === 'string') { + confirms.push({ key: m.key, target: c.target, author: v.author, ts: v.timestamp || 0 }); + } else if (c.type === 'emergencyUpdate' && typeof c.target === 'string') { + updates.push({ key: m.key, target: c.target, author: v.author, ts: v.timestamp || 0, text: safeText(c.text), createdAt: c.createdAt || new Date(v.timestamp || 0).toISOString(), replaces: typeof c.replaces === 'string' ? c.replaces : null }); + } + } + const rootOf = (key) => { + const seen = new Set(); + let cur = key; + while (parentOf.has(cur) && !seen.has(cur)) { seen.add(cur); cur = parentOf.get(cur); } + return cur; + }; + const updateParent = new Map(updates.filter(u => u.replaces).map(u => [u.key, u.replaces])); + const updateRootOf = (key) => { + const seen = new Set(); + let cur = key; + while (updateParent.has(cur) && !seen.has(cur)) { seen.add(cur); cur = updateParent.get(cur); } + return cur; + }; + const updateByKey = new Map(updates.map(u => [u.key, u])); + const updateVersions = new Map(); + for (const u of updates) { + const uroot = updateRootOf(u.key); + if (tomb.has(uroot)) continue; + if (!updateVersions.has(uroot)) updateVersions.set(uroot, []); + updateVersions.get(uroot).push(u); + } + const updateItems = []; + for (const [uroot, versions] of updateVersions) { + versions.sort((a, b) => a.ts - b.ts); + const first = versions[0]; + const own = versions.filter(v => v.author === first.author); + const tip = own[own.length - 1]; + const confirmers = Array.from(new Set(confirms.filter(cf => updateByKey.has(cf.target) && updateRootOf(cf.target) === uroot && cf.author !== first.author).map(cf => cf.author))); + updateItems.push({ + id: uroot, key: uroot, tipId: tip.key, target: first.target, author: first.author, + ts: first.ts, editedTs: tip.ts, text: tip.text, createdAt: first.createdAt, updatedAt: tip.createdAt, edited: tip.key !== uroot, + confirmations: confirmers, confirmationCount: confirmers.length, + lastActivityTs: Math.max(tip.ts, ...confirms.filter(cf => updateByKey.has(cf.target) && updateRootOf(cf.target) === uroot).map(cf => cf.ts)) + }); + } + const versionsByRoot = new Map(); + for (const node of nodes.values()) { + const root = rootOf(node.key); + if (tomb.has(root)) continue; + if (!versionsByRoot.has(root)) versionsByRoot.set(root, []); + versionsByRoot.get(root).push(node); + } + const emergencies = new Map(); + for (const [root, versions] of versionsByRoot) { + versions.sort((a, b) => a.ts - b.ts); + const first = versions[0]; + const own = versions.filter(v => v.author === first.author); + const tip = own[own.length - 1]; + const c = tip.c; + const thread = updateItems.filter(u => nodes.has(u.target) && rootOf(u.target) === root && u.author === first.author).sort((a, b) => a.ts - b.ts); + const confirmers = Array.from(new Set([ + ...confirms.filter(cf => nodes.has(cf.target) && rootOf(cf.target) === root && cf.author !== first.author).map(cf => cf.author), + ...thread.flatMap(u => u.confirmations) + ])); + const expiresAt = c.expiresAt || null; + const expired = !!(expiresAt && Date.parse(expiresAt) < Date.now()); + const status = normU(c.status) === 'RESOLVED' ? 'RESOLVED' : (expired ? 'EXPIRED' : 'ACTIVE'); + emergencies.set(root, { + id: root, rootId: root, tipId: tip.key, + title: safeText(c.title), text: safeText(c.text), + media: (() => { const m = safeText(c.text).match(MEDIA_RE); return m ? { kind: m[1], blobId: m[2] } : null; })(), + category: normalizeCategory(c.category), mapUrl: safeText(c.mapUrl), tags: normalizeTags(c.tags), + author: first.author, createdAt: first.c.createdAt || new Date(first.ts).toISOString(), + updatedAt: c.updatedAt || new Date(tip.ts).toISOString(), ts: tip.ts, lastActivityTs: Math.max(tip.ts, ...thread.map(u => u.lastActivityTs), ...confirms.filter(cf => nodes.has(cf.target) && rootOf(cf.target) === root).map(cf => cf.ts)), + expiresAt, expired, status, resolved: status === 'RESOLVED', + confirmations: confirmers, confirmationCount: confirmers.length, severity: severityOf(confirmers.length), + updates: thread + }); + } + const updatesById = new Map(updateItems.map(u => [u.id, u])); + return { emergencies, rootOf, nodes, updatesById, updateRootOf }; + }; + + const load = async () => { + const ssbClient = await openSsb(); + const idx = memoIndex('emergencies', await readAll(ssbClient), buildIndex); + return { ssbClient, idx }; + }; + + const find = (idx, id) => { + const key = String(id || ''); + if (idx.emergencies.has(key)) return idx.emergencies.get(key); + if (idx.nodes.has(key)) return idx.emergencies.get(idx.rootOf(key)) || null; + return null; + }; + + const publish = (ssbClient, content) => new Promise((resolve, reject) => ssbClient.publish(content, (err, res) => err ? reject(err) : resolve(res))); + + return { + CATEGORIES, + SEVERITY_THRESHOLDS, + EXPIRES_OPTIONS, + severityOf, + + async createEmergency({ title, text, category, mapUrl, tags, expiresIn }) { + const ssbClient = await openSsb(); + const cleanTitle = safeText(title); + if (!cleanTitle) throw new Error('Title required'); + const now = new Date().toISOString(); + const content = { + type: 'emergency', + title: cleanTitle, + text: safeText(text), + category: normalizeCategory(category), + mapUrl: safeText(mapUrl), + tags: normalizeTags(tags), + status: 'ACTIVE', + expiresAt: expiresAtFrom(expiresIn), + author: ssbClient.id, + createdAt: now, + updatedAt: now + }; + return publish(ssbClient, content); + }, + + async updateEmergency(id, data = {}) { + const { ssbClient, idx } = await load(); + const emergency = find(idx, id); + if (!emergency) throw new Error('Emergency not found'); + if (String(emergency.author) !== String(ssbClient.id)) throw new Error('Only the author can update'); + const editsContent = ['title', 'text', 'category', 'mapUrl', 'tags', 'expiresIn'].some(k => data[k] !== undefined); + if (editsContent && (emergency.confirmationCount || 0) > 0) throw new Error('Cannot edit an emergency after it has been confirmed'); + const content = { + type: 'emergency', + title: data.title !== undefined ? (safeText(data.title) || emergency.title) : emergency.title, + text: data.text !== undefined ? safeText(data.text) : emergency.text, + category: data.category !== undefined ? normalizeCategory(data.category) : emergency.category, + mapUrl: data.mapUrl !== undefined ? safeText(data.mapUrl) : emergency.mapUrl, + tags: data.tags !== undefined ? normalizeTags(data.tags) : emergency.tags, + status: data.status !== undefined ? (normU(data.status) === 'RESOLVED' ? 'RESOLVED' : 'ACTIVE') : (emergency.resolved ? 'RESOLVED' : 'ACTIVE'), + expiresAt: data.expiresIn !== undefined && data.expiresIn !== 'keep' ? expiresAtFrom(data.expiresIn) : emergency.expiresAt, + author: ssbClient.id, + createdAt: emergency.createdAt, + updatedAt: new Date().toISOString(), + replaces: emergency.tipId + }; + const res = await publish(ssbClient, content); + return { key: res.key, rootId: emergency.id }; + }, + + async resolveEmergency(id) { + return this.updateEmergency(id, { status: 'RESOLVED' }); + }, + + async confirmEmergency(id) { + const { ssbClient, idx } = await load(); + const emergency = find(idx, id); + if (!emergency) throw new Error('Emergency not found'); + if (String(emergency.author) === String(ssbClient.id)) throw new Error('Cannot confirm own emergency'); + if (emergency.confirmations.includes(ssbClient.id)) throw new Error('Already confirmed'); + return publish(ssbClient, { type: 'emergencyConfirm', target: emergency.id, createdAt: new Date().toISOString() }); + }, + + async confirmUpdate(updateId) { + const { ssbClient, idx } = await load(); + const update = idx.updatesById.get(idx.updateRootOf(String(updateId || ''))); + if (!update) throw new Error('Update not found'); + if (String(update.author) === String(ssbClient.id)) throw new Error('Cannot confirm own update'); + if (update.confirmations.includes(ssbClient.id)) throw new Error('Already confirmed'); + return publish(ssbClient, { type: 'emergencyConfirm', target: update.id, createdAt: new Date().toISOString() }); + }, + + async editUpdate(updateId, text) { + const { ssbClient, idx } = await load(); + const update = idx.updatesById.get(idx.updateRootOf(String(updateId || ''))); + if (!update) throw new Error('Update not found'); + if (String(update.author) !== String(ssbClient.id)) throw new Error('Only the author can edit an update'); + if (update.confirmationCount > 0) throw new Error('Cannot edit an update after it has been confirmed'); + const clean = safeText(text); + if (!clean) throw new Error('Text required'); + const res = await publish(ssbClient, { type: 'emergencyUpdate', target: update.target, text: clean, createdAt: new Date().toISOString(), replaces: update.tipId }); + return { key: res.key, rootId: update.id, emergencyId: update.target }; + }, + + async deleteUpdate(updateId) { + const { ssbClient, idx } = await load(); + const update = idx.updatesById.get(idx.updateRootOf(String(updateId || ''))); + if (!update) throw new Error('Update not found'); + if (String(update.author) !== String(ssbClient.id)) throw new Error('Only the author can delete an update'); + await publish(ssbClient, { type: 'tombstone', target: update.id, deletedAt: new Date().toISOString(), author: ssbClient.id }); + return { key: update.id, emergencyId: update.target }; + }, + + async addUpdate(id, text) { + const { ssbClient, idx } = await load(); + const emergency = find(idx, id); + if (!emergency) throw new Error('Emergency not found'); + if (String(emergency.author) !== String(ssbClient.id)) throw new Error('Only the author can post updates'); + const clean = safeText(text); + if (!clean) throw new Error('Text required'); + return publish(ssbClient, { type: 'emergencyUpdate', target: emergency.id, text: clean, createdAt: new Date().toISOString() }); + }, + + async deleteEmergency(id) { + const { ssbClient, idx } = await load(); + const emergency = find(idx, id); + if (!emergency) throw new Error('Emergency not found'); + if (String(emergency.author) !== String(ssbClient.id)) throw new Error('Only the author can delete'); + return publish(ssbClient, { type: 'tombstone', target: emergency.id, deletedAt: new Date().toISOString(), author: ssbClient.id }); + }, + + async getEmergencyById(id) { + const { idx } = await load(); + return find(idx, id); + }, + + async listAll({ filter = 'all', q = '', viewerId } = {}) { + const { ssbClient, idx } = await load(); + const me = viewerId || ssbClient.id; + let list = Array.from(idx.emergencies.values()); + const f = normU(filter || 'all'); + if (f === 'MINE') list = list.filter(a => String(a.author) === String(me)); + else if (f === 'RECENT') list = list.filter(a => a.lastActivityTs >= Date.now() - RECENT_MS); + else if (f === 'ACTIVE') list = list.filter(a => a.status === 'ACTIVE'); + else if (f === 'RESOLVED') list = list.filter(a => a.status === 'RESOLVED'); + else if (f === 'EXPIRED') list = list.filter(a => a.status === 'EXPIRED'); + else if (['UNVERIFIED', 'LOW', 'MEDIUM', 'HIGH'].includes(f)) list = list.filter(a => a.severity === f); + else if (CATEGORIES.includes(f)) list = list.filter(a => a.category === f); + const needle = safeText(q).toLowerCase(); + if (needle) list = list.filter(a => a.title.toLowerCase().includes(needle) || a.text.toLowerCase().includes(needle) || a.tags.some(t => t.toLowerCase().includes(needle))); + return list.sort((a, b) => b.lastActivityTs - a.lastActivityTs); + }, + + async featured() { + const { idx } = await load(); + const rank = { HIGH: 3, MEDIUM: 2, LOW: 1, UNVERIFIED: 0 }; + const candidates = Array.from(idx.emergencies.values()).filter(a => a.status === 'ACTIVE' && a.severity === 'HIGH'); + candidates.sort((a, b) => (rank[b.severity] - rank[a.severity]) || (b.confirmationCount - a.confirmationCount) || (b.lastActivityTs - a.lastActivityTs)); + const top = candidates[0]; + return top ? { id: top.id, title: top.title, severity: top.severity, category: top.category, confirmationCount: top.confirmationCount, href: `/emergencies/${encodeURIComponent(top.id)}` } : null; + }, + + async resolveRootId(id) { + const { idx } = await load(); + const emergency = find(idx, id); + if (!emergency) throw new Error('Not found'); + return emergency.id; + } + }; +}; diff --git a/src/models/events_model.js b/src/models/events_model.js index fb6513d..fb95fb2 100644 --- a/src/models/events_model.js +++ b/src/models/events_model.js @@ -1,11 +1,21 @@ const pull = require('../server/node_modules/pull-stream'); const moment = require('../server/node_modules/moment'); +const { normalizeImages, normalizeVideo } = require('./media_gallery'); +const { truthy, hasAnyInterval, nextOccurrence, upcomingOccurrences } = require('./recurrence'); const crypto = require('crypto'); const { buildValidatedTombstoneSet } = require('./tombstone_validator'); const { getConfig } = require('../configs/config-manager.js'); -const { dedupeBy, norm } = require('../backend/dedupe'); +const { dedupeBy, norm, versionGroups } = require('../backend/dedupe'); +const { readTyped, memoIndex } = require('./typed_log'); const logLimit = getConfig().ssbLogStream?.limit || 1000; +const EVENT_TYPES = [ + 'event', 'eventOpinion', 'eventAttend', + 'event-invite', 'event-invite-tombstone', + 'event-open-invite', 'event-open-invite-tombstone', + 'tribe-keys', 'tombstone' +]; + module.exports = ({ cooler, tribeCrypto, eventCrypto, tribesModel }) => { let ssb; const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb; }; @@ -14,10 +24,53 @@ module.exports = ({ cooler, tribeCrypto, eventCrypto, tribesModel }) => { const ownCrypto = eventCrypto || tribeCrypto; const lookupKey = (rid) => (ownCrypto && ownCrypto.getKey(rid)) || (tribeCrypto && tribeCrypto.getKey(rid)) || null; + const lookupGen = (rid) => ((ownCrypto && ownCrypto.getGen(rid)) || (tribeCrypto && tribeCrypto.getGen(rid)) || 0); + const readAll = async (ssbClient) => - new Promise((resolve, reject) => - pull(ssbClient.createLogStream({ limit: logLimit }), pull.collect((err, msgs) => err ? reject(err) : resolve(msgs))) - ); + readTyped(ssbClient, EVENT_TYPES, { limit: logLimit }); + + const rotateEventKey = async (rid, attendees) => { + if (!ownCrypto || !tribeCrypto || !rid || !lookupKey(rid)) return; + const ssbClient = await openSsb(); + const ssbKeys = require("../server/node_modules/ssb-keys"); + const newKey = ownCrypto.generateTribeKey(); + const newGen = ownCrypto.addNewKey(rid, newKey); + const memberKeys = {}; + for (const m of new Set([ssbClient.id, ...(Array.isArray(attendees) ? attendees : [])])) { + try { memberKeys[m] = tribeCrypto.boxKeyForMember(newKey, m, ssbKeys); } catch (_) {} + } + await new Promise((resolve) => { + ssbClient.publish({ type: "tribe-keys", tribeId: rid, generation: newGen, memberKeys }, () => resolve()); + }); + }; + + const ensureAttendeeKeys = async (ssbClient, messages, events) => { + if (!tribeCrypto || !ownCrypto) return; + const leavesByRoot = new Map(); + for (const m of messages) { + const c = m.value && m.value.content; + if (!c || c.type !== 'eventAttend' || c.on !== false || typeof c.target !== 'string') continue; + if (!leavesByRoot.has(c.target)) leavesByRoot.set(c.target, []); + leavesByRoot.get(c.target).push({ member: m.value.author, ts: Number(m.timestamp || m.value.timestamp || 0) }); + } + const ssbKeys = require("../server/node_modules/ssb-keys"); + for (const ev of events) { + if (!ev || !ev.rid || ev.organizer !== ssbClient.id || !lookupKey(ev.rid)) continue; + const members = Array.isArray(ev.attendees) ? ev.attendees : []; + const plan = ownCrypto.keyPlan({ rootId: ev.rid, ownerId: ev.organizer, gen: lookupGen(ev.rid), members, messages, leaves: leavesByRoot.get(ev.rid) || [] }); + if (plan.rotate) { await rotateEventKey(ev.rid, members); continue; } + if (!plan.missing.length) continue; + const key = lookupKey(ev.rid); + const memberKeys = {}; + for (const m of plan.missing) { + try { memberKeys[m] = tribeCrypto.boxKeyForMember(key, m, ssbKeys); } catch (_) {} + } + if (!Object.keys(memberKeys).length) continue; + await new Promise((resolve) => { + ssbClient.publish({ type: "tribe-keys", tribeId: ev.rid, generation: lookupGen(ev.rid) || 1, memberKeys }, () => resolve()); + }); + } + }; const ingestOwnTribeKeys = async () => { if (!ownCrypto) return; @@ -26,9 +79,11 @@ module.exports = ({ cooler, tribeCrypto, eventCrypto, tribesModel }) => { const ssbKeys = require("../server/node_modules/ssb-keys"); const cfg = require("../server/ssb_config"); const msgs = await readAll(ssbClient); + const idx = buildEventIndex(msgs); for (const m of msgs) { const c = m.value && m.value.content; if (!c || c.type !== "tribe-keys") continue; + if (!c.tribeId || (m.value.author !== idx.eventAuthor.get(c.tribeId) && m.value.author !== ssbClient.id)) continue; const memberKeys = c.memberKeys; if (!memberKeys || typeof memberKeys !== "object") continue; const boxed = memberKeys[ssbClient.id]; @@ -72,8 +127,22 @@ module.exports = ({ cooler, tribeCrypto, eventCrypto, tribesModel }) => { return m.toISOString(); }; + const recurrenceOf = (c) => ({ + weekly: truthy(c.intervalWeekly), + monthly: truthy(c.intervalMonthly), + yearly: truthy(c.intervalYearly), + until: c.recurrenceUntil || '' + }); + + const effectiveDate = (c) => { + const r = recurrenceOf(c); + if (!hasAnyInterval(r.weekly, r.monthly, r.yearly) || !r.until) return c.date; + const next = nextOccurrence(c.date, r.until, r.weekly, r.monthly, r.yearly); + return next ? next.toISOString() : c.date; + }; + const deriveStatus = (c) => { - const dateM = moment(c.date); + const dateM = moment(effectiveDate(c)); let status = String(c.status || 'OPEN').toUpperCase(); if (dateM.isValid() && dateM.isBefore(moment())) status = 'CLOSED'; if (status !== 'OPEN' && status !== 'CLOSED') status = 'OPEN'; @@ -82,45 +151,18 @@ module.exports = ({ cooler, tribeCrypto, eventCrypto, tribesModel }) => { const buildEventIndex = (messages) => { const eventAuthor = new Map(); - const eventReplaces = new Map(); + const entries = []; for (const m of messages) { const c = m.value && m.value.content; if (!c || c.type !== 'event') continue; eventAuthor.set(m.key, m.value.author); - if (c.replaces) eventReplaces.set(m.key, c.replaces); + entries.push({ key: m.key, author: m.value.author, seq: m.value.sequence || 0, ts: m.value.timestamp || m.timestamp || 0, replaces: typeof c.replaces === 'string' ? c.replaces : null, claimedAuthor: typeof c.organizer === 'string' ? c.organizer : null }); } - for (const [key, target] of Array.from(eventReplaces.entries())) { - if (!eventAuthor.has(target)) { eventReplaces.delete(key); continue; } - if (eventAuthor.get(target) !== eventAuthor.get(key)) { eventReplaces.delete(key); eventAuthor.delete(key); } - } - const strictNext = new Map(); - for (const [key, target] of eventReplaces) { - if (eventAuthor.get(target) === eventAuthor.get(key)) strictNext.set(target, key); - } - const rootOf = (id) => { - let cur = id; - const seen = new Set(); - while (eventReplaces.has(cur) && eventAuthor.has(eventReplaces.get(cur))) { - if (seen.has(cur)) break; - seen.add(cur); - cur = eventReplaces.get(cur); - } - return cur; - }; - const contentTipOf = (root) => { - let cur = root; - const seen = new Set(); - while (strictNext.has(cur)) { - if (seen.has(cur)) break; - seen.add(cur); - cur = strictNext.get(cur); - } - return cur; - }; - return { eventAuthor, eventReplaces, strictNext, rootOf, contentTipOf }; + const vg = versionGroups(entries); + return { eventAuthor, vg, rootOf: vg.rootOf, contentTipOf: vg.tipOf }; }; - const collectCollab = (messages) => { + const collectCollab = (messages, rootOf = (id) => id) => { const opinions = new Map(); const attend = new Map(); for (const m of messages) { @@ -128,13 +170,14 @@ module.exports = ({ cooler, tribeCrypto, eventCrypto, tribesModel }) => { if (!c || typeof c.target !== 'string') continue; const author = m.value.author; const ts = (m.value && m.value.timestamp) || 0; + const target = rootOf(c.target); if (c.type === 'eventOpinion' && typeof c.category === 'string') { - if (!opinions.has(c.target)) opinions.set(c.target, new Map()); - const byAuthor = opinions.get(c.target); + if (!opinions.has(target)) opinions.set(target, new Map()); + const byAuthor = opinions.get(target); if (!byAuthor.has(author)) byAuthor.set(author, c.category); } else if (c.type === 'eventAttend') { - if (!attend.has(c.target)) attend.set(c.target, new Map()); - const byAuthor = attend.get(c.target); + if (!attend.has(target)) attend.set(target, new Map()); + const byAuthor = attend.get(target); const prev = byAuthor.get(author); if (!prev || ts >= prev.ts) byAuthor.set(author, { on: c.on !== false, ts }); } @@ -171,7 +214,7 @@ module.exports = ({ cooler, tribeCrypto, eventCrypto, tribesModel }) => { return idx.rootOf(id); }, - async createEvent(title, description, date, location, price = 0, url = "", attendees = [], tagsRaw = [], isPublic, mapUrl = "", clearnetPublic = false) { + async createEvent(title, description, date, location, price = 0, url = "", attendees = [], tagsRaw = [], isPublic, mapUrl = "", clearnetPublic = false, media = {}, recurrence = {}) { const ssbClient = await openSsb(); const userId = await me(); @@ -203,6 +246,12 @@ module.exports = ({ cooler, tribeCrypto, eventCrypto, tribesModel }) => { isPublic: visibility, mapUrl: String(mapUrl || "").trim(), clearnetPublic: clearnetPublic === true || clearnetPublic === 'true' || clearnetPublic === 'on', + images: normalizeImages(media && media.images), + video: normalizeVideo(media && media.video), + intervalWeekly: truthy(recurrence && recurrence.weekly), + intervalMonthly: truthy(recurrence && recurrence.monthly), + intervalYearly: truthy(recurrence && recurrence.yearly), + recurrenceUntil: String((recurrence && recurrence.until) || '').trim(), opinions: {}, opinions_inhabitants: [] }; @@ -263,23 +312,28 @@ module.exports = ({ cooler, tribeCrypto, eventCrypto, tribesModel }) => { const ssbClient = await openSsb(); const rid = await this.resolveRootId(eventId).catch(() => eventId); const messages = await readAll(ssbClient); + const authorOf = new Map(); + for (const m of messages) if (m && m.key && m.value) authorOf.set(m.key, m.value.author); + const eventAuthor = authorOf.get(rid); + if (!eventAuthor) return null; const markerTomb = new Set(); const invTomb = new Set(); for (const m of messages) { const c = m.value && m.value.content; - if (!c) continue; - if (c.type === 'event-open-invite-tombstone' && typeof c.target === 'string') markerTomb.add(c.target); - if (c.type === 'event-invite-tombstone' && typeof c.target === 'string') invTomb.add(c.target); + if (!c || typeof c.target !== 'string' || authorOf.get(c.target) !== m.value.author) continue; + if (c.type === 'event-open-invite-tombstone') markerTomb.add(c.target); + if (c.type === 'event-invite-tombstone') invTomb.add(c.target); } let best = null; for (const m of messages) { const c = m.value && m.value.content; if (!c || c.type !== 'event-open-invite' || c.v !== 1) continue; if (c.target !== rid || typeof c.code !== 'string') continue; + if (m.value.author !== eventAuthor) continue; if (markerTomb.has(m.key)) continue; if (c.inviteKey && invTomb.has(c.inviteKey)) continue; const ts = (m.value && m.value.timestamp) || 0; - if (!best || ts > best.ts) best = { code: c.code, by: c.by || m.value.author, markerKey: m.key, inviteKey: c.inviteKey || null, ts }; + if (!best || ts > best.ts) best = { code: c.code, by: m.value.author, markerKey: m.key, inviteKey: c.inviteKey || null, ts }; } return best ? { code: best.code, by: best.by, markerKey: best.markerKey, inviteKey: best.inviteKey } : null; }, @@ -324,6 +378,16 @@ module.exports = ({ cooler, tribeCrypto, eventCrypto, tribesModel }) => { if (rec.by !== userId && organizer !== userId) throw new Error("Not allowed to remove this invitation"); await new Promise((resolve, reject) => ssbClient.publish({ type: 'event-open-invite-tombstone', target: rec.markerKey, ts: new Date().toISOString() }, (err) => err ? reject(err) : resolve())); if (rec.inviteKey) await new Promise((resolve, reject) => ssbClient.publish({ type: 'event-invite-tombstone', target: rec.inviteKey, ts: new Date().toISOString() }, (err) => err ? reject(err) : resolve())); + if (organizer === userId) { + const rid = await this.resolveRootId(eventId); + const messages = await readAll(ssbClient); + const idx = buildEventIndex(messages); + const tip = idx.contentTipOf(rid); + const raw = messages.find(x => x.key === tip); + const rc = raw && raw.value && raw.value.content; + const dec = rc && rc.encryptedPayload ? decryptEventContent(rc, rid) : rc; + if (dec && !dec._undecryptable) await rotateEventKey(rid, aggregateCollab(dec, rid, collectCollab(messages, idx.rootOf)).attendees); + } }, async joinByInvite(code) { @@ -368,14 +432,13 @@ module.exports = ({ cooler, tribeCrypto, eventCrypto, tribesModel }) => { const status = deriveStatus(c); if (status === 'CLOSED') throw new Error("Cannot attend a closed event"); - const collab = collectCollab(messages); + const collab = collectCollab(messages, idx.rootOf); const agg = aggregateCollab(c, rid, collab); const isAttending = agg.attendees.includes(userId); const isLeaving = isAttending; const on = !isAttending; const isPrivate = normalizePrivacy(c.isPublic) === 'private'; - const isOrganizer = c.organizer === userId; const result = await new Promise((resolve, reject) => { ssbClient.publish({ type: 'eventAttend', target: rid, on, createdAt: new Date().toISOString() }, (err2, res2) => err2 ? reject(err2) : resolve(res2)); @@ -397,36 +460,20 @@ module.exports = ({ cooler, tribeCrypto, eventCrypto, tribesModel }) => { } catch (_) {} } - if (isPrivate && isLeaving && !isOrganizer && ownCrypto && tribeCrypto) { - try { - const remaining = agg.attendees.filter(a => a !== userId); - const newKey = ownCrypto.generateTribeKey(); - const newGen = ownCrypto.addNewKey(rid, newKey); - const ssbKeys = require("../server/node_modules/ssb-keys"); - const memberKeys = {}; - for (const m of remaining) { - try { memberKeys[m] = tribeCrypto.boxKeyForMember(newKey, m, ssbKeys); } catch (_) {} - } - if (Object.keys(memberKeys).length) { - await new Promise((resolve) => { - ssbClient.publish({ type: "tribe-keys", tribeId: rid, generation: newGen, memberKeys }, () => resolve()); - }); - } - } catch (_) {} - } - return result; }, async deleteEventById(eventId) { const ssbClient = await openSsb(); const userId = await me(); - const ev = await new Promise((res, rej) => ssbClient.get(eventId, (err, ev) => err || !ev || !ev.content ? rej(new Error("Error retrieving event")) : res(ev))); - const rid = await this.resolveRootId(eventId); + const idx = buildEventIndex(await readAll(ssbClient)); + const rid = idx.rootOf(eventId); + const tipId = idx.contentTipOf(rid); + const ev = await new Promise((res, rej) => ssbClient.get(tipId, (err, ev) => err || !ev || !ev.content ? rej(new Error("Error retrieving event")) : res(ev))); const c = ev.content && ev.content.encryptedPayload ? decryptEventContent(ev.content, rid) : ev.content; if (c && c._undecryptable) throw new Error("Event is encrypted and cannot be decrypted"); - if (c.organizer !== userId) throw new Error("Only the organizer can delete this event"); - const tombstone = { type: 'tombstone', target: eventId, deletedAt: new Date().toISOString(), author: userId }; + if (c.organizer !== userId || ev.author !== userId) throw new Error("Only the organizer can delete this event"); + const tombstone = { type: 'tombstone', target: tipId, deletedAt: new Date().toISOString(), author: userId }; return new Promise((resolve, reject) => { ssbClient.publish(tombstone, (err, res) => err ? reject(err) : resolve(res)); }); @@ -435,15 +482,19 @@ module.exports = ({ cooler, tribeCrypto, eventCrypto, tribesModel }) => { async getEventById(eventId) { const ssbClient = await openSsb(); const messages = await readAll(ssbClient); - const idx = buildEventIndex(messages); + const idx = memoIndex('events', messages, buildEventIndex); const rid = idx.rootOf(eventId); + if (idx.vg.isForeignCopy(rid)) throw new Error("Error retrieving event"); const contentTip = idx.contentTipOf(rid); + const tombstoned = buildValidatedTombstoneSet(messages); + if (tombstoned.has(rid) || tombstoned.has(contentTip)) throw new Error("Error retrieving event"); const msg = await new Promise((res, rej) => ssbClient.get(contentTip, (err, msg) => err || !msg || !msg.content ? rej(new Error("Error retrieving event")) : res(msg))); const c = msg.content && msg.content.encryptedPayload ? decryptEventContent(msg.content, rid) : msg.content; if (c && c._undecryptable) throw new Error("Event is encrypted and cannot be decrypted with available keys"); + if (c && c.organizer && c.organizer !== msg.author) throw new Error("Error retrieving event"); const status = deriveStatus(c); - const agg = aggregateCollab(c, rid, collectCollab(messages)); + const agg = aggregateCollab(c, rid, collectCollab(messages, idx.rootOf)); return { id: contentTip, @@ -461,6 +512,15 @@ module.exports = ({ cooler, tribeCrypto, eventCrypto, tribesModel }) => { status, isPublic: normalizePrivacy(c.isPublic), mapUrl: c.mapUrl || "", + images: normalizeImages(c.images), + video: normalizeVideo(c.video), + intervalWeekly: truthy(c.intervalWeekly), + intervalMonthly: truthy(c.intervalMonthly), + intervalYearly: truthy(c.intervalYearly), + recurrenceUntil: c.recurrenceUntil || '', + recurring: hasAnyInterval(truthy(c.intervalWeekly), truthy(c.intervalMonthly), truthy(c.intervalYearly)) && !!c.recurrenceUntil, + nextDate: effectiveDate(c), + occurrences: upcomingOccurrences(c.date, c.recurrenceUntil, truthy(c.intervalWeekly), truthy(c.intervalMonthly), truthy(c.intervalYearly)).map(d => d.toISOString()), clearnetPublic: !!c.clearnetPublic, encrypted: normalizePrivacy(c.isPublic) === 'private', opinions: agg.opinions, @@ -471,11 +531,13 @@ module.exports = ({ cooler, tribeCrypto, eventCrypto, tribesModel }) => { async updateEventById(eventId, updatedData) { const ssbClient = await openSsb(); const userId = await me(); - const ev = await new Promise((res, rej) => ssbClient.get(eventId, (err, ev) => err || !ev || !ev.content ? rej(new Error("Error retrieving event")) : res(ev))); - const rid = await this.resolveRootId(eventId); + const idx = buildEventIndex(await readAll(ssbClient)); + const rid = idx.rootOf(eventId); + const tipId = idx.contentTipOf(rid); + const ev = await new Promise((res, rej) => ssbClient.get(tipId, (err, ev) => err || !ev || !ev.content ? rej(new Error("Error retrieving event")) : res(ev))); const c = ev.content && ev.content.encryptedPayload ? decryptEventContent(ev.content, rid) : ev.content; if (c && c._undecryptable) throw new Error("Event is encrypted and cannot be decrypted"); - if (c.organizer !== userId) throw new Error("Only the organizer can update this event"); + if (c.organizer !== userId || ev.author !== userId) throw new Error("Only the organizer can update this event"); const status = deriveStatus(c); if (status === 'CLOSED') throw new Error("Cannot edit a closed event"); @@ -503,8 +565,14 @@ module.exports = ({ cooler, tribeCrypto, eventCrypto, tribesModel }) => { isPublic: updatedData.isPublic !== undefined ? normalizePrivacy(updatedData.isPublic) : normalizePrivacy(c.isPublic), clearnetPublic: updatedData.clearnetPublic !== undefined ? (updatedData.clearnetPublic === true || updatedData.clearnetPublic === 'true' || updatedData.clearnetPublic === 'on') : !!c.clearnetPublic, attendees: uniq(Array.isArray(c.attendees) ? c.attendees : []), + images: normalizeImages(updatedData.images !== undefined ? updatedData.images : c.images), + video: updatedData.video !== undefined ? normalizeVideo(updatedData.video) : normalizeVideo(c.video), + intervalWeekly: updatedData.intervalWeekly !== undefined ? truthy(updatedData.intervalWeekly) : truthy(c.intervalWeekly), + intervalMonthly: updatedData.intervalMonthly !== undefined ? truthy(updatedData.intervalMonthly) : truthy(c.intervalMonthly), + intervalYearly: updatedData.intervalYearly !== undefined ? truthy(updatedData.intervalYearly) : truthy(c.intervalYearly), + recurrenceUntil: updatedData.recurrenceUntil !== undefined ? String(updatedData.recurrenceUntil || '').trim() : (c.recurrenceUntil || ''), updatedAt: new Date().toISOString(), - replaces: eventId + replaces: tipId }; const wasPrivate = normalizePrivacy(c.isPublic) === 'private'; @@ -554,7 +622,7 @@ module.exports = ({ cooler, tribeCrypto, eventCrypto, tribesModel }) => { const ev = await new Promise((res, rej) => ssbClient.get(contentTip, (err, m) => (err || !m || !m.content) ? rej(new Error('Event not found')) : res(m))); const c = ev.content && ev.content.encryptedPayload ? decryptEventContent(ev.content, rid) : ev.content; if (c && c._undecryptable) throw new Error('Event is encrypted and cannot be decrypted'); - const agg = aggregateCollab(c, rid, collectCollab(messages)); + const agg = aggregateCollab(c, rid, collectCollab(messages, idx.rootOf)); if (agg.opinions_inhabitants.includes(userId)) throw new Error('Already opined'); return new Promise((res, rej) => ssbClient.publish({ type: 'eventOpinion', target: rid, category, createdAt: new Date().toISOString() }, (err, result) => err ? rej(err) : res(result))); }, @@ -562,71 +630,67 @@ module.exports = ({ cooler, tribeCrypto, eventCrypto, tribesModel }) => { async listAll(author = null, filter = 'all') { const ssbClient = await openSsb(); const userId = await me(); - return new Promise((resolve, reject) => { - pull( - ssbClient.createLogStream({ limit: logLimit }), - pull.collect((err, results) => { - if (err) return reject(new Error("Error listing events: " + err.message)); - const tombstoned = buildValidatedTombstoneSet(results); - const idx = buildEventIndex(results); - const collab = collectCollab(results); + const results = await readAll(ssbClient); + const tombstoned = buildValidatedTombstoneSet(results); + const idx = buildEventIndex(results); + const collab = collectCollab(results, idx.rootOf); - const roots = new Set(); - for (const r of results) { - const rawC = r.value && r.value.content; - if (!rawC || rawC.type !== 'event') continue; - if (!idx.eventAuthor.has(r.key)) continue; - roots.add(idx.rootOf(r.key)); - } + const byKey = new Map(results.map(x => [x.key, x])); + const byRoot = new Map(); + for (const rid of idx.vg.roots) { + if (tombstoned.has(rid)) continue; + const contentTip = idx.contentTipOf(rid); + if (tombstoned.has(contentTip)) continue; + const r = byKey.get(contentTip); + if (!r) continue; + const rawC = r.value && r.value.content; + if (!rawC) continue; + const c = rawC.encryptedPayload ? decryptEventContent(rawC, rid) : rawC; + if (!c || c._undecryptable) continue; + if (author && c.organizer !== author) continue; - const byRoot = new Map(); - for (const rid of roots) { - if (tombstoned.has(rid)) continue; - const contentTip = idx.contentTipOf(rid); - if (tombstoned.has(contentTip)) continue; - const r = results.find(x => x.key === contentTip); - if (!r) continue; - const rawC = r.value && r.value.content; - if (!rawC) continue; - const c = rawC.encryptedPayload ? decryptEventContent(rawC, rid) : rawC; - if (!c || c._undecryptable) continue; - if (author && c.organizer !== author) continue; + const status = deriveStatus(c); + const agg = aggregateCollab(c, rid, collab); - const status = deriveStatus(c); - const agg = aggregateCollab(c, rid, collab); + byRoot.set(rid, { + id: contentTip, + title: c.title || '', + description: c.description || '', + date: c.date || '', + location: c.location || '', + price: c.price || 0, + url: c.url || '', + attendees: agg.attendees, + tags: Array.isArray(c.tags) ? c.tags.filter(Boolean) : [], + createdAt: c.createdAt || new Date().toISOString(), + organizer: c.organizer || '', + status, + isPublic: normalizePrivacy(c.isPublic), + mapUrl: c.mapUrl || "", + images: normalizeImages(c.images), + video: normalizeVideo(c.video), + intervalWeekly: truthy(c.intervalWeekly), + intervalMonthly: truthy(c.intervalMonthly), + intervalYearly: truthy(c.intervalYearly), + recurrenceUntil: c.recurrenceUntil || '', + recurring: hasAnyInterval(truthy(c.intervalWeekly), truthy(c.intervalMonthly), truthy(c.intervalYearly)) && !!c.recurrenceUntil, + nextDate: effectiveDate(c), + encrypted: normalizePrivacy(c.isPublic) === 'private', + opinions: agg.opinions, + opinions_inhabitants: agg.opinions_inhabitants + }); + } - byRoot.set(rid, { - id: contentTip, - title: c.title || '', - description: c.description || '', - date: c.date || '', - location: c.location || '', - price: c.price || 0, - url: c.url || '', - attendees: agg.attendees, - tags: Array.isArray(c.tags) ? c.tags.filter(Boolean) : [], - createdAt: c.createdAt || new Date().toISOString(), - organizer: c.organizer || '', - status, - isPublic: normalizePrivacy(c.isPublic), - mapUrl: c.mapUrl || "", - encrypted: normalizePrivacy(c.isPublic) === 'private', - opinions: agg.opinions, - opinions_inhabitants: agg.opinions_inhabitants - }); - } + try { await ensureAttendeeKeys(ssbClient, results, [...byRoot].filter(([rid, e]) => e.encrypted && idx.eventAuthor.get(rid) === userId).map(([rid, e]) => ({ rid, organizer: userId, attendees: e.attendees }))); } catch (_) {} - let out = Array.from(byRoot.values()); - out = dedupeBy(out, e => e.title ? [norm(e.organizer), norm(e.title), norm(e.date)].join('|') : null); + let out = Array.from(byRoot.values()); + out = dedupeBy(out, e => e.title ? [norm(e.organizer), norm(e.title), norm(e.date)].join('|') : null); - if (filter === 'mine') out = out.filter(e => e.organizer === userId); - if (filter === 'open') out = out.filter(e => String(e.status).toUpperCase() === 'OPEN'); - if (filter === 'closed') out = out.filter(e => String(e.status).toUpperCase() === 'CLOSED'); + if (filter === 'mine') out = out.filter(e => e.organizer === userId); + if (filter === 'open') out = out.filter(e => String(e.status).toUpperCase() === 'OPEN'); + if (filter === 'closed') out = out.filter(e => String(e.status).toUpperCase() === 'CLOSED'); - resolve(out); - }) - ); - }); + return out; } }; }; diff --git a/src/models/exportmode_model.js b/src/models/exportmode_model.js index cd535d5..d3d31ba 100644 --- a/src/models/exportmode_model.js +++ b/src/models/exportmode_model.js @@ -1,15 +1,13 @@ const os = require('os'); const fs = require('fs'); const path = require('path'); -const archiver = require('../server/node_modules/archiver'); module.exports = { exportSSB: async (outputPath) => { try { - const homeDir = os.homedir(); - const ssbPath = path.join(homeDir, '.ssb'); - const output = fs.createWriteStream(outputPath); - const archive = archiver('zip', { + const ssbPath = require('../configs/state-manager').ssbDir(); + const output = fs.createWriteStream(outputPath, { mode: 0o600 }); + const archive = require('../server/node_modules/archiver')('zip', { zlib: { level: 9 } }); archive.pipe(output); @@ -22,7 +20,8 @@ module.exports = { const filePath = path.join(dirPath, file); const stat = fs.statSync(filePath); - if (file === 'secret') { + const rel = path.relative(ssbPath, filePath); + if (file === 'secret' || file.startsWith('secret.') || file === 'oasis-config.json' || rel === path.join('oasis', 'keys') || rel.startsWith(path.join('oasis', 'keys') + path.sep)) { return; } diff --git a/src/models/favorites_model.js b/src/models/favorites_model.js index d0fd3f5..43d8334 100644 --- a/src/models/favorites_model.js +++ b/src/models/favorites_model.js @@ -1,4 +1,4 @@ -const mediaFavorites = require("../backend/media-favorites"); +const contentFavorites = require("../backend/content_favorites"); const safeArr = (v) => (Array.isArray(v) ? v : []); const safeText = (v) => String(v || "").trim(); @@ -15,7 +15,7 @@ const toTs = (d) => { return Number.isFinite(t) ? t : 0; }; -module.exports = ({ audiosModel, bookmarksModel, documentsModel, imagesModel, videosModel, mapsModel, padsModel, chatsModel, calendarsModel, torrentsModel, marketModel, shopsModel }) => { +module.exports = ({ audiosModel, bookmarksModel, documentsModel, imagesModel, videosModel, mapsModel, padsModel, roomsModel, chatsModel, calendarsModel, torrentsModel, filesModel, marketModel, shopsModel, eventsModel, tasksModel, reportsModel, votesModel, jobsModel, housingModel, projectsModel, transfersModel, forumModel, blogsModel, pollsModel, schoolModel, wikiModel, emergenciesModel, mailingModel, logisticsModel, podcastsModel, campaignsModel }) => { const kindConfig = { audios: { base: "/audios/", @@ -45,6 +45,34 @@ module.exports = ({ audiosModel, bookmarksModel, documentsModel, imagesModel, vi base: "/pads/", getById: getFn(padsModel, ["getPadById", "getById"]) }, + rooms: { + base: "/rooms/", + getById: getFn(roomsModel, ["getRoomById"]) + }, + wiki: { + base: "/wiki/", + getById: getFn(wikiModel, ["getPage"]) + }, + emergencies: { + base: "/emergencies/", + getById: getFn(emergenciesModel, ["getEmergencyById"]) + }, + mailing: { + base: "/mailing/", + getById: getFn(mailingModel, ["getListById"]) + }, + logistics: { + base: "/logistics/", + getById: getFn(logisticsModel, ["getRouteById"]) + }, + podcasts: { + base: "/podcasts/", + getById: getFn(podcastsModel, ["getAnyById"]) + }, + campaigns: { + base: "/campaigns/", + getById: getFn(campaignsModel, ["getCampaignById"]) + }, chats: { base: "/chats/", getById: getFn(chatsModel, ["getChatById", "getById"]) @@ -57,6 +85,10 @@ module.exports = ({ audiosModel, bookmarksModel, documentsModel, imagesModel, vi base: "/torrents/", getById: getFn(torrentsModel, ["getTorrentById", "getById"]) }, + files: { + base: "/files/", + getById: getFn(filesModel, ["getFileById", "getById"]) + }, market: { base: "/market/", getById: getFn(marketModel, ["getItemById", "getById"]) @@ -64,10 +96,62 @@ module.exports = ({ audiosModel, bookmarksModel, documentsModel, imagesModel, vi shopProducts: { base: "/shops/product/", getById: getFn(shopsModel, ["getProductById"]) + }, + events: { + base: "/events/", + getById: getFn(eventsModel, ["getEventById", "getById"]) + }, + tasks: { + base: "/tasks/", + getById: getFn(tasksModel, ["getTaskById", "getById"]) + }, + reports: { + base: "/reports/", + getById: getFn(reportsModel, ["getReportById", "getById"]) + }, + school: { + base: "/school/course/", + getById: getFn(schoolModel, ["getCourseById", "getById"]) + }, + votes: { + base: "/votes/", + getById: getFn(votesModel, ["getVoteById", "getById"]) + }, + jobs: { + base: "/jobs/", + getById: getFn(jobsModel, ["getJobById", "getById"]) + }, + housing: { + base: "/housing/", + getById: getFn(housingModel, ["getHousingById", "getById"]) + }, + projects: { + base: "/projects/", + getById: getFn(projectsModel, ["getProjectById", "getById"]) + }, + transfers: { + base: "/transfers/", + getById: getFn(transfersModel, ["getTransferById", "getById"]) + }, + forum: { + base: "/forum/", + getById: getFn(forumModel, ["getForumById", "getById"]) + }, + blogs: { + base: "/blogs/", + getById: getFn(blogsModel, ["getBlogById", "getById"]) + }, + polls: { + base: "/polls/", + getById: getFn(pollsModel, ["getPollById", "getById"]) + }, + shops: { + base: "/shops/", + getById: getFn(shopsModel, ["getShopById", "getById"]) } }; - const kindOrder = ["audios", "bookmarks", "calendars", "chats", "documents", "images", "maps", "pads", "torrents", "videos", "market", "shopProducts"]; + const kindOrder = ["audios", "blogs", "bookmarks", "calendars", "campaigns", "chats", "documents", "events", "files", "forum", "housing", "images", "jobs", "logistics", "mailing", "maps", "market", "pads", "podcasts", "polls", "projects", "reports", "rooms", "school", "shopProducts", "shops", "emergencies", "tasks", "torrents", "transfers", "videos", "votes", "wiki"]; const hydrateKind = async (kind, ids) => { const cfg = kindConfig[kind]; @@ -79,9 +163,11 @@ module.exports = ({ audiosModel, bookmarksModel, documentsModel, imagesModel, vi if (!id) return null; try { const obj = await cfg.getById(id); + if (!obj || typeof obj !== "object") return null; const viewId = safeText(obj?.key || obj?.id || id); return { + content: obj, kind, favId: id, viewHref: `${cfg.base}${encodeURIComponent(viewId)}`, @@ -104,7 +190,7 @@ module.exports = ({ audiosModel, bookmarksModel, documentsModel, imagesModel, vi }; const loadAll = async () => { - const sets = await Promise.all(kindOrder.map((k) => mediaFavorites.getFavoriteSet(k))); + const sets = await Promise.all(kindOrder.map((k) => contentFavorites.getFavoriteSet(k))); const idsByKind = {}; kindOrder.forEach((k, i) => { idsByKind[k] = Array.from(sets[i] || []); @@ -118,26 +204,15 @@ module.exports = ({ audiosModel, bookmarksModel, documentsModel, imagesModel, vi const flat = kindOrder.flatMap((k) => byKind[k]); - const counts = { - audios: byKind.audios.length, - bookmarks: byKind.bookmarks.length, - calendars: byKind.calendars.length, - chats: byKind.chats.length, - documents: byKind.documents.length, - images: byKind.images.length, - maps: byKind.maps.length, - pads: byKind.pads.length, - torrents: byKind.torrents.length, - videos: byKind.videos.length, - market: byKind.market.length, - shopProducts: byKind.shopProducts.length, - all: flat.length - }; + const counts = { all: flat.length }; + for (const k of kindOrder) counts[k] = (byKind[k] || []).length; const recentFlat = flat - .slice() + .filter((x) => (toTs(x.updatedAt) || toTs(x.createdAt) || 0) >= Date.now() - 86400000) .sort((a, b) => (toTs(b.updatedAt) || toTs(b.createdAt)) - (toTs(a.updatedAt) || toTs(a.createdAt))); + counts.recent = recentFlat.length; + return { byKind, flat, recentFlat, counts }; }; @@ -167,11 +242,13 @@ module.exports = ({ audiosModel, bookmarksModel, documentsModel, imagesModel, vi return { items: grouped, counts }; }, + kinds: kindOrder.slice(), + async removeFavorite(kind, id) { const k = safeText(kind); const favId = safeText(id); if (!k || !favId) return; - await mediaFavorites.removeFavorite(k, favId); + await contentFavorites.removeFavorite(k, favId); } }; }; diff --git a/src/models/fediverse_model.js b/src/models/fediverse_model.js index a8e1e0d..a61bee1 100644 --- a/src/models/fediverse_model.js +++ b/src/models/fediverse_model.js @@ -1,8 +1,9 @@ const fs = require('fs'); const path = require('path'); +const { Readable } = require('stream'); const { getConfig, saveConfig } = require('../configs/config-manager.js'); -const ACCOUNTS_PATH = path.join(__dirname, '..', 'configs', 'fediverse-accounts.json'); +const ACCOUNTS_PATH = require('../configs/state-manager').statePath('fediverse-accounts.json'); const FETCH_TIMEOUT_MS = 8000; const TIMELINE_CACHE_MS = 60 * 1000; const TIMELINE_LIMIT = 40; @@ -18,9 +19,10 @@ const spoofHeaders = (accept) => ({ }); const isPrivateHost = (hostname) => { - const h = String(hostname || '').toLowerCase(); + let h = String(hostname || '').toLowerCase().trim().replace(/^\[|\]$/g, ''); if (!h) return true; - if (h === 'localhost' || h.endsWith('.localhost') || h.endsWith('.local')) return true; + if (h.startsWith('::ffff:')) h = h.slice(7); + if (h === 'localhost' || h.endsWith('.localhost') || h.endsWith('.local') || h === '0.0.0.0' || h === '::') return true; if (h === '::1' || h.startsWith('fe80:') || h.startsWith('fc') || h.startsWith('fd')) return true; const m = h.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/); if (m) { @@ -34,6 +36,89 @@ const isPrivateHost = (hostname) => { return false; }; + +const TG_TIMEOUT_MS = 20000; +const TG_MEDIA_MAX_BYTES = 25 * 1024 * 1024; +const PROXY_MEDIA_MAX_BYTES = TG_MEDIA_MAX_BYTES; +const networkPaused = () => { + if (process.env.OASIS_NETWORK_PAUSED === '1') return true; + try { return require('../configs/config-manager.js').getConfig().networkPaused === true; } catch (_) { return true; } +}; +const netFetch = (url, opts) => networkPaused() ? Promise.reject(new Error('Network is paused')) : fetch(url, opts); +const publicOnlyLookup = (hostname, options, callback) => { + require('dns').lookup(hostname, { ...(options || {}), all: true }, (err, addresses) => { + if (err) return callback(err); + const list = Array.isArray(addresses) ? addresses : []; + if (!list.length || list.some(a => isPrivateHost(a.address))) return callback(Object.assign(new Error('Refusing a private address'), { code: 'EPRIVATE' })); + if (options && options.all) return callback(null, list); + callback(null, list[0].address, list[0].family); + }); +}; +let publicClient; +const publicFetch = (url, opts) => { + if (networkPaused()) return Promise.reject(new Error('Network is paused')); + if (publicClient === undefined) { + try { + const { Agent, fetch: clientFetch } = require('../server/node_modules/undici'); + publicClient = { fetch: clientFetch, dispatcher: new Agent({ connect: { lookup: publicOnlyLookup } }) }; + } catch (_) { publicClient = null; } + } + return publicClient ? publicClient.fetch(url, { ...opts, dispatcher: publicClient.dispatcher }) : fetch(url, opts); +}; +const readCapped = async (res, max) => { + if (Number(res.headers.get('content-length') || 0) > max) return null; + if (!res.body || typeof res.body.getReader !== 'function') { + const ab = await res.arrayBuffer(); + return ab.byteLength > max ? null : Buffer.from(ab); + } + const reader = res.body.getReader(); + const parts = []; + let total = 0; + for (;;) { + const { done, value } = await reader.read(); + if (done) break; + total += value.length; + if (total > max) { try { await reader.cancel(); } catch (_) {} return null; } + parts.push(Buffer.from(value)); + } + return Buffer.concat(parts); +}; +const TG_MEDIA_CACHE_MAX = 60; +const TG_LOGIN_TTL_MS = 10 * 60 * 1000; +let gram = null; +const loadGram = () => { + if (gram) return gram; + const { TelegramClient, Api } = require('../server/node_modules/telegram'); + const { StringSession } = require('../server/node_modules/telegram/sessions'); + const { Logger, LogLevel } = require('../server/node_modules/telegram/extensions/Logger'); + gram = { TelegramClient, Api, StringSession, Logger, LogLevel }; + return gram; +}; +const withTimeout = (promise, ms = TG_TIMEOUT_MS) => new Promise((resolve, reject) => { + const timer = setTimeout(() => reject(new Error('telegramErrTimeout')), ms); + Promise.resolve(promise).then((v) => { clearTimeout(timer); resolve(v); }, (e) => { clearTimeout(timer); reject(e); }); +}); +const tgDisplayName = (u) => { + if (!u) return ''; + if (u.title) return String(u.title); + const full = [u.firstName, u.lastName].filter(Boolean).join(' ').trim(); + return full || (u.username ? `@${u.username}` : ''); +}; +const tgErrCode = (err) => { + if (err && err.message === 'peerErrPaused') return 'peerErrPaused'; + const m = String((err && (err.errorMessage || err.message)) || '').toUpperCase(); + if (m.includes('PHONE_CODE_INVALID') || m.includes('PHONE_CODE_EMPTY')) return 'telegramErrCode'; + if (m.includes('PHONE_CODE_EXPIRED')) return 'telegramErrCodeExpired'; + if (m.includes('PASSWORD_HASH_INVALID')) return 'telegramErrPassword'; + if (m.includes('PHONE_NUMBER_INVALID') || m.includes('PHONE_NUMBER_BANNED')) return 'telegramErrPhone'; + if (m.includes('API_ID_INVALID') || m.includes('API_ID_PUBLISHED_FLOOD')) return 'telegramErrApi'; + if (m.includes('FLOOD')) return 'telegramErrFlood'; + if (m.includes('TELEGRAMERRTIMEOUT')) return 'telegramErrTimeout'; + if (m.includes('AUTH_KEY') || m.includes('SESSION_REVOKED') || m.includes('USER_DEACTIVATED') || m.includes('AUTH_USER_CANCEL')) return 'telegramErrAuth'; + return 'telegramErrConnect'; +}; +const tgHardError = (code) => code === 'telegramErrPhone' || code === 'telegramErrApi' || code === 'telegramErrFlood' || code === 'telegramErrAuth' || code === 'telegramErrConnect' || code === 'telegramErrTimeout'; + module.exports = ({ isPublic } = {}) => { const cache = new Map(); let mediaHosts = new Set(); @@ -82,7 +167,7 @@ module.exports = ({ isPublic } = {}) => { const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS); try { const headers = Object.assign(spoofHeaders('application/json'), { Authorization: `Bearer ${token}` }, opts.headers || {}); - const res = await fetch(`${instance}${pathName}`, { ...opts, headers, signal: controller.signal }); + const res = await netFetch(`${instance}${pathName}`, { ...opts, headers, signal: controller.signal }); return res; } finally { clearTimeout(timer); @@ -287,8 +372,15 @@ module.exports = ({ isPublic } = {}) => { if (!file || !file.filepath) return null; let buf; try { buf = fs.readFileSync(file.filepath); } catch (_) { throw new Error('fediverseErrMedia'); } + return api.uploadMediaBuffer({ buffer: buf, mimetype: file.mimetype, filename: file.originalFilename }); + }, + + async uploadMediaBuffer({ buffer, mimetype, filename } = {}) { + const m = getMastodon(); + if (!m) throw new Error('fediverseErrAuth'); + if (!buffer || !buffer.length) return null; const fd = new FormData(); - fd.append('file', new Blob([buf], { type: file.mimetype || 'application/octet-stream' }), file.originalFilename || 'upload'); + fd.append('file', new Blob([buffer], { type: mimetype || 'application/octet-stream' }), filename || 'upload'); let res; try { res = await apiFetch(m.instance, m.token, '/api/v2/media', { method: 'POST', body: fd }); @@ -361,6 +453,656 @@ module.exports = ({ isPublic } = {}) => { favourite(id) { return this.action(id, 'favourite'); }, unfavourite(id) { return this.action(id, 'unfavourite'); }, + telegram: (() => { + const getTg = () => readStore().telegram || null; + let client = null; + let pending = null; + const entities = new Map(); + const messages = new Map(); + const mediaCache = new Map(); + const avatarCache = new Map(); + const dialogCache = { ts: 0, value: null }; + + const killClient = async (c) => { + if (!c) return; + try { await withTimeout(c.destroy(), 8000); } catch (_) { try { await c.disconnect(); } catch (__) {} } + }; + const newClient = (sessionStr, apiId, apiHash) => { + const { TelegramClient, StringSession, Logger, LogLevel } = loadGram(); + return new TelegramClient(new StringSession(sessionStr || ''), Number(apiId), String(apiHash), { + connectionRetries: 3, + baseLogger: new Logger(LogLevel.NONE) + }); + }; + const getClient = async () => { + const t = getTg(); + if (!t || !t.session) throw new Error('telegramErrAuth'); + if (networkPaused()) { + const c = client; + client = null; + await killClient(c); + throw new Error('peerErrPaused'); + } + if (client && client.connected) return client; + if (!client) client = newClient(t.session, t.apiId, t.apiHash); + await withTimeout(client.connect()); + return client; + }; + const rememberDialogs = (list) => { + for (const d of list) if (d && d.entity) entities.set(String(d.id), d.entity); + }; + const resolveEntity = async (c, id) => { + const key = String(id); + if (entities.has(key)) return entities.get(key); + try { rememberDialogs(await withTimeout(c.getDialogs({ limit: TIMELINE_LIMIT }))); } catch (_) {} + if (entities.has(key)) return entities.get(key); + const ent = await withTimeout(c.getEntity(Number(key))); + entities.set(key, ent); + return ent; + }; + const peerKind = (d) => (d.isUser ? 'user' : d.isChannel ? 'channel' : 'group'); + const kindOfEntity = (ent) => { + const { Api } = loadGram(); + if (ent instanceof Api.User) return 'user'; + if (ent instanceof Api.Channel) return ent.megagroup ? 'group' : 'channel'; + return 'group'; + }; + const mediaInfo = (msg) => { + const { Api } = loadGram(); + const m = msg && msg.media; + if (!m) return null; + if (m instanceof Api.MessageMediaPhoto) return { kind: 'photo', mime: 'image/jpeg', size: 0, name: '' }; + if (m instanceof Api.MessageMediaDocument && m.document && m.document.mimeType !== undefined) { + const mime = String(m.document.mimeType || 'application/octet-stream'); + const size = Number(m.document.size || 0); + const kind = mime.startsWith('image/') ? 'photo' : mime.startsWith('video/') ? 'video' : mime.startsWith('audio/') ? 'audio' : 'file'; + let name = ''; + for (const attr of (m.document.attributes || [])) if (attr && attr.fileName) name = String(attr.fileName); + return { kind, mime, size, name }; + } + return { kind: 'other', mime: '', size: 0, name: '' }; + }; + const toIso = (unix) => (unix ? new Date(Number(unix) * 1000).toISOString() : ''); + const mapMessage = (msg, chatId, senderNames) => { + const info = mediaInfo(msg); + const sid = msg.senderId ? String(msg.senderId) : ''; + return { + id: Number(msg.id), + chatId: String(chatId), + text: String(msg.message || ''), + date: toIso(msg.date), + out: msg.out === true, + senderId: sid, + senderName: senderNames.get(sid) || '', + media: info && info.kind !== 'other' && info.size <= TG_MEDIA_MAX_BYTES + ? { ...info, url: `/fediverse/telegram/media/${encodeURIComponent(String(chatId))}/${encodeURIComponent(String(msg.id))}` } + : null + }; + }; + const trimCache = (map) => { + while (map.size > TG_MEDIA_CACHE_MAX) map.delete(map.keys().next().value); + }; + const dropPending = async () => { + if (pending && pending.client && pending.client !== client) await killClient(pending.client); + pending = null; + }; + const waitStep = async (p, prevStep, ms = 25000) => { + const started = Date.now(); + while (Date.now() - started < ms) { + if (p.done || p.error || p.step !== prevStep || p.awaiting) return; + await sleep(200); + } + }; + + return { + hasAccount() { return !!(getTg() && getTg().session); }, + getAccount() { + const t = getTg(); + if (!t || !t.session) return null; + return { + network: 'telegram', + username: t.username || '', + displayName: t.displayName || t.username || t.phone || '', + phone: t.phone || '', + avatar: '/fediverse/telegram/avatar/me' + }; + }, + async getAccountStats() { + if (!this.hasAccount()) return null; + const data = await this.getDialogs(); + if (!data || data.error) return null; + return { + chats: data.dialogs.length, + unread: data.dialogs.reduce((sum, d) => sum + (Number(d.unread) || 0), 0) + }; + }, + loginState() { + if (!pending) return null; + if (Date.now() - pending.startedAt > TG_LOGIN_TTL_MS && !pending.done) { dropPending(); return null; } + return { step: pending.step, error: pending.error || '', done: pending.done === true }; + }, + async beginLogin({ apiId, apiHash, phone }) { + if (isPublic) throw new Error('fediverseErrPublic'); + const id = Number(String(apiId || '').trim()); + const hash = String(apiHash || '').trim(); + const ph = String(phone || '').trim(); + if (!id || !hash || !ph) throw new Error('telegramErrMissing'); + await dropPending(); + if (networkPaused()) throw new Error('peerErrPaused'); + const c = newClient('', id, hash); + try { await withTimeout(c.connect()); } catch (err) { throw new Error(tgErrCode(err)); } + const p = { client: c, apiId: id, apiHash: hash, phone: ph, step: 'sending', error: '', done: false, awaiting: false, startedAt: Date.now(), resolveCode: null, resolvePassword: null }; + pending = p; + p.promise = c.start({ + phoneNumber: async () => ph, + phoneCode: async () => new Promise((resolve) => { p.step = 'code'; p.awaiting = true; p.resolveCode = resolve; }), + password: async () => new Promise((resolve) => { p.step = 'password'; p.awaiting = true; p.resolvePassword = resolve; }), + onError: (err) => { + const code = tgErrCode(err); + p.error = code; + return tgHardError(code); + } + }).then(async () => { + const me = await withTimeout(c.getMe()); + const store = readStore(); + store.telegram = { + apiId: id, + apiHash: hash, + phone: ph, + session: c.session.save(), + userId: String(me.id || ''), + username: me.username || '', + displayName: tgDisplayName(me) + }; + writeStore(store); + if (client && client !== c) await killClient(client); + client = c; + entities.clear(); dialogCache.value = null; + p.done = true; p.step = 'done'; p.error = ''; + pending = null; + setModuleFlag('on'); + }).catch(async (err) => { + if (!p.error) p.error = tgErrCode(err); + p.done = true; p.step = 'error'; + await killClient(c); + }); + await waitStep(p, 'sending', 12000); + return this.loginState(); + }, + async submitCode(code) { + const p = pending; + if (!p || p.done || typeof p.resolveCode !== 'function') throw new Error('telegramErrNoLogin'); + const value = String(code || '').replace(/\s+/g, ''); + if (!value) throw new Error('telegramErrCode'); + const resolve = p.resolveCode; + p.resolveCode = null; p.awaiting = false; p.error = ''; + resolve(value); + await waitStep(p, 'code'); + return this.loginState(); + }, + async submitPassword(password) { + const p = pending; + if (!p || p.done || typeof p.resolvePassword !== 'function') throw new Error('telegramErrNoLogin'); + const value = String(password || ''); + if (!value) throw new Error('telegramErrPassword'); + const resolve = p.resolvePassword; + p.resolvePassword = null; p.awaiting = false; p.error = ''; + resolve(value); + await waitStep(p, 'password'); + return this.loginState(); + }, + async cancelLogin() { await dropPending(); }, + async goOffline() { + await dropPending(); + const c = client; + client = null; + await killClient(c); + }, + async disconnect() { + const { Api } = loadGram(); + try { + const c = await getClient(); + await withTimeout(c.invoke(new Api.auth.LogOut()), 8000); + } catch (_) {} + await killClient(client); + client = null; + const store = readStore(); + delete store.telegram; + writeStore(store); + entities.clear(); messages.clear(); mediaCache.clear(); avatarCache.clear(); dialogCache.value = null; + return true; + }, + invalidateCache() { dialogCache.value = null; }, + async getDialogs(force = false) { + if (!this.hasAccount()) return { connected: false, dialogs: [] }; + const now = Date.now(); + if (!force && dialogCache.value && now - dialogCache.ts < TIMELINE_CACHE_MS) return dialogCache.value; + let c; + try { c = await getClient(); } catch (err) { return { connected: true, account: this.getAccount(), dialogs: [], error: tgErrCode(err) }; } + let list; + try { list = await withTimeout(c.getDialogs({ limit: TIMELINE_LIMIT })); } catch (err) { return { connected: true, account: this.getAccount(), dialogs: [], error: tgErrCode(err) }; } + rememberDialogs(list); + const dialogs = list.map((d) => { + const id = String(d.id); + const last = d.message || null; + return { + id, + title: String(d.title || d.name || id), + kind: peerKind(d), + unread: Number(d.unreadCount || 0), + date: toIso(d.date || (last && last.date)), + last: last ? { text: String(last.message || ''), out: last.out === true, hasMedia: !!last.media } : null, + avatar: `/fediverse/telegram/avatar/${encodeURIComponent(id)}` + }; + }); + const value = { connected: true, account: this.getAccount(), dialogs }; + dialogCache.ts = now; dialogCache.value = value; + return value; + }, + async getChat(id) { + const c = await getClient(); + const entity = await resolveEntity(c, id); + const list = await withTimeout(c.getMessages(entity, { limit: TIMELINE_LIMIT })); + const senderNames = new Map(); + for (const msg of list) { + const sid = msg.senderId ? String(msg.senderId) : ''; + if (!sid || senderNames.has(sid)) continue; + try { senderNames.set(sid, tgDisplayName(await withTimeout(msg.getSender(), 8000))); } catch (_) { senderNames.set(sid, ''); } + } + for (const msg of list) messages.set(`${id}:${msg.id}`, msg); + trimCache(messages); + try { await withTimeout(c.markAsRead(entity), 8000); } catch (_) {} + dialogCache.value = null; + return { + id: String(id), + title: tgDisplayName(entity) || String(id), + kind: kindOfEntity(entity), + messages: list.slice().reverse().map((msg) => mapMessage(msg, id, senderNames)) + }; + }, + async sendMessage(id, { text, file } = {}) { + const c = await getClient(); + const entity = await resolveEntity(c, id); + const msg = String(text || '').trim(); + if (file && file.filepath) { + await withTimeout(c.sendFile(entity, { file: file.filepath, caption: msg, forceDocument: false }), 90000); + } else { + if (!msg) throw new Error('fediverseErrEmpty'); + await withTimeout(c.sendMessage(entity, { message: msg })); + } + dialogCache.value = null; + return true; + }, + async getMedia(chatId, msgId) { + const key = `${chatId}:${msgId}`; + if (mediaCache.has(key)) return mediaCache.get(key); + const c = await getClient(); + let msg = messages.get(key) || null; + if (!msg) { + const entity = await resolveEntity(c, chatId); + const arr = await withTimeout(c.getMessages(entity, { ids: [Number(msgId)] })); + msg = arr && arr[0] ? arr[0] : null; + } + if (!msg || !msg.media) return null; + const info = mediaInfo(msg); + if (!info || info.kind === 'other' || info.size > TG_MEDIA_MAX_BYTES) return null; + const buf = await withTimeout(c.downloadMedia(msg, {}), 90000); + if (!buf || !buf.length) return null; + const out = { contentType: info.mime || 'application/octet-stream', buffer: Buffer.from(buf), name: info.name }; + mediaCache.set(key, out); + trimCache(mediaCache); + return out; + }, + async getAvatar(id) { + const key = String(id); + if (avatarCache.has(key)) return avatarCache.get(key); + const c = await getClient(); + const entity = key === 'me' ? 'me' : await resolveEntity(c, key); + let buf = null; + try { buf = await withTimeout(c.downloadProfilePhoto(entity, { isBig: false }), 15000); } catch (_) { buf = null; } + const out = buf && buf.length ? { contentType: 'image/jpeg', buffer: Buffer.from(buf) } : null; + avatarCache.set(key, out); + trimCache(avatarCache); + return out; + } + }; + })(), + + + peertube: (() => { + const getPt = () => readStore().peertube || null; + const feedCache = new Map(); + const PT_UPLOAD_TIMEOUT_MS = 15 * 60 * 1000; + const PT_PRIVACY = { public: 1, unlisted: 2, private: 3 }; + + const fetchTimed = async (url, opts = {}, timeoutMs = FETCH_TIMEOUT_MS) => { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), timeoutMs); + try { return await netFetch(url, { ...opts, signal: controller.signal }); } finally { clearTimeout(timer); } + }; + const oauthClient = async (instance) => { + let res; + try { res = await fetchTimed(`${instance}/api/v1/oauth-clients/local`, { headers: spoofHeaders('application/json') }); } catch (_) { throw new Error('peertubeErrConnect'); } + if (!res.ok) throw new Error('peertubeErrConnect'); + const data = await res.json().catch(() => null); + if (!data || !data.client_id || !data.client_secret) throw new Error('peertubeErrConnect'); + return data; + }; + const tokenRequest = async (instance, params) => { + let res; + try { + res = await fetchTimed(`${instance}/api/v1/users/token`, { + method: 'POST', + headers: { ...spoofHeaders('application/json'), 'Content-Type': 'application/x-www-form-urlencoded' }, + body: new URLSearchParams(params).toString() + }); + } catch (_) { throw new Error('peertubeErrConnect'); } + if (res.status === 400 || res.status === 401 || res.status === 403) throw new Error('peertubeErrAuth'); + if (!res.ok) throw new Error('peertubeErrConnect'); + const data = await res.json().catch(() => null); + if (!data || !data.access_token) throw new Error('peertubeErrAuth'); + return data; + }; + const saveTokens = (pt, data) => { + const store = readStore(); + if (!store.peertube) return null; + store.peertube = { ...store.peertube, token: data.access_token, refreshToken: data.refresh_token || pt.refreshToken || '', expiresAt: Date.now() + (Number(data.expires_in) || 0) * 1000 }; + writeStore(store); + return store.peertube; + }; + const refreshAccess = async (pt) => { + if (!pt || !pt.refreshToken) return null; + try { + const data = await tokenRequest(pt.instance, { client_id: pt.clientId, client_secret: pt.clientSecret, grant_type: 'refresh_token', refresh_token: pt.refreshToken }); + return saveTokens(pt, data); + } catch (_) { return null; } + }; + const ptFetch = async (pathName, opts = {}, retry = true) => { + let pt = getPt(); + if (!pt) throw new Error('peertubeErrAuth'); + if (pt.expiresAt && Date.now() > pt.expiresAt - 60000 && retry) { const fresh = await refreshAccess(pt); if (fresh) pt = fresh; } + const headers = Object.assign(spoofHeaders('application/json'), { Authorization: `Bearer ${pt.token}` }, opts.headers || {}); + let res; + try { res = await fetchTimed(`${pt.instance}${pathName}`, { method: opts.method || 'GET', body: opts.body, headers }, opts.timeoutMs || FETCH_TIMEOUT_MS); } catch (_) { throw new Error('peertubeErrConnect'); } + if (res.status === 401 && retry) { + const fresh = await refreshAccess(pt); + if (fresh) return ptFetch(pathName, opts, false); + } + return res; + }; + const absUrl = (pt, p) => { + if (typeof p !== 'string' || !p) return ''; + if (/^https?:\/\//i.test(p)) return p; + return `${pt.instance}${p.startsWith('/') ? '' : '/'}${p}`; + }; + const rememberHost = (url) => { try { mediaHosts.add(new URL(url).host); } catch (_) {} }; + const avatarOf = (pt, obj) => { + const list = Array.isArray(obj && obj.avatars) ? obj.avatars.slice().sort((a, b) => (Number(b.width) || 0) - (Number(a.width) || 0)) : []; + const p = list.length ? list[0].path : (obj && obj.avatar && obj.avatar.path) || ''; + return p ? proxify(absUrl(pt, p)) : ''; + }; + const channelOf = (pt, ch) => { + if (!ch || typeof ch !== 'object') return { name: '', displayName: '', host: '', handle: '', avatar: '', url: '' }; + const host = ch.host || (() => { try { return new URL(pt.instance).host; } catch (_) { return ''; } })(); + return { name: ch.name || '', displayName: ch.displayName || ch.name || '', host, handle: ch.name ? `${ch.name}@${host}` : '', avatar: avatarOf(pt, ch), url: ch.url || (ch.name ? `${pt.instance}/c/${ch.name}` : '') }; + }; + const mapVideo = (pt, v) => { + if (!v || typeof v !== 'object') return null; + const id = String(v.uuid || v.shortUUID || v.id || ''); + return { + id, + numericId: v.id, + name: typeof v.name === 'string' ? v.name : '', + description: typeof v.description === 'string' ? v.description : '', + thumbnail: proxify(absUrl(pt, v.thumbnailPath || v.previewPath || '')), + duration: Number(v.duration) || 0, + views: Number(v.views) || 0, + likes: Number(v.likes) || 0, + dislikes: Number(v.dislikes) || 0, + publishedAt: v.publishedAt || v.createdAt || '', + channel: channelOf(pt, v.channel), + url: typeof v.url === 'string' && v.url ? v.url : `${pt.instance}/w/${id}`, + privacy: v.privacy && v.privacy.label ? String(v.privacy.label) : '', + privacyId: v.privacy && v.privacy.id ? Number(v.privacy.id) : 0, + state: v.state && v.state.label ? String(v.state.label) : '', + isLive: v.isLive === true, + nsfw: v.nsfw === true + }; + }; + const pickFile = (v) => { + const candidates = []; + for (const f of (Array.isArray(v.files) ? v.files : [])) if (f && f.fileUrl) candidates.push(f); + for (const pl of (Array.isArray(v.streamingPlaylists) ? v.streamingPlaylists : [])) for (const f of (Array.isArray(pl.files) ? pl.files : [])) if (f && f.fileUrl) candidates.push(f); + if (!candidates.length) return null; + const resOf = (f) => Number(f.resolution && f.resolution.id) || 0; + const under = candidates.filter(f => resOf(f) > 0 && resOf(f) <= 720).sort((a, b) => resOf(b) - resOf(a)); + const pick = under[0] || candidates.slice().sort((a, b) => resOf(a) - resOf(b))[0]; + return { url: pick.fileUrl, resolution: resOf(pick), size: Number(pick.size) || 0 }; + }; + const mapComment = (pt, c) => c && typeof c === 'object' ? { + id: String(c.id || ''), + html: typeof c.text === 'string' ? c.text : '', + createdAt: c.createdAt || '', + totalReplies: Number(c.totalReplies) || 0, + account: c.account ? { name: c.account.name || '', displayName: c.account.displayName || c.account.name || '', host: c.account.host || '', avatar: avatarOf(pt, c.account) } : { name: '', displayName: '', host: '', avatar: '' } + } : null; + const listOf = async (pathName) => { + const res = await ptFetch(pathName); + if (res.status === 401 || res.status === 403) return { error: 'peertubeErrAuth' }; + if (!res.ok) return { error: 'peertubeErrFetch' }; + const data = await res.json().catch(() => null); + return { total: Number(data && data.total) || 0, data: Array.isArray(data && data.data) ? data.data : [] }; + }; + const invalidatePt = () => feedCache.clear(); + + return { + hasAccount() { return !!(getPt() && getPt().token); }, + getAccount() { + const pt = getPt(); + if (!pt || !pt.token) return null; + let host = ''; + try { host = new URL(pt.instance).host; } catch (_) {} + return { + network: 'peertube', + instance: pt.instance, + host, + username: pt.username || '', + displayName: pt.displayName || pt.username || '', + handle: pt.username ? `${pt.username}@${host}` : host, + avatar: pt.avatar ? proxify(pt.avatar) : '', + channel: pt.channelName || '', + channelDisplayName: pt.channelDisplayName || pt.channelName || '', + channelUrl: pt.channelName ? `${pt.instance}/c/${pt.channelName}` : pt.instance, + profileUrl: pt.username ? `${pt.instance}/a/${pt.username}` : pt.instance + }; + }, + async connect({ instance, username, password }) { + if (isPublic) throw new Error('fediverseErrPublic'); + const origin = (() => { try { return normalizeInstance(instance); } catch (_) { throw new Error('peertubeErrInstance'); } })(); + const user = String(username || '').trim(); + const pass = String(password || ''); + if (!user || !pass) throw new Error('peertubeErrMissing'); + const client = await oauthClient(origin); + const tokens = await tokenRequest(origin, { client_id: client.client_id, client_secret: client.client_secret, grant_type: 'password', response_type: 'code', username: user, password: pass }); + let me; + try { + const res = await fetchTimed(`${origin}/api/v1/users/me`, { headers: Object.assign(spoofHeaders('application/json'), { Authorization: `Bearer ${tokens.access_token}` }) }); + if (!res.ok) throw new Error('peertubeErrAuth'); + me = await res.json(); + } catch (_) { throw new Error('peertubeErrConnect'); } + const channel = Array.isArray(me.videoChannels) && me.videoChannels.length ? me.videoChannels[0] : null; + const acc = me.account || {}; + const avatars = Array.isArray(acc.avatars) ? acc.avatars.slice().sort((a, b) => (Number(b.width) || 0) - (Number(a.width) || 0)) : []; + const avatarPath = avatars.length ? avatars[0].path : (acc.avatar && acc.avatar.path) || ''; + const store = readStore(); + store.peertube = { + instance: origin, + clientId: client.client_id, + clientSecret: client.client_secret, + token: tokens.access_token, + refreshToken: tokens.refresh_token || '', + expiresAt: Date.now() + (Number(tokens.expires_in) || 0) * 1000, + userId: String(me.id || ''), + username: me.username || user, + displayName: acc.displayName || me.username || user, + avatar: avatarPath ? `${origin}${avatarPath.startsWith('/') ? '' : '/'}${avatarPath}` : '', + accountName: acc.name || me.username || user, + channelId: channel ? String(channel.id) : '', + channelName: channel ? channel.name || '' : '', + channelDisplayName: channel ? channel.displayName || channel.name || '' : '' + }; + writeStore(store); + invalidatePt(); + try { mediaHosts.add(new URL(origin).host); } catch (_) {} + setModuleFlag('on'); + return this.getAccount(); + }, + async disconnect() { + const pt = getPt(); + if (pt && pt.token) { + try { await fetchTimed(`${pt.instance}/api/v1/users/revoke-token`, { method: 'POST', headers: Object.assign(spoofHeaders('application/json'), { Authorization: `Bearer ${pt.token}` }) }); } catch (_) {} + } + const store = readStore(); + delete store.peertube; + writeStore(store); + invalidatePt(); + return true; + }, + invalidateCache() { invalidatePt(); }, + async getAccountStats() { + const pt = getPt(); + if (!pt) return null; + let me = null; + try { const res = await ptFetch('/api/v1/users/me'); if (res.ok) me = await res.json().catch(() => null); } catch (_) {} + if (!me) return null; + const channel = Array.isArray(me.videoChannels) && me.videoChannels.length ? me.videoChannels[0] : null; + let videos = 0, subscriptions = 0; + try { const v = await listOf('/api/v1/users/me/videos?start=0&count=1'); if (!v.error) videos = v.total; } catch (_) {} + try { const sub = await listOf('/api/v1/users/me/subscriptions?start=0&count=1'); if (!sub.error) subscriptions = sub.total; } catch (_) {} + return { + videos, + subscriptions, + followers: channel ? Number(channel.followersCount) || 0 : 0, + quota: Number(me.videoQuota) || 0, + quotaUsed: Number(me.videoQuotaUsed) || 0 + }; + }, + async getFeed(kind = 'subscriptions', force = false) { + const pt = getPt(); + if (!pt) return { connected: false, videos: [] }; + try { mediaHosts.add(new URL(pt.instance).host); } catch (_) {} + const key = kind === 'mine' ? 'mine' : 'subscriptions'; + const now = Date.now(); + const hit = feedCache.get(key); + if (!force && hit && now - hit.ts < TIMELINE_CACHE_MS) return hit.value; + let result; + try { + result = await listOf(key === 'mine' ? `/api/v1/users/me/videos?start=0&count=${TIMELINE_LIMIT}&sort=-publishedAt` : `/api/v1/users/me/subscriptions/videos?start=0&count=${TIMELINE_LIMIT}&sort=-publishedAt`); + } catch (err) { + return { connected: true, account: this.getAccount(), videos: [], error: (err && err.message) || 'peertubeErrFetch' }; + } + if (result.error) return { connected: true, account: this.getAccount(), videos: [], error: result.error }; + const value = { connected: true, account: this.getAccount(), videos: result.data.map(v => mapVideo(pt, v)).filter(Boolean), total: result.total, kind: key }; + feedCache.set(key, { ts: now, value }); + return value; + }, + async getVideo(id) { + const pt = getPt(); + if (!pt) return null; + const vid = encodeURIComponent(String(id || '')); + let res; + try { res = await ptFetch(`/api/v1/videos/${vid}`); } catch (_) { return { error: 'peertubeErrFetch' }; } + if (!res.ok) return { error: 'peertubeErrFetch' }; + const raw = await res.json().catch(() => null); + const video = mapVideo(pt, raw); + if (!video) return { error: 'peertubeErrFetch' }; + const file = pickFile(raw || {}); + if (file) { rememberHost(file.url); video.stream = `/fediverse/peertube/stream?u=${encodeURIComponent(file.url)}`; video.resolution = file.resolution; video.fileSize = file.size; } + video.embedUrl = raw && raw.embedPath ? absUrl(pt, raw.embedPath) : ''; + video.tags = Array.isArray(raw && raw.tags) ? raw.tags.map(String) : []; + let comments = []; + try { + const c = await listOf(`/api/v1/videos/${vid}/comment-threads?start=0&count=${TIMELINE_LIMIT}&sort=-createdAt`); + if (!c.error) comments = c.data.map(x => mapComment(pt, x)).filter(Boolean); + } catch (_) {} + let myRating = 'none'; + try { const r = await ptFetch(`/api/v1/users/me/videos/${vid}/rating`); if (r.ok) { const d = await r.json().catch(() => null); if (d && d.rating) myRating = String(d.rating); } } catch (_) {} + return { video, comments, myRating }; + }, + async rate(id, rating) { + const allowed = { like: 1, dislike: 1, none: 1 }; + if (!allowed[rating]) throw new Error('peertubeErrFetch'); + let res; + try { res = await ptFetch(`/api/v1/videos/${encodeURIComponent(String(id || ''))}/rate`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ rating }) }); } catch (_) { throw new Error('peertubeErrConnect'); } + if (res.status === 401 || res.status === 403) throw new Error('peertubeErrAuth'); + if (!res.ok && res.status !== 204) throw new Error('peertubeErrFetch'); + invalidatePt(); + return true; + }, + async comment(id, text) { + const body = String(text || '').trim(); + if (!body) throw new Error('fediverseErrEmpty'); + let res; + try { res = await ptFetch(`/api/v1/videos/${encodeURIComponent(String(id || ''))}/comment-threads`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: body }) }); } catch (_) { throw new Error('peertubeErrConnect'); } + if (res.status === 401 || res.status === 403) throw new Error('peertubeErrAuth'); + if (!res.ok) throw new Error('peertubeErrComment'); + return true; + }, + async upload({ file, name, description, privacy } = {}) { + const pt = getPt(); + if (!pt) throw new Error('peertubeErrAuth'); + if (!file || !file.filepath || !(Number(file.size) > 0)) throw new Error('peertubeErrNoFile'); + const title = String(name || '').trim(); + if (!title) throw new Error('peertubeErrTitle'); + let buf; + try { buf = fs.readFileSync(file.filepath); } catch (_) { throw new Error('peertubeErrUpload'); } + const fd = new FormData(); + fd.append('videofile', new Blob([buf], { type: file.mimetype || 'video/mp4' }), file.originalFilename || 'video.mp4'); + if (pt.channelId) fd.append('channelId', String(pt.channelId)); + fd.append('name', title.slice(0, 120)); + fd.append('privacy', String(PT_PRIVACY[String(privacy || 'public')] || 1)); + const desc = String(description || '').trim(); + if (desc) fd.append('description', desc.slice(0, 10000)); + let res; + try { res = await ptFetch('/api/v1/videos/upload', { method: 'POST', body: fd, timeoutMs: PT_UPLOAD_TIMEOUT_MS }); } catch (_) { throw new Error('peertubeErrUpload'); } + if (res.status === 401 || res.status === 403) throw new Error('peertubeErrAuth'); + if (!res.ok) throw new Error('peertubeErrUpload'); + const data = await res.json().catch(() => ({})); + invalidatePt(); + const v = data && data.video ? data.video : {}; + return { id: String(v.uuid || v.shortUUID || v.id || '') }; + }, + async openStream(remoteUrl, range) { + let u; + try { u = new URL(String(remoteUrl || '')); } catch (_) { return null; } + if (u.protocol !== 'https:' || isPrivateHost(u.hostname) || !api.isHostAllowed(u.host)) return null; + const headers = spoofHeaders('*/*'); + if (range) headers.Range = String(range); + let res = null; + let current = u; + for (let hop = 0; hop < 5; hop++) { + try { res = await publicFetch(current.href, { headers, redirect: 'manual' }); } catch (_) { return null; } + if (![301, 302, 303, 307, 308].includes(res.status)) break; + let next; + try { next = new URL(res.headers.get('location') || '', current); } catch (_) { return null; } + if (next.protocol !== 'https:' || isPrivateHost(next.hostname)) return null; + current = next; + res = null; + } + if (!res || (!res.ok && res.status !== 206)) return null; + const type = res.headers.get('content-type') || 'application/octet-stream'; + if (!/^(video|audio|application\/octet-stream)/i.test(type)) return null; + return { + status: res.status, + type, + length: res.headers.get('content-length') || '', + contentRange: res.headers.get('content-range') || '', + body: res.body ? Readable.fromWeb(res.body) : null + }; + } + }; + })(), + isHostAllowed(host) { if (mediaHosts.has(host)) return true; const m = getMastodon(); @@ -368,6 +1110,10 @@ module.exports = ({ isPublic } = {}) => { try { if (new URL(m.instance).host === host) return true; } catch (_) {} try { if (m.avatar && new URL(m.avatar).host === host) return true; } catch (_) {} } + const pt = readStore().peertube; + if (pt && pt.instance) { + try { if (new URL(pt.instance).host === host) return true; } catch (_) {} + } return false; }, @@ -382,7 +1128,7 @@ module.exports = ({ isPublic } = {}) => { try { let current = u, res = null; for (let hop = 0; hop < 5; hop++) { - res = await fetch(current.href, { signal: controller.signal, redirect: 'manual', headers: spoofHeaders('image/avif,image/webp,*/*') }); + res = await publicFetch(current.href, { signal: controller.signal, redirect: 'manual', headers: spoofHeaders('image/avif,image/webp,*/*') }); if (res.status < 300 || res.status >= 400) break; const loc = res.headers.get('location'); if (!loc) return null; @@ -395,8 +1141,8 @@ module.exports = ({ isPublic } = {}) => { if (!res || !res.ok) return null; const ct = res.headers.get('content-type') || 'application/octet-stream'; if (!/^(image|video|audio)\//i.test(ct)) return null; - const ab = await res.arrayBuffer(); - return { contentType: ct, buffer: Buffer.from(ab) }; + const buffer = await readCapped(res, PROXY_MEDIA_MAX_BYTES); + return buffer ? { contentType: ct, buffer } : null; } catch (_) { return null; } finally { @@ -407,3 +1153,5 @@ module.exports = ({ isPublic } = {}) => { return api; }; + +module.exports.isPrivateHost = isPrivateHost; diff --git a/src/models/feed_model.js b/src/models/feed_model.js index 80f894b..e37d5c9 100644 --- a/src/models/feed_model.js +++ b/src/models/feed_model.js @@ -2,7 +2,8 @@ const pull = require("../server/node_modules/pull-stream"); const { getConfig } = require("../configs/config-manager.js"); const categories = require("../backend/opinion_categories"); const { buildValidatedTombstoneSet } = require('./tombstone_validator'); -const { dedupeBy, mergeDuplicatesBy, norm } = require('../backend/dedupe'); +const { readTyped } = require('./typed_log'); +const { dedupeBy, mergeDuplicatesBy, norm, versionGroups, versionEntry } = require('../backend/dedupe'); const logLimit = getConfig().ssbLogStream?.limit || 1000; const FEED_TEXT_MIN = Number(getConfig().feed?.minLength ?? 1); @@ -15,10 +16,10 @@ module.exports = ({ cooler }) => { return ssb; }; - const cleanText = (t) => (typeof t === "string" ? t.trim() : ""); + const cleanText = (t) => (typeof t === "string" ? t.replace(/\r\n?/g, "\n").trim() : ""); const isValidFeedText = (t) => { - const s = cleanText(t); + const s = cleanText(t).replace(/!?\[[^\]\n]*\]\(\s*&[^)\s]+\.sha256\s*\)/g, "").trim(); return s.length >= FEED_TEXT_MIN && s.length <= FEED_TEXT_MAX; }; @@ -27,10 +28,9 @@ module.exports = ({ cooler }) => { ssbClient.get(id, (err, val) => (err ? reject(err) : resolve({ key: id, value: val }))); }); - const getAllMessages = (ssbClient) => - new Promise((resolve, reject) => { - pull(ssbClient.createLogStream({ limit: logLimit }), pull.collect((err, msgs) => (err ? reject(err) : resolve(msgs)))); - }); + const FEED_TYPES = ["feed", "feedOpinion", "feed-action", "tombstone"]; + + const getAllMessages = async (ssbClient) => readTyped(ssbClient, FEED_TYPES, { limit: logLimit }); const extractTags = (text) => { const list = (String(text || "").match(/#[A-Za-z0-9_]{1,32}/g) || []).map((t) => t.slice(1).toLowerCase()); @@ -40,11 +40,8 @@ module.exports = ({ cooler }) => { const buildIndex = async (ssbClient) => { const messages = await getAllMessages(ssbClient); - const forward = new Map(); - const replacedIds = new Set(); const tombstoned = buildValidatedTombstoneSet(messages); const feedsById = new Map(); - const authorById = new Map(); const actions = []; const opinionMsgs = []; @@ -59,7 +56,6 @@ module.exports = ({ cooler }) => { } if (c.type === "feed") { feedsById.set(k, msg); - authorById.set(k, msg?.value?.author); continue; } if (c.type === "feed-action") { @@ -68,24 +64,8 @@ module.exports = ({ cooler }) => { } } - for (const [k, msg] of feedsById) { - const t = msg?.value?.content?.replaces; - if (!t) continue; - if (authorById.get(t) === authorById.get(k)) { - forward.set(t, k); - replacedIds.add(t); - } - } - - const resolve = (id) => { - let cur = id; - const seen = new Set(); - while (forward.has(cur) && !seen.has(cur)) { - seen.add(cur); - cur = forward.get(cur); - } - return cur; - }; + const vg = versionGroups(Array.from(feedsById.values()).map(versionEntry)); + const resolve = (id) => vg.tipOf(id); const actionsByRoot = new Map(); for (const a of actions) { @@ -105,7 +85,7 @@ module.exports = ({ cooler }) => { opinionsByTip.get(tip).push(op); } - return { resolve, tombstoned, feedsById, replacedIds, actionsByRoot, opinionsByTip }; + return { resolve, vg, tombstoned, feedsById, actionsByRoot, opinionsByTip }; }; const resolveCurrentId = async (id) => { @@ -114,7 +94,7 @@ module.exports = ({ cooler }) => { return idx.resolve(id); }; - const createFeed = async (text, mentions) => { + const createFeed = async (text, mentions, media = null) => { const ssbClient = await openSsb(); const userId = ssbClient.id; @@ -129,7 +109,7 @@ module.exports = ({ cooler }) => { const content = { type: "feed", - text: cleaned, + text: typeof media === "string" && media.trim() ? `${cleaned}\n${media.trim()}` : cleaned, author: userId, createdAt: new Date().toISOString(), tags: extractTags(cleaned), @@ -146,6 +126,7 @@ module.exports = ({ cooler }) => { const userId = ssbClient.id; const idx = await buildIndex(ssbClient); + if (idx.vg.isForeignCopy(idx.vg.rootOf(contentId))) throw new Error("Invalid feed"); const tipId = idx.resolve(contentId); let msg; @@ -216,6 +197,7 @@ module.exports = ({ cooler }) => { const userId = ssbClient.id; const idx = await buildIndex(ssbClient); + if (idx.vg.isForeignCopy(idx.vg.rootOf(contentId))) throw new Error("Invalid feed"); const tipId = idx.resolve(contentId); const msg = idx.feedsById.get(tipId); @@ -249,30 +231,22 @@ module.exports = ({ cooler }) => { return !!c && c.type === "feed" && isValidFeedText(c.text); }; - let tips = Array.from(idx.feedsById.values()).filter( + let tips = Array.from(idx.vg.roots).map((r) => idx.feedsById.get(idx.resolve(r))).filter( (m) => - !idx.replacedIds.has(m.key) && + !!m && !idx.tombstoned.has(m.key) && isValidFeedMsg(m) ); const textEditedEver = (m) => { - const seen = new Set(); - let cur = m; - let lastText = cur?.value?.content?.text; - while (cur?.value?.content?.replaces) { - const prevId = cur.value.content.replaces; - if (!prevId || seen.has(prevId)) break; - seen.add(prevId); - const prev = idx.feedsById.get(prevId); - if (!prev) break; - if (prev?.value?.author !== cur?.value?.author) break; - const prevText = prev?.value?.content?.text; - if (typeof lastText === "string" && typeof prevText === "string" && lastText !== prevText) return true; - cur = prev; - lastText = prevText; + const root = idx.vg.rootOf(m.key); + const owner = idx.feedsById.get(root)?.value?.author; + const texts = new Set(); + for (const k of (idx.vg.groups.get(root) || [])) { + const v = idx.feedsById.get(k)?.value; + if (v && v.author === owner && typeof v.content?.text === "string") texts.add(v.content.text); } - return false; + return texts.size > 1; }; const materialize = (feedMsg) => { @@ -300,7 +274,7 @@ module.exports = ({ cooler }) => { } if (ac.action === "comment") { - commentCount++; + if (!idx.tombstoned.has(a.key)) commentCount++; continue; } } @@ -367,7 +341,7 @@ module.exports = ({ cooler }) => { if (filter === "MINE") { feeds = feeds.filter((m) => (m.value?.author || m.value?.content?.author) === userId); - } else if (filter === "TODAY") { + } else if (filter === "RECENT" || filter === "TODAY") { feeds = feeds.filter((m) => now - getTs(m) < 86400000); } @@ -388,7 +362,7 @@ module.exports = ({ cooler }) => { const ssbClient = await openSsb(); const idx = await buildIndex(ssbClient); const currentId = idx.resolve(feedId); - if (idx.tombstoned.has(currentId)) return null; + if (idx.tombstoned.has(currentId) || idx.vg.isForeignCopy(idx.vg.rootOf(currentId))) return null; const msg = idx.feedsById.get(currentId); if (!msg) return null; const actions = idx.actionsByRoot.get(currentId) || []; @@ -402,7 +376,7 @@ module.exports = ({ cooler }) => { refeeds++; if (ac.author || a?.value?.author) refeedsInhabitants.push(ac.author || a.value.author); } - if (ac.type === "feed-action" && ac.action === "comment") { + if (ac.type === "feed-action" && ac.action === "comment" && !idx.tombstoned.has(a.key)) { commentCount++; } } @@ -417,7 +391,7 @@ module.exports = ({ cooler }) => { const currentId = idx.resolve(feedId); const actions = idx.actionsByRoot.get(currentId) || []; return actions - .filter(a => a?.value?.content?.type === "feed-action" && a?.value?.content?.action === "comment") + .filter(a => a?.value?.content?.type === "feed-action" && a?.value?.content?.action === "comment" && !idx.tombstoned.has(a.key)) .sort((a, b) => (a?.value?.timestamp || 0) - (b?.value?.timestamp || 0)); }; @@ -430,6 +404,15 @@ module.exports = ({ cooler }) => { }); }; - return { createFeed, createRefeed, addOpinion, listFeeds, resolveCurrentId, getFeedById, getComments, addComment }; + const deleteFeedById = async (id) => { + const ssbClient = await openSsb(); + const userId = ssbClient.id; + const currentId = (await resolveCurrentId(id)) || id; + const item = await new Promise((res, rej) => ssbClient.get(currentId, (e, m) => (e || !m || !m.content) ? rej(new Error("Feed not found")) : res(m))); + if (item.author !== userId && item.content?.author !== userId) throw new Error("Not the author"); + return new Promise((res, rej) => ssbClient.publish({ type: "tombstone", target: currentId, deletedAt: new Date().toISOString(), author: userId }, (e, m) => e ? rej(e) : res(m))); + }; + + return { createFeed, createRefeed, addOpinion, listFeeds, resolveCurrentId, getFeedById, getComments, addComment, deleteFeedById }; }; diff --git a/src/models/files_model.js b/src/models/files_model.js new file mode 100644 index 0000000..94acd6a --- /dev/null +++ b/src/models/files_model.js @@ -0,0 +1,387 @@ +const pull = require("../server/node_modules/pull-stream"); +const { getConfig } = require("../configs/config-manager.js"); +const categories = require("../backend/opinion_categories"); +const { readTyped, memoIndex } = require("./typed_log"); + +const { dedupeBy, norm, versionGroups } = require('../backend/dedupe'); +const logLimit = getConfig().ssbLogStream?.limit || 1000; + +const safeArr = (v) => (Array.isArray(v) ? v : []); + +const normalizeTags = (raw) => { + if (raw === undefined || raw === null) return undefined; + if (Array.isArray(raw)) return raw.map((t) => String(t || "").trim()).filter(Boolean); + return String(raw).split(",").map((t) => t.trim()).filter(Boolean); +}; + +const fileExtra = (extra) => { + if (!extra || typeof extra !== "object") return {}; + const out = {}; + if (extra.mime) out.mime = String(extra.mime); + if (extra.fileName) out.fileName = String(extra.fileName); + if (typeof extra.torrentKey === "string" && extra.torrentKey.startsWith("%")) out.torrentKey = extra.torrentKey; + if (typeof extra.torrentUrl === "string" && extra.torrentUrl.startsWith("&")) out.torrentUrl = extra.torrentUrl; + if (extra.cipher && typeof extra.cipher === "object" && extra.cipher.key && extra.cipher.manifestBlobId) out.cipher = { v: Number(extra.cipher.v) || 1, key: String(extra.cipher.key), manifestBlobId: String(extra.cipher.manifestBlobId), chunkCount: Number(extra.cipher.chunkCount) || 0 }; + return out; +}; + +const parseBlobId = (blobMarkdown) => { + const s = String(blobMarkdown || ""); + const match = s.match(/\((&[^)]+\.sha256)\)/); + if (match) return match[1]; + const fallback = s.match(/\(([^)]+)\)/g); + return fallback ? fallback[fallback.length - 1].slice(1, -1) : s || null; +}; + +const voteSum = (opinions = {}) => + Object.values(opinions || {}).reduce((s, n) => s + (Number(n) || 0), 0); + +module.exports = ({ cooler, tribeCrypto, tribesModel }) => { + let ssb; + + const openSsb = async () => { + if (!ssb) ssb = await cooler.open(); + return ssb; + }; + + const tribeHelpers = tribeCrypto ? tribeCrypto.createHelpers(tribesModel) : null; + const encryptIfTribe = tribeHelpers ? tribeHelpers.encryptIfTribe : async (c) => c; + const decryptIfTribe = tribeHelpers ? tribeHelpers.decryptIfTribe : async (c) => c; + const assertReadable = tribeHelpers ? tribeHelpers.assertReadable : () => {}; + const decryptIndexNodes = tribeHelpers ? tribeHelpers.decryptIndexNodes : async () => {}; + const unwrapForIndex = (msgs) => tribeHelpers ? tribeHelpers.unwrapMessagesForKind(msgs, 'file') : msgs; + const tombFor = async (target, tribeId, author) => tribeHelpers ? tribeHelpers.encryptTombstone(target, tribeId, author) : { type: 'tombstone', target, deletedAt: new Date().toISOString(), author }; + + const FILE_TYPES = ["file", "fileOpinion", "tombstone"]; + + const getAllMessages = async (ssbClient) => readTyped(ssbClient, FILE_TYPES, { limit: logLimit, withWindow: true }); + + const getMsg = async (ssbClient, key) => + new Promise((resolve) => { + ssbClient.get(key, (err, msg) => (err ? resolve(null) : resolve(msg))); + }); + + const buildIndex = (messages) => { + const tomb = new Set(); + const nodes = new Map(); + const authorByKey = new Map(); + const tombRequests = []; + const opinionMsgs = []; + + for (const m of messages) { + const k = m.key; + const v = m.value || {}; + const c = v.content; + if (!c) continue; + + if (c.type === "tombstone" && c.target) { + tombRequests.push({ target: c.target, author: v.author }); + continue; + } + + if (c.type === "fileOpinion") { opinionMsgs.push({ target: c.target, author: v.author, category: c.category }); continue; } + + if (c.type !== "file") continue; + + const ts = v.timestamp || m.timestamp || 0; + let sizeBytes = 0; + try { sizeBytes = Buffer.byteLength(JSON.stringify(v), "utf8"); } catch (_) { sizeBytes = 0; } + nodes.set(k, { key: k, ts, seq: v.sequence || 0, c, sizeBytes, author: v.author }); + authorByKey.set(k, v.author); + } + + const vg = versionGroups([...nodes.values()].map(n => ({ key: n.key, author: n.author, seq: n.seq, ts: n.ts, replaces: n.c.replaces || null, claimedAuthor: typeof n.c.author === "string" ? n.c.author : null }))); + const rootOf = vg.rootOf; + const contentTipOf = vg.tipOf; + + for (const t of tombRequests) { + const targetAuthor = authorByKey.get(t.target); + if (targetAuthor && t.author === targetAuthor) tomb.add(t.target); + } + + const opinionsByRoot = new Map(); + for (const op of opinionMsgs) { if (!nodes.has(op.target)) continue; const r = rootOf(op.target); if (!opinionsByRoot.has(r)) opinionsByRoot.set(r, []); opinionsByRoot.get(r).push(op); } + + const tipByRoot = new Map(); + for (const r of vg.roots) tipByRoot.set(r, contentTipOf(r)); + + const resolveGroup = (root) => { + const contentTip = contentTipOf(root); + const contentNode = nodes.get(contentTip) || nodes.get(root); + const lc = contentNode ? contentNode.c : {}; + const opinions = { ...(lc.opinions || {}) }; + const voters = safeArr(lc.opinions_inhabitants).slice(); + const voterSet = new Set(voters); + for (const op of (opinionsByRoot.get(root) || [])) { + if (voterSet.has(op.author)) continue; + voterSet.add(op.author); voters.push(op.author); + opinions[op.category] = (opinions[op.category] || 0) + 1; + } + return { contentTip, contentNode, opinions, voters }; + }; + + return { tomb, nodes, rootOf, tipByRoot, resolveGroup }; + }; + + const buildFile = (node, rootId, viewerId, agg) => { + const c = node.c || {}; + const undec = c.encryptedPayload && c._decrypted === false; + const opinions = agg ? agg.opinions : (c.opinions || {}); + const voters = agg ? agg.voters : safeArr(c.opinions_inhabitants); + return { + key: node.key, + rootId, + url: undec ? "" : c.url, + createdAt: c.createdAt || new Date(node.ts).toISOString(), + updatedAt: c.updatedAt || null, + tags: safeArr(c.tags), + author: node.author, + title: undec ? "" : (c.title || ""), + description: undec ? "" : (c.description || ""), + size: c.size || 0, + mime: undec ? "" : (c.mime || ""), + fileName: undec ? "" : (c.fileName || ""), + torrentKey: undec ? "" : (typeof c.torrentKey === "string" && c.torrentKey.startsWith("%") ? c.torrentKey : ""), + torrentUrl: undec ? "" : (typeof c.torrentUrl === "string" && c.torrentUrl.startsWith("&") ? c.torrentUrl : ""), + cipher: undec ? null : (c.cipher && typeof c.cipher === "object" && typeof c.cipher.key === "string" && typeof c.cipher.manifestBlobId === "string" ? { v: Number(c.cipher.v) || 1, key: c.cipher.key, manifestBlobId: c.cipher.manifestBlobId, chunkCount: Number(c.cipher.chunkCount) || 0 } : null), + opinions, + opinions_inhabitants: voters, + hasVoted: viewerId ? voters.includes(viewerId) : false, + tribeId: c.tribeId || null, + encrypted: !!undec, + sizeBytes: node.sizeBytes || 0 + }; + }; + + + return { + type: "file", + + async resolveCurrentId(id) { + const ssbClient = await openSsb(); + const messages = await getAllMessages(ssbClient); + const idx = buildIndex(unwrapForIndex(messages)); + + const tip = idx.tipByRoot.get(idx.rootOf(id)) || id; + if (idx.tomb.has(tip)) throw new Error("File not found"); + return tip; + }, + + async resolveRootId(id) { + const ssbClient = await openSsb(); + const messages = await getAllMessages(ssbClient); + const idx = buildIndex(unwrapForIndex(messages)); + + const root = idx.rootOf(id); + if (idx.tomb.has(idx.tipByRoot.get(root) || id)) throw new Error("File not found"); + return root; + }, + + async createFile(blobMarkdown, tagsRaw, title, description, size, tribeId, extra = null) { + const ssbClient = await openSsb(); + const blobId = parseBlobId(blobMarkdown); + const tags = normalizeTags(tagsRaw) || []; + const now = new Date().toISOString(); + + let content = { + type: "file", + url: blobId, + createdAt: now, + updatedAt: null, + author: ssbClient.id, + tags, + title: title || "", + description: description || "", + size: Number(size) || 0, + opinions: {}, + opinions_inhabitants: [], + ...(tribeId ? { tribeId } : {}), + ...fileExtra(extra) + }; + + content = await encryptIfTribe(content); + + return new Promise((resolve, reject) => { + ssbClient.publish(content, (err, res) => (err ? reject(err) : resolve(res))); + }); + }, + + async createLink(url, tagsRaw, title, description, size, extra = null) { + const ssbClient = await openSsb(); + const link = String(url || '').trim(); + if (!/^https?:\/\//i.test(link)) throw new Error('Invalid link'); + const content = { + type: "file", + url: link, + createdAt: new Date().toISOString(), + updatedAt: null, + author: ssbClient.id, + tags: normalizeTags(tagsRaw) || [], + title: title || "", + description: description || "", + size: Number(size) || 0, + opinions: {}, + opinions_inhabitants: [], + ...fileExtra(extra) + }; + return new Promise((resolve, reject) => { + ssbClient.publish(content, (err, res) => (err ? reject(err) : resolve(res))); + }); + }, + + async updateFileById(id, blobMarkdown, tagsRaw, title, description, extra = null) { + const ssbClient = await openSsb(); + const userId = ssbClient.id; + const tipId = await this.resolveCurrentId(id); + const oldMsg = await getMsg(ssbClient, tipId); + + if (!oldMsg) throw new Error("File not found"); + const oldDec = await decryptIfTribe(oldMsg.content); + if (!oldDec || oldDec.type !== "file") throw new Error("File not found"); + assertReadable(oldDec, "File"); + if (oldMsg.author !== userId) throw new Error("Not the author"); + const aggFile = await this.getFileById(id, userId); + if (aggFile && Object.keys(aggFile.opinions || {}).some(k => (aggFile.opinions[k] || 0) > 0)) throw new Error("Cannot edit file after it has received opinions."); + + const tags = tagsRaw !== undefined ? normalizeTags(tagsRaw) || [] : safeArr(oldDec.tags); + const blobId = blobMarkdown ? parseBlobId(blobMarkdown) : null; + const now = new Date().toISOString(); + + let updated = { + type: "file", + replaces: tipId, + url: blobId || oldDec.url, + tags, + title: title !== undefined ? title || "" : oldDec.title || "", + description: description !== undefined ? description || "" : oldDec.description || "", + size: extra && Number(extra.size) > 0 ? Number(extra.size) : (oldDec.size || 0), + ...(extra ? fileExtra(extra) : fileExtra(oldDec)), + opinions: oldDec.opinions || {}, + opinions_inhabitants: oldDec.opinions_inhabitants || [], + author: oldDec.author || userId, + ...((oldDec.tribeId || oldMsg.content.tribeId) ? { tribeId: oldDec.tribeId || oldMsg.content.tribeId } : {}), + createdAt: oldDec.createdAt, + updatedAt: now + }; + + updated = await encryptIfTribe(updated); + + const result = await new Promise((resolve, reject) => { + ssbClient.publish(updated, (err, res) => (err ? reject(err) : resolve(res))); + }); + const tombstone = await tombFor(tipId, oldDec.tribeId || oldMsg.content.tribeId, userId); + await new Promise((res, rej) => ssbClient.publish(tombstone, (e) => (e ? rej(e) : res()))); + return result; + }, + + async deleteFileById(id) { + const ssbClient = await openSsb(); + const userId = ssbClient.id; + const tipId = await this.resolveCurrentId(id); + const msg = await getMsg(ssbClient, tipId); + + if (!msg) throw new Error("File not found"); + const dec = await decryptIfTribe(msg.content); + if (!dec || dec.type !== "file") throw new Error("File not found"); + if (msg.author !== userId) throw new Error("Not the author"); + + const tombstone = await tombFor(tipId, dec.tribeId || msg.content.tribeId, userId); + + return new Promise((resolve, reject) => { + ssbClient.publish(tombstone, (err, res) => (err ? reject(err) : resolve(res))); + }); + }, + + async listAll(filterOrOpts = "all", maybeOpts = {}) { + const ssbClient = await openSsb(); + + const opts = typeof filterOrOpts === "object" ? filterOrOpts : maybeOpts || {}; + const filter = (typeof filterOrOpts === "string" ? filterOrOpts : opts.filter || "all") || "all"; + const q = String(opts.q || "").trim().toLowerCase(); + const sort = String(opts.sort || "recent").trim(); + const viewerId = opts.viewerId || ssbClient.id; + + const messages = await getAllMessages(ssbClient); + const idx = buildIndex(unwrapForIndex(messages)); + await decryptIndexNodes(idx); + + const items = []; + for (const [rootId, tipId] of idx.tipByRoot.entries()) { + if (idx.tomb.has(tipId)) continue; + const node = idx.nodes.get(tipId); + if (!node) continue; + items.push(buildFile(node, rootId, viewerId, idx.resolveGroup(rootId))); + } + + let list = dedupeBy(items, x => x.url ? [norm(x.author), norm(x.url)].join('|') : null); + const now = Date.now(); + + if (filter === "mine") list = list.filter((a) => String(a.author) === String(viewerId)); + else if (filter === "recent") list = list.filter((a) => new Date(a.createdAt).getTime() >= now - 86400000); + else if (filter === "top") { + list = list.slice().sort((a, b) => voteSum(b.opinions) - voteSum(a.opinions) || new Date(b.createdAt) - new Date(a.createdAt)); + } + + if (q) { + list = list.filter((a) => { + const title = String(a.title || "").toLowerCase(); + const desc = String(a.description || "").toLowerCase(); + const tags = safeArr(a.tags).join(" ").toLowerCase(); + const author = String(a.author || "").toLowerCase(); + return title.includes(q) || desc.includes(q) || tags.includes(q) || author.includes(q); + }); + } + + if (sort === "top") { + list = list.slice().sort((a, b) => voteSum(b.opinions) - voteSum(a.opinions) || new Date(b.createdAt) - new Date(a.createdAt)); + } else if (sort === "oldest") { + list = list.slice().sort((a, b) => new Date(a.createdAt) - new Date(b.createdAt)); + } else { + list = list.slice().sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)); + } + + return list; + }, + + async getFileById(id, viewerId = null) { + const ssbClient = await openSsb(); + const viewer = viewerId || ssbClient.id; + const messages = await getAllMessages(ssbClient); + const idx = await memoIndex('files', messages, async (m) => { const built = buildIndex(unwrapForIndex(m)); await decryptIndexNodes(built); return built; }); + + const root = idx.rootOf(id); + if (idx.nodes.has(root) && !idx.tipByRoot.has(root)) throw new Error("File not found"); + const agg = idx.resolveGroup(root); + if (agg.contentNode && !idx.tomb.has(agg.contentTip)) return buildFile(agg.contentNode, root, viewer, agg); + + const tip = idx.tipByRoot.get(root) || id; + if (idx.tomb.has(tip)) throw new Error("File not found"); + + const msg = await getMsg(ssbClient, tip); + if (!msg) throw new Error("File not found"); + let c = msg.content; + if (tribeCrypto && (c?.encryptedPayload || tribeCrypto.isTribeMsg(c)) && tribesModel) { + const dec = await tribeCrypto.decryptFromTribe(c, tribesModel); + c = dec && !dec._undecryptable ? { ...dec, _decrypted: true } : { ...c, _decrypted: false }; + } + if (!c || c.type !== "file") throw new Error("File not found"); + return buildFile({ key: tip, ts: msg.timestamp || 0, c, author: msg.author }, root, viewer, agg); + }, + + async createOpinion(id, category) { + const ssbClient = await openSsb(); + const userId = ssbClient.id; + + if (!categories.includes(category)) throw new Error("Invalid voting category"); + + const file = await this.getFileById(id, userId); + if (!file) throw new Error("File not found"); + if (safeArr(file.opinions_inhabitants).includes(userId)) throw new Error("Already voted"); + + const content = { type: "fileOpinion", target: file.rootId, category, createdAt: new Date().toISOString() }; + return new Promise((resolve, reject) => { + ssbClient.publish(content, (err, res) => (err ? reject(err) : resolve(res))); + }); + } + }; +}; diff --git a/src/models/fileshare_model.js b/src/models/fileshare_model.js new file mode 100644 index 0000000..c3da79b --- /dev/null +++ b/src/models/fileshare_model.js @@ -0,0 +1,234 @@ +const pull = require('../server/node_modules/pull-stream'); +const fs = require('fs'); +const crypto = require('crypto'); +const { Readable } = require('stream'); +const fsc = require('../backend/fileshare_crypto'); + +const MAX_CHUNK_SIZE = 5 * 1024 * 1024; +const MAX_PREFETCH_CHUNKS = 64; +const BLOB_REF = /^&[A-Za-z0-9+/]{43}=\.sha256$/; +const DEFAULT_MAX_SHARE_BYTES = 1024 * 1024 * 1024; +const maxShareBytes = () => { + try { return Number(require('../configs/config-manager.js').getConfig().fileShare.maxSize) || DEFAULT_MAX_SHARE_BYTES; } catch (_) { return DEFAULT_MAX_SHARE_BYTES; } +}; +const checkManifest = (manifest) => { + const size = Number(manifest && manifest.size); + const chunkSize = Number(manifest && manifest.chunkSize); + const chunks = manifest && manifest.chunks; + if (!Number.isInteger(size) || size < 0 || size > maxShareBytes()) throw new Error('fileshare manifest invalid'); + if (!Number.isInteger(chunkSize) || chunkSize <= 0 || chunkSize > MAX_CHUNK_SIZE) throw new Error('fileshare manifest invalid'); + if (!Array.isArray(chunks) || chunks.length !== Math.max(1, Math.ceil(size / chunkSize)) || chunks.some(ref => !BLOB_REF.test(String(ref)))) throw new Error('fileshare manifest invalid'); + if (manifest.plainSha256 !== null && manifest.plainSha256 !== undefined && !/^[0-9a-f]{64}$/.test(String(manifest.plainSha256))) throw new Error('fileshare manifest invalid'); + return manifest; +}; + +module.exports = ({ cooler }) => { + let ssb; + const openSsb = async () => { + if (!ssb) ssb = await cooler.open(); + return ssb; + }; + + const addBlob = (client, buffer) => + new Promise((resolve, reject) => { + pull(pull.values([buffer]), client.blobs.add((err, ref) => err ? reject(err) : resolve(ref))); + }); + + const getBlob = (client, ref) => + new Promise((resolve, reject) => { + pull(client.blobs.get(ref), pull.collect((err, chunks) => err ? reject(err) : resolve(Buffer.concat(chunks)))); + }); + + const hasBlob = (client, ref) => + new Promise((resolve) => { + if (!client.blobs || typeof client.blobs.has !== 'function') return resolve(false); + client.blobs.has(ref, (err, has) => resolve(!err && !!has)); + }); + + const wantBlob = (client, ref, timeoutMs) => + new Promise((resolve) => { + if (typeof client.blobs.want !== 'function') return resolve(true); + let done = false; + const finish = (ok) => { if (done) return; done = true; if (timer) clearTimeout(timer); resolve(ok); }; + const timer = timeoutMs ? setTimeout(() => finish(false), timeoutMs) : null; + client.blobs.want(ref, (err) => finish(!err)); + }); + + const rmBlob = (client, ref) => + new Promise((resolve) => { + if (typeof client.blobs.rm !== 'function') return resolve(false); + client.blobs.rm(ref, (err) => resolve(!err)); + }); + + const createShareFromBuffer = async ({ buffer, filename, mime, chunkSize } = {}) => { + if (!Buffer.isBuffer(buffer)) throw new Error('buffer required'); + const client = await openSsb(); + const key = fsc.generateFileKey(); + const size = fsc.DEFAULT_CHUNK_SIZE; + const useChunk = Math.min(Number(chunkSize) || fsc.DEFAULT_CHUNK_SIZE, MAX_CHUNK_SIZE); + + const chunkHashes = []; + for (const part of fsc.splitBuffer(buffer, useChunk)) { + const payload = fsc.encryptChunk(part, key); + chunkHashes.push(await addBlob(client, payload)); + } + + const manifest = fsc.buildManifest({ + filename, mime, size: buffer.length, chunkSize: useChunk, + chunkHashes, plainSha256: fsc.sha256Hex(buffer) + }); + const manifestBlobId = await addBlob(client, fsc.encryptManifest(manifest, key)); + + return { + type: 'fileShare', + v: 1, + key: fsc.keyToHex(key), + manifestBlobId, + filename: manifest.filename, + mime: manifest.mime, + size: manifest.size, + chunkCount: chunkHashes.length + }; + }; + + const createShareFromFile = async ({ filepath, filename, mime, chunkSize } = {}) => { + if (!filepath) throw new Error('filepath required'); + const client = await openSsb(); + const key = fsc.generateFileKey(); + const useChunk = Math.min(Number(chunkSize) || fsc.DEFAULT_CHUNK_SIZE, MAX_CHUNK_SIZE); + const chunkHashes = []; + const hashAll = crypto.createHash('sha256'); + let total = 0; + let carry = Buffer.alloc(0); + const flush = async (part) => { + hashAll.update(part); + total += part.length; + chunkHashes.push(await addBlob(client, fsc.encryptChunk(part, key))); + }; + for await (const data of fs.createReadStream(filepath, { highWaterMark: useChunk })) { + carry = carry.length ? Buffer.concat([carry, data]) : data; + while (carry.length >= useChunk) { + await flush(carry.subarray(0, useChunk)); + carry = carry.subarray(useChunk); + } + } + if (carry.length || !chunkHashes.length) await flush(carry); + + const manifest = fsc.buildManifest({ + filename, mime, size: total, chunkSize: useChunk, + chunkHashes, plainSha256: hashAll.digest('hex') + }); + const manifestBlobId = await addBlob(client, fsc.encryptManifest(manifest, key)); + + return { + type: 'fileShare', v: 1, + key: fsc.keyToHex(key), manifestBlobId, + filename: manifest.filename, mime: manifest.mime, size: manifest.size, + chunkCount: chunkHashes.length + }; + }; + + const openManifest = async (pointer) => { + const client = await openSsb(); + const key = fsc.keyFromHex(pointer.key); + await wantBlob(client, pointer.manifestBlobId); + const payload = await getBlob(client, pointer.manifestBlobId); + return checkManifest(fsc.decryptManifest(payload, key)); + }; + + const CHUNK_WANT_TIMEOUT = 30000; + const PREFLIGHT_TIMEOUT = 12000; + + const readShareStream = (pointer) => { + const key = fsc.keyFromHex(pointer.key); + async function* gen() { + const client = await openSsb(); + if (!(await wantBlob(client, pointer.manifestBlobId, CHUNK_WANT_TIMEOUT))) throw new Error('fileshare blob unavailable'); + const manifest = checkManifest(fsc.decryptManifest(await getBlob(client, pointer.manifestBlobId), key)); + const hash = crypto.createHash('sha256'); + let total = 0; + for (const ref of manifest.chunks) { + if (!(await wantBlob(client, ref, CHUNK_WANT_TIMEOUT))) throw new Error('fileshare blob unavailable'); + const plain = fsc.decryptChunk(await getBlob(client, ref), key); + total += plain.length; + if (total > manifest.size) throw new Error('fileshare size mismatch'); + hash.update(plain); + yield plain; + } + if (total !== manifest.size) throw new Error('fileshare size mismatch'); + if (manifest.plainSha256 && hash.digest('hex') !== manifest.plainSha256) throw new Error('fileshare content mismatch'); + } + return Readable.from(gen()); + }; + + const ensureAvailable = async (pointer, timeoutMs = PREFLIGHT_TIMEOUT) => { + try { + const client = await openSsb(); + const key = fsc.keyFromHex(pointer.key); + if (!(await wantBlob(client, pointer.manifestBlobId, timeoutMs))) return false; + const manifest = fsc.decryptManifest(await getBlob(client, pointer.manifestBlobId), key); + if (manifest.chunks.length && !(await wantBlob(client, manifest.chunks[0], timeoutMs))) return false; + return true; + } catch (_) { + return false; + } + }; + + const reassembleToBuffer = async (pointer) => { + const parts = []; + for await (const chunk of readShareStream(pointer)) parts.push(chunk); + return Buffer.concat(parts); + }; + + const isAvailable = async (pointer) => { + const client = await openSsb(); + if (!(await hasBlob(client, pointer.manifestBlobId))) return false; + try { + const key = fsc.keyFromHex(pointer.key); + const manifest = fsc.decryptManifest(await getBlob(client, pointer.manifestBlobId), key); + for (const ref of manifest.chunks) if (!(await hasBlob(client, ref))) return false; + return true; + } catch (_) { + return false; + } + }; + + const prefetch = async (pointer) => { + try { + const client = await openSsb(); + if (!(await hasBlob(client, pointer.manifestBlobId))) { wantBlob(client, pointer.manifestBlobId, 0); return; } + const manifest = fsc.decryptManifest(await getBlob(client, pointer.manifestBlobId), fsc.keyFromHex(pointer.key)); + if (!Array.isArray(manifest.chunks) || manifest.chunks.length > MAX_PREFETCH_CHUNKS || manifest.chunks.some(ref => !BLOB_REF.test(String(ref)))) return; + for (const ref of manifest.chunks) if (!(await hasBlob(client, ref))) wantBlob(client, ref, 0); + } catch (_) {} + }; + + const removeLocalBlobs = async (pointer) => { + const client = await openSsb(); + let removed = 0; + try { + const key = fsc.keyFromHex(pointer.key); + if (await hasBlob(client, pointer.manifestBlobId)) { + const manifest = fsc.decryptManifest(await getBlob(client, pointer.manifestBlobId), key); + for (const ref of manifest.chunks) if (await rmBlob(client, ref)) removed++; + } + } catch (_) {} + if (await rmBlob(client, pointer.manifestBlobId)) removed++; + return removed; + }; + + const pruneExpired = async (sentFileShares, ttlMs, now) => { + const cutoff = (Number.isFinite(now) ? now : Date.now()) - (Number(ttlMs) || 0); + let pruned = 0; + for (const item of (Array.isArray(sentFileShares) ? sentFileShares : [])) { + if (!item || !item.pointer) continue; + const sentAt = new Date(item.sentAt || 0).getTime(); + if (!(sentAt <= cutoff)) continue; + const n = await removeLocalBlobs(item.pointer); + if (n > 0) pruned++; + } + return pruned; + }; + + return { createShareFromBuffer, createShareFromFile, openManifest, readShareStream, reassembleToBuffer, isAvailable, ensureAvailable, prefetch, removeLocalBlobs, pruneExpired }; +}; diff --git a/src/models/fork_workflows.js b/src/models/fork_workflows.js new file mode 100644 index 0000000..4bc97ec --- /dev/null +++ b/src/models/fork_workflows.js @@ -0,0 +1,39 @@ +"use strict"; +// Workflows propios de la rama, registrados sin editar workflows_model.js. +// +// workflows_model.js es del dev (lo toca en 8 de cada 39 releases) y ya trae un +// workflow "activists" con unos 50 modulos. En vez de redefinirlo, la rama anade +// los suyos empujandolos a los arrays que ese fichero exporta; getWorkflow, +// currentWorkflow y el asistente de bienvenida los leen de ahi. +// Ver docs/FRONTERA_UPSTREAM.md. + +const wf = require('./workflows_model'); + +const FORK_WORKFLOWS = [ + { + // una acampada: hablar (chats, telefono y salas de voz del dev), escribir juntos, + // compartir lo que pasa y decidir (tribus = comisiones, parlamento y votaciones) + key: 'acampada', + theme: 'Acampada', + homePage: 'activity', + modules: ['chats', 'phone', 'rooms', 'pads', 'forum', 'images', 'videos', 'wiki', 'blogs', 'feed', 'invites', 'backup', + 'tribes', 'parliament', 'votes', 'events'] + } +]; + +let done = false; +const register = () => { + if (done) return; + done = true; + for (const w of FORK_WORKFLOWS) { + if (wf.WORKFLOWS.some(x => x.key === w.key)) continue; + wf.WORKFLOWS.push(w); + wf.WORKFLOW_KEYS.push(w.key); + // en el asistente, detras de activistas (en el movil mobile_ui.workflowPick pone + // Activismo y Acampada las primeras) + const at = wf.WIZARD_WORKFLOWS.indexOf('activists'); + wf.WIZARD_WORKFLOWS.splice(at === -1 ? wf.WIZARD_WORKFLOWS.length : at + 1, 0, w.key); + } +}; + +module.exports = { FORK_WORKFLOWS, register }; diff --git a/src/models/forum_model.js b/src/models/forum_model.js index df27060..3869e60 100644 --- a/src/models/forum_model.js +++ b/src/models/forum_model.js @@ -2,6 +2,8 @@ const pull = require('../server/node_modules/pull-stream'); const crypto = require('crypto'); const { getConfig } = require('../configs/config-manager.js'); const { buildValidatedTombstoneSet } = require('./tombstone_validator'); +const { readTyped, requestScope, memoIndex } = require('./typed_log'); +const longText = require('../backend/long_text'); const logLimit = getConfig().ssbLogStream?.limit || 1000; module.exports = ({ cooler, tribeCrypto, forumCrypto }) => { @@ -33,12 +35,13 @@ module.exports = ({ cooler, tribeCrypto, forumCrypto }) => { const ssbClient = await openSsb(); const ssbKeys = require('../server/node_modules/ssb-keys'); const cfg = require('../server/ssb_config'); - const msgs = await new Promise((res, rej) => - pull(ssbClient.createLogStream({ limit: logLimit }), pull.collect((e, m) => e ? rej(e) : res(m))) - ); + const msgs = await readForumLog(); + const authorOf = new Map(); + for (const m of msgs) if (m && m.key && m.value) authorOf.set(m.key, m.value.author); for (const m of msgs) { const c = m.value && m.value.content; if (!c || c.type !== 'tribe-keys') continue; + if (!c.tribeId || authorOf.get(c.tribeId) !== m.value.author) continue; const memberKeys = c.memberKeys; if (!memberKeys || typeof memberKeys !== 'object') continue; const boxed = memberKeys[ssbClient.id]; @@ -60,41 +63,63 @@ module.exports = ({ cooler, tribeCrypto, forumCrypto }) => { return ssb; }; + const FORUM_TYPES = ['forum', 'forum-invite', 'forum-invite-tombstone', 'forum-open-invite', 'forum-open-invite-tombstone', 'tribe-keys', 'vote', 'tombstone', longText.CHUNK_TYPE]; + + const chunkIndexOf = (msgs) => longText.indexChunks( + msgs.filter(m => m.value && m.value.content && m.value.content.type === longText.CHUNK_TYPE), + (c) => { + if (!c || !c.encryptedPayload) return c; + const dec = decryptForumContent(c, null); + return dec && !dec._undecryptable ? dec : null; + } + ); + const fullText = (idx, c, author) => longText.joinText(c && c.text, c && c.chunks, longText.lookupIn(idx), author); + const readForumLog = async () => { const ssbClient = await openSsb(); - return new Promise((res, rej) => pull(ssbClient.createLogStream({ limit: logLimit }), pull.collect((e, m) => e ? rej(e) : res(m || [])))); + return readTyped(ssbClient, FORUM_TYPES, { limit: logLimit, withWindow: true }); }; - const scanForumOpenInvite = async (forumId) => { - const messages = await readForumLog(); + const authorsByKey = (messages) => { + const out = new Map(); + for (const m of messages) if (m && m.key && m.value) out.set(m.key, m.value.author); + return out; + }; + + const inviteTombstones = (messages, authorOf) => { const markerTomb = new Set(); const invTomb = new Set(); for (const m of messages) { const c = m.value && m.value.content; - if (!c) continue; - if (c.type === 'forum-open-invite-tombstone' && typeof c.target === 'string') markerTomb.add(c.target); - if (c.type === 'forum-invite-tombstone' && typeof c.target === 'string') invTomb.add(c.target); + if (!c || typeof c.target !== 'string' || authorOf.get(c.target) !== m.value.author) continue; + if (c.type === 'forum-open-invite-tombstone') markerTomb.add(c.target); + if (c.type === 'forum-invite-tombstone') invTomb.add(c.target); } + return { markerTomb, invTomb }; + }; + + const scanForumOpenInvite = async (forumId) => { + const messages = await readForumLog(); + const authorOf = authorsByKey(messages); + const forumAuthor = authorOf.get(forumId); + if (!forumAuthor) return null; + const { markerTomb, invTomb } = inviteTombstones(messages, authorOf); let best = null; for (const m of messages) { const c = m.value && m.value.content; if (!c || c.type !== 'forum-open-invite' || c.v !== 1) continue; if (c.target !== forumId || typeof c.code !== 'string') continue; + if (m.value.author !== forumAuthor) continue; if (markerTomb.has(m.key)) continue; if (c.inviteKey && invTomb.has(c.inviteKey)) continue; const ts = (m.value && m.value.timestamp) || 0; - if (!best || ts > best.ts) best = { code: c.code, by: c.by || m.value.author, markerKey: m.key, inviteKey: c.inviteKey || null, ts }; + if (!best || ts > best.ts) best = { code: c.code, by: m.value.author, markerKey: m.key, inviteKey: c.inviteKey || null, ts }; } return best; }; async function collectTombstones(ssbClient) { - return new Promise((resolve, reject) => { - pull( - ssbClient.createLogStream({ limit: logLimit }), - pull.collect((err, msgs) => err ? reject(err) : resolve(buildValidatedTombstoneSet(msgs))) - ); - }); + return deletedOf(await readForumLog()); } async function findActiveVote(ssbClient, targetId, voter) { @@ -108,16 +133,38 @@ module.exports = ({ cooler, tribeCrypto, forumCrypto }) => { }); } - async function aggregateVotes(ssbClient, targetId) { - const tombstoned = await collectTombstones(ssbClient); + const deletedOf = (msgs) => memoIndex('forum:deleted', msgs, buildValidatedTombstoneSet); + + function aggregateVotes(ssbClient, targetId, known = null) { + const store = requestScope.getStore(); + if (!store || store.method !== 'GET') return countVotes(ssbClient, targetId, known); + if (!store.forumVotes) store.forumVotes = new Map(); + let pending = store.forumVotes.get(targetId); + if (!pending) { pending = countVotes(ssbClient, targetId, known); store.forumVotes.set(targetId, pending); } + return pending; + } + + async function countVotes(ssbClient, targetId, known = null) { + const tombstoned = known || await collectTombstones(ssbClient); return new Promise((resolve, reject) => { - let positives = 0, negatives = 0; + const latest = new Map(); pull( ssbClient.links({ source: null, dest: targetId, rel: 'vote', values: true, keys: true }), pull.filter(link => link.value.content?.vote && !tombstoned.has(link.key)), pull.drain( - link => link.value.content.vote.value > 0 ? positives++ : negatives++, - err => err ? reject(err) : resolve({ positives, negatives }) + link => { + const author = link.value.author; + const ts = Number(link.value.timestamp) || 0; + const seq = Number(link.value.sequence) || 0; + const prev = latest.get(author); + if (!prev || ts > prev.ts || (ts === prev.ts && seq > prev.seq)) latest.set(author, { value: Number(link.value.content.vote.value) || 0, ts, seq }); + }, + err => { + if (err) return reject(err); + let positives = 0, negatives = 0; + for (const v of latest.values()) { if (v.value > 0) positives++; else if (v.value < 0) negatives++; } + resolve({ positives, negatives }); + } ) ); }); @@ -141,16 +188,89 @@ module.exports = ({ cooler, tribeCrypto, forumCrypto }) => { } async function getMessageById(id) { - const ssbClient = await openSsb(); - const msgs = await new Promise((res, rej) => - pull(ssbClient.createLogStream({ limit: logLimit }), - pull.collect((err, data) => err ? rej(err) : res(data))) - ); + const msgs = await readForumLog(); const msg = msgs.find(m => m.key === id && m.value.content?.type === 'forum'); if (!msg) throw new Error('Message not found'); - return { key: msg.key, ...msg.value.content, timestamp: msg.value.timestamp }; + return { key: msg.key, ...msg.value.content, author: msg.value.author, timestamp: msg.value.timestamp }; } + const buildForums = async (ssbClient) => { + const msgs = await readForumLog(); + const deleted = deletedOf(msgs); + const chunkIdx = chunkIndexOf(msgs); + const decode = (m) => { + const c = m.value && m.value.content; + if (!c) return null; + if (c.encryptedPayload) { + const dec = decryptForumContent(c, m.value.content.root || m.key); + return (dec && !dec._undecryptable) ? dec : null; + } + return c; + }; + const forums = msgs + .map(m => ({ m, c: decode(m) })) + .filter(({ m, c }) => c && c.type === 'forum' && !c.root && !deleted.has(m.key)) + .map(({ m, c }) => ({ ...c, key: m.key, author: m.value.author, text: fullText(chunkIdx, c, m.value.author) })); + const forumsWithVotes = await Promise.all( + forums.map(async f => { + const { positives, negatives } = await aggregateVotes(ssbClient, f.key, deleted); + return { ...f, positiveVotes: positives, negativeVotes: negatives }; + }) + ); + const repliesByRoot = {}; + msgs.forEach(m => { + const cRaw = m.value && m.value.content; + if (!cRaw) return; + const root = cRaw.encryptedPayload ? null : cRaw.root; + if (!root) { + if (!cRaw.encryptedPayload) return; + const decReply = decryptForumContent(cRaw, null); + if (!decReply || decReply._undecryptable || decReply.type !== 'forum' || !decReply.root) return; + if (deleted.has(m.key)) return; + repliesByRoot[decReply.root] = repliesByRoot[decReply.root] || []; + repliesByRoot[decReply.root].push({ key: m.key, text: fullText(chunkIdx, decReply, m.value.author), author: m.value.author, timestamp: m.value.timestamp }); + return; + } + if (cRaw.type === 'forum' && root && !deleted.has(m.key)) { + repliesByRoot[root] = repliesByRoot[root] || []; + repliesByRoot[root].push({ key: m.key, text: fullText(chunkIdx, cRaw, m.value.author), author: m.value.author, timestamp: m.value.timestamp }); + } + }); + const final = await Promise.all( + forumsWithVotes.map(async f => { + const replies = repliesByRoot[f.key] || []; + await Promise.all(replies.map(async (r) => { + const { positives: rp, negatives: rn } = await aggregateVotes(ssbClient, r.key, deleted); + r.positiveVotes = rp; + r.negativeVotes = rn; + r.score = rp - rn; + })); + const replyPos = replies.reduce((sum, r) => sum + (r.positiveVotes || 0), 0); + const replyNeg = replies.reduce((sum, r) => sum + (r.negativeVotes || 0), 0); + const positiveVotes = f.positiveVotes + replyPos; + const negativeVotes = f.negativeVotes + replyNeg; + const score = positiveVotes - negativeVotes; + const participants = new Set(replies.map(r => r.author).concat(f.author)); + const messagesCount = replies.length + 1; + const lastMessage = + replies.length + ? replies.reduce((a, b) => (new Date(a.timestamp) > new Date(b.timestamp) ? a : b)) + : null; + return { + ...f, + positiveVotes, + negativeVotes, + score, + participants: Array.from(participants), + messagesCount, + lastMessage, + messages: replies + }; + }) + ); + return final; + }; + return { ingestKeys: async () => { await ingestOwnTribeKeys(); }, @@ -168,15 +288,14 @@ module.exports = ({ cooler, tribeCrypto, forumCrypto }) => { votes_inhabitants: [], isPrivate: isPrivateFlag }; - let content = plainContent; let forumKey = null; - if (isPrivateFlag && ownCrypto && tribeCrypto) { - forumKey = ownCrypto.generateTribeKey(); - content = tribeCrypto.encryptContent(plainContent, [forumKey], true); - } - const result = await new Promise((resolve, reject) => - ssbClient.publish(content, (err, res) => err ? reject(err) : resolve(res)) + if (isPrivateFlag && ownCrypto && tribeCrypto) forumKey = ownCrypto.generateTribeKey(); + const seal = (c) => forumKey ? tribeCrypto.encryptContent(c, [forumKey], true) : c; + const publish = (c) => new Promise((resolve, reject) => + ssbClient.publish(seal(c), (err, res) => err ? reject(err) : resolve(res)) ); + const content = await longText.chunkContent(plainContent, 'text', { maxBytes: forumKey ? longText.CONTENT_BYTES.sealed : longText.CONTENT_BYTES.plain, publish }); + const result = await publish(content); if (forumKey) { ownCrypto.setKey(result.key, forumKey, 1); try { @@ -208,14 +327,17 @@ module.exports = ({ cooler, tribeCrypto, forumCrypto }) => { votes_inhabitants: [] }; if (parentId) content.branch = parentId; + let key = null; if (isPrivate && ownCrypto && tribeCrypto) { - const key = lookupKey(forumId); + key = lookupKey(forumId); if (!key) throw new Error('Missing forum key — cannot reply to encrypted forum'); - content = tribeCrypto.encryptContent(content, [key], true); } - return new Promise((resolve, reject) => - ssbClient.publish(content, (err, res) => err ? reject(err) : resolve(res)) + const seal = (c) => key ? tribeCrypto.encryptContent(c, [key], true) : c; + const publish = (c) => new Promise((resolve, reject) => + ssbClient.publish(seal(c), (err, res) => err ? reject(err) : resolve(res)) ); + content = await longText.chunkContent(content, 'text', { maxBytes: key ? longText.CONTENT_BYTES.sealed : longText.CONTENT_BYTES.plain, publish }); + return publish(content); }, generateInvite: async (forumId) => { @@ -226,7 +348,7 @@ module.exports = ({ cooler, tribeCrypto, forumCrypto }) => { const rawC = rawRoot.content; const dec = rawC && rawC.encryptedPayload ? decryptForumContent(rawC, forumId) : rawC; if (dec && dec._undecryptable) throw new Error('Forum is encrypted and cannot be decrypted'); - if (dec.author !== userId) throw new Error('Only the author can generate invites'); + if (rawRoot.author !== userId) throw new Error('Only the author can generate invites'); if (dec.isPrivate !== true) throw new Error('Only private forums use invitation codes'); const key = lookupKey(forumId); if (!key) throw new Error('Missing forum key'); @@ -252,7 +374,7 @@ module.exports = ({ cooler, tribeCrypto, forumCrypto }) => { const rawC = rawRoot.content; const dec = rawC && rawC.encryptedPayload ? decryptForumContent(rawC, forumId) : rawC; if (dec && dec._undecryptable) throw new Error('Forum is encrypted and cannot be decrypted'); - if (dec.author !== userId) throw new Error('Only the author can generate invites'); + if (rawRoot.author !== userId) throw new Error('Only the author can generate invites'); if (dec.isPrivate !== true) throw new Error('Only private forums use invitation codes'); if (await scanForumOpenInvite(forumId)) throw new Error('An open invitation already exists'); const key = lookupKey(forumId); @@ -276,9 +398,7 @@ module.exports = ({ cooler, tribeCrypto, forumCrypto }) => { let author = null; try { const rawRoot = await new Promise((res, rej) => ssbClient.get(forumId, (e, m) => e ? rej(e) : res(m))); - const rawC = rawRoot && rawRoot.content; - const dec = rawC && rawC.encryptedPayload ? decryptForumContent(rawC, forumId) : rawC; - author = dec && dec.author; + author = rawRoot && rawRoot.author; } catch (_) {} if (rec.by !== userId && author !== userId) throw new Error('Not allowed to remove this invitation'); await new Promise((resolve, reject) => ssbClient.publish({ type: 'forum-open-invite-tombstone', target: rec.markerKey, ts: new Date().toISOString() }, (err) => err ? reject(err) : resolve())); @@ -288,19 +408,15 @@ module.exports = ({ cooler, tribeCrypto, forumCrypto }) => { joinByInvite: async (code) => { if (!ownCrypto || !tribeCrypto) throw new Error('Forum crypto unavailable'); const ssbClient = await openSsb(); - const messages = await new Promise((res, rej) => - pull(ssbClient.createLogStream({ limit: logLimit }), pull.collect((e, m) => e ? rej(e) : res(m))) - ); - const invTomb = new Set(); - for (const m of messages) { - const c = m.value && m.value.content; - if (c && c.type === 'forum-invite-tombstone' && typeof c.target === 'string') invTomb.add(c.target); - } + const messages = await readForumLog(); + const authorOf = authorsByKey(messages); + const { invTomb } = inviteTombstones(messages, authorOf); let matched = null; for (const m of messages) { const c = m.value && m.value.content; if (!c || c.type !== 'forum-invite') continue; if (invTomb.has(m.key)) continue; + if (!authorOf.get(c.target) || authorOf.get(c.target) !== m.value.author) continue; try { const hash = tribeCrypto.hashInviteCode(code, c.salt); if (hash === c.codeHash) { matched = c; break; } @@ -356,81 +472,10 @@ module.exports = ({ cooler, tribeCrypto, forumCrypto }) => { listAll: async filter => { const ssbClient = await openSsb(); - const msgs = await new Promise((res, rej) => - pull(ssbClient.createLogStream({ limit: logLimit }), - pull.collect((err, data) => err ? rej(err) : res(data))) - ); - const deleted = buildValidatedTombstoneSet(msgs); - const decode = (m) => { - const c = m.value && m.value.content; - if (!c) return null; - if (c.encryptedPayload) { - const dec = decryptForumContent(c, m.value.content.root || m.key); - return (dec && !dec._undecryptable) ? dec : null; - } - return c; - }; - const forums = msgs - .map(m => ({ m, c: decode(m) })) - .filter(({ m, c }) => c && c.type === 'forum' && !c.root && !deleted.has(m.key)) - .map(({ m, c }) => ({ ...c, key: m.key })); - const forumsWithVotes = await Promise.all( - forums.map(async f => { - const { positives, negatives } = await aggregateVotes(ssbClient, f.key); - return { ...f, positiveVotes: positives, negativeVotes: negatives }; - }) - ); - const repliesByRoot = {}; - msgs.forEach(m => { - const cRaw = m.value && m.value.content; - if (!cRaw) return; - const root = cRaw.encryptedPayload ? null : cRaw.root; - if (!root) { - if (!cRaw.encryptedPayload) return; - const decReply = decryptForumContent(cRaw, null); - if (!decReply || decReply._undecryptable || decReply.type !== 'forum' || !decReply.root) return; - if (deleted.has(m.key)) return; - repliesByRoot[decReply.root] = repliesByRoot[decReply.root] || []; - repliesByRoot[decReply.root].push({ key: m.key, text: decReply.text, author: decReply.author, timestamp: m.value.timestamp }); - return; - } - if (cRaw.type === 'forum' && root && !deleted.has(m.key)) { - repliesByRoot[root] = repliesByRoot[root] || []; - repliesByRoot[root].push({ key: m.key, text: cRaw.text, author: cRaw.author, timestamp: m.value.timestamp }); - } - }); - const final = await Promise.all( - forumsWithVotes.map(async f => { - const replies = repliesByRoot[f.key] || []; - for (let r of replies) { - const { positives: rp, negatives: rn } = await aggregateVotes(ssbClient, r.key); - r.positiveVotes = rp; - r.negativeVotes = rn; - r.score = rp - rn; - } - const replyPos = replies.reduce((sum, r) => sum + (r.positiveVotes || 0), 0); - const replyNeg = replies.reduce((sum, r) => sum + (r.negativeVotes || 0), 0); - const positiveVotes = f.positiveVotes + replyPos; - const negativeVotes = f.negativeVotes + replyNeg; - const score = positiveVotes - negativeVotes; - const participants = new Set(replies.map(r => r.author).concat(f.author)); - const messagesCount = replies.length + 1; - const lastMessage = - replies.length - ? replies.reduce((a, b) => (new Date(a.timestamp) > new Date(b.timestamp) ? a : b)) - : null; - return { - ...f, - positiveVotes, - negativeVotes, - score, - participants: Array.from(participants), - messagesCount, - lastMessage, - messages: replies - }; - }) - ); + const userId = ssbClient.id; + const store = requestScope.getStore(); + if (store && !store.forumAll) store.forumAll = buildForums(ssbClient); + const final = (await (store ? store.forumAll : buildForums(ssbClient))).slice(); const filtered = filter === 'mine' ? final.filter(f => f.author === userId) @@ -442,17 +487,14 @@ module.exports = ({ cooler, tribeCrypto, forumCrypto }) => { getForumById: async id => { const ssbClient = await openSsb(); - const msgs = await new Promise((res, rej) => - pull(ssbClient.createLogStream({ limit: logLimit }), - pull.collect((err, data) => err ? rej(err) : res(data))) - ); - const deleted = buildValidatedTombstoneSet(msgs); + const msgs = await readForumLog(); + const deleted = deletedOf(msgs); const original = msgs.find(m => m.key === id && !deleted.has(m.key)); if (!original || original.value.content?.type !== 'forum') throw new Error('Forum not found'); const rawBase = original.value.content; const base = rawBase.encryptedPayload ? decryptForumContent(rawBase, id) : rawBase; if (base && base._undecryptable) throw new Error('Forum is encrypted and cannot be decrypted with available keys'); - const { positives, negatives } = await aggregateVotes(ssbClient, id); + const { positives, negatives } = await aggregateVotes(ssbClient, id, deleted); const replyAuthors = []; for (const m of msgs) { if (deleted.has(m.key)) continue; @@ -464,11 +506,13 @@ module.exports = ({ cooler, tribeCrypto, forumCrypto }) => { if (!dec || dec._undecryptable) continue; rc = dec; } - if (rc.type === 'forum' && rc.root === id) replyAuthors.push(rc.author); + if (rc.type === 'forum' && rc.root === id) replyAuthors.push(m.value.author); } - const participants = Array.from(new Set(replyAuthors.concat(base.author).filter(Boolean))); + const participants = Array.from(new Set(replyAuthors.concat(original.value.author).filter(Boolean))); return { ...base, + author: original.value.author, + text: fullText(chunkIndexOf(msgs), base, original.value.author), key: id, positiveVotes: positives, negativeVotes: negatives, @@ -479,11 +523,9 @@ module.exports = ({ cooler, tribeCrypto, forumCrypto }) => { getMessagesByForumId: async forumId => { const ssbClient = await openSsb(); - const msgs = await new Promise((res, rej) => - pull(ssbClient.createLogStream({ limit: logLimit }), - pull.collect((err, data) => err ? rej(err) : res(data))) - ); - const deleted = buildValidatedTombstoneSet(msgs); + const msgs = await readForumLog(); + const deleted = deletedOf(msgs); + const chunkIdx = chunkIndexOf(msgs); const decodeReply = (m) => { const c = m.value && m.value.content; if (!c || c.type !== 'forum') return null; @@ -500,18 +542,20 @@ module.exports = ({ cooler, tribeCrypto, forumCrypto }) => { .filter(r => r && !deleted.has(r.m.key)) .map(({ c, m }) => ({ key: m.key, - text: c.text, - author: c.author, + text: fullText(chunkIdx, c, m.value.author), + author: m.value.author, timestamp: m.value.timestamp, parent: c.branch || null })); - for (let r of replies) { - const { positives: rp, negatives: rn } = await aggregateVotes(ssbClient, r.key); - r.positiveVotes = rp; - r.negativeVotes = rn; - r.score = rp - rn; - } - const { positives: p, negatives: n } = await aggregateVotes(ssbClient, forumId); + const [, { positives: p, negatives: n }] = await Promise.all([ + Promise.all(replies.map(async (r) => { + const { positives: rp, negatives: rn } = await aggregateVotes(ssbClient, r.key, deleted); + r.positiveVotes = rp; + r.negativeVotes = rn; + r.score = rp - rn; + })), + aggregateVotes(ssbClient, forumId, deleted) + ]); const replyPos = replies.reduce((sum, r) => sum + (r.positiveVotes || 0), 0); const replyNeg = replies.reduce((sum, r) => sum + (r.negativeVotes || 0), 0); const positiveVotes = p + replyPos; diff --git a/src/models/games_model.js b/src/models/games_model.js index c148226..65b2eac 100644 --- a/src/models/games_model.js +++ b/src/models/games_model.js @@ -1,11 +1,14 @@ const pull = require('../server/node_modules/pull-stream'); const { getConfig } = require('../configs/config-manager.js'); +const { readTyped } = require('./typed_log'); const logLimit = getConfig().ssbLogStream?.limit || 5000; +const GAME_TYPES = ['gameScore']; + const VALID_GAMES = new Set([ 'cocoland', 'ecoinflow', 'neoninfiltrator', 'spaceinvaders', 'arkanoid', 'pingpong', 'asteroids', 'tiktaktoe', 'flipflop', - '8ball', 'artillery', 'labyrinth', 'cocoman', 'tetris' + '8ball', 'artillery', 'labyrinth', 'cocoman', 'tetris', 'minesweeper' ]); module.exports = ({ cooler }) => { @@ -16,12 +19,7 @@ module.exports = ({ cooler }) => { }; async function readAll(ssbClient) { - return new Promise((resolve, reject) => { - pull( - ssbClient.createLogStream({ limit: logLimit }), - pull.collect((err, results) => (err ? reject(err) : resolve(results))) - ); - }); + return readTyped(ssbClient, GAME_TYPES, { limit: logLimit }); } return { diff --git a/src/models/housing_model.js b/src/models/housing_model.js new file mode 100644 index 0000000..1f06356 --- /dev/null +++ b/src/models/housing_model.js @@ -0,0 +1,527 @@ +const pull = require("../server/node_modules/pull-stream") +const moment = require("../server/node_modules/moment") +const categories = require("../backend/opinion_categories") +const { getConfig } = require("../configs/config-manager.js") +const { dedupeByPreferring, versionGroups } = require('../backend/dedupe') +const { buildValidatedTombstoneSet } = require('./tombstone_validator') +const { readTyped, memoIndex } = require('./typed_log') +const logLimit = getConfig().ssbLogStream?.limit || 1000 + +const HOUSING_TYPES = ["sale", "rent", "couchsurfing"] +const PROPERTY_TYPES = ["apartment", "house", "room", "land", "other"] + +const norm = (s) => String(s || "").trim().toLowerCase() +const safeArr = (v) => (Array.isArray(v) ? v : []) + +const toNum = (v) => { + const n = parseFloat(String(v ?? "").replace(",", ".")) + return Number.isFinite(n) ? n : NaN +} + +const toInt = (v, fallback = 0) => { + const n = parseInt(String(v ?? ""), 10) + return Number.isFinite(n) ? n : fallback +} + +const nonNeg = (v) => { + const n = toNum(v) + return Number.isFinite(n) && n >= 0 ? n : 0 +} + +const safeDate = (v) => { + const s = String(v || "").trim() + if (!s) return "" + const d = new Date(s) + return Number.isFinite(d.getTime()) ? s : "" +} + +const normalizeTags = (raw) => { + if (raw === undefined || raw === null) return [] + if (Array.isArray(raw)) return raw.map(t => String(t || "").trim()).filter(Boolean) + return String(raw).split(",").map(t => t.trim()).filter(Boolean) +} + +const safeType = (v) => { + const t = norm(v) + return HOUSING_TYPES.includes(t) ? t : "" +} + +const safeProperty = (v) => { + const t = norm(v) + return PROPERTY_TYPES.includes(t) ? t : "other" +} + +const { MAX_IMAGES, normalizeImages, normalizeVideo } = require('./media_gallery') + +const matchSearch = (item, q) => { + const qq = norm(q) + if (!qq) return true + const hay = [ + item.title, + item.description, + item.place, + item.rules, + item.housing_type, + item.property_type, + safeArr(item.tags).join(" ") + ].map(x => norm(x)).join(" ") + return hay.includes(qq) +} + +module.exports = ({ cooler, tribeCrypto }) => { + let ssb + const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb } + + const isEncrypted = (c) => !!(c && c.encryptedPayload) + + const keysForRoot = (rootId) => { + if (!tribeCrypto || !rootId) return [] + const ks = (tribeCrypto.getKeys && tribeCrypto.getKeys(rootId)) || [] + if (ks.length) return ks + const k = tribeCrypto.getKey ? tribeCrypto.getKey(rootId) : null + return k ? [k] : [] + } + + const decryptHousing = (c, rootId) => { + if (!isEncrypted(c)) return c + if (!tribeCrypto) return { ...c, _undecryptable: true } + const keys = keysForRoot(rootId) + if (!keys.length) return { ...c, _undecryptable: true } + return tribeCrypto.decryptContent(c, keys.map(k => [k])) + } + + const publishHousing = (ssbClient, content, rootId) => new Promise((resolve, reject) => { + const hidden = String(content.visibility || "PUBLIC").toUpperCase() === "HIDDEN" + if (!hidden || !tribeCrypto) { + ssbClient.publish(content, (err, msg) => { + if (err) return reject(err) + if (msg && msg.key && tribeCrypto && !rootId) { + try { tribeCrypto.setKey(msg.key, tribeCrypto.generateTribeKey(), 1) } catch (_) {} + } + resolve(msg) + }) + return + } + let key = rootId ? (keysForRoot(rootId)[0] || null) : null + if (!key) key = tribeCrypto.generateTribeKey() + let envelope + try { envelope = tribeCrypto.encryptContent(content, [key], true) } catch (err) { return reject(err) } + ssbClient.publish(envelope, (err, msg) => { + if (err) return reject(err) + try { tribeCrypto.setKey(rootId || (msg && msg.key), key, 1) } catch (_) {} + resolve(msg) + }) + }) + + const HOUSING_MSG_TYPES = ["housing", "housingOpinion", "housingRequest", "tombstone"] + + const readAll = async (ssbClient) => readTyped(ssbClient, HOUSING_MSG_TYPES, { limit: logLimit, withWindow: true, withPrivate: true }) + + const buildIndex = (messages, ssbClient) => { + const tomb = buildValidatedTombstoneSet(messages) + const nodes = new Map() + const requestMsgs = [] + const opinionMsgs = [] + + for (const m of messages) { + const key = m.key + const v = m.value || {} + const c = v.content + if (!c || typeof c !== "object") continue + + if (c.type === "housing") { + nodes.set(key, { key, ts: v.timestamp || m.timestamp || 0, seq: v.sequence || 0, c, author: v.author }) + continue + } + + if (c.type === "housingOpinion" && c.target) { + opinionMsgs.push({ target: c.target, author: v.author, category: c.category }) + continue + } + + if (c.type === "housingRequest" && c.housingId) { + const author = v.author + if (!author) continue + requestMsgs.push({ ts: v.timestamp || m.timestamp || 0, value: !!c.value, author, housingId: c.housingId }) + continue + } + } + + if (ssbClient) { + for (const m of messages) { + if (typeof m.value?.content !== 'string') continue + try { + const dec = ssbClient.private.unbox({ key: m.key, value: m.value, timestamp: m.value?.timestamp || m.timestamp || 0 }) + const c = dec?.value?.content + if (!c || c.type !== 'housingRequest' || !c.housingId) continue + const author = dec.value.author + if (!author) continue + requestMsgs.push({ ts: dec.value.timestamp || m.timestamp || 0, value: !!c.value, author, housingId: c.housingId }) + } catch {} + } + } + + const vg = versionGroups(Array.from(nodes.values()).map(n => ({ key: n.key, author: n.author, seq: n.seq, ts: n.ts, replaces: typeof n.c.replaces === "string" ? n.c.replaces : null, claimedAuthor: typeof n.c.author === "string" ? n.c.author : null }))) + const rootOf = vg.rootOf + const tipOf = vg.tipOf + + const tipByRoot = new Map() + for (const r of vg.roots) tipByRoot.set(r, tipOf(r)) + + const requestLatest = new Map() + for (const req of requestMsgs) { + const housingId = rootOf(req.housingId) + const k = `${housingId}::${req.author}` + const prev = requestLatest.get(k) + if (!prev || req.ts >= prev.ts) requestLatest.set(k, { ...req, housingId }) + } + + const requestsByRoot = new Map() + const everRequestedByRoot = new Map() + for (const { housingId, author, value } of requestLatest.values()) { + if (!requestsByRoot.has(housingId)) requestsByRoot.set(housingId, new Set()) + if (!everRequestedByRoot.has(housingId)) everRequestedByRoot.set(housingId, new Set()) + everRequestedByRoot.get(housingId).add(author) + const set = requestsByRoot.get(housingId) + if (value) set.add(author) + else set.delete(author) + } + + const opinionsByRoot = new Map() + for (const op of opinionMsgs) { + if (!nodes.has(op.target)) continue + const r = rootOf(op.target) + if (!opinionsByRoot.has(r)) opinionsByRoot.set(r, []) + opinionsByRoot.get(r).push(op) + } + + const aggregateFor = (rootId, content, ownerId) => { + const opinions = { ...((content && content.opinions) || {}) } + const voters = safeArr(content && content.opinions_inhabitants).slice() + const voterSet = new Set(voters) + for (const op of (opinionsByRoot.get(rootId) || [])) { + if (!op.author || op.author === ownerId) continue + if (voterSet.has(op.author)) continue + if (!categories.includes(op.category)) continue + voterSet.add(op.author); voters.push(op.author) + opinions[op.category] = (opinions[op.category] || 0) + 1 + } + return { opinions, opinions_inhabitants: voters } + } + + return { tomb, nodes, vg, rootOf, tipOf, tipByRoot, requestsByRoot, everRequestedByRoot, aggregateFor } + } + + const buildObject = (node, rootId, idx, viewer) => { + const raw = node.c || {} + const c = decryptHousing(raw, rootId) + if (c._undecryptable) return null + const author = node.author || c.author + const requests = Array.from(idx.requestsByRoot.get(rootId) || []) + const visibleRequests = (author === viewer) ? requests : (requests.includes(viewer) ? [viewer] : []) + const agg = idx.aggregateFor(rootId, c, author) + return { + id: node.key, + rootId, + housing_type: safeType(c.housing_type), + property_type: safeProperty(c.property_type), + title: String(c.title || ""), + description: String(c.description || ""), + rules: String(c.rules || ""), + place: String(c.place || ""), + mapUrl: String(c.mapUrl || ""), + price: Number.isFinite(toNum(c.price)) ? toNum(c.price).toFixed(6) : "0.000000", + rooms: toInt(c.rooms, 0), + size: nonNeg(c.size), + capacity: toInt(c.capacity, 0), + availableFrom: safeDate(c.availableFrom), + availableTo: safeDate(c.availableTo), + images: normalizeImages(c.images && c.images.length ? c.images : c.image), + image: normalizeImages(c.images && c.images.length ? c.images : c.image)[0] || null, + video: normalizeVideo(c.video), + author, + createdAt: c.createdAt || new Date(node.ts).toISOString(), + updatedAt: c.updatedAt || null, + status: String(c.status || "OPEN").toUpperCase() === "CLOSED" ? "CLOSED" : "OPEN", + tags: Array.isArray(c.tags) ? c.tags : normalizeTags(c.tags), + requests: visibleRequests, + requestCount: requests.length, + opinions: agg.opinions, + opinions_inhabitants: agg.opinions_inhabitants, + requestedByViewer: requests.includes(viewer), + everRequestedByViewer: (idx.everRequestedByRoot.get(rootId) || new Set()).has(viewer), + ratedByViewer: agg.opinions_inhabitants.includes(viewer), + visibility: String(c.visibility || "PUBLIC").toUpperCase() === "HIDDEN" ? "HIDDEN" : "PUBLIC" + } + } + + const buildContent = (data, opts = {}) => { + const housing_type = safeType(data.housing_type) + if (!housing_type) throw new Error("Invalid housing type") + + const title = String(data.title || "").trim() + if (!title) throw new Error("Invalid title") + const description = String(data.description || "").trim() + if (!description) throw new Error("Invalid description") + + const availableFrom = safeDate(data.availableFrom) + const availableTo = safeDate(data.availableTo) + if (!availableFrom) throw new Error("The start date is required") + if (opts.enforceFutureStart) { + const [y, m, d] = String(availableFrom).split("-").map(Number) + const startsAt = new Date(y, (m || 1) - 1, d || 1) + const today = new Date() + today.setHours(0, 0, 0, 0) + if (startsAt < today) throw new Error("The start date cannot be earlier than today") + } + if (availableTo && new Date(availableTo) < new Date(availableFrom)) { + throw new Error("The end date cannot be earlier than the start date") + } + + const images = normalizeImages(data.images !== undefined ? data.images : data.image) + const video = normalizeVideo(data.video) + + return { + type: "housing", + housing_type, + property_type: safeProperty(data.property_type), + title, + description, + rules: String(data.rules || "").trim(), + place: String(data.place || "").trim(), + mapUrl: String(data.mapUrl || "").trim(), + price: housing_type === "couchsurfing" ? "0.000000" : nonNeg(data.price).toFixed(6), + rooms: Math.max(0, toInt(data.rooms, 0)), + size: nonNeg(data.size), + capacity: Math.max(0, toInt(data.capacity, 0)), + availableFrom, + availableTo, + images, + image: images[0] || null, + video, + tags: normalizeTags(data.tags), + visibility: String(data.visibility || "PUBLIC").toUpperCase() === "HIDDEN" ? "HIDDEN" : "PUBLIC" + } + } + + return { + type: "housing", + HOUSING_TYPES, + PROPERTY_TYPES, + MAX_IMAGES, + + async createHousing(data) { + const ssbClient = await openSsb() + const content = { + ...buildContent(data, { enforceFutureStart: true }), + author: ssbClient.id, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + status: "OPEN", + opinions: {}, + opinions_inhabitants: [] + } + return publishHousing(ssbClient, content, null) + }, + + async resolveCurrentId(id) { + const ssbClient = await openSsb() + const idx = buildIndex(await readAll(ssbClient), ssbClient) + const tip = idx.tipOf(id) + if (idx.tomb.has(tip) || idx.vg.isForeignCopy(idx.rootOf(tip))) throw new Error("Housing not found") + return tip + }, + + async resolveRootId(id) { + const ssbClient = await openSsb() + const idx = buildIndex(await readAll(ssbClient), ssbClient) + const tip = idx.tipOf(id) + if (idx.tomb.has(tip) || idx.vg.isForeignCopy(idx.rootOf(tip))) throw new Error("Housing not found") + return idx.rootOf(tip) + }, + + async updateHousing(id, data) { + const ssbClient = await openSsb() + const idx = buildIndex(await readAll(ssbClient), ssbClient) + const tipId = idx.tipOf(id) + if (idx.tomb.has(tipId) || idx.vg.isForeignCopy(idx.rootOf(tipId))) throw new Error("Housing not found") + const node = idx.nodes.get(tipId) + if (!node || !node.c) throw new Error("Housing not found") + if ((node.author || node.c.author) !== ssbClient.id) throw new Error("Unauthorized") + + const rootId = idx.rootOf(tipId) + const current = decryptHousing(node.c, rootId) + if (current._undecryptable) throw new Error("Cannot decrypt this listing") + const startChanged = data.availableFrom !== undefined && safeDate(data.availableFrom) !== safeDate(current.availableFrom) + const merged = buildContent({ + housing_type: data.housing_type === undefined ? current.housing_type : data.housing_type, + property_type: data.property_type === undefined ? current.property_type : data.property_type, + title: data.title === undefined ? current.title : data.title, + description: data.description === undefined ? current.description : data.description, + rules: data.rules === undefined ? current.rules : data.rules, + place: data.place === undefined ? current.place : data.place, + mapUrl: data.mapUrl === undefined ? current.mapUrl : data.mapUrl, + price: data.price === undefined ? current.price : data.price, + rooms: data.rooms === undefined ? current.rooms : data.rooms, + size: data.size === undefined ? current.size : data.size, + capacity: data.capacity === undefined ? current.capacity : data.capacity, + availableFrom: data.availableFrom === undefined ? current.availableFrom : data.availableFrom, + availableTo: data.availableTo === undefined ? current.availableTo : data.availableTo, + images: data.images === undefined + ? (current.images && current.images.length ? current.images : current.image) + : data.images, + video: data.video === undefined ? current.video : data.video, + tags: data.tags === undefined ? current.tags : data.tags, + visibility: data.visibility === undefined ? current.visibility : data.visibility + }, { enforceFutureStart: startChanged }) + + let status = String(current.status || "OPEN").toUpperCase() + if (data.status !== undefined) { + const s = String(data.status || "").toUpperCase() + if (!["OPEN", "CLOSED"].includes(s)) throw new Error("Invalid status") + status = s + } + + const next = { + ...merged, + status, + author: current.author, + createdAt: current.createdAt, + updatedAt: new Date().toISOString(), + opinions: current.opinions || {}, + opinions_inhabitants: safeArr(current.opinions_inhabitants), + replaces: tipId + } + + const tomb = { type: "tombstone", target: tipId, deletedAt: new Date().toISOString(), author: ssbClient.id } + await new Promise((res, rej) => ssbClient.publish(tomb, (e) => e ? rej(e) : res())) + return publishHousing(ssbClient, next, rootId) + }, + + async updateHousingStatus(id, status) { + return this.updateHousing(id, { status: String(status || "").toUpperCase() }) + }, + + async deleteHousing(id) { + const ssbClient = await openSsb() + const idx = buildIndex(await readAll(ssbClient), ssbClient) + const tipId = idx.tipOf(id) + if (idx.tomb.has(tipId) || idx.vg.isForeignCopy(idx.rootOf(tipId))) throw new Error("Housing not found") + const node = idx.nodes.get(tipId) + if (!node) throw new Error("Housing not found") + if ((node.author || node.c.author) !== ssbClient.id) throw new Error("Unauthorized") + const tomb = { type: "tombstone", target: tipId, deletedAt: new Date().toISOString(), author: ssbClient.id } + return new Promise((res, rej) => ssbClient.publish(tomb, (e, r) => e ? rej(e) : res(r))) + }, + + async requestHousing(id) { + const ssbClient = await openSsb() + const me = ssbClient.id + const item = await this.getHousingById(id) + if (item.author === me) throw new Error("Cannot request your own listing") + if (item.status !== "OPEN") throw new Error("This listing is closed") + if (safeArr(item.requests).includes(me)) return { alreadyRequested: true } + const msg = { type: "housingRequest", housingId: item.rootId, value: true, createdAt: new Date().toISOString() } + return new Promise((res, rej) => ssbClient.private.publish(msg, [me, item.author], (e, m) => e ? rej(e) : res(m))) + }, + + async cancelRequest(id) { + const ssbClient = await openSsb() + const me = ssbClient.id + const item = await this.getHousingById(id) + if (item.author === me) throw new Error("Cannot cancel a request on your own listing") + if (!safeArr(item.requests).includes(me)) return { notRequested: true } + const msg = { type: "housingRequest", housingId: item.rootId, value: false, createdAt: new Date().toISOString() } + return new Promise((res, rej) => ssbClient.private.publish(msg, [me, item.author], (e, m) => e ? rej(e) : res(m))) + }, + + async createOpinion(id, category) { + if (!categories.includes(category)) throw new Error("Invalid category") + const ssbClient = await openSsb() + const me = ssbClient.id + const idx = buildIndex(await readAll(ssbClient), ssbClient) + const tipId = idx.tipOf(id) + if (idx.tomb.has(tipId) || idx.vg.isForeignCopy(idx.rootOf(tipId))) throw new Error("Housing not found") + const node = idx.nodes.get(tipId) + if (!node) throw new Error("Housing not found") + const rootId = idx.rootOf(tipId) + const owner = node.author || node.c.author + if (owner === me) throw new Error("You cannot rate your own listing") + + const agg = idx.aggregateFor(rootId, decryptHousing(node.c, rootId), owner) + if (agg.opinions_inhabitants.includes(me)) throw new Error("Already voted") + + const everRequested = idx.everRequestedByRoot.get(rootId) || new Set() + if (!everRequested.has(me)) throw new Error("You can rate only a place you have requested") + + const content = { type: "housingOpinion", target: rootId, category, createdAt: new Date().toISOString() } + return new Promise((res, rej) => ssbClient.publish(content, (e, m) => e ? rej(e) : res(m))) + }, + + async listHousing(filter = "ALL", viewerId = null, query = {}) { + const ssbClient = await openSsb() + const viewer = viewerId || ssbClient.id + const idx = buildIndex(await readAll(ssbClient), ssbClient) + + const items = [] + for (const [rootId, tipId] of idx.tipByRoot.entries()) { + if (idx.tomb.has(tipId)) continue + const node = idx.nodes.get(tipId) + if (!node) continue + const item = buildObject(node, rootId, idx, viewer) + if (!item) continue + if (item.visibility === "HIDDEN" && item.author !== viewer) continue + items.push(item) + } + + const F = String(filter || "ALL").toUpperCase() + let list = dedupeByPreferring( + items, + (h) => (h.author && h.createdAt) ? [norm(h.author), norm(h.createdAt), norm(h.title)].join("|") : null, + (h) => h.requestCount + ) + + if (F === "MINE") list = list.filter(h => h.author === viewer) + else if (F === "REQUESTED") list = list.filter(h => safeArr(h.requests).includes(viewer)) + else if (HOUSING_TYPES.includes(norm(F))) list = list.filter(h => h.housing_type === norm(F)) + else if (F === "OPEN") list = list.filter(h => h.status === "OPEN") + else if (F === "CLOSED") list = list.filter(h => h.status === "CLOSED") + else if (F === "RECENT") list = list.filter(h => moment(h.createdAt).isAfter(moment().subtract(24, "hours"))) + + const search = String(query.search || query.q || "").trim() + if (search) list = list.filter(h => matchSearch(h, search)) + + const minP = toNum(query.minPrice) + const maxP = toNum(query.maxPrice) + if (Number.isFinite(minP)) list = list.filter(h => toNum(h.price) >= minP) + if (Number.isFinite(maxP)) list = list.filter(h => toNum(h.price) <= maxP) + + const place = String(query.place || "").trim() + if (place) list = list.filter(h => norm(h.place).includes(norm(place))) + + const ratingOf = (h) => safeArr(h.opinions_inhabitants).length + const sort = String(query.sort || "").trim() + if (F === "TOP" || sort === "rating") list.sort((a, b) => ratingOf(b) - ratingOf(a)) + else if (sort === "price") list.sort((a, b) => toNum(a.price) - toNum(b.price)) + else if (sort === "requests") list.sort((a, b) => b.requestCount - a.requestCount) + else list.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)) + + return list + }, + + async getHousingById(id, viewerId = null) { + const ssbClient = await openSsb() + const viewer = viewerId || ssbClient.id + const idx = memoIndex('housing', await readAll(ssbClient), (m) => buildIndex(m, ssbClient)) + const tipId = idx.tipOf(id) + if (idx.tomb.has(tipId) || idx.vg.isForeignCopy(idx.rootOf(tipId))) throw new Error("Housing not found") + const node = idx.nodes.get(tipId) + if (!node) throw new Error("Housing not found") + const rootId = idx.rootOf(tipId) + const item = buildObject(node, rootId, idx, viewer) + if (!item) throw new Error("Housing not found") + if (item.visibility === "HIDDEN" && item.author !== viewer) throw new Error("Housing not found") + return item + } + } +} diff --git a/src/models/images_model.js b/src/models/images_model.js index 50c90e3..e9a38e1 100644 --- a/src/models/images_model.js +++ b/src/models/images_model.js @@ -2,7 +2,8 @@ const pull = require("../server/node_modules/pull-stream"); const { getConfig } = require("../configs/config-manager.js"); const categories = require("../backend/opinion_categories"); const { buildValidatedTombstoneSet } = require('./tombstone_validator'); -const { dedupeBy, norm } = require('../backend/dedupe'); +const { readTyped, memoIndex } = require('./typed_log'); +const { dedupeBy, norm, versionGroups } = require('../backend/dedupe'); const logLimit = getConfig().ssbLogStream?.limit || 1000; @@ -31,13 +32,9 @@ module.exports = ({ cooler }) => { return ssb; }; - const getAllMessages = async (ssbClient) => - new Promise((resolve, reject) => { - pull( - ssbClient.createLogStream({ limit: logLimit }), - pull.collect((err, msgs) => (err ? reject(err) : resolve(msgs))) - ); - }); + const IMAGE_TYPES = ["image", "imageOpinion", "tombstone"]; + + const getAllMessages = async (ssbClient) => readTyped(ssbClient, IMAGE_TYPES, { limit: logLimit }); const getMsg = async (ssbClient, key) => new Promise((resolve, reject) => { @@ -47,9 +44,6 @@ module.exports = ({ cooler }) => { const buildIndex = (messages) => { const tomb = buildValidatedTombstoneSet(messages); const nodes = new Map(); - const parent = new Map(); - const child = new Map(); - const strictChild = new Map(); const opinionMsgs = []; for (const m of messages) { @@ -65,48 +59,16 @@ module.exports = ({ cooler }) => { const ts = v.timestamp || m.timestamp || 0; let sizeBytes = 0; try { sizeBytes = Buffer.byteLength(JSON.stringify(v), "utf8"); } catch (_) { sizeBytes = 0; } - nodes.set(k, { key: k, ts, c, sizeBytes, author: v.author }); - - if (c.replaces) { - parent.set(k, c.replaces); - child.set(c.replaces, k); - } + nodes.set(k, { key: k, ts, seq: v.sequence || 0, c, sizeBytes, author: v.author }); } - for (const [k, t] of Array.from(parent.entries())) { - const cn = nodes.get(k); - const pn = nodes.get(t); - if (!pn) { parent.delete(k); if (child.get(t) === k) child.delete(t); continue; } - if (!cn || String(cn.author) !== String(pn.author)) { parent.delete(k); if (child.get(t) === k) child.delete(t); nodes.delete(k); } - } - - for (const [k, node] of nodes) { - const t = node.c.replaces; - if (t) { const orig = nodes.get(t); if (orig && orig.author === node.author) strictChild.set(t, k); } - } - - const rootOf = (id) => { - let cur = id, g = 0; - while (parent.has(cur) && nodes.has(parent.get(cur)) && g++ < 100000) cur = parent.get(cur); - return cur; - }; - - const contentTipOf = (root) => { - let cur = root, g = 0; - while (strictChild.has(cur) && g++ < 100000) cur = strictChild.get(cur); - const n = nodes.get(cur), rn = nodes.get(root); - return (n && rn && n.author === rn.author) ? cur : root; - }; - - const roots = new Set(); - for (const id of nodes.keys()) roots.add(rootOf(id)); + const vg = versionGroups([...nodes.values()].map(n => ({ key: n.key, author: n.author, seq: n.seq, ts: n.ts, replaces: n.c.replaces || null, claimedAuthor: typeof n.c.author === "string" ? n.c.author : null }))); + const rootOf = vg.rootOf; + const contentTipOf = vg.tipOf; const opinionsByRoot = new Map(); for (const op of opinionMsgs) { if (!nodes.has(op.target)) continue; const r = rootOf(op.target); if (!opinionsByRoot.has(r)) opinionsByRoot.set(r, []); opinionsByRoot.get(r).push(op); } - const forward = new Map(); - for (const [newId, oldId] of parent.entries()) forward.set(oldId, newId); - const resolveGroup = (root) => { const contentTip = contentTipOf(root); const contentNode = nodes.get(contentTip) || nodes.get(root); @@ -123,9 +85,9 @@ module.exports = ({ cooler }) => { }; const tipByRoot = new Map(); - for (const r of roots) tipByRoot.set(r, contentTipOf(r)); + for (const r of vg.roots) tipByRoot.set(r, contentTipOf(r)); - return { tomb, nodes, parent, child, rootOf, tipByRoot, forward, resolveGroup }; + return { tomb, nodes, rootOf, tipByRoot, resolveGroup }; }; const buildImage = (node, rootId, viewerId, agg) => { @@ -141,8 +103,8 @@ module.exports = ({ cooler }) => { author: node.author, title: c.title || "", description: c.description || "", + license: c.license || "", mapUrl: c.mapUrl || "", - meme: !!c.meme, opinions: agg ? agg.opinions : (c.opinions || {}), opinions_inhabitants: voters, hasVoted: viewerId ? voters.includes(viewerId) : false, @@ -168,16 +130,12 @@ module.exports = ({ cooler }) => { const messages = await getAllMessages(ssbClient); const idx = buildIndex(messages); - let tip = id; - while (idx.forward.has(tip)) tip = idx.forward.get(tip); - if (idx.tomb.has(tip)) throw new Error("Image not found"); - - let root = tip; - while (idx.parent.has(root)) root = idx.parent.get(root); + const root = idx.rootOf(id); + if (idx.tomb.has(idx.tipByRoot.get(root) || id)) throw new Error("Image not found"); return root; }, - async createImage(blobMarkdown, tagsRaw, title, description, memeBool, mapUrl) { + async createImage(blobMarkdown, tagsRaw, title, description, mapUrl, license) { const ssbClient = await openSsb(); const blobId = parseBlobId(blobMarkdown); const tags = normalizeTags(tagsRaw) || []; @@ -193,9 +151,9 @@ module.exports = ({ cooler }) => { title: title || "", description: description || "", mapUrl: mapUrl || "", - meme: !!memeBool, opinions: {}, - opinions_inhabitants: [] + opinions_inhabitants: [], + ...(license ? { license } : {}) }; return new Promise((resolve, reject) => { @@ -203,7 +161,7 @@ module.exports = ({ cooler }) => { }); }, - async updateImageById(id, blobMarkdown, tagsRaw, title, description, memeBool, mapUrl) { + async updateImageById(id, blobMarkdown, tagsRaw, title, description, mapUrl, license) { const ssbClient = await openSsb(); const userId = ssbClient.id; const tipId = await this.resolveCurrentId(id); @@ -226,9 +184,9 @@ module.exports = ({ cooler }) => { title: title !== undefined ? title || "" : oldMsg.content.title || "", description: description !== undefined ? description || "" : oldMsg.content.description || "", mapUrl: mapUrl !== undefined ? mapUrl || "" : oldMsg.content.mapUrl || "", - meme: typeof memeBool === "boolean" ? memeBool : !!oldMsg.content.meme, createdAt: oldMsg.content.createdAt, - updatedAt: now + updatedAt: now, + ...(license !== undefined ? { license } : {}) }; const tombstone = { type: "tombstone", target: tipId, deletedAt: now, author: userId }; @@ -280,7 +238,6 @@ module.exports = ({ cooler }) => { if (filter === "mine") list = list.filter((im) => String(im.author) === String(viewerId)); else if (filter === "recent") list = list.filter((im) => new Date(im.createdAt).getTime() >= now - 86400000); - else if (filter === "meme") list = list.filter((im) => im.meme === true); else if (filter === "top") { list = list .slice() @@ -315,10 +272,10 @@ module.exports = ({ cooler }) => { const viewer = viewerId || ssbClient.id; const messages = await getAllMessages(ssbClient); - const idx = buildIndex(messages); + const idx = memoIndex('images', messages, buildIndex); const rootId = idx.rootOf(id); const agg = idx.resolveGroup(rootId); - if (!agg.contentNode || idx.tomb.has(agg.contentTip)) throw new Error("Image not found"); + if (!idx.tipByRoot.has(rootId) || !agg.contentNode || idx.tomb.has(agg.contentTip)) throw new Error("Image not found"); return buildImage(agg.contentNode, rootId, viewer, agg); }, diff --git a/src/models/industry_model.js b/src/models/industry_model.js new file mode 100644 index 0000000..5593924 --- /dev/null +++ b/src/models/industry_model.js @@ -0,0 +1,992 @@ +const pull = require("../server/node_modules/pull-stream") +const { getConfig } = require("../configs/config-manager.js") +const { buildValidatedTombstoneSet } = require('./tombstone_validator') +const { readTyped } = require('./typed_log') +const logLimit = (getConfig().ssbLogStream && getConfig().ssbLogStream.limit) || 1000 + +module.exports = ({ cooler }) => { + let ssb + const openSsb = async () => { + if (!ssb) ssb = await cooler.open() + return ssb + } + + const TYPE = "industry" + const SECTORS = ["software", "hardware", "agriculture", "textile", "energy", "food", "construction", "media", "services", "other"] + const POLICIES = ["open", "vote", "invite"] + + const clampPercent = (n) => { + const x = parseInt(n, 10) + if (!Number.isFinite(x)) return 0 + return Math.max(0, Math.min(100, x)) + } + const clampMajority = (n) => { + const x = parseFloat(n) + if (!Number.isFinite(x)) return 0.5 + return Math.max(0.5, Math.min(1, x)) + } + const clampQuorum = (n) => { + const x = parseInt(n, 10) + if (!Number.isFinite(x) || x < 1) return 1 + return Math.min(100000, x) + } + const safeSector = (s) => { + const v = String(s || "").toLowerCase().trim() + return SECTORS.includes(v) ? v : "other" + } + const safePolicy = (p) => { + const v = String(p || "").toLowerCase().trim() + return POLICIES.includes(v) ? v : "vote" + } + const nonNegNum = (v) => { + const x = parseFloat(v) + return Number.isFinite(x) && x >= 0 ? x : 0 + } + + const INDUSTRY_TYPES = [TYPE, "industryMember", "industryInvite", "industryApply", "industryVote", "industryOpinion", "industryBlueprint", "industryBuild", "industryBuildStatus", "industryBuildDates", "industryBuildUpdate", "industryContribution", "industryAllocation", "tombstone"] + + async function getAllMsgs(ssbClient) { + return readTyped(ssbClient, INDUSTRY_TYPES, { limit: logLimit }) + } + + function chainMaps(nodeMap) { + const next = new Map(), prev = new Map(), strictNext = new Map() + for (const [key, n] of nodeMap) { + const t = n.c.replaces + if (!t) continue + next.set(t, key); prev.set(key, t) + const orig = nodeMap.get(t) + if (orig && orig.author === n.author) strictNext.set(t, key) + } + return { next, prev, strictNext } + } + + function buildIndex(messages) { + const tomb = buildValidatedTombstoneSet(messages) + const nodes = new Map() + const blueprints = new Map() + const builds = new Map() + const collab = { member: [], invite: [], apply: [], vote: [], opinion: [], buildStatus: [], buildDates: [], contribution: [], allocation: [] } + for (const m of messages) { + const c = m && m.value && m.value.content + if (!c) continue + if (c.type === "tombstone") continue + const ts = (m.value && m.value.timestamp) || 0 + const author = m.value && m.value.author + if (c.type === "industryMember") { collab.member.push({ target: c.target, author, action: c.action === "leave" ? "leave" : "join", ts }); continue } + if (c.type === "industryInvite") { collab.invite.push({ target: c.target, author, invitee: c.invitee, ts }); continue } + if (c.type === "industryApply") { collab.apply.push({ target: c.target, author, ts }); continue } + if (c.type === "industryVote") { collab.vote.push({ target: c.target, author, subject: String(c.subject || ""), ref: String(c.ref || ""), choice: c.choice === "no" ? "no" : "yes", ts }); continue } + if (c.type === "industryOpinion") { collab.opinion.push({ target: c.target, author, category: c.category, ts }); continue } + if (c.type === "industryBlueprint") { blueprints.set(m.key, { key: m.key, ts, c, author }); continue } + if (c.type === "industryBuild") { builds.set(m.key, { key: m.key, ts, c, author }); continue } + if (c.type === "industryBuildStatus") { collab.buildStatus.push({ target: c.target, author, status: String(c.status || "").toUpperCase(), ts }); continue } + if (c.type === "industryBuildDates" || c.type === "industryBuildUpdate") { collab.buildDates.push({ target: c.target, author, title: String(c.title || ""), notes: String(c.notes || ""), startDate: String(c.startDate || ""), endDate: String(c.endDate || ""), ts }); continue } + if (c.type === "industryContribution") { collab.contribution.push({ target: c.target, author, kind: c.kind, hours: nonNegNum(c.hours), item: String(c.item || ""), value: nonNegNum(c.value), eco: nonNegNum(c.eco), transferId: c.transferId || null, note: String(c.note || ""), ts }); continue } + if (c.type === "industryAllocation") { collab.allocation.push({ target: c.target, author, shares: (c.shares && typeof c.shares === "object") ? c.shares : {}, amounts: (c.amounts && typeof c.amounts === "object") ? c.amounts : {}, pot: nonNegNum(c.pot), outputValue: nonNegNum(c.outputValue), disposition: c.disposition || null, ts }); continue } + if (c.type !== TYPE) continue + nodes.set(m.key, { key: m.key, ts, c, author }) + } + const fc = chainMaps(nodes) + const bp = chainMaps(blueprints) + return { tomb, nodes, blueprints, builds, collab, naiveNext: fc.next, naivePrev: fc.prev, strictNext: fc.strictNext, bpNext: bp.next, bpPrev: bp.prev, bpStrictNext: bp.strictNext } + } + + const rootOfIdx = (idx, key) => { let x = key, g = 0; while (idx.naivePrev.has(x) && idx.nodes.has(idx.naivePrev.get(x)) && g++ < 100000) x = idx.naivePrev.get(x); return x } + const rootOfBlueprint = (idx, key) => { let x = key, g = 0; while (idx.bpPrev.has(x) && idx.blueprints.has(idx.bpPrev.get(x)) && g++ < 100000) x = idx.bpPrev.get(x); return x } + + function passesThreshold(yesCount, memberCount, quorum, majority) { + const size = Math.max(1, memberCount) + const need = Math.max(Math.min(clampQuorum(quorum), size), Math.ceil(size * clampMajority(majority))) + return yesCount >= need + } + + function actionVoteState(idx, groupRoot, members, subject, ref) { + const pref = new Map() + for (const v of idx.collab.vote.filter(x => x.target === groupRoot && x.subject === subject && x.ref === ref).sort((a, b) => a.ts - b.ts)) { + if (!members.has(v.author)) continue + pref.set(v.author, v.choice) + } + const voters = [...pref.entries()].filter(([, c]) => c === "yes").map(([m]) => m) + return { yes: voters.length, voters } + } + + function voteNeedOf(g) { + const size = Math.max(1, new Set(g.facility.members).size) + return Math.max(Math.min(clampQuorum(g.facility.quorum), size), Math.ceil(size * clampMajority(g.facility.majority))) + } + + function actionApproved(idx, g, subject, ref) { + const members = new Set(g.facility.members) + if (members.size <= 1) return true + const st = actionVoteState(idx, g.root, members, subject, ref) + return passesThreshold(st.yes, members.size, g.facility.quorum, g.facility.majority) + } + + function blueprintGovernance(idx, g, bpRoot) { + const members = new Set(g.facility.members) + const up = actionVoteState(idx, g.root, members, "bpUpdate", bpRoot) + const del = actionVoteState(idx, g.root, members, "bpDelete", bpRoot) + return { + root: bpRoot, + facilityMembers: members.size, + voteNeed: voteNeedOf(g), + updateYes: up.yes, + updateVoters: up.voters, + deleteYes: del.yes, + deleteVoters: del.voters, + updateApproved: actionApproved(idx, g, "bpUpdate", bpRoot), + deleteApproved: actionApproved(idx, g, "bpDelete", bpRoot) + } + } + + function buildGovernance(idx, g, buildId) { + const members = new Set(g.facility.members) + const up = actionVoteState(idx, g.root, members, "buildUpdate", buildId) + const del = actionVoteState(idx, g.root, members, "buildDelete", buildId) + return { + facilityMembers: members.size, + voteNeed: voteNeedOf(g), + updateYes: up.yes, + updateVoters: up.voters, + deleteYes: del.yes, + deleteVoters: del.voters, + updateApproved: actionApproved(idx, g, "buildUpdate", buildId), + deleteApproved: actionApproved(idx, g, "buildDelete", buildId) + } + } + + function resolveGroup(idx, root) { + const { nodes, collab, naiveNext, strictNext } = idx + const followStrict = (key) => { let x = key, g = 0; while (strictNext.has(x) && g++ < 100000) x = strictNext.get(x); return x } + const rootNode = nodes.get(root) + if (!rootNode) return null + const steward = rootNode.author + let tip = followStrict(root) + const tn = nodes.get(tip) + if (!tn || tn.author !== steward) tip = root + const best = nodes.get(tip) + if (!best) return null + + const groupKeys = [] + { let x = root, g = 0; groupKeys.push(x); while (naiveNext.has(x) && g++ < 100000) { x = naiveNext.get(x); groupKeys.push(x) } } + + const facility = { ...best.c, id: tip } + const policy = safePolicy(facility.membershipPolicy) + const quorum = clampQuorum(facility.quorum) + const majority = clampMajority(facility.majority) + + const KIND_ORDER = { invite: 0, apply: 1, member: 2, vote: 3 } + const events = [] + for (const e of collab.member.filter(x => x.target === root)) events.push({ kind: "member", ...e }) + for (const e of collab.invite.filter(x => x.target === root)) events.push({ kind: "invite", ...e }) + for (const e of collab.apply.filter(x => x.target === root)) events.push({ kind: "apply", ...e }) + for (const e of collab.vote.filter(x => x.target === root)) events.push({ kind: "vote", ...e }) + events.sort((a, b) => (a.ts - b.ts) || (KIND_ORDER[a.kind] - KIND_ORDER[b.kind])) + + const members = new Set([steward]) + const invites = new Set() + const applicants = new Set() + const admitVotes = new Map() + const dissolveVoters = new Set() + let dissolved = false + + const admitKey = (uid) => "admit:" + uid + for (const ev of events) { + if (ev.kind === "invite") { + if (members.has(ev.author) && ev.invitee) invites.add(ev.invitee) + continue + } + if (ev.kind === "apply") { + if (!members.has(ev.author)) applicants.add(ev.author) + continue + } + if (ev.kind === "member") { + if (ev.action === "leave") { if (ev.author !== steward) members.delete(ev.author); continue } + if (members.has(ev.author)) continue + if (policy === "open") { members.add(ev.author); applicants.delete(ev.author) } + else if (policy === "invite" && invites.has(ev.author)) { members.add(ev.author); applicants.delete(ev.author) } + continue + } + if (ev.kind === "vote") { + if (!members.has(ev.author)) continue + if (ev.subject === "admit" && ev.ref) { + const k = admitKey(ev.ref) + if (!admitVotes.has(k)) admitVotes.set(k, new Map()) + admitVotes.get(k).set(ev.author, ev.choice) + const yes = [...admitVotes.get(k).values()].filter(v => v === "yes").length + if (!members.has(ev.ref) && passesThreshold(yes, members.size, quorum, majority)) { + members.add(ev.ref); applicants.delete(ev.ref) + } + } else if (ev.subject === "dissolve") { + if (ev.choice === "yes") dissolveVoters.add(ev.author); else dissolveVoters.delete(ev.author) + if (passesThreshold(dissolveVoters.size, members.size, quorum, majority)) dissolved = true + } + continue + } + } + + const pendingApplicants = [...applicants].map(uid => { + const k = admitKey(uid) + const vm = admitVotes.get(k) || new Map() + let yes = 0, no = 0 + for (const v of vm.values()) { if (v === "yes") yes++; else no++ } + return { id: uid, yes, no, voters: [...vm.keys()] } + }) + + const opinions = {} + const opinionSet = new Set() + for (const k of groupKeys) { + const n = nodes.get(k); if (!n || n.author !== steward) continue + const c = n.c + if (c.opinions && typeof c.opinions === "object") { for (const kk of Object.keys(c.opinions)) opinions[kk] = Math.max(opinions[kk] || 0, Number(c.opinions[kk]) || 0) } + for (const v of (Array.isArray(c.opinions_inhabitants) ? c.opinions_inhabitants : [])) opinionSet.add(v) + } + for (const op of collab.opinion.filter(x => x.target === root)) { if (!opinionSet.has(op.author)) { opinionSet.add(op.author); opinions[op.category] = (opinions[op.category] || 0) + 1 } } + + const pausePref = new Map() + for (const ev of events) { + if (ev.kind !== "vote" || ev.subject !== "pause") continue + if (!members.has(ev.author)) continue + pausePref.set(ev.author, ev.choice) + } + const pauseVoters = [...pausePref.entries()].filter(([, c]) => c === "yes").map(([m]) => m) + const paused = passesThreshold(pauseVoters.length, members.size, quorum, majority) + + facility.steward = steward + facility.root = root + facility.membershipPolicy = policy + facility.quorum = quorum + facility.majority = majority + facility.members = [...members] + facility.memberCount = members.size + facility.pendingApplicants = pendingApplicants + facility.invites = [...invites] + facility.opinions = opinions + facility.opinions_inhabitants = [...opinionSet] + facility.pauseVoters = pauseVoters + facility.pauseYes = pauseVoters.length + facility.status = dissolved ? "DISSOLVED" : (paused ? "PAUSED" : "ACTIVE") + + return { tip, root, best, facility, tombstoned: idx.tomb.has(tip) } + } + + const BUILD_STATUSES = ["PROPOSED", "REJECTED", "APPROVED", "STOCKING", "IN_PRODUCTION", "COMPLETED", "FAILED"] + const BUILD_TRANSITIONS = { APPROVED: ["STOCKING", "FAILED"], STOCKING: ["IN_PRODUCTION", "FAILED"], IN_PRODUCTION: ["COMPLETED", "FAILED"], FAILED: ["APPROVED", "STOCKING", "IN_PRODUCTION"], COMPLETED: ["IN_PRODUCTION"] } + const CONTRIBUTABLE = new Set(["APPROVED", "STOCKING", "IN_PRODUCTION"]) + + const normalizeMaterials = (raw, text) => { + if (Array.isArray(raw)) return raw.map(m => ({ item: String((m && m.item) || "").trim(), qty: nonNegNum(m && m.qty), price: nonNegNum(m && m.price) })).filter(m => m.item) + const t = String(text || "") + if (!t.trim()) return [] + return t.split("\n").map(line => { const parts = line.split(/[:,]/).map(x => String(x || "").trim()); return { item: parts[0] || "", qty: nonNegNum(parts[1]), price: nonNegNum(parts[2]) } }).filter(m => m.item) + } + + const safeMaterials = (v) => (Array.isArray(v) ? v : []) + const estimateBlueprint = (bp, laborRate) => { + const materialsCost = safeMaterials(bp.materials).reduce((s, m) => s + nonNegNum(m.qty) * nonNegNum(m.price), 0) + const laborCost = nonNegNum(bp.laborHours) * nonNegNum(laborRate) + return { + estMaterialsCost: Number(materialsCost.toFixed(6)), + estLaborCost: Number(laborCost.toFixed(6)), + estTotal: Number((materialsCost + laborCost).toFixed(6)) + } + } + const estimateForBlueprintRef = (idx, bpRef, laborRate) => { + if (!bpRef) return null + const b = resolveBlueprint(idx, rootOfBlueprint(idx, bpRef)) + if (!b || b.tombstoned) return null + return { blueprintName: b.blueprint.name, blueprintImage: b.blueprint.image || null, blueprintKind: b.blueprint.outKind || null, blueprintLicense: b.blueprint.license || "copyleft", ...estimateBlueprint(b.blueprint, laborRate) } + } + const normalizeSkills = (raw) => { + if (Array.isArray(raw)) return raw.map(x => String(x || "").trim()).filter(Boolean) + return String(raw || "").split(",").map(x => x.trim()).filter(Boolean) + } + + function computeShares(contributions, laborRate) { + const rate = nonNegNum(laborRate) + const points = new Map() + for (const c of contributions) { + let p = 0 + if (c.kind === "labor") p = (c.hours || 0) * rate + else if (c.kind === "material") p = (c.value || 0) + else if (c.kind === "eco") p = (c.eco || 0) + if (p <= 0) continue + points.set(c.author, (points.get(c.author) || 0) + p) + } + let total = 0 + for (const v of points.values()) total += v + const shares = new Map() + if (total > 0) for (const [k, v] of points) shares.set(k, v / total) + return { points, total, shares } + } + + function resolveBlueprint(idx, root) { + const { blueprints, bpStrictNext } = idx + const followStrict = (key) => { let x = key, g = 0; while (bpStrictNext.has(x) && g++ < 100000) x = bpStrictNext.get(x); return x } + const rootNode = blueprints.get(root) + if (!rootNode) return null + const author = rootNode.author + let tip = followStrict(root) + const tn = blueprints.get(tip) + if (!tn || tn.author !== author) tip = root + const best = blueprints.get(tip) + if (!best) return null + return { tip, root, blueprint: { ...best.c, id: tip, author }, tombstoned: idx.tomb.has(tip) } + } + + function resolveBuild(idx, root, facility) { + const node = idx.builds.get(root) + if (!node) return null + const build = { ...node.c, id: root, proposer: node.author } + const steward = facility.steward + const members = new Set(facility.members) + const approveVotes = new Map() + for (const v of idx.collab.vote.filter(x => x.subject === "build" && x.ref === root).sort((a, b) => a.ts - b.ts)) { + if (!members.has(v.author)) continue + approveVotes.set(v.author, v.choice) + } + const approveYes = [...approveVotes.values()].filter(x => x === "yes").length + const approveNo = [...approveVotes.values()].filter(x => x === "no").length + let status = "PROPOSED" + if (passesThreshold(approveYes, members.size, facility.quorum, facility.majority)) status = "APPROVED" + else if (passesThreshold(approveNo, members.size, facility.quorum, facility.majority)) status = "REJECTED" + for (const ev of idx.collab.buildStatus.filter(x => x.target === root).sort((a, b) => a.ts - b.ts)) { + const allowed = BUILD_TRANSITIONS[status] || [] + if (!allowed.includes(ev.status)) continue + const isSteward = ev.author === steward + const isProposer = ev.author === build.proposer + const stewardOnly = ev.status === "COMPLETED" || ev.status === "FAILED" + if (stewardOnly && !isSteward) continue + if (!isSteward && !isProposer) continue + status = ev.status + } + for (const ev of idx.collab.buildDates.filter(x => x.target === root).sort((a, b) => a.ts - b.ts)) { + if (ev.author !== steward && ev.author !== build.proposer) continue + if (ev.title) build.title = ev.title + if (ev.notes) build.notes = ev.notes + if (ev.startDate) build.startDate = ev.startDate + if (ev.endDate) build.endDate = ev.endDate + } + build.status = status + const contributions = idx.collab.contribution.filter(x => x.target === root && members.has(x.author)).sort((a, b) => a.ts - b.ts) + build.contributions = contributions.map(c => ({ author: c.author, kind: c.kind, hours: c.hours, item: c.item, value: c.value, eco: c.eco, note: c.note, at: new Date(c.ts || 0).toISOString() })) + const cs = computeShares(contributions, facility.laborRate) + build.points = Object.fromEntries(cs.points) + build.totalPoints = cs.total + build.shares = Object.fromEntries(cs.shares) + build.treasury = contributions.reduce((s, c) => s + (c.eco || 0), 0) + const allocs = idx.collab.allocation.filter(x => x.target === root && x.author === steward).sort((a, b) => a.ts - b.ts) + const latest = allocs[allocs.length - 1] || null + build.distributed = !!latest + build.allocation = latest ? { shares: latest.shares, amounts: latest.amounts, pot: latest.pot, outputValue: latest.outputValue, disposition: latest.disposition, at: new Date(latest.ts || 0).toISOString() } : null + build.tombstoned = idx.tomb.has(root) + return build + } + + async function getById(id) { + const ssbClient = await openSsb() + const idx = buildIndex(await getAllMsgs(ssbClient)) + const g = resolveGroup(idx, rootOfIdx(idx, id)) + if (!g || g.tombstoned) throw new Error("Facility not found") + return g.facility + } + + async function resolveTipId(id) { + const ssbClient = await openSsb() + const idx = buildIndex(await getAllMsgs(ssbClient)) + const g = resolveGroup(idx, rootOfIdx(idx, id)) + if (!g || g.tombstoned) throw new Error("Facility not found") + return g.tip + } + + const CONTENT_COLLAB_FIELDS = ["id", "root", "steward", "members", "memberCount", "pendingApplicants", "invites", "opinions", "opinions_inhabitants", "pauseVoters", "pauseYes"] + function stripCollab(content) { + const out = { ...content } + for (const f of CONTENT_COLLAB_FIELDS) delete out[f] + out.opinions = {} + out.opinions_inhabitants = [] + return out + } + + async function publishReplace(ssbClient, currentId, content) { + const tomb = { type: "tombstone", target: currentId, deletedAt: new Date().toISOString(), author: ssbClient.id } + const updated = { ...stripCollab(content), type: TYPE, replaces: currentId, updatedAt: new Date().toISOString() } + await new Promise((res, rej) => ssbClient.publish(tomb, (e) => (e ? rej(e) : res()))) + return new Promise((res, rej) => ssbClient.publish(updated, (e, m) => (e ? rej(e) : res(m)))) + } + + const publish = (ssbClient, content) => new Promise((res, rej) => ssbClient.publish(content, (e, m) => (e ? rej(e) : res(m)))) + + return { + type: TYPE, + SECTORS, + POLICIES, + + async createFacility(data) { + const ssbClient = await openSsb() + const content = { + type: TYPE, + name: String(data.name || "").trim(), + sector: safeSector(data.sector), + description: String(data.description || "").trim(), + image: data.image || null, + mapUrl: String(data.mapUrl || "").trim(), + membershipPolicy: safePolicy(data.membershipPolicy), + quorum: clampQuorum(data.quorum), + majority: clampMajority(data.majority), + laborRate: nonNegNum(data.laborRate), + license: String(data.license || "copyleft").trim() || "copyleft", + tags: normalizeSkills(data.tags), + status: "ACTIVE", + author: ssbClient.id, + createdAt: new Date().toISOString(), + updatedAt: null, + opinions: {}, + opinions_inhabitants: [] + } + if (!content.name) throw new Error("Facility name is required") + return publish(ssbClient, content) + }, + + async updateFacility(id, patch) { + const ssbClient = await openSsb() + const idx = buildIndex(await getAllMsgs(ssbClient)) + const g = resolveGroup(idx, rootOfIdx(idx, id)) + if (!g || g.tombstoned) throw new Error("Facility not found") + if (g.facility.steward !== ssbClient.id) throw new Error("Unauthorized") + const current = g.facility + const nextStatus = patch.status === undefined ? current.status : String(patch.status || "").toUpperCase() + if (nextStatus === "DISSOLVED" && current.status !== "DISSOLVED") throw new Error("Dissolution requires a governance vote") + const updated = { + ...current, + ...patch, + name: patch.name === undefined ? current.name : String(patch.name || "").trim(), + sector: patch.sector === undefined ? current.sector : safeSector(patch.sector), + description: patch.description === undefined ? current.description : String(patch.description || "").trim(), + image: patch.image === undefined ? current.image : (patch.image || null), + mapUrl: patch.mapUrl === undefined ? current.mapUrl : String(patch.mapUrl || "").trim(), + membershipPolicy: patch.membershipPolicy === undefined ? current.membershipPolicy : safePolicy(patch.membershipPolicy), + quorum: patch.quorum === undefined ? current.quorum : clampQuorum(patch.quorum), + majority: patch.majority === undefined ? current.majority : clampMajority(patch.majority), + laborRate: patch.laborRate === undefined ? current.laborRate : nonNegNum(patch.laborRate), + tags: patch.tags === undefined ? current.tags : normalizeSkills(patch.tags), + status: nextStatus + } + return publishReplace(ssbClient, current.id, updated) + }, + + async deleteFacility(id) { + const ssbClient = await openSsb() + const idx = buildIndex(await getAllMsgs(ssbClient)) + const g = resolveGroup(idx, rootOfIdx(idx, id)) + if (!g || g.tombstoned) throw new Error("Facility not found") + if (g.facility.steward !== ssbClient.id) throw new Error("Unauthorized") + if (g.facility.memberCount > 1) throw new Error("A facility with members cannot be deleted; it must be dissolved by vote") + const tomb = { type: "tombstone", target: g.tip, deletedAt: new Date().toISOString(), author: ssbClient.id } + return publish(ssbClient, tomb) + }, + + async joinFacility(id) { + const ssbClient = await openSsb() + const idx = buildIndex(await getAllMsgs(ssbClient)) + const g = resolveGroup(idx, rootOfIdx(idx, id)) + if (!g || g.tombstoned) throw new Error("Facility not found") + if (g.facility.status === "DISSOLVED") throw new Error("Facility is dissolved") + const uid = ssbClient.id + if (g.facility.members.includes(uid)) throw new Error("Already a member") + const policy = g.facility.membershipPolicy + if (policy === "vote") return publish(ssbClient, { type: "industryApply", target: g.root, createdAt: new Date().toISOString() }) + if (policy === "invite" && !g.facility.invites.includes(uid)) throw new Error("You need an invitation to join") + return publish(ssbClient, { type: "industryMember", target: g.root, action: "join", createdAt: new Date().toISOString() }) + }, + + async leaveFacility(id) { + const ssbClient = await openSsb() + const idx = buildIndex(await getAllMsgs(ssbClient)) + const g = resolveGroup(idx, rootOfIdx(idx, id)) + if (!g || g.tombstoned) throw new Error("Facility not found") + const uid = ssbClient.id + if (uid === g.facility.steward) throw new Error("The steward cannot leave; dissolve the facility instead") + if (!g.facility.members.includes(uid)) throw new Error("Not a member") + return publish(ssbClient, { type: "industryMember", target: g.root, action: "leave", createdAt: new Date().toISOString() }) + }, + + async inviteToFacility(id, invitee) { + const ssbClient = await openSsb() + const idx = buildIndex(await getAllMsgs(ssbClient)) + const g = resolveGroup(idx, rootOfIdx(idx, id)) + if (!g || g.tombstoned) throw new Error("Facility not found") + if (!g.facility.members.includes(ssbClient.id)) throw new Error("Only members can invite") + if (!/^@[A-Za-z0-9+/]+={0,2}\.ed25519$/.test(String(invitee || ""))) throw new Error("Invalid invitee id") + return publish(ssbClient, { type: "industryInvite", target: g.root, invitee: String(invitee), createdAt: new Date().toISOString() }) + }, + + async voteGovernance(id, subject, ref, choice) { + const ssbClient = await openSsb() + const idx = buildIndex(await getAllMsgs(ssbClient)) + const g = resolveGroup(idx, rootOfIdx(idx, id)) + if (!g || g.tombstoned) throw new Error("Facility not found") + if (!g.facility.members.includes(ssbClient.id)) throw new Error("Only members can vote") + const subj = String(subject || "") + const SUBJECTS = ["admit", "dissolve", "pause", "bpUpdate", "bpDelete", "buildUpdate", "buildDelete"] + if (!SUBJECTS.includes(subj)) throw new Error("Invalid governance subject") + if (subj !== "admit" && subj !== "dissolve" && subj !== "pause" && !String(ref || "").trim()) throw new Error("Governance vote requires a target") + return publish(ssbClient, { type: "industryVote", target: g.root, subject: subj, ref: String(ref || ""), choice: choice === "no" ? "no" : "yes", createdAt: new Date().toISOString() }) + }, + + async createOpinion(id, category) { + const categories = require('../backend/opinion_categories') + if (!categories.includes(category)) throw new Error('Invalid opinion category') + const ssbClient = await openSsb() + const idx = buildIndex(await getAllMsgs(ssbClient)) + const g = resolveGroup(idx, rootOfIdx(idx, id)) + if (!g || g.tombstoned) throw new Error('Facility not found') + if ((g.facility.opinions_inhabitants || []).includes(ssbClient.id)) throw new Error('Already opined') + return publish(ssbClient, { type: 'industryOpinion', target: g.root, category, createdAt: new Date().toISOString() }) + }, + + async getFacilityById(id) { + return getById(id) + }, + + async listFacilities(filter = "ALL") { + const ssbClient = await openSsb() + const uid = ssbClient.id + const idx = buildIndex(await getAllMsgs(ssbClient)) + const roots = new Set() + for (const key of idx.nodes.keys()) roots.add(rootOfIdx(idx, key)) + let list = [] + for (const root of roots) { + const g = resolveGroup(idx, root) + if (!g || g.tombstoned) continue + list.push(g.facility) + } + const f = String(filter || "ALL").toUpperCase() + if (f === "MINE") list = list.filter(x => x.steward === uid) + else if (f === "RECENT") list = list.filter(x => (Date.parse(x.createdAt || "") || 0) >= Date.now() - 86400000) + else if (f === "MEMBER") list = list.filter(x => Array.isArray(x.members) && x.members.includes(uid)) + else if (f === "ACTIVE") list = list.filter(x => x.status === "ACTIVE") + else if (f === "PAUSED") list = list.filter(x => x.status === "PAUSED") + else if (f === "DISSOLVED") list = list.filter(x => x.status === "DISSOLVED") + else if (SECTORS.includes(f.toLowerCase())) list = list.filter(x => x.sector === f.toLowerCase()) + list.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)) + return list + }, + + async facilitiesOf(feedId) { + const ssbClient = await openSsb() + const uid = feedId || ssbClient.id + const idx = buildIndex(await getAllMsgs(ssbClient)) + const roots = new Set() + for (const key of idx.nodes.keys()) roots.add(rootOfIdx(idx, key)) + const list = [] + for (const root of roots) { + const g = resolveGroup(idx, root) + if (!g || g.tombstoned) continue + if (Array.isArray(g.facility.members) && g.facility.members.includes(uid)) list.push(g.facility) + } + list.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)) + return list + }, + + async listMyBuilds() { + const ssbClient = await openSsb() + const uid = ssbClient.id + const idx = buildIndex(await getAllMsgs(ssbClient)) + const facRoots = new Set() + for (const key of idx.nodes.keys()) facRoots.add(rootOfIdx(idx, key)) + const myFacilities = new Map() + for (const root of facRoots) { + const g = resolveGroup(idx, root) + if (!g || g.tombstoned) continue + if (Array.isArray(g.facility.members) && g.facility.members.includes(uid)) myFacilities.set(g.root, g.facility) + } + const out = [] + for (const [key, node] of idx.builds) { + if (idx.tomb.has(key)) continue + const fac = myFacilities.get(node.c.facility) + if (!fac) continue + const build = resolveBuild(idx, key, fac) + build.facilityId = fac.root + build.facilityName = fac.name + out.push(build) + } + out.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)) + return out + }, + + async createBlueprint(facilityId, data) { + const ssbClient = await openSsb() + const idx = buildIndex(await getAllMsgs(ssbClient)) + const g = resolveGroup(idx, rootOfIdx(idx, facilityId)) + if (!g || g.tombstoned) throw new Error("Facility not found") + if (!g.facility.members.includes(ssbClient.id)) throw new Error("Only members can add blueprints") + const content = { + type: "industryBlueprint", + facility: g.root, + name: String(data.name || "").trim(), + description: String(data.description || "").trim(), + image: data.image || null, + sector: safeSector(data.sector || g.facility.sector), + materials: normalizeMaterials(data.materials, data.materialsText), + laborHours: nonNegNum(data.laborHours), + skills: normalizeSkills(data.skills), + outItem: String(data.outItem || "").trim(), + outQty: nonNegNum(data.outQty), + outUnit: String(data.outUnit || "unit").trim() || "unit", + outKind: String(data.outKind || "physical").toLowerCase() === "digital" ? "digital" : "physical", + license: String(data.license || "copyleft").trim() || "copyleft", + createdAt: new Date().toISOString() + } + if (!content.name) throw new Error("Blueprint name is required") + return publish(ssbClient, content) + }, + + async updateBlueprint(id, patch) { + const ssbClient = await openSsb() + const idx = buildIndex(await getAllMsgs(ssbClient)) + const b = resolveBlueprint(idx, rootOfBlueprint(idx, id)) + if (!b || b.tombstoned) throw new Error("Blueprint not found") + if (b.blueprint.author !== ssbClient.id) throw new Error("Unauthorized") + const gb = resolveGroup(idx, rootOfIdx(idx, b.blueprint.facility)) + if (!gb || gb.tombstoned) throw new Error("Facility not found") + if (!gb.facility.members.includes(ssbClient.id)) throw new Error("Only members of the facility can do this") + if (!actionApproved(idx, gb, "bpUpdate", b.root)) throw new Error("Updating this blueprint requires a governance vote") + const cur = b.blueprint + const updated = { + type: "industryBlueprint", + facility: cur.facility, + name: patch.name === undefined ? cur.name : String(patch.name || "").trim(), + description: patch.description === undefined ? (cur.description || "") : String(patch.description || "").trim(), + image: patch.image === undefined ? (cur.image || null) : (patch.image || null), + sector: patch.sector === undefined ? cur.sector : safeSector(patch.sector), + materials: patch.materials === undefined && patch.materialsText === undefined ? cur.materials : normalizeMaterials(patch.materials, patch.materialsText), + laborHours: patch.laborHours === undefined ? cur.laborHours : nonNegNum(patch.laborHours), + skills: patch.skills === undefined ? cur.skills : normalizeSkills(patch.skills), + outItem: patch.outItem === undefined ? cur.outItem : String(patch.outItem || "").trim(), + outQty: patch.outQty === undefined ? cur.outQty : nonNegNum(patch.outQty), + outUnit: patch.outUnit === undefined ? cur.outUnit : String(patch.outUnit || "unit").trim() || "unit", + outKind: patch.outKind === undefined ? cur.outKind : (String(patch.outKind).toLowerCase() === "digital" ? "digital" : "physical"), + license: patch.license === undefined ? cur.license : String(patch.license || "copyleft").trim() || "copyleft", + replaces: b.tip, + createdAt: cur.createdAt, + updatedAt: new Date().toISOString() + } + const tomb = { type: "tombstone", target: b.tip, deletedAt: new Date().toISOString(), author: ssbClient.id } + await publish(ssbClient, tomb) + return publish(ssbClient, updated) + }, + + async deleteBlueprint(id) { + const ssbClient = await openSsb() + const idx = buildIndex(await getAllMsgs(ssbClient)) + const b = resolveBlueprint(idx, rootOfBlueprint(idx, id)) + if (!b || b.tombstoned) throw new Error("Blueprint not found") + const g = resolveGroup(idx, rootOfIdx(idx, b.blueprint.facility)) + if (!g || g.tombstoned) throw new Error("Facility not found") + if (!g.facility.members.includes(ssbClient.id)) throw new Error("Only members of the facility can do this") + if (b.blueprint.author !== ssbClient.id) throw new Error("Only the author can delete a blueprint") + if (!actionApproved(idx, g, "bpDelete", b.root)) throw new Error("Deleting this blueprint requires a governance vote") + const chain = [] + { let x = b.root, guard = 0; chain.push(x); while (idx.bpNext.has(x) && guard++ < 100000) { x = idx.bpNext.get(x); chain.push(x) } } + const chainSet = new Set(chain) + let usedBy = 0 + for (const [key, node] of idx.builds) { + if (idx.tomb.has(key)) continue + if (chainSet.has(node.c.blueprint)) usedBy++ + } + if (usedBy > 0) throw new Error(`This blueprint is used by ${usedBy} build${usedBy > 1 ? "s" : ""}; delete them first`) + let last = null + for (const key of chain) { + const node = idx.blueprints.get(key) + if (!node || node.author !== ssbClient.id) continue + if (idx.tomb.has(key)) continue + last = await publish(ssbClient, { type: "tombstone", target: key, deletedAt: new Date().toISOString(), author: ssbClient.id }) + } + return last + }, + + async getBlueprint(id) { + const ssbClient = await openSsb() + const idx = buildIndex(await getAllMsgs(ssbClient)) + const b = resolveBlueprint(idx, rootOfBlueprint(idx, id)) + if (!b || b.tombstoned) throw new Error("Blueprint not found") + const g = resolveGroup(idx, rootOfIdx(idx, b.blueprint.facility)) + if (!g || g.tombstoned) throw new Error("Facility not found") + const builds = [] + for (const [key, node] of idx.builds) { + if (idx.tomb.has(key)) continue + if (node.c.blueprint !== b.tip && node.c.blueprint !== b.root) continue + const rb = resolveBuild(idx, key, g.facility) + if (rb) builds.push({ id: key, title: rb.title, status: rb.status, startDate: rb.startDate, endDate: rb.endDate }) + } + builds.sort((x, y) => new Date(y.startDate || 0) - new Date(x.startDate || 0)) + return { + ...b.blueprint, + facilityId: g.root, + facilityName: g.facility.name, + members: g.facility.members, + builds, + ...estimateBlueprint(b.blueprint, g.facility.laborRate), + ...blueprintGovernance(idx, g, b.root) + } + }, + + async listBlueprints(facilityId) { + const ssbClient = await openSsb() + const msgs = await getAllMsgs(ssbClient) + const idx = buildIndex(msgs) + const root = rootOfIdx(idx, facilityId) + const g = resolveGroup(idx, root) + const laborRate = g && !g.tombstoned ? g.facility.laborRate : 0 + const roots = new Set() + for (const key of idx.blueprints.keys()) roots.add(rootOfBlueprint(idx, key)) + const list = [] + for (const r of roots) { + const b = resolveBlueprint(idx, r) + if (!b || b.tombstoned) continue + if (b.blueprint.facility !== root) continue + list.push({ ...b.blueprint, ...estimateBlueprint(b.blueprint, laborRate), ...(g && !g.tombstoned ? blueprintGovernance(idx, g, b.root) : {}) }) + } + list.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)) + return list + }, + + async createBuild(facilityId, data) { + const ssbClient = await openSsb() + const idx = buildIndex(await getAllMsgs(ssbClient)) + const g = resolveGroup(idx, rootOfIdx(idx, facilityId)) + if (!g || g.tombstoned) throw new Error("Facility not found") + if (g.facility.status !== "ACTIVE") throw new Error("Facility is not active") + if (!g.facility.members.includes(ssbClient.id)) throw new Error("Only members can propose builds") + let blueprintId = String(data.blueprintId || "").trim() || null + if (!blueprintId) throw new Error("A build needs a blueprint") + { + const b = resolveBlueprint(idx, rootOfBlueprint(idx, blueprintId)) + if (!b || b.tombstoned || b.blueprint.facility !== g.root) throw new Error("Blueprint not found in this facility") + blueprintId = b.tip + } + const content = { type: "industryBuild", facility: g.root, blueprint: blueprintId, title: String(data.title || "").trim(), notes: String(data.notes || "").trim(), image: data.image || null, startDate: String(data.startDate || "").trim() || null, endDate: String(data.endDate || "").trim() || null, createdAt: new Date().toISOString() } + if (!content.title) throw new Error("Build title is required") + const today = new Date().toISOString().slice(0, 10) + if (!content.startDate) throw new Error("Build start date is required") + if (!content.endDate) throw new Error("Build end date is required") + if (content.startDate < today) throw new Error("Start date cannot be in the past") + if (content.startDate && content.endDate && new Date(content.endDate) < new Date(content.startDate)) throw new Error("End date must be after start date") + return publish(ssbClient, content) + }, + + async voteBuild(buildId, choice) { + const ssbClient = await openSsb() + const idx = buildIndex(await getAllMsgs(ssbClient)) + const bnode = idx.builds.get(buildId) + if (!bnode || idx.tomb.has(buildId)) throw new Error("Build not found") + const g = resolveGroup(idx, rootOfIdx(idx, bnode.c.facility)) + if (!g || g.tombstoned) throw new Error("Facility not found") + if (!g.facility.members.includes(ssbClient.id)) throw new Error("Only members can vote") + return publish(ssbClient, { type: "industryVote", target: g.root, subject: "build", ref: buildId, choice: choice === "no" ? "no" : "yes", createdAt: new Date().toISOString() }) + }, + + async setBuildStatus(buildId, status) { + const ssbClient = await openSsb() + const idx = buildIndex(await getAllMsgs(ssbClient)) + const bnode = idx.builds.get(buildId) + if (!bnode || idx.tomb.has(buildId)) throw new Error("Build not found") + const g = resolveGroup(idx, rootOfIdx(idx, bnode.c.facility)) + if (!g || g.tombstoned) throw new Error("Facility not found") + const build = resolveBuild(idx, buildId, g.facility) + const next = String(status || "").toUpperCase() + if (!BUILD_STATUSES.includes(next)) throw new Error("Invalid status") + const allowed = BUILD_TRANSITIONS[build.status] || [] + if (!allowed.includes(next)) throw new Error(`Cannot move build from ${build.status} to ${next}`) + if (g.facility.status !== "ACTIVE") throw new Error("Facility is not active") + if (!g.facility.members.includes(ssbClient.id)) throw new Error("Only members of the facility can do this") + const isSteward = g.facility.steward === ssbClient.id + const isProposer = build.proposer === ssbClient.id + const isReopen = build.status === "COMPLETED" || build.status === "FAILED" + if (build.status === "COMPLETED" && build.distributed) throw new Error("A distributed build cannot be reopened") + if ((next === "COMPLETED" || next === "FAILED" || isReopen) && !isSteward) throw new Error("Only the steward can complete, fail or reopen a build") + if (!isSteward && !isProposer) throw new Error("Only the steward or the proposer can advance the build") + return publish(ssbClient, { type: "industryBuildStatus", target: buildId, status: next, createdAt: new Date().toISOString() }) + }, + + async contribute(buildId, data) { + const ssbClient = await openSsb() + const idx = buildIndex(await getAllMsgs(ssbClient)) + const bnode = idx.builds.get(buildId) + if (!bnode || idx.tomb.has(buildId)) throw new Error("Build not found") + const g = resolveGroup(idx, rootOfIdx(idx, bnode.c.facility)) + if (!g || g.tombstoned) throw new Error("Facility not found") + if (!g.facility.members.includes(ssbClient.id)) throw new Error("Only members can contribute") + if (g.facility.status !== "ACTIVE") throw new Error("Facility is not active") + const build = resolveBuild(idx, buildId, g.facility) + if (!CONTRIBUTABLE.has(build.status)) throw new Error("This build is not accepting contributions") + const kind = ["labor", "material", "eco"].includes(String(data.kind)) ? String(data.kind) : "labor" + const content = { type: "industryContribution", target: buildId, kind, hours: nonNegNum(data.hours), item: String(data.item || "").trim(), value: nonNegNum(data.value), eco: nonNegNum(data.eco), transferId: data.transferId || null, note: String(data.note || "").trim(), createdAt: new Date().toISOString() } + if (kind === "labor") { + if (content.hours <= 0) throw new Error("Labor contributions require hours above zero") + content.item = ""; content.value = 0; content.eco = 0 + } else if (kind === "material") { + if (!content.item) throw new Error("Material contributions require an item") + if (content.value <= 0) throw new Error("Material contributions require a value above zero") + content.hours = 0; content.eco = 0 + } else { + if (content.eco <= 0) throw new Error("ECO contributions require an amount above zero") + content.hours = 0; content.item = ""; content.value = 0 + } + return publish(ssbClient, content) + }, + + async getBuild(buildId) { + const ssbClient = await openSsb() + const msgs = await getAllMsgs(ssbClient) + const idx = buildIndex(msgs) + const bnode = idx.builds.get(buildId) + if (!bnode || idx.tomb.has(buildId)) throw new Error("Build not found") + const g = resolveGroup(idx, rootOfIdx(idx, bnode.c.facility)) + if (!g) throw new Error("Facility not found") + const build = resolveBuild(idx, buildId, g.facility) + build.facilityId = g.root + build.facilityName = g.facility.name + build.steward = g.facility.steward + build.members = g.facility.members + const est = estimateForBlueprintRef(idx, build.blueprint, g.facility.laborRate) + if (est) Object.assign(build, est) + Object.assign(build, buildGovernance(idx, g, buildId)) + return build + }, + + async updateBuild(buildId, data) { + const ssbClient = await openSsb() + const idx = buildIndex(await getAllMsgs(ssbClient)) + const bnode = idx.builds.get(buildId) + if (!bnode || idx.tomb.has(buildId)) throw new Error("Build not found") + const g = resolveGroup(idx, rootOfIdx(idx, bnode.c.facility)) + if (!g || g.tombstoned) throw new Error("Facility not found") + if (!g.facility.members.includes(ssbClient.id)) throw new Error("Only members of the facility can do this") + const build = resolveBuild(idx, buildId, g.facility) + const isSteward = g.facility.steward === ssbClient.id + if (build.proposer !== ssbClient.id && !isSteward) throw new Error("Only the steward or the proposer can update a build") + if (build.distributed) throw new Error("A distributed build cannot be updated") + if (!actionApproved(idx, g, "buildUpdate", buildId)) throw new Error("Updating this build requires a governance vote") + const startDate = String(data.startDate || "").trim() + const endDate = String(data.endDate || "").trim() + if (!startDate) throw new Error("Build start date is required") + if (!endDate) throw new Error("Build end date is required") + if (new Date(endDate) < new Date(startDate)) throw new Error("End date must be after start date") + const today = new Date().toISOString().slice(0, 10) + if (endDate < today) throw new Error("End date cannot be in the past") + const title = data.title === undefined ? String(build.title || "") : String(data.title || "").trim() + if (!title) throw new Error("Build title is required") + const notes = data.notes === undefined ? String(build.notes || "") : String(data.notes || "").trim() + return publish(ssbClient, { type: "industryBuildUpdate", target: buildId, title, notes, startDate, endDate, createdAt: new Date().toISOString() }) + }, + + async deleteBuild(buildId) { + const ssbClient = await openSsb() + const idx = buildIndex(await getAllMsgs(ssbClient)) + const bnode = idx.builds.get(buildId) + if (!bnode || idx.tomb.has(buildId)) throw new Error("Build not found") + const g = resolveGroup(idx, rootOfIdx(idx, bnode.c.facility)) + if (!g || g.tombstoned) throw new Error("Facility not found") + if (!g.facility.members.includes(ssbClient.id)) throw new Error("Only members of the facility can do this") + if (bnode.author !== ssbClient.id) throw new Error("Only the proposer can delete a build") + if (!actionApproved(idx, g, "buildDelete", buildId)) throw new Error("Deleting this build requires a governance vote") + return publish(ssbClient, { type: "tombstone", target: buildId, deletedAt: new Date().toISOString(), author: ssbClient.id }) + }, + + async listBuilds(facilityId, filter = "ALL") { + const ssbClient = await openSsb() + const idx = buildIndex(await getAllMsgs(ssbClient)) + const g = resolveGroup(idx, rootOfIdx(idx, facilityId)) + if (!g || g.tombstoned) throw new Error("Facility not found") + let list = [] + for (const [key, node] of idx.builds) { + if (idx.tomb.has(key)) continue + if (node.c.facility !== g.root) continue + const b = resolveBuild(idx, key, g.facility) + if (!b) continue + const est = estimateForBlueprintRef(idx, b.blueprint, g.facility.laborRate) + list.push({ ...b, ...(est || {}), ...buildGovernance(idx, g, key) }) + } + const f = String(filter || "ALL").toUpperCase() + if (BUILD_STATUSES.includes(f)) list = list.filter(x => x.status === f) + list.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)) + return list + }, + + async listAllBlueprints() { + const ssbClient = await openSsb() + const msgs = await getAllMsgs(ssbClient) + const idx = buildIndex(msgs) + const roots = new Set() + for (const key of idx.blueprints.keys()) roots.add(rootOfBlueprint(idx, key)) + const facCache = new Map() + const list = [] + for (const root of roots) { + const b = resolveBlueprint(idx, root) + if (!b || b.tombstoned) continue + const facRoot = rootOfIdx(idx, b.blueprint.facility) + let g = facCache.get(facRoot) + if (g === undefined) { g = resolveGroup(idx, facRoot); facCache.set(facRoot, g) } + if (!g || g.tombstoned) continue + list.push({ ...b.blueprint, facilityId: g.root, facilityName: g.facility.name, members: g.facility.members, ...estimateBlueprint(b.blueprint, g.facility.laborRate), ...blueprintGovernance(idx, g, b.root) }) + } + list.sort((a, b) => new Date(b.createdAt || 0) - new Date(a.createdAt || 0)) + return list + }, + + async listAllBuilds() { + const ssbClient = await openSsb() + const msgs = await getAllMsgs(ssbClient) + const idx = buildIndex(msgs) + const facCache = new Map() + const list = [] + for (const [key, node] of idx.builds) { + if (idx.tomb.has(key)) continue + const facRoot = rootOfIdx(idx, node.c.facility) + let g = facCache.get(facRoot) + if (g === undefined) { g = resolveGroup(idx, facRoot); facCache.set(facRoot, g) } + if (!g || g.tombstoned) continue + const b = resolveBuild(idx, key, g.facility) + if (!b) continue + const est = estimateForBlueprintRef(idx, b.blueprint, g.facility.laborRate) + list.push({ ...b, facilityId: g.root, facilityName: g.facility.name, members: g.facility.members, ...(est || {}), ...buildGovernance(idx, g, key) }) + } + list.sort((a, b) => new Date(b.createdAt || 0) - new Date(a.createdAt || 0)) + return list + }, + + computeDistributionPlan(build, outputValue) { + const pot = nonNegNum(build.treasury) + nonNegNum(outputValue) + const distributable = pot + const allocations = [] + const amounts = {} + for (const [uid, share] of Object.entries(build.shares || {})) { + const amount = distributable * share + amounts[uid] = amount + if (amount > 0) allocations.push({ to: uid, amount, share }) + } + return { pot, distributable, allocations, amounts } + }, + + async distributeBuild(buildId, data) { + const ssbClient = await openSsb() + const idx = buildIndex(await getAllMsgs(ssbClient)) + const bnode = idx.builds.get(buildId) + if (!bnode || idx.tomb.has(buildId)) throw new Error("Build not found") + const g = resolveGroup(idx, rootOfIdx(idx, bnode.c.facility)) + if (!g || g.tombstoned) throw new Error("Facility not found") + if (g.facility.steward !== ssbClient.id) throw new Error("Only the steward can distribute") + const build = resolveBuild(idx, buildId, g.facility) + if (build.status !== "COMPLETED") throw new Error("Only a completed build can be distributed") + if (build.distributed) throw new Error("This build has already been distributed") + if (build.totalPoints <= 0) throw new Error("There are no contributions to distribute") + const plan = this.computeDistributionPlan(build, data && data.outputValue) + const content = { type: "industryAllocation", target: buildId, shares: build.shares, amounts: plan.amounts, pot: plan.pot, outputValue: nonNegNum(data && data.outputValue), disposition: (data && data.disposition) || null, createdAt: new Date().toISOString() } + await publish(ssbClient, content) + return { plan, facility: g.facility, build } + } + } +} diff --git a/src/models/inhabitants_model.js b/src/models/inhabitants_model.js index 2b3c0dd..a594a75 100644 --- a/src/models/inhabitants_model.js +++ b/src/models/inhabitants_model.js @@ -1,11 +1,7 @@ const pull = require('../server/node_modules/pull-stream'); -const ssbClientGUI = require("../client/gui"); -const coolerInstance = ssbClientGUI({ offline: require('../server/ssb_config').offline }); +const { readTyped, CONTENT_TYPES, requestScope, authorActivity } = require('./typed_log'); +const { isContentVisibleTo } = require('./content_visibility'); const models = require("../models/main_models"); -const { about, friend } = models({ - cooler: coolerInstance, - isPublic: require('../server/ssb_config').public, -}); const { getConfig } = require('../configs/config-manager.js'); const logLimit = getConfig().ssbLogStream?.limit || 1000; @@ -15,36 +11,48 @@ function toImageUrl(imgId, size=256){ return `/image/${size}/${encodeURIComponent(imgId)}`; } -module.exports = ({ cooler }) => { +const MIN_SUGGESTION_AFFINITY = 0.1; +const MAX_SUGGESTED = 12; + +module.exports = ({ cooler, tribesModel = null, dataModel = null }) => { + const { about, friend } = models({ cooler, isPublic: require('../server/ssb_config').public }); let ssb; const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb; }; - async function getLastKarmaScore(feedId) { + const bytesPerAuthor = (ssbClient) => readTyped(ssbClient, CONTENT_TYPES, { limit: logLimit, withWindow: true }).then((msgs) => { + const acc = {}; + for (const m of msgs) { + const author = m && m.value && m.value.author; + if (!author) continue; + try { acc[author] = (acc[author] || 0) + Buffer.byteLength(JSON.stringify(m.value), 'utf8'); } catch (_) {} + } + return acc; + }).catch(() => ({})); + + async function latestKarmaByAuthor() { const ssbClient = await openSsb(); return new Promise(resolve => { + const out = new Map(); const src = ssbClient.messagesByType ? ssbClient.messagesByType({ type: "karmaScore", reverse: true }) : ssbClient.createLogStream && ssbClient.createLogStream({ reverse: true }); - if (!src) return resolve(0); + if (!src) return resolve(out); pull( src, - pull.filter(msg => { + pull.drain(msg => { const v = msg.value || msg; const c = v.content || {}; - return v.author === feedId && c.type === "karmaScore" && typeof c.karmaScore !== "undefined"; - }), - pull.take(1), - pull.collect((err, arr) => { - if (err || !arr || !arr.length) return resolve(0); - const v = arr[0].value || arr[0]; - resolve(v.content.karmaScore || 0); - }) + if (c.type !== "karmaScore" || typeof c.karmaScore === "undefined" || out.has(v.author)) return; + out.set(v.author, c.karmaScore || 0); + }, () => resolve(out)) ); }); } async function getLastActivityTimestamp(feedId) { const ssbClient = await openSsb(); + const activity = await authorActivity(ssbClient); + if (activity) return activity.get(feedId) || null; const norm = (t) => (t && t < 1e12 ? t * 1000 : t || 0); return new Promise((resolve) => { pull( @@ -82,21 +90,34 @@ module.exports = ({ cooler }) => { } }; - async function listAllBase(ssbClient) { - const authorsMsgs = await new Promise((res, rej) => { - pull( - ssbClient.createLogStream({ limit: logLimit, reverse: true }), - pull.filter(msg => !!msg.value?.author && msg.value?.content?.type !== 'tombstone'), - pull.collect((err, msgs) => err ? rej(err) : res(msgs)) - ); - }); - const uniqueFeedIds = Array.from(new Set(authorsMsgs.map(r => r.value.author).filter(Boolean))); + const publicMode = require('../server/ssb_config').public; + const imageUrlFrom = (raw, size = 256) => { + const img = raw == null || raw.link == null ? `&${"0".repeat(43)}=.sha256` : (typeof raw.link === 'string' ? raw.link : raw); + const id = typeof img === 'string' ? img : (img && (img.link || img.url)); + return toImageUrl(id, size); + }; + + async function listAllBase(ssbClient, onlyIds = null) { + const store = requestScope.getStore(); + const memoKey = onlyIds ? null : 'inhabitantsBase'; + if (memoKey && store && store[memoKey]) return store[memoKey]; + const pending = buildBase(ssbClient, onlyIds); + if (memoKey && store) store[memoKey] = pending; + return pending; + } + + async function buildBase(ssbClient, onlyIds) { + const only = onlyIds ? new Set(onlyIds) : null; + const authorsMsgs = (await readTyped(ssbClient, CONTENT_TYPES, { limit: logLimit, withWindow: true })).filter(msg => !!msg.value?.author && msg.value?.content?.type !== 'tombstone').reverse(); + const uniqueFeedIds = Array.from(new Set(authorsMsgs.map(r => r.value.author).filter(Boolean))).filter(id => !only || only.has(id)); + const profiles = publicMode ? null : await about.profiles().catch(() => null); const users = await Promise.all( uniqueFeedIds.map(async (feedId) => { - const rawName = await about.name(feedId); + const p = profiles ? (profiles.get(feedId) || {}) : null; + const rawName = p ? (p.name || feedId.slice(1, 9)) : await about.name(feedId); const name = rawName || feedId.slice(0, 10); - const description = await about.description(feedId); - const photo = await fetchUserImageUrl(feedId, 256); + const description = p ? (p.description || "") : await about.description(feedId); + const photo = p ? imageUrlFrom(p.image, 256) : await fetchUserImageUrl(feedId, 256); const lastActivityTs = await getLastActivityTimestamp(feedId); const { bucket, range } = bucketLastActivity(lastActivityTs); return { id: feedId, name, description, photo, lastActivityTs, lastActivityBucket: bucket, lastActivityRange: range }; @@ -115,8 +136,14 @@ module.exports = ({ cooler }) => { } return { + async countInhabitants() { + const ssbClient = await openSsb(); + const msgs = await readTyped(ssbClient, CONTENT_TYPES, { limit: logLimit, withWindow: true }); + return new Set(msgs.filter(msg => !!msg.value?.author && msg.value?.content?.type !== 'tombstone').map(msg => msg.value.author)).size; + }, + async listInhabitants(options = {}) { - const { filter = 'all', search = '', location = '', language = '', skills = '', includeInactive = false } = options; + const { filter = 'all', search = '', location = '', language = '', skills = '', includeInactive = false, ids = null } = options; const ssbClient = await openSsb(); const userId = ssbClient.id; @@ -130,9 +157,9 @@ module.exports = ({ cooler }) => { return filterInactive(users); } - if (filter === 'all' || filter === 'TOP KARMA' || filter === 'TOP ACTIVITY' || filter === 'TOP ECO') { - let users = await listAllBase(ssbClient); - if (filter !== 'TOP ACTIVITY') { + if (filter === 'all' || filter === 'TOP KARMA' || filter === 'TOP ACTIVITY' || filter === 'TOP INACTIVITY' || filter === 'TOP ECO') { + let users = (await listAllBase(ssbClient, ids)).slice(); + if (filter !== 'TOP ACTIVITY' && filter !== 'TOP INACTIVITY') { users = filterInactive(users); } if (search) { @@ -143,23 +170,12 @@ module.exports = ({ cooler }) => { (u.id || '').toLowerCase().includes(q) ); } - const bytesByAuthor = await new Promise((res) => { - pull( - ssbClient.createLogStream({ limit: logLimit }), - pull.collect((err, msgs) => { - if (err || !Array.isArray(msgs)) return res({}); - const acc = {}; - for (const m of msgs) { - const author = m && m.value && m.value.author; - if (!author) continue; - try { acc[author] = (acc[author] || 0) + Buffer.byteLength(JSON.stringify(m.value), 'utf8'); } catch (_) {} - } - res(acc); - }) - ); - }); + const store = requestScope.getStore(); + if (store && !store.inhabitantBytes) store.inhabitantBytes = bytesPerAuthor(ssbClient); + const bytesByAuthor = await (store ? store.inhabitantBytes : bytesPerAuthor(ssbClient)); + const karmaOf = await latestKarmaByAuthor(); const withMetrics = await Promise.all(users.map(async u => { - const karmaScore = await getLastKarmaScore(u.id); + const karmaScore = karmaOf.get(u.id) || 0; const bytes = (bytesByAuthor && bytesByAuthor[u.id]) || 0; const carbonGrams = (bytes / (1024 * 1024)) * 0.095; if (filter === 'TOP ECO') { @@ -170,6 +186,7 @@ module.exports = ({ cooler }) => { })); if (filter === 'TOP KARMA') return withMetrics.sort((a, b) => (b.karmaScore || 0) - (a.karmaScore || 0)); if (filter === 'TOP ACTIVITY') return withMetrics.sort((a, b) => (b.lastActivityTs || 0) - (a.lastActivityTs || 0)); + if (filter === 'TOP INACTIVITY') return withMetrics.sort((a, b) => (a.lastActivityTs || 0) - (b.lastActivityTs || 0)); if (filter === 'TOP ECO') return withMetrics.sort((a, b) => (b.ecoScore || 0) - (a.ecoScore || 0)); return withMetrics; } @@ -198,80 +215,54 @@ module.exports = ({ cooler }) => { if (filter === 'SUGGESTED') { const base = await listAllBase(ssbClient); const active = filterInactive(base); - const cvRecords = await new Promise((res) => { - pull( - ssbClient.createLogStream({ limit: logLimit, reverse: true }), - pull.filter(msg => msg && msg.value && msg.value.content && msg.value.content.type === 'curriculum'), - pull.collect((err, msgs) => err ? res([]) : res(msgs)) - ); - }); - const cvByAuthor = new Map(); - for (const r of cvRecords) { - const c = r.value && r.value.content; - if (c && c.author && !cvByAuthor.has(c.author)) cvByAuthor.set(c.author, c); - } - const extractSkills = (cv) => cv ? [ - ...(cv.personalSkills || []), - ...(cv.oasisSkills || []), - ...(cv.educationalSkills || []), - ...(cv.professionalSkills || []) - ].map(s => String(s || '').toLowerCase()).filter(Boolean) : []; - const mecv = await this.getCVByUserId().catch(() => null); - const mySkills = extractSkills(mecv); + const affinities = dataModel ? await dataModel.authorAffinities().catch(() => null) : null; + const byAuthor = affinities ? affinities.byAuthor : new Map(); + const karmaOf = await latestKarmaByAuthor(); const rels = await Promise.all( active.map(async u => { if (u.id === userId) return null; const rel = await friend.getRelationship(u.id).catch(() => ({})); const n = normalizeRel(rel); if (n.iFollow || n.blocking || n.blockedBy) return null; - const karmaScore = await getLastKarmaScore(u.id); - const theirSkills = extractSkills(cvByAuthor.get(u.id)); - const commonSkills = mySkills.length && theirSkills.length - ? Array.from(new Set(mySkills.filter(s => theirSkills.includes(s)))) - : []; - const followsMeBonus = n.followsMe ? 20 : 0; - const karmaBonus = Math.min(20, Math.log10(1 + Math.max(0, karmaScore)) * 5); - const skillBonus = commonSkills.length * 4; - const activityBonus = u.lastActivityBucket === 'green' ? 5 : (u.lastActivityBucket === 'orange' ? 2 : 0); - const suggestionScore = followsMeBonus + karmaBonus + skillBonus + activityBonus; - return { user: u, rel: n, karmaScore, commonSkills, suggestionScore }; + const aff = byAuthor.get(u.id) || { score: 0, common: [], reasons: [] }; + const tribeMate = aff.reasons.includes('tribe'); + if (aff.score < MIN_SUGGESTION_AFFINITY && !n.followsMe && !tribeMate) return null; + const social = (n.followsMe ? 0.15 : 0) + (tribeMate ? 0.1 : 0); + const activityBonus = u.lastActivityBucket === 'green' ? 0.05 : (u.lastActivityBucket === 'orange' ? 0.02 : 0); + const suggestionScore = Math.min(1, aff.score + social + activityBonus); + const karmaScore = karmaOf.get(u.id) || 0; + return { user: u, rel: n, karmaScore, commonSkills: aff.common, reasons: aff.reasons, suggestionScore }; }) ); - const candidates = rels.filter(Boolean).filter(x => x.suggestionScore > 0); + const candidates = rels.filter(Boolean); const enriched = candidates.map(x => ({ ...x.user, karmaScore: x.karmaScore, followsYou: x.rel.followsMe, commonSkills: x.commonSkills, + reasons: x.reasons, mutualCount: x.rel.followsMe ? 1 : 0, - suggestionScore: x.suggestionScore + suggestionScore: x.suggestionScore, + affinity: x.suggestionScore })); const unique = Array.from(new Map(enriched.map(u => [u.id, u])).values()); return unique.sort((a, b) => (b.suggestionScore || 0) - (a.suggestionScore || 0) || (b.karmaScore || 0) - (a.karmaScore || 0) || (b.lastActivityTs || 0) - (a.lastActivityTs || 0) - ); + ).slice(0, MAX_SUGGESTED); } - if (filter === 'CVs' || filter === 'MATCHSKILLS') { - const records = await new Promise((res, rej) => { - pull( - ssbClient.createLogStream({ limit: logLimit, reverse: true}), - pull.filter(msg => - msg.value.content?.type === 'curriculum' && - msg.value.content?.type !== 'tombstone' - ), - pull.collect((err, msgs) => err ? rej(err) : res(msgs)) - ); - }); + if (filter === 'CVs') { + const records = (await readTyped(ssbClient, ['curriculum'], { limit: logLimit })).filter(msg => msg.value.content?.type === 'curriculum').reverse(); let cvs = records.map(r => r.value.content); cvs = Array.from(new Map(cvs.map(u => [u.author, u])).values()); if (filter === 'CVs') { + const profiles = publicMode ? null : await about.profiles().catch(() => null); let out = await Promise.all(cvs.map(async c => { - const photo = await fetchUserImageUrl(c.author, 256); + const photo = profiles ? imageUrlFrom((profiles.get(c.author) || {}).image, 256) : await fetchUserImageUrl(c.author, 256); const lastActivityTs = await getLastActivityTimestamp(c.author); const { bucket, range } = bucketLastActivity(lastActivityTs); const base = this._normalizeCurriculum(c, photo); @@ -295,59 +286,19 @@ module.exports = ({ cooler }) => { return out; } - if (filter === 'MATCHSKILLS') { - let base = await Promise.all(cvs.map(async c => { - const photo = await fetchUserImageUrl(c.author, 256); - const lastActivityTs = await getLastActivityTimestamp(c.author); - const { bucket, range } = bucketLastActivity(lastActivityTs); - const norm = this._normalizeCurriculum(c, photo); - const karmaScore = await getLastKarmaScore(c.author).catch(() => 0); - return { ...norm, lastActivityTs, lastActivityBucket: bucket, lastActivityRange: range, karmaScore }; - })); - base = filterInactive(base); - const mecv = await this.getCVByUserId(); - const userSkills = Array.from(new Set( - (mecv - ? [ - ...(mecv.personalSkills || []), - ...(mecv.oasisSkills || []), - ...(mecv.educationalSkills || []), - ...(mecv.professionalSkills || []) - ] - : []).map(s => String(s || '').toLowerCase()).filter(Boolean) - )); - if (!userSkills.length) return []; - const userSet = new Set(userSkills); - const matches = base.map(c => { - if (c.id === userId) return null; - const theirSkillsRaw = (c.skills || []).map(s => String(s || '').toLowerCase()).filter(Boolean); - const theirSet = new Set(theirSkillsRaw); - const common = Array.from(theirSet).filter(s => userSet.has(s)); - if (!common.length) return null; - const unionSize = userSet.size + theirSet.size - common.length; - const matchScore = unionSize > 0 ? common.length / unionSize : 0; - const matchCoverage = userSet.size > 0 ? common.length / userSet.size : 0; - return { ...c, commonSkills: common, matchScore, matchCoverage }; - }).filter(Boolean); - return matches.sort((a, b) => - (b.matchScore - a.matchScore) || - (b.commonSkills.length - a.commonSkills.length) || - ((b.karmaScore || 0) - (a.karmaScore || 0)) || - ((b.lastActivityTs || 0) - (a.lastActivityTs || 0)) - ); - } } return []; }, _normalizeCurriculum(c, photoUrl) { - const photo = photoUrl || toImageUrl(c.photo, 256); + const photo = c.photo ? toImageUrl(c.photo, 256) : (photoUrl || toImageUrl(null, 256)); return { id: c.author, name: c.name, description: c.description, photo, + pdf: c.pdf || null, skills: [ ...(c.personalSkills || []), ...(c.oasisSkills || []), @@ -358,10 +309,20 @@ module.exports = ({ cooler }) => { languages: typeof c.languages === 'string' ? c.languages.split(',').map(x => x.trim()) : Array.isArray(c.languages) ? c.languages : [], + status: c.status, + preferences: c.preferences, createdAt: c.createdAt }; }, + async listSupporters(feedId) { + const ssbClient = await openSsb(); + const graph = await new Promise((resolve) => ssbClient.friends.graph((err, g) => resolve(err ? {} : (g || {})))); + return Object.entries(graph) + .filter(([source, edges]) => source !== feedId && edges && (edges[feedId] === true || Number(edges[feedId]) > 0)) + .map(([source]) => source); + }, + async getLatestAboutById(id) { const ssbClient = await openSsb(); const records = await new Promise((res, rej) => { @@ -407,20 +368,8 @@ module.exports = ({ cooler }) => { const isOwner = viewer === target; const arr = (v) => Array.isArray(v) ? v : []; const up = (v) => String(v || '').toUpperCase(); - const COUNTED = new Set(['post','event','task','forum','tribe','market','job','project','shop','image','video','audio','document','bookmark','transfer','map']); - const accessible = (type, c) => { - if (c.encryptedPayload) return false; - switch (type) { - case 'task': return up(c.isPublic) !== 'PRIVATE' || isOwner || arr(c.assignees).includes(viewer); - case 'event': return String(c.isPublic || '').toLowerCase() !== 'private' || isOwner || arr(c.attendees).includes(viewer); - case 'forum': return c.isPrivate !== true || isOwner; - case 'job': return up(c.visibility) !== 'HIDDEN' || isOwner || arr(c.subscribers).includes(viewer); - case 'market': return up(c.visibility) !== 'HIDDEN' || isOwner; - case 'shop': return up(c.visibility) !== 'CLOSED' || isOwner; - case 'tribe': { const st = up(c.status); return !(st === 'PRIVATE' || st === 'INVITE-ONLY') || isOwner || arr(c.members).includes(viewer); } - default: return true; - } - }; + const COUNTED = new Set(['post','event','task','forum','market','job','housing','project','industry','shop','image','video','audio','document','bookmark','transfer','map']); + const accessible = (type, c) => isOwner || isContentVisibleTo(type, c, viewer, target); const counts = {}; await new Promise((resolve) => { pull( @@ -436,6 +385,13 @@ module.exports = ({ cooler }) => { }, () => resolve()) ); }); + if (tribesModel) { + try { + const visible = await tribesModel.listTribesForViewer(viewer); + const n = visible.filter(t => String(t.author) === String(target)).length; + if (n > 0) counts.tribe = n; + } catch (_) {} + } return counts; }, @@ -472,16 +428,7 @@ module.exports = ({ cooler }) => { addAll(tokenize(job.requirements)); if (keywords.size === 0) return []; - const records = await new Promise((res, rej) => { - pull( - ssbClient.createLogStream({ limit: logLimit, reverse: true }), - pull.filter(msg => - msg.value?.content?.type === 'curriculum' && - msg.value?.content?.type !== 'tombstone' - ), - pull.collect((err, msgs) => err ? rej(err) : res(msgs)) - ); - }); + const records = (await readTyped(ssbClient, ['curriculum'], { limit: logLimit })).filter(msg => msg.value?.content?.type === 'curriculum').reverse(); let cvs = records.map(r => r.value.content); cvs = Array.from(new Map(cvs.map(u => [u.author, u])).values()); cvs = cvs.filter(c => String(c.visibility || 'PUBLIC').toUpperCase() !== 'HIDDEN'); diff --git a/src/models/jobs_model.js b/src/models/jobs_model.js index 3cf3ff6..d396483 100644 --- a/src/models/jobs_model.js +++ b/src/models/jobs_model.js @@ -2,9 +2,12 @@ const pull = require("../server/node_modules/pull-stream") const moment = require("../server/node_modules/moment") const { getConfig } = require("../configs/config-manager.js") const { buildValidatedTombstoneSet } = require('./tombstone_validator'); -const { dedupeByPreferring } = require('../backend/dedupe') +const { dedupeByPreferring, versionGroups } = require('../backend/dedupe') +const { readTyped, memoIndex } = require('./typed_log') const logLimit = getConfig().ssbLogStream?.limit || 1000 +const JOB_TYPES = ["job", "job_sub", "tombstone"] + const norm = (s) => String(s || "").trim().toLowerCase() const toNum = (v) => { const n = parseFloat(String(v ?? "").replace(",", ".")) @@ -40,20 +43,12 @@ module.exports = ({ cooler, tribeCrypto }) => { const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb } const readAll = async (ssbClient) => - new Promise((resolve, reject) => - pull( - ssbClient.createLogStream({ limit: logLimit }), - pull.collect((err, msgs) => err ? reject(err) : resolve(msgs)) - ) - ) + readTyped(ssbClient, JOB_TYPES, { limit: logLimit, withWindow: true, withPrivate: true }) const buildIndex = (messages, ssbClient) => { - const tomb = new Set() + const tomb = buildValidatedTombstoneSet(messages) const jobNodes = new Map() - const parent = new Map() - const child = new Map() - const naiveReplaces = new Map() - const jobSubLatest = new Map() + const subMsgs = [] for (const m of messages) { const key = m.key @@ -61,65 +56,19 @@ module.exports = ({ cooler, tribeCrypto }) => { const c = v.content if (!c) continue - if (c.type === "tombstone" && c.target) { - tomb.add(c.target) - continue - } - if (c.type === "job") { - jobNodes.set(key, { key, ts: v.timestamp || m.timestamp || 0, c, author: v.author }) - if (c.replaces) naiveReplaces.set(key, c.replaces) + jobNodes.set(key, { key, ts: v.timestamp || m.timestamp || 0, seq: v.sequence || 0, c, author: v.author }) continue } if (c.type === "job_sub" && c.jobId) { const author = v.author if (!author) continue - const ts = v.timestamp || m.timestamp || 0 - const jobId = c.jobId - const k = `${jobId}::${author}` - const prev = jobSubLatest.get(k) - if (!prev || ts > prev.ts) jobSubLatest.set(k, { ts, value: !!c.value, author, jobId }) + subMsgs.push({ ts: v.timestamp || m.timestamp || 0, value: !!c.value, author, jobId: c.jobId }) continue } } - for (const [key, replacesId] of naiveReplaces.entries()) { - const node = jobNodes.get(key) - if (!node) continue - const orig = jobNodes.get(replacesId) - if (!orig) continue - if (String(orig.author) !== String(node.author)) { jobNodes.delete(key); continue } - parent.set(key, replacesId) - child.set(replacesId, key) - } - - const rootOf = (id) => { - let cur = id - while (parent.has(cur)) cur = parent.get(cur) - return cur - } - - const roots = new Set() - for (const id of jobNodes.keys()) roots.add(rootOf(id)) - - const tipOf = (id) => { - let cur = id - while (child.has(cur)) cur = child.get(cur) - return cur - } - - const tipByRoot = new Map() - for (const r of roots) tipByRoot.set(r, tipOf(r)) - - const subsByJob = new Map() - for (const { jobId, author, value } of jobSubLatest.values()) { - if (!subsByJob.has(jobId)) subsByJob.set(jobId, new Set()) - const set = subsByJob.get(jobId) - if (value) set.add(author) - else set.delete(author) - } - if (ssbClient) { for (const m of messages) { if (typeof m.value?.content !== 'string') continue @@ -130,16 +79,30 @@ module.exports = ({ cooler, tribeCrypto }) => { if (c.type !== 'job_sub' || !c.jobId) continue const author = dec.value.author if (!author) continue - const ts = dec.value.timestamp || m.timestamp || 0 - const jobId = c.jobId - const k = `${jobId}::${author}` - const prev = jobSubLatest.get(k) - if (!prev || ts > prev.ts) jobSubLatest.set(k, { ts, value: !!c.value, author, jobId }) + subMsgs.push({ ts: dec.value.timestamp || m.timestamp || 0, value: !!c.value, author, jobId: c.jobId }) } catch {} } } - return { tomb, jobNodes, parent, child, rootOf, tipOf, tipByRoot, subsByJob } + const vg = versionGroups(Array.from(jobNodes.values()).map(n => ({ key: n.key, author: n.author, seq: n.seq, ts: n.ts, replaces: typeof n.c.replaces === "string" ? n.c.replaces : null, claimedAuthor: typeof n.c.author === "string" ? n.c.author : null }))) + + const jobSubLatest = new Map() + for (const sub of subMsgs) { + const jobId = vg.rootOf(sub.jobId) + const k = `${jobId}::${sub.author}` + const prev = jobSubLatest.get(k) + if (!prev || sub.ts >= prev.ts) jobSubLatest.set(k, { ...sub, jobId }) + } + + const subsByJob = new Map() + for (const { jobId, author, value } of jobSubLatest.values()) { + if (!subsByJob.has(jobId)) subsByJob.set(jobId, new Set()) + const set = subsByJob.get(jobId) + if (value) set.add(author) + else set.delete(author) + } + + return { tomb, jobNodes, vg, subsByJob } } const buildJobObject = (node, rootId, subscribers) => { @@ -177,6 +140,7 @@ module.exports = ({ cooler, tribeCrypto }) => { status: c.status || "OPEN", tags: Array.isArray(c.tags) ? c.tags : normalizeTags(c.tags), subscribers: Array.isArray(visibleSubs) ? visibleSubs : [], + industry: c.industry || "", mapUrl: c.mapUrl || "", visibility: String(c.visibility || "PUBLIC").toUpperCase() === "HIDDEN" ? "HIDDEN" : "PUBLIC", clearnetPublic: !!c.clearnetPublic @@ -235,6 +199,7 @@ module.exports = ({ cooler, tribeCrypto }) => { createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), status: "OPEN", + industry: String(jobData.industry || "").trim(), mapUrl: String(jobData.mapUrl || "").trim(), visibility: String(jobData.visibility || "PUBLIC").toUpperCase() === "HIDDEN" ? "HIDDEN" : "PUBLIC", clearnetPublic: jobData.clearnetPublic === true || jobData.clearnetPublic === 'true' || jobData.clearnetPublic === 'on' @@ -253,26 +218,20 @@ module.exports = ({ cooler, tribeCrypto }) => { async resolveCurrentId(jobId) { const ssbClient = await openSsb() const messages = await readAll(ssbClient) - const { tomb, child } = buildIndex(messages, ssbClient) + const { tomb, vg } = buildIndex(messages, ssbClient) - let cur = jobId - while (child.has(cur)) cur = child.get(cur) - if (tomb.has(cur)) throw new Error("Job not found") + const cur = vg.tipOf(jobId) + if (tomb.has(cur) || vg.isForeignCopy(vg.rootOf(cur))) throw new Error("Job not found") return cur }, async resolveRootId(jobId) { const ssbClient = await openSsb() const messages = await readAll(ssbClient) - const { tomb, parent, child } = buildIndex(messages, ssbClient) + const { tomb, vg } = buildIndex(messages, ssbClient) - let tip = jobId - while (child.has(tip)) tip = child.get(tip) - if (tomb.has(tip)) throw new Error("Job not found") - - let root = tip - while (parent.has(root)) root = parent.get(root) - return root + if (tomb.has(vg.tipOf(jobId)) || vg.isForeignCopy(vg.rootOf(jobId))) throw new Error("Job not found") + return vg.rootOf(jobId) }, async updateJob(id, jobData) { @@ -466,7 +425,8 @@ module.exports = ({ cooler, tribeCrypto }) => { const idx = buildIndex(messages, ssbClient) const jobs = [] - for (const [rootId, tipId] of idx.tipByRoot.entries()) { + for (const rootId of idx.vg.roots) { + const tipId = idx.vg.tipOf(rootId) if (idx.tomb.has(tipId)) continue const node = idx.jobNodes.get(tipId) if (!node) continue @@ -521,14 +481,11 @@ module.exports = ({ cooler, tribeCrypto }) => { const viewer = viewerId || ssbClient.id const messages = await readAll(ssbClient) - const idx = buildIndex(messages, ssbClient) + const idx = memoIndex('jobs', messages, (m) => buildIndex(m, ssbClient)) - let tipId = id - while (idx.child.has(tipId)) tipId = idx.child.get(tipId) - if (idx.tomb.has(tipId)) throw new Error("Job not found") - - let rootId = tipId - while (idx.parent.has(rootId)) rootId = idx.parent.get(rootId) + const tipId = idx.vg.tipOf(id) + const rootId = idx.vg.rootOf(id) + if (idx.tomb.has(tipId) || idx.vg.isForeignCopy(rootId)) throw new Error("Job not found") const gate = (job) => { if (job.visibility === "HIDDEN" && job.author !== viewer) throw new Error("Job not found") @@ -538,7 +495,7 @@ module.exports = ({ cooler, tribeCrypto }) => { const node = idx.jobNodes.get(tipId) if (!node) { const msg = await new Promise((r, j) => ssbClient.get(tipId, (e, m) => e ? j(e) : r(m))) - if (!msg || !msg.content) throw new Error("Job not found") + if (!msg || !msg.content || msg.content.type !== "job" || (msg.content.author && msg.content.author !== msg.author)) throw new Error("Job not found") const tmpNode = { key: tipId, ts: msg.timestamp || 0, c: msg.content, author: msg.author } const subsSet = idx.subsByJob.get(rootId) || new Set() const subs = Array.from(subsSet) diff --git a/src/models/karma_model.js b/src/models/karma_model.js new file mode 100644 index 0000000..c500986 --- /dev/null +++ b/src/models/karma_model.js @@ -0,0 +1,591 @@ +const pull = require("../server/node_modules/pull-stream"); +const { readTyped, CONTENT_TYPES } = require("./typed_log"); +const { getConfig } = require("../configs/config-manager.js"); +const { config } = require("../server/SSB_server.js"); + +function getLogLimit() { + return getConfig().ssbLogStream?.limit || 1000; +} + +const OWN_SCORE_CAP = 200; +const RECEIVED_AUTHOR_MIN_AGE_MS = 30 * 24 * 60 * 60 * 1000; +const opinionCategories = require("../backend/opinion_categories"); +const CONSTRUCTIVE_OPINIONS = new Set(opinionCategories.karma.constructive); +const MODERATION_OPINIONS = new Set(opinionCategories.karma.moderation); +const NEUTRAL_OPINIONS = new Set(opinionCategories.karma.neutral); +const NEGATIVE_MIN_VOTERS = 3; +const KARMA_PERIOD_MS = 30 * 24 * 60 * 60 * 1000; +const NEGATIVE_PATTERN_MIN_VOTERS = 5; +const NEGATIVE_PATTERN_POINTS = 2; +const POSITIVE_PER_VOTER_CAP = 10; +const TRUST_MIN_FOLLOWERS = 2; +const REACTIONS_PER_MESSAGE_CAP = 10; +const PLAYS_PER_EPISODE_CAP = 20; +const ACTIONS_CACHE_TTL_MS = 60 * 1000; + +function resolveUserId(maybeId) { + const s = String(maybeId || "").trim(); + if (s) return s; + return config?.keys?.id || ""; +} + +function idsEqual(a, b) { + if (!a || !b) return false; + const A = String(a).trim(); + const B = String(b).trim(); + if (A === B) return true; + const strip = s => s.replace(/^@/, "").replace(/\.ed25519$/, ""); + return strip(A) === strip(B); +} + + function inferType(c = {}) { + if (c.vote) return "vote"; + if (c.votes) return "votes"; + if (c.address && c.coin === "ECO" && c.type === "wallet") return "bankWallet"; + if (c.type === "ubiClaimResult" && c.txid && c.epochId) return "ubiClaimResult"; + if (typeof c.amount !== "undefined" && c.epochId && c.allocationId) return "bankClaim"; + if (typeof c.item_type !== "undefined" && typeof c.status !== "undefined") return "market"; + if (typeof c.goal !== "undefined" && typeof c.progress !== "undefined") return "project"; + if (typeof c.members !== "undefined" && typeof c.isAnonymous !== "undefined") return "tribe"; + if (typeof c.date !== "undefined" && typeof c.location !== "undefined") return "event"; + if (typeof c.priority !== "undefined" && typeof c.status !== "undefined" && c.title) return "task"; + if (typeof c.confirmations !== "undefined" && typeof c.severity !== "undefined") return "report"; + if (typeof c.job_type !== "undefined" && typeof c.status !== "undefined") return "job"; + if (typeof c.url !== "undefined" && typeof c.mimeType !== "undefined" && c.type === "audio") return "audio"; + if (typeof c.url !== "undefined" && typeof c.mimeType !== "undefined" && c.type === "video") return "video"; + if (typeof c.url !== "undefined" && c.title && c.key) return "document"; + if (typeof c.text !== "undefined" && typeof c.refeeds !== "undefined") return "feed"; + if (typeof c.text !== "undefined" && typeof c.contentWarning !== "undefined") return "post"; + if (typeof c.contact !== "undefined") return "contact"; + if (typeof c.about !== "undefined") return "about"; + if (typeof c.concept !== "undefined" && typeof c.amount !== "undefined" && c.status) return "transfer"; + return ""; + } + + function normalizeType(a) { + const t = a.type || a.content?.type || inferType(a.content) || ""; + return String(t).toLowerCase(); + } + + function priorityBump(p) { + const s = String(p || "").toUpperCase(); + if (s === "HIGH") return 3; + if (s === "MEDIUM") return 1; + return 0; + } + + function severityBump(s) { + const x = String(s || "").toUpperCase(); + if (x === "CRITICAL") return 6; + if (x === "HIGH") return 4; + if (x === "MEDIUM") return 2; + return 0; + } + + function scoreMarket(c) { + const st = String(c.status || "").toUpperCase(); + let s = 5; + if (st === "SOLD") s += 8; + else if (st === "ACTIVE") s += 3; + const bids = Array.isArray(c.auctions_poll) ? c.auctions_poll.length : 0; + s += Math.min(10, bids); + return s; + } + + function scoreProject(c) { + const st = String(c.status || "ACTIVE").toUpperCase(); + const prog = Number(c.progress || 0); + let s = 8 + Math.min(10, prog / 10); + if (st === "FUNDED") s += 10; + return s; + } + + function calculateOpinionScore(content) { + const cats = content?.opinions || {}; + let s = 0; + for (const k in cats) { + if (!Object.prototype.hasOwnProperty.call(cats, k)) continue; + if (NEUTRAL_OPINIONS.has(k)) continue; + if (k === "interesting" || k === "inspiring") s += 5; + else if (MODERATION_OPINIONS.has(k)) s -= 5; + else if (CONSTRUCTIVE_OPINIONS.has(k)) s -= 3; + else s += 1; + } + return s; + } + +function basePointsFor(action) { + const t = normalizeType(action); + const c = action.content || {}; + const rawType = String(c.type || "").toLowerCase(); + if (t === "post") return 10; + else if (t === "comment") return 5; + else if (t === "like") return 2; + else if (t === "image") return 8; + else if (t === "video") return 12; + else if (t === "audio") return 8; + else if (t === "document") return 6; + else if (t === "bookmark") return 2; + else if (t === "feed") return 6; + else if (t === "forum") return (c.root ? 5 : 10); + else if (t === "vote") return (3 + calculateOpinionScore(c)); + else if (t === "votes") return Math.min(10, Number(c.totalVotes || 0)); + else if (t === "market") return scoreMarket(c); + else if (t === "project") return scoreProject(c); + else if (t === "tribe") return (6 + Math.min(10, Array.isArray(c.members) ? c.members.length * 0.5 : 0)); + else if (t === "event") return (4 + Math.min(10, Array.isArray(c.attendees) ? c.attendees.length : 0)); + else if (t === "task") return (3 + priorityBump(c.priority)); + else if (t === "report") return (4 + (Array.isArray(c.confirmations) ? c.confirmations.length : 0) + severityBump(c.severity)); + else if (t === "curriculum") return 5; + else if (t === "wikipage") return (c.replaces ? 2 : 6); + else if (t === "emergency") return (c.replaces ? 1 : 5); + else if (t === "emergencyconfirm") return 1; + else if (t === "emergencyupdate") return 2; + else if (t === "mailinglist") return (c.replaces ? 1 : 3); + else if (t === "logisticsroute") return (c.replaces ? 1 : 4); + else if (t === "logisticsrating") return 1; + else if (t === "podcast") return (c.replaces ? 1 : 5); + else if (t === "podcastepisode") return (c.replaces ? 1 : 4); + else if (t === "podcastopinion" || t === "podcastplay" || t === "campaignopinion" || t === "logisticsopinion") return 1; + else if (t === "campaign") return (c.replaces ? 1 : 5); + else if (t === "campaignsignature") return 1; + else if (t === "campaignupdate") return 2; + else if (t === "aiexchange") return (Array.isArray(c.ctx) ? Math.min(10, c.ctx.length) : 0); + else if (t === "job") return (4 + (Array.isArray(c.subscribers) ? c.subscribers.length : 0)); + else if (t === "transfer") return 1; + else if (t === "about") return (1 + ((c.visibilityPrefs && c.visibilityPrefs.fediverseHandle) ? 8 : 0)); + else if (t === "contact") return 1; + else if (t === "pub") return 1; + else if (t === "parliamentcandidature" || rawType === "parliamentcandidature") return 12; + else if (t === "parliamentterm" || rawType === "parliamentterm") return 25; + else if (t === "parliamentproposal" || rawType === "parliamentproposal") return 8; + else if (t === "parliamentlaw" || rawType === "parliamentlaw") return 16; + else if (t === "parliamentrevocation" || rawType === "parliamentrevocation") return 10; + else if (t === "courts_case" || t === "courtscase" || rawType === "courts_case") return 4; + else if (t === "courts_evidence" || t === "courtsevidence" || rawType === "courts_evidence") return 3; + else if (t === "courts_answer" || t === "courtsanswer" || rawType === "courts_answer") return 4; + else if (t === "courts_verdict" || t === "courtsverdict" || rawType === "courts_verdict") return 10; + else if (t === "courts_settlement" || t === "courtssettlement" || rawType === "courts_settlement") return 8; + else if (t === "courts_nomination" || t === "courtsnomination" || rawType === "courts_nomination") return 6; + else if (t === "courts_nom_vote" || t === "courtsnomvote" || rawType === "courts_nom_vote") return 3; + else if (t === "courts_public_pref" || t === "courtspublicpref" || rawType === "courts_public_pref") return 1; + else if (t === "courts_mediators" || t === "courtsmediators" || rawType === "courts_mediators") return 6; + else if (t === "courts_open_support" || t === "courtsopensupport" || rawType === "courts_open_support") return 2; + else if (t === "courts_verdict_vote" || t === "courtsverdictvote" || rawType === "courts_verdict_vote") return 3; + else if (t === "courts_judge_assign" || t === "courtsjudgeassign" || rawType === "courts_judge_assign") return 5; + else if (t === "larpjoinhouse") return 15; + else if (t === "larphousepost") return 6; + else if (t === "larptestattempt") return 3; + else if (t === "torrent") return 6; + else if (t === "file") return 6; + else if (t === "shop" || t === "shopproduct") return 6; + else if (t === "shop-purchase") return 2; + else if (t === "pad" || t === "padentry") return 3; + else if (t === "room") return 3; + else if (t === "roommember") return 1; + else if (t === "calendar" || t === "calendarnote" || t === "calendardate") return 3; + else if (t === "chat") return 1; + else if (t === "gamescore") return 2; + else if (t === "pixelia") return 2; + else if (rawType === "industry") return 8; + else if (rawType === "industryblueprint") return 6; + else if (rawType === "industrybuild") return 8; + else if (rawType === "industrycontribution") return 4; + else if (rawType === "industryallocation") return 10; + else if (rawType === "industryvote") return 2; + else if (rawType === "industrymember") return 2; + else if (rawType === "industryopinion") return 1; + else if (rawType === "schoolcourse") return 10; + else if (rawType === "schoollesson") return 6; + else if (rawType === "schoolenroll") return 3; + else if (rawType === "schoolcertificate") return 12; + else if (rawType === "schoolexam") return 6; + else if (rawType === "schoolprogress") return 1; + else if (rawType === "schoolopinion") return 2; + return 0; +} + +function decayFor(ts, nowMs) { + const ageDays = ts ? (nowMs - ts) / 86400000 : Infinity; + return ageDays <= 30 ? 1.0 : ageDays <= 90 ? 0.5 : 0.25; +} + +function scoreFromActions(actions) { + let score = 0; + const nowMs = Date.now(); + const perTypeDay = new Map(); + for (const action of actions) { + const pts = basePointsFor(action); + if (!pts) continue; + const ts = action.value?.timestamp; + const dayKey = `${normalizeType(action)}:${ts ? new Date(ts).toISOString().slice(0, 10) : "?"}`; + const n = (perTypeDay.get(dayKey) || 0) + 1; + perTypeDay.set(dayKey, n); + score += pts * decayFor(ts, nowMs) / n; + } + return Math.max(0, Math.round(Math.min(OWN_SCORE_CAP, score))); +} + +function receivedBreakdownFor(userId, actions) { + const nowMs = Date.now(); + const mine = new Set(); + const firstTs = new Map(); + const follows = new Map(); + for (const a of actions) { + if (!a.author) continue; + const ts = Number(a.receivedAt) || 0; + if (ts && (!firstTs.has(a.author) || ts < firstTs.get(a.author))) firstTs.set(a.author, ts); + if (a.author === userId && a.id) mine.add(a.id); + const c = a.content || {}; + if (c.type === "contact" && typeof c.contact === "string" && c.contact !== a.author) { + const k = `${a.author}|${c.contact}`; + const at = Number(a.value?.timestamp) || ts; + const prev = follows.get(k); + if (!prev || at >= prev.at) follows.set(k, { on: c.following === true && c.blocking !== true, at }); + } + } + const followersOf = new Map(); + for (const [k, f] of follows) { + if (!f.on) continue; + const [from, to] = k.split("|"); + if (!followersOf.has(to)) followersOf.set(to, new Set()); + followersOf.get(to).add(from); + } + const known = (id) => firstTs.has(id) && nowMs - firstTs.get(id) >= RECEIVED_AUTHOR_MIN_AGE_MS; + const trusted = (voter, sameSide) => { + let n = 0; + for (const f of followersOf.get(voter) || []) { + if (f === userId || sameSide.has(f) || !known(f)) continue; + if (++n >= TRUST_MIN_FOLLOWERS) return true; + } + return false; + }; + const seen = new Set(); + const perTarget = new Map(); + const negatives = new Map(); + const votes = new Map(); + const reactions = new Map(); + const credits = []; + let positive = 0; + let negative = 0; + const add = (author, kind, target, pts, ts, capPerTarget = Infinity) => { + const k = `${kind}:${author}:${target}`; + if (seen.has(k)) return; + const n = perTarget.get(`${kind}:${target}`) || 0; + if (n >= capPerTarget) return; + seen.add(k); + perTarget.set(`${kind}:${target}`, n + 1); + credits.push({ voter: author, pts, ts: Number(ts) || 0 }); + }; + const addNegative = (author, target, pts, ts) => { + const k = `neg:${author}:${target}`; + if (seen.has(k)) return; + seen.add(k); + if (!negatives.has(target)) negatives.set(target, []); + negatives.get(target).push({ voter: author, pts, ts: Number(ts) || 0 }); + }; + for (const a of actions) { + const b = a.author; + if (!b || b === userId) continue; + if (!firstTs.has(b) || nowMs - firstTs.get(b) < RECEIVED_AUTHOR_MIN_AGE_MS) continue; + const c = a.content || {}; + const type = String(c.type || ""); + const ts = a.value?.timestamp; + if (/Opinion$/.test(type) && type !== "pollOpinion" && mine.has(c.target)) { + const cat = String(c.category || ""); + if (NEUTRAL_OPINIONS.has(cat)) continue; + if (MODERATION_OPINIONS.has(cat)) addNegative(b, c.target, -5, ts); + else if (CONSTRUCTIVE_OPINIONS.has(cat)) addNegative(b, c.target, -3, ts); + else add(b, "opinion", c.target, 3, ts); + } + else if (type === "vote" && c.vote && mine.has(c.vote.link)) { + const k = `${b}:${c.vote.link}`; + const prev = votes.get(k); + if (!prev || (Number(ts) || 0) >= prev.ts) votes.set(k, { author: b, target: c.vote.link, value: Number(c.vote.value) || 0, ts: Number(ts) || 0 }); + } + else if (type === "feed-action" && c.action === "vote" && mine.has(c.root || c.target)) add(b, "opinion", c.root || c.target, 3, ts); + else if (type === "feed-action" && c.action === "comment" && mine.has(c.root)) add(b, "comment", c.root, 2, ts, 10); + else if (type === "feed-action" && c.action === "refeed" && mine.has(c.root)) add(b, "refeed", c.root, 2, ts, 10); + else if (type === "post" && c.root && mine.has(c.root)) add(b, "comment", c.root, 2, ts, 10); + else if (type === "contact" && c.following === true && c.contact === userId) add(b, "follow", userId, 2, ts); + else if (type === "chatReaction" && mine.has(c.target)) { + const k = `${b}:${c.target}:${c.emoji}`; + const prev = reactions.get(k); + if (!prev || (Number(ts) || 0) >= prev.ts) reactions.set(k, { author: b, target: c.target, emoji: c.emoji, on: c.on !== false, ts: Number(ts) || 0 }); + } + else if (type === "podcastPlay" && mine.has(c.target)) add(b, "play", c.target, 1, ts, PLAYS_PER_EPISODE_CAP); + else if (type === "marketPurchase" && mine.has(c.target)) add(b, "purchase", c.target, 5, ts); + else if (type === "shop-purchase" && c.seller === userId) add(b, "purchase", c.productId || a.id, 5, ts); + else if (type === "schoolEnroll" && c.value === true && mine.has(c.courseId)) add(b, "enroll", c.courseId, 5, ts); + else if (type === "transferConfirm" && mine.has(c.target)) add(b, "confirm", c.target, 3, ts); + } + for (const r of reactions.values()) { + if (!r.on) continue; + if (r.emoji === "down") addNegative(r.author, r.target, -3, r.ts); + else add(r.author, "reaction", r.target, 1, r.ts, REACTIONS_PER_MESSAGE_CAP); + } + for (const v of votes.values()) { + if (v.value > 0) add(v.author, "opinion", v.target, 3, v.ts); + else if (v.value < 0) addNegative(v.author, v.target, -3, v.ts); + } + const vouched = new Map(); + const isTrusted = (voter, side, cache) => { + if (!cache.has(voter)) cache.set(voter, trusted(voter, side)); + return cache.get(voter); + }; + const usedByVoter = new Map(); + for (const c of credits.sort((x, y) => x.ts - y.ts)) { + if (!isTrusted(c.voter, new Set(), vouched)) continue; + const k = `${c.voter}:${Math.floor(c.ts / KARMA_PERIOD_MS)}`; + const used = usedByVoter.get(k) || 0; + const take = Math.min(c.pts, POSITIVE_PER_VOTER_CAP - used); + if (take <= 0) continue; + usedByVoter.set(k, used + take); + positive += take * decayFor(c.ts, nowMs); + } + const blaming = new Set(); + for (const list of negatives.values()) for (const n of list) blaming.add(n.voter); + const vouchedAgainst = new Map(); + const pattern = new Map(); + for (const all of negatives.values()) { + const list = all.filter(n => isTrusted(n.voter, blaming, vouchedAgainst)); + if (list.length >= NEGATIVE_MIN_VOTERS) { + for (const n of list) negative += Math.abs(n.pts) * decayFor(n.ts, nowMs); + continue; + } + for (const n of list) { + if (!n.ts || nowMs - n.ts > KARMA_PERIOD_MS) continue; + if ((pattern.get(n.voter) || 0) < n.ts) pattern.set(n.voter, n.ts); + } + } + if (pattern.size >= NEGATIVE_PATTERN_MIN_VOTERS) { + for (const ts of pattern.values()) negative += NEGATIVE_PATTERN_POINTS * decayFor(ts, nowMs); + } + positive = Math.round(positive); + negative = Math.round(negative); + return { score: positive - negative, positive, negative }; +} + +function receivedScoreFor(userId, actions) { + return receivedBreakdownFor(userId, actions).score; +} + +module.exports = ({ services, ranges } = {}) => { + const normalizeRange = ranges && ranges.normalizeRange ? ranges.normalizeRange : (r) => String(r || "all"); + const widenRange = ranges && ranges.widenRange ? ranges.widenRange : (arr, r) => normalizeRange(r); + const filterByRange = ranges && ranges.filterByRange ? ranges.filterByRange : (arr) => arr; + + let ssbInstance; + async function openSsb() { + if (ssbInstance) return ssbInstance; + if (services?.cooler?.open) ssbInstance = await services.cooler.open(); + else if (global.ssb) ssbInstance = global.ssb; + else { + try { + const srv = require("../server/SSB_server.js"); + ssbInstance = srv?.ssb || srv?.server || srv?.default || null; + } catch (_) { + ssbInstance = null; + } + } + return ssbInstance; + } + + let actionsCache = null; + async function listAllActions() { + const ssb = await openSsb(); + if (!ssb) { + return []; + } + const ownSeq = await ownFeedSeq(ssb); + if (actionsCache && actionsCache.ownSeq === ownSeq && Date.now() - actionsCache.ts < ACTIONS_CACHE_TTL_MS) return actionsCache.value; + const msgs = await readTyped(ssb, CONTENT_TYPES, { limit: Math.max(getLogLimit(), 20000) }); + const value = msgs.map(m => { + const v = m.value || {}; + const c = v.content || {}; + return { + id: v.key || m.key, + author: v.author, + type: (c.type || "").toLowerCase(), + value: v, + content: c, + receivedAt: Number(m.timestamp) || 0 + }; + }); + actionsCache = { ts: Date.now(), ownSeq, value }; + return value; + } + +async function ownFeedSeq(ssb) { + return new Promise((res) => { + try { + pull( + ssb.createUserStream({ id: ssb.id, reverse: true, limit: 1 }), + pull.collect((err, msgs) => res(err || !msgs || !msgs.length ? 0 : (msgs[0].value && msgs[0].value.sequence) || 0)) + ); + } catch (_) { res(0); } + }); +} + +async function publishKarmaScore(userId, karmaScore) { + const ssb = await openSsb(); + if (!ssb || !ssb.publish) return false; + if (!(await ownFeedSeq(ssb))) return false; + const timestamp = new Date().toISOString(); + const content = { type: "karmaScore", karmaScore, userId, timestamp }; + return new Promise((resolve, reject) => { + ssb.publish(content, (err, msg) => err ? reject(err) : resolve(msg)); + }); +} + +async function fetchUserActions(userId) { + const me = resolveUserId(userId); + const actions = await listAllActions(); + const authored = actions.filter(a => + (a.author && a.author === me) || (a.value?.author && a.value.author === me) + ); + if (authored.length) return authored; + return actions.filter(a => { + const c = a.content || {}; + const fields = [c.author, c.organizer, c.seller, c.about, c.contact]; + return fields.some(f => f && f === me); + }); +} + +async function getCarbonGramsForUser(userId) { + const ssb = await openSsb(); + if (!ssb || !userId) return 0; + return new Promise((resolve) => { + let bytes = 0; + pull( + ssb.createUserStream({ id: userId }), + pull.drain( + (m) => { try { bytes += Buffer.byteLength(JSON.stringify(m && m.value), 'utf8'); } catch (_) {} }, + () => resolve((bytes / (1024 * 1024)) * 0.095) + ) + ); + }); +} + +async function getUserEngagementScore(userId) { + const ssb = await openSsb(); + const uid = resolveUserId(userId); + const actions = await fetchUserActions(uid); + const all = await listAllActions(); + const rawKarma = scoreFromActions(actions) + receivedScoreFor(uid, all); + const carbonGrams = await getCarbonGramsForUser(uid).catch(() => 0); + const karmaScore = Math.max(0, Math.round(rawKarma - carbonGrams)); + + const prev = await getLastKarmaScore(uid); + const lastPublishedTimestamp = await getLastPublishedTimestamp(uid); + + const isSelf = idsEqual(uid, ssb.id); + const hasSSB = !!(ssb && ssb.publish); + + const changed = (prev === null) || (karmaScore !== prev); + const nowMs = Date.now(); + const lastMs = lastPublishedTimestamp ? new Date(lastPublishedTimestamp).getTime() : 0; + const cooldownOk = (nowMs - lastMs) >= 24 * 60 * 60 * 1000; + + if (isSelf && hasSSB && changed && cooldownOk) { + await publishKarmaScore(uid, karmaScore); + } + return karmaScore; +} + +async function getLastKarmaScore(userId) { + const ssb = await openSsb(); + if (!ssb) return null; + return new Promise((resolve) => { + const source = ssb.messagesByType + ? ssb.messagesByType({ type: "karmaScore", reverse: true }) + : ssb.createLogStream && ssb.createLogStream({ reverse: true }); + if (!source) return resolve(null); + pull( + source, + pull.filter(msg => { + const v = msg.value || msg; + const c = v.content || {}; + return c && c.type === "karmaScore" && c.userId === userId; + }), + pull.take(1), + pull.collect((err, arr) => { + if (err || !arr || !arr.length) return resolve(null); + const v = arr[0].value || arr[0]; + const c = v.content || {}; + resolve(Number(c.karmaScore) || 0); + }) + ); + }); +} + +async function getLastPublishedTimestamp(userId) { + const ssb = await openSsb(); + if (!ssb) return new Date(0).toISOString(); + const fallback = new Date(0).toISOString(); + return new Promise((resolve) => { + const source = ssb.messagesByType + ? ssb.messagesByType({ type: "karmaScore", reverse: true }) + : ssb.createLogStream && ssb.createLogStream({ reverse: true }); + if (!source) return resolve(fallback); + pull( + source, + pull.filter(msg => { + const v = msg.value || msg; + const c = v.content || {}; + return c && c.type === "karmaScore" && c.userId === userId; + }), + pull.take(1), + pull.collect((err, arr) => { + if (err || !arr || !arr.length) return resolve(fallback); + const v = arr[0].value || arr[0]; + const c = v.content || {}; + resolve(c.timestamp || fallback); + }) + ); + }); +} + + async function getKarmaBreakdown(userId) { + const uid = resolveUserId(userId); + const received = receivedBreakdownFor(uid, await listAllActions()); + return { karmaScore: await getUserEngagementScore(uid), positive: received.positive, negative: received.negative }; + } + + async function listKarmaHistory(userId, range = "all", explicit = true) { + const ssb = await openSsb(); + if (!ssb) return { points: [], hasAny: false, range: normalizeRange(range) }; + const uid = resolveUserId(userId); + const msgs = await readTyped(ssb, ["karmaScore"], { limit: Math.max(getLogLimit(), 5000) }); + const all = msgs + .filter(m => m.value && m.value.author === uid && m.value.content && m.value.content.type === "karmaScore") + .map(m => ({ ts: Date.parse(m.value.content.timestamp || "") || Number(m.value.timestamp) || 0, score: Number(m.value.content.karmaScore) || 0 })) + .filter(x => x.ts > 0) + .sort((a, b) => a.ts - b.ts); + const effective = explicit ? normalizeRange(range) : widenRange(all, range); + return { points: filterByRange(all, effective).slice(-120), hasAny: all.length >= 2, range: effective }; + } + + return { + listAllActions, + fetchUserActions, + scoreFromActions, + receivedBreakdownFor, + receivedScoreFor, + getCarbonGramsForUser, + getUserEngagementScore, + getKarmaBreakdown, + getLastKarmaScore, + getLastPublishedTimestamp, + publishKarmaScore, + listKarmaHistory, + basePointsFor, + decayFor, + NEGATIVE_MIN_VOTERS, + NEGATIVE_PATTERN_MIN_VOTERS, + NEGATIVE_PATTERN_POINTS, + POSITIVE_PER_VOTER_CAP, + KARMA_PERIOD_MS, + TRUST_MIN_FOLLOWERS + }; +}; diff --git a/src/models/larp_model.js b/src/models/larp_model.js index e495d69..3c72b0e 100644 --- a/src/models/larp_model.js +++ b/src/models/larp_model.js @@ -1,4 +1,5 @@ const pull = require('../server/node_modules/pull-stream'); +const { readTyped, requestScope } = require('./typed_log'); const fs = require('fs'); const path = require('path'); @@ -10,6 +11,7 @@ const HOUSE_KEYS = ['academia','solaris','arrakis','terraverde','unsystem','dogm const VALID_KEY = (k) => HOUSE_KEYS.includes(String(k || '').toLowerCase()); const TEST_COOLDOWN_MS = 30 * 24 * 60 * 60 * 1000; +const ACADEMIA_GRACE_MS = 10 * 60 * 1000; const PROFILE_QUESTIONS = [ { @@ -165,9 +167,20 @@ function getGoverningHouseKey(now = new Date()) { return HOUSE_KEYS[now.getMonth() % HOUSE_KEYS.length]; } +function getGoverningPeriodId(now = new Date()) { + const month = String(now.getMonth() + 1).padStart(2, '0'); + return `${getGoverningHouseKey(now)}:${now.getFullYear()}-${month}`; +} + module.exports = ({ cooler, tribesModel, tribeCrypto }) => { let ssb; const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb; }; + const startedAt = Date.now(); + let academiaRetry = null; + const followsSomeone = async (client, me) => { + const contacts = await readTyped(client, ['contact']).catch(() => []); + return contacts.some((m) => m && m.value && m.value.author === me && m.value.content && m.value.content.following === true); + }; const houseTribeTag = (houseKey) => { const h = HOUSES[houseKey]; @@ -187,7 +200,7 @@ module.exports = ({ cooler, tribesModel, tribeCrypto }) => { return tags.includes(tag) && members.includes(me); }); if (!candidates.length) return null; - candidates.sort((a, b) => new Date(a.createdAt || 0).getTime() - new Date(b.createdAt || 0).getTime()); + candidates.sort((a, b) => (new Date(a.createdAt || 0).getTime() - new Date(b.createdAt || 0).getTime()) || String(a.id || '').localeCompare(String(b.id || ''))); return candidates[0]; } @@ -201,78 +214,59 @@ module.exports = ({ cooler, tribesModel, tribeCrypto }) => { return tags.includes(tag); }); if (!candidates.length) return null; - candidates.sort((a, b) => new Date(a.createdAt || 0).getTime() - new Date(b.createdAt || 0).getTime()); + candidates.sort((a, b) => (new Date(a.createdAt || 0).getTime() - new Date(b.createdAt || 0).getTime()) || String(a.id || '').localeCompare(String(b.id || ''))); return candidates[0]; } + const ANCHOR_TYPES = ['larpHouseTribeAnchor', 'larpHouseTribeAnchorTombstone', 'tombstone']; + const authorOfMsg = (client, key) => new Promise((resolve) => client.get(key, (err, v) => resolve(err || !v ? null : v.author || null))); + async function findEarliestHouseAnchor(houseKey) { if (!VALID_KEY(houseKey)) return null; const client = await openSsb(); - return new Promise((resolve) => { - const anchors = []; - const anchorTombstones = []; - const tribeTombstones = new Map(); - const msgAuthorByKey = new Map(); - pull( - client.createLogStream(), - pull.drain((m) => { - if (!m || !m.value) return; - const author = m.value.author; - if (m.key && author) msgAuthorByKey.set(m.key, author); - const c = m.value.content; - if (!c || typeof c !== 'object') return; - if (c.type === 'larpHouseTribeAnchor') { - if (c.house !== houseKey) return; - if (typeof c.tribeRootId !== 'string') return; - const tribeTs = Number(Date.parse(c.tribeCreatedAt || '')) || m.value.timestamp || 0; - anchors.push({ tribeRootId: c.tribeRootId, anchorAuthor: author, tribeTs }); - } else if (c.type === 'larpHouseTribeAnchorTombstone') { - if (c.house !== houseKey) return; - if (typeof c.tribeRootId !== 'string') return; - anchorTombstones.push({ tribeRootId: c.tribeRootId, tombstoneAuthor: author }); - } else if (c.type === 'tombstone' && typeof c.target === 'string') { - tribeTombstones.set(c.target, author); - } - }, () => { - const killedAnchorPairs = new Set(); - for (const t of anchorTombstones) { - killedAnchorPairs.add(`${t.tribeRootId}|${t.tombstoneAuthor}`); - } - const deadTribes = new Set(); - for (const [target, tombAuthor] of tribeTombstones) { - const tribeAuthor = msgAuthorByKey.get(target); - if (tribeAuthor && tribeAuthor === tombAuthor) deadTribes.add(target); - } - const live = anchors.filter(a => - !killedAnchorPairs.has(`${a.tribeRootId}|${a.anchorAuthor}`) && - !deadTribes.has(a.tribeRootId) - ); - if (!live.length) return resolve(null); - live.sort((a, b) => a.tribeTs - b.tribeTs); - const first = live[0]; - resolve({ tribeRootId: first.tribeRootId, author: first.anchorAuthor, tribeTs: first.tribeTs }); - }) - ); - }); + const msgs = await readTyped(client, ANCHOR_TYPES); + const anchors = []; + const killedAnchorPairs = new Set(); + const tombAuthors = new Map(); + for (const m of msgs) { + const c = m && m.value && m.value.content; + if (!c || typeof c !== 'object') continue; + const author = m.value.author; + if (c.type === 'larpHouseTribeAnchor') { + if (c.house !== houseKey || typeof c.tribeRootId !== 'string') continue; + const tribeTs = Number(Date.parse(c.tribeCreatedAt || '')) || m.value.timestamp || 0; + anchors.push({ tribeRootId: c.tribeRootId, anchorAuthor: author, tribeTs }); + } else if (c.type === 'larpHouseTribeAnchorTombstone') { + if (c.house !== houseKey || typeof c.tribeRootId !== 'string') continue; + killedAnchorPairs.add(`${c.tribeRootId}|${author}`); + } else if (c.type === 'tombstone' && typeof c.target === 'string') { + if (!tombAuthors.has(c.target)) tombAuthors.set(c.target, new Set()); + tombAuthors.get(c.target).add(author); + } + } + const candidates = anchors.filter(a => !killedAnchorPairs.has(`${a.tribeRootId}|${a.anchorAuthor}`)); + const deadTribes = new Set(); + await Promise.all([...new Set(candidates.map(a => a.tribeRootId).filter(t => tombAuthors.has(t)))].map(async (t) => { + const tribeAuthor = await authorOfMsg(client, t); + if (tribeAuthor && tombAuthors.get(t).has(tribeAuthor)) deadTribes.add(t); + })); + const live = candidates.filter(a => !deadTribes.has(a.tribeRootId)); + if (!live.length) return null; + live.sort((a, b) => a.tribeTs - b.tribeTs); + const first = live[0]; + return { tribeRootId: first.tribeRootId, author: first.anchorAuthor, tribeTs: first.tribeTs }; } async function findHouseAnchorByTribe(houseKey, tribeRootId) { if (!VALID_KEY(houseKey) || !tribeRootId) return null; const client = await openSsb(); - return new Promise((resolve) => { - let hit = null; - pull( - client.createLogStream(), - pull.drain((m) => { - if (hit) return; - const c = m && m.value && m.value.content; - if (!c || c.type !== 'larpHouseTribeAnchor') return; - if (c.house !== houseKey) return; - if (c.tribeRootId !== tribeRootId) return; - hit = { author: m.value.author, ts: m.value.timestamp || 0 }; - }, () => resolve(hit)) - ); - }); + const msgs = await readTyped(client, ANCHOR_TYPES); + for (const m of msgs) { + const c = m && m.value && m.value.content; + if (!c || c.type !== 'larpHouseTribeAnchor' || c.house !== houseKey || c.tribeRootId !== tribeRootId) continue; + return { author: m.value.author, ts: m.value.timestamp || 0 }; + } + return null; } async function publishHouseTribeAnchor(houseKey, tribeRootId, tribeCreatedAt) { @@ -327,7 +321,7 @@ module.exports = ({ cooler, tribesModel, tribeCrypto }) => { const canonical = await findAnyHouseTribe(houseKey).catch(() => null); const canonRoot = canonical ? await tribesModel.getRootId(canonical.id).catch(() => canonical.id) : null; const myTribes = await listMyHouseTribes(houseKey); - myTribes.sort((a, b) => a.createdAtTs - b.createdAtTs); + myTribes.sort((a, b) => (a.createdAtTs - b.createdAtTs) || String(a.rootId || '').localeCompare(String(b.rootId || ''))); const keepRoot = canonRoot || (myTribes[0] && myTribes[0].rootId); for (const t of myTribes) { if (t.rootId === keepRoot) continue; @@ -340,6 +334,13 @@ module.exports = ({ cooler, tribesModel, tribeCrypto }) => { await redeemPendingAutoInvites().catch(() => {}); return await findMyHouseTribe(houseKey) || canonical; } + if (Date.now() - startedAt < ACADEMIA_GRACE_MS && await followsSomeone(client, me)) { + if (!academiaRetry) { + academiaRetry = setTimeout(() => { academiaRetry = null; enqueue(runCatchup); }, ACADEMIA_GRACE_MS - (Date.now() - startedAt)); + if (academiaRetry.unref) academiaRetry.unref(); + } + return null; + } const house = HOUSES[houseKey] || {}; const tag = houseTribeTag(houseKey); try { @@ -455,6 +456,7 @@ module.exports = ({ cooler, tribesModel, tribeCrypto }) => { joinedAt: new Date().toISOString() }, (err, msg) => err ? reject(err) : resolve(msg)); }); + forgetMemberships(); if (previousHouse && previousHouse !== houseKey) { await leaveMyHouseTribe(previousHouse).catch(() => {}); } @@ -466,53 +468,45 @@ module.exports = ({ cooler, tribesModel, tribeCrypto }) => { async function getUserHouse(feedId) { const client = await openSsb(); const target = feedId || client.id; - return new Promise((resolve) => { - let latest = null; - let latestTs = 0; - pull( - client.createUserStream({ id: target, reverse: true }), - pull.drain((m) => { - const c = m && m.value && m.value.content; - if (!c) return; - const ts = m.value.timestamp || 0; - if (c.type === 'larpJoinHouse' && VALID_KEY(c.house)) { - if (ts > latestTs) { latestTs = ts; latest = c.house; } - } else if (c.type === 'larpLeaveLarp') { - if (ts > latestTs) { latestTs = ts; latest = null; } - } - }, () => resolve(latest)) - ); - }); + return (await listAllMemberships()).get(target) || null; } - async function listAllMemberships() { + async function readMemberships() { const client = await openSsb(); - return new Promise((resolve) => { - const byAuthor = new Map(); - pull( - client.createLogStream({ reverse: true }), - pull.drain((m) => { - const author = m && m.value && m.value.author; - if (!author) return; - const c = m.value.content; - if (!c) return; - const ts = m.value.timestamp || 0; - if (c.type === 'larpJoinHouse' && VALID_KEY(c.house)) { - const prev = byAuthor.get(author); - if (!prev || ts > prev.ts) byAuthor.set(author, { house: c.house, ts }); - } else if (c.type === 'larpLeaveLarp') { - const prev = byAuthor.get(author); - if (!prev || ts > prev.ts) byAuthor.set(author, { house: null, ts }); - } - }, () => { - const result = new Map(); - for (const [a, v] of byAuthor.entries()) { - if (v.house) result.set(a, v.house); - } - resolve(result); - }) - ); - }); + const msgs = await readTyped(client, ['larpJoinHouse', 'larpLeaveLarp'], { limit: 0 }); + const byAuthor = new Map(); + for (const m of msgs) { + const author = m && m.value && m.value.author; + if (!author) continue; + const c = m.value.content; + if (!c) continue; + const ts = m.value.timestamp || 0; + const seq = Number(m.value.sequence) || 0; + const prev = byAuthor.get(author); + const newer = !prev || ts > prev.ts || (ts === prev.ts && seq > prev.seq); + if (c.type === 'larpJoinHouse' && VALID_KEY(c.house)) { + if (newer) byAuthor.set(author, { house: c.house, ts, seq }); + } else if (c.type === 'larpLeaveLarp') { + if (newer) byAuthor.set(author, { house: null, ts, seq }); + } + } + const result = new Map(); + for (const [a, v] of byAuthor.entries()) { + if (v.house) result.set(a, v.house); + } + return result; + } + + const forgetMemberships = () => { + const store = requestScope.getStore(); + if (store) delete store.larpMemberships; + }; + + async function listAllMemberships() { + const store = requestScope.getStore(); + if (!store) return readMemberships(); + if (!store.larpMemberships) store.larpMemberships = readMemberships(); + return store.larpMemberships; } async function publishLeaveLarp() { @@ -525,6 +519,7 @@ module.exports = ({ cooler, tribesModel, tribeCrypto }) => { leftAt: new Date().toISOString() }, (err, msg) => err ? reject(err) : resolve(msg)); }); + forgetMemberships(); if (previousHouse) { await leaveMyHouseTribe(previousHouse).catch(() => {}); } @@ -568,18 +563,28 @@ module.exports = ({ cooler, tribesModel, tribeCrypto }) => { }); } + function wallIsPublic(houseKey, { isGoverning = false } = {}) { + return houseKey === 'academia' || isGoverning === true; + } + async function listHousePosts(houseKey, { viewerHouse = null, isGoverning = false } = {}) { if (!VALID_KEY(houseKey)) return []; const viewerIsMember = viewerHouse === houseKey; - if (!viewerIsMember && !isGoverning) return []; + if (!viewerIsMember && !wallIsPublic(houseKey, { isGoverning })) return []; const client = await openSsb(); const memberships = await listAllMemberships(); return new Promise((resolve) => { const posts = []; + const tombs = new Map(); pull( client.createLogStream({ reverse: true }), pull.drain((m) => { const c = m && m.value && m.value.content; + if (c && c.type === 'tombstone' && typeof c.target === 'string') { + if (!tombs.has(c.target)) tombs.set(c.target, new Set()); + tombs.get(c.target).add(m.value.author); + return; + } if (!c || c.type !== 'larpHousePost') return; if (c.house !== houseKey) return; const author = m.value.author; @@ -593,8 +598,9 @@ module.exports = ({ cooler, tribesModel, tribeCrypto }) => { ts: m.value.timestamp || 0 }); }, () => { - posts.sort((a, b) => b.ts - a.ts); - resolve(posts); + const shown = posts.filter(post => !(tombs.get(post.id) && tombs.get(post.id).has(post.author))); + shown.sort((a, b) => b.ts - a.ts); + resolve(shown); }) ); }); @@ -761,14 +767,20 @@ module.exports = ({ cooler, tribesModel, tribeCrypto }) => { const ssbKeys = require('../server/node_modules/ssb-keys'); const config = require('../server/ssb_config'); const codes = []; + const sealedOnly = client.private && typeof client.private.read === 'function'; await new Promise((resolve) => { pull( - client.createLogStream({ reverse: true, limit: 2000 }), + sealedOnly ? client.private.read({ reverse: true }) : client.createLogStream({ reverse: true, limit: 2000 }), pull.drain((m) => { const c = m && m.value && m.value.content; - if (typeof c !== 'string' || !c.endsWith('.box')) return; let decoded; - try { decoded = ssbKeys.unbox(c, config.keys); } catch (_) { return; } + if (typeof c === 'string') { + if (!c.endsWith('.box')) return; + try { decoded = ssbKeys.unbox(c, config.keys); } catch (_) { return; } + } else { + if (!m.value.private) return; + decoded = c; + } if (!decoded) return; if (typeof decoded === 'string') { try { decoded = JSON.parse(decoded); } catch (_) { return; } @@ -823,6 +835,13 @@ module.exports = ({ cooler, tribesModel, tribeCrypto }) => { await issueAutoInvitesForMyHouse().catch(() => {}); return; } + if (typeof c === 'object' && c.type === 'tribe' && Array.isArray(c.tags) && c.tags.includes(houseTribeTag('academia'))) { + if (m.value.author === me) return; + const myHouse = await getUserHouse(me).catch(() => null); + if (myHouse !== 'academia') return; + await ensureHouseTribe('academia').catch(() => {}); + return; + } if (typeof c === 'object' && c.type === 'larpHouseTribeAnchor' && VALID_KEY(c.house)) { if (m.value.author === me) return; const myHouse = await getUserHouse(me).catch(() => null); @@ -912,14 +931,17 @@ module.exports = ({ cooler, tribesModel, tribeCrypto }) => { return { HOUSES, HOUSE_KEYS, + wallIsPublic, TEST_COOLDOWN_MS, TEST_QUESTIONS_COUNT, PROFILE_QUESTIONS, computeCycle, getGoverningHouseKey, + getGoverningPeriodId, publishJoin, publishLeaveLarp, getUserHouse, + listAllMemberships, listHousesWithCounts, getMembersOfHouse, publishHousePost, diff --git a/src/models/legacy_model.js b/src/models/legacy_model.js deleted file mode 100644 index 3499811..0000000 --- a/src/models/legacy_model.js +++ /dev/null @@ -1,99 +0,0 @@ -const fs = require('fs'); -const path = require('path'); -const crypto = require('crypto'); -const os = require('os'); - -const MAGIC = Buffer.from('OASIS1'); -const SALT_LEN = 16; -const IV_LEN = 12; -const TAG_LEN = 16; - -function normalizePassword(password) { - if (password && typeof password === 'object' && password.password) password = password.password; - return String(password || ''); -} - -function deriveKey(password, salt) { - return crypto.scryptSync(password, salt, 32); -} - -function encryptBuffer(plaintext, password) { - const salt = crypto.randomBytes(SALT_LEN); - const iv = crypto.randomBytes(IV_LEN); - const key = deriveKey(password, salt); - const cipher = crypto.createCipheriv('aes-256-gcm', key, iv); - const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]); - const tag = cipher.getAuthTag(); - return Buffer.concat([MAGIC, salt, iv, tag, ciphertext]); -} - -function decryptBuffer(data, password) { - if (!Buffer.isBuffer(data) || data.length < MAGIC.length + SALT_LEN + IV_LEN + TAG_LEN) { - throw new Error('Unrecognized or corrupt backup file.'); - } - if (!data.slice(0, MAGIC.length).equals(MAGIC)) { - throw new Error('Unrecognized or corrupt backup file.'); - } - let o = MAGIC.length; - const salt = data.slice(o, o += SALT_LEN); - const iv = data.slice(o, o += IV_LEN); - const tag = data.slice(o, o += TAG_LEN); - const ciphertext = data.slice(o); - const key = deriveKey(password, salt); - const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv); - decipher.setAuthTag(tag); - return Buffer.concat([decipher.update(ciphertext), decipher.final()]); -} - -module.exports = { - exportData: async (password) => { - try { - const pw = normalizePassword(password); - const homeDir = os.homedir(); - const secretFilePath = path.join(homeDir, '.ssb', 'secret'); - if (!fs.existsSync(secretFilePath)) { - throw new Error(".ssb/secret file doesn't exist"); - } - const original = fs.readFileSync(secretFilePath); - const encrypted = encryptBuffer(original, pw); - const roundtrip = decryptBuffer(encrypted, pw); - if (!roundtrip.equals(original)) { - throw new Error('Backup verification failed; nothing was written.'); - } - const encryptedFilePath = path.join(homeDir, 'oasis.enc'); - fs.writeFileSync(encryptedFilePath, encrypted, { mode: 0o600 }); - return encryptedFilePath; - } catch (error) { - throw new Error("Error exporting data: " + error.message); - } - }, - importData: async ({ filePath, password }) => { - try { - const pw = normalizePassword(password); - if (!fs.existsSync(filePath)) { - throw new Error('Encrypted file not found.'); - } - const data = fs.readFileSync(filePath); - let decrypted; - try { - decrypted = decryptBuffer(data, pw); - } catch (_) { - throw new Error('Wrong password or corrupt backup file.'); - } - const homeDir = os.homedir(); - const ssbDir = path.join(homeDir, '.ssb'); - fs.mkdirSync(ssbDir, { recursive: true }); - const secretPath = path.join(ssbDir, 'secret'); - if (fs.existsSync(secretPath)) { - fs.copyFileSync(secretPath, path.join(ssbDir, 'secret.bak-' + new Date().toISOString().replace(/[:.]/g, '-'))); - } - const tmpPath = path.join(ssbDir, 'secret.tmp-' + process.pid); - fs.writeFileSync(tmpPath, decrypted, { mode: 0o600 }); - fs.renameSync(tmpPath, secretPath); - try { fs.unlinkSync(filePath); } catch (_) {} - return secretPath; - } catch (error) { - throw new Error("Error importing data: " + error.message); - } - } -}; diff --git a/src/models/logistics_model.js b/src/models/logistics_model.js new file mode 100644 index 0000000..cec6bea --- /dev/null +++ b/src/models/logistics_model.js @@ -0,0 +1,390 @@ +const { readTyped, memoIndex } = require('./typed_log'); +const categories = require('../backend/opinion_categories'); +const { buildValidatedTombstoneSet } = require('./tombstone_validator'); +const { getConfig } = require('../configs/config-manager.js'); +const logLimit = getConfig().ssbLogStream?.limit || 1000; + +const ROUTE_TYPE = 'logisticsRoute'; +const RATING_TYPE = 'logisticsRating'; +const BOOKING_TYPE = 'logistics-booking'; +const BOOKING_STATUS_TYPE = 'logistics-booking-status'; +const OPINION_TYPE = 'logisticsOpinion'; +const PUBLIC_TYPES = [ROUTE_TYPE, RATING_TYPE, OPINION_TYPE, 'tombstone']; +const KINDS = ['TRIP', 'SHIPMENT']; +const MODES = ['OFFER', 'REQUEST']; +const PRICE_TYPES = ['FREE', 'ECO', 'TIME']; +const RECURRENCES = ['NONE', 'DAILY', 'WEEKLY', 'MONTHLY']; +const STATUSES = ['OPEN', 'CLOSED']; +const BOOKING_STATUSES = ['PENDING', 'CONFIRMED', 'REJECTED', 'CANCELLED', 'DELIVERED']; +const OWNER_BOOKING_STATUSES = ['CONFIRMED', 'REJECTED', 'DELIVERED']; +const RECENT_MS = 24 * 60 * 60 * 1000; +const MAX_ZONES = 8; + +const safeText = (v) => String(v == null ? '' : v).trim(); +const normU = (v) => String(v || '').trim().toUpperCase(); +const pick = (list, v, fallback) => (list.includes(normU(v)) ? normU(v) : fallback); +const normalizeTags = (raw) => { + if (!raw) return []; + const arr = Array.isArray(raw) ? raw : String(raw).split(','); + return Array.from(new Set(arr.map(t => String(t || '').trim()).filter(Boolean))); +}; +const toIso = (v) => { const t = Date.parse(v || ''); return Number.isFinite(t) ? new Date(t).toISOString() : ''; }; +const num = (v, min = 0) => { const n = Number(v); return Number.isFinite(n) && n >= min ? n : 0; }; +const MEDIA_RE = /!?\[(image|video):[^\]]*\]\((&[^)]+)\)/; +const mediaOf = (text) => { const m = safeText(text).match(MEDIA_RE); return m ? { kind: m[1], blobId: m[2] } : null; }; +const zoneKey = (s) => safeText(s).toLowerCase(); + +module.exports = ({ cooler }) => { + let ssb; + const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb; }; + + const publish = (ssbClient, content) => new Promise((resolve, reject) => ssbClient.publish(content, (err, res) => err ? reject(err) : resolve(res))); + const publishPrivate = (ssbClient, content, recps) => new Promise((resolve, reject) => ssbClient.private.publish(content, recps, (err, res) => err ? reject(err) : resolve(res))); + + const readPublic = async (ssbClient) => readTyped(ssbClient, PUBLIC_TYPES, { limit: logLimit }); + + const readPrivate = async (ssbClient) => { + const msgs = await readTyped(ssbClient, [BOOKING_TYPE, BOOKING_STATUS_TYPE], { limit: logLimit, withPrivate: true }); + const bookings = []; + const statuses = []; + for (const m of msgs) { + const v = (m && m.value) || {}; + if (v.private !== true) continue; + const c = v.content; + if (!c || typeof c !== 'object') continue; + const ts = v.timestamp || m.timestamp || 0; + if (c.type === BOOKING_TYPE && c.routeId) bookings.push({ id: m.key, author: v.author, ts, c }); + else if (c.type === BOOKING_STATUS_TYPE && c.bookingId) statuses.push({ key: m.key, author: v.author, ts, c }); + } + return { bookings, statuses }; + }; + + const buildIndex = (messages) => { + const tomb = buildValidatedTombstoneSet(messages); + const nodes = new Map(); + const parentOf = new Map(); + const ratings = []; + const opinions = []; + for (const m of messages) { + const v = m.value || {}; + const c = v.content; + if (!c || typeof c !== 'object') continue; + if (c.type === ROUTE_TYPE) { + nodes.set(m.key, { key: m.key, author: v.author, ts: v.timestamp || m.timestamp || 0, c }); + if (typeof c.replaces === 'string') parentOf.set(m.key, c.replaces); + } else if (c.type === OPINION_TYPE && typeof c.target === 'string') { + opinions.push({ target: c.target, author: v.author, category: String(c.category || ''), ts: v.timestamp || 0 }); + } else if (c.type === RATING_TYPE && typeof c.target === 'string') { + ratings.push({ key: m.key, target: c.target, author: v.author, ts: v.timestamp || 0, score: Math.min(5, Math.max(1, num(c.score, 1) || 1)), text: safeText(c.text), createdAt: c.createdAt || new Date(v.timestamp || 0).toISOString() }); + } + } + const rootOf = (key) => { + const seen = new Set(); + let cur = key; + while (parentOf.has(cur) && !seen.has(cur)) { seen.add(cur); cur = parentOf.get(cur); } + return cur; + }; + const versionsByRoot = new Map(); + for (const node of nodes.values()) { + const root = rootOf(node.key); + if (tomb.has(root)) continue; + if (!versionsByRoot.has(root)) versionsByRoot.set(root, []); + versionsByRoot.get(root).push(node); + } + const routes = new Map(); + const now = Date.now(); + for (const [root, versions] of versionsByRoot) { + versions.sort((a, b) => a.ts - b.ts); + const first = versions[0]; + const own = versions.filter(v => v.author === first.author); + const tip = own[own.length - 1]; + const c = tip.c; + const dateIso = toIso(c.date); + const dateTs = Date.parse(dateIso) || 0; + const recurrence = pick(RECURRENCES, c.recurrence, 'NONE'); + const past = !!dateTs && dateTs < now && recurrence === 'NONE'; + const status = pick(STATUSES, c.status, 'OPEN') === 'CLOSED' ? 'CLOSED' : (past ? 'PAST' : 'OPEN'); + const byAuthor = new Map(); + for (const r of ratings.filter(r => nodes.has(r.target) && rootOf(r.target) === root)) { + const prev = byAuthor.get(r.author); + if (!prev || r.ts >= prev.ts) byAuthor.set(r.author, r); + } + const routeRatings = Array.from(byAuthor.values()).sort((a, b) => b.ts - a.ts); + const ops = {}; + const voters = []; + for (const op of opinions.filter(o => nodes.has(o.target) && rootOf(o.target) === root).sort((a, b) => a.ts - b.ts)) { + if (voters.includes(op.author) || !categories.includes(op.category)) continue; + voters.push(op.author); + ops[op.category] = (ops[op.category] || 0) + 1; + } + const ratingAvg = routeRatings.length ? Math.round((routeRatings.reduce((s, r) => s + r.score, 0) / routeRatings.length) * 10) / 10 : 0; + routes.set(root, { + id: root, rootId: root, tipId: tip.key, + kind: pick(KINDS, c.kind, 'TRIP'), mode: pick(MODES, c.mode, 'OFFER'), + title: safeText(c.title), description: safeText(c.description), + origin: safeText(c.origin), destination: safeText(c.destination), mapUrl: safeText(c.mapUrl), + date: dateIso, dateTs, recurrence, + seats: num(c.seats), seatsTaken: num(c.seatsTaken), size: safeText(c.size), weight: safeText(c.weight), media: mediaOf(c.description), + priceType: pick(PRICE_TYPES, c.priceType, 'FREE'), price: num(c.price), + orderRef: safeText(c.orderRef), tags: normalizeTags(c.tags), + status, closed: status !== 'OPEN', + author: first.author, createdAt: first.c.createdAt || new Date(first.ts).toISOString(), + updatedAt: c.updatedAt || new Date(tip.ts).toISOString(), ts: tip.ts, + ratings: routeRatings, ratingCount: routeRatings.length, ratingAvg, + opinions: ops, opinions_inhabitants: voters, opinionCount: voters.length + }); + } + return { routes, rootOf, nodes }; + }; + + const bookingsFor = (priv, routeId, me) => { + const statusById = new Map(); + for (const s of priv.statuses) { + const prev = statusById.get(s.c.bookingId); + if (!prev || s.ts >= prev.ts) statusById.set(s.c.bookingId, { status: pick(BOOKING_STATUSES, s.c.status, 'PENDING'), ts: s.ts, by: s.author, receipt: safeText(s.c.receipt) }); + } + return priv.bookings + .filter(b => String(b.c.routeId) === String(routeId)) + .map(b => { + const st = statusById.get(b.id) || null; + return { + id: b.id, routeId: String(b.c.routeId), booker: b.author, owner: safeText(b.c.owner), + seats: num(b.c.seats), notes: safeText(b.c.notes), orderRef: safeText(b.c.orderRef), + createdAt: b.c.createdAt || new Date(b.ts).toISOString(), ts: b.ts, + status: st ? st.status : 'PENDING', statusAt: st ? new Date(st.ts).toISOString() : null, receipt: st ? st.receipt : '', + mine: b.author === me + }; + }) + .sort((a, b) => b.ts - a.ts); + }; + + const load = async () => { + const ssbClient = await openSsb(); + const [pub, priv] = await Promise.all([readPublic(ssbClient), readPrivate(ssbClient)]); + return { ssbClient, idx: memoIndex('logistics', pub, buildIndex), priv }; + }; + + const find = (idx, id) => { + const key = String(id || ''); + if (idx.routes.has(key)) return idx.routes.get(key); + if (idx.nodes.has(key)) return idx.routes.get(idx.rootOf(key)) || null; + return null; + }; + + const decorate = (route, priv, me) => { + const bookings = bookingsFor(priv, route.id, me); + const confirmed = bookings.filter(b => b.status === 'CONFIRMED' || b.status === 'DELIVERED'); + const taken = Math.max(route.seatsTaken || 0, confirmed.reduce((s, b) => s + (b.seats || 1), 0)); + const myBooking = bookings.find(b => b.mine && b.status !== 'CANCELLED' && b.status !== 'REJECTED') || null; + const isOwner = route.author === me; + const participated = isOwner || confirmed.some(b => b.mine); + const myRating = route.ratings.find(r => r.author === me) || null; + return { + ...route, + bookings: isOwner ? bookings : bookings.filter(b => b.mine), + bookingCount: bookings.filter(b => b.status !== 'CANCELLED' && b.status !== 'REJECTED').length, + confirmedCount: confirmed.length, seatsTaken: taken, + seatsLeft: route.kind === 'TRIP' && route.seats ? Math.max(0, route.seats - taken) : null, + myBooking, isOwner, participated, + canBook: !isOwner && !route.closed && !myBooking, + canRate: participated && route.closed && !myRating, + myRating, + lastActivityTs: Math.max(route.ts, ...bookings.map(b => b.ts), ...route.ratings.map(r => r.ts)) + }; + }; + + const routeContent = (userId, data, base = null) => ({ + type: ROUTE_TYPE, + kind: pick(KINDS, data.kind, base ? base.kind : 'TRIP'), + mode: pick(MODES, data.mode, base ? base.mode : 'OFFER'), + title: safeText(data.title) || (base ? base.title : ''), + description: data.description !== undefined ? safeText(data.description) : (base ? base.description : ''), + origin: data.origin !== undefined ? safeText(data.origin) : (base ? base.origin : ''), + destination: data.destination !== undefined ? safeText(data.destination) : (base ? base.destination : ''), + mapUrl: data.mapUrl !== undefined ? safeText(data.mapUrl) : (base ? base.mapUrl : ''), + date: data.date !== undefined ? toIso(data.date) : (base ? base.date : ''), + recurrence: pick(RECURRENCES, data.recurrence, base ? base.recurrence : 'NONE'), + seats: data.seats !== undefined ? num(data.seats) : (base ? base.seats : 0), + size: data.size !== undefined ? safeText(data.size) : (base ? base.size : ''), + weight: data.weight !== undefined ? safeText(data.weight) : (base ? base.weight : ''), + priceType: pick(PRICE_TYPES, data.priceType, base ? base.priceType : 'FREE'), + price: data.price !== undefined ? num(data.price) : (base ? base.price : 0), + orderRef: data.orderRef !== undefined ? safeText(data.orderRef) : (base ? base.orderRef : ''), + tags: data.tags !== undefined ? normalizeTags(data.tags) : (base ? base.tags : []), + status: data.status !== undefined ? pick(STATUSES, data.status, 'OPEN') : (base ? (base.status === 'CLOSED' ? 'CLOSED' : 'OPEN') : 'OPEN'), + seatsTaken: data.seatsTaken !== undefined ? num(data.seatsTaken) : (base ? base.seatsTaken : 0), + author: userId, + createdAt: base ? base.createdAt : new Date().toISOString(), + updatedAt: new Date().toISOString() + }); + + return { + KINDS, MODES, PRICE_TYPES, RECURRENCES, STATUSES, BOOKING_STATUSES, + + async createRoute(data = {}) { + const ssbClient = await openSsb(); + const content = routeContent(ssbClient.id, data); + if (!content.title) throw new Error('Title is required'); + if (!content.origin || !content.destination) throw new Error('Origin and destination are required'); + if (!content.date) throw new Error('A valid date is required'); + if (!(content.price > 0)) { content.price = 0; content.priceType = 'FREE'; } else if (content.priceType === 'FREE') content.priceType = 'ECO'; + return publish(ssbClient, content); + }, + + async updateRoute(id, data = {}) { + const { ssbClient, idx } = await load(); + const route = find(idx, id); + if (!route) throw new Error('Route not found'); + if (route.author !== ssbClient.id) throw new Error('Only the author can update this route'); + const content = { ...routeContent(ssbClient.id, data, route), replaces: route.tipId }; + if (!(content.price > 0)) { content.price = 0; content.priceType = 'FREE'; } else if (content.priceType === 'FREE') content.priceType = 'ECO'; + const res = await publish(ssbClient, content); + return { key: res.key, rootId: route.id }; + }, + + async createOpinion(id, category) { + if (!categories.includes(category)) throw new Error('Invalid voting category'); + const { ssbClient, idx } = await load(); + const route = find(idx, id); + if (!route) throw new Error('Route not found'); + if (route.opinions_inhabitants.includes(ssbClient.id)) throw new Error('Already voted'); + return publish(ssbClient, { type: OPINION_TYPE, target: route.id, category, createdAt: new Date().toISOString() }); + }, + + async closeRoute(id) { return this.updateRoute(id, { status: 'CLOSED' }); }, + async reopenRoute(id) { return this.updateRoute(id, { status: 'OPEN' }); }, + + async deleteRoute(id) { + const { ssbClient, idx } = await load(); + const route = find(idx, id); + if (!route) throw new Error('Route not found'); + if (route.author !== ssbClient.id) throw new Error('Only the author can delete this route'); + await publish(ssbClient, { type: 'tombstone', target: route.id, deletedAt: new Date().toISOString(), author: ssbClient.id }); + return { key: route.id }; + }, + + async book(id, { seats = 1, notes = '', orderRef = '' } = {}) { + const { ssbClient, idx, priv } = await load(); + const me = ssbClient.id; + const route = find(idx, id); + if (!route) throw new Error('Route not found'); + const view = decorate(route, priv, me); + if (view.isOwner) throw new Error('You cannot book your own route'); + if (view.closed) throw new Error('This route is closed'); + if (view.myBooking) throw new Error('You already booked this route'); + const n = route.kind === 'TRIP' ? Math.max(1, Math.floor(num(seats, 1) || 1)) : 1; + if (view.seatsLeft !== null && n > view.seatsLeft) throw new Error('Not enough seats left'); + const content = { type: BOOKING_TYPE, routeId: route.id, owner: route.author, seats: n, notes: safeText(notes), orderRef: safeText(orderRef), createdAt: new Date().toISOString() }; + const res = await publishPrivate(ssbClient, content, [me, route.author]); + return { ...res, routeId: route.id, owner: route.author, booker: me }; + }, + + async setBookingStatus(bookingId, status, { receipt = '' } = {}) { + const { ssbClient, priv } = await load(); + const me = ssbClient.id; + const st = pick(BOOKING_STATUSES, status, ''); + if (!st || st === 'PENDING') throw new Error('Invalid status'); + const b = priv.bookings.find(x => x.id === bookingId); + if (!b) throw new Error('Booking not found'); + const owner = safeText(b.c.owner); + if (OWNER_BOOKING_STATUSES.includes(st)) { if (me !== owner) throw new Error('Only the route owner can set this status'); } + else if (me !== b.author) throw new Error('Only the booker can cancel'); + const content = { type: BOOKING_STATUS_TYPE, bookingId, routeId: String(b.c.routeId), status: st, ...(st === 'DELIVERED' ? { receipt: safeText(receipt) || `Delivered ${new Date().toISOString()}` } : {}), updatedAt: new Date().toISOString() }; + const res = await publishPrivate(ssbClient, content, Array.from(new Set([me, b.author, owner].filter(Boolean)))); + if (me === owner) { + try { + const fresh = await load(); + const route = find(fresh.idx, b.c.routeId); + if (route && route.kind === 'TRIP' && route.seats) { + const taken = bookingsFor(fresh.priv, route.id, me).filter(x => x.status === 'CONFIRMED' || x.status === 'DELIVERED').reduce((sum, x) => sum + (x.seats || 1), 0); + if (taken !== route.seatsTaken) await publish(ssbClient, { ...routeContent(me, { seatsTaken: taken }, route), replaces: route.tipId }); + } + } catch (_) {} + } + return { ...res, routeId: String(b.c.routeId), owner, booker: b.author, status: st }; + }, + + async rate(id, { score, text = '' } = {}) { + const { ssbClient, idx, priv } = await load(); + const me = ssbClient.id; + const route = find(idx, id); + if (!route) throw new Error('Route not found'); + const view = decorate(route, priv, me); + if (!view.participated) throw new Error('Only participants can rate a route'); + if (!view.closed) throw new Error('Routes are rated once they are closed'); + if (view.myRating) throw new Error('You already rated this route'); + const s = Math.min(5, Math.max(1, Math.round(num(score, 1) || 1))); + return publish(ssbClient, { type: RATING_TYPE, target: route.id, score: s, text: safeText(text), createdAt: new Date().toISOString() }); + }, + + async getRouteById(id) { + const { ssbClient, idx, priv } = await load(); + const route = find(idx, id); + return route ? decorate(route, priv, ssbClient.id) : null; + }, + + async myBookings() { + const { ssbClient, idx, priv } = await load(); + const me = ssbClient.id; + const out = []; + for (const b of priv.bookings) { + if (b.author !== me) continue; + const route = find(idx, b.c.routeId); + if (!route) continue; + const view = decorate(route, priv, me); + const mine = view.bookings.find(x => x.id === b.id); + if (mine) out.push({ ...mine, route: view }); + } + return out.sort((a, b) => b.ts - a.ts); + }, + + zonesOf(list) { + const counts = new Map(); + for (const r of list || []) { + for (const z of [r.origin, r.destination]) { + const k = zoneKey(z); + if (!k) continue; + const prev = counts.get(k) || { label: safeText(z), count: 0 }; + prev.count += 1; + counts.set(k, prev); + } + } + return Array.from(counts.values()).sort((a, b) => b.count - a.count || a.label.localeCompare(b.label)).slice(0, MAX_ZONES); + }, + + async listAll({ filter = 'all', q = '', zone = '' } = {}) { + const { ssbClient, idx, priv } = await load(); + const me = ssbClient.id; + const f = String(filter || 'all').toLowerCase(); + const needle = String(q || '').trim().toLowerCase(); + const z = zoneKey(zone); + let out = Array.from(idx.routes.values()).map(r => decorate(r, priv, me)); + if (f === 'mine') out = out.filter(r => r.isOwner); + else if (f === 'booked') out = out.filter(r => !!r.myBooking); + else if (f === 'recent') out = out.filter(r => r.lastActivityTs >= Date.now() - RECENT_MS); + else if (f === 'trips') out = out.filter(r => r.kind === 'TRIP'); + else if (f === 'shipments') out = out.filter(r => r.kind === 'SHIPMENT'); + else if (f === 'offers') out = out.filter(r => r.mode === 'OFFER'); + else if (f === 'requests') out = out.filter(r => r.mode === 'REQUEST'); + else if (f === 'upcoming') out = out.filter(r => r.status === 'OPEN'); + else if (f === 'past') out = out.filter(r => r.status === 'PAST'); + else if (f === 'closed') out = out.filter(r => r.status === 'CLOSED'); + else if (f === 'history') out = out.filter(r => r.closed && r.participated); + else if (f === 'free') out = out.filter(r => r.priceType === 'FREE'); + else if (f === 'eco') out = out.filter(r => r.priceType === 'ECO'); + else if (f === 'time') out = out.filter(r => r.priceType === 'TIME'); + else if (f === 'top') out = out.filter(r => r.ratingCount > 0); + if (z) out = out.filter(r => zoneKey(r.origin) === z || zoneKey(r.destination) === z); + if (needle) out = out.filter(r => [r.title, r.description, r.origin, r.destination, r.kind, r.mode, ...r.tags].some(v => String(v || '').toLowerCase().includes(needle))); + if (f === 'top') out.sort((a, b) => b.ratingAvg - a.ratingAvg || b.ratingCount - a.ratingCount); + else out.sort((a, b) => (a.closed === b.closed ? 0 : (a.closed ? 1 : -1)) || (a.dateTs && b.dateTs ? a.dateTs - b.dateTs : b.lastActivityTs - a.lastActivityTs)); + return out; + }, + + async resolveRootId(id) { + const { idx } = await load(); + const route = find(idx, id); + return route ? route.id : String(id || ''); + } + }; +}; diff --git a/src/models/logs_model.js b/src/models/logs_model.js index 88bade7..741b44d 100644 --- a/src/models/logs_model.js +++ b/src/models/logs_model.js @@ -1,11 +1,12 @@ const pull = require('../server/node_modules/pull-stream'); -const util = require('../server/node_modules/util'); -const axios = require('../server/node_modules/axios'); +const { requestScope } = require('./typed_log'); +const util = require('util'); const { getConfig } = require('../configs/config-manager.js'); const { buildValidatedTombstoneSet } = require('./tombstone_validator'); const logLimit = getConfig().ssbLogStream?.limit || 1000; + const DAY_MS = 24 * 60 * 60 * 1000; const WEEK_MS = 7 * DAY_MS; const MONTH_MS = 30 * DAY_MS; @@ -21,9 +22,9 @@ const FILTER_WINDOWS = { const ACTION_TYPES = new Set([ 'post', 'about', 'contact', 'feed', 'bookmark', 'image', 'audio', 'video', - 'document', 'torrent', 'event', 'task', 'taskAssignment', - 'votes', 'vote', 'report', 'tribe', 'chat', 'chatMessage', 'pad', 'padEntry', - 'forum', 'market', 'job', 'project', 'pixelia', 'map', 'mapMarker', + 'document', 'torrent', 'file', 'event', 'task', 'taskAssign', + 'votes', 'vote', 'report', 'tribe', 'chat', 'chatMessage', 'pad', 'padEntry', 'room', 'roomMember', + 'forum', 'market', 'job', 'project', 'industry', 'industryBuild', 'pixelia', 'map', 'mapMarker', 'shop', 'shopProduct', 'curriculum', 'gameScore', 'calendar', 'calendarDate', 'calendarNote', 'transfer', 'bankClaim', 'ubiClaim', @@ -32,7 +33,29 @@ const ACTION_TYPES = new Set([ 'courtsCase', 'courtsEvidence', 'courtsAnswer', 'courtsVerdict', 'courtsNomination', 'courtsNominationVote', 'courtsSettlementProposal', 'courtsSettlementAccepted', - 'tribeParliamentCandidature', 'tribeParliamentRule' + 'tribeParliamentCandidature', 'tribeParliamentRule', + 'wikiPage', 'emergency', 'emergencyConfirm', 'emergencyUpdate', 'mailingList', + 'logisticsRoute', 'logisticsRating', 'logisticsOpinion', + 'podcast', 'podcastEpisode', 'podcastOpinion', 'podcastPlay', + 'campaign', 'campaignSignature', 'campaignUpdate', 'campaignOpinion', + 'audioOpinion', 'blogOpinion', 'bookmarkOpinion', 'documentOpinion', 'eventOpinion', 'feedOpinion', + 'fileOpinion', 'imageOpinion', 'videoOpinion', 'torrentOpinion', 'housingOpinion', 'industryOpinion', + 'marketOpinion', 'projectOpinion', 'reportOpinion', 'schoolOpinion', 'shopOpinion', 'taskOpinion', + 'transferOpinion', 'votesOpinion', 'pollOpinion', + 'calendarParticipant', 'chatMember', 'chatPin', 'chatReaction', 'padMember', 'mapMember', + 'courtsJudge', 'courtsMediators', 'courtsSupport', 'courtsVisibility', 'courtsVoteLink', + 'eventAttend', 'feed-action', 'spread', 'subscription', 'job_sub', + 'housing', 'housingRequest', + 'industryAllocation', 'industryApply', 'industryBlueprint', 'industryBuildStatus', 'industryBuildUpdate', + 'industryContribution', 'industryMember', 'industryVote', + 'larpHousePost', 'larpJoinHouse', 'larpLeaveLarp', 'larpTestAttempt', + 'marketBid', 'marketPurchase', 'shopPurchase', + 'parliamentCandidatureVote', 'tribeParliamentTerm', + 'poll', 'pollVote', 'pollClose', 'votesVote', + 'projectClaim', 'projectFollow', 'projectPledge', 'projectPledgeConfirm', + 'reportConfirm', 'transferConfirm', 'ubiRefuse', + 'schoolCourse', 'schoolEnroll', 'schoolLesson', 'schoolLessonMedia', 'schoolExam', 'schoolExamQuestion', + 'schoolExamResult', 'schoolProgress', 'schoolCertificate', 'schoolCommentHide' ]); const ACTION_PHRASES = { @@ -46,9 +69,10 @@ const ACTION_PHRASES = { video: 'uploaded a video', document: 'uploaded a document', torrent: 'shared a torrent', + file: 'shared a file', event: 'created an event', task: 'created a task', - taskAssignment: 'updated a task assignment', + taskAssign: 'updated a task assignment', votes: 'participated in a vote', vote: 'cast a vote', report: 'submitted a report', @@ -57,10 +81,14 @@ const ACTION_PHRASES = { chatMessage: 'sent a chat message', pad: 'worked on a collaborative pad', padEntry: 'edited a pad entry', + room: 'opened a room', + roomMember: 'joined a room', market: 'posted in the market', forum: 'posted in the forum', job: 'posted a job opportunity', project: 'advanced a project', + industry: 'founded a network-owned facility', + industryBuild: 'proposed a production build', pixelia: 'placed a pixel in pixelia', map: 'contributed to a map', mapMarker: 'placed a marker on a map', @@ -88,7 +116,100 @@ const ACTION_PHRASES = { courtsSettlementProposal: 'proposed a courts settlement', courtsSettlementAccepted: 'accepted a courts settlement', tribeParliamentCandidature: 'stood for a tribe parliament', - tribeParliamentRule: 'contributed a tribe parliament rule' + tribeParliamentRule: 'contributed a tribe parliament rule', + wikiPage: 'edited a wiki page', + emergency: 'reported an emergency', + emergencyConfirm: 'confirmed an emergency', + emergencyUpdate: 'posted an emergency update', + mailingList: 'created a mailing list', + logisticsRoute: 'published a logistics route', + logisticsRating: 'rated a logistics route', + logisticsOpinion: 'gave an opinion on a route', + podcast: 'created a podcast', + podcastEpisode: 'published a podcast episode', + podcastOpinion: 'gave an opinion on a podcast', + podcastPlay: 'listened to a podcast episode', + campaign: 'started a campaign', + campaignSignature: 'signed a campaign', + campaignUpdate: 'posted a campaign update', + campaignOpinion: 'gave an opinion on a campaign', + audioOpinion: 'gave an opinion on an audio', + blogOpinion: 'gave an opinion on a blog post', + bookmarkOpinion: 'gave an opinion on a bookmark', + documentOpinion: 'gave an opinion on a document', + eventOpinion: 'gave an opinion on an event', + feedOpinion: 'gave an opinion on a feed', + fileOpinion: 'gave an opinion on a file', + imageOpinion: 'gave an opinion on an image', + videoOpinion: 'gave an opinion on a video', + torrentOpinion: 'gave an opinion on a torrent', + housingOpinion: 'gave an opinion on a housing listing', + industryOpinion: 'gave an opinion on a facility', + marketOpinion: 'gave an opinion on a market item', + projectOpinion: 'gave an opinion on a project', + reportOpinion: 'gave an opinion on a report', + schoolOpinion: 'gave an opinion on a course', + shopOpinion: 'gave an opinion on a shop', + taskOpinion: 'gave an opinion on a task', + transferOpinion: 'gave an opinion on a transfer', + votesOpinion: 'gave an opinion on a votation', + pollOpinion: 'gave an opinion on a poll', + calendarParticipant: 'joined a calendar', + chatMember: 'joined a chat', + chatPin: 'pinned a chat message', + chatReaction: 'reacted to a chat message', + padMember: 'joined a pad', + mapMember: 'joined a map', + courtsJudge: 'chose a judge for a courts case', + courtsMediators: 'proposed mediators for a courts case', + courtsSupport: 'supported a courts case', + courtsVisibility: 'changed the visibility of a courts case', + courtsVoteLink: 'linked a vote to a courts case', + eventAttend: 'joined an event', + 'feed-action': 'refeeded a feed', + spread: 'spread some content', + subscription: 'subscribed to some content', + job_sub: 'subscribed to a job', + housing: 'published a housing listing', + housingRequest: 'requested a housing', + industryAllocation: 'shared out the output of a production build', + industryApply: 'applied to join a facility', + industryBlueprint: 'published a blueprint', + industryBuildStatus: 'updated the status of a production build', + industryBuildUpdate: 'posted a production build update', + industryContribution: 'contributed to a production build', + industryMember: 'joined a facility', + industryVote: 'voted in a facility', + larpHousePost: 'posted in a L.A.R.P. house', + larpJoinHouse: 'joined a L.A.R.P. house', + larpLeaveLarp: 'left L.A.R.P.', + larpTestAttempt: 'took a L.A.R.P. test', + marketBid: 'placed a bid in the market', + marketPurchase: 'bought in the market', + shopPurchase: 'bought in a shop', + parliamentCandidatureVote: 'voted for a parliamentary candidature', + tribeParliamentTerm: 'took part in a tribe parliament term', + poll: 'created a poll', + pollVote: 'voted in a poll', + pollClose: 'closed a poll', + votesVote: 'voted in a votation', + projectClaim: 'claimed a project bounty', + projectFollow: 'followed a project', + projectPledge: 'pledged to a project', + projectPledgeConfirm: 'confirmed a project pledge', + reportConfirm: 'confirmed a report', + transferConfirm: 'confirmed a transfer', + ubiRefuse: 'declined the UBI', + schoolCourse: 'created a course', + schoolEnroll: 'enrolled in a course', + schoolLesson: 'published a lesson', + schoolLessonMedia: 'added media to a lesson', + schoolExam: 'created an exam', + schoolExamQuestion: 'added an exam question', + schoolExamResult: 'completed an exam', + schoolProgress: 'progressed in a course', + schoolCertificate: 'issued a course certificate', + schoolCommentHide: 'hid a comment in a course' }; const compact = (s, n = 200) => String(s || '').replace(/\s+/g, ' ').trim().slice(0, n); @@ -108,7 +229,7 @@ module.exports = ({ cooler }) => { const ssbClient = await openSsb(); const msgs = await new Promise((resolve, reject) => pull( - ssbClient.createLogStream({ reverse: true, limit: logLimit }), + ssbClient.createUserStream({ id: ssbClient.id, reverse: true, limit: logLimit }), pull.collect((err, arr) => err ? reject(err) : resolve(arr)) ) ); @@ -129,10 +250,11 @@ module.exports = ({ cooler }) => { async function callAI(prompt) { if (!prompt) return ''; + try { if ((require('../configs/config-manager.js').getConfig().modules || {}).aiMod !== 'on') return ''; } catch (_) { return ''; } const tryOnce = async () => { try { - const res = await axios.post('http://localhost:4001/ai', { input: prompt, raw: true }, { timeout: 90000 }); - return String(res?.data?.answer || '').trim(); + const answer = await require('../AI/ai_client').ask({ system: 'You write one short first-person diary sentence. No IDs, hashes, quotes, lists or markdown.', input: prompt, maxTokens: 80 }); + return String(answer || '').trim(); } catch { return ''; } }; let out = await tryOnce(); @@ -269,9 +391,10 @@ module.exports = ({ cooler }) => { async function readAllLogMessages() { const ssbClient = await openSsb(); + const sealedOnly = ssbClient.private && typeof ssbClient.private.read === 'function'; const raw = await new Promise((resolve, reject) => pull( - ssbClient.createLogStream({ reverse: false, limit: logLimit }), + sealedOnly ? ssbClient.private.read({ reverse: true, limit: logLimit }) : ssbClient.createLogStream({ reverse: true, limit: logLimit }), pull.collect((err, arr) => err ? reject(err) : resolve(arr)) ) ); @@ -285,13 +408,16 @@ module.exports = ({ cooler }) => { const tsIn = m.timestamp || valueIn?.timestamp || Date.now(); let dec; try { - dec = ssbClient.private.unbox({ key: keyIn, value: valueIn, timestamp: tsIn }); + dec = typeof valueIn.content === 'string' ? ssbClient.private.unbox({ key: keyIn, value: valueIn, timestamp: tsIn }) : (valueIn.private === true ? m : null); } catch { continue; } const v = dec?.value; const c = v?.content; if (!c) continue; if (v.author !== userId) continue; - if (c.type === 'tombstone') continue; + if (c.type === 'tombstone') { + if (typeof c.target === 'string' && c.target) tombstoned.add(c.target); + continue; + } if (c.type !== 'log') continue; if (c.replaces) replaced.set(c.replaces, dec.key || keyIn); items.push({ @@ -306,13 +432,24 @@ module.exports = ({ cooler }) => { ref: c.ref || null }); } + const parentOf = new Map(); + for (const i of items) if (i.replaces) parentOf.set(i.key, i.replaces); + const rootOf = (key) => { + let cur = key; + const seen = new Set(); + while (parentOf.has(cur) && !seen.has(cur)) { seen.add(cur); cur = parentOf.get(cur); } + return cur; + }; const survivors = items.filter(i => !tombstoned.has(i.key) && !replaced.has(i.key)); + for (const i of survivors) i.rootId = rootOf(i.key); survivors.sort((a, b) => b.ts - a.ts); return survivors; } async function listLogs(filter = 'today') { - const items = await readAllLogMessages(); + const store = requestScope.getStore(); + if (store && !store.logsAll) store.logsAll = readAllLogMessages(); + const items = await (store ? store.logsAll : readAllLogMessages()); const win = FILTER_WINDOWS[filter]; if (win === null || win === undefined) return items; const cutoff = Date.now() - win; @@ -327,11 +464,14 @@ module.exports = ({ cooler }) => { async function updateLog(id, { text, label, mode }) { const current = await getLogById(id); if (!current) return { status: 'not_found' }; - await republishLog({ - replaces: current.key, + const next = { text: text !== undefined ? text : current.text, label: label !== undefined ? label : current.label, - mode: mode || current.mode, + mode: mode || current.mode + }; + await republishLog({ + replaces: current.key, + ...next, createdAt: current.createdAt }); return { status: 'ok' }; @@ -357,6 +497,7 @@ module.exports = ({ cooler }) => { getLogById, listLogs, countLogs, - isAImodOn + isAImodOn, + FILTER_WINDOWS }; }; diff --git a/src/models/mailing_model.js b/src/models/mailing_model.js new file mode 100644 index 0000000..eee9543 --- /dev/null +++ b/src/models/mailing_model.js @@ -0,0 +1,437 @@ +const pull = require('../server/node_modules/pull-stream'); +const crypto = require('crypto'); +const { readTyped } = require('./typed_log'); +const { buildValidatedTombstoneSet } = require('./tombstone_validator'); +const longText = require('../backend/long_text'); +const { getConfig } = require('../configs/config-manager.js'); +const logLimit = getConfig().ssbLogStream?.limit || 1000; + +const LIST_TYPE = 'mailingList'; +const LEAVE_TYPE = 'mailingLeave'; +const LIST_TYPES = ['OPEN', 'CLOSED']; +const STATUSES = ['ACTIVE', 'ARCHIVED']; +const BATCH = 6; +const RECENT_MS = 24 * 60 * 60 * 1000; + +const safeText = (v) => String(v == null ? '' : v).trim(); +const normU = (v) => String(v || '').trim().toUpperCase(); +const normalizeListType = (v) => (LIST_TYPES.includes(normU(v)) ? normU(v) : 'OPEN'); +const normalizeStatus = (v) => (STATUSES.includes(normU(v)) ? normU(v) : 'ACTIVE'); +const normalizeTags = (raw) => { + if (!raw) return []; + const arr = Array.isArray(raw) ? raw : String(raw).split(','); + return Array.from(new Set(arr.map(t => String(t || '').trim()).filter(Boolean))); +}; +const isFeedId = (v) => typeof v === 'string' && /^@[A-Za-z0-9+/=]+\.ed25519$/.test(v.trim()); +const normalizeMembers = (raw) => { + if (!raw) return []; + const arr = Array.isArray(raw) ? raw : String(raw).split(/[\s,]+/); + return Array.from(new Set(arr.map(t => String(t || '').trim()).filter(isFeedId))); +}; +const uniq = (list) => Array.from(new Set((list || []).filter(Boolean))); +const canonicalSubject = (s) => String(s || '').replace(/^\s*(RE:\s*)+/i, '').trim(); + +module.exports = ({ cooler, subscriptionsModel = null }) => { + let ssb; + const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb; }; + + const publish = (ssbClient, content) => new Promise((resolve, reject) => ssbClient.publish(content, (err, res) => err ? reject(err) : resolve(res))); + const publishPrivate = (ssbClient, content, recps) => new Promise((resolve, reject) => ssbClient.private.publish(content, recps, (err, res) => err ? reject(err) : resolve(res))); + + const publishPrivateBatched = async (ssbClient, content, recipients) => { + const me = ssbClient.id; + const others = uniq(recipients).filter(id => id !== me); + const out = []; + if (!others.length) { out.push(await publishPrivate(ssbClient, content, [me])); return out; } + for (let i = 0; i < others.length; i += BATCH) { + out.push(await publishPrivate(ssbClient, content, uniq([me, ...others.slice(i, i + BATCH)]))); + } + return out; + }; + + const privateState = new WeakMap(); + const readPrivate = async (ssbClient) => { + const me = ssbClient.id; + let st = privateState.get(ssbClient); + if (!st || st.me !== me) { + st = { me, lastTs: 0, seen: new Set(), lists: [], leaves: [], posts: [], chunks: new Map(), tombClaims: new Map(), authorByKey: new Map() }; + privateState.set(ssbClient, st); + } + const raw = await new Promise((resolve, reject) => { + pull(ssbClient.createLogStream(st.lastTs > 0 ? { reverse: false, gte: st.lastTs } : { reverse: false }), pull.collect((err, arr) => err ? reject(err) : resolve(arr))); + }); + for (const m of raw) { + if (!m || !m.value) continue; + const recv = Number(m.timestamp) || 0; + if (recv > st.lastTs) st.lastTs = recv; + const content = m.value.content; + if (typeof content !== 'string') continue; + if (m.key) { + if (st.seen.has(m.key)) continue; + st.seen.add(m.key); + } + let dec; + try { dec = ssbClient.private.unbox({ key: m.key, value: m.value, timestamp: m.timestamp || m.value.timestamp }); } catch (_) { continue; } + const v = (dec && dec.value) || {}; + const c = v.content; + const k = (dec && dec.key) || m.key; + if (!c || typeof c !== 'object' || !k) continue; + const ts = v.timestamp || m.timestamp || 0; + if (c.type === 'tombstone' && c.target) { + const set = st.tombClaims.get(c.target) || new Set(); + set.add(v.author); + st.tombClaims.set(c.target, set); + continue; + } + if (c.type === longText.CHUNK_TYPE) { + const ch = longText.chunkOf(v.author, c); + if (ch) st.chunks.set(k, ch); + continue; + } + st.authorByKey.set(k, v.author); + if (c.type === LIST_TYPE) st.lists.push({ key: k, author: v.author, ts, seq: v.sequence || 0, arrival: st.lists.length, c }); + else if (c.type === LEAVE_TYPE && c.list) st.leaves.push({ key: k, author: v.author, ts, list: String(c.list) }); + else if (c.type === 'post' && c.list) { + const to = Array.isArray(c.to) ? c.to : []; + if (v.author === me || to.includes(me)) st.posts.push({ key: k, author: v.author, ts, c }); + } + } + const tombed = new Set(); + for (const [target, authors] of st.tombClaims.entries()) { + const orig = st.authorByKey.get(target); + for (const a of authors) if (a === orig || a === me) { tombed.add(target); break; } + } + return { lists: st.lists.slice(), leaves: st.leaves.slice(), posts: st.posts.filter(p => !tombed.has(p.key)), tombed, chunks: st.chunks }; + }; + + const buildIndex = (publicMsgs, priv) => { + const tomb = buildValidatedTombstoneSet(publicMsgs); + const nodes = new Map(); + const parentOf = new Map(); + for (const m of publicMsgs) { + const v = m.value || {}; + const c = v.content; + if (!c || typeof c !== 'object' || c.type !== LIST_TYPE) continue; + nodes.set(m.key, { key: m.key, author: v.author, ts: v.timestamp || m.timestamp || 0, seq: v.sequence || 0, arrival: Number(m.timestamp) || 0, c, closed: false }); + if (typeof c.replaces === 'string') parentOf.set(m.key, c.replaces); + } + const rootOf = (key) => { + const seen = new Set(); + let cur = key; + while (parentOf.has(cur) && !seen.has(cur)) { seen.add(cur); cur = parentOf.get(cur); } + return cur; + }; + const versionsById = new Map(); + for (const node of nodes.values()) { + const root = rootOf(node.key); + if (tomb.has(root)) continue; + if (!versionsById.has(root)) versionsById.set(root, []); + versionsById.get(root).push(node); + } + const privateById = new Map(); + for (const n of priv.lists) { + const id = n.c.listId ? String(n.c.listId) : null; + if (!id || nodes.has(id)) continue; + if (!privateById.has(id)) privateById.set(id, []); + privateById.get(id).push({ ...n, closed: true }); + } + for (const [id, versions] of privateById) versionsById.set(id, versions); + const ownerOf = (id, versions) => { + if (nodes.has(id)) return nodes.get(id).author; + const keys = new Set(versions.map(n => n.key)); + const heads = versions.filter(n => !(typeof n.c.replaces === 'string' && keys.has(n.c.replaces)) && (typeof n.c.author !== 'string' || n.c.author === n.author)); + heads.sort((a, b) => a.arrival - b.arrival); + return heads.length ? heads[0].author : null; + }; + const lists = new Map(); + for (const [id, versions] of versionsById) { + const owner = ownerOf(id, versions); + const own = versions.filter(v => v.author === owner).sort((a, b) => (a.seq - b.seq) || (a.ts - b.ts)); + if (!own.length || own.some(v => v.closed && normU(v.c.status) === 'DELETED')) continue; + const first = own[0]; + const tip = own[own.length - 1]; + const c = tip.c; + const listType = first.closed ? 'CLOSED' : normalizeListType(c.listType); + const updatedAt = c.updatedAt || new Date(tip.ts).toISOString(); + let members = first.closed ? uniq([owner, ...normalizeMembers(c.members)]) : []; + if (first.closed) { + const left = new Set(priv.leaves.filter(l => l.list === id && l.ts >= tip.ts).map(l => l.author)); + members = members.filter(m => m === owner || !left.has(m)); + } + lists.set(id, { + id, rootId: id, tipId: tip.key, + title: safeText(c.title), description: safeText(c.description), + listType, closed: listType === 'CLOSED', status: normalizeStatus(c.status), tags: normalizeTags(c.tags), + members, author: owner, + createdAt: first.c.createdAt || new Date(first.ts).toISOString(), + updatedAt, ts: tip.ts + }); + } + return { lists, rootOf, nodes }; + }; + + const historyFor = (priv, id, me) => { + const seen = new Map(); + for (const p of priv.posts) { + if (String(p.c.list) !== String(id)) continue; + const mid = p.c.mid ? String(p.c.mid) : p.key; + if (seen.has(mid)) continue; + seen.set(mid, p); + } + const out = Array.from(seen.values()).map(p => ({ + key: p.key, mid: p.c.mid ? String(p.c.mid) : p.key, list: String(p.c.list), + author: p.author, subject: safeText(p.c.subject), text: safeText(longText.joinText(p.c.text, p.c.chunks, longText.lookupIn(priv.chunks), p.author)), + thread: p.c.thread ? String(p.c.thread) : (p.c.mid ? String(p.c.mid) : p.key), + sentAt: p.c.sentAt || new Date(p.ts).toISOString(), ts: Date.parse(p.c.sentAt || '') || p.ts + })); + out.sort((a, b) => a.ts - b.ts); + return out; + }; + + const load = async () => { + const ssbClient = await openSsb(); + const [publicMsgs, priv] = await Promise.all([readTyped(ssbClient, [LIST_TYPE, 'tombstone'], { limit: logLimit }), readPrivate(ssbClient)]); + const idx = buildIndex(publicMsgs, priv); + return { ssbClient, idx, priv }; + }; + + const find = (idx, id) => { + const key = String(id || ''); + if (idx.lists.has(key)) return idx.lists.get(key); + if (idx.nodes.has(key)) return idx.lists.get(idx.rootOf(key)) || null; + return null; + }; + + const subscribersMap = async (ids) => { + if (!subscriptionsModel || !ids.length) return new Map(); + if (typeof subscriptionsModel.subscribersMap === 'function') { + try { return await subscriptionsModel.subscribersMap(ids); } catch (_) { return new Map(); } + } + const out = new Map(); + for (const id of ids) { + try { out.set(id, await subscriptionsModel.listSubscribers(id)); } catch (_) { out.set(id, []); } + } + return out; + }; + + const decorate = async (ssbClient, list, priv, subs) => { + const me = ssbClient.id; + const history = historyFor(priv, list.id, me); + const subscribers = list.closed ? [] : ((subs && subs.get(list.id)) || []); + const participants = list.closed ? list.members : uniq([list.author, ...subscribers]); + const isMember = participants.includes(me); + const lastTs = history.length ? history[history.length - 1].ts : list.ts; + return { + ...list, + subscribers, participants, participantCount: participants.length, + isMember, isOwner: list.author === me, canWrite: isMember && list.status === 'ACTIVE', + messageCount: history.length, threadCount: new Set(history.map(h => h.thread)).size, + lastActivityTs: lastTs, history + }; + }; + + const visibleTo = (list, me) => !list.closed || list.members.includes(me); + + const recipientsFor = async (ssbClient, list) => { + const me = ssbClient.id; + const subs = list.closed ? [] : ((await subscribersMap([list.id])).get(list.id) || []); + const all = list.closed ? list.members : uniq([list.author, ...subs]); + return { all, others: all.filter(id => id !== me) }; + }; + + const publishListVersion = async (ssbClient, content, list) => { + if (!list || !list.closed) return publish(ssbClient, content); + const res = await publishPrivateBatched(ssbClient, content, list.members); + return res[0]; + }; + + return { + LIST_TYPE, + LIST_TYPES, + STATUSES, + + async createList({ title, description, listType, members, tags }) { + const ssbClient = await openSsb(); + const me = ssbClient.id; + const now = new Date().toISOString(); + const t = safeText(title); + if (!t) throw new Error('Title is required'); + const type = normalizeListType(listType); + const content = { + type: LIST_TYPE, + title: t, + description: safeText(description), + listType: type, + status: 'ACTIVE', + tags: normalizeTags(tags), + author: me, + createdAt: now, + updatedAt: now + }; + if (type === 'CLOSED') { + const listId = crypto.randomBytes(16).toString('hex'); + const mem = uniq([me, ...normalizeMembers(members)]); + const res = await publishPrivateBatched(ssbClient, { ...content, listId, members: mem, private: true }, mem); + return { key: listId, rootId: listId, msgKey: res[0].key }; + } + const res = await publish(ssbClient, content); + return { key: res.key, rootId: res.key, msgKey: res.key }; + }, + + async updateList(id, data = {}) { + const { ssbClient, idx } = await load(); + const me = ssbClient.id; + const list = find(idx, id); + if (!list) throw new Error('Mailing list not found'); + if (list.author !== me) throw new Error('Only the author can update this mailing list'); + const now = new Date().toISOString(); + const nextMembers = list.closed && data.members !== undefined ? uniq([me, ...normalizeMembers(data.members)]) : list.members; + const content = { + type: LIST_TYPE, + title: data.title !== undefined && safeText(data.title) ? safeText(data.title) : list.title, + description: data.description !== undefined ? safeText(data.description) : list.description, + listType: list.listType, + status: data.status !== undefined ? normalizeStatus(data.status) : list.status, + tags: data.tags !== undefined ? normalizeTags(data.tags) : list.tags, + author: me, + createdAt: list.createdAt, + updatedAt: now, + replaces: list.tipId + }; + if (list.closed) { + const res = await publishPrivateBatched(ssbClient, { ...content, listId: list.id, members: nextMembers, private: true }, uniq([...list.members, ...nextMembers])); + return { key: res[0].key, rootId: list.id }; + } + const res = await publish(ssbClient, content); + return { key: res.key, rootId: list.id }; + }, + + async setStatus(id, status) { + return this.updateList(id, { status: normalizeStatus(status) }); + }, + + async deleteList(id) { + const { ssbClient, idx } = await load(); + const me = ssbClient.id; + const list = find(idx, id); + if (!list) throw new Error('Mailing list not found'); + if (list.author !== me) throw new Error('Only the author can delete this mailing list'); + if (list.closed) { + await publishPrivateBatched(ssbClient, { type: LIST_TYPE, listId: list.id, title: list.title, status: 'DELETED', author: me, createdAt: list.createdAt, updatedAt: new Date().toISOString(), replaces: list.tipId, private: true }, list.members); + return { key: list.id }; + } + await publish(ssbClient, { type: 'tombstone', target: list.id, deletedAt: new Date().toISOString(), author: me }); + return { key: list.id }; + }, + + async leaveList(id) { + const { ssbClient, idx } = await load(); + const me = ssbClient.id; + const list = find(idx, id); + if (!list) throw new Error('Mailing list not found'); + if (!list.closed) { + if (!subscriptionsModel) throw new Error('Subscriptions unavailable'); + return subscriptionsModel.setSubscription(list.id, 'mailing', false); + } + if (list.author === me) throw new Error('The author cannot leave the mailing list'); + if (!list.members.includes(me)) throw new Error('Not a member'); + return publishPrivateBatched(ssbClient, { type: LEAVE_TYPE, list: list.id, private: true, createdAt: new Date().toISOString() }, [list.author]); + }, + + async sendMessage(id, { subject, text, thread = '', replyTo = '' } = {}) { + const { ssbClient, idx, priv } = await load(); + const me = ssbClient.id; + const list = find(idx, id); + if (!list || !visibleTo(list, me)) throw new Error('Mailing list not found'); + if (list.status !== 'ACTIVE') throw new Error('This mailing list is archived'); + let { all, others } = await recipientsFor(ssbClient, list); + if (!all.includes(me) && !list.closed && subscriptionsModel) { + await subscriptionsModel.setSubscription(list.id, 'mailing', true); + ({ all, others } = await recipientsFor(ssbClient, list)); + if (!all.includes(me)) { all = uniq([...all, me]); others = all.filter(id => id !== me); } + } + if (!all.includes(me)) throw new Error('Only participants can write to this mailing list'); + const body = safeText(text); + if (!body) throw new Error('Text is required'); + const mid = crypto.randomBytes(12).toString('hex'); + let threadId = safeText(thread); + let subj = safeText(subject); + if (!threadId && replyTo) { + const parent = historyFor(priv, list.id, me).find(h => h.key === replyTo || h.mid === replyTo); + if (parent) { threadId = parent.thread; if (!subj) subj = `RE: ${canonicalSubject(parent.subject)}`; } + } + const content = { + type: 'post', + from: me, + to: [], + subject: subj || list.title, + text: body, + sentAt: new Date().toISOString(), + private: true, + list: list.id, + mid, + thread: threadId || mid + }; + const sendBatch = async (recps) => { + const sealed = await longText.chunkContent({ ...content, to: recps }, 'text', { maxBytes: longText.CONTENT_BYTES.boxed, extra: { private: true }, publish: (c) => publishPrivate(ssbClient, c, recps) }); + return publishPrivate(ssbClient, sealed, recps); + }; + const out = []; + if (!others.length) out.push(await sendBatch([me])); + for (let i = 0; i < others.length; i += BATCH) { + out.push(await sendBatch(uniq([me, ...others.slice(i, i + BATCH)]))); + } + return { key: out[0].key, mid, thread: content.thread, batches: out.length }; + }, + + async getListById(id) { + const { ssbClient, idx, priv } = await load(); + const list = find(idx, id); + if (!list || !visibleTo(list, ssbClient.id)) return null; + return decorate(ssbClient, list, priv, list.closed ? null : await subscribersMap([list.id])); + }, + + async listAll({ filter = 'all', q = '' } = {}) { + const { ssbClient, idx, priv } = await load(); + const me = ssbClient.id; + const f = String(filter || 'all').toLowerCase(); + const needle = String(q || '').trim().toLowerCase(); + const visible = Array.from(idx.lists.values()).filter(l => visibleTo(l, me)); + const subs = await subscribersMap(visible.filter(l => !l.closed).map(l => l.id)); + let out = []; + for (const list of visible) out.push(await decorate(ssbClient, list, priv, subs)); + if (f === 'mine') out = out.filter(l => l.author === me); + else if (f === 'subscribed') out = out.filter(l => l.isMember && l.author !== me); + else if (f === 'recent') out = out.filter(l => l.lastActivityTs >= Date.now() - RECENT_MS); + else if (f === 'open') out = out.filter(l => !l.closed); + else if (f === 'closed') out = out.filter(l => l.closed); + else if (f === 'active') out = out.filter(l => l.status === 'ACTIVE'); + else if (f === 'archived') out = out.filter(l => l.status === 'ARCHIVED'); + if (needle) out = out.filter(l => [l.title, l.description, ...l.tags].some(v => String(v || '').toLowerCase().includes(needle))); + out.sort((a, b) => b.lastActivityTs - a.lastActivityTs); + return out; + }, + + async composerEntries() { + const all = await this.listAll({ filter: 'all' }); + return all.filter(l => l.isMember && l.status === 'ACTIVE').map(l => ({ target: l.id, scope: 'mailing', title: l.title, owner: l.author, count: l.participantCount })); + }, + + async titlesFor(ids) { + const out = new Map(); + const wanted = new Set((ids || []).map(String)); + if (!wanted.size) return out; + const { ssbClient, idx } = await load(); + for (const list of idx.lists.values()) { + if (wanted.has(list.id) && visibleTo(list, ssbClient.id)) out.set(list.id, { title: list.title, status: list.status, closed: list.closed }); + } + return out; + }, + + async resolveRootId(id) { + const { idx } = await load(); + const list = find(idx, id); + return list ? list.id : String(id || ''); + } + }; +}; diff --git a/src/models/main_models.js b/src/models/main_models.js index d53de86..477692c 100644 --- a/src/models/main_models.js +++ b/src/models/main_models.js @@ -1,6 +1,8 @@ "use strict"; const { buildValidatedTombstoneSet } = require("./tombstone_validator"); +const { readTyped, requestScope, authorActivity } = require("./typed_log"); +const longText = require("../backend/long_text"); const debug = require("../server/node_modules/debug")("oasis"); const { isRoot, isReply: isComment } = require("../server/node_modules/ssb-thread-schema"); const lodash = require("../server/node_modules/lodash"); @@ -12,13 +14,16 @@ const pullSort = require("../server/node_modules/pull-sort"); const path = require('path'); const fs = require('fs/promises'); -const os = require('os'); const ssbRef = require("../server/node_modules/ssb-ref"); const nameCache = require('../backend/nameCache'); +const sharedState = require('../configs/shared-state'); const { getConfig } = require('../configs/config-manager.js'); const logLimit = getConfig().ssbLogStream?.limit || 1000; +const LIFETIME_CONCURRENCY = 32; +const LIFETIME_TTL_MS = 60000; +const LIFETIME_CACHE_MAX = 20000; const isEncrypted = (message) => typeof message.value.content === "string"; const isNotEncrypted = (message) => isEncrypted(message) === false; @@ -65,9 +70,10 @@ const publicOnlyFilter = pull.filter(isNotPrivate); const configure = (...customOptions) => Object.assign({}, defaultOptions, ...customOptions); -// PEERS -const ebtDir = path.join(os.homedir(), '.ssb', 'ebt'); -const unfollowedPath = path.join(os.homedir(), '.ssb', 'gossip_unfollowed.json'); +const ssbConfig = require('../server/ssb_config'); +const peerHealth = require('./peer_health'); +const ebtDir = path.join(ssbConfig.path, 'ebt'); +const unfollowedPath = require('../configs/state-manager').statePath('gossip_unfollowed.json'); async function loadPeersFromEbt() { let result = []; @@ -83,13 +89,15 @@ async function loadPeersFromEbt() { try { const data = await fs.readFile(filePath, 'utf8'); const users = JSON.parse(data); + const touchedAt = (await fs.stat(filePath)).mtimeMs; const userList = Object.keys(users).map(u => ({ id: u, link: `/author/${encodeURIComponent(u)}` })); result.push({ pub: `@${core}.ed25519`, - users: userList + users: userList, + touchedAt }); } catch {} } @@ -119,7 +127,7 @@ const canonicalizePubId = (s) => { }; const parseRemote = (remote) => { - let m = /^net:([^:]+):\d+~shs:([^=]+)=/.exec(remote); + let m = /^(?:net|onion):([^:]+):\d+~shs:([^=]+)=/.exec(remote); if (m) return { host: m[1], pubId: canonicalizePubId(m[2]) }; m = /^wss?:\/\/([^:/]+)(?::\d+)?.*~shs:([^=]+)=/.exec(remote); if (m) return { host: m[1], pubId: canonicalizePubId(m[2]) }; @@ -160,10 +168,9 @@ function toLegacyInvite(s) { return `${m[1]}:${m[2]}:@${key}~${m[4]}`; } -// CORE MODEL module.exports = ({ cooler, isPublic }) => { const models = {}; - const getAbout = async ({ key, feedId }) => { + const readAboutOf = async (feedId) => { const ssb = await cooler.open(); const source = ssb.backlinks.read({ reverse: true, @@ -182,23 +189,31 @@ module.exports = ({ cooler, isPublic }) => { return new Promise((resolve, reject) => pull( source, - pull.find( - (message) => message.value.content[key] !== undefined, - (err, message) => { - if (err) { - reject(err); - } else { - if (message === null) { - resolve(null); - } else { - resolve(message.value.content[key]); - } - } + pull.collect((err, messages) => { + if (err) return reject(err); + const out = {}; + for (const message of messages || []) { + const content = (message && message.value && message.value.content) || {}; + for (const k of Object.keys(content)) if (out[k] === undefined && content[k] !== undefined) out[k] = content[k]; } - ) + resolve(out); + }) ) ); }; + const getAbout = async ({ key, feedId }) => { + const store = requestScope.getStore(); + let pending; + if (store) { + const memo = store.aboutOf || (store.aboutOf = new Map()); + if (!memo.has(feedId)) memo.set(feedId, readAboutOf(feedId)); + pending = memo.get(feedId); + } else { + pending = readAboutOf(feedId); + } + const all = await pending; + return all[key] === undefined ? null : all[key]; + }; const feeds_to_name = {}; let all_the_names = {}; let dirty = false; @@ -207,57 +222,48 @@ module.exports = ({ cooler, isPublic }) => { if (!dirty) return; if (running) return; running = true; - - all_the_names = {}; + dirty = false; const allFeeds = Object.keys(feeds_to_name); + sharedState.setSyncedPeerCount(allFeeds.length); console.log(`- Synced-peers: [ ${allFeeds.length} ]`); console.time("- Sync-time"); - const lookups = []; - for (const feed of allFeeds) { - const e = feeds_to_name[feed]; - let pair = { feed, name: e.name }; - lookups.push(enhanceFeedInfo(pair)); - } - Promise.all(lookups) - .then(() => { - dirty = false; + buildNameTable(allFeeds) + .then((table) => { + all_the_names = table; running = false; console.timeEnd("- Sync-time"); }) .catch((err) => { + dirty = true; running = false; console.warn("- Lookup Sync failed: ", err); }); }; - const enhanceFeedInfo = ({ feed, name }) => { - return new Promise((resolve, reject) => { - getAbout({ feedId: feed, key: "image" }) - .then((img) => { - if ( - img !== null && - typeof img !== "string" && - typeof img === "object" && - typeof img.link === "string" - ) { - img = img.link; - } else if (img === null) { - img = nullImage; - } - - models.friend - .getRelationship(feed) - .then((rel) => { - let feeds_named = all_the_names[name] || []; - feeds_named.push({ feed, name, rel, img }); - all_the_names[name.toLowerCase()] = feeds_named; - resolve(); - }) - .catch(reject); - }) - .catch(reject); - }); + const buildNameTable = async (allFeeds) => { + const ssb = await cooler.open(); + const [profiles, graph] = await Promise.all([ + models.about.profiles(), + new Promise((resolve) => ssb.friends.graph((err, g) => resolve(err || !g ? {} : g))) + ]); + const me = ssb.id; + const mine = graph[me] || {}; + const table = {}; + for (const feed of allFeeds) { + const e = feeds_to_name[feed]; + if (!e || typeof e.name !== "string") continue; + const p = profiles.get(feed) || {}; + const img = p.image && typeof p.image === "object" && typeof p.image.link === "string" + ? p.image.link + : (typeof p.image === "string" ? p.image : nullImage); + const rel = feed === me + ? { me: true, following: false, blocking: false, followsMe: false } + : { me: false, following: mine[feed] >= 0, blocking: Math.round(mine[feed]) === -1, followsMe: !!(graph[feed] && graph[feed][me] >= 0) }; + const k = e.name.toLowerCase(); + (table[k] || (table[k] = [])).push({ feed, name: e.name, rel, img }); + } + return table; }; async function enrichEntries(entries) { @@ -269,7 +275,8 @@ module.exports = ({ cooler, isPublic }) => { const { host, pubId } = parseRemote(remote); const effectiveKey = pubId || (data && data.key ? canonicalizePubId(data.key) : null); const name = host || (effectiveKey ? await models.about.name(effectiveKey).catch(() => (effectiveKey || '').slice(0, 10)) : remote); - const users = effectiveKey && ebtMap.has(effectiveKey) ? ebtMap.get(effectiveKey) : []; + const known = !!(effectiveKey && ebtMap.has(effectiveKey)); + const users = known ? ebtMap.get(effectiveKey) : []; const usersWithNames = await Promise.all( users.map(async (user) => { const userName = await models.about.name(user.id).catch(() => user.id); @@ -282,28 +289,29 @@ module.exports = ({ cooler, isPublic }) => { ...data, key: effectiveKey || remote, name, - users: usersWithNames + users: known ? usersWithNames : null } ]; }) ); }; -// ABOUT MODEL models.about = { publicWebHosting: async (feedId) => { const result = await getAbout({ key: "publicWebHosting", feedId, }); - return result === true; + if (result === true) return true; + const prefs = await getAbout({ key: "visibilityPrefs", feedId }).catch(() => null); + return !!(prefs && typeof prefs === 'object' && prefs.clearnet === true); }, deviceSource: async (feedId) => { const result = await getAbout({ key: "deviceSource", feedId }); return typeof result === 'string' && result.trim() ? result : null; }, - visibilityPrefs: async (feedId) => { - const result = await getAbout({ key: "visibilityPrefs", feedId }); + visibilityPrefs: async (feedId) => models.about.visibilityFrom(await getAbout({ key: "visibilityPrefs", feedId })), + visibilityFrom: (result) => { if (!result || typeof result !== 'object') return null; return { activity: result.activity === true, @@ -314,6 +322,9 @@ models.about = { ecoTax: result.ecoTax !== false, larpSign: result.larpSign === true, gpg: result.gpg !== false, + phone: result.phone === 'mutuals' || result.phone === 'off' ? result.phone : 'whole', + phoneDnd: result.phoneDnd === true, + clearnetSince: typeof result.clearnetSince === 'string' ? result.clearnetSince : '', clearnet: result.clearnet === true, fediverse: result.fediverse === true, fediverseHandle: typeof result.fediverseHandle === 'string' ? result.fediverseHandle : '', @@ -327,7 +338,13 @@ models.about = { clearnetImages: result.clearnetImages === true, clearnetDocuments: result.clearnetDocuments === true, clearnetTorrents: result.clearnetTorrents === true, + clearnetFiles: result.clearnetFiles === true, clearnetBookmarks: result.clearnetBookmarks === true, + clearnetPodcasts: result.clearnetPodcasts === true, + clearnetSchool: result.clearnetSchool === true, + clearnetMarket: result.clearnetMarket === true, + clearnetFeed: result.clearnetFeed === true, + clearnetWiki: result.clearnetWiki === true, profileShops: result.profileShops === true, profileJobs: result.profileJobs === true, profileEvents: result.profileEvents === true, @@ -338,9 +355,41 @@ models.about = { profileImages: result.profileImages === true, profileDocuments: result.profileDocuments === true, profileTorrents: result.profileTorrents === true, - profileBookmarks: result.profileBookmarks === true + profileFiles: result.profileFiles === true, + profileBookmarks: result.profileBookmarks === true, + profilePodcasts: result.profilePodcasts === true, + profileSchool: result.profileSchool === true, + profileMarket: result.profileMarket === true, + profileFeed: result.profileFeed === true, + profileWiki: result.profileWiki === true }; }, + profiles: async () => { + const store = requestScope.getStore(); + if (store && store.aboutProfiles) return store.aboutProfiles; + const pending = (async () => { + const ssb = await cooler.open(); + const out = new Map(); + await new Promise((resolve) => { + pull( + ssb.messagesByType({ type: "about", reverse: true }), + pull.drain((m) => { + const v = (m && m.value) || {}; + const c = v.content || {}; + if (!v.author || c.about !== v.author) return; + const cur = out.get(v.author) || {}; + for (const k of ["name", "description", "image", "visibilityPrefs", "gpgFingerprint", "deviceSource"]) if (cur[k] === undefined && c[k] !== undefined) cur[k] = c[k]; + out.set(v.author, cur); + }, () => resolve()) + ); + }); + const now = Date.now(); + for (const [id, p] of out) if (p.name) nameCache.set(id, p.name, now); + return out; + })(); + if (store) store.aboutProfiles = pending; + return pending; + }, name: async (feedId) => { if (isPublic && (await models.about.publicWebHosting(feedId)) === false) { return "Redacted"; @@ -479,16 +528,14 @@ models.about = { }, }; -// BLOBS MODEL function blobIdToHexPath(blobId) { - const homeDir = os.homedir(); const m = /^&([A-Za-z0-9+/=]+)\.sha256$/.exec(blobId); if (!m) throw new Error('Invalid blobId: ' + blobId); const b64 = m[1]; const buf = Buffer.from(b64, 'base64'); const hex = buf.toString('hex'); const prefix = hex.slice(0, 2); - return path.join(homeDir, '.ssb', 'blobs', 'sha256', prefix, hex); + return path.join(ssbConfig.path, 'blobs', 'sha256', prefix, hex); } async function checkLocalBlob(blobId) { @@ -496,21 +543,57 @@ async function checkLocalBlob(blobId) { try { const buf = await fs.readFile(filePath); if (buf && buf.length) return buf; - } catch (_) { /* not found */ } + } catch (_) {} return null; } models.blob = { + getLocal: async ({ blobId }) => { + const local = await checkLocalBlob(blobId); + if (local) return local; + const ssb = await cooler.open(); + const has = await new Promise((resolve) => ssb.blobs.has(blobId, (err, v) => resolve(!err && !!v))); + if (!has) return null; + return new Promise((resolve) => { + pull(ssb.blobs.get(blobId), pull.collect((err, bufs) => resolve(err || !bufs || !bufs.length ? null : Buffer.concat(bufs)))); + }); + }, + getCached: async ({ blobId }) => { + const local = await checkLocalBlob(blobId); + if (local) return local; + const ssb = await cooler.open(); + const has = await new Promise((resolve) => ssb.blobs.has(blobId, (err, v) => resolve(!err && !!v))); + if (!has) { + try { ssb.blobs.want(blobId, () => {}); } catch (_) {} + return null; + } + return new Promise((resolve) => { + pull( + ssb.blobs.get(blobId), + pull.collect(async (err, bufs) => { + if (err || !bufs || !bufs.length) return resolve(null); + const buffer = Buffer.concat(bufs); + try { + const filePath = blobIdToHexPath(blobId); + await fs.mkdir(path.dirname(filePath), { recursive: true }); + await fs.writeFile(filePath, buffer); + } catch (e) {} + resolve(buffer); + }) + ); + }); + }, getResolved: async ({ blobId, timeout = 30000 }) => { let buf = await checkLocalBlob(blobId); if (buf) return buf; const ssb = await cooler.open(); - await new Promise((resolve, reject) => { - ssb.blobs.want(blobId, (err) => { - if (err) reject(err); - else resolve(); - }); + const arrived = await new Promise((resolve) => { + const wait = setTimeout(() => resolve(false), timeout); + try { + ssb.blobs.want(blobId, (err) => { clearTimeout(wait); resolve(!err); }); + } catch (_) { clearTimeout(wait); resolve(false); } }); + if (!arrived) return null; return new Promise((resolve, reject) => { let timer = setTimeout(() => resolve(null), timeout); pull( @@ -523,7 +606,7 @@ models.blob = { const filePath = blobIdToHexPath(blobId); await fs.mkdir(path.dirname(filePath), { recursive: true }); await fs.writeFile(filePath, buffer); - } catch (e) { /* ignore */ } + } catch (e) {} resolve(buffer); }) ); @@ -540,8 +623,13 @@ models.blob = { } }; -// FRIENDS MODEL models.friend = { + blockedIds: async () => { + const ssb = await cooler.open(); + const graph = await new Promise((resolve) => ssb.friends.graph((err, g) => resolve(err || !g ? {} : g))); + const mine = graph[ssb.id] || {}; + return new Set(Object.keys(mine).filter(feed => Math.round(mine[feed]) === -1)); + }, setRelationship: async ({ feedId, following, blocking }) => { if (following && blocking) { throw new Error("Cannot follow and block at the same time"); @@ -630,7 +718,6 @@ models.friend = { }, }; -// META MODEL models.meta = { myFeedId: async () => { const ssb = await cooler.open(); @@ -729,7 +816,7 @@ models.meta = { let addr = p.address; if (!addr && p.host && p.port) { const core = String(p.key).replace(/^@/, "").replace(/\.ed25519$/, ""); - addr = `net:${p.host}:${p.port}~shs:${core}`; + addr = `${/\.onion$/i.test(String(p.host)) ? 'onion' : 'net'}:${p.host}:${p.port}~shs:${core}`; } if (!addr) continue; seen.add(key); @@ -764,7 +851,7 @@ models.meta = { discovered: async () => { const ssb = await cooler.open(); const snapshot = await ssb.conn.dbPeers(); - const gossipPath = path.join(os.homedir(), '.ssb', 'gossip.json'); + const gossipPath = path.join(ssbConfig.path, 'gossip.json'); let gossipMap = new Map(); try { const gossipData = JSON.parse(await fs.readFile(gossipPath, 'utf8')); @@ -805,7 +892,7 @@ models.meta = { if (dbKeys.has(ck)) continue; let host = data.host, port = data.port; if ((!host || !port) && addr) { - const m = String(addr).match(/^net:([^:]+):(\d+)/); + const m = String(addr).match(/^(?:net|onion):([^:]+):(\d+)/); if (m) { host = host || m[1]; port = port || Number(m[2]); } } mergedSnapshot.push([addr, { key: data.key, host, port, source: data.type || 'staged', verified: data.verified }]); @@ -824,12 +911,19 @@ models.meta = { .map(([, d]) => d && d.key ? canonicalizePubId(d.key) : null) .filter(Boolean) ); - const discoveredPeers = allDbPeers.filter(([, d]) => !onlineKeys.has(canonicalizePubId(d.key))); + const isDeadPeer = (d) => { + const ck = canonicalizePubId(d.key); + const legacy = gossipMap.get(ck) || {}; + return peerHealth.isDead({ key: ck, failures: Math.max(Number(d.failure) || 0, Number(legacy.failure) || 0) }); + }; + const forgotten = await loadUnfollowedSet(); + const discoveredPeers = allDbPeers.filter(([, d]) => !onlineKeys.has(canonicalizePubId(d.key)) && !isDeadPeer(d) && !forgotten.has(canonicalizePubId(d.key))); const discoveredIds = new Set(allDbPeers.map(([, d]) => canonicalizePubId(d.key))); const ebtList = await loadPeersFromEbt(); const ebtMap = new Map(ebtList.map(e => [e.pub, e.users])); const unknownPeers = []; - for (const { pub } of ebtList) { + for (const { pub, touchedAt } of ebtList) { + if (Date.now() - (Number(touchedAt) || 0) >= peerHealth.DEAD_AFTER_MS || forgotten.has(pub)) continue; if (!discoveredIds.has(pub) && !onlineKeys.has(pub)) { const name = await models.about.name(pub).catch(() => pub); unknownPeers.push([pub, { key: pub, name, users: ebtMap.get(pub) || [] }]); @@ -837,68 +931,6 @@ models.meta = { } return { discoveredPeers, unknownPeers }; }, - connStop: async () => { - const ssb = await cooler.open(); - try { - const result = await ssb.conn.stop(); - return result; - } catch (e) { - const expectedName = "TypeError"; - const expectedMessage = "Cannot read property 'close' of null"; - if (e.name === expectedName && e.message === expectedMessage) { - debug("ssbConn is already stopped -- caught error"); - } else { - throw new Error(e); - } - } - }, - connStart: async () => { - const ssb = await cooler.open(); - const result = await ssb.conn.start(); - - return result; - }, - connRestart: async () => { - await models.meta.connStop(); - await models.meta.connStart(); - }, - sync: async () => { - const ssb = await cooler.open(); - - const progress = await ssb.progress(); - let previousTarget = progress.indexes.target; - - let keepGoing = true; - const timeoutInterval = setTimeout(() => { - keepGoing = false; - }, 5 * 60 * 1000); - - await ssb.conn.start(); - - const diff = async () => - new Promise((resolve) => { - setTimeout(async () => { - const currentProgress = await ssb.progress(); - const currentTarget = currentProgress.indexes.target; - const difference = currentTarget - previousTarget; - previousTarget = currentTarget; - debug(`Difference: ${difference} bytes`); - resolve(difference); - }, 5000); - }); - - debug("Starting sync, waiting for new messages..."); - while (keepGoing && (await diff()) === 0) { - debug("Received no new messages."); - } - debug("Finished waiting for first new message."); - while (keepGoing && (await diff()) > 0) { - debug(`Still receiving new messages...`); - } - debug("Finished waiting for last new message."); - clearInterval(timeoutInterval); - await ssb.conn.stop(); - }, acceptInvite: async (invite) => { const ssb = await cooler.open(); const code = toLegacyInvite(String(invite || '')); @@ -934,7 +966,7 @@ models.meta = { return conditions.every((x) => x === true); }; - const maxMessages = 30; // change it to control post overloading + const maxMessages = 30; const getMessages = async ({ myFeedId, @@ -943,23 +975,8 @@ models.meta = { query, filter = null, }) => { - const source = ssb.createLogStream({ reverse: true, limit: logLimit }); - - return new Promise((resolve, reject) => { - pull( - source, - pull.filter((msg) => { - return msg.value.content.type === "post"; - }), - pull.collect((err, collectedMessages) => { - if (err) { - reject(err); - } else { - resolve(collectedMessages); - } - }) - ); - }); + const collected = await readTyped(ssb, ["post"], { limit: logLimit }); + return collected.filter((msg) => msg.value && msg.value.content && msg.value.content.type === "post").reverse(); }; const socialFilter = async ({ @@ -1185,7 +1202,6 @@ models.meta = { return messages.length ? messages[0] : undefined; }; -// POST MODEL const post = { firstBy: async (feedId) => { return getLimitPost(feedId, false); @@ -1269,25 +1285,6 @@ const post = { return { messages, myFeedId }; }, - fromHashtag: async (hashtag, customOptions = {}) => { - const ssb = await cooler.open(); - const myFeedId = ssb.id; - const query = [ - { - $filter: { - dest: `#${hashtag}`, - }, - }, - ]; - const messages = await getMessages({ - myFeedId, - customOptions, - ssb, - query, - }); - - return messages; - }, topicComments: async (rootId, customOptions = {}) => { const ssb = await cooler.open(); const myFeedId = ssb.id; @@ -1322,7 +1319,16 @@ const post = { return msg; }) ); - return fullMessages; + const topic = fullMessages.filter((m) => m && m.value && m.value.content && m.value.content.type === "post" && m.value.content.root === rootId); + const tombstones = topic.length ? await readTyped(ssb, ["tombstone"], { limit: 0 }).catch(() => []) : []; + const gone = buildValidatedTombstoneSet([...topic.filter(m => m && m.key && m.value), ...tombstones]); + const chunkIdx = longText.indexChunks(await readTyped(ssb, [longText.CHUNK_TYPE], { limit: logLimit })); + const lookup = longText.lookupIn(chunkIdx); + return topic + .filter(m => !(m && gone.has(m.key))) + .map(m => m && m.value && longText.hasChunks(m.value.content) + ? { ...m, value: { ...m.value, content: longText.resolveField(m.value.content, "text", m.value.author, lookup) } } + : m); }, likes: async ({ feed }, customOptions = {}) => { const ssb = await cooler.open(); @@ -1400,356 +1406,6 @@ const post = { ); }); - return messages; - }, - latest: async () => { - const ssb = await cooler.open(); - - const myFeedId = ssb.id; - - const source = ssb.query.read( - configure({ - query: [ - { - $filter: { - value: { - timestamp: { $lte: Date.now() }, - content: { - type: { $in: ["post", "blog"] }, - }, - }, - }, - }, - ], - }) - ); - const followingFilter = await socialFilter({ following: true }); - - const messages = await new Promise((resolve, reject) => { - pull( - source, - followingFilter, - publicOnlyFilter, - pull.take(maxMessages), - pull.collect((err, collectedMessages) => { - if (err) { - reject(err); - } else { - resolve(transform(ssb, collectedMessages, myFeedId)); - } - }) - ); - }); - - return messages; - }, - latestExtended: async () => { - const ssb = await cooler.open(); - - const myFeedId = ssb.id; - - const source = ssb.query.read( - configure({ - query: [ - { - $filter: { - value: { - timestamp: { $lte: Date.now() }, - content: { - type: { $in: ["post", "blog"] }, - }, - }, - }, - }, - ], - }) - ); - - const extendedFilter = await socialFilter({ - following: false, - me: false, - }); - - const messages = await new Promise((resolve, reject) => { - pull( - source, - publicOnlyFilter, - extendedFilter, - pull.take(maxMessages), - pull.collect((err, collectedMessages) => { - if (err) { - reject(err); - } else { - resolve(transform(ssb, collectedMessages, myFeedId)); - } - }) - ); - }); - - return messages; - }, - latestTopics: async () => { - const ssb = await cooler.open(); - - const myFeedId = ssb.id; - - const source = ssb.query.read( - configure({ - query: [ - { - $filter: { - value: { - timestamp: { $lte: Date.now() }, - content: { - type: { $in: ["post", "blog"] }, - }, - }, - }, - }, - ], - }) - ); - - const extendedFilter = await socialFilter({ - following: true, - }); - - const messages = await new Promise((resolve, reject) => { - pull( - source, - publicOnlyFilter, - pull.filter(hasNoRoot), - extendedFilter, - pull.take(maxMessages), - pull.collect((err, collectedMessages) => { - if (err) { - reject(err); - } else { - resolve(transform(ssb, collectedMessages, myFeedId)); - } - }) - ); - }); - - return messages; - }, - latestSummaries: async () => { - const ssb = await cooler.open(); - - const myFeedId = ssb.id; - - const options = configure({ - type: "post", - private: false, - }); - - const source = ssb.messagesByType(options); - - const extendedFilter = await socialFilter({ - following: true, - }); - - const messages = await new Promise((resolve, reject) => { - pull( - source, - pull.filter((message) => isNotPrivate(message) && hasNoRoot(message)), - extendedFilter, - pull.take(maxMessages), - pullParallelMap(async (message, cb) => { - const thread = await post.fromThread(message.key); - lodash.set( - message, - "value.meta.thread", - await transform(ssb, thread, myFeedId) - ); - cb(null, message); - }), - pull.collect((err, collectedMessages) => { - if (err) { - reject(err); - } else { - resolve(transform(ssb, collectedMessages, myFeedId)); - } - }) - ); - }); - - return messages; - }, - latestThreads: async () => { - const ssb = await cooler.open(); - - const myFeedId = ssb.id; - - const source = ssb.query.read( - configure({ - query: [ - { - $filter: { - value: { - timestamp: { $lte: Date.now() }, - content: { - type: { $in: ["post", "blog"] }, - }, - }, - }, - }, - ], - }) - ); - const basicSocialFilter = await socialFilter(); - - const messages = await new Promise((resolve, reject) => { - pull( - source, - basicSocialFilter, - pull.filter((message) => isNotPrivate(message) && hasNoRoot(message)), - pull.take(maxMessages), - pullParallelMap(async (message, cb) => { - const thread = await post.fromThread(message.key); - lodash.set( - message, - "value.meta.thread", - await transform(ssb, thread, myFeedId) - ); - cb(null, message); - }), - pull.filter((message) => message.value.meta.thread.length > 1), - pull.collect((err, collectedMessages) => { - if (err) { - reject(err); - } else { - resolve(transform(ssb, collectedMessages, myFeedId)); - } - }) - ); - }); - - return messages; - }, - - popular: async ({ period }) => { - const ssb = await cooler.open(); - - const periodDict = { - day: 1, - week: 7, - month: 30.42, - year: 365, - }; - - if (period in periodDict === false) { - throw new Error("invalid period"); - } - - const myFeedId = ssb.id; - - const now = new Date(); - const earliest = Number(now) - 1000 * 60 * 60 * 24 * periodDict[period]; - const source = ssb.query.read( - configure({ - query: [ - { - $filter: { - value: { - timestamp: { $gte: earliest }, - content: { - type: "vote", - }, - }, - }, - }, - ], - }) - ); - const basicSocialFilter = await socialFilter(); - - const messages = await new Promise((resolve, reject) => { - pull( - source, - publicOnlyFilter, - pull.filter((msg) => { - return ( - isNotEncrypted(msg) && - typeof msg.value.content.vote === "object" && - typeof msg.value.content.vote.link === "string" && - typeof msg.value.content.vote.value === "number" - ); - }), - pull.reduce( - (acc, cur) => { - const author = cur.value.author; - const target = cur.value.content.vote.link; - const value = cur.value.content.vote.value; - - if (acc[author] == null) { - acc[author] = {}; - } - acc[author][target] = Math.max(-1, Math.min(1, value)); - - return acc; - }, - {}, - (err, obj) => { - if (err) { - return reject(err); - } - const adjustedObj = Object.entries(obj).reduce( - (acc, [author, values]) => { - if (author === myFeedId) { - return acc; - } - const entries = Object.entries(values); - const total = 1 + Math.log(entries.length); - - entries.forEach(([link, value]) => { - if (acc[link] == null) { - acc[link] = 0; - } - acc[link] += value / total; - }); - return acc; - }, - [] - ); - - const arr = Object.entries(adjustedObj); - const length = arr.length; - - pull( - pull.values(arr), - pullSort(([, aVal], [, bVal]) => bVal - aVal), - pull.take(Math.min(length, maxMessages)), - pull.map(([key]) => key), - pullParallelMap(async (key, cb) => { - try { - const msg = await post.get(key); - cb(null, msg); - } catch (e) { - cb(null, null); - } - }), - pull.filter( - (message) => - message && - isNotPrivate(message) && - (message.value.content.type === "post" || - message.value.content.type === "blog") - ), - basicSocialFilter, - pull.collect((collectErr, collectedMessages) => { - if (collectErr) { - reject(collectErr); - } else { - resolve(collectedMessages); - } - }) - ); - } - ) - ); - }); - return messages; }, fromThread: async (msgId, customOptions) => { @@ -1884,19 +1540,21 @@ const post = { }, publish: async (options) => { const ssb = await cooler.open(); - const body = { type: "post", ...options }; - return new Promise((resolve, reject) => { - ssb.publish(body, (err, msg) => { + const publish = (content) => new Promise((resolve, reject) => { + ssb.publish(content, (err, msg) => { if (err) reject(err); else resolve(msg); }); }); + const body = { type: "post", ...options }; + const content = typeof body.text === "string" && !body.recps ? await longText.chunkContent(body, "text", { publish }) : body; + return publish(content); }, publishFediverseHandle: async (handle) => { const ssb = await cooler.open(); const current = (await models.about.visibilityPrefs(ssb.id).catch(() => null)) || {}; const h = String(handle || ''); - const prefs = { ...current, fediverseHandle: h }; + const prefs = { ...current, fediverseHandle: current.fediverse === true ? h : '' }; if (h === '') prefs.fediverse = false; return new Promise((resolve, reject) => { ssb.publish({ type: "about", about: ssb.id, visibilityPrefs: prefs }, (err, msg) => err ? reject(err) : resolve(msg)); @@ -1915,6 +1573,9 @@ const post = { ecoTax: r.ecoTax !== false, larpSign: r.larpSign === true, gpg: r.gpg !== false, + phone: r.phone === 'mutuals' || r.phone === 'off' ? r.phone : 'whole', + phoneDnd: !!(getConfig().phone || {}).dnd, + ...(typeof r.clearnetSince === 'string' && r.clearnetSince ? { clearnetSince: r.clearnetSince } : {}), clearnet: r.clearnet === true, fediverse: r.fediverse === true, fediverseHandle: typeof r.fediverseHandle === 'string' ? r.fediverseHandle : '', @@ -1928,7 +1589,13 @@ const post = { clearnetImages: r.clearnetImages === true, clearnetDocuments: r.clearnetDocuments === true, clearnetTorrents: r.clearnetTorrents === true, + clearnetFiles: r.clearnetFiles === true, clearnetBookmarks: r.clearnetBookmarks === true, + clearnetPodcasts: r.clearnetPodcasts === true, + clearnetSchool: r.clearnetSchool === true, + clearnetMarket: r.clearnetMarket === true, + clearnetFeed: r.clearnetFeed === true, + clearnetWiki: r.clearnetWiki === true, profileShops: r.profileShops === true, profileJobs: r.profileJobs === true, profileEvents: r.profileEvents === true, @@ -1939,7 +1606,13 @@ const post = { profileImages: r.profileImages === true, profileDocuments: r.profileDocuments === true, profileTorrents: r.profileTorrents === true, - profileBookmarks: r.profileBookmarks === true + profileFiles: r.profileFiles === true, + profileBookmarks: r.profileBookmarks === true, + profilePodcasts: r.profilePodcasts === true, + profileSchool: r.profileSchool === true, + profileMarket: r.profileMarket === true, + profileFeed: r.profileFeed === true, + profileWiki: r.profileWiki === true }; }; const prefs = visibilityPrefs ? normalizePrefs(visibilityPrefs) : undefined; @@ -1960,9 +1633,9 @@ const post = { if (resolvedBlobId !== undefined) baseFields.gpgBlobId = String(resolvedBlobId || ""); if (image && image.length > 0) { const megabyte = Math.pow(2, 20); - const maxSize = 50 * megabyte; + const maxSize = 75 * megabyte; if (image.length > maxSize) { - throw new Error("File is too big, maximum size is 50 megabytes"); + throw new Error("File is too big, maximum size is 75 megabytes"); } return new Promise((resolve, reject) => { pull( @@ -2115,32 +1788,34 @@ const post = { inbox: async () => { const ssb = await cooler.open(); const myFeedId = ssb.id; - const rawMessages = await new Promise((resolve, reject) => { - pull( - ssb.createLogStream({ reverse: true, limit: logLimit }), - pull.collect((err, msgs) => (err ? reject(err) : resolve(msgs))) - ); - }); - const decryptedMessages = rawMessages.map(msg => { - try { - return ssb.private.unbox(msg); - } catch { - return null; + let decryptedMessages = []; + try { + if (ssb.private && typeof ssb.private.read === 'function') { + decryptedMessages = await new Promise((resolve, reject) => pull(ssb.private.read({ reverse: true, limit: logLimit * 10 }), pull.collect((err, arr) => err ? reject(err) : resolve(arr || [])))); } - }).filter(Boolean); + } catch (_) { decryptedMessages = []; } + if (!decryptedMessages.length) { + const rawMessages = await readTyped(ssb, [], { limit: logLimit, withWindow: true }); + decryptedMessages = rawMessages.map(msg => { + try { + return ssb.private.unbox(msg); + } catch { + return null; + } + }).filter(Boolean); + } const tombstoneTargets = buildValidatedTombstoneSet(decryptedMessages); return decryptedMessages.filter(msg => { if (tombstoneTargets.has(msg.key)) return false; const content = msg.value?.content; const author = msg.value?.author; return content?.type === 'post' && content?.private === true && (author === myFeedId || content.to?.includes(myFeedId)); - }); + }).reverse(); } }; models.post = post; -// SPREAD MODEL models.vote = { publish: async ({ messageKey, value, recps }) => { const ssb = await cooler.open(); @@ -2187,9 +1862,11 @@ models.lifetime = (() => { if (days < FRESH_ORANGE_DAYS) return { bucket: 'orange', range: '2w–6m' }; return { bucket: 'red', range: '≥6m' }; }; - const lastAuthorTs = async (feedId) => { + const lastAuthorTsRaw = async (feedId) => { if (!feedId) return null; const ssbClient = await cooler.open(); + const activity = await authorActivity(ssbClient); + if (activity) return activity.get(feedId) || null; return new Promise((resolve) => { try { pull( @@ -2205,7 +1882,7 @@ models.lifetime = (() => { } catch (_) { resolve(null); } }); }; - const lastBacklinkTs = async (msgKey) => { + const lastBacklinkTsRaw = async (msgKey) => { if (!msgKey) return null; const ssbClient = await cooler.open(); return new Promise((resolve) => { @@ -2221,7 +1898,23 @@ models.lifetime = (() => { } catch (_) { resolve(null); } }); }; + const recent = new Map(); + const remembered = (bucket, key, fn) => { + if (!key) return fn(key); + let memo = recent.get(bucket); + if (!memo) recent.set(bucket, memo = new Map()); + const hit = memo.get(key); + if (hit && Date.now() - hit.at < LIFETIME_TTL_MS) return hit.value; + const value = fn(key); + memo.delete(key); + memo.set(key, { at: Date.now(), value }); + while (memo.size > LIFETIME_CACHE_MAX) memo.delete(memo.keys().next().value); + return value; + }; + const lastAuthorTs = (feedId) => remembered('author', feedId, lastAuthorTsRaw); + const lastBacklinkTs = (msgKey) => remembered('backlink', msgKey, lastBacklinkTsRaw); return { + forget: () => recent.clear(), bucket: bucketOf, lastAuthorTs, lastBacklinkTs, @@ -2234,30 +1927,40 @@ models.lifetime = (() => { const candidates = [authorTs, interactionTs, createdTs].filter(x => typeof x === 'number' && x > 0); const lastTs = candidates.length ? Math.max(...candidates) : null; const { bucket, range } = bucketOf(lastTs); - return { bucket, range, lastTs, authorTs, interactionTs, createdTs }; + const own = [interactionTs, createdTs].filter(x => typeof x === 'number' && x > 0); + const content = bucketOf(own.length ? Math.max(...own) : null); + return { bucket, range, contentBucket: content.bucket, contentRange: content.range, lastTs, authorTs, interactionTs, createdTs }; }, async enrichAndFilter(items, opts = {}) { const { includeDead = false, getKey = (x) => x.id || x.key, getAuthor = (x) => x.author, getCreatedAt = (x) => x.createdAt } = opts; - const authorCache = new Map(); + const list = Array.from(items || []); + const authors = [...new Set(list.map(getAuthor).filter(Boolean))]; + const mapLimit = async (items, fn) => { + const out = new Array(items.length); + let next = 0; + await Promise.all(Array.from({ length: Math.min(LIFETIME_CONCURRENCY, items.length) }, async () => { + while (next < items.length) { const k = next++; out[k] = await fn(items[k]); } + })); + return out; + }; + const authorTsList = await mapLimit(authors, (a) => lastAuthorTs(a)); + const authorCache = new Map(authors.map((a, i) => [a, authorTsList[i]])); + const interactionList = await mapLimit(list, (item) => { const key = getKey(item); return key ? lastBacklinkTs(key) : null; }); const out = []; - for (const item of items) { + for (let i = 0; i < list.length; i++) { + const item = list[i]; const author = getAuthor(item); - let authorTs; - if (author && authorCache.has(author)) { - authorTs = authorCache.get(author); - } else { - authorTs = author ? await lastAuthorTs(author) : null; - if (author) authorCache.set(author, authorTs); - } - const key = getKey(item); - const interactionTs = key ? await lastBacklinkTs(key) : null; + const authorTs = author ? authorCache.get(author) : null; + const interactionTs = interactionList[i]; const createdAt = getCreatedAt(item); const createdTs = createdAt ? new Date(createdAt).getTime() : null; const candidates = [authorTs, interactionTs, createdTs].filter(x => typeof x === 'number' && x > 0); const lastTs = candidates.length ? Math.max(...candidates) : null; const { bucket, range } = bucketOf(lastTs); if (!includeDead && bucket === 'red') continue; - out.push({ ...item, lifetime: { bucket, range, lastTs, authorTs, interactionTs, createdTs } }); + const own = [interactionTs, createdTs].filter(x => typeof x === 'number' && x > 0); + const content = bucketOf(own.length ? Math.max(...own) : null); + out.push({ ...item, lifetime: { bucket, range, contentBucket: content.bucket, contentRange: content.range, lastTs, authorTs, interactionTs, createdTs } }); } return out; } diff --git a/src/models/maps_model.js b/src/models/maps_model.js index b1bd77c..0cae474 100644 --- a/src/models/maps_model.js +++ b/src/models/maps_model.js @@ -3,6 +3,7 @@ const crypto = require("crypto"); const { getConfig } = require("../configs/config-manager.js"); const { buildValidatedTombstoneSet } = require('./tombstone_validator'); const { collabContent, openInviteOf } = require('../backend/collab_content'); +const { readTyped, memoIndex } = require("./typed_log"); const mapCollab = collabContent({ membersField: 'members', undecField: 'encrypted', contentFields: ['title', 'description', 'image'], listFields: ['tags', 'invites', 'markers'] }); const logLimit = getConfig().ssbLogStream?.limit || 1000; @@ -34,6 +35,16 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => { return (tribeCrypto && tribeCrypto.getKeys(rid)) || []; }; const lookupGen = (rid) => ((ownCrypto && ownCrypto.getGen(rid)) || (tribeCrypto && tribeCrypto.getGen(rid)) || 0); + const keyProofFor = (keyHex, id) => { try { return crypto.createHmac("sha256", Buffer.from(keyHex, "hex")).update(String(id), "utf8").digest("hex"); } catch (_) { return null; } }; + const hashCode = (code, salt) => (tribeCrypto ? tribeCrypto.hashInviteCode(code, salt) : null); + const inviteMatches = (inv, code) => { + if (!code) return false; + if (typeof inv === "string") return inv === code; + if (!inv || typeof inv !== "object") return false; + if (typeof inv.code === "string") return inv.code === code; + return typeof inv.ch === "string" && hashCode(code, inv.salt) === inv.ch; + }; + const inviteToken = (inv) => (inv && typeof inv === "object" && typeof inv.ch === "string") ? inv.ch : (typeof inv === "string" ? inv : (inv && inv.code) || ""); const rotateMapKey = async (rootId, remainingMembers) => { if (!ownCrypto || !tribeCrypto || !rootId) return; @@ -41,11 +52,10 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => { if (!existing) return; const newKey = ownCrypto.generateTribeKey(); const newGen = ownCrypto.addNewKey(rootId, newKey); - if (!Array.isArray(remainingMembers) || !remainingMembers.length) return; const ssbClient = await openSsb(); const ssbKeys = require("../server/node_modules/ssb-keys"); const memberKeys = {}; - for (const m of remainingMembers) { + for (const m of new Set([ssbClient.id, ...(Array.isArray(remainingMembers) ? remainingMembers : [])])) { try { memberKeys[m] = tribeCrypto.boxKeyForMember(newKey, m, ssbKeys); } catch (_) {} } if (Object.keys(memberKeys).length) { @@ -61,10 +71,13 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => { const ssbClient = await openSsb(); const ssbKeys = require("../server/node_modules/ssb-keys"); const config = require("../server/ssb_config"); - const msgs = await new Promise((resolve, reject) => pull(ssbClient.createLogStream({ limit: logLimit }), pull.collect((e, m) => e ? reject(e) : resolve(m)))); + const msgs = await getAllMessages(ssbClient); + const idx = buildIndex(unwrapForIndex(msgs)); + const trusted = (root, author) => { const n = idx.nodes.get(root); return !!author && !!n && (author === n.author || author === ssbClient.id); }; for (const m of msgs) { const c = m.value && m.value.content; if (!c || c.type !== "tribe-keys") continue; + if (!trusted(c.tribeId, m.value.author)) continue; const memberKeys = c.memberKeys; if (!memberKeys || typeof memberKeys !== "object") continue; const boxed = memberKeys[ssbClient.id]; @@ -148,13 +161,9 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => { } }; - const getAllMessages = async (ssbClient) => - new Promise((resolve, reject) => { - pull( - ssbClient.createLogStream({ limit: logLimit }), - pull.collect((err, msgs) => (err ? reject(err) : resolve(msgs))) - ); - }); + const MAP_TYPES = ["map", "mapMarker", "mapMember", "tribe-msg", "tribe-keys", "tombstone"]; + + const getAllMessages = async (ssbClient) => readTyped(ssbClient, MAP_TYPES, { limit: logLimit }); const getMsg = async (ssbClient, key) => new Promise((resolve) => { @@ -185,7 +194,7 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => { } if (c.type === "mapMember" && c.target) { - memberMsgs.push({ target: c.target, member: c.member, on: c.on !== false, author: v.author, ts: v.timestamp || m.timestamp || 0, code: typeof c.code === "string" ? c.code : "" }); + memberMsgs.push({ target: c.target, member: c.member, on: c.on !== false, author: v.author, ts: v.timestamp || m.timestamp || 0, code: typeof c.code === "string" ? c.code : "", keyProof: typeof c.keyProof === "string" ? c.keyProof : "" }); continue; } @@ -207,7 +216,7 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => { if (c.type !== "map") continue; const ts = v.timestamp || m.timestamp || 0; - nodes.set(k, { key: k, ts, c, author: v.author }); + nodes.set(k, { key: k, ts, seq: v.sequence || 0, c, author: v.author }); authorByKey.set(k, v.author); if (c.replaces) { @@ -235,21 +244,15 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => { return cur; }; - const contentTipOf = (root) => { - const rn = nodes.get(root); - if (!rn) return root; - let cur = root, best = root, g = 0; - const seen = new Set(); - while (child.has(cur) && !seen.has(cur) && g++ < 100000) { - seen.add(cur); - const next = child.get(cur); - const n = nodes.get(next); - if (!n) break; - if (n.author === rn.author && !tomb.has(next)) best = next; - cur = next; - } - return best; - }; + const ownerTip = new Map(); + for (const [k, n] of nodes) { + const r = rootOf(k); + const rn = nodes.get(r); + if (!rn || n.author !== rn.author) continue; + const prev = ownerTip.has(r) ? nodes.get(ownerTip.get(r)) : null; + if (!prev || n.seq > prev.seq || (n.seq === prev.seq && n.ts >= prev.ts)) ownerTip.set(r, k); + } + const contentTipOf = (root) => ownerTip.get(root) || root; for (const t of tombRequests) { const targetAuthor = authorByKey.get(t.target); @@ -268,8 +271,21 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => { const forward = new Map(); for (const [newId, oldId] of parent.entries()) forward.set(oldId, newId); + const invitesOf = (root) => { + const out = []; + for (const id of [root, contentTipOf(root)]) { const n = nodes.get(id); if (n && Array.isArray(n.c.invites)) out.push(...n.c.invites); } + return out; + }; + const selfJoinAllowed = (root, mm) => { + const oc = (nodes.get(root) || {}).c || {}; + if (!oc.encryptedPayload || oc.tribeId) return true; + if (invitesOf(root).some(inv => inv && typeof inv === "object" && inv.public === true)) return true; + return !!mm.keyProof && lookupKeys(root).some(k => keyProofFor(k, mm.member) === mm.keyProof); + }; + const memberByRoot = new Map(); const consumedByRoot = new Map(); + const codeClaimsByRoot = new Map(); for (const mm of memberMsgs) { if (!nodes.has(mm.target)) continue; const r = rootOf(mm.target); @@ -279,6 +295,11 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => { const byOwner = mm.author === ownerAuthor; if (!self && !byOwner) continue; if (!mm.member) continue; + if (mm.on && mm.code) { + if (!codeClaimsByRoot.has(r)) codeClaimsByRoot.set(r, []); + codeClaimsByRoot.get(r).push({ code: mm.code, member: mm.member, keyProof: mm.keyProof, byOwner }); + } + if (self && !byOwner && mm.on && !selfJoinAllowed(r, mm)) continue; if (mm.on && mm.code) { if (!consumedByRoot.has(r)) consumedByRoot.set(r, new Set()); consumedByRoot.get(r).add(mm.code); @@ -289,12 +310,24 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => { if (!prev || mm.ts >= prev.ts) m2.set(mm.member, { on: mm.on, ts: mm.ts }); } + const codeClaims = (root, codes) => (codeClaimsByRoot.get(root) || []).filter(c => codes.includes(c.code)); + const isCodeConsumed = (root, code) => { if (!code) return false; const set = consumedByRoot.get(root); return !!(set && set.has(code)); }; + for (const [k, n] of nodes) { + const r = rootOf(k); + const rn = nodes.get(r); + if (!rn || n.author === rn.author || !Array.isArray(n.c.members) || !selfJoinAllowed(r, { member: n.author })) continue; + if (!memberByRoot.has(r)) memberByRoot.set(r, new Map()); + const m2 = memberByRoot.get(r); + const prev = m2.get(n.author); + if (!prev || n.ts >= prev.ts) m2.set(n.author, { on: n.c.members.includes(n.author), ts: n.ts }); + } + const resolveMembers = (root) => { const ownerNode = nodes.get(root); const oc = ownerNode ? ownerNode.c : {}; @@ -305,29 +338,31 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => { return [...set]; }; - const ensureMemberKeys = async (ssbClient, messages, items) => { - if (!tribeCrypto) return; - const distributed = new Map(); + return { tomb, nodes, parent, child, strictChild, rootOf, tipOf, contentTipOf, tipByRoot, contentTipByRoot, forward, markers, rawMarkers, resolveMembers, isCodeConsumed, codeClaims }; + }; + + const ensureMemberKeys = async (ssbClient, messages, items) => { + if (!tribeCrypto || !ownCrypto) return; + const leavesByRoot = new Map(); for (const m of messages) { const c = m.value && m.value.content; - if (!c || c.type !== "tribe-keys" || !c.tribeId) continue; - const mk = c.memberKeys; - if (!mk || typeof mk !== "object") continue; - if (!distributed.has(c.tribeId)) distributed.set(c.tribeId, new Set()); - for (const id of Object.keys(mk)) distributed.get(c.tribeId).add(id); + if (!c || c.type !== "mapMember" || c.on !== false || !c.target || !c.member) continue; + if (!leavesByRoot.has(c.target)) leavesByRoot.set(c.target, []); + leavesByRoot.get(c.target).push({ member: c.member, author: m.value.author, ts: Number(m.timestamp || m.value.timestamp || 0) }); } const ssbKeys = require("../server/node_modules/ssb-keys"); for (const item of (Array.isArray(items) ? items : [])) { - if (!item || item.encrypted) continue; + if (!item || item.encrypted || item.author !== ssbClient.id) continue; const rootId = item.rootId; - if (!rootId) continue; + if (!rootId || !lookupKey(rootId)) continue; + const members = Array.isArray(item.members) ? item.members : []; + const leaves = (leavesByRoot.get(rootId) || []).filter(l => l.author === l.member || l.author === item.author); + const plan = ownCrypto.keyPlan({ rootId, ownerId: item.author, gen: lookupGen(rootId), members, messages, leaves }); + if (plan.rotate) { await rotateMapKey(rootId, members); continue; } + if (!plan.missing.length) continue; const key = lookupKey(rootId); - if (!key) continue; - const have = distributed.get(rootId) || new Set(); - const missing = (Array.isArray(item.members) ? item.members : []).filter(m => m && m !== ssbClient.id && !have.has(m)); - if (!missing.length) continue; const memberKeys = {}; - for (const m of missing) { + for (const m of plan.missing) { try { memberKeys[m] = tribeCrypto.boxKeyForMember(key, m, ssbKeys) } catch (_) {} } if (!Object.keys(memberKeys).length) continue; @@ -337,13 +372,11 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => { } } - return { tomb, nodes, parent, child, strictChild, rootOf, tipOf, contentTipOf, tipByRoot, contentTipByRoot, forward, markers, rawMarkers, resolveMembers, isCodeConsumed }; - }; - const expandMarkers = async (idx) => { for (const [mapId, raws] of idx.rawMarkers.entries()) { const list = []; for (const r of raws) { + if (idx.tomb.has(r.key)) continue; let c = r.c; if (c.encryptedPayload && tribeCrypto && tribesModel) { const dec = await tribeCrypto.decryptFromTribe(c, tribesModel); @@ -364,6 +397,15 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => { } }; + const markersFor = (idx, tipId, rootId) => { + const seen = new Set(); + return safeArr(idx.markers.get(tipId)).concat(safeArr(idx.markers.get(rootId))).filter((mk) => { + if (seen.has(mk.key)) return false; + seen.add(mk.key); + return true; + }); + }; + const buildMap = (node, rootId, viewerId, markerList = [], members = null) => { const c = node.c || {}; @@ -384,6 +426,7 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => { invites: Array.isArray(c.invites) ? c.invites : [], tribeId: c.tribeId || null, encrypted: !!undec, + contentEncrypted: !!(c.encryptedPayload || c._decrypted === true), createdAt: c.createdAt || new Date(node.ts).toISOString(), updatedAt: c.updatedAt || null, markers: markerList.filter((mk) => !mk.tombstoned) @@ -396,7 +439,7 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => { if (idx.tomb.has(tipId)) continue; const node = idx.nodes.get(tipId); if (!node) continue; - const markerList = safeArr(idx.markers.get(tipId)).concat(safeArr(idx.markers.get(rootId))); + const markerList = markersFor(idx, tipId, rootId); items.push(buildMap(node, rootId, viewerId, markerList, idx.resolveMembers(rootId))); } return items; @@ -411,18 +454,12 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => { if (!ownCrypto || typeof ownCrypto.getAllRootIds !== "function") return 0; try { const ssbClient = await openSsb(); - const messages = await new Promise((resolve, reject) => pull(ssbClient.createLogStream({ limit: logLimit }), pull.collect((e, m) => e ? reject(e) : resolve(m)))); - const live = new Set(); + const messages = await getAllMessages(ssbClient); const tomb = buildValidatedTombstoneSet(messages); - for (const m of messages) { - const c = m.value && m.value.content; - if (!c) continue; - if (c.type === "map") live.add(m.key); - } const all = ownCrypto.getAllRootIds(); let removed = 0; for (const rid of all) { - if (!live.has(rid) || tomb.has(rid)) { + if (tomb.has(rid)) { try { ownCrypto.dropKey(rid); removed += 1; } catch (_) {} } } @@ -437,11 +474,9 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => { if (!map) throw new Error("Map not found"); if (map.author === userId) throw new Error("Author cannot leave their own map"); if (!Array.isArray(map.members) || !map.members.includes(userId)) return; - const members = map.members.filter(m => m !== userId); const rootId = await this.resolveRootId(map.rootId || map.key); const content = { type: "mapMember", target: rootId, member: userId, on: false, createdAt: new Date().toISOString() }; const result = await new Promise((resolve, reject) => ssbClient.publish(content, (e, r) => e ? reject(e) : resolve(r))); - try { await rotateMapKey(rootId, members); } catch (_) {} return result; }, @@ -593,7 +628,7 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => { if (effectiveTribeId) { updated = await encryptIfTribe(updated); - } else if (mType !== "SINGLE") { + } else if (mType !== "OPEN") { updated = encryptStandalone(updated, rootId); } @@ -629,9 +664,10 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => { const messages = await getAllMessages(ssbClient); const idx = buildIndex(unwrapForIndex(messages)); + await decryptIndexNodes(idx); - let tipId = mapId; - while (idx.forward.has(tipId)) tipId = idx.forward.get(tipId); + const rootId = idx.rootOf(mapId); + const tipId = idx.contentTipOf(rootId); if (idx.tomb.has(tipId)) throw new Error("Map not found"); const node = idx.nodes.get(tipId); @@ -640,12 +676,11 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => { const mapDecrypted = await decryptIfTribe(node.c); assertReadable(mapDecrypted, "Map"); const mapType = mapDecrypted.mapType || node.c.mapType || "SINGLE"; - const mapAuthor = mapDecrypted.author || node.c.author; + const mapAuthor = node.author; if (mapType === "SINGLE") throw new Error("Map does not allow markers"); if (mapType === "CLOSED" && mapAuthor !== userId) throw new Error("Only the map creator can add markers"); const now = new Date().toISOString(); - const rootId = idx.rootOf(tipId); let content = { type: "mapMarker", mapId: rootId, @@ -670,6 +705,24 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => { }); }, + async deleteMarker(markerId) { + const ssbClient = await openSsb(); + const userId = ssbClient.id; + const msg = await getMsg(ssbClient, markerId); + if (!msg || !msg.content) throw new Error("Marker not found"); + let c = msg.content; + if (c.encryptedPayload && tribeCrypto && tribesModel) { + const dec = await tribeCrypto.decryptFromTribe(c, tribesModel); + if (dec && !dec._undecryptable) c = dec; + } + if (c.type !== "mapMarker" && !c.encryptedPayload) throw new Error("Marker not found"); + if (msg.author !== userId) throw new Error("Not the author"); + const tombstone = await tombFor(markerId, c.tribeId, userId); + return new Promise((resolve, reject) => { + ssbClient.publish(tombstone, (err, res) => (err ? reject(err) : resolve(res))); + }); + }, + async listAll(filterOrOpts = "all", maybeOpts = {}) { const ssbClient = await openSsb(); @@ -709,9 +762,7 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => { const viewer = viewerId || ssbClient.id; const messages = await getAllMessages(ssbClient); - const idx = buildIndex(unwrapForIndex(messages)); - await decryptIndexNodes(idx); - await expandMarkers(idx); + const idx = await memoIndex('maps', messages, async (m) => { const built = buildIndex(unwrapForIndex(m)); await decryptIndexNodes(built); await expandMarkers(built); return built; }); let root = id; while (idx.parent.has(root)) root = idx.parent.get(root); @@ -727,11 +778,11 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => { const dec = await tribeCrypto.decryptFromTribe(c, tribesModel); c = dec && !dec._undecryptable ? { ...dec, _decrypted: true } : { ...c, _decrypted: false }; } - const markerList = safeArr(idx.markers.get(tip)).concat(safeArr(idx.markers.get(root))); + const markerList = markersFor(idx, tip, root); return buildMap({ key: tip, ts: msg.timestamp || 0, c, author: msg.author }, root, viewer, markerList, idx.resolveMembers(root)); } - const markerList = safeArr(idx.markers.get(tip)).concat(safeArr(idx.markers.get(root))); + const markerList = markersFor(idx, tip, root); const map = buildMap(node, root, viewer, markerList, idx.resolveMembers(root)); return mapCollab.fold(map, collectMaps(idx, viewer)); }, @@ -746,8 +797,11 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => { let invite = code; if (tribeCrypto && !map.tribeId) { const inviteSalt = tribeCrypto.generateInviteSalt(); + const ident = opts.public ? { code, public: true } : { ch: hashCode(code, inviteSalt) }; const ekChain = tribeCrypto.encryptChainForInvite([map.rootId || map.key], code, inviteSalt); - if (ekChain) invite = { code, ekChain, salt: inviteSalt, gen: lookupGen(map.rootId || map.key) || 1, ...(opts.public ? { public: true } : {}) }; + const mapKey = ekChain ? null : lookupKey(map.rootId || map.key); + if (ekChain) invite = { ...ident, ekChain, salt: inviteSalt, gen: lookupGen(map.rootId || map.key) || 1 }; + else if (mapKey) invite = { ...ident, ek: tribeCrypto.encryptForInvite(mapKey, code, inviteSalt), salt: inviteSalt, gen: lookupGen(map.rootId || map.key) || 1 }; } if (opts.public && typeof invite !== "object") invite = { code, public: true }; const tipId = await this.resolveCurrentId(mapId); @@ -840,6 +894,7 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => { await new Promise((resolve, reject) => ssbClient.publish(updated, (e, r) => e ? reject(e) : resolve(r))); const tomb1 = await tombFor(tipId, effectiveTribeId, userId); await new Promise((resolve, reject) => ssbClient.publish(tomb1, e => e ? reject(e) : resolve())); + if (!effectiveTribeId) await rotateMapKey(rootId, Array.isArray(map.members) ? map.members : []); }, async joinByInvite(code) { @@ -854,8 +909,7 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => { for (const m of maps) { const invs = Array.isArray(m.invites) ? m.invites : []; for (const inv of invs) { - if (typeof inv === "string" && inv === code) { matched = m; matchedInvite = inv; break; } - if (typeof inv === "object" && inv.code === code) { matched = m; matchedInvite = inv; break; } + if (inviteMatches(inv, code)) { matched = m; matchedInvite = inv; break; } } if (matched) break; } @@ -863,30 +917,38 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => { if (Array.isArray(matched.members) && matched.members.includes(userId)) throw new Error("Already a member"); const isPublic = (typeof matchedInvite === "object" && matchedInvite.public === true); const rootId = await this.resolveRootId(matched.rootId || matched.key); - if (!isPublic) { - const idx = buildIndex(unwrapForIndex(await getAllMessages(ssbClient))); - if (idx.isCodeConsumed(rootId, code)) throw new Error("Invite already used"); - } + const token = inviteToken(matchedInvite) || code; let mapKey = null; + let chain = null; if (tribeCrypto && typeof matchedInvite === "object") { if (matchedInvite.ekChain) { - const chain = tribeCrypto.decryptChainFromInvite(matchedInvite.ekChain, code, matchedInvite.salt); - if (Array.isArray(chain) && chain.length) { - for (const entry of chain) { - if (Array.isArray(entry.keys) && entry.keys.length) { - tribeCrypto.setKeys(entry.rootId, entry.keys, entry.gen || entry.keys.length); - } else if (entry.key) { - tribeCrypto.setKey(entry.rootId, entry.key, entry.gen || 1); - } - } - mapKey = chain[0].key; - } + chain = tribeCrypto.decryptChainFromInvite(matchedInvite.ekChain, code, matchedInvite.salt, 3); + if (Array.isArray(chain) && chain.length) mapKey = chain[0].key || (Array.isArray(chain[0].keys) ? chain[0].keys[0] : null); + else chain = null; } else if (matchedInvite.ek) { mapKey = tribeCrypto.decryptFromInvite(matchedInvite.ek, code, matchedInvite.salt); - ownCrypto.setKey(matched.rootId || matched.key, mapKey, matchedInvite.gen || 1); } } - await new Promise((resolve, reject) => ssbClient.publish({ type: "mapMember", target: rootId, member: userId, on: true, createdAt: new Date().toISOString(), ...(isPublic ? {} : { code }) }, (e, r) => e ? reject(e) : resolve(r))); + if (!isPublic) { + const idx = buildIndex(unwrapForIndex(await getAllMessages(ssbClient))); + if (idx.isCodeConsumed(rootId, token) || idx.isCodeConsumed(rootId, code)) throw new Error("Invite already used"); + const claims = idx.codeClaims(rootId, [token, code]).filter(c => c.member !== userId); + const spent = mapKey ? claims.some(c => c.byOwner || keyProofFor(mapKey, c.member) === c.keyProof) : claims.length > 0; + if (spent) throw new Error("Invite already used"); + } + if (chain) { + for (const entry of chain) { + if (Array.isArray(entry.keys) && entry.keys.length) { + tribeCrypto.setKeys(entry.rootId, entry.keys, entry.gen || entry.keys.length); + } else if (entry.key) { + tribeCrypto.setKey(entry.rootId, entry.key, entry.gen || 1); + } + } + } else if (mapKey) { + ownCrypto.setKey(matched.rootId || matched.key, mapKey, matchedInvite.gen || 1); + } + if (!mapKey && matched.contentEncrypted && !matched.tribeId) throw new Error("Invalid or expired invite code"); + await new Promise((resolve, reject) => ssbClient.publish({ type: "mapMember", target: rootId, member: userId, on: true, createdAt: new Date().toISOString(), ...(isPublic ? {} : { code: token }), ...(mapKey ? { keyProof: keyProofFor(mapKey, userId) } : {}) }, (e, r) => e ? reject(e) : resolve(r))); if (tribeCrypto && mapKey) { try { const ssbKeys = require("../server/node_modules/ssb-keys"); diff --git a/src/models/market_model.js b/src/models/market_model.js index f4143a7..9bbf051 100644 --- a/src/models/market_model.js +++ b/src/models/market_model.js @@ -1,8 +1,10 @@ const pull = require("../server/node_modules/pull-stream") const moment = require("../server/node_modules/moment") const { getConfig } = require("../configs/config-manager.js") +const categories = require("../backend/opinion_categories") const { buildValidatedTombstoneSet } = require('./tombstone_validator') -const { dedupeByPreferring, norm } = require('../backend/dedupe') +const { readTyped, memoIndex } = require('./typed_log') +const { dedupeByPreferring, norm, versionGroups } = require('../backend/dedupe') const logLimit = getConfig().ssbLogStream?.limit || 1000 const N = (s) => String(s || "").toUpperCase().replace(/\s+/g, "_") @@ -62,62 +64,60 @@ module.exports = ({ cooler, tribeCrypto }) => { return ssb } - const readAll = async (ssbClient) => { - return new Promise((resolve, reject) => - pull(ssbClient.createLogStream({ limit: logLimit }), pull.collect((err, msgs) => (err ? reject(err) : resolve(msgs)))) - ) - } + const MARKET_TYPES = ["market", "marketBid", "marketPurchase", "marketOpinion", "tombstone"] + + const readAll = async (ssbClient) => readTyped(ssbClient, MARKET_TYPES, { limit: logLimit }) const buildMarketIndex = (messages) => { const tomb = buildValidatedTombstoneSet(messages) const nodes = new Map() const bids = [] const purchases = [] + const opinionMsgs = [] for (const m of messages) { const c = m.value && m.value.content if (!c) continue if (c.type === "tombstone") continue if (c.type === "marketBid") { bids.push({ target: c.target, author: m.value.author, amount: c.amount, time: c.time, ts: (m.value && m.value.timestamp) || 0 }); continue } if (c.type === "marketPurchase") { purchases.push({ target: c.target, author: m.value.author, ts: (m.value && m.value.timestamp) || 0 }); continue } + if (c.type === "marketOpinion" && c.target) { opinionMsgs.push({ target: c.target, author: m.value.author, category: c.category }); continue } if (c.type !== "market") continue - nodes.set(m.key, { key: m.key, ts: (m.value && m.value.timestamp) || m.timestamp || 0, c, author: m.value.author }) + nodes.set(m.key, { key: m.key, ts: (m.value && m.value.timestamp) || m.timestamp || 0, seq: m.value.sequence || 0, c, author: m.value.author }) } - const naivePrev = new Map(), strictNext = new Map() - for (const [key, n] of nodes) { - const t = n.c.replaces - if (!t) continue - naivePrev.set(key, t) - } - for (const [child, parent] of Array.from(naivePrev.entries())) { - const cn = nodes.get(child) - const pn = nodes.get(parent) - if (!pn) { naivePrev.delete(child); continue } - if (!cn || cn.author !== pn.author) { naivePrev.delete(child); nodes.delete(child); continue } - strictNext.set(parent, child) - } - return { tomb, nodes, bids, purchases, naivePrev, strictNext } + const vg = versionGroups(Array.from(nodes.values()).map(n => ({ key: n.key, author: n.author, seq: n.seq, ts: n.ts, replaces: typeof n.c.replaces === "string" ? n.c.replaces : null, claimedAuthor: typeof n.c.seller === "string" ? n.c.seller : null }))) + return { tomb, nodes, bids, purchases, opinionMsgs, vg } } const resolveGroups = (idx) => { - const { tomb, nodes, bids, purchases, naivePrev, strictNext } = idx - const rootOf = (key) => { let x = key, g = 0; while (naivePrev.has(x) && nodes.has(naivePrev.get(x)) && g++ < 100000) x = naivePrev.get(x); return x } - const followStrict = (key) => { let x = key, g = 0; while (strictNext.has(x) && g++ < 100000) x = strictNext.get(x); return x } + const { tomb, nodes, bids, purchases, opinionMsgs, vg } = idx + const rootOf = vg.rootOf - const groups = new Map() - for (const key of nodes.keys()) { const r = rootOf(key); if (!groups.has(r)) groups.set(r, []); groups.get(r).push(key) } const bidsByRoot = new Map() for (const bd of bids) { if (!nodes.has(bd.target)) continue; const r = rootOf(bd.target); if (!bidsByRoot.has(r)) bidsByRoot.set(r, []); bidsByRoot.get(r).push(bd) } const soldByRoot = new Map() - for (const pu of purchases) { if (!nodes.has(pu.target)) continue; const r = rootOf(pu.target); soldByRoot.set(r, (soldByRoot.get(r) || 0) + 1) } + const buyersByRoot = new Map() + for (const pu of purchases) { + if (!nodes.has(pu.target)) continue + const r = rootOf(pu.target) + soldByRoot.set(r, (soldByRoot.get(r) || 0) + 1) + if (!buyersByRoot.has(r)) buyersByRoot.set(r, new Set()) + buyersByRoot.get(r).add(pu.author) + } + const opinionsByRoot = new Map() + for (const op of (opinionMsgs || [])) { + if (!nodes.has(op.target)) continue + const r = rootOf(op.target) + if (!opinionsByRoot.has(r)) opinionsByRoot.set(r, []) + opinionsByRoot.get(r).push(op) + } const out = new Map() - for (const [root, keys] of groups) { + for (const root of vg.roots) { + const keys = vg.groups.get(root) || [root] const rootNode = nodes.get(root) const sellerId = rootNode ? rootNode.author : null const sellerKeys = keys.filter(k => { const n = nodes.get(k); return n && n.author === sellerId }) - let tip = followStrict(root) - const tipNode0 = nodes.get(tip) - if (!tipNode0 || tipNode0.author !== sellerId) tip = root + const tip = vg.tipOf(root) if (tomb.has(tip)) continue let best = nodes.get(tip) if (!best) continue @@ -131,7 +131,16 @@ module.exports = ({ cooler, tribeCrypto }) => { const addLine = (line) => { const b = parseBidEntry(line); if (!b) return; const kk = `${b.bidder}|${b.amount}|${b.time}`; if (pollSet.has(kk)) return; pollSet.add(kk); poll.push(line) } for (const k of (sellerKeys.length ? sellerKeys : keys)) { const n = nodes.get(k); if (n && Array.isArray(n.c.auctions_poll)) for (const line of n.c.auctions_poll) addLine(line) } for (const bd of (bidsByRoot.get(root) || [])) { const amt = Number(bd.amount); addLine(`${bd.author}|${Number.isFinite(amt) ? amt.toFixed(6) : bd.amount}|${bd.time}`) } - out.set(root, { tip, rootId: root, best, statusN: bestS, poll, soldCount: soldByRoot.get(root) || 0 }) + const opinions = {} + const voters = [] + for (const op of (opinionsByRoot.get(root) || [])) { + if (!op.author || op.author === sellerId) continue + if (voters.includes(op.author)) continue + if (!categories.includes(op.category)) continue + voters.push(op.author) + opinions[op.category] = (opinions[op.category] || 0) + 1 + } + out.set(root, { tip, rootId: root, best, statusN: bestS, poll, soldCount: soldByRoot.get(root) || 0, buyers: Array.from(buyersByRoot.get(root) || []), opinions, voters }) } return out } @@ -180,6 +189,7 @@ module.exports = ({ cooler, tribeCrypto }) => { shopProductId: shopOpts.shopProductId || "", shopId: shopOpts.shopId || "", shopTitle: shopOpts.shopTitle || "", + industry: shopOpts.industry || "", visibility: String(visibility || "PUBLIC").toUpperCase() === "HIDDEN" ? "HIDDEN" : "PUBLIC" } @@ -198,10 +208,9 @@ module.exports = ({ cooler, tribeCrypto }) => { async resolveCurrentId(itemId) { const ssbClient = await openSsb() const messages = await readAll(ssbClient) - const { tomb, strictNext } = buildMarketIndex(messages) - let cur = itemId, g = 0 - while (strictNext.has(cur) && g++ < 100000) cur = strictNext.get(cur) - if (tomb.has(cur)) throw new Error("Item not found") + const { tomb, vg } = buildMarketIndex(messages) + const cur = vg.tipOf(itemId) + if (tomb.has(cur) || vg.isForeignCopy(vg.rootOf(cur))) throw new Error("Item not found") return cur }, @@ -227,9 +236,13 @@ module.exports = ({ cooler, tribeCrypto }) => { normalized.price = p.toFixed(6) } + const currentItem = await new Promise((resolve) => ssbClient.get(tipId, (e, m) => resolve(e ? null : (m && m.content) || null))) + if (normalized.deadline !== undefined && normalized.deadline !== null && normalized.deadline !== "") { const dl = moment(normalized.deadline, moment.ISO_8601, true) if (!dl.isValid()) throw new Error("Invalid deadline") + const changed = !currentItem || !currentItem.deadline || !moment(currentItem.deadline).isSame(dl) + if (changed && dl.isBefore(moment(), "minute")) throw new Error("The deadline cannot be in the past") normalized.deadline = dl.toISOString() } @@ -297,7 +310,7 @@ module.exports = ({ cooler, tribeCrypto }) => { const items = [] const now = moment() - for (const { tip, rootId, best, statusN, poll, soldCount } of resolveGroups(buildMarketIndex(messages)).values()) { + for (const { tip, rootId, best, statusN, poll, soldCount, buyers, opinions, voters } of resolveGroups(buildMarketIndex(messages)).values()) { const leaf = tip const c = best.c let status = D(statusN) @@ -344,7 +357,12 @@ module.exports = ({ cooler, tribeCrypto }) => { mapUrl: c.mapUrl || "", shopProductId: c.shopProductId || "", shopId: c.shopId || "", - shopTitle: c.shopTitle || "" + shopTitle: c.shopTitle || "", + industry: c.industry || "", + opinions: opinions || {}, + opinions_inhabitants: voters || [], + purchasedByViewer: Array.isArray(buyers) && buyers.includes(userId), + ratedByViewer: Array.isArray(voters) && voters.includes(userId) }) } @@ -395,11 +413,10 @@ module.exports = ({ cooler, tribeCrypto }) => { const userId = ssbClient.id const messages = await readAll(ssbClient) - const idx = buildMarketIndex(messages) - const rootOf = (key) => { let x = key, g = 0; while (idx.naivePrev.has(x) && idx.nodes.has(idx.naivePrev.get(x)) && g++ < 100000) x = idx.naivePrev.get(x); return x } - const grp = resolveGroups(idx).get(rootOf(itemId)) + const idx = memoIndex('market', messages, buildMarketIndex) + const grp = resolveGroups(idx).get(idx.vg.rootOf(itemId)) if (!grp) return null - const { tip, rootId, best, statusN, poll, soldCount } = grp + const { tip, rootId, best, statusN, poll, soldCount, buyers, opinions, voters } = grp const c = best.c let status = D(statusN) @@ -445,7 +462,12 @@ module.exports = ({ cooler, tribeCrypto }) => { mapUrl: c.mapUrl || "", shopProductId: c.shopProductId || "", shopId: c.shopId || "", - shopTitle: c.shopTitle || "" + shopTitle: c.shopTitle || "", + industry: c.industry || "", + opinions: opinions || {}, + opinions_inhabitants: voters || [], + purchasedByViewer: Array.isArray(buyers) && buyers.includes(userId), + ratedByViewer: Array.isArray(voters) && voters.includes(userId) } }, @@ -454,6 +476,7 @@ module.exports = ({ cooler, tribeCrypto }) => { const myId = ssbClient.id const now = moment() const list = Array.isArray(items) ? items : [] + let changed = 0 for (const item of list) { if (!item || !item.deadline) continue @@ -476,8 +499,10 @@ module.exports = ({ cooler, tribeCrypto }) => { try { await this.updateItemById(item.id, { status }) + changed++ } catch (_) {} } + return changed }, async getItemByShopProductId(shopProductId) { @@ -486,6 +511,21 @@ module.exports = ({ cooler, tribeCrypto }) => { return items.find((i) => i.shopProductId === shopProductId) || null }, + async createOpinion(itemId, category) { + if (!categories.includes(category)) throw new Error("Invalid category") + const ssbClient = await openSsb() + const userId = ssbClient.id + const messages = await readAll(ssbClient) + const idx = buildMarketIndex(messages) + const group = resolveGroups(idx).get(idx.vg.rootOf(itemId)) + if (!group) throw new Error("Item not found") + if (group.best.author === userId) throw new Error("You cannot rate your own item") + if ((group.voters || []).includes(userId)) throw new Error("Already voted") + if (!(group.buyers || []).includes(userId)) throw new Error("You can rate only an item you have bought") + const content = { type: "marketOpinion", target: group.rootId, category, createdAt: new Date().toISOString() } + return new Promise((res, rej) => ssbClient.publish(content, (e, m) => e ? rej(e) : res(m))) + }, + async setItemAsSold(itemId) { const tipId = await this.resolveCurrentId(itemId) const ssbClient = await openSsb() @@ -536,8 +576,7 @@ module.exports = ({ cooler, tribeCrypto }) => { const ssbClient = await openSsb() const messages = await readAll(ssbClient) const idx = buildMarketIndex(messages) - const rootOf = (key) => { let x = key, g = 0; while (idx.naivePrev.has(x) && idx.nodes.has(idx.naivePrev.get(x)) && g++ < 100000) x = idx.naivePrev.get(x); return x } - const grp = resolveGroups(idx).get(rootOf(itemId)) + const grp = resolveGroups(idx).get(idx.vg.rootOf(itemId)) if (!grp) throw new Error("Item not found") const c = grp.best.c diff --git a/src/models/media_gallery.js b/src/models/media_gallery.js new file mode 100644 index 0000000..3d8c7b4 --- /dev/null +++ b/src/models/media_gallery.js @@ -0,0 +1,42 @@ +const MAX_IMAGES = 8 + +const MEDIA_MARKDOWN = /\[(image|video|audio):[^\]]*\]\(/ + +const blobOf = (value) => { + let blobId = value || null + if (blobId && /\(([^)]+)\)/.test(String(blobId))) blobId = String(blobId).match(/\(([^)]+)\)/)[1] + return blobId || null +} + +const keepMarkdown = (value, id) => + MEDIA_MARKDOWN.test(value) || value.startsWith("![") ? value : id + +const normalizeImages = (raw, max = MAX_IMAGES) => { + const list = Array.isArray(raw) ? raw : (raw ? [raw] : []) + const out = [] + const seen = new Set() + for (const entry of list) { + const value = String(entry || "").trim() + const id = blobOf(value) + if (!id || seen.has(id)) continue + seen.add(id) + out.push(keepMarkdown(value, id)) + if (out.length >= max) break + } + return out +} + +const normalizeVideo = (raw) => { + const value = String(Array.isArray(raw) ? raw[0] || "" : raw || "").trim() + const id = blobOf(value) + if (!id) return "" + return keepMarkdown(value, id) +} + +const mergeGallery = (current, uploaded, removeIndex = -1, max = MAX_IMAGES) => { + let list = normalizeImages(current, max) + if (removeIndex >= 0) list = list.filter((_, i) => i !== removeIndex) + return normalizeImages([...list, ...normalizeImages(uploaded, max)], max) +} + +module.exports = { MAX_IMAGES, blobOf, normalizeImages, normalizeVideo, mergeGallery } diff --git a/src/models/melody_model.js b/src/models/melody_model.js index 072b763..57ebd20 100644 --- a/src/models/melody_model.js +++ b/src/models/melody_model.js @@ -22,12 +22,14 @@ const TYPE_TO_DEGREE = { bookmark: 4, feed: 5, pad: 6, + room: 6, chat: 7, audio: 8, video: 9, image: 10, document: 11, torrent: 0, + file: 0, map: 2, pixelia: 3, gameScore: 5, @@ -39,7 +41,35 @@ const TYPE_TO_DEGREE = { courts: 0, market: 1, aiExchange: 6, - tombstone: 4 + tombstone: 4, + wikiPage: 2, + emergency: 11, + emergencyConfirm: 11, + emergencyUpdate: 11, + mailingList: 5, + logisticsRoute: 3, + logisticsRating: 3, + podcast: 8, + podcastEpisode: 8, + podcastPlay: 8, + campaign: 9, + campaignSignature: 9, + campaignUpdate: 9 +}; + +const FAMILY_DEGREE = [ + ['tribeParliament', 11], ['parliament', 11], ['courts', 0], ['school', 1], ['housing', 4], ['industry', 6], + ['poll', 2], ['blog', 0], ['larp', 9], ['chat', 7], ['pad', 6], ['room', 6], ['wiki', 2], ['emergency', 11], + ['mailing', 5], ['logistics', 3], ['podcast', 8], ['campaign', 9], ['project', 6], ['market', 1], ['shop', 1], + ['job', 3], ['event', 8], ['task', 10], ['report', 10], ['votes', 7], ['calendar', 8], ['map', 2], + ['transfer', 11], ['bank', 11], ['ubi', 11], ['feed', 5], ['tribe', 9], ['forum', 7], ['bookmark', 4], + ['audio', 8], ['video', 9], ['image', 10], ['document', 11], ['torrent', 0], ['file', 0] +]; + +const degreeOf = (type) => { + if (TYPE_TO_DEGREE[type] != null) return TYPE_TO_DEGREE[type]; + const family = FAMILY_DEGREE.find(([prefix]) => type.startsWith(prefix)); + return family ? family[1] : (Math.abs(hashStr(type)) % 12); }; const NOTE_FREQS = (() => { @@ -59,7 +89,7 @@ function blockToNote(msg) { const c = msg && msg.value && msg.value.content; if (!c || typeof c !== 'object') return null; const type = String(c.type || '').trim() || 'unknown'; - const degree = TYPE_TO_DEGREE[type] != null ? TYPE_TO_DEGREE[type] : (Math.abs(hashStr(type)) % 12); + const degree = degreeOf(type); const size = Buffer.byteLength(JSON.stringify(msg.value), 'utf8'); const octIdx = size < 256 ? 0 : (size < 1024 ? 1 : 2); const octave = OCTAVES[octIdx]; diff --git a/src/models/mentions_model.js b/src/models/mentions_model.js new file mode 100644 index 0000000..85a9160 --- /dev/null +++ b/src/models/mentions_model.js @@ -0,0 +1,213 @@ +const fs = require('fs'); +const pull = require('../server/node_modules/pull-stream'); +const { readTyped, CONTENT_TYPES } = require('./typed_log'); + +const SEEN_FILE = 'oasis-mentions-seen'; + +const seenPath = () => { + try { return require('../configs/state-manager').statePath(SEEN_FILE); } catch (_) { return null; } +}; + +const MAX_SEEN_KEYS = 500; + +const readSeen = () => { + const p = seenPath(); + if (!p) return { ts: 0, keys: [] }; + let raw = ''; + try { raw = fs.readFileSync(p, 'utf8').trim(); } catch (_) { return { ts: 0, keys: [] }; } + if (!raw) return { ts: 0, keys: [] }; + try { + const parsed = JSON.parse(raw); + if (parsed && typeof parsed === 'object') { + return { ts: Number(parsed.ts) || 0, keys: Array.isArray(parsed.keys) ? parsed.keys : [] }; + } + } catch (_) {} + return { ts: Number(raw) || 0, keys: [] }; +}; + +const writeSeen = (keys, ts) => { + const p = seenPath(); + if (!p) return false; + const trimmed = (Array.isArray(keys) ? keys : []).filter(Boolean).slice(0, MAX_SEEN_KEYS); + try { fs.writeFileSync(p, JSON.stringify({ ts: Number(ts) || Date.now(), keys: trimmed })); return true; } catch (_) { return false; } +}; +const { getConfig } = require('../configs/config-manager.js'); +const { buildValidatedTombstoneSet } = require('./tombstone_validator'); +const { versionGroups, versionEntry } = require('../backend/dedupe'); +const logLimit = getConfig().ssbLogStream?.limit || 1000; + +const TEXT_FIELDS = ['text', 'description', 'title', 'concept', 'question', 'rules', 'message', 'body', 'subject']; + +const SKIP_TYPES = new Set([ + 'about', 'contact', 'vote', 'pub', 'tombstone', 'karmaScore', + 'tribe-invite-msg', 'tribe-invite-tombstone', 'tribe-key-distrib' +]); + +module.exports = ({ cooler }) => { + let ssb; + const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb; }; + + const getAllMessages = async (ssbClient) => readTyped(ssbClient, CONTENT_TYPES, { limit: logLimit, withWindow: true }); + + const mentionsList = (content, feedId) => { + const m = content && content.mentions; + if (!m) return false; + const values = Array.isArray(m) ? m : (typeof m === 'object' ? Object.values(m).flat() : []); + return values.some(v => v && (v.link === feedId || v === feedId)); + }; + + const escapeRe = (s) => String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const nameRes = (names) => names.map(n => new RegExp(`(^|[^\\w\\u0080-\\uFFFF\\[\\]/@])@${escapeRe(n)}(?=$|[^\\w\\u0080-\\uFFFF/+=])`, 'i')); + const myNamesOf = (messages, feedId) => { + let latest = null; + for (const m of messages) { + const v = m && m.value; + const c = v && v.content; + if (!c || c.type !== 'about' || String(v.author) !== String(feedId) || c.about !== feedId || typeof c.name !== 'string' || !c.name.trim()) continue; + const ts = Number(v.timestamp || m.timestamp || 0); + if (!latest || ts >= latest.ts) latest = { ts, name: c.name.trim() }; + } + return latest ? [latest.name] : []; + }; + const mentionsText = (content, feedId, byName = []) => { + const bare = feedId.startsWith('@') ? feedId.slice(1) : feedId; + for (const field of TEXT_FIELDS) { + const v = content[field]; + if (typeof v === 'string' && (v.includes(feedId) || v.includes(bare) || byName.some(re => re.test(v)))) return true; + } + if (Array.isArray(content.tags)) { + if (content.tags.some(t => typeof t === 'string' && (t.includes(feedId) || t.includes(bare)))) return true; + } + return false; + }; + + const snippet = (content) => { + for (const field of TEXT_FIELDS) { + const v = content[field]; + if (typeof v === 'string' && v.trim()) return v.trim(); + } + return ''; + }; + + const scanCache = { fingerprint: '', fresh: null }; + + const titleOf = (content) => { + for (const field of ['title', 'concept', 'question', 'subject']) { + const v = content[field]; + if (typeof v === 'string' && v.trim()) return v.trim(); + } + return ''; + }; + + return { + async listMentions(filter = 'ALL', opts = {}) { + const ssbClient = await openSsb(); + const myFeedId = ssbClient.id; + const messages = await getAllMessages(ssbClient); + const last = messages.length ? messages[messages.length - 1] : null; + const fingerprint = `${myFeedId}|${messages.length}|${last && last.key}`; + const fresh = (scanCache.fingerprint === fingerprint && scanCache.fresh) + ? scanCache.fresh + : (() => { + const tomb = buildValidatedTombstoneSet(messages); + const vg = versionGroups(messages.filter(m => { const c = m && m.value && m.value.content; return c && typeof c === 'object' && c.type && !SKIP_TYPES.has(c.type); }).map(versionEntry)); + + const out = []; + const seen = new Set(); + const byName = nameRes(myNamesOf(messages, myFeedId)); + for (const m of messages) { + const v = m && m.value; + const c = v && v.content; + if (!c || typeof c !== 'object' || !c.type) continue; + if (tomb.has(m.key)) continue; + if (SKIP_TYPES.has(c.type)) continue; + if (c.encryptedPayload || c.encryptedQuestion) continue; + if (Array.isArray(c.recps) || Array.isArray(v.content && v.content.recps)) continue; + if (c.tribeId && c.type !== 'tribe') continue; + if (String(v.author) === String(myFeedId)) continue; + if (vg.tipOf(m.key) !== m.key || !vg.roots.has(vg.rootOf(m.key))) continue; + if (!mentionsList(c, myFeedId) && !mentionsText(c, myFeedId, byName)) continue; + if (seen.has(m.key)) continue; + seen.add(m.key); + out.push({ + id: m.key, + key: m.key, + author: v.author, + type: c.type, + title: titleOf(c), + text: snippet(c), + content: c, + createdAt: new Date(v.timestamp || m.timestamp || 0).toISOString(), + ts: v.timestamp || m.timestamp || 0 + }); + } + + scanCache.fingerprint = fingerprint; + scanCache.fresh = out; + return out; + })(); + const q = String(opts.q || '').trim().toLowerCase(); + let list = q + ? fresh.filter(x => x.text.toLowerCase().includes(q) || x.title.toLowerCase().includes(q) || x.type.toLowerCase().includes(q)) + : fresh; + + const f = String(filter || 'ALL').toUpperCase(); + if (f !== 'ALL' && f !== 'RECENT') list = list.filter(x => x.type === filter); + + list.sort((a, b) => b.ts - a.ts); + return list; + }, + + seenAt: () => readSeen().ts, + + readKeys: () => new Set(readSeen().keys.map(String)), + + markRead(key) { + if (!key) return false; + const seen = readSeen(); + const keys = [String(key)].concat(seen.keys.filter(k => String(k) !== String(key))); + return writeSeen(keys, seen.ts || Date.now()); + }, + + markReadMany(list) { + const seen = readSeen(); + const add = (Array.isArray(list) ? list : []).filter(Boolean).map(String); + const rest = seen.keys.filter(k => !add.includes(String(k))); + return writeSeen(add.concat(rest), seen.ts || Date.now()); + }, + + markUnread(key) { + if (!key) return false; + const seen = readSeen(); + return writeSeen(seen.keys.filter(k => String(k) !== String(key)), seen.ts); + }, + + markSeen(list) { + const items = Array.isArray(list) ? list : []; + const keys = items.map(x => x && x.key).filter(Boolean); + const newest = items.reduce((max, x) => Math.max(max, Number(x && x.ts) || 0), 0); + return writeSeen(keys, newest || Date.now()); + }, + + unseenOf(list) { + const seen = readSeen(); + const known = new Set(seen.keys); + const items = Array.isArray(list) ? list : []; + if (known.size) return items.filter(x => x && !known.has(x.key)); + if (seen.ts) return items.filter(x => x && (Number(x.ts) || 0) > seen.ts); + return items; + }, + + async countUnseen() { + return this.unseenOf(await this.listMentions('ALL')).length; + }, + + async countTypes(list) { + const counts = {}; + for (const item of (Array.isArray(list) ? list : [])) { + counts[item.type] = (counts[item.type] || 0) + 1; + } + return counts; + } + }; +}; diff --git a/src/models/nextcloud_model.js b/src/models/nextcloud_model.js new file mode 100644 index 0000000..38a7b97 --- /dev/null +++ b/src/models/nextcloud_model.js @@ -0,0 +1,151 @@ +const fs = require('fs'); +const path = require('path'); +const { statePath } = require('../configs/state-manager'); + +const ACCOUNT_PATH = statePath('nextcloud-account.json'); +const CHUNK_BYTES = 10 * 1024 * 1024; +const REQUEST_TIMEOUT_MS = 15000; +const CHUNK_TIMEOUT_MS = 10 * 60 * 1000; +const DEFAULT_FOLDER = 'Oasis'; + +const readAccount = () => { + try { + const a = JSON.parse(fs.readFileSync(ACCOUNT_PATH, 'utf8')); + return a && a.server && a.user && a.password ? a : null; + } catch (_) { return null; } +}; +const writeAccount = (a) => { + fs.mkdirSync(path.dirname(ACCOUNT_PATH), { recursive: true }); + fs.writeFileSync(ACCOUNT_PATH, JSON.stringify(a), { mode: 0o600 }); +}; +const normalizeServer = (v) => { + let s = String(v || '').trim(); + if (!s) return ''; + if (!/^https?:\/\//i.test(s)) s = `https://${s}`; + try { + const u = new URL(s); + if (!/^https?:$/.test(u.protocol)) return ''; + return `${u.origin}${u.pathname.replace(/\/+$/, '')}`; + } catch (_) { return ''; } +}; +const cleanFolder = (v) => String(v || '').trim().replace(/^\/+|\/+$/g, '').split('/').map(s => s.trim()).filter(Boolean).join('/') || DEFAULT_FOLDER; +const encPath = (p) => p.split('/').map(encodeURIComponent).join('/'); +const safeName = (v) => String(v || '').replace(/[\\/:*?"<>|\r\n]+/g, ' ').trim().slice(0, 120) || 'file'; +const pad = (n) => String(n).padStart(15, '0'); +const readRange = async (file, start, end) => { + const fh = await fs.promises.open(file, 'r'); + try { + const buf = Buffer.alloc(end - start); + let got = 0; + while (got < buf.length) { + const { bytesRead } = await fh.read(buf, got, buf.length - got, start + got); + if (!bytesRead) break; + got += bytesRead; + } + return got === buf.length ? buf : buf.subarray(0, got); + } finally { await fh.close(); } +}; + +module.exports = ({ isPublic = false } = {}) => { + const auth = (a) => 'Basic ' + Buffer.from(`${a.user}:${a.password}`).toString('base64'); + const call = async (a, method, url, { headers = {}, body, timeout = REQUEST_TIMEOUT_MS } = {}) => { + const ctl = new AbortController(); + const t = setTimeout(() => ctl.abort(), timeout); + try { + return await fetch(url, { method, headers: { Authorization: auth(a), 'OCS-APIRequest': 'true', ...headers }, body, signal: ctl.signal, redirect: 'manual' }); + } finally { clearTimeout(t); } + }; + const davFiles = (a) => `${a.server}/remote.php/dav/files/${encodeURIComponent(a.user)}`; + const davUploads = (a) => `${a.server}/remote.php/dav/uploads/${encodeURIComponent(a.user)}`; + const targetUrl = (a, name) => `${davFiles(a)}/${encPath(a.folder)}/${encodeURIComponent(name)}`; + const whoami = async (a) => { + let res; + try { res = await call(a, 'GET', `${a.server}/ocs/v1.php/cloud/user?format=json`); } catch (_) { throw new Error('nextcloudErrConnect'); } + if (!res.ok) throw new Error('nextcloudErrConnect'); + let data = null; + try { data = await res.json(); } catch (_) {} + const d = data && data.ocs && data.ocs.data; + if (!d || !d.id) throw new Error('nextcloudErrConnect'); + return { id: String(d.id), displayName: String(d['display-name'] || d.displayname || d.id) }; + }; + const mkcol = async (a, url) => { + const res = await call(a, 'MKCOL', url); + if (!(res.ok || res.status === 405)) throw new Error('nextcloudErrUpload'); + }; + const ensureFolder = async (a) => { + let cur = davFiles(a); + for (const seg of a.folder.split('/')) { cur += `/${encodeURIComponent(seg)}`; await mkcol(a, cur); } + }; + const exists = async (a, url) => { + const res = await call(a, 'PROPFIND', url, { headers: { Depth: '0' } }); + return res.status === 207 || res.status === 200; + }; + const freeName = async (a, name) => { + if (!(await exists(a, targetUrl(a, name)))) return name; + const dot = name.lastIndexOf('.'); + const stamp = Date.now().toString(36); + return dot > 0 ? `${name.slice(0, dot)}-${stamp}${name.slice(dot)}` : `${name}-${stamp}`; + }; + const putWhole = async (a, filePath, size, name) => { + const res = await call(a, 'PUT', targetUrl(a, name), { body: await readRange(filePath, 0, size), headers: { 'Content-Type': 'application/octet-stream' }, timeout: CHUNK_TIMEOUT_MS }); + if (!res.ok) throw new Error('nextcloudErrUpload'); + }; + const putChunked = async (a, filePath, size, name) => { + const base = `${davUploads(a)}/oasis-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`; + await mkcol(a, base); + for (let start = 0; start < size; start += CHUNK_BYTES) { + const end = Math.min(size, start + CHUNK_BYTES); + const res = await call(a, 'PUT', `${base}/${pad(start)}-${pad(end - 1)}`, { body: await readRange(filePath, start, end), headers: { 'Content-Type': 'application/octet-stream' }, timeout: CHUNK_TIMEOUT_MS }); + if (!res.ok) throw new Error('nextcloudErrUpload'); + } + const res = await call(a, 'MOVE', `${base}/.file`, { headers: { Destination: targetUrl(a, name), 'OC-Total-Length': String(size) }, timeout: CHUNK_TIMEOUT_MS }); + if (!res.ok) throw new Error('nextcloudErrUpload'); + }; + const share = async (a, name) => { + const body = new URLSearchParams({ path: `/${a.folder}/${name}`, shareType: '3' }); + const res = await call(a, 'POST', `${a.server}/ocs/v2.php/apps/files_sharing/api/v1/shares?format=json`, { headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: body.toString() }); + if (!res.ok) throw new Error('nextcloudErrShare'); + let data = null; + try { data = await res.json(); } catch (_) {} + const url = data && data.ocs && data.ocs.data && data.ocs.data.url; + if (!url || !/^https?:\/\//i.test(String(url))) throw new Error('nextcloudErrShare'); + return String(url); + }; + + return { + getAccount() { + const a = readAccount(); + return a ? { server: a.server, user: a.user, folder: a.folder, displayName: a.displayName || a.user } : null; + }, + + async connect({ server, user, password, folder } = {}) { + if (isPublic) throw new Error('nextcloudErrConnect'); + const a = { server: normalizeServer(server), user: String(user || '').trim(), password: String(password || ''), folder: cleanFolder(folder) }; + if (!a.server || !a.user || !a.password) throw new Error('nextcloudErrConnect'); + const me = await whoami(a); + a.displayName = me.displayName; + await ensureFolder(a); + writeAccount(a); + return this.getAccount(); + }, + + disconnect() { + try { fs.unlinkSync(ACCOUNT_PATH); } catch (_) {} + }, + + async upload({ filePath, size, name } = {}) { + if (isPublic) throw new Error('nextcloudErrUpload'); + const a = readAccount(); + if (!a) throw new Error('nextcloudErrConnect'); + const bytes = Number(size) || 0; + if (!filePath || !(bytes > 0)) throw new Error('nextcloudErrUpload'); + const final = await freeName(a, safeName(name)); + if (bytes <= CHUNK_BYTES) await putWhole(a, filePath, bytes, final); + else await putChunked(a, filePath, bytes, final); + const url = await share(a, final); + return { url, name: final, size: bytes }; + } + }; +}; + +module.exports.CHUNK_BYTES = CHUNK_BYTES; diff --git a/src/models/onboarding_model.js b/src/models/onboarding_model.js new file mode 100644 index 0000000..eac7b74 --- /dev/null +++ b/src/models/onboarding_model.js @@ -0,0 +1,218 @@ +const fs = require('fs'); +const path = require('path'); +const pull = require('../server/node_modules/pull-stream'); + +const STEPS = ['profile', 'ux', 'workflow', 'theme', 'community', 'greeting']; +const MARKED_STEPS = ['profile', 'ux', 'workflow', 'theme', 'community']; +const FLAG = 'oasis-first-contact'; +const PENDING = 'welcome=pending'; +const DISMISSED = 'welcome=dismissed'; +const DONE = 'welcome=done'; + +const ssbPathOf = (given) => { + if (given) return given; + try { return require('../server/ssb_config').path; } catch (_) { return null; } +}; + +const flagPath = (dir) => { + try { + const state = require('../configs/state-manager'); + if (!dir || path.resolve(dir) === state.ssbDir()) return state.statePath(FLAG); + } catch (_) {} + return path.join(dir, FLAG); +}; + +function readFlag(dir) { + if (!dir) return { exists: false, id: '', markers: new Set() }; + let raw = ''; + try { raw = fs.readFileSync(flagPath(dir), 'utf8'); } catch (_) { return { exists: false, id: '', markers: new Set() }; } + const lines = raw.split('\n').map(l => l.trim()).filter(Boolean); + const isMarker = (l) => /^(welcome|step)=/.test(l); + const markers = new Set(lines.filter(isMarker)); + return { exists: true, id: lines.find(l => !isMarker(l)) || '', markers }; +} + +function appendMarker(dir, marker, createIfMissing) { + if (!dir) return false; + const flag = readFlag(dir); + if (flag.markers.has(marker)) return true; + if (!flag.exists && !createIfMissing) return false; + try { + fs.mkdirSync(dir, { recursive: true }); + fs.appendFileSync(flagPath(dir), `${marker}\n`); + return true; + } catch (_) { + return false; + } +} + +const isPending = (dir) => { + if (!dir) return false; + const flag = readFlag(dir); + if (!flag.exists) return true; + if (flag.markers.has(DISMISSED) || flag.markers.has(DONE)) return false; + return flag.markers.has(PENDING); +}; + +const bannerVisible = (ssbPath) => isPending(ssbPathOf(ssbPath)); + +const DEFAULT_LANGUAGE = 'en'; + +const browserLanguage = (header, supported) => { + const list = Array.isArray(supported) ? supported : []; + const wanted = String(header || '').slice(0, 1024).split(',').map((part, i) => { + const [tag, ...params] = part.trim().split(';'); + const qParam = params.map(p => p.trim()).find(p => /^q=/i.test(p)); + const q = qParam ? Number(qParam.slice(2)) : 1; + return { tag: String(tag || '').trim().toLowerCase().replace(/_/g, '-'), q: Number.isFinite(q) ? q : 0, i }; + }).filter(w => w.tag && w.tag !== '*' && w.q > 0).sort((a, b) => b.q - a.q || a.i - b.i); + for (const w of wanted) { + if (list.includes(w.tag)) return w.tag; + const base = w.tag.split('-')[0]; + if (list.includes(base)) return base; + } + return ''; +}; + +const firstRunLanguage = ({ fresh, isPublic, configured, cookie, header, supported } = {}) => { + const list = Array.isArray(supported) ? supported : []; + if (!fresh || isPublic) return { settled: true, language: '' }; + if (list.includes(configured) && configured !== DEFAULT_LANGUAGE) return { settled: true, language: '' }; + const picked = (list.includes(cookie) ? cookie : '') || browserLanguage(header, list); + if (!picked && !String(header || '').trim()) return { settled: false, language: '' }; + return { settled: true, language: picked }; +}; + +module.exports = ({ cooler, ssbPath } = {}) => { + const dir = () => ssbPathOf(ssbPath); + + const ownMessages = async () => { + const ssb = await cooler.open(); + const mine = ssb.id; + const all = await new Promise((resolve, reject) => + pull(ssb.createUserStream({ id: mine, reverse: true, limit: 2000 }), pull.collect((err, msgs) => err ? reject(err) : resolve(msgs || []))) + ); + return { mine, messages: all.filter(m => m && m.value && m.value.author === mine) }; + }; + + const hasProfile = (mine, messages) => messages.some(m => { + const c = m.value.content; + if (!c || typeof c !== 'object' || c.type !== 'about' || c.about !== mine) return false; + return String(c.name || '').trim().length > 0 || String(c.image || '').trim().length > 0; + }); + + const hasGreeted = (messages) => messages.some(m => { + const c = m.value.content; + return c && typeof c === 'object' && c.type === 'feed' && String(c.text || '').trim().length > 0; + }); + + const hasFederated = (mine, messages) => { + try { + const gossip = JSON.parse(fs.readFileSync(path.join(dir(), 'gossip.json'), 'utf8')); + if (Array.isArray(gossip) && gossip.length > 0) return true; + } catch (_) {} + return messages.some(m => { + const c = m.value.content; + return c && typeof c === 'object' && c.type === 'contact' && c.following === true && c.contact && c.contact !== mine; + }); + }; + + return { + STEPS, + + flagPath() { + return flagPath(dir()); + }, + + firstContactSeen(feedId) { + const flag = readFlag(dir()); + return flag.exists && (!feedId || flag.id === String(feedId)); + }, + + begin(feedId) { + const target = dir(); + if (!target) return false; + const flag = readFlag(target); + const closed = flag.markers.has(DISMISSED) || flag.markers.has(DONE); + const markers = closed ? [...flag.markers] : [...new Set([PENDING, ...flag.markers])]; + try { + fs.mkdirSync(target, { recursive: true }); + fs.writeFileSync(flagPath(target), `${String(feedId || '')}\n${new Date().toISOString()}\n${markers.join('\n')}\n`); + return true; + } catch (_) { + return false; + } + }, + + adopt(feedId) { + const target = dir(); + if (!target || !feedId) return false; + try { + fs.mkdirSync(target, { recursive: true }); + fs.writeFileSync(flagPath(target), `${String(feedId)}\n${new Date().toISOString()}\n${DONE}\n`); + return true; + } catch (_) { + return false; + } + }, + + markStep(step) { + if (!MARKED_STEPS.includes(step)) return false; + if (!isPending(dir())) return true; + return appendMarker(dir(), `step=${step}`); + }, + + dismiss() { + return appendMarker(dir(), DISMISSED, true); + }, + + isVisible() { + return isPending(dir()); + }, + + async status(available) { + const target = dir(); + const flag = readFlag(target); + const usable = STEPS.filter(s => !available || available[s] !== false); + let mine = ''; + let messages = []; + try { ({ mine, messages } = await ownMessages()); } catch (_) { mine = ''; messages = []; } + const steps = { + profile: hasProfile(mine, messages) || flag.markers.has('step=profile'), + ux: flag.markers.has('step=ux'), + workflow: flag.markers.has('step=workflow'), + theme: flag.markers.has('step=theme'), + community: hasFederated(mine, messages) || flag.markers.has('step=community'), + greeting: hasGreeted(messages) + }; + const profile = { id: mine, name: '', description: '', image: '' }; + for (const m of messages) { + const c = m.value.content; + if (!c || typeof c !== 'object' || c.type !== 'about' || c.about !== mine) continue; + if (!profile.name && String(c.name || '').trim()) profile.name = String(c.name).trim(); + if (!profile.description && String(c.description || '').trim()) profile.description = String(c.description).trim(); + if (!profile.image && String(c.image || '').trim()) profile.image = String(c.image).trim(); + if (profile.name && profile.description && profile.image) break; + } + const done = usable.filter(s => steps[s]).length; + const complete = done === usable.length; + if (complete && isPending(target)) appendMarker(target, DONE); + return { + id: flag.id, + pending: isPending(target), + dismissed: flag.markers.has(DISMISSED), + steps, + profile, + usable, + done, + total: usable.length, + complete + }; + } + }; +}; + +module.exports.bannerVisible = bannerVisible; +module.exports.STEPS = STEPS; +module.exports.browserLanguage = browserLanguage; +module.exports.firstRunLanguage = firstRunLanguage; diff --git a/src/models/opinions_model.js b/src/models/opinions_model.js index 4ffb636..90e7236 100644 --- a/src/models/opinions_model.js +++ b/src/models/opinions_model.js @@ -2,7 +2,15 @@ const pull = require('../server/node_modules/pull-stream'); const { getConfig } = require('../configs/config-manager.js'); const categories = require('../backend/opinion_categories'); const { buildValidatedTombstoneSet } = require('./tombstone_validator'); +const { readTyped, CONTENT_TYPES, requestScope } = require('./typed_log'); +const longText = require('../backend/long_text'); +const isRootBlogPost = (c) => + !!c && c.type === 'post' && typeof c.text === 'string' && + !c.root && !c.fork && !c.branch && !c.about && !c.target && + c.private !== true && !Array.isArray(c.recps); + const { buildVoteTally } = require('../backend/vote_tally'); +const { versionGroups, versionEntry } = require('../backend/dedupe'); const logLimit = getConfig().ssbLogStream?.limit || 1000; module.exports = ({ cooler }) => { @@ -22,25 +30,18 @@ module.exports = ({ cooler }) => { const validTypes = [ 'bookmark', 'votes', 'transfer', - 'feed', 'image', 'audio', 'video', 'document', 'torrent' + 'feed', 'post', 'image', 'audio', 'video', 'document', 'torrent', 'file', + 'industry', 'project', 'report', 'task', 'event', 'shopProduct', 'housing', 'market', 'schoolCourse', + 'podcast', 'podcastEpisode', 'campaign', 'logisticsRoute' ]; const getPreview = c => { if (c.type === 'bookmark' && c.bookmark) return `🔖 ${c.bookmark}`; - return c.text || c.description || c.title || ''; + if (c.type === 'post') return c.contentWarning || c.text || ''; + return c.text || c.description || c.title || c.name || ''; }; - const OPINION_MSG_TYPE = { - bookmark: 'bookmarkOpinion', - votes: 'votesOpinion', - transfer: 'transferOpinion', - feed: 'feedOpinion', - image: 'imageOpinion', - audio: 'audioOpinion', - video: 'videoOpinion', - document: 'documentOpinion', - torrent: 'torrentOpinion' - }; + const OPINION_MSG_TYPE = categories.messageTypes; const createVote = async (contentId, category) => { const ssbClient = await openSsb(); @@ -60,17 +61,10 @@ module.exports = ({ cooler }) => { ); }; - const listOpinions = async (filter = 'ALL', category = '') => { + const computeBase = async () => { const ssbClient = await openSsb(); - const userId = ssbClient.id; - const messages = await new Promise((res, rej) => { - pull( - ssbClient.createLogStream({ limit: logLimit }), - pull.collect((err, msgs) => err ? rej(err) : res(msgs)) - ); - }); + const messages = await readTyped(ssbClient, CONTENT_TYPES, { limit: logLimit, withWindow: true }); const tombstoned = buildValidatedTombstoneSet(messages); - const replaces = new Map(); const byId = new Map(); const opinionMsgs = []; @@ -78,16 +72,12 @@ module.exports = ({ cooler }) => { const key = msg.key; const c = msg.value?.content; if (!c) continue; - if (c.type === 'tombstone') { - if (tombstoned.has(c.target)) byId.delete(c.target); - continue; - } + if (c.type === 'tombstone') continue; if (typeof c.type === 'string' && c.type.endsWith('Opinion') && c.target) { opinionMsgs.push({ target: c.target, author: msg.value.author, category: c.category }); continue; } - if (c.opinions && !tombstoned.has(key) && !['task', 'event', 'report'].includes(c.type)) { - if (c.replaces) replaces.set(c.replaces, key); + if (c.opinions && !['task', 'event', 'report'].includes(c.type)) { byId.set(key, { key, value: { @@ -97,38 +87,31 @@ module.exports = ({ cooler }) => { } }); } - if (c.type === 'feed' && !tombstoned.has(key) && !byId.has(key)) { - if (c.replaces) replaces.set(c.replaces, key); + if (c.type === 'feed' && !byId.has(key)) { + byId.set(key, { key, value: { ...msg.value, content: c, preview: getPreview(c) } }); + } + if (isRootBlogPost(c) && !byId.has(key)) { byId.set(key, { key, value: { ...msg.value, content: c, preview: getPreview(c) } }); } } - for (const [oldId, newId] of Array.from(replaces.entries())) { - const oldM = byId.get(oldId); - const newM = byId.get(newId); - if (!oldM) { replaces.delete(oldId); continue; } - if (!newM || String(newM.value.author) !== String(oldM.value.author)) { - replaces.delete(oldId); - byId.delete(newId); + const chunkLookup = longText.lookupIn(longText.indexChunks(messages)); + for (const [key, m] of Array.from(byId.entries())) { + const c = m.value?.content; + if (c?.type === 'post' && longText.hasChunks(c)) { + const content = longText.resolveField(c, 'text', m.value.author, chunkLookup); + byId.set(key, { key, value: { ...m.value, content, preview: getPreview(content) } }); } } - for (const replacedId of replaces.keys()) { - byId.delete(replacedId); + const vg = versionGroups(Array.from(byId.values()).map(versionEntry)); + for (const key of Array.from(byId.keys())) { + if (!vg.roots.has(vg.rootOf(key)) || vg.tipOf(key) !== key || tombstoned.has(key)) byId.delete(key); } - const tipToChain = new Map(); - for (const [oldId, newId] of replaces.entries()) { - let tip = newId; let g = 0; - while (replaces.has(tip) && g++ < 100000) tip = replaces.get(tip); - if (!tipToChain.has(tip)) tipToChain.set(tip, new Set([tip])); - tipToChain.get(tip).add(oldId); tipToChain.get(tip).add(newId); - } - const idToTip = new Map(); - for (const [tip, chain] of tipToChain.entries()) for (const id of chain) idToTip.set(id, tip); const opinionByTip = new Map(); for (const op of opinionMsgs) { - const tip = idToTip.get(op.target) || op.target; + const tip = vg.tipOf(op.target); if (!opinionByTip.has(tip)) opinionByTip.set(tip, { opinions: {}, voters: new Set() }); const agg = opinionByTip.get(tip); if (agg.voters.has(op.author)) continue; @@ -147,6 +130,12 @@ module.exports = ({ cooler }) => { } let filtered = Array.from(byId.values()).filter(m => validTypes.includes(m.value?.content?.type)); + filtered = filtered.filter(m => { + const c = m.value?.content; + if (c?.type !== 'schoolCourse') return true; + if (c.encryptedPayload) return false; + return String(c.visibility || 'PUBLIC').toUpperCase() !== 'INVITE'; + }); const blobTypes = ['document', 'image', 'audio', 'video']; const blobCheckCache = new Map(); @@ -226,6 +215,25 @@ module.exports = ({ cooler }) => { } filtered = Array.from(bySig.values()); + return filtered.filter(m => { + const ops = m.value?.content?.opinions || {}; + return Object.values(ops).reduce((acc, x) => acc + (Number(x) || 0), 0) > 0; + }); + }; + + const baseItems = () => { + const store = requestScope.getStore(); + if (store && store.opinionsBase) return store.opinionsBase; + const pending = computeBase(); + if (store) store.opinionsBase = pending; + return pending; + }; + + const listOpinions = async (filter = 'ALL', category = '') => { + const ssbClient = await openSsb(); + const userId = ssbClient.id; + let filtered = (await baseItems()).slice(); + if (filter === 'MINE') { filtered = filtered.filter(m => m.value.author === userId); } else if (filter === 'RECENT') { diff --git a/src/models/pads_model.js b/src/models/pads_model.js index 6390e46..3a9c310 100644 --- a/src/models/pads_model.js +++ b/src/models/pads_model.js @@ -3,6 +3,7 @@ const crypto = require("crypto") const fs = require("fs") const { buildValidatedTombstoneSet } = require('./tombstone_validator') const { collabContent, openInviteOf } = require('../backend/collab_content') +const { readTyped, memoIndex } = require('./typed_log') const padCollab = collabContent({ membersField: 'members', undecField: 'undecryptable', contentFields: ['title', 'deadline', 'status'], listFields: ['tags', 'invites'] }) const path = require("path") const { getConfig } = require("../configs/config-manager.js") @@ -59,6 +60,18 @@ module.exports = ({ cooler, cipherModel, tribeCrypto, padCrypto, tribesModel }) if (ownCrypto) return lookupKey(rootId) try { return JSON.parse(fs.readFileSync(getLegacyKeyringPath(), "utf8"))[rootId] || null } catch (_) { return null } } + const hashCode = (code, salt) => (ownCrypto && typeof ownCrypto.hashInviteCode === "function") ? ownCrypto.hashInviteCode(code, salt) : (tribeCrypto ? tribeCrypto.hashInviteCode(code, salt) : null) + const inviteMatches = (inv, token) => { + if (!token) return false + if (typeof inv === "string") return inv === token + if (!inv || typeof inv !== "object") return false + if (typeof inv.code === "string" && inv.code === token) return true + if (typeof inv.ch === "string" && (inv.ch === token || hashCode(token, inv.salt) === inv.ch)) return true + return false + } + const inviteToken = (inv) => (inv && typeof inv === "object" && typeof inv.ch === "string") ? inv.ch : (typeof inv === "string" ? inv : (inv && inv.code) || "") + const keyProofFor = (keyHex, id) => { try { return crypto.createHmac("sha256", Buffer.from(keyHex, "hex")).update(String(id), "utf8").digest("hex") } catch (_) { return null } } + const padKeys = (rootId) => (ownCrypto && typeof ownCrypto.getKeys === "function" ? ownCrypto.getKeys(rootId) : []).concat(tribeCrypto && typeof tribeCrypto.getKeys === "function" ? tribeCrypto.getKeys(rootId) : []) const setPadKey = (rootId, keyHex) => { migrateLegacyKeyring() if (ownCrypto) { ownCrypto.setKey(rootId, keyHex, 1); return } @@ -117,29 +130,37 @@ module.exports = ({ cooler, cipherModel, tribeCrypto, padCrypto, tribesModel }) let tagsRaw = "" try { deadline = c.deadline ? tryDecryptField(c.deadline, k) : "" } catch (_) {} try { tagsRaw = c.tags ? tryDecryptField(c.tags, k) : "" } catch (_) {} - const ensureMemberKeys = async (ssbClient, messages, items) => { - if (!tribeCrypto) return - const distributed = new Map() + return { title: safeText(title), deadline, tags: normalizeTags(tagsRaw) } + } catch (_) {} + } + return null + } + + const ensureMemberKeys = async (ssbClient, messages, items) => { + if (!tribeCrypto || !ownCrypto) return + const leavesByRoot = new Map() for (const m of messages) { const c = m.value && m.value.content - if (!c || c.type !== "tribe-keys" || !c.tribeId) continue - const mk = c.memberKeys - if (!mk || typeof mk !== "object") continue - if (!distributed.has(c.tribeId)) distributed.set(c.tribeId, new Set()) - for (const id of Object.keys(mk)) distributed.get(c.tribeId).add(id) + if (!c || c.type !== "padMember" || c.on !== false || !c.target || !c.member) continue + if (!leavesByRoot.has(c.target)) leavesByRoot.set(c.target, []) + leavesByRoot.get(c.target).push({ member: c.member, author: m.value.author, ts: Number(m.timestamp || m.value.timestamp || 0) }) } const ssbKeys = require("../server/node_modules/ssb-keys") for (const item of (Array.isArray(items) ? items : [])) { - if (!item || item.undecryptable) continue + if (!item || item.undecryptable || item.author !== ssbClient.id) continue const rootId = item.rootId - if (!rootId) continue + if (!rootId || !lookupKey(rootId)) continue + const members = Array.isArray(item.members) ? item.members : [] + const leaves = (leavesByRoot.get(rootId) || []).filter(l => l.author === l.member || l.author === item.author) + const plan = ownCrypto.keyPlan({ rootId, ownerId: item.author, gen: lookupGen(rootId), members, messages, leaves }) + if (plan.rotate) { + await rotatePadKey(rootId, members) + continue + } + if (!plan.missing.length) continue const key = lookupKey(rootId) - if (!key) continue - const have = distributed.get(rootId) || new Set() - const missing = (Array.isArray(item.members) ? item.members : []).filter(m => m && m !== ssbClient.id && !have.has(m)) - if (!missing.length) continue const memberKeys = {} - for (const m of missing) { + for (const m of plan.missing) { try { memberKeys[m] = tribeCrypto.boxKeyForMember(key, m, ssbKeys) } catch (_) {} } if (!Object.keys(memberKeys).length) continue @@ -149,12 +170,6 @@ module.exports = ({ cooler, cipherModel, tribeCrypto, padCrypto, tribesModel }) } } - return { title: safeText(title), deadline, tags: normalizeTags(tagsRaw) } - } catch (_) {} - } - return null - } - const encryptForInvite = (padKeyHex, code, saltHex) => { const salt = saltHex ? Buffer.from(saltHex, "hex") : Buffer.from(INVITE_SALT) const derived = crypto.scryptSync(code, salt, 32) @@ -189,11 +204,10 @@ module.exports = ({ cooler, cipherModel, tribeCrypto, padCrypto, tribesModel }) if (!existing) return const newKey = crypto.randomBytes(32).toString("hex") const newGen = ownCrypto.addNewKey(rootId, newKey) - if (!Array.isArray(remainingMembers) || !remainingMembers.length) return const ssbClient = await openSsb() const ssbKeys = require("../server/node_modules/ssb-keys") const memberKeys = {} - for (const m of remainingMembers) { + for (const m of new Set([ssbClient.id, ...(Array.isArray(remainingMembers) ? remainingMembers : [])])) { try { memberKeys[m] = tribeCrypto.boxKeyForMember(newKey, m, ssbKeys) } catch (_) {} } if (Object.keys(memberKeys).length) { @@ -210,9 +224,12 @@ module.exports = ({ cooler, cipherModel, tribeCrypto, padCrypto, tribesModel }) const ssbKeys = require("../server/node_modules/ssb-keys") const config = require("../server/ssb_config") const msgs = await readAll(ssbClient) + const idx = buildIndex(msgs) + const trusted = (root, author) => { const n = idx.nodes.get(root); return !!author && !!n && (author === n.author || author === ssbClient.id) } for (const m of msgs) { const c = m.value && m.value.content if (!c || c.type !== "tribe-keys") continue + if (!trusted(c.tribeId, m.value.author)) continue const memberKeys = c.memberKeys if (!memberKeys || typeof memberKeys !== "object") continue const boxed = memberKeys[ssbClient.id] @@ -226,18 +243,15 @@ module.exports = ({ cooler, cipherModel, tribeCrypto, padCrypto, tribesModel }) } catch (_) {} } - const readAll = async (ssbClient) => - new Promise((resolve, reject) => - pull(ssbClient.createLogStream({ limit: logLimit }), pull.collect((err, msgs) => err ? reject(err) : resolve(msgs))) - ) + const PAD_TYPES = ["pad", "padMember", "padEntry", "tribe-keys", "tombstone"] + + const readAll = async (ssbClient) => readTyped(ssbClient, PAD_TYPES, { limit: logLimit }) const buildIndex = (messages) => { const tomb = new Set() const nodes = new Map() const parent = new Map() const child = new Map() - const strictParent = new Map() - const strictChild = new Map() const authorByKey = new Map() const tombRequests = [] const memberMsgs = [] @@ -248,19 +262,14 @@ module.exports = ({ cooler, cipherModel, tribeCrypto, padCrypto, tribesModel }) const c = v.content if (!c) continue if (c.type === "tombstone" && c.target) { tombRequests.push({ target: c.target, author: v.author }); continue } - if (c.type === "padMember" && c.target) { memberMsgs.push({ target: c.target, member: c.member, on: c.on !== false, author: v.author, ts: v.timestamp || m.timestamp || 0, code: typeof c.code === "string" ? c.code : "" }); continue } + if (c.type === "padMember" && c.target) { memberMsgs.push({ target: c.target, member: c.member, on: c.on !== false, author: v.author, ts: v.timestamp || m.timestamp || 0, code: typeof c.code === "string" ? c.code : "", keyProof: typeof c.keyProof === "string" ? c.keyProof : "" }); continue } if (c.type === "pad") { - nodes.set(k, { key: k, ts: v.timestamp || m.timestamp || 0, c, author: v.author }) + nodes.set(k, { key: k, ts: v.timestamp || m.timestamp || 0, seq: v.sequence || 0, c, author: v.author }) authorByKey.set(k, v.author) if (c.replaces) { parent.set(k, c.replaces); child.set(c.replaces, k) } } } - for (const [k, node] of nodes) { - const t = node.c.replaces - if (t) { const orig = nodes.get(t); if (orig && orig.author === node.author) { strictParent.set(k, t); strictChild.set(t, k) } } - } - for (const t of tombRequests) { const targetAuthor = authorByKey.get(t.target) if (targetAuthor && t.author === targetAuthor) tomb.add(t.target) @@ -268,53 +277,86 @@ module.exports = ({ cooler, cipherModel, tribeCrypto, padCrypto, tribesModel }) const rootOf = (id) => { let cur = id, g = 0; while (parent.has(cur) && g++ < 100000) cur = parent.get(cur); return cur } const tipOf = (id) => { let cur = id, g = 0; while (child.has(cur) && g++ < 100000) cur = child.get(cur); return cur } - const strictRootOf = (id) => { let cur = id, g = 0; while (strictParent.has(cur) && g++ < 100000) cur = strictParent.get(cur); return cur } - const contentTipOf = (root) => { - const rn = nodes.get(root) - if (!rn) return root - let cur = root, best = root, g = 0 - const seen = new Set() - while (child.has(cur) && !seen.has(cur) && g++ < 100000) { - seen.add(cur) - const next = child.get(cur) - const n = nodes.get(next) - if (!n) break - if (n.author === rn.author && !tomb.has(next)) best = next - cur = next - } - return best + const rootMemo = new Map() + const groupRootOf = (id) => { + if (rootMemo.has(id)) return rootMemo.get(id) + let cur = id, g = 0 + while (parent.has(cur) && nodes.has(parent.get(cur)) && g++ < 100000) cur = parent.get(cur) + rootMemo.set(id, cur) + return cur } + const ownerTip = new Map() + for (const [k, n] of nodes) { + const r = groupRootOf(k) + const rn = nodes.get(r) + if (!rn || n.author !== rn.author) continue + const prev = ownerTip.has(r) ? nodes.get(ownerTip.get(r)) : null + if (!prev || n.seq > prev.seq || (n.seq === prev.seq && n.ts >= prev.ts)) ownerTip.set(r, k) + } + const contentTipOf = (root) => ownerTip.get(root) || root const roots = new Set() - for (const id of nodes.keys()) roots.add(strictRootOf(id)) + for (const id of nodes.keys()) roots.add(groupRootOf(id)) + + const invitesOf = (root) => { + const out = [] + for (const id of [root, contentTipOf(root)]) { const n = nodes.get(id); if (n && Array.isArray(n.c.invites)) out.push(...n.c.invites) } + return out + } + const selfJoinAllowed = (root, mm) => { + const oc = (nodes.get(root) || {}).c || {} + if (oc.encrypted !== true || oc.tribeId) return true + const invites = invitesOf(root) + if (invites.some(inv => inv && typeof inv === "object" && inv.public === true)) return true + if (mm.keyProof && padKeys(root).some(k => keyProofFor(k, mm.member) === mm.keyProof)) return true + return !!mm.code && invites.some(inv => typeof inv === "string" ? inv === mm.code : !!(inv && typeof inv === "object" && typeof inv.code === "string" && inv.code === mm.code)) + } const memberByRoot = new Map() const consumedByRoot = new Map() + const codeClaimsByRoot = new Map() for (const mm of memberMsgs) { if (!nodes.has(mm.target)) continue - const r = strictRootOf(mm.target) + const r = groupRootOf(mm.target) const ownerAuthor = nodes.get(r) && nodes.get(r).author if (!ownerAuthor) continue const self = mm.member === mm.author const byOwner = mm.author === ownerAuthor if (!self && !byOwner) continue if (!mm.member) continue - if (!memberByRoot.has(r)) memberByRoot.set(r, new Map()) - const m2 = memberByRoot.get(r) - const p = m2.get(mm.member) - if (!p || mm.ts >= p.ts) m2.set(mm.member, { on: mm.on, ts: mm.ts }) + if (mm.on && mm.code) { + if (!codeClaimsByRoot.has(r)) codeClaimsByRoot.set(r, []) + codeClaimsByRoot.get(r).push({ code: mm.code, member: mm.member, keyProof: mm.keyProof, byOwner }) + } + if (self && !byOwner && mm.on && !selfJoinAllowed(r, mm)) continue if (mm.on && mm.code) { if (!consumedByRoot.has(r)) consumedByRoot.set(r, new Set()) consumedByRoot.get(r).add(mm.code) } + if (!memberByRoot.has(r)) memberByRoot.set(r, new Map()) + const m2 = memberByRoot.get(r) + const p = m2.get(mm.member) + if (!p || mm.ts >= p.ts) m2.set(mm.member, { on: mm.on, ts: mm.ts }) } + const codeClaims = (root, codes) => (codeClaimsByRoot.get(root) || []).filter(c => codes.includes(c.code)) + const isCodeConsumed = (root, code) => { if (!code) return false const set = consumedByRoot.get(root) return !!(set && set.has(code)) } + for (const [k, n] of nodes) { + const r = groupRootOf(k) + const rn = nodes.get(r) + if (!rn || n.author === rn.author || !Array.isArray(n.c.members) || !selfJoinAllowed(r, { member: n.author })) continue + if (!memberByRoot.has(r)) memberByRoot.set(r, new Map()) + const m2 = memberByRoot.get(r) + const p = m2.get(n.author) + if (!p || n.ts >= p.ts) m2.set(n.author, { on: n.c.members.includes(n.author), ts: n.ts }) + } + const resolveMembers = (root) => { const ownerNode = nodes.get(root) const oc = ownerNode ? ownerNode.c : {} @@ -326,7 +368,7 @@ module.exports = ({ cooler, cipherModel, tribeCrypto, padCrypto, tribesModel }) const tipByRoot = new Map() for (const r of roots) tipByRoot.set(r, contentTipOf(r)) - return { tomb, nodes, parent, child, rootOf, tipOf, strictRootOf, contentTipOf, tipByRoot, resolveMembers, isCodeConsumed } + return { tomb, nodes, parent, child, rootOf, tipOf, groupRootOf, contentTipOf, tipByRoot, resolveMembers, isCodeConsumed, codeClaims } } const decryptPadFields = (c, rootId, tribeKeys) => { @@ -358,7 +400,7 @@ module.exports = ({ cooler, cipherModel, tribeCrypto, padCrypto, tribesModel }) status: c.status || "OPEN", deadline, tags, - author: c.author || node.author, + author: node.author, members: Array.isArray(members) ? members : (Array.isArray(c.members) ? c.members : []), invites: Array.isArray(c.invites) ? c.invites : [], createdAt: c.createdAt || new Date(node.ts).toISOString(), @@ -423,7 +465,7 @@ module.exports = ({ cooler, cipherModel, tribeCrypto, padCrypto, tribesModel }) const ssbClient = await openSsb() const messages = await readAll(ssbClient) const idx = buildIndex(messages) - const root = idx.strictRootOf(id) + const root = idx.groupRootOf(id) const tip = idx.contentTipOf(root) if (idx.tomb.has(tip)) throw new Error("Not found") return root @@ -433,7 +475,7 @@ module.exports = ({ cooler, cipherModel, tribeCrypto, padCrypto, tribesModel }) const ssbClient = await openSsb() const messages = await readAll(ssbClient) const idx = buildIndex(messages) - const root = idx.strictRootOf(id) + const root = idx.groupRootOf(id) const tip = idx.contentTipOf(root) if (idx.tomb.has(tip)) throw new Error("Not found") return tip @@ -518,7 +560,7 @@ module.exports = ({ cooler, cipherModel, tribeCrypto, padCrypto, tribesModel }) return new Promise(async (resolve, reject) => { ssbClient.get(tipId, async (err, item) => { if (err || !item?.content) return reject(new Error("Pad not found")) - if (item.content.author !== userId) return reject(new Error("Not the author")) + if (item.author !== userId) return reject(new Error("Not the author")) const c = item.content const isEncrypted = c.encrypted === true let keyHex = null @@ -564,7 +606,7 @@ module.exports = ({ cooler, cipherModel, tribeCrypto, padCrypto, tribesModel }) return new Promise(async (resolve, reject) => { ssbClient.get(tipId, async (err, item) => { if (err || !item?.content) return reject(new Error("Pad not found")) - if (item.content.author !== userId) return reject(new Error("Not the author")) + if (item.author !== userId) return reject(new Error("Not the author")) const c = item.content let keyHex = null let usesTribeKey = false @@ -599,13 +641,11 @@ module.exports = ({ cooler, cipherModel, tribeCrypto, padCrypto, tribesModel }) if (!pad) throw new Error("Pad not found") if (pad.author === userId) throw new Error("Author cannot leave their own pad") if (!Array.isArray(pad.members) || !pad.members.includes(userId)) return - const members = pad.members.filter(m => m !== userId) const rootId = await this.resolveRootId(padId) await new Promise((resolve, reject) => { const content = { type: "padMember", target: rootId, member: userId, on: false, createdAt: new Date().toISOString() } ssbClient.publish(content, (e) => e ? reject(e) : resolve()) }) - try { await rotatePadKey(rootId, members) } catch (_) {} }, async ingestKeys() { await ingestOwnTribeKeys() }, @@ -615,17 +655,11 @@ module.exports = ({ cooler, cipherModel, tribeCrypto, padCrypto, tribesModel }) try { const ssbClient = await openSsb() const messages = await readAll(ssbClient) - const live = new Set() const tomb = buildValidatedTombstoneSet(messages) - for (const m of messages) { - const c = m.value && m.value.content - if (!c) continue - if (c.type === "pad") live.add(m.key) - } const all = ownCrypto.getAllRootIds() let removed = 0 for (const rid of all) { - if (!live.has(rid) || tomb.has(rid)) { + if (tomb.has(rid)) { try { ownCrypto.dropKey(rid); removed += 1 } catch (_) {} } } @@ -646,7 +680,8 @@ module.exports = ({ cooler, cipherModel, tribeCrypto, padCrypto, tribesModel }) const key = tryDecryptPublicInviteKey(c.invites) if (key) setPadKey(rootId, key) } - const content = { type: "padMember", target: rootId, member: feedId, on: true, createdAt: new Date().toISOString(), ...(typeof consumedCode === "string" && consumedCode ? { code: consumedCode } : {}) } + const proofKey = feedId === ssbClient.id && c.encrypted === true && !c.tribeId ? getPadKey(rootId) : null + const content = { type: "padMember", target: rootId, member: feedId, on: true, createdAt: new Date().toISOString(), ...(typeof consumedCode === "string" && consumedCode ? { code: consumedCode } : {}), ...(proofKey ? { keyProof: keyProofFor(proofKey, feedId) } : {}) } ssbClient.publish(content, (e, res) => e ? reject(e) : resolve(res)) }) }) @@ -659,7 +694,7 @@ module.exports = ({ cooler, cipherModel, tribeCrypto, padCrypto, tribesModel }) return new Promise((resolve, reject) => { ssbClient.get(tipId, (err, item) => { if (err || !item?.content) return reject(new Error("Pad not found")) - if (item.content.author !== userId) return reject(new Error("Not the author")) + if (item.author !== userId) return reject(new Error("Not the author")) const tombstone = { type: "tombstone", target: tipId, deletedAt: new Date().toISOString(), author: userId } ssbClient.publish(tombstone, (e) => e ? reject(e) : resolve()) }) @@ -669,8 +704,8 @@ module.exports = ({ cooler, cipherModel, tribeCrypto, padCrypto, tribesModel }) async getPadById(id) { const ssbClient = await openSsb() const messages = await readAll(ssbClient) - const idx = buildIndex(messages) - const root = idx.strictRootOf(id) + const idx = memoIndex('pads', messages, buildIndex) + const root = idx.groupRootOf(id) const tip = idx.contentTipOf(root) if (idx.tomb.has(tip)) return null const node = idx.nodes.get(tip) @@ -709,12 +744,13 @@ module.exports = ({ cooler, cipherModel, tribeCrypto, padCrypto, tribesModel }) const code = crypto.randomBytes(INVITE_BYTES).toString("hex") let invite = code const pubFlag = opts.public ? { public: true } : {} + const inviteSalt = generateInviteSalt() if (keyHex) { - const inviteSalt = generateInviteSalt() const ek = encryptForInvite(keyHex, code, inviteSalt) - invite = { code, ek, salt: inviteSalt, ...pubFlag } + invite = opts.public ? { code, ek, salt: inviteSalt, ...pubFlag } : { ch: hashCode(code, inviteSalt), ek, salt: inviteSalt } + } else { + invite = opts.public ? { code, public: true } : { ch: hashCode(code, inviteSalt), salt: inviteSalt } } - if (opts.public && typeof invite !== "object") invite = { code, public: true } const invites = [...pad.invites, invite] await this.updatePadById(padId, { invites }) return code @@ -740,18 +776,18 @@ module.exports = ({ cooler, cipherModel, tribeCrypto, padCrypto, tribesModel }) if (pad.author !== userId) throw new Error("Only the author can remove invites") const invites = (Array.isArray(pad.invites) ? pad.invites : []).filter(inv => !(typeof inv === "object" && inv.public === true)) await this.updatePadById(padId, { invites }) + if (!pad.tribeId) await rotatePadKey(await this.resolveRootId(padId), pad.members) }, async joinByInvite(code) { const ssbClient = await openSsb() const userId = ssbClient.id - const pads = await this.listAll() + const pads = await collectPads(buildIndex(await readAll(ssbClient))) let matchedPad = null let matchedInvite = null for (const p of pads) { for (const inv of p.invites) { - if (typeof inv === "string" && inv === code) { matchedPad = p; matchedInvite = inv; break } - if (typeof inv === "object" && inv.code === code) { matchedPad = p; matchedInvite = inv; break } + if (inviteMatches(inv, code)) { matchedPad = p; matchedInvite = inv; break } } if (matchedPad) break } @@ -759,17 +795,19 @@ module.exports = ({ cooler, cipherModel, tribeCrypto, padCrypto, tribesModel }) if (matchedPad.members.includes(userId)) throw new Error("Already a member") const isPublic = typeof matchedInvite === "object" && matchedInvite.public === true let resolvedRootId = await this.resolveRootId(matchedPad.rootId) + const token = inviteToken(matchedInvite) || code + let padKey = null + if (typeof matchedInvite === "object" && matchedInvite.ek) padKey = decryptFromInvite(matchedInvite.ek, code, matchedInvite.salt) if (!isPublic) { const messages = await readAll(ssbClient) const idx = buildIndex(messages) - if (idx.isCodeConsumed(resolvedRootId, code)) throw new Error("Invite already used") + if (idx.isCodeConsumed(resolvedRootId, token) || idx.isCodeConsumed(resolvedRootId, code)) throw new Error("Invite already used") + const claims = idx.codeClaims(resolvedRootId, [token, code]).filter(c => c.member !== userId) + const spent = padKey ? claims.some(c => c.byOwner || keyProofFor(padKey, c.member) === c.keyProof) : claims.length > 0 + if (spent) throw new Error("Invite already used") } - let padKey = null - if (typeof matchedInvite === "object" && matchedInvite.ek) { - padKey = decryptFromInvite(matchedInvite.ek, code, matchedInvite.salt) - setPadKey(resolvedRootId, padKey) - } - await this.addMemberToPad(matchedPad.rootId, userId, isPublic ? "" : code) + if (padKey) setPadKey(resolvedRootId, padKey) + await this.addMemberToPad(matchedPad.rootId, userId, isPublic ? "" : token) if (tribeCrypto && padKey && resolvedRootId) { try { const ssbKeys = require("../server/node_modules/ssb-keys") @@ -860,7 +898,7 @@ module.exports = ({ cooler, cipherModel, tribeCrypto, padCrypto, tribesModel }) } entries.push({ key: m.key, - author: c.author || v.author, + author: v.author, text, createdAt: c.createdAt || new Date(v.timestamp || 0).toISOString() }) diff --git a/src/models/panicmode_model.js b/src/models/panicmode_model.js index 95933fc..d6edcdc 100644 --- a/src/models/panicmode_model.js +++ b/src/models/panicmode_model.js @@ -1,12 +1,10 @@ -const os = require('os'); const fs = require('fs'); const path = require('path'); module.exports = { removeSSB: async () => { try { - const homeDir = os.homedir(); - const ssbPath = path.join(homeDir, '.ssb'); + const ssbPath = require('../server/ssb_config').path; await fs.promises.rm(ssbPath, { recursive: true, force: true }); } catch (error) { throw new Error("Error deleting data: " + error.message); diff --git a/src/models/parliament_model.js b/src/models/parliament_model.js index 83dc837..ec18860 100644 --- a/src/models/parliament_model.js +++ b/src/models/parliament_model.js @@ -2,8 +2,21 @@ const pull = require('../server/node_modules/pull-stream'); const moment = require('../server/node_modules/moment'); const { getConfig } = require('../configs/config-manager.js'); const { buildValidatedTombstoneSet } = require('./tombstone_validator'); +const { readTyped } = require('./typed_log'); +const { buildVoteResults } = require('../backend/vote_tally'); const logLimit = getConfig().ssbLogStream?.limit || 1000; +const PARLIAMENT_TYPES = [ + 'parliamentCandidature', 'parliamentCandidatureVote', 'parliamentTerm', + 'parliamentProposal', 'parliamentRevocation', 'parliamentLaw', + 'tribe', 'about', 'tombstone' +]; +const VOTE_LOG_TYPES = ['votes', 'votesVote', 'tombstone']; +const PROPOSER_TYPES = new Set(['parliamentCandidature', 'parliamentProposal', 'parliamentRevocation']); +const TRIBE_PARLIAMENT_TYPES = [ + 'tribeParliamentTerm', 'tribeParliamentCandidature', 'tribeParliamentRule', + 'parliamentCandidature', 'tombstone' +]; const TERM_DAYS = 60; const PROPOSAL_DAYS = 7; const REVOCATION_DAYS = 15; @@ -12,6 +25,138 @@ const QUORUM_RATIO = 0.25; const VOTE_METHODS = new Set(['DEMOCRACY', 'ANARCHY', 'MAJORITY', 'MINORITY']); const METHODS = ['DEMOCRACY', 'MAJORITY', 'MINORITY', 'DICTATORSHIP', 'KARMATOCRACY']; const FEED_ID_RE = /^@.+\.ed25519$/; +const TERM_EPOCH = Date.UTC(2020, 0, 1); +const TERM_SPAN_MS = TERM_DAYS * 86400000; + +const termWindowFor = (at = Date.now()) => { + const ms = at instanceof Date ? at.getTime() : Number(at); + const base = Number.isFinite(ms) ? ms : Date.now(); + const index = Math.floor((base - TERM_EPOCH) / TERM_SPAN_MS); + const startMs = TERM_EPOCH + index * TERM_SPAN_MS; + return { + cycle: index, + startAt: new Date(startMs).toISOString(), + endAt: new Date(startMs + TERM_SPAN_MS).toISOString() + }; +}; + +const HEMICYCLE_MAX_SEATS = 400; + +const hemicycleQuorum = (n) => Math.max(PROPOSAL_QUORUM, Math.ceil(Number(n || 0) * QUORUM_RATIO)); + +const passThreshold = (method, n) => { + const m = String(method || '').toUpperCase(); + const total = Number(n || 0); + if (m === 'DEMOCRACY' || m === 'ANARCHY') return Math.floor(total / 2) + 1; + if (m === 'MAJORITY') return Math.ceil(total * 0.8); + if (m === 'MINORITY') return Math.ceil(total * 0.2); + return null; +}; + +const buildHemicycle = ({ census = [], candidatures = [], powerType = 'none', powerId = null, leaderId = null, method = 'ANARCHY', powerMembers = [], houses = {} } = {}) => { + const people = []; + const seen = new Set(); + for (const p of census) { + const id = typeof p === 'string' ? p : (p && p.id); + if (!id || seen.has(id)) continue; + seen.add(id); + people.push({ id, name: (p && typeof p === 'object' && p.name) ? String(p.name) : '' }); + } + const n = people.length; + const m = String(method || 'ANARCHY').toUpperCase(); + const power = new Set((Array.isArray(powerMembers) ? powerMembers : []).map(String)); + const leader = leaderId || (powerType === 'inhabitant' ? powerId : null); + + const cands = (Array.isArray(candidatures) ? candidatures : []).map(c => ({ + id: String(c.id || ''), + label: String(c.label || c.targetTitle || c.targetId || c.candidateId || ''), + href: c.href || null, + targetType: c.targetType || (c.candidateId ? 'inhabitant' : 'inhabitant'), + targetId: String(c.targetId || c.candidateId || ''), + method: String(c.method || '').toUpperCase(), + voters: Array.from(new Set((Array.isArray(c.voters) ? c.voters : []).map(String))).filter(v => v !== String(c.targetId || c.candidateId || '')) + })); + const isGovernment = (c) => powerType !== 'none' && powerId && c.targetId === String(powerId); + const government = cands.find(isGovernment) || null; + const opposition = cands.filter(c => c !== government && c.voters.some(v => seen.has(v))); + opposition.sort((a, b) => b.voters.length - a.voters.length || a.label.localeCompare(b.label)); + + const blocOf = new Map(); + if (leader && seen.has(String(leader)) && powerType !== 'none') blocOf.set(String(leader), 'gov'); + if (powerType === 'tribe') for (const id of power) if (seen.has(id)) blocOf.set(id, 'gov'); + if (government) for (const v of government.voters) if (seen.has(v) && !blocOf.has(v)) blocOf.set(v, 'gov'); + opposition.forEach((c, i) => { for (const v of c.voters) if (seen.has(v) && !blocOf.has(v)) blocOf.set(v, `opp${i}`); }); + + const seats = people.map(p => ({ + id: p.id, + name: p.name, + bloc: blocOf.get(p.id) || 'none', + leader: !!leader && p.id === String(leader), + inPower: power.has(p.id), + house: houses && houses[p.id] ? String(houses[p.id]) : null + })); + const order = { gov: 0 }; + opposition.forEach((c, i) => { order[`opp${i}`] = i + 1; }); + order.none = opposition.length + 1; + seats.sort((a, b) => (order[a.bloc] - order[b.bloc]) || (b.leader - a.leader) || a.id.localeCompare(b.id)); + + const count = (key) => seats.filter(s => s.bloc === key).length; + const blocs = []; + if (government) blocs.push({ key: 'gov', kind: 'government', label: government.label, href: government.href, targetId: government.targetId, method: government.method, count: count('gov') }); + opposition.forEach((c, i) => blocs.push({ key: `opp${i}`, kind: 'opposition', label: c.label, href: c.href, targetId: c.targetId, method: c.method, count: count(`opp${i}`) })); + blocs.push({ key: 'none', kind: 'abstention', label: '', href: null, count: count('none') }); + + const houseCounts = {}; + for (const s of seats) if (s.house) houseCounts[s.house] = (houseCounts[s.house] || 0) + 1; + + return { + population: n, + method: m, + powerType, + powerId: powerId || null, + leaderId: leader || null, + seats, + blocs: blocs.filter(b => b.count > 0 || b.kind === 'government'), + houses: houseCounts, + thresholds: { quorum: hemicycleQuorum(n), pass: passThreshold(m, n) }, + seatUnit: n > HEMICYCLE_MAX_SEATS ? Math.ceil(n / HEMICYCLE_MAX_SEATS) : 1 + }; +}; + +const seatPositions = (n, { width = 720, height = 380 } = {}) => { + if (n <= 0) return []; + const cx = width / 2; + const cy = height - 20; + const outer = Math.min(width / 2 - 16, height - 60); + let rows = 1; + const rowGap = () => outer / (rows + 1.5); + const capacity = (r) => { + let total = 0; + for (let i = 0; i < r; i++) { const radius = outer - i * rowGap(); total += Math.max(1, Math.floor((Math.PI * radius) / (rowGap() * 0.95))); } + return total; + }; + while (capacity(rows) < n && rows < 30) rows += 1; + const gap = rowGap(); + const radii = []; + for (let i = 0; i < rows; i++) radii.push(outer - i * gap); + const per = radii.map(r => Math.max(1, Math.floor((Math.PI * r) / (gap * 0.95)))); + const totalCap = per.reduce((a, b) => a + b, 0); + const alloc = per.map(c => Math.floor((c / totalCap) * n)); + let left = n - alloc.reduce((a, b) => a + b, 0); + for (let i = 0; left > 0; i = (i + 1) % rows) { if (alloc[i] < per[i]) { alloc[i] += 1; left -= 1; } } + const pts = []; + radii.forEach((r, row) => { + const k = alloc[row]; + for (let j = 0; j < k; j++) { + const angle = k === 1 ? Math.PI / 2 : Math.PI - (Math.PI * (j + 0.5)) / k; + pts.push({ angle, x: cx + r * Math.cos(angle), y: cy - r * Math.sin(angle), row }); + } + }); + pts.sort((a, b) => b.angle - a.angle || a.row - b.row); + const radius = Math.max(3, Math.min(11, gap * 0.4)); + return pts.map(p => ({ x: Number(p.x.toFixed(1)), y: Number(p.y.toFixed(1)), angle: p.angle, r: Number(radius.toFixed(1)) })); +}; + module.exports = ({ cooler, services = {} }) => { let ssb; @@ -19,6 +164,7 @@ module.exports = ({ cooler, services = {} }) => { const CACHE_MS = 250; let logCache = { at: 0, arr: null }; + let voteCache = { at: 0, results: null, tomb: null }; let electionInFlight = null; let sweepInFlight = null; @@ -53,6 +199,7 @@ module.exports = ({ cooler, services = {} }) => { ssbClient.publish(content, (e, r) => (e ? reject(e) : resolve(r))) ); logCache = { at: 0, arr: null }; + voteCache = { at: 0, results: null, tomb: null }; return res; } @@ -60,49 +207,75 @@ module.exports = ({ cooler, services = {} }) => { const now = Date.now(); if (logCache.arr && now - logCache.at < CACHE_MS) return logCache.arr; const ssbClient = await openSsb(); - const arr = await new Promise((res, rej) => { - pull( - ssbClient.createLogStream({ limit: logLimit }), - pull.collect((err, out) => (err ? rej(err) : res(out || []))) - ); - }); + const arr = await readTyped(ssbClient, PARLIAMENT_TYPES, { limit: logLimit }); logCache = { at: now, arr }; return arr; } - function listByTypeFromMsgs(msgs, type) { + function chainIndex(msgs, type) { const tomb = buildValidatedTombstoneSet(msgs); - const rep = new Map(); - const children = new Map(); - const map = new Map(); - + const nodes = new Map(); for (const m of msgs || []) { - const k = m.key; const v = m.value || {}; const c = v.content; - if (!c) continue; -if (c.type === type) { - if (c.replaces) { - const oldId = c.replaces; - const ts = normMs(v.timestamp || m.timestamp || Date.now()); - const prev = rep.get(oldId); - if (!prev || ts > prev.ts) rep.set(oldId, { id: k, ts }); - if (!children.has(oldId)) children.set(oldId, new Set()); - children.get(oldId).add(k); - } - map.set(k, { ...c, id: k }); - } + if (!c || c.type !== type) continue; + nodes.set(m.key, { key: m.key, author: v.author, ts: normMs(v.timestamp || m.timestamp || Date.now()), c }); } + const consistent = (n) => { + if (PROPOSER_TYPES.has(type)) return n.c.proposer === n.author; + if (type === 'parliamentTerm') return !n.c.createdBy || n.c.createdBy === n.author; + return true; + }; + const valid = new Map(); + const settle = (key) => { + const path = []; + const seen = new Set(); + let cur = key; + while (!valid.has(cur)) { + const n = nodes.get(cur); + if (!n || !consistent(n)) { valid.set(cur, false); break; } + const parent = n.c.replaces ? nodes.get(n.c.replaces) : null; + if (!parent) { valid.set(cur, true); break; } + if (parent.author !== n.author || seen.has(cur)) { valid.set(cur, false); break; } + seen.add(cur); + path.push(cur); + cur = parent.key; + } + for (let i = path.length - 1; i >= 0; i--) { + if (!valid.has(path[i])) valid.set(path[i], valid.get(nodes.get(path[i]).c.replaces) === true); + } + return valid.get(key) === true; + }; + const next = new Map(); + for (const n of nodes.values()) { + if (!n.c.replaces || !nodes.has(n.c.replaces) || !settle(n.key)) continue; + const prev = next.get(n.c.replaces); + if (!prev || n.ts > nodes.get(prev).ts) next.set(n.c.replaces, n.key); + } + const tipOf = (k) => { let x = k, g = 0; while (next.has(x) && g++ < 100000) x = next.get(x); return x; }; + const rootOf = (k) => { let x = k, g = 0; while (nodes.has(x) && nodes.has(nodes.get(x).c.replaces) && g++ < 100000) x = nodes.get(x).c.replaces; return x; }; + const items = []; + for (const n of nodes.values()) { + if ((n.c.replaces && nodes.has(n.c.replaces)) || !settle(n.key)) continue; + const tip = tipOf(n.key); + if (tomb.has(tip)) continue; + items.push({ ...nodes.get(tip).c, id: tip }); + } + const resolve = (k) => (nodes.has(k) && settle(k) ? tipOf(rootOf(k)) : null); + const authorOf = (k) => (nodes.has(k) ? nodes.get(k).author : null); + return { items, resolve, authorOf }; + } - for (const oldId of rep.keys()) map.delete(oldId); - for (const [oldId, kids] of children.entries()) { - const winner = rep.get(oldId)?.id || null; - for (const kid of kids) { - if (kid !== winner) map.delete(kid); - } - } - for (const tId of tomb) map.delete(tId); - return [...map.values()]; + function listByTypeFromMsgs(msgs, type) { + const items = chainIndex(msgs, type).items; + return type === 'parliamentProposal' ? items.map(withCampaignRef) : items; + } + + function withCampaignRef(p) { + if (!p || p.campaignId) return p; + const m = String(p.description || '').match(/\n*\s*(\d+) signatures: \/campaigns\/(%[^\s]+\.sha256)\s*$/); + if (!m) return p; + return { ...p, campaignId: m[2], signatures: Number(m[1]) || 0, description: String(p.description || '').slice(0, m.index).trim() }; } async function listByType(type) { @@ -198,11 +371,14 @@ if (c.type === type) { const s = String(candidateInput || '').trim(); if (!s) return null; const tribes = await listTribesAny(); - const t = tribes.find(tr => + let t = tribes.find(tr => tr.id === s || (tr.title && tr.title.toLowerCase() === s.toLowerCase()) || (tr.name && tr.name.toLowerCase() === s.toLowerCase()) ); + if (!t && s.startsWith('%') && services.tribes && services.tribes.getTribeById) { + try { t = await services.tribes.getTribeById(s); } catch (_) { t = null; } + } if (t) { return { type: 'tribe', id: t.id, title: t.title || t.name || t.id, members: ensureArray(t.members) }; } @@ -228,8 +404,9 @@ if (c.type === type) { let n = 0; try { if (services.inhabitants?.listInhabitants) { - const list = await services.inhabitants.listInhabitants({ filter: 'all', includeInactive: true }); - n = Array.isArray(list) ? list.length : 0; + n = typeof services.inhabitants.countInhabitants === 'function' + ? Number(await services.inhabitants.countInhabitants()) || 0 + : ((await services.inhabitants.listInhabitants({ filter: 'all', includeInactive: true })) || []).length; } } catch {} _inhCache = { at: now, n }; @@ -251,7 +428,13 @@ if (c.type === type) { async function listCandidaturesOpenRaw() { const all = await listByType('parliamentCandidature'); - const filtered = all.filter(c => (c.status || 'OPEN') === 'OPEN'); + const window = termWindowFor(Date.now()); + const from = new Date(window.startAt).getTime(); + const filtered = all.filter(c => { + if ((c.status || 'OPEN') !== 'OPEN') return false; + const created = new Date(c.createdAt).getTime(); + return Number.isFinite(created) ? created >= from : true; + }); return filtered.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)); } @@ -320,9 +503,186 @@ if (c.type === type) { return enriched; } + function supportFor(msgs) { + const cands = chainIndex(msgs, 'parliamentCandidature'); + const byId = new Map(cands.items.map(c => [c.id, c])); + const votes = []; + for (const m of msgs || []) { + const v = m.value || {}; + const c = v.content; + if (!c || c.type !== 'parliamentCandidatureVote' || typeof c.target !== 'string' || !v.author) continue; + const id = cands.resolve(c.target); + const cand = id ? byId.get(id) : null; + if (!cand) continue; + if (cand.targetType === 'inhabitant' && String(cand.targetId) === String(v.author)) continue; + votes.push({ cand, voter: v.author, ts: normMs(v.timestamp || m.timestamp || 0), seq: Number(v.sequence) || 0 }); + } + votes.sort((a, b) => (a.ts - b.ts) || (a.seq - b.seq)); + return (term) => { + const from = new Date(term.startAt).getTime(); + const to = new Date(term.endAt).getTime(); + const counted = new Map(); + for (const x of votes) { + if (counted.has(x.voter)) continue; + const created = new Date(x.cand.createdAt).getTime(); + if (!Number.isFinite(created) || created < from || created >= to) continue; + counted.set(x.voter, x.cand); + } + const perCand = new Map(); + for (const cand of counted.values()) perCand.set(cand, (perCand.get(cand) || 0) + 1); + let best = 0; + for (const [cand, n] of perCand) { + if (cand.targetType !== term.powerType || String(cand.targetId) !== String(term.powerId)) continue; + if (String(cand.method || '').toUpperCase() !== String(term.method || '').toUpperCase()) continue; + if (n > best) best = n; + } + return { votes: best, total: counted.size }; + }; + } + + function termShapeOk(t) { + const s = new Date(t && t.startAt).getTime(); + const e = new Date(t && t.endAt).getTime(); + if (!Number.isFinite(s) || !Number.isFinite(e) || e <= s || s > Date.now()) return false; + const window = termWindowFor(s); + if (new Date(window.startAt).getTime() === s && new Date(window.endAt).getTime() === e) return true; + return e <= Date.now() && Math.abs(e - s - TERM_SPAN_MS) <= 86400000; + } + + function validTermsFrom(msgs) { + const supportOf = supportFor(msgs); + const out = []; + for (const t of chainIndex(msgs, 'parliamentTerm').items) { + const method = String(t.method || '').toUpperCase(); + if (!METHODS.includes(method) || !termShapeOk(t)) continue; + const inhabitant = t.powerType === 'inhabitant' && FEED_ID_RE.test(String(t.powerId || '')); + const tribe = t.powerType === 'tribe' && !!t.powerId; + if (!inhabitant && !tribe) continue; + const support = supportOf({ ...t, method }); + if (support.votes < PROPOSAL_QUORUM) continue; + out.push({ ...t, method, winnerVotes: support.votes, totalVotes: support.total }); + } + return collapseOverlappingTerms(out, (t) => t.winnerVotes); + } + + async function listValidTerms() { + return validTermsFrom(await readLog()); + } + + async function readVoteResults() { + const now = Date.now(); + if (voteCache.results && now - voteCache.at < CACHE_MS) return voteCache; + const ssbClient = await openSsb(); + const arr = await readTyped(ssbClient, VOTE_LOG_TYPES, { limit: logLimit }); + voteCache = { at: now, results: buildVoteResults(arr), tomb: buildValidatedTombstoneSet(arr) }; + return voteCache; + } + + async function voteResult(voteId, creator) { + if (!voteId) return null; + const { results, tomb } = await readVoteResults(); + const r = results.get(voteId); + if (!r || tomb.has(r.rootId) || tomb.has(voteId)) return null; + if (creator && r.creator !== creator) return null; + const vm = r.votes || {}; + const due = r.deadline ? new Date(r.deadline).getTime() : NaN; + return { + YES: Number(vm.YES ?? vm.Yes ?? vm.yes ?? 0), + NO: Number(vm.NO ?? vm.No ?? vm.no ?? 0), + ABSTENTION: Number(vm.ABSTENTION ?? vm.Abstention ?? vm.abstention ?? 0), + total: Number(r.totalVotes || 0), + deadline: r.deadline || null, + closed: Number.isFinite(due) && due <= Date.now() + }; + } + + function termForId(termId, terms) { + const id = String(termId || ''); + const found = terms.find(t => t.id === id || t.startAt === id); + if (found) return found; + const m = id.match(/^anarchy:(-?\d+)$/); + return m ? virtualAnarchyTerm(termWindowFor(TERM_EPOCH + Number(m[1]) * TERM_SPAN_MS)) : null; + } + + async function outcomeContext() { + const msgs = await readLog(); + const terms = validTermsFrom(msgs); + const proposals = chainIndex(msgs, 'parliamentProposal'); + const revocations = chainIndex(msgs, 'parliamentRevocation'); + const voteOwner = new Map(); + const byAge = [...proposals.items, ...revocations.items].sort((a, b) => + ((new Date(a.createdAt).getTime() || 0) - (new Date(b.createdAt).getTime() || 0)) || String(a.id).localeCompare(String(b.id)) + ); + for (const x of byAge) if (x.voteId && !voteOwner.has(x.voteId)) voteOwner.set(x.voteId, x.id); + return { msgs, terms, proposals, revocations, voteOwner, members: new Map(), karma: new Map() }; + } + + async function mayPropose(term, feedId, ctx) { + if (!term || !feedId) return false; + if (term.virtual) return true; + if (term.powerType === 'inhabitant') return String(term.powerId) === String(feedId); + if (term.powerType === 'tribe') { + if (!ctx.members.has(term.powerId)) { + let members = []; + if (services.tribes) { + try { const tribe = await services.tribes.getTribeById(term.powerId); members = ensureArray(tribe && tribe.members); } catch {} + } + ctx.members.set(term.powerId, members); + } + return ctx.members.get(term.powerId).includes(feedId); + } + return false; + } + + async function karmaOf(feedId, ctx) { + if (!ctx.karma.has(feedId)) ctx.karma.set(feedId, await getInhabitantKarma(feedId)); + return ctx.karma.get(feedId); + } + + async function wonKarmatocracy(p, term, ctx) { + const spanOf = (x) => [new Date(x.createdAt).getTime() || 0, new Date(x.deadline).getTime()]; + const [ps, pe] = spanOf(p); + const rivals = []; + for (const q of ctx.proposals.items) { + if (q.termId !== p.termId || String(q.method || '').toUpperCase() !== 'KARMATOCRACY') continue; + const [qs, qe] = spanOf(q); + if (!Number.isFinite(qe) || qe > Date.now() || qs > pe || ps > qe) continue; + if (!(await mayPropose(term, q.proposer, ctx))) continue; + rivals.push({ id: q.id, proposer: q.proposer, karma: await karmaOf(q.proposer, ctx), createdAtMs: qs }); + } + rivals.sort((a, b) => (b.karma - a.karma) || (a.createdAtMs - b.createdAtMs) || String(a.proposer).localeCompare(String(b.proposer)) || String(a.id).localeCompare(String(b.id))); + return rivals.length > 0 && rivals[0].id === p.id; + } + + async function deriveOutcome(p, ctx, kind = 'proposal') { + if (!p || !p.proposer) return null; + const term = termForId(p.termId, ctx.terms); + if (!term) return null; + const method = String(p.method || '').toUpperCase(); + if (method !== String(term.method || '').toUpperCase()) return null; + if (!(await mayPropose(term, p.proposer, ctx))) return null; + const status = String(p.status || 'OPEN').toUpperCase(); + if (status === 'REJECTED' || status === 'DISCARDED') return status; + if (VOTE_METHODS.has(method)) { + if (!p.voteId || ctx.voteOwner.get(p.voteId) !== p.id) return null; + const v = await voteResult(p.voteId, p.proposer); + if (!v) return null; + if (!v.closed) return 'OPEN'; + return (await passesThreshold(method, v.total, v.YES)) ? 'APPROVED' : 'REJECTED'; + } + const due = new Date(p.deadline).getTime(); + const expired = Number.isFinite(due) && due <= Date.now(); + if (method === 'DICTATORSHIP') return (expired || status === 'APPROVED' || status === 'ENACTED') ? 'APPROVED' : 'OPEN'; + if (method === 'KARMATOCRACY') { + if (!expired) return 'OPEN'; + if (kind === 'revocation') return 'APPROVED'; + return (await wonKarmatocracy(p, term, ctx)) ? 'APPROVED' : 'REJECTED'; + } + return null; + } + async function listTermsBase(filter = 'all') { - const all = await listByType('parliamentTerm'); - const collapsed = collapseOverlappingTerms(all); + const collapsed = await listValidTerms(); const latestStart = collapsed.reduce((mx, t) => Math.max(mx, new Date(t.startAt).getTime() || 0), 0); let arr = collapsed.map(t => { const superseded = (new Date(t.startAt).getTime() || 0) < latestStart; @@ -334,8 +694,7 @@ if (c.type === type) { } async function getLatestTermAny() { - const terms = await listByType('parliamentTerm'); - const collapsed = collapseOverlappingTerms(terms); + const collapsed = await listValidTerms(); return collapsed[0] || null; } @@ -345,6 +704,28 @@ if (c.type === type) { return isExpiredTerm(t) ? null : t; } + function virtualAnarchyTerm(window = termWindowFor(Date.now())) { + return { + id: `anarchy:${window.cycle}`, + virtual: true, + type: 'parliamentTerm', + cycle: window.cycle, + method: 'ANARCHY', + powerType: 'none', + powerId: null, + powerTitle: 'ANARCHY', + winnerTribeId: null, + winnerInhabitantId: null, + winnerVotes: 0, + totalVotes: 0, + population: 0, + startAt: window.startAt, + endAt: window.endAt, + createdBy: null, + createdAt: window.startAt + }; + } + function currentCycleStart(term) { return term ? term.startAt : moment().subtract(TERM_DAYS, 'days').toISOString(); } @@ -352,6 +733,7 @@ if (c.type === type) { async function archiveAllCandidatures() { const all = await listCandidaturesOpenRaw(); for (const c of all) { + if (String(c.proposer || '') !== String(userId)) continue; const updated = { ...stripId(c), replaces: c.id, status: 'DISCARDED', updatedAt: nowISO() }; await publishMsg(updated); } @@ -409,53 +791,28 @@ if (c.type === type) { } return x.termId === termId || (termStart && x.termId === termStart); }; - const proposals = await listByType('parliamentProposal'); - const mine = termId ? proposals.filter(matchesTerm) : []; - const proposed = mine.length; + const ctx = await outcomeContext(); + const mine = termId ? ctx.proposals.items.filter(matchesTerm) : []; + let proposed = 0; let approved = 0; let declined = 0; let discarded = 0; for (const p of mine) { - const baseStatus = String(p.status || 'OPEN').toUpperCase(); - let finalStatus = baseStatus; - let isDiscarded = false; - if (p.voteId && services.votes?.getVoteById) { - try { - const v = await services.votes.getVoteById(p.voteId); - const votesMap = v.votes || {}; - const sum = Object.values(votesMap).reduce((s, n) => s + Number(n || 0), 0); - const total = Number(v.totalVotes ?? v.total ?? sum); - const yes = Number(votesMap.YES ?? votesMap.Yes ?? votesMap.yes ?? 0); - const dl = v.deadline || v.endAt || v.expiresAt || null; - const closed = v.status === 'CLOSED' || (dl && moment(dl).isBefore(moment())); - const reached = await passesThreshold(p.method, total, yes); - if (!closed) { - if (dl && moment(dl).isBefore(moment()) && !reached) isDiscarded = true; - else finalStatus = 'OPEN'; - } else { - finalStatus = reached ? 'APPROVED' : 'REJECTED'; - } - } catch {} - } else { - if (baseStatus === 'OPEN' && p.deadline && moment(p.deadline).isBefore(moment())) isDiscarded = true; - } - if (isDiscarded) { - discarded++; - continue; - } - if (finalStatus === 'ENACTED' || finalStatus === 'APPROVED') { - approved++; - continue; - } - if (finalStatus === 'REJECTED') { - declined++; - continue; + const outcome = await deriveOutcome(p, ctx); + if (!outcome) continue; + proposed++; + if (outcome === 'APPROVED') approved++; + else if (outcome === 'REJECTED') declined++; + else if (outcome === 'DISCARDED') discarded++; + } + let revocated = 0; + if (termId) { + for (const r of ctx.revocations.items) { + if (!matchesTerm(r)) continue; + const rTerm = termForId(r.termId, ctx.terms); + if (rTerm && !rTerm.virtual && isExpiredTerm(rTerm) && (await deriveOutcome(r, ctx, 'revocation')) === 'APPROVED') revocated++; } } - const revs = await listByType('parliamentRevocation'); - const revocated = termId - ? revs.filter(r => r.status === 'ENACTED' && matchesTerm(r)).length - : 0; return { proposed, approved, declined, discarded, revocated }; } @@ -474,7 +831,12 @@ if (c.type === type) { const pol = await summarizePoliciesForTerm({ ...term }); const eff = pol.proposed > 0 ? Math.round((pol.approved / pol.proposed) * 100) : 0; const cands = await listByType('parliamentCandidature'); - const presented = cands.length; + const cycleStartMs = term.startAt ? new Date(term.startAt).getTime() : 0; + const cycleEndMs = term.endAt ? new Date(term.endAt).getTime() : Number.MAX_SAFE_INTEGER; + const presented = cands.filter(c => { + const t = c.createdAt ? new Date(c.createdAt).getTime() : 0; + return t >= cycleStartMs && t <= cycleEndMs; + }).length; return { method, powerType: term.powerType, @@ -504,84 +866,6 @@ if (c.type === type) { return nProp + nLaw; } - async function closeExpiredKarmatocracy(term) { - const termId = term.id || term.startAt; - const all = await listByType('parliamentProposal'); - const pending = all.filter(p => - p.termId === termId && - String(p.method).toUpperCase() === 'KARMATOCRACY' && - (p.status || 'OPEN') !== 'ENACTED' && - p.deadline && moment().isAfter(parseISO(p.deadline)) - ); - if (!pending.length) return; - const withKarma = await Promise.all(pending.map(async p => ({ - ...p, - karma: await getInhabitantKarma(p.proposer), - createdAtMs: new Date(p.createdAt).getTime() || 0 - }))); - withKarma.sort((a, b) => { - if (b.karma !== a.karma) return b.karma - a.karma; - if (a.createdAtMs !== b.createdAtMs) return a.createdAtMs - b.createdAtMs; - return String(a.proposer).localeCompare(String(b.proposer)); - }); - const winner = withKarma[0]; - const losers = withKarma.slice(1); - const approve = { ...stripId(winner), replaces: winner.id, status: 'APPROVED', updatedAt: nowISO() }; - await publishMsg(approve); - for (const lo of losers) { - const rej = { ...stripId(lo), replaces: lo.id, status: 'REJECTED', updatedAt: nowISO() }; - await publishMsg(rej); - } - } - - async function closeExpiredDictatorship(term) { - const termId = term.id || term.startAt; - const all = await listByType('parliamentProposal'); - const pending = all.filter(p => - p.termId === termId && - String(p.method).toUpperCase() === 'DICTATORSHIP' && - (p.status || 'OPEN') !== 'ENACTED' && - p.deadline && moment().isAfter(parseISO(p.deadline)) - ); - if (!pending.length) return; - for (const p of pending) { - const updated = { ...stripId(p), replaces: p.id, status: 'APPROVED', updatedAt: nowISO() }; - await publishMsg(updated); - } - } - - async function closeExpiredRevocationKarmatocracy(term) { - const termId = term.id || term.startAt; - const all = await listByType('parliamentRevocation'); - const pending = all.filter(p => - p.termId === termId && - String(p.method).toUpperCase() === 'KARMATOCRACY' && - (p.status || 'OPEN') !== 'ENACTED' && - p.deadline && moment().isAfter(parseISO(p.deadline)) - ); - if (!pending.length) return; - for (const p of pending) { - const updated = { ...stripId(p), replaces: p.id, status: 'APPROVED', updatedAt: nowISO() }; - await publishMsg(updated); - } - } - - async function closeExpiredRevocationDictatorship(term) { - const termId = term.id || term.startAt; - const all = await listByType('parliamentRevocation'); - const pending = all.filter(p => - p.termId === termId && - String(p.method).toUpperCase() === 'DICTATORSHIP' && - (p.status || 'OPEN') !== 'ENACTED' && - p.deadline && moment().isAfter(parseISO(p.deadline)) - ); - if (!pending.length) return; - for (const p of pending) { - const updated = { ...stripId(p), replaces: p.id, status: 'APPROVED', updatedAt: nowISO() }; - await publishMsg(updated); - } - } - async function createRevocation({ lawId, title, reasons }) { const term = await getCurrentTermBase(); if (!term) throw new Error('No active government'); @@ -589,7 +873,7 @@ if (c.type === type) { if (!allowed) throw new Error('You are not in the goverment, yet.'); const lawIdStr = String(lawId || '').trim(); if (!lawIdStr) throw new Error('Law required'); - const laws = await listByType('parliamentLaw'); + const laws = await listLaws(); const law = laws.find(l => l.id === lawIdStr); if (!law) throw new Error('Law not found'); const method = String(term.method || 'DEMOCRACY').toUpperCase(); @@ -630,36 +914,28 @@ if (c.type === type) { return await publishMsg(rev); } - async function closeRevocation(revId) { - const ssbClient = await openSsb(); - const msg = await new Promise((resolve, reject) => - ssbClient.get(revId, (e, m) => (e || !m) ? reject(new Error('Revocation not found')) : resolve(m)) - ); - if (msg.content?.type !== 'parliamentRevocation') throw new Error('Revocation not found'); - const p = msg.content; + async function settleOwn(kind, id, manual) { + const ctx = await outcomeContext(); + const index = kind === 'revocation' ? ctx.revocations : ctx.proposals; + const tipId = index.resolve(String(id || '')); + const p = tipId ? index.items.find(x => x.id === tipId) : null; + if (!p) throw new Error(kind === 'revocation' ? 'Revocation not found' : 'Proposal not found'); const currentStatus = String(p.status || 'OPEN').toUpperCase(); if (currentStatus === 'ENACTED' || currentStatus === 'REJECTED' || currentStatus === 'DISCARDED') return p; - const method = String(p.method || '').toUpperCase(); - if (method === 'DICTATORSHIP') { - if (currentStatus === 'APPROVED') return p; - const updated = { ...p, replaces: revId, status: 'APPROVED', updatedAt: nowISO() }; - await publishMsg(updated); - return updated; - } - if (method === 'KARMATOCRACY') return p; - const v = await services.votes.getVoteById(p.voteId); - const votesMap = v.votes || {}; - const sum = Object.values(votesMap).reduce((s, n) => s + Number(n || 0), 0); - const total = Number(v.totalVotes ?? v.total ?? sum); - const yes = Number(votesMap.YES ?? votesMap.Yes ?? votesMap.yes ?? 0); - const ok = await passesThreshold(method, total, yes); - const desiredStatus = ok ? 'APPROVED' : 'REJECTED'; - if (currentStatus === desiredStatus) return p; - const updated = { ...p, replaces: revId, status: desiredStatus, updatedAt: nowISO() }; + if (String(p.proposer) !== String(userId)) return p; + let outcome = await deriveOutcome(p, ctx, kind); + if (manual && outcome === 'OPEN' && String(p.method || '').toUpperCase() === 'DICTATORSHIP') outcome = 'APPROVED'; + if (outcome !== 'APPROVED' && outcome !== 'REJECTED') return p; + if (currentStatus === outcome) return p; + const updated = { ...stripId(p), replaces: p.id, status: outcome, updatedAt: nowISO() }; await publishMsg(updated); return updated; } + async function closeRevocation(revId) { + return await settleOwn('revocation', revId, true); + } + async function proposeCandidature({ candidateId, method }) { const m = String(method || '').toUpperCase(); if (!METHODS.includes(m)) throw new Error('Invalid method'); @@ -706,12 +982,15 @@ if (c.type === type) { return await publishMsg(content); } - async function createProposal({ title, description }) { - let term = await getCurrentTermBase(); - if (!term) { - await resolveElection(); - term = await getCurrentTermBase(); + async function createProposal({ title, description, campaignId = '', signatures = 0, goal = 0 }) { + const campaignRef = String(campaignId || '').trim(); + if (campaignRef) { + const existing = await listByType('parliamentProposal'); + if (existing.some(p => String(p.campaignId || '') === campaignRef)) throw new Error('This campaign was already raised to Parliament'); } + const extra = campaignRef ? { campaignId: campaignRef, signatures: Math.max(0, Math.floor(Number(signatures) || 0)), goal: Math.max(0, Math.floor(Number(goal) || 0)) } : {}; + let term = await getCurrentTermBase(); + if (!term) term = await resolveElection(); if (!term) throw new Error('No active government'); const allowed = await canPropose(); if (!allowed) throw new Error('You are not in the goverment, yet.'); @@ -722,42 +1001,16 @@ if (c.type === type) { const method = String(term.method || 'DEMOCRACY').toUpperCase(); const deadline = moment().add(PROPOSAL_DAYS, 'days').toISOString(); if (method === 'DICTATORSHIP' || method === 'KARMATOCRACY') { - const proposal = { type: 'parliamentProposal', title, description: description || '', method, termId: term.id || term.startAt, proposer: userId, status: 'OPEN', deadline, createdAt: nowISO() }; + const proposal = { type: 'parliamentProposal', title, description: description || '', method, termId: term.id || term.startAt, proposer: userId, status: 'OPEN', deadline, createdAt: nowISO(), ...extra }; return await publishMsg(proposal); } const voteMsg = await services.votes.createVote(title, deadline, ['YES', 'NO', 'ABSTENTION'], [`gov:${term.id || term.startAt}`, `govMethod:${method}`, 'proposal']); - const proposal = { type: 'parliamentProposal', title, description: description || '', method, voteId: voteMsg.key || voteMsg.id, termId: term.id || term.startAt, proposer: userId, status: 'OPEN', createdAt: nowISO() }; + const proposal = { type: 'parliamentProposal', title, description: description || '', method, voteId: voteMsg.key || voteMsg.id, termId: term.id || term.startAt, proposer: userId, status: 'OPEN', createdAt: nowISO(), ...extra }; return await publishMsg(proposal); } async function closeProposal(proposalId) { - const ssbClient = await openSsb(); - const msg = await new Promise((resolve, reject) => - ssbClient.get(proposalId, (e, m) => (e || !m) ? reject(new Error('Proposal not found')) : resolve(m)) - ); - if (msg.content?.type !== 'parliamentProposal') throw new Error('Proposal not found'); - const p = msg.content; - const currentStatus = String(p.status || 'OPEN').toUpperCase(); - if (currentStatus === 'ENACTED' || currentStatus === 'REJECTED' || currentStatus === 'DISCARDED') return p; - const method = String(p.method || '').toUpperCase(); - if (method === 'DICTATORSHIP') { - if (currentStatus === 'APPROVED') return p; - const updated = { ...p, replaces: proposalId, status: 'APPROVED', updatedAt: nowISO() }; - await publishMsg(updated); - return updated; - } - if (method === 'KARMATOCRACY') return p; - const v = await services.votes.getVoteById(p.voteId); - const votesMap = v.votes || {}; - const sum = Object.values(votesMap).reduce((s, n) => s + Number(n || 0), 0); - const total = Number(v.totalVotes ?? v.total ?? sum); - const yes = Number(votesMap.YES ?? votesMap.Yes ?? votesMap.yes ?? 0); - const ok = await passesThreshold(method, total, yes); - const desiredStatus = ok ? 'APPROVED' : 'REJECTED'; - if (currentStatus === desiredStatus) return p; - const updated = { ...p, replaces: proposalId, status: desiredStatus, updatedAt: nowISO() }; - await publishMsg(updated); - return updated; + return await settleOwn('proposal', proposalId, true); } async function sweepProposals() { @@ -765,48 +1018,15 @@ if (c.type === type) { sweepInFlight = (async () => { const term = await getCurrentTermBase(); if (!term) return; - await closeExpiredKarmatocracy(term); - await closeExpiredDictatorship(term); - - const allProps = await listByType('parliamentProposal'); - const voteProps = allProps.filter(p => { - const m = String(p.method || '').toUpperCase(); - return (m === 'DEMOCRACY' || m === 'ANARCHY' || m === 'MAJORITY' || m === 'MINORITY') && p.voteId; - }); - - for (const p of voteProps) { - try { - const v = await services.votes.getVoteById(p.voteId); - const votesMap = v.votes || {}; - const sum = Object.values(votesMap).reduce((s, n) => s + Number(n || 0), 0); - const total = Number(v.totalVotes ?? v.total ?? sum); - const yes = Number(votesMap.YES ?? votesMap.Yes ?? votesMap.yes ?? 0); - const deadline = v.deadline || v.endAt || v.expiresAt || null; - const closed = v.status === 'CLOSED' || (deadline && moment(deadline).isBefore(moment())); - if (closed) { try { await closeProposal(p.id); } catch {} ; continue; } - } catch {} + const termId = term.id || term.startAt; + const ctx = await outcomeContext(); + for (const p of ctx.proposals.items) { + if (p.termId !== termId || String(p.proposer) !== String(userId)) continue; + try { await settleOwn('proposal', p.id, false); } catch {} } - - await closeExpiredRevocationKarmatocracy(term); - await closeExpiredRevocationDictatorship(term); - - const revs = await listByType('parliamentRevocation'); - const voteRevs = revs.filter(p => { - const m = String(p.method || '').toUpperCase(); - return (m === 'DEMOCRACY' || m === 'ANARCHY' || m === 'MAJORITY' || m === 'MINORITY') && p.voteId; - }); - - for (const p of voteRevs) { - try { - const v = await services.votes.getVoteById(p.voteId); - const votesMap = v.votes || {}; - const sum = Object.values(votesMap).reduce((s, n) => s + Number(n || 0), 0); - const total = Number(v.totalVotes ?? v.total ?? sum); - const yes = Number(votesMap.YES ?? votesMap.Yes ?? votesMap.yes ?? 0); - const deadline = v.deadline || v.endAt || v.expiresAt || null; - const closed = v.status === 'CLOSED' || (deadline && moment(deadline).isBefore(moment())); - if (closed) { try { await closeRevocation(p.id); } catch {} ; continue; } - } catch {} + for (const r of ctx.revocations.items) { + if (r.termId !== termId || String(r.proposer) !== String(userId)) continue; + try { await settleOwn('revocation', r.id, false); } catch {} } })().finally(() => { sweepInFlight = null; }); @@ -827,7 +1047,65 @@ if (c.type === type) { return await listTermsBase(filter); } + async function getHemicycle(termInput = null) { + const term = termInput || await getCurrentTerm().catch(() => null); + const window = term && term.startAt ? { startAt: term.startAt, endAt: term.endAt } : termWindowFor(Date.now()); + const from = new Date(window.startAt).getTime(); + const to = window.endAt ? new Date(window.endAt).getTime() : Number.MAX_SAFE_INTEGER; + const msgs = await readLog(); + const rootOf = new Map(); + const cands = new Map(); + for (const m of msgs || []) { + const c = m.value && m.value.content; + if (!c || c.type !== 'parliamentCandidature') continue; + const root = c.replaces ? (rootOf.get(c.replaces) || c.replaces) : m.key; + rootOf.set(m.key, root); + if (!cands.has(root)) { + cands.set(root, { id: root, targetType: c.targetType, targetId: c.targetId, targetTitle: c.targetTitle, method: c.method, createdAt: c.createdAt || new Date(normMs(m.value.timestamp || m.timestamp)).toISOString(), voters: new Set() }); + } + } + for (const m of msgs || []) { + const c = m.value && m.value.content; + if (!c || c.type !== 'parliamentCandidatureVote' || !c.target) continue; + const root = rootOf.get(c.target) || c.target; + const cand = cands.get(root); + if (cand) cand.voters.add(m.value.author); + } + const inWindow = [...cands.values()].filter(c => { const t = new Date(c.createdAt).getTime(); return Number.isFinite(t) ? (t >= from && t <= to) : false; }); + let census = []; + try { + if (services.inhabitants?.listInhabitants) census = await services.inhabitants.listInhabitants({ filter: 'all', includeInactive: true }); + } catch {} + let powerMembers = []; + if (term && term.powerType === 'tribe' && term.powerId && services.tribes?.getTribeById) { + try { const t = await services.tribes.getTribeById(term.powerId); powerMembers = Array.isArray(t && t.members) ? t.members : []; } catch {} + } + const candidatures = inWindow.map(c => ({ + id: c.id, + label: c.targetTitle || c.targetId, + href: c.targetType === 'tribe' ? `/tribe/${encodeURIComponent(c.targetId)}` : `/author/${encodeURIComponent(c.targetId)}`, + targetType: c.targetType, + targetId: c.targetId, + method: c.method, + voters: [...c.voters] + })); + return buildHemicycle({ + census: (census || []).map(u => ({ id: u.id, name: u.name || '' })), + candidatures, + powerType: term ? term.powerType : 'none', + powerId: term ? term.powerId : null, + leaderId: term && term.powerType === 'inhabitant' ? term.powerId : null, + method: term ? term.method : 'ANARCHY', + powerMembers + }); + } + async function getCurrentTerm() { + const published = await getCurrentTermBase(); + return published || virtualAnarchyTerm(); + } + + async function getPublishedTerm() { return await getCurrentTermBase(); } @@ -865,207 +1143,104 @@ if (c.type === type) { return rows; } - async function listProposalsCurrent() { - const all = await listByType('parliamentProposal'); - const rows = all.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)); + async function listCurrentOf(kind) { + const ctx = await outcomeContext(); + const index = kind === 'revocation' ? ctx.revocations : ctx.proposals; + const items = kind === 'revocation' ? index.items : index.items.map(withCampaignRef); + const rows = items.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)); + const needed = await proposalQuorum(); const out = []; for (const p of rows) { - let deadline = p.deadline || null; - let yes = 0; - let total = 0; - const baseStatus = String(p.status || 'OPEN').toUpperCase(); - let derivedStatus = baseStatus; - if (p.voteId && services.votes?.getVoteById) { - try { - const v = await services.votes.getVoteById(p.voteId); - const votesMap = v.votes || {}; - const sum = Object.values(votesMap).reduce((s, n) => s + Number(n || 0), 0); - total = Number(v.totalVotes ?? v.total ?? sum); - yes = Number(votesMap.YES ?? votesMap.Yes ?? votesMap.yes ?? 0); - deadline = deadline || v.deadline || v.endAt || v.expiresAt || null; - const closed = v.status === 'CLOSED' || (deadline && moment(deadline).isBefore(moment())); - const reached = await passesThreshold(p.method, total, yes); - if (closed) derivedStatus = reached ? 'APPROVED' : 'REJECTED'; - else derivedStatus = 'OPEN'; - } catch { - derivedStatus = baseStatus; - } - } else { - if (baseStatus === 'OPEN' && p.deadline && moment(p.deadline).isBefore(moment())) derivedStatus = 'DISCARDED'; - } - if (derivedStatus === 'ENACTED' || derivedStatus === 'REJECTED' || derivedStatus === 'DISCARDED') continue; - const needed = await proposalQuorum(); + if (String(p.status || 'OPEN').toUpperCase() === 'ENACTED') continue; + const derivedStatus = await deriveOutcome(p, ctx, kind); + if (!derivedStatus || derivedStatus === 'REJECTED' || derivedStatus === 'DISCARDED') continue; + const v = p.voteId ? await voteResult(p.voteId, p.proposer) : null; + const yes = v ? v.YES : 0; + const total = v ? v.total : 0; + const deadline = p.deadline || (v && v.deadline) || null; const onTrack = await passesThreshold(p.method, total, yes); out.push({ ...p, deadline, yes, total, needed, onTrack, derivedStatus }); } return out; } + async function listFutureOf(kind) { + const term = await getCurrentTermBase(); + if (!term) return []; + const termId = term.id || term.startAt; + const ctx = await outcomeContext(); + const index = kind === 'revocation' ? ctx.revocations : ctx.proposals; + const items = kind === 'revocation' ? index.items : index.items.map(withCampaignRef); + const rows = items + .filter(p => p.termId === termId && String(p.status || 'OPEN').toUpperCase() !== 'ENACTED') + .sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)); + const needed = await proposalQuorum(); + const out = []; + for (const p of rows) { + if ((await deriveOutcome(p, ctx, kind)) !== 'APPROVED') continue; + const v = p.voteId ? await voteResult(p.voteId, p.proposer) : null; + const deadline = p.deadline || (v && v.deadline) || null; + out.push({ ...p, deadline, yes: v ? v.YES : 0, total: v ? v.total : 0, needed }); + } + return out; + } + + async function listProposalsCurrent() { + return await listCurrentOf('proposal'); + } + async function deriveProposalStatus(method, voteId) { const m = String(method || '').toUpperCase(); - if (!voteId || !(m === 'DEMOCRACY' || m === 'ANARCHY' || m === 'MAJORITY' || m === 'MINORITY')) return null; + if (!voteId || !VOTE_METHODS.has(m)) return null; try { - const v = await services.votes.getVoteById(voteId); - const votesMap = v.votes || {}; - const sum = Object.values(votesMap).reduce((s, n) => s + Number(n || 0), 0); - const total = Number(v.totalVotes ?? v.total ?? sum); - const yes = Number(votesMap.YES ?? votesMap.Yes ?? votesMap.yes ?? 0); - const deadline = v.deadline || v.endAt || v.expiresAt || null; - const closed = v.status === 'CLOSED' || (deadline && moment(deadline).isBefore(moment())); - if (!closed) return 'OPEN'; - return (await passesThreshold(m, total, yes)) ? 'APPROVED' : 'REJECTED'; + const v = await voteResult(voteId); + if (!v) return null; + if (!v.closed) return 'OPEN'; + return (await passesThreshold(m, v.total, v.YES)) ? 'APPROVED' : 'REJECTED'; } catch { return null; } } async function listFutureLawsCurrent() { - const term = await getCurrentTermBase(); - if (!term) return []; - const termId = term.id || term.startAt; - const all = await listByType('parliamentProposal'); - const rows = all - .filter(p => p.termId === termId) - .filter(p => p.status === 'APPROVED') - .sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)); - const out = []; - for (const p of rows) { - let yes = 0; - let total = 0; - let deadline = p.deadline || null; - let voteClosed = true; - if (p.voteId && services.votes?.getVoteById) { - try { - const v = await services.votes.getVoteById(p.voteId); - const votesMap = v.votes || {}; - const sum = Object.values(votesMap).reduce((s, n) => s + Number(n || 0), 0); - total = Number(v.totalVotes ?? v.total ?? sum); - yes = Number(votesMap.YES ?? votesMap.Yes ?? votesMap.yes ?? 0); - deadline = deadline || v.deadline || v.endAt || v.expiresAt || null; - voteClosed = v.status === 'CLOSED' || (deadline && moment(deadline).isBefore(moment())); - if (!voteClosed) continue; - if (VOTE_METHODS.has(String(p.method || '').toUpperCase()) && !(await passesThreshold(p.method, total, yes))) continue; - } catch {} - } - const needed = await proposalQuorum(); - out.push({ ...p, deadline, yes, total, needed }); - } - return out; + return await listFutureOf('proposal'); } async function listRevocationsCurrent() { - const all = await listByType('parliamentRevocation'); - const rows = all.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)); - const out = []; - for (const p of rows) { - let deadline = p.deadline || null; - let yes = 0; - let total = 0; - const baseStatus = String(p.status || 'OPEN').toUpperCase(); - let derivedStatus = baseStatus; - if (p.voteId && services.votes?.getVoteById) { - try { - const v = await services.votes.getVoteById(p.voteId); - const votesMap = v.votes || {}; - const sum = Object.values(votesMap).reduce((s, n) => s + Number(n || 0), 0); - total = Number(v.totalVotes ?? v.total ?? sum); - yes = Number(votesMap.YES ?? votesMap.Yes ?? votesMap.yes ?? 0); - deadline = deadline || v.deadline || v.endAt || v.expiresAt || null; - const closed = v.status === 'CLOSED' || (deadline && moment(deadline).isBefore(moment())); - const reached = await passesThreshold(p.method, total, yes); - if (closed) derivedStatus = reached ? 'APPROVED' : 'REJECTED'; - else derivedStatus = 'OPEN'; - } catch { - derivedStatus = baseStatus; - } - } else { - if (baseStatus === 'OPEN' && p.deadline && moment(p.deadline).isBefore(moment())) derivedStatus = 'DISCARDED'; - } - if (derivedStatus === 'ENACTED' || derivedStatus === 'REJECTED' || derivedStatus === 'DISCARDED') continue; - const needed = await proposalQuorum(); - const onTrack = await passesThreshold(p.method, total, yes); - out.push({ ...p, deadline, yes, total, needed, onTrack, derivedStatus }); - } - return out; + return await listCurrentOf('revocation'); } async function listFutureRevocationsCurrent() { - const term = await getCurrentTermBase(); - if (!term) return []; - const termId = term.id || term.startAt; - const all = await listByType('parliamentRevocation'); - const rows = all - .filter(p => p.termId === termId) - .filter(p => p.status === 'APPROVED') - .sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)); - const out = []; - for (const p of rows) { - let yes = 0; - let total = 0; - let deadline = p.deadline || null; - let voteClosed = true; - if (p.voteId && services.votes?.getVoteById) { - try { - const v = await services.votes.getVoteById(p.voteId); - const votesMap = v.votes || {}; - const sum = Object.values(votesMap).reduce((s, n) => s + Number(n || 0), 0); - total = Number(v.totalVotes ?? v.total ?? sum); - yes = Number(votesMap.YES ?? votesMap.Yes ?? votesMap.yes ?? 0); - deadline = deadline || v.deadline || v.endAt || v.expiresAt || null; - voteClosed = v.status === 'CLOSED' || (deadline && moment(deadline).isBefore(moment())); - if (!voteClosed) continue; - if (VOTE_METHODS.has(String(p.method || '').toUpperCase()) && !(await passesThreshold(p.method, total, yes))) continue; - } catch {} - } - const needed = await proposalQuorum(); - out.push({ ...p, deadline, yes, total, needed }); - } - return out; + return await listFutureOf('revocation'); } async function countRevocationsEnacted() { - const all = await listByType('parliamentRevocation'); - return all.filter(r => r.status === 'ENACTED').length; + const ctx = await outcomeContext(); + let n = 0; + for (const r of ctx.revocations.items) { + const term = termForId(r.termId, ctx.terms); + if (term && !term.virtual && isExpiredTerm(term) && (await deriveOutcome(r, ctx, 'revocation')) === 'APPROVED') n++; + } + return n; } - async function voteSnapshot(voteId) { - if (!voteId || !services.votes?.getVoteById) return null; - try { - const v = await services.votes.getVoteById(voteId); - const vm = v?.votes || {}; - const sum = Object.values(vm).reduce((s, n) => s + Number(n || 0), 0); - const total = Number(v.totalVotes ?? v.total ?? sum); - return { - YES: Number(vm.YES ?? vm.Yes ?? vm.yes ?? 0), - NO: Number(vm.NO ?? vm.No ?? vm.no ?? 0), - ABSTENTION: Number(vm.ABSTENTION ?? vm.Abstention ?? vm.abstention ?? 0), - total - }; - } catch { - return null; - } - } + const votesOf = (v) => (v ? { YES: v.YES, NO: v.NO, ABSTENTION: v.ABSTENTION, total: v.total } : null); async function enactApprovedChanges(expiringTerm) { if (!expiringTerm) return; const termId = expiringTerm.id || expiringTerm.startAt; + const ctx = await outcomeContext(); - const proposals = await listByType('parliamentProposal'); - const revocations = await listByType('parliamentRevocation'); - - const approvedProps = proposals.filter(p => p.termId === termId && p.status === 'APPROVED'); - for (const p of approvedProps) { - const snap = await voteSnapshot(p.voteId); - const votesFinal = - (p.votes && Object.keys(p.votes).length ? p.votes : null) || - snap || - { YES: 1, NO: 0, ABSTENTION: 0, total: 1 }; - - const totalFinal = Number(votesFinal.total ?? Object.entries(votesFinal).reduce((s, [k, n]) => s + (k === 'total' ? 0 : Number(n || 0)), 0)); - const yesFinal = Number(votesFinal.YES ?? votesFinal.Yes ?? votesFinal.yes ?? 0); - if (VOTE_METHODS.has(String(p.method || '').toUpperCase()) && !(await passesThreshold(p.method, totalFinal, yesFinal))) { + for (const p of ctx.proposals.items.map(withCampaignRef)) { + if (p.termId !== termId || String(p.proposer) !== String(userId)) continue; + const status = String(p.status || 'OPEN').toUpperCase(); + if (status === 'ENACTED' || status === 'REJECTED' || status === 'DISCARDED') continue; + const outcome = await deriveOutcome(p, ctx); + if (outcome === 'REJECTED') { const rej = { ...stripId(p), replaces: p.id, status: 'REJECTED', updatedAt: nowISO() }; await publishMsg(rej); continue; } + if (outcome !== 'APPROVED') continue; + const snap = p.voteId ? await voteResult(p.voteId, p.proposer) : null; const law = { type: 'parliamentLaw', @@ -1075,7 +1250,7 @@ if (c.type === type) { proposer: p.proposer, termId: p.termId, voteId: p.voteId || null, - votes: votesFinal, + votes: votesOf(snap) || { YES: 1, NO: 0, ABSTENTION: 0, total: 1 }, proposedAt: p.createdAt, proposalId: p.id, enactedAt: nowISO() @@ -1086,17 +1261,24 @@ if (c.type === type) { await publishMsg(updated); } - const approvedRevs = revocations.filter(r => r.termId === termId && r.status === 'APPROVED'); - for (const r of approvedRevs) { - const tomb = { type: 'tombstone', target: r.lawId, deletedAt: nowISO(), author: userId }; - await publishMsg(tomb); + for (const r of ctx.revocations.items) { + if (r.termId !== termId || String(r.proposer) !== String(userId)) continue; + const status = String(r.status || 'OPEN').toUpperCase(); + if (status === 'ENACTED' || status === 'REJECTED' || status === 'DISCARDED') continue; + if ((await deriveOutcome(r, ctx, 'revocation')) !== 'APPROVED') continue; + const lawMsg = (ctx.msgs || []).find(m => m.key === r.lawId); + const lawValue = lawMsg && lawMsg.value; + if (lawValue && lawValue.author === userId && lawValue.content && lawValue.content.type === 'parliamentLaw') { + const tomb = { type: 'tombstone', target: r.lawId, deletedAt: nowISO(), author: userId }; + await publishMsg(tomb); + } - const snap = await voteSnapshot(r.voteId); + const snap = r.voteId ? await voteResult(r.voteId, r.proposer) : null; const updated = { ...stripId(r), replaces: r.id, status: 'ENACTED', - votes: (r.votes && Object.keys(r.votes).length ? r.votes : null) || snap || undefined, + votes: votesOf(snap) || undefined, updatedAt: nowISO() }; await publishMsg(updated); @@ -1112,7 +1294,10 @@ if (c.type === type) { try { await enactApprovedChanges(latestAny); } catch (e) { console.error('enactApprovedChanges failed:', e); } } - const open = await listCandidaturesOpen(); + const open = (await listCandidaturesOpen()).filter(c => + METHODS.includes(String(c.method || '').toUpperCase()) && + ((c.targetType === 'inhabitant' && FEED_ID_RE.test(String(c.targetId || ''))) || (c.targetType === 'tribe' && !!c.targetId)) + ); let chosen = null; let totalVotes = 0; let winnerVotes = 0; @@ -1130,44 +1315,21 @@ if (c.type === type) { } } - const startAt = now.toISOString(); - const endAt = moment(startAt).add(TERM_DAYS, 'days').toISOString(); + const window = termWindowFor(now.valueOf()); + const startAt = window.startAt; + const endAt = window.endAt; + const population = await inhabitantsCount(); + if (chosen && supportFor(await readLog())({ ...window, powerType: chosen.targetType, powerId: chosen.targetId, method: chosen.method }).votes < PROPOSAL_QUORUM) chosen = null; if (!chosen) { - const termAnarchy = { - type: 'parliamentTerm', - method: 'ANARCHY', - powerType: 'none', - powerId: null, - powerTitle: 'ANARCHY', - winnerTribeId: null, - winnerInhabitantId: null, - winnerVotes: 0, - totalVotes: 0, - startAt, - endAt, - createdBy: userId, - createdAt: nowISO() - }; - - const resAnarchy = await publishMsg(termAnarchy); - try { - await sleep(250); - const canonical = await getCurrentTermBase(); - const myId = resAnarchy.key || resAnarchy.id; - if (canonical && canonical.id && myId && canonical.id !== myId) { - const tomb = { type: 'tombstone', target: myId, deletedAt: nowISO(), author: userId }; - await publishMsg(tomb); - await archiveAllCandidatures(); - return canonical; - } - } catch {} await archiveAllCandidatures(); - return resAnarchy; + return virtualAnarchyTerm(window); } const term = { type: 'parliamentTerm', + cycle: window.cycle, + population, method: chosen.method, powerType: chosen.targetType, powerId: chosen.targetId, @@ -1205,13 +1367,81 @@ if (c.type === type) { } async function getGovernmentCard() { - const term = await getCurrentTermBase(); + const term = (await getCurrentTermBase()) || virtualAnarchyTerm(); if (!term) return null; return await computeGovernmentCard({ ...term, id: term.id || term.startAt }); } + async function getLatestTerm() { + return await getLatestTermAny(); + } + + async function getLatestGovernmentCard() { + const term = await getLatestTermAny(); + if (!term) return null; + const card = await computeGovernmentCard({ ...term, id: term.id || term.startAt }); + const current = termWindowFor(Date.now()); + const termEnd = term.endAt ? new Date(term.endAt).getTime() : NaN; + const rolled = Number.isFinite(termEnd) && termEnd <= Date.now(); + if (rolled) { + return { + ...card, + id: term.id || term.startAt, + cycle: current.cycle, + since: current.startAt, + end: current.endAt, + method: 'ANARCHY', + powerType: 'none', + powerId: null, + powerTitle: 'ANARCHY', + winnerVotes: 0, + totalVotes: 0, + votesReceived: 0, + presented: 0, + proposed: 0, + approved: 0, + declined: 0, + discarded: 0, + revocated: 0, + efficiency: 0, + expired: false, + rolled: true + }; + } + return { ...card, id: term.id || term.startAt, since: term.startAt, end: term.endAt, expired: false, rolled: false }; + } + async function listLaws() { - const items = await listByType('parliamentLaw'); + const ctx = await outcomeContext(); + const proposalsById = new Map(ctx.proposals.items.map(p => [p.id, p])); + const laws = chainIndex(ctx.msgs, 'parliamentLaw'); + const chosen = new Map(); + const groupOfLaw = new Map(); + for (const l of laws.items) { + const pid = ctx.proposals.resolve(String(l.proposalId || '')); + const p = pid ? proposalsById.get(pid) : null; + if (!p || String(p.proposer) !== String(l.proposer) || p.termId !== l.termId) continue; + const voted = withCampaignRef(p); + if (l.question !== voted.title || String(l.description || '') !== String(voted.description || '') || String(l.method || '') !== String(voted.method || '')) continue; + const term = termForId(p.termId, ctx.terms); + if (!term || term.virtual || !isExpiredTerm(term)) continue; + if ((await deriveOutcome(p, ctx)) !== 'APPROVED') continue; + groupOfLaw.set(l.id, pid); + const own = laws.authorOf(l.id) === p.proposer; + const prev = chosen.get(pid); + const better = !prev + || (own && !prev.own) + || (own === prev.own && (new Date(l.enactedAt).getTime() || 0) < (new Date(prev.law.enactedAt).getTime() || 0)); + if (better) chosen.set(pid, { law: l, own }); + } + for (const r of ctx.revocations.items) { + const pid = groupOfLaw.get(String(r.lawId || '')); + if (!pid || !chosen.has(pid)) continue; + const term = termForId(r.termId, ctx.terms); + if (!term || term.virtual || !isExpiredTerm(term)) continue; + if ((await deriveOutcome(r, ctx, 'revocation')) === 'APPROVED') chosen.delete(pid); + } + const items = [...chosen.values()].map(x => x.law); return items.sort((a, b) => new Date(b.enactedAt) - new Date(a.enactedAt)); } @@ -1243,12 +1473,19 @@ if (c.type === type) { const tribeReadLog = async () => { const client = await openSsb(); - return new Promise((resolve, reject) => { - pull( - client.createLogStream({ limit: logLimit }), - pull.collect((err, msgs) => err ? reject(err) : resolve(msgs || [])) - ); - }); + return readTyped(client, TRIBE_PARLIAMENT_TYPES, { limit: logLimit }); + }; + + const tribeMembersOf = async (ids) => { + const out = new Set(); + for (const id of ids) { + try { + const t = services.tribes && services.tribes.getTribeById ? await services.tribes.getTribeById(id) : null; + if (t && t.author) out.add(t.author); + for (const m of (t && Array.isArray(t.members) ? t.members : [])) out.add(m); + } catch (_) {} + } + return out; }; const tribeListByType = async (type, tribeId) => { @@ -1258,24 +1495,91 @@ if (c.type === type) { ? await services.tribes.getChainIds(tribeId) : [tribeId]; } catch (_) { chainIds = [tribeId]; } - const tribeIdSet = new Set(Array.isArray(chainIds) && chainIds.length ? chainIds : [tribeId]); + const ids = Array.isArray(chainIds) && chainIds.length ? chainIds : [tribeId]; + const tribeIdSet = new Set(ids); + const members = await tribeMembersOf(ids); const msgs = await tribeReadLog(); const tomb = buildValidatedTombstoneSet(msgs); - const replaced = new Set(); - const items = new Map(); + const roots = new Map(); + const rewrites = []; for (const m of msgs) { const c = m.value?.content; if (!c) continue; - if (c.type === 'tombstone') continue; if (c.type !== type) continue; if (!tribeIdSet.has(c.tribeId)) continue; - if (c.replaces) replaced.add(c.replaces); - items.set(m.key, { ...c, id: m.key, _ts: m.value?.timestamp || 0 }); + const author = m.value?.author; + if (!members.has(author)) continue; + const item = { ...c, id: m.key, author, _ts: m.value?.timestamp || 0 }; + if (c.replaces) { rewrites.push(item); continue; } + if (type === 'tribeParliamentCandidature' && c.proposer !== author) continue; + if (type === 'tribeParliamentTerm') { + if (c.createdBy && c.createdBy !== author) continue; + const method = String(c.method || '').toUpperCase(); + if (!METHODS.includes(method) && method !== 'ANARCHY') continue; + if (c.leaderId && !members.has(c.leaderId)) continue; + } + roots.set(m.key, item); } - return [...items.values()].filter(it => !tomb.has(it.id) && !replaced.has(it.id)); + if (type === 'tribeParliamentCandidature') { + const byId = new Map(rewrites.map(r => [r.id, r])); + const rootOfRewrite = (id) => { let cur = id, g = 0; while (byId.has(cur) && g++ < 1000) cur = byId.get(cur).replaces; return cur; }; + const ballotsOf = new Map(); + for (const r of rewrites) { + const root = roots.get(rootOfRewrite(r.id)); + if (!root || r.author === root.candidateId) continue; + if (!ballotsOf.has(root.id)) ballotsOf.set(root.id, []); + ballotsOf.get(root.id).push({ author: r.author, ts: r._ts }); + } + return [...roots.values()].map(it => ({ ...it, _ballots: ballotsOf.get(it.id) || [], _closed: tomb.has(it.id) })); + } + const replacedBySameAuthor = new Map(); + for (const r of rewrites) { const orig = roots.get(r.replaces); if (orig && orig.author === r.author) replacedBySameAuthor.set(r.replaces, r); } + const out = []; + for (const it of roots.values()) { + let cur = it, g = 0; + while (replacedBySameAuthor.has(cur.id) && g++ < 1000) cur = replacedBySameAuthor.get(cur.id); + if (!tomb.has(cur.id) && !tomb.has(it.id)) out.push(cur); + } + return out; + }; + + const tallyCandidatures = (cands, fromTs, toTs) => { + const createdOf = (c) => Date.parse(c.createdAt) || c._ts || 0; + const inCycle = cands.filter(c => createdOf(c) >= fromTs && createdOf(c) < toTs); + const ballots = []; + for (const c of inCycle) for (const b of (c._ballots || [])) if (b.ts >= fromTs && b.ts < toTs) ballots.push({ ...b, root: c.id }); + ballots.sort((a, b) => a.ts - b.ts); + const seen = new Set(); + const votersOf = new Map(); + for (const b of ballots) { + if (seen.has(b.author)) continue; + seen.add(b.author); + if (!votersOf.has(b.root)) votersOf.set(b.root, []); + votersOf.get(b.root).push(b.author); + } + return inCycle.map(c => { const { _ballots, _closed, ...rest } = c; const voters = votersOf.get(c.id) || []; return { ...rest, voters, votes: voters.length, ...(_closed ? { _closed } : {}) }; }); + }; + + const tribeValidTerms = async (tribeId) => { + const terms = (await tribeListByType('tribeParliamentTerm', tribeId)).slice().sort((a, b) => (Date.parse(a.startAt) || 0) - (Date.parse(b.startAt) || 0)); + if (!terms.some(t => t.leaderId)) return terms; + const cands = await tribeListByType('tribeParliamentCandidature', tribeId); + const quorum = await tribeElectionQuorum(tribeId); + const out = []; + for (const t of terms) { + const start = Date.parse(t.startAt) || 0; + if (t.leaderId) { + const prevStart = out.length ? (Date.parse(out[out.length - 1].startAt) || 0) : 0; + const tally = tallyCandidatures(cands, prevStart, start); + const backing = tally.filter(c => c.candidateId === t.leaderId).reduce((n, c) => Math.max(n, c.votes), 0); + if (backing < quorum) continue; + } + out.push(t); + } + return out; }; const tribeGetCurrentTerm = async (tribeId) => { - const terms = await tribeListByType('tribeParliamentTerm', tribeId); + const terms = await tribeValidTerms(tribeId); if (terms.length === 0) return null; const now = moment(); const active = terms.find(t => moment(t.startAt).isSameOrBefore(now) && moment(t.endAt).isAfter(now)); @@ -1323,11 +1627,7 @@ if (c.type === type) { const latest = await tribeGetCurrentTerm(tribeId); if (latest && !isExpiredTerm(latest)) return latest; const opens = (await tribeListCandidatures(tribeId)) - .filter(c => (c.status || 'OPEN') === 'OPEN') - .map(c => { - const voters = ensureArray(c.voters).filter(v => String(v) !== String(c.candidateId)); - return { ...c, voters, votes: voters.length }; - }); + .filter(c => (c.status || 'OPEN') === 'OPEN'); let chosen = null, totalVotes = 0, winnerVotes = 0; if (opens.length) { opens.sort((a, b) => Number(b.votes || 0) - Number(a.votes || 0) || new Date(a.createdAt) - new Date(b.createdAt)); @@ -1372,7 +1672,11 @@ if (c.type === type) { return await tribeResolveElection(tribeId); } - const tribeListCandidatures = (tribeId) => tribeListByType('tribeParliamentCandidature', tribeId); + const tribeListCandidatures = async (tribeId) => { + const cands = await tribeListByType('tribeParliamentCandidature', tribeId); + const term = await tribeGetCurrentTerm(tribeId); + return tallyCandidatures(cands, term ? (Date.parse(term.startAt) || 0) : 0, Infinity).filter(c => !c._closed); + }; const tribeListRules = (tribeId) => tribeListByType('tribeParliamentRule', tribeId); const tribePublishCandidature = async ({ tribeId, candidateId, method }) => { @@ -1464,6 +1768,10 @@ if (c.type === type) { listCandidatures, listTerms, getCurrentTerm, + getPublishedTerm, + getLatestTerm, + getLatestGovernmentCard, + getHemicycle, listLeaders, sweepProposals, getActorMeta, @@ -1492,7 +1800,22 @@ if (c.type === type) { function sleep(ms) { return new Promise(r => setTimeout(r, ms)); } -function collapseOverlappingTerms(terms = []) { +function compareTermsForWindow(a, b, supportOf = () => 0) { + const support = (t) => Number(supportOf(t)) || 0; + const anarchy = (t) => (String(t && t.method || '').toUpperCase() === 'ANARCHY' ? 1 : 0); + + const supportA = support(a); + const supportB = support(b); + if (supportA !== supportB) return supportB - supportA; + + const anarchyA = anarchy(a); + const anarchyB = anarchy(b); + if (anarchyA !== anarchyB) return anarchyA - anarchyB; + + return String(a && a.id || '').localeCompare(String(b && b.id || '')); +} + +function collapseOverlappingTerms(terms = [], supportOf) { if (!terms.length) return []; const sorted = [...terms].sort((a, b) => new Date(a.startAt) - new Date(b.startAt)); const groups = []; @@ -1512,20 +1835,15 @@ function collapseOverlappingTerms(terms = []) { if (!placed) groups.push({ items: [t], minStart: tStart, maxEnd: tEnd }); } const winners = groups.map(g => { - g.items.sort((a, b) => { - const aAn = String(a.method || '').toUpperCase() === 'ANARCHY' ? 1 : 0; - const bAn = String(b.method || '').toUpperCase() === 'ANARCHY' ? 1 : 0; - if (aAn !== bAn) return aAn - bAn; - const aC = new Date(a.createdAt || a.startAt).getTime(); - const bC = new Date(b.createdAt || b.startAt).getTime(); - if (aC !== bC) return aC - bC; - const aS = new Date(a.startAt).getTime(); - const bS = new Date(b.startAt).getTime(); - if (aS !== bS) return aS - bS; - return String(a.id || '').localeCompare(String(b.id || '')); - }); + g.items.sort((a, b) => compareTermsForWindow(a, b, supportOf)); return g.items[0]; }); return winners.sort((a, b) => new Date(b.startAt) - new Date(a.startAt)); } +module.exports.termWindowFor = termWindowFor; +module.exports.compareTermsForWindow = compareTermsForWindow; +module.exports.collapseOverlappingTerms = collapseOverlappingTerms; +module.exports.TERM_DAYS = TERM_DAYS; + +module.exports.hemicycle = { build: buildHemicycle, seatPositions, passThreshold }; diff --git a/src/models/peer_health.js b/src/models/peer_health.js new file mode 100644 index 0000000..e7a75b3 --- /dev/null +++ b/src/models/peer_health.js @@ -0,0 +1,105 @@ +const fs = require('fs'); + +const FAILURES_MIN = 3; +const DEAD_AFTER_MS = 7 * 24 * 60 * 60 * 1000; +const RETRY_CAP_MS = 3 * 60 * 60 * 1000; +const FEED_ID = /^@[A-Za-z0-9+/]{43}=\.ed25519$/; + +const healthPath = () => require('../configs/state-manager').statePath('peer-health.json'); +let cache = null; +let cachePath = null; + +const canonicalKey = (key) => { + let k = String(key || '').trim().replace(/-/g, '+').replace(/_/g, '/'); + if (!k) return null; + if (!k.startsWith('@')) k = '@' + k; + if (!k.endsWith('.ed25519')) k += '.ed25519'; + if (!/=\.ed25519$/.test(k)) k = k.replace(/\.ed25519$/, '=.ed25519'); + return FEED_ID.test(k) ? k : null; +}; + +const load = () => { + const p = healthPath(); + if (!cache || cachePath !== p) { + cachePath = p; + try { cache = JSON.parse(fs.readFileSync(p, 'utf8')) || {}; } catch (_) { cache = {}; } + if (typeof cache !== 'object' || Array.isArray(cache)) cache = {}; + } + return cache; +}; + +const save = () => { try { fs.writeFileSync(cachePath, JSON.stringify(cache)); } catch (_) {} }; + +const observe = ({ connected = [], failing = [], now = Date.now() } = {}) => { + const h = load(); + let changed = false; + const up = new Set(connected.map(canonicalKey).filter(Boolean)); + for (const id of up) { + if (h[id]) { delete h[id]; changed = true; } + } + for (const k of failing) { + const id = canonicalKey(k); + if (!id || up.has(id) || h[id]) continue; + h[id] = { failingSince: now }; + changed = true; + } + if (changed) save(); + return changed; +}; + +const isDead = ({ key, failures = 0, now = Date.now() } = {}) => { + if (!(Number(failures) >= FAILURES_MIN)) return false; + const e = load()[canonicalKey(key)] || {}; + const observed = Number(e.failingSince) ? now - Number(e.failingSince) : 0; + return Math.max(observed, Number(failures) * RETRY_CAP_MS) >= DEAD_AFTER_MS; +}; + +const failingSince = (key) => Number((load()[canonicalKey(key)] || {}).failingSince) || 0; + +const addressOf = (item) => { + const m = Array.isArray(item) ? String(item[0] || '').match(/^(?:net|onion):([^:~]+):(\d+)/) : null; + if (m) return `${m[1]}:${m[2]}`; + const data = Array.isArray(item) ? item[1] : item; + return data && data.host ? `${data.host}:${Number(data.port) || 8008}` : null; +}; +const failuresByKey = (...sources) => { + const byKey = new Map(); + for (const list of sources) { + for (const item of list || []) { + const data = Array.isArray(item) ? item[1] : item; + const id = data && canonicalKey(data.key); + if (!id) continue; + const f = Number(data.failure) || 0; + const e = byKey.get(id) || { any: 0, addrs: new Map() }; + const where = addressOf(item); + if (where) e.addrs.set(where, Math.max(e.addrs.get(where) || 0, f)); + else e.any = Math.max(e.any, f); + byKey.set(id, e); + } + } + const out = new Map(); + for (const [id, e] of byKey) out.set(id, e.addrs.size ? Math.min(...[...e.addrs.values()].map(f => Math.max(f, e.any))) : e.any); + return out; +}; + +const classifyNetError = (err, address = '') => { + const msg = `${(err && err.code) || ''} ${(err && err.message) || err || ''}`.toLowerCase(); + const onion = /\.onion/i.test(String(address || '')); + if (msg.includes('network paused')) return 'paused'; + if (onion && /only know|could not connect to/.test(msg)) return 'tor'; + if (/127\.0\.0\.1:9[01]50/.test(msg) || (onion && /econnrefused|socks/.test(msg) && !/ttl|hostunreachable|host unreachable|general/.test(msg))) return 'tor'; + if (/shs\.client: (error when expecting server to accept challenge|server responded with invalid challenge)|application cap/.test(msg)) return 'keys'; + if (/shs\.client: (server hung up when we sent hello|the server's response accepting)/.test(msg)) return 'identity'; + if (/econnrefused/.test(msg)) return 'refused'; + if (/enotfound|eai_again|getaddrinfo/.test(msg)) return 'notfound'; + if (/enetunreach|ehostunreach|ehostdown|enetdown|hostunreachable|host unreachable|networkunreachable/.test(msg)) return 'unreachable'; + if (/timed? ?out|etimedout|ttl/.test(msg)) return 'timeout'; + if (/econnreset|epipe|hung up|aborted|closed/.test(msg)) return 'dropped'; + return 'other'; +}; +const NET_REASON_KEYS = { + paused: 'peerErrPaused', tor: 'peerErrTor', keys: 'peerErrKeys', identity: 'peerErrIdentity', refused: 'peerErrRefused', + notfound: 'peerErrNotFound', unreachable: 'peerErrUnreachable', timeout: 'peerErrTimeout', dropped: 'peerErrDropped', other: 'peerErrOther', silent: 'peerErrSilent' +}; + +module.exports = { canonicalKey, observe, isDead, failingSince, failuresByKey, classifyNetError, NET_REASON_KEYS, DEAD_AFTER_MS }; diff --git a/src/models/phone_model.js b/src/models/phone_model.js new file mode 100644 index 0000000..4dabb6b --- /dev/null +++ b/src/models/phone_model.js @@ -0,0 +1,325 @@ +const fs = require('fs'); +const pull = require('../server/node_modules/pull-stream'); +const sharedState = require('../configs/shared-state'); +const { notify: desktopNotify, i18nNow, displayName } = require('../backend/desktopNotify'); +const { statePath } = require('../configs/state-manager'); + +const HISTORY_MAX = 200; +const HEARD_MAX = 1000; +const LATEST_MAX = 5; +const PUBS_TTL_MS = 5 * 60 * 1000; +const RECOUNT_DELAY_MS = 500; +const FEED_ID = /^@[A-Za-z0-9+/]{43}=\.ed25519$/; + +const readJson = (p, fallback) => { try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch (_) { return fallback; } }; +const writeJson = (p, data) => { try { fs.writeFileSync(p, JSON.stringify(data, null, 2)); } catch (_) {} }; +const asP = (fn, ...args) => new Promise((resolve, reject) => fn(...args, (err, v) => err ? reject(err) : resolve(v))); +const rpcValue = (fn, ...args) => new Promise((resolve) => { + try { + const r = fn(...args, (err, v) => resolve(err ? null : v)); + if (r && typeof r.then === 'function') r.then(resolve, () => resolve(null)); + else if (r !== undefined) resolve(r); + } catch (_) { resolve(null); } +}); + +module.exports = ({ cooler, pmModel, nameOf, isPublic, encryptFile, isAvailable, prefetch }) => { + const historyFile = statePath('phone-history.json'); + const seenFile = statePath('phone-seen.json'); + let ssb = null; + let subscribed = false; + let pubsAt = 0; + let pubsTimer = null; + + const open = async () => { if (!ssb) ssb = await cooler.open(); return ssb; }; + const phone = async () => { const s = await open(); return s && s.phone ? s.phone : null; }; + const history = () => { const h = readJson(historyFile, []); return Array.isArray(h) ? h : []; }; + const seen = () => { + const s = readJson(seenFile, {}); + return { missedSeenAt: Number(s.missedSeenAt) || 0, heard: Array.isArray(s.heard) ? s.heard : [] }; + }; + + const refreshCount = async () => { + let n = 0; + const st = sharedState.getPhoneCall ? sharedState.getPhoneCall() : null; + if (st && st.dir === 'in' && st.phase === 'incoming') n += 1; + const sn = seen(); + n += history().filter(h => h && h.outcome === 'missed' && h.endedAt > sn.missedSeenAt).length; + try { + const me = (await open()).id; + const heard = new Set(sn.heard); + n += (await pmModel.listPams()).filter(m => m.value.author !== me && !heard.has(m.key)).length; + } catch (_) {} + sharedState.setPhoneCount(n); + return n; + }; + + const notify = async (peer, callId) => { + const i18n = i18nNow(); + let name = ''; + try { name = await nameOf(peer); } catch (_) {} + desktopNotify(i18n.phoneTitle, `${displayName(peer, name)} ${i18n.phoneIsCalling}`, callId ? `call:${callId}` : null, { + timeoutMs: require('../server/phone_module').RING_MS, + actions: [{ key: 'answer', label: i18n.phoneAnswer }, { key: 'reject', label: i18n.phoneReject }], + onAction: async (chosen) => { + try { + const ph = await phone(); + const st = ph ? await rpcValue(ph.state) : null; + if (!st || st.id !== callId || st.phase !== 'incoming') return; + await act(chosen === 'answer' ? 'accept' : 'reject'); + } catch (_) {} + } + }); + }; + + const ROOM_EVENT_TEXT = { join: 'roomEventJoined', leave: 'roomEventLeft', recStart: 'roomEventRecStart', recStop: 'roomEventRecStop', mute: 'roomEventMuted', unmute: 'roomEventUnmuted', hand: 'roomEventHand' }; + const roomNotices = { rid: null, since: 0, seen: new Set() }; + const announceRoom = async (room) => { + if (!room || !room.rid) { roomNotices.rid = null; roomNotices.seen = new Set(); return; } + if (roomNotices.rid !== room.rid) { roomNotices.rid = room.rid; roomNotices.since = Number(room.joinedAt) || Date.now(); roomNotices.seen = new Set(); } + if (!room.notify) { roomNotices.since = Date.now() + 1; return; } + const events = Array.isArray(room.events) ? room.events : []; + const keyOf = (e) => `${e.ts}:${e.t}:${e.id}`; + const fresh = events.filter(e => e && ROOM_EVENT_TEXT[e.t] && !roomNotices.seen.has(keyOf(e)) && Number(e.ts) >= roomNotices.since); + roomNotices.seen = new Set(events.filter(Boolean).map(keyOf)); + if (!fresh.length) return; + let me = null; + try { me = (await open()).id; } catch (_) {} + const i18n = i18nNow(); + for (const e of fresh) { + if (e.id === me) continue; + let name = ''; + try { name = await nameOf(e.id); } catch (_) {} + desktopNotify(i18n.roomsTitle, String(i18n[ROOM_EVENT_TEXT[e.t]] || '{name}').replace('{name}', displayName(e.id, name))); + } + }; + + const subscribe = async () => { + if (subscribed) return; + const ph = await phone(); + if (!ph || subscribed) return; + subscribed = true; + pull(ph.events(), pull.drain((ev) => { + if (!ev) return; + if (ev.type === 'state') { + sharedState.setPhoneCall(ev.state || null); + refreshCount(); + } else if (ev.type === 'room') { + sharedState.setPhoneRoom(ev.room || null); + announceRoom(ev.room).catch(() => {}); + } else if (ev.type === 'recordLimit') { + finishPam().catch(() => {}); + } else if (ev.type === 'incoming') { + notify(ev.peer, ev.id); + } else if (ev.type === 'ended' && ev.call) { + const h = history(); + h.unshift(ev.call); + writeJson(historyFile, h.slice(0, HISTORY_MAX)); + refreshCount(); + } + }, () => { + subscribed = false; + const retry = setTimeout(() => { subscribe().catch(() => {}); }, 5000); + if (retry.unref) retry.unref(); + })); + try { sharedState.setPhoneCall((await rpcValue(ph.state)) || null); } catch (_) {} + try { if (typeof ph.roomState === 'function') sharedState.setPhoneRoom((await rpcValue(ph.roomState)) || null); } catch (_) {} + }; + + let arrivalsWatched = false; + let recountTimer = null; + const scheduleRecount = () => { + if (recountTimer) return; + recountTimer = setTimeout(() => { recountTimer = null; refreshCount().catch(() => {}); }, RECOUNT_DELAY_MS); + if (recountTimer.unref) recountTimer.unref(); + }; + const watchArrivals = async () => { + if (arrivalsWatched) return; + const s = await open(); + if (!s || !s.db || typeof s.db.onMsgAdded !== 'function') return; + arrivalsWatched = true; + s.db.onMsgAdded((ev) => { + const v = ev && ev.kvt && ev.kvt.value; + if (!v || v.author === s.id || typeof v.content !== 'string') return; + scheduleRecount(); + }, false); + }; + + const act = async (method, ...args) => { + await subscribe(); + const ph = await phone(); + if (!ph) throw new Error('unavailable'); + return asP(ph[method], ...args); + }; + + const finishPam = async () => { + const ph = await phone(); + if (!ph) return false; + const out = await asP(ph.recordStop); + if (!out || !out.file) return false; + try { + const enc = await encryptFile(out.file); + await pmModel.sendPam(out.peer, enc.cipher, Number(out.durationMs) / 1000); + return true; + } finally { + fs.unlink(out.file, () => {}); + } + }; + + const findPam = async (key) => (await pmModel.listPams()).find(m => m.key === key) || null; + + const byType = (s, type) => new Promise((resolve) => { + if (!s || typeof s.messagesByType !== 'function') return resolve([]); + try { pull(s.messagesByType({ type }), pull.collect((err, msgs) => resolve(err ? [] : msgs))); } catch (_) { resolve([]); } + }); + const defaultPubId = () => { + const m = String((require('../configs/config-manager').defaultInvite() || {}).code || '').match(/(@[A-Za-z0-9+/]{43}=\.ed25519)/); + return m ? m[1] : ''; + }; + let announcedPubs = new Set(); + const refreshPubs = async (force = false) => { + const ids = new Set(); + const add = (id) => { if (FEED_ID.test(String(id || ''))) ids.add(String(id)); }; + try { + const s = await open(); + if (force || Date.now() - pubsAt >= PUBS_TTL_MS) { + pubsAt = Date.now(); + const found = new Set(); + for (const m of await byType(s, 'pub')) { const v = m && m.value; const k = v && v.content && v.content.address && v.content.address.key; if (FEED_ID.test(String(k || '')) && (v.author === k || v.author === s.id)) found.add(String(k)); } + for (const m of await byType(s, 'pubAvailability')) { const k = m && m.value && m.value.author; if (FEED_ID.test(String(k || ''))) found.add(String(k)); } + announcedPubs = found; + } + add(defaultPubId()); + for (const id of announcedPubs) add(id); + try { const ph = s.phone; if (ph && typeof ph.pubs === 'function') for (const id of (await rpcValue(ph.pubs)) || []) add(id); } catch (_) {} + ids.delete(s.id); + } catch (_) { add(defaultPubId()); } + sharedState.setPubIds(ids); + return ids; + }; + const isPub = (id) => sharedState.isPubId(id); + + return { + async start() { + await subscribe(); + await watchArrivals().catch(() => {}); + await refreshCount(); + await refreshPubs(true); + if (!pubsTimer) { pubsTimer = setInterval(() => { refreshPubs(true).catch(() => {}); }, PUBS_TTL_MS); if (pubsTimer.unref) pubsTimer.unref(); } + }, + refreshCount, + refreshPubs, + isPub, + async available() { + if (isPublic) return false; + const ph = await phone(); + return !!(ph && await rpcValue(ph.available)); + }, + async state() { + await subscribe(); + const ph = await phone(); + return ph ? rpcValue(ph.state) : null; + }, + async call(to, opts = {}) { + await refreshPubs(); + const list = Array.isArray(to) ? to : [to]; + if (list.some(isPub)) throw new Error('pub'); + return act('call', list.length === 1 ? list[0] : list, { legacy: Array.isArray(opts.legacy) ? opts.legacy : [] }); + }, + accept: () => act('accept'), + reject: () => act('reject'), + hangup: () => act('end'), + mute: (flag) => act('mute', !!flag), + camera: (flag) => act('camera', !!flag), + async videoFrames(peer) { + const ph = await phone(); + return ph && typeof ph.videoFrames === 'function' ? ph.videoFrames({ peer: String(peer || '') }) : null; + }, + silence: (id, flag) => act('silence', { id, on: !!flag }), + dismiss: () => act('dismiss'), + recordCancel: () => act('recordCancel'), + async sendPam() { await subscribe(); return finishPam(); }, + history, + async markAllHeard(keys) { + const s = seen(); + const set = new Set(s.heard); + for (const k of keys || []) if (typeof k === 'string' && k) set.add(k); + s.heard = [...set].slice(-HEARD_MAX); + writeJson(seenFile, s); + refreshCount(); + }, + deleteHistory(ids) { + const drop = new Set((ids || []).map(String)); + if (!drop.size) return; + writeJson(historyFile, history().filter(h => !drop.has(String(h && h.id)))); + }, + latestCalls(items) { + const me = ssb && ssb.id; + const out = []; + const seenPeers = new Set(); + for (const h of items || history()) { + for (const peer of (Array.isArray(h.peers) && h.peers.length ? h.peers : [h.peer])) { + if (!peer || peer === me || seenPeers.has(peer) || isPub(peer)) continue; + seenPeers.add(peer); + out.push({ id: peer, dir: h.dir, outcome: h.outcome, startedAt: h.startedAt }); + if (out.length >= LATEST_MAX) return out; + } + } + return out; + }, + async pams() { + const me = (await open()).id; + const heard = new Set(seen().heard); + const validShare = (s) => !!s && typeof s === 'object' && typeof s.key === 'string' && typeof s.manifestBlobId === 'string'; + const list = (await pmModel.listPams()) + .filter(m => m.value.author !== me && validShare(m.value.content && m.value.content.share)) + .map(m => ({ key: m.key, from: m.value.author, sentAt: m.value.content.sentAt || new Date(m.timestamp || 0).toISOString(), durationSec: Number(m.value.content.durationSec) || 0, heard: heard.has(m.key), share: m.value.content.share, ready: true })) + .sort((a, b) => String(b.sentAt).localeCompare(String(a.sentAt))); + if (typeof isAvailable === 'function') { + for (const item of list) { + item.ready = await isAvailable(item.share).catch(() => false); + if (!item.ready && typeof prefetch === 'function') prefetch(item.share).catch(() => {}); + } + } + return list.map(({ share, ...rest }) => rest); + }, + async pamCipher(key) { + const m = await findPam(key); + const share = m && m.value && m.value.content && m.value.content.share; + if (!share || typeof share.key !== 'string' || typeof share.manifestBlobId !== 'string') return null; + return share; + }, + markHeard(key) { + const s = seen(); + if (s.heard.includes(key)) return false; + s.heard.push(key); + s.heard = s.heard.slice(-HEARD_MAX); + writeJson(seenFile, s); + refreshCount(); + return true; + }, + async deletePam(key) { + const m = await findPam(key); + if (!m) return null; + return pmModel.deleteMessageById(key); + }, + async knownFeeds() { + const s = await open(); + await refreshPubs(); + const hops = await new Promise((resolve) => { + try { s.friends.hops({ start: s.id, max: 2 }, (err, h) => resolve(err ? {} : (h || {}))); } catch (_) { resolve({}); } + }); + const out = new Map(); + for (const [id, hop] of Object.entries(hops)) { + if (id === s.id || isPub(id) || !(Number(hop) >= 0)) continue; + out.set(id, Number(hop)); + } + return out; + }, + markMissedSeen() { + const s = seen(); + s.missedSeenAt = Date.now(); + writeJson(seenFile, s); + refreshCount(); + } + }; +}; diff --git a/src/models/phone_number.js b/src/models/phone_number.js new file mode 100644 index 0000000..c15a46e --- /dev/null +++ b/src/models/phone_number.js @@ -0,0 +1,28 @@ +const crypto = require('crypto'); + +const FEED_ID = /^@[A-Za-z0-9+/]{43}=\.ed25519$/; +const MSG_ID = /^%[A-Za-z0-9+/]{43}=?\.sha256$/; + +const digitsOf = (id) => { + const n = crypto.createHash('sha256').update(String(id)).digest().readUInt32BE(0) % 1000000; + const s = String(n).padStart(6, '0'); + return `${s.slice(0, 3)}-${s.slice(3)}`; +}; +const phoneNumberOf = (id) => (FEED_ID.test(String(id || '')) ? digitsOf(id) : null); +const roomNumberOf = (id) => (MSG_ID.test(String(id || '')) ? digitsOf(id) : null); + +const normalizeNumber = (value) => { + const digits = String(value || '').replace(/[^0-9A-Za-z]/g, ''); + return /^\d{6}$/.test(digits) ? `${digits.slice(0, 3)}-${digits.slice(3)}` : null; +}; + +const resolveNumber = (value, known) => { + const number = normalizeNumber(value); + if (!number) return []; + return [...(known || new Map())] + .filter(([id]) => phoneNumberOf(id) === number) + .map(([id, hop]) => ({ id, hop: Number(hop) })) + .sort((a, b) => a.hop - b.hop); +}; + +module.exports = { phoneNumberOf, roomNumberOf, normalizeNumber, resolveNumber }; diff --git a/src/models/pixelia_model.js b/src/models/pixelia_model.js index 1b14854..04c91ff 100644 --- a/src/models/pixelia_model.js +++ b/src/models/pixelia_model.js @@ -1,8 +1,72 @@ const pull = require('../server/node_modules/pull-stream'); const { getConfig } = require('../configs/config-manager.js'); const { buildValidatedTombstoneSet } = require('./tombstone_validator'); +const { readTyped } = require('./typed_log'); const logLimit = getConfig().ssbLogStream?.limit || 1000; +const PIXELIA_TYPES = ['pixelia', 'tombstone']; +const COLOR_RE = /^#[0-9a-fA-F]{6}$/; + +const pixelCoordinate = (c) => { + if (!Number.isInteger(c.x) || !Number.isInteger(c.y) || c.x < 0 || c.x > 49 || c.y < 0 || c.y > 199) return null; + const key = `${c.x + 1}:${c.y + 1}`; + if (c.coordinateKey != null && c.coordinateKey !== key) return null; + return key; +}; + +const resolvePixels = (messages) => { + const tombstoned = buildValidatedTombstoneSet(messages); + const byKey = new Map(); + + for (const m of messages) { + const c = m.value?.content; + if (!c || c.type !== 'pixelia' || !m.key) continue; + const coordinateKey = pixelCoordinate(c); + if (!coordinateKey || typeof c.color !== 'string' || !COLOR_RE.test(c.color)) continue; + byKey.set(m.key, { m, coordinateKey }); + } + + const replaced = new Set(); + const parentOf = (k) => { + const { m, coordinateKey } = byKey.get(k); + const prev = byKey.get(m.value.content.replaces); + return prev && prev.coordinateKey === coordinateKey ? prev.m.key : null; + }; + for (const k of byKey.keys()) { + const parent = parentOf(k); + if (parent) replaced.add(parent); + } + + const depthOf = new Map(); + const depth = (key) => { + const chain = []; + const inChain = new Set(); + let k = key; + while (k && !depthOf.has(k) && !inChain.has(k)) { + chain.push(k); + inChain.add(k); + k = parentOf(k); + } + let d = k && depthOf.has(k) ? depthOf.get(k) + 1 : 0; + for (let i = chain.length - 1; i >= 0; i--) depthOf.set(chain[i], d++); + return depthOf.get(key); + }; + + const latest = new Map(); + for (const [k, { m, coordinateKey }] of byKey) { + if (replaced.has(k) || tombstoned.has(k)) continue; + const cur = latest.get(coordinateKey); + if (!cur) { latest.set(coordinateKey, m); continue; } + const d = depth(k); + const curD = depth(cur.key); + const ts = m.value.timestamp || 0; + const curTs = cur.value.timestamp || 0; + if (d > curD || (d === curD && (ts > curTs || (ts === curTs && k > cur.key)))) latest.set(coordinateKey, m); + } + + return latest; +}; + module.exports = ({ cooler }) => { let ssb; @@ -13,52 +77,22 @@ module.exports = ({ cooler }) => { const getPixelByCoordinate = async (coordinateKey) => { const ssbClient = await openSsb(); - const messages = await new Promise((res, rej) => { - pull( - ssbClient.createLogStream({ limit: logLimit }), - pull.collect((err, msgs) => err ? rej(err) : res(msgs)) - ); - }); - - const tombstoned = buildValidatedTombstoneSet(messages); - - const replaces = new Map(); - const byId = new Map(); - - for (const m of messages) { - const c = m.value?.content; - const k = m.key; - if (!c || c.type !== 'pixelia' || c.coordinateKey !== coordinateKey) continue; - if (tombstoned.has(k)) continue; - if (c.replaces) replaces.set(c.replaces, k); - byId.set(k, m); - } - - for (const [replacedId, replacingId] of replaces.entries()) { - const orig = byId.get(replacedId); - if (!orig) continue; - const rep = byId.get(replacingId); - if (!rep || rep.value.author !== orig.value.author) { - byId.delete(replacingId); - continue; - } - byId.delete(replacedId); - } - - return [...byId.values()][0] || null; + const messages = await readTyped(ssbClient, PIXELIA_TYPES, { limit: logLimit }); + return resolvePixels(messages).get(coordinateKey) || null; }; const paintPixel = async (x, y, color) => { - if (x < 1 || x > 50 || y < 1 || y > 200) { + if (!Number.isInteger(x) || !Number.isInteger(y) || x < 1 || x > 50 || y < 1 || y > 200) { throw new Error('Coordinates out of bounds. Please use x (1-50) and y (1-200)'); } + if (typeof color !== 'string' || !COLOR_RE.test(color)) throw new Error('Invalid color'); const ssbClient = await openSsb(); const userId = ssbClient.id; const coordinateKey = `${x}:${y}`; const existingPixel = await getPixelByCoordinate(coordinateKey); - if (existingPixel) { + if (existingPixel && existingPixel.value.author === userId) { const tombstone = { type: 'tombstone', target: existingPixel.key, @@ -69,7 +103,7 @@ module.exports = ({ cooler }) => { ); } - const contributors = existingPixel?.value?.content?.contributors_inhabitants || []; + const contributors = Array.isArray(existingPixel?.value?.content?.contributors_inhabitants) ? existingPixel.value.content.contributors_inhabitants : []; const contributors_inhabitants = contributors.includes(userId) ? contributors : [...contributors, userId]; @@ -93,46 +127,14 @@ module.exports = ({ cooler }) => { const listPixels = async () => { const ssbClient = await openSsb(); - const messages = await new Promise((res, rej) => { - pull( - ssbClient.createLogStream({ limit: logLimit }), - pull.collect((err, msgs) => err ? rej(err) : res(msgs)) - ); - }); + const messages = await readTyped(ssbClient, PIXELIA_TYPES, { limit: logLimit }); - const tombstoned = buildValidatedTombstoneSet(messages); - const replaces = new Map(); - const byKey = new Map(); - - for (const m of messages) { - const c = m.value?.content; - const k = m.key; - if (!c) continue; - if (c.type === 'tombstone') continue; - if (c.type === 'pixelia') { - if (tombstoned.has(k)) continue; - if (c.replaces) replaces.set(c.replaces, k); - byKey.set(k, m); - } - } - - for (const [replacedId, replacingId] of replaces.entries()) { - const orig = byKey.get(replacedId); - if (!orig) continue; - const rep = byKey.get(replacingId); - if (!rep || rep.value.author !== orig.value.author) { - byKey.delete(replacingId); - continue; - } - byKey.delete(replacedId); - } - - return Array.from(byKey.values()).map(m => ({ + return Array.from(resolvePixels(messages).values()).map(m => ({ x: m.value.content.x + 1, y: m.value.content.y + 1, color: m.value.content.color, author: m.value.author, - contributors_inhabitants: m.value.content.contributors_inhabitants || [], + contributors_inhabitants: [...new Set([...(Array.isArray(m.value.content.contributors_inhabitants) ? m.value.content.contributors_inhabitants : []).filter(id => typeof id === 'string'), m.value.author])], timestamp: m.value.timestamp })); }; diff --git a/src/models/pm_model.js b/src/models/pm_model.js index 03be1d3..3bbd6c1 100644 --- a/src/models/pm_model.js +++ b/src/models/pm_model.js @@ -1,5 +1,89 @@ const pull = require('../server/node_modules/pull-stream'); -const util = require('../server/node_modules/util'); +const util = require('util'); +const longText = require('../backend/long_text'); + +const fs = require('fs'); + +const INBOX_READ_FILE = 'oasis-inbox-read'; +const INBOX_ARCHIVE_FILE = 'oasis-inbox-archived'; +const flagPath = (name) => { + try { return require('../configs/state-manager').statePath(name); } catch (_) { return null; } +}; +const readFlagSet = (name) => { + const file = flagPath(name); + if (!file) return new Set(); + try { const arr = JSON.parse(fs.readFileSync(file, 'utf8')); return new Set(Array.isArray(arr) ? arr.map(String) : []); } catch (_) { return new Set(); } +}; +const writeFlagSet = (name, set) => { + const file = flagPath(name); + if (!file) return false; + try { fs.writeFileSync(file, JSON.stringify(Array.from(set))); return true; } catch (_) { return false; } +}; +const readKeys = () => readFlagSet(INBOX_READ_FILE); +const markRead = (key) => { if (!key) return false; const set = readKeys(); set.add(String(key)); return writeFlagSet(INBOX_READ_FILE, set); }; +const markUnread = (key) => { if (!key) return false; const set = readKeys(); set.delete(String(key)); return writeFlagSet(INBOX_READ_FILE, set); }; +const markReadMany = (keys) => { const set = readKeys(); for (const k of (keys || [])) { if (k) set.add(String(k)); } return writeFlagSet(INBOX_READ_FILE, set); }; +const archivedKeys = () => readFlagSet(INBOX_ARCHIVE_FILE); +const archive = (key) => { if (!key) return false; const set = archivedKeys(); set.add(String(key)); return writeFlagSet(INBOX_ARCHIVE_FILE, set); }; +const unarchive = (key) => { if (!key) return false; const set = archivedKeys(); set.delete(String(key)); return writeFlagSet(INBOX_ARCHIVE_FILE, set); }; + +const INBOX_BOTS = { + blogs: ['BLOG_NEW'], + jobs: ['JOB_MATCH', 'JOB_SUBSCRIBED', 'JOB_UNSUBSCRIBED'], + projects: ['PROJECT_FOLLOWED', 'PROJECT_UNFOLLOWED', 'PROJECT_PLEDGE'], + market: ['MARKET_SOLD'], + shops: ['SHOP_SOLD'], + political: ['LARP_RULING', 'PARLIAMENT_GOV', 'TRIBE_GOV'], + banking: ['BANKING_UBI_PAID', 'BANKING_UBI_AVAILABLE', 'BANKING_CONFIRM_PENDING', 'WALLET_PAYMENT'], + school: ['SCHOOL_ENROLLED', 'SCHOOL_INVITED', 'SCHOOL_ADMITTED', 'SCHOOL_CERTIFICATE', 'SCHOOL_PASSED', 'SCHOOL_LESSON_NEW'], + industry: ['INDUSTRY_ADMITTED', 'INDUSTRY_APPLICATION', 'INDUSTRY_INVITED', 'INDUSTRY_DISSOLVED', 'INDUSTRY_BUILD_APPROVED', 'INDUSTRY_DISTRIBUTED'], + housing: ['HOUSING_REQUESTED', 'HOUSING_CANCELLED', 'HOUSING_UNAVAILABLE'], + wiki: ['WIKI_EDITED', 'WIKI_RESTORED'], + emergencies: ['EMERGENCY_UPDATED', 'EMERGENCY_RESOLVED'], + podcasts: ['PODCAST_EPISODE'], + campaigns: ['CAMPAIGN_UPDATED', 'CAMPAIGN_ACHIEVED', 'CAMPAIGN_RAISED'], + logistics: ['LOGISTICS_UPDATED', 'LOGISTICS_CLOSED', 'LOGISTICS_BOOKED', 'LOGISTICS_CANCELLED', 'LOGISTICS_CONFIRMED', 'LOGISTICS_REJECTED', 'LOGISTICS_DELIVERED'] +}; +const BOT_BY_SUBJECT = new Map(); +for (const [bot, subjects] of Object.entries(INBOX_BOTS)) for (const s of subjects) BOT_BY_SUBJECT.set(s, bot); +const isReminderSubject = (s) => /^(Task Reminder:|Calendar Reminder:)/i.test(String(s || '')); +const botOf = (content) => { + const c = content || {}; + if (isReminderSubject(c.subject)) return 'reminders'; + if (c.meta && c.meta.type === 'project-pledge') return 'projects'; + return BOT_BY_SUBJECT.get(String(c.subject || '').toUpperCase()) || null; +}; +const mutedBots = (cfg) => { + const c = cfg || {}; + return new Set(Array.isArray(c.inboxMutedBots) ? c.inboxMutedBots.map(String) : []); +}; + +const privateCaches = new WeakMap(); +const privateCacheFor = (ssb, userId) => { + let byUser = privateCaches.get(ssb); + if (!byUser) { byUser = new Map(); privateCaches.set(ssb, byUser); } + let c = byUser.get(userId); + if (!c) { + c = { lastTs: 0, seen: new Set(), posts: new Map(), pams: new Map(), chunks: new Map(), tombClaims: new Map(), authorByKey: new Map(), recpsByKey: new Map(), version: 0 }; + byUser.set(userId, c); + } + return c; +}; + +const tombedKeys = (cache, userId) => { + const tombed = new Set(); + for (const [target, tombAuthors] of cache.tombClaims.entries()) { + const origAuthor = cache.authorByKey.get(target); + const origRecps = cache.recpsByKey.get(target) || []; + for (const tombAuthor of tombAuthors) { + if (tombAuthor === origAuthor || tombAuthor === userId || origRecps.includes(tombAuthor)) { + tombed.add(target); + break; + } + } + } + return tombed; +}; module.exports = ({ cooler }) => { let ssb; @@ -26,12 +110,35 @@ module.exports = ({ cooler }) => { } return { + readKeys, + markRead, + markUnread, + markReadMany, + archivedKeys, + archive, + unarchive, + botOf, + mutedBots, + INBOX_BOTS, type: 'post', - async sendMessage(recipients = [], subject = '', text = '', crypter = false) { + async sendToMany(recipients = [], subject = '', text = '', crypter = false, ref = '') { + const others = uniqueRecps(recipients).filter(id => id !== userId); + if (!others.length) return []; + const out = []; + for (let i = 0; i < others.length; i += 6) { + const batch = others.slice(i, i + 6); + const r = await this.sendMessage(batch, subject, text, crypter, ref).catch(() => null); + if (r) out.push(r); + } + return out; + }, + + async sendMessage(recipients = [], subject = '', text = '', crypter = false, ref = '') { const ssbClient = await openSsb(); const recps = uniqueRecps([userId, ...recipients]); - const content = { + const publishAsync = util.promisify(ssbClient.private.publish); + const content = await longText.chunkContent({ type: 'post', from: userId, to: recps, @@ -39,6 +146,38 @@ module.exports = ({ cooler }) => { text, sentAt: new Date().toISOString(), private: true, + ...(ref ? { ref: String(ref) } : {}), + ...(crypter ? { crypter: true } : {}) + }, 'text', { maxBytes: longText.CONTENT_BYTES.boxed, extra: { private: true }, publish: (c) => publishAsync(c, recps) }); + return publishAsync(content, recps); + }, + + async sentRefs() { + const out = new Set(); + let messages = []; + try { messages = await this.listAllPrivate({ includeDeleted: true }); } catch (_) { return out; } + for (const m of (messages || [])) { + const c = m && m.value && m.value.content; + if (!c || !c.ref) continue; + if (m.value.author !== userId) continue; + out.add(`${String(c.subject || '')}|${String(c.ref)}`); + } + return out; + }, + + async sendFileShare(recipients = [], subject = '', fileShare = null, crypter = false) { + const ssbClient = await openSsb(); + const recps = uniqueRecps([userId, ...recipients]); + if (!fileShare || typeof fileShare !== 'object') throw new Error('Invalid file share'); + const content = { + type: 'post', + from: userId, + to: recps, + subject, + text: '', + sentAt: new Date().toISOString(), + private: true, + fileShare, ...(crypter ? { crypter: true } : {}) }; const publishAsync = util.promisify(ssbClient.private.publish); @@ -83,22 +222,23 @@ module.exports = ({ cooler }) => { return publishAsync(tombstone, tombstoneRecps); }, - async listAllPrivate() { + async listAllPrivate(opts = {}) { + const includeDeleted = !!(opts && opts.includeDeleted); const ssbClient = await openSsb(); + const cache = privateCacheFor(ssbClient, userId); const raw = await new Promise((resolve, reject) => { pull( - ssbClient.createLogStream({ reverse: false }), + ssbClient.createLogStream(cache.lastTs > 0 ? { reverse: false, gte: cache.lastTs } : { reverse: false }), pull.collect((err, arr) => err ? reject(err) : resolve(arr)) ); }); - const posts = []; - const tombed = new Set(); - const tombClaims = new Map(); - const authorByKey = new Map(); - const recpsByKey = new Map(); for (const m of raw) { if (!m || !m.value) continue; const keyIn = m.key || m.value?.key || m.value?.hash || ''; + if (!keyIn || cache.seen.has(keyIn)) continue; + cache.seen.add(keyIn); + const recvTs = Number(m.timestamp) || 0; + if (recvTs > cache.lastTs) cache.lastTs = recvTs; const valueIn = m.value || m; const tsIn = m.timestamp || m.value?.timestamp || Date.now(); let dec; @@ -112,18 +252,23 @@ module.exports = ({ cooler }) => { const k = dec?.key || keyIn; if (!c || c.private !== true || !k) continue; if (c.type === 'tombstone' && c.target) { - const set = tombClaims.get(c.target) || new Set(); + const set = cache.tombClaims.get(c.target) || new Set(); set.add(v.author); - tombClaims.set(c.target, set); + cache.tombClaims.set(c.target, set); continue; } - authorByKey.set(k, v.author); - if (c.type === 'post') { + if (c.type === longText.CHUNK_TYPE) { + const ch = longText.chunkOf(v.author, c); + if (ch) cache.chunks.set(k, ch); + continue; + } + cache.authorByKey.set(k, v.author); + if (c.type === 'post' || c.type === 'pam') { const to = Array.isArray(c.to) ? c.to : []; - recpsByKey.set(k, to); + cache.recpsByKey.set(k, to); const author = v.author; if (author === userId || to.includes(userId)) { - posts.push({ + (c.type === 'pam' ? cache.pams : cache.posts).set(k, { key: k, value: { author, content: c }, timestamp: v.timestamp || tsIn @@ -131,17 +276,48 @@ module.exports = ({ cooler }) => { } } } - for (const [target, tombAuthors] of tombClaims.entries()) { - const origAuthor = authorByKey.get(target); - const origRecps = recpsByKey.get(target) || []; - for (const tombAuthor of tombAuthors) { - if (tombAuthor === origAuthor || tombAuthor === userId || origRecps.includes(tombAuthor)) { - tombed.add(target); - break; - } - } - } - return posts.filter(m => m && m.key && !tombed.has(m.key)); + cache.version += raw.length ? 1 : 0; + const tombed = tombedKeys(cache, userId); + const lookup = longText.lookupIn(cache.chunks); + return Array.from(cache.posts.values()) + .filter(m => m && m.key && (includeDeleted || !tombed.has(m.key))) + .map(m => longText.hasChunks(m.value.content) + ? { ...m, value: { ...m.value, content: longText.resolveField(m.value.content, 'text', m.value.author, lookup) } } + : m); + }, + + async listPams() { + await this.listAllPrivate(); + const cache = privateCacheFor(ssb, userId); + const tombed = tombedKeys(cache, userId); + return Array.from(cache.pams.values()).filter(m => m && m.key && !tombed.has(m.key)); + }, + + async sendPam(recipient, share, durationSec) { + const ssbClient = await openSsb(); + if (!share || typeof share !== 'object') throw new Error('Invalid audio'); + const recps = uniqueRecps([userId, recipient]); + const content = { + type: 'pam', + from: userId, + to: recps, + share, + durationSec: Math.max(0, Math.round(Number(durationSec) || 0)), + sentAt: new Date().toISOString(), + private: true + }; + const publishAsync = util.promisify(ssbClient.private.publish); + return publishAsync(content, recps); + }, + + privateCacheVersion() { + return ssb ? privateCacheFor(ssb, userId).version : 0; } }; }; + +module.exports.botOf = botOf; +module.exports.mutedBots = mutedBots; +module.exports.INBOX_BOTS = INBOX_BOTS; +module.exports.readKeys = readKeys; +module.exports.archivedKeys = archivedKeys; diff --git a/src/models/podcasts_model.js b/src/models/podcasts_model.js new file mode 100644 index 0000000..5d0d6a7 --- /dev/null +++ b/src/models/podcasts_model.js @@ -0,0 +1,329 @@ +const { readTyped, memoIndex } = require('./typed_log'); +const { buildValidatedTombstoneSet } = require('./tombstone_validator'); +const { getConfig } = require('../configs/config-manager.js'); +const categories = require('../backend/opinion_categories'); +const logLimit = getConfig().ssbLogStream?.limit || 1000; + +const CHANNEL_TYPE = 'podcast'; +const EPISODE_TYPE = 'podcastEpisode'; +const OPINION_TYPE = 'podcastOpinion'; +const PLAY_TYPE = 'podcastPlay'; +const TYPES = [CHANNEL_TYPE, EPISODE_TYPE, OPINION_TYPE, PLAY_TYPE, 'tombstone']; +const CATEGORIES = ['NEWS', 'MUSIC', 'TALK', 'EDUCATION', 'TECH', 'CULTURE', 'COMMUNITY', 'OASIS', 'OTHER']; +const RECENT_MS = 24 * 60 * 60 * 1000; +const MEDIA_RE = /!?\[(image|audio|video):([^\]]*)\]\((&[^)]+)\)/; + +const safeText = (v) => String(v == null ? '' : v).trim(); +const normU = (v) => String(v || '').trim().toUpperCase(); +const normalizeCategory = (v) => (CATEGORIES.includes(normU(v)) ? normU(v) : 'OTHER'); +const normalizeTags = (raw) => { + if (!raw) return []; + const arr = Array.isArray(raw) ? raw : String(raw).split(','); + return Array.from(new Set(arr.map(t => String(t || '').trim()).filter(Boolean))); +}; +const num = (v) => { const n = Number(v); return Number.isFinite(n) && n > 0 ? Math.floor(n) : 0; }; + +const parseMedia = (raw) => { + if (!raw) return null; + if (typeof raw === 'object' && raw.blobId) return { kind: String(raw.kind || 'audio'), name: safeText(raw.name), blobId: String(raw.blobId) }; + const m = String(raw).match(MEDIA_RE); + if (!m) return null; + return { kind: m[1], name: safeText(m[2]) || 'file', blobId: m[3] }; +}; + +module.exports = ({ cooler }) => { + let ssb; + const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb; }; + const publish = (ssbClient, content) => new Promise((resolve, reject) => ssbClient.publish(content, (err, res) => err ? reject(err) : resolve(res))); + + const buildIndex = (messages) => { + const tomb = buildValidatedTombstoneSet(messages); + const nodes = new Map(); + const parentOf = new Map(); + const opinions = []; + const plays = []; + for (const m of messages) { + const v = m.value || {}; + const c = v.content; + if (!c || typeof c !== 'object') continue; + if (c.type === CHANNEL_TYPE || c.type === EPISODE_TYPE) { + nodes.set(m.key, { key: m.key, type: c.type, author: v.author, ts: v.timestamp || m.timestamp || 0, c }); + if (typeof c.replaces === 'string') parentOf.set(m.key, c.replaces); + } else if (c.type === OPINION_TYPE && typeof c.target === 'string') { + opinions.push({ target: c.target, author: v.author, category: String(c.category || ''), ts: v.timestamp || 0 }); + } else if (c.type === PLAY_TYPE && typeof c.target === 'string') { + plays.push({ target: c.target, author: v.author, ts: v.timestamp || 0 }); + } + } + const rootOf = (key) => { + const seen = new Set(); + let cur = key; + while (parentOf.has(cur) && !seen.has(cur)) { seen.add(cur); cur = parentOf.get(cur); } + return cur; + }; + const versionsByRoot = new Map(); + for (const node of nodes.values()) { + const root = rootOf(node.key); + if (tomb.has(root)) continue; + if (!versionsByRoot.has(root)) versionsByRoot.set(root, []); + versionsByRoot.get(root).push(node); + } + const tips = new Map(); + for (const [root, versions] of versionsByRoot) { + versions.sort((a, b) => a.ts - b.ts); + const first = versions[0]; + const own = versions.filter(v => v.author === first.author && v.type === first.type); + tips.set(root, { root, first, tip: own[own.length - 1] }); + } + const episodes = new Map(); + const opinionsByRoot = new Map(); + for (const op of opinions) { + if (!nodes.has(op.target)) continue; + const r = rootOf(op.target); + if (!opinionsByRoot.has(r)) opinionsByRoot.set(r, []); + opinionsByRoot.get(r).push(op); + } + const playsByRoot = new Map(); + for (const pl of plays) { + if (!nodes.has(pl.target)) continue; + const r = rootOf(pl.target); + if (!playsByRoot.has(r)) playsByRoot.set(r, new Set()); + playsByRoot.get(r).add(pl.author); + } + for (const { root, first, tip } of tips.values()) { + if (first.type !== EPISODE_TYPE) continue; + const c = tip.c; + const channelId = safeText(c.channel); + if (!channelId) continue; + const ops = {}; + const voters = []; + for (const op of (opinionsByRoot.get(root) || [])) { + if (voters.includes(op.author) || !categories.includes(op.category)) continue; + voters.push(op.author); + ops[op.category] = (ops[op.category] || 0) + 1; + } + const listeners = Array.from(playsByRoot.get(root) || []); + episodes.set(root, { + id: root, rootId: root, tipId: tip.key, channelId, + number: num(c.number), title: safeText(c.title), description: safeText(c.description), + media: parseMedia(c.media), tags: normalizeTags(c.tags), + author: first.author, createdAt: first.c.createdAt || new Date(first.ts).toISOString(), + updatedAt: c.updatedAt || new Date(tip.ts).toISOString(), ts: tip.ts, publishedTs: Date.parse(first.c.createdAt || '') || first.ts, + opinions: ops, opinions_inhabitants: voters, opinionCount: voters.length, + listeners, playCount: listeners.length + }); + } + const channels = new Map(); + for (const { root, first, tip } of tips.values()) { + if (first.type !== CHANNEL_TYPE) continue; + const c = tip.c; + const eps = Array.from(episodes.values()).filter(e => e.channelId === root && e.author === first.author).sort((a, b) => a.publishedTs - b.publishedTs || a.number - b.number); + eps.forEach((e, i) => { if (!e.number) e.number = i + 1; }); + const playCount = eps.reduce((s, e) => s + e.playCount, 0); + const ops = {}; + const voters = []; + for (const op of (opinionsByRoot.get(root) || [])) { + if (voters.includes(op.author) || !categories.includes(op.category)) continue; + voters.push(op.author); + ops[op.category] = (ops[op.category] || 0) + 1; + } + const opinionCount = voters.length + eps.reduce((s, e) => s + e.opinionCount, 0); + channels.set(root, { + id: root, rootId: root, tipId: tip.key, + title: safeText(c.title), description: safeText(c.description), category: normalizeCategory(c.category), + cover: parseMedia(c.cover), tags: normalizeTags(c.tags), + author: first.author, createdAt: first.c.createdAt || new Date(first.ts).toISOString(), + updatedAt: c.updatedAt || new Date(tip.ts).toISOString(), ts: tip.ts, + episodes: eps, episodeCount: eps.length, playCount, opinionCount, + opinions: ops, opinions_inhabitants: voters, + lastEpisodeTs: eps.length ? eps[eps.length - 1].publishedTs : 0, + lastActivityTs: Math.max(tip.ts, ...eps.map(e => e.ts)) + }); + } + return { channels, episodes, rootOf, nodes }; + }; + + const load = async () => { + const ssbClient = await openSsb(); + const idx = memoIndex('podcasts', await readTyped(ssbClient, TYPES, { limit: logLimit }), buildIndex); + return { ssbClient, idx }; + }; + + const findChannel = (idx, id) => { + const key = String(id || ''); + if (idx.channels.has(key)) return idx.channels.get(key); + if (idx.nodes.has(key)) return idx.channels.get(idx.rootOf(key)) || null; + return null; + }; + const findEpisode = (idx, id) => { + const key = String(id || ''); + if (idx.episodes.has(key)) return idx.episodes.get(key); + if (idx.nodes.has(key)) return idx.episodes.get(idx.rootOf(key)) || null; + return null; + }; + + return { + CATEGORIES, + parseMedia, + + async createChannel({ title, description, category, cover, tags }) { + const ssbClient = await openSsb(); + const t = safeText(title); + if (!t) throw new Error('Title is required'); + const now = new Date().toISOString(); + const media = parseMedia(cover); + return publish(ssbClient, { + type: CHANNEL_TYPE, title: t, description: safeText(description), category: normalizeCategory(category), + ...(media ? { cover: media } : {}), tags: normalizeTags(tags), author: ssbClient.id, createdAt: now, updatedAt: now + }); + }, + + async updateChannel(id, data = {}) { + const { ssbClient, idx } = await load(); + const ch = findChannel(idx, id); + if (!ch) throw new Error('Podcast not found'); + if (ch.author !== ssbClient.id) throw new Error('Only the author can update this podcast'); + const media = data.cover !== undefined && data.cover ? parseMedia(data.cover) : ch.cover; + const res = await publish(ssbClient, { + type: CHANNEL_TYPE, + title: safeText(data.title) || ch.title, + description: data.description !== undefined ? safeText(data.description) : ch.description, + category: data.category !== undefined ? normalizeCategory(data.category) : ch.category, + ...(media ? { cover: media } : {}), + tags: data.tags !== undefined ? normalizeTags(data.tags) : ch.tags, + author: ssbClient.id, createdAt: ch.createdAt, updatedAt: new Date().toISOString(), replaces: ch.tipId + }); + return { key: res.key, rootId: ch.id }; + }, + + async deleteChannel(id) { + const { ssbClient, idx } = await load(); + const ch = findChannel(idx, id); + if (!ch) throw new Error('Podcast not found'); + if (ch.author !== ssbClient.id) throw new Error('Only the author can delete this podcast'); + await publish(ssbClient, { type: 'tombstone', target: ch.id, deletedAt: new Date().toISOString(), author: ssbClient.id }); + return { key: ch.id }; + }, + + async addEpisode(channelId, { title, description, media, number, tags }) { + const { ssbClient, idx } = await load(); + const ch = findChannel(idx, channelId); + if (!ch) throw new Error('Podcast not found'); + if (ch.author !== ssbClient.id) throw new Error('Only the author can publish episodes'); + const m = parseMedia(media); + if (!m || (m.kind !== 'audio' && m.kind !== 'video')) throw new Error('An audio or video file is required'); + const t = safeText(title); + if (!t) throw new Error('Title is required'); + const now = new Date().toISOString(); + const res = await publish(ssbClient, { + type: EPISODE_TYPE, channel: ch.id, number: num(number) || ch.episodeCount + 1, + title: t, description: safeText(description), media: m, tags: normalizeTags(tags), + author: ssbClient.id, createdAt: now, updatedAt: now + }); + return { key: res.key, rootId: res.key, channelId: ch.id }; + }, + + async updateEpisode(id, data = {}) { + const { ssbClient, idx } = await load(); + const ep = findEpisode(idx, id); + if (!ep) throw new Error('Episode not found'); + if (ep.author !== ssbClient.id) throw new Error('Only the author can update this episode'); + const m = data.media ? parseMedia(data.media) : ep.media; + const res = await publish(ssbClient, { + type: EPISODE_TYPE, channel: ep.channelId, number: data.number !== undefined && num(data.number) ? num(data.number) : ep.number, + title: safeText(data.title) || ep.title, description: data.description !== undefined ? safeText(data.description) : ep.description, + media: m, tags: data.tags !== undefined ? normalizeTags(data.tags) : ep.tags, + author: ssbClient.id, createdAt: ep.createdAt, updatedAt: new Date().toISOString(), replaces: ep.tipId + }); + return { key: res.key, rootId: ep.id, channelId: ep.channelId }; + }, + + async deleteEpisode(id) { + const { ssbClient, idx } = await load(); + const ep = findEpisode(idx, id); + if (!ep) throw new Error('Episode not found'); + if (ep.author !== ssbClient.id) throw new Error('Only the author can delete this episode'); + await publish(ssbClient, { type: 'tombstone', target: ep.id, deletedAt: new Date().toISOString(), author: ssbClient.id }); + return { key: ep.id, channelId: ep.channelId }; + }, + + async markPlayed(id) { + const { ssbClient, idx } = await load(); + const ep = findEpisode(idx, id); + if (!ep) throw new Error('Episode not found'); + if (ep.listeners.includes(ssbClient.id)) return null; + return publish(ssbClient, { type: PLAY_TYPE, target: ep.id, createdAt: new Date().toISOString() }); + }, + + async createOpinion(id, category) { + if (!categories.includes(category)) throw new Error('Invalid voting category'); + const { ssbClient, idx } = await load(); + const target = findEpisode(idx, id) || findChannel(idx, id); + if (!target) throw new Error('Episode not found'); + if (target.opinions_inhabitants.includes(ssbClient.id)) throw new Error('Already voted'); + return publish(ssbClient, { type: OPINION_TYPE, target: target.id, category, createdAt: new Date().toISOString() }); + }, + + async getChannelById(id) { + const { idx } = await load(); + return findChannel(idx, id); + }, + + async getAnyById(id) { + const { idx } = await load(); + return findChannel(idx, id) || findEpisode(idx, id); + }, + + async getEpisodeById(id) { + const { idx } = await load(); + const ep = findEpisode(idx, id); + if (!ep) return null; + return { ...ep, channel: idx.channels.get(ep.channelId) || null }; + }, + + async listAll({ filter = 'all', q = '', category = '', viewerId = null } = {}) { + const { ssbClient, idx } = await load(); + const me = viewerId || ssbClient.id; + const f = String(filter || 'all').toLowerCase(); + const needle = String(q || '').trim().toLowerCase(); + const cat = normU(category); + let out = Array.from(idx.channels.values()); + if (f === 'mine') out = out.filter(c => c.author === me); + else if (f === 'recent') out = out.filter(c => c.lastActivityTs >= Date.now() - RECENT_MS); + else if (f === 'viewers') out = out.filter(c => c.playCount > 0); + else if (f === 'top') out = out.filter(c => c.opinionCount > 0 || (c.spreadCount || 0) > 0); + else if (CATEGORIES.includes(f.toUpperCase())) out = out.filter(c => c.category === f.toUpperCase()); + if (cat && CATEGORIES.includes(cat)) out = out.filter(c => c.category === cat); + if (needle) out = out.filter(c => [c.title, c.description, c.category, ...c.tags, ...c.episodes.map(e => e.title)].some(v => String(v || '').toLowerCase().includes(needle))); + if (f === 'viewers') out.sort((a, b) => b.playCount - a.playCount || b.lastActivityTs - a.lastActivityTs); + else if (f === 'top') out.sort((a, b) => b.opinionCount - a.opinionCount || b.playCount - a.playCount); + else out.sort((a, b) => b.lastActivityTs - a.lastActivityTs); + return out; + }, + + async listEpisodes({ filter = 'all', q = '' } = {}) { + const { ssbClient, idx } = await load(); + const me = ssbClient.id; + const f = String(filter || 'all').toLowerCase(); + const needle = String(q || '').trim().toLowerCase(); + let out = Array.from(idx.episodes.values()).filter(e => idx.channels.has(e.channelId)).map(e => ({ ...e, channel: idx.channels.get(e.channelId) })); + if (f === 'mine') out = out.filter(e => e.author === me); + else if (f === 'recent') out = out.filter(e => e.ts >= Date.now() - RECENT_MS); + else if (f === 'viewers') out = out.filter(e => e.playCount > 0); + else if (f === 'top') out = out.filter(e => e.opinionCount > 0); + if (needle) out = out.filter(e => [e.title, e.description, ...e.tags, e.channel.title].some(v => String(v || '').toLowerCase().includes(needle))); + if (f === 'viewers') out.sort((a, b) => b.playCount - a.playCount); + else if (f === 'top') out.sort((a, b) => b.opinionCount - a.opinionCount); + else out.sort((a, b) => b.publishedTs - a.publishedTs); + return out; + }, + + async resolveRootId(id) { + const { idx } = await load(); + const ch = findChannel(idx, id); + if (ch) return ch.id; + const ep = findEpisode(idx, id); + return ep ? ep.id : String(id || ''); + } + }; +}; diff --git a/src/models/polls_model.js b/src/models/polls_model.js new file mode 100644 index 0000000..c61a5c2 --- /dev/null +++ b/src/models/polls_model.js @@ -0,0 +1,425 @@ +const pull = require('../server/node_modules/pull-stream'); +const { getConfig } = require('../configs/config-manager.js'); +const { buildValidatedTombstoneSet } = require('./tombstone_validator'); +const { readTyped, memoIndex } = require('./typed_log'); +const { versionGroups } = require('../backend/dedupe'); + +const logLimit = getConfig().ssbLogStream?.limit || 1000; + +const POLL_TYPE = 'poll'; +const VOTE_TYPE = 'pollVote'; +const OPINION_TYPE = 'pollOpinion'; + +const { MAX_OPTIONS, MIN_OPTIONS, MAX_OPTION_LENGTH } = require('./polls_model_limits'); + +const clean = (v) => String(v == null ? '' : v).trim(); + +const normalizeOptions = (raw) => { + const list = Array.isArray(raw) ? raw : String(raw || '').split('\n'); + const out = []; + const seen = new Set(); + for (const entry of list) { + const value = clean(entry).slice(0, MAX_OPTION_LENGTH); + if (!value) continue; + const key = value.toLowerCase(); + if (seen.has(key)) continue; + seen.add(key); + out.push(value); + if (out.length >= MAX_OPTIONS) break; + } + return out; +}; + +const normalizeTags = (raw) => { + const list = Array.isArray(raw) ? raw : String(raw || '').split(','); + return list.map(t => clean(t)).filter(Boolean).slice(0, 20); +}; + +module.exports = ({ cooler, isPublic = false, tribeCrypto = null, chatsModel = null }) => { + let ssb; + const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb; }; + + const POLLS_TYPES = [POLL_TYPE, VOTE_TYPE, OPINION_TYPE, 'pollClose', 'post', 'tombstone']; + + const getAllMessages = async (ssbClient) => readTyped(ssbClient, POLLS_TYPES, { limit: logLimit }); + + const decryptField = (value, keys) => { + if (!tribeCrypto || typeof value !== 'string' || !value) return null; + for (const k of keys) { + try { + const out = tribeCrypto.decryptWithKey(value, k); + if (typeof out === 'string' && out) return out; + } catch (_) {} + } + return null; + }; + + const scopeKey = async (scope) => { + if (!scope || !chatsModel) return null; + if (!scope.chatId && !scope.tribeId) return null; + try { return await chatsModel.encryptionKeyFor(scope.chatId || null, scope.tribeId || null); } catch (_) { return null; } + }; + + const buildIndex = (messages) => { + const tomb = buildValidatedTombstoneSet(messages); + const nodes = new Map(); + const votesByRoot = new Map(); + const opinionsByRoot = new Map(); + const closedRoots = new Set(); + const commentsByRoot = new Map(); + const others = []; + + for (const m of messages) { + const v = m && m.value; + const c = v && v.content; + if (!c || typeof c !== 'object') continue; + if (c.type === POLL_TYPE) nodes.set(m.key, { key: m.key, author: v.author, seq: v.sequence || 0, ts: v.timestamp || m.timestamp || 0, c }); + else others.push(m); + } + + const vg = versionGroups(Array.from(nodes.values()).map(n => ({ key: n.key, author: n.author, seq: n.seq, ts: n.ts, replaces: typeof n.c.replaces === 'string' ? n.c.replaces : null, claimedAuthor: typeof n.c.author === 'string' ? n.c.author : null }))); + const rootOf = vg.rootOf; + const tipOf = vg.tipOf; + + for (const m of others) { + const v = m.value; + const c = v.content; + if (c.type === VOTE_TYPE && typeof c.target === 'string') { + const target = rootOf(c.target); + const entry = votesByRoot.get(target) || new Map(); + const list = entry.get(v.author) || []; + list.push({ + ts: v.timestamp || m.timestamp || 0, + choices: Array.isArray(c.choices) ? c.choices : [], + encryptedChoices: typeof c.encryptedChoices === 'string' ? c.encryptedChoices : null + }); + entry.set(v.author, list); + votesByRoot.set(target, entry); + continue; + } + if (c.type === OPINION_TYPE && typeof c.target === 'string') { + const target = rootOf(c.target); + const entry = opinionsByRoot.get(target) || { counts: {}, voters: [] }; + if (!entry.voters.includes(v.author)) { + entry.voters.push(v.author); + entry.counts[c.category] = (entry.counts[c.category] || 0) + 1; + } + opinionsByRoot.set(target, entry); + continue; + } + if (c.type === 'pollClose' && typeof c.target === 'string') { + closedRoots.add(`${v.author}|${rootOf(c.target)}`); + continue; + } + if (c.type === 'post' && typeof c.root === 'string') { + const target = rootOf(c.root); + commentsByRoot.set(target, (commentsByRoot.get(target) || 0) + 1); + } + } + + return { tomb, nodes, vg, votesByRoot, opinionsByRoot, closedRoots, commentsByRoot, rootOf, tipOf }; + }; + + const isExpired = (c) => { + if (!c.deadline) return false; + const t = Date.parse(c.deadline); + return Number.isFinite(t) && t <= Date.now(); + }; + + const buildPoll = (idx, rootId, viewerId, keys = []) => { + const tipId = idx.tipOf(rootId); + const node = idx.nodes.get(tipId); + if (!node) return null; + const c = node.c || {}; + let question = clean(c.question); + let rawOptions = c.options; + let undecryptable = false; + if (c.encryptedQuestion) { + const q = decryptField(c.encryptedQuestion, keys); + const o = decryptField(c.encryptedOptions, keys); + if (q === null) undecryptable = true; + question = q || ''; + try { rawOptions = o ? JSON.parse(o) : []; } catch (_) { rawOptions = []; } + } + const options = normalizeOptions(rawOptions); + const opensAt = (idx.nodes.get(rootId) || {}).ts || 0; + const closesAt = c.deadline ? Date.parse(c.deadline) : NaN; + const voteMap = new Map(); + for (const [author, list] of (idx.votesByRoot.get(rootId) || new Map()).entries()) { + const inWindow = list.filter(b => b.ts >= opensAt && (!Number.isFinite(closesAt) || b.ts <= closesAt)); + if (inWindow.length) voteMap.set(author, inWindow.reduce((a, b) => (b.ts >= a.ts ? b : a))); + } + + const counts = {}; + for (const o of options) counts[o] = 0; + let totalVoters = 0; + const voters = []; + let myChoices = []; + for (const [author, entry] of voteMap.entries()) { + let choices = Array.isArray(entry.choices) ? entry.choices : []; + if (entry.encryptedChoices) { + const plain = decryptField(entry.encryptedChoices, keys); + try { choices = plain ? JSON.parse(plain) : []; } catch (_) { choices = []; } + } + const picked = (Array.isArray(choices) ? choices : []).filter(o => options.includes(o)); + if (!picked.length) continue; + totalVoters += 1; + voters.push(author); + for (const o of picked) counts[o] += 1; + if (viewerId && author === viewerId) myChoices = picked; + } + + const closed = idx.closedRoots.has(`${node.author}|${rootId}`) || isExpired(c); + const op = idx.opinionsByRoot.get(rootId) || { counts: {}, voters: [] }; + + return { + id: rootId, + rootId, + tipId, + author: node.author, + question, + options, + encrypted: !!c.encryptedQuestion, + undecryptable, + chatId: c.chatId || null, + anonymous: c.anonymous === true, + multiple: c.multiple === true, + deadline: c.deadline || '', + tags: normalizeTags(c.tags), + tribeId: c.tribeId || null, + houseKey: c.houseKey || null, + createdAt: c.createdAt || new Date(node.ts).toISOString(), + updatedAt: c.updatedAt || null, + status: closed ? 'CLOSED' : 'OPEN', + counts, + totalVoters, + voters: c.anonymous === true ? [] : voters, + votersHidden: c.anonymous === true, + myChoices, + hasVoted: myChoices.length > 0, + commentCount: idx.commentsByRoot.get(rootId) || 0, + opinions: op.counts, + opinions_inhabitants: op.voters + }; + }; + + const collect = async (viewerId) => { + const ssbClient = await openSsb(); + const messages = await getAllMessages(ssbClient); + const idx = memoIndex('polls', messages, buildIndex); + + const keyCache = new Map(); + const keysFor = async (c) => { + if (!c || (!c.chatId && !c.tribeId)) return []; + const cacheKey = `${c.chatId || ''}|${c.tribeId || ''}`; + if (keyCache.has(cacheKey)) return keyCache.get(cacheKey); + const k = await scopeKey({ chatId: c.chatId || null, tribeId: c.tribeId || null }); + const keys = k ? [k] : []; + keyCache.set(cacheKey, keys); + return keys; + }; + + const list = []; + for (const rootId of idx.vg.roots) { + const tipId = idx.tipOf(rootId); + if (idx.tomb.has(tipId) || idx.tomb.has(rootId)) continue; + const node = idx.nodes.get(tipId); + const keys = await keysFor(node && node.c); + const poll = buildPoll(idx, rootId, viewerId || ssbClient.id, keys); + if (!poll) continue; + if (poll.undecryptable) { list.push(poll); continue; } + if (!poll.question || poll.options.length < MIN_OPTIONS) continue; + list.push(poll); + } + return { list, idx, viewerId: viewerId || ssbClient.id }; + }; + + return { + POLL_TYPE, + VOTE_TYPE, + MAX_OPTIONS, + MIN_OPTIONS, + + async listAll(filter = 'ALL', opts = {}) { + const { list, viewerId } = await collect(); + const f = String(filter || 'ALL').toUpperCase(); + const scopeChat = opts.chatId || null; + const scopeTribe = opts.tribeId || null; + const scopeHouse = opts.houseKey || null; + let out = list.filter(p => + scopeChat ? p.chatId === scopeChat + : scopeTribe ? (p.tribeId === scopeTribe && !p.chatId) + : scopeHouse ? p.houseKey === scopeHouse + : (!p.tribeId && !p.chatId && !p.houseKey)); + + if (f === 'MINE') out = out.filter(p => String(p.author) === String(viewerId)); + else if (f === 'OPEN') out = out.filter(p => p.status === 'OPEN'); + else if (f === 'CLOSED') out = out.filter(p => p.status === 'CLOSED'); + else if (f === 'VOTED') out = out.filter(p => p.hasVoted); + else if (f === 'RECENT') out = out.filter(p => (Date.parse(p.createdAt || '') || 0) >= Date.now() - 86400000); + else if (f === 'FAVORITES') { + const fav = new Set((opts.favorites || []).map(String)); + out = out.filter(p => fav.has(String(p.id))); + } + + const q = clean(opts.q).toLowerCase(); + if (q) out = out.filter(p => + p.question.toLowerCase().includes(q) || + p.options.some(o => o.toLowerCase().includes(q)) || + p.tags.some(t => t.toLowerCase().includes(q))); + + if (f === 'TOP') out.sort((a, b) => b.totalVoters - a.totalVoters || new Date(b.createdAt) - new Date(a.createdAt)); + else out.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)); + + return out; + }, + + async getPollById(id, viewerId = null) { + const { list, idx } = await collect(viewerId); + const rootId = idx.rootOf(id); + const poll = list.find(p => p.id === rootId || p.id === id || p.tipId === id); + if (!poll) throw new Error('Poll not found'); + return poll; + }, + + async resolveRootId(id) { + const { idx } = await collect(); + return idx.rootOf(id); + }, + + async createPoll(data = {}) { + if (isPublic) throw new Error('Not available in public mode'); + const question = clean(data.question); + if (!question) throw new Error('Question is required'); + const options = normalizeOptions(data.options); + if (options.length < MIN_OPTIONS) throw new Error(`A poll needs at least ${MIN_OPTIONS} options`); + const deadline = clean(data.deadline); + if (deadline) { + const t = Date.parse(deadline); + if (!Number.isFinite(t)) throw new Error('Invalid deadline'); + if (t <= Date.now()) throw new Error('Deadline must be in the future'); + } + const ssbClient = await openSsb(); + const content = { + type: POLL_TYPE, + anonymous: data.anonymous === true || data.anonymous === 'true' || data.anonymous === '1', + multiple: data.multiple === true || data.multiple === 'true' || data.multiple === '1', + deadline, + tags: normalizeTags(data.tags), + createdAt: new Date().toISOString(), + ...(data.tribeId ? { tribeId: String(data.tribeId) } : {}), + ...(data.chatId ? { chatId: String(data.chatId) } : {}), + ...(data.houseKey ? { houseKey: String(data.houseKey) } : {}) + }; + const key = await scopeKey({ chatId: data.chatId, tribeId: data.tribeId }); + if (key && tribeCrypto) { + content.encryptedQuestion = tribeCrypto.encryptWithKey(question, key); + content.encryptedOptions = tribeCrypto.encryptWithKey(JSON.stringify(options), key); + } else { + content.question = question; + content.options = options; + } + return new Promise((res, rej) => ssbClient.publish(content, (err, msg) => err ? rej(err) : res(msg))); + }, + + async updatePoll(id, data = {}) { + const ssbClient = await openSsb(); + const userId = ssbClient.id; + const poll = await this.getPollById(id, userId); + if (poll.author !== userId) throw new Error('Not the author'); + if (poll.status === 'CLOSED') throw new Error('Cannot edit a closed poll'); + if (poll.totalVoters > 0) throw new Error('Cannot edit a poll that already has votes'); + + const options = data.options !== undefined ? normalizeOptions(data.options) : poll.options; + if (options.length < MIN_OPTIONS) throw new Error(`A poll needs at least ${MIN_OPTIONS} options`); + const deadline = data.deadline !== undefined ? clean(data.deadline) : poll.deadline; + if (deadline) { + const t = Date.parse(deadline); + if (!Number.isFinite(t)) throw new Error('Invalid deadline'); + } + const question = data.question !== undefined ? clean(data.question) : poll.question; + const content = { + type: POLL_TYPE, + replaces: poll.tipId, + anonymous: data.anonymous !== undefined + ? (data.anonymous === true || data.anonymous === 'true' || data.anonymous === '1') + : poll.anonymous, + multiple: data.multiple !== undefined + ? (data.multiple === true || data.multiple === 'true' || data.multiple === '1') + : poll.multiple, + deadline, + tags: data.tags !== undefined ? normalizeTags(data.tags) : poll.tags, + createdAt: poll.createdAt, + updatedAt: new Date().toISOString(), + ...(poll.tribeId ? { tribeId: poll.tribeId } : {}), + ...(poll.chatId ? { chatId: poll.chatId } : {}), + ...(poll.houseKey ? { houseKey: poll.houseKey } : {}) + }; + const key = await scopeKey({ chatId: poll.chatId, tribeId: poll.tribeId }); + if (key && tribeCrypto) { + content.encryptedQuestion = tribeCrypto.encryptWithKey(question, key); + content.encryptedOptions = tribeCrypto.encryptWithKey(JSON.stringify(options), key); + } else { + content.question = question; + content.options = options; + } + return new Promise((res, rej) => ssbClient.publish(content, (err, msg) => err ? rej(err) : res(msg))); + }, + + async vote(id, choicesRaw) { + const ssbClient = await openSsb(); + const userId = ssbClient.id; + const poll = await this.getPollById(id, userId); + if (poll.status === 'CLOSED') throw new Error('This poll is closed'); + + const wanted = (Array.isArray(choicesRaw) ? choicesRaw : [choicesRaw]) + .map(c => clean(c)) + .filter(c => poll.options.includes(c)); + const unique = [...new Set(wanted)]; + if (!unique.length) throw new Error('Pick at least one option'); + if (!poll.multiple && unique.length > 1) throw new Error('This poll only accepts one option'); + + const content = { + type: VOTE_TYPE, + target: poll.rootId, + createdAt: new Date().toISOString(), + ...(poll.chatId ? { chatId: poll.chatId } : {}) + }; + const key = await scopeKey({ chatId: poll.chatId, tribeId: poll.tribeId }); + if (key && tribeCrypto) content.encryptedChoices = tribeCrypto.encryptWithKey(JSON.stringify(unique), key); + else content.choices = unique; + return new Promise((res, rej) => ssbClient.publish(content, (err, msg) => err ? rej(err) : res(msg))); + }, + + async closePoll(id) { + const ssbClient = await openSsb(); + const userId = ssbClient.id; + const poll = await this.getPollById(id, userId); + if (poll.author !== userId) throw new Error('Not the author'); + if (poll.status === 'CLOSED') return { status: 'already_closed' }; + const content = { type: 'pollClose', target: poll.rootId, createdAt: new Date().toISOString() }; + await new Promise((res, rej) => ssbClient.publish(content, (err, msg) => err ? rej(err) : res(msg))); + return { status: 'ok' }; + }, + + async deletePoll(id) { + const ssbClient = await openSsb(); + const userId = ssbClient.id; + const poll = await this.getPollById(id, userId); + if (poll.author !== userId) throw new Error('Not the author'); + const content = { type: 'tombstone', target: poll.tipId, deletedAt: new Date().toISOString(), author: userId }; + return new Promise((res, rej) => ssbClient.publish(content, (err, msg) => err ? rej(err) : res(msg))); + }, + + async createOpinion(id, category) { + const categories = require('../backend/opinion_categories'); + if (!categories.includes(category)) throw new Error('Invalid opinion category'); + const ssbClient = await openSsb(); + const userId = ssbClient.id; + const poll = await this.getPollById(id, userId); + if (poll.opinions_inhabitants.includes(userId)) throw new Error('Already opined'); + const content = { type: OPINION_TYPE, target: poll.rootId, category, createdAt: new Date().toISOString() }; + return new Promise((res, rej) => ssbClient.publish(content, (err, result) => err ? rej(err) : res(result))); + } + }; +}; diff --git a/src/models/polls_model_limits.js b/src/models/polls_model_limits.js new file mode 100644 index 0000000..ba1e774 --- /dev/null +++ b/src/models/polls_model_limits.js @@ -0,0 +1,5 @@ +const MAX_OPTIONS = 10; +const MIN_OPTIONS = 2; +const MAX_OPTION_LENGTH = 120; + +module.exports = { MAX_OPTIONS, MIN_OPTIONS, MAX_OPTION_LENGTH }; diff --git a/src/models/projects_model.js b/src/models/projects_model.js index 05ecf90..76d3b3a 100644 --- a/src/models/projects_model.js +++ b/src/models/projects_model.js @@ -3,6 +3,7 @@ const moment = require("../server/node_modules/moment") const { getConfig } = require("../configs/config-manager.js") const { buildValidatedTombstoneSet } = require('./tombstone_validator'); const { dedupeBy, norm } = require('../backend/dedupe') +const { readTyped } = require("./typed_log") const logLimit = (getConfig().ssbLogStream && getConfig().ssbLogStream.limit) || 1000 module.exports = ({ cooler }) => { @@ -20,13 +21,10 @@ module.exports = ({ cooler }) => { return Math.max(0, Math.min(100, x)) } + const PROJECT_TYPES = ["project", "projectOpinion", "projectFollow", "projectPledge", "projectClaim", "projectPledgeConfirm", "tombstone"] + async function getAllMsgs(ssbClient) { - return new Promise((r, j) => { - pull( - ssbClient.createLogStream({ limit: logLimit }), - pull.collect((e, m) => (e ? j(e) : r(m))) - ) - }) + return readTyped(ssbClient, PROJECT_TYPES, { limit: logLimit }) } function extractBlobId(possibleMarkdownImage) { @@ -239,6 +237,7 @@ module.exports = ({ cooler }) => { if (goal < 0) goal = 0 const deadlineISO = data.deadline ? new Date(data.deadline).toISOString() : null + if (deadlineISO && new Date(deadlineISO).getTime() < Date.now()) throw new Error("The deadline cannot be in the past") const content = { type: TYPE, @@ -323,7 +322,11 @@ module.exports = ({ cooler }) => { } let deadline = patch.deadline === undefined ? current.deadline : patch.deadline - if (deadline != null && deadline !== "") deadline = new Date(deadline).toISOString() + if (deadline != null && deadline !== "") { + deadline = new Date(deadline).toISOString() + const changed = !current.deadline || new Date(current.deadline).getTime() !== new Date(deadline).getTime() + if (changed && new Date(deadline).getTime() < Date.now()) throw new Error("The deadline cannot be in the past") + } else if (deadline === "") deadline = null const updated = { @@ -546,7 +549,7 @@ module.exports = ({ cooler }) => { return publishReplace(ssbClient, project.id, patch) }, - async listProjects(filter) { + async listProjects(filter, opts = {}) { const ssbClient = await openSsb() const currentUserId = ssbClient.id const idx = buildProjectIndex(await getAllMsgs(ssbClient)) @@ -580,6 +583,15 @@ module.exports = ({ cooler }) => { else if (F === "RECENT") list = list.filter((p) => p && moment(p.createdAt).isAfter(moment().subtract(24, "hours"))) else if (F === "FOLLOWING") list = list.filter((p) => Array.isArray(p.followers) && p.followers.includes(currentUserId)) + const q = norm(opts.q) + if (q) { + list = list.filter((p) => + norm(p.title).includes(q) || + norm(p.description).includes(q) || + (Array.isArray(p.tags) && p.tags.some(t => norm(t).includes(q))) + ) + } + if (F === "TOP") { list.sort((a, b) => (parseFloat(b.pledged || 0) / (parseFloat(b.goal || 1))) - (parseFloat(a.pledged || 0) / (parseFloat(a.goal || 1)))) } else { diff --git a/src/models/pub_invites.js b/src/models/pub_invites.js new file mode 100644 index 0000000..8cc408a --- /dev/null +++ b/src/models/pub_invites.js @@ -0,0 +1,93 @@ +const ssbKeys = require('../server/node_modules/ssb-keys'); +const { readTyped } = require('./typed_log'); + +const ANNOUNCE_TYPE = 'pub-invites'; +const MAX_CODES = 40; +const FEED_ID = /@[A-Za-z0-9+/]{43}=\.ed25519/; +const SHS_KEY = /~shs:([A-Za-z0-9+/]{43}=)/; + +const LEGACY_ADDRESS = /^([^:~\s]+):(\d+):@/; +const MODERN_ADDRESS = /^(?:net|onion):([^:~\s]+):(\d+)~/; +const parseInvite = (code) => { + const s = String(code || '').trim(); + const legacy = s.match(FEED_ID); + const modern = s.match(SHS_KEY); + const pub = legacy ? legacy[0] : modern ? `@${modern[1]}.ed25519` : null; + const address = s.match(LEGACY_ADDRESS) || s.match(MODERN_ADDRESS); + const host = address ? address[1] : null; + const port = address ? Number(address[2]) : null; + let id = null; + try { + const seed = Buffer.from(s.split('~').pop().replace(/^invite:/, ''), 'base64'); + if (seed.length === 32) id = ssbKeys.generate('ed25519', seed).id; + } catch (_) {} + return { pub, id, host, port }; +}; + +const count = (n) => Math.max(0, Math.floor(Number(n) || 0)); + +const summarize = (rows) => { + const list = (Array.isArray(rows) ? rows : []) + .filter((r) => r && typeof r.id === 'string' && count(r.total) > 0) + .map((r) => ({ id: r.id, total: count(r.total), used: Math.min(count(r.used), count(r.total)) })); + const codes = list.filter((r) => r.total > 1) + .sort((a, b) => (b.total - b.used) - (a.total - a.used)) + .slice(0, MAX_CODES) + .sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)); + return { + total: list.reduce((n, r) => n + r.total, 0), + used: list.reduce((n, r) => n + r.used, 0), + codes + }; +}; + +const latestByAuthor = (msgs) => { + const out = new Map(); + for (const m of msgs || []) { + const v = m && m.value; + const c = v && v.content; + if (!c || c.type !== ANNOUNCE_TYPE) continue; + const prev = out.get(v.author); + if (!prev || (Number(v.sequence) || 0) > (Number(prev.value.sequence) || 0)) out.set(v.author, m); + } + return out; +}; + +const listUses = (ssb) => new Promise((resolve) => { + if (!ssb || !ssb.invite || typeof ssb.invite.list !== 'function') return resolve(null); + try { ssb.invite.list((err, rows) => resolve(err ? null : rows)); } catch (_) { resolve(null); } +}); + +const announce = async (ssb) => { + const rows = await listUses(ssb); + if (!rows) return null; + const next = summarize(rows); + const last = latestByAuthor(await readTyped(ssb, [ANNOUNCE_TYPE])).get(ssb.id); + const c = last && last.value.content; + const prev = c ? { total: c.total, used: c.used, codes: c.codes } : null; + if (prev ? JSON.stringify(prev) === JSON.stringify(next) : !next.total) return null; + return new Promise((resolve, reject) => ssb.publish({ type: ANNOUNCE_TYPE, ...next, updatedAt: new Date().toISOString() }, (err, msg) => (err ? reject(err) : resolve(msg)))); +}; + +const statusFor = (entries, msgs) => { + const latest = latestByAuthor(msgs); + return (Array.isArray(entries) ? entries : []).map((e) => { + const { pub, id } = parseInvite(e && e.code); + const m = pub ? latest.get(pub) : null; + if (!m) return { ...e, invites: null, exhausted: false }; + const c = m.value.content; + const total = count(c.total); + const used = Math.min(count(c.used), total); + const own = id && Array.isArray(c.codes) ? c.codes.find((x) => x && x.id === id) : null; + const exhausted = (total > 0 && used >= total) || (!!own && count(own.used) >= count(own.total)); + return { ...e, invites: total > 0 ? { used, total } : null, exhausted }; + }); +}; + +const readStatus = async (ssb, entries) => { + let msgs = []; + try { msgs = await readTyped(ssb, [ANNOUNCE_TYPE]); } catch (_) { msgs = []; } + return statusFor(entries, msgs); +}; + +module.exports = { ANNOUNCE_TYPE, parseInvite, summarize, announce, readStatus }; diff --git a/src/models/recurrence.js b/src/models/recurrence.js new file mode 100644 index 0000000..0c696aa --- /dev/null +++ b/src/models/recurrence.js @@ -0,0 +1,40 @@ +const truthy = (v) => v === true || v === 1 || v === '1' || v === 'on' || v === 'true'; + +const hasAnyInterval = (weekly, monthly, yearly) => truthy(weekly) || truthy(monthly) || truthy(yearly); + +const expandRecurrence = (firstDate, until, weekly, monthly, yearly) => { + const start = new Date(firstDate); + if (isNaN(start.getTime())) return []; + const out = [start]; + if (!until || !hasAnyInterval(weekly, monthly, yearly)) return out; + const end = new Date(until).getTime(); + if (!Number.isFinite(end)) return out; + const seen = new Set([start.getTime()]); + const walk = (mutate) => { + const n = new Date(start); + mutate(n); + let guard = 0; + while (n.getTime() <= end && guard++ < 5000) { + const t = n.getTime(); + if (!seen.has(t)) { seen.add(t); out.push(new Date(n)); } + mutate(n); + } + }; + if (truthy(weekly)) walk((d) => d.setUTCDate(d.getUTCDate() + 7)); + if (truthy(monthly)) walk((d) => d.setUTCMonth(d.getUTCMonth() + 1)); + if (truthy(yearly)) walk((d) => d.setUTCFullYear(d.getUTCFullYear() + 1)); + return out.sort((a, b) => a.getTime() - b.getTime()); +}; + +const nextOccurrence = (firstDate, until, weekly, monthly, yearly, from = Date.now()) => { + const list = expandRecurrence(firstDate, until, weekly, monthly, yearly); + for (const d of list) if (d.getTime() >= from) return d; + return list.length ? list[list.length - 1] : null; +}; + +const upcomingOccurrences = (firstDate, until, weekly, monthly, yearly, limit = 12, from = Date.now()) => + expandRecurrence(firstDate, until, weekly, monthly, yearly) + .filter(d => d.getTime() >= from) + .slice(0, limit); + +module.exports = { truthy, hasAnyInterval, expandRecurrence, nextOccurrence, upcomingOccurrences }; diff --git a/src/models/reports_model.js b/src/models/reports_model.js index 8bb1b29..39631d9 100644 --- a/src/models/reports_model.js +++ b/src/models/reports_model.js @@ -1,7 +1,9 @@ const pull = require('../server/node_modules/pull-stream'); +const { normalizeImages, normalizeVideo } = require('./media_gallery'); const { getConfig } = require('../configs/config-manager.js'); const { buildValidatedTombstoneSet } = require('./tombstone_validator'); -const { dedupeBy, norm } = require('../backend/dedupe'); +const { dedupeBy, norm, versionGroups } = require('../backend/dedupe'); +const { readTyped, memoIndex } = require('./typed_log'); const logLimit = getConfig().ssbLogStream?.limit || 1000; const normU = (v) => String(v || '').trim().toUpperCase(); @@ -39,7 +41,7 @@ const normalizeTemplate = (category, tpl) => { } if (cat === 'CONTENT') { - return pick(['contentLocation', 'whyInappropriate', 'requestedAction', 'evidenceLinks']); + return pick(['contentLocation', 'whyInappropriate', 'evidenceLinks']); } return {}; @@ -49,20 +51,13 @@ module.exports = ({ cooler }) => { let ssb; const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb; }; - const getAllMessages = async (ssbClient) => - new Promise((resolve, reject) => { - pull( - ssbClient.createLogStream({ limit: logLimit }), - pull.collect((err, msgs) => err ? reject(err) : resolve(msgs)) - ); - }); + const REPORT_TYPES = ['report', 'reportConfirm', 'reportOpinion', 'tombstone']; + + const getAllMessages = async (ssbClient) => readTyped(ssbClient, REPORT_TYPES, { limit: logLimit }); const buildIndex = (messages) => { const tomb = buildValidatedTombstoneSet(messages); const nodes = new Map(); - const parent = new Map(); - const child = new Map(); - const strictChild = new Map(); const confirms = []; const opinionMsgs = []; @@ -78,46 +73,24 @@ module.exports = ({ cooler }) => { if (c.type !== 'report') continue; const ts = v.timestamp || m.timestamp || 0; - nodes.set(k, { key: k, ts, c, author: v.author }); - - if (c.replaces) { - parent.set(k, c.replaces); - child.set(c.replaces, k); - } + nodes.set(k, { key: k, ts, seq: v.sequence || 0, c, author: v.author }); } - for (const [k, node] of nodes) { - const t = node.c.replaces; - if (t) { const orig = nodes.get(t); if (orig && orig.author === node.author) strictChild.set(t, k); } - } - - const rootOf = (id) => { - let cur = id, g = 0; - while (parent.has(cur) && nodes.has(parent.get(cur)) && g++ < 100000) cur = parent.get(cur); - return cur; - }; - - const contentTipOf = (root) => { - let cur = root, g = 0; - while (strictChild.has(cur) && g++ < 100000) cur = strictChild.get(cur); - const n = nodes.get(cur), rn = nodes.get(root); - return (n && rn && n.author === rn.author) ? cur : root; - }; - - const roots = new Set(); - for (const id of nodes.keys()) roots.add(rootOf(id)); + const vg = versionGroups([...nodes.values()].map(n => ({ key: n.key, author: n.author, seq: n.seq, ts: n.ts, replaces: n.c.replaces || null, claimedAuthor: typeof n.c.author === 'string' ? n.c.author : null }))); + const rootOf = vg.rootOf; + const contentTipOf = vg.tipOf; const confirmsByRoot = new Map(), opinionsByRoot = new Map(); for (const cf of confirms) { if (!nodes.has(cf.target)) continue; const r = rootOf(cf.target); if (!confirmsByRoot.has(r)) confirmsByRoot.set(r, []); confirmsByRoot.get(r).push(cf); } for (const op of opinionMsgs) { if (!nodes.has(op.target)) continue; const r = rootOf(op.target); if (!opinionsByRoot.has(r)) opinionsByRoot.set(r, []); opinionsByRoot.get(r).push(op); } const tipByRoot = new Map(); - for (const r of roots) tipByRoot.set(r, contentTipOf(r)); + for (const r of vg.roots) tipByRoot.set(r, contentTipOf(r)); const resolveGroup = (root) => { const contentTip = contentTipOf(root); const contentNode = nodes.get(contentTip) || nodes.get(root); - if (!contentNode) return null; + if (!contentNode || !tipByRoot.has(root)) return null; const rootAuthor = nodes.get(root) ? nodes.get(root).author : null; const lc = contentNode.c || {}; @@ -125,8 +98,7 @@ module.exports = ({ cooler }) => { const voterSet = new Set(ensureArray(lc.opinions_inhabitants)); const confirmSet = new Set(ensureArray(lc.confirmations)); - const groupKeys = []; { let x = root, g = 0; groupKeys.push(x); while (child.has(x) && g++ < 100000) { x = child.get(x); groupKeys.push(x); } } - for (const k of groupKeys) { + for (const k of (vg.groups.get(root) || [root])) { const n = nodes.get(k); if (!n) continue; if (n.author !== rootAuthor) continue; const c = n.c || {}; for (const cb of ensureArray(c.confirmations)) confirmSet.add(cb); for (const vv of ensureArray(c.opinions_inhabitants)) voterSet.add(vv); @@ -150,7 +122,7 @@ module.exports = ({ cooler }) => { }; }; - return { tomb, nodes, parent, child, rootOf, tipByRoot, resolveGroup }; + return { tomb, nodes, rootOf, tipByRoot, resolveGroup }; }; const buildReport = (node, agg) => { @@ -164,6 +136,8 @@ module.exports = ({ cooler }) => { ...c, category: cat, status: normalizeStatus(c.status || 'OPEN'), + images: normalizeImages(c.images && c.images.length ? c.images : c.image), + video: normalizeVideo(c.video), severity: normalizeSeverity(c.severity) || 'low', confirmations, tags: ensureArray(c.tags), @@ -174,14 +148,13 @@ module.exports = ({ cooler }) => { }; return { - async createReport(title, description, category, image, tagsRaw = [], severity = 'low', template = {}) { + async createReport(title, description, category, image, tagsRaw = [], severity = 'low', template = {}, media = {}) { const ssb = await openSsb(); const userId = ssb.id; - let blobId = null; - if (image) { - blobId = String(image).trim() || null; - } + const images = normalizeImages((media && media.images && media.images.length) ? media.images : image); + const blobId = images[0] || null; + const clip = normalizeVideo(media && media.video); const tags = Array.isArray(tagsRaw) ? tagsRaw.filter(Boolean) @@ -196,6 +169,8 @@ module.exports = ({ cooler }) => { createdAt: new Date().toISOString(), author: userId, image: blobId, + images, + video: clip, tags, confirmations: [], severity: normalizeSeverity(severity) || 'low', @@ -227,10 +202,15 @@ module.exports = ({ cooler }) => { ? String(updatedContent.tags || '').split(',').map(t => t.trim()).filter(Boolean) : ensureArray(report.content.tags); - let blobId = report.content.image || null; - if (updatedContent.image) { - blobId = String(updatedContent.image).trim() || null; - } + const nextImages = normalizeImages( + Object.prototype.hasOwnProperty.call(updatedContent, 'images') + ? updatedContent.images + : (report.content.images && report.content.images.length ? report.content.images : (updatedContent.image || report.content.image)) + ); + const blobId = nextImages[0] || null; + const nextVideo = Object.prototype.hasOwnProperty.call(updatedContent, 'video') + ? normalizeVideo(updatedContent.video) + : normalizeVideo(report.content.video); const nextStatus = Object.prototype.hasOwnProperty.call(updatedContent, 'status') ? normalizeStatus(updatedContent.status) @@ -258,6 +238,8 @@ module.exports = ({ cooler }) => { type: 'report', replaces: tipId, image: blobId, + images: nextImages, + video: nextVideo, tags, confirmations, opinions: agg.opinions, @@ -293,7 +275,7 @@ module.exports = ({ cooler }) => { const ssb = await openSsb(); const messages = await getAllMessages(ssb); - const idx = buildIndex(messages); + const idx = memoIndex('reports', messages, buildIndex); const root = idx.rootOf(id); const agg = idx.resolveGroup(root); if (!agg || agg.tombstoned) throw new Error('Report not found'); diff --git a/src/models/rooms_model.js b/src/models/rooms_model.js new file mode 100644 index 0000000..2de9adb --- /dev/null +++ b/src/models/rooms_model.js @@ -0,0 +1,860 @@ +const crypto = require("crypto") +const { buildValidatedTombstoneSet } = require('./tombstone_validator') +const { collabContent, openInviteOf } = require('../backend/collab_content') +const { readTyped, memoIndex } = require('./typed_log') +const { roomNumberOf, normalizeNumber } = require('./phone_number') +const { getConfig } = require("../configs/config-manager.js") +const roomCollab = collabContent({ membersField: 'members', undecField: 'undecryptable', contentFields: ['title', 'description', 'image', 'status'], listFields: ['tags', 'invites'] }) +const logLimit = getConfig().ssbLogStream?.limit || 1000 + +const ROOM_MAX = 50 +const ROOM_TYPES = ["room", "roomMember", "tribe-keys", "tombstone"] +const STATUSES = ["OPEN", "INVITE-ONLY"] +const INVITE_SALT = "SolarNET.HuB-rooms" +const INVITE_BYTES = 16 +const RECENT_MS = 86400000 +const RID = /^[0-9a-f]{32}$/ +const FEED_ID = /^@[A-Za-z0-9+/]{43}=\.ed25519$/ +const BLOB_ID = /^&[A-Za-z0-9+/]{43}=\.sha256$/ + +const safeText = (v) => String(v || "").trim() +const normalizeTags = (raw) => { + if (!raw) return [] + if (Array.isArray(raw)) return raw.map(t => String(t || "").trim()).filter(Boolean) + return String(raw).split(",").map(t => t.trim()).filter(Boolean) +} +const normalizeStatus = (v) => (STATUSES.includes(String(v || "").toUpperCase()) ? String(v).toUpperCase() : "OPEN") +const SEALED_REF = /^\/blob\/sealed\/%26[A-Za-z0-9%]+\.sha256\?k=[0-9a-f]{64}&t=[^\s"<>]*$/ +const blobRef = (v) => (BLOB_ID.test(safeText(v)) || SEALED_REF.test(safeText(v)) ? safeText(v) : "") +const asP = (fn, ...args) => new Promise((resolve, reject) => fn(...args, (err, v) => err ? reject(err) : resolve(v))) +const rpcValue = (fn, ...args) => new Promise((resolve) => { + try { + const r = fn(...args, (err, v) => resolve(err ? null : v)) + if (r && typeof r.then === "function") r.then(resolve, () => resolve(null)) + else if (r !== undefined) resolve(r) + } catch (_) { resolve(null) } +}) + +module.exports = ({ cooler, tribeCrypto, roomCrypto, tribesModel }) => { + let ssb + const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb } + const phone = async () => { const s = await openSsb(); return s && s.phone ? s.phone : null } + + const ownCrypto = roomCrypto || tribeCrypto + const lookupKey = (rid) => (ownCrypto && ownCrypto.getKey(rid)) || (tribeCrypto && tribeCrypto.getKey(rid)) || null + const lookupKeys = (rid) => { + const a = (ownCrypto && ownCrypto.getKeys(rid)) || [] + if (a.length) return a + return (tribeCrypto && tribeCrypto.getKeys(rid)) || [] + } + const lookupGen = (rid) => ((ownCrypto && ownCrypto.getGen(rid)) || (tribeCrypto && tribeCrypto.getGen(rid)) || 0) + const setRoomKey = (rootId, keyHex) => { if (ownCrypto) ownCrypto.setKey(rootId, keyHex, 1) } + const hashCode = (code, salt) => (ownCrypto && typeof ownCrypto.hashInviteCode === "function") ? ownCrypto.hashInviteCode(code, salt) : (tribeCrypto ? tribeCrypto.hashInviteCode(code, salt) : null) + const inviteMatches = (inv, token) => { + if (!token) return false + if (typeof inv === "string") return inv === token + if (!inv || typeof inv !== "object") return false + if (typeof inv.code === "string" && inv.code === token) return true + if (typeof inv.ch === "string" && (inv.ch === token || hashCode(token, inv.salt) === inv.ch)) return true + return false + } + const inviteToken = (inv) => (inv && typeof inv === "object" && typeof inv.ch === "string") ? inv.ch : (typeof inv === "string" ? inv : (inv && inv.code) || "") + const keyProofFor = (keyHex, id) => { try { return crypto.createHmac("sha256", Buffer.from(keyHex, "hex")).update(String(id), "utf8").digest("hex") } catch (_) { return null } } + const deriveRid = (keyHex, roomId) => crypto.createHmac("sha256", Buffer.from(String(keyHex), "hex")).update("oasis-room-rid|" + roomId).digest("hex").slice(0, 32) + const currentKeyFor = (rootId, tribeId, tribeKeys) => (tribeId && Array.isArray(tribeKeys) && tribeKeys.length ? tribeKeys[0] : lookupKeys(rootId)[0]) || null + + const encryptField = (text, keyHex) => { + const key = Buffer.from(keyHex, "hex") + const iv = crypto.randomBytes(12) + const cipher = crypto.createCipheriv("aes-256-gcm", key, iv) + const enc = Buffer.concat([cipher.update(text, "utf8"), cipher.final()]) + const authTag = cipher.getAuthTag() + return iv.toString("hex") + authTag.toString("hex") + enc.toString("hex") + } + + const tryDecryptField = (encrypted, keyHex) => { + const key = Buffer.from(keyHex, "hex") + const iv = Buffer.from(encrypted.slice(0, 24), "hex") + const authTag = Buffer.from(encrypted.slice(24, 56), "hex") + const ciphertext = Buffer.from(encrypted.slice(56), "hex") + const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv) + decipher.setAuthTag(authTag) + return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf8") + } + + const decryptField = (encrypted, keyHex) => { + try { return tryDecryptField(encrypted, keyHex) } catch (_) { return "" } + } + + const getTribeKeysFor = async (tribeId) => { + if (!tribeCrypto || !tribesModel || !tribeId) return [] + try { + const rootId = await tribesModel.getRootId(tribeId) + return tribeCrypto.getKeys(rootId) || [] + } catch (_) { return [] } + } + + const fieldsWith = (c, keyHex, strict) => { + const dec = strict ? tryDecryptField : decryptField + const title = c.title ? dec(c.title, keyHex) : "" + const opt = (v) => { try { return v ? dec(v, keyHex) : "" } catch (_) { return "" } } + return { + title: safeText(title), + description: safeText(opt(c.description)), + image: blobRef(opt(c.image)), + tags: normalizeTags(opt(c.tags)), + rid: RID.test(opt(c.rid)) ? opt(c.rid) : "" + } + } + + const decryptWithKeys = (c, keys) => { + if (!c.title || !keys.length) return null + for (const k of keys) { + try { return fieldsWith(c, k, true) } catch (_) {} + } + return null + } + + const encryptForInvite = (roomKeyHex, code, saltHex) => { + const salt = saltHex ? Buffer.from(saltHex, "hex") : Buffer.from(INVITE_SALT) + const derived = crypto.scryptSync(code, salt, 32) + return encryptField(roomKeyHex, derived.toString("hex")) + } + + const decryptFromInvite = (encryptedKey, code, saltHex) => { + const salt = saltHex ? Buffer.from(saltHex, "hex") : Buffer.from(INVITE_SALT) + const derived = crypto.scryptSync(code, salt, 32) + return decryptField(encryptedKey, derived.toString("hex")) + } + + const tryDecryptPublicInviteKey = (invites) => { + if (!Array.isArray(invites)) return null + for (const inv of invites) { + if (!inv || typeof inv !== "object" || inv.public !== true) continue + if (typeof inv.code !== "string" || typeof inv.ek !== "string") continue + try { + const key = decryptFromInvite(inv.ek, inv.code, inv.salt) + if (key) return key + } catch (_) {} + } + return null + } + + const generateInviteSalt = () => crypto.randomBytes(16).toString("hex") + + const rotateRoomKey = async (rootId, remainingMembers) => { + if (!ownCrypto || !tribeCrypto || !rootId) return + if (!lookupKey(rootId)) return + const newKey = crypto.randomBytes(32).toString("hex") + const newGen = ownCrypto.addNewKey(rootId, newKey) + const ssbClient = await openSsb() + const ssbKeys = require("../server/node_modules/ssb-keys") + const memberKeys = {} + for (const m of new Set([ssbClient.id, ...(Array.isArray(remainingMembers) ? remainingMembers : [])])) { + try { memberKeys[m] = tribeCrypto.boxKeyForMember(newKey, m, ssbKeys) } catch (_) {} + } + if (Object.keys(memberKeys).length) { + await new Promise((resolve) => { + ssbClient.publish({ type: "tribe-keys", tribeId: rootId, generation: newGen, memberKeys }, () => resolve()) + }) + } + } + + const readAll = async (ssbClient) => readTyped(ssbClient, ROOM_TYPES, { limit: logLimit }) + + const ensureMemberKeys = async (ssbClient, messages, rooms) => { + if (!tribeCrypto || !ownCrypto) return + const leavesByRoot = new Map() + for (const m of messages) { + const c = m.value && m.value.content + if (!c || c.type !== "roomMember" || c.on !== false || !c.target || !c.member) continue + if (!leavesByRoot.has(c.target)) leavesByRoot.set(c.target, []) + leavesByRoot.get(c.target).push({ member: c.member, author: m.value.author, ts: Number(m.timestamp || m.value.timestamp || 0) }) + } + const ssbKeys = require("../server/node_modules/ssb-keys") + for (const room of (Array.isArray(rooms) ? rooms : [])) { + if (!room || room.tribeId || room.author !== ssbClient.id) continue + const rootId = room.rootId + if (!rootId || !lookupKey(rootId)) continue + const members = Array.isArray(room.members) ? room.members : [] + const leaves = (leavesByRoot.get(rootId) || []).filter(l => l.author === l.member || l.author === room.author) + const plan = ownCrypto.keyPlan({ rootId, ownerId: room.author, gen: lookupGen(rootId), members, messages, leaves }) + if (plan.rotate) { + await rotateRoomKey(rootId, members) + continue + } + if (!plan.missing.length) continue + const key = lookupKey(rootId) + const memberKeys = {} + for (const m of plan.missing) { + try { memberKeys[m] = tribeCrypto.boxKeyForMember(key, m, ssbKeys) } catch (_) {} + } + if (!Object.keys(memberKeys).length) continue + await asP(ssbClient.publish, { type: "tribe-keys", tribeId: rootId, generation: lookupGen(rootId) || 1, memberKeys }) + } + } + + + const ingestOwnTribeKeys = async () => { + if (!ownCrypto) return + try { + const ssbClient = await openSsb() + const ssbKeys = require("../server/node_modules/ssb-keys") + const config = require("../server/ssb_config") + const rooms = new Set() + const msgs = await readAll(ssbClient) + for (const m of msgs) { + const c = m.value && m.value.content + if (c && c.type === "room") rooms.add(m.key) + } + const idx = buildIndex(msgs) + const trusted = (root, author) => { const owner = idx.nodes.get(root) && idx.nodes.get(root).author; return !!author && !!owner && (author === owner || author === ssbClient.id) } + for (const m of msgs) { + const c = m.value && m.value.content + if (!c || c.type !== "tribe-keys" || !rooms.has(c.tribeId)) continue + if (!trusted(c.tribeId, m.value.author)) continue + const boxed = c.memberKeys && typeof c.memberKeys === "object" ? c.memberKeys[ssbClient.id] : null + if (!boxed) continue + try { + const unboxed = ssbKeys.unbox(boxed, config.keys) + const key = typeof unboxed === "string" ? unboxed : (unboxed && unboxed.toString ? unboxed.toString() : null) + if (key) ownCrypto.addNewKey(c.tribeId, key) + } catch (_) {} + } + } catch (_) {} + } + + const buildIndex = (messages) => { + const tomb = new Set() + const nodes = new Map() + const parent = new Map() + const child = new Map() + const authorByKey = new Map() + const tombRequests = [] + const memberMsgs = [] + + for (const m of messages) { + const k = m.key + const v = m.value || {} + const c = v.content + if (!c) continue + if (c.type === "tombstone" && c.target) { tombRequests.push({ target: c.target, author: v.author }); continue } + if (c.type === "roomMember" && c.target) { memberMsgs.push({ target: c.target, member: c.member, on: c.on !== false, author: v.author, ts: v.timestamp || m.timestamp || 0, code: typeof c.code === "string" ? c.code : "", keyProof: typeof c.keyProof === "string" ? c.keyProof : "" }); continue } + if (c.type === "room") { + nodes.set(k, { key: k, ts: v.timestamp || m.timestamp || 0, seq: v.sequence || 0, c, author: v.author }) + authorByKey.set(k, v.author) + if (c.replaces) { parent.set(k, c.replaces); child.set(c.replaces, k) } + } + } + + for (const t of tombRequests) { + const targetAuthor = authorByKey.get(t.target) + if (targetAuthor && t.author === targetAuthor) tomb.add(t.target) + } + + const rootOf = (id) => { let cur = id, g = 0; while (parent.has(cur) && g++ < 100000) cur = parent.get(cur); return cur } + const rootMemo = new Map() + const groupRootOf = (id) => { + if (rootMemo.has(id)) return rootMemo.get(id) + let cur = id, g = 0 + while (parent.has(cur) && nodes.has(parent.get(cur)) && g++ < 100000) cur = parent.get(cur) + rootMemo.set(id, cur) + return cur + } + const ownerTip = new Map() + for (const [k, n] of nodes) { + const r = groupRootOf(k) + const rn = nodes.get(r) + if (!rn || n.author !== rn.author) continue + const prev = ownerTip.has(r) ? nodes.get(ownerTip.get(r)) : null + if (!prev || n.seq > prev.seq || (n.seq === prev.seq && n.ts >= prev.ts)) ownerTip.set(r, k) + } + const contentTipOf = (root) => ownerTip.get(root) || root + + const roots = new Set() + for (const id of nodes.keys()) roots.add(groupRootOf(id)) + + const invitesOf = (root) => { + const out = [] + for (const id of [root, contentTipOf(root)]) { const n = nodes.get(id); if (n && Array.isArray(n.c.invites)) out.push(...n.c.invites) } + return out + } + const selfJoinAllowed = (root, mm) => { + const oc = (nodes.get(root) || {}).c || {} + if (oc.encrypted !== true || oc.tribeId) return true + const invites = invitesOf(root) + if (invites.some(inv => inv && typeof inv === "object" && inv.public === true)) return true + if (mm.keyProof && lookupKeys(root).some(k => keyProofFor(k, mm.member) === mm.keyProof)) return true + return !!mm.code && invites.some(inv => inviteMatches(inv, mm.code)) + } + + const memberByRoot = new Map() + const consumedByRoot = new Map() + for (const mm of memberMsgs) { + if (!nodes.has(mm.target)) continue + const r = groupRootOf(mm.target) + const ownerAuthor = nodes.get(r) && nodes.get(r).author + if (!ownerAuthor || !mm.member) continue + const self = mm.member === mm.author + if (!self && mm.author !== ownerAuthor) continue + if (self && mm.author !== ownerAuthor && mm.on && !selfJoinAllowed(r, mm)) continue + if (!memberByRoot.has(r)) memberByRoot.set(r, new Map()) + const m2 = memberByRoot.get(r) + const p = m2.get(mm.member) + if (!p || mm.ts >= p.ts) m2.set(mm.member, { on: mm.on, ts: mm.ts }) + if (mm.on && mm.code) { + if (!consumedByRoot.has(r)) consumedByRoot.set(r, new Set()) + consumedByRoot.get(r).add(mm.code) + } + } + + const isCodeConsumed = (root, code) => { + if (!code) return false + const set = consumedByRoot.get(root) + return !!(set && set.has(code)) + } + + for (const [k, n] of nodes) { + const r = groupRootOf(k) + const rn = nodes.get(r) + if (!rn || n.author === rn.author || !Array.isArray(n.c.members) || !selfJoinAllowed(r, { member: n.author })) continue + if (!memberByRoot.has(r)) memberByRoot.set(r, new Map()) + const m2 = memberByRoot.get(r) + const p = m2.get(n.author) + if (!p || n.ts >= p.ts) m2.set(n.author, { on: n.c.members.includes(n.author), ts: n.ts }) + } + + const resolveMembers = (root) => { + const ownerNode = nodes.get(root) + const oc = ownerNode ? ownerNode.c : {} + const set = new Set(Array.isArray(oc.members) ? oc.members.filter(x => typeof x === "string" && x) : []) + for (const [mem, st] of (memberByRoot.get(root) || new Map())) { if (st.on) set.add(mem); else set.delete(mem) } + return [...set] + } + + const tipByRoot = new Map() + for (const r of roots) tipByRoot.set(r, contentTipOf(r)) + + return { tomb, nodes, parent, child, rootOf, groupRootOf, contentTipOf, tipByRoot, resolveMembers, isCodeConsumed } + } + + const decryptRoomFields = (c, rootId, tribeKeys) => { + if (c.encrypted !== true) { + return { title: safeText(c.title), description: safeText(c.description), image: blobRef(c.image), tags: normalizeTags(c.tags), rid: RID.test(String(c.rid || "")) ? String(c.rid) : "", _undec: false } + } + if (c.tribeId && Array.isArray(tribeKeys) && tribeKeys.length) { + const viaTribe = decryptWithKeys(c, tribeKeys) + if (viaTribe) return { ...viaTribe, _undec: false } + } + for (const k of [...lookupKeys(rootId), tryDecryptPublicInviteKey(c.invites)].filter(Boolean)) { + const out = decryptWithKeys(c, [k]) + if (out) return { ...out, _undec: false } + } + return { title: "", description: "", image: "", tags: [], rid: "", _undec: true } + } + + const roomTypeOf = (c) => c.status === "INVITE-ONLY" || (c.status === "CLOSED" && c.encrypted === true && !c.tribeId) ? "INVITE-ONLY" : "OPEN" + + const buildRoom = (node, rootId, tribeKeys, members) => { + const c = node.c || {} + if (c.type !== "room") return null + const f = decryptRoomFields(c, rootId, tribeKeys) + const author = c.author || node.author + const roomId = RID.test(String(c.roomId || "")) ? String(c.roomId) : "" + const liveKey = roomId && c.encrypted === true && !f._undec ? currentKeyFor(rootId, c.tribeId, tribeKeys) : null + return { + key: node.key, + rootId, + roomId, + title: f.title, + description: f.description, + image: f.image, + tags: f.tags, + status: ["OPEN", "INVITE-ONLY", "CLOSED"].includes(c.status) ? c.status : "OPEN", + type: roomTypeOf(c), + author, + members: Array.isArray(members) ? members : (Array.isArray(c.members) ? c.members : []), + invites: Array.isArray(c.invites) ? c.invites : [], + createdAt: c.createdAt || new Date(node.ts).toISOString(), + updatedAt: c.updatedAt || null, + tribeId: c.tribeId || null, + encrypted: c.encrypted === true, + undecryptable: !!f._undec, + rid: roomId ? (liveKey ? deriveRid(liveKey, roomId) : "") : f.rid, + token: typeof c.token === "string" ? c.token : "", + hub: FEED_ID.test(String(c.hub || "")) ? c.hub : author, + hubAddress: typeof c.hubAddress === "string" ? c.hubAddress : "", + media: c.media === "video" ? "video" : "audio", + maxParticipants: ROOM_MAX, + number: roomNumberOf(rootId), + line: c.line === "SWITCHBOARD" ? "SWITCHBOARD" : "DND" + } + } + + const collectRooms = async (idx) => { + const tribeKeyCache = new Map() + const items = [] + for (const [rootId, tipId] of idx.tipByRoot.entries()) { + if (idx.tomb.has(tipId)) continue + const node = idx.nodes.get(tipId) + if (!node || node.c.type !== "room") continue + let tKeys = [] + if (node.c.tribeId) { + if (!tribeKeyCache.has(node.c.tribeId)) tribeKeyCache.set(node.c.tribeId, await getTribeKeysFor(node.c.tribeId)) + tKeys = tribeKeyCache.get(node.c.tribeId) + } + const room = buildRoom(node, rootId, tKeys, idx.resolveMembers(rootId)) + if (!room) continue + room.isClosed = room.status === "CLOSED" + items.push(room) + } + return items + } + + const pickHub = async () => { + const ph = await phone() + const me = (await openSsb()).id + const h = ph && typeof ph.roomHubFor === "function" ? await rpcValue(ph.roomHubFor) : null + if (h && FEED_ID.test(String(h.key || ""))) return { hub: h.key, hubAddress: String(h.address || "") } + return { hub: me, hubAddress: "" } + } + + const tokenFor = async (rid) => { + const ph = await phone() + const t = ph && typeof ph.roomToken === "function" ? await rpcValue(ph.roomToken, rid) : null + return typeof t === "string" ? t : "" + } + + const republish = async (id, mutate) => { + const self = api + const tipId = await self.resolveCurrentId(id) + const ssbClient = await openSsb() + const userId = ssbClient.id + const rootId = await self.resolveRootId(id) + const item = await asP(ssbClient.get, tipId).catch(() => null) + if (!item || !item.content) throw new Error("Room not found") + if (item.content.author !== userId) throw new Error("Not the author") + const c = item.content + let keyHex = null + let usesTribeKey = false + if (c.encrypted === true) { + if (c.tribeId) { + const tKeys = await getTribeKeysFor(c.tribeId) + if (tKeys.length) { keyHex = tKeys[0]; usesTribeKey = true } + } + if (!keyHex) keyHex = lookupKey(rootId) + if (!keyHex) throw new Error("Missing room key — cannot update room") + } + const enc = (text) => (c.encrypted === true ? encryptField(String(text), keyHex) : String(text)) + const updated = { ...mutate(c, enc), updatedAt: new Date().toISOString(), replaces: tipId } + await asP(ssbClient.publish, { type: "tombstone", target: tipId, deletedAt: new Date().toISOString(), author: userId }) + const res = await asP(ssbClient.publish, updated) + if (keyHex && !usesTribeKey) setRoomKey(res.key, keyHex) + return res + } + + const api = { + type: "room", + ROOM_MAX, + + async decryptContent(content, rootId) { + const tKeys = content && content.tribeId ? await getTribeKeysFor(content.tribeId) : [] + return decryptRoomFields(content, rootId, tKeys) + }, + + decryptContentPublicSync(content) { + if (!content) return null + if (content.encrypted !== true) return { title: safeText(content.title), description: safeText(content.description), tags: normalizeTags(content.tags) } + if (content.tribeId) return null + const keyHex = tryDecryptPublicInviteKey(content.invites) + if (!keyHex) return null + const f = decryptWithKeys(content, [keyHex]) + return f ? { title: f.title, description: f.description, tags: f.tags } : null + }, + + async resolveRootId(id) { + const ssbClient = await openSsb() + const idx = buildIndex(await readAll(ssbClient)) + const root = idx.groupRootOf(id) + if (idx.tomb.has(idx.contentTipOf(root))) throw new Error("Not found") + return root + }, + + async resolveCurrentId(id) { + const ssbClient = await openSsb() + const idx = buildIndex(await readAll(ssbClient)) + const tip = idx.contentTipOf(idx.groupRootOf(id)) + if (idx.tomb.has(tip)) throw new Error("Not found") + return tip + }, + + async createRoom({ title, description, image, status, tags, tribeId } = {}) { + const ssbClient = await openSsb() + const userId = ssbClient.id + const now = new Date().toISOString() + const type = tribeId ? "INVITE-ONLY" : normalizeStatus(status) + const tagsArr = normalizeTags(tags) + const rid = crypto.randomBytes(16).toString("hex") + const roomId = crypto.randomBytes(16).toString("hex") + const sealed = !!tribeId || type !== "OPEN" + const live = { token: await tokenFor(sealed ? roomId : rid), ...(await pickHub()), media: "audio" } + const base = { type: "room", status: tribeId ? "OPEN" : type, author: userId, members: [userId], invites: [], createdAt: now, updatedAt: now, ...live } + + if (type === "OPEN" && !tribeId) { + return asP(ssbClient.publish, { ...base, title: safeText(title), description: safeText(description), image: blobRef(image), tags: tagsArr, rid, encrypted: false }) + } + + let keyHex = null + let usesTribeKey = false + if (tribeId) { + const tKeys = await getTribeKeysFor(tribeId) + if (tKeys.length) { keyHex = tKeys[0]; usesTribeKey = true } + } + if (!keyHex) keyHex = crypto.randomBytes(32).toString("hex") + const enc = (text) => encryptField(String(text), keyHex) + const msg = await asP(ssbClient.publish, { + ...base, + title: enc(safeText(title)), + description: enc(safeText(description)), + image: enc(blobRef(image)), + tags: enc(tagsArr.join(",")), + roomId, + encrypted: true, + ...(tribeId ? { tribeId } : {}) + }) + if (!usesTribeKey) { + setRoomKey(msg.key, keyHex) + if (tribeCrypto) { + try { + const ssbKeys = require("../server/node_modules/ssb-keys") + await asP(ssbClient.publish, { type: "tribe-keys", tribeId: msg.key, generation: 1, memberKeys: { [userId]: tribeCrypto.boxKeyForMember(keyHex, userId, ssbKeys) } }) + } catch (_) {} + } + } + return msg + }, + + async updateRoomById(id, data = {}) { + const hub = data.refreshHub ? await pickHub() : null + return republish(id, (c, enc) => ({ + ...c, + title: data.title !== undefined ? enc(safeText(data.title)) : c.title, + description: data.description !== undefined ? enc(safeText(data.description)) : c.description, + image: data.image !== undefined ? enc(blobRef(data.image)) : c.image, + tags: data.tags !== undefined ? (c.encrypted === true ? enc(normalizeTags(data.tags).join(",")) : normalizeTags(data.tags)) : c.tags, + invites: data.invites !== undefined ? data.invites : c.invites, + ...(hub || {}) + })) + }, + + async rehomeHub(id) { + const ssbClient = await openSsb() + const picked = await pickHub() + if (!picked || picked.hub === ssbClient.id) return null + return republish(id, (c) => ({ ...c, ...picked })) + }, + + async closeRoomById(id) { + return republish(id, (c) => ({ ...c, status: "CLOSED" })) + }, + + async reopenRoomById(id) { + return republish(id, (c) => ({ ...c, status: roomTypeOf(c) })) + }, + + async setRoomLine(id, line) { + return republish(id, (c) => ({ ...c, line: line === "SWITCHBOARD" ? "SWITCHBOARD" : "DND" })) + }, + + async deleteRoomById(id) { + const tipId = await this.resolveCurrentId(id) + const ssbClient = await openSsb() + const item = await asP(ssbClient.get, tipId).catch(() => null) + if (!item || !item.content) throw new Error("Room not found") + if (item.content.author !== ssbClient.id) throw new Error("Not the author") + await asP(ssbClient.publish, { type: "tombstone", target: tipId, deletedAt: new Date().toISOString(), author: ssbClient.id }) + }, + + async addMemberToRoom(roomId, feedId, consumedCode) { + const tipId = await this.resolveCurrentId(roomId) + const ssbClient = await openSsb() + const rootId = await this.resolveRootId(roomId) + const item = await asP(ssbClient.get, tipId).catch(() => null) + if (!item || !item.content) throw new Error("Room not found") + const c = item.content + if (c.encrypted === true && !c.tribeId && !lookupKey(rootId)) { + const key = tryDecryptPublicInviteKey(c.invites) + if (key) setRoomKey(rootId, key) + } + const proofKey = feedId === ssbClient.id && c.encrypted === true && !c.tribeId ? lookupKey(rootId) : null + return asP(ssbClient.publish, { type: "roomMember", target: rootId, member: feedId, on: true, createdAt: new Date().toISOString(), ...(typeof consumedCode === "string" && consumedCode ? { code: consumedCode } : {}), ...(proofKey ? { keyProof: keyProofFor(proofKey, feedId) } : {}) }) + }, + + async leaveRoomMembership(roomId) { + const ssbClient = await openSsb() + const userId = ssbClient.id + const room = await this.getRoomById(roomId) + if (!room) throw new Error("Room not found") + if (room.author === userId) throw new Error("Author cannot leave their own room") + if (!room.members.includes(userId)) return + await asP(ssbClient.publish, { type: "roomMember", target: room.rootId, member: userId, on: false, createdAt: new Date().toISOString() }) + }, + + async ingestKeys() { await ingestOwnTribeKeys() }, + + async pruneOrphanKeys() { + if (!ownCrypto || typeof ownCrypto.getAllRootIds !== "function") return 0 + try { + const ssbClient = await openSsb() + const tomb = buildValidatedTombstoneSet(await readAll(ssbClient)) + let removed = 0 + for (const rid of ownCrypto.getAllRootIds()) { + if (tomb.has(rid)) { try { ownCrypto.dropKey(rid); removed += 1 } catch (_) {} } + } + return removed + } catch (_) { return 0 } + }, + + async getRoomById(id) { + const ssbClient = await openSsb() + const idx = memoIndex('rooms', await readAll(ssbClient), buildIndex) + const root = idx.groupRootOf(id) + const tip = idx.contentTipOf(root) + if (idx.tomb.has(tip)) return null + const node = idx.nodes.get(tip) + if (!node || node.c.type !== "room") return null + const tKeys = node.c.tribeId ? await getTribeKeysFor(node.c.tribeId) : [] + const room = buildRoom(node, root, tKeys, idx.resolveMembers(root)) + if (!room) return null + room.isClosed = room.status === "CLOSED" + return roomCollab.fold(room, await collectRooms(idx)) + }, + + async listAll({ filter = "all", viewerId } = {}) { + const ssbClient = await openSsb() + const uid = viewerId || ssbClient.id + const messages = await readAll(ssbClient) + const idx = buildIndex(messages) + let list = roomCollab.visibleThenCollapsed(await collectRooms(idx), uid) + try { await ensureMemberKeys(ssbClient, messages, list) } catch (_) {} + if (filter === "mine") list = list.filter(r => r.author === uid) + else if (filter === "recent") list = list.filter(r => new Date(r.createdAt).getTime() >= Date.now() - RECENT_MS) + else if (filter === "open") list = list.filter(r => r.type === "OPEN" && !r.isClosed) + else if (filter === "invite") list = list.filter(r => r.type === "INVITE-ONLY" && !r.isClosed) + else if (filter === "closed") list = list.filter(r => r.isClosed) + return list.slice().sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)) + }, + + async generateInvite(roomId, opts = {}) { + const ssbClient = await openSsb() + const room = await this.getRoomById(roomId) + if (!room) throw new Error("Room not found") + if (room.author !== ssbClient.id) throw new Error("Only the author can generate invites") + const rootId = await this.resolveRootId(roomId) + const keyHex = lookupKey(rootId) + const code = crypto.randomBytes(INVITE_BYTES).toString("hex") + let invite = code + const pubFlag = opts.public ? { public: true } : {} + const inviteSalt = generateInviteSalt() + if (keyHex) { + invite = opts.public + ? { code, ek: encryptForInvite(keyHex, code, inviteSalt), salt: inviteSalt, ...pubFlag } + : { ch: hashCode(code, inviteSalt), ek: encryptForInvite(keyHex, code, inviteSalt), salt: inviteSalt } + } else { + invite = opts.public ? { code, public: true } : { ch: hashCode(code, inviteSalt), salt: inviteSalt } + } + await this.updateRoomById(roomId, { invites: [...room.invites, invite] }) + return code + }, + + async getOpenInvite(roomId) { + return openInviteOf(await this.getRoomById(roomId).catch(() => null)) + }, + + async generateOpenInvite(roomId) { + const room = await this.getRoomById(roomId) + if (!room) throw new Error("Room not found") + if ((Array.isArray(room.invites) ? room.invites : []).find(inv => typeof inv === "object" && inv.public === true)) throw new Error("An open invitation already exists") + return this.generateInvite(roomId, { public: true }) + }, + + async removeOpenInvite(roomId) { + const ssbClient = await openSsb() + const room = await this.getRoomById(roomId) + if (!room) throw new Error("Room not found") + if (room.author !== ssbClient.id) throw new Error("Only the author can remove invites") + await this.updateRoomById(roomId, { invites: (Array.isArray(room.invites) ? room.invites : []).filter(inv => !(typeof inv === "object" && inv.public === true)) }) + await rotateRoomKey(await this.resolveRootId(roomId), [...new Set([room.author, ...(Array.isArray(room.members) ? room.members : [])])]) + }, + + async joinByInvite(code) { + const ssbClient = await openSsb() + const userId = ssbClient.id + const rooms = await collectRooms(buildIndex(await readAll(ssbClient))) + let matchedRoom = null + let matchedInvite = null + for (const r of rooms) { + for (const inv of r.invites) { + if (inviteMatches(inv, code)) { matchedRoom = r; matchedInvite = inv; break } + } + if (matchedRoom) break + } + if (!matchedRoom) throw new Error("Invalid or expired invite code") + if (matchedRoom.members.includes(userId)) throw new Error("Already a member") + const isPublic = typeof matchedInvite === "object" && matchedInvite.public === true + const resolvedRootId = await this.resolveRootId(matchedRoom.rootId) + const token = inviteToken(matchedInvite) || code + if (!isPublic) { + const idx = buildIndex(await readAll(ssbClient)) + if (idx.isCodeConsumed(resolvedRootId, token) || idx.isCodeConsumed(resolvedRootId, code)) throw new Error("Invite already used") + } + let roomKey = null + if (typeof matchedInvite === "object" && matchedInvite.ek) { + roomKey = decryptFromInvite(matchedInvite.ek, code, matchedInvite.salt) + if (roomKey) setRoomKey(resolvedRootId, roomKey) + } + await this.addMemberToRoom(matchedRoom.rootId, userId, isPublic ? "" : token) + if (tribeCrypto && roomKey) { + try { + const ssbKeys = require("../server/node_modules/ssb-keys") + const memberKeys = {} + try { memberKeys[userId] = tribeCrypto.boxKeyForMember(roomKey, userId, ssbKeys) } catch (_) {} + if (matchedRoom.author && matchedRoom.author !== userId) { + try { memberKeys[matchedRoom.author] = tribeCrypto.boxKeyForMember(roomKey, matchedRoom.author, ssbKeys) } catch (_) {} + } + if (Object.keys(memberKeys).length) await asP(ssbClient.publish, { type: "tribe-keys", tribeId: resolvedRootId, generation: 1, memberKeys }) + } catch (_) {} + } + return matchedRoom.rootId + }, + + async liveParams(room) { + if (!room || !RID.test(String(room.rid || "")) || !room.token) return null + let secrets = null + if (room.tribeId) secrets = (await getTribeKeysFor(room.tribeId)).slice(0, 1) + else if (room.type === "INVITE-ONLY") secrets = lookupKeys(room.rootId).slice(0, 1) + return { + rid: room.rid, owner: room.author, token: room.token, hub: room.hub, address: room.hubAddress, + ref: room.rootId, title: room.title, secrets: Array.isArray(secrets) && secrets.length ? secrets : null, + ...(room.roomId ? { roomId: room.roomId } : {}) + } + }, + + async occupancy(room) { + const ph = await phone() + if (!ph || typeof ph.roomCount !== "function" || !room || !RID.test(String(room.rid || ""))) return null + try { return await asP(ph.roomCount, { rid: room.rid, hub: room.hub }) } catch (_) { return null } + }, + + async occupancies(rooms) { + const out = new Map() + await Promise.all((rooms || []).map(async (r) => { const o = await this.occupancy(r); if (o) out.set(r.rootId, o) })) + return out + }, + + async join(room) { + const ph = await phone() + if (!ph || typeof ph.roomJoin !== "function") throw new Error("unavailable") + const params = await this.liveParams(room) + if (!params) throw new Error("invalid") + return asP(ph.roomJoin, params) + }, + + async leave() { + const ph = await phone() + if (ph && typeof ph.roomLeave === "function") await asP(ph.roomLeave).catch(() => null) + }, + + async mute(flag) { + const ph = await phone() + if (!ph || typeof ph.roomMute !== "function") return null + return asP(ph.roomMute, !!flag) + }, + + async hand(flag) { + const ph = await phone() + if (!ph || typeof ph.roomHand !== "function") return null + return asP(ph.roomHand, !!flag) + }, + + async silence(id, flag) { + const ph = await phone() + if (!ph || typeof ph.silence !== "function") return null + return asP(ph.silence, { id, on: !!flag }) + }, + + async camera(flag) { + const ph = await phone() + if (!ph || typeof ph.camera !== "function") return null + return asP(ph.camera, !!flag) + }, + + async stream(opts) { + const ph = await phone() + if (!ph || typeof ph.stream !== "function") return null + return asP(ph.stream, opts || {}) + }, + + async viewerJoin(room) { + const ph = await phone() + if (!ph || typeof ph.viewerJoin !== "function") return null + const params = await this.liveParams(room) + if (!params || params.secrets) return null + return asP(ph.viewerJoin, params) + }, + + async viewerLeave(rid) { + const ph = await phone() + if (!ph || typeof ph.viewerLeave !== "function") return null + return asP(ph.viewerLeave, { rid: String(rid || "") }) + }, + + async viewerRooms() { + const ph = await phone() + if (!ph || typeof ph.viewerRooms !== "function") return [] + const list = await rpcValue(ph.viewerRooms) + return Array.isArray(list) ? list : [] + }, + + async publicFeed(rid, kind) { + const ph = await phone() + return ph && typeof ph.publicFeed === "function" ? ph.publicFeed({ rid: String(rid || ""), kind: kind === "sound" ? "sound" : "grid" }) : null + }, + + async recStart() { + const ph = await phone() + if (!ph || typeof ph.roomRecStart !== "function") return null + return asP(ph.roomRecStart) + }, + + async recStop() { + const ph = await phone() + if (!ph || typeof ph.roomRecStop !== "function") return null + return asP(ph.roomRecStop) + }, + + async notify(flag) { + const ph = await phone() + if (!ph || typeof ph.roomNotify !== "function") return null + return asP(ph.roomNotify, !!flag) + }, + + async clearEvents() { + const ph = await phone() + if (!ph || typeof ph.roomClearEvents !== "function") return null + return asP(ph.roomClearEvents) + }, + + async liveState() { + const ph = await phone() + return ph && typeof ph.roomState === "function" ? rpcValue(ph.roomState) : null + }, + + async findByNumber(value, viewerId) { + const number = normalizeNumber(value) + if (!number) return [] + return (await this.listAll({ filter: "all", viewerId })).filter(r => r.number === number && !r.isClosed) + } + } + return api +} diff --git a/src/models/school_model.js b/src/models/school_model.js new file mode 100644 index 0000000..7e63678 --- /dev/null +++ b/src/models/school_model.js @@ -0,0 +1,1423 @@ +const pull = require("../server/node_modules/pull-stream") +const moment = require("../server/node_modules/moment") +const { getConfig } = require("../configs/config-manager.js") +const { readTyped, memoIndex } = require("./typed_log") +const { buildValidatedTombstoneSet } = require("./tombstone_validator") +const { versionGroups } = require("../backend/dedupe") +const opinionCategories = require("../backend/opinion_categories") +const logLimit = getConfig().ssbLogStream?.limit || 1000 + +const norm = (s) => String(s || "").trim().toLowerCase() +const toNum = (v) => { + const n = parseFloat(String(v ?? "").replace(",", ".")) + return Number.isFinite(n) ? n : NaN +} + +const normalizeTags = (raw) => { + if (raw === undefined || raw === null) return [] + if (Array.isArray(raw)) return raw.map(t => String(t || "").trim()).filter(Boolean) + return String(raw).split(",").map(t => t.trim()).filter(Boolean) +} + +const normalizeDate = (raw) => { + const t = Date.parse(String(raw || "")) + return Number.isFinite(t) ? new Date(t).toISOString() : null +} + +const normalizeIds = (raw) => { + if (raw === undefined || raw === null) return [] + const arr = Array.isArray(raw) ? raw : String(raw).split(/[\s,]+/) + return Array.from(new Set(arr.map(t => String(t || "").trim()).filter(t => t.startsWith("@")))) +} + +const matchSearch = (course, q) => { + const qq = norm(q) + if (!qq) return true + const hay = [ + course.title, + course.description, + Array.isArray(course.tags) ? course.tags.join(" ") : "" + ].map(x => norm(x)).join(" ") + return hay.includes(qq) +} + +module.exports = ({ cooler, transfersModel, schoolCrypto, chatsModel }) => { + const ssbKeys = require("../server/node_modules/ssb-keys") + const nodeCrypto = require("crypto") + let ssb + const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb } + + const SCHOOL_TYPES = [ + "schoolCourse", "schoolLesson", "schoolLessonMedia", "schoolExam", "schoolExamQuestion", + "schoolExamResult", "schoolProgress", "schoolOpinion", "schoolCertificate", "schoolCommentHide", + "schoolEnroll", "school-invite", "tombstone", "tribe-keys", "transfer", "transferConfirm", "chatMember", "chat" + ] + + const readAll = async (ssbClient) => readTyped(ssbClient, SCHOOL_TYPES, { limit: logLimit, withWindow: true, withPrivate: true }) + + const indexOf = (messages, ssbClient) => memoIndex('school', messages, (m) => buildIndex(m, ssbClient)) + + const buildIndex = (messages, ssbClient) => { + const tomb = buildValidatedTombstoneSet(messages) + const courseNodes = new Map() + const lessonNodes = new Map() + const certNodes = new Map() + const lessonReplaces = new Map() + const enrollLatest = new Map() + const transferAgg = new Map() + const transferNodes = new Map() + const transferConfirms = [] + const tribeKeyMsgs = [] + const opinionMsgs = [] + const examNodes = new Map() + const questionNodes = new Map() + const progressLatest = new Map() + const examResultLatest = new Map() + const chatByCourse = new Map() + const chatMsgs = [] + + for (const m of messages) { + const key = m.key + const v = m.value || {} + const c = v.content + if (!c) continue + + if (c.type === "chat") { + if (typeof c.courseId === "string" && c.courseId) chatMsgs.push({ courseId: c.courseId, key, author: v.author }) + continue + } + + if (c.type === "tombstone") continue + + if (c.type === "schoolCourse") { + courseNodes.set(key, { key, ts: v.timestamp || m.timestamp || 0, seq: v.sequence || 0, c, author: v.author }) + continue + } + + if (c.type === "schoolLesson" && c.courseId) { + lessonNodes.set(key, { key, ts: v.timestamp || m.timestamp || 0, c, author: v.author }) + if (c.replaces) lessonReplaces.set(key, c.replaces) + continue + } + + if (c.type === "schoolCertificate" && c.courseId) { + certNodes.set(key, { key, ts: v.timestamp || m.timestamp || 0, c, author: v.author }) + continue + } + + if (c.type === "schoolEnroll" && c.courseId) { + const author = v.author + if (!author) continue + const ts = v.timestamp || m.timestamp || 0 + const k = `${c.courseId}::${author}` + const prev = enrollLatest.get(k) + if (!prev || ts >= prev.ts) enrollLatest.set(k, { ts, value: !!c.value, author, courseId: c.courseId, transferId: c.transferId || null, keyProof: c.keyProof || null }) + continue + } + + if (c.type === "transfer") { + if (!v.author || c.from !== v.author) continue + transferNodes.set(key, { author: v.author, to: c.to, amount: toNum(c.amount), concept: String(c.concept || ""), discarded: String(c.status || "").toUpperCase() === "DISCARDED", replaces: c.replaces || null }) + continue + } + + if (c.type === "transferConfirm" && c.target) { + if (v.author) transferConfirms.push({ target: c.target, author: v.author }) + continue + } + + if (c.type === "tribe-keys" && c.tribeId && c.memberKeys && typeof c.memberKeys === "object") { + tribeKeyMsgs.push({ tribeId: c.tribeId, memberKeys: c.memberKeys, author: v.author }) + continue + } + + if (c.type === "schoolOpinion" && c.target) { + opinionMsgs.push({ target: c.target, author: v.author, category: c.category }) + continue + } + + if (c.type === "schoolLessonMedia" && c.courseId && c.target) { + examNodes.set("media:" + key, { key, ts: v.timestamp || m.timestamp || 0, c, author: v.author, isMedia: true }) + continue + } + + if (c.type === "schoolExamQuestion" && c.courseId && c.target) { + questionNodes.set(key, { key, ts: v.timestamp || m.timestamp || 0, c, author: v.author }) + continue + } + + if (c.type === "schoolExam" && c.courseId) { + examNodes.set(key, { key, ts: v.timestamp || m.timestamp || 0, c, author: v.author }) + continue + } + + if (c.type === "schoolProgress" && c.courseId && c.lessonId) { + const ts = v.timestamp || m.timestamp || 0 + const k = `${c.courseId}::${c.lessonId}::${v.author}` + const prev = progressLatest.get(k) + if (!prev || ts >= prev.ts) progressLatest.set(k, { ts, value: c.value !== false, author: v.author, courseId: c.courseId, lessonId: c.lessonId }) + continue + } + } + + if (ssbClient && ssbClient.private && typeof ssbClient.private.unbox === "function") { + for (const m of messages) { + const sealed = typeof m.value?.content === "string" + if (!sealed && m.value?.private !== true) continue + try { + const dec = sealed ? ssbClient.private.unbox({ key: m.key, value: m.value, timestamp: m.value?.timestamp || m.timestamp || 0 }) : m + const c = dec?.value?.content + const author = dec?.value?.author + if (!c || !author) continue + const ts = dec.value.timestamp || m.timestamp || 0 + if (c.type === "schoolEnroll" && c.courseId) { + const k = `${c.courseId}::${author}` + const prev = enrollLatest.get(k) + if (!prev || ts >= prev.ts) enrollLatest.set(k, { ts, value: !!c.value, author, courseId: c.courseId, transferId: c.transferId || null, keyProof: c.keyProof || null }) + continue + } + if (c.type === "schoolProgress" && c.courseId && c.lessonId) { + const k = `${c.courseId}::${c.lessonId}::${author}` + const prev = progressLatest.get(k) + if (!prev || ts >= prev.ts) progressLatest.set(k, { ts, value: c.value !== false, author, courseId: c.courseId, lessonId: c.lessonId }) + continue + } + if (c.type === "schoolExamResult" && c.courseId && c.examId) { + const k = `${c.examId}::${author}` + const prev = examResultLatest.get(k) + if (!prev || ts >= prev.ts) examResultLatest.set(k, { ts, author, courseId: c.courseId, examId: c.examId, score: c.score, total: c.total }) + continue + } + } catch {} + } + } + + const transferChild = new Map() + const transferForked = new Set() + for (const [key, t] of transferNodes.entries()) { + const orig = t.replaces ? transferNodes.get(t.replaces) : null + if (!orig || orig.author !== t.author) continue + if (transferChild.has(t.replaces)) transferForked.add(t.replaces) + transferChild.set(t.replaces, key) + } + const confirmersOf = new Map() + for (const cf of transferConfirms) { + if (!confirmersOf.has(cf.target)) confirmersOf.set(cf.target, new Set()) + confirmersOf.get(cf.target).add(cf.author) + } + for (const [key, t] of transferNodes.entries()) { + const orig = t.replaces ? transferNodes.get(t.replaces) : null + if (orig && orig.author === t.author) continue + const chain = [key] + while (transferChild.has(chain[chain.length - 1])) chain.push(transferChild.get(chain[chain.length - 1])) + const tipKey = chain[chain.length - 1] + const tip = transferNodes.get(tipKey) + const signers = new Set() + for (const ck of chain) for (const signer of (confirmersOf.get(ck) || [])) if (signer === tip.to) signers.add(signer) + const bill = { author: tip.author, to: tip.to, amount: tip.amount, concept: tip.concept, void: tip.discarded || tomb.has(tipKey) || chain.some(ck => transferForked.has(ck)), signers } + for (const ck of chain) transferAgg.set(ck, bill) + } + + const lessonParent = new Map() + for (const [key, target] of lessonReplaces.entries()) { + const node = lessonNodes.get(key) + const orig = lessonNodes.get(target) + if (node && orig && String(orig.author) === String(node.author)) lessonParent.set(key, target) + } + const lessonChildOf = new Map() + for (const [k, t] of lessonParent.entries()) lessonChildOf.set(t, k) + const lessonRootOf = (id) => { + let cur = id + while (lessonParent.has(cur)) cur = lessonParent.get(cur) + return cur + } + + const vg = versionGroups(Array.from(courseNodes.values()).map(n => ({ key: n.key, author: n.author, seq: n.seq, ts: n.ts, replaces: typeof n.c.replaces === "string" ? n.c.replaces : null, claimedAuthor: typeof n.c.author === "string" ? n.c.author : null }))) + const rootOf = vg.rootOf + const tipOf = vg.tipOf + + const tipByRoot = new Map() + for (const r of vg.roots) tipByRoot.set(r, tipOf(r)) + + const courseAuthorOf = (id) => { const n = courseNodes.get(rootOf(id)); return n ? n.author : null } + for (const [key, node] of lessonNodes.entries()) if (courseAuthorOf(node.c.courseId) !== node.author) lessonNodes.delete(key) + for (const [key, node] of certNodes.entries()) if (courseAuthorOf(node.c.courseId) !== node.author) certNodes.delete(key) + for (const [key, node] of examNodes.entries()) if (courseAuthorOf(node.c.courseId) !== node.author) examNodes.delete(key) + for (const [key, node] of questionNodes.entries()) if (courseAuthorOf(node.c.courseId) !== node.author) questionNodes.delete(key) + for (const ch of chatMsgs) { + if (chatByCourse.has(ch.courseId) || courseAuthorOf(ch.courseId) !== ch.author) continue + chatByCourse.set(ch.courseId, ch.key) + } + + const enrollByCourse = new Map() + for (const { courseId, author, value, transferId, keyProof } of enrollLatest.values()) { + const root = rootOf(courseId) + if (!enrollByCourse.has(root)) enrollByCourse.set(root, new Map()) + const enrollments = enrollByCourse.get(root) + if (value) enrollments.set(author, { transferId, keyProof: keyProof || null }) + else enrollments.delete(author) + } + + const opinionsByCourse = new Map() + for (const op of opinionMsgs) { + const root = rootOf(op.target) + if (!courseNodes.has(root)) continue + if (!opinionsByCourse.has(root)) opinionsByCourse.set(root, { byAuthor: new Map() }) + const agg = opinionsByCourse.get(root) + if (!op.author || agg.byAuthor.has(op.author)) continue + agg.byAuthor.set(op.author, op.category) + } + + const progressByCourse = new Map() + for (const pr of progressLatest.values()) { + if (!pr.value) continue + const root = rootOf(pr.courseId) + if (!courseNodes.has(root)) continue + if (!progressByCourse.has(root)) progressByCourse.set(root, new Map()) + const perStudent = progressByCourse.get(root) + if (!perStudent.has(pr.author)) perStudent.set(pr.author, new Set()) + perStudent.get(pr.author).add(lessonRootOf(pr.lessonId)) + } + + const grantsByCourse = new Map() + const myId = ssbClient ? ssbClient.id : null + for (const tk of tribeKeyMsgs) { + const root = rootOf(tk.tribeId) + if (!courseNodes.has(root)) continue + if (courseNodes.get(root).author !== tk.author) continue + if (!grantsByCourse.has(root)) grantsByCourse.set(root, new Set()) + for (const member of Object.keys(tk.memberKeys)) grantsByCourse.get(root).add(member) + if (schoolCrypto && myId && tk.memberKeys[myId]) { + try { + const cfg = require("../server/ssb_config") + const unboxed = ssbKeys.unbox(tk.memberKeys[myId], cfg.keys) + if (typeof unboxed === "string" && unboxed) schoolCrypto.addNewKey(root, unboxed) + } catch {} + } + } + + return { tomb, courseNodes, lessonNodes, certNodes, examNodes, vg, rootOf, tipOf, tipByRoot, enrollByCourse, transferAgg, grantsByCourse, opinionsByCourse, progressByCourse, examResultLatest, lessonRootOf, lessonChildOf, questionNodes, chatByCourse } + } + + const keyProofFor = (keyHex, studentId) => { + try { return nodeCrypto.createHmac("sha256", Buffer.from(keyHex, "hex")).update(String(studentId), "utf8").digest("hex") } catch (_) { return null } + } + + const buildCourseObject = (node, rootId, enrollments, transferAgg, grants, opinionAgg, chatByCourse = new Map()) => { + let c = node.c || {} + const ringKeys = schoolCrypto ? (schoolCrypto.getKeys(rootId) || []) : [] + if (c.encryptedPayload) { + const dec = ringKeys.length && schoolCrypto ? schoolCrypto.decryptContent(c, ringKeys.map(k => [k])) : { ...c, _undecryptable: true } + if (dec._undecryptable) return { undecryptable: true, id: node.key, rootId, author: node.author } + c = dec + } + let blobId = c.image || null + if (blobId && /\(([^)]+)\)/.test(String(blobId))) blobId = String(blobId).match(/\(([^)]+)\)/)[1] + + const priceN = toNum(c.price) + const paid = Number.isFinite(priceN) && priceN > 0 + const students = [] + const pending = [] + const grantedViaInvite = [] + for (const [student, info] of (enrollments || new Map())) { + if (info.keyProof && ringKeys.some(k => keyProofFor(k, student) === info.keyProof)) grantedViaInvite.push(student) + if (paid) { + const bill = info.transferId ? (transferAgg && transferAgg.get(info.transferId)) : null + const settled = !!bill && !bill.void && bill.author === student && bill.to === node.author && bill.amount >= priceN && bill.concept.includes(rootId) && bill.signers.has(node.author) + if (settled) students.push(student) + else pending.push({ author: student, transferId: info.transferId || null }) + } else { + students.push(student) + } + } + const studentOpinions = { opinions: {}, voters: [] } + for (const [author, category] of ((opinionAgg && opinionAgg.byAuthor) || new Map())) { + if (!students.includes(author)) continue + studentOpinions.voters.push(author) + if (category) studentOpinions.opinions[category] = (studentOpinions.opinions[category] || 0) + 1 + } + return { + id: node.key, + rootId, + title: c.title, + description: c.description, + tags: Array.isArray(c.tags) ? c.tags : normalizeTags(c.tags), + image: blobId, + price: Number.isFinite(priceN) && priceN > 0 ? priceN.toFixed(6) : "0.000000", + visibility: String(c.visibility || "PUBLIC").toUpperCase() === "INVITE" ? "INVITE" : "PUBLIC", + status: String(c.status || "ONGOING").toUpperCase() === "CLOSED" ? "CLOSED" : "ONGOING", + invited: normalizeIds(c.invited).filter(id => !students.includes(id)), + startDate: c.startDate || null, + chatId: c.chatId || chatByCourse.get(rootId) || null, + inviteCode: c.inviteCode || null, + author: node.author, + createdAt: c.createdAt || new Date(node.ts).toISOString(), + updatedAt: c.updatedAt || null, + students, + pending, + granted: Array.from(new Set([...(grants ? Array.from(grants) : []), ...grantedViaInvite])), + opinions: studentOpinions.opinions, + opinions_inhabitants: studentOpinions.voters + } + } + + const isProtected = (course) => Number(course.price) > 0 || course.visibility === "INVITE" + + const addToChat = async (ssbClient, course, members) => { + if (!course.chatId || !members.length) return + for (const member of members) { + try { + await new Promise((res) => ssbClient.publish({ type: "chatMember", target: course.chatId, member, on: true, createdAt: new Date().toISOString() }, () => res())) + } catch {} + } + if (chatsModel && typeof chatsModel.listAll === "function") { + try { await chatsModel.listAll({ filter: "all", viewerId: ssbClient.id }) } catch {} + } + } + + const canView = (course, viewer) => + course.visibility === "PUBLIC" || + course.author === viewer || + course.invited.includes(viewer) || + course.students.includes(viewer) || + course.pending.some(p => p.author === viewer) + + const ensureCourseKey = async (ssbClient, rootId) => { + if (!schoolCrypto) return null + let key = schoolCrypto.getKey(rootId) + if (key) return key + key = schoolCrypto.generateTribeKey() + schoolCrypto.setKey(rootId, key, 1) + await publishKeyGrant(ssbClient, rootId, key, [ssbClient.id]) + return key + } + + const publishKeyGrant = (ssbClient, rootId, key, members) => { + const memberKeys = {} + for (const member of members) { + try { memberKeys[member] = schoolCrypto.boxKeyForMember(key, member, ssbKeys) } catch {} + } + if (!Object.keys(memberKeys).length) return Promise.resolve(null) + return new Promise((res) => ssbClient.publish({ type: "tribe-keys", tribeId: rootId, generation: 1, memberKeys }, () => res(null))) + } + + return { + type: "schoolCourse", + + async createCourse(data) { + const ssbClient = await openSsb() + + const title = String(data.title || "").trim() + const description = String(data.description || "").trim() + if (!title) throw new Error("Invalid title") + if (!description) throw new Error("Invalid description") + + let blobId = data.image || null + if (blobId && /\(([^)]+)\)/.test(String(blobId))) blobId = String(blobId).match(/\(([^)]+)\)/)[1] + + const priceN = toNum(data.price) + const protectedCourse = (Number.isFinite(priceN) && priceN > 0) || String(data.visibility || "PUBLIC").toUpperCase() === "INVITE" + const content = { + type: "schoolCourse", + title, + description, + tags: normalizeTags(data.tags), + image: blobId, + price: Number.isFinite(priceN) && priceN > 0 ? priceN.toFixed(6) : "0.000000", + visibility: String(data.visibility || "PUBLIC").toUpperCase() === "INVITE" ? "INVITE" : "PUBLIC", + status: "ONGOING", + invited: normalizeIds(data.invited), + startDate: (() => { + const d = normalizeDate(data.startDate) + if (d && d.slice(0, 10) < new Date().toISOString().slice(0, 10)) throw new Error("Start date cannot be in the past") + return d + })(), + chatId: null, + author: ssbClient.id, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString() + } + + let toPublish = content + let courseKey = null + if (content.visibility === "INVITE" && schoolCrypto) { + courseKey = schoolCrypto.generateTribeKey() + toPublish = schoolCrypto.encryptContent(content, [courseKey], true) + } + const published = await new Promise((res, rej) => ssbClient.publish(toPublish, (e, m) => e ? rej(e) : res(m))) + if (published && published.key && chatsModel) { + try { + await chatsModel.createChat(`Course: ${title}`, description, blobId, "school", protectedCourse ? "INVITE-ONLY" : "OPEN", normalizeTags(data.tags), null, { courseId: published.key }) + } catch {} + } + if (published && published.key) { + if (courseKey) { + schoolCrypto.setKey(published.key, courseKey, 1) + await publishKeyGrant(ssbClient, published.key, courseKey, [ssbClient.id]) + } else if (Number(content.price) > 0) { + await ensureCourseKey(ssbClient, published.key) + } + } + return published + }, + + async resolveCurrentId(courseId) { + const ssbClient = await openSsb() + const messages = await readAll(ssbClient) + const { tomb, vg } = indexOf(messages, ssbClient) + + const cur = vg.tipOf(courseId) + if (tomb.has(cur) || vg.isForeignCopy(vg.rootOf(cur))) throw new Error("Course not found") + return cur + }, + + async resolveRootId(courseId) { + const ssbClient = await openSsb() + const messages = await readAll(ssbClient) + const { tomb, vg } = indexOf(messages, ssbClient) + + if (tomb.has(vg.tipOf(courseId)) || vg.isForeignCopy(vg.rootOf(courseId))) throw new Error("Course not found") + return vg.rootOf(courseId) + }, + + async updateCourse(id, data) { + const ssbClient = await openSsb() + const messages = await readAll(ssbClient) + const idx = indexOf(messages, ssbClient) + + const tipId = await this.resolveCurrentId(id) + const node = idx.courseNodes.get(tipId) + if (!node || !node.c) throw new Error("Course not found") + + let existingContent = node.c + if (existingContent.encryptedPayload) { + const rootForKeys = idx.rootOf(tipId) + const keys = schoolCrypto ? (schoolCrypto.getKeys(rootForKeys) || []) : [] + const dec = keys.length && schoolCrypto ? schoolCrypto.decryptContent(existingContent, keys.map(k => [k])) : { ...existingContent, _undecryptable: true } + if (dec._undecryptable) throw new Error("Course not found") + delete dec._decrypted + existingContent = dec + } + const author = existingContent.author + if (author !== ssbClient.id) throw new Error("Unauthorized") + + const patch = {} + + if (data.title !== undefined) { + const t = String(data.title || "").trim() + if (!t) throw new Error("Invalid title") + patch.title = t + } + + if (data.description !== undefined) { + const d = String(data.description || "").trim() + if (!d) throw new Error("Invalid description") + patch.description = d + } + + if (data.tags !== undefined) patch.tags = normalizeTags(data.tags) + + if (data.image !== undefined) { + let blobId = data.image + if (blobId && /\(([^)]+)\)/.test(String(blobId))) blobId = String(blobId).match(/\(([^)]+)\)/)[1] + patch.image = blobId || null + } + + if (data.price !== undefined) { + const p = toNum(data.price) + if (Number.isFinite(p) && p < 0) throw new Error("Invalid price") + patch.price = Number.isFinite(p) && p > 0 ? p.toFixed(6) : "0.000000" + } + + if (data.visibility !== undefined) { + patch.visibility = String(data.visibility || "PUBLIC").toUpperCase() === "INVITE" ? "INVITE" : "PUBLIC" + } + + if (data.status !== undefined) { + const s = String(data.status || "").toUpperCase() + if (!["ONGOING", "CLOSED"].includes(s)) throw new Error("Invalid status") + patch.status = s + } + + if (data.invited !== undefined) patch.invited = normalizeIds(data.invited) + + if (data.inviteCode !== undefined) patch.inviteCode = String(data.inviteCode || "") || null + + if (data.startDate !== undefined) patch.startDate = normalizeDate(data.startDate) + + const next = { + ...existingContent, + ...patch, + author, + createdAt: existingContent.createdAt, + updatedAt: new Date().toISOString(), + replaces: tipId, + type: "schoolCourse" + } + + const tombMsg = { + type: "tombstone", + target: tipId, + deletedAt: new Date().toISOString(), + author: ssbClient.id + } + + await new Promise((res, rej) => ssbClient.publish(tombMsg, (e) => e ? rej(e) : res())) + let toPublish = next + if (String(next.visibility || "").toUpperCase() === "INVITE" && schoolCrypto) { + const key = await ensureCourseKey(ssbClient, idx.rootOf(tipId)) + if (key) toPublish = schoolCrypto.encryptContent(next, [key], true) + } + const published = await new Promise((res, rej) => ssbClient.publish(toPublish, (e, m) => e ? rej(e) : res(m))) + const linkedChatId = existingContent.chatId || idx.chatByCourse.get(idx.rootOf(tipId)) || null + if (patch.image !== undefined && linkedChatId && chatsModel && typeof chatsModel.updateChatById === "function") { + try { await chatsModel.updateChatById(linkedChatId, { image: patch.image }) } catch {} + } + return published + }, + + async updateCourseStatus(id, status) { + return this.updateCourse(id, { status: String(status || "").toUpperCase() }) + }, + + async deleteCourse(id) { + const ssbClient = await openSsb() + const tipId = await this.resolveCurrentId(id) + const course = await this.getCourseById(tipId) + if (!course || course.author !== ssbClient.id) throw new Error("Unauthorized") + + const tombMsg = { + type: "tombstone", + target: tipId, + deletedAt: new Date().toISOString(), + author: ssbClient.id + } + + return new Promise((res, rej) => ssbClient.publish(tombMsg, (e, r) => e ? rej(e) : res(r))) + }, + + async inviteStudent(id, studentId) { + const ssbClient = await openSsb() + const course = await this.getCourseById(id) + const ids = normalizeIds(studentId) + if (!ids.length) throw new Error("Invalid student id") + const updated = await this.updateCourse(course.id, { invited: course.invited.concat(ids) }) + if (isProtected(course) && schoolCrypto) { + const key = await ensureCourseKey(ssbClient, course.rootId) + if (key) await publishKeyGrant(ssbClient, course.rootId, key, ids) + } + return updated + }, + + async uninviteStudent(id, studentId) { + const course = await this.getCourseById(id) + const drop = new Set(normalizeIds(studentId)) + return this.updateCourse(course.id, { invited: course.invited.filter(s => !drop.has(s)) }) + }, + + async enroll(id) { + const ssbClient = await openSsb() + const me = ssbClient.id + + const course = await this.getCourseById(id) + if (course.author === me) throw new Error("Cannot enroll in your own course") + if (course.status !== "ONGOING") throw new Error("Course is closed") + if (course.students.includes(me)) return { alreadyEnrolled: true } + if (course.pending.some(p => p.author === me)) return { alreadyPending: true } + if (course.visibility === "INVITE" && !course.invited.includes(me)) throw new Error("Invitation required") + + const rootId = course.rootId || (await this.resolveRootId(id)) + + let transferId = null + if (Number(course.price) > 0) { + if (!transfersModel) throw new Error("Transfers module unavailable") + const deadline = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString() + const bill = await transfersModel.createTransfer(course.author, `SCHOOL ${rootId}`, course.price, deadline, ["SCHOOL"], "ECONOMIC") + transferId = bill && bill.key ? bill.key : null + } + + const msg = { + type: "schoolEnroll", + courseId: rootId, + value: true, + transferId, + createdAt: new Date().toISOString() + } + + if (isProtected(course)) { + return new Promise((res, rej) => ssbClient.private.publish(msg, [me, course.author], (e, m) => e ? rej(e) : res(m))) + } + const published = await new Promise((res, rej) => ssbClient.publish(msg, (e, m) => e ? rej(e) : res(m))) + if (course.chatId && chatsModel && typeof chatsModel.joinChat === "function") { + try { await chatsModel.joinChat(course.chatId) } catch {} + } + return published + }, + + async unenroll(id) { + const ssbClient = await openSsb() + const me = ssbClient.id + + const course = await this.getCourseById(id) + if (!course.students.includes(me) && !course.pending.some(pn => pn.author === me)) return { notEnrolled: true } + + const rootId = course.rootId || (await this.resolveRootId(id)) + + const msg = { + type: "schoolEnroll", + courseId: rootId, + value: false, + createdAt: new Date().toISOString() + } + + if (isProtected(course)) { + return new Promise((res, rej) => ssbClient.private.publish(msg, [me, course.author], (e, m) => e ? rej(e) : res(m))) + } + return new Promise((res, rej) => ssbClient.publish(msg, (e, m) => e ? rej(e) : res(m))) + }, + + async addLesson(courseId, data) { + const ssbClient = await openSsb() + const course = await this.getCourseById(courseId) + if (course.author !== ssbClient.id) throw new Error("Unauthorized") + + const title = String(data.title || "").trim() + const text = String(data.text || "").trim() + if (!title) throw new Error("Invalid title") + if (!text) throw new Error("Invalid text") + + const rootId = course.rootId || (await this.resolveRootId(courseId)) + + const orderN = toNum(data.order) + let content = { + type: "schoolLesson", + courseId: rootId, + title, + text, + unit: String(data.unit || "").trim() || null, + order: Number.isFinite(orderN) ? orderN : null, + sessionDate: (() => { + const d = normalizeDate(data.sessionDate) + if (d && d.slice(0, 10) < new Date().toISOString().slice(0, 10)) throw new Error("Session date cannot be in the past") + return d + })(), + author: ssbClient.id, + createdAt: new Date().toISOString() + } + + if (isProtected(course) && schoolCrypto) { + const key = await ensureCourseKey(ssbClient, rootId) + if (key) content = schoolCrypto.encryptContent(content, [key], true) + } + + return new Promise((res, rej) => ssbClient.publish(content, (e, m) => e ? rej(e) : res(m))) + }, + + async createOpinion(courseId, category) { + if (!opinionCategories.includes(category)) throw new Error("Invalid voting category") + const ssbClient = await openSsb() + const me = ssbClient.id + const course = await this.getCourseById(courseId) + if (course.author === me) throw new Error("Cannot rate your own course") + if (!course.students.includes(me)) throw new Error("Only students can rate a course") + if (course.opinions_inhabitants.includes(me)) throw new Error("Already voted") + + const content = { type: "schoolOpinion", target: course.rootId, category, createdAt: new Date().toISOString() } + return new Promise((res, rej) => ssbClient.publish(content, (e, m) => e ? rej(e) : res(m))) + }, + + async markLesson(courseId, lessonId, value = true) { + const ssbClient = await openSsb() + const me = ssbClient.id + const course = await this.getCourseById(courseId) + if (!course.students.includes(me)) throw new Error("Only students can track progress") + + const msg = { + type: "schoolProgress", + courseId: course.rootId, + lessonId, + value: value !== false, + createdAt: new Date().toISOString() + } + + if (isProtected(course)) { + return new Promise((res, rej) => ssbClient.private.publish(msg, [me, course.author], (e, m) => e ? rej(e) : res(m))) + } + return new Promise((res, rej) => ssbClient.publish(msg, (e, m) => e ? rej(e) : res(m))) + }, + + async progressForCourse(courseId) { + const ssbClient = await openSsb() + const messages = await readAll(ssbClient) + const idx = indexOf(messages, ssbClient) + const rootId = idx.rootOf(courseId) + const perStudent = idx.progressByCourse.get(rootId) || new Map() + const out = {} + for (const [student, lessonSet] of perStudent) out[student] = lessonSet.size + return out + }, + + async createExam(courseId, title, opts = {}) { + const ssbClient = await openSsb() + const course = await this.getCourseById(courseId) + if (course.author !== ssbClient.id) throw new Error("Unauthorized") + if (!isProtected(course)) throw new Error("Exams are only for paid or invite courses") + if (!schoolCrypto) throw new Error("School crypto unavailable") + + const examTitle = String(title || "").trim() + if (!examTitle) throw new Error("Invalid title") + + const rootId = course.rootId + const key = await ensureCourseKey(ssbClient, rootId) + if (!key) throw new Error("Course key unavailable") + + let content = { + type: "schoolExam", + courseId: rootId, + title: examTitle, + lessonId: opts.lessonId ? String(opts.lessonId) : null, + author: ssbClient.id, + createdAt: new Date().toISOString() + } + content = schoolCrypto.encryptContent(content, [key], true) + + return new Promise((res, rej) => ssbClient.publish(content, (e, m) => e ? rej(e) : res(m))) + }, + + async addExamQuestion(courseId, examId, data) { + const ssbClient = await openSsb() + const course = await this.getCourseById(courseId) + if (course.author !== ssbClient.id) throw new Error("Unauthorized") + if (!schoolCrypto) throw new Error("School crypto unavailable") + + const q = String(data.q || "").trim() + const options = [data.o1, data.o2, data.o3, data.o4].map(o => String(o || "").trim()) + const correct = parseInt(String(data.correct), 10) + if (!q) throw new Error("Invalid question") + if (options.some(o => !o)) throw new Error("Four options are required") + if (!Number.isFinite(correct) || correct < 0 || correct > 3) throw new Error("Invalid correct option") + + const key = await ensureCourseKey(ssbClient, course.rootId) + if (!key) throw new Error("Course key unavailable") + + let content = { + type: "schoolExamQuestion", + courseId: course.rootId, + target: examId, + q, + options, + correct, + author: ssbClient.id, + createdAt: new Date().toISOString() + } + content = schoolCrypto.encryptContent(content, [key], true) + + return new Promise((res, rej) => ssbClient.publish(content, (e, m) => e ? rej(e) : res(m))) + }, + + async deleteExamQuestion(questionId) { + const ssbClient = await openSsb() + const messages = await readAll(ssbClient) + const idx = indexOf(messages, ssbClient) + const node = idx.questionNodes.get(questionId) + if (!node || idx.tomb.has(questionId)) throw new Error("Question not found") + if (node.author !== ssbClient.id) throw new Error("Unauthorized") + const tombMsg = { type: "tombstone", target: questionId, deletedAt: new Date().toISOString(), author: ssbClient.id } + return new Promise((res, rej) => ssbClient.publish(tombMsg, (e, r) => e ? rej(e) : res(r))) + }, + + async deleteExam(examId) { + const ssbClient = await openSsb() + const messages = await readAll(ssbClient) + const idx = indexOf(messages, ssbClient) + const node = idx.examNodes.get(examId) + if (!node || node.isMedia || idx.tomb.has(examId)) throw new Error("Exam not found") + if (node.author !== ssbClient.id) throw new Error("Unauthorized") + const tombMsg = { type: "tombstone", target: examId, deletedAt: new Date().toISOString(), author: ssbClient.id } + return new Promise((res, rej) => ssbClient.publish(tombMsg, (e, r) => e ? rej(e) : res(r))) + }, + + async listExams(courseId) { + const ssbClient = await openSsb() + const messages = await readAll(ssbClient) + const idx = indexOf(messages, ssbClient) + const rootId = idx.rootOf(courseId) + const keys = schoolCrypto ? (schoolCrypto.getKeys(rootId) || []) : [] + + const exams = [] + for (const node of idx.examNodes.values()) { + if (node.isMedia) continue + if (idx.tomb.has(node.key)) continue + if (idx.rootOf(node.c.courseId) !== rootId) continue + let c = node.c + let locked = false + if (c.encryptedPayload) { + const dec = keys.length && schoolCrypto ? schoolCrypto.decryptContent(c, keys.map(k => [k])) : { ...c, _undecryptable: true } + if (dec._undecryptable) locked = true + else c = dec + } + const myResult = idx.examResultLatest.get(`${node.key}::${ssbClient.id}`) || null + const results = [] + for (const r of idx.examResultLatest.values()) { + if (r.examId === node.key) results.push({ author: r.author, score: r.score, total: r.total }) + } + const questions = [] + if (!locked) { + for (const qn of idx.questionNodes.values()) { + if (idx.tomb.has(qn.key)) continue + if (qn.c.target !== node.key) continue + let qc = qn.c + if (qc.encryptedPayload) { + const dec = keys.length && schoolCrypto ? schoolCrypto.decryptContent(qc, keys.map(k => [k])) : { ...qc, _undecryptable: true } + if (dec._undecryptable) continue + qc = dec + } + questions.push({ id: qn.key, q: qc.q, options: qc.options, correct: qc.correct, createdAt: qc.createdAt || new Date(qn.ts).toISOString() }) + } + questions.sort((a, b) => new Date(a.createdAt) - new Date(b.createdAt)) + } + const passMark = locked ? null : 10 + exams.push({ + id: node.key, + courseId: rootId, + locked, + title: locked ? null : c.title, + lessonId: locked ? null : (c.lessonId ? idx.lessonRootOf(c.lessonId) : null), + passMark, + questions, + author: node.author, + createdAt: c.createdAt || new Date(node.ts).toISOString(), + myResult: myResult ? { score: myResult.score, total: myResult.total, at: myResult.ts, passed: passMark != null && Number(myResult.score) >= passMark } : null, + results: results.map(r => ({ ...r, passed: passMark != null && Number(r.score) >= passMark })) + }) + } + return exams.sort((a, b) => new Date(a.createdAt) - new Date(b.createdAt)) + }, + + async takeExam(courseId, examId, answers) { + const ssbClient = await openSsb() + const me = ssbClient.id + const course = await this.getCourseById(courseId) + if (course.author === me) throw new Error("Cannot take your own exam") + if (!course.students.includes(me)) throw new Error("Only students can take exams") + + const exams = await this.listExams(courseId) + const exam = exams.find(x => x.id === examId) + if (!exam || exam.locked) throw new Error("Exam not found") + if (exam.myResult && exam.myResult.passed) throw new Error("Exam already passed") + if (exam.myResult && Date.now() - Number(exam.myResult.at || 0) < 24 * 60 * 60 * 1000) throw new Error("Retry available after 24 hours") + + if (!exam.questions.length) throw new Error("Exam has no questions") + + let correctCount = 0 + exam.questions.forEach((question, i) => { + const given = parseInt(String(Array.isArray(answers) ? answers[i] : (answers || {})[`q${i}`]), 10) + if (given === question.correct) correctCount += 1 + }) + const score = Math.round((correctCount / exam.questions.length) * 100) / 10 + + const msg = { + type: "schoolExamResult", + courseId: course.rootId, + examId, + score, + total: 10, + createdAt: new Date().toISOString() + } + + return new Promise((res, rej) => ssbClient.private.publish(msg, [me, course.author], (e, m) => e ? rej(e) : res(m))) + }, + + async addLessonMaterial(courseId, lessonId, mediaMarkdown, caption = "") { + const ssbClient = await openSsb() + const course = await this.getCourseById(courseId) + if (course.author !== ssbClient.id) throw new Error("Unauthorized") + const media = String(mediaMarkdown || "").trim() + if (!media) throw new Error("Invalid material") + + let content = { + type: "schoolLessonMedia", + courseId: course.rootId, + target: lessonId, + media, + caption: String(caption || "").trim() || null, + author: ssbClient.id, + createdAt: new Date().toISOString() + } + + if (isProtected(course) && schoolCrypto) { + const key = await ensureCourseKey(ssbClient, course.rootId) + if (key) content = schoolCrypto.encryptContent(content, [key], true) + } + + return new Promise((res, rej) => ssbClient.publish(content, (e, m) => e ? rej(e) : res(m))) + }, + + async deleteLessonMaterial(materialId) { + const ssbClient = await openSsb() + const messages = await readAll(ssbClient) + const idx = indexOf(messages, ssbClient) + const node = idx.examNodes.get("media:" + materialId) + if (!node || idx.tomb.has(materialId)) throw new Error("Material not found") + if (node.author !== ssbClient.id) throw new Error("Unauthorized") + const tombMsg = { type: "tombstone", target: materialId, deletedAt: new Date().toISOString(), author: ssbClient.id } + return new Promise((res, rej) => ssbClient.publish(tombMsg, (e, r) => e ? rej(e) : res(r))) + }, + + async listLessonMaterials(courseId, lessonId) { + const ssbClient = await openSsb() + const messages = await readAll(ssbClient) + const idx = indexOf(messages, ssbClient) + const rootId = idx.rootOf(courseId) + const keys = schoolCrypto ? (schoolCrypto.getKeys(rootId) || []) : [] + + const materials = [] + for (const node of idx.examNodes.values()) { + if (!node.isMedia) continue + if (idx.tomb.has(node.key)) continue + if (idx.rootOf(node.c.courseId) !== rootId) continue + if (idx.lessonRootOf(node.c.target) !== idx.lessonRootOf(lessonId)) continue + let c = node.c + let locked = false + if (c.encryptedPayload) { + const dec = keys.length && schoolCrypto ? schoolCrypto.decryptContent(c, keys.map(k => [k])) : { ...c, _undecryptable: true } + if (dec._undecryptable) locked = true + else c = dec + } + materials.push({ + id: node.key, + lessonId, + locked, + media: locked ? null : (c.media || ""), + caption: locked ? null : (c.caption || null), + author: node.author, + createdAt: c.createdAt || new Date(node.ts).toISOString() + }) + } + return materials.sort((a, b) => new Date(a.createdAt) - new Date(b.createdAt)) + }, + + async generateInvite(courseId) { + const ssbClient = await openSsb() + const course = await this.getCourseById(courseId) + if (course.author !== ssbClient.id) throw new Error("Unauthorized") + if (course.visibility !== "INVITE") throw new Error("Only invite courses use invitation codes") + if (!schoolCrypto) throw new Error("School crypto unavailable") + + if (course.inviteCode) { + const messages = await readAll(ssbClient) + const alive = messages.some(m => { + const c = m.value && m.value.content + if (!c || c.type !== "school-invite" || c.target !== course.rootId || m.value.author !== course.author) return false + try { return schoolCrypto.hashInviteCode(course.inviteCode, c.salt) === c.codeHash } catch { return false } + }) + if (alive) return { code: course.inviteCode, courseId: course.rootId } + const key = await ensureCourseKey(ssbClient, course.rootId) + if (!key) throw new Error("Course key unavailable") + const salt = schoolCrypto.generateInviteSalt() + const ek = schoolCrypto.encryptForInvite(key, course.inviteCode, salt) + await new Promise((res, rej) => ssbClient.publish({ + type: "school-invite", target: course.rootId, ek, salt, + codeHash: schoolCrypto.hashInviteCode(course.inviteCode, salt) + }, (e) => e ? rej(e) : res())) + return { code: course.inviteCode, courseId: course.rootId } + } + + const key = await ensureCourseKey(ssbClient, course.rootId) + if (!key) throw new Error("Course key unavailable") + + const code = nodeCrypto.randomBytes(16).toString("hex") + const salt = schoolCrypto.generateInviteSalt() + const ek = schoolCrypto.encryptForInvite(key, code, salt) + await new Promise((res, rej) => ssbClient.publish({ + type: "school-invite", target: course.rootId, ek, salt, + codeHash: schoolCrypto.hashInviteCode(code, salt) + }, (e) => e ? rej(e) : res())) + try { await this.updateCourse(courseId, { inviteCode: code }) } catch (_) {} + return { code, courseId: course.rootId } + }, + + async joinByInvite(rawCode) { + if (!schoolCrypto) throw new Error("School crypto unavailable") + const ssbClient = await openSsb() + const me = ssbClient.id + const messages = await readAll(ssbClient) + const idx = indexOf(messages, ssbClient) + + const candidates = Array.from(new Set([String(rawCode || "").trim(), String(rawCode || "").trim().toLowerCase()])).filter(Boolean) + let matched = null + let code = null + for (const m of messages) { + const c = m.value && m.value.content + if (!c || c.type !== "school-invite") continue + const owner = idx.courseNodes.get(idx.rootOf(c.target)) + if (!owner || owner.author !== m.value.author) continue + for (const cand of candidates) { + try { + if (schoolCrypto.hashInviteCode(cand, c.salt) === c.codeHash) { matched = c; code = cand; break } + } catch {} + } + if (matched) break + } + if (!matched) throw new Error("Invalid or expired invite code") + + const courseKey = schoolCrypto.decryptFromInvite(matched.ek, code, matched.salt) + if (!courseKey) throw new Error("Could not decrypt invite") + schoolCrypto.addNewKey(matched.target, courseKey) + + const tipId = idx.tipOf(matched.target) + const node = idx.courseNodes.get(tipId) + if (!node) throw new Error("Course not found") + const teacherId = node.author + let cc = node.c + if (cc.encryptedPayload) { + const dec = schoolCrypto.decryptContent(cc, [[courseKey]]) + if (dec && !dec._undecryptable) cc = dec + } + const priceN = toNum(cc.price) + + if (teacherId !== me) { + const enrollments = idx.enrollByCourse.get(matched.target) || new Map() + if (!enrollments.has(me)) { + let transferId = null + if (Number.isFinite(priceN) && priceN > 0 && transfersModel) { + const deadline = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString() + const bill = await transfersModel.createTransfer(teacherId, `SCHOOL ${matched.target}`, priceN.toFixed(6), deadline, ["SCHOOL"], "ECONOMIC") + transferId = bill && bill.key ? bill.key : null + } + const msg = { type: "schoolEnroll", courseId: matched.target, value: true, transferId, keyProof: keyProofFor(courseKey, me), createdAt: new Date().toISOString() } + await new Promise((res, rej) => ssbClient.private.publish(msg, [me, teacherId], (e, m) => e ? rej(e) : res(m))) + } + const enrollChatId = cc.chatId || idx.chatByCourse.get(matched.target) || null + if (enrollChatId) { + try { + await new Promise((res) => ssbClient.publish({ type: "chatMember", target: enrollChatId, member: me, on: true, createdAt: new Date().toISOString() }, () => res())) + } catch {} + } + } + return { ok: true, courseId: matched.target } + }, + + async grantAccess(courseId, studentId) { + const ssbClient = await openSsb() + const course = await this.getCourseById(courseId) + if (course.author !== ssbClient.id) throw new Error("Unauthorized") + if (!schoolCrypto) throw new Error("School crypto unavailable") + + const ids = normalizeIds(studentId) + if (!ids.length) throw new Error("Invalid student id") + for (const student of ids) { + const known = course.students.includes(student) || course.invited.includes(student) || course.pending.some(p => p.author === student) + if (!known) throw new Error("Not a student of this course") + } + + const key = await ensureCourseKey(ssbClient, course.rootId) + const grant = await publishKeyGrant(ssbClient, course.rootId, key, ids) + await addToChat(ssbClient, course, ids) + return grant + }, + + async updateLesson(courseId, lessonId, data) { + const ssbClient = await openSsb() + const course = await this.getCourseById(courseId) + if (course.author !== ssbClient.id) throw new Error("Unauthorized") + + const lessons = await this.listLessons(course.rootId) + const current = lessons.find(l => l.id === lessonId) + if (!current || current.locked) throw new Error("Lesson not found") + + const title = data.title !== undefined ? String(data.title || "").trim() : current.title + const text = data.text !== undefined ? String(data.text || "").trim() : current.text + if (!title || !text) throw new Error("Invalid lesson") + + const orderN = toNum(data.order) + let content = { + type: "schoolLesson", + courseId: course.rootId, + title, + text, + unit: data.unit !== undefined ? (String(data.unit || "").trim() || null) : current.unit, + order: data.order !== undefined ? (Number.isFinite(orderN) ? orderN : null) : current.order, + sessionDate: data.sessionDate !== undefined ? normalizeDate(data.sessionDate) : current.sessionDate, + author: ssbClient.id, + createdAt: current.createdAt, + updatedAt: new Date().toISOString(), + replaces: lessonId + } + + if (isProtected(course) && schoolCrypto) { + const key = await ensureCourseKey(ssbClient, course.rootId) + if (key) content = schoolCrypto.encryptContent(content, [key], true) + } + + const tombMsg = { type: "tombstone", target: lessonId, deletedAt: new Date().toISOString(), author: ssbClient.id } + await new Promise((res, rej) => ssbClient.publish(tombMsg, (e) => e ? rej(e) : res())) + return new Promise((res, rej) => ssbClient.publish(content, (e, m) => e ? rej(e) : res(m))) + }, + + async deleteLesson(lessonId) { + const ssbClient = await openSsb() + const messages = await readAll(ssbClient) + const idx = indexOf(messages, ssbClient) + + const node = idx.lessonNodes.get(lessonId) + if (!node || idx.tomb.has(lessonId)) throw new Error("Lesson not found") + if (node.author !== ssbClient.id) throw new Error("Unauthorized") + + const tombMsg = { + type: "tombstone", + target: lessonId, + deletedAt: new Date().toISOString(), + author: ssbClient.id + } + + return new Promise((res, rej) => ssbClient.publish(tombMsg, (e, r) => e ? rej(e) : res(r))) + }, + + async listLessons(courseId) { + const ssbClient = await openSsb() + const messages = await readAll(ssbClient) + const idx = indexOf(messages, ssbClient) + + const rootId = idx.rootOf(courseId) + const keys = schoolCrypto ? (schoolCrypto.getKeys(rootId) || []) : [] + const lessons = [] + for (const node of idx.lessonNodes.values()) { + if (idx.tomb.has(node.key)) continue + if (idx.lessonChildOf.has(node.key)) continue + if (idx.rootOf(node.c.courseId) !== rootId) continue + let c = node.c + let locked = false + if (c.encryptedPayload) { + const dec = keys.length && schoolCrypto ? schoolCrypto.decryptContent(c, keys.map(k => [k])) : { ...c, _undecryptable: true } + if (dec._undecryptable) locked = true + else c = dec + } + lessons.push({ + id: node.key, + courseId: rootId, + locked, + title: locked ? null : c.title, + text: locked ? null : c.text, + unit: locked ? null : (c.unit || null), + order: locked ? null : (Number.isFinite(toNum(c.order)) ? toNum(c.order) : null), + sessionDate: locked ? null : (c.sessionDate || null), + author: node.author, + createdAt: c.createdAt || new Date(node.ts).toISOString() + }) + } + const myProgress = (idx.progressByCourse.get(rootId) || new Map()).get(ssbClient.id) || new Set() + for (const lesson of lessons) lesson.completed = myProgress.has(idx.lessonRootOf(lesson.id)) + return lessons.sort((a, b) => + ((a.order ?? Infinity) - (b.order ?? Infinity)) || (new Date(a.createdAt) - new Date(b.createdAt)) + ) + }, + + async hasPassedCourse(courseId, studentId) { + const ssbClient = await openSsb() + const course = await this.getCourseById(courseId) + const student = normalizeIds(studentId)[0] || ssbClient.id + const lessons = await this.listLessons(course.rootId) + if (!lessons.length) return false + const exams = await this.listExams(course.rootId) + const messages = await readAll(ssbClient) + const idx = indexOf(messages, ssbClient) + const progressSet = (idx.progressByCourse.get(course.rootId) || new Map()).get(student) || new Set() + for (const lesson of lessons) { + const lessonRoot = idx.lessonRootOf(lesson.id) + const lessonExams = exams.filter(x => !x.locked && x.questions.length && x.lessonId && x.lessonId === lessonRoot) + if (lessonExams.length) { + for (const x of lessonExams) { + const r = idx.examResultLatest.get(`${x.id}::${student}`) + if (!r || x.passMark == null || Number(r.score) < x.passMark) return false + } + } else if (!progressSet.has(lessonRoot)) { + return false + } + } + return true + }, + + async issueCertificate(courseId, studentId, text = "") { + const ssbClient = await openSsb() + const course = await this.getCourseById(courseId) + if (course.author !== ssbClient.id) throw new Error("Unauthorized") + + const student = normalizeIds(studentId)[0] + if (!student) throw new Error("Invalid student id") + if (!course.students.includes(student)) throw new Error("Student is not enrolled") + + const lessons = await this.listLessons(course.rootId) + if (lessons.length) { + const exams = await this.listExams(course.rootId) + const messages2 = await readAll(ssbClient) + const idx2 = indexOf(messages2, ssbClient) + const progressSet = (idx2.progressByCourse.get(course.rootId) || new Map()).get(student) || new Set() + for (const lesson of lessons) { + const lessonRoot = idx2.lessonRootOf(lesson.id) + const lessonExams = exams.filter(x => !x.locked && x.lessonId && x.lessonId === lessonRoot) + if (lessonExams.length) { + for (const x of lessonExams) { + const r = idx2.examResultLatest.get(`${x.id}::${student}`) + if (!r || x.passMark == null || Number(r.score) < x.passMark) throw new Error("Student has not passed all lessons") + } + } else if (!progressSet.has(lessonRoot)) { + throw new Error("Student has not completed all lessons") + } + } + } + + const rootId = course.rootId || (await this.resolveRootId(courseId)) + const existing = await this.listCertificates(rootId) + if (existing.some(cert => cert.student === student)) return { alreadyIssued: true } + + const content = { + type: "schoolCertificate", + courseId: rootId, + courseTitle: course.title, + student, + text: String(text || "").trim(), + author: ssbClient.id, + createdAt: new Date().toISOString() + } + + return new Promise((res, rej) => ssbClient.publish(content, (e, m) => e ? rej(e) : res(m))) + }, + + async listCertificates(courseId) { + const ssbClient = await openSsb() + const messages = await readAll(ssbClient) + const idx = indexOf(messages, ssbClient) + + const rootId = idx.rootOf(courseId) + const certs = [] + for (const node of idx.certNodes.values()) { + if (idx.tomb.has(node.key)) continue + if (idx.rootOf(node.c.courseId) !== rootId) continue + certs.push({ + id: node.key, + courseId: rootId, + courseTitle: node.c.courseTitle, + student: node.c.student, + text: node.c.text || "", + author: node.author, + createdAt: node.c.createdAt || new Date(node.ts).toISOString() + }) + } + return certs.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)) + }, + + async listCertificatesForStudent(studentId) { + const ssbClient = await openSsb() + const messages = await readAll(ssbClient) + const idx = indexOf(messages, ssbClient) + + const certs = [] + for (const node of idx.certNodes.values()) { + if (idx.tomb.has(node.key)) continue + if (node.c.student !== studentId) continue + certs.push({ + id: node.key, + courseId: idx.rootOf(node.c.courseId), + courseTitle: node.c.courseTitle, + student: node.c.student, + text: node.c.text || "", + author: node.author, + createdAt: node.c.createdAt || new Date(node.ts).toISOString() + }) + } + return certs.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)) + }, + + async lessonRootOf(lessonId) { + const ssbClient = await openSsb() + const messages = await readAll(ssbClient) + const idx = indexOf(messages, ssbClient) + return idx.lessonRootOf(lessonId) || lessonId + }, + + async listHiddenComments(courseId) { + const ssbClient = await openSsb() + const course = await this.getCourseById(courseId) + const messages = await readAll(ssbClient) + const hidden = new Set() + for (const m of messages) { + const c = m.value?.content + if (!c || c.type !== "schoolCommentHide" || !c.target) continue + if (m.value?.author !== course.author) continue + if (c.courseId !== course.rootId) continue + hidden.add(c.target) + } + return hidden + }, + + async hideComment(courseId, commentId) { + const ssbClient = await openSsb() + const course = await this.getCourseById(courseId) + if (course.author !== ssbClient.id) throw new Error("Only the teacher can hide comments") + const content = { type: "schoolCommentHide", courseId: course.rootId, target: commentId, author: ssbClient.id, createdAt: new Date().toISOString() } + return new Promise((res, rej) => ssbClient.publish(content, (e, m) => e ? rej(e) : res(m))) + }, + + async listCourses(filter = "ALL", viewerId = null, query = {}) { + const ssbClient = await openSsb() + const viewer = viewerId || ssbClient.id + + const messages = await readAll(ssbClient) + const idx = indexOf(messages, ssbClient) + + const courses = [] + for (const [rootId, tipId] of idx.tipByRoot.entries()) { + if (idx.tomb.has(tipId)) continue + const node = idx.courseNodes.get(tipId) + if (!node) continue + const course = buildCourseObject(node, rootId, idx.enrollByCourse.get(rootId), idx.transferAgg, idx.grantsByCourse.get(rootId), idx.opinionsByCourse.get(rootId), idx.chatByCourse) + if (course.undecryptable) continue + if (!canView(course, viewer)) continue + courses.push(course) + } + + const F = String(filter || "ALL").toUpperCase() + let list = courses + + if (F === "MINE") list = list.filter(c => c.author === viewer) + else if (F === "APPLIED") list = list.filter(c => c.students.includes(viewer) || c.pending.some(p => p.author === viewer)) + else if (F === "OPEN") list = list.filter(c => !(Number(c.price) > 0)) + else if (F === "ONGOING") list = list.filter(c => c.status === "ONGOING") + else if (F === "CLOSED") list = list.filter(c => c.status === "CLOSED") + else if (F === "RECENT") list = list.filter(c => moment(c.createdAt).isAfter(moment().subtract(24, "hours"))) + else if (F === "TOP") list = [...list].sort((a, b) => b.students.length - a.students.length) + + const search = String(query.q || query.search || "").trim() + if (search) list = list.filter(c => matchSearch(c, search)) + + const sort = String(query.sort || "").trim() + const byRecent = () => list.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)) + if (sort === "students") list.sort((a, b) => b.students.length - a.students.length) + else if (sort === "title") list.sort((a, b) => String(a.title).localeCompare(String(b.title))) + else if (F !== "TOP") byRecent() + + return list + }, + + async getCourseById(id, viewerId = null) { + const ssbClient = await openSsb() + const viewer = viewerId || ssbClient.id + + const messages = await readAll(ssbClient) + const idx = indexOf(messages, ssbClient) + + const tipId = idx.tipOf(id) + const rootId = idx.rootOf(id) + if (idx.tomb.has(tipId) || idx.vg.isForeignCopy(rootId)) throw new Error("Course not found") + + const node = idx.courseNodes.get(tipId) + if (!node) throw new Error("Course not found") + + const course = buildCourseObject(node, rootId, idx.enrollByCourse.get(rootId), idx.transferAgg, idx.grantsByCourse.get(rootId), idx.opinionsByCourse.get(rootId), idx.chatByCourse) + if (course.undecryptable) throw new Error("Course not found") + if (!canView(course, viewer)) throw new Error("Course not found") + return course + } + } +} diff --git a/src/models/search_model.js b/src/models/search_model.js index 650b736..1849c42 100644 --- a/src/models/search_model.js +++ b/src/models/search_model.js @@ -2,6 +2,9 @@ const pull = require('../server/node_modules/pull-stream'); const moment = require('../server/node_modules/moment'); const { getConfig } = require('../configs/config-manager.js'); const { buildValidatedTombstoneSet } = require('./tombstone_validator'); +const { readTyped, CONTENT_TYPES } = require('./typed_log'); +const longText = require('../backend/long_text'); +const { versionGroups, versionEntry } = require('../backend/dedupe'); const logLimit = getConfig().ssbLogStream?.limit || 1000; module.exports = ({ cooler, padsModel, tribeCrypto, tribesModel }) => { @@ -39,8 +42,8 @@ module.exports = ({ cooler, padsModel, tribeCrypto, tribesModel }) => { const searchableTypes = [ 'post', 'about', 'curriculum', 'tribe', 'transfer', 'feed', 'votes', 'report', 'task', 'event', 'bookmark', 'document', - 'image', 'audio', 'video', 'torrent', 'market', 'bankWallet', 'bankClaim', - 'project', 'job', 'forum', 'vote', 'contact', 'pub', 'map', 'shop', 'shopProduct', 'chat', 'pad' + 'image', 'audio', 'video', 'torrent', 'file', 'market', 'bankWallet', 'bankClaim', + 'project', 'job', 'housing', 'industry', 'industryBlueprint', 'forum', 'vote', 'contact', 'pub', 'map', 'shop', 'shopProduct', 'chat', 'pad', 'room', 'poll', 'schoolCourse', 'wikiPage', 'emergency', 'mailingList', 'logisticsRoute', 'podcast', 'podcastEpisode', 'campaign' ]; const getRelevantFields = (type, content) => { @@ -60,12 +63,13 @@ module.exports = ({ cooler, padsModel, tribeCrypto, tribesModel }) => { case 'audio': return [content?.url, content?.mimeType, content?.title, content?.description, ...(content?.tags || [])]; case 'image': - return [content?.url, content?.title, content?.description, ...(content?.tags || []), content?.meme]; + return [content?.url, content?.title, content?.description, ...(content?.tags || [])]; case 'video': return [content?.url, content?.mimeType, content?.title, content?.description, ...(content?.tags || [])]; case 'document': return [content?.url, content?.title, content?.description, ...(content?.tags || []), content?.key]; case 'torrent': + case 'file': return [content?.title, content?.description, ...(content?.tags || []), content?.url]; case 'market': return [content?.item_type, content?.title, content?.description, content?.price, ...(content?.tags || []), content?.status, content?.item_status, content?.deadline, content?.includesShipping, content?.seller, content?.image, content?.auctions_poll, content?.stock]; @@ -83,10 +87,22 @@ module.exports = ({ cooler, padsModel, tribeCrypto, tribesModel }) => { return [content?.address]; case 'bankClaim': return [content?.amount, content?.epochId, content?.allocationId, content?.txid]; + case 'schoolCourse': + return [content?.title, content?.description, ...(content?.tags || []), content?.price, content?.status, content?.visibility]; case 'project': return [content?.title, content?.status, content?.progress, content?.goal, content?.pledged, content?.deadline, (content?.followers || []).length, (content?.backers || []).length, (content?.milestones || []).length, content?.bounty, content?.bountyAmount, content?.bounty_currency, content?.activity?.kind, content?.activityActor]; + case 'poll': + return content?.encryptedQuestion + ? [] + : [content?.question, ...(content?.options || []), ...(content?.tags || [])]; + case 'housing': + return [content?.title, content?.housing_type, content?.property_type, content?.place, content?.price, content?.rooms, content?.size, content?.capacity, content?.status]; case 'job': return [content?.title, content?.job_type, ...(content?.tasks || []), content?.location, content?.vacants, content?.salary, content?.status, (content?.subscribers || []).length]; + case 'industry': + return [content?.name, content?.sector, content?.description, content?.membershipPolicy, content?.status, content?.license, ...(content?.tags || [])]; + case 'industryBlueprint': + return [content?.name, content?.description, content?.sector, content?.outItem, content?.outKind, content?.license, ...(content?.skills || []), ...((content?.materials || []).map(m => m && m.item))]; case 'forum': return [content?.root, content?.category, content?.title, content?.text, content?.key]; case 'vote': @@ -105,6 +121,22 @@ module.exports = ({ cooler, padsModel, tribeCrypto, tribesModel }) => { return [content?.title, content?.description, content?.category, ...(content?.tags || []), content?.status, content?.author]; case 'pad': return [content?.title, content?.status, content?.deadline, ...(content?.tags || []), content?.author]; + case 'room': + return content?.encrypted === true ? [] : [content?.title, content?.description, content?.status, ...(content?.tags || []), content?.author]; + case 'wikiPage': + return [content?.title, content?.body, content?.slug, ...(content?.tags || []), ...(content?.aliases || []), content?.author]; + case 'emergency': + return [content?.title, content?.text, content?.category, content?.status, ...(content?.tags || []), content?.author]; + case 'mailingList': + return [content?.title, content?.description, content?.listType, content?.status, ...(content?.tags || []), content?.author]; + case 'logisticsRoute': + return [content?.title, content?.description, content?.origin, content?.destination, content?.kind, content?.mode, ...(content?.tags || []), content?.author]; + case 'podcast': + return [content?.title, content?.description, content?.category, ...(content?.tags || []), content?.author]; + case 'podcastEpisode': + return [content?.title, content?.description, ...(content?.tags || []), content?.author]; + case 'campaign': + return [content?.title, content?.text, content?.category, content?.status, ...(content?.tags || []), content?.author]; case 'gameScore': return [content?.game, content?.player]; default: @@ -134,6 +166,7 @@ module.exports = ({ cooler, padsModel, tribeCrypto, tribesModel }) => { if (t === 'audio') return `audio:${c.url || `${author}|${norm(c.title)}|${norm(c.description)}` || msg.key}`; if (t === 'video') return `video:${c.url || `${author}|${norm(c.title)}|${norm(c.description)}` || msg.key}`; if (t === 'torrent') return `torrent:${c.url || `${author}|${norm(c.title)}|${norm(c.description)}` || msg.key}`; + if (t === 'file') return `file:${c.url || `${author}|${norm(c.title)}|${norm(c.description)}` || msg.key}`; if (t === 'bookmark') return `bookmark:${author}|${c.url || norm(c.description) || msg.key}`; if (t === 'tribe') { @@ -218,6 +251,9 @@ module.exports = ({ cooler, padsModel, tribeCrypto, tribesModel }) => { ].join('|'); } + if (t === 'schoolCourse') { + return ['schoolCourse', author, norm(c.title), norm(c.createdAt)].join('|'); + } if (t === 'project') { return [ 'project', @@ -228,6 +264,17 @@ module.exports = ({ cooler, padsModel, tribeCrypto, tribesModel }) => { ].join('|'); } + if (t === 'housing') { + return [ + 'housing', + author, + norm(c.title), + norm(c.place), + norm(c.price), + norm(c.housing_type) + ].join('|'); + } + if (t === 'job') { return [ 'job', @@ -239,6 +286,14 @@ module.exports = ({ cooler, padsModel, tribeCrypto, tribesModel }) => { ].join('|'); } + if (t === 'industry') { + return ['industry', author, norm(c.name), norm(c.sector)].join('|'); + } + + if (t === 'industryBlueprint') { + return ['industryBlueprint', author, norm(c.facility), norm(c.name)].join('|'); + } + if (t === 'forum') { return `forum:${c.key || c.root || `${author}|${norm(c.title)}` || msg.key}`; } @@ -259,6 +314,30 @@ module.exports = ({ cooler, padsModel, tribeCrypto, tribesModel }) => { return ['pad', author, norm(c.title), norm(c.deadline)].join('|'); } + if (t === 'room') { + return ['room', author, norm(c.rid || c.title), norm(c.createdAt)].join('|'); + } + + if (t === 'wikiPage') { + return ['wikiPage', norm(c.tribeId), norm(c.slug)].join('|'); + } + + if (t === 'emergency') { + return ['emergency', author, norm(c.title), norm(c.createdAt)].join('|'); + } + + if (t === 'mailingList') { + return ['mailingList', author, norm(c.createdAt)].join('|'); + } + + if (t === 'logisticsRoute') { + return ['logisticsRoute', author, norm(c.createdAt)].join('|'); + } + + if (t === 'podcast' || t === 'podcastEpisode' || t === 'campaign') { + return [t, author, norm(c.createdAt)].join('|'); + } + return `${t}:${msg.key}`; }; @@ -274,20 +353,14 @@ module.exports = ({ cooler, padsModel, tribeCrypto, tribesModel }) => { return Array.from(map.values()); }; - const search = async ({ query, types = [], resultsPerPage = "10" }) => { + const search = async ({ query, types = [] }) => { const ssbClient = await openSsb(); const viewerId = ssbClient.id; const queryLower = String(query || '').toLowerCase(); - const messages = await new Promise((res, rej) => { - pull( - ssbClient.createLogStream({ limit: logLimit }), - pull.collect((err, msgs) => err ? rej(err) : res(msgs)) - ); - }); + const messages = await readTyped(ssbClient, CONTENT_TYPES, { limit: logLimit, withWindow: true }); const tombstoned = buildValidatedTombstoneSet(messages); - const replacesMap = new Map(); const latestByKey = new Map(); for (const msg of messages) { @@ -295,23 +368,12 @@ module.exports = ({ cooler, padsModel, tribeCrypto, tribesModel }) => { const c = msg?.value?.content; const t = c?.type; if (!t || !searchableTypes.includes(t)) continue; - if (tombstoned.has(k)) continue; - if (c.replaces) replacesMap.set(c.replaces, k); latestByKey.set(k, msg); } - for (const [oldId, newId] of Array.from(replacesMap.entries())) { - const orig = latestByKey.get(oldId); - const repl = latestByKey.get(newId); - if (!orig) { replacesMap.delete(oldId); continue; } - if (!repl || String(repl?.value?.author) !== String(orig?.value?.author)) { - replacesMap.delete(oldId); - latestByKey.delete(newId); - } - } - - for (const oldId of replacesMap.keys()) { - latestByKey.delete(oldId); + const vg = versionGroups(Array.from(latestByKey.values()).map(versionEntry)); + for (const k of Array.from(latestByKey.keys())) { + if (!vg.roots.has(vg.rootOf(k)) || vg.tipOf(k) !== k || tombstoned.has(k)) latestByKey.delete(k); } const viewerTribeIds = new Set(); @@ -347,6 +409,14 @@ module.exports = ({ cooler, padsModel, tribeCrypto, tribesModel }) => { } } + const chunkLookup = longText.lookupIn(longText.indexChunks(messages)); + for (const [k, msg] of Array.from(latestByKey.entries())) { + const c = msg?.value?.content; + if ((c?.type === 'post' || c?.type === 'forum') && longText.hasChunks(c)) { + latestByKey.set(k, { ...msg, value: { ...msg.value, content: longText.resolveField(c, 'text', msg.value.author, chunkLookup) } }); + } + } + if (padsModel) { for (const msg of latestByKey.values()) { const c = msg?.value?.content; @@ -393,11 +463,6 @@ module.exports = ({ cooler, padsModel, tribeCrypto, tribesModel }) => { return acc; }, {}); - if (resultsPerPage !== "all") { - const limit = parseInt(resultsPerPage, 10); - for (const key in grouped) grouped[key] = grouped[key].slice(0, limit); - } - return grouped; }; diff --git a/src/models/shops_model.js b/src/models/shops_model.js index 5dedbd2..54f79ae 100644 --- a/src/models/shops_model.js +++ b/src/models/shops_model.js @@ -3,6 +3,8 @@ const { getConfig } = require("../configs/config-manager.js") const categories = require("../backend/opinion_categories") const { buildValidatedTombstoneSet } = require('./tombstone_validator'); const { dedupeByPreferring, norm } = require('../backend/dedupe') +const { readTyped, memoIndex } = require('./typed_log') +const FileType = require('../server/node_modules/file-type') const logLimit = getConfig().ssbLogStream?.limit || 1000 const safeArr = (v) => (Array.isArray(v) ? v : []) @@ -18,31 +20,90 @@ module.exports = ({ cooler, tribeCrypto }) => { let ssb const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb } - const readAll = async (ssbClient) => - new Promise((resolve, reject) => - pull(ssbClient.createLogStream({ limit: logLimit }), pull.collect((err, msgs) => err ? reject(err) : resolve(msgs))) - ) + const SHOP_TYPES = ["shop", "shopProduct", "shopOpinion", "shopPurchase", "shop-invite", "shop-invite-tombstone", "shop-open-invite", "shop-open-invite-tombstone", "tombstone"] + + const readAll = async (ssbClient) => readTyped(ssbClient, SHOP_TYPES, { limit: logLimit, withWindow: true, withPrivate: true }) + const mediaKinds = new Map() + const blobMediaKind = async (ssbClient, blobId) => { + if (mediaKinds.has(blobId)) return mediaKinds.get(blobId) + let kind + try { + const head = await new Promise((resolve, reject) => pull(ssbClient.blobs.get(blobId), pull.take(1), pull.collect((err, chunks) => err ? reject(err) : resolve(Buffer.concat(chunks))))) + const ft = await FileType.fromBuffer(head) + kind = ft && ft.mime ? ft.mime.split('/')[0] : 'other' + } catch (_) { return null } + mediaKinds.set(blobId, kind) + return kind + } + const withMediaKind = async (ssbClient, items) => { + for (const p of items) { + if (!p || !p.image || !String(p.image).startsWith('&')) continue + const kind = await blobMediaKind(ssbClient, p.image) + if (kind === 'video') p.image = `[video:](${p.image})` + else if (kind === 'audio') p.image = `[audio:](${p.image})` + } + return items + } + const openSealed = (ssbClient, m) => { + if (typeof m.value?.content === "string") return ssbClient.private.unbox({ key: m.key, value: m.value, timestamp: m.value?.timestamp || m.timestamp || 0 }) + return m.value?.private === true ? m : null + } const SELLER_STATUSES = ["ACCEPTED", "REJECTED", "PAID", "SHIPPED"] const BUYER_STATUSES = ["RECEIVED"] const ORDER_STATUSES = [...SELLER_STATUSES, ...BUYER_STATUSES] + const ORDER_FLOW = { PENDING: ["ACCEPTED", "REJECTED"], ACCEPTED: ["PAID", "REJECTED"], PAID: ["SHIPPED"], SHIPPED: ["RECEIVED"] } const buildOrderStatusMap = (ssbClient, messages) => { const map = new Map() for (const m of messages) { - if (typeof m.value?.content !== "string") continue try { - const dec = ssbClient.private.unbox({ key: m.key, value: m.value, timestamp: m.value?.timestamp || m.timestamp || 0 }) + const dec = openSealed(ssbClient, m) const dc = dec?.value?.content if (!dc || dc.type !== "shop-purchase-status" || !dc.orderId) continue - const ts = dec.value.timestamp || m.timestamp || 0 - const prev = map.get(dc.orderId) - if (!prev || ts >= prev.ts) map.set(dc.orderId, { status: String(dc.status || "").toUpperCase(), ts }) + if (!map.has(dc.orderId)) map.set(dc.orderId, []) + map.get(dc.orderId).push({ status: String(dc.status || "").toUpperCase(), author: dec.value.author, ts: dec.value.timestamp || m.timestamp || 0, price: dc.price !== undefined ? String(dc.price) : undefined, title: dc.title !== undefined ? String(dc.title) : undefined }) } catch (_) {} } return map } + const orderStatusOf = (updates, buyer, seller) => { + const signed = (updates || []).filter(u => (SELLER_STATUSES.includes(u.status) && u.author === seller) || (BUYER_STATUSES.includes(u.status) && u.author === buyer)).sort((a, b) => a.ts - b.ts) + let status = "PENDING", g = 0 + while (g++ < 100) { + const next = signed.find(u => (ORDER_FLOW[status] || []).includes(u.status)) + if (!next) break + status = next.status + } + return status + } + + const acceptedTermsOf = (updates, seller) => { + const accepted = (updates || []).filter(u => u.status === "ACCEPTED" && u.author === seller && u.price !== undefined).sort((a, b) => a.ts - b.ts)[0] + return accepted ? { price: accepted.price, ...(accepted.title ? { title: accepted.title } : {}) } : null + } + const withOrderStatus = (statusMap) => (o) => { + const updates = statusMap.get(o.id) + return { ...o, ...(acceptedTermsOf(updates, o.seller) || {}), status: orderStatusOf(updates, o.buyer, o.seller) } + } + + const resolveOrder = (idx, dc) => { + const productRoot = idx.rootOf(String(dc.productId || "")) + const productNode = idx.nodes.get(productRoot) + if (!productNode || productNode.c.type !== "shopProduct") return null + const shopRoot = idx.rootOf(productShopId(productNode.c)) + const shopNode = idx.nodes.get(shopRoot) + if (!shopNode || shopNode.c.type !== "shop" || shopNode.author !== productNode.author) return null + let ref = productRoot + let g = 0 + while (idx.strictChild.has(ref) && g++ < 100000) ref = idx.strictChild.get(ref) + const raw = idx.nodes.get(ref).c + const c = isEncrypted(raw) ? decryptShopContent(raw, shopRoot) : raw + const readable = !c._undecryptable + return { productId: productRoot, shopId: shopRoot, seller: shopNode.author, title: readable ? String(c.title || "") : "", price: readable ? (c.price || "") : "" } + } + const decryptBuyers = (val, key) => { if (Array.isArray(val)) return val if (typeof val === 'string' && tribeCrypto && key) { @@ -77,7 +138,7 @@ module.exports = ({ cooler, tribeCrypto }) => { } const buildIndex = (messages) => { - const tomb = new Set() + const tomb = buildValidatedTombstoneSet(messages) const nodes = new Map() const parent = new Map() const child = new Map() @@ -90,7 +151,7 @@ module.exports = ({ cooler, tribeCrypto }) => { const v = m.value || {} const c = v.content if (!c) continue - if (c.type === "tombstone" && c.target) { tomb.add(c.target); continue } + if (c.type === "tombstone") continue if (c.type === "shopOpinion" && c.target) { opinionMsgs.push({ target: c.target, author: v.author, category: c.category }); continue } if (c.type === "shopPurchase" && c.target) { purchaseMsgs.push({ target: c.target, author: v.author }); continue } if (c.type === "shop" || c.type === "shopProduct") { @@ -162,7 +223,7 @@ module.exports = ({ cooler, tribeCrypto }) => { tags: safeArr(c.tags), visibility: c.visibility || (encrypted ? "CLOSED" : "OPEN"), clearnetPublic: !!c.clearnetPublic, - author: raw.author || c.author || node.author, + author: node.author, createdAt: c.createdAt || raw.createdAt || new Date(node.ts).toISOString(), updatedAt: c.updatedAt || raw.updatedAt || null, opinions: agg.opinions, @@ -182,7 +243,6 @@ module.exports = ({ cooler, tribeCrypto }) => { const undecryptable = !!c._undecryptable const agg = idx ? idx.aggregateFor(rootId, c) : { opinions: c.opinions || {}, opinions_inhabitants: safeArr(c.opinions_inhabitants) } const sellerStock = Number(c.stock) || 0 - const purchaseCount = idx ? idx.purchaseCountFor(rootId) : 0 const sellerBuyers = encrypted ? safeArr(c.buyers) : decryptBuyers(c.buyers, tribeCrypto ? tribeCrypto.getKey(rootId) : null) const buyers = Array.from(new Set(sellerBuyers.concat(idx ? idx.purchaseAuthorsFor(rootId) : []))) return { @@ -193,9 +253,9 @@ module.exports = ({ cooler, tribeCrypto }) => { description: c.description || "", image: c.image || null, price: c.price || "0.000000", - stock: Math.max(0, sellerStock - purchaseCount), + stock: Math.max(0, sellerStock), featured: !!c.featured, - author: raw.author || c.author || node.author, + author: node.author, createdAt: c.createdAt || raw.createdAt || new Date(node.ts).toISOString(), updatedAt: c.updatedAt || raw.updatedAt || null, opinions: agg.opinions, @@ -346,7 +406,7 @@ module.exports = ({ cooler, tribeCrypto }) => { async getShopById(id) { const ssbClient = await openSsb() const messages = await readAll(ssbClient) - const idx = buildIndex(messages) + const idx = memoIndex('shops', messages, buildIndex) const tip = idx.strictTipOf(id) if (idx.tomb.has(tip)) return null @@ -521,7 +581,7 @@ module.exports = ({ cooler, tribeCrypto }) => { async getProductById(id) { const ssbClient = await openSsb() const messages = await readAll(ssbClient) - const idx = buildIndex(messages) + const idx = memoIndex('shops', messages, buildIndex) const tip = idx.strictTipOf(id) if (idx.tomb.has(tip)) return null @@ -531,7 +591,7 @@ module.exports = ({ cooler, tribeCrypto }) => { const root = idx.rootOf(tip) - return buildProduct(node, root, idx) + return (await withMediaKind(ssbClient, [buildProduct(node, root, idx)]))[0] }, async listProducts(shopRootId) { @@ -549,7 +609,7 @@ module.exports = ({ cooler, tribeCrypto }) => { if (prod && !prod.undecryptable) items.push(prod) } - return items.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)) + return withMediaKind(ssbClient, items.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt))) }, async listFeaturedProducts(shopRootId) { @@ -568,7 +628,7 @@ module.exports = ({ cooler, tribeCrypto }) => { if (prod && !prod.undecryptable) items.push(prod) } - return items.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)).slice(0, 4) + return withMediaKind(ssbClient, items.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)).slice(0, 4)) }, async listAllProducts({ filter = "all", sort = "recent" } = {}) { @@ -585,8 +645,10 @@ module.exports = ({ cooler, tribeCrypto }) => { if (prod && !prod.undecryptable) items.push(prod) } - if (filter === "top") return items.slice().sort((a, b) => voteSum(b.opinions) - voteSum(a.opinions) || new Date(b.createdAt) - new Date(a.createdAt)) - return items.slice().sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)) + const sorted = filter === "top" + ? items.slice().sort((a, b) => voteSum(b.opinions) - voteSum(a.opinions) || new Date(b.createdAt) - new Date(a.createdAt)) + : items.slice().sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)) + return withMediaKind(ssbClient, sorted) }, async buyProduct(productId) { @@ -608,8 +670,7 @@ module.exports = ({ cooler, tribeCrypto }) => { const shopRoot = productShopId(raw) const c = encrypted ? decryptShopContent(raw, shopRoot) : raw if (c._undecryptable) throw new Error("Cannot access product") - const stock = Math.max(0, (Number(c.stock) || 0) - idx.purchaseCountFor(rootId)) - if (stock <= 0) throw new Error("Out of stock") + if (!((Number(c.stock) || 0) > 0)) throw new Error("Out of stock") const content = { type: "shopPurchase", target: rootId, createdAt: new Date().toISOString() } return new Promise((res, rej) => ssbClient.publish(content, (e, m) => e ? rej(e) : res(m))) @@ -626,11 +687,14 @@ module.exports = ({ cooler, tribeCrypto }) => { const rootId = idx.rootOf(tipId) const node = idx.nodes.get(tipId) - if (!node) throw new Error("Product not found") + if (!node || node.c.type !== "shopProduct") throw new Error("Product not found") const raw = node.c - const shopOwner = raw.author - if (shopOwner === userId) throw new Error("Cannot buy your own product") const shopRoot = productShopId(raw) + const shopNode = idx.nodes.get(idx.rootOf(shopRoot)) + const productRootNode = idx.nodes.get(rootId) + if (!shopNode || shopNode.c.type !== "shop" || !productRootNode || productRootNode.author !== shopNode.author || node.author !== shopNode.author) throw new Error("Product not found") + const shopOwner = shopNode.author + if (shopOwner === userId) throw new Error("Cannot buy your own product") const c = isEncrypted(raw) ? decryptShopContent(raw, shopRoot) : raw const content = { @@ -655,31 +719,24 @@ module.exports = ({ cooler, tribeCrypto }) => { const ssbClient = await openSsb() const me = ssbClient.id const messages = await readAll(ssbClient) + const idx = buildIndex(messages) const out = [] for (const m of messages) { - if (typeof m.value?.content !== "string") continue try { - const dec = ssbClient.private.unbox({ key: m.key, value: m.value, timestamp: m.value?.timestamp || m.timestamp || 0 }) + const dec = openSealed(ssbClient, m) if (!dec?.value?.content) continue const dc = dec.value.content if (dc.type !== "shop-purchase") continue if (dec.value.author !== me) continue - out.push({ id: m.key, ...dc, buyer: dec.value.author, ts: dec.value.timestamp || m.timestamp || 0 }) + const ts = dec.value.timestamp || m.timestamp || 0 + const resolved = resolveOrder(idx, dc) + if (!resolved) continue + out.push({ id: m.key, ...dc, ...resolved, buyer: dec.value.author, ts }) } catch (_) {} } const statusMap = buildOrderStatusMap(ssbClient, messages) - const sellerCache = new Map() - for (const o of out) { - if (!o.seller && o.shopId) { - if (!sellerCache.has(o.shopId)) { - const shop = await this.getShopById(o.shopId).catch(() => null) - sellerCache.set(o.shopId, (shop && shop.author) || null) - } - o.seller = sellerCache.get(o.shopId) || null - } - } return out - .map(o => ({ ...o, status: (statusMap.get(o.id) || {}).status || "PENDING" })) + .map(withOrderStatus(statusMap)) .sort((a, b) => b.ts - a.ts) }, @@ -689,23 +746,26 @@ module.exports = ({ cooler, tribeCrypto }) => { const shop = await this.getShopById(shopRootId).catch(() => null) if (!shop) throw new Error("Shop not found") if (shop.author !== me) throw new Error("Not the shop owner") + const rootId = shop.rootId || shopRootId const messages = await readAll(ssbClient) + const idx = buildIndex(messages) const out = [] for (const m of messages) { - if (typeof m.value?.content !== "string") continue try { - const dec = ssbClient.private.unbox({ key: m.key, value: m.value, timestamp: m.value?.timestamp || m.timestamp || 0 }) + const dec = openSealed(ssbClient, m) if (!dec?.value?.content) continue const dc = dec.value.content if (dc.type !== "shop-purchase") continue - if (dc.shopId !== shopRootId) continue - out.push({ id: m.key, ...dc, buyer: dec.value.author, ts: dec.value.timestamp || m.timestamp || 0 }) + const ts = dec.value.timestamp || m.timestamp || 0 + const resolved = resolveOrder(idx, dc) + if (!resolved || resolved.shopId !== rootId || resolved.seller !== me) continue + out.push({ id: m.key, ...dc, ...resolved, buyer: dec.value.author, ts }) } catch (_) {} } const statusMap = buildOrderStatusMap(ssbClient, messages) return out - .map(o => ({ ...o, status: (statusMap.get(o.id) || {}).status || "PENDING" })) + .map(withOrderStatus(statusMap)) .sort((a, b) => b.ts - a.ts) }, @@ -715,27 +775,38 @@ module.exports = ({ cooler, tribeCrypto }) => { const st = String(status || "").toUpperCase() if (!ORDER_STATUSES.includes(st)) throw new Error("Invalid status") const messages = await readAll(ssbClient) + const idx = buildIndex(messages) let order = null for (const m of messages) { if (m.key !== orderId) continue - if (typeof m.value?.content !== "string") break try { - const dec = ssbClient.private.unbox({ key: m.key, value: m.value, timestamp: m.value?.timestamp || m.timestamp || 0 }) + const dec = openSealed(ssbClient, m) const dc = dec?.value?.content - if (dc && dc.type === "shop-purchase") order = { ...dc, buyer: dec.value.author } + const resolved = dc && dc.type === "shop-purchase" ? resolveOrder(idx, dc) : null + if (resolved) order = { ...dc, ...resolved, buyer: dec.value.author } } catch (_) {} break } if (!order) throw new Error("Order not found") - const seller = order.seller || (await this.getShopById(order.shopId).catch(() => null) || {}).author || null + const seller = order.seller if (BUYER_STATUSES.includes(st)) { if (me !== order.buyer) throw new Error("Only the buyer can set this status") } else { if (me !== seller) throw new Error("Only the seller can set this status") } + const current = orderStatusOf(buildOrderStatusMap(ssbClient, messages).get(orderId), order.buyer, seller) + if (!(ORDER_FLOW[current] || []).includes(st)) throw new Error("Invalid status transition") const recps = Array.from(new Set([order.buyer, seller].filter(Boolean))) - const content = { type: "shop-purchase-status", orderId, shopId: order.shopId, status: st, updatedAt: new Date().toISOString() } - return new Promise((res, rej) => ssbClient.private.publish(content, recps, (e, m) => e ? rej(e) : res(m))) + const content = { type: "shop-purchase-status", orderId, shopId: order.shopId, status: st, ...(st === "ACCEPTED" ? { price: String(order.price || ""), title: String(order.title || "") } : {}), updatedAt: new Date().toISOString() } + const published = await new Promise((res, rej) => ssbClient.private.publish(content, recps, (e, m) => e ? rej(e) : res(m))) + const delta = st === "ACCEPTED" ? -1 : (st === "REJECTED" && current === "ACCEPTED" ? 1 : 0) + if (delta) { + try { + const product = await this.getProductById(order.productId) + await this.updateProductById(order.productId, { stock: String(Math.max(0, (Number(product.stock) || 0) + delta)) }) + } catch (_) {} + } + return published }, async countPendingOrders(shopRootId) { diff --git a/src/models/stats_model.js b/src/models/stats_model.js index bc14f72..fed5fa6 100644 --- a/src/models/stats_model.js +++ b/src/models/stats_model.js @@ -1,16 +1,23 @@ const pull = require('../server/node_modules/pull-stream'); -const os = require('os'); const fs = require('fs'); const path = require('path'); +const ssbDirOf = () => require('../server/ssb_config').path; const { getConfig } = require('../configs/config-manager.js'); const { buildValidatedTombstoneSet } = require('./tombstone_validator'); +const { readTyped, CONTENT_TYPES } = require('./typed_log'); +const { versionGroups, versionEntry } = require('../backend/dedupe'); +const opinionCategories = require('../backend/opinion_categories'); +const CONSTRUCTIVE_OPINIONS = new Set(opinionCategories.karma.constructive); +const MODERATION_OPINIONS = new Set(opinionCategories.karma.moderation); +const NEUTRAL_OPINIONS = new Set(opinionCategories.karma.neutral); const logLimit = getConfig().ssbLogStream?.limit || 1000; +const DISK_USAGE_TTL_MS = 10 * 60 * 1000; -const STORAGE_DIR = path.join(__dirname, "..", "configs"); -const ADDR_FILE = path.join(STORAGE_DIR, "wallet_addresses.json"); +const addrFile = () => process.env.OASIS_BANKING_DIR ? path.join(process.env.OASIS_BANKING_DIR, "wallet-addresses.json") : require('../configs/state-manager').statePath("wallet-addresses.json"); function readAddrMap() { try { + const ADDR_FILE = addrFile(); if (!fs.existsSync(ADDR_FILE)) return {}; const raw = fs.readFileSync(ADDR_FILE, 'utf8'); const obj = JSON.parse(raw || '{}'); @@ -22,7 +29,7 @@ function readAddrMap() { const listPubsFromEbt = () => { try { - const ebtDir = path.join(os.homedir(), '.ssb', 'ebt'); + const ebtDir = path.join(ssbDirOf(), 'ebt'); const files = fs.readdirSync(ebtDir); return files.filter(f => f.endsWith('.ed25519')); } catch { @@ -58,26 +65,52 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { }; const types = [ - 'bookmark','event','task','votes','report','feed','project', - 'image','torrent','audio','video','document','transfer','post','tribe', + 'bookmark','event','task','votes','report','feed','project','industry','industryBlueprint', + 'image','torrent','file','audio','video','document','transfer','post','tribe', 'market','forum','job','aiExchange','map','shop','shopProduct','chat','chatMessage', - 'pad','padEntry','gameScore','calendar','calendarDate','calendarNote','log', + 'pad','padEntry','room','roomMember','wikiPage','emergency','emergencyConfirm','emergencyUpdate','mailingList','logisticsRoute','logisticsRating','podcast','podcastEpisode','podcastPlay','campaign','campaignSignature','campaignUpdate','gameScore','calendar','calendarDate','calendarNote','log', + 'schoolCourse','schoolLesson','schoolEnroll','schoolCertificate', 'parliamentCandidature','parliamentTerm','parliamentProposal','parliamentRevocation','parliamentLaw', 'courtsCase','courtsEvidence','courtsAnswer','courtsVerdict','courtsSettlement','courtsSettlementProposal','courtsSettlementAccepted','courtsNomination','courtsNominationVote' ]; - const getFolderSize = (folderPath) => { - let files; - try { files = fs.readdirSync(folderPath); } catch (_) { return 0; } - let totalSize = 0; - for (const file of files) { - const filePath = `${folderPath}/${file}`; + const walkBytes = async (dir) => { + let names; + try { names = await fs.promises.readdir(dir); } catch (_) { return 0; } + let total = 0; + for (const name of names) { + const filePath = path.join(dir, name); try { - const st = fs.statSync(filePath); - totalSize += st.isDirectory() ? getFolderSize(filePath) : st.size; + const st = await fs.promises.stat(filePath); + total += st.isDirectory() ? await walkBytes(filePath) : st.size; } catch (_) {} } - return totalSize; + return total; + }; + const diskUsage = new Map(); + const ssbDiskUsage = (root) => { + const hit = diskUsage.get(root); + if (hit && (hit.pending || Date.now() - hit.at < DISK_USAGE_TTL_MS)) return hit.pending || Promise.resolve(hit.value); + const pending = (async () => { + let names = []; + try { names = await fs.promises.readdir(root); } catch (_) {} + const usage = { total: 0, log: 0, blobs: 0 }; + for (const name of names) { + const filePath = path.join(root, name); + let bytes = 0; + try { + const st = await fs.promises.stat(filePath); + bytes = st.isDirectory() ? await walkBytes(filePath) : st.size; + } catch (_) {} + usage.total += bytes; + if (name === 'db2') usage.log = bytes; + if (name === 'blobs') usage.blobs = bytes; + } + return usage; + })(); + diskUsage.set(root, { pending, at: 0, value: null }); + pending.then((value) => diskUsage.set(root, { pending: null, at: Date.now(), value }), () => diskUsage.delete(root)); + return pending; }; const formatSize = (sizeInBytes) => { @@ -194,8 +227,10 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { let s = 0; for (const k in cats) { if (!Object.prototype.hasOwnProperty.call(cats, k)) continue; + if (NEUTRAL_OPINIONS.has(k)) continue; if (k === 'interesting' || k === 'inspiring') s += 5; - else if (k === 'boring' || k === 'spam' || k === 'propaganda') s -= 3; + else if (MODERATION_OPINIONS.has(k)) s -= 5; + else if (CONSTRUCTIVE_OPINIONS.has(k)) s -= 3; else s += 1; } return s; @@ -274,13 +309,56 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { else if (t === 'larphousepost') score += 6; else if (t === 'larptestattempt') score += 3; else if (t === 'torrent') score += 6; + else if (t === 'file') score += 6; else if (t === 'shop' || t === 'shopproduct') score += 6; + else if (t === 'schoolcourse' || rawType === 'schoolcourse') score += 10; + else if (t === 'schoollesson' || rawType === 'schoollesson') score += 6; + else if (t === 'schoolenroll' || rawType === 'schoolenroll') score += 3; + else if (t === 'schoolcertificate' || rawType === 'schoolcertificate') score += 12; + else if (t === 'schoolexam' || rawType === 'schoolexam') score += 6; + else if (t === 'schoolprogress' || rawType === 'schoolprogress') score += 1; + else if (t === 'schoolopinion' || rawType === 'schoolopinion') score += 2; else if (t === 'shop-purchase') score += 2; else if (t === 'pad' || t === 'padentry') score += 3; + else if (t === 'room') score += 3; + else if (t === 'roommember') score += 1; + else if (t === 'wikipage') score += 3; + else if (t === 'emergency') score += 4; + else if (t === 'emergencyconfirm' || t === 'emergencyupdate') score += 1; + else if (t === 'mailinglist') score += 3; + else if (t === 'logisticsroute') score += 3; + else if (t === 'logisticsrating') score += 1; + else if (t === 'podcast') score += 4; + else if (t === 'podcastepisode') score += 3; + else if (t === 'podcastopinion' || t === 'podcastplay') score += 1; + else if (t === 'campaign') score += 4; + else if (t === 'campaignsignature') score += 1; + else if (t === 'campaignupdate') score += 2; else if (t === 'calendar' || t === 'calendarnote' || t === 'calendardate') score += 3; else if (t === 'chat') score += 1; else if (t === 'gamescore') score += 2; else if (t === 'pixelia') score += 2; + else if (rawType === 'industry') score += 8; + else if (rawType === 'industryblueprint') score += 6; + else if (rawType === 'industrybuild') score += 8; + else if (rawType === 'industrycontribution') score += 4; + else if (rawType === 'industryallocation') score += 10; + else if (rawType === 'industryvote') score += 2; + else if (rawType === 'industrymember') score += 2; + else if (rawType === 'industryopinion') score += 1; + else if (rawType === 'wikipage') score += c.replaces ? 2 : 6; + else if (rawType === 'emergency') score += c.replaces ? 1 : 5; + else if (rawType === 'emergencyconfirm') score += 1; + else if (rawType === 'emergencyupdate') score += 2; + else if (rawType === 'mailinglist') score += c.replaces ? 1 : 3; + else if (rawType === 'logisticsroute') score += c.replaces ? 1 : 4; + else if (rawType === 'logisticsrating' || rawType === 'logisticsopinion') score += 1; + else if (rawType === 'podcast') score += c.replaces ? 1 : 5; + else if (rawType === 'podcastepisode') score += c.replaces ? 1 : 4; + else if (rawType === 'podcastopinion' || rawType === 'podcastplay') score += 1; + else if (rawType === 'campaign') score += c.replaces ? 1 : 5; + else if (rawType === 'campaignsignature' || rawType === 'campaignopinion') score += 1; + else if (rawType === 'campaignupdate') score += 2; } return Math.max(0, Math.round(score)); }; @@ -289,12 +367,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { const ssbClient = await openSsb(); const userId = ssbClient.id; - const messages = await new Promise((res, rej) => { - pull( - ssbClient.createLogStream({ limit: logLimit, reverse: true }), - pull.collect((err, msgs) => err ? rej(err) : res(msgs)) - ); - }); + const messages = (await readTyped(ssbClient, CONTENT_TYPES, { limit: logLimit, withWindow: true })).reverse(); const allMsgs = messages.filter(m => { const c = m.value && m.value.content; @@ -308,15 +381,15 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { const scopedMsgs = filter === 'MINE' ? allMsgs.filter(m => m.value.author === userId) : allMsgs; const byType = {}; - const parentOf = {}; + const versionsOf = {}; for (const t of types) { byType[t] = new Map(); - parentOf[t] = new Map(); + versionsOf[t] = []; } const fpIdx = tribeCrypto ? tribeCrypto.buildFingerprintIndex() : null; const accessibleTribeIds = await buildAccessibleTribeIds(); - for (const m of scopedMsgs) { + for (const m of allMsgs) { const k = m.key; let c = m.value.content; if (tribeCrypto && tribeCrypto.isTribeMsg(c)) { @@ -336,27 +409,20 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { let theType = c.type; if (!types.includes(theType)) continue; byType[theType].set(k, { key: k, ts: m.value.timestamp, content: c, author: m.value.author }); - if (c.replaces) parentOf[theType].set(k, c.replaces); + versionsOf[theType].push(versionEntry({ key: k, value: { ...m.value, content: c } })); } - const findRoot = (t, id) => { - let cur = id; - const pMap = parentOf[t]; - while (pMap.has(cur)) cur = pMap.get(cur); - return cur; - }; + const vgOf = {}; + for (const t of types) vgOf[t] = versionGroups(versionsOf[t]); + const findRoot = (t, id) => vgOf[t].rootOf(id); const tipOf = {}; for (const t of types) { tipOf[t] = new Map(); - const pMap = parentOf[t]; - const fwd = new Map(); - for (const [child, parent] of pMap.entries()) fwd.set(parent, child); const allMap = byType[t]; - const roots = new Set(Array.from(allMap.keys()).map(id => findRoot(t, id))); - for (const root of roots) { - let tip = root; - while (fwd.has(tip)) tip = fwd.get(tip); + for (const root of vgOf[t].roots) { + if (filter === 'MINE' && allMap.get(root).author !== userId) continue; + const tip = vgOf[t].tipOf(root); if (tombTargets.has(tip)) continue; const node = allMap.get(tip) || allMap.get(root); if (node) tipOf[t].set(root, node); @@ -387,6 +453,11 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { .filter(n => n.content?.isAnonymous !== false && Array.isArray(n.content?.members) && n.content.members.includes(userId)) .map(n => ({ id: findRoot('tribe', n.key), name: n.content.name || n.content.title || n.key })); + const opinionTargets = new Set(); + for (const m of allMsgs) { + const c = m && m.value && m.value.content; + if (c && typeof c.type === 'string' && c.type.endsWith('Opinion') && typeof c.target === 'string') opinionTargets.add(c.target); + } const content = {}; const opinions = {}; for (const t of types) { @@ -398,7 +469,9 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { if (t === 'forum') vals = vals.filter(c => !(c.root && tombTargets.has(c.root))); } content[t] = vals.length || 0; - opinions[t] = vals.filter(e => Array.isArray(e.opinions_inhabitants) && e.opinions_inhabitants.length > 0).length || 0; + const embedded = vals.filter(e => Array.isArray(e.opinions_inhabitants) && e.opinions_inhabitants.length > 0).length || 0; + const external = t === 'tribe' ? 0 : Array.from(tipOf[t].keys()).filter(root => vgOf[t].groups.get(root).some(id => opinionTargets.has(id))).length; + opinions[t] = Math.max(embedded, external); } if (filter === 'MINE') { @@ -420,12 +493,10 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { const inhabitants = new Set(allMsgs.map(m => m.value.author)).size; - const secretStat = fs.statSync(`${os.homedir()}/.ssb/secret`); + const secretStat = fs.statSync(path.join(ssbDirOf(), 'secret')); const createdAt = secretStat.birthtime.toLocaleString(); - const folderSize = getFolderSize(`${os.homedir()}/.ssb`); - const flumeSize = getFolderSize(`${os.homedir()}/.ssb/flume`); - const blobsSize = getFolderSize(`${os.homedir()}/.ssb/blobs`); + const { total: folderSize, log: logSize, blobs: blobsSize } = await ssbDiskUsage(ssbDirOf()); const allTs = scopedMsgs.map(m => m.value.timestamp || 0).filter(Boolean); const lastTs = allTs.length ? Math.max(...allTs) : 0; @@ -564,7 +635,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { userTombstoneCount: scopedMsgs.filter(m => m.value.content.type === 'tombstone' && m.value.author === userId).length, networkTombstoneCount: validatedTombstoneCount, folderSize: formatSize(folderSize), - statsBlockchainSize: formatSize(flumeSize), + statsBlockchainSize: formatSize(logSize), statsBlobsSize: formatSize(blobsSize), pubsCount, activity: { diff --git a/src/models/subscriptions_model.js b/src/models/subscriptions_model.js new file mode 100644 index 0000000..bc94f6a --- /dev/null +++ b/src/models/subscriptions_model.js @@ -0,0 +1,104 @@ +const { getConfig } = require('../configs/config-manager.js'); +const { readTyped } = require('./typed_log'); +const logLimit = getConfig().ssbLogStream?.limit || 1000; + +module.exports = ({ cooler }) => { + let ssb; + const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb; }; + + const latestByTargetAuthor = (messages) => { + const latest = new Map(); + for (const m of messages) { + const c = m?.value?.content; + if (!c || c.type !== 'subscription' || typeof c.target !== 'string') continue; + const author = m.value.author; + const ts = m.value.timestamp || 0; + const k = `${c.target}::${author}`; + const prev = latest.get(k); + if (!prev || ts >= prev.ts) latest.set(k, { ts, target: c.target, scope: String(c.scope || ''), author, on: c.on !== false }); + } + return latest; + }; + + return { + async setSubscription(target, scope, on) { + const ssbClient = await openSsb(); + if (typeof target !== 'string' || !target.trim()) throw new Error('Invalid target'); + const content = { + type: 'subscription', + target: target.trim(), + scope: String(scope || '').trim().slice(0, 32), + on: on !== false, + updatedAt: new Date().toISOString() + }; + return new Promise((res, rej) => ssbClient.publish(content, (e, m) => (e ? rej(e) : res(m)))); + }, + + async myState(target) { + const ssbClient = await openSsb(); + const messages = await readTyped(ssbClient, ['subscription'], { limit: logLimit }); + const latest = latestByTargetAuthor(messages); + const mine = latest.get(`${target}::${ssbClient.id}`); + if (!mine) return null; + return mine.on ? 'on' : 'off'; + }, + + async isSubscribed(target) { + const ssbClient = await openSsb(); + const messages = await readTyped(ssbClient, ['subscription'], { limit: logLimit }); + const latest = latestByTargetAuthor(messages); + const mine = latest.get(`${target}::${ssbClient.id}`); + return !!(mine && mine.on); + }, + + async listSubscribers(target) { + const ssbClient = await openSsb(); + const messages = await readTyped(ssbClient, ['subscription'], { limit: logLimit }); + const latest = latestByTargetAuthor(messages); + const out = []; + for (const entry of latest.values()) { + if (entry.target === target && entry.on) out.push(entry.author); + } + return Array.from(new Set(out)); + }, + + async mySubscriptions() { + const ssbClient = await openSsb(); + const messages = await readTyped(ssbClient, ['subscription'], { limit: logLimit }); + const latest = latestByTargetAuthor(messages); + const out = []; + for (const entry of latest.values()) { + if (entry.author === ssbClient.id && entry.on) out.push({ target: entry.target, scope: entry.scope }); + } + return out; + }, + + async subscribersMap(targets) { + const ssbClient = await openSsb(); + const messages = await readTyped(ssbClient, ['subscription'], { limit: logLimit }); + const latest = latestByTargetAuthor(messages); + const out = new Map(); + for (const t of targets || []) out.set(t, []); + for (const entry of latest.values()) { + if (!out.has(entry.target) || !entry.on) continue; + out.get(entry.target).push(entry.author); + } + return out; + }, + + async subscriberCounts(targets) { + const ssbClient = await openSsb(); + const messages = await readTyped(ssbClient, ['subscription'], { limit: logLimit }); + const latest = latestByTargetAuthor(messages); + const counts = new Map(); + for (const t of targets || []) counts.set(t, 0); + const mine = new Set(); + for (const entry of latest.values()) { + if (!counts.has(entry.target) || !entry.on) continue; + counts.set(entry.target, counts.get(entry.target) + 1); + if (entry.author === ssbClient.id) mine.add(entry.target); + } + return { counts, mine }; + } + }; +}; diff --git a/src/models/tags_model.js b/src/models/tags_model.js index f074453..f88a6ae 100644 --- a/src/models/tags_model.js +++ b/src/models/tags_model.js @@ -1,6 +1,8 @@ const pull = require('../server/node_modules/pull-stream'); const { getConfig } = require('../configs/config-manager.js'); const { buildValidatedTombstoneSet } = require('./tombstone_validator'); +const { readTyped, CONTENT_TYPES } = require('./typed_log'); +const { versionGroups, versionEntry } = require('../backend/dedupe'); const logLimit = getConfig().ssbLogStream?.limit || 1000; module.exports = ({ cooler, padsModel, tribesModel }) => { @@ -13,7 +15,10 @@ module.exports = ({ cooler, padsModel, tribesModel }) => { const norm = (v) => String(v == null ? '' : v).trim().toLowerCase(); const normalizeTag = (tag) => String(tag == null ? '' : tag).trim().replace(/^#/, ''); - const tagKey = (tag) => normalizeTag(tag).toLowerCase(); + const tagKey = (tag) => { + const k = normalizeTag(tag).toLowerCase(); + return k.includes(':') ? '' : k; + }; const getDedupeKey = (msg) => { const c = msg?.value?.content || {}; @@ -78,6 +83,10 @@ module.exports = ({ cooler, padsModel, tribesModel }) => { return ['job', author, norm(c.title), norm(c.location), norm(c.salary), norm(c.job_type)].join('|'); } + if (t === 'industry') { + return ['industry', author, norm(c.name), norm(c.sector)].join('|'); + } + if (t === 'forum') { return `forum:${c.key || c.root || `${author}|${norm(c.title)}` || msg.key}`; } @@ -98,19 +107,15 @@ module.exports = ({ cooler, padsModel, tribesModel }) => { }; return { - async listTags(filter = 'all') { + async listTags(filter = 'all', search = '', opts = {}) { const ssbClient = await openSsb(); + const hiddenAuthors = opts.hiddenAuthors instanceof Set ? opts.hiddenAuthors : null; + const hiddenIds = opts.hiddenIds instanceof Set ? opts.hiddenIds : null; - const messages = await new Promise((resolve, reject) => { - pull( - ssbClient.createLogStream({ limit: logLimit }), - pull.collect((err, msgs) => err ? reject(err) : resolve(msgs)) - ); - }); + const messages = await readTyped(ssbClient, CONTENT_TYPES, { limit: logLimit, withWindow: true }); const tombstoned = buildValidatedTombstoneSet(messages); - const replacesMap = new Map(); const latestByKey = new Map(); for (const msg of messages) { @@ -118,24 +123,15 @@ module.exports = ({ cooler, padsModel, tribesModel }) => { const c = msg?.value?.content; const t = c?.type; if (!k || !c || !t) continue; - if (tombstoned.has(k)) continue; if (t === 'tombstone') continue; - if (c.replaces) replacesMap.set(c.replaces, k); latestByKey.set(k, msg); } - for (const [oldId, newId] of Array.from(replacesMap.entries())) { - const oldMsg = latestByKey.get(oldId); - if (!oldMsg) { replacesMap.delete(oldId); continue; } - const newMsg = latestByKey.get(newId); - if (!newMsg || String(newMsg?.value?.author) !== String(oldMsg?.value?.author)) { - replacesMap.delete(oldId); - latestByKey.delete(newId); - } + const vg = versionGroups(Array.from(latestByKey.values()).map(versionEntry)); + for (const k of Array.from(latestByKey.keys())) { + if (!vg.roots.has(vg.rootOf(k)) || vg.tipOf(k) !== k || tombstoned.has(k)) latestByKey.delete(k); } - for (const oldId of replacesMap.keys()) latestByKey.delete(oldId); - const viewerId = ssbClient.id; const viewerVisibleTribeIds = new Set(); if (tribesModel) { @@ -146,6 +142,8 @@ module.exports = ({ cooler, padsModel, tribesModel }) => { let filtered = Array.from(latestByKey.values()).filter(msg => { const c = msg?.value?.content; if (!c || c.type === 'tombstone') return false; + if (hiddenAuthors && hiddenAuthors.has(String(msg?.value?.author || ''))) return false; + if (hiddenIds && hiddenIds.has(msg.key)) return false; if (tombstoned.has(msg.key)) return false; if (c.encryptedPayload) return false; if (!Array.isArray(c.tags) || !c.tags.filter(Boolean).length) return false; @@ -154,21 +152,35 @@ module.exports = ({ cooler, padsModel, tribesModel }) => { if (c.type === 'task' && String(c.isPublic).toUpperCase() === 'PRIVATE') return false; if ((c.type === 'chat' || c.type === 'pad' || c.type === 'map' || c.type === 'calendar') && c.status === 'INVITE-ONLY' && c.author !== viewerId && !(Array.isArray(c.members) && c.members.includes(viewerId))) return false; if (c.type === 'shop' && c.visibility === 'CLOSED') return false; + if (c.type === 'housing' && String(c.visibility || '').toUpperCase() === 'HIDDEN' && c.author !== viewerId) return false; + if (c.type === 'poll' && c.chatId) return false; return true; }); filtered = dedupeKeepLatest(filtered); const counts = new Map(); + const DAY = 24 * 60 * 60 * 1000; + + const bump = (k, display, { mine, ts }) => { + const prev = counts.get(k); + if (!prev) counts.set(k, { name: display, count: 1, mine: mine ? 1 : 0, lastTs: ts || 0 }); + else counts.set(k, { + name: prev.name || display, + count: prev.count + 1, + mine: prev.mine + (mine ? 1 : 0), + lastTs: Math.max(prev.lastTs || 0, ts || 0) + }); + }; for (const record of filtered) { const tagsArr = record?.value?.content?.tags || []; const uniqueTags = new Set(tagsArr.map(tagKey).filter(Boolean)); + const mine = String(record?.value?.author || '') === String(viewerId); + const ts = Number(record?.value?.timestamp || record?.timestamp || 0); for (const k of uniqueTags) { const display = normalizeTag(tagsArr.find(t => tagKey(t) === k) || k) || k; - const prev = counts.get(k); - if (!prev) counts.set(k, { name: display, count: 1 }); - else counts.set(k, { name: prev.name || display, count: prev.count + 1 }); + bump(k, display, { mine, ts }); } } @@ -177,21 +189,29 @@ module.exports = ({ cooler, padsModel, tribesModel }) => { const viewerId = ssbClient2.id; const pads = await padsModel.listAll({ filter: 'all', viewerId }).catch(() => []); for (const pad of pads) { + if (hiddenAuthors && hiddenAuthors.has(String(pad.author || ''))) continue; + if (hiddenIds && (hiddenIds.has(pad.id) || hiddenIds.has(pad.key) || hiddenIds.has(pad.rootId))) continue; if (pad.status === 'INVITE-ONLY' && pad.author !== viewerId && !(Array.isArray(pad.members) && pad.members.includes(viewerId))) continue; if (!Array.isArray(pad.tags)) continue; const uniquePadTags = new Set(pad.tags.map(tagKey).filter(Boolean)); for (const k of uniquePadTags) { const display = normalizeTag(pad.tags.find(t => tagKey(t) === k) || k) || k; - const prev = counts.get(k); - if (!prev) counts.set(k, { name: display, count: 1 }); - else counts.set(k, { name: prev.name || display, count: prev.count + 1 }); + bump(k, display, { mine: String(pad.author || '') === String(viewerId), ts: Number(pad.createdAt ? new Date(pad.createdAt).getTime() : 0) }); } } } let tags = Array.from(counts.values()); - if (filter === 'top') { + const q = String(search || '').trim().toLowerCase().replace(/^#/, ''); + if (q) tags = tags.filter(t => String(t.name || '').toLowerCase().includes(q)); + + if (filter === 'mine') { + tags = tags.filter(t => (t.mine || 0) > 0).sort((a, b) => b.mine - a.mine || a.name.localeCompare(b.name)); + } else if (filter === 'recent') { + const since = Date.now() - DAY; + tags = tags.filter(t => (t.lastTs || 0) >= since).sort((a, b) => (b.lastTs || 0) - (a.lastTs || 0)); + } else if (filter === 'top') { tags.sort((a, b) => b.count - a.count || a.name.localeCompare(b.name)); } else if (filter === 'cloud') { const max = Math.max(...tags.map(t => t.count), 1); diff --git a/src/models/tasks_model.js b/src/models/tasks_model.js index a750a3b..c810f37 100644 --- a/src/models/tasks_model.js +++ b/src/models/tasks_model.js @@ -1,8 +1,10 @@ const pull = require('../server/node_modules/pull-stream'); const moment = require('../server/node_modules/moment'); +const { normalizeImages, normalizeVideo } = require('./media_gallery'); const { getConfig } = require('../configs/config-manager.js'); const { buildValidatedTombstoneSet } = require('./tombstone_validator'); -const { dedupeBy, norm } = require('../backend/dedupe'); +const { readTyped, memoIndex } = require('./typed_log'); +const { dedupeBy, norm, versionGroups } = require('../backend/dedupe'); const logLimit = getConfig().ssbLogStream?.limit || 1000; module.exports = ({ cooler, pmModel }) => { @@ -22,13 +24,9 @@ module.exports = ({ cooler, pmModel }) => { return fallback; }; - const getAllMessages = async (ssbClient) => - new Promise((resolve, reject) => { - pull( - ssbClient.createLogStream({ limit: logLimit }), - pull.collect((err, msgs) => (err ? reject(err) : resolve(msgs))) - ); - }); + const TASK_TYPES = ['task', 'taskOpinion', 'taskAssign', 'taskReminderSent', 'tombstone']; + + const getAllMessages = async (ssbClient) => readTyped(ssbClient, TASK_TYPES, { limit: logLimit }); const getMsg = async (ssbClient, key) => new Promise((resolve, reject) => { @@ -38,9 +36,6 @@ module.exports = ({ cooler, pmModel }) => { const buildIndex = (messages) => { const tomb = buildValidatedTombstoneSet(messages); const nodes = new Map(); - const parent = new Map(); - const child = new Map(); - const strictChild = new Map(); const opinionMsgs = []; const assignMsgs = []; @@ -56,34 +51,12 @@ module.exports = ({ cooler, pmModel }) => { if (c.type !== 'task') continue; const ts = v.timestamp || m.timestamp || 0; - nodes.set(k, { key: k, ts, c, author: v.author }); - - if (c.replaces) { - parent.set(k, c.replaces); - child.set(c.replaces, k); - } + nodes.set(k, { key: k, ts, seq: v.sequence || 0, c, author: v.author }); } - for (const [k, node] of nodes) { - const t = node.c.replaces; - if (t) { const orig = nodes.get(t); if (orig && orig.author === node.author) strictChild.set(t, k); } - } - - const rootOf = (id) => { - let cur = id, g = 0; - while (parent.has(cur) && nodes.has(parent.get(cur)) && g++ < 100000) cur = parent.get(cur); - return cur; - }; - - const contentTipOf = (root) => { - let cur = root, g = 0; - while (strictChild.has(cur) && g++ < 100000) cur = strictChild.get(cur); - const n = nodes.get(cur), rn = nodes.get(root); - return (n && rn && n.author === rn.author) ? cur : root; - }; - - const roots = new Set(); - for (const id of nodes.keys()) roots.add(rootOf(id)); + const vg = versionGroups([...nodes.values()].map(n => ({ key: n.key, author: n.author, seq: n.seq, ts: n.ts, replaces: n.c.replaces || null, claimedAuthor: typeof n.c.author === 'string' ? n.c.author : null }))); + const rootOf = vg.rootOf; + const contentTipOf = vg.tipOf; const opinionsByRoot = new Map(); for (const op of opinionMsgs) { if (!nodes.has(op.target)) continue; const r = rootOf(op.target); if (!opinionsByRoot.has(r)) opinionsByRoot.set(r, []); opinionsByRoot.get(r).push(op); } @@ -116,9 +89,9 @@ module.exports = ({ cooler, pmModel }) => { }; const tipByRoot = new Map(); - for (const r of roots) tipByRoot.set(r, contentTipOf(r)); + for (const r of vg.roots) tipByRoot.set(r, contentTipOf(r)); - return { tomb, nodes, parent, child, rootOf, tipByRoot, resolveGroup }; + return { tomb, nodes, rootOf, tipByRoot, resolveGroup }; }; const buildTask = (node, rootId, agg, now) => { @@ -131,6 +104,8 @@ module.exports = ({ cooler, pmModel }) => { id: node.key, rootId, ...c, + images: normalizeImages(c.images), + video: normalizeVideo(c.video), assignees, opinions, opinions_inhabitants: voters, @@ -138,8 +113,13 @@ module.exports = ({ cooler, pmModel }) => { }; }; + const currentTipId = async (ssb, id) => { + const idx = buildIndex(await getAllMessages(ssb)); + return idx.tipByRoot.get(idx.rootOf(id)) || id; + }; + return { - async createTask(title, description, startTime, endTime, priority, location = '', tagsRaw = [], isPublic) { + async createTask(title, description, startTime, endTime, priority, location = '', tagsRaw = [], isPublic, media = {}) { const ssb = await openSsb(); const userId = ssb.id; @@ -166,6 +146,8 @@ module.exports = ({ cooler, pmModel }) => { location, tags, isPublic: visibility, + images: normalizeImages(media && media.images), + video: normalizeVideo(media && media.video), assignees: [userId], createdAt: new Date().toISOString(), status: 'OPEN', @@ -180,9 +162,10 @@ module.exports = ({ cooler, pmModel }) => { async deleteTaskById(taskId) { const ssb = await openSsb(); const userId = ssb.id; - const task = await new Promise((res, rej) => ssb.get(taskId, (err, task) => err ? rej(new Error('Task not found')) : res(task))); + const tipId = await currentTipId(ssb, taskId); + const task = await new Promise((res, rej) => ssb.get(tipId, (err, task) => err ? rej(new Error('Task not found')) : res(task))); if (task.content.author !== userId) throw new Error('Not the author'); - const tombstone = { type: 'tombstone', target: taskId, deletedAt: new Date().toISOString(), author: userId }; + const tombstone = { type: 'tombstone', target: tipId, deletedAt: new Date().toISOString(), author: userId }; return new Promise((res, rej) => ssb.publish(tombstone, (err, result) => err ? rej(err) : res(result))); }, @@ -193,8 +176,9 @@ module.exports = ({ cooler, pmModel }) => { const keys = Object.keys(updatedData || {}).filter(k => updatedData[k] !== undefined); if (keys.length === 1 && keys[0] === 'assignees') return this.toggleAssignee(taskId); + const tipId = await currentTipId(ssb, taskId); const old = await new Promise((res, rej) => - ssb.get(taskId, (err, msg) => err || !msg ? rej(new Error('Task not found')) : res(msg)) + ssb.get(tipId, (err, msg) => err || !msg ? rej(new Error('Task not found')) : res(msg)) ); const c = old.content; @@ -253,9 +237,11 @@ module.exports = ({ cooler, pmModel }) => { tags: newTags, isPublic: newVisibility, status: newStatus, + images: normalizeImages(updatedData.images !== undefined ? updatedData.images : c.images), + video: updatedData.video !== undefined ? normalizeVideo(updatedData.video) : normalizeVideo(c.video), assignees: nextAssignees, updatedAt: new Date().toISOString(), - replaces: taskId + replaces: tipId }; return new Promise((res, rej) => ssb.publish(updated, (err, result) => err ? rej(err) : res(result))); @@ -271,10 +257,10 @@ module.exports = ({ cooler, pmModel }) => { const ssb = await openSsb(); const now = moment(); const messages = await getAllMessages(ssb); - const idx = buildIndex(messages); + const idx = memoIndex('tasks', messages, buildIndex); const rootId = idx.rootOf(taskId); const agg = idx.resolveGroup(rootId); - if (!agg.contentNode || idx.tomb.has(agg.contentTip)) throw new Error('Task not found'); + if (!idx.tipByRoot.has(rootId) || !agg.contentNode || idx.tomb.has(agg.contentTip)) throw new Error('Task not found'); return buildTask(agg.contentNode, rootId, agg, now); }, diff --git a/src/models/tombstone_validator.js b/src/models/tombstone_validator.js index a3a5b4d..e90cdb8 100644 --- a/src/models/tombstone_validator.js +++ b/src/models/tombstone_validator.js @@ -2,26 +2,27 @@ function buildValidatedTombstoneSet(messages) { if (!Array.isArray(messages)) return new Set(); const tombClaims = new Map(); const targetAuthors = new Map(); + const targetContentAuthors = new Map(); const targetRoots = new Map(); + const claims = []; for (const m of messages) { if (!m || !m.value) continue; const v = m.value; const c = v.content; if (!c) continue; if (typeof c === 'object' && c.type === 'tombstone' && typeof c.target === 'string') { - const ts = v.timestamp || 0; - const prev = tombClaims.get(c.target); - if (!prev || ts > prev.ts) tombClaims.set(c.target, { author: v.author, ts, rootId: c._rootId || null }); + claims.push({ target: c.target, author: v.author, rootId: c._rootId || null }); continue; } if (m.key) { targetAuthors.set(m.key, v.author); + if (typeof c === 'object' && c.type === 'feed' && typeof c.author === 'string') targetContentAuthors.set(m.key, c.author); if (typeof c === 'object' && c._rootId) targetRoots.set(m.key, c._rootId); } } const out = new Set(); - for (const [target, { author, rootId }] of tombClaims.entries()) { - if (targetAuthors.get(target) !== author) continue; + for (const { target, author, rootId } of claims) { + if (targetAuthors.get(target) !== author && targetContentAuthors.get(target) !== author) continue; if (rootId) { const targetRoot = targetRoots.get(target); if (targetRoot !== rootId) continue; diff --git a/src/models/torrent_downloads_model.js b/src/models/torrent_downloads_model.js new file mode 100644 index 0000000..7da185f --- /dev/null +++ b/src/models/torrent_downloads_model.js @@ -0,0 +1,169 @@ +const pull = require('../server/node_modules/pull-stream'); +const Abortable = require('../server/node_modules/pull-abortable'); + +const JOB_TTL_MS = 6 * 60 * 60 * 1000; +const PROBE_TIMEOUT_MS = 8000; +const RECHECK_MS = 15000; + +module.exports = ({ cooler }) => { + const jobs = new Map(); + let recheckTimer = null; + + const openSsb = () => cooler.open(); + const withTimeout = (p, ms) => Promise.race([p, new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), ms))]); + + const peerRpcs = (ssb) => { + const out = []; + const live = ssb && ssb.peers && typeof ssb.peers === 'object' ? ssb.peers : {}; + for (const id of Object.keys(live)) { + if (id === ssb.id) continue; + for (const rpc of (Array.isArray(live[id]) ? live[id] : [])) { + if (rpc && rpc.blobs && typeof rpc.blobs.get === 'function' && !rpc.closed) out.push({ id, rpc }); + } + } + return out; + }; + + const hasLocal = (ssb, blobId) => new Promise((resolve) => { + try { ssb.blobs.has(blobId, (err, has) => resolve(!err && !!has)); } catch (_) { resolve(false); } + }); + + const remoteHas = (peer, blobId) => withTimeout(new Promise((resolve) => { + try { peer.rpc.blobs.has(blobId, (err, has) => resolve(!err && !!has)); } catch (_) { resolve(false); } + }), PROBE_TIMEOUT_MS).catch(() => false); + + const findSeeds = async (ssb, blobId) => { + const peers = peerRpcs(ssb); + const flags = await Promise.all(peers.map((p) => remoteHas(p, blobId))); + return peers.filter((_, i) => flags[i]); + }; + + const fetchFrom = (ssb, seed, job) => new Promise((resolve, reject) => { + const abortable = Abortable(); + job.abort = () => abortable.abort(new Error('cancelled')); + let received = 0; + const cap = job.size > 0 ? job.size : 75 * 1024 * 1024; + const opts = { key: job.blobId, max: cap }; + pull( + seed.rpc.blobs.get(opts), + abortable, + pull.through((buf) => { received += buf.length; job.received = received; if (received > cap) abortable.abort(new Error('too large')); }), + ssb.blobs.add(job.blobId, (err) => { job.abort = null; err ? reject(err) : resolve(received); }) + ); + }); + + const finish = (job, state, error) => { + job.state = state; + job.error = error ? String(error.message || error) : ''; + job.finishedAt = Date.now(); + job.abort = null; + if (state === 'done' && job.size > 0) job.received = job.size; + }; + + const ensureRecheck = () => { + if (recheckTimer) return; + recheckTimer = setInterval(async () => { + const pending = Array.from(jobs.values()).filter((j) => j.state === 'nosource'); + if (!pending.length) { clearInterval(recheckTimer); recheckTimer = null; return; } + let ssb; + try { ssb = await openSsb(); } catch (_) { return; } + for (const job of pending) { + if (await hasLocal(ssb, job.blobId)) { finish(job, 'done'); continue; } + const seeds = await findSeeds(ssb, job.blobId); + if (seeds.length) run(job); + } + }, RECHECK_MS); + if (recheckTimer.unref) recheckTimer.unref(); + }; + + const run = async (job) => { + if (job.state === 'downloading' || job.state === 'probing') return; + let ssb; + try { ssb = await openSsb(); } catch (err) { finish(job, 'failed', err); return; } + if (await hasLocal(ssb, job.blobId)) { finish(job, 'done'); return; } + job.state = 'probing'; + job.error = ''; + const seeds = await findSeeds(ssb, job.blobId); + job.seeds = seeds.map((s) => s.id); + if (!seeds.length) { + job.state = 'nosource'; + try { ssb.blobs.want(job.blobId, () => {}); } catch (_) {} + ensureRecheck(); + return; + } + job.state = 'downloading'; + job.startedAt = job.startedAt || Date.now(); + let lastErr = null; + for (const seed of seeds) { + if (job.cancelled) { finish(job, 'cancelled'); return; } + job.peer = seed.id; + job.received = 0; + try { + await fetchFrom(ssb, seed, job); + finish(job, 'done'); + return; + } catch (err) { + lastErr = err; + if (job.cancelled) { finish(job, 'cancelled'); return; } + } + } + finish(job, 'failed', lastErr); + }; + + const sweep = () => { + const now = Date.now(); + for (const [key, job] of jobs) if (job.finishedAt && now - job.finishedAt > JOB_TTL_MS) jobs.delete(key); + }; + + const snapshot = (job) => ({ + blobId: job.blobId, + name: job.name, + size: job.size, + received: Math.min(job.received || 0, job.size || job.received || 0), + state: job.state, + seeds: Array.isArray(job.seeds) ? job.seeds.length : 0, + peer: job.peer || '', + error: job.error || '', + torrentKey: job.torrentKey || '', + startedAt: job.startedAt || 0, + finishedAt: job.finishedAt || 0, + active: job.state === 'queued' || job.state === 'probing' || job.state === 'downloading' || job.state === 'nosource' + }); + + return { + start({ blobId, size, name, torrentKey } = {}) { + const id = String(blobId || ''); + if (!id.startsWith('&')) throw new Error('torrentFromContentMissing'); + sweep(); + let job = jobs.get(id); + if (job && (job.state === 'downloading' || job.state === 'probing' || job.state === 'done')) return snapshot(job); + job = { blobId: id, size: Number(size) || 0, name: String(name || ''), torrentKey: torrentKey || '', received: 0, state: 'queued', seeds: [], error: '', cancelled: false, startedAt: 0, finishedAt: 0, abort: null, peer: '' }; + jobs.set(id, job); + run(job); + return snapshot(job); + }, + cancel(blobId) { + const job = jobs.get(String(blobId || '')); + if (!job) return false; + job.cancelled = true; + if (typeof job.abort === 'function') { try { job.abort(); } catch (_) {} } + else if (job.state !== 'done') finish(job, 'cancelled'); + return true; + }, + remove(blobId) { return jobs.delete(String(blobId || '')); }, + get(blobId) { + const job = jobs.get(String(blobId || '')); + return job ? snapshot(job) : null; + }, + list() { + sweep(); + return Array.from(jobs.values()).map(snapshot).sort((a, b) => (b.startedAt || 0) - (a.startedAt || 0)); + }, + async hasLocal(blobId) { + try { return await hasLocal(await openSsb(), String(blobId || '')); } catch (_) { return false; } + }, + async seedsFor(blobId) { + try { return (await findSeeds(await openSsb(), String(blobId || ''))).map((s) => s.id); } catch (_) { return []; } + } + }; +}; diff --git a/src/models/torrents_model.js b/src/models/torrents_model.js index 9dc74cb..485e05d 100644 --- a/src/models/torrents_model.js +++ b/src/models/torrents_model.js @@ -1,8 +1,9 @@ const pull = require("../server/node_modules/pull-stream"); const { getConfig } = require("../configs/config-manager.js"); const categories = require("../backend/opinion_categories"); +const { readTyped, memoIndex } = require("./typed_log"); -const { dedupeBy, norm } = require('../backend/dedupe'); +const { dedupeBy, norm, versionGroups } = require('../backend/dedupe'); const logLimit = getConfig().ssbLogStream?.limit || 1000; const safeArr = (v) => (Array.isArray(v) ? v : []); @@ -13,6 +14,10 @@ const normalizeTags = (raw) => { return String(raw).split(",").map((t) => t.trim()).filter(Boolean); }; +const cipherField = (cipher) => (cipher && typeof cipher === "object" && cipher.key && cipher.manifestBlobId) + ? { cipher: { v: Number(cipher.v) || 1, key: String(cipher.key), manifestBlobId: String(cipher.manifestBlobId), chunkCount: Number(cipher.chunkCount) || 0 } } + : {}; + const parseBlobId = (blobMarkdown) => { const s = String(blobMarkdown || ""); const match = s.match(/\((&[^)]+\.sha256)\)/); @@ -40,10 +45,9 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { const unwrapForIndex = (msgs) => tribeHelpers ? tribeHelpers.unwrapMessagesForKind(msgs, 'torrent') : msgs; const tombFor = async (target, tribeId, author) => tribeHelpers ? tribeHelpers.encryptTombstone(target, tribeId, author) : { type: 'tombstone', target, deletedAt: new Date().toISOString(), author }; - const getAllMessages = async (ssbClient) => - new Promise((resolve, reject) => { - pull(ssbClient.createLogStream({ limit: logLimit }), pull.collect((err, msgs) => (err ? reject(err) : resolve(msgs)))); - }); + const TORRENT_TYPES = ["torrent", "torrentOpinion", "tombstone"]; + + const getAllMessages = async (ssbClient) => readTyped(ssbClient, TORRENT_TYPES, { limit: logLimit, withWindow: true }); const getMsg = async (ssbClient, key) => new Promise((resolve) => { @@ -53,9 +57,6 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { const buildIndex = (messages) => { const tomb = new Set(); const nodes = new Map(); - const parent = new Map(); - const child = new Map(); - const strictChild = new Map(); const authorByKey = new Map(); const tombRequests = []; const opinionMsgs = []; @@ -78,49 +79,24 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { const ts = v.timestamp || m.timestamp || 0; let sizeBytes = 0; try { sizeBytes = Buffer.byteLength(JSON.stringify(v), "utf8"); } catch (_) { sizeBytes = 0; } - nodes.set(k, { key: k, ts, c, sizeBytes, author: v.author }); + nodes.set(k, { key: k, ts, seq: v.sequence || 0, c, sizeBytes, author: v.author }); authorByKey.set(k, v.author); - - if (c.replaces) { - parent.set(k, c.replaces); - child.set(c.replaces, k); - } } - for (const [k, node] of nodes) { - const t = node.c.replaces; - if (t) { const orig = nodes.get(t); if (orig && orig.author === node.author) strictChild.set(t, k); } - } - - const rootOf = (id) => { - let cur = id, g = 0; - while (parent.has(cur) && nodes.has(parent.get(cur)) && g++ < 100000) cur = parent.get(cur); - return cur; - }; - - const contentTipOf = (root) => { - let cur = root, g = 0; - while (strictChild.has(cur) && g++ < 100000) cur = strictChild.get(cur); - const n = nodes.get(cur), rn = nodes.get(root); - return (n && rn && n.author === rn.author) ? cur : root; - }; + const vg = versionGroups([...nodes.values()].map(n => ({ key: n.key, author: n.author, seq: n.seq, ts: n.ts, replaces: n.c.replaces || null, claimedAuthor: typeof n.c.author === "string" ? n.c.author : null }))); + const rootOf = vg.rootOf; + const contentTipOf = vg.tipOf; for (const t of tombRequests) { const targetAuthor = authorByKey.get(t.target); if (targetAuthor && t.author === targetAuthor) tomb.add(t.target); } - const roots = new Set(); - for (const id of nodes.keys()) roots.add(rootOf(id)); - const opinionsByRoot = new Map(); for (const op of opinionMsgs) { if (!nodes.has(op.target)) continue; const r = rootOf(op.target); if (!opinionsByRoot.has(r)) opinionsByRoot.set(r, []); opinionsByRoot.get(r).push(op); } const tipByRoot = new Map(); - for (const r of roots) tipByRoot.set(r, contentTipOf(r)); - - const forward = new Map(); - for (const [newId, oldId] of parent.entries()) forward.set(oldId, newId); + for (const r of vg.roots) tipByRoot.set(r, contentTipOf(r)); const resolveGroup = (root) => { const contentTip = contentTipOf(root); @@ -137,7 +113,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { return { contentTip, contentNode, opinions, voters }; }; - return { tomb, nodes, parent, child, rootOf, tipByRoot, forward, resolveGroup }; + return { tomb, nodes, rootOf, tipByRoot, resolveGroup }; }; const buildTorrent = (node, rootId, viewerId, agg) => { @@ -156,6 +132,10 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { title: undec ? "" : (c.title || ""), description: undec ? "" : (c.description || ""), size: c.size || 0, + source: undec ? "" : (typeof c.source === "string" && c.source.startsWith("&") ? c.source : ""), + sourceName: undec ? "" : (c.sourceName || ""), + cipher: undec ? null : (c.cipher && typeof c.cipher === "object" && typeof c.cipher.key === "string" && typeof c.cipher.manifestBlobId === "string" ? { v: Number(c.cipher.v) || 1, key: c.cipher.key, manifestBlobId: c.cipher.manifestBlobId, chunkCount: Number(c.cipher.chunkCount) || 0 } : null), + sourceMime: undec ? "" : (c.sourceMime || ""), opinions, opinions_inhabitants: voters, hasVoted: viewerId ? voters.includes(viewerId) : false, @@ -184,16 +164,12 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { const messages = await getAllMessages(ssbClient); const idx = buildIndex(unwrapForIndex(messages)); - let tip = id; - while (idx.forward.has(tip)) tip = idx.forward.get(tip); - if (idx.tomb.has(tip)) throw new Error("Torrent not found"); - - let root = tip; - while (idx.parent.has(root)) root = idx.parent.get(root); + const root = idx.rootOf(id); + if (idx.tomb.has(idx.tipByRoot.get(root) || id)) throw new Error("Torrent not found"); return root; }, - async createTorrent(blobMarkdown, tagsRaw, title, description, size, tribeId) { + async createTorrent(blobMarkdown, tagsRaw, title, description, size, tribeId, source = null) { const ssbClient = await openSsb(); const blobId = parseBlobId(blobMarkdown); const tags = normalizeTags(tagsRaw) || []; @@ -211,7 +187,11 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { size: Number(size) || 0, opinions: {}, opinions_inhabitants: [], - ...(tribeId ? { tribeId } : {}) + ...(tribeId ? { tribeId } : {}), + ...(source && typeof source.source === "string" && source.source.startsWith("&") + ? { source: source.source, sourceName: String(source.sourceName || ""), sourceMime: String(source.sourceMime || "") } + : {}), + ...cipherField(source && source.cipher) }; content = await encryptIfTribe(content); @@ -221,7 +201,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { }); }, - async updateTorrentById(id, blobMarkdown, tagsRaw, title, description) { + async updateTorrentById(id, blobMarkdown, tagsRaw, title, description, extra = null) { const ssbClient = await openSsb(); const userId = ssbClient.id; const tipId = await this.resolveCurrentId(id); @@ -231,7 +211,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { const oldDec = await decryptIfTribe(oldMsg.content); if (!oldDec || oldDec.type !== "torrent") throw new Error("Torrent not found"); assertReadable(oldDec, "Torrent"); - if ((oldDec.author || oldMsg.content.author) !== userId) throw new Error("Not the author"); + if (oldMsg.author !== userId) throw new Error("Not the author"); const aggTorrent = await this.getTorrentById(id, userId); if (aggTorrent && Object.keys(aggTorrent.opinions || {}).some(k => (aggTorrent.opinions[k] || 0) > 0)) throw new Error("Cannot edit torrent after it has received opinions."); @@ -243,6 +223,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { type: "torrent", replaces: tipId, url: blobId || oldDec.url, + ...(blobId ? cipherField(extra && extra.cipher) : cipherField(oldDec.cipher)), tags, title: title !== undefined ? title || "" : oldDec.title || "", description: description !== undefined ? description || "" : oldDec.description || "", @@ -250,7 +231,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { opinions: oldDec.opinions || {}, opinions_inhabitants: oldDec.opinions_inhabitants || [], author: oldDec.author || userId, - ...(oldMsg.content.tribeId ? { tribeId: oldMsg.content.tribeId } : {}), + ...((oldDec.tribeId || oldMsg.content.tribeId) ? { tribeId: oldDec.tribeId || oldMsg.content.tribeId } : {}), createdAt: oldDec.createdAt, updatedAt: now }; @@ -274,7 +255,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { if (!msg) throw new Error("Torrent not found"); const dec = await decryptIfTribe(msg.content); if (!dec || dec.type !== "torrent") throw new Error("Torrent not found"); - if ((dec.author || msg.content.author) !== userId) throw new Error("Not the author"); + if (msg.author !== userId) throw new Error("Not the author"); const tombstone = await tombFor(tipId, dec.tribeId || msg.content.tribeId, userId); @@ -338,10 +319,10 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { const ssbClient = await openSsb(); const viewer = viewerId || ssbClient.id; const messages = await getAllMessages(ssbClient); - const idx = buildIndex(unwrapForIndex(messages)); - await decryptIndexNodes(idx); + const idx = await memoIndex('torrents', messages, async (m) => { const built = buildIndex(unwrapForIndex(m)); await decryptIndexNodes(built); return built; }); const root = idx.rootOf(id); + if (idx.nodes.has(root) && !idx.tipByRoot.has(root)) throw new Error("Torrent not found"); const agg = idx.resolveGroup(root); if (agg.contentNode && !idx.tomb.has(agg.contentTip)) return buildTorrent(agg.contentNode, root, viewer, agg); diff --git a/src/models/transfers_model.js b/src/models/transfers_model.js index 8e012e2..a4c2014 100644 --- a/src/models/transfers_model.js +++ b/src/models/transfers_model.js @@ -1,11 +1,14 @@ const pull = require("../server/node_modules/pull-stream") const moment = require("../server/node_modules/moment") -const { getConfig } = require("../configs/config-manager.js") +const { getConfig, defaultInvite } = require("../configs/config-manager.js") const categories = require("../backend/opinion_categories") const { buildValidatedTombstoneSet } = require('./tombstone_validator'); const { dedupeByPreferring, norm } = require('../backend/dedupe') +const { readTyped, memoIndex } = require('./typed_log') const logLimit = getConfig().ssbLogStream?.limit || 1000 +const TRANSFER_TYPES = ["transfer", "transferConfirm", "transferOpinion", "ubiClaim", "tombstone"] + const isValidId = (to) => /^@[A-Za-z0-9+/]+={0,2}\.ed25519$/.test(String(to || "")) const parseNum = (v) => { @@ -19,6 +22,11 @@ const normalizeTags = (raw) => { return String(raw).split(",").map(t => t.trim()).filter(Boolean) } +const defaultPubId = () => { + const m = String((defaultInvite() || {}).code || "").match(/(@[A-Za-z0-9+/]+={0,2}\.ed25519)/) + return m ? m[1] : "" +} + const CATEGORIES = ["ECONOMIC", "TIME", "TRUST"] const normalizeCategory = (raw) => { const c = String(raw || "ECONOMIC").trim().toUpperCase() @@ -28,14 +36,16 @@ const normalizeCategory = (raw) => { module.exports = ({ cooler }) => { let ssb const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb } + const ubiEpochOf = (c) => { + const tags = Array.isArray(c.tags) ? c.tags.map(t => String(t)) : [] + const fromTag = tags.find(t => /^epoch:\d{4}-\d{2}$/i.test(t)) + if (fromTag) return fromTag.slice(6) + const m = String(c.concept || "").match(/(\d{4}-\d{2})/) + return m ? m[1] : "" + } const getAllMessages = async (ssbClient) => - new Promise((resolve, reject) => { - pull( - ssbClient.createLogStream({ limit: logLimit }), - pull.collect((err, msgs) => err ? reject(err) : resolve(msgs)) - ) - }) + readTyped(ssbClient, TRANSFER_TYPES, { limit: logLimit }) const getMsg = async (ssbClient, key) => new Promise((resolve, reject) => { @@ -43,7 +53,7 @@ module.exports = ({ cooler }) => { }) const buildIndex = (messages) => { - const tomb = new Set() + const tomb = buildValidatedTombstoneSet(messages) const nodes = new Map() const parent = new Map() const child = new Map() @@ -51,7 +61,7 @@ module.exports = ({ cooler }) => { const confirms = [] const opinionMsgs = [] const ubiByPub = new Map() - const ubiByUser = new Map() + const claimedPubs = new Map() const ubiClaimNodes = [] for (const m of messages) { @@ -60,43 +70,43 @@ module.exports = ({ cooler }) => { const c = v.content if (!c) continue - if (c.type === "tombstone" && c.target) { - tomb.add(c.target) - continue - } - if (c.type === "transferConfirm") { confirms.push({ target: c.target, author: v.author, ts: v.timestamp || 0 }); continue } + if (c.type === "tombstone") continue + if (c.type === "transferConfirm") { confirms.push({ target: c.target, tip: typeof c.tip === "string" ? c.tip : null, author: v.author, ts: v.timestamp || 0 }); continue } if (c.type === "transferOpinion") { opinionMsgs.push({ target: c.target, author: v.author, category: c.category, ts: v.timestamp || 0 }); continue } if (c.type === "transfer") { + if (c.from !== v.author) continue nodes.set(k, { key: k, ts: v.timestamp || m.timestamp || 0, c, author: v.author }) if (c.replaces) { parent.set(k, c.replaces) child.set(c.replaces, k) } const tags = Array.isArray(c.tags) ? c.tags.map(t => String(t).toUpperCase()) : [] - if (tags.includes("UBI") && c.to && c.concept) { - const key = `${c.to}::${c.concept}` - if (v.author === c.from) ubiByPub.set(key, k) - else ubiByUser.set(key, k) + if (tags.includes("UBI") && !tags.includes("REBALANCE") && c.to && c.concept) { + const key = `${c.to}::${ubiEpochOf(c) || c.concept}` + if (!ubiByPub.has(key)) ubiByPub.set(key, new Set()) + ubiByPub.get(key).add(v.author) } } if (c.type === "ubiClaim") { ubiClaimNodes.push({ k, v, c, ts: v.timestamp || m.timestamp || 0 }) + if (c.pubId) { + if (!claimedPubs.has(v.author)) claimedPubs.set(v.author, new Set()) + claimedPubs.get(v.author).add(c.pubId) + } } } - for (const [key, userMsgKey] of ubiByUser.entries()) { - if (ubiByPub.has(key)) tomb.add(userMsgKey) - } - + const defaultPub = defaultPubId() for (const { k, v, c, ts } of ubiClaimNodes) { if (tomb.has(k)) continue const claimantId = v.author const epochId = c.epochId || "" - const concept = `UBI ${epochId} ${claimantId}`.trim() - const key = `${claimantId}::${concept}` - if (ubiByPub.has(key) || ubiByUser.has(key)) continue + const concept = `UBI - ${epochId}`.trim() + const key = `${claimantId}::${epochId || concept}` + const payers = ubiByPub.get(key) + if (payers && ((c.pubId && payers.has(c.pubId)) || (defaultPub && payers.has(defaultPub)))) continue const synthetic = { type: "transfer", from: c.pubId || "", @@ -106,7 +116,7 @@ module.exports = ({ cooler }) => { createdAt: c.claimedAt || new Date(ts).toISOString(), updatedAt: c.claimedAt || new Date(ts).toISOString(), deadline: null, - confirmedBy: [c.pubId || ""].filter(Boolean), + confirmedBy: [], status: "UNCONFIRMED", tags: ["UBI", "PENDING"], opinions: {}, @@ -153,42 +163,43 @@ module.exports = ({ cooler }) => { const resolveGroup = (root) => { const contentTip = contentTipOf(root) - const best = nodes.get(contentTip) || nodes.get(root) - if (!best) return null + const latest = nodes.get(contentTip) || nodes.get(root) + if (!latest) return null const rootAuthor = nodes.get(root) ? nodes.get(root).author : null - const groupKeys = []; { let x = root, g = 0; groupKeys.push(x); while (child.has(x) && g++ < 100000) { x = child.get(x); groupKeys.push(x) } } + const rootConfirms = confirmsByRoot.get(root) || [] + const seal = rootConfirms.filter(cf => { const n = cf.tip ? nodes.get(cf.tip) : null; return n && n.author === rootAuthor && rootOf(cf.tip) === root && cf.author === n.c.to }).sort((a, b) => a.ts - b.ts)[0] + const best = seal ? nodes.get(seal.tip) : latest const confirmedBy = new Set() const opinions = {}; const opinionSet = new Set() - for (const k of groupKeys) { - const n = nodes.get(k); if (!n) continue; if (n.author !== rootAuthor) continue; const c = n.c - for (const cb of (Array.isArray(c.confirmedBy) ? c.confirmedBy : [])) confirmedBy.add(cb) - for (const v of (Array.isArray(c.opinions_inhabitants) ? c.opinions_inhabitants : [])) opinionSet.add(v) - if (c.opinions && typeof c.opinions === "object") for (const kk of Object.keys(c.opinions)) opinions[kk] = Math.max(opinions[kk] || 0, Number(c.opinions[kk]) || 0) - } - if (best.c.from) confirmedBy.add(best.c.from) - for (const cf of (confirmsByRoot.get(root) || [])) confirmedBy.add(cf.author) + if (best.c.from && best.c.from === best.author) confirmedBy.add(best.c.from) + for (const cf of rootConfirms) { if (cf.author === best.c.to) confirmedBy.add(cf.author) } for (const op of (opinionsByRoot.get(root) || [])) { if (!opinionSet.has(op.author)) { opinionSet.add(op.author); opinions[op.category] = (opinions[op.category] || 0) + 1 } } - return { contentTip, best, confirmedBy: [...confirmedBy], opinions, opinions_inhabitants: [...opinionSet], tombstoned: tomb.has(contentTip) } + const ubiPayerTrusted = !!best.author && (best.author === defaultPub || !!(claimedPubs.get(best.c.to) && claimedPubs.get(best.c.to).has(best.author))) + return { contentTip, best, confirmedBy: [...confirmedBy], opinions, opinions_inhabitants: [...opinionSet], tombstoned: tomb.has(contentTip) || tomb.has(best.key), ubiPayerTrusted } } return { tomb, nodes, parent, child, rootOf, tipByRoot, resolveGroup } } - const deriveStatus = (t) => { - const status = String(t.status || "").toUpperCase() + const isSettledUbi = (t) => { + const tags = Array.isArray(t.tags) ? t.tags.map(x => String(x).toUpperCase()) : [] + return tags.includes("UBI") && !tags.includes("PENDING") && /^[0-9a-f]{64}$/i.test(String(t.txid || "")) + } + + const deriveStatus = (t, ubiPayerTrusted = false) => { const required = t.from === t.to ? 1 : 2 const confirmedCount = Array.isArray(t.confirmedBy) ? t.confirmedBy.length : 0 - if (status === "DISCARDED") return "DISCARDED" + if (ubiPayerTrusted && isSettledUbi(t)) return "CLOSED" if (confirmedCount >= required) return "CLOSED" const dl = t.deadline ? moment(t.deadline) : null if (dl && dl.isValid() && dl.isBefore(moment())) return "DISCARDED" - return status === "CLOSED" ? "CLOSED" : "UNCONFIRMED" + return "UNCONFIRMED" } const buildTransfer = (node, agg) => { const c = node.c || {} - const confirmedBy = agg ? agg.confirmedBy : (Array.isArray(c.confirmedBy) ? c.confirmedBy : []) + const confirmedBy = agg ? agg.confirmedBy : (c.from && c.from === node.author ? [c.from] : []) const opinions = agg ? agg.opinions : (c.opinions || {}) const opinions_inhabitants = agg ? agg.opinions_inhabitants : (Array.isArray(c.opinions_inhabitants) ? c.opinions_inhabitants : []) return { @@ -203,8 +214,9 @@ module.exports = ({ cooler }) => { updatedAt: c.updatedAt || null, deadline: c.deadline, confirmedBy, - status: deriveStatus({ ...c, confirmedBy }), + status: deriveStatus({ ...c, confirmedBy }, !!(agg && agg.ubiPayerTrusted)), tags: Array.isArray(c.tags) ? c.tags : [], + txid: typeof c.txid === "string" ? c.txid : null, opinions, opinions_inhabitants } @@ -269,6 +281,7 @@ module.exports = ({ cooler }) => { const old = await getMsg(ssbClient, tipId) if (!old?.content || old.content.type !== "transfer") throw new Error("Transfer not found") + if (old.author !== old.content.from) throw new Error("Not the author") const current = old.content const idxU = buildIndex(await getAllMessages(ssbClient)) @@ -331,6 +344,7 @@ module.exports = ({ cooler }) => { if (!msg?.content) throw new Error("Not found") if (msg.content.type !== "transfer") throw new Error("Not found") + if (msg.author !== msg.content.from) throw new Error("Not found") const idx = buildIndex(await getAllMessages(ssbClient)) const root = idx.rootOf(tipId) @@ -345,7 +359,7 @@ module.exports = ({ cooler }) => { if ((Array.isArray(t.confirmedBy) ? t.confirmedBy : []).includes(userId)) throw new Error("Already confirmed") - const content = { type: "transferConfirm", target: root, createdAt: new Date().toISOString() } + const content = { type: "transferConfirm", target: root, tip: tipId, createdAt: new Date().toISOString() } return new Promise((resolve, reject) => { ssbClient.publish(content, (e2, result) => e2 ? reject(e2) : resolve(result)) }) @@ -358,6 +372,7 @@ module.exports = ({ cooler }) => { const msg = await getMsg(ssbClient, tipId) if (!msg?.content || msg.content.type !== "transfer") throw new Error("Not found") + if (msg.author !== msg.content.from) throw new Error("Not the author") const idxD = buildIndex(await getAllMessages(ssbClient)) const gD = idxD.resolveGroup(idxD.rootOf(tipId)) @@ -396,7 +411,7 @@ module.exports = ({ cooler }) => { async getTransferById(id) { const ssbClient = await openSsb() const messages = await getAllMessages(ssbClient) - const idx = buildIndex(messages) + const idx = memoIndex('transfers', messages, buildIndex) const root = idx.rootOf(id) const g = idx.resolveGroup(root) @@ -405,6 +420,7 @@ module.exports = ({ cooler }) => { const msg = await getMsg(ssbClient, id) if (!msg?.content || msg.content.type !== "transfer") throw new Error("Not found") + if (msg.author !== msg.content.from) throw new Error("Not found") const tmpNode = { key: id, ts: msg.timestamp || 0, c: msg.content, author: msg.author } return buildTransfer(tmpNode) diff --git a/src/models/trending_model.js b/src/models/trending_model.js index 0528d61..e25dd9d 100644 --- a/src/models/trending_model.js +++ b/src/models/trending_model.js @@ -3,7 +3,15 @@ const { getConfig } = require('../configs/config-manager.js'); const logLimit = getConfig().ssbLogStream?.limit || 1000; const opinionCategories = require('../backend/opinion_categories'); const { buildValidatedTombstoneSet } = require('./tombstone_validator'); +const { readTyped, CONTENT_TYPES, requestScope } = require('./typed_log'); +const longText = require('../backend/long_text'); +const isRootBlogPost = (c) => + !!c && c.type === 'post' && typeof c.text === 'string' && + !c.root && !c.fork && !c.branch && !c.about && !c.target && + c.private !== true && !Array.isArray(c.recps); + const { buildVoteTally } = require('../backend/vote_tally'); +const { versionGroups, versionEntry } = require('../backend/dedupe'); module.exports = ({ cooler }) => { let ssb; @@ -19,61 +27,80 @@ module.exports = ({ cooler }) => { }; const types = [ - 'bookmark', 'votes', 'feed', - 'image', 'audio', 'video', 'document', 'torrent', 'transfer' + 'bookmark', 'votes', 'poll', 'feed', 'post', + 'image', 'audio', 'video', 'document', 'torrent', 'file', 'transfer', + 'industry', 'project', 'report', 'task', 'event', 'shopProduct', 'housing', 'market', 'schoolCourse', + 'podcast', 'podcastEpisode', 'campaign', 'logisticsRoute' ]; const categories = opinionCategories; - const listTrending = async (filter = 'ALL') => { + const computeBase = async () => { const ssbClient = await openSsb(); - const userId = ssbClient.id; - - const messages = await new Promise((res, rej) => { - pull( - ssbClient.createLogStream({ limit: logLimit }), - pull.collect((err, xs) => err ? rej(err) : res(xs)) - ); - }); + const messages = await readTyped(ssbClient, CONTENT_TYPES, { limit: logLimit, withWindow: true }); const tombstoned = buildValidatedTombstoneSet(messages); - const replaces = new Map(); const itemsById = new Map(); + const opinionMsgs = []; for (const m of messages) { const k = m.key; const c = m.value?.content; if (!c) continue; - if (c.type === 'tombstone' && c.target) { - tombstoned.add(c.target); - itemsById.delete(c.target); + if (c.type === 'tombstone') continue; + + if (typeof c.type === 'string' && c.type.endsWith('Opinion') && c.target) { + opinionMsgs.push({ target: c.target, author: m.value?.author, category: c.category }); continue; } - if (c.opinions && !tombstoned.has(k) && !['task', 'event', 'report'].includes(c.type)) { - if (c.replaces) replaces.set(c.replaces, k); - itemsById.set(k, m); - } + if (c.opinions && !['task', 'event', 'report'].includes(c.type)) itemsById.set(k, m); - if (c.type === 'feed' && !tombstoned.has(k) && !itemsById.has(k)) { - if (c.replaces) replaces.set(c.replaces, k); - itemsById.set(k, m); + if (c.type === 'feed' && !itemsById.has(k)) itemsById.set(k, m); + if (isRootBlogPost(c) && !itemsById.has(k)) itemsById.set(k, m); + } + + const chunkLookup = longText.lookupIn(longText.indexChunks(messages)); + for (const [k, m] of Array.from(itemsById.entries())) { + const c = m.value?.content; + if (c?.type === 'post' && longText.hasChunks(c)) { + itemsById.set(k, { ...m, value: { ...m.value, content: longText.resolveField(c, 'text', m.value.author, chunkLookup) } }); } } - for (const [replacedId, newId] of replaces.entries()) { - const oldMsg = itemsById.get(replacedId); - const newMsg = itemsById.get(newId); - if (!oldMsg) continue; - if (!newMsg || String(newMsg.value?.author) !== String(oldMsg.value?.author)) { - itemsById.delete(newId); - continue; - } - itemsById.delete(replacedId); + const vg = versionGroups(Array.from(itemsById.values()).map(versionEntry)); + for (const k of Array.from(itemsById.keys())) { + if (!vg.roots.has(vg.rootOf(k)) || vg.tipOf(k) !== k || tombstoned.has(k)) itemsById.delete(k); + } + + const opinionByTip = new Map(); + for (const op of opinionMsgs) { + const tip = vg.tipOf(op.target); + if (!opinionByTip.has(tip)) opinionByTip.set(tip, { opinions: {}, voters: new Set() }); + const agg = opinionByTip.get(tip); + if (agg.voters.has(op.author)) continue; + agg.voters.add(op.author); + if (op.category) agg.opinions[op.category] = (agg.opinions[op.category] || 0) + 1; + } + for (const [k, m] of Array.from(itemsById.entries())) { + const agg = opinionByTip.get(k); + if (!agg) continue; + const c = m.value?.content || {}; + const mergedVoters = new Set([...(Array.isArray(c.opinions_inhabitants) ? c.opinions_inhabitants : [])]); + const mergedOpinions = { ...(c.opinions || {}) }; + for (const [cat, n] of Object.entries(agg.opinions)) mergedOpinions[cat] = (mergedOpinions[cat] || 0) + n; + for (const v of agg.voters) mergedVoters.add(v); + itemsById.set(k, { ...m, value: { ...m.value, content: { ...c, opinions: mergedOpinions, opinions_inhabitants: [...mergedVoters] } } }); } let rawItems = Array.from(itemsById.values()).filter(m => types.includes(m.value?.content?.type)); + rawItems = rawItems.filter(m => { + const c = m.value?.content; + if (c?.type !== 'schoolCourse') return true; + if (c.encryptedPayload) return false; + return String(c.visibility || 'PUBLIC').toUpperCase() !== 'INVITE'; + }); const blobTypes = ['document', 'image', 'audio', 'video']; let items = await Promise.all( @@ -101,7 +128,8 @@ module.exports = ({ cooler }) => { case 'feed': return `feed::${(c.text || '').replace(/\s+/g, ' ').trim()}`; case 'votes': - return `votes::${(c.question || '').replace(/\s+/g, ' ').trim()}`; + case 'poll': + return `${c.type}::${(c.question || '').replace(/\s+/g, ' ').trim()}`; case 'transfer': return `transfer::${(c.concept || '')}|${c.amount || ''}|${c.from || ''}|${c.to || ''}|${c.deadline || ''}`; default: @@ -135,7 +163,21 @@ module.exports = ({ cooler }) => { bySig.set(sig, m); } } - items = Array.from(bySig.values()); + return Array.from(bySig.values()); + }; + + const baseItems = () => { + const store = requestScope.getStore(); + if (store && store.trendingBase) return store.trendingBase; + const pending = computeBase(); + if (store) store.trendingBase = pending; + return pending; + }; + + const listTrending = async (filter = 'ALL') => { + const ssbClient = await openSsb(); + const userId = ssbClient.id; + let items = (await baseItems()).slice(); if (filter === 'MINE') { items = items.filter(m => m.value.author === userId); @@ -148,9 +190,7 @@ module.exports = ({ cooler }) => { items = items.filter(m => m.value.content.type === filter); } - if (filter !== 'ALL' && !types.includes(filter)) { - items = items.filter(m => (m.value.content.opinions_inhabitants || []).length > 0); - } + items = items.filter(m => (m.value.content.opinions_inhabitants || []).length > 0); if (filter === 'TOP') { items.sort((a, b) => { @@ -193,31 +233,12 @@ module.exports = ({ cooler }) => { const inhabitants = Array.isArray(msg.content.opinions_inhabitants) ? msg.content.opinions_inhabitants : []; if (inhabitants.includes(userId)) throw new Error('Already voted'); + const otype = categories.messageTypes[type]; + if (!otype) throw new Error('Voting not allowed on this content type'); - const tombstone = { - type: 'tombstone', - target: contentId, - deletedAt: new Date().toISOString(), - author: userId - }; - - const updated = { - ...msg.content, - opinions: { - ...(msg.content.opinions || {}), - [category]: ((msg.content.opinions && msg.content.opinions[category]) || 0) + 1 - }, - opinions_inhabitants: inhabitants.concat(userId), - updatedAt: new Date().toISOString(), - replaces: contentId - }; - - await new Promise((res, rej) => { - ssbClient.publish(tombstone, err => err ? rej(err) : res()); - }); - + const content = { type: otype, target: contentId, category, createdAt: new Date().toISOString() }; return new Promise((res, rej) => { - ssbClient.publish(updated, (err, result) => err ? rej(err) : res(result)); + ssbClient.publish(content, (err, result) => err ? rej(err) : res(result)); }); }; diff --git a/src/models/tribe_crypto.js b/src/models/tribe_crypto.js deleted file mode 100644 index f5576b7..0000000 --- a/src/models/tribe_crypto.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = (configPath) => require('./crypto')(configPath, 'tribes'); diff --git a/src/models/tribes_content_model.js b/src/models/tribes_content_model.js index 83fe943..048f3e9 100644 --- a/src/models/tribes_content_model.js +++ b/src/models/tribes_content_model.js @@ -2,11 +2,30 @@ const pull = require('../server/node_modules/pull-stream'); const { getConfig } = require('../configs/config-manager.js'); const logLimit = getConfig().ssbLogStream?.limit || 1000; const tribeLogLimit = Math.max(logLimit, 100000); +const { readTyped } = require('./typed_log'); +const longText = require('../backend/long_text'); +const { versionGroups } = require('../backend/dedupe'); +const TRIBE_LOG_TYPES = ['tribe', 'tribe-msg', 'tribe-keys-distrib', 'tribe-invite-msg', 'tribe-invite-tombstone', 'tribe-open-invite-tombstone', 'tombstone', 'contact']; -const VALID_CONTENT_TYPES = ['event', 'task', 'report', 'votation', 'forum', 'forum-reply', 'market', 'job', 'project', 'media', 'feed', 'pixelia']; +const VALID_CONTENT_TYPES = ['event', 'task', 'report', 'votation', 'forum', 'forum-reply', 'market', 'job', 'project', 'media', 'feed']; const categories = require('../backend/opinion_categories'); const VALID_STATUSES = ['OPEN', 'CLOSED', 'IN-PROGRESS']; const VALID_PRIORITIES = ['LOW', 'MEDIUM', 'HIGH', 'CRITICAL']; +const EXPOSURE_TYPE = 'tribeExposure'; +const REACH_LEVELS = ['tribe', 'oasis', 'clearnet']; +const REACH_RANK = { tribe: 0, oasis: 1, clearnet: 2 }; +const SNAPSHOT_FIELDS = ['contentType', 'title', 'description', 'status', 'date', 'location', 'price', 'salary', 'priority', 'options', 'category', 'tags', 'image', 'mediaType', 'license', 'url', 'deadline', 'goal', 'dates', 'lat', 'lng', 'markers', 'createdAt', 'updatedAt']; +const snapshotOf = (item, replies = []) => { + const out = {}; + for (const f of SNAPSHOT_FIELDS) { + const v = item[f]; + if (v === undefined || v === null || v === '' || (Array.isArray(v) && !v.length)) continue; + out[f] = v; + } + out.author = item.author; + if (item.contentType === 'forum') out.replies = (replies || []).map(r => ({ author: r.author, description: r.description || '', createdAt: r.createdAt || null })); + return out; +}; module.exports = ({ cooler, tribeCrypto, tribesModel }) => { let ssb; @@ -14,12 +33,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { const readLog = async () => { const client = await openSsb(); - return new Promise((resolve, reject) => - pull( - client.createLogStream({ limit: tribeLogLimit }), - pull.collect((err, msgs) => err ? reject(err) : resolve(msgs)) - ) - ); + return readTyped(client, TRIBE_LOG_TYPES, { limit: tribeLogLimit }); }; const fingerprintsForRoot = (rootId) => { @@ -38,6 +52,12 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { ); }; + const publishContent = async (rootId, body) => { + const publishChunk = (c) => wrapAndPublishContent(rootId, { k: longText.CHUNK_TYPE, rootId, text: c.text, index: c.index, total: c.total, group: c.group }); + const sealed = await longText.chunkContent(body, 'description', { maxBytes: longText.CONTENT_BYTES.sealed, extra: { rootId }, publish: publishChunk }); + return wrapAndPublishContent(rootId, sealed); + }; + const decodeContentMsgs = async (msgs, opts) => { const fpIdx = tribeCrypto.buildFingerprintIndex(); const targetRootId = opts && opts.rootId ? opts.rootId : null; @@ -45,6 +65,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { const allowedFps = targetRootId ? fingerprintsForRoot(targetRootId) : null; const content = new Map(); + const chunks = new Map(); const tombRequests = []; const collabMsgs = []; @@ -62,36 +83,30 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { tombRequests.push({ target: b.target, author: m.value.author }); continue; } + if (b.k === longText.CHUNK_TYPE) { + const ch = longText.chunkOf(m.value.author, b); + if (ch) chunks.set(m.key, ch); + continue; + } if (b.k === 'tc-collab') { - collabMsgs.push({ sub: b.sub, target: b.target, author: m.value.author, ts: m.value.timestamp || 0, option: b.option, on: b.on, category: b.category }); + collabMsgs.push({ sub: b.sub, target: b.target, author: m.value.author, ts: m.value.timestamp || 0, seq: m.value.sequence || 0, option: b.option, on: b.on, category: b.category }); continue; } if (b.k !== 'tribe-content') continue; - content.set(m.key, { author: m.value.author, body: b, ts: m.value.timestamp }); + content.set(m.key, { author: m.value.author, body: b, ts: m.value.timestamp, seq: m.value.sequence || 0 }); } - const naiveNext = new Map(); - const strictNext = new Map(); - for (const [key, node] of content) { - const t = node.body.replaces; - if (!t) continue; - naiveNext.set(t, key); - const orig = content.get(t); - if (orig && orig.author === node.author) strictNext.set(t, key); + if (targetRootId && tribesModel && typeof tribesModel.departures === 'function') { + const gone = await tribesModel.departures(targetRootId).catch(() => new Map()); + const afterLeaving = (author, ts, seq) => { + const d = gone.get(author); + return !!d && ((d.seq !== undefined && seq > d.seq) || (d.ts !== undefined && (Number(ts) || 0) > d.ts)); + }; + for (const [key, node] of [...content]) if (afterLeaving(node.author, node.ts, node.seq)) content.delete(key); + for (let i = collabMsgs.length - 1; i >= 0; i--) if (afterLeaving(collabMsgs[i].author, collabMsgs[i].ts, collabMsgs[i].seq)) collabMsgs.splice(i, 1); } - for (const [t, key] of Array.from(naiveNext.entries())) { - const orig = content.get(t); - if (!orig) { naiveNext.delete(t); continue; } - const node = content.get(key); - if (!node || node.author !== orig.author) { naiveNext.delete(t); content.delete(key); } - } - const naivePrev = new Map(); - for (const [t, key] of naiveNext) naivePrev.set(key, t); - const followTo = (start, nextMap) => { let x = start, g = 0; while (nextMap.has(x) && g++ < 100000) x = nextMap.get(x); return x; }; - const rootOf = (key) => { let x = key, g = 0; while (naivePrev.has(x) && content.has(naivePrev.get(x)) && g++ < 100000) x = naivePrev.get(x); return x; }; - const roots = new Set(); - for (const [key, node] of content) { const t = node.body.replaces; if (!t || !content.has(t)) roots.add(key); } + const vg = versionGroups([...content].map(([key, node]) => ({ key, author: node.author, seq: node.seq, ts: node.ts, replaces: typeof node.body.replaces === 'string' ? node.body.replaces : null, claimedAuthor: typeof node.body.author === 'string' ? node.body.author : null }))); const tombstoned = new Set(); for (const t of tombRequests) { const orig = content.get(t.target); if (orig && t.author === orig.author) tombstoned.add(t.target); } @@ -100,7 +115,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { const getCB = (root) => { if (!collabByRoot.has(root)) collabByRoot.set(root, { vote: new Map(), attend: new Map(), assign: new Map(), refeed: new Set(), opinion: new Map() }); return collabByRoot.get(root); }; for (const cm of collabMsgs) { if (!content.has(cm.target)) continue; - const a = getCB(rootOf(cm.target)); + const a = getCB(vg.rootOf(cm.target)); if (cm.sub === 'vote' && Number.isInteger(cm.option)) { if (!a.vote.has(cm.author)) a.vote.set(cm.author, cm.option); } else if (cm.sub === 'attend') { const p = a.attend.get(cm.author); if (!p || cm.ts >= p.ts) a.attend.set(cm.author, { on: cm.on !== false, ts: cm.ts }); } else if (cm.sub === 'assign') { const p = a.assign.get(cm.author); if (!p || cm.ts >= p.ts) a.assign.set(cm.author, { on: cm.on !== false, ts: cm.ts }); } @@ -112,15 +127,15 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { const tipOf = new Map(); const EMPTY = { vote: new Map(), attend: new Map(), assign: new Map(), refeed: new Set(), opinion: new Map() }; - for (const root of roots) { - const contentTip = followTo(root, strictNext); + for (const root of vg.roots) { + const contentTip = vg.tipOf(root); const tipNode = content.get(contentTip); if (!tipNode) continue; const body = tipNode.body; const lb = tipNode.body; const a = collabByRoot.get(root) || EMPTY; - const item = { id: contentTip, ...body, author: tipNode.author, _ts: tipNode.ts }; + const item = { id: contentTip, ...body, description: longText.joinText(body.description, body.chunks, longText.lookupIn(chunks), tipNode.author), author: tipNode.author, _ts: tipNode.ts, originId: root }; const voteAuthors = new Set(); const votes = {}; const legacyVotes = lb.votes && typeof lb.votes === 'object' ? lb.votes : {}; @@ -145,8 +160,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { for (const [au, cat] of a.opinion) if (!opInh.has(au)) { opInh.add(au); opinions[cat] = (opinions[cat] || 0) + 1; } item.opinions_inhabitants = [...opInh]; item.opinions = opinions; - let cur = root, g = 0; tipOf.set(root, contentTip); - while (naiveNext.has(cur) && g++ < 100000) { cur = naiveNext.get(cur); tipOf.set(cur, contentTip); } + for (const k of (vg.groups.get(root) || [root])) tipOf.set(k, contentTip); if (tombstoned.has(contentTip)) continue; if (targetRootId && body.rootId !== targetRootId) continue; @@ -157,7 +171,23 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { return { items, tipOf, tombstoned }; }; + const chainIsPublic = async (tribeId) => { + let cur; + try { cur = await tribesModel.getTribeById(tribeId); } catch (_) { return false; } + const seen = new Set(); + while (cur) { + if (cur.isAnonymous) return false; + if (!cur.parentTribeId) return true; + if (seen.has(cur.parentTribeId)) return false; + seen.add(cur.parentTribeId); + try { cur = await tribesModel.getTribeById(cur.parentTribeId); } catch (_) { return false; } + } + return false; + }; + return { + chainIsPublic, + async create(tribeId, contentType, data) { if (!VALID_CONTENT_TYPES.includes(contentType)) throw new Error('Invalid content type'); if (data.status && !VALID_STATUSES.includes(data.status)) throw new Error('Invalid status. Must be OPEN, CLOSED, or IN-PROGRESS'); @@ -187,6 +217,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { tags: data.tags || [], image: data.image || null, mediaType: data.mediaType || null, + ...(data.license ? { license: data.license } : {}), url: data.url || null, attendees: data.attendees || [], deadline: data.deadline || null, @@ -200,7 +231,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { createdAt: now, updatedAt: now }; - return wrapAndPublishContent(rootId, body); + return publishContent(rootId, body); }, async update(contentId, data, existing) { @@ -215,7 +246,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { k: 'tribe-content', rootId, contentType: existing.contentType, - replaces: contentId, + replaces: existing.id || contentId, title: data.title !== undefined ? data.title : existing.title, description: data.description !== undefined ? data.description : existing.description, status: data.status !== undefined ? data.status : existing.status, @@ -232,6 +263,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { tags: data.tags !== undefined ? data.tags : existing.tags, image: data.image !== undefined ? data.image : existing.image, mediaType: data.mediaType !== undefined ? data.mediaType : existing.mediaType, + ...((data.license !== undefined ? data.license : existing.license) ? { license: data.license !== undefined ? data.license : existing.license } : {}), url: data.url !== undefined ? data.url : existing.url, attendees: data.attendees !== undefined ? data.attendees : existing.attendees, deadline: data.deadline !== undefined ? data.deadline : existing.deadline, @@ -245,7 +277,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { createdAt: existing.createdAt, updatedAt: now }; - return wrapAndPublishContent(rootId, body); + return publishContent(rootId, body); }, async deleteById(contentId) { @@ -256,7 +288,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { const body = { k: 'tombstone', rootId, - target: contentId, + target: existing.id || contentId, author: client.id, deletedAt: new Date().toISOString() }; @@ -347,6 +379,103 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { return wrapAndPublishContent(item.rootId, { k: 'tc-collab', sub: 'opinion', rootId: item.rootId, target: item.id, category }); }, + reachLevels: REACH_LEVELS, + + async publishExposure(item, level, replies = []) { + if (!REACH_LEVELS.includes(level)) throw new Error('Invalid level'); + if (!item || !item.rootId) throw new Error('Content not found'); + const client = await openSsb(); + const publish = (c) => new Promise((resolve, reject) => client.publish(c, (err, r) => err ? reject(err) : resolve(r))); + const content = { + type: EXPOSURE_TYPE, + tribeId: item.rootId, + origin: item.originId || item.id, + contentType: item.contentType, + level, + item: null, + at: new Date().toISOString() + }; + if (level !== 'tribe') { + const { replies: snapReplies, ...snapshot } = snapshotOf(item, replies); + const budget = longText.CONTENT_BYTES.plain - longText.contentBytes(content); + const sealed = await longText.chunkContent(snapshot, 'description', { maxBytes: budget, publish }); + if (Array.isArray(snapReplies)) { + sealed.replies = snapReplies.slice(); + while (sealed.replies.length && longText.contentBytes({ ...content, item: sealed }) > longText.CONTENT_BYTES.plain) sealed.replies.pop(); + } + content.item = sealed; + } + return publish(content); + }, + + async exposureState({ ignorePrivacy = false } = {}) { + const client = await openSsb(); + const msgs = await readTyped(client, [EXPOSURE_TYPE, longText.CHUNK_TYPE], { limit: tribeLogLimit }); + const lookup = longText.lookupIn(longText.indexChunks(msgs)); + const byOrigin = new Map(); + for (const m of msgs) { + const c = m && m.value && m.value.content; + if (!c || c.type !== EXPOSURE_TYPE || typeof c.origin !== 'string' || typeof c.tribeId !== 'string' || !REACH_LEVELS.includes(c.level)) continue; + if (!byOrigin.has(c.origin)) byOrigin.set(c.origin, []); + byOrigin.get(c.origin).push({ by: m.value.author, ts: Number(m.value.timestamp) || 0, c }); + } + const memberCache = new Map(); + const isMember = async (author, tribeId) => { + const k = `${tribeId}|${author}`; + if (!memberCache.has(k)) memberCache.set(k, await tribesModel.isTribeMember(author, tribeId).catch(() => false)); + return memberCache.get(k); + }; + const everCache = new Map(); + const wasMember = async (author, tribeId) => { + const k = `${tribeId}|${author}`; + if (!everCache.has(k)) everCache.set(k, await tribesModel.wasEverMember(author, tribeId).catch(() => false)); + return everCache.get(k); + }; + const privacyCache = new Map(); + const isPrivate = async (tribeId) => { + if (!privacyCache.has(tribeId)) privacyCache.set(tribeId, !(await chainIsPublic(tribeId))); + return privacyCache.get(tribeId); + }; + const state = new Map(); + for (const [origin, list] of byOrigin) { + list.sort((a, b) => (b.ts - a.ts) || (REACH_RANK[a.c.level] - REACH_RANK[b.c.level])); + let chosen = null; + for (const e of list) { + if (await isMember(e.by, e.c.tribeId)) { chosen = e; break; } + if (e.c.level === 'tribe' && await wasMember(e.by, e.c.tribeId)) { chosen = e; break; } + } + if (!chosen) continue; + const priv = await isPrivate(chosen.c.tribeId); + const level = !ignorePrivacy && priv ? 'tribe' : chosen.c.level; + state.set(origin, { origin, tribeId: chosen.c.tribeId, contentType: chosen.c.contentType, level, item: level === 'tribe' || !chosen.c.item ? null : longText.resolveField(chosen.c.item, 'description', chosen.by, lookup), by: chosen.by, ts: chosen.ts }); + } + return state; + }, + + async levelOf(item, state = null) { + const st = state || await this.exposureState(); + const e = st.get(item && (item.originId || item.id)); + return e ? e.level : 'tribe'; + }, + + async listExposed(tribeId, contentType = null, minLevel = 'oasis', state = null) { + const rootId = await tribesModel.getRootId(tribeId).catch(() => tribeId); + const st = state || await this.exposureState(); + const out = []; + for (const e of st.values()) { + if (e.tribeId !== rootId || !e.item || REACH_RANK[e.level] < REACH_RANK[minLevel]) continue; + const base = { ...e.item, id: e.origin, originId: e.origin, rootId, reach: e.level, exposedBy: e.by, _ts: e.ts, attendees: [], assignees: [], votes: {}, opinions: {}, opinions_inhabitants: [], refeeds: 0, refeeds_inhabitants: [] }; + if (contentType === 'forum-reply') { + if (e.item.contentType !== 'forum') continue; + (e.item.replies || []).forEach((r, i) => out.push({ id: `${e.origin}:${i}`, originId: `${e.origin}:${i}`, rootId, contentType: 'forum-reply', parentId: e.origin, description: r.description, author: r.author, createdAt: r.createdAt, reach: e.level, attendees: [], assignees: [], votes: {}, opinions: {}, opinions_inhabitants: [], refeeds_inhabitants: [] })); + continue; + } + if (contentType && e.item.contentType !== contentType) continue; + out.push(base); + } + return out.sort((a, b) => (Date.parse(b.updatedAt || b.createdAt) || b._ts || 0) - (Date.parse(a.updatedAt || a.createdAt) || a._ts || 0)); + }, + async getThread(forumId) { const msgs = await readLog(); const { items } = await decodeContentMsgs(msgs, {}); diff --git a/src/models/tribes_model.js b/src/models/tribes_model.js index 7d2a799..8aa345b 100644 --- a/src/models/tribes_model.js +++ b/src/models/tribes_model.js @@ -4,6 +4,8 @@ const { getConfig } = require('../configs/config-manager.js'); const { buildValidatedTombstoneSet } = require('./tombstone_validator'); const logLimit = getConfig().ssbLogStream?.limit || 1000; const tribeLogLimit = Math.max(logLimit, 100000); +const { readTyped } = require('./typed_log'); +const TRIBE_LOG_TYPES = ['tribe', 'tribe-msg', 'tribe-keys-distrib', 'tribe-invite-msg', 'tribe-invite-tombstone', 'tribe-open-invite', 'tribe-open-invite-tombstone', 'tombstone', 'contact']; const INVITE_CODE_BYTES = 16; const VALID_INVITE_MODES = ['strict', 'open']; @@ -25,13 +27,13 @@ module.exports = ({ cooler, tribeCrypto }) => { return true; }; - const validMembershipDelta = (prev, next, author) => { + const validMembershipDelta = (prev, next, author, selfJoinOk = () => true) => { const a = Array.isArray(prev) ? prev : []; const b = Array.isArray(next) ? next : []; const added = b.filter(m => !a.includes(m)); const removed = a.filter(m => !b.includes(m)); if (added.length === 0 && removed.length === 0) return true; - if (added.length === 1 && removed.length === 0 && added[0] === author) return true; + if (added.length === 1 && removed.length === 0 && added[0] === author) return selfJoinOk() === true; if (removed.length === 1 && added.length === 0 && removed[0] === author) return true; return false; }; @@ -49,19 +51,29 @@ module.exports = ({ cooler, tribeCrypto }) => { const x = a[f]; const y = b[f]; if (Array.isArray(x) || Array.isArray(y)) { if (!arraysEqual(x, y)) return false; continue; } - if (x !== y && !(x == null && y == null)) return false; + if (x !== y && !((x == null || x === '') && (y == null || y === ''))) return false; } return true; }; const streamLog = async () => { const client = await openSsb(); - return new Promise((resolve, reject) => { - pull( - client.createLogStream({ limit: tribeLogLimit }), - pull.collect((err, msgs) => err ? reject(err) : resolve(msgs)) - ); - }); + return readTyped(client, TRIBE_LOG_TYPES, { limit: tribeLogLimit }); + }; + const readKeyDistribs = async () => { + const client = await openSsb(); + const ssbKeys = require('../server/node_modules/ssb-keys'); + const config = require('../server/ssb_config'); + const msgs = await readTyped(client, TRIBE_LOG_TYPES, { limit: tribeLogLimit, withPrivate: true }); + const out = []; + for (const m of msgs) { + const v = m && m.value; + if (!v) continue; + const c = v.content; + const dec = (c && typeof c === 'object') ? (v.private === true && tribeCrypto.isKeyDistribContent(c) ? c : null) : tribeCrypto.tryUnboxKeyDistrib(c, config.keys, ssbKeys); + if (dec) out.push({ m, dec }); + } + return { msgs, distribs: out }; }; const buildTribeIndex = async () => { @@ -72,17 +84,31 @@ module.exports = ({ cooler, tribeCrypto }) => { const tribeMsgs = new Map(); const tombstones = new Map(); + const inviteAuthorByCh = new Map(); + const inviteAuthorByKey = new Map(); + const inviteTombstonesByAuthor = new Map(); + const sealedInvites = []; for (const m of msgs) { const c = m.value && m.value.content; if (!c) continue; const author = m.value.author; const ts = m.value.timestamp; + if (c.type === 'tribe-invite-msg' && typeof c.ch === 'string') { inviteAuthorByCh.set(c.ch, author); inviteAuthorByKey.set(m.key, author); continue; } + if (c.type === 'tribe-invite-tombstone' && typeof c.target === 'string') { + if (!inviteTombstonesByAuthor.has(author)) inviteTombstonesByAuthor.set(author, new Set()); + inviteTombstonesByAuthor.get(author).add(c.target); + continue; + } let body = null; if (tribeCrypto.isTribeMsg(c)) { const r = tribeCrypto.unwrapMsg(c, fpIdx); if (!r || !r.body || !r.body.k) continue; + if (r.body.k === 'open-invite' || r.body.k === 'open-invite-tombstone') { + sealedInvites.push({ key: m.key, author, body: r.body, keyRoot: r.rootId, ts }); + continue; + } body = r.body; } else if (c.type === 'tribe' && typeof c === 'object') { body = { @@ -104,7 +130,9 @@ module.exports = ({ cooler, tribeCrypto }) => { mapUrl: c.mapUrl, createdAt: c.createdAt, updatedAt: c.updatedAt, - author: c.author + author: c.author, + joinedWith: typeof c.joinedWith === 'string' ? c.joinedWith : undefined, + removedSeqs: c.removedSeqs && typeof c.removedSeqs === 'object' ? c.removedSeqs : undefined }; } else if (c.type === 'tombstone' && c.target) { tombstones.set(c.target, { author, ts }); @@ -125,6 +153,7 @@ module.exports = ({ cooler, tribeCrypto }) => { op: body.op || (body.rootId ? 'update' : 'create'), content: body, author, + seq: (m.value && m.value.sequence) || 0, _ts: ts }); } @@ -142,49 +171,58 @@ module.exports = ({ cooler, tribeCrypto }) => { } } - let progress = true; - while (progress) { - progress = false; - const candidatesByReplaces = new Map(); - for (const [k, entry] of tribeMsgs.entries()) { + const byReplaces = new Map(); + for (const [k, entry] of tribeMsgs.entries()) { + if (!entry.replaces) continue; + if (!byReplaces.has(entry.replaces)) byReplaces.set(entry.replaces, []); + byReplaces.get(entry.replaces).push([k, entry]); + } + const queue = [...tribes.keys()]; + for (let qi = 0; qi < queue.length; qi++) { + const replaces = queue[qi]; + if (child.has(replaces)) continue; + const parentEntry = tribes.get(replaces); + const root = rootByTip.get(replaces); + const rootEntry = tribes.get(root); + const rootAuthor = rootEntry && rootEntry.author; + const prevMembers = Array.isArray(parentEntry.content.members) ? parentEntry.content.members : []; + const candidates = []; + for (const [k, entry] of (byReplaces.get(replaces) || [])) { if (tribes.has(k)) continue; - const replaces = entry.replaces; - if (!replaces) continue; - const parentEntry = tribes.get(replaces); - if (!parentEntry) continue; - if (child.has(replaces)) continue; - const root = rootByTip.get(replaces); - const rootEntry = tribes.get(root); - const rootAuthor = rootEntry && rootEntry.author; const isRootAuthor = entry.author === rootAuthor; - const prevMembers = Array.isArray(parentEntry.content.members) ? parentEntry.content.members : []; if (!isRootAuthor) { if (!prevMembers.includes(entry.author) && !(entry.content.members || []).includes(entry.author)) continue; - if (!validMembershipDelta(prevMembers, entry.content.members, entry.author)) continue; + const mode = String(parentEntry.content.inviteMode || (rootEntry && rootEntry.content.inviteMode) || 'strict'); + const inviterOk = (inviter) => !!inviter && (inviter === rootAuthor || (mode === 'open' && prevMembers.includes(inviter))); + const selfJoinOk = () => { + const proof = entry.content.joinedWith; + if (proof && inviterOk(inviteAuthorByCh.get(proof))) return true; + const mine = inviteTombstonesByAuthor.get(entry.author); + if (mine) for (const target of mine) if (inviterOk(inviteAuthorByKey.get(target))) return true; + return false; + }; + if (!validMembershipDelta(prevMembers, entry.content.members, entry.author, selfJoinOk)) continue; if (!validInvitesDelta(parentEntry.content.invites, entry.content.invites, entry.author, rootAuthor)) continue; if (!structuralFieldsEqual(parentEntry.content, entry.content)) continue; } - if (!candidatesByReplaces.has(replaces)) candidatesByReplaces.set(replaces, []); - candidatesByReplaces.get(replaces).push({ k, entry, isRootAuthor, root }); + candidates.push({ k, entry, isRootAuthor, root }); } - for (const [replaces, candidates] of candidatesByReplaces.entries()) { - if (child.has(replaces)) continue; - let winner = candidates[0]; - for (let i = 1; i < candidates.length; i++) { - const c = candidates[i]; - if (c.isRootAuthor && !winner.isRootAuthor) { winner = c; continue; } - if (winner.isRootAuthor && !c.isRootAuthor) continue; - const wt = winner.entry._ts || 0; - const ct = c.entry._ts || 0; - if (ct < wt) winner = c; - else if (ct === wt && c.k < winner.k) winner = c; - } - parent.set(winner.k, replaces); - child.set(replaces, winner.k); - tribes.set(winner.k, winner.entry); - rootByTip.set(winner.k, winner.root); - progress = true; + if (!candidates.length) continue; + let winner = candidates[0]; + for (let i = 1; i < candidates.length; i++) { + const c = candidates[i]; + if (c.isRootAuthor && !winner.isRootAuthor) { winner = c; continue; } + if (winner.isRootAuthor && !c.isRootAuthor) continue; + const wt = winner.entry._ts || 0; + const ct = c.entry._ts || 0; + if (ct < wt) winner = c; + else if (ct === wt && c.k < winner.k) winner = c; } + parent.set(winner.k, replaces); + child.set(replaces, winner.k); + tribes.set(winner.k, winner.entry); + rootByTip.set(winner.k, winner.root); + queue.push(winner.k); } const tombstoned = new Set(); @@ -197,37 +235,34 @@ module.exports = ({ cooler, tribeCrypto }) => { } const rootOf = (id) => rootByTip.get(id) || id; - const tipOf = (id) => { let cur = id; while (child.has(cur)) cur = child.get(cur); return cur; }; const tipByRoot = new Map(); for (const k of tribes.keys()) { const root = rootOf(k); - const tip = tipOf(root); + if (tipByRoot.has(root)) continue; + let tip = root; + while (child.has(tip)) tip = child.get(tip); tipByRoot.set(root, tip); } const effectivelyTombstoned = new Set(tombstoned); - let cascade = true; - const seen = new Set(); - while (cascade) { - cascade = false; - for (const k of tribes.keys()) { - if (effectivelyTombstoned.has(k)) continue; - const root = rootOf(k); - if (effectivelyTombstoned.has(root)) { effectivelyTombstoned.add(k); cascade = true; continue; } + const deadMemo = new Map(); + const isDead = (k) => { + if (tombstoned.has(k)) return true; + if (deadMemo.has(k)) return deadMemo.get(k); + deadMemo.set(k, false); + const root = rootOf(k); + let dead = tombstoned.has(tipByRoot.get(root)) || (root !== k && isDead(root)); + if (!dead) { const e = tribes.get(k); const pid = e && e.content && e.content.parentTribeId; - if (!pid) continue; - if (seen.has(`${k}:${pid}`)) continue; - seen.add(`${k}:${pid}`); - const parentRoot = rootOf(pid); - if (effectivelyTombstoned.has(parentRoot) || effectivelyTombstoned.has(pid)) { - effectivelyTombstoned.add(k); - cascade = true; - } + if (pid) dead = isDead(rootOf(pid)) || isDead(pid); } - } + deadMemo.set(k, dead); + return dead; + }; + for (const k of tribes.keys()) if (isDead(k)) effectivelyTombstoned.add(k); - tribeIndex = { tribes, tombstoned, effectivelyTombstoned, parent, child, tipByRoot, rootByTip }; + tribeIndex = { tribes, tombstoned, effectivelyTombstoned, parent, child, tipByRoot, rootByTip, sealedInvites }; tribeIndexTs = Date.now(); return tribeIndex; }; @@ -253,8 +288,9 @@ module.exports = ({ cooler, tribeCrypto }) => { }; })(); - const normalizeTribe = (entry) => { + const normalizeTribe = (entry, rootEntry) => { const c = entry.content; + const root = rootEntry || entry; return { id: entry.id, title: c.title || '', @@ -269,9 +305,9 @@ module.exports = ({ cooler, tribeCrypto }) => { status: c.status || 'OPEN', parentTribeId: c.parentTribeId || null, mapUrl: c.mapUrl || '', - createdAt: c.createdAt, + createdAt: root.content.createdAt, updatedAt: c.updatedAt, - author: c.author || entry.author, + author: root.author, _ts: entry._ts }; }; @@ -290,28 +326,95 @@ module.exports = ({ cooler, tribeCrypto }) => { const scanOpenInviteMarkers = async () => { const msgs = await streamLog(); - const tombstoned = new Set(); - for (const m of msgs) { - const c = m.value && m.value.content; - if (!c) continue; - if (c.type === 'tribe-open-invite-tombstone' && typeof c.target === 'string') tombstoned.add(c.target); - if (c.type === 'tribe-invite-tombstone' && typeof c.target === 'string') tombstoned.add('inv:' + c.target); - } - const byRoot = new Map(); + const idx = await buildTribeIndex(); + const tombBy = new Map(); + const markTomb = (target, author) => { + if (!tombBy.has(target)) tombBy.set(target, new Set()); + tombBy.get(target).add(author); + }; + const markers = []; for (const m of msgs) { const v = m.value; const c = v && v.content; - if (!c || c.type !== 'tribe-open-invite' || c.v !== 1) continue; + if (!c) continue; + if (c.type === 'tribe-open-invite-tombstone' && typeof c.target === 'string') markTomb(c.target, v.author); + else if (c.type === 'tribe-invite-tombstone' && typeof c.target === 'string') markTomb('inv:' + c.target, v.author); + else if (c.type === 'tribe-open-invite' && c.v === 1) markers.push({ key: m.key, author: v.author, body: c, sealed: false, ts: v.timestamp || 0 }); + } + for (const e of idx.sealedInvites || []) { + if (e.body.k === 'open-invite-tombstone' && typeof e.body.target === 'string') markTomb(e.body.target, e.author); + else if (e.body.k === 'open-invite' && e.body.rootId === e.keyRoot) markers.push({ key: e.key, author: e.author, body: e.body, sealed: true, ts: e.ts || 0 }); + } + const killed = (target, allowed) => { + const by = tombBy.get(target); + return !!by && allowed.some((a) => a && by.has(a)); + }; + const byRoot = new Map(); + for (const mk of markers) { + const c = mk.body; if (typeof c.rootId !== 'string' || typeof c.code !== 'string') continue; - if (tombstoned.has(m.key)) continue; - if (c.inviteKey && tombstoned.has('inv:' + c.inviteKey)) continue; - const ts = (v && v.timestamp) || 0; + const rootEntry = idx.tribes.get(c.rootId); + if (!rootEntry || !mk.author) continue; + const tip = idx.tribes.get(idx.tipByRoot.get(c.rootId) || c.rootId) || rootEntry; + if (tip.content.isAnonymous !== false && !mk.sealed) continue; + const members = Array.isArray(tip.content.members) ? tip.content.members : []; + if (mk.author !== rootEntry.author && !members.includes(mk.author)) continue; + const allowed = [mk.author, rootEntry.author]; + if (killed(mk.key, allowed)) continue; + if (c.inviteKey && killed('inv:' + c.inviteKey, allowed)) continue; const prev = byRoot.get(c.rootId); - if (!prev || ts > prev.ts) byRoot.set(c.rootId, { code: c.code, by: c.by || v.author, markerKey: m.key, inviteKey: c.inviteKey || null, ts }); + if (!prev || mk.ts > prev.ts) byRoot.set(c.rootId, { code: c.code, by: c.by || mk.author, markerKey: mk.key, inviteKey: c.inviteKey || null, sealed: mk.sealed, ts: mk.ts }); } return byRoot; }; + const findInvite = async (code) => { + const msgs = await streamLog(); + const inviteTombstonedBy = new Map(); + const clearOpenInvites = new Set(); + for (const m of msgs) { + const v = m.value; + const c = v && v.content; + if (!c) continue; + if (c.type === 'tribe-open-invite' && typeof c.inviteKey === 'string') { clearOpenInvites.add(c.inviteKey); continue; } + if (c.type !== 'tribe-invite-tombstone' || typeof c.target !== 'string') continue; + if (!inviteTombstonedBy.has(c.target)) inviteTombstonedBy.set(c.target, new Set()); + inviteTombstonedBy.get(c.target).add(v.author); + } + let matched = null; + for (const m of msgs) { + const v = m.value; + const c = v && v.content; + if (!c || c.type !== 'tribe-invite-msg' || c.v !== 1) continue; + if (typeof c.ch !== 'string' || typeof c.s !== 'string' || typeof c.ek !== 'string') continue; + const multi = c.multi === 1 || c.multi === true; + if (!multi && inviteTombstonedBy.has(m.key)) continue; + if (tribeCrypto.hashInviteCode(code, c.s) !== c.ch) continue; + const chain = tribeCrypto.decryptChainFromInvite(c.ek, code, c.s, 3); + if (Array.isArray(chain) && chain.length) { + matched = { msgKey: m.key, codeHash: c.ch, chain, multi, inviteAuthor: v.author }; + break; + } + } + if (!matched) return null; + const fresh = matched.chain.filter(entry => entry && Array.isArray(entry.keys) && entry.keys.length && !tribeCrypto.getKeys(entry.rootId).length); + for (const entry of fresh) tribeCrypto.setKeys(entry.rootId, entry.keys, entry.gen || entry.keys.length); + tribeIndex = null; + const undo = () => { for (const entry of fresh) tribeCrypto.dropKey(entry.rootId); tribeIndex = null; }; + const rootId = matched.chain[0].rootId; + const idx = await buildTribeIndex().catch(() => null); + const entry = idx && idx.tribes.get(idx.tipByRoot.get(rootId) || rootId); + const tribe = entry && !idx.effectivelyTombstoned.has(rootId) ? normalizeTribe(entry, idx.tribes.get(rootId)) : null; + if (!tribe) { undo(); return null; } + if (matched.multi) { + const by = inviteTombstonedBy.get(matched.msgKey); + const revoked = !!by && (by.has(matched.inviteAuthor) || by.has(tribe.author)); + const exposed = tribe.isAnonymous && clearOpenInvites.has(matched.msgKey); + if (revoked || exposed) { undo(); return null; } + } + return { matched, rootId, tribe, fresh, undo }; + }; + return { type: 'tribe', @@ -359,6 +462,27 @@ module.exports = ({ cooler, tribeCrypto }) => { return result; }, + async departures(tribeId) { + const idx = await buildTribeIndex(); + const root = idx.rootByTip.get(tribeId) || tribeId; + const out = new Map(); + const rootAuthor = idx.tribes.get(root) && idx.tribes.get(root).author; + let cur = root; + let prev = null; + let g = 0; + while (cur && g++ < 100000) { + const e = idx.tribes.get(cur); + if (!e) break; + const members = Array.isArray(e.content.members) ? e.content.members : []; + const noted = rootAuthor && e.author === rootAuthor && e.content.removedSeqs ? e.content.removedSeqs : {}; + if (prev) for (const m of prev) if (!members.includes(m)) out.set(m, e.author === m ? { seq: e.seq || 0 } : (Number.isFinite(Number(noted[m])) && noted[m] !== null ? { seq: Number(noted[m]) } : { ts: e._ts || 0 })); + for (const m of members) out.delete(m); + prev = members; + cur = idx.child.get(cur); + } + return out; + }, + async getRootId(tribeId) { const idx = await buildTribeIndex(); let cur = tribeId; @@ -400,7 +524,7 @@ module.exports = ({ cooler, tribeCrypto }) => { if (idx.tombstoned.has(latestId) || idx.effectivelyTombstoned.has(latestId)) throw new Error('Tribe not found'); const entry = idx.tribes.get(latestId); if (!entry) throw new Error('Tribe not found'); - return normalizeTribe(entry); + return normalizeTribe(entry, idx.tribes.get(idx.rootByTip.get(latestId) || latestId)); }, async listAll() { @@ -411,7 +535,7 @@ module.exports = ({ cooler, tribeCrypto }) => { if (idx.effectivelyTombstoned.has(root) || idx.effectivelyTombstoned.has(tip)) continue; const entry = idx.tribes.get(tip); if (!entry) continue; - items.push(normalizeTribe(entry)); + items.push(normalizeTribe(entry, idx.tribes.get(root))); } return items; }, @@ -452,6 +576,22 @@ module.exports = ({ cooler, tribeCrypto }) => { return out; }, + async wasEverMember(userId, tribeId) { + if (!userId || !tribeId) return false; + const idx = await buildTribeIndex(); + let root = tribeId; + while (idx.parent.has(root)) root = idx.parent.get(root); + for (const entry of idx.tribes.values()) { + let r = entry.id; + while (idx.parent.has(r)) r = idx.parent.get(r); + if (r !== root) continue; + if (entry.author === userId) return true; + const members = entry.content && Array.isArray(entry.content.members) ? entry.content.members : []; + if (members.includes(userId)) return true; + } + return false; + }, + async isTribeMember(userId, tribeId) { if (!userId || !tribeId) return false; try { @@ -512,7 +652,9 @@ module.exports = ({ cooler, tribeCrypto }) => { mapUrl: updatedContent.mapUrl !== undefined ? updatedContent.mapUrl : tribe.mapUrl, createdAt: tribe.createdAt, updatedAt: now, - author: tribe.author + author: tribe.author, + ...(typeof updatedContent.joinedWith === 'string' && updatedContent.joinedWith ? { joinedWith: updatedContent.joinedWith } : {}), + ...(updatedContent.removedSeqs && typeof updatedContent.removedSeqs === 'object' ? { removedSeqs: updatedContent.removedSeqs } : {}) }; if (fields.isAnonymous) { return wrapAndPublish(rootId, { k: 'tribe', op: 'update', rootId, replaces: tipId, ...fields }); @@ -533,7 +675,15 @@ module.exports = ({ cooler, tribeCrypto }) => { async updateTribeMembers(tribeId, members) { const tribe = await this.getTribeById(tribeId); const old = tribe.members || []; - await this.updateTribeById(tribeId, { members }); + const leaving = old.filter(m => !members.includes(m)); + const removedSeqs = {}; + if (leaving.length) { + const client = await openSsb(); + for (const m of leaving) { + removedSeqs[m] = await new Promise((resolve) => pull(client.createUserStream({ id: m, reverse: true, limit: 1 }), pull.collect((err, list) => resolve(!err && list && list[0] && list[0].value ? Number(list[0].value.sequence) || 0 : 0)))); + } + } + await this.updateTribeById(tribeId, { members, ...(leaving.length ? { removedSeqs } : {}) }); const removed = old.filter(m => !members.includes(m)); const added = members.filter(m => !old.includes(m)); if (removed.length > 0) { @@ -589,7 +739,7 @@ module.exports = ({ cooler, tribeCrypto }) => { const root = await this.getRootId(tribeId).catch(() => tribeId); const map = await scanOpenInviteMarkers(); const rec = map.get(root); - return rec ? { code: rec.code, by: rec.by, markerKey: rec.markerKey, inviteKey: rec.inviteKey } : null; + return rec ? { code: rec.code, by: rec.by, markerKey: rec.markerKey, inviteKey: rec.inviteKey, sealed: rec.sealed } : null; }, async enrichOpenInvites(tribes) { @@ -627,9 +777,14 @@ module.exports = ({ cooler, tribeCrypto }) => { const invitePub = await new Promise((resolve, reject) => client.publish({ type: 'tribe-invite-msg', v: 1, multi: 1, ch: codeHash, s: salt, ek: ekChain }, (err, r) => err ? reject(err) : resolve(r)) ); - await new Promise((resolve, reject) => - client.publish({ type: 'tribe-open-invite', v: 1, rootId: targetRoot, code, inviteKey: invitePub.key, by: userId, createdAt: new Date().toISOString() }, (err, r) => err ? reject(err) : resolve(r)) - ); + const marker = { v: 1, rootId: targetRoot, code, inviteKey: invitePub.key, by: userId, createdAt: new Date().toISOString() }; + if (tribe.isAnonymous) { + await wrapAndPublish(targetRoot, { k: 'open-invite', ...marker }); + } else { + await new Promise((resolve, reject) => + client.publish({ type: 'tribe-open-invite', ...marker }, (err, r) => err ? reject(err) : resolve(r)) + ); + } tribeIndex = null; return code; }, @@ -643,56 +798,61 @@ module.exports = ({ cooler, tribeCrypto }) => { if (rec.by !== userId && !(tribe && tribe.author === userId)) { throw new Error('Not allowed to remove this invitation'); } - await new Promise((resolve, reject) => - client.publish({ type: 'tribe-open-invite-tombstone', v: 1, target: rec.markerKey, ts: new Date().toISOString() }, (err, r) => err ? reject(err) : resolve(r)) - ); + const ts = new Date().toISOString(); + if (rec.sealed) { + await wrapAndPublish(await this.getRootId(tribeId), { k: 'open-invite-tombstone', target: rec.markerKey, ts }); + } else { + await new Promise((resolve, reject) => + client.publish({ type: 'tribe-open-invite-tombstone', v: 1, target: rec.markerKey, ts }, (err, r) => err ? reject(err) : resolve(r)) + ); + } if (rec.inviteKey) await this.publishInviteTombstone(rec.inviteKey).catch(() => {}); tribeIndex = null; }, + async previewInvite(rawCode) { + const found = await findInvite(String(rawCode || '').trim()); + if (!found) return null; + found.undo(); + return { rootId: found.rootId, title: found.tribe.title, tags: found.tribe.tags }; + }, + async joinByInvite(rawCode) { const code = String(rawCode || '').trim(); if (!code) throw new Error('Invalid or expired invite code'); const client = await openSsb(); const userId = client.id; - const msgs = await streamLog(); - const inviteTombstoned = new Set(); - for (const m of msgs) { - const v = m.value; - const c = v && v.content; - if (!c || c.type !== 'tribe-invite-tombstone' || typeof c.target !== 'string') continue; - inviteTombstoned.add(c.target); - } - let matched = null; - for (const m of msgs) { - const v = m.value; - const c = v && v.content; - if (!c || c.type !== 'tribe-invite-msg' || c.v !== 1) continue; - if (typeof c.ch !== 'string' || typeof c.s !== 'string' || typeof c.ek !== 'string') continue; - if (inviteTombstoned.has(m.key)) continue; - if (tribeCrypto.hashInviteCode(code, c.s) !== c.ch) continue; - const chain = tribeCrypto.decryptChainFromInvite(c.ek, code, c.s); - if (Array.isArray(chain) && chain.length) { - matched = { msgKey: m.key, codeHash: c.ch, chain, multi: c.multi === 1 || c.multi === true }; - break; - } - } - if (!matched) throw new Error('Invalid or expired invite code'); - for (const entry of matched.chain) { - tribeCrypto.setKeys(entry.rootId, entry.keys, entry.gen || entry.keys.length); - } - tribeIndex = null; - const rootId = matched.chain[0].rootId; + const found = await findInvite(code); + if (!found) throw new Error('Invalid or expired invite code'); + const { matched, rootId, fresh, undo } = found; let tribe; try { tribe = await this.getTribeById(rootId); } catch (_) { tribe = null; } - if (!tribe) throw new Error('Tribe not found after key import'); - if (tribe.members.includes(userId)) throw new Error('Already a member of this tribe'); + if (!tribe) { undo(); throw new Error('Tribe not found'); } + if (tribe.members.includes(userId)) { undo(); throw new Error('Already a member of this tribe'); } + const lineage = new Set([rootId]); + let cursor = tribe; + for (let hops = 0; cursor && cursor.parentTribeId && hops < 8; hops++) { + let parentTribe = null; + try { parentTribe = await this.getTribeById(cursor.parentTribeId); } catch (_) { parentTribe = null; } + if (!parentTribe) break; + const parentCircle = [parentTribe.author, ...(Array.isArray(parentTribe.members) ? parentTribe.members : [])]; + if (!parentCircle.includes(cursor.author)) break; + lineage.add(parentTribe.id); + lineage.add(cursor.parentTribeId); + cursor = parentTribe; + } + for (const entry of matched.chain) { + if (!entry || !Array.isArray(entry.keys) || !entry.keys.length) continue; + if (fresh.includes(entry)) { if (!lineage.has(entry.rootId)) tribeCrypto.dropKey(entry.rootId); continue; } + if (lineage.has(entry.rootId)) tribeCrypto.mergeKeys(entry.rootId, entry.keys, entry.gen || entry.keys.length); + } + tribeIndex = null; const members = [...tribe.members, userId]; if (matched.multi) { - await this.updateTribeById(tribe.id, { members }); + await this.updateTribeById(tribe.id, { members, joinedWith: matched.codeHash }); } else { const invites = (tribe.invites || []).filter(inv => inv.codeHash !== matched.codeHash); - await this.updateTribeById(tribe.id, { members, invites }); + await this.updateTribeById(tribe.id, { members, invites, joinedWith: matched.codeHash }); await this.publishInviteTombstone(matched.msgKey).catch(() => {}); } await this.ensureFollowTribeMembers(tribe.id).catch(() => {}); @@ -728,9 +888,6 @@ module.exports = ({ cooler, tribeCrypto }) => { return; } await this.updateTribeById(tribeId, { members }); - if (members.length > 0) { - await this.rotateTribeKey(tribeId, members).catch(() => {}); - } }, async distributeTribeKey(tribeId, toMembers) { @@ -741,13 +898,16 @@ module.exports = ({ cooler, tribeCrypto }) => { const gen = tribeCrypto.getGen(rootId); if (!keys.length) return; const payload = tribeCrypto.buildKeyDistribPayload(rootId, keys, gen); - const batch = tribeCrypto.KEY_DISTRIB_BATCH; - for (let i = 0; i < toMembers.length; i += batch) { - const recps = toMembers.slice(i, i + batch); + const others = [...new Set(toMembers)].filter(m => m && m !== client.id); + const batch = tribeCrypto.KEY_DISTRIB_BATCH - 1; + let i = 0; + do { + const recps = [client.id, ...others.slice(i, i + batch)]; await new Promise((resolve, reject) => client.publish({ ...payload, recps }, (err) => err ? reject(err) : resolve()) ); - } + i += batch; + } while (i < others.length); }, async rotateTribeKey(tribeId, remainingMembers) { @@ -770,38 +930,41 @@ module.exports = ({ cooler, tribeCrypto }) => { const rootId = await this.getRootId(tribeId); if (!tribeCrypto.getKey(rootId)) return; const others = (tribe.members || []).filter(m => m !== userId); - if (!others.length) return; - const ssbKeys = require('../server/node_modules/ssb-keys'); - const config = require('../server/ssb_config'); - const msgs = await streamLog(); + const { distribs } = await readKeyDistribs(); const distributed = new Set(); - for (const m of msgs) { - if (m.value && m.value.author !== userId) continue; - const c = m.value && m.value.content; - const dec = tribeCrypto.tryUnboxKeyDistrib(c, config.keys, ssbKeys); - if (!dec || dec.rootId !== rootId) continue; + const currentGen = tribeCrypto.getGen(rootId); + const holdersOfCurrent = new Set(); + for (const { m, dec } of distribs) { + if (m.value.author !== userId) continue; + if (dec.rootId !== rootId) continue; const recps = Array.isArray(dec.recps) ? dec.recps : []; for (const r of recps) distributed.add(r); + if (Number(dec.gen) === Number(currentGen)) for (const r of recps) holdersOfCurrent.add(r); } + const departed = [...holdersOfCurrent].filter(r => r !== userId && !(tribe.members || []).includes(r)); + if (departed.length > 0) { await this.rotateTribeKey(tribeId, others).catch(() => {}); return; } + if (!others.length) return; const missing = others.filter(m => !distributed.has(m)); if (missing.length > 0) await this.distributeTribeKey(tribeId, missing).catch(() => {}); }, async processIncomingKeys() { - const client = await openSsb(); - const ssbKeys = require('../server/node_modules/ssb-keys'); - const config = require('../server/ssb_config'); - const msgs = await streamLog(); + const { msgs, distribs } = await readKeyDistribs(); const byTribe = new Map(); - for (const m of msgs) { - const c = m.value && m.value.content; - const dec = tribeCrypto.tryUnboxKeyDistrib(c, config.keys, ssbKeys); - if (!dec) continue; + const authorOf = new Map(); + for (const m of msgs) if (m && m.key && m.value) authorOf.set(m.key, m.value.author); + for (const { m, dec } of distribs) { const list = byTribe.get(dec.rootId) || []; - list.push({ generation: dec.gen || dec.keys.length, keys: dec.keys, ts: m.value.timestamp }); + list.push({ generation: dec.gen || dec.keys.length, keys: dec.keys, ts: m.value.timestamp, author: m.value.author }); byTribe.set(dec.rootId, list); } - for (const [rootId, entries] of byTribe.entries()) { + for (const [rootId, all] of byTribe.entries()) { + const owner = authorOf.get(rootId); + if (!owner) continue; + let members = []; + try { const t = await this.getTribeById(rootId); members = Array.isArray(t && t.members) ? t.members : []; } catch (_) {} + const allowed = new Set([owner, ...members]); + const entries = all.filter(e => allowed.has(e.author)); entries.sort((a, b) => b.generation - a.generation); const top = entries[0]; if (top && Array.isArray(top.keys) && top.keys.length) { @@ -929,7 +1092,7 @@ module.exports = ({ cooler, tribeCrypto }) => { const all = tribeCrypto.getAllRootIds(); let removed = 0; for (const rid of all) { - if (!idx.tribes.has(rid) || idx.effectivelyTombstoned.has(rid)) { + if (idx.effectivelyTombstoned.has(rid)) { try { tribeCrypto.dropKey(rid); removed++; } catch (_) {} } } diff --git a/src/models/typed_log.js b/src/models/typed_log.js new file mode 100644 index 0000000..e7d4d60 --- /dev/null +++ b/src/models/typed_log.js @@ -0,0 +1,285 @@ +const pull = require('../server/node_modules/pull-stream'); +const { AsyncLocalStorage } = require('async_hooks'); + +const requestScope = new AsyncLocalStorage(); +const noteCapped = (limit, size) => { + if (!limit || !(size >= limit)) return; + const store = requestScope.getStore(); + if (store) { store.capped = true; store.limit = limit; } +}; + +const TAIL_PROBE = 50; +const LOG_TAIL_PROBE = 20; + +const collectStream = (stream) => + new Promise((resolve, reject) => { + pull(stream, pull.collect((err, msgs) => (err ? reject(err) : resolve(msgs)))); + }); + +const isDeletion = (m) => { + const c = m && m.value && m.value.content; + return !!c && typeof c === 'object' && c.type === 'tombstone'; +}; + +const readContentWindow = (ssb, limit) => { + if (!limit) return collectStream(ssb.createLogStream({ reverse: true })); + let kept = 0; + return collectStream(pull( + ssb.createLogStream({ reverse: true }), + pull.take((m) => { if (!isDeletion(m)) kept++; return kept <= limit; }) + )); +}; + +const caches = new WeakMap(); + +const cacheFor = (ssb) => { + let c = caches.get(ssb); + if (!c) { + c = { types: new Map(), window: null, results: new Map() }; + caches.set(ssb, c); + } + return c; +}; + +const insert = (entry, msgs) => { + for (let i = msgs.length - 1; i >= 0; i--) { + const m = msgs[i]; + if (m && m.key && !entry.byKey.has(m.key)) entry.byKey.set(m.key, m); + } +}; + +const UNCAPPED_TYPES = new Set(['tombstone']); +const syncType = async (ssb, cache, type, requestedLimit) => { + const limit = UNCAPPED_TYPES.has(type) ? 0 : requestedLimit; + let entry = cache.types.get(type); + if (!entry) { + entry = { byKey: new Map(), warming: null, warm: false }; + cache.types.set(type, entry); + entry.warming = collectStream(ssb.messagesByType(limit ? { type, reverse: true, limit } : { type, reverse: true })) + .then((msgs) => { insert(entry, msgs); entry.warm = true; entry.capped = !!(limit && msgs.length >= limit); }) + .catch(() => { cache.types.delete(type); }); + await entry.warming; + return entry; + } + await entry.warming; + if (!entry.warm) return entry; + const tail = await collectStream(ssb.messagesByType({ type, reverse: true, limit: TAIL_PROBE })); + const fresh = tail.filter((m) => m && m.key && !entry.byKey.has(m.key)); + if (fresh.length === tail.length && tail.length === TAIL_PROBE) { + const all = await collectStream(ssb.messagesByType(limit ? { type, reverse: true, limit } : { type, reverse: true })); + insert(entry, all); + entry.capped = !!(limit && all.length >= limit); + } else { + insert(entry, fresh); + } + return entry; +}; + +const syncWindow = async (ssb, cache, limit) => { + let entry = cache.window; + if (!entry) { + entry = { byKey: new Map(), warming: null, warm: false }; + cache.window = entry; + entry.warming = readContentWindow(ssb, limit) + .then((msgs) => { insert(entry, msgs); entry.warm = true; }) + .catch(() => { cache.window = null; }); + await entry.warming; + return entry; + } + await entry.warming; + if (!entry.warm) return entry; + const tail = await collectStream(ssb.createLogStream({ reverse: true, limit: TAIL_PROBE })); + const fresh = tail.filter((m) => m && m.key && !entry.byKey.has(m.key)); + if (fresh.length === tail.length && tail.length === TAIL_PROBE) { + const all = await readContentWindow(ssb, limit); + insert(entry, all); + } else { + insert(entry, fresh); + } + return entry; +}; + +const syncPrivate = async (ssb, cache) => { + if (!ssb.private || typeof ssb.private.read !== 'function') return null; + let entry = cache.private; + if (!entry) { + entry = { byKey: new Map(), warming: null, warm: false }; + cache.private = entry; + entry.warming = collectStream(ssb.private.read({ reverse: true })) + .then((msgs) => { insert(entry, msgs); entry.warm = true; }) + .catch(() => { cache.private = null; }); + await entry.warming; + return entry; + } + await entry.warming; + if (!entry.warm) return entry; + const tail = await collectStream(ssb.private.read({ reverse: true, limit: TAIL_PROBE })); + const fresh = tail.filter((m) => m && m.key && !entry.byKey.has(m.key)); + if (fresh.length === tail.length && tail.length === TAIL_PROBE) { + insert(entry, await collectStream(ssb.private.read({ reverse: true }))); + } else { + insert(entry, fresh); + } + return entry; +}; + +const logTailOf = async (ssb, n) => { + if (ssb.activity && typeof ssb.activity.tail === 'function') { + const store = requestScope.getStore(); + const fresh = !(store && store.method === 'GET'); + const fast = await new Promise((resolve) => { + try { ssb.activity.tail(n, { fresh }, (err, arr) => resolve(err ? null : arr)); } catch (_) { resolve(null); } + }); + if (Array.isArray(fast)) return fast; + } + return collectStream(ssb.createLogStream({ reverse: true, limit: n })); +}; + +const readTyped = async (ssbClient, types, opts = {}) => { + const limit = opts.limit; + if (typeof ssbClient.messagesByType !== 'function') { + const all = await collectStream(ssbClient.createLogStream(limit ? { limit } : {})); + noteCapped(limit, all.length); + return all; + } + const cache = cacheFor(ssbClient); + const wanted = new Set(types); + + const logTail = (await logTailOf(ssbClient, LOG_TAIL_PROBE)).reverse(); + const tipKey = logTail.length ? logTail[logTail.length - 1].key : null; + const inTail = new Set(logTail.map((m) => m && m.key)); + const upToDate = (e) => !!e && e.warm && e.tip === tipKey; + const caughtUp = (e) => !!e && e.warm && !!e.tip && (e.tip === tipKey || inTail.has(e.tip)); + const allWarm = types.every((type) => upToDate(cache.types.get(type))) + && (!opts.withWindow || upToDate(cache.window)); + + const entries = await Promise.all(types.map((type) => { + const e = cache.types.get(type); + return caughtUp(e) ? e : syncType(ssbClient, cache, type, limit); + })); + const windowEntry = opts.withWindow + ? (caughtUp(cache.window) ? cache.window : await syncWindow(ssbClient, cache, limit)) + : null; + const withPrivate = opts.withPrivate === true; + const resultKey = `${types.join(',')}|${limit || 0}|${opts.withWindow ? 1 : 0}|${withPrivate ? 1 : 0}`; + if (allWarm && (!withPrivate || !wanted.size || upToDate(cache.private))) { + const hit = cache.results.get(resultKey); + if (hit && hit.tip === tipKey) { + if (hit.capped) noteCapped(limit, limit); + return hit.list.slice(); + } + } + const privateEntry = withPrivate && wanted.size + ? (!upToDate(cache.private) ? await syncPrivate(ssbClient, cache) : cache.private) + : null; + for (const e of [...entries, windowEntry, privateEntry]) if (e && e.warm) e.tip = tipKey; + + for (const m of logTail) { + if (!m || !m.key || !m.value) continue; + const t = m.value.content && m.value.content.type; + if (typeof t === 'string' && wanted.has(t)) { + const entry = cache.types.get(t); + if (entry && entry.warm && !entry.byKey.has(m.key)) entry.byKey.set(m.key, m); + } + if (windowEntry && windowEntry.warm && !windowEntry.byKey.has(m.key)) windowEntry.byKey.set(m.key, m); + } + + const union = new Map(); + let capped = false; + for (const entry of entries) { + if (!entry) continue; + if (entry.capped) { capped = true; noteCapped(limit, limit); } + for (const [k, m] of entry.byKey) if (!union.has(k)) union.set(k, m); + } + if (windowEntry) { + for (const [k, m] of windowEntry.byKey) if (!union.has(k)) union.set(k, m); + } + if (privateEntry && privateEntry.warm) { + for (const [k, m] of privateEntry.byKey) { + const t = m && m.value && m.value.content && m.value.content.type; + if (typeof t === 'string' && wanted.has(t)) union.set(k, m); + } + } + const sorted = Array.from(union.values()).sort((a, b) => { + const at = (a.value && a.value.timestamp) || 0; + const bt = (b.value && b.value.timestamp) || 0; + if (at !== bt) return at - bt; + const ar = a.timestamp || 0; + const br = b.timestamp || 0; + if (ar !== br) return ar - br; + if (a.value && b.value && a.value.author === b.value.author) return (a.value.sequence || 0) - (b.value.sequence || 0); + return 0; + }); + const complete = entries.every((e) => e && e.warm) + && (!windowEntry === !opts.withWindow) && (!windowEntry || windowEntry.warm) + && (!(withPrivate && wanted.size) || (privateEntry && privateEntry.warm)); + if (tipKey && complete) cache.results.set(resultKey, { tip: tipKey, capped, list: sorted }); + return sorted.slice(); +}; + +const NON_MESSAGE_LITERALS = new Set([ + 'hidden', 'submit', 'text', 'file', 'number', 'checkbox', 'radio', 'password', 'date', 'datetime-local', 'time', 'url', 'email', 'search', 'button', 'reset', + 'png', 'jpeg', 'jpg', 'gif', 'svg', 'webp', 'pdf', 'zip', 'json', 'html', 'error', 'meta', 'msg', 'blob', 'peer', 'inhabitant', 'chatThread', 'taskAssignment' +]); +const CORE_TYPES = ['post', 'about', 'contact', 'vote', 'pub', 'tombstone', 'file', 'textChunk']; + +const discoverContentTypes = () => { + const fs = require('fs'); + const path = require('path'); + const found = new Set(CORE_TYPES); + const sources = []; + try { + for (const f of fs.readdirSync(__dirname)) if (f.endsWith('.js') && f !== 'typed_log.js') sources.push(path.join(__dirname, f)); + sources.push(path.join(__dirname, '..', 'backend', 'backend.js')); + } catch (_) {} + for (const file of sources) { + let src = ''; + try { src = fs.readFileSync(file, 'utf8'); } catch (_) { continue; } + for (const m of src.matchAll(/\btype:\s*['"]([A-Za-z][\w-]*)['"]/g)) { + if (!NON_MESSAGE_LITERALS.has(m[1])) found.add(m[1]); + } + for (const m of src.matchAll(/\b(?:const|let)\s+(?:[A-Z][A-Z0-9_]*_)?TYPE\s*=\s*['"]([A-Za-z][\w-]*)['"]/g)) { + if (!NON_MESSAGE_LITERALS.has(m[1])) found.add(m[1]); + } + } + return Array.from(found).sort(); +}; + +const CONTENT_TYPES = discoverContentTypes(); + +const logTip = async (ssb) => { + const tail = await logTailOf(ssb, 1); + return tail[0] && tail[0].key ? tail[0].key : null; +}; + +const activityCache = new WeakMap(); +const ACTIVITY_TTL_MS = 5000; +const authorActivity = (ssb) => { + if (!ssb || !ssb.activity || typeof ssb.activity.latest !== 'function') return Promise.resolve(null); + const hit = activityCache.get(ssb); + if (hit && Date.now() - hit.at < ACTIVITY_TTL_MS) return hit.pending; + const store = requestScope.getStore(); + const fresh = !(store && store.method === 'GET'); + const pending = new Promise((resolve) => { + try { ssb.activity.latest(null, { fresh }, (err, all) => resolve(err || !all ? null : new Map(Object.entries(all)))); } catch (_) { resolve(null); } + }); + activityCache.set(ssb, { at: Date.now(), pending }); + return pending; +}; + +const memoIndex = (name, messages, build) => { + const store = requestScope.getStore(); + if (!store) return build(messages); + const list = Array.isArray(messages) ? messages : []; + const first = list[0]; + const last = list[list.length - 1]; + const sig = `${list.length}|${first && first.key}|${last && last.key}`; + if (!store.indexMemo) store.indexMemo = new Map(); + const hit = store.indexMemo.get(name); + if (hit && hit.sig === sig) return hit.idx; + const idx = build(messages); + store.indexMemo.set(name, { sig, idx }); + return idx; +}; + +module.exports = { readTyped, collectStream, readContentWindow, CONTENT_TYPES, discoverContentTypes, requestScope, memoIndex, logTip, authorActivity }; diff --git a/src/models/videos_model.js b/src/models/videos_model.js index 24b8a99..d9bc7a3 100644 --- a/src/models/videos_model.js +++ b/src/models/videos_model.js @@ -1,7 +1,8 @@ const pull = require("../server/node_modules/pull-stream"); const { getConfig } = require("../configs/config-manager.js"); const { buildValidatedTombstoneSet } = require('./tombstone_validator'); -const { dedupeBy, norm } = require('../backend/dedupe'); +const { dedupeBy, norm, versionGroups } = require('../backend/dedupe'); +const { readTyped, memoIndex } = require('./typed_log'); const categories = require("../backend/opinion_categories"); const logLimit = getConfig().ssbLogStream?.limit || 1000; @@ -31,13 +32,9 @@ module.exports = ({ cooler }) => { return ssb; }; - const getAllMessages = async (ssbClient) => - new Promise((resolve, reject) => { - pull( - ssbClient.createLogStream({ limit: logLimit }), - pull.collect((err, msgs) => (err ? reject(err) : resolve(msgs))) - ); - }); + const VIDEO_TYPES = ["video", "videoOpinion", "tombstone"]; + + const getAllMessages = async (ssbClient) => readTyped(ssbClient, VIDEO_TYPES, { limit: logLimit }); const getMsg = async (ssbClient, key) => new Promise((resolve, reject) => { @@ -47,9 +44,6 @@ module.exports = ({ cooler }) => { const buildIndex = (messages) => { const tomb = buildValidatedTombstoneSet(messages); const nodes = new Map(); - const parent = new Map(); - const child = new Map(); - const strictChild = new Map(); const opinionMsgs = []; for (const m of messages) { @@ -65,41 +59,16 @@ module.exports = ({ cooler }) => { const ts = v.timestamp || m.timestamp || 0; let sizeBytes = 0; try { sizeBytes = Buffer.byteLength(JSON.stringify(v), "utf8"); } catch (_) { sizeBytes = 0; } - nodes.set(k, { key: k, ts, c, sizeBytes, author: v.author }); - - if (c.replaces) { - parent.set(k, c.replaces); - child.set(c.replaces, k); - } + nodes.set(k, { key: k, ts, seq: v.sequence || 0, c, sizeBytes, author: v.author }); } - for (const [k, node] of nodes) { - const t = node.c.replaces; - if (t) { const orig = nodes.get(t); if (orig && orig.author === node.author) strictChild.set(t, k); } - } - - const rootOf = (id) => { - let cur = id, g = 0; - while (parent.has(cur) && nodes.has(parent.get(cur)) && g++ < 100000) cur = parent.get(cur); - return cur; - }; - - const contentTipOf = (root) => { - let cur = root, g = 0; - while (strictChild.has(cur) && g++ < 100000) cur = strictChild.get(cur); - const n = nodes.get(cur), rn = nodes.get(root); - return (n && rn && n.author === rn.author) ? cur : root; - }; - - const roots = new Set(); - for (const id of nodes.keys()) roots.add(rootOf(id)); + const vg = versionGroups([...nodes.values()].map(n => ({ key: n.key, author: n.author, seq: n.seq, ts: n.ts, replaces: n.c.replaces || null, claimedAuthor: typeof n.c.author === "string" ? n.c.author : null }))); + const rootOf = vg.rootOf; + const contentTipOf = vg.tipOf; const opinionsByRoot = new Map(); for (const op of opinionMsgs) { if (!nodes.has(op.target)) continue; const r = rootOf(op.target); if (!opinionsByRoot.has(r)) opinionsByRoot.set(r, []); opinionsByRoot.get(r).push(op); } - const forward = new Map(); - for (const [newId, oldId] of parent.entries()) forward.set(oldId, newId); - const resolveGroup = (root) => { const contentTip = contentTipOf(root); const contentNode = nodes.get(contentTip) || nodes.get(root); @@ -116,9 +85,9 @@ module.exports = ({ cooler }) => { }; const tipByRoot = new Map(); - for (const r of roots) tipByRoot.set(r, contentTipOf(r)); + for (const r of vg.roots) tipByRoot.set(r, contentTipOf(r)); - return { tomb, nodes, parent, child, rootOf, tipByRoot, forward, resolveGroup }; + return { tomb, nodes, rootOf, tipByRoot, resolveGroup }; }; const buildVideo = (node, rootId, viewerId, agg) => { @@ -132,9 +101,10 @@ module.exports = ({ cooler }) => { createdAt: c.createdAt || new Date(node.ts).toISOString(), updatedAt: c.updatedAt || null, tags: safeArr(c.tags), - author: c.author, + author: node.author, title: c.title || "", description: c.description || "", + license: c.license || "", mapUrl: c.mapUrl || "", opinions, opinions_inhabitants: voters, @@ -161,16 +131,12 @@ module.exports = ({ cooler }) => { const messages = await getAllMessages(ssbClient); const idx = buildIndex(messages); - let tip = id; - while (idx.forward.has(tip)) tip = idx.forward.get(tip); - if (idx.tomb.has(tip)) throw new Error("Video not found"); - - let root = tip; - while (idx.parent.has(root)) root = idx.parent.get(root); + const root = idx.rootOf(id); + if (idx.tomb.has(idx.tipByRoot.get(root) || id)) throw new Error("Video not found"); return root; }, - async createVideo(blobMarkdown, tagsRaw, title, description, mapUrl) { + async createVideo(blobMarkdown, tagsRaw, title, description, mapUrl, license) { const ssbClient = await openSsb(); const blobId = parseBlobId(blobMarkdown); const tags = normalizeTags(tagsRaw) || []; @@ -187,7 +153,8 @@ module.exports = ({ cooler }) => { description: description || "", mapUrl: mapUrl || "", opinions: {}, - opinions_inhabitants: [] + opinions_inhabitants: [], + ...(license ? { license } : {}) }; return new Promise((resolve, reject) => { @@ -195,7 +162,7 @@ module.exports = ({ cooler }) => { }); }, - async updateVideoById(id, blobMarkdown, tagsRaw, title, description, mapUrl) { + async updateVideoById(id, blobMarkdown, tagsRaw, title, description, mapUrl, license) { const ssbClient = await openSsb(); const userId = ssbClient.id; const tipId = await this.resolveCurrentId(id); @@ -219,7 +186,8 @@ module.exports = ({ cooler }) => { description: description !== undefined ? description || "" : oldMsg.content.description || "", mapUrl: mapUrl !== undefined ? mapUrl || "" : oldMsg.content.mapUrl || "", createdAt: oldMsg.content.createdAt, - updatedAt: now + updatedAt: now, + ...(license !== undefined ? { license } : {}) }; const tombstone = { type: "tombstone", target: tipId, deletedAt: now, author: userId }; @@ -304,10 +272,10 @@ module.exports = ({ cooler }) => { const ssbClient = await openSsb(); const viewer = viewerId || ssbClient.id; const messages = await getAllMessages(ssbClient); - const idx = buildIndex(messages); + const idx = memoIndex('videos', messages, buildIndex); const rootId = idx.rootOf(id); const agg = idx.resolveGroup(rootId); - if (!agg.contentNode || idx.tomb.has(agg.contentTip)) throw new Error("Video not found"); + if (!idx.tipByRoot.has(rootId) || !agg.contentNode || idx.tomb.has(agg.contentTip)) throw new Error("Video not found"); return buildVideo(agg.contentNode, rootId, viewer, agg); }, diff --git a/src/models/viewer_filters.js b/src/models/viewer_filters.js index cef023e..d4dc5b7 100644 --- a/src/models/viewer_filters.js +++ b/src/models/viewer_filters.js @@ -3,7 +3,7 @@ const path = require('path'); const { getConfig } = require('../configs/config-manager.js'); const COOLDOWN_MS = 5 * 60 * 1000; -const statePath = path.join(__dirname, '../configs/follow_state.json'); +const statePath = require('../configs/state-manager').statePath('follow_state.json'); const readJson = (p, fallback) => { try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch (e) { return fallback; } @@ -21,9 +21,175 @@ const loadState = () => { }; const saveState = (s) => writeJson(statePath, s); +const hiddenPath = require('../configs/state-manager').statePath('viewer_hidden.json'); +let hiddenState = null; +const loadHidden = () => { + if (!hiddenState) { + const raw = readJson(hiddenPath, {}); + const muted = Array.isArray(raw && raw.muted) ? raw.muted : []; + const hidden = Array.isArray(raw && raw.hidden) ? raw.hidden : []; + hiddenState = { + muted: muted.map(x => (typeof x === 'string' ? { id: x, at: 0 } : x)).filter(x => x && typeof x.id === 'string'), + hidden: hidden.filter(x => x && typeof x.id === 'string') + }; + } + return hiddenState; +}; +const saveHidden = () => writeJson(hiddenPath, loadHidden()); + +const mutedList = () => loadHidden().muted.slice(); +const mutedIds = () => new Set(loadHidden().muted.map(x => x.id)); +const isMuted = (id) => !!id && loadHidden().muted.some(x => x.id === id); +const mute = (id) => { + const s = loadHidden(); + if (!id || s.muted.some(x => x.id === id)) return false; + s.muted.push({ id, at: Date.now() }); + saveHidden(); + return true; +}; +const unmute = (id) => { + const s = loadHidden(); + const before = s.muted.length; + s.muted = s.muted.filter(x => x.id !== id); + if (s.muted.length !== before) saveHidden(); + return s.muted.length !== before; +}; + +const hiddenList = () => loadHidden().hidden.slice(); +const hiddenIds = () => new Set(loadHidden().hidden.map(x => x.id)); +const isHidden = (id) => !!id && loadHidden().hidden.some(x => x.id === id); +const hideContent = ({ id, href = '', label = '' } = {}) => { + const s = loadHidden(); + if (!id || s.hidden.some(x => x.id === id)) return false; + s.hidden.push({ id, href: String(href || '').slice(0, 300), label: String(label || '').slice(0, 120), at: Date.now() }); + saveHidden(); + return true; +}; +const unhideContent = (id) => { + const s = loadHidden(); + const before = s.hidden.length; + s.hidden = s.hidden.filter(x => x.id !== id); + if (s.hidden.length !== before) saveHidden(); + return s.hidden.length !== before; +}; + +const idsOfItem = (item) => { + if (!item || typeof item !== 'object') return []; + return [item.id, item.key, item.rootId, item.favId].filter(v => typeof v === 'string'); +}; +const isItemHidden = (item, authorOf, { authors, ids } = {}) => { + if (!item) return false; + const a = typeof authorOf === 'function' ? authorOf(item) : null; + if (a && authors && authors.has(String(a))) return true; + if (ids && ids.size && idsOfItem(item).some(id => ids.has(id))) return true; + return false; +}; + +const lanPeersPath = require('../configs/state-manager').statePath('lan-peers.json'); +const FEED_ID = /^@[A-Za-z0-9+/]{43}=\.ed25519$/; +const LAN_SEEN_STEP_MS = 60 * 1000; +let lanSeen = null; + +const canonicalFeedId = (key) => { + let k = String(key || '').trim(); + if (!k) return null; + if (!k.startsWith('@')) k = '@' + k; + if (!k.endsWith('.ed25519')) k += '.ed25519'; + return FEED_ID.test(k) ? k : null; +}; + +const isPrivateHost = (host) => { + const h = String(host || '').replace(/^\[|\]$/g, '').replace(/^::ffff:/i, '').toLowerCase(); + if (!h) return false; + if (/^(10|127)\./.test(h) || /^192\.168\./.test(h) || /^169\.254\./.test(h)) return true; + const m = h.match(/^172\.(\d+)\./); + if (m && Number(m[1]) >= 16 && Number(m[1]) <= 31) return true; + return h === '::1' || /^f[cd][0-9a-f]{0,2}:/.test(h) || /^fe[89ab][0-9a-f]?:/.test(h); +}; + +const hostOfAddress = (address) => { + const net = String(address || '').split('~')[0]; + if (!net.startsWith('net:')) return ''; + const hp = net.slice(4); + const i = hp.lastIndexOf(':'); + return i > 0 ? hp.slice(0, i) : hp; +}; + +const lanKeysFromConn = (entries) => { + const out = []; + for (const e of entries || []) { + if (!Array.isArray(e) || !e[1]) continue; + const [address, data] = e; + const key = canonicalFeedId(data.key); + if (!key) continue; + if (data.type === 'lan' || data.inferredType === 'lan' || isPrivateHost(hostOfAddress(address))) out.push(key); + } + return out; +}; + +const lanPeersSeen = () => { + if (!lanSeen) { + const list = readJson(lanPeersPath, []); + lanSeen = new Map(); + for (const item of (Array.isArray(list) ? list : [])) { + const id = canonicalFeedId(typeof item === 'string' ? item : item && item.key); + if (id) lanSeen.set(id, Number(item && item.seenAt) || 0); + } + } + return lanSeen; +}; + +const lanPeers = () => new Set(lanPeersSeen().keys()); + +const rememberLanPeers = (keys, now = Date.now()) => { + const seen = lanPeersSeen(); + let changed = false; + for (const k of keys || []) { + const id = canonicalFeedId(k); + if (!id) continue; + const prev = seen.get(id); + if (prev === undefined || now - prev >= LAN_SEEN_STEP_MS) { seen.set(id, now); changed = true; } + } + if (changed) writeJson(lanPeersPath, [...seen].map(([key, seenAt]) => ({ key, seenAt }))); + return changed; +}; + +const forgetLanPeers = (keys) => { + const seen = lanPeersSeen(); + let changed = false; + for (const k of keys || []) { + const id = canonicalFeedId(k); + if (id && seen.delete(id)) changed = true; + } + if (changed) writeJson(lanPeersPath, [...seen].map(([key, seenAt]) => ({ key, seenAt }))); + return changed; +}; + +const MUTUAL_CONCURRENCY = 16; +const filterByWish = async (items, { wish, viewer, authorOf: author, isOwn, isMutual, lan } = {}) => { + if (!Array.isArray(items)) return items; + const own = (it) => !!(isOwn && isOwn(it, viewer)); + if (wish === 'local') return items.filter(own); + if (wish === 'only-lan') { + const near = lan || new Set(); + return items.filter(it => { const a = author(it); return !a || a === viewer || own(it) || near.has(a); }); + } + if (wish === 'mutuals' && typeof isMutual === 'function') { + const authors = [...new Set(items.map(author).filter(a => a && a !== viewer))]; + const verdict = new Map(); + let next = 0; + await Promise.all(Array.from({ length: Math.min(MUTUAL_CONCURRENCY, authors.length) }, async () => { + while (next < authors.length) { const a = authors[next++]; verdict.set(a, !!(await isMutual(a))); } + })); + return items.filter(it => { const a = author(it); return !a || a === viewer || verdict.get(a); }); + } + return items; +}; + const wishMutualsOnly = () => getConfig().wish === 'mutuals'; const pmMutualsOnly = () => getConfig().pmVisibility === 'mutuals'; -const isFrictionActive = () => wishMutualsOnly() || pmMutualsOnly(); +const pmFollowingOnly = () => getConfig().pmVisibility === 'following'; +const isFrictionActive = () => wishMutualsOnly() || pmMutualsOnly() || pmFollowingOnly(); const listPending = () => loadState().pending; const enqueuePending = (followerId, extra = {}) => { @@ -102,8 +268,13 @@ const applyMutualSupportFilter = async (items, viewerId, friendModel) => { }; const canSendPmTo = async (viewerId, recipientId, friendModel) => { - if (!pmMutualsOnly()) return { allowed: true }; if (viewerId === recipientId) return { allowed: true }; + if (pmFollowingOnly()) { + let rel = null; + try { rel = friendModel && typeof friendModel.getRelationship === 'function' ? await friendModel.getRelationship(recipientId) : null; } catch (_) { rel = null; } + return rel && rel.following ? { allowed: true } : { allowed: false, reason: 'not-following' }; + } + if (!pmMutualsOnly()) return { allowed: true }; const isMutual = makeMutualCache(friendModel); if (await isMutual(recipientId)) return { allowed: true }; return { allowed: false, reason: 'non-mutual' }; @@ -113,6 +284,7 @@ module.exports = { COOLDOWN_MS, wishMutualsOnly, pmMutualsOnly, + pmFollowingOnly, isFrictionActive, listPending, enqueuePending, @@ -127,4 +299,22 @@ module.exports = { applyMutualSupportFilter, canSendPmTo, authorOf, + isPrivateHost, + lanKeysFromConn, + lanPeers, + lanPeersSeen, + rememberLanPeers, + forgetLanPeers, + filterByWish, + mutedList, + mutedIds, + isMuted, + mute, + unmute, + hiddenList, + hiddenIds, + isHidden, + hideContent, + unhideContent, + isItemHidden, }; diff --git a/src/models/votes_model.js b/src/models/votes_model.js index 7802606..96deddb 100644 --- a/src/models/votes_model.js +++ b/src/models/votes_model.js @@ -5,9 +5,18 @@ const { getConfig } = require('../configs/config-manager.js'); const { dedupeBy, norm } = require('../backend/dedupe'); const { buildVoteTally } = require('../backend/vote_tally'); const categories = require('../backend/opinion_categories'); +const { readTyped, memoIndex } = require('./typed_log'); const logLimit = getConfig().ssbLogStream?.limit || 1000; const MIN_VOTE_DAYS = 7; +const INSTITUTIONAL_VOTE_TAG = /^(gov|govMethod|courtsCase|courtsMethod):/; +const isVoteLocked = (v) => !!v && ( + (parseInt(v.totalVotes || 0, 10) || 0) > 0 + || Object.values(v.opinions || {}).some(n => (Number(n) || 0) > 0) + || (Array.isArray(v.tags) && v.tags.some(t => INSTITUTIONAL_VOTE_TAG.test(String(t)))) +); +const lockedError = () => Object.assign(new Error('Vote locked'), { code: 'VOTE_LOCKED' }); + module.exports = ({ cooler }) => { let ssb; const openSsb = async () => { @@ -17,13 +26,10 @@ module.exports = ({ cooler }) => { const TYPE = 'votes'; + const VOTES_TYPES = [TYPE, 'votesVote', 'votesOpinion', 'tombstone']; + async function getAllMessages(ssbClient) { - return new Promise((resolve, reject) => { - pull( - ssbClient.createLogStream({ limit: logLimit }), - pull.collect((err, results) => (err ? reject(err) : resolve(results))) - ); - }); + return readTyped(ssbClient, VOTES_TYPES, { limit: logLimit }); } function buildIndex(messages) { @@ -170,12 +176,18 @@ module.exports = ({ cooler }) => { } return { + MIN_VOTE_DAYS, + async createVote(question, deadline, options = ['YES', 'NO', 'ABSTENTION', 'CONFUSED', 'FOLLOW_MAJORITY', 'NOT_INTERESTED'], tagsRaw = []) { const ssbClient = await openSsb(); const userId = ssbClient.id; const parsedDeadline = moment(deadline, moment.ISO_8601, true); if (!parsedDeadline.isValid()) throw new Error('Invalid deadline'); - if (parsedDeadline.isBefore(moment().add(MIN_VOTE_DAYS, 'days').subtract(2, 'minutes'))) throw new Error(`Deadline must be at least ${MIN_VOTE_DAYS} days from now`); + if (parsedDeadline.isBefore(moment().add(MIN_VOTE_DAYS, 'days').subtract(2, 'minutes'))) { + const err = new Error(`Deadline must be at least ${MIN_VOTE_DAYS} days from now`); + err.code = 'VOTE_DEADLINE_MIN'; + throw err; + } const tags = Array.isArray(tagsRaw) ? tagsRaw.filter(Boolean) @@ -214,6 +226,7 @@ module.exports = ({ cooler }) => { ssbClient.get(tipId, (err, msg) => (err || !msg ? rej(new Error('Vote not found')) : res(msg))) ); if (!vote.content || vote.content.createdBy !== userId) throw new Error('Not the author'); + if (isVoteLocked(vote.content) || isVoteLocked(await this.getVoteById(id).catch(() => null))) throw lockedError(); const tombstone = { type: 'tombstone', target: tipId, @@ -240,13 +253,17 @@ module.exports = ({ cooler }) => { if (c.createdBy !== userId) throw new Error('Not the author'); const agg = await this.getVoteById(id); const aggTotalVotes = agg ? (parseInt(agg.totalVotes || 0, 10) || 0) : 0; - if (agg && Object.keys(agg.opinions || {}).some(k => (agg.opinions[k] || 0) > 0)) throw new Error('Cannot edit vote after it has received opinions.'); + if (isVoteLocked(c) || isVoteLocked(agg)) throw lockedError(); let newDeadline = c.deadline; if (deadline != null && deadline !== '') { const parsed = moment(deadline, moment.ISO_8601, true); if (!parsed.isValid()) throw new Error('Invalid deadline'); - if (parsed.isBefore(moment().add(MIN_VOTE_DAYS, 'days').subtract(2, 'minutes'))) throw new Error(`Deadline must be at least ${MIN_VOTE_DAYS} days from now`); + if (parsed.isBefore(moment().add(MIN_VOTE_DAYS, 'days').subtract(2, 'minutes'))) { + const err = new Error(`Deadline must be at least ${MIN_VOTE_DAYS} days from now`); + err.code = 'VOTE_DEADLINE_MIN'; + throw err; + } newDeadline = parsed.toISOString(); } @@ -314,7 +331,7 @@ module.exports = ({ cooler }) => { async getVoteById(id) { const ssbClient = await openSsb(); const messages = await getAllMessages(ssbClient); - const index = buildIndex(messages); + const index = memoIndex('votes', messages, buildIndex); const activeList = computeActiveVotes(index); const byId = new Map(activeList.map(v => [v.id, v])); @@ -329,19 +346,7 @@ module.exports = ({ cooler }) => { return candidate; } - const msg = await new Promise((res, rej) => - ssbClient.get(id, (err, vote) => (err || !vote ? rej(new Error('Vote not found')) : res(vote))) - ); - - const content = msg.content || {}; - const status = statusFromContent(content, moment()); - - return { - id, - latestId: id, - ...content, - status - }; + throw new Error('Vote not found'); }, async listAll(filter = 'all') { @@ -353,6 +358,8 @@ module.exports = ({ cooler }) => { if (filter === 'mine') { list = list.filter(v => v.createdBy === userId); + } else if (filter === 'recent') { + list = list.filter(v => (Date.parse(v.createdAt || '') || 0) >= Date.now() - 86400000); } else if (filter === 'open') { list = list.filter(v => v.status === 'OPEN'); } else if (filter === 'closed') { @@ -379,4 +386,4 @@ module.exports = ({ cooler }) => { } }; }; - +module.exports.isVoteLocked = isVoteLocked; diff --git a/src/models/wallet_model.js b/src/models/wallet_model.js index cafa6cf..91ad69f 100644 --- a/src/models/wallet_model.js +++ b/src/models/wallet_model.js @@ -1,16 +1,19 @@ -const { - RequestManager, - HTTPTransport, - Client -} = require("../server/node_modules/@open-rpc/client-js"); +const RPC_TIMEOUT_MS = 30000; async function makeClient(url, user, pass) { - const headers = {}; + const headers = { 'Content-Type': 'application/json' }; if (user !== undefined || pass !== undefined) { headers['Authorization'] = 'Basic ' + Buffer.from(`${user || ''}:${pass || ''}`).toString('base64'); } - const transport = new HTTPTransport(url, { headers }); - return new Client(new RequestManager([transport])); + let nextId = 0; + return { + request: async ({ method, params = [] }) => { + const res = await fetch(url, { method: 'POST', headers, body: JSON.stringify({ jsonrpc: '1.0', id: ++nextId, method, params }), signal: AbortSignal.timeout(RPC_TIMEOUT_MS) }); + const body = await res.json(); + if (!body || body.error) throw new Error((body && body.error && body.error.message) || `RPC ${res.status}`); + return body.result; + } + }; } module.exports = { @@ -50,8 +53,8 @@ module.exports = { const errors = []; const addrInfo = await module.exports.execute(url, user, pass, "validateaddress", [address]); const addressValid = !!addrInfo?.isvalid; - const amountValid = Number(amount) > 0; - const feeValid = Number(fee) >= 0; + const amountValid = Number.isFinite(Number(amount)) && Number(amount) > 0; + const feeValid = Number.isFinite(Number(fee)) && Number(fee) >= 0; if (!addressValid) errors.push("invalid_dest"); if (!amountValid) errors.push("invalid_amount"); if (!feeValid) errors.push("invalid_fee"); diff --git a/src/models/wiki_model.js b/src/models/wiki_model.js new file mode 100644 index 0000000..ee8ffe6 --- /dev/null +++ b/src/models/wiki_model.js @@ -0,0 +1,485 @@ +const pull = require('../server/node_modules/pull-stream'); +const { readTyped, memoIndex } = require('./typed_log'); +const { buildValidatedTombstoneSet } = require('./tombstone_validator'); +const { getConfig } = require('../configs/config-manager.js'); +const logLimit = getConfig().ssbLogStream?.limit || 1000; + +const WIKILINK_RE = /\[\[([^\]|\n]{1,120})(?:\|([^\]\n]{1,120}))?\]\]/g; + +const slugify = (value) => String(value == null ? '' : value) + .normalize('NFD') + .replace(/[̀-ͯ]/g, '') + .toLowerCase() + .trim() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, '') + .slice(0, 80); + +const linkTarget = (raw) => { + const s = String(raw || '').trim(); + const isUrl = /^[a-z]+:\/\//i.test(s); + const wikiPath = (isUrl || s.startsWith('/')) ? s.match(/\/wiki\/([^?#]+)/) : null; + if (wikiPath) { try { return decodeURIComponent(wikiPath[1]).trim(); } catch (_) { return wikiPath[1].trim(); } } + if (isUrl) { + const seg = s.replace(/[?#].*$/, '').split('/').filter(Boolean).pop() || ''; + try { return decodeURIComponent(seg).trim(); } catch (_) { return seg.trim(); } + } + const namespaced = s.match(/^[a-z]+:(?=\S)(.+)$/i); + return namespaced ? namespaced[1].trim() : s; +}; + +const extractWikiLinks = (text) => { + const out = []; + const seen = new Set(); + for (const m of String(text || '').matchAll(WIKILINK_RE)) { + const slug = slugify(linkTarget(m[1])); + if (!slug || seen.has(slug)) continue; + seen.add(slug); + out.push(slug); + } + return out; +}; + +const WIKI_TYPE = 'wikiPage'; +const ENVELOPE_TYPE = 'tribe-msg'; +const EDIT_POLICIES = ['open', 'author', 'tribe']; +const RECENT_MS = 24 * 60 * 60 * 1000; +const MAX_CHANGES = 60; +const MAX_TITLE = 100; +const IMAGE_RE = /!\[image:[^\]\[\n]{0,160}\]\((&[^)\s]{1,120}\.sha256)\)/; +const BODY_INLINE_MAX = 6000; +const bodyCache = new Map(); +const addBlob = (ssbClient, buf) => new Promise((resolve, reject) => pull(pull.values([buf]), ssbClient.blobs.add((err, ref) => err ? reject(err) : resolve(ref)))); +const getBlob = (ssbClient, ref) => new Promise((resolve) => { + try { + pull(ssbClient.blobs.get(ref), pull.collect((err, chunks) => resolve(err || !chunks || !chunks.length ? null : Buffer.concat(chunks.map(c => Buffer.isBuffer(c) ? c : Buffer.from(c)))))); + } catch (_) { resolve(null); } +}); + +const safeText = (v) => String(v == null ? '' : v).trim(); +const normalizeList = (raw) => { + if (!raw) return []; + const arr = Array.isArray(raw) ? raw : String(raw).split(','); + return Array.from(new Set(arr.map(t => String(t || '').trim()).filter(Boolean))); +}; +const normalizePolicy = (v) => { + const s = String(v || '').toLowerCase(); + if (s === 'closed') return 'author'; + return EDIT_POLICIES.includes(s) ? s : 'open'; +}; + +module.exports = ({ cooler, tribeCrypto = null, tribesModel = null }) => { + let ssb; + const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb; }; + + const tribeRootId = async (tribeId) => { + if (!tribesModel || !tribeId) return tribeId; + try { return await tribesModel.getRootId(tribeId); } catch (_) { return tribeId; } + }; + + const tribeKeyFor = async (tribeId) => { + if (!tribeCrypto || !tribeId) return null; + const rootId = await tribeRootId(tribeId); + return tribeCrypto.getKey(rootId) || null; + }; + + const storeBody = async (ssbClient, body, tribeId) => { + const text = safeText(body); + if (Buffer.byteLength(text, 'utf8') <= BODY_INLINE_MAX) return { body: text }; + let payload = text; + if (tribeId) { + const key = await tribeKeyFor(tribeId); + if (!key) throw new Error('Missing tribe key — cannot publish encrypted wiki page'); + payload = JSON.stringify(tribeCrypto.encryptContent({ body: text }, [key], true)); + } + const ref = await addBlob(ssbClient, Buffer.from(payload, 'utf8')); + bodyCache.set(ref, text); + return { body: '', bodyBlob: ref }; + }; + + const resolveBody = async (ssbClient, c) => { + if (!c || typeof c.bodyBlob !== 'string' || !c.bodyBlob) return c; + if (bodyCache.has(c.bodyBlob)) return { ...c, body: bodyCache.get(c.bodyBlob) }; + const buf = await getBlob(ssbClient, c.bodyBlob); + if (!buf) return { ...c, body: safeText(c.body) }; + let text = buf.toString('utf8'); + if (c.tribeId) { + try { + const key = await tribeKeyFor(c.tribeId); + const r = tribeCrypto.decryptContent(JSON.parse(text), key ? [[key]] : []); + text = r && !r._undecryptable && typeof r.body === 'string' ? r.body : ''; + } catch (_) { text = ''; } + } + bodyCache.set(c.bodyBlob, text); + return { ...c, body: text }; + }; + + const readEntries = async (ssbClient) => { + const msgs = await readTyped(ssbClient, [WIKI_TYPE, 'tombstone', ENVELOPE_TYPE], { limit: logLimit }); + const fpIdx = tribeCrypto && typeof tribeCrypto.buildFingerprintIndex === 'function' ? tribeCrypto.buildFingerprintIndex() : null; + const out = []; + for (const m of msgs) { + const v = m && m.value; + const c = v && v.content; + if (!c || typeof c !== 'object') continue; + if (c.type === WIKI_TYPE || c.type === 'tombstone') { out.push(m); continue; } + if (!fpIdx || !tribeCrypto.isTribeMsg(c)) continue; + const r = tribeCrypto.unwrapMsg(c, fpIdx); + if (!r || !r.body || typeof r.body !== 'object') continue; + const body = r.body; + if (body.k === WIKI_TYPE) { + const flat = { ...body, type: WIKI_TYPE }; + delete flat.k; + out.push({ ...m, value: { ...v, content: flat } }); + } else if (body.k === 'tombstone' && typeof body.target === 'string') { + out.push({ ...m, value: { ...v, content: { type: 'tombstone', target: body.target, deletedAt: body.deletedAt, author: body.author || v.author } } }); + } + } + for (let i = 0; i < out.length; i++) { + const c = out[i].value && out[i].value.content; + if (c && c.type === WIKI_TYPE && c.bodyBlob) out[i] = { ...out[i], value: { ...out[i].value, content: await resolveBody(ssbClient, c) } }; + } + return out; + }; + + const tribeMembersFor = async (entries) => { + const out = new Map(); + if (!tribesModel) return out; + for (const m of entries) { + const c = m.value.content; + if (c.type !== WIKI_TYPE || !c.tribeId || out.has(String(c.tribeId))) continue; + let members = []; + try { const t = await tribesModel.getTribeById(c.tribeId); members = Array.isArray(t && t.members) ? t.members : []; } catch (_) {} + out.set(String(c.tribeId), members); + } + return out; + }; + + const buildIndex = (entries, membersOf = new Map()) => { + const tomb = buildValidatedTombstoneSet(entries); + const nodes = new Map(); + for (const m of entries) { + const c = m.value.content; + if (c.type !== WIKI_TYPE) continue; + nodes.set(m.key, { key: m.key, author: m.value.author, ts: m.value.timestamp || m.timestamp || 0, c }); + } + const mayEdit = (state, author) => { + if (author === state.owner) return true; + if (state.policy === 'author') return false; + if (state.tribeId) return (membersOf.get(state.tribeId) || []).includes(author); + return state.policy !== 'tribe'; + }; + const states = new Map(); + const stateOf = (key) => { + const path = []; + const seen = new Set(); + let cur = key; + while (!states.has(cur)) { + const node = nodes.get(cur); + if (!node) { states.set(cur, null); break; } + if (typeof node.c.replaces !== 'string') { + states.set(cur, { root: cur, owner: node.author, tribeId: String(node.c.tribeId || ''), policy: normalizePolicy(node.c.editPolicy), license: safeText(node.c.license) }); + break; + } + if (seen.has(cur)) { states.set(cur, null); break; } + seen.add(cur); + path.push(cur); + cur = node.c.replaces; + } + for (let i = path.length - 1; i >= 0; i--) { + const k = path[i]; + if (states.has(k)) continue; + const node = nodes.get(k); + const parent = states.get(node.c.replaces); + if (!parent || String(node.c.tribeId || '') !== parent.tribeId || !mayEdit(parent, node.author)) { states.set(k, null); continue; } + states.set(k, node.author === parent.owner ? { ...parent, policy: normalizePolicy(node.c.editPolicy), license: safeText(node.c.license) } : parent); + } + return states.get(key); + }; + const rootOf = (key) => { const s = stateOf(key); return s ? s.root : key; }; + const versionsByRoot = new Map(); + for (const node of nodes.values()) { + if (!stateOf(node.key)) continue; + const root = rootOf(node.key); + if (tomb.has(root)) continue; + if (!versionsByRoot.has(root)) versionsByRoot.set(root, []); + versionsByRoot.get(root).push(node); + } + const pages = []; + for (const [root, versions] of versionsByRoot) { + versions.sort((a, b) => a.ts - b.ts); + const first = versions[0]; + const tip = versions[versions.length - 1]; + const state = stateOf(tip.key); + const c = tip.c; + const body = safeText(c.body); + pages.push({ + id: root, + rootId: root, + tipId: tip.key, + slug: slugify(c.title) || safeText(c.slug), + title: safeText(c.title).slice(0, MAX_TITLE), + body, + image: (body.match(IMAGE_RE) || [])[1] || null, + tags: normalizeList(c.tags), + aliases: [...new Set([...normalizeList(c.aliases).map(slugify), safeText(c.slug)].filter(a => a && a !== slugify(c.title)))], + editPolicy: state.policy, + license: state.license, + tribeId: c.tribeId || null, + encrypted: !!c.tribeId, + author: state.owner, + lastAuthor: tip.author, + createdAt: first.c.createdAt || new Date(first.ts).toISOString(), + updatedAt: c.updatedAt || new Date(tip.ts).toISOString(), + ts: tip.ts, + versionCount: versions.length, + links: extractWikiLinks(body), + versions: versions.map(vn => ({ + key: vn.key, + author: vn.author, + ts: vn.ts, + createdAt: vn.c.updatedAt || vn.c.createdAt || new Date(vn.ts).toISOString(), + title: safeText(vn.c.title), + body: safeText(vn.c.body), + tags: normalizeList(vn.c.tags), + summary: safeText(vn.c.summary) + })) + }); + } + pages.sort((a, b) => a.createdAt < b.createdAt ? -1 : a.createdAt > b.createdAt ? 1 : 0); + return { pages, rootOf, nodes }; + }; + + const sameScope = (page, tribeId) => String(page.tribeId || '') === String(tribeId || ''); + + const scopePages = async (pages, tribeId) => { + if (tribeId === 'any') return pages; + if (!tribeId) return pages.filter(p => !p.tribeId); + const chain = tribesModel && typeof tribesModel.getChainIds === 'function' + ? await tribesModel.getChainIds(tribeId).catch(() => [tribeId]) + : [tribeId]; + const set = new Set(chain.map(String)); + return pages.filter(p => p.tribeId && set.has(String(p.tribeId))); + }; + + const decorate = (pages, scoped) => { + const bySlug = new Map(); + for (const p of scoped) { + const keys = [p.slug, ...p.aliases]; + for (const s of keys) if (s && !bySlug.has(s)) bySlug.set(s, p); + } + const inbound = new Map(); + for (const p of scoped) { + for (const s of p.links) { + const target = bySlug.get(s); + if (!target || target.id === p.id) continue; + if (!inbound.has(target.id)) inbound.set(target.id, new Set()); + inbound.get(target.id).add(p.id); + } + } + return scoped.map(p => ({ + ...p, + backlinkCount: (inbound.get(p.id) || new Set()).size, + missingLinks: p.links.filter(s => !bySlug.has(s)), + isLinked: (inbound.get(p.id) || new Set()).size > 0 + })); + }; + + const findInScope = (scoped, idOrSlug) => { + const needle = String(idOrSlug || '').trim(); + if (!needle) return null; + if (needle.startsWith('%')) return scoped.find(p => p.id === needle || p.tipId === needle || p.versions.some(v => v.key === needle)) || null; + const slug = slugify(needle); + return scoped.find(p => p.slug === slug) || scoped.find(p => p.aliases.includes(slug)) || null; + }; + + const publishContent = async (ssbClient, content) => { + let msg = content; + if (content.tribeId) { + const key = await tribeKeyFor(content.tribeId); + if (!key) throw new Error('Missing tribe key — cannot publish encrypted wiki page'); + msg = tribeCrypto.wrapMsg({ k: WIKI_TYPE, ...content }, key); + } + return new Promise((resolve, reject) => ssbClient.publish(msg, (err, res) => err ? reject(err) : resolve(res))); + }; + + const load = async () => { + const ssbClient = await openSsb(); + const entries = await readEntries(ssbClient); + const members = await tribeMembersFor(entries); + const idx = memoIndex('wiki', entries, (e) => buildIndex(e, members)); + return { ssbClient, idx }; + }; + + const canEdit = async (page, viewer) => { + if (!page) return false; + const owner = String(page.author) === String(viewer); + if (page.editPolicy === 'author') return owner; + if (page.tribeId) { + if (!tribesModel) return owner; + try { + const t = await tribesModel.getTribeById(page.tribeId); + return owner || (Array.isArray(t.members) && t.members.includes(viewer)); + } catch (_) { return owner; } + } + if (page.editPolicy === 'tribe') return owner; + return true; + }; + + return { + WIKI_TYPE, + EDIT_POLICIES, + slugify, + + async createPage({ title, body, tags, aliases, editPolicy, license, tribeId = null }) { + const { ssbClient, idx } = await load(); + const cleanTitle = safeText(title).slice(0, MAX_TITLE); + if (!cleanTitle) throw new Error('Title required'); + const slug = slugify(cleanTitle); + if (!slug) throw new Error('Title required'); + const scoped = await scopePages(idx.pages, tribeId); + const existing = scoped.find(p => p.slug === slug); + if (existing) return { key: existing.id, existing: true }; + const now = new Date().toISOString(); + const content = { + type: WIKI_TYPE, + title: cleanTitle, + slug, + ...(await storeBody(ssbClient, body, tribeId)), + tags: normalizeList(tags), + aliases: normalizeList(aliases).map(slugify).filter(Boolean), + editPolicy: normalizePolicy(editPolicy), + ...(license ? { license } : {}), + author: ssbClient.id, + createdAt: now, + updatedAt: now, + ...(tribeId ? { tribeId } : {}) + }; + const res = await publishContent(ssbClient, content); + return { key: res.key, existing: false }; + }, + + async updatePage(id, data = {}) { + const { ssbClient, idx } = await load(); + const page = findInScope(idx.pages, id); + if (!page) throw new Error('Page not found'); + if (!(await canEdit(page, ssbClient.id))) throw new Error('Not allowed to edit'); + const now = new Date().toISOString(); + const nextTitle = data.title !== undefined ? safeText(data.title).slice(0, MAX_TITLE) : page.title; + const nextLicense = data.license !== undefined && String(page.author) === String(ssbClient.id) ? safeText(data.license) : page.license; + const content = { + type: WIKI_TYPE, + title: nextTitle || page.title, + slug: page.slug, + ...(await storeBody(ssbClient, data.body !== undefined ? data.body : page.body, page.tribeId || null)), + tags: data.tags !== undefined ? normalizeList(data.tags) : page.tags, + aliases: data.aliases !== undefined ? normalizeList(data.aliases).map(slugify).filter(Boolean) : page.aliases, + editPolicy: data.editPolicy !== undefined ? normalizePolicy(data.editPolicy) : page.editPolicy, + ...(nextLicense ? { license: nextLicense } : {}), + summary: safeText(data.summary).slice(0, 200), + author: ssbClient.id, + createdAt: page.createdAt, + updatedAt: now, + replaces: page.tipId, + ...(page.tribeId ? { tribeId: page.tribeId } : {}) + }; + const res = await publishContent(ssbClient, content); + return { key: res.key, rootId: page.id, author: page.author }; + }, + + async restoreVersion(id, versionKey) { + const { idx } = await load(); + const page = findInScope(idx.pages, id); + if (!page) throw new Error('Page not found'); + const version = page.versions.find(v => v.key === versionKey); + if (!version) throw new Error('Version not found'); + return this.updatePage(page.id, { title: version.title, body: version.body, tags: version.tags, summary: `restore:${versionKey}` }); + }, + + async deletePage(id) { + const { ssbClient, idx } = await load(); + const page = findInScope(idx.pages, id); + if (!page) throw new Error('Page not found'); + if (String(page.author) !== String(ssbClient.id)) throw new Error('Only the creator can delete'); + const deletedAt = new Date().toISOString(); + const tomb = { type: 'tombstone', target: page.id, deletedAt, author: ssbClient.id }; + let msg = tomb; + if (page.tribeId) { + const key = await tribeKeyFor(page.tribeId); + if (!key) throw new Error('Missing tribe key — cannot delete encrypted wiki page'); + msg = tribeCrypto.wrapMsg({ k: 'tombstone', target: page.id, deletedAt, author: ssbClient.id }, key); + } + return new Promise((resolve, reject) => ssbClient.publish(msg, (err, res) => err ? reject(err) : resolve(res))); + }, + + async getPage(idOrSlug, { tribeId = null } = {}) { + const { ssbClient, idx } = await load(); + let scoped = await scopePages(idx.pages, tribeId); + let page = findInScope(scoped, idOrSlug); + if (!page && String(idOrSlug || '').startsWith('%')) { + page = findInScope(idx.pages, idOrSlug); + if (page) scoped = await scopePages(idx.pages, page.tribeId || null); + } + if (!page) return null; + const decorated = decorate(idx.pages, scoped); + const full = decorated.find(p => p.id === page.id) || page; + const bySlug = new Map(); + for (const p of scoped) for (const s of [p.slug, ...p.aliases]) if (s && !bySlug.has(s)) bySlug.set(s, p); + const backlinks = decorated.filter(p => p.id !== full.id && p.links.some(s => bySlug.get(s) && bySlug.get(s).id === full.id)); + const outbound = []; + const seen = new Set([full.id]); + for (const slug of full.links) { + const target = bySlug.get(slug); + if (!target || seen.has(target.id)) continue; + seen.add(target.id); + outbound.push(decorated.find(p => p.id === target.id) || target); + } + const linkedPages = [...backlinks]; + const inList = new Set(backlinks.map(p => p.id)); + for (const p of outbound) if (!inList.has(p.id)) linkedPages.push(p); + return { ...full, backlinks, outbound, linkedPages, canEdit: await canEdit(full, ssbClient.id), isOwner: String(full.author) === String(ssbClient.id) }; + }, + + async listPages({ tribeId = null, filter = 'all', q = '', viewerId } = {}) { + const { ssbClient, idx } = await load(); + const me = viewerId || ssbClient.id; + const scoped = decorate(idx.pages, await scopePages(idx.pages, tribeId)); + let list = scoped; + const f = String(filter || 'all').toLowerCase(); + if (f === 'mine') list = list.filter(p => String(p.author) === String(me) || p.versions.some(v => String(v.author) === String(me))); + else if (f === 'recent') list = list.filter(p => Date.now() - p.ts <= RECENT_MS); + else if (f === 'linked') list = list.filter(p => p.isLinked); + const needle = safeText(q).toLowerCase(); + if (needle) list = list.filter(p => p.title.toLowerCase().includes(needle) || p.body.toLowerCase().includes(needle) || p.tags.some(t => t.toLowerCase().includes(needle))); + return list.slice().sort((a, b) => b.ts - a.ts); + }, + + async recentChanges({ tribeId = null } = {}) { + const { idx } = await load(); + const scoped = await scopePages(idx.pages, tribeId); + const changes = []; + for (const p of scoped) { + p.versions.forEach((v, i) => changes.push({ + pageId: p.id, slug: p.slug, title: v.title || p.title, tribeId: p.tribeId || null, + versionKey: v.key, author: v.author, ts: v.ts, createdAt: v.createdAt, summary: v.summary, index: i + 1, isCreation: i === 0 + })); + } + return changes.sort((a, b) => b.ts - a.ts || b.index - a.index).slice(0, MAX_CHANGES); + }, + + async resolveRootId(id) { + const { idx } = await load(); + const page = findInScope(idx.pages, id); + if (!page) throw new Error('Not found'); + return page.id; + }, + + async canEditPage(id, viewer) { + const { ssbClient, idx } = await load(); + const page = findInScope(idx.pages, id); + return canEdit(page, viewer || ssbClient.id); + } + }; +}; + +Object.assign(module.exports, { WIKILINK_RE, slugify, linkTarget, extractWikiLinks }); diff --git a/src/models/workflows_model.js b/src/models/workflows_model.js new file mode 100644 index 0000000..81ba66f --- /dev/null +++ b/src/models/workflows_model.js @@ -0,0 +1,156 @@ +const ALL_MODULES = [ + 'agenda', 'ai', 'aiNav', 'emergencies', 'audios', 'backup', 'banking', 'blogs', 'bookmarks', 'calendars', 'campaigns', 'chats', 'cipher', + 'courts', 'dev', 'docs', 'events', 'favorites', 'fediverse', 'feed', 'files', 'forum', 'games', 'graphos', + 'housing', 'images', 'industry', 'invites', 'jobs', 'larp', 'logistics', 'logs', 'mailing', 'maps', 'market', + 'melody', 'opinions', 'pads', 'wiki', 'parliament', 'phone', 'pixelia', 'podcasts', 'polls', 'projects', 'reports', 'rooms', 'school', 'shops', + 'tags', 'tasks', 'torrents', 'transfers', 'trending', 'tribes', 'videos', 'votes', 'wallet' +]; + +const NETWORK = ['feed', 'blogs', 'tags', 'trending', 'opinions', 'pads', 'wiki', 'forum', 'maps', 'chats', 'rooms']; +const MEDIA = ['audios', 'bookmarks', 'docs', 'files', 'images', 'torrents', 'videos', 'podcasts']; +const OFFICE = ['agenda', 'calendars', 'campaigns', 'events', 'tasks', 'reports', 'mailing', 'phone', 'favorites']; +const GOVERNANCE = ['tribes', 'larp', 'votes', 'polls', 'school', 'parliament', 'courts', 'emergencies', 'logs']; +const ECONOMY = ['banking', 'wallet', 'transfers', 'market', 'logistics', 'housing', 'jobs', 'shops', 'industry', 'projects']; + +const PRESET_BASE = ['backup', 'invites', 'favorites', 'tags', 'trending', 'cipher']; +const PRESETS = { + minimal: ['feed', 'forum', 'games', 'images', 'videos', 'audios', 'bookmarks', 'tags', 'trending', 'blogs', 'polls', 'opinions', 'cipher', 'backup'], + social: ['agenda', 'emergencies', 'audios', 'bookmarks', 'calendars', 'campaigns', 'chats', 'cipher', 'courts', 'docs', 'events', 'favorites', 'fediverse', 'feed', 'forum', 'games', 'images', 'invites', 'larp', 'backup', 'logs', 'mailing', 'phone', 'maps', 'blogs', 'polls', 'opinions', 'pads', 'wiki', 'parliament', 'pixelia', 'podcasts', 'melody', 'projects', 'reports', 'rooms', 'school', 'tags', 'tasks', 'trending', 'tribes', 'videos', 'votes'], + economy: ['agenda', 'emergencies', 'audios', 'bookmarks', 'calendars', 'campaigns', 'chats', 'cipher', 'courts', 'docs', 'events', 'favorites', 'fediverse', 'feed', 'forum', 'games', 'images', 'invites', 'larp', 'backup', 'logs', 'mailing', 'phone', 'maps', 'blogs', 'polls', 'opinions', 'pads', 'wiki', 'parliament', 'pixelia', 'podcasts', 'melody', 'projects', 'reports', 'rooms', 'tags', 'tasks', 'trending', 'tribes', 'videos', 'votes', 'banking', 'wallet', 'transfers', 'market', 'housing', 'jobs', 'shops', 'industry', 'school', 'logistics'], + office: [...new Set([...OFFICE, 'votes', 'polls', 'projects', 'docs', 'pads', 'wiki', 'tribes', 'chats', 'rooms', 'feed', ...PRESET_BASE])], + library: [...new Set([...MEDIA, 'feed', ...PRESET_BASE])], + mobile: null, + full: null +}; + +const MOBILE_MODULES = [ + 'agenda', 'favorites', 'wallet', 'tribes', 'larp', 'votes', 'polls', 'events', 'calendars', 'tasks', + 'reports', 'banking', 'market', 'housing', 'jobs', 'shops', 'school', 'courts', 'transfers', 'cipher', 'invites', + 'games', 'audios', 'bookmarks', 'docs', 'files', 'images', 'torrents', 'emergencies', 'mailing', 'logistics', 'podcasts', 'campaigns', ...NETWORK.filter(m => m !== 'rooms') +]; + +const WORKFLOWS = [ + { + key: 'default', + theme: 'Dark-SNH', + homePage: 'activity', + modules: ALL_MODULES.filter(m => m !== 'aiNav') + }, + { + key: 'jobs', + theme: 'Clear-SNH', + homePage: 'activity', + modules: [...OFFICE, 'jobs', 'projects', 'industry', 'market', 'shops', 'banking', 'wallet', 'transfers', + 'forum', 'chats', 'rooms', 'pads', 'wiki', 'feed', 'tags', 'trending', 'docs', 'invites'] + }, + { + key: 'social', + theme: 'Clear-SNH', + homePage: 'feed', + modules: [...NETWORK, ...MEDIA, 'events', 'calendars', 'agenda', 'favorites', 'phone', 'games', 'pixelia', + 'melody', 'tribes', 'fediverse', 'invites', 'polls', 'school', 'emergencies'] + }, + { + key: 'business', + theme: 'Dark-SNH', + homePage: 'activity', + modules: [...ECONOMY, ...OFFICE, 'forum', 'chats', 'rooms', 'pads', 'wiki', 'emergencies', 'docs', 'feed', 'tags', 'trending', 'invites', 'cipher'] + }, + { + key: 'night', + theme: 'Matrix-SNH', + homePage: 'feed', + modules: ['feed', 'blogs', 'opinions', 'chats', 'rooms', 'forum', 'tags', 'trending', 'audios', 'videos', + 'images', 'bookmarks', 'melody', 'games', 'pixelia', 'favorites', 'school'] + }, + { + key: 'kids', + theme: 'Clear-SNH', + homePage: 'activity', + modules: ['school', 'games', 'pixelia', 'melody', 'images', 'audios', 'videos', 'docs', 'bookmarks', + 'agenda', 'calendars', 'events', 'tasks', 'favorites', 'feed', 'blogs', 'chats', 'pads', 'maps', + 'tags', 'trending', 'tribes', 'larp', 'polls', 'opinions', 'invites'] + }, + { + key: 'activists', + theme: 'Purple-SNH', + homePage: 'activity', + modules: [...GOVERNANCE, ...OFFICE, + 'forum', 'chats', 'rooms', 'feed', 'blogs', 'opinions', 'pads', 'wiki', 'docs', 'images', 'videos', 'audios', 'podcasts', + 'files', 'torrents', 'maps', 'bookmarks', 'graphos', 'melody', 'cipher', 'fediverse', 'projects', 'logistics', + 'invites', 'tags', 'trending', 'backup'] + }, + { + key: 'mobile', + theme: 'Acampada', + homePage: 'activity', + modules: MOBILE_MODULES + } +]; + +const WORKFLOW_KEYS = WORKFLOWS.map(w => w.key); + +const isMobile = () => process.env.OASIS_MOBILE === '1'; +// en el movil el workflow no toca el skin: se queda el que haya elegido cada cual +// (Acampada u OasisMobile); si no hay ninguno de movil, Acampada +const MOBILE_THEMES = ['Acampada', 'OasisMobile']; +const mobileSkin = (cfg) => { + const cur = cfg && cfg.themes && cfg.themes.current; + return MOBILE_THEMES.includes(cur) ? cur : 'Acampada'; +}; +const themeOf = (workflow, cfg) => (isMobile() ? mobileSkin(cfg) : workflow.theme); +const sameTheme = (theme, workflow) => (isMobile() ? MOBILE_THEMES.includes(theme) : theme === workflow.theme); + +const getWorkflow = (key) => WORKFLOWS.find(w => w.key === String(key || '')) || null; + +const modulesOf = (workflow) => { + const set = new Set(workflow.modules.filter(m => ALL_MODULES.includes(m))); + return ALL_MODULES.filter(m => set.has(m)); +}; + +const currentWorkflow = (config) => { + const modules = (config && config.modules) || {}; + const theme = (config && config.themes && config.themes.current) || ''; + return WORKFLOW_KEYS.find(key => { + const w = getWorkflow(key); + if (!sameTheme(theme, w)) return false; + const on = new Set(modulesOf(w)); + return ALL_MODULES.every(m => (modules[`${m}Mod`] === 'on') === on.has(m)); + }) || null; +}; + +const applyWorkflow = (cfg, workflow) => { + const enabled = new Set(modulesOf(workflow)); + cfg.modules = cfg.modules || {}; + ALL_MODULES.forEach(mod => { cfg.modules[`${mod}Mod`] = enabled.has(mod) ? 'on' : 'off'; }); + if (workflow.theme) cfg.themes = { ...(cfg.themes || {}), current: themeOf(workflow, cfg) }; + if (workflow.homePage) cfg.homePage = workflow.homePage; + return cfg; +}; + +const FULL_WORKFLOW = { key: 'full', modules: ALL_MODULES }; + +const WIZARD_WORKFLOWS = ['full', 'social', 'activists', 'kids', 'night', 'jobs', 'business', 'mobile']; + +const wizardWorkflow = (key) => { + const k = String(key || ''); + if (!WIZARD_WORKFLOWS.includes(k)) return null; + return k === 'full' ? FULL_WORKFLOW : getWorkflow(k); +}; + +PRESETS.mobile = MOBILE_MODULES; +PRESETS.full = ALL_MODULES; + +module.exports = { + ALL_MODULES, + MOBILE_MODULES, + PRESETS, + WORKFLOWS, + WORKFLOW_KEYS, + getWorkflow, + modulesOf, + currentWorkflow, + applyWorkflow, + WIZARD_WORKFLOWS, + wizardWorkflow +}; diff --git a/src/server/SSB_server.js b/src/server/SSB_server.js index cd69637..ea12deb 100644 --- a/src/server/SSB_server.js +++ b/src/server/SSB_server.js @@ -6,7 +6,7 @@ const fs = require('fs'); const path = require('path'); const SecretStack = require('secret-stack'); const caps = require('ssb-caps'); -const SSB = require('ssb-db'); +const db2Defaults = require('ssb-db2/defaults'); const config = require('./ssb_config'); const { printMetadata } = require('./ssb_metadata'); @@ -27,6 +27,7 @@ const { printMetadata } = require('./ssb_metadata'); return `${m[1].replace(/^::ffff:/, '')}:${m[2]}`; }; const logRejection = (peer) => { + if (!(process.argv.includes('--debug') || process.env.OASIS_DEBUG === '1' || process.env.OASIS_DEBUG === 'true')) return; const ts = new Date().toISOString().replace('T', ' ').slice(0, 19); realErr.call(console, `[${ts}] REJECTED ${peer} (wrong SHS cap)`); }; @@ -51,35 +52,41 @@ const { printMetadata } = require('./ssb_metadata'); }; })(); -require('ssb-plugins').loadUserPlugins(SecretStack({ caps }), config); +try { + if (require('../configs/config-manager.js').getConfig().networkPaused === true || process.env.OASIS_NETWORK_PAUSED === '1') config.conn = { ...(config.conn || {}), autostart: false }; +} catch (_) {} const Server = SecretStack({ caps }) - .use(SSB) + .use(require('ssb-db2/core')) + .use(require('ssb-classic')) + .use(require('ssb-box')) + .use(require('ssb-db2/compat/publish')) + .use(require('ssb-db2/compat/post')) + .use(require('ssb-db2/compat/db')) + .use(require('ssb-db2/compat/log-stream')) + .use(require('ssb-db2/compat/history-stream')) + .use(require('ssb-db2/compat/ebt')) + .use(require('ssb-db2/compat/feedstate')) + .use(require('./db2_legacy')) .use(require('ssb-master')) - .use(require('ssb-gossip')) .use(require('ssb-ebt')) .use(require('ssb-friends')) .use(require('ssb-blobs')) - .use(require('ssb-meme')) - .use(require('ssb-plugins')) .use(require('ssb-conn')) - .use(require('ssb-box')) - .use(require('ssb-search')) - .use(require('ssb-private')) - .use(require('ssb-friend-pub')) .use(config.pub ? require('ssb-invite') : require('ssb-invite-client')) .use(require('ssb-logging')) .use(require('ssb-replication-scheduler')) - .use(require('ssb-partial-replication')) - .use(require('ssb-about')) .use(require('ssb-onion')) .use(require('ssb-unix-socket')) .use(require('ssb-no-auth')) - .use(require('ssb-backlinks')) - .use(require('ssb-links')) - .use(require('ssb-tangle')) - .use(require('ssb-query')); + .use(require('./snapshot_plugin')) + .use((() => { const pm = require('./phone_module'); try { if (process.env.OASIS_MOBILE === '1') { pm.setAudioFactory(require('./phone_audio_android')); pm.setVideoFactory(require('./phone_video_android')); pm.setStreamFactory(require('./phone_stream_android')); } } catch (_) {} return pm; })()) + .use(require('./network_pause')); +if (!config.pub) { + Server.use(require('ssb-lan')); + Server.use(require('./lanRouter')); +} if (config.autofollow && typeof config.autofollow === 'object' && !Array.isArray(config.autofollow)) { if (config.autofollow.enabled === false) { @@ -104,20 +111,135 @@ const isLockError = (err) => { return /Resource temporarily unavailable/i.test(msg) && /\.ssb\/.*LOCK/i.test(msg); }; +const isCorruptStoreError = (err) => { + const msg = String((err && err.message) || ''); + const stack = String((err && err.stack) || ''); + return /JSON|Unexpected token|Unexpected end|bipf|Invalid|corrupt/i.test(msg) + && /flumelog-offset|aligned-block-file|flumeview|flumedb|async-append-only-log|jitdb|bipf|ssb-db2/i.test(stack); +}; + +const migration = { running: false, percent: 0, done: false, error: null }; +let migrationPromise = null; +const MIGRATED_MARKER = 'OASIS: this log was migrated to ssb-db2'; +const oldLogIsMarker = (file) => { + try { + const st = fs.statSync(file); + return st.size < 4096 && fs.readFileSync(file, 'utf8').startsWith(MIGRATED_MARKER); + } catch (_) { return false; } +}; +const writeMigratedMarker = () => { + try { + fs.mkdirSync(db2Defaults.flumePath(config.path), { recursive: true }); + fs.writeFileSync(db2Defaults.oldLogPath(config.path), `${MIGRATED_MARKER} (${config.path}/db2/log.bipf) on ${new Date().toISOString()}.\nThis file is a guard: an Oasis older than the ssb-db2 move cannot read it and refuses to start, instead of starting with an empty log and forking your feed. Do not delete it unless you are moving back to that version on purpose, and never run both versions on this folder.\n`); + } catch (_) {} +}; +const migrateFlume = () => { + if (migrationPromise) return migrationPromise; + migrationPromise = new Promise((resolve, reject) => { + const oldLog = db2Defaults.oldLogPath(config.path); + if (!fs.existsSync(oldLog) || oldLogIsMarker(oldLog)) { migration.done = true; return resolve(false); } + if (isDebug()) console.log('- Database: migrating the local log to ssb-db2'); + migration.running = true; + let mig; + try { + mig = SecretStack({ caps }) + .use(require('ssb-db2/migrate')) + .call(null, { ...config, connections: { incoming: {}, outgoing: {} }, db2: { ...(config.db2 || {}), automigrate: false, dangerouslyKillFlumeWhenMigrated: true } }); + } catch (err) { migration.running = false; migration.error = err; return reject(err); } + const pull = require('pull-stream'); + let shown = -1; + pull(mig.db2migrate.progress(), pull.drain((p) => { + const pct = Math.max(0, Math.min(100, Math.floor((Number(p) || 0) * 100))); + migration.percent = pct; + if (pct !== shown && isDebug()) { shown = pct; console.log(` migrating ${pct}%`); } + })); + const finish = () => { + const waitGone = (tries) => { + if (!fs.existsSync(db2Defaults.oldLogPath(config.path))) { + mig.close(() => { + writeMigratedMarker(); + migration.running = false; migration.done = true; migration.percent = 100; + if (isDebug()) console.log(' migrated 100%'); + resolve(true); + }); + return; + } + if (tries <= 0) { + try { fs.rmSync(db2Defaults.flumePath(config.path), { recursive: true, force: true }); } catch (_) {} + mig.close(() => { + if (!fs.existsSync(db2Defaults.oldLogPath(config.path))) writeMigratedMarker(); + else if (isDebug()) console.log(' the old log could not be removed; it is left in place'); + migration.running = false; migration.done = true; migration.percent = 100; + resolve(true); + }); + return; + } + setTimeout(() => waitGone(tries - 1), 200); + }; + waitGone(300); + }; + try { mig.db2migrate.start(); } catch (err) { migration.running = false; migration.error = err; return reject(err); } + mig.db2migrate.synchronized((synced) => { + if (synced !== true) return; + finish(); + return false; + }); + }); + return migrationPromise; +}; + +const isDebug = () => process.argv.includes('--debug') || process.env.OASIS_DEBUG === '1' || process.env.OASIS_DEBUG === 'true'; + +const isPortInUseError = (err) => String((err && err.code) || '') === 'EADDRINUSE'; + const handleFatal = (err) => { + if (isPortInUseError(err)) { + console.log(''); + console.log('Another Oasis instance is already running on this device. Close the other instance (or kill the process) and try again.'); + if (isDebug()) console.log(`Detail: ${String((err && err.message) || err)}`); + console.log(''); + process.exit(1); + } if (isLockError(err)) { console.log(''); console.log('Another Oasis instance is already running on this device. Close the other instance (or kill the process) and try again.'); + if (isDebug()) { + console.log(`Detail: ${String((err && err.message) || err)}`); + console.log(String((err && err.stack) || '').split('\n').slice(1, 4).join('\n')); + } console.log(''); process.exit(1); } + if (isCorruptStoreError(err)) { + console.log(''); + console.log('Oasis could not read its local database (a record in ~/.ssb/db2 looks corrupted).'); + console.log('This usually happens after an unclean shutdown or a disk error.'); + console.log(''); + console.log('What you can try, in order:'); + console.log(' 1. Simply start Oasis again — transient read errors often clear on the next boot.'); + console.log(' 2. Check your disk health (e.g. run fsck on the partition holding ~/.ssb).'); + console.log(' 3. Rebuild the indexes from Settings once Oasis starts, or remove ~/.ssb/db2/indexes'); + console.log(' and ~/.ssb/db2/jit (NOT log.bipf, NOT secret) so they are regenerated from the log.'); + console.log(''); + if (isDebug()) console.log(`Technical detail: ${String((err && err.message) || err)}`); + process.exit(1); + } + if (server) { + console.error('[oasis] unexpected error, still running:', (err && err.stack) || err); + return; + } throw err; }; process.on('uncaughtException', handleFatal); -if (argv[0] === 'start') { +const tightenStateDir = () => { + try { if (config.path && fs.existsSync(config.path)) fs.chmodSync(config.path, 0o700); } catch (_) {} +}; + +const startServerOnly = () => { try { + tightenStateDir(); server = Server(config); } catch (err) { handleFatal(err); @@ -133,7 +255,7 @@ if (argv[0] === 'start') { manifest.config = 'sync'; server.config = cb => { - console.log(JSON.stringify(config, null, 2)); + console.log(JSON.stringify(config, (k, v) => (k === 'keys' || k === 'private' || k === 'secret') ? undefined : v, 2)); cb(); }; @@ -182,10 +304,51 @@ if (argv[0] === 'start') { } catch (_) {} }, 1000); + const ownFeedSeq = () => new Promise((res) => { + try { + const pull = require('pull-stream'); + pull( + server.createUserStream({ id: server.id, reverse: true, limit: 1 }), + pull.collect((err, msgs) => res(err || !msgs || !msgs.length ? 0 : (msgs[0].value && msgs[0].value.sequence) || 0)) + ); + } catch (_) { res(0); } + }); + + setTimeout(async () => { + try { + if (!(await ownFeedSeq())) return; + const pull = require('pull-stream'); + const version = String(require('./package.json').version || ''); + if (!version) return; + pull( + server.createUserStream({ id: server.id, reverse: true }), + pull.filter(m => m && m.value && m.value.content && m.value.content.type === 'oasisVersion'), + pull.take(1), + pull.collect((err, msgs) => { + const mine = !err && msgs && msgs.length ? msgs[0].value.content.version : null; + if (mine !== version) { + try { server.publish({ type: 'oasisVersion', version, updatedAt: new Date().toISOString() }, () => {}); } catch (_) {} + } + }) + ); + } catch (_) {} + }, 7000); + +}; + +if (argv[0] === 'start') { + migrateFlume().then(startServerOnly, handleFatal); } module.exports = { config, - server: server || Server(config), - open: async () => server || Server(config) + get server() { + return server; + }, + migrationStatus: () => ({ ...migration }), + open: async () => { + await migrateFlume(); + if (!server) { tightenStateDir(); server = Server(config); } + return server; + } }; diff --git a/src/server/db2_legacy.js b/src/server/db2_legacy.js new file mode 100644 index 0000000..8550072 --- /dev/null +++ b/src/server/db2_legacy.js @@ -0,0 +1,651 @@ +const pull = require('pull-stream'); +const cat = require('pull-cat'); +const defer = require('pull-defer'); +const pl = require('pull-level'); +const bipf = require('bipf'); +const ssbKeys = require('ssb-keys'); +const ssbMsgs = require('ssb-msgs'); +const { heads } = require('ssb-sort'); +const Plugin = require('ssb-db2/indexes/plugin'); +const { where, and, or, type, author, descending, sortByArrival, toPullStream, toCallback, startFrom, paginate, seqs, isDecrypted, batch, lt, lte } = require('ssb-db2/operators'); + +const BIPF_CONTENT = bipf.allocAndEncode('content'); +const BIPF_AUTHOR = bipf.allocAndEncode('author'); +const BIPF_TIMESTAMP = bipf.allocAndEncode('timestamp'); +const BIPF_TYPE = bipf.allocAndEncode('type'); +const LINK_RE = /^[@%&]/; +const SEARCH_LIMIT = 100; +const ARRIVAL_SLACK_MS = 10 * 60 * 1000; +const ACTIVITY_DRAIN_MS = 3000; +const RECENT_KEEP = 64; +const FIRST_PAGE = 1000; +const LAST_PAGE = 64000; +const OPS = ['$gt', '$gte', '$lt', '$lte', '$in', '$is', '$prefix', '$ne', '$truthy', '$eq']; + +class LinksIndex extends Plugin { + constructor(log, dir) { + super(log, dir, 'oasisLinks', 1, 'json', 'json'); + } + + processRecord(record, seq, pValue) { + const buf = record.value; + const pContent = bipf.seekKey2(buf, pValue, BIPF_CONTENT, 0); + if (pContent < 0) return; + const content = bipf.decode(buf, pContent); + if (!content || typeof content !== 'object') return; + const pAuthor = bipf.seekKey2(buf, pValue, BIPF_AUTHOR, 0); + const source = pAuthor < 0 ? null : bipf.decode(buf, pAuthor); + const seen = new Set(); + ssbMsgs.indexLinks(content, (link, rel) => { + const dest = link && link.link; + if (typeof dest !== 'string' || !LINK_RE.test(dest)) return; + const mark = `${dest}\u0000${rel}`; + if (seen.has(mark)) return; + seen.add(mark); + this.batch.push({ type: 'put', key: [dest, seq, rel], value: { s: seq, a: source, r: rel } }); + }); + } + + indexesContent() { + return true; + } + + getLinks(dest, cb) { + pull( + pl.read(this.level, { gte: [dest, ''], lte: [dest, undefined], keyEncoding: this.keyEncoding, valueEncoding: this.valueEncoding, keys: false }), + pull.collect((err, arr) => err ? cb(err) : cb(null, (arr || []).filter(Boolean))) + ); + } +} + +class ActivityIndex extends Plugin { + constructor(log, dir) { + super(log, dir, 'oasisActivity', 2, undefined, 'json'); + this.latest = new Map(); + this.dirty = new Set(); + this.reached = -1; + this.waiting = []; + this.recent = []; + const onRecord = this.onRecord; + this.onRecord = (record, isLive, pValue) => { + onRecord.call(this, record, isLive, pValue); + const last = this.recent.length ? this.recent[this.recent.length - 1] : -1; + if (record.value && record.offset > last) { + this.recent.push(record.offset); + if (this.recent.length > RECENT_KEEP) this.recent.shift(); + } + this.reach(record.offset); + }; + this.offset((offset) => this.reach(offset)); + } + + reach(offset) { + if (!(offset > this.reached)) return; + this.reached = offset; + if (!this.waiting.length) return; + const due = this.waiting.filter((w) => w.offset <= offset); + this.waiting = this.waiting.filter((w) => w.offset > offset); + for (const w of due) w.done(); + } + + whenReached(offset, ms, done) { + if (this.reached >= offset) return done(true); + let settled = false; + const finish = (ok) => { if (settled) return; settled = true; clearTimeout(timer); done(ok); }; + const timer = setTimeout(() => { this.waiting = this.waiting.filter((w) => w.done !== hit); finish(false); }, ms); + const hit = () => finish(true); + this.waiting.push({ offset, done: hit }); + } + + onLoaded(cb) { + pull( + pl.read(this.level, { gt: '\x00', valueEncoding: this.valueEncoding }), + pull.drain(({ key, value }) => { this.latest.set(key, value); }, (err) => cb(err && err !== true ? err : undefined)) + ); + } + + processRecord(record, seq, pValue) { + const buf = record.value; + const pAuthor = bipf.seekKey2(buf, pValue, BIPF_AUTHOR, 0); + if (pAuthor < 0) return; + const author = bipf.decode(buf, pAuthor); + if (typeof author !== 'string') return; + const pContent = bipf.seekKey2(buf, pValue, BIPF_CONTENT, 0); + if (pContent >= 0 && bipf.getEncodedType(buf, pContent) === bipf.types.object) { + const pType = bipf.seekKey2(buf, pContent, BIPF_TYPE, 0); + if (pType >= 0 && bipf.decode(buf, pType) === 'tombstone') return; + } + const pDeclared = bipf.seekKey2(buf, pValue, BIPF_TIMESTAMP, 0); + const pArrival = bipf.seekKey2(buf, 0, BIPF_TIMESTAMP, 0); + let ts = Number(pDeclared >= 0 ? bipf.decode(buf, pDeclared) : 0) || Number(pArrival >= 0 ? bipf.decode(buf, pArrival) : 0) || 0; + if (ts > 0 && ts < 1e12) ts *= 1000; + if (!(ts > (this.latest.get(author) || 0))) return; + this.latest.set(author, ts); + this.dirty.add(author); + } + + onFlush(cb) { + for (const author of this.dirty) this.batch.push({ type: 'put', key: author, value: this.latest.get(author) }); + this.dirty.clear(); + cb(); + } + + indexesContent() { + return false; + } + + reset() { + this.latest.clear(); + this.dirty.clear(); + this.recent = []; + } +} + +const linksOf = (content) => { + const out = []; + if (!content || typeof content !== 'object') return out; + ssbMsgs.indexLinks(content, (link, rel) => { + const dest = link && link.link; + if (typeof dest === 'string' && LINK_RE.test(dest)) out.push({ dest, rel }); + }); + return out; +}; + +const plainOf = (kvt, keys, opts) => { + if (!kvt || !kvt.value) return null; + const base = { key: kvt.key, value: kvt.value, timestamp: kvt.timestamp }; + const c = kvt.value.content; + if (typeof c !== 'string') return opts.decryptedOnly ? null : base; + if (!opts.private && !opts.decryptedOnly) return base; + let plain = null; + try { plain = ssbKeys.unbox(c, keys.private); } catch (_) { plain = null; } + if (!plain) return opts.decryptedOnly ? null : base; + return legacy({ key: kvt.key, value: { ...kvt.value, content: plain }, timestamp: kvt.timestamp, meta: { private: true, originalContent: c } }, { private: opts.private }); +}; + +const liveTap = (sbot, keys, opts, match) => { + const items = []; + let waiting = null; + let ended = null; + const remove = sbot.db.onMsgAdded((ev) => { + if (ended) return false; + const m = plainOf(ev && ev.kvt, keys, opts); + if (!m || (match && !match(m))) return; + items.push(m); + if (waiting) { const w = waiting; waiting = null; w(null, items.shift()); } + }, false); + return (abort, cb) => { + if (abort) { + if (!ended) { ended = abort; items.length = 0; remove(); } + return cb(abort); + } + if (items.length) return cb(null, items.shift()); + if (ended) return cb(ended); + waiting = cb; + }; +}; + +const withSync = (old$, live$, opts = {}) => { + const seen = new Set(); + const parts = []; + if (opts.old !== false) parts.push(pull(old$, pull.through((m) => { if (m && m.key) seen.add(m.key); }))); + if (opts.sync !== false) parts.push(pull.values([{ sync: true }])); + parts.push(pull(live$, pull.filter((m) => !(m && m.key && seen.has(m.key))))); + return cat(parts); +}; + +const legacy = (msg, opts = {}) => { + if (!msg || !msg.value || msg.sync) return msg; + const priv = msg.meta && msg.meta.private === true; + if (!priv) return { key: msg.key, value: msg.value, timestamp: msg.timestamp }; + if (!opts.private) return { key: msg.key, value: { ...msg.value, content: msg.meta.originalContent }, timestamp: msg.timestamp }; + return { + key: msg.key, + value: { ...msg.value, private: true, meta: { ...(msg.value.meta || {}), private: true, original: { content: msg.meta.originalContent } } }, + timestamp: msg.timestamp + }; +}; + +const isOpObject = (f) => f && typeof f === 'object' && !Array.isArray(f) && Object.keys(f).some(k => OPS.includes(k)); + +const matchValue = (v, f) => { + if (isOpObject(f)) { + for (const op of Object.keys(f)) { + const arg = f[op]; + if (op === '$gt' && !(v > arg)) return false; + if (op === '$gte' && !(v >= arg)) return false; + if (op === '$lt' && !(v < arg)) return false; + if (op === '$lte' && !(v <= arg)) return false; + if (op === '$in' && !(Array.isArray(arg) && arg.includes(v))) return false; + if (op === '$is' && typeof v !== arg) return false; + if (op === '$prefix' && !(typeof v === 'string' && v.startsWith(arg))) return false; + if (op === '$ne' && v === arg) return false; + if (op === '$truthy' && !v) return false; + if (op === '$eq' && v !== arg) return false; + } + return true; + } + if (f && typeof f === 'object' && !Array.isArray(f)) { + if (!v || typeof v !== 'object') return false; + return Object.keys(f).every(k => matchValue(v[k], f[k])); + } + if (Array.isArray(f)) return JSON.stringify(v) === JSON.stringify(f); + return v === f; +}; + +const formatter = (opts = {}) => { + if (opts.keys === false && opts.values !== false) return (m) => (m && m.sync) ? m : m.value; + if (opts.values === false) return (m) => (m && m.sync) ? m : m.key; + return (m) => m; +}; + +const matchFilter = (msg, filter) => { + if (!filter) return true; + return Object.keys(filter).every(k => k === 'dest' ? true : matchValue(msg[k], filter[k])); +}; + +const opsFor = (filter) => { + const ops = []; + const value = filter && filter.value; + const content = value && value.content; + const t = content && content.type; + if (typeof t === 'string') ops.push(type(t)); + else if (t && Array.isArray(t.$in) && t.$in.length) ops.push(or(...t.$in.map(x => type(x)))); + if (value && typeof value.author === 'string') ops.push(author(value.author)); + if (!ops.length) return null; + return ops.length === 1 ? ops[0] : and(...ops); +}; + +const filterOf = (filter) => filter ? (m) => matchFilter(m, filter) : null; + +module.exports = [ + { + manifest: { + createUserStream: 'source', + messagesByType: 'source', + links: 'source', + createLogStream: 'source', + get: 'async', + whoami: 'sync', + status: 'sync', + progress: 'sync', + rebuild: 'async' + }, + init(sbot, config) { + sbot.db.registerIndex(LinksIndex); + const linksIndex = () => sbot.db.getIndex('oasisLinks'); + + const pages = (ops, extra) => { + let seq = 0; + let size = FIRST_PAGE; + let total = Infinity; + let ended = false; + const next = (end, cb) => { + if (end) return cb(end); + if (ended || seq >= total) return cb(true); + sbot.db.query(...[ops ? where(ops) : null, ...extra, startFrom(seq), paginate(size), toCallback((err, answer) => { + if (err) return cb(err); + if (!answer || !answer.total || !answer.results || !answer.results.length) { ended = true; return cb(true); } + total = answer.total; + seq = answer.nextSeq; + size = Math.min(size * 2, LAST_PAGE); + cb(null, answer.results); + })].filter(Boolean)); + }; + return pull(next, pull.map((page) => pull.values(page)), pull.flatten()); + }; + + const query = (ops, extra, size) => size + ? sbot.db.query(...[ops ? where(ops) : null, ...extra, batch(size), toPullStream()].filter(Boolean)) + : pages(ops, extra); + + const stream = (ops, opts = {}) => { + const shape = () => pull.map(m => legacy(m, { private: opts.private })); + const boxed = () => opts.skipPrivate ? pull.filter(m => m.sync || typeof (m.value && m.value.content) !== 'string') : pull.through(); + const pass = () => opts.filter ? pull.filter(opts.filter) : pull.through(); + const limit = typeof opts.limit === 'number' && opts.limit >= 0 ? opts.limit : -1; + const order = [opts.arrival ? sortByArrival() : null, opts.reverse ? descending() : null].filter(Boolean); + const fmt = formatter(opts); + const size = limit >= 0 ? Math.max(1, Math.min(limit, FIRST_PAGE)) : undefined; + if (!opts.live) return pull(query(ops, order, size), shape(), boxed(), pass(), limit >= 0 ? pull.take(limit) : pull.through(), pull.map(fmt)); + const match = (m) => (!opts.skipPrivate || typeof (m.value && m.value.content) !== 'string') && (!opts.match || opts.match(m)) && (!opts.filter || opts.filter(m)); + const live$ = liveTap(sbot, config.keys, { private: opts.private, decryptedOnly: opts.decryptedOnly }, match); + const old$ = pull(query(ops, order, size), shape(), boxed(), pass(), limit >= 0 ? pull.take(limit) : pull.through()); + return pull(withSync(old$, live$, opts), pull.map(fmt)); + }; + + const bySeqs = (list, opts = {}) => { + const unique = [...new Set(list)]; + if (!unique.length) return pull.empty(); + return pull(query(seqs(unique), []), pull.map(m => legacy(m, { private: opts.private }))); + }; + + const linked = (dest, opts, cb) => { + sbot.db.onDrain('oasisLinks', () => { + const idx = linksIndex(); + if (!idx) return cb(null, []); + idx.getLinks(dest, (err, entries) => { + if (err) return cb(err); + const wanted = entries.filter(e => (!opts.rel || e.r === opts.rel) && (!opts.source || e.a === opts.source)); + pull(bySeqs(wanted.map(e => e.s), opts), pull.collect(cb)); + }); + }); + }; + + const liveLinked = (dest, opts) => liveTap(sbot, config.keys, { private: opts.private }, (m) => + linksOf(m.value && m.value.content).some(l => l.dest === dest && (!opts.rel || l.rel === opts.rel)) && (!opts.source || m.value.author === opts.source)); + + const sortLegacy = (arr, byTs, reverse) => { + const claimed = (m) => Number(m.value && m.value.timestamp) || 0; + const arrival = (m) => Number(m.timestamp) || 0; + const seq = (m) => Number(m.value && m.value.sequence) || 0; + arr.sort((a, b) => (byTs ? (claimed(a) - claimed(b)) : 0) || (arrival(a) - arrival(b)) || (seq(a) - seq(b))); + if (reverse) arr.reverse(); + return arr; + }; + + let drained = false; + try { sbot.db.onDrain('base', () => { drained = true; }); } catch (_) { drained = true; } + const status = () => { + const st = (sbot.db.getStatus && sbot.db.getStatus().value) || { log: 0, jit: {}, indexes: {}, progress: 1 }; + return { + ready: drained, + sync: { since: Number(st.log) || 0, plugins: { ...(st.indexes || {}) } }, + jit: { ...(st.jit || {}) }, + progress: typeof st.progress === 'number' ? st.progress : 1, + db: 'ssb-db2' + }; + }; + + const progress = () => { + const st = status(); + const target = st.sync.since; + return { indexes: { start: 0, current: Math.min(target, Math.round(st.progress * target)), target } }; + }; + + const rebuild = (cb) => { + if (typeof cb === 'function') return sbot.db.reset(cb); + return new Promise((resolve, reject) => sbot.db.reset((err) => err ? reject(err) : resolve())); + }; + + const createUserStream = (opts = {}) => { + const id = typeof opts === 'string' ? opts : opts.id; + if (!id) return pull.error(new Error('createUserStream: missing id')); + const seqOf = (m) => Number(m.value && m.value.sequence) || 0; + const filter = (opts.gt !== undefined || opts.gte !== undefined || opts.lt !== undefined || opts.lte !== undefined) + ? (m) => (opts.gt === undefined || seqOf(m) > opts.gt) && (opts.gte === undefined || seqOf(m) >= opts.gte) && (opts.lt === undefined || seqOf(m) < opts.lt) && (opts.lte === undefined || seqOf(m) <= opts.lte) + : null; + return stream(author(id), { reverse: opts.reverse === true, limit: opts.limit, live: opts.live === true, old: opts.old, sync: opts.sync, filter, match: (m) => m.value && m.value.author === id, private: opts.private === true, arrival: true, keys: opts.keys, values: opts.values }); + }; + + const messagesByType = (opts = {}) => { + const t = typeof opts === 'string' ? opts : opts.type; + if (!t) return pull.error(new Error('messagesByType: missing type')); + const o = typeof opts === 'string' ? {} : opts; + return stream(type(t), { reverse: o.reverse === true, limit: o.limit, live: o.live === true, old: o.old, sync: o.sync, skipPrivate: true, match: (m) => !!(m.value && m.value.content && m.value.content.type === t), arrival: true, keys: o.keys, values: o.values }); + }; + + const links = (opts = {}) => { + const src = defer.source(); + const toLink = (m) => opts.values ? m : { source: m.value.author, dest: opts.dest, rel: opts.rel, key: m.key }; + if (opts.dest) { + const live$ = opts.live ? liveLinked(opts.dest, { rel: opts.rel, source: opts.source }) : null; + linked(opts.dest, { rel: opts.rel, source: opts.source }, (err, arr) => { + if (err) { if (live$) live$(err, () => {}); return src.abort(err); } + const out = sortLegacy(arr, false, opts.reverse === true); + if (!live$) return src.resolve(pull(pull.values(out), pull.map(toLink))); + src.resolve(pull(withSync(pull.values(out), live$, opts), pull.map((m) => m.sync ? m : toLink(m)))); + }); + return src; + } + const ops = opts.source ? author(opts.source) : null; + src.resolve(pull( + stream(ops, { reverse: opts.reverse === true, arrival: true }), + pull.map(m => linksOf(m.value && m.value.content).filter(l => !opts.rel || l.rel === opts.rel).map(l => opts.values ? m : { source: m.value.author, dest: l.dest, rel: l.rel, key: m.key })), + pull.flatten() + )); + return src; + }; + + const createLogStream = (opts = {}) => { + const bound = (k) => opts[k] !== undefined && opts[k] !== null && Number.isFinite(Number(opts[k])); + const upper = [bound('lt') ? lt(Number(opts.lt), 'timestamp') : null, bound('lte') ? lte(Number(opts.lte), 'timestamp') : null].filter(Boolean); + const upperOps = upper.length ? (upper.length === 1 ? upper[0] : and(...upper)) : null; + const hasRange = ['gt', 'gte', 'lt', 'lte'].some(k => opts[k] !== undefined && opts[k] !== null); + const ts = (m) => Number(m && m.timestamp) || 0; + const inRange = (m) => !m || m.sync || typeof m !== 'object' || ( + (opts.gt === undefined || opts.gt === null || ts(m) > opts.gt) && + (opts.gte === undefined || opts.gte === null || ts(m) >= opts.gte) && + (opts.lt === undefined || opts.lt === null || ts(m) < opts.lt) && + (opts.lte === undefined || opts.lte === null || ts(m) <= opts.lte) + ); + const lower = Math.max(bound('gt') ? Number(opts.gt) : -Infinity, bound('gte') ? Number(opts.gte) : -Infinity); + if (lower > -Infinity && opts.live !== true) { + const floor = lower - ARRIVAL_SLACK_MS; + const src = defer.source(); + pull( + stream(upperOps, { reverse: true, arrival: true }), + pull.take((m) => !m || typeof m !== 'object' || ts(m) >= floor), + pull.filter(inRange), + pull.collect((err, arr) => { + if (err) return src.resolve(pull.error(err)); + const ordered = opts.reverse === true ? arr : arr.reverse(); + const limited = opts.limit > 0 ? ordered.slice(0, opts.limit) : ordered; + src.resolve(pull.values(limited)); + }) + ); + return src; + } + const shaped = stream(upperOps, { reverse: opts.reverse === true, limit: hasRange ? undefined : opts.limit, live: opts.live === true, old: opts.old, sync: opts.sync, arrival: true, keys: opts.keys, values: opts.values }); + if (!hasRange) return shaped; + const filtered = pull(shaped, pull.filter(inRange)); + return opts.limit > 0 ? pull(filtered, pull.take(opts.limit)) : filtered; + }; + + const get = (idOrObject, cb) => { + const isObj = !!(idOrObject && typeof idOrObject === 'object'); + const id = isObj ? idOrObject.id : idOrObject; + sbot.db.getMsg(id, (err, msg) => { + if (err) return cb(err); + const shaped = legacy(msg, { private: isObj && idOrObject.private === true }); + cb(null, isObj && idOrObject.meta ? shaped : shaped.value); + }); + }; + + const whoami = () => ({ id: sbot.id }); + + sbot._oasisDb2 = { stream, linked, liveLinked, bySeqs, sortLegacy, legacy, matchFilter, opsFor, filterOf, linksOf }; + return { createUserStream, messagesByType, links, createLogStream, get, whoami, status, progress, rebuild }; + } + }, + { + name: 'private', + version: '1.0.0', + manifest: { publish: 'async', unbox: 'sync', read: 'source' }, + init(sbot, config) { + return { + publish(content, recps, cb) { + const list = (Array.isArray(recps) ? recps : [recps]).map(r => typeof r === 'string' ? r : (r && r.link)).filter(Boolean); + if (!list.length) return cb(new Error('private.publish: no recipients')); + sbot.db.create({ content, recps: list, encryptionFormat: 'box' }, (err, kvt) => err ? cb(err) : cb(null, legacy(kvt))); + }, + unbox(msgOrData) { + if (typeof msgOrData === 'string') return ssbKeys.unbox(msgOrData, config.keys.private); + if (!msgOrData || !msgOrData.value || typeof msgOrData.value.content !== 'string') return undefined; + let plain = null; + try { plain = ssbKeys.unbox(msgOrData.value.content, config.keys.private); } catch (_) { plain = null; } + if (!plain) return undefined; + const v = msgOrData.value; + return { + key: msgOrData.key, + value: { previous: v.previous, author: v.author, sequence: v.sequence, timestamp: v.timestamp, hash: v.hash, content: plain, private: true }, + timestamp: msgOrData.timestamp + }; + }, + read(opts = {}) { + return sbot._oasisDb2.stream(isDecrypted('box'), { reverse: opts.reverse === true, limit: opts.limit, live: opts.live === true, old: opts.old, sync: opts.sync, private: true, decryptedOnly: true, arrival: true, keys: opts.keys, values: opts.values }); + } + }; + } + }, + { + name: 'activity', + version: '1.0.0', + manifest: { latest: 'async', tail: 'async' }, + init(sbot) { + sbot.db.registerIndex(ActivityIndex); + return { + latest(ids, opts, cb) { + if (typeof ids === 'function') { cb = ids; ids = null; opts = {}; } + if (typeof opts === 'function') { cb = opts; opts = {}; } + const idx = sbot.db.getIndex('oasisActivity'); + if (!idx) return cb(null, null); + let settled = false; + const settle = (ok) => { + if (settled) return; + settled = true; + clearTimeout(timer); + if (!ok && !(idx.offset.value >= 0)) return cb(null, null); + const out = {}; + if (Array.isArray(ids)) { for (const id of ids) if (idx.latest.has(id)) out[id] = idx.latest.get(id); } + else for (const [id, ts] of idx.latest) out[id] = ts; + cb(null, out); + }; + const timer = setTimeout(() => settle(false), ACTIVITY_DRAIN_MS); + const log = sbot.db.getLog(); + const read = () => { + const since = log.since.value; + if (!Number.isFinite(since)) return settle(idx.offset.value >= 0); + idx.whenReached(since, ACTIVITY_DRAIN_MS, settle); + }; + if (opts && opts.fresh === false) read(); + else log.onDrain(read); + }, + tail(limit, opts, cb) { + if (typeof limit === 'function') { cb = limit; limit = 1; opts = {}; } + if (typeof opts === 'function') { cb = opts; opts = {}; } + const want = Math.max(1, Math.min(Number(limit) || 1, RECENT_KEEP)); + const idx = sbot.db.getIndex('oasisActivity'); + if (!idx) return cb(null, null); + const log = sbot.db.getLog(); + const read = () => { + const since = log.since.value; + if (!Number.isFinite(since)) return cb(null, []); + idx.whenReached(since, ACTIVITY_DRAIN_MS, (ok) => { + const offsets = idx.recent; + if (!ok || !offsets.length || offsets[offsets.length - 1] !== since) return cb(null, null); + if (offsets.length < want && offsets[0] !== 0) return cb(null, null); + const picked = offsets.slice(-want).reverse(); + const out = new Array(picked.length); + let left = picked.length; + let failed = false; + picked.forEach((offset, i) => log.get(offset, (err, buf) => { + if (failed) return; + let m = null; + try { m = !err && buf ? bipf.decode(buf, 0) : null; } catch (_) { m = null; } + if (!m || !m.key || !m.value) { failed = true; return cb(null, null); } + out[i] = legacy(m); + if (--left === 0) cb(null, out); + })); + }); + }; + if (opts && opts.fresh === false) read(); + else log.onDrain(read); + } + }; + } + }, + { + name: 'backlinks', + version: '1.0.0', + manifest: { read: 'source' }, + init(sbot) { + return { + read(opts = {}) { + const { stream, linked, liveLinked, sortLegacy, matchFilter, opsFor } = sbot._oasisDb2; + const filter = (opts.query && opts.query[0] && opts.query[0].$filter) || {}; + const priv = opts.private === true; + const pass = (m) => matchFilter(m, filter); + if (!filter.dest) { + return stream(opsFor(filter), { reverse: opts.reverse === true, limit: opts.limit, live: opts.live === true, old: opts.old, sync: opts.sync, filter: pass, private: priv }); + } + const src = defer.source(); + const limit = typeof opts.limit === 'number' && opts.limit >= 0 ? opts.limit : -1; + const fmt = formatter(opts); + const live$ = opts.live ? pull(liveLinked(filter.dest, { private: priv }), pull.filter(pass)) : null; + const finish = (arr) => { + let out = sortLegacy(arr.filter(pass), opts.index === 'DTA', opts.reverse === true); + if (limit >= 0) out = out.slice(0, limit); + if (!live$) return src.resolve(pull(pull.values(out), pull.map(fmt))); + src.resolve(pull(withSync(pull.values(out), live$, opts), pull.map(fmt))); + }; + if (opts.old === false && opts.live) return finish([]), src; + linked(filter.dest, { private: priv }, (err, arr) => { + if (!err) return finish(arr); + if (live$) live$(err, () => {}); + src.abort(err); + }); + return src; + } + }; + } + }, + { + name: 'query', + version: '1.0.0', + manifest: { read: 'source' }, + init(sbot) { + return { + read(opts = {}) { + const { stream, opsFor, filterOf } = sbot._oasisDb2; + const filter = (opts.query && opts.query[0] && opts.query[0].$filter) || null; + return stream(opsFor(filter), { reverse: opts.reverse === true, limit: opts.limit, live: opts.live === true, old: opts.old, sync: opts.sync, filter: filterOf(filter), skipPrivate: true, keys: opts.keys, values: opts.values }); + } + }; + } + }, + { + name: 'search', + version: '1.0.0', + manifest: { query: 'source' }, + init(sbot) { + const textOf = (c) => ['text', 'title', 'description', 'body', 'name', 'summary'].map(k => c && typeof c[k] === 'string' ? c[k] : '').join('\n').toLowerCase(); + return { + query(opts = {}) { + const needle = String(opts.query || '').trim().toLowerCase(); + if (!needle) return pull.empty(); + const terms = needle.split(/\s+/).filter(Boolean); + const limit = typeof opts.limit === 'number' && opts.limit > 0 ? opts.limit : SEARCH_LIMIT; + return sbot._oasisDb2.stream(null, { + reverse: opts.reverse !== false, + limit, + skipPrivate: true, + arrival: true, + filter: (m) => { const c = m.value && m.value.content; if (!c || typeof c !== 'object') return false; const hay = textOf(c); return terms.every(t => hay.includes(t)); } + }); + } + }; + } + }, + { + name: 'tangle', + version: '1.0.0', + manifest: { branch: 'async' }, + init(sbot) { + return { + branch(rootKey, cb) { + if (typeof rootKey !== 'string' || !rootKey.startsWith('%')) return cb(new Error('tangle.branch requires a message key')); + sbot._oasisDb2.linked(rootKey, {}, (err, arr) => { + if (err) return cb(err); + const thread = arr.filter(m => m.value && m.value.content && typeof m.value.content === 'object' && m.value.content.root === rootKey); + cb(null, heads([{ key: rootKey }, ...thread])); + }); + } + }; + } + } +]; + +module.exports.LinksIndex = LinksIndex; +module.exports.legacy = legacy; +module.exports.matchFilter = matchFilter; diff --git a/src/server/lanRouter.js b/src/server/lanRouter.js new file mode 100644 index 0000000..a4c6079 --- /dev/null +++ b/src/server/lanRouter.js @@ -0,0 +1,97 @@ +const pull = require('./node_modules/pull-stream'); +const Ref = require('./node_modules/ssb-ref'); +const fs = require('fs'); +const path = require('path'); + +const staged = new Set(); + +function readOasisConfig() { + try { + const dir = process.env.OASIS_STATE_DIR || process.env.ssb_path || path.join(require('os').homedir(), '.ssb'); + const p = [path.join(dir, 'oasis', 'oasis-config.json'), path.join(dir, 'oasis-config.json')].find(f => fs.existsSync(f)); + return JSON.parse(fs.readFileSync(p || path.join(__dirname, '..', 'configs', 'oasis-config.json'), 'utf8')) || {}; + } catch (_) { return {}; } +} + +function stagePeer(ssb, address, key, eagerReplicate) { + if (!address || !key || key === ssb.id) return; + if (staged.has(address)) return; + staged.add(address); + let routed = false; + try { + if (ssb.conn && typeof ssb.conn.stage === 'function') { + ssb.conn.stage(address, { type: 'lan', key }); + routed = true; + } + } catch (_) {} + try { require('../models/viewer_filters').rememberLanPeers([key]); } catch (_) {} + if (!routed) { + try { + if (ssb.gossip && typeof ssb.gossip.add === 'function') { + ssb.gossip.add(address, 'local'); + } + } catch (_) {} + } + if (eagerReplicate) { + try { + if (ssb.ebt && typeof ssb.ebt.request === 'function') { + ssb.ebt.request(key, true); + } + } catch (_) {} + try { + if (ssb.replicate && typeof ssb.replicate.request === 'function') { + ssb.replicate.request(key, true); + } + } catch (_) {} + } +} + +function handleDiscovery(ssb, d, opts) { + if (!d || !d.address) return; + if (!d.verified && !opts.acceptUnverified) return; + let key = null; + try { key = Ref.getKeyFromAddress(d.address); } catch (_) {} + if (key) stagePeer(ssb, d.address, key, opts.eagerReplicate); +} + +function guardLan(ssb, isEnabled) { + const lan = ssb && ssb.lan; + if (!lan || typeof lan.start !== 'function' || lan.oasisGuarded) return false; + const start = lan.start.bind(lan); + lan.start = (...args) => { + if (!isEnabled()) return undefined; + try { return start(...args); } catch (_) { return undefined; } + }; + lan.oasisGuarded = true; + if (!isEnabled() && typeof lan.stop === 'function') { try { lan.stop(); } catch (_) {} } + return true; +} + +function startRouter(ssb, opts, isEnabled) { + if (!ssb.lan || typeof ssb.lan.discoveredPeers !== 'function') return; + if (isEnabled()) { try { ssb.lan.start(); } catch (_) {} } + else if (typeof ssb.lan.stop === 'function') { try { ssb.lan.stop(); } catch (_) {} } + pull( + ssb.lan.discoveredPeers(), + pull.drain(d => handleDiscovery(ssb, d, opts), () => {}) + ); +} + +module.exports = { + name: 'lanRouter', + version: '1.1.0', + manifest: {}, + init(ssb, config) { + const lanCfg = (config && config.lan) || {}; + const opts = { + acceptUnverified: lanCfg.acceptUnverified === true, + eagerReplicate: lanCfg.eagerReplicate === true + }; + const isEnabled = () => { const c = readOasisConfig(); return c.lanBroadcasting !== false && c.networkPaused !== true; }; + guardLan(ssb, isEnabled); + setImmediate(() => startRouter(ssb, opts, isEnabled)); + return {}; + } +}; + +module.exports.guardLan = guardLan; diff --git a/src/server/network_pause.js b/src/server/network_pause.js new file mode 100644 index 0000000..9b92677 --- /dev/null +++ b/src/server/network_pause.js @@ -0,0 +1,210 @@ +const fs = require('fs'); +const path = require('path'); + +const readOasisConfig = () => { + try { + const dir = process.env.OASIS_STATE_DIR || process.env.ssb_path || path.join(require('os').homedir(), '.ssb'); + const p = [path.join(dir, 'oasis', 'oasis-config.json'), path.join(dir, 'oasis-config.json')].find(f => fs.existsSync(f)); + return JSON.parse(fs.readFileSync(p || path.join(__dirname, '..', 'configs', 'oasis-config.json'), 'utf8')) || {}; + } catch (_) { return {}; } +}; + +module.exports = { + name: 'networkPause', + version: '1.0.0', + manifest: { pause: 'sync', resume: 'sync', paused: 'sync', lastErrors: 'sync' }, + init(server, config) { + server.oasisNetworkPaused = readOasisConfig().networkPaused === true || process.env.OASIS_NETWORK_PAUSED === '1'; + const refused = () => new Error('network paused'); + + if (server.auth && typeof server.auth.hook === 'function') { + server.auth.hook(function (fn, args) { + const cb = args[args.length - 1]; + if (server.oasisNetworkPaused && args[0] !== server.id && typeof cb === 'function') return cb(refused()); + return fn.apply(this, args); + }); + } + if (server.connect && typeof server.connect.hook === 'function') { + server.connect.hook(function (fn, args) { + const cb = args[args.length - 1]; + if (server.oasisNetworkPaused && typeof cb === 'function') return cb(refused()); + return fn.apply(this, args); + }); + } + + const closeAll = () => { + try { for (const [addr] of server.conn.query().peersInConnection()) server.conn.disconnect(addr, () => {}); } catch (_) {} + for (const id of Object.keys(server.peers || {})) { + if (id === server.id) continue; + for (const rpc of (server.peers[id] || []).slice()) { try { rpc.close(true, () => {}); } catch (_) {} } + } + }; + + const lastErrors = new Map(); + const keyOf = (ev) => { + const { canonicalKey } = require('../models/peer_health'); + const m = String((ev && ev.address) || '').match(/~shs:([A-Za-z0-9+/=_-]{43,44})/); + return canonicalKey((ev && ev.key) || (m ? m[1] : '')); + }; + const majorMinor = (v) => String(v || '').split('.').slice(0, 2).map(n => parseInt(n, 10) || 0); + const ownVersion = (() => { try { return String(require('./package.json').version || ''); } catch (_) { return ''; } })(); + const upToDate = (v) => { + const [a1, b1] = majorMinor(v), [a2, b2] = majorMinor(ownVersion); + return a1 > a2 || (a1 === a2 && b1 >= b2); + }; + const rank = { versions: new Map(), graph: {} }; + const refreshRank = () => { + const pull = require('pull-stream'); + const latest = new Map(); + const done = () => { + rank.versions = new Map([...latest].map(([k, m]) => [k, m.version])); + try { server.friends.graph((err, g) => { if (!err && g) rank.graph = g; }); } catch (_) {} + }; + try { + if (typeof server.messagesByType !== 'function') return done(); + pull(server.messagesByType({ type: 'oasisVersion' }), pull.drain((m) => { + const author = m && m.value && m.value.author; + const c = m && m.value && m.value.content; + if (!author || !c || typeof c.version !== 'string') return; + const ts = Number(m.value.timestamp) || 0; + const prev = latest.get(author); + if (!prev || prev.ts <= ts) latest.set(author, { version: c.version, ts }); + }, done)); + } catch (_) { done(); } + }; + const peerRank = (peer) => { + const key = peer && peer[1] && peer[1].key; + if (!key) return 0; + const follows = rank.graph[key] || {}; + const mine = rank.graph[server.id] || {}; + let total = 0, shared = 0; + for (const [k, v] of Object.entries(follows)) { + if (!(v >= 0) || k === key) continue; + total++; + if (mine[k] >= 0) shared++; + } + const v = rank.versions.get(key); + const tier = v ? (upToDate(v) ? 2 : 0) : (total > 0 ? 0 : 1); + return tier * 1e9 + Math.min(shared, 99999) * 1e4 + Math.min(total, 9999); + }; + server.oasisPeerRank = peerRank; + setImmediate(refreshRank); + const rankTimer = setInterval(refreshRank, 10 * 60 * 1000); + if (rankTimer.unref) rankTimer.unref(); + const fallbackAt = new Map(); + const tryOtherAddress = (key, failed, reason) => { + if (reason === 'paused' || server.oasisNetworkPaused) return; + if (Array.isArray(server.peers && server.peers[key]) && server.peers[key].length) return; + const now = Date.now(); + if (now - (fallbackAt.get(key) || 0) < 10 * 60 * 1000) return; + const { canonicalKey } = require('../models/peer_health'); + let entries = []; + try { entries = server.conn.dbPeers(); } catch (_) { return; } + const failedOnion = /^onion:/.test(String(failed || '')); + const alt = entries.find(([addr, data]) => addr && addr !== failed && data && canonicalKey(data.key) === key && /^onion:/.test(addr) !== failedOnion); + if (!alt) return; + fallbackAt.set(key, now); + try { server.conn.connect(alt[0], { ...(alt[1] || {}) }, () => {}); } catch (_) {} + }; + let listening = false; + const listen = () => { + if (listening) return; + try { + const { classifyNetError } = require('../models/peer_health'); + const hub = server.conn && typeof server.conn.hub === 'function' ? server.conn.hub() : null; + const stream = hub && typeof hub.listen === 'function' ? hub.listen() : null; + if (!stream) return; + listening = true; + require('pull-stream')(stream, require('pull-stream').drain((ev) => { + const key = ev && keyOf(ev); + if (!key) return; + if (ev.type === 'connected') lastErrors.delete(key); + else if (ev.type === 'connecting-failed') { + const reason = classifyNetError(ev.details, ev.address); + lastErrors.set(key, { reason, at: Date.now() }); + if (lastErrors.size > 500) lastErrors.delete(lastErrors.keys().next().value); + tryOtherAddress(key, ev.address, reason); + } + }, () => { listening = false; })); + } catch (_) { listening = false; } + }; + const listenTimer = setInterval(() => { listen(); if (listening) clearInterval(listenTimer); }, 1000); + if (listenTimer.unref) listenTimer.unref(); + + const readList = (file) => { try { const a = JSON.parse(fs.readFileSync(file, 'utf8')); return Array.isArray(a) ? a : []; } catch (_) { return []; } }; + const syncPeerBook = () => { + if (!server.conn || typeof server.conn.dbPeers !== 'function' || !config || !config.path) return; + const { canonicalKey } = require('../models/peer_health'); + let forgottenFile = null; + try { forgottenFile = require('../configs/state-manager').statePath('gossip_unfollowed.json'); } catch (_) {} + const forgotten = new Set((forgottenFile ? readList(forgottenFile) : []).map(e => e && canonicalKey(e.key)).filter(Boolean)); + let entries = []; + try { entries = server.conn.dbPeers(); } catch (_) { return; } + for (const [addr, data] of entries) { + const m = String(addr).match(/^(.*~shs:)([A-Za-z0-9+/]{43})$/); + if (!m) continue; + const padded = `${m[1]}${m[2]}=`; + try { + if (!server.conn.db().has(padded)) server.conn.remember(padded, { ...(data || {}) }); + else if (data && data.autoconnect === true && typeof server.conn.db().update === 'function') server.conn.db().update(padded, { autoconnect: true }); + server.conn.forget(addr); + } catch (_) {} + } + try { entries = server.conn.dbPeers(); } catch (_) { return; } + for (const [addr, data] of entries) { + const key = data && canonicalKey(data.key); + if (key && forgotten.has(key)) { try { server.conn.forget(addr); } catch (_) {} } + } + const chosen = new Map(); + for (const p of readList(path.join(config.path, 'gossip.json'))) { + const key = p && canonicalKey(p.key); + const host = p ? String(p.host || '').trim() : ''; + if (!key || key === server.id || forgotten.has(key) || !host) continue; + const onion = /\.onion$/i.test(host); + const prev = chosen.get(key); + if (!prev || (prev.onion && !onion)) chosen.set(key, { host, port: Number(p.port) || 8008, onion }); + } + for (const [key, p] of chosen) { + const addr = `${p.onion ? 'onion' : 'net'}:${p.host}:${p.port}~shs:${key.slice(1, -8)}`; + try { if (!server.conn.db().has(addr)) server.conn.remember(addr, { key, type: 'pub', autoconnect: true }); } catch (_) {} + } + const seeds = [].concat(config.seeds || [], (config.connections && config.connections.seeds) || []); + for (const seed of seeds) { + const addr = String(seed || '').trim(); + const m = addr.match(/~shs:([A-Za-z0-9+/]{43}=?)$/); + const key = m && canonicalKey(m[1]); + if (!key || key === server.id || forgotten.has(key)) continue; + try { if (!server.conn.db().has(addr)) server.conn.remember(addr, { key, type: 'pub', autoconnect: true }); } catch (_) {} + } + if (config.pub && typeof server.conn.db().update === 'function') { + try { entries = server.conn.dbPeers(); } catch (_) { return; } + for (const [addr, data] of entries) { + const key = data && canonicalKey(data.key); + if (!key || key === server.id || forgotten.has(key) || data.type !== 'pub' || data.autoconnect !== false) continue; + try { server.conn.db().update(addr, { autoconnect: true }); } catch (_) {} + } + } + }; + for (const ms of [5000, 30000]) { const t = setTimeout(syncPeerBook, ms); if (t.unref) t.unref(); } + const bookTimer = setInterval(syncPeerBook, 10 * 60 * 1000); + if (bookTimer.unref) bookTimer.unref(); + + return { + lastErrors() { return Object.fromEntries(lastErrors); }, + pause() { + server.oasisNetworkPaused = true; + try { server.conn.stop(); } catch (_) {} + try { if (server.lan && typeof server.lan.stop === 'function') server.lan.stop(); } catch (_) {} + closeAll(); + return true; + }, + resume() { + server.oasisNetworkPaused = false; + try { server.conn.start(); } catch (_) {} + try { if (server.lan && typeof server.lan.start === 'function') server.lan.start(); } catch (_) {} + return false; + }, + paused() { return !!server.oasisNetworkPaused; } + }; + } +}; diff --git a/src/server/package-lock.json b/src/server/package-lock.json index f8ea18b..734be1b 100644 --- a/src/server/package-lock.json +++ b/src/server/package-lock.json @@ -1,32 +1,27 @@ { "name": "@krakenslab/oasis", - "version": "0.8.7", + "version": "1.3.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@krakenslab/oasis", - "version": "0.7.8", + "version": "1.2.3", "hasInstallScript": true, "license": "AGPL-3.0", "dependencies": { "@koa/router": "^13.1.0", - "@open-rpc/client-js": "^1.8.1", - "@xenova/transformers": "^2.17.2", - "abstract-level": "^2.0.1", "archiver": "^7.0.1", "axios": "^1.10.0", - "cors": "^2.8.5", + "bencode": "^2.0.3", + "bipf": "^1.9.0", + "chloride": "^2.4.1", "debug": "^4.3.1", "dompurify": "^3.3.1", "env-paths": "^2.2.1", - "epidemic-broadcast-trees": "^9.0.4", - "express": "^5.1.0", "file-type": "^16.5.4", "highlight.js": "11.0.0", "hyperaxe": "^2.0.1", - "ip": "https://registry.npmjs.org/neoip/-/neoip-3.0.0.tgz", - "is-svg": "^4.4.0", "jsdom": "^28.0.0", "koa": "^2.7.0", "koa-body": "^6.0.1", @@ -36,90 +31,58 @@ "minimist": "^1.2.8", "module-alias": "^2.2.3", "moment": "^2.30.1", - "multiblob": "^1.13.0", - "multiserver": "^3.3.1", - "multiserver-address": "^1.0.1", - "muxrpc": "^8.0.0", - "muxrpc-validation": "^3.0.2", - "muxrpcli": "^3.1.2", - "node-llama-cpp": "^3.10.0", - "non-private-ip": "^2.2.0", "open": "^8.4.2", "openpgp": "^6.3.0", - "packet-stream": "^2.0.6", - "packet-stream-codec": "^1.2.0", - "pdfjs-dist": "^5.2.133", + "opusscript": "^0.1.1", "pretty-ms": "^7.0.1", "pull-abortable": "^4.1.1", "pull-cat": "~1.1.5", - "pull-file": "^1.0.0", - "pull-many": "~1.0.6", + "pull-defer": "^0.2.3", + "pull-level": "^2.0.4", "pull-paramap": "^1.2.2", "pull-pushable": "^2.2.0", "pull-sort": "^1.0.2", "pull-stream": "^3.7.0", - "punycode.js": "^2.3.1", "qrcode": "^1.5.4", "secret-stack": "^6.3.1", - "ssb-about": "^2.0.1", "ssb-autofollow": "^1.1.0", - "ssb-backlinks": "^2.1.1", "ssb-blobs": "^2.0.1", "ssb-box": "^1.0.1", "ssb-caps": "^1.0.1", + "ssb-classic": "^1.1.0", "ssb-client": "^4.9.0", "ssb-config": "^3.4.4", "ssb-conn": "6.0.3", - "ssb-conn-db": "^1.0.5", - "ssb-conn-hub": "^1.2.0", - "ssb-conn-staging": "^1.0.0", - "ssb-db": "^20.4.1", - "ssb-device-address": "^1.1.6", + "ssb-db2": "^6.3.3", "ssb-ebt": "^9.1.2", - "ssb-friend-pub": "^1.0.7", "ssb-friends": "^5.0.0", - "ssb-gossip": "^1.1.1", "ssb-invite": "^3.0.3", "ssb-invite-client": "^1.3.3", "ssb-keys": "^8.0.0", "ssb-lan": "^1.0.0", - "ssb-legacy-conn": "^1.0.17", - "ssb-links": "^3.0.10", "ssb-logging": "^1.0.0", - "ssb-markdown": "^3.6.0", "ssb-master": "^1.0.3", - "ssb-meme": "^1.1.0", "ssb-mentions": "^0.5.2", "ssb-msgs": "^5.2.0", "ssb-no-auth": "^1.0.0", "ssb-onion": "^1.0.0", - "ssb-partial-replication": "^3.0.1", - "ssb-plugins": "^1.0.2", - "ssb-private": "^1.1.0", - "ssb-query": "^2.4.5", "ssb-ref": "^2.16.0", "ssb-replication-scheduler": "^3.0.0", - "ssb-search": "^1.3.0", - "ssb-server": "file:packages/ssb-server", - "ssb-tangle": "^1.0.1", + "ssb-sort": "^1.1.3", "ssb-thread-schema": "^1.1.1", - "ssb-threads": "^10.0.4", "ssb-unix-socket": "^1.0.0", - "ssb-ws": "^6.2.3", - "util": "^0.12.5", + "ssb-validate": "^4.1.4", + "stream-to-pull-stream": "^1.7.3", + "telegram": "^2.26.22", "yargs": "^17.7.2" }, "bin": { "oasis": "npm run start" }, - "devDependencies": { - "nodemon": "^3.1.0" - }, "engines": { "node": "^10.0.0 || >=12.0.0" }, "optionalDependencies": { - "fsevents": "^2.3.2", "sharp": "^0.33.5" } }, @@ -131,6 +94,8 @@ }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", "license": "MIT", "engines": { "node": ">=10" @@ -189,177 +154,11 @@ "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", "license": "MIT" }, - "node_modules/@babel/code-frame": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", - "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.28.5", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/generator": { - "version": "7.29.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", - "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.29.0", - "@babel/types": "^7.29.0", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-environment-visitor": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz", - "integrity": "sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-function-name": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.7.tgz", - "integrity": "sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.24.7", - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-hoist-variables": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.7.tgz", - "integrity": "sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-split-export-declaration": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", - "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", - "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.29.0" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/template": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", - "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/parser": "^7.28.6", - "@babel/types": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.23.2", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", - "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.22.13", - "@babel/generator": "^7.23.0", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.23.0", - "@babel/types": "^7.23.0", - "debug": "^4.1.0", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - } + "node_modules/@cryptography/aes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@cryptography/aes/-/aes-0.1.1.tgz", + "integrity": "sha512-PcYz4FDGblO6tM2kSC+VzhhK62vml6k6/YAkiWtyPvrgJVfnDRoHGDtKn5UiaRRUrvUTTocBpvc2rRgTCqxjsg==", + "license": "GPL-3.0-or-later" }, "node_modules/@csstools/color-helpers": { "version": "6.0.1", @@ -506,36 +305,71 @@ }, "node_modules/@hapi/bourne": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@hapi/bourne/-/bourne-3.0.0.tgz", + "integrity": "sha512-Waj1cwPXJDucOib4a3bAISsKJVb15MKi9IvmTI/7ssVEm6sywXGjVJDhl6/umt1pK1ZS7PacXU3A1PmFKHEZ2w==", "license": "BSD-3-Clause" }, - "node_modules/@huggingface/jinja": { - "version": "0.5.5", - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/@img/sharp-libvips-linux-x64": { + "node_modules/@img/sharp-libvips-linux-arm64": { "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", + "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", "cpu": [ - "x64" + "arm64" ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ "linux" ], + "libc": [ + "glibc" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", + "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", + "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "libc": [ + "musl" + ], "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-libvips-linuxmusl-x64": { "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", + "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", "cpu": [ "x64" ], "license": "LGPL-3.0-or-later", - "optional": true, "os": [ "linux" ], @@ -543,13 +377,39 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", + "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "libc": [ + "glibc" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.0.4" + } + }, "node_modules/@img/sharp-linux-x64": { "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", + "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", "cpu": [ "x64" ], "license": "Apache-2.0", - "optional": true, "os": [ "linux" ], @@ -563,13 +423,39 @@ "@img/sharp-libvips-linux-x64": "1.0.4" } }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", + "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "libc": [ + "musl" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" + } + }, "node_modules/@img/sharp-linuxmusl-x64": { "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", + "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", "cpu": [ "x64" ], "license": "Apache-2.0", - "optional": true, "os": [ "linux" ], @@ -585,6 +471,8 @@ }, "node_modules/@isaacs/cliui": { "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", "license": "ISC", "dependencies": { "string-width": "^5.1.2", @@ -600,6 +488,8 @@ }, "node_modules/@isaacs/cliui/node_modules/ansi-regex": { "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "license": "MIT", "engines": { "node": ">=12" @@ -610,6 +500,8 @@ }, "node_modules/@isaacs/cliui/node_modules/strip-ansi": { "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" @@ -621,59 +513,10 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/@isaacs/fs-minipass": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", - "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", - "license": "ISC", - "dependencies": { - "minipass": "^7.0.4" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, "node_modules/@koa/router": { "version": "13.1.0", + "resolved": "https://registry.npmjs.org/@koa/router/-/router-13.1.0.tgz", + "integrity": "sha512-mNVu1nvkpSd8Q8gMebGbCkDWJ51ODetrFvLKYusej+V0ByD4btqHYnPIzTBLXnQMVUlm/oxVwqmWBY3zQfZilw==", "license": "MIT", "dependencies": { "http-errors": "^2.0.0", @@ -684,91 +527,10 @@ "node": ">= 18" } }, - "node_modules/@kwsites/file-exists": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", - "integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==", - "license": "MIT", - "dependencies": { - "debug": "^4.1.1" - } - }, - "node_modules/@kwsites/promise-deferred": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz", - "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==", - "license": "MIT" - }, - "node_modules/@ljharb/resumer": { - "version": "0.0.1", - "license": "MIT", - "dependencies": { - "@ljharb/through": "^2.3.9" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/@ljharb/through": { - "version": "2.3.14", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/@napi-rs/canvas": { - "version": "0.1.70", - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 10" - }, - "optionalDependencies": { - "@napi-rs/canvas-android-arm64": "0.1.70", - "@napi-rs/canvas-darwin-arm64": "0.1.70", - "@napi-rs/canvas-darwin-x64": "0.1.70", - "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.70", - "@napi-rs/canvas-linux-arm64-gnu": "0.1.70", - "@napi-rs/canvas-linux-arm64-musl": "0.1.70", - "@napi-rs/canvas-linux-riscv64-gnu": "0.1.70", - "@napi-rs/canvas-linux-x64-gnu": "0.1.70", - "@napi-rs/canvas-linux-x64-musl": "0.1.70", - "@napi-rs/canvas-win32-x64-msvc": "0.1.70" - } - }, - "node_modules/@napi-rs/canvas-linux-x64-gnu": { - "version": "0.1.70", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/canvas-linux-x64-musl": { - "version": "0.1.70", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, "node_modules/@noble/hashes": { "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", "license": "MIT", "engines": { "node": "^14.21.3 || >=16" @@ -777,521 +539,10 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/@node-llama-cpp/linux-arm64": { - "version": "3.16.2", - "resolved": "https://registry.npmjs.org/@node-llama-cpp/linux-arm64/-/linux-arm64-3.16.2.tgz", - "integrity": "sha512-CxzgPsS84wL3W5sZRgxP3c9iJKEW+USrak1SmX6EAJxW/v9QGzehvT6W/aR1FyfidiIyQtOp3ga0Gg/9xfJPGw==", - "cpu": [ - "arm64", - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@node-llama-cpp/linux-armv7l": { - "version": "3.16.2", - "resolved": "https://registry.npmjs.org/@node-llama-cpp/linux-armv7l/-/linux-armv7l-3.16.2.tgz", - "integrity": "sha512-9G6W/MkQ/DLwGmpcj143NQ50QJg5gQZfzVf5RYx77VczBqhgwkgYHILekYrOs4xanOeqeJ8jnOnQQSp1YaJZUg==", - "cpu": [ - "arm", - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@node-llama-cpp/linux-x64": { - "version": "3.16.2", - "resolved": "https://registry.npmjs.org/@node-llama-cpp/linux-x64/-/linux-x64-3.16.2.tgz", - "integrity": "sha512-OXYf8rVfoDyvN+YrfKk8F9An9a5GOxVIM8OcR1U911tc0oRNf8yfJrQ8KrM75R26gwq0Y6YZwVTP0vRCInwWOw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@node-llama-cpp/linux-x64-cuda": { - "version": "3.16.2", - "resolved": "https://registry.npmjs.org/@node-llama-cpp/linux-x64-cuda/-/linux-x64-cuda-3.16.2.tgz", - "integrity": "sha512-LTBQFqjin7tyrLNJz0XWTB5QAHDsZV71/qiiRRjXdBKSZHVVaPLfdgxypGu7ggPeBNsv+MckRXdlH5C7yMtE4A==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@node-llama-cpp/linux-x64-cuda-ext": { - "version": "3.16.2", - "resolved": "https://registry.npmjs.org/@node-llama-cpp/linux-x64-cuda-ext/-/linux-x64-cuda-ext-3.16.2.tgz", - "integrity": "sha512-47d9myCJauZyzAlN7IK1eIt/4CcBMslF+yHy4q+yJotD/RV/S6qRpK2kGn+ybtdVjkPGNCoPkHKcyla9iIVjbw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@node-llama-cpp/linux-x64-vulkan": { - "version": "3.16.2", - "resolved": "https://registry.npmjs.org/@node-llama-cpp/linux-x64-vulkan/-/linux-x64-vulkan-3.16.2.tgz", - "integrity": "sha512-HDLAw4ZhwJuhKuF6n4x520yZXAQZahUOXtCGvPubjfpmIOElKrfDvCVlRsthAP0JwcwINzIQlVys3boMIXfBgw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@node-llama-cpp/mac-arm64-metal": { - "version": "3.16.2", - "resolved": "https://registry.npmjs.org/@node-llama-cpp/mac-arm64-metal/-/mac-arm64-metal-3.16.2.tgz", - "integrity": "sha512-nEZ74qB0lUohF88yR741YUrUqz/qD+FJFzUTHj0FwxAynSZCjvwtzEDtavRlh3qd3yLD/0ChNn00/RQ54ISImw==", - "cpu": [ - "arm64", - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@node-llama-cpp/mac-x64": { - "version": "3.16.2", - "resolved": "https://registry.npmjs.org/@node-llama-cpp/mac-x64/-/mac-x64-3.16.2.tgz", - "integrity": "sha512-BjA+DgeDt+kRxVMV6kChb9XVXm7U5b90jUif7Z/s6ZXtOOnV6exrTM2W09kbSqAiNhZmctcVY83h2dwNTZ/yIw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@node-llama-cpp/win-arm64": { - "version": "3.16.2", - "resolved": "https://registry.npmjs.org/@node-llama-cpp/win-arm64/-/win-arm64-3.16.2.tgz", - "integrity": "sha512-XHNFQzUjYODtkZjIn4NbQVrBtGB9RI9TpisiALryqfrIqagQmjBh6dmxZWlt5uduKAfT7M2/2vrABGR490FACA==", - "cpu": [ - "arm64", - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@node-llama-cpp/win-x64": { - "version": "3.16.2", - "resolved": "https://registry.npmjs.org/@node-llama-cpp/win-x64/-/win-x64-3.16.2.tgz", - "integrity": "sha512-etrivzbyLNVhZlUosFW8JSL0OSiuKQf9qcI3dNdehD907sHquQbBJrG7lXcdL6IecvXySp3oAwCkM87VJ0b3Fg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@node-llama-cpp/win-x64-cuda": { - "version": "3.16.2", - "resolved": "https://registry.npmjs.org/@node-llama-cpp/win-x64-cuda/-/win-x64-cuda-3.16.2.tgz", - "integrity": "sha512-jStDELHrU3rKQMOk5Hs5bWEazyjE2hzHwpNf6SblOpaGkajM/HJtxEZoL0mLHJx5qeXs4oOVkr7AzuLy0WPpNA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@node-llama-cpp/win-x64-cuda-ext": { - "version": "3.16.2", - "resolved": "https://registry.npmjs.org/@node-llama-cpp/win-x64-cuda-ext/-/win-x64-cuda-ext-3.16.2.tgz", - "integrity": "sha512-sdv4Kzn9bOQWNBRvw6B/zcn8dQRfZhjIHv5AfDBIOfRlSCgjebFpBeYUoU4wZPpjr3ISwcqO5MEWsw+AbUdV3Q==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@node-llama-cpp/win-x64-vulkan": { - "version": "3.16.2", - "resolved": "https://registry.npmjs.org/@node-llama-cpp/win-x64-vulkan/-/win-x64-vulkan-3.16.2.tgz", - "integrity": "sha512-9xuHFCOhCQjZgQSFrk79EuSKn9nGWt/SAq/3wujQSQLtgp8jGdtZgwcmuDUoemInf10en2dcOmEt7t8dQdC3XA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@octokit/app": { - "version": "16.1.2", - "license": "MIT", - "dependencies": { - "@octokit/auth-app": "^8.1.2", - "@octokit/auth-unauthenticated": "^7.0.3", - "@octokit/core": "^7.0.6", - "@octokit/oauth-app": "^8.0.3", - "@octokit/plugin-paginate-rest": "^14.0.0", - "@octokit/types": "^16.0.0", - "@octokit/webhooks": "^14.0.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/auth-app": { - "version": "8.2.0", - "license": "MIT", - "dependencies": { - "@octokit/auth-oauth-app": "^9.0.3", - "@octokit/auth-oauth-user": "^6.0.2", - "@octokit/request": "^10.0.6", - "@octokit/request-error": "^7.0.2", - "@octokit/types": "^16.0.0", - "toad-cache": "^3.7.0", - "universal-github-app-jwt": "^2.2.0", - "universal-user-agent": "^7.0.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/auth-oauth-app": { - "version": "9.0.3", - "license": "MIT", - "dependencies": { - "@octokit/auth-oauth-device": "^8.0.3", - "@octokit/auth-oauth-user": "^6.0.2", - "@octokit/request": "^10.0.6", - "@octokit/types": "^16.0.0", - "universal-user-agent": "^7.0.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/auth-oauth-device": { - "version": "8.0.3", - "license": "MIT", - "dependencies": { - "@octokit/oauth-methods": "^6.0.2", - "@octokit/request": "^10.0.6", - "@octokit/types": "^16.0.0", - "universal-user-agent": "^7.0.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/auth-oauth-user": { - "version": "6.0.2", - "license": "MIT", - "dependencies": { - "@octokit/auth-oauth-device": "^8.0.3", - "@octokit/oauth-methods": "^6.0.2", - "@octokit/request": "^10.0.6", - "@octokit/types": "^16.0.0", - "universal-user-agent": "^7.0.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/auth-token": { - "version": "6.0.0", - "license": "MIT", - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/auth-unauthenticated": { - "version": "7.0.3", - "license": "MIT", - "dependencies": { - "@octokit/request-error": "^7.0.2", - "@octokit/types": "^16.0.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/core": { - "version": "7.0.6", - "license": "MIT", - "dependencies": { - "@octokit/auth-token": "^6.0.0", - "@octokit/graphql": "^9.0.3", - "@octokit/request": "^10.0.6", - "@octokit/request-error": "^7.0.2", - "@octokit/types": "^16.0.0", - "before-after-hook": "^4.0.0", - "universal-user-agent": "^7.0.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/endpoint": { - "version": "11.0.2", - "license": "MIT", - "dependencies": { - "@octokit/types": "^16.0.0", - "universal-user-agent": "^7.0.2" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/graphql": { - "version": "9.0.3", - "license": "MIT", - "dependencies": { - "@octokit/request": "^10.0.6", - "@octokit/types": "^16.0.0", - "universal-user-agent": "^7.0.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/oauth-app": { - "version": "8.0.3", - "license": "MIT", - "dependencies": { - "@octokit/auth-oauth-app": "^9.0.2", - "@octokit/auth-oauth-user": "^6.0.1", - "@octokit/auth-unauthenticated": "^7.0.2", - "@octokit/core": "^7.0.5", - "@octokit/oauth-authorization-url": "^8.0.0", - "@octokit/oauth-methods": "^6.0.1", - "@types/aws-lambda": "^8.10.83", - "universal-user-agent": "^7.0.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/oauth-authorization-url": { - "version": "8.0.0", - "license": "MIT", - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/oauth-methods": { - "version": "6.0.2", - "license": "MIT", - "dependencies": { - "@octokit/oauth-authorization-url": "^8.0.0", - "@octokit/request": "^10.0.6", - "@octokit/request-error": "^7.0.2", - "@octokit/types": "^16.0.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/openapi-types": { - "version": "27.0.0", - "license": "MIT" - }, - "node_modules/@octokit/openapi-webhooks-types": { - "version": "12.1.0", - "license": "MIT" - }, - "node_modules/@octokit/plugin-paginate-graphql": { - "version": "6.0.0", - "license": "MIT", - "engines": { - "node": ">= 20" - }, - "peerDependencies": { - "@octokit/core": ">=6" - } - }, - "node_modules/@octokit/plugin-paginate-rest": { - "version": "14.0.0", - "license": "MIT", - "dependencies": { - "@octokit/types": "^16.0.0" - }, - "engines": { - "node": ">= 20" - }, - "peerDependencies": { - "@octokit/core": ">=6" - } - }, - "node_modules/@octokit/plugin-rest-endpoint-methods": { - "version": "17.0.0", - "license": "MIT", - "dependencies": { - "@octokit/types": "^16.0.0" - }, - "engines": { - "node": ">= 20" - }, - "peerDependencies": { - "@octokit/core": ">=6" - } - }, - "node_modules/@octokit/plugin-retry": { - "version": "8.0.3", - "license": "MIT", - "dependencies": { - "@octokit/request-error": "^7.0.2", - "@octokit/types": "^16.0.0", - "bottleneck": "^2.15.3" - }, - "engines": { - "node": ">= 20" - }, - "peerDependencies": { - "@octokit/core": ">=7" - } - }, - "node_modules/@octokit/plugin-throttling": { - "version": "11.0.3", - "license": "MIT", - "dependencies": { - "@octokit/types": "^16.0.0", - "bottleneck": "^2.15.3" - }, - "engines": { - "node": ">= 20" - }, - "peerDependencies": { - "@octokit/core": "^7.0.0" - } - }, - "node_modules/@octokit/request": { - "version": "10.0.7", - "license": "MIT", - "dependencies": { - "@octokit/endpoint": "^11.0.2", - "@octokit/request-error": "^7.0.2", - "@octokit/types": "^16.0.0", - "fast-content-type-parse": "^3.0.0", - "universal-user-agent": "^7.0.2" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/request-error": { - "version": "7.1.0", - "license": "MIT", - "dependencies": { - "@octokit/types": "^16.0.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/types": { - "version": "16.0.0", - "license": "MIT", - "dependencies": { - "@octokit/openapi-types": "^27.0.0" - } - }, - "node_modules/@octokit/webhooks": { - "version": "14.2.0", - "license": "MIT", - "dependencies": { - "@octokit/openapi-webhooks-types": "12.1.0", - "@octokit/request-error": "^7.0.0", - "@octokit/webhooks-methods": "^6.0.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/webhooks-methods": { - "version": "6.0.0", - "license": "MIT", - "engines": { - "node": ">= 20" - } - }, - "node_modules/@open-rpc/client-js": { - "version": "1.8.1", - "license": "Apache-2.0", - "dependencies": { - "isomorphic-fetch": "^3.0.0", - "isomorphic-ws": "^5.0.0", - "strict-event-emitter-types": "^2.0.0", - "ws": "^7.0.0" - } - }, "node_modules/@paralleldrive/cuid2": { "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.2.2.tgz", + "integrity": "sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==", "license": "MIT", "dependencies": { "@noble/hashes": "^1.1.5" @@ -1299,130 +550,17 @@ }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", "license": "MIT", - "optional": true, "engines": { "node": ">=14" } }, - "node_modules/@protobufjs/aspromise": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", - "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/base64": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/codegen": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", - "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/eventemitter": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", - "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/fetch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", - "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", - "license": "BSD-3-Clause", - "dependencies": { - "@protobufjs/aspromise": "^1.1.1", - "@protobufjs/inquire": "^1.1.0" - } - }, - "node_modules/@protobufjs/float": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", - "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/inquire": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.1.tgz", - "integrity": "sha512-mnzgDV26ueAvk7rsbt9L7bE0SuAoqyuys/sMMrmVcN5x9VsxpcG3rqAUSgDyLp0UZlmNfIbQ4fHfCtreVBk8Ew==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/path": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", - "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/pool": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", - "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/utf8": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz", - "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==", - "license": "BSD-3-Clause" - }, - "node_modules/@reflink/reflink": { - "version": "0.1.19", - "resolved": "https://registry.npmjs.org/@reflink/reflink/-/reflink-0.1.19.tgz", - "integrity": "sha512-DmCG8GzysnCZ15bres3N5AHCmwBwYgp0As6xjhQ47rAUTUXxJiK+lLUxaGsX3hd/30qUpVElh05PbGuxRPgJwA==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 10" - }, - "optionalDependencies": { - "@reflink/reflink-darwin-arm64": "0.1.19", - "@reflink/reflink-darwin-x64": "0.1.19", - "@reflink/reflink-linux-arm64-gnu": "0.1.19", - "@reflink/reflink-linux-arm64-musl": "0.1.19", - "@reflink/reflink-linux-x64-gnu": "0.1.19", - "@reflink/reflink-linux-x64-musl": "0.1.19", - "@reflink/reflink-win32-arm64-msvc": "0.1.19", - "@reflink/reflink-win32-x64-msvc": "0.1.19" - } - }, - "node_modules/@reflink/reflink-linux-x64-gnu": { - "version": "0.1.19", - "resolved": "https://registry.npmjs.org/@reflink/reflink-linux-x64-gnu/-/reflink-linux-x64-gnu-0.1.19.tgz", - "integrity": "sha512-jbI8jvuYCaA3MVUdu8vLoLAFqC+iNMpiSuLbxlAgg7x3K5bsS8nOpTRnkLF7vISJ+rVR8W+7ThXlXlUQ93ulkw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@reflink/reflink-linux-x64-musl": { - "version": "0.1.19", - "resolved": "https://registry.npmjs.org/@reflink/reflink-linux-x64-musl/-/reflink-linux-x64-musl-0.1.19.tgz", - "integrity": "sha512-e9FBWDe+lv7QKAwtKOt6A2W/fyy/aEEfr0g6j/hWzvQcrzHCsz07BNQYlNOjTfeytrtLU7k449H1PI95jA4OjQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, "node_modules/@sammacbeth/random-access-idb-mutable-file": { "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@sammacbeth/random-access-idb-mutable-file/-/random-access-idb-mutable-file-0.1.1.tgz", + "integrity": "sha512-jHnpuu2qtFgwCmhgrpCCk3/hU3XqXTqhidh4XmcTijkVsGwh1c2T0+r2hkHs1PRfsxeimx8qDAotphpRoYB2eg==", "license": "MIT", "dependencies": { "buffer": "5.1.0", @@ -1431,6 +569,8 @@ }, "node_modules/@sammacbeth/random-access-idb-mutable-file/node_modules/buffer": { "version": "5.1.0", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.1.0.tgz", + "integrity": "sha512-YkIRgwsZwJWTnyQrsBTWefizHh+8GYj3kbL1BTiAQ/9pwpino0G7B2gp5tx/FUBqUlvtxV85KNR3mwfAtv15Yw==", "license": "MIT", "dependencies": { "base64-js": "^1.0.2", @@ -1439,28 +579,17 @@ }, "node_modules/@sammacbeth/random-access-idb-mutable-file/node_modules/random-access-storage": { "version": "1.3.0", + "resolved": "https://registry.npmjs.org/random-access-storage/-/random-access-storage-1.3.0.tgz", + "integrity": "sha512-pdS9Mcb9TB7oICypPRALlheaSuszuAKmLVEPKJMuYor7R/zDuHh5ALuQoS+ox31XRwQUL+tDwWH2GPdyspwelA==", "license": "MIT", "dependencies": { "inherits": "^2.0.3" } }, - "node_modules/@simple-git/args-pathspec": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@simple-git/args-pathspec/-/args-pathspec-1.0.3.tgz", - "integrity": "sha512-ngJMaHlsWDTfjyq9F3VIQ8b7NXbBLq5j9i5bJ6XLYtD6qlDXT7fdKY2KscWWUF8t18xx052Y/PUO1K1TRc9yKA==", - "license": "MIT" - }, - "node_modules/@simple-git/argv-parser": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@simple-git/argv-parser/-/argv-parser-1.1.1.tgz", - "integrity": "sha512-Q9lBcfQ+VQCpQqGJFHe5yooOS5hGdLFFbJ5R+R5aDsnkPCahtn1hSkMcORX65J2Z5lxSkD0lQorMsncuBQxYUw==", - "license": "MIT", - "dependencies": { - "@simple-git/args-pathspec": "^1.0.3" - } - }, "node_modules/@tangle/graph": { "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@tangle/graph/-/graph-3.2.0.tgz", + "integrity": "sha512-I92Prubc9XVRmMZkshMr2u+REuwqhDQYcFtZuHmY8yP6TVpeDmWRUZY5AKCWYdeA58igTPUbPCpvLT9iRXiRBA==", "license": "LGPL-3.0-only", "dependencies": { "lodash.clone": "^4.5.0", @@ -1469,6 +598,8 @@ }, "node_modules/@tangle/overwrite-fields": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@tangle/overwrite-fields/-/overwrite-fields-2.0.3.tgz", + "integrity": "sha512-0VKHs1czriZOpVgc1aZve3lwpwgw+1+/P/wis2hH3FbACbfIYByIU1EgykVKeu/HbE3nRWLip83llFbr6Rjq4A==", "license": "LGPL-3.0-or-later", "dependencies": { "is-my-json-valid": "^2.20.5", @@ -1477,6 +608,8 @@ }, "node_modules/@tangle/reduce": { "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@tangle/reduce/-/reduce-5.0.5.tgz", + "integrity": "sha512-+GRGgvzt57fZjyGCamEc3uyh8onHlHYxuFF4HhRY4WDk9aky+WUBa8YSeFKANnGpBrptdzQuo3+VFIYucbpS3w==", "license": "LGPL-3.0-only", "dependencies": { "@tangle/graph": "^3.2.0" @@ -1484,42 +617,33 @@ }, "node_modules/@tangle/strategy": { "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@tangle/strategy/-/strategy-4.1.2.tgz", + "integrity": "sha512-P5qGsPuAMqZx/zJA20nwpqD3A5aGDnmjEbp221jRTClI+NSU/spcqkryOP7iH8UCRWiuzjWHA9To5ymw/xOsCA==", "license": "LGPL-3.0-only", "dependencies": { "is-my-json-valid": "^2.20.5", "lodash.isequal": "^4.5.0" } }, - "node_modules/@tinyhttp/content-disposition": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/@tinyhttp/content-disposition/-/content-disposition-2.2.4.tgz", - "integrity": "sha512-5Kc5CM2Ysn3vTTArBs2vESUt0AQiWZA86yc1TI3B+lxXmtEq133C1nxXNOgnzhrivdPZIh3zLj5gDnZjoLL5GA==", - "license": "MIT", - "engines": { - "node": ">=12.17.0" - }, - "funding": { - "type": "individual", - "url": "https://github.com/tinyhttp/tinyhttp?sponsor=1" - } - }, "node_modules/@tokenizer/token": { "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", "license": "MIT" }, "node_modules/@types/accepts": { "version": "1.3.7", + "resolved": "https://registry.npmjs.org/@types/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Pay9fq2lM2wXPWbteBsRAGiWH2hig4ZE2asK+mm7kUzlxRTfL961rj89I6zV/E3PcIkDqyuBEcMxFT7rccugeQ==", "license": "MIT", "dependencies": { "@types/node": "*" } }, - "node_modules/@types/aws-lambda": { - "version": "8.10.160", - "license": "MIT" - }, "node_modules/@types/body-parser": { "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", "license": "MIT", "dependencies": { "@types/connect": "*", @@ -1528,6 +652,8 @@ }, "node_modules/@types/co-body": { "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@types/co-body/-/co-body-6.1.3.tgz", + "integrity": "sha512-UhuhrQ5hclX6UJctv5m4Rfp52AfG9o9+d9/HwjxhVB5NjXxr5t9oKgJxN8xRHgr35oo8meUEHUPFWiKg6y71aA==", "license": "MIT", "dependencies": { "@types/node": "*", @@ -1536,6 +662,8 @@ }, "node_modules/@types/connect": { "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", "license": "MIT", "dependencies": { "@types/node": "*" @@ -1543,10 +671,14 @@ }, "node_modules/@types/content-disposition": { "version": "0.5.9", + "resolved": "https://registry.npmjs.org/@types/content-disposition/-/content-disposition-0.5.9.tgz", + "integrity": "sha512-8uYXI3Gw35MhiVYhG3s295oihrxRyytcRHjSjqnqZVDDy/xcGBRny7+Xj1Wgfhv5QzRtN2hB2dVRBUX9XW3UcQ==", "license": "MIT" }, "node_modules/@types/cookies": { "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@types/cookies/-/cookies-0.9.1.tgz", + "integrity": "sha512-E/DPgzifH4sM1UMadJMWd6mO2jOd4g1Ejwzx8/uRCDpJis1IrlyQEcGAYEomtAqRYmD5ORbNXMeI9U0RiVGZbg==", "license": "MIT", "dependencies": { "@types/connect": "*", @@ -1557,6 +689,8 @@ }, "node_modules/@types/express": { "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.3.tgz", + "integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==", "license": "MIT", "dependencies": { "@types/body-parser": "*", @@ -1566,6 +700,8 @@ }, "node_modules/@types/express-serve-static-core": { "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.6.tgz", + "integrity": "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==", "license": "MIT", "dependencies": { "@types/node": "*", @@ -1576,6 +712,8 @@ }, "node_modules/@types/formidable": { "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/formidable/-/formidable-2.0.6.tgz", + "integrity": "sha512-L4HcrA05IgQyNYJj6kItuIkXrInJvsXTPC5B1i64FggWKKqSL+4hgt7asiSNva75AoLQjq29oPxFfU4GAQ6Z2w==", "license": "MIT", "dependencies": { "@types/node": "*" @@ -1583,18 +721,26 @@ }, "node_modules/@types/http-assert": { "version": "1.5.6", + "resolved": "https://registry.npmjs.org/@types/http-assert/-/http-assert-1.5.6.tgz", + "integrity": "sha512-TTEwmtjgVbYAzZYWyeHPrrtWnfVkm8tQkP8P21uQifPgMRgjrow3XDEYqucuC8SKZJT7pUnhU/JymvjggxO9vw==", "license": "MIT" }, "node_modules/@types/http-errors": { "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", "license": "MIT" }, "node_modules/@types/keygrip": { "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/keygrip/-/keygrip-1.0.6.tgz", + "integrity": "sha512-lZuNAY9xeJt7Bx4t4dx0rYCDqGPW8RXhQZK1td7d4H6E9zYbLoOtjBvfwdTKpsyxQI/2jv+armjX/RW+ZNpXOQ==", "license": "MIT" }, "node_modules/@types/koa": { "version": "2.15.0", + "resolved": "https://registry.npmjs.org/@types/koa/-/koa-2.15.0.tgz", + "integrity": "sha512-7QFsywoE5URbuVnG3loe03QXuGajrnotr3gQkXcEBShORai23MePfFYdhz90FEtBBpkyIYQbVD+evKtloCgX3g==", "license": "MIT", "dependencies": { "@types/accepts": "*", @@ -1609,23 +755,23 @@ }, "node_modules/@types/koa-compose": { "version": "3.2.8", + "resolved": "https://registry.npmjs.org/@types/koa-compose/-/koa-compose-3.2.8.tgz", + "integrity": "sha512-4Olc63RY+MKvxMwVknCUDhRQX1pFQoBZ/lXcRLP69PQkEpze/0cr8LNqJQe5NFb/b19DWi2a5bTi2VAlQzhJuA==", "license": "MIT", "dependencies": { "@types/koa": "*" } }, - "node_modules/@types/long": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", - "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==", - "license": "MIT" - }, "node_modules/@types/mime": { "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", "license": "MIT" }, "node_modules/@types/node": { "version": "22.15.30", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.30.tgz", + "integrity": "sha512-6Q7lr06bEHdlfplU6YRbgG1SFBdlsfNC4/lX+SkhiTs0cpJkOElmWls8PxDFv4yY/xKb8Y6SO0OmSX4wgqTZbA==", "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -1633,14 +779,20 @@ }, "node_modules/@types/qs": { "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", "license": "MIT" }, "node_modules/@types/range-parser": { "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", "license": "MIT" }, "node_modules/@types/send": { "version": "0.17.5", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz", + "integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==", "license": "MIT", "dependencies": { "@types/mime": "^1", @@ -1649,6 +801,8 @@ }, "node_modules/@types/serve-static": { "version": "1.15.8", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.8.tgz", + "integrity": "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==", "license": "MIT", "dependencies": { "@types/http-errors": "*", @@ -1660,63 +814,12 @@ "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", - "license": "MIT", - "optional": true - }, - "node_modules/@xenova/transformers": { - "version": "2.17.2", - "resolved": "https://registry.npmjs.org/@xenova/transformers/-/transformers-2.17.2.tgz", - "integrity": "sha512-lZmHqzrVIkSvZdKZEx7IYY51TK0WDrC8eR0c5IMnBsO8di8are1zzw8BlLhyO2TklZKLN5UffNGs1IJwT6oOqQ==", - "license": "Apache-2.0", - "dependencies": { - "@huggingface/jinja": "^0.2.2", - "onnxruntime-web": "1.14.0", - "sharp": "^0.32.0" - }, - "optionalDependencies": { - "onnxruntime-node": "1.14.0" - } - }, - "node_modules/@xenova/transformers/node_modules/@huggingface/jinja": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@huggingface/jinja/-/jinja-0.2.2.tgz", - "integrity": "sha512-/KPde26khDUIPkTGU82jdtTW9UAuvUTumCAbFs/7giR0SxsvZC4hru51PBvpijH6BVkHcROcvZM/lpy5h1jRRA==", - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/@xenova/transformers/node_modules/node-addon-api": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", - "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==", "license": "MIT" }, - "node_modules/@xenova/transformers/node_modules/sharp": { - "version": "0.32.6", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.32.6.tgz", - "integrity": "sha512-KyLTWwgcR9Oe4d9HwCwNM2l7+J0dUQwn/yf7S0EnTtb0eVS4RxO0eUSvxPtzT4F3SY+C4K6fqdv/DO27sJ/v/w==", - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "color": "^4.2.3", - "detect-libc": "^2.0.2", - "node-addon-api": "^6.1.0", - "prebuild-install": "^7.1.1", - "semver": "^7.5.4", - "simple-get": "^4.0.1", - "tar-fs": "^3.0.4", - "tunnel-agent": "^0.6.0" - }, - "engines": { - "node": ">=14.15.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, "node_modules/abort-controller": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", "license": "MIT", "dependencies": { "event-target-shim": "^5.0.0" @@ -1725,23 +828,10 @@ "node": ">=6.5" } }, - "node_modules/abstract-level": { - "version": "2.0.2", - "license": "MIT", - "dependencies": { - "buffer": "^6.0.3", - "is-buffer": "^2.0.5", - "level-supports": "^6.0.0", - "level-transcoder": "^1.0.1", - "maybe-combine-errors": "^1.0.0", - "module-error": "^1.0.1" - }, - "engines": { - "node": ">=16" - } - }, "node_modules/abstract-leveldown": { "version": "6.2.3", + "resolved": "https://registry.npmjs.org/abstract-leveldown/-/abstract-leveldown-6.2.3.tgz", + "integrity": "sha512-BsLm5vFMRUrrLeCcRc+G0t2qOaTzpoJQLOubq2XM72eNpjF5UdU5o/5NvlNhx95XHcAvcl8OMXr4mlg/fRgUXQ==", "license": "MIT", "dependencies": { "buffer": "^5.5.0", @@ -1756,6 +846,8 @@ }, "node_modules/abstract-leveldown/node_modules/buffer": { "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", "funding": [ { "type": "github", @@ -1778,6 +870,8 @@ }, "node_modules/abstract-leveldown/node_modules/level-supports": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/level-supports/-/level-supports-1.0.1.tgz", + "integrity": "sha512-rXM7GYnW8gsl1vedTJIbzOrRv85c/2uCMpiiCzO2fndd06U/kUXEEU9evYn4zFggBOg36IsBW8LzqIpETwwQzg==", "license": "MIT", "dependencies": { "xtend": "^4.0.2" @@ -1788,6 +882,8 @@ }, "node_modules/accepts": { "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", "license": "MIT", "dependencies": { "mime-types": "~2.1.34", @@ -1808,6 +904,8 @@ }, "node_modules/aligned-block-file": { "version": "1.2.2", + "resolved": "https://registry.npmjs.org/aligned-block-file/-/aligned-block-file-1.2.2.tgz", + "integrity": "sha512-2Sy0hWhifVb8ycNFJgicL8fDPL2Ct1r62XOVxXnykn36z22MPZwnQlCmB2viQlY/lwfuO67GaQjUZ0rJgdVP7Q==", "license": "MIT", "dependencies": { "hashlru": "^2.1.0", @@ -1820,6 +918,8 @@ }, "node_modules/aligned-block-file/node_modules/mkdirp": { "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "license": "MIT", "dependencies": { "minimist": "^1.2.6" @@ -1828,20 +928,10 @@ "mkdirp": "bin/cmd.js" } }, - "node_modules/ansi-escapes": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-6.2.1.tgz", - "integrity": "sha512-4nJ3yixlEthEJ9Rk4vPcdBRkZvQZlYyu8j4/Mqz5sgIkddmEnH2Yj2ZrnP9S3tQOvSNRUIgVNF/1yPpRAGNRig==", - "license": "MIT", - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/ansi-regex": { "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "license": "MIT", "engines": { "node": ">=8" @@ -1849,6 +939,8 @@ }, "node_modules/ansi-styles": { "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -1860,34 +952,16 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/anymatch": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.2.tgz", - "integrity": "sha512-0XNayC8lTHQ2OI8aljNCN3sSx6hsr/1+rlcDAotXJR7C1oZZHCNsfpbKwMjRA3Uqb5tF1Rae2oloTr4xpq+WjA==", - "license": "ISC", - "dependencies": { - "micromatch": "^2.1.5", - "normalize-path": "^2.0.0" - } - }, - "node_modules/anymatch/node_modules/normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==", - "license": "MIT", - "dependencies": { - "remove-trailing-separator": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/append-batch": { "version": "0.0.2", + "resolved": "https://registry.npmjs.org/append-batch/-/append-batch-0.0.2.tgz", + "integrity": "sha512-v1lD0bKqM4MTtGzapIx6qCGcwbVJyKaTmbzIzA6kpnVwTY1E+VNMeYoOvAywl+hXEEF4CcqkrH/4iFMbzM8XXw==", "license": "MIT" }, "node_modules/archiver": { "version": "7.0.1", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz", + "integrity": "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==", "license": "MIT", "dependencies": { "archiver-utils": "^5.0.2", @@ -1904,6 +978,8 @@ }, "node_modules/archiver-utils": { "version": "5.0.2", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-5.0.2.tgz", + "integrity": "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==", "license": "MIT", "dependencies": { "glob": "^10.0.0", @@ -1918,38 +994,10 @@ "node": ">= 14" } }, - "node_modules/arr-diff": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", - "integrity": "sha512-dtXTVMkh6VkEEA7OhXnN1Ecb8aAGFdZ1LFxtOCoqj4qkyOJMt7+qs6Ahdy6p/NQCPYsRSXXivhSB/J5E9jmYKA==", - "license": "MIT", - "dependencies": { - "arr-flatten": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/arr-flatten": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", - "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/arr-union": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", - "integrity": "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/array-buffer-byte-length": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -1962,48 +1010,10 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/array-find-index": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", - "integrity": "sha512-M1HQyIXcBGtVywBt8WVdim+lrNaK7VHp99Qt5pSNziXznKHViIBbXWtfRTpEFpF/c4FdfxNAsCCwPp5phBYJtw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/array-union": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", - "integrity": "sha512-Dxr6QJj/RdU/hCaBjOfxW+q6lyuVE6JFWIrAUpuOOhoJJoQ99cUn3igRaHVB5P9WrgFVN0FfArM3x0cueOU8ng==", - "license": "MIT", - "dependencies": { - "array-uniq": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/array-uniq": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", - "integrity": "sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/array-unique": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", - "integrity": "sha512-G2n5bG5fSUCpnsXz4+8FUkYsGPkNfLn9YvS66U5qbTIXI2Ynnlo4Bi42bWv+omKUCqz+ejzfClwne0alJWJPhg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/arraybuffer.prototype.slice": { "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", "license": "MIT", "dependencies": { "array-buffer-byte-length": "^1.0.1", @@ -2021,34 +1031,22 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/arrify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", - "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/asap": { "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", "license": "MIT" }, - "node_modules/assign-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", - "integrity": "sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/async": { "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", "license": "MIT" }, "node_modules/async-append-only-log": { "version": "4.3.10", + "resolved": "https://registry.npmjs.org/async-append-only-log/-/async-append-only-log-4.3.10.tgz", + "integrity": "sha512-OYibcLkDBG9J8FFX4Nyf8RJOhO1EJTfCfb3rWou+qjcmuuJwBu7EuVVDpRtzJ+mPGVmwlXVPKtaw3X1lfHhxaQ==", "license": "LGPL-3.0", "dependencies": { "@alloc/quick-lru": "^5.2.0", @@ -2065,71 +1063,40 @@ "push-stream-to-pull-stream": "^1.0.3" } }, - "node_modules/async-each": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.6.tgz", - "integrity": "sha512-c646jH1avxr+aVpndVMeAfYw7wAa6idufrlN3LPA4PmKS0QEGp6PIC9nwz0WQkkvBGAMEki3pFdtxaF39J9vvg==", - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], - "license": "MIT" - }, "node_modules/async-function": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", "license": "MIT", "engines": { "node": ">= 0.4" } }, - "node_modules/async-retry": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", - "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", + "node_modules/async-mutex": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.3.2.tgz", + "integrity": "sha512-HuTK7E7MT7jZEh1P9GtRW9+aTWiDWWi9InbZ5hjxrnRa39KS4BW04+xLBhYNS2aXhHUIKZSw3gj4Pn1pj+qGAA==", "license": "MIT", "dependencies": { - "retry": "0.13.1" - } - }, - "node_modules/async-retry/node_modules/retry": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", - "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", - "license": "MIT", - "engines": { - "node": ">= 4" + "tslib": "^2.3.1" } }, "node_modules/async-single": { "version": "1.0.5", - "license": "MIT" - }, - "node_modules/async-write": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/async-write/-/async-write-2.1.0.tgz", - "integrity": "sha512-eKEYj1+jDgQ15jazjmelcPGXO77pa5epzlyXyEQcwwLtnACtBTCrxaW+96uXF2j4rzIikmUtVlzXwejx6iIhWw==", + "resolved": "https://registry.npmjs.org/async-single/-/async-single-1.0.5.tgz", + "integrity": "sha512-I0SMZyvxwnr00advlzQbcfztaSEBv3Dw7Vg5i7pJVrXfjqHJSP1wljuOFKanBec5WKd+V4CIF4F9nhsukBikHA==", "license": "MIT" }, "node_modules/asynckit": { "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "license": "MIT" }, - "node_modules/atob": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", - "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", - "license": "(MIT OR Apache-2.0)", - "bin": { - "atob": "bin/atob.js" - }, - "engines": { - "node": ">= 4.5.0" - } - }, "node_modules/atomic-file": { "version": "2.1.1", + "resolved": "https://registry.npmjs.org/atomic-file/-/atomic-file-2.1.1.tgz", + "integrity": "sha512-Eh6pW+fRC2/1RxPq3hO8+PkZKv+wujzKky2MP/n69eC8yMkbNFfuEb/riZHqf13M7gr6Hvglpk/kISgBSBb6bQ==", "license": "MIT", "dependencies": { "flumecodec": "0.0.1", @@ -2139,6 +1106,8 @@ }, "node_modules/atomic-file-rw": { "version": "0.3.0", + "resolved": "https://registry.npmjs.org/atomic-file-rw/-/atomic-file-rw-0.3.0.tgz", + "integrity": "sha512-XMFpe/ub9Mwdlyq3DUdOc/g2sdutDMdYw0XNFSvNGQpYdmxqVmLbeklu5KhemUYyrAj1kIrwoHOx6IxA3a7w3A==", "license": "MIT", "dependencies": { "idb-kv-store": "^4.5.0", @@ -2147,6 +1116,8 @@ }, "node_modules/atomic-file/node_modules/flumecodec": { "version": "0.0.1", + "resolved": "https://registry.npmjs.org/flumecodec/-/flumecodec-0.0.1.tgz", + "integrity": "sha512-JT0xivzdV7uTucjsLMw6JhK2e1K5TmU4fGmoQqnrJbydgY/V6+m71QoxX5ZtRR1pKoD48uhPDWWE6G5MlNoGkg==", "license": "MIT", "dependencies": { "level-codec": "^6.2.0" @@ -2154,19 +1125,14 @@ }, "node_modules/atomic-file/node_modules/level-codec": { "version": "6.2.0", + "resolved": "https://registry.npmjs.org/level-codec/-/level-codec-6.2.0.tgz", + "integrity": "sha512-J437PvCMZZKNT88+VRh6dkmh1ndZzwGwDzb5ZZl3QEsl+U9wIlt8hG+Y1gXVOhH75gf8JyceKGiG6WFUBbxyGQ==", "license": "MIT" }, - "node_modules/attach-ware": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/attach-ware/-/attach-ware-1.1.1.tgz", - "integrity": "sha512-OpavlXWZkyE7m28fpCWF/RmxCukC1edukJp9IKjEpZs/O11H3896DkLpK7lMiL8ZDx2yxo9FrZQaeHkyJGcIuQ==", - "license": "MIT", - "dependencies": { - "unherit": "^1.0.0" - } - }, "node_modules/available-typed-arrays": { "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", "license": "MIT", "dependencies": { "possible-typed-array-names": "^1.0.0" @@ -2191,20 +1157,14 @@ }, "node_modules/b4a": { "version": "1.6.7", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", + "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==", "license": "Apache-2.0" }, - "node_modules/bail": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/bail/-/bail-1.0.5.tgz", - "integrity": "sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/balanced-match": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, "node_modules/bare-events": { @@ -2221,129 +1181,10 @@ } } }, - "node_modules/bare-fs": { - "version": "4.7.1", - "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.7.1.tgz", - "integrity": "sha512-WDRsyVN52eAx/lBamKD6uyw8H4228h/x0sGGGegOamM2cd7Pag88GfMQalobXI+HaEUxpCkbKQUDOQqt9wawRw==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "bare-events": "^2.5.4", - "bare-path": "^3.0.0", - "bare-stream": "^2.6.4", - "bare-url": "^2.2.2", - "fast-fifo": "^1.3.2" - }, - "engines": { - "bare": ">=1.16.0" - }, - "peerDependencies": { - "bare-buffer": "*" - }, - "peerDependenciesMeta": { - "bare-buffer": { - "optional": true - } - } - }, - "node_modules/bare-os": { - "version": "3.9.1", - "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.9.1.tgz", - "integrity": "sha512-6M5XjcnsygQNPMCMPXSK379xrJFiZ/AEMNBmFEmQW8d/789VQATvriyi5r0HYTL9TkQ26rn3kgdTG3aisbrXkQ==", - "license": "Apache-2.0", - "optional": true, - "engines": { - "bare": ">=1.14.0" - } - }, - "node_modules/bare-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", - "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "bare-os": "^3.0.1" - } - }, - "node_modules/bare-stream": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.1.tgz", - "integrity": "sha512-Vp0cnjYyrEC4whYTymQ+YZi6pBpfiICZO3cfRG8sy67ZNWe951urv1x4eW1BKNngw3U+3fPYb5JQvHbCtxH7Ow==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "streamx": "^2.25.0", - "teex": "^1.0.1" - }, - "peerDependencies": { - "bare-abort-controller": "*", - "bare-buffer": "*", - "bare-events": "*" - }, - "peerDependenciesMeta": { - "bare-abort-controller": { - "optional": true - }, - "bare-buffer": { - "optional": true - }, - "bare-events": { - "optional": true - } - } - }, - "node_modules/bare-url": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.3.tgz", - "integrity": "sha512-Kccpc7ACfXaxfeInfqKcZtW4pT5YBn1mesc4sCsun6sRwtbJ4h+sNOaksUpYEJUKfN65YWC6Bw2OJEFiKxq8nQ==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "bare-path": "^3.0.0" - } - }, - "node_modules/base": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", - "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", - "license": "MIT", - "dependencies": { - "cache-base": "^1.0.1", - "class-utils": "^0.3.5", - "component-emitter": "^1.2.1", - "define-property": "^1.0.0", - "isobject": "^3.0.1", - "mixin-deep": "^1.2.0", - "pascalcase": "^0.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/base/node_modules/define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", - "license": "MIT", - "dependencies": { - "is-descriptor": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/base/node_modules/isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/base64-js": { "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", "funding": [ { "type": "github", @@ -2362,6 +1203,8 @@ }, "node_modules/base64-url": { "version": "2.3.3", + "resolved": "https://registry.npmjs.org/base64-url/-/base64-url-2.3.3.tgz", + "integrity": "sha512-dLMhIsK7OplcDauDH/tZLvK7JmUZK3A7KiQpjNzsBrM6Etw7hzNI1tLEywqJk9NnwkgWuFKSlx/IUO7vF6Mo8Q==", "license": "ISC", "engines": { "node": ">=6" @@ -2369,14 +1212,14 @@ }, "node_modules/bash-color": { "version": "0.0.4", + "resolved": "https://registry.npmjs.org/bash-color/-/bash-color-0.0.4.tgz", + "integrity": "sha512-ZNB4525U7BxT6v9C8LEtywyCgB4Pjnm7/bh+ru/Z9Ecxvg3fDjaJ6z305z9a61orQdbB1zqYHh5JbUqx4s4K0g==", "license": "MIT" }, - "node_modules/before-after-hook": { - "version": "4.0.0", - "license": "Apache-2.0" - }, "node_modules/bencode": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/bencode/-/bencode-2.0.3.tgz", + "integrity": "sha512-D/vrAD4dLVX23NalHwb8dSvsUsxeRPO8Y7ToKA015JQYq69MLDOMkC0uGZYA/MPpltLO8rt8eqFC2j8DxjTZ/w==", "license": "MIT" }, "node_modules/bidi-js": { @@ -2388,52 +1231,572 @@ "require-from-string": "^2.0.2" } }, - "node_modules/binary-extensions": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", - "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", - "license": "MIT", + "node_modules/big-integer": { + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", + "license": "Unlicense", "engines": { - "node": ">=0.10.0" + "node": ">=0.6" } }, - "node_modules/binary-search": { - "version": "1.3.6", - "license": "CC0-1.0" - }, "node_modules/binary-search-bounds": { "version": "2.0.5", + "resolved": "https://registry.npmjs.org/binary-search-bounds/-/binary-search-bounds-2.0.5.tgz", + "integrity": "sha512-H0ea4Fd3lS1+sTEB2TgcLoK21lLhwEJzlQv3IN47pJS976Gx4zoWe0ak3q+uYh60ppQxg9F16Ri4tS1sfD4+jA==", "license": "MIT" }, - "node_modules/bindings": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "file-uri-to-path": "1.0.0" - } - }, "node_modules/bipf": { "version": "1.9.0", + "resolved": "https://registry.npmjs.org/bipf/-/bipf-1.9.0.tgz", + "integrity": "sha512-B/d8IADy5Y4v/CTMRWxLD8ONd2qRkF+2DbZLeIUql7PukfAiBhlGlw5qJcIU03l21qMEyvbi4PdntatH+j40vA==", "license": "MIT", "dependencies": { "fast-varint": "^1.0.0" } }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "license": "MIT", + "node_modules/blake2b": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/blake2b/-/blake2b-2.1.4.tgz", + "integrity": "sha512-AyBuuJNI64gIvwx13qiICz6H6hpmjvYS5DGkG6jbXMOT8Z3WUJ3V1X0FlhIoT1b/5JtHE3ki+xjtMvu1nn+t9A==", + "license": "ISC", "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" + "blake2b-wasm": "^2.4.0", + "nanoassert": "^2.0.0" } }, - "node_modules/bl/node_modules/buffer": { + "node_modules/blake2b-wasm": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/blake2b-wasm/-/blake2b-wasm-2.4.0.tgz", + "integrity": "sha512-S1kwmW2ZhZFFFOghcx73+ZajEfKBqhP82JMssxtLVMxlaPea1p9uoLiUZ5WYyHn0KddwbLc+0vh4wR0KBNoT5w==", + "license": "MIT", + "dependencies": { + "b4a": "^1.0.1", + "nanoassert": "^2.0.0" + } + }, + "node_modules/blake2s": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/blake2s/-/blake2s-1.1.0.tgz", + "integrity": "sha512-lvCxvg+up7AmujO8vijTi4GsbIOuusWa+b/nN5+VAanFjzbauq0er5VzgjTC6pevhEO8SYTzY784zS2KQauO0A==", + "license": "Public Domain" + }, + "node_modules/blake3": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/blake3/-/blake3-2.1.7.tgz", + "integrity": "sha512-5d+TdKJvju96IyEaGJ0eO6CHbckWi+NBrCezGYM/WsnI3R03aLL2TWfsuZSh1rs0fTv/L3ps/r0vykjYurcIwA==", + "hasInstallScript": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/browser-split": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/browser-split/-/browser-split-0.0.0.tgz", + "integrity": "sha512-CNXO3AXAS1H/kOGQkPjucm1161/XoF3aVkMfujqwk85XN/D/MkQMvoB81lXyX/2rerZS+hPAYYRR3mAW05awjQ==", + "license": "MIT" + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-alloc": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", + "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", + "license": "MIT", + "dependencies": { + "buffer-alloc-unsafe": "^1.1.0", + "buffer-fill": "^1.0.0" + } + }, + "node_modules/buffer-alloc-unsafe": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", + "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==", + "license": "MIT" + }, + "node_modules/buffer-crc32": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", + "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/buffer-fill": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", + "integrity": "sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==", + "license": "MIT" + }, + "node_modules/buffer-xor": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-2.0.2.tgz", + "integrity": "sha512-eHslX0bin3GB+Lx2p7lEYRShRewuNZL3fUl4qlVJGGiwoPGftmt8JQgk2Y9Ji5/01TnVDo33E5b5O3vUB1HdqQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.1" + } + }, + "node_modules/bufferutil": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.1.0.tgz", + "integrity": "sha512-ZMANVnAixE6AWWnPzlW2KpUrxhm9woycYvPOo67jWHyFowASTEd9s+QN1EIMsSDtwhIxN4sWE1jotpuDUIgyIw==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, + "node_modules/butt64": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/butt64/-/butt64-2.0.1.tgz", + "integrity": "sha512-rTxg4F4IH+/u6BN2/SJjJnQsOQwXeV7Hsm9ataUsxagoqXZ2xbt+vDVDYTbS1GEDpUE/38LEuG9Mk9pgoLUzpw==", + "license": "MIT" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cache-content-type": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-content-type/-/cache-content-type-1.0.1.tgz", + "integrity": "sha512-IKufZ1o4Ut42YUrZSo8+qnMTrFuKkvyoLXUywKz9GJ5BrhOFGhLdkx9sG4KAnVvbY6kEcSFjLQul+DVmBm2bgA==", + "license": "MIT", + "dependencies": { + "mime-types": "^2.1.18", + "ylru": "^1.2.0" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chacha20-universal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chacha20-universal/-/chacha20-universal-1.0.4.tgz", + "integrity": "sha512-/IOxdWWNa7nRabfe7+oF+jVkGjlr2xUL4J8l/OvzZhj+c9RpMqoo3Dq+5nU1j/BflRV4BKnaQ4+4oH1yBpQG1Q==", + "license": "ISC", + "dependencies": { + "nanoassert": "^2.0.0" + } + }, + "node_modules/charwise": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/charwise/-/charwise-3.0.1.tgz", + "integrity": "sha512-RcdumNsM6fJZ5HHbYunqj2bpurVRGsXour3OR+SlLEHFhG6ALm54i6Osnh+OvO7kEoSBzwExpblYFH8zKQiEPw==", + "license": "MIT" + }, + "node_modules/chloride": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/chloride/-/chloride-2.4.1.tgz", + "integrity": "sha512-ZiID87W2o2llvuF4C7Fvt9GJisazSdMsSkjAq4WaMed9zn77nlkcy08ZfrPtOGAXyaxTDj0VjnuyD97EdJLz3g==", + "license": "MIT", + "dependencies": { + "sodium-browserify": "^1.2.7", + "sodium-browserify-tweetnacl": "^0.2.5", + "sodium-chloride": "^1.1.2" + }, + "optionalDependencies": { + "sodium-native": "^3.0.0" + } + }, + "node_modules/chloride-test": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/chloride-test/-/chloride-test-1.2.4.tgz", + "integrity": "sha512-9vhoi1qXSBPn6//ZxIgSe3M2QhKHzIPZQzmrZgmPADsqW0Jxpe3db1e7aGSRUMXbxAQ04SfypdT8dGaSvIvKDw==", + "license": "MIT", + "dependencies": { + "json-buffer": "^2.0.11" + } + }, + "node_modules/chloride-test/node_modules/json-buffer": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-2.0.11.tgz", + "integrity": "sha512-Wu4/hxSZX7Krzjor+sZHWaRau6Be4WQHlrkl3v8cmxRBBewF2TotlgHUedKQJyFiUyFxnK/ZlRYnR9UNVZ7pkg==", + "license": "MIT" + }, + "node_modules/clarify-error": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clarify-error/-/clarify-error-1.0.0.tgz", + "integrity": "sha512-f96oT3/Cdwz1eB+7RaH/XRR42lwGqVPnDl9NAm9ugT+BwAFoUS/pVnkgXUo/5UaUmwMMs6/GNFP8A8gCgmgvog==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/class-list": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/class-list/-/class-list-0.1.1.tgz", + "integrity": "sha512-zqR0uW+VsLtyQhixBhkdQ+z6B8+Y8HTh28kdSVjJ4zTTKM7Xz2asAQSya9VI6m/34F6N6Ktm0mrchKB+E5a8Xw==", + "dependencies": { + "indexof": "0.0.1" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/co-body": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/co-body/-/co-body-6.2.0.tgz", + "integrity": "sha512-Kbpv2Yd1NdL1V/V4cwLVxraHDV6K8ayohr2rmH0J87Er8+zJjcTa6dAn9QMPC9CRgU8+aNajKbSf1TzDB1yKPA==", + "license": "MIT", + "dependencies": { + "@hapi/bourne": "^3.0.0", + "inflation": "^2.0.0", + "qs": "^6.5.2", + "raw-body": "^2.3.3", + "type-is": "^1.6.16" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/compress-commons": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz", + "integrity": "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==", + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "crc32-stream": "^6.0.0", + "is-stream": "^2.0.1", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/cont": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cont/-/cont-1.0.3.tgz", + "integrity": "sha512-lZzUsnNnzBW4EN+Mll8J54cdHn6JzKyHpa9ftxxsPwkWfhdYRIHph9dFdmi0ibfqhnvR103tQ61eAF57FF87IA==", + "license": "MIT", + "dependencies": { + "continuable": "~1.2.0", + "continuable-para": "~1.2.0", + "continuable-series": "~1.2.0" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/continuable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/continuable/-/continuable-1.2.0.tgz", + "integrity": "sha512-DMksZyrS34yEX+DTGyt9h8/6ONHRBH+PpQ5ev3YdCy5J41r0nMNv+psAujoeoT1mJf+fzKhzacR9o1M1ys2tBQ==" + }, + "node_modules/continuable-hash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/continuable-hash/-/continuable-hash-0.1.4.tgz", + "integrity": "sha512-Gu5kdDzwmCymZ2tROViH0VYSd0iXDDaOjn8iG51FyZreS6OuqEJAE+a3sJ6/Ky6roKOHYHDYD8Ttxj+a1tyYrg==", + "dependencies": { + "continuable": "~1.1.6" + } + }, + "node_modules/continuable-hash/node_modules/continuable": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/continuable/-/continuable-1.1.8.tgz", + "integrity": "sha512-DrVON8s+uwIH+OJa/HdbLE7ViLKpDeF0+flOhLf7hKtffIvyI2l38i7jq6G5i2GFqodpkz7RJPq60VkmwxfVnA==" + }, + "node_modules/continuable-list": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/continuable-list/-/continuable-list-0.1.6.tgz", + "integrity": "sha512-dqfYFtC/YRR4fUuZyJ8A+VKX/oV8E+atSCCcOvkuHsB6MFkiyI9LrDlUXCnHYowuuDu5vYqcAiRU2hiGXtcb7w==", + "dependencies": { + "continuable": "~1.1.6" + } + }, + "node_modules/continuable-list/node_modules/continuable": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/continuable/-/continuable-1.1.8.tgz", + "integrity": "sha512-DrVON8s+uwIH+OJa/HdbLE7ViLKpDeF0+flOhLf7hKtffIvyI2l38i7jq6G5i2GFqodpkz7RJPq60VkmwxfVnA==" + }, + "node_modules/continuable-para": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/continuable-para/-/continuable-para-1.2.0.tgz", + "integrity": "sha512-XjPo974RGEJqnvFVT9PWxG97d/NCg58mwmWDbKXqTZBZuUnMIjkx0vde+xerkUuymk8DR+YUAcllK8J5/O4Leg==", + "license": "MIT", + "dependencies": { + "continuable-hash": "~0.1.4", + "continuable-list": "~0.1.5" + } + }, + "node_modules/continuable-series": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/continuable-series/-/continuable-series-1.2.0.tgz", + "integrity": "sha512-xMAlREs3m/IM0VCzuh0TRBXpjd6tl6zi3cMgiwcywIPFcCmNlcFNA6XCtA2cEx6Kq7EE6c1JVDVl3WGepp6a5g==", + "license": "MIT" + }, + "node_modules/cookies": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.9.1.tgz", + "integrity": "sha512-TG2hpqe4ELx54QER/S3HQ9SRVnQnGBtKUz5bLQWtYAQ+o6GpgMs6sYUvaiJjVxb+UXwhRhAEP3m7LbsIZ77Hmw==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "keygrip": "~1.1.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/cpu-percentage": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cpu-percentage/-/cpu-percentage-1.0.3.tgz", + "integrity": "sha512-LP7N2+hS7optwpkMisZoDfswnLeCzyETNOogkjw7v2YteQSPDd0vEv3nSbU8FVu58fXCkVAWtXaeSXEedT1a5g==", + "license": "MIT", + "engines": { + "node": ">=6.1" + } + }, + "node_modules/crc": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/crc/-/crc-3.6.0.tgz", + "integrity": "sha512-K9CVP4+ugmpRvZidsumVVL1swX7t0cqYrLhT+5rAp7/S3TjiEtpovD8TISzy+3moanm6v6/cbXOzE6JbzB+siw==", + "license": "MIT", + "dependencies": { + "buffer": "^5.1.0" + } + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc/node_modules/buffer": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", @@ -2457,1108 +1820,10 @@ "ieee754": "^1.1.13" } }, - "node_modules/bl/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/blake2b": { - "version": "2.1.4", - "license": "ISC", - "dependencies": { - "blake2b-wasm": "^2.4.0", - "nanoassert": "^2.0.0" - } - }, - "node_modules/blake2b-wasm": { - "version": "2.4.0", - "license": "MIT", - "dependencies": { - "b4a": "^1.0.1", - "nanoassert": "^2.0.0" - } - }, - "node_modules/blake2s": { - "version": "1.1.0", - "license": "Public Domain" - }, - "node_modules/blake3": { - "version": "2.1.7", - "hasInstallScript": true, - "license": "MIT" - }, - "node_modules/body-parser": { - "version": "2.2.2", - "license": "MIT", - "dependencies": { - "bytes": "^3.1.2", - "content-type": "^1.0.5", - "debug": "^4.4.3", - "http-errors": "^2.0.0", - "iconv-lite": "^0.7.0", - "on-finished": "^2.4.1", - "qs": "^6.14.1", - "raw-body": "^3.0.1", - "type-is": "^2.0.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/body-parser/node_modules/http-errors": { - "version": "2.0.1", - "license": "MIT", - "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/body-parser/node_modules/iconv-lite": { - "version": "0.7.2", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/body-parser/node_modules/media-typer": { - "version": "1.1.0", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/body-parser/node_modules/mime-db": { - "version": "1.54.0", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/body-parser/node_modules/mime-types": { - "version": "3.0.2", - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/body-parser/node_modules/raw-body": { - "version": "3.0.2", - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.7.0", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/body-parser/node_modules/statuses": { - "version": "2.0.2", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/body-parser/node_modules/type-is": { - "version": "2.0.1", - "license": "MIT", - "dependencies": { - "content-type": "^1.0.5", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/bottleneck": { - "version": "2.19.5", - "license": "MIT" - }, - "node_modules/brace-expansion": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", - "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/braces": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", - "integrity": "sha512-xU7bpz2ytJl1bH9cgIurjpg/n8Gohy9GTw81heDYLJQ4RU60dlyJsa+atVF2pI0yMMvKxI9HkKwjePCj5XI1hw==", - "license": "MIT", - "dependencies": { - "expand-range": "^1.8.1", - "preserve": "^0.2.0", - "repeat-element": "^1.1.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/broadcast-stream": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/broadcast-stream/-/broadcast-stream-0.2.2.tgz", - "integrity": "sha512-d6iHc4P5YC2RvXv6qZxH3aoue88BiWhzCPki+i1Dj2igeZyNtUoMInloINRfnyrB7Fm+2f1q1bmQYhXQeKWwkQ==", - "license": "MIT", - "bin": { - "broadcast-stream": "bin.js" - } - }, - "node_modules/browser-split": { - "version": "0.0.0", - "license": "MIT" - }, - "node_modules/buffer": { - "version": "6.0.3", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, - "node_modules/buffer-alloc": { - "version": "1.2.0", - "license": "MIT", - "dependencies": { - "buffer-alloc-unsafe": "^1.1.0", - "buffer-fill": "^1.0.0" - } - }, - "node_modules/buffer-alloc-unsafe": { - "version": "1.1.0", - "license": "MIT" - }, - "node_modules/buffer-crc32": { - "version": "1.0.0", - "license": "MIT", - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/buffer-fill": { - "version": "1.0.0", - "license": "MIT" - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "license": "MIT" - }, - "node_modules/buffer-xor": { - "version": "2.0.2", - "license": "MIT", - "dependencies": { - "safe-buffer": "^5.1.1" - } - }, - "node_modules/butt64": { - "version": "2.0.1", - "license": "MIT" - }, - "node_modules/bytes": { - "version": "3.1.2", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/cache-base": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", - "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", - "license": "MIT", - "dependencies": { - "collection-visit": "^1.0.0", - "component-emitter": "^1.2.1", - "get-value": "^2.0.6", - "has-value": "^1.0.0", - "isobject": "^3.0.1", - "set-value": "^2.0.0", - "to-object-path": "^0.3.0", - "union-value": "^1.0.0", - "unset-value": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/cache-base/node_modules/isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/cache-content-type": { - "version": "1.0.1", - "license": "MIT", - "dependencies": { - "mime-types": "^2.1.18", - "ylru": "^1.2.0" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/call-bind": { - "version": "1.0.8", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.0", - "es-define-property": "^1.0.0", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/camelcase": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", - "integrity": "sha512-FxAv7HpHrXbh3aPo4o2qxHay2lkLY3x5Mw3KeE4KQE8ysVfziWeRZDwcjauvwBSGEC/nXUPzZy8zeh4HokqOnw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/camelcase-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-4.2.0.tgz", - "integrity": "sha512-Ej37YKYbFUI8QiYlvj9YHb6/Z60dZyPJW0Cs8sFilMbd2lP0bw3ylAq9yJkK4lcTA2dID5fG8LjmJYbO7kWb7Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "camelcase": "^4.1.0", - "map-obj": "^2.0.0", - "quick-lru": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/cat-names": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/cat-names/-/cat-names-2.0.0.tgz", - "integrity": "sha512-yjcTHwA7tcro823P5yIfmTvXT8p5YxJd4eASWHAHvqaekuJoGu1qjWoldgfV2mH4tgLp+FNAf13uiDEo/O7LGg==", - "dev": true, - "license": "MIT", - "dependencies": { - "meow": "^5.0.0", - "unique-random-array": "^1.0.0" - }, - "bin": { - "cat-names": "cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/ccount": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/ccount/-/ccount-1.1.0.tgz", - "integrity": "sha512-vlNK021QdI7PNeiUh/lKkC/mNHHfV0m/Ad5JoI0TYtlBnJAslM/JIkm/tGC88bkLIwO6OQ5uV6ztS6kVAtCDlg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/chacha20-universal": { - "version": "1.0.4", - "license": "ISC", - "dependencies": { - "nanoassert": "^2.0.0" - } - }, - "node_modules/chalk": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", - "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/character-entities": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz", - "integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-entities-html4": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-1.1.4.tgz", - "integrity": "sha512-HRcDxZuZqMx3/a+qrzxdBKBPUpxWEq9xw2OPZ3a/174ihfrQKVsFhqtthBInFy1zZ9GgZyFXOatNujm8M+El3g==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-entities-legacy": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz", - "integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-reference-invalid": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz", - "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/charwise": { - "version": "3.0.1", - "license": "MIT" - }, - "node_modules/chloride": { - "version": "2.4.1", - "license": "MIT", - "dependencies": { - "sodium-browserify": "^1.2.7", - "sodium-browserify-tweetnacl": "^0.2.5", - "sodium-chloride": "^1.1.2" - }, - "optionalDependencies": { - "sodium-native": "^3.0.0" - } - }, - "node_modules/chloride-test": { - "version": "1.2.4", - "license": "MIT", - "dependencies": { - "json-buffer": "^2.0.11" - } - }, - "node_modules/chloride-test/node_modules/json-buffer": { - "version": "2.0.11", - "license": "MIT" - }, - "node_modules/chmodrp": { - "version": "1.0.2", - "license": "MIT" - }, - "node_modules/chokidar": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz", - "integrity": "sha512-mk8fAWcRUOxY7btlLtitj3A45jOwSAxH4tOFOoEGbVsl6cL6pPMWUy7dwZ/canfj3QEdP6FHSnf/l1c6/WkzVg==", - "license": "MIT", - "dependencies": { - "anymatch": "^1.3.0", - "async-each": "^1.0.0", - "glob-parent": "^2.0.0", - "inherits": "^2.0.1", - "is-binary-path": "^1.0.0", - "is-glob": "^2.0.0", - "path-is-absolute": "^1.0.0", - "readdirp": "^2.0.0" - }, - "optionalDependencies": { - "fsevents": "^1.0.0" - } - }, - "node_modules/chokidar/node_modules/fsevents": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", - "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", - "deprecated": "Upgrade to fsevents v2 to mitigate potential security issues", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "dependencies": { - "bindings": "^1.5.0", - "nan": "^2.12.1" - }, - "engines": { - "node": ">= 4.0" - } - }, - "node_modules/chownr": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", - "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/ci-info": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", - "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/clarify-error": { - "version": "1.0.0", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/class-list": { - "version": "0.1.1", - "dependencies": { - "indexof": "0.0.1" - } - }, - "node_modules/class-utils": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", - "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", - "license": "MIT", - "dependencies": { - "arr-union": "^3.1.0", - "define-property": "^0.2.5", - "isobject": "^3.0.0", - "static-extend": "^0.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/class-utils/node_modules/define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", - "license": "MIT", - "dependencies": { - "is-descriptor": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/class-utils/node_modules/is-descriptor": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", - "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", - "license": "MIT", - "dependencies": { - "is-accessor-descriptor": "^1.0.1", - "is-data-descriptor": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/class-utils/node_modules/isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/cli-cursor": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", - "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", - "license": "MIT", - "dependencies": { - "restore-cursor": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-spinners": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", - "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cliui": { - "version": "8.0.1", - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/cliui/node_modules/emoji-regex": { - "version": "8.0.0", - "license": "MIT" - }, - "node_modules/cliui/node_modules/string-width": { - "version": "4.2.3", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/wrap-ansi": { - "version": "7.0.0", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/cmake-js": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/cmake-js/-/cmake-js-8.0.0.tgz", - "integrity": "sha512-YbUP88RDwCvoQkZhRtGURYm9RIpWdtvZuhT87fKNoLjk8kIFIFeARpKfuZQGdwfH99GZpUmqSfcDrK62X7lTgg==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.3", - "fs-extra": "^11.3.3", - "node-api-headers": "^1.8.0", - "rc": "1.2.8", - "semver": "^7.7.3", - "tar": "^7.5.6", - "url-join": "^4.0.1", - "which": "^6.0.0", - "yargs": "^17.7.2" - }, - "bin": { - "cmake-js": "bin/cmake-js" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/cmake-js/node_modules/isexe": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", - "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=20" - } - }, - "node_modules/cmake-js/node_modules/which": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", - "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", - "license": "ISC", - "dependencies": { - "isexe": "^4.0.0" - }, - "bin": { - "node-which": "bin/which.js" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/co": { - "version": "4.6.0", - "license": "MIT", - "engines": { - "iojs": ">= 1.0.0", - "node": ">= 0.12.0" - } - }, - "node_modules/co-body": { - "version": "6.2.0", - "license": "MIT", - "dependencies": { - "@hapi/bourne": "^3.0.0", - "inflation": "^2.0.0", - "qs": "^6.5.2", - "raw-body": "^2.3.3", - "type-is": "^1.6.16" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/collapse-white-space": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-1.0.6.tgz", - "integrity": "sha512-jEovNnrhMuqyCcjfEJA56v0Xq8SkIoPKDyaHahwo3POf4qcSXqMYuwNcOTzp74vTsR9Tn08z4MxWqAhcekogkQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/collection-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", - "integrity": "sha512-lNkKvzEeMBBjUGHZ+q6z9pSJla0KWAQPvtzhEV9+iGyQYG+pBpl7xKDhxoNSOZH2hhv0v5k0y2yAM4o4SjoSkw==", - "license": "MIT", - "dependencies": { - "map-visit": "^1.0.0", - "object-visit": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/color": { - "version": "4.2.3", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1", - "color-string": "^1.9.0" - }, - "engines": { - "node": ">=12.5.0" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "license": "MIT" - }, - "node_modules/color-string": { - "version": "1.9.1", - "license": "MIT", - "dependencies": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" - } - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/commander": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", - "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", - "license": "MIT", - "engines": { - "node": ">=14" - } - }, - "node_modules/compare-at-paths": { - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "libnested": "^1.3.2", - "tape": "^4.9.1", - "typewiselite": "^1.0.0" - } - }, - "node_modules/compatibility": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/compatibility/-/compatibility-1.0.1.tgz", - "integrity": "sha512-0OzQu7V1TlJvKiEGC4b4TEXypGBHzcRX8ZSJ9svyMIKnE73AFrBAmVyPIabrFKhKc0L0iDLw6voyB3vA3hEXHg==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.1.1", - "npm-install-package": "^2.1.0", - "semver": "^6.1.0" - }, - "bin": { - "compatibility": "bin.js" - } - }, - "node_modules/compatibility/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/component-emitter": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", - "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/compress-commons": { - "version": "6.0.2", - "license": "MIT", - "dependencies": { - "crc-32": "^1.2.0", - "crc32-stream": "^6.0.0", - "is-stream": "^2.0.1", - "normalize-path": "^3.0.0", - "readable-stream": "^4.0.0" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "license": "MIT" - }, - "node_modules/concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", - "engines": [ - "node >= 0.8" - ], - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" - } - }, - "node_modules/concat-stream/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "license": "MIT" - }, - "node_modules/concat-stream/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/concat-stream/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "license": "MIT" - }, - "node_modules/concat-stream/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/cont": { - "version": "1.0.3", - "license": "MIT", - "dependencies": { - "continuable": "~1.2.0", - "continuable-para": "~1.2.0", - "continuable-series": "~1.2.0" - } - }, - "node_modules/content-disposition": { - "version": "0.5.4", - "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/continuable": { - "version": "1.2.0" - }, - "node_modules/continuable-hash": { - "version": "0.1.4", - "dependencies": { - "continuable": "~1.1.6" - } - }, - "node_modules/continuable-hash/node_modules/continuable": { - "version": "1.1.8" - }, - "node_modules/continuable-list": { - "version": "0.1.6", - "dependencies": { - "continuable": "~1.1.6" - } - }, - "node_modules/continuable-list/node_modules/continuable": { - "version": "1.1.8" - }, - "node_modules/continuable-para": { - "version": "1.2.0", - "license": "MIT", - "dependencies": { - "continuable-hash": "~0.1.4", - "continuable-list": "~0.1.5" - } - }, - "node_modules/continuable-series": { - "version": "1.2.0", - "license": "MIT" - }, - "node_modules/cookie": { - "version": "0.7.1", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.2.2", - "license": "MIT", - "engines": { - "node": ">=6.6.0" - } - }, - "node_modules/cookies": { - "version": "0.9.1", - "license": "MIT", - "dependencies": { - "depd": "~2.0.0", - "keygrip": "~1.1.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/copy-descriptor": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", - "integrity": "sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/core-util-is": { - "version": "1.0.3", - "license": "MIT" - }, - "node_modules/cors": { - "version": "2.8.5", - "license": "MIT", - "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/cpu-percentage": { - "version": "1.0.3", - "license": "MIT", - "engines": { - "node": ">=6.1" - } - }, - "node_modules/crc": { - "version": "3.6.0", - "license": "MIT", - "dependencies": { - "buffer": "^5.1.0" - } - }, - "node_modules/crc-32": { - "version": "1.2.2", - "license": "Apache-2.0", - "bin": { - "crc32": "bin/crc32.njs" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/crc/node_modules/buffer": { - "version": "5.7.1", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, "node_modules/crc32-stream": { "version": "6.0.0", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-6.0.0.tgz", + "integrity": "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==", "license": "MIT", "dependencies": { "crc-32": "^1.2.0", @@ -3570,6 +1835,8 @@ }, "node_modules/cross-spawn": { "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -3617,17 +1884,17 @@ "node": "20 || >=22" } }, - "node_modules/currently-unhandled": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", - "integrity": "sha512-/fITjgjGU50vjQ4FH6eUoYu+iUoUKIXws2hL15JJpIR+BbTxaXQsMuuyjtNh2WqsSBS5nsaZHFsFecyw5CCAng==", - "dev": true, - "license": "MIT", + "node_modules/d": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz", + "integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==", + "license": "ISC", "dependencies": { - "array-find-index": "^1.0.1" + "es5-ext": "^0.10.64", + "type": "^2.7.2" }, "engines": { - "node": ">=0.10.0" + "node": ">=0.12" } }, "node_modules/data-urls": { @@ -3680,6 +1947,8 @@ }, "node_modules/data-view-buffer": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -3695,6 +1964,8 @@ }, "node_modules/data-view-byte-length": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -3710,6 +1981,8 @@ }, "node_modules/data-view-byte-offset": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -3725,6 +1998,8 @@ }, "node_modules/debug": { "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -3740,33 +2015,8 @@ }, "node_modules/decamelize": { "version": "1.2.0", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/decamelize-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.1.tgz", - "integrity": "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==", - "dev": true, - "license": "MIT", - "dependencies": { - "decamelize": "^1.1.0", - "map-obj": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/decamelize-keys/node_modules/map-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", - "integrity": "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==", - "dev": true, + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -3778,36 +2028,16 @@ "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", "license": "MIT" }, - "node_modules/decode-uri-component": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", - "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", - "license": "MIT", - "engines": { - "node": ">=0.10" - } - }, - "node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "license": "MIT", - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/deep-equal": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", + "integrity": "sha512-bHtC0iYvWhyaTzvV3CZgPeZQqCOBGyGsVV7v4eevpdkLHfiSrXUdBG+qAuSz4RI70sszvjQ1QSZ98An1yNwpSw==", "license": "MIT" }, "node_modules/deep-extend": { "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", "license": "MIT", "engines": { "node": ">=4.0.0" @@ -3815,6 +2045,8 @@ }, "node_modules/deferred-leveldown": { "version": "5.3.0", + "resolved": "https://registry.npmjs.org/deferred-leveldown/-/deferred-leveldown-5.3.0.tgz", + "integrity": "sha512-a59VOT+oDy7vtAbLRCZwWgxu2BaCfd5Hk7wxJd48ei7I+nsg8Orlb9CLG0PMZienk9BSUKgeAqkO2+Lw+1+Ukw==", "license": "MIT", "dependencies": { "abstract-leveldown": "~6.2.1", @@ -3826,6 +2058,8 @@ }, "node_modules/define-data-property": { "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "license": "MIT", "dependencies": { "es-define-property": "^1.0.0", @@ -3841,6 +2075,8 @@ }, "node_modules/define-lazy-prop": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", "license": "MIT", "engines": { "node": ">=8" @@ -3848,6 +2084,8 @@ }, "node_modules/define-properties": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", "license": "MIT", "dependencies": { "define-data-property": "^1.0.1", @@ -3861,37 +2099,10 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", - "license": "MIT", - "dependencies": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/define-property/node_modules/isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/defined": { - "version": "1.0.1", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/delayed-stream": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "license": "MIT", "engines": { "node": ">=0.4.0" @@ -3899,10 +2110,14 @@ }, "node_modules/delegates": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", "license": "MIT" }, "node_modules/depd": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -3910,23 +2125,18 @@ }, "node_modules/destroy": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", "license": "MIT", "engines": { "node": ">= 0.8", "npm": "1.2.8000 || >= 1.4.16" } }, - "node_modules/detab": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/detab/-/detab-1.0.2.tgz", - "integrity": "sha512-J7vnPWu/Rq5Hi8xPPnwsv55CLkJmzZZCJOs0sAc6QcC7uf1X2iOrAXyGB6bXJBl5s4anllkMBKkHz13/BenuUQ==", - "license": "MIT", - "dependencies": { - "repeat-string": "^1.5.2" - } - }, "node_modules/detect-libc": { "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", "license": "Apache-2.0", "engines": { "node": ">=8" @@ -3934,6 +2144,8 @@ }, "node_modules/dezalgo": { "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", "license": "ISC", "dependencies": { "asap": "^2.0.0", @@ -3942,6 +2154,8 @@ }, "node_modules/dgram-broadcast": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/dgram-broadcast/-/dgram-broadcast-1.0.0.tgz", + "integrity": "sha512-7IPzhuQZVnU2iGo+Eq+dRNpYvlCkT7/5yD6BHx5P6MtT/cDxajR7Qh4+ve2WamsXaXVRLvj16ah8YIF7qcXOLw==", "license": "MIT", "bin": { "dgram-broadcast": "bin.js" @@ -3952,260 +2166,64 @@ }, "node_modules/dijkstrajs": { "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", "license": "MIT" }, "node_modules/discontinuous-range": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/discontinuous-range/-/discontinuous-range-1.0.0.tgz", + "integrity": "sha512-c68LpLbO+7kP/b1Hr1qs8/BJ09F5khZGTxqxZuhzxpmwJKOgRFHJWIb9/KmqnqHhLdO55aOxFH/EGBvUQbL/RQ==", "license": "MIT" }, - "node_modules/dog-names": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/dog-names/-/dog-names-1.0.2.tgz", - "integrity": "sha512-3+2tu1P9L+J3od+yTWQpfXkcUlukfylLYpsSgAMFaUvxk8Ah7kStNdUDcg9D4hpH1D8b+uMGHv5LKlB4uaH4ng==", - "dev": true, + "node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", "license": "MIT", "dependencies": { - "meow": "^3.0.0", - "unique-random-array": "^1.0.0" + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" }, - "bin": { - "dog-names": "cli.js" - }, - "engines": { - "node": ">=0.10.0" + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" } }, - "node_modules/dog-names/node_modules/camelcase": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", - "integrity": "sha512-DLIsRzJVBQu72meAKPkWQOLcujdXT32hwdfnkI1frSiSRMK1MofjKHf+MEx0SB6fjEFXL8fBDv1dKymBlOp4Qw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/dog-names/node_modules/camelcase-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", - "integrity": "sha512-bA/Z/DERHKqoEOrp+qeGKw1QlvEQkGZSc0XaY6VnTxZr+Kv1G5zFwttpjv8qxZ/sBPT4nthwZaAcsAZTJlSKXQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "camelcase": "^2.0.0", - "map-obj": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/dog-names/node_modules/find-up": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", - "integrity": "sha512-jvElSjyuo4EMQGoTwo1uJU5pQMwTW5lS1x05zzfJuTIyLR3zwO27LYrxNg+dlvKpGOuGy/MzBdXh80g0ve5+HA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-exists": "^2.0.0", - "pinkie-promise": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/dog-names/node_modules/indent-string": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", - "integrity": "sha512-aqwDFWSgSgfRaEwao5lg5KEcVd/2a+D1rvoG7NdilmYz0NwRk6StWpWdz/Hpk34MKPpx7s8XxUqimfcQK6gGlg==", - "dev": true, - "license": "MIT", - "dependencies": { - "repeating": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/dog-names/node_modules/load-json-file": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", - "integrity": "sha512-cy7ZdNRXdablkXYNI049pthVeXFurRyb9+hA/dZzerZ0pGTx42z+y+ssxBaVV2l70t1muq5IdKhn4UtcoGUY9A==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.1.2", - "parse-json": "^2.2.0", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0", - "strip-bom": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/dog-names/node_modules/map-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", - "integrity": "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/dog-names/node_modules/meow": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", - "integrity": "sha512-TNdwZs0skRlpPpCUK25StC4VH+tP5GgeY1HQOOGP+lQ2xtdkN2VtT/5tiX9k3IWpkBPV9b3LsAWXn4GGi/PrSA==", - "dev": true, - "license": "MIT", - "dependencies": { - "camelcase-keys": "^2.0.0", - "decamelize": "^1.1.2", - "loud-rejection": "^1.0.0", - "map-obj": "^1.0.1", - "minimist": "^1.1.3", - "normalize-package-data": "^2.3.4", - "object-assign": "^4.0.1", - "read-pkg-up": "^1.0.1", - "redent": "^1.0.0", - "trim-newlines": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/dog-names/node_modules/parse-json": { + "node_modules/dom-serializer/node_modules/entities": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", - "integrity": "sha512-QR/GGaKCkhwk1ePQNYDRKYZ3mwU9ypsKhB0XyFnLQdomyEqk3e8wpW3V5Jp88zbxK4n5ST1nqo+g9juTpownhQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "error-ex": "^1.2.0" - }, - "engines": { - "node": ">=0.10.0" + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/dog-names/node_modules/path-exists": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", - "integrity": "sha512-yTltuKuhtNeFJKa1PiRzfLAU5182q1y4Eb4XCJ3PBqyzEDkAZRzBrKKBct682ls9reBVHf9udYLN5Nd+K1B9BQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "pinkie-promise": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/dog-names/node_modules/path-type": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", - "integrity": "sha512-S4eENJz1pkiQn9Znv33Q+deTOKmbl+jj1Fl+qiP/vYezj+S8x+J3Uo0ISrx/QoEvIlOaDWJhPaRd1flJ9HXZqg==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.1.2", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/dog-names/node_modules/pify": { + "node_modules/domelementtype": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" }, - "node_modules/dog-names/node_modules/read-pkg": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", - "integrity": "sha512-7BGwRHqt4s/uVbuyoeejRn4YmFnYZiFl4AuaeXHlgZf3sONF0SOGlxs2Pw8g6hCKupo08RafIO5YXFNOKTfwsQ==", - "dev": true, - "license": "MIT", + "node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "license": "BSD-2-Clause", "dependencies": { - "load-json-file": "^1.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^1.0.0" + "domelementtype": "^2.2.0" }, "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/dog-names/node_modules/read-pkg-up": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", - "integrity": "sha512-WD9MTlNtI55IwYUS27iHh9tK3YoIVhxis8yKhLpTqWtml739uXc9NWTpxoHkfZf3+DkCCsXox94/VWZniuZm6A==", - "dev": true, - "license": "MIT", - "dependencies": { - "find-up": "^1.0.0", - "read-pkg": "^1.0.0" + "node": ">= 4" }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/dog-names/node_modules/redent": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", - "integrity": "sha512-qtW5hKzGQZqKoh6JNSD+4lfitfPKGz42e6QwiRmPM5mmKtR0N41AbJRYu0xJi7nhOJ4WDgRkKvAk6tw4WIwR4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "indent-string": "^2.1.0", - "strip-indent": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/dog-names/node_modules/strip-bom": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", - "integrity": "sha512-kwrX1y7czp1E69n2ajbG65mIo9dqvJ+8aBQXOGVxqwvNbsXdFM6Lq37dLAY3mknUwru8CfcCbfOLL/gMo+fi3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-utf8": "^0.2.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/dog-names/node_modules/strip-indent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", - "integrity": "sha512-I5iQq6aFMM62fBEAIB/hXzwJD6EEZ0xEGCX2t7oXqaKPIRgt4WruAQ285BISgdkP+HLGWyeGmNJcpIwFeRYRUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-stdin": "^4.0.1" - }, - "bin": { - "strip-indent": "cli.js" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/dog-names/node_modules/trim-newlines": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", - "integrity": "sha512-Nm4cF79FhSTzrLKGDMi3I4utBtFv8qKy4sq1enftf2gMdpqI8oVQTAfySkTz5r49giVzDj88SVZXP4CeYQwjaw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" } }, "node_modules/dompurify": { @@ -4217,18 +2235,24 @@ "@types/trusted-types": "^2.0.7" } }, - "node_modules/dotignore": { - "version": "0.1.2", - "license": "MIT", + "node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "license": "BSD-2-Clause", "dependencies": { - "minimatch": "^3.0.4" + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" }, - "bin": { - "ignored": "bin/ignored" + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" } }, "node_modules/dunder-proto": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -4241,6 +2265,8 @@ }, "node_modules/dynamic-dijkstra": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/dynamic-dijkstra/-/dynamic-dijkstra-1.0.2.tgz", + "integrity": "sha512-1N+eCCrepIeK1+qtWrMEO1CV68Hn+TLbiR9c70VB3xnut3DmUxT+3T7sRHhb0mpK2F/74IfP+loQDriU2W9lkA==", "license": "MIT", "dependencies": { "heap": "^0.2.6", @@ -4249,10 +2275,14 @@ }, "node_modules/eastasianwidth": { "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "license": "MIT" }, "node_modules/ed2curve": { "version": "0.1.4", + "resolved": "https://registry.npmjs.org/ed2curve/-/ed2curve-0.1.4.tgz", + "integrity": "sha512-hDZWhCHZ1wu4P2g2RVsM2MjDmmJzhvcsXr5qHUSBJZXvuhJSunhbVsWoBXdIe0/yTa3RV4UaWpOmFmrVsKr0wA==", "license": "Public domain", "dependencies": { "tweetnacl": "0.x.x" @@ -4260,50 +2290,29 @@ }, "node_modules/ee-first": { "version": "1.1.1", - "license": "MIT" - }, - "node_modules/elegant-spinner": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/elegant-spinner/-/elegant-spinner-1.0.1.tgz", - "integrity": "sha512-B+ZM+RXvRqQaAmkMlO/oSe5nMUOaUnyfGYCEHoR8wrXsZR2mA0XVibsxV1bvTwxdRWah1PkQqso2EzhILGHtEQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/emoji-named-characters": { - "version": "1.0.2", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "license": "MIT" }, "node_modules/emoji-regex": { "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "license": "MIT" }, - "node_modules/emoji-server": { - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "emoji-named-characters": "~1.0.2" - } - }, "node_modules/encodeurl": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", "license": "MIT", "engines": { "node": ">= 0.8" } }, - "node_modules/encoding": { - "version": "0.1.13", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "iconv-lite": "^0.6.2" - } - }, "node_modules/encoding-down": { "version": "6.3.0", + "resolved": "https://registry.npmjs.org/encoding-down/-/encoding-down-6.3.0.tgz", + "integrity": "sha512-QKrV0iKR6MZVJV08QY0wp1e7vF6QbhnbQhb07bwpEyuz4uZiZgPlEGdkCROuFkUwdxlFaiPIhjyarH1ee/3vhw==", "license": "MIT", "dependencies": { "abstract-leveldown": "^6.2.1", @@ -4315,15 +2324,6 @@ "node": ">=6" } }, - "node_modules/end-of-stream": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, "node_modules/entities": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", @@ -4338,20 +2338,17 @@ }, "node_modules/env-paths": { "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", "license": "MIT", "engines": { "node": ">=6" } }, - "node_modules/env-var": { - "version": "7.5.0", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, "node_modules/envelope-js": { "version": "1.3.2", + "resolved": "https://registry.npmjs.org/envelope-js/-/envelope-js-1.3.2.tgz", + "integrity": "sha512-U3MQnyczN9QvwgXWlxDzAiEbNjwGD1aG+l1fNdwSZijOYboiu1aLHT8vIPSITShLiD4+n9p+EEwR79rp0vsKdw==", "license": "MIT", "dependencies": { "buffer-xor": "^2.0.2", @@ -4363,6 +2360,8 @@ }, "node_modules/envelope-spec": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/envelope-spec/-/envelope-spec-1.1.1.tgz", + "integrity": "sha512-7eIn5kw7eIt2H2YEPE5oWT8ATx9AYkVFKQi4GG+3qRxgcBKmPWt71OOI3DE4Do/hryZohh9eamoxGhSgZGQpIw==", "license": "LGPL-3.0-only", "dependencies": { "ssb-bfe": "^1.0.1" @@ -4370,6 +2369,8 @@ }, "node_modules/envelope-spec/node_modules/ssb-bfe": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ssb-bfe/-/ssb-bfe-1.1.0.tgz", + "integrity": "sha512-viY/ZzQENnO+gvahYQ35tf3OPV6ChQ+CxnyPpiloDZu7bzQNL0E24wjmoGefF+ftaCK/meeWKP3Hxm7pG50cFA==", "license": "LGPL-3.0", "dependencies": { "is-canonical-base64": "^1.1.1", @@ -4378,6 +2379,8 @@ }, "node_modules/epidemic-broadcast-trees": { "version": "9.0.4", + "resolved": "https://registry.npmjs.org/epidemic-broadcast-trees/-/epidemic-broadcast-trees-9.0.4.tgz", + "integrity": "sha512-f1Y2ayalcLQNn2I6umep29UDkTo+8nF4D//KulmwjDGP4ZPOO+ch8/LuxpXaMoeV6RO5arvvaEL53yzkKGSHTg==", "license": "MIT", "dependencies": { "push-stream": "^11.0.0" @@ -4385,6 +2388,8 @@ }, "node_modules/errno": { "version": "0.1.8", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", "license": "MIT", "dependencies": { "prr": "~1.0.1" @@ -4393,18 +2398,10 @@ "errno": "cli.js" } }, - "node_modules/error-ex": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", - "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, "node_modules/es-abstract": { "version": "1.24.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", + "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", "license": "MIT", "dependencies": { "array-buffer-byte-length": "^1.0.2", @@ -4471,6 +2468,8 @@ }, "node_modules/es-define-property": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -4478,6 +2477,8 @@ }, "node_modules/es-errors": { "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -4485,6 +2486,8 @@ }, "node_modules/es-get-iterator": { "version": "1.1.3", + "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", + "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", "license": "MIT", "dependencies": { "call-bind": "^1.0.2", @@ -4503,6 +2506,8 @@ }, "node_modules/es-map": { "version": "1.0.6", + "resolved": "https://registry.npmjs.org/es-map/-/es-map-1.0.6.tgz", + "integrity": "sha512-JWn54TeYJe93WHhduTh9MUqEkjhbCjU8vqq1q9FyCrOle/utr1i8PmzK2vXnP6BeLsBLvb1zV4fSkpVnT4LW1A==", "license": "MIT", "dependencies": { "call-bind": "^1.0.7", @@ -4526,6 +2531,8 @@ }, "node_modules/es-object-atoms": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -4536,6 +2543,8 @@ }, "node_modules/es-set": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-set/-/es-set-1.1.2.tgz", + "integrity": "sha512-tGLC1yArXhXwZTm3D6xbu1JL4S3cUtRRggbFWHhqTxpPc975fmcLL1VB0QOnAY8q5TBdul7YTjfEOT2VpUGcCg==", "license": "MIT", "dependencies": { "call-bind": "^1.0.7", @@ -4564,6 +2573,8 @@ }, "node_modules/es-set-tostringtag": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -4577,6 +2588,8 @@ }, "node_modules/es-to-primitive": { "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", "license": "MIT", "dependencies": { "is-callable": "^1.2.7", @@ -4590,8 +2603,50 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es5-ext": { + "version": "0.10.64", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz", + "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==", + "hasInstallScript": true, + "license": "ISC", + "dependencies": { + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.3", + "esniff": "^2.0.1", + "next-tick": "^1.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", + "license": "MIT", + "dependencies": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, + "node_modules/es6-symbol": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz", + "integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==", + "license": "ISC", + "dependencies": { + "d": "^1.0.2", + "ext": "^1.7.0" + }, + "engines": { + "node": ">=0.12" + } + }, "node_modules/escalade": { "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "license": "MIT", "engines": { "node": ">=6" @@ -4599,39 +2654,48 @@ }, "node_modules/escape-html": { "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "license": "MIT" }, - "node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "license": "MIT", + "node_modules/esniff": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", + "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==", + "license": "ISC", + "dependencies": { + "d": "^1.0.1", + "es5-ext": "^0.10.62", + "event-emitter": "^0.3.5", + "type": "^2.7.2" + }, "engines": { - "node": ">=0.8.0" + "node": ">=0.10" } }, - "node_modules/etag": { - "version": "1.8.1", + "node_modules/event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==", "license": "MIT", - "engines": { - "node": ">= 0.6" + "dependencies": { + "d": "1", + "es5-ext": "~0.10.14" } }, "node_modules/event-target-shim": { "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", "license": "MIT", "engines": { "node": ">=6" } }, - "node_modules/eventemitter3": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", - "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", - "license": "MIT" - }, "node_modules/events": { "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", "license": "MIT", "engines": { "node": ">=0.8.x" @@ -4646,272 +2710,55 @@ "bare-events": "^2.7.0" } }, - "node_modules/exit-hook": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-1.1.1.tgz", - "integrity": "sha512-MsG3prOVw1WtLXAZbM3KiYtooKR1LvxHh3VHsVtIy0uiUu8usxgB/94DP2HxtD/661lLdB6yzQ09lGJSQr6nkg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/expand-brackets": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", - "integrity": "sha512-hxx03P2dJxss6ceIeri9cmYOT4SRs3Zk3afZwWpOsRqLqprhTR8u++SlC+sFGsQr7WGFPdMF7Gjc1njDLDK6UA==", - "license": "MIT", - "dependencies": { - "is-posix-bracket": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/expand-range": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz", - "integrity": "sha512-AFASGfIlnIbkKPQwX1yHaDjFvh/1gyKJODme52V6IORh69uEYgZp0o9C+qsIGNVEiuuhQU0CSSl++Rlegg1qvA==", - "license": "MIT", - "dependencies": { - "fill-range": "^2.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/expand-template": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", - "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", - "license": "(MIT OR WTFPL)", - "engines": { - "node": ">=6" - } - }, "node_modules/explain-error": { "version": "1.0.4", + "resolved": "https://registry.npmjs.org/explain-error/-/explain-error-1.0.4.tgz", + "integrity": "sha512-/wSgNMxFusiYRy1rd19LT2SQlIXDppHpumpWo06wxjflD1OYxDLbl6rMVw+U3bxD5Nuhex4TKqv9Aem4D0lVzQ==", "license": "MIT" }, - "node_modules/express": { - "version": "5.1.0", - "license": "MIT", + "node_modules/ext": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", + "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", + "license": "ISC", "dependencies": { - "accepts": "^2.0.0", - "body-parser": "^2.2.0", - "content-disposition": "^1.0.0", - "content-type": "^1.0.5", - "cookie": "^0.7.1", - "cookie-signature": "^1.2.1", - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "finalhandler": "^2.1.0", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "merge-descriptors": "^2.0.0", - "mime-types": "^3.0.0", - "on-finished": "^2.4.1", - "once": "^1.4.0", - "parseurl": "^1.3.3", - "proxy-addr": "^2.0.7", - "qs": "^6.14.0", - "range-parser": "^1.2.1", - "router": "^2.2.0", - "send": "^1.1.0", - "serve-static": "^2.2.0", - "statuses": "^2.0.1", - "type-is": "^2.0.1", - "vary": "^1.1.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "type": "^2.7.2" } }, - "node_modules/express/node_modules/accepts": { - "version": "2.0.0", - "license": "MIT", - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express/node_modules/content-disposition": { - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express/node_modules/encodeurl": { - "version": "2.0.0", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/express/node_modules/fresh": { - "version": "2.0.0", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/express/node_modules/media-typer": { - "version": "1.1.0", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/express/node_modules/mime-db": { - "version": "1.54.0", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express/node_modules/mime-types": { - "version": "3.0.1", - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express/node_modules/negotiator": { - "version": "1.0.0", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express/node_modules/type-is": { - "version": "2.0.1", - "license": "MIT", - "dependencies": { - "content-type": "^1.0.5", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "license": "MIT" - }, - "node_modules/extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", - "license": "MIT", - "dependencies": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/extend-shallow/node_modules/is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "license": "MIT", - "dependencies": { - "is-plain-object": "^2.0.4" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/extend.js": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/extend.js/-/extend.js-0.0.2.tgz", - "integrity": "sha512-kSK5oO9X2i9qUptwhkilKqBfLG322xXY2ZO6/dlPY/ozt0fc+Ac9Qo6hZE/RiRTau5XUvVv2y6z1G6lNZ8f1WA==" - }, - "node_modules/extglob": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", - "integrity": "sha512-1FOj1LOwn42TMrruOHGt18HemVnbwAmAak7krWk+wa93KXxGbK+2jpezm+ytJYDaBX0/SPLZFHKM7m+tKobWGg==", - "license": "MIT", - "dependencies": { - "is-extglob": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fast-content-type-parse": { - "version": "3.0.0", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT" - }, "node_modules/fast-deep-equal": { "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "license": "MIT" }, "node_modules/fast-fifo": { "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", "license": "MIT" }, "node_modules/fast-varint": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fast-varint/-/fast-varint-1.0.1.tgz", + "integrity": "sha512-Vi9Db6dVe0GbWHtZxYzlpLbIq4o1FnVIaVud3RMYF/E7d4l0r37aepMY16qZBNfniw/o02BP9LAqCYCKjFTy+Q==", "license": "MIT" }, - "node_modules/fast-xml-parser": { - "version": "4.5.6", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.6.tgz", - "integrity": "sha512-Yd4vkROfJf8AuJrDIVMVmYfULKmIJszVsMv7Vo71aocsKgFxpdlpSHXSaInvyYfgw2PRuObQSW2GFpVMUjxu9A==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "dependencies": { - "strnum": "^1.0.5" - }, - "bin": { - "fxparser": "src/cli/cli.js" - } - }, "node_modules/fastintcompression": { "version": "0.0.4", + "resolved": "https://registry.npmjs.org/fastintcompression/-/fastintcompression-0.0.4.tgz", + "integrity": "sha512-sRtejNHcWjJl0Fr+gsbybbgOKKj0sQYvv7rA787EbeFu1fMpmzjHuBW8fLBImSZeqDuR++TZU2n60mRrpAUR9g==", "license": "Apache-2.0" }, "node_modules/fastpriorityqueue": { "version": "0.7.5", + "resolved": "https://registry.npmjs.org/fastpriorityqueue/-/fastpriorityqueue-0.7.5.tgz", + "integrity": "sha512-3Pa0n9gwy8yIbEsT3m2j/E9DXgWvvjfiZjjqcJ+AdNKTAlVMIuFYrYG5Y3RHEM8O6cwv9hOpOWY/NaMfywoQVA==", "license": "Apache-2.0" }, "node_modules/file-type": { "version": "16.5.4", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-16.5.4.tgz", + "integrity": "sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw==", "license": "MIT", "dependencies": { "readable-web-to-node-stream": "^3.0.0", @@ -4925,108 +2772,10 @@ "url": "https://github.com/sindresorhus/file-type?sponsor=1" } }, - "node_modules/file-uri-to-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", - "license": "MIT", - "optional": true - }, - "node_modules/filename-regex": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", - "integrity": "sha512-BTCqyBaWBTsauvnHiE8i562+EdJj+oUpkqWp2R1iCoR8f6oo8STRu3of7WJJ0TqWtxN50a5YFpzYK4Jj9esYfQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/filename-reserved-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-3.0.0.tgz", - "integrity": "sha512-hn4cQfU6GOT/7cFHXBqeBg2TbrMBgdD0kcjLhvSQYYwm3s4B6cjvBfb7nBALJLAXqmU5xajSa7X2NnUud/VCdw==", - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/filenamify": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-6.0.0.tgz", - "integrity": "sha512-vqIlNogKeyD3yzrm0yhRMQg8hOVwYcYRfjEoODd49iCprMn4HL85gK3HcykQE53EPIpX3HcAbGA5ELQv216dAQ==", - "license": "MIT", - "dependencies": { - "filename-reserved-regex": "^3.0.0" - }, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/fill-range": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.4.tgz", - "integrity": "sha512-cnrcCbj01+j2gTG921VZPnHbjmdAf8oQV/iGeV2kZxGSyfYjjTyY79ErsK1WJWMpw6DaApEX72binqJE+/d+5Q==", - "license": "MIT", - "dependencies": { - "is-number": "^2.1.0", - "isobject": "^2.0.0", - "randomatic": "^3.0.0", - "repeat-element": "^1.1.2", - "repeat-string": "^1.5.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/finalhandler": { - "version": "2.1.0", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "on-finished": "^2.4.1", - "parseurl": "^1.3.3", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/finalhandler/node_modules/encodeurl": { - "version": "2.0.0", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/flatbuffers": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/flatbuffers/-/flatbuffers-1.12.0.tgz", - "integrity": "sha512-c7CZADjRcl6j0PlvFy0ZqXQ67qSEZfrVPynmnL+2zPc+NtMvrF8Y0QceMo7QqnSPc7+uWjUIAbvCQ5WIKlMVdQ==", - "license": "SEE LICENSE IN LICENSE.txt" - }, "node_modules/flumecodec": { "version": "0.0.0", + "resolved": "https://registry.npmjs.org/flumecodec/-/flumecodec-0.0.0.tgz", + "integrity": "sha512-1UhNtCE8Al1mMrEqvRGxKU2GoZHXPKLWmAm6Oe2J9x+oQeMinCW9jpegJaEuGgUtro94MogbObFjEBFJHc36fA==", "license": "MIT", "dependencies": { "level-codec": "^6.2.0" @@ -5034,27 +2783,14 @@ }, "node_modules/flumecodec/node_modules/level-codec": { "version": "6.2.0", - "license": "MIT" - }, - "node_modules/flumedb": { - "version": "2.1.8", - "license": "MIT", - "dependencies": { - "cont": "^1.0.3", - "explain-error": "^1.0.3", - "obz": "1.0.2", - "pull-abortable": "^4.1.1", - "pull-cont": "^0.1.1", - "pull-looper": "^1.0.0", - "pull-stream": "^3.6.14" - } - }, - "node_modules/flumedb/node_modules/obz": { - "version": "1.0.2", + "resolved": "https://registry.npmjs.org/level-codec/-/level-codec-6.2.0.tgz", + "integrity": "sha512-J437PvCMZZKNT88+VRh6dkmh1ndZzwGwDzb5ZZl3QEsl+U9wIlt8hG+Y1gXVOhH75gf8JyceKGiG6WFUBbxyGQ==", "license": "MIT" }, "node_modules/flumelog-offset": { "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flumelog-offset/-/flumelog-offset-3.4.4.tgz", + "integrity": "sha512-sakCD+dVx541h3VeVq3Ti2lWPRrJf8PBRmnbm9EMBVLJnZkS3UD2lAlClZROxgKbh/JkMPyffvhDGv4VHNCVbA==", "license": "MIT", "dependencies": { "aligned-block-file": "^1.2.0", @@ -5069,96 +2805,10 @@ "uint48be": "^2.0.1" } }, - "node_modules/flumeview-hashtable": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/flumeview-hashtable/-/flumeview-hashtable-1.1.1.tgz", - "integrity": "sha512-73LAN0qF4zVMHMExYhK0z1swDo6FEh/bt1HF5/UnWkgI5JsECXOK2bTokWVU1levtr9bd+IxYChnJKJNnEzD0A==", - "license": "MIT", - "dependencies": { - "async-single": "^1.0.5", - "atomic-file": "^1.1.3", - "obv": "0.0.1", - "pull-stream": "^3.6.0" - } - }, - "node_modules/flumeview-hashtable/node_modules/atomic-file": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/atomic-file/-/atomic-file-1.1.5.tgz", - "integrity": "sha512-TG+5YFiaKQ6CZiSQsosGMJ/IJzwMZ4V/rSdEXlD6+DwKyv8OyeUcprq34kp4yuS6bfQYXhxBC2Vm8PWo+iKBGQ==", - "license": "MIT" - }, - "node_modules/flumeview-level": { - "version": "4.0.4", - "license": "MIT", - "dependencies": { - "charwise": "^3.0.1", - "explain-error": "^1.0.4", - "level": "^6.0.1", - "ltgt": "^2.1.3", - "mkdirp": "^1.0.4", - "obz": "^1.0.2", - "pull-level": "^2.0.3", - "pull-paramap": "^1.2.1", - "pull-stream": "^3.6.14", - "pull-write": "^1.1.1" - } - }, - "node_modules/flumeview-links": { - "version": "2.1.0", - "license": "MIT", - "dependencies": { - "deep-equal": "^2.0.3", - "flumeview-level": "^4.0.3", - "map-filter-reduce": "^3.2.2", - "pull-flatmap": "0.0.1", - "pull-stream": "^3.6.14" - } - }, - "node_modules/flumeview-links/node_modules/deep-equal": { - "version": "2.2.3", - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.0", - "call-bind": "^1.0.5", - "es-get-iterator": "^1.1.3", - "get-intrinsic": "^1.2.2", - "is-arguments": "^1.1.1", - "is-array-buffer": "^3.0.2", - "is-date-object": "^1.0.5", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "isarray": "^2.0.5", - "object-is": "^1.1.5", - "object-keys": "^1.1.1", - "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.5.1", - "side-channel": "^1.0.4", - "which-boxed-primitive": "^1.0.2", - "which-collection": "^1.0.1", - "which-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/flumeview-query": { - "version": "8.0.0", - "license": "MIT", - "dependencies": { - "deep-equal": "^1.0.1", - "flumeview-level": "^4.0.3", - "map-filter-reduce": "^3.2.0", - "pull-flatmap": "0.0.1", - "pull-paramap": "^1.1.3", - "pull-sink-through": "0.0.0", - "pull-stream": "^3.4.0" - } - }, "node_modules/flumeview-reduce": { "version": "1.4.0", + "resolved": "https://registry.npmjs.org/flumeview-reduce/-/flumeview-reduce-1.4.0.tgz", + "integrity": "sha512-ShCMtY5YZl3icnUAWTfwzRXSGN1AUo4Isj1wTRLpMMNgM9p1T0gc6N+r1ajampf1GO19SkqrgZHdPPOaEitvhg==", "license": "MIT", "dependencies": { "async-single": "^1.0.5", @@ -5170,16 +2820,6 @@ "pull-stream": "^3.5.0" } }, - "node_modules/flumeview-search": { - "version": "2.0.0", - "license": "MIT", - "dependencies": { - "flumeview-level": "^4.0.3", - "pull-paramap": "^1.2.2", - "pull-pushable": "^2.2.0", - "pull-stream": "^3.0.1" - } - }, "node_modules/follow-redirects": { "version": "1.16.0", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", @@ -5202,6 +2842,8 @@ }, "node_modules/for-each": { "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", "license": "MIT", "dependencies": { "is-callable": "^1.2.7" @@ -5213,29 +2855,10 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/for-in": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/for-own": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", - "integrity": "sha512-SKmowqGTJoPzLO1T0BBJpkfp3EMacCMOuH40hOUbrbzElVktk4DioXVM99QkLCyKoiuOmyjgcWMpVz2xjE7LZw==", - "license": "MIT", - "dependencies": { - "for-in": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/foreground-child": { "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", "license": "ISC", "dependencies": { "cross-spawn": "^7.0.6", @@ -5250,6 +2873,8 @@ }, "node_modules/form-data": { "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -5264,6 +2889,8 @@ }, "node_modules/formidable": { "version": "2.1.5", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.5.tgz", + "integrity": "sha512-Oz5Hwvwak/DCaXVVUtPn4oLMLLy1CdclLKO1LFgU7XzDpVMUU5UjlSLpGMocyQNNk8F6IJW9M/YdooSn2MRI+Q==", "license": "MIT", "dependencies": { "@paralleldrive/cuid2": "^2.2.2", @@ -5275,72 +2902,25 @@ "url": "https://ko-fi.com/tunnckoCore/commissions" } }, - "node_modules/forwarded": { - "version": "0.2.0", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fragment-cache": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", - "integrity": "sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA==", - "license": "MIT", - "dependencies": { - "map-cache": "^0.2.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/fresh": { "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", "license": "MIT", "engines": { "node": ">= 0.6" } }, - "node_modules/fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "license": "MIT" - }, - "node_modules/fs-extra": { - "version": "11.3.3", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.3.tgz", - "integrity": "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==", - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, "node_modules/fs.realpath": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "license": "ISC" }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/function-bind": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -5348,6 +2928,8 @@ }, "node_modules/function.prototype.name": { "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -5366,6 +2948,8 @@ }, "node_modules/functions-have-names": { "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -5373,6 +2957,8 @@ }, "node_modules/futoin-hkdf": { "version": "1.5.3", + "resolved": "https://registry.npmjs.org/futoin-hkdf/-/futoin-hkdf-1.5.3.tgz", + "integrity": "sha512-SewY5KdMpaoCeh7jachEWFsh1nNlaDjNHZXWqL5IGwtpEYHTgkr2+AMCgNwKWkcc0wpSYrZfR7he4WdmHFtDxQ==", "license": "Apache-2.0", "engines": { "node": ">=8" @@ -5380,6 +2966,8 @@ }, "node_modules/generate-function": { "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", "license": "MIT", "dependencies": { "is-property": "^1.0.2" @@ -5387,6 +2975,8 @@ }, "node_modules/generate-object-property": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz", + "integrity": "sha512-TuOwZWgJ2VAMEGJvAyPWvpqxSANF0LDpmyHauMjFYzaACvn+QTT/AZomvPCzVBV7yDN3OmwHQ5OvHaeLKre3JQ==", "license": "MIT", "dependencies": { "is-property": "^1.0.0" @@ -5394,25 +2984,17 @@ }, "node_modules/get-caller-file": { "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" } }, - "node_modules/get-east-asian-width": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz", - "integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/get-intrinsic": { "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -5435,6 +3017,8 @@ }, "node_modules/get-proto": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -5444,18 +3028,10 @@ "node": ">= 0.4" } }, - "node_modules/get-stdin": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", - "integrity": "sha512-F5aQMywwJ2n85s4hJPTT9RPxGmubonuB10MNYo17/xph174n2MIR33HRguhzVag10O/npM7SPk73LMZNP+FaWw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/get-symbol-description": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -5469,23 +3045,10 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-value": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", - "integrity": "sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/github-from-package": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", - "license": "MIT" - }, "node_modules/glob": { "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", @@ -5502,28 +3065,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/glob-base": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz", - "integrity": "sha512-ab1S1g1EbO7YzauaJLkgLp7DZVAqj9M/dvKlTt8DkXA2tiOIcSMrlVI2J1RZyB5iJVccEscjGn+kpOG9788MHA==", - "license": "MIT", - "dependencies": { - "glob-parent": "^2.0.0", - "is-glob": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/glob-parent": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", - "integrity": "sha512-JDYOvfxio/t42HKdxkAYaCiBN7oYiuxykOxKxdaUW5Qn0zaYN3gRQWolrwdnf0shM9/EP0ebuuTmyoXNr1cC5w==", - "license": "ISC", - "dependencies": { - "is-glob": "^2.0.0" - } - }, "node_modules/glob/node_modules/minimatch": { "version": "9.0.9", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", @@ -5539,18 +3080,10 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/globalthis": { "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", "license": "MIT", "dependencies": { "define-properties": "^1.2.1", @@ -5563,51 +3096,10 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/globby": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-4.1.0.tgz", - "integrity": "sha512-JPDtMSr0bt25W64q792rvlrSwIaZwqUAhqdYKSr57Wh/xBcQ5JDWLM85ndn+Q1WdBQXLb9YGCl0QN/T0HpqU0A==", - "license": "MIT", - "dependencies": { - "array-union": "^1.0.1", - "arrify": "^1.0.0", - "glob": "^6.0.1", - "object-assign": "^4.0.1", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/globby/node_modules/glob": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", - "integrity": "sha512-MKZeRNyYZAVVVG1oZeLaWie1uweH40m9AZwIwxyPbTSX4hHrVYSzLg0Ro5Z5R7XKkIX+Cc6oD1rqeDJnwsB8/A==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "license": "ISC", - "dependencies": { - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "2 || 3", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - } - }, - "node_modules/globby/node_modules/pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/gopd": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -5616,56 +3108,16 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/gossip-query": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/gossip-query/-/gossip-query-2.0.2.tgz", - "integrity": "sha512-17MflOq79BYcK01RrqHhtG5Dl88ztSu7gfD+4i1GrpzkbvLDKkJ7LoKiFKzBWzzd+Jd5VhIiLtQRvjRiKa6PUg==", - "license": "MIT", - "dependencies": { - "obv": "0.0.1", - "pull-stream": "^3.6.0" - } - }, "node_modules/graceful-fs": { "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "license": "ISC" }, - "node_modules/guid-typescript": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/guid-typescript/-/guid-typescript-1.0.9.tgz", - "integrity": "sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ==", - "license": "ISC" - }, - "node_modules/has": { - "version": "1.0.4", - "license": "MIT", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/has-ansi/node_modules/ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/has-bigints": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -5674,26 +3126,16 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/has-network": { - "version": "0.0.1", - "license": "MIT" - }, "node_modules/has-network2": { "version": "0.0.3", + "resolved": "https://registry.npmjs.org/has-network2/-/has-network2-0.0.3.tgz", + "integrity": "sha512-EvEZguA+LkyiS8G/Qks5I6imKnM2Z3NPN3eoQhviUQ7O6/d8nyZ7sDozBk6kTIA+Qj/S/V8ubRA1rqJcxc3qBQ==", "license": "MIT" }, "node_modules/has-property-descriptors": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "license": "MIT", "dependencies": { "es-define-property": "^1.0.0" @@ -5704,6 +3146,8 @@ }, "node_modules/has-proto": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", "license": "MIT", "dependencies": { "dunder-proto": "^1.0.0" @@ -5717,6 +3161,8 @@ }, "node_modules/has-symbols": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -5727,6 +3173,8 @@ }, "node_modules/has-tostringtag": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -5738,90 +3186,16 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", - "integrity": "sha512-IBXk4GTsLYdQ7Rvt+GRBrFSVEkmuOUy4re0Xjd9kJSUQpnTrWR4/y9RpfexN9vkAPMFuQoeWKwqzPozRTlasGw==", - "license": "MIT", - "dependencies": { - "get-value": "^2.0.6", - "has-values": "^1.0.0", - "isobject": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/has-value/node_modules/isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/has-values": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", - "integrity": "sha512-ODYZC64uqzmtfGMEAX/FvZiRyWLpAC3vYnNunURUnkGVTS+mI0smVsWaPydRBsE3g+ok7h960jChO8mFcWlHaQ==", - "license": "MIT", - "dependencies": { - "is-number": "^3.0.0", - "kind-of": "^4.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/has-values/node_modules/is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "license": "MIT" - }, - "node_modules/has-values/node_modules/is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", - "license": "MIT", - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/has-values/node_modules/is-number/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "license": "MIT", - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/has-values/node_modules/kind-of": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", - "integrity": "sha512-24XsCxmEbRwEDbz/qz3stgin8TTzZ1ESR56OMCN0ujYg+vRutNSiOj9bHH9u85DKgXguraugV5sFuvbD4FW/hw==", - "license": "MIT", - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/hashlru": { "version": "2.3.0", + "resolved": "https://registry.npmjs.org/hashlru/-/hashlru-2.3.0.tgz", + "integrity": "sha512-0cMsjjIC8I+D3M44pOQdsy0OHXGLVz6Z0beRuufhKa0KfaD2wGwAev6jILzXsd3/vpnNQJmWyZtIILqM1N+n5A==", "license": "MIT" }, "node_modules/hasown": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -5830,31 +3204,16 @@ "node": ">= 0.4" } }, - "node_modules/he": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/he/-/he-0.5.0.tgz", - "integrity": "sha512-DoufbNNOFzwRPy8uecq+j+VCPQ+JyDelHTmSgygrA5TsR8Cbw4Qcir5sGtWiusB4BdT89nmlaVDhSJOqC/33vw==", - "license": "MIT", - "bin": { - "he": "bin/he" - } - }, "node_modules/heap": { "version": "0.2.7", + "resolved": "https://registry.npmjs.org/heap/-/heap-0.2.7.tgz", + "integrity": "sha512-2bsegYkkHO+h/9MGbn6KWcE45cHZgPANo5LXF7EvWdT0yT2EguSVO1nDgU5c8+ZOPwp2vMNa7YFsJhVcDR9Sdg==", "license": "MIT" }, - "node_modules/hexpp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hexpp/-/hexpp-2.0.0.tgz", - "integrity": "sha512-5qB1Uar+ka6iQm++gOs3rZDH7q3d7gIxgwYRz00KF5OI4saTKEnGMsv5ebqW9XRkBXnkMUghd9b9b5gJpVROtA==", - "dev": true, - "license": "MIT", - "bin": { - "hexpp": "bin.js" - } - }, "node_modules/highlight.js": { "version": "11.0.0", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.0.0.tgz", + "integrity": "sha512-ByaTMfsSuoqerTwemOgpIhfULEIaK52JJYhky/sK7/Yqc0+t7Uh5DHay9vIC94YXSupnQ1Vqfc9VXrYP4eXW3Q==", "license": "BSD-3-Clause", "engines": { "node": ">=12.0.0" @@ -5862,17 +3221,14 @@ }, "node_modules/hoox": { "version": "0.0.1", + "resolved": "https://registry.npmjs.org/hoox/-/hoox-0.0.1.tgz", + "integrity": "sha512-4tKFjXcp8AWuw5lLTL7Xnixj1w88r+y1j9HKE8GoSeqDfsv6fLNMLjnrkB/H9tH+LqLp4+7eLss5IFS3Qra4lw==", "license": "MIT" }, - "node_modules/hosted-git-info": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", - "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", - "dev": true, - "license": "ISC" - }, "node_modules/html-element": { "version": "2.3.1", + "resolved": "https://registry.npmjs.org/html-element/-/html-element-2.3.1.tgz", + "integrity": "sha512-xnFt2ZkbFcjc+JoAtg3Hl89VeEZDjododu4VCPkRvFmBTHHA9U1Nt6hLUWfW2O+6Sl/rT1hHK/PivleX3PdBJQ==", "license": "MIT", "dependencies": { "class-list": "~0.1.1" @@ -5895,6 +3251,8 @@ }, "node_modules/html-tags": { "version": "3.3.1", + "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz", + "integrity": "sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==", "license": "MIT", "engines": { "node": ">=8" @@ -5903,8 +3261,38 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/htmlparser2": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", + "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.0.0", + "domutils": "^2.5.2", + "entities": "^2.0.0" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/http-assert": { "version": "1.5.0", + "resolved": "https://registry.npmjs.org/http-assert/-/http-assert-1.5.0.tgz", + "integrity": "sha512-uPpH7OKX4H25hBmU6G1jWNaqJGpTXxey+YOUizJUAgu0AjLUeC8D73hTrhvDS5D+GJN1DN1+hhc/eF/wpxtp0w==", "license": "MIT", "dependencies": { "deep-equal": "~1.0.1", @@ -5916,6 +3304,8 @@ }, "node_modules/http-assert/node_modules/depd": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -5923,6 +3313,8 @@ }, "node_modules/http-assert/node_modules/http-errors": { "version": "1.8.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", "license": "MIT", "dependencies": { "depd": "~1.1.2", @@ -5937,6 +3329,8 @@ }, "node_modules/http-assert/node_modules/statuses": { "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -5944,6 +3338,8 @@ }, "node_modules/http-errors": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", "license": "MIT", "dependencies": { "depd": "2.0.0", @@ -5984,6 +3380,8 @@ }, "node_modules/hyperaxe": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/hyperaxe/-/hyperaxe-2.0.1.tgz", + "integrity": "sha512-vp/x8kZnGoCQ5fwHtqQEFsv7sGoWFENNqSCKk46XjottHTL0feSW2LHSr6uK/JkeA8QydjoYPRjAl15mVHSM3g==", "license": "ISC", "dependencies": { "html-tags": "^3.0.0", @@ -5995,6 +3393,8 @@ }, "node_modules/hyperscript": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hyperscript/-/hyperscript-2.0.2.tgz", + "integrity": "sha512-uggBAYfHFC5WyZQXlJ61BNZbPmJbschcvfYNhYdZWCp+0J8KYb5Du8nQuk8Ru+ThoCNb01B0tPtnTRqnrFBkVg==", "license": "MIT", "dependencies": { "browser-split": "0.0.0", @@ -6002,20 +3402,10 @@ "html-element": "^2.0.0" } }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/idb-kv-store": { "version": "4.5.0", + "resolved": "https://registry.npmjs.org/idb-kv-store/-/idb-kv-store-4.5.0.tgz", + "integrity": "sha512-snvtAQRforYUI+C2+45L2LBJy/0/uQUffxv8/uwiS98fSUoXHVrFPClgzWZWxT0drwkLHJRm9inZcYzTR42GLA==", "license": "MIT", "dependencies": { "inherits": "^2.0.3", @@ -6024,6 +3414,8 @@ }, "node_modules/ieee754": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", "funding": [ { "type": "github", @@ -6040,43 +3432,36 @@ ], "license": "BSD-3-Clause" }, - "node_modules/ignore-by-default": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", - "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", - "dev": true, - "license": "ISC" - }, "node_modules/immediate": { "version": "3.3.0", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.3.0.tgz", + "integrity": "sha512-HR7EVodfFUdQCTIeySw+WDRFJlPcLOJbXfwwZ7Oom6tjsvZ3bOkCDJHehQC3nxJrv7+f9XecwazynjU8e4Vw3Q==", "license": "MIT" }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, "node_modules/increment-buffer": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/increment-buffer/-/increment-buffer-1.0.1.tgz", + "integrity": "sha512-djREfebnCm+rsaMKhxQ3iQI0uUicnKoWUt3eHBLAMqqh9PRJYb+gc2Sty7HYpXrE0JdgdND/wNZIqOyUcMdejw==", "license": "MIT" }, - "node_modules/indent-string": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-3.2.0.tgz", - "integrity": "sha512-BYqTHXTGUIvg7t1r4sJNKcbDZkL92nkXA8YtRpbjFHRHGDL/NtUeiBJMeE60kIFN/Mg8ESaWQvftaYMGJzQZCQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/indexof": { - "version": "0.0.1" - }, - "node_modules/infer-partial-order": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/infer-partial-order/-/infer-partial-order-0.1.1.tgz", - "integrity": "sha512-ahwueME5/IpvDbTWjG+ZawATtmvp7p04/ctMBPhwpvq8myV2FXXbJTJhvB8lgU2+1E2Nzipk+jbOmqZ+VZ3Dpw==", - "dev": true, - "license": "MIT" + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", + "integrity": "sha512-i0G7hLJ1z0DE8dsqJa2rycj9dBmNKgXBvotXtZYXakU9oivfB9Uj2ZBC27qqef2U58/ZLwalxa1X/RDCdkHtVg==" }, "node_modules/inflation": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/inflation/-/inflation-2.1.0.tgz", + "integrity": "sha512-t54PPJHG1Pp7VQvxyVCJ9mBbjG3Hqryges9bXoOO6GExCPa+//i/d5GSuFtpx3ALLd7lgIAur6zrIlBQyJuMlQ==", "license": "MIT", "engines": { "node": ">= 0.8.0" @@ -6084,6 +3469,8 @@ }, "node_modules/inflight": { "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "license": "ISC", "dependencies": { "once": "^1.3.0", @@ -6092,30 +3479,26 @@ }, "node_modules/inherits": { "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, "node_modules/ini": { "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "license": "ISC" }, "node_modules/int53": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/int53/-/int53-1.0.0.tgz", + "integrity": "sha512-u8BMiMa05OPBgd32CKTead0CVTsFVgwFk23nNXo1teKPF6Sxcu0lXxEzP//zTcaKzXbGgPDXGmj/woyv+I4C5w==", "license": "BSD-3-Clause" }, - "node_modules/interleavings": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/interleavings/-/interleavings-1.0.0.tgz", - "integrity": "sha512-DAzMdwFFmBZcVvi3cXiEXDY3SMZvSedK6qFICOk0A6ietIUMKyZSgXlyvssakingGkSOLN6SaGnQzqrI6KWIYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.1.1", - "infer-partial-order": "~0.1.1", - "rng": "~0.2.2" - } - }, "node_modules/internal-slot": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -6135,6 +3518,8 @@ }, "node_modules/ip-address": { "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", "license": "MIT", "dependencies": { "jsbn": "1.1.0", @@ -6146,205 +3531,14 @@ }, "node_modules/ip-address/node_modules/sprintf-js": { "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", "license": "BSD-3-Clause" }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/ipull": { - "version": "3.9.5", - "resolved": "https://registry.npmjs.org/ipull/-/ipull-3.9.5.tgz", - "integrity": "sha512-5w/yZB5lXmTfsvNawmvkCjYo4SJNuKQz/av8TC1UiOyfOHyaM+DReqbpU2XpWYfmY+NIUbRRH8PUAWsxaS+IfA==", - "license": "MIT", - "dependencies": { - "@tinyhttp/content-disposition": "^2.2.0", - "async-retry": "^1.3.3", - "chalk": "^5.3.0", - "ci-info": "^4.0.0", - "cli-spinners": "^2.9.2", - "commander": "^10.0.0", - "eventemitter3": "^5.0.1", - "filenamify": "^6.0.0", - "fs-extra": "^11.1.1", - "is-unicode-supported": "^2.0.0", - "lifecycle-utils": "^2.0.1", - "lodash.debounce": "^4.0.8", - "lowdb": "^7.0.1", - "pretty-bytes": "^6.1.0", - "pretty-ms": "^8.0.0", - "sleep-promise": "^9.1.0", - "slice-ansi": "^7.1.0", - "stdout-update": "^4.0.1", - "strip-ansi": "^7.1.0" - }, - "bin": { - "ipull": "dist/cli/cli.js" - }, - "engines": { - "node": ">=18.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/ido-pluto/ipull?sponsor=1" - }, - "optionalDependencies": { - "@reflink/reflink": "^0.1.16" - } - }, - "node_modules/ipull/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/ipull/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/ipull/node_modules/is-fullwidth-code-point": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", - "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", - "license": "MIT", - "dependencies": { - "get-east-asian-width": "^1.3.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ipull/node_modules/lifecycle-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/lifecycle-utils/-/lifecycle-utils-2.1.0.tgz", - "integrity": "sha512-AnrXnE2/OF9PHCyFg0RSqsnQTzV991XaZA/buhFDoc58xU7rhSCDgCz/09Lqpsn4MpoPHt7TRAXV1kWZypFVsA==", - "license": "MIT" - }, - "node_modules/ipull/node_modules/parse-ms": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-3.0.0.tgz", - "integrity": "sha512-Tpb8Z7r7XbbtBTrM9UhpkzzaMrqA2VXMT3YChzYltwV3P3pM6t8wl7TvpMnSTosz1aQAdVib7kdoys7vYOPerw==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ipull/node_modules/pretty-ms": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-8.0.0.tgz", - "integrity": "sha512-ASJqOugUF1bbzI35STMBUpZqdfYKlJugy6JBziGi2EE+AL5JPJGSzvpeVXojxrr0ViUYoToUjb5kjSEGf7Y83Q==", - "license": "MIT", - "dependencies": { - "parse-ms": "^3.0.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ipull/node_modules/slice-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", - "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.1", - "is-fullwidth-code-point": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, - "node_modules/ipull/node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/irregular-plurals": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/irregular-plurals/-/irregular-plurals-1.4.0.tgz", - "integrity": "sha512-kniTIJmaZYiwa17eTtWIfm0K342seyugl6vuC8DiiyiRAJWAVlLkqGCI0Im0neo0TkXw+pRcKaBPRdcKHnQJ6Q==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-accessor-descriptor": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.1.tgz", - "integrity": "sha512-YBUanLI8Yoihw923YeFUS5fs0fF2f5TSFTNiYAAzhhDscDa3lEqYuz1pDOEP5KvX94I9ey3vsqjJcLVFVU+3QA==", - "license": "MIT", - "dependencies": { - "hasown": "^2.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is-alphabetical": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz", - "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-alphanumerical": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz", - "integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==", - "license": "MIT", - "dependencies": { - "is-alphabetical": "^1.0.0", - "is-decimal": "^1.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/is-arguments": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -6359,6 +3553,8 @@ }, "node_modules/is-array-buffer": { "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -6372,15 +3568,10 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true, - "license": "MIT" - }, "node_modules/is-async-function": { "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", "license": "MIT", "dependencies": { "async-function": "^1.0.0", @@ -6398,6 +3589,8 @@ }, "node_modules/is-bigint": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", "license": "MIT", "dependencies": { "has-bigints": "^1.0.2" @@ -6409,20 +3602,10 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-binary-path": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", - "integrity": "sha512-9fRVlXc0uCxEDj1nQzaWONSpbTfx0FmJfzHF7pwlI8DkWGoHBBea4Pg5Ky0ojwwxQmnSifgbKkI06Qv0Ljgj+Q==", - "license": "MIT", - "dependencies": { - "binary-extensions": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-boolean-object": { "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -6435,33 +3618,16 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-buffer": { - "version": "2.0.5", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/is-buffer-zero": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-buffer-zero/-/is-buffer-zero-1.0.0.tgz", + "integrity": "sha512-eqgpqrTMGaAd5dQxg0dcZ79C8wTlDVYrM+zvB8kUXXSBzOqG5JeKWne1Zv9LDV3ePovx06fHLbp372OFbp/cIA==", "license": "MIT" }, "node_modules/is-callable": { "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -6472,10 +3638,14 @@ }, "node_modules/is-canonical-base64": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-canonical-base64/-/is-canonical-base64-1.1.1.tgz", + "integrity": "sha512-o6t/DwgEapC0bsloqtegAQyZzQXaQ5+8fzsyf2KmLqupC2ifLFq/lMQiFCJeGpdSrK1o6GL+WW2lRU050lLlFg==", "license": "MIT" }, "node_modules/is-core-module": { "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", "license": "MIT", "dependencies": { "hasown": "^2.0.2" @@ -6487,20 +3657,10 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-data-descriptor": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.1.tgz", - "integrity": "sha512-bc4NlCDiCr28U4aEsQ3Qs2491gVq4V8G7MQyws968ImqjKuYtTJXrl7Vq7jsN7Ly/C3xj5KWFrY7sHNeDkAzXw==", - "license": "MIT", - "dependencies": { - "hasown": "^2.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/is-data-view": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -6516,6 +3676,8 @@ }, "node_modules/is-date-object": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -6528,31 +3690,10 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-decimal": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz", - "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-descriptor": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.3.tgz", - "integrity": "sha512-JCNNGbwWZEVaSPtS45mdtrneRWJFp07LLmykxeFV5F6oBvNF8vHSfJuJgoT472pSfk+Mf8VnlrspaFBHWM8JAw==", - "license": "MIT", - "dependencies": { - "is-accessor-descriptor": "^1.0.1", - "is-data-descriptor": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/is-docker": { "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", "license": "MIT", "bin": { "is-docker": "cli.js" @@ -6564,47 +3705,10 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-dotfile": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz", - "integrity": "sha512-9YclgOGtN/f8zx0Pr4FQYMdibBiTaH3sn52vjYip4ZSf6C4/6RfTEZ+MR4GvKhCxdPh21Bg42/WL55f6KSnKpg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-equal-shallow": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz", - "integrity": "sha512-0EygVC5qPvIyb+gSz7zdD5/AAoS6Qrx1e//6N4yv4oNm30kqvdmG66oZFWVlQHUWe5OjP08FuTw2IdT0EOTcYA==", - "license": "MIT", - "dependencies": { - "is-primitive": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-extglob": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", - "integrity": "sha512-7Q+VbVafe6x2T+Tu6NcOf6sRklazEPmBoB3IWk3WdGZM2iGUwU/Oe3Wtq5lSEkDTTlpp8yx+5t4pzO/i9Ty1ww==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-finalizationregistry": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", "license": "MIT", "dependencies": { "call-bound": "^1.0.3" @@ -6616,21 +3720,10 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-finite": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.1.0.tgz", - "integrity": "sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "license": "MIT", "engines": { "node": ">=8" @@ -6638,6 +3731,8 @@ }, "node_modules/is-generator-function": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", + "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -6652,42 +3747,10 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-glob": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", - "integrity": "sha512-a1dBeB19NXsf/E0+FHqkagizel/LQw2DjSQpvQrj3zT+jYPpaUCryPnrQajXKFLCMuf4I6FhRpaGtw4lPrG6Eg==", - "license": "MIT", - "dependencies": { - "is-extglob": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-hexadecimal": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz", - "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-interactive": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", - "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-map": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -6698,10 +3761,14 @@ }, "node_modules/is-my-ip-valid": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-my-ip-valid/-/is-my-ip-valid-1.0.1.tgz", + "integrity": "sha512-jxc8cBcOWbNK2i2aTkCZP6i7wkHF1bqKFrwEHuN5Jtg5BSaZHUZQ/JTOJwoV41YvHnOaRyWWh72T/KvfNz9DJg==", "license": "MIT" }, "node_modules/is-my-json-valid": { "version": "2.20.6", + "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.20.6.tgz", + "integrity": "sha512-1JQwulVNjx8UqkPE/bqDaxtH4PXCe/2VRh/y3p99heOV87HG4Id5/VfDswd+YiAfHcRTfDlWgISycnHuhZq1aw==", "license": "MIT", "dependencies": { "generate-function": "^2.0.0", @@ -6713,6 +3780,8 @@ }, "node_modules/is-my-ssb-valid": { "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-my-ssb-valid/-/is-my-ssb-valid-1.2.2.tgz", + "integrity": "sha512-13B1KdYuXwfVASWWWAKcbtKHtXhFNzLtg16oKh/3UTAWVh9GBdf/zjvY9BFY3xYsYFQYC1WhZEuifbYRzeiz7w==", "license": "LGPL-3.0-only", "dependencies": { "is-my-json-valid": "^2.20.0", @@ -6721,6 +3790,8 @@ }, "node_modules/is-negative-zero": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -6729,20 +3800,10 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-number": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", - "integrity": "sha512-QUzH43Gfb9+5yckcrSA0VBDwEtDUchrk4F6tfJZQuNzDJbEDB9cZNzSfXGQ1jqmdDY/kl41lUOWM9syA8z8jlg==", - "license": "MIT", - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-number-object": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -6757,76 +3818,29 @@ }, "node_modules/is-options": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-options/-/is-options-1.0.2.tgz", + "integrity": "sha512-u+Ai74c8Q74aS8BuHwPdI1jptGOT1FQXgCq8/zv0xRuE+wRgSMEJLj8lVO8Zp9BeGb29BXY6AsNPinfqjkr7Fg==", "license": "MIT", "dependencies": { "b4a": "^1.1.1" } }, - "node_modules/is-plain-obj": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", - "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "license": "MIT", - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-plain-object/node_modules/isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-posix-bracket": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz", - "integrity": "sha512-Yu68oeXJ7LeWNmZ3Zov/xg/oDBnBK2RNxwYY1ilNJX+tKKZqgPK+qOn/Gs9jEu66KDY9Netf5XLKNGzas/vPfQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-potential-custom-element-name": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", "license": "MIT" }, - "node_modules/is-primitive": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz", - "integrity": "sha512-N3w1tFaRfk3UrPfqeRyD+GYDASU3W5VinKhlORy8EWVf/sIdDL9GAcew85XmktCfH+ngG7SRXEVDoO18WMdB/Q==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-promise": { - "version": "4.0.0", - "license": "MIT" - }, "node_modules/is-property": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", "license": "MIT" }, "node_modules/is-regex": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -6843,6 +3857,8 @@ }, "node_modules/is-set": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -6853,6 +3869,8 @@ }, "node_modules/is-shared-array-buffer": { "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", "license": "MIT", "dependencies": { "call-bound": "^1.0.3" @@ -6866,6 +3884,8 @@ }, "node_modules/is-stream": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", "license": "MIT", "engines": { "node": ">=8" @@ -6876,6 +3896,8 @@ }, "node_modules/is-string": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -6888,21 +3910,10 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-svg": { - "version": "4.4.0", - "license": "MIT", - "dependencies": { - "fast-xml-parser": "^4.1.3" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-symbol": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -6918,6 +3929,8 @@ }, "node_modules/is-typed-array": { "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", "license": "MIT", "dependencies": { "which-typed-array": "^1.1.16" @@ -6931,29 +3944,14 @@ }, "node_modules/is-typedarray": { "version": "1.0.0", - "license": "MIT" - }, - "node_modules/is-unicode-supported": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", - "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-utf8": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", - "integrity": "sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q==", - "dev": true, + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", "license": "MIT" }, "node_modules/is-weakmap": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -6964,6 +3962,8 @@ }, "node_modules/is-weakref": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", "license": "MIT", "dependencies": { "call-bound": "^1.0.3" @@ -6977,6 +3977,8 @@ }, "node_modules/is-weakset": { "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -6989,17 +3991,10 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-windows": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-wsl": { "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", "license": "MIT", "dependencies": { "is-docker": "^2.0.0" @@ -7010,86 +4005,20 @@ }, "node_modules/isarray": { "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", "license": "MIT" }, "node_modules/isexe": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "license": "ISC" }, - "node_modules/isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==", - "license": "MIT", - "dependencies": { - "isarray": "1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/isobject/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "license": "MIT" - }, - "node_modules/isomorphic-fetch": { - "version": "3.0.0", - "license": "MIT", - "dependencies": { - "node-fetch": "^2.6.1", - "whatwg-fetch": "^3.4.1" - } - }, - "node_modules/isomorphic-ws": { - "version": "5.0.0", - "license": "MIT", - "peerDependencies": { - "ws": "*" - } - }, - "node_modules/istanbul-lib-coverage": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz", - "integrity": "sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=6" - } - }, - "node_modules/istanbul-lib-instrument": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-3.3.0.tgz", - "integrity": "sha512-5nnIN4vo5xQZHdXno/YDXJ0G+I3dAm4XgzfSVTPLQpj/zAV2dV6Juy0yaf10/zrJOJeHoN3fraFe+XRq2bFVZA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@babel/generator": "^7.4.0", - "@babel/parser": "^7.4.3", - "@babel/template": "^7.4.0", - "@babel/traverse": "^7.4.3", - "@babel/types": "^7.4.0", - "istanbul-lib-coverage": "^2.0.5", - "semver": "^6.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/istanbul-lib-instrument/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/iterate-iterator": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/iterate-iterator/-/iterate-iterator-1.0.2.tgz", + "integrity": "sha512-t91HubM4ZDQ70M9wqp+pcNpu8OyJ9UAtXntT/Bcsvp5tZMnz9vRa+IunKXeI8AnfZMTv0jNuVEmGeLSMjVvfPw==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -7097,6 +4026,8 @@ }, "node_modules/iterate-value": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/iterate-value/-/iterate-value-1.0.2.tgz", + "integrity": "sha512-A6fMAio4D2ot2r/TYzr4yUWrmwNdsN5xL7+HUiyACE4DXm+q8HtPcnFTp+NnW3k4N05tZ7FVYFFb2CR13NxyHQ==", "license": "MIT", "dependencies": { "es-get-iterator": "^1.0.2", @@ -7108,6 +4039,8 @@ }, "node_modules/jackspeak": { "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/cliui": "^8.0.2" @@ -7121,6 +4054,8 @@ }, "node_modules/jitdb": { "version": "7.0.7", + "resolved": "https://registry.npmjs.org/jitdb/-/jitdb-7.0.7.tgz", + "integrity": "sha512-D/b1uyZzEvQewg9gDamgkCjvCji3ZYnirG86p3VHm/UzCohASd6ojLZrRPZHlsDRWsO5qhZxBzWoH7fSRtztdA==", "license": "LGPL-3.0", "dependencies": { "atomic-file-rw": "^0.3.0", @@ -7152,15 +4087,10 @@ "async-append-only-log": "^4.3.2" } }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, - "license": "MIT" - }, "node_modules/jsbn": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", "license": "MIT" }, "node_modules/jsdom": { @@ -7239,6 +4169,8 @@ }, "node_modules/jsesc": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", "license": "MIT", "bin": { "jsesc": "bin/jsesc" @@ -7247,27 +4179,10 @@ "node": ">=6" } }, - "node_modules/json-parse-better-errors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", - "dev": true, - "license": "MIT" - }, - "node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, "node_modules/jsonpointer": { "version": "5.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", + "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -7275,6 +4190,8 @@ }, "node_modules/key-value-file-store": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/key-value-file-store/-/key-value-file-store-1.1.1.tgz", + "integrity": "sha512-j2Vq2ftRSt99InoUUvMBgLX6BLrHZSa8nedntZyhqj2njzBaeL3IiUpT3DYx2p2+Fbf4vcmn74rJcds0uGpc0g==", "license": "MIT", "dependencies": { "atomic-file-rw": "^0.3.0" @@ -7282,6 +4199,8 @@ }, "node_modules/keygrip": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz", + "integrity": "sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==", "license": "MIT", "dependencies": { "tsscmp": "1.0.6" @@ -7290,24 +4209,6 @@ "node": ">= 0.6" } }, - "node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "license": "MIT", - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/kind-of/node_modules/is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "license": "MIT" - }, "node_modules/koa": { "version": "2.16.4", "resolved": "https://registry.npmjs.org/koa/-/koa-2.16.4.tgz", @@ -7344,6 +4245,8 @@ }, "node_modules/koa-body": { "version": "6.0.1", + "resolved": "https://registry.npmjs.org/koa-body/-/koa-body-6.0.1.tgz", + "integrity": "sha512-M8ZvMD8r+kPHy28aWP9VxL7kY8oPWA+C7ZgCljrCMeaU7uX6wsIQgDHskyrAr9sw+jqnIXyv4Mlxri5R4InIJg==", "license": "MIT", "dependencies": { "@types/co-body": "^6.1.0", @@ -7356,10 +4259,14 @@ }, "node_modules/koa-compose": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-4.1.0.tgz", + "integrity": "sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==", "license": "MIT" }, "node_modules/koa-convert": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/koa-convert/-/koa-convert-2.0.0.tgz", + "integrity": "sha512-asOvN6bFlSnxewce2e/DK3p4tltyfC4VM7ZwuTuepI7dEQVcvpyFuBcEARu1+Hxg8DIwytce2n7jrZtRlPrARA==", "license": "MIT", "dependencies": { "co": "^4.6.0", @@ -7371,6 +4278,8 @@ }, "node_modules/koa-mount": { "version": "4.2.0", + "resolved": "https://registry.npmjs.org/koa-mount/-/koa-mount-4.2.0.tgz", + "integrity": "sha512-2iHQc7vbA9qLeVq5gKAYh3m5DOMMlMfIKjW/REPAS18Mf63daCJHHVXY9nbu7ivrnYn5PiPC4CE523Tf5qvjeQ==", "license": "MIT", "dependencies": { "debug": "^4.0.1", @@ -7382,6 +4291,8 @@ }, "node_modules/koa-send": { "version": "5.0.1", + "resolved": "https://registry.npmjs.org/koa-send/-/koa-send-5.0.1.tgz", + "integrity": "sha512-tmcyQ/wXXuxpDxyNXv5yNNkdAMdFRqwtegBXUaowiQzUKqJehttS0x2j0eOZDQAyloAth5w6wwBImnFzkUz3pQ==", "license": "MIT", "dependencies": { "debug": "^4.1.1", @@ -7394,6 +4305,8 @@ }, "node_modules/koa-send/node_modules/depd": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -7401,6 +4314,8 @@ }, "node_modules/koa-send/node_modules/http-errors": { "version": "1.8.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", "license": "MIT", "dependencies": { "depd": "~1.1.2", @@ -7415,6 +4330,8 @@ }, "node_modules/koa-send/node_modules/statuses": { "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -7422,6 +4339,8 @@ }, "node_modules/koa-static": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/koa-static/-/koa-static-5.0.0.tgz", + "integrity": "sha512-UqyYyH5YEXaJrf9S8E23GoJFQZXkBVJ9zYYMPGz919MSX1KuvAcycIuS0ci150HCoPf4XQVhQ84Qf8xRPWxFaQ==", "license": "MIT", "dependencies": { "debug": "^3.1.0", @@ -7433,6 +4352,8 @@ }, "node_modules/koa/node_modules/http-errors": { "version": "1.8.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", "license": "MIT", "dependencies": { "depd": "~1.1.2", @@ -7447,6 +4368,8 @@ }, "node_modules/koa/node_modules/http-errors/node_modules/depd": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -7454,6 +4377,8 @@ }, "node_modules/koa/node_modules/statuses": { "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -7461,6 +4386,8 @@ }, "node_modules/layered-graph": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/layered-graph/-/layered-graph-1.2.0.tgz", + "integrity": "sha512-VnjZHUyaXXsOR6KZ8JjTbkyjtq8VaU5ncGJ8cKBvg2bSLqVHE4bcGJ+20dDE7T84sss9d/BCHRj34Yh2fz9sbg==", "license": "MIT", "dependencies": { "dynamic-dijkstra": "^1.0.0", @@ -7471,6 +4398,8 @@ }, "node_modules/lazystream": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", "license": "MIT", "dependencies": { "readable-stream": "^2.0.5" @@ -7481,10 +4410,14 @@ }, "node_modules/lazystream/node_modules/isarray": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", "license": "MIT" }, "node_modules/lazystream/node_modules/readable-stream": { "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "license": "MIT", "dependencies": { "core-util-is": "~1.0.0", @@ -7498,10 +4431,14 @@ }, "node_modules/lazystream/node_modules/safe-buffer": { "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "license": "MIT" }, "node_modules/lazystream/node_modules/string_decoder": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "license": "MIT", "dependencies": { "safe-buffer": "~5.1.0" @@ -7509,6 +4446,8 @@ }, "node_modules/level": { "version": "6.0.1", + "resolved": "https://registry.npmjs.org/level/-/level-6.0.1.tgz", + "integrity": "sha512-psRSqJZCsC/irNhfHzrVZbmPYXDcEYhA5TVNwr+V92jF44rbf86hqGp8fiT702FyiArScYIlPSBTDUASCVNSpw==", "license": "MIT", "dependencies": { "level-js": "^5.0.0", @@ -7525,6 +4464,8 @@ }, "node_modules/level-codec": { "version": "9.0.2", + "resolved": "https://registry.npmjs.org/level-codec/-/level-codec-9.0.2.tgz", + "integrity": "sha512-UyIwNb1lJBChJnGfjmO0OR+ezh2iVu1Kas3nvBS/BzGnx79dv6g7unpKIDNPMhfdTEGoc7mC8uAu51XEtX+FHQ==", "license": "MIT", "dependencies": { "buffer": "^5.6.0" @@ -7535,6 +4476,8 @@ }, "node_modules/level-codec/node_modules/buffer": { "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", "funding": [ { "type": "github", @@ -7557,6 +4500,8 @@ }, "node_modules/level-concat-iterator": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/level-concat-iterator/-/level-concat-iterator-2.0.1.tgz", + "integrity": "sha512-OTKKOqeav2QWcERMJR7IS9CUo1sHnke2C0gkSmcR7QuEtFNLLzHQAvnMw8ykvEcv0Qtkg0p7FOwP1v9e5Smdcw==", "license": "MIT", "engines": { "node": ">=6" @@ -7564,6 +4509,8 @@ }, "node_modules/level-errors": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/level-errors/-/level-errors-2.0.1.tgz", + "integrity": "sha512-UVprBJXite4gPS+3VznfgDSU8PTRuVX0NXwoWW50KLxd2yw4Y1t2JUR5In1itQnudZqRMT9DlAM3Q//9NCjCFw==", "license": "MIT", "dependencies": { "errno": "~0.1.1" @@ -7574,6 +4521,8 @@ }, "node_modules/level-iterator-stream": { "version": "4.0.2", + "resolved": "https://registry.npmjs.org/level-iterator-stream/-/level-iterator-stream-4.0.2.tgz", + "integrity": "sha512-ZSthfEqzGSOMWoUGhTXdX9jv26d32XJuHz/5YnuHZzH6wldfWMOVwI9TBtKcya4BKTyTt3XVA0A3cF3q5CY30Q==", "license": "MIT", "dependencies": { "inherits": "^2.0.4", @@ -7586,6 +4535,8 @@ }, "node_modules/level-iterator-stream/node_modules/readable-stream": { "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "license": "MIT", "dependencies": { "inherits": "^2.0.3", @@ -7598,6 +4549,8 @@ }, "node_modules/level-js": { "version": "5.0.2", + "resolved": "https://registry.npmjs.org/level-js/-/level-js-5.0.2.tgz", + "integrity": "sha512-SnBIDo2pdO5VXh02ZmtAyPP6/+6YTJg2ibLtl9C34pWvmtMEmRTWpra+qO/hifkUtBTOtfx6S9vLDjBsBK4gRg==", "license": "MIT", "dependencies": { "abstract-leveldown": "~6.2.3", @@ -7608,6 +4561,8 @@ }, "node_modules/level-js/node_modules/buffer": { "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", "funding": [ { "type": "github", @@ -7630,6 +4585,8 @@ }, "node_modules/level-packager": { "version": "5.1.1", + "resolved": "https://registry.npmjs.org/level-packager/-/level-packager-5.1.1.tgz", + "integrity": "sha512-HMwMaQPlTC1IlcwT3+swhqf/NUO+ZhXVz6TY1zZIIZlIR0YSn8GtAAWmIvKjNY16ZkEg/JcpAuQskxsXqC0yOQ==", "license": "MIT", "dependencies": { "encoding-down": "^6.3.0", @@ -7641,31 +4598,17 @@ }, "node_modules/level-post": { "version": "1.0.7", + "resolved": "https://registry.npmjs.org/level-post/-/level-post-1.0.7.tgz", + "integrity": "sha512-PWYqG4Q00asOrLhX7BejSajByB4EmG2GaKHfj3h5UmmZ2duciXLPGYWIjBzLECFWUGOZWlm5B20h/n3Gs3HKew==", "license": "MIT", "dependencies": { "ltgt": "^2.1.2" } }, - "node_modules/level-supports": { - "version": "6.2.0", - "license": "MIT", - "engines": { - "node": ">=16" - } - }, - "node_modules/level-transcoder": { - "version": "1.0.1", - "license": "MIT", - "dependencies": { - "buffer": "^6.0.3", - "module-error": "^1.0.1" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/leveldown": { "version": "5.6.0", + "resolved": "https://registry.npmjs.org/leveldown/-/leveldown-5.6.0.tgz", + "integrity": "sha512-iB8O/7Db9lPaITU1aA2txU/cBEXAt4vWwKQRrrWuS6XDgbP4QZGj9BL2aNbwb002atoQ/lIotJkfyzz+ygQnUQ==", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -7679,6 +4622,8 @@ }, "node_modules/leveldown/node_modules/node-gyp-build": { "version": "4.1.1", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.1.1.tgz", + "integrity": "sha512-dSq1xmcPDKPZ2EED2S6zw/b9NKsqzXRE6dVr8TVQnI3FJOTteUMuqF3Qqs6LZg+mLGYJWqQzMbIjMtJqTv87nQ==", "license": "MIT", "bin": { "node-gyp-build": "bin.js", @@ -7688,6 +4633,8 @@ }, "node_modules/levelup": { "version": "4.4.0", + "resolved": "https://registry.npmjs.org/levelup/-/levelup-4.4.0.tgz", + "integrity": "sha512-94++VFO3qN95cM/d6eBXvd894oJE0w3cInq9USsyQzzoJxmiYzPAocNcuGCPGGjoXqDVJcr3C1jzt1TSjyaiLQ==", "license": "MIT", "dependencies": { "deferred-leveldown": "~5.3.0", @@ -7702,6 +4649,8 @@ }, "node_modules/levelup/node_modules/level-supports": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/level-supports/-/level-supports-1.0.1.tgz", + "integrity": "sha512-rXM7GYnW8gsl1vedTJIbzOrRv85c/2uCMpiiCzO2fndd06U/kUXEEU9evYn4zFggBOg36IsBW8LzqIpETwwQzg==", "license": "MIT", "dependencies": { "xtend": "^4.0.2" @@ -7710,67 +4659,21 @@ "node": ">=6" } }, - "node_modules/libnested": { - "version": "1.5.2", - "license": "MIT" - }, "node_modules/libsodium": { "version": "0.7.15", + "resolved": "https://registry.npmjs.org/libsodium/-/libsodium-0.7.15.tgz", + "integrity": "sha512-sZwRknt/tUpE2AwzHq3jEyUU5uvIZHtSssktXq7owd++3CSgn8RGrv6UZJJBpP7+iBghBqe7Z06/2M31rI2NKw==", "license": "ISC" }, "node_modules/libsodium-wrappers": { "version": "0.7.15", + "resolved": "https://registry.npmjs.org/libsodium-wrappers/-/libsodium-wrappers-0.7.15.tgz", + "integrity": "sha512-E4anqJQwcfiC6+Yrl01C1m8p99wEhLmJSs0VQqST66SbQXXBoaJY0pF4BNjRYa/sOQAxx6lXAaAFIlx+15tXJQ==", "license": "ISC", "dependencies": { "libsodium": "^0.7.15" } }, - "node_modules/lifecycle-utils": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/lifecycle-utils/-/lifecycle-utils-3.1.1.tgz", - "integrity": "sha512-gNd3OvhFNjHykJE3uGntz7UuPzWlK9phrIdXxU9Adis0+ExkwnZibfxCJWiWWZ+a6VbKiZrb+9D9hCQWd4vjTg==", - "license": "MIT" - }, - "node_modules/load-json-file": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", - "integrity": "sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.1.2", - "parse-json": "^4.0.0", - "pify": "^3.0.0", - "strip-bom": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/locate-path/node_modules/path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/lodash": { "version": "4.18.1", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", @@ -7779,30 +4682,38 @@ }, "node_modules/lodash.chunk": { "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.chunk/-/lodash.chunk-4.2.0.tgz", + "integrity": "sha512-ZzydJKfUHJwHa+hF5X66zLFCBrWn5GeF28OHEr4WVWtNDXlQ/IjWKPBiikqKo2ne0+v6JgCgJ0GzJp8k8bHC7w==", "license": "MIT" }, "node_modules/lodash.clone": { "version": "4.5.0", - "license": "MIT" - }, - "node_modules/lodash.clonedeep": { - "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clone/-/lodash.clone-4.5.0.tgz", + "integrity": "sha512-GhrVeweiTD6uTmmn5hV/lzgCQhccwReIVRLHp7LT4SopOjqEZ5BbX8b5WWEtAKasjmy8hR7ZPwsYlxRCku5odg==", "license": "MIT" }, "node_modules/lodash.debounce": { "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", "license": "MIT" }, "node_modules/lodash.find": { "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.find/-/lodash.find-4.6.0.tgz", + "integrity": "sha512-yaRZoAV3Xq28F1iafWN1+a0rflOej93l1DQUejs3SZ41h2O9UJBoS9aueGjPDgAl4B6tPC0NuuchLKaDQQ3Isg==", "license": "MIT" }, "node_modules/lodash.get": { "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", "license": "MIT" }, "node_modules/lodash.isequal": { "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", "license": "MIT" }, "node_modules/lodash.set": { @@ -7812,248 +4723,39 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "license": "MIT" }, - "node_modules/log-symbols": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz", - "integrity": "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==", - "license": "MIT", - "dependencies": { - "is-unicode-supported": "^2.0.0", - "yoctocolors": "^2.1.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/log-update/-/log-update-1.0.2.tgz", - "integrity": "sha512-4vSow8gbiGnwdDNrpy1dyNaXWKSCIPop0EHdE8GrnngHoJujM3QhvHUN/igsYCgPoHo7pFOezlJ61Hlln0KHyA==", - "license": "MIT", - "dependencies": { - "ansi-escapes": "^1.0.0", - "cli-cursor": "^1.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/log-update/node_modules/ansi-escapes": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-1.4.0.tgz", - "integrity": "sha512-wiXutNjDUlNEDWHcYH3jtZUhd3c4/VojassD8zHdHCY13xbZy2XbW+NKQwA0tWGBVzDA9qEzYwfoSsWmviidhw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/log-update/node_modules/cli-cursor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz", - "integrity": "sha512-25tABq090YNKkF6JH7lcwO0zFJTRke4Jcq9iX2nr/Sz0Cjjv4gckmwlW6Ty/aoyFd6z3ysR2hMGC2GFugmBo6A==", - "license": "MIT", - "dependencies": { - "restore-cursor": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/log-update/node_modules/onetime": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz", - "integrity": "sha512-GZ+g4jayMqzCRMgB2sol7GiCLjKfS1PINkjmx8spcKce1LiVqcbQreXwqs2YAFXC6R03VIG28ZS31t8M866v6A==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/log-update/node_modules/restore-cursor": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-1.0.1.tgz", - "integrity": "sha512-reSjH4HuiFlxlaBaFCiS6O76ZGG2ygKoSlCsipKdaZuKSPx/+bt9mULkn4l0asVzbEfQQmXRg6Wp6gv6m0wElw==", - "license": "MIT", - "dependencies": { - "exit-hook": "^1.0.0", - "onetime": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/long": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", - "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==", - "license": "Apache-2.0" - }, - "node_modules/longest-streak": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-1.0.0.tgz", - "integrity": "sha512-84jGpz/1j02Xm/L4y4uEXGxFFPHFabKjMHQ+rEPi0gPQbD5p0J3aZomvk0ZpUPpTtcVqhtSEq+4WNQbJjWiZ1Q==", - "license": "MIT" - }, "node_modules/looper": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/looper/-/looper-4.0.0.tgz", + "integrity": "sha512-NjGRcX4vCwyfbujv03omakGfAYh6St5kVsZFKfU23MFO1Z9/mZT8ypTZMEnvVC7nJeYtbqkRPFV4GoJBPdJgYw==", "license": "MIT" }, - "node_modules/lossy-store": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lossy-store/-/lossy-store-1.2.4.tgz", - "integrity": "sha512-rvqTCHZxWLvHAJv2w5vdCr5xICAW/FiXQ9wZGaAfBlNGG2FuLX0URNcj/zXC3qr5zQ8lBCbW8EEmDCKi7Dsp1g==", - "license": "MIT", - "dependencies": { - "mkdirp": "^0.5.1", - "tape": "^4.6.3" - } - }, - "node_modules/lossy-store/node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "license": "MIT", - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/loud-rejection": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", - "integrity": "sha512-RPNliZOFkqFumDhvYqOaNY4Uz9oJM2K9tC6JWsJJsNdhuONW4LQHRBpb0qf4pJApVffI5N39SwzWZJuEhfd7eQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "currently-unhandled": "^0.4.1", - "signal-exit": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/loud-rejection/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/lowdb": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/lowdb/-/lowdb-7.0.1.tgz", - "integrity": "sha512-neJAj8GwF0e8EpycYIDFqEPcx9Qz4GUho20jWFR7YiFeXzF1YMLdxB36PypcTSPMA+4+LvgyMacYhlr18Zlymw==", - "license": "MIT", - "dependencies": { - "steno": "^4.0.2" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/typicode" - } - }, "node_modules/lru-cache": { "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "license": "ISC" }, "node_modules/ltgt": { "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ltgt/-/ltgt-2.2.1.tgz", + "integrity": "sha512-AI2r85+4MquTw9ZYqabu4nMwy9Oftlfa/e/52t9IjtfG+mGBbTNdAoZ3RQKLHR6r0wQnwZnPIEh/Ya6XTWAKNA==", "license": "MIT" }, - "node_modules/map-cache": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", - "integrity": "sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/map-filter-reduce": { - "version": "3.2.2", - "license": "MIT", - "dependencies": { - "binary-search": "^1.2.0", - "compare-at-paths": "^1.0.0", - "pull-sink-through": "0.0.0", - "pull-sort": "^1.0.1", - "pull-stream": "^3.4.3", - "typewiselite": "^1.0.0" - } - }, "node_modules/map-merge": { "version": "1.1.0", - "license": "MIT" - }, - "node_modules/map-obj": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-2.0.0.tgz", - "integrity": "sha512-TzQSV2DiMYgoF5RycneKVUzIa9bQsj/B3tTgsE3dOGqlzHnGIDaC7XBE7grnA+8kZPnfqSGFe95VHc2oc0VFUQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/map-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", - "integrity": "sha512-4y7uGv8bd2WdM9vpQsiQNo41Ln1NvhvDRuVt0k2JZQ+ezN2uaQes7lZeZ+QQUHOLQAtDaBJ+7wCbi+ab/KFs+w==", - "license": "MIT", - "dependencies": { - "object-visit": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/markdown-table": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-0.4.0.tgz", - "integrity": "sha512-9i/E3ZtVAoaDulRQjoPseX2X5pBNdeR8MInQb57JFvCAq4glz/w2q31eL0NHMKOntzy2D6X3plZDH4+OGuz5Fw==", + "resolved": "https://registry.npmjs.org/map-merge/-/map-merge-1.1.0.tgz", + "integrity": "sha512-TGNNg3WqoLsS5HnlK6GHIXFvM/0wYMtlyflc1nAQUhgptr9wIu7JwQ2YsqnpFSqbSYFv2U6rgXQTHJmWJOtlzQ==", "license": "MIT" }, "node_modules/math-intrinsics": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", "license": "MIT", "engines": { "node": ">= 0.4" } }, - "node_modules/math-random": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/math-random/-/math-random-1.0.4.tgz", - "integrity": "sha512-rUxjysqif/BZQH2yhd5Aaq7vXMSx9NdEsQcyA07uEzIvxgI7zIr33gGsh+RU0/XjmQpCW7RsVof1vlkvQVCK5A==", - "license": "MIT" - }, - "node_modules/maybe-combine-errors": { - "version": "1.0.0", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/mdmanifest": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/mdmanifest/-/mdmanifest-1.0.8.tgz", - "integrity": "sha512-x7QEK3/RbANozWnUhmSPzT01yL/PsO2zBDWzDRr+tMRs0o/HQxbDzGb34JiQ2bGnSf88eKuMgrAzMR1+hyRBLw==", - "license": "MIT", - "dependencies": { - "minimist": "^1.2.0", - "remark": "^3.2.2", - "remark-html": "^2.0.2", - "word-wrap": "^1.1.0" - }, - "bin": { - "mdm": "mdm.js" - } - }, "node_modules/mdn-data": { "version": "2.12.2", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", @@ -8062,90 +4764,29 @@ }, "node_modules/media-typer": { "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", "license": "MIT", "engines": { "node": ">= 0.6" } }, - "node_modules/meow": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/meow/-/meow-5.0.0.tgz", - "integrity": "sha512-CbTqYU17ABaLefO8vCU153ZZlprKYWDljcndKKDCFcYQITzWCXZAVk4QMFZPgvzrnUQ3uItnIE/LoUOwrT15Ig==", - "dev": true, + "node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", "license": "MIT", - "dependencies": { - "camelcase-keys": "^4.0.0", - "decamelize-keys": "^1.0.0", - "loud-rejection": "^1.0.0", - "minimist-options": "^3.0.1", - "normalize-package-data": "^2.3.4", - "read-pkg-up": "^3.0.0", - "redent": "^2.0.0", - "trim-newlines": "^2.0.0", - "yargs-parser": "^10.0.0" + "bin": { + "mime": "cli.js" }, "engines": { - "node": ">=6" - } - }, - "node_modules/meow/node_modules/yargs-parser": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-10.1.0.tgz", - "integrity": "sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "camelcase": "^4.1.0" - } - }, - "node_modules/merge-descriptors": { - "version": "2.0.0", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/micromatch": { - "version": "2.3.11", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", - "integrity": "sha512-LnU2XFEk9xxSJ6rfgAry/ty5qwUTyHYOBU0g4R6tIw5ljwgGIBmiKhRWLw5NpMOnrgUNcDJ4WMp8rl3sYVHLNA==", - "license": "MIT", - "dependencies": { - "arr-diff": "^2.0.0", - "array-unique": "^0.2.1", - "braces": "^1.8.2", - "expand-brackets": "^0.1.4", - "extglob": "^0.3.1", - "filename-regex": "^2.0.0", - "is-extglob": "^1.0.0", - "is-glob": "^2.0.1", - "kind-of": "^3.0.2", - "normalize-path": "^2.0.1", - "object.omit": "^2.0.0", - "parse-glob": "^3.0.4", - "regex-cache": "^0.4.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/micromatch/node_modules/normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==", - "license": "MIT", - "dependencies": { - "remove-trailing-separator": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" + "node": ">=10.0.0" } }, "node_modules/mime-db": { "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -8153,6 +4794,8 @@ }, "node_modules/mime-types": { "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "license": "MIT", "dependencies": { "mime-db": "1.52.0" @@ -8161,30 +4804,6 @@ "node": ">= 0.6" } }, - "node_modules/mimic-function": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", - "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/minimatch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", @@ -8209,71 +4828,26 @@ }, "node_modules/minimist": { "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/minimist-options": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-3.0.2.tgz", - "integrity": "sha512-FyBrT/d0d4+uiZRbqznPXqw3IpZZG3gl3wKWiX784FycUKVwBt0uLBFkQrtE4tZOrgo78nZp2jnKz3L65T5LdQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "arrify": "^1.0.1", - "is-plain-obj": "^1.1.0" - }, - "engines": { - "node": ">= 4" - } - }, "node_modules/minipass": { "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", "license": "ISC", "engines": { "node": ">=16 || 14 >=14.17" } }, - "node_modules/minizlib": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", - "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", - "license": "MIT", - "dependencies": { - "minipass": "^7.1.2" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/mixin-deep": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", - "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", - "license": "MIT", - "dependencies": { - "for-in": "^1.0.2", - "is-extendable": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/mixin-deep/node_modules/is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "license": "MIT", - "dependencies": { - "is-plain-object": "^2.0.4" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/mkdirp": { "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", "license": "MIT", "bin": { "mkdirp": "bin/cmd.js" @@ -8284,39 +4858,20 @@ }, "node_modules/mkdirp-classic": { "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", "license": "MIT" }, - "node_modules/mock-property": { - "version": "1.0.3", - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.1", - "functions-have-names": "^1.2.3", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "hasown": "^2.0.0", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/module-alias": { "version": "2.2.3", + "resolved": "https://registry.npmjs.org/module-alias/-/module-alias-2.2.3.tgz", + "integrity": "sha512-23g5BFj4zdQL/b6tor7Ji+QY4pEfNH784BMslY9Qb0UnJWRAt+lQGLYmRaM0KDBwIG23ffEBELhZDP2rhi9f/Q==", "license": "MIT" }, - "node_modules/module-error": { - "version": "1.0.2", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, "node_modules/moment": { "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", "license": "MIT", "engines": { "node": "*" @@ -8324,18 +4879,26 @@ }, "node_modules/monotonic-timestamp": { "version": "0.0.9", + "resolved": "https://registry.npmjs.org/monotonic-timestamp/-/monotonic-timestamp-0.0.9.tgz", + "integrity": "sha512-PQcys7iTcXiMyW8cgK5B/fhKGteB/WLqWN1f+s7IJHyyRd80JnJYoNo03kTQcGZhVtsKz5kEWiS7W94gfY6qcQ==", "license": "MIT" }, "node_modules/moo": { "version": "0.5.2", + "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz", + "integrity": "sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==", "license": "BSD-3-Clause" }, "node_modules/ms": { "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, "node_modules/multiblob": { "version": "1.13.8", + "resolved": "https://registry.npmjs.org/multiblob/-/multiblob-1.13.8.tgz", + "integrity": "sha512-YXNUflHc7I7iUiLctHMTvwDQOjMbCv+Pdbf+1lScFOsjP/CSZgwblRknPntcD+KEPGFudXPBtJR10TIrun3zmQ==", "license": "MIT", "dependencies": { "blake2s": "^1.0.1", @@ -8355,24 +4918,15 @@ "stream-to-pull-stream": "^1.7.2" } }, - "node_modules/multiblob-http": { - "version": "1.2.1", - "license": "MIT", - "dependencies": { - "pull-many": "^1.0.9", - "pull-stream": "^3.6.14", - "range-parser": "^1.2.1", - "stream-to-pull-stream": "^1.7.3" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/multicb": { - "version": "1.2.2" + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/multicb/-/multicb-1.2.2.tgz", + "integrity": "sha512-PZM4dhYFmCF6uZGWpEmoPMUqJBywS9IcAgybT2GmSpYI1BvGvoWSdbio+ik+q/YD2vodhvslESWIS3NnkKYdqQ==" }, "node_modules/multiserver": { "version": "3.8.2", + "resolved": "https://registry.npmjs.org/multiserver/-/multiserver-3.8.2.tgz", + "integrity": "sha512-gmR/5dY+N81EN0yDaziSJXAJhJjG+3Rv3lJc74OSA1ySRKOMqZwEuGJjqG6ZcZiaqcHvhQZkZavDJsAwtmnvoA==", "license": "MIT", "dependencies": { "debug": "^4.1.1", @@ -8391,6 +4945,8 @@ }, "node_modules/multiserver-address": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/multiserver-address/-/multiserver-address-1.0.1.tgz", + "integrity": "sha512-IfZMAGs9onCLkYNSnNBri3JxuvhQYllMyh3W9ry86iEDcfW9uPVsHTHDsjDxQtL+dPq3byshmA+Y4LN2wLHwNw==", "license": "MIT", "dependencies": { "nearley": "^2.15.1" @@ -8398,6 +4954,8 @@ }, "node_modules/multiserver-scopes": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/multiserver-scopes/-/multiserver-scopes-2.0.0.tgz", + "integrity": "sha512-XWv9J617i3mWtZIZQNTpYI9iq4goUpsKy3GdUEDls23z1VaMzuRp2rL3S3IKrheVdgmrf0zHbErcXokGxqQfzw==", "license": "MIT", "dependencies": { "non-private-ip": "^2.0.0" @@ -8405,197 +4963,45 @@ }, "node_modules/mutexify": { "version": "1.4.0", + "resolved": "https://registry.npmjs.org/mutexify/-/mutexify-1.4.0.tgz", + "integrity": "sha512-pbYSsOrSB/AKN5h/WzzLRMFgZhClWccf2XIB4RSMC8JbquiB0e0/SH5AIfdQMdyHmYtv4seU7yV/TvAwPLJ1Yg==", "license": "MIT", "dependencies": { "queue-tick": "^1.0.0" } }, - "node_modules/muxrpc": { - "version": "8.0.0", - "license": "MIT", - "dependencies": { - "clarify-error": "^1.0.0", - "debug": "^4.3.3", - "packet-stream": "~2.0.0", - "packet-stream-codec": "^1.2.0", - "pull-goodbye": "0.0.3", - "pull-stream": "^3.6.10" - } - }, - "node_modules/muxrpc-usage": { - "version": "2.1.0", - "license": "MIT", - "dependencies": { - "right-pad": "^1.0.1" - } - }, "node_modules/muxrpc-validation": { "version": "3.0.2", + "resolved": "https://registry.npmjs.org/muxrpc-validation/-/muxrpc-validation-3.0.2.tgz", + "integrity": "sha512-iWo/23xFnl+IGeX+LlfwoVKtyY4volPSodf3nwPScPgxjws4k2ZUozPG98OouMA0yn0JamqApjRw7eqLrzyV2A==", "license": "MIT", "dependencies": { "pull-stream": "^3.6.11", "zerr": "^1.0.4" } }, - "node_modules/muxrpcli": { - "version": "3.1.2", - "license": "MIT", - "dependencies": { - "cont": "^1.0.3", - "minimist": "^1.2.0", - "muxrpc-usage": "^2.0.1", - "pull-stream": "^3.6.9", - "stream-to-pull-stream": "^1.7.3", - "word-wrap": "^1.2.3" - } - }, - "node_modules/mv": { - "version": "2.1.1", - "license": "MIT", - "dependencies": { - "mkdirp": "~0.5.1", - "ncp": "~2.0.0", - "rimraf": "~2.4.0" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/mv/node_modules/glob": { - "version": "6.0.4", - "license": "ISC", - "dependencies": { - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "2 || 3", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - } - }, - "node_modules/mv/node_modules/mkdirp": { - "version": "0.5.6", - "license": "MIT", - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/mv/node_modules/rimraf": { - "version": "2.4.5", - "license": "ISC", - "dependencies": { - "glob": "^6.0.1" - }, - "bin": { - "rimraf": "bin.js" - } - }, - "node_modules/nan": { - "version": "2.26.2", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.26.2.tgz", - "integrity": "sha512-0tTvBTYkt3tdGw22nrAy50x7gpbGCCFH3AFcyS5WiUu7Eu4vWlri1woE6qHBSfy11vksDqkiwjOnlR7WV8G1Hw==", - "license": "MIT", - "optional": true - }, "node_modules/nano-equal": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/nano-equal/-/nano-equal-2.0.2.tgz", + "integrity": "sha512-RtAioZtSSppEWRruYU8oz2AKPJWTW74TpzcqP79n7Kh4nHW+gmrJGcTuELmid9tvGR4Xp2cCoTtYIIDy9iWO4g==", "license": "MIT" }, "node_modules/nanoassert": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/nanoassert/-/nanoassert-2.0.0.tgz", + "integrity": "sha512-7vO7n28+aYO4J+8w96AzhmU8G+Y/xpPDJz/se19ICsqj/momRbb9mh9ZUtkoJ5X3nTnPdhEJyc0qnM6yAsHBaA==", "license": "ISC" }, - "node_modules/nanoid": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.6.tgz", - "integrity": "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.js" - }, - "engines": { - "node": "^18 || >=20" - } - }, - "node_modules/nanomatch": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", - "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", - "license": "MIT", - "dependencies": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "fragment-cache": "^0.2.1", - "is-windows": "^1.0.2", - "kind-of": "^6.0.2", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nanomatch/node_modules/arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nanomatch/node_modules/array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha512-SleRWjh9JUud2wH1hPs9rZBZ33H6T9HOiL0uwGnGx9FpE6wKGyfWugmbkEOIs6qWrZhg0LWeLziLrEwQJhs5mQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nanomatch/node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/napi-build-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", - "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", - "license": "MIT" - }, "node_modules/napi-macros": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-macros/-/napi-macros-2.0.0.tgz", + "integrity": "sha512-A0xLykHtARfueITVDernsAWdtIMbOJgKgcluwENp3AlsKN/PloyO10HtmoqnFAQAcxPkgZN7wdfPfEd0zNGxbg==", "license": "MIT" }, - "node_modules/ncp": { - "version": "2.0.0", - "license": "MIT", - "bin": { - "ncp": "bin/ncp" - } - }, "node_modules/nearley": { "version": "2.20.1", + "resolved": "https://registry.npmjs.org/nearley/-/nearley-2.20.1.tgz", + "integrity": "sha512-+Mc8UaAebFzgV+KpI5n7DasuuQCHA89dmwm7JXw3TV43ukfNQ9DnBH3Mdb2g/I4Fdxc26pwimBWvjIw0UAILSQ==", "license": "MIT", "dependencies": { "commander": "^2.19.0", @@ -8616,10 +5022,14 @@ }, "node_modules/nearley/node_modules/commander": { "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "license": "MIT" }, "node_modules/negotiator": { "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -8627,57 +5037,14 @@ }, "node_modules/next-tick": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", + "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", "license": "ISC" }, - "node_modules/nice-try": { - "version": "1.0.5", - "license": "MIT" - }, - "node_modules/node-abi": { - "version": "3.92.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.92.0.tgz", - "integrity": "sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==", - "license": "MIT", - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/node-addon-api": { - "version": "8.5.0", - "license": "MIT", - "engines": { - "node": "^18 || ^20 || >= 21" - } - }, - "node_modules/node-api-headers": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/node-api-headers/-/node-api-headers-1.8.0.tgz", - "integrity": "sha512-jfnmiKWjRAGbdD1yQS28bknFM1tbHC1oucyuMPjmkEs+kpiu76aRs40WlTmBmyEgzDM76ge1DQ7XJ3R5deiVjQ==", - "license": "MIT" - }, - "node_modules/node-fetch": { - "version": "2.7.0", - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, "node_modules/node-gyp-build": { "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", "license": "MIT", "bin": { "node-gyp-build": "bin.js", @@ -8685,409 +5052,22 @@ "node-gyp-build-test": "build-test.js" } }, - "node_modules/node-llama-cpp": { - "version": "3.16.2", - "resolved": "https://registry.npmjs.org/node-llama-cpp/-/node-llama-cpp-3.16.2.tgz", - "integrity": "sha512-ovhuTaXSWfcoyfI8ljWxO2Rg63mNxqQQAbDGkXRhlgsL7UjPqm2Nsy1bTNa0ZaQRg3vezG4agnCJTImrICY/0A==", - "hasInstallScript": true, + "node_modules/node-localstorage": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/node-localstorage/-/node-localstorage-2.2.1.tgz", + "integrity": "sha512-vv8fJuOUCCvSPjDjBLlMqYMHob4aGjkmrkaE42/mZr0VT+ZAU10jRF8oTnX9+pgU9/vYJ8P7YT3Vd6ajkmzSCw==", "license": "MIT", "dependencies": { - "@huggingface/jinja": "^0.5.5", - "async-retry": "^1.3.3", - "bytes": "^3.1.2", - "chalk": "^5.6.2", - "chmodrp": "^1.0.2", - "cmake-js": "^8.0.0", - "cross-spawn": "^7.0.6", - "env-var": "^7.5.0", - "filenamify": "^6.0.0", - "fs-extra": "^11.3.0", - "ignore": "^7.0.4", - "ipull": "^3.9.5", - "is-unicode-supported": "^2.1.0", - "lifecycle-utils": "^3.1.1", - "log-symbols": "^7.0.1", - "nanoid": "^5.1.6", - "node-addon-api": "^8.5.0", - "octokit": "^5.0.5", - "ora": "^9.3.0", - "pretty-ms": "^9.3.0", - "proper-lockfile": "^4.1.2", - "semver": "^7.7.1", - "simple-git": "^3.31.1", - "slice-ansi": "^8.0.0", - "stdout-update": "^4.0.1", - "strip-ansi": "^7.1.2", - "validate-npm-package-name": "^7.0.2", - "which": "^6.0.1", - "yargs": "^17.7.2" - }, - "bin": { - "nlc": "dist/cli/cli.js", - "node-llama-cpp": "dist/cli/cli.js" + "write-file-atomic": "^1.1.4" }, "engines": { - "node": ">=20.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/giladgd" - }, - "optionalDependencies": { - "@node-llama-cpp/linux-arm64": "3.16.2", - "@node-llama-cpp/linux-armv7l": "3.16.2", - "@node-llama-cpp/linux-x64": "3.16.2", - "@node-llama-cpp/linux-x64-cuda": "3.16.2", - "@node-llama-cpp/linux-x64-cuda-ext": "3.16.2", - "@node-llama-cpp/linux-x64-vulkan": "3.16.2", - "@node-llama-cpp/mac-arm64-metal": "3.16.2", - "@node-llama-cpp/mac-x64": "3.16.2", - "@node-llama-cpp/win-arm64": "3.16.2", - "@node-llama-cpp/win-x64": "3.16.2", - "@node-llama-cpp/win-x64-cuda": "3.16.2", - "@node-llama-cpp/win-x64-cuda-ext": "3.16.2", - "@node-llama-cpp/win-x64-vulkan": "3.16.2" - }, - "peerDependencies": { - "typescript": ">=5.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/node-llama-cpp/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/node-llama-cpp/node_modules/ignore": { - "version": "7.0.5", - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/node-llama-cpp/node_modules/isexe": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", - "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=20" - } - }, - "node_modules/node-llama-cpp/node_modules/parse-ms": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", - "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/node-llama-cpp/node_modules/pretty-ms": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.0.tgz", - "integrity": "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==", - "license": "MIT", - "dependencies": { - "parse-ms": "^4.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/node-llama-cpp/node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/node-llama-cpp/node_modules/which": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", - "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", - "license": "ISC", - "dependencies": { - "isexe": "^4.0.0" - }, - "bin": { - "node-which": "bin/which.js" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/nodemon": { - "version": "3.1.14", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.14.tgz", - "integrity": "sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==", - "dev": true, - "license": "MIT", - "dependencies": { - "chokidar": "^3.5.2", - "debug": "^4", - "ignore-by-default": "^1.0.1", - "minimatch": "^10.2.1", - "pstree.remy": "^1.1.8", - "semver": "^7.5.3", - "simple-update-notifier": "^2.0.0", - "supports-color": "^5.5.0", - "touch": "^3.1.0", - "undefsafe": "^2.0.5" - }, - "bin": { - "nodemon": "bin/nodemon.js" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/nodemon" - } - }, - "node_modules/nodemon/node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/nodemon/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/nodemon/node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/nodemon/node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/nodemon/node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/nodemon/node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/nodemon/node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/nodemon/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/nodemon/node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/nodemon/node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nodemon/node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nodemon/node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/nodemon/node_modules/minimatch": { - "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.5" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/nodemon/node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/nodemon/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/nodemon/node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" + "node": ">=0.12" } }, "node_modules/non-private-ip": { "version": "2.2.0", + "resolved": "https://registry.npmjs.org/non-private-ip/-/non-private-ip-2.2.0.tgz", + "integrity": "sha512-NZ3Upr3K2whD6vdZ9k8gHsijsrQl5O6IARLIUDyvQwSuO/owM1kOMu8wDMMsIR8ujlLvhPNjlTZC2SXzWwWByQ==", "license": "MIT", "dependencies": { "ip": "^1.1.5" @@ -9096,1417 +5076,19 @@ "non-private-ip": "bin.js" } }, - "node_modules/normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - } - }, - "node_modules/normalize-package-data/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver" - } - }, "node_modules/normalize-path": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "license": "MIT", "engines": { "node": ">=0.10.0" } }, - "node_modules/normalize-uri": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/normalize-uri/-/normalize-uri-1.1.3.tgz", - "integrity": "sha512-ECNgiM5IAeZNuXYu5kF4JV8t+MSFixHsvjexQtf/bLTOsL+KycDv3pod1a88N8uHtzsktYLRX6cAawg4aHeLVQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/npm-install-package": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/npm-install-package/-/npm-install-package-2.1.0.tgz", - "integrity": "sha512-F37OfFN2Fk6ixI3bn8CNllSGMjpRxTqPcnN5j2zFlqNiaf/LGG6fCq3DeBGKRinyVMR5ROS34nWKQmn7tABuJg==", - "dev": true, - "license": "MIT" - }, - "node_modules/npm-prefix": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/npm-prefix/-/npm-prefix-1.2.0.tgz", - "integrity": "sha512-EkGZ7jtA2onsULFpnZ/P5S0DPy8w9qH1TVytPhY54s+dmtLXBmp1evt8W9nfg5JEay24K3bX9WWTIHR8WQcOJA==", - "license": "MIT", - "dependencies": { - "rc": "^1.1.0", - "shellsubstitute": "^1.1.0", - "untildify": "^2.1.0" - } - }, - "node_modules/number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nyc": { - "version": "13.3.0", - "resolved": "https://registry.npmjs.org/nyc/-/nyc-13.3.0.tgz", - "integrity": "sha512-P+FwIuro2aFG6B0Esd9ZDWUd51uZrAEoGutqZxzrVmYl3qSfkLgcQpBPBjtDFsUQLFY1dvTQJPOyeqr8S9GF8w==", - "bundleDependencies": [ - "archy", - "arrify", - "caching-transform", - "convert-source-map", - "find-cache-dir", - "find-up", - "foreground-child", - "glob", - "istanbul-lib-coverage", - "istanbul-lib-hook", - "istanbul-lib-report", - "istanbul-lib-source-maps", - "istanbul-reports", - "make-dir", - "merge-source-map", - "resolve-from", - "rimraf", - "signal-exit", - "spawn-wrap", - "test-exclude", - "uuid", - "yargs", - "yargs-parser" - ], - "dev": true, - "license": "ISC", - "dependencies": { - "archy": "^1.0.0", - "arrify": "^1.0.1", - "caching-transform": "^3.0.1", - "convert-source-map": "^1.6.0", - "find-cache-dir": "^2.0.0", - "find-up": "^3.0.0", - "foreground-child": "^1.5.6", - "glob": "^7.1.3", - "istanbul-lib-coverage": "^2.0.3", - "istanbul-lib-hook": "^2.0.3", - "istanbul-lib-instrument": "^3.1.0", - "istanbul-lib-report": "^2.0.4", - "istanbul-lib-source-maps": "^3.0.2", - "istanbul-reports": "^2.1.1", - "make-dir": "^1.3.0", - "merge-source-map": "^1.1.0", - "resolve-from": "^4.0.0", - "rimraf": "^2.6.3", - "signal-exit": "^3.0.2", - "spawn-wrap": "^1.4.2", - "test-exclude": "^5.1.0", - "uuid": "^3.3.2", - "yargs": "^12.0.5", - "yargs-parser": "^11.1.1" - }, - "bin": { - "nyc": "bin/nyc.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/nyc/node_modules/ansi-regex": { - "version": "3.0.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/nyc/node_modules/append-transform": { - "version": "1.0.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "default-require-extensions": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/nyc/node_modules/archy": { - "version": "1.0.0", - "inBundle": true, - "license": "MIT" - }, - "node_modules/nyc/node_modules/arrify": { - "version": "1.0.1", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nyc/node_modules/async": { - "version": "2.6.2", - "inBundle": true, - "license": "MIT", - "dependencies": { - "lodash": "^4.17.11" - } - }, - "node_modules/nyc/node_modules/balanced-match": { - "version": "1.0.0", - "inBundle": true, - "license": "MIT" - }, - "node_modules/nyc/node_modules/brace-expansion": { - "version": "1.1.11", - "inBundle": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/nyc/node_modules/caching-transform": { - "version": "3.0.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "hasha": "^3.0.0", - "make-dir": "^1.3.0", - "package-hash": "^3.0.0", - "write-file-atomic": "^2.3.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/nyc/node_modules/camelcase": { - "version": "5.0.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/nyc/node_modules/cliui": { - "version": "4.1.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "string-width": "^2.1.1", - "strip-ansi": "^4.0.0", - "wrap-ansi": "^2.0.0" - } - }, - "node_modules/nyc/node_modules/code-point-at": { - "version": "1.1.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nyc/node_modules/commander": { - "version": "2.17.1", - "inBundle": true, - "license": "MIT" - }, - "node_modules/nyc/node_modules/commondir": { - "version": "1.0.1", - "inBundle": true, - "license": "MIT" - }, - "node_modules/nyc/node_modules/concat-map": { - "version": "0.0.1", - "inBundle": true, - "license": "MIT" - }, - "node_modules/nyc/node_modules/convert-source-map": { - "version": "1.6.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.1" - } - }, - "node_modules/nyc/node_modules/cross-spawn": { - "version": "4.0.2", - "inBundle": true, - "license": "MIT", - "dependencies": { - "lru-cache": "^4.0.1", - "which": "^1.2.9" - } - }, - "node_modules/nyc/node_modules/debug": { - "version": "4.1.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/nyc/node_modules/decamelize": { - "version": "1.2.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nyc/node_modules/default-require-extensions": { - "version": "2.0.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "strip-bom": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/nyc/node_modules/end-of-stream": { - "version": "1.4.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/nyc/node_modules/error-ex": { - "version": "1.3.2", - "inBundle": true, - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/nyc/node_modules/es6-error": { - "version": "4.1.1", - "inBundle": true, - "license": "MIT" - }, - "node_modules/nyc/node_modules/execa": { - "version": "1.0.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/nyc/node_modules/execa/node_modules/cross-spawn": { - "version": "6.0.5", - "inBundle": true, - "license": "MIT", - "dependencies": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - }, - "engines": { - "node": ">=4.8" - } - }, - "node_modules/nyc/node_modules/find-cache-dir": { - "version": "2.0.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "commondir": "^1.0.1", - "make-dir": "^1.0.0", - "pkg-dir": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/nyc/node_modules/find-up": { - "version": "3.0.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "locate-path": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/nyc/node_modules/foreground-child": { - "version": "1.5.6", - "inBundle": true, - "license": "ISC", - "dependencies": { - "cross-spawn": "^4", - "signal-exit": "^3.0.0" - } - }, - "node_modules/nyc/node_modules/fs.realpath": { - "version": "1.0.0", - "inBundle": true, - "license": "ISC" - }, - "node_modules/nyc/node_modules/get-caller-file": { - "version": "1.0.3", - "inBundle": true, - "license": "ISC" - }, - "node_modules/nyc/node_modules/get-stream": { - "version": "4.1.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/nyc/node_modules/glob": { - "version": "7.1.3", - "inBundle": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - } - }, - "node_modules/nyc/node_modules/graceful-fs": { - "version": "4.1.15", - "inBundle": true, - "license": "ISC" - }, - "node_modules/nyc/node_modules/handlebars": { - "version": "4.1.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "async": "^2.5.0", - "optimist": "^0.6.1", - "source-map": "^0.6.1" - }, - "bin": { - "handlebars": "bin/handlebars" - }, - "engines": { - "node": ">=0.4.7" - }, - "optionalDependencies": { - "uglify-js": "^3.1.4" - } - }, - "node_modules/nyc/node_modules/handlebars/node_modules/source-map": { - "version": "0.6.1", - "inBundle": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nyc/node_modules/has-flag": { - "version": "3.0.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/nyc/node_modules/hasha": { - "version": "3.0.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "is-stream": "^1.0.1" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/nyc/node_modules/hosted-git-info": { - "version": "2.7.1", - "inBundle": true, - "license": "ISC" - }, - "node_modules/nyc/node_modules/imurmurhash": { - "version": "0.1.4", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/nyc/node_modules/inflight": { - "version": "1.0.6", - "inBundle": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/nyc/node_modules/inherits": { - "version": "2.0.3", - "inBundle": true, - "license": "ISC" - }, - "node_modules/nyc/node_modules/invert-kv": { - "version": "2.0.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/nyc/node_modules/is-arrayish": { - "version": "0.2.1", - "inBundle": true, - "license": "MIT" - }, - "node_modules/nyc/node_modules/is-fullwidth-code-point": { - "version": "2.0.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/nyc/node_modules/is-stream": { - "version": "1.1.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nyc/node_modules/isexe": { - "version": "2.0.0", - "inBundle": true, - "license": "ISC" - }, - "node_modules/nyc/node_modules/istanbul-lib-coverage": { - "version": "2.0.3", - "inBundle": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=6" - } - }, - "node_modules/nyc/node_modules/istanbul-lib-hook": { - "version": "2.0.3", - "inBundle": true, - "license": "BSD-3-Clause", - "dependencies": { - "append-transform": "^1.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/nyc/node_modules/istanbul-lib-report": { - "version": "2.0.4", - "inBundle": true, - "license": "BSD-3-Clause", - "dependencies": { - "istanbul-lib-coverage": "^2.0.3", - "make-dir": "^1.3.0", - "supports-color": "^6.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/nyc/node_modules/istanbul-lib-report/node_modules/supports-color": { - "version": "6.1.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/nyc/node_modules/istanbul-lib-source-maps": { - "version": "3.0.2", - "inBundle": true, - "license": "BSD-3-Clause", - "dependencies": { - "debug": "^4.1.1", - "istanbul-lib-coverage": "^2.0.3", - "make-dir": "^1.3.0", - "rimraf": "^2.6.2", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/nyc/node_modules/istanbul-lib-source-maps/node_modules/source-map": { - "version": "0.6.1", - "inBundle": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nyc/node_modules/istanbul-reports": { - "version": "2.1.1", - "inBundle": true, - "license": "BSD-3-Clause", - "dependencies": { - "handlebars": "^4.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/nyc/node_modules/json-parse-better-errors": { - "version": "1.0.2", - "inBundle": true, - "license": "MIT" - }, - "node_modules/nyc/node_modules/lcid": { - "version": "2.0.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "invert-kv": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/nyc/node_modules/load-json-file": { - "version": "4.0.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.1.2", - "parse-json": "^4.0.0", - "pify": "^3.0.0", - "strip-bom": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/nyc/node_modules/locate-path": { - "version": "3.0.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/nyc/node_modules/lodash": { - "version": "4.17.11", - "inBundle": true, - "license": "MIT" - }, - "node_modules/nyc/node_modules/lodash.flattendeep": { - "version": "4.4.0", - "inBundle": true, - "license": "MIT" - }, - "node_modules/nyc/node_modules/lru-cache": { - "version": "4.1.5", - "inBundle": true, - "license": "ISC", - "dependencies": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" - } - }, - "node_modules/nyc/node_modules/make-dir": { - "version": "1.3.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "pify": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/nyc/node_modules/map-age-cleaner": { - "version": "0.1.3", - "inBundle": true, - "license": "MIT", - "dependencies": { - "p-defer": "^1.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/nyc/node_modules/mem": { - "version": "4.1.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "map-age-cleaner": "^0.1.1", - "mimic-fn": "^1.0.0", - "p-is-promise": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/nyc/node_modules/merge-source-map": { - "version": "1.1.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "source-map": "^0.6.1" - } - }, - "node_modules/nyc/node_modules/merge-source-map/node_modules/source-map": { - "version": "0.6.1", - "inBundle": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nyc/node_modules/mimic-fn": { - "version": "1.2.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/nyc/node_modules/minimatch": { - "version": "3.0.4", - "inBundle": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/nyc/node_modules/minimist": { - "version": "0.0.10", - "inBundle": true, - "license": "MIT" - }, - "node_modules/nyc/node_modules/mkdirp": { - "version": "0.5.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "minimist": "0.0.8" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/nyc/node_modules/mkdirp/node_modules/minimist": { - "version": "0.0.8", - "inBundle": true, - "license": "MIT" - }, - "node_modules/nyc/node_modules/ms": { - "version": "2.1.1", - "inBundle": true, - "license": "MIT" - }, - "node_modules/nyc/node_modules/nice-try": { - "version": "1.0.5", - "inBundle": true, - "license": "MIT" - }, - "node_modules/nyc/node_modules/normalize-package-data": { - "version": "2.5.0", - "inBundle": true, - "license": "BSD-2-Clause", - "dependencies": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - } - }, - "node_modules/nyc/node_modules/npm-run-path": { - "version": "2.0.2", - "inBundle": true, - "license": "MIT", - "dependencies": { - "path-key": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/nyc/node_modules/number-is-nan": { - "version": "1.0.1", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nyc/node_modules/once": { - "version": "1.4.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/nyc/node_modules/optimist": { - "version": "0.6.1", - "inBundle": true, - "license": "MIT/X11", - "dependencies": { - "minimist": "~0.0.1", - "wordwrap": "~0.0.2" - } - }, - "node_modules/nyc/node_modules/os-homedir": { - "version": "1.0.2", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nyc/node_modules/os-locale": { - "version": "3.1.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "execa": "^1.0.0", - "lcid": "^2.0.0", - "mem": "^4.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/nyc/node_modules/p-defer": { - "version": "1.0.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/nyc/node_modules/p-finally": { - "version": "1.0.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/nyc/node_modules/p-is-promise": { - "version": "2.0.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/nyc/node_modules/p-limit": { - "version": "2.1.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/nyc/node_modules/p-locate": { - "version": "3.0.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "p-limit": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/nyc/node_modules/p-try": { - "version": "2.0.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/nyc/node_modules/package-hash": { - "version": "3.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "graceful-fs": "^4.1.15", - "hasha": "^3.0.0", - "lodash.flattendeep": "^4.4.0", - "release-zalgo": "^1.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/nyc/node_modules/parse-json": { - "version": "4.0.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/nyc/node_modules/path-exists": { - "version": "3.0.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/nyc/node_modules/path-is-absolute": { - "version": "1.0.1", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nyc/node_modules/path-key": { - "version": "2.0.1", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/nyc/node_modules/path-parse": { - "version": "1.0.6", - "inBundle": true, - "license": "MIT" - }, - "node_modules/nyc/node_modules/path-type": { - "version": "3.0.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "pify": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/nyc/node_modules/pify": { - "version": "3.0.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/nyc/node_modules/pkg-dir": { - "version": "3.0.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "find-up": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/nyc/node_modules/pseudomap": { - "version": "1.0.2", - "inBundle": true, - "license": "ISC" - }, - "node_modules/nyc/node_modules/pump": { - "version": "3.0.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/nyc/node_modules/read-pkg": { - "version": "3.0.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "load-json-file": "^4.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/nyc/node_modules/read-pkg-up": { - "version": "4.0.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "find-up": "^3.0.0", - "read-pkg": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/nyc/node_modules/release-zalgo": { - "version": "1.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "es6-error": "^4.0.1" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/nyc/node_modules/require-directory": { - "version": "2.1.1", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nyc/node_modules/require-main-filename": { - "version": "1.0.1", - "inBundle": true, - "license": "ISC" - }, - "node_modules/nyc/node_modules/resolve": { - "version": "1.10.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "path-parse": "^1.0.6" - } - }, - "node_modules/nyc/node_modules/resolve-from": { - "version": "4.0.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/nyc/node_modules/rimraf": { - "version": "2.6.3", - "inBundle": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, - "node_modules/nyc/node_modules/safe-buffer": { - "version": "5.1.2", - "inBundle": true, - "license": "MIT" - }, - "node_modules/nyc/node_modules/semver": { - "version": "5.6.0", - "inBundle": true, - "license": "ISC", - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/nyc/node_modules/set-blocking": { - "version": "2.0.0", - "inBundle": true, - "license": "ISC" - }, - "node_modules/nyc/node_modules/shebang-command": { - "version": "1.2.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nyc/node_modules/shebang-regex": { - "version": "1.0.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nyc/node_modules/signal-exit": { - "version": "3.0.2", - "inBundle": true, - "license": "ISC" - }, - "node_modules/nyc/node_modules/spawn-wrap": { - "version": "1.4.2", - "inBundle": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^1.5.6", - "mkdirp": "^0.5.0", - "os-homedir": "^1.0.1", - "rimraf": "^2.6.2", - "signal-exit": "^3.0.2", - "which": "^1.3.0" - } - }, - "node_modules/nyc/node_modules/spdx-correct": { - "version": "3.1.0", - "inBundle": true, - "license": "Apache-2.0", - "dependencies": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/nyc/node_modules/spdx-exceptions": { - "version": "2.2.0", - "inBundle": true, - "license": "CC-BY-3.0" - }, - "node_modules/nyc/node_modules/spdx-expression-parse": { - "version": "3.0.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/nyc/node_modules/spdx-license-ids": { - "version": "3.0.3", - "inBundle": true, - "license": "CC0-1.0" - }, - "node_modules/nyc/node_modules/string-width": { - "version": "2.1.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/nyc/node_modules/strip-ansi": { - "version": "4.0.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/nyc/node_modules/strip-bom": { - "version": "3.0.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/nyc/node_modules/strip-eof": { - "version": "1.0.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nyc/node_modules/test-exclude": { - "version": "5.1.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "arrify": "^1.0.1", - "minimatch": "^3.0.4", - "read-pkg-up": "^4.0.0", - "require-main-filename": "^1.0.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/nyc/node_modules/uglify-js": { - "version": "3.4.9", - "inBundle": true, - "license": "BSD-2-Clause", - "dependencies": { - "commander": "~2.17.1", - "source-map": "~0.6.1" - }, - "bin": { - "uglifyjs": "bin/uglifyjs" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/nyc/node_modules/uglify-js/node_modules/source-map": { - "version": "0.6.1", - "inBundle": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nyc/node_modules/uuid": { - "version": "3.3.2", - "inBundle": true, - "license": "MIT", - "bin": { - "uuid": "bin/uuid" - } - }, - "node_modules/nyc/node_modules/validate-npm-package-license": { - "version": "3.0.4", - "inBundle": true, - "license": "Apache-2.0", - "dependencies": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "node_modules/nyc/node_modules/which": { - "version": "1.3.1", - "inBundle": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "which": "bin/which" - } - }, - "node_modules/nyc/node_modules/which-module": { - "version": "2.0.0", - "inBundle": true, - "license": "ISC" - }, - "node_modules/nyc/node_modules/wordwrap": { - "version": "0.0.3", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/nyc/node_modules/wrap-ansi": { - "version": "2.1.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nyc/node_modules/wrap-ansi/node_modules/ansi-regex": { - "version": "2.1.1", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nyc/node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": { - "version": "1.0.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "number-is-nan": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nyc/node_modules/wrap-ansi/node_modules/string-width": { - "version": "1.0.2", - "inBundle": true, - "license": "MIT", - "dependencies": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nyc/node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "3.0.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nyc/node_modules/wrappy": { - "version": "1.0.2", - "inBundle": true, - "license": "ISC" - }, - "node_modules/nyc/node_modules/write-file-atomic": { - "version": "2.4.2", - "inBundle": true, - "license": "ISC", - "dependencies": { - "graceful-fs": "^4.1.11", - "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.2" - } - }, - "node_modules/nyc/node_modules/y18n": { - "version": "4.0.0", - "inBundle": true, - "license": "ISC" - }, - "node_modules/nyc/node_modules/yallist": { - "version": "2.1.2", - "inBundle": true, - "license": "ISC" - }, - "node_modules/nyc/node_modules/yargs": { - "version": "12.0.5", - "inBundle": true, - "license": "MIT", - "dependencies": { - "cliui": "^4.0.0", - "decamelize": "^1.2.0", - "find-up": "^3.0.0", - "get-caller-file": "^1.0.1", - "os-locale": "^3.0.0", - "require-directory": "^2.1.1", - "require-main-filename": "^1.0.1", - "set-blocking": "^2.0.0", - "string-width": "^2.0.0", - "which-module": "^2.0.0", - "y18n": "^3.2.1 || ^4.0.0", - "yargs-parser": "^11.1.1" - } - }, - "node_modules/nyc/node_modules/yargs-parser": { - "version": "11.1.1", - "inBundle": true, - "license": "ISC", - "dependencies": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-copy": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", - "integrity": "sha512-79LYn6VAb63zgtmAteVOWo9Vdj71ZVBy3Pbse+VqxDpEP83XuujMrGqHIwAXJ5I/aM0zU7dIyIAhifVTPrNItQ==", - "license": "MIT", - "dependencies": { - "copy-descriptor": "^0.1.0", - "define-property": "^0.2.5", - "kind-of": "^3.0.3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-copy/node_modules/define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", - "license": "MIT", - "dependencies": { - "is-descriptor": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-copy/node_modules/is-descriptor": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", - "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", - "license": "MIT", - "dependencies": { - "is-accessor-descriptor": "^1.0.1", - "is-data-descriptor": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/object-inspect": { "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -10515,50 +5097,19 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/object-is": { - "version": "1.1.6", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/object-keys": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", "license": "MIT", "engines": { "node": ">= 0.4" } }, - "node_modules/object-visit": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", - "integrity": "sha512-GBaMwwAVK9qbQN3Scdo0OyvgPW7l3lnaVMj84uTOZlswkX0KpF6fyDBJhtTthf7pymztoN36/KEr1DyhF96zEA==", - "license": "MIT", - "dependencies": { - "isobject": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-visit/node_modules/isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/object.assign": { "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -10577,6 +5128,8 @@ }, "node_modules/object.entries": { "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -10588,92 +5141,28 @@ "node": ">= 0.4" } }, - "node_modules/object.omit": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz", - "integrity": "sha512-UiAM5mhmIuKLsOvrL+B0U2d1hXHF3bFYWIuH1LMpuV2EJEHG1Ntz06PgLEHjm6VFd87NpH8rastvPoyv6UW2fA==", - "license": "MIT", - "dependencies": { - "for-own": "^0.1.4", - "is-extendable": "^0.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object.pick": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", - "integrity": "sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==", - "license": "MIT", - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object.pick/node_modules/isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/observ": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/observ/-/observ-0.2.0.tgz", - "integrity": "sha512-7SKbYTiFXMvqbeinO/GwTGtXSvbRq5AHKszmq5cx3fV7bKEjNCK7AdkhGzsuQAoT+IPf3mlxDmPrVW8wgshpcQ==" - }, - "node_modules/observ-debounce": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/observ-debounce/-/observ-debounce-1.1.1.tgz", - "integrity": "sha512-vP3EdCDE0+6tftznrvmmfI66qiOEkSV+zJ/TLo/3x5BZjm0a3I9A12V16iPck1v2cwAgGRPtWVF3vUmFN9vtPw==", - "license": "MIT", - "dependencies": { - "observ": "~0.2.0" - } - }, "node_modules/obv": { "version": "0.0.1", + "resolved": "https://registry.npmjs.org/obv/-/obv-0.0.1.tgz", + "integrity": "sha512-Iq/Q3TXAfHgABGL7mKlG3ILJnT8jgetOt0sH9a6SGLLCvjh58bsH7RzixLJWkVO6aQK6hI4rxVYHF0rk9KmIwA==", "license": "MIT" }, "node_modules/obz": { "version": "1.1.1", - "license": "MIT" - }, - "node_modules/octokit": { - "version": "5.0.5", - "license": "MIT", - "dependencies": { - "@octokit/app": "^16.1.2", - "@octokit/core": "^7.0.6", - "@octokit/oauth-app": "^8.0.3", - "@octokit/plugin-paginate-graphql": "^6.0.0", - "@octokit/plugin-paginate-rest": "^14.0.0", - "@octokit/plugin-rest-endpoint-methods": "^17.0.0", - "@octokit/plugin-retry": "^8.0.3", - "@octokit/plugin-throttling": "^11.0.3", - "@octokit/request-error": "^7.0.2", - "@octokit/types": "^16.0.0", - "@octokit/webhooks": "^14.0.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/on-change-network": { - "version": "0.0.2", + "resolved": "https://registry.npmjs.org/obz/-/obz-1.1.1.tgz", + "integrity": "sha512-eUiAX663dASOUIEWuRzUycf2ERZJFLbdjfzYRtju9z2eBfDPywcWjfyHV5JNbVoBjuHGVodyfeyy8Qu09+JCXw==", "license": "MIT" }, "node_modules/on-change-network-strict": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/on-change-network-strict/-/on-change-network-strict-1.0.0.tgz", + "integrity": "sha512-ldHCpTJWgr5KUJy3/TVoSGNwBUA8BP9UFmd0iQqe4aGaXY4PJyzQPiVBIo8VBSlSoKyaJY3vcpW0hixZb6gPaA==", "license": "MIT" }, "node_modules/on-finished": { "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", "license": "MIT", "dependencies": { "ee-first": "1.1.1" @@ -10684,79 +5173,28 @@ }, "node_modules/on-wakeup": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/on-wakeup/-/on-wakeup-1.0.1.tgz", + "integrity": "sha512-3ufOvnTvh39ah2/TT++HpLailHVmEVVrKtzKLKifAUyWbulKLGGJGOF7ywKC4k/iQGmn9KooV6WmQl/6BVwklA==", "license": "MIT" }, "node_modules/once": { "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "license": "ISC", "dependencies": { "wrappy": "1" } }, - "node_modules/onetime": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", - "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", - "license": "MIT", - "dependencies": { - "mimic-function": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/only": { - "version": "0.0.2" - }, - "node_modules/onnx-proto": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/onnx-proto/-/onnx-proto-4.0.4.tgz", - "integrity": "sha512-aldMOB3HRoo6q/phyB6QRQxSt895HNNw82BNyZ2CMh4bjeKv7g/c+VpAFtJuEMVfYLMbRx61hbuqnKceLeDcDA==", - "license": "MIT", - "dependencies": { - "protobufjs": "^6.8.8" - } - }, - "node_modules/onnxruntime-common": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.14.0.tgz", - "integrity": "sha512-3LJpegM2iMNRX2wUmtYfeX/ytfOzNwAWKSq1HbRrKc9+uqG/FsEA0bbKZl1btQeZaXhC26l44NWpNUeXPII7Ew==", - "license": "MIT" - }, - "node_modules/onnxruntime-node": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/onnxruntime-node/-/onnxruntime-node-1.14.0.tgz", - "integrity": "sha512-5ba7TWomIV/9b6NH/1x/8QEeowsb+jBEvFzU6z0T4mNsFwdPqXeFUM7uxC6QeSRkEbWu3qEB0VMjrvzN/0S9+w==", - "license": "MIT", - "optional": true, - "os": [ - "win32", - "darwin", - "linux" - ], - "dependencies": { - "onnxruntime-common": "~1.14.0" - } - }, - "node_modules/onnxruntime-web": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/onnxruntime-web/-/onnxruntime-web-1.14.0.tgz", - "integrity": "sha512-Kcqf43UMfW8mCydVGcX9OMXI2VN17c0p6XvR7IPSZzBf/6lteBzXHvcEVWDPmCKuGombl997HgLqj91F11DzXw==", - "license": "MIT", - "dependencies": { - "flatbuffers": "^1.12.0", - "guid-typescript": "^1.0.9", - "long": "^4.0.0", - "onnx-proto": "^4.0.4", - "onnxruntime-common": "~1.14.0", - "platform": "^1.3.6" - } + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/only/-/only-0.0.2.tgz", + "integrity": "sha512-Fvw+Jemq5fjjyWz6CpKx6w9s7xxqo3+JCyM0WXWeCSOboZ8ABkyvP8ID4CZuChA/wxSx+XSJmdOm8rGVyJ1hdQ==" }, "node_modules/open": { "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", "license": "MIT", "dependencies": { "define-lazy-prop": "^2.0.0", @@ -10770,13 +5208,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/opencollective-postinstall": { - "version": "2.0.3", - "license": "MIT", - "bin": { - "opencollective-postinstall": "index.js" - } - }, "node_modules/openpgp": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/openpgp/-/openpgp-6.3.0.tgz", @@ -10786,107 +5217,25 @@ "node": ">= 18.0.0" } }, - "node_modules/ora": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/ora/-/ora-9.3.0.tgz", - "integrity": "sha512-lBX72MWFduWEf7v7uWf5DHp9Jn5BI8bNPGuFgtXMmr2uDz2Gz2749y3am3agSDdkhHPHYmmxEGSKH85ZLGzgXw==", - "license": "MIT", - "dependencies": { - "chalk": "^5.6.2", - "cli-cursor": "^5.0.0", - "cli-spinners": "^3.2.0", - "is-interactive": "^2.0.0", - "is-unicode-supported": "^2.1.0", - "log-symbols": "^7.0.1", - "stdin-discarder": "^0.3.1", - "string-width": "^8.1.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ora/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/ora/node_modules/cli-spinners": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-3.4.0.tgz", - "integrity": "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw==", - "license": "MIT", - "engines": { - "node": ">=18.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ora/node_modules/string-width": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.0.tgz", - "integrity": "sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw==", - "license": "MIT", - "dependencies": { - "get-east-asian-width": "^1.5.0", - "strip-ansi": "^7.1.2" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ora/node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } + "node_modules/opusscript": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/opusscript/-/opusscript-0.1.1.tgz", + "integrity": "sha512-mL0fZZOUnXdZ78woRXp18lApwpp0lF5tozJOD1Wut0dgrA9WuQTgSels/CSmFleaAZrJi/nci5KOVtbuxeWoQA==", + "license": "MIT" }, "node_modules/os-homedir": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==", "license": "MIT", "engines": { "node": ">=0.10.0" } }, - "node_modules/os-tmpdir": { - "version": "1.0.2", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/osenv": { - "version": "0.1.5", - "license": "ISC", - "dependencies": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, "node_modules/own-keys": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", "license": "MIT", "dependencies": { "get-intrinsic": "^1.2.6", @@ -10902,49 +5251,17 @@ }, "node_modules/p-defer": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-3.0.0.tgz", + "integrity": "sha512-ugZxsxmtTln604yeYd29EGrNhazN2lywetzpKhfmQjW/VJmhpDmWbiX+h0zL8V91R0UXkhb3KtPmyq9PZw3aYw==", "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/p-limit": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-try": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/p-limit/node_modules/p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^1.1.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/p-try": { "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", "license": "MIT", "engines": { "node": ">=6" @@ -10952,10 +5269,14 @@ }, "node_modules/package-json-from-dist": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "license": "BlueOak-1.0.0" }, "node_modules/packet-stream": { "version": "2.0.6", + "resolved": "https://registry.npmjs.org/packet-stream/-/packet-stream-2.0.6.tgz", + "integrity": "sha512-kSxHpoTqlgNEetMp77snCTVILwLw4dJX6pB/z1g1PRG5xylH8cf9upIPygt+epBC3l14XrcZH4/kQYSrzp2Ijg==", "license": "MIT", "engines": { "node": ">=8" @@ -10963,6 +5284,8 @@ }, "node_modules/packet-stream-codec": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/packet-stream-codec/-/packet-stream-codec-1.2.0.tgz", + "integrity": "sha512-3xoTsSVqCPd+0mPsQGlfYm2ecvJK9tS1HOxrjnKEiB1Ynq0fOJHEXcZV/hxW6BkOSGBsGX7dTN8bjdNTU3nKBA==", "license": "MIT", "dependencies": { "pull-reader": "^1.3.1", @@ -10972,51 +5295,26 @@ "node": ">=12" } }, - "node_modules/parse-entities": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-1.2.2.tgz", - "integrity": "sha512-NzfpbxW/NPrzZ/yYSoQxyqUZMZXIdCfE0OIN4ESsnptHJECoUk3FZktxNuzQf4tjt5UEopnxpYJbvYuxIFDdsg==", - "license": "MIT", - "dependencies": { - "character-entities": "^1.0.0", - "character-entities-legacy": "^1.0.0", - "character-reference-invalid": "^1.0.0", - "is-alphanumerical": "^1.0.0", - "is-decimal": "^1.0.0", - "is-hexadecimal": "^1.0.0" - } - }, - "node_modules/parse-glob": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz", - "integrity": "sha512-FC5TeK0AwXzq3tUBFtH74naWkPQCEWs4K+xMxWZBlKDWu0bVHXGZa+KKqxKidd7xwhdZ19ZNuF2uO1M/r196HA==", - "license": "MIT", - "dependencies": { - "glob-base": "^0.3.0", - "is-dotfile": "^1.0.0", - "is-extglob": "^1.0.0", - "is-glob": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", - "dev": true, - "license": "MIT", - "dependencies": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" - }, - "engines": { - "node": ">=4" - } + "node_modules/pako": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.2.0.tgz", + "integrity": "sha512-zJq6RP/5q+TO2OpFV3FHzlPnFjmkb7Nc99a5SNjJE+uu/PkpChs+NIZSSzbBoD+6kjiISXjfYdwj1ZRQ81dz/w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "(MIT AND Zlib)" }, "node_modules/parse-ms": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-2.1.0.tgz", + "integrity": "sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA==", "license": "MIT", "engines": { "node": ">=6" @@ -11036,22 +5334,23 @@ }, "node_modules/parseurl": { "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", "license": "MIT", "engines": { "node": ">= 0.8" } }, - "node_modules/pascalcase": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", - "integrity": "sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "license": "MIT" }, "node_modules/path-exists": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "license": "MIT", "engines": { "node": ">=8" @@ -11059,6 +5358,8 @@ }, "node_modules/path-is-absolute": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -11066,6 +5367,8 @@ }, "node_modules/path-key": { "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "license": "MIT", "engines": { "node": ">=8" @@ -11073,10 +5376,14 @@ }, "node_modules/path-parse": { "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "license": "MIT" }, "node_modules/path-scurry": { "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", "license": "BlueOak-1.0.0", "dependencies": { "lru-cache": "^10.2.0", @@ -11091,33 +5398,14 @@ }, "node_modules/path-to-regexp": { "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", "license": "MIT" }, - "node_modules/path-type": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", - "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", - "dev": true, - "license": "MIT", - "dependencies": { - "pify": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/pdfjs-dist": { - "version": "5.3.31", - "license": "Apache-2.0", - "engines": { - "node": ">=20.16.0 || >=22.3.0" - }, - "optionalDependencies": { - "@napi-rs/canvas": "^0.1.67" - } - }, "node_modules/peek-readable": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-4.1.0.tgz", + "integrity": "sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg==", "license": "MIT", "engines": { "node": ">=8" @@ -11127,77 +5415,10 @@ "url": "https://github.com/sponsors/Borewit" } }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/pinkie": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pinkie-promise": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", - "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==", - "license": "MIT", - "dependencies": { - "pinkie": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/platform": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz", - "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==", - "license": "MIT" - }, - "node_modules/plur": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/plur/-/plur-2.1.2.tgz", - "integrity": "sha512-WhcHk576xg9y/iv6RWOuroZgsqvCbJN+XGvAypCJwLAYs2iWDp5LUmvaCdV6JR2O0SMBf8l6p7A94AyLCFVMlQ==", - "license": "MIT", - "dependencies": { - "irregular-plurals": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/pngjs": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", "license": "MIT", "engines": { "node": ">=10.13.0" @@ -11205,126 +5426,27 @@ }, "node_modules/polyraf": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/polyraf/-/polyraf-1.1.0.tgz", + "integrity": "sha512-wKRyhZQE6AC70nJyMCWbwd/dX4S6UsFz+58qAF8HTpCn6C7pvr63FiH4vR9vT6VrhZrJKuT7A81ea/lIslS0bA==", "license": "MIT", "dependencies": { "random-access-file": "^2.1.0", "random-access-web": "^2.0.1" } }, - "node_modules/posix-character-classes": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", - "integrity": "sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/possible-typed-array-names": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", "license": "MIT", "engines": { "node": ">= 0.4" } }, - "node_modules/prebuild-install": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", - "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", - "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", - "license": "MIT", - "dependencies": { - "detect-libc": "^2.0.0", - "expand-template": "^2.0.3", - "github-from-package": "0.0.0", - "minimist": "^1.2.3", - "mkdirp-classic": "^0.5.3", - "napi-build-utils": "^2.0.0", - "node-abi": "^3.3.0", - "pump": "^3.0.0", - "rc": "^1.2.7", - "simple-get": "^4.0.0", - "tar-fs": "^2.0.0", - "tunnel-agent": "^0.6.0" - }, - "bin": { - "prebuild-install": "bin.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/prebuild-install/node_modules/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "license": "ISC" - }, - "node_modules/prebuild-install/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/prebuild-install/node_modules/tar-fs": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", - "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", - "license": "MIT", - "dependencies": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.1.4" - } - }, - "node_modules/prebuild-install/node_modules/tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "license": "MIT", - "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/preserve": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz", - "integrity": "sha512-s/46sYeylUfHNjI+sA/78FAHlmIuKqI9wNnzEOGehAlUUYeObv5C2mOinXBjyUyWmJ2SfcS2/ydApH4hTF4WXQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pretty-bytes": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.1.1.tgz", - "integrity": "sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==", - "license": "MIT", - "engines": { - "node": "^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/pretty-ms": { "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-7.0.1.tgz", + "integrity": "sha512-973driJZvxiGOQ5ONsFhOF/DtzPMOMtgC11kCpUrPGMTgqp2q/1gwzCquocrN33is0VZ5GFHXZYMM9l6h67v2Q==", "license": "MIT", "dependencies": { "parse-ms": "^2.1.0" @@ -11338,10 +5460,14 @@ }, "node_modules/print-tree": { "version": "0.1.5", + "resolved": "https://registry.npmjs.org/print-tree/-/print-tree-0.1.5.tgz", + "integrity": "sha512-t5eFtuheMgWGR3kj1vtxsvfWrrecEEWow3IgEtbW0EPbYRufHAvB+ShK1DgRjWxEYYEqO4B6DNlkoPmh7E3LGQ==", "license": "MIT" }, "node_modules/private-box": { "version": "0.3.1", + "resolved": "https://registry.npmjs.org/private-box/-/private-box-0.3.1.tgz", + "integrity": "sha512-abAuk3ZDyQvPLY6MygtwaDTUBIZ0C5wMMuX1jXa0svazV+keTwn7cPobRv4WYA9ctsDUztm/9CYu4y2TPL08xw==", "license": "MIT", "dependencies": { "chloride": "^2.2.9" @@ -11349,6 +5475,8 @@ }, "node_modules/private-group-spec": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/private-group-spec/-/private-group-spec-1.2.0.tgz", + "integrity": "sha512-O7SfG+vZIZgqDXy/wjsuTRI5LaozW4rxaZBpGmwlcDfjIvxvYWNboyNm1PoQUU6j4dQ02V1tOQVLDq9u2RzolA==", "license": "LGPL-3.0-only", "dependencies": { "is-my-ssb-valid": "^1.2.0" @@ -11356,6 +5484,8 @@ }, "node_modules/process": { "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", "license": "MIT", "engines": { "node": ">= 0.6.0" @@ -11363,10 +5493,14 @@ }, "node_modules/process-nextick-args": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "license": "MIT" }, "node_modules/promisify-4loc": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/promisify-4loc/-/promisify-4loc-1.0.0.tgz", + "integrity": "sha512-u/XtndUyqqDXAuhFEgFgkpjHG8IizREoj80j5dL4t41eE9yH0gzFPyOD21/VnikdPJtRziuqf6ryTu1HoTjyog==", "license": "MIT", "engines": { "node": ">=6" @@ -11374,6 +5508,8 @@ }, "node_modules/promisify-tuple": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/promisify-tuple/-/promisify-tuple-1.2.0.tgz", + "integrity": "sha512-DRI8QrLUzbQxgwLiwKhtVCpSqtAUnnyPaCi3cad2+0avb2o5UzobLWHkXUOAYQB8e4fSJVef22eVm77c/8n//g==", "license": "MIT", "engines": { "node": ">=6" @@ -11381,58 +5517,10 @@ }, "node_modules/promisize": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/promisize/-/promisize-1.1.2.tgz", + "integrity": "sha512-6/X05CD1iri6YyLy6TW7a23HY0igsrb/qetltYKfJznLfzmspWtN/cY/UR0By3M5i13hBDWfmM2P42ovKl3GAw==", "license": "MIT" }, - "node_modules/proper-lockfile": { - "version": "4.1.2", - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "retry": "^0.12.0", - "signal-exit": "^3.0.2" - } - }, - "node_modules/proper-lockfile/node_modules/signal-exit": { - "version": "3.0.7", - "license": "ISC" - }, - "node_modules/protobufjs": { - "version": "6.11.6", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.6.tgz", - "integrity": "sha512-k8BHqgPBOtrlougZZqF2uUk5Z7bN8f0wj+3e8M3hvtSv0NBAz4VBy5f6R5Nxq/l+i7mRFTgNZb2trxqTpHNY/A==", - "hasInstallScript": true, - "license": "BSD-3-Clause", - "dependencies": { - "@protobufjs/aspromise": "^1.1.2", - "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.4", - "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.0", - "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.0", - "@protobufjs/path": "^1.1.2", - "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.0", - "@types/long": "^4.0.1", - "@types/node": ">=13.7.0", - "long": "^4.0.0" - }, - "bin": { - "pbjs": "bin/pbjs", - "pbts": "bin/pbts" - } - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, "node_modules/proxy-from-env": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", @@ -11444,42 +5532,35 @@ }, "node_modules/prr": { "version": "1.0.1", - "license": "MIT" - }, - "node_modules/pstree.remy": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", - "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", - "dev": true, + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", "license": "MIT" }, "node_modules/pull-abortable": { "version": "4.1.1", + "resolved": "https://registry.npmjs.org/pull-abortable/-/pull-abortable-4.1.1.tgz", + "integrity": "sha512-/zz8tW+T5tvtDc4NQVZTp3lJZoGEpUb2VDV3R5Q2/9lavNTwHK+hC5yF/3Q3LMmdn5AluFy23RFoqXIoySPl1w==", "license": "MIT" }, "node_modules/pull-async": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/pull-async/-/pull-async-1.0.0.tgz", + "integrity": "sha512-mm0mZe9tb3yjIhfXt384c8EXGFAtMbau20At2yNqZDPXHevjrfom5b82sk8BMQfGhj6gXimnOde1lhUKkF/v9w==", "license": "Apache-2.0" }, "node_modules/pull-awaitable": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/pull-awaitable/-/pull-awaitable-1.0.0.tgz", + "integrity": "sha512-tfufzU9wMd1rM38tcgunaKMUNGXMj1Qms1ygEw+ZerkMmLAwilEtMsyzrDSLhNlnfhSzHXovOarieSNrTD1ovQ==", "license": "MIT", "dependencies": { "pull-thenable": "~1.0.0" } }, - "node_modules/pull-bitflipper": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/pull-bitflipper/-/pull-bitflipper-0.1.1.tgz", - "integrity": "sha512-MloQIUQkooVrlc4qom1rDeUsnx9Fol4ieMhlJEjpT/v2x37+ydL164jbRkvDFCpWBp+9qLKPgWoQum/zh8iHkA==", - "dev": true, - "license": "MIT", - "dependencies": { - "pull-stream": "^3.6.11" - } - }, "node_modules/pull-box-stream": { "version": "1.0.13", + "resolved": "https://registry.npmjs.org/pull-box-stream/-/pull-box-stream-1.0.13.tgz", + "integrity": "sha512-OPg5cTnBCH9jpTCNZfm+BrtrAzmu5NsWDyDadaWiB06oWQujSMfRJp58VqhuJOCEgxVkeKFhXTaSKE+StNKRHw==", "license": "MIT", "dependencies": { "chloride": "^2.2.7", @@ -11492,18 +5573,26 @@ }, "node_modules/pull-cat": { "version": "1.1.11", + "resolved": "https://registry.npmjs.org/pull-cat/-/pull-cat-1.1.11.tgz", + "integrity": "sha512-i3w+xZ3DCtTVz8S62hBOuNLRHqVDsHMNZmgrZsjPnsxXUgbWtXEee84lo1XswE7W2a3WHyqsNuDJTjVLAQR8xg==", "license": "MIT" }, "node_modules/pull-catch": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pull-catch/-/pull-catch-1.1.0.tgz", + "integrity": "sha512-x6DkWZfBVvYrjUIcFroe3GtBkZcRINi8PsYgx3qBYluIeawLn4Z5oJI++WE5AJPbIZBmxHFx+zIvQmhRYaSeFA==", "license": "ISC" }, "node_modules/pull-cont": { "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pull-cont/-/pull-cont-0.1.1.tgz", + "integrity": "sha512-OAJRFBoEW00F0++qkeNuIWdaUwqrlZGj/6JAm+094iWKDnuzBXUWi2IipDqNa+yzIJrOE8BjwugiyGtLvPAk4w==", "license": "MIT" }, "node_modules/pull-cursor": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pull-cursor/-/pull-cursor-3.0.0.tgz", + "integrity": "sha512-95lZVSF2eSEdOmUtlOBaD9p5YOvlYeCr5FBv2ySqcj/4rpaXI6d8OH+zPHHjKAf58R8QXJRZuyfHkcCX8TZbAg==", "license": "MIT", "dependencies": { "looper": "^4.0.0", @@ -11513,10 +5602,14 @@ }, "node_modules/pull-defer": { "version": "0.2.3", + "resolved": "https://registry.npmjs.org/pull-defer/-/pull-defer-0.2.3.tgz", + "integrity": "sha512-/An3KE7mVjZCqNhZsr22k1Tx8MACnUnHZZNPSJ0S62td8JtYr/AiRG42Vz7Syu31SoTLUzVIe61jtT/pNdjVYA==", "license": "MIT" }, "node_modules/pull-drain-gently": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pull-drain-gently/-/pull-drain-gently-1.1.0.tgz", + "integrity": "sha512-ZUPsNrn8jkU6Y2B4w8Jz3gXAmjSpb+qn4AQhAL8qTWUHULglH16ANr+6qnfOEa1kUoUGVCQZaORTd2NSQFAnhA==", "license": "MIT", "dependencies": { "cpu-percentage": "~1.0.3", @@ -11525,6 +5618,8 @@ }, "node_modules/pull-file": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pull-file/-/pull-file-1.1.0.tgz", + "integrity": "sha512-yP3hNN3C0PrxA5isqyzHvsvFxd7Ohjvp4aLreeHm8mhnipxUfbY08Mk7o/KDsnR8N4WT9W3CqMDysU+ZufMr6w==", "license": "MIT", "dependencies": { "pull-utf8-decoder": "^1.0.2" @@ -11532,6 +5627,8 @@ }, "node_modules/pull-flat-merge": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pull-flat-merge/-/pull-flat-merge-2.1.0.tgz", + "integrity": "sha512-V/HzK88m+N867KfDkDA0u9lHrQIcLMBsSllVReO4mU3hJ2PaPOPxtTXQuQc+eIWMHavWqaWSkim8O8s2P4dTLg==", "license": "ISC", "dependencies": { "pull-many": "^1.0.8", @@ -11540,10 +5637,14 @@ }, "node_modules/pull-flatmap": { "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pull-flatmap/-/pull-flatmap-0.0.1.tgz", + "integrity": "sha512-9BgwZPZ6J22kPf9ExoVI3m2yMHdCK8uPf58p6L63t36IgH7NrCX+p3QV8cQ4JmYjwvXDZzimVuJ7IW7iLxm7cA==", "license": "MIT" }, "node_modules/pull-fs": { "version": "1.1.6", + "resolved": "https://registry.npmjs.org/pull-fs/-/pull-fs-1.1.6.tgz", + "integrity": "sha512-xO+d44h/FdlSW61RpxUdqLhNz5vHKnjqEyGaXIVvZa5TxkOvIzhkWd7BXqH8oakXCxb8NozN6F/w1BtfP8Z64Q==", "license": "MIT", "dependencies": { "pull-file": "^0.5.0", @@ -11554,6 +5655,8 @@ }, "node_modules/pull-fs/node_modules/pull-file": { "version": "0.5.0", + "resolved": "https://registry.npmjs.org/pull-file/-/pull-file-0.5.0.tgz", + "integrity": "sha512-4s/IfBLebH/eWUlhp3F3M29mk+B1cryp3W99t/k9J/c033f+HyLLVB2EAMGoN02j8ff7ObkLF1tMaHok3nGS6Q==", "license": "MIT", "dependencies": { "pull-utf8-decoder": "^1.0.2" @@ -11561,6 +5664,8 @@ }, "node_modules/pull-glob": { "version": "1.0.7", + "resolved": "https://registry.npmjs.org/pull-glob/-/pull-glob-1.0.7.tgz", + "integrity": "sha512-nQrhcraiOUfMYiFoJyGLICb5AzlmWm5Vzwhqt+wNuimCG1kqrjdWycw5Dk9xQbHYbq5U55hs3Hee0qZgpjZ3bA==", "license": "MIT", "dependencies": { "pull-fs": "~1.1.6", @@ -11572,6 +5677,8 @@ }, "node_modules/pull-goodbye": { "version": "0.0.3", + "resolved": "https://registry.npmjs.org/pull-goodbye/-/pull-goodbye-0.0.3.tgz", + "integrity": "sha512-fl3RcIHKsxFaygdU3dcwSznLr73HYGOEU9IshpiatYSV+PW3TOUEjtfdu1L8uIsUoDajJz3HM/+mG0mFD4+v5A==", "license": "MIT", "dependencies": { "pull-stream": "^3.6.14" @@ -11579,6 +5686,8 @@ }, "node_modules/pull-handshake": { "version": "1.1.4", + "resolved": "https://registry.npmjs.org/pull-handshake/-/pull-handshake-1.1.4.tgz", + "integrity": "sha512-+dT8auWatbSNt1o43GKygECvOM2znXdyWwhEQaxztJSvxrNtEqo/wgZttknLfyxxbkbS6qUten6LsXTOwYj0yg==", "license": "MIT", "dependencies": { "pull-cat": "^1.1.9", @@ -11589,10 +5698,14 @@ }, "node_modules/pull-hash": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/pull-hash/-/pull-hash-1.0.0.tgz", + "integrity": "sha512-m+6RcvwRdpYLxWgSmviXo3tmZHdpniWDJj3ryC39RYH4oc6AXhkoBHkQuA4yhmkM0WQh+OP+vD9F5oCozhhxIg==", "license": "Fair" }, "node_modules/pull-inactivity": { "version": "2.1.4", + "resolved": "https://registry.npmjs.org/pull-inactivity/-/pull-inactivity-2.1.4.tgz", + "integrity": "sha512-W2Q+6Jk0oRMICYXBXom3/ipz2U5YPUSQUfLvVgUqL/daHop7QQB3Jz5XpnnxsekCPoM61lGvXc7kFDZt0uWzMg==", "license": "MIT", "dependencies": { "pull-abortable": "^4.1.1", @@ -11601,6 +5714,8 @@ }, "node_modules/pull-level": { "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pull-level/-/pull-level-2.0.4.tgz", + "integrity": "sha512-fW6pljDeUThpq5KXwKbRG3X7Ogk3vc75d5OQU/TvXXui65ykm+Bn+fiktg+MOx2jJ85cd+sheufPL+rw9QSVZg==", "license": "MIT", "dependencies": { "level-post": "^1.0.7", @@ -11614,6 +5729,8 @@ }, "node_modules/pull-live": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pull-live/-/pull-live-1.0.1.tgz", + "integrity": "sha512-tkNz1QT5gId8aPhV5+dmwoIiA1nmfDOzJDlOOUpU5DNusj6neNd3EePybJ5+sITr2FwyCs/FVpx74YMCfc8YeA==", "license": "MIT", "dependencies": { "pull-cat": "^1.1.9", @@ -11622,6 +5739,8 @@ }, "node_modules/pull-looper": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/pull-looper/-/pull-looper-1.0.0.tgz", + "integrity": "sha512-djlD60A6NGe5goLdP5pgbqzMEiWmk1bInuAzBp0QOH4vDrVwh05YDz6UP8+pOXveKEk8wHVP+rB2jBrK31QMPA==", "license": "Fair", "dependencies": { "looper": "^4.0.0" @@ -11629,19 +5748,17 @@ }, "node_modules/pull-many": { "version": "1.0.9", + "resolved": "https://registry.npmjs.org/pull-many/-/pull-many-1.0.9.tgz", + "integrity": "sha512-+jUydDVlj/HsvtDqxWMSsiRq3B0HVo7RhBV4C0p2nZRS3mFTUEu9SPEBN+B5PMaW8KTnblYhTIaKg7oXgGnj4Q==", "license": "MIT", "dependencies": { "pull-stream": "^3.4.5" } }, - "node_modules/pull-next": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/pull-next/-/pull-next-1.0.1.tgz", - "integrity": "sha512-CAOxYSfmFmQ+jxuwnmaXmovYOmVD+XC7X+5uyZh4hVg06DUK7O9uccsIQkGu493eGkAqASlmB5zekEH7oxS0AQ==", - "license": "MIT" - }, "node_modules/pull-notify": { "version": "0.1.2", + "resolved": "https://registry.npmjs.org/pull-notify/-/pull-notify-0.1.2.tgz", + "integrity": "sha512-oooAxYEUGNbOVsUrmqqTWWsAUMRIs4sYglnxgleiVcWyvrWgOuk/WUoZDajPTsYix2/rd+z5xSclzHLA7QygcQ==", "license": "MIT", "dependencies": { "pull-pushable": "^2.0.0" @@ -11649,10 +5766,14 @@ }, "node_modules/pull-pair": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pull-pair/-/pull-pair-1.1.0.tgz", + "integrity": "sha512-7VEFLxWnj2AKaN3AvtyaM508d1+56/ulSjDLu4j6kcd9DnEM6LfG8b1R9gCLqzIjJo5stFrWynVc6e451OW4LQ==", "license": "MIT" }, "node_modules/pull-paramap": { "version": "1.2.2", + "resolved": "https://registry.npmjs.org/pull-paramap/-/pull-paramap-1.2.2.tgz", + "integrity": "sha512-0ALwLcASif3KmKCXO4liySCKsTfC/mGYhRFch+xLvfJJrYOIfXPthrykDUZpeZtbjEXglPySzfUmYcnBqzTj5w==", "license": "MIT", "dependencies": { "looper": "^4.0.0" @@ -11660,10 +5781,14 @@ }, "node_modules/pull-pause": { "version": "0.0.2", + "resolved": "https://registry.npmjs.org/pull-pause/-/pull-pause-0.0.2.tgz", + "integrity": "sha512-yQs63NDgD/FeAsdF7INpy4uDolkstkS4Gx1Z3BvMR2gQS8RREJd2tsy6d/K6T+/CDStGnCQI7JLrkjz10YNiSA==", "license": "MIT" }, "node_modules/pull-ping": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pull-ping/-/pull-ping-2.0.3.tgz", + "integrity": "sha512-nbY4yHnMesJBrvkbhMim4VXUC9k1VCkgrkQu49pf8mxFbmb/U2KQrsuePvSmLjRL+VgkBVRSUXUoOY7DtSvhKw==", "license": "MIT", "dependencies": { "pull-pushable": "^2.0.0", @@ -11673,10 +5798,14 @@ }, "node_modules/pull-pushable": { "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pull-pushable/-/pull-pushable-2.2.0.tgz", + "integrity": "sha512-M7dp95enQ2kaHvfCt2+DJfyzgCSpWVR2h2kWYnVsW6ZpxQBx5wOu0QWOvQPVoPnBLUZYitYP2y7HyHkLQNeGXg==", "license": "MIT" }, "node_modules/pull-rate": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pull-rate/-/pull-rate-1.0.2.tgz", + "integrity": "sha512-HH4fXHM7gu2uCO6u2SqTuGGDqPZNVHKXjTdyP9igg8jAROnvSph9plMAQqmv4elYFCm+Bfah2ASGWWe/+NdzMQ==", "license": "MIT", "dependencies": { "pull-stream": "^3.6.0" @@ -11684,14 +5813,14 @@ }, "node_modules/pull-reader": { "version": "1.3.1", - "license": "MIT" - }, - "node_modules/pull-sink-through": { - "version": "0.0.0", + "resolved": "https://registry.npmjs.org/pull-reader/-/pull-reader-1.3.1.tgz", + "integrity": "sha512-CBkejkE5nX50SiSEzu0Qoz4POTJMS/mw8G6aj3h3M/RJoKgggLxyF0IyTZ0mmpXFlXRcLmLmIEW4xeYn7AeDYw==", "license": "MIT" }, "node_modules/pull-sort": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pull-sort/-/pull-sort-1.0.2.tgz", + "integrity": "sha512-jGcAHMP+0Le+bEIhSODlbNNd3jW+S6XrXOlhVzfcKU5HQZjP92OzQSgHHSlwvWRsiTWi+UGgbFpL/5gGgmFoVQ==", "license": "MIT", "dependencies": { "pull-defer": "^0.2.3", @@ -11700,10 +5829,14 @@ }, "node_modules/pull-stream": { "version": "3.7.0", + "resolved": "https://registry.npmjs.org/pull-stream/-/pull-stream-3.7.0.tgz", + "integrity": "sha512-Eco+/R004UaCK2qEDE8vGklcTG2OeZSVm1kTUQNrykEjDwcFXDZhygFDsW49DbXyJMEhHeRL3z5cRVqPAhXlIw==", "license": "MIT" }, "node_modules/pull-thenable": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/pull-thenable/-/pull-thenable-1.0.0.tgz", + "integrity": "sha512-gio2Yanuj5lcN8X+VXtx6F2iWxnhxfaHSKaSJHUzJMNNboEz48/+udzZFDaya8kAwj3DbuFFt1pbb8m1tIS6PQ==", "license": "MIT", "dependencies": { "quicktask": "~1.0.0" @@ -11711,6 +5844,8 @@ }, "node_modules/pull-through": { "version": "1.0.18", + "resolved": "https://registry.npmjs.org/pull-through/-/pull-through-1.0.18.tgz", + "integrity": "sha512-t8BBRdKstI/JoWS3FAXRnBvo1ahoLcKo8nm8ZUzr4OUgIf47RodD6HjnPIpw/d6HQZaMu+TotLAYCNwLTnvcNw==", "license": "MIT", "dependencies": { "looper": "~3.0.0" @@ -11718,18 +5853,26 @@ }, "node_modules/pull-through/node_modules/looper": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/looper/-/looper-3.0.0.tgz", + "integrity": "sha512-LJ9wplN/uSn72oJRsXTx+snxPet5c8XiZmOKCm906NVYu+ag6SB6vUcnJcWxgnl2NfbIyeobAn7Bwv6xRj2XJg==", "license": "MIT" }, "node_modules/pull-traverse": { "version": "1.0.3", + "resolved": "https://registry.npmjs.org/pull-traverse/-/pull-traverse-1.0.3.tgz", + "integrity": "sha512-tdvTsDPh7PwuBEJ3o7DI6h/bQ5ocm9W0TYWNWEQ37ZS9Mt9aRfJqfvmAIdrpg3DgahU9PSITX0fYg8xTbqEdjQ==", "license": "MIT" }, "node_modules/pull-utf8-decoder": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pull-utf8-decoder/-/pull-utf8-decoder-1.0.2.tgz", + "integrity": "sha512-hpGheDlDMMEOkiBwXpiUqaFVR0SlGb23vZyJuHe1s8CWGJVcDEzhy9HHUEIuPjTEajYb90ikKsDmt6lkmTtk5g==", "license": "MIT" }, "node_modules/pull-websocket": { "version": "3.4.2", + "resolved": "https://registry.npmjs.org/pull-websocket/-/pull-websocket-3.4.2.tgz", + "integrity": "sha512-hGFWC4/fzRdO2FEsyR9woVzgv/yG4PIk3RXPN4azBpomGzGQFRUORwKQDS3j7RAIy8tjvN2W+qjU8jNn2NWeNQ==", "license": "ISC", "dependencies": { "relative-url": "^1.0.2", @@ -11739,6 +5882,8 @@ }, "node_modules/pull-window": { "version": "2.1.4", + "resolved": "https://registry.npmjs.org/pull-window/-/pull-window-2.1.4.tgz", + "integrity": "sha512-cbDzN76BMlcGG46OImrgpkMf/VkCnupj8JhsrpBw3aWBM9ye345aYnqitmZCgauBkc0HbbRRn9hCnsa3k2FNUg==", "license": "MIT", "dependencies": { "looper": "^2.0.0" @@ -11746,40 +5891,20 @@ }, "node_modules/pull-window/node_modules/looper": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/looper/-/looper-2.0.0.tgz", + "integrity": "sha512-6DzMHJcjbQX/UPHc1rRCBfKlLwDkvuGZ715cIR36wSdYqWXFT35uLXq5P/2orl3tz+t+VOVPxw4yPinQlUDGDQ==", "license": "MIT" }, - "node_modules/pull-write": { - "version": "1.1.4", - "license": "MIT", - "dependencies": { - "looper": "^4.0.0", - "pull-cat": "^1.1.11", - "pull-stream": "^3.4.5" - } - }, "node_modules/pull-write-file": { "version": "0.2.4", + "resolved": "https://registry.npmjs.org/pull-write-file/-/pull-write-file-0.2.4.tgz", + "integrity": "sha512-XqtjwmjObiJYj/ojJskRayIzlmP6ufzG6Ec5oSd/qhlUWr4zMsAcJoiSPjUi079QFqvlb54HaWCBkbpg2zZiKQ==", "license": "MIT" }, - "node_modules/pump": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", - "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, "node_modules/punycode": { "version": "2.3.1", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/punycode.js": { - "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "license": "MIT", "engines": { "node": ">=6" @@ -11787,10 +5912,14 @@ }, "node_modules/push-stream": { "version": "11.2.0", + "resolved": "https://registry.npmjs.org/push-stream/-/push-stream-11.2.0.tgz", + "integrity": "sha512-MbiU+tFKDFv6IRwxfU2pltftFiIsW6mhLT3q91EJQwZYd2A9aaytdKAnRGYCx9KAuWQ11/cClK/FvHY996WwxA==", "license": "MIT" }, "node_modules/push-stream-to-pull-stream": { "version": "1.0.5", + "resolved": "https://registry.npmjs.org/push-stream-to-pull-stream/-/push-stream-to-pull-stream-1.0.5.tgz", + "integrity": "sha512-oQfzDroAv+SySQIXFiBVkShIh8Vgpr+hd7TrwyUna1kVrbv3i6D+QQC+31QdI7D6Jow61QLQW+uWToxv4cXI2w==", "license": "MIT", "dependencies": { "pull-looper": "^1.0.0", @@ -11799,6 +5928,8 @@ }, "node_modules/qrcode": { "version": "1.5.4", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", "license": "MIT", "dependencies": { "dijkstrajs": "^1.0.1", @@ -11814,6 +5945,8 @@ }, "node_modules/qrcode/node_modules/camelcase": { "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", "license": "MIT", "engines": { "node": ">=6" @@ -11821,6 +5954,8 @@ }, "node_modules/qrcode/node_modules/cliui": { "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", "license": "ISC", "dependencies": { "string-width": "^4.2.0", @@ -11830,10 +5965,14 @@ }, "node_modules/qrcode/node_modules/emoji-regex": { "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "license": "MIT" }, "node_modules/qrcode/node_modules/find-up": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", "license": "MIT", "dependencies": { "locate-path": "^5.0.0", @@ -11845,6 +5984,8 @@ }, "node_modules/qrcode/node_modules/locate-path": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", "license": "MIT", "dependencies": { "p-locate": "^4.1.0" @@ -11855,6 +5996,8 @@ }, "node_modules/qrcode/node_modules/p-limit": { "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "license": "MIT", "dependencies": { "p-try": "^2.0.0" @@ -11868,6 +6011,8 @@ }, "node_modules/qrcode/node_modules/p-locate": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", "license": "MIT", "dependencies": { "p-limit": "^2.2.0" @@ -11878,6 +6023,8 @@ }, "node_modules/qrcode/node_modules/string-width": { "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -11890,6 +6037,8 @@ }, "node_modules/qrcode/node_modules/wrap-ansi": { "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -11902,10 +6051,14 @@ }, "node_modules/qrcode/node_modules/y18n": { "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", "license": "ISC" }, "node_modules/qrcode/node_modules/yargs": { "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", "license": "MIT", "dependencies": { "cliui": "^6.0.0", @@ -11926,6 +6079,8 @@ }, "node_modules/qrcode/node_modules/yargs-parser": { "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", "license": "ISC", "dependencies": { "camelcase": "^5.0.0", @@ -11952,28 +6107,26 @@ }, "node_modules/queue-tick": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", + "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==", "license": "MIT" }, - "node_modules/quick-lru": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-1.1.0.tgz", - "integrity": "sha512-tRS7sTgyxMXtLum8L65daJnHUhfDUgboRdcWW2bR9vBfrj2+O5HSMbQOJfJJjIVSPFqbBCF37FpwWXGitDc5tA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/quicktask": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/quicktask/-/quicktask-1.0.1.tgz", + "integrity": "sha512-+jhR01aSsi6Iw9wbYumKusRK72QK1ub2oE0kOnRyMkMdD/rXMMGW/TVl5edjcLjuuIKP1ezkr+xQzUMD5/4JHw==", "license": "MIT" }, "node_modules/railroad-diagrams": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz", + "integrity": "sha512-cz93DjNeLY0idrCNOH6PviZGRN9GJhsdm9hpn1YCS879fj4W+x5IFJhhkRZcwVgMmFF7R82UA/7Oh+R8lLZg6A==", "license": "CC0-1.0" }, "node_modules/randexp": { "version": "0.4.6", + "resolved": "https://registry.npmjs.org/randexp/-/randexp-0.4.6.tgz", + "integrity": "sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ==", "license": "MIT", "dependencies": { "discontinuous-range": "1.0.0", @@ -11985,6 +6138,8 @@ }, "node_modules/random-access-chrome-file": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/random-access-chrome-file/-/random-access-chrome-file-1.2.0.tgz", + "integrity": "sha512-M1NOdkHEcjRB+acKrdQkwf8aMTnZUIGboiH6i2PMNkjfChBIJiB4j4MuhpOn+u+XU2n7GqpocPN4bzfv0jrBsg==", "license": "MIT", "dependencies": { "random-access-storage": "^1.3.0" @@ -11992,6 +6147,8 @@ }, "node_modules/random-access-file": { "version": "2.2.1", + "resolved": "https://registry.npmjs.org/random-access-file/-/random-access-file-2.2.1.tgz", + "integrity": "sha512-RGU0xmDqdOyEiynob1KYSeh8+9c9Td1MJ74GT1viMEYAn8SJ9oBtWCXLsYZukCF46yududHOdM449uRYbzBrZQ==", "license": "MIT", "dependencies": { "mkdirp-classic": "^0.5.2", @@ -12000,6 +6157,8 @@ }, "node_modules/random-access-idb": { "version": "1.2.2", + "resolved": "https://registry.npmjs.org/random-access-idb/-/random-access-idb-1.2.2.tgz", + "integrity": "sha512-NroFuBNVh5wVIHKN/jEYrgkkffppkfxNWFX9OEwC2VP7dYc3sa+Qxv7tMa1Gi9Jp/ObVfLeCZBt/8Sbn1WU1Xg==", "license": "BSD", "dependencies": { "buffer-alloc": "^1.1.0", @@ -12012,6 +6171,8 @@ }, "node_modules/random-access-idb-mutable-file": { "version": "0.3.0", + "resolved": "https://registry.npmjs.org/random-access-idb-mutable-file/-/random-access-idb-mutable-file-0.3.0.tgz", + "integrity": "sha512-CdVAoFNNDn5uAgYOJ8J3ICSaFzaMOa95XnYcX+taj4jirJuRASiTyQSOGR+Z0K8ZkBGuj0A8ivyeRAWuxRCgQA==", "license": "MIT", "dependencies": { "buffer": "5.1.0", @@ -12020,6 +6181,8 @@ }, "node_modules/random-access-idb-mutable-file/node_modules/buffer": { "version": "5.1.0", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.1.0.tgz", + "integrity": "sha512-YkIRgwsZwJWTnyQrsBTWefizHh+8GYj3kbL1BTiAQ/9pwpino0G7B2gp5tx/FUBqUlvtxV85KNR3mwfAtv15Yw==", "license": "MIT", "dependencies": { "base64-js": "^1.0.2", @@ -12028,6 +6191,8 @@ }, "node_modules/random-access-idb-mutable-file/node_modules/random-access-storage": { "version": "1.3.0", + "resolved": "https://registry.npmjs.org/random-access-storage/-/random-access-storage-1.3.0.tgz", + "integrity": "sha512-pdS9Mcb9TB7oICypPRALlheaSuszuAKmLVEPKJMuYor7R/zDuHh5ALuQoS+ox31XRwQUL+tDwWH2GPdyspwelA==", "license": "MIT", "dependencies": { "inherits": "^2.0.3" @@ -12035,10 +6200,14 @@ }, "node_modules/random-access-idb/node_modules/buffer-from": { "version": "0.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-0.1.2.tgz", + "integrity": "sha512-RiWIenusJsmI2KcvqQABB83tLxCByE3upSP8QU3rJDMVFGPWLvPQJt/O1Su9moRWeH7d+Q2HYb68f6+v+tw2vg==", "license": "MIT" }, "node_modules/random-access-memory": { "version": "3.1.4", + "resolved": "https://registry.npmjs.org/random-access-memory/-/random-access-memory-3.1.4.tgz", + "integrity": "sha512-rqgqd/8ec65gbpKaYHnDOW391OR39d+eXn8NI87G+f3sUKrtGib9jC+/5/9MBFBwwHAZIS8RLJ8yyB4etzbYTA==", "license": "MIT", "dependencies": { "inherits": "^2.0.3", @@ -12048,6 +6217,8 @@ }, "node_modules/random-access-storage": { "version": "1.4.3", + "resolved": "https://registry.npmjs.org/random-access-storage/-/random-access-storage-1.4.3.tgz", + "integrity": "sha512-D5e2iIC5dNENWyBxsjhEnNOMCwZZ64TARK6dyMN+3g4OTC4MJxyjh9hKLjTGoNhDOPrgjI+YlFEHFnrp/cSnzQ==", "license": "MIT", "dependencies": { "events": "^3.3.0", @@ -12057,6 +6228,8 @@ }, "node_modules/random-access-web": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/random-access-web/-/random-access-web-2.0.3.tgz", + "integrity": "sha512-nN3AAgl4/lTOYMk5Qm44SzFsglOmaG2d0Kh0603umh35+rk9QXYLFf0nFJ0GOv9INBsP9iT1lub24r8PjyCtvA==", "license": "MIT", "dependencies": { "@sammacbeth/random-access-idb-mutable-file": "^0.1.1", @@ -12067,47 +6240,10 @@ "random-access-storage": "^1.3.0" } }, - "node_modules/randomatic": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-3.1.1.tgz", - "integrity": "sha512-TuDE5KxZ0J461RVjrJZCJc+J+zCkTb1MbH9AQUq68sMhOMcy9jLcb3BrZKgp9q9Ncltdg4QVqWrH02W2EFFVYw==", - "license": "MIT", - "dependencies": { - "is-number": "^4.0.0", - "kind-of": "^6.0.0", - "math-random": "^1.0.1" - }, - "engines": { - "node": ">= 0.10.0" - } - }, - "node_modules/randomatic/node_modules/is-number": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", - "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/randomatic/node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/raw-body": { "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", "license": "MIT", "dependencies": { "bytes": "3.1.2", @@ -12121,6 +6257,8 @@ }, "node_modules/raw-body/node_modules/iconv-lite": { "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3" @@ -12131,6 +6269,8 @@ }, "node_modules/rc": { "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", "dependencies": { "deep-extend": "^0.6.0", @@ -12144,42 +6284,17 @@ }, "node_modules/rc/node_modules/strip-json-comments": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", "license": "MIT", "engines": { "node": ">=0.10.0" } }, - "node_modules/read-pkg": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", - "integrity": "sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "load-json-file": "^4.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/read-pkg-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-3.0.0.tgz", - "integrity": "sha512-YFzFrVvpC6frF1sz8psoHDBGF7fLPc+llq/8NB43oagqWkx8ar5zYtsTORtOjw9W2RHLpWP+zTWwBvf1bCmcSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "find-up": "^2.0.0", - "read-pkg": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/readable-stream": { "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", "license": "MIT", "dependencies": { "abort-controller": "^3.0.0", @@ -12194,6 +6309,8 @@ }, "node_modules/readable-web-to-node-stream": { "version": "3.0.4", + "resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.4.tgz", + "integrity": "sha512-9nX56alTf5bwXQ3ZDipHJhusu9NTQJ/CVPtb/XHAJCXihZeitfJvIRS4GqQ/mfIoOE3IelHMrpayVrosdHBuLw==", "license": "MIT", "dependencies": { "readable-stream": "^4.7.0" @@ -12208,6 +6325,8 @@ }, "node_modules/readdir-glob": { "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", "license": "Apache-2.0", "dependencies": { "minimatch": "^5.1.0" @@ -12225,320 +6344,16 @@ "node": ">=10" } }, - "node_modules/readdirp": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", - "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.1.11", - "micromatch": "^3.1.10", - "readable-stream": "^2.0.2" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/readdirp/node_modules/arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/readdirp/node_modules/array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha512-SleRWjh9JUud2wH1hPs9rZBZ33H6T9HOiL0uwGnGx9FpE6wKGyfWugmbkEOIs6qWrZhg0LWeLziLrEwQJhs5mQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/readdirp/node_modules/braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "license": "MIT", - "dependencies": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/readdirp/node_modules/braces/node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", - "license": "MIT", - "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/readdirp/node_modules/expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha512-w/ozOKR9Obk3qoWeY/WDi6MFta9AoMR+zud60mdnbniMcBxRuFJyDt2LdX/14A1UABeqk+Uk+LDfUpvoGKppZA==", - "license": "MIT", - "dependencies": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/readdirp/node_modules/expand-brackets/node_modules/define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", - "license": "MIT", - "dependencies": { - "is-descriptor": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/readdirp/node_modules/expand-brackets/node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", - "license": "MIT", - "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/readdirp/node_modules/expand-brackets/node_modules/is-descriptor": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", - "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", - "license": "MIT", - "dependencies": { - "is-accessor-descriptor": "^1.0.1", - "is-data-descriptor": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/readdirp/node_modules/extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "license": "MIT", - "dependencies": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/readdirp/node_modules/extglob/node_modules/define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", - "license": "MIT", - "dependencies": { - "is-descriptor": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/readdirp/node_modules/extglob/node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", - "license": "MIT", - "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/readdirp/node_modules/fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==", - "license": "MIT", - "dependencies": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/readdirp/node_modules/fill-range/node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", - "license": "MIT", - "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/readdirp/node_modules/is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "node_modules/real-cancellable-promise": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/real-cancellable-promise/-/real-cancellable-promise-1.2.3.tgz", + "integrity": "sha512-hBI5Gy/55VEeeMtImMgEirD7eq5UmqJf1J8dFZtbJZA/3rB0pYFZ7PayMGueb6v4UtUtpKpP+05L0VwyE1hI9Q==", "license": "MIT" }, - "node_modules/readdirp/node_modules/is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", - "license": "MIT", - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/readdirp/node_modules/is-number/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "license": "MIT", - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/readdirp/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "license": "MIT" - }, - "node_modules/readdirp/node_modules/isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/readdirp/node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/readdirp/node_modules/micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "license": "MIT", - "dependencies": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/readdirp/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/readdirp/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "license": "MIT" - }, - "node_modules/readdirp/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/redent": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-2.0.0.tgz", - "integrity": "sha512-XNwrTx77JQCEMXTeb8movBKuK75MgH0RZkujNuDKCezemx/voapl9i2gCSi8WWm8+ox5ycJi1gxF22fR7c0Ciw==", - "dev": true, - "license": "MIT", - "dependencies": { - "indent-string": "^3.0.0", - "strip-indent": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -12557,33 +6372,10 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/regex-cache": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.4.tgz", - "integrity": "sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ==", - "license": "MIT", - "dependencies": { - "is-equal-shallow": "^0.1.3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/regex-not": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", - "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", - "license": "MIT", - "dependencies": { - "extend-shallow": "^3.0.2", - "safe-regex": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/regexp.prototype.flags": { "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -12602,182 +6394,14 @@ }, "node_modules/relative-url": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/relative-url/-/relative-url-1.0.2.tgz", + "integrity": "sha512-jI1AmBVFFMXwQ3I6tIYVmVOjy8f+ogHbqkeb8/LL9tszEQiTV8I0l8XT4oEomUOoxfm698f92gYlNKeA/9LJVQ==", "license": "MIT" }, - "node_modules/remark": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/remark/-/remark-3.2.3.tgz", - "integrity": "sha512-E+WibagMmLjs7y5d6qWRiwbL2/z0zbkLwCqvmNlxBTx2Y1ufZ06sir+ZdyQXTxDiEbkbsrX6kG883ItjrEkaSQ==", - "license": "MIT", - "dependencies": { - "camelcase": "^2.0.0", - "ccount": "^1.0.0", - "chalk": "^1.0.0", - "chokidar": "^1.0.5", - "collapse-white-space": "^1.0.0", - "commander": "^2.0.0", - "concat-stream": "^1.0.0", - "debug": "^2.0.0", - "elegant-spinner": "^1.0.0", - "extend.js": "0.0.2", - "glob": "^6.0.1", - "globby": "^4.0.0", - "he": "^0.5.0", - "log-update": "^1.0.1", - "longest-streak": "^1.0.0", - "markdown-table": "^0.4.0", - "minimatch": "^3.0.0", - "npm-prefix": "^1.0.1", - "parse-entities": "^1.0.0", - "repeat-string": "^1.5.0", - "stringify-entities": "^1.0.0", - "to-vfile": "^1.0.0", - "trim": "^0.0.1", - "trim-trailing-lines": "^1.0.0", - "unified": "^2.0.0", - "user-home": "^2.0.0", - "vfile": "^1.1.0", - "vfile-find-down": "^1.0.0", - "vfile-find-up": "^1.0.0", - "vfile-reporter": "^1.5.0", - "ware": "^1.3.0" - }, - "bin": { - "remark": "bin/remark" - } - }, - "node_modules/remark-html": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/remark-html/-/remark-html-2.0.2.tgz", - "integrity": "sha512-2yj8Oad44uo/IWOssZs3Y4AvfrA9nGZqiHsL7ePyZuntPKjtljHZDcroljlQQ+sJryO3Bjnnj30e7wi8aTp/hQ==", - "license": "MIT", - "dependencies": { - "collapse-white-space": "^1.0.0", - "detab": "^1.0.0", - "normalize-uri": "^1.0.0", - "object-assign": "^4.0.1", - "trim": "0.0.1", - "trim-lines": "^1.0.0", - "unist-util-visit": "^1.0.0" - } - }, - "node_modules/remark/node_modules/ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/remark/node_modules/ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/remark/node_modules/camelcase": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", - "integrity": "sha512-DLIsRzJVBQu72meAKPkWQOLcujdXT32hwdfnkI1frSiSRMK1MofjKHf+MEx0SB6fjEFXL8fBDv1dKymBlOp4Qw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/remark/node_modules/chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/remark/node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "license": "MIT" - }, - "node_modules/remark/node_modules/glob": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", - "integrity": "sha512-MKZeRNyYZAVVVG1oZeLaWie1uweH40m9AZwIwxyPbTSX4hHrVYSzLg0Ro5Z5R7XKkIX+Cc6oD1rqeDJnwsB8/A==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "license": "ISC", - "dependencies": { - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "2 || 3", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - } - }, - "node_modules/remark/node_modules/strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/remove-trailing-separator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", - "integrity": "sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==", - "license": "ISC" - }, - "node_modules/repeat-element": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.4.tgz", - "integrity": "sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", - "license": "MIT", - "engines": { - "node": ">=0.10" - } - }, - "node_modules/repeating": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", - "integrity": "sha512-ZqtSMuVybkISo2OWvqvm7iHSWngvdaW3IpsT9/uP8v4gMi591LY6h35wdOfvQdWCKFWZWm2Y1Opp4kV7vQKT6A==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-finite": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/require-directory": { "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -12794,10 +6418,14 @@ }, "node_modules/require-main-filename": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", "license": "ISC" }, "node_modules/resolve": { "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", "license": "MIT", "dependencies": { "is-core-module": "^2.16.0", @@ -12816,6 +6444,8 @@ }, "node_modules/resolve-path": { "version": "1.4.0", + "resolved": "https://registry.npmjs.org/resolve-path/-/resolve-path-1.4.0.tgz", + "integrity": "sha512-i1xevIst/Qa+nA9olDxLWnLk8YZbi8R/7JPbCMcgyWaFR6bKWaexgJgEB5oc2PKMjYdrHynyz0NY+if+H98t1w==", "license": "MIT", "dependencies": { "http-errors": "~1.6.2", @@ -12827,6 +6457,8 @@ }, "node_modules/resolve-path/node_modules/depd": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -12834,6 +6466,8 @@ }, "node_modules/resolve-path/node_modules/http-errors": { "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", "license": "MIT", "dependencies": { "depd": "~1.1.2", @@ -12847,65 +6481,38 @@ }, "node_modules/resolve-path/node_modules/inherits": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", "license": "ISC" }, "node_modules/resolve-path/node_modules/setprototypeof": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", "license": "ISC" }, "node_modules/resolve-path/node_modules/statuses": { "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", "license": "MIT", "engines": { "node": ">= 0.6" } }, - "node_modules/resolve-url": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", - "integrity": "sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==", - "deprecated": "https://github.com/lydell/resolve-url#deprecated", - "license": "MIT" - }, - "node_modules/restore-cursor": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", - "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", - "license": "MIT", - "dependencies": { - "onetime": "^7.0.0", - "signal-exit": "^4.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/ret": { "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", "license": "MIT", "engines": { "node": ">=0.12" } }, - "node_modules/retry": { - "version": "0.12.0", - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/right-pad": { - "version": "1.1.1", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, "node_modules/rimraf": { "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "license": "ISC", "dependencies": { "glob": "^7.1.3" @@ -12919,6 +6526,8 @@ }, "node_modules/rimraf/node_modules/glob": { "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", @@ -12937,59 +6546,20 @@ }, "node_modules/rng": { "version": "0.2.2", - "license": "MIT" - }, - "node_modules/router": { - "version": "2.2.0", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "depd": "^2.0.0", - "is-promise": "^4.0.0", - "parseurl": "^1.3.3", - "path-to-regexp": "^8.0.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/router/node_modules/path-to-regexp": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.0.tgz", - "integrity": "sha512-PuseHIvAnz3bjrM2rGJtSgo1zjgxapTLZ7x2pjhzWwlp4SJQgK3f3iZIQwkpEnBaKz6seKBADpM4B4ySkuYypg==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/run-series": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/run-series/-/run-series-1.1.9.tgz", - "integrity": "sha512-Arc4hUN896vjkqCYrUXquBFtRZdv1PfLbTYP71efP6butxyQ0kWpiNJyAgsxscmQg1cqvHY32/UCBzXedTpU2g==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], + "resolved": "https://registry.npmjs.org/rng/-/rng-0.2.2.tgz", + "integrity": "sha512-3F/A3swXdqb4JLpF22zwlCllyXlUWEE3T35GzJ+Vvtx4HnUYVAXovaK2TyUC04UlpxdVxB+E7HaKdDrGeOcJuA==", "license": "MIT" }, "node_modules/rwlock": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/rwlock/-/rwlock-5.0.0.tgz", + "integrity": "sha512-XgzRqLMfCcm9QfZuPav9cV3Xin5TRcIlp4X/SH3CvB+x5D2AakdlEepfJKDd8ByncvfpcxNWdRZVUl38PS6ZJg==", "license": "MIT" }, "node_modules/safe-array-concat": { "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -13007,6 +6577,8 @@ }, "node_modules/safe-buffer": { "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", "funding": [ { "type": "github", @@ -13025,6 +6597,8 @@ }, "node_modules/safe-push-apply": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -13037,17 +6611,10 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/safe-regex": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", - "integrity": "sha512-aJXcif4xnaNUzvUuC5gcb46oTS7zvg4jpMTnuqtrEPlR3vFr4pxtdTwaF1Qs3Enjn9HK+ZlwQui+a7z0SywIzg==", - "license": "MIT", - "dependencies": { - "ret": "~0.1.10" - } - }, "node_modules/safe-regex-test": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -13063,10 +6630,14 @@ }, "node_modules/safer-buffer": { "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, "node_modules/sanitize-filename": { "version": "1.6.3", + "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz", + "integrity": "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==", "license": "WTFPL OR ISC", "dependencies": { "truncate-utf8-bytes": "^1.0.0" @@ -13086,6 +6657,8 @@ }, "node_modules/secret-handshake": { "version": "1.1.21", + "resolved": "https://registry.npmjs.org/secret-handshake/-/secret-handshake-1.1.21.tgz", + "integrity": "sha512-e02+IddZv40tNJmaRZNZ6aYje95h9MvmpmbOg2PVjLKRA5p9QnrQypJlVE2dU516TyYc9jikBjqhXV1IlkwNUw==", "license": "MIT", "dependencies": { "chloride": "^2.2.8", @@ -13097,6 +6670,8 @@ }, "node_modules/secret-stack": { "version": "6.4.2", + "resolved": "https://registry.npmjs.org/secret-stack/-/secret-stack-6.4.2.tgz", + "integrity": "sha512-aUJNQ1DmZo0loxcnHOU7pvu9N2aOE0H63a1hpd77+I1GbmDBkK5hI9vO16IFf4OYDgPBiBIY3NGAHF23sdJC8g==", "license": "MIT", "dependencies": { "debug": "^4.1.0", @@ -13115,10 +6690,14 @@ }, "node_modules/secret-stack-decorators": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/secret-stack-decorators/-/secret-stack-decorators-1.1.0.tgz", + "integrity": "sha512-wYl0Mcul/fuEbZwn9tN62c+W4LP2RPus/ilt3wdBNQqfBFSMlDXTLaIsrA4SEElb7JEBH4xzdQbOCnTvYHeWCA==", "license": "MIT" }, "node_modules/secret-stack/node_modules/muxrpc": { "version": "6.7.3", + "resolved": "https://registry.npmjs.org/muxrpc/-/muxrpc-6.7.3.tgz", + "integrity": "sha512-yVr66BigJxHIvHOLsdJzuvrHt46Vq9DiDCBTZiS6q+49mXh4mkDjRiXo9FnGXQv1WJOqnRJk5fcfwV6TaSgNvA==", "license": "MIT", "dependencies": { "clarify-error": "^1.0.0", @@ -13141,87 +6720,22 @@ "node": ">=10" } }, - "node_modules/send": { - "version": "1.2.0", - "license": "MIT", - "dependencies": { - "debug": "^4.3.5", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "mime-types": "^3.0.1", - "ms": "^2.1.3", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/send/node_modules/encodeurl": { - "version": "2.0.0", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/send/node_modules/fresh": { - "version": "2.0.0", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/send/node_modules/mime-db": { - "version": "1.54.0", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/send/node_modules/mime-types": { - "version": "3.0.1", - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/separator-escape": { "version": "0.0.1", + "resolved": "https://registry.npmjs.org/separator-escape/-/separator-escape-0.0.1.tgz", + "integrity": "sha512-daCzTzZVoowYzjW7x9xMH6zr+lt/zsGxV1rtXaoTnlues7ZDx6Qu0l5W3jCdgnXGE1ONAGL+XPWY+IRDxnJ9EQ==", "license": "MIT" }, - "node_modules/serve-static": { - "version": "2.2.0", - "license": "MIT", - "dependencies": { - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "parseurl": "^1.3.3", - "send": "^1.2.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/serve-static/node_modules/encodeurl": { - "version": "2.0.0", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/set-blocking": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", "license": "ISC" }, "node_modules/set-function-length": { "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", "license": "MIT", "dependencies": { "define-data-property": "^1.1.4", @@ -13237,6 +6751,8 @@ }, "node_modules/set-function-name": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", "license": "MIT", "dependencies": { "define-data-property": "^1.1.4", @@ -13250,6 +6766,8 @@ }, "node_modules/set-proto": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -13260,35 +6778,10 @@ "node": ">= 0.4" } }, - "node_modules/set-value": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", - "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", - "license": "MIT", - "dependencies": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.3", - "split-string": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/set-value/node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", - "license": "MIT", - "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/set.prototype.difference": { "version": "1.1.7", + "resolved": "https://registry.npmjs.org/set.prototype.difference/-/set.prototype.difference-1.1.7.tgz", + "integrity": "sha512-u6NOoxMovg0cqKd9wa5JoZ9F/5SF5wCdO0w3zKNjKps3VJ594mCQJDGXYtKaJUpwlIZFiZGEbtsNzkWlcr6GLA==", "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -13309,6 +6802,8 @@ }, "node_modules/set.prototype.issubsetof": { "version": "1.1.4", + "resolved": "https://registry.npmjs.org/set.prototype.issubsetof/-/set.prototype.issubsetof-1.1.4.tgz", + "integrity": "sha512-m7paZD4cLQAXj5TLpdvpyX6gQ62onRV65zxjUH4qpZX5BouTBcL89X8BQ3QDhcFKwO2eZql8yTtNKsEmCOb1Tw==", "license": "MIT", "dependencies": { "call-bind": "^1.0.7", @@ -13328,10 +6823,14 @@ }, "node_modules/setprototypeof": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "license": "ISC" }, "node_modules/sha.js": { "version": "2.4.5", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.5.tgz", + "integrity": "sha512-459AP5kwdIhtMLLE1+h2q5a6E7hyY7Ri88GUIhFsyrQinKkm+7qj1ARHELow7GkFS7oimWmIwhXTSxBsuNuOsw==", "license": "MIT", "dependencies": { "inherits": "^2.0.1" @@ -13342,6 +6841,8 @@ }, "node_modules/sha256-universal": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/sha256-universal/-/sha256-universal-1.2.1.tgz", + "integrity": "sha512-ghn3muhdn1ailCQqqceNxRgkOeZSVfSE13RQWEg6njB+itsFzGVSJv+O//2hvNXZuxVIRyNzrgsZ37SPDdGJJw==", "license": "ISC", "dependencies": { "b4a": "^1.0.1", @@ -13350,6 +6851,8 @@ }, "node_modules/sha256-wasm": { "version": "2.2.2", + "resolved": "https://registry.npmjs.org/sha256-wasm/-/sha256-wasm-2.2.2.tgz", + "integrity": "sha512-qKSGARvao+JQlFiA+sjJZhJ/61gmW/3aNLblB2rsgIxDlDxsJPHo8a1seXj12oKtuHVgJSJJ7QEGBUYQN741lQ==", "license": "ISC", "dependencies": { "b4a": "^1.0.1", @@ -13358,6 +6861,8 @@ }, "node_modules/sha512-universal": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/sha512-universal/-/sha512-universal-1.2.1.tgz", + "integrity": "sha512-kehYuigMoRkIngCv7rhgruLJNNHDnitGTBdkcYbCbooL8Cidj/bS78MDxByIjcc69M915WxcQTgZetZ1JbeQTQ==", "license": "ISC", "dependencies": { "b4a": "^1.0.1", @@ -13366,6 +6871,8 @@ }, "node_modules/sha512-wasm": { "version": "2.3.4", + "resolved": "https://registry.npmjs.org/sha512-wasm/-/sha512-wasm-2.3.4.tgz", + "integrity": "sha512-akWoxJPGCB3aZCrZ+fm6VIFhJ/p8idBv7AWGFng/CZIrQo51oQNsvDbTSRXWAzIiZJvpy16oIDiCCPqTe21sKg==", "license": "ISC", "dependencies": { "b4a": "^1.0.1", @@ -13374,9 +6881,10 @@ }, "node_modules/sharp": { "version": "0.33.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", + "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", "hasInstallScript": true, "license": "Apache-2.0", - "optional": true, "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", @@ -13412,6 +6920,8 @@ }, "node_modules/shebang-command": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -13422,19 +6932,17 @@ }, "node_modules/shebang-regex": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/shellsubstitute": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shellsubstitute/-/shellsubstitute-1.2.0.tgz", - "integrity": "sha512-CI1ViFC5a3ub86aaBmBVQ7kqg8eFypZLgBh+Bmq+ehHy9g7vu9kqCj5hS82cPzLwfdJRgiPB2hNHnd6oetiakQ==", - "license": "ISC" - }, "node_modules/side-channel": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -13452,6 +6960,8 @@ }, "node_modules/side-channel-list": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -13466,6 +6976,8 @@ }, "node_modules/side-channel-map": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -13482,6 +6994,8 @@ }, "node_modules/side-channel-weakmap": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -13499,6 +7013,8 @@ }, "node_modules/signal-exit": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "license": "ISC", "engines": { "node": ">=14" @@ -13507,70 +7023,10 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/simple-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/simple-get": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", - "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "decompress-response": "^6.0.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - } - }, - "node_modules/simple-git": { - "version": "3.36.0", - "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.36.0.tgz", - "integrity": "sha512-cGQjLjK8bxJw4QuYT7gxHw3/IouVESbhahSsHrX97MzCL1gu2u7oy38W6L2ZIGECEfIBG4BabsWDPjBxJENv9Q==", - "license": "MIT", - "dependencies": { - "@kwsites/file-exists": "^1.1.1", - "@kwsites/promise-deferred": "^1.1.1", - "@simple-git/args-pathspec": "^1.0.3", - "@simple-git/argv-parser": "^1.1.0", - "debug": "^4.4.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/steveukx/git-js?sponsor=1" - } - }, "node_modules/simple-swizzle": { "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", "license": "MIT", "dependencies": { "is-arrayish": "^0.3.1" @@ -13578,190 +7034,42 @@ }, "node_modules/simple-swizzle/node_modules/is-arrayish": { "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", "license": "MIT" }, - "node_modules/simple-update-notifier": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", - "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/siphash24": { "version": "1.3.1", + "resolved": "https://registry.npmjs.org/siphash24/-/siphash24-1.3.1.tgz", + "integrity": "sha512-moemC3ZKiTzH29nbFo3Iw8fbemWWod4vNs/WgKbQ54oEs6mE6XVlguxvinYjB+UmaE0PThgyED9fUkWvirT8hA==", "license": "MIT", "dependencies": { "nanoassert": "^2.0.0" } }, - "node_modules/sleep-promise": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/sleep-promise/-/sleep-promise-9.1.0.tgz", - "integrity": "sha512-UHYzVpz9Xn8b+jikYSD6bqvf754xL2uBUzDFwiU6NcdZeifPr6UfgU43xpkPu67VMS88+TI2PSI7Eohgqf2fKA==", - "license": "MIT" - }, - "node_modules/slice-ansi": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-8.0.0.tgz", - "integrity": "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.3", - "is-fullwidth-code-point": "^5.1.0" - }, + "node_modules/slide": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz", + "integrity": "sha512-NwrtjCg+lZoqhFU8fOwl4ay2ei8PaqCBOUV3/ektPY9trO1yQ1oXEfmHAhKArUVUr/hOHvy5f6AdP17dCM0zMw==", + "license": "ISC", "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, - "node_modules/slice-ansi/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/slice-ansi/node_modules/is-fullwidth-code-point": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", - "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", - "license": "MIT", - "dependencies": { - "get-east-asian-width": "^1.3.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": "*" } }, "node_modules/smart-buffer": { "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", "license": "MIT", "engines": { "node": ">= 6.0.0", "npm": ">= 3.0.0" } }, - "node_modules/snapdragon": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", - "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", - "license": "MIT", - "dependencies": { - "base": "^0.11.1", - "debug": "^2.2.0", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "map-cache": "^0.2.2", - "source-map": "^0.5.6", - "source-map-resolve": "^0.5.0", - "use": "^3.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon-node": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", - "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", - "license": "MIT", - "dependencies": { - "define-property": "^1.0.0", - "isobject": "^3.0.0", - "snapdragon-util": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon-node/node_modules/define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", - "license": "MIT", - "dependencies": { - "is-descriptor": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon-node/node_modules/isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon-util": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", - "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", - "license": "MIT", - "dependencies": { - "kind-of": "^3.2.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon/node_modules/define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", - "license": "MIT", - "dependencies": { - "is-descriptor": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon/node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", - "license": "MIT", - "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon/node_modules/is-descriptor": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", - "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", - "license": "MIT", - "dependencies": { - "is-accessor-descriptor": "^1.0.1", - "is-data-descriptor": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/socks": { "version": "2.8.4", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.4.tgz", + "integrity": "sha512-D3YaD0aRxR3mEcqnidIs7ReYJFVzWdd6fXJYUM8ixcQcJRGTka/b3saV0KflYhyVJXKhb947GndU35SxYNResQ==", "license": "MIT", "dependencies": { "ip-address": "^9.0.5", @@ -13774,6 +7082,8 @@ }, "node_modules/sodium-browserify": { "version": "1.3.0", + "resolved": "https://registry.npmjs.org/sodium-browserify/-/sodium-browserify-1.3.0.tgz", + "integrity": "sha512-1KRS6Oew3X13AIZhbmGF0YBdt2pQdafJMfv83OZHWbzxG92YBBnN8HYx/VKmYB4xCe90eidNaDJWBEFw/o3ahw==", "license": "MIT", "dependencies": { "libsodium-wrappers": "^0.7.4", @@ -13784,6 +7094,8 @@ }, "node_modules/sodium-browserify-tweetnacl": { "version": "0.2.6", + "resolved": "https://registry.npmjs.org/sodium-browserify-tweetnacl/-/sodium-browserify-tweetnacl-0.2.6.tgz", + "integrity": "sha512-ZnEI26hdluilpYY28Xc4rc1ALfmEp2TWihkJX6Mdtw0z9RfHfpZJU7P8DoKbN1HcBdU9aJmguFZs7igE8nLJPg==", "license": "MIT", "dependencies": { "chloride-test": "^1.1.0", @@ -13795,6 +7107,8 @@ }, "node_modules/sodium-browserify-tweetnacl/node_modules/sha.js": { "version": "2.4.12", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", + "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", "license": "(MIT AND BSD-3-Clause)", "dependencies": { "inherits": "^2.0.4", @@ -13813,14 +7127,20 @@ }, "node_modules/sodium-browserify-tweetnacl/node_modules/tweetnacl": { "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", + "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==", "license": "Unlicense" }, "node_modules/sodium-chloride": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sodium-chloride/-/sodium-chloride-1.1.2.tgz", + "integrity": "sha512-8AVzr9VHueXqfzfkzUA0aXe/Q4XG3UTmhlP6Pt+HQc5bbAPIJFo7ZIMh9tvn+99QuiMcyDJdYumegGAczl0N+g==", "license": "MIT" }, "node_modules/sodium-javascript": { "version": "0.8.0", + "resolved": "https://registry.npmjs.org/sodium-javascript/-/sodium-javascript-0.8.0.tgz", + "integrity": "sha512-rEBzR5mPxPES+UjyMDvKPIXy9ImF17KOJ32nJNi9uIquWpS/nfj+h6m05J5yLJaGXjgM72LmQoUbWZVxh/rmGg==", "license": "MIT", "dependencies": { "blake2b": "^2.1.1", @@ -13834,6 +7154,8 @@ }, "node_modules/sodium-native": { "version": "3.4.1", + "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-3.4.1.tgz", + "integrity": "sha512-PaNN/roiFWzVVTL6OqjzYct38NSXewdl2wz8SRB51Br/MLIJPrbM3XexhVWkq7D3UWMysfrhKVf1v1phZq6MeQ==", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -13842,6 +7164,8 @@ }, "node_modules/sodium-universal": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/sodium-universal/-/sodium-universal-3.1.0.tgz", + "integrity": "sha512-N2gxk68Kg2qZLSJ4h0NffEhp4BjgWHCHXVlDi1aG1hA3y+ZeWEmHqnpml8Hy47QzfL1xLy5nwr9LcsWAg2Ep0A==", "license": "MIT", "dependencies": { "blake2b": "^2.1.1", @@ -13856,15 +7180,6 @@ "xsalsa20": "^1.0.0" } }, - "node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -13874,88 +7189,16 @@ "node": ">=0.10.0" } }, - "node_modules/source-map-resolve": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", - "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", - "deprecated": "See https://github.com/lydell/source-map-resolve#deprecated", - "license": "MIT", - "dependencies": { - "atob": "^2.1.2", - "decode-uri-component": "^0.2.0", - "resolve-url": "^0.2.1", - "source-map-url": "^0.4.0", - "urix": "^0.1.0" - } - }, - "node_modules/source-map-url": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.1.tgz", - "integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==", - "deprecated": "See https://github.com/lydell/source-map-url#deprecated", - "license": "MIT" - }, - "node_modules/spdx-correct": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", - "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-exceptions": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", - "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", - "dev": true, - "license": "CC-BY-3.0" - }, - "node_modules/spdx-expression-parse": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-license-ids": { - "version": "3.0.23", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.23.tgz", - "integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==", - "dev": true, - "license": "CC0-1.0" - }, "node_modules/split-buffer": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/split-buffer/-/split-buffer-1.0.0.tgz", + "integrity": "sha512-cbHOF413jAsBL7JLpU1RWX07Xs+dPb25DbeCvwDzSN3+4bNmRPJa2QHpDdtREgGKgIfx6Tzj/jiqyOS3kNLPnQ==", "license": "MIT" }, - "node_modules/split-string": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", - "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", - "license": "MIT", - "dependencies": { - "extend-shallow": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ssb-about": { - "version": "2.0.1", - "license": "MIT", - "dependencies": { - "ssb-social-index": "^1.0.0" - } - }, "node_modules/ssb-about-self": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ssb-about-self/-/ssb-about-self-1.1.0.tgz", + "integrity": "sha512-AG07Dg2XG9CJvK52qrpwz54cXu+G6sPKwPiOb2LX5K+ORGhA+4MJUVViwuMEcOCI1a1wmXvOKFJbeTaFtosSAw==", "license": "LGPL-3.0", "dependencies": { "bipf": "^1.6.3", @@ -13974,24 +7217,18 @@ }, "node_modules/ssb-autofollow": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ssb-autofollow/-/ssb-autofollow-1.1.0.tgz", + "integrity": "sha512-2YBiFhVB9vKSN4pxGg11xwGpP4eVRWf5H3YthKodIfP84/IB/sUNFa+Z7VuL7IAczB3v3DQSWSTc+nK4WOc7ww==", "license": "MIT", "dependencies": { "debug": "^3.1.0", "pull-stream": "^3.6.9" } }, - "node_modules/ssb-backlinks": { - "version": "2.1.1", - "license": "MIT", - "dependencies": { - "base64-url": "^2.3.3", - "flumeview-links": "^2.1.0", - "pull-stream": "^3.6.14", - "ssb-ref": "^2.14.0" - } - }, "node_modules/ssb-bendy-butt": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/ssb-bendy-butt/-/ssb-bendy-butt-1.0.2.tgz", + "integrity": "sha512-PGIfYKLcpyKAGO6AS3BHctjxL58xK/DN6qmDBYVlxxqT+DI+482XYBCNKGtYfnCUlyBK+qO7lmbJp/DQhR8jmQ==", "license": "LGPL-3.0", "dependencies": { "bencode": "^2.0.1", @@ -14002,6 +7239,8 @@ }, "node_modules/ssb-bfe": { "version": "3.7.0", + "resolved": "https://registry.npmjs.org/ssb-bfe/-/ssb-bfe-3.7.0.tgz", + "integrity": "sha512-uJVMPAQhnm8Vz+5H7mP36kGhu0ZIloiDkNrkiO/xGWcr8gMv+GXX2DyNNPD7j0xZmL7HfFqgTvPXFeuni2w4IQ==", "license": "LGPL-3.0", "dependencies": { "is-canonical-base64": "^1.1.1", @@ -14012,10 +7251,14 @@ }, "node_modules/ssb-bfe-spec": { "version": "0.8.0", + "resolved": "https://registry.npmjs.org/ssb-bfe-spec/-/ssb-bfe-spec-0.8.0.tgz", + "integrity": "sha512-0NuyOS0CK1LAoJY+c3d9VmhGFuT1ylFKp+3T7Cttgq7LHhh0zMYZdzeJCUtTYgjsW/grvwRzUuNtYffJSYT0aA==", "license": "CC0-1.0" }, "node_modules/ssb-blobs": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ssb-blobs/-/ssb-blobs-2.0.1.tgz", + "integrity": "sha512-WuF96Yc8bd0l62wGxpQNT2OGf2XEhdMLEYsWJeXilYtEJLW+IZoO4YUjPanmp4fkhU2KunN+a/cQr6vqDqLphA==", "license": "MIT", "dependencies": { "level": "^6.0.0", @@ -14030,6 +7273,8 @@ }, "node_modules/ssb-box": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ssb-box/-/ssb-box-1.0.1.tgz", + "integrity": "sha512-/lZOqFr5glmzKTmpXZ8/QbvYA6/mVQ6dDPT/+zsYQkKKEHaloMkMPRRXlK86xG+/Bsuw2HGjcm78W09aWshS+g==", "license": "LGPL-3.0", "dependencies": { "chloride": "^2.4.1", @@ -14043,6 +7288,8 @@ }, "node_modules/ssb-box2": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ssb-box2/-/ssb-box2-3.0.1.tgz", + "integrity": "sha512-jQhrsEyrpqnUciEb1qzqc/SJCAx3hTm48BMzMy1bE//xQthWRahigTffflOM7pdRieGlxwGlHc8PpagBBZOkhA==", "license": "LGPL-3.0-only", "dependencies": { "envelope-js": "^1.3.0", @@ -14056,6 +7303,8 @@ }, "node_modules/ssb-buttwoo": { "version": "0.3.3", + "resolved": "https://registry.npmjs.org/ssb-buttwoo/-/ssb-buttwoo-0.3.3.tgz", + "integrity": "sha512-TA4xIs+4abz8Zn40LfrM2qM/hkpn5gYmy76yFGo9KkbDYoiVc5+Wk589yUf1ahcCzfDZN/lltRgBUzyeEY36uw==", "license": "LGPL-3.0", "dependencies": { "base64-url": "^2.3.3", @@ -14070,10 +7319,14 @@ }, "node_modules/ssb-caps": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ssb-caps/-/ssb-caps-1.1.0.tgz", + "integrity": "sha512-qe3qpvchJ+gnH8M/ge4rpL+7eRbSmsEAzNwHkDdrW06OBcziQ6/KuAdmcR6joxCbNeoAXAZF+inkefgE16okXA==", "license": "CC0-1.0" }, "node_modules/ssb-classic": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ssb-classic/-/ssb-classic-1.1.0.tgz", + "integrity": "sha512-aR1nifEXIpyNP+tRmQ8Sw2ra9nhPGrJnhH6xGTmSn4GXSwy5Gm4JCrpzcKvzUKzKMsfmqw5HVug+2/IVkvEVXQ==", "license": "LGPL-3.0", "dependencies": { "bipf": "^1.9.0", @@ -14087,6 +7340,8 @@ }, "node_modules/ssb-client": { "version": "4.9.0", + "resolved": "https://registry.npmjs.org/ssb-client/-/ssb-client-4.9.0.tgz", + "integrity": "sha512-5wPrVA82Uk3deV2oiCZEbm1kMp9r/5xTT2VvwwMLZS3fDD9FnLOi0kjUUh3OTP+jGBK7bVHRmnvXIVy6ETnSpA==", "license": "MIT", "dependencies": { "explain-error": "^1.0.1", @@ -14101,6 +7356,8 @@ }, "node_modules/ssb-client/node_modules/mkdirp": { "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "license": "MIT", "dependencies": { "minimist": "^1.2.6" @@ -14111,6 +7368,8 @@ }, "node_modules/ssb-client/node_modules/muxrpc": { "version": "6.7.3", + "resolved": "https://registry.npmjs.org/muxrpc/-/muxrpc-6.7.3.tgz", + "integrity": "sha512-yVr66BigJxHIvHOLsdJzuvrHt46Vq9DiDCBTZiS6q+49mXh4mkDjRiXo9FnGXQv1WJOqnRJk5fcfwV6TaSgNvA==", "license": "MIT", "dependencies": { "clarify-error": "^1.0.0", @@ -14123,6 +7382,8 @@ }, "node_modules/ssb-client/node_modules/ssb-keys": { "version": "7.2.2", + "resolved": "https://registry.npmjs.org/ssb-keys/-/ssb-keys-7.2.2.tgz", + "integrity": "sha512-FPeyYU/3LpxcagnbmVWE+Q/qzg6keqeOBPbD7sEH9UKixUASeufPKiORDgh8nVX7J9Z+0vUaHt/WG999kGjvVQ==", "license": "MIT", "dependencies": { "chloride": "^2.2.8", @@ -14132,6 +7393,8 @@ }, "node_modules/ssb-config": { "version": "3.4.6", + "resolved": "https://registry.npmjs.org/ssb-config/-/ssb-config-3.4.6.tgz", + "integrity": "sha512-arKSuP3S5qlsqY7w5g0VGCidi7oullM5KBEqy/0M9OgedN1uW5SKyIgEjCE85PbkHBfCpAcCNps4exZzP317FA==", "license": "MIT", "dependencies": { "deep-extend": "^0.6.0", @@ -14145,6 +7408,8 @@ }, "node_modules/ssb-conn": { "version": "6.0.3", + "resolved": "https://registry.npmjs.org/ssb-conn/-/ssb-conn-6.0.3.tgz", + "integrity": "sha512-XPZgTlLlfetsYqRtJlO0+tUE2OBGFf7BrdUkNPmSbqXHNZK1NDFPgohMYevywBt08xdTWfbSyljIsZs1buId3g==", "license": "MIT", "dependencies": { "debug": "^4.3.1", @@ -14172,6 +7437,8 @@ }, "node_modules/ssb-conn-db": { "version": "1.0.5", + "resolved": "https://registry.npmjs.org/ssb-conn-db/-/ssb-conn-db-1.0.5.tgz", + "integrity": "sha512-2rbqjvlDMTke2/qgauA415QSQ4hUJziXHqcXuC2Yh1uPrCDXHXrp0dhCchn0b/taEeL0Ppqo/Tsy6szQdqlEDw==", "license": "MIT", "dependencies": { "atomic-file-rw": "^0.3.0", @@ -14183,6 +7450,8 @@ }, "node_modules/ssb-conn-hub": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ssb-conn-hub/-/ssb-conn-hub-1.2.1.tgz", + "integrity": "sha512-EazMSgPJSOZTp/NdhUOq4kduAliT9k0GNJvao3Muvqk4Y17V4yIwCKt8SENHI5BMz2ATrDmFLj9uf5z/83Txww==", "license": "MIT", "dependencies": { "debug": "^4.3.1", @@ -14198,6 +7467,8 @@ }, "node_modules/ssb-conn-query": { "version": "1.2.2", + "resolved": "https://registry.npmjs.org/ssb-conn-query/-/ssb-conn-query-1.2.2.tgz", + "integrity": "sha512-pvCU8oxpX9sBap6oBrLUwDGAYnHSxkhYAYnj6RxGNotPKDIRZUBZB04yQ4nA1rHm7oHOnBgcuK0GRWls78pnkQ==", "license": "MIT", "dependencies": { "ssb-conn-db": "~1.0.3", @@ -14207,6 +7478,8 @@ }, "node_modules/ssb-conn-staging": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ssb-conn-staging/-/ssb-conn-staging-1.0.0.tgz", + "integrity": "sha512-NOy1qZoBkhH0XNzLteaaKKePXigEJSCntD4RPRU6vWLMlQ10+SzlIz4TWKgvEVx0LfCe6tEx/vhQf6vBWO/Ylw==", "license": "MIT", "dependencies": { "debug": "^4.1.1", @@ -14216,54 +7489,10 @@ "pull-stream": "^3.6.9" } }, - "node_modules/ssb-db": { - "version": "20.4.1", - "license": "MIT", - "dependencies": { - "flumedb": "^2.1.8", - "flumelog-offset": "^3.4.2", - "flumeview-level": "^4.0.4", - "flumeview-reduce": "^1.4.0", - "hashlru": "^2.3.0", - "lodash.clonedeep": "^4.5.0", - "ltgt": "^2.2.0", - "mkdirp": "^1.0.4", - "monotonic-timestamp": "~0.0.8", - "muxrpc-validation": "^3.0.0", - "obv": "0.0.1", - "pull-cat": "^1.1.11", - "pull-cont": "^0.1.1", - "pull-notify": "^0.1.1", - "pull-stream": "^3.4.0", - "rimraf": "^3.0.0", - "ssb-keys": "^7.1.3", - "ssb-msgs": "^5.0.0", - "ssb-ref": "^2.14.0", - "ssb-validate": "^4.0.0", - "zerr": "^1.0.0" - } - }, - "node_modules/ssb-db/node_modules/ssb-keys": { - "version": "7.2.2", - "license": "MIT", - "dependencies": { - "chloride": "^2.2.8", - "mkdirp": "~0.5.0", - "private-box": "^0.3.0" - } - }, - "node_modules/ssb-db/node_modules/ssb-keys/node_modules/mkdirp": { - "version": "0.5.6", - "license": "MIT", - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, "node_modules/ssb-db2": { "version": "6.3.3", + "resolved": "https://registry.npmjs.org/ssb-db2/-/ssb-db2-6.3.3.tgz", + "integrity": "sha512-H5WztPw1lJmUR1e12McRABT4OStiwygDutirHhx1P520ssu5pTmuKrcUM/uSaEPbEPsLif17HpdKKd1fK2uxYg==", "license": "LGPL-3.0", "dependencies": { "async-append-only-log": "^4.3.7", @@ -14306,6 +7535,8 @@ }, "node_modules/ssb-db2/node_modules/flumecodec": { "version": "0.0.1", + "resolved": "https://registry.npmjs.org/flumecodec/-/flumecodec-0.0.1.tgz", + "integrity": "sha512-JT0xivzdV7uTucjsLMw6JhK2e1K5TmU4fGmoQqnrJbydgY/V6+m71QoxX5ZtRR1pKoD48uhPDWWE6G5MlNoGkg==", "license": "MIT", "dependencies": { "level-codec": "^6.2.0" @@ -14313,18 +7544,14 @@ }, "node_modules/ssb-db2/node_modules/flumecodec/node_modules/level-codec": { "version": "6.2.0", + "resolved": "https://registry.npmjs.org/level-codec/-/level-codec-6.2.0.tgz", + "integrity": "sha512-J437PvCMZZKNT88+VRh6dkmh1ndZzwGwDzb5ZZl3QEsl+U9wIlt8hG+Y1gXVOhH75gf8JyceKGiG6WFUBbxyGQ==", "license": "MIT" }, - "node_modules/ssb-device-address": { - "version": "1.1.6", - "license": "MIT", - "dependencies": { - "pull-stream": "^3.6.9", - "ssb-ref": "^2.13.3" - } - }, "node_modules/ssb-ebt": { "version": "9.1.2", + "resolved": "https://registry.npmjs.org/ssb-ebt/-/ssb-ebt-9.1.2.tgz", + "integrity": "sha512-nNR26/3da8e+39fARLFDhrNdbkycxvmXqoK7QxrHGJaY/opBGtZ7GaHjNKkaXcF4Fl04ee+NhkYaiq18pGI0RQ==", "license": "MIT", "dependencies": { "base64-url": "^2.2.0", @@ -14343,56 +7570,10 @@ "node": ">=10" } }, - "node_modules/ssb-feed": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/ssb-feed/-/ssb-feed-2.3.0.tgz", - "integrity": "sha512-PKdrDl+KpqpLmIAJA/l7+8njHsDkJ8siDnMDxcGJhr63uwdib74H9BMaV9qbW9kRI7S0UJHvuPcre0pB3tcxgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "cont": "~1.0.3", - "monotonic-timestamp": "~0.0.9", - "pull-stream": "^3.4.2", - "ssb-keys": "^7.0.0", - "ssb-ref": "^2.0.0" - } - }, - "node_modules/ssb-feed/node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dev": true, - "license": "MIT", - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/ssb-feed/node_modules/ssb-keys": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/ssb-keys/-/ssb-keys-7.2.2.tgz", - "integrity": "sha512-FPeyYU/3LpxcagnbmVWE+Q/qzg6keqeOBPbD7sEH9UKixUASeufPKiORDgh8nVX7J9Z+0vUaHt/WG999kGjvVQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "chloride": "^2.2.8", - "mkdirp": "~0.5.0", - "private-box": "^0.3.0" - } - }, - "node_modules/ssb-friend-pub": { - "version": "1.0.7", - "license": "Beerware", - "dependencies": { - "pull-stream": "^3.6.9", - "ssb-ref": "^2.13.9", - "ssb-sort": "^1.1.3" - } - }, "node_modules/ssb-friends": { "version": "5.1.7", + "resolved": "https://registry.npmjs.org/ssb-friends/-/ssb-friends-5.1.7.tgz", + "integrity": "sha512-oBoMmcw5URa16CXM7LV1YG+YAGdwsQHkHevkh1VVKqhxxi8UAQ1qseb0iQ9Xdzwgadh9lRaW/aGz+175xrcxig==", "license": "MIT", "dependencies": { "bipf": "^1.5.1", @@ -14414,6 +7595,8 @@ }, "node_modules/ssb-friends/node_modules/flumecodec": { "version": "0.0.1", + "resolved": "https://registry.npmjs.org/flumecodec/-/flumecodec-0.0.1.tgz", + "integrity": "sha512-JT0xivzdV7uTucjsLMw6JhK2e1K5TmU4fGmoQqnrJbydgY/V6+m71QoxX5ZtRR1pKoD48uhPDWWE6G5MlNoGkg==", "license": "MIT", "dependencies": { "level-codec": "^6.2.0" @@ -14421,43 +7604,14 @@ }, "node_modules/ssb-friends/node_modules/level-codec": { "version": "6.2.0", - "license": "MIT" - }, - "node_modules/ssb-generate": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ssb-generate/-/ssb-generate-1.0.1.tgz", - "integrity": "sha512-Ube4EVI2QCHLwtxPwi3julUGW1j45oZFCA/keST5JtWLAmt7Tegq7VWwlTHxXRInTZKcT8Q6AJF+KtipR+FC1A==", - "dev": true, - "license": "MIT", - "dependencies": { - "pull-paramap": "^1.2.2", - "pull-stream": "^3.6.0" - } - }, - "node_modules/ssb-gossip": { - "version": "1.1.1", - "license": "MIT", - "dependencies": { - "atomic-file": "^1.1.5", - "deep-equal": "^1.0.1", - "has-network": "0.0.1", - "ip": "^1.1.5", - "muxrpc-validation": "^3.0.0", - "on-change-network": "0.0.2", - "on-wakeup": "^1.0.1", - "pull-notify": "^0.1.1", - "pull-ping": "^2.0.2", - "pull-stream": "^3.6.9", - "ssb-ref": "^2.13.9", - "statistics": "^3.3.0" - } - }, - "node_modules/ssb-gossip/node_modules/atomic-file": { - "version": "1.1.5", + "resolved": "https://registry.npmjs.org/level-codec/-/level-codec-6.2.0.tgz", + "integrity": "sha512-J437PvCMZZKNT88+VRh6dkmh1ndZzwGwDzb5ZZl3QEsl+U9wIlt8hG+Y1gXVOhH75gf8JyceKGiG6WFUBbxyGQ==", "license": "MIT" }, "node_modules/ssb-index-feeds": { "version": "0.10.2", + "resolved": "https://registry.npmjs.org/ssb-index-feeds/-/ssb-index-feeds-0.10.2.tgz", + "integrity": "sha512-j4j24OeD5CMGWcfjlk9ZUiQda6Obx/1z4X/SjYmBJBsX6mjVLJ0Kz0MbO607oVCls7AXdlgBCs6UsCGoO/VJiA==", "license": "LGPL-3.0", "dependencies": { "debug": "^4.3.2", @@ -14474,6 +7628,8 @@ }, "node_modules/ssb-invite": { "version": "3.0.3", + "resolved": "https://registry.npmjs.org/ssb-invite/-/ssb-invite-3.0.3.tgz", + "integrity": "sha512-zf0i9SL1phZJjXQE0poeguw+WOl3YdHV+Ajqnwka2AbDXaounA9RpoSl/SsxgLnCF/p8lOhNGMGU5Nkm/t7gVA==", "license": "MIT", "dependencies": { "cont": "^1.0.3", @@ -14488,6 +7644,8 @@ }, "node_modules/ssb-invite-client": { "version": "1.3.3", + "resolved": "https://registry.npmjs.org/ssb-invite-client/-/ssb-invite-client-1.3.3.tgz", + "integrity": "sha512-JBj5GxtWAquFp9L4TISSmCq1bsTEwpp5KUeALFuRBWuG+y6Nib55qaLJz8Q6r+HbxiVz3Soq68RaG5uW9FCcrQ==", "license": "MIT", "dependencies": { "clarify-error": "^1.0.0", @@ -14501,6 +7659,8 @@ }, "node_modules/ssb-keyring": { "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ssb-keyring/-/ssb-keyring-2.2.0.tgz", + "integrity": "sha512-pXNzYsQbtdWoWAjEPT5lVrZs/Px4ypJU2m4S34tTN6KivZukHPcflj1/IbZSDj7kHJGQhJRcyqGQ3MxgQdFBrA==", "license": "LGPL-3.0", "dependencies": { "charwise": "^3.0.1", @@ -14516,6 +7676,8 @@ }, "node_modules/ssb-keyring/node_modules/ssb-private-group-keys": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/ssb-private-group-keys/-/ssb-private-group-keys-1.1.2.tgz", + "integrity": "sha512-0UPPmxy61qmbDmP71J2vhX6UPfCtXa/CNehxYTgk2+AaLXsnA0perGZAiOWm9niGEU50TYYC5/jsIfjz4IiD9A==", "license": "LGPL-3.0-only", "dependencies": { "envelope-js": "^1.3.2", @@ -14527,6 +7689,8 @@ }, "node_modules/ssb-keys": { "version": "8.5.0", + "resolved": "https://registry.npmjs.org/ssb-keys/-/ssb-keys-8.5.0.tgz", + "integrity": "sha512-Fdgnz5QQ/oK/bMf5Hkaqss/INqiKHFHG4RKDk3StWrQC4fUKMSL/GfnkxFlcLFGhVomzxUmhtAnhJQx2WecEKQ==", "license": "MIT", "dependencies": { "chloride": "~2.4.1", @@ -14540,6 +7704,8 @@ }, "node_modules/ssb-keys/node_modules/mkdirp": { "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "license": "MIT", "dependencies": { "minimist": "^1.2.6" @@ -14550,6 +7716,8 @@ }, "node_modules/ssb-lan": { "version": "1.2.3", + "resolved": "https://registry.npmjs.org/ssb-lan/-/ssb-lan-1.2.3.tgz", + "integrity": "sha512-HJdc9oLIYO4u2eE9F3w8aXDP+idUiJ+ODcJKCKY8VKAlyn8psP4TC+Ol9VpT8NzFdOKQ5LnaAW9yThawfi2VUg==", "license": "MIT", "dependencies": { "debug": "^4.3.1", @@ -14565,237 +7733,19 @@ "secret-stack": ">=6.2.0" } }, - "node_modules/ssb-legacy-conn": { - "version": "1.0.25", - "license": "MIT", - "dependencies": { - "debug": "~4.1.1", - "has-network": "0.0.1", - "ip": "^1.1.5", - "multiserver-address": "~1.0.1", - "on-change-network": "0.0.2", - "on-wakeup": "^1.0.1", - "pull-notify": "^0.1.1", - "pull-ping": "^2.0.2", - "pull-stream": "^3.6.9", - "ssb-conn-db": "0.1.0", - "ssb-conn-hub": "0.0.3", - "ssb-conn-staging": "0.0.1", - "ssb-ref": "^2.13.9", - "statistics": "^3.3.0" - } - }, - "node_modules/ssb-legacy-conn/node_modules/atomic-file": { - "version": "1.1.5", - "license": "MIT" - }, - "node_modules/ssb-legacy-conn/node_modules/ssb-conn-db": { - "version": "0.1.0", - "license": "MIT", - "dependencies": { - "atomic-file": "^1.1.5", - "debug": "~4.1.1", - "multiserver-address": "~1.0.1", - "pull-notify": "~0.1.1", - "ssb-ref": "~2.13.9" - } - }, - "node_modules/ssb-legacy-conn/node_modules/ssb-conn-hub": { - "version": "0.0.3", - "license": "MIT", - "dependencies": { - "debug": "^4.1.1", - "multiserver-address": "~1.0.1", - "promisify-tuple": "^1.0.0", - "pull-notify": "~0.1.1", - "ssb-ref": "~2.13.9" - } - }, - "node_modules/ssb-legacy-conn/node_modules/ssb-conn-staging": { - "version": "0.0.1", - "license": "MIT", - "dependencies": { - "debug": "^4.1.1", - "multiserver-address": "~1.0.1", - "pull-notify": "~0.1.1", - "pull-stream": "^3.6.9", - "ssb-conn-hub": "~0.0.3" - } - }, - "node_modules/ssb-links": { - "version": "3.0.10", - "license": "MIT", - "dependencies": { - "flumeview-links": "^1.0.1", - "map-filter-reduce": "^2.0.0", - "ssb-msgs": "^5.2.0" - } - }, - "node_modules/ssb-links/node_modules/abstract-leveldown": { - "version": "6.0.3", - "license": "MIT", - "dependencies": { - "level-concat-iterator": "~2.0.0", - "xtend": "~4.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/ssb-links/node_modules/deep-equal": { - "version": "2.2.3", - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.0", - "call-bind": "^1.0.5", - "es-get-iterator": "^1.1.3", - "get-intrinsic": "^1.2.2", - "is-arguments": "^1.1.1", - "is-array-buffer": "^3.0.2", - "is-date-object": "^1.0.5", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "isarray": "^2.0.5", - "object-is": "^1.1.5", - "object-keys": "^1.1.1", - "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.5.1", - "side-channel": "^1.0.4", - "which-boxed-primitive": "^1.0.2", - "which-collection": "^1.0.1", - "which-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/ssb-links/node_modules/flumeview-level": { - "version": "3.0.14", - "license": "MIT", - "dependencies": { - "charwise": "^3.0.1", - "explain-error": "^1.0.4", - "level": "^5.0.0", - "ltgt": "^2.1.3", - "mkdirp": "^0.5.1", - "obv": "0.0.1", - "pull-level": "^2.0.3", - "pull-paramap": "^1.2.1", - "pull-stream": "^3.5.0", - "pull-write": "^1.1.1" - } - }, - "node_modules/ssb-links/node_modules/flumeview-links": { - "version": "1.0.1", - "license": "MIT", - "dependencies": { - "deep-equal": "^2.0.3", - "flumeview-level": "^3.0.14", - "map-filter-reduce": "^3.2.2", - "pull-flatmap": "0.0.1", - "pull-stream": "^3.6.14" - } - }, - "node_modules/ssb-links/node_modules/flumeview-links/node_modules/map-filter-reduce": { - "version": "3.2.2", - "license": "MIT", - "dependencies": { - "binary-search": "^1.2.0", - "compare-at-paths": "^1.0.0", - "pull-sink-through": "0.0.0", - "pull-sort": "^1.0.1", - "pull-stream": "^3.4.3", - "typewiselite": "^1.0.0" - } - }, - "node_modules/ssb-links/node_modules/immediate": { - "version": "3.2.3", - "license": "MIT" - }, - "node_modules/ssb-links/node_modules/level": { - "version": "5.0.1", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "level-js": "^4.0.0", - "level-packager": "^5.0.0", - "leveldown": "^5.0.0", - "opencollective-postinstall": "^2.0.0" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/ssb-links/node_modules/level-js": { - "version": "4.0.2", - "license": "MIT", - "dependencies": { - "abstract-leveldown": "~6.0.1", - "immediate": "~3.2.3", - "inherits": "^2.0.3", - "ltgt": "^2.1.2", - "typedarray-to-buffer": "~3.1.5" - } - }, - "node_modules/ssb-links/node_modules/map-filter-reduce": { - "version": "2.2.1", - "license": "MIT", - "dependencies": { - "binary-search": "^1.2.0", - "pull-sink-through": "0.0.0", - "pull-stream": "^3.3.0", - "typewiselite": "^1.0.0" - } - }, - "node_modules/ssb-links/node_modules/mkdirp": { - "version": "0.5.6", - "license": "MIT", - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/ssb-links/node_modules/typedarray-to-buffer": { - "version": "3.1.5", - "license": "MIT", - "dependencies": { - "is-typedarray": "^1.0.0" - } - }, - "node_modules/ssb-local": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/ssb-local/-/ssb-local-1.0.0.tgz", - "integrity": "sha512-aIkz+tDsAQFLbcN5d9itipHG71EG62UdIwojz78BQBxD7kGTwMezZofkMplURp9/njwdqMiqxYEUMvOh8Emh/w==", - "license": "MIT", - "dependencies": { - "broadcast-stream": "^0.2.2", - "ssb-ref": "^2.13.9" - } - }, "node_modules/ssb-logging": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ssb-logging/-/ssb-logging-1.0.0.tgz", + "integrity": "sha512-6apTG47+VgLAD3MYkpRTbO27DMDh0YLJIvWfcJUEo5FdrjnLsqDl1kkzQ4B5MbH08z5Vklx5907t4by9rhlROQ==", "license": "MIT", "dependencies": { "bash-color": "0.0.4" } }, - "node_modules/ssb-markdown": { - "version": "3.6.0", - "license": "MIT", - "dependencies": { - "emoji-named-characters": "^1.0.2", - "ssb-marked": "^0.7.3", - "ssb-msgs": "^5.2.0", - "ssb-ref": "^2.3.0" - } - }, "node_modules/ssb-marked": { "version": "0.7.4", + "resolved": "https://registry.npmjs.org/ssb-marked/-/ssb-marked-0.7.4.tgz", + "integrity": "sha512-7Dnl4iTmugLXchTXN5mVi1t9in/9GsKYMD9Pm0o1JKGftyWP9ztSTl6Lukix6FoNRfLJRhjGzfXj+Aw27TN4pQ==", "license": "MIT", "bin": { "ssb-marked": "bin/marked" @@ -14803,19 +7753,14 @@ }, "node_modules/ssb-master": { "version": "1.0.3", + "resolved": "https://registry.npmjs.org/ssb-master/-/ssb-master-1.0.3.tgz", + "integrity": "sha512-N1Cxm9WscGD9VEZrWbF2amyQai2U2g9gtq57W5zTqbhlQTLUUvl84U9A6fg6GPkECnUXadulnTw+mMYVkLLHjQ==", "license": "MIT" }, - "node_modules/ssb-meme": { - "version": "1.1.0", - "license": "MIT", - "dependencies": { - "flumeview-search": "^2.0.0", - "is-my-json-valid": "^2.17.2", - "ssb-ref": "^2.11.1" - } - }, "node_modules/ssb-mentions": { "version": "0.5.2", + "resolved": "https://registry.npmjs.org/ssb-mentions/-/ssb-mentions-0.5.2.tgz", + "integrity": "sha512-+1lslVCkfnmJOlswJluia+70K5tTLzwRxbLmluvV0rf2KW5T4DIXBPuAhB3vBsP26emAcTHEOj7QtD8iQ9VWtA==", "license": "MIT", "dependencies": { "lodash.isequal": "^4.5.0", @@ -14825,6 +7770,8 @@ }, "node_modules/ssb-meta-feeds": { "version": "0.39.0", + "resolved": "https://registry.npmjs.org/ssb-meta-feeds/-/ssb-meta-feeds-0.39.0.tgz", + "integrity": "sha512-vnfGA5FT0iHVMTE3Ore10qEi2Bpi2TqIiD4FvHbU+rdBOJyKKE0E3KwDFWhlqGDB5YwSt9YP364WbEjn1VTLQw==", "license": "LGPL-3.0", "dependencies": { "bencode": "^2.0.2", @@ -14850,10 +7797,14 @@ }, "node_modules/ssb-msg-content": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ssb-msg-content/-/ssb-msg-content-1.0.1.tgz", + "integrity": "sha512-M6W0Ef+jif829USmGvh6XeS4lYb/F2lgFhfEoCE/md7ESILNOGidp8frJE2uVOzSr2wVRA265tPrnVb7rYHkug==", "license": "MIT" }, "node_modules/ssb-msgs": { "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ssb-msgs/-/ssb-msgs-5.2.0.tgz", + "integrity": "sha512-a5SF3fnXhXhcEIT406QNPftf+9YbPWFYFABNudreB19RrE0N3JsJL7bm/eKWgVd0fXNpARZJF3f5BYYMUv6lFw==", "license": "MIT", "dependencies": { "ssb-ref": "^2.0.0" @@ -14861,10 +7812,14 @@ }, "node_modules/ssb-network-errors": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ssb-network-errors/-/ssb-network-errors-1.0.1.tgz", + "integrity": "sha512-Re6q7yZL4GreJXbfa7QWk24Nr/I7bs3cIszV2usiSqVYyDphkcwT8iOXFyqjWxG9gVA3Lfjj+6uwX2WI7SorPw==", "license": "LGPL-3.0" }, "node_modules/ssb-no-auth": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ssb-no-auth/-/ssb-no-auth-1.0.0.tgz", + "integrity": "sha512-+o9WnY566shLyiJp6l21+SHZfgv40SEr35PtoF3uuFYI24vFhgcfv77pXvWpDINVyX2uQ152o2eQHg1amlp6lQ==", "license": "MIT", "dependencies": { "multiserver": "^3.3.3" @@ -14872,183 +7827,17 @@ }, "node_modules/ssb-onion": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ssb-onion/-/ssb-onion-1.0.0.tgz", + "integrity": "sha512-iRXjeI0sbRbgMwaKg+poqhHSH7GvUVgJ+8ysi8q7Hjj4wHkczLGAOyVjnyhjF+ErPDFn9kXN+46joneKqSgEDg==", "license": "MIT", "dependencies": { "multiserver": "^3.3.3" } }, - "node_modules/ssb-ooo": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/ssb-ooo/-/ssb-ooo-1.3.3.tgz", - "integrity": "sha512-6yI5XBpBHkwFdAELpKwlf1C1V32Z0/AKEJYsoU7lk+Eh88blv8LKydo4a1DnjMiPy4ywn4lRU5oayQaPE5MRtQ==", - "license": "MIT", - "dependencies": { - "flumecodec": "0.0.1", - "flumelog-offset": "^3.4.3", - "flumeview-hashtable": "^1.1.1", - "gossip-query": "^2.0.2", - "mkdirp": "^0.5.1", - "pull-stream": "^3.6.14", - "ssb-keys": "^7.2.0", - "ssb-ref": "^2.13.3", - "ssb-validate": "^4.1.0" - } - }, - "node_modules/ssb-ooo/node_modules/flumecodec": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/flumecodec/-/flumecodec-0.0.1.tgz", - "integrity": "sha512-JT0xivzdV7uTucjsLMw6JhK2e1K5TmU4fGmoQqnrJbydgY/V6+m71QoxX5ZtRR1pKoD48uhPDWWE6G5MlNoGkg==", - "license": "MIT", - "dependencies": { - "level-codec": "^6.2.0" - } - }, - "node_modules/ssb-ooo/node_modules/level-codec": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/level-codec/-/level-codec-6.2.0.tgz", - "integrity": "sha512-J437PvCMZZKNT88+VRh6dkmh1ndZzwGwDzb5ZZl3QEsl+U9wIlt8hG+Y1gXVOhH75gf8JyceKGiG6WFUBbxyGQ==", - "deprecated": "Superseded by level-transcoder (https://github.com/Level/community#faq)", - "license": "MIT" - }, - "node_modules/ssb-ooo/node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "license": "MIT", - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/ssb-ooo/node_modules/ssb-keys": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/ssb-keys/-/ssb-keys-7.2.2.tgz", - "integrity": "sha512-FPeyYU/3LpxcagnbmVWE+Q/qzg6keqeOBPbD7sEH9UKixUASeufPKiORDgh8nVX7J9Z+0vUaHt/WG999kGjvVQ==", - "license": "MIT", - "dependencies": { - "chloride": "^2.2.8", - "mkdirp": "~0.5.0", - "private-box": "^0.3.0" - } - }, - "node_modules/ssb-partial-replication": { - "version": "3.0.1", - "license": "Beerware", - "dependencies": { - "pull-cont": "^0.1.1", - "pull-stream": "^3.6.14", - "ssb-sort": "^1.1.3" - } - }, - "node_modules/ssb-plugins": { - "version": "1.0.4", - "license": "MIT", - "dependencies": { - "cross-spawn": "^6.0.5", - "explain-error": "^1.0.4", - "mkdirp": "^0.5.1", - "muxrpc": "^6.4.6", - "muxrpc-validation": "^3.0.0", - "mv": "^2.1.1", - "osenv": "^0.1.5", - "pull-cat": "^1.1.11", - "pull-many": "^1.0.8", - "pull-pushable": "^2.2.0", - "pull-stream": "^3.6.9", - "stream-to-pull-stream": "^1.7.2" - } - }, - "node_modules/ssb-plugins/node_modules/cross-spawn": { - "version": "6.0.6", - "license": "MIT", - "dependencies": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - }, - "engines": { - "node": ">=4.8" - } - }, - "node_modules/ssb-plugins/node_modules/mkdirp": { - "version": "0.5.6", - "license": "MIT", - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/ssb-plugins/node_modules/muxrpc": { - "version": "6.7.3", - "license": "MIT", - "dependencies": { - "clarify-error": "^1.0.0", - "debug": "^4.3.3", - "packet-stream": "~2.0.0", - "packet-stream-codec": "^1.2.0", - "pull-goodbye": "0.0.3", - "pull-stream": "^3.6.10" - } - }, - "node_modules/ssb-plugins/node_modules/path-key": { - "version": "2.0.1", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/ssb-plugins/node_modules/semver": { - "version": "5.7.2", - "license": "ISC", - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/ssb-plugins/node_modules/shebang-command": { - "version": "1.2.0", - "license": "MIT", - "dependencies": { - "shebang-regex": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ssb-plugins/node_modules/shebang-regex": { - "version": "1.0.0", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ssb-plugins/node_modules/which": { - "version": "1.3.1", - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "which": "bin/which" - } - }, - "node_modules/ssb-private": { - "version": "1.1.0", - "license": "MIT", - "dependencies": { - "base64-url": "^2.2.0", - "explain-error": "^1.0.4", - "flumeview-links": "^2.0.1", - "ssb-keys": "^7.0.14" - } - }, "node_modules/ssb-private-group-keys": { "version": "0.4.1", + "resolved": "https://registry.npmjs.org/ssb-private-group-keys/-/ssb-private-group-keys-0.4.1.tgz", + "integrity": "sha512-Npl9NpUZvHho10h18/tphMfuyMlj+6lLW0mm+u8ooIRgVh5+hyoOtgJL22VmTEyD8KQVJG6SOjiv6MiGXzIL3g==", "license": "LGPL-3.0-only", "dependencies": { "envelope-js": "^1.1.2", @@ -15058,36 +7847,10 @@ "ssb-bfe": "^3.1.1" } }, - "node_modules/ssb-private/node_modules/mkdirp": { - "version": "0.5.6", - "license": "MIT", - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/ssb-private/node_modules/ssb-keys": { - "version": "7.2.2", - "license": "MIT", - "dependencies": { - "chloride": "^2.2.8", - "mkdirp": "~0.5.0", - "private-box": "^0.3.0" - } - }, - "node_modules/ssb-query": { - "version": "2.4.5", - "license": "MIT", - "dependencies": { - "explain-error": "^1.0.1", - "flumeview-query": "^8.0.0", - "pull-stream": "^3.6.2" - } - }, "node_modules/ssb-ref": { "version": "2.16.0", + "resolved": "https://registry.npmjs.org/ssb-ref/-/ssb-ref-2.16.0.tgz", + "integrity": "sha512-ylyrfz9NLxwTCbeDDAdLo++O3elhNs6/gUqMhZ22F+gSOIjwXy2X7dpg5Q1YTH7uALOSu307Rpo1UfK9sj7Sjw==", "license": "MIT", "dependencies": { "ip": "^1.1.3", @@ -15098,62 +7861,17 @@ }, "node_modules/ssb-ref/node_modules/is-valid-domain": { "version": "0.0.20", + "resolved": "https://registry.npmjs.org/is-valid-domain/-/is-valid-domain-0.0.20.tgz", + "integrity": "sha512-Yd9oD7sgCycVvH8CHy5U4fLXibPwxVw2+diudYbT8ZfAiQDtW1H9WvPRR4+rtN9qOll+r+KAfO4SjO28OPpitA==", "license": "MIT", "dependencies": { "punycode": "^1.4.1" } }, - "node_modules/ssb-replicate": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/ssb-replicate/-/ssb-replicate-1.3.5.tgz", - "integrity": "sha512-XJcsI8xMEpkEF4VF3eUd0edkakBEokakmIPLk2kR6ryNNYo72M+WPB6q+t2JSNkpGIaKFBJTCEMTkNJnLrM9EQ==", - "license": "MIT", - "dependencies": { - "deep-equal": "^2.2.3", - "observ-debounce": "^1.1.1", - "pull-cat": "^1.1.11", - "pull-next": "^1.0.1", - "pull-notify": "^0.1.2", - "pull-paramap": "^1.2.2", - "pull-pushable": "^2.2.0", - "pull-stream": "^3.7.0", - "ssb-ref": "^2.16.0" - } - }, - "node_modules/ssb-replicate/node_modules/deep-equal": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz", - "integrity": "sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==", - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.0", - "call-bind": "^1.0.5", - "es-get-iterator": "^1.1.3", - "get-intrinsic": "^1.2.2", - "is-arguments": "^1.1.1", - "is-array-buffer": "^3.0.2", - "is-date-object": "^1.0.5", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "isarray": "^2.0.5", - "object-is": "^1.1.5", - "object-keys": "^1.1.1", - "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.5.1", - "side-channel": "^1.0.4", - "which-boxed-primitive": "^1.0.2", - "which-collection": "^1.0.1", - "which-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/ssb-replication-scheduler": { "version": "3.1.1", + "resolved": "https://registry.npmjs.org/ssb-replication-scheduler/-/ssb-replication-scheduler-3.1.1.tgz", + "integrity": "sha512-mLGSs8RjY9LpyVa4uJ+FIFvlqv6A4aBb3YAKHziAY8IRhkbIQmS/HbHf5p2IKBSf9wo0/Ac+EzPDQdeO8hCCpA==", "license": "LGPL-3.0", "dependencies": { "debug": "^4.3.2", @@ -15174,6 +7892,8 @@ }, "node_modules/ssb-replication-scheduler/node_modules/ssb-box2": { "version": "7.5.0", + "resolved": "https://registry.npmjs.org/ssb-box2/-/ssb-box2-7.5.0.tgz", + "integrity": "sha512-cYhq4CdjHXlQCQZqqzbkBygD9Hyf5C86i4xHHNqoMW1FPS20m0uEjwTTpMFJOZfhXs/wgeCuAJ1h4UG1Es+slg==", "license": "LGPL-3.0-only", "dependencies": { "envelope-js": "^1.3.2", @@ -15188,6 +7908,8 @@ }, "node_modules/ssb-replication-scheduler/node_modules/ssb-keyring": { "version": "7.0.0", + "resolved": "https://registry.npmjs.org/ssb-keyring/-/ssb-keyring-7.0.0.tgz", + "integrity": "sha512-HBeAFCdjqSLCiors10s+AHfaFm78ac97oqeWgDg9Es4gK2LUs/ZQ1DjoYH+BS4EnTh5yE+Y9E8ska4WWc0p4Dw==", "license": "LGPL-3.0", "dependencies": { "charwise": "^3.0.1", @@ -15204,6 +7926,8 @@ }, "node_modules/ssb-replication-scheduler/node_modules/ssb-keyring/node_modules/private-group-spec": { "version": "8.1.0", + "resolved": "https://registry.npmjs.org/private-group-spec/-/private-group-spec-8.1.0.tgz", + "integrity": "sha512-vxChFtmkWW85VJjh3VmByB1zeOp40JZmJ+0YBCibxlxBHuaW5aFa6BZ/zamkH2sBtXAjwZnou4f5Ty7CUj3NvA==", "license": "LGPL-3.0-only", "dependencies": { "is-my-ssb-valid": "^1.2.0" @@ -15211,6 +7935,8 @@ }, "node_modules/ssb-replication-scheduler/node_modules/ssb-private-group-keys": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/ssb-private-group-keys/-/ssb-private-group-keys-1.1.2.tgz", + "integrity": "sha512-0UPPmxy61qmbDmP71J2vhX6UPfCtXa/CNehxYTgk2+AaLXsnA0perGZAiOWm9niGEU50TYYC5/jsIfjz4IiD9A==", "license": "LGPL-3.0-only", "dependencies": { "envelope-js": "^1.3.2", @@ -15222,37 +7948,17 @@ }, "node_modules/ssb-schema-definitions": { "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ssb-schema-definitions/-/ssb-schema-definitions-1.3.0.tgz", + "integrity": "sha512-UUNBnXpaISaM3djBDH8HB5RZtXBS+/ThehppCLQ6LJru7IMubu/hOPbD3EYahfidw13pvdCQXSuGkIeKIcOv8g==", "license": "MIT", "dependencies": { "ssb-ref": "^2.13.6" } }, - "node_modules/ssb-search": { - "version": "1.3.0", - "license": "MIT", - "dependencies": { - "flumeview-search": "^2.0.0", - "pull-cont": "^0.1.1", - "pull-stream": "^3.6.7", - "ssb-msgs": "^5.2.0" - } - }, - "node_modules/ssb-server": { - "resolved": "packages/ssb-server", - "link": true - }, - "node_modules/ssb-social-index": { - "version": "1.1.1", - "license": "MIT", - "dependencies": { - "bipf": "^1.4.0", - "pull-defer": "^0.2.3", - "pull-stream": "^3.6.9", - "ssb-ref": "^2.7.1" - } - }, "node_modules/ssb-sort": { "version": "1.1.3", + "resolved": "https://registry.npmjs.org/ssb-sort/-/ssb-sort-1.1.3.tgz", + "integrity": "sha512-oPsF8lGgcHcIb4F1GddV3CbZTJZ0OzxI9fHXH0Zc7ZjqjFlYdqMDxFSuvqJnmtDydJcswyGANiziP1ghd69jOw==", "license": "MIT", "dependencies": { "ssb-ref": "^2.3.0" @@ -15260,6 +7966,8 @@ }, "node_modules/ssb-subset-ql": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ssb-subset-ql/-/ssb-subset-ql-1.0.1.tgz", + "integrity": "sha512-msZA4Oc1rgEEAVLyzMiQNxJf85+XBrw7IEQLHQpMDv+o/p4GGX9Ygijc2RtItnWSu1H5fFcZChL8pf/q+j3q+w==", "license": "LGPL-3.0", "dependencies": { "nano-equal": "^2.0.2", @@ -15267,18 +7975,10 @@ "ssb-ref": "^2.13.0" } }, - "node_modules/ssb-tangle": { - "version": "1.0.1", - "license": "AGPL-3.0", - "dependencies": { - "is-my-json-valid": "^2.20.0", - "pull-stream": "^3.6.11", - "ssb-ref": "^2.13.9", - "ssb-sort": "^1.1.3" - } - }, "node_modules/ssb-thread-schema": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ssb-thread-schema/-/ssb-thread-schema-1.1.1.tgz", + "integrity": "sha512-93RcHfnev5vj7J5UZI47lu7l71bCevUdO80xUDZSgIPGQw+FrHo0LGSxDTfzNngJaJTlat5mEkRwrrY+wYoahg==", "license": "AGPL-3.0", "dependencies": { "is-my-json-valid": "^2.20.0", @@ -15286,26 +7986,10 @@ "ssb-schema-definitions": "^1.1.5" } }, - "node_modules/ssb-threads": { - "version": "10.8.0", - "license": "MIT", - "dependencies": { - "bipf": "^1.9.0", - "pull-cat": "^1.1.11", - "pull-level": "^2.0.4", - "pull-stream": "^3.6.2", - "secret-stack-decorators": "1.1.0", - "ssb-db2": ">=3.4.1", - "ssb-ref": "^2.13.0", - "ssb-sort": "1.1.x", - "ssb-typescript": "^2.5.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/ssb-tribes2": { "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ssb-tribes2/-/ssb-tribes2-1.4.0.tgz", + "integrity": "sha512-3Cv1HNUheBtTgfi4CobjJLd5s9xcz4d1pgNcyEUPK+mQOnD185ZkMHa/+RIbOjTSBDTlGgjt50Ymixbi+LpJtQ==", "license": "LGPL-3.0-only", "dependencies": { "@tangle/overwrite-fields": "^2.0.3", @@ -15345,6 +8029,8 @@ }, "node_modules/ssb-tribes2/node_modules/flumecodec": { "version": "0.0.1", + "resolved": "https://registry.npmjs.org/flumecodec/-/flumecodec-0.0.1.tgz", + "integrity": "sha512-JT0xivzdV7uTucjsLMw6JhK2e1K5TmU4fGmoQqnrJbydgY/V6+m71QoxX5ZtRR1pKoD48uhPDWWE6G5MlNoGkg==", "license": "MIT", "dependencies": { "level-codec": "^6.2.0" @@ -15352,10 +8038,14 @@ }, "node_modules/ssb-tribes2/node_modules/flumecodec/node_modules/level-codec": { "version": "6.2.0", + "resolved": "https://registry.npmjs.org/level-codec/-/level-codec-6.2.0.tgz", + "integrity": "sha512-J437PvCMZZKNT88+VRh6dkmh1ndZzwGwDzb5ZZl3QEsl+U9wIlt8hG+Y1gXVOhH75gf8JyceKGiG6WFUBbxyGQ==", "license": "MIT" }, "node_modules/ssb-tribes2/node_modules/private-group-spec": { "version": "8.1.0", + "resolved": "https://registry.npmjs.org/private-group-spec/-/private-group-spec-8.1.0.tgz", + "integrity": "sha512-vxChFtmkWW85VJjh3VmByB1zeOp40JZmJ+0YBCibxlxBHuaW5aFa6BZ/zamkH2sBtXAjwZnou4f5Ty7CUj3NvA==", "license": "LGPL-3.0-only", "dependencies": { "is-my-ssb-valid": "^1.2.0" @@ -15363,6 +8053,8 @@ }, "node_modules/ssb-tribes2/node_modules/ssb-box2": { "version": "7.5.0", + "resolved": "https://registry.npmjs.org/ssb-box2/-/ssb-box2-7.5.0.tgz", + "integrity": "sha512-cYhq4CdjHXlQCQZqqzbkBygD9Hyf5C86i4xHHNqoMW1FPS20m0uEjwTTpMFJOZfhXs/wgeCuAJ1h4UG1Es+slg==", "license": "LGPL-3.0-only", "dependencies": { "envelope-js": "^1.3.2", @@ -15377,6 +8069,8 @@ }, "node_modules/ssb-tribes2/node_modules/ssb-db2": { "version": "7.2.0", + "resolved": "https://registry.npmjs.org/ssb-db2/-/ssb-db2-7.2.0.tgz", + "integrity": "sha512-XxqzmccNgE2YmUZ+lXhw08BVlLEGA8LnIXBeeem4+084/fOPdBdGbXy8rb1G3UoiKVYFLVBc64LhbRQ/JfaS6w==", "license": "LGPL-3.0-only", "dependencies": { "async-append-only-log": "^4.3.7", @@ -15421,6 +8115,8 @@ }, "node_modules/ssb-tribes2/node_modules/ssb-db2/node_modules/private-group-spec": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/private-group-spec/-/private-group-spec-1.2.0.tgz", + "integrity": "sha512-O7SfG+vZIZgqDXy/wjsuTRI5LaozW4rxaZBpGmwlcDfjIvxvYWNboyNm1PoQUU6j4dQ02V1tOQVLDq9u2RzolA==", "license": "LGPL-3.0-only", "dependencies": { "is-my-ssb-valid": "^1.2.0" @@ -15428,6 +8124,8 @@ }, "node_modules/ssb-tribes2/node_modules/ssb-db2/node_modules/ssb-box2": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ssb-box2/-/ssb-box2-3.0.1.tgz", + "integrity": "sha512-jQhrsEyrpqnUciEb1qzqc/SJCAx3hTm48BMzMy1bE//xQthWRahigTffflOM7pdRieGlxwGlHc8PpagBBZOkhA==", "license": "LGPL-3.0-only", "dependencies": { "envelope-js": "^1.3.0", @@ -15441,6 +8139,8 @@ }, "node_modules/ssb-tribes2/node_modules/ssb-db2/node_modules/ssb-box2/node_modules/ssb-private-group-keys": { "version": "0.4.1", + "resolved": "https://registry.npmjs.org/ssb-private-group-keys/-/ssb-private-group-keys-0.4.1.tgz", + "integrity": "sha512-Npl9NpUZvHho10h18/tphMfuyMlj+6lLW0mm+u8ooIRgVh5+hyoOtgJL22VmTEyD8KQVJG6SOjiv6MiGXzIL3g==", "license": "LGPL-3.0-only", "dependencies": { "envelope-js": "^1.1.2", @@ -15452,6 +8152,8 @@ }, "node_modules/ssb-tribes2/node_modules/ssb-db2/node_modules/ssb-keyring": { "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ssb-keyring/-/ssb-keyring-2.2.0.tgz", + "integrity": "sha512-pXNzYsQbtdWoWAjEPT5lVrZs/Px4ypJU2m4S34tTN6KivZukHPcflj1/IbZSDj7kHJGQhJRcyqGQ3MxgQdFBrA==", "license": "LGPL-3.0", "dependencies": { "charwise": "^3.0.1", @@ -15467,6 +8169,8 @@ }, "node_modules/ssb-tribes2/node_modules/ssb-keyring": { "version": "7.0.0", + "resolved": "https://registry.npmjs.org/ssb-keyring/-/ssb-keyring-7.0.0.tgz", + "integrity": "sha512-HBeAFCdjqSLCiors10s+AHfaFm78ac97oqeWgDg9Es4gK2LUs/ZQ1DjoYH+BS4EnTh5yE+Y9E8ska4WWc0p4Dw==", "license": "LGPL-3.0", "dependencies": { "charwise": "^3.0.1", @@ -15483,6 +8187,8 @@ }, "node_modules/ssb-tribes2/node_modules/ssb-private-group-keys": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/ssb-private-group-keys/-/ssb-private-group-keys-1.1.2.tgz", + "integrity": "sha512-0UPPmxy61qmbDmP71J2vhX6UPfCtXa/CNehxYTgk2+AaLXsnA0perGZAiOWm9niGEU50TYYC5/jsIfjz4IiD9A==", "license": "LGPL-3.0-only", "dependencies": { "envelope-js": "^1.3.2", @@ -15494,6 +8200,8 @@ }, "node_modules/ssb-tribes2/node_modules/ssb-private-group-keys/node_modules/private-group-spec": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/private-group-spec/-/private-group-spec-1.2.0.tgz", + "integrity": "sha512-O7SfG+vZIZgqDXy/wjsuTRI5LaozW4rxaZBpGmwlcDfjIvxvYWNboyNm1PoQUU6j4dQ02V1tOQVLDq9u2RzolA==", "license": "LGPL-3.0-only", "dependencies": { "is-my-ssb-valid": "^1.2.0" @@ -15501,10 +8209,14 @@ }, "node_modules/ssb-typescript": { "version": "2.8.0", + "resolved": "https://registry.npmjs.org/ssb-typescript/-/ssb-typescript-2.8.0.tgz", + "integrity": "sha512-akqVsc2HNM0x5q4iN8v/t0jDuNOyU8E8UfyObqU4qWALIXLUptUQd1n03bWOcDn/+sjb2CLpjq584gDoQ4gEjw==", "license": "MIT" }, "node_modules/ssb-unix-socket": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ssb-unix-socket/-/ssb-unix-socket-1.0.0.tgz", + "integrity": "sha512-Z4jBj917W+dKAiDglwxCpWm8vINOMtkpHQIgk50NQTb5jHqHI5Rcyiy7EO0uRcWwRWqXi1ZwOTEFVyLyyuittA==", "license": "MIT", "dependencies": { "multiserver": "^3.3.3" @@ -15512,6 +8224,8 @@ }, "node_modules/ssb-uri2": { "version": "2.4.1", + "resolved": "https://registry.npmjs.org/ssb-uri2/-/ssb-uri2-2.4.1.tgz", + "integrity": "sha512-3Oia/V8Q7kWj2/Wcw67yKSkMcxQIAIHGDZ4CsQxqVrEjqApYkQ5Drj0nlyFCA/qX8HF0ILFwIrHuQesk0s2PkQ==", "license": "LGPL-3.0", "dependencies": { "ssb-typescript": "^2.5.0" @@ -15522,6 +8236,8 @@ }, "node_modules/ssb-validate": { "version": "4.1.4", + "resolved": "https://registry.npmjs.org/ssb-validate/-/ssb-validate-4.1.4.tgz", + "integrity": "sha512-nzj5EQnhm5fBGXgtzuuWgxv45dW+CJJm4eCLZKiOxyG1NE/WJZwju2DmqZfiE9zr9bC2T2hPHkckDP0CCP8v8w==", "license": "MIT", "dependencies": { "is-canonical-base64": "^1.1.1", @@ -15530,176 +8246,24 @@ "ssb-ref": "^2.6.2" } }, - "node_modules/ssb-ws": { - "version": "6.2.3", - "license": "MIT", - "dependencies": { - "emoji-server": "^1.0.0", - "multiblob-http": "^1.0.0", - "multiserver": "^3.0.2", - "pull-box-stream": "^1.0.13", - "pull-stream": "^3.6.9", - "ssb-ref": "^2.3.0", - "stack": "^0.1.0" - } - }, - "node_modules/stack": { - "version": "0.1.0", - "engines": [ - "node >= 0.2.0" - ] - }, - "node_modules/static-extend": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", - "integrity": "sha512-72E9+uLc27Mt718pMHt9VMNiAL4LMsmDbBva8mxWUCkT07fSzEGMYUCk0XWY6lp0j6RBAG4cJ3mWuZv2OE3s0g==", - "license": "MIT", - "dependencies": { - "define-property": "^0.2.5", - "object-copy": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/static-extend/node_modules/define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", - "license": "MIT", - "dependencies": { - "is-descriptor": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/static-extend/node_modules/is-descriptor": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", - "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", - "license": "MIT", - "dependencies": { - "is-accessor-descriptor": "^1.0.1", - "is-data-descriptor": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/statistics": { - "version": "3.3.0" + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/statistics/-/statistics-3.3.0.tgz", + "integrity": "sha512-9+dmo0XcSK1AU6/uNuSUV/9/KnGUT+7ZKeL+J4IWcjcczflFTWwYQaInBBSo2zyEFVkFd59bGVxzZ8SSBWC66g==" }, "node_modules/statuses": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", "license": "MIT", "engines": { "node": ">= 0.8" } }, - "node_modules/stdin-discarder": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.3.1.tgz", - "integrity": "sha512-reExS1kSGoElkextOcPkel4NE99S0BWxjUHQeDFnR8S993JxpPX7KU4MNmO19NXhlJp+8dmdCbKQVNgLJh2teA==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/stdout-update": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/stdout-update/-/stdout-update-4.0.1.tgz", - "integrity": "sha512-wiS21Jthlvl1to+oorePvcyrIkiG/6M3D3VTmDUlJm7Cy6SbFhKkAvX+YBuHLxck/tO3mrdpC/cNesigQc3+UQ==", - "license": "MIT", - "dependencies": { - "ansi-escapes": "^6.2.0", - "ansi-styles": "^6.2.1", - "string-width": "^7.1.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/stdout-update/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/stdout-update/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/stdout-update/node_modules/emoji-regex": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", - "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", - "license": "MIT" - }, - "node_modules/stdout-update/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/stdout-update/node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/steno": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/steno/-/steno-4.0.2.tgz", - "integrity": "sha512-yhPIQXjrlt1xv7dyPQg2P17URmXbuM5pdGkpiMB3RenprfiBlvK415Lctfe0eshk90oA7/tNq7WEiMK8RSP39A==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/typicode" - } - }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -15709,8 +8273,16 @@ "node": ">= 0.4" } }, + "node_modules/store2": { + "version": "2.14.4", + "resolved": "https://registry.npmjs.org/store2/-/store2-2.14.4.tgz", + "integrity": "sha512-srTItn1GOvyvOycgxjAnPA63FZNwy0PTyUBFMHRM+hVFltAeoh0LmNBz9SZqUS9mMqGk8rfyWyXn3GH5ReJ8Zw==", + "license": "MIT" + }, "node_modules/stream-to-pull-stream": { "version": "1.7.3", + "resolved": "https://registry.npmjs.org/stream-to-pull-stream/-/stream-to-pull-stream-1.7.3.tgz", + "integrity": "sha512-6sNyqJpr5dIOQdgNy/xcDWwDuzAsAwVzhzrWlAPAQ7Lkjx/rv0wgvxEyKwTq6FmNd5rjTrELt/CLmaSw7crMGg==", "license": "MIT", "dependencies": { "looper": "^3.0.0", @@ -15719,6 +8291,8 @@ }, "node_modules/stream-to-pull-stream/node_modules/looper": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/looper/-/looper-3.0.0.tgz", + "integrity": "sha512-LJ9wplN/uSn72oJRsXTx+snxPet5c8XiZmOKCm906NVYu+ag6SB6vUcnJcWxgnl2NfbIyeobAn7Bwv6xRj2XJg==", "license": "MIT" }, "node_modules/streamx": { @@ -15732,19 +8306,10 @@ "text-decoder": "^1.1.0" } }, - "node_modules/strict-event-emitter-types": { - "version": "2.0.0", - "license": "ISC" - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, "node_modules/string-width": { "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", "license": "MIT", "dependencies": { "eastasianwidth": "^0.2.0", @@ -15759,8 +8324,10 @@ } }, "node_modules/string-width-cjs": { - "name": "string-width", "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "name": "string-width", "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -15773,10 +8340,14 @@ }, "node_modules/string-width-cjs/node_modules/emoji-regex": { "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "license": "MIT" }, "node_modules/string-width/node_modules/ansi-regex": { "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "license": "MIT", "engines": { "node": ">=12" @@ -15787,6 +8358,8 @@ }, "node_modules/string-width/node_modules/strip-ansi": { "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" @@ -15800,6 +8373,8 @@ }, "node_modules/string.prototype.trim": { "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -15819,6 +8394,8 @@ }, "node_modules/string.prototype.trimend": { "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -15835,6 +8412,8 @@ }, "node_modules/string.prototype.trimstart": { "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", "license": "MIT", "dependencies": { "call-bind": "^1.0.7", @@ -15848,20 +8427,19 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/stringify-entities": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-1.3.2.tgz", - "integrity": "sha512-nrBAQClJAPN2p+uGCVJRPIPakKeKWZ9GtBCmormE7pWOSlHat7+x5A8gx85M7HM5Dt0BP3pP5RhVW77WdbJJ3A==", + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", "license": "MIT", "dependencies": { - "character-entities-html4": "^1.0.0", - "character-entities-legacy": "^1.0.0", - "is-alphanumerical": "^1.0.0", - "is-hexadecimal": "^1.0.0" + "safe-buffer": "~5.2.0" } }, "node_modules/strip-ansi": { "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -15871,8 +8449,10 @@ } }, "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "name": "strip-ansi", "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -15881,38 +8461,10 @@ "node": ">=8" } }, - "node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/strip-indent": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-2.0.0.tgz", - "integrity": "sha512-RsSNPLpq6YUL7QYy44RnPVTn/lcVZtb48Uof3X5JLbF4zD/Gs7ZFDv2HWol+leoQN2mT86LAzSshGfkTlSOpsA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/strnum": { - "version": "1.1.2", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT" - }, "node_modules/strtok3": { "version": "6.3.0", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-6.3.0.tgz", + "integrity": "sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw==", "license": "MIT", "dependencies": { "@tokenizer/token": "^0.3.0", @@ -15926,17 +8478,10 @@ "url": "https://github.com/sponsors/Borewit" } }, - "node_modules/supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==", - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -15951,123 +8496,10 @@ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", "license": "MIT" }, - "node_modules/tape": { - "version": "4.17.0", - "license": "MIT", - "dependencies": { - "@ljharb/resumer": "~0.0.1", - "@ljharb/through": "~2.3.9", - "call-bind": "~1.0.2", - "deep-equal": "~1.1.1", - "defined": "~1.0.1", - "dotignore": "~0.1.2", - "for-each": "~0.3.3", - "glob": "~7.2.3", - "has": "~1.0.3", - "inherits": "~2.0.4", - "is-regex": "~1.1.4", - "minimist": "~1.2.8", - "mock-property": "~1.0.0", - "object-inspect": "~1.12.3", - "resolve": "~1.22.6", - "string.prototype.trim": "~1.2.8" - }, - "bin": { - "tape": "bin/tape" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/tape/node_modules/deep-equal": { - "version": "1.1.2", - "license": "MIT", - "dependencies": { - "is-arguments": "^1.1.1", - "is-date-object": "^1.0.5", - "is-regex": "^1.1.4", - "object-is": "^1.1.5", - "object-keys": "^1.1.1", - "regexp.prototype.flags": "^1.5.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/tape/node_modules/glob": { - "version": "7.2.3", - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/tape/node_modules/is-regex": { - "version": "1.1.4", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/tape/node_modules/object-inspect": { - "version": "1.12.3", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/tar": { - "version": "7.5.11", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.11.tgz", - "integrity": "sha512-ChjMH33/KetonMTAtpYdgUFr0tbz69Fp2v7zWxQfYZX4g5ZN2nOBXm1R2xyA+lMIKrLKIoKAwFj93jE/avX9cQ==", - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/fs-minipass": "^4.0.0", - "chownr": "^3.0.0", - "minipass": "^7.1.2", - "minizlib": "^3.1.0", - "yallist": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/tar-fs": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.2.tgz", - "integrity": "sha512-QGxxTxxyleAdyM3kpFs14ymbYmNFrfY+pHj7Z8FgtbZ7w2//VAgLMac7sT6nRpIHjppXO2AwwEOg0bPFVRcmXw==", - "license": "MIT", - "dependencies": { - "pump": "^3.0.0", - "tar-stream": "^3.1.5" - }, - "optionalDependencies": { - "bare-fs": "^4.0.1", - "bare-path": "^3.0.0" - } - }, "node_modules/tar-stream": { "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", "license": "MIT", "dependencies": { "b4a": "^1.6.4", @@ -16075,29 +8507,42 @@ "streamx": "^2.15.0" } }, - "node_modules/teex": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", - "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "node_modules/telegram": { + "version": "2.26.22", + "resolved": "https://registry.npmjs.org/telegram/-/telegram-2.26.22.tgz", + "integrity": "sha512-EIj7Yrjiu0Yosa3FZ/7EyPg9s6UiTi/zDQrFmR/2Mg7pIUU+XjAit1n1u9OU9h2oRnRM5M+67/fxzQluZpaJJg==", + "deprecated": "This package is archived and no longer maintained. Development continues in teleproto (https://npmjs.com/package/teleproto), a largely compatible, actively maintained fork. See the migration guide at https://docs.teleproto.dev/migrating-from-gramjs", "license": "MIT", - "optional": true, "dependencies": { - "streamx": "^2.12.5" + "@cryptography/aes": "^0.1.1", + "async-mutex": "^0.3.0", + "big-integer": "^1.6.48", + "buffer": "^6.0.3", + "htmlparser2": "^6.1.0", + "mime": "^3.0.0", + "node-localstorage": "^2.2.1", + "pako": "^2.0.3", + "path-browserify": "^1.0.1", + "real-cancellable-promise": "^1.1.1", + "socks": "^2.6.2", + "store2": "^2.13.0", + "ts-custom-error": "^3.2.0", + "websocket": "^1.0.34" + }, + "optionalDependencies": { + "bufferutil": "^4.0.3", + "utf-8-validate": "^5.0.5" } }, "node_modules/text-decoder": { "version": "1.2.3", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", + "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", "license": "Apache-2.0", "dependencies": { "b4a": "^1.6.4" } }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "license": "MIT" - }, "node_modules/tldts": { "version": "7.0.23", "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.23.tgz", @@ -16118,6 +8563,8 @@ }, "node_modules/to-buffer": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.1.tgz", + "integrity": "sha512-tB82LpAIWjhLYbqjx3X4zEeHN6M8CiuOEy2JY8SEQVdYRe3CCHOFaqrBW1doLDrfpWhplcW7BL+bO3/6S3pcDQ==", "license": "MIT", "dependencies": { "isarray": "^2.0.5", @@ -16130,6 +8577,8 @@ }, "node_modules/to-camel-case": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/to-camel-case/-/to-camel-case-1.0.0.tgz", + "integrity": "sha512-nD8pQi5H34kyu1QDMFjzEIYqk0xa9Alt6ZfrdEMuHCFOfTLhDG5pgTu/aAM9Wt9lXILwlXmWP43b8sav0GNE8Q==", "license": "MIT", "dependencies": { "to-space-case": "^1.0.0" @@ -16137,85 +8586,23 @@ }, "node_modules/to-no-case": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/to-no-case/-/to-no-case-1.0.2.tgz", + "integrity": "sha512-Z3g735FxuZY8rodxV4gH7LxClE4H0hTIyHNIHdk+vpQxjLm0cwnKXq/OFVZ76SOQmto7txVcwSCwkU5kqp+FKg==", "license": "MIT" }, - "node_modules/to-object-path": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", - "integrity": "sha512-9mWHdnGRuh3onocaHzukyvCZhzvr6tiflAy/JRFXcJX0TjgfWA9pk9t8CMbzmBE4Jfw58pXbkngtBtqYxzNEyg==", - "license": "MIT", - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/to-regex": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", - "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", - "license": "MIT", - "dependencies": { - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "regex-not": "^1.0.2", - "safe-regex": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==", - "license": "MIT", - "dependencies": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/to-regex-range/node_modules/is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", - "license": "MIT", - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/to-space-case": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/to-space-case/-/to-space-case-1.0.0.tgz", + "integrity": "sha512-rLdvwXZ39VOn1IxGL3V6ZstoTbwLRckQmn/U8ZDLuWwIXNpuZDhQ3AiRUlhTbOXFVE9C+dR51wM0CBDhk31VcA==", "license": "MIT", "dependencies": { "to-no-case": "^1.0.0" } }, - "node_modules/to-vfile": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/to-vfile/-/to-vfile-1.0.0.tgz", - "integrity": "sha512-BHc+hdHwULe8x6xmQhSuTsiiPHyTCCf7dtH7l6WkBoYBR2rDfYtoJufKLDDAYGMfA+1XoRq44HfyjoB9vMBr1w==", - "license": "MIT", - "dependencies": { - "vfile": "^1.0.0" - } - }, - "node_modules/toad-cache": { - "version": "3.7.0", - "license": "MIT", - "engines": { - "node": ">=12" - } - }, "node_modules/toidentifier": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", "license": "MIT", "engines": { "node": ">=0.6" @@ -16223,6 +8610,8 @@ }, "node_modules/token-types": { "version": "4.2.1", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-4.2.1.tgz", + "integrity": "sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ==", "license": "MIT", "dependencies": { "@tokenizer/token": "^0.3.0", @@ -16238,21 +8627,13 @@ }, "node_modules/too-hot": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/too-hot/-/too-hot-1.0.1.tgz", + "integrity": "sha512-ymyiJ8bM8e2wKfJpxbq4Fi7GwLy9w7P8PKHnsWlDgbRIampbMtjv8eFrvpMIC0bK5dKybYrYqvbLUVu5/sDQbw==", "license": "MIT", "dependencies": { "cpu-percentage": "~1.0.3" } }, - "node_modules/touch": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", - "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", - "dev": true, - "license": "ISC", - "bin": { - "nodetouch": "bin/nodetouch.js" - } - }, "node_modules/tough-cookie": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", @@ -16265,12 +8646,10 @@ "node": ">=16" } }, - "node_modules/tr46": { - "version": "0.0.3", - "license": "MIT" - }, "node_modules/traverse": { "version": "0.6.11", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.11.tgz", + "integrity": "sha512-vxXDZg8/+p3gblxB6BhhG5yWVn1kGRlaL8O78UDXc3wRnPizB5g83dcvWV1jpDMIPnjZjOFuxlMmE82XJ4407w==", "license": "MIT", "dependencies": { "gopd": "^1.2.0", @@ -16284,81 +8663,64 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/trim": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/trim/-/trim-0.0.3.tgz", - "integrity": "sha512-h82ywcYhHK7veeelXrCScdH7HkWfbIT1D/CgYO+nmDarz3SGNssVBMws6jU16Ga60AJCRAvPV6w6RLuNerQqjg==", - "deprecated": "Use String.prototype.trim() instead" - }, - "node_modules/trim-lines": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-1.1.3.tgz", - "integrity": "sha512-E0ZosSWYK2mkSu+KEtQ9/KqarVjA9HztOSX+9FDdNacRAq29RRV6ZQNgob3iuW8Htar9vAfEa6yyt5qBAHZDBA==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/trim-newlines": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-2.0.0.tgz", - "integrity": "sha512-MTBWv3jhVjTU7XR3IQHllbiJs8sc75a80OEhB6or/q7pLTWgQ0bMGQXXYQSrSuXe6WiKWDZ5txXY5P59a/coVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/trim-trailing-lines": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/trim-trailing-lines/-/trim-trailing-lines-1.1.4.tgz", - "integrity": "sha512-rjUWSqnfTNrjbB9NQWfPMH/xRK1deHeGsHoVfpxJ++XeYXE0d6B1En37AHfw3jtfTU7dzMzZL2jjpe8Qb5gLIQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/truncate-utf8-bytes": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", + "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", "license": "WTFPL", "dependencies": { "utf8-byte-length": "^1.0.1" } }, + "node_modules/ts-custom-error": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/ts-custom-error/-/ts-custom-error-3.3.1.tgz", + "integrity": "sha512-5OX1tzOjxWEgsr/YEUWSuPrQ00deKLh6D7OTWcvNHm12/7QPyRh8SYpyWvA4IZv8H/+GQWQEh/kwo95Q9OVW1A==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/tsscmp": { "version": "1.0.6", + "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", + "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==", "license": "MIT", "engines": { "node": ">=0.6.x" } }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, "node_modules/tweetnacl": { "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", "license": "Unlicense" }, "node_modules/tweetnacl-auth": { "version": "0.3.1", + "resolved": "https://registry.npmjs.org/tweetnacl-auth/-/tweetnacl-auth-0.3.1.tgz", + "integrity": "sha512-9/c8c6qRMTfWuv54ETFhihgYoofi0M9HUovMSGJ1rLRUj6O5A0vuCg2L/qKfvmcjLVhaTgAJCLy2EqCLJK2QLw==", "license": "Unlicense", "dependencies": { "tweetnacl": "0.x.x" } }, + "node_modules/type": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz", + "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==", + "license": "ISC" + }, "node_modules/type-is": { "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", "license": "MIT", "dependencies": { "media-typer": "0.3.0", @@ -16370,6 +8732,8 @@ }, "node_modules/typed-array-buffer": { "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -16382,6 +8746,8 @@ }, "node_modules/typed-array-byte-length": { "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -16399,6 +8765,8 @@ }, "node_modules/typed-array-byte-offset": { "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", @@ -16418,6 +8786,8 @@ }, "node_modules/typed-array-length": { "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", "license": "MIT", "dependencies": { "call-bind": "^1.0.7", @@ -16434,14 +8804,10 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", - "license": "MIT" - }, "node_modules/typedarray-to-buffer": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-4.0.0.tgz", + "integrity": "sha512-6dOYeZfS3O9RtRD1caom0sMxgK59b27+IwoNy8RDPsmslSGOyU+mpTamlaIW7aNKi90ZQZ9DFaZL3YRoiSCULQ==", "funding": [ { "type": "github", @@ -16460,6 +8826,8 @@ }, "node_modules/typedarray.prototype.slice": { "version": "1.0.5", + "resolved": "https://registry.npmjs.org/typedarray.prototype.slice/-/typedarray.prototype.slice-1.0.5.tgz", + "integrity": "sha512-q7QNVDGTdl702bVFiI5eY4l/HkgCM6at9KhcFbgUAzezHFbOVy4+0O/lCjsABEQwbZPravVfBIiBVGo89yzHFg==", "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -16480,31 +8848,20 @@ }, "node_modules/typedfastbitset": { "version": "0.2.1", + "resolved": "https://registry.npmjs.org/typedfastbitset/-/typedfastbitset-0.2.1.tgz", + "integrity": "sha512-B2B+wo5gC7VRAqcFEiUCjS6CJ1vmeYZ7uzY3Jsu6UNStHiF+W0vkhZAmQaj5m9sC2KVrpyHGRzGuhz3M6+n/8A==", "license": "Apache-2.0" }, - "node_modules/typescript": { - "version": "5.8.3", - "license": "Apache-2.0", - "optional": true, - "peer": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/typewiselite": { - "version": "1.0.0", - "license": "MIT" - }, "node_modules/uint48be": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/uint48be/-/uint48be-2.0.1.tgz", + "integrity": "sha512-LQvWofTo3RCz+XaQR3VNch+dDFwpIvWr/98imhQne++vFhpQP16YAC/a8w9N00Heqqra00ACjHT18cgvn5H+bg==", "license": "MIT" }, "node_modules/unbox-primitive": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -16519,13 +8876,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/undefsafe": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", - "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", - "dev": true, - "license": "MIT" - }, "node_modules/undici": { "version": "7.24.2", "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.2.tgz", @@ -16537,408 +8887,53 @@ }, "node_modules/undici-types": { "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "license": "MIT" }, - "node_modules/unherit": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/unherit/-/unherit-1.1.3.tgz", - "integrity": "sha512-Ft16BJcnapDKp0+J/rqFC3Rrk6Y/Ng4nzsC028k2jdDII/rdZ7Wd3pPT/6+vIIxRagwRc9K0IUX0Ra4fKvw+WQ==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.0", - "xtend": "^4.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/unified": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/unified/-/unified-2.1.4.tgz", - "integrity": "sha512-qa4nA26ms49OczPueTt7G46r89TOlwAJ4pEk2U4mwkV1wNhjttItF03SE/YnfkgWg14tzmAHXGhJp2GhDYwn1A==", - "license": "MIT", - "dependencies": { - "attach-ware": "^1.0.0", - "bail": "^1.0.0", - "extend": "^3.0.0", - "unherit": "^1.0.4", - "vfile": "^1.0.0", - "ware": "^1.3.0" - } - }, - "node_modules/union-value": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", - "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", - "license": "MIT", - "dependencies": { - "arr-union": "^3.1.0", - "get-value": "^2.0.6", - "is-extendable": "^0.1.1", - "set-value": "^2.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/unique-random": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unique-random/-/unique-random-1.0.0.tgz", - "integrity": "sha512-K1sUkPf9EXCZFNIlMCoX4icAqcvkR4FMPH4Z61HbyiWhQl1ZGo0zYeV2bJmocK8Cp6tnKYrCnpkeKGebXZoRTQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/unique-random-array": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/unique-random-array/-/unique-random-array-1.0.1.tgz", - "integrity": "sha512-z9J/SV8CUIhIRROcHe9YUoAT6XthUJt0oUyLGgobiXJprDP9O9dsErNevvSaAv5BkhwFEVPn6nIEOKeNE6Ck1Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "unique-random": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/unist-util-is": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-3.0.0.tgz", - "integrity": "sha512-sVZZX3+kspVNmLWBPAB6r+7D9ZgAFPNWm66f7YNb420RlQSbn+n8rG8dGZSkrER7ZIXGQYNm5pqC3v3HopH24A==", - "license": "MIT" - }, - "node_modules/unist-util-visit": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-1.4.1.tgz", - "integrity": "sha512-AvGNk7Bb//EmJZyhtRUnNMEpId/AZ5Ph/KUpTI09WHQuDZHKovQ1oEv3mfmKpWKtoMzyMC4GLBm1Zy5k12fjIw==", - "license": "MIT", - "dependencies": { - "unist-util-visit-parents": "^2.0.0" - } - }, - "node_modules/unist-util-visit-parents": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-2.1.2.tgz", - "integrity": "sha512-DyN5vD4NE3aSeB+PXYNKxzGsfocxp6asDc2XXE3b0ekO2BaRUpBicbbUygfSvYfUz1IkmjFR1YF7dPklraMZ2g==", - "license": "MIT", - "dependencies": { - "unist-util-is": "^3.0.0" - } - }, - "node_modules/universal-github-app-jwt": { - "version": "2.2.2", - "license": "MIT" - }, - "node_modules/universal-user-agent": { - "version": "7.0.3", - "license": "ISC" - }, - "node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, "node_modules/unpipe": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", "license": "MIT", "engines": { "node": ">= 0.8" } }, - "node_modules/unset-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", - "integrity": "sha512-PcA2tsuGSF9cnySLHTLSh2qrQiJ70mn+r+Glzxv2TWZblxsxCC52BDlZoPCsz7STd9pN7EZetkWZBAvk4cgZdQ==", + "node_modules/utf-8-validate": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", + "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", + "hasInstallScript": true, "license": "MIT", "dependencies": { - "has-value": "^0.3.1", - "isobject": "^3.0.0" + "node-gyp-build": "^4.3.0" }, "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/unset-value/node_modules/has-value": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", - "integrity": "sha512-gpG936j8/MzaeID5Yif+577c17TxaDmhuyVgSwtnL/q8UUTySg8Mecb+8Cf1otgLoD7DDH75axp86ER7LFsf3Q==", - "license": "MIT", - "dependencies": { - "get-value": "^2.0.3", - "has-values": "^0.1.4", - "isobject": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/unset-value/node_modules/has-value/node_modules/isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==", - "license": "MIT", - "dependencies": { - "isarray": "1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/unset-value/node_modules/has-values": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", - "integrity": "sha512-J8S0cEdWuQbqD9//tlZxiMuMNmxB8PlEwvYwuxsTmR1G5RXUePEX/SJn7aD0GMLieuZYSwNH0cQuJGwnYunXRQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/unset-value/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "license": "MIT" - }, - "node_modules/unset-value/node_modules/isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/untildify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/untildify/-/untildify-2.1.0.tgz", - "integrity": "sha512-sJjbDp2GodvkB0FZZcn7k6afVisqX5BZD7Yq3xp4nN2O15BBK0cLm3Vwn2vQaF7UDS0UUsrQMkkplmDI5fskig==", - "license": "MIT", - "dependencies": { - "os-homedir": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/urix": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", - "integrity": "sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg==", - "deprecated": "Please see https://github.com/lydell/urix#deprecated", - "license": "MIT" - }, - "node_modules/url-join": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", - "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==", - "license": "MIT" - }, - "node_modules/use": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", - "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/user-home": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/user-home/-/user-home-2.0.0.tgz", - "integrity": "sha512-KMWqdlOcjCYdtIJpicDSFBQ8nFwS2i9sslAd6f4+CBGcU4gist2REnr2fxj2YocvJFxSF3ZOHLYLVZnUxv4BZQ==", - "license": "MIT", - "dependencies": { - "os-homedir": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" + "node": ">=6.14.2" } }, "node_modules/utf8-byte-length": { "version": "1.0.5", + "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", + "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==", "license": "(WTFPL OR MIT)" }, - "node_modules/util": { - "version": "0.12.5", - "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", - "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "is-arguments": "^1.0.4", - "is-generator-function": "^1.0.7", - "is-typed-array": "^1.1.3", - "which-typed-array": "^1.1.2" - } - }, "node_modules/util-deprecate": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, - "node_modules/validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "node_modules/validate-npm-package-name": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-7.0.2.tgz", - "integrity": "sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A==", - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, "node_modules/vary": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", "license": "MIT", "engines": { "node": ">= 0.8" } }, - "node_modules/vfile": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-1.4.0.tgz", - "integrity": "sha512-7Fz639rwERslMqQCuf1/0H4Tqe2q484Xl6X/jsKqrP7IjFcDODFURhv0GekMnImpbj9pTOojtqL7r39LJJkjGA==", - "license": "MIT" - }, - "node_modules/vfile-find-down": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/vfile-find-down/-/vfile-find-down-1.0.0.tgz", - "integrity": "sha512-AOXiJrVKizToYfRosXd1p9Fq8b0u0qchvSwVF1/ue3JE7o7KuQ/UH24bNAPLDUG/RIM1DZ6zWtDsiLShcma4WA==", - "license": "MIT", - "dependencies": { - "to-vfile": "^1.0.0" - } - }, - "node_modules/vfile-find-up": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/vfile-find-up/-/vfile-find-up-1.0.0.tgz", - "integrity": "sha512-t97P/jQswvX0n//+RB74Wj43VOg3tel2InzaJYryaBewd4uN4pNXuoH/F00PkI3U1fBp+w/SIyrTjzIzPwDODg==", - "license": "MIT", - "dependencies": { - "to-vfile": "^1.0.0" - } - }, - "node_modules/vfile-reporter": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/vfile-reporter/-/vfile-reporter-1.5.0.tgz", - "integrity": "sha512-VFF1LK0O8/nLmrPcc+5VMEnyP21BTzdVoq1rbxTaVt6cmSVk5MQs1POhkfY/cctndmZheNgirTcAMoiKj3aJYA==", - "license": "MIT", - "dependencies": { - "chalk": "^1.1.0", - "log-symbols": "^1.0.2", - "plur": "^2.0.0", - "repeat-string": "^1.5.0", - "string-width": "^1.0.0", - "text-table": "^0.2.0", - "vfile-sort": "^1.0.0" - } - }, - "node_modules/vfile-reporter/node_modules/ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/vfile-reporter/node_modules/ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/vfile-reporter/node_modules/chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/vfile-reporter/node_modules/is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw==", - "license": "MIT", - "dependencies": { - "number-is-nan": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/vfile-reporter/node_modules/log-symbols": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-1.0.2.tgz", - "integrity": "sha512-mmPrW0Fh2fxOzdBbFv4g1m6pR72haFLPJ2G5SJEELf1y+iaQrDG6cWCPjy54RHYbZAt7X+ls690Kw62AdWXBzQ==", - "license": "MIT", - "dependencies": { - "chalk": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/vfile-reporter/node_modules/string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==", - "license": "MIT", - "dependencies": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/vfile-reporter/node_modules/strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/vfile-sort": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/vfile-sort/-/vfile-sort-1.0.0.tgz", - "integrity": "sha512-6qIalNEKUt2YyVFzyJptdEo9sm/pMrSKvOJ35lH4us9YeW08zRs3E9VbdJ0O0n2Thxc1TWINP5QVhucy/YiGPA==", - "license": "MIT" - }, "node_modules/w3c-xmlserializer": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", @@ -16951,22 +8946,31 @@ "node": ">=18" } }, - "node_modules/ware": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/ware/-/ware-1.3.0.tgz", - "integrity": "sha512-Y2HUDMktriUm+SR2gZWxlrszcgtXExlhQYZ8QJNYbl22jum00KIUcHJ/h/sdAXhWTJcbSkiMYN9Z2tWbWYSrrw==", - "license": "MIT", + "node_modules/websocket": { + "version": "1.0.35", + "resolved": "https://registry.npmjs.org/websocket/-/websocket-1.0.35.tgz", + "integrity": "sha512-/REy6amwPZl44DDzvRCkaI1q1bIiQB0mEFQLUrhz3z2EK91cp3n72rAjUlrTP0zV22HJIUOVHQGPxhFRjxjt+Q==", + "license": "Apache-2.0", "dependencies": { - "wrap-fn": "^0.1.0" + "bufferutil": "^4.0.1", + "debug": "^2.2.0", + "es5-ext": "^0.10.63", + "typedarray-to-buffer": "^3.1.5", + "utf-8-validate": "^5.0.2", + "yaeti": "^0.0.6" + }, + "engines": { + "node": ">=4.0.0" } }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "license": "BSD-2-Clause" - }, - "node_modules/whatwg-fetch": { - "version": "3.6.20", - "license": "MIT" + "node_modules/websocket/node_modules/typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "license": "MIT", + "dependencies": { + "is-typedarray": "^1.0.0" + } }, "node_modules/whatwg-mimetype": { "version": "5.0.0", @@ -16977,16 +8981,10 @@ "node": ">=20" } }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "license": "MIT", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, "node_modules/which": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -17000,6 +8998,8 @@ }, "node_modules/which-boxed-primitive": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", "license": "MIT", "dependencies": { "is-bigint": "^1.1.0", @@ -17017,6 +9017,8 @@ }, "node_modules/which-builtin-type": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -17042,6 +9044,8 @@ }, "node_modules/which-collection": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", "license": "MIT", "dependencies": { "is-map": "^2.0.3", @@ -17058,10 +9062,14 @@ }, "node_modules/which-module": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", "license": "ISC" }, "node_modules/which-typed-array": { "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", @@ -17079,15 +9087,10 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/word-wrap": { - "version": "1.2.5", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/wrap-ansi": { "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", "license": "MIT", "dependencies": { "ansi-styles": "^6.1.0", @@ -17102,8 +9105,10 @@ } }, "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "name": "wrap-ansi", "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -17119,10 +9124,14 @@ }, "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "license": "MIT" }, "node_modules/wrap-ansi-cjs/node_modules/string-width": { "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -17135,6 +9144,8 @@ }, "node_modules/wrap-ansi/node_modules/ansi-regex": { "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "license": "MIT", "engines": { "node": ">=12" @@ -17145,6 +9156,8 @@ }, "node_modules/wrap-ansi/node_modules/ansi-styles": { "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", "license": "MIT", "engines": { "node": ">=12" @@ -17155,6 +9168,8 @@ }, "node_modules/wrap-ansi/node_modules/strip-ansi": { "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" @@ -17166,27 +9181,27 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/wrap-fn": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/wrap-fn/-/wrap-fn-0.1.5.tgz", - "integrity": "sha512-xDLdGx0M8JQw9QDAC9s5NUxtg9MI09F6Vbxa2LYoSoCvzJnx2n81YMIfykmXEGsUvuLaxnblJTzhSOjUOX37ag==", - "license": "MIT", - "dependencies": { - "co": "3.1.0" - } - }, - "node_modules/wrap-fn/node_modules/co": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/co/-/co-3.1.0.tgz", - "integrity": "sha512-CQsjCRiNObI8AtTsNIBDRMQ4oMR83CzEswHYahClvul7gKk+lDQiOKv+5qh7LQWf5sh6jkZNispz/QlsZxyNgA==", - "license": "MIT" - }, "node_modules/wrappy": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "node_modules/write-file-atomic": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-1.3.4.tgz", + "integrity": "sha512-SdrHoC/yVBPpV0Xq/mUZQIpW2sWXAShb/V4pomcJXh92RuaO+f3UTWItiR3Px+pLnV2PvC2/bfn5cwr5X6Vfxw==", + "license": "ISC", + "dependencies": { + "graceful-fs": "^4.1.11", + "imurmurhash": "^0.1.4", + "slide": "^1.1.5" + } + }, "node_modules/ws": { "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", "license": "MIT", "engines": { "node": ">=8.3.0" @@ -17221,10 +9236,14 @@ }, "node_modules/xsalsa20": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/xsalsa20/-/xsalsa20-1.2.0.tgz", + "integrity": "sha512-FIr/DEeoHfj7ftfylnoFt3rAIRoWXpx2AoDfrT2qD2wtp7Dp+COajvs/Icb7uHqRW9m60f5iXZwdsJJO3kvb7w==", "license": "MIT" }, "node_modules/xtend": { "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", "license": "MIT", "engines": { "node": ">=0.4" @@ -17232,22 +9251,27 @@ }, "node_modules/y18n": { "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "license": "ISC", "engines": { "node": ">=10" } }, - "node_modules/yallist": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", - "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", - "license": "BlueOak-1.0.0", + "node_modules/yaeti": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/yaeti/-/yaeti-0.0.6.tgz", + "integrity": "sha512-MvQa//+KcZCUkBTIC9blM+CU9J2GzuTytsOUwf2lidtvkx/6gnEp1QvJv34t9vdjhFmha/mUiNDbN0D0mJWdug==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "MIT", "engines": { - "node": ">=18" + "node": ">=0.10.32" } }, "node_modules/yargs": { "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "license": "MIT", "dependencies": { "cliui": "^8.0.1", @@ -17264,6 +9288,8 @@ }, "node_modules/yargs-parser": { "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "license": "ISC", "engines": { "node": ">=12" @@ -17271,10 +9297,14 @@ }, "node_modules/yargs/node_modules/emoji-regex": { "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "license": "MIT" }, "node_modules/yargs/node_modules/string-width": { "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -17287,31 +9317,29 @@ }, "node_modules/ylru": { "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ylru/-/ylru-1.4.0.tgz", + "integrity": "sha512-2OQsPNEmBCvXuFlIni/a+Rn+R2pHW9INm0BxXJ4hVDA8TirqMj+J/Rp9ItLatT/5pZqWwefVrTQcHpixsxnVlA==", "license": "MIT", "engines": { "node": ">= 4.0.0" } }, - "node_modules/yoctocolors": { - "version": "2.1.1", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/zerr": { "version": "1.0.4", + "resolved": "https://registry.npmjs.org/zerr/-/zerr-1.0.4.tgz", + "integrity": "sha512-u3WW9JA9xNmhy3cK6iSi3uWe4XmuqPCZ+jOrngEIwYz//ZUhY6bexVFrWLcxrIDUbJeXnyJORqVYPi2YU6gA+A==", "license": "MIT" }, "node_modules/ziii": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/ziii/-/ziii-1.0.2.tgz", + "integrity": "sha512-q1FogtBIchy1W0fkxUpe6A4n4WUvAM+hAHN1J6LjBNCV42ZegeC5JSz0mcNv4qxnI0V4cL4FNeEhPMm97Ed0kA==", "license": "MIT" }, "node_modules/zip-stream": { "version": "6.0.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz", + "integrity": "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==", "license": "MIT", "dependencies": { "archiver-utils": "^5.0.0", @@ -17324,490 +9352,12 @@ }, "node_modules/zod": { "version": "3.25.56", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.56.tgz", + "integrity": "sha512-rd6eEF3BTNvQnR2e2wwolfTmUTnp70aUTqr0oaGbHifzC3BKJsoV+Gat8vxUMR1hwOKBs6El+qWehrHbCpW6SQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" } - }, - "packages/ssb-server": { - "version": "15.3.0", - "license": "MIT", - "dependencies": { - "bash-color": "~0.0.3", - "broadcast-stream": "^0.2.1", - "cont": "~1.0.3", - "cross-spawn": "^6.0.5", - "debug": "^4.1.1", - "deep-equal": "^1.0.1", - "explain-error": "^1.0.3", - "has-network": "0.0.1", - "ip": "^1.1.5", - "mdmanifest": "^1.0.8", - "minimist": "^1.1.3", - "mkdirp": "~0.5.0", - "multiblob": "^1.13.0", - "multiserver": "^3.3.1", - "multiserver-address": "^1.0.1", - "muxrpc-validation": "^3", - "muxrpcli": "3", - "mv": "^2.1.1", - "osenv": "^0.1.5", - "pull-cat": "~1.1.5", - "pull-file": "^1.0.0", - "pull-many": "~1.0.6", - "pull-pushable": "^2.2.0", - "pull-stream": "^3.6.2", - "rimraf": "^2.4.2", - "secret-stack": "^6.3.1", - "ssb-blobs": "1.2.2", - "ssb-caps": "^1.0.1", - "ssb-client": "^4.7.9", - "ssb-config": "^3.2.5", - "ssb-db": "19.2.0", - "ssb-ebt": "^5.6.1", - "ssb-friends": "^4.1.0", - "ssb-gossip": "^1.1.0", - "ssb-invite": "^2.1.2", - "ssb-keys": "^7.1.1", - "ssb-links": "^3.0.10", - "ssb-local": "^1.0.0", - "ssb-logging": "^1.0.0", - "ssb-master": "^1.0.2", - "ssb-no-auth": "^1.0.0", - "ssb-onion": "^1.0.0", - "ssb-ooo": "^1.3.0", - "ssb-plugins": "1.0.0", - "ssb-query": "^2.4.0", - "ssb-ref": "^2.13.9", - "ssb-replicate": "^1.3.0", - "ssb-unix-socket": "^1.0.0", - "ssb-ws": "^6.2.1", - "stream-to-pull-stream": "^1.6.10", - "zerr": "^1.0.0" - }, - "bin": { - "sbot": "bin.js", - "ssb-server": "bin.js" - }, - "devDependencies": { - "cat-names": "^2.0.0", - "compatibility": "^1.0.0", - "dog-names": "~1.0.2", - "hexpp": "^2.0.0", - "interleavings": "^1.0.0", - "npm-install-package": "^2.1.0", - "nyc": "^13.2.0", - "pull-abortable": "^4.1.1", - "pull-bitflipper": "^0.1.0", - "pull-paramap": "^1.2.2", - "rng": "^0.2.2", - "run-series": "^1.1.8", - "semver": "^5.6.0", - "ssb-feed": "^2.3.0", - "ssb-generate": "^1.0.1", - "ssb-validate": "^4", - "tape": "^4.9.1", - "typewiselite": "^1.0.0" - } - }, - "packages/ssb-server/node_modules/abstract-leveldown": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/abstract-leveldown/-/abstract-leveldown-6.0.3.tgz", - "integrity": "sha512-jzewKKpZbaYUa6HTThnrl+GrJhzjEAeuc7hTVpZdzg7kupXZFoqQDFwyOwLNbmJKJlmzw8yiipMPkDiuKkT06Q==", - "deprecated": "Superseded by abstract-level (https://github.com/Level/community#faq)", - "license": "MIT", - "dependencies": { - "level-concat-iterator": "~2.0.0", - "xtend": "~4.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "packages/ssb-server/node_modules/cross-spawn": { - "version": "6.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", - "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", - "license": "MIT", - "dependencies": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - }, - "engines": { - "node": ">=4.8" - } - }, - "packages/ssb-server/node_modules/epidemic-broadcast-trees": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/epidemic-broadcast-trees/-/epidemic-broadcast-trees-7.0.0.tgz", - "integrity": "sha512-nm1o1ncxcJvXtLJVoPl4AJFX7Bh0BhPsTC9XhCRWwdRJXp1Ud1DcGpi/6c3sUA/URmyoIc1WONNdub+/AE5vTQ==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "push-stream": "^10.0.0" - } - }, - "packages/ssb-server/node_modules/flumecodec": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/flumecodec/-/flumecodec-0.0.1.tgz", - "integrity": "sha512-JT0xivzdV7uTucjsLMw6JhK2e1K5TmU4fGmoQqnrJbydgY/V6+m71QoxX5ZtRR1pKoD48uhPDWWE6G5MlNoGkg==", - "license": "MIT", - "dependencies": { - "level-codec": "^6.2.0" - } - }, - "packages/ssb-server/node_modules/flumedb": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/flumedb/-/flumedb-1.1.0.tgz", - "integrity": "sha512-Bwol+72GU5z2DxZlnaxUA9A8qaRcQcdTprmRcgfqn2ldn147ByVh9Zyp90hVGPlo/oEN/yjOBUXcNkK3SYjbgA==", - "license": "MIT", - "dependencies": { - "cont": "^1.0.3", - "explain-error": "^1.0.3", - "obv": "0.0.1", - "pull-cont": "^0.1.1", - "pull-looper": "^1.0.0", - "pull-stream": "^3.5.0" - } - }, - "packages/ssb-server/node_modules/flumeview-level": { - "version": "3.0.14", - "resolved": "https://registry.npmjs.org/flumeview-level/-/flumeview-level-3.0.14.tgz", - "integrity": "sha512-Nl0gJOgrDGpJGZDkP6gvo6s1Q9WmRynbHUvI/JY3eQ81YgzUUa2FKLlfu6OHV5ho5NeXP+00F+0K1yBVaEgJOQ==", - "license": "MIT", - "dependencies": { - "charwise": "^3.0.1", - "explain-error": "^1.0.4", - "level": "^5.0.0", - "ltgt": "^2.1.3", - "mkdirp": "^0.5.1", - "obv": "0.0.1", - "pull-level": "^2.0.3", - "pull-paramap": "^1.2.1", - "pull-stream": "^3.5.0", - "pull-write": "^1.1.1" - } - }, - "packages/ssb-server/node_modules/flumeview-level/node_modules/level": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/level/-/level-5.0.1.tgz", - "integrity": "sha512-wcak5OQeA4rURGacqS62R/xNHjCYnJSQDBOlm4KNUGJVE9bWv2B04TclqReYejN+oD65PzD4FsqeWoI5wNC5Lg==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "level-js": "^4.0.0", - "level-packager": "^5.0.0", - "leveldown": "^5.0.0", - "opencollective-postinstall": "^2.0.0" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "packages/ssb-server/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "packages/ssb-server/node_modules/immediate": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.2.3.tgz", - "integrity": "sha512-RrGCXRm/fRVqMIhqXrGEX9rRADavPiDFSoMb/k64i9XMk8uH4r/Omi5Ctierj6XzNecwDbO4WuFbDD1zmpl3Tg==", - "license": "MIT" - }, - "packages/ssb-server/node_modules/ip": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.9.tgz", - "integrity": "sha512-cyRxvOEpNHNtchU3Ln9KC/auJgup87llfQpQ+t5ghoC/UhL16SWzbueiCsdTnWmqAWl7LadfuwhlqmtOaqMHdQ==", - "license": "MIT" - }, - "packages/ssb-server/node_modules/level-codec": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/level-codec/-/level-codec-6.2.0.tgz", - "integrity": "sha512-J437PvCMZZKNT88+VRh6dkmh1ndZzwGwDzb5ZZl3QEsl+U9wIlt8hG+Y1gXVOhH75gf8JyceKGiG6WFUBbxyGQ==", - "deprecated": "Superseded by level-transcoder (https://github.com/Level/community#faq)", - "license": "MIT" - }, - "packages/ssb-server/node_modules/level-js": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/level-js/-/level-js-4.0.2.tgz", - "integrity": "sha512-PeGjZsyMG4O89KHiez1zoMJxStnkM+oBIqgACjoo5PJqFiSUUm3GNod/KcbqN5ktyZa8jkG7I1T0P2u6HN9lIg==", - "deprecated": "Superseded by browser-level (https://github.com/Level/community#faq)", - "license": "MIT", - "dependencies": { - "abstract-leveldown": "~6.0.1", - "immediate": "~3.2.3", - "inherits": "^2.0.3", - "ltgt": "^2.1.2", - "typedarray-to-buffer": "~3.1.5" - } - }, - "packages/ssb-server/node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "license": "MIT", - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "packages/ssb-server/node_modules/muxrpc": { - "version": "6.7.3", - "resolved": "https://registry.npmjs.org/muxrpc/-/muxrpc-6.7.3.tgz", - "integrity": "sha512-yVr66BigJxHIvHOLsdJzuvrHt46Vq9DiDCBTZiS6q+49mXh4mkDjRiXo9FnGXQv1WJOqnRJk5fcfwV6TaSgNvA==", - "license": "MIT", - "dependencies": { - "clarify-error": "^1.0.0", - "debug": "^4.3.3", - "packet-stream": "~2.0.0", - "packet-stream-codec": "^1.2.0", - "pull-goodbye": "0.0.3", - "pull-stream": "^3.6.10" - } - }, - "packages/ssb-server/node_modules/path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "packages/ssb-server/node_modules/push-stream": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/push-stream/-/push-stream-10.1.2.tgz", - "integrity": "sha512-wCDN1KkAWOMXsiV0XxH/CggHqOYeXvyn00t9Zjp5RKsLZ2rzg/lLJIMAVxYLyh79T168W3fBYcG5TRRJAlQr6g==", - "license": "MIT" - }, - "packages/ssb-server/node_modules/rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, - "packages/ssb-server/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "license": "ISC", - "bin": { - "semver": "bin/semver" - } - }, - "packages/ssb-server/node_modules/shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", - "license": "MIT", - "dependencies": { - "shebang-regex": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "packages/ssb-server/node_modules/shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "packages/ssb-server/node_modules/ssb-blobs": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/ssb-blobs/-/ssb-blobs-1.2.2.tgz", - "integrity": "sha512-N+X46lE/KaIH9y1w3LQ9pPnt2tQr5VCSj1dAo/pJGYnSwnHz8GhIiboq7UGzrCZwCWgVUs22/2YiytCXSC9ASg==", - "license": "MIT", - "dependencies": { - "cont": "^1.0.3", - "debug": "^4.1.1", - "level": "^5.0.1", - "multiblob": "^1.12.0", - "pull-level": "^2.0.4", - "pull-notify": "^0.1.0", - "pull-stream": "^3.3.0", - "ssb-ref": "^2.3.0" - } - }, - "packages/ssb-server/node_modules/ssb-blobs/node_modules/level": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/level/-/level-5.0.1.tgz", - "integrity": "sha512-wcak5OQeA4rURGacqS62R/xNHjCYnJSQDBOlm4KNUGJVE9bWv2B04TclqReYejN+oD65PzD4FsqeWoI5wNC5Lg==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "level-js": "^4.0.0", - "level-packager": "^5.0.0", - "leveldown": "^5.0.0", - "opencollective-postinstall": "^2.0.0" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "packages/ssb-server/node_modules/ssb-db": { - "version": "19.2.0", - "resolved": "https://registry.npmjs.org/ssb-db/-/ssb-db-19.2.0.tgz", - "integrity": "sha512-pJAFizB6OcuJLX4RJJuU9HWyPwM2CqLi/vs08lhVIR3TGxacxpavvK5LzbxT+Y3iWkBchOTKS5hHCigA5aaung==", - "license": "MIT", - "dependencies": { - "async-write": "^2.1.0", - "cont": "~1.0.0", - "explain-error": "~1.0.1", - "flumedb": "^1.0.0", - "flumelog-offset": "^3.3.1", - "flumeview-hashtable": "^1.0.3", - "flumeview-level": "^3.0.5", - "flumeview-reduce": "^1.3.9", - "ltgt": "^2.2.0", - "mdmanifest": "^1.0.8", - "mkdirp": "^0.5.1", - "monotonic-timestamp": "~0.0.8", - "muxrpc-validation": "^3.0.0", - "obv": "0.0.1", - "osenv": "^0.1.5", - "pull-cont": "^0.1.1", - "pull-stream": "^3.4.0", - "rimraf": "^2.6.2", - "ssb-keys": "^7.1.3", - "ssb-msgs": "^5.0.0", - "ssb-ref": "^2.12.0", - "ssb-validate": "^4.0.0", - "typewiselite": "^1.0.0", - "zerr": "^1.0.0" - } - }, - "packages/ssb-server/node_modules/ssb-ebt": { - "version": "5.6.7", - "resolved": "https://registry.npmjs.org/ssb-ebt/-/ssb-ebt-5.6.7.tgz", - "integrity": "sha512-ifPgPNmDE8EKuuoqtXibwgYNtDZNry7sJL1epSUb3XgQr62bUV31N9R5LHKDsI2kx96OgWRwWY2PfZ7vf/hU8Q==", - "license": "MIT", - "dependencies": { - "base64-url": "^2.2.0", - "epidemic-broadcast-trees": "7", - "lossy-store": "^1.2.3", - "pull-stream": "^3.5.0", - "push-stream-to-pull-stream": "^1.0.0", - "ssb-ref": "^2.9.1" - } - }, - "packages/ssb-server/node_modules/ssb-friends": { - "version": "4.4.10", - "resolved": "https://registry.npmjs.org/ssb-friends/-/ssb-friends-4.4.10.tgz", - "integrity": "sha512-APTTt47PRVv5nzo+8VLWymAxSZPiAA3p5wPu9VlCqr4qQVaDkCF7Xn5Lk9j8VkjEnclNt7aeE/YgnXCld/BPNA==", - "license": "MIT", - "dependencies": { - "bipf": "^1.9.0", - "flumecodec": "0.0.1", - "flumeview-reduce": "^1.4.0", - "layered-graph": "^1.2.0", - "pull-flatmap": "0.0.1", - "pull-level": "^2.0.4", - "pull-notify": "^0.1.2", - "pull-stream": "^3.7.0", - "ssb-ref": "^2.16.0" - } - }, - "packages/ssb-server/node_modules/ssb-invite": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/ssb-invite/-/ssb-invite-2.1.8.tgz", - "integrity": "sha512-fuBu0jtZzfsejLWUQXVzNq6s6IekJGZqVpY6EZLx3J2mM0+3rXuwDI/1LtqL2VOfzrPbC4jRJi6I21PosBGveg==", - "license": "MIT", - "dependencies": { - "cont": "^1.0.3", - "explain-error": "^1.0.4", - "ip": "^1.1.8", - "level": "^6.0.1", - "muxrpc-validation": "^3.0.2", - "ssb-client": "^4.9.0", - "ssb-keys": "^7.2.2", - "ssb-ref": "^2.16.0" - } - }, - "packages/ssb-server/node_modules/ssb-keys": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/ssb-keys/-/ssb-keys-7.2.2.tgz", - "integrity": "sha512-FPeyYU/3LpxcagnbmVWE+Q/qzg6keqeOBPbD7sEH9UKixUASeufPKiORDgh8nVX7J9Z+0vUaHt/WG999kGjvVQ==", - "license": "MIT", - "dependencies": { - "chloride": "^2.2.8", - "mkdirp": "~0.5.0", - "private-box": "^0.3.0" - } - }, - "packages/ssb-server/node_modules/ssb-plugins": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/ssb-plugins/-/ssb-plugins-1.0.0.tgz", - "integrity": "sha512-eM8vid+K8MhwZwzk/CDUhSNhUoS6wYgq9clJrrKaP0/Otdd3zZzcBQw54Xvm0olMcOgpTSlY3m2rT4iqjZPIBw==", - "license": "MIT", - "dependencies": { - "cross-spawn": "^6.0.5", - "explain-error": "^1.0.4", - "mdmanifest": "^1.0.8", - "mkdirp": "^0.5.1", - "muxrpc": "^6.4.0", - "muxrpc-validation": "^3.0.0", - "mv": "^2.1.1", - "osenv": "^0.1.5", - "pull-cat": "^1.1.11", - "pull-many": "^1.0.8", - "pull-pushable": "^2.2.0", - "pull-stream": "^3.6.9", - "stream-to-pull-stream": "^1.7.2" - } - }, - "packages/ssb-server/node_modules/typedarray-to-buffer": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", - "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", - "license": "MIT", - "dependencies": { - "is-typedarray": "^1.0.0" - } - }, - "packages/ssb-server/node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "which": "bin/which" - } } } } diff --git a/src/server/package.json b/src/server/package.json index 00bb761..3acea84 100644 --- a/src/server/package.json +++ b/src/server/package.json @@ -1,6 +1,6 @@ { "name": "@krakenslab/oasis", - "version": "0.8.7", + "version": "1.3.3", "description": "Oasis - Social Networking Utopia", "repository": { "type": "git", @@ -17,27 +17,22 @@ "start:backend": "node ../backend/backend.js", "start:ssb": "node SSB_server.js start &", "dev": "npm run dev:backend", - "dev:backend": "nodemon", + "dev:backend": "npx --yes nodemon@3.1.14", "postinstall": "node ../../scripts/patch-node-modules.js" }, "dependencies": { "@koa/router": "^13.1.0", - "@open-rpc/client-js": "^1.8.1", - "@xenova/transformers": "^2.17.2", - "abstract-level": "^2.0.1", "archiver": "^7.0.1", "axios": "^1.10.0", - "cors": "^2.8.5", + "bencode": "^2.0.3", + "bipf": "^1.9.0", + "chloride": "^2.4.1", "debug": "^4.3.1", "dompurify": "^3.3.1", "env-paths": "^2.2.1", - "epidemic-broadcast-trees": "^9.0.4", - "express": "^5.1.0", "file-type": "^16.5.4", "highlight.js": "11.0.0", "hyperaxe": "^2.0.1", - "ip": "https://registry.npmjs.org/neoip/-/neoip-3.0.0.tgz", - "is-svg": "^4.4.0", "jsdom": "^28.0.0", "koa": "^2.7.0", "koa-body": "^6.0.1", @@ -47,77 +42,49 @@ "minimist": "^1.2.8", "module-alias": "^2.2.3", "moment": "^2.30.1", - "multiblob": "^1.13.0", - "multiserver": "^3.3.1", - "multiserver-address": "^1.0.1", - "muxrpc": "^8.0.0", - "muxrpc-validation": "^3.0.2", - "muxrpcli": "^3.1.2", - "node-llama-cpp": "^3.10.0", - "non-private-ip": "^2.2.0", "open": "^8.4.2", "openpgp": "^6.3.0", - "packet-stream": "^2.0.6", - "packet-stream-codec": "^1.2.0", - "pdfjs-dist": "^5.2.133", + "opusscript": "^0.1.1", "pretty-ms": "^7.0.1", "pull-abortable": "^4.1.1", "pull-cat": "~1.1.5", - "pull-file": "^1.0.0", - "pull-many": "~1.0.6", + "pull-defer": "^0.2.3", + "pull-level": "^2.0.4", "pull-paramap": "^1.2.2", "pull-pushable": "^2.2.0", "pull-sort": "^1.0.2", "pull-stream": "^3.7.0", - "punycode.js": "^2.3.1", "qrcode": "^1.5.4", "secret-stack": "^6.3.1", - "ssb-about": "^2.0.1", "ssb-autofollow": "^1.1.0", - "ssb-backlinks": "^2.1.1", "ssb-blobs": "^2.0.1", "ssb-box": "^1.0.1", "ssb-caps": "^1.0.1", + "ssb-classic": "^1.1.0", "ssb-client": "^4.9.0", "ssb-config": "^3.4.4", "ssb-conn": "6.0.3", - "ssb-conn-db": "^1.0.5", - "ssb-conn-hub": "^1.2.0", - "ssb-conn-staging": "^1.0.0", - "ssb-db": "^20.4.1", - "ssb-device-address": "^1.1.6", + "ssb-db2": "^6.3.3", "ssb-ebt": "^9.1.2", - "ssb-friend-pub": "^1.0.7", "ssb-friends": "^5.0.0", - "ssb-gossip": "^1.1.1", "ssb-invite": "^3.0.3", "ssb-invite-client": "^1.3.3", "ssb-keys": "^8.0.0", "ssb-lan": "^1.0.0", - "ssb-legacy-conn": "^1.0.17", - "ssb-links": "^3.0.10", "ssb-logging": "^1.0.0", - "ssb-markdown": "^3.6.0", "ssb-master": "^1.0.3", - "ssb-meme": "^1.1.0", "ssb-mentions": "^0.5.2", "ssb-msgs": "^5.2.0", "ssb-no-auth": "^1.0.0", "ssb-onion": "^1.0.0", - "ssb-partial-replication": "^3.0.1", - "ssb-plugins": "^1.0.2", - "ssb-private": "^1.1.0", - "ssb-query": "^2.4.5", "ssb-ref": "^2.16.0", "ssb-replication-scheduler": "^3.0.0", - "ssb-search": "^1.3.0", - "ssb-server": "file:packages/ssb-server", - "ssb-tangle": "^1.0.1", + "ssb-sort": "^1.1.3", "ssb-thread-schema": "^1.1.1", - "ssb-threads": "^10.0.4", "ssb-unix-socket": "^1.0.0", - "ssb-ws": "^6.2.3", - "util": "^0.12.5", + "ssb-validate": "^4.1.4", + "stream-to-pull-stream": "^1.7.3", + "telegram": "^2.26.22", "yargs": "^17.7.2" }, "overrides": { @@ -135,15 +102,14 @@ "ssb-ref": "^2.16.0", "secret-stack": "^6.3.1", "ip": "https://registry.npmjs.org/neoip/-/neoip-3.0.0.tgz", - "lodash.set": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" + "lodash.set": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "onnxruntime-node": "1.19.2", + "onnxruntime-common": "1.19.2", + "onnxruntime-web": "1.19.2" }, "optionalDependencies": { - "fsevents": "^2.3.2", "sharp": "^0.33.5" }, - "devDependencies": { - "nodemon": "^3.1.0" - }, "bugs": { "url": "https://github.com/epsylon/oasis/issues" }, @@ -153,7 +119,7 @@ }, "keywords": [], "engines": { - "node": "^10.0.0 || >=12.0.0" + "node": ">=22.0.0" }, "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" } diff --git a/src/server/phone_audio_android.js b/src/server/phone_audio_android.js new file mode 100644 index 0000000..eb45a2e --- /dev/null +++ b/src/server/phone_audio_android.js @@ -0,0 +1,45 @@ +const fs = require('fs'); +const net = require('net'); +const path = require('path'); + +const MARKER = path.join(__dirname, '..', '..', '.audio_bridge'); +const MAX_BACKLOG = 4000; + +const bridgePort = () => { + try { + const port = Number(String(fs.readFileSync(MARKER, 'utf8')).trim()); + return Number.isFinite(port) && port > 0 ? port : 0; + } catch (_) { + return 0; + } +}; + +module.exports = () => { + const port = bridgePort(); + if (!port) return null; + const open = (mode) => { + const sock = net.connect(port, '127.0.0.1'); + sock.on('error', () => {}); + sock.setNoDelay(true); + sock.write(mode); + return sock; + }; + return { + capture(onPcm) { + const sock = open('R'); + sock.on('data', onPcm); + return { stop() { try { sock.destroy(); } catch (_) {} } }; + }, + player() { + const sock = open('P'); + return { + write(buf) { + if (sock.destroyed || sock.writableLength > MAX_BACKLOG) return false; + sock.write(buf); + return true; + }, + stop() { try { sock.end(); sock.destroy(); } catch (_) {} } + }; + } + }; +}; diff --git a/src/server/phone_module.js b/src/server/phone_module.js new file mode 100644 index 0000000..32a6f41 --- /dev/null +++ b/src/server/phone_module.js @@ -0,0 +1,2124 @@ +const fs = require('fs'); +const os = require('os'); +const path = require('path'); +const { spawn } = require('child_process'); +const nodeCrypto = require('crypto'); +const pull = require('./node_modules/pull-stream'); +const Pushable = require('./node_modules/pull-pushable'); +const sodium = require('./node_modules/chloride'); +const ssbKeys = require('./node_modules/ssb-keys'); + +const RING_MS = 20000; +const PAM_MIN_MS = 500; +const dbg = process.env.OASIS_DEBUG ? (...a) => console.log('[phone]', ...a) : () => {}; +const sid = (id) => String(id || '').slice(0, 9); +const PAM_WARN_MS = 10000; +const VOICEMAIL_MAX_MS = 60000; +const RATE = 8000; +const FRAME_BYTES = 320; +const FRAME_MS = 20; +const MAX_BACKLOG = 4000; +const CLOCK_SKEW_MS = 5 * 60 * 1000; +const RING_WINDOW_MS = 60 * 1000; +const RING_PER_WINDOW = 20; +const SEEN_MAX = 5000; +const HUB_ROOMS_MAX = 256; +const HUB_RIDS_PER_ROOM = 4; +const GROUP_MAX = 10; +const MIX_QUEUE = 10; +const ROOM_MAX = 50; +const ROOM_QUIET_AT = 10; +const ROOM_JOIN_MS = 10000; +const ROOM_PEEK_MS = 1500; +const ROOM_FRAME_MAX = 2048; +const ROOM_RATE_MAX = 120; +const HUB_LAG_MAX = 2000; +const ROOM_ACK_EVERY = 100; +const ROUTES_MAX = 4096; +const MUTE_BEACON_MS = 2000; +const MUTE_TTL_MS = 6000; +const ROOM_EVENTS_MAX = 50; +const REC_TOGGLE_MIN_MS = 250; +const CHIMES_AT_ONCE = 3; +const ROOM_REC_MAX_BYTES = 4 * 3600 * 8000 * 2; +const CHIMES = { + join: [[660, 90], [0, 20], [990, 140]], + leave: [[990, 90], [0, 20], [660, 190]], + recStart: [[880, 70], [0, 60], [880, 70], [0, 60], [1320, 160]], + recStop: [[1320, 70], [0, 60], [660, 200]], + mute: [[523, 80], [0, 30], [392, 160]], + unmute: [[392, 80], [0, 30], [523, 180]], + hand: [[784, 60], [0, 40], [988, 60], [0, 40], [1175, 110]] +}; +const chimeMs = (kind) => (CHIMES[kind] || []).reduce((n, [, ms]) => n + ms, 0); +const K_CTRL = 0; +const K_AUDIO = 1; +const K_DATA = 2; +const K_KEY = 3; +const K_VIDEO = 4; +const VIDEO_CHUNK = 8 * 1024; +const VIDEO_PAD = 1024; +const VIDEO_HDR = 6; +const VIDEO_FILM = 0x80; +const VIDEO_FRAME_MAX = 256 * 1024; +const VIDEO_FPS = 8; +const VIDEO_SIZE = { w: 320, h: 240 }; +const STREAM_SIZE = { w: 640, h: 480 }; +const GRID_TILE_LARGE = { w: 640, h: 480 }; +const GRID_LARGE_MAX = 4; +const ROOM_VIDEO_CHUNK_EXTRA = 45; +const VIDEO_HOLD_MS = 3000; +const ROOM_VIDEO_FRAME_MAX = 10 * 1024; +const ROOM_VIDEO_BYTES_PER_S = 200 * 1024; +const ROOM_VIDEO_SEND_MAX = Math.floor(ROOM_VIDEO_BYTES_PER_S * 0.9); +const VAD_MIN = 200; +const VAD_FLOOR_MAX = 1500; +const VAD_HANG = 15; +const RID = /^[0-9a-f]{32}$/; + +const BIAS = 0x84; +const CLIP = 32635; +const ulawEncodeSample = (sample) => { + let s = sample; + const sign = (s >> 8) & 0x80; + if (sign) s = -s; + if (s > CLIP) s = CLIP; + s += BIAS; + let exponent = 7; + for (let mask = 0x4000; (s & mask) === 0 && exponent > 0; exponent--, mask >>= 1); + const mantissa = (s >> (exponent + 3)) & 0x0f; + return ~(sign | (exponent << 4) | mantissa) & 0xff; +}; +const ULAW_DECODE = new Int16Array(256); +for (let i = 0; i < 256; i++) { + const u = ~i & 0xff; + const t = (((u & 0x0f) << 3) + BIAS) << ((u & 0x70) >> 4); + ULAW_DECODE[i] = (u & 0x80) ? BIAS - t : t - BIAS; +} +const ulawEncode = (pcm) => { + const out = Buffer.alloc(pcm.length >> 1); + for (let i = 0; i < out.length; i++) out[i] = ulawEncodeSample(pcm.readInt16LE(i * 2)); + return out; +}; +const ulawDecode = (ulaw) => { + const out = Buffer.alloc(ulaw.length * 2); + for (let i = 0; i < ulaw.length; i++) out.writeInt16LE(ULAW_DECODE[ulaw[i]], i * 2); + return out; +}; + +const wavFile = (pcm) => { + const h = Buffer.alloc(44); + h.write('RIFF', 0); h.writeUInt32LE(36 + pcm.length, 4); h.write('WAVE', 8); + h.write('fmt ', 12); h.writeUInt32LE(16, 16); h.writeUInt16LE(1, 20); h.writeUInt16LE(1, 22); + h.writeUInt32LE(RATE, 24); h.writeUInt32LE(RATE * 2, 28); h.writeUInt16LE(2, 32); h.writeUInt16LE(16, 34); + h.write('data', 36); h.writeUInt32LE(pcm.length, 40); + return Buffer.concat([h, pcm]); +}; + +const connectingCycle = () => { + const n = RATE * 2; + const buf = Buffer.alloc(n * 2); + let seed = 0x2545f491; + let lp = 0; + for (let i = 0; i < n; i++) { + seed = (seed * 1103515245 + 12345) >>> 0; + lp += (((seed >>> 8) / 0x800000) - 1 - lp) * 0.08; + const env = Math.sin(Math.PI * i / n); + buf.writeInt16LE(Math.round(lp * 9000 * env * env), i * 2); + } + return buf; +}; +const toneCycle = (kind) => { + if (kind === 'connecting') return connectingCycle(); + const segs = kind === 'ring' + ? [{ ms: 1000, freq: (k) => (Math.floor(k / (RATE / 20)) % 2 ? 1250 : 1000) }, { ms: 2000 }] + : [{ ms: 1500, freq: () => 425 }, { ms: 3000 }]; + const total = segs.reduce((a, s) => a + s.ms, 0) * RATE / 1000; + const buf = Buffer.alloc(total * 2); + let i = 0; + let phase = 0; + for (const s of segs) { + const n = s.ms * RATE / 1000; + for (let k = 0; k < n; k++, i++) { + let v = 0; + if (s.freq) { phase += 2 * Math.PI * s.freq(k) / RATE; v = Math.round(Math.sin(phase) * 6000); } + buf.writeInt16LE(v, i * 2); + } + } + return buf; +}; + +const onPath = (bin) => (process.env.PATH || '').split(path.delimiter).some(d => d && fs.existsSync(path.join(d, bin))); +const BACKENDS = [ + { rec: ['parec', ['--raw', `--rate=${RATE}`, '--channels=1', '--format=s16le', '--latency-msec=20']], play: ['pacat', ['--playback', '--raw', `--rate=${RATE}`, '--channels=1', '--format=s16le', '--latency-msec=60']] }, + { rec: ['pw-record', ['--rate', String(RATE), '--channels', '1', '--format', 's16', '-']], play: ['pw-play', ['--rate', String(RATE), '--channels', '1', '--format', 's16', '-']] }, + { rec: ['arecord', ['-q', '-t', 'raw', '-f', 'S16_LE', '-r', String(RATE), '-c', '1']], play: ['aplay', ['-q', '-t', 'raw', '-f', 'S16_LE', '-r', String(RATE), '-c', '1']] } +]; +const systemAudio = () => { + const backend = BACKENDS.find(b => onPath(b.rec[0]) && onPath(b.play[0])); + if (!backend) return null; + return { + capture(onPcm) { + const p = spawn(backend.rec[0], backend.rec[1], { stdio: ['ignore', 'pipe', 'ignore'] }); + p.on('error', () => {}); + p.stdout.on('data', onPcm); + return { stop() { try { p.kill('SIGTERM'); } catch (_) {} } }; + }, + player() { + const p = spawn(backend.play[0], backend.play[1], { stdio: ['pipe', 'ignore', 'ignore'] }); + p.on('error', () => {}); + p.stdin.on('error', () => {}); + return { + write(buf) { + if (!p.stdin.writable || p.stdin.writableLength > MAX_BACKLOG) return false; + p.stdin.write(buf); + return true; + }, + end() { try { p.stdin.end(); } catch (_) {} }, + stop() { try { p.stdin.end(); p.kill('SIGTERM'); } catch (_) {} } + }; + } + }; +}; +let audioFactory = systemAudio; + +const JPEG_SOI = Buffer.from([0xff, 0xd8, 0xff]); +const JPEG_EOI = Buffer.from([0xff, 0xd9]); +const jpegSplitter = (onJpeg) => { + let pending = Buffer.alloc(0); + return (chunk) => { + pending = pending.length ? Buffer.concat([pending, chunk]) : Buffer.from(chunk); + for (;;) { + const start = pending.indexOf(JPEG_SOI); + if (start < 0) { pending = Buffer.alloc(0); return; } + const end = pending.indexOf(JPEG_EOI, start + 3); + if (end < 0) { pending = pending.length - start > VIDEO_FRAME_MAX ? Buffer.alloc(0) : pending.subarray(start); return; } + onJpeg(Buffer.from(pending.subarray(start, end + 2))); + pending = pending.subarray(end + 2); + } + }; +}; +const VIDEO_BACKENDS = [ + { bin: 'v4l2-ctl', args: (dev) => ['--device', dev, `--set-fmt-video=width=${VIDEO_SIZE.w},height=${VIDEO_SIZE.h},pixelformat=MJPG`, `--set-parm=${VIDEO_FPS}`, '--stream-mmap', '--stream-to=-'] }, + { bin: 'gst-launch-1.0', args: (dev) => ['-q', 'v4l2src', `device=${dev}`, '!', `video/x-raw,width=${VIDEO_SIZE.w},height=${VIDEO_SIZE.h}`, '!', 'videorate', '!', `video/x-raw,framerate=${VIDEO_FPS}/1`, '!', 'videoconvert', '!', 'jpegenc', 'quality=60', '!', 'fdsink', 'fd=1'] }, + { bin: 'ffmpeg', args: (dev) => ['-loglevel', 'quiet', '-f', 'v4l2', '-video_size', `${VIDEO_SIZE.w}x${VIDEO_SIZE.h}`, '-i', dev, '-vf', `fps=${VIDEO_FPS}`, '-c:v', 'mjpeg', '-q:v', '8', '-f', 'image2pipe', '-'] } +]; +const videoDevices = () => { + try { return fs.readdirSync('/dev').filter(n => /^video\d+$/.test(n)).sort((a, b) => Number(a.slice(5)) - Number(b.slice(5))).map(n => `/dev/${n}`); } catch (_) { return []; } +}; +const systemVideo = (server, device) => { + const backend = VIDEO_BACKENDS.find(b => onPath(b.bin)); + const dev = device || videoDevices()[0]; + if (!backend || !dev) return null; + return { + capture(onJpeg) { + const p = spawn(backend.bin, backend.args(dev), { stdio: ['ignore', 'pipe', 'ignore'] }); + p.on('error', () => {}); + p.stdout.on('data', jpegSplitter(onJpeg)); + return { stop() { try { p.kill('SIGTERM'); } catch (_) {} } }; + } + }; +}; +let videoFactory = systemVideo; + +const hasBin = (bin) => (path.isAbsolute(String(bin || '')) ? fs.existsSync(bin) : onPath(bin)); +const gstQuote = (s) => `"${String(s).replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; +const STREAM_BACKENDS = [ + { + bin: () => process.env.OASIS_FFMPEG || 'ffmpeg', + retryWithoutAudio: true, + args: (file, withAudio) => [ + '-loglevel', 'quiet', '-protocol_whitelist', 'file,crypto', '-re', '-i', file, + '-map', '0:v:0', '-vf', `fps=${VIDEO_FPS},scale=${STREAM_SIZE.w}:${STREAM_SIZE.h}:force_original_aspect_ratio=decrease:flags=lanczos,pad=${STREAM_SIZE.w}:${STREAM_SIZE.h}:(ow-iw)/2:(oh-ih)/2`, + '-c:v', 'mjpeg', '-q:v', '9', '-f', 'image2pipe', 'pipe:1', + ...(withAudio ? ['-map', '0:a:0', '-ac', '1', '-ar', String(RATE), '-f', 's16le', 'pipe:3'] : []) + ] + }, + { + bin: () => process.env.OASIS_GST_LAUNCH || 'gst-launch-1.0', + args: (file) => [ + '-q', 'filesrc', `location=${gstQuote(file)}`, '!', 'decodebin', 'name=d', + 'd.', '!', 'queue', '!', 'videoconvert', '!', 'videoscale', 'add-borders=true', '!', `video/x-raw,width=${STREAM_SIZE.w},height=${STREAM_SIZE.h},pixel-aspect-ratio=1/1`, '!', 'videorate', '!', `video/x-raw,framerate=${VIDEO_FPS}/1`, '!', 'jpegenc', 'quality=50', '!', 'fdsink', 'fd=1', 'sync=true', + 'd.', '!', 'queue', '!', 'audioconvert', '!', 'audioresample', '!', `audio/x-raw,format=S16LE,rate=${RATE},channels=1`, '!', 'fdsink', 'fd=3', 'sync=true', 'async=false' + ] + } +]; +const STREAM_STALL_MS = 5000; +const STREAM_START_MS = 20000; +const systemStream = () => { + const backend = STREAM_BACKENDS.find(b => hasBin(b.bin())); + if (!backend) return null; + return { + open(file, { onJpeg, onPcm, onEnd }) { + let proc = null; + let frames = 0; + let lastAt = 0; + let ended = false; + const startedAt = Date.now(); + const watch = setInterval(() => { + const now = Date.now(); + if (frames ? now - lastAt > STREAM_STALL_MS : now - startedAt > STREAM_START_MS) finish(); + }, 1000); + if (watch.unref) watch.unref(); + const halt = () => { ended = true; clearInterval(watch); try { if (proc) proc.kill('SIGTERM'); } catch (_) {} }; + const finish = () => { if (ended) return; halt(); onEnd(); }; + const run = (withAudio) => { + const p = spawn(backend.bin(), backend.args(file, withAudio), { stdio: ['ignore', 'pipe', 'ignore', 'pipe'] }); + proc = p; + p.on('error', () => {}); + p.stdout.on('error', () => {}); + p.stdout.on('data', jpegSplitter((jpeg) => { if (ended || proc !== p) return; frames++; lastAt = Date.now(); onJpeg(jpeg); })); + if (p.stdio[3]) { + p.stdio[3].on('error', () => {}); + p.stdio[3].on('data', (pcm) => { if (!ended && proc === p) onPcm(pcm); }); + } + p.on('close', () => { + if (ended || proc !== p) return; + if (withAudio && !frames && backend.retryWithoutAudio) return run(false); + finish(); + }); + }; + run(true); + return { stop() { if (!ended) halt(); } }; + } + }; +}; +let streamFactory = systemStream; + +const videoChunks = (seq, jpeg, track = 0) => { + const n = Math.ceil(jpeg.length / VIDEO_CHUNK); + const out = []; + for (let i = 0; i < n; i++) { + const data = jpeg.subarray(i * VIDEO_CHUNK, (i + 1) * VIDEO_CHUNK); + const plain = Buffer.alloc(Math.ceil((VIDEO_HDR + data.length) / VIDEO_PAD) * VIDEO_PAD); + plain.writeUInt16BE(seq & 0xffff, 0); + plain[2] = i; + plain[3] = n | (track ? VIDEO_FILM : 0); + plain.writeUInt16BE(data.length, 4); + data.copy(plain, VIDEO_HDR); + nodeCrypto.randomFillSync(plain, VIDEO_HDR + data.length); + out.push(plain); + } + return out; +}; +const videoOff = (track = 0) => { const plain = Buffer.alloc(VIDEO_PAD); plain[3] = track ? VIDEO_FILM : 0; nodeCrypto.randomFillSync(plain, VIDEO_HDR); return plain; }; +const videoTrack = (plain) => (Buffer.isBuffer(plain) && plain.length >= VIDEO_HDR && (plain[3] & VIDEO_FILM) ? 1 : 0); +const videoAssembler = () => { + let cur = null; + return (plain) => { + if (!Buffer.isBuffer(plain) || plain.length < VIDEO_HDR) return null; + const seq = plain.readUInt16BE(0), idx = plain[2], n = plain[3] & ~VIDEO_FILM, len = plain.readUInt16BE(4); + if (!n) { cur = null; return 'off'; } + if (idx >= n || VIDEO_HDR + len > plain.length || n * VIDEO_CHUNK > VIDEO_FRAME_MAX) return null; + if (!cur || cur.seq !== seq) cur = { seq, n, parts: new Array(n), got: 0 }; + if (cur.parts[idx]) return null; + cur.parts[idx] = Buffer.from(plain.subarray(VIDEO_HDR, VIDEO_HDR + len)); + cur.got++; + if (cur.got < n) return null; + const jpeg = Buffer.concat(cur.parts); + cur = null; + return jpeg; + }; +}; + +const BEEP_MS = 700; +const playBeep = (audio, count = 1) => { + const on = (count > 1 ? 150 : BEEP_MS) * RATE / 1000; + const gap = 150 * RATE / 1000; + const pcm = Buffer.alloc((on * count + gap * (count - 1)) * 2); + let phase = 0; + for (let b = 0; b < count; b++) { + const start = b * (on + gap); + for (let k = 0; k < on; k++) { + phase += 2 * Math.PI * 1000 / RATE; + pcm.writeInt16LE(Math.round(Math.sin(phase) * 9000), (start + k) * 2); + } + } + const player = audio.player(); + player.write(pcm); + if (typeof player.end === 'function') player.end(); + else setTimeout(() => player.stop(), pcm.length / (RATE * 2) * 1000 + 300); +}; + +const chimeBusy = new WeakMap(); +const playChime = (audio, kind) => { + const notes = CHIMES[kind]; + if (!audio || !notes) return; + const now = Date.now(); + const playing = (chimeBusy.get(audio) || []).filter(t => t > now); + if (playing.length >= CHIMES_AT_ONCE) return; + playing.push(now + chimeMs(kind)); + chimeBusy.set(audio, playing); + const pcm = Buffer.alloc(Math.round(chimeMs(kind) * RATE / 1000) * 2); + let at = 0, phase = 0; + for (const [freq, ms] of notes) { + const n = Math.round(ms * RATE / 1000); + for (let k = 0; k < n && (at + k) * 2 + 1 < pcm.length; k++) { + if (freq > 0) { + phase += 2 * Math.PI * freq / RATE; + const env = Math.min(1, k / 40, (n - k) / 40); + pcm.writeInt16LE(Math.round(Math.sin(phase) * 7000 * env), (at + k) * 2); + } + } + at += n; + } + const player = audio.player(); + player.write(pcm); + if (typeof player.end === 'function') player.end(); + else setTimeout(() => player.stop(), pcm.length / (RATE * 2) * 1000 + 300); +}; +const recordingPrefix = (ref) => nodeCrypto.createHash('sha256').update(String(ref || '')).digest('hex').slice(0, 16); + +const startTone = (audio, kind) => { + const cycle = toneCycle(kind); + const player = audio.player(); + const chunk = RATE * 2 / 10; + let pos = 0; + const tick = () => { + const end = Math.min(pos + chunk, cycle.length); + player.write(cycle.subarray(pos, end)); + pos = end >= cycle.length ? 0 : end; + }; + tick(); + const t = setInterval(tick, 100); + return { stop() { clearInterval(t); player.stop(); } }; +}; + +const mixInto = (acc, pcm) => { for (let i = 0; i < acc.length && i * 2 + 1 < pcm.length; i++) acc[i] += pcm.readInt16LE(i * 2); }; +const clampPcm = (acc) => { + const out = Buffer.alloc(acc.length * 2); + for (let i = 0; i < acc.length; i++) out.writeInt16LE(Math.max(-32768, Math.min(32767, acc[i])), i * 2); + return out; +}; + +let OpusScript = null; +try { OpusScript = require('opusscript'); } catch (_) {} +let sharpLib = null; +try { sharpLib = require('./node_modules/sharp'); } catch (_) {} +const OPUS_TAG = 0xf0; +const OPUS_BITRATE = 16000; +const makeEncoder = (codec) => { + if (codec !== 'opus' || !OpusScript) return { encode: ulawEncode, free() {} }; + let enc = null; + return { + encode(pcm) { + try { + if (!enc) { enc = new OpusScript(RATE, 1, OpusScript.Application.VOIP); try { enc.encoderCTL(4002, OPUS_BITRATE); } catch (_) {} } + return Buffer.concat([Buffer.from([OPUS_TAG]), enc.encode(pcm, pcm.length >> 1)]); + } catch (_) { return ulawEncode(pcm); } + }, + free() { if (enc) { try { enc.delete(); } catch (_) {} enc = null; } } + }; +}; +const makeDecoder = () => { + let dec = null; + return { + decode(buf) { + if (buf.length === FRAME_BYTES / 2 || buf[0] !== OPUS_TAG || !OpusScript) return ulawDecode(buf); + try { + if (!dec) dec = new OpusScript(RATE, 1, OpusScript.Application.VOIP); + return Buffer.from(dec.decode(buf.subarray(1))); + } catch (_) { return Buffer.alloc(FRAME_BYTES); } + }, + free() { if (dec) { try { dec.delete(); } catch (_) {} dec = null; } } + }; +}; +const ctrlFrame = (obj) => Buffer.concat([Buffer.from([K_CTRL]), Buffer.from(JSON.stringify(obj))]); +const levelOf = (pcm) => { + const n = pcm.length >> 1; + let sum = 0; + for (let i = 0; i < n; i++) sum += Math.abs(pcm.readInt16LE(i * 2)); + return n ? sum / n : 0; +}; +const voiceGate = () => { + let floor = 0; + let hang = 0; + return (pcm) => { + const lv = levelOf(pcm); + floor = lv < floor ? lv : floor + (lv - floor) * 0.002; + if (lv > Math.max(VAD_MIN, Math.min(floor, VAD_FLOOR_MAX) * 3)) hang = VAD_HANG; + else if (hang > 0) hang--; + return hang > 0; + }; +}; + +const b64 = (buf) => Buffer.from(buf).toString('base64'); +const isFeedId = (id) => typeof id === 'string' && /^@[A-Za-z0-9+/]{43}=\.ed25519$/.test(id); +const pkOf = (id) => Buffer.from(String(id).slice(1, -8), 'base64'); +const signParts = (keys, parts) => b64(sodium.crypto_sign_detached(Buffer.from(parts.join('|')), Buffer.from(String(keys.private).replace(/\.ed25519$/, ''), 'base64'))); +const verifyParts = (id, parts, sig) => { + try { return !!sodium.crypto_sign_verify_detached(Buffer.from(String(sig), 'base64'), Buffer.from(parts.join('|')), pkOf(id)); } catch (_) { return false; } +}; +const ephKey = (v) => { + try { const b = Buffer.from(String(v), 'base64'); return b.length === 32 ? b : null; } catch (_) { return null; } +}; +const deriveKeys = (mySecret, theirPublic, callId, callerPk, calleePk) => { + const shared = sodium.crypto_scalarmult(mySecret, theirPublic); + const label = (l) => sodium.crypto_hash_sha256(Buffer.concat([Buffer.from(shared), Buffer.from(l), Buffer.from(callId, 'hex'), callerPk, calleePk])); + return { a2b: label('oasis-voices-a2b'), b2a: label('oasis-voices-b2a'), va2b: label('oasis-video-a2b'), vb2a: label('oasis-video-b2a') }; +}; +const sealer = (key) => { + const prefix = nodeCrypto.randomBytes(16); + let counter = 0n; + return (plain) => { + const nonce = Buffer.alloc(24); + prefix.copy(nonce, 0); + nonce.writeBigUInt64BE(++counter, 16); + return Buffer.concat([nonce, Buffer.from(sodium.crypto_secretbox_easy(plain, nonce, key))]); + }; +}; +const opener = (key) => { + let prefix = null; + let last = 0n; + return (frame) => { + if (!Buffer.isBuffer(frame) || frame.length < 40) return null; + const nonce = frame.subarray(0, 24); + const p = nonce.subarray(0, 16).toString('hex'); + const counter = nonce.readBigUInt64BE(16); + if ((prefix && p !== prefix) || counter <= last) return null; + const plain = sodium.crypto_secretbox_open_easy(frame.subarray(24), nonce, key); + if (!plain) return null; + prefix = p; + last = counter; + return Buffer.from(plain); + }; +}; + +module.exports = { + name: 'phone', + version: '1.0.0', + manifest: { + ring: 'async', answer: 'async', hangup: 'async', audio: 'duplex', relay: 'async', relayAudio: 'duplex', video: 'duplex', relayVideo: 'duplex', + call: 'async', accept: 'async', reject: 'async', end: 'async', mute: 'async', silence: 'async', dismiss: 'async', camera: 'async', stream: 'async', videoFrames: 'source', + viewerJoin: 'async', viewerLeave: 'async', viewerRooms: 'sync', publicFeed: 'source', + recordStop: 'async', recordCancel: 'async', + roomHub: 'duplex', roomInfo: 'async', roomAdmits: 'async', roomJoin: 'async', roomLeave: 'async', roomMute: 'async', roomHand: 'async', roomCount: 'async', + roomRecStart: 'async', roomRecStop: 'async', roomNotify: 'async', roomClearEvents: 'async', + roomState: 'sync', roomToken: 'sync', roomHubFor: 'async', + state: 'sync', available: 'sync', pubs: 'sync', events: 'source' + }, + permissions: { anonymous: { allow: ['ring', 'answer', 'hangup', 'audio', 'relay', 'relayAudio', 'video', 'relayVideo', 'roomHub', 'roomInfo', 'roomAdmits'] } }, + init(server, config) { + const keys = config.keys; + const audio = audioFactory(server); + const recBase = String((config && config.path) || os.tmpdir()); + const recDir = path.join(recBase, 'oasis', 'rooms'); + try { if (!fs.existsSync(recDir) && fs.existsSync(path.join(recBase, 'rooms-recordings'))) { fs.mkdirSync(path.dirname(recDir), { recursive: true }); fs.renameSync(path.join(recBase, 'rooms-recordings'), recDir); } } catch (_) {} + const subscribers = new Set(); + const seenCalls = new Map(); + const routes = new Map(); + const hubRooms = new Map(); + let call = null; + let room = null; + const silenced = new Set(); + + const policy = () => { + if (config.phone && typeof config.phone === 'object') return config.phone; + try { + const cfg = require('../configs/config-manager.js').getConfig(); + return { ...(cfg.phone || {}), off: !!(cfg.modules && cfg.modules.phoneMod === 'off') }; + } catch (_) { return {}; } + }; + const ringMs = Number(policy().ringMs) > 0 ? Number(policy().ringMs) : RING_MS; + const voicemailMaxMs = Number(policy().voicemailMaxMs) > 0 ? Number(policy().voicemailMaxMs) : VOICEMAIL_MAX_MS; + const pamWarnMs = Number(policy().pamWarnMs) > 0 ? Number(policy().pamWarnMs) : PAM_WARN_MS; + const roomMax = Number(policy().roomMax) > 0 ? Math.min(ROOM_MAX, Number(policy().roomMax)) : ROOM_MAX; + const roomQuietAt = Number(policy().roomQuietAt) > 0 ? Number(policy().roomQuietAt) : ROOM_QUIET_AT; + const video = videoFactory(server, policy().videoDevice); + const streamer = streamFactory(server); + const videoAllowed = () => policy().video !== false; + const videoOn = () => !!video && videoAllowed(); + const codecsOffered = () => [...(OpusScript && policy().opus !== false ? ['opus'] : []), ...(videoOn() ? ['mjpeg'] : [])]; + const pickCodec = (offer) => (codecsOffered().includes('opus') && Array.isArray(offer) && offer.includes('opus') ? 'opus' : 'ulaw'); + const pickVideo = (offer, mine = false) => (videoAllowed() && ((Array.isArray(offer) && offer.includes('mjpeg')) || mine) ? 'mjpeg' : null); + const frameWatchers = new Map(); + const publishFrame = (target, jpeg) => { const set = frameWatchers.get(target); if (set) for (const p of set) p.push(jpeg); }; + const endFrames = (target) => { + const set = frameWatchers.get(target); + if (!set) return; + frameWatchers.delete(target); + for (const p of set) { try { p.end(); } catch (_) {} } + }; + let cam = null; + let camSeq = 0; + const startCam = (onJpeg) => { + if (cam || !videoOn()) return false; + const cap = video.capture((jpeg) => { + if (!cam || jpeg.length > VIDEO_FRAME_MAX) return; + camSeq = (camSeq + 1) & 0xffff; + publishFrame('me', jpeg); + onJpeg(jpeg, camSeq); + }); + cam = { stop() { cap.stop(); } }; + return true; + }; + const stopCam = () => { if (!cam) return; const c = cam; cam = null; c.stop(); endFrames('me'); }; + const relation = (method, source, dest, done) => { + const friends = server.friends; + if (!friends || typeof friends[method] !== 'function') return done(false); + friends[method]({ source, dest }, (err, yes) => done(!err && !!yes)); + }; + const allowedCaller = (from, done) => { + const p = policy(); + if (p.off || p.dnd === true) return done(false); + relation('isBlocking', server.id, from, (blocked) => { + if (blocked) return done(false); + if (p.visibility !== 'mutuals') return done(true); + relation('isFollowing', server.id, from, (a) => { + if (!a) return done(false); + relation('isFollowing', from, server.id, (b) => done(!!b)); + }); + }); + }; + const relayAllowed = (a, b, done) => { + const p = policy(); + if (p.relay === false) return done(false); + if (p.relayOpen === true) return done(true); + relation('isFollowing', server.id, a, (x) => x ? done(true) : relation('isFollowing', server.id, b, (y) => done(!!y))); + }; + const seal = (payload, to) => { try { return ssbKeys.box(payload, [to]); } catch (_) { return null; } }; + const unseal = (sealed) => { try { return ssbKeys.unbox(String(sealed), keys) || null; } catch (_) { return null; } }; + const opened = (req) => (req && typeof req.sealed === 'string' ? unseal(req.sealed) : req); + const pubKeys = () => { + const set = new Set(); + const add = (list) => { for (const e of list || []) if (Array.isArray(e) && e[1] && e[1].type === 'pub' && e[1].key) set.add(e[1].key); }; + try { add(server.conn.dbPeers()); } catch (_) {} + try { add(server.conn.query().peersAll()); } catch (_) {} + return set; + }; + const knownPubs = () => { + const connected = Object.keys(server.peers || {}).filter(id => id !== server.id && liveRpc(id)); + if (!server.conn || typeof server.conn.query !== 'function') return connected; + const set = pubKeys(); + return connected.filter(id => set.has(id)); + }; + let routesPrunedAt = 0; + const learn = (cid, target, from, forwarded) => { + const now = Date.now(); + if (now - routesPrunedAt > 1000) { + routesPrunedAt = now; + for (const [k, v] of routes) if (now - v.ts > CLOCK_SKEW_MS * 2) routes.delete(k); + } + if (!routes.has(cid) && routes.size >= ROUTES_MAX) return { id: target, via: liveRpc(target) ? target : null }; + const r = routes.get(cid) || { sides: [], ts: now }; + let t = r.sides.find(x => x.id === target) || r.sides.find(x => x.id === null); + if (!t) { t = { id: target, via: null }; r.sides.push(t); } + t.id = target; + if (liveRpc(target)) t.via = target; + let o = r.sides.find(x => x !== t); + if (!o) { o = { id: forwarded ? null : from, via: from }; r.sides.push(o); } + else if (!o.via) o.via = from; + r.ts = now; + routes.set(cid, r); + return t; + }; + let seenPrunedAt = 0; + const directRings = new Map(); + const ringBudget = new Map(); + const trimOldest = (map, max) => { while (map.size > max) map.delete(map.keys().next().value); }; + const firstSeen = (callId) => { + const now = Date.now(); + if (now - seenPrunedAt > 1000) { + seenPrunedAt = now; + for (const [k, t] of seenCalls) if (now - t > CLOCK_SKEW_MS * 2) seenCalls.delete(k); + for (const [k, t] of directRings) if (now - t > CLOCK_SKEW_MS * 2) directRings.delete(k); + for (const [k, list] of ringBudget) { const live = list.filter(t => now - t < RING_WINDOW_MS); if (live.length) ringBudget.set(k, live); else ringBudget.delete(k); } + } + if (seenCalls.has(callId)) return false; + seenCalls.set(callId, now); + trimOldest(seenCalls, SEEN_MAX); + trimOldest(directRings, SEEN_MAX); + return true; + }; + const callerAllowed = (from) => { + const now = Date.now(); + const list = (ringBudget.get(from) || []).filter(t => now - t < RING_WINDOW_MS); + if (list.length >= RING_PER_WINDOW) { ringBudget.set(from, list); return false; } + list.push(now); + ringBudget.set(from, list); + trimOldest(ringBudget, SEEN_MAX); + return true; + }; + + const MOBILE_STATE = path.join(__dirname, '..', '..', '.phone_state.json'); + let mobileCall = null; + let mobileRoom = null; + const writeMobileState = () => { + if (process.env.OASIS_MOBILE !== '1') return; + const s = mobileCall || mobileRoom ? { ...(mobileCall || {}), room: mobileRoom ? { phase: mobileRoom.phase, cam: !!mobileRoom.cam } : null } : null; + try { fs.writeFileSync(MOBILE_STATE, JSON.stringify(s)); } catch (_) {} + }; + writeMobileState(); + const emit = (ev) => { for (const p of subscribers) p.push(ev); }; + const snapshot = () => call ? { + id: call.id, peer: call.peer, dir: call.dir, phase: call.phase, + startedAt: call.startedAt, answeredAt: call.answeredAt || 0, ringUntil: call.ringUntil || 0, + muted: !!call.muted, silenced: silenced.has(call.peer), recordingStartedAt: call.recordingStartedAt || 0, + reached: call.group ? call.legs.some(l => l.reached) : !!call.reached, why: call.why || '', codec: call.codec || '', + video: !!call.video, cam: !!call.cam, peerCam: !!call.peerCam, videoAvailable: videoOn(), + ...(call.group ? { group: true, peers: call.legs.map(l => ({ id: l.peer, phase: l.phase, reached: !!l.reached, silenced: silenced.has(l.peer) })) } : {}) + } : null; + const changed = () => { const s = snapshot(); mobileCall = s; writeMobileState(); emit({ type: 'state', state: s }); }; + const stopTone = (c) => { if (c.tone) { c.tone.stop(); c.tone = null; } }; + const stopRecorder = (c) => { clearTimeout(c.recDelay); clearTimeout(c.recWarn); if (c.recorder) { c.recorder.stop(); c.recorder = null; } clearTimeout(c.recTimer); }; + const startRecording = (c) => { + const max = voicemailMaxMs * RATE * 2 / 1000; + c.rec = { chunks: [], bytes: 0 }; + c.phase = 'recording'; + c.recordingStartedAt = Date.now() + BEEP_MS + 400; + playBeep(audio); + c.recDelay = setTimeout(() => { + if (call !== c || c.phase !== 'recording') return; + c.recordingStartedAt = Date.now(); + c.recorder = audio.capture((pcm) => { + if (!c.rec || c.rec.bytes >= max) return; + const take = Buffer.from(pcm.subarray(0, Math.max(0, max - c.rec.bytes))); + c.rec.chunks.push(take); + c.rec.bytes += take.length; + }); + if (voicemailMaxMs > pamWarnMs) c.recWarn = setTimeout(() => { if (call === c && c.phase === 'recording') playBeep(audio, 2); }, voicemailMaxMs - pamWarnMs); + c.recTimer = setTimeout(() => { + if (call !== c || c.phase !== 'recording') return; + stopRecorder(c); + c.phase = 'recorded'; + changed(); + emit({ type: 'recordLimit', id: c.id }); + }, voicemailMaxMs); + changed(); + }, BEEP_MS + 400); + }; + const finish = (outcome) => { + const c = call; + if (!c) return; + call = null; + clearTimeout(c.timer); + stopTone(c); + stopRecorder(c); + if (c.media) c.media.stop(); + stopCallVideo(c); + if (c.group) { + for (const leg of c.legs) { clearTimeout(leg.timer); if (leg.out) { try { leg.out.end(); } catch (_) {} } } + if (c.mixer) c.mixer.stop(); + for (const leg of c.legs) { if (leg.enc) leg.enc.free(); if (leg.dec) leg.dec.free(); } + } + emit({ type: 'ended', call: { id: c.id, peer: c.peer, ...(c.group ? { peers: c.legs.map(l => l.peer) } : {}), dir: c.dir, startedAt: c.startedAt, answeredAt: c.answeredAt || 0, endedAt: Date.now(), outcome } }); + changed(); + }; + const liveRpc = (id) => { + const list = server.peers && server.peers[id]; + return Array.isArray(list) ? list.find(r => r && !r.closed && r.phone) || null : null; + }; + const sendVia = (via, to, method, payload, cb = () => {}) => { + try { + if (via) { + const r = liveRpc(via); + const sealed = seal(payload, to); + if (!r || !sealed) return cb(new Error('no-connection')); + return r.phone.relay({ to, method, cid: payload.callId, sealed }, cb); + } + const r = liveRpc(to); + if (!r) return cb(new Error('no-connection')); + return r.phone[method](payload, cb); + } catch (err) { cb(err); } + }; + const notifyHangup = (c) => { + const ts = Date.now(); + const payload = { callId: c.id, from: server.id, ts, sig: signParts(keys, ['hangup', c.id, c.peer, ts]) }; + if (c.phase === 'calling' && c.dir === 'out') { + for (const via of c.paths || []) sendVia(via, c.peer, 'hangup', payload, (err) => dbg('hangup sent', sid(c.peer), via ? 'via ' + sid(via) : 'direct', err ? 'error ' + err.message : 'ok')); + } else { + sendVia(c.via || null, c.peer, 'hangup', payload, (err) => dbg('hangup sent', sid(c.peer), c.via ? 'via ' + sid(c.via) : 'direct', err ? 'error ' + err.message : 'ok')); + } + }; + const senderOf = (rpcCtx, payload) => { + const direct = rpcCtx && rpcCtx.id; + const claimed = payload && isFeedId(payload.from) ? payload.from : direct; + return { sender: claimed, via: claimed && direct && claimed !== direct ? direct : null }; + }; + const netFirst = (entries) => entries.slice().sort((x, y) => (/^onion:/.test(String(x && x[0])) ? 1 : 0) - (/^onion:/.test(String(y && y[0])) ? 1 : 0)); + const reach = (id, done) => { + const now = liveRpc(id); + if (now) return done(now); + let entries = []; + try { entries = netFirst(server.conn.query().peersAll()); } catch (_) {} + const hit = entries.find(e => Array.isArray(e) && e[0] && e[1] && e[1].key === id); + if (!hit) return done(null); + try { + server.conn.connect(hit[0], (err, rpc) => done(!err && rpc && rpc.phone ? rpc : liveRpc(id))); + } catch (_) { done(null); } + }; + const relayCandidates = (target) => knownPubs().filter(id => id !== target); + + const calleePubs = (target, done) => { + if (!server.friends || typeof server.friends.hops !== 'function' || !server.conn || typeof server.conn.query !== 'function') return done([]); + try { + server.friends.hops({ start: target, max: 1 }, (err, hops) => { + if (err || !hops) return done([]); + let entries = []; + try { entries = netFirst(server.conn.dbPeers().concat(server.conn.query().peersAll())); } catch (_) {} + const seen = new Set(); + done(entries + .filter(e => Array.isArray(e) && e[0] && e[1] && e[1].type === 'pub' && hops[e[1].key] === 1 && !liveRpc(e[1].key) && !seen.has(e[1].key) && seen.add(e[1].key)) + .slice(0, 5) + .map(e => ({ address: e[0], key: e[1].key }))); + }); + } catch (_) { done([]); } + }; + + const ringPaths = (holder, target, req, alive, onReached, legacy = false) => { + dbg('ring', sid(target), 'legacy=' + legacy, 'live-pubs=' + knownPubs().map(sid).join(',')); + reach(target, (rpc) => { + dbg('direct', sid(target), rpc ? 'reachable' : 'unreachable'); + if (!alive() || !rpc) return; + holder.paths.push(null); + if (legacy) onReached(); + sendVia(null, target, 'ring', req, (err) => { dbg('direct ring', sid(target), err ? 'error ' + err.message : 'delivered'); if (!err && alive()) onReached(); }); + }); + for (const via of relayCandidates(target)) { + holder.paths.push(via); + sendVia(via, target, 'ring', req, (err) => dbg('relay via', sid(via), err ? 'error ' + err.message : 'accepted')); + } + calleePubs(target, (pubs) => { + dbg('callee pubs', pubs.map(p => sid(p.key) + '@' + p.address).join(' ') || 'none'); + for (const entry of pubs) { + try { + server.conn.connect(entry.address, (err) => { + dbg('connect callee pub', sid(entry.key), err ? 'failed ' + err.message : 'ok'); + if (err || !alive()) return; + holder.paths.push(entry.key); + sendVia(entry.key, target, 'ring', req, (e2) => dbg('relay via callee pub', sid(entry.key), e2 ? 'error ' + e2.message : 'accepted')); + }); + } catch (_) {} + } + }); + }; + const ringRequest = (callId, eph, target) => { + const ts = Date.now(); + const ephPk = b64(eph.publicKey); + return { callId, from: server.id, to: target, ephPk, ts, sig: signParts(keys, ['ring', callId, ephPk, target, ts]), codecs: codecsOffered() }; + }; + + const legOf = (c, callId) => (c && c.group ? c.legs.find(l => l.id === callId) || null : null); + const toVoicemail = (c, why) => { + if (call !== c || c.phase !== 'calling') return; + dbg('voicemail', sid(c.peer), why, 'reached=' + !!c.reached); + clearTimeout(c.timer); + stopTone(c); + if (why !== 'busy') notifyHangup(c); + c.why = why; + startRecording(c); + changed(); + }; + const reachedCall = (c) => { + if (call !== c || c.phase !== 'calling' || c.reached) return; + dbg('reached', sid(c.peer), 'now ringing there'); + c.reached = true; + c.ringUntil = Date.now() + ringMs; + stopTone(c); + c.tone = startTone(audio, 'ringback'); + clearTimeout(c.timer); + c.timer = setTimeout(() => toVoicemail(c, 'noanswer'), ringMs); + changed(); + }; + const reachedLeg = (c, leg) => { + if (call !== c || leg.phase !== 'calling' || leg.reached) return; + leg.reached = true; + clearTimeout(leg.timer); + leg.timer = setTimeout(() => { if (call === c && leg.phase === 'calling') dropLeg(c, leg, true); }, ringMs); + if (c.phase === 'calling' && !c.ringing) { stopTone(c); c.ringing = true; c.tone = startTone(audio, 'ringback'); c.ringUntil = Date.now() + ringMs; } + changed(); + }; + const ringingAck = (c, req, from, cb) => { + const target = c && c.group ? legOf(c, req.callId) : c; + if (!target || c.dir !== 'out' || target.phase !== 'calling' || from !== target.peer || req.callId !== target.id) return cb(new Error('no-call')); + if (Math.abs(Date.now() - Number(req.ts)) > CLOCK_SKEW_MS || !verifyParts(from, ['ringing', req.callId, req.ringing, server.id, req.ts], req.sig)) return cb(new Error('rejected')); + cb(null, true); + if (req.ringing === 'busy') { + if (c.group) { target.reached = true; return dropLeg(c, target, false); } + c.reached = true; + return toVoicemail(c, 'busy'); + } + if (c.group) reachedLeg(c, target); + else reachedCall(c); + }; + const hangupLeg = (leg) => { + const ts = Date.now(); + const payload = { callId: leg.id, from: server.id, ts, sig: signParts(keys, ['hangup', leg.id, leg.peer, ts]) }; + if (leg.phase === 'calling') for (const via of leg.paths) sendVia(via, leg.peer, 'hangup', payload); + else sendVia(leg.via || null, leg.peer, 'hangup', payload); + }; + const settleGroup = (c) => { + if (call !== c) return; + if (c.legs.some(l => l.phase === 'connected' || l.phase === 'calling')) return changed(); + finish(c.answeredAt ? 'ended' : 'noanswer'); + }; + const dropLeg = (c, leg, notify) => { + if (leg.phase === 'gone') return; + if (notify) hangupLeg(leg); + clearTimeout(leg.timer); + leg.phase = 'gone'; + if (leg.out) { try { leg.out.end(); } catch (_) {} leg.out = null; } + if (leg.enc) leg.enc.free(); + if (leg.dec) leg.dec.free(); + leg.queue = []; + settleGroup(c); + }; + const startMixer = (c) => { + if (c.mixer) return; + const player = audio.player(); + const samples = FRAME_BYTES / 2; + let pending = Buffer.alloc(0); + const capture = audio.capture((pcm) => { + if (call !== c) return; + pending = pending.length ? Buffer.concat([pending, pcm]) : Buffer.from(pcm); + while (pending.length >= FRAME_BYTES) { + const mic = pending.subarray(0, FRAME_BYTES); + pending = pending.subarray(FRAME_BYTES); + const legs = c.legs.filter(l => l.phase === 'connected' && l.out); + const heard = legs.map(l => l.queue.shift() || null); + const all = new Int32Array(samples); + heard.forEach((f, i) => { if (f && !silenced.has(legs[i].peer)) mixInto(all, f); }); + player.write(clampPcm(all)); + legs.forEach((leg, i) => { + const acc = new Int32Array(samples); + if (!c.muted) mixInto(acc, mic); + heard.forEach((f, j) => { if (f && j !== i) mixInto(acc, f); }); + leg.out.push(leg.seal(leg.enc.encode(clampPcm(acc)))); + }); + } + }); + c.mixer = { stop() { capture.stop(); player.stop(); } }; + }; + const startLegMedia = (c, leg, duplex) => { + const open = opener(leg.keys.b2a); + leg.seal = sealer(leg.keys.a2b); + leg.enc = makeEncoder(leg.codec); + leg.dec = makeDecoder(); + leg.out = Pushable(() => {}); + leg.queue = []; + pull(leg.out, duplex.sink); + pull(duplex.source, pull.drain((frame) => { + const plain = open(Buffer.isBuffer(frame) ? frame : Buffer.from(frame || [])); + if (!plain || leg.phase !== 'connected') return; + leg.queue.push(leg.dec.decode(plain)); + if (leg.queue.length > MIX_QUEUE) leg.queue.shift(); + }, () => { if (call === c) dropLeg(c, leg, false); })); + startMixer(c); + }; + const groupCall = (targets, legacy, cb) => { + const startedAt = Date.now(); + const c = { id: nodeCrypto.randomBytes(16).toString('hex'), peer: targets[0], dir: 'out', group: true, phase: 'calling', startedAt, ringUntil: startedAt + ringMs, legs: [] }; + call = c; + c.tone = startTone(audio, 'connecting'); + for (const peer of targets) { + const leg = { id: nodeCrypto.randomBytes(16).toString('hex'), peer, phase: 'calling', eph: sodium.crypto_box_keypair(), paths: [], via: null, queue: [] }; + c.legs.push(leg); + leg.timer = setTimeout(() => { if (call === c && leg.phase === 'calling') dropLeg(c, leg, true); }, ringMs); + ringPaths(leg, peer, ringRequest(leg.id, leg.eph, peer), () => call === c && leg.phase === 'calling', () => reachedLeg(c, leg), legacy.has(peer)); + } + changed(); + cb(null, snapshot()); + }; + const groupAnswer = (c, req, from, via, cb) => { + const leg = legOf(c, req && req.callId); + if (!leg || leg.phase !== 'calling' || from !== leg.peer) return cb(new Error('no-call')); + const calleePk = ephKey(req.ephPk); + if (!calleePk || !verifyParts(from, ['answer', req.callId, req.ephPk, server.id, req.ts], req.sig)) return cb(new Error('rejected')); + const path = via ? liveRpc(via) : liveRpc(from); + if (!path) return cb(new Error('no-connection')); + leg.keys = deriveKeys(leg.eph.secretKey, calleePk, leg.id, Buffer.from(leg.eph.publicKey), calleePk); + leg.codec = pickCodec([req.codec]); + clearTimeout(leg.timer); + leg.via = via; + leg.phase = 'connected'; + leg.answeredAt = Date.now(); + if (c.phase === 'calling') { stopTone(c); c.phase = 'connected'; c.answeredAt = leg.answeredAt; } + cb(null, true); + changed(); + const quiet = () => {}; + startLegMedia(c, leg, via ? path.phone.relayAudio({ to: leg.peer, callId: leg.id }, quiet) : path.phone.audio({ callId: leg.id }, quiet)); + }; + + const startMedia = (c, duplex, role) => { + const seal = sealer(role === 'out' ? c.keys.a2b : c.keys.b2a); + const open = opener(role === 'out' ? c.keys.b2a : c.keys.a2b); + const out = Pushable(() => {}); + const player = audio.player(); + const enc = makeEncoder(c.codec); + const dec = makeDecoder(); + let pending = Buffer.alloc(0); + const capture = audio.capture((pcm) => { + if (call !== c || c.phase !== 'connected') return; + pending = pending.length ? Buffer.concat([pending, pcm]) : Buffer.from(pcm); + while (pending.length >= FRAME_BYTES) { + const frame = pending.subarray(0, FRAME_BYTES); + pending = pending.subarray(FRAME_BYTES); + if (!c.muted) out.push(seal(enc.encode(frame))); + } + }); + const incoming = []; + c.audioLag = () => incoming.length * FRAME_MS; + const pace = setInterval(() => { + if (call !== c) return; + if (incoming.length > MIX_QUEUE) incoming.splice(0, incoming.length - MIX_QUEUE); + const next = incoming.shift(); + if (next) player.write(next); + }, FRAME_MS); + if (pace.unref) pace.unref(); + const sink = pull.drain((frame) => { + const plain = open(Buffer.isBuffer(frame) ? frame : Buffer.from(frame || [])); + if (plain && !silenced.has(c.peer)) incoming.push(dec.decode(plain)); + }, () => { if (call === c) finish('ended'); }); + c.media = { stop() { clearInterval(pace); try { out.end(); } catch (_) {} capture.stop(); player.stop(); enc.free(); dec.free(); } }; + if (!duplex) return { source: out, sink }; + pull(out, duplex.sink); + pull(duplex.source, sink); + return null; + }; + + const peerFrameSeen = (c) => { + clearTimeout(c.peerTimer); + c.peerTimer = setTimeout(() => { if (call === c && c.peerCam) { c.peerCam = false; endFrames(c.peer); changed(); } }, VIDEO_HOLD_MS); + if (c.peerTimer.unref) c.peerTimer.unref(); + if (!c.peerCam) { c.peerCam = true; changed(); } + }; + const startVideo = (c, duplex, role) => { + const seal = sealer(role === 'out' ? c.keys.va2b : c.keys.vb2a); + const open = opener(role === 'out' ? c.keys.vb2a : c.keys.va2b); + const out = Pushable(() => {}); + const assemble = videoAssembler(); + const sink = pull.drain((frame) => { + const plain = open(Buffer.isBuffer(frame) ? frame : Buffer.from(frame || [])); + if (!plain || call !== c) return; + const got = assemble(plain); + if (got === 'off') { clearTimeout(c.peerTimer); if (c.peerCam) { c.peerCam = false; endFrames(c.peer); changed(); } return; } + if (!got) return; + peerFrameSeen(c); + const hold = setTimeout(() => { if (call === c && c.peerCam) publishFrame(c.peer, got); }, typeof c.audioLag === 'function' ? c.audioLag() : 0); + if (hold.unref) hold.unref(); + }, () => { if (call === c) stopCallVideo(c); }); + c.vmedia = { out, seal, stop() { try { out.end(); } catch (_) {} } }; + if (!duplex) return { source: out, sink }; + pull(out, duplex.sink); + pull(duplex.source, sink); + return null; + }; + const stopCallVideo = (c) => { + clearTimeout(c.peerTimer); + if (c.cam) { c.cam = false; stopCam(); } + if (c.vmedia) { c.vmedia.stop(); c.vmedia = null; } + endFrames(c.peer); + c.peerCam = false; + }; + const sendCallVideo = (c, plain) => { if (c.vmedia) c.vmedia.out.push(c.vmedia.seal(plain)); }; + + const deliver = (m, frame, droppable) => { + if (droppable && m.sent - m.acked > HUB_LAG_MAX) return; + m.sent++; + m.out.push(frame); + }; + const hubBroadcast = (r, from, frame, droppable) => { + for (const [slot, m] of r.members) { + if (slot !== from) deliver(m, frame, droppable); + } + }; + const hubHumans = (h) => [...h.members.values()].filter(m => !m.viewer).length; + const hubAttach = (rid, id, out, scope = null, viewer = false) => { + let r = hubRooms.get(rid); + if (!r) { + if (hubRooms.size >= HUB_ROOMS_MAX) return null; + if (scope && [...hubRooms.values()].filter(h => h.scope === scope).length >= HUB_RIDS_PER_ROOM) return null; + r = { members: new Map(), scope }; + hubRooms.set(rid, r); + } + if (scope && r.scope !== scope) return null; + for (const old of [...r.members.values()]) if (old.id === id) old.kick(); + if (!hubRooms.has(rid)) hubRooms.set(rid, r); + if (r.members.size >= roomMax) return null; + let slot = 0; + while (r.members.has(slot)) slot++; + const peers = [...r.members].map(([s, m]) => (m.viewer ? { slot: s, id: m.id, viewer: true } : { slot: s, id: m.id })); + const m = { id, out, sent: 0, acked: 0, viewer: !!viewer }; + m.leave = () => { + if (r.members.get(slot) !== m) return; + r.members.delete(slot); + if (r.members.size) hubBroadcast(r, slot, ctrlFrame({ t: 'leave', slot }), false); + else if (hubRooms.get(rid) === r) hubRooms.delete(rid); + }; + m.kick = () => { m.leave(); try { out.end(); } catch (_) {} }; + m.handle = (frame) => { + if (r.members.get(slot) !== m || frame.length < 2) return; + const kind = frame[0]; + if (kind === K_VIDEO) { + if (frame.length > ROOM_VIDEO_FRAME_MAX || policy().video === false) return; + const at = Date.now(); + if (at - (m.vwindowAt || 0) >= 1000) { m.vwindowAt = at; m.vbytes = 0; } + m.vbytes = (m.vbytes || 0) + frame.length; + if (m.vbytes > ROOM_VIDEO_BYTES_PER_S) return; + hubBroadcast(r, slot, Buffer.concat([Buffer.from([K_VIDEO, slot]), frame.subarray(1)]), true); + return; + } + if (frame.length > ROOM_FRAME_MAX) return; + if (kind === K_CTRL) { + let msg = null; + try { msg = JSON.parse(frame.subarray(1).toString('utf8')); } catch (_) {} + if (msg && msg.t === 'bye') return m.kick(); + const n = msg && msg.t === 'ack' ? Number(msg.n) : NaN; + if (Number.isFinite(n) && n > m.acked && n <= m.sent) m.acked = n; + return; + } + const now = Date.now(); + if (now - (m.windowAt || 0) >= 1000) { m.windowAt = now; m.windowCount = 0; } + if (++m.windowCount > ROOM_RATE_MAX) return; + if (kind === K_AUDIO || kind === K_DATA) { + hubBroadcast(r, slot, Buffer.concat([Buffer.from([kind, slot]), frame.subarray(1)]), true); + } else if (kind === K_KEY && frame.length > 2 && frame[1] !== slot) { + const target = r.members.get(frame[1]); + if (target) deliver(target, Buffer.concat([Buffer.from([K_KEY, slot]), frame.subarray(2)]), false); + } + }; + r.members.set(slot, m); + deliver(m, ctrlFrame({ t: 'hello', slot, peers, max: roomMax, video: policy().video !== false }), false); + hubBroadcast(r, slot, ctrlFrame(viewer ? { t: 'join', slot, id, viewer: true } : { t: 'join', slot, id }), false); + return m; + }; + const hubDuplex = (from, opts) => { + let member = null; + let closed = false; + const out = Pushable(() => { closed = true; if (member) member.leave(); }); + const sink = pull.drain((frame) => { + if (member) member.handle(Buffer.isBuffer(frame) ? frame : Buffer.from(frame || [])); + }, () => { closed = true; if (member) member.leave(); try { out.end(); } catch (_) {} }); + const refuse = (t) => { out.push(ctrlFrame({ t })); out.end(); }; + const rid = String((opts && opts.rid) || ''); + const roomId = String((opts && opts.roomId) || ''); + const tokenOk = roomId ? (RID.test(roomId) && verifyParts(opts.owner, ['room', roomId], opts.token)) : verifyParts(opts.owner, ['room', rid], opts.token); + if (!from || !RID.test(rid) || !isFeedId(opts.owner) || !tokenOk) { + refuse('refused'); + return { source: out, sink }; + } + const admit = () => { + if (closed) return; + member = hubAttach(rid, from, out, roomId ? `${opts.owner}|${roomId}` : null, opts.viewer === true); + if (!member) refuse('full'); + }; + if (opts.owner === server.id || from === server.id) admit(); + else relayAllowed(opts.owner, from, (ok) => (ok ? admit() : refuse('refused'))); + return { source: out, sink }; + }; + + const viewers = new Map(); + const active = (r) => !!r && (r === room || viewers.get(r.rid) === r); + const touched = (r) => { if (r === room) roomChanged(); }; + const humansIn = (r) => [...r.peers.values()].filter(p => !p.viewer).length; + const roomSnapshot = () => { + const r = room; + if (!r) return null; + const now = Date.now(); + return { + rid: r.rid, ref: r.ref, title: r.title, phase: r.phase, joinedAt: r.joinedAt, muted: !!r.muted, hand: !!r.hand, handSince: r.hand ? r.handAt : 0, + secure: !!r.kid, count: humansIn(r) + 1, max: r.max, public: [...r.peers.values()].some(p => p.viewer), speaking: now - r.sentAt < 1000, + cam: !!r.cam, videoAvailable: videoOn() && r.hubVideo === true, grid: !!sharpLib, gridOrder: gridIds(r), + streaming: r.stream ? r.stream.name : null, streamAvailable: fileStreamOn() && r.hubVideo === true, + notify: !!r.notify, recording: !!r.rec, recordingSince: r.rec ? r.rec.at : 0, recordingBy: [...r.recPeers], + events: r.notify ? r.events.slice(-ROOM_EVENTS_MAX) : [], + peers: [...r.peers.values()].map(p => ({ id: p.id, speaking: now - p.heardAt < 1000, muted: now - p.mutedAt < MUTE_TTL_MS, hand: now - (p.handBeat || 0) < MUTE_TTL_MS, handSince: p.handSince || 0, silenced: silenced.has(p.id), recording: r.recPeers.has(p.id), cam: now - (p.frameAt || 0) < VIDEO_HOLD_MS, film: now - (p.filmAt || 0) < VIDEO_HOLD_MS, viewer: !!p.viewer })) + }; + }; + const roomChanged = () => { const r = roomSnapshot(); mobileRoom = r; writeMobileState(); emit({ type: 'room', room: r }); }; + const pushEvent = (r, t, id) => { + r.events.push({ t, id, ts: Date.now() }); + if (r.events.length > ROOM_EVENTS_MAX) r.events.splice(0, r.events.length - ROOM_EVENTS_MAX); + touched(r); + }; + const startRoomRec = (r) => { + if (!r || r.rec || r.phase !== 'live') return false; + try { + fs.mkdirSync(recDir, { recursive: true, mode: 0o700 }); + try { fs.chmodSync(recDir, 0o700); } catch (_) {} + const file = path.join(recDir, `${recordingPrefix(r.ref)}-${Date.now()}.wav`); + const fd = fs.openSync(file, 'w', 0o600); + fs.writeSync(fd, wavFile(Buffer.alloc(0))); + r.rec = { fd, file, bytes: 0, at: Date.now() }; + } catch (_) { return false; } + sendRoomData(r, { t: 'rec', on: true }); + playChime(audio, 'recStart'); + pushEvent(r, 'recStart', server.id); + return true; + }; + const stopRoomRec = (r) => { + if (!r || !r.rec) return false; + const rec = r.rec; + r.rec = null; + try { + const h = Buffer.alloc(4); + h.writeUInt32LE(36 + rec.bytes, 0); fs.writeSync(rec.fd, h, 0, 4, 4); + h.writeUInt32LE(rec.bytes, 0); fs.writeSync(rec.fd, h, 0, 4, 40); + fs.closeSync(rec.fd); + if (!rec.bytes) fs.unlinkSync(rec.file); + } catch (_) {} + if (room === r) { + sendRoomData(r, { t: 'rec', on: false }); + playChime(audio, 'recStop'); + pushEvent(r, 'recStop', server.id); + } + if (rec.bytes) emit({ type: 'roomRecorded', room: { ref: r.ref, title: r.title }, file: rec.file, bytes: rec.bytes, durationMs: Math.round(rec.bytes / (RATE * 2) * 1000) }); + return true; + }; + const keeperSlot = (r) => r.order[0]; + const trimKeys = (r) => { while (r.keys.size > 3) r.keys.delete(r.keys.keys().next().value); }; + const useKey = (r, kid, key) => { + if (r.kid === kid && r.sealer && r.keys.get(kid) && r.keys.get(kid).equals(key)) return; + r.keys.set(kid, key); + trimKeys(r); + r.kid = kid; + r.sealer = sealer(key); + sendCodecs(r); + }; + const newRoomKey = (r) => useKey(r, nodeCrypto.randomBytes(4).toString('hex'), nodeCrypto.randomBytes(32)); + const sendRoomKey = (r, slot) => { + const p = r.peers.get(slot); + const key = r.kid && r.keys.get(r.kid); + if (!p || !key) return; + const ts = Date.now(); + const sealed = seal({ rid: r.rid, kid: r.kid, key: b64(key), from: server.id, ts, sig: signParts(keys, ['roomkey', r.rid, r.kid, p.id, ts]) }, p.id); + if (sealed) r.out.push(Buffer.concat([Buffer.from([K_KEY, slot]), Buffer.from(sealed)])); + }; + const peerEntry = (id, viewer = false) => ({ id, viewer: !!viewer, queue: [], openers: new Map(), heardAt: 0, mutedAt: 0, dec: null, opus: false, greeted: false }); + const dropPeer = (r, slot) => { const p = r.peers.get(slot); if (p) { if (p.dec) p.dec.free(); clearTimeout(p.vtimer); clearTimeout(p.ftimer); endFrames(r.tag + p.id); endFrames(r.tag + filmKey(p.id)); gridNote(r, p.id, null); gridNote(r, filmKey(p.id), null); } r.peers.delete(slot); }; + const sendCodecs = (r) => { if (r.out && r.sealer && codecsOffered().length) sendRoomData(r, { t: 'codecs', codecs: codecsOffered() }); }; + const onRoomCtrl = (r, msg) => { + if (msg.t === 'hello' && r.slot === null) { + r.slot = Number(msg.slot); + r.max = Number(msg.max) > 0 ? Number(msg.max) : ROOM_MAX; + r.hubVideo = msg.video === true; + for (const p of Array.isArray(msg.peers) ? msg.peers : []) { + if (!p || !isFeedId(p.id)) continue; + r.peers.set(Number(p.slot), peerEntry(p.id, p.viewer === true)); + r.order.push(Number(p.slot)); + } + r.order.push(r.slot); + if (r.peers.size >= roomQuietAt) r.muted = true; + if (!r.static && keeperSlot(r) === r.slot) newRoomKey(r); + r.phase = 'live'; + if (r.ready) return r.ready(null); + } else if (msg.t === 'join' && isFeedId(msg.id)) { + const slot = Number(msg.slot); + dropPeer(r, slot); + r.peers.set(slot, peerEntry(msg.id, msg.viewer === true)); + r.order = r.order.filter(s => s !== slot).concat(slot); + if (!r.static && keeperSlot(r) === r.slot) sendRoomKey(r, slot); + if (r.notify && msg.viewer !== true) playChime(audio, 'join'); + pushEvent(r, 'join', msg.id); + } else if (msg.t === 'leave') { + const slot = Number(msg.slot); + const wasKeeper = keeperSlot(r) === slot; + const gone = r.peers.get(slot); + dropPeer(r, slot); + r.order = r.order.filter(s => s !== slot); + if (gone) { + if (r.recPeers.delete(gone.id)) pushEvent(r, 'recStop', gone.id); + if (r.notify) playChime(audio, 'leave'); + pushEvent(r, 'leave', gone.id); + } + if (!r.static && keeperSlot(r) === r.slot && (wasKeeper || !r.kid)) { + newRoomKey(r); + for (const s of r.peers.keys()) sendRoomKey(r, s); + } + } else if (msg.t === 'full' || msg.t === 'refused') { + if (r.ready) return r.ready(new Error(msg.t)); + } + touched(r); + }; + const onRoomKey = (r, slot, payload) => { + const p = r.peers.get(slot); + if (r.static || !p || keeperSlot(r) !== slot) return; + const msg = unseal(payload.toString('utf8')); + if (!msg || msg.rid !== r.rid || msg.from !== p.id || !/^[0-9a-f]{8}$/.test(String(msg.kid))) return; + if (!verifyParts(p.id, ['roomkey', r.rid, msg.kid, server.id, msg.ts], msg.sig)) return; + const key = Buffer.from(String(msg.key || ''), 'base64'); + if (key.length !== 32) return; + useKey(r, msg.kid, key); + touched(r); + }; + const openRoomFrame = (r, p, payload) => { + if (!p || payload.length < 44) return null; + const kid = payload.subarray(0, 4).toString('hex'); + const key = r.keys.get(kid); + if (!key) return null; + let open = p.openers.get(kid); + if (!open) { open = opener(key); p.openers.set(kid, open); } + return open(payload.subarray(4)); + }; + const sendRoomData = (r, msg) => { + if (!r.sealer) return; + r.out.push(Buffer.concat([Buffer.from([K_DATA]), Buffer.from(r.kid, 'hex'), r.sealer(Buffer.from(JSON.stringify(msg)))])); + r.beaconAt = Date.now(); + }; + const onRoomAudio = (r, slot, payload) => { + const p = r.peers.get(slot); + const plain = openRoomFrame(r, p, payload); + if (!plain) return; + if (!p.dec) p.dec = makeDecoder(); + p.queue.push(p.dec.decode(plain)); + if (p.queue.length > MIX_QUEUE) p.queue.shift(); + p.heardAt = Date.now(); + p.mutedAt = 0; + noteMute(r, p, false); + }; + const noteMute = (r, p, on, quiet = false) => { + if (!!p.muted === on) return; + p.muted = on; + if (quiet) return touched(r); + const now = Date.now(); + const announce = now - (p.muteToggleAt || 0) >= REC_TOGGLE_MIN_MS; + p.muteToggleAt = now; + if (!announce) return touched(r); + if (r.notify) playChime(audio, on ? 'mute' : 'unmute'); + pushEvent(r, on ? 'mute' : 'unmute', p.id); + }; + const noteHand = (r, p, on, at, quiet = false) => { + if (!!p.hand === on) return; + p.hand = on; + const now = Date.now(); + p.handSince = on ? (quiet && Number.isFinite(at) && at > 0 ? Math.min(at, now) : now) : 0; + if (!on || quiet) return touched(r); + const announce = now - (p.handToggleAt || 0) >= REC_TOGGLE_MIN_MS; + p.handToggleAt = now; + if (!announce) return touched(r); + if (r.notify) playChime(audio, 'hand'); + pushEvent(r, 'hand', p.id); + }; + const onRoomData = (r, slot, payload) => { + const p = r.peers.get(slot); + const plain = openRoomFrame(r, p, payload); + if (!plain) return; + let msg = null; + try { msg = JSON.parse(plain.toString('utf8')); } catch (_) {} + if (msg && msg.t === 'codecs') { + p.opus = Array.isArray(msg.codecs) && msg.codecs.includes('opus'); + if (!p.greeted) { p.greeted = true; sendCodecs(r); } + return; + } + if (msg && msg.t === 'rec') { + const was = r.recPeers.has(p.id); + if (!!msg.on === was) return; + const now = Date.now(); + const announce = now - (p.recToggleAt || 0) >= REC_TOGGLE_MIN_MS; + p.recToggleAt = now; + if (msg.on) r.recPeers.add(p.id); else r.recPeers.delete(p.id); + if (announce) { playChime(audio, msg.on ? 'recStart' : 'recStop'); pushEvent(r, msg.on ? 'recStart' : 'recStop', p.id); } + else touched(r); + return; + } + if (msg && msg.t === 'hand') { + p.handBeat = msg.on ? Date.now() : 0; + noteHand(r, p, !!msg.on, Number(msg.at), msg.beacon === true); + return; + } + if (!msg || msg.t !== 'mute') return; + p.mutedAt = msg.on ? Date.now() : 0; + noteMute(r, p, !!msg.on, msg.beacon === true); + }; + const gridIds = (r) => { + const now = Date.now(); + const ids = []; + if (r.cam) ids.push(server.id); + if (r.stream) ids.push(filmKey(server.id)); + for (const slot of r.order) { + const p = r.peers.get(slot); + if (!p) continue; + if (now - (p.frameAt || 0) < VIDEO_HOLD_MS) ids.push(p.id); + if (now - (p.filmAt || 0) < VIDEO_HOLD_MS) ids.push(filmKey(p.id)); + } + return ids; + }; + const newGrid = () => ({ frames: new Map(), busy: false, dirty: false, timer: null }); + const composeGrid = async (r) => { + const g = r.grid; + const ids = gridIds(r).filter(id => g.frames.has(id)); + if (!ids.length) { + if (r.tag && !g.empty) { g.empty = true; const b = await blankFrame(); if (b && active(r)) publishFrame(r.tag + 'grid', b); } + return; + } + g.empty = false; + if (ids.length === 1 && ids[0] !== server.id) { if (active(r)) publishFrame(r.tag + 'grid', g.frames.get(ids[0])); return; } + const size = ids.length <= GRID_LARGE_MAX ? GRID_TILE_LARGE : VIDEO_SIZE; + const cols = Math.ceil(Math.sqrt(ids.length)); + const rows = Math.ceil(ids.length / cols); + const tiles = []; + for (let i = 0; i < ids.length; i++) { + try { + let tile = sharpLib(g.frames.get(ids[i])).resize(size.w, size.h, { fit: 'contain', background: '#000000' }); + if (ids[i] === server.id) tile = tile.flop(); + tiles.push({ input: await tile.toBuffer(), left: (i % cols) * size.w, top: Math.floor(i / cols) * size.h }); + } catch (_) {} + } + if (!tiles.length) return; + const jpeg = await sharpLib({ create: { width: cols * size.w, height: rows * size.h, channels: 3, background: '#000000' } }).composite(tiles).jpeg({ quality: 80 }).toBuffer(); + if (active(r)) publishFrame(r.tag + 'grid', jpeg); + }; + const gridTick = (r) => { + const g = r && r.grid; + if (!g || !active(r) || !sharpLib || g.busy || !g.dirty || !frameWatchers.has(r.tag + 'grid')) return; + g.dirty = false; + g.busy = true; + composeGrid(r).catch(() => {}).then(() => { g.busy = false; }); + }; + const gridNote = (r, id, jpeg) => { if (!r || !r.grid) return; if (jpeg) r.grid.frames.set(id, jpeg); else r.grid.frames.delete(id); r.grid.dirty = true; }; + const startGrid = (r) => { if (!sharpLib || !r.grid || r.grid.timer) return; r.grid.timer = setInterval(() => gridTick(r), Math.round(1000 / VIDEO_FPS)); if (r.grid.timer.unref) r.grid.timer.unref(); }; + const stopGrid = (r) => { if (!r || !r.grid) return; clearInterval(r.grid.timer); r.grid.timer = null; r.grid.frames.clear(); r.grid.dirty = false; endFrames(r.tag + 'grid'); }; + const onRoomVideo = (r, slot, payload) => { + const p = r.peers.get(slot); + if (!videoAllowed()) return; + const plain = openRoomFrame(r, p, payload); + if (!plain) return; + const film = videoTrack(plain) === 1; + const key = film ? filmKey(p.id) : p.id; + const at = film ? 'filmAt' : 'frameAt'; + const timer = film ? 'ftimer' : 'vtimer'; + const asm = film ? 'fasm' : 'vasm'; + if (!p[asm]) p[asm] = videoAssembler(); + const got = p[asm](plain); + if (got === 'off') { clearTimeout(p[timer]); if (p[at]) { p[at] = 0; endFrames(r.tag + key); gridNote(r, key, null); touched(r); } return; } + if (!got) return; + const fresh = !(Date.now() - (p[at] || 0) < VIDEO_HOLD_MS); + p[at] = Date.now(); + clearTimeout(p[timer]); + p[timer] = setTimeout(() => { if (active(r)) { endFrames(r.tag + key); gridNote(r, key, null); touched(r); } }, VIDEO_HOLD_MS); + if (p[timer].unref) p[timer].unref(); + const hold = setTimeout(() => { if (!active(r) || !(Date.now() - (p[at] || 0) < VIDEO_HOLD_MS)) return; publishFrame(r.tag + key, got); gridNote(r, key, got); }, p.queue.length * FRAME_MS); + if (hold.unref) hold.unref(); + if (fresh) touched(r); + }; + const sendRoomVideo = (r, plain) => { + if (!r.sealer || !r.hubVideo) return; + r.out.push(Buffer.concat([Buffer.from([K_VIDEO]), Buffer.from(r.kid, 'hex'), r.sealer(plain)])); + }; + const fileStreamOn = () => videoAllowed() && !!streamer; + const streamFile = (file) => { + const raw = String(file || ''); + if (!path.isAbsolute(raw)) return null; + try { const full = fs.realpathSync(raw); return fs.statSync(full).isFile() ? full : null; } catch (_) { return null; } + }; + const filmKey = (id) => `film:${id}`; + const videoBudget = (r, chunks) => { + const bytes = chunks.reduce((n, c) => n + c.length + ROOM_VIDEO_CHUNK_EXTRA, 0); + const now = Date.now(); + const sent = r.vsent || (r.vsent = []); + while (sent.length && now - sent[0][0] >= 1000) sent.shift(); + if (sent.reduce((n, e) => n + e[1], 0) + bytes > ROOM_VIDEO_SEND_MAX) return false; + sent.push([now, bytes]); + return true; + }; + const sendRoomJpeg = (r, seq, jpeg, track = 0) => { + const chunks = videoChunks(seq, jpeg, track); + if (!videoBudget(r, chunks)) return; + for (const plain of chunks) sendRoomVideo(r, plain); + }; + const stopRoomStream = (r, notify = true) => { + const s = r && r.stream; + if (!s) return false; + r.stream = null; + try { if (s.handle) s.handle.stop(); } catch (_) {} + endFrames('film:me'); + gridNote(r, filmKey(server.id), null); + if (room === r) sendRoomVideo(r, videoOff(1)); + if (notify) roomChanged(); + return true; + }; + const startRoomStream = (r, file, name) => { + if (!r || r.phase !== 'live' || !r.hubVideo || !fileStreamOn()) return false; + stopRoomStream(r, false); + const s = { name: String(name || '').slice(0, 120), pcm: Buffer.alloc(0), seq: 0, handle: null }; + r.stream = s; + try { + s.handle = streamer.open(file, { + onJpeg: (jpeg) => { + if (r.stream !== s || room !== r || !Buffer.isBuffer(jpeg) || jpeg.length > VIDEO_FRAME_MAX) return; + s.seq = (s.seq + 1) & 0xffff; + publishFrame('film:me', jpeg); + gridNote(r, filmKey(server.id), jpeg); + sendRoomJpeg(r, s.seq, jpeg, 1); + }, + onPcm: (pcm) => { + if (r.stream !== s || !pcm || !pcm.length) return; + s.pcm = s.pcm.length ? Buffer.concat([s.pcm, pcm]) : Buffer.from(pcm); + const drop = (s.pcm.length - RATE * 2) & ~1; + if (drop > 0) s.pcm = s.pcm.subarray(drop); + }, + onEnd: () => { if (r.stream === s) stopRoomStream(r); } + }); + } catch (_) { s.handle = null; } + if (!s.handle || r.stream !== s) { if (r.stream === s) r.stream = null; return false; } + return true; + }; + const streamFrame = (r) => { + const s = r.stream; + if (!s || s.pcm.length < FRAME_BYTES) return null; + const f = s.pcm.subarray(0, FRAME_BYTES); + s.pcm = s.pcm.subarray(FRAME_BYTES); + return f; + }; + const onRoomFrame = (r, frame) => { + if (!active(r)) return; + const buf = Buffer.isBuffer(frame) ? frame : Buffer.from(frame || []); + if (!buf.length) return; + r.received = (r.received || 0) + 1; + if (r.received - (r.ackedAt || 0) >= ROOM_ACK_EVERY) { r.ackedAt = r.received; r.out.push(ctrlFrame({ t: 'ack', n: r.received })); } + if (buf[0] === K_CTRL) { + let msg = null; + try { msg = JSON.parse(buf.subarray(1).toString('utf8')); } catch (_) {} + if (msg && typeof msg === 'object') onRoomCtrl(r, msg); + return; + } + if (buf.length < 3) return; + if (buf[0] === K_AUDIO) onRoomAudio(r, buf[1], buf.subarray(2)); + else if (buf[0] === K_VIDEO) onRoomVideo(r, buf[1], buf.subarray(2)); + else if (buf[0] === K_DATA) onRoomData(r, buf[1], buf.subarray(2)); + else if (buf[0] === K_KEY) onRoomKey(r, buf[1], buf.subarray(2)); + }; + const startRoomMedia = (r) => { + const player = audio.player(); + const gate = voiceGate(); + const samples = FRAME_BYTES / 2; + let pending = Buffer.alloc(0); + const capture = audio.capture((pcm) => { + if (room !== r) return; + pending = pending.length ? Buffer.concat([pending, pcm]) : Buffer.from(pcm); + while (pending.length >= FRAME_BYTES) { + const mic = pending.subarray(0, FRAME_BYTES); + pending = pending.subarray(FRAME_BYTES); + const acc = new Int32Array(samples); + for (const p of r.peers.values()) { const f = p.queue.shift(); if (f && !silenced.has(p.id)) mixInto(acc, f); } + const film = streamFrame(r); + if (film) mixInto(acc, film); + player.write(clampPcm(acc)); + if (r.rec) { + const both = Int32Array.from(acc); + if (!r.muted) mixInto(both, mic); + const take = clampPcm(both); + try { fs.writeSync(r.rec.fd, take); r.rec.bytes += take.length; } catch (_) { stopRoomRec(r); } + if (r.rec && r.rec.bytes >= ROOM_REC_MAX_BYTES) stopRoomRec(r); + if (r.rec && r.sealer && Date.now() - (r.recBeaconAt || 0) >= MUTE_BEACON_MS) { sendRoomData(r, { t: 'rec', on: true }); r.recBeaconAt = Date.now(); } + } + const voiced = gate(mic); + if (r.muted && r.sealer && Date.now() - (r.beaconAt || 0) >= MUTE_BEACON_MS) sendRoomData(r, { t: 'mute', on: true, beacon: true }); + if (r.hand && r.sealer && Date.now() - (r.handBeaconAt || 0) >= MUTE_BEACON_MS) { sendRoomData(r, { t: 'hand', on: true, at: r.handAt, beacon: true }); r.handBeaconAt = Date.now(); } + const talk = !r.muted && voiced; + if (!r.sealer || (!talk && !film)) continue; + r.sentAt = Date.now(); + let send = mic; + if (film) { const mix = new Int32Array(samples); mixInto(mix, film); if (talk) mixInto(mix, mic); send = clampPcm(mix); } + const opus = codecsOffered().includes('opus') && r.peers.size > 0 && [...r.peers.values()].every(p => p.opus); + if (opus && !r.enc) r.enc = makeEncoder('opus'); + r.out.push(Buffer.concat([Buffer.from([K_AUDIO]), Buffer.from(r.kid, 'hex'), r.sealer(opus ? r.enc.encode(send) : ulawEncode(send))])); + } + }); + r.media = { stop() { capture.stop(); player.stop(); } }; + }; + const closeDuplex = (duplex) => { + if (!duplex) return; + try { duplex.source(true, () => {}); } catch (_) {} + try { pull(pull.empty(), duplex.sink); } catch (_) {} + }; + const leaveRoom = (r, outcome) => { + if (!r || room !== r) return; + if (r.ready) { r.ready(new Error('left')); return; } + stopRoomRec(r); + room = null; + clearTimeout(r.timer); + stopRoomStream(r, false); + if (r.cam) { r.cam = false; stopCam(); } + for (const p of r.peers.values()) { clearTimeout(p.vtimer); clearTimeout(p.ftimer); endFrames(p.id); endFrames(filmKey(p.id)); } + stopGrid(r); + if (r.media) r.media.stop(); + if (r.enc) { r.enc.free(); r.enc = null; } + for (const p of r.peers.values()) if (p.dec) p.dec.free(); + try { r.out.push(ctrlFrame({ t: 'bye' })); } catch (_) {} + try { r.out.end(); } catch (_) {} + if (outcome) emit({ type: 'roomEnded', room: { rid: r.rid, ref: r.ref, title: r.title, joinedAt: r.joinedAt, endedAt: Date.now(), outcome } }); + roomChanged(); + }; + const openHub = (opts, req, attach) => { + if (opts.hub === server.id) return attach(hubDuplex(server.id, req)); + const viaRpc = (rpc) => attach(rpc && rpc.phone && typeof rpc.phone.roomHub === 'function' ? rpc.phone.roomHub(req, () => {}) : null); + const live = liveRpc(opts.hub); + if (live) return viaRpc(live); + if (typeof opts.address === 'string' && opts.address) { + try { + return server.conn.connect(opts.address, (err, rpc) => { + const got = !err && rpc && rpc.phone ? rpc : liveRpc(opts.hub); + return got ? viaRpc(got) : reach(opts.hub, viaRpc); + }); + } catch (_) {} + } + reach(opts.hub, viaRpc); + }; + let blank = null; + const blankFrame = async () => { + if (blank || !sharpLib) return blank; + try { blank = await sharpLib({ create: { width: GRID_TILE_LARGE.w, height: GRID_TILE_LARGE.h, channels: 3, background: '#000000' } }).jpeg().toBuffer(); } catch (_) { blank = null; } + return blank; + }; + const soundKey = (r) => r.tag + 'sound'; + const dropViewer = (r) => { + if (!r || viewers.get(r.rid) !== r) return; + viewers.delete(r.rid); + clearTimeout(r.timer); + clearInterval(r.mixTimer); + stopGrid(r); + endFrames(soundKey(r)); + for (const p of r.peers.values()) { if (p.dec) p.dec.free(); clearTimeout(p.vtimer); clearTimeout(p.ftimer); } + try { r.out.push(ctrlFrame({ t: 'bye' })); } catch (_) {} + try { r.out.end(); } catch (_) {} + }; + const startViewerMedia = (r) => { + const samples = FRAME_BYTES / 2; + r.mixTimer = setInterval(() => { + if (viewers.get(r.rid) !== r) return; + const acc = new Int32Array(samples); + for (const p of r.peers.values()) { const f = p.queue.shift(); if (f) mixInto(acc, f); } + if (frameWatchers.has(soundKey(r))) publishFrame(soundKey(r), clampPcm(acc)); + if (r.sealer && Date.now() - (r.beaconAt || 0) >= MUTE_BEACON_MS) sendRoomData(r, { t: 'mute', on: true, beacon: true }); + }, FRAME_MS); + if (r.mixTimer.unref) r.mixTimer.unref(); + }; + const staticKey = (rid, secret) => { + const key = Buffer.from(sodium.crypto_hash_sha256(Buffer.concat([Buffer.from('oasis-room|'), Buffer.from(rid, 'hex'), Buffer.from(String(secret))]))); + return { kid: Buffer.from(sodium.crypto_hash_sha256(key)).subarray(0, 4).toString('hex'), key }; + }; + + if (server.close && typeof server.close.hook === 'function') { + server.close.hook(function (fn, args) { finish('ended'); leaveRoom(room, 'ended'); stopCam(); for (const v of [...viewers.values()]) dropViewer(v); return fn.apply(this, args); }); + } + + return { + ring(raw, cb) { + const reply = typeof cb === 'function' ? cb : () => {}; + const req = opened(raw); + const { sender: from, via } = senderOf(this, req); + reply(null, true); + if (!from || from === server.id || !req || typeof req !== 'object') return; + const { callId, to, ephPk, ts, sig } = req; + dbg('incoming ring from', sid(from), via ? 'via ' + sid(via) : 'direct'); + if (!/^[0-9a-f]{32}$/.test(String(callId)) || to !== server.id || Math.abs(Date.now() - Number(ts)) > CLOCK_SKEW_MS) return; + const remotePk = ephKey(ephPk); + if (!remotePk || !verifyParts(from, ['ring', callId, ephPk, to, ts], sig)) return; + if (!seenCalls.has(callId) && !callerAllowed(from)) return; + if (!via) directRings.set(callId, Date.now()); + if (call && call.id === callId && call.dir === 'in') { + if (!via && call.phase === 'incoming' && call.peer === from) call.via = null; + return; + } + if (!firstSeen(callId)) return; + const ack = (state) => { + const at = Date.now(); + sendVia(directRings.has(callId) ? null : via, from, 'answer', { callId, from: server.id, ringing: state, ts: at, sig: signParts(keys, ['ringing', callId, state, from, at]) }); + }; + allowedCaller(from, (ok) => { + if (!ok || !audio) return ack('ringing'); + if (call || room) { + ack('busy'); + const at = Date.now(); + emit({ type: 'ended', call: { id: callId, peer: from, dir: 'in', startedAt: at, answeredAt: 0, endedAt: at, outcome: 'missed' } }); + return; + } + const startedAt = Date.now(); + call = { id: callId, peer: from, dir: 'in', phase: 'incoming', startedAt, ringUntil: startedAt + ringMs, remotePk, via: directRings.has(callId) ? null : via, codec: pickCodec(req.codecs), video: pickVideo(req.codecs, videoOn()) }; + call.tone = startTone(audio, 'ring'); + call.timer = setTimeout(() => { if (call && call.id === callId && call.phase === 'incoming') finish('missed'); }, ringMs); + changed(); + emit({ type: 'incoming', id: callId, peer: from }); + ack('ringing'); + }); + }, + answer(raw, cb) { + const req = opened(raw); + const { sender: from, via } = senderOf(this, req); + const c = call; + if (req && (req.ringing === 'ringing' || req.ringing === 'busy')) return ringingAck(c, req, from, cb); + if (c && c.group) return groupAnswer(c, req, from, via, cb); + if (!c || c.dir !== 'out' || c.phase !== 'calling' || from !== c.peer || !req || req.callId !== c.id) return cb(new Error('no-call')); + const calleePk = ephKey(req.ephPk); + if (!calleePk || !verifyParts(from, ['answer', req.callId, req.ephPk, server.id, req.ts], req.sig)) return cb(new Error('rejected')); + const path = via ? liveRpc(via) : liveRpc(from); + if (!path) return cb(new Error('no-connection')); + c.keys = deriveKeys(c.eph.secretKey, calleePk, c.id, Buffer.from(c.eph.publicKey), calleePk); + c.codec = pickCodec([req.codec]); + c.video = pickVideo([req.video]); + clearTimeout(c.timer); + stopTone(c); + c.via = via; + c.phase = 'connected'; + c.answeredAt = Date.now(); + cb(null, true); + changed(); + const quiet = () => {}; + startMedia(c, via ? path.phone.relayAudio({ to: c.peer, callId: c.id }, quiet) : path.phone.audio({ callId: c.id }, quiet), 'out'); + if (c.video) startVideo(c, via ? path.phone.relayVideo({ to: c.peer, callId: c.id }, quiet) : path.phone.video({ callId: c.id }, quiet), 'out'); + }, + hangup(raw, cb) { + const reply = typeof cb === 'function' ? cb : () => {}; + const req = opened(raw); + const { sender: from } = senderOf(this, req); + const c = call; + dbg('hangup received from', sid(from), c ? 'call ' + sid(c.peer) + ' ' + c.phase : 'no call'); + if (c && c.group) { + const leg = legOf(c, req && req.callId); + if (leg && from === leg.peer && verifyParts(from, ['hangup', req.callId, server.id, req.ts], req.sig)) dropLeg(c, leg, false); + return reply(null, true); + } + if (c && req && req.callId === c.id && from === c.peer && verifyParts(from, ['hangup', req.callId, server.id, req.ts], req.sig)) { + if (c.dir === 'in') finish(c.phase === 'incoming' ? 'missed' : 'ended'); + else if (c.phase === 'calling' || c.phase === 'connected') finish(c.phase === 'connected' ? 'ended' : 'noanswer'); + } + reply(null, true); + }, + audio(opts) { + const direct = this && this.id; + const from = opts && isFeedId(opts.from) ? opts.from : direct; + const c = call; + const viaOk = direct === c?.peer || (c?.via && direct === c.via); + if (!c || c.dir !== 'in' || !c.keys || c.media || from !== c.peer || !viaOk || !opts || opts.callId !== c.id || (c.phase !== 'connecting' && c.phase !== 'connected')) { + return { source: pull.error(new Error('no-call')), sink: pull.drain(null, () => {}) }; + } + c.phase = 'connected'; + return startMedia(c, null, 'in'); + }, + relay(req, cb) { + const reply = typeof cb === 'function' ? cb : () => {}; + const from = this && this.id; + reply(null, true); + if (!from || policy().relay === false || !req || typeof req !== 'object' || !isFeedId(req.to) || req.to === from) return; + if (!['ring', 'answer', 'hangup'].includes(req.method) || typeof req.sealed !== 'string' || !/^[0-9a-f]{32}$/.test(String(req.cid))) return; + const forwarded = req.hop === 1; + dbg('relay', req.method, 'from', sid(from), 'to', sid(req.to), forwarded ? 'forwarded' : 'origin', 'target-live=' + !!liveRpc(req.to)); + if (forwarded && !knownPubs().includes(from)) return; + const msg = { to: req.to, method: req.method, cid: req.cid, sealed: req.sealed, hop: 1 }; + const side = learn(req.cid, req.to, from, forwarded); + if (side.via === req.to) { + relayAllowed(req.to, forwarded ? req.to : from, (ok) => { + const target = ok ? liveRpc(req.to) : null; + dbg('relay deliver', sid(req.to), ok ? (target ? 'delivering' : 'not connected here') : 'policy refuses'); + if (!target) return; + try { target.phone[req.method]({ sealed: req.sealed }, () => {}); } catch (_) {} + }); + return; + } + if (side.via && side.via !== from && liveRpc(side.via)) { + try { liveRpc(side.via).phone.relay(msg, () => {}); } catch (_) {} + return; + } + if (forwarded || req.method !== 'ring') return; + relayAllowed(from, from, (ok) => { + if (!ok) return; + for (const pub of knownPubs().filter(id => id !== from)) { + try { liveRpc(pub).phone.relay(msg, () => {}); } catch (_) {} + } + }); + }, + relayAudio(opts) { + const from = this && this.id; + const refuse = { source: pull.error(new Error('no-route')), sink: pull.drain(null, () => {}) }; + if (!from || !opts || !isFeedId(opts.to) || opts.to === from || policy().relay === false) return refuse; + const forwarded = knownPubs().includes(from) && isFeedId(opts.from); + const caller = forwarded ? opts.from : from; + const r = routes.get(String(opts.callId || '')); + const side = r && r.sides.find(x => x.id === opts.to); + const direct = liveRpc(opts.to); + const next = direct || (side && side.via && side.via !== from ? liveRpc(side.via) : null); + if (!next) return refuse; + const remote = direct + ? next.phone.audio({ callId: opts.callId, from: caller }, () => {}) + : next.phone.relayAudio({ to: opts.to, callId: opts.callId, from: caller }, () => {}); + return { source: remote.source, sink: remote.sink }; + }, + video(opts) { + const direct = this && this.id; + const from = opts && isFeedId(opts.from) ? opts.from : direct; + const c = call; + const viaOk = direct === c?.peer || (c?.via && direct === c.via); + if (!c || c.dir !== 'in' || !c.keys || !c.video || c.vmedia || from !== c.peer || !viaOk || !opts || opts.callId !== c.id || (c.phase !== 'connecting' && c.phase !== 'connected')) { + return { source: pull.error(new Error('no-call')), sink: pull.drain(null, () => {}) }; + } + return startVideo(c, null, 'in'); + }, + relayVideo(opts) { + const from = this && this.id; + const refuse = { source: pull.error(new Error('no-route')), sink: pull.drain(null, () => {}) }; + if (!from || !opts || !isFeedId(opts.to) || opts.to === from || policy().relay === false || policy().video === false) return refuse; + const forwarded = knownPubs().includes(from) && isFeedId(opts.from); + const caller = forwarded ? opts.from : from; + const r = routes.get(String(opts.callId || '')); + const side = r && r.sides.find(x => x.id === opts.to); + const direct = liveRpc(opts.to); + const next = direct || (side && side.via && side.via !== from ? liveRpc(side.via) : null); + if (!next) return refuse; + const remote = direct + ? next.phone.video({ callId: opts.callId, from: caller }, () => {}) + : next.phone.relayVideo({ to: opts.to, callId: opts.callId, from: caller }, () => {}); + return { source: remote.source, sink: remote.sink }; + }, + camera(flag, cb) { + const on = !!flag; + const c = call; + if (c && c.vmedia && !c.group) { + if (on === !!c.cam) return cb(null, snapshot()); + if (on) { + if (!startCam((jpeg, seq) => { if (call === c) for (const plain of videoChunks(seq, jpeg)) sendCallVideo(c, plain); })) return cb(new Error('unavailable')); + c.cam = true; + } else { + c.cam = false; + stopCam(); + sendCallVideo(c, videoOff()); + } + changed(); + return cb(null, snapshot()); + } + const r = room; + if (r && r.phase === 'live') { + if (on === !!r.cam) return cb(null, roomSnapshot()); + if (on) { + if (!r.hubVideo || !startCam((jpeg, seq) => { if (room !== r) return; gridNote(r, server.id, jpeg); sendRoomJpeg(r, seq, jpeg, 0); })) return cb(new Error('unavailable')); + r.cam = true; + } else { + r.cam = false; + stopCam(); + gridNote(r, server.id, null); + sendRoomVideo(r, videoOff()); + } + roomChanged(); + return cb(null, roomSnapshot()); + } + cb(new Error('no-call')); + }, + stream(opts, cb) { + const o = opts && typeof opts === 'object' ? opts : {}; + const r = room; + if (!r || r.phase !== 'live') return cb(new Error('no-room')); + if (o.stop) { stopRoomStream(r); return cb(null, roomSnapshot()); } + const file = streamFile(o.file); + if (!file) return cb(new Error('invalid')); + if (!startRoomStream(r, file, o.name || path.basename(file))) return cb(new Error('unavailable')); + roomChanged(); + cb(null, roomSnapshot()); + }, + viewerJoin(opts, cb) { + const o = opts && typeof opts === 'object' ? opts : {}; + const rid = String(o.rid || ''); + if (!RID.test(rid) || !isFeedId(o.owner) || !isFeedId(o.hub) || typeof o.token !== 'string') return cb(new Error('invalid')); + if (viewers.has(rid)) return cb(null, true); + if (room && room.rid === rid) return cb(new Error('busy')); + const r = { + rid, ref: String(o.ref || ''), title: '', hub: o.hub, phase: 'joining', joinedAt: Date.now(), muted: true, slot: null, max: ROOM_MAX, + peers: new Map(), order: [], keys: new Map(), kid: null, sealer: null, static: false, sentAt: 0, + events: [], notify: false, rec: null, recPeers: new Set(), hand: false, handAt: 0, tag: `pub:${rid}:`, grid: newGrid() + }; + viewers.set(rid, r); + r.out = Pushable(() => {}); + r.ready = (err) => { + r.ready = null; + clearTimeout(r.timer); + if (err) { dropViewer(r); return cb(err); } + startViewerMedia(r); + startGrid(r); + cb(null, true); + }; + r.timer = setTimeout(() => { if (r.ready) r.ready(new Error('unreachable')); }, ROOM_JOIN_MS); + const attach = (duplex) => { + if (viewers.get(rid) !== r || !r.ready) return closeDuplex(duplex); + if (!duplex) return r.ready(new Error('unreachable')); + pull(r.out, duplex.sink); + pull(duplex.source, pull.drain((frame) => onRoomFrame(r, frame), () => { + if (viewers.get(rid) !== r) return; + if (r.ready) r.ready(new Error('unreachable')); + else dropViewer(r); + })); + }; + openHub(o, { rid, owner: o.owner, token: o.token, viewer: true, ...(RID.test(String(o.roomId || '')) ? { roomId: String(o.roomId) } : {}) }, attach); + }, + viewerLeave(opts, cb) { + dropViewer(viewers.get(String((opts && opts.rid) || ''))); + cb(null, true); + }, + viewerRooms() { return [...viewers.values()].filter(r => r.phase === 'live').map(r => r.rid); }, + publicFeed(opts) { + const rid = String((opts && opts.rid) || ''); + const kind = opts && opts.kind === 'sound' ? 'sound' : 'grid'; + const r = viewers.get(rid); + const target = r ? r.tag + kind : ''; + const p = Pushable(() => { const set = frameWatchers.get(target); if (set) { set.delete(p); if (!set.size) frameWatchers.delete(target); } }); + if (!r || r.phase !== 'live' || (kind === 'grid' && !sharpLib)) { p.end(); return p; } + if (!frameWatchers.has(target)) frameWatchers.set(target, new Set()); + frameWatchers.get(target).add(p); + if (kind === 'grid') { r.grid.dirty = true; r.grid.empty = false; } + return p; + }, + videoFrames(opts) { + const target = String((opts && opts.peer) || ''); + const p = Pushable(() => { const set = frameWatchers.get(target); if (set) { set.delete(p); if (!set.size) frameWatchers.delete(target); } }); + const now = Date.now(); + const filmOf = target.startsWith('film:') && target !== 'film:me' ? target.slice(5) : null; + const live = target === 'me' + ? !!cam + : target === 'film:me' ? !!(room && room.stream) + : target === 'grid' ? !!(room && sharpLib && gridIds(room).length) + : filmOf ? !!room && [...room.peers.values()].some(x => x.id === filmOf && now - (x.filmAt || 0) < VIDEO_HOLD_MS) + : call ? (call.peer === target && !!call.peerCam) + : room ? [...room.peers.values()].some(x => x.id === target && now - (x.frameAt || 0) < VIDEO_HOLD_MS) + : false; + if (!live) { p.end(); return p; } + if (!frameWatchers.has(target)) frameWatchers.set(target, new Set()); + frameWatchers.get(target).add(p); + return p; + }, + + call(target, opts, cb) { + if (typeof opts === 'function') { cb = opts; opts = {}; } + const legacy = new Set(Array.isArray(opts && opts.legacy) ? opts.legacy : []); + if (!audio) return cb(new Error('unavailable')); + const targets = [...new Set(Array.isArray(target) ? target : [target])]; + if (!targets.length || targets.length > GROUP_MAX || targets.some(t => !isFeedId(t) || t === server.id)) return cb(new Error('invalid')); + if (call || room) return cb(new Error('busy')); + if (targets.length > 1) return groupCall(targets, legacy, cb); + target = targets[0]; + const id = nodeCrypto.randomBytes(16).toString('hex'); + const eph = sodium.crypto_box_keypair(); + const startedAt = Date.now(); + const c = { id, peer: target, dir: 'out', phase: 'calling', startedAt, ringUntil: startedAt + ringMs, eph, paths: [], via: null }; + call = c; + c.tone = startTone(audio, 'connecting'); + c.timer = setTimeout(() => toVoicemail(c, 'unreachable'), ringMs); + changed(); + ringPaths(c, target, ringRequest(id, eph, target), () => call === c && c.phase === 'calling', () => reachedCall(c), legacy.has(target)); + cb(null, snapshot()); + }, + accept(cb) { + const c = call; + if (!c || c.dir !== 'in' || c.phase !== 'incoming') return cb(new Error('no-call')); + const eph = sodium.crypto_box_keypair(); + c.keys = deriveKeys(eph.secretKey, c.remotePk, c.id, c.remotePk, Buffer.from(eph.publicKey)); + stopTone(c); + clearTimeout(c.timer); + c.phase = 'connecting'; + c.answeredAt = Date.now(); + changed(); + const ts = Date.now(); + const ephPk = b64(eph.publicKey); + sendVia(c.via || null, c.peer, 'answer', { callId: c.id, from: server.id, ephPk, ts, sig: signParts(keys, ['answer', c.id, ephPk, c.peer, ts]), codec: c.codec, video: c.video }, (err) => { + if (call !== c) return; + if (err) return finish('failed'); + if (c.phase === 'connecting') { c.phase = 'connected'; changed(); } + }); + cb(null, snapshot()); + }, + reject(cb) { + const c = call; + if (c && c.dir === 'in' && c.phase === 'incoming') finish('rejected'); + cb(null, snapshot()); + }, + end(cb) { + const c = call; + if (!c) return cb(null, null); + if (c.group) { + for (const leg of c.legs) if (leg.phase === 'calling' || leg.phase === 'connected') hangupLeg(leg); + finish(c.answeredAt ? 'ended' : 'cancelled'); + return cb(null, null); + } + if (c.dir === 'in' && c.phase === 'incoming') { finish('rejected'); return cb(null, null); } + if (c.phase === 'calling' || c.phase === 'connecting' || c.phase === 'connected') notifyHangup(c); + finish(c.phase === 'connected' || c.phase === 'connecting' ? 'ended' : (c.phase === 'calling' ? 'cancelled' : 'noanswer')); + cb(null, null); + }, + mute(flag, cb) { + if (call) { call.muted = !!flag; changed(); } + cb(null, snapshot()); + }, + silence(opts, cb) { + const id = opts && opts.id; + if (!isFeedId(id) || id === server.id) return cb(new Error('invalid')); + if (opts.on) silenced.add(id); else silenced.delete(id); + if (call) changed(); + if (room) roomChanged(); + cb(null, { id, on: silenced.has(id) }); + }, + dismiss(cb) { + const c = call; + if (c && c.dir === 'out' && (c.phase === 'noanswer' || c.phase === 'recorded')) finish('noanswer'); + cb(null, snapshot()); + }, + recordStop(cb) { + const c = call; + if (!c || c.dir !== 'out' || (c.phase !== 'recording' && c.phase !== 'recorded') || !c.rec) return cb(new Error('no-recording')); + stopRecorder(c); + const pcm = Buffer.concat(c.rec.chunks); + c.rec = null; + if (pcm.length < RATE * 2 * PAM_MIN_MS / 1000) { finish('noanswer'); return cb(new Error('empty')); } + const file = path.join(os.tmpdir(), `oasis-voice-${c.id}.wav`); + try { fs.writeFileSync(file, wavFile(pcm), { mode: 0o600 }); } catch (err) { return cb(err); } + const out = { file, durationMs: Math.round(pcm.length / (RATE * 2) * 1000), peer: c.peer, callId: c.id }; + finish('pam'); + cb(null, out); + }, + recordCancel(cb) { + const c = call; + if (c && (c.phase === 'recording' || c.phase === 'recorded')) { + stopRecorder(c); + c.rec = null; + finish('noanswer'); + } + cb(null, snapshot()); + }, + roomHub(opts) { return hubDuplex(this && this.id, opts); }, + roomInfo(opts, cb) { + const r = hubRooms.get(String((opts && opts.rid) || '')); + cb(null, { count: r ? hubHumans(r) : 0, max: roomMax, video: policy().video !== false }); + }, + roomAdmits(opts, cb) { + const from = this && this.id; + if (!from) return cb(null, false); + relayAllowed(from, from, (ok) => cb(null, !!ok)); + }, + roomJoin(opts, cb) { + if (!audio) return cb(new Error('unavailable')); + if (call || room) return cb(new Error('busy')); + const rid = String((opts && opts.rid) || ''); + if (viewers.has(rid)) dropViewer(viewers.get(rid)); + if (!RID.test(rid) || !isFeedId(opts.owner) || !isFeedId(opts.hub) || typeof opts.token !== 'string') return cb(new Error('invalid')); + const secrets = (Array.isArray(opts.secrets) ? opts.secrets : []).filter(x => typeof x === 'string' && x); + const r = { + rid, ref: String(opts.ref || ''), title: String(opts.title || '').slice(0, 200), hub: opts.hub, + phase: 'joining', joinedAt: Date.now(), muted: false, slot: null, max: ROOM_MAX, peers: new Map(), order: [], + keys: new Map(), kid: null, sealer: null, static: secrets.length > 0, sentAt: 0, + events: [], notify: true, rec: null, recPeers: new Set(), hand: false, handAt: 0, tag: '', grid: newGrid() + }; + if (r.static) { + for (const secret of secrets.slice().reverse()) { const k = staticKey(rid, secret); r.keys.set(k.kid, k.key); } + const first = staticKey(rid, secrets[0]); + r.kid = first.kid; + r.sealer = sealer(first.key); + } + room = r; + r.out = Pushable(() => {}); + r.ready = (err) => { + r.ready = null; + clearTimeout(r.timer); + if (err) { leaveRoom(r, null); return cb(err); } + startRoomMedia(r); + startGrid(r); + roomChanged(); + cb(null, roomSnapshot()); + }; + r.timer = setTimeout(() => { if (r.ready) r.ready(new Error('unreachable')); }, ROOM_JOIN_MS); + const attach = (duplex) => { + if (room !== r || !r.ready) return closeDuplex(duplex); + if (!duplex) return r.ready(new Error('unreachable')); + pull(r.out, duplex.sink); + pull(duplex.source, pull.drain((frame) => onRoomFrame(r, frame), () => { + if (room !== r) return; + if (r.ready) r.ready(new Error('unreachable')); + else leaveRoom(r, 'lost'); + })); + }; + openHub(opts, { rid, owner: opts.owner, token: opts.token, ...(RID.test(String(opts.roomId || '')) ? { roomId: String(opts.roomId) } : {}) }, attach); + }, + roomLeave(cb) { + leaveRoom(room, 'left'); + cb(null, null); + }, + roomRecStart(cb) { cb(null, startRoomRec(room) ? roomSnapshot() : null); }, + roomRecStop(cb) { cb(null, stopRoomRec(room) ? roomSnapshot() : null); }, + roomNotify(flag, cb) { + if (room) { room.notify = !!flag; roomChanged(); } + cb(null, roomSnapshot()); + }, + roomClearEvents(cb) { + if (room) { room.events = []; roomChanged(); } + cb(null, roomSnapshot()); + }, + roomHand(flag, cb) { + if (room) { + room.hand = !!flag; + room.handAt = room.hand ? Date.now() : 0; + sendRoomData(room, { t: 'hand', on: room.hand, at: room.handAt }); + room.handBeaconAt = Date.now(); + roomChanged(); + } + cb(null, roomSnapshot()); + }, + roomMute(flag, cb) { + if (room) { room.muted = !!flag; sendRoomData(room, { t: 'mute', on: room.muted }); roomChanged(); } + cb(null, roomSnapshot()); + }, + roomCount(opts, cb) { + const rid = String((opts && opts.rid) || ''); + if (!RID.test(rid) || !isFeedId(opts.hub)) return cb(null, null); + if (room && room.rid === rid && room.phase === 'live') return cb(null, { count: humansIn(room) + 1, max: room.max }); + if (opts.hub === server.id) { const h = hubRooms.get(rid); return cb(null, { count: h ? hubHumans(h) : 0, max: roomMax }); } + const rpc = liveRpc(opts.hub); + if (!rpc || typeof rpc.phone.roomInfo !== 'function') return cb(null, null); + let done = false; + const finishPeek = (v) => { if (done) return; done = true; clearTimeout(t); cb(null, v); }; + const t = setTimeout(() => finishPeek(null), ROOM_PEEK_MS); + try { + rpc.phone.roomInfo({ rid }, (err, info) => finishPeek(!err && info && Number.isFinite(Number(info.count)) ? { count: Number(info.count), max: Number(info.max) > 0 ? Math.min(ROOM_MAX, Number(info.max)) : ROOM_MAX } : null)); + } catch (_) { finishPeek(null); } + }, + roomState() { return roomSnapshot(); }, + roomToken(rid) { return RID.test(String(rid || '')) ? signParts(keys, ['room', String(rid)]) : null; }, + roomHubFor(cb) { + const done = typeof cb === 'function' ? cb : () => {}; + let entries = []; + try { entries = netFirst(server.conn.query().peersAll()); } catch (_) {} + const ranked = typeof server.oasisPeerRank === 'function' ? (id) => server.oasisPeerRank([null, { key: id }]) : () => 0; + const candidates = knownPubs().filter(id => { const rpc = liveRpc(id); return rpc && rpc.phone && typeof rpc.phone.roomHub === 'function'; }).sort((x, y) => ranked(y) - ranked(x)); + const admits = (id, next) => { + const rpc = liveRpc(id); + const byGraph = () => relation('isFollowing', id, server.id, next); + if (!rpc || typeof rpc.phone.roomAdmits !== 'function') return byGraph(); + let settled = false; + const finishAsk = (fn) => { if (settled) return; settled = true; clearTimeout(t); fn(); }; + const t = setTimeout(() => finishAsk(() => next(false)), ROOM_PEEK_MS); + try { rpc.phone.roomAdmits({}, (err, ok) => finishAsk(err ? byGraph : () => next(!!ok))); } catch (_) { finishAsk(byGraph); } + }; + const pick = (i) => { + if (i >= candidates.length) return done(null, { key: server.id, address: '' }); + admits(candidates[i], (ok) => { + if (!ok) return pick(i + 1); + const hit = entries.find(e => Array.isArray(e) && e[0] && e[1] && e[1].key === candidates[i]); + done(null, { key: candidates[i], address: hit ? hit[0] : '' }); + }); + }; + pick(0); + }, + state() { return snapshot(); }, + available() { return !!audio; }, + pubs() { return [...pubKeys()]; }, + events() { + const p = Pushable(() => subscribers.delete(p)); + subscribers.add(p); + p.push({ type: 'state', state: snapshot() }); + return p; + } + }; + } +}; + +module.exports.setAudioFactory = (factory) => { audioFactory = typeof factory === 'function' ? factory : systemAudio; }; +module.exports.setVideoFactory = (factory) => { videoFactory = typeof factory === 'function' ? factory : systemVideo; }; +module.exports.setStreamFactory = (factory) => { streamFactory = typeof factory === 'function' ? factory : systemStream; }; +module.exports.jpegSplitter = jpegSplitter; +module.exports.VIDEO_CHUNK = VIDEO_CHUNK; +module.exports.VIDEO_HOLD_MS = VIDEO_HOLD_MS; +module.exports.ROOM_VIDEO_BYTES_PER_S = ROOM_VIDEO_BYTES_PER_S; +module.exports.ulawEncode = ulawEncode; +module.exports.ulawDecode = ulawDecode; +module.exports.RING_MS = RING_MS; +module.exports.GROUP_MAX = GROUP_MAX; +module.exports.ROOM_MAX = ROOM_MAX; +module.exports.RING_PER_WINDOW = RING_PER_WINDOW; +module.exports.chimeMs = chimeMs; +module.exports.recordingPrefix = recordingPrefix; +module.exports.VOICEMAIL_MAX_MS = VOICEMAIL_MAX_MS; +module.exports.SOUND_RATE = RATE; diff --git a/src/server/phone_stream_android.js b/src/server/phone_stream_android.js new file mode 100644 index 0000000..f2c525c --- /dev/null +++ b/src/server/phone_stream_android.js @@ -0,0 +1,53 @@ +const fs = require('fs'); +const net = require('net'); +const path = require('path'); + +const MARKER = path.join(__dirname, '..', '..', '.audio_bridge'); +const FRAME_MAX = 4 * 1024 * 1024; + +const bridgePort = () => { + try { + const port = Number(String(fs.readFileSync(MARKER, 'utf8')).trim()); + return Number.isFinite(port) && port > 0 ? port : 0; + } catch (_) { + return 0; + } +}; + +module.exports = () => ({ + open(file, { onJpeg, onPcm, onEnd }) { + let ended = false; + const finish = () => { + if (ended) return; + ended = true; + try { onEnd(); } catch (_) {} + }; + const port = bridgePort(); + if (!port) { + setImmediate(finish); + return { stop() { ended = true; } }; + } + const sock = net.connect(port, '127.0.0.1'); + let buf = Buffer.alloc(0); + sock.setNoDelay(true); + sock.on('error', finish); + sock.on('close', finish); + sock.on('connect', () => sock.write(Buffer.concat([Buffer.from('S'), Buffer.from(`${file}\n`, 'utf8')]))); + sock.on('data', (chunk) => { + buf = buf.length ? Buffer.concat([buf, chunk]) : chunk; + while (buf.length >= 5) { + const len = buf.readUInt32BE(1); + if (len > FRAME_MAX) { sock.destroy(); return; } + if (buf.length < 5 + len) return; + const type = buf[0]; + const payload = Buffer.from(buf.subarray(5, 5 + len)); + buf = buf.subarray(5 + len); + if (ended) continue; + if (type === 0x4a) onJpeg(payload); + else if (type === 0x41) onPcm(payload); + else if (type === 0x45) { finish(); sock.destroy(); return; } + } + }); + return { stop() { ended = true; try { sock.destroy(); } catch (_) {} } }; + } +}); diff --git a/src/server/phone_video_android.js b/src/server/phone_video_android.js new file mode 100644 index 0000000..deb6354 --- /dev/null +++ b/src/server/phone_video_android.js @@ -0,0 +1,30 @@ +const fs = require('fs'); +const net = require('net'); +const path = require('path'); +const { jpegSplitter } = require('./phone_module'); + +const MARKER = path.join(__dirname, '..', '..', '.audio_bridge'); + +const bridgePort = () => { + try { + const port = Number(String(fs.readFileSync(MARKER, 'utf8')).trim()); + return Number.isFinite(port) && port > 0 ? port : 0; + } catch (_) { + return 0; + } +}; + +module.exports = () => { + const port = bridgePort(); + if (!port) return null; + return { + capture(onJpeg) { + const sock = net.connect(port, '127.0.0.1'); + sock.on('error', () => {}); + sock.setNoDelay(true); + sock.write('V'); + sock.on('data', jpegSplitter(onJpeg)); + return { stop() { try { sock.destroy(); } catch (_) {} } }; + } + }; +}; diff --git a/src/server/snapshot_plugin.js b/src/server/snapshot_plugin.js new file mode 100644 index 0000000..9c865ef --- /dev/null +++ b/src/server/snapshot_plugin.js @@ -0,0 +1,45 @@ +const fs = require('fs'); +const path = require('path'); +const pull = require('./node_modules/pull-stream'); +const defer = require('./node_modules/pull-defer'); +const toPull = require('./node_modules/stream-to-pull-stream'); + +module.exports = { + name: 'snapshot', + version: '1.0.0', + manifest: { info: 'async', get: 'source' }, + permissions: { anonymous: { allow: ['info', 'get'] } }, + init(server, config) { + const file = (opts) => path.join(config.path, 'oasis', 'content', opts && opts.tier === 'recent' ? 'snapshot-recent.oasissn' : 'snapshot.oasissn'); + const stat = (opts) => { + try { const st = fs.statSync(file(opts)); return { tier: opts && opts.tier === 'recent' ? 'recent' : 'full', bytes: st.size, createdAt: new Date(st.mtimeMs).toISOString() }; } catch (_) { return null; } + }; + const allowed = (rpc, cb) => { + const id = rpc && rpc.id; + if (!id) return cb(null, false); + if (id === server.id) return cb(null, true); + if (!server.friends || typeof server.friends.isFollowing !== 'function') return cb(null, false); + server.friends.isFollowing({ source: server.id, dest: id }, (err, yes) => cb(null, !err && !!yes)); + }; + return { + info(opts, cb) { + if (typeof opts === 'function') { cb = opts; opts = {}; } + allowed(this, (_, ok) => { + if (!ok) return cb(new Error('snapshot: not allowed')); + const s = stat(opts); + if (!s) return cb(new Error('snapshot: not available')); + cb(null, s); + }); + }, + get(opts) { + const source = defer.source(); + allowed(this, (_, ok) => { + if (!ok) return source.resolve(pull.error(new Error('snapshot: not allowed'))); + if (!stat(opts)) return source.resolve(pull.error(new Error('snapshot: not available'))); + source.resolve(toPull.source(fs.createReadStream(file(opts)))); + }); + return source; + } + }; + } +}; diff --git a/src/server/ssb_config.js b/src/server/ssb_config.js index 6e9e36b..881a94d 100644 --- a/src/server/ssb_config.js +++ b/src/server/ssb_config.js @@ -3,15 +3,36 @@ const path = require('path'); const Config = require('ssb-config/inject'); const minimist = require('minimist'); -const configPath = path.resolve(__dirname, '../configs', 'server-config.json'); -const configData = JSON.parse(fs.readFileSync(configPath, 'utf8')); +const configData = require('../configs/config-manager').readServerConfig(); const argv = process.argv.slice(2); const i = argv.indexOf('--'); const conf = argv.slice(i + 1); const cliArgs = ~i ? argv.slice(0, i) : argv; -let config = Config('ssb', minimist(conf)); +// Nombre de la cuenta: define donde vive todo (~/.) y de ahi cuelgan el +// secret, la base de datos, los blobs y el socket. Sin la variable se usa 'ssb', +// que es lo de siempre, asi que las instalaciones existentes no notan nada. +// +// Se valida a proposito: este nombre acaba en rutas de fichero y hay operaciones +// destructivas (el modo panico borra el directorio de la cuenta) que no deben poder +// apuntar fuera de sitio. +// +// Manda la variable de entorno; si no esta, la cuenta que dejo anotada el selector +// de identidades de los ajustes; y si tampoco, 'ssb'. +const ACCOUNT_RE = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,31}$/; +let annotated = null; +try { + const f = path.resolve(__dirname, '../configs', 'active-account'); + annotated = fs.readFileSync(f, 'utf8').trim() || null; +} catch (_) {} +const rawAccount = process.env.OASIS_ACCOUNT || annotated || 'ssb'; +const account = ACCOUNT_RE.test(rawAccount) ? rawAccount : 'ssb'; +if (account !== rawAccount) { + console.error(`[oasis] cuenta no valida (${rawAccount}); se usa 'ssb'`); +} + +let config = Config(account, minimist(conf)); config = { ...config, ...configData }; const debug = process.argv.includes('--debug') || process.env.OASIS_DEBUG === '1' || process.env.OASIS_DEBUG === 'true'; @@ -21,6 +42,23 @@ if (debug) { const megabyte = Math.pow(2, 20); config.blobs = config.blobs || {}; -config.blobs.max = 50 * megabyte; +config.blobs.max = 75 * megabyte; + +config.db2 = { automigrate: false, dangerouslyKillFlumeWhenMigrated: false, ...(config.db2 || {}) }; + +const lanOn = (() => { try { return require('../configs/config-manager').getConfig().lanBroadcasting !== false; } catch (_) { return true; } })(); +const bindHost = config.pub ? '0.0.0.0' : lanOn ? null : '127.0.0.1'; +for (const entry of (config.connections && config.connections.incoming && config.connections.incoming.net) || []) { + if (entry && typeof entry === 'object' && !entry.host && bindHost) entry.host = bindHost; +} + +config.account = account; + +config.statePath = (name) => { + // El directorio externo se separa por cuenta: sin esto dos identidades + // compartirian estado, que es justo lo que hay que evitar. + if (process.env.OASIS_STATE_DIR) return path.join(process.env.OASIS_STATE_DIR, account, name); + return config.path ? path.join(config.path, name) : null; +}; module.exports = config; diff --git a/src/server/ssb_metadata.js b/src/server/ssb_metadata.js index 37ca6c1..1646ebd 100644 --- a/src/server/ssb_metadata.js +++ b/src/server/ssb_metadata.js @@ -5,18 +5,28 @@ const pkg = require('./package.json'); const config = require('./ssb_config'); const updater = require('../backend/updater.js'); +const formatMB = (bytes, decimals = 0) => `${(bytes / (1024 * 1024)).toFixed(decimals)} MB`; + +const blobUsageOnDisk = (ssbPath) => { + const out = { bytes: 0, files: 0 }; + const walk = (dir) => { + let entries = []; + try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (_) { return; } + for (const e of entries) { + const full = path.join(dir, e.name); + if (e.isDirectory()) walk(full); + else if (e.isFile()) { try { out.bytes += fs.statSync(full).size; out.files += 1; } catch (_) {} } + } + }; + if (ssbPath) walk(path.join(ssbPath, 'blobs', 'sha256')); + return out; +}; + let printed = false; let checkedForUpdate = false; -let pendingClearnetModules = null; -let clearnetReady = false; let pendingWarmup = null; let warmupReady = false; -function setClearnetModules(modules) { - pendingClearnetModules = Array.isArray(modules) ? modules : []; - clearnetReady = true; -} - function setWarmupTime(str) { pendingWarmup = str; warmupReady = true; @@ -64,19 +74,6 @@ async function checkForUpdate() { await updater.getRemoteVersion(); } -function waitForClearnet(timeoutMs = 25000) { - return new Promise((resolve) => { - if (clearnetReady) return resolve(pendingClearnetModules || []); - const started = Date.now(); - const tick = () => { - if (clearnetReady) return resolve(pendingClearnetModules || []); - if (Date.now() - started >= timeoutMs) return resolve(null); - setTimeout(tick, 250); - }; - tick(); - }); -} - async function printMetadata(mode, modeColor = colors.cyan, httpPort = 3000, httpHost = 'localhost', offline = false, isPublic = false) { if (printed) return; printed = true; @@ -90,40 +87,86 @@ async function printMetadata(mode, modeColor = colors.cyan, httpPort = 3000, htt const httpUrl = hasHttp ? `http://${httpHost}:${httpPort}` : ''; const oscLink = hasHttp ? `\x1b]8;;${httpUrl}\x07${httpUrl}\x1b]8;;\x07` : ''; const ssbPort = config.connections?.incoming?.net?.[0]?.port || config.port || 8008; - const localDiscovery = config.local === true; + const localDiscovery = (() => { try { return require('../configs/config-manager.js').getConfig().lanBroadcasting !== false; } catch (_) { return false; } })(); const hops = config.conn?.hops ?? config.friends?.hops ?? 2; console.log("========================="); console.log(`Running mode: ${modeColor}${mode}${colors.reset}`); console.log("========================="); + const walletId = (() => { + try { + const w = (require('../configs/config-manager.js').getConfig() || {}).wallet || {}; + if (!(String(w.url || '').trim() && String(w.user || '').trim() && String(w.pass || '').trim())) return ''; + const file = process.env.OASIS_BANKING_DIR ? path.join(process.env.OASIS_BANKING_DIR, 'wallet-addresses.json') : require('../configs/state-manager').statePath('wallet-addresses.json'); + const map = JSON.parse(fs.readFileSync(file, 'utf8')); + const v = map[`@${publicKey}`]; + const addr = typeof v === 'string' ? v : (v && v.address) || ''; + return /^E[1-9A-HJ-NP-Za-km-z]{32,34}$/.test(addr) ? addr : ''; + } catch (_) { return ''; } + })(); + console.log(`- OASIS ID: [ ${colors.orange}@${publicKey}${colors.reset} ]`); console.log(`- Package: ${colors.blue}${name} ${colors.yellow}[Version: ${version}]${colors.reset}`); - if (hasHttp) console.log(`- URL: ${colors.cyan}${oscLink}${colors.reset}`); - console.log(`- Oasis ID: [ ${colors.orange}@${publicKey}${colors.reset} ]`); + if (hasHttp) console.log(`- GUI: ${colors.cyan}${oscLink}${colors.reset}`); + const phoneEnabled = (() => { + try { return ((require('../configs/config-manager.js').getConfig() || {}).modules || {}).phoneMod !== 'off'; } catch (_) { return false; } + })(); + const phoneNumber = phoneEnabled ? (() => { try { return require('../models/phone_number').phoneNumberOf(`@${publicKey}`); } catch (_) { return null; } })() : null; + console.log(`- VoIP ID: ${phoneEnabled ? (phoneNumber ? `[ ${colors.orange}${phoneNumber}${colors.reset} ]` : 'enabled') : 'disabled'}`); + console.log(walletId ? `- ECOin ID: [ ${colors.orange}${walletId}${colors.reset} ]` : `- ECOin ID: disabled`); console.log("- Logging Level:", logLevel); + console.log("- Engine: db2-legacy"); const ifaces = os.networkInterfaces(); const isOnline = Object.values(ifaces).some(list => list && list.some(i => !i.internal && i.family === 'IPv4') ); console.log(`- Protocol (port): ${ssbPort}`); - console.log(`- Mode: ${isOnline ? 'online' : 'offline'}`); + const networkPaused = (() => { try { return require('../configs/config-manager.js').getConfig().networkPaused === true || process.env.OASIS_NETWORK_PAUSED === '1'; } catch (_) { return false; } })(); + console.log(`- Mode: ${networkPaused ? 'paused' : (isOnline ? 'online' : 'offline')}`); console.log(`- Replication (hops): ${hops}`); + const oasisCfg = (() => { try { return require('../configs/config-manager.js').getConfig() || {}; } catch (_) { return {}; } })(); + console.log(`- Blockchain backlog: ${Number(oasisCfg.ssbLogStream && oasisCfg.ssbLogStream.limit) || 1000} blocks`); + console.log(`- Blob size limit: ${formatMB(Number(config.blobs && config.blobs.max) || 0)}`); + const cacheMb = Number(oasisCfg.blobCache && (isPublic ? oasisCfg.blobCache.pubMaxMB : oasisCfg.blobCache.maxMB)) || 0; + const cacheUsage = blobUsageOnDisk(config.path); + console.log(`- Media cache limit: ${cacheMb > 0 ? formatMB(cacheMb * 1024 * 1024) : 'Unlimited'}`); + console.log(`- Current usage: ${formatMB(cacheUsage.bytes, 1)} · ${cacheUsage.files} files`); console.log(`- LAN Broadcasting (UDP): ${localDiscovery ? 'enabled' : 'disabled'}`); - const clearnetModules = await waitForClearnet(); - const clearnetStatus = (clearnetModules && clearnetModules.length > 0) ? clearnetModules.join(', ') : 'disabled'; - let fediverseConnected = false; - try { - const acc = JSON.parse(fs.readFileSync(path.join(__dirname, '../configs/fediverse-accounts.json'), 'utf8')); - fediverseConnected = !!(acc && acc.mastodon); - } catch (_) {} - console.log(`- Internet Broadcasting:`); - console.log(` - Clearnet: ${clearnetStatus}`); - console.log(` - Fediverse: ${fediverseConnected ? 'enabled' : 'disabled'}`); + const multiverse = (() => { + if ((oasisCfg.modules || {}).fediverseMod === 'off') return []; + try { + const acc = JSON.parse(fs.readFileSync(require('../configs/state-manager').statePath('fediverse-accounts.json'), 'utf8')) || {}; + return [ + acc.mastodon && acc.mastodon.token ? 'Mastodon' : null, + acc.peertube && acc.peertube.token ? 'PeerTube' : null, + acc.telegram && acc.telegram.session ? 'Telegram' : null + ].filter(Boolean); + } catch (_) { return []; } + })(); + console.log(`- Multiverse: ${multiverse.length ? multiverse.join(', ') : 'disabled'}`); + if (hasHttp) { + const en = (() => { try { return require('../client/assets/translations/i18n').en || {}; } catch (_) { return {}; } })(); + const uxNames = { blocks: en.uxModeMenus, ainav: en.uxModeAINav, chats: en.uxModeChats, feed: en.uxModeFeed, phone: en.phoneTitle, rooms: en.roomsTitle }; + const wishNames = { whole: en.settingsWishWhole, mutuals: en.settingsWishMutuals, 'only-lan': en.settingsWishOnlyLan, local: en.settingsWishLocal }; + const workflow = (() => { + try { + const wf = require('../models/workflows_model'); + const key = wf.currentWorkflow(oasisCfg); + if (key) return en[`workflow_${key}`] || key; + const mods = oasisCfg.modules || {}; + return wf.ALL_MODULES.every(m => mods[`${m}Mod`] === 'on') ? (en.welcomeWorkflow_full || 'Default') : 'custom'; + } catch (_) { return 'custom'; } + })(); + const ux = (oasisCfg.ux && oasisCfg.ux.current) || 'blocks'; + const wish = oasisCfg.wish || 'whole'; + console.log(`- Workflow: ${workflow}`); + console.log(`- UX mode: ${uxNames[ux] || ux}`); + console.log(`- Wish: ${wishNames[wish] || wish}`); + } console.log(""); console.log("========================="); console.log("Modules loaded: [", modules.length, "]"); console.log("========================="); - // Check for updates await checkForUpdate(); console.log("========================="); @@ -135,7 +178,6 @@ async function printMetadata(mode, modeColor = colors.cyan, httpPort = 3000, htt module.exports = { printMetadata, - setClearnetModules, setWarmupTime, colors }; diff --git a/src/views/AI_view.js b/src/views/AI_view.js index e7a0d14..3d0bb6f 100644 --- a/src/views/AI_view.js +++ b/src/views/AI_view.js @@ -1,147 +1,102 @@ -const { div, h2, p, section, button, form, textarea, br, span, input, label, select, option } = require("../server/node_modules/hyperaxe"); -const { template, i18n } = require('./main_views'); -const { renderUrl } = require('../backend/renderUrl'); +const { div, h2, p, section, button, form, textarea, span, input, br } = require("../server/node_modules/hyperaxe"); +const moment = require("../server/node_modules/moment"); +const { template, i18n, userLinkLabel } = require('./main_views'); +const config = require('../server/ssb_config'); +const { renderStyledText } = require('../backend/renderStyledText'); -exports.aiView = (history = [], userPrompt = '') => { +const statusBanner = (status) => { + if (!status) return null; + if (status.installed === false || /model_missing/.test(String(status.error || ''))) { + return div({ class: 'ai-status ai-status-missing' }, i18n.aiStatusMissingModel); + } + if (status.error && !/model_missing/.test(String(status.error))) { + return div({ class: 'ai-status ai-status-loading' }, `${i18n.aiServerError} (${status.error})`); + } + return null; +}; + +const renderAnswerText = (text) => { + const lines = String(text || '').split('\n').map(l => l.trim()).filter(Boolean); + return lines.flatMap((line, i) => (i > 0 ? [br()] : []).concat(renderStyledText(line))); +}; + +const myNick = () => { try { return userLinkLabel(config.keys.id); } catch (_) { return '@me'; } }; + +const ratingRow = (entry) => { + if (entry.source !== 'model') return null; + const rating = Math.max(0, Math.min(5, Number(entry.rating) || 0)); + if (entry.trainStatus === 'approved') { + return div({ class: 'ai-rating ai-rating-done', title: i18n.aiTrainApproved }, + span({ class: 'ai-stars-static' }, '★'.repeat(rating || 5) + '☆'.repeat(5 - (rating || 5))), + span({ class: 'ai-rating-label' }, i18n.aiTrainApproved) + ); + } + return form({ method: 'POST', action: '/ai/approve', class: 'ai-rating ai-stars', title: i18n.aiApproveTrain }, + input({ type: 'hidden', name: 'ts', value: String(entry.timestamp) }), + ...[5, 4, 3, 2, 1].map(n => button({ type: 'submit', name: 'rating', value: String(n), class: 'ai-star-btn', title: `${n} ★` }, '★')) + ); +}; + +const SPLIT_MARKER = ''; + +const stateChip = (kind) => span({ class: `pm-exposition-chip ai-chip ai-chip-${kind}` }, + span({ class: 'pm-exposition-icon' }, kind === 'thinking' ? '⏳' : '🤖'), + span({ class: 'pm-exposition-text' }, kind === 'thinking' ? i18n.aiThinking : i18n.aiWaiting) +); + + +const renderEntry = (entry, { split = false } = {}) => { + const time = entry.timestamp ? moment(entry.timestamp).format("YYYY/MM/DD HH:mm") : ''; + const thinking = entry.trainStatus === 'thinking'; + return [ + div({ class: 'chat-bubble-row ai-row' }, + div({ class: 'chat-message ai-bubble-user' }, + span({ class: 'chat-bubble-sender' }, myNick()), + div({ class: 'chat-message-text' }, ...renderStyledText(String(entry.question || ''))), + span({ class: 'chat-bubble-time' }, time) + ) + ), + thinking ? stateChip('thinking') : null, + split ? span({ id: 'oasis-ai-split' }) : null, + thinking ? null : div({ class: 'chat-bubble-row chat-bubble-row-self ai-row' }, + div({ class: `chat-message chat-message-self ai-bubble-42 ai-bubble-${entry.source || 'model'}` }, + span({ class: 'chat-bubble-sender chat-bubble-sender-owner' }, '🤖 42'), + div({ class: 'chat-message-text' }, ...renderAnswerText(entry.answer)), + span({ class: 'chat-bubble-time' }, time), + ratingRow(entry) + ) + ) + ]; +}; + +exports.aiView = (history = [], userPrompt = '', { status = null, split = false } = {}) => { + const ordered = [...history].reverse(); + const thinking = ordered.length ? ordered[ordered.length - 1].trainStatus === 'thinking' : false; return template( i18n.aiTitle, section( - div({ class: "tags-header" }, + div({ class: "tags-header module-header-line" }, h2(i18n.aiTitle), - p(i18n.aiDescription), - userPrompt ? div({ class: 'user-prompt', style: 'margin-bottom: 2em; font-size: 0.95em; color: #888;' }, - `${i18n.aiPromptUsed || 'System Prompt'}: `, - span({ style: 'font-style: italic;' }, `"${userPrompt}"`) - ) : null, - form({ method: 'POST', action: '/ai', style: "margin-bottom: 0;" }, - textarea({ name: 'input', rows: 4, placeholder: i18n.aiInputPlaceholder, required: true }), - br(), - div({ style: "display: flex; gap: 1.5em; justify-content: flex-end; align-items: center; margin-top: 0.7em;" }, - button({ type: 'submit' }, i18n.aiSubmitButton) - ) - ), - div({ style: "display: flex; justify-content: flex-end; margin-bottom: 2em;" }, - form({ method: 'POST', action: '/ai/clear', style: "display: inline;" }, - button({ - type: 'submit', - style: ` - background: #b80c09; - color: #fff; - border: none; - padding: 0.4em 1.2em; - border-radius: 6px; - cursor: pointer; - font-size: 1em; - margin-left: 1em; - ` - }, i18n.aiClearHistory) - ) - ), - br(), - ...history.map(entry => - div({ - class: 'chat-entry', - style: ` - margin-bottom: 2em; - position: relative; - background: #191919; - border-radius: 10px; - box-shadow: 0 0 8px #0004; - padding-top: 1.8em; - ` - }, - entry.timestamp ? span({ - style: ` - position: absolute; - top: 0.5em; - right: 1.3em; - font-size: 0.92em; - color: #888; - ` - }, new Date(entry.timestamp).toLocaleString()) : null, - br(), br(), - div({ class: 'user-question', style: 'margin-bottom: 0.75em;' }, - h2(`${i18n.aiUserQuestion}:`), - p(...renderUrl(entry.question)) - ), - div({ - class: 'ai-response', - style: ` - max-width: 800px; - margin: auto; - background: #111; - padding: 1.25em; - border-radius: 6px; - font-family: sans-serif; - line-height: 1.6; - color: #ffcc00; - ` - }, - h2(`${i18n.aiResponseTitle}:`), - ...String(entry.answer || '') - .split('\n\n') - .flatMap(paragraph => - paragraph - .split('\n') - .map(line => - p({ style: "margin-bottom: 1.2em;" }, ...renderUrl(line.trim())) - ) - ) - ), - div({ - class: 'ai-train-bar', - style: ` - display:flex; - align-items:center; - gap:12px; - margin: 12px auto 8px auto; - max-width: 800px; - padding: 8px 0; - border-top: 1px solid #2a2a2a; - flex-wrap: wrap; - ` - }, - Array.isArray(entry.snippets) && entry.snippets.length - ? span({ style: 'color:#9aa; font-size:0.95em;' }, `${i18n.aiSnippetsUsed}: ${entry.snippets.length}`) - : null, - h2(`${i18n.statsAITraining}:`), - entry.trainStatus === 'approved' - ? span({ style: 'color:#5ad25a; font-weight:600;' }, i18n.aiTrainApproved) - : entry.trainStatus === 'rejected' - ? span({ style: 'color:#ff6b6b; font-weight:600;' }, i18n.aiTrainRejected) - : null, - entry.trainStatus === 'approved' || entry.trainStatus === 'rejected' - ? null - : div({ class: 'ai-approve-block' }, - form({ method: 'POST', action: '/ai/approve', class: 'ai-approve-form' }, - input({ type: 'hidden', name: 'ts', value: String(entry.timestamp) }), - div({ class: 'ai-approve-meta' }, - label({ class: 'ai-approve-meta-label' }, i18n.aiApproveTagsLabel || 'Tags (comma-separated)'), - input({ type: 'text', name: 'tags', placeholder: i18n.aiApproveTagsPlaceholder || 'e.g. oasis, governance, ecology', maxlength: '160' }), - label({ class: 'ai-approve-meta-label' }, i18n.aiApproveRatingLabel || 'Rating'), - select({ name: 'rating' }, - option({ value: '0' }, '—'), - option({ value: '1' }, '★'), - option({ value: '2' }, '★★'), - option({ value: '3' }, '★★★'), - option({ value: '4' }, '★★★★'), - option({ value: '5' }, '★★★★★') - ) - ), - textarea({ name: 'custom', rows: 3, placeholder: i18n.aiCustomAnswerPlaceholder, class: 'ai-approve-custom' }), - div({ class: 'ai-approve-actions' }, - button({ type: 'submit', class: 'approve-btn' }, i18n.aiApproveTrain) - ) - ), - form({ method: 'POST', action: '/ai/reject', class: 'ai-approve-reject' }, - input({ type: 'hidden', name: 'ts', value: String(entry.timestamp) }), - button({ type: 'submit', class: 'reject-btn' }, i18n.aiRejectTrain) - ) - ) - ) + p(i18n.aiDescription) + ), + statusBanner(status), + div({ class: 'chat-workspace ai-workspace' }, + ordered.length ? div({ class: 'chat-messages-list ai-messages-list' }, ...ordered.flatMap((e, i) => renderEntry(e, { split: split && i === ordered.length - 1 }))) : null, + thinking ? null : div({ class: 'chat-message-form ai-message-form' }, + form({ id: 'ai-prompt-form', method: 'POST', action: '/ai', class: 'ai-prompt-form' }, + textarea({ maxlength: "5000", name: 'input', rows: 3, placeholder: i18n.aiInputPlaceholder, required: true }) + ), + div({ class: 'ai-submit-row' }, + stateChip('waiting'), + button({ type: 'submit', form: 'ai-prompt-form' }, i18n.aiSubmitButton), + ordered.length ? form({ method: 'POST', action: '/ai/clear', class: 'ai-clear-form' }, + button({ type: 'submit', class: 'ai-clear-btn' }, i18n.aiClearHistory) + ) : null ) ) ) ) ); }; + +exports.SPLIT_MARKER = SPLIT_MARKER; diff --git a/src/views/activity_view.js b/src/views/activity_view.js index fe9fb95..842979a 100644 --- a/src/views/activity_view.js +++ b/src/views/activity_view.js @@ -1,35 +1,52 @@ -const { div, h2, p, section, button, form, a, input, img, textarea, br, span, video: videoHyperaxe, audio: audioHyperaxe, table, tr, td, th, details, summary, ul, li, label } = require("../server/node_modules/hyperaxe"); -const { template, i18n, userLink, userLinkLabel, renderSpreadButton, renderContentActions } = require('./main_views'); +const { div, h2, p, section, button, form, a, input, img, textarea, br, span, video: videoHyperaxe, audio: audioHyperaxe, table, tr, td, th, details, summary } = require("../server/node_modules/hyperaxe"); +const RAW_HTML = Symbol.for('oasis.rawHtml'); +const { template, i18n, paged, userLink, userLinkLabel, renderStateChip, renderPhoneChip, renderSpreadButton, renderContentActions, renderVotesSummary, renderModuleStats, renderCardMetaRow, renderTorrentDownload, renderTorrentSourceDownload, renderFileDownloads, torrentDownloadHref, CONTENT_FAV_KIND, CONTENT_SPREADABLE, CONTENT_TORRENTABLE, contentDeleteAction } = require('./main_views'); const opinionCategories = require('../backend/opinion_categories'); +const { roomNumberOf } = require('../models/phone_number'); -const OPINION_TYPES = new Set(['bookmark','votes','feed','image','audio','video','document','torrent','transfer']); +const OPINION_TYPES = new Set(['bookmark','votes','feed','image','audio','video','document','torrent','file']); +const TORRENTABLE_TYPES = CONTENT_TORRENTABLE; const OPINION_ROUTES = { feed: (id) => `/feed/opinions/${encodeURIComponent(id)}`, bookmark: (id) => `/bookmarks/opinions/${encodeURIComponent(id)}`, image: (id) => `/images/opinions/${encodeURIComponent(id)}`, audio: (id) => `/audios/opinions/${encodeURIComponent(id)}`, torrent: (id) => `/torrents/opinions/${encodeURIComponent(id)}`, + file: (id) => `/files/opinions/${encodeURIComponent(id)}`, video: (id) => `/videos/opinions/${encodeURIComponent(id)}`, document: (id) => `/documents/opinions/${encodeURIComponent(id)}`, - votes: (id) => `/votes/opinions/${encodeURIComponent(id)}`, - transfer: (id) => `/transfers/opinions/${encodeURIComponent(id)}` + votes: (id) => `/votes/opinions/${encodeURIComponent(id)}` }; const moment = require("../server/node_modules/moment"); -const { renderUrl } = require('../backend/renderUrl'); +const { renderStyledText, escapeHtml, safeExternalHref } = require('../backend/renderStyledText'); +const { letterOf } = require('./polls_view'); const { getConfig } = require("../configs/config-manager.js"); const { sanitizeHtml } = require('../backend/sanitizeHtml'); +const { renderZoomableImage } = require('./gallery_view'); +const { renderTribeOriginChip } = require('./clearnet_view'); + +const MEDIA_MD_RE = /!?\[(?:image|video|audio|pdf|torrent)[^\]]*\]\(\s*&[^)\s]+\s*\)/g +const stripMediaMarkdown = (text) => String(text || '').replace(MEDIA_MD_RE, '').replace(/\n{3,}/g, '\n\n').trim() +const renderMediaObject = (m, href = null) => { + if (!m || typeof m !== 'object' || !m.blobId) return null + const src = `/blob/${encodeURIComponent(String(m.blobId))}` + if (href && m.kind !== 'video' && m.kind !== 'audio') return div({ class: 'card-field image-container media-left' }, a({ href, class: 'podcast-cover-link' }, img({ src, class: 'post-image', alt: '' }))) + if (m.kind === 'video') return div({ class: 'card-field video-container' }, videoHyperaxe({ controls: true, class: 'post-video', src, preload: 'metadata' })) + if (m.kind === 'audio') return div({ class: 'card-field audio-container' }, audioHyperaxe({ controls: true, class: 'post-audio', src, preload: 'metadata' })) + return div({ class: 'card-field image-container media-left' }, renderZoomableImage(src, { imgClass: 'post-image' })) +} const renderMediaBlob = (value, fallbackSrc = null) => { if (!value) return fallbackSrc ? img({ src: fallbackSrc, class: 'post-image' }) : null const s = String(value).trim() if (!s) return fallbackSrc ? img({ src: fallbackSrc, class: 'post-image' }) : null - if (s.startsWith('&')) return img({ src: `/blob/${encodeURIComponent(s)}`, class: 'post-image' }) + if (s.startsWith('&')) return renderZoomableImage(`/blob/${encodeURIComponent(s)}`, { imgClass: 'post-image' }) const mVideo = s.match(/\[video:[^\]]*\]\(\s*(&[^)\s]+\.sha256)\s*\)/) - if (mVideo) return videoHyperaxe({ controls: true, class: 'post-video', src: `/blob/${encodeURIComponent(mVideo[1])}` }) + if (mVideo) return videoHyperaxe({ controls: true, class: 'post-video', src: `/blob/${encodeURIComponent(mVideo[1])}`, preload: 'metadata' }) const mAudio = s.match(/\[audio:[^\]]*\]\(\s*(&[^)\s]+\.sha256)\s*\)/) if (mAudio) return audioHyperaxe({ controls: true, class: 'post-audio', src: `/blob/${encodeURIComponent(mAudio[1])}` }) const mImg = s.match(/!\[[^\]]*\]\(\s*(&[^)\s]+\.sha256)\s*\)/) - if (mImg) return img({ src: `/blob/${encodeURIComponent(mImg[1])}`, class: 'post-image' }) + if (mImg) return renderZoomableImage(`/blob/${encodeURIComponent(mImg[1])}`, { imgClass: 'post-image' }) return fallbackSrc ? img({ src: fallbackSrc, class: 'post-image' }) : null } @@ -41,7 +58,7 @@ function cleanFeedText(t) { } function isValidFeedText(t) { - const s = cleanFeedText(t); + const s = cleanFeedText(t).replace(/!?\[[^\]\n]*\]\(\s*&[^)\s]+\.sha256\s*\)/g, '').trim(); return s.length >= FEED_TEXT_MIN && s.length <= FEED_TEXT_MAX; } @@ -109,25 +126,7 @@ const decodeMaybe = (s) => { try { return decodeURIComponent(String(s || '')); } catch { return String(s || ''); } }; -const rewriteHashtagLinks = (html) => { - const s = String(html || ''); - return s.replace(/href=(["'])(?:https?:\/\/[^"']+)?\/hashtag\/([^"'?#]+)([^"']*)\1/g, (m, q, tag, rest) => { - const t = decodeMaybe(tag).replace(/^#/, '').trim().toLowerCase(); - const href = `/search?query=%23${encodeURIComponent(t)}`; - return `href=${q}${href}${q}`; - }); -}; -function renderUrlPreserveNewlines(text) { - const s = String(text || ''); - const lines = s.split(/\r\n|\r|\n/); - const out = []; - for (let i = 0; i < lines.length; i++) { - if (i) out.push(br()); - out.push(...renderUrl(lines[i])); - } - return out; -} function getThreadIdFromPost(action) { const c = action.value?.content || action.content || {}; @@ -199,6 +198,8 @@ function buildActivityItemsWithPostThreads(deduped, allActions) { .slice() .sort((a, b) => (a.ts || 0) - (b.ts || 0)); + if (!rootAction) continue; + const rootType = rootAction.type || 'post'; out.push({ id: `thread:${threadId}`, type: 'postThread', @@ -206,18 +207,21 @@ function buildActivityItemsWithPostThreads(deduped, allActions) { ts: latest.ts, content: { threadId, + rootType, + rootHref: rootType === 'post' ? `/blogs/${encodeURIComponent(threadId)}` : getViewDetailsAction(rootType, rootAction), root: rootAction ? { id: safeMsgId(rootAction), author: rootAction.author, - text: excerptPostText(rootAction.value?.content || rootAction.content || {}, 240) + subject: String((rootAction.value?.content || rootAction.content || {}).contentWarning || '').trim(), + text: excerptPostText(rootAction.value?.content || rootAction.content || {}, 600) } : null, replies: replies.map(p => ({ id: safeMsgId(p), author: p.author, ts: p.ts, - text: excerptPostText(p.value?.content || p.content || {}, 200) + text: excerptPostText(p.value?.content || p.content || {}, 600) })) } }); @@ -228,7 +232,50 @@ function buildActivityItemsWithPostThreads(deduped, allActions) { } exports.renderActionCards = renderActionCards; -function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { +const SPREADABLE_TYPES = CONTENT_SPREADABLE; + +const TRIBE_OPENED_TYPE = { event: 'typeEvent', task: 'typeTask', votation: 'typeVotes', forum: 'typeForum', feed: 'typeFeed', report: 'typeReport', image: 'typeImage', audio: 'typeAudio', video: 'typeVideo', document: 'typeDocument', bookmark: 'typeBookmark', torrent: 'typeTorrent' }; +const TRIBE_OPENED_FILTER = { event: 'event', task: 'task', votation: 'votes', forum: 'forum', feed: 'feed', report: 'report', image: 'image', audio: 'audio', video: 'video', document: 'document', bookmark: 'bookmark', torrent: 'torrent' }; +const tribeOpenedKey = (action) => { + const c = (action && action.content) || {}; + return c.contentType === 'media' ? String(c.mediaType || '') : String(c.contentType || ''); +}; +const openedIn = (action, filter) => !!action && action.type === 'tribeOpened' && TRIBE_OPENED_FILTER[tribeOpenedKey(action)] === filter; + +const renderTribeOpenedCard = (action, date, authorName) => { + const c = action.content || {}; + const origin = c.tribeOrigin || {}; + const key = tribeOpenedKey(action); + const label = String(i18n[TRIBE_OPENED_TYPE[key]] || key || '').toUpperCase(); + const blob = typeof c.image === 'string' && c.image.startsWith('&') ? `/blob/${encodeURIComponent(c.image)}` : null; + const media = !blob ? null + : c.mediaType === 'video' ? videoHyperaxe({ controls: true, class: 'post-video', src: blob, preload: 'metadata' }) + : c.mediaType === 'audio' ? audioHyperaxe({ controls: true, class: 'post-audio', src: blob }) + : c.mediaType === 'image' ? renderZoomableImage(blob, { imgClass: 'post-image' }) + : null; + const bookmarkHref = c.mediaType === 'bookmark' ? safeExternalHref(c.url) : ''; + return div({ class: 'trending-card' }, + div({ class: 'card-header activity-card-header' }, + div({ class: 'card-chips-row' }, + span({ class: 'pm-exposition-chip pm-exposition-whole' }, span({ class: 'pm-exposition-text' }, label)), + renderTribeOriginChip(origin) + ) + ), + div({ class: 'card-section' }, + c.title ? h2(origin.href ? a({ href: origin.href }, c.title) : c.title) : null, + c.description ? p({ class: 'post-text post-text-pre' }, ...renderStyledText(String(c.description).slice(0, 600))) : null, + c.date || c.location ? p({ class: 'post-text' }, [c.date, c.location].filter(Boolean).join(' · ')) : null, + media, + bookmarkHref ? a({ href: bookmarkHref, target: '_blank', rel: 'noopener noreferrer' }, c.url) : null + ), + p({ class: 'card-footer' }, + span({ class: 'date-link' }, `${date}`), + userLink(action.author, authorName) + ) + ); +}; + +function renderActionCards(actions, userId, allActions, spreadMap = new Map(), extras = {}) { const all = Array.isArray(allActions) ? allActions : actions; const byIdAll = new Map(); for (const a0 of all) { @@ -289,7 +336,8 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { } const seenDocumentTitles = new Set(); - const items = buildActivityItemsWithPostThreads(deduped, all); + const grouped = buildActivityItemsWithPostThreads(deduped, all); + const items = extras && extras.paged ? paged(grouped) : grouped; const spreadOrdinalById = new Map(); const spreadsByLink = new Map(); @@ -323,8 +371,9 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { } const cards = items.map(action => { - const date = action.ts ? new Date(action.ts).toLocaleString() : ""; + const date = action.ts ? moment(action.ts).format("YYYY/MM/DD HH:mm") : ""; const type = action.type || 'unknown'; + if (type === 'tribeOpened') return renderTribeOpenedCard(action, date, (action.authorNames && action.authorNames[action.author]) || getProfile(action.author).name); let skip = false; let headerText; if (type.startsWith('parliament')) { @@ -342,14 +391,18 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { headerText = `[${String(i18n.typeTask || 'TASK').toUpperCase()} · ASSIGNMENT]`; } else if (type === 'shopProduct') { headerText = `[SHOP · PRODUCT]`; - } else if (type === 'chat') { - headerText = `[CHAT \u00b7 NEW]`; } else if (type === 'pad') { headerText = `[PAD · ${String(i18n.padNew || 'NEW').toUpperCase()}]`; - } else if (type === 'ubiClaim') { - headerText = `[UBI · CLAIM]`; - } else if (type === 'ubiclaimresult') { - headerText = `[UBI · RESULT]`; + } else if (type === 'wikiPage') { + headerText = `[${String(i18n.typeWiki || 'WIKI').toUpperCase()}]`; + } else if (type === 'emergencyUpdate') { + headerText = `[${String(i18n.typeEmergency || 'EMERGENCY').toUpperCase()} · ${String(i18n.emergencyLastUpdate || 'LAST UPDATE').toUpperCase()}]`; + } else if (type === 'campaignUpdate') { + headerText = `[${String(i18n.typeCampaign || 'CAMPAIGN').toUpperCase()} · ${String(i18n.emergencyLastUpdate || 'LAST UPDATE').toUpperCase()}]`; + } else if (type === 'podcastEpisode') { + headerText = `[${String(i18n.typePodcast || 'PODCAST').toUpperCase()} · ${String(i18n.typePodcastEpisode || 'EPISODE').toUpperCase()}]`; + } else if (type === 'larpHousePost') { + headerText = `[${String(i18n.typeLarp || 'L.A.R.P.').toUpperCase()}]`; } else { const typeLabel = i18n[`type${capitalize(type)}`] || type; headerText = `[${String(typeLabel).toUpperCase()}]`; @@ -371,7 +424,7 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { const voteOutcome = (() => { const totalNum = Number(totalVotes || 0); - const noResult = { text: i18n.voteNoQuorum || 'NO QUORUM', color: '#ffcc00' }; + const noResult = { text: i18n.voteNoQuorum || 'NO QUORUM', tone: 'none' }; if (totalNum < 2) return noResult; const entries = Object.entries(votes || {}).filter(([o]) => o !== 'FOLLOW_MAJORITY'); const maxCount = entries.reduce((m, [, c]) => Math.max(m, Number(c) || 0), 0); @@ -379,7 +432,7 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { if (maxCount === 0 || top.length > 1) return noResult; const w = top[0][0]; const label = i18n['vote' + w.split('_').map(x => x.charAt(0) + x.slice(1).toLowerCase()).join('')] || w; - return { text: label, color: w === 'YES' ? '#4caf50' : w === 'NO' ? '#e53935' : '#ffcc00' }; + return { text: label, tone: w === 'YES' ? 'yes' : w === 'NO' ? 'no' : 'none' }; })(); cardBody.push( @@ -392,7 +445,7 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { div( { class: 'card-field' }, span({ class: 'card-label' }, i18n.deadline + ':'), - span({ class: 'card-value' }, deadline ? new Date(deadline).toLocaleString() : '') + span({ class: 'card-value' }, deadline ? moment(deadline).format("YYYY/MM/DD HH:mm") : '') ), div( { class: 'card-field' }, @@ -402,7 +455,7 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { div( { class: 'card-field' }, span({ class: 'card-label' }, (i18n.voteResults || 'Results') + ':'), - span({ class: 'card-value', style: `color:${voteOutcome.color};font-weight:bold;` }, voteOutcome.text) + span({ class: `card-value vote-outcome vote-outcome-${voteOutcome.tone}` }, voteOutcome.text) ), div( { class: 'card-field' }, @@ -423,7 +476,7 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { div({ class: 'card-section transfer' }, div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.concept + ':'), span({ class: 'card-value' }, concept)), div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.amount + ':'), span({ class: 'card-value' }, amount)), - div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.deadline + ':'), span({ class: 'card-value' }, deadline ? new Date(deadline).toLocaleString() : '')), + deadline ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.deadline + ':'), span({ class: 'card-value' }, moment(deadline).format("YYYY/MM/DD HH:mm"))) : null, div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.status + ':'), span({ class: 'card-value' }, status)) ) ); @@ -441,90 +494,8 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { ); } - if (type === 'bankClaim') { - const { amount, epochId, allocationId, txid } = content; - const amt = Number(amount || 0); - cardBody.push( - div({ class: 'card-section banking-claim' }, - div({ class: 'card-field' }, - span({ class: 'card-label' }, i18n.bankUbiReceived + ':' ), - span({ class: 'card-value' }, `${amt.toFixed(6)} ECO`) - ), - epochId ? div({ class: 'card-field' }, - span({ class: 'card-label' }, i18n.bankEpochShort + ':' ), - span({ class: 'card-value' }, epochId) - ) : "", - allocationId ? div({ class: 'card-field' }, - span({ class: 'card-label' }, i18n.bankAllocId + ':' ), - span({ class: 'card-value' }, allocationId) - ) : "", - txid ? div({ class: 'card-field' }, - span({ class: 'card-label' }, i18n.bankTx + ':' ), - a({ href: `https://ecoin.03c8.net/blockexplorer/search?q=${txid}`, target: '_blank' }, txid) - ) : "" - ) - ); - } - if (type === 'ubiClaim') { - const { pubId, amount, epochId, claimedAt } = content; - const amt = Number(amount || 0); - const inhabitantId = action.author || ''; - cardBody.push( - div({ class: 'card-section banking-ubi' }, - div({ class: 'card-field' }, - span({ class: 'card-label' }, i18n.bankUbiInhabitant + ':'), - span({ class: 'card-value' }, userLink(inhabitantId)) - ), - pubId ? div({ class: 'card-field' }, - span({ class: 'card-label' }, i18n.bankUbiPub + ':'), - span({ class: 'card-value' }, userLink(pubId)) - ) : "", - div({ class: 'card-field' }, - span({ class: 'card-label' }, i18n.bankUbiClaimedAmount + ':'), - span({ class: 'card-value' }, `${amt.toFixed(6)} ECO`) - ), - epochId ? div({ class: 'card-field' }, - span({ class: 'card-label' }, i18n.bankEpochShort + ':'), - span({ class: 'card-value' }, epochId) - ) : "", - div({ class: 'card-field' }, - span({ class: 'card-label' }, i18n.status + ':'), - span({ class: 'card-value' }, 'UNCONFIRMED') - ), - claimedAt ? div({ class: 'card-field' }, - span({ class: 'card-label' }, i18n.date + ':'), - span({ class: 'card-value' }, moment(claimedAt).format('YYYY-MM-DD HH:mm:ss')) - ) : "" - ) - ); - } - if (type === 'ubiclaimresult') { - const { txid, userId: inhabitantId, amount, epochId } = content; - const pubAuthor = action.author || action.value?.author || ''; - const amt = Number(amount || 0); - cardBody.push( - div({ class: 'card-section banking-ubi' }, - div({ class: 'card-field' }, - span({ class: 'card-label' }, i18n.bankUbiPub + ':'), - span({ class: 'card-value' }, pubAuthor) - ), - div({ class: 'card-field' }, - span({ class: 'card-label' }, i18n.bankUbiInhabitant + ':'), - span({ class: 'card-value' }, inhabitantId || '') - ), - div({ class: 'card-field' }, - span({ class: 'card-label' }, i18n.bankUbiClaimedAmount + ':'), - span({ class: 'card-value' }, `${amt.toFixed(6)} ECO`) - ), - txid ? div({ class: 'card-field' }, - span({ class: 'card-label' }, i18n.bankTx + ':'), - a({ href: `https://ecoin.03c8.net/blockexplorer/search?q=${txid}`, target: '_blank' }, txid) - ) : "" - ) - ); - } if (type === 'pixelia') { const { author } = content; @@ -545,35 +516,25 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { img({ src: `/assets/larp/images/${encodeURIComponent(String(house).toLowerCase())}.jpg`, alt: house, class: 'larp-house-mini' }), span(`L.A.R.P · ${String(house).toUpperCase()}`) ) : null, - text ? p({ class: 'post-text post-text-pre' }, ...renderUrlPreserveNewlines(String(text))) : null + text ? p({ class: 'post-text post-text-pre' }, ...renderStyledText(String(text))) : null ) ); } if (type === 'tribe') { - const { title, image, description, location, tags, inviteMode, isAnonymous } = content; + const { title, image, description, isAnonymous } = content; if (isAnonymous === true) { skip = true; } - const validTags = Array.isArray(tags) ? tags : []; cardBody.push( div({ class: 'card-section tribe' }, h2({ class: 'tribe-title' }, a({ href: `/tribe/${encodeURIComponent(action.id)}`, class: "user-link" }, title) ), - div({ style: 'display:flex; gap:.6em; flex-wrap:wrap;' }, - location ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.tribeLocationLabel.toUpperCase()) + ':'), span({ class: 'card-value' }, ...renderUrl(location))) : "", - typeof isAnonymous === 'boolean' ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.tribeIsAnonymousLabel+ ':'), span({ class: 'card-value' }, isAnonymous ? i18n.tribePrivate : i18n.tribePublic)) : "", - inviteMode ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.tribeModeLabel) + ':'), span({ class: 'card-value' }, inviteMode.toUpperCase())) : "" - ), image ? (/^(\/|https?:)/.test(String(image)) - ? img({ src: image, class: 'feed-image tribe-image' }) + ? renderZoomableImage(image, { imgClass: 'feed-image tribe-image' }) : renderMediaBlob(image, null)) : null, - p({ class: 'tribe-description' }, ...renderUrl(description || '')), - validTags.length - ? div({ class: 'card-tags' }, validTags.map(tag => - a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link" }, `#${tag}`))) - : "" + p({ class: 'tribe-description' }, ...renderStyledText(description || '')) ) ); } @@ -605,11 +566,11 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { photo ? [ br(), - img({ class: "cv-photo", src: `/blob/${encodeURIComponent(photo)}` }), + renderZoomableImage(`/blob/${encodeURIComponent(photo)}`, { imgClass: 'cv-photo', linkClass: 'zoom-inline' }), br() ] : "", - p(...renderUrl(description || "")), + p(...renderStyledText(description || "")), personalSkills && personalSkills.length ? div({ class: 'card-tags' }, personalSkills.map(skill => a({ href: `/search?query=%23${encodeURIComponent(skill)}`, class: "tag-link" }, `#${skill}`) @@ -634,7 +595,7 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { const { url } = content; cardBody.push( div({ class: 'card-section image' }, - img({ src: `/blob/${encodeURIComponent(url)}`, class: 'post-image' }) + renderZoomableImage(`/blob/${encodeURIComponent(url)}`, { imgClass: 'post-image' }) ) ); } @@ -644,7 +605,7 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { const mapKey = action.id || action.key || ''; cardBody.push( div({ class: 'card-section map' }, - title ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.mapTitle || 'Map') + ':'), span({ class: 'card-value' }, mapKey ? a({ href: `/maps/${encodeURIComponent(mapKey)}`, class: 'user-link' }, title) : title)) : "", + title ? div({ class: 'card-field' }, span({ class: 'card-value' }, mapKey ? a({ href: `/maps/${encodeURIComponent(mapKey)}`, class: 'user-link' }, title) : title)) : "", div({ class: 'map-card-info' }, span({ class: 'map-type-badge' }, mapType || 'SINGLE'), span({ class: 'map-coords' }, `${(parseFloat(lat) || 0).toFixed(4)}, ${(parseFloat(lng) || 0).toFixed(4)}`) @@ -705,17 +666,28 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { } if (type === 'torrent') { - const { title } = content; + const { title, url, source, sourceName } = content; cardBody.push( div({ class: 'card-section' }, - title ? div({ class: 'card-field' }, - span({ class: 'card-label' }, (i18n.torrentTitleLabel || 'Title') + ':'), - span({ class: 'card-value' }, title) + title?.trim() ? h2({ class: 'torrent-title' }, title) : "", + url ? div({ class: 'torrent-card-actions' }, + renderTorrentSourceDownload(action.tipId || action.id || action.key, source), + renderTorrentDownload(torrentDownloadHref(url, title)) ) : null ) ); } + if (type === 'file') { + const { title, url, fileName, torrentUrl } = content; + cardBody.push( + div({ class: 'card-section' }, + title?.trim() ? h2({ class: 'torrent-title' }, title) : "", + url ? renderFileDownloads(action.tipId || action.id || action.key, torrentUrl, fileName || title) : null + ) + ); + } + if (type === 'document') { const { url, title, key } = content; const docKey = title ? `${action.author || ''}::${title.trim()}` : null; @@ -739,22 +711,21 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { const { url } = content; cardBody.push( div({ class: 'card-section bookmark' }, - h2(url ? p(a({ href: url, target: '_blank', class: "bookmark-url" }, url)) : "") + url ? div({ class: 'card-field' }, a({ href: safeExternalHref(url), target: '_blank', rel: 'noreferrer noopener', class: 'bookmark-url' }, url)) : '' ) ); } if (type === 'event') { - const { title, description, date, location, price, attendees, organizer, isPublic } = content; + const { title, description, date, location, price, attendees, isPublic } = content; cardBody.push( div({ class: 'card-section event' }, div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.title + ':'), span({ class: 'card-value' }, title)), - date ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.date + ':'), span({ class: 'card-value' }, new Date(date).toLocaleString())) : "", + date ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.date + ':'), span({ class: 'card-value' }, moment(date).format("YYYY/MM/DD HH:mm"))) : "", location ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.location || 'Location') + ':'), span({ class: 'card-value' }, location)) : "", typeof isPublic === 'boolean' ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.isPublic || 'Public') + ':'), span({ class: 'card-value' }, isPublic ? 'Yes' : 'No')) : "", - price ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.price || 'Price') + ':'), span({ class: 'card-value' }, price + " ECO")) : "", + Number(price) > 0 ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.price || 'Price') + ':'), span({ class: 'card-value' }, price + " ECO")) : "", br(), - organizer ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.organizer || 'Organizer') + ': '), userLink(organizer)) : "", Array.isArray(attendees) ? h2({ class: 'card-label' }, (i18n.attendees || 'Attendees') + ': ' + attendees.length) : "" ) ); @@ -766,8 +737,8 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { div({ class: 'card-section task' }, div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.title + ':'), span({ class: 'card-value' }, title)), priority ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.priority || 'Priority') + ':'), span({ class: 'card-value' }, priority)) : "", - startTime ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.taskStartTimeLabel || 'Start') + ':'), span({ class: 'card-value' }, new Date(startTime).toLocaleString())) : "", - endTime ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.taskEndTimeLabel || 'End') + ':'), span({ class: 'card-value' }, new Date(endTime).toLocaleString())) : "", + startTime ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.taskStartTimeLabel || 'Start') + ':'), span({ class: 'card-value' }, moment(startTime).format("YYYY/MM/DD HH:mm"))) : "", + endTime ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.taskEndTimeLabel || 'End') + ':'), span({ class: 'card-value' }, moment(endTime).format("YYYY/MM/DD HH:mm"))) : "", status ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.status + ':'), span({ class: 'card-value' }, status)) : "" ) ); @@ -802,15 +773,15 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { } if (type === 'feed') { - const { renderTextWithStyles } = require('../backend/renderTextWithStyles'); + const { renderStyledHtml } = require('../backend/renderStyledText'); const { text, refeeds } = content; if (!isValidFeedText(text)) return null; const safeText = cleanFeedText(text); - const htmlText = safeText ? rewriteHashtagLinks(renderTextWithStyles(safeText)) : ''; + const textNodes = safeText ? renderStyledText(safeText, { zoomImages: true }) : []; const refeedsNum = Number(refeeds || 0) || 0; cardBody.push( div({ class: 'card-section feed' }, - div({ class: 'feed-text', innerHTML: sanitizeHtml(htmlText) }), + div({ class: 'feed-text' }, ...textNodes), refeedsNum > 0 ? h2({ class: 'card-field' }, span({ class: 'card-label' }, i18n.tribeFeedRefeeds + ': '), @@ -824,7 +795,7 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { if (type === 'post') { const { contentWarning, text } = content || {}; const rawText = text || ''; - const POST_TRUNCATE_LEN = 300; + const POST_TRUNCATE_LEN = 1000; const isTruncated = rawText.length > POST_TRUNCATE_LEN; const displayText = isTruncated ? rawText.slice(0, POST_TRUNCATE_LEN) + '…' : rawText; const isHtml = typeof displayText === 'string' && /<\/?[a-z][\s\S]*>/i.test(displayText); @@ -838,9 +809,11 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { (url) => `${url}` ); - bodyNode = div({ class: 'post-text', style: 'max-height:180px;overflow:hidden;', innerHTML: sanitizeHtml(linkified) }); + bodyNode = div({ class: 'feed-text post-text post-text-clamped', [RAW_HTML]: sanitizeHtml(linkified) }); } else { - bodyNode = p({ class: 'post-text post-text-pre', style: 'max-height:180px;overflow:hidden;' }, ...renderUrlPreserveNewlines(displayText)); + bodyNode = div({ class: 'feed-text' }, + p({ class: 'post-text post-text-pre post-text-clamped' }, ...renderStyledText(displayText)) + ); } const threadId = getThreadIdFromPost(action); const replyToId = getReplyToIdFromPost(action, byIdAll); @@ -855,20 +828,19 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { div({ class: 'card-section post' }, isReply ? div( - { class: 'reply-context', style: 'border-left:3px solid #666;padding-left:10px;margin-bottom:8px;opacity:0.85;' }, - span({ style: 'font-size:0.85em;' }, + { class: 'reply-context' }, + span({ class: 'reply-context-label' }, a({ href: ctxHref, class: 'tag-link' }, i18n.inReplyTo || 'IN REPLY TO'), parentAuthor ? span(' ', userLink(parentAuthor, parentName)) : '' ), - parentText ? p({ class: 'post-text reply-context-text post-text-pre', style: 'font-size:0.85em;max-height:80px;overflow:hidden;margin-top:4px;' }, ...renderUrlPreserveNewlines(parentText)) : '' + parentText ? p({ class: 'post-text reply-context-text post-text-pre' }, ...renderStyledText(parentText)) : '' ) : '', contentWarning ? h2({ class: 'content-warning' }, contentWarning) : '', bodyNode, - isTruncated && threadId - ? div({ style: 'margin-top:6px;' }, - a({ href: `/thread/${encodeURIComponent(threadId)}#${encodeURIComponent(action.id || threadId)}`, class: 'filter-btn' }, i18n.keepReading || 'Keep reading...') - ) + Array.isArray(content?.tags) && content.tags.filter(Boolean).length + ? div({ class: 'card-tags' }, content.tags.filter(Boolean).map(tag => + a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link" }, `#${tag}`))) : '' ) ); @@ -877,49 +849,125 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { if (type === 'postThread') { const c = action.content || {}; const threadId = c.threadId; - const href = `/thread/${encodeURIComponent(threadId)}#${encodeURIComponent(threadId)}`; + const rootType = c.rootType || 'post'; + const isBlogThread = rootType === 'post'; + const baseHref = c.rootHref || `/blogs/${encodeURIComponent(threadId)}`; + const href = isBlogThread ? baseHref : `${baseHref}${baseHref.includes('?') ? '&' : '?'}comments=open#comments-latest`; + const rootTypeLabel = isBlogThread + ? (i18n.typePost || 'BLOG') + : (i18n[`type${rootType.charAt(0).toUpperCase()}${rootType.slice(1)}`] || rootType); const root = c.root; const replies = Array.isArray(c.replies) ? c.replies : []; const repliesAsc = replies.slice().sort((a, b) => (a.ts || 0) - (b.ts || 0)); - const limit = 5; - const overflow = repliesAsc.length > limit; - const show = repliesAsc.slice(Math.max(0, repliesAsc.length - limit)); - const lastId = repliesAsc.length ? repliesAsc[repliesAsc.length - 1].id : threadId; - const viewMoreHref = `/thread/${encodeURIComponent(threadId)}#${encodeURIComponent(lastId)}`; + const latest = repliesAsc.length ? repliesAsc[repliesAsc.length - 1] : null; + const titleSource = root ? (root.subject || root.text || '') : ''; + const titleText = titleSource + ? (titleSource.length > 90 ? `${titleSource.slice(0, 90)}…` : titleSource) + : threadId; return div({ class: 'trending-card post-thread' + (String(action.author) === String(userId) ? ' own-content' : '') }, div({ class: 'card-header activity-card-header' }, div({ class: 'card-chips-row' }, span({ class: 'pm-exposition-chip pm-exposition-whole' }, - span({ class: 'pm-exposition-text' }, `${String(i18n.typePost || 'POST').toUpperCase()} · THREAD`) + span({ class: 'pm-exposition-text' }, `${String(rootTypeLabel).toUpperCase()} · ${String(i18n.activityUpdateLabel || 'UPDATE').toUpperCase()}`) ) ), - renderContentActions(threadId, href) + renderContentActions(threadId, baseHref, { + author: c.rootAuthor || (root && root.author) || action.author, + reportTitle: titleText, + spread: spreadMap.get(threadId) || null, + ...favOptsFor(rootType, threadId, extras) + }) ), div({ class: 'card-body' }, - root && root.text - ? div({ class: 'card-section' }, - p({ class: 'post-text', style: 'white-space:pre-wrap;' }, ...renderUrlPreserveNewlines(root.text)) - ) - : '', - div({ class: 'card-section' }, - show.map(r => { - const rDate = r.ts ? new Date(r.ts).toLocaleString() : ''; - return div({ class: 'thread-reply-item' }, - div({ class: 'thread-reply' }, - r.text ? p({ class: 'post-text', style: 'white-space:pre-wrap;' }, ...renderUrlPreserveNewlines(r.text)) : '' - ), - div({ class: 'card-footer thread-reply-footer' }, - span({ class: 'date-link' }, rDate), - userLink(r.author, action.authorNames && action.authorNames[r.author]) - ) - ); - }), + div({ class: 'card-section' }, + div({ class: 'card-field activity-update-title' }, + a({ href, class: 'card-value user-link' }, titleText), + span({ class: 'card-label activity-update-counts' }, `💬 ${replies.length}`) + ), + latest && latest.text + ? div({ class: 'feed-text activity-update-msg' }, + p({ class: 'post-text post-text-pre' }, ...renderStyledText(latest.text)) + ) + : '' + ) + ), + p({ class: 'card-footer' }, + span({ class: 'date-link' }, `${action.ts ? moment(action.ts).format("YYYY/MM/DD HH:mm") : ''}`), + userLink(action.author, action.authorNames && action.authorNames[action.author]) + ) + ); + } + + if (type === 'chatThread') { + const c = action.content || {}; + const chatRoot = c.chatRoot; + const href = `/chats/${encodeURIComponent(chatRoot)}`; + const latestHref = `${href}#chat-latest`; + const chatTitle = c.title || chatRoot; + const replies = Array.isArray(c.replies) ? c.replies : []; + const repliesAsc = replies.slice().sort((a, b) => (a.ts || 0) - (b.ts || 0)); + const limit = 2; + const show = repliesAsc.slice(Math.max(0, repliesAsc.length - limit)); + return div({ class: 'trending-card post-thread chat-thread' + (String(action.author) === String(userId) ? ' own-content' : '') }, + div({ class: 'card-header activity-card-header' }, + div({ class: 'card-chips-row' }, + span({ class: 'pm-exposition-chip pm-exposition-whole' }, + span({ class: 'pm-exposition-text' }, `${String(i18n.typeChat || 'CHAT').toUpperCase()} \u00b7 ${String(i18n.activityUpdateLabel || 'UPDATE').toUpperCase()}`) + ) + ), + renderContentActions(chatRoot, href, { + author: c.chatAuthor || action.author, + reportTitle: chatTitle, + spread: spreadMap.get(chatRoot) || null, + ...favOptsFor('chat', chatRoot, extras) + }) + ), + div({ class: 'card-body' }, + div({ class: 'card-section chat' }, + div({ class: 'card-field activity-update-title' }, + a({ href: latestHref, class: 'card-value user-link' }, chatTitle), + span({ class: 'card-label activity-update-counts' }, `👥: ${c.members || 0} · 💬 ${c.messageCount || show.length}`) + ), + (() => { + const latest = show.length ? show[show.length - 1] : null; + if (!latest) return ''; + const rawBlob = String(latest.image || '').trim(); + const attSrc = rawBlob.startsWith('&') ? `/blob/${encodeURIComponent(rawBlob)}` : null; + const mime = String(latest.mimeType || ''); + const attNode = attSrc + ? (mime.startsWith('video/') + ? videoHyperaxe({ controls: true, class: 'post-video', src: attSrc, type: mime, preload: 'metadata' }) + : mime.startsWith('audio/') + ? audioHyperaxe({ controls: true, class: 'post-audio', src: attSrc, type: mime, preload: 'metadata' }) + : mime === 'application/pdf' + ? a({ href: attSrc, target: '_blank', rel: 'noopener', class: 'filter-btn' }, '📄 PDF') + : mime.includes('bittorrent') || mime === 'application/x-torrent' + ? renderTorrentDownload(attSrc) + : renderZoomableImage(attSrc, { imgClass: 'post-image' })) + : renderMediaBlob(latest.image); + const textNode = latest.text + ? p({ class: 'post-text post-text-pre' }, ...(latest.poll ? [span({ class: 'card-label' }, `${String(i18n.pollInChat || 'Poll').toUpperCase()}: `)] : []), ...renderStyledText(latest.text)) + : null; + const quoted = latest.reply || null; + const replyNode = quoted + ? div({ class: 'reply-context' }, + span({ class: 'reply-context-meta' }, + a({ href: `${href}#msg-${String(quoted.id || '').replace(/[^a-zA-Z0-9]/g, '')}`, class: 'tag-link' }, i18n.inReplyTo || 'IN REPLY TO'), + quoted.author ? span(' ', userLink(quoted.author, action.authorNames && action.authorNames[quoted.author])) : '' + ), + quoted.text ? p({ class: 'post-text reply-context-text' }, quoted.text) : '' + ) + : null; + return (textNode || attNode || replyNode) + ? div({ class: 'feed-text activity-update-msg' }, ...[replyNode, textNode, attNode].filter(Boolean)) + : ''; + })() + ) + ), + p({ class: 'card-footer' }, + span({ class: 'date-link' }, `${action.ts ? moment(action.ts).format("YYYY/MM/DD HH:mm") : ''}`), + userLink(action.author, action.authorNames && action.authorNames[action.author]) ) - ), - p({ class: 'card-footer' }, - span({ class: 'date-link' }, `${action.ts ? new Date(action.ts).toLocaleString() : ''} ${i18n.performed} `), - userLink(action.author, action.authorNames && action.authorNames[action.author]) - ) ); } @@ -930,9 +978,10 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { const linkText = (title && String(title).trim()) ? title : ''; cardBody.push( div({ class: 'card-section forum' }, - div({ class: 'card-field', style: "font-size:1.12em; margin-bottom:5px;" }, - span({ class: 'card-label', style: "font-weight:800;color:#ff9800;" }, i18n.title + ': '), - a({ href: `/forum/${encodeURIComponent(linkKey)}`, style: "font-weight:800;color:#4fc3f7;" }, linkText) + div({ class: 'card-field' }, + linkKey + ? a({ href: `/forum/${encodeURIComponent(linkKey)}`, class: 'card-value user-link' }, linkText || linkKey) + : span({ class: 'card-value' }, linkText) ) ) ); @@ -941,21 +990,23 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { const parentForum = byIdAll.get(rootId) || actions.find(a => a.type === 'forum' && !a.content?.root && (a.id === rootId || a.content?.key === rootId)); const parentContent = parentForum ? (parentForum.value?.content || parentForum.content || {}) : {}; const parentTitle = (parentContent?.title && String(parentContent.title).trim()) ? parentContent.title : ((rootTitle && String(rootTitle).trim()) ? rootTitle : ''); - const parentAuthor = parentForum?.author || ''; - const parentName = parentForum?.authorName || ''; + const branchId = typeof content.branch === 'string' && content.branch !== rootId && content.branch !== rootKey ? content.branch : ''; + const branchMsg = branchId ? (byIdAll.get(branchId) || actions.find(a => a.type === 'forum' && (a.id === branchId || a.content?.key === branchId))) : null; + const repliedAuthor = branchMsg ? (branchMsg.author || '') : ''; + const repliedName = branchMsg ? (branchMsg.authorName || '') : ''; const hrefKey = rootKey || rootId; cardBody.push( div({ class: 'card-section forum' }, div( - { class: 'reply-context', style: 'border-left:3px solid #666;padding-left:10px;margin-bottom:8px;opacity:0.85;' }, - span({ style: 'font-size:0.85em;' }, - a({ href: `/forum/${encodeURIComponent(hrefKey)}`, class: 'tag-link' }, i18n.inReplyTo || 'IN REPLY TO'), - parentAuthor ? span(' ', userLink(parentAuthor, parentName)) : '' + { class: 'reply-context' }, + span({ class: 'reply-context-meta' }, + a({ href: `/forum/${encodeURIComponent(hrefKey)}#msg-${String(action.tipId || action.id || '').replace(/[^a-zA-Z0-9]/g, '')}`, class: 'tag-link' }, repliedAuthor ? i18n.inReplyTo : i18n.forumInThread), + repliedAuthor ? span(' ', userLink(repliedAuthor, repliedName)) : '' ), - parentTitle ? p({ class: 'post-text reply-context-text', style: 'font-size:0.85em;max-height:60px;overflow:hidden;margin-top:4px;font-weight:bold;color:#4fc3f7;' }, parentTitle) : '' + parentTitle ? p({ class: 'post-text reply-context-text' }, parentTitle) : '' ), - div({ class: 'card-field', style: 'margin-bottom:12px;' }, - p({ style: "margin:0 0 8px 0; word-break:break-all;" }, ...renderUrl(text)) + div({ class: 'card-field forum-reply-body' }, + p({ class: 'forum-reply-text' }, ...renderStyledText(text)) ) ) ); @@ -998,11 +1049,10 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { spreadTitle ? h2({ class: 'post-title activity-spread-title' }, spreadTitle) : '', spreadContentWarning ? h2({ class: 'content-warning' }, spreadContentWarning) : '', spreadExcerpt - ? div({ class: 'post-text activity-spread-text post-text-pre', style: 'max-height:200px;overflow:hidden;' }, ...renderUrlPreserveNewlines(spreadExcerpt)) - : div({ class: 'post-text activity-spread-text', style: 'opacity:0.6;font-style:italic;' }, i18n.spreadContentUnavailable || 'Content not yet available (pending replication)'), + ? div({ class: 'post-text activity-spread-text post-text-pre activity-spread-clamped' }, ...renderStyledText(spreadExcerpt)) + : div({ class: 'post-text activity-spread-text activity-spread-missing' }, i18n.spreadContentUnavailable || 'Content not yet available (pending replication)'), spreadOriginalAuthor ? div({ class: 'card-field' }, - span({ class: 'card-label' }, (i18n.spreadBy || 'By') + ': '), span({ class: 'card-value' }, userLink(spreadOriginalAuthor)) ) : '', @@ -1013,7 +1063,7 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { ) : '', link - ? div({ style: 'margin-top:6px;' }, + ? div({ class: 'card-section-action' }, a({ href: `/thread/${encodeURIComponent(link)}#${encodeURIComponent(link)}`, class: 'filter-btn' }, i18n.viewDetails || 'View details') ) : '' @@ -1037,30 +1087,23 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { const imgId = image ? (typeof image === 'string' ? image : (image.link || '')) : ''; cardBody.push( div({ class: 'card-section about' }, - imgId - ? img({ src: `/blob/${encodeURIComponent(imgId)}`, alt: name, class: 'activity-avatar' }) - : img({ src: '/assets/images/default-avatar.png', alt: name, class: 'activity-avatar' }), - h2(userLink(about, name)), - description ? p({ class: 'tribe-side-description' }, ...renderUrlPreserveNewlines(String(description))) : null + div({ class: 'about-card-cols' }, + div({ class: 'about-card-col-img' }, + imgId + ? renderZoomableImage(`/blob/${encodeURIComponent(imgId)}`, { imgClass: 'activity-avatar', alt: name, linkClass: 'zoom-inline' }) + : img({ src: '/assets/images/default-avatar.png', alt: name, class: 'activity-avatar' }) + ), + div({ class: 'about-card-col-desc' }, + h2({ class: 'about-card-header' }, userLink(about, name)), + description + ? p({ class: 'tribe-side-description' }, ...renderStyledText(String(description))) + : null + ) + ) ) ); } - if (type === 'pub') { - const { address } = content || {}; - const { key } = address || {}; - const pr = getProfile(key || ''); - const src = pr.image ? `/blob/${encodeURIComponent(pr.image)}` : '/assets/images/default-avatar.png'; - cardBody.push( - div({ class: 'card-section pub activity-pub' }, - br(), - userLink(pr.id, pr.name), - br(), - img({ src, alt: pr.name || pr.id, class: 'activity-avatar' }) - ) - ); - } - if (type === 'market') { const { item_type, title, price, status, deadline, stock, image, auctions_poll, seller } = content; const isSeller = seller && userId && seller === userId; @@ -1069,16 +1112,11 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.marketItemTitle + ':'), span({ class: 'card-value' }, title)), div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.marketItemType + ':'), span({ class: 'card-value' }, item_type.toUpperCase())), div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.marketItemStatus + ": " ), span({ class: 'card-value' }, status.toUpperCase())), - div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.deadline + ':'), span({ class: 'card-value' }, deadline ? new Date(deadline).toLocaleString() : "")), + div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.deadline + ':'), span({ class: 'card-value' }, deadline ? moment(deadline).format("YYYY/MM/DD HH:mm") : "")), div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.marketItemStock + ':'), span({ class: 'card-value' }, stock)), + div({ class: "price-chip" }, `${price} ECO`), br(), - image - ? renderMediaBlob(image, '/assets/images/default-market.png') - : img({ src: '/assets/images/default-market.png', alt: title, class: 'post-image' }), - br(), - div({ class: "market-card price" }, - p(`${i18n.marketItemPrice}: ${price} ECO`) - ), + image ? renderMediaBlob(image, '/assets/images/default-market.png') : null, item_type === 'auction' && status !== 'SOLD' && status !== 'DISCARDED' && !isSeller ? div({ class: "auction-info" }, auctions_poll && auctions_poll.length > 0 @@ -1098,16 +1136,7 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { ); }) ) - : p(i18n.marketNoBids), - form({ method: "POST", action: `/market/bid/${encodeURIComponent(action.id)}` }, - input({ type: "number", name: "bidAmount", step: "0.000001", min: "0.000001", placeholder: i18n.marketYourBid, required: true }), - br(), - button({ class: "buy-btn", type: "submit" }, i18n.marketPlaceBidButton) - ) - ) : "", - item_type === 'exchange' && status !== 'SOLD' && status !== 'DISCARDED' && !isSeller - ? form({ method: "POST", action: `/market/buy/${encodeURIComponent(action.id)}` }, - button({ class: "buy-btn", type: "submit" }, i18n.marketActionsBuy) + : p(i18n.marketNoBids) ) : "" ) ); @@ -1119,9 +1148,8 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { const displayDesc = shortDescription || (description ? (description.length > 140 ? description.slice(0, 140) + "\u2026" : description) : ""); cardBody.push( div({ class: 'card-section shop' }, - div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.shopTitle || 'Shop') + ':'), span({ class: 'card-value' }, shopKey ? a({ href: `/shops/${encodeURIComponent(shopKey)}`, class: 'user-link' }, title || shopKey) : (title || ''))), - displayDesc ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.description || 'Description') + ':'), span({ class: 'card-value' }, displayDesc)) : "", - visibility ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.shopVisibility || 'Visibility') + ':'), span({ class: 'card-value' }, visibility)) : "", + div({ class: 'card-field' }, span({ class: 'card-value' }, shopKey ? a({ href: `/shops/${encodeURIComponent(shopKey)}`, class: 'user-link' }, title || shopKey) : (title || ''))), + displayDesc ? div({ class: 'card-field' }, span({ class: 'card-value' }, displayDesc)) : "", location ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.shopLocation || 'Location') + ':'), span({ class: 'card-value' }, location)) : "" ) ); @@ -1134,15 +1162,109 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { const prodImageNode = renderMediaBlob(prodImage); cardBody.push( div({ class: 'card-section shop' }, - shopId ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.shopTitle || 'Shop') + ':'), span({ class: 'card-value' }, a({ href: `/shops/${encodeURIComponent(shopId)}`, class: 'user-link' }, shopLabel))) : '', - div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.shopProductTitle || 'Product') + ':'), span({ class: 'card-value' }, title || '')), + shopId ? div({ class: 'card-field' }, span({ class: 'card-value' }, a({ href: `/shops/${encodeURIComponent(shopId)}`, class: 'user-link' }, shopLabel))) : '', + div({ class: 'card-field' }, span({ class: 'card-value' }, title || '')), + div({ class: 'price-chip' }, `${Number(price || 0).toFixed(6)} ECO`), prodImageNode ? div({ class: 'card-field' }, prodImageNode) : '', - div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.shopProductPrice || 'Price') + ':'), span({ class: 'card-value' }, `${Number(price || 0).toFixed(6)} ECO`)), div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.shopProductStock || 'Stock') + ':'), span({ class: 'card-value' }, String(stock || 0))) ) ); } + if (type === 'industry') { + const { name, sector, description, image, tags, membershipPolicy, status } = content; + const facKey = action.id || action.key || ''; + const validTags = Array.isArray(tags) ? tags : []; + const statusValue = (() => { + const u = String(status || 'ACTIVE').toUpperCase(); + if (u === 'PAUSED') return i18n.industryStatusPaused || 'PAUSED'; + if (u === 'DISSOLVED') return i18n.industryStatusDissolved || 'DISSOLVED'; + return i18n.industryStatusActive || 'WORKING'; + })(); + const policyValue = String(i18n[`industryPolicy_${membershipPolicy}`] || membershipPolicy || '').toUpperCase(); + cardBody.push( + div({ class: 'card-section tribe industry' }, + h2({ class: 'tribe-title' }, + a({ href: `/industry/${encodeURIComponent(facKey)}`, class: "user-link" }, name || (i18n.industryTitle || 'Industry')) + ), + div({ class: 'card-field-row' }, + sector ? div({ class: 'card-field' }, span({ class: 'card-label' }, String(i18n.industrySector || 'Sector').toUpperCase() + ':'), span({ class: 'card-value' }, String(sector).toUpperCase())) : "", + status ? div({ class: 'card-field' }, span({ class: 'card-label' }, String(i18n.industryStatusLabel || 'Status').toUpperCase() + ':'), span({ class: 'card-value' }, statusValue)) : "", + membershipPolicy ? div({ class: 'card-field' }, span({ class: 'card-label' }, String(i18n.industryMembershipPolicy || 'Membership policy').toUpperCase() + ':'), span({ class: 'card-value' }, policyValue)) : "" + ), + image + ? (/^(\/|https?:)/.test(String(image)) + ? renderZoomableImage(image, { imgClass: 'feed-image tribe-image' }) + : renderMediaBlob(image, null)) + : null, + p({ class: 'tribe-description' }, ...renderStyledText(description || '')), + validTags.length + ? div({ class: 'card-tags' }, validTags.map(tag => + a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link" }, `#${tag}`))) + : "" + ) + ); + } + + if (type === 'industryBuild') { + const { title, notes, image, startDate, endDate, buildStatus } = content; + const buildKey = action.id || action.key || ''; + const daysLeft = endDate ? Math.max(0, Math.ceil((new Date(endDate) - Date.now()) / 86400000)) : null; + cardBody.push( + div({ class: 'card-section tribe industry' }, + h2({ class: 'tribe-title' }, + a({ href: `/industry/build/${encodeURIComponent(buildKey)}`, class: "user-link" }, title || (i18n.industryBuild || 'Build')) + ), + (buildStatus || startDate || endDate) ? div({ class: 'card-field-row' }, + buildStatus ? div({ class: 'card-field' }, span({ class: 'card-label' }, String(i18n.industryStatusLabel || 'Status').toUpperCase() + ':'), span({ class: 'card-value' }, String(i18n['industryBuildStatus_' + buildStatus] || buildStatus).toUpperCase())) : "", + startDate ? div({ class: 'card-field' }, span({ class: 'card-label' }, String(i18n.industryBuildStart || 'Start date').toUpperCase() + ':'), span({ class: 'card-value' }, moment(startDate).format("YYYY/MM/DD"))) : "", + endDate ? div({ class: 'card-field' }, span({ class: 'card-label' }, String(i18n.industryBuildEnd || 'End date').toUpperCase() + ':'), span({ class: 'card-value' }, moment(endDate).format('YYYY-MM-DD'))) : "", + daysLeft != null ? div({ class: 'card-field' }, span({ class: 'card-label' }, String(i18n.industryTimeLeft || 'Time left').toUpperCase() + ':'), span({ class: 'card-value' }, `${daysLeft}d`)) : "" + ) : null, + image + ? (/^(\/|https?:)/.test(String(image)) + ? renderZoomableImage(image, { imgClass: 'feed-image tribe-image' }) + : renderMediaBlob(image, null)) + : null, + notes ? p({ class: 'tribe-description' }, ...renderStyledText(notes)) : "" + ) + ); + } + + if (type === 'industryBlueprint') { + const { name, facility, description, outKind, laborHours, image, estTotal } = content; + cardBody.push( + div({ class: 'card-section tribe industry' }, + h2({ class: 'tribe-title' }, + facility + ? a({ href: `/industry/${encodeURIComponent(facility)}`, class: "user-link" }, name || (i18n.industryBlueprint || 'Blueprint')) + : (name || (i18n.industryBlueprint || 'Blueprint')) + ), + div({ class: 'card-field-row' }, + outKind ? div({ class: 'card-field' }, span({ class: 'card-label' }, String(i18n.industryOutputKind || 'Product type').toUpperCase() + ':'), span({ class: 'card-value' }, String(i18n['industryKind_' + outKind] || outKind).toUpperCase())) : "", + laborHours ? div({ class: 'card-field' }, span({ class: 'card-label' }, String(i18n.industryLaborHours || 'Labor hours').toUpperCase() + ':'), span({ class: 'card-value' }, String(laborHours))) : "", + estTotal != null ? div({ class: 'card-field' }, span({ class: 'card-label' }, String(i18n.industryEstTotal || 'Estimated price').toUpperCase() + ':'), span({ class: 'card-value' }, `${Number(estTotal).toFixed(2)} ECO`)) : "" + ), + image + ? (/^(\/|https?:)/.test(String(image)) + ? renderZoomableImage(image, { imgClass: 'feed-image tribe-image' }) + : renderMediaBlob(image, null)) + : null, + description ? p({ class: 'tribe-description' }, ...renderStyledText(description)) : "" + ) + ); + } + + if (type === 'industryAllocation') { + const { target, pot } = content; + cardBody.push( + div({ class: 'card-section industry' }, + div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.industryDistribution || 'Distribution') + ':'), span({ class: 'card-value' }, target ? a({ href: `/industry/build/${encodeURIComponent(target)}`, class: 'user-link' }, i18n.industryBuild || 'Build') : '')), + div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.industryPot || 'Pot') + ':'), span({ class: 'card-value' }, `${Number(pot || 0).toFixed(6)} ECO`)) + ) + ); + } + if (type === 'chat') { const { title, description, image, category, status } = content; const chatKey = action.id || action.key || ''; @@ -1150,10 +1272,12 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { const chatImageNode = renderMediaBlob(image); cardBody.push( div({ class: 'card-section chat' }, - div({ class: 'card-field' }, span({ class: 'card-label' }, 'Chat:'), span({ class: 'card-value' }, chatKey ? a({ href: `/chats/${encodeURIComponent(chatKey)}`, class: 'user-link' }, title || chatKey) : (title || ''))), - displayDesc ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.chatDescription || 'Description') + ':'), span({ class: 'card-value' }, displayDesc)) : '', - category ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.chatCategoryLabel || 'Category') + ':'), span({ class: 'card-value' }, category)) : '', - status ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.chatStatus || 'Status') + ':'), span({ class: 'card-value' }, status)) : '' + div({ class: 'card-field' }, + chatKey + ? a({ href: `/chats/${encodeURIComponent(chatKey)}#chat-latest`, class: 'card-value user-link' }, title || chatKey) + : span({ class: 'card-value' }, title || '')), + displayDesc ? div({ class: 'card-field' }, span({ class: 'card-value' }, displayDesc)) : '', + category ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.chatCategoryLabel || 'Category') + ':'), span({ class: 'card-value' }, category)) : '' ) ); } @@ -1163,19 +1287,185 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { const padTitle = content.title || action.title || ''; cardBody.push( div({ class: 'card-section' }, - div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.padTitle || 'Pad') + ':'), span({ class: 'card-value' }, padKey ? a({ href: `/pads/${encodeURIComponent(padKey)}`, class: 'user-link' }, padTitle || padKey) : '')), + div({ class: 'card-field' }, padKey ? a({ href: `/pads/${encodeURIComponent(padKey)}`, class: 'card-value user-link' }, padTitle || padKey) : span({ class: 'card-value' }, padTitle || '')), content.deadline ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.padDeadlineLabel || 'Deadline') + ':'), span({ class: 'card-value' }, content.deadline)) : '' ) ); } + if (type === 'room') { + const roomKey = action.id || action.key || ''; + const roomTitle = content.title || action.title || ''; + const roomNumber = roomNumberOf(action.rootId || roomKey); + cardBody.push( + div({ class: 'card-section' }, + div({ class: 'card-field activity-room-title' }, + roomKey ? a({ href: `/rooms/${encodeURIComponent(roomKey)}`, class: 'card-value user-link' }, roomTitle || roomKey) : span({ class: 'card-value' }, roomTitle || ''), + roomNumber ? renderPhoneChip(roomNumber, content.line === 'SWITCHBOARD' ? i18n.roomNumberHint : i18n.roomLineDndError) : '' + ), + content.description ? div({ class: 'card-field' }, span({ class: 'card-value' }, String(content.description).slice(0, 200))) : '' + ) + ); + } + + if (type === 'emergency') { + const emergencyKey = action.id || action.key || ''; + const emergencyTitle = content.title || action.title || ''; + cardBody.push( + div({ class: 'card-section' }, + div({ class: 'card-field' }, emergencyKey ? a({ href: `/emergencies/${encodeURIComponent(emergencyKey)}`, class: 'card-value user-link' }, emergencyTitle || emergencyKey) : span({ class: 'card-value' }, emergencyTitle || '')), + content.status ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.statusLabel || 'Status') + ':'), span({ class: 'card-value' }, String(content.status).toUpperCase())) : '', + content.category ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.emergencyCategoryLabel || 'Category') + ':'), span({ class: 'card-value' }, String(content.category).toUpperCase())) : '', + renderMediaBlob(content.text, null), + stripMediaMarkdown(content.text) ? p({ class: 'tribe-description' }, ...renderStyledText(stripMediaMarkdown(content.text))) : '' + ) + ); + } + + if (type === 'mailingList') { + const listKey = action.id || action.key || ''; + const listTitle = content.title || action.title || ''; + cardBody.push( + div({ class: 'card-section' }, + div({ class: 'card-field' }, listKey ? a({ href: `/mailing/${encodeURIComponent(listKey)}`, class: 'card-value user-link' }, listTitle || listKey) : span({ class: 'card-value' }, listTitle || '')) + ) + ); + } + + if (type === 'logisticsRoute') { + const routeKey = action.id || action.key || ''; + const routeTitle = content.title || action.title || ''; + cardBody.push( + div({ class: 'card-section' }, + div({ class: 'card-field' }, routeKey ? a({ href: `/logistics/${encodeURIComponent(routeKey)}`, class: 'card-value user-link' }, routeTitle || routeKey) : span({ class: 'card-value' }, routeTitle || '')), + content.origin || content.destination ? div({ class: 'card-field' }, span({ class: 'card-value' }, `${content.origin || ''} → ${content.destination || ''}`)) : '', + content.kind ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.logisticsKindLabel || 'Type') + ':'), span({ class: 'card-value' }, `${String(content.kind).toUpperCase()} · ${String(content.mode || '').toUpperCase()}`)) : '', + content.date ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.logisticsDateLabel || 'Date') + ':'), span({ class: 'card-value' }, moment(content.date).format('YYYY/MM/DD HH:mm'))) : '', + renderMediaBlob(content.description, null), + stripMediaMarkdown(content.description) ? p({ class: 'tribe-description' }, ...renderStyledText(stripMediaMarkdown(content.description))) : '' + ) + ); + } + + if (type === 'podcast' || type === 'podcastEpisode') { + const pKey = action.id || action.key || ''; + const pTitle = content.title || action.title || ''; + const pHref = type === 'podcast' ? `/podcasts/${encodeURIComponent(pKey)}` : `/podcasts/episode/${encodeURIComponent(pKey)}`; + const pDesc = stripMediaMarkdown(content.description); + const pShortDesc = pDesc ? (pDesc.length > 140 ? pDesc.slice(0, 140) + "\u2026" : pDesc) : ''; + cardBody.push( + div({ class: 'card-section' }, + type === 'podcastEpisode' ? '' : div({ class: 'card-field' }, pKey ? a({ href: pHref, class: 'card-value user-link' }, pTitle || pKey) : span({ class: 'card-value' }, pTitle || '')), + pShortDesc ? div({ class: 'card-field' }, span({ class: 'card-value' }, pShortDesc)) : '', + type === 'podcast' && content.category ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.podcastCategoryLabel || 'Category') + ':'), span({ class: 'card-value' }, String(content.category).toUpperCase())) : '', + renderMediaObject(type === 'podcast' ? content.cover : content.media, type === 'podcast' ? pHref : null) + ) + ); + } + + if (type === 'campaign') { + const cpKey = action.id || action.key || ''; + const cpTitle = content.title || action.title || ''; + cardBody.push( + div({ class: 'card-section' }, + div({ class: 'card-field' }, cpKey ? a({ href: `/campaigns/${encodeURIComponent(cpKey)}`, class: 'card-value user-link' }, cpTitle || cpKey) : span({ class: 'card-value' }, cpTitle || '')), + content.category ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.campaignCategoryLabel || 'Category') + ':'), span({ class: 'card-value' }, String(content.category).toUpperCase())) : '', + content.status ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.statusLabel || 'Status') + ':'), span({ class: 'card-value' }, String(content.status).toUpperCase())) : '', + renderMediaBlob(content.text, null), + stripMediaMarkdown(content.text) ? p({ class: 'tribe-description' }, ...renderStyledText(stripMediaMarkdown(content.text))) : '', + content.goal ? div({ class: 'card-field' }, span({ class: 'card-label' }, String(i18n.campaignSignaturesLabel || 'Signatures').toUpperCase() + ':'), span({ class: 'card-value' }, `${new Set(all.filter(x => x && x.type === 'campaignSignature' && x.content && (x.content.target === cpKey || x.content.target === (action.rootId || cpKey))).map(x => x.author)).size} / ${content.goal}`)) : '' + ) + ); + } + + if (type === 'campaignUpdate') { + const targetId = String(content.target || ''); + const parent = all.find(x => x && (x.id === targetId || x.key === targetId || x.rootId === targetId || x.tipId === targetId)) || null; + const parentContent = parent ? (parent.value?.content || parent.content || {}) : {}; + const parentTitle = parentContent.title || ''; + cardBody.push( + div({ class: 'card-section' }, + div({ class: 'reply-context' }, + span({ class: 'reply-context-meta' }, + a({ href: `/campaigns/${encodeURIComponent(targetId)}`, class: 'tag-link' }, i18n.emergencyLastUpdate || 'LAST UPDATE'), + parentTitle ? span(' ', a({ href: `/campaigns/${encodeURIComponent(targetId)}`, class: 'user-link' }, parentTitle)) : '' + ) + ), + renderMediaBlob(content.text, null), + stripMediaMarkdown(content.text) ? div({ class: 'card-field' }, p({ class: 'forum-reply-text' }, ...renderStyledText(stripMediaMarkdown(content.text)))) : '' + ) + ); + } + + if (type === 'emergencyUpdate') { + const targetId = String(content.target || ''); + const parent = all.find(x => x && (x.id === targetId || x.key === targetId || x.rootId === targetId || x.tipId === targetId)) || null; + const parentContent = parent ? (parent.value?.content || parent.content || {}) : {}; + const parentTitle = parentContent.title || ''; + cardBody.push( + div({ class: 'card-section' }, + div({ class: 'reply-context' }, + span({ class: 'reply-context-meta' }, + a({ href: `/emergencies/${encodeURIComponent(targetId)}`, class: 'tag-link' }, i18n.emergencyLastUpdate || 'LAST UPDATE'), + parentTitle ? span(' ', a({ href: `/emergencies/${encodeURIComponent(targetId)}`, class: 'user-link' }, parentTitle)) : '' + ) + ), + renderMediaBlob(content.text, null), + stripMediaMarkdown(content.text) ? div({ class: 'card-field' }, p({ class: 'forum-reply-text' }, ...renderStyledText(stripMediaMarkdown(content.text)))) : '' + ) + ); + } + + if (type === 'wikiPage') { + const wikiKey = action.id || action.key || ''; + const wikiTitle = content.title || action.title || ''; + cardBody.push( + div({ class: 'card-section' }, + div({ class: 'card-field' }, wikiKey ? a({ href: `/wiki/${encodeURIComponent(wikiKey)}`, class: 'card-value user-link' }, wikiTitle || wikiKey) : span({ class: 'card-value' }, wikiTitle || '')) + ) + ); + } + if (type === 'calendar') { const calKey = action.id || action.key || ''; const calTitle = content.title || action.title || ''; cardBody.push( div({ class: 'card-section' }, - div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.calendarTitle || 'Calendar') + ':'), span({ class: 'card-value' }, calKey ? a({ href: `/calendars/${encodeURIComponent(calKey)}`, class: 'user-link' }, calTitle || calKey) : '')), - content.status ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.calendarStatusLabel || 'Status') + ':'), span({ class: 'card-value' }, content.status)) : '' + div({ class: 'card-field' }, calKey ? a({ href: `/calendars/${encodeURIComponent(calKey)}`, class: 'card-value user-link' }, calTitle || calKey) : span({ class: 'card-value' }, calTitle || '')) + ) + ); + } + + if (type === 'poll') { + const options = Array.isArray(content.options) ? content.options : []; + const counts = (action.pollCounts && typeof action.pollCounts === 'object') ? action.pollCounts : {}; + const voters = Number(action.pollVoters) || 0; + const pctOf = (opt) => (voters > 0 ? Math.round(((counts[opt] || 0) / voters) * 100) : 0); + const step = (opt) => Math.round(pctOf(opt) / 5) * 5; + cardBody.push( + div({ class: 'card-section poll' }, + div({ class: 'card-field' }, + span({ class: 'card-value' }, content.question || '') + ), + options.length + ? table({ class: 'poll-results' }, + ...options.map((opt, i) => + tr( + td({ class: 'poll-result-option' }, `${letterOf(i)})`), + td({ class: 'poll-result-bar' }, + div({ class: 'poll-bar' }, + div({ class: `poll-bar-fill poll-bar-fill-${step(opt)} poll-hue-${i % 8}` }) + ) + ), + td({ class: 'poll-result-count' }, `${counts[opt] || 0} (${pctOf(opt)}%)`) + ) + ) + ) + : null, + div({ class: 'card-field' }, + span({ class: 'card-label' }, `${i18n.pollVoters}: `), + span({ class: 'card-value' }, String(voters)) + ) ) ); } @@ -1227,10 +1517,12 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { : 'ACTION'; const actorId = activity.activityActor || ''; + const actorLink = `${escapeHtml(userLinkLabel(actorId))}`; + const projectLink = `${escapeHtml(title || '')}`; const msgHtml = tmpl - .replace('%OASIS%', `${userLinkLabel(actorId)}`) - .replace('%PROJECT%', `${title || ''}`) - .replace('%ACTION%', `${actionWord}`); + .replace('%OASIS%', () => actorLink) + .replace('%PROJECT%', () => projectLink) + .replace('%ACTION%', () => `${escapeHtml(actionWord)}`); return div({ class: 'card card-rpg' }, div({ class: 'card-header' }, @@ -1240,10 +1532,10 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { ) ), div( - p({ innerHTML: sanitizeHtml(msgHtml) }) + p({ [RAW_HTML]: sanitizeHtml(msgHtml) }) ), p({ class: 'card-footer' }, - span({ class: 'date-link' }, `${action.ts ? new Date(action.ts).toLocaleString() : ''} ${i18n.performed} `), + span({ class: 'date-link' }, `${action.ts ? moment(action.ts).format("YYYY/MM/DD HH:mm") : ''}`), userLink(action.author, action.authorNames && action.authorNames[action.author]) ) ); @@ -1299,38 +1591,6 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { ); } - if (type === 'aiExchange') { - const { ctx, lang, tags, rating } = content; - const helpful = Number(action.helpfulVotes || 0); - cardBody.push( - div({ class: 'card-section ai-exchange' }, - Array.isArray(ctx) && ctx.length - ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.aiSnippetsLearned || 'Snippets learned') + ':'), span({ class: 'card-value' }, String(ctx.length))) - : null, - lang - ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.aiExchangeLang || 'Language') + ':'), span({ class: 'card-value' }, String(lang).toUpperCase())) - : null, - Array.isArray(tags) && tags.length - ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.aiExchangeTags || 'Tags') + ':'), - span({ class: 'card-value' }, tags.map(t => span({ class: 'ai-exchange-tag' }, '#' + t)))) - : null, - rating > 0 - ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.aiExchangeRating || 'Rating') + ':'), span({ class: 'card-value' }, '★'.repeat(rating) + '☆'.repeat(Math.max(0, 5 - rating)))) - : null, - div({ class: 'card-field' }, - span({ class: 'card-label' }, (i18n.aiExchangeHelpful || 'Helpful') + ':'), - span({ class: 'card-value' }, String(helpful)), - form({ method: 'POST', action: '/ai/exchange/vote', class: 'ai-exchange-vote-form' }, - input({ type: 'hidden', name: 'target', value: action.id }), - input({ type: 'hidden', name: 'helpful', value: 'yes' }), - input({ type: 'hidden', name: 'returnTo', value: '/activity' }), - button({ type: 'submit', class: 'filter-btn' }, i18n.aiExchangeMarkHelpful || '+1 helpful') - ) - ) - ) - ); - } - if (type === 'karmaScore') { const { karmaScore } = content; cardBody.push( @@ -1343,6 +1603,75 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { ); } + if (type === 'housing') { + const { title, housing_type, property_type, price, place, status, rooms, size, capacity } = content; + const isFree = String(housing_type || '').toLowerCase() === 'couchsurfing'; + cardBody.push( + div({ class: 'card-section report' }, + div({ class: 'card-field' }, + span({ class: 'card-label' }, i18n.title + ':'), + span({ class: 'card-value' }, title) + ), + housing_type ? div({ class: 'card-field' }, + span({ class: 'card-label' }, i18n.housingType + ':'), + span({ class: 'card-value' }, String(housing_type).toUpperCase()) + ) : null, + property_type ? div({ class: 'card-field' }, + span({ class: 'card-label' }, i18n.housingProperty + ':'), + span({ class: 'card-value' }, String(property_type).toUpperCase()) + ) : null, + div({ class: 'card-field' }, + span({ class: 'card-label' }, i18n.housingPrice + ':'), + span({ class: 'card-value' }, isFree ? (i18n.housingFree || 'FREE') : `${Number(price || 0).toFixed(2)} ECO`) + ), + place ? div({ class: 'card-field' }, + span({ class: 'card-label' }, i18n.housingPlace + ':'), + span({ class: 'card-value' }, place) + ) : null, + Number(rooms) > 0 ? div({ class: 'card-field' }, + span({ class: 'card-label' }, i18n.housingRooms + ':'), + span({ class: 'card-value' }, String(rooms)) + ) : null, + Number(size) > 0 ? div({ class: 'card-field' }, + span({ class: 'card-label' }, i18n.housingSize + ':'), + span({ class: 'card-value' }, `${size} m²`) + ) : null, + Number(capacity) > 0 ? div({ class: 'card-field' }, + span({ class: 'card-label' }, i18n.housingCapacity + ':'), + span({ class: 'card-value' }, String(capacity)) + ) : null, + status ? div({ class: 'card-field' }, + span({ class: 'card-label' }, i18n.housingStatus + ':'), + span({ class: 'card-value' }, String(status).toUpperCase()) + ) : null + ) + ); + } + + if (type === 'schoolCourse') { + const { title, description, price, visibility, startDate } = content; + const coursePaid = Number(price) > 0; + cardBody.push( + div({ class: 'card-section report' }, + div({ class: 'card-field' }, + span({ class: 'card-label' }, i18n.title + ':'), + span({ class: 'card-value' }, title) + ), + description ? div({ class: 'card-field' }, + span({ class: 'card-value' }, String(description).length > 220 ? String(description).slice(0, 220) + '…' : description) + ) : null, + coursePaid ? div({ class: 'card-field' }, + span({ class: 'card-label' }, (i18n.schoolPrice || 'Price (ECO)') + ':'), + span({ class: 'card-value' }, `${Number(price).toFixed(2)} ECO`) + ) : null, + startDate ? div({ class: 'card-field' }, + span({ class: 'card-label' }, (i18n.schoolStartDate || 'Start date') + ':'), + span({ class: 'card-value' }, moment(startDate).format("YYYY/MM/DD HH:mm")) + ) : null + ) + ); + } + if (type === 'job') { const { title, job_type, tasks, location, vacants, salary, status, subscribers } = content; cardBody.push( @@ -1351,26 +1680,26 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { span({ class: 'card-label' }, i18n.title + ':'), span({ class: 'card-value' }, title) ), - salary && div({ class: 'card-field' }, + salary ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.jobSalary + ':'), span({ class: 'card-value' }, salary + ' ECO') - ), - status && div({ class: 'card-field' }, + ) : null, + status ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.jobStatus + ':'), - span({ class: 'card-value' }, status.toUpperCase()) - ), - job_type && div({ class: 'card-field' }, + span({ class: 'card-value' }, String(status).toUpperCase()) + ) : null, + job_type ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.jobType + ':'), - span({ class: 'card-value' }, job_type.toUpperCase()) - ), - location && div({ class: 'card-field' }, + span({ class: 'card-value' }, String(job_type).toUpperCase()) + ) : null, + location ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.jobLocation + ':'), - span({ class: 'card-value' }, location.toUpperCase()) - ), - vacants && div({ class: 'card-field' }, + span({ class: 'card-value' }, String(location).toUpperCase()) + ) : null, + Number(vacants) > 0 ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.jobVacants + ':'), - span({ class: 'card-value' }, vacants) - ), + span({ class: 'card-value' }, String(vacants)) + ) : null, div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.jobSubscribers + ':'), span({ class: 'card-value' }, @@ -1402,7 +1731,7 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { } if (type === 'parliamentTerm') { - const { method, powerType, powerId, powerTitle, winnerVotes, totalVotes, startAt, endAt } = content; + const { method, powerType, powerId, powerTitle, startAt, endAt } = content; const powerTypeNorm = String(powerType || '').toLowerCase(); const winnerLink = powerTypeNorm === 'tribe' @@ -1411,23 +1740,23 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { ? a({ href: `/parliament?filter=government`, class: 'user-link' }, (i18n.parliamentAnarchy || 'ANARCHY')) : userLink(powerId, powerTitle); - const methodUpper = String( - i18n['parliamentMethod' + String(method || '').toUpperCase()] || method - ).toUpperCase(); - cardBody.push( div({ class: 'card-section parliament' }, - startAt ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentElectionsStart.toUpperCase() || 'Elections start') + ':'), span({ class: 'card-value' }, new Date(startAt).toLocaleString())) : '', - endAt ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentElectionsEnd.toUpperCase() || 'Elections end') + ':'), span({ class: 'card-value' }, new Date(endAt).toLocaleString())) : '', - div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentCurrentLeader.toUpperCase() || 'Winning candidature') + ':'), span({ class: 'card-value' }, winnerLink)), - div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentGovMethod.toUpperCase() || 'Method') + ':'), span({ class: 'card-value' }, methodUpper)), - div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentVotesReceived.toUpperCase() || 'Votes received') + ':'), span({ class: 'card-value' }, `${Number(winnerVotes || 0)} (${Number(totalVotes || 0)})`)) + startAt ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentElectionsStart.toUpperCase() || 'Elections start') + ':'), span({ class: 'card-value' }, moment(startAt).format("YYYY/MM/DD HH:mm"))) : '', + endAt ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentElectionsEnd.toUpperCase() || 'Elections end') + ':'), span({ class: 'card-value' }, moment(endAt).format("YYYY/MM/DD HH:mm"))) : '', + div({ class: 'card-field' }, span({ class: 'card-value' }, winnerLink)) ) ); } if (type === 'parliamentProposal') { - const { title, description, method, status, voteId, createdAt } = content; + const { title, method, status, voteId, createdAt } = content; + const legacy = !content.campaignId ? String(content.description || '').match(/\n*\s*(\d+) signatures: \/campaigns\/(%[^\s]+\.sha256)\s*$/) : null; + const description = legacy ? String(content.description || '').slice(0, legacy.index).trim() : content.description; + const campaignId = content.campaignId || (legacy ? legacy[2] : ''); + const liveSignatures = campaignId ? new Set(all.filter(x => x && x.type === 'campaignSignature' && x.content && x.content.target === campaignId).map(x => x.author)).size : 0; + const signatures = Math.max(liveSignatures, content.campaignId ? (Number(content.signatures) || 0) : (legacy ? Number(legacy[1]) || 0 : 0)); + const goal = Number(content.goal) || 0; const methodUpper = String( i18n['parliamentMethod' + String(method || '').toUpperCase()] || method @@ -1436,9 +1765,12 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { cardBody.push( div({ class: 'card-section parliament' }, title ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentProposalTitle.toUpperCase() || 'Title') + ':'), span({ class: 'card-value' }, title)) : '', - description ? p({ style: 'margin:.4rem 0' }, description) : '', + renderMediaBlob(description, null), + stripMediaMarkdown(description) ? p({ class: 'card-section-text' }, ...renderStyledText(stripMediaMarkdown(description))) : '', + campaignId ? div({ class: 'card-field' }, span({ class: 'card-label' }, String(i18n.campaignSignaturesLabel || 'Signatures').toUpperCase() + ':'), span({ class: 'card-value' }, `${signatures}${goal > 0 ? ` / ${goal}` : ''}`)) : '', + campaignId ? div({ class: 'card-field' }, a({ href: `/campaigns/${encodeURIComponent(campaignId)}`, class: 'card-value user-link' }, title || campaignId)) : '', div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentGovMethod || 'Method') + ':'), span({ class: 'card-value' }, methodUpper)), - createdAt ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.createdAt.toUpperCase() || 'Created at') + ':'), span({ class: 'card-value' }, new Date(createdAt).toLocaleString())) : '', + createdAt ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.createdAt.toUpperCase() || 'Created at') + ':'), span({ class: 'card-value' }, moment(createdAt).format("YYYY/MM/DD HH:mm"))) : '', voteId ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentOpenVote.toUpperCase() || 'Open vote') + ':'), a({ href: `/votes/${encodeURIComponent(voteId)}`, class: 'tag-link' }, i18n.viewDetails || 'View details')) : '', status ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentStatus.toUpperCase() || 'Status') + ':'), span({ class: 'card-value' }, status)) : '' ) @@ -1454,9 +1786,9 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { cardBody.push( div({ class: 'card-section parliament' }, title ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentProposalTitle.toUpperCase() || 'Title') + ':'), span({ class: 'card-value' }, title)) : '', - reasons ? p({ style: 'margin:.4rem 0' }, reasons) : '', + reasons ? p({ class: 'card-section-text' }, reasons) : '', div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentGovMethod || 'Method') + ':'), span({ class: 'card-value' }, methodUpper)), - createdAt ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.createdAt.toUpperCase() || 'Created at') + ':'), span({ class: 'card-value' }, new Date(createdAt).toLocaleString())) : '', + createdAt ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.createdAt.toUpperCase() || 'Created at') + ':'), span({ class: 'card-value' }, moment(createdAt).format("YYYY/MM/DD HH:mm"))) : '', voteId ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentOpenVote.toUpperCase() || 'Open vote') + ':'), a({ href: `/votes/${encodeURIComponent(voteId)}`, class: 'tag-link' }, i18n.viewDetails || 'View details')) : '', status ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentStatus.toUpperCase() || 'Status') + ':'), span({ class: 'card-value' }, status)) : '' ) @@ -1475,10 +1807,10 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { cardBody.push( div({ class: 'card-section parliament' }, question ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentLawQuestion || 'Question') + ':'), span({ class: 'card-value' }, question)) : '', - description ? p({ style: 'margin:.4rem 0' }, description) : '', + description ? p({ class: 'card-section-text' }, description) : '', div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentLawMethod || 'Method') + ':'), span({ class: 'card-value' }, methodUpper)), proposer ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentLawProposer || 'Proposer') + ':'), span({ class: 'card-value' }, userLink(proposer))) : '', - enactedAt ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentLawEnacted || 'Enacted at') + ':'), span({ class: 'card-value' }, new Date(enactedAt).toLocaleString())) : '', + enactedAt ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentLawEnacted || 'Enacted at') + ':'), span({ class: 'card-value' }, moment(enactedAt).format("YYYY/MM/DD HH:mm"))) : '', (total || yes) ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentLawVotes || 'Votes') + ':'), span({ class: 'card-value' }, `${yes}/${total}`)) : '' ) ); @@ -1492,9 +1824,9 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { title ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.courtsCaseTitle.toUpperCase() + ':'), span({ class: 'card-value' }, title)) : '', status ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.courtsThStatus.toUpperCase() + ':'), span({ class: 'card-value' }, status)) : '', method ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.courtsMethod.toUpperCase() + ':'), span({ class: 'card-value' }, String(i18n['courtsMethod' + String(method).toUpperCase()] || method).toUpperCase())) : '', - answerBy ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.courtsThAnswerBy + ':'), span({ class: 'card-value' }, new Date(answerBy).toLocaleString())) : '', - evidenceBy ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.courtsThEvidenceBy + ':'), span({ class: 'card-value' }, new Date(evidenceBy).toLocaleString())) : '', - decisionBy ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.courtsThDecisionBy + ':'), span({ class: 'card-value' }, new Date(decisionBy).toLocaleString())) : '', + answerBy ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.courtsThAnswerBy + ':'), span({ class: 'card-value' }, moment(answerBy).format("YYYY/MM/DD HH:mm"))) : '', + evidenceBy ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.courtsThEvidenceBy + ':'), span({ class: 'card-value' }, moment(evidenceBy).format("YYYY/MM/DD HH:mm"))) : '', + decisionBy ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.courtsThDecisionBy + ':'), span({ class: 'card-value' }, moment(decisionBy).format("YYYY/MM/DD HH:mm"))) : '', accuser ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.courtsAccuser + ':'), span({ class: 'card-value' }, userLink(accuser))) : '', typeof needed !== 'undefined' ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.courtsVotesNeeded + ':'), span({ class: 'card-value' }, String(needed))) : '', (typeof yes !== 'undefined' || typeof total !== 'undefined') ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.courtsVotesSlashTotal + ':'), span({ class: 'card-value' }, `${Number(yes || 0)}/${Number(total || 0)}`)) : '', @@ -1537,7 +1869,7 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { return null; } - const detailHref = (type !== 'feed' && type !== 'aiExchange' && type !== 'bankWallet') + const detailHref = (type !== 'bankWallet') ? (isParliamentTarget ? `/parliament?filter=${encodeURIComponent(parliamentFilter)}` : isCourtsTarget @@ -1550,46 +1882,60 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { div({ class: 'card-header activity-card-header' }, div({ class: 'card-chips-row' }, span({ class: 'pm-exposition-chip pm-exposition-whole' }, - span({ class: 'pm-exposition-text' }, String(type || '').toUpperCase()) + span({ class: 'pm-exposition-text' }, String(headerText || type || '').replace(/^\[|\]$/g, '')) ) ), - renderContentActions(msgId, detailHref) + renderContentActions(msgId, detailHref, { + author: action.author, + contact: type === 'about', + reportTitle: (content && (content.title || content.question || content.concept || content.name)) || '', + spread: SPREADABLE_TYPES.has(type) ? (spreadMap.get(action.id) || null) : undefined, + ...(type === 'torrent' || type === 'file' ? { spreadTitle: i18n.seedAction } : {}), + ...(TORRENTABLE_TYPES.has(type) && content && typeof content.url === 'string' && content.url.startsWith('&') + ? { torrentFrom: { blobId: content.url, name: content.title || content.fileName || '' } } + : {}), + ...favOptsFor(type, msgId, extras), + deleteAction: isOwn ? activityDeleteAction(type, action, content, msgId, userId) : undefined + }) ), ...cardBody, (() => { - if (!OPINION_TYPES.has(type)) return null; - const routeFn = OPINION_ROUTES[type]; - if (!routeFn) return null; + const footerAuthorId = action.author || (content && content.proposer) || ''; + const footerNode = p({ class: 'card-footer' }, + span({ class: 'date-link' }, `${date}`), + userLink(footerAuthorId, (action.authorNames && action.authorNames[footerAuthorId]) || getProfile(footerAuthorId).name) + ); + const commentTotal = Number(action.commentCount) || 0; + const commentsHref = detailHref + ? `${detailHref}${detailHref.includes('?') ? '&' : '?'}comments=open${commentTotal ? '#comments-latest' : ''}` + : ''; + const commentsNode = detailHref && type !== 'about' + ? a({ + href: commentsHref, + class: commentTotal ? 'comments-summary-link engage-on' : 'comments-summary-link', + title: i18n.commentsGoToLast || i18n.voteNewCommentLabel || 'Comments' + }, + span({ class: 'comments-summary-icon' }, '✑'), + span({ class: 'comments-summary-count' }, `(${commentTotal})`) + ) + : null; + const routeFn = OPINION_TYPES.has(type) ? OPINION_ROUTES[type] : null; + if (!routeFn) return commentsNode ? [renderCardMetaRow(commentsNode, footerNode)] : footerNode; const ops = (action.value?.content?.opinions) || (action.content?.opinions) || {}; const opsTotal = Object.values(ops).reduce((s, n) => s + (Number(n) || 0), 0); - return details({ class: 'opinions-voting-collapse' }, - summary({ class: 'opinions-summary' }, `${i18n.opinionsTitle || 'Opinions'} (${opsTotal})`), + const votingNode = details({ class: 'opinions-voting-collapse' }, + summary({ class: 'opinions-summary' }, + span({ class: 'opinions-summary-icon' }, 'ꔍ'), + span({ class: 'opinions-summary-count' }, `(${opsTotal})`)), div({ class: 'voting-buttons' }, opinionCategories.map(cat => form({ method: 'POST', action: `${routeFn(action.id)}/${cat}` }, - button({ class: 'vote-btn' }, `${i18n['vote' + cat.charAt(0).toUpperCase() + cat.slice(1)] || cat} [${ops[cat] || 0}]`) + button({ class: 'vote-btn' }, `${String(i18n['vote' + cat.charAt(0).toUpperCase() + cat.slice(1)] || cat).toUpperCase()} [${ops[cat] || 0}]`) ) ) ) ); - })(), - (() => { - const SPREADABLE = new Set([ - 'post','audio','video','image','document','torrent','bookmark', - 'event','calendar','task','votes','vote','market','shop','shopProduct', - 'project','transfer','job','report', - 'chat','chatMessage','pad','padEntry','forum','map' - ]); - if (!SPREADABLE.has(type)) return null; - const btn = renderSpreadButton(action.id, spreadMap.get(action.id)); - return btn ? div({ class: 'card-spread-left' }, btn) : null; - })(), - (() => { - const footerAuthorId = action.author || (content && content.proposer) || ''; - return p({ class: 'card-footer' }, - span({ class: 'date-link' }, `${date} ${i18n.performed} `), - userLink(footerAuthorId, (action.authorNames && action.authorNames[footerAuthorId]) || getProfile(footerAuthorId).name) - ); + return [renderVotesSummary(ops), renderCardMetaRow(votingNode, commentsNode, footerNode)]; })() ); }); @@ -1601,6 +1947,32 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { return filteredCards; } +const FAV_KIND_BY_TYPE = CONTENT_FAV_KIND; + +const activityDeleteAction = (type, action, content, msgId, userId) => { + const c = content || {}; + if (c.author && String(c.author) !== String(userId)) return undefined; + if (type === 'transfer') { + const required = c.from === c.to ? 1 : 2; + const confirmed = Array.isArray(c.confirmedBy) ? c.confirmedBy.length : 0; + const dl = c.deadline ? moment(c.deadline) : null; + const expired = dl && dl.isValid() ? dl.isBefore(moment()) : false; + if (String(c.status || '').toUpperCase() !== 'UNCONFIRMED' || expired || confirmed >= required) return undefined; + } + return contentDeleteAction(type, type === 'forum' ? safeMsgId(c.key || action.tipId || action.id) : msgId); +}; + +const favOptsFor = (type, id, extras = {}) => { + const favKind = FAV_KIND_BY_TYPE[type]; + if (!favKind) return {}; + const index = extras.favIndex instanceof Map ? extras.favIndex : null; + return { + favKind, + isFavorite: index ? index.get(String(id)) === favKind : false, + returnTo: extras.returnTo || '/activity' + }; +}; + function getViewDetailsAction(type, action) { const id = encodeURIComponent(safeMsgId(action.tipId || action.id || action.key || action)); switch (type) { @@ -1622,11 +1994,18 @@ function getViewDetailsAction(type, action) { const link = normalizeSpreadLink(action.content?.spreadTargetId || action.content?.vote?.link || ''); return link ? `/thread/${encodeURIComponent(link)}#${encodeURIComponent(link)}` : `/activity`; } - case 'post': return `/thread/${id}#${id}`; + case 'post': { + const root = action.content && action.content.root; + return root + ? `/blogs/${encodeURIComponent(root)}#${id}` + : `/blogs/${id}`; + } + case 'feed': return `/feed/${id}`; case 'vote': return `/thread/${encodeURIComponent(action.content.vote.link)}#${encodeURIComponent(action.content.vote.link)}`; case 'votes': return `/votes/${id}`; case 'transfer': return `/transfers/${id}`; case 'pixelia': return `/pixelia`; + case 'poll': return `/polls/${id}`; case 'larpHousePost': return action.content?.house ? `/larp/${encodeURIComponent(action.content.house)}` : `/activity`; case 'tribe': return `/tribe/${id}`; case 'curriculum': return `/inhabitant/${encodeURIComponent(action.author)}`; @@ -1637,39 +2016,61 @@ function getViewDetailsAction(type, action) { case 'audio': return `/audios/${id}`; case 'video': return `/videos/${id}`; case 'torrent': return `/torrents/${id}`; - case 'forum': return `/forum/${encodeURIComponent(action.content?.key || action.tipId || action.id)}`; + case 'file': return `/files/${id}`; + case 'forum': { + const root = action.content?.root; + const rootId = typeof root === 'string' ? root : (root && (root.key || root.id)) || ''; + if (rootId) return `/forum/${encodeURIComponent(action.content?.rootKey || rootId)}#msg-${String(action.tipId || action.id || '').replace(/[^a-zA-Z0-9]/g, '')}`; + return `/forum/${encodeURIComponent(action.content?.key || action.tipId || action.id)}`; + } case 'document': return `/documents/${id}`; case 'bookmark': return `/bookmarks/${id}`; case 'event': return `/events/${id}`; case 'task': return `/tasks/${id}`; case 'taskAssignment': return `/tasks/${encodeURIComponent(action.content?.taskId || action.tipId || action.id)}`; case 'about': return `/author/${encodeURIComponent(action.author)}`; - case 'pub': return `/invites`; case 'market': return `/market/${id}`; case 'shop': return `/shops/${id}`; case 'shopProduct': return `/shops/product/${id}`; case 'chat': return `/chats/${id}`; case 'pad': return `/pads/${id}`; + case 'room': return `/rooms/${id}`; + case 'wikiPage': return `/wiki/${id}`; + case 'emergency': return `/emergencies/${id}`; + case 'emergencyUpdate': return action.content && action.content.target ? `/emergencies/${encodeURIComponent(action.content.target)}` : '/emergencies'; + case 'campaignUpdate': return action.content && action.content.target ? `/campaigns/${encodeURIComponent(action.content.target)}` : '/campaigns'; + case 'mailingList': return `/mailing/${id}`; + case 'logisticsRoute': return `/logistics/${id}`; + case 'podcast': return `/podcasts/${id}`; + case 'podcastEpisode': return `/podcasts/episode/${id}`; + case 'campaign': return `/campaigns/${id}`; case 'calendar': return `/calendars/${id}`; + case 'housing': return `/housing/${id}`; case 'job': return `/jobs/${id}`; case 'project': return `/projects/${id}`; + case 'schoolCourse': return `/school/course/${id}`; + case 'industry': return `/industry/${id}`; + case 'industryBuild': return `/industry/build/${id}`; + case 'industryBlueprint': return `/industry/blueprint/${id}`; + case 'industryAllocation': return action.content?.target ? `/industry/build/${encodeURIComponent(action.content.target)}` : '/industry?filter=BUILDS'; case 'report': return `/reports/${id}`; case 'bankWallet': return `/wallet`; - case 'bankClaim': return `/banking${action.content?.epochId ? `/epoch/${encodeURIComponent(action.content.epochId)}` : ''}`; - case 'ubiClaim': return action.content?.transferId ? `/transfers/${encodeURIComponent(action.content.transferId)}` : `/transfers?filter=ubi`; default: return `/activity`; } } +exports.getViewDetailsAction = getViewDetailsAction; + exports.activityView = (actions, filter, userId, q = '', extras = {}) => { const spreadMap = (extras && extras.spreadMap) || new Map(); const title = filter === 'mine' ? i18n.yourActivity : i18n.globalActivity; const desc = i18n.activityDesc; const activityTypes = [ - { type: 'recent', label: i18n.typeRecent }, { type: 'all', label: i18n.allButton }, + { type: 'recent', label: i18n.typeRecent }, { type: 'mine', label: i18n.mineButton }, + { type: 'top', label: i18n.typeTop }, { type: 'inhabitants', label: i18n.typeInhabitants }, { type: 'tribe', label: i18n.typeTribe }, { type: 'larp', label: i18n.typeLarp }, @@ -1684,39 +2085,56 @@ exports.activityView = (actions, filter, userId, q = '', extras = {}) => { { type: 'feed', label: i18n.typeFeed }, { type: 'chat', label: i18n.typeChat }, { type: 'pad', label: i18n.typePad }, + { type: 'room', label: i18n.typeRoom }, + { type: 'wiki', label: i18n.typeWiki }, + { type: 'emergency', label: i18n.typeEmergency }, + { type: 'mailing', label: i18n.typeMailingList }, + { type: 'logistics', label: i18n.typeLogisticsRoute }, + { type: 'podcast', label: i18n.typePodcast }, + { type: 'campaign', label: i18n.typeCampaign }, { type: 'forum', label: i18n.typeForum }, { type: 'map', label: i18n.typeMap }, - { type: 'banking', label: i18n.typeBanking }, { type: 'market', label: i18n.typeMarket }, + { type: 'schoolCourse', label: i18n.typeSchool }, { type: 'project', label: i18n.typeProject }, + { type: 'industry', label: i18n.typeIndustry }, + { type: 'housing', label: i18n.typeHousing }, { type: 'job', label: i18n.typeJob }, { type: 'shop', label: i18n.typeShop }, - { type: 'transfer', label: i18n.typeTransfer }, - { type: 'curriculum',label: i18n.typeCurriculum }, { type: 'audio', label: i18n.typeAudio }, { type: 'bookmark', label: i18n.typeBookmark }, { type: 'document', label: i18n.typeDocument }, { type: 'image', label: i18n.typeImage }, { type: 'torrent', label: i18n.typeTorrent }, + { type: 'file', label: i18n.typeFile }, { type: 'video', label: i18n.typeVideo } ]; - const FILTER_META = new Set(['recent', 'all', 'mine']); + const FILTER_META = new Set(['all', 'mine', 'recent']); const GROUP_SUBTYPES = { parliament: ['parliamentCandidature', 'parliamentTerm', 'parliamentProposal', 'parliamentRevocation', 'parliamentLaw'], courts: ['courtsCase', 'courtsNomination', 'courtsNominationVote'], - banking: ['bankWallet', 'bankClaim', 'ubiClaim'], task: ['task', 'taskAssignment'], + votes: ['votes', 'poll'], shop: ['shop', 'shopProduct'], larp: ['larpHousePost'], - inhabitants:['about'] + inhabitants:['about'], + chat: ['chat', 'chatThread'], + wiki: ['wikiPage'], + mailing: ['mailingList'], + emergency: ['emergency', 'emergencyUpdate'], + logistics: ['logisticsRoute'], + podcast: ['podcast', 'podcastEpisode'], + campaign: ['campaign', 'campaignUpdate'], + industry: ['industry', 'industryBuild', 'industryBlueprint', 'industryAllocation'] }; - const ALLOWED_TYPES = new Set(); + const ALLOWED_TYPES = new Set(['tribeOpened']); for (const { type } of activityTypes) { if (FILTER_META.has(type)) continue; if (GROUP_SUBTYPES[type]) GROUP_SUBTYPES[type].forEach(t => ALLOWED_TYPES.add(t)); else ALLOWED_TYPES.add(type); } + const rawActions = actions; actions = actions.filter(action => ALLOWED_TYPES.has(action.type)); let filteredActions; @@ -1726,9 +2144,9 @@ exports.activityView = (actions, filter, userId, q = '', extras = {}) => { const now = Date.now(); filteredActions = actions.filter(action => action.type !== 'tombstone' && action.ts && now - action.ts < 24 * 60 * 60 * 1000); } else if (filter === 'banking') { - filteredActions = actions.filter(action => action.type !== 'tombstone' && (action.type === 'bankWallet' || action.type === 'bankClaim' || action.type === 'ubiClaim')); + filteredActions = []; } else if (filter === 'tribe') { - filteredActions = actions.filter(action => action.type === 'tribe'); + filteredActions = actions.filter(action => action.type === 'tribe' || action.type === 'tribeOpened'); } else if (filter === 'larp') { filteredActions = actions.filter(action => action.type === 'larpHousePost'); } else if (filter === 'inhabitants') { @@ -1741,11 +2159,37 @@ exports.activityView = (actions, filter, userId, q = '', extras = {}) => { return t === 'courtscase' || t === 'courtsnomination' || t === 'courtsnominationvote'; }); } else if (filter === 'task') { - filteredActions = actions.filter(action => action.type !== 'tombstone' && (action.type === 'task' || action.type === 'taskAssignment')); + filteredActions = actions.filter(action => action.type !== 'tombstone' && (action.type === 'task' || action.type === 'taskAssignment' || openedIn(action, 'task'))); } else if (filter === 'torrent') { - filteredActions = actions.filter(action => action.type === 'torrent'); + filteredActions = actions.filter(action => action.type === 'torrent' || openedIn(action, 'torrent')); + } else if (filter === 'file') { + filteredActions = actions.filter(action => action.type === 'file'); + } else if (filter === 'chat') { + filteredActions = actions.filter(action => (action.type === 'chat' || action.type === 'chatThread') && action.type !== 'tombstone'); + } else if (filter === 'wiki') { + filteredActions = actions.filter(action => action.type === 'wikiPage'); + } else if (filter === 'emergency') { + filteredActions = actions.filter(action => action.type === 'emergency' || action.type === 'emergencyUpdate'); + } else if (filter === 'mailing') { + filteredActions = actions.filter(action => action.type === 'mailingList'); + } else if (filter === 'room') { + filteredActions = actions.filter(action => action.type === 'room'); + } else if (filter === 'logistics') { + filteredActions = actions.filter(action => action.type === 'logisticsRoute'); + } else if (filter === 'podcast') { + filteredActions = actions.filter(action => action.type === 'podcast' || action.type === 'podcastEpisode'); + } else if (filter === 'campaign') { + filteredActions = actions.filter(action => action.type === 'campaign' || action.type === 'campaignUpdate'); + } else if (filter === 'industry') { + filteredActions = actions.filter(action => ['industry', 'industryBuild', 'industryBlueprint', 'industryAllocation'].includes(action.type) && action.type !== 'tombstone'); } else { - filteredActions = actions.filter(action => (action.type === filter || filter === 'all' || (filter === 'shop' && action.type === 'shopProduct')) && action.type !== 'tombstone'); + filteredActions = actions.filter(action => + (action.type === filter + || filter === 'all' + || openedIn(action, filter) + || (filter === 'shop' && action.type === 'shopProduct') + || (filter === 'votes' && action.type === 'poll')) + && action.type !== 'tombstone'); } const larpEarliest = new Map(); @@ -1809,89 +2253,88 @@ exports.activityView = (actions, filter, userId, q = '', extras = {}) => { } const MODULE_SUB_FILTERS = { - audio: { url: '/audios', filters: ['all', 'mine', 'recent', 'top', 'favorites'] }, - video: { url: '/videos', filters: ['all', 'mine', 'recent', 'top', 'favorites'] }, - image: { url: '/images', filters: ['all', 'mine', 'recent', 'top', 'favorites'] }, - document:{ url: '/documents', filters: ['all', 'mine', 'recent', 'top', 'favorites'] }, - bookmark:{ url: '/bookmarks', filters: ['all', 'mine', 'recent', 'top', 'favorites'] }, - torrent: { url: '/torrents', filters: ['all', 'mine', 'recent', 'top', 'favorites'] }, - map: { url: '/maps', filters: ['all', 'mine', 'recent'] }, - forum: { url: '/forum', filters: ['all', 'mine', 'recent', 'top'] }, - event: { url: '/events', filters: ['all', 'mine', 'recent', 'top'] }, + audio: { url: '/audios', filters: ['all', 'recent', 'mine', 'top', 'favorites'] }, + video: { url: '/videos', filters: ['all', 'recent', 'mine', 'top', 'favorites'] }, + image: { url: '/images', filters: ['all', 'recent', 'mine', 'top', 'favorites'] }, + document:{ url: '/documents', filters: ['all', 'recent', 'mine', 'top', 'favorites'] }, + bookmark:{ url: '/bookmarks', filters: ['all', 'recent', 'mine', 'top', 'favorites'] }, + torrent: { url: '/torrents', filters: ['all', 'recent', 'mine', 'top', 'favorites'] }, + file: { url: '/files', filters: ['all', 'recent', 'mine', 'top', 'favorites'] }, + map: { url: '/maps', filters: ['all', 'recent', 'mine'] }, + forum: { url: '/forum', filters: ['all', 'recent', 'mine', 'top'] }, + event: { url: '/events', filters: ['all', 'recent', 'mine', 'top'] }, task: { url: '/tasks', filters: ['all', 'mine', 'assigned', 'open', 'closed'] }, - votes: { url: '/votes', filters: ['all', 'mine', 'recent', 'top'] }, + votes: { url: '/votes', filters: ['all', 'recent', 'mine', 'top'] }, transfer:{ url: '/transfers', filters: ['all', 'mine', 'pending', 'unconfirmed', 'closed'] }, market: { url: '/market', filters: ['all', 'mine', 'exchange', 'auctions', 'for sale', 'sold'] }, - shop: { url: '/shops', filters: ['all', 'mine', 'recent'] }, + shop: { url: '/shops', filters: ['all', 'recent', 'mine'] }, job: { url: '/jobs', filters: ['ALL', 'MINE', 'REMOTE', 'PRESENCIAL', 'OPEN', 'CLOSED'] }, project: { url: '/projects', filters: ['all', 'mine', 'active', 'completed'] }, + industry:{ url: '/industry', filters: ['ALL', 'MINE', 'ACTIVE', 'PAUSED', 'DISSOLVED', 'BLUEPRINTS', 'BUILDS', 'MEMBER', 'RULES'] }, chat: { url: '/chats', filters: ['all', 'mine'] }, pad: { url: '/pads', filters: ['all', 'mine'] }, calendar:{ url: '/calendars', filters: ['all', 'mine'] }, - report: { url: '/reports', filters: ['all', 'mine', 'recent'] }, + report: { url: '/reports', filters: ['all', 'recent', 'mine'] }, curriculum:{ url: '/cv', filters: ['view', 'edit'] } }; const sub = MODULE_SUB_FILTERS[filter]; + const emptyAct = actions.length === 0 && !qs; let html = template( title, section( - div({ class: 'tags-header' }, + div({ class: 'tags-header activity-header-line' }, h2(i18n.activityList), p(desc) ), - (() => { - const labelOf = {}; - activityTypes.forEach(t => { labelOf[t.type] = t.label; }); - const mkBtn = (type) => form({ method: 'GET', action: '/activity' }, - input({ type: 'hidden', name: 'filter', value: type }), - button({ type: 'submit', class: filter === type ? 'filter-btn active' : 'filter-btn' }, labelOf[type] || type) - ); - const QUICK = ['recent', 'all', 'mine']; - const GROUPS = [ - { id: 'act-comunidad', emoji: '👥', title: i18n.activityGroupCommunity || 'Community & Governance', types: ['inhabitants', 'tribe', 'larp', 'parliament', 'courts', 'votes', 'report'] }, - { id: 'act-organizacion', emoji: '🗓', title: i18n.activityGroupOrg || 'Organization', types: ['event', 'calendar', 'task', 'project', 'job'] }, - { id: 'act-comunicacion', emoji: '💬', title: i18n.activityGroupComm || 'Communication', types: ['post', 'feed', 'chat', 'pad', 'forum'] }, - { id: 'act-economia', emoji: '💰', title: i18n.activityGroupEconomy || 'Economy', types: ['banking', 'market', 'shop', 'transfer'] }, - { id: 'act-media', emoji: '🎬', title: i18n.activityGroupMedia || 'Content & Media', types: ['audio', 'bookmark', 'document', 'image', 'torrent', 'video'] }, - { id: 'act-otros', emoji: '⋯', title: i18n.activityGroupOther || 'Others', types: ['map', 'curriculum'] }, - ]; - return div({ class: 'activity-filter-acc' }, - div({ class: 'activity-quick-modes' }, QUICK.map(mkBtn)), - ul({ class: 'oasis-nav-groups activity-groups' }, - GROUPS.map(g => { - const openByFilter = g.types.includes(filter); - return li({ class: 'oasis-nav-group' }, - input({ type: 'checkbox', id: `oasis-nav-group-${g.id}`, class: 'oasis-nav-toggle', ...(openByFilter ? { checked: true } : {}) }), - label({ for: `oasis-nav-group-${g.id}`, class: 'oasis-nav-header' }, - span({ class: 'emoji' }, g.emoji), - span({ class: 'nav-text' }, g.title), - span({ class: 'act-count' }, String(g.types.length)), - span({ class: 'oasis-nav-arrow' }, '▾') - ), - ul({ class: 'oasis-nav-list' }, - g.types.map(t => li(mkBtn(t))) - ) - ); - }) + emptyAct ? null : div({ class: 'activity-filter-chips' }, + (() => { + const ORDER = [ + 'all', 'recent', 'mine', 'top', + 'inhabitants', 'tribe', 'larp', 'schoolCourse', 'parliament', 'courts', 'emergency', + 'votes', 'event', 'calendar', 'task', 'report', 'campaign', + 'banking', 'market', 'housing', 'project', 'industry', 'job', 'shop', 'logistics', + 'post', 'feed', 'chat', 'room', 'pad', 'wiki', 'mailing', 'forum', 'map', + 'audio', 'bookmark', 'document', 'file', 'image', 'torrent', 'video', 'podcast' + ]; + const byType = new Map(activityTypes.map(t => [t.type, t])); + const placed = new Set(ORDER); + const presentActionTypes = new Set(actions.map(a2 => a2.type)); + const metaKeep = new Set(['all', 'mine', 'recent', 'top']); + const hasContentFor = (type) => + metaKeep.has(type) || (GROUP_SUBTYPES[type] || [type]).some(t2 => presentActionTypes.has(t2)); + const ordered = ORDER.map(type => byType.get(type)).filter(Boolean) + .concat(activityTypes.filter(t => !placed.has(t.type))) + .filter(({ type }) => hasContentFor(type)); + return ordered.map(({ type, label }) => + a({ + href: `/activity?filter=${encodeURIComponent(type)}`, + class: filter === type ? 'activity-chip active' : 'activity-chip' + }, label) + ); + })() + ), + emptyAct ? null : div({ class: 'activity-filter-chips activity-toolbar-row' }, + renderModuleStats(filteredActions.length, [], null, { showInhabitants: true, showTribes: true }), + sub ? span({ class: 'activity-subchip-label' }, '\u21b3') : '', + sub ? sub.filters.map(f => a({ href: `${sub.url}?filter=${encodeURIComponent(f)}`, class: 'activity-chip' }, String(f).toUpperCase())) : '', + form({ method: 'GET', action: '/activity', class: 'filter-box' }, + input({ type: 'hidden', name: 'filter', value: filter }), + input({ type: 'text', name: 'q', value: qs, placeholder: i18n.activitySearchPlaceholder, class: 'filter-box__input' }), + div({ class: 'filter-box__controls' }, + button({ type: 'submit', class: 'filter-box__button' }, i18n.searchButton) ) - ); - })(), - sub - ? div({ class: 'activity-sub-filter' }, - sub.filters.map(f => a({ href: `${sub.url}?filter=${encodeURIComponent(f)}`, class: 'filter-btn' }, String(f).toUpperCase())) - ) - : null, - section({ class: 'feed-container' }, renderActionCards(filteredActions, userId, actions, spreadMap)) + ) + ), + section({ class: 'feed-container' }, renderActionCards(filteredActions, userId, rawActions, spreadMap, { ...extras, paged: true })) ) ); const hasDocument = actions.some(a => a && a.type === 'document'); if (hasDocument) { html += ` - - + `; } return html; diff --git a/src/views/agenda_view.js b/src/views/agenda_view.js index c13b8e2..e7e5512 100644 --- a/src/views/agenda_view.js +++ b/src/views/agenda_view.js @@ -1,19 +1,11 @@ -const { div, h2, p, section, button, form, img, textarea, a, br, h1, span } = require("../server/node_modules/hyperaxe"); -const { template, i18n, userLink, renderContentActions } = require('./main_views'); +const { div, h2, p, section, button, form, img, input, textarea, a, br, h1, span } = require("../server/node_modules/hyperaxe"); +const { safeExternalHref } = require("../backend/renderStyledText"); +const { template, i18n, userLink, renderContentActions, renderModuleStats, renderStateChip, CONTENT_FAV_KIND, CONTENT_SPREADABLE, contentDeleteAction, paged } = require('./main_views'); const moment = require('../server/node_modules/moment'); const { config } = require('../server/SSB_server.js'); const userId = config.keys.id; -const renderCardField = (labelText, value) => - div({ class: 'card-field' }, - span({ class: 'card-label' }, labelText), - span( - { class: 'card-value' }, - ...(Array.isArray(value) ? value : [value ?? '']) - ) - ); - function getViewDetailsAction(item) { switch (item.type) { case 'transfer': return `/transfers/${encodeURIComponent(item.id)}`; @@ -24,129 +16,116 @@ function getViewDetailsAction(item) { case 'report': return `/reports/${encodeURIComponent(item.id)}`; case 'job': return `/jobs/${encodeURIComponent(item.id)}`; case 'project': return `/projects/${encodeURIComponent(item.id)}`; + case 'industry': return `/industry/build/${encodeURIComponent(item.id)}`; + case 'housing': return `/housing/${encodeURIComponent(item.id)}`; + case 'schoolCourse': return `/school/course/${encodeURIComponent(item.id)}`; + case 'campaign': return `/campaigns/${encodeURIComponent(item.id)}`; + case 'logisticsRoute': return `/logistics/${encodeURIComponent(item.id)}`; case 'calendar': return `/calendars/${encodeURIComponent(item.id)}`; + case 'calendarDate': return `/calendars/${encodeURIComponent(item.calendarId)}`; default: return `/messages/${encodeURIComponent(item.id)}`; } } -const renderAgendaItem = (item, userId, filter) => { - const fmt = d => moment(d).format('YYYY/MM/DD HH:mm:ss'); +const agendaDeleteAction = (item) => { + if (item.type === 'transfer') { + const required = item.from === item.to ? 1 : 2; + const confirmed = Array.isArray(item.confirmedBy) ? item.confirmedBy.length : 0; + const dl = item.deadline ? moment(item.deadline) : null; + const expired = dl && dl.isValid() ? dl.isBefore(moment()) : false; + return String(item.status || '').toUpperCase() === 'UNCONFIRMED' && !expired && confirmed < required ? contentDeleteAction('transfer', item.id) : undefined; + } + return contentDeleteAction(item.type, item.id); +}; + +const chip = (text, kind = "whole", icon = null) => (text === null || text === undefined || text === "") ? null : renderStateChip(kind, icon, String(text).toUpperCase()); +const timeChip = (value, fmt = "YYYY/MM/DD HH:mm") => value ? p({ class: "time-chip" }, moment(value).format(fmt)) : null; +const metaLine = (text) => text && String(text).trim() ? p({ class: "job-meta-line" }, String(text)) : null; +const priceChip = (text) => text ? div({ class: "price-chip" }, text) : null; +const countLine = (labelText, value) => div({ class: "tribe-card-members" }, span({ class: "tribe-members-count" }, `${labelText}: ${value}`)); +const statusKind = (status) => String(status || "").toUpperCase() === "CLOSED" ? "closed" : "mutuals"; + +const renderAgendaItem = (item, userId, filter, extras = {}) => { const author = item.seller || item.organizer || item.from || item.author || ''; - - const commonFields = [ - p({ class: 'card-footer' }, - span({ class: 'date-link' }, `${item.createdAt ? moment(item.createdAt).format('YYYY/MM/DD HH:mm:ss') : ''} ${i18n.performed} `), - author ? userLink(author) : '' - ) - ]; - - let details = []; + const chips = []; + const body = []; let actionButton = null; if (filter === 'discarded') { - actionButton = form({ method: 'POST', action: `/agenda/restore/${encodeURIComponent(item.id)}` }, - button({ type: 'submit', class: 'restore-btn' }, i18n.agendaRestoreButton) + actionButton = form({ method: 'POST', action: `/agenda/restore/${encodeURIComponent(item.id)}`, class: 'phone-action-form' }, + button({ type: 'submit', class: 'tribe-action-btn' }, String(i18n.agendaRestoreButton).toUpperCase()) ); } else { - actionButton = form({ method: 'POST', action: `/agenda/discard/${encodeURIComponent(item.id)}` }, - button({ type: 'submit', class: 'discard-btn' }, i18n.agendaDiscardButton) + actionButton = form({ method: 'POST', action: `/agenda/discard/${encodeURIComponent(item.id)}`, class: 'phone-action-form' }, + button({ type: 'submit', class: 'tribe-action-btn' }, String(i18n.agendaDiscardButton).toUpperCase()) ); } + const extraActions = []; + if (filter === 'discarded') extraActions.push(form({ method: 'POST', action: `/agenda/remove/${encodeURIComponent(item.id)}`, class: 'phone-action-form' }, + button({ type: 'submit', class: 'tribe-action-btn danger-btn' }, String(i18n.agendaRemoveButton).toUpperCase()) + )); if (item.type === 'market') { - details = [ - renderCardField(i18n.marketItemType + ":", String(item.item_type || '').toUpperCase()), - renderCardField(i18n.marketItemStatus + ":", item.status), - renderCardField(i18n.marketItemStock + ":", item.stock), - renderCardField(i18n.marketItemPrice + ":", `${item.price} ECO`), - renderCardField(i18n.marketItemIncludesShipping + ":", item.includesShipping ? i18n.agendaYes : i18n.agendaNo), - renderCardField(i18n.deadline + ":", item.deadline ? new Date(item.deadline).toLocaleString() : '') - ]; + chips.push(chip(item.item_type), chip(item.status, statusKind(item.status)), chip(`${i18n.marketItemStock}: ${item.stock}`), item.includesShipping ? chip(i18n.marketItemIncludesShipping, 'mutuals', '✓') : null); if (String(item.item_type || '').toLowerCase() === 'auction') { const bids = Array.isArray(item.auctions_poll) ? item.auctions_poll.map(bid => parseFloat(String(bid).split(':')[1])).filter(n => !isNaN(n)) : []; const maxBid = bids.length ? Math.max(...bids) : 0; - details.push(renderCardField(i18n.marketItemHighestBid + ":", `${maxBid} ECO`)); + chips.push(chip(`${i18n.marketItemHighestBid}: ${maxBid} ECO`, 'whole', '▲')); } - const seller = author ? p(userLink(author)) : ''; - details.push(br(), div({ class: 'members-list' }, i18n.marketItemSeller + ': ', seller)); + body.push(priceChip(`${item.price} ECO`), timeChip(item.deadline)); } if (item.type === 'tribe') { - details = [ - renderCardField(i18n.agendaAnonymousLabel + ":", item.isAnonymous ? i18n.agendaYes : i18n.agendaNo), - renderCardField(i18n.agendaInviteModeLabel + ":", (item.inviteMode ? String(item.inviteMode).toUpperCase() : i18n.noInviteMode)), - renderCardField(i18n.agendaLocationLabel + ":", item.location || i18n.noLocation), - renderCardField(i18n.agendaMembersCount + ":", Array.isArray(item.members) ? item.members.length : 0), - br() - ]; - const membersList = Array.isArray(item.members) ? item.members.map(member => p(userLink(member))) : []; - details.push(div({ class: 'members-list' }, `${i18n.agendaMembersLabel}:`, membersList)); + chips.push(item.isAnonymous ? chip(i18n.agendaAnonymousLabel, 'hidden') : null, chip(item.inviteMode ? item.inviteMode : i18n.noInviteMode)); + body.push(metaLine(item.location || i18n.noLocation), countLine(i18n.agendaMembersLabel, Array.isArray(item.members) ? item.members.length : 0)); } if (item.type === 'report') { - details = [ - renderCardField(i18n.agendareportStatus + ":", item.status || i18n.noStatus), - renderCardField(i18n.agendareportCategory + ":", item.category || i18n.noCategory), - renderCardField(i18n.agendareportSeverity + ":", (item.severity ? String(item.severity).toUpperCase() : i18n.noSeverity)) - ]; + chips.push(chip(item.status || i18n.noStatus, statusKind(item.status)), chip(item.category || i18n.noCategory), chip(item.severity || i18n.noSeverity, 'closed')); } if (item.type === 'event') { - details = [ - renderCardField(i18n.eventDateLabel + ":", item.date ? fmt(item.date) : ''), - renderCardField(i18n.eventLocationLabel + ":", item.location || ''), - renderCardField(i18n.eventPriceLabel + ":", `${item.price} ECO`), - renderCardField( - i18n.eventUrlLabel + ":", - item.url ? p(a({ href: item.url, target: "_blank" }, item.url)) : p(i18n.noUrl) - ) - ]; - actionButton = actionButton || form({ method: 'POST', action: `/events/attend/${encodeURIComponent(item.id)}` }, - button({ type: 'submit', class: 'assign-btn' }, `${i18n.eventAttendButton}`) - ); + body.push(timeChip(item.date), metaLine(item.location), parseFloat(item.price || 0) > 0 ? priceChip(`${item.price} ECO`) : null, + item.url ? p({ class: 'job-meta-line' }, a({ href: safeExternalHref(item.url), target: "_blank", rel: "noopener noreferrer" }, item.url)) : null); } if (item.type === 'task') { - details = [ - renderCardField(i18n.taskStatus + ":", item.status), - renderCardField(i18n.taskPriorityLabel + ":", item.priority), - renderCardField(i18n.taskStartTimeLabel + ":", item.startTime ? new Date(item.startTime).toLocaleString() : ''), - renderCardField(i18n.taskEndTimeLabel + ":", item.endTime ? new Date(item.endTime).toLocaleString() : ''), - renderCardField(i18n.taskLocationLabel + ":", item.location || '') - ]; - const assigned = Array.isArray(item.assignees) && item.assignees.includes(userId); - actionButton = actionButton || form({ method: 'POST', action: `/tasks/assign/${encodeURIComponent(item.id)}` }, - button({ type: 'submit', class: 'assign-btn' }, assigned ? i18n.taskUnassignButton : i18n.taskAssignButton) - ); + chips.push(chip(item.status, statusKind(item.status)), chip(item.priority, 'closed')); + body.push(timeChip(item.startTime), timeChip(item.endTime), metaLine(item.location)); } if (item.type === 'transfer') { - details = [ - renderCardField(i18n.agendaTransferConcept + ":", item.concept), - renderCardField(i18n.agendaTransferAmount + ":", item.amount), - renderCardField(i18n.agendaTransferDeadline + ":", item.deadline ? fmt(item.deadline) : ''), - br() - ]; - const membersList = item.to ? p(userLink(item.to)) : ''; - details.push(div({ class: 'members-list' }, i18n.to + ': ', membersList)); + body.push(metaLine(item.concept), priceChip(`${item.amount} ECO`), timeChip(item.deadline), + item.to && process.env.OASIS_MOBILE !== '1' ? p({ class: 'job-meta-line' }, `${i18n.to}: `, userLink(item.to)) : null); } - + if (item.type === 'project') { - details = [ - renderCardField(i18n.projectStatus + ":", item.status || i18n.noStatus), - renderCardField(i18n.projectProgress + ":", `${item.progress || 0}%`), - renderCardField(i18n.projectGoal + ":", `${item.goal} ECO`), - renderCardField(i18n.projectPledged + ":", `${item.pledged || 0} ECO`), - renderCardField(i18n.projectDeadline + ":", item.deadline ? new Date(item.deadline).toLocaleString() : i18n.noDeadline) - ]; + chips.push(chip(item.status || i18n.noStatus, statusKind(item.status)), chip(`${item.progress || 0}%`, 'mutuals')); + body.push(priceChip(`${item.pledged || 0} / ${item.goal} ECO`), item.deadline ? timeChip(item.deadline) : metaLine(i18n.noDeadline)); } if (item.type === 'calendar') { - details = [ - renderCardField((i18n.calendarStatusLabel || 'Status') + ':', item.isClosed ? (i18n.calendarStatusClosed || 'CLOSED') : (i18n.calendarStatusOpen || 'OPEN')), - renderCardField((i18n.calendarDeadlineLabel || 'Deadline') + ':', item.deadline ? moment(item.deadline).format('YYYY/MM/DD HH:mm') : ''), - renderCardField((i18n.calendarParticipantsLabel || 'Participants') + ':', Array.isArray(item.participants) ? item.participants.length : 0) - ]; + chips.push(chip(item.isClosed ? (i18n.calendarStatusClosed || 'CLOSED') : (i18n.calendarStatusOpen || 'OPEN'), item.isClosed ? 'closed' : 'mutuals')); + body.push(timeChip(item.deadline), countLine(i18n.calendarParticipantsLabel || 'Participants', Array.isArray(item.participants) ? item.participants.length : 0)); + } + + if (item.type === 'campaign') { + chips.push(chip(`${i18n.campaignSignaturesLabel}: ${item.signatureCount || 0} / ${item.goal || 0}`, 'mutuals', '✍')); + body.push(timeChip(item.deadline)); + } + + if (item.type === 'logisticsRoute') { + chips.push(chip(item.kind), chip(item.mode)); + body.push(metaLine(`${item.origin || ''} → ${item.destination || ''}`), timeChip(item.date)); + } + + if (item.type === 'housing') { + const isOwner = String(item.author) === String(userId); + const requestCount = Number(item.requestCount) || 0; + chips.push(chip(i18n["housingType" + String(item.housing_type || '').toUpperCase()] || item.housing_type), + chip(String(item.status || '').toUpperCase() === 'CLOSED' ? i18n.housingStatusCLOSED : i18n.housingStatusOPEN, statusKind(item.status)), + isOwner ? chip(`${i18n.housingRequests}: ${requestCount}`) : chip(i18n.housingRequestedBadge || 'REQUESTED', 'mutuals', '✓')); + body.push(metaLine(item.place), priceChip(String(item.housing_type) === 'couchsurfing' ? (i18n.housingFree || 'FREE') : `${item.price} ECO`), timeChip(item.availableFrom, 'YYYY/MM/DD')); } if (item.type === 'job') { @@ -154,106 +133,106 @@ const renderAgendaItem = (item, userId, filter) => { ? item.subscribers : (typeof item.subscribers === 'string' ? item.subscribers.split(',').map(s => s.trim()).filter(Boolean) - : (item.subscribers && typeof item.subscribers.length === 'number' - ? Array.from(item.subscribers) - : [])); + : (item.subscribers && typeof item.subscribers.length === 'number' ? Array.from(item.subscribers) : [])); + chips.push(chip(item.status, statusKind(item.status)), chip(item.job_type), chip(item.languages), chip(`${i18n.jobVacants}: ${item.vacants}`)); + body.push(metaLine(item.location), priceChip(`${item.salary} ECO`), countLine(i18n.jobSubscribers, subs.length)); + } - const subsInterleaved = subs - .map((id, i) => [i > 0 ? ', ' : '', userLink(id)]) - .flat(); - - details = [ - renderCardField(i18n.jobStatus + ":", item.status), - renderCardField(i18n.jobLocation + ":", (item.location || '').toUpperCase()), - renderCardField(i18n.jobType + ":", (item.job_type || '').toUpperCase()), - renderCardField(i18n.jobSalary + ":", `${item.salary} ECO`), - renderCardField(i18n.jobVacants + ":", item.vacants), - renderCardField(i18n.jobLanguages + ":", (item.languages || '').toUpperCase()), - br(), - div( - { class: 'members-list' }, - i18n.jobSubscribers + ': ',br(),br(), - ...(subs.length ? subsInterleaved : [i18n.noSubscribers.toUpperCase()]) - ), - ]; - - const subscribed = subs.includes(userId); - if (!subscribed && String(item.status).toUpperCase() !== 'CLOSED' && item.author !== userId) { - actionButton = form({ method: 'POST', action: `/jobs/subscribe/${encodeURIComponent(item.id)}` }, - button({ type: 'submit', class: 'subscribe-btn' }, i18n.jobSubscribeButton) - ); - } + if (item.type === 'industry') { + const st = String(item.status || 'PROPOSED').toUpperCase(); + chips.push(chip(i18n['industryBuildStatus_' + st] || st, statusKind(st))); + body.push(metaLine(item.facilityName)); } const isOwn = author && String(author) === String(userId); + const favKind = CONTENT_FAV_KIND[item.type]; + const favIndex = extras.favIndex instanceof Map ? extras.favIndex : null; + const spreadMap = extras.spreadMap instanceof Map ? extras.spreadMap : null; + const href = getViewDetailsAction(item); + const title = item.title || item.name || item.concept || ''; return div({ class: 'trending-card agenda-card' + (isOwn ? ' own-content' : '') }, div({ class: 'card-header activity-card-header' }, span({ class: 'pm-exposition-chip pm-exposition-whole' }, span({ class: 'pm-exposition-text' }, String(item.type || '').toUpperCase()) ), - renderContentActions(item.id, getViewDetailsAction(item)) + renderContentActions(item.id, href, { + author, + reportTitle: title, + spread: CONTENT_SPREADABLE.has(item.type) ? ((spreadMap && spreadMap.get(item.id)) || null) : undefined, + ...(favKind ? { favKind, isFavorite: item.isFavorite === true || (!!favIndex && [item.rootId, item.id].some(id => id && favIndex.get(String(id)) === favKind)) } : {}), + returnTo: `/agenda?filter=${encodeURIComponent(filter || 'all')}`, + deleteAction: isOwn ? agendaDeleteAction(item) : undefined + }) ), div({ class: 'card-section agenda-card-body' }, - actionButton, - br(), - h2(item.title || item.name || item.concept || ''), - ...details, - ...commonFields + div({ class: 'shop-title-row' }, h2({ class: 'tribe-card-title' }, a({ href }, title))), + chips.filter(Boolean).length ? div({ class: 'card-chips-row' }, ...chips.filter(Boolean)) : null, + ...body.filter(Boolean), + p({ class: 'card-footer' }, + span({ class: 'date-link' }, `${item.createdAt ? moment(item.createdAt).format('YYYY/MM/DD HH:mm') : ''}`), + author ? userLink(author) : '' + ), + div({ class: 'agenda-card-actions' }, actionButton, ...extraActions) ) ); }; -exports.agendaView = async (data, filter) => { +exports.agendaView = async (data, filter, q = '', extras = {}) => { const { items = [], counts: _c = {} } = data || {}; - const counts = { all: 0, open: 0, closed: 0, events: 0, tasks: 0, reports: 0, tribes: 0, jobs: 0, market: 0, projects: 0, transfers: 0, calendars: 0, discarded: 0, ..._c }; + const counts = { all: 0, open: 0, closed: 0, events: 0, tasks: 0, reports: 0, tribes: 0, jobs: 0, market: 0, projects: 0, transfers: 0, calendars: 0, housing: 0, discarded: 0, ..._c }; + const emptyAgenda = Number(counts.all || 0) === 0 && !String(q || '').trim(); return template( i18n.agendaTitle, section( - div({ class: 'tags-header' }, + div({ class: 'tags-header module-header-line' }, h2(i18n.agendaTitle), p(i18n.agendaDescription) ), - div({ class: 'filters' }, - form({ method: 'GET', action: '/agenda' }, - button({ type: 'submit', name: 'filter', value: 'all', class: filter === 'all' ? 'filter-btn active' : 'filter-btn' }, - `${i18n.agendaFilterAll} (${counts.all})`), - button({ type: 'submit', name: 'filter', value: 'today', class: filter === 'today' ? 'filter-btn active' : 'filter-btn' }, - `${i18n.agendaFilterToday || 'TODAY'} (${counts.today || 0})`), - button({ type: 'submit', name: 'filter', value: 'upcoming', class: filter === 'upcoming' ? 'filter-btn active' : 'filter-btn' }, - `${i18n.agendaFilterUpcoming || 'UPCOMING'} (${counts.upcoming || 0})`), - button({ type: 'submit', name: 'filter', value: 'overdue', class: filter === 'overdue' ? 'filter-btn active' : 'filter-btn' }, - `${i18n.agendaFilterOverdue || 'OVERDUE'} (${counts.overdue || 0})`), - button({ type: 'submit', name: 'filter', value: 'open', class: filter === 'open' ? 'filter-btn active' : 'filter-btn' }, - `${i18n.agendaFilterOpen} (${counts.open})`), - button({ type: 'submit', name: 'filter', value: 'closed', class: filter === 'closed' ? 'filter-btn active' : 'filter-btn' }, - `${i18n.agendaFilterClosed} (${counts.closed})`), - button({ type: 'submit', name: 'filter', value: 'events', class: filter === 'events' ? 'filter-btn active' : 'filter-btn' }, - `${i18n.agendaFilterEvents} (${counts.events})`), - button({ type: 'submit', name: 'filter', value: 'tasks', class: filter === 'tasks' ? 'filter-btn active' : 'filter-btn' }, - `${i18n.agendaFilterTasks} (${counts.tasks})`), - button({ type: 'submit', name: 'filter', value: 'reports', class: filter === 'reports' ? 'filter-btn active' : 'filter-btn' }, - `${i18n.agendaFilterReports} (${counts.reports})`), - button({ type: 'submit', name: 'filter', value: 'tribes', class: filter === 'tribes' ? 'filter-btn active' : 'filter-btn' }, - `${i18n.agendaFilterTribes} (${counts.tribes})`), - button({ type: 'submit', name: 'filter', value: 'jobs', class: filter === 'jobs' ? 'filter-btn active' : 'filter-btn' }, - `${i18n.agendaFilterJobs} (${counts.jobs})`), - button({ type: 'submit', name: 'filter', value: 'market', class: filter === 'market' ? 'filter-btn active' : 'filter-btn' }, - `${i18n.agendaFilterMarket} (${counts.market})`), - button({ type: 'submit', name: 'filter', value: 'projects', class: filter === 'projects' ? 'filter-btn active' : 'filter-btn' }, - `${i18n.agendaFilterProjects} (${counts.projects})`), - button({ type: 'submit', name: 'filter', value: 'calendars', class: filter === 'calendars' ? 'filter-btn active' : 'filter-btn' }, - `${i18n.agendaFilterCalendars || 'CALENDARS'} (${counts.calendars})`), - button({ type: 'submit', name: 'filter', value: 'transfers', class: filter === 'transfers' ? 'filter-btn active' : 'filter-btn' }, - `${i18n.agendaFilterTransfers} (${counts.transfers})`), - button({ type: 'submit', name: 'filter', value: 'discarded', class: filter === 'discarded' ? 'filter-btn active' : 'filter-btn' }, - `DISCARDED (${counts.discarded})`) + emptyAgenda ? null : div({ class: 'mode-buttons-row' }, + ...[ + ['all', i18n.agendaFilterAll], + ['today', i18n.agendaFilterToday || 'TODAY'], + ['upcoming', i18n.agendaFilterUpcoming || 'UPCOMING'], + ['overdue', i18n.agendaFilterOverdue || 'OVERDUE'], + ['open', i18n.agendaFilterOpen], + ['closed', i18n.agendaFilterClosed], + ['events', i18n.agendaFilterEvents], + ['tasks', i18n.agendaFilterTasks], + ['reports', i18n.agendaFilterReports], + ['tribes', i18n.agendaFilterTribes], + ['jobs', i18n.agendaFilterJobs], + ['market', i18n.agendaFilterMarket], + ['projects', i18n.agendaFilterProjects], + ['industry', i18n.agendaFilterIndustry || 'INDUSTRY'], + ['housing', i18n.agendaFilterHousing || 'HOUSING'], + ['school', i18n.agendaFilterSchool || 'SCHOOL'], + ['campaigns', i18n.agendaFilterCampaigns || 'CAMPAIGNS'], + ['logistics', i18n.agendaFilterLogistics || 'LOGISTICS'], + ['calendars', i18n.agendaFilterCalendars || 'CALENDARS'], + ['transfers', i18n.agendaFilterTransfers], + ['discarded', 'DISCARDED'] + ].filter(([value]) => value === 'all' || filter === value || Number(counts[value] || 0) > 0) + .map(([value, labelText]) => + form({ method: 'GET', action: '/agenda' }, + input({ type: 'hidden', name: 'filter', value }), + button({ type: 'submit', class: filter === value ? 'filter-btn active' : 'filter-btn' }, String(labelText).toUpperCase()))) + ), + emptyAgenda ? null : div({ class: 'filters activity-filter-chips activity-toolbar-row' }, + renderModuleStats(items.length, [ + ['today', i18n.agendaFilterToday || 'TODAY'], ['upcoming', i18n.agendaFilterUpcoming || 'UPCOMING'], ['overdue', i18n.agendaFilterOverdue || 'OVERDUE'], + ['open', i18n.agendaFilterOpen], ['closed', i18n.agendaFilterClosed], ['discarded', 'DISCARDED'] + ].map(([value, label]) => ({ label: String(label).toUpperCase(), count: Number(counts[value] || 0) }))), + form({ method: 'GET', action: '/agenda', class: 'filter-box' }, + input({ type: 'hidden', name: 'filter', value: filter }), + input({ type: 'text', name: 'q', value: q, placeholder: i18n.agendaSearchPlaceholder, class: 'filter-box__input' }), + div({ class: 'filter-box__controls' }, + button({ type: 'submit', class: 'filter-box__button' }, i18n.searchButton) + ) ) ), - div({ class: 'agenda-list' }, - items.length - ? items.map(item => renderAgendaItem(item, userId, filter)) - : p(i18n.agendaNoItems) - ) + items.length + ? div({ class: 'jobs-grid agenda-list' }, ...paged(items).map(item => renderAgendaItem(item, userId, filter, extras || {}))) + : div({ class: 'no-content-box' }, p({ class: 'no-content' }, i18n.agendaNoItems)) ) ); }; diff --git a/src/views/audio_view.js b/src/views/audio_view.js index 7b4f57c..83e9b6b 100644 --- a/src/views/audio_view.js +++ b/src/views/audio_view.js @@ -15,12 +15,14 @@ const { label, option } = require("../server/node_modules/hyperaxe"); +const { renderCommentsSection: renderSharedCommentsSection, renderCommentsLink } = require("./comments_view"); -const { template, i18n, renderOpinionsVoting, userLink, renderSpreadButton, renderEcoTax, renderLifespanChip, renderContentActions } = require("./main_views"); +const { renderLicenseChip, renderLicenseSelect, renderReachChip, renderClearnetSelector, renderClearnetSwitch, renderTribeOriginChip } = require('./clearnet_view'); +const { clearnetItemHref, template, i18n, renderOpinionsVoting, renderEngagement, userLink, renderSpreadButton, renderEcoTax, renderLifespanChip, renderContentActions, renderSpreadEditWarning, renderModuleStats, moduleIsEmpty, contentDeleteAction, paged, blobUrlOf } = require("./main_views"); const moment = require("../server/node_modules/moment"); const { config } = require("../server/SSB_server.js"); -const { renderUrl } = require("../backend/renderUrl") -const { renderMapLocationVisitLabel } = require("./maps_view"); +const { renderStyledText } = require("../backend/renderStyledText") +const { renderMapLocationVisitLabel, renderMapEmbed } = require("./maps_view"); const userId = config.keys.id; @@ -47,21 +49,6 @@ const renderTags = (tags) => { : null; }; -const renderAudioFavoriteToggle = (audioObj, returnTo = "") => - form( - { - method: "POST", - action: audioObj.isFavorite - ? `/audios/favorites/remove/${encodeURIComponent(audioObj.key)}` - : `/audios/favorites/add/${encodeURIComponent(audioObj.key)}` - }, - returnTo ? input({ type: "hidden", name: "returnTo", value: returnTo }) : null, - button( - { type: "submit", class: "filter-btn" }, - audioObj.isFavorite ? i18n.audioRemoveFavoriteButton : i18n.audioAddFavoriteButton - ) - ); - const renderTranscodeButton = (audioObj) => audioObj.isBcs ? form( @@ -74,7 +61,7 @@ const renderAudioPlayer = (audioObj, opts = {}) => audioObj?.url ? div( { class: "audio-container" }, - audioHyperaxe({ controls: true, src: `/blob/${encodeURIComponent(audioObj.url)}`, preload: "metadata" }), + audioHyperaxe({ controls: true, src: blobUrlOf(audioObj.url), preload: "metadata" }), opts.skipTranscode ? null : renderTranscodeButton(audioObj) ) : p(i18n.audioNoFile); @@ -96,78 +83,16 @@ const renderAudioOwnerActions = (filter, audioObj, params = {}) => { ) ); } - items.push( - form( - { method: "POST", action: `/audios/delete/${encodeURIComponent(audioObj.key)}` }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - button({ class: "delete-btn", type: "submit" }, i18n.audioDeleteButton) - ) - ); return items; }; const renderAudioCommentsSection = (audioId, comments = [], returnTo = null) => { - const list = safeArr(comments).filter(c => { - const t = c && c.value && c.value.content && c.value.content.text; - return t && String(t).trim(); + return renderSharedCommentsSection({ + action: `/audios/${encodeURIComponent(audioId)}/comments`, + comments: comments, + returnTo: returnTo }); - const commentsCount = list.length; - - return div( - { class: "vote-comments-section" }, - div( - { class: "comments-count" }, - span({ class: "card-label" }, i18n.voteCommentsLabel + ": "), - span({ class: "card-value" }, String(commentsCount)) - ), - div( - { class: "comment-form-wrapper" }, - h2({ class: "comment-form-title" }, i18n.voteNewCommentLabel), - form( - { method: "POST", action: `/audios/${encodeURIComponent(audioId)}/comments`, class: "comment-form", enctype: "multipart/form-data" }, - returnTo ? input({ type: "hidden", name: "returnTo", value: returnTo }) : null, - textarea({ - id: "comment-text", - name: "text", - rows: 4, - class: "comment-textarea", - placeholder: i18n.voteNewCommentPlaceholder - }), - div({ class: "comment-file-upload" }, label(i18n.uploadMedia), input({ type: "file", name: "blob" })), - br(), - button({ type: "submit", class: "comment-submit-btn" }, i18n.voteNewCommentButton) - ) - ), - list.length - ? div( - { class: "comments-list" }, - list.map((c) => { - const author = c?.value?.author || ""; - const ts = c?.value?.timestamp || c?.timestamp; - const absDate = ts ? moment(ts).format("YYYY/MM/DD HH:mm:ss") : ""; - const relDate = ts ? moment(ts).fromNow() : ""; - const content = c?.value?.content || {}; - const rootId = content.fork || content.root || null; - const text = content.text || ""; - - return div( - { class: "votations-comment-card" }, - span( - { class: "created-at" }, - span(i18n.createdBy), - author ? userLink(author) : span("(unknown)"), - absDate ? span(" | ") : "", - absDate ? span({ class: "votations-comment-date" }, absDate) : "", - relDate ? span({ class: "votations-comment-date" }, " | ", i18n.sendTime) : "", - relDate && rootId ? a({ href: `/thread/${encodeURIComponent(rootId)}#${encodeURIComponent(c.key)}` }, relDate) : "" - ), - p({ class: "votations-comment-text" }, ...renderUrl(text)) - ); - }) - ) - : p({ class: "votations-no-comments" }, i18n.voteNoCommentsYet) - ); }; const renderAudioList = exports.renderAudioList = (audios, filter, params = {}) => { @@ -177,63 +102,34 @@ const renderAudioList = exports.renderAudioList = (audios, filter, params = {}) ? audios.map((audioObj) => { const commentCount = typeof audioObj.commentCount === "number" ? audioObj.commentCount : 0; const title = safeText(audioObj.title); - const ownerActions = renderAudioOwnerActions(filter, audioObj, params); const isOwn = audioObj.author && String(audioObj.author) === String(userId); + const headerActions = typeof params.headerActions === "function" + ? params.headerActions(audioObj) + : audioObj.tribeOrigin + ? renderContentActions(null, audioObj.tribeOrigin.href) + : renderContentActions(audioObj.key, `/audios/${encodeURIComponent(audioObj.key)}`, { spread: (params.spreadMap && params.spreadMap.get(audioObj.key)) || params.spreads || null, author: audioObj.author, favKind: 'audios', torrentFrom: { blobId: audioObj.url, name: audioObj.title }, isFavorite: audioObj.isFavorite, reportTitle: audioObj.title, deleteAction: isOwn ? contentDeleteAction('audio', audioObj.key) : undefined, returnTo }); + const titleHref = typeof params.headerActions === "function" ? null : audioObj.tribeOrigin ? audioObj.tribeOrigin.href : `/audios/${encodeURIComponent(audioObj.key)}`; + const engagement = typeof params.engagement === "function" + ? params.engagement(audioObj) + : audioObj.tribeOrigin ? null : renderEngagement(audioObj.key, + renderOpinionsVoting('/audios/opinions', audioObj.key, audioObj.opinions, returnTo, audioObj.opinions_inhabitants), + renderCommentsLink({ href: `/audios/${encodeURIComponent(audioObj.key)}`, count: commentCount }) + ); return div( { class: "trending-card audio-card" + (isOwn ? " own-content" : "") }, div( { class: "card-header activity-card-header" }, span(), - renderContentActions(audioObj.key, `/audios/${encodeURIComponent(audioObj.key)}`) + headerActions ), div( { class: "card-section audio-card-body" }, - div( - { class: "bookmark-topbar" }, - div( - { class: "bookmark-topbar-left" }, - renderAudioFavoriteToggle(audioObj, returnTo), - audioObj.author && String(audioObj.author) !== String(userId) - ? form( - { method: "GET", action: "/pm" }, - input({ type: "hidden", name: "recipients", value: audioObj.author }), - button({ type: "submit", class: "filter-btn" }, i18n.audioMessageAuthorButton) - ) - : null - ), - ownerActions.length - ? div({ class: "bookmark-topbar-right" }, div({ class: "bookmark-actions" }, ...ownerActions)) - : null - ), - title ? h2(title) : null, + div({ class: "shop-title-row" }, title ? h2(titleHref ? a({ href: titleHref, class: "card-title-link" }, title) : title) : null, audioObj.tribeOrigin ? renderTribeOriginChip(audioObj.tribeOrigin) : audioObj.clearnet === true ? renderReachChip(true, i18n, clearnetItemHref('audios', audioObj.title, audioObj.key)) : null, renderLicenseChip(audioObj.license), ...(typeof params.titleChips === "function" ? params.titleChips(audioObj) : [])), audioObj.lifetime ? div({ class: "card-chips-row" }, renderLifespanChip(audioObj.lifetime, i18n)) : null, renderAudioPlayer(audioObj), - div( - { class: "card-comments-summary" }, - span({ class: "card-label" }, i18n.voteCommentsLabel + ":"), - span({ class: "card-value" }, String(commentCount)), - br(), - br(), - form( - { method: "GET", action: `/audios/${encodeURIComponent(audioObj.key)}` }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - input({ type: "hidden", name: "filter", value: filter || "all" }), - params.q ? input({ type: "hidden", name: "q", value: params.q }) : null, - params.sort ? input({ type: "hidden", name: "sort", value: params.sort }) : null, - button({ type: "submit", class: "filter-btn" }, i18n.voteCommentsForumButton) - ) - ), - div( - { class: "card-comments-summary feed-opinions-section" }, - div( - { class: "comments-count" }, - span({ class: "card-label" }, (i18n.opinionsTitle || "Opinions") + ": "), - span({ class: "card-value" }, String(Object.values(audioObj.opinions || {}).reduce((s, n) => s + (Number(n) || 0), 0))) - ), - renderOpinionsVoting('/audios/opinions', audioObj.key, audioObj.opinions, returnTo, audioObj.opinions_inhabitants) - ), - div({ class: "card-spread-left" }, renderSpreadButton(audioObj.key, (params.spreadMap && params.spreadMap.get(audioObj.key)) || params.spreads)), + ...(typeof params.bodyExtra === "function" ? params.bodyExtra(audioObj) : []), + engagement, renderMapLocationVisitLabel(audioObj.mapUrl), br(), (() => { @@ -243,12 +139,12 @@ const renderAudioList = exports.renderAudioList = (audios, filter, params = {}) return p( { class: "card-footer" }, - span({ class: "date-link" }, `${moment(audioObj.createdAt).format("YYYY/MM/DD HH:mm:ss")} ${i18n.performed} `), + span({ class: "date-link" }, `${moment(audioObj.createdAt).format("YYYY/MM/DD HH:mm")}`), userLink(audioObj.author), showUpdated ? span( { class: "votations-comment-date" }, - ` | ${i18n.audioUpdatedAt}: ${moment(audioObj.updatedAt).format("YYYY/MM/DD HH:mm:ss")}` + ` | ${i18n.audioUpdatedAt}: ${moment(audioObj.updatedAt).format("YYYY/MM/DD HH:mm")}` ) : null ); @@ -263,6 +159,7 @@ const renderAudioForm = (filter, audioId, audioToEdit, params = {}) => { const returnTo = safeText(params.returnTo) || buildReturnTo("all", params); return div( { class: "div-center audio-form" }, + params.spreadWarning || null, form( { action: filter === "edit" ? `/audios/update/${encodeURIComponent(audioId)}` : "/audios/create", @@ -272,16 +169,16 @@ const renderAudioForm = (filter, audioId, audioToEdit, params = {}) => { input({ type: "hidden", name: "returnTo", value: returnTo }), span(i18n.audioFileLabel), br(), - input({ type: "file", name: "audio", required: filter !== "edit" }), + input({ type: "file", name: "audio", accept: "audio/*", required: filter !== "edit" }), br(), br(), span(i18n.audioTitleLabel), br(), - input({ type: "text", name: "title", placeholder: i18n.audioTitlePlaceholder, value: audioToEdit?.title || "" }), + input({ type: "text", name: "title", maxlength: "100", placeholder: i18n.audioTitlePlaceholder, value: audioToEdit?.title || "" }), br(), span(i18n.audioDescriptionLabel), br(), - textarea({ name: "description", placeholder: i18n.audioDescriptionPlaceholder, rows: "4" }, audioToEdit?.description || ""), + textarea({ maxlength: "5000", name: "description", placeholder: i18n.audioDescriptionPlaceholder, rows: "4" }, audioToEdit?.description || ""), br(), span(i18n.mapLocationTitle || "Map Location"), br(), @@ -296,62 +193,61 @@ const renderAudioForm = (filter, audioId, audioToEdit, params = {}) => { value: safeArr(audioToEdit?.tags).join(", ") }), br(), + ...renderLicenseSelect(audioToEdit?.license, i18n), + br(), + renderClearnetSelector(filter === "edit" ? !!audioToEdit?.clearnet : false, i18n), br(), button({ type: "submit" }, filter === "edit" ? i18n.audioUpdateButton : i18n.audioCreateButton) ) ); }; +const mediaChipFor = (filter, censusM) => (mode) => { + if (mode === filter) return true; + if (!Array.isArray(censusM)) return true; + if (mode === "top") return censusM.some((x) => !x.tribeOrigin); + if (mode === "mine") return censusM.some((x) => String(x.author) === String(userId)); + if (mode === "recent") return censusM.length > 0; + if (mode === "favorites") return censusM.some((x) => x.isFavorite); + if (mode === "bcs") return censusM.some((x) => !x.tribeOrigin && String(x.title || "").toUpperCase().startsWith("BCS-")); + return true; +}; + exports.audioView = async (audios, filter = "all", audioId = null, params = {}) => { - const title = - filter === "mine" - ? i18n.audioMineSectionTitle - : filter === "create" - ? i18n.audioCreateSectionTitle - : filter === "edit" - ? i18n.audioUpdateSectionTitle - : filter === "recent" - ? i18n.audioRecentSectionTitle - : filter === "top" - ? i18n.audioTopSectionTitle - : filter === "favorites" - ? i18n.audioFavoritesSectionTitle - : i18n.audioAllSectionTitle; + if (filter === "edit") params = { ...params, spreadWarning: await renderSpreadEditWarning(audioId) }; + const title = i18n.audioTitle; const q = safeText(params.q || ""); const sort = safeText(params.sort || "recent"); const list = safeArr(audios); + const emptyMod = moduleIsEmpty(list, filter, "all", q); + const mediaChip = mediaChipFor(filter, Array.isArray(params.censusList) ? params.censusList : list); const audioToEdit = audioId ? list.find((a) => a.key === audioId) : null; return template( title, section( - div({ class: "tags-header" }, + div({ class: "tags-header module-header-line" }, h2(title), p(i18n.audioDescription) ), - (() => { - const { renderReachChip } = require('./clearnet_view'); - const isClearnet = !!(params.viewerPrefs && params.viewerPrefs.clearnetAudios); - return div({ class: "shop-title-row" }, renderReachChip(isClearnet, i18n)); - })(), - br(), div( { class: "filters" }, form( { method: "GET", action: "/audios", class: "ui-toolbar ui-toolbar--filters" }, - input({ type: "hidden", name: "q", value: q }), input({ type: "hidden", name: "sort", value: sort }), - button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, i18n.audioFilterAll), - button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, i18n.audioFilterMine), - button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, i18n.audioFilterRecent), - button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, i18n.audioFilterTop), - button({ type: "submit", name: "filter", value: "bcs", class: filter === "bcs" ? "filter-btn active" : "filter-btn" }, i18n.audioFilterBcs || "BCS"), - button( + ...(emptyMod ? [] : [ + button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.audioFilterAll).toUpperCase()), + ...(mediaChip("recent") ? [button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.audioFilterRecent).toUpperCase())] : []), + ...(mediaChip("mine") ? [button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.audioFilterMine).toUpperCase())] : []), + ...(mediaChip("favorites") ? [button( { type: "submit", name: "filter", value: "favorites", class: filter === "favorites" ? "filter-btn active" : "filter-btn" }, - i18n.audioFilterFavorites - ), + String(i18n.audioFilterFavorites).toUpperCase() + )] : []), + ...(mediaChip("top") ? [button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, String(i18n.audioFilterTop).toUpperCase())] : []), + ...(mediaChip("bcs") ? [button({ type: "submit", name: "filter", value: "bcs", class: filter === "bcs" ? "filter-btn active" : "filter-btn" }, i18n.audioFilterBcs || "BCS")] : []), + ]), button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.audioCreateButton) ) ) @@ -360,8 +256,9 @@ exports.audioView = async (audios, filter = "all", audioId = null, params = {}) filter === "create" || filter === "edit" ? renderAudioForm(filter, audioId, audioToEdit, { ...params, filter }) : section( - div( - { class: "audios-search" }, + emptyMod ? null : div( + { class: "audios-search activity-filter-chips activity-toolbar-row" }, + renderModuleStats(list.length), form( { method: "GET", action: "/audios", class: "filter-box" }, input({ type: "hidden", name: "filter", value: filter }), @@ -376,29 +273,29 @@ exports.audioView = async (audios, filter = "all", audioId = null, params = {}) { class: "filter-box__controls" }, select( { name: "sort", class: "filter-box__select" }, - option({ value: "recent", selected: sort === "recent" }, i18n.audioSortRecent), - option({ value: "oldest", selected: sort === "oldest" }, i18n.audioSortOldest), - option({ value: "top", selected: sort === "top" }, i18n.audioSortTop) + option({ value: "recent", ...(sort === "recent" ? { selected: true } : {})}, i18n.audioSortRecent), + option({ value: "oldest", ...(sort === "oldest" ? { selected: true } : {})}, i18n.audioSortOldest), + option({ value: "top", ...(sort === "top" ? { selected: true } : {})}, i18n.audioSortTop) ), button({ type: "submit", class: "filter-box__button" }, i18n.audioSearchButton) ) ) ), - div({ class: "audios-list" }, renderAudioList(list, filter, { q, sort })) + div({ class: "audios-list" }, renderAudioList(paged(list), filter, { q, sort, spreadMap: params.spreadMap })) ) ) ); }; exports.singleAudioView = async (audioObj, filter = "all", comments = [], params = {}) => { + const mediaChip = mediaChipFor(filter, Array.isArray(params.censusList) ? params.censusList : null); const q = safeText(params.q || ""); const sort = safeText(params.sort || "recent"); const returnTo = safeText(params.returnTo) || buildReturnTo(filter, { q, sort }); const title = safeText(audioObj.title); const isAuthor = String(audioObj.author) === String(userId); - const { renderReachChip } = require('./clearnet_view'); - const isClearnet = !!(params.authorPrefs && params.authorPrefs.clearnetAudios); + const isClearnet = !!audioObj.clearnet; const chips = [ renderLifespanChip(audioObj.lifetime, i18n), @@ -407,17 +304,9 @@ exports.singleAudioView = async (audioObj, filter = "all", comments = [], params const ownerActions = renderAudioOwnerActions(filter, audioObj, { q, sort }); const sideActions = []; - sideActions.push(renderAudioFavoriteToggle(audioObj, returnTo)); - if (audioObj.author && String(audioObj.author) !== String(userId)) { - sideActions.push(form( - { method: "GET", action: "/pm" }, - input({ type: "hidden", name: "recipients", value: audioObj.author }), - button({ type: "submit", class: "filter-btn" }, i18n.audioMessageAuthorButton) - )); - } if (audioObj.isBcs) { sideActions.push(form( - { method: "GET", action: `/melody/transcode/${encodeURIComponent(audioObj.key)}` }, + { method: "GET", action: `/melody/transcode/${encodeURIComponent(audioObj.key)}`, class: "audio-transcode-form" }, button({ type: "submit", class: "filter-btn" }, i18n.audioTranscodeButton || "TRANSCODE") )); } @@ -425,22 +314,36 @@ exports.singleAudioView = async (audioObj, filter = "all", comments = [], params const tagsNode = renderTags(audioObj.tags); + const detailActions = div({ class: "card-header activity-card-header" }, + renderContentActions(audioObj.key, `/audios/${encodeURIComponent(audioObj.key)}`, { + author: audioObj.author, + favKind: 'audios', torrentFrom: { blobId: audioObj.url, name: audioObj.title }, + isFavorite: audioObj.isFavorite, + spread: params.spreads || null, + returnTo, + reportTitle: audioObj.title, + deleteAction: isAuthor ? contentDeleteAction('audio', audioObj.key) : undefined + }) + ); + const audioSide = div({ class: "tribe-side" }, div({ class: "shop-title-row" }, title ? h2({ class: "tribe-card-title" }, title) : null, - renderReachChip(isClearnet, i18n) + renderReachChip(isClearnet, i18n, isClearnet ? clearnetItemHref('audios', audioObj.title, audioObj.key) : null), + isAuthor ? renderClearnetSwitch('audios', audioObj.rootId || audioObj.key, isClearnet) : null, + renderLicenseChip(audioObj.license) ), chips.length ? div({ class: "card-chips-row" }, ...chips) : null, safeText(audioObj.description) - ? p({ class: "tribe-side-description" }, ...renderUrl(audioObj.description)) + ? p({ class: "tribe-side-description" }, ...renderStyledText(audioObj.description)) : null, tagsNode, - div({ class: "card-spread-centered" }, renderSpreadButton(audioObj.key, params.spreads)), - renderMapLocationVisitLabel(audioObj.mapUrl), + renderMapEmbed(params.mapData, audioObj.mapUrl), sideActions.length ? div({ class: "tribe-side-actions" }, ...sideActions) : null ); const audioMain = div({ class: "tribe-main" }, + detailActions, renderAudioPlayer(audioObj), (() => { const createdTs = audioObj.createdAt ? new Date(audioObj.createdAt).getTime() : NaN; @@ -449,24 +352,26 @@ exports.singleAudioView = async (audioObj, filter = "all", comments = [], params return p( { class: "card-footer" }, - span({ class: "date-link" }, `${moment(audioObj.createdAt).format("YYYY/MM/DD HH:mm:ss")} ${i18n.performed} `), + span({ class: "date-link" }, `${moment(audioObj.createdAt).format("YYYY/MM/DD HH:mm")}`), userLink(audioObj.author), showUpdated ? span( { class: "votations-comment-date" }, - ` | ${i18n.audioUpdatedAt}: ${moment(audioObj.updatedAt).format("YYYY/MM/DD HH:mm:ss")}` + ` | ${i18n.audioUpdatedAt}: ${moment(audioObj.updatedAt).format("YYYY/MM/DD HH:mm")}` ) : null ); })(), - renderOpinionsVoting('/audios/opinions', audioObj.key, audioObj.opinions, returnTo, audioObj.opinions_inhabitants), - renderAudioCommentsSection(audioObj.key, comments, returnTo) + renderEngagement(audioObj.key, + renderOpinionsVoting('/audios/opinions', audioObj.key, audioObj.opinions, returnTo, audioObj.opinions_inhabitants), + renderAudioCommentsSection(audioObj.key, comments, returnTo) + ) ); return template( i18n.audioTitle, section( - div({ class: "tags-header" }, + div({ class: "tags-header module-header-line" }, h2(i18n.audioAllSectionTitle || i18n.audioTitle), p(i18n.audioDescription) ), @@ -474,17 +379,16 @@ exports.singleAudioView = async (audioObj, filter = "all", comments = [], params { class: "filters" }, form( { method: "GET", action: "/audios", class: "ui-toolbar ui-toolbar--filters" }, - input({ type: "hidden", name: "q", value: q }), input({ type: "hidden", name: "sort", value: sort }), - button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, i18n.audioFilterAll), - button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, i18n.audioFilterMine), - button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, i18n.audioFilterRecent), - button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, i18n.audioFilterTop), - button({ type: "submit", name: "filter", value: "bcs", class: filter === "bcs" ? "filter-btn active" : "filter-btn" }, i18n.audioFilterBcs || "BCS"), - button( + button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.audioFilterAll).toUpperCase()), + ...(mediaChip("recent") ? [button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.audioFilterRecent).toUpperCase())] : []), + ...(mediaChip("mine") ? [button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.audioFilterMine).toUpperCase())] : []), + ...(mediaChip("favorites") ? [button( { type: "submit", name: "filter", value: "favorites", class: filter === "favorites" ? "filter-btn active" : "filter-btn" }, - i18n.audioFilterFavorites - ), + String(i18n.audioFilterFavorites).toUpperCase() + )] : []), + button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, String(i18n.audioFilterTop).toUpperCase()), + ...(mediaChip("bcs") ? [button({ type: "submit", name: "filter", value: "bcs", class: filter === "bcs" ? "filter-btn active" : "filter-btn" }, i18n.audioFilterBcs || "BCS")] : []), button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.audioCreateButton) ) ), @@ -499,12 +403,12 @@ exports.audioTranscodeDetailView = async ({ audio, decoded = false, stegoPayload const title = i18n.audioTranscodeDetailTitle || "Transcode"; const composition = Array.isArray(audio.bcsComposition) ? audio.bcsComposition : []; const hasStego = decoded && stegoPayload && (stegoPayload.id || stegoPayload.ts || stegoPayload.msg); - const stegoDate = hasStego && Number.isFinite(stegoPayload.ts) ? moment(stegoPayload.ts).format("YYYY/MM/DD HH:mm:ss") : null; + const stegoDate = hasStego && Number.isFinite(stegoPayload.ts) ? moment(stegoPayload.ts).format("YYYY/MM/DD HH:mm") : null; return template( title, section( - div({ class: "tags-header" }, + div({ class: "tags-header module-header-line" }, h2(title), p(i18n.audioTranscodeDetailDescription || "Decode the embedded payload and the original blockchain composition map.") ), @@ -520,15 +424,14 @@ exports.audioTranscodeDetailView = async ({ audio, decoded = false, stegoPayload p({ class: "transcode-meta card-footer" }, userLink(audio.author), span({ class: "melody-meta-sep" }, " · "), - span({ class: "card-value" }, moment(audio.createdAt).format("YYYY/MM/DD HH:mm:ss")), + span({ class: "card-value" }, moment(audio.createdAt).format("YYYY/MM/DD HH:mm")), itemSize ? span({ class: "melody-meta-sep" }, " · ") : null, itemSize ? renderEcoTax(itemSize, audio.key) : null ), safeText(audio.description) ? p({ class: "melody-bcs-desc" }, audio.description) : null, renderTags(audio.tags), - br(), - form({ method: "POST", action: `/melody/transcode/${encodeURIComponent(audio.key)}`, class: "audio-transcode-run-form" }, - button({ type: "submit", class: "filter-btn" }, i18n.audioTranscodeButton || "TRANSCODE") + form({ method: "POST", action: `/melody/transcode/${encodeURIComponent(audio.key)}`, class: "audio-transcode-run-form audio-transcode-form" }, + button({ type: "submit", class: "create-button" }, i18n.audioTranscodeButton || "TRANSCODE") ), br(), decoded @@ -540,7 +443,6 @@ exports.audioTranscodeDetailView = async ({ audio, decoded = false, stegoPayload span({ class: "card-value" }, stegoDate || (i18n.audioTranscodeStegoUnknown || "—")) ), div({ class: "transcode-stego-field" }, - span({ class: "card-label" }, (i18n.audioTranscodeStegoOasisId || "By") + ": "), stegoPayload.id ? userLink(stegoPayload.id) : span({ class: "card-value" }, i18n.audioTranscodeStegoUnknown || "—") ), div({ class: "transcode-stego-field transcode-stego-msg" }, diff --git a/src/views/backup_view.js b/src/views/backup_view.js new file mode 100644 index 0000000..f3e401c --- /dev/null +++ b/src/views/backup_view.js @@ -0,0 +1,197 @@ +const { div, h2, h3, p, section, button, form, a, span, br, hr, input, label, select, option, pre, img, ul, li, strong, progress } = require("../server/node_modules/hyperaxe"); +const { template, i18n } = require("./main_views"); +const moment = require("../server/node_modules/moment"); +const crypto = require("crypto"); + +const generateRandomPassword = (length = 32) => crypto.randomBytes(length).toString("hex").slice(0, length); +const fmt = (d) => moment(d).format("YYYY/MM/DD HH:mm"); +const humanBytes = (n) => { + const b = Number(n) || 0; + if (b < 1024) return `${b} B`; + if (b < 1024 * 1024) return `${(b / 1024).toFixed(1)} KB`; + if (b < 1024 * 1024 * 1024) return `${(b / (1024 * 1024)).toFixed(1)} MB`; + return `${(b / (1024 * 1024 * 1024)).toFixed(2)} GB`; +}; + +const BACKUP_TYPES = ["RECOVERY", "KEYS", "INSTANT", "FULL", "RESTORE"]; +const typeLabel = (t) => String(i18n[`backupType${t.charAt(0) + t.slice(1).toLowerCase()}`] || t).toUpperCase(); +const normalizeType = (t) => (BACKUP_TYPES.includes(String(t || "").toUpperCase()) ? String(t).toUpperCase() : "RECOVERY"); + +const renderTypeFilters = (type) => + div({ class: "filters" }, + form({ method: "GET", action: "/backup", class: "ui-toolbar ui-toolbar--filters" }, + ...BACKUP_TYPES.map(t => button({ type: "submit", name: "type", value: t, class: type === t ? "filter-btn active" : "filter-btn" }, typeLabel(t))) + ) + ); + +const renderRandomPassword = () => p({ id: "randomPassword" }, h2({ class: "generated-password" }, generateRandomPassword())); + +const renderRecovery = (kit) => kit + ? div({ class: "backup-kit" }, + p({ class: "backup-kit-line" }, strong(`${i18n.backupKitId}: `), span({ class: "backup-kit-id" }, kit.id || "—")), + p({ class: "backup-kit-line" }, strong(`${i18n.backupKitDate}: `), fmt(kit.createdAt)), + pre({ class: "backup-kit-secret" }, kit.secret), + div({ class: "backup-kit-qr" }, img({ src: "/backup/recovery-kit/qr.png", alt: i18n.backupKitQrAlt, class: "backup-kit-qr-img" })), + ul({ class: "backup-kit-steps" }, + li(i18n.backupKitStep1), + li(i18n.backupKitStep2), + li(strong(i18n.backupKitStep3)) + ), + div({ class: "doc-export-actions doc-export-left" }, + form({ method: "GET", action: "/backup/recovery-kit/pdf" }, button({ type: "submit", class: "filter-btn" }, i18n.backupKitPdfButton)) + ) + ) + : p({ class: "backup-warning" }, i18n.backupKitPageDescription); + +const renderKeysExport = () => + div({ class: "backup-section" }, + renderRandomPassword(), + p(i18n.exportDescription), + form({ action: "/backup/keys/export", method: process.env.OASIS_MOBILE === '1' ? "GET" : "POST", id: "exportForm" }, + label(i18n.exportPasswordLabel), br(), + input({ type: "password", name: "password", id: "password", required: true, placeholder: i18n.exportPasswordPlaceholder, minlength: 32 }), br(), + p({ class: "file-info" }, i18n.fileInfo), + button({ type: "submit" }, i18n.backupKeysExportButton) + ) + ); + +const renderFullBackup = (options) => { + const opt = options || {}; + const sinceValue = opt.sinceTs ? moment(opt.sinceTs).format("YYYY-MM-DD") : ""; + return div({ class: "backup-section" }, + renderRandomPassword(), + form({ method: process.env.OASIS_MOBILE === '1' ? "GET" : "POST", action: "/backup/export" }, + label(i18n.backupScopeLabel), br(), + select({ name: "scope" }, + option({ value: "all", ...(opt.scope !== "mine" ? { selected: true } : {}) }, i18n.backupScopeAll), + option({ value: "mine", ...(opt.scope === "mine" ? { selected: true } : {}) }, i18n.backupScopeMine) + ), br(), br(), + label(i18n.backupSinceLabel), br(), + input({ type: "date", name: "since", value: sinceValue }), br(), br(), + label(i18n.exportPasswordLabel), br(), + input({ type: "password", name: "password", required: true, placeholder: i18n.exportPasswordPlaceholder, minlength: 32 }), br(), + p({ class: "file-info" }, i18n.backupFileInfo), + button({ type: "submit" }, i18n.backupExportButton) + ) + ); +}; + +const restoreSummary = (r) => + `${i18n.backupRestoredMessages}: ${r.messages} · ${i18n.backupRestoredSkipped}: ${r.skipped} · ${i18n.backupRestoredBlobs}: ${r.blobs}${r.files ? ` · ${i18n.backupRestoredState}: ${r.files}` : ""}${r.forked ? ` · ${i18n.backupRestoredForked}: ${r.forked}` : ""}${r.failed ? ` · ${i18n.backupRestoredFailed}: ${r.failed}` : ""}`; + +const renderRestoreStatus = (job) => { + if (!job) return null; + if (job.running) { + const elapsed = Math.max(0, Math.round((Date.now() - Date.parse(job.startedAt)) / 1000)); + const pct = Math.max(0, Math.min(100, Number((job.progress || {}).percent) || 0)); + return div({ class: "backup-restored" }, + p({ class: "backup-warning" }, `⏳ ${i18n.backupRestoreRunning}`), + div({ class: "indexing-progress-block" }, + progress({ value: String(pct), max: "100", class: "indexing-progress" }), + p({ class: "indexing-percent" }, strong(`${pct.toFixed(1)} %`)) + ), + p(`${restoreSummary(job.progress || {})} · ${i18n.backupRestoreElapsed}: ${elapsed}s`) + ); + } + if (job.error) return div({ class: "backup-restored" }, p({ class: "backup-warning" }, `✗ ${i18n.backupRestoreFailedLine} ${job.error}`)); + const r = job.result || {}; + const mine = (r.forks || []).some(f => f.mine); + return div({ class: "backup-restored" }, + p(`✓ ${i18n.backupRestoredLine} ${restoreSummary(r)}`), + mine ? p({ class: "backup-warning" }, `⚠ ${i18n.backupRestoreForkMine}`) : null + ); +}; + +const renderRestore = (job) => + div({ class: "backup-section" }, + renderRestoreStatus(job), + job && job.running ? null : form({ method: "POST", action: "/backup/import", enctype: "multipart/form-data" }, + input({ type: "file", name: "uploadedFile", required: true, accept: ".oasisbk" }), br(), br(), + p(i18n.backupRestorePassword), + input({ type: "password", name: "importPassword", required: true, placeholder: i18n.importPasswordPlaceholder, minlength: 32 }), br(), + button({ type: "submit" }, i18n.backupRestoreButton) + ), + br(), + p(i18n.importDescription), + form({ action: "/backup/keys/import", method: "POST", enctype: "multipart/form-data" }, + input({ type: "file", name: "uploadedFile", required: true }), br(), br(), + p(i18n.passwordImport), + input({ type: "password", name: "importPassword", required: true, placeholder: i18n.importPasswordPlaceholder, minlength: 32 }), br(), + button({ type: "submit" }, i18n.backupKeysImportButton) + ) + ); + +const renderInstantBackup = () => + div({ class: "backup-form" }, + p({ class: "backup-hint" }, i18n.backupInstantDescription, strong(i18n.backupInstantRisk)), + form({ action: "/export/create", method: "POST" }, + button({ type: "submit" }, i18n.exportDataButton) + ) + ); + +exports.backupView = async ({ type = "RECOVERY", options = null, restoreJob = null, kit = null } = {}) => { + const t = normalizeType(type); + const body = t === "RECOVERY" ? renderRecovery(kit) + : t === "KEYS" ? renderKeysExport() + : t === "FULL" ? renderFullBackup(options) + : t === "INSTANT" ? renderInstantBackup() + : renderRestore(restoreJob); + const html = template( + i18n.backupTitle, + section( + div({ class: "tags-header module-header-line" }, h2(i18n.backupTitle), p(i18n.backupDescription)), + renderTypeFilters(t) + ), + section( + div({ class: "div-center backup-wrap" }, body) + ) + ); + return restoreJob && restoreJob.running ? html.replace('', '') : html; +}; + +exports.renderRestoreStatus = renderRestoreStatus; +exports.recoveryKitView = async (kit) => exports.backupView({ type: "RECOVERY", kit }); + +exports.renderRebuildReport = (report) => { + if (!report) return null; + const idx = report.indexes || {}; + const line = (ok, text) => li({ class: ok ? "backup-check-ok" : "backup-check-bad" }, `${ok ? "✓" : "✗"} ${text}`); + return div({ class: "backup-verification" }, + p({ class: "backup-hint" }, `${fmt(report.checkedAt)} · ${report.tookMs} ms · ${i18n.verificationTotalMessages}: ${report.totalMessages}`), + ul( + report.ok + ? line(true, `${i18n.indexesRebuilt}`) + : line(false, `${i18n.indexesRebuildFailed}: ${report.error || ''}`), + li({ class: "backup-check-ok" }, `· ${i18n.indexesFiles}: ${idx.files || 0} (${humanBytes(idx.bytes || 0)})`) + ) + ); +}; + +exports.renderVerificationReport = (report) => { + if (!report) return null; + const feed = report.feed || {}; + const blobs = report.blobs || {}; + const line = (ok, text) => li({ class: ok ? "backup-check-ok" : "backup-check-bad" }, `${ok ? "✓" : "✗"} ${text}`); + return div({ class: "backup-verification" }, + p({ class: "backup-hint" }, `${fmt(report.checkedAt)} · ${report.tookMs} ms · ${i18n.verificationTotalMessages}: ${report.totalMessages}`), + ul( + line(feed.ok, `${i18n.verificationFeed}: ${feed.messages} ${i18n.verificationMessages}, ${i18n.verificationLastSequence} ${feed.lastSequence}`), + feed.gaps && feed.gaps.length ? li({ class: "backup-check-bad" }, `✗ ${i18n.verificationGaps}: ${feed.gaps.map(g => `${g.expected}→${g.found}`).join(", ")}`) : null, + feed.brokenLinks && feed.brokenLinks.length ? li({ class: "backup-check-bad" }, `✗ ${i18n.verificationBrokenLinks}: ${feed.brokenLinks.join(", ")}`) : null, + feed.badHashes && feed.badHashes.length ? li({ class: "backup-check-bad" }, `✗ ${i18n.verificationBadHashes}: ${feed.badHashes.join(", ")}`) : null, + feed.badSignatures && feed.badSignatures.length ? li({ class: "backup-check-bad" }, `✗ ${i18n.verificationBadSignatures}: ${feed.badSignatures.join(", ")}`) : null, + feed.duplicates && feed.duplicates.length ? li({ class: "backup-check-bad" }, `✗ ${i18n.verificationDuplicates}: ${feed.duplicates.join(", ")}`) : null, + line(!report.forkCount, `${i18n.verificationForks}: ${report.forkCount}`), + ...(report.forks || []).map(f => li({ class: "backup-check-bad backup-fork" }, `${f.mine ? "⚠ " : ""}${f.author} · ${f.points.map(pt => `#${pt.sequence} (${pt.keys.length})`).join(", ")}`)), + blobs.listed + ? line(!blobs.missingOwn, `${i18n.verificationBlobs}: ${blobs.present} ${i18n.verificationBlobsPresent}, ${blobs.referenced} ${i18n.verificationBlobsReferenced}, ${blobs.orphan} ${i18n.verificationBlobsOrphan} (${humanBytes(blobs.orphanBytes)})`) + : li(`· ${i18n.verificationBlobsUnavailable}`), + blobs.listed && blobs.missingOwn + ? li({ class: "backup-check-bad" }, `✗ ${i18n.verificationBlobsMissingOwn}: ${blobs.missingOwn}`) + : null, + blobs.listed && (blobs.missing - (blobs.missingOwn || 0)) > 0 + ? li({ class: "backup-check-warn" }, `⚠ ${i18n.verificationBlobsMissingOthers}: ${blobs.missing - (blobs.missingOwn || 0)}`) + : null + ) + ); +}; diff --git a/src/views/banking_views.js b/src/views/banking_views.js index 5928d3e..0db52a8 100644 --- a/src/views/banking_views.js +++ b/src/views/banking_views.js @@ -1,9 +1,11 @@ -const { div, h2, h3, p, section, button, form, a, input, span, pre, table, thead, tbody, tr, td, th, br, strong, label } = require("../server/node_modules/hyperaxe"); -const { template, i18n, userLink, formatCarbon } = require("../views/main_views"); +const { div, h2, h3, p, section, button, form, a, input, span, pre, table, thead, tbody, tr, td, th, br, strong, label, ul, li } = require("../server/node_modules/hyperaxe"); +const RAW_HTML = Symbol.for('oasis.rawHtml'); +const { template, i18n, userLink, formatCarbon, renderStateChip, renderModuleStats, renderWalletChip, paged, renderIcon } = require("../views/main_views"); const moment = require("../server/node_modules/moment"); const FILTER_LABELS = { overview: i18n.bankOverview, + ubi: i18n.bankUbiTab, exchange: i18n.bankExchange, taxes: i18n.bankTaxes || "Taxes", mine: i18n.mine, @@ -63,79 +65,254 @@ const escAttr = (s) => String(s) .replace(/"/g, '"') .replace(/'/g, '''); -const buildEcoValueChartSvg = (history, labels) => { - const arr = Array.isArray(history) ? history.slice(-120) : []; - const W = 720, H = 320; - const padL = 56, padR = 16, padT = 16, padB = 70; +const CHART_LINE_CLASSES = ["value", "supply", "inflation"]; +const legendLayout = (labels, x0, maxX) => { + const rows = [[]]; + let x = x0; + for (const label of labels) { + const width = 18 + String(label).length * 6.6 + 24; + if (x + width > maxX && rows[rows.length - 1].length) { rows.push([]); x = x0; } + rows[rows.length - 1].push({ label, x }); + x += width; + } + return rows; +}; + +const axisDecimals = (max, fallback) => { + const m = Math.abs(Number(max) || 0); + if (m >= 10000) return 0; + if (m >= 100) return 2; + return fallback; +}; + +const buildSeriesChartSvg = ({ series = [], xLabels = [], empty = "", yDecimals = 4, yDomain = null }) => { + const valid = series.filter(s => Array.isArray(s.points) && s.points.length >= 2); + const W = 720; + const padR = 16, padT = 16; + const first = valid[0] ? valid[0].points.map(v => Number(v) || 0) : [0]; + const yMax = yDomain ? Number(yDomain.max) : Math.max(...first); + const yMin = yDomain ? Number(yDomain.min) : Math.min(...first); + const decimals = axisDecimals(Math.max(Math.abs(yMax), Math.abs(yMin)), yDecimals); + const widestLabel = Math.max(...[yMax, yMin].map(v => v.toFixed(decimals).length)); + const padL = Math.max(56, Math.round(widestLabel * 6.2) + 14); + const legendRows = legendLayout(valid.map(s => s.label || ""), padL, W - padR); + const padB = 54 + 16 * Math.max(1, legendRows.length); + const H = 250 + padB; const plotW = W - padL - padR; const plotH = H - padT - padB; - if (arr.length < 2) { + if (!valid.length) { return `` + `` - + `${escAttr(labels.empty || 'Not enough samples yet')}` + + `${escAttr(empty || 'Not enough samples yet')}` + ``; } - const values = arr.map(s => Number(s.ecoValue || 0)); - const supplies = arr.map(s => Number(s.currentSupply || 0)); - const inflations = arr.map(s => Number(s.inflationFactor || 0)); - const minV = Math.min(...values); - const maxV = Math.max(...values); - const rangeV = maxV - minV || 1; - const minS = Math.min(...supplies); - const maxS = Math.max(...supplies); - const rangeS = maxS - minS || 1; - const minI = Math.min(...inflations); - const maxI = Math.max(...inflations); - const rangeI = maxI - minI || 1; - const stepX = arr.length > 1 ? plotW / (arr.length - 1) : plotW; - const xy = (i, v, minR, rangeR) => { - const x = padL + i * stepX; - const y = padT + plotH - ((v - minR) / rangeR) * plotH; - return `${x.toFixed(2)},${y.toFixed(2)}`; - }; - const pointsValue = values.map((v, i) => xy(i, v, minV, rangeV)).join(' '); - const pointsSupply = supplies.map((v, i) => xy(i, v, minS, rangeS)).join(' '); - const pointsInfl = inflations.map((v, i) => xy(i, v, minI, rangeI)).join(' '); - const tsStart = moment(arr[0].ts).format('YYYY-MM-DD HH:mm'); - const tsEnd = moment(arr[arr.length - 1].ts).format('YYYY-MM-DD HH:mm'); - const tsMid = moment(arr[Math.floor(arr.length / 2)].ts).format('YYYY-MM-DD HH:mm'); + const lines = valid.map((s, idx) => { + const vals = s.points.map(v => Number(v) || 0); + const min = yDomain ? yMin : Math.min(...vals); + const max = yDomain ? yMax : Math.max(...vals); + const range = max - min || 1; + const stepX = plotW / (vals.length - 1); + const pts = vals.map((v, i) => `${(padL + i * stepX).toFixed(2)},${(padT + plotH - ((v - min) / range) * plotH).toFixed(2)}`).join(" "); + return { cls: CHART_LINE_CLASSES[idx % CHART_LINE_CLASSES.length], label: s.label || "", pts, min, max, range }; + }); const yTicks = 4; const grid = []; for (let i = 0; i <= yTicks; i++) { const y = padT + (plotH / yTicks) * i; grid.push(``); - const val = maxV - (rangeV / yTicks) * i; - grid.push(`${val.toFixed(4)}`); + const val = lines[0].max - (lines[0].range / yTicks) * i; + grid.push(`${val.toFixed(decimals)}`); } const xLabelY = padT + plotH + 16; - const xLabels = `${escAttr(tsStart)}` - + `${escAttr(tsMid)}` - + `${escAttr(tsEnd)}`; - const legendY = padT + plotH + 44; - const legendBaseX = padL; - const legend = `` - + `` - + `${escAttr(labels.value || 'Value')}` - + `` - + `${escAttr(labels.supply || 'Supply')}` - + `` - + `${escAttr(labels.inflation || 'Inflation')}` - + ``; + const [xs = "", xm = "", xe = ""] = xLabels; + const xLabelsSvg = `${escAttr(xs)}` + + `${escAttr(xm)}` + + `${escAttr(xe)}`; + const legendY0 = padT + plotH + 44; + const legend = `` + legendRows.map((row, r) => row.map((item, i) => { + const idx = legendRows.slice(0, r).reduce((acc, rr) => acc + rr.length, 0) + i; + const l = lines[idx]; + const y = legendY0 + r * 16; + return `` + + `${escAttr(l.label)}`; + }).join("")).join("") + ``; return `` + `` - + grid.join('') - + `` - + `` - + `` - + xLabels + + grid.join("") + + lines.slice().reverse().map(l => ``).join("") + + xLabelsSvg + legend + ``; }; -const renderExchange = (ex, history, taxStats) => { +const tsLabels = (arr, key) => { + if (!arr.length) return ["", "", ""]; + const f = (x) => moment(x[key]).format("YYYY-MM-DD HH:mm"); + return [f(arr[0]), f(arr[Math.floor(arr.length / 2)]), f(arr[arr.length - 1])]; +}; + +const buildEcoValueChartSvg = (history, labels) => { + const arr = Array.isArray(history) ? history.slice(-120) : []; + return buildSeriesChartSvg({ + series: [ + { label: labels.value || "Value", points: arr.map(s => Number(s.ecoValue || 0)) }, + { label: labels.supply || "Supply", points: arr.map(s => Number(s.currentSupply || 0)) }, + { label: labels.inflation || "Inflation", points: arr.map(s => Number(s.inflationFactor || 0)) } + ], + xLabels: tsLabels(arr, "ts"), + empty: labels.empty + }); +}; + +const renderChartBlock = (id, title, svg) => + div({ class: "bank-eco-chart-block" }, + h2({ class: "bank-eco-chart-title" }, title), + a({ href: `#${id}`, id: `${id}-src`, class: "bank-eco-chart-zoom-link", title: i18n.bankChartZoomHint }, + div({ class: "bank-eco-chart-canvas", [RAW_HTML]: svg }) + ), + div({ id, class: "lightbox bank-eco-chart-lightbox" }, + a({ href: `#${id}-src`, class: "lightbox-close" }, "\u00d7"), + div({ class: "bank-eco-chart-canvas-zoom", [RAW_HTML]: svg }) + ) + ); + +const renderRangedChart = (id, title, filterName, range, hasAnyData, svg) => { + if (!hasAnyData) return null; + return div({ class: "bank-eco-chart-block", id: `${id}-block` }, + h2({ class: "bank-eco-chart-title" }, title), + renderRangeTabs(filterName, range, `${id}-block`), + svg ? a({ href: `#${id}`, id: `${id}-src`, class: "bank-eco-chart-zoom-link", title: i18n.bankChartZoomHint }, div({ class: "bank-eco-chart-canvas", [RAW_HTML]: svg })) : p({ class: "bank-chart-empty-note" }, i18n.bankChartNoDataYet), + svg ? div({ id, class: "lightbox bank-eco-chart-lightbox" }, a({ href: `#${id}-src`, class: "lightbox-close" }, "\u00d7"), div({ class: "bank-eco-chart-canvas-zoom", [RAW_HTML]: svg })) : null + ); +}; + +const bucketLabel = (bucket) => bucket === "month" ? i18n.bankChartPerMonth : bucket === "day" ? i18n.bankChartPerDay : i18n.bankChartPerHour; +const bucketXLabels = (pts, bucket) => { + const fmt = (x) => bucket === "month" ? moment(x.ts).format("YYYY-MM") : bucket === "day" ? moment(x.ts).format("YYYY-MM-DD") : moment(x.ts).format("YYYY-MM-DD HH:mm"); + return pts.length ? [fmt(pts[0]), fmt(pts[Math.floor(pts.length / 2)]), fmt(pts[pts.length - 1])] : ["", "", ""]; +}; + +const renderBucketChart = (id, title, filterName, data, field, label, decimals = 2) => { + const pts = Array.isArray(data && data.points) ? data.points : []; + const svg = pts.length >= 2 ? buildSeriesChartSvg({ + series: [{ label: `${label} ${bucketLabel(data.bucket)}`, points: pts.map(p => Number(p[field]) || 0) }], + xLabels: bucketXLabels(pts, data.bucket), + empty: i18n.bankExchangeChartEmpty, yDecimals: decimals + }) : null; + return renderRangedChart(id, title, filterName, (data && data.range) || "today", !!(data && data.hasAnyData), svg); +}; + +const renderSupplyChart = (history, range, hasAnyData, totalSupply = 0) => { + const arr = Array.isArray(history) ? history.slice(-240) : []; + const total = Number(totalSupply) || 0; + const svg = arr.length >= 2 ? buildSeriesChartSvg({ + series: [{ label: i18n.bankCurrentSupply, points: arr.map(s => Number(s.currentSupply) || 0) }], + xLabels: tsLabels(arr, "ts"), + empty: i18n.bankExchangeChartEmpty, yDecimals: 0 + }) : null; + return renderRangedChart("eco-supply-chart", i18n.bankChartSupplyTitle, "exchange", range, !!hasAnyData, svg); +}; + +const renderInflationChart = (history, range, hasAnyData) => { + const arr = Array.isArray(history) ? history.slice(-240) : []; + const svg = arr.length >= 2 ? buildSeriesChartSvg({ + series: [{ label: i18n.bankChartInflationSeries, points: arr.map(s => Number(s.inflationFactor) || 0) }], + xLabels: tsLabels(arr, "ts"), + empty: i18n.bankExchangeChartEmpty, yDecimals: 2 + }) : null; + return renderRangedChart("eco-inflation-chart", i18n.bankChartInflationTitle, "exchange", range, !!hasAnyData, svg); +}; + +const renderUbiCharts = (charts) => { + const pool = Array.isArray(charts && charts.pool) ? charts.pool : []; + const empty = i18n.bankExchangeChartEmpty; + const poolSvg = pool.length >= 2 ? buildSeriesChartSvg({ + series: [{ label: i18n.bankChartPool, points: pool.map(x => x.balance) }], + xLabels: tsLabels(pool, "ts"), + empty, yDecimals: 2 + }) : null; + const out = [ + renderBucketChart("ubi-chart-network", i18n.bankChartNetworkUbiTitle, "ubi", charts && charts.payments, "distributed", i18n.bankChartDistributed, 6), + renderRangedChart("ubi-chart-pool", i18n.bankChartPoolTitle, "ubi", (charts && charts.poolRange) || "today", !!(charts && charts.hasPool), poolSvg), + renderBucketChart("ubi-chart-rebalance", i18n.bankChartRebalanceTitle, "ubi", charts && charts.rebalances, "rebalanced", i18n.bankChartRebalanced, 6) + ].filter(Boolean); + return out.length ? div(...out) : null; +}; + +const RANGES = ["today", "week", "month", "year", "5y", "all"]; +const rangeLabel = (r) => ({ today: i18n.bankRangeToday, week: i18n.bankRangeWeek, month: i18n.bankRangeMonth, year: i18n.bankRangeYear, "5y": i18n.bankRangeFiveYears, all: i18n.bankRangeAll })[r] || r; +const renderRangeTabs = (filter, current, anchor) => + div({ class: "bank-range-tabs" }, + ...RANGES.map(r => a({ href: `/banking?filter=${filter}&range=${r}#${anchor}`, class: r === current ? "activity-chip active" : "activity-chip" }, String(rangeLabel(r)).toUpperCase())) + ); + +const renderValueChart = (history, range, hasAnyData) => { + const arr = Array.isArray(history) ? history.slice(-240) : []; + if (!hasAnyData) return null; + const hasTime = arr.some(s => Number(s.ecoTimeMs) > 0); + const hasInflation = arr.some(s => Number(s.inflationFactor) > 0); + const svg = arr.length >= 2 ? buildSeriesChartSvg({ + series: [ + { label: i18n.bankExchangeChartValue || "Value (ECO/h)", points: arr.map(s => Number(s.ecoValue || 0)) }, + ...(hasTime ? [{ label: i18n.bankChartValueHours, points: arr.map(s => Number(s.ecoTimeMs || 0) / 3600000) }] : []), + ...(hasInflation ? [{ label: i18n.bankExchangeChartInflation || "Inflation %", points: arr.map(s => Number(s.inflationFactor || 0)) }] : []) + ], + xLabels: tsLabels(arr, "ts"), + empty: i18n.bankExchangeChartEmpty + }) : null; + return renderRangedChart("eco-value-chart", i18n.bankChartValueTitle, "exchange", range, !!hasAnyData, svg); +}; + +const renderWealthChart = (wealth, range) => { + const pts = Array.isArray(wealth && wealth.points) ? wealth.points : []; + if (!(wealth && wealth.hasAnyData)) return null; + if (pts.length < 2) return div({ class: "bank-eco-chart-block", id: "wealth-chart-block" }, + h2({ class: "bank-eco-chart-title" }, i18n.bankChartWealthTitle), + renderRangeTabs("exchange", range, "wealth-chart-block"), + p({ class: "bank-chart-empty-note" }, i18n.bankChartNoDataYet) + ); + const fmtBucket = (x) => wealth.bucket === "month" ? moment(x.ts).format("YYYY-MM") : wealth.bucket === "day" ? moment(x.ts).format("YYYY-MM-DD") : moment(x.ts).format("YYYY-MM-DD HH:mm"); + const per = wealth.bucket === "month" ? i18n.bankChartPerMonth : wealth.bucket === "day" ? i18n.bankChartPerDay : i18n.bankChartPerHour; + const svg = buildSeriesChartSvg({ + series: [ + { label: `${i18n.bankChartDistributed} ${per}`, points: pts.map(p => p.distributed) }, + { label: `${i18n.bankChartTaxes} ${per}`, points: pts.map(p => p.taxes) } + ], + xLabels: [fmtBucket(pts[0]), fmtBucket(pts[Math.floor(pts.length / 2)]), fmtBucket(pts[pts.length - 1])], + empty: i18n.bankExchangeChartEmpty, yDecimals: 2 + }); + const enough = pts.length >= 2; + return div({ class: "bank-eco-chart-block", id: "wealth-chart-block" }, + h2({ class: "bank-eco-chart-title" }, i18n.bankChartWealthTitle), + renderRangeTabs("exchange", range, "wealth-chart-block"), + enough ? a({ href: "#wealth-chart", id: "wealth-chart-src", class: "bank-eco-chart-zoom-link", title: i18n.bankChartZoomHint }, div({ class: "bank-eco-chart-canvas", [RAW_HTML]: svg })) : p({ class: "bank-chart-empty-note" }, i18n.bankChartNoDataRange), + enough ? div({ id: "wealth-chart", class: "lightbox bank-eco-chart-lightbox" }, a({ href: "#wealth-chart-src", class: "lightbox-close" }, "\u00d7"), div({ class: "bank-eco-chart-canvas-zoom", [RAW_HTML]: svg })) : null + ); +}; + +const renderFundsChart = (history) => { + const arr = Array.isArray(history && history.points) ? history.points : []; + const svg = arr.length >= 2 ? buildSeriesChartSvg({ + series: [{ label: "ECO", points: arr.map(x => x.balance) }], + xLabels: tsLabels(arr, "ts"), + empty: i18n.bankExchangeChartEmpty, yDecimals: 2 + }) : null; + return renderRangedChart("funds-chart", i18n.bankChartFundsTitle, "overview", (history && history.range) || "today", !!(history && history.hasAny), svg); +}; + +const renderKarmaChart = (history) => { + const arr = Array.isArray(history && history.points) ? history.points : []; + const svg = arr.length >= 2 ? buildSeriesChartSvg({ + series: [{ label: i18n.bankChartKarma, points: arr.map(x => x.score) }], + xLabels: tsLabels(arr, "ts"), + empty: i18n.bankExchangeChartEmpty, yDecimals: 0 + }) : null; + return renderRangedChart("karma-chart", i18n.bankChartKarmaTitle, "overview", (history && history.range) || "today", !!(history && history.hasAny), svg); +}; + +const renderExchange = (ex, history, taxStats, wealth, range, valueRange = range, valueHasAnyData = false) => { if (!ex) return div(p(i18n.bankExchangeNoData)); const syncStatus = ex.isSynced ? i18n.bankingSyncStatusSynced : i18n.bankingSyncStatusOutdated; - const syncStatusClass = ex.isSynced ? 'synced' : 'outdated'; const ecoTimeLabel = ex.isSynced ? fmtEcoTime(ex.ecoTimeMs) : fmtEcoTime(0); const chartLabels = { value: i18n.bankExchangeChartValue || 'Value (ECO/h)', @@ -154,24 +331,30 @@ const renderExchange = (ex, history, taxStats) => { table({ class: "bank-info-table" }, tbody( kvRow(i18n.bankingSyncStatus, - span({ class: syncStatusClass }, syncStatus) + span({ class: ex.isSynced ? "ubi-tick-ok" : "ubi-tick-bad", title: syncStatus }, ex.isSynced ? "✓" : "✗") ), - kvRow(i18n.bankExchangeCurrentValue, `${fmtIndex(ex.ecoValue)} ECO`), - kvRow(i18n.bankCurrentSupply, `${Number(ex.currentSupply || 0).toFixed(6)} ECO`), - kvRow(i18n.bankTotalSupply, `${Number(ex.totalSupply || 0).toFixed(6)} ECO`), - kvRow(i18n.bankEcoinHours, ecoTimeLabel), - kvRow(i18n.bankInflation, `${ex.inflationFactor.toFixed(2)}%`), - kvRow(i18n.bankInflationMonthly, `${Number(ex.inflationMonthly || 0).toFixed(2)}%`), - ...taxRows + kvRow(strong(i18n.bankTotalSupply), strong(`${Number(ex.totalSupply || 0).toFixed(6)} ECO`)), + ...(ex.isSynced ? [kvRow(strong(i18n.bankCurrentSupply), strong(`${Number(ex.currentSupply || 0).toFixed(6)} ECO`))] : []), + ...(ex.holdingSupply != null ? [kvRow(strong(i18n.bankHoldingSupply), strong(`${Number(ex.holdingSupply).toFixed(6)} ECO`))] : []), + kvRow(strong(i18n.bankPubsSupply), strong(`${Number(ex.pubsSupply || 0).toFixed(6)} ECO`)), + ...(ex.isSynced ? [ + kvRow(strong(i18n.bankInflation), strong(`${Number(ex.inflationFactor || 0).toFixed(2)}%`)), + kvRow(i18n.bankInflationMonthly, `${Number(ex.inflationMonthly || 0).toFixed(2)}%`) + ] : []), + kvRow(strong(i18n.bankTotalUbiDistributed), strong(`${Number((wealth && wealth.totals && wealth.totals.distributed) || 0).toFixed(6)} ECO`)), + kvRow(strong(i18n.bankTotalTaxesWithheld), strong(`${Number((wealth && wealth.totals && wealth.totals.taxes) || 0).toFixed(6)} ECO`)), + ...taxRows, + ...(ex.isSynced ? [ + kvRow(strong(i18n.bankExchangeCurrentValue), strong(`${fmtIndex(ex.ecoValue)} ECO`)), + kvRow(strong(i18n.bankEcoinHours), strong(ecoTimeLabel)) + ] : []) ) ) ), - hasEnoughSamples - ? div({ class: "bank-eco-chart-block" }, - h2({ class: "bank-eco-chart-title" }, i18n.bankExchangeChartTitle || 'ECOin value over time'), - div({ class: "bank-eco-chart-canvas", innerHTML: buildEcoValueChartSvg(history, chartLabels) }) - ) - : null + ex.isSynced ? renderValueChart(history, valueRange, !!valueHasAnyData) : null, + ex.isSynced ? renderWealthChart(wealth, (wealth && wealth.range) || range) : null, + renderSupplyChart(history, range, hasEnoughSamples, ex.totalSupply), + renderInflationChart(history, range, hasEnoughSamples) ); }; @@ -248,7 +431,7 @@ const renderTaxes = (data, lookup) => { (i18n.bankTaxesUserNoteOutro || ', and their ongoing contribution may be required by networking consensus.') ), p({ class: "bank-taxes-user-note" }, i18n.bankTaxesUserNoteChips || 'The ECO Tax chip shown next to each item changes color (green / yellow / red) depending on the item size relative to the largest block ever seen on the network, softened by the number of active inhabitants — a larger solar-punk network distributes the load and pushes more items into the green band.'), - p({ class: "bank-taxes-user-note" }, i18n.bankTaxesUserNoteChipsClick || 'Click any chip to inspect its block in the blockexplorer.'), + process.env.OASIS_MOBILE === '1' ? null : p({ class: "bank-taxes-user-note" }, i18n.bankTaxesUserNoteChipsClick || 'Click any chip to inspect its block in the blockexplorer.'), (() => { const firstBlock = data.firstBlock || null; const sampleSize = Number(data.firstBlockSize || 0); @@ -258,17 +441,17 @@ const renderTaxes = (data, lookup) => { const sampleValue = sampleSize > 0 ? formatCarbon(sampleSize) : '— CO₂'; const label = i18n.ecoTaxLabel || 'ECO Tax'; return p({ class: "bank-taxes-example-chips" }, - a({ href: sampleHref, class: 'eco-tax-chip eco-tax-chip-low', title: label + ' · low' }, + (process.env.OASIS_MOBILE === '1' ? span : a)({ ...(process.env.OASIS_MOBILE === '1' ? {} : { href: sampleHref }), class: 'eco-tax-chip eco-tax-chip-low', title: label + ' · low' }, span({ class: 'eco-tax-chip-label' }, label + ': '), span({ class: 'eco-tax-chip-value' }, sampleValue) ), ' ', - a({ href: sampleHref, class: 'eco-tax-chip eco-tax-chip-mid', title: label + ' · mid' }, + (process.env.OASIS_MOBILE === '1' ? span : a)({ ...(process.env.OASIS_MOBILE === '1' ? {} : { href: sampleHref }), class: 'eco-tax-chip eco-tax-chip-mid', title: label + ' · mid' }, span({ class: 'eco-tax-chip-label' }, label + ': '), span({ class: 'eco-tax-chip-value' }, sampleValue) ), ' ', - a({ href: sampleHref, class: 'eco-tax-chip eco-tax-chip-high', title: label + ' · high' }, + (process.env.OASIS_MOBILE === '1' ? span : a)({ ...(process.env.OASIS_MOBILE === '1' ? {} : { href: sampleHref }), class: 'eco-tax-chip eco-tax-chip-high', title: label + ' · high' }, span({ class: 'eco-tax-chip-label' }, label + ': '), span({ class: 'eco-tax-chip-value' }, sampleValue) ) @@ -297,7 +480,35 @@ const renderTaxes = (data, lookup) => { ); }; -const renderOverviewSummaryTable = (s, rules, userEcoinTax) => { +const renderUbiPubs = (pubs, hasValidWallet) => { + const fmt = (ts) => Number(ts) > 0 ? moment(Number(ts)).format("YYYY/MM/DD HH:mm") : "—"; + if (!pubs.length) return div({ class: "bank-summary" }, p(i18n.bankUbiPubNone)); + return div({ class: "addr-cards ubi-pub-cards" }, + ...pubs.map(p => div({ class: "addr-card ubi-pub-card" + (p.isThisNode || p.isConnected ? " own-content" : "") }, + div({ class: "addr-card-head" }, + userLink(p.pubId), + p.isThisNode || p.isConnected ? span({ class: "status you" }, p.isThisNode ? i18n.bankUbiPubThisNode : i18n.bankUbiPubConnected) : null, + hasValidWallet && p.address + ? div({ class: "content-actions" }, form({ method: "GET", action: "/banking/fund", class: "content-action-form" }, input({ type: "hidden", name: "pub", value: p.pubId }), button({ type: "submit", class: "btn-singleview btn-donate", title: i18n.bankDonateEco }, renderIcon("donate")))) + : null + ), + div({ class: "bank-summary ubi-pub-summary" }, + table({ class: "bank-info-table" }, + tbody( + kvRow(i18n.bankUbiAvailability, span({ class: p.available ? "ubi-tick-ok" : "ubi-tick-bad" }, p.available ? "✓" : "✗")), + kvRow(i18n.bankUbiPool, strong(`${Number(p.balance || 0).toFixed(6)} ECO`)), + kvRow(i18n.bankUbiLastSeen, fmt(p.timestamp)), + kvRow(i18n.bankUbiLastPayout, fmt(p.lastPayoutAt)), + kvRow(i18n.bankUbiPaidOut, strong(`${Number(p.paidOut || 0).toFixed(6)} ECO`)) + ) + ) + ) + )) + ); +}; + +const renderOverviewSummaryTable = (s, rules, userEcoinTax, isPub = false) => { + const ubiWired = !!s.pubId || isPub; const score = Number(s.userEngagementScore || 0); const pool = Number(s.pool || 0); const W = Math.max(1, Number(s.weightsSum || 1)); @@ -307,36 +518,48 @@ const renderOverviewSummaryTable = (s, rules, userEcoinTax) => { const gross = Math.max(floor, Math.min(pool * (w / W), cap)); const surplus = Math.max(0, gross - floor); const tax = Number(userEcoinTax || 0); - const future = floor + Math.max(0, surplus - tax); - const availClass = s.ubiAvailability === "OK" ? "ubi-available" : "ubi-unavailable"; - const availLabel = s.ubiAvailability === "OK" ? i18n.bankUbiAvailableOk : i18n.bankUbiAvailableNo; + const ubiOn = ubiWired && s.ubiAvailability === "OK"; + const future = ubiOn ? floor + Math.max(0, surplus - tax) : 0; return div({ class: "bank-summary" }, table({ class: "bank-info-table" }, tbody( - kvRow(i18n.bankUserBalance, `${Number(s.userBalance || 0).toFixed(6)} ECO`), - kvRow(i18n.bankUbiAvailability, span({ class: availClass }, availLabel)), - s.pubId ? kvRow(i18n.pubIdLabel, userLink(s.pubId)) : null, + kvRow(strong(i18n.bankUserBalance), strong(`${Number(s.userBalance || 0).toFixed(6)} ECO`)), + Number(s.industryNetworkTotal || 0) > 0 ? kvRow(strong(i18n.bankIndustryBalance || "Industry Production"), a({ href: '/industry' }, strong(`${Number(s.industryNetworkTotal || 0).toFixed(6)} ECO`))) : null, kvRow(i18n.bankEpoch, String(s.epochId || "-")), kvRow(i18n.bankPool, `${pool.toFixed(6)} ECO`), kvRow(i18n.bankWeightsSum, String(W.toFixed(6))), - kvRow(i18n.bankingUserEngagementScore, String(score)), - kvRow(i18n.bankOverviewYourTaxes || 'Your taxes', a({ href: '/banking?filter=taxes' }, `${tax.toFixed(6)} ECO`)), - kvRow(i18n.bankUbiThisMonth, `${future.toFixed(6)} ECO`) + kvRow(strong(i18n.bankingUserEngagementScore), strong(String(score))), + ubiWired ? kvRow(i18n.bankYourUbiMonth || 'Your UBI (this month)', `${future.toFixed(6)} ECO`) : null, + Number(s.industryBalance || 0) > 0 ? kvRow(i18n.bankYourIndustryBalance || 'Your Industry Share', a({ href: '/industry?filter=MEMBER' }, `${Number(s.industryBalance || 0).toFixed(6)} ECO`)) : null, + Number(s.schoolBalance || 0) > 0 ? kvRow(i18n.bankYourSchoolBalance || 'Your School Earnings', a({ href: '/school?filter=mine' }, `${Number(s.schoolBalance || 0).toFixed(6)} ECO`)) : null, + kvRow(i18n.bankTotalUbiDistributed, `${Number((s.wealthTotals && s.wealthTotals.distributed) || 0).toFixed(6)} ECO`), + kvRow(i18n.bankTotalTaxesWithheld, `${Number((s.wealthTotals && s.wealthTotals.taxes) || 0).toFixed(6)} ECO`), + kvRow(strong(i18n.bankOverviewYourTaxes || 'Total Taxes'), a({ href: '/banking?filter=taxes' }, strong(`${tax.toFixed(6)} ECO`))), + kvRow(strong(i18n.bankYourFundsMonth || 'Total Funds'), strong(`${((ubiWired ? future : 0) + Number(s.industryBalance || 0) + Number(s.schoolBalance || 0)).toFixed(6)} ECO`)) ) ) ); }; -const renderClaimUBIBlock = (pendingAllocation, isPub, alreadyClaimed, pubId, hasValidWallet, ubiAvailability) => { +const renderClaimUBIBlock = (pendingAllocation, isPub, alreadyClaimed, pubId, hasValidWallet, ubiAvailability, alreadyRefused = false, addressPublished = false, ecoinSynced = false) => { + if (!ecoinSynced) return ""; if (alreadyClaimed) return ""; + if (alreadyRefused) return div({ class: "bank-claim-ubi" }, div({ class: "bank-claim-card" }, p(i18n.bankUbiRefusedThisMonth))); if (!pubId && !isPub) return ""; - if (!isPub && !hasValidWallet) return ""; + if (!isPub && !(hasValidWallet && addressPublished)) return ""; if (!isPub && ubiAvailability !== "OK") return ""; - if (!pendingAllocation && !isPub) { + if (!isPub) { return div({ class: "bank-claim-ubi" }, div({ class: "bank-claim-card" }, - form({ method: "POST", action: "/banking/claim-ubi" }, - button({ type: "submit", class: "create-button bank-claim-btn" }, i18n.bankClaimUBI) + pendingAllocation ? p(`${i18n.bankUbiThisMonth}: `, span({ class: "accent" }, `${Number(pendingAllocation.amount || 0).toFixed(6)} ECO`)) : null, + pendingAllocation ? p(`${i18n.bankEpoch}: `, span(pendingAllocation.concept || "")) : null, + div({ class: "bank-claim-actions" }, + form({ method: "POST", action: "/banking/claim-ubi" }, + button({ type: "submit", class: "create-button bank-claim-btn" }, i18n.bankClaimUBI) + ), + form({ method: "POST", action: "/banking/refuse-ubi" }, + button({ type: "submit", class: "delete-btn bank-claim-btn" }, i18n.bankRefuseUBI) + ) ) ) ); @@ -355,7 +578,7 @@ const renderClaimUBIBlock = (pendingAllocation, isPub, alreadyClaimed, pubId, ha const filterAllocations = (allocs, filter, userId) => { if (filter === "mine") return allocs.filter(a => a.to === userId && (a.status === "UNCLAIMED" || a.status === "UNCONFIRMED")); - if (filter === "pending") return allocs.filter(a => a.status === "UNCLAIMED" || a.status === "UNCONFIRMED"); + if (filter === "pending") return allocs.filter(a => a.status !== "EXPIRED"); if (filter === "closed") return allocs.filter(a => a.status === "CLOSED"); if (filter === "claimed") return allocs.filter(a => a.status === "CLAIMED"); if (filter === "expired") return allocs.filter(a => a.status === "EXPIRED"); @@ -374,27 +597,23 @@ const allocationsTable = (rows = [], userId) => th(i18n.bankAllocFrom), th(i18n.bankAllocTo), th(i18n.bankAllocAmount), - th(i18n.bankAllocStatus), - th("") + th(i18n.bankAllocStatus) ) ), tbody( ...rows.map(r => tr( - td(new Date(r.createdAt).toLocaleString()), + td(moment(r.createdAt).format("YYYY/MM/DD HH:mm")), td(r.concept || ""), td(userLink(r.from)), td(userLink(r.to)), td(String(Number(r.amount || 0).toFixed(6))), - td(r.status), td( - (r.status === "UNCLAIMED" || r.status === "UNCONFIRMED") && r.to === userId - ? form({ method: "POST", action: `/banking/claim/${encodeURIComponent(r.id)}` }, - button({ type: "submit", class: "filter-btn" }, i18n.bankClaimNow) - ) - : r.status === "CLOSED" && r.txid - ? a({ href: `https://ecoin.03c8.net/blockexplorer/search?q=${encodeURIComponent(r.txid)}`, target: "_blank", class: "btn-singleview" }, i18n.bankViewTx) - : null + r.status === "CLOSED" || r.status === "CLAIMED" + ? span({ class: "ubi-tick-ok", title: r.status }, "✓") + : r.status === "UNCLAIMED" || r.status === "UNCONFIRMED" + ? span({ class: "ubi-tick-bad", title: r.status }, "✗") + : r.status ) ) ) @@ -406,16 +625,15 @@ const renderEpochList = (epochs = []) => ? div(p(i18n.bankNoEpochs)) : table( { class: "bank-epochs" }, - thead(tr(th(i18n.bankEpochId), th(i18n.bankPool), th(i18n.bankWeightsSum), th(i18n.bankRuleHash), th(""))), + thead(tr(th(i18n.bankEpochId), th(i18n.bankPool), th(i18n.bankEpochAllocations), th(""))), tbody( - ...epochs - .sort((a, b) => String(b.id).localeCompare(String(a.id))) + ...paged(epochs + .sort((a, b) => String(b.id).localeCompare(String(a.id)))) .map(e => tr( td(e.id), td(String(Number(e.pool || 0).toFixed(6))), - td(String(Number(e.weightsSum || 0).toFixed(6))), - td(e.hash || "-"), + td(String(Number(e.recipients || 0))), td( form({ method: "GET", action: `/banking/epoch/${encodeURIComponent(e.id)}` }, button({ type: "submit", class: "filter-btn" }, i18n.bankViewEpoch) @@ -432,6 +650,9 @@ const rulesBlock = (rules, taxRules) => { const tr = taxRules || {}; const fmt = (n, d = 4) => (Number.isFinite(Number(n)) ? Number(n).toFixed(d) : "—"); return div({ class: "bank-rules" }, + h2(i18n.bankRulesEligibilityTitle), + ul({ class: "bank-rules-list" }, ...[i18n.bankRulesEligibility1, i18n.bankRulesEligibility2, i18n.bankRulesEligibility3, i18n.bankRulesEligibility4, i18n.bankRulesEligibility5, i18n.bankRulesEligibility6].map(x => li(x))), + h2(i18n.bankRulesPoolTitle || "Monthly pool"), table({ class: "bank-info-table" }, tbody( @@ -512,118 +733,100 @@ const flashText = (key) => { if (key === "claimed_pending") return i18n.bankClaimedPending; if (key === "already_claimed") return i18n.bankAlreadyClaimedThisMonth; if (key === "no_pub_configured") return i18n.bankNoPubConfigured; + if (key === "no_pub_address") return i18n.bankNoPubAddress; + if (key === "refused") return i18n.bankUbiRefusedThisMonth; + if (key === "already_refused") return i18n.bankUbiRefusedThisMonth; if (key === "no_funds") return i18n.bankUbiAvailableNo; if (key === "forbidden") return i18n.bankAddressForbidden; return ""; }; -const flashBanner = (msgKey) => - !msgKey ? null : div({ class: "flash-banner" }, p(flashText(msgKey) || msgKey)); - -const addressesToolbar = (rows = [], search = "") => - div({ class: "addr-toolbar" }, - div({ class: "addr-counter accent-pill" }, - span({ class: "acc-title accent" }, i18n.bankAddressTotal + ":"), - span({ class: "acc-badge" }, String(rows.length)) - ), - form({ method: "GET", action: "/banking", class: "addr-search" }, - input({ type: "hidden", name: "filter", value: "addresses" }), - input({ type: "text", name: "q", placeholder: i18n.bankAddressSearch, value: search || "" }), - br(), - button({ type: "submit", class: "filter-btn" }, i18n.search) - ) - ); - const renderAddresses = (data, userId) => { const rows = data.addresses || []; const search = data.search || ""; + const hasContent = rows.length > 0 || !!search; return div( - data.flash ? flashBanner(data.flash) : null, - addressesToolbar(rows, search), - div({ class: "bank-addresses-stack" }, - div({ class: "addr-form-card wide" }, - h2(i18n.bankAddAddressTitle), - form({ method: "POST", action: "/banking/addresses", class: "addr-form" }, - div({ class: "form-row" }, - span({ class: "form-label accent" }, i18n.bankAddAddressUser + ":"), - input({ - class: "form-input xl", - type: "text", - name: "userId", - required: true, - pattern: "^@[A-Za-z0-9+/]+={0,2}\\.ed25519$", - placeholder: "@...=.ed25519", - id: "addr-user-id" - }) - ), - div({ class: "form-row" }, - span({ class: "form-label accent" }, i18n.bankAddAddressAddress + ":"), - input({ - class: "form-input xl", - type: "text", - name: "address", - required: true, - pattern: "^[A-Za-z0-9]{20,64}$", - placeholder: "ETQ17sBv8QFoiCPGKDQzNcDJeXmB2317HX" - }) - ), - div({ class: "form-actions" }, - button({ type: "submit", class: "filter-btn" }, i18n.bankAddAddressSave) + div({ class: "bank-summary" }, + h2(i18n.bankAddAddressTitle), + form({ method: "POST", action: "/banking/addresses", class: "bank-form" }, + label({ for: "addr-label" }, i18n.bankAddAddressLabel), br(), + input({ type: "text", id: "addr-label", name: "label", maxlength: "80", placeholder: i18n.bankAddAddressLabelPlaceholder }), br(), + label({ for: "addr-user-id" }, i18n.bankAddAddressUserOptional), br(), + input({ type: "text", id: "addr-user-id", name: "userId", pattern: "^@[A-Za-z0-9+/]+={0,2}\\.ed25519$", placeholder: "@...=.ed25519" }), br(), + label({ for: "addr-address" }, i18n.bankAddAddressAddress), br(), + input({ type: "text", id: "addr-address", name: "address", required: true, pattern: "^[A-Za-z0-9]{20,64}$", placeholder: "ETQ17sBv8QFoiCPGKDQzNcDJeXmB2317HX" }), br(), + button({ type: "submit", class: "create-button" }, i18n.bankAddAddressSave) + ) + ), + hasContent + ? form({ method: "GET", action: "/banking", class: "filter-box bank-addr-search" }, + input({ type: "hidden", name: "filter", value: "addresses" }), + input({ type: "text", name: "q", placeholder: i18n.bankAddressSearch, value: search || "", class: "filter-box__input" }), + div({ class: "filter-box__controls" }, button({ type: "submit", class: "filter-box__button" }, i18n.searchButton)) + ) + : null, + rows.length === 0 + ? div({ class: "no-content-box" }, p(i18n.bankNoAddresses)) + : div({ class: "bank-summary" }, + renderModuleStats(rows.length, [ + { label: i18n.bankLocal, count: rows.filter(r => r.source !== "ssb").length }, + { label: i18n.bankFromOasis, count: rows.filter(r => r.source === "ssb").length } + ]), + table({ class: "bank-addresses" }, + thead(tr(th(i18n.bankAddAddressLabel.replace(/\s*\(.*\)\s*$/, "")), th(i18n.bankAddAddressUser), th(i18n.bankAddress), th(i18n.bankAddressSource), th(""))), + tbody( + ...paged(rows).map(r => tr( + td(r.label || (r.id ? "" : i18n.bankAddressUnnamed)), + td(r.id ? userLink(r.id) : ""), + td(strong({ class: "bank-address-code" }, r.address)), + td(r.source === "ssb" ? i18n.bankFromOasis : i18n.bankLocal), + td(r.source === "ssb" && String(r.id) !== String(userId) + ? "" + : form({ method: "POST", action: "/banking/addresses/delete", class: "addr-del" }, + input({ type: "hidden", name: "userId", value: r.id || "" }), + input({ type: "hidden", name: "source", value: r.source || "local" }), + input({ type: "hidden", name: "entryId", value: r.entryId || "" }), + button({ type: "submit", class: "delete-btn" }, i18n.bankAddressDelete) + )) + )) + ) ) ) - ), - div({ class: "addr-list-card" }, - rows.length === 0 - ? div(p(i18n.bankNoAddresses)) - : table( - { class: "bank-addresses" }, - thead( - tr( - th(i18n.bankUser), - th(i18n.bankAddress), - th(i18n.bankAddressSource), - th(i18n.bankAddressActions) - ) - ), - tbody( - ...rows.map(r => - tr( - td(userLink(r.id)), - td(r.address), - td(r.source === "local" ? i18n.bankLocal : i18n.bankFromOasis), - td( - div({ class: "row-actions" }, - form({ method: "POST", action: "/banking/addresses/delete", class: "addr-del" }, - input({ type: "hidden", name: "userId", value: r.id }), - input({ type: "hidden", name: "source", value: r.source || "local" }), - button({ type: "submit", class: "delete-btn", onclick: `return confirm(${JSON.stringify(i18n.bankAddressDeleteConfirm)})` }, i18n.bankAddressDelete) - ) - ) - ) - ) - ) - ) - ) - ) - ) ); }; +const bankingFilters = (data, filter, userId) => { + const allocs = data.allocations || []; + const subsets = ["mine", "pending", "closed", "claimed", "expired"].filter(f => filterAllocations(allocs, f, userId).length > 0 || filter === f); + return [ + "overview", "exchange", "ubi", "taxes", + ...subsets, + ...((data.epochs || []).length || filter === "epochs" || String(filter || "").startsWith("epoch:") ? ["epochs"] : []), + "addresses", "rules" + ]; +}; + const renderBankingView = (data, filter, userId, isPub) => template( i18n.banking, section( - div({ class: "tags-header" }, h2(i18n.banking), p(i18n.bankingDescription)), + div({ class: "tags-header module-header-line" }, h2(i18n.banking), p(i18n.bankingDescription), renderWalletChip()), data.flash ? div({ class: "flash-banner" }, p(flashText(data.flash) || data.flash)) : null, - generateFilterButtons(["overview","exchange","taxes","mine","pending","closed","claimed","expired","epochs","rules","addresses"], filter, "/banking"), + generateFilterButtons(bankingFilters(data, filter, userId), filter, "/banking"), filter === "overview" ? div( - renderOverviewSummaryTable(data.summary || {}, data.rules, data.userTotalTax || data.userEcoinTax), - renderClaimUBIBlock(data.pendingUBI || null, isPub, data.alreadyClaimed, (data.summary || {}).pubId, (data.summary || {}).hasValidWallet, (data.summary || {}).ubiAvailability), - allocationsTable((data.allocations || []).sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)), userId) + renderOverviewSummaryTable(data.summary || {}, data.rules, data.userTotalTax || data.userEcoinTax, isPub), + renderKarmaChart(data.karmaHistory), + renderFundsChart(data.fundsHistory) + ) + : filter === "ubi" + ? div( + renderClaimUBIBlock(data.pendingUBI || null, isPub, data.alreadyClaimed, (data.summary || {}).pubId, (data.summary || {}).hasValidWallet, (data.summary || {}).ubiAvailability, (data.summary || {}).alreadyRefused, (data.summary || {}).addressPublished, !!(data.exchange && data.exchange.isSynced)), + renderUbiPubs(data.ubiPubs || [], !!(data.summary || {}).hasValidWallet), + renderUbiCharts({ ...(data.ubiCharts || {}), payments: data.ubiPayments, rebalances: data.ubiRebalances }) ) : filter === "exchange" - ? renderExchange(data.exchange, data.exchangeHistory, data.taxStats) + ? renderExchange(data.exchange, data.exchangeHistory, data.taxStats, data.wealth, data.range, data.valueRange || data.range, !!data.valueHasAnyData) : filter === "taxes" ? renderTaxes(data, data.lookup || null) : filter === "epochs" @@ -633,7 +836,7 @@ const renderBankingView = (data, filter, userId, isPub) => : filter === "addresses" ? renderAddresses(data, userId) : allocationsTable( - filterAllocations((data.allocations || []).sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)), filter, userId), + paged(filterAllocations((data.allocations || []).sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)), filter, userId)), userId ) ) @@ -655,7 +858,6 @@ const renderSingleAllocationView = (alloc, userId) => { kvRow(i18n.bankAllocAmount, `${Number(alloc.amount || 0).toFixed(6)} ECO`), kvRow(i18n.bankAllocStatus, alloc.status || "-"), kvRow(i18n.bankAllocDate, alloc.createdAt ? fmtDate(alloc.createdAt) : "-"), - alloc.txid ? kvRow("TxID", a({ href: `https://ecoin.03c8.net/blockexplorer/search?q=${encodeURIComponent(alloc.txid)}`, target: "_blank" }, alloc.txid)) : null ) ) ), @@ -669,26 +871,36 @@ const renderSingleAllocationView = (alloc, userId) => { ); }; -const renderEpochView = (epoch, allocations) => { - if (!epoch) return template(i18n.banking, section(div(p(i18n.bankNoEpochs)))); +const bankingHeader = () => + div({ class: "tags-header module-header-line" }, h2(i18n.banking), p(i18n.bankingDescription), renderWalletChip()); + +const renderEpochView = (epoch, allocations, userId = "", data = {}) => { + const rows = allocations || []; return template( i18n.banking, section( - div({ class: "tags-header" }, h2(`${i18n.bankEpoch}: ${epoch.id}`)), - div({ class: "bank-summary" }, - table({ class: "bank-info-table" }, - tbody( - kvRow(i18n.bankEpochId, epoch.id || "-"), - kvRow(i18n.bankPool, `${Number(epoch.pool || 0).toFixed(6)} ECO`), - kvRow(i18n.bankWeightsSum, String(Number(epoch.weightsSum || 0).toFixed(6))), - kvRow(i18n.bankRuleHash, epoch.hash || "-") + bankingHeader(), + generateFilterButtons(bankingFilters({ ...data, allocations: rows, epochs: data.epochs || [{}] }, "epochs", userId), "epochs", "/banking"), + !epoch + ? div({ class: "no-content-box" }, p(i18n.bankNoEpochs)) + : div( + h2(`${i18n.bankEpoch}: ${epoch.id}`), + div({ class: "bank-summary" }, + table({ class: "bank-info-table" }, + tbody( + kvRow(i18n.bankEpochId, epoch.id || "-"), + kvRow(i18n.bankPool, `${Number(epoch.pool || 0).toFixed(6)} ECO`), + Number.isFinite(Number(epoch.weightsSum)) && Number(epoch.weightsSum) > 0 ? kvRow(i18n.bankWeightsSum, String(Number(epoch.weightsSum).toFixed(6))) : null, + kvRow(i18n.bankEpochAllocations, String(rows.length)), + epoch.hash && epoch.hash !== "-" ? kvRow(i18n.bankRuleHash, epoch.hash) : null + ) + ) + ), + h2(i18n.bankEpochAllocations), + allocationsTable(paged(rows), userId) ) - ) - ), - h2(i18n.bankEpochAllocations), - allocationsTable(allocations || [], "") ) ); }; -module.exports = { renderBankingView, renderSingleAllocationView, renderEpochView }; +module.exports = { renderBankingView, renderSingleAllocationView, renderEpochView, bankingFlashText: flashText }; diff --git a/src/views/blockchain_view.js b/src/views/blockchain_view.js index a70d752..2c9ae2e 100644 --- a/src/views/blockchain_view.js +++ b/src/views/blockchain_view.js @@ -1,5 +1,5 @@ const { div, h2, h3, p, section, button, form, a, input, span, pre, table, tr, td, strong } = require("../server/node_modules/hyperaxe"); -const { template, i18n, userLink } = require("../views/main_views"); +const { template, i18n, userLink, paged, renderIcon } = require("../views/main_views"); const moment = require("../server/node_modules/moment"); const FILTER_LABELS = { @@ -9,19 +9,20 @@ const FILTER_LABELS = { feed: i18n.typeFeed, event: i18n.typeEvent, task: i18n.typeTask, report: i18n.typeReport, image: i18n.typeImage, audio: i18n.typeAudio, video: i18n.typeVideo, post: i18n.typePost, forum: i18n.typeForum, about: i18n.typeAbout, contact: i18n.typeContact, pub: i18n.typePub, - transfer: i18n.typeTransfer, market: i18n.typeMarket, job: i18n.typeJob, tribe: i18n.typeTribe, - project: i18n.typeProject, banking: i18n.typeBanking, bankWallet: i18n.typeBankWallet, bankClaim: i18n.typeBankClaim, + transfer: i18n.typeTransfer, market: i18n.typeMarket, job: i18n.typeJob, housing: i18n.typeHousing, tribe: i18n.typeTribe, + project: i18n.typeProject, industry: i18n.typeIndustry, industryBlueprint: i18n.industryBlueprints, banking: i18n.typeBanking, bankWallet: i18n.typeBankWallet, bankClaim: i18n.typeBankClaim, aiExchange: i18n.typeAiExchange, parliament: i18n.typeParliament, courts: i18n.typeCourts, map: i18n.typeMap, shop: i18n.typeShop, shopProduct: i18n.typeShopProduct || 'Shop Product', - pad: i18n.typePad || 'PAD', chat: i18n.typeChat || 'CHAT', gameScore: i18n.typeGameScore || 'GAME SCORE', - calendar: i18n.typeCalendar || 'CALENDAR', torrent: i18n.typeTorrent + pad: i18n.typePad || 'PAD', room: i18n.typeRoom || 'ROOM', roomMember: i18n.typeRoomMember || 'ROOM PARTICIPANT', wikiPage: i18n.typeWiki || 'WIKI', emergency: i18n.typeEmergency || 'EMERGENCY', emergencyConfirm: i18n.typeEmergencyConfirm || 'EMERGENCY CONFIRM', emergencyUpdate: i18n.typeEmergencyUpdate || 'EMERGENCY UPDATE', mailingList: i18n.typeMailingList || 'MAILING LIST', logisticsRoute: i18n.typeLogisticsRoute || 'ROUTE', logisticsRating: i18n.typeLogisticsRating || 'ROUTE RATING', podcast: i18n.typePodcast || 'PODCAST', podcastEpisode: i18n.typePodcastEpisode || 'EPISODE', podcastOpinion: i18n.typePodcastOpinion || 'EPISODE OPINION', podcastPlay: i18n.typePodcastPlay || 'EPISODE PLAY', campaign: i18n.typeCampaign || 'CAMPAIGN', campaignSignature: i18n.typeCampaignSignature || 'SIGNATURE', campaignUpdate: i18n.typeCampaignUpdate || 'CAMPAIGN UPDATE', chat: i18n.typeChat || 'CHAT', gameScore: i18n.typeGameScore || 'GAME SCORE', + calendar: i18n.typeCalendar || 'CALENDAR', torrent: i18n.typeTorrent, file: i18n.typeFile, + school: i18n.typeSchool || 'SCHOOL' }; -const BASE_FILTERS = ['recent', 'all', 'mine', 'tombstone', 'logs']; -const CAT_BLOCK1 = ['votes', 'event', 'task', 'report', 'calendar', 'parliament', 'courts']; +const BASE_FILTERS = ['all', 'recent', 'mine', 'tombstone', 'logs']; +const CAT_BLOCK1 = ['votes', 'event', 'task', 'report', 'calendar', 'school', 'parliament', 'courts']; const CAT_BLOCK2 = ['pub', 'tribe', 'about', 'contact', 'curriculum', 'vote', 'aiExchange']; -const CAT_BLOCK3 = ['banking', 'job', 'market', 'project', 'transfer', 'feed', 'post', 'pixelia', 'shop', 'gameScore']; -const CAT_BLOCK4 = ['forum', 'pad', 'chat', 'bookmark', 'image', 'video', 'audio', 'document', 'map', 'torrent']; +const CAT_BLOCK3 = ['banking', 'job', 'housing', 'market', 'project', 'industry', 'transfer', 'feed', 'post', 'pixelia', 'shop', 'gameScore']; +const CAT_BLOCK4 = ['forum', 'pad', 'chat', 'room', 'bookmark', 'image', 'video', 'audio', 'document', 'map', 'torrent', 'file']; const SEARCH_FIELDS = ['author','id','from','to']; @@ -75,6 +76,7 @@ const filterBlocks = (blocks, filter, userId) => { return blocks.filter(b => cset.has(b.type)); } if (filter === 'shop') return blocks.filter(b => b.type === 'shop' || b.type === 'shopProduct'); + if (filter === 'school') return blocks.filter(b => ['schoolCourse', 'schoolLesson', 'schoolEnroll', 'schoolCertificate'].includes(b.type)); if (filter === 'logs') return blocks.filter(b => b.type === 'log' && b.author === userId); return blocks.filter(b => b.type === filter); }; @@ -100,7 +102,7 @@ const computeStats = (blocks) => { const renderStatsPanel = (stats, currentFilter, search) => { if (!stats || stats.total === 0) return null; - const topTypes = stats.typeBreakdown.slice(0, 10); + const topTypes = stats.typeBreakdown.filter(t => t.type !== 'industryBlueprint').slice(0, 10); return div({ class: 'blockchain-stats' }, div({ class: 'tags-header' }, h3(`${i18n.blockchainStatsTitle || 'Stats'} (${stats.total})`) @@ -173,13 +175,19 @@ const getViewDetailsAction = (type, block) => { case 'event': return `/events/${encodeURIComponent(block.id)}`; case 'task': return `/tasks/${encodeURIComponent(block.id)}`; case 'about': return `/author/${encodeURIComponent(block.author)}`; - case 'post': return `/thread/${encodeURIComponent(block.id)}#${encodeURIComponent(block.id)}`; + case 'post': return block.content && typeof block.content.root === 'string' + ? `/thread/${encodeURIComponent(block.content.root)}#${encodeURIComponent(block.id)}` + : `/blogs/${encodeURIComponent(block.id)}`; case 'vote': return `/thread/${encodeURIComponent(block.content.vote.link)}#${encodeURIComponent(block.content.vote.link)}`; case 'contact': return `/inhabitants`; case 'pub': return `/invites`; case 'market': return `/market/${encodeURIComponent(block.id)}`; case 'job': return `/jobs/${encodeURIComponent(block.id)}`; + case 'housing': return `/housing/${encodeURIComponent(block.id)}`; case 'project': return `/projects/${encodeURIComponent(block.id)}`; + case 'schoolCourse': return `/school/course/${encodeURIComponent(block.id)}`; + case 'industry': return `/industry/${encodeURIComponent(block.id)}`; + case 'industryBlueprint': return `/industry/blueprint/${encodeURIComponent(block.id)}`; case 'report': return `/reports/${encodeURIComponent(block.id)}`; case 'calendar': return `/calendars/${encodeURIComponent(block.id)}`; case 'bankWallet': return `/wallet`; @@ -200,13 +208,17 @@ const getViewDetailsAction = (type, block) => { case 'courtsNominationVote': return `/courts`; case 'map': return `/maps/${encodeURIComponent(block.id)}`; case 'torrent': return `/torrents/${encodeURIComponent(block.id)}`; + case 'file': return `/files/${encodeURIComponent(block.id)}`; case 'mapMarker': return block.content?.mapId ? `/maps/${encodeURIComponent(block.content.mapId)}` : `/maps`; case 'shop': return `/shops/${encodeURIComponent(block.id)}`; case 'shopProduct': return `/shops/product/${encodeURIComponent(block.id)}`; case 'pad': return `/pads/${encodeURIComponent(block.id)}`; + case 'room': return `/rooms/${encodeURIComponent(block.id)}`; + case 'roomMember': return block.content?.target ? `/rooms/${encodeURIComponent(block.content.target)}` : `/rooms`; case 'chat': return `/chats/${encodeURIComponent(block.id)}`; case 'gameScore': return `/games?filter=scoring`; case 'log': return `/logs/view/${encodeURIComponent(block.id)}`; + case 'poll': return `/polls/${encodeURIComponent(block.id)}`; case 'calendarDate': case 'calendarNote': return block.content?.calendarId ? `/calendars/${encodeURIComponent(block.content.calendarId)}` : `/calendars`; case 'padEntry': return block.content?.padId ? `/pads/${encodeURIComponent(block.content.padId)}` : `/pads`; @@ -219,8 +231,8 @@ const TYPE_COLORS = { post:'#3498db', vote:'#9b59b6', votes:'#9b59b6', about:'#1abc9c', contact:'#16a085', pub:'#2ecc71', tribe:'#e67e22', event:'#e74c3c', task:'#f39c12', report:'#c0392b', image:'#2980b9', audio:'#8e44ad', video:'#d35400', document:'#27ae60', bookmark:'#f1c40f', - forum:'#1abc9c', feed:'#95a5a6', transfer:'#e74c3c', market:'#e67e22', job:'#3498db', - project:'#2ecc71', banking:'#f39c12', bankWallet:'#f39c12', bankClaim:'#f39c12', + forum:'#1abc9c', feed:'#95a5a6', transfer:'#e74c3c', market:'#e67e22', job:'#3498db', housing:'#16a085', + project:'#2ecc71', schoolCourse:'#1abc9c', schoolLesson:'#1abc9c', schoolEnroll:'#1abc9c', schoolCertificate:'#1abc9c', banking:'#f39c12', bankWallet:'#f39c12', bankClaim:'#f39c12', pixelia:'#9b59b6', curriculum:'#1abc9c', aiExchange:'#3498db', tombstone:'#7f8c8d', parliamentTerm:'#8e44ad', parliamentProposal:'#8e44ad', parliamentLaw:'#8e44ad', parliamentCandidature:'#8e44ad', parliamentRevocation:'#8e44ad', @@ -228,7 +240,7 @@ const TYPE_COLORS = { courtsVerdict:'#c0392b', courtsSettlement:'#c0392b', courtsNomination:'#c0392b', map:'#27ae60', mapMarker:'#27ae60', shop:'#e67e22', shopProduct:'#e67e22', - pad:'#2ecc71', chat:'#3498db', gameScore:'#f39c12', + pad:'#2ecc71', room:'#27ae60', roomMember:'#27ae60', wikiPage:'#1abc9c', emergency:'#e74c3c', emergencyConfirm:'#e74c3c', emergencyUpdate:'#e74c3c', mailingList:'#8e44ad', logisticsRoute:'#16a085', logisticsRating:'#16a085', podcast:'#d35400', podcastEpisode:'#d35400', podcastOpinion:'#d35400', podcastPlay:'#d35400', campaign:'#c0392b', campaignSignature:'#c0392b', campaignUpdate:'#c0392b', chat:'#3498db', gameScore:'#f39c12', calendar:'#e74c3c' }; @@ -303,7 +315,7 @@ const renderSingleBlockView = (block, filter = 'recent', userId, search = {}, vi return template( i18n.blockchain, section( - div({ class: 'tags-header' }, + div({ class: 'tags-header module-header-line' }, h2(i18n.blockchain), p(i18n.blockchainDescription) ), @@ -389,7 +401,7 @@ const renderSingleBlockView = (block, filter = 'recent', userId, search = {}, vi return template( i18n.blockchain, section( - div({ class: 'tags-header' }, + div({ class: 'tags-header module-header-line' }, h2(i18n.blockchain), p(i18n.blockchainDescription) ), @@ -414,9 +426,7 @@ const renderSingleBlockView = (block, filter = 'recent', userId, search = {}, vi button({ type:'submit', class:'filter-btn' }, `← ${i18n.blockchainBack}`) ), !block.isTombstoned && !block.isReplaced && getViewDetailsAction(block.type, block) ? - form({ method:'GET', action:getViewDetailsAction(block.type, block) }, - button({ type:'submit', class:'filter-btn' }, i18n.visitContent) - ) + a({ href:getViewDetailsAction(block.type, block), class:'btn-singleview btn-content', title:i18n.visitContent }, renderIcon('open')) : (block.isTombstoned || block.isReplaced) ? div({ class: 'deleted-label' }, i18n.blockchainContentDeleted || "This content has been deleted." @@ -438,25 +448,28 @@ const renderBlockchainView = (blocks, filter, userId, search = {}, extras = {}) const shown = filterBlocks(blocks, filter, userId); const qs = toQueryString(filter, s); + const censusBlocks = Array.isArray(extras && extras.censusBlocks) ? extras.censusBlocks : blocks; + const chipVisible = (mode) => mode === filter || filterBlocks(censusBlocks, mode, userId).length > 0; + const baseChips = ['all', 'recent', 'mine', ...(chipVisible('tombstone') ? ['tombstone'] : []), ...(chipVisible('logs') ? ['logs'] : [])]; return template( i18n.blockchain, section( - div({ class:'tags-header' }, + div({ class:'tags-header module-header-line' }, h2(i18n.blockchain), p(i18n.blockchainDescription) ), div({ class:'mode-buttons-row' }, div({ class: 'filter-column' }, - generateFilterButtons(BASE_FILTERS, filter, '/blockexplorer', s) + generateFilterButtons(baseChips, filter, '/blockexplorer', s) ), div({ class: 'filter-column' }, - generateFilterButtons(CAT_BLOCK1, filter, '/blockexplorer', s), - generateFilterButtons(CAT_BLOCK2, filter, '/blockexplorer', s) + generateFilterButtons(CAT_BLOCK1.filter(chipVisible), filter, '/blockexplorer', s), + generateFilterButtons(CAT_BLOCK2.filter(chipVisible), filter, '/blockexplorer', s) ), div({ class: 'filter-column' }, - generateFilterButtons(CAT_BLOCK3, filter, '/blockexplorer', s), - generateFilterButtons(CAT_BLOCK4, filter, '/blockexplorer', s) + generateFilterButtons(CAT_BLOCK3.filter(chipVisible), filter, '/blockexplorer', s), + generateFilterButtons(CAT_BLOCK4.filter(chipVisible), filter, '/blockexplorer', s) ) ), div({ class: 'blockexplorer-search' }, @@ -472,7 +485,7 @@ const renderBlockchainView = (blocks, filter, userId, search = {}, extras = {}) input({ type: 'datetime-local', name: 'to', value: toVal, class: 'blockexplorer-search-input' }) ), div({ class: 'blockexplorer-search-actions' }, - button({ type: 'submit', class: 'filter-box__button' }, i18n.searchSubmit) + div({ class: 'filter-box__controls' }, button({ type: 'submit', class: 'filter-box__button' }, i18n.searchSubmit)) ) ) ) @@ -510,7 +523,7 @@ const renderBlockchainView = (blocks, filter, userId, search = {}, extras = {}) h2({ class: 'block-diagram-title' }, 'Blockchain Blocks'), shown.length === 0 ? div(p(i18n.blockchainNoBlocks)) - : shown + : paged(shown .sort((a,b)=>{ const ta = a.type==='market'&&a.content.updatedAt ? new Date(a.content.updatedAt).getTime() @@ -519,16 +532,14 @@ const renderBlockchainView = (blocks, filter, userId, search = {}, extras = {}) ? new Date(b.content.updatedAt).getTime() : b.ts; return tb - ta; - }) + })) .map(block=> div({ class:'block' }, div({ class:'block-buttons' }, - block.restricted ? null : a({ href:`/blockexplorer/block/${encodeURIComponent(block.id)}${qs}`, class:'btn-singleview', title:i18n.blockchainDetails }, '⦿'), - block.restricted ? null : a({ href:`/blockexplorer/block/${encodeURIComponent(block.id)}${qs}&view=datagram`, class:'btn-singleview btn-datagram', title:i18n.blockchainDatagram || 'Datagram' }, '⊞'), + block.restricted ? null : a({ href:`/blockexplorer/block/${encodeURIComponent(block.id)}${qs}`, class:'btn-singleview', title:i18n.blockchainDetails }, renderIcon('block')), + block.restricted ? null : a({ href:`/blockexplorer/block/${encodeURIComponent(block.id)}${qs}&view=datagram`, class:'btn-singleview btn-datagram', title:i18n.blockchainDatagram }, renderIcon('datagram')), !block.isTombstoned && !block.isReplaced && getViewDetailsAction(block.type, block) ? - form({ method:'GET', action:getViewDetailsAction(block.type, block) }, - button({ type:'submit', class:'filter-btn' }, i18n.visitContent) - ) + a({ href:getViewDetailsAction(block.type, block), class:'btn-singleview btn-content', title:i18n.visitContent }, renderIcon('open')) : (block.isTombstoned || block.isReplaced) ? div({ class: 'deleted-label' }, i18n.blockchainContentDeleted || "This content has been deleted." diff --git a/src/views/blog_view.js b/src/views/blog_view.js new file mode 100644 index 0000000..d9bbfe1 --- /dev/null +++ b/src/views/blog_view.js @@ -0,0 +1,236 @@ +const { div, h2, p, section, button, form, a, input, label, span, textarea, br, table, tr, td } = require("../server/node_modules/hyperaxe"); +const RAW_HTML = Symbol.for('oasis.rawHtml'); +const { TEXT_CAP } = require('../backend/long_text'); +const { renderCommentsSection: renderSharedCommentsSection } = require("./comments_view"); +const { clearnetItemHref, template, i18n, userLink, renderOpinionsVoting, renderEngagement, renderSpreadButton, renderContentActions, contentDeleteAction, renderSubscriptionBox, renderModuleStats, moduleIsEmpty, paged } = require("./main_views"); +const { renderReachChip, renderClearnetSelector, renderClearnetSwitch } = require("./clearnet_view"); +const moment = require("../server/node_modules/moment"); +const { config } = require("../server/SSB_server.js"); +const { renderStyledHtml } = require("../backend/renderStyledText"); +const { sanitizeHtml } = require("../backend/sanitizeHtml"); + +const userId = config.keys.id; + +const FILTERS = [ + { key: "ALL", i18n: "blogFilterAll" }, + { key: "RECENT", i18n: "blogFilterRecent" }, + { key: "MINE", i18n: "blogFilterMine" }, + { key: "FAVORITES", i18n: "blogFilterFavorites" }, + { key: "TOP", i18n: "blogFilterTop" } +]; + +const safeText = (v) => String(v == null ? "" : v).trim(); + +const renderBlogTags = (tags) => { + const list = (Array.isArray(tags) ? tags : []).map(t => String(t || "").trim().replace(/^#+/, "")).filter(Boolean); + return list.length + ? div({ class: "card-tags" }, list.map(tag => a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link" }, `#${tag}`))) + : null; +}; + +const excerpt = (text, max = 420) => { + const s = safeText(text); + return s.length > max ? s.slice(0, max).replace(/\s+$/, "") + "…" : s; +}; + +const renderFilterBar = (filter, q, showSearch = true, total = null, censusList = null) => { + const emptyMod = total !== null && moduleIsEmpty({ length: Number(total) || 0 }, filter, "ALL", q); + const censusB = Array.isArray(censusList) ? censusList : []; + const blogChip = (x) => { + const m = x.key; + if (m === filter || m === "ALL") return true; + if (m === "TOP") return censusB.length > 0; + if (m === "MINE") return censusB.some(b => String(b.author) === String(userId)); + if (m === "RECENT") return censusB.length > 0; + if (m === "FAVORITES") return censusB.some(b => b.isFavorite); + return true; + }; + return section( + div({ class: "filters" }, + form({ method: "GET", action: "/blogs", class: "ui-toolbar ui-toolbar--filters" }, + ...(emptyMod ? [] : FILTERS.filter(blogChip).map(f => + button({ type: "submit", name: "filter", value: f.key, class: filter === f.key ? "filter-btn active" : "filter-btn" }, String(i18n[f.i18n]).toUpperCase()) + )), + button({ type: "submit", name: "filter", value: "CREATE", class: "create-button" }, i18n.blogCreateButton) + ) + ), + showSearch && !emptyMod + ? div({ class: "filters activity-filter-chips activity-toolbar-row" }, + total != null ? renderModuleStats(total) : null, + form({ method: "GET", action: "/blogs", class: "filter-box" }, + input({ type: "hidden", name: "filter", value: filter }), + input({ type: "text", name: "q", value: q || "", placeholder: i18n.blogSearchPlaceholder, class: "filter-box__input" }), + div({ class: "filter-box__controls" }, + button({ type: "submit", class: "filter-box__button" }, i18n.searchButton) + ) + ) + ) + : null + ); +}; + +const renderBlogCard = (blog, filter, spreadInfo) => { + const href = `/blogs/${encodeURIComponent(blog.id)}`; + const isOwn = String(blog.author) === String(userId); + return div({ class: "trending-card blog-card" + (isOwn ? " own-content" : "") }, + div({ class: "card-header activity-card-header" }, + renderContentActions(blog.id, href, { spread: spreadInfo || null, author: blog.author, favKind: 'blogs', isFavorite: blog.isFavorite, reportTitle: blog.subject || blog.text, deleteAction: isOwn ? contentDeleteAction('blog', blog.id) : undefined }) + ), + div({ class: "card-section blog-card-body" }, + blog.subject + ? div({ class: "shop-title-row" }, + h2({ class: "tribe-card-title" }, a({ href }, blog.subject)) + ) + : null, + blog.clearnet === true ? div({ class: "card-chips-row" }, renderReachChip(true, i18n, clearnetItemHref("blog", blog.subject || blog.text, blog.id))) : null, + div({ class: "blog-card-text", [RAW_HTML]: sanitizeHtml(renderStyledHtml(excerpt(blog.text))) }), + renderBlogTags(blog.tags), + p({ class: "card-footer" }, + span({ class: "date-link" }, `${moment(blog.createdAt).format("YYYY/MM/DD HH:mm")}`), + userLink(blog.author) + ) + ) + ); +}; + +const renderCreateForm = (draft = null) => { + const subjectValue = draft ? safeText(draft.subject) : ""; + const textValue = draft ? String(draft.text || "") : ""; + const allowComments = draft ? draft.allowComments !== false : true; + const tagsValue = draft ? (Array.isArray(draft.tags) ? draft.tags.join(", ") : safeText(draft.tags)) : ""; + return section( + draft + ? section({ class: "post-preview blog-preview" }, + div({ class: "preview-content" }, + h2(i18n.messagePreview), + subjectValue ? h2({ class: "tribe-card-title" }, subjectValue) : null, + div({ class: "blog-detail-text", [RAW_HTML]: sanitizeHtml(renderStyledHtml(textValue)) }), + renderBlogTags(draft.tags) + ) + ) + : null, + div({ class: "publish-form" }, + form({ action: "/blogs/create", method: "POST", enctype: "multipart/form-data" }, + label({ for: "subject" }, i18n.blogSubject), + br(), + input({ type: "text", name: "subject", id: "subject", maxlength: "150", placeholder: i18n.contentWarningPlaceholder, value: subjectValue }), + br(), + label({ for: "text" }, i18n.blogMessage), + br(), + textarea({ required: true, name: "text", id: "text", rows: "8", class: "publish-textarea", maxlength: String(TEXT_CAP), placeholder: i18n.publishWarningPlaceholder }, textValue), + br(), + label({ for: "blob" }, i18n.blogMedia), + br(), + input({ type: "file", id: "blob", name: "blob", multiple: true, accept: "image/*,video/*,audio/*,application/pdf" }), + br(), + br(), + label({ for: "tags" }, i18n.tagsLabel || "Tags"), + br(), + input({ type: "text", name: "tags", id: "tags", placeholder: i18n.tagsPlaceholder || "", value: tagsValue }), + br(), + div({ class: "blog-allow-comments" }, + input({ type: "hidden", name: "allowComments", value: "0" }), + label( + input({ type: "checkbox", name: "allowComments", value: "1", ...(allowComments ? { checked: true } : {}) }), + " ", + i18n.blogAllowComments + ) + ), + renderClearnetSelector(!!(draft && draft.clearnet === true), i18n), + br(), + button({ type: "submit", class: "filter-btn", formaction: "/blogs/preview", attrs: { formmethod: "POST" } }, i18n.preview), + " ", + button({ type: "submit", class: "create-button" }, i18n.blogPublish) + ) + ) + ); +}; + +const renderCommentsSection = (blog, comments = []) => { + const href = `/blogs/${encodeURIComponent(blog.id)}`; + return renderSharedCommentsSection({ + action: `${href}/comments`, + comments: comments, + returnTo: href, + closedNote: blog.allowComments ? null : i18n.blogCommentsClosed + }); +}; + +exports.blogView = async (blogs = [], filter = "ALL", params = {}) => { + const showForm = String(filter).toUpperCase() === "CREATE"; + const spreadMap = params.spreadMap instanceof Map ? params.spreadMap : new Map(); + return template( + i18n.blogTitle, + section( + div({ class: "tags-header module-header-line" }, + h2(i18n.blogTitle), + p(i18n.blogDescription) + ) + ), + renderFilterBar(showForm ? "CREATE" : filter, params.q, !showForm, Array.isArray(blogs) ? blogs.length : 0, params.censusList), + showForm + ? renderCreateForm(params.draft || null) + : section( + blogs.length + ? div({ class: "blogs-grid" }, ...paged(blogs).map(b => renderBlogCard(b, filter, spreadMap.get(b.id)))) + : div({ class: "no-content-box" }, p({ class: "no-content" }, i18n.blogNoItems)) + ) + ); +}; + +exports.singleBlogView = async (blog, comments = [], params = {}) => { + const href = `/blogs/${encodeURIComponent(blog.id)}`; + const isAuthor = String(blog.author) === String(userId); + const isClearnet = !!blog.clearnet; + + const blogSide = div({ class: "tribe-side" }, + div({ class: "card-header activity-card-header" }, + renderContentActions(blog.id, href, { spread: params.spreads || null, author: blog.author, favKind: 'blogs', isFavorite: blog.isFavorite, reportTitle: blog.subject || blog.text, returnTo: href, deleteAction: isAuthor ? contentDeleteAction('blog', blog.id) : undefined }) + ), + div({ class: "shop-title-row" }, + h2({ class: "tribe-card-title" }, blog.subject || i18n.blogTitle), + renderReachChip(isClearnet, i18n, isClearnet ? clearnetItemHref("blog", blog.subject || blog.text, blog.id) : null) + ), + isAuthor ? renderClearnetSwitch("posts", blog.rootId || blog.id, isClearnet) : null, + table({ class: "tribe-info-table jobs-info-table" }, + tr( + td({ class: "tribe-info-label" }, i18n.createdAtLabel || "Created at"), + td({ class: "tribe-info-value", colspan: "3" }, moment(blog.createdAt).format("YYYY/MM/DD HH:mm")) + ), + tr( + td({ class: "tribe-info-value", colspan: "4" }, userLink(blog.author)) + ) + ), + div({ class: "tribe-card-members" }, + span({ class: "tribe-members-count" }, `${i18n.blogComments}: ${blog.commentCount || 0}`) + ), + params.subscription + ? renderSubscriptionBox({ + target: blog.author, + scope: "blogs", + subscribed: params.subscription.subscribed === true, + count: params.subscription.count, + isOwner: isAuthor, + returnTo: href + }) + : null, + ); + + const blogMain = div({ class: "tribe-main" }, + div({ class: "job-section" }, + div({ class: "blog-detail-text", [RAW_HTML]: sanitizeHtml(renderStyledHtml(blog.text || "")) }), + renderBlogTags(blog.tags) + ), + renderEngagement(blog.id, + renderOpinionsVoting('/blogs/opinions', blog.id, blog.opinions, href, blog.opinions_inhabitants), + renderCommentsSection(blog, comments) + ) + ); + + return template( + blog.subject || i18n.blogTitle, + section(div({ class: "tags-header module-header-line" }, h2(i18n.blogTitle), p(i18n.blogDescription))), + renderFilterBar("ALL", params.q, false, null, params.censusList), + section(div({ class: "tribe-details" }, blogSide, blogMain)) + ); +}; diff --git a/src/views/bookmark_view.js b/src/views/bookmark_view.js index 24381ba..8b6d2eb 100644 --- a/src/views/bookmark_view.js +++ b/src/views/bookmark_view.js @@ -1,10 +1,12 @@ const { form, button, div, h2, p, section, input, label, textarea, br, a, span, select, option } = require("../server/node_modules/hyperaxe"); +const { renderCommentsSection: renderSharedCommentsSection, renderCommentsLink } = require("./comments_view"); -const { template, i18n, renderOpinionsVoting, userLink, renderSpreadButton, renderEcoTax, renderLifespanChip, renderContentActions } = require("./main_views"); +const { clearnetItemHref, template, i18n, renderOpinionsVoting, renderEngagement, userLink, renderSpreadButton, renderEcoTax, renderLifespanChip, renderContentActions, renderSpreadEditWarning, renderModuleStats, moduleIsEmpty, contentDeleteAction, paged } = require("./main_views"); +const { renderReachChip, renderClearnetSelector, renderClearnetSwitch, renderTribeOriginChip } = require("./clearnet_view"); const moment = require("../server/node_modules/moment"); const { config } = require("../server/SSB_server.js"); -const { renderUrl } = require("../backend/renderUrl"); +const { renderStyledText, safeExternalHref } = require("../backend/renderStyledText"); const userId = config.keys.id; @@ -33,95 +35,12 @@ const renderPMButton = (recipient, className = "filter-btn") => { ); }; -const renderBookmarkActions = (filter, bookmark, params = {}) => { - const returnTo = buildReturnTo(filter, params); - const isAuthor = String(bookmark.author) === String(userId); - const hasOpinions = Object.keys(bookmark.opinions || {}).length > 0; - - return isAuthor - ? div( - { class: "bookmark-actions" }, - !hasOpinions - ? form( - { method: "GET", action: `/bookmarks/edit/${encodeURIComponent(bookmark.id)}` }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - button({ class: "update-btn", type: "submit" }, i18n.bookmarkUpdateButton) - ) - : null, - form( - { method: "POST", action: `/bookmarks/delete/${encodeURIComponent(bookmark.id)}` }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - button({ class: "delete-btn", type: "submit" }, i18n.bookmarkDeleteButton) - ) - ) - : null; -}; - const renderBookmarkCommentsSection = (bookmarkId, rootId, comments = [], returnTo = null) => { - const list = safeArr(comments).filter(c => { - const t = c && c.value && c.value.content && c.value.content.text; - return t && String(t).trim(); + return renderSharedCommentsSection({ + action: `/bookmarks/${encodeURIComponent(bookmarkId)}/comments`, + comments: comments, + returnTo: returnTo }); - const commentsCount = list.length; - - return div( - { class: "vote-comments-section" }, - div( - { class: "comments-count" }, - span({ class: "card-label" }, i18n.voteCommentsLabel + ": "), - span({ class: "card-value" }, String(commentsCount)) - ), - div( - { class: "comment-form-wrapper" }, - h2({ class: "comment-form-title" }, i18n.voteNewCommentLabel), - form( - { method: "POST", action: `/bookmarks/${encodeURIComponent(bookmarkId)}/comments`, class: "comment-form", enctype: "multipart/form-data" }, - returnTo ? input({ type: "hidden", name: "returnTo", value: returnTo }) : null, - rootId ? input({ type: "hidden", name: "rootId", value: rootId }) : null, - textarea({ - id: "comment-text", - name: "text", - rows: 4, - class: "comment-textarea", - placeholder: i18n.voteNewCommentPlaceholder - }), - div({ class: "comment-file-upload" }, label(i18n.uploadMedia), input({ type: "file", name: "blob" })), - br(), - button({ type: "submit", class: "comment-submit-btn" }, i18n.voteNewCommentButton) - ) - ), - list.length - ? div( - { class: "comments-list" }, - list.map((c) => { - const author = c?.value?.author || ""; - const ts = c?.value?.timestamp || c?.timestamp; - const absDate = ts ? moment(ts).format("YYYY/MM/DD HH:mm:ss") : ""; - const relDate = ts ? moment(ts).fromNow() : ""; - - const content = c?.value?.content || {}; - const text = content.text || ""; - const threadRoot = content.fork || content.root || null; - - return div( - { class: "votations-comment-card" }, - span( - { class: "created-at" }, - span(i18n.createdBy), - author ? userLink(author) : span("(unknown)"), - absDate ? span(" | ") : "", - absDate ? span({ class: "votations-comment-date" }, absDate) : "", - relDate ? span({ class: "votations-comment-date" }, " | ", i18n.sendTime) : "", - relDate && threadRoot - ? a({ href: `/thread/${encodeURIComponent(threadRoot)}#${encodeURIComponent(c.key)}` }, relDate) - : "" - ), - p({ class: "votations-comment-text" }, ...renderUrl(text)) - ); - }) - ) - : p({ class: "votations-no-comments" }, i18n.voteNoCommentsYet) - ); }; const renderCardField = (labelText, value) => @@ -131,22 +50,6 @@ const renderCardField = (labelText, value) => span({ class: "card-value" }, value) ); -const renderFavoriteToggle = (bookmark, returnTo) => - form( - { - method: "POST", - action: bookmark.isFavorite - ? `/bookmarks/favorites/remove/${encodeURIComponent(bookmark.id)}` - : `/bookmarks/favorites/add/${encodeURIComponent(bookmark.id)}`, - class: "bookmark-favorite-form" - }, - returnTo ? input({ type: "hidden", name: "returnTo", value: returnTo }) : null, - button( - { type: "submit", class: "filter-btn" }, - bookmark.isFavorite ? i18n.bookmarkRemoveFavoriteButton : i18n.bookmarkAddFavoriteButton - ) - ); - const renderTags = (tags) => { const list = safeArr(tags).map((t) => String(t || "").trim()).filter(Boolean); return list.length @@ -157,7 +60,7 @@ const renderTags = (tags) => { : null; }; -const renderBookmarkList = (filteredBookmarks, filter, params = {}) => { +const renderBookmarkList = exports.renderBookmarkList = (filteredBookmarks, filter, params = {}) => { const returnTo = buildReturnTo(filter, params); return filteredBookmarks.length @@ -167,65 +70,44 @@ const renderBookmarkList = (filteredBookmarks, filter, params = {}) => { const lastVisit = bookmark.lastVisit ? moment(bookmark.lastVisit) : null; const lastVisitTxt = lastVisit && lastVisit.isValid() - ? `${lastVisit.format("YYYY/MM/DD HH:mm:ss")} (${lastVisit.fromNow()})` + ? `${lastVisit.format("YYYY/MM/DD HH:mm")} (${lastVisit.fromNow()})` : i18n.noLastVisit; const urlLink = bookmark.url - ? a({ href: bookmark.url, target: "_blank", rel: "noreferrer noopener", class: "bookmark-url" }, bookmark.url) + ? a({ href: safeExternalHref(bookmark.url), target: "_blank", rel: "noreferrer noopener", class: "bookmark-url" }, bookmark.url) : i18n.noUrl; const isOwn = bookmark.author && String(bookmark.author) === String(userId); + const reachChip = bookmark.tribeOrigin ? renderTribeOriginChip(bookmark.tribeOrigin) : bookmark.clearnet === true ? renderReachChip(true, i18n, clearnetItemHref("bookmarks", bookmark.title || bookmark.url, bookmark.id)) : null; + const extraChips = typeof params.titleChips === "function" ? params.titleChips(bookmark).filter(Boolean) : []; + const headerActions = typeof params.headerActions === "function" + ? params.headerActions(bookmark) + : bookmark.tribeOrigin + ? renderContentActions(null, bookmark.tribeOrigin.href) + : renderContentActions(bookmark.id, `/bookmarks/${encodeURIComponent(bookmark.id)}`, { spread: (params.spreadMap && params.spreadMap.get(bookmark.id)) || params.spreads || null, author: bookmark.author, favKind: 'bookmarks', isFavorite: bookmark.isFavorite, reportTitle: bookmark.title, deleteAction: isOwn ? contentDeleteAction('bookmark', bookmark.id) : undefined, returnTo }); + const titleHref = typeof params.headerActions === "function" ? null : bookmark.tribeOrigin ? bookmark.tribeOrigin.href : `/bookmarks/${encodeURIComponent(bookmark.id)}`; + const engagement = typeof params.engagement === "function" + ? params.engagement(bookmark) + : bookmark.tribeOrigin ? null : renderEngagement(bookmark.id, + renderOpinionsVoting('/bookmarks/opinions', bookmark.id, bookmark.opinions, returnTo, bookmark.opinions_inhabitants), + renderCommentsLink({ href: `/bookmarks/${encodeURIComponent(bookmark.id)}`, count: commentCount }) + ); return div( { class: "trending-card bookmark-card" + (isOwn ? " own-content" : "") }, div( { class: "card-header activity-card-header" }, span(), - renderContentActions(bookmark.id, `/bookmarks/${encodeURIComponent(bookmark.id)}`) + headerActions ), div( { class: "card-section bookmark-card-body" }, - div( - { class: "bookmark-topbar" }, - div( - { class: "bookmark-topbar-left" }, - renderPMButton(bookmark.author), - renderFavoriteToggle(bookmark, returnTo) - ), - div( - { class: "bookmark-topbar-right" }, - renderBookmarkActions(filter, bookmark, params) - ) - ), - h2({ class: "bookmark-title" }, bookmark.category || bookmark.url || ""), - bookmark.lifetime ? div({ class: "card-chips-row" }, renderLifespanChip(bookmark.lifetime, i18n)) : null, - renderCardField(i18n.bookmarkUrlLabel + ":", urlLink), - renderCardField(i18n.bookmarkLastVisitLabel + ":", lastVisitTxt), + h2({ class: "bookmark-title" }, titleHref ? a({ href: titleHref, class: "card-title-link" }, bookmark.title || bookmark.url || "") : (bookmark.url ? urlLink : (bookmark.title || ""))), + bookmark.lifetime || reachChip || extraChips.length ? div({ class: "card-chips-row" }, renderLifespanChip(bookmark.lifetime, i18n), reachChip, ...extraChips) : null, + titleHref ? (bookmark.title && bookmark.url ? p({ class: "bookmark-subtitle" }, urlLink) : null) : (bookmark.title && bookmark.url ? p({ class: "bookmark-subtitle" }, bookmark.title) : null), + params.hideLastVisit ? null : renderCardField(i18n.bookmarkLastVisitLabel + ":", lastVisitTxt), + ...(typeof params.bodyExtra === "function" ? params.bodyExtra(bookmark) : []), br, - div( - { class: "card-comments-summary" }, - span({ class: "card-label" }, i18n.voteCommentsLabel + ":"), - span({ class: "card-value" }, String(commentCount)), - br(), - br(), - form( - { method: "GET", action: `/bookmarks/${encodeURIComponent(bookmark.id)}` }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - input({ type: "hidden", name: "filter", value: filter || "all" }), - params.q ? input({ type: "hidden", name: "q", value: params.q }) : null, - params.sort ? input({ type: "hidden", name: "sort", value: params.sort }) : null, - button({ type: "submit", class: "filter-btn" }, i18n.voteCommentsForumButton) - ) - ), - div( - { class: "card-comments-summary feed-opinions-section" }, - div( - { class: "comments-count" }, - span({ class: "card-label" }, (i18n.opinionsTitle || "Opinions") + ": "), - span({ class: "card-value" }, String(Object.values(bookmark.opinions || {}).reduce((s, n) => s + (Number(n) || 0), 0))) - ), - renderOpinionsVoting('/bookmarks/opinions', bookmark.id, bookmark.opinions, returnTo, bookmark.opinions_inhabitants) - ), - div({ class: "card-spread-left" }, renderSpreadButton(bookmark.id, (params.spreadMap && params.spreadMap.get(bookmark.id)) || params.spreads)), + engagement, (() => { const createdTs = bookmark.createdAt ? new Date(bookmark.createdAt).getTime() : NaN; const updatedTs = bookmark.updatedAt ? new Date(bookmark.updatedAt).getTime() : NaN; @@ -233,12 +115,12 @@ const renderBookmarkList = (filteredBookmarks, filter, params = {}) => { return p( { class: "card-footer" }, - span({ class: "date-link" }, `${moment(bookmark.createdAt).format("YYYY/MM/DD HH:mm:ss")} ${i18n.performed} `), + span({ class: "date-link" }, `${moment(bookmark.createdAt).format("YYYY/MM/DD HH:mm")}`), userLink(bookmark.author), showUpdated ? span( { class: "votations-comment-date" }, - ` | ${i18n.bookmarkUpdatedAt}: ${moment(bookmark.updatedAt).format("YYYY/MM/DD HH:mm:ss")}` + ` | ${i18n.bookmarkUpdatedAt}: ${moment(bookmark.updatedAt).format("YYYY/MM/DD HH:mm")}` ) : null ); @@ -262,6 +144,7 @@ const renderBookmarkForm = (filter, bookmarkId, bookmarkToEdit, tags, params = { return div( { class: "div-center bookmark-form" }, + params.spreadWarning || null, form( { action: filter === "edit" ? `/bookmarks/update/${encodeURIComponent(bookmarkId)}` : "/bookmarks/create", method: "POST" }, input({ type: "hidden", name: "returnTo", value: returnTo }), @@ -294,16 +177,6 @@ const renderBookmarkForm = (filter, bookmarkId, bookmarkToEdit, tags, params = { }), br(), br(), - label(i18n.bookmarkCategoryLabel), - br(), - input({ - type: "text", - name: "category", - id: "category", - placeholder: i18n.bookmarkCategoryPlaceholder, - value: filter === "edit" ? bookmarkToEdit.category || "" : "" - }), - br(), label(i18n.bookmarkTagsLabel), br(), input({ @@ -315,65 +188,64 @@ const renderBookmarkForm = (filter, bookmarkId, bookmarkToEdit, tags, params = { }), br(), br(), + renderClearnetSelector(filter === "edit" ? !!bookmarkToEdit.clearnet : false, i18n), + br(), button({ type: "submit" }, filter === "edit" ? i18n.bookmarkUpdateButton : i18n.bookmarkCreateButton) ) ); }; +const mediaChipFor = (filter, censusM) => (mode) => { + if (mode === filter) return true; + if (!Array.isArray(censusM)) return true; + if (mode === "top") return censusM.some((x) => !x.tribeOrigin); + if (mode === "mine") return censusM.some((x) => String(x.author) === String(userId)); + if (mode === "recent") return censusM.length > 0; + if (mode === "favorites") return censusM.some((x) => x.isFavorite); + if (mode === "bcs") return censusM.some((x) => String(x.title || "").toUpperCase().startsWith("BCS-")); + return true; +}; + exports.bookmarkView = async (bookmarks, filter = "all", bookmarkId = null, params = {}) => { - const title = - filter === "mine" - ? i18n.bookmarkMineSectionTitle - : filter === "create" - ? i18n.bookmarkCreateSectionTitle - : filter === "edit" - ? i18n.bookmarkUpdateSectionTitle - : filter === "recent" - ? i18n.bookmarkRecentSectionTitle - : filter === "top" - ? i18n.bookmarkTopSectionTitle - : filter === "favorites" - ? i18n.bookmarkFavoritesSectionTitle - : i18n.bookmarkAllSectionTitle; + const bookmarkEditWarning = filter === "edit" ? await renderSpreadEditWarning(bookmarkId) : null; + const title = i18n.bookmarkTitle; const q = safeText(params.q || ""); const sort = safeText(params.sort || "recent"); const list = safeArr(bookmarks); + const emptyMod = moduleIsEmpty(list, filter, "all", q); + const mediaChip = mediaChipFor(filter, Array.isArray(params.censusList) ? params.censusList : list); const bookmarkToEdit = bookmarkId ? list.find((b) => b.id === bookmarkId) : null; const tags = bookmarkToEdit && Array.isArray(bookmarkToEdit.tags) ? bookmarkToEdit.tags : []; return template( title, section( - div({ class: "tags-header" }, h2(title), p(i18n.bookmarkDescription)), - (() => { - const { renderReachChip } = require('./clearnet_view'); - const isClearnet = !!(params.viewerPrefs && params.viewerPrefs.clearnetBookmarks); - return div({ class: "shop-title-row" }, renderReachChip(isClearnet, i18n)); - })(), - br(), + div({ class: "tags-header module-header-line" }, h2(title), p(i18n.bookmarkDescription)), div( { class: "filters" }, form( { method: "GET", action: "/bookmarks", class: "ui-toolbar ui-toolbar--filters" }, - input({ type: "hidden", name: "q", value: q }), input({ type: "hidden", name: "sort", value: sort }), - button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, i18n.bookmarkFilterAll), - button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, i18n.bookmarkFilterMine), - button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, i18n.bookmarkFilterTop), - button({ type: "submit", name: "filter", value: "favorites", class: filter === "favorites" ? "filter-btn active" : "filter-btn" }, i18n.bookmarkFilterFavorites), - button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, i18n.bookmarkFilterRecent), + ...(emptyMod ? [] : [ + button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.bookmarkFilterAll).toUpperCase()), + ...(mediaChip("recent") ? [button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.bookmarkFilterRecent).toUpperCase())] : []), + ...(mediaChip("mine") ? [button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.bookmarkFilterMine).toUpperCase())] : []), + ...(mediaChip("favorites") ? [button({ type: "submit", name: "filter", value: "favorites", class: filter === "favorites" ? "filter-btn active" : "filter-btn" }, String(i18n.bookmarkFilterFavorites).toUpperCase())] : []), + ...(mediaChip("top") ? [button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, String(i18n.bookmarkFilterTop).toUpperCase())] : []), + ]), button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.bookmarkCreateButton) ) ) ), section( filter === "edit" || filter === "create" - ? renderBookmarkForm(filter, bookmarkId, bookmarkToEdit || {}, tags, { ...params, filter }) + ? renderBookmarkForm(filter, bookmarkId, bookmarkToEdit || {}, tags, { ...params, filter, spreadWarning: bookmarkEditWarning }) : section( - div( - { class: "bookmarks-search" }, + emptyMod ? null : div( + { class: "bookmarks-search activity-filter-chips activity-toolbar-row" }, + renderModuleStats(list.length), form( { method: "GET", action: "/bookmarks", class: "filter-box" }, input({ type: "hidden", name: "filter", value: filter }), @@ -382,38 +254,38 @@ exports.bookmarkView = async (bookmarks, filter = "all", bookmarkId = null, para { class: "filter-box__controls" }, select( { name: "sort", class: "filter-box__select" }, - option({ value: "recent", selected: sort === "recent" }, i18n.bookmarkSortRecent), - option({ value: "oldest", selected: sort === "oldest" }, i18n.bookmarkSortOldest), - option({ value: "top", selected: sort === "top" }, i18n.bookmarkSortTop) + option({ value: "recent", ...(sort === "recent" ? { selected: true } : {})}, i18n.bookmarkSortRecent), + option({ value: "oldest", ...(sort === "oldest" ? { selected: true } : {})}, i18n.bookmarkSortOldest), + option({ value: "top", ...(sort === "top" ? { selected: true } : {})}, i18n.bookmarkSortTop) ), button({ type: "submit", class: "filter-box__button" }, i18n.bookmarkSearchButton) ) ) ), - div({ class: "bookmark-list" }, renderBookmarkList(list, filter, { q, sort })) + div({ class: "bookmark-list" }, renderBookmarkList(paged(list), filter, { q, sort, spreadMap: params.spreadMap })) ) ) ); }; exports.singleBookmarkView = async (bookmark, filter = "all", comments = [], params = {}) => { + const mediaChip = mediaChipFor(filter, Array.isArray(params.censusList) ? params.censusList : null); const q = safeText(params.q || ""); const sort = safeText(params.sort || "recent"); const returnTo = params.returnTo || buildReturnTo(filter, { q, sort }); const isAuthor = String(bookmark.author) === String(userId); const hasOpinions = Object.keys(bookmark.opinions || {}).length > 0; - const { renderReachChip } = require('./clearnet_view'); - const isClearnet = !!(params.authorPrefs && params.authorPrefs.clearnetBookmarks); + const isClearnet = !!bookmark.clearnet; const lastVisit = bookmark.lastVisit ? moment(bookmark.lastVisit) : null; const lastVisitTxt = lastVisit && lastVisit.isValid() - ? `${lastVisit.format("YYYY/MM/DD HH:mm:ss")} (${lastVisit.fromNow()})` + ? `${lastVisit.format("YYYY/MM/DD HH:mm")} (${lastVisit.fromNow()})` : i18n.noLastVisit; const urlLink = bookmark.url - ? a({ href: bookmark.url, target: "_blank", rel: "noreferrer noopener", class: "bookmark-url" }, bookmark.url) + ? a({ href: safeExternalHref(bookmark.url), target: "_blank", rel: "noreferrer noopener", class: "bookmark-url" }, bookmark.url) : i18n.noUrl; const chips = [ @@ -422,10 +294,7 @@ exports.singleBookmarkView = async (bookmark, filter = "all", comments = [], par ].filter(Boolean); const sideActions = []; - sideActions.push(renderFavoriteToggle(bookmark, returnTo)); - if (bookmark.author && String(bookmark.author) !== String(userId)) { - sideActions.push(renderPMButton(bookmark.author)); - } + sideActions.push(renderPMButton(bookmark.author)); if (isAuthor && !hasOpinions) { sideActions.push(form( { method: "GET", action: `/bookmarks/edit/${encodeURIComponent(bookmark.id)}` }, @@ -433,35 +302,38 @@ exports.singleBookmarkView = async (bookmark, filter = "all", comments = [], par button({ class: "update-btn", type: "submit" }, i18n.bookmarkUpdateButton) )); } - if (isAuthor) { - sideActions.push(form( - { method: "POST", action: `/bookmarks/delete/${encodeURIComponent(bookmark.id)}` }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - button({ class: "delete-btn", type: "submit" }, i18n.bookmarkDeleteButton) - )); - } const tagsNode = renderTags(bookmark.tags); + const detailActions = div({ class: "card-header activity-card-header" }, + renderContentActions(bookmark.id, `/bookmarks/${encodeURIComponent(bookmark.id)}`, { + author: bookmark.author, + favKind: 'bookmarks', + isFavorite: bookmark.isFavorite, + spread: params.spreads || null, + returnTo, + reportTitle: bookmark.title, + deleteAction: isAuthor ? contentDeleteAction('bookmark', bookmark.id) : undefined + }) + ); + const bookmarkSide = div({ class: "tribe-side" }, div({ class: "shop-title-row" }, - h2({ class: "tribe-card-title" }, bookmark.category || bookmark.url || ""), - renderReachChip(isClearnet, i18n) + h2({ class: "tribe-card-title" }, bookmark.url ? urlLink : (bookmark.title || "")), + renderReachChip(isClearnet, i18n, isClearnet ? clearnetItemHref("bookmarks", bookmark.title || bookmark.url, bookmark.id) : null) ), + isAuthor ? renderClearnetSwitch("bookmarks", bookmark.rootId || bookmark.id, isClearnet) : null, + bookmark.title && bookmark.url ? p({ class: "bookmark-subtitle" }, bookmark.title) : null, chips.length ? div({ class: "card-chips-row" }, ...chips) : null, safeText(bookmark.description) - ? p({ class: "tribe-side-description" }, ...renderUrl(bookmark.description)) - : null, - safeText(bookmark.category) - ? renderCardField(i18n.bookmarkCategoryLabel + ":", safeText(bookmark.category)) + ? p({ class: "tribe-side-description" }, ...renderStyledText(bookmark.description)) : null, tagsNode, - div({ class: "card-spread-centered" }, renderSpreadButton(bookmark.id, params.spreads)), sideActions.length ? div({ class: "tribe-side-actions" }, ...sideActions) : null ); const bookmarkMain = div({ class: "tribe-main" }, - renderCardField(i18n.bookmarkUrlLabel + ":", urlLink), + detailActions, renderCardField(i18n.bookmarkLastVisitLabel + ":", lastVisitTxt), (() => { const createdTs = bookmark.createdAt ? new Date(bookmark.createdAt).getTime() : NaN; @@ -469,24 +341,26 @@ exports.singleBookmarkView = async (bookmark, filter = "all", comments = [], par const showUpdated = Number.isFinite(updatedTs) && (!Number.isFinite(createdTs) || updatedTs !== createdTs); return p( { class: "card-footer" }, - span({ class: "date-link" }, `${moment(bookmark.createdAt).format("YYYY/MM/DD HH:mm:ss")} ${i18n.performed} `), + span({ class: "date-link" }, `${moment(bookmark.createdAt).format("YYYY/MM/DD HH:mm")}`), userLink(bookmark.author), showUpdated ? span( { class: "votations-comment-date" }, - ` | ${i18n.bookmarkUpdatedAt}: ${moment(bookmark.updatedAt).format("YYYY/MM/DD HH:mm:ss")}` + ` | ${i18n.bookmarkUpdatedAt}: ${moment(bookmark.updatedAt).format("YYYY/MM/DD HH:mm")}` ) : null ); })(), - renderOpinionsVoting('/bookmarks/opinions', bookmark.id, bookmark.opinions, returnTo, bookmark.opinions_inhabitants), - renderBookmarkCommentsSection(bookmark.id, bookmark.rootId, comments, returnTo) + renderEngagement(bookmark.id, + renderOpinionsVoting('/bookmarks/opinions', bookmark.id, bookmark.opinions, returnTo, bookmark.opinions_inhabitants), + renderBookmarkCommentsSection(bookmark.id, bookmark.rootId, comments, returnTo) + ) ); return template( i18n.bookmarkTitle, section( - div({ class: "tags-header" }, + div({ class: "tags-header module-header-line" }, h2(i18n.bookmarkAllSectionTitle || i18n.bookmarkTitle), p(i18n.bookmarkDescription) ), @@ -494,13 +368,12 @@ exports.singleBookmarkView = async (bookmark, filter = "all", comments = [], par { class: "filters" }, form( { method: "GET", action: "/bookmarks", class: "ui-toolbar ui-toolbar--filters" }, - input({ type: "hidden", name: "q", value: q }), input({ type: "hidden", name: "sort", value: sort }), - button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, i18n.bookmarkFilterAll), - button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, i18n.bookmarkFilterMine), - button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, i18n.bookmarkFilterTop), - button({ type: "submit", name: "filter", value: "favorites", class: filter === "favorites" ? "filter-btn active" : "filter-btn" }, i18n.bookmarkFilterFavorites), - button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, i18n.bookmarkFilterRecent), + button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.bookmarkFilterAll).toUpperCase()), + ...(mediaChip("recent") ? [button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.bookmarkFilterRecent).toUpperCase())] : []), + ...(mediaChip("mine") ? [button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.bookmarkFilterMine).toUpperCase())] : []), + ...(mediaChip("favorites") ? [button({ type: "submit", name: "filter", value: "favorites", class: filter === "favorites" ? "filter-btn active" : "filter-btn" }, String(i18n.bookmarkFilterFavorites).toUpperCase())] : []), + button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, String(i18n.bookmarkFilterTop).toUpperCase()), button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.bookmarkCreateButton) ) ), diff --git a/src/views/calendars_view.js b/src/views/calendars_view.js index 2ae1127..f1990c4 100644 --- a/src/views/calendars_view.js +++ b/src/views/calendars_view.js @@ -1,41 +1,31 @@ -const { div, h2, h3, h4, p, section, button, form, a, span, br, textarea, input, label, select, option, table, tr, td, ul, li } = require("../server/node_modules/hyperaxe") -const { template, i18n, userLink, renderStateChip, renderLifespanChip, renderSpreadButton } = require("./main_views") -const { renderEncryptedChip } = require("./clearnet_view") +const { div, h2, h3, h4, p, section, button, form, a, span, br, hr, textarea, input, label, select, option, table, tr, td, ul, li } = require("../server/node_modules/hyperaxe") +const { renderStyledText } = require("../backend/renderStyledText") +const { clearnetItemHref, template, i18n, userLink, renderStateChip, renderLifespanChip, renderSpreadButton , renderSpreadEditWarning, renderContentActions, renderDocumentActions, renderInviteQrCard, renderSubscriptionBox, renderModuleStatsBy, moduleIsEmpty, contentDeleteAction, paged } = require("./main_views") +const { renderMapEmbed } = require("./maps_view") +const { renderEncryptedChip, renderReachChip, renderClearnetSelector, renderClearnetSwitch, renderTribeOriginChip } = require("./clearnet_view") const moment = require("../server/node_modules/moment") const { config } = require("../server/SSB_server.js") const userId = config.keys.id +const CAL_REACH = ["OPEN", "CLOSED"] +const calReachLabel = (s) => s === "CLOSED" ? (i18n.calendarStatusClosed || "CLOSED") : (i18n.calendarStatusOpen || "OPEN") +const hasPublicInvite = (cal) => Array.isArray(cal.invites) && cal.invites.some(i => i && typeof i === "object" && i.public === true) +const clearnetEligible = (cal) => !!cal && !cal.tribeId && !cal.encrypted && String(cal.status || "").toUpperCase() === "OPEN" && !cal.isClosed && (!cal.contentEncrypted || hasPublicInvite(cal)) +const calClearnetHref = (cal) => clearnetItemHref("calendars", cal.title, cal.rootId) -const renderNoteText = (text) => { - if (!text) return [] - const urlRegex = /(https?:\/\/[^\s]+)/g - const result = [] - let last = 0 - text.replace(urlRegex, (match, _g, offset) => { - if (offset > last) result.push(text.slice(last, offset)) - result.push(a({ href: match, target: "_blank" }, match)) - last = offset + match.length - }) - if (last < text.length) result.push(text.slice(last)) - return result -} +const renderNoteText = (text) => renderStyledText(String(text || "")) -const renderCalendarFavoriteToggle = (cal, returnTo) => - form( - { method: "POST", action: cal.isFavorite ? `/calendars/favorites/remove/${encodeURIComponent(cal.rootId)}` : `/calendars/favorites/add/${encodeURIComponent(cal.rootId)}` }, - returnTo ? input({ type: "hidden", name: "returnTo", value: returnTo }) : null, - button({ type: "submit", class: "tribe-action-btn" }, cal.isFavorite ? (i18n.calendarRemoveFavorite || "Remove Favorite") : (i18n.calendarAddFavorite || "Add Favorite")) - ) - -const renderModeButtons = (currentFilter) => +const renderModeButtons = (currentFilter, emptyMod = false, modesAvail = null) => div({ class: "tribe-mode-buttons" }, - ["all", "mine", "recent", "favorites", "open", "closed"].map(f => + ...(emptyMod ? [] : [ + ["all", "recent", "mine", "favorites", "open", "closed"].filter(f => f === "all" || f === currentFilter || (modesAvail && modesAvail[f] !== false)).map(f => form({ method: "GET", action: "/calendars" }, input({ type: "hidden", name: "filter", value: f }), button({ type: "submit", class: currentFilter === f ? "filter-btn active" : "filter-btn" }, i18n[`calendarFilter${f.charAt(0).toUpperCase() + f.slice(1)}`] || f.toUpperCase()) ) ), + ]), form({ method: "GET", action: "/calendars" }, input({ type: "hidden", name: "filter", value: "create" }), button({ type: "submit", class: "create-button" }, i18n.calendarCreate || "Create Calendar") @@ -55,104 +45,138 @@ const renderCalendarStatusChip = (cal) => { return renderStateChip(variant, icon, label) } -const renderCalendarCard = (cal, spreadInfo) => { - const href = `/calendars/${encodeURIComponent(cal.rootId)}` +const renderCalendarCard = exports.renderCalendarCard = (cal, spreadInfo) => { + const origin = cal.tribeOrigin || null + const href = origin ? origin.href : `/calendars/${encodeURIComponent(cal.rootId)}` const chips = [ renderCalendarStatusChip(cal), - renderEncryptedChip(i18n), - renderLifespanChip(cal.lifetime, i18n) + origin ? renderTribeOriginChip(origin) : renderEncryptedChip(i18n), + renderLifespanChip(cal.lifetime, i18n), + !origin && cal.clearnet === true && clearnetEligible(cal) ? renderReachChip(true, i18n, calClearnetHref(cal)) : null, + cal.subscriptionIn === true + ? renderStateChip("mutuals", "✉", i18n.subscriptionOn) + : (cal.subscriptionIn === false ? renderStateChip("closed", "✉", i18n.subscriptionOff) : null) ].filter(Boolean) return div({ class: "tribe-card" }, + div({ class: "card-header activity-card-header" }, + renderContentActions(cal.rootId, href, origin + ? { author: cal.author, reportTitle: cal.title } + : { spread: spreadInfo || null, author: cal.author, favKind: 'calendars', isFavorite: cal.isFavorite, reportTitle: cal.title, deleteAction: String(cal.author) === String(userId) ? contentDeleteAction('calendar', cal.rootId) : undefined }) + ), div({ class: "tribe-card-body" }, div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, a({ href }, cal.title || "\u2014")) ), chips.length ? div({ class: "card-chips-row" }, ...chips) : null, cal.deadline - ? p({ class: "job-meta-line" }, `${i18n.calendarDeadlineLabel || "Deadline"}: ${moment(cal.deadline).format("YYYY-MM-DD HH:mm")}`) + ? div({ class: "card-field card-field-mb" }, + span({ class: "card-label" }, `${String(i18n.calendarDeadlineLabel).toUpperCase()}: `), + span({ class: "card-value" }, moment(cal.deadline).format("YYYY/MM/DD HH:mm")) + ) : null, - div({ class: "tribe-card-members" }, - span({ class: "tribe-members-count calendar-participants-count" }, `${i18n.calendarParticipantsLabel || "Participants"}: ${cal.participants.length}`) - ), - div({ class: "card-spread-centered" }, renderSpreadButton(cal.rootId, spreadInfo)), - div({ class: "card-visit-btn-centered" }, - a({ href, class: "filter-btn" }, i18n.calendarVisitCalendar || "Visit Calendar") - ) + origin && Array.isArray(cal.dates) && cal.dates.length + ? div(...cal.dates.map(d => p({ class: "job-meta-line" }, `📅 ${moment(d.date).format("YYYY/MM/DD HH:mm")}${d.label ? ` · ${d.label}` : ""}`))) + : null, + origin + ? null + : div({ class: "tribe-card-members" }, + span({ class: "tribe-members-count calendar-participants-count" }, `${i18n.calendarParticipantsLabel || "Participants"}: ${cal.participants.length}`) + ) ) ) } -const renderIntervalBlock = () => - div({ class: "calendar-interval-block" }, +const renderIntervalBlock = (bounds = {}, current = "", until = "") => { + const sel = (v) => (String(current || "") === v ? { selected: true } : {}) + return div({ class: "calendar-interval-block" }, span({ class: "calendar-interval-label" }, i18n.calendarIntervalLabel || "Interval"), - div({ class: "calendar-interval-row" }, - input({ type: "hidden", name: "intervalWeekly", value: "0" }), - label({ class: "calendar-interval-option" }, - input({ type: "checkbox", name: "intervalWeekly", value: "1" }), - " ", i18n.calendarIntervalWeekly || "Weekly" - ), - input({ type: "hidden", name: "intervalMonthly", value: "0" }), - label({ class: "calendar-interval-option" }, - input({ type: "checkbox", name: "intervalMonthly", value: "1" }), - " ", i18n.calendarIntervalMonthly || "Monthly" - ), - input({ type: "hidden", name: "intervalYearly", value: "0" }), - label({ class: "calendar-interval-option" }, - input({ type: "checkbox", name: "intervalYearly", value: "1" }), - " ", i18n.calendarIntervalYearly || "Yearly" - ) + select({ name: "interval", class: "calendar-interval-select" }, + option({ value: "", ...sel("") }, i18n.calendarIntervalNone), + option({ value: "weekly", ...sel("weekly") }, i18n.calendarIntervalWeekly || "Weekly"), + option({ value: "monthly", ...sel("monthly") }, i18n.calendarIntervalMonthly || "Monthly"), + option({ value: "yearly", ...sel("yearly") }, i18n.calendarIntervalYearly || "Yearly") ), span({ class: "calendar-interval-label calendar-interval-until" }, i18n.calendarIntervalUntil || "Until"), - input({ type: "datetime-local", name: "intervalDeadline" }), + input({ + type: "datetime-local", + name: "intervalDeadline", + ...(until ? { value: until } : {}), + ...(bounds.min ? { min: bounds.min } : {}), + ...(bounds.max ? { max: bounds.max } : {}) + }), br() ) +} const renderCreateForm = (calendarToEdit, params) => { const isEdit = !!calendarToEdit const tribeId = (params && params.tribeId) || "" + const draft = isEdit ? calendarToEdit : ((params && params.draft) || {}) + const reachRaw = String((params && params.reach) || draft.status || "").toUpperCase() + const reach = CAL_REACH.includes(reachRaw) ? reachRaw : "OPEN" + const showClearnet = reach === "OPEN" && !tribeId && clearnetEligible({ ...(isEdit ? calendarToEdit : {}), status: reach, isClosed: false }) + const asLocal = (v) => v ? moment(v).format("YYYY-MM-DDTHH:mm") : "" const now = moment().add(1, "minute").format("YYYY-MM-DDTHH:mm") + const deadlineMax = calendarToEdit && calendarToEdit.deadline + ? moment(calendarToEdit.deadline).format("YYYY-MM-DDTHH:mm") + : "" const action = isEdit ? `/calendars/update/${encodeURIComponent(calendarToEdit.rootId)}` : "/calendars/create" const sectionTitle = isEdit ? (i18n.calendarUpdateSectionTitle || "Update Calendar") : (i18n.calendarCreateSectionTitle || "Create New Calendar") return div({ class: "div-center audio-form" }, h2(sectionTitle), + (params && params.spreadWarning) || null, + form({ method: "GET", action: "/calendars" }, + input({ type: "hidden", name: "filter", value: isEdit ? "edit" : "create" }), + isEdit ? input({ type: "hidden", name: "id", value: calendarToEdit.rootId }) : null, + tribeId ? input({ type: "hidden", name: "tribeId", value: tribeId }) : null, + label(i18n.calendarTypeLabel), br(), + div({ class: "apply-row" }, + select({ name: "status", class: "report-category-select" }, + ...CAL_REACH.map(s => option({ value: s, ...(reach === s ? { selected: true } : {}) }, calReachLabel(s))) + ), + button({ type: "submit", class: "create-button" }, i18n.apply || "Apply") + ) + ), + hr({ class: "form-sep" }), + h2({ class: "report-category-fixed" }, calReachLabel(reach)), form({ method: "POST", action }, tribeId ? input({ type: "hidden", name: "tribeId", value: tribeId }) : null, + input({ type: "hidden", name: "status", value: reach }), span(i18n.calendarTitleLabel || "Title"), br(), - input({ type: "text", name: "title", required: true, placeholder: i18n.calendarTitlePlaceholder || "Calendar title...", value: calendarToEdit ? calendarToEdit.title : "" }), - br(), br(), - span(i18n.calendarStatusLabel || "Status"), br(), - select({ name: "status", required: true }, - option({ value: "OPEN", ...((!calendarToEdit || calendarToEdit.status === "OPEN") ? { selected: true } : {}) }, i18n.calendarStatusOpen || "OPEN"), - option({ value: "CLOSED", ...((calendarToEdit && calendarToEdit.status === "CLOSED") ? { selected: true } : {}) }, i18n.calendarStatusClosed || "CLOSED") - ), + input({ type: "text", name: "title", maxlength: "100", required: true, placeholder: i18n.calendarTitlePlaceholder || "Calendar title...", value: draft.title || "" }), br(), br(), span(i18n.calendarDeadlineLabel || "Deadline"), br(), - input({ type: "datetime-local", name: "deadline", required: true, min: now, value: calendarToEdit && calendarToEdit.deadline ? moment(calendarToEdit.deadline).format("YYYY-MM-DDTHH:mm") : "" }), + input({ type: "datetime-local", name: "deadline", required: true, min: now, value: asLocal(draft.deadline) }), br(), br(), span(i18n.calendarTagsLabel || "Tags"), br(), - input({ type: "text", name: "tags", placeholder: i18n.calendarTagsPlaceholder || "tag1, tag2...", value: calendarToEdit && Array.isArray(calendarToEdit.tags) ? calendarToEdit.tags.join(", ") : "" }), + input({ type: "text", name: "tags", placeholder: i18n.calendarTagsPlaceholder || "Enter tags separated by commas", value: Array.isArray(draft.tags) ? draft.tags.join(", ") : "" }), + br(), br(), + span(i18n.mapLocationTitle || "Map Location"), br(), + input({ type: "text", name: "mapUrl", placeholder: i18n.mapUrlPlaceholder || "/maps/MAP_ID", value: draft.mapUrl || "" }), br(), br(), !isEdit ? [ span(i18n.calendarFirstDateLabel || "Date"), br(), - input({ type: "datetime-local", name: "firstDate", required: true, min: now }), + input({ type: "datetime-local", name: "firstDate", required: true, min: now, max: deadlineMax || undefined, ...(draft.firstDate ? { value: asLocal(draft.firstDate) } : {}) }), br(), br(), span(i18n.calendarFormDescription || "Description"), br(), - input({ type: "text", name: "firstDateLabel", placeholder: i18n.calendarDatePlaceholder || "Describe this date..." }), + input({ type: "text", name: "firstDateLabel", placeholder: i18n.calendarDatePlaceholder || "Describe this date...", value: draft.firstDateLabel || "" }), br(), br(), - renderIntervalBlock(), span(i18n.calendarFirstNoteLabel || "Notes"), br(), - textarea({ name: "firstNote", rows: "3", placeholder: i18n.calendarNotePlaceholder || "Add a note..." }), + textarea({ maxlength: "5000", name: "firstNote", rows: "3", placeholder: i18n.calendarNotePlaceholder || "Add a note..." }, draft.firstNote || ""), + br(), br(), + renderIntervalBlock({ min: now, max: deadlineMax || undefined }, draft.interval || "", asLocal(draft.intervalDeadline)), br(), br() ] : null, + showClearnet ? renderClearnetSelector(draft.clearnet === true || String(draft.clearnet || "") === "1", i18n) : null, button({ type: "submit", class: "create-button" }, isEdit ? (i18n.calendarUpdate || "Update") : (i18n.calendarCreate || "Create Calendar")) ) ) } const renderMonthGrid = (year, month, datesMap, calendarId) => { - const DAY_NAMES = ["Mon","Tue","Wed","Thu","Fri","Sat","Sun"] + const DAY_NAMES = String(i18n.calendarDayInitials || "M,T,W,T,F,S,S").split(",") const firstDay = new Date(year, month, 1) const daysInMonth = new Date(year, month + 1, 0).getDate() const startPad = (firstDay.getDay() + 6) % 7 @@ -192,41 +216,39 @@ exports.renderCalendarInvitePage = (code) => { } exports.calendarsView = async (calendars, filter, calendarToEdit, params) => { + if (calendarToEdit) params = { ...(params || {}), spreadWarning: await renderSpreadEditWarning(calendarToEdit.id || calendarToEdit.key || calendarToEdit.rootId) }; const q = (params && params.q) || "" const showForm = filter === "create" || filter === "edit" || !!calendarToEdit - const headerMap = { - all: i18n.calendarAllSectionTitle || "Calendars", - mine: i18n.calendarMineSectionTitle || "Your Calendars", - recent: i18n.calendarRecentSectionTitle || "Recent Calendars", - favorites: i18n.calendarFavoritesSectionTitle || "Favorites", - open: i18n.calendarOpenSectionTitle || "Open Calendars", - closed: i18n.calendarClosedSectionTitle || "Closed Calendars" - } - const headerText = headerMap[filter] || headerMap.all + const emptyMod = moduleIsEmpty(Array.isArray(calendars) ? calendars : [], filter || "all", "all", q) + const headerText = i18n.calendarsTitle || "Calendars" return template( i18n.calendarsTitle || "Calendars", section( - div({ class: "tags-header" }, + div({ class: "tags-header module-header-line" }, h2(headerText), p(i18n.calendarsDescription || "Discover and manage calendars.") ), - renderModeButtons(filter), - q - ? div({ class: "filters" }, - form({ method: "GET", action: "/calendars" }, - input({ type: "text", name: "q", value: q, placeholder: i18n.calendarSearchPlaceholder || "Search calendars..." }), - button({ type: "submit", class: "filter-btn" }, i18n.searchButton || "Search") + renderModeButtons(filter, emptyMod, (params && params.modesAvail) || null), + showForm || emptyMod + ? null + : div({ class: "filters activity-filter-chips activity-toolbar-row" }, + renderModuleStatsBy(calendars, c => c.isClosed ? 'CLOSED' : String(c.status || 'OPEN').toUpperCase(), [{ value: 'OPEN', label: i18n.calendarStatusOpen }, { value: 'CLOSED', label: i18n.calendarStatusClosed }]), + form({ method: "GET", action: "/calendars", class: "filter-box" }, + input({ type: "hidden", name: "filter", value: filter }), + input({ type: "text", name: "q", value: q, placeholder: i18n.calendarSearchPlaceholder || "Search calendars...", class: "filter-box__input" }), + div({ class: "filter-box__controls" }, + button({ type: "submit", class: "filter-box__button" }, i18n.searchButton) + ) ) ) - : null ), section( showForm ? renderCreateForm(calendarToEdit, params) : (calendars.length > 0 - ? div({ class: "tribe-grid" }, ...calendars.map(c => renderCalendarCard(c, params && params.spreadMap && params.spreadMap.get(c.rootId)))) - : p({ class: "no-content" }, i18n.calendarsNoItems || "No calendars found.")) + ? div({ class: "tribe-grid" }, ...paged(calendars).map(c => renderCalendarCard(c, params && params.spreadMap && params.spreadMap.get(c.rootId)))) + : div({ class: "no-content-box" }, p({ class: "no-content" }, i18n.calendarsNoItems || "No calendars found."))) ) ) } @@ -253,39 +275,39 @@ exports.singleCalendarView = async (calendar, dates, notesByDate, params) => { } const tags = Array.isArray(calendar.tags) && calendar.tags.length > 0 - ? div({ class: "tribe-side-tags" }, ...calendar.tags.map(t => a({ href: `/search?query=%23${encodeURIComponent(t)}` }, `#${t} `))) + ? div({ class: "tribe-side-tags" }, ...calendar.tags.map(t => a({ href: `/search?query=%23${encodeURIComponent(t)}`, class: "tag-link" }, `#${t}`))) : null + const subscriptionIn = isAuthor || (calendar.subscription && calendar.subscription.subscribed === true) + const canClearnet = clearnetEligible(calendar) + const isClearnet = canClearnet && calendar.clearnet === true const detailChips = [ renderCalendarStatusChip(calendar), renderEncryptedChip(i18n), - renderLifespanChip(calendar.lifetime, i18n) + renderLifespanChip(calendar.lifetime, i18n), + canClearnet ? renderReachChip(isClearnet, i18n, isClearnet ? calClearnetHref(calendar) : null) : null, + isAuthor && canClearnet ? renderClearnetSwitch("calendars", calendar.rootId, isClearnet) : null, + (isAuthor || isParticipant) + ? renderStateChip(subscriptionIn ? "mutuals" : "closed", "✉", subscriptionIn ? i18n.subscriptionOn : i18n.subscriptionOff) + : null ].filter(Boolean) const calSide = div({ class: "tribe-side" }, div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, calendar.title || "\u2014") ), detailChips.length ? div({ class: "card-chips-row" }, ...detailChips) : null, - div({ class: "card-spread-centered" }, renderSpreadButton(calendar.rootId, params && params.spreads)), - div({ class: "shop-share" }, - span({ class: "tribe-info-label" }, i18n.calendarsShareUrl || "Share URL"), - input({ type: "text", readonly: true, value: shareUrl, class: "shop-share-input" }) - ), table({ class: "tribe-info-table" }, - tr(td({ class: "tribe-info-label" }, i18n.calendarCreated || "Created"), td({ class: "tribe-info-value", colspan: "3" }, moment(calendar.createdAt).format("YYYY-MM-DD"))), + tr(td({ class: "tribe-info-label" }, i18n.calendarCreated || "Created"), td({ class: "tribe-info-value", colspan: "3" }, moment(calendar.createdAt).format("YYYY/MM/DD HH:mm"))), + calendar.deadline ? tr(td({ class: "tribe-info-label" }, i18n.calendarDeadlineLabel || "Deadline"), td({ class: "tribe-info-value", colspan: "3" }, moment(calendar.deadline).format("YYYY/MM/DD HH:mm"))) : null, tr(td({ class: "tribe-info-label" }, i18n.calendarStatusLabel || "Status"), td({ class: "tribe-info-value", colspan: "3" }, renderStatus(calendar))), - tr(td({ class: "tribe-info-value", colspan: "4" }, userLink(calendar.author))), - calendar.deadline ? tr(td({ class: "tribe-info-label" }, i18n.calendarDeadlineLabel || "Deadline"), td({ class: "tribe-info-value", colspan: "3" }, moment(calendar.deadline).format("YYYY-MM-DD HH:mm"))) : null + tr(td({ class: "tribe-info-value", colspan: "4" }, userLink(calendar.author))) ), - div({ class: "tribe-side-actions" }, - renderCalendarFavoriteToggle(calendar, shareUrl), - isAuthor - ? form({ method: "GET", action: "/calendars" }, - input({ type: "hidden", name: "filter", value: "edit" }), - input({ type: "hidden", name: "id", value: calendar.rootId }), - button({ type: "submit", class: "tribe-action-btn" }, i18n.calendarUpdate || "Update") - ) - : null, + tags, + renderMapEmbed(params && params.mapData, calendar.mapUrl), + div({ class: "tribe-card-members" }, + span({ class: "tribe-members-count calendar-participants-count" }, `${i18n.calendarParticipantsLabel || "Participants"}: ${calendar.participants.length}`) + ), + div({ class: "tribe-side-actions calendar-invite-actions" }, isAuthor && calendar.status !== "OPEN" ? form({ method: "POST", action: `/calendars/generate-invite/${encodeURIComponent(calendar.rootId)}` }, button({ type: "submit", class: "tribe-action-btn" }, i18n.tribeGenerateInvite) @@ -295,34 +317,27 @@ exports.singleCalendarView = async (calendar, dates, notesByDate, params) => { if (!(isAuthor && !calendar.tribeId)) return null const invs = Array.isArray(calendar.invites) ? calendar.invites : [] const openInvite = invs.find(inv => typeof inv === "object" && inv && inv.public === true && inv.code) - if (openInvite) return [ + if (openInvite) return div({ class: "calendar-open-invite-block" }, div({ class: "tribe-open-invite" }, span({ class: "card-label" }, i18n.tribeInviteCodeText), - span({ class: "tribe-open-invite-code" }, openInvite.code) + span({ class: "tribe-open-invite-code" }, openInvite.code), + renderInviteQrCard({ qrDataUrl: `/qr-invite-code/calendars/${encodeURIComponent(openInvite.code)}` }) ), form({ method: "POST", action: `/calendars/open-invite/remove/${encodeURIComponent(calendar.rootId)}` }, button({ type: "submit", class: "tribe-action-btn danger-btn" }, i18n.tribeRemoveInvitation) ) - ] + ) return form({ method: "POST", action: `/calendars/open-invite/create/${encodeURIComponent(calendar.rootId)}` }, button({ type: "submit", class: "tribe-action-btn" }, i18n.tribeOpenInvitation) ) })(), - isAuthor - ? form({ method: "POST", action: `/calendars/delete/${encodeURIComponent(calendar.rootId)}` }, - button({ type: "submit", class: "tribe-action-btn danger-btn" }, i18n.calendarDelete || "Delete") - ) - : null, - !isAuthor - ? a({ href: `/pm?to=${encodeURIComponent(calendar.author)}`, class: "tribe-action-btn" }, "PM") - : null, !isAuthor && !isParticipant && calendar.status === "OPEN" ? form({ method: "POST", action: `/calendars/join/${encodeURIComponent(calendar.rootId)}` }, button({ type: "submit", class: "create-button" }, i18n.calendarJoin || "Join Calendar") ) : null, !isAuthor && !isParticipant && calendar.status !== "OPEN" - ? a({ class: "tribe-action-btn", href: "/invites#invites-calendars" }, i18n.tribeEnterInvite) + ? a({ class: "tribe-action-btn", href: "/invites?filter=others" }, i18n.tribeEnterInvite) : null, !isAuthor && isParticipant ? form({ method: "POST", action: `/calendars/leave/${encodeURIComponent(calendar.rootId)}` }, @@ -330,44 +345,61 @@ exports.singleCalendarView = async (calendar, dates, notesByDate, params) => { ) : null ), - tags, - div({ class: "tribe-card-members" }, - span({ class: "tribe-members-count calendar-participants-count" }, `${i18n.calendarParticipantsLabel || "Participants"}: ${calendar.participants.length}`) - ) + (calendar.subscription && (isAuthor || isParticipant)) + ? renderSubscriptionBox({ + target: calendar.rootId || calendar.key, + scope: "calendars", + subscribed: calendar.subscription.subscribed === true, + count: calendar.subscription.count, + isOwner: isAuthor, + returnTo: shareUrl + }) + : null, + isAuthor + ? div({ class: "tribe-side-actions calendar-owner-actions" }, + form({ method: "GET", action: "/calendars" }, + input({ type: "hidden", name: "filter", value: "edit" }), + input({ type: "hidden", name: "id", value: calendar.rootId }), + button({ type: "submit", class: "tribe-action-btn" }, i18n.calendarUpdate || "Update") + ) + ) + : null, + renderDocumentActions('calendars', calendar.rootId) ) const minDate = now.add(1, "minute").format("YYYY-MM-DDTHH:mm") + const calMax = calendar.deadline ? moment(calendar.deadline).format("YYYY-MM-DDTHH:mm") : "" const canAddDate = !calClosed && (calendar.status === "OPEN" || isAuthor) const unifiedForm = canAddDate ? div({ class: "div-center audio-form" }, - h4(i18n.calendarAddEntry || "Add Entry"), form({ method: "POST", action: `/calendars/add-date/${encodeURIComponent(calendar.rootId)}` }, span(i18n.calendarDateLabel || "Date"), br(), - input({ type: "datetime-local", name: "date", required: true, min: minDate }), + input({ type: "datetime-local", name: "date", required: true, min: minDate, max: calMax || undefined }), br(), br(), span(i18n.calendarFormDescription || "Description"), br(), input({ type: "text", name: "label", placeholder: i18n.calendarDatePlaceholder || "Describe this date..." }), br(), br(), - renderIntervalBlock(), - br(), isParticipant ? [ - span(i18n.calendarNoteLabel || "Note (optional)"), br(), - textarea({ name: "text", rows: "3", placeholder: i18n.calendarNotePlaceholder || "Add a note..." }), + span(i18n.calendarNoteLabel), br(), + textarea({ maxlength: "5000", name: "text", rows: "3", placeholder: i18n.calendarNotePlaceholder || "Add a note..." }), br(), br() ] : null, + renderIntervalBlock({ min: minDate, max: calMax || undefined }), + br(), button({ type: "submit", class: "create-button" }, i18n.calendarAddEntry || "Add Entry") ) ) : null - const monthLabel = currentMonth.format("MMMM YYYY") + const monthNames = String(i18n.calendarMonthNames || "").split(",") + const monthLabel = `${monthNames[currentMonth.month()] || currentMonth.format("MMMM")} ${currentMonth.year()}` const calNav = div({ class: "calendar-nav" }, - a({ href: `${shareUrl}?month=${prevMonth}`, class: "filter-btn" }, i18n.calendarMonthPrev || "\u2190 Prev"), + a({ href: `${shareUrl}?month=${prevMonth}`, class: "filter-btn", title: i18n.calendarMonthPrev || "Prev" }, "\u2190"), span({ class: "tribe-info-label" }, monthLabel), - a({ href: `${shareUrl}?month=${nextMonth}`, class: "filter-btn" }, i18n.calendarMonthNext || "Next \u2192") + a({ href: `${shareUrl}?month=${nextMonth}`, class: "filter-btn", title: i18n.calendarMonthNext || "Next" }, "\u2192") ) const grid = renderMonthGrid(year, month, datesMap, calendar.rootId) @@ -391,7 +423,7 @@ exports.singleCalendarView = async (calendar, dates, notesByDate, params) => { ) : null, div({ class: "calendar-date-item-header" }, - `${moment(d.date).format("YYYY-MM-DD HH:mm")}${d.label ? " \u2014 " + d.label : ""}` + `${moment(d.date).format("YYYY/MM/DD HH:mm")}${d.label ? " \u2014 " + d.label : ""}` ), (() => { const visibleNotes = notes.filter(n => n.text && String(n.text).trim()) @@ -424,6 +456,18 @@ exports.singleCalendarView = async (calendar, dates, notesByDate, params) => { : null const calMain = div({ class: "tribe-main" }, + div({ class: "card-header activity-card-header" }, + renderContentActions(calendar.rootId, shareUrl, { + author: calendar.author, + favKind: 'calendars', + isFavorite: calendar.isFavorite, + spread: (params && params.spreads) || null, + returnTo: shareUrl, + reportTitle: calendar.title, + deleteAction: isAuthor ? contentDeleteAction('calendar', calendar.rootId) : undefined + }) + ), + calNav, grid, dayNotesSection, @@ -433,12 +477,65 @@ exports.singleCalendarView = async (calendar, dates, notesByDate, params) => { return template( calendar.title || i18n.calendarsTitle || "Calendar", section( - div({ class: "tags-header" }, + div({ class: "tags-header module-header-line" }, h2(i18n.calendarsTitle || "Calendars"), p(i18n.calendarsDescription || "Discover and manage calendars.") ), - renderModeButtons("all") + renderModeButtons("all", false, (params && params.modesAvail) || null) ), - section(div({ class: "tribe-details" }, calSide, calMain)) + section(div({ class: "tribe-details calendar-details" }, calSide, calMain)) ) } + +exports.clearnetCalendarView = async (calendar, dates, notesByDate = {}) => { + const { escapeHtml: esc, renderRichText, renderKindTag, renderTagChips, renderClearnetPage } = require("./clearnet_view") + const name = calendar.title || i18n.calendarTitle + const fmt = (v) => esc(moment(v).format("YYYY/MM/DD HH:mm")) + const entries = [] + const byKey = new Map() + for (const d of Array.isArray(dates) ? dates : []) { + if (!byKey.has(d.key)) { + const entry = { key: d.key, label: d.label || "", when: [] } + byKey.set(d.key, entry) + entries.push(entry) + } + byKey.get(d.key).when.push(d.date) + } + const entriesHtml = entries.map(e => { + const notes = (notesByDate[e.key] || []).filter(n => n && String(n.text || "").trim()) + return `
+ ${e.label ? `` : ""} +
${e.when.map(w => `📅 ${fmt(w)}`).join("")}
+ ${notes.map(n => `

${renderRichText(n.text)}

`).join("")} +
` + }).join("") + const extraCss = ` +.cn-cal-title{color:var(--fg);margin:0 0 16px 0;font-size:32px;font-weight:700;word-break:break-word} +.cn-cal-meta{display:flex;flex-wrap:wrap;gap:10px;align-items:center;margin-bottom:16px} +.cn-cal-entry{border:1px solid var(--border);border-radius:8px;padding:14px 16px;margin:12px 0;background:var(--bg-elev)} +.cn-cal-entry-label{color:var(--fg);font-weight:600;font-size:16px;margin-bottom:8px;word-break:break-word} +.cn-cal-dates{display:flex;flex-wrap:wrap;gap:6px} +.cn-cal-note{color:var(--fg-soft);line-height:1.5;margin:10px 0 0 0;word-break:break-word} +.cn-cal-empty{color:var(--fg-dim)} +` + const body = ` +

${esc(name)}

+
+ ${renderKindTag("calendar")} + ${calendar.deadline ? `⏳ ${esc(i18n.calendarDeadlineLabel || "Deadline")}: ${fmt(calendar.deadline)}` : ""} +
+ ${renderTagChips(calendar.tags)} +
+ ${entriesHtml || `

${esc(i18n.calendarNoDates || "No dates added yet.")}

`} +` + return renderClearnetPage({ + title: `${name} | Oasis`, + ogTitle: name, + ogDescription: "", + extraCss, + body, + hubFeedId: calendar.author || null + }) +} + +exports.renderIntervalBlock = renderIntervalBlock diff --git a/src/views/campaigns_view.js b/src/views/campaigns_view.js new file mode 100644 index 0000000..d3758fb --- /dev/null +++ b/src/views/campaigns_view.js @@ -0,0 +1,335 @@ +const { div, h2, h3, p, section, button, form, a, span, br, textarea, input, label, select, option, img, progress, table, tr, td, video: videoHyperaxe } = require("../server/node_modules/hyperaxe"); +const { clearnetItemHref, template, i18n, userLink, renderStateChip, renderContentActions, renderSubscriptionBox, renderModuleStats, renderOpinionsVoting, renderEngagement, moduleIsEmpty, contentDeleteAction, paged } = require("./main_views"); +const { renderReachChip, renderClearnetSelector, renderClearnetSwitch } = require("./clearnet_view"); +const { renderCommentsSection } = require("./comments_view"); +const { renderMapEmbed } = require("./maps_view"); +const { renderStyledText } = require("../backend/renderStyledText"); +const moment = require("../server/node_modules/moment"); +const { config } = require("../server/SSB_server.js"); +const sharedState = require("../configs/shared-state"); + +const userId = config.keys.id; +const BASE_FILTERS = ["ALL", "RECENT", "MINE", "SIGNED", "TOP"]; +const STATUSES = ["OPEN", "ACHIEVED", "CLOSED"]; +const CATEGORIES = ["ENVIRONMENT", "RIGHTS", "HEALTH", "EDUCATION", "INFRASTRUCTURE", "CULTURE", "ECONOMY", "OTHER"]; + +const cap = (s) => s.charAt(0) + s.slice(1).toLowerCase(); +const filterLabel = (f) => String(i18n[`campaignFilter${cap(f)}`] || f).toUpperCase(); +const statusLabel = (s) => String(i18n[`campaignStatus${cap(s)}`] || s).toUpperCase(); +const catLabel = (c) => String(i18n[`campaignCategory${cap(c)}`] || c).toUpperCase(); +const campaignHref = (cp) => `/campaigns/${encodeURIComponent(cp.id)}`; +const fmt = (d) => moment(d).format("YYYY/MM/DD HH:mm"); + +const statusChip = (cp) => renderStateChip(cp.closed ? "closed" : "mutuals", cp.closed ? "✗" : "✓", statusLabel(cp.closed ? "CLOSED" : "OPEN")); +const outcomeChip = (cp) => cp.closed ? renderStateChip(cp.achieved ? "mutuals" : "closed", cp.achieved ? "✓" : "✗", cp.achieved ? statusLabel("ACHIEVED") : String(i18n.campaignStatusUnachieved || "UNACHIEVED").toUpperCase()) : null; +const categoryChip = (cp) => renderStateChip("neutral", "", catLabel(cp.category)); +const clearnetEligible = (cp) => String(cp.status || "OPEN").toUpperCase() !== "CLOSED" && !cp.tribeId && !cp.encrypted; + +const renderTags = (tags) => (Array.isArray(tags) && tags.length) + ? div({ class: "card-tags" }, ...tags.map(t => a({ href: `/search?query=%23${encodeURIComponent(t)}`, class: "tag-link" }, `#${t}`))) + : null; + +const renderProgress = (cp) => + div({ class: "tribe-card-members campaign-progress" }, + span({ class: "tribe-members-count" }, `${i18n.campaignSignaturesLabel}: ${cp.signatureCount} / ${cp.goal} (${cp.progress}%)`), + progress({ class: "confirmations-progress", value: String(cp.signatureCount), max: String(cp.goal || 1) }) + ); + +const renderCover = (cp) => { + if (!cp.media || !cp.media.blobId) return null; + if (cp.media.kind === "video") return videoHyperaxe({ class: "campaign-card-cover", src: `/blob/${encodeURIComponent(cp.media.blobId)}`, controls: true, preload: "metadata" }); + return a({ href: campaignHref(cp) }, img({ loading: 'lazy', class: "campaign-card-cover", src: `/blob/${encodeURIComponent(cp.media.blobId)}`, alt: cp.title || "" })); +}; + +const campaignChipFor = (mode, census) => { + if (mode === "ALL") return true; + if (!Array.isArray(census)) return true; + if (mode === "MINE") return census.some(c => String(c.author) === String(userId)); + if (mode === "SIGNED") return census.some(c => Array.isArray(c.signers) && c.signers.includes(userId)); + if (mode === "RECENT") return census.length > 0; + if (mode === "TOP") return census.some(c => c.signatureCount > 0); + if (STATUSES.includes(mode)) return census.some(c => c.status === mode); + if (CATEGORIES.includes(mode)) return census.some(c => c.category === mode); + return census.length > 0; +}; +exports.campaignChipFor = campaignChipFor; + +const renderCampaignCard = (cp, params = {}) => + div({ class: `tribe-card campaign-card campaign-${cp.status.toLowerCase()}` }, + div({ class: "card-header activity-card-header" }, + span(), + renderContentActions(cp.id, campaignHref(cp), { author: cp.author, favKind: "campaigns", isFavorite: cp.isFavorite, reportTitle: cp.title, spread: (params.spreadMap && params.spreadMap.get(cp.id)) || null, deleteAction: cp.isOwner ? contentDeleteAction("campaign", cp.id) : undefined }) + ), + div({ class: "tribe-card-body" }, + renderCover(cp), + div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, a({ href: campaignHref(cp) }, cp.title || "—"))), + div({ class: "card-chips-row" }, statusChip(cp), outcomeChip(cp), categoryChip(cp), cp.clearnet === true && clearnetEligible(cp) ? renderReachChip(true, i18n, clearnetItemHref("campaigns", cp.title, cp.id)) : null), + renderProgress(cp), + cp.deadline ? p({ class: "job-meta-line" }, `${i18n.campaignDeadlineLabel}: ${fmt(cp.deadline)}`) : null + ) + ); + +const chipButtons = (modes, filter, census, labelOf) => + modes.filter(m => m === filter || campaignChipFor(m, census)).map(m => button({ type: "submit", name: "filter", value: m, class: filter === m ? "filter-btn active" : "filter-btn" }, labelOf(m))); + +const renderFilters = (filter, q, census, emptyMod) => + section( + div({ class: "filters" }, + form({ method: "GET", action: "/campaigns", class: "ui-toolbar ui-toolbar--filters" }, + ...(emptyMod ? [] : [ + ...chipButtons(BASE_FILTERS, filter, census, filterLabel), + ...chipButtons(STATUSES, filter, census, statusLabel), + ...chipButtons(CATEGORIES, filter, census, catLabel) + ]), + button({ type: "submit", name: "filter", value: "CREATE", class: "create-button" }, i18n.campaignCreate) + ) + ) + ); + +const renderForm = (cp) => + div({ class: "report-form" }, + h2(cp ? i18n.campaignUpdateSectionTitle : i18n.campaignCreateSectionTitle), + form({ method: "POST", action: cp ? `/campaigns/update/${encodeURIComponent(cp.id)}` : "/campaigns/create", enctype: "multipart/form-data" }, + label(i18n.campaignTitleLabel), br(), + input({ type: "text", name: "title", maxlength: "120", required: true, placeholder: i18n.campaignTitlePlaceholder, value: cp ? cp.title : "" }), br(), + label(i18n.campaignTextLabel), br(), + textarea({ name: "text", rows: 10, maxlength: "8000", placeholder: i18n.campaignTextPlaceholder }, cp ? cp.text : ""), br(), + label(i18n.uploadMedia), br(), + input({ type: "file", name: "blob", accept: "image/*,video/*,audio/*,application/pdf,.torrent" }), br(), br(), + label(i18n.campaignCategoryLabel), br(), + select({ name: "category" }, ...CATEGORIES.map(c => option({ value: c, ...((cp ? cp.category : "OTHER") === c ? { selected: true } : {}) }, catLabel(c)))), br(), br(), + label(i18n.campaignGoalLabel), br(), + input({ type: "number", name: "goal", min: "1", step: "1", required: true, placeholder: String((sharedState.getInhabitantCount && sharedState.getInhabitantCount()) || 100), value: cp ? String(cp.goal) : "" }), br(), br(), + label(i18n.campaignDeadlineLabel), br(), + input({ type: "datetime-local", name: "deadline", min: moment().format("YYYY-MM-DDTHH:mm"), value: cp && cp.deadline ? moment(cp.deadline).format("YYYY-MM-DDTHH:mm") : "" }), br(), br(), + label(i18n.mapLocationTitle || "Map Location"), br(), + input({ type: "text", name: "mapUrl", placeholder: i18n.mapUrlPlaceholder || "/maps/MAP_ID", value: cp ? cp.mapUrl : "" }), br(), + label(i18n.campaignTagsLabel), br(), + input({ type: "text", name: "tags", maxlength: "200", placeholder: i18n.campaignTagsPlaceholder, value: cp ? cp.tags.join(", ") : "" }), br(), br(), + ...(!cp || clearnetEligible(cp) ? [renderClearnetSelector(!!cp && cp.clearnet === true, i18n), br()] : []), + button({ type: "submit", class: "create-button" }, cp ? i18n.campaignUpdate : i18n.campaignCreate) + ) + ); + +exports.campaignsView = async (campaigns, filter = "ALL", params = {}) => { + const list = Array.isArray(campaigns) ? campaigns : []; + const q = String(params.q || "").trim(); + const census = Array.isArray(params.censusList) ? params.censusList : list; + const f = String(filter || "ALL").toUpperCase(); + const isForm = f === "CREATE" || f === "EDIT"; + const emptyMod = !isForm && moduleIsEmpty(census, "ALL", "ALL", q); + return template( + i18n.campaignsTitle, + section( + div({ class: "tags-header module-header-line" }, h2(i18n.campaignsTitle), p(i18n.campaignsDescription)), + renderFilters(isForm ? "ALL" : f, q, isForm ? [] : census, emptyMod) + ), + section( + isForm + ? renderForm(f === "EDIT" ? params.campaign : null) + : [ + emptyMod ? null : div({ class: "filters activity-filter-chips activity-toolbar-row" }, + renderModuleStats(list.length, STATUSES.map(s => ({ label: statusLabel(s), count: list.filter(c => c.status === s).length }))), + form({ method: "GET", action: "/campaigns", class: "filter-box" }, + input({ type: "hidden", name: "filter", value: f }), + input({ type: "text", name: "q", value: q, placeholder: i18n.campaignSearchPlaceholder, class: "filter-box__input" }), + div({ class: "filter-box__controls" }, button({ type: "submit", class: "filter-box__button" }, i18n.searchButton)) + ) + ), + div({ class: "tribe-grid" }, list.length ? paged(list).map(c => renderCampaignCard(c, params)) : p(i18n.campaignNoItems)) + ] + ) + ); +}; + +const renderSignatures = (cp) => + div({ class: "card-section campaign-signatures" }, + h3(`${i18n.campaignSignaturesTitle} (${cp.signatureCount})`), + cp.signatureCount + ? div({ class: "campaign-signature-list" }, ...cp.signatures.slice().reverse().map(s => div({ class: "campaign-signature" }, + div({ class: "campaign-signature-head" }, userLink(s.author), span({ class: "date-link" }, fmt(s.createdAt))), + s.text ? p({ class: "campaign-signature-text" }, s.text) : null + ))) + : p(i18n.campaignNoSignatures) + ); + +const renderUpdate = (cp, u, href, editing) => + div({ class: "campaign-update", id: `update-${encodeURIComponent(u.id)}` }, + div({ class: "emergency-update-head campaign-update-head" }, + div({ class: "card-chips-row" }, + u.edited ? renderStateChip("neutral", "", i18n.campaignUpdateEdited) : null + ), + cp.isOwner + ? div({ class: "tribe-side-actions emergency-update-actions" }, + editing ? null : form({ method: "GET", action: `${href}#update-${encodeURIComponent(u.id)}` }, input({ type: "hidden", name: "editUpdate", value: u.id }), button({ type: "submit", class: "update-btn" }, i18n.campaignUpdate)), + form({ method: "POST", action: `/campaigns/updates/${encodeURIComponent(u.id)}/delete` }, button({ type: "submit", class: "delete-btn" }, i18n.campaignDelete)) + ) + : null + ), + editing + ? form({ method: "POST", action: `/campaigns/updates/${encodeURIComponent(u.id)}/edit`, enctype: "multipart/form-data", class: "campaign-update-form" }, + textarea({ name: "text", rows: 3, maxlength: "3000", required: true, autofocus: true }, u.text), br(), + input({ type: "file", name: "blob", accept: "image/*,video/*,audio/*,application/pdf,.torrent" }), br(), br(), + button({ type: "submit", class: "create-button" }, i18n.campaignUpdate) + ) + : div({ class: "campaign-update-text" }, ...renderStyledText(u.text)), + p({ class: "card-footer" }, span({ class: "date-link" }, `${fmt(u.createdAt)}`), userLink(u.author)) + ); + +const renderUpdates = (cp, href, editUpdate) => + div({ class: "card-section campaign-updates" }, + h3(i18n.campaignUpdatesTitle), + cp.updates.length + ? div({ class: "campaign-updates-list" }, ...cp.updates.slice().reverse().map(u => renderUpdate(cp, u, href, editUpdate === u.id))) + : p(i18n.campaignNoUpdates), + cp.isOwner + ? form({ method: "POST", action: `/campaigns/updates/${encodeURIComponent(cp.id)}`, enctype: "multipart/form-data", class: "campaign-update-form" }, + textarea({ name: "text", rows: 3, maxlength: "3000", required: true, placeholder: i18n.campaignUpdatePlaceholder }), br(), + input({ type: "file", name: "blob", accept: "image/*,video/*,audio/*,application/pdf,.torrent" }), br(), br(), + button({ type: "submit", class: "create-button" }, i18n.campaignAddUpdate) + ) + : null + ); + +exports.singleCampaignView = async (cp, params = {}) => { + const href = campaignHref(cp); + const canClearnet = clearnetEligible(cp); + const isClearnet = canClearnet && !!cp.clearnet; + const census = Array.isArray(params.censusList) ? params.censusList : []; + const sideActions = []; + const ownerActions = cp.isOwner + ? [ + form({ method: "GET", action: "/campaigns" }, input({ type: "hidden", name: "filter", value: "edit" }), input({ type: "hidden", name: "id", value: cp.id }), button({ type: "submit", class: "update-btn" }, i18n.campaignUpdate)) + ].filter(Boolean) + : []; + const side = div({ class: "tribe-side" }, + div({ class: "card-header activity-card-header" }, + renderContentActions(cp.id, href, { author: cp.author, favKind: "campaigns", isFavorite: cp.isFavorite, reportTitle: cp.title, spread: params.spread || null, returnTo: href, deleteAction: cp.isOwner ? contentDeleteAction("campaign", cp.id) : undefined }) + ), + div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, cp.title)), + div({ class: "card-chips-row" }, + statusChip(cp), outcomeChip(cp), categoryChip(cp), cp.proposalId ? renderStateChip("neutral", "⚖", String(i18n.campaignElevated).toUpperCase()) : null, + renderReachChip(isClearnet, i18n, isClearnet ? clearnetItemHref("campaigns", cp.title, cp.id) : null), + cp.isOwner && canClearnet ? renderClearnetSwitch("campaigns", cp.rootId || cp.id, isClearnet) : null + ), + table({ class: "tribe-info-table jobs-info-table" }, + tr( + td({ class: "tribe-info-label" }, i18n.createdAtLabel || "Created at"), + td({ class: "tribe-info-value", colspan: "3" }, fmt(cp.createdAt)) + ), + cp.deadline + ? tr( + td({ class: "tribe-info-label" }, i18n.campaignDeadlineLabel), + td({ class: "tribe-info-value", colspan: "3" }, fmt(cp.deadline)) + ) + : null, + tr( + td({ class: "tribe-info-value", colspan: "4" }, userLink(cp.author)) + ) + ), + div({ class: "campaign-qr" }, + img({ src: `${href}/qr.png`, alt: i18n.campaignQrAlt, class: "campaign-qr-img" }), + p({ class: "campaign-qr-caption" }, i18n.campaignQrCaption) + ), + renderProgress(cp), + renderMapEmbed(params.mapData, cp.mapUrl), + sideActions.length ? div({ class: "tribe-side-actions" }, ...sideActions) : null, + div({ class: "campaign-signed" }, + cp.signed ? renderStateChip("mutuals", "✓", i18n.campaignSigned) : renderStateChip("closed", "✗", i18n.campaignNotSigned) + ), + cp.canElevate + ? div({ class: "tribe-side-actions campaign-elevate" }, form({ method: "POST", action: `/campaigns/elevate/${encodeURIComponent(cp.id)}` }, button({ type: "submit", class: "tribe-action-btn" }, String(i18n.campaignElevate).toUpperCase()))) + : null, + cp.isOwner && !cp.expired + ? div({ class: "tribe-side-actions housing-status-row" }, + span({ class: "card-label" }, `${i18n.statusLabel || "Status"}: `), + statusChip(cp), + form({ method: "POST", action: `/campaigns/status/${encodeURIComponent(cp.id)}`, class: "inline-form" }, + button({ class: "tribe-action-btn", type: "submit", name: "status", value: cp.status === "CLOSED" ? "OPEN" : "CLOSED" }, + String(cp.status === "CLOSED" ? i18n.campaignReopen : i18n.campaignClose).toUpperCase()) + ) + ) + : null, + div({ class: "doc-export-actions" }, + form({ method: "GET", action: `${href}/pdf` }, button({ type: "submit", class: "filter-btn" }, i18n.campaignGenerateReport)), + form({ method: "POST", action: `${href}/share` }, button({ type: "submit", class: "filter-btn" }, i18n.sharePm)) + ), + renderTags(cp.tags), + ownerActions.length ? div({ class: "tribe-side-actions owner-actions" }, ...ownerActions) : null, + params.subscription + ? renderSubscriptionBox({ target: cp.id, scope: "campaigns", subscribed: params.subscription.subscribed, count: params.subscription.count, isOwner: cp.isOwner, returnTo: href, canWrite: cp.isOwner }) + : null + ); + const main = div({ class: "tribe-main" }, + cp.text ? div({ class: "campaign-body" }, ...renderStyledText(cp.text)) : null, + cp.canSign + ? div({ class: "card-section campaign-sign" }, + h3(i18n.campaignSignTitle), + form({ method: "POST", action: `/campaigns/sign/${encodeURIComponent(cp.id)}`, class: "campaign-sign-form" }, + textarea({ name: "text", rows: 3, maxlength: "500", placeholder: i18n.campaignSignPlaceholder }), br(), br(), + button({ type: "submit", class: "create-button" }, i18n.campaignSign) + ) + ) + : null, + renderUpdates(cp, href, String(params.editUpdate || "")), + renderSignatures(cp), + renderEngagement(cp.id, + renderOpinionsVoting("/campaigns/opinions", cp.id, cp.opinions, href, cp.opinions_inhabitants), + renderCommentsSection({ action: `/campaigns/${encodeURIComponent(cp.id)}/comments`, comments: params.comments || [], returnTo: href }) + ) + ); + return template( + cp.title, + section(div({ class: "tags-header module-header-line" }, h2(i18n.campaignsTitle), p(i18n.campaignsDescription)), renderFilters("ALL", "", census, census.length === 0)), + section(div({ class: "tribe-details" }, side, main)) + ); +}; + +exports.clearnetCampaignView = async (cp, params = {}) => { + const { escapeHtml: esc, renderRichText, renderKindTag, renderTagChips, blobUrl: cnBlob, renderClearnetPage } = require("./clearnet_view"); + const title = esc(cp.title || i18n.campaignLabel); + const desc = renderRichText(cp.text || ""); + const point = params.mapPoint || null; + const cover = cp.media && cp.media.kind === "image" ? cnBlob(cp.media.blobId) : null; + const goal = Number(cp.goal) || 0; + const signed = Number(cp.signatureCount) || 0; + const pct = Math.max(0, Math.min(100, Number(cp.progress) || 0)); + const updates = (Array.isArray(cp.updates) ? cp.updates : []).filter(u => String(u.author) === String(cp.author)); + const extraCss = ` +.cn-campaign-title{color:var(--fg);margin:0 0 16px 0;font-size:32px;font-weight:700} +.cn-campaign-meta{display:flex;flex-wrap:wrap;gap:10px;margin-bottom:20px} +.cn-campaign-meta-item{background:var(--bg-sub);border:1px solid var(--border);border-radius:6px;padding:8px 14px;font-size:14px;color:var(--fg-soft);display:inline-flex;align-items:center;gap:6px} +.cn-campaign-progress{margin:0 0 20px 0} +.cn-campaign-progress-label{color:var(--fg);font-weight:700;font-size:15px;margin-bottom:8px} +.cn-campaign-progress progress{width:100%;height:14px;accent-color:var(--fg)} +.cn-campaign-desc{color:var(--fg-soft);line-height:1.6;font-size:15px;margin:0 0 20px 0;word-break:break-word} +.cn-campaign-update{border:1px solid var(--border);border-radius:8px;padding:12px 16px;margin:12px 0;background:var(--bg-elev)} +.cn-campaign-update-date{color:var(--fg-dim);font-size:12px;margin-bottom:6px} +.cn-campaign-update-text{color:var(--fg-soft);line-height:1.5;word-break:break-word} +`; + const body = ` +

${title}

+
+ ${renderKindTag("campaign")} + ${esc(statusLabel(cp.achieved ? "ACHIEVED" : "OPEN"))} + ${esc(catLabel(String(cp.category || "OTHER")))} + ${cp.createdAt ? `📅 ${esc(fmt(cp.createdAt))}` : ""} + ${cp.deadline ? `⏳ ${esc(i18n.campaignDeadlineLabel)}: ${esc(fmt(cp.deadline))}` : ""} + ${point ? `📍 ${point.title ? `${esc(point.title)} · ` : ""}${point.lat}, ${point.lng}` : ""} +
+ ${goal > 0 ? `
${esc(i18n.campaignSignaturesLabel)}: ${signed} / ${goal} (${pct}%)
` : ""} +
+ ${desc ? `
${desc}
` : ""} + ${updates.length ? `

${esc(i18n.campaignUpdatesTitle)}

${updates.slice().reverse().map(u => `
${esc(fmt(u.updatedAt || u.createdAt))}
${renderRichText(u.text || "")}
`).join("")}` : ""} + ${renderTagChips(cp.tags)} +`; + return renderClearnetPage({ + title: `${cp.title || i18n.campaignLabel} | Oasis`, + ogTitle: cp.title || i18n.campaignLabel, + ogDescription: String(cp.text || "").replace(/!?\[[^\]]*\]\(\s*&[^)\s]+\s*\)/g, "").trim(), + ogImage: cover, + extraCss, + body, + hubFeedId: cp.author || null + }); +}; diff --git a/src/views/chats_view.js b/src/views/chats_view.js index a3c2c00..bb8202f 100644 --- a/src/views/chats_view.js +++ b/src/views/chats_view.js @@ -1,9 +1,11 @@ -const { div, h2, p, section, button, form, a, span, textarea, br, input, label, select, option, img, table, tr, td, ul, li } = require("../server/node_modules/hyperaxe") -const { template, i18n, userLink, renderStateChip, renderLifespanChip, renderSpreadButton } = require("./main_views") +const { div, h2, p, section, button, form, a, span, textarea, br, input, label, select, option, img, table, tr, td, ul, li, details, summary, video: videoHyperaxe, audio: audioHyperaxe } = require("../server/node_modules/hyperaxe") +const { template, i18n, userLink, userLinkLabel, renderStateChip, renderLifespanChip, renderSpreadButton, renderContentActions, renderInviteQrCard, renderSubscriptionBox, renderModuleStatsBy, moduleIsEmpty, renderTorrentDownload, torrentDownloadHref, contentDeleteAction, paged } = require("./main_views") const { renderEncryptedChip } = require("./clearnet_view") +const { renderResults, renderBallot, outcomeOf } = require("./polls_view") const moment = require("../server/node_modules/moment") const { config } = require("../server/SSB_server.js") -const { renderUrl } = require("../backend/renderUrl") +const { renderStyledText } = require("../backend/renderStyledText") +const { renderZoomableImage } = require("./gallery_view") const userId = config.keys.id const safeArr = (v) => (Array.isArray(v) ? v : []) @@ -17,13 +19,22 @@ const ALL_CATS = [...CAT_BLOCK1, ...CAT_BLOCK2, ...CAT_BLOCK3] const catKey = (c) => "forumCat" + String(c || "").replace(/\./g, "").replace(/[\s-]/g, "").toUpperCase() const catLabel = (c) => i18n[catKey(c)] || c +const blobSrcOf = (value) => { + const s = String(value || "").trim() + if (s.startsWith("/blob/sealed/")) return s + if (s.startsWith("&")) return `/blob/${encodeURIComponent(s)}` + const mImg = s.match(/!\[[^\]]*\]\(\s*(&[^)\s]+\.sha256)\s*\)/) + return mImg ? `/blob/${encodeURIComponent(mImg[1])}` : null +} + const renderMediaBlob = (value, fallbackSrc = null, attrs = {}) => { if (!value) return fallbackSrc ? img({ src: fallbackSrc, ...attrs }) : null const s = String(value).trim() if (!s) return fallbackSrc ? img({ src: fallbackSrc, ...attrs }) : null - if (s.startsWith("&")) return img({ src: `/blob/${encodeURIComponent(s)}`, ...attrs }) - const mImg = s.match(/!\[[^\]]*\]\(\s*(&[^)\s]+\.sha256)\s*\)/) - if (mImg) return img({ src: `/blob/${encodeURIComponent(mImg[1])}`, ...attrs }) + if (s.startsWith("/blob/sealed/")) return img({ loading: 'lazy', src: s, ...attrs }) + if (s.startsWith("&")) return img({ loading: 'lazy', src: `/blob/${encodeURIComponent(s)}`, ...attrs }) + const mImg = s.match(/!\[[^\]]*\]\(\s*(&[^)\s]+\.sha256|\/blob\/sealed\/[^)\s]+)\s*\)/) + if (mImg) return img({ loading: 'lazy', src: mImg[1].startsWith("/blob/sealed/") ? mImg[1] : `/blob/${encodeURIComponent(mImg[1])}`, ...attrs }) return fallbackSrc ? img({ src: fallbackSrc, ...attrs }) : null } @@ -35,14 +46,16 @@ const buildReturnTo = (filter, params = {}) => { return `/chats?${parts.join("&")}` } -const renderModeButtons = (currentFilter) => +const renderModeButtons = (currentFilter, emptyMod = false, modesAvail = null) => div({ class: "tribe-mode-buttons" }, - ["all", "mine", "recent", "favorites", "open", "closed"].map(f => + ...(emptyMod ? [] : [ + ["all", "recent", "mine", "favorites", "open", "closed"].filter(f => f === "all" || f === currentFilter || (modesAvail && modesAvail[f] !== false)).map(f => form({ method: "GET", action: "/chats" }, input({ type: "hidden", name: "filter", value: f }), button({ type: "submit", class: currentFilter === f ? "filter-btn active" : "filter-btn" }, i18n[`chatFilter${f.charAt(0).toUpperCase() + f.slice(1)}`] || f.toUpperCase()) ) ), + ]), form({ method: "GET", action: "/chats" }, input({ type: "hidden", name: "filter", value: "create" }), button({ type: "submit", class: "create-button" }, i18n.chatCreate) @@ -59,36 +72,44 @@ const renderChatStatusChip = (status) => { return renderStateChip(variant, icon, label) } -const renderChatCard = (chat, filter, params = {}) => { +const renderChatCard = exports.renderChatCard = (chat, filter, params = {}) => { const chips = [ renderChatStatusChip(chat.status), renderEncryptedChip(i18n), - renderLifespanChip(chat.lifetime, i18n) + renderLifespanChip(chat.lifetime, i18n), + chat.subscriptionIn === true + ? renderStateChip("mutuals", "✉", i18n.subscriptionOn) + : (chat.subscriptionIn === false ? renderStateChip("closed", "✉", i18n.subscriptionOff) : null) ].filter(Boolean) + const href = `/chats/${encodeURIComponent(chat.key)}` return div({ class: "tribe-card" }, + div({ class: "card-header activity-card-header" }, + renderContentActions(chat.key, href, { + spread: params.spread || null, + author: chat.author, + favKind: 'chats', + isFavorite: chat.isFavorite, + returnTo: buildReturnTo(filter, params), + reportTitle: chat.title, + deleteAction: String(chat.author) === String(userId) ? contentDeleteAction('chat', chat.key) : undefined + }) + ), div({ class: "tribe-card-image-wrapper" }, - a({ href: `/chats/${encodeURIComponent(chat.key)}` }, + a({ href: `/chats/${encodeURIComponent(chat.key)}#chat-latest` }, renderMediaBlob(chat.image, "/assets/images/default-avatar.png", { class: "tribe-card-hero-image" }) ) ), div({ class: "tribe-card-body" }, div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, - a({ href: `/chats/${encodeURIComponent(chat.key)}` }, chat.title || i18n.chatUntitled) + a({ href: `/chats/${encodeURIComponent(chat.key)}#chat-latest` }, chat.title || i18n.chatUntitled) ) ), chips.length ? div({ class: "card-chips-row" }, ...chips) : null, chat.description ? p({ class: "tribe-card-description" }, chat.description) : null, div({ class: "tribe-card-members" }, span({ class: "tribe-members-count" }, `${i18n.chatParticipants}: ${safeArr(chat.members).length}`) - ), - (() => { - const btn = renderSpreadButton(chat.key, (params && params.spreadMap && params.spreadMap.get(chat.key)) || (params && params.spreads)); - return btn ? div({ class: "card-spread-left" }, btn) : null; - })(), - div({ class: "visit-btn-centered" }, - a({ href: `/chats/${encodeURIComponent(chat.key)}`, class: "filter-btn" }, i18n.chatVisitChat) ) ) ) @@ -104,19 +125,18 @@ const renderChatForm = (filter, chat = {}, params = {}) => { input({ type: "hidden", name: "returnTo", value: returnTo }), tribeId ? input({ type: "hidden", name: "tribeId", value: tribeId }) : null, span(i18n.title || "Title"), br(), - input({ type: "text", name: "title", required: true, placeholder: i18n.chatTitlePlaceholder, value: chat.title || "" }), br(), br(), + input({ type: "text", name: "title", maxlength: "100", required: true, placeholder: i18n.chatTitlePlaceholder, value: chat.title || "" }), br(), br(), span(i18n.chatDescription), br(), - textarea({ name: "description", rows: 4, placeholder: i18n.chatDescriptionPlaceholder }, chat.description || ""), br(), br(), - span(i18n.chatImageLabel || "Select an image file (.jpeg, .jpg, .png, .gif)"), br(), + textarea({ name: "description", rows: 4, maxlength: "1000", placeholder: i18n.chatDescriptionPlaceholder }, chat.description || ""), br(), br(), + span(i18n.uploadMedia), br(), input({ type: "file", name: "image", accept: "image/*" }), br(), br(), span(i18n.chatCategory), br(), select({ name: "category" }, - option({ value: "" }, "\u2014"), ALL_CATS.map(cat => - option({ value: cat, ...(chat.category === cat ? { selected: true } : {}) }, catLabel(cat)) + option({ value: cat, ...((chat.category || "GENERAL") === cat ? { selected: true } : {}) }, catLabel(cat)) ) ), br(), br(), - span(i18n.chatStatusLabel || "Status"), br(), + span(i18n.chatTypeLabel), br(), select({ name: "status" }, option({ value: "OPEN", ...((!chat.status || chat.status === "OPEN") ? { selected: true } : {}) }, i18n.chatStatusOpen), option({ value: "INVITE-ONLY", ...(chat.status === "INVITE-ONLY" ? { selected: true } : {}) }, i18n.chatStatusInviteOnly) @@ -128,34 +148,169 @@ const renderChatForm = (filter, chat = {}, params = {}) => { ) } -const renderMessageText = (text) => { - if (!text) return span({ class: "chat-message-text" }, "") - const lines = String(text).split("\n") - const nodes = [] - lines.forEach((line, idx) => { - const rendered = renderUrl(line) - nodes.push(...rendered) - if (idx < lines.length - 1) nodes.push(br()) - }) - return span({ class: "chat-message-text" }, ...nodes) +const renderMessageText = (text) => span({ class: "chat-message-text" }, ...renderStyledText(String(text || ""))) + +const chatActivityTs = (c) => Math.max( + Number(c.lastMsgAt || 0), + Date.parse(c.updatedAt || "") || 0, + Date.parse(c.createdAt || "") || 0 +) + +const renderChatTopics = (chats, activeKey) => + div({ class: "chat-topics" }, + div({ class: "chat-topics-head" }, + a({ href: "/chats", class: "chat-topics-title-link" }, i18n.chatsTitle), + a({ href: "/chats?filter=create", class: "filter-btn chat-topics-new" }, "+") + ), + ...safeArr(chats).slice().sort((a, b) => chatActivityTs(b) - chatActivityTs(a)).map(c => { + const key = c.rootId || c.key + const active = activeKey && String(key) === String(activeKey) + return a({ href: `/chats/${encodeURIComponent(key)}#chat-latest`, class: active ? "chat-topic chat-topic-active" : "chat-topic" }, + renderMediaBlob(c.image, "/assets/images/default-avatar.png", { class: "chat-topic-thumb" }), + span({ class: "chat-topic-title" }, c.title || i18n.chatUntitled), + span({ class: "chat-topic-meta" }, `\u2689 ${safeArr(c.members).length}`), + c.lastMsg && (c.lastMsg.text || c.lastMsg.author) ? (() => { + const who = userLinkLabel(c.lastMsg.author) || '' + const shortWho = who.length > 20 ? who.slice(0, 10) + '\u2026' : who + return span({ class: "chat-topic-last" }, + span({ class: "chat-topic-last-author" }, shortWho + ': '), + String(c.lastMsg.text || '').replace(/\s+/g, ' ').slice(0, 80) + ) + })() : null + ) + }) + ) + +const renderSenderLink = (author, isOwner) => + author + ? a({ href: `/author/${encodeURIComponent(author)}`, class: isOwner ? "chat-bubble-sender chat-bubble-sender-owner" : "chat-bubble-sender" }, userLinkLabel(author)) + : null + +const renderChatPoll = (poll, chat) => { + const isSelf = String(poll.author) === String(userId) + const isOwner = String(poll.author) === String(chat.author) + const showResults = poll.hasVoted || poll.status === "CLOSED" + return div({ class: isSelf ? "chat-bubble-row chat-bubble-row-self" : "chat-bubble-row" }, + div({ class: isSelf ? "chat-message chat-message-self chat-poll" : "chat-message chat-poll" }, + isSelf ? null : renderSenderLink(poll.author, isOwner), + div({ class: "chat-poll-head" }, + span({ class: "chat-poll-tag" }, i18n.pollInChat), + poll.anonymous ? span({ class: "chat-poll-tag" }, i18n.pollAnonymous) : null, + poll.multiple ? span({ class: "chat-poll-tag" }, i18n.pollMultiple) : null, + poll.status === "CLOSED" ? span({ class: "chat-poll-tag" }, i18n.pollStatusClosed) : null + ), + poll.undecryptable + ? p({ class: "chat-poll-question" }, i18n.chatAccessDenied) + : [ + p({ class: "chat-poll-question" }, poll.question), + showResults ? renderResults(poll) : null, + (() => { + const canClose = String(poll.author) === String(userId) && poll.status === "OPEN"; + const closeFormId = `poll-close-${String(poll.id).replace(/[^a-zA-Z0-9]/g, "")}`; + const ballot = renderBallot(poll, `/chats/${encodeURIComponent(chat.key)}`, "/polls", + canClose ? [button({ type: "submit", form: closeFormId, class: "danger-btn" }, i18n.pollCloseButton)] : null); + return div({ class: "chat-poll-actions" }, + ballot, + canClose + ? form({ id: closeFormId, method: "POST", action: `/polls/close/${encodeURIComponent(poll.id)}`, class: "chat-poll-close-form" }, + input({ type: "hidden", name: "returnTo", value: `/chats/${encodeURIComponent(chat.key)}` }), + ballot ? null : button({ type: "submit", class: "danger-btn" }, i18n.pollCloseButton) + ) + : null + ); + })(), + div({ class: "chat-poll-meta" }, + span({ class: "card-label" }, `${i18n.pollVoters}: `), + span({ class: "card-value" }, String(poll.totalVoters)), + span({ class: "card-label" }, ` · ${i18n.pollOutcome}: `), + span({ class: "card-value" }, outcomeOf(poll)) + ) + ], + span({ class: "chat-bubble-time" }, moment(poll.createdAt).format("HH:mm")) + ) + ) } -const renderMessage = (msg, chatAuthor) => { - const isAuthor = String(msg.author) === String(chatAuthor) +const REACTIONS = [ + ["up", "👍"], + ["heart", "❤️"], + ["laugh", "😂"], + ["down", "👎"] +] + +const anchorIdOf = (key) => "msg-" + String(key || "").replace(/[^a-zA-Z0-9]/g, "") + +const renderMessage = (msg, chat, opts = {}) => { + const isAuthor = String(msg.author) === String(chat.author) const isSelf = String(msg.author) === String(userId) - const dateStr = moment(msg.createdAt).format("YYYY/MM/DD HH:mm") - const shortId = msg.author ? "@" + msg.author.slice(1, 9) + "\u2026" : "?" - const authorLink = msg.author ? userLink(msg.author) : span("?") + const imageSrc = blobSrcOf(msg.image) + const mime = String(msg.mimeType || "") + const imageNode = imageSrc + ? (mime.startsWith("video/") + ? videoHyperaxe({ controls: true, class: "post-video chat-message-media", src: imageSrc, type: mime, preload: "metadata" }) + : mime.startsWith("audio/") + ? audioHyperaxe({ controls: true, class: "post-audio chat-message-media", src: imageSrc, type: mime, preload: "metadata" }) + : mime === "application/pdf" + ? a({ href: imageSrc, target: "_blank", rel: "noopener", class: "filter-btn chat-message-file" }, "📄 PDF") + : mime.includes("bittorrent") || mime === "application/x-torrent" + ? renderTorrentDownload(imageSrc, { class: "filter-btn chat-message-file" }) + : renderZoomableImage(imageSrc, { imgClass: "chat-message-image" })) + : (msg.image ? renderMediaBlob(msg.image, null, { class: "chat-message-image" }) : null) - const imageNode = msg.image ? renderMediaBlob(msg.image, null, { class: "chat-message-image" }) : null + const counts = (msg.reactions && msg.reactions.counts) || {} + const mine = (msg.reactions && msg.reactions.mine) || {} + const reactionNodes = REACTIONS.map(([code, icon]) => { + const n = Number(counts[code] || 0) + const label = n > 0 ? `${icon} ${n}` : icon + if (!opts.canWrite) return n > 0 ? span({ class: "chat-react-btn" }, label) : null + return form({ method: "POST", action: `/chats/${encodeURIComponent(chat.key)}/react`, class: "chat-react-form" }, + input({ type: "hidden", name: "target", value: msg.key }), + input({ type: "hidden", name: "emoji", value: code }), + button({ type: "submit", class: mine[code] ? "chat-react-btn chat-react-active" : "chat-react-btn" }, label) + ) + }).filter(Boolean) - return div({ class: isSelf ? "chat-message chat-message-self" : isAuthor ? "chat-message chat-message-author" : "chat-message" }, - div({ class: "chat-message-meta" }, - span({ class: "chat-message-sender" }, authorLink), - span({ class: "chat-message-date" }, ` [ ${dateStr} ]`) - ), - imageNode ? div({ class: "chat-message-image-wrap" }, imageNode) : null, - renderMessageText(msg.text || "") + const replyLink = opts.canWrite + ? a({ href: `/chats/${encodeURIComponent(chat.key)}?reply=${encodeURIComponent(msg.key)}#chat-message-form`, class: "chat-react-btn chat-reply-btn", title: i18n.chatReply }, "↩") + : null + + const pinBtn = opts.isChatAuthor + ? form({ method: "POST", action: `/chats/${encodeURIComponent(chat.key)}/pin`, class: "chat-react-form chat-pin-toggle" }, + input({ type: "hidden", name: "target", value: msg.key }), + button({ type: "submit", class: msg.pinned ? "chat-react-btn chat-react-active" : "chat-react-btn", title: msg.pinned ? i18n.chatUnpin : i18n.chatPin }, "📌") + ) + : null + + const quoteText = String((msg.reply && msg.reply.text) || "").trim() + const quote = msg.reply + ? a({ href: `#${anchorIdOf(msg.replyTo)}`, class: "chat-reply-quote" }, + span({ class: "chat-reply-arrow" }, "↩ "), + span({ class: "chat-reply-text" }, quoteText ? `${quoteText}${quoteText.length >= 120 ? "…" : ""}` : userLinkLabel(msg.reply.author)) + ) + : null + + const timeNode = span({ class: "chat-bubble-time" }, moment(msg.createdAt).format("HH:mm")) + const actionsRow = (reactionNodes.length || replyLink) + ? div({ class: "chat-msg-actions" }, ...reactionNodes, replyLink, + span({ class: replyLink ? "chat-bubble-time chat-time-inline" : "chat-bubble-time chat-time-inline chat-time-push" }, moment(msg.createdAt).format("HH:mm"))) + : null + + const bubbleClass = [ + "chat-message", + isSelf ? "chat-message-self" : (isAuthor ? "chat-message-author" : ""), + msg.pinned ? "chat-message-pinned" : "", + pinBtn ? "chat-has-pin-toggle" : "" + ].filter(Boolean).join(" ") + + return div({ id: anchorIdOf(msg.key), class: isSelf ? "chat-bubble-row chat-bubble-row-self" : "chat-bubble-row" }, + div({ class: bubbleClass }, + pinBtn, + isSelf ? null : renderSenderLink(msg.author, isAuthor), + quote, + renderMessageText(msg.text || ""), + imageNode ? div({ class: "chat-message-image-wrap" }, imageNode) : null, + actionsRow || timeNode + ) ) } @@ -174,37 +329,45 @@ exports.renderChatInvitePage = (code) => { exports.chatsView = async (chats, filter, chatToEdit = null, params = {}) => { const q = safeText(params.q || "") const list = safeArr(chats) + const emptyMod = moduleIsEmpty(list, filter || "all", "all", q) const isForm = filter === "create" || filter === "edit" - const chatHeaderMap = { - all: i18n.chatsTitle, - mine: i18n.chatMineSectionTitle || "Your Chats", - recent: i18n.chatRecentTitle || "Recent Chats", - favorites: i18n.chatFavoritesTitle || "Favorites", - open: i18n.chatOpenTitle || "Open Chats", - closed: i18n.chatClosedTitle || "Closed Chats" + const headerText = i18n.chatsTitle + + if (params.workspace && !isForm) { + return template( + i18n.chatsTitle, + section({ class: "chat-workspace-section chat-workspace-list" }, + div({ class: "chat-workspace" }, + renderChatTopics(list, null), + div({ class: "chat-workspace-main chat-workspace-empty" }, + p({ class: "chat-no-messages" }, list.length ? i18n.chatPickChat : i18n.chatNoneYet) + ) + ) + ) + ) } - const headerText = chatHeaderMap[filter] || i18n.chatsTitle return template( i18n.chatsTitle, section( - div({ class: "tags-header" }, + div({ class: "tags-header module-header-line" }, h2(headerText), p(i18n.modulesChatsDescription) ) ), - section(renderModeButtons(filter)), - !isForm + section(renderModeButtons(filter, emptyMod, (params && params.modesAvail) || null)), + !isForm && !emptyMod ? section( - div({ class: "filters" }, - form({ method: "GET", action: "/chats" }, + div({ class: "filters activity-filter-chips activity-toolbar-row" }, + renderModuleStatsBy(list, c => String(c.status || 'OPEN').toUpperCase(), [{ value: 'OPEN', label: i18n.chatStatusOpen }, { value: 'INVITE-ONLY', label: i18n.chatStatusInviteOnly }, { value: 'CLOSED', label: i18n.chatStatusClosed }]), + form({ method: "GET", action: "/chats", class: "filter-box" }, input({ type: "hidden", name: "filter", value: filter }), - input({ type: "text", name: "q", placeholder: i18n.chatSearchPlaceholder, value: q }), - br(), - button({ type: "submit" }, i18n.search), - br() + input({ type: "text", name: "q", placeholder: i18n.chatSearchPlaceholder, value: q, class: "filter-box__input" }), + div({ class: "filter-box__controls" }, + button({ type: "submit", class: "filter-box__button" }, i18n.searchButton) + ) ) ) ) @@ -214,7 +377,7 @@ exports.chatsView = async (chats, filter, chatToEdit = null, params = {}) => { ? renderChatForm(filter, filter === "edit" ? (chatToEdit || {}) : {}, params) : div({ class: "tribe-grid" }, list.length - ? list.map(chat => renderChatCard(chat, filter, { q })) + ? paged(list).map(chat => renderChatCard(chat, filter, { q, spread: params.spreadMap && params.spreadMap.get(chat.key) })) : p(i18n.chatNoItems) ) ) @@ -226,50 +389,62 @@ exports.singleChatView = async (chat, filter, messages = [], params = {}) => { const returnTo = safeText(params.returnTo) || buildReturnTo(filter, { q }) const isAuthor = String(chat.author) === String(userId) const isMember = safeArr(chat.members).includes(userId) || (!!chat.tribeId && !!chat.isTribeMember) - const fullShareUrl = `/chats/${encodeURIComponent(chat.key)}` const isRestrictedInviteOnly = !isMember && !isAuthor && chat.status === "INVITE-ONLY" const statusLabel = chat.status === "CLOSED" ? i18n.chatStatusClosed : chat.status === "INVITE-ONLY" ? i18n.chatStatusInviteOnly : i18n.chatStatusOpen + const chatShares = isAuthor || isMember const detailChips = [ renderChatStatusChip(chat.status), renderEncryptedChip(i18n), - renderLifespanChip(chat.lifetime, i18n) + renderLifespanChip(chat.lifetime, i18n), + chat.subscription && chatShares + ? ((isAuthor || chat.subscription.subscribed === true) + ? renderStateChip("mutuals", "✉", i18n.subscriptionOn) + : renderStateChip("closed", "✉", i18n.subscriptionOff)) + : null ].filter(Boolean) const chatSide = div({ class: "tribe-side" }, + div({ class: "card-header activity-card-header" }, + renderContentActions(chat.key, `/chats/${encodeURIComponent(chat.key)}`, { spread: params.spreads || null, author: chat.author, favKind: 'chats', isFavorite: chat.isFavorite, returnTo: params.returnTo, reportTitle: chat.title, deleteAction: isAuthor ? contentDeleteAction('chat', chat.key) : undefined }) + ), div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, chat.title || i18n.chatUntitled) ), + chat.description ? p({ class: "tribe-card-description" }, chat.description) : null, detailChips.length ? div({ class: "card-chips-row" }, ...detailChips) : null, renderMediaBlob(chat.image, "/assets/images/default-avatar.png", { class: "tribe-detail-image" }), - div({ class: "shop-share" }, - span({ class: "tribe-info-label" }, `${i18n.chatShareUrl}: `), - input({ type: "text", value: fullShareUrl, readonly: true, class: "shop-share-input" }) - ), - (() => { - const btn = renderSpreadButton(chat.key, params.spreads); - return btn ? div({ class: "card-spread-centered" }, btn) : null; - })(), div({ class: "tribe-card-members" }, span({ class: "tribe-members-count" }, `${i18n.chatParticipants}: ${safeArr(chat.members).length}`) ), + chat.subscription && chatShares + ? renderSubscriptionBox({ + target: chat.rootId || chat.key, + scope: "chats", + subscribed: chat.subscription.subscribed === true, + count: chat.subscription.count, + isOwner: isAuthor, + returnTo + }) + : null, table({ class: "tribe-info-table" }, tr( td({ class: "tribe-info-label" }, i18n.chatCreatedAt), td({ class: "tribe-info-value", colspan: "3" }, moment(chat.createdAt).format("YYYY/MM/DD HH:mm")) ), + !isRestrictedInviteOnly && chat.category ? tr( + td({ class: "tribe-info-label" }, i18n.chatCategoryLabel), + td({ class: "tribe-info-value", colspan: "3" }, catLabel(chat.category)) + ) : null, isRestrictedInviteOnly ? null : tr( td({ class: "tribe-info-value", colspan: "4" }, userLink(chat.author) ) - ), - !isRestrictedInviteOnly && chat.category ? tr( - td({ class: "tribe-info-label" }, i18n.chatCategoryLabel), - td({ class: "tribe-info-value", colspan: "3" }, catLabel(chat.category)) - ) : null + ) ), - isRestrictedInviteOnly ? null : div({ class: "tribe-side-actions" }, + isRestrictedInviteOnly ? null : (() => { + const sideActionNodes = [ isAuthor && chat.status === "INVITE-ONLY" ? form({ method: "POST", action: `/chats/generate-invite` }, input({ type: "hidden", name: "chatId", value: chat.key }), @@ -283,7 +458,8 @@ exports.singleChatView = async (chat, filter, messages = [], params = {}) => { if (openInvite) return [ div({ class: "tribe-open-invite" }, span({ class: "card-label" }, i18n.tribeInviteCodeText), - span({ class: "tribe-open-invite-code" }, openInvite.code) + span({ class: "tribe-open-invite-code" }, openInvite.code), + renderInviteQrCard({ qrDataUrl: `/qr-invite-code/chats/${encodeURIComponent(openInvite.code)}` }) ), form({ method: "POST", action: `/chats/open-invite/remove` }, input({ type: "hidden", name: "chatId", value: chat.key }), @@ -295,43 +471,35 @@ exports.singleChatView = async (chat, filter, messages = [], params = {}) => { button({ type: "submit", class: "tribe-action-btn" }, i18n.tribeOpenInvitation) ) })(), - form( - { method: "POST", action: chat.isFavorite ? `/chats/favorites/remove/${encodeURIComponent(chat.key)}` : `/chats/favorites/add/${encodeURIComponent(chat.key)}` }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - button({ type: "submit", class: "tribe-action-btn" }, chat.isFavorite ? i18n.chatRemoveFavorite : i18n.chatAddFavorite) - ), - chat.author && String(chat.author) !== String(userId) - ? form({ method: "GET", action: "/pm" }, - input({ type: "hidden", name: "recipients", value: chat.author }), - button({ type: "submit", class: "tribe-action-btn" }, i18n.chatPM || i18n.privateMessage) - ) - : null, - isAuthor - ? form({ method: "GET", action: `/chats/edit/${encodeURIComponent(chat.key)}` }, - button({ type: "submit", class: "tribe-action-btn" }, i18n.chatUpdate) - ) - : null, - isAuthor && chat.status !== "CLOSED" - ? form({ method: "POST", action: `/chats/close/${encodeURIComponent(chat.key)}` }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - button({ type: "submit", class: "tribe-action-btn" }, i18n.chatClose) - ) - : null, - isAuthor - ? form({ method: "POST", action: `/chats/delete/${encodeURIComponent(chat.key)}` }, - button({ type: "submit", class: "tribe-action-btn danger-btn" }, i18n.chatDelete) - ) - : null, !isAuthor && isMember ? form({ method: "POST", action: `/chats/leave/${encodeURIComponent(chat.key)}` }, input({ type: "hidden", name: "returnTo", value: returnTo }), button({ type: "submit", class: "tribe-action-btn danger-btn" }, i18n.tribeLeaveButton) ) : null + ].flat().filter(Boolean) + return sideActionNodes.length ? div({ class: "tribe-side-actions" }, ...sideActionNodes) : null + })(), + isAuthor + ? div({ class: "tribe-side-actions housing-status-row" }, + span({ class: "card-label" }, `${i18n.chatStatusLabel}: `), + renderStateChip(chat.status === "CLOSED" ? "hidden" : "mutuals", chat.status === "CLOSED" ? "🔒" : "👁", String(chat.status || "OPEN").toUpperCase()), + chat.status !== "CLOSED" + ? form({ method: "POST", action: `/chats/close/${encodeURIComponent(chat.key)}`, class: "inline-form" }, + input({ type: "hidden", name: "returnTo", value: returnTo }), + button({ type: "submit", class: "tribe-action-btn" }, i18n.chatClose) + ) + : null + ) + : null, + !isAuthor ? null : div({ class: "tribe-side-actions owner-actions" }, + form({ method: "GET", action: `/chats/edit/${encodeURIComponent(chat.key)}` }, + button({ type: "submit", class: "tribe-action-btn" }, i18n.chatUpdate) + ) ), !isMember && chat.status === "INVITE-ONLY" ? div({ class: "chat-join-section" }, - a({ class: "tribe-action-btn", href: "/invites#invites-chats" }, i18n.tribeEnterInvite) + a({ class: "tribe-action-btn", href: "/invites?filter=others" }, i18n.tribeEnterInvite) ) : null, !isRestrictedInviteOnly && safeArr(chat.tags).length @@ -344,38 +512,112 @@ exports.singleChatView = async (chat, filter, messages = [], params = {}) => { const msgList = safeArr(messages) const canWrite = (isMember || chat.status === "OPEN") && chat.status !== "CLOSED" + const pinnedMsgs = msgList.filter(m => m.pinned).sort((a, b) => new Date(a.createdAt) - new Date(b.createdAt)) + const replyMsg = params.reply ? msgList.find(m => String(m.key) === String(params.reply)) : null + const chatMain = isRestrictedInviteOnly ? div({ class: "tribe-main chat-full-width" }, p({ class: "access-denied-msg" }, i18n.chatAccessDenied)) : div({ class: "tribe-main chat-full-width" }, + div({ class: "chat-jump-row" }, + a({ href: "/chats", class: "filter-btn chat-back-btn" }, "\u00ab " + (i18n.chatsTitle || "Chats")), + msgList.length && pinnedMsgs.length + ? a({ href: `#${anchorIdOf(pinnedMsgs[0].key)}`, class: "filter-btn chat-pinned-link" }, `${i18n.chatPinned} (${pinnedMsgs.length})`) + : null, + msgList.length ? a({ href: "#chat-latest", class: "filter-btn chat-jump-latest" }, i18n.chatJumpLatest) : null + ), + div({ class: "chat-messages-list" }, + (() => { + const visible = msgList + .filter(msg => (msg.text && String(msg.text).trim()) || msg.image) + .map(msg => ({ kind: 'message', ts: Date.parse(msg.createdAt) || 0, msg })) + const pollItems = safeArr(params.polls).map(poll => ({ + kind: 'poll', ts: Date.parse(poll.createdAt) || 0, poll + })) + const stream = [...visible, ...pollItems].sort((a, b) => a.ts - b.ts) + if (!stream.length) return p({ class: "chat-no-messages" }, i18n.chatNoMessages) + const nodes = [] + let prevDay = null + stream.forEach((entry, i) => { + const day = moment(entry.ts).format("YYYY/MM/DD") + if (day !== prevDay) { + nodes.push(div({ class: "chat-day-separator" }, span({ class: "chat-day-chip" }, day))) + prevDay = day + } + const last = i === stream.length - 1 + const node = entry.kind === 'poll' + ? renderChatPoll(entry.poll, chat) + : renderMessage(entry.msg, chat, { canWrite, isChatAuthor: isAuthor }) + nodes.push(last ? div({ id: "chat-latest", class: "chat-latest-anchor" }, node) : node) + }) + return nodes + })() + ), canWrite - ? div({ class: "chat-message-form" }, + ? div({ id: "chat-message-form", class: "chat-message-form" }, + replyMsg + ? div({ class: "chat-replying-banner" }, + span({ class: "chat-reply-arrow" }, "↩ "), + span({ class: "chat-reply-text" }, String(replyMsg.text || "").trim() ? ` ${String(replyMsg.text).trim().slice(0, 120)}` : userLinkLabel(replyMsg.author)), + a({ href: `/chats/${encodeURIComponent(chat.key)}#chat-message-form`, class: "chat-reply-cancel", title: i18n.chatReplyCancel || "✕" }, "✕") + ) + : null, form({ method: "POST", action: `/chats/${encodeURIComponent(chat.key)}/message`, enctype: "multipart/form-data" }, - input({ type: "hidden", name: "returnTo", value: `/chats/${encodeURIComponent(chat.key)}` }), - textarea({ name: "text", rows: 3, placeholder: i18n.chatMessagePlaceholder }), br(), - span(i18n.chatImageLabel || "Select an image file (.jpeg, .jpg, .png, .gif)"), br(), - input({ type: "file", name: "image", accept: "image/*" }), br(), br(), + input({ type: "hidden", name: "returnTo", value: `/chats/${encodeURIComponent(chat.key)}#chat-latest` }), + replyMsg ? input({ type: "hidden", name: "replyTo", value: replyMsg.key }) : null, + textarea({ name: "text", rows: 3, maxlength: "3000", placeholder: i18n.chatMessagePlaceholder }), br(), + span(i18n.uploadMedia), br(), + input({ type: "file", name: "image", accept: "image/*,video/*,audio/*,application/pdf,.torrent" }), br(), br(), button({ type: "submit", class: "filter-btn" }, i18n.chatSendMessage) ) ) : null, - div({ class: "chat-messages-list" }, - (() => { - const visible = msgList.filter(msg => (msg.text && String(msg.text).trim()) || msg.image) - return visible.length - ? visible.map(msg => renderMessage(msg, chat.author)) - : p({ class: "chat-no-messages" }, i18n.chatNoMessages) - })() - ) + canWrite && params.pollsEnabled + ? details({ class: "chat-poll-form" }, + summary({ class: "chat-poll-summary" }, i18n.pollChatCreate), + form({ method: "POST", action: `/chats/${encodeURIComponent(chat.key)}/polls/create` }, + input({ type: "hidden", name: "returnTo", value: `/chats/${encodeURIComponent(chat.key)}` }), + span(i18n.pollQuestion), br(), + input({ type: "text", name: "question", required: true, maxlength: "300", placeholder: i18n.pollQuestionPlaceholder }), br(), br(), + span(i18n.pollOptions), br(), + textarea({ maxlength: "1000", name: "options", rows: 4, required: true, placeholder: i18n.pollOptionsPlaceholder }), br(), br(), + div({ class: "poll-switch" }, + input({ type: "hidden", name: "anonymous", value: "0" }), + label(input({ type: "checkbox", name: "anonymous", value: "1" }), " ", i18n.pollAnonymousLabel) + ), + div({ class: "poll-switch" }, + input({ type: "hidden", name: "multiple", value: "0" }), + label(input({ type: "checkbox", name: "multiple", value: "1" }), " ", i18n.pollMultipleLabel) + ), + button({ type: "submit", class: "filter-btn" }, i18n.pollPublishButton) + ) + ) + : null ) + if (params.workspace) { + return template( + chat.title || i18n.chatUntitled, + section({ class: "chat-workspace-section" }, + div({ class: "chat-workspace" }, + div({ class: "chat-workspace-main" }, + div({ class: "tribe-details chat-workspace-details" }, + chatSide, + chatMain + ) + ) + ) + ) + ) + } + return template( chat.title || i18n.chatUntitled, section( - div({ class: "tags-header" }, + div({ class: "tags-header module-header-line" }, h2(i18n.chatsTitle), p(i18n.modulesChatsDescription) ), - renderModeButtons(filter || "all") + renderModeButtons(filter || "all", false, (params && params.modesAvail) || null) ), section( div({ class: "tribe-details" }, diff --git a/src/views/cipher_view.js b/src/views/cipher_view.js index 6e3ecb0..b79b48d 100644 --- a/src/views/cipher_view.js +++ b/src/views/cipher_view.js @@ -9,7 +9,7 @@ function generateRandomPassword(length = 32) { const cipherView = async (encryptedText = "", decryptedText = "", iv = "", password = "") => { const randomPassword = generateRandomPassword(); - const header = div({ class: "tags-header" }, + const header = div({ class: "tags-header module-header-line" }, h2(i18n.cipherTitle), p(i18n.cipherDescription) ); @@ -83,9 +83,7 @@ const cipherView = async (encryptedText = "", decryptedText = "", iv = "", passw i18n.cipherTitle, section( header, - div({ id: "randomPassword" }, - h2({ class: "generated-password" }, randomPassword) - ), + p({ id: "randomPassword" }, h2({ class: "generated-password" }, randomPassword)), div({ class: "div-center" }, encryptForm, br(), diff --git a/src/views/clearnet_view.js b/src/views/clearnet_view.js index a863462..a54687d 100644 --- a/src/views/clearnet_view.js +++ b/src/views/clearnet_view.js @@ -1,4 +1,31 @@ -const { a, br, div, input, span, strong } = require("../server/node_modules/hyperaxe"); +const { a, br, div, input, option, select, span, strong, form, button, label } = require("../server/node_modules/hyperaxe"); +const { renderStyledHtml } = require('../backend/renderStyledText'); +const sharedState = require('../configs/shared-state'); +const cnPkg = (() => { try { return require('../server/package.json'); } catch (_) { return {}; } })(); +const mv = () => require('./main_views'); +const cnScope = () => { try { return require('../models/typed_log').requestScope.getStore() || {}; } catch (_) { return {}; } }; +const cnWithLang = (href, lang) => { + if (!lang || !/^\/c(?:[/?#]|$)/.test(href) || /[?&]lang=/.test(href)) return href; + const [pathAndQuery, hash] = href.split('#'); + const joined = `${pathAndQuery}${pathAndQuery.includes('?') ? '&' : '?'}lang=${lang}`; + return hash != null ? `${joined}#${hash}` : joined; +}; +const propagateLang = (html, lang) => !lang ? html : String(html) + .replace(/href="(\/c(?:[/?#][^"]*)?)"/g, (m, href) => `href="${cnWithLang(href, lang)}"`) + .replace(/(]*action="\/c(?:\/[^"]*)?"[^>]*>)/g, (m) => `${m}`); +const renderLangSelector = (current) => { + const langs = Object.keys(require('../client/assets/translations/i18n')); + const scope = cnScope(); + const base = String(scope.path || '/c'); + const params = new URLSearchParams(String(scope.query || '')); + params.delete('lang'); + const others = langs.filter(l => l !== current).map(l => { + const q = new URLSearchParams(params); q.set('lang', l); + return `${l.toUpperCase()}`; + }).join(''); + return `
${escapeHtml(String(current).toUpperCase())}
${others}
`; +}; +const cnText = (key, fallback) => { const v = mv().i18n[key]; return typeof v === 'string' && v ? v : fallback; }; const STAT_TYPE_KEYS = { post:'statsPost', event:'statsEvent', task:'statsTask', forum:'statsForum', tribe:'statsTribe', market:'statsMarket', job:'statsJob', project:'statsProject', shop:'statsShop', image:'statsImage', video:'statsVideo', audio:'statsAudio', document:'statsDocument', bookmark:'statsBookmark', transfer:'statsTransfer', map:'statsMap' }; const STAT_ORDER = ['post','event','task','forum','tribe','market','job','project','shop','image','video','audio','document','bookmark','transfer','map']; @@ -6,7 +33,7 @@ const renderContentStats = (stats, i18nObj = {}) => { if (!stats || typeof stats !== 'object') return null; const chips = STAT_ORDER .filter(t => (stats[t] || 0) > 0) - .map(t => span({ class: 'inhabitant-stat' }, + .map(t => span({ class: 'inhabitant-stat', title: `${i18nObj[STAT_TYPE_KEYS[t]] || t}: ${stats[t]}` }, span({ class: 'inhabitant-stat-label' }, i18nObj[STAT_TYPE_KEYS[t]] || t), strong({ class: 'inhabitant-stat-value' }, String(stats[t])) )); @@ -23,6 +50,31 @@ const escapeHtml = (s) => String(s || '') .replace(/"/g, '"') .replace(/'/g, '''); +const renderTagChips = (tags) => { + const list = (Array.isArray(tags) ? tags : []) + .map(t => String(t || '').trim()) + .filter(Boolean) + .slice(0, 12); + if (!list.length) return ''; + const chips = list.map(raw => String(raw).replace(/^#+/, '')).filter(Boolean).map(t => `#${escapeHtml(t)}`).join(''); + return `
${chips}
`; +}; + +const KIND_KEYS = { audio: 'cnKindAudio', blog: 'cnKindBlog', bookmark: 'cnKindBookmark', document: 'cnKindDocument', event: 'cnKindEvent', feed: 'cnKindFeed', image: 'cnKindImage', job: 'cnKindJob', market: 'cnKindMarket', podcast: 'cnKindPodcast', project: 'cnKindProject', course: 'cnKindCourse', shop: 'cnKindShop', torrent: 'cnKindTorrent', file: 'cnKindFile', video: 'cnKindVideo', wiki: 'cnKindWiki', emergency: 'emergencyLabel', campaign: 'campaignLabel', housing: 'housingTitle', room: 'cnKindRoom', map: 'cnKindMap', calendar: 'calendarTitle' }; +const kindLabel = (kind) => cnText(KIND_KEYS[String(kind || '').toLowerCase()], String(kind || '')); +const renderKindTag = (kind) => `[${escapeHtml(kindLabel(kind).toUpperCase())}]`; + +const renderRichText = (s, { links = true, wikiLinks = null } = {}) => renderStyledHtml(s, { + blobPrefix: '/c/blob/', + internalLinks: false, + links, + plainUrlClass: 'cn-url', + hashtagHref: links ? (tag) => `/c?q=%23${encodeURIComponent(tag)}` : null, + wikiLink: wikiLinks ? (target, slug, label) => wikiLinks.has(slug) + ? a({ href: wikiLinks.get(slug), class: 'cn-wiki-link' }, label) + : label : null +}).replace(/\n/g, '
'); + const blobIdOf = (v) => { if (!v) return null; const s = String(v).trim(); @@ -38,6 +90,22 @@ const blobUrl = (v) => { return id ? `/c/blob/${encodeURIComponent(id)}` : null; }; +const renderClearnetSelector = (on = false, i18nObj = {}) => + div({ class: 'clearnet-choice' }, + div({ class: 'clearnet-choice-options' }, + label({ class: 'clearnet-choice-option' }, input({ type: 'radio', name: 'clearnet', value: '0', ...(on ? {} : { checked: true }) }), renderReachChip(false, i18nObj)), + label({ class: 'clearnet-choice-option' }, input({ type: 'radio', name: 'clearnet', value: '1', ...(on ? { checked: true } : {}) }), renderReachChip(true, i18nObj)) + ) + ); + +const renderClearnetSwitch = (kind, id, on = false) => id + ? form({ method: 'POST', action: `/clearnet/item/${encodeURIComponent(id)}`, class: 'clearnet-switch' }, + input({ type: 'hidden', name: 'kind', value: kind }), + button({ type: 'submit', name: 'on', value: '0', class: on ? 'clearnet-switch-btn' : 'clearnet-switch-btn active', ...(on ? {} : { disabled: true }) }, 'OASIS'), + button({ type: 'submit', name: 'on', value: '1', class: on ? 'clearnet-switch-btn active' : 'clearnet-switch-btn', ...(on ? { disabled: true } : {}) }, 'CLEARNET') + ) + : null; + const renderReachChip = (isClearnet, i18nObj = {}, href = null) => { const icon = isClearnet ? '🌐' : '🏝'; const label = isClearnet @@ -53,6 +121,15 @@ const renderReachChip = (isClearnet, i18nObj = {}, href = null) => { return chip; }; +const renderTribeOriginChip = (origin) => origin && origin.title + ? a({ href: origin.tribeHref || origin.href, class: 'pm-exposition-chip-link' }, + span({ class: 'pm-exposition-chip pm-exposition-tribe' }, + span({ class: 'pm-exposition-icon' }, 'ꖥ'), + span({ class: 'pm-exposition-text' }, origin.title) + ) + ) + : null; + const fediverseProfileUrl = (handle) => { const h = String(handle || '').trim().replace(/^@/, ''); const at = h.indexOf('@'); @@ -71,13 +148,59 @@ const renderFediverseReach = (prefs, i18nObj = {}) => { a({ href: url, target: '_blank', rel: 'noopener noreferrer', class: 'pm-exposition-chip-link' }, span({ class: 'pm-exposition-chip pm-exposition-fediverse' }, span({ class: 'pm-exposition-icon' }, '🐘'), - span({ class: 'pm-exposition-text' }, i18nObj.fediverse || 'Fediverse') + span({ class: 'pm-exposition-text' }, i18nObj.fediverse || 'Multiverse') ) ), a({ href: url, target: '_blank', rel: 'noopener noreferrer', class: 'fediverse-reach-url' }, handle) ); }; +const LICENSES = [ + { id: 'CC0-1.0', label: 'Public Domain', name: 'Public Domain' }, + { id: 'CC-BY-4.0', label: 'CC BY 4.0', name: 'Creative Commons Attribution 4.0' }, + { id: 'CC-BY-SA-4.0', label: 'CC BY-SA 4.0', name: 'Creative Commons Attribution-ShareAlike 4.0' }, + { id: 'GFDL-1.3-or-later', label: 'GNU FDL 1.3', name: 'GNU Free Documentation License 1.3' }, + { id: 'LAL-1.3', label: 'Free Art License 1.3', name: 'Free Art License 1.3' } +]; +const DEFAULT_LICENSE = LICENSES[0].id; +const LICENSED_KINDS = new Set(['wiki', 'audio', 'video', 'image', 'document']); +const licenseInfo = (id) => { + const v = String(id || '').trim() || DEFAULT_LICENSE; + return LICENSES.find(l => l.id === v) || null; +}; +const normalizeLicense = (id) => (LICENSES.find(l => l.id === String(id || '').trim()) || LICENSES[0]).id; + +const renderLicenseChip = (id) => { + const l = licenseInfo(id); + if (!l) return null; + return span({ class: 'pm-exposition-chip pm-exposition-license', title: l.name }, + span({ class: 'pm-exposition-icon' }, '\u2696'), + span({ class: 'pm-exposition-text' }, l.label) + ); +}; + +const renderLicenseSelect = (current, i18nObj = {}) => { + const selectedId = normalizeLicense(current); + return [ + span(i18nObj.licenseLabel || 'License'), + br(), + select({ name: 'license' }, ...LICENSES.map(l => option({ value: l.id, ...(l.id === selectedId ? { selected: true } : {}) }, l.label))), + br() + ]; +}; + +const renderClearnetLicense = (id) => { + const l = licenseInfo(id); + return l ? `\u2696 ${escapeHtml(l.label)}` : ''; +}; + +const renderTransportChip = (i18nObj = {}) => { + return span({ class: 'pm-exposition-chip pm-exposition-transport', title: 'Secret Handshake + boxstream · SHA-256' }, + span({ class: 'pm-exposition-icon' }, '\u21C4'), + span({ class: 'pm-exposition-text' }, i18nObj.transportChipLabel || 'Encrypted in transit') + ); +}; + const renderEncryptedChip = (i18nObj = {}) => { return span({ class: 'pm-exposition-chip pm-exposition-encrypted' }, span({ class: 'pm-exposition-icon' }, '🔒'), @@ -92,16 +215,7 @@ const renderDoubleEncryptionChip = (i18nObj = {}) => { ); }; -const INTERNAL_OASIS_PATHS = [ - 'author','thread','hashtag','inbox','pm','profile','settings','banking','wallet', - 'jobs','events','projects','shops','audios','videos','images','documents','torrents', - 'tribes','tribe','forum','votes','votations','reports','tasks','maps','chats','pads', - 'calendars','trending','opinions','feed','pixelia','cv','invites','peers','stats', - 'blockexplorer','modules','publish','search','tags','mentions','popular','threads', - 'topics','latest','summaries','multiverse','legacy','cipher','graphos','agenda', - 'favorites','logs','games','parliament','courts','market','ai','public','spread', - 'follow','unfollow','block','like','unlike' -]; +const { INTERNAL_PATHS: INTERNAL_OASIS_PATHS } = require('../backend/renderStyledText'); const stripInternalAnchors = (html) => { if (typeof html !== 'string' || !html) return html; @@ -111,10 +225,10 @@ const stripInternalAnchors = (html) => { return html.replace(hrefedClosed, '$1').replace(hrefedBare, '$1'); }; -const renderClearnetSearchForm = ({ authorFeedId = '', query = '', placeholder = 'Search…' }) => { +const renderClearnetSearchForm = ({ authorFeedId = '', query = '', placeholder = '' }) => { if (!authorFeedId) return ''; const safeQuery = escapeHtml(query || ''); - const safePh = escapeHtml(placeholder); + const safePh = escapeHtml(placeholder || cnText('cnSearchPlaceholder', 'Search…')); return ``; }; @@ -124,6 +238,84 @@ const renderClearnetUrlBlock = ({ baseUrl = '', path, i18nObj = {} }) => { ); }; +const CLEARNET_ZOOM_CSS = ` +.cn-zoom{display:contents} +.cn-zoom img{cursor:zoom-in} +.cn-lightbox{display:none;position:fixed;inset:0;z-index:1000;background:rgba(0,0,0,.9);align-items:center;justify-content:center} +.cn-lightbox:target{display:flex} +.cn-lightbox-link{display:flex;align-items:center;justify-content:center;width:100%;height:100%;cursor:zoom-out} +.cn-lightbox-img{display:block;max-width:92vw;max-height:88vh;object-fit:contain} +.cn-lightbox-full{background:#000} +.cn-lightbox-full .cn-lightbox-img{width:100vw;height:100vh;max-width:100vw;max-height:100vh} +.cn-lightbox-close,.cn-lightbox-fs{position:absolute;top:14px;z-index:1;width:42px;height:42px;display:flex;align-items:center;justify-content:center;border-radius:50%;background:rgba(0,0,0,.6);color:#fff;text-decoration:none;font-size:24px;line-height:1} +.cn-lightbox-close{right:16px} +.cn-lightbox-fs{right:66px} +.cn-lightbox-fs svg{display:block;width:20px;height:20px} +`; +const CN_FULLSCREEN_ICON = ''; +const cnZoom = (id, src, alt = '', { full = false, imgClass = '' } = {}) => { + const zid = `cnz-${String(id).replace(/[^A-Za-z0-9_-]/g, '')}`; + const a = escapeHtml(alt); + const u = escapeHtml(src); + const cls = imgClass ? ` class="${escapeHtml(imgClass)}"` : ''; + const fs = full ? `${CN_FULLSCREEN_ICON}` : ''; + return `` + + `
×${fs}${a}
` + + (full ? `` : ''); +}; +const CN_ZOOM_SKIP = /\bcn-(lightbox-img|footer-logo)\b/; +const zoomClearnetImages = (html) => { + let depth = 0; + let n = 0; + return String(html || '').replace(/]*>|<\/a\s*>|]*>/gi, (tag) => { + if (/^ 0) return tag; + const cls = (tag.match(/\sclass="([^"]*)"/i) || [])[1] || ''; + const src = (tag.match(/\ssrc="([^"]*)"/i) || [])[1] || ''; + if (!src || CN_ZOOM_SKIP.test(cls)) return tag; + const alt = (tag.match(/\salt="([^"]*)"/i) || [])[1] || ''; + const zid = `cnz-i${++n}`; + return `${tag}×${alt}`; + }); +}; +const CLEARNET_TEXT_CSS = ` +.cn-tags{display:flex;flex-wrap:wrap;gap:6px;margin:10px 0} +.cn-hub-priceline{margin:10px 0 0 0} +.cn-hub-details{display:flex;flex-wrap:wrap;gap:8px;margin:8px 0 0 0} +.cn-detail{display:inline-flex;align-items:center;border:1px solid var(--border);border-radius:5px;padding:3px 8px;font-size:11px;letter-spacing:1px;text-transform:uppercase;color:var(--fg-soft);background:var(--bg-sub);white-space:nowrap} +.cn-tag{border:1px solid var(--border);border-radius:5px;padding:3px 8px;font-size:11px;color:var(--fg-soft);background:var(--bg-sub);text-decoration:none;white-space:nowrap} +.cn-tag:hover{color:var(--fg);border-color:var(--fg)} +.cn-license{display:inline-flex;align-items:center;gap:4px;border:1px solid var(--border);border-radius:5px;padding:3px 8px;font-size:11px;letter-spacing:1px;color:var(--fg-soft);background:var(--bg-sub);text-decoration:none;white-space:nowrap} +a.tag-link{color:var(--accent);text-decoration:none} +a.cn-wiki-link{color:var(--accent);text-decoration:underline;text-underline-offset:2px} +a.cn-wiki-link:hover{color:var(--fg)} +.cn-price{display:inline-flex;align-items:center;color:var(--fg);background:var(--bg-sub);border:1px solid var(--fg);border-radius:4px;padding:4px 10px;font-weight:bold;font-size:14px} +a.tag-link:hover{text-decoration:underline} +.rt-header{display:block;font-weight:bold;margin:10px 0 4px;line-height:1.3} +.rt-header-1{font-size:1.5em} +.rt-header-2{font-size:1.3em} +.rt-header-3{font-size:1.15em} +.rt-item{display:block;padding-left:1.4em;text-indent:-0.7em} +.rt-item::before{content:"\u2022 ";opacity:.7} +.rt-item-2{padding-left:3em} +.rt-item-3{padding-left:4.6em} +.rt-item-4{padding-left:6.2em} +.rt-item-number{text-indent:-1.2em} +.rt-item-number::before{content:none} +.rt-quote{display:block;border-left:3px solid currentColor;padding-left:10px;margin:6px 0;opacity:.85} +.rt-rule{display:block;border-top:1px solid currentColor;opacity:.4;margin:10px 0} +.rt-code{font-family:monospace;background:rgba(128,128,128,.18);padding:1px 4px;border-radius:3px;word-break:break-word} +.rt-code-block{display:block;font-family:monospace;white-space:pre-wrap;background:rgba(128,128,128,.18);padding:10px;border-radius:5px;margin:8px 0;overflow-x:auto;word-break:break-word} +.post-image{display:block;width:auto;max-width:100%;max-height:480px;height:auto;margin:12px auto;border:1px solid var(--border);border-radius:6px} +.post-video{display:block;width:100%;max-width:100%;max-height:480px;margin:12px auto;border-radius:6px;background:#000} +.post-audio{display:block;width:100%;max-width:100%;margin:12px 0} +.cn-media-frame img{max-height:480px;width:auto;margin:0 auto} +.cn-media-frame video{max-height:480px} +.post-pdf{display:inline-block;margin:8px 0;padding:8px 14px;background:var(--bg-sub);border:1px solid var(--border);border-radius:6px;color:var(--fg);text-decoration:none} +.post-pdf:hover{border-color:var(--fg)} +`; + const CLEARNET_SEARCH_CSS = ` .cn-search{margin:0} .cn-search input[type=text]{width:240px;max-width:100%;background:var(--bg-sub);color:var(--fg);border:1px solid var(--border);border-radius:6px;padding:8px 12px;font-size:14px;font-family:inherit} @@ -189,24 +381,30 @@ header.cn-header{display:flex;align-items:center;gap:16px;padding-bottom:16px;ma .cn-brand{font-size:20px;font-weight:700;color:var(--fg);letter-spacing:1px} .cn-brand-sub{color:var(--fg-dim);font-size:12px;text-transform:uppercase;letter-spacing:2px;margin-top:2px} .cn-header-extra{flex:1 1 auto;display:flex;justify-content:flex-end;align-items:center;min-width:0} +.cn-kind-tag{font-weight:700;color:var(--fg);letter-spacing:1px} +.cn-sep{border:0;border-top:1px solid var(--border);margin:24px 0} h2.cn-section{color:var(--fg);font-size:18px;text-transform:uppercase;letter-spacing:2px;margin:32px 0 16px 0;padding-bottom:8px;border-bottom:1px solid var(--border)} footer.cn-footer{margin-top:48px;padding-top:20px;border-top:1px solid var(--border);font-size:12px;color:var(--fg-dim);text-align:center;letter-spacing:0.5px} footer.cn-footer a{color:var(--fg-soft)} footer.cn-footer .cn-footer-logo{width:56px;height:auto;display:block;margin:0 auto 10px auto;border-radius:6px} +footer.cn-footer .cn-footer-line{margin:3px 0} `; -const renderClearnetPage = ({ title, ogTitle, ogDescription = '', ogImage = null, extraCss = '', body, headerExtra = '', hubFeedId = null }) => { +const renderClearnetHubSearch = ({ type = '', query = '' } = {}) => + ``; + +const renderClearnetPage = ({ title, ogTitle, ogDescription = '', ogImage = null, extraCss = '', body, headerExtra = null, hubFeedId = null }) => { const safeTitle = escapeHtml(title || 'Oasis'); const safeOgTitle = escapeHtml(ogTitle || title || 'Oasis'); const safeOgDesc = escapeHtml(ogDescription || ''); const palette = getCurrentPalette(); const baseCss = buildBaseCss(palette); - const brandInner = `
⛱ Oasis HUB
Libre · P2P · Federated
`; - const brandBlock = hubFeedId - ? `${brandInner}` - : `
${brandInner}
`; + const brandInner = `
⛱ Oasis HUB
${escapeHtml(cnText('cnBrandSub', 'Libre · P2P · Federated'))}
`; + const lang = escapeHtml(mv().getLanguage ? mv().getLanguage() : 'en'); + const langOverride = cnScope().cnLang || ''; + const brandBlock = `${brandInner}`; return ` - + @@ -216,21 +414,33 @@ const renderClearnetPage = ({ title, ogTitle, ogDescription = '', ogImage = null ${ogImage ? `` : ''} -
${brandBlock} - ${headerExtra ? `
${headerExtra}
` : ''} +
${renderLangSelector(lang)}${propagateLang(headerExtra == null ? renderClearnetHubSearch() : headerExtra, langOverride)}
- ${stripInternalAnchors(body)} + ${propagateLang(stripInternalAnchors(zoomClearnetImages(body)), langOverride)}
- - Powered by Oasis + + + +
`; @@ -242,14 +452,44 @@ const renderClearnetNotFound = () => { ogTitle: 'Oasis', ogDescription: '', extraCss: `.cn-notfound{color:var(--fg-soft);font-size:16px;max-width:480px;margin:80px auto 40px auto;text-align:center;line-height:1.5}`, - body: `

The content is not accessible at this moment.

` + body: `

${escapeHtml(cnText('cnNotAccessible', 'The content is not accessible at this moment.'))}

` }); }; +const CLEARNET_PAGER_CSS = ` +.cn-pager{display:flex;justify-content:flex-end;align-items:center;flex-wrap:wrap;gap:8px;margin:24px 0 8px 0} +.cn-pager-info{color:var(--fg-dim);font-size:12px;margin-right:4px} +.cn-pager-sizes{display:flex;align-items:center;flex-wrap:wrap;gap:6px;margin-right:auto} +.cn-pager-btn{display:inline-block;padding:6px 14px;background:var(--bg-elev);color:var(--fg-soft);border:1px solid var(--border);border-radius:14px;font-size:13px;text-decoration:none;transition:border-color .15s ease,color .15s ease,background .15s ease} +.cn-pager-btn:hover,.cn-pager-btn.active{border-color:var(--fg);color:var(--fg);text-decoration:none} +`; +const paginateClearnet = (list, page) => mv().slicePage(list, page, mv().listPerPage(cnScope().query)); +const renderClearnetPager = ({ base, params = {}, page, pages, per, total, pageParam = 'page' }) => { + const query = String(cnScope().query || ''); + const sizes = !!per && mv().showPageSizes(total, query); + if (!(pages > 1) && !sizes) return ''; + const chosen = new URLSearchParams(query).has('perPage'); + const href = (n, size) => { + const q = new URLSearchParams(); + for (const [k, v] of Object.entries(params)) if (v) q.set(k, String(v)); + if (size || chosen) q.set('perPage', size || per); + if (n > 1) q.set(pageParam, String(n)); + const s = q.toString(); + return escapeHtml(s ? `${base}?${s}` : base); + }; + const sizeLinks = sizes + ? `${escapeHtml(cnText('searchPerPageLabel', 'Results per page'))}${mv().LIST_PAGE_SIZES.map(s => `${escapeHtml(mv().pageSizeLabel(s))}`).join('')}` + : ''; + const info = pages > 1 ? `${escapeHtml(cnText('cnPageOf', 'Page {page} of {pages}').replace('{page}', String(page)).replace('{pages}', String(pages)))}` : ''; + const prev = page > 1 ? `${escapeHtml(cnText('cnPrevPage', '← Previous'))}` : ''; + const next = page < pages ? `${escapeHtml(cnText('cnNextPage', 'Next →'))}` : ''; + return `
${sizeLinks}${info}${prev}${next}
`; +}; + const renderClearnetMediaView = ({ kind, item }) => { const blob = blobUrl(item.url); - const title = escapeHtml(item.title || 'Untitled'); - const desc = escapeHtml(item.description || ''); + const title = escapeHtml(item.title || cnText('cnUntitled', 'Untitled')); + const desc = renderRichText(item.description || '', { wikiLinks: item.wikiLinks instanceof Map ? item.wikiLinks : null }); const dateStr = item.createdAt ? escapeHtml(new Date(item.createdAt).toISOString().slice(0, 10)) : ''; const extraCss = ` .cn-media-meta{color:var(--fg-dim);font-size:13px;margin-bottom:16px;display:flex;gap:14px;flex-wrap:wrap;align-items:baseline} @@ -259,9 +499,11 @@ const renderClearnetMediaView = ({ kind, item }) => { .cn-media-frame{margin:16px 0} .cn-media-frame img{max-width:100%;height:auto;border-radius:6px;border:1px solid var(--border);display:block} .cn-media-frame audio,.cn-media-frame video{width:100%;max-width:100%;display:block;border-radius:6px;background:#000} -.cn-media-frame .cn-media-doc{display:inline-block;background:var(--bg-elev);border:1px solid var(--border);border-radius:6px;padding:10px 18px;color:var(--fg);text-decoration:none} +.cn-media-frame .cn-media-doc{display:inline-block;max-width:100%;box-sizing:border-box;overflow-wrap:anywhere;background:var(--bg-elev);border:1px solid var(--border);border-radius:6px;padding:10px 18px;color:var(--fg);text-decoration:none} .cn-media-frame .cn-media-doc:hover{border-color:var(--fg)} +.cn-media-frame .cn-media-docs{display:flex;flex-direction:column;align-items:flex-start;gap:10px} `; + const fileKind = kind === 'document' || kind === 'torrent' || kind === 'file'; let mediaHtml = ''; if (blob) { if (kind === 'image') { @@ -270,21 +512,38 @@ const renderClearnetMediaView = ({ kind, item }) => { mediaHtml = ``; } else if (kind === 'video') { mediaHtml = ``; - } else if (kind === 'document' || kind === 'torrent') { + } else if (kind === 'file' || kind === 'torrent') { + const dlName = (v) => String(v || '').replace(/[\\/:*?"<>|\r\n]+/g, ' ').replace(/\s+/g, ' ').trim().slice(0, 120); + const dlLink = (href, name) => `⇩ ${escapeHtml(name)}`; + const original = kind === 'file' ? blob : blobUrl(item.source); + const originalName = dlName(kind === 'file' ? item.fileName : item.sourceName) || dlName(item.title) || 'file'; + const torrentBlob = kind === 'file' ? blobUrl(item.torrentUrl) : blob; + const torrentName = `${kind === 'file' ? originalName : (dlName(item.title) || 'download').replace(/\.torrent$/i, '')}.torrent`; + mediaHtml = `
${[original ? dlLink(original, originalName) : '', torrentBlob ? dlLink(torrentBlob, torrentName) : ''].filter(Boolean).join('')}
`; + } else if (kind === 'document') { mediaHtml = `⇩ ${title}`; + } else { + mediaHtml = `${title}`; } } const body = `
+ ${renderKindTag(kind)} ${dateStr ? `📅 ${dateStr}` : ''} + ${(Array.isArray(item.details) ? item.details : []).map(d => `${escapeHtml(String(d))}`).join('')} + ${item.price ? `${escapeHtml(String(item.price))} ECO` : ''} + ${LICENSED_KINDS.has(kind) ? renderClearnetLicense(item.license) : ''}
-

${title}

- ${mediaHtml ? `
${mediaHtml}
` : ''} + ${item.title ? `

${title}

` : ''} +
+ ${mediaHtml && !fileKind ? `
${mediaHtml}
` : ''} ${desc ? `

${desc}

` : ''} + ${mediaHtml && fileKind ? `
${mediaHtml}
` : ''} + ${renderTagChips(item.tags)} `; return renderClearnetPage({ - title: `${title} — Oasis`, - ogTitle: item.title || 'Oasis', + title: `${item.title || (kind ? kindLabel(kind) : 'Oasis')} | Oasis`, + ogTitle: item.title || (kind ? kindLabel(kind) : 'Oasis'), ogDescription: item.description || '', ogImage: (kind === 'image') ? blob : null, extraCss, @@ -293,18 +552,82 @@ const renderClearnetMediaView = ({ kind, item }) => { }); }; +const renderClearnetPodcastView = ({ channel }) => { + const title = escapeHtml(channel.title || cnText('cnUntitled', 'Untitled')); + const desc = renderRichText(channel.description || ''); + const cover = channel.cover && channel.cover.blobId ? blobUrl(channel.cover.blobId) : null; + const coverIsVideo = !!(channel.cover && channel.cover.kind === 'video'); + const dateStr = channel.createdAt ? escapeHtml(new Date(channel.createdAt).toISOString().slice(0, 10)) : ''; + const extraCss = ` +.cn-media-meta{color:var(--fg-dim);font-size:13px;margin-bottom:16px;display:flex;gap:14px;flex-wrap:wrap;align-items:baseline} +.cn-media-title{color:var(--fg);font-size:26px;font-weight:700;margin:0 0 12px 0} +.cn-media-desc{color:var(--fg-soft);white-space:pre-wrap;line-height:1.6;margin:16px 0} +.cn-podcast-cover{max-width:240px;width:100%;border-radius:6px;border:1px solid var(--border);display:block;margin:12px 0} +.cn-episode{border:1px solid var(--border);border-radius:8px;padding:14px 16px;margin:12px 0;background:var(--bg-elev)} +.cn-episode h2{margin:0 0 8px 0;font-size:17px;color:var(--fg)} +.cn-episode .cn-episode-meta{color:var(--fg-dim);font-size:12px;margin-bottom:8px} +.cn-episode audio,.cn-episode video{width:100%;max-width:100%;display:block;border-radius:6px;background:#000} +.cn-episode p{color:var(--fg-soft);white-space:pre-wrap;line-height:1.5;margin:8px 0 0 0} +`; + const episodes = Array.isArray(channel.episodes) ? channel.episodes.slice().reverse() : []; + const epHtml = episodes.map(ep => { + const src = ep.media && ep.media.blobId ? blobUrl(ep.media.blobId) : null; + const player = !src ? '' : (ep.media.kind === 'video' + ? `` + : ``); + const epDate = ep.createdAt ? escapeHtml(new Date(ep.createdAt).toISOString().slice(0, 10)) : ''; + return `

#${Number(ep.number) || 0} · ${escapeHtml(ep.title || '')}

${epDate}
${player}${ep.description ? `

${renderRichText(ep.description)}

` : ''}
`; + }).join(''); + const body = ` +
${renderKindTag('podcast')}${dateStr ? `📅 ${dateStr}` : ''}🎧 ${episodes.length}
+

${title}

+ ${cover ? (coverIsVideo ? `` : `${title}`) : ''} + ${desc ? `

${desc}

` : ''} + ${epHtml ? `
${epHtml}` : ''} +`; + return renderClearnetPage({ + title: `${channel.title || cnText('cnUntitled', 'Untitled')} | Oasis`, + ogTitle: channel.title || 'Oasis', + ogDescription: channel.description || '', + ogImage: cover && !coverIsVideo ? cover : null, + extraCss, + body, + hubFeedId: channel.author || null + }); +}; + module.exports = { + cnZoom, + kindLabel, + renderTagChips, + renderClearnetPodcastView, escapeHtml, + renderRichText, + renderKindTag, blobIdOf, blobUrl, renderReachChip, + renderTribeOriginChip, + renderClearnetSelector, + renderClearnetSwitch, renderFediverseReach, renderContentStats, renderEncryptedChip, + renderTransportChip, + LICENSES, + licenseInfo, + normalizeLicense, + renderLicenseChip, + renderLicenseSelect, + renderClearnetLicense, renderDoubleEncryptionChip, renderClearnetUrlBlock, renderClearnetSearchForm, + renderClearnetHubSearch, renderClearnetPage, renderClearnetNotFound, + CLEARNET_PAGER_CSS, + paginateClearnet, + renderClearnetPager, renderClearnetMediaView }; diff --git a/src/views/comments_view.js b/src/views/comments_view.js new file mode 100644 index 0000000..b71f73f --- /dev/null +++ b/src/views/comments_view.js @@ -0,0 +1,98 @@ +const { div, p, h2, span, a, form, input, textarea, button, label, br, details, summary } = require("../server/node_modules/hyperaxe"); +const { TEXT_CAP } = require('../backend/long_text'); +const moment = require("../server/node_modules/moment"); +const { i18n, userLink, renderHideIcon, renderIcon } = require("./main_views"); +const { config } = require("../server/SSB_server.js"); +const { renderStyledText } = require("../backend/renderStyledText"); + +const COMMENT_ICON = "✑"; + +let commentsOpenByDefault = false; +const setCommentsOpen = (value) => { commentsOpenByDefault = !!value; }; + +const visibleComments = (comments) => (Array.isArray(comments) ? comments : []).filter(c => { + const t = c && c.value && c.value.content && c.value.content.text; + return t && String(t).trim(); +}); + +const renderCommentCard = (c, extra = null, returnTo = null) => { + const author = (c && c.value && c.value.author) || ""; + const ts = (c && c.value && c.value.timestamp) || (c && c.timestamp); + const absDate = ts ? moment(ts).format("YYYY/MM/DD HH:mm") : ""; + const relDate = ts ? moment(ts).fromNow() : ""; + const content = (c && c.value && c.value.content) || {}; + const rootId = content.fork || content.root || null; + const text = content.text || ""; + const own = !!author && !!(config && config.keys) && author === config.keys.id; + + return div({ class: own ? "votations-comment-card own-comment" : "votations-comment-card", id: c && c.key ? c.key : undefined }, + own && c.key + ? form({ method: "POST", action: `/comments/delete/${encodeURIComponent(c.key)}`, class: "comment-delete-form" }, + button({ type: "submit", class: "btn-singleview btn-delete", title: i18n.chatDelete }, renderIcon("close"))) + : null, + !own && c.key + ? form({ method: "POST", action: `/hide/${encodeURIComponent(c.key)}`, class: "comment-delete-form" }, + input({ type: "hidden", name: "returnTo", value: returnTo || (rootId ? `/thread/${encodeURIComponent(rootId)}` : "/activity") }), + input({ type: "hidden", name: "label", value: String(text).replace(/\s+/g, " ").trim().slice(0, 80) }), + button({ type: "submit", class: "btn-singleview btn-hide", title: i18n.hideContent }, renderHideIcon())) + : null, + span({ class: "created-at" }, + author ? userLink(author) : span("(unknown)"), + absDate ? span(" | ") : "", + absDate ? span({ class: "votations-comment-date" }, absDate) : "", + relDate ? span({ class: "votations-comment-date" }, " | ", i18n.sendTime) : "", + relDate && rootId ? a({ href: `/thread/${encodeURIComponent(rootId)}#${encodeURIComponent(c.key)}` }, relDate) : "" + ), + p({ class: "votations-comment-text" }, ...renderStyledText(String(text), { zoomImages: true })), + extra + ); +}; + +const renderCommentsSection = ({ action, comments = [], returnTo = null, closedNote = null, extraClass = "", open = false, commentExtra = null } = {}) => { + const list = visibleComments(comments); + return details({ class: "comments-collapse" + (extraClass ? ` ${extraClass}` : ""), ...((open || commentsOpenByDefault) ? { open: true } : {}), ...(process.env.OASIS_MOBILE === '1' ? { id: "comments" } : {}) }, + summary({ class: list.length > 0 ? "comments-summary engage-on" : "comments-summary" }, + span({ class: "comments-summary-icon" }, COMMENT_ICON), + span({ class: "comments-summary-count" }, `(${list.length})`) + ), + div({ class: "vote-comments-section" }, + list.length + ? div({ class: "comments-list" }, ...list.map((c, i) => { + const card = renderCommentCard(c, typeof commentExtra === "function" ? commentExtra(c) : null, returnTo); + return i === list.length - 1 ? div({ id: "comments-latest", class: "comments-latest-anchor" }, card) : card; + })) + : (closedNote ? null : p({ class: "votations-no-comments" }, i18n.voteNoCommentsYet)), + closedNote + ? p({ class: "muted" }, closedNote) + : div({ class: "comment-form-wrapper" }, + h2({ class: "comment-form-title" }, i18n.voteNewCommentLabel), + form({ method: "POST", action, class: "comment-form", enctype: "multipart/form-data" }, + returnTo ? input({ type: "hidden", name: "returnTo", value: returnTo }) : null, + textarea({ name: "text", rows: 4, maxlength: String(TEXT_CAP), class: "comment-textarea", placeholder: i18n.voteNewCommentPlaceholder, required: true }), + div({ class: "comment-file-upload" }, label(i18n.uploadMedia), input({ type: "file", name: "blob" })), + br(), + button({ type: "submit", class: "comment-submit-btn" }, i18n.voteNewCommentButton) + ) + ) + ) + ); +}; + +const renderCommentsLink = ({ href, count = 0 } = {}) => { + if (!href) return null; + const total = Number(count) || 0; + const target = process.env.OASIS_MOBILE === '1' ? `${href}#comments` : href; + return a({ href: target, class: total > 0 ? "comments-summary engage-on comments-link" : "comments-summary comments-link" }, + span({ class: "comments-summary-icon" }, COMMENT_ICON), + span({ class: "comments-summary-count" }, `(${total})`) + ); +}; + +module.exports = { + COMMENT_ICON, + renderCommentsLink, + visibleComments, + renderCommentCard, + renderCommentsSection, + setCommentsOpen +}; diff --git a/src/views/courts_view.js b/src/views/courts_view.js index ef8d681..5f9fa9d 100644 --- a/src/views/courts_view.js +++ b/src/views/courts_view.js @@ -1,6 +1,7 @@ const { form, button, div, h2, p, section, input, label, br, a, span, table, thead, tbody, tr, th, td, textarea, select, option, ul, li, img } = require('../server/node_modules/hyperaxe'); const moment = require('../server/node_modules/moment'); -const { template, i18n, userLink, renderStateChip } = require('./main_views'); +const { template, i18n, userLink, renderStateChip, renderModuleStatsBy, paged } = require('./main_views'); +const { renderStyledText, safeExternalHref } = require('../backend/renderStyledText'); const CourtsE2EChip = () => renderStateChip('encrypted', '🔒', i18n.encryptedChipLabel || 'E2E'); @@ -31,6 +32,19 @@ const CASE_TITLE_PRESETS = [ 'Governance dispute' ]; +const PRESET_KEYS = { + 'Minor conflict': 'courtsPresetMinorConflict', + 'Moderate conflict': 'courtsPresetModerateConflict', + 'Severe conflict': 'courtsPresetSevereConflict', + 'Harassment or abuse': 'courtsPresetHarassment', + 'Content moderation': 'courtsPresetContentModeration', + 'Ban or restriction': 'courtsPresetBanRestriction', + 'Governance dispute': 'courtsPresetGovernanceDispute' +}; +const presetLabel = (t) => i18n[PRESET_KEYS[t]] || t; +const caseTitle = (title) => CASE_TITLE_PRESETS.reduce((acc, t) => acc.split(t).join(presetLabel(t)), String(title || '')); +const statusLabel = (status) => String(i18n[`courtsStatus${String(status || '').toUpperCase()}`] || status || '').toUpperCase(); + const FILTERS = [ { value: 'cases', key: 'courtsFilterCases' }, { value: 'mycases', key: 'courtsFilterMyCases' }, @@ -41,12 +55,12 @@ const FILTERS = [ { value: 'open', key: 'courtsFilterOpenCase' } ]; -const Tabs = (active) => +const Tabs = (active, emptyCourts = false) => div( { class: 'filters' }, form( { method: 'GET', action: '/courts' }, - FILTERS.map((f) => { + FILTERS.filter((f) => !emptyCourts || f.value === 'open' || f.value === 'rules' || (f.value === active && active !== 'cases')).map((f) => { const isOpen = f.value === 'open'; const cls = isOpen @@ -67,7 +81,7 @@ const Tabs = (active) => ) ); -const CaseForm = () => +const CaseForm = (prefill = {}) => div( { class: 'div-center' }, h2(i18n.courtsCaseFormTitle), @@ -82,14 +96,15 @@ const CaseForm = () => type: 'text', name: 'titleSuffix', required: true, - placeholder: 'Subject or short description' + placeholder: i18n.courtsCaseSubjectPlaceholder, + value: prefill.titleSuffix || '' }), br(), - label('Case type'), + label(i18n.courtsCaseType), br(), select( { name: 'titlePreset', required: true }, - CASE_TITLE_PRESETS.map((t) => option({ value: t }, t)) + CASE_TITLE_PRESETS.map((t) => option({ value: t, ...(prefill.titlePreset === t ? { selected: true } : {}) }, presetLabel(t))) ), br(), br(), @@ -101,7 +116,8 @@ const CaseForm = () => placeholder: i18n.courtsCaseRespondentPh, required: true, pattern: '^@[A-Za-z0-9+/]+=*\\.ed25519$', - title: i18n.courtsRespondentInvalid || 'Must be a valid SSB ID (@...ed25519)' + title: i18n.courtsRespondentInvalid || 'Must be a valid SSB ID (@...ed25519)', + value: prefill.respondentId || '' }), br(), label(i18n.courtsCaseMethod), @@ -109,7 +125,7 @@ const CaseForm = () => select( { name: 'method', required: true }, ['JUDGE', 'DICTATOR', 'POPULAR', 'MEDIATION', 'KARMATOCRACY'].map((m) => - option({ value: m }, i18n[`courtsMethod${m}`]) + option({ value: m, ...(prefill.method === m ? { selected: true } : {}) }, i18n[`courtsMethod${m}`]) ) ), br(), @@ -162,7 +178,7 @@ const EvidenceForm = (caseId) => }), br(), br(), - label(i18n.uploadMedia || 'Upload media (max-size: 50MB)'), + label(i18n.uploadMedia || 'Upload media (max-size: 75MB)'), br(), input({ type: 'file', name: 'image' }), br(), @@ -323,31 +339,6 @@ const AcceptSettlementForm = (caseId) => button({ type: 'submit', class: 'create-button' }, i18n.courtsSettlementAcceptBtn) ); -const VerdictVoteForm = (caseId) => - div( - { class: 'div-center' }, - h2(i18n.courtsVerdictVoteTitle), - form( - { - method: 'POST', - action: `/courts/cases/${encodeURIComponent(caseId)}/verdict/vote` - }, - label(i18n.courtsVerdictVoteLabel), - br(), - select( - { name: 'decision' }, - option({ value: 'ACCEPT' }, i18n.courtsVerdictVoteAccept), - option({ value: 'REJECT' }, i18n.courtsVerdictVoteReject) - ), - br(), - br(), - button( - { type: 'submit', class: 'create-button' }, - i18n.courtsVerdictVoteSubmit - ) - ) - ); - const shortId = (id) => { const s = String(id || ''); if (!s) return ''; @@ -359,60 +350,9 @@ const UserLinkCompact = (id) => id ? userLink(id) : span(''); const UserLinkFull = (id) => id ? userLink(id) : span(''); const renderRichTextNodes = (raw) => { - const text = String(raw || ''); + const text = String(raw || '').trim(); if (!text) return []; - const nodes = []; - let remaining = text; - const imgRegex = /!\[[^\]]*]\(([^)]+)\)/; - const linkRegex = /\[([^\]]+)]\((https?:\/\/[^)]+)\)/; - while (remaining.length) { - const imgMatch = imgRegex.exec(remaining); - const linkMatch = linkRegex.exec(remaining); - let next = null; - let type = null; - if (imgMatch && (!linkMatch || imgMatch.index < linkMatch.index)) { - next = imgMatch; - type = 'img'; - } else if (linkMatch) { - next = linkMatch; - type = 'link'; - } - if (!next) { - if (remaining.trim()) nodes.push(p(remaining)); - break; - } - const idx = next.index; - if (idx > 0) { - const before = remaining.slice(0, idx); - if (before.trim()) nodes.push(p(before)); - } - if (type === 'img') { - const ref = next[1]; - nodes.push( - img({ - class: 'evidence-image', - src: `/blob/${encodeURIComponent(ref)}`, - alt: 'evidence' - }) - ); - } else { - const labelText = next[1]; - const url = next[2]; - nodes.push( - a( - { - class: 'evidence-link', - href: url, - target: '_blank', - rel: 'noopener noreferrer' - }, - labelText - ) - ); - } - remaining = remaining.slice(idx + next[0].length); - } - return nodes; + return [p(...renderStyledText(text))]; }; const RichTextBlock = (raw) => { @@ -468,11 +408,11 @@ const CaseCard = (c) => { select( { name: 'preference' }, option( - { value: 'YES', selected: c.myPublicPreference === true }, + { value: 'YES', ...(c.myPublicPreference === true ? { selected: true } : {})}, i18n.courtsPublicPrefYes ), option( - { value: 'NO', selected: c.myPublicPreference === false }, + { value: 'NO', ...(c.myPublicPreference === false ? { selected: true } : {})}, i18n.courtsPublicPrefNo ) ), @@ -527,7 +467,7 @@ const CaseCard = (c) => { { class: 'card-header' }, div( { class: 'card-header__meta' }, - h2(c.title || ''), + h2(caseTitle(c.title)), div({ class: 'card-chips-row' }, CourtsE2EChip()), div( { class: 'card-field' }, @@ -537,7 +477,7 @@ const CaseCard = (c) => { div( { class: 'card-field' }, span({ class: 'card-label' }, i18n.courtsThStatus + ': '), - span({ class: 'card-value' }, String(c.status || '')) + span({ class: 'card-value' }, statusLabel(c.status)) ), c.answerBy ? div( @@ -683,11 +623,11 @@ const MyCaseCard = (c) => { select( { name: 'preference' }, option( - { value: 'YES', selected: c.myPublicPreference === true }, + { value: 'YES', ...(c.myPublicPreference === true ? { selected: true } : {})}, i18n.courtsPublicPrefYes ), option( - { value: 'NO', selected: c.myPublicPreference === false }, + { value: 'NO', ...(c.myPublicPreference === false ? { selected: true } : {})}, i18n.courtsPublicPrefNo ) ), @@ -745,13 +685,13 @@ const MyCaseCard = (c) => { div( { class: 'card-field' }, span({ class: 'card-label' }, i18n.courtsThCase + ': '), - span({ class: 'card-value' }, c.title || '') + span({ class: 'card-value' }, caseTitle(c.title)) ), div({ class: 'card-chips-row' }, CourtsE2EChip()), div( { class: 'card-field' }, span({ class: 'card-label' }, i18n.courtsThStatus + ': '), - span({ class: 'card-value' }, String(c.status || '')) + span({ class: 'card-value' }, statusLabel(c.status)) ), div( { class: 'card-field' }, @@ -899,8 +839,8 @@ const CasesTable = (rows = [], opts = {}) => { const bodyRows = rows.map((c) => { const role = showRole ? roleTextForCase(c) : ''; return tr( - td(c.title || ''), - td( + td({ 'data-label': i18n.courtsThCase }, caseTitle(c.title)), + td({ 'data-label': i18n.courtsAccuser }, c.accuser ? a( { @@ -911,10 +851,10 @@ const CasesTable = (rows = [], opts = {}) => { ) : '' ), - td(methodLabel(c.method)), - td(c.createdAt ? fmt(c.createdAt) : ''), - showRole ? td(role) : null, - td( + td({ 'data-label': i18n.courtsCaseMethod }, methodLabel(c.method)), + td({ 'data-label': i18n.courtsThCreatedAt }, c.createdAt ? fmt(c.createdAt) : ''), + showRole ? td({ 'data-label': i18n.courtsThRole || 'Role' }, role) : null, + td({ class: 'table-row-action' }, c.id ? form( { @@ -958,10 +898,10 @@ const NominationsTable = (nominations = [], currentUserId = '') => { currentUserId && String(n.judgeId || '') === String(currentUserId || ''); return tr( - td(userLink(n.judgeId)), - td(String(n.supports || 0)), - td(fmt(n.createdAt)), - td( + td({ 'data-label': i18n.courtsThJudge }, userLink(n.judgeId)), + td({ 'data-label': i18n.courtsThSupports }, String(n.supports || 0)), + td({ 'data-label': i18n.courtsThDate }, fmt(n.createdAt)), + td({ class: 'table-row-action' }, isSelf ? span('') : form( @@ -1022,7 +962,7 @@ const HistoryList = (rows = []) => { span({ class: 'card-label' }, i18n.courtsThStatus + ': '), span({ class: 'card-value' }, String(hh.status || '')) ), - h2(hh.title || ''), + h2(caseTitle(hh.title)), canShowDescription ? RichTextBlock(hh.description || '') : null, form( { @@ -1063,21 +1003,23 @@ const RulesContent = () => { ); }; -const CaseSearch = (filter, search = '') => +const CaseSearch = (filter, search = '', items = null) => div( - { class: 'filters' }, + { class: 'filters activity-filter-chips activity-toolbar-row' }, + items ? renderModuleStatsBy(items, c => c.status === 'OPEN' ? 'OPEN' : c.status === 'IN_PROGRESS' ? 'IN_PROGRESS' : 'DONE', [{ value: 'OPEN', label: i18n.taskStatusOpen }, { value: 'IN_PROGRESS', label: i18n.taskStatusInProgress }, { value: 'DONE', label: i18n.taskStatusClosed }]) : null, form( - { method: 'GET', action: '/courts' }, + { method: 'GET', action: '/courts', class: 'filter-box' }, input({ type: 'hidden', name: 'filter', value: filter }), input({ type: 'text', name: 'search', placeholder: i18n.searchCasesPlaceholder, - value: search || '' + value: search || '', + class: 'filter-box__input' }), - br(), - button({ type: 'submit' }, i18n.applyFilters), - br() + div({ class: 'filter-box__controls' }, + button({ type: 'submit', class: 'filter-box__button' }, i18n.searchButton) + ) ) ); @@ -1133,7 +1075,7 @@ const CaseDetailsBlock = (c) => { { class: 'card-header' }, div( { class: 'card-header__meta' }, - h2(c.title || ''), + h2(caseTitle(c.title)), div({ class: 'card-chips-row' }, CourtsE2EChip()), c.method ? div( @@ -1150,7 +1092,7 @@ const CaseDetailsBlock = (c) => { div( { class: 'card-field' }, span({ class: 'card-label' }, i18n.courtsThStatus + ': '), - span({ class: 'card-value' }, String(c.status || '')) + span({ class: 'card-value' }, statusLabel(c.status)) ), c.createdAt ? div( @@ -1241,8 +1183,8 @@ const CaseDetailsBlock = (c) => { a( { class: 'evidence-link', - href: e.link, - target: '_blank', + href: safeExternalHref(e.link), + target: '_blank', rel: 'noopener noreferrer', rel: 'noopener noreferrer' }, e.link @@ -1360,29 +1302,29 @@ const courtsView = async (state) => { i18n.courtsTitle, section( div( - { class: 'tags-header' }, + { class: 'tags-header module-header-line' }, h2(i18n.courtsTitle), p(i18n.courtsDescription) ), - Tabs(filter), - filter === 'cases' ? CaseSearch(filter, search) : null + Tabs(filter, cases.length === 0 && myCases.length === 0 && history.length === 0 && nominations.length === 0 && !String(search || '').trim()), + filter === 'cases' && ((Array.isArray(cases) && cases.length > 0) || String(search || '').trim()) ? CaseSearch(filter, search, cases) : null ), section( - filter === 'cases' ? CasesTable(cases) : null, + filter === 'cases' ? CasesTable(paged(cases)) : null, filter === 'mycases' ? myCases.length - ? CasesTable(myCases, { showRole: true }) + ? CasesTable(paged(myCases), { showRole: true }) : div({ class: 'empty' }, p(i18n.courtsNoMyCases)) : null, filter === 'actions' ? myCases.length - ? MyCasesList(myCases) + ? MyCasesList(paged(myCases)) : div({ class: 'empty' }, p(i18n.courtsNoMyCases)) : null, - filter === 'judges' ? JudgesSection(nominations, userId) : null, - filter === 'history' ? HistoryList(history) : null, + filter === 'judges' ? JudgesSection(paged(nominations), userId) : null, + filter === 'history' ? HistoryList(paged(history)) : null, filter === 'rules' ? RulesContent() : null, - filter === 'open' ? CaseForm() : null + filter === 'open' ? CaseForm(state.prefill || {}) : null ) ); }; @@ -1393,7 +1335,7 @@ const courtsCaseView = async (state) => { i18n.courtsTitle, section( div( - { class: 'tags-header' }, + { class: 'tags-header module-header-line' }, h2(i18n.courtsTitle), p(i18n.courtsDescription) ), diff --git a/src/views/cv_view.js b/src/views/cv_view.js index 9725175..b6c8ff5 100644 --- a/src/views/cv_view.js +++ b/src/views/cv_view.js @@ -1,6 +1,7 @@ -const { form, button, div, h2, p, section, textarea, label, input, br, img, a, select, option, span } = require("../server/node_modules/hyperaxe"); -const { template, i18n, userLink, renderVisibilityChip } = require('./main_views'); -const { renderUrl } = require('../backend/renderUrl'); +const { form, button, div, h2, p, section, textarea, label, input, br, img, a, select, option, span, table, tr, td } = require("../server/node_modules/hyperaxe"); +const moment = require("../server/node_modules/moment"); +const { template, i18n, userLink, renderVisibilityChip, renderDocumentActions } = require('./main_views'); +const { renderStyledText } = require('../backend/renderStyledText'); const generateCVBox = (label, content, className) => { return div({ class: `cv-box ${className}` }, @@ -9,27 +10,13 @@ const generateCVBox = (label, content, className) => { ); }; -const generateTags = (tags) => { - return tags && tags.length - ? div( - tags.map(tag => - a({ - href: `/search?query=%23${encodeURIComponent(tag)}`, - class: "tag-link", - style: "margin-right:0.8em;margin-bottom:0.5em;" - }, `#${tag}`) - ) - ) - : null; -}; - exports.createCVView = async (cv = {}, editMode = false) => { - const title = editMode ? i18n.cvEditSectionTitle : i18n.cvCreateSectionTitle; + const title = i18n.cvTitle; return template( title, section( - div({ class: "tags-header" }, + div({ class: "tags-header module-header-line" }, h2(title), p(i18n.cvDescription) ), @@ -40,38 +27,54 @@ exports.createCVView = async (cv = {}, editMode = false) => { enctype: "multipart/form-data" }, + div({ class: "cv-box cv-ai-box" }, + div({ class: "poll-switch" }, + input({ type: "hidden", name: "aiManaged", value: "0" }), + label( + input({ type: "checkbox", name: "aiManaged", value: "1", ...(cv.aiManaged === false ? {} : { checked: true }) }), + " ", i18n.cvAiManaged + ) + ), + div({ class: "cv-threshold-row" }, + label({ for: "cv_match_threshold" }, `${i18n.cvMatchThreshold}: `), + input({ type: "number", id: "cv_match_threshold", name: "matchThreshold", min: "0", max: "100", step: "5", value: String(cv.matchThreshold != null ? cv.matchThreshold : 80) }) + ) + ), + generateCVBox(i18n.cvPersonal, [ label(i18n.cvNameLabel), br(), input({ type: "text", name: "name", required: true, value: cv.name || "" }), br(), label(i18n.cvDescriptionLabel), br(), - textarea({ name: "description", required: true, rows: 4 }, cv.description || ""), br(), + textarea({ maxlength: "5000", name: "description", required: true, rows: 4 }, cv.description || ""), br(), label(i18n.cvLanguagesLabel), br(), input({ type: "text", name: "languages", value: cv.languages || "" }), br(), label(i18n.cvPhotoLabel), br(), input({ type: "file", name: "image" }), br(), br(), + label(i18n.cvPdfLabel), br(), + input({ type: "file", name: "cvPdf", accept: "application/pdf" }), br(), br(), label(i18n.cvPersonalExperiencesLabel), br(), - textarea({ name: "personalExperiences", rows: 4 }, cv.personalExperiences || ""), br(), + textarea({ maxlength: "5000", name: "personalExperiences", rows: 4 }, cv.personalExperiences || ""), br(), label(i18n.cvPersonalSkillsLabel), br(), input({ type: "text", name: "personalSkills", required: true, value: (cv.personalSkills || []).join(", ") }), br() ], "personal"), generateCVBox(i18n.cvOasis, [ label(i18n.cvOasisExperiencesLabel), br(), - textarea({ name: "oasisExperiences", rows: 4 }, cv.oasisExperiences || ""), br(), + textarea({ maxlength: "5000", name: "oasisExperiences", rows: 4 }, cv.oasisExperiences || ""), br(), label(i18n.cvOasisSkillsLabel), br(), input({ type: "text", name: "oasisSkills", value: (cv.oasisSkills || []).join(", ") }), br() ], "oasis"), generateCVBox(i18n.cvEducational, [ label(i18n.cvEducationExperiencesLabel), br(), - textarea({ name: "educationExperiences", rows: 4 }, cv.educationExperiences || ""), br(), + textarea({ maxlength: "5000", name: "educationExperiences", rows: 4 }, cv.educationExperiences || ""), br(), label(i18n.cvEducationalSkillsLabel), br(), input({ type: "text", name: "educationalSkills", value: (cv.educationalSkills || []).join(", ") }), br() ], "education"), generateCVBox(i18n.cvProfessional, [ label(i18n.cvProfessionalExperiencesLabel), br(), - textarea({ name: "professionalExperiences", rows: 4 }, cv.professionalExperiences || ""), br(), + textarea({ maxlength: "5000", name: "professionalExperiences", rows: 4 }, cv.professionalExperiences || ""), br(), label(i18n.cvProfessionalSkillsLabel), br(), input({ type: "text", name: "professionalSkills", value: (cv.professionalSkills || []).join(", ") }), br() ], "professional"), @@ -81,19 +84,19 @@ exports.createCVView = async (cv = {}, editMode = false) => { input({ type: "text", name: "location", required: true, value: cv.location || "UNKNOWN" }), br(), label(i18n.cvStatusLabel), br(), select({ name: "status", required: true }, - option({ value: "AVAILABLE", selected: cv.status === "AVAILABLE FOR COLLABORATION" }, "AVAILABLE FOR COLLABORATION"), - option({ value: "UNAVAILABLE", selected: cv.status === "NOT CURRENTLY AVAILABLE" }, "NOT CURRENTLY AVAILABLE"), - option({ value: "LOOKING FOR WORK", selected: !cv.status || cv.status === "LOOKING FOR WORK" }, "LOOKING FOR WORK") + option({ value: "AVAILABLE", ...(cv.status === "AVAILABLE FOR COLLABORATION" ? { selected: true } : {})}, "AVAILABLE FOR COLLABORATION"), + option({ value: "UNAVAILABLE", ...(cv.status === "NOT CURRENTLY AVAILABLE" ? { selected: true } : {})}, "NOT CURRENTLY AVAILABLE"), + option({ value: "LOOKING FOR WORK", ...(!cv.status || cv.status === "LOOKING FOR WORK" ? { selected: true } : {})}, "LOOKING FOR WORK") ), br(), br(), label(i18n.cvPreferencesLabel), br(), select({ name: "preferences", required: true }, - option({ value: "IN PERSON", selected: cv.preferences === "IN-PERSON ONLY" }, "IN-PERSON ONLY"), - option({ value: "REMOTE WORKING", selected: !cv.preferences || cv.preferences === "REMOTE WORKING" }, "REMOTE-WORKING") + option({ value: "IN PERSON", ...(cv.preferences === "IN-PERSON ONLY" ? { selected: true } : {})}, "IN-PERSON ONLY"), + option({ value: "REMOTE WORKING", ...(!cv.preferences || cv.preferences === "REMOTE WORKING" ? { selected: true } : {})}, "REMOTE-WORKING") ), br(), br(), label(i18n.visibilityLabel || "Visibility"), br(), select({ name: "visibility" }, - option({ value: "PUBLIC", selected: (cv.visibility || "PUBLIC") === "PUBLIC" }, i18n.visibilityPublic || "Public"), - option({ value: "HIDDEN", selected: cv.visibility === "HIDDEN" }, i18n.visibilityHidden || "Hidden") + option({ value: "PUBLIC", ...((cv.visibility || "PUBLIC") === "PUBLIC" ? { selected: true } : {})}, i18n.visibilityPublic || "Public"), + option({ value: "HIDDEN", ...(cv.visibility === "HIDDEN" ? { selected: true } : {})}, i18n.visibilityHidden || "Hidden") ), br() ], "availability"), @@ -104,154 +107,162 @@ exports.createCVView = async (cv = {}, editMode = false) => { ) }; -exports.cvView = async (cv) => { +exports.cvView = async (cv, certificates = []) => { const title = i18n.cvTitle; if (!cv) { return template( title, section( - div({ class: "tags-header" }, + div({ class: "tags-header module-header-line" }, h2(title), p(i18n.cvDescription) ), - div({ class: "no-cv" }, - p(i18n.cvNoCV), + div({ class: "filters" }, form({ method: "GET", action: "/cv/create" }, - button({ type: "submit" }, i18n.cvCreateButton) + button({ type: "submit", class: "create-button" }, i18n.cvCreateButton) ) - ) + ), + div({ class: "no-content-box" }, p(i18n.cvNoCV)), + Array.isArray(certificates) && certificates.length + ? div({ class: "cv-section-block school-certificates" }, + h2(i18n.schoolCertificates), + certificates.map(cert => + div({ class: "school-certificate" }, + span("🎓 "), + a({ href: `/school/course/${encodeURIComponent(cert.courseId)}` }, cert.courseTitle || cert.courseId), + span(" — "), + userLink(cert.author), + span({ class: "school-certificate-date" }, ` (${moment(cert.createdAt).format("YYYY/MM/DD")})`), + a({ href: `/school/certificate/pdf/${encodeURIComponent(cert.courseId)}/${encodeURIComponent(cert.id)}`, class: "filter-btn school-cert-pdf" }, `⬇ ${i18n.schoolCertificatePdf || "PDF"}`) + ) + ) + ) + : null ) ) } - const hasPersonal = cv.contact || cv.name || cv.description || cv.photo || typeof cv.oasisContributor === "boolean" || (cv.personalSkills && cv.personalSkills.length); - const hasPersonalExp = cv.personalExperiences; - const hasOasis = cv.oasisExperiences || (cv.oasisSkills && cv.oasisSkills.length); - const hasEducational = cv.educationExperiences || cv.languages || (cv.educationalSkills && cv.educationalSkills.length); - const hasProfessional = cv.professionalExperiences || (cv.professionalSkills && cv.professionalSkills.length); - const hasAvailability = cv.location || cv.status || cv.preferences; + const skills = (list) => (Array.isArray(list) ? list.filter(Boolean) : []); + const allSkills = [ + ...skills(cv.personalSkills), + ...skills(cv.educationalSkills), + ...skills(cv.professionalSkills), + ...skills(cv.oasisSkills) + ]; + const uniqueSkills = Array.from(new Set(allSkills)); + + const renderSkillTags = (list) => skills(list).length + ? div({ class: "card-tags" }, + skills(list).map(tag => + a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link" }, `#${tag}`)) + ) + : null; + + const renderBlock = (heading, body, list) => (body || skills(list).length) + ? div({ class: "cv-box" }, + h2({ class: "job-section-title" }, heading), + body ? p({ class: "tribe-side-description" }, ...renderStyledText(String(body))) : null, + renderSkillTags(list) + ) + : null; + + const vis = (cv.visibility || 'PUBLIC').toUpperCase() === 'HIDDEN' ? 'HIDDEN' : 'PUBLIC'; + const nextVis = vis === 'PUBLIC' ? 'HIDDEN' : 'PUBLIC'; + + const infoRows = []; + const pushRow = (labelText, valueNode) => + infoRows.push(tr( + td({ class: "tribe-info-label" }, labelText), + td({ class: "tribe-info-value" }, valueNode) + )); + if (cv.location) pushRow(i18n.cvLocationLabel, cv.location); + if (cv.status) pushRow(i18n.cvStatusLabel, cv.status); + if (cv.preferences) pushRow(i18n.cvPreferencesLabel, cv.preferences); + if (cv.languages) pushRow(i18n.cvLanguagesLabel, String(cv.languages).toUpperCase()); + pushRow(i18n.cvAiManaged, cv.aiManaged === false ? i18n.switchOff : `${i18n.switchOn} · ${cv.matchThreshold != null ? cv.matchThreshold : 80}%`); + pushRow(i18n.cvCreatedAt, moment(cv.createdAt).format("YYYY/MM/DD HH:mm")); + if (cv.updatedAt) pushRow(i18n.cvUpdatedAt, moment(cv.updatedAt).format("YYYY/MM/DD HH:mm")); + + const cvSide = div({ class: "tribe-side" }, + div({ class: "shop-title-row" }, + h2({ class: "tribe-card-title" }, cv.name || i18n.unnamed || 'Anonymous') + ), + cv.photo + ? img({ loading: 'lazy', src: `/blob/${encodeURIComponent(cv.photo)}`, class: "cv-photo tribe-detail-image" }) + : null, + (cv.contact || cv.author) + ? div({ class: 'profile-qr' }, + a({ href: `/author/${encodeURIComponent(cv.contact || cv.author)}` }, + img({ class: 'profile-qr-img', src: `/qr/${encodeURIComponent(cv.contact || cv.author)}?size=240`, alt: 'QR' }))) + : null, + cv.contact ? p(userLink(cv.contact)) : null, + cv.pdf + ? p(a({ href: `/blob/${encodeURIComponent(cv.pdf)}`, target: "_blank", rel: "noopener", class: "filter-btn" }, "📄 " + i18n.cvPdfLabel)) + : null, + table({ class: "tribe-info-table jobs-info-table" }, ...infoRows), + uniqueSkills.length + ? div({ class: "tribe-card-members" }, + span({ class: "tribe-members-count" }, `${i18n.cvSkillsCount}: ${uniqueSkills.length}`) + ) + : null, + div({ class: "tribe-side-actions cv-visibility-row housing-status-row" }, + span({ class: "card-label" }, `${i18n.visibilityLabel || 'Visibility'}: `), + renderVisibilityChip(vis, i18n), + form({ method: "POST", action: `/cv/visibility/${encodeURIComponent(cv.id)}`, class: "inline-form" }, + input({ type: "hidden", name: "visibility", value: nextVis }), + button({ type: "submit", class: "filter-btn" }, + nextVis === 'PUBLIC' ? (i18n.visibilityMakePublic || 'Make public') : (i18n.visibilityMakeHidden || 'Make hidden') + ) + ) + ), + div({ class: "tribe-side-actions" }, + form({ method: "GET", action: `/cv/edit/${encodeURIComponent(cv.id)}` }, + button({ type: "submit", class: "update-btn" }, i18n.cvEditButton) + ), + form({ method: "POST", action: `/cv/delete/${encodeURIComponent(cv.id)}` }, + button({ type: "submit", class: "delete-btn" }, i18n.cvDeleteButton) + ) + ), + renderDocumentActions('cv', null) + ); + + const cvMain = div({ class: "tribe-main" }, + cv.description + ? div({ class: "job-section" }, + h2({ class: "job-section-title" }, i18n.cvDescriptionLabel), + p({ class: "tribe-side-description" }, ...renderStyledText(String(cv.description))) + ) + : null, + renderBlock(i18n.cvPersonal, cv.personalExperiences, cv.personalSkills), + renderBlock(i18n.cvEducationalView, cv.educationExperiences, cv.educationalSkills), + renderBlock(i18n.cvProfessionalView, cv.professionalExperiences, cv.professionalSkills), + renderBlock(i18n.cvOasisContributorView, cv.oasisExperiences, cv.oasisSkills), + Array.isArray(certificates) && certificates.length + ? div({ class: "cv-section-block school-certificates" }, + h2(i18n.schoolCertificates), + certificates.map(cert => + div({ class: "school-certificate" }, + span("🎓 "), + a({ href: `/school/course/${encodeURIComponent(cert.courseId)}` }, cert.courseTitle || cert.courseId), + span(" — "), + userLink(cert.author), + span({ class: "school-certificate-date" }, ` (${moment(cert.createdAt).format("YYYY/MM/DD")})`), + a({ href: `/school/certificate/pdf/${encodeURIComponent(cert.courseId)}/${encodeURIComponent(cert.id)}`, class: "filter-btn school-cert-pdf" }, `⬇ ${i18n.schoolCertificatePdf || "PDF"}`) + ) + ) + ) + : null + ); return template( title, section( - div({ class: "tags-header" }, + div({ class: "tags-header module-header-line" }, h2(title), p(i18n.cvDescription) ), - div({ class: "cv-section" }, - div({ class: "cv-item" }, ...[ - (() => { - const vis = (cv.visibility || 'PUBLIC').toUpperCase() === 'HIDDEN' ? 'HIDDEN' : 'PUBLIC'; - const next = vis === 'PUBLIC' ? 'HIDDEN' : 'PUBLIC'; - return div({ class: "cv-visibility-row" }, - span({ class: "card-label" }, `${i18n.visibilityLabel || 'Visibility'}: `), - renderVisibilityChip(vis, i18n), - " ", - form({ method: "POST", action: `/cv/visibility/${encodeURIComponent(cv.id)}`, class: "inline-form" }, - input({ type: "hidden", name: "visibility", value: next }), - button({ type: "submit", class: "filter-btn" }, - next === 'PUBLIC' ? (i18n.visibilityMakePublic || 'Make public') : (i18n.visibilityMakeHidden || 'Make hidden') - ) - ) - ); - })(), - div({ class: "cv-actions" }, - form({ method: "GET", action: `/cv/edit/${encodeURIComponent(cv.id)}` }, - button({ type: "submit" }, i18n.cvEditButton) - ), - form({ method: "POST", action: `/cv/delete/${encodeURIComponent(cv.id)}` }, - button({ type: "submit", class: "danger-btn" }, i18n.cvDeleteButton) - ) - ), - div({ class: "cv-meta" }, - p(`${i18n.cvCreatedAt}: ${new Date(cv.createdAt).toLocaleString()}`), - cv.updatedAt ? p(`${i18n.cvUpdatedAt}: ${new Date(cv.updatedAt).toLocaleString()}`) : null - ), - hasPersonal ? div({ class: "cv-box personal" }, ...[ - cv.photo - ? img({ - src: `/blob/${encodeURIComponent(cv.photo)}`, - class: "cv-photo" - }) - : null, - cv.name ? h2(`${cv.name}`) : null, - (cv.contact || cv.author) - ? a({ href: `/author/${encodeURIComponent(cv.contact || cv.author)}`, class: 'inhabitant-qr-link' }, - img({ class: 'inhabitant-qr-small', src: `/qr/${encodeURIComponent(cv.contact || cv.author)}?size=120`, alt: 'QR' })) - : null, - cv.contact ? p(userLink(cv.contact)) : null, - cv.description ? p(...renderUrl(`${cv.description}`)) : null, - (cv.personalSkills && cv.personalSkills.length) - ? div( - cv.personalSkills.map(tag => - a({ - href: `/search?query=%23${encodeURIComponent(tag)}`, - class: "tag-link", - style: "margin-right:0.8em;margin-bottom:0.5em;" - }, `#${tag}`) - ) - ) - : null - ]) : null, - hasPersonal ? div({ class: "cv-box personal" }, ...[ - h2(i18n.cvLanguagesLabel), - cv.languages ? p(`${cv.languages.toUpperCase()}`) : null - ]) : null, - hasEducational ? div({ class: "cv-box education" }, ...[ - h2(i18n.cvEducationalView), - cv.educationExperiences ? p(...renderUrl(`${cv.educationExperiences}`)) : null, - (cv.educationalSkills && cv.educationalSkills.length) - ? div( - cv.educationalSkills.map(tag => - a({ - href: `/search?query=%23${encodeURIComponent(tag)}`, - class: "tag-link", - style: "margin-right:0.8em;margin-bottom:0.5em;" - }, `#${tag}`) - ) - ) - : null - ]) : null, - hasProfessional ? div({ class: "cv-box professional" }, ...[ - h2(i18n.cvProfessionalView), - cv.professionalExperiences ? p(...renderUrl(`${cv.professionalExperiences}`)) : null, - (cv.professionalSkills && cv.professionalSkills.length) - ? div( - cv.professionalSkills.map(tag => - a({ - href: `/search?query=%23${encodeURIComponent(tag)}`, - class: "tag-link", - style: "margin-right:0.8em;margin-bottom:0.5em;" - }, `#${tag}`) - ) - ) - : null - ]) : null, - hasOasis ? div({ class: "cv-box oasis" }, ...[ - h2(i18n.cvOasisContributorView), - cv.oasisExperiences ? p(...renderUrl(`${cv.oasisExperiences}`)) : null, - (cv.oasisSkills && cv.oasisSkills.length) - ? div( - cv.oasisSkills.map(tag => - a({ - href: `/search?query=%23${encodeURIComponent(tag)}`, - class: "tag-link", - style: "margin-right:0.8em;margin-bottom:0.5em;" - }, `#${tag}`) - ) - ) - : null - ]) : null, - hasAvailability ? div({ class: "cv-box availability" }, ...[ - h2(i18n.cvAvailabilityView), - cv.location ? p(`${i18n.cvLocationLabel}: ${cv.location}`) : null, - cv.status ? p(`${i18n.cvStatusLabel}: ${cv.status}`) : null, - cv.preferences ? p(`${i18n.cvPreferencesLabel}: ${cv.preferences}`) : null - ]) : null - ]) - ) + div({ class: "tribe-details" }, cvSide, cvMain) ) ); }; - diff --git a/src/views/data_view.js b/src/views/data_view.js new file mode 100644 index 0000000..4462bf4 --- /dev/null +++ b/src/views/data_view.js @@ -0,0 +1,252 @@ +const { div, h2, p, section, button, form, a, input, span, table, tr, td, ul, li } = require("../server/node_modules/hyperaxe"); +const { template, i18n, userLink, renderModuleStats, paged, renderIcon } = require("./main_views"); + +const KIND_LABEL = { + inhabitants: () => i18n.dataKindInhabitants, + jobs: () => i18n.dataKindJobs, + projects: () => i18n.dataKindProjects, + events: () => i18n.dataKindEvents, + tribes: () => i18n.dataKindTribes, + market: () => i18n.dataKindMarket, + housing: () => i18n.dataKindHousing, + industry: () => i18n.dataKindIndustry, + tasks: () => i18n.dataKindTasks, + reports: () => i18n.dataKindReports, + votes: () => i18n.dataKindVotes, + audios: () => i18n.audioTitle, + videos: () => i18n.videoTitle, + images: () => i18n.imageTitle, + documents: () => i18n.documentTitle, + bookmarks: () => i18n.bookmarkTitle, + torrents: () => i18n.typeTorrent, + files: () => i18n.typeFile, + chats: () => i18n.chatsTitle, + pads: () => i18n.padsTitle, + rooms: () => i18n.roomsTitle, + maps: () => i18n.mapTitle, + calendars: () => i18n.calendarsTitle, + forum: () => i18n.forumTitle, + school: () => i18n.schoolTitle, + wiki: () => i18n.wikiTitle, + emergencies: () => i18n.emergenciesTitle, + mailing: () => i18n.mailingTitle, + logistics: () => i18n.logisticsTitle, + podcasts: () => i18n.podcastsTitle, + campaigns: () => i18n.campaignsTitle, + blogs: () => i18n.blogTitle +}; + +const REASON_LABEL = { + mutual: () => i18n.dataReasonMutual, + following: () => i18n.dataReasonFollowing, + supportsYou: () => i18n.dataReasonSupportsYou, + tribe: () => i18n.dataReasonTribe, + alike: () => i18n.dataReasonAlike, + cv: () => i18n.dataReasonCv, + content: () => i18n.dataReasonContent, + pinned: () => i18n.dataReasonPinned, + related: () => i18n.dataReasonRelated, + rated: () => i18n.dataReasonRated, + near: () => i18n.dataReasonNear +}; + +const FILTER_COLUMNS = [ + ["ALL", "RECENT", "TOP"], + ["INHABITANTS", "TRIBES"], + ["VOTES", "EVENTS", "CALENDARS", "TASKS", "REPORTS", "EMERGENCIES", "MAILING", "CAMPAIGNS"], + ["MARKET", "HOUSING", "JOBS", "PROJECTS", "INDUSTRY", "LOGISTICS"], + ["BLOGS", "FORUM", "CHATS", "ROOMS", "PADS", "WIKI", "MAPS", "SCHOOL"], + ["AUDIOS", "BOOKMARKS", "DOCUMENTS", "FILES", "IMAGES", "TORRENTS", "VIDEOS", "PODCASTS"] +]; + +const kindLabel = (kind) => (KIND_LABEL[kind] ? KIND_LABEL[kind]() : String(kind || '').toUpperCase()); + +const reasonButton = (value, label, current, filter) => + form({ method: "GET", action: "/data" }, + filter && filter !== "ALL" ? input({ type: "hidden", name: "filter", value: filter }) : null, + value ? input({ type: "hidden", name: "reason", value }) : null, + button({ type: "submit", class: current === value ? "filter-btn active" : "filter-btn" }, String(label).toUpperCase()) + ); + +const filterButton = (value, label, current, reason) => + form({ method: "GET", action: "/data" }, + input({ type: "hidden", name: "filter", value }), + reason ? input({ type: "hidden", name: "reason", value: reason }) : null, + button({ type: "submit", class: current === value ? "filter-btn active" : "filter-btn" }, String(label).toUpperCase()) + ); + +const sectionId = (kind) => `data-${String(kind || '').toLowerCase()}`; + +const renderFilters = (q, payload = {}, kindsPresent = []) => { + const matches = Array.isArray(payload.matches) ? payload.matches : []; + const reason = String(payload.reason || ""); + const filter = String(payload.filter || "ALL").toUpperCase(); + const isKindFilter = !FILTER_COLUMNS[0].includes(filter); + if (!matches.length && !String(q || "").trim() && !reason && !isKindFilter) return null; + const avail = (Array.isArray(payload.reasonsAvail) ? payload.reasonsAvail : []).filter(r => REASON_LABEL[r]); + const present = new Set(kindsPresent); + const columns = [FILTER_COLUMNS[0]].concat( + FILTER_COLUMNS.slice(1) + .map(col => col.filter(k => present.has(k.toLowerCase()))) + .filter(col => col.length) + ); + return section( + div({ class: "activity-sub-filter" }, + reasonButton("", i18n.dataFilterAll, reason, filter), + ...avail.map(r => reasonButton(r, REASON_LABEL[r](), reason, filter)) + ), + div({ class: "activity-filter-grid" }, + ...columns.map(col => + div({ class: "activity-filter-col" }, + ...col.map(k => filterButton(k, FILTER_COLUMNS[0].includes(k) ? (i18n[k + "Button"] || k) : kindLabel(k.toLowerCase()), filter, reason)) + ) + ) + ), + div({ class: "data-search activity-filter-chips activity-toolbar-row" }, + renderModuleStats(matches.length), + form({ method: "GET", action: "/data", class: "filter-box" }, + reason ? input({ type: "hidden", name: "reason", value: reason }) : null, + filter !== "ALL" ? input({ type: "hidden", name: "filter", value: filter }) : null, + input({ type: "text", name: "q", value: q || "", placeholder: i18n.dataSearchPlaceholder, class: "filter-box__input" }), + div({ class: "filter-box__controls" }, + button({ type: "submit", class: "filter-box__button" }, i18n.searchButton) + ) + ) + ) + ); +}; + +const scorePct = (score) => ((Number(score) || 0) * 100).toFixed(2).replace(/\.?0+$/, ''); +const scaleOf = (pct) => Math.min(4, Math.floor((Number(pct) || 0) / 20)); + +const renderCohesion = (c) => { + const measurable = c.entities >= 2; + const pct = measurable ? Number(c.percent) || 0 : 0; + return section( + div({ class: "tags-header module-header-line" }, + h2(i18n.dataCohesionTitle), + p(i18n.dataCohesionHint) + ), + div({ class: "data-cohesion" }, + p({ class: "data-cohesion-value" }, + span({ class: "card-label data-cohesion-label" }, `${i18n.dataCcShort}: `), + measurable ? `${c.percent}%` : "—"), + div({ class: "poll-bar data-cohesion-bar" }, + div({ class: `poll-bar-fill poll-bar-fill-${Math.round(pct / 5) * 5} data-scale-${scaleOf(pct)}` })), + div({ class: "data-cohesion-figures" }, + div({ class: "data-figure" }, + span({ class: "data-figure-value" }, String(c.entities)), + span({ class: "data-figure-label" }, i18n.dataStatEntities) + ), + div({ class: "data-figure" }, + span({ class: "data-figure-value" }, String(c.distinctTerms)), + span({ class: "data-figure-label" }, i18n.dataStatTerms) + ), + div({ class: "data-figure" }, + span({ class: "data-figure-value" }, String(c.pairs)), + span({ class: "data-figure-label" }, i18n.dataStatPairs) + ), + div({ class: "data-figure" }, + span({ class: "data-figure-value" }, String(c.isolated || 0)), + span({ class: "data-figure-label" }, i18n.dataStatIsolated) + ), + div({ class: "data-figure" }, + span({ class: "data-figure-value" }, String(c.connected || 0)), + span({ class: "data-figure-label" }, i18n.dataStatConnected) + ) + ) + ) + ); +}; + +const TITLE_MAX = 120; +const shortTitle = (title) => { + const t = String(title || "").replace(/\s+/g, " ").trim(); + return t.length > TITLE_MAX ? `${t.slice(0, TITLE_MAX - 1)}…` : t; +}; + +const shortId = (id) => { + const value = String(id || ""); + return value.length > 14 ? `${value.slice(0, 12)}…` : value; +}; + +const renderMatchRow = (m, isBest) => { + const common = Array.isArray(m.common) ? m.common : []; + const reasons = (Array.isArray(m.reasons) ? m.reasons : []).filter(r => REASON_LABEL[r]); + return div({ class: isBest ? "data-card data-best-card" : "data-card" }, + div({ class: "data-side-top" }, + span({ class: "data-kind-chip" }, String(kindLabel(m.kind)).toUpperCase()), + span({ class: "card-label data-score-label" }, `${isBest ? i18n.dataBestMatch : (i18n.dataCcShort || "CC")}: `), + span({ class: isBest ? "data-score-value data-best-score" : "data-score-value" }, `${scorePct(m.score)}%`), + a({ href: m.href, class: "btn-singleview btn-content data-side-visit", title: i18n.visitContent }, renderIcon("open")) + ), + a({ href: m.href, class: "data-card-title" }, shortTitle(m.title) || shortId(m.id)), + span({ class: "data-card-author" }, userLink(m.author)), + common.length + ? div({ class: "data-common-row" }, + span({ class: "card-label" }, `${i18n.dataCommonTerms}: `), + ...common.slice(0, 8).map(t => + a({ href: `/search?query=%23${encodeURIComponent(t)}`, class: "tag-link" }, `#${t}`)) + ) + : null, + reasons.length + ? div({ class: "data-reasons-row" }, + ...reasons.map(r => span({ class: "data-kind-chip data-reason-chip" }, REASON_LABEL[r]())) + ) + : null + ); +}; + +const groupByKind = (matches) => { + const groups = new Map(); + for (const m of matches) { + if (!groups.has(m.kind)) groups.set(m.kind, []); + groups.get(m.kind).push(m); + } + const ordered = [...groups.entries()].map(([kind, items]) => { + items.sort((x, y) => (y.score || 0) - (x.score || 0) || (y.ts || 0) - (x.ts || 0)); + return { kind, items }; + }); + ordered.sort((x, y) => (y.items[0].score || 0) - (x.items[0].score || 0)); + return ordered; +}; + +exports.dataView = async (payload = {}) => { + const q = payload.q || ''; + const reason = String(payload.reason || ''); + + const matches = Array.isArray(payload.matches) ? payload.matches : []; + const emptyData = matches.length === 0 && payload.hasProfile === false && !String(q || '').trim() && !reason; + const groups = groupByKind(matches); + const best = groups.length ? groups[0].items[0] : null; + + return template( + i18n.dataTitle, + section(div({ class: "tags-header module-header-line" }, h2(i18n.dataTitle), p(i18n.dataDescription))), + renderFilters(q, { ...payload, matches }, payload.kindsMatched && typeof payload.kindsMatched === "object" ? Object.keys(payload.kindsMatched) : groups.map(g => g.kind)), + emptyData ? null : (payload.cohesion ? renderCohesion(payload.cohesion) : null), + section( + emptyData ? null : div({ class: "tags-header" }, + h2(i18n.dataMatchesTitle), + p(i18n.dataMatchesHint) + ), + best + ? div({ class: "data-section data-top-best" }, + h2({ class: "data-section-title" }, String(i18n.dataBestMatch).toUpperCase()), + div({ class: "data-list" }, renderMatchRow(best, true)) + ) + : null, + ...groupByKind(paged(groups.flatMap(g => g.items))).map(g => + div({ id: sectionId(g.kind), class: "data-section" }, + h2({ class: "data-section-title" }, `${String(kindLabel(g.kind)).toUpperCase()} (${(groups.find(x => x.kind === g.kind) || g).items.length})`), + div({ class: "data-list" }, ...g.items.map((m, idx) => renderMatchRow(m, idx === 0))) + ) + ), + matches.length + ? null + : div({ class: "no-content-box" }, p({ class: "no-content" }, payload.hasProfile === false + ? i18n.dataNoProfile + : i18n.dataNoMatches)) + ) + ); +}; diff --git a/src/views/dev_view.js b/src/views/dev_view.js new file mode 100644 index 0000000..ccc1468 --- /dev/null +++ b/src/views/dev_view.js @@ -0,0 +1,271 @@ +const { form, button, div, h2, p, section, input, a, span, strong, ul, li, table, thead, tbody, tr, td, th, pre, code, select, option } = require("../server/node_modules/hyperaxe") +const { template, i18n } = require("./main_views") + +const FILTERS = [ + { key: "tree", href: "/dev", i18n: "devFilterFiles", fallback: "FILES" }, + { key: "map", href: "/dev/map", i18n: "devFilterModules", fallback: "MODULES" }, + { key: "translations", href: "/dev?group=translations", i18n: "devFilterTranslations", fallback: "TRANSLATIONS" }, + { key: "styles", href: "/dev?group=styles", i18n: "devFilterStyles", fallback: "STYLES" }, + { key: "tests", href: "/dev?group=tests", i18n: "devFilterTests", fallback: "TESTS" } +] + +const ICONS = { + folder: "📁", + parent: "↰", + ".js": "📜", + ".mjs": "📜", + ".json": "🧾", + ".css": "🎨", + ".md": "📖", + ".sh": "⌨", + ".html": "🌐", + ".txt": "📄", + ".yml": "🧾", + ".yaml": "🧾" +} + +const safeText = (v) => String(v == null ? "" : v) + +const iconFor = (file) => (file.viewable ? (ICONS[file.ext] || "📄") : "📦") + +const fmtSize = (bytes) => { + const n = Number(bytes) || 0 + if (n < 1024) return `${n} B` + if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB` + return `${(n / (1024 * 1024)).toFixed(1)} MB` +} + +const fmtCount = (n) => String(Number(n) || 0) + +const fileHref = (path, from) => `/dev/file?path=${encodeURIComponent(path)}${from ? `&from=${from}` : ""}` +const dirHref = (path) => (path ? `/dev?path=${encodeURIComponent(path)}` : "/dev") + +const renderHeader = (active) => [ + div({ class: "tags-header module-header-line" }, + h2(i18n.devTitle || "Developer"), + p(i18n.devDescription || "Explore the source code running on this node.") + ), + div({ class: "filters" }, + div({ class: "ui-toolbar ui-toolbar--filters dev-toolbar" }, + FILTERS.map(f => a({ + class: active === f.key ? "filter-btn active" : "filter-btn", + href: f.href + }, i18n[f.i18n] || f.fallback)) + ) + ) +] + +const groupLabel = (key) => { + const f = FILTERS.find(x => x.key === key) + return f ? (i18n[f.i18n] || f.fallback) : String(key || "") +} + +const renderPathRow = (parents, currentIsFile, group) => (group + ? div({ class: "dev-breadcrumb" }, + a({ href: "/dev" }, i18n.devRoot || "root"), + span({ class: "dev-breadcrumb-sep" }, " / "), + span({ class: "dev-breadcrumb-current" }, groupLabel(group))) + : renderBreadcrumb(parents, currentIsFile)) + +const renderStats = (stats) => { + const bit = (labelKey, fallback, value) => + span(`${i18n[labelKey] || fallback}: `, strong(String(value))) + return div({ class: "module-stats-line" }, + bit("devVersion", "Version", stats.version || "-"), + bit("devNodeVersion", "Node", stats.node || "-"), + bit("devStatsFiles", "Files", fmtCount(stats.files)), + bit("devStatsLines", "Lines", fmtCount(stats.lines)), + bit("devStatsModules", "Modules", fmtCount(stats.modules)), + bit("devStatsLanguages", "Languages", fmtCount(stats.languages)), + bit("devStatsTests", "Tests", fmtCount(stats.testMods)) + ) +} + +const renderBreadcrumb = (parents, currentIsFile) => div({ class: "dev-breadcrumb" }, + a({ href: "/dev" }, i18n.devRoot || "root"), + (parents || []).map((seg, idx) => [ + span({ class: "dev-breadcrumb-sep" }, " / "), + (currentIsFile && idx === parents.length - 1) + ? span({ class: "dev-breadcrumb-current" }, safeText(seg.name)) + : a({ href: dirHref(seg.path) }, safeText(seg.name)) + ]) +) + +const renderSearchForm = (query, ext, stats = null) => div({ class: "dev-search activity-filter-chips activity-toolbar-row" }, + stats ? renderStats(stats) : null, + form({ method: "GET", action: "/dev/search", class: "filter-box" }, + input({ type: "text", name: "q", value: safeText(query), placeholder: i18n.devSearchPlaceholder || "Text to find in the code", minlength: "2", required: true, class: "filter-box__input" }), + div({ class: "filter-box__controls" }, + select({ name: "ext", class: "filter-box__select" }, + option({ value: "", ...(ext ? {} : { selected: true }) }, i18n.devAnyExtension || "Any file"), + [".js", ".json", ".css", ".md", ".sh"].map(x => option({ value: x, ...(ext === x ? { selected: true } : {}) }, x)) + ), + button({ type: "submit", class: "filter-box__button" }, i18n.searchButton || "Search") + ) + ) +) + +const renderDesktop = (tree) => { + const dirs = (tree && tree.dirs) || [] + const files = (tree && tree.files) || [] + const parents = (tree && tree.parents) || [] + const items = [] + if (tree.path) { + const up = parents.length > 1 ? parents[parents.length - 2].path : "" + items.push(a({ class: "dev-icon dev-icon-up", href: dirHref(up) }, + span({ class: "dev-icon-glyph" }, ICONS.parent), + span({ class: "dev-icon-name" }, ".."), + span({ class: "dev-icon-meta" }, i18n.devParentFolder || "Parent folder") + )) + } + for (const d of dirs) { + items.push(a({ class: "dev-icon", href: dirHref(d.path) }, + span({ class: "dev-icon-glyph" }, ICONS.folder), + span({ class: "dev-icon-name" }, safeText(d.name)), + span({ class: "dev-icon-meta" }, i18n.devOpenFolder || "open") + )) + } + for (const f of files) { + const glyph = span({ class: "dev-icon-glyph" }, iconFor(f)) + const name = span({ class: "dev-icon-name" }, safeText(f.name)) + const meta = span({ class: "dev-icon-meta" }, f.viewable ? fmtSize(f.size) : (i18n.devNotViewable || "not viewable")) + items.push(f.viewable + ? a({ class: "dev-icon", href: fileHref(f.path) }, glyph, name, meta) + : div({ class: "dev-icon dev-icon-off" }, glyph, name, meta)) + } + if (!items.length) return p({ class: "dev-hint" }, i18n.devEmptyFolder || "This folder is empty.") + return div({ class: "dev-desktop" }, ...items) +} + +const renderPager = (file) => div({ class: "dev-pager" }, + file.hasPrev + ? a({ class: "filter-btn", href: fileHref(file.path, Math.max(1, file.from - file.pageLines)) }, i18n.devPrevPage || "Previous lines") + : null, + file.hasNext + ? a({ class: "filter-btn", href: fileHref(file.path, file.to + 1) }, i18n.devNextPage || "Next lines") + : null +) + +exports.devTreeView = async (tree, stats, active) => { + return template( + i18n.devTitle || "Developer", + section( + ...renderHeader(active || (tree.group ? tree.group : "tree")), + renderSearchForm("", "", stats), + renderPathRow(tree.parents, false, tree.group), + renderDesktop(tree) + ) + ) +} + +exports.devFileView = async (file) => { + const reportTitle = `${file.path}:${file.from}` + const reportDescription = `${i18n.devReportContext || "Found while reading the Oasis source code"}: /${file.path} (${i18n.devLine || "line"} ${file.from})` + const taskTitle = `${i18n.devTaskPrefix || "Review"} ${file.path}` + return template( + i18n.devTitle || "Developer", + section( + ...renderHeader("tree"), + renderPathRow(file.parents, true, null), + div({ class: "dev-file-head" }, + span({ class: "dev-icon-glyph dev-file-glyph" }, iconFor({ ext: `.${String(file.name).split('.').pop()}`, viewable: true })), + h2({ class: "dev-file-name" }, safeText(file.name)) + ), + div({ class: "dev-file-meta" }, + p( + span({ class: "dev-label" }, `${i18n.devStatsLines || "Lines"}: `), fmtCount(file.totalLines), + span({ class: "dev-sep" }, " · "), + span({ class: "dev-label" }, `${i18n.devSize || "Size"}: `), fmtSize(file.size), + span({ class: "dev-sep" }, " · "), + span({ class: "dev-label" }, `${i18n.devShowing || "Showing"}: `), `${fmtCount(file.from)}-${fmtCount(file.to)}` + ) + ), + div({ class: "dev-actions" }, + form({ method: "GET", action: "/reports" }, + input({ type: "hidden", name: "filter", value: "create" }), + input({ type: "hidden", name: "category", value: "BUGS" }), + input({ type: "hidden", name: "title", maxlength: "100", value: reportTitle }), + input({ type: "hidden", name: "description", value: reportDescription }), + button({ type: "submit", class: "filter-btn" }, i18n.devReportBug || "Report Bug") + ), + form({ method: "GET", action: "/tasks" }, + input({ type: "hidden", name: "filter", value: "create" }), + input({ type: "hidden", name: "title", maxlength: "100", value: taskTitle }), + input({ type: "hidden", name: "description", value: reportDescription }), + button({ type: "submit", class: "filter-btn" }, i18n.devCreateTask || "Create Task") + ), + form({ method: "GET", action: "/dev/raw" }, + input({ type: "hidden", name: "path", value: file.path }), + button({ type: "submit", class: "filter-btn" }, i18n.devRaw || "RAW") + ) + ), + div({ class: "dev-code" }, + table({ class: "dev-code-table" }, + tbody( + file.lines.map(l => tr({ id: `L${l.n}`, class: "dev-code-row" }, + td({ class: "dev-code-num" }, a({ href: `${fileHref(file.path, file.from)}#L${l.n}` }, String(l.n))), + td({ class: "dev-code-line" }, pre(code(safeText(l.text)))) + )) + ) + ) + ), + renderPager(file) + ) + ) +} + +exports.devSearchView = async (result, ext) => { + const results = (result && result.results) || [] + return template( + i18n.devTitle || "Developer", + section( + ...renderHeader("search"), + renderSearchForm(result.query, ext), + result.query + ? p({ class: "dev-hint" }, `${fmtCount(results.length)}${result.truncated ? "+" : ""} ${i18n.devMatches || "matches"} · ${fmtCount(result.scanned)} ${i18n.devFilesScanned || "files scanned"}`) + : null, + results.length + ? ul({ class: "dev-list dev-results" }, + results.map(r => li({ class: "dev-entry" }, + div({ class: "dev-result-head" }, + span({ class: "dev-icon-glyph dev-result-glyph" }, ICONS[`.${r.path.split('.').pop()}`] || "📄"), + a({ href: `${fileHref(r.path, Math.max(1, r.line - 20))}#L${r.line}` }, `${safeText(r.path)}:${r.line}`) + ), + div({ class: "dev-result-text" }, pre(code(safeText(r.text)))) + )) + ) + : (result.query ? p({ class: "dev-hint" }, i18n.devNoResults || "No matches, yet.") : null) + ) + ) +} + +exports.devMapView = async (modules) => { + return template( + i18n.devTitle || "Developer", + section( + ...renderHeader("map"), + div({ class: "dev-listing" }, + table({ class: "dev-table" }, + thead(tr( + th(i18n.devColModule || "Module"), + th(i18n.devColState || "State"), + th(i18n.devColModel || "Model"), + th(i18n.devColView || "View"), + th(i18n.devColRoutes || "Routes"), + th(i18n.devColTests || "Tests") + )), + tbody( + (modules || []).map(m => tr( + td(safeText(m.name)), + td(m.enabled ? (i18n.devOn || "on") : (i18n.devOff || "off")), + td(m.model ? a({ href: fileHref(m.model) }, safeText(m.model)) : span({ class: "dev-hint" }, "-")), + td(m.view ? a({ href: fileHref(m.view) }, safeText(m.view)) : span({ class: "dev-hint" }, "-")), + td(String(m.routes || 0)), + td(m.tests ? a({ href: dirHref(m.tests) }, safeText(m.tests)) : span({ class: "dev-hint" }, "-")) + )) + ) + ) + ) + ) + ) +} diff --git a/src/views/document_view.js b/src/views/document_view.js index e1e98f2..e609e1a 100644 --- a/src/views/document_view.js +++ b/src/views/document_view.js @@ -1,10 +1,12 @@ const { form, button, div, h2, p, section, input, label, br, a, span, textarea, select, option } = require("../server/node_modules/hyperaxe"); +const { renderCommentsSection: renderSharedCommentsSection, renderCommentsLink } = require("./comments_view"); const moment = require("../server/node_modules/moment"); -const { template, i18n, renderOpinionsVoting, userLink, renderSpreadButton, renderEcoTax, renderLifespanChip, renderContentActions } = require("./main_views"); +const { renderLicenseChip, renderLicenseSelect, renderReachChip, renderClearnetSelector, renderClearnetSwitch, renderTribeOriginChip } = require('./clearnet_view'); +const { clearnetItemHref, template, i18n, renderOpinionsVoting, renderEngagement, userLink, renderSpreadButton, renderEcoTax, renderLifespanChip, renderContentActions, renderSpreadEditWarning, renderModuleStats, moduleIsEmpty, contentDeleteAction, paged, blobUrlOf } = require("./main_views"); const { config } = require("../server/SSB_server.js"); -const { renderUrl } = require("../backend/renderUrl"); +const { renderStyledText } = require("../backend/renderStyledText"); const userId = config.keys.id; @@ -27,116 +29,18 @@ const renderTags = (tags) => { const list = safeArr(tags).map((t) => String(t || "").trim()).filter(Boolean); return list.length ? div( - { class: "card-tags" }, + { class: "tribe-side-tags" }, list.map((tag) => a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link" }, `#${tag}`)) ) : null; }; -const renderFavoriteToggle = (doc, returnTo) => - form( - { - method: "POST", - action: doc.isFavorite - ? `/documents/favorites/remove/${encodeURIComponent(doc.key)}` - : `/documents/favorites/add/${encodeURIComponent(doc.key)}` - }, - returnTo ? input({ type: "hidden", name: "returnTo", value: returnTo }) : null, - button( - { type: "submit", class: "filter-btn" }, - doc.isFavorite ? i18n.documentRemoveFavoriteButton : i18n.documentAddFavoriteButton - ) - ); - -const renderDocumentActions = (filter, doc, params = {}) => { - const returnTo = buildReturnTo(filter, params); - const isAuthor = String(doc.author) === String(userId); - const hasOpinions = Object.keys(doc.opinions || {}).length > 0; - - return isAuthor - ? div( - { class: "bookmark-actions" }, - !hasOpinions - ? form( - { method: "GET", action: `/documents/edit/${encodeURIComponent(doc.key)}` }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - button({ class: "update-btn", type: "submit" }, i18n.documentUpdateButton) - ) - : null, - form( - { method: "POST", action: `/documents/delete/${encodeURIComponent(doc.key)}` }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - button({ class: "delete-btn", type: "submit" }, i18n.documentDeleteButton) - ) - ) - : null; -}; - const renderDocumentCommentsSection = (documentKey, rootId, comments = [], returnTo = null) => { - const list = safeArr(comments).filter(c => { - const t = c && c.value && c.value.content && c.value.content.text; - return t && String(t).trim(); + return renderSharedCommentsSection({ + action: `/documents/${encodeURIComponent(documentKey)}/comments`, + comments: comments, + returnTo: returnTo }); - const commentsCount = list.length; - - return div( - { class: "vote-comments-section" }, - div( - { class: "comments-count" }, - span({ class: "card-label" }, i18n.voteCommentsLabel + ": "), - span({ class: "card-value" }, String(commentsCount)) - ), - div( - { class: "comment-form-wrapper" }, - h2({ class: "comment-form-title" }, i18n.voteNewCommentLabel), - form( - { method: "POST", action: `/documents/${encodeURIComponent(documentKey)}/comments`, class: "comment-form", enctype: "multipart/form-data" }, - returnTo ? input({ type: "hidden", name: "returnTo", value: returnTo }) : null, - rootId ? input({ type: "hidden", name: "rootId", value: rootId }) : null, - textarea({ - id: "comment-text", - name: "text", - rows: 4, - class: "comment-textarea", - placeholder: i18n.voteNewCommentPlaceholder - }), - div({ class: "comment-file-upload" }, label(i18n.uploadMedia), input({ type: "file", name: "blob" })), - br(), - button({ type: "submit", class: "comment-submit-btn" }, i18n.voteNewCommentButton) - ) - ), - list.length - ? div( - { class: "comments-list" }, - list.map((c) => { - const author = c?.value?.author || ""; - const ts = c?.value?.timestamp || c?.timestamp; - const absDate = ts ? moment(ts).format("YYYY/MM/DD HH:mm:ss") : ""; - const relDate = ts ? moment(ts).fromNow() : ""; - - const content = c?.value?.content || {}; - const text = content.text || ""; - const threadRoot = content.fork || content.root || null; - - return div( - { class: "votations-comment-card" }, - span( - { class: "created-at" }, - span(i18n.createdBy), - author ? userLink(author) : span("(unknown)"), - absDate ? span(" | ") : "", - absDate ? span({ class: "votations-comment-date" }, absDate) : "", - relDate ? span({ class: "votations-comment-date" }, " | ", i18n.sendTime) : "", - relDate && threadRoot - ? a({ href: `/thread/${encodeURIComponent(threadRoot)}#${encodeURIComponent(c.key)}` }, relDate) - : "" - ), - p({ class: "votations-comment-text" }, ...renderUrl(text)) - ); - }) - ) - : p({ class: "votations-no-comments" }, i18n.voteNoCommentsYet) - ); }; const renderDocumentList = exports.renderDocumentList = (documents, filter, params = {}) => { @@ -148,64 +52,35 @@ const renderDocumentList = exports.renderDocumentList = (documents, filter, para const title = safeText(doc.title); const pdfId = safeDomId("pdf-container-", doc.key); - const topbarLeft = - doc.author && String(doc.author) !== String(userId) - ? form( - { method: "GET", action: "/pm" }, - input({ type: "hidden", name: "recipients", value: doc.author }), - button({ type: "submit", class: "filter-btn" }, i18n.documentMessageAuthorButton) - ) - : null; - const isOwn = doc.author && String(doc.author) === String(userId); + const headerActions = typeof params.headerActions === "function" + ? params.headerActions(doc) + : doc.tribeOrigin + ? renderContentActions(null, doc.tribeOrigin.href) + : renderContentActions(doc.key, `/documents/${encodeURIComponent(doc.key)}`, { spread: (params.spreadMap && params.spreadMap.get(doc.key)) || params.spreads || null, author: doc.author, favKind: 'documents', torrentFrom: { blobId: doc.url, name: doc.title }, isFavorite: doc.isFavorite, reportTitle: doc.title, deleteAction: isOwn ? contentDeleteAction('document', doc.key) : undefined, returnTo }); + const titleHref = typeof params.headerActions === "function" ? null : doc.tribeOrigin ? doc.tribeOrigin.href : `/documents/${encodeURIComponent(doc.key)}`; + const engagement = typeof params.engagement === "function" + ? params.engagement(doc) + : doc.tribeOrigin ? null : renderEngagement(doc.key, + renderOpinionsVoting('/documents/opinions', doc.key, doc.opinions, returnTo, doc.opinions_inhabitants), + renderCommentsLink({ href: `/documents/${encodeURIComponent(doc.key)}`, count: commentCount }) + ); return div( { class: "trending-card document-card" + (isOwn ? " own-content" : "") }, div( { class: "card-header activity-card-header" }, span(), - renderContentActions(doc.key, `/documents/${encodeURIComponent(doc.key)}`) + headerActions ), div( { class: "card-section document-card-body" }, - div( - { class: "bookmark-topbar" }, - div( - { class: "bookmark-topbar-left" }, - renderFavoriteToggle(doc, returnTo), - topbarLeft - ), - renderDocumentActions(filter, doc, params) - ), - title ? h2(title) : null, + div({ class: "shop-title-row" }, title ? h2(titleHref ? a({ href: titleHref, class: "card-title-link" }, title) : title) : null, doc.tribeOrigin ? renderTribeOriginChip(doc.tribeOrigin) : doc.clearnet === true ? renderReachChip(true, i18n, clearnetItemHref('documents', doc.title, doc.key)) : null, renderLicenseChip(doc.license), ...(typeof params.titleChips === "function" ? params.titleChips(doc) : [])), doc.lifetime ? div({ class: "card-chips-row" }, renderLifespanChip(doc.lifetime, i18n)) : null, doc?.url - ? div({ id: pdfId, class: "pdf-viewer-container", "data-pdf-url": `/blob/${encodeURIComponent(doc.url)}` }) + ? div({ id: pdfId, class: "pdf-viewer-container", "data-pdf-url": blobUrlOf(doc.url) }) : p(i18n.documentNoFile), - div( - { class: "card-comments-summary" }, - span({ class: "card-label" }, i18n.voteCommentsLabel + ":"), - span({ class: "card-value" }, String(commentCount)), - br(), - br(), - form( - { method: "GET", action: `/documents/${encodeURIComponent(doc.key)}` }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - input({ type: "hidden", name: "filter", value: filter || "all" }), - params.q ? input({ type: "hidden", name: "q", value: params.q }) : null, - params.sort ? input({ type: "hidden", name: "sort", value: params.sort }) : null, - button({ type: "submit", class: "filter-btn" }, i18n.voteCommentsForumButton) - ) - ), - div( - { class: "card-comments-summary feed-opinions-section" }, - div( - { class: "comments-count" }, - span({ class: "card-label" }, (i18n.opinionsTitle || "Opinions") + ": "), - span({ class: "card-value" }, String(Object.values(doc.opinions || {}).reduce((s, n) => s + (Number(n) || 0), 0))) - ), - renderOpinionsVoting('/documents/opinions', doc.key, doc.opinions, returnTo, doc.opinions_inhabitants) - ), - div({ class: "card-spread-left" }, renderSpreadButton(doc.key, (params.spreadMap && params.spreadMap.get(doc.key)) || params.spreads)), + ...(typeof params.bodyExtra === "function" ? params.bodyExtra(doc) : []), + engagement, br(), (() => { const createdTs = doc.createdAt ? new Date(doc.createdAt).getTime() : NaN; @@ -214,12 +89,12 @@ const renderDocumentList = exports.renderDocumentList = (documents, filter, para return p( { class: "card-footer" }, - span({ class: "date-link" }, `${moment(doc.createdAt).format("YYYY/MM/DD HH:mm:ss")} ${i18n.performed} `), + span({ class: "date-link" }, `${moment(doc.createdAt).format("YYYY/MM/DD HH:mm")}`), userLink(doc.author), showUpdated ? span( { class: "votations-comment-date" }, - ` | ${i18n.documentUpdatedAt}: ${moment(doc.updatedAt).format("YYYY/MM/DD HH:mm:ss")}` + ` | ${i18n.documentUpdatedAt}: ${moment(doc.updatedAt).format("YYYY/MM/DD HH:mm")}` ) : null ); @@ -237,6 +112,7 @@ const renderDocumentForm = (filter, documentId, docToEdit, params = {}) => { return div( { class: "div-center document-form" }, + params.spreadWarning || null, form( { action: filter === "edit" ? `/documents/update/${encodeURIComponent(documentId)}` : "/documents/create", @@ -251,71 +127,70 @@ const renderDocumentForm = (filter, documentId, docToEdit, params = {}) => { br(), label(i18n.documentTitleLabel), br(), - input({ type: "text", name: "title", placeholder: i18n.documentTitlePlaceholder, value: docToEdit?.title || "" }), + input({ type: "text", name: "title", maxlength: "100", placeholder: i18n.documentTitlePlaceholder, value: docToEdit?.title || "" }), br(), label(i18n.documentDescriptionLabel), br(), - textarea({ name: "description", placeholder: i18n.documentDescriptionPlaceholder, rows: "4" }, docToEdit?.description || ""), + textarea({ maxlength: "5000", name: "description", placeholder: i18n.documentDescriptionPlaceholder, rows: "4" }, docToEdit?.description || ""), br(), label(i18n.documentTagsLabel), br(), input({ type: "text", name: "tags", placeholder: i18n.documentTagsPlaceholder, value: tagsValue }), br(), + ...renderLicenseSelect(docToEdit?.license, i18n), + br(), + renderClearnetSelector(filter === "edit" ? !!docToEdit?.clearnet : false, i18n), br(), button({ type: "submit" }, filter === "edit" ? i18n.documentUpdateButton : i18n.documentCreateButton) ) ); }; +const mediaChipFor = (filter, censusM) => (mode) => { + if (mode === filter) return true; + if (!Array.isArray(censusM)) return true; + if (mode === "top") return censusM.some((x) => !x.tribeOrigin); + if (mode === "mine") return censusM.some((x) => String(x.author) === String(userId)); + if (mode === "recent") return censusM.length > 0; + if (mode === "favorites") return censusM.some((x) => x.isFavorite); + if (mode === "bcs") return censusM.some((x) => String(x.title || "").toUpperCase().startsWith("BCS-")); + return true; +}; + exports.documentView = async (documents, filter = "all", documentId = null, params = {}) => { - const title = - filter === "mine" - ? i18n.documentMineSectionTitle - : filter === "create" - ? i18n.documentCreateSectionTitle - : filter === "edit" - ? i18n.documentUpdateSectionTitle - : filter === "recent" - ? i18n.documentRecentSectionTitle - : filter === "top" - ? i18n.documentTopSectionTitle - : filter === "favorites" - ? i18n.documentFavoritesSectionTitle - : i18n.documentAllSectionTitle; + if (filter === "edit") params = { ...params, spreadWarning: await renderSpreadEditWarning(documentId) }; + const title = i18n.documentTitle; const q = safeText(params.q || ""); const sort = safeText(params.sort || "recent"); const list = safeArr(documents); + const emptyMod = moduleIsEmpty(list, filter, "all", q); + const mediaChip = mediaChipFor(filter, Array.isArray(params.censusList) ? params.censusList : list); const docToEdit = documentId ? list.find((d) => d.key === documentId) : null; const tpl = template( title, section( - div({ class: "tags-header" }, + div({ class: "tags-header module-header-line" }, h2(title), p(i18n.documentDescription) ), - (() => { - const { renderReachChip } = require('./clearnet_view'); - const isClearnet = !!(params.viewerPrefs && params.viewerPrefs.clearnetDocuments); - return div({ class: "shop-title-row" }, renderReachChip(isClearnet, i18n)); - })(), - br(), div( { class: "filters" }, form( { method: "GET", action: "/documents", class: "ui-toolbar ui-toolbar--filters" }, - input({ type: "hidden", name: "q", value: q }), input({ type: "hidden", name: "sort", value: sort }), - button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, i18n.documentFilterAll), - button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, i18n.documentFilterMine), - button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, i18n.documentFilterTop), - button( + ...(emptyMod ? [] : [ + button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.documentFilterAll).toUpperCase()), + ...(mediaChip("recent") ? [button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.documentFilterRecent).toUpperCase())] : []), + ...(mediaChip("mine") ? [button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.documentFilterMine).toUpperCase())] : []), + ...(mediaChip("favorites") ? [button( { type: "submit", name: "filter", value: "favorites", class: filter === "favorites" ? "filter-btn active" : "filter-btn" }, - i18n.documentFilterFavorites - ), - button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, i18n.documentFilterRecent), + String(i18n.documentFilterFavorites).toUpperCase() + )] : []), + ...(mediaChip("top") ? [button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, String(i18n.documentFilterTop).toUpperCase())] : []), + ]), button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.documentCreateButton) ) ) @@ -324,8 +199,9 @@ exports.documentView = async (documents, filter = "all", documentId = null, para filter === "create" || filter === "edit" ? renderDocumentForm(filter, documentId, docToEdit || {}, { ...params, filter }) : section( - div( - { class: "documents-search" }, + emptyMod ? null : div( + { class: "documents-search activity-filter-chips activity-toolbar-row" }, + renderModuleStats(list.length), form( { method: "GET", action: "/documents", class: "filter-box" }, input({ type: "hidden", name: "filter", value: filter }), @@ -334,23 +210,24 @@ exports.documentView = async (documents, filter = "all", documentId = null, para { class: "filter-box__controls" }, select( { name: "sort", class: "filter-box__select" }, - option({ value: "recent", selected: sort === "recent" }, i18n.documentSortRecent), - option({ value: "oldest", selected: sort === "oldest" }, i18n.documentSortOldest), - option({ value: "top", selected: sort === "top" }, i18n.documentSortTop) + option({ value: "recent", ...(sort === "recent" ? { selected: true } : {})}, i18n.documentSortRecent), + option({ value: "oldest", ...(sort === "oldest" ? { selected: true } : {})}, i18n.documentSortOldest), + option({ value: "top", ...(sort === "top" ? { selected: true } : {})}, i18n.documentSortTop) ), button({ type: "submit", class: "filter-box__button" }, i18n.documentSearchButton) ) ) ), - div({ class: "documents-list" }, renderDocumentList(list, filter, { q, sort })) + div({ class: "documents-list" }, renderDocumentList(paged(list), filter, { q, sort, spreadMap: params.spreadMap })) ) ) ); - return `${tpl}`; + return `${tpl}`; }; exports.singleDocumentView = async (doc, filter = "all", comments = [], params = {}) => { + const mediaChip = mediaChipFor(filter, Array.isArray(params.censusList) ? params.censusList : null); const q = safeText(params.q || ""); const sort = safeText(params.sort || "recent"); const returnTo = safeText(params.returnTo) || buildReturnTo(filter, { q, sort }); @@ -359,8 +236,7 @@ exports.singleDocumentView = async (doc, filter = "all", comments = [], params = const pdfId = safeDomId("pdf-container-", doc.key); const isAuthor = String(doc.author) === String(userId); const hasOpinions = Object.keys(doc.opinions || {}).length > 0; - const { renderReachChip } = require('./clearnet_view'); - const isClearnet = !!(params.authorPrefs && params.authorPrefs.clearnetDocuments); + const isClearnet = !!doc.clearnet; const chips = [ renderLifespanChip(doc.lifetime, i18n), @@ -368,14 +244,6 @@ exports.singleDocumentView = async (doc, filter = "all", comments = [], params = ].filter(Boolean); const sideActions = []; - sideActions.push(renderFavoriteToggle(doc, returnTo)); - if (doc.author && String(doc.author) !== String(userId)) { - sideActions.push(form( - { method: "GET", action: "/pm" }, - input({ type: "hidden", name: "recipients", value: doc.author }), - button({ type: "submit", class: "filter-btn" }, i18n.documentMessageAuthorButton) - )); - } if (isAuthor && !hasOpinions) { sideActions.push(form( { method: "GET", action: `/documents/edit/${encodeURIComponent(doc.key)}` }, @@ -383,33 +251,40 @@ exports.singleDocumentView = async (doc, filter = "all", comments = [], params = button({ class: "update-btn", type: "submit" }, i18n.documentUpdateButton) )); } - if (isAuthor) { - sideActions.push(form( - { method: "POST", action: `/documents/delete/${encodeURIComponent(doc.key)}` }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - button({ class: "delete-btn", type: "submit" }, i18n.documentDeleteButton) - )); - } const tagsNode = renderTags(doc.tags); + const detailActions = div({ class: "card-header activity-card-header" }, + renderContentActions(doc.key, `/documents/${encodeURIComponent(doc.key)}`, { + author: doc.author, + favKind: 'documents', torrentFrom: { blobId: doc.url, name: doc.title }, + isFavorite: doc.isFavorite, + spread: params.spreads || null, + returnTo: buildReturnTo(filter, { q, sort }), + reportTitle: doc.title, + deleteAction: isAuthor ? contentDeleteAction('document', doc.key) : undefined + }) + ); + const docSide = div({ class: "tribe-side" }, div({ class: "shop-title-row" }, title ? h2({ class: "tribe-card-title" }, title) : null, - renderReachChip(isClearnet, i18n) + renderReachChip(isClearnet, i18n, isClearnet ? clearnetItemHref('documents', doc.title, doc.key) : null), + isAuthor ? renderClearnetSwitch('documents', doc.rootId || doc.key, isClearnet) : null, + renderLicenseChip(doc.license) ), chips.length ? div({ class: "card-chips-row" }, ...chips) : null, safeText(doc.description) - ? p({ class: "tribe-side-description" }, ...renderUrl(doc.description)) + ? p({ class: "tribe-side-description" }, ...renderStyledText(doc.description)) : null, tagsNode, - div({ class: "card-spread-centered" }, renderSpreadButton(doc.key, params.spreads)), sideActions.length ? div({ class: "tribe-side-actions" }, ...sideActions) : null ); const docMain = div({ class: "tribe-main" }, + detailActions, doc?.url - ? div({ id: pdfId, class: "pdf-viewer-container", "data-pdf-url": `/blob/${encodeURIComponent(doc.url)}` }) + ? div({ id: pdfId, class: "pdf-viewer-container", "data-pdf-url": blobUrlOf(doc.url) }) : p(i18n.documentNoFile), (() => { const createdTs = doc.createdAt ? new Date(doc.createdAt).getTime() : NaN; @@ -418,24 +293,26 @@ exports.singleDocumentView = async (doc, filter = "all", comments = [], params = return p( { class: "card-footer" }, - span({ class: "date-link" }, `${moment(doc.createdAt).format("YYYY/MM/DD HH:mm:ss")} ${i18n.performed} `), + span({ class: "date-link" }, `${moment(doc.createdAt).format("YYYY/MM/DD HH:mm")}`), userLink(doc.author), showUpdated ? span( { class: "votations-comment-date" }, - ` | ${i18n.documentUpdatedAt}: ${moment(doc.updatedAt).format("YYYY/MM/DD HH:mm:ss")}` + ` | ${i18n.documentUpdatedAt}: ${moment(doc.updatedAt).format("YYYY/MM/DD HH:mm")}` ) : null ); })(), - renderOpinionsVoting('/documents/opinions', doc.key, doc.opinions, returnTo, doc.opinions_inhabitants), - renderDocumentCommentsSection(doc.key, doc.rootId || doc.key, comments, returnTo) + renderEngagement(doc.key, + renderOpinionsVoting('/documents/opinions', doc.key, doc.opinions, returnTo, doc.opinions_inhabitants), + renderDocumentCommentsSection(doc.key, doc.rootId || doc.key, comments, returnTo) + ) ); const tpl = template( i18n.documentTitle, section( - div({ class: "tags-header" }, + div({ class: "tags-header module-header-line" }, h2(i18n.documentAllSectionTitle || i18n.documentTitle), p(i18n.documentDescription) ), @@ -443,16 +320,15 @@ exports.singleDocumentView = async (doc, filter = "all", comments = [], params = { class: "filters" }, form( { method: "GET", action: "/documents", class: "ui-toolbar ui-toolbar--filters" }, - input({ type: "hidden", name: "q", value: q }), input({ type: "hidden", name: "sort", value: sort }), - button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, i18n.documentFilterAll), - button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, i18n.documentFilterMine), - button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, i18n.documentFilterTop), - button( + button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.documentFilterAll).toUpperCase()), + ...(mediaChip("recent") ? [button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.documentFilterRecent).toUpperCase())] : []), + ...(mediaChip("mine") ? [button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.documentFilterMine).toUpperCase())] : []), + ...(mediaChip("favorites") ? [button( { type: "submit", name: "filter", value: "favorites", class: filter === "favorites" ? "filter-btn active" : "filter-btn" }, - i18n.documentFilterFavorites - ), - button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, i18n.documentFilterRecent), + String(i18n.documentFilterFavorites).toUpperCase() + )] : []), + button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, String(i18n.documentFilterTop).toUpperCase()), button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.documentCreateButton) ) ), @@ -460,6 +336,6 @@ exports.singleDocumentView = async (doc, filter = "all", comments = [], params = ) ); - return `${tpl}`; + return `${tpl}`; }; diff --git a/src/views/emergencies_view.js b/src/views/emergencies_view.js new file mode 100644 index 0000000..b5c00e2 --- /dev/null +++ b/src/views/emergencies_view.js @@ -0,0 +1,307 @@ +const { div, h2, h3, p, section, button, form, a, span, br, textarea, input, label, select, option, img, video: videoHyperaxe, table, tr, td } = require("../server/node_modules/hyperaxe"); +const { clearnetItemHref, template, i18n, userLink, renderStateChip, renderContentActions, renderSubscriptionBox, renderModuleStats, moduleIsEmpty, contentDeleteAction, paged } = require("./main_views"); +const { renderReachChip, renderClearnetSelector, renderClearnetSwitch } = require("./clearnet_view"); +const { renderCommentsSection } = require("./comments_view"); +const { renderMapEmbed } = require("./maps_view"); +const { renderStyledText } = require("../backend/renderStyledText"); +const moment = require("../server/node_modules/moment"); +const { config } = require("../server/SSB_server.js"); + +const userId = config.keys.id; +const CATEGORIES = ["WEATHER", "INFRASTRUCTURE", "HEALTH", "SECURITY", "LOST", "NEIGHBORHOOD"]; +const SEVERITIES = ["UNVERIFIED", "LOW", "MEDIUM", "HIGH"]; +const STATUSES = ["ACTIVE", "RESOLVED", "EXPIRED"]; +const BASE_FILTERS = ["ALL", "RECENT", "MINE"]; +const EXPIRES = ["1d", "3d", "7d", "30d"]; + +const catLabel = (c) => String(i18n[`emergencyCategory${c.charAt(0) + c.slice(1).toLowerCase()}`] || c).toUpperCase(); +const sevLabel = (s) => String(i18n[`emergencySeverity${s.charAt(0) + s.slice(1).toLowerCase()}`] || s).toUpperCase(); +const statusLabel = (s) => String(i18n[`emergencyStatus${s.charAt(0) + s.slice(1).toLowerCase()}`] || s).toUpperCase(); +const baseLabel = (f) => String(i18n[`emergencyFilter${f.charAt(0) + f.slice(1).toLowerCase()}`] || f).toUpperCase(); + +const severityChip = (emergency) => span({ class: `emergency-severity emergency-severity-${String(emergency.severity || "UNVERIFIED").toLowerCase()}` }, `${sevLabel(emergency.severity)} · ${emergency.confirmationCount || 0}`); +const statusChip = (emergency) => renderStateChip(emergency.status === "ACTIVE" ? "mutuals" : "closed", "", statusLabel(emergency.status)); +const categoryChip = (emergency) => renderStateChip("neutral", "", catLabel(emergency.category)); +const clearnetEligible = (emergency) => String(emergency.status || "ACTIVE").toUpperCase() === "ACTIVE" && !emergency.tribeId && !emergency.encrypted; + +const renderTags = (tags) => (Array.isArray(tags) && tags.length) + ? div({ class: "card-tags" }, ...tags.map(t => a({ href: `/search?query=%23${encodeURIComponent(t)}`, class: "tag-link" }, `#${t}`))) + : null; + +const emergencyHref = (emergency) => `/emergencies/${encodeURIComponent(emergency.id)}`; + +const renderEmergencyCard = (emergency, params = {}) => + div({ class: `tribe-card emergency-card emergency-card-${String(emergency.severity || "UNVERIFIED").toLowerCase()}` }, + div({ class: "card-header activity-card-header" }, + span(), + renderContentActions(emergency.id, emergencyHref(emergency), { author: emergency.author, favKind: "emergencies", isFavorite: emergency.isFavorite, reportTitle: emergency.title, spread: (params.spreadMap && params.spreadMap.get(emergency.id)) || null, deleteAction: String(emergency.author) === String(userId) ? contentDeleteAction("emergency", emergency.id) : undefined }) + ), + div({ class: "tribe-card-body" }, + emergency.media && emergency.media.kind === "image" ? a({ href: emergencyHref(emergency) }, img({ loading: 'lazy', class: "emergency-card-cover", src: `/blob/${encodeURIComponent(emergency.media.blobId)}`, alt: emergency.title || "" })) : null, + emergency.media && emergency.media.kind === "video" ? videoHyperaxe({ class: "emergency-card-cover", src: `/blob/${encodeURIComponent(emergency.media.blobId)}`, controls: true, preload: "metadata" }) : null, + div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, a({ href: emergencyHref(emergency) }, emergency.title || "—"))), + div({ class: "card-chips-row" }, severityChip(emergency), statusChip(emergency), categoryChip(emergency), emergency.clearnet === true && clearnetEligible(emergency) ? renderReachChip(true, i18n, clearnetItemHref("emergencies", emergency.title, emergency.id)) : null), + emergency.expiresAt ? p({ class: "job-meta-line" }, `${i18n.emergencyExpiresAt}: ${moment(emergency.expiresAt).format("YYYY/MM/DD HH:mm")}`) : null + ) + ); + +const chipVisible = (mode, filter, census) => { + if (mode === filter || mode === "ALL") return true; + if (mode === "MINE") return census.some(al => String(al.author) === String(userId)); + if (mode === "RECENT") return census.length > 0; + if (STATUSES.includes(mode)) return census.some(al => al.status === mode); + if (SEVERITIES.includes(mode)) return census.some(al => al.severity === mode); + if (CATEGORIES.includes(mode)) return census.some(al => al.category === mode); + return census.length > 0; +}; + +const chipButtons = (modes, filter, census, labelOf) => + modes.filter(m => chipVisible(m, filter, census)).map(m => button({ type: "submit", name: "filter", value: m, class: filter === m ? "filter-btn active" : "filter-btn" }, labelOf(m))); + +const renderFilters = (filter, q, census, emptyMod) => + section( + div({ class: "filters" }, + form({ method: "GET", action: "/emergencies", class: "ui-toolbar ui-toolbar--filters" }, + ...(emptyMod ? [] : [ + ...chipButtons(BASE_FILTERS, filter, census, baseLabel), + ...chipButtons(SEVERITIES, filter, census, sevLabel), + ...chipButtons(STATUSES, filter, census, statusLabel), + ...chipButtons(CATEGORIES, filter, census, catLabel) + ]), + button({ type: "submit", name: "filter", value: "CREATE", class: "create-button" }, i18n.emergencyCreate) + ) + ) + ); + +const renderForm = (emergency, params = {}) => + div({ class: "div-center audio-form" }, + h2(emergency ? i18n.emergencyUpdateSectionTitle : i18n.emergencyCreateSectionTitle), + form({ method: "POST", action: emergency ? `/emergencies/update/${encodeURIComponent(emergency.id)}` : "/emergencies/create", enctype: "multipart/form-data" }, + label(i18n.emergencyTitleLabel), br(), + input({ type: "text", name: "title", maxlength: "100", required: true, placeholder: i18n.emergencyTitlePlaceholder, value: emergency ? emergency.title : "" }), br(), br(), + label(i18n.emergencyCategoryLabel), br(), + select({ name: "category" }, ...CATEGORIES.map(c => option({ value: c, ...((emergency ? emergency.category : "NEIGHBORHOOD") === c ? { selected: true } : {}) }, catLabel(c)))), br(), br(), + label(i18n.emergencyTextLabel), br(), + textarea({ name: "text", rows: 8, maxlength: "5000", placeholder: i18n.emergencyTextPlaceholder }, emergency ? emergency.text : ""), br(), + label(i18n.uploadMedia), br(), + input({ type: "file", name: "blob", accept: "image/*,video/*,audio/*,application/pdf,.torrent" }), br(), br(), + label(i18n.mapLocationTitle || "Map Location"), br(), + input({ type: "text", name: "mapUrl", placeholder: i18n.mapUrlPlaceholder || "/maps/MAP_ID", value: emergency ? emergency.mapUrl : "" }), br(), + label(i18n.emergencyExpiresLabel), br(), + select({ name: "expiresIn" }, + emergency && emergency.expiresAt ? option({ value: "keep", selected: true }, i18n.emergencyExpiresKeep) : null, + option({ value: "", ...(!(emergency && emergency.expiresAt) ? { selected: true } : {}) }, i18n.emergencyExpiresNone), + ...EXPIRES.map(e => option({ value: e }, i18n[`emergencyExpires${e}`] || e)) + ), br(), br(), + label(i18n.emergencyTagsLabel), br(), + input({ type: "text", name: "tags", maxlength: "200", placeholder: i18n.emergencyTagsPlaceholder, value: emergency ? emergency.tags.join(", ") : "" }), br(), br(), + ...(!emergency || clearnetEligible(emergency) ? [renderClearnetSelector(!!emergency && emergency.clearnet === true, i18n), br()] : []), + button({ type: "submit", class: "create-button" }, emergency ? i18n.emergencyUpdate : i18n.emergencyCreate) + ) + ); + +const renderHeader = () => div({ class: "tags-header module-header-line" }, h2(i18n.emergenciesTitle), p(i18n.emergenciesDescription)); + +exports.emergenciesView = async (emergencies, filter = "ALL", params = {}) => { + const list = Array.isArray(emergencies) ? emergencies : []; + const q = String(params.q || "").trim(); + const census = Array.isArray(params.censusList) ? params.censusList : list; + const f = String(filter || "ALL").toUpperCase(); + const isForm = f === "CREATE" || f === "EDIT"; + const emptyMod = !isForm && moduleIsEmpty(census, "ALL", "ALL", q); + return template( + i18n.emergenciesTitle, + section( + div({ class: "tags-header module-header-line" }, h2(i18n.emergenciesTitle), p(i18n.emergenciesDescription)), + renderFilters(isForm ? "ALL" : f, q, isForm ? [] : census, emptyMod) + ), + section( + isForm + ? renderForm(f === "EDIT" ? params.emergency : null, params) + : [ + emptyMod ? null : div({ class: "filters activity-filter-chips activity-toolbar-row" }, + renderModuleStats(list.length, SEVERITIES.map(s => ({ label: sevLabel(s), count: list.filter(al => al.severity === s).length }))), + form({ method: "GET", action: "/emergencies", class: "filter-box" }, + input({ type: "hidden", name: "filter", value: f }), + input({ type: "text", name: "q", value: q, placeholder: i18n.emergencySearchPlaceholder, class: "filter-box__input" }), + div({ class: "filter-box__controls" }, button({ type: "submit", class: "filter-box__button" }, i18n.searchButton)) + ) + ), + div({ class: "tribe-grid" }, list.length ? paged(list).map(al => renderEmergencyCard(al, params)) : p(i18n.emergencyNoItems)) + ] + ) + ); +}; + +exports.singleEmergencyView = async (emergency, params = {}) => { + const isAuthor = String(emergency.author) === String(userId); + const canClearnet = clearnetEligible(emergency); + const isClearnet = canClearnet && !!emergency.clearnet; + const confirmed = Array.isArray(emergency.confirmations) && emergency.confirmations.includes(userId); + const href = emergencyHref(emergency); + const census = Array.isArray(params.censusList) ? params.censusList : []; + const sideActions = [ + !isAuthor && !confirmed && emergency.status === "ACTIVE" + ? form({ method: "POST", action: `/emergencies/confirm/${encodeURIComponent(emergency.id)}` }, button({ type: "submit", class: "tribe-action-btn" }, `✓ ${String(i18n.emergencyConfirmButton).toUpperCase()}`)) + : null, + confirmed ? span({ class: "status supporting" }, i18n.emergencyConfirmed) : null + ].filter(Boolean); + const ownerActions = isAuthor + ? [ + (emergency.confirmationCount || 0) > 0 + ? null + : form({ method: "GET", action: "/emergencies" }, input({ type: "hidden", name: "filter", value: "edit" }), input({ type: "hidden", name: "id", value: emergency.id }), button({ type: "submit", class: "update-btn" }, i18n.emergencyUpdate)) + ].filter(Boolean) + : []; + const updates = Array.isArray(emergency.updates) ? emergency.updates : []; + const side = div({ class: "tribe-side" }, + div({ class: "card-header activity-card-header" }, + renderContentActions(emergency.id, href, { author: emergency.author, favKind: "emergencies", isFavorite: emergency.isFavorite, reportTitle: emergency.title, spread: params.spread || null, returnTo: href, deleteAction: isAuthor ? contentDeleteAction("emergency", emergency.id) : undefined }) + ), + div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, emergency.title)), + div({ class: "card-chips-row" }, + severityChip(emergency), statusChip(emergency), categoryChip(emergency), + renderReachChip(isClearnet, i18n, isClearnet ? clearnetItemHref("emergencies", emergency.title, emergency.id) : null), + isAuthor && canClearnet ? renderClearnetSwitch("emergencies", emergency.rootId || emergency.id, isClearnet) : null + ), + table({ class: "tribe-info-table jobs-info-table" }, + tr( + td({ class: "tribe-info-label" }, i18n.createdAtLabel || "Created at"), + td({ class: "tribe-info-value", colspan: "3" }, moment(emergency.createdAt).format("YYYY/MM/DD HH:mm")) + ), + emergency.expiresAt + ? tr( + td({ class: "tribe-info-label" }, i18n.emergencyExpiresAt), + td({ class: "tribe-info-value", colspan: "3" }, moment(emergency.expiresAt).format("YYYY/MM/DD HH:mm")) + ) + : null, + tr( + td({ class: "tribe-info-value", colspan: "4" }, userLink(emergency.author)) + ) + ), + div({ class: "tribe-card-members" }, + span({ class: "tribe-members-count" }, `${i18n.emergencyConfirmations}: ${emergency.confirmationCount || 0}`) + ), + renderMapEmbed(params.mapData, emergency.mapUrl), + sideActions.length ? div({ class: "tribe-side-actions" }, ...sideActions) : null, + isAuthor && emergency.status !== "EXPIRED" + ? div({ class: "tribe-side-actions housing-status-row" }, + span({ class: "card-label" }, `${i18n.statusLabel || "Status"}: `), + statusChip(emergency), + form({ method: "POST", action: `/emergencies/status/${encodeURIComponent(emergency.id)}`, class: "inline-form" }, + button({ class: "tribe-action-btn", type: "submit", name: "status", value: emergency.status === "ACTIVE" ? "RESOLVED" : "ACTIVE" }, + String(emergency.status === "ACTIVE" ? i18n.emergencyResolveButton : i18n.emergencyReopenButton).toUpperCase()) + ) + ) + : null, + div({ class: "doc-export-actions" }, + form({ method: "GET", action: `/emergencies/${encodeURIComponent(emergency.id)}/pdf` }, button({ type: "submit", class: "filter-btn" }, i18n.emergencyGenerateReport)), + form({ method: "POST", action: `/emergencies/${encodeURIComponent(emergency.id)}/share` }, button({ type: "submit", class: "filter-btn" }, i18n.sharePm)) + ), + renderTags(emergency.tags), + ownerActions.length ? div({ class: "tribe-side-actions owner-actions" }, ...ownerActions) : null, + params.subscription + ? renderSubscriptionBox({ target: emergency.id, scope: "emergencies", subscribed: params.subscription.subscribed, count: params.subscription.count, isOwner: isAuthor, returnTo: href }) + : null + ); + const main = div({ class: "tribe-main" }, + emergency.text ? div({ class: "emergency-body" }, ...renderStyledText(emergency.text)) : null, + div({ class: "card-section emergency-updates" }, + h3(i18n.emergencyUpdatesTitle), + updates.length + ? div({ class: "emergency-updates-list" }, ...updates.map(u => { + const mineUpdate = String(u.author) === String(userId); + const confirmedUpdate = Array.isArray(u.confirmations) && u.confirmations.includes(userId); + const editing = params.editUpdate && params.editUpdate === u.id; + return div({ class: "emergency-update", id: `update-${encodeURIComponent(u.id)}` }, + div({ class: "emergency-update-head" }, + div({ class: "card-chips-row" }, + renderStateChip(u.confirmationCount > 0 ? "mutuals" : "neutral", "✓", `${i18n.emergencyConfirmations} · ${u.confirmationCount || 0}`), + u.edited ? renderStateChip("neutral", "", i18n.emergencyUpdateEdited) : null + ), + div({ class: "tribe-side-actions emergency-update-actions" }, + !mineUpdate && !confirmedUpdate && emergency.status === "ACTIVE" + ? form({ method: "POST", action: `/emergencies/updates/${encodeURIComponent(u.id)}/confirm` }, input({ type: "hidden", name: "emergency", value: emergency.id }), button({ type: "submit", class: "tribe-action-btn" }, `✓ ${String(i18n.emergencyConfirmButton).toUpperCase()}`)) + : null, + confirmedUpdate ? span({ class: "status supporting" }, i18n.emergencyConfirmed) : null, + mineUpdate && !editing && u.confirmationCount === 0 + ? form({ method: "GET", action: `${href}#update-${encodeURIComponent(u.id)}` }, input({ type: "hidden", name: "editUpdate", value: u.id }), button({ type: "submit", class: "update-btn" }, i18n.emergencyUpdate)) + : null, + mineUpdate + ? form({ method: "POST", action: `/emergencies/updates/${encodeURIComponent(u.id)}/delete` }, button({ type: "submit", class: "delete-btn" }, i18n.emergencyDelete)) + : null + ), + ), + editing + ? form({ method: "POST", action: `/emergencies/updates/${encodeURIComponent(u.id)}/edit`, enctype: "multipart/form-data", class: "emergency-update-form" }, + textarea({ name: "text", rows: 3, maxlength: "2000", required: true, autofocus: true }, u.text), br(), + input({ type: "file", name: "blob", accept: "image/*,video/*,audio/*,application/pdf,.torrent" }), br(), br(), + button({ type: "submit", class: "create-button" }, i18n.emergencyUpdate) + ) + : div({ class: "emergency-update-text" }, ...renderStyledText(u.text)), + p({ class: "card-footer" }, span({ class: "date-link" }, `${moment(u.createdAt).format("YYYY/MM/DD HH:mm")}`), userLink(u.author)) + ); + })) + : p(i18n.emergencyNoUpdates), + isAuthor && emergency.status === "ACTIVE" + ? form({ method: "POST", action: `/emergencies/updates/${encodeURIComponent(emergency.id)}`, enctype: "multipart/form-data", class: "emergency-update-form" }, + textarea({ name: "text", rows: 3, maxlength: "2000", required: true, placeholder: i18n.emergencyUpdatePlaceholder }), br(), + input({ type: "file", name: "blob", accept: "image/*,video/*,audio/*,application/pdf,.torrent" }), br(), br(), + button({ type: "submit", class: "create-button" }, i18n.emergencyAddUpdate) + ) + : null + ), + renderCommentsSection({ action: `/emergencies/${encodeURIComponent(emergency.id)}/comments`, comments: params.comments || [], returnTo: href }) + ); + return template( + emergency.title, + section(renderHeader(), renderFilters("ALL", "", census, census.length === 0)), + section(div({ class: "tribe-details" }, side, main)) + ); +}; + +exports.clearnetEmergencyView = async (emergency, params = {}) => { + const { escapeHtml: esc, renderRichText, renderKindTag, renderTagChips, blobUrl: cnBlob, renderClearnetPage } = require("./clearnet_view"); + const title = esc(emergency.title || i18n.emergencyLabel); + const desc = renderRichText(emergency.text || ""); + const severity = String(emergency.severity || "UNVERIFIED").toUpperCase(); + const point = params.mapPoint || null; + const cover = emergency.media && emergency.media.kind === "image" ? cnBlob(emergency.media.blobId) : null; + const updates = (Array.isArray(emergency.updates) ? emergency.updates : []).filter(u => String(u.author) === String(emergency.author)); + const fmtDate = (v) => esc(moment(v).format("YYYY/MM/DD HH:mm")); + const extraCss = ` +.cn-emergency-title{color:var(--fg);margin:0 0 16px 0;font-size:32px;font-weight:700} +.cn-emergency-meta{display:flex;flex-wrap:wrap;gap:10px;margin-bottom:20px} +.cn-emergency-meta-item{background:var(--bg-sub);border:1px solid var(--border);border-radius:6px;padding:8px 14px;font-size:14px;color:var(--fg-soft);display:inline-flex;align-items:center;gap:6px} +.cn-emergency-severity-medium,.cn-emergency-severity-high{border-color:var(--fg);color:var(--fg);font-weight:700} +.cn-emergency-desc{color:var(--fg-soft);line-height:1.6;font-size:15px;margin:0 0 20px 0;word-break:break-word} +.cn-emergency-update{border:1px solid var(--border);border-radius:8px;padding:12px 16px;margin:12px 0;background:var(--bg-elev)} +.cn-emergency-update-date{color:var(--fg-dim);font-size:12px;margin-bottom:6px} +.cn-emergency-update-text{color:var(--fg-soft);line-height:1.5;word-break:break-word} +`; + const body = ` +

${title}

+
+ ${renderKindTag("emergency")} + ⚠ ${esc(sevLabel(severity))} + ✓ ${esc(i18n.emergencyConfirmations)}: ${Number(emergency.confirmationCount) || 0} + ${esc(catLabel(String(emergency.category || "NEIGHBORHOOD")))} + ${emergency.createdAt ? `📅 ${fmtDate(emergency.createdAt)}` : ""} + ${emergency.expiresAt ? `⏳ ${esc(i18n.emergencyExpiresAt)}: ${fmtDate(emergency.expiresAt)}` : ""} + ${point ? `📍 ${point.title ? `${esc(point.title)} · ` : ""}${point.lat}, ${point.lng}` : ""} +
+
+ ${desc ? `
${desc}
` : ""} + ${updates.length ? `

${esc(String(i18n.emergencyUpdatesTitle || "").replace(/[::]\s*$/, ""))}

${updates.map(u => `
${fmtDate(u.updatedAt || u.createdAt)}
${renderRichText(u.text || "")}
`).join("")}` : ""} + ${renderTagChips(emergency.tags)} +`; + return renderClearnetPage({ + title: `${emergency.title || i18n.emergencyLabel} | Oasis`, + ogTitle: emergency.title || i18n.emergencyLabel, + ogDescription: String(emergency.text || "").replace(/!?\[[^\]]*\]\(\s*&[^)\s]+\s*\)/g, "").trim(), + ogImage: cover, + extraCss, + body, + hubFeedId: emergency.author || null + }); +}; diff --git a/src/views/event_view.js b/src/views/event_view.js index c3b29fa..220277c 100644 --- a/src/views/event_view.js +++ b/src/views/event_view.js @@ -1,9 +1,13 @@ -const { div, h2, p, section, button, form, a, span, textarea, br, input, label, select, option, table, tr, td } = require("../server/node_modules/hyperaxe"); -const { template, i18n, renderOpinionsVoting, userLink, renderStateChip, renderOpenClosedChip, renderPrivacyChip, renderLifespanChip, renderEcoTax, renderSpreadButton, renderContentActions } = require("./main_views"); +const { div, h2, p, section, button, form, a, span, textarea, br, input, label, select, option, table, tr, td, details, summary, ul, li, hr } = require("../server/node_modules/hyperaxe"); +const { renderCommentsSection: renderSharedCommentsSection } = require("./comments_view"); +const { clearnetItemHref, template, i18n, renderOpinionsVoting, renderEngagement, userLink, renderStateChip, renderOpenClosedChip, renderPrivacyChip, renderLifespanChip, renderEcoTax, renderSpreadButton, renderContentActions, renderSpreadEditWarning, renderDocumentActions, renderInviteQrCard, renderSubscriptionBox, renderModuleStatsBy, moduleIsEmpty, renderEcoValueChip, contentDeleteAction, paged, renderCreatedRows } = require("./main_views"); +const { renderPhotoGallery, renderGalleryFields } = require("./gallery_view"); +const { renderIntervalBlock } = require("./calendars_view"); const moment = require("../server/node_modules/moment"); const { config } = require("../server/SSB_server.js"); -const { renderUrl } = require("../backend/renderUrl"); +const { renderStyledText, safeExternalHref } = require("../backend/renderStyledText"); const { renderMapLocationUrl, renderMapEmbed, renderMapLocationVisitLabel } = require("./maps_view"); +const { renderReachChip, renderEncryptedChip, renderClearnetSelector, renderClearnetSwitch, renderTribeOriginChip } = require("./clearnet_view"); const userId = config.keys.id; @@ -18,7 +22,7 @@ exports.renderEventInvitePage = (code) => { }; const opt = (value, isSelected, text) => - option(Object.assign({ value }, isSelected ? { selected: "selected" } : {}), text); + option(Object.assign({ value }, isSelected ? { ...("selected" ? { selected: true } : {})} : {}), text); const safeArray = (v) => (Array.isArray(v) ? v : []); @@ -29,12 +33,6 @@ const normalizePrivacy = (v) => { const privacyLabel = (v) => (normalizePrivacy(v) === "private" ? i18n.eventPrivate : i18n.eventPublic); -const safeExternalHref = (url) => { - const s = String(url || "").trim(); - const lower = s.toLowerCase(); - if (lower.startsWith("http://") || lower.startsWith("https://") || lower.startsWith("mailto:")) return s; - return ""; -}; const normalizeEventStatus = (v) => { const up = String(v || "").toUpperCase(); @@ -42,6 +40,8 @@ const normalizeEventStatus = (v) => { return up || "OPEN"; }; +const clearnetEligible = (e) => normalizeEventStatus(e.status) !== "CLOSED" && normalizePrivacy(e.isPublic) === "public" && !e.encrypted; + const eventStatusLabel = (v) => { const st = normalizeEventStatus(v); if (st === "OPEN") return i18n.eventStatusOpen; @@ -59,11 +59,6 @@ const renderEventOwnerActions = (e, returnTo) => { { method: "GET", action: `/events/edit/${encodeURIComponent(e.id)}` }, input({ type: "hidden", name: "returnTo", value: returnTo }), button({ type: "submit", class: "update-btn" }, i18n.eventUpdateButton) - ), - form( - { method: "POST", action: `/events/delete/${encodeURIComponent(e.id)}` }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - button({ type: "submit", class: "delete-btn" }, i18n.eventDeleteButton) ) ]; if (normalizePrivacy(e.isPublic) === "private") { @@ -74,7 +69,8 @@ const renderEventOwnerActions = (e, returnTo) => { if (e.openInviteCode) { actions.push(div({ class: "tribe-open-invite" }, span({ class: "card-label" }, i18n.tribeInviteCodeText), - span({ class: "tribe-open-invite-code" }, e.openInviteCode) + span({ class: "tribe-open-invite-code" }, e.openInviteCode), + renderInviteQrCard({ qrDataUrl: `/qr-invite-code/events/${encodeURIComponent(e.openInviteCode)}` }) )); actions.push(form( { method: "POST", action: `/events/open-invite/remove/${encodeURIComponent(e.id)}` }, @@ -95,7 +91,7 @@ const renderEventAttendAction = (e, isAttending, returnTo) => { if (st !== "OPEN") return null; if (e.organizer === userId) return null; if (normalizePrivacy(e.isPublic) === "private" && !isAttending) { - return a({ class: "tribe-action-btn", href: "/invites#invites-events" }, i18n.tribeEnterInvite); + return a({ class: "tribe-action-btn", href: "/invites?filter=others" }, i18n.tribeEnterInvite); } return form( { method: "POST", action: `/events/attend/${encodeURIComponent(e.id)}` }, @@ -105,70 +101,12 @@ const renderEventAttendAction = (e, isAttending, returnTo) => { }; const renderEventCommentsSection = (eventId, comments = [], currentFilter = "all") => { - const commentsCount = Array.isArray(comments) ? comments.length : 0; const returnTo = `/events/${encodeURIComponent(eventId)}?filter=${encodeURIComponent(currentFilter || "all")}`; - - return div( - { class: "vote-comments-section" }, - div( - { class: "comments-count" }, - span({ class: "card-label" }, i18n.voteCommentsLabel + ": "), - span({ class: "card-value" }, String(commentsCount)) - ), - div( - { class: "comment-form-wrapper" }, - h2({ class: "comment-form-title" }, i18n.voteNewCommentLabel), - form( - { method: "POST", action: `/events/${encodeURIComponent(eventId)}/comments`, class: "comment-form", enctype: "multipart/form-data" }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - textarea({ - id: "comment-text", - name: "text", - rows: 4, - class: "comment-textarea", - placeholder: i18n.voteNewCommentPlaceholder - }), - div({ class: "comment-file-upload" }, label(i18n.uploadMedia), input({ type: "file", name: "blob" })), - br(), - button({ type: "submit", class: "comment-submit-btn" }, i18n.voteNewCommentButton) - ) - ), - (() => { - const visibleComments = (comments || []).filter(c => { - const t = c && c.value && c.value.content && c.value.content.text; - return t && String(t).trim(); - }); - return visibleComments.length - ? div( - { class: "comments-list" }, - visibleComments.map((c) => { - const author = c.value && c.value.author ? c.value.author : ""; - const ts = c.value && c.value.timestamp ? c.value.timestamp : c.timestamp; - const absDate = ts ? moment(ts).format("YYYY/MM/DD HH:mm:ss") : ""; - const relDate = ts ? moment(ts).fromNow() : ""; - - const content = c.value && c.value.content ? c.value.content : {}; - const root = content.fork || content.root || ""; - const text = content.text || ""; - - return div( - { class: "votations-comment-card" }, - span( - { class: "created-at" }, - span(i18n.createdBy), - author ? userLink(author) : span("(unknown)"), - absDate ? span(" | ") : "", - absDate ? span({ class: "votations-comment-date" }, absDate) : "", - relDate ? span({ class: "votations-comment-date" }, " | ", i18n.sendTime) : "", - relDate && root ? a({ href: `/thread/${encodeURIComponent(root)}#${encodeURIComponent(c.key)}` }, relDate) : "" - ), - p({ class: "votations-comment-text" }, ...renderUrl(String(text))) - ); - }) - ) - : p({ class: "votations-no-comments" }, i18n.voteNoCommentsYet); - })() - ); + return renderSharedCommentsSection({ + action: `/events/${encodeURIComponent(eventId)}/comments`, + comments: comments, + returnTo: returnTo + }); }; const renderEventStatusChip = (status) => { @@ -176,66 +114,90 @@ const renderEventStatusChip = (status) => { return renderOpenClosedChip(status, { statusChipOPEN: localized, statusChipCLOSED: localized }); }; -const renderEventItem = exports.renderEventItem = (e, filter, spreadInfo) => { +const renderEventItem = exports.renderEventItem = (e, filter, spreadInfo, opts = {}) => { const currentFilter = filter || "all"; const attendees = safeArray(e.attendees); const isPrivate = normalizePrivacy(e.isPublic) === "private"; const isAttending = attendees.includes(userId); const price = parseFloat(e.price || 0); + const origin = e.tribeOrigin || null; + const href = opts.href !== undefined ? opts.href : (origin ? origin.href : `/events/${encodeURIComponent(e.id)}`); const chips = [ - renderEventStatusChip(e.status), renderPrivacyChip(isPrivate, i18n), + renderEventStatusChip(e.status), e.encrypted ? renderStateChip("encrypted", "🔒", i18n.encryptedChipLabel || "E2E") : null, isAttending ? renderStateChip("whole", "★", i18n.eventAttended) : null, - renderLifespanChip(e.lifetime, i18n) + renderLifespanChip(e.lifetime, i18n), + !origin && e.clearnet === true && clearnetEligible(e) ? renderReachChip(true, i18n, clearnetItemHref('events', e.title, e.id)) : null, + origin ? null : e.subscriptionIn === true + ? renderStateChip("mutuals", "✉", i18n.subscriptionOn) + : (e.subscriptionIn === false ? renderStateChip("closed", "✉", i18n.subscriptionOff) : null), + origin ? renderTribeOriginChip(origin) : null, + ...(Array.isArray(opts.extraChips) ? opts.extraChips : []) ].filter(Boolean); const dateText = e.date ? moment(e.date).format("YYYY/MM/DD HH:mm") : ""; const isOwn = e.organizer && String(e.organizer) === String(userId); + const headerActions = opts.headerActions !== undefined + ? opts.headerActions + : origin ? null : renderContentActions(e.id, `/events/${encodeURIComponent(e.id)}?filter=${encodeURIComponent(currentFilter)}`, { spread: spreadInfo || null, author: e.organizer || e.author, favKind: 'events', isFavorite: e.isFavorite, reportTitle: e.title, deleteAction: isOwn ? contentDeleteAction('event', e.id) : undefined, returnTo: `/events?filter=${encodeURIComponent(currentFilter)}` }); return div({ class: "trending-card event-card" + (isOwn ? " own-content" : "") }, - div( + headerActions ? div( { class: "card-header activity-card-header" }, span(), - renderContentActions(e.id, `/events/${encodeURIComponent(e.id)}?filter=${encodeURIComponent(currentFilter)}`) - ), + headerActions + ) : null, div({ class: "card-section event-card-body" }, div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, - a({ href: `/events/${encodeURIComponent(e.id)}` }, e.title || i18n.eventsTitle) + href ? a({ href }, e.title || i18n.eventsTitle) : (e.title || i18n.eventsTitle) ) ), chips.length ? div({ class: "card-chips-row" }, ...chips) : null, - dateText ? p({ class: "card-date-highlight" }, dateText) : null, e.location && String(e.location).trim() - ? p({ class: "job-meta-line" }, String(e.location)) + ? div({ class: "card-field card-field-mb" }, + span({ class: "card-label" }, `${String(i18n.eventLocation).toUpperCase()}: `), + span({ class: "card-value" }, String(e.location)) + ) : null, + dateText ? p({ class: "time-chip" }, dateText) : null, price > 0 - ? div({ class: "job-price-line card-salary" }, `${price.toFixed(6)} ECO`) + ? div({ class: "price-chip" }, `${price.toFixed(6)} ECO`) : null, - div({ class: "tribe-card-members" }, + origin ? null : div({ class: "tribe-card-members" }, span({ class: "tribe-members-count" }, `${i18n.eventAttendees}: ${attendees.length}`) ), - div({ class: "card-spread-centered" }, renderSpreadButton(e.id, spreadInfo)) + ...(Array.isArray(opts.bodyExtra) ? opts.bodyExtra.filter(Boolean) : []) ) ); }; +const eventChipFor = (currentFilter, censusEvents) => (mode) => { + if (mode === currentFilter) return true; + if (!Array.isArray(censusEvents)) return true; + const pubE = (e) => normalizePrivacy(e.isPublic) === "public"; + if (mode === "mine") return censusEvents.some((e) => e.organizer === userId); + if (mode === "recent") return censusEvents.length > 0; + if (mode === "today") return censusEvents.some((e) => pubE(e) && moment(e.date).isSame(moment(), "day")); + if (mode === "week") return censusEvents.some((e) => pubE(e) && moment(e.date).isBetween(moment(), moment().add(7, "days"), null, "[]")); + if (mode === "month") return censusEvents.some((e) => pubE(e) && moment(e.date).isBetween(moment(), moment().add(1, "month"), null, "[]")); + if (mode === "year") return censusEvents.some((e) => pubE(e) && moment(e.date).isBetween(moment(), moment().add(1, "year"), null, "[]")); + if (mode === "archived") return censusEvents.some((e) => pubE(e) && normalizeEventStatus(e.status) === "CLOSED"); + return true; +}; + exports.eventView = async (events, filter, eventId, returnTo, params = {}) => { const list = Array.isArray(events) ? events : [events]; const currentFilter = filter || "all"; - const { renderReachChip: renderReachChipEvents } = require('./clearnet_view'); - const viewerClearnetEvents = !!(params.viewerPrefs && params.viewerPrefs.clearnetEvents); - const title = - currentFilter === "mine" ? i18n.eventMineSectionTitle : - currentFilter === "create" ? i18n.eventCreateSectionTitle : - currentFilter === "edit" ? i18n.eventUpdateSectionTitle : - i18n.eventAllSectionTitle; + const title = i18n.eventsTitle; const eventToEdit = list.find((e) => e.id === eventId) || {}; - const editTags = Array.isArray(eventToEdit.tags) ? eventToEdit.tags.filter(Boolean) : []; + const formData = currentFilter === "edit" ? eventToEdit : (params.draft || {}); + const spreadWarning = currentFilter === "edit" ? await renderSpreadEditWarning(eventToEdit && eventToEdit.id) : null; + const editTags = Array.isArray(formData.tags) ? formData.tags.filter(Boolean) : (typeof formData.tags === "string" ? formData.tags.split(",").map(t => t.trim()).filter(Boolean) : []); const canSee = (e) => { const isPub = normalizePrivacy(e.isPublic) === "public"; @@ -245,10 +207,13 @@ exports.eventView = async (events, filter, eventId, returnTo, params = {}) => { }; const visible = list.filter(canSee); + const censusEvents = (Array.isArray(params.censusList) ? params.censusList : list).filter(canSee); let filtered; if (currentFilter === "all") { filtered = visible.filter((e) => normalizePrivacy(e.isPublic) === "public"); + } else if (currentFilter === "recent") { + filtered = visible.filter((e) => normalizePrivacy(e.isPublic) === "public" && (Date.parse(e.createdAt || "") || 0) >= Date.now() - 86400000); } else if (currentFilter === "mine") { filtered = visible.filter((e) => e.organizer === userId); } else if (currentFilter === "today") { @@ -270,36 +235,77 @@ exports.eventView = async (events, filter, eventId, returnTo, params = {}) => { const minCreate = moment().add(1, "minute").format("YYYY-MM-DDTHH:mm"); const ret = typeof returnTo === "string" && returnTo.startsWith("/events") ? returnTo : "/events?filter=mine"; - const editPrivacy = normalizePrivacy(eventToEdit.isPublic); + const emptyMod = moduleIsEmpty(censusEvents, currentFilter, "all", params.q); + const pubE = (e) => normalizePrivacy(e.isPublic) === "public"; + const eventChipVisible = eventChipFor(currentFilter, censusEvents); + const reachRaw = String(params.reach || "").toLowerCase(); + const editPrivacy = reachRaw === "public" || reachRaw === "private" ? reachRaw : normalizePrivacy(formData.isPublic); + const showClearnet = editPrivacy === "public" && clearnetEligible({ ...formData, isPublic: editPrivacy }); + const editInterval = formData.interval + || (formData.intervalWeekly ? "weekly" : formData.intervalMonthly ? "monthly" : formData.intervalYearly ? "yearly" : ""); + const editIntervalUntil = (formData.recurrenceUntil || formData.intervalDeadline) + ? moment(formData.recurrenceUntil || formData.intervalDeadline).format("YYYY-MM-DDTHH:mm") + : ""; return template( title, section( - div({ class: "tags-header" }, + div({ class: "tags-header module-header-line" }, h2(i18n.eventsTitle), p(i18n.eventsDescription) ), - div({ class: "shop-title-row" }, renderReachChipEvents(viewerClearnetEvents, i18n)), - br(), div( { class: "filters" }, form( { method: "GET", action: "/events" }, - button({ type: "submit", name: "filter", value: "all", class: currentFilter === "all" ? "filter-btn active" : "filter-btn" }, i18n.eventFilterAll), - button({ type: "submit", name: "filter", value: "mine", class: currentFilter === "mine" ? "filter-btn active" : "filter-btn" }, i18n.eventFilterMine), - button({ type: "submit", name: "filter", value: "today", class: currentFilter === "today" ? "filter-btn active" : "filter-btn" }, i18n.eventFilterToday), - button({ type: "submit", name: "filter", value: "week", class: currentFilter === "week" ? "filter-btn active" : "filter-btn" }, i18n.eventFilterWeek), - button({ type: "submit", name: "filter", value: "month", class: currentFilter === "month" ? "filter-btn active" : "filter-btn" }, i18n.eventFilterMonth), - button({ type: "submit", name: "filter", value: "year", class: currentFilter === "year" ? "filter-btn active" : "filter-btn" }, i18n.eventFilterYear), - button({ type: "submit", name: "filter", value: "archived", class: currentFilter === "archived" ? "filter-btn active" : "filter-btn" }, i18n.eventFilterArchived), + ...(emptyMod ? [] : [ + button({ type: "submit", name: "filter", value: "all", class: currentFilter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.eventFilterAll).toUpperCase()), + ...(eventChipVisible("recent") ? [button({ type: "submit", name: "filter", value: "recent", class: currentFilter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.eventFilterRecent).toUpperCase())] : []), + ...(eventChipVisible("mine") ? [button({ type: "submit", name: "filter", value: "mine", class: currentFilter === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.eventFilterMine).toUpperCase())] : []), + ...(eventChipVisible("today") ? [button({ type: "submit", name: "filter", value: "today", class: currentFilter === "today" ? "filter-btn active" : "filter-btn" }, String(i18n.eventFilterToday).toUpperCase())] : []), + ...(eventChipVisible("week") ? [button({ type: "submit", name: "filter", value: "week", class: currentFilter === "week" ? "filter-btn active" : "filter-btn" }, String(i18n.eventFilterWeek).toUpperCase())] : []), + ...(eventChipVisible("month") ? [button({ type: "submit", name: "filter", value: "month", class: currentFilter === "month" ? "filter-btn active" : "filter-btn" }, String(i18n.eventFilterMonth).toUpperCase())] : []), + ...(eventChipVisible("year") ? [button({ type: "submit", name: "filter", value: "year", class: currentFilter === "year" ? "filter-btn active" : "filter-btn" }, String(i18n.eventFilterYear).toUpperCase())] : []), + ...(eventChipVisible("archived") ? [button({ type: "submit", name: "filter", value: "archived", class: currentFilter === "archived" ? "filter-btn active" : "filter-btn" }, String(i18n.eventFilterArchived).toUpperCase())] : []), + ]), button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.eventCreateButton) ) - ) + ), + currentFilter === "edit" || currentFilter === "create" + ? null + : emptyMod ? null : div({ class: "filters activity-filter-chips activity-toolbar-row" }, + renderModuleStatsBy(filtered, e => normalizeEventStatus(e.status), [{ value: 'OPEN', label: i18n.eventStatusOpen }, { value: 'CLOSED', label: i18n.eventStatusClosed }]), + form({ method: "GET", action: "/events", class: "filter-box" }, + input({ type: "hidden", name: "filter", value: currentFilter }), + input({ type: "text", name: "q", value: params.q || "", placeholder: i18n.eventSearchPlaceholder, class: "filter-box__input" }), + div({ class: "filter-box__controls" }, + button({ type: "submit", class: "filter-box__button" }, i18n.searchButton) + ) + ) + ) ), section( currentFilter === "edit" || currentFilter === "create" ? div( { class: "event-form" }, + spreadWarning, + form( + { method: "GET", action: currentFilter === "edit" ? `/events/edit/${encodeURIComponent(eventId)}` : "/events" }, + currentFilter === "edit" ? null : input({ type: "hidden", name: "filter", value: "create" }), + typeof returnTo === "string" && returnTo.startsWith("/events") ? input({ type: "hidden", name: "returnTo", value: returnTo }) : null, + label(i18n.eventPrivacyLabel), + br(), + div({ class: "apply-row" }, + select( + { name: "isPublic", class: "report-category-select" }, + opt("public", editPrivacy !== "private", i18n.eventPublic), + opt("private", editPrivacy === "private", i18n.eventPrivate) + ), + button({ type: "submit", class: "create-button" }, i18n.apply || "Apply") + ) + ), + hr({ class: "form-sep" }), + h2({ class: "report-category-fixed" }, privacyLabel(editPrivacy)), form( { action: currentFilter === "edit" ? `/events/update/${encodeURIComponent(eventId)}` : "/events/create", @@ -307,27 +313,27 @@ exports.eventView = async (events, filter, eventId, returnTo, params = {}) => { enctype: "multipart/form-data" }, input({ type: "hidden", name: "returnTo", value: ret }), + input({ type: "hidden", name: "isPublic", value: editPrivacy }), label(i18n.eventTitleLabel), br(), input({ type: "text", - name: "title", + name: "title", maxlength: "100", id: "title", required: true, - value: currentFilter === "edit" ? eventToEdit.title || "" : "" + value: formData.title || "" }), br(), label(i18n.eventDescriptionLabel), br(), textarea( { name: "description", id: "description", placeholder: i18n.eventDescriptionPlaceholder, rows: "4" }, - currentFilter === "edit" ? eventToEdit.description || "" : "" + formData.description || "" ), br(), - label(i18n.uploadMedia), - br(), - input({ type: "file", name: "image", accept: "image/*" }), - br(), + ...renderGalleryFields(formData, currentFilter === "edit"), + label(i18n.uploadMedia), br(), + input({ type: "file", name: "blob" }), br(), br(), br(), label(i18n.eventDateLabel), br(), @@ -336,20 +342,11 @@ exports.eventView = async (events, filter, eventId, returnTo, params = {}) => { name: "date", id: "date", required: true, - min: currentFilter === "create" ? minCreate : undefined, - value: currentFilter === "edit" && eventToEdit.date ? moment(eventToEdit.date).format("YYYY-MM-DDTHH:mm") : "" + min: minCreate, + value: formData.date ? moment(formData.date).format("YYYY-MM-DDTHH:mm") : "" }), br(), br(), - label(i18n.eventPrivacyLabel), - br(), - select( - { name: "isPublic", id: "isPublic" }, - opt("public", editPrivacy !== "private", i18n.eventPublic), - opt("private", editPrivacy === "private", i18n.eventPrivate) - ), - br(), - br(), label(i18n.eventLocationLabel), br(), input({ @@ -357,17 +354,17 @@ exports.eventView = async (events, filter, eventId, returnTo, params = {}) => { name: "location", id: "location", required: true, - value: currentFilter === "edit" ? eventToEdit.location || "" : "" + value: formData.location || "" }), br(), label(i18n.mapLocationTitle || "Map Location"), br(), - input({ type: "text", name: "mapUrl", placeholder: i18n.mapUrlPlaceholder || "/maps/MAP_ID", value: eventToEdit?.mapUrl || "" }), + input({ type: "text", name: "mapUrl", placeholder: i18n.mapUrlPlaceholder || "/maps/MAP_ID", value: formData.mapUrl || "" }), br(), br(), label(i18n.eventUrlLabel), br(), - input({ type: "url", name: "url", id: "url", value: currentFilter === "edit" ? eventToEdit.url || "" : "" }), + input({ type: "url", name: "url", id: "url", value: formData.url || "" }), br(), br(), label(i18n.eventPriceLabel), @@ -378,15 +375,23 @@ exports.eventView = async (events, filter, eventId, returnTo, params = {}) => { id: "price", min: "0.000000", step: "0.000001", - value: currentFilter === "edit" ? parseFloat(eventToEdit.price || 0).toFixed(6) : (0).toFixed(6) + value: parseFloat(formData.price || 0).toFixed(6) }), + renderEcoValueChip(), br(), br(), label(i18n.eventTagsLabel), br(), - input({ type: "text", name: "tags", id: "tags", value: currentFilter === "edit" ? editTags.join(", ") : "" }), + input({ type: "text", name: "tags", id: "tags", placeholder: i18n.tagsPlaceholder, value: editTags.join(", ") }), br(), br(), + div({ class: "event-recurrence-block" }, + label(i18n.eventRecurrenceLabel), + br(), + renderIntervalBlock({ min: minCreate }, editInterval, editIntervalUntil) + ), + br(), + showClearnet ? renderClearnetSelector(formData.clearnet === true || String(formData.clearnet || "") === "1", i18n) : null, ...(currentFilter === "create" ? [ input({ type: "hidden", name: "addToCalendar", value: "0" }), label(i18n.eventAddToCalendar || "Add to Calendar"), @@ -399,13 +404,14 @@ exports.eventView = async (events, filter, eventId, returnTo, params = {}) => { ) ) : filtered.length > 0 - ? div({ class: "jobs-grid" }, filtered.map((e) => renderEventItem(e, currentFilter, params.spreadMap && params.spreadMap.get(e.id)))) - : p(i18n.noevents) + ? div({ class: "jobs-grid" }, paged(filtered).map((e) => renderEventItem(e, currentFilter, params.spreadMap && params.spreadMap.get(e.id)))) + : div({ class: "no-content-box" }, p(i18n.noevents)) ) ); }; exports.singleEventView = async (event, filter, comments = [], params = {}) => { + const eventChipVisible = (mode) => eventChipFor(currentFilter, Array.isArray(params.censusList) ? params.censusList : null)(mode); const currentFilter = filter || "all"; const commentCount = typeof event.commentCount === "number" ? event.commentCount : 0; const attendees = safeArray(event.attendees); @@ -419,13 +425,14 @@ exports.singleEventView = async (event, filter, comments = [], params = {}) => { { class: "filters" }, form( { method: "GET", action: "/events" }, - button({ type: "submit", name: "filter", value: "all", class: currentFilter === "all" ? "filter-btn active" : "filter-btn" }, i18n.eventFilterAll), - button({ type: "submit", name: "filter", value: "mine", class: currentFilter === "mine" ? "filter-btn active" : "filter-btn" }, i18n.eventFilterMine), - button({ type: "submit", name: "filter", value: "today", class: currentFilter === "today" ? "filter-btn active" : "filter-btn" }, i18n.eventFilterToday), - button({ type: "submit", name: "filter", value: "week", class: currentFilter === "week" ? "filter-btn active" : "filter-btn" }, i18n.eventFilterWeek), - button({ type: "submit", name: "filter", value: "month", class: currentFilter === "month" ? "filter-btn active" : "filter-btn" }, i18n.eventFilterMonth), - button({ type: "submit", name: "filter", value: "year", class: currentFilter === "year" ? "filter-btn active" : "filter-btn" }, i18n.eventFilterYear), - button({ type: "submit", name: "filter", value: "archived", class: currentFilter === "archived" ? "filter-btn active" : "filter-btn" }, i18n.eventFilterArchived), + button({ type: "submit", name: "filter", value: "all", class: currentFilter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.eventFilterAll).toUpperCase()), + ...(eventChipVisible("recent") ? [button({ type: "submit", name: "filter", value: "recent", class: currentFilter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.eventFilterRecent).toUpperCase())] : []), + ...(eventChipVisible("mine") ? [button({ type: "submit", name: "filter", value: "mine", class: currentFilter === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.eventFilterMine).toUpperCase())] : []), + ...(eventChipVisible("today") ? [button({ type: "submit", name: "filter", value: "today", class: currentFilter === "today" ? "filter-btn active" : "filter-btn" }, String(i18n.eventFilterToday).toUpperCase())] : []), + ...(eventChipVisible("week") ? [button({ type: "submit", name: "filter", value: "week", class: currentFilter === "week" ? "filter-btn active" : "filter-btn" }, String(i18n.eventFilterWeek).toUpperCase())] : []), + ...(eventChipVisible("month") ? [button({ type: "submit", name: "filter", value: "month", class: currentFilter === "month" ? "filter-btn active" : "filter-btn" }, String(i18n.eventFilterMonth).toUpperCase())] : []), + ...(eventChipVisible("year") ? [button({ type: "submit", name: "filter", value: "year", class: currentFilter === "year" ? "filter-btn active" : "filter-btn" }, String(i18n.eventFilterYear).toUpperCase())] : []), + ...(eventChipVisible("archived") ? [button({ type: "submit", name: "filter", value: "archived", class: currentFilter === "archived" ? "filter-btn active" : "filter-btn" }, String(i18n.eventFilterArchived).toUpperCase())] : []), button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.eventCreateButton) ) ); @@ -437,38 +444,39 @@ exports.singleEventView = async (event, filter, comments = [], params = {}) => { ); } - const { renderReachChip, renderEncryptedChip } = require('./clearnet_view'); - const isClearnet = !!(params.authorPrefs && params.authorPrefs.clearnetEvents && normalizeEventStatus(event.status) !== 'CLOSED' && normalizePrivacy(event.isPublic) === 'public'); + const canClearnet = clearnetEligible(event); + const isClearnet = canClearnet && !!event.clearnet; const isPrivate = normalizePrivacy(event.isPublic) === 'private'; const isEncrypted = !!event.encrypted || isPrivate; const isAttending = attendees.includes(userId); const isOrganizer = String(event.organizer) === String(userId); + const isEventOwner = String(event.organizer || event.author) === String(userId); + const sharesEvent = isEventOwner || attendees.includes(userId); + const eventSubIn = isEventOwner || (event.subscription && event.subscription.subscribed === true); const returnToSelf = `/events/${encodeURIComponent(event.id)}?filter=${encodeURIComponent(currentFilter)}`; const chips = [ - renderEventStatusChip(event.status), renderPrivacyChip(isPrivate, i18n), - isEncrypted ? renderEncryptedChip(i18n) : renderReachChip(isClearnet, i18n), + renderEventStatusChip(event.status), + isEncrypted ? renderEncryptedChip(i18n) : renderReachChip(isClearnet, i18n, isClearnet ? clearnetItemHref('events', event.title, event.id) : null), + isOrganizer && canClearnet ? renderClearnetSwitch('events', event.rootId || event.id, isClearnet) : null, isAttending ? renderStateChip("whole", "★", i18n.eventAttended) : null, renderLifespanChip(event.lifetime, i18n), - renderEcoTax(event.msgSize, event.id) + renderEcoTax(event.msgSize, event.id), + (event.subscription && sharesEvent) + ? renderStateChip(eventSubIn ? "mutuals" : "closed", "✉", eventSubIn ? i18n.subscriptionOn : i18n.subscriptionOff) + : null ].filter(Boolean); const sideActions = []; - if (event.organizer && event.organizer !== userId) { - sideActions.push(form({ method: "GET", action: "/pm" }, - input({ type: "hidden", name: "recipients", value: event.organizer }), - button({ type: "submit", class: "filter-btn" }, i18n.privateMessage) - )); - } const attendNode = renderEventAttendAction(event, isAttending, returnToSelf); if (attendNode) sideActions.push(attendNode); if (params.linkedCalendarId) { sideActions.push(form({ method: "GET", action: `/calendars/${encodeURIComponent(params.linkedCalendarId)}` }, - button({ type: "submit", class: "filter-btn" }, i18n.eventVisitCalendar || "Visit calendar") + button({ type: "submit", class: "tribe-action-btn" }, i18n.eventVisitCalendar || "Visit calendar") )); } - if (isOrganizer) sideActions.push(...renderEventOwnerActions(event, returnToSelf)); + const ownerActions = isOrganizer ? renderEventOwnerActions(event, returnToSelf) : []; const tagsNode = event.tags && event.tags.filter(Boolean).length ? div({ class: "card-tags" }, @@ -482,7 +490,16 @@ exports.singleEventView = async (event, filter, comments = [], params = {}) => { td({ class: "tribe-info-label" }, labelText), td({ class: "tribe-info-value" }, valueNode) )); - if (event.date) pushRow(i18n.eventDateLabel, moment(event.date).format("YYYY/MM/DD HH:mm")); + if (event.date) pushRow(i18n.eventDateLabel, moment(event.recurring && event.nextDate ? event.nextDate : event.date).format("YYYY/MM/DD HH:mm")); + if (event.recurring) { + const parts = [ + event.intervalWeekly ? (i18n.calendarIntervalWeekly || "Weekly") : null, + event.intervalMonthly ? (i18n.calendarIntervalMonthly || "Monthly") : null, + event.intervalYearly ? (i18n.calendarIntervalYearly || "Yearly") : null + ].filter(Boolean); + pushRow(i18n.eventRecurrenceLabel, parts.join(" · ")); + if (event.recurrenceUntil) pushRow(i18n.eventRecurrenceUntil, moment(event.recurrenceUntil).format("YYYY/MM/DD HH:mm")); + } pushRow(i18n.eventStatus, eventStatusLabel(event.status)); pushRow(i18n.eventPrivacyLabel, privacyLabel(event.isPublic)); if (event.location && String(event.location).trim()) pushRow(i18n.eventLocationLabel, event.location); @@ -497,18 +514,41 @@ exports.singleEventView = async (event, filter, comments = [], params = {}) => { : null; const eventSide = div({ class: "tribe-side" }, + div({ class: "card-header activity-card-header" }, + renderContentActions(event.id, returnToSelf, { spread: params.spreads || null, author: event.organizer || event.author, favKind: 'events', isFavorite: event.isFavorite, reportTitle: event.title, deleteAction: isOrganizer ? contentDeleteAction('event', event.id) : undefined, returnTo: `/events?filter=${encodeURIComponent(currentFilter)}` }) + ), div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, event.title) ), chips.length ? div({ class: "card-chips-row" }, ...chips) : null, - div({ class: "card-spread-centered" }, renderSpreadButton(event.id, params.spreads)), - table({ class: "tribe-info-table jobs-info-table" }, ...infoRows), + renderPhotoGallery(event, 'event'), + table({ class: "tribe-info-table jobs-info-table" }, ...infoRows, ...renderCreatedRows(event.createdAt, event.organizer)), + event.recurring && Array.isArray(event.occurrences) && event.occurrences.length > 1 + ? details({ class: "event-occurrences" }, + summary({ class: "chat-thread-summary" }, `${i18n.eventUpcomingDates} (${event.occurrences.length})`), + ul({ class: "event-occurrence-list" }, + ...event.occurrences.map(d => li(moment(d).format("YYYY/MM/DD HH:mm"))) + ) + ) + : null, tagsNode, div({ class: "tribe-card-members" }, span({ class: "tribe-members-count" }, `${i18n.eventAttendees}: ${attendees.length}`) ), attendeesListNode, - sideActions.length ? div({ class: "tribe-side-actions" }, ...sideActions) : null + sideActions.length ? div({ class: "tribe-side-actions" }, ...sideActions) : null, + ownerActions.length ? div({ class: "tribe-side-actions owner-actions" }, ...ownerActions) : null, + (event.subscription && sharesEvent) + ? renderSubscriptionBox({ + target: event.id, + scope: "events", + subscribed: event.subscription.subscribed === true, + count: event.subscription.count, + isOwner: isEventOwner, + returnTo: returnToSelf + }) + : null, + renderDocumentActions('events', event.id) ); const returnToOpinions = `/events/${encodeURIComponent(event.id)}?filter=${encodeURIComponent(currentFilter)}`; @@ -518,21 +558,17 @@ exports.singleEventView = async (event, filter, comments = [], params = {}) => { event.description ? div({ class: "job-section" }, h2({ class: "job-section-title" }, i18n.eventDescriptionLabel), - p({ class: "tribe-side-description" }, ...renderUrl(event.description)) + p({ class: "tribe-side-description" }, ...renderStyledText(event.description)) ) : null, event.mapUrl ? div({ class: "job-section" }, renderMapEmbed(params.mapData, event.mapUrl)) : null, - p({ class: "card-footer" }, - span({ class: "date-link" }, `${moment(event.createdAt).format("YYYY/MM/DD HH:mm")} ${i18n.performed} `), - userLink(event.organizer) - ), - opinionsBar, - renderEventCommentsSection(event.id, comments, currentFilter) + renderEngagement(event.id, opinionsBar, renderEventCommentsSection(event.id, comments, currentFilter)) ); return template( event.title, section( + div({ class: "tags-header module-header-line" }, h2(i18n.eventsTitle), p(i18n.eventsDescription)), filterBar, div({ class: "tribe-details" }, eventSide, eventMain) ) @@ -540,10 +576,10 @@ exports.singleEventView = async (event, filter, comments = [], params = {}) => { }; exports.clearnetEventView = async (event) => { - const { escapeHtml: esc, renderClearnetPage } = require('./clearnet_view'); - const title = esc(event.title || 'Event'); - const desc = esc(event.description || ''); - const dateStr = event.date ? esc(moment(event.date).format("YYYY-MM-DD HH:mm")) : ''; + const { escapeHtml: esc, renderRichText, renderKindTag, renderClearnetPage } = require('./clearnet_view'); + const title = esc(event.title || i18n.cnKindEvent); + const desc = renderRichText(event.description || ''); + const dateStr = event.date ? esc(moment(event.date).format("YYYY/MM/DD HH:mm")) : ''; const loc = esc(event.location || ''); const price = parseFloat(event.price || 0); const priceStr = price > 0 ? `${price.toFixed(2)} ECO` : ''; @@ -559,15 +595,17 @@ exports.clearnetEventView = async (event) => { const body = `

${title}

+ ${renderKindTag('event')} ${dateStr ? `📅 ${dateStr}` : ''} ${loc ? `📍 ${loc}` : ''} ${priceStr ? `💰 ${priceStr}` : ''}
+
${desc ? `

${desc}

` : ''} - ${urlHref ? `More info →` : ''} + ${urlHref ? `${esc(i18n.cnMoreInfo)}` : ''} `; return renderClearnetPage({ - title: `${event.title || 'Event'} — Oasis`, + title: `${event.title || 'Event'} | Oasis`, ogTitle: event.title || 'Event', ogDescription: event.description || '', extraCss, diff --git a/src/views/favorites_view.js b/src/views/favorites_view.js index a5e3106..4642562 100644 --- a/src/views/favorites_view.js +++ b/src/views/favorites_view.js @@ -1,15 +1,43 @@ const { form, button, div, h2, p, section, input, a, span, img } = require("../server/node_modules/hyperaxe"); -const { template, i18n, userLink, renderContentActions } = require("./main_views"); +const { template, i18n, userLink, renderContentActions, renderModuleStats, CONTENT_FAV_KIND, CONTENT_TORRENTABLE, paged } = require("./main_views"); const moment = require("../server/node_modules/moment"); const { config } = require("../server/SSB_server.js"); -const { renderUrl } = require("../backend/renderUrl"); +const { renderStyledText, safeExternalHref } = require("../backend/renderStyledText"); const userId = config.keys.id; const safeArr = (v) => (Array.isArray(v) ? v : []); const safeText = (v) => String(v || "").trim(); +const TORRENTABLE_KINDS = new Set(Object.keys(CONTENT_FAV_KIND).filter((t) => CONTENT_TORRENTABLE.has(t)).map((t) => CONTENT_FAV_KIND[t])); + +const FILTER_KINDS = [ + { value: "audios", label: () => i18n.favoritesFilterAudios }, + { value: "bookmarks", label: () => i18n.favoritesFilterBookmarks }, + { value: "calendars", label: () => i18n.favoritesFilterCalendars }, + { value: "chats", label: () => i18n.favoritesFilterChats }, + { value: "documents", label: () => i18n.favoritesFilterDocuments }, + { value: "events", label: () => i18n.favoritesFilterEvents }, + { value: "files", label: () => i18n.favoritesFilterFiles }, + { value: "forum", label: () => i18n.favoritesFilterForum }, + { value: "housing", label: () => i18n.favoritesFilterHousing }, + { value: "images", label: () => i18n.favoritesFilterImages }, + { value: "jobs", label: () => i18n.favoritesFilterJobs }, + { value: "maps", label: () => i18n.favoritesFilterMaps }, + { value: "market", label: () => i18n.favoritesFilterMarket }, + { value: "pads", label: () => i18n.favoritesFilterPads }, + { value: "projects", label: () => i18n.favoritesFilterProjects }, + { value: "reports", label: () => i18n.favoritesFilterReports }, + { value: "rooms", label: () => i18n.favoritesFilterRooms }, + { value: "shopProducts", label: () => i18n.favoritesFilterShopProducts }, + { value: "tasks", label: () => i18n.favoritesFilterTasks }, + { value: "torrents", label: () => i18n.favoritesFilterTorrents }, + { value: "transfers", label: () => i18n.favoritesFilterTransfers }, + { value: "videos", label: () => i18n.favoritesFilterVideos }, + { value: "votes", label: () => i18n.favoritesFilterVotes } +]; + const buildReturnTo = (filter) => { const f = safeText(filter || "all"); return `/favorites?filter=${encodeURIComponent(f)}`; @@ -25,12 +53,53 @@ const renderTags = (tags) => { : null; }; +const DETAIL_KEYS = [ + "status", "category", "severity", "priority", "location", "place", + "date", "deadline", "startTime", "endTime", "price", "salary", "amount", + "concept", "question", "housing_type", "property_type", "item_type", "sector" +]; + +const fieldLabel = (key) => { + const camel = key.replace(/_(.)/g, (_, c) => c.toUpperCase()); + return i18n[camel + "Label"] || i18n["search" + camel.charAt(0).toUpperCase() + camel.slice(1)] || camel.toUpperCase(); +}; + +const fieldValue = (v) => { + if (v === null || v === undefined) return ""; + if (Array.isArray(v)) return v.filter(Boolean).join(", "); + if (typeof v === "object") return ""; + return String(v).trim(); +}; + +const renderDetailFields = (item) => { + const content = item && item.content; + if (!content || typeof content !== "object") return null; + const rows = DETAIL_KEYS + .map((k) => [k, fieldValue(content[k])]) + .filter(([, v]) => v) + .map(([k, v]) => + div({ class: "card-field" }, + span({ class: "card-label" }, `${fieldLabel(k)}: `), + span({ class: "card-value" }, v) + ) + ); + const members = ["members", "participants", "attendees", "assignees", "confirmedBy"] + .map((k) => [k, Array.isArray(content[k]) ? content[k].length : null]) + .find(([, n]) => typeof n === "number" && n > 0); + if (members) { + rows.push(div({ class: "tribe-card-members" }, + span({ class: "tribe-members-count" }, `${fieldLabel(members[0])}: ${members[1]}`) + )); + } + return rows.length ? div({ class: "cv-card-fields" }, ...rows) : null; +}; + const renderBookmarkUrl = (item) => { if (item.kind !== "bookmarks") return null; if (!item.url) return null; return p( a( - { href: item.url, target: "_blank", rel: "noreferrer noopener", class: "bookmark-url" }, + { href: safeExternalHref(item.url), target: "_blank", rel: "noreferrer noopener", class: "bookmark-url" }, item.url ) ); @@ -54,13 +123,13 @@ const renderImagePreview = (item) => { ); }; -const renderFavoriteCard = (item, filter) => { +const renderFavoriteCard = (item, filter, spreadMap = null) => { const returnTo = buildReturnTo(filter); const title = safeText(item.title) || safeText(item.name) || safeText(item.category) || safeText(item.url) || ""; const ts = item.updatedAt || item.createdAt; - const absDate = ts ? moment(ts).format("YYYY/MM/DD HH:mm:ss") : ""; + const absDate = ts ? moment(ts).format("YYYY/MM/DD HH:mm") : ""; const isOwn = item.author && String(item.author) === String(userId); return div( @@ -72,48 +141,48 @@ const renderFavoriteCard = (item, filter) => { span({ class: 'pm-exposition-text' }, String(item.kind || '').toUpperCase()) ) ), - renderContentActions(item.favId, item.viewHref) + renderContentActions(item.favId, item.viewHref, { + author: item.author, + spread: (spreadMap && spreadMap.get(item.favId)) || null, + reportTitle: title, + ...(TORRENTABLE_KINDS.has(item.kind) && typeof item.url === "string" && item.url.startsWith("&") ? { torrentFrom: { blobId: item.url, name: item.title } } : {}) + }) ), div( { class: "card-section favorites-card-body" }, - div( - { class: "bookmark-topbar" }, - div( - { class: "bookmark-topbar-left" }, - form( - { - method: "POST", - action: `/favorites/remove/${encodeURIComponent(item.kind)}/${encodeURIComponent(item.favId)}`, - class: "bookmark-favorite-form" - }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - button({ type: "submit", class: "filter-btn" }, i18n.favoritesRemoveButton) - ) - ) - ), - title ? h2(title) : null, + title ? div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, item.viewHref ? a({ href: item.viewHref }, title) : title)) : null, renderImagePreview(item), renderBookmarkUrl(item), - safeText(item.description) ? p(...renderUrl(item.description)) : null, + safeText(item.description) ? p(...renderStyledText(item.description)) : null, + renderDetailFields(item), renderTags(item.tags), p( - { class: "card-footer" }, - absDate ? span({ class: "date-link" }, `${absDate} ${i18n.performed} `) : "", + { class: "card-footer favorites-meta" }, + absDate ? span({ class: "date-link" }, `${absDate}`) : "", item.author ? userLink(item.author) : "" + ), + p( + { class: "card-footer" }, + form( + { method: "POST", action: `/favorites/remove/${encodeURIComponent(item.kind)}/${encodeURIComponent(item.favId)}`, class: "phone-action-form" }, + input({ type: "hidden", name: "returnTo", value: returnTo }), + button({ type: "submit", class: "tribe-action-btn" }, String(i18n.agendaDiscardButton).toUpperCase()) + ) ) ) ); }; -exports.favoritesView = async (items, filter = "all", counts = {}) => { +exports.favoritesView = async (items, filter = "all", counts = {}, q = "", params = {}) => { const c = counts || {}; const total = typeof c.all === "number" ? c.all : safeArr(items).length; + const emptyFav = total === 0 && !String(q || "").trim(); return template( i18n.favoritesTitle, section( - div({ class: "tags-header" }, h2(i18n.favoritesTitle), p(i18n.favoritesDescription)), - div( + div({ class: "tags-header module-header-line" }, h2(i18n.favoritesTitle), p(i18n.favoritesDescription)), + emptyFav ? null : div( { class: "filters" }, form( { method: "GET", action: "/favorites", class: "ui-toolbar ui-toolbar--filters" }, @@ -121,61 +190,33 @@ exports.favoritesView = async (items, filter = "all", counts = {}) => { { type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, `${i18n.favoritesFilterAll} (${total})` ), - button( - { type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, - `${i18n.favoritesFilterRecent} (${total})` - ), - button( - { type: "submit", name: "filter", value: "audios", class: filter === "audios" ? "filter-btn active" : "filter-btn" }, - `${i18n.favoritesFilterAudios} (${c.audios || 0})` - ), - button( - { type: "submit", name: "filter", value: "bookmarks", class: filter === "bookmarks" ? "filter-btn active" : "filter-btn" }, - `${i18n.favoritesFilterBookmarks} (${c.bookmarks || 0})` - ), - button( - { type: "submit", name: "filter", value: "documents", class: filter === "documents" ? "filter-btn active" : "filter-btn" }, - `${i18n.favoritesFilterDocuments} (${c.documents || 0})` - ), - button( - { type: "submit", name: "filter", value: "images", class: filter === "images" ? "filter-btn active" : "filter-btn" }, - `${i18n.favoritesFilterImages} (${c.images || 0})` - ), - button( - { type: "submit", name: "filter", value: "maps", class: filter === "maps" ? "filter-btn active" : "filter-btn" }, - `${i18n.favoritesFilterMaps} (${c.maps || 0})` - ), - button( - { type: "submit", name: "filter", value: "pads", class: filter === "pads" ? "filter-btn active" : "filter-btn" }, - `${i18n.favoritesFilterPads || "PADS"} (${c.pads || 0})` - ), - button( - { type: "submit", name: "filter", value: "chats", class: filter === "chats" ? "filter-btn active" : "filter-btn" }, - `${i18n.favoritesFilterChats || "CHATS"} (${c.chats || 0})` - ), - button( - { type: "submit", name: "filter", value: "calendars", class: filter === "calendars" ? "filter-btn active" : "filter-btn" }, - `${i18n.favoritesFilterCalendars || "CALENDARS"} (${c.calendars || 0})` - ), - button( - { type: "submit", name: "filter", value: "videos", class: filter === "videos" ? "filter-btn active" : "filter-btn" }, - `${i18n.favoritesFilterVideos} (${c.videos || 0})` - ), - button( - { type: "submit", name: "filter", value: "torrents", class: filter === "torrents" ? "filter-btn active" : "filter-btn" }, - `${i18n.favoritesFilterTorrents || "TORRENTS"} (${c.torrents || 0})` - ), - button( - { type: "submit", name: "filter", value: "market", class: filter === "market" ? "filter-btn active" : "filter-btn" }, - `${i18n.favoritesFilterMarket || "MARKET"} (${c.market || 0})` - ), - button( - { type: "submit", name: "filter", value: "shopProducts", class: filter === "shopProducts" ? "filter-btn active" : "filter-btn" }, - `${i18n.favoritesFilterShopProducts || "SHOP ITEMS"} (${c.shopProducts || 0})` + (c.recent || 0) > 0 || filter === "recent" + ? button( + { type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, + `${i18n.favoritesFilterRecent} (${c.recent || 0})` + ) + : null, + ...FILTER_KINDS.filter((k) => (c[k.value] || 0) > 0).map((k) => + button( + { type: "submit", name: "filter", value: k.value, class: filter === k.value ? "filter-btn active" : "filter-btn" }, + `${k.label()} (${c[k.value] || 0})` + ) ) ) ), - div({ class: "bookmark-list" }, safeArr(items).length ? safeArr(items).map((it) => renderFavoriteCard(it, filter)) : p(i18n.favoritesNoItems)) + emptyFav ? null : div( + { class: "filters activity-filter-chips activity-toolbar-row" }, + renderModuleStats(items.length), + form( + { method: "GET", action: "/favorites", class: "filter-box" }, + input({ type: "hidden", name: "filter", value: filter }), + input({ type: "text", name: "q", value: q, placeholder: i18n.favoritesSearchPlaceholder, class: "filter-box__input" }), + div({ class: "filter-box__controls" }, + button({ type: "submit", class: "filter-box__button" }, i18n.searchButton) + ) + ) + ), + div({ class: "bookmark-list" }, safeArr(items).length ? paged(safeArr(items)).map((it) => renderFavoriteCard(it, filter, params.spreadMap)) : p(i18n.favoritesNoItems)) ) ); }; diff --git a/src/views/fediverse_view.js b/src/views/fediverse_view.js index c651941..580eab7 100644 --- a/src/views/fediverse_view.js +++ b/src/views/fediverse_view.js @@ -1,12 +1,14 @@ -const { div, h2, h3, p, section, button, form, a, span, strong, input, label, img, textarea, br, hr, video: videoHyperaxe, audio: audioHyperaxe } = require("../server/node_modules/hyperaxe"); +const { div, h2, h3, h4, p, section, button, form, a, span, strong, input, label, img, textarea, br, hr, select, option, video: videoHyperaxe, audio: audioHyperaxe } = require("../server/node_modules/hyperaxe"); +const RAW_HTML = Symbol.for('oasis.rawHtml'); const { template, i18n } = require("./main_views"); +const { renderZoomableImage } = require("./gallery_view"); const { sanitizeHtml } = require('../backend/sanitizeHtml'); const moment = require("../server/node_modules/moment"); const fmtDate = (ts) => { if (!ts) return ""; const m = moment(ts); - return m.isValid() ? m.format("YYYY-MM-DD HH:mm") : ""; + return m.isValid() ? m.format("YYYY/MM/DD HH:mm") : ""; }; const renderMedia = (m) => { @@ -17,7 +19,7 @@ const renderMedia = (m) => { if (m.type === 'audio') { return audioHyperaxe({ class: "post-audio", src: m.url, controls: true }); } - return img({ class: "post-image", src: m.url, alt: m.description || "" }); + return renderZoomableImage(m.url, { imgClass: "post-image", alt: m.description || "" }); }; const renderComposeForm = (opts = {}) => { @@ -26,7 +28,7 @@ const renderComposeForm = (opts = {}) => { form( { method: "POST", action, enctype: "multipart/form-data" }, replyToId ? input({ type: "hidden", name: "inReplyToId", value: replyToId }) : "", - textarea({ name: "text", id: "text", rows: "6", class: "publish-textarea", placeholder: placeholder || i18n.fediverseComposePlaceholder }), + textarea({ maxlength: "5000", name: "text", id: "text", rows: "6", class: "publish-textarea", placeholder: placeholder || i18n.fediverseComposePlaceholder }), br(), label({ for: "fediverse_media" }, i18n.fediverseAttach), br(), @@ -67,7 +69,7 @@ const renderPost = (post, returnTo, opts = {}) => { ), span({ class: "fediverse-date" }, fmtDate(post.createdAt)) ), - div({ class: "feed-text", innerHTML: sanitizeHtml(post.html) }), + div({ class: "feed-text", [RAW_HTML]: sanitizeHtml(post.html) }), post.media && post.media.length ? div({ class: "fediverse-media" }, post.media.map(renderMedia)) : "", opts.noActions ? "" : renderActions(post, returnTo) ); @@ -90,7 +92,7 @@ const mastodonBox = (account, stats, actions, showLabel) => { ), actions && actions.length ? div({ class: "fediverse-compact-actions" }, actions) : "" ), - (stats && stats.bio) ? div({ class: "fediverse-bio", innerHTML: sanitizeHtml(stats.bio) }) : "", + (stats && stats.bio) ? div({ class: "fediverse-bio", [RAW_HTML]: sanitizeHtml(stats.bio) }) : "", stats ? div({ class: "fediverse-stats" }, stat(i18n.fediverseFollowers, stats.followers), @@ -102,7 +104,7 @@ const mastodonBox = (account, stats, actions, showLabel) => { (stats && Array.isArray(stats.fields) && stats.fields.length) ? div({ class: "fediverse-fields" }, stats.fields.map(f => div({ class: "fediverse-field" }, span({ class: "fediverse-field-name" }, f.name, f.verified ? span({ class: "fediverse-field-verified", title: "verified" }, " ✓") : ""), - span({ class: "fediverse-field-value", innerHTML: sanitizeHtml(f.value) }) + span({ class: "fediverse-field-value", [RAW_HTML]: sanitizeHtml(f.value) }) ))) : "" ) @@ -111,7 +113,7 @@ const mastodonBox = (account, stats, actions, showLabel) => { const disconnectedBlock = () => { const text = String(i18n.fediverseDisconnected || ""); - const settingsLink = a({ href: "/settings#fediverse" }, i18n.fediverseEmptyLink || "settings"); + const settingsLink = a({ href: "/settings?filter=multiverse" }, i18n.fediverseEmptyLink || "settings"); const parts = text.split("%LINK%"); const para = parts.length > 1 ? p(parts[0], settingsLink, parts.slice(1).join("%LINK%")) @@ -119,16 +121,322 @@ const disconnectedBlock = () => { return div({ class: "fediverse-disconnected" }, h2(i18n.fediverseStatus), para); }; -exports.fediverseOverviewView = ({ account, stats } = {}) => { - const box = account - ? mastodonBox(account, stats, [ - form({ method: "GET", action: "/fediverse/mastodon" }, button({ type: "submit", class: "filter-btn" }, i18n.fediverseManage)) - ], true) - : null; + +const telegramBox = (account, stats, actions, showLabel) => { + const stat = (lbl, val) => div({ class: "fediverse-stat" }, span({ class: "fediverse-stat-label" }, lbl), strong(String(val))); + const handle = account.username ? `@${account.username}` : (account.phone || ""); + return div({ class: "fediverse-network" }, + showLabel ? h3("Telegram") : "", + div({ class: "fediverse-profile" }, + div({ class: "fediverse-namerow" }, + account.avatar ? img({ class: "fediverse-avatar", src: account.avatar, alt: account.displayName }) : "", + div({ class: "fediverse-profile-id" }, + div({ class: "fediverse-name" }, account.displayName || handle), + span({ class: "fediverse-acct" }, handle) + ), + actions && actions.length ? div({ class: "fediverse-compact-actions" }, actions) : "" + ), + stats + ? div({ class: "fediverse-stats" }, + stat(i18n.telegramChats, stats.chats), + stat(i18n.telegramUnread, stats.unread) + ) + : "" + ) + ); +}; + +const telegramKindLabel = (kind) => kind === 'channel' ? i18n.telegramChannel : kind === 'group' ? i18n.telegramGroup : i18n.telegramPrivate; + +const renderTelegramMedia = (m) => { + if (!m || !m.url) return ""; + if (m.kind === 'photo') return renderZoomableImage(m.url, { imgClass: "post-image", alt: m.name || "" }); + if (m.kind === 'video') return videoHyperaxe({ class: "post-video", src: m.url, controls: true, preload: "metadata" }); + if (m.kind === 'audio') return audioHyperaxe({ class: "post-audio", src: m.url, controls: true }); + return a({ href: m.url, class: "filter-btn" }, `📎 ${m.name || m.mime || "file"}`); +}; + +const renderTelegramDialog = (d) => { + const href = `/fediverse/telegram/chat/${encodeURIComponent(d.id)}#telegram-latest`; + const preview = d.last ? ((d.last.out ? "→ " : "") + (d.last.text || (d.last.hasMedia ? "📎" : ""))) : ""; + return div({ class: "feed-card fediverse-card telegram-dialog" + (d.unread > 0 ? " telegram-unread" : "") }, + div({ class: "fediverse-head" }, + img({ class: "fediverse-avatar", src: d.avatar, alt: "" }), + div({ class: "fediverse-author" }, + a({ class: "fediverse-name", href }, d.title), + span({ class: "fediverse-acct" }, telegramKindLabel(d.kind)) + ), + d.unread > 0 ? span({ class: "telegram-unread-badge" }, String(d.unread)) : "", + span({ class: "fediverse-date" }, fmtDate(d.date)) + ), + preview ? p({ class: "telegram-preview" }, preview.length > 160 ? `${preview.slice(0, 160)}…` : preview) : "", + div({ class: "fediverse-actions" }, + form({ method: "GET", action: href }, button({ type: "submit", class: "filter-btn" }, i18n.telegramOpen)) + ) + ); +}; + +const renderTelegramMessage = (m) => div({ class: "feed-card fediverse-card telegram-message" + (m.out ? " telegram-out" : "") }, + div({ class: "fediverse-head" }, + span({ class: "fediverse-name" }, m.out ? i18n.telegramYou : (m.senderName || "")), + span({ class: "fediverse-date" }, fmtDate(m.date)) + ), + m.text ? div({ class: "feed-text telegram-text" }, m.text) : "", + m.media ? div({ class: "fediverse-media" }, renderTelegramMedia(m.media)) : "" +); + +exports.telegramDialogsView = ({ account, stats, dialogs, error } = {}) => { + if (!account) return exports.fediverseOverviewView({ account: null }); + const body = []; + body.push(div({ class: "tags-header module-header-line" }, h2(i18n.fediverse), p(i18n.fediverseDescription))); + body.push(telegramBox(account, stats, [ + a({ href: "/fediverse", class: "filter-btn" }, `← ${i18n.fediverse}`), + a({ href: "/fediverse/telegram?refresh=1", class: "filter-btn" }, i18n.fediverseRefresh) + ], false)); + if (error) body.push(div({ class: "fediverse-error" }, i18n[error] || i18n.telegramErrConnect)); + const list = Array.isArray(dialogs) ? dialogs : []; + if (!list.length && !error) body.push(p({ class: "muted" }, i18n.telegramNoChats)); + else body.push(section({ class: "feed-container" }, list.map(renderTelegramDialog))); + return template(i18n.fediverse, section(...body)); +}; + +exports.telegramChatView = ({ account, stats, chat, error } = {}) => { + if (!account) return exports.fediverseOverviewView({ account: null }); + const header = section( + div({ class: "tags-header module-header-line" }, h2(i18n.fediverse), p(i18n.fediverseDescription)), + telegramBox(account, stats, [ + a({ href: "/fediverse/telegram", class: "filter-btn" }, `← ${i18n.telegramChats}`) + ], false) + ); + if (!chat) return template(i18n.fediverse, header, section(div({ class: "fediverse-error" }, i18n[error] || i18n.telegramErrFetch))); + const body = []; + body.push(div({ class: "telegram-chat-title" }, + h3(chat.title), + span({ class: "fediverse-acct" }, telegramKindLabel(chat.kind)), + a({ href: "#telegram-latest", class: "filter-btn chat-jump-latest" }, i18n.chatJumpLatest) + )); + if (error) body.push(div({ class: "fediverse-error" }, i18n[error] || i18n.telegramErrConnect)); + const list = Array.isArray(chat.messages) ? chat.messages : []; + if (!list.length) body.push(p({ class: "muted" }, i18n.telegramNoMessages)); + else body.push(section({ class: "feed-container" }, + list.map((m, idx) => { + const node = renderTelegramMessage(m); + return idx === list.length - 1 ? div({ id: "telegram-latest", class: "chat-latest-anchor" }, node) : node; + }) + )); + body.push(br(), hr(), br()); + body.push(div({ class: "publish-form" }, + form({ method: "POST", action: `/fediverse/telegram/chat/${encodeURIComponent(chat.id)}/send`, enctype: "multipart/form-data" }, + textarea({ maxlength: "4096", name: "text", rows: "4", class: "publish-textarea", placeholder: i18n.telegramComposePlaceholder }), + br(), + label({ for: "telegram_media" }, i18n.fediverseAttach), br(), + input({ type: "file", id: "telegram_media", name: "media", accept: "image/*,video/*,audio/*,application/pdf" }), + br(), br(), + button({ type: "submit", class: "filter-btn" }, i18n.telegramSend) + ) + )); + return template(i18n.fediverse, header, section(...body)); +}; + + +const fmtDuration = (sec) => { + const n = Math.max(0, Math.floor(Number(sec) || 0)); + const h = Math.floor(n / 3600), m = Math.floor((n % 3600) / 60), sInt = n % 60; + const mm = String(m).padStart(h ? 2 : 1, "0"), ss = String(sInt).padStart(2, "0"); + return h ? `${h}:${mm}:${ss}` : `${mm}:${ss}`; +}; + +const fmtBytes = (n) => { + const b = Number(n) || 0; + if (b >= 1024 ** 3) return `${(b / 1024 ** 3).toFixed(1)} GB`; + if (b >= 1024 ** 2) return `${(b / 1024 ** 2).toFixed(1)} MB`; + if (b >= 1024) return `${(b / 1024).toFixed(0)} KB`; + return `${b} B`; +}; + +const peertubeBox = (account, stats, actions, showLabel) => { + const stat = (lbl, val) => div({ class: "fediverse-stat" }, span({ class: "fediverse-stat-label" }, lbl), strong(String(val))); + return div({ class: "fediverse-network" }, + showLabel ? h3("PeerTube") : "", + div({ class: "fediverse-profile" }, + div({ class: "fediverse-namerow" }, + account.avatar ? img({ class: "fediverse-avatar", src: account.avatar, alt: account.displayName }) : "", + div({ class: "fediverse-profile-id" }, + div({ class: "fediverse-name" }, account.displayName || account.handle), + a({ class: "fediverse-acct", href: account.channelUrl || account.profileUrl, target: "_blank", rel: "noopener noreferrer" }, account.handle) + ), + actions && actions.length ? div({ class: "fediverse-compact-actions" }, actions) : "" + ), + stats + ? div({ class: "fediverse-stats" }, + stat(i18n.peertubeVideos, stats.videos), + stat(i18n.peertubeSubscriptions, stats.subscriptions), + stat(i18n.peertubeFollowers, stats.followers), + stats.quota > 0 ? stat(i18n.peertubeQuota, `${fmtBytes(stats.quotaUsed)} / ${fmtBytes(stats.quota)}`) : "" + ) + : "" + ) + ); +}; + +const peertubeHeader = (account, stats, actions) => section( + div({ class: "tags-header module-header-line" }, h2(i18n.fediverse), p(i18n.fediverseDescription)), + peertubeBox(account, stats, actions, false) +); + +const renderPeertubeVideo = (v) => { + const href = `/fediverse/peertube/video/${encodeURIComponent(v.id)}`; + const ch = v.channel || {}; + const chips = []; + if (v.privacyId && v.privacyId !== 1 && v.privacy) chips.push(span({ class: "card-label" }, v.privacy)); + if (v.state && !/published/i.test(v.state)) chips.push(span({ class: "card-label" }, v.state)); + return div({ class: "feed-card fediverse-card peertube-card" }, + a({ href, class: "peertube-thumb" }, + v.thumbnail ? img({ src: v.thumbnail, alt: v.name, loading: "lazy" }) : div({ class: "peertube-thumb-empty" }), + v.duration ? span({ class: "peertube-duration" }, fmtDuration(v.duration)) : "" + ), + div({ class: "peertube-card-body" }, + a({ href, class: "peertube-title" }, v.name || v.id), + div({ class: "peertube-meta" }, + ch.avatar ? img({ class: "peertube-channel-avatar", src: ch.avatar, alt: "" }) : "", + span({ class: "fediverse-acct" }, ch.displayName || ch.handle || ""), + span({ class: "fediverse-date" }, fmtDate(v.publishedAt)), + span(`${v.views} ${i18n.peertubeViews} · ${v.likes} ${i18n.peertubeLikes}`) + ), + chips.length ? div({ class: "card-chips-row" }, ...chips) : "" + ) + ); +}; + +exports.peertubeFeedView = ({ account, stats, videos, kind, error, notice } = {}) => { + if (!account) return exports.fediverseOverviewView({ account: null }); + const current = kind === "mine" ? "mine" : "subscriptions"; + const body = []; + if (error) body.push(div({ class: "fediverse-error" }, i18n[error] || i18n.peertubeErrConnect)); + if (notice) body.push(div({ class: "feed-success-msg" }, p(i18n[notice] || String(notice)))); + body.push(div({ class: "mode-buttons-row" }, + form({ method: "GET", action: "/fediverse/peertube" }, + button({ type: "submit", name: "filter", value: "subscriptions", class: current === "subscriptions" ? "filter-btn active" : "filter-btn" }, String(i18n.peertubeSubscriptions).toUpperCase()), + button({ type: "submit", name: "filter", value: "mine", class: current === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.peertubeMyVideos).toUpperCase()) + ) + )); + const list = Array.isArray(videos) ? videos : []; + if (!list.length && !error) body.push(p({ class: "muted" }, i18n.peertubeNoVideos)); + else body.push(section({ class: "peertube-grid" }, list.map(renderPeertubeVideo))); + return template(i18n.fediverse, peertubeHeader(account, stats, [ + a({ href: "/fediverse", class: "filter-btn" }, `← ${i18n.fediverse}`), + a({ href: `/fediverse/peertube?filter=${current}&refresh=1`, class: "filter-btn" }, i18n.fediverseRefresh), + a({ href: "/fediverse/peertube/upload", class: "filter-btn" }, i18n.peertubeUpload) + ]), section(...body)); +}; + +const renderPeertubeComment = (c) => { + const acc = c.account || {}; + return div({ class: "feed-card fediverse-card" }, + div({ class: "fediverse-head" }, + acc.avatar ? img({ class: "fediverse-avatar", src: acc.avatar, alt: acc.displayName }) : "", + div({ class: "fediverse-author" }, + span({ class: "fediverse-name" }, acc.displayName || acc.name), + span({ class: "fediverse-acct" }, acc.name ? `@${acc.name}${acc.host ? "@" + acc.host : ""}` : "") + ), + span({ class: "fediverse-date" }, fmtDate(c.createdAt)) + ), + div({ class: "feed-text", [RAW_HTML]: sanitizeHtml(c.html) }) + ); +}; + +exports.peertubeVideoView = ({ account, stats, video, comments, myRating, error } = {}) => { + if (!account) return exports.fediverseOverviewView({ account: null }); + const header = peertubeHeader(account, stats, [a({ href: "/fediverse/peertube", class: "filter-btn" }, `← ${i18n.peertubeVideos}`)]); + if (!video) return template(i18n.fediverse, header, section(div({ class: "fediverse-error" }, i18n[error] || i18n.peertubeErrFetch))); + const id = encodeURIComponent(video.id); + const ch = video.channel || {}; + const liked = myRating === "like"; + const body = []; + if (error) body.push(div({ class: "fediverse-error" }, i18n[error] || i18n.peertubeErrConnect)); + body.push(div({ class: "feed-card fediverse-card peertube-watch" }, + video.stream + ? videoHyperaxe({ class: "peertube-player", src: video.stream, controls: true, preload: "metadata", poster: video.thumbnail || undefined }) + : div({ class: "peertube-nostream" }, video.thumbnail ? img({ src: video.thumbnail, alt: video.name, class: "post-image" }) : "", p(i18n.peertubeNoStream)), + h3({ class: "peertube-title" }, video.name), + div({ class: "peertube-meta" }, + ch.avatar ? img({ class: "peertube-channel-avatar", src: ch.avatar, alt: "" }) : "", + ch.url ? a({ href: ch.url, target: "_blank", rel: "noopener noreferrer", class: "fediverse-acct" }, ch.displayName || ch.handle) : span({ class: "fediverse-acct" }, ch.displayName || ""), + span({ class: "fediverse-date" }, fmtDate(video.publishedAt)), + span(`${video.views} ${i18n.peertubeViews}`), + video.duration ? span(fmtDuration(video.duration)) : "" + ), + div({ class: "fediverse-actions" }, + form({ method: "POST", action: `/fediverse/peertube/video/${id}/rate` }, + input({ type: "hidden", name: "rating", value: liked ? "none" : "like" }), + button({ type: "submit", class: liked ? "filter-btn active" : "filter-btn" }, `${liked ? "★" : "☆"} ${video.likes}`) + ), + a({ href: video.url, target: "_blank", rel: "noopener noreferrer", class: "filter-btn" }, i18n.peertubeOpen) + ), + video.description ? div({ class: "feed-text peertube-description" }, video.description) : "", + Array.isArray(video.tags) && video.tags.length ? div({ class: "card-chips-row" }, video.tags.map(t => span({ class: "tag-link" }, `#${t}`))) : "" + )); + body.push(h4(`${i18n.peertubeComments} · ${Array.isArray(comments) ? comments.length : 0}`)); + const list = Array.isArray(comments) ? comments : []; + if (!list.length) body.push(p({ class: "muted" }, i18n.peertubeNoComments)); + else body.push(section({ class: "feed-container" }, list.map(renderPeertubeComment))); + body.push(div({ class: "publish-form" }, + form({ method: "POST", action: `/fediverse/peertube/video/${id}/comment` }, + textarea({ maxlength: "4000", name: "text", rows: "4", class: "publish-textarea", placeholder: i18n.peertubeCommentPlaceholder, required: true }), + br(), + button({ type: "submit", class: "filter-btn" }, i18n.peertubeComment) + ) + )); + return template(i18n.fediverse, header, section(...body)); +}; + +exports.peertubeUploadView = ({ account, stats, error } = {}) => { + if (!account) return exports.fediverseOverviewView({ account: null }); + const header = peertubeHeader(account, stats, [a({ href: "/fediverse/peertube", class: "filter-btn" }, `← ${i18n.peertubeVideos}`)]); + return template(i18n.fediverse, header, section( + error ? div({ class: "fediverse-error" }, i18n[error] || i18n.peertubeErrUpload) : "", + div({ class: "publish-form" }, + form({ method: "POST", action: "/fediverse/peertube/upload", enctype: "multipart/form-data" }, + p(i18n.peertubeUploadHelp), + label({ for: "peertube_video" }, i18n.fediverseAttach), br(), + input({ type: "file", id: "peertube_video", name: "video", accept: "video/*", required: true }), br(), br(), + label({ for: "peertube_name" }, i18n.peertubeTitleLabel), br(), + input({ type: "text", id: "peertube_name", name: "name", maxlength: "120", required: true }), br(), + label({ for: "peertube_description" }, i18n.peertubeDescriptionLabel), br(), + textarea({ id: "peertube_description", name: "description", rows: "5", maxlength: "10000", class: "publish-textarea" }), br(), + label({ for: "peertube_privacy" }, i18n.peertubePrivacyLabel), br(), + select({ id: "peertube_privacy", name: "privacy" }, + option({ value: "public" }, i18n.peertubePrivacyPublic), + option({ value: "unlisted" }, i18n.peertubePrivacyUnlisted), + option({ value: "private" }, i18n.peertubePrivacyPrivate) + ), br(), br(), + button({ type: "submit", class: "filter-btn" }, i18n.peertubeUpload) + ) + ) + )); +}; + +exports.fediverseOverviewView = ({ account, stats, telegram, telegramStats, peertube, peertubeStats } = {}) => { + const boxes = []; + if (account) { + boxes.push(mastodonBox(account, stats, [ + form({ method: "GET", action: "/fediverse/mastodon" }, button({ type: "submit", class: "filter-btn" }, i18n.fediverseManage)) + ], true)); + } + if (telegram) { + boxes.push(telegramBox(telegram, telegramStats, [ + form({ method: "GET", action: "/fediverse/telegram" }, button({ type: "submit", class: "filter-btn" }, i18n.fediverseManage)) + ], true)); + } + if (peertube) { + boxes.push(peertubeBox(peertube, peertubeStats, [ + form({ method: "GET", action: "/fediverse/peertube" }, button({ type: "submit", class: "filter-btn" }, i18n.fediverseManage)) + ], true)); + } return template(i18n.fediverse, section( - div({ class: "tags-header" }, h2(i18n.fediverse), p(i18n.fediverseDescription)), - account ? box : disconnectedBlock() + div({ class: "tags-header module-header-line" }, h2(i18n.fediverse), p(i18n.fediverseDescription)), + boxes.length ? boxes : disconnectedBlock() )); }; @@ -136,7 +444,7 @@ exports.fediverseView = ({ account, posts, error, stats } = {}) => { if (!account) return exports.fediverseOverviewView({ account: null }); const body = []; - body.push(div({ class: "tags-header" }, h2(i18n.fediverse), p(i18n.fediverseDescription))); + body.push(div({ class: "tags-header module-header-line" }, h2(i18n.fediverse), p(i18n.fediverseDescription))); body.push(mastodonBox(account, stats, [ a({ href: "/fediverse", class: "filter-btn" }, `← ${i18n.fediverse}`), a({ href: "/fediverse/mastodon?refresh=1", class: "filter-btn" }, i18n.fediverseRefresh), @@ -156,7 +464,7 @@ exports.fediverseView = ({ account, posts, error, stats } = {}) => { exports.fediverseThreadView = ({ account, stats, thread, error } = {}) => { const header = section( - div({ class: "tags-header" }, h2(i18n.fediverse), p(i18n.fediverseDescription)), + div({ class: "tags-header module-header-line" }, h2(i18n.fediverse), p(i18n.fediverseDescription)), account ? mastodonBox(account, stats, [ a({ href: "/fediverse/mastodon", class: "filter-btn" }, `← ${i18n.fediverse}`) ], true) : "" @@ -198,7 +506,7 @@ exports.fediversePreviewView = ({ account, stats, text, media, error, replyToId, return template(i18n.fediverse, section( - div({ class: "tags-header" }, h2(i18n.fediverse), p(i18n.fediverseDescription)), + div({ class: "tags-header module-header-line" }, h2(i18n.fediverse), p(i18n.fediverseDescription)), account ? mastodonBox(account, stats, [ a({ href: backHref, class: "filter-btn" }, `← ${i18n.fediverse}`) ], true) : "" @@ -209,7 +517,7 @@ exports.fediversePreviewView = ({ account, stats, text, media, error, replyToId, div({ class: "publish-form" }, form( { action: postAction, method: "POST", enctype: "multipart/form-data" }, - textarea({ name: "text", id: "text", rows: "6", class: "publish-textarea", placeholder: isReply ? i18n.fediverseReplyPlaceholder : i18n.fediverseComposePlaceholder }, text || ""), + textarea({ maxlength: "5000", name: "text", id: "text", rows: "6", class: "publish-textarea", placeholder: isReply ? i18n.fediverseReplyPlaceholder : i18n.fediverseComposePlaceholder }, text || ""), ...mediaList.map(m => input({ type: "hidden", name: "tmp", value: m.name })), br(), label({ for: "fediverse_media" }, i18n.fediverseAttach), diff --git a/src/views/feed_view.js b/src/views/feed_view.js index 8e1cf97..dc561ce 100644 --- a/src/views/feed_view.js +++ b/src/views/feed_view.js @@ -1,10 +1,11 @@ -const { div, h2, p, section, button, form, a, span, textarea, br, input, h1, label } = require("../server/node_modules/hyperaxe"); -const { template, i18n, renderOpinionsVoting, userLink, renderContentActions } = require("./main_views"); +const { div, h2, p, section, button, form, a, span, textarea, br, input, h1, label, img } = require("../server/node_modules/hyperaxe"); +const { renderCommentsSection: renderSharedCommentsSection, renderCommentsLink } = require("./comments_view"); +const { clearnetItemHref, template, i18n, renderOpinionsVoting, userLink, renderContentActions, renderEngagement, renderModuleStats, moduleIsEmpty, paged } = require("./main_views"); +const { renderReachChip, renderClearnetSelector, renderClearnetSwitch, renderTribeOriginChip } = require("./clearnet_view"); const { config } = require("../server/SSB_server.js"); -const { renderTextWithStyles } = require("../backend/renderTextWithStyles"); +const { renderStyledHtml, renderStyledText } = require("../backend/renderStyledText"); const moment = require("../server/node_modules/moment"); const { sanitizeHtml } = require('../backend/sanitizeHtml'); -const { renderUrl } = require("../backend/renderUrl"); const FEED_TEXT_MIN = Number(config?.feed?.minLength ?? 1); const FEED_TEXT_MAX = Number(config?.feed?.maxLength ?? 280); @@ -22,7 +23,7 @@ const normalizeOptions = (opts) => { const formatDate = (feed) => { const ts = feed?.value?.timestamp || Date.parse(feed?.value?.content?.createdAt || "") || 0; - return ts ? new Date(ts).toLocaleString() : ""; + return ts ? moment(ts).format("YYYY/MM/DD HH:mm") : ""; }; const extractTags = (text) => { @@ -30,19 +31,6 @@ const extractTags = (text) => { return Array.from(new Set(list)); }; -const rewriteHashtagLinks = (html) => { - return String(html || '').replace( - /href=(["'])\/hashtag\/([^"'?#\s<]+)\1/gi, - (m, q, rawTag) => { - let t = String(rawTag || ''); - try { t = decodeURIComponent(t); } catch {} - t = t.replace(/[^A-Za-z0-9_]/g, ''); - const tag = t.toLowerCase(); - const query = encodeURIComponent(`#${tag}`); - return `href=${q}/search?query=${query}${q}`; - } - ); -}; const generateFilterButtons = (filters, currentFilter, action, extra = {}) => { const cur = String(currentFilter || "").toUpperCase(); @@ -56,21 +44,11 @@ const generateFilterButtons = (filters, currentFilter, action, extra = {}) => { { method: "GET", action }, input({ type: "hidden", name: "filter", value: mode }), ...hiddenInputs(extra), - button({ type: "submit", class: cur === mode ? "filter-btn active" : "filter-btn" }, i18n[mode + "Button"] || mode) + button({ type: "submit", class: cur === mode ? "filter-btn active" : "filter-btn" }, String(i18n[mode + "Button"] || mode).toUpperCase()) ) ); }; -const renderVotesSummary = (opinions = {}) => { - const entries = Object.entries(opinions).filter(([, v]) => Number(v) > 0); - if (!entries.length) return null; - entries.sort((a, b) => Number(b[1]) - Number(a[1]) || String(a[0]).localeCompare(String(b[0]))); - return div( - { class: "votes" }, - entries.map(([category, count]) => span({ class: "vote-category" }, `${category}: ${count}`)) - ); -}; - const renderCardField = (labelText, value) => div( { class: "card-field" }, @@ -79,68 +57,59 @@ const renderCardField = (labelText, value) => ); const renderFeedCommentsSection = (feedKey, comments = []) => { - const list = Array.isArray(comments) ? comments : []; - const commentsCount = list.length; + return renderSharedCommentsSection({ + action: `/feed/${encodeURIComponent(feedKey)}/comments`, + comments: comments, + returnTo: null, + open: true + }); +}; - return div( - { class: "vote-comments-section" }, - div( - { class: "comments-count" }, - span({ class: "card-label" }, i18n.voteCommentsLabel + ": "), - span({ class: "card-value" }, String(commentsCount)) - ), - div( - { class: "comment-form-wrapper" }, - h2({ class: "comment-form-title" }, i18n.voteNewCommentLabel || i18n.feedPostComment || "Post a comment"), - form( - { method: "POST", action: `/feed/${encodeURIComponent(feedKey)}/comments`, class: "comment-form", enctype: "multipart/form-data" }, - textarea({ - id: "comment-text", - name: "text", - rows: 4, - class: "comment-textarea", - placeholder: i18n.voteNewCommentPlaceholder || "" - }), - div({ class: "comment-file-upload" }, label(i18n.uploadMedia || "Upload media"), input({ type: "file", name: "blob" })), - br(), - button({ type: "submit", class: "comment-submit-btn" }, i18n.voteNewCommentButton || i18n.feedPostComment || "Send") + +const FEED_MEDIA_RE = /^\n?!?\[[^\]\n]{0,200}\]\(&[A-Za-z0-9+/=]{44}\.sha256\)$/; +const renderMastodonToggle = (on = false) => + div({ class: "clearnet-choice" }, + div({ class: "clearnet-choice-options" }, + label({ class: "clearnet-choice-option" }, input({ type: "checkbox", name: "mastodon", value: "1", ...(on ? { checked: true } : {}) }), + span({ class: "pm-exposition-chip pm-exposition-whole" }, span({ class: "pm-exposition-icon" }, "⌬"), span({ class: "pm-exposition-text" }, "Mastodon")) ) - ), - list.length - ? div( - { class: "comments-list" }, - list.map((c) => { - const author = c?.value?.author || ""; - const ts = c?.value?.timestamp || c?.timestamp; - const absDate = ts ? moment(ts).format("YYYY/MM/DD HH:mm:ss") : ""; - const relDate = ts ? moment(ts).fromNow() : ""; + ) + ); - const content = c?.value?.content || {}; - const text = content.text || c?.value?.text || ""; - const threadRoot = content.fork || content.root || null; - - return div( - { class: "votations-comment-card" }, - span( - { class: "created-at" }, - span(i18n.createdBy), - author ? userLink(author) : span("(unknown)"), - absDate ? span(" | ") : "", - absDate ? span({ class: "votations-comment-date" }, absDate) : "", - relDate ? span({ class: "votations-comment-date" }, " | ", i18n.sendTime) : "", - relDate && threadRoot - ? a({ href: `/thread/${encodeURIComponent(threadRoot)}#${encodeURIComponent(c.key)}` }, relDate) - : "" - ), - p({ class: "votations-comment-text" }, ...renderUrl(text)) - ); - }) +const renderFeedComposer = ({ text = "", media = "", rows = 5, clearnet = false, mastodon = false, mastodonOn = false } = {}) => { + const cleanMedia = FEED_MEDIA_RE.test(String(media || "")) ? String(media).trim() : ""; + const draft = String(text || ""); + const hasPreview = !!(draft.trim() || cleanMedia); + const previewNodes = hasPreview ? renderStyledText(cleanMedia ? `${draft.trim()}\n${cleanMedia}` : draft.trim(), { zoomImages: true }) : []; + return form( + { method: "POST", action: "/feed/create", enctype: "multipart/form-data" }, + textarea({ + name: "text", + required: true, + minlength: String(FEED_TEXT_MIN), + maxlength: String(FEED_TEXT_MAX), + rows: String(rows), + cols: 50, + placeholder: i18n.feedPlaceholder + }, draft), + cleanMedia ? input({ type: "hidden", name: "media", value: cleanMedia }) : null, + div({ class: "comment-file-upload" }, label(i18n.uploadMedia), input({ type: "file", name: "blob" })), + hasPreview + ? div({ class: "feed-preview" }, + h2({ class: "feed-preview-title" }, i18n.messagePreview || "Preview"), + div({ class: "trending-card feed-card" }, div({ class: "feed-text" }, ...previewNodes)) ) - : p({ class: "votations-no-comments" }, i18n.voteNoCommentsYet || i18n.noComments || "") + : null, + renderClearnetSelector(clearnet, i18n), + mastodon ? renderMastodonToggle(mastodonOn) : null, + div({ class: "feed-compose-actions" }, + button({ type: "submit", class: "filter-btn", formaction: "/feed/preview" }, i18n.preview || "Preview"), + button({ type: "submit", class: "create-button" }, i18n.createFeedButton || "Send Feed!") + ) ); }; -const renderFeedCard = (feed) => { +const renderFeedCard = exports.renderFeedCard = (feed, spreadMap = null, opts = {}) => { const content = feed.value.content || {}; const rawText = typeof content.text === "string" ? content.text : ""; const safeText = rawText.trim(); @@ -153,87 +122,146 @@ const renderFeedCard = (feed) => { const alreadyRefeeded = Array.isArray(content.refeeds_inhabitants) && me ? content.refeeds_inhabitants.includes(me) : false; - const authorId = feed.value.author || content.author || ""; + const authorId = content.author || feed.value.author || ""; + const signerId = feed.value.author || ""; const refeedsNum = Number(content.refeeds || 0) || 0; const commentCount = Number(content.commentCount || 0); - const styledHtml = rewriteHashtagLinks(renderTextWithStyles(safeText)); + const styledNodes = renderStyledText(safeText, { zoomImages: true, ...(typeof opts.hashtagHref === 'function' ? { hashtagHref: opts.hashtagHref } : {}) }); + const refeedAction = opts.refeedAction || `/feed/refeed/${encodeURIComponent(feed.key)}`; + const canRefeed = opts.canRefeed !== false && !(authorId && String(authorId) === String(me)); + const headerActions = opts.headerActions !== undefined + ? opts.headerActions + : feed.tribeOrigin + ? renderContentActions(null, feed.tribeOrigin.href) + : renderContentActions(feed.key, `/feed/${encodeURIComponent(feed.key)}`, { + author: authorId, + spread: (spreadMap && spreadMap.get(feed.key)) || null, + reportTitle: safeText, + ...(((signerId && String(signerId) === String(me)) || (authorId && String(authorId) === String(me))) ? { deleteAction: `/feed/delete/${encodeURIComponent(feed.key)}` } : {}) + }); + const chipsRow = opts.chips !== undefined + ? opts.chips + : feed.tribeOrigin ? div({ class: "card-chips-row" }, renderTribeOriginChip(feed.tribeOrigin)) : feed.clearnet === true ? div({ class: "card-chips-row" }, renderReachChip(true, i18n, clearnetItemHref("feed", "", feed.key))) : null; + const engagement = opts.engagement !== undefined + ? opts.engagement + : feed.tribeOrigin ? null : renderEngagement(feed.key, + renderOpinionsVoting('/feed/opinions', feed.key, content.opinions, null, content.opinions_inhabitants), + renderCommentsLink({ href: `/feed/${encodeURIComponent(feed.key)}`, count: commentCount }) + ); + + if (process.env.OASIS_MOBILE === '1') { + return div( + { class: "trending-card feed-card" + (authorId && String(authorId) === String(me) ? " own-content" : "") }, + div({ class: "card-header activity-card-header" }, span(), headerActions), + div( + { class: "card-section feed-card-body" }, + div({ class: "feed-text" }, ...styledNodes), + chipsRow, + feed.tribeOrigin ? null : div( + { class: "refeed-column" }, + h1(String(refeedsNum)), + canRefeed + ? form( + { method: "POST", action: refeedAction }, + button({ class: alreadyRefeeded ? "refeed-btn active" : "refeed-btn", type: "submit", title: i18n.refeedButton, "aria-label": i18n.refeedButton, ...(alreadyRefeeded ? { disabled: true } : {}) }, "\u25b2") + ) + : null, + alreadyRefeeded ? p({ class: "muted" }, i18n.alreadyRefeeded) : null + ), + engagement, + p( + { class: "card-footer" }, + span({ class: "date-link" }, `${createdAt}`), + userLink(authorId), + content._textEdited ? span({ class: "edited-badge" }, ` \u00b7 ${i18n.edited || "edited"}`) : null + ) + ) + ); + } return div( { class: "trending-card feed-card" + (authorId && String(authorId) === String(me) ? " own-content" : "") }, div( { class: "card-header activity-card-header" }, span(), - renderContentActions(feed.key, `/feed/${encodeURIComponent(feed.key)}`) + headerActions ), div( { class: "card-section feed-card-body" }, div( { class: "feed-row" }, - div( + feed.tribeOrigin ? null : div( { class: "refeed-column" }, h1(String(refeedsNum)), - form( - { method: "POST", action: `/feed/refeed/${encodeURIComponent(feed.key)}` }, - button({ class: alreadyRefeeded ? "refeed-btn active" : "refeed-btn", type: "submit", ...(alreadyRefeeded ? { disabled: true } : {}) }, i18n.refeedButton) - ), - alreadyRefeeded ? p({ class: "muted" }, i18n.alreadyRefeeded) : null + canRefeed + ? form( + { method: "POST", action: refeedAction }, + button({ class: alreadyRefeeded ? "refeed-btn active" : "refeed-btn", type: "submit", title: i18n.refeedButton, "aria-label": i18n.refeedButton, ...(alreadyRefeeded ? { disabled: true } : {}) }, "▲") + ) + : null, ), div( { class: "feed-main" }, - div({ class: "feed-text", innerHTML: sanitizeHtml(styledHtml) }), - h2( - `${i18n.totalOpinions}: ${totalCount}`, - ...(() => { - const entries = voteEntries.filter(([, v]) => Number(v) > 0); - if (!entries.length) return []; - const maxVal = Math.max(...entries.map(([, v]) => Number(v))); - const dominant = entries.filter(([, v]) => Number(v) === maxVal).map(([k]) => i18n['vote' + k.charAt(0).toUpperCase() + k.slice(1)] || k); - return [ - span({ style: 'margin:0 8px;opacity:0.5;' }, '|'), - span({ style: 'font-weight:700;' }, `${i18n.moreVoted || 'More Voted'}: ${dominant.join(' + ')}`) - ]; - })() - ), + div({ class: "feed-text" }, ...styledNodes), + chipsRow, p( { class: "card-footer" }, - span({ class: "date-link" }, `${createdAt} ${i18n.performed} `), + span({ class: "date-link" }, `${createdAt}`), userLink(authorId), content._textEdited ? span({ class: "edited-badge" }, ` · ${i18n.edited || "edited"}`) : null ) ) ), - div( - { class: "card-comments-summary" }, - span({ class: "card-label" }, `${i18n.voteCommentsLabel || "Comments"}:`), - span({ class: "card-value" }, String(commentCount)), - br(), - br(), - form( - { method: "GET", action: `/feed/${encodeURIComponent(feed.key)}` }, - button({ type: "submit", class: "filter-btn" }, i18n.voteCommentsForumButton || i18n.feedOpenDiscussion || "Open Discussion") - ) - ), - div( - { class: "card-comments-summary feed-opinions-section" }, - div( - { class: "comments-count" }, - span({ class: "card-label" }, (i18n.opinionsTitle || "Opinions") + ": "), - span({ class: "card-value" }, String(totalCount)) - ), - renderOpinionsVoting('/feed/opinions', feed.key, content.opinions, null, content.opinions_inhabitants) - ) + engagement ) ); }; +const renderFeedSideTags = (trendingTags) => + div({ class: "feed-side feed-side-right" }, + div({ class: "feed-side-box" }, + h2({ class: "feed-side-title" }, i18n.feedTrendingTitle || "Trending Tags"), + trendingTags.length + ? div({ class: "feed-side-tags" }, + trendingTags.map(t => a({ href: `/feed?tag=${encodeURIComponent(t.name || t)}`, class: "tag-link" }, `#${t.name || t}`))) + : p({ class: "muted" }, "—") + ) + ); + +const renderFeedSideUsers = (activeUsers) => + div({ class: "feed-side feed-side-left" }, + div({ class: "feed-side-box" }, + h2({ class: "feed-side-title" }, i18n.feedActiveTitle || "Active Inhabitants"), + activeUsers.length + ? div({ class: "feed-side-users" }, + activeUsers.map(u => div({ class: "feed-side-user" }, + img({ src: u.avatarUrl || "/assets/images/default-avatar.png", class: "feed-side-avatar" }), + userLink(u.id) + ))) + : p({ class: "muted" }, "—") + ) + ); + +const feedChipFor = (filter, censusF) => (mode) => { + if (mode === filter) return true; + if (!Array.isArray(censusF)) return true; + if (mode === "MINE") return censusF.some((f) => String(f && f.value && f.value.author) === String(config.keys.id)); + if (mode === "RECENT") return censusF.length > 0; + return true; +}; + exports.feedView = (feeds, opts = "ALL") => { const { filter, q, tag, msg } = normalizeOptions(opts); + const workspace = !!(opts && typeof opts === "object" && opts.workspace) && require("../configs/config-manager.js").getConfig().ux?.current === "feed"; + const spreadMap = (opts && typeof opts === "object" && opts.spreadMap instanceof Map) ? opts.spreadMap : null; + const trendingTags = (opts && typeof opts === "object" && Array.isArray(opts.trendingTags)) ? opts.trendingTags : []; + const activeUsers = (opts && typeof opts === "object" && Array.isArray(opts.activeUsers)) ? opts.activeUsers : []; const title = filter === "MINE" ? i18n.MINEButton - : filter === "TODAY" - ? i18n.TODAYButton + : filter === "RECENT" + ? i18n.RECENTButton : filter === "TOP" ? i18n.TOPButton : filter === "CREATE" @@ -244,55 +272,67 @@ exports.feedView = (feeds, opts = "ALL") => { ? `${i18n.searchTitle || "Search"}: “${q}”` : i18n.feedTitle; - const header = div({ class: "tags-header" }, h2(title), p(i18n.FeedshareYourOpinions)); + const header = div({ class: "tags-header module-header-line" }, h2(title), p(i18n.FeedshareYourOpinions)); const successBanner = msg === 'feedPublished' ? div({ class: 'feed-success-msg' }, p('✓ ' + (i18n.feedPublishedSuccess || 'Feed published successfully!'))) : null; const extra = { q, tag }; + const emptyMod = moduleIsEmpty(feeds, filter, "ALL", q || tag); + const censusF = Array.isArray(opts.censusList) ? opts.censusList : feeds; + const feedChip = feedChipFor(filter, censusF); - return template( - title, - section( - header, - successBanner, - div( - { class: "mode-buttons-row" }, - ...generateFilterButtons(["ALL", "MINE", "TODAY", "TOP"], filter, "/feed", extra), - form({ method: "GET", action: "/feed/create" }, button({ type: "submit", class: "create-button filter-btn" }, i18n.createFeedTitle || "Create Feed")) - ), - div( - { class: "feed-tools-row" }, - form( - { method: "GET", action: "/feed", class: "feed-search-form" }, - input({ type: "hidden", name: "filter", value: filter }), - tag ? input({ type: "hidden", name: "tag", value: tag }) : null, - input({ type: "text", name: "q", value: q, placeholder: i18n.searchPlaceholder || "Search", class: "feed-search-input" }), - button({ type: "submit", class: "filter-btn feed-search-btn" }, i18n.searchButton || "Search") + const centerContent = section( + header, + successBanner, + div( + { class: "mode-buttons-row" }, + ...(emptyMod ? [] : generateFilterButtons(["ALL", "RECENT", "MINE", "TOP"].filter(feedChip), filter, "/feed", extra)), + form({ method: "GET", action: "/feed/create" }, button({ type: "submit", class: "create-button filter-btn" }, i18n.createFeedTitle || "Create Feed")) + ), + emptyMod ? null : div( + { class: "feed-tools-row activity-filter-chips activity-toolbar-row" }, + renderModuleStats(feeds.length), + form( + { method: "GET", action: "/feed", class: "filter-box" }, + input({ type: "hidden", name: "filter", value: filter }), + tag ? input({ type: "hidden", name: "tag", value: tag }) : null, + input({ type: "text", name: "q", value: q, placeholder: i18n.feedSearchPlaceholder || i18n.searchPlaceholder || "Search", class: "filter-box__input" }), + div({ class: "filter-box__controls" }, + button({ type: "submit", class: "filter-box__button" }, i18n.searchButton || "Search") ) - ), - section( - filter === "CREATE" - ? form( - { method: "POST", action: "/feed/create" }, - textarea({ - name: "text", - placeholder: i18n.feedPlaceholder, - required: true, - minlength: String(FEED_TEXT_MIN), - maxlength: String(FEED_TEXT_MAX), - rows: 4, - cols: 50 - }), - br(), - button({ type: "submit", class: "create-button" }, i18n.createFeedButton) - ) - : feeds && feeds.length > 0 - ? div({ class: "feed-container" }, feeds.map((feed) => renderFeedCard(feed)).filter(Boolean)) - : div({ class: "no-results" }, p(i18n.noFeedsFound)) ) + ), + (process.env.OASIS_MOBILE === '1' && workspace) ? div({ class: "feed-sides-row" }, renderFeedSideUsers(activeUsers), renderFeedSideTags(trendingTags)) : null, + section( + filter === "CREATE" + ? renderFeedComposer({ rows: 4, mastodon: opts.mastodon === true }) + : feeds && feeds.length > 0 + ? div({ class: "feed-container" }, paged(feeds.filter((feed) => String((feed.value.content || {}).text || "").trim())).map((feed) => renderFeedCard(feed, spreadMap)).filter(Boolean)) + : div({ class: "no-results" }, p(i18n.noFeedsFound)) ) ); + + if (workspace) { + if (process.env.OASIS_MOBILE === '1') { + return template( + title, + div({ class: "feed-workspace" }, + div({ class: "feed-workspace-center" }, centerContent) + ) + ); + } + return template( + title, + div({ class: "feed-workspace" }, + renderFeedSideUsers(activeUsers), + div({ class: "feed-workspace-center" }, centerContent), + renderFeedSideTags(trendingTags) + ) + ); + } + + return template(title, centerContent); }; exports.feedCreateView = (opts = {}) => { @@ -301,94 +341,91 @@ exports.feedCreateView = (opts = {}) => { return template( i18n.createFeedTitle, section( - div({ class: "tags-header" }, h2(i18n.createFeedTitle), p(i18n.FeedshareYourOpinions)), - div({ class: "mode-buttons-row" }, ...generateFilterButtons(["ALL", "MINE", "TODAY", "TOP"], "CREATE", "/feed", { q, tag })), - form( - { method: "POST", action: "/feed/create" }, - textarea({ - name: "text", - required: true, - minlength: String(FEED_TEXT_MIN), - maxlength: String(FEED_TEXT_MAX), - rows: 5, - cols: 50, - placeholder: i18n.feedPlaceholder - }), - br(), - button({ type: "submit", class: "create-button" }, i18n.createFeedButton || "Send Feed!") - ) + div({ class: "tags-header module-header-line" }, h2(i18n.createFeedTitle), p(i18n.FeedshareYourOpinions)), + div({ class: "mode-buttons-row" }, ...generateFilterButtons(["ALL"], "CREATE", "/feed", { q, tag })), + renderFeedComposer({ text: opts.text, media: opts.media, clearnet: opts.clearnet === true, mastodon: opts.mastodon === true, mastodonOn: opts.mastodonOn === true }) ) ); }; -exports.singleFeedView = (feed, comments = []) => { +exports.singleFeedView = (feed, comments = [], params = {}) => { + const feedChip = feedChipFor("ALL", Array.isArray(params.censusList) ? params.censusList : null); const content = feed.value?.content || {}; const rawText = typeof content.text === "string" ? content.text : ""; const safeText = rawText.trim(); - const authorId = feed.value?.author || content.author || ""; + const authorId = content.author || feed.value?.author || ""; + const signerId = feed.value?.author || ""; const createdAt = formatDate(feed); - const styledHtml = rewriteHashtagLinks(renderTextWithStyles(safeText)); + const styledNodes = renderStyledText(safeText, { zoomImages: true }); const me = config?.keys?.id; const alreadyRefeeded = Array.isArray(content.refeeds_inhabitants) && me ? content.refeeds_inhabitants.includes(me) : false; const refeedsNum = Number(content.refeeds || 0) || 0; const tags = extractTags(safeText); + const isClearnet = !!feed.clearnet; return template( i18n.feedDetailTitle || "Feed", + section(div({ class: "tags-header module-header-line" }, h2(i18n.feedTitle), p(i18n.FeedshareYourOpinions))), section( div( { class: "filters" }, form( { method: "GET", action: "/feed", class: "ui-toolbar ui-toolbar--filters" }, button({ type: "submit", name: "filter", value: "ALL", class: "filter-btn" }, i18n.ALLButton || "ALL"), - button({ type: "submit", name: "filter", value: "MINE", class: "filter-btn" }, i18n.MINEButton || "MINE"), - button({ type: "submit", name: "filter", value: "TODAY", class: "filter-btn" }, i18n.TODAYButton || "TODAY"), + ...(feedChip("RECENT") ? [button({ type: "submit", name: "filter", value: "RECENT", class: "filter-btn" }, i18n.RECENTButton || "RECENT")] : []), + ...(feedChip("MINE") ? [button({ type: "submit", name: "filter", value: "MINE", class: "filter-btn" }, i18n.MINEButton || "MINE")] : []), button({ type: "submit", name: "filter", value: "TOP", class: "filter-btn" }, i18n.TOPButton || "TOP"), form({ method: "GET", action: "/feed/create" }, button({ type: "submit", class: "create-button" }, i18n.createFeedTitle || "Create Feed")) ) ), div( { class: "bookmark-item card feed-detail-card" }, + div({ class: "card-header activity-card-header" }, + span(), + renderContentActions(feed.key, `/feed/${encodeURIComponent(feed.key)}`, { + spread: params.spreads || null, + author: authorId, + reportTitle: safeText, + ...(((signerId && String(signerId) === String(me)) || (authorId && String(authorId) === String(me))) ? { deleteAction: `/feed/delete/${encodeURIComponent(feed.key)}` } : {}) + }) + ), br, div( { class: "feed-row" }, div( { class: "refeed-column" }, h1(String(refeedsNum)), - form( - { method: "POST", action: `/feed/refeed/${encodeURIComponent(feed.key)}` }, - button({ class: alreadyRefeeded ? "refeed-btn active" : "refeed-btn", type: "submit", ...(alreadyRefeeded ? { disabled: true } : {}) }, i18n.refeedButton) - ), - alreadyRefeeded ? p({ class: "muted" }, i18n.alreadyRefeeded) : null + (authorId && String(authorId) === String(me)) + ? null + : form( + { method: "POST", action: `/feed/refeed/${encodeURIComponent(feed.key)}` }, + button({ class: alreadyRefeeded ? "refeed-btn active" : "refeed-btn", type: "submit", title: i18n.refeedButton, "aria-label": i18n.refeedButton, ...(alreadyRefeeded ? { disabled: true } : {}) }, "▲") + ), ), div( { class: "feed-main" }, - div({ class: "feed-text", innerHTML: sanitizeHtml(styledHtml) }), + div({ class: "feed-text" }, ...styledNodes), tags.length ? div( { class: "card-tags" }, tags.map((tag) => a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link" }, `#${tag}`)) ) : null, + div({ class: "card-chips-row" }, renderReachChip(isClearnet, i18n, isClearnet ? clearnetItemHref("feed", "", feed.key) : null)), + authorId && String(authorId) === String(me) ? renderClearnetSwitch("feed", feed.rootId || feed.key, isClearnet) : null, br, p( { class: "card-footer" }, - span({ class: "date-link" }, `${createdAt} ${i18n.performed} `), + span({ class: "date-link" }, `${createdAt}`), userLink(authorId), content._textEdited ? span({ class: "edited-badge" }, ` · ${i18n.edited || "edited"}`) : null ) ) ) ), - renderFeedCommentsSection(feed.key, comments), - div( - { class: "card-comments-summary feed-opinions-section" }, - div( - { class: "comments-count" }, - span({ class: "card-label" }, (i18n.opinionsTitle || "Opinions") + ": "), - span({ class: "card-value" }, String(Object.values(content.opinions || {}).reduce((s, n) => s + (Number(n) || 0), 0))) - ), - renderOpinionsVoting('/feed/opinions', feed.key, content.opinions, null, content.opinions_inhabitants) + renderEngagement(feed.key, + renderOpinionsVoting('/feed/opinions', feed.key, content.opinions, null, content.opinions_inhabitants), + renderFeedCommentsSection(feed.key, comments) ) ) ); diff --git a/src/views/files_view.js b/src/views/files_view.js new file mode 100644 index 0000000..6fc9773 --- /dev/null +++ b/src/views/files_view.js @@ -0,0 +1,421 @@ +const { form, button, div, h2, h3, p, section, input, br, a, span, textarea, select, label, option, table, tr, th, td, progress, strong } = require("../server/node_modules/hyperaxe"); +const { renderCommentsSection: renderSharedCommentsSection } = require("./comments_view"); +const { renderReachChip, renderClearnetSelector, renderClearnetSwitch } = require('./clearnet_view'); + +const { clearnetItemHref, template, i18n, renderOpinionsVoting, renderEngagement, userLink, renderSpreadButton, renderEcoTax, renderLifespanChip , renderSpreadEditWarning, renderContentActions, renderModuleStats, moduleIsEmpty, contentDeleteAction, paged } = require("./main_views"); +const moment = require("../server/node_modules/moment"); +const { config } = require("../server/SSB_server.js"); +const { renderStyledText, safeExternalHref } = require("../backend/renderStyledText"); + +const userId = config.keys.id; + +const safeArr = (v) => (Array.isArray(v) ? v : []); +const safeText = (v) => String(v || "").trim(); + +const buildReturnTo = (filter, params = {}) => { + const f = safeText(filter || "all"); + const q = safeText(params.q || ""); + const sort = safeText(params.sort || "recent"); + const parts = [`filter=${encodeURIComponent(f)}`]; + if (q) parts.push(`q=${encodeURIComponent(q)}`); + if (sort) parts.push(`sort=${encodeURIComponent(sort)}`); + return `/files?${parts.join("&")}`; +}; + +const renderTags = (tags) => { + const list = safeArr(tags).map((t) => String(t || "").trim()).filter(Boolean); + return list.length + ? div( + { class: "card-tags" }, + list.map((tag) => a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link" }, `#${tag}`)) + ) + : null; +}; + +const renderFileOwnerActions = (filter, fileObj, params = {}) => { + const returnTo = buildReturnTo(filter, params); + const isAuthor = String(fileObj.author) === String(userId); + const hasOpinions = Object.keys(fileObj.opinions || {}).length > 0; + + if (!isAuthor || hasOpinions) return []; + + return [ + form( + { method: "GET", action: `/files/edit/${encodeURIComponent(fileObj.key)}` }, + input({ type: "hidden", name: "returnTo", value: returnTo }), + button({ class: "update-btn", type: "submit" }, i18n.fileUpdateButton) + ) + ]; +}; + +const renderFileCommentsSection = (fileId, comments = [], returnTo = null) => { + return renderSharedCommentsSection({ + action: `/files/${encodeURIComponent(fileId)}/comments`, + comments: comments, + returnTo: returnTo + }); +}; + +const blobSha256Hex = (blobId) => { + const m = /^&([A-Za-z0-9+/=]+)\.sha256$/.exec(String(blobId || "")); + if (!m) return ""; + try { return Buffer.from(m[1], "base64").toString("hex"); } catch (_) { return ""; } +}; + +const formatSize = (bytes) => { + const n = Number(bytes) || 0; + if (n === 0) return "—"; + if (n < 1024) return n + " B"; + if (n < 1024 * 1024) return (n / 1024).toFixed(1) + " KB"; + return (n / (1024 * 1024)).toFixed(1) + " MB"; +}; + +const spreadInfoOf = (t, spreadMap) => (spreadMap instanceof Map && spreadMap.get(t.key)) || null; +const renderFileSeeds = (t, spreadMap) => { + const info = spreadInfoOf(t, spreadMap); + return String(info && typeof info.count === "number" ? info.count : (info && Array.isArray(info.voters) ? info.voters.length : 0)); +}; +const renderFileSpread = (t, spreadMap) => String(t.author) === String(userId) ? "—" : renderSpreadButton(t.key, spreadInfoOf(t, spreadMap), i18n.seedAction, false); + +const renderFileTable = exports.renderFileTable = (files, filter, params = {}) => { + const returnTo = buildReturnTo(filter, params); + + if (!files.length) return p(params.q ? i18n.fileNoMatch : i18n.noFiles); + + return table( + { border: "1", class: "file-table" }, + tr( + th(i18n.createdAt || "DATE"), + th(i18n.fileTitleLabel || "TITLE"), + th(i18n.fileSizeLabel || "SIZE"), + th({ class: "torrent-spread-cell" }, i18n.seedsLabel), + th({ class: "torrent-spread-cell" }, i18n.seedAction), + th("") + ), + files.map((t) => + tr( + td({ class: "row-chips" }, t.clearnet === true ? renderReachChip(true, i18n, clearnetItemHref('files', t.title, t.key)) : null), + td({ "data-label": i18n.createdAt || "DATE" }, moment(t.createdAt).format("YYYY/MM/DD HH:mm")), + td({ class: "row-title", "data-label": i18n.fileTitleLabel || "TITLE" }, t.title || ""), + td({ "data-label": i18n.fileSizeLabel || "SIZE" }, formatSize(t.size)), + td({ class: "torrent-spread-cell", "data-label": i18n.seedsLabel }, renderFileSeeds(t, params.spreadMap)), + td({ class: "torrent-spread-cell", "data-label": i18n.seedAction }, renderFileSpread(t, params.spreadMap)), + td( + form( + { method: "GET", action: `/files/${encodeURIComponent(t.key)}` }, + input({ type: "hidden", name: "returnTo", value: returnTo }), + input({ type: "hidden", name: "filter", value: filter || "all" }), + params.q ? input({ type: "hidden", name: "q", value: params.q }) : null, + params.sort ? input({ type: "hidden", name: "sort", value: params.sort }) : null, + button({ type: "submit", class: "filter-btn" }, i18n.fileDetailsButton) + ) + ) + ) + ) + ); +}; + +const isExternalUrl = (u) => /^https?:\/\//i.test(String(u || "")); + +const renderNextcloudForm = (returnTo) => div( + { class: "div-center audio-form" }, + h2("Nextcloud"), + form( + { action: "/files/nextcloud", method: "POST", enctype: "multipart/form-data" }, + input({ type: "hidden", name: "returnTo", value: returnTo }), + span(i18n.nextcloudFileLabel), + br(), + input({ type: "file", name: "file", required: true }), + br(), + br(), + span(i18n.fileTitleLabel), + br(), + input({ type: "text", name: "title", maxlength: "100", placeholder: i18n.fileTitlePlaceholder, required: true }), + br(), + span(i18n.fileDescriptionLabel), + br(), + textarea({ maxlength: "5000", name: "description", placeholder: i18n.fileDescriptionPlaceholder, rows: "4" }), + br(), + span(i18n.fileTagsLabel), + br(), + input({ type: "text", name: "tags", placeholder: i18n.fileTagsPlaceholder }), + br(), + br(), + renderClearnetSelector(false, i18n), br(), + button({ type: "submit" }, i18n.nextcloudUploadButton) + ) +); + +const renderFileForm = (filter, fileId, fileToEdit, params = {}) => { + const returnTo = safeText(params.returnTo) || buildReturnTo("all", params); + const tribeId = safeText(params.tribeId || ""); + const clearnetAllowed = !tribeId && !fileToEdit?.tribeId && !fileToEdit?.encrypted && !fileToEdit?.cipher; + return div( + { class: "div-center audio-form" }, + params.spreadWarning || null, + form( + { + action: filter === "edit" ? `/files/update/${encodeURIComponent(fileId)}` : "/files/create", + method: "POST", + enctype: "multipart/form-data" + }, + input({ type: "hidden", name: "returnTo", value: returnTo }), + tribeId ? input({ type: "hidden", name: "tribeId", value: tribeId }) : null, + span(i18n.fileFileLabel), + br(), + input({ type: "file", name: "file", required: filter !== "edit" }), + br(), + br(), + span(i18n.fileTitleLabel), + br(), + input({ type: "text", name: "title", maxlength: "100", placeholder: i18n.fileTitlePlaceholder, value: fileToEdit?.title || "", required: true }), + br(), + span(i18n.fileDescriptionLabel), + br(), + textarea({ maxlength: "5000", name: "description", placeholder: i18n.fileDescriptionPlaceholder, rows: "4" }, fileToEdit?.description || ""), + br(), + span(i18n.fileTagsLabel), + br(), + input({ + type: "text", + name: "tags", + placeholder: i18n.fileTagsPlaceholder, + value: safeArr(fileToEdit?.tags).join(", ") + }), + br(), + br(), + ...(clearnetAllowed ? [renderClearnetSelector(filter === "edit" ? !!fileToEdit?.clearnet : false, i18n), br()] : []), + button({ type: "submit" }, filter === "edit" ? i18n.fileUpdateButton : i18n.fileCreateButton) + ) + ); +}; + +const mediaChipFor = (filter, censusM) => (mode) => { + if (mode === filter) return true; + if (!Array.isArray(censusM)) return true; + if (mode === "top") return censusM.length > 0; + if (mode === "mine") return censusM.some((x) => String(x.author) === String(userId)); + if (mode === "recent") return censusM.length > 0; + if (mode === "favorites") return censusM.some((x) => x.isFavorite); + if (mode === "bcs") return censusM.some((x) => String(x.title || "").toUpperCase().startsWith("BCS-")); + return true; +}; + +exports.filesView = async (files, filter = "all", fileId = null, params = {}) => { + if (filter === "edit") params = { ...params, spreadWarning: await renderSpreadEditWarning(fileId) }; + const title = i18n.filesTitle; + + const q = safeText(params.q || ""); + const sort = safeText(params.sort || "recent"); + + const list = safeArr(files); + const emptyMod = moduleIsEmpty(list, filter, "all", q); + const mediaChip = mediaChipFor(filter, Array.isArray(params.censusList) ? params.censusList : list); + const fileToEdit = fileId ? list.find((t) => t.key === fileId) : null; + + return template( + title, + section( + div({ class: "tags-header module-header-line" }, + h2(title), + p(i18n.filesDescription) + ), + div( + { class: "filters" }, + form( + { method: "GET", action: "/files", class: "ui-toolbar ui-toolbar--filters" }, + input({ type: "hidden", name: "sort", value: sort }), + ...(emptyMod ? [] : [ + button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.fileFilterAll).toUpperCase()), + ...(mediaChip("recent") ? [button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.fileFilterRecent).toUpperCase())] : []), + ...(mediaChip("mine") ? [button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.fileFilterMine).toUpperCase())] : []), + ...(mediaChip("favorites") ? [button( + { type: "submit", name: "filter", value: "favorites", class: filter === "favorites" ? "filter-btn active" : "filter-btn" }, + String(i18n.fileFilterFavorites).toUpperCase() + )] : []), + + ...(mediaChip("top") ? [button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, String(i18n.fileFilterTop).toUpperCase())] : []), + ]), + button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.fileCreateButton) + ) + ) + ), + section( + filter === "create" || filter === "edit" + ? [renderFileForm(filter, fileId, fileToEdit, { ...params, filter }), filter !== "edit" && params.nextcloud && !params.tribeId ? renderNextcloudForm(safeText(params.returnTo) || buildReturnTo("all", params)) : null] + : section( + emptyMod ? null : div( + { class: "audios-search activity-filter-chips activity-toolbar-row" }, + renderModuleStats(list.length), + form( + { method: "GET", action: "/files", class: "filter-box" }, + input({ type: "hidden", name: "filter", value: filter }), + input({ + type: "text", + name: "q", + value: q, + placeholder: i18n.fileSearchPlaceholder, + class: "filter-box__input" + }), + div( + { class: "filter-box__controls" }, + select( + { name: "sort", class: "filter-box__select" }, + option({ value: "recent", ...(sort === "recent" ? { selected: true } : {})}, i18n.fileSortRecent), + option({ value: "oldest", ...(sort === "oldest" ? { selected: true } : {})}, i18n.fileSortOldest), + option({ value: "top", ...(sort === "top" ? { selected: true } : {})}, i18n.fileSortTop) + ), + button({ type: "submit", class: "filter-box__button" }, i18n.fileSearchButton) + ) + ) + ), + div({ class: "audios-list" }, renderFileTable(paged(list), filter, { q, sort, spreadMap: params.spreadMap })) + ) + ) + ); +}; + +exports.singleFileView = async (fileObj, filter = "all", comments = [], params = {}) => { + const mediaChip = mediaChipFor(filter, Array.isArray(params.censusList) ? params.censusList : null); + const q = safeText(params.q || ""); + const sort = safeText(params.sort || "recent"); + const returnTo = safeText(params.returnTo) || buildReturnTo(filter, { q, sort }); + + const title = safeText(fileObj.title); + const { renderEncryptedChip, renderTransportChip } = require('./clearnet_view'); + const isAuthor = String(fileObj.author) === String(userId); + const clearnetAllowed = !fileObj.tribeId && !fileObj.encrypted && !fileObj.cipher; + const isClearnet = !!fileObj.clearnet; + + const chips = [ + renderLifespanChip(fileObj.lifetime, i18n), + fileObj.sizeBytes ? renderEcoTax(fileObj.sizeBytes, fileObj.key) : null + ].filter(Boolean); + + const ownerActions = renderFileOwnerActions(filter, fileObj, { q, sort }); + const sideActions = []; + for (const a of ownerActions) sideActions.push(a); + + const tagsNode = renderTags(fileObj.tags); + + const detailActions = div({ class: "card-header activity-card-header" }, + renderContentActions(fileObj.key, `/files/${encodeURIComponent(fileObj.key)}`, { + spreadTitle: i18n.seedAction, + author: fileObj.author, + deleteAction: String(fileObj.author) === String(userId) ? contentDeleteAction("file", fileObj.key) : null, + favKind: 'files', + ...(fileObj.url && fileObj.url.startsWith("&") ? { torrentFrom: { blobId: fileObj.url, name: fileObj.fileName || fileObj.title } } : {}), + isFavorite: fileObj.isFavorite, + spread: params.spreads || null, + returnTo, + reportTitle: fileObj.title + }) + ); + + const fileSide = div({ class: "tribe-side" }, + div({ class: "shop-title-row" }, + title ? h2({ class: "tribe-card-title" }, title) : null, + fileObj.tribeId && fileObj.cipher ? renderEncryptedChip(i18n) : renderTransportChip(i18n), + clearnetAllowed ? renderReachChip(isClearnet, i18n, isClearnet ? clearnetItemHref('files', fileObj.title, fileObj.key) : null) : null, + clearnetAllowed && isAuthor ? renderClearnetSwitch('files', fileObj.rootId || fileObj.key, isClearnet) : null + ), + chips.length ? div({ class: "card-chips-row" }, ...chips) : null, + safeText(fileObj.description) + ? p({ class: "tribe-side-description" }, ...renderStyledText(fileObj.description)) + : null, + tagsNode, + sideActions.length ? div({ class: "tribe-side-actions" }, ...sideActions) : null + ); + + const fileMain = div({ class: "tribe-main" }, + detailActions, + fileObj.url && fileObj.url.startsWith("&") + ? div({ class: "torrent-download" }, + div({ class: "torrent-oasis torrent-detail-section" }, + h3({ class: "torrent-section-title" }, i18n.torrentOriginalSection || "Original"), + table({ class: "tribe-info-table torrent-file-info" }, + tr(td({ class: "tribe-info-label" }, i18n.fileShareFileLabel), td({ class: "tribe-info-value" }, fileObj.fileName || fileObj.title || "download")), + tr(td({ class: "tribe-info-label" }, i18n.fileSizeLabel), td({ class: "tribe-info-value" }, formatSize(fileObj.size))), + fileObj.mime ? tr(td({ class: "tribe-info-label" }, i18n.fileTypeLabel), td({ class: "tribe-info-value" }, fileObj.mime)) : null, + fileObj.cipher + ? tr(td({ class: "tribe-info-label" }, i18n.encryptedChipLabel || "E2E"), td({ class: "tribe-info-value" }, "AES-256-GCM")) + : tr(td({ class: "tribe-info-label" }, "SHA-256"), td({ class: "tribe-info-value" }, span({ class: "bank-address-code" }, blobSha256Hex(fileObj.url)))) + ), + a({ href: `/files/${encodeURIComponent(fileObj.key)}/get`, class: "filter-btn" }, `\u2B07 ${i18n.torrentOasisButton || "OASIS"}`) + ), + fileObj.torrentUrl + ? div({ class: "torrent-detail-section" }, + h3({ class: "torrent-section-title" }, "Torrent"), + table({ class: "tribe-info-table torrent-file-info" }, + tr(td({ class: "tribe-info-label" }, i18n.fileShareFileLabel), td({ class: "tribe-info-value" }, `${String(fileObj.fileName || fileObj.title || "download").replace(/\.torrent$/i, "")}.torrent`)), + tr(td({ class: "tribe-info-label" }, "SHA-256"), td({ class: "tribe-info-value" }, span({ class: "bank-address-code" }, blobSha256Hex(fileObj.torrentUrl)))) + ), + a({ href: `/blob/${encodeURIComponent(fileObj.torrentUrl)}?name=${encodeURIComponent(`${String(fileObj.fileName || fileObj.title || "download").replace(/\.torrent$/i, "")}.torrent`)}`, class: "filter-btn" }, `\u2B07 ${i18n.torrentExternalButton || "EXTERNAL"}`) + ) + : null + ) + : isExternalUrl(fileObj.url) + ? div({ class: "torrent-download" }, + div({ class: "torrent-oasis torrent-detail-section" }, + h3({ class: "torrent-section-title" }, i18n.fileExternalLink), + table({ class: "tribe-info-table torrent-file-info" }, + tr(td({ class: "tribe-info-label" }, i18n.fileShareFileLabel), td({ class: "tribe-info-value" }, fileObj.fileName || fileObj.title || "download")), + tr(td({ class: "tribe-info-label" }, i18n.fileSizeLabel), td({ class: "tribe-info-value" }, formatSize(fileObj.size))), + fileObj.mime ? tr(td({ class: "tribe-info-label" }, i18n.fileTypeLabel), td({ class: "tribe-info-value" }, fileObj.mime)) : null, + tr(td({ class: "tribe-info-label" }, "URL"), td({ class: "tribe-info-value" }, a({ href: safeExternalHref(fileObj.url), target: "_blank", rel: "noopener noreferrer" }, fileObj.url))) + ), + a({ href: safeExternalHref(fileObj.url), target: "_blank", rel: "noopener noreferrer", class: "filter-btn" }, `\u2B07 ${i18n.fileExternalLink}`) + ) + ) + : p(i18n.fileNoFile), + (() => { + const createdTs = fileObj.createdAt ? new Date(fileObj.createdAt).getTime() : NaN; + const updatedTs = fileObj.updatedAt ? new Date(fileObj.updatedAt).getTime() : NaN; + const showUpdated = Number.isFinite(updatedTs) && (!Number.isFinite(createdTs) || updatedTs !== createdTs); + + return p( + { class: "card-footer" }, + span({ class: "date-link" }, `${moment(fileObj.createdAt).format("YYYY/MM/DD HH:mm")}`), + userLink(fileObj.author), + showUpdated + ? span( + { class: "votations-comment-date" }, + ` | ${i18n.fileUpdatedAt}: ${moment(fileObj.updatedAt).format("YYYY/MM/DD HH:mm")}` + ) + : null + ); + })(), + fileObj.tribeId ? null : renderEngagement(fileObj.key, + renderOpinionsVoting('/files/opinions', fileObj.key, fileObj.opinions, returnTo, fileObj.opinions_inhabitants), + renderFileCommentsSection(fileObj.key, comments, returnTo) + ) + ); + + return template( + i18n.filesTitle, + section( + div({ class: "tags-header module-header-line" }, + h2(i18n.fileAllSectionTitle || i18n.filesTitle), + p(i18n.filesDescription) + ), + div( + { class: "filters" }, + form( + { method: "GET", action: "/files", class: "ui-toolbar ui-toolbar--filters" }, + input({ type: "hidden", name: "sort", value: sort }), + button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.fileFilterAll).toUpperCase()), + ...(mediaChip("recent") ? [button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.fileFilterRecent).toUpperCase())] : []), + ...(mediaChip("mine") ? [button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.fileFilterMine).toUpperCase())] : []), + ...(mediaChip("favorites") ? [button( + { type: "submit", name: "filter", value: "favorites", class: filter === "favorites" ? "filter-btn active" : "filter-btn" }, + String(i18n.fileFilterFavorites).toUpperCase() + )] : []), + + button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, String(i18n.fileFilterTop).toUpperCase()), + button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.fileCreateButton) + ) + ), + div({ class: "tribe-details" }, fileSide, fileMain) + ) + ); +}; diff --git a/src/views/forum_view.js b/src/views/forum_view.js index 4b201a1..0f7be4b 100644 --- a/src/views/forum_view.js +++ b/src/views/forum_view.js @@ -1,14 +1,16 @@ const { - div, a, span, form, button, section, p, - input, label, br, select, option, h2, textarea + div, a, span, form, button, section, p, ul, li, + input, label, br, select, option, h2, textarea, details, summary } = require("../server/node_modules/hyperaxe"); +const RAW_HTML = Symbol.for('oasis.rawHtml'); const moment = require("../server/node_modules/moment"); -const { template, i18n, userLink, renderSpreadButton, renderPrivacyChip, renderLifespanChip } = require('./main_views'); -const { renderEncryptedChip: renderForumEncryptedChip } = require('./clearnet_view'); +const { template, i18n, userLink, renderSpreadButton, renderPrivacyChip, renderLifespanChip, renderContentActions, renderInviteQrCard, renderStateChip, renderSubscriptionBox, renderModuleStats, moduleIsEmpty, paged } = require('./main_views'); +const { renderEncryptedChip: renderForumEncryptedChip, renderTribeOriginChip } = require('./clearnet_view'); const { config } = require('../server/SSB_server.js'); -const { renderUrl } = require('../backend/renderUrl'); -const { renderTextWithStyles } = require('../backend/renderTextWithStyles'); +const { renderStyledText } = require('../backend/renderStyledText'); +const { renderStyledHtml } = require('../backend/renderStyledText'); const { sanitizeHtml } = require('../backend/sanitizeHtml'); +const { TEXT_CAP } = require('../backend/long_text'); const userId = config.keys.id; @@ -22,14 +24,15 @@ exports.renderForumInvitePage = (code) => { return template(i18n.invitesForumsTitle || 'Forums', section(pageContent)); }; -const BASE_FILTERS = ['hot','all','mine','recent','top']; -const CAT_BLOCK1 = ['GENERAL','OASIS','L.A.R.P.','POLITICS','TECH']; -const CAT_BLOCK2 = ['SCIENCE','MUSIC','ART','GAMING','BOOKS','FILMS']; +const BASE_FILTERS = ['hot','all','recent','mine','top']; +const CAT_BLOCK1 = ['GENERAL','OASIS','L.A.R.P.']; +const CAT_BLOCK2 = ['POLITICS','TECH','SCIENCE','MUSIC','ART','GAMING','BOOKS','FILMS']; const CAT_BLOCK3 = ['PHILOSOPHY','SOCIETY','PRIVACY','CYBERWARFARE','SURVIVALISM']; const ALL_CATS = [...CAT_BLOCK1, ...CAT_BLOCK2, ...CAT_BLOCK3]; const catKey = (c) => 'forumCat' + String(c || '').replace(/\./g,'').replace(/[\s-]/g,'').toUpperCase(); const catLabel = (c) => i18n[catKey(c)] || c; +const tribeCatLabel = (c) => i18n['tribeForumCat' + String(c || '').charAt(0) + String(c || '').slice(1).toLowerCase()] || c; const Z = 1.96; function wilsonScore(pos, neg) { @@ -43,9 +46,9 @@ function getFilteredForums(filter, forums) { const now = Date.now(); if (filter === 'mine') return forums.filter(f => f.author === userId); if (filter === 'recent') return forums.filter(f => new Date(f.createdAt).getTime() >= now - 86400000); - if (filter === 'top') return forums.slice().sort((a,b) => b.score - a.score); + if (filter === 'top') return forums.filter(f => !f.tribeOrigin).sort((a,b) => b.score - a.score); if (filter === 'hot') return forums - .filter(f => new Date(f.createdAt).getTime() >= now - 86400000) + .filter(f => !f.tribeOrigin && new Date(f.createdAt).getTime() >= now - 86400000) .sort((a,b) => b.score - a.score); if (ALL_CATS.includes(filter)) return forums.filter(f => f.category === filter); @@ -84,61 +87,75 @@ const renderVotes = (target, score, forumId) => ) ); -const renderForumForm = () => +const renderForumForm = (draft = null) => div({ class: 'forum-form' }, - form({ action: '/forum/create', method: 'POST' }, + form({ action: '/forum/create', method: 'POST', enctype: 'multipart/form-data' }, label(i18n.forumCategoryLabel), br(), select({ name: 'category', required: true }, - ALL_CATS.map(cat => option({ value: cat }, catLabel(cat))) + ALL_CATS.map(cat => option({ value: cat, ...(draft && draft.category === cat ? { selected: true } : {}) }, catLabel(cat))) ), br(), br(), label(i18n.eventPrivacyLabel), br(), select({ name: 'isPublic', id: 'isPublic' }, - option({ value: 'public', selected: true }, i18n.eventPublic), - option({ value: 'private' }, i18n.eventPrivate) + option({ value: 'public', ...(draft && draft.isPrivate ? {} : { selected: true }) }, i18n.eventPublic), + option({ value: 'private', ...(draft && draft.isPrivate ? { selected: true } : {}) }, i18n.eventPrivate) ), br(), br(), label(i18n.forumTitleLabel), br(), input({ type: 'text', - name: 'title', + name: 'title', maxlength: '100', required: true, - placeholder: i18n.forumTitlePlaceholder + placeholder: i18n.forumTitlePlaceholder, + value: draft ? String(draft.title || '') : '' }), br(), br(), label(i18n.forumMessageLabel), br(), - textarea({ + textarea({ maxlength: String(TEXT_CAP), name: 'text', required: true, - rows: 4, + rows: 8, placeholder: i18n.forumMessagePlaceholder - }), br(), br(), + }, draft ? String(draft.text || '') : ''), br(), br(), + label(i18n.uploadMedia), br(), + input({ type: 'file', name: 'blob' }), br(), br(), button({ type: 'submit' }, i18n.forumCreateButton) ) ); -const renderThread = (nodes, level = 0, forumId) => { +const bySupport = (a, b) => + wilsonScore(b.positiveVotes, b.negativeVotes) - wilsonScore(a.positiveVotes, a.negativeVotes); + +const bestReplyKey = (nodes) => { + const flat = []; + const walk = (list) => { for (const m of Array.isArray(list) ? list : []) { if (m && m.text && String(m.text).trim()) flat.push(m); walk(m && m.children); } }; + walk(nodes); + if (!flat.length) return null; + const sorted = flat.slice().sort(bySupport); + const top = sorted[0]; + if (wilsonScore(top.positiveVotes, top.negativeVotes) > 0) return top.key; + const first = (Array.isArray(nodes) ? nodes : []).filter(m => m && m.text && String(m.text).trim()).sort(bySupport)[0]; + return first ? first.key : null; +}; + +const renderThread = (nodes, level = 0, forumId, bestKey = null) => { if (!Array.isArray(nodes)) return []; return [...nodes] .filter(m => (m && m.text && String(m.text).trim()) || (m && Array.isArray(m.children) && m.children.length)) - .sort((a, b) => - wilsonScore(b.positiveVotes, b.negativeVotes) - - wilsonScore(a.positiveVotes, a.negativeVotes) - ) - .flatMap((m, i) => { - const isTopLevelWinner = level === 0 && i === 0; + .sort(bySupport) + .flatMap((m) => { const classList = [ 'forum-comment', `level-${level}`, - isTopLevelWinner ? 'highlighted-reply' : '' + bestKey && m.key === bestKey ? 'highlighted-reply' : '' ].filter(Boolean).join(' '); const commentBox = div( - { class: classList }, + { class: classList, id: `msg-${String(m.key || '').replace(/[^a-zA-Z0-9]/g, '')}` }, div({ class: 'comment-header' }, span({ class: 'date-link' }, - `${moment(m.timestamp).format('YYYY/MM/DD HH:mm:ss')} ${i18n.performed}`), + `${moment(m.timestamp).format('YYYY/MM/DD HH:mm')}`), userLink(m.author), div({ class: 'comment-votes' }, span({ class: 'votes-count' }, `▲: ${m.positiveVotes || 0}`), - span({ class: 'votes-count', style: 'margin-left:12px;' }, + span({ class: 'votes-count votes-count-negative' }, `▼: ${m.negativeVotes || 0}`) ) ), @@ -151,128 +168,129 @@ const renderThread = (nodes, level = 0, forumId) => { ...(m.text || '').split('\n') .map(l => l.trim()) .filter(l => l) - .map(l => p(...renderUrl(l))) + .map(l => p(...renderStyledText(l, { zoomImages: true }))) ) ) ), - div({ class: 'new-reply' }, + details({ class: 'new-reply forum-inline-reply', ...(draftFor(m.key) ? { open: true } : {}) }, + summary({ class: 'forum-inline-reply-toggle' }, i18n.tribeForumReply), form({ method: 'POST', action: `/forum/${encodeURIComponent(forumId)}/message`, - class: 'comment-form' + class: 'comment-form', + enctype: 'multipart/form-data' }, input({ type: 'hidden', name: 'parentId', value: m.key }), - textarea({ + textarea({ maxlength: String(TEXT_CAP), name: 'message', - rows: 2, + rows: 4, required: true, placeholder: i18n.forumMessagePlaceholder, class: 'comment-textarea' - }), - button({ type: 'submit', class: 'forum-send-btn' }, 'Reply') + }, draftFor(m.key)), + div({ class: 'comment-file-upload' }, input({ type: 'file', name: 'blob' })), + button({ type: 'submit', class: 'forum-send-btn' }, i18n.tribeForumReply) ) ) ); - return [ commentBox, ...renderThread(m.children || [], level + 1, forumId) ]; + return [ commentBox, ...renderThread(m.children || [], level + 1, forumId, bestKey) ]; }); }; +const forumHref = (f) => `/forum/${encodeURIComponent(f.key)}`; + +const forumCounts = (f) => span({ class: 'card-label activity-update-counts mailing-counts' }, + `👥: ${f.participants?.length || 1} · 💬 ${Math.max(0, (f.messagesCount || 1) - 1)} · ▲ ${f.positiveVotes || 0} · ▼ ${f.negativeVotes || 0}`); + const renderForumList = (forums, currentFilter, spreadMap = new Map()) => { const visibleForums = (Array.isArray(forums) ? forums : []).filter(f => (f && f.title && String(f.title).trim()) || (f && f.text && String(f.text).trim()) ) - return div({ class: 'forum-list' }, - visibleForums.length - ? visibleForums.map(f => - div({ class: 'forum-card' }, - div({ class: 'forum-score-col' }, - renderVotes(f.key, f.score, f.key) - ), - div({ class: 'forum-main-col' }, - div({ class: 'forum-header-row' }, - a({ - class: 'forum-category', - href: `/forum?filter=${encodeURIComponent(f.category)}` - }, `[${catLabel(f.category)}]`), - a({ - class: 'forum-title', - href: `/forum/${encodeURIComponent(f.key)}` - }, f.title), - f.isPrivate ? renderPrivacyChip(true, i18n) : null, - f.isPrivate ? renderForumEncryptedChip(i18n) : null, - renderLifespanChip(f.lifetime, i18n) - ), - div({ - class: 'forum-body', - innerHTML: sanitizeHtml(renderTextWithStyles(f.text || '')) - }), - div({ class: 'forum-meta' }, - span({ class: 'forum-positive-votes' }, - `▲: ${f.positiveVotes || 0}`), - span({ class: 'forum-negative-votes', style: 'margin-left:12px;' }, - `▼: ${f.negativeVotes || 0}`), - span({ class: 'forum-participants' }, - `${i18n.forumParticipants.toUpperCase()}: ${f.participants?.length || 1}`), - span({ class: 'forum-messages' }, - `${i18n.forumMessages.toUpperCase()}: ${(f.messagesCount || 1) - 1}`), - form({ method: 'GET', action: `/forum/${encodeURIComponent(f.key)}`, class: 'visit-forum-form' }, - button({ type: 'submit', class: 'filter-btn' }, i18n.forumVisitButton) - ) - ), - (() => { - const btn = renderSpreadButton(f.key, spreadMap.get(f.key)); - return btn ? div({ class: 'card-spread-left' }, btn) : null; - })(), - div({ class: 'forum-footer' }, - span({ class: 'date-link' }, - `${moment(f.createdAt).format('YYYY/MM/DD HH:mm:ss')} ${i18n.performed}`), - userLink(f.author) - ), - currentFilter === 'mine' && f.author === userId - ? div({ class: 'forum-owner-actions' }, - form({ - method: 'POST', - action: `/forum/delete/${encodeURIComponent(f.key)}`, - class: 'forum-delete-form' - }, - button({ type: 'submit', class: 'delete-btn' }, - i18n.forumDeleteButton) - ) - ) - : null + if (!visibleForums.length) return div({ class: 'tribe-grid' }, p(i18n.noForums)); + return ul({ class: 'mailing-archive' }, + ...paged(visibleForums).map(f => { + if (f.tribeOrigin) { + return li({ class: 'mailing-archive-item' }, + div({ class: 'emergency-update-head mailing-archive-head' }, + div({ class: 'mailing-archive-meta' }, + a({ href: f.tribeOrigin.href, class: 'user-link' }, f.title || '—'), + ALL_CATS.includes(f.category) + ? a({ class: 'forum-category', href: `/forum?filter=${encodeURIComponent(f.category)}` }, `[${catLabel(f.category)}]`) + : (f.category ? span({ class: 'forum-category' }, `[${tribeCatLabel(f.category)}]`) : null), + span({ class: 'card-label activity-update-counts mailing-counts' }, `👥: ${f.participants?.length || 1} · 💬 ${Math.max(0, (f.messagesCount || 1) - 1)}`), + renderLifespanChip(f.lifetime, i18n), + renderTribeOriginChip(f.tribeOrigin) + ) ) + ); + } + return li({ class: 'mailing-archive-item' }, + div({ class: 'emergency-update-head mailing-archive-head' }, + div({ class: 'mailing-archive-meta' }, + a({ href: forumHref(f), class: 'user-link' }, f.title || '—'), + a({ class: 'forum-category', href: `/forum?filter=${encodeURIComponent(f.category)}` }, `[${catLabel(f.category)}]`), + forumCounts(f), + f.isPrivate ? renderPrivacyChip(true, i18n) : null, + f.isPrivate ? renderForumEncryptedChip(i18n) : null, + renderLifespanChip(f.lifetime, i18n), + f.subscriptionIn === true + ? renderStateChip('mutuals', '✉', i18n.subscriptionOn) + : (f.subscriptionIn === false ? renderStateChip('closed', '✉', i18n.subscriptionOff) : null) + ), + renderContentActions(f.key, forumHref(f), { spread: spreadMap.get(f.key) || null, author: f.author, favKind: 'forum', isFavorite: f.isFavorite, reportTitle: f.title, deleteAction: f.author === userId ? `/forum/delete/${encodeURIComponent(f.key)}` : null }) ) - ) - : p(i18n.noForums) + ); + }) ); } exports.forumView = async (forums, currentFilter, params = {}) => { const CAT_I18N_MAP_UP = ALL_CATS.reduce((m,c)=>{ m[c]=(catLabel(c)||c).toUpperCase(); return m; },{}); + const emptyModForum = moduleIsEmpty(getFilteredForums(currentFilter || 'all', forums), currentFilter || 'all', 'all', params.q); + const censusForums = Array.isArray(params.censusList) ? params.censusList : (Array.isArray(forums) ? forums : []); + const presentCats = new Set(censusForums.map(f => f && f.category).filter(Boolean)); + const baseChipVisible = (mode) => { + if (mode === currentFilter || mode === 'all') return true; + if (mode === 'mine') return censusForums.some(f => String(f.author) === String(userId)); + if (mode === 'recent') return censusForums.length > 0; + return censusForums.length > 0; + }; return template(i18n.forumTitle, section( - div({ class: 'tags-header' }, - h2(currentFilter === 'create' - ? i18n.forumCreateSectionTitle - : i18n.forumTitle), + div({ class: 'tags-header module-header-line' }, + h2(i18n.forumTitle), p(i18n.forumDescription) ), - div({ class: 'mode-buttons-cols' }, - generateFilterButtons(BASE_FILTERS, currentFilter, '/forum', { + div({ class: 'mode-buttons-row' }, + ...(emptyModForum ? [] : [ + generateFilterButtons(BASE_FILTERS.filter(baseChipVisible), currentFilter, '/forum', { hot: i18n.forumFilterHot, all: i18n.forumFilterAll, mine: i18n.forumFilterMine, recent: i18n.forumFilterRecent, top: i18n.forumFilterTop }), - generateFilterButtons(CAT_BLOCK1, currentFilter, '/forum', CAT_I18N_MAP_UP), - generateFilterButtons(CAT_BLOCK2, currentFilter, '/forum', CAT_I18N_MAP_UP), - generateFilterButtons(CAT_BLOCK3, currentFilter, '/forum', CAT_I18N_MAP_UP), + generateFilterButtons(CAT_BLOCK1.filter(c => presentCats.has(c) || c === currentFilter), currentFilter, '/forum', CAT_I18N_MAP_UP), + generateFilterButtons(CAT_BLOCK2.filter(c => presentCats.has(c) || c === currentFilter), currentFilter, '/forum', CAT_I18N_MAP_UP), + generateFilterButtons(CAT_BLOCK3.filter(c => presentCats.has(c) || c === currentFilter), currentFilter, '/forum', CAT_I18N_MAP_UP) + ]), renderCreateForumButton() ), currentFilter === 'create' - ? renderForumForm() + ? null + : emptyModForum ? null : div({ class: 'filters activity-filter-chips activity-toolbar-row' }, + renderModuleStats(getFilteredForums(currentFilter || 'all', forums).length), + form({ method: 'GET', action: '/forum', class: 'filter-box' }, + input({ type: 'hidden', name: 'filter', value: currentFilter || 'all' }), + input({ type: 'text', name: 'q', value: params.q || '', placeholder: i18n.forumSearchPlaceholder, class: 'filter-box__input' }), + div({ class: 'filter-box__controls' }, + button({ type: 'submit', class: 'filter-box__button' }, i18n.searchButton) + ) + ) + ), + currentFilter === 'create' + ? renderForumForm(params.draft || null) : renderForumList( getFilteredForums(currentFilter || 'all', forums), currentFilter, @@ -282,15 +300,22 @@ exports.forumView = async (forums, currentFilter, params = {}) => { ); }; -exports.singleForumView = async (forum, messagesData, currentFilter) => { +let replyDraft = null; +const draftFor = (parentId) => replyDraft && String(replyDraft.parentId || '') === String(parentId || '') ? String(replyDraft.message || '') : ''; + +exports.singleForumView = async (forum, messagesData, currentFilter, replyId = null, params = {}) => { + replyDraft = params.draft || null; const CAT_I18N_MAP_UP = ALL_CATS.reduce((m,c)=>{ m[c]=(catLabel(c)||c).toUpperCase(); return m; },{}); + const isForumOwner = String(forum.author) === String(userId); + const sharesForum = isForumOwner || (forum.participants || []).includes(userId); + const forumSubIn = isForumOwner || (forum.subscription && forum.subscription.subscribed === true); return template(forum.title, section( - div({ class: 'tags-header' }, + div({ class: 'tags-header module-header-line' }, h2(i18n.forumTitle), p(i18n.forumDescription) ), - div({ class: 'mode-buttons-cols' }, + div({ class: 'mode-buttons-row' }, generateFilterButtons(BASE_FILTERS, currentFilter, '/forum', { hot: i18n.forumFilterHot, all: i18n.forumFilterAll, @@ -305,22 +330,23 @@ exports.singleForumView = async (forum, messagesData, currentFilter) => { ) ), div({ class: 'forum-thread-container' }, - div({ - class: 'forum-card forum-thread-header', - style: 'display:flex;align-items:flex-start;' - }, - div({ - class: 'root-vote-col', - style: 'width:60px;text-align:center;' - }, renderVotes( + div({ class: 'forum-card forum-thread-header' }, + div({ class: 'root-vote-col' }, renderVotes( forum.key, messagesData.totalScore, forum.key )), - div({ - class: 'forum-main-col', - style: 'flex:1;padding-left:10px;' - }, + div({ class: 'forum-main-col' }, + div({ class: 'card-header activity-card-header' }, + renderContentActions(forum.key, forumHref(forum), { + author: forum.author, + favKind: 'forum', + isFavorite: forum.isFavorite, + spread: params.spreads || null, + reportTitle: forum.title, + deleteAction: isForumOwner ? `/forum/delete/${encodeURIComponent(forum.key)}` : null + }) + ), div({ class: 'forum-header-row' }, a({ class: 'forum-category', @@ -333,6 +359,9 @@ exports.singleForumView = async (forum, messagesData, currentFilter) => { forum.isPrivate ? renderPrivacyChip(true, i18n) : null, forum.isPrivate ? renderForumEncryptedChip(i18n) : null, renderLifespanChip(forum.lifetime, i18n), + (forum.subscription && sharesForum) + ? renderStateChip(forumSubIn ? 'mutuals' : 'closed', '✉', forumSubIn ? i18n.subscriptionOn : i18n.subscriptionOff) + : null, (forum.isPrivate && forum.author === userId) ? form({ method: 'POST', action: `/forum/generate-invite/${encodeURIComponent(forum.key)}`, class: 'forum-invite-form' }, button({ type: 'submit', class: 'tribe-action-btn' }, i18n.tribeGenerateInvite)) @@ -342,73 +371,70 @@ exports.singleForumView = async (forum, messagesData, currentFilter) => { button({ type: 'submit', class: 'tribe-action-btn' }, i18n.tribeOpenInvitation)) : null, (forum.isPrivate && forum.author === userId && forum.openInviteCode) - ? span({ class: 'tribe-open-invite' }, + ? div({ class: 'tribe-open-invite' }, span({ class: 'card-label' }, i18n.tribeInviteCodeText), - span({ class: 'tribe-open-invite-code' }, forum.openInviteCode)) + span({ class: 'tribe-open-invite-code' }, forum.openInviteCode), + renderInviteQrCard({ qrDataUrl: `/qr-invite-code/forum/${encodeURIComponent(forum.openInviteCode)}` })) : null, (forum.isPrivate && forum.author === userId && forum.openInviteCode) ? form({ method: 'POST', action: `/forum/open-invite/remove/${encodeURIComponent(forum.key)}`, class: 'forum-invite-form' }, button({ type: 'submit', class: 'tribe-action-btn danger-btn' }, i18n.tribeRemoveInvitation)) : null, (forum.isPrivate && forum.author !== userId) - ? a({ class: 'tribe-action-btn', href: '/invites#invites-forums' }, i18n.tribeEnterInvite) - : null, - (forum.author === userId) - ? form({ method: 'POST', action: `/forum/delete/${encodeURIComponent(forum.key)}`, class: 'forum-delete-form' }, - button({ type: 'submit', class: 'tribe-action-btn danger-btn' }, i18n.forumDeleteButton)) + ? a({ class: 'tribe-action-btn', href: '/invites?filter=others' }, i18n.tribeEnterInvite) : null ), + (forum.subscription && sharesForum) + ? renderSubscriptionBox({ + target: forum.rootId || forum.key, + scope: 'forum', + subscribed: forum.subscription.subscribed === true, + count: forum.subscription.count, + isOwner: isForumOwner, + returnTo: `/forum/${encodeURIComponent(forum.key)}` + }) + : null, div({ class: 'forum-body', - innerHTML: sanitizeHtml(renderTextWithStyles(forum.text || '')) + [RAW_HTML]: sanitizeHtml(renderStyledHtml(forum.text || '')) }), div({ class: 'forum-meta' }, - span({ class: 'votes-count' }, + span({ class: 'forum-positive-votes' }, `▲: ${messagesData.positiveVotes}`), - span({ - class: 'votes-count', - style: 'margin-left:12px;' - }, `▼: ${messagesData.negativeVotes}`), + span({ class: 'forum-negative-votes' }, + `▼: ${messagesData.negativeVotes}`), span({ class: 'forum-participants' }, `${i18n.forumParticipants.toUpperCase()}: ${forum.participants?.length || 1}`), span({ class: 'forum-messages' }, `${i18n.forumMessages.toUpperCase()}: ${messagesData.total}`) ), - (() => { - const btn = renderSpreadButton(forum.key); - return btn ? div({ class: 'card-spread-left' }, btn) : null; - })(), div({ class: 'forum-footer' }, span({ class: 'date-link' }, - `${moment(forum.createdAt).format('YYYY/MM/DD HH:mm:ss')} ${i18n.performed}`), + `${moment(forum.createdAt).format('YYYY/MM/DD HH:mm')}`), userLink(forum.author) ) ) ), - div({ - class: 'new-message-wrapper', - style: 'margin-top:12px;' - }, + div({ class: 'new-message-wrapper' }, form({ method: 'POST', action: `/forum/${encodeURIComponent(forum.key)}/message`, - class: 'new-message-form' + class: 'new-message-form', + enctype: 'multipart/form-data' }, - textarea({ + textarea({ maxlength: String(TEXT_CAP), name: 'message', - rows: 4, + rows: 8, required: true, placeholder: i18n.forumMessagePlaceholder, - style: 'width:100%;' - }), br(), - button({ - type: 'submit', - class: 'forum-send-btn', - style: 'margin-top:4px;' - }, i18n.forumSendButton) + class: 'new-message-textarea' + }, draftFor(null)), br(), + label(i18n.uploadMedia), br(), + input({ type: 'file', name: 'blob' }), br(), + button({ type: 'submit', class: 'forum-send-btn' }, i18n.forumSendButton) ) ), - ...renderThread(messagesData.messages, 0, forum.key) + ...renderThread(messagesData.messages, 0, forum.key, bestReplyKey(messagesData.messages)) ) ); }; diff --git a/src/views/gallery_view.js b/src/views/gallery_view.js new file mode 100644 index 0000000..741661c --- /dev/null +++ b/src/views/gallery_view.js @@ -0,0 +1,121 @@ +const { div, a, span, img, video, input, button, label, br } = require("../server/node_modules/hyperaxe") +const { i18n } = require("./main_views") +const { MAX_IMAGES } = require("../models/media_gallery") + +const safeArr = (v) => (Array.isArray(v) ? v : []) +const safeText = (v) => String(v == null ? "" : v).trim() +const downloadHref = (src) => `${src}${String(src).includes("?") ? "&" : "?"}download=1` + +const blobIdOf = (entry) => { + const value = safeText(entry) + const m = value.match(/\(([^)\s]+)\)/) + return m ? m[1] : value +} + +const blobUrl = (id, size) => size + ? `/image/${size}/${encodeURIComponent(id)}` + : `/blob/${encodeURIComponent(id)}` + +const isVideoEntry = (entry) => /\[video:[^\]]*\]\(/.test(String(entry || "")) + +const imagesOf = (item) => { + const list = safeArr(item && item.images).filter(Boolean) + if (list.length) return list + return item && item.image ? [item.image] : [] +} + +const videoOf = (item) => safeText(item && item.video) + +const renderMediaThumb = (entry, alt = "") => isVideoEntry(entry) + ? video({ controls: true, class: "gallery-image", src: blobUrl(blobIdOf(entry)), preload: 'metadata' }) + : img({ src: blobUrl(blobIdOf(entry), 256), class: "gallery-image", alt }) + +const lightboxId = (scope, itemId, index) => `${scope}-photo-${encodeURIComponent(itemId)}-${index}` + +let zoomSeq = 0 +const renderZoomableImage = (src, { alt = "", imgClass = "", linkClass = "" } = {}) => { + if (!src) return null + zoomSeq = (zoomSeq + 1) % 1000000 + const id = `zoom-${zoomSeq}-${String(src).replace(/[^a-zA-Z0-9]/g, "").slice(-10)}` + return [ + a({ href: `#${id}`, id: `${id}-src`, class: linkClass ? `zoom-link ${linkClass}` : "zoom-link" }, img({ src, class: imgClass, alt })), + div({ id, class: "lightbox" }, + a({ href: downloadHref(src), class: "lightbox-download", title: i18n.fileShareDownload || "Download" }, "⤓"), + a({ href: `#${id}-src`, class: "lightbox-close" }, "×"), + img({ src, class: "lightbox-image", alt }) + ) + ] +} + +const renderPhotoGallery = (item, scope = "media") => { + const list = imagesOf(item) + const clip = videoOf(item) + if (!list.length && !clip) return null + const id = (item && (item.id || item.key || item.rootId)) || "" + return div({ class: "media-gallery" }, + list.length + ? div({ class: "gallery" }, + list.map((entry, i) => + isVideoEntry(entry) + ? span({ class: "gallery-item" }, renderMediaThumb(entry)) + : a({ href: `#${lightboxId(scope, id, i)}`, id: `${lightboxId(scope, id, i)}-src`, class: "gallery-item" }, renderMediaThumb(entry)) + ) + ) + : null, + list.filter(entry => !isVideoEntry(entry)).map((entry) => + div({ id: lightboxId(scope, id, list.indexOf(entry)), class: "lightbox" }, + a({ href: downloadHref(blobUrl(blobIdOf(entry))), class: "lightbox-download", title: i18n.fileShareDownload || "Download" }, "⤓"), + a({ href: `#${lightboxId(scope, id, list.indexOf(entry))}-src`, class: "lightbox-close" }, "×"), + img({ src: blobUrl(blobIdOf(entry)), class: "lightbox-image", alt: "" }) + ) + ), + clip + ? div({ class: "media-video media-video-centered" }, + video({ controls: true, class: "media-video-player", src: blobUrl(blobIdOf(clip)), preload: 'metadata' }) + ) + : null + ) +} + +const renderGalleryFields = (item = {}, isEdit = false, maxImages = MAX_IMAGES) => { + const list = imagesOf(item) + return [ + label(i18n.galleryImages), + br(), + ...list.map(entry => input({ type: "hidden", name: "keepImages", value: entry })), + list.length < maxImages + ? div({ class: "media-file-row" }, + input({ type: "file", name: "images", accept: "image/*", multiple: true }), + button({ type: "submit", name: "action", value: "addPhoto", attrs: { formnovalidate: "formnovalidate" }, class: "filter-btn media-add-btn" }, i18n.galleryAddPhoto) + ) + : null, + br(), + list.length + ? [ + div({ class: "gallery media-form-gallery" }, + list.map((entry, i) => + span({ class: "gallery-item" }, + renderMediaThumb(entry), + button({ type: "submit", name: "removePhoto", value: String(i), attrs: { formnovalidate: "formnovalidate" }, class: "media-remove-btn" }, i18n.galleryRemovePhoto) + ) + ) + ), + br() + ] + : null, + br() + ] +} + +module.exports = { + MAX_IMAGES, + blobIdOf, + blobUrl, + isVideoEntry, + imagesOf, + videoOf, + renderMediaThumb, + renderPhotoGallery, + renderGalleryFields, + renderZoomableImage +} diff --git a/src/views/games_view.js b/src/views/games_view.js index 5619f82..88bbb0a 100644 --- a/src/views/games_view.js +++ b/src/views/games_view.js @@ -1,5 +1,5 @@ const { div, h2, h3, p, section, form, input, button, a, img, table, tr, td, th, span, iframe } = require("../server/node_modules/hyperaxe"); -const { template, i18n, userLink} = require('./main_views'); +const { template, i18n, userLink, renderModuleStats } = require('./main_views'); const moment = require("../server/node_modules/moment"); const getGames = () => [ @@ -7,6 +7,7 @@ const getGames = () => [ { id: 'ecoinflow', title: () => i18n.gamesTheFlowTitle, desc: () => i18n.gamesTheFlowDesc }, { id: 'neoninfiltrator', title: () => i18n.gamesNeonInfiltratorTitle, desc: () => i18n.gamesNeonInfiltratorDesc }, { id: 'audiopendulum', title: () => i18n.gamesAudioPendulumTitle, desc: () => i18n.gamesAudioPendulumDesc }, + { id: 'visualdj', title: () => i18n.gamesVisualDjTitle, desc: () => i18n.gamesVisualDjDesc }, { id: 'spaceinvaders', title: () => i18n.gamesSpaceInvadersTitle, desc: () => i18n.gamesSpaceInvadersDesc }, { id: 'arkanoid', title: () => i18n.gamesArkanoidTitle, desc: () => i18n.gamesArkanoidDesc }, { id: 'pingpong', title: () => i18n.gamesPingPongTitle, desc: () => i18n.gamesPingPongDesc }, @@ -18,16 +19,16 @@ const getGames = () => [ { id: 'artillery', title: () => i18n.gamesArtilleryTitle, desc: () => i18n.gamesArtilleryDesc }, { id: 'labyrinth', title: () => i18n.gamesLabyrinthTitle, desc: () => i18n.gamesLabyrinthDesc }, { id: 'cocoman', title: () => i18n.gamesCocomanTitle, desc: () => i18n.gamesCocomanDesc }, - { id: 'tetris', title: () => i18n.gamesTetrisTitle, desc: () => i18n.gamesTetrisDesc } + { id: 'tetris', title: () => i18n.gamesTetrisTitle, desc: () => i18n.gamesTetrisDesc }, + { id: 'minesweeper', title: () => i18n.gamesMinesweeperTitle, desc: () => i18n.gamesMinesweeperDesc } ]; const shortId = (feedId) => feedId ? '@' + feedId.slice(1, 9) + '...' : '?'; -const renderHallOfFame = (hall) => { - const games = getGames(); +const renderHallOfFame = (hall, games) => { const gamesWithScores = games.filter(g => hall[g.id] && hall[g.id].length > 0); if (gamesWithScores.length === 0) { - return p({ class: 'no-content' }, i18n.gamesNoScores || 'No scores yet.'); + return div({ class: 'no-content-box' }, p({ class: 'no-content' }, i18n.gamesNoScores || 'No scores yet.')); } return div({ class: 'games-scoring-list' }, gamesWithScores.map(game => @@ -55,7 +56,7 @@ const renderHallOfFame = (hall) => { ); }; -const VALID_GAME_IDS = new Set(['cocoland','ecoinflow','neoninfiltrator','audiopendulum','spaceinvaders','arkanoid','pingpong','asteroids','rockpaperscissors','tiktaktoe','flipflop','8ball','artillery','labyrinth','cocoman','tetris']); +const VALID_GAME_IDS = new Set(['cocoland','ecoinflow','neoninfiltrator','audiopendulum','visualdj','spaceinvaders','arkanoid','pingpong','asteroids','rockpaperscissors','tiktaktoe','flipflop','8ball','artillery','labyrinth','cocoman','tetris','minesweeper']); exports.gameShellView = (name) => { if (!VALID_GAME_IDS.has(name)) { @@ -75,7 +76,7 @@ exports.gameShellView = (name) => { return template( game ? game.title() : name, section( - div({ class: 'tags-header' }, + div({ class: 'tags-header module-header-line' }, h2(i18n.gamesTitle), p(i18n.gamesDescription || 'Discover and play some mini-games in your network.') ), @@ -92,26 +93,38 @@ exports.gameShellView = (name) => { ); }; -exports.gamesView = (filter = 'all', hall = null) => { - const games = getGames(); +exports.gamesView = (filter = 'all', hall = null, q = '') => { + const ql = String(q || '').trim().toLowerCase(); + const games = getGames().filter(g => !ql || `${g.title()} ${g.desc()}`.toLowerCase().includes(ql)); const filterBar = div({ class: 'filter-group' }, form({ method: 'GET', action: '/games' }, input({ type: 'hidden', name: 'filter', value: 'all' }), button({ type: 'submit', class: filter === 'all' ? 'filter-btn active' : 'filter-btn' }, - i18n.gamesFilterAll + String(i18n.gamesFilterAll).toUpperCase() ) ), form({ method: 'GET', action: '/games' }, input({ type: 'hidden', name: 'filter', value: 'scoring' }), button({ type: 'submit', class: filter === 'scoring' ? 'filter-btn active' : 'filter-btn' }, - i18n.gamesFilterScoring + String(i18n.gamesFilterScoring).toUpperCase() + ) + ) + ); + + const searchBox = div({ class: 'filters activity-filter-chips activity-toolbar-row' }, + renderModuleStats(games.length), + form({ method: 'GET', action: '/games', class: 'filter-box' }, + input({ type: 'hidden', name: 'filter', value: filter }), + input({ type: 'text', name: 'q', value: q, placeholder: i18n.gamesSearchPlaceholder, class: 'filter-box__input' }), + div({ class: 'filter-box__controls' }, + button({ type: 'submit', class: 'filter-box__button' }, i18n.searchButton) ) ) ); const content = filter === 'scoring' && hall - ? renderHallOfFame(hall) + ? renderHallOfFame(hall, games) : div({ class: 'games-single-col' }, games.map(game => { const topScore = hall && hall[game.id] && hall[game.id].length > 0 ? hall[game.id][0] : null; @@ -140,12 +153,13 @@ exports.gamesView = (filter = 'all', hall = null) => { return template( i18n.gamesTitle, section( - div({ class: 'tags-header' }, + div({ class: 'tags-header module-header-line' }, h2(i18n.gamesTitle), p(filter === 'scoring' ? i18n.gamesHallOfFame : (i18n.gamesDescription || 'Discover and play some mini-games in your network.')) ), filterBar ), + section(searchBox), section(content) ); }; diff --git a/src/views/graphos_view.js b/src/views/graphos_view.js index ea2332e..e291f6b 100644 --- a/src/views/graphos_view.js +++ b/src/views/graphos_view.js @@ -1,6 +1,7 @@ const h = require("../server/node_modules/hyperaxe"); -const { div, h2, p, section, button, form, input, span, a } = h; -const { template, i18n } = require('./main_views'); +const RAW_HTML = Symbol.for('oasis.rawHtml'); +const { div, h2, p, section, button, form, input, span, a, table, tr, td } = h; +const { template, i18n, renderStateChip } = require('./main_views'); const TAU = Math.PI * 2; @@ -26,7 +27,7 @@ const adaptiveLayout = (n) => { return { rRatio: 0.44, nodeR: 14, labelGap: 12 }; }; -const buildGraphSvg = (me, peers) => { +const buildGraphSvg = (me, peers, links = []) => { const W = 900; const H = 600; const cx = W / 2; @@ -48,6 +49,11 @@ const buildGraphSvg = (me, peers) => { const edges = positions.map(({ peer, x, y }) => `` ).join(''); + const at = new Map(positions.map(({ peer, x, y }) => [peer.key, { x, y }])); + const chords = links.filter(l => at.has(l.a) && at.has(l.b)).map(l => { + const A = at.get(l.a), B = at.get(l.b); + return ``; + }).join(''); const nodes = positions.map(({ peer, x, y }) => { const xs = x.toFixed(2); @@ -84,13 +90,30 @@ const buildGraphSvg = (me, peers) => { + ``; return `` - + edges + nodes + center + + chords + edges + nodes + center + ``; }; -const kpi = (label, value) => div({ class: 'stats-kpi' }, - div({ class: 'stats-kpi-label' }, label), - div({ class: 'stats-kpi-value' }, String(value)) +const zoomableGraph = (id, svg, stats = []) => [ + div({ class: 'graphos-legend graphos-statsbar' }, ...stats.filter(Boolean)), + div({ class: 'graphos-canvas' }, + a({ href: process.env.OASIS_MOBILE === '1' ? `#${id}-focus` : `#${id}`, id: `${id}-src`, class: 'tribe-action-btn graphos-zoom-btn' }, `\u2922 ${i18n.graphosZoom || 'Enlarge'}`), + div({ class: 'graphos-canvas-svg', [RAW_HTML]: svg }) + ), + div({ id, class: 'lightbox graphos-lightbox' }, + a({ href: `#${id}-src`, class: 'lightbox-close' }, '\u00d7'), + div({ class: 'graphos-canvas-zoom' }, + div({ class: 'graphos-zoom-stage' }, + div({ class: 'graphos-zoom-svg', [RAW_HTML]: svg }), + span({ id: `${id}-focus`, class: 'graphos-zoom-focus' }) + ) + ) + ) +]; + +const kpi = (label, value) => span({ class: 'graphos-legend-item graphos-kpi' }, + span({ class: 'stats-kpi-label' }, String(label).toUpperCase()), + span({ class: 'stats-kpi-value' }, String(value)) ); const legendItem = (kind, label) => @@ -99,16 +122,16 @@ const legendItem = (kind, label) => span(label) ); -exports.graphosView = ({ filter, me, peers, kpis, focus = null, focusId = null, shown = null, total = null }) => { +exports.graphosView = ({ filter, me, peers, links = [], kpis, focus = null, focusId = null, shown = null, total = null }) => { const title = i18n.graphos || 'Graphos'; const description = i18n.graphosDescription || 'Interactive map of the network around you.'; - const modes = ['ALL', 'MINE']; + const modes = ['ALL', 'MINE', 'FEDERATION']; const capped = Number.isFinite(shown) && Number.isFinite(total) && total > shown; return template( title, section( - div({ class: 'tags-header' }, + div({ class: 'tags-header module-header-line' }, h2(title), p(description) ), @@ -126,28 +149,160 @@ exports.graphosView = ({ filter, me, peers, kpis, focus = null, focusId = null, modes.map(m => form({ method: 'GET', action: '/graphos' }, input({ type: 'hidden', name: 'filter', value: m }), - button({ type: 'submit', class: filter === m ? 'filter-btn active' : 'filter-btn' }, i18n[m + 'Button']) + button({ type: 'submit', class: filter === m ? 'filter-btn active' : 'filter-btn' }, String(m === 'FEDERATION' ? i18n.graphosFederation : i18n[m + 'Button']).toUpperCase()) ) ) ), capped ? p({ class: 'graphos-cap-note' }, `${i18n.graphosShowing || 'Showing'} ${shown} / ${total}`) : null, + ...zoomableGraph('graphos-zoom', buildGraphSvg(me, peers, links), [ + kpi(i18n.graphosTotalNodes || 'Total nodes', kpis.total), + kpi(i18n.online || 'Online', kpis.online), + filter !== 'MINE' ? kpi(i18n.discovered || 'Discovered', kpis.discovered) : null, + filter !== 'MINE' ? kpi(i18n.unknown || 'Unknown', kpis.unknown) : null + ]), div({ class: 'graphos-legend' }, legendItem('me', i18n.graphosYou || 'You'), legendItem('online', i18n.online || 'Online'), filter !== 'MINE' ? legendItem('discovered', i18n.discovered || 'Discovered') : null, - filter !== 'MINE' ? legendItem('unknown', i18n.unknown || 'Unknown') : null - ), - div({ class: 'graphos-canvas', innerHTML: buildGraphSvg(me, peers) }), - div({ class: 'stats-block' }, - div({ class: 'stats-grid' }, - kpi(i18n.graphosTotalNodes || 'Total nodes', kpis.total), - kpi(i18n.online || 'Online', kpis.online), - filter !== 'MINE' ? kpi(i18n.discovered || 'Discovered', kpis.discovered) : null, - filter !== 'MINE' ? kpi(i18n.unknown || 'Unknown', kpis.unknown) : null - ) + filter !== 'MINE' ? legendItem('unknown', i18n.unknown || 'Unknown') : null, + span({ class: 'graphos-legend-item' }, span({ class: 'graphos-legend-line graphos-fed-line-mutual' }), span(i18n.graphosFedMutual)), + span({ class: 'graphos-legend-item' }, span({ class: 'graphos-legend-line graphos-fed-line-oneway' }), span(i18n.graphosFedOneWay)) ) ) ); }; + +const majorMinor = (v) => String(v || '').split('.').slice(0, 2).map(n => parseInt(n, 10) || 0); +const olderThan = (v, own) => { + if (!v || !own) return false; + const [a1, b1] = majorMinor(v), [a2, b2] = majorMinor(own); + return a1 < a2 || (a1 === a2 && b1 < b2); +}; +const NET_REASON_KEYS = require('../models/peer_health').NET_REASON_KEYS; + +const pubLabel = (row) => { + const n = String(row.name || '').trim(); + if (n && n !== String(row.key).replace(/^@/, '').slice(0, 8)) return n.length > 22 ? n.slice(0, 21) + '…' : n; + if (row.host) return row.host.length > 22 ? row.host.slice(0, 21) + '…' : row.host; + return '@' + String(row.key).replace(/^@/, '').slice(0, 8) + '…'; +}; + +const buildFederationSvg = (myName, rows, links, ownVersion) => { + const W = 900; + const H = 640; + const cx = W / 2; + const cy = H / 2; + const N = Math.max(1, rows.length); + const r = Math.min(W, H) * (N <= 6 ? 0.34 : 0.42); + const pos = new Map(rows.map((row, i) => { + const angle = (i / N) * TAU - Math.PI / 2; + return [row.key, { x: cx + r * Math.cos(angle), y: cy + r * Math.sin(angle) }]; + })); + const radius = (row) => 12 + Math.min(14, Math.sqrt(row.inhabitants || 0) * 1.6); + const fed = links.map(l => { + const A = pos.get(l.a), B = pos.get(l.b); + return ``; + }).join(''); + const mine = rows.filter(row => row.state === 'connected' || row.following).map(row => { + const P = pos.get(row.key); + return ``; + }).join(''); + const nodes = rows.map(row => { + const P = pos.get(row.key); + const rr = radius(row); + const old = olderThan(row.version, ownVersion) || (!row.version && row.known); + const name = escText(pubLabel(row)); + const enc = escAttr(encodeURIComponent(row.key)); + const below = (P.y + rr + 14).toFixed(2); + return `` + + `${escText(row.name || row.host || row.key)}${row.version ? ' · ' + escText(row.version) : ''}` + + `` + + (old ? `` : '') + + (row.isolated ? `` : '') + + `` + + `${name}` + + (row.transport === 'tor' ? `TOR` : '') + + ``; + }).join(''); + const me = `${escText(myName)}` + + `` + + `${escText(myName)}`; + return `` + + fed + mine + nodes + me + + ``; +}; + +exports.graphosFederationView = ({ myName, rows = [], links = [], ownVersion = '' }) => { + const title = i18n.graphos || 'Graphos'; + const ORDER = { connected: 0, available: 1, unreachable: 2, unknown: 3 }; + const sorted = rows.slice().sort((x, y) => (ORDER[x.state] - ORDER[y.state]) || (y.inhabitants - x.inhabitants) || pubLabel(x).localeCompare(pubLabel(y))); + const outdated = (row) => olderThan(row.version, ownVersion) || (!row.version && row.known); + const count = (fn) => sorted.filter(fn).length; + const stateChip = (row) => { + if (row.state === 'connected') return renderStateChip('mutuals', null, String(i18n.peerStateConnected).toUpperCase()); + if (row.state === 'available') return renderStateChip(outdated(row) ? 'whole' : 'mutuals', null, String(i18n.peerStateStaged).toUpperCase()); + if (row.state === 'unreachable') return renderStateChip('hidden', null, String(i18n[NET_REASON_KEYS[row.reason] || 'peerErrSilent'] || i18n.peerErrOther).toUpperCase()); + return renderStateChip('hidden', null, String(i18n.graphosFedNoData).toUpperCase()); + }; + const versionChip = (row) => row.version + ? renderStateChip(outdated(row) ? 'closed' : 'mutuals', null, row.version) + : (row.known ? span({ title: i18n.peerVersionUnannounced }, renderStateChip('closed', null, String(i18n.peerOutdated).toUpperCase())) : '—'); + const transportChip = (row) => row.transport === 'unknown' ? '—' + : renderStateChip(row.transport === 'tor' ? 'fediverse' : 'transport', null, String(row.transport === 'tor' ? i18n.peerErrTor : row.transport === 'both' ? i18n.graphosFedBoth : i18n.graphosFedClearnet).toUpperCase()); + const linksCell = (row) => !row.known ? '—' : row.isolated ? renderStateChip('closed', null, String(i18n.graphosFedIsolated).toUpperCase()) : String(row.links); + return template( + title, + section( + div({ class: 'tags-header module-header-line' }, + h2(title), + p(i18n.graphosDescription || '') + ), + div({ class: 'mode-buttons stats-mode-row' }, + ['ALL', 'MINE', 'FEDERATION'].map(m => + form({ method: 'GET', action: '/graphos' }, + input({ type: 'hidden', name: 'filter', value: m }), + button({ type: 'submit', class: m === 'FEDERATION' ? 'filter-btn active' : 'filter-btn' }, String(m === 'FEDERATION' ? i18n.graphosFederation : i18n[m + 'Button']).toUpperCase()) + ) + ) + ), + ...(sorted.length ? zoomableGraph('graphos-fed-zoom', buildFederationSvg(myName || '', sorted, links, ownVersion), [ + kpi(i18n.graphosFedPubs, sorted.length), + kpi(i18n.peerStateConnected, count(r => r.state === 'connected')), + kpi(i18n.graphosFedUnreachable, count(r => r.state === 'unreachable')), + kpi(i18n.peerOutdated, count(outdated)), + kpi(i18n.peerErrTor, count(r => r.transport === 'tor')), + kpi(i18n.graphosFedLinks, links.filter(l => l.mutual).length), + kpi(i18n.graphosFedIsolated, count(r => r.isolated)) + ]) : [p(i18n.graphosFedEmpty)]), + div({ class: 'graphos-legend' }, + legendItem('me', i18n.graphosYou || 'You'), + span({ class: 'graphos-legend-item' }, span({ class: 'graphos-legend-dot graphos-fed-dot-connected' }), span(i18n.peerStateConnected)), + span({ class: 'graphos-legend-item' }, span({ class: 'graphos-legend-dot graphos-fed-dot-available' }), span(i18n.peerStateStaged)), + span({ class: 'graphos-legend-item' }, span({ class: 'graphos-legend-dot graphos-fed-dot-unreachable' }), span(i18n.graphosFedUnreachable)), + span({ class: 'graphos-legend-item' }, span({ class: 'graphos-legend-dot graphos-fed-dot-outdated' }), span(i18n.peerOutdated)), + span({ class: 'graphos-legend-item' }, span({ class: 'graphos-legend-line graphos-fed-line-mutual' }), span(i18n.graphosFedMutual)), + span({ class: 'graphos-legend-item' }, span({ class: 'graphos-legend-line graphos-fed-line-oneway' }), span(i18n.graphosFedOneWay)) + ), + sorted.length ? table({ class: 'block-info-table graphos-fed-table' }, + tr( + td({ class: 'card-label' }, 'PUB'), + td({ class: 'card-label' }, i18n.peersOasisVersion), + td({ class: 'card-label' }, i18n.graphosFedTransport), + td({ class: 'card-label' }, i18n.peerStateLabel), + td({ class: 'card-label' }, i18n.graphosFedWith), + td({ class: 'card-label' }, i18n.graphosFedInhabitants) + ), + ...sorted.map(row => tr( + td({ 'data-label': 'PUB' }, a({ href: `/author/${encodeURIComponent(row.key)}`, class: 'user-link', title: row.host || row.key }, pubLabel(row))), + td({ 'data-label': i18n.peersOasisVersion }, versionChip(row)), + td({ 'data-label': i18n.graphosFedTransport }, transportChip(row)), + td({ 'data-label': i18n.peerStateLabel }, stateChip(row)), + td({ 'data-label': i18n.graphosFedWith }, linksCell(row)), + td({ 'data-label': i18n.graphosFedInhabitants }, row.known ? String(row.inhabitants) : '—') + )) + ) : null + ) + ); +}; diff --git a/src/views/hive_views.js b/src/views/hive_views.js new file mode 100644 index 0000000..6a5421e --- /dev/null +++ b/src/views/hive_views.js @@ -0,0 +1,302 @@ +"use strict"; +// Navegacion propia de la rama experimental para movil: topbar, barra inferior +// personalizable y lanzador de modulos. +// +// Vive fuera de main_views.js a proposito: upstream reescribe ese fichero en cada +// release (de 0.9.1 a 0.9.5 movio +256/-502 lineas) y esta interfaz se perdia en +// cada integracion. Aqui solo depende de i18n, que se inyecta porque main_views +// lo muta en cada peticion sin reasignarlo. + +const { a, button, div, form, h2, h3, img, input, label, nav, p, script, section, span } = require("../server/node_modules/hyperaxe"); +const { getConfig } = require("../configs/config-manager.js"); +const sharedState = require("../configs/shared-state"); +const { iconNode, iconKeyOf } = require("./mobile_icons"); + +// con pocos modulos activos (el workflow activistas) todo cabe en una pantalla: +// sobran el filtro Personal/Social y los accesos de usuario avanzado +const COMPACT_MAX = 16; + +module.exports = ({ i18n }) => { + // === Oasis UX (portado de 0.8.7_UX): topbar + bottombar + lanzador === + // topbar fija arriba: [logo] [Personal] [Community] [avatar] en una línea (sin título OASIS) + const renderMobileTopbar = () => { + // solo en movil: en escritorio manda la cabecera de upstream + if (process.env.OASIS_MOBILE !== '1') return ""; + // Personal/Social ya no van aqui: filtraban el lanzador, que ahora esta en la hoja + // del menu, y con muchos modulos (workflow activistas) no cabian a 360px y tapaban + // el logo. Queda el hueco para el boton de No molestar del telefono + return div( + { class: "oasis-mobile-topbar" }, + a({ class: "omt-logo", href: "/" }, img({ src: "/assets/images/snh-oasis.jpg", alt: "Oasis" })), + div({ class: "omt-menus" }), + a({ class: "omt-avatar", href: "/profile" }, img({ src: (typeof global !== "undefined" && global.__OASIS_SELF_AVATAR__) || "/assets/images/default-avatar.png", alt: i18n.profile || "Profile" })) + ); + }; + + const PINNABLE_MODULES = [ + { key: 'search', href: '/search', glyph: 'ꔅ', labelKey: 'searchTitle', mod: null }, + { key: 'inbox', href: '/inbox', glyph: '☂', labelKey: 'inbox', mod: null }, + { key: 'publish', href: '/publish', glyph: '❂', labelKey: 'publish', mod: null }, + { key: 'activity', href: '/activity', glyph: 'ꔙ', labelKey: 'activityTitle', mod: null }, + { key: 'tribes', href: '/tribes', glyph: 'ꖥ', labelKey: 'tribesTitle', mod: 'tribesMod' }, + { key: 'chats', href: '/chats', glyph: 'ꖒ', labelKey: 'chatsTitle', mod: 'chatsMod' }, + { key: 'rooms', href: '/rooms', glyph: 'ꕢ', labelKey: 'roomsTitle', mod: 'roomsMod' }, + { key: 'wiki', href: '/wiki', glyph: 'ꖿ', labelKey: 'wikiTitle', mod: 'wikiMod' }, + { key: 'polls', href: '/polls', glyph: 'ꖙ', labelKey: 'pollsTitle', mod: 'pollsMod' }, + { key: 'blogs', href: '/blogs', glyph: 'ꔫ', labelKey: 'blogTitle', mod: 'blogsMod' }, + { key: 'data', href: '/data', glyph: 'ꘑ', labelKey: 'dataTitle', mod: 'dataMod' }, + { key: 'market', href: '/market', glyph: 'ꕻ', labelKey: 'marketTitle', mod: 'marketMod' }, + { key: 'events', href: '/events', glyph: 'ꕆ', labelKey: 'eventsLabel', mod: 'eventsMod' }, + { key: 'calendars', href: '/calendars', glyph: 'ꖯ', labelKey: 'calendarsTitle', mod: 'calendarsMod' }, + { key: 'banking', href: '/banking', glyph: 'ꗴ', labelKey: 'bankingTitle', mod: 'bankingMod' }, + { key: 'forum', href: '/forum', glyph: 'ꕒ', labelKey: 'forumTitle', mod: 'forumMod' }, + { key: 'videos', href: '/videos', glyph: 'ꗟ', labelKey: 'videosLabel', mod: 'videosMod' }, + { key: 'images', href: '/images', glyph: 'ꕥ', labelKey: 'imagesLabel', mod: 'imagesMod' }, + { key: 'audios', href: '/audios', glyph: 'ꔿ', labelKey: 'audiosLabel', mod: 'audiosMod' }, + { key: 'documents', href: '/documents', glyph: 'ꕨ', labelKey: 'docsLabel', mod: 'docsMod' }, + { key: 'bookmarks', href: '/bookmarks', glyph: 'ꔪ', labelKey: 'bookmarksLabel', mod: 'bookmarksMod' }, + { key: 'maps', href: '/maps', glyph: 'ꔌ', labelKey: 'mapsLabel', mod: 'mapsMod' }, + { key: 'trending', href: '/trending', glyph: 'ꗝ', labelKey: 'trendingLabel', mod: 'trendingMod' }, + { key: 'jobs', href: '/jobs', glyph: 'ꗒ', labelKey: 'jobsTitle', mod: 'jobsMod' }, + { key: 'agenda', href: '/agenda', glyph: 'ꗤ', labelKey: 'agendaTitle', mod: 'agendaMod' }, + { key: 'wallet', href: '/wallet', glyph: '❄', labelKey: 'wallet', mod: 'walletMod' }, + { key: 'tasks', href: '/tasks', glyph: 'ꖧ', labelKey: 'tasksTitle', mod: 'tasksMod' }, + { key: 'torrents', href: '/torrents', glyph: 'ꖅ', labelKey: 'torrentsLabel', mod: 'torrentsMod' }, + { key: 'tags', href: '/tags', glyph: 'ꖶ', labelKey: 'tagsLabel', mod: 'tagsMod' }, + { key: 'pads', href: '/pads', glyph: 'ꔗ', labelKey: 'padsTitle', mod: 'padsMod' }, + { key: 'projects', href: '/projects', glyph: 'ꕧ', labelKey: 'projectsTitle', mod: 'projectsMod' }, + { key: 'shops', href: '/shops', glyph: 'ꔜ', labelKey: 'shopsTitle', mod: 'shopsMod' }, + { key: 'transfers', href: '/transfers', glyph: 'ꘉ', labelKey: 'transfersTitle', mod: 'transfersMod' }, + { key: 'feed', href: '/feed', glyph: 'ꕿ', labelKey: 'feedTitle', mod: 'feedMod' }, + { key: 'pixelia', href: '/pixelia', glyph: 'ꔘ', labelKey: 'pixeliaTitle', mod: 'pixeliaMod' }, + { key: 'melody', href: '/melody', glyph: '♪', labelKey: 'melodyTitle', mod: 'melodyMod' }, + { key: 'games', href: '/games', glyph: 'ꕇ', labelKey: 'gamesTitle', mod: 'gamesMod' }, + { key: 'votes', href: '/votes', glyph: 'ꔰ', labelKey: 'votationsTitle', mod: 'votesMod' }, + { key: 'reports', href: '/reports', glyph: 'ꕥ', labelKey: 'reportsTitle', mod: 'reportsMod' }, + { key: 'opinions', href: '/opinions', glyph: 'ꔍ', labelKey: 'opinionsTitle', mod: 'opinionsMod' }, + { key: 'parliament',href: '/parliament',glyph: 'ꗞ', labelKey: 'parliamentTitle',mod: 'parliamentMod' }, + { key: 'courts', href: '/courts', glyph: 'ꖻ', labelKey: 'courtsTitle', mod: 'courtsMod' }, + { key: 'larp', href: '/larp', glyph: '✦', labelKey: 'larpTitle', mod: 'larpMod' }, + { key: 'ai', href: '/ai', glyph: 'ꘜ', labelKey: 'ai', mod: 'aiMod' }, + { key: 'cipher', href: '/cipher', glyph: 'ꗄ', labelKey: 'cipher', mod: 'cipherMod' }, + { key: 'fediverse', href: '/fediverse', glyph: 'ꗵ', labelKey: 'fediverseTimeline', mod: 'fediverseMod' }, + { key: 'inhabitants', href: '/inhabitants', glyph: 'ꖘ', labelKey: 'inhabitantsLabel', mod: null }, + { key: 'cv', href: '/cv', glyph: 'ꕛ', labelKey: 'cvTitle', mod: null }, + { key: 'stats', href: '/stats', glyph: 'ꕷ', labelKey: 'statistics', mod: null }, + { key: 'blockexplorer', href: '/blockexplorer', glyph: 'ꖸ', labelKey: 'blockchain', mod: null }, + ]; + + const renderBottomBar = () => { + if (process.env.OASIS_MOBILE !== '1') return ""; + const inboxN = sharedState.getInboxCount(); + const cfg = getConfig(); + const modOn = (m) => !m || cfg.modules[m] === 'on' || cfg.modules[m] === undefined; + // la pestaña de la seccion abierta se marca (pastilla de color tras el icono) + let here = ''; + try { here = (require('../models/typed_log').requestScope.getStore() || {}).path || ''; } catch (_) {} + const isHere = (href) => here === href || (href !== '/' && here.startsWith(href + '/')) || (href === '/activity' && here === '/'); + const bbLink = (href, ico, label, badge) => a( + { class: isHere(href) ? "bb-item current" : "bb-item", href, ...(isHere(href) ? { 'aria-current': 'page' } : {}) }, + iconNode("bb-ico", iconKeyOf(href), ico), + (badge && badge > 0) ? span({ class: "bb-badge" }, String(badge)) : "", + span({ class: "bb-lbl" }, label) + ); + // barra PERSONALIZABLE: solo los pins (max 4). Editarla y Peers estan en el + // lanzador (Herramientas): 7 botones abajo eran demasiados para un pulgar. + const MAX = 4; + const pins = (cfg.bottomBarPins || []) + .map(key => PINNABLE_MODULES.find(m => m.key === key)) + .filter(m => m && modOn(m.mod)) + .slice(0, MAX); + const slots = pins.map(m => bbLink( + m.href, m.glyph, i18n[m.labelKey] || m.key, m.key === 'inbox' ? inboxN : 0 + )); + // quinta pestaña fija: Menu abre desde abajo una hoja con todos los modulos (el + // lanzador). Sin JS: una casilla y CSS. autocomplete=off para que al volver atras + // el navegador no la deje abierta. Ver docs/ESTUDIO_NAVEGACION.md + const menuLabel = i18n.bbMenu || 'Menu'; + return nav( + { class: "oasis-bottombar", attrs: { "aria-label": i18n.bbQuickActions || "Quick actions" } }, + input({ type: 'checkbox', id: 'oasis-menu-toggle', class: 'menu-toggle', autocomplete: 'off', 'aria-label': menuLabel }), + ...slots, + label({ class: 'bb-item bb-menu', for: 'oasis-menu-toggle' }, + iconNode('bb-ico', 'modules', '▦'), + span({ class: 'bb-lbl' }, menuLabel) + ), + label({ class: 'menu-sheet-backdrop', for: 'oasis-menu-toggle', 'aria-hidden': 'true' }), + div({ class: 'menu-sheet', role: 'dialog', 'aria-label': menuLabel }, + div({ class: 'menu-sheet-head' }, + span({ class: 'menu-sheet-title' }, menuLabel), + label({ class: 'menu-sheet-close', for: 'oasis-menu-toggle', 'aria-label': i18n.bbMenuClose || 'Close' }, '✕') + ), + renderHiveNav() + ), + // atras cierra la hoja (sin este script el menu funciona igual) + script({ src: '/js/mobile_menu.js', defer: 'defer' }) + ); + }; + + // Lanzador de modulos de la home: secciones con una rejilla de iconos, como el + // cajon de apps del movil. Solo salen los modulos activos y las secciones que + // tienen alguno, asi que con un workflow corto (activistas) cabe en una pantalla. + const GROUPS = [ + { id: 'community', titleKey: 'menuNetwork', side: 'community', mods: [ + { href: '/activity', glyph: 'ꔙ', labelKey: 'activityTitle', mod: null }, + { href: '/feed', glyph: 'ꕿ', labelKey: 'feedTitle', mod: 'feedMod' }, + { href: '/chats', glyph: 'ꖒ', labelKey: 'chatsTitle', mod: 'chatsMod' }, + { href: '/rooms', glyph: 'ꕢ', labelKey: 'roomsTitle', mod: 'roomsMod' }, + { href: '/forum', glyph: 'ꕒ', labelKey: 'forumTitle', mod: 'forumMod' }, + { href: '/pads', glyph: 'ꔗ', labelKey: 'padsTitle', mod: 'padsMod' }, + { href: '/wiki', glyph: 'ꖿ', labelKey: 'wikiTitle', mod: 'wikiMod' }, + { href: '/blogs', glyph: 'ꔫ', labelKey: 'blogTitle', mod: 'blogsMod' }, + { href: '/opinions', glyph: 'ꔍ', labelKey: 'opinionsTitle', mod: 'opinionsMod' }, + { href: '/mentions', glyph: '✺', labelKey: 'mentions', mod: null, extra: true }, + { href: '/tags', glyph: 'ꖶ', labelKey: 'tagsLabel', mod: 'tagsMod' }, + { href: '/trending', glyph: 'ꗝ', labelKey: 'trendingLabel', mod: 'trendingMod' }, + { href: '/maps', glyph: 'ꔌ', labelKey: 'mapsLabel', mod: 'mapsMod' }, + { href: '/graphos', glyph: '⌬', labelKey: 'modulesGraphosLabel', mod: 'graphosMod' }, + { href: '/inhabitants', glyph: 'ꖘ', labelKey: 'inhabitantsLabel', mod: null, extra: true }, + { href: '/fediverse', glyph: 'ꗵ', labelKey: 'fediverseTimeline', mod: 'fediverseMod' } ] }, + { id: 'media', titleKey: 'menuMedia', side: 'community', mods: [ + { href: '/images', glyph: 'ꕥ', labelKey: 'imagesLabel', mod: 'imagesMod' }, + { href: '/videos', glyph: 'ꗟ', labelKey: 'videosLabel', mod: 'videosMod' }, + { href: '/audios', glyph: 'ꔿ', labelKey: 'audiosLabel', mod: 'audiosMod' }, + { href: '/podcasts', glyph: '◉', labelKey: 'podcastsTitle', mod: 'podcastsMod' }, + { href: '/documents', glyph: 'ꕨ', labelKey: 'docsLabel', mod: 'docsMod' }, + { href: '/files', glyph: 'ꖃ', labelKey: 'filesTitle', mod: 'filesMod' }, + { href: '/bookmarks', glyph: 'ꔪ', labelKey: 'bookmarksLabel', mod: 'bookmarksMod' }, + { href: '/torrents', glyph: 'ꖅ', labelKey: 'torrentsLabel', mod: 'torrentsMod' } ] }, + { id: 'creative', titleKey: 'menuCreative', side: 'community', mods: [ + { href: '/games', glyph: 'ꕇ', labelKey: 'gamesTitle', mod: 'gamesMod' }, + { href: '/pixelia', glyph: 'ꔘ', labelKey: 'pixeliaTitle', mod: 'pixeliaMod' }, + { href: '/melody', glyph: '♪', labelKey: 'melodyTitle', mod: 'melodyMod' } ] }, + { id: 'office', titleKey: 'menuOffice', side: 'personal', mods: [ + { href: '/events', glyph: 'ꕆ', labelKey: 'eventsLabel', mod: 'eventsMod' }, + { href: '/calendars', glyph: 'ꖯ', labelKey: 'calendarsTitle', mod: 'calendarsMod' }, + { href: '/tasks', glyph: 'ꖧ', labelKey: 'tasksTitle', mod: 'tasksMod' }, + { href: '/campaigns', glyph: '⚑', labelKey: 'campaignsTitle', mod: 'campaignsMod' }, + { href: '/mailing', glyph: '✉', labelKey: 'mailingTitle', mod: 'mailingMod' }, + { href: '/votes', glyph: 'ꔰ', labelKey: 'votationsTitle', mod: 'votesMod' }, + { href: '/polls', glyph: 'ꖙ', labelKey: 'pollsTitle', mod: 'pollsMod' }, + { href: '/reports', glyph: 'ꕤ', labelKey: 'reportsTitle', mod: 'reportsMod' } ] }, + { id: 'governance', titleKey: 'menuGovernance', side: 'personal', mods: [ + { href: '/tribes', glyph: 'ꖥ', labelKey: 'tribesTitle', mod: 'tribesMod' }, + { href: '/emergencies', glyph: '✚', labelKey: 'emergenciesTitle', mod: 'emergenciesMod' }, + { href: '/school', glyph: 'ꗳ', labelKey: 'schoolTitle', mod: 'schoolMod' }, + { href: '/larp', glyph: '✦', labelKey: 'larpTitle', mod: 'larpMod' }, + { href: '/parliament', glyph: 'ꗞ', labelKey: 'parliamentTitle', mod: 'parliamentMod' }, + { href: '/courts', glyph: 'ꖻ', labelKey: 'courtsTitle', mod: 'courtsMod' } ] }, + { id: 'economy', titleKey: 'menuEconomy', side: 'personal', mods: [ + { href: '/market', glyph: 'ꕻ', labelKey: 'marketTitle', mod: 'marketMod' }, + { href: '/shops', glyph: 'ꔜ', labelKey: 'shopsTitle', mod: 'shopsMod' }, + { href: '/jobs', glyph: 'ꗒ', labelKey: 'jobsTitle', mod: 'jobsMod' }, + { href: '/projects', glyph: 'ꕧ', labelKey: 'projectsTitle', mod: 'projectsMod' }, + { href: '/housing', glyph: '⌂', labelKey: 'housingTitle', mod: 'housingMod' }, + { href: '/logistics', glyph: '⇄', labelKey: 'logisticsTitle', mod: 'logisticsMod' }, + { href: '/banking', glyph: 'ꗴ', labelKey: 'bankingTitle', mod: 'bankingMod' }, + { href: '/transfers', glyph: 'ꘉ', labelKey: 'transfersTitle', mod: 'transfersMod' } ] }, + { id: 'personal', titleKey: 'menuPersonal', side: 'personal', mods: [ + { href: '/profile', glyph: '⚉', labelKey: 'profile', mod: null, extra: true }, + { href: '/agenda', glyph: 'ꗤ', labelKey: 'agendaTitle', mod: 'agendaMod' }, + { href: '/favorites', glyph: 'ꘝ', labelKey: 'favoritesTitle', mod: 'favoritesMod' }, + { href: '/phone', glyph: '☏', labelKey: 'phoneTitle', mod: 'phoneMod' }, + { href: '/cv', glyph: 'ꕛ', labelKey: 'cvTitle', mod: null, extra: true }, + { href: '/wallet', glyph: '❄', labelKey: 'wallet', mod: 'walletMod' }, + { href: '/logs', glyph: 'ꗯ', labelKey: 'logsTitle', mod: 'logsMod' } ] }, + { id: 'tools', titleKey: 'menuTools', side: 'personal', mods: [ + { href: '/invites', glyph: 'ꔹ', labelKey: 'invites', mod: 'invitesMod' }, + { href: '/peers', glyph: '⧖', labelKey: 'peers', mod: null }, + { href: '/settings/bottombar', glyph: '✎', labelKey: 'bbYourBar', mod: null }, + { href: '/backup', glyph: 'ꖤ', labelKey: 'backupTitle', mod: 'backupMod' }, + { href: '/cipher', glyph: 'ꗄ', labelKey: 'cipher', mod: 'cipherMod' }, + { href: '/ai', glyph: 'ꘜ', labelKey: 'ai', mod: 'aiMod' }, + { href: '/stats', glyph: 'ꕷ', labelKey: 'statistics', mod: null, extra: true }, + { href: '/blockexplorer', glyph: 'ꖸ', labelKey: 'blockchain', mod: null, extra: true }, + { href: '/modules', glyph: 'ꗣ', labelKey: 'modules', mod: null, extra: true }, + { href: '/settings', glyph: '⚙', labelKey: 'settings', mod: null } ] }, + ]; + + const isCompact = () => { + const cfg = getConfig(); + const on = (m) => m && cfg.modules[m] === 'on'; + return GROUPS.reduce((n, g) => n + g.mods.filter(x => on(x.mod)).length, 0) <= COMPACT_MAX; + }; + + const renderHiveNav = () => { + if (process.env.OASIS_MOBILE !== '1') return ""; + const cfg = getConfig(); + const modOn = (m) => !m || cfg.modules[m] === 'on' || cfg.modules[m] === undefined; + const compact = isCompact(); + // Personal/Social de la topbar (lo pone el middleware desde ?hive=) parte el lanzador en dos + const hiveFilter = compact ? "" : ((typeof global !== "undefined" && global.__OASIS_HIVE_FILTER__) || ""); + const app = (m) => a({ class: 'ln-app', href: m.href }, + iconNode('ln-ico', iconKeyOf(m.href), m.glyph), + span({ class: 'ln-lbl' }, i18n[m.labelKey] || m.href.slice(1))); + const groups = GROUPS + .filter(g => !hiveFilter || g.side === hiveFilter) + .map(g => ({ g, mods: g.mods.filter(m => modOn(m.mod) && !(compact && m.extra)) })) + .filter(x => x.mods.length); + return nav({ class: 'launcher', 'aria-label': i18n.modules || 'Modules' }, + ...groups.map(({ g, mods }) => div({ class: 'ln-group' }, + h3({ class: 'ln-title' }, i18n[g.titleKey] || g.id), + div({ class: 'ln-grid' }, ...mods.map(app)) + )) + ); + }; + + + // pantalla para personalizar la barra inferior (/settings/bottombar) + const bottomBarPickerView = (template) => { + const cfg = getConfig(); + const modOn = (m) => !m || cfg.modules[m] === 'on' || cfg.modules[m] === undefined; + const MAX = 4; + const pinnedKeys = cfg.bottomBarPins || []; + const pinned = pinnedKeys + .map(key => PINNABLE_MODULES.find(m => m.key === key)) + .filter(Boolean); + const full = pinned.length >= MAX; + const actionForm = (action, key, cls, node) => form({ method: 'POST', action: '/settings/bottombar', class: cls }, + input({ type: 'hidden', name: 'action', value: action }), + input({ type: 'hidden', name: 'key', value: key }), + node + ); + const pinnedRow = (m) => actionForm('remove', m.key, 'bb-manage-form', + button({ type: 'submit', class: 'bb-manage-btn', 'aria-label': i18n.bbRemove || 'Remove' }, + iconNode('bb-manage-ico', iconKeyOf(m.href), m.glyph), + span({ class: 'bb-manage-lbl' }, i18n[m.labelKey] || m.key), + span({ class: 'bb-manage-x' }, '✕') + ) + ); + const addBtn = (m) => actionForm('add', m.key, 'bb-pick-form', + button({ type: 'submit', class: 'bb-pick-btn' }, + iconNode('bb-pick-ico', iconKeyOf(m.href), m.glyph), + span({ class: 'bb-pick-lbl' }, i18n[m.labelKey] || m.key) + ) + ); + const addable = PINNABLE_MODULES.filter(m => modOn(m.mod) && !pinnedKeys.includes(m.key)); + return template( + i18n.bbManage || 'Edit bar', + section( + div({ class: 'tags-header' }, + h2(i18n.bbPickTitle || 'Customize the bar'), + p(i18n.bbPickDesc || 'Choose up to 4 shortcuts for the bottom bar.') + ), + div({ class: 'bb-manage' }, + h3(`${i18n.bbYourBar || 'Your bar'} (${pinned.length}/${MAX})`), + pinned.length + ? div({ class: 'bb-manage-list' }, ...pinned.map(pinnedRow)) + : p({ class: 'bb-manage-empty' }, i18n.bbPickNone || 'None') + ), + div({ class: 'bb-manage-add' }, + h3(i18n.bbAdd || 'Add'), + full + ? p({ class: 'bb-manage-full' }, i18n.bbFull || 'Maximum reached') + : div({ class: 'bb-pick-grid' }, ...addable.map(addBtn)) + ), + a({ class: 'filter-btn bb-manage-done', href: '/activity' }, i18n.bbDone || 'Done') + ) + ); + }; + + return { renderMobileTopbar, renderBottomBar, renderHiveNav, PINNABLE_MODULES, bottomBarPickerView, GROUPS }; +}; diff --git a/src/views/housing_view.js b/src/views/housing_view.js new file mode 100644 index 0000000..a0b5d45 --- /dev/null +++ b/src/views/housing_view.js @@ -0,0 +1,579 @@ +const { form, button, div, h2, p, section, input, label, textarea, br, a, span, select, option, img, video, table, tr, td, hr } = require("../server/node_modules/hyperaxe") +const { renderCommentsSection: renderSharedCommentsSection } = require("./comments_view"); +const { renderReachChip, renderClearnetSelector, renderClearnetSwitch } = require("./clearnet_view") +const { clearnetItemHref, template, i18n, renderOpenClosedChip, renderStateChip, renderVisibilityChip, renderLifespanChip, renderEcoTax, renderSpreadButton, renderContentActions, renderOpinionsVoting, renderEngagement, renderSpreadEditWarning, renderModuleStatsBy, moduleIsEmpty, renderEcoValueChip, contentDeleteAction, paged, renderCreatedRows } = require("./main_views") +const { blobUrl, blobIdOf, isVideoEntry, imagesOf, renderMediaThumb, renderPhotoGallery, renderGalleryFields } = require("./gallery_view") +const moment = require("../server/node_modules/moment") +const { config } = require("../server/SSB_server.js") +const { renderStyledText } = require("../backend/renderStyledText") +const opinionCategories = require("../backend/opinion_categories") +const { renderMapEmbed, renderMapLocationVisitLabel } = require("./maps_view") + +const userId = config.keys.id + +const FILTERS = [ + { key: "ALL", i18n: "housingFilterAll" }, + { key: "RECENT", i18n: "housingFilterRecent" }, + { key: "MINE", i18n: "housingFilterMine" }, + { key: "TOP", i18n: "housingFilterTop" }, + { key: "REQUESTED", i18n: "housingFilterRequested" }, + { key: "SALE", i18n: "housingFilterSale" }, + { key: "RENT", i18n: "housingFilterRent" }, + { key: "COUCHSURFING", i18n: "housingFilterCouchsurfing" }, + { key: "OPEN", i18n: "housingFilterOpen" }, + { key: "CLOSED", i18n: "housingFilterClosed" } +] + +const MAX_IMAGES = 8 + +const safeArr = (v) => (Array.isArray(v) ? v : []) +const safeText = (v) => String(v || "").trim() +const clearnetEligible = (item) => String(item.status || "OPEN").toUpperCase() === "OPEN" && String(item.visibility || "PUBLIC").toUpperCase() === "PUBLIC" && !item.encrypted && !item.tribeId +const housingClearnetHref = (item) => clearnetItemHref("housing", item.title, item.rootId || item.id) +const HOUSING_REACH = ["PUBLIC", "HIDDEN"] +const housingReachLabel = (v) => v === "HIDDEN" ? (i18n.visibilityHidden || "Hidden") : (i18n.visibilityPublic || "Public") + +const parseNum = (v) => { + const n = parseFloat(String(v ?? "").replace(",", ".")) + return Number.isFinite(n) ? n : NaN +} + +const fmtPrice = (v) => { + const n = parseNum(v) + return Number.isFinite(n) ? n.toFixed(2) : String(v ?? "") +} + + + +const priceLabel = (item) => { + if (item.housing_type === "couchsurfing") return i18n.housingFree || "FREE" + const suffix = item.housing_type === "rent" ? ` ${i18n.housingPerMonth || "/month"}` : "" + return `${fmtPrice(item.price)} ECO${suffix}` +} + +const sumCats = (opinions = {}, cats = []) => (cats || []).reduce((s, c) => s + (Number((opinions || {})[c]) || 0), 0) + +const renderStarRating = (opinions, voterCount) => { + const pos = sumCats(opinions, opinionCategories.positive) + const neg = sumCats(opinions, opinionCategories.constructive) + sumCats(opinions, opinionCategories.moderation) + const total = pos + neg + const full = total > 0 ? Math.round((pos / total) * 5) : 0 + const stars = "★".repeat(full) + "☆".repeat(5 - full) + return span({ class: "housing-stars" }, `${stars} (${voterCount})`) +} + +const buildReturnTo = (filter, params = {}) => { + const parts = [`filter=${encodeURIComponent(safeText(filter || "ALL"))}`] + const q = safeText(params.search || "") + if (q) parts.push(`search=${encodeURIComponent(q)}`) + if (String(params.minPrice ?? "") !== "") parts.push(`minPrice=${encodeURIComponent(String(params.minPrice))}`) + if (String(params.maxPrice ?? "") !== "") parts.push(`maxPrice=${encodeURIComponent(String(params.maxPrice))}`) + if (safeText(params.place)) parts.push(`place=${encodeURIComponent(safeText(params.place))}`) + if (safeText(params.sort)) parts.push(`sort=${encodeURIComponent(safeText(params.sort))}`) + return `/housing?${parts.join("&")}` +} + +const renderTags = (tags = []) => { + const arr = safeArr(tags).map(t => String(t || "").trim()).filter(Boolean) + return arr.length + ? div({ class: "card-tags" }, arr.map(tag => a({ class: "tag-link", href: `/search?query=%23${encodeURIComponent(tag)}` }, `#${tag}`))) + : null +} + +const renderTypeChip = (item) => { + const t = String(item.housing_type || "").toUpperCase() + const emoji = item.housing_type === "sale" ? "🏷" : item.housing_type === "rent" ? "🔑" : "🛋" + return renderStateChip("whole", emoji, i18n["housingType" + t] || t) +} + +const renderStatusChip = (status) => { + const s = String(status || "").toUpperCase() + return renderOpenClosedChip(s, { + statusChipOPEN: i18n.housingStatusOPEN || "OPEN", + statusChipCLOSED: i18n.housingStatusCLOSED || "CLOSED" + }) +} + +const renderRequestedChip = () => renderStateChip("whole", "★", i18n.housingRequestedBadge || "REQUESTED") + +const renderInfoTable = (item) => { + const rows = [] + const pushRow = (labelText, valueNode, valueClass) => + rows.push(tr( + td({ class: "tribe-info-label" }, labelText), + td({ class: `tribe-info-value${valueClass ? " " + valueClass : ""}` }, valueNode) + )) + pushRow(i18n.housingProperty, i18n["housingProperty" + String(item.property_type || "").toUpperCase()] || "—") + pushRow(i18n.housingPrice, priceLabel(item), "card-salary") + if (item.rooms > 0) pushRow(i18n.housingRooms, String(item.rooms)) + if (item.size > 0) pushRow(i18n.housingSize, `${item.size} m²`) + if (item.capacity > 0) pushRow(i18n.housingCapacity, String(item.capacity)) + if (item.availableFrom) pushRow(i18n.housingAvailableFrom, moment(item.availableFrom).format("YYYY/MM/DD")) + if (item.availableTo) pushRow(i18n.housingAvailableTo, moment(item.availableTo).format("YYYY/MM/DD")) + if (safeText(item.place)) pushRow(i18n.housingPlace, safeText(item.place)) + if (item.mapUrl) { + const mapNode = renderMapLocationVisitLabel(item.mapUrl) + if (mapNode) pushRow(i18n.mapLocationTitle || "Map", mapNode) + } + return table({ class: "tribe-info-table housing-info-table" }, ...rows, ...renderCreatedRows(item.createdAt, item.author)) +} + +const today = () => moment().format("YYYY-MM-DD") + +const renderStatusRow = (item, returnTo) => { + if (String(item.author) !== String(userId)) return null + const isOpen = item.status === "OPEN" + return div({ class: "tribe-side-actions housing-status-row" }, + span({ class: "card-label" }, `${i18n.housingStatus}: `), + renderStatusChip(item.status), + form({ method: "POST", action: `/housing/status/${encodeURIComponent(item.id)}`, class: "inline-form" }, + input({ type: "hidden", name: "returnTo", value: returnTo }), + button({ class: "tribe-action-btn", type: "submit", name: "status", value: isOpen ? "CLOSED" : "OPEN" }, + isOpen ? (i18n.housingSetClosed || "Close") : (i18n.housingSetOpen || "Reopen")) + ) + ) +} + +const renderOwnerActions = (item, returnTo) => { + if (String(item.author) !== String(userId)) return [] + return [ + form({ method: "GET", action: `/housing/edit/${encodeURIComponent(item.id)}` }, + input({ type: "hidden", name: "returnTo", value: returnTo }), + button({ class: "update-btn", type: "submit" }, i18n.housingUpdateButton) + ) + ] +} + +const renderRequestToggle = (item, returnTo) => { + if (String(item.author) === String(userId)) return null + if (item.status !== "OPEN") return null + const requested = safeArr(item.requests).includes(userId) + return requested + ? form({ method: "POST", action: `/housing/cancel/${encodeURIComponent(item.id)}` }, + input({ type: "hidden", name: "returnTo", value: returnTo }), + button({ type: "submit", class: "filter-btn" }, i18n.housingCancelButton) + ) + : form({ method: "POST", action: `/housing/request/${encodeURIComponent(item.id)}` }, + input({ type: "hidden", name: "returnTo", value: returnTo }), + button({ type: "submit", class: "filter-btn" }, i18n.housingRequestButton) + ) +} + +const renderHousingList = (items, filter, params = {}) => { + const list = safeArr(items) + if (!list.length) return p(i18n.housingNoItems) + const returnTo = buildReturnTo(filter, params) + + return div({ class: "housing-grid" }, + list.map((item) => { + const clip = safeText(item.video) + const cover = imagesOf(item)[0] + const isOwn = item.author && String(item.author) === String(userId) + const requested = safeArr(item.requests).includes(userId) + const chips = [ + item.visibility === "HIDDEN" ? renderVisibilityChip("HIDDEN", i18n) : null, + renderTypeChip(item), + renderStatusChip(item.status), + requested ? renderRequestedChip() : null, + renderLifespanChip(item.lifetime, i18n), + item.clearnet === true && clearnetEligible(item) ? renderReachChip(true, i18n, housingClearnetHref(item)) : null + ].filter(Boolean) + + return div({ class: "trending-card housing-card" + (isOwn ? " own-content" : "") }, + div({ class: "card-header activity-card-header" }, + span(), + renderContentActions(item.id, `/housing/${encodeURIComponent(item.id)}`, { spread: params.spreadMap && params.spreadMap.get(item.id) || null, author: item.author, favKind: 'housing', isFavorite: item.isFavorite, reportTitle: item.title, returnTo, deleteAction: isOwn ? contentDeleteAction("housing", item.id) : null }) + ), + div({ class: "card-section housing-card-body" }, + clip || (cover && isVideoEntry(cover)) + ? div({ class: "tribe-card-image-wrapper housing-card-video" }, + video({ controls: true, class: "housing-card-hero-video", src: blobUrl(blobIdOf(clip || cover)), preload: 'metadata' }) + ) + : cover + ? div({ class: "tribe-card-image-wrapper" }, + a({ href: `/housing/${encodeURIComponent(item.id)}` }, + img({ src: blobUrl(blobIdOf(cover), 256), class: "tribe-card-hero-image", alt: "" }) + ) + ) + : null, + div({ class: "tribe-card-body" }, + div({ class: "shop-title-row" }, + h2({ class: "tribe-card-title" }, + a({ href: `/housing/${encodeURIComponent(item.id)}` }, safeText(item.title) || i18n.housingTitle) + ) + ), + chips.length ? div({ class: "card-chips-row" }, ...chips) : null, + renderStarRating(item.opinions, safeArr(item.opinions_inhabitants).length), + div({ class: "price-chip" }, priceLabel(item)), + safeText(item.place) + ? div({ class: "card-field card-field-mb" }, + span({ class: "card-label" }, `${String(i18n.housingPlace).toUpperCase()}: `), + span({ class: "card-value" }, safeText(item.place)) + ) + : null, + div({ class: "tribe-card-members" }, + span({ class: "tribe-members-count" }, `${i18n.housingRequests}: ${item.requestCount || 0}`) + ), + imagesOf(item).length > 1 + ? div({ class: "card-field" }, + span({ class: "card-label" }, `${i18n.galleryPhotos}: `), + span({ class: "card-value" }, String(imagesOf(item).length)) + ) + : null + ) + ) + ) + }) + ) +} + +const renderHousingForm = (item = {}, mode = "create", maxImages = MAX_IMAGES, spreadWarning = null, params = {}) => { + const isEdit = mode === "edit" + const type = String(item.housing_type || "").toLowerCase() + const reachRaw = String(params.reach || "").toUpperCase() + const ownReach = String(item.visibility || "").toUpperCase() + const reach = HOUSING_REACH.includes(reachRaw) ? reachRaw : (HOUSING_REACH.includes(ownReach) ? ownReach : "PUBLIC") + const showClearnet = reach === "PUBLIC" && clearnetEligible({ ...item, visibility: reach }) + return div({ class: "div-center housing-form" }, + isEdit ? spreadWarning : null, + form({ method: "GET", action: isEdit ? `/housing/edit/${encodeURIComponent(item.id)}` : "/housing" }, + isEdit ? null : input({ type: "hidden", name: "filter", value: "CREATE" }), + label(i18n.visibilityLabel || "Visibility"), + br(), + div({ class: "apply-row" }, + select({ name: "visibility", class: "report-category-select" }, + option({ value: "PUBLIC", ...(reach === "PUBLIC" ? { selected: true } : {}) }, i18n.visibilityPublic || "Public"), + option({ value: "HIDDEN", ...(reach === "HIDDEN" ? { selected: true } : {}) }, i18n.visibilityHidden || "Hidden") + ), + button({ type: "submit", class: "create-button" }, i18n.apply || "Apply") + ) + ), + hr({ class: "form-sep" }), + h2({ class: "report-category-fixed" }, housingReachLabel(reach)), + form({ + action: isEdit ? `/housing/update/${encodeURIComponent(item.id)}` : "/housing/create", + method: "POST", + enctype: "multipart/form-data" + }, + input({ type: "hidden", name: "returnTo", value: "/housing?filter=MINE" }), + input({ type: "hidden", name: "visibility", value: reach }), + label(i18n.housingType), + br(), + select({ name: "housing_type", required: true }, + option({ value: "sale", selected: type === "sale" ? "selected" : undefined }, i18n.housingTypeSALE), + option({ value: "rent", selected: type === "rent" ? "selected" : undefined }, i18n.housingTypeRENT), + option({ value: "couchsurfing", selected: type === "couchsurfing" ? "selected" : undefined }, i18n.housingTypeCOUCHSURFING) + ), + br(), + br(), + label(i18n.housingProperty), + br(), + select({ name: "property_type" }, + option({ value: "apartment", ...(item.property_type === "apartment" ? { selected: true } : {})}, i18n.housingPropertyAPARTMENT), + option({ value: "house", ...(item.property_type === "house" ? { selected: true } : {})}, i18n.housingPropertyHOUSE), + option({ value: "room", ...(item.property_type === "room" ? { selected: true } : {})}, i18n.housingPropertyROOM), + option({ value: "land", ...(item.property_type === "land" ? { selected: true } : {})}, i18n.housingPropertyLAND), + option({ value: "other", ...(item.property_type === "other" ? { selected: true } : {})}, i18n.housingPropertyOTHER) + ), + br(), + br(), + label(i18n.housingTitleLabel), + br(), + input({ type: "text", name: "title", maxlength: "100", required: true, placeholder: i18n.housingTitlePlaceholder, value: item.title || "" }), + br(), + br(), + ...renderGalleryFields(item, isEdit, maxImages), + label(i18n.housingDescription), + br(), + textarea({ maxlength: "5000", name: "description", rows: "6", required: true, placeholder: i18n.housingDescriptionPlaceholder }, item.description || ""), + br(), + br(), + label(i18n.housingRules), + br(), + textarea({ maxlength: "5000", name: "rules", rows: "4", placeholder: i18n.housingRulesPlaceholder }, item.rules || ""), + br(), + br(), + label(i18n.housingPlace), + br(), + input({ type: "text", name: "place", placeholder: i18n.housingPlacePlaceholder, value: item.place || "" }), + br(), + br(), + label(i18n.mapLocationTitle || "Map Location"), + br(), + input({ type: "text", name: "mapUrl", placeholder: i18n.mapUrlPlaceholder || "/maps/MAP_ID", value: item.mapUrl || "" }), + br(), + br(), + label(i18n.housingPriceLabel), + br(), + input({ type: "number", name: "price", step: "0.01", min: "0", value: item.price || "" }), + renderEcoValueChip(), + br(), + br(), + label(i18n.housingRooms), + br(), + input({ type: "number", name: "rooms", min: "0", value: item.rooms || "" }), + br(), + br(), + label(i18n.housingSizeLabel), + br(), + input({ type: "number", name: "size", step: "0.01", min: "0", value: item.size || "" }), + br(), + br(), + label(i18n.housingCapacity), + br(), + input({ type: "number", name: "capacity", min: "0", value: item.capacity || "" }), + br(), + br(), + label(i18n.housingAvailableFrom), + br(), + input({ type: "date", name: "availableFrom", required: true, min: today(), value: item.availableFrom ? String(item.availableFrom).slice(0, 10) : "" }), + br(), + br(), + label(i18n.housingAvailableTo), + br(), + input({ type: "date", name: "availableTo", min: item.availableFrom ? String(item.availableFrom).slice(0, 10) : today(), value: item.availableTo ? String(item.availableTo).slice(0, 10) : "" }), + br(), + br(), + label(i18n.housingTags), + br(), + input({ type: "text", name: "tags", placeholder: i18n.tagsPlaceholder, value: Array.isArray(item.tags) ? item.tags.join(", ") : (item.tags || "") }), + br(), + br(), + ...(showClearnet ? [renderClearnetSelector(item.clearnet === true || String(item.clearnet || "") === "1", i18n), br()] : []), + button({ type: "submit" }, isEdit ? i18n.housingUpdateButton : i18n.housingCreateButton) + ) + ) +} + +const housingChip = (censusH, filter, x) => { + const m = x.key + if (m === filter) return true + if (m === "TOP") return censusH.length > 0 + if (m === "MINE") return censusH.some(h => String(h.author) === String(userId)) + if (m === "RECENT") return censusH.length > 0 + if (m === "REQUESTED") return censusH.some(h => safeArr(h.requests).includes(userId)) + if (m === "SALE" || m === "RENT" || m === "COUCHSURFING") return censusH.some(h => String(h.housing_type || "").toUpperCase() === m) + if (m === "OPEN" || m === "CLOSED") return censusH.some(h => String(h.status || "OPEN").toUpperCase() === m) + return true +} + +const renderFiltersBar = (filter, params = {}, emptyMod = false, censusH = null) => + div({ class: "filters" }, + form({ method: "GET", action: "/housing", class: "ui-toolbar ui-toolbar--filters" }, + input({ type: "hidden", name: "search", value: safeText(params.search || "") }), + input({ type: "hidden", name: "minPrice", value: String(params.minPrice ?? "") }), + input({ type: "hidden", name: "maxPrice", value: String(params.maxPrice ?? "") }), + input({ type: "hidden", name: "place", value: safeText(params.place || "") }), + input({ type: "hidden", name: "sort", value: safeText(params.sort || "") }), + ...(emptyMod ? [] : (censusH ? FILTERS.filter(x => housingChip(censusH, filter, x)) : FILTERS).map(f => + button({ type: "submit", name: "filter", value: f.key, class: filter === f.key ? "filter-btn active" : "filter-btn" }, String(i18n[f.i18n]).toUpperCase()) + )), + button({ type: "submit", name: "filter", value: "CREATE", class: "create-button" }, i18n.housingCreateButton) + ) + ) + +exports.housingView = async (items, filter = "ALL", params = {}) => { + const search = safeText(params.search || "") + const minPrice = params.minPrice ?? "" + const maxPrice = params.maxPrice ?? "" + const place = safeText(params.place || "") + const sort = safeText(params.sort || "recent") + + const isForm = filter === "CREATE" || filter === "EDIT" + const emptyMod = moduleIsEmpty(Array.isArray(items) ? items : [], filter, "ALL", search || place || String(minPrice || "") || String(maxPrice || "")) + + return template( + i18n.housingTitle, + section( + div({ class: "tags-header module-header-line" }, + h2(i18n.housingTitle), + p(i18n.housingDescriptionText) + ), + renderFiltersBar(filter, { search, minPrice, maxPrice, place, sort }, emptyMod, Array.isArray(params.censusList) ? params.censusList : (Array.isArray(items) ? items : [])) + ), + section( + isForm + ? renderHousingForm(filter === "EDIT" ? (Array.isArray(items) ? items[0] : items) || {} : (params.draft || {}), filter === "EDIT" ? "edit" : "create", Number(params.maxImages) > 0 ? Number(params.maxImages) : MAX_IMAGES, await renderSpreadEditWarning(filter === "EDIT" ? ((Array.isArray(items) ? items[0] : items) || {}).id : null), params) + : section( + emptyMod ? null : div({ class: "housing-search activity-filter-chips activity-toolbar-row" }, + renderModuleStatsBy(items, it => String(it.status || '').toUpperCase(), [{ value: 'OPEN', label: i18n.housingFilterOpen }, { value: 'CLOSED', label: i18n.housingFilterClosed }]), + form({ method: "GET", action: "/housing", class: "filter-box" }, + input({ type: "hidden", name: "filter", value: filter || "ALL" }), + input({ type: "text", name: "search", value: search, placeholder: i18n.housingSearchPlaceholder, class: "filter-box__input" }), + input({ type: "text", name: "place", value: place, placeholder: i18n.housingPlacePlaceholder, class: "filter-box__input housing-place-input" }), + input({ type: "number", name: "minPrice", step: "0.01", min: "0", value: String(minPrice ?? ""), placeholder: i18n.housingMinPrice, class: "filter-box__number transfer-amount-input" }), + input({ type: "number", name: "maxPrice", step: "0.01", min: "0", value: String(maxPrice ?? ""), placeholder: i18n.housingMaxPrice, class: "filter-box__number transfer-amount-input" }), + select({ name: "sort", class: "filter-box__select" }, + option({ value: "recent", ...(sort === "recent" ? { selected: true } : {})}, i18n.housingSortRecent), + option({ value: "price", ...(sort === "price" ? { selected: true } : {})}, i18n.housingSortPrice), + option({ value: "requests", ...(sort === "requests" ? { selected: true } : {})}, i18n.housingSortRequests), + option({ value: "rating", ...(sort === "rating" ? { selected: true } : {})}, i18n.housingSortRating) + ), + button({ type: "submit", class: "filter-box__button" }, i18n.housingSearchButton) + ) + ), + div({ class: "housing-list" }, renderHousingList(paged(items), filter, params)) + ) + ) + ) +} + +const renderCommentsSection = (itemId, returnTo, comments = []) => { + return renderSharedCommentsSection({ + action: `/housing/${encodeURIComponent(itemId)}/comments`, + comments: comments, + returnTo: returnTo + }); +}; + +exports.singleHousingView = async (item, filter = "ALL", comments = [], params = {}) => { + const returnTo = safeText(params.returnTo) || buildReturnTo(filter, params) + const isAuthor = String(item.author) === String(userId) + const requested = safeArr(item.requests).includes(userId) + const voters = safeArr(item.opinions_inhabitants) + const canClearnet = clearnetEligible(item) + const isClearnet = canClearnet && !!item.clearnet + + const chips = [ + item.visibility === "HIDDEN" ? renderVisibilityChip("HIDDEN", i18n) : null, + renderTypeChip(item), + renderStatusChip(item.status), + requested ? renderRequestedChip() : null, + renderLifespanChip(item.lifetime, i18n), + renderEcoTax(item.msgSize, item.id), + renderReachChip(isClearnet, i18n, isClearnet ? housingClearnetHref(item) : null), + isAuthor && canClearnet ? renderClearnetSwitch("housing", item.rootId || item.id, isClearnet) : null + ].filter(Boolean) + + const sideActions = [] + const requestToggle = renderRequestToggle(item, returnTo) + if (requestToggle) sideActions.push(requestToggle) + const ownerActions = renderOwnerActions(item, returnTo) + + const nextVisibility = item.visibility === "PUBLIC" ? "HIDDEN" : "PUBLIC" + const isHidden = item.visibility === "HIDDEN" + const visibilityRow = isAuthor + ? div({ class: "tribe-side-actions shop-visibility-row housing-visibility-row" }, + span({ class: "card-label" }, `${i18n.visibilityLabel || "Visibility"}: `), + isHidden + ? renderStateChip("encrypted", "🔒", i18n.encryptedChipLabel || "E2E") + : renderStateChip("mutuals", "👁", i18n.visibilityPublic || "PUBLIC"), + form({ method: "POST", action: `/housing/visibility/${encodeURIComponent(item.id)}`, class: "inline-form" }, + input({ type: "hidden", name: "returnTo", value: returnTo }), + input({ type: "hidden", name: "visibility", value: nextVisibility }), + button({ type: "submit", class: "tribe-action-btn" }, + nextVisibility === "PUBLIC" ? (i18n.visibilityMakePublic || "Make public") : (i18n.visibilityMakeHidden || "Make hidden")) + ) + ) + : null + + const cover = imagesOf(item)[0] + const housingSide = div({ class: "tribe-side" }, + div({ class: "card-header activity-card-header" }, + renderContentActions(item.id, `/housing/${encodeURIComponent(item.id)}`, { spread: params.spreads || null, author: item.author, favKind: 'housing', isFavorite: item.isFavorite, reportTitle: item.title, returnTo, deleteAction: isAuthor ? contentDeleteAction("housing", item.id) : null }) + ), + div({ class: "shop-title-row" }, + h2({ class: "tribe-card-title" }, safeText(item.title) || i18n.housingTitle) + ), + chips.length ? div({ class: "card-chips-row" }, ...chips) : null, + renderStarRating(item.opinions, voters.length), + cover && !isVideoEntry(cover) ? img({ src: blobUrl(blobIdOf(cover), 256), class: "tribe-detail-image", alt: "" }) : null, + renderInfoTable(item), + div({ class: "tribe-card-members" }, + span({ class: "tribe-members-count" }, `${i18n.housingRequests}: ${item.requestCount || 0}`) + ), + sideActions.length ? div({ class: "tribe-side-actions" }, ...sideActions) : null, + renderStatusRow(item, returnTo), + ownerActions.length ? div({ class: "tribe-side-actions owner-actions" }, ...ownerActions) : null, + visibilityRow, + renderTags(item.tags) + ) + + const renderSection = (titleText, bodyText) => + safeText(bodyText) + ? div({ class: "job-section" }, + h2({ class: "job-section-title" }, titleText), + p({ class: "tribe-side-description" }, ...renderStyledText(bodyText)) + ) + : null + + const housingMain = div({ class: "tribe-main" }, + renderPhotoGallery(item), + renderSection(i18n.housingDescription, item.description), + renderSection(i18n.housingRules, item.rules), + item.mapUrl ? div({ class: "job-section" }, renderMapEmbed(params.mapData, item.mapUrl)) : null, + renderEngagement(item.id, + !isAuthor && item.everRequestedByViewer + ? renderOpinionsVoting("/housing/opinions", item.id, item.opinions, returnTo, voters) + : null, + renderCommentsSection(item.id, returnTo, comments) + ) + ) + + return template( + i18n.housingTitle, + section( + div({ class: "tags-header module-header-line" }, h2(i18n.housingTitle), p(i18n.housingDescriptionText)), + renderFiltersBar(filter, params), + div({ class: "tribe-details" }, housingSide, housingMain) + ) + ) +} + +exports.clearnetHousingView = async (item, params = {}) => { + const { escapeHtml: esc, renderRichText, renderKindTag, renderTagChips, blobUrl: cnBlob, renderClearnetPage } = require("./clearnet_view") + const title = esc(item.title || i18n.housingTitle) + const point = params.mapPoint || null + const media = imagesOf(item).map(entry => ({ src: cnBlob(entry), video: isVideoEntry(entry) })).filter(m => m.src) + const clip = cnBlob(item.video) + if (clip && !media.some(m => m.src === clip)) media.push({ src: clip, video: true }) + const cover = (media.find(m => !m.video) || {}).src || null + const day = (v) => moment(v).format("YYYY/MM/DD") + const rows = [ + [i18n.housingProperty, i18n["housingProperty" + String(item.property_type || "").toUpperCase()] || ""], + [i18n.housingRooms, item.rooms > 0 ? String(item.rooms) : ""], + [i18n.housingSize, item.size > 0 ? `${item.size} m²` : ""], + [i18n.housingCapacity, item.capacity > 0 ? String(item.capacity) : ""], + [i18n.housingAvailableFrom, item.availableFrom ? day(item.availableFrom) : ""], + [i18n.housingAvailableTo, item.availableTo ? day(item.availableTo) : ""] + ].filter(([, v]) => v) + const textSection = (heading, text) => safeText(text) ? `

${esc(heading)}

${renderRichText(text)}
` : "" + const extraCss = ` +.cn-housing-title{color:var(--fg);margin:0 0 16px 0;font-size:32px;font-weight:700} +.cn-housing-meta{display:flex;flex-wrap:wrap;gap:10px;margin-bottom:20px} +.cn-housing-meta-item{background:var(--bg-sub);border:1px solid var(--border);border-radius:6px;padding:8px 14px;font-size:14px;color:var(--fg-soft);display:inline-flex;align-items:center;gap:6px} +.cn-housing-gallery{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:10px;margin:0 0 20px 0} +.cn-housing-gallery img,.cn-housing-gallery video{width:100%;height:auto;display:block;border:1px solid var(--border);border-radius:6px;background:#000} +.cn-housing-info{border-collapse:collapse;margin:0 0 20px 0} +.cn-housing-info td{padding:6px 14px 6px 0;font-size:14px;color:var(--fg-soft);border-bottom:1px solid var(--border)} +.cn-housing-info td:first-child{color:var(--fg-dim);text-transform:uppercase;letter-spacing:1px;font-size:12px} +.cn-housing-section h2{color:var(--fg);font-size:18px;text-transform:uppercase;letter-spacing:2px;margin:24px 0 10px;padding-bottom:6px;border-bottom:1px solid var(--border)} +.cn-housing-text{color:var(--fg-soft);line-height:1.6;font-size:15px;word-break:break-word} +` + const body = ` +

${title}

+
+ ${renderKindTag("housing")} + ${esc(i18n["housingType" + String(item.housing_type || "").toUpperCase()] || item.housing_type || "")} + ${safeText(item.place) ? `📍 ${esc(item.place)}` : ""} + ${point ? `⌖ ${point.title ? `${esc(point.title)} · ` : ""}${point.lat}, ${point.lng}` : ""} + ${item.createdAt ? `📅 ${esc(day(item.createdAt))}` : ""} + ${esc(priceLabel(item))} +
+
+ ${media.length ? `` : ""} + ${rows.length ? `${rows.map(([k, v]) => ``).join("")}
${esc(k)}${esc(v)}
` : ""} + ${textSection(i18n.housingDescription, item.description)} + ${textSection(i18n.housingRules, item.rules)} + ${renderTagChips(item.tags)} +` + return renderClearnetPage({ + title: `${item.title || i18n.housingTitle} | Oasis`, + ogTitle: item.title || i18n.housingTitle, + ogDescription: item.description || "", + ogImage: cover, + extraCss, + body, + hubFeedId: item.author || null + }) +} diff --git a/src/views/identities_view.js b/src/views/identities_view.js new file mode 100644 index 0000000..956b54f --- /dev/null +++ b/src/views/identities_view.js @@ -0,0 +1,74 @@ +/** + * Selector de identidades para los ajustes. + * + * Vive aparte de settings_view.js para que las reescrituras de upstream sobre ese + * fichero no arrastren esto: alli solo hay una linea que llama a renderIdentities(). + * + * No cambia de identidad en caliente. Anota cual sera la del proximo arranque y lo + * dice; el porque esta en src/backend/accounts.js. + */ + +module.exports = ({ i18n, html }) => { + const { form, button, div, h2, p, section, input, span, ul, li, a } = html; + const accounts = require('../backend/accounts.js'); + + const renderIdentities = (activeAccount) => { + const names = accounts.list(); + const feeds = new Map(names.map((n) => [n, accounts.feedIdOf(n)])); + + // dos carpetas con el mismo feed son la misma identidad copiada: publicar desde + // las dos parte el registro y no tiene arreglo, asi que se avisa en la lista + const seen = new Map(); + for (const [n, id] of feeds) { + if (!id) continue; + seen.set(id, (seen.get(id) || 0) + 1); + } + + const row = (name) => { + const id = feeds.get(name); + const isActive = name === activeAccount; + const duplicated = id && seen.get(id) > 1; + return li({ class: "identity-row" }, + div({ class: "identity-info" }, + span({ class: "identity-name" }, name), + span({ class: "identity-feed" }, id ? `${id.slice(0, 18)}…` : "—"), + duplicated ? span({ class: "identity-warn" }, i18n.identitiesSameFeed) : null + ), + isActive + ? span({ class: "identity-active" }, i18n.identitiesActive) + : form({ method: "POST", action: "/settings/identity/use" }, + input({ type: "hidden", name: "account", value: name }), + button({ type: "submit" }, i18n.identitiesUse) + ) + ); + }; + + // en movil el backend lo mantiene vivo un servicio en primer plano, asi que + // cerrar la app no basta para reiniciarlo: hace falta pedirlo + const restart = process.env.OASIS_MOBILE === '1' + ? form({ method: "POST", action: "/settings/identity/restart" }, + button({ type: "submit" }, i18n.identitiesRestart)) + : null; + + return section({ class: "identities" }, + div({ class: "tags-header" }, + h2(i18n.identitiesTitle), + p(i18n.identitiesDescription) + ), + ul({ class: "identity-list" }, names.map(row)), + form({ method: "POST", action: "/settings/identity/create", class: "identity-create" }, + input({ type: "text", name: "account", placeholder: i18n.identitiesNewName, + maxlength: "32", autocomplete: "off" }), + button({ type: "submit" }, i18n.identitiesCreate) + ), + div({ class: "identity-actions" }, + restart, + // sacar o meter una identidad ya existente es cosa del modulo legacy, que + // es el que sabe cifrar el secret con una contraseña + a({ href: "/legacy", class: "identity-legacy" }, i18n.identitiesBackup) + ) + ); + }; + + return { renderIdentities }; +}; diff --git a/src/views/image_view.js b/src/views/image_view.js index 36e74e8..9547b7f 100644 --- a/src/views/image_view.js +++ b/src/views/image_view.js @@ -1,11 +1,14 @@ const { form, button, div, h2, p, section, input, label, br, a, img, span, textarea, select, option } = require("../server/node_modules/hyperaxe"); +const { renderCommentsSection: renderSharedCommentsSection, renderCommentsLink } = require("./comments_view"); const moment = require("../server/node_modules/moment"); -const { template, i18n, renderOpinionsVoting, userLink, renderSpreadButton, renderEcoTax, renderLifespanChip, renderStateChip, renderContentActions } = require("./main_views"); +const { renderLicenseChip, renderLicenseSelect, renderReachChip, renderClearnetSelector, renderClearnetSwitch, renderTribeOriginChip } = require('./clearnet_view'); +const { clearnetItemHref, template, i18n, renderOpinionsVoting, renderEngagement, userLink, renderSpreadButton, renderEcoTax, renderLifespanChip, renderStateChip, renderContentActions, renderSpreadEditWarning, renderModuleStats, moduleIsEmpty, contentDeleteAction, paged, blobUrlOf } = require("./main_views"); const { config } = require("../server/SSB_server.js"); -const { renderUrl } = require("../backend/renderUrl") -const { renderMapLocationVisitLabel } = require("./maps_view"); +const { renderStyledText } = require("../backend/renderStyledText") +const { renderMapLocationVisitLabel, renderMapEmbed } = require("./maps_view"); +const { renderZoomableImage } = require("./gallery_view"); const userId = config.keys.id; @@ -22,18 +25,6 @@ const buildReturnTo = (filter, params = {}) => { return `/images?${parts.join("&")}`; }; -const renderPMButton = (recipient, className = "filter-btn") => { - const r = safeText(recipient); - if (!r) return null; - if (String(r) === String(userId)) return null; - - return form( - { method: "GET", action: "/pm" }, - input({ type: "hidden", name: "recipients", value: r }), - button({ type: "submit", class: className }, i18n.privateMessage) - ); -}; - const renderTags = (tags) => { const list = safeArr(tags).map((t) => String(t || "").trim()).filter(Boolean); return list.length @@ -44,35 +35,13 @@ const renderTags = (tags) => { : null; }; -const renderImageFavoriteToggle = (imgObj, returnTo = "") => - form( - { - method: "POST", - action: imgObj.isFavorite - ? `/images/favorites/remove/${encodeURIComponent(imgObj.key)}` - : `/images/favorites/add/${encodeURIComponent(imgObj.key)}` - }, - returnTo ? input({ type: "hidden", name: "returnTo", value: returnTo }) : null, - button( - { type: "submit", class: "filter-btn" }, - imgObj.isFavorite ? i18n.imageRemoveFavoriteButton : i18n.imageAddFavoriteButton - ) - ); - const renderImageMedia = (imgObj, filter, params = {}) => { - const src = imgObj?.url ? `/blob/${encodeURIComponent(imgObj.url)}` : ""; + const src = imgObj?.url ? blobUrlOf(imgObj.url) : ""; return imgObj?.url ? div( - { class: "image-container", style: "display:flex;gap:8px;align-items:center;flex-wrap:wrap;" }, - a( - { - href: `/images/${encodeURIComponent(imgObj.key)}?filter=${encodeURIComponent(filter || "all")}${ - params.q ? `&q=${encodeURIComponent(params.q)}` : "" - }${params.sort ? `&sort=${encodeURIComponent(params.sort)}` : ""}` - }, - img({ src, alt: imgObj.title || "", class: "media-preview", loading: "lazy" }) - ) + { class: "image-container image-container-row" }, + renderZoomableImage(src, { alt: imgObj.title || "", imgClass: "post-image" }) ) : p(i18n.imageNoFile); }; @@ -82,27 +51,15 @@ const renderImageOwnerActions = (filter, imgObj, params = {}) => { const isAuthor = String(imgObj.author) === String(userId); const hasOpinions = Object.keys(imgObj.opinions || {}).length > 0; - if (!isAuthor) return []; + if (!isAuthor || hasOpinions) return []; - const items = []; - if (!hasOpinions) { - items.push( - form( - { method: "GET", action: `/images/edit/${encodeURIComponent(imgObj.key)}` }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - button({ class: "update-btn", type: "submit" }, i18n.imageUpdateButton) - ) - ); - } - items.push( + return [ form( - { method: "POST", action: `/images/delete/${encodeURIComponent(imgObj.key)}` }, + { method: "GET", action: `/images/edit/${encodeURIComponent(imgObj.key)}` }, input({ type: "hidden", name: "returnTo", value: returnTo }), - button({ class: "delete-btn", type: "submit" }, i18n.imageDeleteButton) + button({ class: "update-btn", type: "submit" }, i18n.imageUpdateButton) ) - ); - - return items; + ]; }; const renderImageList = exports.renderImageList = (images, filter, params = {}) => { @@ -111,58 +68,36 @@ const renderImageList = exports.renderImageList = (images, filter, params = {}) ? images.map((imgObj) => { const commentCount = typeof imgObj.commentCount === "number" ? imgObj.commentCount : 0; const title = safeText(imgObj.title); - const ownerActions = renderImageOwnerActions(filter, imgObj, params); const isOwn = imgObj.author && String(imgObj.author) === String(userId); + const headerActions = typeof params.headerActions === "function" + ? params.headerActions(imgObj) + : imgObj.tribeOrigin + ? renderContentActions(null, imgObj.tribeOrigin.href) + : renderContentActions(imgObj.key, `/images/${encodeURIComponent(imgObj.key)}`, { spread: (params.spreadMap && params.spreadMap.get(imgObj.key)) || params.spreads || null, author: imgObj.author, favKind: 'images', torrentFrom: { blobId: imgObj.url, name: imgObj.title }, isFavorite: imgObj.isFavorite, reportTitle: imgObj.title, returnTo, deleteAction: isOwn ? contentDeleteAction("image", imgObj.key) : null }); + const titleHref = typeof params.headerActions === "function" ? null : imgObj.tribeOrigin ? imgObj.tribeOrigin.href : `/images/${encodeURIComponent(imgObj.key)}`; + const engagement = typeof params.engagement === "function" + ? params.engagement(imgObj) + : imgObj.tribeOrigin ? null : renderEngagement(imgObj.key, + renderOpinionsVoting('/images/opinions', imgObj.key, imgObj.opinions, returnTo, imgObj.opinions_inhabitants), + renderCommentsLink({ href: `/images/${encodeURIComponent(imgObj.key)}`, count: commentCount }) + ); return div( { class: "trending-card image-card" + (isOwn ? " own-content" : "") }, div( { class: "card-header activity-card-header" }, span(), - renderContentActions(imgObj.key, `/images/${encodeURIComponent(imgObj.key)}`) + headerActions ), div( { class: "card-section image-card-body" }, - div( - { class: "bookmark-topbar" }, - div( - { class: "bookmark-topbar-left" }, - renderImageFavoriteToggle(imgObj, returnTo), - renderPMButton(imgObj.author) - ), - ownerActions.length ? div({ class: "bookmark-actions" }, ...ownerActions) : null - ), - title ? h2(title) : null, - (imgObj.meme || imgObj.lifetime) ? div({ class: "card-chips-row" }, - imgObj.meme ? a({ href: "/images?filter=meme", class: "chip-link" }, renderStateChip("mutuals", null, i18n.imageFilterMeme || "MEME")) : null, + div({ class: "shop-title-row" }, title ? h2(titleHref ? a({ href: titleHref, class: "card-title-link" }, title) : title) : null, imgObj.tribeOrigin ? renderTribeOriginChip(imgObj.tribeOrigin) : imgObj.clearnet === true ? renderReachChip(true, i18n, clearnetItemHref('images', imgObj.title, imgObj.key)) : null, renderLicenseChip(imgObj.license), ...(typeof params.titleChips === "function" ? params.titleChips(imgObj) : [])), + imgObj.lifetime ? div({ class: "card-chips-row" }, imgObj.lifetime ? renderLifespanChip(imgObj.lifetime, i18n) : null ) : null, renderImageMedia(imgObj, filter, params), - div( - { class: "card-comments-summary" }, - span({ class: "card-label" }, i18n.voteCommentsLabel + ":"), - span({ class: "card-value" }, String(commentCount)), - br(), - br(), - form( - { method: "GET", action: `/images/${encodeURIComponent(imgObj.key)}` }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - input({ type: "hidden", name: "filter", value: filter || "all" }), - params.q ? input({ type: "hidden", name: "q", value: params.q }) : null, - params.sort ? input({ type: "hidden", name: "sort", value: params.sort }) : null, - button({ type: "submit", class: "filter-btn" }, i18n.voteCommentsForumButton) - ) - ), - div( - { class: "card-comments-summary feed-opinions-section" }, - div( - { class: "comments-count" }, - span({ class: "card-label" }, (i18n.opinionsTitle || "Opinions") + ": "), - span({ class: "card-value" }, String(Object.values(imgObj.opinions || {}).reduce((s, n) => s + (Number(n) || 0), 0))) - ), - renderOpinionsVoting('/images/opinions', imgObj.key, imgObj.opinions, returnTo, imgObj.opinions_inhabitants) - ), - div({ class: "card-spread-left" }, renderSpreadButton(imgObj.key, (params.spreadMap && params.spreadMap.get(imgObj.key)) || params.spreads)), + ...(typeof params.bodyExtra === "function" ? params.bodyExtra(imgObj) : []), + engagement, renderMapLocationVisitLabel(imgObj.mapUrl), br(), (() => { @@ -172,12 +107,12 @@ const renderImageList = exports.renderImageList = (images, filter, params = {}) return p( { class: "card-footer" }, - span({ class: "date-link" }, `${moment(imgObj.createdAt).format("YYYY/MM/DD HH:mm:ss")} ${i18n.performed} `), + span({ class: "date-link" }, `${moment(imgObj.createdAt).format("YYYY/MM/DD HH:mm")}`), userLink(imgObj.author), showUpdated ? span( { class: "votations-comment-date" }, - ` | ${i18n.imageUpdatedAt}: ${moment(imgObj.updatedAt).format("YYYY/MM/DD HH:mm:ss")}` + ` | ${i18n.imageUpdatedAt}: ${moment(imgObj.updatedAt).format("YYYY/MM/DD HH:mm")}` ) : null ); @@ -195,6 +130,7 @@ const renderImageForm = (filter, imageId, imageToEdit, params = {}) => { return div( { class: "div-center image-form" }, + params.spreadWarning || null, form( { action: filter === "edit" ? `/images/update/${encodeURIComponent(imageId)}` : "/images/create", @@ -208,35 +144,27 @@ const renderImageForm = (filter, imageId, imageToEdit, params = {}) => { br(), br(), imageToEdit?.url - ? img({ src: `/blob/${encodeURIComponent(imageToEdit.url)}`, class: "media-preview", alt: imageToEdit?.title || "" }) + ? img({ loading: 'lazy', src: blobUrlOf(imageToEdit.url), class: "media-preview", alt: imageToEdit?.title || "" }) : null, label(i18n.imageTitleLabel), br(), - input({ type: "text", name: "title", placeholder: i18n.imageTitlePlaceholder, value: imageToEdit?.title || "" }), + input({ type: "text", name: "title", maxlength: "100", placeholder: i18n.imageTitlePlaceholder, value: imageToEdit?.title || "" }), br(), label(i18n.imageDescriptionLabel), br(), - textarea({ name: "description", placeholder: i18n.imageDescriptionPlaceholder, rows: "4" }, imageToEdit?.description || ""), + textarea({ maxlength: "5000", name: "description", placeholder: i18n.imageDescriptionPlaceholder, rows: "4" }, imageToEdit?.description || ""), br(), label(i18n.mapLocationTitle || "Map Location"), br(), input({ type: "text", name: "mapUrl", placeholder: i18n.mapUrlPlaceholder || "/maps/MAP_ID", value: imageToEdit?.mapUrl || "" }), br(), - input({ type: "hidden", name: "meme", value: "0" }), label(i18n.imageTagsLabel), br(), input({ type: "text", name: "tags", placeholder: i18n.imageTagsPlaceholder, value: tagsValue }), br(), - label(i18n.imageMemeLabel), - input({ - id: "meme-checkbox", - type: "checkbox", - name: "meme", - value: "1", - class: "meme-checkbox", - ...(imageToEdit?.meme ? { checked: true } : {}) - }), + ...renderLicenseSelect(imageToEdit?.license, i18n), br(), + renderClearnetSelector(filter === "edit" ? !!imageToEdit?.clearnet : false, i18n), br(), button({ type: "submit" }, filter === "edit" ? i18n.imageUpdateButton : i18n.imageCreateButton) ) @@ -251,7 +179,7 @@ const renderGallery = (images) => { images.map((imgObj) => { const src = imgObj.url ? `/image/256/${encodeURIComponent(imgObj.url)}` : ""; return a( - { href: `#img-${encodeURIComponent(imgObj.key)}`, class: "gallery-item" }, + { href: `#img-${encodeURIComponent(imgObj.key)}`, id: `img-${encodeURIComponent(imgObj.key)}-src`, class: "gallery-item" }, img({ src, alt: imgObj.title || "", class: "gallery-image", loading: "lazy" }) ); }) @@ -260,136 +188,73 @@ const renderGallery = (images) => { const renderLightbox = (images) => images.map((imgObj) => { - const src = imgObj.url ? `/blob/${encodeURIComponent(imgObj.url)}` : ""; + const src = imgObj.url ? blobUrlOf(imgObj.url) : ""; return div( { id: `img-${encodeURIComponent(imgObj.key)}`, class: "lightbox" }, - a({ href: "#", class: "lightbox-close" }, "×"), + a({ href: `#img-${encodeURIComponent(imgObj.key)}-src`, class: "lightbox-close" }, "×"), img({ src, class: "lightbox-image", alt: imgObj.title || "" }) ); }); const renderImageCommentsSection = (imageKey, comments = [], returnTo = null) => { - const list = safeArr(comments).filter(c => { - const t = c && c.value && c.value.content && c.value.content.text; - return t && String(t).trim(); + return renderSharedCommentsSection({ + action: `/images/${encodeURIComponent(imageKey)}/comments`, + comments: comments, + returnTo: returnTo }); - const commentsCount = list.length; +}; - return div( - { class: "vote-comments-section" }, - div( - { class: "comments-count" }, - span({ class: "card-label" }, i18n.voteCommentsLabel + ": "), - span({ class: "card-value" }, String(commentsCount)) - ), - div( - { class: "comment-form-wrapper" }, - h2({ class: "comment-form-title" }, i18n.voteNewCommentLabel), - form( - { method: "POST", action: `/images/${encodeURIComponent(imageKey)}/comments`, class: "comment-form", enctype: "multipart/form-data" }, - returnTo ? input({ type: "hidden", name: "returnTo", value: returnTo }) : null, - textarea({ - id: "comment-text", - name: "text", - rows: 4, - class: "comment-textarea", - placeholder: i18n.voteNewCommentPlaceholder - }), - div({ class: "comment-file-upload" }, label(i18n.uploadMedia), input({ type: "file", name: "blob" })), - br(), - button({ type: "submit", class: "comment-submit-btn" }, i18n.voteNewCommentButton) - ) - ), - list.length - ? div( - { class: "comments-list" }, - list.map((c) => { - const author = c?.value?.author || ""; - const ts = c?.value?.timestamp || c?.timestamp; - const absDate = ts ? moment(ts).format("YYYY/MM/DD HH:mm:ss") : ""; - const relDate = ts ? moment(ts).fromNow() : ""; - - const content = c?.value?.content || {}; - const text = content.text || ""; - const threadRoot = content.fork || content.root || null; - - return div( - { class: "votations-comment-card" }, - span( - { class: "created-at" }, - span(i18n.createdBy), - author ? userLink(author) : span("(unknown)"), - absDate ? span(" | ") : "", - absDate ? span({ class: "votations-comment-date" }, absDate) : "", - relDate ? span({ class: "votations-comment-date" }, " | ", i18n.sendTime) : "", - relDate && threadRoot ? a({ href: `/thread/${encodeURIComponent(threadRoot)}#${encodeURIComponent(c.key)}` }, relDate) : "" - ), - p({ class: "votations-comment-text" }, ...renderUrl(text)) - ); - }) - ) - : p({ class: "votations-no-comments" }, i18n.voteNoCommentsYet) - ); +const mediaChipFor = (filter, censusM) => (mode) => { + if (mode === filter) return true; + if (!Array.isArray(censusM)) return true; + if (mode === "top") return censusM.some((x) => !x.tribeOrigin); + if (mode === "gallery") return censusM.length > 0; + if (mode === "mine") return censusM.some((x) => String(x.author) === String(userId)); + if (mode === "recent") return censusM.length > 0; + if (mode === "favorites") return censusM.some((x) => x.isFavorite); + if (mode === "bcs") return censusM.some((x) => String(x.title || "").toUpperCase().startsWith("BCS-")); + return true; }; exports.imageView = async (images, filter = "all", imageId = null, params = {}) => { - const title = - filter === "mine" - ? i18n.imageMineSectionTitle - : filter === "create" - ? i18n.imageCreateSectionTitle - : filter === "edit" - ? i18n.imageUpdateSectionTitle - : filter === "gallery" - ? i18n.imageGallerySectionTitle - : filter === "meme" - ? i18n.imageMemeSectionTitle - : filter === "recent" - ? i18n.imageRecentSectionTitle - : filter === "top" - ? i18n.imageTopSectionTitle - : filter === "favorites" - ? i18n.imageFavoritesSectionTitle - : i18n.imageAllSectionTitle; + if (filter === "edit") params = { ...params, spreadWarning: await renderSpreadEditWarning(imageId) }; + const title = i18n.imageTitle; const q = safeText(params.q || ""); const sort = safeText(params.sort || "recent"); const list = safeArr(images); + const emptyMod = moduleIsEmpty(list, filter, "all", q); + const mediaChip = mediaChipFor(filter, Array.isArray(params.censusList) ? params.censusList : list); const imageToEdit = imageId ? list.find((im) => im.key === imageId) : null; return template( title, section( - div({ class: "tags-header" }, + div({ class: "tags-header module-header-line" }, h2(title), p(i18n.imageDescription) ), - (() => { - const { renderReachChip } = require('./clearnet_view'); - const isClearnet = !!(params.viewerPrefs && params.viewerPrefs.clearnetImages); - return div({ class: "shop-title-row" }, renderReachChip(isClearnet, i18n)); - })(), - br(), div( { class: "filters" }, form( { method: "GET", action: "/images", class: "ui-toolbar ui-toolbar--filters" }, - input({ type: "hidden", name: "q", value: q }), input({ type: "hidden", name: "sort", value: sort }), - button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, i18n.imageFilterAll), - button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, i18n.imageFilterMine), - button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, i18n.imageFilterRecent), - button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, i18n.imageFilterTop), - button( + ...(emptyMod ? [] : [ + button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.imageFilterAll).toUpperCase()), + ...(mediaChip("recent") ? [button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.imageFilterRecent).toUpperCase())] : []), + ...(mediaChip("mine") ? [button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.imageFilterMine).toUpperCase())] : []), + ...(mediaChip("favorites") ? [button( { type: "submit", name: "filter", value: "favorites", class: filter === "favorites" ? "filter-btn active" : "filter-btn" }, - i18n.imageFilterFavorites - ), - button( + String(i18n.imageFilterFavorites).toUpperCase() + )] : []), + + ...(mediaChip("top") ? [button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, String(i18n.imageFilterTop).toUpperCase())] : []), + ...(mediaChip("gallery") ? [button( { type: "submit", name: "filter", value: "gallery", class: filter === "gallery" ? "filter-btn active" : "filter-btn" }, - i18n.imageFilterGallery - ), - button({ type: "submit", name: "filter", value: "meme", class: filter === "meme" ? "filter-btn active" : "filter-btn" }, i18n.imageFilterMeme), + String(i18n.imageFilterGallery).toUpperCase() + )] : []), + ]), button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.imageCreateButton) ) ) @@ -398,8 +263,9 @@ exports.imageView = async (images, filter = "all", imageId = null, params = {}) filter === "create" || filter === "edit" ? renderImageForm(filter, imageId, imageToEdit, { ...params, filter }) : section( - div( - { class: "images-search" }, + emptyMod ? null : div( + { class: "images-search activity-filter-chips activity-toolbar-row" }, + renderModuleStats(list.length), form( { method: "GET", action: "/images", class: "filter-box" }, input({ type: "hidden", name: "filter", value: filter }), @@ -414,70 +280,77 @@ exports.imageView = async (images, filter = "all", imageId = null, params = {}) { class: "filter-box__controls" }, select( { name: "sort", class: "filter-box__select" }, - option({ value: "recent", selected: sort === "recent" }, i18n.imageSortRecent), - option({ value: "oldest", selected: sort === "oldest" }, i18n.imageSortOldest), - option({ value: "top", selected: sort === "top" }, i18n.imageSortTop) + option({ value: "recent", ...(sort === "recent" ? { selected: true } : {})}, i18n.imageSortRecent), + option({ value: "oldest", ...(sort === "oldest" ? { selected: true } : {})}, i18n.imageSortOldest), + option({ value: "top", ...(sort === "top" ? { selected: true } : {})}, i18n.imageSortTop) ), button({ type: "submit", class: "filter-box__button" }, i18n.imageSearchButton) ) ) ), - filter === "gallery" ? renderGallery(list) : div({ class: "images-list" }, renderImageList(list, filter, { q, sort })) + filter === "gallery" ? renderGallery(paged(list)) : div({ class: "images-list" }, renderImageList(paged(list), filter, { q, sort, spreadMap: params.spreadMap })) ) ), - ...(filter === "gallery" ? renderLightbox(list) : []) + ...(filter === "gallery" ? renderLightbox(paged(list)) : []) ); }; exports.singleImageView = async (imageObj, filter = "all", comments = [], params = {}) => { + const mediaChip = mediaChipFor(filter, Array.isArray(params.censusList) ? params.censusList : null); const q = safeText(params.q || ""); const sort = safeText(params.sort || "recent"); const returnTo = safeText(params.returnTo) || buildReturnTo(filter, { q, sort }); const title = safeText(imageObj.title); - const { renderReachChip } = require('./clearnet_view'); - const isClearnet = !!(params.authorPrefs && params.authorPrefs.clearnetImages); + const isAuthor = String(imageObj.author) === String(userId); + const isClearnet = !!imageObj.clearnet; - const memeChip = imageObj.meme - ? a({ href: "/images?filter=meme", class: "chip-link" }, renderStateChip("mutuals", null, i18n.imageFilterMeme || "MEME")) - : null; const chips = [ - memeChip, renderLifespanChip(imageObj.lifetime, i18n), imageObj.sizeBytes ? renderEcoTax(imageObj.sizeBytes, imageObj.key) : null ].filter(Boolean); const ownerActions = renderImageOwnerActions(filter, imageObj, { q, sort }); const sideActions = []; - sideActions.push(renderImageFavoriteToggle(imageObj, returnTo)); - if (imageObj.author && String(imageObj.author) !== String(userId)) { - sideActions.push(renderPMButton(imageObj.author)); - } for (const a of ownerActions) sideActions.push(a); const tagsNode = renderTags(imageObj.tags); + const detailActions = div({ class: "card-header activity-card-header" }, + renderContentActions(imageObj.key, `/images/${encodeURIComponent(imageObj.key)}`, { + author: imageObj.author, + deleteAction: String(imageObj.author) === String(userId) ? contentDeleteAction("image", imageObj.key) : null, + favKind: 'images', torrentFrom: { blobId: imageObj.url, name: imageObj.title }, + isFavorite: imageObj.isFavorite, + spread: params.spreads || null, + returnTo, + reportTitle: imageObj.title + }) + ); + const imageSide = div({ class: "tribe-side" }, div({ class: "shop-title-row" }, title ? h2({ class: "tribe-card-title" }, title) : null, - renderReachChip(isClearnet, i18n) + renderReachChip(isClearnet, i18n, isClearnet ? clearnetItemHref('images', imageObj.title, imageObj.key) : null), + isAuthor ? renderClearnetSwitch('images', imageObj.rootId || imageObj.key, isClearnet) : null, + renderLicenseChip(imageObj.license) ), chips.length ? div({ class: "card-chips-row" }, ...chips) : null, safeText(imageObj.description) - ? p({ class: "tribe-side-description" }, ...renderUrl(imageObj.description)) + ? p({ class: "tribe-side-description" }, ...renderStyledText(imageObj.description)) : null, tagsNode, - div({ class: "card-spread-centered" }, renderSpreadButton(imageObj.key, params.spreads)), - renderMapLocationVisitLabel(imageObj.mapUrl), + renderMapEmbed(params.mapData, imageObj.mapUrl), sideActions.length ? div({ class: "tribe-side-actions" }, ...sideActions) : null ); const imageMain = div({ class: "tribe-main" }, + detailActions, imageObj?.url ? div( { class: "image-container" }, img({ - src: `/blob/${encodeURIComponent(imageObj.url)}`, + src: blobUrlOf(imageObj.url), alt: imageObj.title || "", class: "media-preview", loading: "lazy" @@ -491,24 +364,26 @@ exports.singleImageView = async (imageObj, filter = "all", comments = [], params return p( { class: "card-footer" }, - span({ class: "date-link" }, `${moment(imageObj.createdAt).format("YYYY/MM/DD HH:mm:ss")} ${i18n.performed} `), + span({ class: "date-link" }, `${moment(imageObj.createdAt).format("YYYY/MM/DD HH:mm")}`), userLink(imageObj.author), showUpdated ? span( { class: "votations-comment-date" }, - ` | ${i18n.imageUpdatedAt}: ${moment(imageObj.updatedAt).format("YYYY/MM/DD HH:mm:ss")}` + ` | ${i18n.imageUpdatedAt}: ${moment(imageObj.updatedAt).format("YYYY/MM/DD HH:mm")}` ) : null ); })(), - renderOpinionsVoting('/images/opinions', imageObj.key, imageObj.opinions, returnTo, imageObj.opinions_inhabitants), - renderImageCommentsSection(imageObj.key, comments, returnTo) + renderEngagement(imageObj.key, + renderOpinionsVoting('/images/opinions', imageObj.key, imageObj.opinions, returnTo, imageObj.opinions_inhabitants), + renderImageCommentsSection(imageObj.key, comments, returnTo) + ) ); return template( i18n.imageTitle, section( - div({ class: "tags-header" }, + div({ class: "tags-header module-header-line" }, h2(i18n.imageAllSectionTitle || i18n.imageTitle), p(i18n.imageDescription) ), @@ -516,21 +391,19 @@ exports.singleImageView = async (imageObj, filter = "all", comments = [], params { class: "filters" }, form( { method: "GET", action: "/images", class: "ui-toolbar ui-toolbar--filters" }, - input({ type: "hidden", name: "q", value: q }), input({ type: "hidden", name: "sort", value: sort }), - button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, i18n.imageFilterAll), - button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, i18n.imageFilterMine), - button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, i18n.imageFilterRecent), - button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, i18n.imageFilterTop), - button( + button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.imageFilterAll).toUpperCase()), ...(mediaChip("favorites") ? [button( + ...(mediaChip("recent") ? [button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.imageFilterRecent).toUpperCase())] : []), + ...(mediaChip("mine") ? [button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.imageFilterMine).toUpperCase())] : []), { type: "submit", name: "filter", value: "favorites", class: filter === "favorites" ? "filter-btn active" : "filter-btn" }, - i18n.imageFilterFavorites - ), - button( + String(i18n.imageFilterFavorites).toUpperCase() + )] : []), + + button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, String(i18n.imageFilterTop).toUpperCase()), + ...(mediaChip("gallery") ? [button( { type: "submit", name: "filter", value: "gallery", class: filter === "gallery" ? "filter-btn active" : "filter-btn" }, - i18n.imageFilterGallery - ), - button({ type: "submit", name: "filter", value: "meme", class: filter === "meme" ? "filter-btn active" : "filter-btn" }, i18n.imageFilterMeme), + String(i18n.imageFilterGallery).toUpperCase() + )] : []), button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.imageCreateButton) ) ), diff --git a/src/views/indexing_view.js b/src/views/indexing_view.js index c2d758b..56d3d65 100644 --- a/src/views/indexing_view.js +++ b/src/views/indexing_view.js @@ -4,7 +4,7 @@ const { getConfig } = require('../configs/config-manager.js'); const doctypeString = ''; -exports.indexingView = ({ percent }) => { +exports.indexingView = ({ percent, refresh = 10 }) => { let metaRefresh; try { const cfg = getConfig() || {}; @@ -12,28 +12,23 @@ exports.indexingView = ({ percent }) => { void theme; } catch (_) {} + const known = percent !== null && percent !== undefined && Number.isFinite(Number(percent)); const pct = Math.max(0, Math.min(100, Number(percent) || 0)); const headingText = i18n.indexingTitle || 'Synchronizing'; - const message = i18n.indexingMessage || 'Oasis is trying to syncronize a huge network of inhabitants. Just wait!'; const refreshNote = i18n.indexingRefreshNote || 'This page refreshes every 10 seconds.'; - const editProfileLabel = i18n.indexingEditProfileLink || 'Set up my avatar'; return template( headingText, section( - div({ class: 'tags-header' }, - h2(`❤ ${headingText}`), - p(message) + div({ class: 'tags-header module-header-line' }, + h2(`❤ ${headingText}`) ), div({ class: 'indexing-progress-block' }, - progress({ value: String(pct), max: '100', class: 'indexing-progress' }), - p({ class: 'indexing-percent' }, strong(`${pct.toFixed(1)} %`)), - p({ class: 'indexing-note' }, refreshNote) - ), - div({ class: 'indexing-actions' }, - a({ href: '/profile/edit', class: 'filter-btn welcome-action-primary' }, editProfileLabel), - a({ href: '/modules', class: 'filter-btn' }, i18n.modulesTitle || 'Modules') + p({ class: 'indexing-message' }, `\ud83c\udf34 ${i18n.indexingMessage || 'Oasis is synchronizing the distributed network of nodes. Please wait.'}`), + known ? progress({ value: String(pct), max: '100', class: 'indexing-progress' }) : progress({ max: '100', class: 'indexing-progress' }), + known ? p({ class: 'indexing-percent' }, strong(`${pct.toFixed(1)} %`)) : null, + p({ class: 'indexing-note' }, String(refreshNote).replace('10', String(Number(refresh) || 10))) ) ) - ).replace('', ''); + ).replace('', ``); }; diff --git a/src/views/industry_view.js b/src/views/industry_view.js new file mode 100644 index 0000000..13c7a65 --- /dev/null +++ b/src/views/industry_view.js @@ -0,0 +1,1012 @@ +const { form, button, div, h2, p, section, input, label, textarea, br, a, span, select, option, ul, li, img, video, audio, table, thead, tbody, tr, td, th } = require("../server/node_modules/hyperaxe") +const { renderZoomableImage } = require("./gallery_view") +const { template, i18n, renderOpinionsVoting, renderEngagement, userLink, renderStateChip, renderLifespanChip, renderEcoTax, renderContentActions , renderSpreadEditWarning, renderSubscriptionBox, renderModuleStatsBy, renderCardMetaRow, moduleIsEmpty, paged } = require("./main_views") +const moment = require("../server/node_modules/moment") +const { config } = require("../server/SSB_server.js") +const { renderMapEmbedWithZoom } = require("./maps_view") +const { renderStyledText } = require("../backend/renderStyledText") + +const userId = config.keys.id + +const renderMediaBlob = (value, attrs = {}) => { + if (!value) return null + const s = String(value).trim() + if (!s) return null + if (s.startsWith('&')) return renderZoomableImage(`/blob/${encodeURIComponent(s)}`, { imgClass: attrs.class || 'post-image', alt: attrs.alt || '' }) + const mVideo = s.match(/\[video:[^\]]*\]\(\s*(&[^)\s]+\.sha256)\s*\)/) + if (mVideo) return video({ controls: true, class: attrs.class || 'post-video', src: `/blob/${encodeURIComponent(mVideo[1])}`, preload: 'metadata' }) + const mAudio = s.match(/\[audio:[^\]]*\]\(\s*(&[^)\s]+\.sha256)\s*\)/) + if (mAudio) return audio({ controls: true, class: attrs.class || 'post-audio', src: `/blob/${encodeURIComponent(mAudio[1])}` }) + const mImg = s.match(/!\[[^\]]*\]\(\s*(&[^)\s]+\.sha256)\s*\)/) + if (mImg) return renderZoomableImage(`/blob/${encodeURIComponent(mImg[1])}`, { imgClass: attrs.class || 'post-image', alt: attrs.alt || '' }) + return null +} + +const SECTORS = ["software", "hardware", "agriculture", "textile", "energy", "food", "construction", "media", "services", "other"] +const POLICIES = ["open", "vote", "invite"] + +const FILTERS = [ + { key: "ALL", i18n: "industryFilterAll", title: "industryAllTitle" }, + { key: "RECENT", i18n: "industryFilterRecent", title: "industryRecentTitle" }, + { key: "MINE", i18n: "industryFilterMine", title: "industryMineTitle" }, + { key: "ACTIVE", i18n: "industryFilterActive", title: "industryActiveTitle" }, + { key: "PAUSED", i18n: "industryFilterPaused", title: "industryPausedTitle" }, + { key: "DISSOLVED", i18n: "industryFilterDissolved", title: "industryDissolvedTitle" }, + { key: "BLUEPRINTS", i18n: "industryFilterBlueprints", title: "industryBlueprints" }, + { key: "BUILDS", i18n: "industryFilterBuilds", title: "industryBuilds" }, + { key: "MEMBER", i18n: "industryFilterMember", title: "industryMemberTitle" }, + { key: "RULES", i18n: "industryFilterRules", title: "industryRulesTitle" } +] + +const renderIndustryRules = () => { + const points = [ + i18n.industryRulesIntro, + i18n.industryRulesSteward, + i18n.industryRulesMembership, + i18n.industryRulesQuorum, + i18n.industryRulesMajority, + i18n.industryRulesDecisions, + i18n.industryRulesBlueprints, + i18n.industryRulesBuilds, + i18n.industryRulesContributions, + i18n.industryRulesLaborRate, + i18n.industryRulesShares, + i18n.industryRulesDistribution, + i18n.industryRulesTreasury + ] + return div({ class: "card" }, + h2(i18n.industryRulesTitle || "Rules"), + div({ class: "rules-points" }, + points.filter(Boolean).map((t, i) => + div({ class: "rules-point" }, + span({ class: "rules-point-num" }, String(i + 1)), + span({ class: "rules-point-text" }, t) + ) + ) + ) + ) +} + +const safeArr = (v) => (Array.isArray(v) ? v : []) +const safeText = (v) => String(v || "").trim() + +const sectorLabel = (key) => i18n[`industrySector_${key}`] || key +const policyLabel = (key) => i18n[`industryPolicy_${key}`] || key + +const renderStatusChip = (status) => { + const s = String(status || "ACTIVE").toUpperCase() + const map = { + ACTIVE: ["whole", "🟢", i18n.industryStatusActive || "ACTIVE"], + PAUSED: ["half", "⏸", i18n.industryStatusPaused || "PAUSED"], + DISSOLVED: ["tombstoned", "⚰", i18n.industryStatusDissolved || "DISSOLVED"] + } + const [variant, icon, text] = map[s] || map.ACTIVE + return renderStateChip(variant, icon, text) +} + +const renderStatusBlock = (fc, returnTo) => { + const isMember = safeArr(fc.members).includes(userId) + const status = String(fc.status || "ACTIVE").toUpperCase() + const myPause = safeArr(fc.pauseVoters).includes(userId) + return div({ class: "industry-section industry-status-block" }, + div({ class: "card-chips-row" }, renderStatusChip(status)), + (isMember && status !== "DISSOLVED") + ? [ + p({ class: "industry-hint" }, `${i18n.industryPauseVotes || "Pause votes"}: ${fc.pauseYes || 0}`), + form({ method: "POST", action: `/industry/govern/${encodeURIComponent(fc.id)}` }, + input({ type: "hidden", name: "returnTo", value: returnTo }), + input({ type: "hidden", name: "subject", value: "pause" }), + input({ type: "hidden", name: "ref", value: "" }), + button({ type: "submit", name: "choice", value: myPause ? "no" : "yes", class: "filter-btn" }, myPause ? (i18n.industryResumeButton || "Vote to resume") : (i18n.industryPauseButton || "Vote to pause")) + ) + ] + : null + ) +} + +const renderFacilityList = exports.renderFacilityList = (facilities, filter, spreadMap = new Map()) => { + const list = safeArr(facilities) + if (!list.length) return p(i18n.industryNoFacilitiesFound || "No facilities found.") + return div({ class: "tribe-grid" }, + list.map((fc) => { + const isOwn = fc.steward && String(fc.steward) === String(userId) + const isMember = safeArr(fc.members).includes(userId) + const memberCount = fc.memberCount != null ? fc.memberCount : safeArr(fc.members).length + const href = `/industry/${encodeURIComponent(fc.id)}` + const chips = [ + renderStatusChip(fc.status), + renderStateChip("whole", "🏭", sectorLabel(fc.sector)), + renderStateChip("half", "⚖", policyLabel(fc.membershipPolicy)), + isMember ? renderStateChip("whole", "★", i18n.industryMemberBadge || "MEMBER") : null, + renderLifespanChip(fc.lifetime, i18n), + fc.subscriptionIn === true + ? renderStateChip("mutuals", "✉", i18n.subscriptionOn) + : (fc.subscriptionIn === false ? renderStateChip("closed", "✉", i18n.subscriptionOff) : null) + ].filter(Boolean) + return div({ class: "trending-card tribes-card industry-card" + (isOwn ? " own-content" : "") }, + div({ class: "card-header activity-card-header" }, + span(), + renderContentActions(fc.id || fc.key, href, { spread: spreadMap.get(fc.id || fc.key) || null, author: fc.steward, reportTitle: fc.name, deleteAction: isOwn && memberCount <= 1 ? `/industry/delete/${encodeURIComponent(fc.id)}` : null }) + ), + div({ class: "card-section tribes-card-body" }, + div({ class: "tribe-card-image-wrapper" }, + a({ href }, renderMediaBlob(fc.image, { class: "tribe-card-hero-image" }) || div({ class: "industry-card-noimage" }, span("🏭"))), + isMember + ? form({ method: "GET", action: href, class: "tribe-visit-btn-wrapper" }, + button({ type: "submit", class: "filter-btn" }, String(i18n.industryVisitButton || "VISIT").toUpperCase()) + ) + : null + ), + div({ class: "tribe-card-body" }, + div({ class: "shop-title-row" }, + h2({ class: "tribe-card-title" }, a({ href }, safeText(fc.name) || (i18n.industryTitle || "Industry"))) + ), + div({ class: "card-chips-row" }, ...chips), + fc.description ? p({ class: "tribe-card-description" }, safeText(fc.description).slice(0, 220)) : null, + div({ class: "tribe-card-members" }, + span({ class: "tribe-members-count" }, `${i18n.industryMembers || "Members"}: ${memberCount}`) + ), + ) + ) + ) + }) + ) +} + +const renderFacilityForm = (facility, mode, spreadWarning = null) => { + const fc = facility || {} + const isEdit = mode === "edit" + const returnTo = "/industry?filter=MINE" + const curPolicy = fc.membershipPolicy || "vote" + const curSector = fc.sector || "other" + return div({ class: "div-center industry-form" }, + isEdit ? spreadWarning : null, + form({ + action: isEdit ? `/industry/update/${encodeURIComponent(fc.id)}` : "/industry/create", + method: "POST", + enctype: "multipart/form-data" + }, + input({ type: "hidden", name: "returnTo", value: returnTo }), + label(i18n.industryName || "Name"), + br(), + input({ type: "text", name: "name", required: true, maxlength: "80", placeholder: i18n.industryNamePlaceholder || "Facility name", value: fc.name || "" }), + br(), + label(i18n.industryDescriptionLabel || "Description"), + br(), + textarea({ name: "description", rows: "5", maxlength: "2000", placeholder: i18n.industryDescriptionPlaceholder || "What does this facility produce?" }, fc.description || ""), + br(), + label(i18n.uploadMedia || "Upload media (max-size: 75MB)"), + br(), + input({ type: "file", name: "image" }), + br(), + label(i18n.uploadMedia), + br(), + input({ type: "file", name: "blob" }), + fc.image ? div({ class: "industry-form-media" }, renderMediaBlob(fc.image, { class: "industry-hero-image" })) : null, + br(), + label(i18n.industrySector || "Sector"), + br(), + select({ name: "sector" }, + SECTORS.map((s) => option({ value: s, ...(s === curSector ? { selected: true } : {}) }, sectorLabel(s))) + ), + br(), + label(i18n.mapLocationTitle || "Map Location"), + br(), + input({ type: "text", name: "mapUrl", placeholder: i18n.mapUrlPlaceholder || "/maps/MAP_ID", value: fc.mapUrl || "" }), + br(), + label(i18n.shopTags || "Tags"), + br(), + input({ type: "text", name: "tags", placeholder: i18n.shopTagsPlaceholder || "Enter tags separated by commas", value: safeArr(fc.tags).join(", ") }), + br(), + label(i18n.industryMembershipPolicy || "Membership policy"), + br(), + select({ name: "membershipPolicy" }, + POLICIES.map((pol) => option({ value: pol, ...(pol === curPolicy ? { selected: true } : {}) }, policyLabel(pol))) + ), + br(), + label(i18n.industryQuorum || "Quorum"), + br(), + input({ type: "number", name: "quorum", min: "1", step: "1", value: fc.quorum != null ? fc.quorum : 1 }), + br(), + label(i18n.industryMajority || "Majority"), + br(), + input({ type: "number", name: "majority", min: "0.5", max: "1", step: "0.01", value: fc.majority != null ? fc.majority : 0.5 }), + br(), + label(i18n.industryLaborRate || "Labor rate"), + br(), + input({ type: "number", name: "laborRate", min: "0", step: "0.01", value: fc.laborRate != null ? fc.laborRate : 0 }), + br(), + button({ type: "submit" }, isEdit ? (i18n.industryUpdateButton || "Update facility") : (i18n.industryCreateButton || "Create facility")) + ) + ) +} + +const renderGlobalBlueprints = (blueprints, spreadMap = new Map()) => { + const list = safeArr(blueprints) + if (!list.length) return p(i18n.industryNoBlueprints || "No blueprints yet.") + return div({ class: "industry-blueprints" }, + list.map((bp) => div({ class: "industry-card-wrap" }, + div({ class: "card-header activity-card-header" }, span(), renderContentActions(bp.id, "/industry/blueprint/" + encodeURIComponent(bp.id), { spread: spreadMap.get(bp.id) || null, author: bp.author, reportTitle: bp.name })), + div({ class: "industry-blueprint-card" + (bp.author === userId ? " own-content" : "") }, + div({ class: "card-chips-row" }, + renderStateChip("half", bp.outKind === "digital" ? "💾" : "📦", i18n["industryKind_" + bp.outKind] || bp.outKind), + renderStateChip("half", "⚖", String(bp.license || "copyleft").toUpperCase()) + ), + p({ class: "industry-facility-line" }, span({ class: "industry-meta-label" }, `${i18n.industryFacility || "Facility"}: `), a({ href: `/industry/${encodeURIComponent(bp.facilityId)}` }, safeText(bp.facilityName))), + div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, a({ href: `/industry/blueprint/${encodeURIComponent(bp.id)}` }, safeText(bp.name)))), + bp.image ? renderMediaBlob(bp.image, { class: "post-image" }) : null, + bp.description ? p({ class: "tribe-card-description" }, ...renderStyledText(safeText(bp.description))) : null, + renderBlueprintLaborHours(bp), + safeArr(bp.materials).length ? div({ class: "industry-materials" }, ul({ class: "industry-materials-list" }, bp.materials.map(m => li(`${m.qty || 0} × ${safeText(m.item)}${m.price != null ? ` = ${m.price} ECO` : ""}`)))) : null, + renderBlueprintEstimate(bp, i18n.industryBuildingPrice || "Building price"), + safeArr(bp.skills).length ? p(span({ class: "industry-meta-label" }, `${i18n.industrySkills || "Skills"}: `), bp.skills.join(", ")) : null, + (() => { + const acts = renderBlueprintGovernActions(bp.facilityId, bp, safeArr(bp.members).includes(userId) || bp.author === userId, "/industry?filter=BLUEPRINTS") + return acts.length ? div({ class: "industry-actions" }, ...acts) : null + })() + ) + )) + ) +} + +const renderGovernVoteButton = (facilityId, subject, ref, voters, yes, need, labelKey, fallback, returnTo) => { + const mine = safeArr(voters).includes(userId) + return form({ method: "POST", action: `/industry/govern/${encodeURIComponent(facilityId)}` }, + input({ type: "hidden", name: "returnTo", value: returnTo }), + input({ type: "hidden", name: "subject", value: subject }), + input({ type: "hidden", name: "ref", value: ref }), + button({ type: "submit", name: "choice", value: mine ? "no" : "yes", class: "filter-btn" }, + `${mine ? (i18n.industryRevokeVote || "Revoke") : (i18n[labelKey] || fallback)} (${yes || 0}/${need || 1})`) + ) +} + +const renderBuildGovernActions = (b, isMember, returnTo) => { + const isProposer = b.proposer === userId + const multi = (b.facilityMembers || 1) > 1 + const facilityId = b.facilityId || "" + return [ + (isMember && isProposer && b.updateApproved !== false) + ? form({ method: "GET", action: `/industry/builds/${encodeURIComponent(b.id)}/edit` }, button({ type: "submit", class: "update-btn" }, i18n.industryUpdateButton || "Update")) + : null, + (isMember && isProposer && b.deleteApproved !== false) + ? form({ method: "POST", action: `/industry/builds/${encodeURIComponent(b.id)}/delete` }, button({ type: "submit", class: "delete-btn danger-btn" }, i18n.industryDeleteButton || "Delete")) + : null, + (isMember && multi) ? renderGovernVoteButton(facilityId, "buildUpdate", b.id, b.updateVoters, b.updateYes, b.voteNeed, "industryVoteUpdateButton", "Vote update", returnTo) : null, + (isMember && multi) ? renderGovernVoteButton(facilityId, "buildDelete", b.id, b.deleteVoters, b.deleteYes, b.voteNeed, "industryVoteDeleteButton", "Vote delete", returnTo) : null + ].filter(Boolean) +} + +const renderBuildCard = (b, opts = {}) => { + const href = `/industry/build/${encodeURIComponent(b.id)}` + const card = div({ class: "industry-blueprint-card" + (b.proposer === userId ? " own-content" : "") }, + div({ class: "card-chips-row" }, + renderBuildStatusChip(b.status), + ...((b.blueprintKind && opts.withBlueprintChips !== false) ? [ + renderStateChip("half", b.blueprintKind === "digital" ? "💾" : "📦", i18n["industryKind_" + b.blueprintKind] || b.blueprintKind), + renderStateChip("half", "⚖", String(b.blueprintLicense || "copyleft").toUpperCase()) + ] : []) + ), + opts.withFacility ? p({ class: "industry-facility-line" }, span({ class: "industry-meta-label" }, `${i18n.industryFacility || "Facility"}: `), a({ href: `/industry/${encodeURIComponent(b.facilityId)}` }, safeText(b.facilityName))) : null, + opts.withProposer !== false ? p(span({ class: "industry-meta-label" }, `${i18n.industryProposer || "Proposer"}: `), userLink(b.proposer)) : null, + div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, a({ href }, safeText(b.title)))), + b.blueprintImage ? renderMediaBlob(b.blueprintImage, { class: "post-image" }) : null, + b.notes ? p({ class: "tribe-card-description" }, ...renderStyledText(safeText(b.notes))) : null, + div({ class: "industry-blueprint-meta industry-dates-row" }, + b.startDate ? p(span({ class: "industry-meta-label" }, `${i18n.industryBuildStart || "Start date"}: `), moment(b.startDate).format("YYYY/MM/DD")) : null, + b.endDate ? p(span({ class: "industry-meta-label" }, `${i18n.industryBuildEnd || "End date"}: `), moment(b.endDate).format("YYYY/MM/DD")) : null, + ), + renderBlueprintEstimate(b, i18n.industryFinalPrice || "Final price"), + p(span({ class: "industry-meta-label" }, `${i18n.industryContributions || "Contributions"}: `), String(safeArr(b.contributions).length)), + (opts.actions && opts.actions.length) ? div({ class: "industry-actions" }, ...opts.actions) : null + ) + if (!opts.withActions) return card + return div({ class: "industry-card-wrap" }, + div({ class: "card-header activity-card-header" }, span(), renderContentActions(b.id, href, { spread: opts.spread || null, author: b.proposer, reportTitle: b.title })), + card + ) +} + +const renderGlobalBuilds = (builds, spreadMap = new Map()) => { + const list = safeArr(builds) + if (!list.length) return p(i18n.industryNoBuilds || "No builds yet.") + return div({ class: "industry-blueprints" }, + list.map((b) => renderBuildCard(b, { withFacility: true, withActions: true, spread: spreadMap.get(b.id), actions: renderBuildGovernActions(b, safeArr(b.members).includes(userId) || b.proposer === userId, "/industry?filter=BUILDS") })) + ) +} + +exports.industryView = async (facilitiesOrForm, filter, params = {}) => { + const f = String(filter || "ALL").toUpperCase() + const facilityEditWarning = f === "EDIT" ? await renderSpreadEditWarning((safeArr(facilitiesOrForm)[0] || {}).id) : null + const search = safeText(params.search) + const sectorSel = safeText(params.sector) + const isForm = f === "CREATE" || f === "EDIT" + const isRules = f === "RULES" + const emptyMod = moduleIsEmpty(safeArr(facilitiesOrForm), f, "ALL", search || sectorSel) + const censusI = Array.isArray(params.censusList) ? params.censusList : safeArr(facilitiesOrForm) + const indChip = (x) => { + const m = x.key + if (m === f) return true + if (m === "MINE") return censusI.some(fc => String(fc.steward) === String(userId) || safeArr(fc.members).includes(userId)) + if (m === "ACTIVE" || m === "PAUSED" || m === "DISSOLVED") return censusI.some(fc => String(fc.status || "ACTIVE").toUpperCase() === m) + if (m === "MEMBER") return censusI.some(fc => safeArr(fc.members).includes(userId)) + if (m === "RECENT") return censusI.length > 0 + if (m === "BLUEPRINTS" || m === "BUILDS") return censusI.length > 0 + return true + } + return template( + i18n.industryTitle || "Industry", + section( + div({ class: "tags-header module-header-line" }, + h2(i18n.industryTitle || "Industry"), + p(i18n.industryDescription || "Network-owned production facilities.") + ), + div({ class: "filters" }, + form({ method: "GET", action: "/industry", class: "ui-toolbar ui-toolbar--filters" }, + input({ type: "hidden", name: "search", value: search }), + input({ type: "hidden", name: "sector", value: sectorSel }), + (emptyMod ? [] : FILTERS.filter(indChip).map((x) => button({ type: "submit", name: "filter", value: x.key, class: f === x.key ? "filter-btn active" : "filter-btn" }, String(i18n[x.i18n] || x.key).toUpperCase()))) + .concat(button({ type: "submit", name: "filter", value: "CREATE", class: "create-button" }, i18n.industryCreateFacility || "Create facility")) + ) + ), + isRules + ? renderIndustryRules() + : isForm + ? renderFacilityForm(f === "EDIT" ? (safeArr(facilitiesOrForm)[0] || {}) : {}, f === "EDIT" ? "edit" : "create", facilityEditWarning) + : f === "BLUEPRINTS" + ? div({ class: "industry-list" }, renderGlobalBlueprints(paged(facilitiesOrForm), params.spreadMap || new Map())) + : f === "BUILDS" + ? div({ class: "industry-list" }, renderGlobalBuilds(paged(facilitiesOrForm), params.spreadMap || new Map())) + : section( + emptyMod ? null : div({ class: "industry-search activity-filter-chips activity-toolbar-row" }, + renderModuleStatsBy(facilitiesOrForm, fc => String(fc.status || 'ACTIVE').toUpperCase(), [{ value: 'ACTIVE', label: i18n.industryStatusActive }, { value: 'PAUSED', label: i18n.industryStatusPaused }, { value: 'DISSOLVED', label: i18n.industryStatusDissolved }]), + form({ method: "GET", action: "/industry", class: "filter-box" }, + input({ type: "hidden", name: "filter", value: f || "ALL" }), + input({ type: "text", name: "search", value: search, placeholder: i18n.industrySearchPlaceholder || "Search facilities…", class: "filter-box__input" }), + div({ class: "filter-box__controls" }, + select({ name: "sector", class: "filter-box__number" }, + [option({ value: "", ...(sectorSel ? {} : { selected: true }) }, i18n.industryAllSectors || "All sectors")] + .concat(SECTORS.map((s) => option({ value: s, ...(s === sectorSel ? { selected: true } : {}) }, sectorLabel(s)))) + ), + button({ type: "submit", class: "filter-box__button" }, i18n.searchButton) + ) + ) + ), + div({ class: "industry-list" }, renderFacilityList(paged(facilitiesOrForm), f, params.spreadMap)) + ) + ) + ) +} + +const membershipActions = (fc, returnTo) => { + const isSteward = fc.steward === userId + const isMember = safeArr(fc.members).includes(userId) + const status = String(fc.status || "ACTIVE").toUpperCase() + const policy = fc.membershipPolicy + const invited = safeArr(fc.invites).includes(userId) + const rows = [] + if (!isMember && status !== "DISSOLVED") { + if (policy === "vote") { + rows.push(form({ method: "POST", action: `/industry/apply/${encodeURIComponent(fc.id)}` }, + input({ type: "hidden", name: "returnTo", value: returnTo }), + button({ type: "submit", class: "subscribe-btn" }, i18n.industryApplyButton || "Request to join") + )) + } else if (policy === "invite") { + rows.push(invited + ? form({ method: "POST", action: `/industry/join/${encodeURIComponent(fc.id)}` }, + input({ type: "hidden", name: "returnTo", value: returnTo }), + button({ type: "submit", class: "subscribe-btn" }, i18n.industryJoinButton || "Join") + ) + : p({ class: "industry-hint" }, i18n.industryInviteNeeded || "You need an invitation to join.") + ) + } else { + rows.push(form({ method: "POST", action: `/industry/join/${encodeURIComponent(fc.id)}` }, + input({ type: "hidden", name: "returnTo", value: returnTo }), + button({ type: "submit", class: "subscribe-btn" }, i18n.industryJoinButton || "Join") + )) + } + } + if (isMember && !isSteward) { + rows.push(form({ method: "POST", action: `/industry/leave/${encodeURIComponent(fc.id)}` }, + input({ type: "hidden", name: "returnTo", value: returnTo }), + button({ type: "submit", class: "unsubscribe-btn" }, i18n.industryLeaveButton || "Leave") + )) + } + return rows +} + +const renderInviteSection = (fc, returnTo) => { + if (!safeArr(fc.members).includes(userId) || String(fc.status || "ACTIVE").toUpperCase() === "DISSOLVED") return null + if (fc.membershipPolicy !== "invite") return null + return div({ class: "industry-section" }, + h2(i18n.industryInviteButton || "Invite"), + form({ method: "POST", action: `/industry/invite/${encodeURIComponent(fc.id)}`, class: "industry-invite-form" }, + input({ type: "hidden", name: "returnTo", value: returnTo }), + input({ type: "text", name: "invitee", required: true, placeholder: i18n.industryInvitePlaceholder || "Oasis ID (@...ed25519)" }), + button({ type: "submit", class: "create-button" }, i18n.industryCreateInvite || "Generate invite") + ) + ) +} + +const renderPendingApplicants = (fc, returnTo) => { + const isMember = safeArr(fc.members).includes(userId) + const pending = safeArr(fc.pendingApplicants) + if (!isMember || !pending.length) return null + return div({ class: "industry-section" }, + h2(i18n.industryPendingApplicants || "Pending applicants"), + ul({ class: "industry-applicants" }, + pending.map((ap) => li( + userLink(ap.id), + span({ class: "industry-vote-count" }, ` (${i18n.industryVotesYes || "yes"}: ${ap.yes} / ${i18n.industryVotesNo || "no"}: ${ap.no})`), + ap.voters.includes(userId) + ? span({ class: "industry-hint" }, ` — ${i18n.industryAlreadyVoted || "voted"}`) + : form({ method: "POST", action: `/industry/govern/${encodeURIComponent(fc.id)}`, class: "industry-vote-form" }, + input({ type: "hidden", name: "returnTo", value: returnTo }), + input({ type: "hidden", name: "subject", value: "admit" }), + input({ type: "hidden", name: "ref", value: ap.id }), + button({ type: "submit", name: "choice", value: "yes", class: "subscribe-btn" }, i18n.industryAdmit || "Admit"), + button({ type: "submit", name: "choice", value: "no", class: "unsubscribe-btn" }, i18n.industryReject || "Reject") + ) + )) + ) + ) +} + +const renderDissolveBlock = (fc, returnTo) => { + const isMember = safeArr(fc.members).includes(userId) + const status = String(fc.status || "ACTIVE").toUpperCase() + const memberCount = fc.memberCount != null ? fc.memberCount : safeArr(fc.members).length + if (!isMember || status === "DISSOLVED" || memberCount <= 1) return null + return div({ class: "industry-section" }, + h2(i18n.industryDissolveTitle || "Dissolve facility"), + form({ method: "POST", action: `/industry/govern/${encodeURIComponent(fc.id)}` }, + input({ type: "hidden", name: "returnTo", value: returnTo }), + input({ type: "hidden", name: "subject", value: "dissolve" }), + input({ type: "hidden", name: "ref", value: "" }), + button({ type: "submit", name: "choice", value: "yes", class: "unsubscribe-btn danger-btn" }, i18n.industryDissolveVote || "Vote to dissolve") + ) + ) +} + +const BUILD_STATUSES = ["PROPOSED", "APPROVED", "STOCKING", "IN_PRODUCTION", "COMPLETED", "FAILED"] +const BUILD_TRANSITIONS = { APPROVED: ["STOCKING", "FAILED"], STOCKING: ["IN_PRODUCTION", "FAILED"], IN_PRODUCTION: ["COMPLETED", "FAILED"], FAILED: ["APPROVED", "STOCKING", "IN_PRODUCTION"], COMPLETED: ["IN_PRODUCTION"] } +const CONTRIBUTABLE_VIEW = new Set(["APPROVED", "STOCKING", "IN_PRODUCTION"]) +const buildStatusLabel = (s) => i18n["industryBuildStatus_" + String(s || "").toUpperCase()] || s + +const renderBuildStatusChip = (status) => { + const s = String(status || "PROPOSED").toUpperCase() + const map = { PROPOSED: ["half", "📋"], REJECTED: ["tombstoned", "🚫"], APPROVED: ["whole", "✅"], STOCKING: ["half", "📦"], IN_PRODUCTION: ["half", "⚙"], COMPLETED: ["whole", "🏁"], FAILED: ["tombstoned", "✗"] } + const [v, ic] = map[s] || map.PROPOSED + return renderStateChip(v, ic, buildStatusLabel(s)) +} + +const fmtEco = (n) => `${Number(n || 0).toFixed(2)} ECO` + +const renderBlueprintLaborHours = (bp) => { + if (bp.laborHours == null) return null + return p({ class: "industry-labor-hours" }, span({ class: "industry-meta-label" }, `${i18n.industryLaborHours || "Labor (hours)"}: `), `${bp.laborHours || 0}h`) +} + +const renderBlueprintEstimate = (bp, totalLabel) => { + if (bp.estTotal == null) return null + return div({ class: "industry-estimate" }, + p(span({ class: "industry-meta-label" }, `${i18n.industryEstMaterials || "Total Materials"}: `), fmtEco(bp.estMaterialsCost)), + p(span({ class: "industry-meta-label" }, `${i18n.industryEstLabor || "Total Labor"}: `), fmtEco(bp.estLaborCost)), + br(), + p({ class: "industry-estimate-total" }, span({ class: "industry-meta-label" }, `${totalLabel || i18n.industryEstTotal || "Estimated price"}: `), fmtEco(bp.estTotal)) + ) +} + +const renderBlueprintGovernActions = (facilityId, bp, isMember, returnTo) => { + const isAuthor = bp.author === userId + const multi = (bp.facilityMembers || 1) > 1 + const ref = bp.root || bp.id + const voteBtn = (subject, voters, yes, labelKey, fallback) => { + const mine = safeArr(voters).includes(userId) + return form({ method: "POST", action: `/industry/govern/${encodeURIComponent(facilityId)}` }, + input({ type: "hidden", name: "returnTo", value: returnTo }), + input({ type: "hidden", name: "subject", value: subject }), + input({ type: "hidden", name: "ref", value: ref }), + button({ type: "submit", name: "choice", value: mine ? "no" : "yes", class: "filter-btn" }, + `${mine ? (i18n.industryRevokeVote || "Revoke") : (i18n[labelKey] || fallback)} (${yes || 0}/${bp.voteNeed || 1})`) + ) + } + return [ + (isMember && isAuthor && bp.updateApproved !== false) + ? form({ method: "GET", action: `/industry/blueprints/${encodeURIComponent(bp.id)}/edit` }, button({ type: "submit", class: "update-btn" }, i18n.industryUpdateButton || "Update")) + : null, + (isMember && isAuthor && bp.deleteApproved !== false) + ? form({ method: "POST", action: `/industry/blueprints/${encodeURIComponent(bp.id)}/delete` }, button({ type: "submit", class: "delete-btn danger-btn" }, i18n.industryDeleteButton || "Delete")) + : null, + (isMember && multi) ? voteBtn("bpUpdate", bp.updateVoters, bp.updateYes, "industryVoteUpdateButton", "Vote update") : null, + (isMember && multi) ? voteBtn("bpDelete", bp.deleteVoters, bp.deleteYes, "industryVoteDeleteButton", "Vote delete") : null + ].filter(Boolean) +} + +const renderBlueprintsSection = (fc, blueprints, isMember, spreadMap = new Map()) => { + const list = safeArr(blueprints) + const cards = list.map((bp) => { + const actions = renderBlueprintGovernActions(fc.id, bp, isMember, `/industry/${encodeURIComponent(fc.id)}`) + return div({ class: "industry-card-wrap" }, + div({ class: "card-header activity-card-header" }, span(), renderContentActions(bp.id, `/industry/blueprint/${encodeURIComponent(bp.id)}`, { spread: spreadMap.get(bp.id) || null, author: bp.author, reportTitle: bp.name })), + div({ class: "industry-blueprint-card" }, + div({ class: "card-chips-row" }, + renderStateChip("half", bp.outKind === "digital" ? "💾" : "📦", i18n["industryKind_" + bp.outKind] || bp.outKind), + renderStateChip("half", "⚖", String(bp.license || "copyleft").toUpperCase()) + ), + div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, safeText(bp.name))), + bp.image ? renderMediaBlob(bp.image, { class: "post-image" }) : null, + bp.description ? p({ class: "tribe-card-description" }, ...renderStyledText(safeText(bp.description))) : (bp.outItem ? p(span({ class: "industry-meta-label" }, `${i18n.industryOutput || "Output"}: `), `${bp.outQty || 0} × ${safeText(bp.outItem)}`) : null), + renderBlueprintLaborHours(bp), + safeArr(bp.materials).length ? div({ class: "industry-materials" }, ul({ class: "industry-materials-list" }, bp.materials.map(m => li(`${m.qty || 0} × ${safeText(m.item)}${m.price != null ? ` = ${m.price} ECO` : ""}`)))) : null, + renderBlueprintEstimate(bp, i18n.industryBuildingPrice || "Building price"), + safeArr(bp.skills).length ? p(span({ class: "industry-meta-label" }, `${i18n.industrySkills || "Skills"}: `), bp.skills.join(", ")) : null, + actions.length ? div({ class: "industry-actions" }, ...actions) : null + ) + ) + }) + return div({ class: "industry-section" }, + h2(i18n.industryBlueprints || "Blueprints"), + list.length ? div({ class: "industry-blueprints" }, ...cards) : p({ class: "industry-hint" }, i18n.industryNoBlueprints || "No blueprints yet."), + isMember ? renderBlueprintForm(fc, {}, "create") : null + ) +} + +const renderBlueprintForm = (fc, bp, mode, spreadWarning = null) => { + const isEdit = mode === "edit" + const curKind = bp.outKind || "physical" + const materialsText = safeArr(bp.materials).map(m => `${m.item}:${m.qty}:${m.price != null ? m.price : 0}`).join("\n") + return div({ class: "industry-form industry-blueprint-form" }, + h2(isEdit ? (i18n.industryEditBlueprint || "Edit blueprint") : (i18n.industryNewBlueprint || "New blueprint")), + isEdit ? spreadWarning : null, + form({ method: "POST", action: isEdit ? `/industry/blueprints/${encodeURIComponent(bp.id)}/update` : `/industry/${encodeURIComponent(fc.id)}/blueprints`, enctype: "multipart/form-data" }, + label(i18n.industryName || "Name"), + br(), input({ type: "text", name: "name", required: true, maxlength: "80", value: bp.name || "" }), br(), + label(i18n.industryDescriptionLabel || "Description"), + br(), textarea({ name: "description", rows: "4", maxlength: "2000", placeholder: i18n.industryBlueprintDescPlaceholder || "Specs, dimensions, weight, docs…" }, bp.description || ""), br(), + label(i18n.uploadMedia || "Upload media (max-size: 75MB)"), + br(), input({ type: "file", name: "image" }), + bp.image ? div({ class: "industry-form-media" }, renderMediaBlob(bp.image, { class: "post-image" })) : null, + br(), + label(i18n.industryOutputKind || "Product type"), + br(), select({ name: "outKind" }, ["physical", "digital"].map(k => option({ value: k, ...(k === curKind ? { selected: true } : {}) }, i18n["industryKind_" + k] || k))), br(), + label(i18n.industryMaterials || "Materials"), + br(), textarea({ maxlength: "5000", name: "materialsText", rows: "3", placeholder: i18n.industryMaterialsPlaceholder || "solar-panel:2:120\nbattery:1:80\nkit:1:15" }, materialsText), br(), + label(i18n.industryLaborHours || "Labor hours"), + br(), input({ type: "number", name: "laborHours", min: "0", step: "0.01", value: bp.laborHours != null ? bp.laborHours : "" }), br(), + button({ type: "submit" }, isEdit ? (i18n.industryUpdateButton || "Update") : (i18n.industryCreateBlueprintButton || "Create blueprint")) + ) + ) +} + +const renderBuildForm = (fc, blueprints, build, mode, spreadWarning = null) => { + const b = build || {} + const isEdit = mode === "edit" + const today = moment().format("YYYY-MM-DD") + return div({ class: "industry-form industry-build-form" }, + h2(isEdit ? (i18n.industryEditBuild || "Edit build") : (i18n.industryNewBuild || "Propose a build")), + isEdit ? spreadWarning : null, + form({ method: "POST", action: isEdit ? `/industry/builds/${encodeURIComponent(b.id)}/update` : `/industry/${encodeURIComponent(fc.id)}/builds`, enctype: "multipart/form-data" }, + label(i18n.industryBuildTitle || "Title"), + br(), input({ type: "text", name: "title", required: true, maxlength: "120", value: safeText(b.title) }), br(), + label(i18n.industryBlueprint || "Blueprint"), + br(), isEdit + ? input({ type: "text", disabled: true, value: safeText(b.blueprintName) }) + : select({ name: "blueprintId", required: true }, safeArr(blueprints).map(bp => option({ value: bp.id }, safeText(bp.name)))), br(), + label(i18n.industryBuildStart || "Start date"), + br(), input({ type: "date", name: "startDate", required: true, value: b.startDate || "", min: today }), br(), + label(i18n.industryBuildEnd || "End date"), + br(), input({ type: "date", name: "endDate", required: true, value: b.endDate || "", min: today }), br(), + label(i18n.industryBuildNotes || "Notes"), + br(), textarea({ name: "notes", rows: "3", maxlength: "1000" }, safeText(b.notes)), br(), + button({ type: "submit" }, isEdit ? (i18n.industryUpdateButton || "Update") : (i18n.industryProposeBuildButton || "Propose build")) + ) + ) +} + +const renderBuildsSection = (fc, builds, blueprints, isMember, spreadMap = new Map()) => { + const list = safeArr(builds) + return div({ class: "industry-section" }, + h2(i18n.industryBuilds || "Builds"), + list.length ? div({ class: "industry-blueprints" }, ...list.map((b) => renderBuildCard(b, { withActions: true, withBlueprintChips: false, withProposer: false, spread: spreadMap.get(b.id), actions: renderBuildGovernActions(b, isMember, `/industry/${encodeURIComponent(fc.id)}`) }))) : p({ class: "industry-hint" }, i18n.industryNoBuilds || "No builds yet."), + (isMember && fc.status === "ACTIVE" && !safeArr(blueprints).length) + ? p({ class: "industry-hint" }, i18n.industryNeedBlueprint || "Create a blueprint first: every build produces one.") + : null, + (isMember && fc.status === "ACTIVE" && safeArr(blueprints).length) ? renderBuildForm(fc, blueprints, {}, "create") : null + ) +} + +const renderFacilityJobsSection = (fc, jobs) => { + const list = safeArr(jobs) + if (!list.length) return null + const rows = list.map((j) => li( + a({ href: `/jobs/${encodeURIComponent(j.id || j.key)}` }, safeText(j.title) || (i18n.jobsTitle || "Job")), + j.salary ? span({ class: "industry-hint" }, ` · ${j.salary} ECO · `) : span({ class: "industry-hint" }, " · "), + renderStateChip(String(j.status || "OPEN").toUpperCase() === "OPEN" ? "whole" : "tombstoned", "", String(j.status || "OPEN").toUpperCase()) + )) + return div({ class: "industry-section" }, + h2(i18n.industryJobs || "Jobs"), + ul({ class: "industry-jobs-list" }, ...rows) + ) +} + +const renderFacilitySide = (fc, returnTo, params = {}) => { + const isSteward = fc.steward === userId + const isMember = safeArr(fc.members).includes(userId) + const isOwner = String(fc.steward || fc.author) === String(userId) + const memberCount = fc.memberCount != null ? fc.memberCount : safeArr(fc.members).length + + const chips = [ + renderStateChip("whole", "🏭", sectorLabel(fc.sector)), + renderStateChip("half", "⚖", policyLabel(fc.membershipPolicy)), + renderLifespanChip(fc.lifetime, i18n), + renderEcoTax(fc.msgSize, fc.id || fc.key), + (isSteward || isMember) + ? ((isOwner || (fc.subscription && fc.subscription.subscribed === true)) + ? renderStateChip("mutuals", "✉", i18n.subscriptionOn) + : renderStateChip("closed", "✉", i18n.subscriptionOff)) + : null + ].filter(Boolean) + + const sideActions = [] + for (const act of membershipActions(fc, returnTo)) sideActions.push(act) + if (!isSteward && fc.steward) { + sideActions.push(a({ href: `/reports?filter=create&category=ABUSE&title=${encodeURIComponent(`${i18n.industryTitle || "Industry"}: ${safeText(fc.name)}`)}`, class: "filter-btn" }, i18n.industryReportButton || "Report")) + } + if (isSteward) { + sideActions.push(form({ method: "GET", action: `/industry/edit/${encodeURIComponent(fc.id)}` }, + button({ type: "submit", class: "tribe-action-btn" }, i18n.industryUpdateButton || "Update") + )) + } + + return div({ class: "tribe-side" }, + div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, a({ href: `/industry/${encodeURIComponent(fc.id)}` }, safeText(fc.name) || (i18n.industryTitle || "Industry")))), + div({ class: "card-chips-row" }, ...chips), + fc.image ? renderMediaBlob(fc.image, { class: "tribe-detail-image" }) : null, + fc.description ? p({ class: "tribe-side-description" }, ...renderStyledText(safeText(fc.description))) : null, + table({ class: "tribe-info-table" }, + tr(td({ class: "tribe-info-label" }, i18n.industryLaborRate || "Labor rate"), td({ class: "tribe-info-value", colspan: "3" }, `${fc.laborRate || 0} ECO/h`)), + tr(td({ class: "tribe-info-label" }, i18n.industryQuorum || "Quorum"), td({ class: "tribe-info-value", colspan: "3" }, String(fc.quorum))), + tr(td({ class: "tribe-info-label" }, i18n.industryMajority || "Majority"), td({ class: "tribe-info-value", colspan: "3" }, `${Math.round((fc.majority || 0.5) * 100)}%`)), + tr(td({ class: "tribe-info-value tribe-author-cell", colspan: "4" }, userLink(fc.steward))) + ), + fc.mapUrl ? renderMapEmbedWithZoom(params.mapData, fc.mapUrl, returnTo, params.zoom) : null, + safeArr(fc.tags).length + ? div({ class: "tribe-side-tags" }, safeArr(fc.tags).map(tag => a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link" }, `#${tag}`))) + : null, + renderStatusBlock(fc, returnTo), + h2({ class: "tribe-members-count" }, `${i18n.industryMembers || "Members"}: ${memberCount}`), + renderInviteSection(fc, returnTo), + sideActions.length ? div({ class: "tribe-side-actions" }, ...sideActions) : null, + (fc.subscription && (isSteward || isMember)) + ? renderSubscriptionBox({ + target: fc.rootId || fc.id, + scope: "industry", + subscribed: fc.subscription.subscribed === true, + count: fc.subscription.count, + isOwner: isOwner, + returnTo: returnTo + }) + : null, + renderDissolveBlock(fc, returnTo) + ) +} + +exports.singleFacilityView = async (facility, filter, params = {}) => { + const fc = facility || {} + const f = String(filter || "ALL").toUpperCase() + const returnTo = `/industry/${encodeURIComponent(fc.id)}?filter=${encodeURIComponent(f)}` + const isMember = safeArr(fc.members).includes(userId) + const memberCount = fc.memberCount != null ? fc.memberCount : safeArr(fc.members).length + const facilitySide = renderFacilitySide(fc, returnTo, params) + + const facilityMain = div({ class: "tribe-main" }, + renderBlueprintsSection(fc, params.blueprints, isMember, params.childSpreadMap || new Map()), + renderBuildsSection(fc, params.builds, params.blueprints, isMember, params.childSpreadMap || new Map()), + renderFacilityJobsSection(fc, params.facilityJobs), + renderCardMetaRow(renderOpinionsVoting('/industry/opinions', fc.id || fc.key, fc.opinions, null, fc.opinions_inhabitants)), + div({ class: "industry-section" }, + h2(i18n.industryMembers || "Members"), + ul({ class: "industry-members-list" }, safeArr(fc.members).map((mid) => li( + userLink(mid), + span({ class: "industry-role" }, ` · ${mid === fc.steward ? (i18n.industrySteward || "Steward") : (i18n.industryMember || "Member")}`) + ))) + ), + renderPendingApplicants(fc, returnTo) + ) + + return template( + i18n.industryTitle || "Industry", + section( + div({ class: "card-header activity-card-header" }, + renderContentActions(fc.id || fc.key, `/industry/${encodeURIComponent(fc.id)}`, { spread: params.spreads || null, author: fc.steward, reportTitle: fc.name, deleteAction: fc.steward === userId && memberCount <= 1 ? `/industry/delete/${encodeURIComponent(fc.id)}` : null }) + ), + div({ class: "tags-header module-header-line" }, h2(i18n.industryTitle || "Industry"), p(i18n.industryDescription || "Network-owned production facilities.")), + div({ class: "filters" }, + form({ method: "GET", action: "/industry", class: "ui-toolbar ui-toolbar--filters" }, + FILTERS.map((x) => button({ type: "submit", name: "filter", value: x.key, class: f === x.key ? "filter-btn active" : "filter-btn" }, String(i18n[x.i18n] || x.key).toUpperCase())) + .concat(button({ type: "submit", name: "filter", value: "CREATE", class: "create-button" }, i18n.industryCreateFacility || "Create facility")) + ) + ), + div({ class: "tribe-details" }, facilitySide, facilityMain) + ) + ) +} + +exports.blueprintEditView = async (bp, fc) => template( + i18n.industryTitle || "Industry", + section( + div({ class: "card-header activity-card-header" }, + renderContentActions(bp.id, `/industry/blueprint/${encodeURIComponent(bp.id)}`, { author: bp.author, reportTitle: bp.name }) + ), + div({ class: "tags-header module-header-line" }, h2(i18n.industryTitle || "Industry"), p(i18n.industryDescription || "Network-owned production facilities.")), + div({ class: "filters" }, + form({ method: "GET", action: "/industry", class: "ui-toolbar ui-toolbar--filters" }, + FILTERS.map((x) => button({ type: "submit", name: "filter", value: x.key, class: "filter-btn" }, String(i18n[x.i18n] || x.key).toUpperCase())) + .concat(button({ type: "submit", name: "filter", value: "CREATE", class: "create-button" }, i18n.industryCreateFacility || "Create facility")) + ) + ), + renderBlueprintForm(fc, bp, "edit", await renderSpreadEditWarning(bp && bp.id)) + ) +) + +exports.buildEditView = async (b, fc) => template( + i18n.industryTitle || "Industry", + section( + div({ class: "card-header activity-card-header" }, + renderContentActions(b.id, `/industry/build/${encodeURIComponent(b.id)}`, { author: b.proposer, reportTitle: b.title }) + ), + div({ class: "tags-header module-header-line" }, h2(i18n.industryTitle || "Industry"), p(i18n.industryDescription || "Network-owned production facilities.")), + div({ class: "filters" }, + form({ method: "GET", action: "/industry", class: "ui-toolbar ui-toolbar--filters" }, + FILTERS.map((x) => button({ type: "submit", name: "filter", value: x.key, class: "filter-btn" }, String(i18n[x.i18n] || x.key).toUpperCase())) + .concat(button({ type: "submit", name: "filter", value: "CREATE", class: "create-button" }, i18n.industryCreateFacility || "Create facility")) + ) + ), + renderBuildForm(fc, [], b, "edit", await renderSpreadEditWarning(b && b.id)) + ) +) + +exports.singleBlueprintView = async (blueprint, params = {}) => { + const bp = blueprint || {} + const isMember = safeArr(bp.members).includes(userId) + const facilityHref = `/industry/${encodeURIComponent(bp.facilityId || "")}` + const returnTo = `/industry/blueprint/${encodeURIComponent(bp.id)}` + const actions = renderBlueprintGovernActions(bp.facilityId, bp, isMember, returnTo) + + return template( + i18n.industryTitle || "Industry", + section(div({ class: "tags-header module-header-line" }, h2(i18n.industryTitle), p(i18n.industryDescription))), + section( + div({ class: "filters" }, + form({ method: "GET", action: "/industry", class: "ui-toolbar ui-toolbar--filters" }, + FILTERS.map((x) => button({ type: "submit", name: "filter", value: x.key, class: "filter-btn" }, String(i18n[x.i18n] || x.key).toUpperCase())) + .concat(button({ type: "submit", name: "filter", value: "CREATE", class: "create-button" }, i18n.industryCreateFacility || "Create facility")) + ) + ) + ), + section( + div({ class: "shop-detail" }, + div({ class: "card-header activity-card-header" }, + span(), + renderContentActions(bp.id, returnTo, { spread: params.spreads || null, author: bp.author, reportTitle: bp.name || bp.title }) + ), + div({ class: "card-chips-row" }, + renderStateChip("half", bp.outKind === "digital" ? "💾" : "📦", i18n["industryKind_" + bp.outKind] || bp.outKind), + renderStateChip("half", "⚖", String(bp.license || "copyleft").toUpperCase()) + ), + p({ class: "industry-facility-line" }, span({ class: "industry-meta-label" }, `${i18n.industryFacility || "Facility"}: `), a({ href: facilityHref }, safeText(bp.facilityName))), + h2(safeText(bp.name)), + bp.image ? div({ class: "shop-detail-media" }, renderMediaBlob(bp.image, { class: "post-image" })) : null, + bp.description ? p({ class: "tribe-card-description" }, ...renderStyledText(safeText(bp.description))) : null, + div({ class: "industry-meta" }, + p(span({ class: "industry-meta-label" }, `${i18n.industryAuthor || "Author"}: `), userLink(bp.author)) + ), + renderBlueprintLaborHours(bp), + safeArr(bp.materials).length ? div({ class: "industry-materials" }, ul({ class: "industry-materials-list" }, bp.materials.map(m => li(`${m.qty || 0} × ${safeText(m.item)}${m.price != null ? ` = ${m.price} ECO` : ""}`)))) : null, + renderBlueprintEstimate(bp, i18n.industryBuildingPrice || "Building price"), + safeArr(bp.skills).length ? p(span({ class: "industry-meta-label" }, `${i18n.industrySkills || "Skills"}: `), bp.skills.join(", ")) : null, + actions.length ? div({ class: "industry-actions" }, ...actions) : null, + safeArr(bp.builds).length + ? div({ class: "industry-section" }, + h2(i18n.industryBuilds || "Builds"), + ul({ class: "industry-builds-list" }, bp.builds.map(b => li( + a({ href: `/industry/build/${encodeURIComponent(b.id)}` }, safeText(b.title)), + " ", renderBuildStatusChip(b.status) + ))) + ) + : null + ) + ) + ) +} + +exports.singleBuildView = async (build, params = {}) => { + const b = build || {} + const isMember = safeArr(b.members).includes(userId) + const isSteward = b.steward === userId + const isProposer = b.proposer === userId + const status = String(b.status || "PROPOSED").toUpperCase() + const returnTo = `/industry/build/${encodeURIComponent(b.id)}` + const facilityHref = `/industry/${encodeURIComponent(b.facilityId)}` + + const contribRows = safeArr(b.contributions).map(c => tr( + td(userLink(c.author)), + td(i18n["industryKind_" + c.kind] || c.kind), + td(c.kind === "labor" ? `${c.hours} h` : c.kind === "material" ? `${c.value} ECO · ${safeText(c.item)}` : `${c.eco} ECO`), + td(safeText(c.note)) + )) + + const canUpdateBuild = isMember && (isProposer || isSteward) && !b.distributed && b.updateApproved !== false + const govRows = [] + if ((status === "PROPOSED" || status === "REJECTED") && isMember) { + govRows.push(form({ method: "POST", action: `/industry/builds/${encodeURIComponent(b.id)}/vote`, class: "industry-build-vote" }, + input({ type: "hidden", name: "returnTo", value: returnTo }), + button({ type: "submit", name: "choice", value: "yes", class: "subscribe-btn" }, i18n.industryApproveBuild || "Approve"), + button({ type: "submit", name: "choice", value: "no", class: "unsubscribe-btn" }, i18n.industryReject || "Reject") + )) + } + const isReopen = status === "COMPLETED" || status === "FAILED" + const lockedByDistribution = status === "COMPLETED" && b.distributed + const transitions = ((!isMember || lockedByDistribution) ? [] : (BUILD_TRANSITIONS[status] || [])).filter(nextS => { + const stewardOnly = nextS === "COMPLETED" || nextS === "FAILED" || isReopen + return isSteward || (isProposer && !stewardOnly) + }) + if (transitions.length) { + govRows.push(form({ method: "POST", action: `/industry/builds/${encodeURIComponent(b.id)}/status`, class: "project-control-form project-control-form--status" }, + input({ type: "hidden", name: "returnTo", value: returnTo }), + select({ name: "status", class: "project-control-select" }, + transitions.map(nextS => option({ value: nextS }, buildStatusLabel(nextS))) + ), + button({ type: "submit", class: "status-btn project-control-btn" }, i18n.industryAdvanceTo || "Set to") + )) + } + if (canUpdateBuild) { + govRows.push(form({ method: "GET", action: `/industry/builds/${encodeURIComponent(b.id)}/edit` }, + button({ type: "submit", class: "update-btn" }, i18n.industryUpdateButton || "Update") + )) + } + if (isMember && isProposer && b.deleteApproved !== false) { + govRows.push(form({ method: "POST", action: `/industry/builds/${encodeURIComponent(b.id)}/delete` }, + button({ type: "submit", class: "delete-btn danger-btn" }, i18n.industryDeleteButton || "Delete") + )) + } + if (isMember && (b.facilityMembers || 1) > 1) { + govRows.push(renderGovernVoteButton(b.facilityId, "buildUpdate", b.id, b.updateVoters, b.updateYes, b.voteNeed, "industryVoteUpdateButton", "Vote update", returnTo)) + govRows.push(renderGovernVoteButton(b.facilityId, "buildDelete", b.id, b.deleteVoters, b.deleteYes, b.voteNeed, "industryVoteDeleteButton", "Vote delete", returnTo)) + } + + const kind = ["labor", "material", "eco"].includes(String(params.kind)) ? String(params.kind) : "labor" + + const contributeAction = `/industry/builds/${encodeURIComponent(b.id)}/contribute` + const contributeBlock = (CONTRIBUTABLE_VIEW.has(status) && isMember) + ? div({ class: "industry-section", id: "contribute" }, + h2(i18n.industryContribute || "Contribute"), + form({ method: "GET", action: `/industry/build/${encodeURIComponent(b.id)}#contribute`, class: "industry-form industry-contrib-kind" }, + label(i18n.industryKindLabel || "Kind"), + select({ name: "kind", class: "project-control-select" }, + option({ value: "labor", ...(kind === "labor" ? { selected: true } : {}) }, i18n.industryKind_labor || "Labor"), + option({ value: "material", ...(kind === "material" ? { selected: true } : {}) }, i18n.industryKind_material || "Material"), + option({ value: "eco", ...(kind === "eco" ? { selected: true } : {}) }, i18n.industryKind_eco || "Funds") + ), + button({ type: "submit", class: "create-button" }, i18n.apply || "Apply") + ), + form({ method: "POST", action: contributeAction, class: "industry-form industry-contrib-form-single" }, + input({ type: "hidden", name: "kind", value: kind }), + kind === "labor" ? [ + label(i18n.industryHours || "Hours"), + input({ type: "number", name: "hours", min: "0.01", step: "0.01", required: true, class: "industry-input-num" }) + ] : null, + kind === "material" ? [ + label(i18n.industryMaterialItem || "Material"), + textarea({ name: "item", rows: "2", maxlength: "80", required: true }), + label(i18n.industryMaterialValue || "Value (ECO)"), + input({ type: "number", name: "value", min: "0.01", step: "0.01", required: true, class: "industry-input-num" }) + ] : null, + kind === "eco" ? [ + label(i18n.industryEcoAmount || "Amount (ECO)"), + input({ type: "number", name: "eco", min: "0.000001", step: "0.000001", required: true, class: "industry-input-num" }) + ] : null, + label(i18n.industryNote || "Note"), + textarea({ name: "note", rows: "2", maxlength: "140" }), + button({ type: "submit", class: "filter-btn" }, i18n.industryContributeButton || "Contribute") + ) + ) + : null + + const distributeBlock = (status === "COMPLETED" && isSteward && !b.distributed) + ? div({ class: "industry-section" }, + h2(i18n.industryDistribute || "Distribute"), + form({ method: "POST", action: `/industry/builds/${encodeURIComponent(b.id)}/distribute` }, + input({ type: "hidden", name: "returnTo", value: returnTo }), + label(i18n.industryOutputValue || "Output value (ECO, e.g. from sale)"), + br(), input({ type: "number", name: "outputValue", min: "0", step: "0.000001", value: "0" }), br(), + button({ type: "submit", class: "subscribe-btn" }, i18n.industryDistributeButton || "Distribute by shares") + ), + p({ class: "industry-hint" }, `${i18n.industryTreasury || "Treasury"}: ${b.treasury || 0} ECO`) + ) + : null + + const allocationBlock = b.distributed && b.allocation + ? div({ class: "industry-section" }, + h2(i18n.industryDistribution || "Distribution"), + p(`${i18n.industryPot || "Pot"}: ${(b.allocation.pot || 0).toFixed(2)} ECO`) + ) + : null + + const buildLabel = `${i18n.industryBuild || "Build"}: ${safeText(b.title)}` + const facilityName = safeText(b.facilityName) + const productName = safeText(b.blueprintName) || safeText(b.title) + const crossTags = [facilityName, productName].filter(Boolean).map(x => x.toLowerCase().replace(/\s+/g, "-")).join(", ") + const crossDates = [ + b.startDate ? `${i18n.industryBuildStart || "Start date"}: ${moment(b.startDate).format("YYYY/MM/DD")}` : null, + b.endDate ? `${i18n.industryBuildEnd || "End date"}: ${moment(b.endDate).format("YYYY/MM/DD")}` : null + ].filter(Boolean).join(" · ") + const crossContext = [ + `${i18n.industryFacility || "Facility"}: ${facilityName}`, + b.blueprintName ? `${i18n.industryBlueprint || "Blueprint"}: ${safeText(b.blueprintName)}` : null, + crossDates || null, + b.estTotal != null ? `${i18n.industryFinalPrice || "Final price"}: ${fmtEco(b.estTotal)}` : null + ].filter(Boolean).join("\n") + const crossDescription = [safeText(b.notes), crossContext].filter(Boolean).join("\n\n") + const crossLinks = isMember + ? div({ class: "industry-actions" }, + a({ href: `/tasks?filter=create&returnTo=${encodeURIComponent(returnTo)}&title=${encodeURIComponent(buildLabel)}&description=${encodeURIComponent(crossDescription)}`, class: "filter-btn" }, i18n.industryCreateTask || "Create Task"), + a({ href: `/jobs?filter=CREATE&industry=${encodeURIComponent(b.facilityId || "")}&title=${encodeURIComponent(buildLabel)}&description=${encodeURIComponent(crossDescription)}&tasks=${encodeURIComponent(safeText(b.notes))}&salary=${encodeURIComponent(b.estLaborCost != null ? b.estLaborCost : "")}`, class: "filter-btn" }, i18n.industryPostJob || "Create Job"), + a({ href: `/projects?filter=CREATE&title=${encodeURIComponent(buildLabel)}&description=${encodeURIComponent(crossDescription)}&goal=${encodeURIComponent(b.estTotal != null ? b.estTotal : "")}`, class: "filter-btn" }, i18n.industrySendToProjects || "Create Project"), + a({ href: `/market?filter=create&industry=${encodeURIComponent(b.facilityId || "")}&title=${encodeURIComponent(productName)}&description=${encodeURIComponent(crossDescription)}&price=${encodeURIComponent(b.estTotal != null ? b.estTotal : "")}&tags=${encodeURIComponent(crossTags)}`, class: "filter-btn" }, i18n.industrySellOnMarket || "Send to Market"), + (!isSteward && b.steward) ? a({ href: `/courts?filter=open&method=MEDIATION&respondentId=${encodeURIComponent(b.steward)}&titleSuffix=${encodeURIComponent(buildLabel)}`, class: "filter-btn" }, i18n.industryOpenDispute || "Open dispute") : null + ) + : null + + return template( + i18n.industryTitle || "Industry", + section(div({ class: "tags-header module-header-line" }, h2(i18n.industryTitle), p(i18n.industryDescription))), + section( + div({ class: "filters" }, + form({ method: "GET", action: "/industry", class: "ui-toolbar ui-toolbar--filters" }, + FILTERS.map((x) => button({ type: "submit", name: "filter", value: x.key, class: "filter-btn" }, String(i18n[x.i18n] || x.key).toUpperCase())) + .concat(button({ type: "submit", name: "filter", value: "CREATE", class: "create-button" }, i18n.industryCreateFacility || "Create facility")) + ) + ) + ), + section( + div({ class: "shop-detail" }, + div({ class: "card-header activity-card-header" }, + span(), + renderContentActions(b.id, returnTo, { spread: params.spreads || null, author: b.proposer, reportTitle: b.title }) + ), + b.image ? div({ class: "shop-detail-media" }, renderMediaBlob(b.image, { class: "post-image" })) : null, + h2(safeText(b.title) || (i18n.industryBuild || "Build")), + div({ class: "card-chips-row" }, renderBuildStatusChip(status)), + div({ class: "industry-meta" }, + b.blueprintName ? p(span({ class: "industry-meta-label" }, `${i18n.industryBlueprint || "Blueprint"}: `), safeText(b.blueprintName)) : null, + b.blueprintImage ? renderMediaBlob(b.blueprintImage, { class: "post-image" }) : null, + p(span({ class: "industry-meta-label" }, `${i18n.industryProposer || "Proposer"}: `), userLink(b.proposer)), + b.notes ? p({ class: "industry-build-notes" }, span({ class: "industry-meta-label" }, `${i18n.industryBuildNotes || "Notes"}: `), safeText(b.notes)) : null, + div({ class: "industry-dates-row" }, + b.startDate ? p(span({ class: "industry-meta-label" }, `${i18n.industryBuildStart || "Start date"}: `), moment(b.startDate).format("YYYY/MM/DD")) : null, + b.endDate ? p(span({ class: "industry-meta-label" }, `${i18n.industryBuildEnd || "End date"}: `), moment(b.endDate).format("YYYY/MM/DD")) : null, + (b.endDate && status !== "COMPLETED" && status !== "FAILED") + ? p(span({ class: "industry-meta-label" }, `${i18n.industryTimeLeft || "Time left"}: `), `${Math.max(0, Math.ceil((new Date(b.endDate) - Date.now()) / 86400000))}d`) + : null + ) + ), + renderBlueprintEstimate(b, i18n.industryFinalPrice || "Final price"), + govRows.length ? div({ class: "industry-actions" }, ...govRows) : null, + crossLinks, + safeArr(b.contributions).length + ? div({ class: "industry-section" }, + h2(i18n.industryContributions || "Contributions"), + table({ class: "industry-table" }, + thead(tr(th(i18n.industryMember || "Member"), th(i18n.industryKindLabel || "Kind"), th(i18n.industryAmount || "Amount"), th(i18n.industryNote || "Note"))), + tbody(...contribRows) + ) + ) + : null, + contributeBlock, + distributeBlock, + allocationBlock + ) + ) + ) +} diff --git a/src/views/inhabitants_view.js b/src/views/inhabitants_view.js index 58f0ba0..399059e 100644 --- a/src/views/inhabitants_view.js +++ b/src/views/inhabitants_view.js @@ -1,7 +1,9 @@ -const { div, h2, p, section, button, form, img, a, textarea, input, br, span, strong } = require("../server/node_modules/hyperaxe"); -const { template, i18n, userLink, renderUserSensors, renderContentActions } = require('./main_views'); +const { div, h2, p, section, button, form, img, a, textarea, input, span, strong } = require("../server/node_modules/hyperaxe"); +const moment = require("../server/node_modules/moment"); +const { renderCallButton, template, i18n, userLink, renderUserSensors, renderContentActions, renderRelationshipBlock, renderModuleStats, paged } = require('./main_views'); +const { renderZoomableImage } = require('./gallery_view'); const { renderContentStats } = require('./clearnet_view'); -const { renderUrl } = require('../backend/renderUrl'); +const { renderStyledText } = require('../backend/renderStyledText'); const { getConfig } = require('../configs/config-manager'); const DEFAULT_HASH_ENC = "%260000000000000000000000000000000000000000000%3D.sha256"; @@ -48,20 +50,44 @@ function resolvePhoto(photoField, size = 256) { if (photoField.startsWith('/blob/')) return photoField; if (photoField.startsWith('/image/')) { if (isDefaultImageId(photoField)) return '/assets/images/default-avatar.png'; - return photoField.replace('/image/256/','/image/'+size+'/').replace('/image/512/','/image/'+size+'/'); + return photoField.replace('/image/256/','/image/'+size+'/').replace('/image/512/','/image/'+size+'/') + '?fallback=avatar'; } } - return toImageUrl(photoField, size); + return toImageUrl(photoField, size) + '?fallback=avatar'; } -const generateFilterButtons = (filters, currentFilter) => +const filterLabel = (mode) => + String(i18n[mode + 'Button'] || i18n[mode + 'SectionTitle'] || mode).toUpperCase(); + +const generateFilterButtons = (filters, currentFilter, labelOf = filterLabel) => filters.map(mode => form({ method: 'GET', action: '/inhabitants' }, input({ type: 'hidden', name: 'filter', value: mode }), button({ type: 'submit', class: currentFilter === mode ? 'filter-btn active' : 'filter-btn' - }, i18n[mode + 'Button'] || i18n[mode + 'SectionTitle'] || mode) + }, labelOf(mode)) + ) + ); + +const MAIN_FILTERS = ['all', 'TOP', 'contacts', 'blocked', 'CVs', 'SUGGESTED', 'GALLERY']; + +const renderMainFilters = (filter, isTop, census = {}) => + div({ class: 'inhabitant-action' }, + ...MAIN_FILTERS.filter(mode => { + if (mode === 'all' || mode === 'TOP') return true; + if (mode === 'TOP' ? isTop : filter === mode) return true; + return census[mode] !== false; + }).map(mode => + form({ method: 'GET', action: '/inhabitants' }, + input({ type: 'hidden', name: 'filter', value: mode === 'TOP' ? 'TOP ACTIVITY' : mode }), + button({ + type: 'submit', + class: (mode === 'TOP' ? isTop : filter === mode) ? 'filter-btn active' : 'filter-btn' + }, mode === 'TOP' + ? String(i18n.topSectionTitle || 'TOP').toUpperCase() + : filterLabel(mode)) + ) ) ); @@ -89,10 +115,34 @@ function lastActivityBadge(user, isMe) { const lightboxId = (id) => 'inhabitant_' + String(id || 'unknown').replace(/[^a-zA-Z0-9_-]/g, '_'); +const cvField = (labelText, value) => value + ? div({ class: 'card-field' }, + span({ class: 'card-label' }, `${labelText}: `), + span({ class: 'card-value' }, value) + ) + : null; + +const renderCvFields = (user) => { + const languages = Array.isArray(user.languages) ? user.languages.filter(Boolean) : []; + const skills = Array.isArray(user.skills) ? user.skills.filter(Boolean) : []; + const fields = [ + cvField(i18n.locationLabel, user.location), + cvField(i18n.languagesLabel, languages.length ? languages.join(', ').toUpperCase() : ''), + cvField(i18n.skillsLabel, skills.length ? skills.join(', ') : ''), + cvField(i18n.statusLabel || 'Status', user.status), + cvField(i18n.preferencesLabel || 'Preferences', user.preferences), + cvField(i18n.createdAtLabel || 'Created at', user.createdAt ? moment(user.createdAt).format("YYYY/MM/DD HH:mm") : ''), + user.pdf + ? div({ class: 'card-field' }, a({ href: `/blob/${encodeURIComponent(user.pdf)}`, target: '_blank', rel: 'noopener', class: 'filter-btn' }, '📄 ' + i18n.cvPdfLabel)) + : null + ].filter(Boolean); + return fields.length ? div({ class: 'cv-card-fields' }, ...fields) : null; +}; + const renderInhabitantCard = (user, filter, currentUserId, fediverseConfigured) => { const isMe = user.id === currentUserId; const raw = user.visibilityPrefs || {}; - const clearnetSubKeys = ['clearnetShops','clearnetJobs','clearnetEvents','clearnetProjects','clearnetPosts','clearnetAudios','clearnetVideos','clearnetImages','clearnetDocuments','clearnetTorrents','clearnetBookmarks']; + const clearnetSubKeys = ['clearnetShops','clearnetJobs','clearnetEvents','clearnetProjects','clearnetPosts','clearnetAudios','clearnetVideos','clearnetImages','clearnetDocuments','clearnetTorrents','clearnetFiles','clearnetBookmarks','clearnetPodcasts']; const hasClearnet = raw.clearnet === true || clearnetSubKeys.some(k => raw[k] === true); const prefs = { activity: raw.activity === true, @@ -102,17 +152,14 @@ const renderInhabitantCard = (user, filter, currentUserId, fediverseConfigured) wallet: raw.wallet === true, ecoTax: raw.ecoTax !== false, larpSign: raw.larpSign === true, - gpg: raw.gpg !== false, + gpg: raw.gpg === true, + phone: raw.phone === 'mutuals' || raw.phone === 'off' ? raw.phone : 'whole', + phoneDnd: raw.phoneDnd === true, clearnet: hasClearnet, fediverse: raw.fediverse === true, fediverseHandle: typeof raw.fediverseHandle === 'string' ? raw.fediverseHandle : '' }; return div({ class: 'trending-card inhabitants-card' + (isMe ? ' own-content' : '') }, - div( - { class: 'card-header activity-card-header' }, - span(), - renderContentActions(user.id, `/inhabitant/${encodeURIComponent(user.id)}`) - ), div({ class: 'card-section inhabitants-card-body' }, div({ class: 'inhabitant-card' }, div({ class: 'inhabitant-left' }, @@ -120,73 +167,44 @@ const renderInhabitantCard = (user, filter, currentUserId, fediverseConfigured) { href: `/author/${encodeURIComponent(user.id)}` }, img({ class: 'inhabitant-photo-details', src: resolvePhoto(user.photo, 256), alt: user.name || 'Anonymous' }) ), + user.name ? p({ class: 'inhabitant-left-name' }, userLink(user.id, user.name)) : null, ...renderUserSensors({ isMe, fediverseConfigured, prefs, id: user.id, - karmaScore: user.karmaScore, carbonGrams: user.carbonGrams, + karmaScore: user.karmaScore, karmaReceived: user.karmaReceived || null, carbonGrams: user.carbonGrams, deviceSource: user.deviceSource, activityBucket: user.lastActivityBucket, gpgFingerprint: user.gpgFingerprint, ecoAddress: user.ecoAddress, estimatedUBI: user.estimatedUBI, lastClaimedDate: user.lastClaimedDate, totalClaimed: user.totalClaimed, - larpHouse: user.larpHouse, stats: user.stats - }, { relationshipNode: isMe ? span({ class: 'status you' }, i18n.relationshipYou) : null, excludeContent: true }), - !isMe + larpHouse: user.larpHouse, stats: user.stats, + mutual: !!(user.relationship && user.relationship.following && user.relationship.followsMe) + }, { excludeContent: true, sensorsOnly: true, dndToggle: false }), + isMe ? p({ class: 'inhabitant-left-name' }, span({ class: 'status you' }, i18n.relationshipYou)) : null, + filter === 'CVs' ? div( - { class: 'cv-actions' }, + { class: 'cv-actions doc-export-actions' }, form( - { method: 'GET', action: '/pm' }, - input({ type: 'hidden', name: 'recipients', value: user.id }), - button({ type: 'submit', class: 'btn' }, i18n.pmCreateButton) + { method: 'GET', action: `/cv/pdf/${encodeURIComponent(user.id)}` }, + button({ type: 'submit', class: 'filter-btn' }, i18n.generatePdf) + ), + form( + { method: 'POST', action: `/cv/share/${encodeURIComponent(user.id)}` }, + button({ type: 'submit', class: 'filter-btn' }, i18n.sharePm) ) ) : null ), - div({ class: 'inhabitant-details' }, - h2(user.name || 'Anonymous'), - user.description ? p(...renderUrl(user.description)) : null, - filter === 'MATCHSKILLS' && user.commonSkills?.length - ? div({ class: 'matchskills' }, - p(`${i18n.commonSkills}: ${user.commonSkills.join(', ')}`), - p(`${i18n.matchScore}: ${Math.round((user.matchScore || 0) * 100)}%`) - ) - : null, - filter === 'SUGGESTED' && (user.followsYou || user.commonSkills?.length || user.mutualCount) - ? div({ class: 'suggested-meta' }, - user.followsYou ? span({ class: 'suggested-badge' }, i18n.suggestedFollowsYou || 'Follows you') : null, - user.commonSkills?.length - ? p(`${i18n.commonSkills || 'Common skills'}: ${user.commonSkills.join(', ')}`) - : null, - user.mutualCount ? p(`${i18n.mutualFollowers}: ${user.mutualCount}`) : null - ) - : null, - filter === 'blocked' && user.isBlocked - ? p(i18n.blockedLabel) : null, - p(userLink(user.id)), - !isMe ? (() => { - const rel = user.relationship || {} - const blockedBoth = rel.blocking && rel.blockedBy - const mutual = rel.following && rel.followsMe - const supportAction = rel.following ? 'unfollow' : (rel.blocking ? 'unblock' : 'follow') - return div({ class: 'relationship-status inhabitant-relationship' }, - blockedBoth - ? span({ class: 'status blocked' }, i18n.relationshipMutualBlock) - : [ - rel.blocking ? span({ class: 'status blocked' }, i18n.relationshipBlocking) : null, - rel.blockedBy ? span({ class: 'status blocked-by' }, i18n.relationshipBlockedBy) : null, - mutual - ? span({ class: 'status mutual' }, i18n.relationshipMutuals) - : [ - span({ class: 'status supporting' }, rel.following ? i18n.relationshipFollowing : i18n.relationshipNone), - span({ class: 'status supported-by' }, rel.followsMe ? i18n.relationshipTheyFollow : i18n.relationshipNotFollowing) - ] - ], - div({ class: 'relationship-actions' }, - form({ method: 'POST', action: `/${supportAction}/${encodeURIComponent(user.id)}` }, - button({ type: 'submit', class: 'btn' }, i18n[supportAction]) + (() => { + const detailNodes = [ + filter === 'CVs' ? renderCvFields(user) : null, + filter === 'SUGGESTED' && user.commonSkills?.length + ? div({ class: 'suggested-meta' }, + p(`${i18n.commonSkills || 'Common skills'}: ${user.commonSkills.slice(0, 8).join(', ')}`) ) - ) - ) - })() : null, - renderContentStats(user.stats, i18n) - ) + : null, + filter === 'blocked' && user.isBlocked + ? p(i18n.blockedLabel) : null + ].filter(Boolean); + return detailNodes.length ? div({ class: 'inhabitant-details' }, ...detailNodes) : null; + })() ) ) ); @@ -197,7 +215,7 @@ const renderGalleryInhabitants = inhabitants => { class: "gallery" }, inhabitants.length ? inhabitants.map(u => - a({ href: `#${lightboxId(u.id)}`, class: "gallery-item" }, + a({ href: `#${lightboxId(u.id)}`, id: `${lightboxId(u.id)}-src`, class: "gallery-item" }, img({ src: resolvePhoto(u.photo, 256), alt: u.name || "Anonymous", class: "gallery-image" }) ) ) @@ -208,7 +226,7 @@ const renderLightbox = inhabitants => inhabitants.map(u => div( { id: lightboxId(u.id), class: "lightbox" }, - a({ href: "#", class: "lightbox-close" }, "×"), + a({ href: `#${lightboxId(u.id)}-src`, class: "lightbox-close" }, "×"), img({ src: resolvePhoto(u.photo, 256), class: "lightbox-image", alt: u.name || "Anonymous" }) ) ); @@ -228,59 +246,66 @@ function msgIdOf(m) { return m && (m.key || m.value?.key || m.value?.content?.root || m.value?.content?.branch || null); } -exports.inhabitantsView = (inhabitants, filter, query, currentUserId, fediverseConfigured) => { - const title = filter === 'contacts' ? i18n.yourContacts - : filter === 'CVs' ? i18n.allCVs - : filter === 'MATCHSKILLS' ? i18n.matchSkills - : filter === 'SUGGESTED' ? i18n.suggestedSectionTitle - : filter === 'blocked' ? i18n.blockedSectionTitle - : filter === 'GALLERY' ? i18n.gallerySectionTitle - : filter === 'TOP KARMA' ? i18n.topkarmaSectionTitle - : filter === 'TOP ECO' ? (i18n.topecoSectionTitle || 'Top Eco') - : filter === 'TOP ACTIVITY' ? i18n.topactivitySectionTitle - : i18n.allInhabitants; +exports.inhabitantsView = (inhabitants, filter, query, currentUserId, fediverseConfigured, filterCensus = {}) => { + const title = i18n.allInhabitants; - const showCVFilters = filter === 'CVs' || filter === 'MATCHSKILLS'; - const filters = ['all', 'TOP ACTIVITY', 'TOP KARMA', 'TOP ECO', 'contacts', 'SUGGESTED', 'blocked', 'CVs', 'MATCHSKILLS', 'GALLERY']; + const showCVFilters = filter === 'CVs'; + const bucketCounts = inhabitants.reduce((acc, u) => { + const b = u && u.lastActivityBucket; + if (b === 'green' || b === 'orange' || b === 'red') acc[b]++; + return acc; + }, { green: 0, orange: 0, red: 0 }); + const TOP_FILTERS = ['TOP ACTIVITY', 'TOP INACTIVITY', 'TOP KARMA', 'TOP ECO']; + const isTop = TOP_FILTERS.includes(filter); return template( title, section( - div({ class: 'tags-header' }, + div({ class: 'tags-header module-header-line' }, h2(title), p(i18n.discoverPeople) ), - div({ class: 'filters' }, - form({ method: 'GET', action: '/inhabitants' }, + renderMainFilters(filter, isTop, filterCensus), + isTop + ? div({ class: 'inhabitant-action inhabitant-subfilters' }, + ...generateFilterButtons(TOP_FILTERS, filter, (mode) => filterLabel(mode).replace(/^TOP\s+/, '')) + ) + : null, + div({ class: 'filters activity-filter-chips activity-toolbar-row' }, + renderModuleStats(inhabitants.length, [ + { label: '<2w', count: bucketCounts.green, labelClass: 'stat-label-green' }, + { label: '2w–6m', count: bucketCounts.orange, labelClass: 'stat-label-orange' }, + { label: '>6m', count: bucketCounts.red, labelClass: 'stat-label-red' } + ]), + form({ method: 'GET', action: '/inhabitants', class: 'filter-box' }, input({ type: 'hidden', name: 'filter', value: filter }), input({ type: 'text', name: 'search', placeholder: i18n.searchInhabitantsPlaceholder, - value: (query && query.search) || '' + value: (query && query.search) || '', + class: 'filter-box__input' }), - showCVFilters - ? [ - input({ type: 'text', name: 'location', placeholder: i18n.filterLocation, value: (query && query.location) || '' }), - input({ type: 'text', name: 'language', placeholder: i18n.filterLanguage, value: (query && query.language) || '' }), - input({ type: 'text', name: 'skills', placeholder: i18n.filterSkills, value: (query && query.skills) || '' }) - ] - : null, - br(), - button({ type: 'submit' }, i18n.applyFilters) + div({ class: 'filter-box__controls' }, + showCVFilters + ? [ + input({ type: 'text', name: 'location', placeholder: i18n.filterLocation, value: (query && query.location) || '', class: 'filter-box__number' }), + input({ type: 'text', name: 'language', placeholder: i18n.filterLanguage, value: (query && query.language) || '', class: 'filter-box__number' }), + input({ type: 'text', name: 'skills', placeholder: i18n.filterSkills, value: (query && query.skills) || '', class: 'filter-box__number' }) + ] + : null, + button({ type: 'submit', class: 'filter-box__button' }, i18n.searchButton) + ) ) ), - div({ class: 'inhabitant-action' }, - ...generateFilterButtons(filters, filter) - ), filter === 'GALLERY' - ? renderGalleryInhabitants(inhabitants) - : div({ class: 'inhabitants-list' }, + ? renderGalleryInhabitants(paged(inhabitants)) + : div({ class: filter === 'CVs' ? 'inhabitants-list' : 'inhabitants-list inhabitants-grid' }, inhabitants.length - ? inhabitants.map(user => renderInhabitantCard(user, filter, currentUserId, fediverseConfigured)) + ? paged(inhabitants).map(user => renderInhabitantCard(user, filter, currentUserId, fediverseConfigured)) : p({ class: 'no-results' }, i18n.noInhabitantsFound) ), - ...renderLightbox(inhabitants) + ...renderLightbox(paged(inhabitants)) ) ); }; @@ -320,7 +345,7 @@ exports.inhabitantsProfileView = (payload, currentUserId, fediverseConfigured) = ]; const status = (cv && cv.status) || ''; const preferences = (cv && cv.preferences) || ''; - const createdAt = (cv && cv.createdAt) ? new Date(cv.createdAt).toLocaleString() : ''; + const createdAt = (cv && cv.createdAt) ? moment(cv.createdAt).format("YYYY/MM/DD HH:mm") : ''; const isMe = id && id === currentUserId; const title = i18n.inhabitantProfileTitle || i18n.inhabitantviewDetails; const karmaScore = typeof safe.karmaScore === 'number' ? safe.karmaScore : 0; @@ -329,7 +354,7 @@ exports.inhabitantsProfileView = (payload, currentUserId, fediverseConfigured) = const totalClaimed = typeof safe.totalClaimed === 'number' ? safe.totalClaimed : 0; const ecoAddress = typeof safe.ecoAddress === 'string' ? safe.ecoAddress : null; const rawPrefs = safe.visibilityPrefs || {}; - const clearnetSubKeys = ['clearnetShops','clearnetJobs','clearnetEvents','clearnetProjects','clearnetPosts','clearnetAudios','clearnetVideos','clearnetImages','clearnetDocuments','clearnetTorrents','clearnetBookmarks']; + const clearnetSubKeys = ['clearnetShops','clearnetJobs','clearnetEvents','clearnetProjects','clearnetPosts','clearnetAudios','clearnetVideos','clearnetImages','clearnetDocuments','clearnetTorrents','clearnetFiles','clearnetBookmarks','clearnetPodcasts']; const prefs = { activity: rawPrefs.activity === true, device: rawPrefs.device === true, @@ -338,7 +363,9 @@ exports.inhabitantsProfileView = (payload, currentUserId, fediverseConfigured) = wallet: rawPrefs.wallet === true, ecoTax: rawPrefs.ecoTax !== false, larpSign: rawPrefs.larpSign === true, - gpg: rawPrefs.gpg !== false, + gpg: rawPrefs.gpg === true, + phone: rawPrefs.phone === 'mutuals' || rawPrefs.phone === 'off' ? rawPrefs.phone : 'whole', + phoneDnd: rawPrefs.phoneDnd === true, clearnet: rawPrefs.clearnet === true || clearnetSubKeys.some(k => rawPrefs[k] === true), fediverse: rawPrefs.fediverse === true, fediverseHandle: typeof rawPrefs.fediverseHandle === 'string' ? rawPrefs.fediverseHandle : '' @@ -350,26 +377,28 @@ exports.inhabitantsProfileView = (payload, currentUserId, fediverseConfigured) = const providedBucket = typeof safe.lastActivityBucket === 'string' ? safe.lastActivityBucket : null; const dotClass = providedBucket === 'green' ? 'green' : providedBucket === 'orange' ? 'orange' : 'red'; + const fieldNodes = [ + cvField(i18n.locationLabel, location), + cvField(i18n.languagesLabel, languages.length ? languages.join(', ').toUpperCase() : ''), + cvField(i18n.skillsLabel, skills.length ? skills.join(', ') : ''), + cvField(i18n.statusLabel || 'Status', status), + cvField(i18n.preferencesLabel || 'Preferences', preferences), + cvField(i18n.createdAtLabel || 'Created at', createdAt) + ].filter(Boolean); + const detailNodes = [ - description ? p(...renderUrl(description)) : null, - location ? p(`${i18n.locationLabel}: ${location}`) : null, - languages.length ? p(`${i18n.languagesLabel}: ${languages.join(', ').toUpperCase()}`) : null, - skills.length ? p(`${i18n.skillsLabel}: ${skills.join(', ')}`) : null, - status ? p(`${i18n.statusLabel || 'Status'}: ${status}`) : null, - preferences ? p(`${i18n.preferencesLabel || 'Preferences'}: ${preferences}`) : null, - createdAt ? p(`${i18n.createdAtLabel || 'Created at'}: ${createdAt}`) : null + description ? p(...renderStyledText(description)) : null, + fieldNodes.length ? div({ class: 'cv-card-fields' }, ...fieldNodes) : null ].filter(Boolean); return template( name, section( - div({ class: 'tags-header' }, + div({ class: 'tags-header module-header-line' }, h2(title), p(i18n.discoverPeople) ), - div({ class: 'mode-buttons' }, - ...generateFilterButtons(['all', 'TOP ACTIVITY', 'TOP KARMA', 'TOP ECO', 'contacts', 'SUGGESTED', 'blocked', 'CVs', 'MATCHSKILLS', 'GALLERY'], 'all') - ), + renderMainFilters('all', false), div({ class: 'inhabitant-card' }, div({ class: 'inhabitant-left' }, img({ class: 'inhabitant-photo-details', src: image, alt: name || 'Anonymous' }), @@ -377,16 +406,23 @@ exports.inhabitantsProfileView = (payload, currentUserId, fediverseConfigured) = safe.oasisVersion ? div({ class: 'profile-side-version' }, '🌴 ' + (i18n.oasisVersionLabel || 'Oasis Version') + ': ', strong(safe.oasisVersion)) : null, ...renderUserSensors({ isMe, fediverseConfigured, prefs, id: id || viewedId, - karmaScore, carbonGrams, deviceSource: safe.deviceSource, activityBucket: providedBucket, + karmaScore, karmaReceived: safe.karmaReceived || null, carbonGrams, deviceSource: safe.deviceSource, activityBucket: providedBucket, gpgFingerprint, ecoAddress, estimatedUBI, lastClaimedDate, totalClaimed, - larpHouse, stats: safe.stats + larpHouse, stats: safe.stats, + mutual: !!(safe.relationship && safe.relationship.following && safe.relationship.followsMe) }, { excludeContent: true }), (!isMe && (id || viewedId)) - ? form( - { method: 'GET', action: '/pm' }, - input({ type: 'hidden', name: 'recipients', value: id || viewedId }), - button({ type: 'submit', class: 'btn' }, i18n.pmCreateButton) + ? div({ class: 'cv-actions doc-export-actions' }, + form( + { method: 'GET', action: '/pm' }, + input({ type: 'hidden', name: 'recipients', value: id || viewedId }), + button({ type: 'submit', class: 'filter-btn' }, `✉ ${i18n.pmCreateButton}`) + ), + renderCallButton(id || viewedId, { pam: true }) ) + : null, + (!isMe && (id || viewedId) && safe.relationship) + ? div({ class: 'inhabitant-relationship' }, renderRelationshipBlock(safe.relationship, id || viewedId)) : null ), (() => { @@ -397,15 +433,16 @@ exports.inhabitantsProfileView = (payload, currentUserId, fediverseConfigured) = const raw = (m.value?.content?.text || '').replace(//g, ''); const parts = stripAndCollectImgs(raw); const tid = msgIdOf(m); - const visitBtn = tid - ? form({ method: 'GET', action: `/thread/${encodeURIComponent(tid)}#${encodeURIComponent(tid)}` }, - button({ type:'submit', class:'filter-btn' }, i18n.visitContent) + const actions = tid + ? div({ class: 'card-header activity-card-header' }, + span(), + renderContentActions(tid, `/thread/${encodeURIComponent(tid)}#${encodeURIComponent(tid)}`, { author: m.value?.author, reportTitle: parts.clean }) ) : null; return div({ class: 'post' }, - visitBtn, - parts.clean && parts.clean.trim() ? p(...renderUrl(parts.clean)) : null, - ...(parts.imgs || []).map(src => img({ src, class: 'post-image', alt: 'image' })) + actions, + parts.clean && parts.clean.trim() ? p(...renderStyledText(parts.clean)) : null, + ...(parts.imgs || []).map(src => renderZoomableImage(src, { imgClass: 'post-image', alt: 'image' })) ); }) ) ] @@ -420,3 +457,5 @@ exports.inhabitantsProfileView = (payload, currentUserId, fediverseConfigured) = }; exports.lastActivityBadge = lastActivityBadge; +exports.resolvePhoto = resolvePhoto; +exports.renderInhabitantCard = renderInhabitantCard; diff --git a/src/views/invites_view.js b/src/views/invites_view.js index f58703a..8f64cb2 100644 --- a/src/views/invites_view.js +++ b/src/views/invites_view.js @@ -1,12 +1,13 @@ -const { form, button, div, h2, h3, p, section, ul, li, a, br, hr, input, label, span, table, tr, td, textarea } = require("../server/node_modules/hyperaxe"); +const { form, button, div, h2, h3, p, section, a, br, input, label, table, tr, td, textarea } = require("../server/node_modules/hyperaxe"); +const moment = require("../server/node_modules/moment"); const path = require("path"); const fs = require('fs'); -const { renderUrl } = require("../backend/renderUrl"); +const { renderStyledText } = require("../backend/renderStyledText"); const { template, i18n } = require('./main_views'); -const homedir = require('os').homedir(); -const gossipPath = path.join(homedir, ".ssb", "gossip.json"); -const unfollowedPath = path.join(homedir, ".ssb", "gossip_unfollowed.json"); +const gossipPath = path.join(require("../server/ssb_config").path, "gossip.json"); +const unfollowedPath = require("../configs/state-manager").statePath("gossip_unfollowed.json"); +const peerHealth = require("../models/peer_health"); const encodePubLink = (key) => { let core = String(key).replace(/^@/, '').replace(/\.ed25519$/, '').replace(/-/g, '+').replace(/_/g, '/'); @@ -14,12 +15,21 @@ const encodePubLink = (key) => { return `/author/${encodeURIComponent('@' + core)}.ed25519`; }; -const snhInvitePath = path.join(__dirname, '..', 'configs', 'snh-invite-code.json'); - -let snhInvite = null; -try { - snhInvite = JSON.parse(fs.readFileSync(snhInvitePath, 'utf8')); -} catch {} +const INVITE_KIND_LABEL = { + tribes: 'invitesTribesTitle', + houses: 'invitesHousesTitle', + chats: 'invitesChatsTitle', + pads: 'invitesPadsTitle', + rooms: 'invitesRoomsTitle', + calendars: 'invitesCalendarsTitle', + events: 'invitesEventsTitle', + forum: 'invitesForumsTitle', + school: 'invitesSchoolTitle', + maps: 'invitesMapsTitle', + shops: 'invitesShopsTitle', + inhabitants: 'invitesInhabitantsTitle', + pubs: 'invitesPubsTitle' +}; const deduplicateByHost = (list) => { const seen = new Set(); @@ -31,7 +41,25 @@ const deduplicateByHost = (list) => { }); }; -const invitesView = ({ invitesEnabled, flash }) => { +const renderSuggestedPub = (entry, canJoin) => div({ class: 'snh-invite-box' }, + h3({ class: 'snh-invite-name' }, entry.name), + p({ class: 'snh-invite-name' }, entry.description), + p({ class: 'snh-invite-name' }, renderStyledText(entry.url)), + entry.createdAt ? p({ class: 'snh-invite-date' }, `${i18n.statsCreatedAt || 'Created'}: ${moment(entry.createdAt).format("YYYY/MM/DD")}`) : null, + p({ class: 'snh-invite-name snh-invite-code' }, entry.code), + entry.invites ? p({ class: 'snh-invite-date' }, `${i18n.invitesPubUses}: ${entry.invites.used}/${entry.invites.total}`) : null, + entry.exhausted + ? p({ class: 'snh-invite-name' }, i18n.invitesPubExhausted) + : canJoin + ? form({ action: '/settings/invite/accept', method: 'post' }, + input({ type: 'hidden', name: 'invite', value: entry.code }), + button({ type: 'submit' }, i18n.invitesAcceptInvite) + ) + : null +); + +const invitesView = (opts = {}) => { + const canJoin = opts.canJoin !== false; let pubs = []; let pubsValue = "false"; let unfollowed = []; @@ -60,15 +88,8 @@ const invitesView = ({ invitesEnabled, flash }) => { ? deduplicateByHost(pubs.filter(pubItem => !unfollowed.find(u => u.key === pubItem.key))) : []; - const hasError = (pubItem) => pubItem && (pubItem.error || (typeof pubItem.failure === 'number' && pubItem.failure > 0)); - - const sanitizeError = (err) => { - if (!err) return i18n.genericError || 'Unknown error'; - const firstLine = String(err).split('\n')[0].replace(/^Error:\s*/, '').trim(); - return firstLine || (i18n.genericError || 'Unknown error'); - }; - - const unreachableLabel = i18n.currentlyUnreachable || i18n.currentlyUnrecheable || 'ERROR!'; + const deadKeys = opts.deadKeys instanceof Set ? opts.deadKeys : new Set(); + const hasError = (pubItem) => !!(pubItem && deadKeys.has(peerHealth.canonicalKey(pubItem.key))); const pubTableHeader = () => tr( td({ class: 'card-label' }, 'PUB'), @@ -78,7 +99,9 @@ const invitesView = ({ invitesEnabled, flash }) => { ); const activePubs = filteredPubs.filter(pubItem => !hasError(pubItem)); - const unreachablePubs = pubs.filter(hasError); + const filter = opts.filter === 'others' ? 'others' : 'pubs'; + const suggested = Array.isArray(opts.inviteCodes) ? opts.inviteCodes : []; + const used = (Array.isArray(opts.invitesUsed) ? opts.invitesUsed : []).filter((e) => e && typeof e.href === 'string' && e.href.startsWith('/')); const renderPubTable = (items, actionFn) => table({ class: 'block-info-table invites-pubs-table' }, pubTableHeader(), @@ -96,54 +119,44 @@ const invitesView = ({ invitesEnabled, flash }) => { return template( title, section( - div({ class: 'tags-header' }, + div({ class: 'tags-header module-header-line' }, h2(title), p(description) + ), + div({ class: 'mode-buttons-row' }, + a({ class: filter === 'pubs' ? 'filter-btn active' : 'filter-btn', href: '/invites?filter=pubs' }, String(i18n.invitesPubsTitle).toUpperCase()), + a({ class: filter === 'others' ? 'filter-btn active' : 'filter-btn', href: '/invites?filter=others' }, String(i18n.invitesFilterOthers).toUpperCase()) ) ), - flash === 'alreadyFederated' - ? section(div({ class: 'message-banner' }, p(i18n.invitesAlreadyFederated || 'You are already federated with this pub.'))) - : null, - section( + filter === 'pubs' && suggested.length ? section( + div({ class: 'pubs-section' }, + h2(`${i18n.invitesSuggestedPubs} (${suggested.length})`), + ...suggested.map((entry) => renderSuggestedPub(entry, canJoin)) + ) + ) : null, + filter === 'pubs' ? section( div({ class: 'pubs-section' }, h2(i18n.invitesPubsTitle), - form( - { action: '/settings/invite/accept', method: 'post' }, - input({ name: 'invite', type: 'text', placeholder: i18n.invitesPubInviteCodePlaceholder, autofocus: true, required: true }), - br(), - button({ type: 'submit' }, i18n.invitesAcceptInvite) - ), - br(), - snhInvite ? div({ class: 'snh-invite-box' }, - h3({ class: 'snh-invite-name' }, snhInvite.name), - p({ class: 'snh-invite-name' }, snhInvite.description), - p({ class: 'snh-invite-name' }, renderUrl(snhInvite.url)), - snhInvite.createdAt ? p({ class: 'snh-invite-date' }, `${i18n.statsCreatedAt || 'Created'}: ${new Date(snhInvite.createdAt).toLocaleDateString()}`) : null, - form({ action: '/settings/invite/accept', method: 'post' }, - input({ type: 'hidden', name: 'invite', value: snhInvite.code }), - button({ type: 'submit', class: 'filter-btn' }, snhInvite.code) - ) - ) : null + p(i18n.invitesPubsHint), + canJoin + ? form( + { action: '/settings/invite/accept', method: 'post' }, + input({ name: 'invite', type: 'text', placeholder: i18n.invitesPubInviteCodePlaceholder, autofocus: true, required: true }), + br(), + button({ type: 'submit' }, i18n.invitesAcceptInvite) + ) + : p({ class: 'invites-join-note' }, opts.publicMode === true ? i18n.invitesJoinPubMode : i18n.invitesJoinLocalOnly) ) - ), - section( + ) : null, + filter === 'pubs' ? section( div({ class: 'federations-section' }, h2(i18n.invitesFederationsTitle || 'Federations'), + p(i18n.invitesFederationsHint), div({ class: 'conn-actions invites-pubs-actions' }, form({ action: '/invites/refresh-pubs', method: 'post' }, button({ type: 'submit' }, i18n.invitesPubsRefresh || 'Refresh')), - form({ action: '/invites/clear-unreachable', method: 'post' }, button({ type: 'submit' }, i18n.invitesPubsClearUnreachable || 'Remove unreachable')), + form({ action: '/invites/clear-unreachable', method: 'post' }, button({ type: 'submit' }, i18n.invitesPubsClearUnreachable || 'Clean')), form({ action: '/invites/export-pubs', method: 'get' }, button({ type: 'submit' }, i18n.invitesPubsExport || 'Export')) ), - form( - { action: '/invites/import-pubs', method: 'post', enctype: 'multipart/form-data', class: 'peers-import-form' }, - label({ class: 'peers-import-label' }, i18n.invitesPubsImportTitle || 'Import pubs'), - br(), - textarea({ name: 'peerList', rows: '4', placeholder: i18n.invitesPubsImportPlaceholder || 'Paste one multiserver address or invite code per line…' }), - br(), - input({ type: 'file', name: 'peerFile', accept: '.txt,text/plain' }), - br(), - button({ type: 'submit', class: 'filter-btn' }, i18n.invitesPubsImport || 'Import') - ), h2(`${i18n.invitesAcceptedInvites} (${activePubs.length})`), activePubs.length ? renderPubTable(activePubs, pubItem => @@ -153,7 +166,6 @@ const invitesView = ({ invitesEnabled, flash }) => { ) ) : p(i18n.invitesNoFederatedPubs), - hr(), h2(`${i18n.invitesUnfollowedInvites} (${unfollowed.length})`), unfollowed.length ? renderPubTable(unfollowed, pubItem => @@ -165,135 +177,51 @@ const invitesView = ({ invitesEnabled, flash }) => { ) ) : p(i18n.invitesNoUnfollowed), - hr(), - h2(`${i18n.invitesUnreachablePubs} (${unreachablePubs.length})`), - unreachablePubs.length - ? renderPubTable(unreachablePubs, pubItem => - div({ class: 'error-box' }, - p({ class: 'error-title' }, i18n.errorDetails), - p({ class: 'error-pre' }, sanitizeError(pubItem.error)) - ) + form( + { action: '/invites/import-pubs', method: 'post', enctype: 'multipart/form-data', class: 'peers-import-form' }, + label({ class: 'peers-import-label' }, i18n.invitesPubsImportTitle || 'Import pubs'), + br(), + textarea({ name: 'peerList', rows: '4', placeholder: i18n.invitesPubsImportPlaceholder || 'Paste one multiserver address or invite code per line…' }), + br(), + input({ type: 'file', name: 'peerFile', accept: '.txt,text/plain' }), + br(), + button({ type: 'submit' }, i18n.invitesPubsImport || 'Import') + ) + ) + ) : null, + filter === 'others' ? section( + div( + h2(i18n.invitesJoinTitle), + p(i18n.invitesJoinHint), + form( + { action: '/invites/join', method: 'post' }, + input({ name: 'code', type: 'text', placeholder: i18n.invitesJoinPlaceholder, required: true }), + br(), + button({ type: 'submit' }, i18n.invitesJoinButton) + ), + h2(`${i18n.invitesUsedTitle} (${used.length})`), + used.length + ? table({ class: 'block-info-table invites-used-table' }, + tr( + td({ class: 'card-label' }, i18n.invitesUsedType), + td({ class: 'card-label' }, i18n.invitesUsedName), + td({ class: 'card-label' }, i18n.invitesUsedDate) + ), + used.map((entry) => tr( + td(INVITE_KIND_LABEL[entry.kind] ? i18n[INVITE_KIND_LABEL[entry.kind]] : entry.kind), + td(a({ href: entry.href }, entry.title || entry.href)), + td(moment(entry.at).format('YYYY/MM/DD HH:mm')) + )) ) - : p(i18n.invitesNoUnreachablePubs) + : p(i18n.invitesUsedEmpty) ) - ), - section( - div({ class: 'invites-houses', id: 'invites-houses' }, - h2(i18n.invitesHousesTitle || 'Houses'), - form( - { action: '/larp/invite/redeem', method: 'post' }, - input({ type: 'hidden', name: 'returnTo', value: '/invites' }), - input({ name: 'code', type: 'text', placeholder: i18n.invitesHouseInviteCodePlaceholder || 'House invitation code', required: true, maxlength: 32 }), - br(), - button({ type: 'submit' }, i18n.invitesHouseJoinButton || 'Join House') - ) - ) - ), - section( - div({ class: 'invites-tribes', id: 'invites-tribes' }, - h2(i18n.invitesTribesTitle), - form( - { action: '/tribes/join-code', method: 'post' }, - input({ name: 'inviteCode', type: 'text', placeholder: i18n.invitesTribeInviteCodePlaceholder, required: true }), - br(), - button({ type: 'submit' }, i18n.invitesTribeJoinButton) - ) - ) - ), - section( - div({ class: 'invites-chats', id: 'invites-chats' }, - h2(i18n.invitesChatsTitle || 'Chats'), - form( - { action: '/chats/join-code', method: 'post' }, - input({ name: 'code', type: 'text', placeholder: i18n.invitesChatInviteCodePlaceholder || 'Enter chat invite code', required: true }), - br(), - button({ type: 'submit' }, i18n.invitesChatJoinButton || 'Join Chat') - ) - ) - ), - section( - div({ class: 'invites-pads', id: 'invites-pads' }, - h2(i18n.invitesPadsTitle || 'Pads'), - form( - { action: '/pads/join-code', method: 'post' }, - input({ name: 'code', type: 'text', placeholder: i18n.invitesPadInviteCodePlaceholder || 'Enter pad invite code', required: true }), - br(), - button({ type: 'submit' }, i18n.invitesPadJoinButton || 'Join Pad') - ) - ) - ), - section( - div({ class: 'invites-calendars', id: 'invites-calendars' }, - h2(i18n.invitesCalendarsTitle || 'Calendars'), - form( - { action: '/calendars/join-code', method: 'post' }, - input({ name: 'code', type: 'text', placeholder: i18n.invitesCalendarInviteCodePlaceholder || 'Enter calendar invite code', required: true }), - br(), - button({ type: 'submit' }, i18n.invitesCalendarJoinButton || 'Join Calendar') - ) - ) - ), - section( - div({ class: 'invites-events', id: 'invites-events' }, - h2(i18n.invitesEventsTitle || 'Events'), - form( - { action: '/events/join-code', method: 'post' }, - input({ name: 'code', type: 'text', placeholder: i18n.invitesEventInviteCodePlaceholder || 'Enter event invite code', required: true }), - br(), - button({ type: 'submit' }, i18n.invitesEventJoinButton || 'Join Event') - ) - ) - ), - section( - div({ class: 'invites-forums', id: 'invites-forums' }, - h2(i18n.invitesForumsTitle || 'Forums'), - form( - { action: '/forum/join-code', method: 'post' }, - input({ name: 'code', type: 'text', placeholder: i18n.invitesForumInviteCodePlaceholder || 'Enter forum invite code', required: true }), - br(), - button({ type: 'submit' }, i18n.invitesForumJoinButton || 'Join Forum') - ) - ) - ), - section( - div({ class: 'invites-maps', id: 'invites-maps' }, - h2(i18n.invitesMapsTitle || 'Maps'), - form( - { action: '/maps/join-code', method: 'post' }, - input({ name: 'code', type: 'text', placeholder: i18n.invitesMapInviteCodePlaceholder || 'Enter map invite code', required: true }), - br(), - button({ type: 'submit' }, i18n.invitesMapJoinButton || 'Join Map') - ) - ) - ), - section( - div({ class: 'invites-shops', id: 'invites-shops' }, - h2(i18n.invitesShopsTitle || 'Shops'), - form( - { action: '/shops/join-code', method: 'post' }, - input({ name: 'code', type: 'text', placeholder: i18n.invitesShopInviteCodePlaceholder || 'Enter shop invite code', required: true }), - br(), - button({ type: 'submit' }, i18n.invitesShopJoinButton || 'Join Shop') - ) - ) - ), - section( - div({ class: 'invites-inhabitants', id: 'invites-inhabitants' }, - h2(i18n.invitesInhabitantsTitle || 'Inhabitants'), - form( - { action: '/invites/inhabitant/follow', method: 'post' }, - input({ name: 'feedId', id: 'inh_oasis_id', type: 'text', placeholder: '@...=.ed25519', pattern: '@[A-Za-z0-9+/_\\-]{43}=\\.ed25519', required: true, maxlength: 56 }), - br(), - button({ type: 'submit' }, i18n.invitesInhabitantsFollow || 'Give Support') - ) - ) - ), - section( - div({ class: 'invites-peers', id: 'invites-peers' }, + ) : null, + filter === 'others' ? section( + div( h2(i18n.peers || 'Peers'), p(i18n.directConnectDescription), form({ action: '/peers/connect', method: 'post' }, - input({ type: 'text', id: 'peer_host', name: 'host', required: true, placeholder: '192.168.1.100', pattern: '(\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}|[a-zA-Z0-9]([a-zA-Z0-9\\-]*[a-zA-Z0-9])?(\\.[a-zA-Z0-9]([a-zA-Z0-9\\-]*[a-zA-Z0-9])?)*)', title: i18n.peerHostValidation || 'Valid IPv4 (e.g. 192.168.1.100) or hostname (e.g. pub.example.com)', maxlength: 253 }), + input({ type: 'text', id: 'peer_host', name: 'host', required: true, placeholder: '192.168.1.100', pattern: '(\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}|[a-zA-Z0-9]([a-zA-Z0-9\\-]*[a-zA-Z0-9])?(\\.[a-zA-Z0-9]([a-zA-Z0-9\\-]*[a-zA-Z0-9])?)*)', title: i18n.peerHostValidation || 'Valid IPv4 (e.g. 192.168.1.100) or hostname (e.g. pub.solarnethub.com)', maxlength: 253 }), br(), label({ for: 'peer_port' }, i18n.peerPort), br(), @@ -306,9 +234,10 @@ const invitesView = ({ invitesEnabled, flash }) => { button({ type: 'submit' }, i18n.connectAndFollow) ) ) - ) + ) : null ); }; exports.invitesView = invitesView; +exports.renderSuggestedPub = renderSuggestedPub; diff --git a/src/views/jobs_view.js b/src/views/jobs_view.js index 8c6694b..9d89c07 100644 --- a/src/views/jobs_view.js +++ b/src/views/jobs_view.js @@ -1,21 +1,24 @@ -const { form, button, div, h2, p, section, input, label, textarea, br, a, span, select, option, img, progress, video, audio, table, tr, td } = require("../server/node_modules/hyperaxe") -const { template, i18n, userLink, renderStateChip, renderOpenClosedChip, renderVisibilityChip, renderLifespanChip, renderEcoTax, renderSpreadButton, renderContentActions } = require("./main_views") -const moment = require("../server/node_modules/moment") +const { form, button, div, h2, p, section, input, label, textarea, br, a, span, select, option, img, progress, video, audio, table, tr, td, hr } = require("../server/node_modules/hyperaxe") +const { renderZoomableImage } = require("./gallery_view") +const { renderCommentsSection: renderSharedCommentsSection } = require("./comments_view"); +const { renderCallButton, clearnetItemHref, template, i18n, userLink, renderStateChip, renderOpenClosedChip, renderVisibilityChip, renderLifespanChip, renderEcoTax, renderSpreadButton, renderContentActions, renderSpreadEditWarning, renderModuleStatsBy, moduleIsEmpty, renderEcoValueChip, contentDeleteAction, paged, renderCreatedRows } = require("./main_views") const { config } = require("../server/SSB_server.js") -const { renderUrl } = require("../backend/renderUrl") +const { renderStyledText } = require("../backend/renderStyledText") const { renderMapLocationUrl, renderMapEmbed, renderMapLocationVisitLabel } = require("./maps_view") +const { resolvePhoto } = require("./inhabitants_view") +const { renderReachChip, renderClearnetSelector, renderClearnetSwitch } = require("./clearnet_view") const renderMediaBlob = (value, attrs = {}) => { if (!value) return null const s = String(value).trim() if (!s) return null - if (s.startsWith('&')) return img({ src: `/blob/${encodeURIComponent(s)}`, ...attrs }) + if (s.startsWith('&')) return renderZoomableImage(`/blob/${encodeURIComponent(s)}`, { imgClass: attrs.class || 'post-image', alt: attrs.alt || '' }) const mVideo = s.match(/\[video:[^\]]*\]\(\s*(&[^)\s]+\.sha256)\s*\)/) - if (mVideo) return video({ controls: true, class: attrs.class || 'post-video', src: `/blob/${encodeURIComponent(mVideo[1])}` }) + if (mVideo) return video({ controls: true, class: attrs.class || 'post-video', src: `/blob/${encodeURIComponent(mVideo[1])}`, preload: 'metadata' }) const mAudio = s.match(/\[audio:[^\]]*\]\(\s*(&[^)\s]+\.sha256)\s*\)/) if (mAudio) return audio({ controls: true, class: attrs.class || 'post-audio', src: `/blob/${encodeURIComponent(mAudio[1])}` }) const mImg = s.match(/!\[[^\]]*\]\(\s*(&[^)\s]+\.sha256)\s*\)/) - if (mImg) return img({ src: `/blob/${encodeURIComponent(mImg[1])}`, class: attrs.class || 'post-image' }) + if (mImg) return renderZoomableImage(`/blob/${encodeURIComponent(mImg[1])}`, { imgClass: attrs.class || 'post-image', alt: attrs.alt || '' }) return null } @@ -23,7 +26,9 @@ const userId = config.keys.id const FILTERS = [ { key: "ALL", i18n: "jobsFilterAll", title: "jobsAllTitle" }, + { key: "RECENT", i18n: "jobsFilterRecent", title: "jobsRecentTitle" }, { key: "MINE", i18n: "jobsFilterMine", title: "jobsMineTitle" }, + { key: "TOP", i18n: "jobsFilterTop", title: "jobsTopTitle" }, { key: "APPLIED", i18n: "jobsFilterApplied", title: "jobsAppliedTitle" }, { key: "REMOTE", i18n: "jobsFilterRemote", title: "jobsRemoteTitle" }, { key: "PRESENCIAL", i18n: "jobsFilterPresencial", title: "jobsPresencialTitle" }, @@ -32,19 +37,14 @@ const FILTERS = [ { key: "EXCHANGE", i18n: "jobsFilterExchange", title: "jobsExchangeTitle" }, { key: "OPEN", i18n: "jobsFilterOpen", title: "jobsOpenTitle" }, { key: "CLOSED", i18n: "jobsFilterClosed", title: "jobsClosedTitle" }, - { key: "RECENT", i18n: "jobsFilterRecent", title: "jobsRecentTitle" }, - { key: "TOP", i18n: "jobsFilterTop", title: "jobsTopTitle" }, { key: "CV", i18n: "jobsCV", title: "jobsCVTitle" } ] -function resolvePhoto(photoField, size = 256) { - if (typeof photoField === "string" && photoField.startsWith("/image/")) return photoField - if (typeof photoField === "string" && /^&[A-Za-z0-9+/=]+\.sha256$/.test(photoField)) return `/image/${size}/${encodeURIComponent(photoField)}` - return "/assets/images/default-avatar.png" -} - const safeArr = (v) => (Array.isArray(v) ? v : []) const safeText = (v) => String(v || "").trim() +const clearnetEligible = (job) => String(job.status || "").toUpperCase() !== "CLOSED" && String(job.visibility || "PUBLIC").toUpperCase() !== "HIDDEN" +const JOB_REACH = ["PUBLIC", "HIDDEN"] +const jobReachLabel = (v) => v === "HIDDEN" ? (i18n.visibilityHidden || "Hidden") : (i18n.visibilityPublic || "Public") const parseNum = (v) => { const n = parseFloat(String(v ?? "").replace(",", ".")) @@ -72,11 +72,11 @@ const buildReturnTo = (filter, params = {}) => { const renderPmButton = (recipientId) => recipientId && String(recipientId) !== String(userId) - ? form( + ? [form( { method: "GET", action: "/pm" }, input({ type: "hidden", name: "recipients", value: recipientId }), button({ type: "submit", class: "filter-btn" }, i18n.privateMessage) - ) + ), renderCallButton(recipientId, { pam: true })] : null const renderTags = (tags = []) => { @@ -104,15 +104,6 @@ const renderApplicantsProgress = (subsCount, vacants) => { ) } -const renderUpdatedLabel = (createdAt, updatedAt) => { - const createdTs = createdAt ? new Date(createdAt).getTime() : NaN - const updatedTs = updatedAt ? new Date(updatedAt).getTime() : NaN - const showUpdated = Number.isFinite(updatedTs) && (!Number.isFinite(createdTs) || updatedTs !== createdTs) - return showUpdated - ? span({ class: "votations-comment-date" }, ` | ${i18n.jobsUpdatedAt}: ${moment(updatedAt).format("YYYY/MM/DD HH:mm:ss")}`) - : null -} - const renderJobOwnerActions = (job, returnTo) => { const isAuthor = String(job.author) === String(userId) if (!isAuthor) return [] @@ -127,11 +118,6 @@ const renderJobOwnerActions = (job, returnTo) => { { method: "GET", action: `/jobs/edit/${encodeURIComponent(job.id)}` }, input({ type: "hidden", name: "returnTo", value: returnTo }), button({ class: "update-btn", type: "submit" }, i18n.jobsUpdateButton) - ), - form( - { method: "POST", action: `/jobs/delete/${encodeURIComponent(job.id)}` }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - button({ class: "delete-btn", type: "submit" }, i18n.jobsDeleteButton) ) ] } @@ -208,13 +194,14 @@ const renderJobExtraDetails = (job) => { pushRow(i18n.jobsHoursRequested || "Hours requested", `${Number(job.hoursRequested) || 0} h`) if (skill) pushRow(i18n.jobsExchangeSkill || "Skill", skill) } - return table({ class: "tribe-info-table jobs-info-table" }, ...rows) + return table({ class: "tribe-info-table jobs-info-table" }, ...rows, ...(opts.includeCreated ? renderCreatedRows(job.createdAt, job.author, job.updatedAt) : [])) } const renderJobList = exports.renderJobList = (jobs, filter, params = {}) => { const list = safeArr(jobs) - if (!list.length) return p(i18n.noJobsMatch || i18n.noJobsFound) + if (!list.length) return p((params.search || params.q || params.minSalary || params.maxSalary) ? (i18n.noJobsMatch || i18n.noJobsFound) : (i18n.noJobsFound || i18n.noJobsMatch)) + const returnTo = buildReturnTo(filter, params) return div({ class: "jobs-grid" }, list.map((job) => { @@ -232,7 +219,9 @@ const renderJobList = exports.renderJobList = (jobs, filter, params = {}) => { renderJobStatusChip(job.status), isHidden ? renderJobHiddenChip() : null, isSubscribed ? renderJobAppliedChip() : null, - renderLifespanChip(job.lifetime, i18n) + job.industry ? a({ href: `/industry/${encodeURIComponent(job.industry)}` }, renderStateChip("whole", "🏭", String(i18n.industryTitle || "Industry").toUpperCase())) : null, + renderLifespanChip(job.lifetime, i18n), + job.clearnet === true && clearnetEligible(job) ? renderReachChip(true, i18n, clearnetItemHref('jobs', job.title, job.id)) : null ].filter(Boolean) const isExchange = String(job.job_type || "").toLowerCase() === "exchange" const compensationText = isExchange @@ -244,7 +233,7 @@ const renderJobList = exports.renderJobList = (jobs, filter, params = {}) => { div( { class: "card-header activity-card-header" }, span(), - renderContentActions(job.id, `/jobs/${encodeURIComponent(job.id)}`) + renderContentActions(job.id, `/jobs/${encodeURIComponent(job.id)}`, { spread: params.spreadMap && params.spreadMap.get(job.id) || null, author: job.author, favKind: 'jobs', isFavorite: job.isFavorite, reportTitle: job.title, returnTo, deleteAction: isOwn ? contentDeleteAction("job", job.id) : null }) ), div({ class: "card-section job-card-body" }, heroNode, @@ -255,11 +244,10 @@ const renderJobList = exports.renderJobList = (jobs, filter, params = {}) => { ) ), chips.length ? div({ class: "card-chips-row" }, ...chips) : null, - div({ class: "card-date-highlight" }, compensationText), + div({ class: "price-chip" }, compensationText), div({ class: "tribe-card-members" }, span({ class: "tribe-members-count" }, `${i18n.jobSubscribers}: ${subs.length}`) - ), - div({ class: "card-spread-centered" }, renderSpreadButton(job.id, params.spreadMap && params.spreadMap.get(job.id))) + ) ) ) ) @@ -267,10 +255,32 @@ const renderJobList = exports.renderJobList = (jobs, filter, params = {}) => { ) } -const renderJobForm = (job = {}, mode = "create") => { +const renderJobForm = (job = {}, mode = "create", spreadWarning = null, params = {}) => { const isEdit = mode === "edit" + const reachRaw = String(params.reach || "").toUpperCase() + const ownReach = String(job.visibility || "").toUpperCase() + const reach = JOB_REACH.includes(reachRaw) ? reachRaw : (JOB_REACH.includes(ownReach) ? ownReach : "PUBLIC") + const prefillKeys = ["industry", "title", "description", "tasks", "salary"] return div( { class: "div-center job-form" }, + isEdit ? spreadWarning : null, + form( + { method: "GET", action: isEdit ? `/jobs/edit/${encodeURIComponent(job.id)}` : "/jobs" }, + isEdit ? null : input({ type: "hidden", name: "filter", value: "CREATE" }), + ...(isEdit ? [] : prefillKeys.filter((k) => params[k]).map((k) => input({ type: "hidden", name: k, value: params[k] }))), + label(i18n.visibilityLabel || "Visibility"), + br(), + div({ class: "apply-row" }, + select( + { name: "visibility", class: "report-category-select" }, + option({ value: "PUBLIC", ...(reach === "PUBLIC" ? { selected: true } : {})}, i18n.visibilityPublic || "Public"), + option({ value: "HIDDEN", ...(reach === "HIDDEN" ? { selected: true } : {})}, i18n.visibilityHidden || "Hidden") + ), + button({ type: "submit", class: "create-button" }, i18n.apply || "Apply") + ) + ), + hr({ class: "form-sep" }), + h2({ class: "report-category-fixed" }, jobReachLabel(reach)), form( { action: isEdit ? `/jobs/update/${encodeURIComponent(job.id)}` : "/jobs/create", @@ -278,6 +288,8 @@ const renderJobForm = (job = {}, mode = "create") => { enctype: "multipart/form-data" }, input({ type: "hidden", name: "returnTo", value: "/jobs?filter=MINE" }), + input({ type: "hidden", name: "visibility", value: reach }), + job.industry ? input({ type: "hidden", name: "industry", value: job.industry }) : null, label(i18n.jobType), br(), select( @@ -290,7 +302,7 @@ const renderJobForm = (job = {}, mode = "create") => { br(), label(i18n.jobTitle), br(), - input({ type: "text", name: "title", required: true, placeholder: i18n.jobTitlePlaceholder, value: job.title || "" }), + input({ type: "text", name: "title", maxlength: "100", required: true, placeholder: i18n.jobTitlePlaceholder, value: job.title || "" }), br(), br(), label(i18n.jobImage), @@ -301,17 +313,17 @@ const renderJobForm = (job = {}, mode = "create") => { br(), label(i18n.jobDescription), br(), - textarea({ name: "description", rows: "6", required: true, placeholder: i18n.jobDescriptionPlaceholder }, job.description || ""), + textarea({ maxlength: "5000", name: "description", rows: "6", required: true, placeholder: i18n.jobDescriptionPlaceholder }, job.description || ""), br(), br(), label(i18n.jobRequirements), br(), - textarea({ name: "requirements", rows: "6", placeholder: i18n.jobRequirementsPlaceholder }, job.requirements || ""), + textarea({ maxlength: "5000", name: "requirements", rows: "6", placeholder: i18n.jobRequirementsPlaceholder }, job.requirements || ""), br(), br(), label(i18n.jobsTagsLabel), br(), - input({ type: "text", name: "tags", value: Array.isArray(job.tags) ? job.tags.join(", ") : (job.tags || "") }), + input({ type: "text", name: "tags", placeholder: i18n.tagsPlaceholder, value: Array.isArray(job.tags) ? job.tags.join(", ") : (job.tags || "") }), br(), br(), label(i18n.jobLanguages), @@ -323,22 +335,22 @@ const renderJobForm = (job = {}, mode = "create") => { br(), select( { name: "job_time", required: true }, - option({ value: "partial", selected: job.job_time === "partial" }, i18n.jobTimePartial), - option({ value: "complete", selected: job.job_time === "complete" }, i18n.jobTimeComplete) + option({ value: "partial", ...(job.job_time === "partial" ? { selected: true } : {})}, i18n.jobTimePartial), + option({ value: "complete", ...(job.job_time === "complete" ? { selected: true } : {})}, i18n.jobTimeComplete) ), br(), br(), label(i18n.jobTasks), br(), - textarea({ name: "tasks", rows: "6", placeholder: i18n.jobTasksPlaceholder }, job.tasks || ""), + textarea({ maxlength: "5000", name: "tasks", rows: "6", placeholder: i18n.jobTasksPlaceholder }, job.tasks || ""), br(), br(), label(i18n.jobLocation), br(), select( { name: "location", required: true }, - option({ value: "remote", selected: job.location === "remote" }, i18n.jobLocationRemote), - option({ value: "presencial", selected: job.location === "presencial" }, i18n.jobLocationPresencial) + option({ value: "remote", ...(job.location === "remote" ? { selected: true } : {})}, i18n.jobLocationRemote), + option({ value: "presencial", ...(job.location === "presencial" ? { selected: true } : {})}, i18n.jobLocationPresencial) ), br(), br(), @@ -347,15 +359,7 @@ const renderJobForm = (job = {}, mode = "create") => { input({ type: "text", name: "mapUrl", placeholder: i18n.mapUrlPlaceholder || "/maps/MAP_ID", value: job.mapUrl || "" }), br(), br(), - label(i18n.visibilityLabel || "Visibility"), - br(), - select( - { name: "visibility" }, - option({ value: "PUBLIC", selected: (job.visibility || "PUBLIC") === "PUBLIC" }, i18n.visibilityPublic || "Public"), - option({ value: "HIDDEN", selected: job.visibility === "HIDDEN" }, i18n.visibilityHidden || "Hidden") - ), - br(), - br(), + reach === "PUBLIC" && clearnetEligible({ ...job, visibility: reach }) ? renderClearnetSelector(job.clearnet === true || String(job.clearnet || "") === "1", i18n) : null, label(i18n.jobVacants), br(), input({ type: "number", name: "vacants", min: "1", placeholder: i18n.jobVacantsPlaceholder, value: job.vacants || 1, required: true }), @@ -366,6 +370,7 @@ const renderJobForm = (job = {}, mode = "create") => { label(i18n.jobSalary), br(), input({ type: "number", name: "salary", step: "0.000001", min: "0", value: job.salary || "" }), + renderEcoValueChip(), br(), br() ] @@ -415,11 +420,26 @@ const renderCVList = (inhabitants) => ), div( { class: "inhabitant-details" }, - user.description ? p(...renderUrl(user.description)) : null, + user.description ? p(...renderStyledText(user.description)) : null, p(userLink(user.id)), + user.skills && user.skills.length + ? div({ class: "card-tags" }, + user.skills.slice(0, 12).map((skill) => + a({ href: `/search?query=%23${encodeURIComponent(skill)}`, class: "tag-link" }, `#${skill}`)) + ) + : null, + typeof user.matchScore === "number" + ? div({ class: "cv-match-row" }, + span({ class: "card-label" }, `${i18n.matchScore}: `), + span({ class: "card-value" }, `${Math.round(user.matchScore * 100)}%`) + ) + : null, div( { class: "cv-actions" }, - form({ method: "GET", action: `/inhabitant/${encodeURIComponent(user.id)}` }, button({ type: "submit", class: "filter-btn" }, i18n.inhabitantviewDetails)), + form({ method: "GET", action: `/inhabitant/${encodeURIComponent(user.id)}` }, button({ type: "submit", class: "filter-btn" }, i18n.cvVisitButton)), + user.pdf ? a({ href: `/blob/${encodeURIComponent(user.pdf)}`, target: "_blank", rel: "noopener", class: "filter-btn" }, "📄 " + i18n.cvPdfLabel) : null, + form({ method: "GET", action: `/cv/pdf/${encodeURIComponent(user.id)}` }, button({ type: "submit", class: "filter-btn" }, i18n.generatePdf)), + form({ method: "POST", action: `/cv/share/${encodeURIComponent(user.id)}` }, button({ type: "submit", class: "filter-btn" }, i18n.sharePm)), !isMe ? renderPmButton(user.id) : null ) ) @@ -429,25 +449,41 @@ const renderCVList = (inhabitants) => ) exports.jobsView = async (jobsOrCVs, filter = "ALL", params = {}) => { + if (filter === "EDIT") { + const editing = Array.isArray(jobsOrCVs) ? jobsOrCVs[0] : jobsOrCVs + params = { ...params, spreadWarning: await renderSpreadEditWarning(editing && editing.id) } + } const search = safeText(params.search || "") const minSalary = params.minSalary ?? "" const maxSalary = params.maxSalary ?? "" const sort = safeText(params.sort || "recent") + const emptyMod = moduleIsEmpty(Array.isArray(jobsOrCVs) ? jobsOrCVs : [], filter, "ALL", search || String(minSalary || "") || String(maxSalary || "")); + const censusJ = Array.isArray(params.censusList) ? params.censusList : (Array.isArray(jobsOrCVs) ? jobsOrCVs : []); + const jobChip = (x) => { + const m = x.key; + if (m === filter) return true; + if (m === "TOP") return censusJ.length > 0; + if (m === "MINE") return censusJ.some(j => String(j.author) === String(userId)); + if (m === "RECENT") return censusJ.length > 0; + if (m === "APPLIED") return censusJ.some(j => safeArr(j.subscribers).includes(userId)); + if (m === "REMOTE") return censusJ.some(j => String(j.location || "").toLowerCase() === "remote"); + if (m === "PRESENCIAL") return censusJ.some(j => String(j.location || "").toLowerCase() === "presencial"); + if (m === "FREELANCER" || m === "EMPLOYEE" || m === "EXCHANGE") return censusJ.some(j => String(j.job_type || "").toUpperCase() === m); + if (m === "OPEN" || m === "CLOSED") return censusJ.some(j => String(j.status || "OPEN").toUpperCase() === m); + if (m === "CV") return params.anyCVs === true; + return true; + }; const filterObj = FILTERS.find((f) => f.key === filter) || FILTERS[0] const sectionTitle = i18n[filterObj.title] || i18n.jobsTitle - const { renderReachChip: renderReachChipJobs } = require('./clearnet_view'); - const viewerClearnet = !!(params.viewerPrefs && params.viewerPrefs.clearnetJobs) return template( i18n.jobsTitle, section( - div({ class: "tags-header" }, + div({ class: "tags-header module-header-line" }, h2(sectionTitle), p(i18n.jobsDescription) ), - div({ class: "shop-title-row" }, renderReachChipJobs(viewerClearnet, i18n)), - br(), div( { class: "filters" }, form( @@ -456,9 +492,9 @@ exports.jobsView = async (jobsOrCVs, filter = "ALL", params = {}) => { input({ type: "hidden", name: "minSalary", value: String(minSalary ?? "") }), input({ type: "hidden", name: "maxSalary", value: String(maxSalary ?? "") }), input({ type: "hidden", name: "sort", value: sort }), - ...FILTERS.map((f) => - button({ type: "submit", name: "filter", value: f.key, class: filter === f.key ? "filter-btn active" : "filter-btn" }, i18n[f.i18n]) - ), + ...(emptyMod ? [] : FILTERS.filter(jobChip).map((f) => + button({ type: "submit", name: "filter", value: f.key, class: filter === f.key ? "filter-btn active" : "filter-btn" }, String(i18n[f.i18n]).toUpperCase()) + )), button({ type: "submit", name: "filter", value: "CREATE", class: "create-button" }, i18n.jobsCreateJob) ) ) @@ -477,100 +513,54 @@ exports.jobsView = async (jobsOrCVs, filter = "ALL", params = {}) => { ) ), br(), - renderCVList(jobsOrCVs) + renderCVList(paged(jobsOrCVs)) ) : filter === "CREATE" || filter === "EDIT" ? (() => { - const jobToEdit = filter === "EDIT" ? (Array.isArray(jobsOrCVs) ? jobsOrCVs[0] : {}) : {} - return renderJobForm(jobToEdit, filter === "EDIT" ? "edit" : "create") + const jobToEdit = filter === "EDIT" + ? (Array.isArray(jobsOrCVs) ? jobsOrCVs[0] : {}) + : { + ...(params.industry ? { industry: params.industry } : {}), + ...(params.title ? { title: params.title } : {}), + ...(params.description ? { description: params.description } : {}), + ...(params.tasks ? { tasks: params.tasks } : {}), + ...(params.salary ? { salary: params.salary } : {}), + ...(params.draft || {}) + } + return renderJobForm(jobToEdit, filter === "EDIT" ? "edit" : "create", params.spreadWarning, params) })() : section( - div( - { class: "jobs-search" }, + emptyMod ? null : div( + { class: "jobs-search activity-filter-chips activity-toolbar-row" }, + renderModuleStatsBy(jobsOrCVs, j => String(j.status || 'OPEN').toUpperCase(), [{ value: 'OPEN', label: i18n.jobsFilterOpen }, { value: 'CLOSED', label: i18n.jobsFilterClosed }]), form( { method: "GET", action: "/jobs", class: "filter-box" }, input({ type: "hidden", name: "filter", value: filter || "ALL" }), input({ type: "text", name: "search", value: search, placeholder: i18n.jobsSearchPlaceholder, class: "filter-box__input" }), - div( - { class: "filter-box__controls" }, - div( - { class: "transfer-range" }, - input({ type: "number", name: "minSalary", step: "0.000001", min: "0", value: String(minSalary ?? ""), placeholder: i18n.jobsMinSalaryLabel, class: "filter-box__number transfer-amount-input" }), - input({ type: "number", name: "maxSalary", step: "0.000001", min: "0", value: String(maxSalary ?? ""), placeholder: i18n.jobsMaxSalaryLabel, class: "filter-box__number transfer-amount-input" }) - ), - select( - { name: "sort", class: "filter-box__select" }, - option({ value: "recent", selected: sort === "recent" }, i18n.jobsSortRecent), - option({ value: "salary", selected: sort === "salary" }, i18n.jobsSortSalary), - option({ value: "subscribers", selected: sort === "subscribers" }, i18n.jobsSortSubscribers) - ), - button({ type: "submit", class: "filter-box__button" }, i18n.jobsSearchButton) - ) + input({ type: "number", name: "minSalary", step: "0.000001", min: "0", value: String(minSalary ?? ""), placeholder: i18n.jobsMinSalaryLabel, class: "filter-box__number transfer-amount-input" }), + input({ type: "number", name: "maxSalary", step: "0.000001", min: "0", value: String(maxSalary ?? ""), placeholder: i18n.jobsMaxSalaryLabel, class: "filter-box__number transfer-amount-input" }), + select( + { name: "sort", class: "filter-box__select" }, + option({ value: "recent", ...(sort === "recent" ? { selected: true } : {})}, i18n.jobsSortRecent), + option({ value: "salary", ...(sort === "salary" ? { selected: true } : {})}, i18n.jobsSortSalary), + option({ value: "subscribers", ...(sort === "subscribers" ? { selected: true } : {})}, i18n.jobsSortSubscribers) + ), + button({ type: "submit", class: "filter-box__button" }, i18n.jobsSearchButton) ) ), - br(), - div({ class: "jobs-list" }, renderJobList(jobsOrCVs, filter, { ...params, search, minSalary, maxSalary, sort })) + div({ class: "jobs-list" }, renderJobList(paged(jobsOrCVs), filter, { ...params, search, minSalary, maxSalary, sort })) ) ) ) } const renderJobCommentsSection = (jobId, returnTo, comments = []) => { - const list = safeArr(comments).filter(c => { - const t = c && c.value && c.value.content && c.value.content.text - return t && String(t).trim() - }) - const commentsCount = list.length - - return div( - { id: "comments", class: "vote-comments-section" }, - div( - { class: "comments-count" }, - span({ class: "card-label" }, i18n.voteCommentsLabel + ": "), - span({ class: "card-value" }, String(commentsCount)) - ), - div( - { class: "comment-form-wrapper" }, - h2({ class: "comment-form-title" }, i18n.voteNewCommentLabel), - form( - { method: "POST", action: `/jobs/${encodeURIComponent(jobId)}/comments`, class: "comment-form", enctype: "multipart/form-data" }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - textarea({ id: "comment-text", name: "text", rows: 4, class: "comment-textarea", placeholder: i18n.voteNewCommentPlaceholder }), - div({ class: "comment-file-upload" }, label(i18n.uploadMedia), input({ type: "file", name: "blob" })), - br(), - button({ type: "submit", class: "comment-submit-btn" }, i18n.voteNewCommentButton) - ) - ), - list.length - ? div( - { class: "comments-list" }, - list.map((c) => { - const author = c?.value?.author || "" - const ts = c?.value?.timestamp || c?.timestamp - const absDate = ts ? moment(ts).format("YYYY/MM/DD HH:mm:ss") : "" - const relDate = ts ? moment(ts).fromNow() : "" - const userName = author && author.includes("@") ? author.split("@")[1] : author - const rootId = c?.value?.content ? (c.value.content.fork || c.value.content.root) : null - const text = c?.value?.content?.text || "" - - return div( - { class: "votations-comment-card" }, - span( - { class: "created-at" }, - span(i18n.createdBy), - author ? userLink(author) : span("(unknown)"), - absDate ? span(" | ") : "", - absDate ? span({ class: "votations-comment-date" }, absDate) : "", - relDate ? span({ class: "votations-comment-date" }, " | ", i18n.sendTime) : "", - relDate && rootId ? a({ href: `/thread/${encodeURIComponent(rootId)}#${encodeURIComponent(c.key)}` }, relDate) : "" - ), - p({ class: "votations-comment-text" }, ...renderUrl(text)) - ) - }) - ) - : p({ class: "votations-no-comments" }, i18n.voteNoCommentsYet) - ) -} + return renderSharedCommentsSection({ + action: `/jobs/${encodeURIComponent(jobId)}/comments`, + comments: comments, + returnTo: returnTo + }); +}; const renderCandidates = (candidates, jobId) => { if (!Array.isArray(candidates) || candidates.length === 0) return null; @@ -595,7 +585,7 @@ const renderCandidates = (candidates, jobId) => { ), div( { class: "inhabitant-details" }, - c.description ? p(...renderUrl(c.description)) : null, + c.description ? p(...renderStyledText(c.description)) : null, p(userLink(c.id)), div({ class: "matchskills" }, p(`${i18n.matchScore || 'Match score'}: ${Math.round(c.matchScore * 100)}%`), @@ -613,7 +603,8 @@ const renderCandidates = (candidates, jobId) => { input({ type: 'hidden', name: 'subject', value: `${i18n.jobsTitle || 'Job'}: ${job.title || ''}`.slice(0, 150) }), input({ type: 'hidden', name: 'text', value: `${i18n.jobsCandidatesPmBody || 'Hi, I think your profile matches my job opening'}: /jobs/${jobId}` }), button({ type: 'submit', class: 'filter-btn' }, i18n.pmCreateButton || 'Send PM') - ) + ), + renderCallButton(c.id, { pam: true }) ) ) )) @@ -627,21 +618,22 @@ exports.singleJobsView = async (job, filter = "ALL", comments = [], params = {}) const tagsNode = renderTags(job.tags) const isAuthor = String(job.author) === String(userId) const isSubscribed = subs.includes(userId) - const { renderReachChip } = require('./clearnet_view') - const isClearnet = !!(params.authorPrefs && params.authorPrefs.clearnetJobs && String(job.status || '').toUpperCase() !== 'CLOSED' && String(job.visibility || 'PUBLIC').toUpperCase() !== 'HIDDEN') + const canClearnet = clearnetEligible(job) + const isClearnet = canClearnet && !!job.clearnet const candidatesBlock = isAuthor ? renderCandidates(params.candidates || [], job.id) : null const ownerActions = renderJobOwnerActions(job, returnTo) const applyToggle = renderJobApplyToggle(job, returnTo) - const pmBtn = renderPmButton(job.author) const visibility = (job.visibility || 'PUBLIC').toUpperCase() === 'HIDDEN' ? 'HIDDEN' : 'PUBLIC' const chips = [ renderJobStatusChip(job.status), visibility === 'HIDDEN' ? renderJobHiddenChip() : null, isSubscribed ? renderJobAppliedChip() : null, + job.industry ? a({ href: `/industry/${encodeURIComponent(job.industry)}` }, renderStateChip("whole", "🏭", String(i18n.industryTitle || "Industry").toUpperCase())) : null, renderLifespanChip(job.lifetime, i18n), renderEcoTax(job.msgSize, job.id), - renderReachChip(isClearnet, i18n) + renderReachChip(isClearnet, i18n, isClearnet ? clearnetItemHref('jobs', job.title, job.id) : null), + isAuthor && canClearnet ? renderClearnetSwitch('jobs', job.rootId || job.id, isClearnet) : null ].filter(Boolean) const nextVisibility = visibility === 'PUBLIC' ? 'HIDDEN' : 'PUBLIC' @@ -660,19 +652,20 @@ exports.singleJobsView = async (job, filter = "ALL", comments = [], params = {}) const sideActions = [] if (applyToggle) sideActions.push(applyToggle) - if (pmBtn) sideActions.push(pmBtn) for (const a of ownerActions) sideActions.push(a) const jobSide = div({ class: "tribe-side" }, + div({ class: "card-header activity-card-header" }, + renderContentActions(job.id, `/jobs/${encodeURIComponent(job.id)}`, { spread: params.spreads || null, author: job.author, favKind: 'jobs', isFavorite: job.isFavorite, reportTitle: job.title, returnTo, deleteAction: isAuthor ? contentDeleteAction("job", job.id) : null }) + ), div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, safeText(job.title) || i18n.jobsTitle) ), chips.length ? div({ class: "card-chips-row" }, ...chips) : null, - div({ class: "card-spread-centered" }, renderSpreadButton(job.id, params.spreads)), job.image ? renderMediaBlob(job.image, { class: "tribe-detail-image" }) : null, - renderJobInfoTable(job, { includeVacants: true, includeSubscribers: false }), + renderJobInfoTable(job, { includeVacants: true, includeSubscribers: false, includeCreated: true }), div({ class: "tribe-card-members" }, span({ class: "tribe-members-count" }, `${i18n.jobSubscribers}: ${subs.length}`) ), @@ -687,7 +680,7 @@ exports.singleJobsView = async (job, filter = "ALL", comments = [], params = {}) safeText(bodyText) ? div({ class: "job-section" }, h2({ class: "job-section-title" }, titleText), - p({ class: "tribe-side-description" }, ...renderUrl(bodyText)) + p({ class: "tribe-side-description" }, ...renderStyledText(bodyText)) ) : null @@ -698,17 +691,13 @@ exports.singleJobsView = async (job, filter = "ALL", comments = [], params = {}) extraDetailsTable ? div({ class: "job-section" }, extraDetailsTable) : null, renderJobSection(i18n.jobTasks, job.tasks), job.mapUrl ? div({ class: "job-section" }, renderMapEmbed(params.mapData, job.mapUrl)) : null, - p({ class: "card-footer" }, - span({ class: "date-link" }, `${moment(job.createdAt).format("YYYY/MM/DD HH:mm")} ${i18n.performed} `), - userLink(job.author), - renderUpdatedLabel(job.createdAt, job.updatedAt) - ), candidatesBlock ? candidatesBlock : null, renderJobCommentsSection(job.id, returnTo, comments) ) return template( i18n.jobsTitle, + section(div({ class: "tags-header module-header-line" }, h2(i18n.jobsTitle), p(i18n.jobsDescription))), section( div( { class: "filters" }, @@ -719,7 +708,7 @@ exports.singleJobsView = async (job, filter = "ALL", comments = [], params = {}) input({ type: "hidden", name: "maxSalary", value: String(params.maxSalary ?? "") }), input({ type: "hidden", name: "sort", value: safeText(params.sort || "recent") }), ...FILTERS.map((f) => - button({ type: "submit", name: "filter", value: f.key, class: filter === f.key ? "filter-btn active" : "filter-btn" }, i18n[f.i18n]) + button({ type: "submit", name: "filter", value: f.key, class: filter === f.key ? "filter-btn active" : "filter-btn" }, String(i18n[f.i18n]).toUpperCase()) ), button({ type: "submit", name: "filter", value: "CREATE", class: "create-button" }, i18n.jobsCreateJob) ) @@ -730,17 +719,19 @@ exports.singleJobsView = async (job, filter = "ALL", comments = [], params = {}) } exports.clearnetJobView = async (job) => { - const { escapeHtml: esc, blobUrl: cnBlob, renderClearnetPage } = require('./clearnet_view'); - const title = esc(job.title || 'Job'); - const desc = esc(job.description || ''); + const { escapeHtml: esc, renderRichText, renderKindTag, blobUrl: cnBlob, renderClearnetPage } = require('./clearnet_view'); + const title = esc(job.title || i18n.cnKindJob); + const desc = renderRichText(job.description || ''); const req = esc(job.requirements || ''); const lang = esc(String(job.languages || '').toUpperCase()); const loc = esc(String(job.location || '').toUpperCase()); const jobType = String(job.job_type || '').toLowerCase(); - const jobTypeLabel = jobType === 'exchange' ? 'Hour exchange' : jobType === 'employee' ? 'Employee' : 'Freelancer'; + const jobTime = String(job.job_time || '').toLowerCase(); + const jobTimeLabel = jobTime === 'partial' ? i18n.jobTimePartial : jobTime === 'complete' ? i18n.jobTimeComplete : ''; + const jobTypeLabel = jobType === 'exchange' ? i18n.jobTypeExchange : jobType === 'employee' ? i18n.jobTypeSalary : i18n.jobTypeFreelance; let compensation = ''; if (jobType === 'exchange') { - compensation = `${Number(job.hoursOffered || 0)}h offered · ${Number(job.hoursRequested || 0)}h requested`; + compensation = `${esc(i18n.jobsHoursOffered)}: ${Number(job.hoursOffered || 0)}h · ${esc(i18n.jobsHoursRequested)}: ${Number(job.hoursRequested || 0)}h`; if (job.exchangeSkill) compensation += ` · ${esc(job.exchangeSkill)}`; } else { compensation = `${parseFloat(job.salary || 0).toFixed(2)} ECO`; @@ -750,7 +741,6 @@ exports.clearnetJobView = async (job) => { .cn-job-title{color:var(--fg);margin:0 0 16px 0;font-size:32px;font-weight:700} .cn-job-meta{display:flex;flex-wrap:wrap;gap:10px;margin-bottom:20px} .cn-job-meta-item{background:var(--bg-sub);border:1px solid var(--border);border-radius:6px;padding:8px 14px;font-size:14px;color:var(--fg-soft);display:inline-flex;align-items:center;gap:6px} -.cn-job-comp{background:var(--bg-sub);border:1px solid var(--fg);color:var(--fg);padding:8px 16px;border-radius:6px;font-weight:600;display:inline-block;margin-bottom:20px} .cn-job-img{display:block;max-width:100%;border:1px solid var(--border);border-radius:8px;margin-bottom:20px} .cn-job-section h2{color:var(--fg);font-size:18px;text-transform:uppercase;letter-spacing:2px;margin:24px 0 10px;padding-bottom:6px;border-bottom:1px solid var(--border)} .cn-job-section p{color:var(--fg-soft);white-space:pre-wrap;line-height:1.6;font-size:15px} @@ -758,18 +748,21 @@ exports.clearnetJobView = async (job) => { const body = `

${title}

- 💼 ${jobTypeLabel} + ${renderKindTag('job')} + 💼 ${esc(jobTypeLabel)} + ${jobTimeLabel ? `⏱ ${esc(jobTimeLabel)}` : ''} ${job.createdAt ? `📅 ${esc(new Date(job.createdAt).toISOString().slice(0,10))}` : ''} ${loc ? `📍 ${loc}` : ''} ${lang ? `🗣 ${lang}` : ''} + ${compensation ? `${compensation}` : ''}
-
${compensation}
+
${jobImg ? `${title}` : ''} - ${desc ? `

Description

${desc}

` : ''} - ${req ? `

Requirements

${req}

` : ''} + ${desc ? `

${esc(i18n.description)}

${desc}

` : ''} + ${req ? `

${esc(i18n.jobRequirements)}

${req}

` : ''} `; return renderClearnetPage({ - title: `${job.title || 'Job'} — Oasis`, + title: `${job.title || 'Job'} | Oasis`, ogTitle: job.title || 'Job', ogDescription: job.description || '', ogImage: jobImg, diff --git a/src/views/larp_view.js b/src/views/larp_view.js index b9de0b5..fe69c1f 100644 --- a/src/views/larp_view.js +++ b/src/views/larp_view.js @@ -1,5 +1,9 @@ -const { div, h1, h2, h3, p, section, button, form, input, span, img, a, br, table, tr, td, textarea, label, strong } = require("../server/node_modules/hyperaxe"); -const { template, i18n, userLink } = require("./main_views"); +const { div, h1, h2, h3, p, section, button, form, input, span, img, a, br, table, tr, td, textarea, label, strong, details, summary } = require("../server/node_modules/hyperaxe"); +const { renderStyledText } = require("../backend/renderStyledText"); +const { template, i18n, userLink, paged, renderIcon } = require("./main_views"); +const { config } = require("../server/SSB_server.js"); + +const userId = config.keys.id; const moment = require("../server/node_modules/moment"); const fmtCycle = (c) => c && c.formatted ? c.formatted : ''; @@ -11,16 +15,24 @@ const renderCycleBanner = (cycle) => div({ class: 'larp-cycle-banner' }, span({ class: 'larp-cycle-value' }, fmtCycle(cycle)) ); -const renderHouseBadges = ({ myHouse, governingHouse }) => div({ class: 'larp-house-badges' }, - myHouse ? div({ class: 'larp-my-house' }, - span({ class: 'larp-my-house-label' }, i18n.larpMyHouse || 'My House'), - a({ href: `/larp/${myHouse.key}`, class: 'larp-my-house-link' }, myHouse.name) - ) : null, - governingHouse ? div({ class: 'larp-my-house' }, - span({ class: 'larp-my-house-label' }, i18n.larpGoverning || 'Governing'), - a({ href: `/larp/${governingHouse.key}`, class: 'larp-my-house-link' }, governingHouse.name) - ) : null -); +const renderHouseBadges = ({ myHouse, governingHouse, houses }) => { + const academia = Array.isArray(houses) ? houses.find(h => h.key === 'academia') : null; + const isNewcomer = !myHouse; + return div({ class: 'larp-house-badges' }, + myHouse ? div({ class: 'larp-my-house' }, + span({ class: 'larp-my-house-label' }, i18n.larpMyHouse || 'My House'), + a({ href: `/larp/${myHouse.key}`, class: 'larp-my-house-link' }, myHouse.name) + ) : null, + governingHouse ? div({ class: 'larp-my-house' }, + span({ class: 'larp-my-house-label' }, i18n.larpGoverning || 'Ruling:'), + a({ href: `/larp/${governingHouse.key}`, class: 'larp-my-house-link' }, governingHouse.name) + ) : null, + isNewcomer && academia ? div({ class: 'larp-my-house larp-academia-hint' }, + span({ class: 'larp-my-house-label' }, i18n.larpStartHere), + a({ href: `/larp/${academia.key}`, class: 'larp-my-house-link' }, academia.name) + ) : null + ); +}; const renderHouseNav = (houses, currentKey) => div({ class: 'larp-house-nav' }, span({ class: 'larp-house-nav-label' }, i18n.larpAllHouses || 'Houses:'), @@ -31,7 +43,7 @@ const renderHouseNav = (houses, currentKey) => div({ class: 'larp-house-nav' }, }, h.name)) ); -const renderHousePanel = (house, members, posts, { canPost, viewerHouseKey, cyclesUntilRuling }) => { +const renderHousePanel = (house, members, posts, { canPost, viewerHouseKey, cyclesUntilRuling, canTakeTest = false }) => { const { renderEncryptedChip } = require('./clearnet_view'); const isInHouse = viewerHouseKey === house.key; const isInLarp = viewerHouseKey !== null && viewerHouseKey !== undefined; @@ -66,6 +78,9 @@ const renderHousePanel = (house, members, posts, { canPost, viewerHouseKey, cycl ), p({ class: 'larp-detail-description' }, house.description), div({ class: 'larp-actions' }, + isAcademia && isInHouse && canTakeTest + ? a({ href: '/larp/test', class: 'filter-btn larp-start-journey' }, i18n.larpStartJourney) + : null, isInHouse ? a({ href: `/larp/tribe/${encodeURIComponent(house.key)}`, class: 'filter-btn' }, i18n.larpVisitTribe || 'Visit Tribe') : null, @@ -162,53 +177,56 @@ const renderAcademiaJoinPanel = (allHouses, testStatus, housesById, questions, m div({ class: 'larp-academia-grid' }, ordered.map(h => a({ href: `/larp/${h.key}`, class: h.key === myKey ? 'larp-academia-thumb larp-academia-thumb-mine' : 'larp-academia-thumb' }, img({ src: houseImageSrc(h), alt: h.name, class: 'larp-academia-thumb-image' }), - span({ class: 'larp-academia-thumb-name' }, h.name) + span({ class: 'larp-academia-thumb-name' }, h.name), + table({ class: 'larp-info-table larp-academia-thumb-table' }, + tr(td({ class: 'card-label' }, i18n.larpRolesLabel || 'Roles'), td({ class: 'card-value' }, h.roles)), + tr(td({ class: 'card-label' }, i18n.larpFunctionLabel || 'Function'), td({ class: 'card-value' }, h.function)), + tr(td({ class: 'card-label' }, i18n.larpMembersCount || 'Members'), td({ class: 'card-value' }, String(h.memberCount || 0))) + ) )) ), - canTake && qs.length - ? [ - h2(i18n.larpWillTestTitle || 'WILL test'), - p({ class: 'larp-will-test-hint' }, i18n.larpWillTestHint || 'Once completed, you will be assigned the house that best matches your answers. Your individual answers are processed locally and never stored — only the resulting house assignment is published to your feed.'), - form({ method: 'POST', action: '/larp/test', class: 'larp-test-form larp-test-form-compact' }, - qs.map((q, idx) => div({ class: 'larp-test-question' }, - p({ class: 'larp-test-q-text' }, strong(`${idx + 1}. `), i18n[q.key] || q.question), - div({ class: 'larp-test-options' }, - q.options.map((opt, oi) => label({ class: 'larp-test-option' }, - input({ type: 'radio', name: `q${idx}`, value: String(oi), required: 'required' }), - ' ', i18n[opt.key] || opt.text - )) - ) - )), - div({ class: 'larp-actions' }, - button({ type: 'submit', class: 'filter-btn' }, i18n.larpTestSubmit || 'Join House') - ) - ) - ] - : null ); }; -const renderPostsBlock = (posts, house, canPost) => div({ class: 'larp-posts-block' }, - h2(i18n.larpPostsTitle || 'Wall'), - canPost - ? form({ method: 'POST', action: '/larp/post', class: 'larp-post-form' }, - input({ type: 'hidden', name: 'house', value: house.key }), - textarea({ id: 'larp_post_text', name: 'text', rows: '3', maxlength: '4000', placeholder: i18n.larpPostPlaceholder || 'What does this house need to say?' }), - button({ type: 'submit', class: 'filter-btn larp-post-submit' }, i18n.larpPostSubmit || 'Publish') - ) - : null, - posts.length === 0 - ? p({ class: 'empty' }, i18n.larpPostsEmpty || 'No posts yet.') - : div({ class: 'larp-posts-list' }, - posts.map(post => div({ class: 'larp-post' }, - div({ class: 'larp-post-head' }, - userLink(post.author), - span({ class: 'larp-post-time' }, moment(post.createdAt).format('YYYY-MM-DD HH:mm')) - ), - p({ class: 'larp-post-text' }, post.text) - )) - ) -); + +const renderPostsBlock = (posts, house, canPost) => { + const stream = (Array.isArray(posts) ? posts : []) + .map(post => ({ ts: Date.parse(post.createdAt) || post.ts || 0, post })) + .sort((a, b) => b.ts - a.ts); + + return div({ class: 'larp-posts-block' }, + h2(i18n.larpPostsTitle || 'Wall'), + canPost + ? form({ method: 'POST', action: '/larp/post', class: 'larp-post-form' }, + input({ type: 'hidden', name: 'house', value: house.key }), + textarea({ id: 'larp_post_text', name: 'text', rows: '3', maxlength: '4000', placeholder: i18n.larpPostPlaceholder || 'What does this house need to say?' }), + button({ type: 'submit', class: 'filter-btn larp-post-submit' }, i18n.larpPostSubmit || 'Publish') + ) + : null, + stream.length === 0 + ? p({ class: 'empty' }, i18n.larpPostsEmpty || 'No posts yet.') + : div({ class: 'larp-posts-list' }, + paged(stream).map(entry => div({ class: entry.post.author === userId ? 'larp-post own-comment' : 'larp-post' }, + entry.post.author === userId && entry.post.id + ? form({ method: 'POST', action: `/larp/post/delete/${encodeURIComponent(entry.post.id)}`, class: 'comment-delete-form' }, + button({ type: 'submit', class: 'btn-singleview btn-delete', title: i18n.chatDelete }, renderIcon('close'))) + : null, + div({ class: 'larp-post-head' }, + userLink(entry.post.author), + span({ class: 'larp-post-time' }, moment(entry.post.createdAt).format('YYYY/MM/DD HH:mm')) + ), + p({ class: 'larp-post-text' }, ...renderStyledText(String(entry.post.text || ''))) + )) + ) + ); +}; + +const matchesHouse = (house, q) => { + const needle = String(q || '').trim().toLowerCase(); + if (!needle) return true; + return ['name', 'motto', 'roles', 'function', 'description'] + .some(f => String(house[f] || '').toLowerCase().includes(needle)); +}; const renderModeButtons = (filter) => div({ class: 'mode-buttons stats-mode-row' }, ['ruling', 'houses', 'rules'].map(m => @@ -226,33 +244,29 @@ const renderModeButtons = (filter) => div({ class: 'mode-buttons stats-mode-row' ) ); -const renderRules = () => div({ class: 'larp-rules' }, - h2(i18n.larpRulesTitle || 'F.A.Q.'), - div({ class: 'larp-rules-section' }, - h3(i18n.larpRulesEntryTitle || 'Starting house'), - p(i18n.larpRulesEntryText || 'Every inhabitant begins in ACADEMIA by default.') - ), - div({ class: 'larp-rules-section' }, - h3(i18n.larpRulesOneHouseTitle || 'One house at a time'), - p(i18n.larpRulesOneHouseText || 'You can belong to at most one house at any given moment. If you belong to none, you are in ACADEMIA. Every non-ACADEMIA house page shows a "Leave House" button to its members that resets membership back to ACADEMIA. Leaving does NOT waive the test cooldown.') - ), - div({ class: 'larp-rules-section' }, - h3(i18n.larpRulesTestEntry || 'WILL test'), - p(i18n.larpRulesTestEntryText || 'Each option weights one or more houses; when you submit, the system tallies the scores and auto-admits you into the highest-scoring house.') - ), - div({ class: 'larp-rules-section' }, - h3(i18n.larpRulesInviteEntry || 'Invitation code'), - p(i18n.larpRulesInviteEntryText || 'House members can generate invitation codes that grant direct access to the house.') - ), - div({ class: 'larp-rules-section' }, - h3(i18n.larpRulesGovernanceTitle || 'Governance Wall'), - p(i18n.larpRulesGovernanceText || 'During its cycle, the governing house wears the "Ruling" badge and is the only one whose Wall is surfaced publicly under the RULING tab.') - ), - div({ class: 'larp-rules-section' }, - h3(i18n.larpRulesSignTitle || 'L.A.R.P. Emblem'), - p(i18n.larpRulesSignText || 'Inhabitants can choose whether to add a sensor on their avatar with the emblem of their current house.') - ) -); +const renderRules = () => { + const points = [ + i18n.larpRulesEntryText, + i18n.larpRulesOneHouseText, + i18n.larpRulesTestEntryText, + i18n.larpRulesInviteEntryText, + i18n.larpRulesGovernanceText, + i18n.larpRulesWallText, + i18n.larpRulesSignText + ]; + return div( + { class: 'card' }, + h2(i18n.larpRulesTitle || 'F.A.Q.'), + div({ class: 'rules-points' }, + points.filter(Boolean).map((text, i) => + div({ class: 'rules-point' }, + span({ class: 'rules-point-num' }, String(i + 1)), + span({ class: 'rules-point-text' }, text) + ) + ) + ) + ); +}; const renderHousesGrid = (houses, myHouseKey, governingKey) => { const inLarp = !!myHouseKey; @@ -277,7 +291,9 @@ const renderHousesGrid = (houses, myHouseKey, governingKey) => { ), p({ class: 'larp-card-motto' }, '“' + h.motto + '”'), p({ class: 'larp-card-roles' }, h.roles), - p({ class: 'larp-card-count' }, `${i18n.larpMembersCount || 'Members'}: ${h.memberCount || 0}`), + div({ class: 'tribe-card-members' }, + span({ class: 'tribe-members-count' }, `${i18n.larpMembersCount || 'Members'}: ${h.memberCount || 0}`) + ), h.openInviteCode ? p({ class: 'larp-card-count' }, i18n.tribeInviteCodeText, span({ class: 'tribe-open-invite-code' }, h.openInviteCode)) : null ) ); @@ -285,24 +301,28 @@ const renderHousesGrid = (houses, myHouseKey, governingKey) => { ); }; -exports.larpListView = ({ filter, houses, myHouseKey, cycle, governingKey, governingHouse, governingMembers, governingPosts, canPost }) => { +exports.larpListView = ({ filter, houses, myHouseKey, cycle, governingKey, governingHouse, governingMembers, governingPosts, canPost, q }) => { const title = i18n.larpTitle || 'L.A.R.P.'; const description = i18n.larpDescription || 'A live action role-playing layer for collaborative experimentation.'; const myHouse = houses.find(h => h.key === myHouseKey) || null; - const mode = filter === 'houses' ? 'houses' : filter === 'rules' ? 'rules' : 'ruling'; + const search = String(q || '').trim(); + const mode = search ? 'houses' : filter === 'houses' ? 'houses' : filter === 'rules' ? 'rules' : 'ruling'; + const matched = search ? houses.filter(h => matchesHouse(h, search)) : houses; return template( title, section( - div({ class: 'tags-header' }, - h1(title), + div({ class: 'tags-header module-header-line' }, + h2(title), p(description) ), - renderCycleBanner(cycle), - renderHouseBadges({ myHouse, governingHouse }), renderModeButtons(mode), + renderCycleBanner(cycle), + renderHouseBadges({ myHouse, governingHouse, houses }), mode === 'houses' - ? renderHousesGrid(houses, myHouseKey, governingKey) + ? (matched.length + ? renderHousesGrid(matched, myHouseKey, governingKey) + : div({ class: 'no-content-box' }, p({ class: 'no-content' }, i18n.larpNoHousesMatch))) : mode === 'rules' ? renderRules() : [ @@ -330,7 +350,8 @@ exports.larpHouseView = ({ house, members, myHouseKey, cycle, governingKey, hous const isAcademia = house.key === 'academia'; const viewerInAcademia = myHouseKey === 'academia'; const viewerIsMember = myHouseKey === house.key && house.key !== 'academia'; - const showWall = viewerIsMember || house.key === governingKey; + const canWriteWall = myHouseKey === house.key; + const showWall = viewerIsMember || house.key === governingKey || house.key === 'academia'; const cycles = computeCyclesUntilRuling(house, governingKey); const housesById = Object.fromEntries(houses.map(h => [h.key, h])); @@ -342,9 +363,14 @@ exports.larpHouseView = ({ house, members, myHouseKey, cycle, governingKey, hous p({ class: 'larp-detail-motto' }, '“' + house.motto + '”') ), renderCycleBanner(cycle), - renderHouseBadges({ myHouse, governingHouse }), + renderHouseBadges({ myHouse, governingHouse, houses }), renderHouseNav(houses, house.key), - renderHousePanel(house, members, posts, { canPost, viewerHouseKey: myHouseKey, cyclesUntilRuling: cycles }), + renderHousePanel(house, members, posts, { + canPost, + viewerHouseKey: myHouseKey, + cyclesUntilRuling: cycles, + canTakeTest: isAcademia && viewerInAcademia && !!(testStatus && testStatus.allowed) + }), inviteCode ? div({ class: 'larp-invite-banner' }, p({ class: 'larp-invite-banner-title' }, strong(i18n.larpInviteBannerTitle || 'New invitation code')), @@ -352,10 +378,10 @@ exports.larpHouseView = ({ house, members, myHouseKey, cycle, governingKey, hous p({ class: 'larp-invite-banner-code' }, inviteCode) ) : null, + null, showWall - ? renderPostsBlock(posts, house, viewerIsMember) - : null, - isAcademia && viewerInAcademia ? renderAcademiaJoinPanel(houses, testStatus, housesById, questions, myHouseKey) : null + ? renderPostsBlock(posts, house, canWriteWall) + : null ) ); }; @@ -363,17 +389,19 @@ exports.larpHouseView = ({ house, members, myHouseKey, cycle, governingKey, hous exports.larpTestView = ({ questions, cycle, houses, myHouseKey, governingKey, testStatus }) => { const myHouse = houses.find(h => h.key === myHouseKey) || null; const governingHouse = houses.find(h => h.key === governingKey) || null; - const titleText = i18n.larpTestTitle || 'Profile test'; + const title = i18n.larpTitle || 'L.A.R.P.'; + const description = i18n.larpDescription || 'A live action role-playing layer for collaborative experimentation.'; return template( - titleText, + title, section( div({ class: 'tags-header' }, - h1(titleText), - p(i18n.larpTestIntro || 'Answer the psychological questions. The system will pick the house that best fits your answers. You can attempt one test every 30 cycles.') + h1(title), + p(description) ), renderCycleBanner(cycle), - renderHouseBadges({ myHouse, governingHouse }), + renderHouseBadges({ myHouse, governingHouse, houses }), + renderModeButtons(''), testStatus && !testStatus.allowed ? div({ class: 'larp-test-cooldown' }, p((i18n.larpTestCooldownActive || 'Next test available in') + ' ' + formatCooldown(testStatus.nextAt - Date.now()) + ' ' + (i18n.larpTestCooldownDays || 'cycles')), @@ -409,7 +437,7 @@ exports.larpTestResultView = ({ house, result, cycle, houses, myHouseKey, govern section( div({ class: 'tags-header' }, h1(titleText)), renderCycleBanner(cycle), - renderHouseBadges({ myHouse, governingHouse }), + renderHouseBadges({ myHouse, governingHouse, houses }), div({ class: 'larp-test-result' }, ranking.length ? div({ class: 'larp-test-ranking' }, diff --git a/src/views/legacy_view.js b/src/views/legacy_view.js deleted file mode 100644 index 2a8964c..0000000 --- a/src/views/legacy_view.js +++ /dev/null @@ -1,65 +0,0 @@ -const { form, button, div, h2, p, section, input, label, br } = require("../server/node_modules/hyperaxe"); -const { template, i18n } = require('./main_views'); - -const crypto = require('crypto'); - -function generateRandomPassword(length = 32) { - return crypto.randomBytes(length).toString('hex').slice(0, length); -} - -const legacyView = async () => { - const randomPassword = generateRandomPassword(); - - const header = div({ class: 'tags-header' }, - h2(i18n.legacyTitle), - p(i18n.legacyDescription) - ); - - return template( - i18n.legacyTitle, - section( - header, - p({ id: "randomPassword" }, h2({ class: "generated-password" }, randomPassword)), - div({ class: "div-center legacy-section" }, - p(i18n.exportDescription), - form( - { - action: "/legacy/export", - method: "POST", - id: "exportForm" - }, - label(i18n.exportPasswordLabel), - input({ - type: "password", - name: "password", - id: "password", - required: true, - placeholder: i18n.exportPasswordPlaceholder, - minlength: 32 - }), - p({ class: "file-info" }, i18n.fileInfo), - button({ type: "submit" }, i18n.legacyExportButton) - ), - br(), - p(i18n.importDescription), - form( - { action: "/legacy/import", method: "POST", enctype: "multipart/form-data" }, - input({ type: "file", name: "uploadedFile", required: true }), - br(), - p(i18n.passwordImport), - input({ - type: "password", - name: "importPassword", - required: true, - placeholder: i18n.importPasswordPlaceholder, - minlength: 32 - }), - br(), - button({ type: "submit" }, i18n.legacyImportButton) - ) - ) - ) - ); -}; - -exports.legacyView = legacyView; diff --git a/src/views/logistics_view.js b/src/views/logistics_view.js new file mode 100644 index 0000000..4153b26 --- /dev/null +++ b/src/views/logistics_view.js @@ -0,0 +1,399 @@ +const { hr, div, h2, h3, p, section, button, form, a, span, br, textarea, input, label, select, option, img, table, tr, td, video: videoHyperaxe } = require("../server/node_modules/hyperaxe"); +const { template, i18n, userLink, renderStateChip, renderContentActions, renderModuleStats, renderOpinionsVoting, renderEngagement, moduleIsEmpty, renderEcoValueChip, contentDeleteAction, paged } = require("./main_views"); +const { renderCommentsSection } = require("./comments_view"); +const { renderMapEmbed } = require("./maps_view"); +const { renderStyledText } = require("../backend/renderStyledText"); +const moment = require("../server/node_modules/moment"); +const { config } = require("../server/SSB_server.js"); + +const userId = config.keys.id; +const BASE_FILTERS = ["ALL", "RECENT", "MINE", "BOOKED", "HISTORY", "TOP"]; +const KIND_FILTERS = ["TRIPS", "SHIPMENTS"]; +const MODE_FILTERS = ["OFFERS", "REQUESTS"]; +const DATE_FILTERS = ["UPCOMING", "PAST", "CLOSED"]; +const PRICE_FILTERS = ["FREE", "ECO", "TIME"]; +const KINDS = ["TRIP", "SHIPMENT"]; +const MODES = ["OFFER", "REQUEST"]; +const RECURRENCES = ["NONE", "DAILY", "WEEKLY", "MONTHLY"]; + +const cap = (s) => s.charAt(0) + s.slice(1).toLowerCase(); +const lbl = (prefix, v) => String(i18n[`logistics${prefix}${cap(v)}`] || v).toUpperCase(); +const filterLabel = (f) => lbl("Filter", f); +const kindLabel = (k) => lbl("Kind", k); +const modeLabel = (m) => lbl("Mode", m); +const statusLabel = (s) => lbl("Status", s); +const priceLabel = (t) => lbl("Price", t); +const recurrenceLabel = (r) => lbl("Recurrence", r); +const bookingLabel = (s) => lbl("Booking", s); + +const routeHref = (route) => `/logistics/${encodeURIComponent(route.id)}`; +const fmt = (d) => moment(d).format("YYYY/MM/DD HH:mm"); +const stars = (n) => "★".repeat(Math.round(n)) + "☆".repeat(5 - Math.round(n)); + +const priceText = (route) => { + if (route.priceType === "ECO") return `${route.price} ECO`; + if (route.priceType === "TIME") return `${route.price} ${i18n.logisticsHours}`; + return priceLabel("FREE"); +}; + +const kindChip = (route) => renderStateChip("neutral", route.kind === "TRIP" ? "🚗" : "📦", kindLabel(route.kind)); +const modeChip = (route) => renderStateChip(route.mode === "OFFER" ? "mutuals" : "whole", "", modeLabel(route.mode)); +const statusChip = (route) => renderStateChip(route.status === "OPEN" ? "mutuals" : (route.status === "PAST" ? "whole" : "closed"), route.status === "OPEN" ? "✓" : (route.status === "CLOSED" ? "✗" : ""), statusLabel(route.status)); +const priceChip = (route) => renderStateChip("neutral", "", priceText(route)); + +const renderTags = (tags) => (Array.isArray(tags) && tags.length) + ? div({ class: "card-tags" }, ...tags.map(t => a({ href: `/search?query=%23${encodeURIComponent(t)}`, class: "tag-link" }, `#${t}`))) + : null; + +const routeLine = (route) => p({ class: "logistics-route-line" }, span({ class: "logistics-zone" }, route.origin), " → ", span({ class: "logistics-zone" }, route.destination)); +const whenLine = (route) => p({ class: "job-meta-line" }, `📅 ${fmt(route.date)}${route.recurrence !== "NONE" ? ` · ${recurrenceLabel(route.recurrence)}` : ""}`); +const capacityLine = (route) => { + if (route.kind === "TRIP") return route.seats ? p({ class: "job-meta-line" }, `💺 ${route.seatsLeft} / ${route.seats}`) : null; + const parts = [route.size ? `${i18n.logisticsSizeLabel}: ${route.size}` : null, route.weight ? `${i18n.logisticsWeightLabel}: ${route.weight}` : null].filter(Boolean); + return parts.length ? p({ class: "job-meta-line" }, `📦 ${parts.join(" · ")}`) : null; +}; +const countersBlock = (route) => + div({ class: "tribe-card-members" }, + span({ class: "tribe-members-count" }, `${i18n.logisticsRatingsTitle}: ${route.ratingCount || 0}${route.ratingCount ? ` · ${stars(route.ratingAvg)} ${route.ratingAvg}` : ""}`) + ); +const bookingsBlock = (route) => route.isOwner + ? div({ class: "tribe-card-members" }, span({ class: "tribe-members-count" }, `${i18n.logisticsBookingsLabel}: ${route.bookingCount || 0}`)) + : null; +const renderCover = (route) => { + if (!route.media || !route.media.blobId) return null; + if (route.media.kind === "video") return videoHyperaxe({ class: "logistics-card-cover", src: `/blob/${encodeURIComponent(route.media.blobId)}`, controls: true, preload: "metadata" }); + return a({ href: routeHref(route) }, img({ loading: 'lazy', class: "logistics-card-cover", src: `/blob/${encodeURIComponent(route.media.blobId)}`, alt: route.title || "" })); +}; + +const logisticsChipFor = (mode, census) => { + if (mode === "ALL") return true; + if (!Array.isArray(census)) return true; + if (mode === "MINE") return census.some(r => String(r.author) === String(userId)); + if (mode === "BOOKED") return census.some(r => !!r.myBooking); + if (mode === "RECENT") return census.length > 0; + if (mode === "HISTORY") return census.some(r => r.closed && r.participated); + if (mode === "TOP") return census.some(r => r.ratingCount > 0); + if (mode === "TRIPS") return census.some(r => r.kind === "TRIP"); + if (mode === "SHIPMENTS") return census.some(r => r.kind === "SHIPMENT"); + if (mode === "OFFERS") return census.some(r => r.mode === "OFFER"); + if (mode === "REQUESTS") return census.some(r => r.mode === "REQUEST"); + if (mode === "UPCOMING") return census.some(r => r.status === "OPEN"); + if (mode === "PAST") return census.some(r => r.status === "PAST"); + if (mode === "CLOSED") return census.some(r => r.status === "CLOSED"); + if (PRICE_FILTERS.includes(mode)) return census.some(r => r.priceType === mode); + return census.length > 0; +}; +exports.logisticsChipFor = logisticsChipFor; + +const renderRouteCard = (route, params = {}) => + div({ class: `tribe-card logistics-card logistics-${route.kind.toLowerCase()}` }, + div({ class: "card-header activity-card-header" }, + span(), + renderContentActions(route.id, routeHref(route), { author: route.author, favKind: "logistics", isFavorite: route.isFavorite, reportTitle: route.title, spread: (params.spreadMap && params.spreadMap.get(route.id)) || null, deleteAction: route.isOwner ? contentDeleteAction("logisticsRoute", route.id) : null }) + ), + div({ class: "tribe-card-body" }, + renderCover(route), + div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, a({ href: routeHref(route) }, route.title || "—"))), + div({ class: "card-chips-row" }, kindChip(route), modeChip(route), statusChip(route), priceChip(route)), + routeLine(route), + whenLine(route), + capacityLine(route), + countersBlock(route) + ) + ); + +const chipForm = (mode, filter, q, zone, labelText, extra = {}) => + form({ class: "sub-filter-form", method: "GET", action: "/logistics" }, + input({ type: "hidden", name: "q", value: q || "" }), + input({ type: "hidden", name: "filter", value: extra.filter !== undefined ? extra.filter : mode }), + input({ type: "hidden", name: "zone", value: extra.zone !== undefined ? extra.zone : (zone || "") }), + button({ type: "submit", class: extra.active ? "filter-btn active" : "filter-btn" }, labelText) + ); + +const chipButtons = (modes, filter, census, labelOf) => + modes.filter(m => m === filter || logisticsChipFor(m, census)).map(m => button({ type: "submit", name: "filter", value: m, class: filter === m ? "filter-btn active" : "filter-btn" }, labelOf(m))); + +const zoneRow = (zones, filter, q, zone) => { + if (!Array.isArray(zones) || !zones.length) return null; + const current = String(zone || "").toLowerCase(); + return div({ class: "activity-sub-filter logistics-zones" }, + span({ class: "inbox-filters-label" }, `${i18n.logisticsZoneLabel}:`), + ...zones.map(z => chipForm(filter, filter, q, zone, z.label, { filter, zone: current === z.label.toLowerCase() ? "" : z.label, active: current === z.label.toLowerCase() })) + ); +}; + +const renderFilters = (filter, q, zone, census, zones, emptyMod) => + section( + div({ class: "filters" }, + form({ method: "GET", action: "/logistics", class: "ui-toolbar ui-toolbar--filters" }, + input({ type: "hidden", name: "zone", value: zone || "" }), + ...(emptyMod ? [] : [ + ...chipButtons(BASE_FILTERS, filter, census, filterLabel), + ...chipButtons(KIND_FILTERS, filter, census, kindLabel), + ...chipButtons(MODE_FILTERS, filter, census, modeLabel), + ...chipButtons(DATE_FILTERS, filter, census, statusLabel), + ...chipButtons(PRICE_FILTERS, filter, census, priceLabel) + ]), + button({ type: "submit", name: "filter", value: "CREATE", class: "create-button" }, i18n.logisticsCreate) + ) + ), + emptyMod ? null : zoneRow(zones, filter, q, zone) + ); + +const renderForm = (route, params = {}) => { + const kind = route ? route.kind : (KINDS.includes(String(params.kind || "").toUpperCase()) ? String(params.kind).toUpperCase() : "TRIP"); + const isTrip = kind === "TRIP"; + const formId = route ? "logistics-edit-form" : "logistics-create-form"; + return div({ class: "report-form" }, + route + ? null + : form({ id: "logistics-kind-form", method: "GET", action: "/logistics" }, + input({ type: "hidden", name: "filter", value: "CREATE" }), + label(i18n.logisticsKindLabel), br(), + div({ class: "apply-row" }, + select({ name: "kind", class: "report-category-select" }, ...KINDS.map(k => option({ value: k, ...(k === kind ? { selected: true } : {}) }, kindLabel(k)))), + button({ type: "submit", class: "create-button" }, i18n.apply || "Apply") + ) + ), + route ? null : hr({ class: "form-sep" }), + route ? null : h2({ class: "report-category-fixed" }, kindLabel(kind)), + form({ id: formId, method: "POST", action: route ? `/logistics/update/${encodeURIComponent(route.id)}` : "/logistics/create", enctype: "multipart/form-data" }, + input({ type: "hidden", name: "kind", value: kind }), + label(i18n.logisticsTitleLabel), br(), + input({ type: "text", name: "title", maxlength: "100", required: true, placeholder: i18n.logisticsTitlePlaceholder, value: route ? route.title : "" }), br(), + label(i18n.logisticsModeLabel), br(), + select({ name: "mode" }, ...MODES.map(m => option({ value: m, ...((route ? route.mode : "OFFER") === m ? { selected: true } : {}) }, modeLabel(m)))), br(), br(), + label(i18n.logisticsDescriptionLabel), br(), + textarea({ name: "description", rows: 5, maxlength: "3000", placeholder: i18n.logisticsDescriptionPlaceholder }, route ? route.description : ""), br(), + label(i18n.uploadMedia), br(), + input({ type: "file", name: "blob", accept: "image/*,video/*,audio/*,application/pdf,.torrent" }), br(), br(), + label(i18n.logisticsOriginLabel), br(), + input({ type: "text", name: "origin", maxlength: "100", required: true, placeholder: i18n.logisticsOriginPlaceholder, value: route ? route.origin : "" }), br(), + label(i18n.logisticsDestinationLabel), br(), + input({ type: "text", name: "destination", maxlength: "100", required: true, placeholder: i18n.logisticsDestinationPlaceholder, value: route ? route.destination : "" }), br(), + label(i18n.mapLocationTitle || "Map Location"), br(), + input({ type: "text", name: "mapUrl", placeholder: i18n.mapUrlPlaceholder || "/maps/MAP_ID", value: route ? route.mapUrl : "" }), br(), + label(i18n.logisticsDateLabel), br(), + input({ type: "datetime-local", name: "date", required: true, min: moment().format("YYYY-MM-DDTHH:mm"), value: route && route.date ? moment(route.date).format("YYYY-MM-DDTHH:mm") : "" }), br(), br(), + label(i18n.logisticsRecurrenceLabel), br(), + select({ name: "recurrence" }, ...RECURRENCES.map(r => option({ value: r, ...((route ? route.recurrence : "NONE") === r ? { selected: true } : {}) }, recurrenceLabel(r)))), br(), br(), + isTrip + ? [ + label(i18n.logisticsSeatsLabel), br(), + input({ type: "number", name: "seats", min: "1", max: "99", step: "1", required: true, class: "logistics-short", value: route ? String(route.seats || 1) : "1" }), br(), br() + ] + : [ + label(i18n.logisticsSizeLabel), br(), + input({ type: "text", name: "size", maxlength: "60", placeholder: i18n.logisticsSizePlaceholder, value: route ? route.size : "" }), br(), + label(i18n.logisticsWeightLabel), br(), + input({ type: "text", name: "weight", maxlength: "60", placeholder: i18n.logisticsWeightPlaceholder, value: route ? route.weight : "" }), br() + ], + label(i18n.logisticsPriceLabel), br(), + input({ type: "number", name: "price", min: "0", step: "0.01", placeholder: "0", class: "logistics-short", value: route && route.price ? String(route.price) : "" }), + select({ name: "priceType", class: "logistics-price-unit" }, ...["ECO", "TIME"].map(t => option({ value: t, ...((route && route.priceType === "TIME" ? "TIME" : "ECO") === t ? { selected: true } : {}) }, priceLabel(t)))), renderEcoValueChip(), br(), br(), + label(i18n.logisticsOrderRefLabel), br(), + input({ type: "text", name: "orderRef", maxlength: "200", placeholder: i18n.logisticsOrderRefPlaceholder, value: route ? route.orderRef : "" }), br(), + label(i18n.logisticsTagsLabel), br(), + input({ type: "text", name: "tags", maxlength: "200", placeholder: i18n.logisticsTagsPlaceholder, value: route ? route.tags.join(", ") : "" }), br(), br(), + button({ type: "submit", class: "create-button" }, route ? i18n.logisticsUpdate : i18n.logisticsCreate) + ) + ); +}; + +const renderHeader = () => div({ class: "tags-header module-header-line" }, h2(i18n.logisticsTitle), p(i18n.logisticsDescription)); + +exports.logisticsView = async (routes, filter = "ALL", params = {}) => { + const list = Array.isArray(routes) ? routes : []; + const q = String(params.q || "").trim(); + const zone = String(params.zone || "").trim(); + const census = Array.isArray(params.censusList) ? params.censusList : list; + const zones = Array.isArray(params.zones) ? params.zones : []; + const f = String(filter || "ALL").toUpperCase(); + const isForm = f === "CREATE" || f === "EDIT"; + const emptyMod = !isForm && moduleIsEmpty(census, "ALL", "ALL", q) && !zone; + return template( + i18n.logisticsTitle, + section( + div({ class: "tags-header module-header-line" }, h2(i18n.logisticsTitle), p(i18n.logisticsDescription)), + renderFilters(isForm ? "ALL" : f, q, isForm ? "" : zone, isForm ? [] : census, isForm ? [] : zones, emptyMod) + ), + section( + isForm + ? renderForm(f === "EDIT" ? params.route : null, params) + : [ + emptyMod ? null : div({ class: "filters activity-filter-chips activity-toolbar-row" }, + renderModuleStats(list.length, [ + { label: kindLabel("TRIP"), count: list.filter(r => r.kind === "TRIP").length }, + { label: kindLabel("SHIPMENT"), count: list.filter(r => r.kind === "SHIPMENT").length }, + { label: statusLabel("OPEN"), count: list.filter(r => r.status === "OPEN").length } + ]), + form({ method: "GET", action: "/logistics", class: "filter-box" }, + input({ type: "hidden", name: "filter", value: f }), + input({ type: "hidden", name: "zone", value: zone }), + input({ type: "text", name: "q", value: q, placeholder: i18n.logisticsSearchPlaceholder, class: "filter-box__input" }), + div({ class: "filter-box__controls" }, button({ type: "submit", class: "filter-box__button" }, i18n.searchButton)) + ) + ), + div({ class: "tribe-grid" }, list.length ? paged(list).map(r => renderRouteCard(r, params)) : p(i18n.logisticsNoItems)) + ] + ) + ); +}; + +const bookingStatusChip = (b) => renderStateChip(b.status === "CONFIRMED" || b.status === "DELIVERED" ? "mutuals" : (b.status === "PENDING" ? "whole" : "closed"), "", bookingLabel(b.status)); + +const bookingStatusForm = (route, b, status, cls, text) => + form({ method: "POST", action: `/logistics/booking/${encodeURIComponent(b.id)}/status` }, + input({ type: "hidden", name: "status", value: status }), + input({ type: "hidden", name: "route", value: route.id }), + button({ type: "submit", class: cls }, String(text).toUpperCase()) + ); + +const renderBooking = (route, b) => + div({ class: "logistics-booking" }, + div({ class: "emergency-update-head logistics-booking-head" }, + div({ class: "card-chips-row" }, bookingStatusChip(b), route.kind === "TRIP" ? renderStateChip("neutral", "💺", String(b.seats)) : null), + div({ class: "tribe-side-actions emergency-update-actions" }, + route.isOwner && b.status === "PENDING" ? bookingStatusForm(route, b, "CONFIRMED", "tribe-action-btn", i18n.logisticsConfirm) : null, + route.isOwner && b.status === "PENDING" ? bookingStatusForm(route, b, "REJECTED", "delete-btn", i18n.logisticsReject) : null, + b.mine && (b.status === "PENDING" || b.status === "CONFIRMED") ? bookingStatusForm(route, b, "CANCELLED", "delete-btn", i18n.logisticsCancelBooking) : null + ) + ), + b.notes ? p({ class: "logistics-booking-notes" }, b.notes) : null, + b.orderRef ? p({ class: "job-meta-line" }, a({ href: b.orderRef, class: "user-link" }, `🛍 ${i18n.logisticsOrderLink}`)) : null, + b.receipt ? p({ class: "logistics-receipt" }, `🧾 ${i18n.logisticsReceiptLabel}: ${b.receipt}`) : null, + route.isOwner && b.status === "CONFIRMED" + ? form({ method: "POST", action: `/logistics/booking/${encodeURIComponent(b.id)}/status`, class: "logistics-receipt-form" }, + input({ type: "hidden", name: "status", value: "DELIVERED" }), + input({ type: "hidden", name: "route", value: route.id }), + input({ type: "text", name: "receipt", maxlength: "200", placeholder: i18n.logisticsReceiptPlaceholder }), + button({ type: "submit", class: "tribe-action-btn" }, String(i18n.logisticsDeliver).toUpperCase()) + ) + : null, + p({ class: "card-footer" }, span({ class: "date-link" }, `${fmt(b.createdAt)}`), userLink(b.booker)) + ); + +const renderBookForm = (route) => + form({ method: "POST", action: `/logistics/${encodeURIComponent(route.id)}/book`, class: "logistics-book-form" }, + h3(i18n.logisticsBookTitle), + route.kind === "TRIP" ? [label(i18n.logisticsSeatsLabel), br(), input({ type: "number", name: "seats", min: "1", max: String(route.seatsLeft || 99), step: "1", value: "1" }), br(), br()] : null, + label(i18n.logisticsNotesLabel), br(), + textarea({ name: "notes", rows: 3, maxlength: "1000", placeholder: i18n.logisticsNotesPlaceholder }), br(), br(), + label(i18n.logisticsOrderRefLabel), br(), + input({ type: "text", name: "orderRef", maxlength: "200", placeholder: i18n.logisticsOrderRefPlaceholder }), br(), br(), + button({ type: "submit", class: "create-button" }, i18n.logisticsBook) + ); + +const renderRatings = (route) => + div({ class: "card-section logistics-ratings" }, + h3(i18n.logisticsRatingsTitle), + route.ratings.length + ? div({ class: "logistics-ratings-list" }, ...route.ratings.map(r => div({ class: "logistics-rating" }, + div({ class: "emergency-update-head logistics-rating-head" }, div({ class: "card-chips-row" }, renderStateChip("neutral", "", stars(r.score)))), + r.text ? p(r.text) : null, + p({ class: "card-footer" }, span({ class: "date-link" }, `${fmt(r.createdAt)}`), userLink(r.author)) + ))) + : p(route.closed ? i18n.logisticsNoRatings : i18n.logisticsRatingsWhenClosed), + route.canRate + ? form({ method: "POST", action: `/logistics/${encodeURIComponent(route.id)}/rate`, class: "logistics-rate-form" }, + label(i18n.logisticsScoreLabel), br(), + select({ name: "score" }, ...[5, 4, 3, 2, 1].map(n => option({ value: String(n) }, stars(n)))), br(), br(), + textarea({ name: "text", rows: 3, maxlength: "1000", placeholder: i18n.logisticsRatingPlaceholder }), br(), br(), + button({ type: "submit", class: "create-button" }, i18n.logisticsRate) + ) + : null + ); + +exports.singleLogisticsView = async (route, params = {}) => { + const href = routeHref(route); + const census = Array.isArray(params.censusList) ? params.censusList : []; + const zones = Array.isArray(params.zones) ? params.zones : []; + const ownerActions = route.isOwner + ? [ + form({ method: "GET", action: "/logistics" }, input({ type: "hidden", name: "filter", value: "edit" }), input({ type: "hidden", name: "id", value: route.id }), button({ type: "submit", class: "update-btn" }, i18n.logisticsUpdate)) + ] + : []; + const capacity = route.kind === "TRIP" + ? (route.seats ? [{ label: i18n.logisticsSeatsLabel, value: `${route.seatsLeft} / ${route.seats}` }] : []) + : [route.size ? { label: i18n.logisticsSizeLabel, value: route.size } : null, route.weight ? { label: i18n.logisticsWeightLabel, value: route.weight } : null].filter(Boolean); + const side = div({ class: "tribe-side" }, + div({ class: "card-header activity-card-header" }, + renderContentActions(route.id, href, { author: route.author, favKind: "logistics", isFavorite: route.isFavorite, reportTitle: route.title, spread: params.spread || null, deleteAction: route.isOwner ? contentDeleteAction("logisticsRoute", route.id) : null }) + ), + div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, route.title)), + div({ class: "card-chips-row" }, kindChip(route), modeChip(route), statusChip(route), priceChip(route)), + table({ class: "tribe-info-table jobs-info-table" }, + tr( + td({ class: "tribe-info-label" }, i18n.createdAtLabel || "Created at"), + td({ class: "tribe-info-value", colspan: "3" }, fmt(route.createdAt)) + ), + tr( + td({ class: "tribe-info-label" }, i18n.logisticsDateLabel), + td({ class: "tribe-info-value", colspan: "3" }, fmt(route.date)) + ), + route.recurrence !== "NONE" + ? tr( + td({ class: "tribe-info-label" }, i18n.logisticsFrequencyLabel), + td({ class: "tribe-info-value", colspan: "3" }, recurrenceLabel(route.recurrence)) + ) + : null, + tr( + td({ class: "tribe-info-label" }, i18n.logisticsOriginLabel), + td({ class: "tribe-info-value", colspan: "3" }, route.origin) + ), + tr( + td({ class: "tribe-info-label" }, i18n.logisticsDestinationLabel), + td({ class: "tribe-info-value", colspan: "3" }, route.destination) + ), + ...capacity.map(c => tr( + td({ class: "tribe-info-label" }, c.label), + td({ class: "tribe-info-value", colspan: "3" }, c.value) + )), + tr( + td({ class: "tribe-info-value", colspan: "4" }, userLink(route.author)) + ) + ), + countersBlock(route), + renderMapEmbed(params.mapData, route.mapUrl), + route.orderRef + ? div({ class: "tribe-side-actions" }, form({ method: "GET", action: route.orderRef }, button({ type: "submit", class: "tribe-action-btn" }, `🛍 ${String(i18n.logisticsOrderLink).toUpperCase()}`))) + : null, + route.isOwner + ? div({ class: "tribe-side-actions housing-status-row" }, + span({ class: "card-label" }, `${i18n.statusLabel || "Status"}: `), + statusChip(route), + form({ method: "POST", action: `/logistics/status/${encodeURIComponent(route.id)}`, class: "inline-form" }, + button({ class: "tribe-action-btn", type: "submit", name: "status", value: route.status === "CLOSED" ? "OPEN" : "CLOSED" }, + String(route.status === "CLOSED" ? i18n.logisticsReopen : i18n.logisticsClose).toUpperCase()) + ) + ) + : null, + bookingsBlock(route), + div({ class: "doc-export-actions" }, + form({ method: "GET", action: `${href}/pdf` }, button({ type: "submit", class: "filter-btn" }, i18n.logisticsGenerateReport)), + form({ method: "POST", action: `${href}/share` }, button({ type: "submit", class: "filter-btn" }, i18n.sharePm)) + ), + renderTags(route.tags), + ownerActions.length ? div({ class: "tribe-side-actions owner-actions" }, ...ownerActions) : null + ); + const main = div({ class: "tribe-main" }, + route.description ? div({ class: "logistics-body" }, ...renderStyledText(route.description)) : null, + route.canBook ? div({ class: "card-section" }, renderBookForm(route)) : null, + route.bookings.length + ? div({ class: "card-section logistics-bookings" }, + h3(route.isOwner ? `${i18n.logisticsBookingsTitle} (${route.bookings.length})` : i18n.logisticsMyBookingTitle), + ...route.bookings.map(b => renderBooking(route, b)) + ) + : null, + renderRatings(route), + renderEngagement(route.id, + renderOpinionsVoting("/logistics/opinions", route.id, route.opinions, href, route.opinions_inhabitants), + renderCommentsSection({ action: `/logistics/${encodeURIComponent(route.id)}/comments`, comments: params.comments || [], returnTo: href }) + ) + ); + return template( + route.title, + section(renderHeader(), renderFilters("ALL", "", "", census, zones, census.length === 0)), + section(div({ class: "tribe-details" }, side, main)) + ); +}; diff --git a/src/views/logs_view.js b/src/views/logs_view.js index 60d0149..b43b790 100644 --- a/src/views/logs_view.js +++ b/src/views/logs_view.js @@ -1,7 +1,10 @@ -const { div, h2, p, section, button, form, span, table, thead, tbody, tr, th, td, input, textarea, br, option, select } = require("../server/node_modules/hyperaxe"); -const { template, i18n } = require("./main_views"); +const { div, h2, p, section, button, form, span, table, thead, tbody, tr, th, td, input, textarea, br, option, select, a, label } = require("../server/node_modules/hyperaxe"); +const { template, i18n, renderModuleStatsBy, paged } = require("./main_views"); const moment = require("../server/node_modules/moment"); -const { renderUrl } = require("../backend/renderUrl"); +const { config } = require("../server/SSB_server.js"); +const { renderStyledText } = require("../backend/renderStyledText"); + +const userId = config.keys.id; const safeArr = v => Array.isArray(v) ? v : []; @@ -18,30 +21,31 @@ const filterLabel = (f) => { return map[f] || f.toUpperCase(); }; -const renderFilterBar = (current, hasItems = false) => - div({ class: "logs-toolbar" }, - form({ method: "GET", action: "/logs", class: "logs-toolbar-inline" }, - FILTERS.map(f => +const renderFilterBar = (current, hasItems = true, emptyLogs = false, avail = null) => + div({ class: "activity-sub-filter" }, + emptyLogs ? null : form({ method: "GET", action: "/logs", class: "sub-filter-form" }, + FILTERS.filter(f => f === current || f === "always" || !avail || avail[f] !== false).map(f => button({ type: "submit", name: "filter", value: f, class: current === f ? "filter-btn active" : "filter-btn" - }, filterLabel(f)) + }, String(filterLabel(f)).toUpperCase()) ) ), - form({ method: "GET", action: "/logs", class: "logs-toolbar-inline" }, + form({ method: "GET", action: "/logs", class: "sub-filter-form" }, input({ type: "hidden", name: "view", value: "create" }), button({ type: "submit", class: "create-button" }, i18n.logsCreate || 'Create Log') ), hasItems - ? form({ method: "GET", action: "/logs/export", class: "logs-toolbar-inline" }, + ? form({ method: "GET", action: "/logs/export", class: "sub-filter-form" }, button({ type: "submit", class: "create-button" }, i18n.logsExport || 'Export Logs') ) : null ); -const renderSearchBox = (current, search) => { +const renderSearchBox = (current, search, items = null) => { const q = search || {}; - return div({ class: "logs-search" }, + return div({ class: "logs-search activity-filter-chips activity-toolbar-row" }, + items ? renderModuleStatsBy(items, e => (e && e.mode === 'ai' ? 'ai' : 'manual'), [{ value: 'manual', label: i18n.logsModeManual }, { value: 'ai', label: i18n.logsModeAI }]) : null, form({ method: "GET", action: "/logs", class: "filter-box" }, input({ type: "hidden", name: "filter", value: current || 'today' }), input({ @@ -65,23 +69,27 @@ const renderSearchBox = (current, search) => { ); }; -const renderToolbar = (current, search, hasItems) => - div({ class: "logs-toolbar-wrap" }, - renderFilterBar(current, hasItems), - renderSearchBox(current, search) - ); +const renderToolbar = (current, search, hasItems, items = null, emptyLogs = false, avail = null) => + emptyLogs + ? div({ class: "filters" }, + form({ method: "GET", action: "/logs", class: "ui-toolbar ui-toolbar--filters" }, + input({ type: "hidden", name: "view", value: "create" }), + button({ type: "submit", class: "create-button" }, i18n.logsCreate || 'Create Log') + ) + ) + : div({ class: "logs-toolbar-wrap" }, + renderFilterBar(current, hasItems, emptyLogs, avail), + renderSearchBox(current, search, items) + ); -const MAX_PREVIEW = 140; - -const truncate = (str) => { - const s = String(str || ''); - if (s.length <= MAX_PREVIEW) return s; - return s.slice(0, MAX_PREVIEW).replace(/\s+\S*$/, '') + '…'; +const truncate = (value, max = 160) => { + const text = String(value == null ? '' : value).trim(); + return text.length > max ? `${text.slice(0, max)}\u2026` : text; }; const renderLogPreview = (item) => { const text = truncate(item.text); - return div({ class: "logs-entry-text" }, ...renderUrl(text)); + return div({ class: "logs-entry-text" }, ...renderStyledText(text)); }; const renderTable = (items) => { @@ -91,9 +99,8 @@ const renderTable = (items) => { tr( th(i18n.logsColumnDate || 'Date'), th(i18n.logsColumnType || 'Type'), - th(i18n.logsColumnLog || 'Log'), - th(''), - th('') + th({ class: "logs-col-log" }, i18n.logsColumnLog || 'Log'), + th({ class: "logs-col-actions" }, '') ) ), tbody( @@ -114,13 +121,13 @@ const renderTable = (items) => { renderLogPreview(item) ), td({ class: "logs-col-actions" }, - form({ method: "GET", action: `/logs/view/${encodeURIComponent(item.key)}` }, - button({ type: "submit", class: "filter-btn" }, i18n.logsViewDetails || 'View Details') - ) - ), - td({ class: "logs-col-actions" }, - form({ method: "GET", action: `/logs/export/${encodeURIComponent(item.key)}` }, - button({ type: "submit", class: "filter-btn" }, i18n.logsExportOne || 'Export') + div({ class: "logs-row-actions" }, + form({ method: "GET", action: `/logs/view/${encodeURIComponent(item.key)}` }, + button({ type: "submit", class: "filter-btn" }, i18n.logsViewDetails || 'View Details') + ), + form({ method: "GET", action: `/logs/export/${encodeURIComponent(item.key)}` }, + button({ type: "submit", class: "filter-btn" }, i18n.logsExportOne || 'Export') + ) ) ) ) @@ -161,8 +168,7 @@ const renderCreateForm = (mode, aiModOn) => { : div({ class: "div-center audio-form" }, form({ method: "POST", action: "/logs/create" }, input({ type: "hidden", name: "mode", value: "manual" }), - span(i18n.logsManualPrompt || 'Write your log'), br(), - textarea({ name: "text", rows: "8", required: true, placeholder: i18n.logsTextPlaceholder || 'Describe your experiences...' }), + textarea({ maxlength: "5000", name: "text", rows: "8", required: true, placeholder: i18n.logsTextPlaceholder || 'Describe your experiences...' }), br(), br(), button({ type: "submit", class: "create-button" }, i18n.logsWriteButton || 'Write') ) @@ -172,10 +178,9 @@ const renderCreateForm = (mode, aiModOn) => { const renderEditForm = (entry) => { return div({ class: "div-center audio-form" }, - h2(i18n.logsEditTitle || 'Edit Log'), - form({ method: "POST", action: `/logs/edit/${encodeURIComponent(entry.key)}` }, - span(i18n.logsManualPrompt || 'Write your log...'), br(), - textarea({ name: "text", rows: "8", required: true }, entry.text || ''), + h2(i18n.logsEditTitle || 'Update Log'), + form({ method: "POST", action: `/logs/update/${encodeURIComponent(entry.key)}` }, + textarea({ maxlength: "5000", name: "text", rows: "8", required: true }, entry.text || ''), input({ type: "hidden", name: "label", value: entry.label || '' }), br(), br(), button({ type: "submit", class: "create-button" }, i18n.logsUpdateButton || 'Update') @@ -188,26 +193,25 @@ const renderDetail = (entry) => { return div({ class: "div-center audio-form logs-detail" }, h2(headerLine), entry.label ? div({ class: "logs-entry-label" }, entry.label) : null, - div({ class: "logs-detail-text" }, ...renderUrl(String(entry.text || ''))), - div({ class: "logs-detail-actions" }, + div({ class: "logs-detail-text" }, ...renderStyledText(String(entry.text || ''))), + div({ class: "tribe-side-actions logs-detail-actions" }, form({ method: "GET", action: "/logs" }, - button({ type: "submit", class: "filter-btn" }, i18n.walletBack || 'Back') + button({ type: "submit", class: "tribe-action-btn" }, i18n.walletBack || 'Back') ), form({ method: "GET", action: `/logs/export/${encodeURIComponent(entry.key)}` }, - button({ type: "submit", class: "filter-btn" }, i18n.logsExportOne || 'Export') + button({ type: "submit", class: "tribe-action-btn" }, i18n.generatePdf) ), - form({ method: "GET", action: "/logs" }, - input({ type: "hidden", name: "view", value: "edit" }), - input({ type: "hidden", name: "id", value: entry.key }), - button({ type: "submit", class: "filter-btn" }, i18n.logsEdit || 'Edit') + form({ method: "GET", action: `/logs/edit/${encodeURIComponent(entry.key)}` }, + button({ type: "submit", class: "tribe-action-btn" }, i18n.logsEdit || 'Update') ), form({ method: "POST", action: `/logs/delete/${encodeURIComponent(entry.key)}` }, - button({ type: "submit", class: "filter-btn danger-btn" }, i18n.logsDelete || 'Delete') + button({ type: "submit", class: "tribe-action-btn danger-btn" }, i18n.logsDelete || 'Delete') ) ) ); }; + exports.logsView = (items, filter, mode, opts = {}) => { const listTitle = i18n.logsTitle || 'Logs'; const description = i18n.logsDescription || 'Record your experience in the network.'; @@ -215,35 +219,34 @@ exports.logsView = (items, filter, mode, opts = {}) => { const aiModOn = !!opts.aiModOn; const hasItems = Array.isArray(items) && items.length > 0; + const screen = (...blocks) => template( + listTitle, + section( + div({ class: "tags-header module-header-line" }, h2(listTitle), p(description)), + div({ class: "activity-sub-filter" }, + form({ method: "GET", action: "/logs", class: "sub-filter-form" }, + button({ type: "submit", name: "filter", value: filter || "today", class: "filter-btn active" }, String(filterLabel(filter || "today")).toUpperCase()) + ) + ), + ...blocks + ) + ); + if (view === 'create') { - const h = i18n.logsCreateTitle || 'Create Log'; - const body = section( - div({ class: "tags-header" }, h2(h), p(description)), - renderFilterBar(filter, hasItems), - renderCreateForm(mode, aiModOn) - ); - return template(h, body); + return screen(renderCreateForm(mode, aiModOn)); } if (view === 'edit' && opts.entry) { - const h = i18n.logsEditTitle || 'Edit Log'; - const body = section( - div({ class: "tags-header" }, h2(h), p(description)), - renderEditForm(opts.entry) - ); - return template(h, body); + return screen(renderEditForm(opts.entry)); } if (view === 'detail' && opts.entry) { - const h = i18n.logsViewTitle || 'Log'; - const body = section( - div({ class: "tags-header" }, h2(h), p(description)), - renderDetail(opts.entry) - ); - return template(h, body); + return screen(renderDetail(opts.entry)); } + const sr = opts.search || {}; + const emptyLogs = Number(opts.total ?? items.length) === 0 && !String(sr.q || '').trim() && !String(sr.type || '').trim() && !String(sr.date || '').trim(); const body = section( - div({ class: "tags-header" }, h2(listTitle), p(description)), - renderToolbar(filter, opts.search || {}, hasItems), - div({ class: "logs-list" }, renderTable(items)) + div({ class: "tags-header module-header-line" }, h2(listTitle), p(description)), + renderToolbar(filter, opts.search || {}, hasItems, items, emptyLogs, opts.avail || null), + div({ class: "logs-list" }, renderTable(paged(items))) ); return template(listTitle, body); }; diff --git a/src/views/mailing_view.js b/src/views/mailing_view.js new file mode 100644 index 0000000..071b884 --- /dev/null +++ b/src/views/mailing_view.js @@ -0,0 +1,321 @@ +const { div, h2, h3, p, section, button, form, a, span, br, textarea, input, label, select, option, details, summary, ul, li, table, tr, td } = require("../server/node_modules/hyperaxe"); +const { TEXT_CAP } = require('../backend/long_text'); +const { template, i18n, userLink, renderStateChip, renderOpenClosedChip, renderContentActions, renderModuleStats, moduleIsEmpty, contentDeleteAction, paged } = require("./main_views"); +const { renderStyledText } = require("../backend/renderStyledText"); +const moment = require("../server/node_modules/moment"); +const { config } = require("../server/SSB_server.js"); + +const userId = config.keys.id; +const BASE_FILTERS = ["ALL", "RECENT", "MINE", "SUBSCRIBED"]; +const TYPE_FILTERS = ["OPEN", "CLOSED"]; +const STATUS_FILTERS = ["ACTIVE", "ARCHIVED"]; +const HISTORY_MODES = ["DATE", "THREADS", "INHABITANTS"]; + +const cap = (s) => s.charAt(0) + s.slice(1).toLowerCase(); +const baseLabel = (f) => String(i18n[`mailingFilter${cap(f)}`] || f).toUpperCase(); +const typeLabel = (t) => String(i18n[`mailingType${cap(t)}`] || t).toUpperCase(); +const statusLabel = (s) => String(i18n[`mailingStatus${cap(s)}`] || s).toUpperCase(); +const historyLabel = (m) => String(i18n[`mailingHistory${cap(m)}`] || m).toUpperCase(); + +const listHref = (list) => `/mailing/${encodeURIComponent(list.id)}`; +const fmt = (d) => moment(d).format("YYYY/MM/DD HH:mm"); + +const statusChip = (list) => renderStateChip(list.status === "ACTIVE" ? "mutuals" : "closed", list.status === "ACTIVE" ? "✓" : "✗", statusLabel(list.status)); +const countsLine = (list) => span({ class: "card-label activity-update-counts mailing-counts" }, `👥: ${list.participantCount || 0} · 💬 ${list.messageCount || 0}`); + +const subscribeForm = (list, returnTo) => { + if (list.isOwner) return null; + if (list.closed) { + return list.isMember + ? form({ method: "POST", action: `/mailing/leave/${encodeURIComponent(list.id)}` }, button({ type: "submit", class: "tribe-action-btn danger-btn" }, String(i18n.subscriptionUnsubscribe).toUpperCase())) + : null; + } + return form({ method: "POST", action: "/subscriptions/toggle" }, + input({ type: "hidden", name: "target", value: list.id }), + input({ type: "hidden", name: "scope", value: "mailing" }), + input({ type: "hidden", name: "on", value: list.isMember ? "0" : "1" }), + input({ type: "hidden", name: "returnTo", value: returnTo }), + button({ type: "submit", class: list.isMember ? "tribe-action-btn danger-btn" : "tribe-action-btn" }, String(list.isMember ? i18n.subscriptionUnsubscribe : i18n.subscriptionSubscribe).toUpperCase()) + ); +}; + +const writeForm = (list) => (list.status === "ACTIVE" && (!list.closed || list.isMember)) + ? form({ method: "GET", action: `${listHref(list)}#compose` }, input({ type: "hidden", name: "write", value: "1" }), button({ type: "submit", class: "tribe-action-btn" }, String(i18n.mailingWriteButton).toUpperCase())) + : null; + +const renderTags = (tags) => (Array.isArray(tags) && tags.length) + ? div({ class: "card-tags" }, ...tags.map(t => a({ href: `/search?query=%23${encodeURIComponent(t)}`, class: "tag-link" }, `#${t}`))) + : null; + +const mailingChipFor = (mode, census) => { + if (mode === "ALL") return true; + if (!Array.isArray(census)) return true; + if (mode === "MINE") return census.some(l => String(l.author) === String(userId)); + if (mode === "SUBSCRIBED") return census.some(l => l.isMember && String(l.author) !== String(userId)); + if (mode === "RECENT") return census.length > 0; + if (mode === "OPEN") return census.some(l => !l.closed); + if (mode === "CLOSED") return census.some(l => l.closed); + if (STATUS_FILTERS.includes(mode)) return census.some(l => l.status === mode); + return census.length > 0; +}; +exports.mailingChipFor = mailingChipFor; + +const renderArchiveItem = (list, params = {}) => + li({ class: "mailing-archive-item" }, + div({ class: "emergency-update-head mailing-archive-head" }, + div({ class: "mailing-archive-meta" }, + a({ href: listHref(list), class: "user-link" }, list.title || "—"), + countsLine(list), + renderOpenClosedChip(list.listType, { statusChipOPEN: typeLabel("OPEN"), statusChipCLOSED: typeLabel("CLOSED") }), + statusChip(list), + list.isMember && !list.isOwner ? renderStateChip("mutuals", "✉", i18n.subscriptionOn) : null + ), + renderContentActions(list.id, listHref(list), { author: list.author, favKind: "mailing", isFavorite: list.isFavorite, reportTitle: list.title, spread: (params.spreadMap && params.spreadMap.get(list.id)) || null, deleteAction: String(list.author) === String(userId) ? contentDeleteAction("mailingList", list.id) : null }) + ) + ); + +const chipButtons = (modes, filter, census, labelOf) => + modes.filter(m => m === filter || mailingChipFor(m, census)).map(m => button({ type: "submit", name: "filter", value: m, class: filter === m ? "filter-btn active" : "filter-btn" }, labelOf(m))); + +const renderFilters = (filter, q, census, emptyMod) => + section( + div({ class: "filters" }, + form({ method: "GET", action: "/mailing", class: "ui-toolbar ui-toolbar--filters" }, + ...(emptyMod ? [] : [ + ...chipButtons(BASE_FILTERS, filter, census, baseLabel), + ...chipButtons(TYPE_FILTERS, filter, census, typeLabel), + ...chipButtons(STATUS_FILTERS, filter, census, statusLabel) + ]), + button({ type: "submit", name: "filter", value: "CREATE", class: "create-button" }, i18n.mailingCreate) + ) + ) + ); + +const renderForm = (list) => + div({ class: "report-form" }, + h2(list ? i18n.mailingUpdateSectionTitle : i18n.mailingCreateSectionTitle), + form({ method: "POST", action: list ? `/mailing/update/${encodeURIComponent(list.id)}` : "/mailing/create" }, + label(i18n.mailingTitleLabel), br(), + input({ type: "text", name: "title", maxlength: "100", required: true, placeholder: i18n.mailingTitlePlaceholder, value: list ? list.title : "" }), br(), + label(i18n.mailingDescriptionLabel), br(), + textarea({ name: "description", rows: 4, maxlength: "1000", placeholder: i18n.mailingDescriptionPlaceholder }, list ? list.description : ""), br(), + list + ? [ + label(i18n.mailingStatusLabel), br(), + select({ name: "status" }, ...STATUS_FILTERS.map(st => option({ value: st, ...(st === list.status ? { selected: true } : {}) }, statusLabel(st)))), br(), br() + ] + : [ + label(i18n.mailingTypeLabel), br(), + select({ name: "listType" }, ...TYPE_FILTERS.map(t => option({ value: t }, typeLabel(t)))), br(), br() + ], + (!list || list.closed) + ? [ + label(i18n.mailingMembersLabel), br(), + textarea({ name: "members", rows: 3, maxlength: "4000", placeholder: i18n.mailingMembersPlaceholder }, list ? list.members.filter(m => m !== list.author).join(", ") : ""), br() + ] + : null, + label(i18n.mailingTagsLabel), br(), + input({ type: "text", name: "tags", maxlength: "200", placeholder: i18n.mailingTagsPlaceholder, value: list ? list.tags.join(", ") : "" }), br(), br(), + button({ type: "submit", class: "create-button" }, list ? i18n.mailingUpdate : i18n.mailingCreate) + ) + ); + +const renderHeader = () => div({ class: "tags-header module-header-line" }, h2(i18n.mailingTitle), p(i18n.mailingDescription)); + +exports.mailingView = async (lists, filter = "ALL", params = {}) => { + const list = Array.isArray(lists) ? lists : []; + const q = String(params.q || "").trim(); + const census = Array.isArray(params.censusList) ? params.censusList : list; + const f = String(filter || "ALL").toUpperCase(); + const isForm = f === "CREATE" || f === "EDIT"; + const emptyMod = !isForm && moduleIsEmpty(census, "ALL", "ALL", q); + return template( + i18n.mailingTitle, + section( + div({ class: "tags-header module-header-line" }, h2(i18n.mailingTitle), p(i18n.mailingDescription)), + renderFilters(isForm ? "ALL" : f, q, isForm ? [] : census, emptyMod) + ), + section( + isForm + ? renderForm(f === "EDIT" ? params.list : null) + : [ + emptyMod ? null : div({ class: "filters activity-filter-chips activity-toolbar-row" }, + renderModuleStats(list.length, [ + { label: typeLabel("OPEN"), count: list.filter(l => !l.closed).length }, + { label: typeLabel("CLOSED"), count: list.filter(l => l.closed).length }, + { label: statusLabel("ARCHIVED"), count: list.filter(l => l.status === "ARCHIVED").length } + ]), + form({ method: "GET", action: "/mailing", class: "filter-box" }, + input({ type: "hidden", name: "filter", value: f }), + input({ type: "text", name: "q", value: q, placeholder: i18n.mailingSearchPlaceholder, class: "filter-box__input" }), + div({ class: "filter-box__controls" }, button({ type: "submit", class: "filter-box__button" }, i18n.searchButton)) + ) + ), + list.length ? ul({ class: "mailing-archive" }, ...paged(list.slice().sort((x, y) => (y.lastActivityTs || 0) - (x.lastActivityTs || 0))).map(l => renderArchiveItem(l, params))) : div({ class: "tribe-grid" }, p(i18n.mailingNoItems)) + ] + ) + ); +}; + +const renderMessage = (list, m, opts = {}) => + div({ class: "mailing-message" }, + div({ class: "emergency-update-head mailing-message-head" }, + span({ class: "mailing-message-subject" }, m.subject || i18n.pmNoSubject), + list.canWrite && !opts.noReply + ? div({ class: "tribe-side-actions emergency-update-actions" }, + form({ method: "GET", action: `${listHref(list)}#compose`, class: "mailing-reply-form" }, + input({ type: "hidden", name: "write", value: "1" }), + input({ type: "hidden", name: "reply", value: m.mid }), + input({ type: "hidden", name: "history", value: opts.mode || "DATE" }), + button({ type: "submit", class: "tribe-action-btn" }, String(i18n.pmReply || "Reply").toUpperCase()) + ) + ) + : null + ), + div({ class: "mailing-message-text" }, ...renderStyledText(m.text || "")), + p({ class: "card-footer" }, span({ class: "date-link" }, `${fmt(m.sentAt)}`), userLink(m.author)) + ); + +const renderHistory = (list, history, mode) => { + if (!history.length) return p(i18n.mailingNoMessages); + if (mode === "THREADS") { + const groups = new Map(); + for (const m of history) { + if (!groups.has(m.thread)) groups.set(m.thread, []); + groups.get(m.thread).push(m); + } + const ordered = Array.from(groups.values()).sort((x, y) => y[y.length - 1].ts - x[x.length - 1].ts); + return div({ class: "mailing-threads" }, ...ordered.map(msgs => { + const root = msgs[0]; + const replies = msgs.slice(1); + return div({ class: "mailing-thread" }, + renderMessage(list, root, { mode }), + replies.length + ? details({ class: "mailing-thread-details" }, + summary(`${replies.length} ${replies.length === 1 ? (i18n.pmReply || "reply") : (i18n.pmReplies || "replies")}`), + ...replies.map(r => renderMessage(list, r, { mode })) + ) + : null + ); + })); + } + if (mode === "INHABITANTS") { + const groups = new Map(); + for (const m of history) { + if (!groups.has(m.author)) groups.set(m.author, []); + groups.get(m.author).push(m); + } + const ordered = Array.from(groups.entries()).sort((x, y) => y[1].length - x[1].length); + return div({ class: "mailing-by-author" }, ...ordered.map(([author, msgs]) => + div({ class: "mailing-author-group" }, + h3({ class: "mailing-author-title" }, userLink(author), span({ class: "mailing-author-count" }, ` · ${msgs.length}`)), + ...msgs.slice().sort((x, y) => y.ts - x.ts).map(m => renderMessage(list, m, { mode })) + ) + )); + } + return div({ class: "mailing-by-date" }, ...history.slice().sort((x, y) => y.ts - x.ts).map(m => renderMessage(list, m, { mode }))); +}; + +const renderCompose = (list, params) => { + const parent = params.replyTo ? (list.history || []).find(m => m.mid === params.replyTo || m.key === params.replyTo) : null; + const draft = params.draft || null; + const subject = draft ? String(draft.subject || "") : parent ? (/^\s*RE:/i.test(parent.subject) ? parent.subject : `RE: ${parent.subject}`) : ""; + const thread = parent ? parent.thread : draft ? String(draft.thread || "") : ""; + return div({ class: "div-center audio-form mailing-compose", id: "compose" }, + h2(parent ? i18n.mailingReplyTitle : i18n.mailingWriteTitle), + form({ method: "POST", action: `/mailing/${encodeURIComponent(list.id)}/message` }, + thread ? input({ type: "hidden", name: "thread", value: thread }) : null, + params.returnTo ? input({ type: "hidden", name: "returnTo", value: params.returnTo }) : null, + label(i18n.pmSubject), br(), + input({ type: "text", name: "subject", maxlength: "150", placeholder: i18n.pmSubjectHint, value: subject }), br(), br(), + label(i18n.pmText), br(), + textarea({ name: "text", rows: 8, maxlength: String(TEXT_CAP), required: true, autofocus: true, placeholder: parent ? parent.text.split("\n").map(l => `> ${l}`).join("\n") : i18n.mailingTextPlaceholder }, draft ? String(draft.text || "") : ""), br(), br(), + button({ type: "submit", class: "create-button" }, i18n.mailingSend) + ) + ); +}; + +exports.singleMailingView = async (list, params = {}) => { + const isAuthor = String(list.author) === String(userId); + const href = listHref(list); + const census = Array.isArray(params.censusList) ? params.censusList : []; + const history = Array.isArray(list.history) ? list.history : []; + const mode = HISTORY_MODES.includes(String(params.history || "").toUpperCase()) ? String(params.history).toUpperCase() : "THREADS"; + const q = String(params.q || "").trim(); + const shown = q ? history.filter(m => [m.subject, m.text, m.author].some(v => String(v || "").toLowerCase().includes(q.toLowerCase()))) : history; + const historyChips = history.length + ? div({ class: "activity-sub-filter" }, + ...HISTORY_MODES.map(m => form({ class: "sub-filter-form", method: "GET", action: `${href}#history` }, + input({ type: "hidden", name: "history", value: m }), + input({ type: "hidden", name: "q", value: q }), + button({ type: "submit", class: mode === m ? "filter-btn active" : "filter-btn" }, historyLabel(m)) + )) + ) + : null; + const sideActions = [subscribeForm(list, href)].filter(Boolean); + const ownerActions = isAuthor + ? [ + form({ method: "GET", action: "/mailing" }, input({ type: "hidden", name: "filter", value: "edit" }), input({ type: "hidden", name: "id", value: list.id }), button({ type: "submit", class: "update-btn" }, i18n.mailingUpdate)) + ] + : []; + const side = div({ class: "tribe-side" }, + div({ class: "card-header activity-card-header" }, + renderContentActions(list.id, href, { author: list.author, favKind: "mailing", isFavorite: list.isFavorite, reportTitle: list.title, spread: params.spread || null, deleteAction: isAuthor ? contentDeleteAction("mailingList", list.id) : null }) + ), + div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, list.title)), + div({ class: "card-chips-row" }, renderOpenClosedChip(list.listType, { statusChipOPEN: typeLabel("OPEN"), statusChipCLOSED: typeLabel("CLOSED") }), statusChip(list)), + list.description ? p({ class: "tribe-side-description" }, ...renderStyledText(list.description)) : null, + table({ class: "tribe-info-table jobs-info-table" }, + tr( + td({ class: "tribe-info-label" }, i18n.createdAtLabel || "Created at"), + td({ class: "tribe-info-value", colspan: "3" }, fmt(list.createdAt)) + ), + tr( + td({ class: "tribe-info-value", colspan: "4" }, userLink(list.author)) + ) + ), + div({ class: "tribe-card-members" }, + span({ class: "tribe-members-count" }, `${i18n.mailingSubscribers}: ${list.participantCount || 0}`) + ), + sideActions.length ? div({ class: "tribe-side-actions" }, ...sideActions) : null, + isAuthor + ? div({ class: "tribe-side-actions housing-status-row" }, + span({ class: "card-label" }, `${i18n.statusLabel || "Status"}: `), + statusChip(list), + form({ method: "POST", action: `/mailing/status/${encodeURIComponent(list.id)}`, class: "inline-form" }, + button({ class: "tribe-action-btn", type: "submit", name: "status", value: list.status === "ACTIVE" ? "ARCHIVED" : "ACTIVE" }, + String(list.status === "ACTIVE" ? i18n.mailingArchive : i18n.mailingActivate).toUpperCase()) + ) + ) + : null, + div({ class: "tribe-card-members" }, + span({ class: "tribe-members-count" }, `${i18n.mailingMessagesLabel}: ${list.messageCount || 0}`) + ), + writeForm(list) ? div({ class: "tribe-side-actions" }, writeForm(list)) : null, + div({ class: "doc-export-actions" }, + form({ method: "GET", action: `${href}/pdf` }, button({ type: "submit", class: "filter-btn" }, i18n.mailingGenerateReport)), + form({ method: "POST", action: `${href}/share` }, button({ type: "submit", class: "filter-btn" }, i18n.sharePm)) + ), + renderTags(list.tags), + ownerActions.length ? div({ class: "tribe-side-actions owner-actions" }, ...ownerActions) : null + ); + const main = div({ class: "tribe-main" }, + params.write && list.status === "ACTIVE" && (!list.closed || list.isMember) ? renderCompose(list, params) : null, + div({ class: "card-section mailing-history", id: "history" }, + historyChips, + history.length + ? form({ method: "GET", action: `${href}#history`, class: "filter-box" }, + input({ type: "hidden", name: "history", value: mode }), + input({ type: "text", name: "q", value: q, placeholder: i18n.mailingHistorySearchPlaceholder, class: "filter-box__input" }), + div({ class: "filter-box__controls" }, button({ type: "submit", class: "filter-box__button" }, i18n.searchButton)) + ) + : null, + renderHistory(list, shown, mode) + ) + ); + return template( + list.title, + section(renderHeader(), renderFilters("ALL", "", census, census.length === 0)), + section(div({ class: "tribe-details" }, side, main)) + ); +}; diff --git a/src/views/main_views.js b/src/views/main_views.js index b16b17e..8ee372e 100644 --- a/src/views/main_views.js +++ b/src/views/main_views.js @@ -1,15 +1,16 @@ "use strict"; const path = require("path"); +const RAW_HTML = Symbol.for('oasis.rawHtml'); const fs = require("fs"); const envPaths = require("../server/node_modules/env-paths"); const debug = require("../server/node_modules/debug")("oasis"); -const highlightJs = require("../server/node_modules/highlight.js"); +const highlightJs = require("../server/node_modules/highlight.js/lib/core"); +highlightJs.registerLanguage("json", require("../server/node_modules/highlight.js/lib/languages/json")); const prettyMs = require("../server/node_modules/pretty-ms"); const moment = require('../server/node_modules/moment'); -const QRCode = require('../server/node_modules/qrcode'); -const { renderUrl } = require('../backend/renderUrl'); +const { renderStyledText, renderStyledHtml, plainText, safeExternalHref } = require('../backend/renderStyledText'); const ssbClientGUI = require("../client/gui"); const config = require("../server/ssb_config"); const cooler = ssbClientGUI({ offline: config.offline }); @@ -18,15 +19,17 @@ const sharedState = require('../configs/shared-state'); let ssb, userId; const getUserId = async () => { + if (userId) return userId; + if (config && config.keys && config.keys.id) { userId = config.keys.id; return userId; } if (!ssb) ssb = await cooler.open(); - if (!userId) userId = ssb.id; + userId = ssb.id; return userId; }; +exports.getUserId = getUserId; -const { a, article, br, body, button, details, div, em, footer, form, h1, h2, h3, head, header, hr, html, img, input, label, li, link, main, meta, nav, option, p, pre, section, select, span, summary, table, td, textarea, title, tr, ul, strong, script, video: videoHyperaxe, audio: audioHyperaxe } = require("../server/node_modules/hyperaxe"); +const { a, article, br, body, button, details, div, em, footer, form, h1, h2, h3, head, header, hr, html, img, input, label, li, link, main, meta, nav, option, p, pre, section, select, span, summary, table, td, textarea, title, tr, ul, strong, video: videoHyperaxe, audio: audioHyperaxe } = require("../server/node_modules/hyperaxe"); const lodash = require("../server/node_modules/lodash"); -const markdown = require("./markdown"); const { sanitizeHtml } = require('../backend/sanitizeHtml'); const nameCache = require('../backend/nameCache'); @@ -46,18 +49,137 @@ const userLink = (feedId, knownName) => { }; exports.userLink = userLink; + +const renderCreatedRows = (createdAt, author, updatedAt) => { + const createdTs = createdAt ? new Date(createdAt).getTime() : NaN; + const updatedTs = updatedAt ? new Date(updatedAt).getTime() : NaN; + const showUpdated = Number.isFinite(updatedTs) && (!Number.isFinite(createdTs) || updatedTs !== createdTs); + return [ + createdAt ? tr(td({ class: 'tribe-info-label' }, i18n.createdAtLabel), td({ class: 'tribe-info-value', colspan: '3' }, moment(createdAt).format('YYYY/MM/DD HH:mm'))) : null, + showUpdated ? tr(td({ class: 'tribe-info-label' }, i18n.jobsUpdatedAt), td({ class: 'tribe-info-value', colspan: '3' }, moment(updatedAt).format('YYYY/MM/DD HH:mm'))) : null, + author ? tr(td({ class: 'tribe-info-value', colspan: '4' }, userLink(author))) : null + ]; +}; +exports.renderCreatedRows = renderCreatedRows; + +const inboxBadges = () => { + const inboxCount = Number(sharedState.getInboxCount()) || 0; + return [inboxCount > 0 ? span({ class: 'inbox-badge' }, String(inboxCount)) : '']; +}; +const phoneOn = () => !config.public && getConfig().modules.phoneMod !== 'off'; +const PHONE_PAM_MAX_MS = require('../server/phone_module').VOICEMAIL_MAX_MS; +const renderCallButton = (feedId, { upper = false, cls = null, pam = false } = {}) => { + if (!phoneOn() || !feedId || String(feedId) === String(config.keys.id) || sharedState.isPubId(feedId)) return null; + if (pam) return a({ href: `/phone?filter=create&to=${encodeURIComponent(feedId)}`, class: cls || "filter-btn" }, `✆ ${i18n.phoneCallTooltip}`); + const label = upper ? String(i18n.phoneCallButton).toUpperCase() : i18n.phoneCallButton; + return form({ method: "POST", action: "/phone/call", class: "phone-action-form" }, + input({ type: "hidden", name: "to", value: feedId }), + button({ type: "submit", class: cls || "filter-btn", title: i18n.phoneCallButton }, `✆ ${label}`) + ); +}; +exports.renderCallButton = renderCallButton; +const pad2 = (n) => String(n).padStart(2, '0'); +const liveClock = (fromMs, { max = 0 } = {}) => { + const from = Number(fromMs) || Date.now(); + const e = Math.max(0, Math.floor((Date.now() - from) / 1000)); + const roll = (cls, labels) => span({ class: 'live-clock-win' }, span({ class: `live-clock-roll ${cls}` }, ...labels.map(l => span(l)))); + if (max > 0) { + const cap = Math.floor(max / 1000); + return span({ class: 'live-clock' }, roll('live-clock-once', Array.from({ length: 60 }, (_, i) => { const v = Math.min(cap, e + i); return `${pad2(Math.floor(v / 60))}:${pad2(v % 60)}`; }))); + } + const m = Math.floor(e / 60), sec = e % 60; + return span({ class: 'live-clock' }, + roll(`live-clock-min lc-d${sec}`, Array.from({ length: 60 }, (_, i) => `${pad2(m + i)}:`)), + roll('live-clock-sec', Array.from({ length: 60 }, (_, i) => pad2((sec + i) % 60))) + ); +}; +exports.liveClock = liveClock; +const renderPhoneTopLink = () => { + if (!phoneOn()) return null; + const n = Number(sharedState.getPhoneCount()) || 0; + return li(a({ href: "/phone" }, span({ class: "emoji" }, "✆"), nbsp, i18n.phoneTitle, n > 0 ? span({ class: 'inbox-badge' }, String(n)) : '')); +}; exports.userLinkLabel = userLinkLabel; const renderInviteQrCard = ({ qrDataUrl }) => qrDataUrl ? div({ class: 'invite-qr-card' }, img({ src: qrDataUrl, alt: 'QR', class: 'invite-qr-img' })) : null; exports.renderInviteQrCard = renderInviteQrCard; +const renderLiveFeed = (src, imgClass) => { + const id = `live-${String(src).replace(/[^a-zA-Z0-9]/g, '').slice(-24)}`; + return [ + a({ href: `#${id}`, id: `${id}-src`, class: 'zoom-link zoom-inline' }, img({ src, alt: '', class: imgClass })), + div({ id, class: 'lightbox live-lightbox' }, + a({ href: `#${id}-src`, class: 'lightbox-close' }, '\u00d7'), + a({ href: `#${id}-full`, class: 'live-fullscreen-btn', title: i18n.liveFullscreen }, renderIcon('fullscreen')), + a({ href: `#${id}-src`, class: 'lightbox-image-link' }, img({ src, alt: '', class: 'lightbox-image', loading: 'lazy' })) + ), + div({ id: `${id}-full`, class: 'lightbox live-lightbox live-fullscreen' }, + a({ href: `#${id}`, class: 'lightbox-close' }, '\u00d7'), + a({ href: `#${id}`, class: 'lightbox-image-link' }, img({ src, alt: '', class: 'lightbox-image', loading: 'lazy' })) + ) + ]; +}; +exports.renderLiveFeed = renderLiveFeed; + +const renderVideoGrid = (peers, me, { film = false } = {}) => { + const list = (Array.isArray(peers) ? peers : []).filter(p => p && p.id); + const filmName = (...label) => span({ class: 'video-tile-name video-tile-film' }, '▶ ', ...label); + const tiles = [ + me ? div({ class: 'video-tile video-tile-me' }, ...renderLiveFeed('/phone/video/me', 'video-feed'), span({ class: 'video-tile-name' }, i18n.roomYou)) : null, + film ? div({ class: 'video-tile' }, ...renderLiveFeed(`/phone/video/${encodeURIComponent('film:me')}`, 'video-feed'), filmName(i18n.roomYou)) : null, + ...list.filter(p => p.on).map(p => div({ class: 'video-tile' }, ...renderLiveFeed(`/phone/video/${encodeURIComponent(p.id)}`, 'video-feed'), userLink(p.id))), + ...list.filter(p => p.film).map(p => div({ class: 'video-tile' }, ...renderLiveFeed(`/phone/video/${encodeURIComponent(`film:${p.id}`)}`, 'video-feed'), filmName(userLink(p.id)))) + ].filter(Boolean); + return tiles.length ? div({ class: tiles.length === 1 ? 'video-grid video-grid-solo' : 'video-grid' }, ...tiles) : null; +}; +exports.renderVideoGrid = renderVideoGrid; + +const renderSubscriptionBox = ({ target, scope, subscribed, count, isOwner, returnTo, canWrite, inline, compact }) => { + if (!target) return null; + if (isOwner && (Number(count) || 0) <= 1) return null; + const showPm = canWrite !== undefined ? canWrite : (isOwner || subscribed); + const total = Number(count) || 0; + const countSuffix = total ? ` (${total})` : ''; + const parts = [ + total && !compact ? span({ class: 'card-label' }, `${i18n.subscriptionTitle} (${total})${isOwner ? '' : ':'}`) : null, + compact && isOwner && total ? renderStateChip('mutuals', '🔔', `${i18n.subscriptionTitle}${countSuffix}`) : null, + span({ class: 'subscription-actions' }, + isOwner + ? null + : form({ method: 'POST', action: '/subscriptions/toggle', class: 'subscription-form' }, + input({ type: 'hidden', name: 'target', value: target }), + input({ type: 'hidden', name: 'scope', value: String(scope || '') }), + input({ type: 'hidden', name: 'on', value: subscribed ? '0' : '1' }), + returnTo ? input({ type: 'hidden', name: 'returnTo', value: returnTo }) : null, + compact + ? button({ type: 'submit', class: subscribed ? 'delete-btn' : 'update-btn' }, `${subscribed ? i18n.subscriptionUnsubscribe : i18n.subscriptionSubscribe}${countSuffix}`) + : button({ type: 'submit', class: subscribed ? 'tribe-action-btn danger-btn' : 'tribe-action-btn' }, `${subscribed ? '🔕' : '🔔'} ${String(subscribed ? i18n.subscriptionUnsubscribe : i18n.subscriptionSubscribe).toUpperCase()}`) + ), + showPm && total + ? a({ href: `/pm?list=${encodeURIComponent(target)}`, class: 'btn-singleview btn-pm', title: i18n.pmCreateButton || 'Write a PM' }, renderIcon('mail')) + : null + ) + ]; + const nodes = parts.filter(Boolean); + return inline ? nodes : div({ class: 'tribe-side-actions shop-visibility-row stacked-action-row subscription-box-block' }, ...nodes); +}; +exports.renderSubscriptionBox = renderSubscriptionBox; + const renderStateChip = (variant, icon, text) => span({ class: `pm-exposition-chip pm-exposition-${variant}` }, icon ? span({ class: "pm-exposition-icon" }, icon) : null, span({ class: "pm-exposition-text" }, String(text || "")) ); +const renderPhoneCallForm = (to, inner, title) => !phoneOn() || !to + ? inner + : form({ method: 'POST', action: '/phone/call', class: 'phone-chip-form' }, + input({ type: 'hidden', name: 'to', value: String(to) }), + button({ type: 'submit', class: 'phone-chip-btn', title: title || i18n.phoneCallButton }, inner)); +const renderPhoneChip = (number, title) => number ? renderPhoneCallForm(number, renderStateChip('whole', '✆', number), title) : null; +exports.renderPhoneChip = renderPhoneChip; + const renderOpenClosedChip = (status, i18nObj) => { const s = String(status || "").toUpperCase(); const isOpen = s === "OPEN"; @@ -87,9 +209,10 @@ const renderModeChip = (mode, i18nObj) => { const renderLifespanChip = (lifetime, i18nObj) => { const lt = lifetime || null; if (!lt || !lt.bucket) return null; - const range = lt.range || ""; + const range = lt.contentRange || lt.range || ""; + const bucket = lt.contentBucket || lt.bucket; if (range) { - return span({ class: `pm-exposition-chip pm-exposition-lifespan-${lt.bucket}` }, + return span({ class: `pm-exposition-chip pm-exposition-lifespan-${bucket}` }, span({ class: "pm-exposition-text" }, range) ); } @@ -99,18 +222,284 @@ const renderLifespanChip = (lifetime, i18nObj) => { ); }; -const renderContentActions = (msgId, viewHref) => { +const DATE_TIME_FORMAT = "YYYY/MM/DD HH:mm"; +const DATE_FORMAT = "YYYY/MM/DD"; +const fmtDateTime = (v) => (v ? moment(v).format(DATE_TIME_FORMAT) : ""); +const fmtDay = (v) => (v ? moment(v).format(DATE_FORMAT) : ""); +exports.DATE_TIME_FORMAT = DATE_TIME_FORMAT; +exports.DATE_FORMAT = DATE_FORMAT; +exports.fmtDateTime = fmtDateTime; +exports.fmtDay = fmtDay; + +const clearnetShortId = (id) => String(id || '').replace(/^[%&@]/, '').replace(/[^A-Za-z0-9]/g, '').slice(0, 8).toLowerCase(); +const clearnetSlugFor = (title, id) => { + const base = String(title || '').normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 60).replace(/-+$/, ''); + const short = clearnetShortId(id); + return base ? `${base}-${short}` : short; +}; +exports.clearnetShortId = clearnetShortId; +exports.clearnetSlugFor = clearnetSlugFor; +const clearnetItemHref = (modulePath, title, id) => `/c/${modulePath}/${encodeURIComponent(clearnetSlugFor(title, id))}`; +exports.clearnetItemHref = clearnetItemHref; +const CLEARNET_PATHS = { blogs: 'blog', wiki: 'wiki', market: 'market', audios: 'audios', videos: 'videos', images: 'images', documents: 'documents', bookmarks: 'bookmarks', events: 'events', feed: 'feed', jobs: 'jobs', podcasts: 'podcasts', projects: 'projects', torrents: 'torrents', files: 'files', shops: 'shops', emergencies: 'emergencies', campaigns: 'campaigns', housing: 'housing' }; +let clearnetBaseCache = null; +const clearnetBase = () => { + if (clearnetBaseCache !== null) return clearnetBaseCache; + clearnetBaseCache = String((require('../configs/config-manager.js').defaultInvite() || {}).url || '').replace(/\/+$/, ''); + return clearnetBaseCache; +}; +const clearnetHrefFor = (viewHref, blockId, title) => { + const m = String(viewHref || '').match(/^\/(school\/course|[a-z]+)\/([^/?#]+)/); + if (!m) return null; + const seg = m[1] === 'school/course' ? 'school' : CLEARNET_PATHS[m[1]]; + if (!seg) return null; + const base = clearnetBase(); + const id = blockId ? encodeURIComponent(clearnetSlugFor(title, blockId)) : m[2]; + return base ? `${base}/c/${seg}/${id}` : null; +}; +let linkBoxSeq = 0; +const renderLinkBox = (blockId, viewHref, title) => { + linkBoxSeq = (linkBoxSeq + 1) % 1000000; + const id = `linkbox-${linkBoxSeq}-${String(blockId).replace(/[^a-zA-Z0-9]/g, '').slice(-10)}`; + const clearnetHref = clearnetHrefFor(viewHref, blockId, title); + return [ + a({ href: `#${id}`, id: `${id}-src`, class: 'btn-singleview btn-link', title: i18n.copyLinkTitle }, renderIcon('link')), + span({ id, class: 'lightbox link-lightbox' }, + a({ href: `#${id}-src`, class: 'lightbox-close' }, '\u00d7'), + span({ class: 'link-box' }, + span({ class: 'link-box-row' }, span({ class: 'link-box-label' }, i18n.linkBoxOasis), input({ type: 'text', readonly: true, class: 'link-box-input', value: viewHref })), + clearnetHref ? span({ class: 'link-box-row' }, span({ class: 'link-box-label' }, i18n.linkBoxClearnet), input({ type: 'text', readonly: true, class: 'link-box-input', value: clearnetHref })) : null + ) + ) + ]; +}; + +const renderWalletChip = () => { + try { if (getConfig().modules.walletMod !== 'on') return null; } catch (_) { return null; } + const ready = sharedState.getWalletReady ? sharedState.getWalletReady() : false; + const chip = renderStateChip(ready ? 'mutuals' : 'closed', '❄', ready ? i18n.bankWalletReady : i18n.bankWalletNotConfigured); + return ready ? chip : a({ href: '/wallet', class: 'wallet-chip-link' }, chip); +}; +exports.renderWalletChip = renderWalletChip; + +const torrentFileName = (title) => `${String(title || 'download').replace(/\.torrent$/i, '')}.torrent`; +const torrentDownloadHref = (blobId, title) => `/blob/${encodeURIComponent(blobId)}?name=${encodeURIComponent(torrentFileName(title))}`; +const renderTorrentDownload = (href, opts = {}) => + a({ href, class: opts.class || 'filter-btn' }, `\u2B07 ${i18n.torrentExternalButton || 'EXTERNAL'}`); +exports.torrentDownloadHref = torrentDownloadHref; +exports.renderTorrentDownload = renderTorrentDownload; +const renderTorrentSourceDownload = (torrentKey, source, opts = {}) => { + if (typeof source !== 'string' || !source.startsWith('&') || !torrentKey) return null; + return a({ href: `/torrents/${encodeURIComponent(torrentKey)}/get`, class: opts.class || 'filter-btn' }, `\u2B07 ${i18n.torrentOasisButton || 'OASIS'}`); +}; +exports.renderTorrentSourceDownload = renderTorrentSourceDownload; +const renderFileDownloads = (fileKey, torrentUrl, name, opts = {}) => { + if (!fileKey) return null; + const btnClass = opts.class || 'filter-btn'; + const torrentName = `${String(name || 'download').replace(/\.torrent$/i, '')}.torrent`; + return div({ class: 'torrent-card-actions' }, + a({ href: `/files/${encodeURIComponent(fileKey)}/get`, class: btnClass }, `\u2B07 ${i18n.torrentOasisButton || 'OASIS'}`), + typeof torrentUrl === 'string' && torrentUrl.startsWith('&') + ? a({ href: `/blob/${encodeURIComponent(torrentUrl)}?name=${encodeURIComponent(torrentName)}`, class: btnClass }, `\u2B07 ${i18n.torrentExternalButton || 'EXTERNAL'}`) + : null + ); +}; +exports.renderFileDownloads = renderFileDownloads; + +const blobUrlOf = (v) => (String(v || '').startsWith('/blob/sealed/') ? String(v) : `/blob/${encodeURIComponent(v)}`); +exports.blobUrlOf = blobUrlOf; +const ACTION_ICONS = { + hide: '', + spread: '', + pin: '', + report: '', + mail: '', + call: '', + link: '', + block: '', + datagram: '', + open: '', + donate: '', + torrent: '', + close: '', + film: '', + fullscreen: '' +}; +const renderIcon = (name) => span({ class: 'btn-icon', [RAW_HTML]: `` }); +const renderHideIcon = () => renderIcon('hide'); +exports.renderIcon = renderIcon; +exports.renderHideIcon = renderHideIcon; +const renderContentActions = (msgId, viewHref, opts = {}) => { + const o = (opts && typeof opts === 'object') ? opts : {}; const blockId = (typeof msgId === 'string' && msgId.startsWith('%')) ? msgId : null; - const chainBtn = blockId - ? a({ href: `/blockexplorer/block/${encodeURIComponent(blockId)}`, class: 'btn-singleview', title: i18n.blockchainViewBlockexplorer || 'View blockexplorer' }, '⦿') + const myId = (config.keys && config.keys.id) ? config.keys.id : ''; + const linkBtn = blockId && viewHref ? renderLinkBox(blockId, viewHref, o.reportTitle) : null; + + const pinBtn = o.favKind && blockId + ? form({ method: 'POST', action: `/${o.favKind}/favorites/${o.isFavorite ? 'remove' : 'add'}/${encodeURIComponent(blockId)}`, class: 'content-action-form' }, + o.returnTo ? input({ type: 'hidden', name: 'returnTo', value: o.returnTo }) : null, + button({ type: 'submit', class: o.isFavorite ? 'btn-singleview btn-pin-on' : 'btn-singleview', title: o.isFavorite ? i18n.favoriteRemove : i18n.favoriteAdd }, renderIcon('pin')) + ) : null; - const contentBtn = viewHref - ? a({ href: viewHref, class: 'btn-singleview btn-content', title: i18n.visitContent || 'Visit content' }, '↗') + + const spreadBtn = o.spread !== undefined && blockId && !(o.author && String(o.author) === String(myId)) ? renderSpreadButton(blockId, o.spread, o.spreadTitle || null) : null; + + const torrentBtn = (() => { + if (process.env.OASIS_MOBILE === '1') return null; + const src = o.torrentFrom && typeof o.torrentFrom === 'object' ? o.torrentFrom : null; + if (!src || typeof src.blobId !== 'string' || !src.blobId.startsWith('&')) return null; + try { if (getConfig().modules.torrentsMod !== 'on') return null; } catch (_) { return null; } + const name = String(src.name || o.reportTitle || '').slice(0, 100); + return a({ href: `/torrents?filter=create&fromBlob=${encodeURIComponent(src.blobId)}&name=${encodeURIComponent(name)}`, class: 'btn-singleview btn-torrent', title: i18n.torrentFromContent }, renderIcon('torrent')); + })(); + + const chainBtn = blockId && process.env.OASIS_MOBILE !== '1' + ? a({ href: `/blockexplorer/block/${encodeURIComponent(blockId)}`, class: 'btn-singleview', title: i18n.blockchainViewBlockexplorer }, renderIcon('block')) : null; - if (!chainBtn && !contentBtn) return null; - return div({ class: 'content-actions' }, chainBtn, contentBtn); + + const herePath = (listScope() || {}).path; + const onItself = (() => { try { return !!herePath && decodeURIComponent(String(viewHref || '').split(/[?#]/)[0]) === decodeURIComponent(String(herePath)); } catch (_) { return false; } })(); + const contentBtn = viewHref && !onItself + ? a({ href: viewHref, class: 'btn-singleview btn-content', title: i18n.visitContent }, renderIcon('open')) + : null; + + const donateBtn = (() => { + try { if (getConfig().modules.walletMod !== 'on') return null; } catch (_) { return null; } + if (!o.author || String(o.author) === String(myId)) return null; + const donatable = sharedState.getDonatableAuthors ? sharedState.getDonatableAuthors() : null; + if (!donatable || !donatable.has(String(o.author))) return null; + const ref = viewHref ? `?returnTo=${encodeURIComponent(viewHref)}` : ''; + return a({ href: `/wallet/donate/${encodeURIComponent(o.author)}${ref}`, class: 'btn-singleview btn-donate', title: i18n.donateTooltip }, renderIcon('donate')); + })(); + + const reportHref = blockId + ? `/reports?filter=create&category=CONTENT&title=${encodeURIComponent(String(o.reportTitle || blockId).slice(0, 120))}&description=${encodeURIComponent(`${viewHref || blockId}`)}` + : null; + const reportBtn = o.report !== false && reportHref && !(o.author && String(o.author) === String(myId)) + ? a({ href: reportHref, class: 'btn-singleview btn-report', title: i18n.reportContent }, renderIcon('report')) + : null; + + const pmBtn = o.contact === true && o.author && String(o.author) !== String(myId) + ? a({ href: `/pm?recipients=${encodeURIComponent(o.author)}`, class: 'btn-singleview btn-pm', title: i18n.pmContentTooltip }, renderIcon('mail')) + : null; + const callBtn = o.contact === true && o.author && String(o.author) !== String(myId) && phoneOn() && !sharedState.isPubId(o.author) + ? a({ href: `/phone?filter=create&to=${encodeURIComponent(o.author)}`, class: 'btn-singleview btn-call', title: i18n.phoneCallTooltip }, renderIcon('call')) + : null; + + const deleteBtn = o.deleteAction && process.env.OASIS_MOBILE === '1' + ? require('./mobile_ui').deleteConfirm(o.deleteAction, o.returnTo && o.returnTo !== viewHref ? input({ type: 'hidden', name: 'returnTo', value: o.returnTo }) : null) + : o.deleteAction + ? form({ method: 'POST', action: o.deleteAction, class: 'content-action-form content-action-delete' }, + o.returnTo && o.returnTo !== viewHref ? input({ type: 'hidden', name: 'returnTo', value: o.returnTo }) : null, + button({ type: 'submit', class: 'btn-singleview btn-delete', title: i18n.chatDelete }, renderIcon('close')) + ) + : null; + + const hideBtn = blockId && o.hide !== false && !(o.author && String(o.author) === String(myId)) + ? form({ method: 'POST', action: `/hide/${encodeURIComponent(blockId)}`, class: 'content-action-form' }, + input({ type: 'hidden', name: 'returnTo', value: o.returnTo || viewHref || '/' }), + viewHref ? input({ type: 'hidden', name: 'href', value: viewHref }) : null, + o.reportTitle ? input({ type: 'hidden', name: 'label', value: String(o.reportTitle).slice(0, 120) }) : null, + button({ type: 'submit', class: 'btn-singleview btn-hide', title: i18n.hideContent }, renderHideIcon()) + ) + : null; + + if (!pinBtn && !spreadBtn && !chainBtn && !contentBtn && !reportBtn && !pmBtn && !callBtn && !deleteBtn && !donateBtn && !torrentBtn && !hideBtn) return null; + // movil: borrar al final, lejos del pulgar + if (process.env.OASIS_MOBILE === '1') return div({ class: 'content-actions' }, contentBtn, linkBtn, pinBtn, spreadBtn, pmBtn, callBtn, donateBtn, reportBtn, hideBtn, deleteBtn); + return div({ class: 'content-actions' }, deleteBtn, spreadBtn, pinBtn, reportBtn, hideBtn, donateBtn, pmBtn, callBtn, torrentBtn, linkBtn, chainBtn, contentBtn); }; exports.renderContentActions = renderContentActions; +const CONTENT_FAV_KIND = { + image: 'images', audio: 'audios', video: 'videos', document: 'documents', + bookmark: 'bookmarks', torrent: 'torrents', file: 'files', event: 'events', task: 'tasks', + report: 'reports', votes: 'votes', poll: 'polls', market: 'market', + housing: 'housing', job: 'jobs', project: 'projects', shop: 'shops', shopProduct: 'shopProducts', + chat: 'chats', chatThread: 'chats', pad: 'pads', room: 'rooms', calendar: 'calendars', + map: 'maps', forum: 'forum', transfer: 'transfers', post: 'blogs', schoolCourse: 'school', + wikiPage: 'wiki', emergency: 'emergencies', mailingList: 'mailing', logisticsRoute: 'logistics', + podcast: 'podcasts', podcastEpisode: 'podcasts', campaign: 'campaigns' +}; +const CONTENT_SPREADABLE = new Set([ + 'post', 'audio', 'video', 'image', 'document', 'torrent', 'file', 'bookmark', + 'event', 'calendar', 'task', 'votes', 'vote', 'market', 'shop', 'shopProduct', + 'project', 'transfer', 'housing', 'job', 'report', 'industry', 'industryBuild', 'industryBlueprint', + 'chat', 'chatMessage', 'pad', 'padEntry', 'room', 'wikiPage', 'emergency', 'mailingList', 'logisticsRoute', 'podcast', 'podcastEpisode', 'campaign', 'forum', 'map', 'poll', 'blog', 'schoolCourse', 'feed' +]); +const CONTENT_TORRENTABLE = new Set(['audio', 'video', 'image', 'document', 'file']); +const CONTENT_DELETE_ROUTE = { + audio: 'audios', video: 'videos', image: 'images', document: 'documents', bookmark: 'bookmarks', + torrent: 'torrents', file: 'files', event: 'events', task: 'tasks', report: 'reports', votes: 'votes', + poll: 'polls', market: 'market', housing: 'housing', job: 'jobs', project: 'projects', shop: 'shops', + shopProduct: 'shops/product', chat: 'chats', pad: 'pads', room: 'rooms', calendar: 'calendars', map: 'maps', mapMarker: 'maps/marker', + forum: 'forum', transfer: 'transfers', emergency: 'emergencies', mailingList: 'mailing', + logisticsRoute: 'logistics', podcast: 'podcasts', podcastEpisode: 'podcasts/episode', campaign: 'campaigns', + feed: 'feed', schoolCourse: 'school', tribe: 'tribes', blog: 'blogs' +}; +const contentDeleteAction = (type, id) => CONTENT_DELETE_ROUTE[type] && id ? `/${CONTENT_DELETE_ROUTE[type]}/delete/${encodeURIComponent(id)}` : undefined; +exports.CONTENT_FAV_KIND = CONTENT_FAV_KIND; +exports.CONTENT_SPREADABLE = CONTENT_SPREADABLE; +exports.CONTENT_TORRENTABLE = CONTENT_TORRENTABLE; +exports.contentDeleteAction = contentDeleteAction; +exports.THEMES = [['Acampada', 'Acampada'], ['Dark-SNH', 'Dark-SNH'], ['Clear-SNH', 'Clear-SNH'], ['Purple-SNH', 'Purple-SNH'], ['Matrix-SNH', 'Matrix-SNH'], ['OasisMobile', 'Oasis-Mobile']]; +// los dos skins de movil; los de escritorio no estan pensados para la maqueta del movil +exports.MOBILE_THEMES = [['Acampada', 'Acampada'], ['OasisMobile', 'Oasis Mobile']]; +exports.WIZARD_THEMES = process.env.OASIS_MOBILE === '1' + ? exports.MOBILE_THEMES + : exports.THEMES.filter(([value]) => value !== 'OasisMobile' && value !== 'Acampada'); +// en el movil la maqueta de la rama (styles/mobile_base.css) va antes de cualquier skin; +// encima, Acampada.css o el OasisMobile.css del dev. hive_acampada.css va detras del +// tema Acampada: pinta lanzador, barras y tarjetas. +const acampadaBase = () => process.env.OASIS_MOBILE === '1' ? link({ rel: "stylesheet", href: `/assets/styles/mobile_base.css?v=${assetVersion()}` }) : null; +const acampadaLink = (theme) => theme === 'Acampada' ? link({ rel: "stylesheet", href: `/assets/styles/hive_acampada.css?v=${assetVersion()}` }) : null; + +const renderDocumentActions = (kind, id, leadingNodes = []) => { + const base = id ? `/${kind}/${encodeURIComponent(id)}` : `/${kind}`; + return div({ class: 'doc-export-actions' }, + ...leadingNodes, + form({ method: 'GET', action: `${base}/pdf` }, + button({ type: 'submit', class: 'filter-btn' }, i18n.generatePdf) + ), + form({ method: 'POST', action: `${base}/share` }, + button({ type: 'submit', class: 'filter-btn' }, i18n.sharePm) + ) + ); +}; +exports.renderDocumentActions = renderDocumentActions; + +const renderRelationshipBlock = (relationship, feedId) => { + const rel = relationship || {}; + if (rel.me) return span({ class: 'status you' }, i18n.relationshipYou); + const muted = (() => { try { return require('../models/viewer_filters').isMuted(feedId); } catch (_) { return false; } })(); + const actions = []; + const icons = { follow: '♥', unfollow: '✕', block: '⊘', unblock: '↺', mute: '◌', unmute: '●' }; + const addAction = (action) => actions.push( + form({ action: `/${action}/${encodeURIComponent(feedId)}`, method: 'post' }, + button({ type: 'submit', class: 'filter-btn' }, `${icons[action]} ${i18n[action]}`) + ) + ); + if (rel.following) addAction('unfollow'); + else if (rel.blocking) addAction('unblock'); + else { addAction('follow'); addAction('block'); } + if (!rel.blocking) addAction(muted ? 'unmute' : 'mute'); + return div({ class: 'relationship-status' }, + muted ? span({ class: 'status blocked' }, i18n.relationshipMuted) : null, + rel.blocking && rel.blockedBy + ? span({ class: 'status blocked' }, i18n.relationshipMutualBlock) + : [ + rel.blocking ? span({ class: 'status blocked' }, i18n.relationshipBlocking) : null, + rel.blockedBy ? span({ class: 'status blocked-by' }, i18n.relationshipBlockedBy) : null, + rel.following && rel.followsMe + ? span({ class: 'status mutual' }, i18n.relationshipMutuals) + : [ + span({ class: 'status supporting' }, rel.following ? i18n.relationshipFollowing : i18n.relationshipNone), + span({ class: 'status supported-by' }, rel.followsMe ? i18n.relationshipTheyFollow : i18n.relationshipNotFollowing) + ] + ], + actions.length ? div({ class: 'relationship-actions' }, ...actions) : null + ); +}; +exports.renderRelationshipBlock = renderRelationshipBlock; exports.renderStateChip = renderStateChip; exports.renderOpenClosedChip = renderOpenClosedChip; @@ -156,7 +545,7 @@ const renderEcoTax = (sizeBytes, blockId) => { ? ` · reducer ×${reducer.toFixed(2)} (${inhabitants} inhabitants)` : ''; const title = `${label} · ${formatCarbon(sizeBytes)}${reducerNote}`; - return a({ href, class: `eco-tax-chip eco-tax-chip-${band}`, title }, + return (process.env.OASIS_MOBILE === '1' ? span : a)({ ...(process.env.OASIS_MOBILE === '1' ? {} : { href }), class: `eco-tax-chip eco-tax-chip-${band}`, title }, span({ class: 'eco-tax-chip-label' }, label + ': '), span({ class: 'eco-tax-chip-value' }, formatCarbon(sizeBytes)) ); @@ -165,6 +554,101 @@ const renderEcoTax = (sizeBytes, blockId) => { exports.formatCarbon = formatCarbon; exports.renderEcoTax = renderEcoTax; +const renderEcoValueChip = () => { + const v = Number(sharedState.getEcoValue ? sharedState.getEcoValue() : NaN); + if (!Number.isFinite(v) || v <= 0) return null; + const title = (i18n && i18n.bankExchangeCurrentValue) ? i18n.bankExchangeCurrentValue : 'ECOin Value (1h)'; + return a({ href: '/banking?filter=exchange', class: 'eco-tax-chip eco-tax-chip-mid eco-value-chip', title }, + span({ class: 'eco-tax-chip-label' }, 'ECO/h: '), + span({ class: 'eco-tax-chip-value' }, v.toFixed(4)) + ); +}; +exports.renderEcoValueChip = renderEcoValueChip; + +const renderMobileCounters = () => { + if (process.env.OASIS_MOBILE === '1') return null; + try { + const theme = (getConfig().themes || {}).current; + if (theme !== 'OasisMobile' && process.env.OASIS_MOBILE !== '1') return null; + } catch (_) { return null; } + const inbox = Number(sharedState.getInboxCount()) || 0; + const mentions = Number(sharedState.getMentionsCount()) || 0; + const phone = phoneOn() ? (Number(sharedState.getPhoneCount()) || 0) : 0; + if (inbox <= 0 && mentions <= 0 && phone <= 0) return null; + return div({ class: 'mobile-counters' }, + inbox > 0 ? a({ href: '/inbox', class: 'mobile-counter', title: i18n.inbox }, '☂ ', String(inbox)) : null, + phone > 0 ? a({ href: '/phone', class: 'mobile-counter', title: i18n.phoneTitle }, '✆ ', String(phone)) : null, + mentions > 0 ? a({ href: '/mentions', class: 'mobile-counter', title: i18n.mentions }, '✺ ', String(mentions)) : null + ); +}; + +const LIST_PAGE_SIZE = 50; +const LIST_PAGE_SIZES = ['10', '50', '100', 'all']; +const listScope = () => { try { return require('../models/typed_log').requestScope.getStore() || null; } catch (_) { return null; } }; +const listPerPage = (query) => { + const v = String(new URLSearchParams(String(query || '')).get('perPage') || '').toLowerCase(); + return LIST_PAGE_SIZES.includes(v) ? v : String(LIST_PAGE_SIZE); +}; +const slicePage = (list, page, per) => { + const all = Array.isArray(list) ? list : []; + const size = per === 'all' ? Math.max(1, all.length) : Number(per) || LIST_PAGE_SIZE; + const pages = Math.max(1, Math.ceil(all.length / size)); + const current = Math.min(pages, Math.max(1, parseInt(page, 10) || 1)); + return { items: all.slice((current - 1) * size, current * size), page: current, pages, total: all.length, per: String(per) }; +}; +const showPageSizes = (total, query) => total > Number(LIST_PAGE_SIZES[0]) && (total > LIST_PAGE_SIZE || new URLSearchParams(String(query || '')).has('perPage')); +const pageSizeLabel = (s) => s === 'all' ? String(i18n.all || 'All').toUpperCase() : s; +exports.LIST_PAGE_SIZE = LIST_PAGE_SIZE; +exports.LIST_PAGE_SIZES = LIST_PAGE_SIZES; +exports.listPerPage = listPerPage; +exports.slicePage = slicePage; +exports.showPageSizes = showPageSizes; +exports.pageSizeLabel = pageSizeLabel; +const paged = (list) => { + const store = listScope(); + const query = store ? store.query : ''; + const { items, ...pager } = slicePage(list, new URLSearchParams(String(query || '')).get('page'), listPerPage(query)); + if (store) { store.pager = pager; store.pageItems = items; } + return items; +}; +exports.paged = paged; +exports.currentPerPage = () => { const store = listScope(); return listPerPage(store ? store.query : ''); }; +const renderListPager = () => { + const store = listScope(); + const pg = store && store.pager; + if (!pg) return null; + const sizes = showPageSizes(pg.total, store.query); + if (!(pg.pages > 1) && !sizes) return null; + const href = (n, per) => { + const q = new URLSearchParams(String(store.query || '')); + for (const k of ['page', 'error', 'errsig']) q.delete(k); + if (per) q.set('perPage', per); + if (n > 1) q.set('page', String(n)); + const s = q.toString(); + return s ? `${store.path}?${s}` : store.path; + }; + return nav({ class: 'oasis-pager' }, + sizes ? span({ class: 'oasis-pager-sizes' }, + span({ class: 'oasis-pager-info' }, i18n.searchPerPageLabel || 'Results per page'), + ...LIST_PAGE_SIZES.map(s => a({ href: href(1, s), class: s === pg.per ? 'oasis-pager-btn active' : 'oasis-pager-btn' }, pageSizeLabel(s))) + ) : null, + pg.pages > 1 ? span({ class: 'oasis-pager-info' }, String(i18n.cnPageOf || 'Page {page} of {pages}').replace('{page}', String(pg.page)).replace('{pages}', String(pg.pages))) : null, + pg.page > 1 ? a({ href: href(pg.page - 1), class: 'oasis-pager-btn' }, i18n.cnPrevPage || '← Previous') : null, + pg.page < pg.pages ? a({ href: href(pg.page + 1), class: 'oasis-pager-btn' }, i18n.cnNextPage || 'Next →') : null + ); +}; + +const renderLogWindowNote = () => { + let store = null; + try { store = require('../models/typed_log').requestScope.getStore(); } catch (_) { store = null; } + if (!store || !store.capped || !(store.limit > 0)) return null; + return div({ class: 'log-window-note' }, + a({ href: '/settings#logstream', class: 'eco-tax-chip eco-tax-chip-low log-window-chip', title: i18n.ssbLogStreamDescription }, + span({ class: 'eco-tax-chip-value' }, String(i18n.logWindowChip).replace('{n}', String(store.limit))) + ) + ); +}; + const errorView = ({ title, message, backHref }) => { const heading = title || i18n.errorPageTitle || 'Error'; return exports.template( @@ -181,27 +665,85 @@ const errorView = ({ title, message, backHref }) => { }; exports.errorView = errorView; -const renderSpreadButton = (msgKey, opts) => { +const keepTextView = ({ title, action, fields = [], hidden = [], enctype = null, backHref = null }) => { + const formAttrs = { method: 'POST', action }; + if (enctype) formAttrs.enctype = enctype; + return exports.template( + title || i18n.errorPageTitle || 'Error', + section( + div({ class: 'tags-header' }, + h2(title || ''), + form(formAttrs, + ...hidden.map(h => input({ type: 'hidden', name: h.name, value: String(h.value == null ? '' : h.value) })), + ...fields.map(f => { + const attrs = { name: f.name, id: f.name }; + if (f.maxlength) attrs.maxlength = String(f.maxlength); + if (f.required) attrs.required = true; + const value = String(f.value == null ? '' : f.value); + const control = f.type === 'textarea' + ? textarea({ ...attrs, rows: f.rows || 10 }, value) + : input({ ...attrs, type: 'text', value }); + return [label({ for: f.name }, f.label || f.name), br(), control, br(), br()]; + }).flat(), + button({ type: 'submit', class: 'create-button' }, i18n.keepTextRetry), + ' ', + backHref ? a({ href: backHref, class: 'filter-btn' }, i18n.goBack || 'Go back') : null + ) + ) + ) + ); +}; +exports.keepTextView = keepTextView; + +const confirmView = ({ title, message, action, hidden = [], backHref = null }) => exports.template( + title || i18n.confirmActionTitle, + section( + div({ class: 'tags-header' }, + h2(title || i18n.confirmActionTitle), + message ? p({ class: 'error-page-message' }, String(message)) : null, + form({ method: 'POST', action, class: 'confirm-actions' }, + ...hidden.map(h => input({ type: 'hidden', name: h.name, value: String(h.value == null ? '' : h.value) })), + button({ type: 'submit', class: 'create-button' }, i18n.confirmActionButton), + ' ', + a({ href: backHref || '/', class: 'filter-btn' }, i18n.goBack || 'Go back') + ) + ) + ) +); +exports.confirmView = confirmView; + +exports.renderInlineError = (message, dismissHref) => section({ class: 'inline-error' }, + div({ class: 'tags-header inline-error-box' }, + p({ class: 'error-page-message' }, String(message || '')), + dismissHref ? a({ href: dismissHref, class: 'inline-error-dismiss' }, i18n.errorDismiss || 'OK') : null + ) +).outerHTML; + +const renderVotesSummary = (opinions = {}) => { + const entries = Object.entries(opinions).filter(([, v]) => Number(v) > 0); + if (!entries.length) return null; + entries.sort((a, b) => Number(b[1]) - Number(a[1]) || String(a[0]).localeCompare(String(b[0]))); + return div( + { class: "votes feed-votes-summary" }, + entries.map(([category, count]) => { + const label = i18n['vote' + category.charAt(0).toUpperCase() + category.slice(1)] || category; + return span({ class: "vote-category" }, `${label} ${count}`); + }) + ); +}; +exports.renderVotesSummary = renderVotesSummary; + +const renderSpreadButton = (msgKey, opts, title = null, showCount = true) => { if (!msgKey || typeof msgKey !== 'string' || !msgKey.startsWith('%') || !/\.sha256$/.test(msgKey)) return null; const o = (opts && typeof opts === 'object') ? opts : {}; const voters = Array.isArray(o.voters) ? o.voters : []; const count = typeof o.count === 'number' ? o.count : voters.length; const alreadySpread = o.alreadySpread === true; - const maxNames = 5; - const maxLen = 16; - const lastVoters = voters.slice(-maxNames); - const tooltipNames = lastVoters - .map(v => (v && typeof v === 'object' ? (v.name || v.key || '') : String(v || ''))) - .filter(Boolean) - .map(n => n.slice(0, maxLen)) - .join(', '); - const extra = count > maxNames ? ` +${count - maxNames} ${i18n.spreadMore || 'more'}` : ''; - const tooltip = count > 0 ? `${tooltipNames}${extra}` : (i18n.spreadHint || 'Spread this to your supporters (replicates via your feed).'); return form( { method: 'POST', action: `/spread/${encodeURIComponent(msgKey)}`, class: 'spread-form' }, button( - { type: 'submit', class: alreadySpread ? 'spread-btn spread-btn-on' : 'spread-btn', title: tooltip }, - `🔁 ${count}` + { type: 'submit', class: alreadySpread ? 'btn-singleview btn-spread-on' : 'btn-singleview', title: title || i18n.spreadContent }, + renderIcon('spread'), showCount ? span({ class: 'btn-count' }, String(count)) : null ) ); }; @@ -213,7 +755,16 @@ const aiNavResultsView = ({ query, results }) => { const safeResults = Array.isArray(results) ? results : []; const fmtScore = (s) => { const n = Number(s); - return Number.isFinite(n) ? n.toFixed(2) : '—'; + return Number.isFinite(n) ? `${Math.round(n * 100)}%` : '—'; + }; + const routeLabel = (p) => { + const [base, qs] = String(p || '').split('?'); + let label = decodeURIComponent(base).replace(/^\//, '').replace(/\//g, ' · '); + try { + const filter = new URLSearchParams(qs || '').get('filter'); + if (filter) label += ` · ${filter}`; + } catch (_) {} + return label.toUpperCase(); }; const splitTerms = (desc) => String(desc || '') .split(/[,;]/) @@ -231,7 +782,7 @@ const aiNavResultsView = ({ query, results }) => { safeResults.map(r => div({ class: 'ai-nav-result-card card-section' }, div({ class: 'card-field' }, span({ class: 'card-label' }, `${(i18n.aiNavResultMatch || 'Match').toUpperCase()}: ${fmtScore(r.score)}`), - span({ class: 'card-value' }, a({ href: r.path, class: 'filter-btn' }, r.path)) + span({ class: 'card-value' }, a({ href: r.path, class: 'filter-btn' }, routeLabel(r.path))) ) )) ) @@ -245,39 +796,112 @@ const i18nBase = require("../client/assets/translations/i18n"); let selectedLanguage = "en"; let i18n = {}; Object.assign(i18n, i18nBase[selectedLanguage]); +exports.getLanguage = () => (i18nBase[selectedLanguage] ? selectedLanguage : 'en'); exports.setLanguage = (language) => { + if (language === selectedLanguage) return; selectedLanguage = language; const newLang = i18nBase[selectedLanguage] || i18nBase['en']; Object.keys(i18n).forEach(k => delete i18n[k]); Object.assign(i18n, newLang); + const momentLocale = moment.locale(selectedLanguage === 'zh' ? 'zh-cn' : (i18nBase[selectedLanguage] ? selectedLanguage : 'en')); + if (moment.localeData(momentLocale).postformat('1') !== '1') moment.updateLocale(momentLocale, { preparse: (str) => str, postformat: (str) => str }); }; exports.i18n = i18n; Object.defineProperty(exports, 'selectedLanguage', { get: () => selectedLanguage }); const opinionCategoriesList = require('../backend/opinion_categories'); -const renderOpinionsVoting = (basePath, id, opinions, returnTo, voters) => { +const renderEngagement = (id, opinionsNode, commentsNode) => { + const parts = [opinionsNode, commentsNode].filter(Boolean); + if (!parts.length) return null; + return div({ class: 'card-engage' }, ...parts); +}; +exports.renderEngagement = renderEngagement; + +const renderModuleStats = (total, segments = [], totalLabel = null, opts = {}) => { + if (!(Number(total) > 0)) return null; + const inh = opts.showInhabitants && sharedState.getInhabitantCount ? sharedState.getInhabitantCount() : 0; + const trb = opts.showTribes && sharedState.getTribesCount ? sharedState.getTribesCount() : 0; + const network = [ + inh > 0 ? { label: i18n.inhabitants, count: inh } : null, + trb > 0 ? { label: i18n.tribesTitle, count: trb } : null + ]; + const toNode = s => span(s.labelClass ? span({ class: s.labelClass }, `${s.label}: `) : `${s.label}: `, strong(String(s.count))); + return div({ class: 'module-stats-line' }, + ...network.filter(Boolean).map(toNode), + span(`${totalLabel || i18n.statsTotalLabel}: `, strong(String(total))), + ...segments.filter(s => s && Number(s.count) > 0).map(toNode) + ); +}; +exports.renderModuleStats = renderModuleStats; + +const renderModuleStatsBy = (items, field, specs, totalLabel = null) => { + const arr = Array.isArray(items) ? items : []; + const counts = {}; + for (const it of arr) { const v = typeof field === 'function' ? field(it) : (it && it[field]); counts[v] = (counts[v] || 0) + 1; } + return renderModuleStats(arr.length, specs.map(s => ({ label: s.label, count: counts[s.value] || 0 })), totalLabel); +}; +exports.renderModuleStatsBy = renderModuleStatsBy; + +const moduleIsEmpty = (items, filter, defaultFilter = 'all', q = '') => + (!items || items.length === 0) && + String(filter || defaultFilter).toUpperCase() === String(defaultFilter).toUpperCase() && + !String(q || '').trim(); +exports.moduleIsEmpty = moduleIsEmpty; + +const renderCardMetaRow = (...nodes) => { + const flat = nodes.flat(Infinity).filter(Boolean); + if (!flat.length) return null; + return div({ class: 'card-meta-row' }, ...flat); +}; +exports.renderCardMetaRow = renderCardMetaRow; + +const renderOpinionsVoting = (basePath, id, opinions, returnTo, voters, pathFor = null) => { const ops = opinions || {}; const total = Object.values(ops).reduce((s, n) => s + (Number(n) || 0), 0); const myId = (config.keys && config.keys.id) ? config.keys.id : ''; const alreadyVoted = Array.isArray(voters) && myId ? voters.includes(myId) : false; - return details({ class: 'opinions-voting-collapse' }, - summary({ class: 'opinions-summary' }, - `${i18n.opinionsTitle || 'Opinions'} (${total})`), + const actionFor = (category) => typeof pathFor === 'function' ? pathFor(id, category) : `${basePath}/${encodeURIComponent(id)}/${category}`; + const votingDetails = details({ class: 'opinions-voting-collapse' }, + summary({ class: total > 0 ? 'opinions-summary engage-on' : 'opinions-summary' }, + span({ class: 'opinions-summary-icon' }, 'ꔍ'), + span({ class: 'opinions-summary-count' }, `(${total})`)), div({ class: 'voting-buttons' }, opinionCategoriesList.map((category) => - form({ method: 'POST', action: `${basePath}/${encodeURIComponent(id)}/${category}` }, + form({ method: 'POST', action: actionFor(category) }, returnTo ? input({ type: 'hidden', name: 'returnTo', value: returnTo }) : null, button({ class: alreadyVoted ? 'vote-btn disabled' : 'vote-btn', type: 'submit', ...(alreadyVoted ? { disabled: true } : {}) }, - `${i18n['vote' + category.charAt(0).toUpperCase() + category.slice(1)] || category} [${ops[category] || 0}]`) + `${String(i18n['vote' + category.charAt(0).toUpperCase() + category.slice(1)] || category).toUpperCase()} [${ops[category] || 0}]`) ) ) ), alreadyVoted ? p({ class: 'muted' }, i18n.alreadyVoted) : null ); + return [renderVotesSummary(ops), votingDetails].filter(Boolean); }; exports.renderOpinionsVoting = renderOpinionsVoting; -// markdown +let spreadsModel = null; +const spreadsFor = async (msgId) => { + if (!msgId || typeof msgId !== 'string' || !msgId.startsWith('%')) return 0; + try { + if (!spreadsModel) spreadsModel = require('../models/main_models')({ cooler, isPublic: config.public }).spreads; + const info = await spreadsModel.forMessage(msgId); + return info && Number(info.count) > 0 ? Number(info.count) : 0; + } catch (_) { + return 0; + } +}; + +const renderSpreadEditWarning = async (msgId) => { + const count = await spreadsFor(msgId); + if (!count) return null; + return div({ class: 'error-box spread-edit-warning' }, + p(`${i18n.spreadEditWarning} (${count})`) + ); +}; +exports.renderSpreadEditWarning = renderSpreadEditWarning; + + const markdownUrl = "https://commonmark.org/help/"; const doctypeString = ""; @@ -292,8 +916,37 @@ const nbsp = "\xa0"; const { getConfig } = require('../configs/config-manager.js'); -// menu INIT -const readPkg = () => { +const shortCache = (ttl, fn) => { + let at = 0; + let value; + return () => { + const now = Date.now(); + if (!at || now - at > ttl) { value = fn(); at = now; } + return value; + }; +}; + +const computeAssetVersion = () => { + try { + const base = path.resolve(__dirname, "..", "client", "assets"); + let latest = 0; + for (const rel of ["styles", "themes"]) { + const dir = path.join(base, rel); + for (const f of fs.readdirSync(dir)) { + const t = fs.statSync(path.join(dir, f)).mtimeMs; + if (t > latest) latest = t; + } + } + const v = String(readPkg()?.version || '0'); + return encodeURIComponent(latest ? `${v}-${Math.floor(latest / 1000).toString(36)}` : v); + } catch (_) { + try { return encodeURIComponent(String(readPkg()?.version || '0')); } catch (_) { return '0'; } + } +}; + +const assetVersion = shortCache(5000, () => computeAssetVersion()); + +const readPkgFile = () => { const file = path.resolve(__dirname, "..", "server", "package.json"); try { const txt = fs.readFileSync(file, "utf8"); @@ -303,6 +956,7 @@ const readPkg = () => { return {}; } }; +const readPkg = shortCache(30000, readPkgFile); const renderFooter = () => { const pkg = readPkg(); @@ -314,11 +968,13 @@ const renderFooter = () => { const hcH = sharedState.getCarbonHcH(); const peersOnline = sharedState.getOnlinePeerCount ? sharedState.getOnlinePeerCount() : null; + const syncedPeers = sharedState.getSyncedPeerCount ? sharedState.getSyncedPeerCount() : null; const inboxUnread = sharedState.getInboxUnreadCount ? sharedState.getInboxUnreadCount() : null; const lastSyncTs = sharedState.getLastSyncTs ? sharedState.getLastSyncTs() : null; const lastSyncLabel = lastSyncTs ? moment(lastSyncTs).fromNow() : '–'; const lastActivity = sharedState.getLastActivity ? sharedState.getLastActivity() : null; + if (process.env.OASIS_MOBILE === '1') return require('./mobile_ui').footerLine(syncedPeers, pkgVersion); return div( { class: "oasis-footer" }, div( @@ -346,11 +1002,17 @@ const renderFooter = () => { ]; })(), br(), + span({ class: "oasis-footer-peers" }, + span("Synced-peers: [ "), + a({ href: "/peers" }, String(syncedPeers || 0)), + span(" ]") + ), + br(), span({ class: "oasis-footer-carbon" }, span("HcT: "), - a({ href: "/stats?filter=ALL" }, hcT != null ? String(hcT) : '–'), + a({ href: "/stats?filter=ALL#carbon" }, hcT != null ? String(hcT) : '–'), span(" | HcH: "), - a({ href: "/stats?filter=MINE" }, hcH != null ? String(hcH) : '–') + a({ href: "/stats?filter=MINE#carbon" }, hcH != null ? String(hcH) : '–') ), br(), a( @@ -386,30 +1048,43 @@ const navLink = ({ href, emoji, text, current, class: extraClass }) => ) ); -const customCSS = (filename) => { - const customStyleFile = path.join( - envPaths("oasis", { suffix: "" }).config, - filename - ); - try { - if (fs.existsSync(customStyleFile)) { - return link({ rel: "stylesheet", href: filename }); - } - } catch (error) { - return ""; +const currentNavPath = () => { + let store = null; + try { store = require('../models/typed_log').requestScope.getStore(); } catch (_) { store = null; } + return store && typeof store.path === 'string' ? store.path : ''; +}; + +const firstHref = (node) => { + if (!node || typeof node !== 'object') return ''; + const own = typeof node.href === 'string' && node.href ? node.href : (typeof node.getAttribute === 'function' ? node.getAttribute('href') : null); + if (own) return String(own); + for (const child of node.childNodes || []) { + const found = firstHref(child); + if (found) return found; } + return ''; +}; + +const navGroupHasPath = (items, path) => { + if (!path) return false; + return items.flat(Infinity).some(item => { + const href = firstHref(item).split(/[?#]/)[0]; + if (!href || href === '/') return false; + return path === href || path.startsWith(href + '/'); + }); }; const navGroup = ({ id, emoji, title, defaultOpen = false }, ...items) => { const active = items.filter(Boolean); if (!active.length) return null; + const open = process.env.OASIS_MOBILE !== '1' && (defaultOpen || navGroupHasPath(active, currentNavPath())); return li( { class: "oasis-nav-group" }, input({ type: "checkbox", id: `oasis-nav-group-${id}`, class: "oasis-nav-toggle", - ...(defaultOpen ? { checked: true } : {}) + ...(open ? { checked: true } : {}) }), label( { for: `oasis-nav-group-${id}`, class: "oasis-nav-header" }, @@ -421,72 +1096,36 @@ const navGroup = ({ id, emoji, title, defaultOpen = false }, ...items) => { ); }; -const renderPopularLink = () => { - const popularMod = getConfig().modules.popularMod === "on"; - return popularMod + + + + + + +const renderPollsLink = () => { + const pollsMod = getConfig().modules.pollsMod === "on"; + return pollsMod ? navLink({ - href: "/public/popular/day", - emoji: "⌘", - text: i18n.popular, - class: "popular-link enabled" + href: "/polls", + emoji: "☑", + text: i18n.pollsTitle, + class: "polls-link enabled" }) : ""; }; -const renderTopicsLink = () => { - const topicsMod = getConfig().modules.topicsMod === "on"; - return topicsMod +const renderBlogsLink = () => { + const blogsMod = getConfig().modules.blogsMod === "on"; + return blogsMod ? navLink({ - href: "/public/latest/topics", - emoji: "ϟ", - text: i18n.topics, - class: "topics-link enabled" + href: "/blogs", + emoji: "✦", + text: i18n.blogTitle, + class: "blogs-link enabled" }) : ""; }; -const renderSummariesLink = () => { - const summariesMod = getConfig().modules.summariesMod === "on"; - if (summariesMod) { - return [ - navLink({ - href: "/public/latest/summaries", - emoji: "※", - text: i18n.summaries, - class: "summaries-link enabled" - }) - ]; - } - return ""; -}; - -const renderLatestLink = () => { - const latestMod = getConfig().modules.latestMod === "on"; - return latestMod - ? navLink({ - href: "/public/latest", - emoji: "☄", - text: i18n.latest, - class: "latest-link enabled" - }) - : ""; -}; - -const renderThreadsLink = () => { - const threadsMod = getConfig().modules.threadsMod === "on"; - if (threadsMod) { - return [ - navLink({ - href: "/public/latest/threads", - emoji: "♺", - text: i18n.threads, - class: "threads-link enabled" - }) - ]; - } - return ""; -}; - const renderInvitesLink = () => { const invitesMod = getConfig().modules.invitesMod === "on"; return invitesMod @@ -514,15 +1153,30 @@ const renderWalletLink = () => { return ""; }; -const renderLegacyLink = () => { - const legacyMod = getConfig().modules.legacyMod === "on"; - if (legacyMod) { +const renderDevLink = () => { + const devMod = getConfig().modules.devMod === "on"; + if (devMod) { return [ navLink({ - href: "/legacy", + href: "/dev", + emoji: "⌘", + text: i18n.developer, + class: "dev-link enabled" + }) + ]; + } + return ""; +}; + +const renderBackupLink = () => { + const backupMod = getConfig().modules.backupMod === "on"; + if (backupMod) { + return [ + navLink({ + href: "/backup", emoji: "ꖤ", - text: i18n.legacy, - class: "legacy-link enabled" + text: i18n.backupTitle, + class: "backup-link enabled" }) ]; } @@ -577,7 +1231,7 @@ const renderImagesLink = () => { return [ navLink({ href: "/images", - emoji: "ꕥ", + emoji: "▦", text: i18n.imagesLabel, class: "images-link enabled" }) @@ -586,6 +1240,21 @@ const renderImagesLink = () => { return ""; }; +const renderFilesLink = () => { + const filesMod = getConfig().modules.filesMod === "on"; + if (filesMod) { + return [ + navLink({ + href: "/files", + emoji: "▣", + text: i18n.filesLabel, + class: "files-link enabled" + }) + ]; + } + return ""; +}; + const renderTorrentsLink = () => { const torrentsMod = getConfig().modules.torrentsMod === "on"; if (torrentsMod) { @@ -690,21 +1359,19 @@ const renderTagsLink = () => { : ""; }; -const renderMultiverseLink = () => { - const multiverseMod = getConfig().modules.multiverseMod === "on"; - return multiverseMod - ? navLink({ - href: "/public/latest/extended", - emoji: "∞", - text: i18n.multiverse, - class: "multiverse-link enabled" - }) - : ""; + +const hasMultiverseAccount = () => { + try { + const store = JSON.parse(fs.readFileSync(require('../configs/state-manager').statePath('fediverse-accounts.json'), 'utf8')); + return !!(store && ((store.mastodon && store.mastodon.token) || (store.peertube && store.peertube.token) || (store.telegram && store.telegram.session))); + } catch (_) { + return false; + } }; const renderMastodonLink = () => { const fediverseMod = getConfig().modules.fediverseMod === "on"; - return fediverseMod + return fediverseMod && hasMultiverseAccount() ? navLink({ href: "/fediverse", emoji: "ꗵ", @@ -726,6 +1393,19 @@ const renderMarketLink = () => { : ""; }; +const renderHousingLink = () => { + const housingMod = getConfig().modules.housingMod === "on"; + return housingMod + ? [ + navLink({ + href: "/housing", + emoji: "⌂", + text: i18n.housingTitle + }) + ] + : ""; +}; + const renderJobsLink = () => { const jobsMod = getConfig().modules.jobsMod === "on"; return jobsMod @@ -765,6 +1445,19 @@ const renderProjectsLink = () => { : ""; }; +const renderIndustryLink = () => { + const industryMod = getConfig().modules.industryMod === "on"; + return industryMod + ? [ + navLink({ + href: "/industry", + emoji: "ꖴ", + text: i18n.industryTitle + }) + ] + : ""; +}; + const renderBankingLink = () => { const bankingMod = getConfig().modules.bankingMod === "on"; return bankingMod @@ -796,7 +1489,7 @@ const renderLarpLink = () => { ? [ navLink({ href: "/larp", - emoji: "✦", + emoji: "♜", text: i18n.larpTitle || "L.A.R.P.", class: "larp-link enabled" }) @@ -818,6 +1511,18 @@ const renderParliamentLink = () => { : ""; }; +const renderSchoolLink = () => { + const schoolMod = getConfig().modules.schoolMod === "on"; + return schoolMod + ? navLink({ + href: "/school", + emoji: "ꖂ", + text: i18n.schoolTitle, + class: "school-link enabled" + }) + : ""; +}; + const renderCourtsLink = () => { const courtsMod = getConfig().modules.courtsMod === "on"; return courtsMod @@ -830,6 +1535,78 @@ const renderCourtsLink = () => { : ""; }; +const renderEmergenciesLink = () => { + const emergenciesMod = getConfig().modules.emergenciesMod === "on"; + return emergenciesMod + ? navLink({ + href: "/emergencies", + emoji: "ꕥ", + text: i18n.emergenciesTitle, + class: "emergencies-link enabled" + }) + : ""; +}; + +const renderCampaignsLink = () => { + const campaignsMod = getConfig().modules.campaignsMod === "on"; + return campaignsMod + ? navLink({ + href: "/campaigns", + emoji: "✍\ufe0e", + text: i18n.campaignsTitle, + class: "campaigns-link enabled" + }) + : ""; +}; + +const renderPodcastsLink = () => { + const podcastsMod = getConfig().modules.podcastsMod === "on"; + return podcastsMod + ? navLink({ + href: "/podcasts", + emoji: "ꖜ", + text: i18n.podcastsTitle, + class: "podcasts-link enabled" + }) + : ""; +}; + +const renderLogisticsLink = () => { + const logisticsMod = getConfig().modules.logisticsMod === "on"; + return logisticsMod + ? navLink({ + href: "/logistics", + emoji: "⇄", + text: i18n.logisticsTitle, + class: "logistics-link enabled" + }) + : ""; +}; + +const renderRoomsLink = () => { + const roomsMod = getConfig().modules.roomsMod === "on"; + return roomsMod + ? navLink({ + href: "/rooms", + emoji: "ꘒ", + text: i18n.roomsTitle, + class: "rooms-link enabled" + }) + : ""; +}; + +const renderMailingLink = () => { + const mailingMod = getConfig().modules.mailingMod === "on"; + return mailingMod + ? navLink({ + href: "/mailing", + emoji: "✉", + text: i18n.mailingTitle, + class: "mailing-link enabled" + }) + : ""; +}; + const renderVotationsLink = () => { const votesMod = getConfig().modules.votesMod === "on"; return votesMod @@ -864,7 +1641,7 @@ const renderReportsLink = () => { ? [ navLink({ href: "/reports", - emoji: "ꕥ", + emoji: "⚐", text: i18n.reportsTitle, class: "reports-link enabled" }) @@ -900,6 +1677,20 @@ const renderPadsLink = () => { : ""; }; +const renderWikiLink = () => { + const wikiMod = getConfig().modules.wikiMod === "on"; + return wikiMod + ? [ + navLink({ + href: "/wiki", + emoji: "¶", + text: i18n.wikiTitle, + class: "wiki-link enabled" + }) + ] + : ""; +}; + const renderTransfersLink = () => { const transfersMod = getConfig().modules.transfersMod === "on"; return transfersMod @@ -1073,49 +1864,152 @@ const renderTasksLink = () => { : ""; }; -const renderMobileTopbar = () => div( - { class: "oasis-mobile-topbar" }, - a({ class: "omt-logo", href: "/" }, img({ src: "/assets/images/snh-oasis.jpg", alt: "Oasis" })), - a({ class: "omt-title", href: "/" }, "OASIS"), - a({ class: "omt-avatar", href: "/profile" }, img({ src: (typeof global !== "undefined" && global.__OASIS_SELF_AVATAR__) || "/assets/images/default-avatar.png", alt: i18n.profile || "Profile" })) -); - -const renderBottomBar = () => { - const inboxN = sharedState.getInboxCount(); - const bbLink = (href, ico, label, badge) => a( - { class: "bb-item", href }, - span({ class: "bb-ico" }, ico), - (badge && badge > 0) ? span({ class: "bb-badge" }, String(badge)) : "", - span({ class: "bb-lbl" }, label) - ); - return nav( - { class: "oasis-bottombar", style: "display:none", "aria-label": "Quick actions" }, - bbLink("/search", "ꔅ", i18n.searchTitle || "Buscar"), - bbLink("/inbox", "☂", i18n.inbox || "Inbox", inboxN), - bbLink("/publish", "❂", i18n.publish || "Write"), - bbLink("/activity", "ꔙ", i18n.activityTitle || "Activity"), - div({ class: "bb-pair" }, - bbLink("/peers", "⧖", i18n.peers || "Peers"), - bbLink("/invites", "ꔮ", i18n.invites || "Invites") - ) +// === Oasis UX: topbar + bottombar + hive === +// La implementacion vive en ./hive_views.js para que las reescrituras de +// upstream sobre este fichero no arrastren la interfaz de la rama. +const forkNav = require('./hive_views')({ i18n }); +const renderMobileTopbar = forkNav.renderMobileTopbar; +const renderBottomBar = forkNav.renderBottomBar; +const renderHiveNav = forkNav.renderHiveNav; +exports.PINNABLE_MODULES = forkNav.PINNABLE_MODULES; +exports.LAUNCHER_GROUPS = forkNav.GROUPS; +exports.renderHiveNav = renderHiveNav; +// se resuelve al llamarse, cuando template ya esta definido mas abajo +exports.bottomBarPickerView = () => forkNav.bottomBarPickerView(template); +const renderEmergencyBanner = (featured, opts = {}) => { + if (!featured || !featured.href) return null; + const compact = !!opts.compact; + const cap = compact ? 42 : 80; + const sev = String(featured.severity || "UNVERIFIED"); + const sevLabel = String(i18n[`emergencySeverity${sev.charAt(0) + sev.slice(1).toLowerCase()}`] || sev).toUpperCase(); + const t = String(featured.title || "").trim(); + const label = `${t.length > cap ? t.slice(0, cap) + "…" : t} (${sevLabel} · ${featured.confirmationCount || 0})`; + return div( + { class: (compact ? "ai-suggestion-banner ai-suggestion-inline" : "update-banner ai-suggestion-banner") + ` emergency-banner emergency-banner-${sev.toLowerCase()}` }, + span({ class: "update-banner-icon" }, "🚨"), + compact ? null : span({ class: "update-banner-text" }, i18n.emergencyBannerLabel), + a({ href: featured.href, class: "update-banner-link" }, label), + form({ method: "POST", action: "/emergencies/banner/dismiss", class: "welcome-banner-close" }, + button({ type: "submit", class: "welcome-banner-close-btn" }, "✕")) ); }; const template = (titlePrefix, ...elements) => { + const scope = listScope(); + if (scope && scope.dryRun) return ''; const currentConfig = getConfig(); const theme = currentConfig.themes.current || "Dark-SNH"; - const uxMode = currentConfig.ux?.current === "ainav" ? "ainav" : "blocks"; + const buildAiSuggestion = (compact) => { + try { + const { getConfig } = require('../configs/config-manager.js'); + if (getConfig().ai?.suggestions === false) return null; + } catch (_) {} + let here = ''; + try { here = (require('../models/typed_log').requestScope.getStore() || {}).path || ''; } catch (_) {} + const own = sharedState.getSectionMatch ? sharedState.getSectionMatch(here.split('/')[1] || '') : null; + const suggestion = (own && own.href !== here) ? own : (sharedState.getBestMatch ? sharedState.getBestMatch() : null); + if (!suggestion || !suggestion.href) return null; + if (sharedState.isSuggestionDismissed && sharedState.isSuggestionDismissed(suggestion.href)) return null; + if (sharedState.areSuggestionsQuiet && sharedState.areSuggestionsQuiet()) return null; + const cap = compact ? 52 : 96; + const t = String(suggestion.title || '').trim(); + const label = t.length > cap ? t.slice(0, cap) + '…' : t; + return div( + { class: compact ? "ai-suggestion-banner ai-suggestion-inline" : "update-banner ai-suggestion-banner" }, + span({ class: "update-banner-icon" }, "🤖"), + a({ href: suggestion.href, class: "update-banner-link" }, label), + form( + { method: "POST", action: "/ai/suggestion/dismiss", class: "welcome-banner-close" }, + input({ type: "hidden", name: "href", value: suggestion.href }), + button({ type: "submit", class: "welcome-banner-close-btn" }, "✕") + ) + ); + }; + const bannerAct = (action, label, kls, fields = {}) => form({ method: "POST", action, class: "phone-action-form" }, + ...Object.entries(fields).map(([k, v]) => input({ type: "hidden", name: k, value: String(v) })), + button({ type: "submit", class: kls }, String(label).toUpperCase())); + const bannerCls = () => "update-banner phone-banner"; + const bannerChip = (icon, value) => span({ class: "pm-exposition-chip pm-exposition-whole phone-banner-chip" }, + span({ class: "pm-exposition-icon" }, icon), + span({ class: "pm-exposition-text" }, value)); + const buildRoomBanner = (here) => { + if (config.public || getConfig().modules.roomsMod !== 'on') return null; + const room = sharedState.getPhoneRoom(); + if (room && room.ref) { + const href = `/rooms/${encodeURIComponent(room.ref)}`; + if (here === href) return null; + return div({ class: bannerCls() }, + span({ class: "phone-banner-dot" }), + a({ href, class: "update-banner-link" }, room.title), + bannerChip("👥", `${room.count}/${room.max}`), + room.joinedAt ? bannerChip("◷", liveClock(room.joinedAt)) : null, + room.hand ? bannerChip("✋", String((Array.isArray(room.peers) ? room.peers : []).filter(p => p && p.hand && (Number(p.handSince) || 0) < (Number(room.handSince) || 0)).length + 1)) : null, + bannerAct("/rooms/mute", room.muted ? i18n.phoneUnmute : i18n.phoneMute, "tribe-action-btn", { mute: room.muted ? 0 : 1 }), + bannerAct("/rooms/hand", `✋ ${room.hand ? i18n.roomLowerHand : i18n.roomRaiseHand}`, room.hand ? "tribe-action-btn room-hand-btn-on" : "tribe-action-btn", { hand: room.hand ? 0 : 1 }), + bannerAct("/rooms/leave", i18n.roomLeave, "tribe-action-btn danger-btn") + ); + } + return null; + }; + const buildPhoneBanner = () => { + let here = ''; + try { here = (require('../models/typed_log').requestScope.getStore() || {}).path || ''; } catch (_) {} + const st = phoneOn() ? sharedState.getPhoneCall() : null; + if (!st) return buildRoomBanner(here); + if (here === '/phone') return null; + const nameOf = (id) => nameCache.get(id) || String(id).slice(0, 10); + const name = st.group && Array.isArray(st.peers) ? st.peers.filter(p => p.phase !== 'gone').map(p => nameOf(p.id)).join(', ') : nameOf(st.peer); + const shell = (text, ...rest) => div({ class: bannerCls() }, + span({ class: "phone-banner-dot" }), + a({ href: "/phone", class: "update-banner-link" }, text), + ...rest + ); + if (st.dir === 'in' && st.phase === 'incoming') { + return shell(`${name} ${i18n.phoneIsCalling}`, + bannerAct("/phone/accept", i18n.phoneAnswer, "tribe-action-btn"), + bannerAct("/phone/reject", i18n.phoneReject, "tribe-action-btn danger-btn")); + } + if (st.phase === 'calling') { + return shell(`${st.reached ? i18n.phoneCalling : i18n.phoneConnecting} ${name}`, + bannerChip("◷", liveClock(st.startedAt)), + bannerAct("/phone/hangup", i18n.phoneHangup, "tribe-action-btn danger-btn")); + } + if (st.phase === 'recording' || st.phase === 'recorded') { + return shell(`${i18n.phoneRecording} ${name}`, + st.phase === 'recorded' ? null : bannerChip("●", liveClock(st.recordingStartedAt, { max: PHONE_PAM_MAX_MS })), + bannerAct("/phone/pam/send", i18n.phoneSendPam, "tribe-action-btn"), + bannerAct("/phone/pam/cancel", i18n.phoneCancel, "tribe-action-btn danger-btn")); + } + return shell(name, + bannerChip("◷", liveClock(st.answeredAt || st.startedAt)), + bannerAct("/phone/mute", st.muted ? i18n.phoneUnmute : i18n.phoneMute, "tribe-action-btn", { mute: st.muted ? 0 : 1 }), + bannerAct("/phone/hangup", i18n.phoneHangup, "tribe-action-btn danger-btn")); + }; + const buildEmergencyBanner = (compact) => { + try { + if (getConfig().modules.emergenciesMod !== "on") return null; + const featured = sharedState.getFeaturedEmergency ? sharedState.getFeaturedEmergency() : null; + if (!featured || !featured.id) return null; + if (sharedState.getDismissedEmergency && sharedState.getDismissedEmergency() === featured.id) return null; + return renderEmergencyBanner(featured, { compact }); + } catch (_) { return null; } + }; + const uxMode = currentConfig.ux?.current === "ainav" ? "ainav" : currentConfig.ux?.current === "chats" ? "chats" : currentConfig.ux?.current === "feed" ? "feed" : currentConfig.ux?.current === "phone" ? "phone" : currentConfig.ux?.current === "rooms" ? "rooms" : "blocks"; const themeLink = link({ rel: "stylesheet", - href: `/assets/themes/${theme}.css` + href: `/assets/themes/${theme}.css?v=${assetVersion()}` }); const nodes = html( - { lang: "en" }, + { lang: i18nBase[selectedLanguage] ? selectedLanguage : "en" }, head( title(titlePrefix, " | Oasis"), - link({ rel: "stylesheet", href: "/assets/styles/style.css" }), - themeLink, - link({ rel: "stylesheet", href: "/assets/styles/mobile.css", ...(process.env.OASIS_MOBILE === '1' ? {} : { media: "(max-width: 768px)" }) }), + link({ rel: "stylesheet", href: `/assets/styles/style.css?v=${assetVersion()}` }), + // movil: el tema va DESPUES de mobile.css para ganar por cascada sin !important; + // escritorio: orden original de epsylon + ...(process.env.OASIS_MOBILE === '1' + ? [ link({ rel: "stylesheet", href: `/assets/styles/mobile.css?v=${assetVersion()}` }), acampadaBase(theme), themeLink, acampadaLink(theme) ] + : [ themeLink, link({ rel: "stylesheet", href: `/assets/styles/mobile.css?v=${assetVersion()}`, media: "(max-width: 768px)" }) ]), + process.env.OASIS_MOBILE === '1' ? link({ rel: "stylesheet", href: `/assets/styles/mobile_ux.css?v=${assetVersion()}` }) : null, link({ rel: "icon", href: "/assets/images/favicon.svg" }), meta({ charset: "utf-8" }), meta({ name: "description", content: i18n.oasisDescription }), @@ -1123,17 +2017,21 @@ const template = (titlePrefix, ...elements) => { name: "viewport", content: toAttributes({ width: "device-width", - "initial-scale": 1, - "viewport-fit": "cover" + "initial-scale": 1 }) }) ), body( - input({ type: "checkbox", id: "nav-left-toggle", class: "oasis-nav-side-toggle" }), - input({ type: "checkbox", id: "nav-right-toggle", class: "oasis-nav-side-toggle" }), + ...(uxMode === "blocks" ? [ + input({ type: "radio", name: "oasis-nav-side", id: "nav-none-toggle", class: "oasis-nav-side-toggle", checked: true }), + input({ type: "radio", name: "oasis-nav-side", id: "nav-left-toggle", class: "oasis-nav-side-toggle" }), + input({ type: "radio", name: "oasis-nav-side", id: "nav-right-toggle", class: "oasis-nav-side-toggle" }) + ] : []), div( { class: uxMode === "ainav" ? "header ainav-only" : "header" }, renderMobileTopbar(), + // hive (hexagonos) SOLO en la home; en el resto de paginas el top queda limpio + ((typeof global !== "undefined" && global.__OASIS_SHOW_HIVE__) ? renderHiveNav() : ""), div( { class: "top-bar-left" }, a( @@ -1144,52 +2042,75 @@ const template = (titlePrefix, ...elements) => { alt: "Oasis Logo" }) ), + (process.env.OASIS_MOBILE === '1' && uxMode === "blocks" && phoneOn()) ? (() => { + const dnd = !!(getConfig().phone || {}).dnd; + return form({ method: 'POST', action: '/phone/dnd', class: 'oasis-phone-state' }, + input({ type: 'hidden', name: 'dnd', value: dnd ? '0' : '1' }), + button({ type: 'submit', class: `oasis-phone-state-btn pm-exposition-chip pm-exposition-${dnd ? 'hidden' : 'mutuals'}`, title: String(dnd ? i18n.phoneDnd : i18n.phoneOpen), 'aria-label': String(dnd ? i18n.phoneDnd : i18n.phoneOpen) }, + span({ class: 'pm-exposition-icon' }, dnd ? '⊘' : '✆') + ) + ); + })() : null, uxMode === "ainav" ? nav( ul( (() => { - const inboxCount = sharedState.getInboxCount(); - const badge = inboxCount > 0 ? span({ class: 'inbox-badge' }, String(inboxCount)) : ''; return li( a({ href: "/inbox" }, - span({ class: "emoji" }, "☂"), nbsp, i18n.inbox, badge + span({ class: "emoji" }, "☂"), nbsp, i18n.inbox, ...inboxBadges() ) ); })(), + renderPhoneTopLink(), navLink({ href: "/settings", emoji: "⚙", text: i18n.settings }), navLink({ href: "/invites", emoji: "ꔮ", text: i18n.invites }) ) ) : nav( ul( (() => { - const inboxCount = sharedState.getInboxCount(); - const badge = inboxCount > 0 ? span({ class: 'inbox-badge' }, String(inboxCount)) : ''; + const mentionsTotal = sharedState.getMentionsTotal ? sharedState.getMentionsTotal() : 0; + if (!(mentionsTotal > 0) && process.env.OASIS_MOBILE !== '1') return null; + const mentionsCount = sharedState.getMentionsCount(); + const badge = mentionsCount > 0 ? span({ class: 'inbox-badge' }, String(mentionsCount)) : ''; return li( - a({ href: "/inbox" }, - span({ class: "emoji" }, "☂"), nbsp, i18n.inbox, badge + a({ href: "/mentions" }, + span({ class: "emoji" }, "✺"), nbsp, i18n.mentions, badge ) ); })(), - navLink({ - href: "/pm", - emoji: "ꕕ", - text: i18n.privateMessage - }), - navLink({ href: "/publish", emoji: "❂", text: i18n.publish }) + (() => { + return li( + a({ href: "/inbox" }, + span({ class: "emoji" }, "☂"), nbsp, i18n.inbox, ...inboxBadges() + ) + ); + })(), + renderPhoneTopLink(), + (uxMode === "chats" || uxMode === "feed" || uxMode === "phone" || uxMode === "rooms") + ? navLink({ href: "/settings", emoji: "⚙", text: i18n.settings }) + : null ) ) ), - div( + uxMode === "blocks" ? div( { class: "oasis-mobile-menus" }, - label({ for: "nav-left-toggle", class: "oasis-menu-btn" }, - span({ class: "oasis-menu-ico" }, "☰"), span(i18n.menuPersonal || "Personal")), - label({ for: "nav-right-toggle", class: "oasis-menu-btn" }, - span({ class: "oasis-menu-ico" }, "☰"), span(i18n.menuCommunity || "Community")) - ), + span({ class: "oasis-menu-slot oasis-menu-slot-left" }, + label({ for: "nav-left-toggle", class: "oasis-menu-btn oasis-menu-open" }, + span({ class: "oasis-menu-ico" }, "☰"), span(i18n.menuPersonal || "Personal")), + label({ for: "nav-none-toggle", class: "oasis-menu-btn oasis-menu-close" }, + span({ class: "oasis-menu-ico" }, "☰"), span(i18n.menuPersonal || "Personal"))), + span({ class: "oasis-menu-slot oasis-menu-slot-right" }, + label({ for: "nav-right-toggle", class: "oasis-menu-btn oasis-menu-open" }, + span({ class: "oasis-menu-ico" }, "☰"), span(i18n.menuNetwork || "Community")), + label({ for: "nav-none-toggle", class: "oasis-menu-btn oasis-menu-close" }, + span({ class: "oasis-menu-ico" }, "☰"), span(i18n.menuNetwork || "Community"))) + ) : null, (() => { const aiNavOn = getConfig().modules.aiNavMod === 'on'; if (!aiNavOn && uxMode !== 'ainav') return null; return div( { class: uxMode === 'ainav' ? "top-bar-center top-bar-center-ainav" : "top-bar-center" }, + div({ class: 'ai-ask-row' }, + nav({ class: 'ai-ask-nav' }, ul(...(renderAILink() || []))), form( { method: 'POST', action: '/ai/ask', class: 'ai-ask-form' }, input({ @@ -1202,7 +2123,10 @@ const template = (titlePrefix, ...elements) => { autofocus: uxMode === 'ainav' ? 'autofocus' : undefined }), button({ type: 'submit', class: 'ai-ask-btn' }, '➤') - ) + ), + buildAiSuggestion(true) + ), + buildEmergencyBanner(true) ); })(), uxMode === "ainav" ? div( @@ -1214,32 +2138,69 @@ const template = (titlePrefix, ...elements) => { navLink({ href: "/peers", emoji: "⧖", text: i18n.peers }) ) ) + ) : (uxMode === "chats" || uxMode === "feed" || uxMode === "phone" || uxMode === "rooms") ? div( + { class: "top-bar-right" }, + nav( + ul( + navLink({ href: "/activity", emoji: "ꔙ", text: i18n.activityTitle }), + renderTrendingLink(), + navLink({ href: "/search", emoji: "⌕", text: i18n.searchTitle }) + ) + ) ) : div( { class: "top-bar-right" }, nav( ul( - navLink({ href: "/search", emoji: "ꔅ", text: i18n.searchTitle }), - renderGraphosLink(), - navLink({ href: "/peers", emoji: "⧖", text: i18n.peers }) + ...((getConfig().modules.aiNavMod === 'on' || uxMode === 'ainav') ? [] : (renderAILink() || [])), + navLink({ href: "/data", emoji: "⚯", text: i18n.dataTitle }), + renderTagsLink(), + navLink({ href: "/search", emoji: "⌕", text: i18n.searchTitle }), + process.env.OASIS_MOBILE === '1' ? navLink({ href: "/settings", emoji: "⚙", text: i18n.settings }) : null ) ) ) ), (() => { - const updateFlagPath = path.join(__dirname, '../server/.update_required'); - if (fs.existsSync(updateFlagPath)) { + const updateInfo = (() => { try { return require('../backend/updater').readUpdateInfo(); } catch (_) { return null; } })(); + if (updateInfo) { return div( { class: "update-banner" }, - span({ class: "update-banner-icon" }, "⟳"), - span({ class: "update-banner-text" }, i18n.updateBannerText), + span({ class: "update-banner-icon" }, "🛠️"), + span({ class: "update-banner-text" }, updateInfo.version ? `${String(i18n.updateBannerText).replace(/[.。।!]\s*$/, '')}: v${updateInfo.version}` : i18n.updateBannerText), a({ href: "/settings", class: "update-banner-link" }, i18n.updateBannerAction) ); } return null; })(), + (() => { + try { + const onboarding = require('../models/onboarding_model'); + if (!onboarding.bannerVisible(config && config.path)) return null; + let here = ''; + try { here = (require('../models/typed_log').requestScope.getStore() || {}).path || ''; } catch (_) {} + if (here === '/welcome' || here.startsWith('/welcome/')) return null; + const mobileUi = require('./mobile_ui'); + if (mobileUi.isMobile() && !mobileUi.welcomeBannerHere(here)) return null; + return div( + { class: "update-banner welcome-banner" }, + span({ class: "update-banner-icon" }, "🌴"), + span({ class: "update-banner-text" }, mobileUi.isMobile() ? mobileUi.welcomeBannerText() : i18n.welcomeBannerText), + a({ href: "/welcome", class: "create-button welcome-banner-start" }, mobileUi.isMobile() ? mobileUi.welcomeBannerAction() : i18n.welcomeBannerAction), + form( + { method: "POST", action: "/welcome/dismiss", class: "welcome-banner-close" }, + button({ type: "submit", class: "welcome-banner-close-btn" }, "✕") + ) + ); + } catch (_) { + return null; + } + })(), + (getConfig().modules.aiNavMod === 'on' || uxMode === 'ainav') ? null : buildAiSuggestion(false), + buildPhoneBanner(), + buildEmergencyBanner(false), div( - { class: uxMode === "ainav" ? "main-content ainav-only" : "main-content" }, - uxMode === "ainav" ? null : div( + { class: uxMode === "ainav" ? "main-content ainav-only" : uxMode === "chats" ? "main-content chatsux-only" : uxMode === "feed" || uxMode === "phone" || uxMode === "rooms" ? "main-content chatsux-only" : "main-content" }, + uxMode !== "blocks" ? null : div( { class: "sidebar-left" }, nav( ul( @@ -1262,7 +2223,7 @@ const template = (titlePrefix, ...elements) => { renderAgendaLink(), renderFavoritesLink(), renderLogsLink(), - renderWalletLink(), + renderInvitesLink(), navLink({ href: "/modules", emoji: "ꗣ", @@ -1285,10 +2246,29 @@ const template = (titlePrefix, ...elements) => { emoji: "ꖘ", text: i18n.inhabitantsLabel }), - renderTribesLink(), - renderLarpLink(), + renderEmergenciesLink(), + renderSchoolLink(), renderParliamentLink(), - renderCourtsLink() + renderCourtsLink(), + renderLarpLink(), + renderTribesLink() + ), + navGroup( + { + id: "economy", + emoji: "¤", + title: i18n.menuEconomy + }, + renderWalletLink(), + renderBankingLink(), + renderMarketLink(), + renderShopsLink(), + renderIndustryLink(), + renderLogisticsLink(), + renderProjectsLink(), + renderJobsLink(), + renderHousingLink(), + renderTransfersLink() ), navGroup( { @@ -1297,23 +2277,13 @@ const template = (titlePrefix, ...elements) => { title: i18n.menuOffice }, renderVotationsLink(), + renderPollsLink(), + renderCampaignsLink(), renderEventsLink(), renderCalendarsLink(), renderTasksLink(), - renderReportsLink() - ), - navGroup( - { - id: "economy", - emoji: "¤", - title: i18n.menuEconomy - }, - renderBankingLink(), - renderMarketLink(), - renderProjectsLink(), - renderJobsLink(), - renderShopsLink(), - renderTransfersLink() + renderReportsLink(), + renderMailingLink() ), navGroup( { @@ -1321,26 +2291,26 @@ const template = (titlePrefix, ...elements) => { emoji: "⚒", title: i18n.menuTools }, - renderAILink(), - navLink({ + process.env.OASIS_MOBILE === '1' ? null : navLink({ href: "/blockexplorer", emoji: "ꖸ", text: i18n.blockchain }), + renderGraphosLink(), + navLink({ href: "/peers", emoji: "⧖", text: i18n.peers }), + renderDevLink(), renderCipherLink(), - renderInvitesLink(), - renderLegacyLink(), navLink({ href: "/stats", emoji: "ꕷ", text: i18n.statistics - }) + }), + renderBackupLink() ) ) ) ), - main({ id: "content", class: "main-column" }, elements), - uxMode === "ainav" ? null : div( + uxMode !== "blocks" ? null : div( { class: "sidebar-right" }, nav( ul( @@ -1355,42 +2325,16 @@ const template = (titlePrefix, ...elements) => { emoji: "ꔙ", text: i18n.activityTitle }), - renderTagsLink(), renderTrendingLink(), renderOpinionsLink(), - renderPadsLink(), - renderForumLink(), - renderMapsLink(), - renderChatsLink() - ), - navGroup( - { - id: "blogs", - emoji: "✦", - title: i18n.menuBlogs - }, - navLink({ - href: "/mentions", - emoji: "✺", - text: i18n.mentions - }), - renderLatestLink(), - renderThreadsLink(), - renderTopicsLink(), - renderSummariesLink(), - renderPopularLink(), - renderMultiverseLink() - ), - navGroup( - { - id: "creative", - emoji: "✎", - title: i18n.menuCreative - }, renderFeedLink(), - renderGamesLink(), - renderPixeliaLink(), - renderMelodyLink() + renderBlogsLink(), + renderForumLink(), + renderPadsLink(), + renderWikiLink(), + renderMapsLink(), + renderRoomsLink(), + renderChatsLink() ), navGroup( { @@ -1402,8 +2346,20 @@ const template = (titlePrefix, ...elements) => { renderBookmarksLink(), renderDocsLink(), renderImagesLink(), + renderFilesLink(), renderTorrentsLink(), - renderVideosLink() + renderVideosLink(), + renderPodcastsLink() + ), + navGroup( + { + id: "creative", + emoji: "✎", + title: i18n.menuCreative + }, + renderGamesLink(), + renderPixeliaLink(), + renderMelodyLink() ), navGroup( { @@ -1415,16 +2371,15 @@ const template = (titlePrefix, ...elements) => { ) ) ) - ) + ), + main({ id: "content", class: "main-column" }, elements, renderListPager(), renderLogWindowNote(), renderMobileCounters()) ), renderBottomBar(), - script({ src: "/js/oasis-mobile-nav.js", defer: "defer" }), renderFooter() ) ); return doctypeString + nodes.outerHTML; }; -// menu END exports.template = template; @@ -1433,12 +2388,16 @@ exports.ainavHomeView = ({ recentTags = [] } = {}) => { const theme = currentConfig.themes.current || "Dark-SNH"; const placeholder = i18n.aiNavPlaceholder || 'Where do you want to go?'; const nodes = html( - { lang: "en" }, + { lang: i18nBase[selectedLanguage] ? selectedLanguage : "en" }, head( title(placeholder, " | Oasis"), - link({ rel: "stylesheet", href: "/assets/styles/style.css" }), - link({ rel: "stylesheet", href: `/assets/themes/${theme}.css` }), - link({ rel: "stylesheet", href: "/assets/styles/mobile.css", ...(process.env.OASIS_MOBILE === '1' ? {} : { media: "(max-width: 768px)" }) }), + link({ rel: "stylesheet", href: `/assets/styles/style.css?v=${assetVersion()}` }), + // movil: el tema va DESPUES de mobile.css para ganar por cascada sin !important; + // escritorio: orden original de epsylon + ...(process.env.OASIS_MOBILE === '1' + ? [ link({ rel: "stylesheet", href: `/assets/styles/mobile.css?v=${assetVersion()}` }), acampadaBase(theme), link({ rel: "stylesheet", href: `/assets/themes/${theme}.css?v=${assetVersion()}` }), acampadaLink(theme) ] + : [ link({ rel: "stylesheet", href: `/assets/themes/${theme}.css?v=${assetVersion()}` }), link({ rel: "stylesheet", href: `/assets/styles/mobile.css?v=${assetVersion()}`, media: "(max-width: 768px)" }) ]), + process.env.OASIS_MOBILE === '1' ? link({ rel: "stylesheet", href: `/assets/styles/mobile_ux.css?v=${assetVersion()}` }) : null, link({ rel: "icon", href: "/assets/images/favicon.svg" }), meta({ charset: "utf-8" }), meta({ name: "description", content: i18n.oasisDescription }), @@ -1451,14 +2410,13 @@ exports.ainavHomeView = ({ recentTags = [] } = {}) => { nav( ul( (() => { - const inboxCount = sharedState.getInboxCount(); - const badge = inboxCount > 0 ? span({ class: 'inbox-badge' }, String(inboxCount)) : ''; return li( a({ href: "/inbox" }, - span({ class: "emoji" }, "☂"), nbsp, i18n.inbox, badge + span({ class: "emoji" }, "☂"), nbsp, i18n.inbox, ...inboxBadges() ) ); })(), + renderPhoneTopLink(), navLink({ href: "/settings", emoji: "⚙", text: i18n.settings }), navLink({ href: "/invites", emoji: "ꔮ", text: i18n.invites }) ) @@ -1838,7 +2796,7 @@ const post = ({ msg, aside = false, preview = false, spreadInfo = null }) => { const u = safeStr(c.url); if (u) { nodes.push( - renderField((i18n.url || 'URL') + ':', a({ href: u, target: '_blank', rel: 'noopener noreferrer' }, u)) + renderField((i18n.url || 'URL') + ':', a({ href: safeExternalHref(u), target: '_blank', rel: 'noopener noreferrer' }, u)) ); } } else if (t === 'image') { @@ -1846,7 +2804,7 @@ const post = ({ msg, aside = false, preview = false, spreadInfo = null }) => { if (u && isMsgId(u)) { nodes.push( div({ class: 'card-field card-field-mt' }, - img({ src: `/blob/${encodeURIComponent(u)}`, class: 'feed-image img-content' }) + img({ loading: 'lazy', src: `/blob/${encodeURIComponent(u)}`, class: 'feed-image img-content' }) ) ); } @@ -1864,7 +2822,7 @@ const post = ({ msg, aside = false, preview = false, spreadInfo = null }) => { if (u && isMsgId(u)) { nodes.push( div({ class: 'card-field card-field-mt' }, - videoHyperaxe({ controls: true, src: `/blob/${encodeURIComponent(u)}` }) + videoHyperaxe({ controls: true, src: `/blob/${encodeURIComponent(u)}`, preload: 'metadata' }) ) ); } @@ -1933,7 +2891,7 @@ const post = ({ msg, aside = false, preview = false, spreadInfo = null }) => { details( summary(i18n.viewJson || 'View JSON'), pre({ - innerHTML: highlightJs.highlight( + [RAW_HTML]: highlightJs.highlight( JSON.stringify(msg, null, 2), { language: "json", ignoreIllegals: true } ).value, @@ -1948,11 +2906,20 @@ const post = ({ msg, aside = false, preview = false, spreadInfo = null }) => { (u) => `${u}` ); } - articleElement = article({ class: "content", innerHTML: sanitizeHtml(html) }); + articleElement = article({ class: "content", [RAW_HTML]: sanitizeHtml(html) }); } else { articleElement = article( { class: "content" }, - p({ class: "post-text" }, ...renderUrl(rawText)) + p({ class: "post-text" }, ...renderStyledText(rawText)) + ); + } + const postTags = String(content.type || '') === 'post' && Array.isArray(content.tags) ? content.tags.filter(Boolean) : []; + if (postTags.length) { + articleElement = div( + articleElement, + div({ class: 'card-tags card-tags-mt' }, + ...postTags.map(tag => a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: 'tag-link' }, `#${tag}`)) + ) ); } @@ -1981,10 +2948,6 @@ const post = ({ msg, aside = false, preview = false, spreadInfo = null }) => { const fallbackVoted = !!msg.value?.meta?.voted; const fallbackVoteCount = msg.value?.meta?.votes?.length || 0; - const fallbackVoteNames = (msg.value?.meta?.votes || []) - .map((person) => person.name) - .filter(Boolean); - const spreadInfoObj = (spreadInfo && typeof spreadInfo === 'object') ? spreadInfo : null; const spreadCount = spreadInfoObj && typeof spreadInfoObj.count === 'number' ? spreadInfoObj.count @@ -1992,22 +2955,6 @@ const post = ({ msg, aside = false, preview = false, spreadInfo = null }) => { const alreadySpread = spreadInfoObj && typeof spreadInfoObj.alreadySpread === 'boolean' ? spreadInfoObj.alreadySpread : fallbackVoted; - const spreadVoters = (spreadInfoObj && Array.isArray(spreadInfoObj.voters)) - ? spreadInfoObj.voters - .map(v => (v && typeof v === 'object') ? (v.name || v.key || '') : String(v || '')) - .filter(Boolean) - : fallbackVoteNames; - - const maxSpreadNameLength = 16; - const maxSpreadNames = 16; - const spreadByNames = spreadVoters - .slice(0, maxSpreadNames) - .map((n) => String(n).slice(0, maxSpreadNameLength)) - .join(", "); - const additionalSpreadsMessage = - spreadCount > maxSpreadNames ? `+${spreadCount - maxSpreadNames} more` : ``; - const spreadByMessage = - spreadCount > 0 ? `${spreadByNames} ${additionalSpreadsMessage}`.trim() : (i18n.spreadHint || 'Spread this to your supporters (replicates via your feed).'); const spreadButtonClass = alreadySpread ? 'liked' : null; const messageClasses = ["post"]; @@ -2064,7 +3011,6 @@ const post = ({ msg, aside = false, preview = false, spreadInfo = null }) => { ), span( { class: "created-at" }, - `${i18n.createdBy} `, userLink(authorIdForName, msg.value?.meta?.author?.name), ` | ${timeAbsolute} | ${i18n.sendTime} `, a({ href: url.link }, timeAgo) @@ -2082,7 +3028,7 @@ const post = ({ msg, aside = false, preview = false, spreadInfo = null }) => { { type: "submit", class: spreadButtonClass, - title: spreadByMessage, + title: i18n.spreadContent, }, `☉ ${spreadCount}` ) @@ -2113,6 +3059,7 @@ exports.editProfileView = ({ name, description, visibilityPrefs = {}, feedId = ' ubi: visibilityPrefs.ubi === true, wallet: visibilityPrefs.wallet === true, clearnetShops: visibilityPrefs.clearnetShops === true, + clearnetSchool: visibilityPrefs.clearnetSchool === true, clearnetJobs: visibilityPrefs.clearnetJobs === true, clearnetEvents: visibilityPrefs.clearnetEvents === true, clearnetProjects: visibilityPrefs.clearnetProjects === true, @@ -2122,7 +3069,12 @@ exports.editProfileView = ({ name, description, visibilityPrefs = {}, feedId = ' clearnetImages: visibilityPrefs.clearnetImages === true, clearnetDocuments: visibilityPrefs.clearnetDocuments === true, clearnetTorrents: visibilityPrefs.clearnetTorrents === true, + clearnetFiles: visibilityPrefs.clearnetFiles === true, clearnetBookmarks: visibilityPrefs.clearnetBookmarks === true, + clearnetPodcasts: visibilityPrefs.clearnetPodcasts === true, + clearnetMarket: visibilityPrefs.clearnetMarket === true, + clearnetFeed: visibilityPrefs.clearnetFeed === true, + clearnetWiki: visibilityPrefs.clearnetWiki === true, profileShops: visibilityPrefs.profileShops === true, profileJobs: visibilityPrefs.profileJobs === true, profileEvents: visibilityPrefs.profileEvents === true, @@ -2133,14 +3085,23 @@ exports.editProfileView = ({ name, description, visibilityPrefs = {}, feedId = ' profileImages: visibilityPrefs.profileImages === true, profileDocuments: visibilityPrefs.profileDocuments === true, profileTorrents: visibilityPrefs.profileTorrents === true, + profileFiles: visibilityPrefs.profileFiles === true, profileBookmarks: visibilityPrefs.profileBookmarks === true, + profilePodcasts: visibilityPrefs.profilePodcasts === true, + profileSchool: visibilityPrefs.profileSchool === true, + profileMarket: visibilityPrefs.profileMarket === true, + profileFeed: visibilityPrefs.profileFeed === true, + profileWiki: visibilityPrefs.profileWiki === true, ecoTax: visibilityPrefs.ecoTax !== false, larpSign: visibilityPrefs.larpSign === true, fediverse: visibilityPrefs.fediverse === true, - gpg: visibilityPrefs.gpg !== false + gpg: visibilityPrefs.gpg === true }; const fediverseHandleValue = typeof visibilityPrefs.fediverseHandle === 'string' ? visibilityPrefs.fediverseHandle : ''; - prefs.clearnet = prefs.clearnetShops || prefs.clearnetJobs || prefs.clearnetEvents || prefs.clearnetProjects || prefs.clearnetPosts || prefs.clearnetAudios || prefs.clearnetVideos || prefs.clearnetImages || prefs.clearnetDocuments || prefs.clearnetTorrents || prefs.clearnetBookmarks; + prefs.clearnet = prefs.clearnetShops || prefs.clearnetSchool || prefs.clearnetJobs || prefs.clearnetEvents || prefs.clearnetProjects || prefs.clearnetPosts || prefs.clearnetAudios || prefs.clearnetVideos || prefs.clearnetImages || prefs.clearnetDocuments || prefs.clearnetTorrents || prefs.clearnetFiles || prefs.clearnetBookmarks || prefs.clearnetPodcasts || prefs.clearnetMarket || prefs.clearnetFeed || prefs.clearnetWiki; + const pillRows = (entries) => div({ class: "pref-pill-row" }, + ...entries.slice().sort((a, b) => String(a[1]).localeCompare(String(b[1]))).map(([key, labelText]) => togglePill(key, labelText)) + ); const togglePill = (key, labelText, forced = false) => label( { class: forced ? "pref-pill pref-pill-forced" : "pref-pill", for: forced ? undefined : `vis_${key}`, title: forced ? (i18n.profileDeviceLockedHint || 'On mobile devices this sensor is mandatory and cannot be disabled.') : undefined }, forced ? input({ type: "hidden", name: `vis_${key}`, value: "1" }) : null, @@ -2187,7 +3148,7 @@ exports.editProfileView = ({ name, description, visibilityPrefs = {}, feedId = ' div({ class: "gpg-edit-row" }, input({ type: "file", name: "gpgKey", accept: ".asc,.gpg,.pgp,application/pgp-keys,text/plain" }), gpgFingerprint - ? button({ type: "submit", formaction: "/profile/gpg/remove", formenctype: "application/x-www-form-urlencoded", formnovalidate: true, class: "gpg-remove-btn" }, + ? button({ type: "submit", formaction: "/profile/gpg/remove", attrs: { formenctype: "application/x-www-form-urlencoded", formnovalidate: "formnovalidate" }, class: "gpg-remove-btn" }, (i18n.profileGpgRemove || 'Remove') + ' (' + String(gpgFingerprint).slice(-8).toUpperCase() + ')' ) : null @@ -2195,64 +3156,51 @@ exports.editProfileView = ({ name, description, visibilityPrefs = {}, feedId = ' ), br(), br(), div({ class: "prefs-card" }, - div({ class: "tags-header" }, + div({ class: "tags-header module-header-line" }, h2(i18n.profileContentSectionTitle || 'Avatar Content'), p({ class: "prefs-help" }, i18n.profileContentHelp || 'Choose which of your modules will be displayed on your profile.') ), - div({ class: "pref-pill-row" }, - togglePill('profileShops', i18n.profileClearnetShopsLabel || 'Shops'), - togglePill('profileJobs', i18n.profileClearnetJobsLabel || 'Jobs'), - togglePill('profileEvents', i18n.profileClearnetEventsLabel || 'Events'), - togglePill('profileProjects', i18n.profileClearnetProjectsLabel || 'Projects'), - togglePill('profilePosts', i18n.profileClearnetPostsLabel || 'Blogs'), - togglePill('profileAudios', i18n.profileClearnetAudiosLabel || 'Audios'), - togglePill('profileVideos', i18n.profileClearnetVideosLabel || 'Videos'), - togglePill('profileImages', i18n.profileClearnetImagesLabel || 'Images'), - togglePill('profileDocuments', i18n.profileClearnetDocumentsLabel || 'Documents'), - togglePill('profileTorrents', i18n.profileClearnetTorrentsLabel || 'Torrents'), - togglePill('profileBookmarks', i18n.profileClearnetBookmarksLabel || 'Bookmarks') - ) + pillRows([ + ['profileAudios', i18n.profileClearnetAudiosLabel || 'Audios'], + ['profilePosts', i18n.profileClearnetPostsLabel || 'Blogs'], + ['profileBookmarks', i18n.profileClearnetBookmarksLabel || 'Bookmarks'], + ['profileDocuments', i18n.profileClearnetDocumentsLabel || 'Documents'], + ['profileEvents', i18n.profileClearnetEventsLabel || 'Events'], + ['profileFeed', i18n.profileClearnetFeedLabel || 'Feed'], + ['profileImages', i18n.profileClearnetImagesLabel || 'Images'], + ['profileJobs', i18n.profileClearnetJobsLabel || 'Jobs'], + ['profileMarket', i18n.profileClearnetMarketLabel || 'Market'], + ['profilePodcasts', i18n.profileClearnetPodcastsLabel || 'Podcasts'], + ['profileProjects', i18n.profileClearnetProjectsLabel || 'Projects'], + ['profileSchool', i18n.profileClearnetSchoolLabel || 'School'], + ['profileShops', i18n.profileClearnetShopsLabel || 'Shops'], + ['profileTorrents', i18n.profileClearnetTorrentsLabel || 'Torrents'], + ['profileFiles', i18n.profileClearnetFilesLabel || 'Files'], + ['profileVideos', i18n.profileClearnetVideosLabel || 'Videos'], + ['profileWiki', i18n.profileClearnetWikiLabel || 'Wikis'] + ]) ), br(), div({ class: "prefs-card" }, - div({ class: "tags-header" }, + div({ class: "tags-header module-header-line" }, h2(i18n.profileSensorsSectionTitle || 'Sensors'), p({ class: "prefs-help" }, i18n.profileSensorsHelp || 'Optional metrics shown on your profile.') ), - div({ class: "pref-pill-row" }, - togglePill('karma', i18n.profileVisibilityKarma || 'KARMA Scoring'), - togglePill('activity', i18n.profileVisibilityActivity || 'Activity Level'), - togglePill('fediverse', i18n.profileVisibilityFediverse || 'Fediverse'), - togglePill('device', i18n.profileVisibilityDevice || 'Device', process.env.OASIS_MOBILE === '1'), - togglePill('larpSign', i18n.profileVisibilityLarpSign || 'L.A.R.P. Sign'), - togglePill('gpg', i18n.profileVisibilityGpg || 'GPG Key'), - togglePill('wallet', i18n.profileVisibilityWallet || 'ECOIN Wallet'), - togglePill('ubi', i18n.profileVisibilityUbi || 'UBI'), - togglePill('ecoTax', i18n.profileVisibilityEcoTax || 'ECO Tax') - ), + pillRows([ + ['activity', i18n.profileVisibilityActivity || 'Activity Level'], + ['ecoTax', i18n.profileVisibilityEcoTax || 'ECO Tax'], + ['wallet', i18n.profileVisibilityWallet || 'ECOIN Wallet'], + ['gpg', i18n.profileVisibilityGpg || 'GPG Key'], + ['karma', i18n.profileVisibilityKarma || 'KARMA Scoring'], + ['larpSign', i18n.profileVisibilityLarpSign || 'L.A.R.P. Sign'], + ['fediverse', i18n.profileVisibilityFediverse || 'Multiverse'] + ]), + process.env.OASIS_MOBILE === '1' + ? div({ class: "pref-pill-row" }, togglePill('device', i18n.profileVisibilityDevice || 'Device', true)) + : null, input({ type: "hidden", name: "fediverseHandle", value: fediverseHandleValue }) ), br(), - div({ class: "prefs-card" }, - div({ class: "tags-header" }, - h2(i18n.clearnetSectionTitle || 'Clearnet'), - p({ class: "prefs-help" }, i18n.profileClearnetHelp || 'Modules that can be accessed from outside Oasis.') - ), - div({ class: "pref-pill-row" }, - togglePill('clearnetShops', i18n.profileClearnetShopsLabel || 'Shops'), - togglePill('clearnetJobs', i18n.profileClearnetJobsLabel || 'Jobs'), - togglePill('clearnetEvents', i18n.profileClearnetEventsLabel || 'Events'), - togglePill('clearnetProjects', i18n.profileClearnetProjectsLabel || 'Projects'), - togglePill('clearnetPosts', i18n.profileClearnetPostsLabel || 'Blogs'), - togglePill('clearnetAudios', i18n.profileClearnetAudiosLabel || 'Audios'), - togglePill('clearnetVideos', i18n.profileClearnetVideosLabel || 'Videos'), - togglePill('clearnetImages', i18n.profileClearnetImagesLabel || 'Images'), - togglePill('clearnetDocuments', i18n.profileClearnetDocumentsLabel || 'Documents'), - togglePill('clearnetTorrents', i18n.profileClearnetTorrentsLabel || 'Torrents'), - togglePill('clearnetBookmarks', i18n.profileClearnetBookmarksLabel || 'Bookmarks') - ) - ), - br(), button( { type: "submit", @@ -2264,41 +3212,41 @@ exports.editProfileView = ({ name, description, visibilityPrefs = {}, feedId = ' ); }; -exports.clearnetBlogView = async ({ msgKey, text, author, authorName, contentWarning, sentAt }) => { - const { escapeHtml: esc, renderClearnetPage } = require('./clearnet_view'); +exports.clearnetBlogView = async ({ msgKey, text, author, authorName, contentWarning, sentAt, tags = [] }) => { + const { escapeHtml: esc, renderKindTag, renderClearnetPage, renderRichText, renderTagChips } = require('./clearnet_view'); const rawText = String(text || ''); - const renderedHtml = sanitizeHtml(markdown(rawText)) - .replace(/(["'])\/blob\//g, '$1/c/blob/'); + const renderedHtml = sanitizeHtml(renderRichText(rawText)); const plainPreview = rawText.replace(/!\[[^\]]*\]\([^)]*\)/g, '').replace(/<[^>]+>/g, '').slice(0, 200); const authorEsc = esc(authorName || (author || '').slice(1, 9)); const dateStr = sentAt ? esc(new Date(sentAt).toISOString().slice(0, 10)) : ''; const cw = esc(contentWarning || ''); const firstLine = rawText.replace(/!\[[^\]]*\]\([^)]*\)/g, '').replace(/<[^>]+>/g, '').split('\n').map(s => s.trim()).find(Boolean) || ''; - const titleText = cw || firstLine.slice(0, 100) || 'Post'; + const titleText = cw || firstLine.slice(0, 100) || i18n.cnKindBlog; const extraCss = ` .cn-blog-meta{color:var(--fg-dim);font-size:13px;margin-bottom:16px;display:flex;gap:14px;flex-wrap:wrap} .cn-blog-cw{background:#663d00;color:#ffd700;border:1px solid #ff7300;padding:8px 14px;border-radius:6px;margin-bottom:16px;font-weight:600} .cn-blog-body{color:var(--fg-soft);line-height:1.7;font-size:16px;margin:0;word-wrap:break-word} -.cn-blog-body p{margin:0 0 14px 0} .cn-blog-body img{max-width:100%;height:auto;border-radius:6px;border:1px solid var(--border);display:block;margin:10px 0} .cn-blog-body a{color:var(--fg);text-decoration:underline} .cn-blog-body a:hover{color:var(--accent)} -.cn-blog-body pre,.cn-blog-body code{background:var(--bg-sub);color:var(--fg);border:1px solid var(--border);border-radius:4px;padding:2px 6px;font-family:monospace;font-size:13px} -.cn-blog-body pre{padding:10px 14px;overflow-x:auto;white-space:pre-wrap;word-break:break-word} -.cn-blog-body blockquote{margin:10px 0;padding:6px 14px;border-left:3px solid var(--fg);color:var(--fg-soft);background:var(--bg-sub);border-radius:0 4px 4px 0} -.cn-blog-body h1,.cn-blog-body h2,.cn-blog-body h3{color:var(--fg);margin:18px 0 10px 0} -.cn-blog-body ul,.cn-blog-body ol{padding-left:24px;margin:8px 0} +.cn-blog-body .rt-code{background:var(--bg-sub);color:var(--fg);border:1px solid var(--border);border-radius:4px;padding:2px 6px;font-family:monospace;font-size:13px} +.cn-blog-body .rt-code-block{background:var(--bg-sub);color:var(--fg);border:1px solid var(--border);border-radius:4px;padding:10px 14px;font-family:monospace;font-size:13px} +.cn-blog-body .rt-quote{padding:6px 14px;border-left:3px solid var(--fg);color:var(--fg-soft);background:var(--bg-sub);border-radius:0 4px 4px 0} +.cn-blog-body .rt-header{color:var(--fg);margin:18px 0 10px 0} .cn-blog-body video,.cn-blog-body audio{max-width:100%;display:block;margin:10px 0} `; const body = `
+ ${renderKindTag('blog')} ${dateStr ? `📅 ${dateStr}` : ''}
${cw ? `
${cw}
` : ''} +
${renderedHtml}
+ ${renderTagChips(tags)} `; return renderClearnetPage({ - title: `${esc(titleText)} — Oasis`, + title: `${titleText} | Oasis`, ogTitle: titleText, ogDescription: plainPreview, extraCss, @@ -2307,48 +3255,216 @@ exports.clearnetBlogView = async ({ msgKey, text, author, authorName, contentWar }); }; -exports.clearnetInhabitantView = async ({ feedId, name, description, image, prefs, items = {}, query = '', filterType = '' }) => { - const { blobUrl: cnBlob, escapeHtml: esc, renderClearnetPage } = require('./clearnet_view'); - const blobAvatarUrl = cnBlob(image); - const avatarSrc = blobAvatarUrl || '/assets/images/default-avatar.png'; - const qrSrc = feedId ? `/qr/${encodeURIComponent(feedId)}` : null; - const displayName = esc(name || 'Anonymous'); - const desc = esc(description || ''); +const cnModuleLabel = (m) => (m && i18n[m.labelKey]) || (m && m.label) || ''; +exports.cnModuleLabel = cnModuleLabel; +const CLEARNET_MODULES = [ + { key: 'audios', label: 'Audios', kind: 'audio', labelKey: 'audiosLabel', prefKey: 'clearnetAudios' }, + { key: 'posts', label: 'Blogs', kind: 'blog', labelKey: 'blogTitle', prefKey: 'clearnetPosts', modulePath: 'blog' }, + { key: 'bookmarks', label: 'Bookmarks', kind: 'bookmark', labelKey: 'bookmarksLabel', prefKey: 'clearnetBookmarks' }, + { key: 'calendars', label: 'Calendars', kind: 'calendar', labelKey: 'calendarsTitle', prefKey: 'clearnetCalendars' }, + { key: 'campaigns', label: 'Campaigns', kind: 'campaign', labelKey: 'campaignsTitle', prefKey: 'clearnetCampaigns' }, + { key: 'documents', label: 'Documents', kind: 'document', labelKey: 'docsLabel', prefKey: 'clearnetDocuments' }, + { key: 'emergencies', label: 'Emergencies', kind: 'emergency', labelKey: 'emergenciesTitle', prefKey: 'clearnetEmergencies' }, + { key: 'events', label: 'Events', kind: 'event', labelKey: 'eventsLabel', prefKey: 'clearnetEvents' }, + { key: 'feed', label: 'Feed', kind: 'feed', labelKey: 'feedTitle', prefKey: 'clearnetFeed' }, + { key: 'housing', label: 'Housing', kind: 'housing', labelKey: 'housingTitle', prefKey: 'clearnetHousing' }, + { key: 'images', label: 'Images', kind: 'image', labelKey: 'imagesLabel', prefKey: 'clearnetImages' }, + { key: 'jobs', label: 'Jobs', kind: 'job', labelKey: 'modulesJobsLabel', prefKey: 'clearnetJobs' }, + { key: 'maps', label: 'Maps', kind: 'map', labelKey: 'mapTitle', prefKey: 'clearnetMaps' }, + { key: 'market', label: 'Market', kind: 'market', labelKey: 'marketTitle', prefKey: 'clearnetMarket' }, + { key: 'podcasts', label: 'Podcasts', kind: 'podcast', labelKey: 'podcastsTitle', prefKey: 'clearnetPodcasts' }, + { key: 'projects', label: 'Projects', kind: 'project', labelKey: 'projectsTitle', prefKey: 'clearnetProjects' }, + { key: 'rooms', label: 'Rooms', kind: 'room', labelKey: 'roomsTitle', prefKey: 'clearnetRooms' }, + { key: 'school', label: 'School', kind: 'course', labelKey: 'schoolTitle', prefKey: 'clearnetSchool' }, + { key: 'shops', label: 'Shops', kind: 'shop', labelKey: 'shopsTitle', prefKey: 'clearnetShops' }, + { key: 'torrents', label: 'Torrents', kind: 'torrent', labelKey: 'torrentsLabel', prefKey: 'clearnetTorrents' }, + { key: 'files', label: 'Files', kind: 'file', labelKey: 'filesLabel', prefKey: 'clearnetFiles' }, + { key: 'videos', label: 'Videos', kind: 'video', labelKey: 'videosLabel', prefKey: 'clearnetVideos' }, + { key: 'wiki', label: 'Wikis', kind: 'wiki', labelKey: 'wikiTitle', prefKey: 'clearnetWiki' } +]; + +exports.CLEARNET_MODULES = CLEARNET_MODULES; + +const CLEARNET_HUB_CSS = ` +.cn-filter-row{display:flex;flex-wrap:wrap;gap:8px;margin:24px 0 12px 0} +.cn-filter-btn{display:inline-block;padding:6px 14px;background:var(--bg-elev);color:var(--fg-soft);border:1px solid var(--border);border-radius:14px;font-size:13px;text-decoration:none;transition:border-color .15s ease,color .15s ease,background .15s ease} +.cn-filter-btn:hover{border-color:var(--fg);color:var(--fg);text-decoration:none} +.cn-filter-btn.active{background:var(--bg-sub);border-color:var(--fg);color:var(--fg);font-weight:600} +.cn-hub-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:14px;margin-top:16px} +.cn-hub-kind{color:var(--fg-dim);font-size:10px;text-transform:uppercase;letter-spacing:2px;font-weight:600} +.cn-hub-card{display:flex;flex-direction:column;background:var(--bg-elev);border:1px solid var(--border);border-radius:8px;overflow:hidden;transition:border-color .15s ease;color:var(--fg);text-decoration:none} +.cn-hub-card:hover{border-color:var(--fg);text-decoration:none} +.cn-hub-card a{color:var(--fg);text-decoration:none} +.cn-hub-card a:hover{text-decoration:underline} +.cn-hub-player{width:100%;display:block;margin:6px 0;border-radius:6px;background:#000} +video.cn-hub-player{max-height:160px;object-fit:cover} +img.cn-hub-player{max-height:200px;object-fit:cover} +audio.cn-hub-player{background:transparent;height:36px} +.cn-hub-thumb{width:100%;height:140px;object-fit:cover;background:#000;border-bottom:1px solid var(--border)} +.cn-pop-row{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:14px;margin-top:16px} +.cn-pop-thumb{width:100%;height:90px;object-fit:cover;background:#000;border-bottom:1px solid var(--border)} +.cn-pop-card .cn-hub-body{padding:8px 10px;gap:4px} +.cn-pop-card .cn-hub-title{font-size:13px} +@media (max-width:700px){.cn-pop-row{grid-template-columns:none;grid-auto-flow:column;grid-auto-columns:42%;overflow-x:auto}} +.cn-hub-body{padding:12px 14px;display:flex;flex-direction:column;gap:6px;min-width:0} +.cn-hub-title{color:var(--fg);font-weight:600;font-size:15px;word-break:break-word} +.cn-hub-snippet{color:var(--fg-soft);font-size:13px;line-height:1.4;word-break:break-word;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden} +.cn-hub-snippet .cn-url{text-decoration:underline} +.cn-hub-card{position:relative} +.cn-hub-cover{position:absolute;inset:0;z-index:1} +.cn-hub-player,.cn-hub-card a:not(.cn-hub-cover){position:relative;z-index:2} +.cn-hub-meta{color:var(--fg-dim);font-size:11px;text-transform:uppercase;letter-spacing:1px;margin-top:auto} +.cn-hub-author{color:var(--fg-dim);font-size:11px;word-break:break-all} +.cn-empty-content{background:var(--bg-elev);border:1px dashed var(--border);border-radius:8px;padding:24px;text-align:center;color:var(--fg-dim);font-size:14px} +`; + +const buildClearnetHub = ({ items = {}, filterBase, filterType = '', query = '', showAuthor = false, page = 1 }) => { + const { blobUrl: cnBlob, escapeHtml: esc, renderRichText, renderTagChips, kindLabel, paginateClearnet, renderClearnetPager } = require('./clearnet_view'); const renderHubItem = (modulePath, it) => { const blob = cnBlob(it.image); - const title = esc(it.title || 'Untitled'); - const snippet = esc((it.snippet || '').slice(0, 160)); + const href = clearnetItemHref(modulePath, it.title, it.id); + const title = esc(it.title || i18n.cnUntitled); + const raw = String(it.snippet || ''); + const snippet = renderRichText(raw.slice(0, 300)); const meta = esc(it.meta || ''); const kind = esc(it.kind || ''); - return ` + const mediaSrc = it.media && it.media.blobId ? cnBlob(it.media.blobId) : null; + const player = mediaSrc + ? (it.media.kind === 'video' + ? `` + : it.media.kind === 'image' + ? `` + : ``) + : ''; + const chipRow = [ + meta ? `📅 ${meta}` : '', + ...(Array.isArray(it.details) ? it.details : []).map(d => `${esc(String(d))}`), + it.price ? `${esc(String(it.price))} ECO` : '' + ].filter(Boolean).join(''); + const author = showAuthor && it.feedId + ? `${esc(it.authorName || String(it.feedId).slice(1, 9))}` + : ''; + return `
+ ${blob ? `` : ''}
${kind ? `
${kind}
` : ''} -
${title}
- ${snippet ? `
${snippet}${(it.snippet || '').length > 160 ? '…' : ''}
` : ''} - ${meta ? `
${meta}
` : ''} + ${it.title ? `
${title}
` : ''} + ${author} + ${player} + ${snippet ? `
${snippet}${raw.length > 300 ? '…' : ''}
` : ''} + ${chipRow ? `
${chipRow}
` : ''} + ${renderTagChips(it.tags)}
- `; +
`; }; - const moduleDef = [ - { key: 'shops', label: 'Shops', kind: 'Shop', prefKey: 'clearnetShops' }, - { key: 'jobs', label: 'Jobs', kind: 'Job', prefKey: 'clearnetJobs' }, - { key: 'events', label: 'Events', kind: 'Event', prefKey: 'clearnetEvents' }, - { key: 'projects', label: 'Projects', kind: 'Project', prefKey: 'clearnetProjects' }, - { key: 'posts', label: 'Blogs', kind: 'Blog', prefKey: 'clearnetPosts', modulePath: 'blog' }, - { key: 'audios', label: 'Audios', kind: 'Audio', prefKey: 'clearnetAudios' }, - { key: 'videos', label: 'Videos', kind: 'Video', prefKey: 'clearnetVideos' }, - { key: 'images', label: 'Images', kind: 'Image', prefKey: 'clearnetImages' }, - { key: 'documents', label: 'Documents', kind: 'Document', prefKey: 'clearnetDocuments' }, - { key: 'torrents', label: 'Torrents', kind: 'Torrent', prefKey: 'clearnetTorrents' }, - { key: 'bookmarks', label: 'Bookmarks', kind: 'Bookmark', prefKey: 'clearnetBookmarks' } - ]; + const q = String(query || '').trim().toLowerCase(); + const matches = (it) => !q || [it.title, it.snippet, it.meta, ...(Array.isArray(it.tags) ? it.tags.map(t => `#${t}`) : [])].some(v => String(v || '').toLowerCase().includes(q)); + const allItems = []; + for (const m of CLEARNET_MODULES) { + for (const it of (items[m.key] || [])) { + if (!matches(it)) continue; + allItems.push({ ...it, modulePath: m.modulePath || m.key, kind: kindLabel(m.kind), _moduleKey: m.key }); + } + } + allItems.sort((a, b) => (Number(b.ts) || 0) - (Number(a.ts) || 0)); + const activeFilter = String(filterType || '').toLowerCase(); + const visibleItems = activeFilter ? allItems.filter(it => it._moduleKey === activeFilter) : allItems; + const qs = q ? `&q=${encodeURIComponent(query)}` : ''; + const filterButtons = `
+ ${esc(i18n.all)} (${allItems.length}) + ${CLEARNET_MODULES.filter(m => allItems.some(it => it._moduleKey === m.key)).map(m => { + const count = allItems.filter(it => it._moduleKey === m.key).length; + return `${esc(cnModuleLabel(m))} (${count})`; + }).join('')} +
`; + const paged = paginateClearnet(visibleItems, page); + const pager = renderClearnetPager({ base: filterBase, params: { type: activeFilter, q: query }, page: paged.page, pages: paged.pages, per: paged.per, total: paged.total }); + const sections = visibleItems.length + ? `${filterButtons}

${esc(i18n.cnNetworkContent)} (${visibleItems.length})

${paged.items.map(it => renderHubItem(it.modulePath, it)).join('')}
${pager}` + : (allItems.length ? `${filterButtons}
${esc(i18n.cnCategoryEmpty)}
` : ''); + return { sections, total: allItems.length, visible: visibleItems.length, page: paged.page, pages: paged.pages }; +}; + +exports.CLEARNET_HUB_CSS = CLEARNET_HUB_CSS; + +exports.clearnetHubView = async ({ authors = [], items = {}, tribes = [], filterType = '', query = '', page = 1, tribesPage = 1 }) => { + const { escapeHtml: esc, renderClearnetPage, renderClearnetHubSearch, blobUrl: cnBlob, CLEARNET_PAGER_CSS, renderClearnetPager } = require('./clearnet_view'); + const hub = buildClearnetHub({ items, filterBase: '/c', filterType, query, page }); + const extraCss = CLEARNET_HUB_CSS + CLEARNET_PAGER_CSS + ` +`; + const q = String(query || '').trim().toLowerCase(); + const shownTribes = (filterType || hub.page > 1) ? [] : (tribes || []).filter(t => !q || [t.title, t.description].some(v => String(v || '').toLowerCase().includes(q))); + const ranked = shownTribes.slice().sort((x, y) => (Number(y.content) || 0) - (Number(x.content) || 0)); + const tribePage = slicePage(ranked, tribesPage, 10); + const tribeCard = (t) => { + const img = cnBlob(t.image) || '/c/assets/images/default-tribe.png'; + const href = `/c/tribe/${encodeURIComponent(clearnetSlugFor(t.title, t.id))}`; + return `
${esc(t.title || i18n.cnUntitled)}
${esc(i18n.cnPublicContent)}: ${Number(t.content) || 0}
`; + }; + const tribesMore = renderClearnetPager({ base: '/c', params: { q: query }, page: tribePage.page, pages: tribePage.pages, total: tribePage.total, pageParam: 'tpage' }); + const tribesBlock = ranked.length + ? `

${esc(i18n.cnTribesContent)} (${ranked.length})

${tribePage.items.map(tribeCard).join('')}
${tribesMore}` + : ''; + const body = ` + ${tribesBlock} + ${hub.total ? hub.sections : (tribesBlock ? '' : `
${esc(i18n.cnHubEmpty)}
`)} +`; + return renderClearnetPage({ + title: `${i18n.cnHubTitle} | Oasis`, + ogTitle: `${i18n.cnHubTitle} | Oasis`, + ogDescription: i18n.cnHubDescription, + extraCss, + body, + headerExtra: renderClearnetHubSearch({ type: filterType, query }) + }); +}; + +exports.clearnetInhabitantView = async ({ feedId, name, description, image, prefs, items = {}, query = '', filterType = '', page = 1 }) => { + const { blobUrl: cnBlob, escapeHtml: esc, renderRichText, renderTagChips, renderClearnetPage, kindLabel, paginateClearnet, renderClearnetPager, CLEARNET_PAGER_CSS } = require('./clearnet_view'); + const blobAvatarUrl = cnBlob(image); + const avatarSrc = blobAvatarUrl || '/assets/images/default-avatar.png'; + const qrSrc = feedId ? `/c/qr/${encodeURIComponent(feedId)}` : null; + const displayName = esc(name && name !== 'Redacted' ? name : feedId); + const desc = renderRichText(description && description !== 'Redacted' ? description : ''); + const renderHubItem = (modulePath, it) => { + const blob = cnBlob(it.image); + const title = esc(it.title || i18n.cnUntitled); + const raw = String(it.snippet || ''); + const snippet = renderRichText(raw.slice(0, 300)); + const meta = esc(it.meta || ''); + const kind = esc(it.kind || ''); + const mediaSrc = it.media && it.media.blobId ? cnBlob(it.media.blobId) : null; + const preview = mediaSrc && it.media.kind === 'image' + ? `` + : ''; + const href = clearnetItemHref(modulePath, it.title, it.id); + const chipRow = [ + meta ? `📅 ${meta}` : '', + ...(Array.isArray(it.details) ? it.details : []).map(d => `${esc(String(d))}`), + it.price ? `${esc(String(it.price))} ECO` : '' + ].filter(Boolean).join(''); + return `
+ + ${blob ? `` : ''} +
+ ${kind ? `
${kind}
` : ''} + ${it.title ? `
${title}
` : ''} + ${preview} + ${snippet ? `
${snippet}${raw.length > 300 ? '…' : ''}
` : ''} + ${chipRow ? `
${chipRow}
` : ''} + ${renderTagChips(it.tags)} +
+
`; + }; + const moduleDef = CLEARNET_MODULES; const allItems = []; for (const m of moduleDef) { for (const it of (items[m.key] || [])) { - allItems.push({ ...it, modulePath: m.modulePath || m.key, kind: m.kind, _moduleKey: m.key }); + allItems.push({ ...it, modulePath: m.modulePath || m.key, kind: kindLabel(m.kind), _moduleKey: m.key }); } } + allItems.sort((a, b) => (Number(b.ts) || 0) - (Number(a.ts) || 0)); const activeFilter = (filterType || '').toLowerCase(); const visibleItems = activeFilter ? allItems.filter(it => it._moduleKey === activeFilter) @@ -2356,18 +3472,20 @@ exports.clearnetInhabitantView = async ({ feedId, name, description, image, pref const totalCount = visibleItems.length; const filterBase = `/c/inhabitant/${encodeURIComponent(feedId)}`; const filterButtons = `
- All (${allItems.length}) - ${moduleDef.filter(m => prefs && prefs[m.prefKey] && (items[m.key] || []).length).map(m => { + ${esc(i18n.all)} (${allItems.length}) + ${moduleDef.filter(m => (items[m.key] || []).length).map(m => { const isActive = activeFilter === m.key; const count = (items[m.key] || []).length; - return `${esc(m.label)} (${count})`; + return `${esc(cnModuleLabel(m))} (${count})`; }).join('')}
`; + const paged = paginateClearnet(visibleItems, page); + const pager = renderClearnetPager({ base: filterBase, params: { type: activeFilter }, page: paged.page, pages: paged.pages, per: paged.per, total: paged.total }); const sections = totalCount - ? `${filterButtons}

Public Content (${totalCount})

${visibleItems.map(it => renderHubItem(it.modulePath, it)).join('')}
` - : (allItems.length ? `${filterButtons}
No content in this category.
` : ''); - const noResults = '
This inhabitant has not published content to Clearnet yet.
'; - const extraCss = ` + ? `${filterButtons}

${esc(i18n.cnPublicContent)} (${totalCount})

${paged.items.map(it => renderHubItem(it.modulePath, it)).join('')}
${pager}` + : (allItems.length ? `${filterButtons}
${esc(i18n.cnCategoryEmpty)}
` : ''); + const noResults = `
${esc(i18n.cnInhabitantEmpty)}
`; + const extraCss = CLEARNET_PAGER_CSS + ` .cn-profile{display:flex;gap:24px;flex-wrap:wrap;align-items:flex-start;margin-bottom:24px} .cn-avatar{width:160px;height:160px;border-radius:8px;border:3px solid var(--fg);object-fit:cover;background:#000;flex:0 0 auto} .cn-profile-body{flex:1 1 280px;min-width:0} @@ -2384,16 +3502,26 @@ exports.clearnetInhabitantView = async ({ feedId, name, description, image, pref .cn-hub-kind{color:var(--fg-dim);font-size:10px;text-transform:uppercase;letter-spacing:2px;font-weight:600} .cn-hub-card{display:flex;flex-direction:column;background:var(--bg-elev);border:1px solid var(--border);border-radius:8px;overflow:hidden;transition:border-color .15s ease;color:var(--fg);text-decoration:none} .cn-hub-card:hover{border-color:var(--fg);text-decoration:none} +.cn-hub-card a{color:var(--fg);text-decoration:none} +.cn-hub-card a:hover{text-decoration:underline} +.cn-hub-player{width:100%;display:block;margin:6px 0;border-radius:6px;background:#000} +video.cn-hub-player{max-height:160px;object-fit:cover} +img.cn-hub-player{max-height:200px;object-fit:cover} +audio.cn-hub-player{background:transparent;height:36px} .cn-hub-thumb{width:100%;height:140px;object-fit:cover;background:#000;border-bottom:1px solid var(--border)} .cn-hub-body{padding:12px 14px;display:flex;flex-direction:column;gap:6px;min-width:0} .cn-hub-title{color:var(--fg);font-weight:600;font-size:15px;word-break:break-word} .cn-hub-snippet{color:var(--fg-soft);font-size:13px;line-height:1.4;word-break:break-word;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden} +.cn-hub-snippet .cn-url{text-decoration:underline} +.cn-hub-card{position:relative} +.cn-hub-cover{position:absolute;inset:0;z-index:1} +.cn-hub-player,.cn-hub-card a:not(.cn-hub-cover){position:relative;z-index:2} .cn-hub-meta{color:var(--fg-dim);font-size:11px;text-transform:uppercase;letter-spacing:1px;margin-top:auto} .cn-empty-content{background:var(--bg-elev);border:1px dashed var(--border);border-radius:8px;padding:24px;text-align:center;color:var(--fg-dim);font-size:14px} `; const body = `
- ${displayName} + ${displayName}

${displayName}

${esc(feedId)}
@@ -2404,7 +3532,7 @@ exports.clearnetInhabitantView = async ({ feedId, name, description, image, pref ${totalCount > 0 ? sections : noResults} `; return renderClearnetPage({ - title: `${name || 'Inhabitant'} — Oasis`, + title: `${name || 'Inhabitant'} | Oasis`, ogTitle: name || 'Oasis', ogDescription: description || '', ogImage: blobAvatarUrl, @@ -2443,7 +3571,17 @@ const renderUserSensors = (u, opts = {}) => { })() : null; const items = []; - if (show('karma')) items.push(span({ class: 'karma-line' }, `${i18n.bankingUserEngagementScore}: `, strong(String(u.karmaScore !== undefined ? u.karmaScore : 0)))); + const kr = u.karmaReceived && typeof u.karmaReceived === 'object' ? u.karmaReceived : null; + const krPos = kr ? Number(kr.positive) || 0 : 0; + const krNeg = kr ? Number(kr.negative) || 0 : 0; + const krTone = !kr ? '' : krPos > krNeg ? ' karma-value-positive' : krPos < krNeg ? ' karma-value-negative' : ' karma-value-neutral'; + if (show('karma')) items.push(span({ class: kr ? 'karma-line karma-line-stacked' : 'karma-line' }, + span({ class: 'karma-line-main' }, `${i18n.bankingUserEngagementScore}: `, strong({ class: 'karma-value' + krTone }, String(u.karmaScore !== undefined ? u.karmaScore : 0))), + kr ? span({ class: 'karma-received-row' }, + span({ class: 'karma-received karma-received-up' }, span({ class: 'karma-arrow' }, '▲'), String(krPos)), + span({ class: 'karma-received karma-received-down' }, span({ class: 'karma-arrow' }, '▼'), String(krNeg)) + ) : null + )); if (show('ecoTax')) items.push(span({ class: 'karma-line eco-tax-line' }, `${i18n.profileVisibilityEcoTax || 'ECO Tax'}: `, strong(fmtCarbon(u.carbonGrams)))); if (activityChip) items.push(activityChip); if (deviceChip) items.push(deviceChip); @@ -2459,16 +3597,32 @@ const renderUserSensors = (u, opts = {}) => { items.push(span({ class: 'gpg-line' }, `${i18n.profileGpgChip || 'GPG'}: `, gpgNode)); } if (show('wallet') && (u.ecoAddress || isMe)) { - const wt = u.ecoAddress || (i18n.statsEcoWalletNotConfigured || 'Not configured!'); - const wn = isMe ? a({ href: '/wallet' }, strong(wt)) : strong(wt); - items.push(span({ class: 'ubi-line' }, `${i18n.statsEcoWalletLabel || 'ECOin Wallet'}: `, wn)); + const addressNode = u.ecoAddress + ? span({ class: 'wallet-address', title: i18n.statsEcoWalletLabel }, u.ecoAddress) + : (isMe ? a({ href: '/wallet' }, strong(i18n.statsEcoWalletNotConfigured || 'Not configured!')) : strong(i18n.statsEcoWalletNotConfigured || 'Not configured!')); + items.push(div({ class: 'wallet-line' }, + span({ class: 'wallet-line-head', title: i18n.statsEcoWalletLabel }, span({ class: 'wallet-icon' }, '❄')), + isMe && u.ecoAddress ? a({ href: '/wallet', class: 'wallet-line-link' }, addressNode) : addressNode + )); } - if (show('ubi')) { - items.push(span({ class: 'ubi-line' }, `${i18n.bankUbiThisMonth || 'UBI'}: `, strong(`${Number(u.estimatedUBI || 0).toFixed(6)} ECO`))); - items.push(span({ class: 'ubi-line' }, `${i18n.bankUbiLastClaimed || 'Last claimed'}: `, u.lastClaimedDate ? a({ href: '/transfers?filter=ubi', class: 'user-link' }, new Date(u.lastClaimedDate).toLocaleDateString()) : strong(i18n.bankUbiNeverClaimed || 'Never claimed'))); - items.push(span({ class: 'ubi-line' }, `${i18n.bankUbiTotalClaimed || 'Total claimed'}: `, strong(`${Number(u.totalClaimed || 0).toFixed(6)} ECO`))); + const phoneVisible = isMe || prefs.phone === 'whole' || prefs.phone === undefined || (prefs.phone === 'mutuals' && u.mutual === true); + if (phoneOn() && prefs.phone !== 'off' && phoneVisible && u.id && !sharedState.isPubId(u.id)) { + const number = require('../models/phone_number').phoneNumberOf(u.id); + const dnd = isMe ? !!(getConfig().phone || {}).dnd : prefs.phoneDnd === true; + if (number) items.push(div({ class: 'wallet-line phone-number-line' }, + span({ class: 'wallet-line-head', title: i18n.profileVisibilityPhone }, span({ class: 'phone-number-icon' }, '✆')), + isMe || dnd + ? span({ class: 'wallet-address phone-number-value', title: isMe ? i18n.profileVisibilityPhone : i18n.phoneRecipientRefuses }, number) + : renderPhoneCallForm(u.id, span({ class: 'wallet-address phone-number-value' }, number), i18n.phoneCallButton), + renderStateChip(dnd ? 'hidden' : 'mutuals', null, String(dnd ? i18n.phoneDnd : i18n.phoneOpen).toUpperCase()), + isMe && opts.dndToggle !== false && process.env.OASIS_MOBILE !== '1' ? form({ method: 'POST', action: '/phone/dnd', class: 'phone-dnd-form' }, + input({ type: 'hidden', name: 'dnd', value: dnd ? '0' : '1' }), + button({ type: 'submit', class: 'tribe-action-btn' }, String(dnd ? i18n.phoneOpen : i18n.phoneDnd).toUpperCase()) + ) : null + )); } const sensorsBox = items.length ? div({ class: 'profile-sensors-box' }, ...items) : null; + if (opts.sensorsOnly) return sensorsBox ? [sensorsBox] : []; const larpNode = (show('larpSign') && u.larpHouse && u.larpHouse.key) ? a({ href: `/larp/${u.larpHouse.key}`, class: 'larp-sign-block', title: u.larpHouse.name }, img({ src: u.larpHouse.image || '/assets/larp/images/default.jpg', alt: u.larpHouse.name, class: 'larp-sign-large' })) : null; @@ -2477,13 +3631,12 @@ const renderUserSensors = (u, opts = {}) => { const path = `/c/inhabitant/${encodeURIComponent(u.id)}`; return div({ class: 'profile-reach' }, renderReachChip(true, i18n, path), - renderInviteQrCard({ qrDataUrl: `/qr-clearnet/${encodeURIComponent(u.id)}` }), - isMe ? form({ method: 'POST', action: '/profile/clearnet-toggle', class: 'profile-reach-toggle' }, button({ type: 'submit', class: 'btn' }, i18n.profileSwitchToOasis || 'Return to Oasis')) : null + renderInviteQrCard({ qrDataUrl: `/qr-clearnet/${encodeURIComponent(u.id)}` }) ); })() : null; const contentNode = opts.excludeContent ? null : renderContentStats(u.stats, i18n); - return [fediverseNode, opts.relationshipNode || null, sensorsBox, larpNode, reachNode, contentNode].filter(Boolean); + return [opts.relationshipNode || null, sensorsBox, fediverseNode, larpNode, reachNode, contentNode].filter(Boolean); }; exports.renderUserSensors = renderUserSensors; @@ -2499,6 +3652,7 @@ exports.authorView = async ({ relationship, ecoAddress, karmaScore = 0, + karmaReceived = null, estimatedUBI = 0, lastClaimedDate = null, totalClaimed = 0, @@ -2521,7 +3675,7 @@ exports.authorView = async ({ const rawPrefs = visibilityPrefs || {}; const prefs = { activity: rawPrefs.activity === true, - device: rawPrefs.device !== false, + device: rawPrefs.device === true, karma: rawPrefs.karma !== false, ubi: rawPrefs.ubi === true, wallet: rawPrefs.wallet === true, @@ -2530,9 +3684,11 @@ exports.authorView = async ({ clearnet: rawPrefs.clearnet === true, fediverse: rawPrefs.fediverse === true, fediverseHandle: typeof rawPrefs.fediverseHandle === 'string' ? rawPrefs.fediverseHandle : '', - gpg: rawPrefs.gpg !== false + gpg: rawPrefs.gpg === true, + phone: rawPrefs.phone === 'mutuals' || rawPrefs.phone === 'off' ? rawPrefs.phone : 'whole', + phoneDnd: rawPrefs.phoneDnd === true }; - const clearnetSubKeys = ['clearnetShops','clearnetJobs','clearnetEvents','clearnetProjects','clearnetPosts','clearnetAudios','clearnetVideos','clearnetImages','clearnetDocuments','clearnetTorrents','clearnetBookmarks']; + const clearnetSubKeys = CLEARNET_MODULES.map(m => m.prefKey); const anySubClearnet = clearnetSubKeys.some(k => rawPrefs[k] === true); prefs.clearnet = prefs.clearnet || anySubClearnet; const showField = (key) => prefs[key]; @@ -2542,57 +3698,14 @@ exports.authorView = async ({ const escHtml = s => String(s).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); const markdownMention = `[@${escHtml(name)}](${escHtml(feedId)})`; - const contactForms = []; - const addForm = ({ action }) => - contactForms.push( - form( - { action: `/${action}/${encodeURIComponent(feedId)}`, method: "post" }, - button({ type: "submit", class: "btn" }, i18n[action]) - ) - ); - - if (relationship.me === false) { - if (relationship.following) addForm({ action: "unfollow" }); - else if (relationship.blocking) addForm({ action: "unblock" }); - else { addForm({ action: "follow" }); addForm({ action: "block" }) } - } - - const relationshipMessage = (() => { - if (relationship.me) return i18n.relationshipYou; - const following = relationship.following === true; - const followsMe = relationship.followsMe === true; - if (following && followsMe) return i18n.relationshipMutuals; - const messagesArr = []; - messagesArr.push(following ? i18n.relationshipFollowing : i18n.relationshipNone); - messagesArr.push(followsMe ? i18n.relationshipTheyFollow : i18n.relationshipNotFollowing); - return messagesArr.join(". ") + "."; - })(); - - const relationshipBlock = relationship.me - ? span({ class: "status you" }, i18n.relationshipYou) - : div({ class: "relationship-status" }, - relationship.blocking && relationship.blockedBy - ? span({ class: "status blocked" }, i18n.relationshipMutualBlock) - : [ - relationship.blocking ? span({ class: "status blocked" }, i18n.relationshipBlocking) : null, - relationship.blockedBy ? span({ class: "status blocked-by" }, i18n.relationshipBlockedBy) : null, - relationship.following && relationship.followsMe - ? span({ class: "status mutual" }, i18n.relationshipMutuals) - : [ - span({ class: "status supporting" }, relationship.following ? i18n.relationshipFollowing : i18n.relationshipNone), - span({ class: "status supported-by" }, relationship.followsMe ? i18n.relationshipTheyFollow : i18n.relationshipNotFollowing) - ] - ], - contactForms.length - ? div({ class: "relationship-actions" }, ...contactForms) - : null - ); + const relationshipBlock = renderRelationshipBlock(relationship, feedId); const userSensors = renderUserSensors({ isMe: isOwnProfile, fediverseConfigured, prefs, id: feedId, - karmaScore, carbonGrams, deviceSource, activityBucket: lastActivityBucket, + karmaScore, karmaReceived, carbonGrams, deviceSource, activityBucket: lastActivityBucket, gpgFingerprint, ecoAddress, estimatedUBI, lastClaimedDate, totalClaimed, - larpHouse, stats + larpHouse, stats, + mutual: !!(relationship && relationship.following && relationship.followsMe) }, { relationshipNode: div({ class: "profile-side-relationship" }, relationshipBlock) }); const sideColumn = div({ class: "tribe-side profile-side" }, @@ -2607,13 +3720,15 @@ exports.authorView = async ({ ) : null, feedId ? renderInviteQrCard({ qrDataUrl: `/qr/${encodeURIComponent(feedId)}` }) : null, description !== "" - ? div({ class: "profile-side-description", innerHTML: sanitizeHtml(markdown(description)) }) + ? div({ class: "profile-side-description", [RAW_HTML]: sanitizeHtml(renderStyledHtml(description)) }) : null, ...userSensors, div({ class: "profile-side-actions" }, - isOwnProfile ? a({ href: `/profile/edit`, class: "btn" }, i18n.editProfile) : null, - a({ href: `/likes/${encodeURIComponent(feedId)}`, class: "btn" }, i18n.viewLikes), - !isOwnProfile ? a({ href: `/pm?recipients=${encodeURIComponent(feedId)}`, class: "btn" }, i18n.pmCreateButton) : null + isOwnProfile ? a({ href: `/profile/edit`, class: "filter-btn" }, `✎ ${i18n.editProfile}`) : null, + isOwnProfile ? a({ href: "/supporters", class: "filter-btn" }, `♥ ${i18n.viewSupporters}`) : null, + a({ href: `/likes/${encodeURIComponent(feedId)}`, class: "filter-btn" }, `⟳ ${i18n.viewLikes}`), + !isOwnProfile ? a({ href: `/pm?recipients=${encodeURIComponent(feedId)}`, class: "filter-btn" }, `✉ ${i18n.pmCreateButton}`) : null, + !isOwnProfile ? renderCallButton(feedId, { pam: true }) : null ) ); @@ -2630,25 +3745,40 @@ exports.authorView = async ({ videos: new Set(['video']), images: new Set(['image']), documents: new Set(['document']), - torrents: new Set(['torrent']) + torrents: new Set(['torrent']), + files: new Set(['file']), + bookmarks: new Set(['bookmark']), + podcasts: new Set(['podcast']), + school: new Set(['schoolCourse']), + market: new Set(['market']), + feed: new Set(['feed']), + wiki: new Set(['wikiPage']) }; const moduleDef = [ - { key: 'shops', label: i18n.profileClearnetShopsLabel || 'Shops' }, - { key: 'jobs', label: i18n.profileClearnetJobsLabel || 'Jobs' }, - { key: 'events', label: i18n.profileClearnetEventsLabel || 'Events' }, - { key: 'projects', label: i18n.profileClearnetProjectsLabel || 'Projects' }, - { key: 'posts', label: i18n.profileClearnetPostsLabel || 'Blogs' }, { key: 'audios', label: i18n.profileClearnetAudiosLabel || 'Audios' }, - { key: 'videos', label: i18n.profileClearnetVideosLabel || 'Videos' }, - { key: 'images', label: i18n.profileClearnetImagesLabel || 'Images' }, + { key: 'posts', label: i18n.profileClearnetPostsLabel || 'Blogs' }, + { key: 'bookmarks', label: i18n.profileClearnetBookmarksLabel || 'Bookmarks' }, { key: 'documents', label: i18n.profileClearnetDocumentsLabel || 'Documents' }, - { key: 'torrents', label: i18n.profileClearnetTorrentsLabel || 'Torrents' } + { key: 'events', label: i18n.profileClearnetEventsLabel || 'Events' }, + { key: 'feed', label: i18n.profileClearnetFeedLabel || 'Feed' }, + { key: 'images', label: i18n.profileClearnetImagesLabel || 'Images' }, + { key: 'jobs', label: i18n.profileClearnetJobsLabel || 'Jobs' }, + { key: 'market', label: i18n.profileClearnetMarketLabel || 'Market' }, + { key: 'podcasts', label: i18n.profileClearnetPodcastsLabel || 'Podcasts' }, + { key: 'projects', label: i18n.profileClearnetProjectsLabel || 'Projects' }, + { key: 'school', label: i18n.profileClearnetSchoolLabel || 'School' }, + { key: 'shops', label: i18n.profileClearnetShopsLabel || 'Shops' }, + { key: 'torrents', label: i18n.profileClearnetTorrentsLabel || 'Torrents' }, + { key: 'files', label: i18n.profileClearnetFilesLabel || 'Files' }, + { key: 'videos', label: i18n.profileClearnetVideosLabel || 'Videos' }, + { key: 'wiki', label: i18n.profileClearnetWikiLabel || 'Wikis' } ]; const enabledKeys = moduleDef.filter(m => rawPrefs[`profile${m.key.charAt(0).toUpperCase() + m.key.slice(1)}`] === true).map(m => m.key); if (enabledKeys.length > 0) { const allowedTypes = new Set(); for (const k of enabledKeys) for (const t of keyToTypes[k]) allowedTypes.add(t); - const authorActions = allActions.filter(a => a && a.author === feedId && allowedTypes.has(a.type)); + const channelsWithEpisodes = new Set(allActions.filter(a => a && a.type === 'podcastEpisode' && a.content && a.content.channel).map(a => String(a.content.channel))); + const authorActions = allActions.filter(a => a && a.author === feedId && allowedTypes.has(a.type) && (a.type !== 'podcast' || channelsWithEpisodes.has(String(a.rootId || a.id)) || channelsWithEpisodes.has(String(a.id)))); const counts = {}; for (const k of enabledKeys) counts[k] = 0; for (const a of authorActions) { @@ -2673,11 +3803,10 @@ exports.authorView = async ({ ? authorActions.filter(a => keyToTypes[activeFilter].has(a.type)) : authorActions; visible.sort((a, b) => (b.ts || 0) - (a.ts || 0)); - const limited = visible.slice(0, 50); const { renderActionCards } = require('./activity_view'); mainColumnContent.push(filterRow); mainColumnContent.push(div({ class: 'feed-container profile-module-section' }, - renderActionCards(limited, feedId, allActions || limited, spreadMap instanceof Map ? spreadMap : new Map()) + renderActionCards(visible, (config.keys && config.keys.id) ? config.keys.id : '', allActions || visible, spreadMap instanceof Map ? spreadMap : new Map(), { paged: true }) )); } } @@ -2695,8 +3824,7 @@ exports.authorView = async ({ const hasDocument = Array.isArray(allActions) && allActions.some(a => a && a.author === feedId && a.type === 'document'); if (hasDocument) { html += ` - - + `; } return html; @@ -2827,7 +3955,7 @@ exports.commentView = async ( br(), label( { for: "blob" }, - i18n.blogImage || "Upload media (max-size: 50MB)" + i18n.blogImage || "Upload media (max-size: 75MB)" ), input({ type: "file", id: "blob", name: "blob" }), br(), @@ -2841,7 +3969,7 @@ exports.commentView = async ( const renderMessage = (msg) => { const content = lodash.get(msg, "value.content", {}); const authorId = msg.value.author || "Anonymous"; - const createdAt = new Date(msg.value.timestamp).toLocaleString(); + const createdAt = moment(msg.value.timestamp).format("YYYY/MM/DD HH:mm"); const mentionsText = content.text || ''; const isTribe = content.type === 'tribe-content'; const visitUrl = isTribe @@ -2868,116 +3996,85 @@ const renderMessage = (msg) => { div({ class: 'card-body' }, div({ class: 'card-section' }, badge, - p({ class: 'post-text' }, ...renderUrl(mentionsText || '[No content]')) + p({ class: 'post-text' }, ...renderStyledText(mentionsText || '[No content]')) ) ), p({ class: 'card-footer' }, - span({ class: 'date-link' }, `${createdAt} ${i18n.performed || ''} `), + span({ class: 'date-link' }, `${createdAt}`), userLink(authorId) ) ); }; -const hasMention = (msg, feedId) => { - const content = lodash.get(msg, "value.content", {}); - const mentions = content.mentions; - if (mentions) { - if (Array.isArray(mentions)) { - if (mentions.some(m => m.link === feedId || m.feed === feedId)) return true; - } else if (typeof mentions === 'object') { - for (const arr of Object.values(mentions)) { - if (Array.isArray(arr) && arr.some(m => m.link === feedId || m.feed === feedId)) return true; - if (arr && (arr.link === feedId || arr.feed === feedId)) return true; - } - } - } - const text = content.text || ''; - if (text.includes(feedId) || text.includes(feedId.slice(1))) return true; - return false; -}; -exports.mentionsView = ({ messages, myFeedId }) => { - const title = i18n.mentions; - const description = i18n.mentionsDescription; - if (!Array.isArray(messages) || messages.length === 0) { - return template( - title, - section( - div({ class: "tags-header" }, - h2(title), - p(description) - ) - ), - section( - div({ class: "mentions-list" }, - p({ class: "empty" }, i18n.noMentions) - ) - ) - ); - } - const filteredMessages = messages - .filter(msg => hasMention(msg, myFeedId)) - .sort((a, b) => (b.value.timestamp || 0) - (a.value.timestamp || 0)); - if (filteredMessages.length === 0) { - return template( - title, - section( - div({ class: "tags-header" }, - h2(title), - p(description) - ) - ), - section( - div({ class: "mentions-list" }, - p({ class: "empty" }, i18n.noMentions) - ) - ) - ); - } - return template( - title, - section( - div({ class: "tags-header" }, - h2(title), - p(description) - ) - ), - section( - div({ class: "mentions-list" }, - filteredMessages.map(renderMessage) - ) - ) - ); -}; -exports.privateView = async (messagesInput, filter, decrypted = null) => { +const { botOf: inboxBotOf, INBOX_BOTS } = require('../models/pm_model'); +const INBOX_BOT_ORDER = Object.keys(INBOX_BOTS).concat(['reminders']); +const inboxBotLabel = (bot) => ({ + blogs: i18n.pmBotBlogs, jobs: i18n.pmBotJobs, projects: i18n.pmBotProjects, market: i18n.pmBotMarket, shops: i18n.pmBotShops, + political: i18n.pmBotPolitical, banking: i18n.pmBotBanking, school: i18n.pmBotSchool, industry: i18n.pmBotIndustry, + housing: i18n.pmBotHousing, wiki: i18n.pmBotWiki, emergencies: i18n.pmBotEmergencies, podcasts: i18n.pmBotPodcasts, + campaigns: i18n.pmBotCampaigns, logistics: i18n.pmBotLogistics, reminders: i18n.privateReminders +})[bot] || bot; +exports.INBOX_BOT_ORDER = INBOX_BOT_ORDER; +exports.inboxBotLabel = inboxBotLabel; + +exports.privateView = async (messagesInput, filter, decrypted = null, notice = '', q = '') => { const messagesRaw = Array.isArray(messagesInput) ? messagesInput : messagesInput.messages - const messages = (messagesRaw || []).filter(m => m && m.key && m.value && m.value.content && m.value.content.type === 'post' && m.value.content.private === true) + const listTitles = (!Array.isArray(messagesInput) && messagesInput.listTitles && typeof messagesInput.listTitles === 'object') ? messagesInput.listTitles : {} + const readKeys = new Set((!Array.isArray(messagesInput) && Array.isArray(messagesInput.readKeys)) ? messagesInput.readKeys.map(String) : []) + const archivedKeys = new Set((!Array.isArray(messagesInput) && Array.isArray(messagesInput.archivedKeys)) ? messagesInput.archivedKeys.map(String) : []) + const mutedBots = new Set((!Array.isArray(messagesInput) && Array.isArray(messagesInput.mutedBots)) ? messagesInput.mutedBots.map(String) : []) + const botFilter = (!Array.isArray(messagesInput) && typeof messagesInput.bot === 'string' && INBOX_BOT_ORDER.includes(messagesInput.bot)) ? messagesInput.bot : '' + const sortMode = (!Array.isArray(messagesInput) && messagesInput.sort === 'recent') ? 'recent' : 'thread' + const isMutedMsg = m => { const b = inboxBotOf(m?.value?.content); return !!(b && mutedBots.has(b)) } + const messages = (messagesRaw || []).filter(m => m && m.key && m.value && m.value.content && m.value.content.type === 'post' && m.value.content.private === true).filter(m => !isMutedMsg(m)) + const qNorm = String(q || '').trim().toLowerCase() + const qMatch = (m) => { + if (!qNorm) return true + const c = m.value.content || {} + const ids = [c.from, m.value.author, ...(Array.isArray(c.to) ? c.to : [])].filter(Boolean) + return [c.subject, c.text, ...ids, ...ids.map(id => userLinkLabel(id))] + .some(v => String(v || '').toLowerCase().includes(qNorm)) + } const userId = await getUserId() const isSent = m => (m?.value?.author === userId) || (m?.value?.content?.from === userId) + const isList = m => !!(m?.value?.content?.list) const isToUser = m => Array.isArray(m?.value?.content?.to) && m.value.content.to.includes(userId) + const isReminder = m => { + const s = String(m?.value?.content?.subject || '') + return /^(Task Reminder:|Calendar Reminder:)/i.test(s) + } + const isNotification = m => !!inboxBotOf(m?.value?.content) + const isSelfNotice = m => isSent(m) && isToUser(m) && isNotification(m) + const isArchived = m => archivedKeys.has(String(m?.key)) + const toUserKeys = new Set(messages.filter(isToUser).map(m => String(m.key))) + const isUnreadKey = (key) => toUserKeys.has(String(key)) && !readKeys.has(String(key)) && !archivedKeys.has(String(key)) + const firstHref = (str) => { const m = String(str || '').match(/\]\((\/[^)\s]+)\)/); return m ? m[1] : null } const linkAuthor = (id) => userLink(id) const hrefFor = { job: (id) => `/jobs/${encodeURIComponent(id)}`, project: (id) => `/projects/${encodeURIComponent(id)}`, - market: (id) => `/market/${encodeURIComponent(id)}` + market: (id) => `/market/${encodeURIComponent(id)}`, + shopProduct: (id) => `/shops/product/${encodeURIComponent(id)}` } - const clickableCardProps = (href, extraClass = '') => { - const props = { class: `pm-card ${extraClass}` } - if (href) { - props.onclick = `window.location='${href}'` - props.tabindex = 0 - props.onkeypress = `if(event.key==='Enter') window.location='${href}'` - } - return props - } + const titleLink = (href, text) => href ? a({ href, class: 'pm-title-link' }, text) : text const chip = (txt) => span({ class: 'chip' }, txt) + const fmtBytes = (n) => { + const b = Number(n) || 0 + if (b < 1024) return `${b} B` + const units = ['KB', 'MB', 'GB', 'TB'] + let v = b / 1024, i = 0 + while (v >= 1024 && i < units.length - 1) { v /= 1024; i++ } + return `${v.toFixed(v >= 100 || i === 0 ? 0 : 1)} ${units[i]}` + } + const e2eChip = () => span({ class: 'pm-exposition-chip pm-exposition-encrypted' }, span({ class: 'pm-exposition-icon' }, '🔒'), span({ class: 'pm-exposition-text' }, i18n.encryptedChipLabel || 'E2E') @@ -3004,7 +4101,10 @@ exports.privateView = async (messagesInput, filter, decrypted = null) => { ), tr( td({ class: 'card-label' }, i18n.pmSubjectLabel || 'Subject:'), - td({ class: 'card-value' }, subject || i18n.pmNoSubject || '(no subject)') + td({ class: 'card-value pm-subject-cell' }, + span(subject || i18n.pmNoSubject || '(no subject)'), + isUnreadKey(msgKey) ? span({ class: 'pm-exposition-chip pm-unread-chip' }, span({ class: 'pm-exposition-text' }, i18n.inboxUnreadChip)) : null + ) ), ecoChip ? tr( @@ -3023,28 +4123,25 @@ exports.privateView = async (messagesInput, filter, decrypted = null) => { try { return Buffer.byteLength(JSON.stringify(m && m.value), 'utf8'); } catch (_) { return 0; } } - function bubbleMeta({ sentAt, subjectRaw, crypter, msgSize, msgKey }) { - const ecoChip = msgSize ? renderEcoTax(msgSize, msgKey) : null - return div({ class: 'pm-bubble-meta' }, - subjectRaw ? span({ class: 'pm-bubble-subject' }, subjectRaw) : null, - span({ class: 'pm-bubble-time' }, moment(sentAt).format('DD/MM HH:mm')), - span({ class: 'pm-bubble-lock', title: crypter ? '2xE2E' : 'E2E' }, crypter ? '🔒🔒' : '🔒'), - ecoChip ? span({ class: 'pm-bubble-eco' }, ecoChip) : null - ) - } - - function actions({ key, replyId, subjectRaw, text }) { - const stop = { onclick: 'event.stopPropagation()' } + function actions({ key, replyId, subjectRaw, text, extra = null }) { const subjectReply = /^(\s*RE:\s*)/i.test(subjectRaw || '') ? (subjectRaw || '') : `RE: ${subjectRaw || ''}` const isSelf = replyId === userId return div({ class: 'pm-actions' }, - isSelf ? null : form({ method: 'GET', action: '/pm', class: 'pm-action-form', ...stop }, + isSelf ? null : form({ method: 'GET', action: '/pm', class: 'pm-action-form' }, input({ type: 'hidden', name: 'recipients', value: replyId }), input({ type: 'hidden', name: 'subject', value: subjectReply }), input({ type: 'hidden', name: 'quote', value: text || '' }), button({ type: 'submit', class: 'pm-btn reply-btn' }, i18n.pmReply.toUpperCase()) ), - form({ method: 'POST', action: `/inbox/delete/${encodeURIComponent(key)}`, class: 'pm-action-form', ...stop }, + isSelf ? null : renderCallButton(replyId, { upper: true, cls: 'pm-btn' }), + extra || null, + toUserKeys.has(String(key)) && !archivedKeys.has(String(key)) ? form({ method: 'POST', action: `/inbox/${readKeys.has(String(key)) ? 'unread' : 'read'}/${encodeURIComponent(key)}`, class: 'pm-action-form' }, + button({ type: 'submit', class: 'pm-btn read-btn' }, String(readKeys.has(String(key)) ? i18n.inboxMarkUnread : i18n.inboxMarkRead).toUpperCase()) + ) : null, + toUserKeys.has(String(key)) ? form({ method: 'POST', action: `/inbox/${archivedKeys.has(String(key)) ? 'unarchive' : 'archive'}/${encodeURIComponent(key)}`, class: 'pm-action-form' }, + button({ type: 'submit', class: 'pm-btn archive-btn' }, String(archivedKeys.has(String(key)) ? i18n.inboxUnarchive : i18n.inboxArchive).toUpperCase()) + ) : null, + form({ method: 'POST', action: `/inbox/delete/${encodeURIComponent(key)}`, class: 'pm-action-form' }, button({ type: 'submit', class: 'pm-btn delete-btn danger-btn' }, i18n.privateDelete.toUpperCase()) ) ) @@ -3061,7 +4158,9 @@ exports.privateView = async (messagesInput, filter, decrypted = null) => { } function threadId(m) { - return canonicalSubject(m?.value?.content?.subject || '') + '||' + participantsKey(m) + const c = m?.value?.content || {} + if (c.list) return canonicalSubject(c.subject || '') + '||list:' + String(c.thread || c.list) + return canonicalSubject(c.subject || '') + '||' + participantsKey(m) } function threadLevel(s) { @@ -3087,6 +4186,10 @@ exports.privateView = async (messagesInput, filter, decrypted = null) => { const m = str.match(/\/market\/([%A-Za-z0-9/+._=-]+\.sha256)/) return m ? m[1] : '' } + if (kind === 'shopProduct') { + const m = str.match(/\/shops\/product\/([%A-Za-z0-9/+._=-]+\.sha256)/) + return m ? m[1] : '' + } return '' } @@ -3109,15 +4212,27 @@ exports.privateView = async (messagesInput, filter, decrypted = null) => { } } flushQuote() - return parts.join('
') - .replace(/(@[a-zA-Z0-9/+._=-]+\.ed25519)/g, (match, id) => `${userLinkLabel(id)}`) + const mdLinks = [] + const masked = parts.join('
') + .replace(/\[([^\]\n]{1,120})\]\((https?:\/\/[^)\s]+|\/(?![\/\\])[^)\s]*)\)/g, (match, label, href) => { + const idx = mdLinks.length + const safeHref = String(href).replace(/"/g, '%22') + const ext = /^https?:/.test(href) ? ' target="_blank" rel="noopener noreferrer"' : '' + mdLinks.push(`${label}`) + return `\u0000MD${idx}\u0000` + }) + return masked + .replace(/(@[a-zA-Z0-9/+._=-]+\.ed25519)/g, (match, id) => `${escapeHtml(userLinkLabel(id))}`) .replace(/\/jobs\/([%A-Za-z0-9/+._=-]+\.sha256)/g, (match, id) => `${match}`) .replace(/\/projects\/([%A-Za-z0-9/+._=-]+\.sha256)/g, (match, id) => `${match}`) .replace(/\/market\/([%A-Za-z0-9/+._=-]+\.sha256)/g, (match, id) => `${match}`) .replace(/\/calendars\/([%A-Za-z0-9/+._=-]+\.sha256)/g, (match, id) => `${match}`) .replace(/\/ai\/ask\?[^\s<"]+/g, (match) => `${match}`) - .replace(/(? `${match}`) + .replace(/\/tribe\/([%A-Za-z0-9/+._=-]+\.sha256)(\?section=[a-zA-Z]+)?/g, (match) => `${match}`) + .replace(/\/larp\/([a-zA-Z]+)/g, (match) => `${match}`) + .replace(/(? `${match}`) .replace(/(https?:\/\/[^\s<"]+)/g, (match) => `${match}`) + .replace(/\u0000MD(\d+)\u0000/g, (m, i) => mdLinks[Number(i)] !== undefined ? mdLinks[Number(i)] : m) } const threads = {} @@ -3130,24 +4245,39 @@ exports.privateView = async (messagesInput, filter, decrypted = null) => { const inboxSet = new Set() for (const arr of Object.values(threads)) { for (const m of arr) { - if (!isSent(m) && isToUser(m)) inboxSet.add(m) + if ((!isSent(m) || isSelfNotice(m)) && isToUser(m) && !isArchived(m)) inboxSet.add(m) } } - const isReminder = m => { - const s = String(m?.value?.content?.subject || '') - return /^(Task Reminder:|Calendar Reminder:)/i.test(s) - } + const received = Array.from(inboxSet) + const archivedMsgs = messages.filter(isArchived) + const notifications = messages.filter(m => isNotification(m) && !isArchived(m) && (!isSent(m) || isReminder(m) || isSelfNotice(m))) + const mailing = messages.filter(m => isList(m) && !isArchived(m)) + const botCounts = {} + for (const m of notifications) { const b = inboxBotOf(m.value.content); if (b) botCounts[b] = (botCounts[b] || 0) + 1 } + const botsPresent = INBOX_BOT_ORDER.filter(b => botCounts[b] > 0) - const data = - filter === 'sent' ? messages.filter(m => isSent(m) && !isReminder(m)) : - filter === 'reminders' ? messages.filter(isReminder) : - filter === 'inbox' ? Array.from(inboxSet).filter(m => !isReminder(m)) : - messages + const data = ( + filter === 'sent' ? messages.filter(m => isSent(m) && !isReminder(m) && !isSelfNotice(m) && !isArchived(m)) : + filter === 'reminders' ? messages.filter(m => isReminder(m) && !isArchived(m)) : + filter === 'pms' ? received.filter(m => !isNotification(m) && !isList(m)) : + filter === 'notifications' ? (botFilter ? notifications.filter(m => inboxBotOf(m.value.content) === botFilter) : notifications) : + filter === 'mailing' ? mailing : + filter === 'archived' ? archivedMsgs : + filter === 'inbox' ? received.filter(m => !isReminder(m)) : + messages.filter(m => !isArchived(m)) + ).filter(qMatch) - const inboxCount = Array.from(inboxSet).filter(m => !isReminder(m)).length - const sentCount = messages.filter(m => isSent(m) && !isReminder(m)).length - const reminderCount = messages.filter(isReminder).length + const inboxCount = received.filter(m => !isReminder(m)).length + const sentCount = messages.filter(m => isSent(m) && !isReminder(m) && !isSelfNotice(m) && !isArchived(m)).length + const pmCount = received.filter(m => !isNotification(m) && !isList(m)).length + const notifCount = notifications.length + const mailingCount = mailing.length + const archivedCount = archivedMsgs.length + const emptyInbox = messages.length === 0 && !qNorm + const shownKeys = data.map(m => String(m.key)) + const unreadShown = shownKeys.filter(k => isUnreadKey(k)) + const bulkDeletable = filter === 'notifications' || filter === 'reminders' || filter === 'archived' const sorted = [...data].sort((a, b) => { const ta = threadId(a) @@ -3167,9 +4297,9 @@ exports.privateView = async (messagesInput, filter, decrypted = null) => { const jobId = pickLink(text, 'job') const href = jobId ? hrefFor.job(jobId) : null return div( - clickableCardProps(href, `job-notification thread-level-0`), - headerLine({ sentAt, from, toLinks, subject: type, msgKey: key, msgSize }), - h2({ class: 'pm-title' }, `${icon} ${i18n.pmBotJobs} · ${titleH}`), + { class: 'pm-card job-notification thread-level-0' }, + headerLine({ sentAt, from, toLinks, subject: titleH, msgKey: key, msgSize }), + h2({ class: 'pm-title' }, `${icon} ${i18n.pmBotJobs} · `, titleLink(href, titleH)), p( i18n.pmInhabitantWithId, ' ', linkAuthor(from), ' ', @@ -3191,9 +4321,9 @@ exports.privateView = async (messagesInput, filter, decrypted = null) => { const projectId = pickLink(text, 'project') const href = projectId ? hrefFor.project(projectId) : null return div( - clickableCardProps(href, `project-${isFollow ? 'follow' : 'unfollow'}-notification thread-level-0`), - headerLine({ sentAt, from, toLinks, subject: type, msgKey: key, msgSize }), - h2({ class: 'pm-title' }, `${icon} ${i18n.pmBotProjects} · ${titleH}`), + { class: `pm-card project-${isFollow ? 'follow' : 'unfollow'}-notification thread-level-0` }, + headerLine({ sentAt, from, toLinks, subject: titleH, msgKey: key, msgSize }), + h2({ class: 'pm-title' }, `${icon} ${i18n.pmBotProjects} · `, titleLink(href, titleH)), p( i18n.pmInhabitantWithId, ' ', userLink(from), @@ -3206,23 +4336,28 @@ exports.privateView = async (messagesInput, filter, decrypted = null) => { ) } - function MarketSoldCard({ sentAt, from, toLinks, subject, text, key, msgSize }) { + function MarketSoldCard({ sentAt, from, toLinks, subject, text, key, msgSize, variant = 'market' }) { + const isShop = variant === 'shop' const itemTitle = quoted(subject) || quoted(text) || 'item' const buyerId = (text.match(/OASIS ID:\s*([\w=/+.-]+)/) || [])[1] || from - const price = (text.match(/for:\s*\$([\d.]+)/) || [])[1] || '' + const price = (text.match(/for:\s*\$?([\d.]+)/) || [])[1] || '' const marketId = pickLink(text, 'market') - const href = marketId ? hrefFor.market(marketId) : null + const shopProductId = pickLink(text, 'shopProduct') + const href = isShop ? (shopProductId ? hrefFor.shopProduct(shopProductId) : null) : (marketId ? hrefFor.market(marketId) : null) + const icon = isShop ? '🛍️' : '💰' + const botLabel = isShop ? i18n.pmBotShops : i18n.pmBotMarket + const soldTitle = isShop ? (i18n.inboxShopSoldTitle || 'Product Sold') : i18n.inboxMarketItemSoldTitle return div( - clickableCardProps(href, 'market-sold-notification thread-level-0'), - headerLine({ sentAt, from, toLinks, subject, msgKey: key, msgSize }), - h2({ class: 'pm-title' }, `💰 ${i18n.pmBotMarket} · ${i18n.inboxMarketItemSoldTitle}`), + { class: `pm-card ${isShop ? 'shop-sold-notification' : 'market-sold-notification'} thread-level-0` }, + headerLine({ sentAt, from, toLinks, subject: soldTitle, msgKey: key, msgSize }), + h2({ class: 'pm-title' }, `${icon} ${botLabel} · `, titleLink(href, soldTitle)), p( i18n.pmYourItem, ' ', - href ? a({ class: 'market-link', href }, `"${itemTitle}"`) : `"${itemTitle}"`, + href ? a({ class: isShop ? 'shop-link' : 'market-link', href }, `"${itemTitle}"`) : `"${itemTitle}"`, ' ', i18n.pmHasBeenSoldTo, ' ', linkAuthor(buyerId), - price ? ` ${i18n.pmFor} $${price}.` : '.' + price ? ` ${i18n.pmFor} ${price} ECO.` : '.' ), actions({ key, replyId: buyerId, subjectRaw: itemTitle, text }) ) @@ -3234,9 +4369,9 @@ exports.privateView = async (messagesInput, filter, decrypted = null) => { const projectId = content.meta?.projectId ?? pickLink(text, 'project') const href = projectId ? hrefFor.project(projectId) : null return div( - clickableCardProps(href, 'project-pledge-notification thread-level-0'), - headerLine({ sentAt, from, toLinks, subject: 'PROJECT_PLEDGE', msgKey: key, msgSize }), - h2({ class: 'pm-title' }, `💚 ${i18n.pmBotProjects} · ${i18n.inboxProjectPledgedTitle}`), + { class: 'pm-card project-pledge-notification thread-level-0' }, + headerLine({ sentAt, from, toLinks, subject: i18n.inboxProjectPledgedTitle, msgKey: key, msgSize }), + h2({ class: 'pm-title' }, `💚 ${i18n.pmBotProjects} · `, titleLink(href, i18n.inboxProjectPledgedTitle)), p( i18n.pmInhabitantWithId, ' ', linkAuthor(from), ' ', @@ -3249,64 +4384,323 @@ exports.privateView = async (messagesInput, filter, decrypted = null) => { ) } + function PoliticalBotCard({ subjectU, sentAt, from, toLinks, text, key, msgSize }) { + const titleMap = { + LARP_RULING: i18n.politicalBotLarpTitle || 'The ruling house of the L.A.R.P has changed.', + PARLIAMENT_GOV: i18n.politicalBotParliamentTitle || 'A new government cycle has begun in the Parliament.', + TRIBE_GOV: i18n.politicalBotTribeTitle || 'A new government cycle has begun in one of your Tribes.' + } + const title = titleMap[subjectU] || (i18n.pmBotPolitical || 'PoliticalBot') + const href = firstHref(text) + return div( + { class: 'pm-card political-bot-notification thread-level-0' }, + headerLine({ sentAt, from, toLinks, subject: title, msgKey: key, msgSize }), + h2({ class: 'pm-title' }, `🏛️ ${i18n.pmBotPolitical} · `, href ? a({ href, class: 'pm-title-link' }, title) : title), + div({ class: 'message-text', [RAW_HTML]: sanitizeHtml(clickableLinks(text || '')) }), + actions({ key, replyId: from, subjectRaw: title, text }) + ) + } + + function IndustryBotCard({ subjectU, sentAt, from, toLinks, text, key, msgSize }) { + const titleMap = { + INDUSTRY_ADMITTED: i18n.industryBotAdmittedTitle || 'You have been admitted to a facility.', + INDUSTRY_APPLICATION: i18n.industryBotApplicationTitle || 'New membership application in your facility.', + INDUSTRY_INVITED: i18n.industryBotInvitedTitle || 'You have been invited to a facility.', + INDUSTRY_DISSOLVED: i18n.industryBotDissolvedTitle || 'A facility you belong to has been dissolved.', + INDUSTRY_BUILD_APPROVED: i18n.industryBotBuildApprovedTitle || 'A build has been approved.', + INDUSTRY_DISTRIBUTED: i18n.industryBotDistributedTitle || 'A build output has been distributed.' + } + const title = titleMap[subjectU] || (i18n.pmBotIndustry || 'IndustryBot') + return div( + { class: 'pm-card industry-bot-notification thread-level-0' }, + headerLine({ sentAt, from, toLinks, subject: title, msgKey: key, msgSize }), + h2({ class: 'pm-title' }, `🏭 ${i18n.pmBotIndustry || 'IndustryBot'} · ${title}`), + div({ class: 'message-text', [RAW_HTML]: sanitizeHtml(clickableLinks(text || '')) }), + actions({ key, replyId: from, subjectRaw: title, text }) + ) + } + + function BankingBotCard({ subjectU, sentAt, from, toLinks, text, key, msgSize }) { + const title = subjectU === 'BANKING_UBI_PAID' + ? (i18n.bankingBotUbiPaidTitle || 'You have received your UBI.') + : subjectU === 'BANKING_UBI_AVAILABLE' + ? (i18n.bankingBotUbiAvailableTitle || 'You can claim your UBI.') + : subjectU === 'BANKING_CONFIRM_PENDING' + ? (i18n.bankingBotConfirmTitle || 'You have transfers awaiting your confirmation.') + : (i18n.bankingBotPaymentTitle || 'You have received a payment.') + const href = firstHref(text) + return div( + { class: 'pm-card banking-bot-notification thread-level-0' }, + headerLine({ sentAt, from, toLinks, subject: title, msgKey: key, msgSize }), + h2({ class: 'pm-title' }, `💰 ${i18n.pmBotBanking || 'BankingBot'} · `, href ? a({ href, class: 'pm-title-link' }, title) : title), + div({ class: 'message-text', [RAW_HTML]: sanitizeHtml(clickableLinks(text || '')) }), + actions({ key, replyId: from, subjectRaw: title, text }) + ) + } + + function SchoolBotCard({ subjectU, sentAt, from, toLinks, text, key, msgSize }) { + const titleMap = { + SCHOOL_ENROLLED: i18n.schoolBotEnrolledTitle || 'A new student has enrolled in your course.', + SCHOOL_INVITED: i18n.schoolBotInvitedTitle || 'You have been invited to a course.', + SCHOOL_ADMITTED: i18n.schoolBotAdmittedTitle || 'You have been admitted to a course.', + SCHOOL_CERTIFICATE: i18n.schoolBotCertificateTitle || 'You have received a certificate.', + SCHOOL_PASSED: i18n.schoolBotPassedTitle || 'A student has passed your course.', + SCHOOL_LESSON_NEW: i18n.schoolBotLessonTitle || 'A new lesson has been published.' + } + const title = titleMap[subjectU] || (i18n.pmBotSchool || 'EducaBot') + return div( + { class: 'pm-card school-bot-notification thread-level-0' }, + headerLine({ sentAt, from, toLinks, subject: title, msgKey: key, msgSize }), + h2({ class: 'pm-title' }, `🎓 ${i18n.pmBotSchool || 'EducaBot'} · ${title}`), + div({ class: 'message-text', [RAW_HTML]: sanitizeHtml(clickableLinks(text || '')) }), + actions({ key, replyId: from, subjectRaw: title, text }) + ) + } + + function EmergencyBotCard({ subjectU, sentAt, from, toLinks, text, key, msgSize }) { + const titleMap = { + EMERGENCY_UPDATED: i18n.emergencyBotUpdatedTitle || 'An emergency you follow has an update.', + EMERGENCY_RESOLVED: i18n.emergencyBotResolvedTitle || 'An emergency you follow has been resolved.' + } + const title = titleMap[subjectU] || (i18n.pmBotEmergencies || 'EmergencyBot') + return div( + { class: 'pm-card emergency-bot-notification thread-level-0' }, + headerLine({ sentAt, from, toLinks, subject: title, msgKey: key, msgSize }), + h2({ class: 'pm-title' }, `🚨 ${i18n.pmBotEmergencies || 'EmergencyBot'} · ${title}`), + div({ class: 'message-text', [RAW_HTML]: sanitizeHtml(clickableLinks(text || '')) }), + actions({ key, replyId: from, subjectRaw: title, text }) + ) + } + + function MailingCard({ sentAt, from, toLinks, subject, text, key, msgSize, content }) { + const listId = String(content.list || '') + const info = listTitles[listId] || null + const listHref = `/mailing/${encodeURIComponent(listId)}` + const listName = info && info.title ? info.title : (i18n.mailingTitle || 'Mailing list') + const subjectReply = /^(\s*RE:\s*)/i.test(subject || '') ? (subject || '') : `RE: ${subject || ''}` + const canReply = !!info && info.status === 'ACTIVE' + const replyForm = canReply + ? form({ method: 'POST', action: `${listHref}/message`, class: 'pm-action-form mailing-inbox-reply' }, + input({ type: 'hidden', name: 'thread', value: String(content.thread || content.mid || '') }), + input({ type: 'hidden', name: 'subject', value: subjectReply }), + input({ type: 'hidden', name: 'returnTo', value: '/inbox?filter=mailing' }), + textarea({ name: 'text', rows: 2, maxlength: String(require('../backend/long_text').TEXT_CAP), required: true, placeholder: i18n.mailingReplyPlaceholder || 'Reply to the list...', class: 'mailing-inbox-reply-text' }), + button({ type: 'submit', class: 'pm-btn reply-btn' }, String(i18n.mailingReplyToList || 'Reply to list').toUpperCase()) + ) + : null + return div( + { class: 'pm-card mailing-pm' }, + headerLine({ sentAt, from, toLinks, subject, msgKey: key, msgSize }), + h2({ class: 'pm-title' }, '✉ ', a({ href: listHref, class: 'mailing-inbox-list' }, listName)), + div({ class: 'message-text', [RAW_HTML]: sanitizeHtml(clickableLinks(text || '')) }), + actions({ key, replyId: userId, subjectRaw: subject, text, extra: replyForm }) + ) + } + + function CampaignBotCard({ subjectU, sentAt, from, toLinks, text, key, msgSize }) { + const titleMap = { + CAMPAIGN_UPDATED: i18n.campaignBotUpdatedTitle || 'A campaign you follow has an update.', + CAMPAIGN_ACHIEVED: i18n.campaignBotAchievedTitle || 'A campaign you follow has reached its goal.', + CAMPAIGN_RAISED: i18n.campaignBotRaisedTitle || 'A campaign you signed has been raised to Parliament.' + } + const title = titleMap[subjectU] || (i18n.pmBotCampaigns || 'CampaignBot') + return div( + { class: 'pm-card campaign-bot-notification thread-level-0' }, + headerLine({ sentAt, from, toLinks, subject: title, msgKey: key, msgSize }), + h2({ class: 'pm-title' }, `✍ ${i18n.pmBotCampaigns || 'CampaignBot'} · ${title}`), + div({ class: 'message-text', [RAW_HTML]: sanitizeHtml(clickableLinks(text || '')) }), + actions({ key, replyId: from, subjectRaw: title, text }) + ) + } + + function LogisticsBotCard({ subjectU, sentAt, from, toLinks, text, key, msgSize }) { + const titleMap = { + LOGISTICS_UPDATED: i18n.logisticsBotUpdatedTitle || 'A route you booked has changed.', + LOGISTICS_CLOSED: i18n.logisticsBotClosedTitle || 'A route you booked has been closed.', + LOGISTICS_BOOKED: i18n.logisticsBotBookedTitle || 'Someone booked your route.', + LOGISTICS_CANCELLED: i18n.logisticsBotCancelledTitle || 'A booking on your route was cancelled.', + LOGISTICS_CONFIRMED: i18n.logisticsBotConfirmedTitle || 'Your booking was confirmed: the package is on its way.', + LOGISTICS_REJECTED: i18n.logisticsBotRejectedTitle || 'Your booking was rejected.', + LOGISTICS_DELIVERED: i18n.logisticsBotDeliveredTitle || 'Your package has arrived.' + } + const title = titleMap[subjectU] || (i18n.pmBotLogistics || 'LogisticsBot') + return div( + { class: 'pm-card logistics-bot-notification thread-level-0' }, + headerLine({ sentAt, from, toLinks, subject: title, msgKey: key, msgSize }), + h2({ class: 'pm-title' }, `${i18n.pmBotLogistics || 'LogisticsBot'} · ${title}`), + div({ class: 'message-text', [RAW_HTML]: sanitizeHtml(clickableLinks(text || '')) }), + actions({ key, replyId: from, subjectRaw: title, text }) + ) + } + + function BlogBotCard({ sentAt, from, toLinks, text, key, msgSize }) { + const title = i18n.blogBotNewTitle + return div( + { class: 'pm-card blog-bot-notification thread-level-0' }, + headerLine({ sentAt, from, toLinks, subject: title, msgKey: key, msgSize }), + h2({ class: 'pm-title' }, `📝 ${i18n.pmBotBlogs} · `, titleLink(firstHref(text), title)), + div({ class: 'message-text', [RAW_HTML]: sanitizeHtml(clickableLinks(text || '')) }), + actions({ key, replyId: from, subjectRaw: title, text }) + ) + } + + function PodcastBotCard({ sentAt, from, toLinks, text, key, msgSize }) { + const title = i18n.podcastBotEpisodeTitle || 'A podcast you follow has a new episode.' + return div( + { class: 'pm-card podcast-bot-notification thread-level-0' }, + headerLine({ sentAt, from, toLinks, subject: title, msgKey: key, msgSize }), + h2({ class: 'pm-title' }, `${i18n.pmBotPodcasts || 'PodcastBot'} · ${title}`), + div({ class: 'message-text', [RAW_HTML]: sanitizeHtml(clickableLinks(text || '')) }), + actions({ key, replyId: from, subjectRaw: title, text }) + ) + } + + function WikiBotCard({ subjectU, sentAt, from, toLinks, text, key, msgSize }) { + const titleMap = { + WIKI_EDITED: i18n.wikiBotEditedTitle || 'A wiki page you follow has been edited.', + WIKI_RESTORED: i18n.wikiBotRestoredTitle || 'An earlier version of a wiki page you follow has been restored.' + } + const title = titleMap[subjectU] || (i18n.pmBotWiki || 'WikiBot') + return div( + { class: 'pm-card wiki-bot-notification thread-level-0' }, + headerLine({ sentAt, from, toLinks, subject: title, msgKey: key, msgSize }), + h2({ class: 'pm-title' }, `📖 ${i18n.pmBotWiki || 'WikiBot'} · ${title}`), + div({ class: 'message-text', [RAW_HTML]: sanitizeHtml(clickableLinks(text || '')) }), + actions({ key, replyId: from, subjectRaw: title, text }) + ) + } + + function JobsBotCard({ sentAt, from, toLinks, text, key, msgSize }) { + const title = i18n.jobsBotMatchTitle + return div( + { class: 'pm-card jobs-bot-notification thread-level-0' }, + headerLine({ sentAt, from, toLinks, subject: title, msgKey: key, msgSize }), + h2({ class: 'pm-title' }, `💼 ${i18n.pmBotJobs} · ${title}`), + div({ class: 'message-text', [RAW_HTML]: sanitizeHtml(clickableLinks(text || '')) }), + actions({ key, replyId: from, subjectRaw: title, text }) + ) + } + + function HousingBotCard({ subjectU, sentAt, from, toLinks, text, key, msgSize }) { + const myId = (config.keys && config.keys.id) ? config.keys.id : '' + const mine = String(from) === String(myId) + const titleMap = mine + ? { + HOUSING_REQUESTED: i18n.housingBotYouRequestedTitle || 'You have requested a place.', + HOUSING_CANCELLED: i18n.housingBotYouCancelledTitle || 'You have cancelled a request.', + HOUSING_UNAVAILABLE: i18n.housingBotUnavailableSentTitle || 'You have told the people who requested this place.' + } + : { + HOUSING_REQUESTED: i18n.housingBotRequestedTitle || 'Somebody has requested one of your places.', + HOUSING_CANCELLED: i18n.housingBotCancelledTitle || 'A request on one of your places was cancelled.', + HOUSING_UNAVAILABLE: i18n.housingBotUnavailableTitle || 'A place you requested is no longer available.' + } + const title = titleMap[subjectU] || (i18n.pmBotHousing || 'HousingBot') + return div( + { class: 'pm-card housing-bot-notification thread-level-0' }, + headerLine({ sentAt, from, toLinks, subject: title, msgKey: key, msgSize }), + h2({ class: 'pm-title' }, `🏠 ${i18n.pmBotHousing || 'HousingBot'} · ${title}`), + div({ class: 'message-text', [RAW_HTML]: sanitizeHtml(clickableLinks(text || '')) }), + actions({ key, replyId: from, subjectRaw: title, text }) + ) + } + const { renderEncryptedChip } = require('./clearnet_view'); return template( i18n.private, section( - div({ class: 'tags-header' }, - div({ class: 'title-with-chip' }, h2(i18n.private), renderEncryptedChip(i18n)), - p(i18n.privateDescription) + div({ class: 'tags-header module-header-line' }, + h2(i18n.private), + p(i18n.privateDescription), + renderEncryptedChip(i18n) ), - (() => { - const pmVis = getConfig().pmVisibility === 'mutuals' ? 'mutuals' : 'whole' - const pmVisLabel = pmVis === 'mutuals' ? i18n.settingsPmVisibilityMutuals : i18n.settingsPmVisibilityWhole - const pmVisIcon = pmVis === 'mutuals' ? '🤝' : '🌐' - const nextVis = pmVis === 'mutuals' ? 'whole' : 'mutuals' - const nextLabel = nextVis === 'mutuals' - ? (i18n.inboxToggleToMutuals || 'Switch to mutuals') - : (i18n.inboxToggleToWhole || 'Switch to whole') - return div({ class: 'pm-exposition inbox-exposition' }, - span({ class: 'inbox-filters-label' }, i18n.inboxFiltersLabel || 'Filters:'), - span({ class: `pm-exposition-chip pm-exposition-${pmVis}` }, - span({ class: 'pm-exposition-icon' }, pmVisIcon), - span({ class: 'pm-exposition-text' }, pmVisLabel) - ), - form({ method: 'POST', action: '/settings/pm-visibility?returnTo=/inbox', class: 'inbox-vis-toggle' }, - input({ type: 'hidden', name: 'pmVisibility', value: nextVis }), - button({ type: 'submit', class: 'btn' }, nextLabel) - ) - ) - })(), div({ class: 'filters' }, form({ method: 'GET', action: '/inbox' }, [ + ...(emptyInbox ? [] : [ button({ type: 'submit', name: 'filter', value: 'inbox', class: filter === 'inbox' ? 'filter-btn active' : 'filter-btn' - }, `${i18n.privateInbox} (${inboxCount})`), - button({ + }, `${String(i18n.allButton || 'ALL').toUpperCase()} (${inboxCount})`), + (pmCount > 0 || filter === 'pms') ? button({ type: 'submit', name: 'filter', - value: 'reminders', - class: filter === 'reminders' ? 'filter-btn active' : 'filter-btn' - }, `${i18n.privateReminders || 'Reminders'} (${reminderCount})`), - button({ + value: 'pms', + class: filter === 'pms' ? 'filter-btn active' : 'filter-btn' + }, `${String(i18n.inboxFilterPms || 'PMs').toUpperCase()} (${pmCount})`) : null, + (mailingCount > 0 || filter === 'mailing') ? button({ + type: 'submit', + name: 'filter', + value: 'mailing', + class: filter === 'mailing' ? 'filter-btn active' : 'filter-btn' + }, `${String(i18n.inboxFilterMailing || 'Mailing lists').toUpperCase()} (${mailingCount})`) : null, + (sentCount > 0 || filter === 'sent') ? button({ type: 'submit', name: 'filter', value: 'sent', class: filter === 'sent' ? 'filter-btn active' : 'filter-btn' - }, `${i18n.privateSent} (${sentCount})`), + }, `${String(i18n.privateSent || 'SENT').toUpperCase()} (${sentCount})`) : null, + (notifCount > 0 || filter === 'notifications' || filter === 'reminders') ? button({ + type: 'submit', + name: 'filter', + value: 'notifications', + class: (filter === 'notifications' || filter === 'reminders') ? 'filter-btn active' : 'filter-btn' + }, `${String(i18n.inboxFilterNotifications || 'Notifications').toUpperCase()} (${notifCount})`) : null, + (archivedCount > 0 || filter === 'archived') ? button({ + type: 'submit', + name: 'filter', + value: 'archived', + class: filter === 'archived' ? 'filter-btn active' : 'filter-btn' + }, `${String(i18n.inboxFilterArchived).toUpperCase()} (${archivedCount})`) : null, + ]), button({ type: 'submit', name: 'filter', value: 'create', class: 'create-button', formaction: '/pm', - formmethod: 'GET' + attrs: { formmethod: 'GET' } }, i18n.pmCreateButton) ]) ), + (filter === 'notifications' && botsPresent.length) ? div({ class: 'filters inbox-bot-filters' }, + form({ method: 'GET', action: '/inbox' }, + input({ type: 'hidden', name: 'filter', value: 'notifications' }), + sortMode === 'recent' ? input({ type: 'hidden', name: 'sort', value: 'recent' }) : null, + button({ type: 'submit', name: 'bot', value: '', class: botFilter ? 'filter-btn' : 'filter-btn active' }, `${String(i18n.inboxAllBots).toUpperCase()} (${notifCount})`), + ...botsPresent.map(b => button({ type: 'submit', name: 'bot', value: b, class: botFilter === b ? 'filter-btn active' : 'filter-btn' }, `${String(inboxBotLabel(b)).toUpperCase()} (${botCounts[b]})`)) + ) + ) : null, + emptyInbox ? null : div({ class: 'filters activity-filter-chips activity-toolbar-row' }, + (() => { + return div({ class: 'pm-exposition inbox-exposition' }, + span({ class: 'inbox-filters-label' }, i18n.inboxFiltersLabel || 'Filters:'), + form({ method: 'GET', action: '/inbox', class: 'inbox-vis-toggle' }, + input({ type: 'hidden', name: 'filter', value: filter || 'inbox' }), + botFilter ? input({ type: 'hidden', name: 'bot', value: botFilter }) : null, + qNorm ? input({ type: 'hidden', name: 'q', value: String(q || '') }) : null, + sortMode === 'recent' ? null : input({ type: 'hidden', name: 'sort', value: 'recent' }), + button({ type: 'submit', class: 'btn' }, sortMode === 'recent' ? i18n.inboxSortThread : i18n.inboxSortRecent) + ), + unreadShown.length ? form({ method: 'POST', action: '/inbox/read-all', class: 'inbox-vis-toggle' }, + ...unreadShown.map(k => input({ type: 'hidden', name: 'keys', value: k })), + button({ type: 'submit', class: 'btn' }, `${i18n.inboxMarkAllRead} (${unreadShown.length})`) + ) : null, + (bulkDeletable && shownKeys.length) ? form({ method: 'POST', action: '/inbox/delete-many', class: 'inbox-vis-toggle inbox-bulk-delete' }, + ...shownKeys.map(k => input({ type: 'hidden', name: 'keys', value: k })), + button({ type: 'submit', class: 'btn delete-btn' }, `${String(i18n.inboxDeleteShown).toUpperCase()} (${shownKeys.length})`) + ) : null + ) + })(), + form({ method: 'GET', action: '/inbox', class: 'filter-box' }, + input({ type: 'hidden', name: 'filter', value: filter || 'inbox' }), + botFilter ? input({ type: 'hidden', name: 'bot', value: botFilter }) : null, + sortMode === 'recent' ? input({ type: 'hidden', name: 'sort', value: 'recent' }) : null, + input({ type: 'text', name: 'q', value: String(q || ''), placeholder: i18n.inboxSearchPlaceholder || 'Search in Inbox...', class: 'filter-box__input' }), + button({ type: 'submit', class: 'filter-box__button' }, i18n.searchButton) + ) + ), div({ class: 'message-list' }, (() => { function renderMsg(msg) { @@ -3316,12 +4710,43 @@ exports.privateView = async (messagesInput, filter, decrypted = null) => { const subjectU = subjectRaw.toUpperCase() const text = content.text || '' const sentAt = new Date(content.sentAt || msg.timestamp) - const fromResolved = content.from || author + const fromResolved = author const toLinks = Array.isArray(content.to) ? content.to.map(addr => linkAuthor(addr)) : [] const level = threadLevel(subjectRaw) const msgSize = msgSizeBytes(msg) - const isMob = process.env.OASIS_MOBILE === '1' + if (content.fileShare && typeof content.fileShare === 'object') { + const fsp = content.fileShare + const dblKey = (decrypted && decrypted.key === msg.key) ? decrypted : null + return div( + { class: 'pm-card normal-pm pm-fileshare-card' }, + headerLine({ sentAt, from: fromResolved, toLinks, subject: subjectRaw, msgKey: msg.key, msgSize, crypter: !!fsp.crypter }), + div({ class: 'pm-fileshare-info' }, + span({ class: 'pm-fileshare-icon' }, '📎'), + span({ class: 'pm-fileshare-name' }, String(fsp.filename || 'file')), + span({ class: 'pm-fileshare-meta' }, `${fmtBytes(fsp.size)} · ${String(fsp.mime || 'application/octet-stream')}`) + ), + fsp.crypter + ? form({ method: 'POST', action: `/inbox/file/${encodeURIComponent(msg.key)}`, class: 'pm-fileshare-download-form' }, + dblKey && dblKey.error ? div({ class: 'pm-form-error-msg' }, p('✗ ' + (i18n.pmCrypterBadKey || 'Your shared key is incorrect!'))) : null, + input({ type: 'text', name: 'key', placeholder: 'd66d7d32f4d30f34812aee3d01347154', minlength: 32, maxlength: 32, size: 34, required: true, class: 'pm-crypter-key-input' }), + button({ type: 'submit', class: 'pm-btn pm-fileshare-download' }, (i18n.fileShareDownload || 'Download').toUpperCase()) + ) + : null, + actions({ + key: msg.key, replyId: fromResolved, subjectRaw, text: '', + extra: fsp.crypter + ? null + : form({ method: 'GET', action: `/inbox/file/${encodeURIComponent(msg.key)}`, class: 'pm-action-form' }, + button({ type: 'submit', class: 'pm-btn pm-fileshare-download' }, (i18n.fileShareDownload || 'Download').toUpperCase()) + ) + }) + ) + } + + if (subjectU === 'JOB_MATCH') { + return JobsBotCard({ sentAt, from: fromResolved, toLinks, text, key: msg.key, msgSize }) + } if (subjectU === 'JOB_SUBSCRIBED' || subjectU === 'JOB_UNSUBSCRIBED') { return JobCard({ type: subjectU, sentAt, from: fromResolved, toLinks, text, key: msg.key, msgSize }) } @@ -3329,68 +4754,86 @@ exports.privateView = async (messagesInput, filter, decrypted = null) => { return ProjectFollowCard({ type: subjectU, sentAt, from: fromResolved, toLinks, text, key: msg.key, msgSize }) } if (subjectU === 'MARKET_SOLD') { - return MarketSoldCard({ sentAt, from: fromResolved, toLinks, subject: subjectRaw, text, key: msg.key, msgSize }) + return MarketSoldCard({ sentAt, from: fromResolved, toLinks, subject: subjectRaw, text, key: msg.key, msgSize, variant: 'market' }) + } + if (subjectU === 'SHOP_SOLD') { + return MarketSoldCard({ sentAt, from: fromResolved, toLinks, subject: subjectRaw, text, key: msg.key, msgSize, variant: 'shop' }) + } + if (subjectU === 'LARP_RULING' || subjectU === 'PARLIAMENT_GOV' || subjectU === 'TRIBE_GOV') { + return PoliticalBotCard({ subjectU, sentAt, from: fromResolved, toLinks, text, key: msg.key, msgSize }) + } + if (subjectU === 'SCHOOL_ENROLLED' || subjectU === 'SCHOOL_INVITED' || subjectU === 'SCHOOL_ADMITTED' || subjectU === 'SCHOOL_CERTIFICATE' || subjectU === 'SCHOOL_PASSED' || subjectU === 'SCHOOL_LESSON_NEW') { + return SchoolBotCard({ subjectU, sentAt, from: fromResolved, toLinks, text, key: msg.key, msgSize }) + } + if (subjectU === 'BANKING_UBI_PAID' || subjectU === 'BANKING_UBI_AVAILABLE' || subjectU === 'BANKING_CONFIRM_PENDING' || subjectU === 'WALLET_PAYMENT') { + return BankingBotCard({ subjectU, sentAt, from: fromResolved, toLinks, text, key: msg.key, msgSize }) + } + if (subjectU === 'INDUSTRY_ADMITTED' || subjectU === 'INDUSTRY_APPLICATION' || subjectU === 'INDUSTRY_INVITED' || subjectU === 'INDUSTRY_DISSOLVED' || subjectU === 'INDUSTRY_BUILD_APPROVED' || subjectU === 'INDUSTRY_DISTRIBUTED') { + return IndustryBotCard({ subjectU, sentAt, from: fromResolved, toLinks, text, key: msg.key, msgSize }) + } + if (subjectU === 'HOUSING_REQUESTED' || subjectU === 'HOUSING_CANCELLED' || subjectU === 'HOUSING_UNAVAILABLE') { + return HousingBotCard({ subjectU, sentAt, from: fromResolved, toLinks, text, key: msg.key, msgSize }) + } + if (subjectU === 'WIKI_EDITED' || subjectU === 'WIKI_RESTORED') { + return WikiBotCard({ subjectU, sentAt, from: fromResolved, toLinks, text, key: msg.key, msgSize }) + } + if (subjectU === 'EMERGENCY_UPDATED' || subjectU === 'EMERGENCY_RESOLVED') { + return EmergencyBotCard({ subjectU, sentAt, from: fromResolved, toLinks, text, key: msg.key, msgSize }) + } + if (subjectU === 'BLOG_NEW') { + return BlogBotCard({ sentAt, from: fromResolved, toLinks, text, key: msg.key, msgSize }) + } + if (subjectU === 'PODCAST_EPISODE') { + return PodcastBotCard({ sentAt, from: fromResolved, toLinks, text, key: msg.key, msgSize }) + } + if (subjectU === 'CAMPAIGN_UPDATED' || subjectU === 'CAMPAIGN_ACHIEVED' || subjectU === 'CAMPAIGN_RAISED') { + return CampaignBotCard({ subjectU, sentAt, from: fromResolved, toLinks, text, key: msg.key, msgSize }) + } + if (subjectU.startsWith('LOGISTICS_')) { + return LogisticsBotCard({ subjectU, sentAt, from: fromResolved, toLinks, text, key: msg.key, msgSize }) } if (subjectU === 'PROJECT_PLEDGE' || content.meta?.type === 'project-pledge') { return ProjectPledgeCard({ sentAt, from: fromResolved, toLinks, content, text, key: msg.key, msgSize }) } + if (content.list) { + return MailingCard({ sentAt, from: fromResolved, toLinks, subject: subjectRaw, text, key: msg.key, msgSize, content }) + } if (content.crypter === true) { const dec = (decrypted && decrypted.key === msg.key) ? decrypted : null const shownText = dec && typeof dec.text === 'string' ? dec.text : '' - const cbody = dec && typeof dec.text === 'string' - ? div({ class: 'message-text', innerHTML: sanitizeHtml(clickableLinks(dec.text)) }) - : dec && dec.error - ? div({ class: 'pm-form-error-msg' }, p('✗ ' + i18n.pmCrypterBadKey)) - : null - const cform = dec && typeof dec.text === 'string' - ? null - : form({ method: 'POST', action: '/inbox/decrypt', class: 'pm-crypter-decrypt-form' }, - input({ type: 'hidden', name: 'id', value: msg.key }), - input({ type: 'hidden', name: 'returnFilter', value: filter || 'all' }), - input({ type: 'text', name: 'key', placeholder: 'd66d7d32f4d30f34812aee3d01347154', minlength: 32, maxlength: 32, size: 34, required: true, class: 'pm-crypter-key-input' }), - button({ type: 'submit', class: 'pm-btn pm-crypter-decrypt-btn' }, (i18n.pmCrypterDecryptButton || 'Decrypt').toUpperCase()) - ) - const cacts = actions({ key: msg.key, replyId: fromResolved, subjectRaw, text: shownText }) - if (isMob) { - const mine = isSent(msg) - return div( - { class: `pm-card normal-pm pm-crypter-card pm-bubble ${mine ? 'pm-sent' : 'pm-recv'} thread-level-${level}` }, - mine ? null : div({ class: 'pm-bubble-sender' }, linkAuthor(fromResolved)), - cbody, - cform, - bubbleMeta({ sentAt, subjectRaw, crypter: true, msgSize, msgKey: msg.key }), - cacts - ) - } return div( { class: 'pm-card normal-pm pm-crypter-card' }, headerLine({ sentAt, from: fromResolved, toLinks, subject: subjectRaw, msgKey: msg.key, msgSize, crypter: true }), - cbody, - cform, - cacts - ) - } - - if (isMob) { - const mine = isSent(msg) - return div( - { class: `pm-card normal-pm pm-bubble ${mine ? 'pm-sent' : 'pm-recv'} thread-level-${level}` }, - mine ? null : div({ class: 'pm-bubble-sender' }, linkAuthor(fromResolved)), - div({ class: 'message-text', innerHTML: sanitizeHtml(clickableLinks(text)) }), - bubbleMeta({ sentAt, subjectRaw, crypter: false, msgSize, msgKey: msg.key }), - actions({ key: msg.key, replyId: fromResolved, subjectRaw, text }) + dec && typeof dec.text === 'string' + ? div({ class: 'message-text', [RAW_HTML]: sanitizeHtml(clickableLinks(dec.text)) }) + : null, + dec && typeof dec.text === 'string' + ? null + : form({ method: 'POST', action: '/inbox/decrypt', class: 'pm-crypter-decrypt-form' }, + input({ type: 'hidden', name: 'id', value: msg.key }), + input({ type: 'hidden', name: 'returnFilter', value: filter || 'all' }), + input({ type: 'text', name: 'key', placeholder: 'd66d7d32f4d30f34812aee3d01347154', minlength: 32, maxlength: 32, size: 34, required: true, class: 'pm-crypter-key-input' }), + button({ type: 'submit', class: 'pm-btn pm-crypter-decrypt-btn' }, (i18n.pmCrypterDecryptButton || 'Decrypt').toUpperCase()) + ), + actions({ key: msg.key, replyId: fromResolved, subjectRaw, text: shownText }) ) } return div( { class: 'pm-card normal-pm' }, headerLine({ sentAt, from: fromResolved, toLinks, subject: subjectRaw, msgKey: msg.key, msgSize }), - div({ class: 'message-text', innerHTML: sanitizeHtml(clickableLinks(text)) }), + div({ class: 'message-text', [RAW_HTML]: sanitizeHtml(clickableLinks(text)) }), actions({ key: msg.key, replyId: fromResolved, subjectRaw, text }) ) } + const msgTs = (m) => new Date(m?.value?.content?.sentAt || m.timestamp || 0).getTime() + if (sortMode === 'recent') { + if (!sorted.length) return p({ class: 'empty' }, i18n.noPrivateMessages) + return paged([...sorted].sort((a, b) => msgTs(b) - msgTs(a))).map(renderMsg) + } + const threadGroups = {} const threadOrder = [] for (const msg of sorted) { @@ -3404,26 +4847,32 @@ exports.privateView = async (messagesInput, filter, decrypted = null) => { if (!threadOrder.length) return p({ class: 'empty' }, i18n.noPrivateMessages) - return threadOrder.map(tid => { - const msgs = threadGroups[tid] - const original = msgs[0] - const replies = msgs.slice(1) + const threadTs = (tid) => (threadGroups[tid] || []).reduce((max, m) => { + const t = new Date(m?.value?.content?.sentAt || m.timestamp || 0).getTime() + return t > max ? t : max + }, 0) + threadOrder.sort((a, b) => threadTs(b) - threadTs(a)) - if (!replies.length) { - return renderMsg(original) + return paged(threadOrder).map(tid => { + const msgs = threadGroups[tid] + const latest = msgs[msgs.length - 1] + const earlier = msgs.slice(0, -1) + + if (!earlier.length) { + return renderMsg(latest) } - const replyLabel = `${replies.length} ${replies.length === 1 ? (i18n.pmReply || 'reply') : (i18n.pmReplies || 'replies')}` + const earlierLabel = `${earlier.length} ${i18n.inboxEarlierMessages}` return div({ class: 'pm-thread' }, - renderMsg(original), + renderMsg(latest), details({ class: 'pm-thread-details' }, summary({ class: 'pm-thread-toggle' }, span({ class: 'pm-thread-icon' }, '▶'), - span(replyLabel) + span(earlierLabel) ), div({ class: 'pm-thread-replies' }, - ...replies.map(renderMsg) + ...earlier.map(renderMsg) ) ) ) @@ -3434,46 +4883,11 @@ exports.privateView = async (messagesInput, filter, decrypted = null) => { ) } -exports.publishCustomView = async () => { - const action = "/publish/custom"; - const method = "post"; - return template( - i18n.publishCustom, - section( - div({ class: "tags-header" }, - h2(i18n.publishCustom), - p(i18n.publishCustomDescription) - ), - form( - { action, method }, - textarea( - { - autofocus: true, - required: true, - name: "text", - rows: 10, - class: "textarea-full" - }, - "{\n", - ' "type": "feed",\n', - ' "hello": "world"\n', - "}" - ), - br(), - br(), - button({ type: "submit" }, i18n.submit) - ) - ), - section( - div({ class: "tags-header" }, - p(i18n.publishBasicInfo({ href: "/publish" })) - ) - ) - ); -}; - -exports.threadView = ({ messages, spreadMap = null }) => { +exports.threadView = ({ messages, spreadMap = null, hiddenRoot = false }) => { + if (!Array.isArray(messages) || !messages.length) { + return template(i18n.hiddenByYou, section(p({ class: 'muted hidden-note' }, i18n.hiddenByYou))); + } const rootMessage = messages[0]; const rootAuthorName = rootMessage.value.meta.author.name; @@ -3484,81 +4898,50 @@ exports.threadView = ({ messages, spreadMap = null }) => { const tpl = template( [`@${rootAuthorName}`], + hiddenRoot ? p({ class: 'muted hidden-note' }, i18n.hiddenByYou) : null, div(thread(messages, spreadMap)) ); return `${tpl}${ needsPdfViewer - ? ` - ` + ? `` : "" }`; }; -exports.publishView = (preview, text, contentWarning) => { +exports.supportersView = async ({ supporters, feed, name }) => { + const list = Array.isArray(supporters) ? supporters : []; return template( - i18n.publish, + i18n.viewSupporters, section( - div({ class: "tags-header" }, - h2(i18n.publishBlog), - p(i18n.publishLabel({ markdownUrl, linkTarget: "_blank" })) + div({ class: "tags-header module-header-line" }, + h2(`${i18n.viewSupporters} (${list.length})`), + p(userLink(feed, name)) ) ), - section( - div({ class: "publish-form" }, - form( - { - action: "/publish/preview", - method: "post", - enctype: "multipart/form-data", - }, - [ - label({ for: "contentWarning" }, i18n.blogSubject), - br(), - input({ - name: "contentWarning", - id: "contentWarning", - type: "text", - class: "contentWarning", - value: contentWarning || "", - placeholder: i18n.contentWarningPlaceholder - }), - br(), - label({ for: "text" }, i18n.blogMessage), - br(), - textarea( - { - required: true, - name: "text", - id: "text", - rows: "6", - cols: "50", - placeholder: i18n.publishWarningPlaceholder, - class: "publish-textarea", - maxlength: "8096" - }, - text || "" - ), - br(), - label({ for: "blob" }, i18n.blogImage || "Upload media (max-size: 50MB)"), - br(), - input({ type: "file", id: "blob", name: "blob" }), - br(), br(), - button({ type: "submit" }, i18n.blogPublish) - ] - ) - ) - ), - preview || "", - section( - div({ class: "tags-header" }, - p(i18n.publishCustomInfo({ href: "/publish/custom" })) - ) - ) + list.length + ? section(div({ class: "supporters-list" }, ...list.map(id => span({ class: "pad-member-chip" }, userLink(id))))) + : p({ class: "no-content" }, i18n.no_results) ); }; -//generate preview +exports.likesView = async ({ messages, feed, name, spreadMap = null }) => { + const list = Array.isArray(messages) ? messages : []; + return template( + i18n.viewLikes, + section( + div({ class: "tags-header module-header-line" }, + h2(i18n.viewLikes), + p(userLink(feed, name)) + ) + ), + list.length + ? div(thread(list, spreadMap)) + : p({ class: "no-content" }, i18n.no_results) + ); +}; + + const ensureAt = (id) => { const s = String(id || "").trim() if (!s) return "" @@ -3616,51 +4999,6 @@ const injectResolvedMentions = (text, mentions) => { return out } -const markdownMentionsToHtml = (markdownText) => { - const escaped = escapeHtml(String(markdownText || "")) - const withBr = escaped.replace(/\r\n|\r|\n/g, "
") - - const unescapeBlob = (b) => b.replace(/&/g, '&') - - const escAttr = (s) => String(s).replace(/"/g, '"').replace(/'/g, ''') - - const withImages = withBr.replace( - /!\[([^\]]*)\]\(\s*(&[^)\s]+\.sha256)\s*\)/g, - (_m, alt, blob) => `${escAttr(alt)}` - ) - - const withVideos = withImages.replace( - /\[video:([^\]]*)\]\(\s*(&[^)\s]+\.sha256)\s*\)/g, - (_m, _name, blob) => `` - ) - - const withAudios = withVideos.replace( - /\[audio:([^\]]*)\]\(\s*(&[^)\s]+\.sha256)\s*\)/g, - (_m, _name, blob) => `` - ) - - const withPdfs = withAudios.replace( - /\[pdf:([^\]]*)\]\(\s*(&[^)\s]+\.sha256)\s*\)/g, - (_m, name, blob) => `${escapeHtml(name || i18n.pdfFallbackLabel || 'PDF')}` - ) - - const withMentions = withPdfs.replace( - /\[@([^\]]+)\]\(\s*@?([^) \t\r\n]+\.ed25519)\s*\)/g, - (_m, label, feed) => { - const href = authorHref(feed) - const shown = `@${String(label || "").replace(/^@+/, "")}` - return `${escapeHtml(shown)}` - } - ) - - const withLinks = withMentions.replace( - /(https?:\/\/[^\s"'<>]+)/g, - (u) => `${escAttr(u)}` - ) - - return withLinks -} - const generatePreview = ({ previewData, contentWarning, action }) => { const mentions = previewData && previewData.mentions && typeof previewData.mentions === "object" @@ -3672,7 +5010,7 @@ const generatePreview = ({ previewData, contentWarning, action }) => { const injected = injectResolvedMentions(normalized, mentions) const publishText = normalizeMentionLinks(injected) - const previewHtml = markdownMentionsToHtml(publishText) + const previewHtml = renderStyledHtml(publishText) const mentionCards = Object.entries(mentions) .map(([_token, matches]) => { @@ -3738,7 +5076,7 @@ const generatePreview = ({ previewData, contentWarning, action }) => { { class: "preview-content" }, h2(i18n.messagePreview), contentWarning ? div({ class: "content-warning-preview" }, escapeHtml(contentWarning)) : null, - div({ class: "preview-rendered", innerHTML: previewHtml }) + div({ class: "preview-rendered", [RAW_HTML]: previewHtml }) ) ), section( @@ -3758,159 +5096,6 @@ const generatePreview = ({ previewData, contentWarning, action }) => { ) } -exports.previewView = ({ previewData, contentWarning }) => { - const publishAction = "/publish" - const preview = generatePreview({ - previewData, - contentWarning, - action: publishAction, - }) - return exports.publishView(preview, (previewData && previewData.text) || "", contentWarning) -} - -const viewInfoBox = ({ viewTitle = null, viewDescription = null }) => { - if (!viewTitle && !viewDescription) { - return null - } - return section( - { class: "viewInfo" }, - viewTitle ? h1(viewTitle) : null, - viewDescription ? em(viewDescription) : null - ) -} -//generate preview - -exports.likesView = async ({ messages, feed, name, spreadMap = null }) => { - const authorLink = a( - { href: `/author/${encodeURIComponent(feed)}` }, - "@" + name - ); - const getSpread = (key) => (spreadMap instanceof Map ? spreadMap.get(key) : null) || null; - - return template( - ["@", name], - viewInfoBox({ - viewTitle: span(authorLink), - viewDescription: span(i18n.spreadedDescription) - }), - messages.map((msg) => post({ msg, spreadInfo: getSpread(msg.key) })) - ); -}; - -const messageListView = ({ - messages, - viewTitle = null, - viewDescription = null, - viewElements = null, - aside = null, - spreadMap = null, -}) => { - const hasHeader = !!viewElements; - const titleBlock = hasHeader - ? viewElements - : div({ class: "tags-header" }, - h2(viewTitle), - p(viewDescription) - ); - const getSpread = (key) => (spreadMap instanceof Map ? spreadMap.get(key) : null) || null; - return template( - viewTitle, - section(titleBlock), - messages.map((msg) => post({ msg, aside, spreadInfo: getSpread(msg.key) })) - ); -}; - -exports.popularView = ({ messages, prefix, spreadMap = null }) => { - const header = div({ class: "tags-header" }, - h2(i18n.popular), - p(i18n.popularDescription) - ); - return messageListView({ - messages, - viewTitle: i18n.popular, - viewElements: [header, prefix], - spreadMap - }); -}; - -exports.extendedView = ({ messages, spreadMap = null }) => { - const header = div({ class: "tags-header" }, - h2(i18n.extended), - p(i18n.extendedDescription) - ); - return messageListView({ - messages, - viewTitle: i18n.extended, - viewElements: header, - spreadMap - }); -}; - -exports.latestView = ({ messages, spreadMap = null }) => { - const header = div({ class: "tags-header" }, - h2(i18n.latest), - p(i18n.latestDescription) - ); - return messageListView({ - messages, - viewTitle: i18n.latest, - viewElements: header, - spreadMap - }); -}; - -exports.topicsView = ({ messages, prefix, spreadMap = null }) => { - const header = div({ class: "tags-header" }, - h2(i18n.topics), - p(i18n.topicsDescription) - ); - return messageListView({ - messages, - viewTitle: i18n.topics, - viewElements: [header, prefix], - spreadMap - }); -}; - -exports.summaryView = ({ messages, spreadMap = null }) => { - const header = div({ class: "tags-header" }, - h2(i18n.summaries), - p(i18n.summariesDescription) - ); - return messageListView({ - messages, - viewTitle: i18n.summaries, - viewElements: header, - aside: true, - spreadMap - }); -}; - -exports.spreadedView = ({ messages }) => { - const header = div({ class: "tags-header" }, - h2(i18n.spreaded), - p(i18n.spreadedDescription) - ); - return spreadedListView({ - messages, - viewTitle: i18n.spreaded, - viewElements: header - }); -}; - -exports.threadsView = ({ messages, spreadMap = null }) => { - const header = div({ class: "tags-header" }, - h2(i18n.threads), - p(i18n.threadsDescription) - ); - return messageListView({ - messages, - viewTitle: i18n.threads, - viewElements: header, - aside: true, - spreadMap - }); -}; exports.previewSubtopicView = async ({ previewData, @@ -3996,7 +5181,7 @@ exports.subtopicView = async ( br(), label( { for: "blob" }, - i18n.blogImage || "Upload media (max-size: 50MB)" + i18n.blogImage || "Upload media (max-size: 75MB)" ), input({ type: "file", id: "blob", name: "blob" }), br(), diff --git a/src/views/maps_view.js b/src/views/maps_view.js index 997555f..ff17622 100644 --- a/src/views/maps_view.js +++ b/src/views/maps_view.js @@ -1,16 +1,23 @@ -const { form, button, div, h2, h3, p, section, input, label, br, a, span, textarea, select, option, img, strong } = +const { form, button, div, h2, h3, p, section, input, label, br, hr, a, span, textarea, select, option, img, strong, table, tr, td } = require("../server/node_modules/hyperaxe"); +const RAW_HTML = Symbol.for('oasis.rawHtml'); +const { renderStyledText, renderStyledHtml, escapeHtml } = require("../backend/renderStyledText"); const moment = require("../server/node_modules/moment"); -const { template, i18n, userLink, renderLifespanChip, renderSpreadButton, renderContentActions } = require("./main_views"); -const { renderEncryptedChip } = require("./clearnet_view"); +const { clearnetItemHref, template, i18n, userLink, renderStateChip, renderLifespanChip, renderSpreadButton, renderContentActions, renderSpreadEditWarning, renderDocumentActions, renderInviteQrCard, renderModuleStats, moduleIsEmpty, contentDeleteAction, paged, renderIcon } = require("./main_views"); +const { renderEncryptedChip, renderReachChip, renderClearnetSelector, renderClearnetSwitch, renderTribeOriginChip } = require("./clearnet_view"); const { config } = require("../server/SSB_server.js"); -const { renderMapWithPins, renderZoomedMapWithPins, getViewportBounds, latLngToPx, pxToLatLng, MAP_W, MAP_H, getMaxTileZoom } = require("../maps/map_renderer"); -const { sanitizeHtml } = require('../backend/sanitizeHtml'); +const { renderMapHtml, resolveView, makeView, fitView, parseView, viewParam, parsePick, MIN_ZOOM, MAX_ZOOM } = require("../maps/map_renderer"); +const { searchPlaces, nearestPlace } = require("../maps/map_data"); const userId = config.keys.id; const safeArr = (v) => (Array.isArray(v) ? v : []); +const localHref = (u) => (typeof u === 'string' && /^\/(?![\/\\])\S*$/.test(u) ? u : null); const safeText = (v) => String(v || "").trim(); +const MAP_REACH = ["SINGLE", "OPEN", "CLOSED"]; +const mapReachLabel = (t) => t === "OPEN" ? i18n.mapTypeOpen : t === "CLOSED" ? i18n.mapTypeClosed : i18n.mapTypeSingle; +const clearnetEligible = (m) => !!m && !m.tribeId && !m.encrypted && !m.contentEncrypted && String(m.mapType || "").toUpperCase() === "OPEN"; +const mapClearnetHref = (m) => clearnetItemHref("maps", m.title, m.rootId || m.key); const buildReturnTo = (filter, params = {}) => { const f = safeText(filter || "all"); @@ -20,14 +27,6 @@ const buildReturnTo = (filter, params = {}) => { return `/maps?${parts.join("&")}`; }; -const renderPMButton = (recipient) => { - const r = safeText(recipient); - if (!r || String(r) === String(userId)) return null; - return form({ method: "GET", action: "/pm" }, - input({ type: "hidden", name: "recipients", value: r }), - button({ type: "submit", class: "filter-btn" }, i18n.privateMessage)); -}; - const renderTags = (tags) => { const list = safeArr(tags).map((t) => String(t || "").trim()).filter(Boolean); return list.length @@ -35,126 +34,105 @@ const renderTags = (tags) => { : null; }; -const renderMapFavoriteToggle = (mapObj, returnTo = "") => - form({ - method: "POST", - action: mapObj.isFavorite ? `/maps/favorites/remove/${encodeURIComponent(mapObj.key)}` : `/maps/favorites/add/${encodeURIComponent(mapObj.key)}` - }, - returnTo ? input({ type: "hidden", name: "returnTo", value: returnTo }) : null, - button({ type: "submit", class: "filter-btn" }, mapObj.isFavorite ? i18n.mapRemoveFavoriteButton : i18n.mapAddFavoriteButton)); - let areaCounter = 0; +const lastOf = (v) => (Array.isArray(v) ? v[v.length - 1] : v); +const numOrNull = (v) => { const n = parseFloat(lastOf(v)); return isFinite(n) ? n : null; }; +const isSealed = (v) => !!v && String(v).startsWith("/blob/sealed/"); +const isBlob = (v) => !!v && (String(v).startsWith("&") || isSealed(v)); +const blobSrc = (image, base) => (isSealed(image) ? (base ? "" : String(image)) : (isBlob(image) ? `${base}/blob/${encodeURIComponent(image)}` : "")); -const buildAreas = (clickUrl, latParam = "lat", lngParam = "lng", viewport = null) => { - const GRID = 16; - const cellW = MAP_W / GRID; - const cellH = MAP_H / GRID; - const areas = []; - for (let gy = 0; gy < GRID; gy++) { - for (let gx = 0; gx < GRID; gx++) { - let c; - if (viewport) { - const lat = viewport.latMax - (gy + 0.5) / GRID * (viewport.latMax - viewport.latMin); - const lng = viewport.lngMin + (gx + 0.5) / GRID * (viewport.lngMax - viewport.lngMin); - c = { lat: Math.round(lat * 10000) / 10000, lng: Math.round(lng * 10000) / 10000 }; - } else { - const cx = Math.round(gx * cellW + cellW / 2); - const cy = Math.round(gy * cellH + cellH / 2); - c = pxToLatLng(cx, cy); - } - const x1 = Math.round(gx * cellW); - const y1 = Math.round(gy * cellH); - const x2 = Math.round((gx + 1) * cellW); - const y2 = Math.round((gy + 1) * cellH); - areas.push(`${c.lat},${c.lng}`); - } +const buildQuery = (params) => Object.entries(params || {}) + .filter(([, v]) => v !== undefined && v !== null && String(v) !== "") + .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`) + .join("&"); + +const viewHrefFor = (basePath, baseParams, anchor = "#map") => (target, extra = {}) => { + const q = buildQuery({ ...baseParams, ...extra, view: target ? viewParam(target) : undefined }); + return `${basePath}${q ? "?" + q : ""}${anchor}`; +}; + +const navText = () => ({ zoomIn: i18n.mapZoomIn, zoomOut: i18n.mapZoomOut, fit: i18n.mapFitMarkers, pan: i18n.mapPanLabel }); + +const viewForPlace = (hit) => { + if (!hit) return null; + if (hit.kind === "country" && Array.isArray(hit.bbox)) { + const b = hit.bbox; + const f = fitView([{ lat: b[1], lng: b[0] }, { lat: b[3], lng: b[2] }], { maxZoom: 7, pad: 40 }); + return makeView(f.lat, f.lng, f.zoom); } - return areas; + return makeView(hit.lat, hit.lng, MAX_ZOOM); +}; + +const placeLabel = (hit) => hit.country ? `${hit.name}, ${hit.country}` : hit.name; + +const nearText = (lat, lng) => { + const near = nearestPlace(lat, lng); + return near ? `${i18n.mapNear} ${near.name}, ${near.country}` : ""; +}; + +const pickedSpot = (lat, lng) => { + const la = numOrNull(lat), lo = numOrNull(lng); + if (la === null || lo === null) return p({ class: "map-pick-hint" }, i18n.mapPickHint); + const near = nearText(la, lo); + return div({ class: "map-picked" }, + span({ class: "map-coord-pin" }, "📍"), + strong(`${la.toFixed(4)}, ${lo.toFixed(4)}`), + near ? span({ class: "map-picked-near" }, near) : null); }; const renderMap = (markers, clickUrl, mainIdx, opts = {}) => { areaCounter++; - const mapName = `m${areaCounter}`; - const latParam = opts.latParam || "lat"; - const lngParam = opts.lngParam || "lng"; const pinLabels = opts.pinLabels || []; const pinImages = opts.pinImages || []; - const pfx = opts.pinPrefix || `pin${areaCounter}`; - const zoom = parseInt(opts.zoom) || 2; - const centerLat = typeof opts.centerLat === "number" ? opts.centerLat : 0; - const centerLng = typeof opts.centerLng === "number" ? opts.centerLng : 0; - - const pinList = safeArr(markers).filter((m) => m && typeof m.lat === "number" && typeof m.lng === "number"); - const useZoom = zoom > 2; - const mapFile = useZoom - ? renderZoomedMapWithPins(centerLat, centerLng, zoom, pinList, mainIdx) - : (pinList.length > 0 ? renderMapWithPins(pinList, mainIdx) : null); - const imgSrc = mapFile ? `/mapcache/${mapFile}` : "/assets/images/worldmap-z2.png"; - const viewport = useZoom && clickUrl ? getViewportBounds(centerLat, centerLng, zoom) : null; - - const useMap = clickUrl || pinLabels.length > 0; - const mapTag = useMap ? mapName : ""; - - let gridAreasHtml = ""; - if (clickUrl) { - const clickUrlWithZoom = zoom > 2 ? `${clickUrl}zoom=${zoom}&` : clickUrl; - gridAreasHtml = buildAreas(clickUrlWithZoom, latParam, lngParam, viewport).join(""); + const pinHrefs = opts.pinHrefs || []; + const base = opts.publicBase || ""; + const focus = Number.isInteger(opts.focus) ? opts.focus : -1; + const main = mainIdx || 0; + const pins = safeArr(markers) + .filter((m) => m && numOrNull(m.lat) !== null && numOrNull(m.lng) !== null) + .map((m, i) => ({ + lat: numOrNull(m.lat), + lng: numOrNull(m.lng), + label: pinLabels[i] !== undefined ? pinLabels[i] : (m.label || ""), + image: blobSrc(pinImages[i] !== undefined ? pinImages[i] : m.image, base), + href: pinHrefs[i] || m.href || null, + title: m.title || "", + main: i === main, + focus: i === focus + })); + if (opts.pick && numOrNull(opts.pick.lat) !== null && numOrNull(opts.pick.lng) !== null) { + pins.push({ lat: numOrNull(opts.pick.lat), lng: numOrNull(opts.pick.lng), pick: true }); } - let popupAreasHtml = ""; - let popupsHtml = ""; - if (pinLabels.length > 0) { - const vp = useZoom ? getViewportBounds(centerLat, centerLng, zoom) : null; - pinList.forEach((m, i) => { - const lbl = pinLabels[i] || ""; - let px; - if (vp) { - px = { - x: ((m.lng - vp.lngMin) / (vp.lngMax - vp.lngMin)) * MAP_W, - y: ((vp.latMax - m.lat) / (vp.latMax - vp.latMin)) * MAP_H - }; - } else { - px = latLngToPx(m.lat, m.lng); - } - const escaped = lbl.replace(/&/g, "&").replace(/"/g, """).replace(//g, ">"); - const withLinks = escaped.replace(/https?:\/\/[^\s&"<>]+/g, (url) => { - const clean = url.replace(/&/g, "&"); - return `${url}`; - }).replace(/\n/g, "
"); - const sz = 20 * Math.pow(2, Math.max(0, zoom - getMaxTileZoom())); - const x1 = Math.max(0, px.x - sz); - const y1 = Math.max(0, px.y - sz); - const x2 = Math.min(MAP_W, px.x + sz); - const y2 = Math.min(MAP_H, px.y + sz); - const popupId = `${pfx}_${i}`; - const latStr = typeof m.lat === "number" ? m.lat.toFixed(4) : ""; - const lngStr = typeof m.lng === "number" ? m.lng.toFixed(4) : ""; - const imgBlobId = pinImages[i] && String(pinImages[i]).startsWith("&") ? pinImages[i] : ""; - const imgHtml = imgBlobId ? `` : ""; - popupAreasHtml += `${escaped}`; - popupsHtml += `
✕${imgHtml}
${sanitizeHtml(withLinks)}
${latStr}, ${lngStr}
`; - }); - } - const mapHtml = useMap ? `${popupAreasHtml}${gridAreasHtml}` : ""; - const useAttr = useMap ? ` usemap="#${mapTag}"` : ""; - - const mapWrapHtml = `
map${mapHtml}
`; - const viewerEl = div({ class: "map-viewer" }, { innerHTML: mapWrapHtml }); - if (!popupsHtml) return viewerEl; - return div({ class: "map-zone" }, viewerEl, div({ class: "map-popup-container", innerHTML: popupsHtml })); + const view = opts.view || resolveView({ zoom: opts.zoom, clat: opts.centerLat, clng: opts.centerLng }, pins.filter((x) => !x.pick), { singleZoom: opts.singleZoom || 8 }); + const html = renderMapHtml(view, pins, { + id: opts.id || `map${areaCounter}`, + thumb: !!opts.thumb, + publicBase: base, + nav: opts.nav || null, + pick: opts.pickForm ? { form: opts.pickForm, action: opts.pickAction, name: opts.pickName || "pick" } : null, + clickUrl: clickUrl || null, + latParam: opts.latParam, + lngParam: opts.lngParam, + anchor: typeof opts.anchor === "string" ? opts.anchor : "", + viewHref: opts.viewHref || null, + popup: opts.popup || null, + clusterTitle: i18n.mapClusterTitle, + text: navText() + }); + return div({ class: opts.thumb ? "map-viewer map-viewer-thumb" : "map-viewer", [RAW_HTML]: html }); }; -const renderCoordPreview = (lat, lng) => { - if (!lat && !lng) return null; - return span({ class: "map-coord-inline" }, - span({ class: "map-coord-pin" }, "📍"), - strong(`${lat}, ${lng}`)); +const renderPlaceResults = (hits, toEl) => { + if (!hits) return null; + if (!hits.length) return span({ class: "map-place-none" }, i18n.mapNoPlaces); + return div({ class: "map-place-results" }, hits.map((h) => toEl(h))); }; const renderLocalEmbed = (lat, lng) => { const la = parseFloat(lat) || 0; const lo = parseFloat(lng) || 0; if (!la && !lo) return null; - return renderMap([{ lat: la, lng: lo }], null, 0); + return renderMap([{ lat: la, lng: lo }], null, 0, { zoom: MIN_ZOOM, centerLat: la, centerLng: lo }); }; const renderMapUrl = (mapObj) => @@ -169,22 +147,20 @@ const renderMapOwnerActions = (filter, mapObj, params = {}) => { const actions = [ form({ method: "GET", action: `/maps/edit/${encodeURIComponent(mapObj.key)}` }, input({ type: "hidden", name: "returnTo", value: returnTo }), - button({ class: "update-btn", type: "submit" }, i18n.mapUpdateButton)), - form({ method: "POST", action: `/maps/delete/${encodeURIComponent(mapObj.key)}` }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - button({ class: "delete-btn", type: "submit" }, i18n.mapDeleteButton)) + button({ class: "update-btn", type: "submit" }, i18n.mapUpdateButton)) ]; - const isPrivateMap = !mapObj.tribeId && mapObj.mapType !== "OPEN"; - if (isPrivateMap) { + const invitable = !mapObj.tribeId && mapObj.mapType === "CLOSED"; + if (invitable) { actions.push(form({ method: "POST", action: `/maps/generate-invite/${encodeURIComponent(mapObj.key)}` }, button({ type: "submit", class: "tribe-action-btn" }, i18n.tribeGenerateInvite))); } const openInvite = Array.isArray(mapObj.invites) ? mapObj.invites.find(inv => typeof inv === "object" && inv.public === true && inv.code) : null; - if (!mapObj.tribeId) { + if (invitable) { if (openInvite) { actions.push(div({ class: 'tribe-open-invite' }, span({ class: 'card-label' }, i18n.tribeInviteCodeText), - span({ class: 'tribe-open-invite-code' }, openInvite.code) + span({ class: 'tribe-open-invite-code' }, openInvite.code), + renderInviteQrCard({ qrDataUrl: `/qr-invite-code/maps/${encodeURIComponent(openInvite.code)}` }) )); actions.push(form({ method: "POST", action: `/maps/open-invite/remove/${encodeURIComponent(mapObj.key)}` }, button({ type: "submit", class: "tribe-action-btn danger-btn" }, i18n.tribeRemoveInvitation))); @@ -196,195 +172,270 @@ const renderMapOwnerActions = (filter, mapObj, params = {}) => { return actions; }; -const renderFilters = (filter, q) => - div({ class: "filters" }, +const renderFilters = (filter, q, emptyMod = false, chip = null, search = null) => + div({ class: search ? "filters activity-filter-chips activity-toolbar-row" : "filters" }, form({ method: "GET", action: "/maps", class: "ui-toolbar ui-toolbar--filters" }, - input({ type: "hidden", name: "q", value: q || "" }), - button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, i18n.mapFilterAll), - button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, i18n.mapFilterMine), - button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, i18n.mapFilterRecent), - button({ type: "submit", name: "filter", value: "favorites", class: filter === "favorites" ? "filter-btn active" : "filter-btn" }, i18n.mapFilterFavorites), - button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.mapUploadButton))); + ...(emptyMod ? [] : [ + button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.mapFilterAll).toUpperCase()), + ...(!chip || chip("recent") ? [button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.mapFilterRecent).toUpperCase())] : []), + ...(!chip || chip("mine") ? [button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.mapFilterMine).toUpperCase())] : []), + ...(!chip || chip("favorites") ? [button({ type: "submit", name: "filter", value: "favorites", class: filter === "favorites" ? "filter-btn active" : "filter-btn" }, String(i18n.mapFilterFavorites).toUpperCase())] : []), + ]), + button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.mapCreateButton)), + search); const renderMapForm = (filter, mapId, mapToEdit, params = {}) => { const returnFilter = filter === "create" ? "all" : params.filter || "all"; const returnTo = safeText(params.returnTo) || buildReturnTo(returnFilter, params); - const latVal = params.lat !== undefined ? String(params.lat) : String(mapToEdit?.lat || ""); - const lngVal = params.lng !== undefined ? String(params.lng) : String(mapToEdit?.lng || ""); + const picked = parsePick(params.pick); + const latVal = picked ? String(picked.lat) : (params.lat !== undefined ? String(lastOf(params.lat)) : String(mapToEdit?.lat || "")); + const lngVal = picked ? String(picked.lng) : (params.lng !== undefined ? String(lastOf(params.lng)) : String(mapToEdit?.lng || "")); const titleVal = params.title || mapToEdit?.title || ""; const descVal = params.description || mapToEdit?.description || ""; const markerLabelVal = params.markerLabel !== undefined ? params.markerLabel : (mapToEdit?.markerLabel || ""); const tagsValue = params.tags !== undefined ? params.tags : safeArr(mapToEdit?.tags).join(", "); - const mapTypeVal = params.mapType || mapToEdit?.mapType || "SINGLE"; - const maxTileZoom = getMaxTileZoom(); - const zoomVal = parseInt(params.zoom) || 2; - const cleanUrl = `/maps?filter=create${params.tribeId ? '&tribeId=' + encodeURIComponent(params.tribeId) : ''}`; + const isEdit = filter === "edit"; + const reachRaw = String(params.reach || params.mapType || "").toUpperCase(); + const mapTypeVal = MAP_REACH.includes(reachRaw) ? reachRaw : (mapToEdit?.mapType || "SINGLE"); + const showClearnet = mapTypeVal === "OPEN" && !params.tribeId && clearnetEligible({ ...(mapToEdit || {}), mapType: mapTypeVal }); + const clearnetOn = params.clearnet !== undefined ? String(params.clearnet) === "1" : mapToEdit?.clearnet === true; + const stepAction = isEdit ? `/maps/edit/${encodeURIComponent(mapId)}` : "/maps"; + const cleanUrl = isEdit + ? `${stepAction}?mapType=${encodeURIComponent(mapTypeVal)}` + : `/maps?filter=create&mapType=${encodeURIComponent(mapTypeVal)}${params.tribeId ? '&tribeId=' + encodeURIComponent(params.tribeId) : ''}`; const pickerMarkers = latVal && lngVal ? [{ lat: parseFloat(latVal), lng: parseFloat(lngVal) }] : []; + const placeQ = safeText(lastOf(params.place)); + const placeHits = placeQ ? searchPlaces(placeQ, 5) : null; + const explicitView = parseView(params.view); + const prevView = Array.isArray(params.view) ? parseView(params.view[0]) : null; + const view = explicitView + ? makeView(explicitView.lat, explicitView.lng, explicitView.zoom) + : (placeHits && placeHits.length ? viewForPlace(placeHits[0]) : resolveView({ zoom: params.zoom, clat: prevView ? prevView.lat : undefined, clng: prevView ? prevView.lng : undefined }, pickerMarkers, { singleZoom: 8 })); + const zoomVal = view.zoom; + const formId = "map-main-form"; + const nav = { form: formId, action: stepAction }; + const keptKeys = ["lat", "lng", "zoom", "view", "title", "description", "markerLabel", "tags"]; - return div({ class: "map-create-layout" }, - div({ class: "map-form map-form-full" }, - form({ - action: filter === "edit" ? `/maps/update/${encodeURIComponent(mapId)}` : "/maps/create", + return div({ class: "div-center audio-form" }, + params.spreadWarning || null, + h2(isEdit ? i18n.mapUpdateButton : i18n.mapCreateButton), + form({ method: "GET", action: stepAction }, + isEdit ? null : input({ type: "hidden", name: "filter", value: "create" }), + params.tribeId ? input({ type: "hidden", name: "tribeId", value: params.tribeId }) : null, + isEdit && safeText(params.returnTo) ? input({ type: "hidden", name: "returnTo", value: safeText(params.returnTo) }) : null, + ...keptKeys.filter((k) => params[k] !== undefined && String(lastOf(params[k])) !== "").map((k) => input({ type: "hidden", name: k, value: String(lastOf(params[k])) })), + label(i18n.mapTypeLabel), br(), + div({ class: "apply-row" }, + select({ name: "mapType", class: "report-category-select" }, + ...MAP_REACH.map((t) => option({ value: t, ...(mapTypeVal === t ? { selected: true } : {}) }, mapReachLabel(t)))), + button({ type: "submit", class: "create-button" }, i18n.apply || "Apply"))), + hr({ class: "form-sep" }), + h2({ class: "report-category-fixed" }, mapReachLabel(mapTypeVal)), + form({ + id: formId, + action: isEdit ? `/maps/update/${encodeURIComponent(mapId)}` : "/maps/create", method: "POST", enctype: "multipart/form-data" }, input({ type: "hidden", name: "returnTo", value: returnTo }), - input({ type: "hidden", name: "filter", value: "create" }), + isEdit ? null : input({ type: "hidden", name: "filter", value: "create" }), + input({ type: "hidden", name: "mapType", value: mapTypeVal }), + input({ type: "hidden", name: "view", value: viewParam(view) }), params.tribeId ? input({ type: "hidden", name: "tribeId", value: params.tribeId }) : null, - label(i18n.title || "Title"), - input({ type: "text", name: "title", placeholder: i18n.mapTitlePlaceholder || "Map title", value: titleVal }), - label(i18n.mapDescriptionLabel), - textarea({ name: "description", placeholder: i18n.mapDescriptionPlaceholder, rows: "3" }, descVal), - label(i18n.mapTagsLabel), + label(i18n.title || "Title"), br(), + input({ type: "text", name: "title", maxlength: "100", placeholder: i18n.mapTitlePlaceholder || "Map title", value: titleVal }), + br(), br(), + label(i18n.mapDescriptionLabel), br(), + textarea({ maxlength: "5000", name: "description", placeholder: i18n.mapDescriptionPlaceholder, rows: "3" }, descVal), + br(), br(), + label(i18n.mapTagsLabel), br(), input({ type: "text", name: "tags", placeholder: i18n.mapTagsPlaceholder, value: tagsValue }), - label(i18n.mapTypeLabel), - select({ name: "mapType" }, - option({ value: "SINGLE", ...(mapTypeVal === "SINGLE" ? { selected: true } : {}) }, "SINGLE"), - option({ value: "OPEN", ...(mapTypeVal === "OPEN" ? { selected: true } : {}) }, "OPEN"), - option({ value: "CLOSED", ...(mapTypeVal === "CLOSED" ? { selected: true } : {}) }, "CLOSED")), - br(),br(), - label(i18n.mapMarkerLabelField), - textarea({ name: "markerLabel", placeholder: i18n.mapMarkerLabelPlaceholder, rows: "3" }, markerLabelVal), - label(i18n.markerImageLabel || "Marker Image"), + br(), br(), + showClearnet ? renderClearnetSelector(clearnetOn, i18n) : null, + label(i18n.mapMarkerLabelField), br(), + textarea({ maxlength: "5000", name: "markerLabel", placeholder: i18n.mapMarkerLabelPlaceholder, rows: "3" }, markerLabelVal), + br(), br(), + label(i18n.markerImageLabel || "Marker Image"), br(), input({ type: "file", name: "image", accept: "image/*" }), br(), br(), - label(i18n.mapLatLabel), + label(i18n.mapLatLabel), br(), input({ type: "text", name: "lat", placeholder: i18n.mapLatPlaceholder, value: latVal }), - label(i18n.mapLngLabel), + br(), br(), + label(i18n.mapLngLabel), br(), input({ type: "text", name: "lng", placeholder: i18n.mapLngPlaceholder, value: lngVal }), + br(), br(), div({ class: "map-form-row" }, - button({ type: "submit", attrs: { formmethod: "GET" }, formaction: "/maps", class: "filter-btn" }, i18n.mapAddMarkerButton || "Add Marker"), + button({ type: "submit", attrs: { formmethod: "GET" }, formaction: stepAction, class: "filter-btn" }, i18n.mapAddMarkerButton || "Add Marker"), a({ href: cleanUrl, class: "filter-btn" }, i18n.mapCleanMarkerButton || "Clean Marker")), - renderCoordPreview(latVal, lngVal), - label(i18n.mapZoomLabel || "Zoom"), - select({ name: "zoom" }, - [2, 3, 4, 5, 6, 7, 8].map(z => - option({ value: String(z), ...(zoomVal === z ? { selected: true } : {}) }, String(z)))), - br(),br(), - button({ type: "submit", attrs: { formmethod: "GET" }, formaction: "/maps", class: "filter-btn" }, i18n.mapApplyZoom || "Apply Zoom"), + pickedSpot(latVal, lngVal), + div({ class: "map-toolbar" }, + div({ class: "map-place-search" }, + input({ type: "text", name: "place", value: placeQ, placeholder: i18n.mapPlaceSearchPlaceholder, class: "filter-box__input" }), + button({ type: "submit", attrs: { formmethod: "GET" }, formaction: stepAction, name: "view", value: "", class: "filter-btn" }, i18n.mapSearchButton))), + renderPlaceResults(placeHits, (h) => { + const v = viewForPlace(h); + return button({ type: "submit", attrs: { formmethod: "GET" }, formaction: stepAction, name: "view", value: viewParam(v), class: "map-place-result" }, + span(h.name), h.country ? span({ class: "map-place-result-country" }, h.country) : null); + }), div({ class: "map-form-map-slot" }, - renderMap(pickerMarkers, null, 0, { zoom: zoomVal, centerLat: parseFloat(latVal) || 0, centerLng: parseFloat(lngVal) || 0 })), - button({ type: "submit", class: "create-button" }, filter === "edit" ? i18n.mapUpdateButton : i18n.mapCreateButton)))); + renderMap(pickerMarkers, null, 0, { view, id: "map", nav, pickForm: formId, pickAction: stepAction })), + button({ type: "submit", class: "create-button" }, isEdit ? i18n.mapUpdateButton : i18n.mapCreateButton))); }; -const renderMarkerForm = (mapObj, returnTo, params = {}, tribeMembers = []) => { - if (mapObj.mapType === "SINGLE") return null; - if (mapObj.mapType === "CLOSED" && String(mapObj.author) !== String(userId)) return null; - if (mapObj.mapType === "OPEN" && mapObj.tribeId && !tribeMembers.includes(userId)) return null; - const mkLat = params.mkLat || ""; - const mkLng = params.mkLng || ""; - const zoomVal = parseInt(params.zoom) || 2; +const MARKER_FORM_ID = "add-marker-form"; - const existingMarkers = [{ lat: mapObj.lat, lng: mapObj.lng }].concat( - safeArr(mapObj.markers).map((m) => ({ lat: m.lat, lng: m.lng }))); - if (mkLat && mkLng) existingMarkers.push({ lat: parseFloat(mkLat), lng: parseFloat(mkLng) }); +const canAddMarkers = (mapObj, tribeMembers = []) => { + if (mapObj.mapType === "SINGLE") return false; + if (mapObj.mapType === "CLOSED" && String(mapObj.author) !== String(userId)) return false; + if (mapObj.mapType === "OPEN" && mapObj.tribeId && !safeArr(tribeMembers).includes(userId)) return false; + return true; +}; - const pinLabels = [mapObj.markerLabel || mapObj.description || mapObj.title || ""].concat( - safeArr(mapObj.markers).map((m) => m.label || "")); +const allMarkersOf = (mapObj) => [{ + key: mapObj.key, + lat: mapObj.lat, + lng: mapObj.lng, + label: mapObj.markerLabel || mapObj.description || mapObj.title || i18n.mapMarkerDefault, + image: mapObj.image || "", + author: mapObj.author, + createdAt: mapObj.createdAt, + root: true +}].concat(safeArr(mapObj.markers).map((m) => ({ ...m, root: false }))); - const mkCleanUrl = `/maps/${encodeURIComponent(mapObj.key)}?filter=${encodeURIComponent(params.filter || "all")}`; - const clickUrl = `/maps/${encodeURIComponent(mapObj.key)}?filter=${encodeURIComponent(params.filter || "all")}&zoom=${zoomVal}&`; +const firstLine = (s, n = 140) => { + const line = String(s || "").split(/\r?\n/)[0].trim(); + return line.length > n ? line.slice(0, n - 1) + "…" : line; +}; + +const renderMarkerForm = (mapObj, returnTo, params = {}, tribeMembers = [], ctx = {}) => { + if (!canAddMarkers(mapObj, tribeMembers)) return null; + const mkLat = ctx.mkLat || ""; + const mkLng = ctx.mkLng || ""; + const base = `/maps/${encodeURIComponent(mapObj.key)}`; + const filter = params.filter || "all"; + const viewVal = ctx.view ? viewParam(ctx.view) : ""; + const mkCleanUrl = `${base}?${buildQuery({ filter, view: viewVal, focus: ctx.focus >= 0 ? ctx.focus : undefined })}#add-marker`; return div({ class: "map-marker-form", id: "add-marker" }, h3(i18n.mapAddMarkerTitle), - form({ method: "POST", action: `/maps/${encodeURIComponent(mapObj.key)}/marker`, class: "map-form", enctype: "multipart/form-data" }, - returnTo ? input({ type: "hidden", name: "returnTo", value: returnTo }) : null, + form({ id: MARKER_FORM_ID, method: "POST", action: `${base}/marker`, class: "map-form", enctype: "multipart/form-data" }, + returnTo ? input({ type: "hidden", name: "returnTo", value: returnTo }) : null, + input({ type: "hidden", name: "filter", value: filter }), + viewVal ? input({ type: "hidden", name: "view", value: viewVal }) : null, + ctx.focus >= 0 ? input({ type: "hidden", name: "focus", value: String(ctx.focus) }) : null, + pickedSpot(mkLat, mkLng), label(i18n.mapMarkerLabelField), - textarea({ name: "label", placeholder: i18n.mapMarkerLabelPlaceholder, rows: "3" }, params.mkMarkerLabel || ""), + textarea({ maxlength: "5000", name: "label", placeholder: i18n.mapMarkerLabelPlaceholder, rows: "3" }, params.mkMarkerLabel || ""), label(i18n.markerImageLabel || "Marker Image"), input({ type: "file", name: "image", accept: "image/*" }), - br(),br(), + br(), br(), label(i18n.mapMarkerLatLabel), input({ type: "text", name: "mkLat", placeholder: i18n.mapLatPlaceholder, value: String(mkLat) }), label(i18n.mapMarkerLngLabel), input({ type: "text", name: "mkLng", placeholder: i18n.mapLngPlaceholder, value: String(mkLng) }), - div({ class: "map-form-row" }, - button({ type: "submit", attrs: { formmethod: "GET" }, formaction: `/maps/${encodeURIComponent(mapObj.key)}`, class: "filter-btn" }, i18n.mapAddMarkerButton || "Add Marker"), - a({ href: mkCleanUrl, class: "filter-btn" }, i18n.mapCleanMarkerButton || "Clean Marker")), - renderCoordPreview(mkLat, mkLng), - label(i18n.mapZoomLabel || "Zoom"), - select({ name: "zoom" }, - [2, 3, 4, 5, 6, 7, 8].map(z => - option({ value: String(z), ...(zoomVal === z ? { selected: true } : {}) }, String(z)))), - br(),br(), - button({ type: "submit", attrs: { formmethod: "GET" }, formaction: `/maps/${encodeURIComponent(mapObj.key)}`, class: "filter-btn" }, i18n.mapApplyZoom || "Apply Zoom"), - div({ class: "map-form-map-slot" }, - renderMap(existingMarkers, clickUrl, 0, { latParam: "mkLat", lngParam: "mkLng", pinLabels, pinPrefix: `mk${areaCounter}`, zoom: zoomVal, centerLat: parseFloat(mkLat) || parseFloat(mapObj.lat) || 0, centerLng: parseFloat(mkLng) || parseFloat(mapObj.lng) || 0 })), - button({ type: "submit", class: "create-button" }, i18n.mapAddMarkerButton))); + div({ class: "map-form-row map-form-actions" }, + button({ type: "submit", class: "create-button" }, i18n.mapAddMarkerButton), + a({ href: mkCleanUrl, class: "filter-btn" }, i18n.mapClearPick)))); }; -const renderMarkersList = (markers, mapObj) => { - const allMarkers = []; - if (mapObj) { - allMarkers.push({ - lat: mapObj.lat, - lng: mapObj.lng, - label: mapObj.markerLabel || mapObj.description || mapObj.title || i18n.mapMarkerDefault, - author: mapObj.author, - createdAt: mapObj.createdAt - }); - } - allMarkers.push(...safeArr(markers)); +const popupNotes = (s, n = 600) => { + const text = String(s || "").trim(); + if (text.length <= n) return text; + const cut = text.slice(0, n); + return cut.slice(0, Math.max(cut.lastIndexOf(" "), n - 40)) + "…"; +}; + +const renderPinPopup = (mk, i, ctx = {}) => { + const lat = numOrNull(mk.lat) === null ? 0 : numOrNull(mk.lat); + const lng = numOrNull(mk.lng) === null ? 0 : numOrNull(mk.lng); + const base = ctx.publicBase || ""; + const notes = ctx.notesHtml ? ctx.notesHtml(popupNotes(mk.label)) : renderStyledHtml(popupNotes(mk.label)); + const own = !ctx.readOnly && !mk.root && mk.key && String(mk.author) === String(userId); + const parts = [ + `
${mk.root ? "★" : i}` + + (ctx.closeHref ? `×` : "") + `
`, + isBlob(mk.image) ? `` : "", + notes ? `
${notes}
` : "", + `${lat.toFixed(4)}, ${lng.toFixed(4)}`, + `
` + (ctx.readOnly ? "" : userLink(mk.author).outerHTML) + + (mk.createdAt ? `${escapeHtml(moment(mk.createdAt).fromNow())}` : "") + `
`, + `
` + + (ctx.centerHref ? `${escapeHtml(i18n.mapCenterHere)}` : "") + + (own ? form({ method: "POST", action: contentDeleteAction('mapMarker', mk.key) }, + ctx.returnTo ? input({ type: "hidden", name: "returnTo", value: ctx.returnTo }) : null, + button({ type: "submit", class: "btn-singleview btn-delete", title: i18n.mapDeleteMarker }, renderIcon("close"))).outerHTML : "") + `
` + ]; + return parts.join(""); +}; + +const renderMarkersList = (markers, mapObj, ctx = {}) => { + const allMarkers = mapObj ? allMarkersOf({ ...mapObj, markers }) : safeArr(markers); if (!allMarkers.length) return null; + const focus = Number.isInteger(ctx.focus) ? ctx.focus : -1; + const base = ctx.publicBase || ""; return div({ class: "map-markers-list" }, - h3(i18n.mapMarkersTitle), - br(), - div(allMarkers.flatMap((mk, i) => [ - ...(i > 0 ? [br()] : []), - div({ class: "map-marker-info" }, - span({ class: "map-marker-dot" }, "ꔌ"), - span({ class: "map-marker-coords" }, `${(typeof mk.lat === 'number' ? mk.lat : 0).toFixed(4)}, ${(typeof mk.lng === 'number' ? mk.lng : 0).toFixed(4)}`), - span({ class: "map-marker-meta" }, - userLink(mk.author), - ` · ${moment(mk.createdAt).fromNow()}`)) - ]))); + h3(`${i18n.mapMarkersTitle} (${allMarkers.length})`), + div(allMarkers.map((mk, i) => { + const lat = numOrNull(mk.lat) === null ? 0 : numOrNull(mk.lat); + const lng = numOrNull(mk.lng) === null ? 0 : numOrNull(mk.lng); + const centerHref = ctx.viewHref ? ctx.viewHref({ zoom: MAX_ZOOM, lat, lng }, { focus: i }) : null; + const own = !ctx.readOnly && !mk.root && mk.key && String(mk.author) === String(userId); + return div({ class: "map-marker-row" + (i === focus ? " map-marker-row--focus" : ""), id: `marker-${i}` }, + span({ class: "map-marker-idx" + (mk.root ? " map-marker-idx--main" : "") }, mk.root ? "★" : String(i)), + div({ class: "map-marker-body" }, + div({ class: "map-marker-text" }, + firstLine(mk.label) ? span({ class: "map-marker-label" }, firstLine(mk.label)) : null, + span({ class: "map-marker-coords" }, `${lat.toFixed(4)}, ${lng.toFixed(4)}`), + span({ class: "map-marker-meta" }, + ctx.readOnly ? null : userLink(mk.author), + mk.createdAt ? span({ class: "map-marker-date" }, moment(mk.createdAt).fromNow()) : null)), + isBlob(mk.image) ? img({ src: blobSrc(mk.image, base), class: "map-marker-thumb", alt: "" }) : null), + div({ class: "map-marker-actions" }, + centerHref ? a({ href: centerHref, class: "map-center-link" }, i18n.mapCenterHere) : null, + own ? form({ method: "POST", action: contentDeleteAction('mapMarker', mk.key) }, + ctx.returnTo ? input({ type: "hidden", name: "returnTo", value: ctx.returnTo }) : null, + button({ type: "submit", class: "btn-singleview btn-delete", title: i18n.mapDeleteMarker }, renderIcon("close"))) : null)); + }))); }; -const renderMapCard = (mapObj, filter, params = {}) => { - const returnTo = buildReturnTo(filter, params); - const ownerActions = renderMapOwnerActions(filter, mapObj, params); - const markerCount = safeArr(mapObj.markers).length; +const renderMapCard = exports.renderMapCard = (mapObj, filter, params = {}) => { + const returnTo = params.returnTo || buildReturnTo(filter, params); + const markerCount = safeArr(mapObj.markers).length + 1; const thumbMarkers = [{ lat: mapObj.lat, lng: mapObj.lng }].concat( safeArr(mapObj.markers).map((m) => ({ lat: m.lat, lng: m.lng }))); - const thumbFile = renderMapWithPins(thumbMarkers, 0); - const thumbSrc = thumbFile ? `/mapcache/${thumbFile}` : "/assets/images/worldmap-z2.png"; + const origin = mapObj.tribeOrigin || null; const chips = [ - renderEncryptedChip(i18n), - renderLifespanChip(mapObj.lifetime, i18n) + renderStateChip("whole", "🗺", String(mapObj.mapType || "").toUpperCase()), + origin ? renderTribeOriginChip(origin) : renderEncryptedChip(i18n), + renderLifespanChip(mapObj.lifetime, i18n), + !origin && mapObj.clearnet === true && clearnetEligible(mapObj) ? renderReachChip(true, i18n, mapClearnetHref(mapObj)) : null ].filter(Boolean); const isOwn = mapObj.author && String(mapObj.author) === String(userId); - return div({ class: "trending-card map-card" + (isOwn ? " own-content" : "") }, + const href = origin ? origin.href : `/maps/${encodeURIComponent(mapObj.key)}?filter=${encodeURIComponent(filter)}`; + return div({ class: "tribe-card" + (isOwn ? " own-content" : "") }, div({ class: "card-header activity-card-header" }, - span({ class: "map-type-badge" }, mapObj.mapType), - renderContentActions(mapObj.key, `/maps/${encodeURIComponent(mapObj.key)}?filter=${encodeURIComponent(filter)}`)), - div({ class: "card-section maps-card-body" }, - a({ href: `/maps/${encodeURIComponent(mapObj.key)}?filter=${encodeURIComponent(filter)}`, class: "map-card-thumb-link" }, - { innerHTML: `map` }), - div({ class: "map-card-body" }, - div({ class: "shop-title-row" }, - mapObj.title ? h2({ class: "tribe-card-title" }, a({ href: `/maps/${encodeURIComponent(mapObj.key)}?filter=${encodeURIComponent(filter)}` }, mapObj.title)) : null - ), - chips.length ? div({ class: "card-chips-row" }, ...chips) : null, - div({ class: "map-card-header" }, - div({ class: "map-card-info" }, - span({ class: "map-coords" }, `📍 ${mapObj.lat.toFixed(4)}, ${mapObj.lng.toFixed(4)}`), - markerCount > 0 ? span({ class: "map-marker-count" }, `▾ ${markerCount}`) : null, - mapObj.key ? renderMapUrl(mapObj) : null), - div({ class: "map-card-actions" }, - renderMapFavoriteToggle(mapObj, returnTo), - renderPMButton(mapObj.author), - ...ownerActions)), - safeText(mapObj.description) ? p({ class: "map-description" }, mapObj.description) : null, - (() => { - const btn = renderSpreadButton(mapObj.key, params.spreadMap && params.spreadMap.get(mapObj.key)); - return btn ? div({ class: "card-spread-left" }, btn) : null; - })(), - p({ class: "card-footer" }, - span({ class: "date-link" }, moment(mapObj.createdAt).fromNow()), - span(" · "), - userLink(mapObj.author))))); + span(), + renderContentActions(mapObj.key, href, origin + ? { author: mapObj.author, reportTitle: mapObj.title } + : { spread: params.spreadMap && params.spreadMap.get(mapObj.key) || null, author: mapObj.author, favKind: 'maps', isFavorite: mapObj.isFavorite, reportTitle: mapObj.title, returnTo, deleteAction: isOwn ? contentDeleteAction('map', mapObj.key) : undefined }) + ), + div({ class: "tribe-card-image-wrapper map-card-thumb" }, + a({ href }, renderMap(thumbMarkers, null, 0, { thumb: true, singleZoom: 5 })) + ), + div({ class: "tribe-card-body" }, + div({ class: "shop-title-row" }, + h2({ class: "tribe-card-title" }, a({ href }, mapObj.title || i18n.mapAllSectionTitle)) + ), + chips.length ? div({ class: "card-chips-row" }, ...chips) : null, + p({ class: "job-meta-line" }, `📍 ${mapObj.lat.toFixed(4)}, ${mapObj.lng.toFixed(4)}`), + safeText(mapObj.description) ? p({ class: "tribe-card-description" }, ...renderStyledText(safeText(mapObj.description))) : null, + div({ class: "tribe-card-members" }, + span({ class: "tribe-members-count" }, `${i18n.mapMarkersTitle || "Markers"}: ${markerCount}`) + ) + ) + ); }; const renderMapList = (maps, filter, params = {}) => @@ -392,33 +443,50 @@ const renderMapList = (maps, filter, params = {}) => ? maps.map((mapObj) => renderMapCard(mapObj, filter, params)) : p(params.q ? i18n.mapNoMatch : i18n.noMaps); +exports.renderMapInvitePage = (code) => { + const pageContent = div({ class: "invite-page" }, + h2(i18n.tribeInviteCodeText, code), + form({ method: "GET", action: "/maps" }, + input({ type: "hidden", name: "filter", value: "all" }), + button({ type: "submit", class: "filter-btn" }, i18n.walletBack) + ) + ); + return template(i18n.mapInviteMode || i18n.tribeInviteCodeText || "Invite", section(pageContent)); +}; + exports.mapsView = async (maps, filter = "all", mapId = null, params = {}) => { - const title = filter === "mine" ? i18n.mapMineSectionTitle - : filter === "create" ? i18n.mapCreateSectionTitle - : filter === "edit" ? i18n.mapUpdateSectionTitle - : filter === "recent" ? i18n.mapRecentSectionTitle - : filter === "favorites" ? i18n.mapFavoritesSectionTitle - : i18n.mapAllSectionTitle; + if (filter === "edit") params = { ...params, spreadWarning: await renderSpreadEditWarning(mapId) }; + const title = i18n.mapTitle; const q = safeText(params.q || ""); const list = safeArr(maps); const mapToEdit = mapId ? list.find((m) => m.key === mapId) : null; const allMarkers = list.map((m) => ({ lat: m.lat, lng: m.lng, href: `/maps/${encodeURIComponent(m.key)}` })); + const emptyMod = moduleIsEmpty(list, filter, "all", q); + const censusMp = Array.isArray(params.censusList) ? params.censusList : list; + const mapsChip = (mode) => { + if (mode === filter) return true; + if (mode === "mine") return censusMp.some((x) => String(x.author) === String(userId)); + if (mode === "recent") return censusMp.length > 0; + if (mode === "favorites") return censusMp.some((x) => x.isFavorite); + return true; + }; return template(title, section( - div({ class: "tags-header" }, h2(title), p(i18n.mapDescription)), - renderFilters(filter, q)), + div({ class: "tags-header module-header-line" }, h2(title), p(i18n.mapDescription)), + renderFilters(filter, q, emptyMod, mapsChip)), section( filter === "create" || filter === "edit" ? renderMapForm(filter, mapId, mapToEdit, { ...params, filter }) : section( - div({ class: "maps-search" }, + emptyMod ? null : div({ class: "maps-search activity-filter-chips activity-toolbar-row" }, + renderModuleStats(list.length), form({ method: "GET", action: "/maps", class: "filter-box" }, input({ type: "hidden", name: "filter", value: filter }), input({ type: "text", name: "q", value: q, placeholder: i18n.mapSearchPlaceholder, class: "filter-box__input" }), div({ class: "filter-box__controls" }, button({ type: "submit", class: "filter-box__button" }, i18n.mapSearchButton)))), - div({ class: "maps-list" }, renderMapList(list, filter, { q }))))); + div({ class: "jobs-grid" }, renderMapList(paged(list), filter, { q, spreadMap: params.spreadMap }))))); }; exports.singleMapView = async (mapObj, filter = "all", params = {}) => { @@ -426,74 +494,277 @@ exports.singleMapView = async (mapObj, filter = "all", params = {}) => { const returnTo = safeText(params.returnTo) || buildReturnTo(filter, { q }); const ownerActions = renderMapOwnerActions(filter, mapObj, { q }); const tribeMembers = safeArr(params.tribeMembers); - const zoomVal = parseInt(params.zoom) || 2; + const canClearnet = clearnetEligible(mapObj); + const isClearnet = canClearnet && mapObj.clearnet === true; - const allMarkers = [{ lat: mapObj.lat, lng: mapObj.lng }].concat( - safeArr(mapObj.markers).map((m) => ({ lat: m.lat, lng: m.lng }))); + const base = `/maps/${encodeURIComponent(mapObj.key)}`; + const allMarkers = allMarkersOf(mapObj); + const focusRaw = parseInt(lastOf(params.focus)); + const focus = Number.isInteger(focusRaw) && focusRaw >= 0 && focusRaw < allMarkers.length ? focusRaw : -1; + const picked = parsePick(params.pick); + const mkLat = picked ? String(picked.lat) : safeText(lastOf(params.mkLat)); + const mkLng = picked ? String(picked.lng) : safeText(lastOf(params.mkLng)); + const placeQ = safeText(lastOf(params.place)); + const placeHits = placeQ ? searchPlaces(placeQ, 5) : null; + const explicitView = parseView(params.view); + const view = explicitView + ? makeView(explicitView.lat, explicitView.lng, explicitView.zoom) + : (placeHits && placeHits.length ? viewForPlace(placeHits[0]) : resolveView({ zoom: params.zoom, clat: params.clat, clng: params.clng }, allMarkers, { singleZoom: 8 })); + const baseParams = { filter, q, mkLat: mkLat || undefined, mkLng: mkLng || undefined, focus: focus >= 0 ? focus : undefined }; + const viewHref = viewHrefFor(base, baseParams, "#map"); + const pinHrefs = allMarkers.map((m, i) => viewHref({ zoom: view.zoom, lat: m.lat, lng: m.lng }, { focus: i })); + const canAdd = canAddMarkers(mapObj, tribeMembers); + const pageReturnTo = viewHref(view); + const popup = focus >= 0 ? renderPinPopup(allMarkers[focus], focus, { + closeHref: viewHrefFor(base, { ...baseParams, focus: undefined }, "#map")(view), + centerHref: viewHref({ zoom: MAX_ZOOM, lat: allMarkers[focus].lat, lng: allMarkers[focus].lng }, { focus }), + returnTo: pageReturnTo + }) : null; - const pinLabels = [mapObj.markerLabel || mapObj.description || mapObj.title || ""].concat( - safeArr(mapObj.markers).map((m) => m.label || "")); - const pinImages = [mapObj.image || ""].concat(safeArr(mapObj.markers).map((m) => m.image || "")); + const mapSide = div({ class: "tribe-side map-detail-side" }, + div({ class: "card-header activity-card-header" }, + renderContentActions(mapObj.key, `/maps/${encodeURIComponent(mapObj.key)}`, { + author: mapObj.author, + favKind: 'maps', + isFavorite: mapObj.isFavorite, + spread: params.spreads || null, + returnTo, + reportTitle: mapObj.title, + deleteAction: String(mapObj.author) === String(userId) ? contentDeleteAction('map', mapObj.key) : undefined + }) + ), + div({ class: "shop-title-row" }, + h2({ class: "tribe-card-title" }, mapObj.title || i18n.mapAllSectionTitle) + ), + div({ class: "card-chips-row" }, + renderStateChip("whole", "🗺", String(mapObj.mapType || "").toUpperCase()), + renderEncryptedChip(i18n), + renderLifespanChip(mapObj.lifetime, i18n), + canClearnet ? renderReachChip(isClearnet, i18n, isClearnet ? mapClearnetHref(mapObj) : null) : null, + canClearnet && String(mapObj.author) === String(userId) ? renderClearnetSwitch("maps", mapObj.rootId || mapObj.key, isClearnet) : null + ), + safeText(mapObj.description) ? p({ class: "tribe-side-description" }, ...renderStyledText(safeText(mapObj.description))) : null, + table({ class: "tribe-info-table jobs-info-table" }, + tr( + td({ class: "tribe-info-label" }, i18n.mapLatLabel), + td({ class: "tribe-info-value", colspan: "3" }, mapObj.lat.toFixed(6)) + ), + tr( + td({ class: "tribe-info-label" }, i18n.mapLngLabel), + td({ class: "tribe-info-value", colspan: "3" }, mapObj.lng.toFixed(6)) + ), + tr( + td({ class: "tribe-info-label" }, i18n.createdAtLabel || "Created at"), + td({ class: "tribe-info-value", colspan: "3" }, moment(mapObj.createdAt).format("YYYY/MM/DD HH:mm")) + ), + tr( + td({ class: "tribe-info-value", colspan: "4" }, userLink(mapObj.author)) + ) + ), + renderTags(mapObj.tags), + div({ class: "tribe-card-members" }, + span({ class: "tribe-members-count" }, `${i18n.mapMarkersTitle || "Markers"}: ${safeArr(mapObj.markers).length + 1}`) + ), + (String(mapObj.author) !== String(userId) && !mapObj.tribeId && mapObj.mapType !== "OPEN") + ? div({ class: "tribe-side-actions" }, + a({ class: "tribe-action-btn", href: "/invites?filter=others" }, i18n.tribeEnterInvite) + ) + : null, + renderDocumentActions('maps', mapObj.key), + ownerActions.length ? div({ class: "tribe-side-actions owner-actions" }, ...ownerActions) : null + ); + + const hiddenState = (withView) => [ + input({ type: "hidden", name: "filter", value: filter }), + q ? input({ type: "hidden", name: "q", value: q }) : null, + mkLat && mkLng ? input({ type: "hidden", name: "mkLat", value: mkLat }) : null, + mkLat && mkLng ? input({ type: "hidden", name: "mkLng", value: mkLng }) : null, + focus >= 0 ? input({ type: "hidden", name: "focus", value: String(focus) }) : null, + withView ? input({ type: "hidden", name: "clat", value: String(view.lat) }) : null, + withView ? input({ type: "hidden", name: "clng", value: String(view.lng) }) : null + ]; + + const placeSearch = form({ method: "GET", action: base, class: "filter-box" }, + ...hiddenState(false), + input({ type: "text", name: "place", value: placeQ, placeholder: i18n.mapPlaceSearchPlaceholder, class: "filter-box__input" }), + div({ class: "filter-box__controls" }, button({ type: "submit", class: "filter-box__button" }, i18n.mapSearchButton))); + + const mapHero = div({ class: "map-hero" }, + renderPlaceResults(placeHits, (h) => a({ href: viewHref(viewForPlace(h)), class: "map-place-result" }, + span(h.name), h.country ? span({ class: "map-place-result-country" }, h.country) : null)), + renderMapUrl(mapObj), + renderMap(allMarkers, null, 0, { + id: "map", + view, + focus, + pinHrefs, + popup, + nav: { href: viewHref }, + viewHref, + pick: mkLat && mkLng ? { lat: mkLat, lng: mkLng } : null, + pickForm: canAdd ? MARKER_FORM_ID : null, + pickAction: base + })); + + const mapMain = div({ class: "tribe-main map-detail-main" }, + renderMarkersList(mapObj.markers, mapObj, { focus, view, viewHref, returnTo: pageReturnTo }), + p({ class: "card-footer" }, + span({ class: "date-link" }, `${moment(mapObj.createdAt).format("YYYY/MM/DD HH:mm")}`), + userLink(mapObj.author), + mapObj.updatedAt && mapObj.updatedAt !== mapObj.createdAt + ? span({ class: "votations-comment-date" }, ` · ${i18n.mapUpdatedAt}: ${moment(mapObj.updatedAt).format("YYYY/MM/DD HH:mm")}`) + : null), + renderMarkerForm(mapObj, returnTo, { ...params, filter }, tribeMembers, { mkLat, mkLng, view, focus }) + ); return template(mapObj.title || i18n.mapTitle, - section(renderFilters(filter, q)), - section( - div({ class: "map-detail" }, - mapObj.title ? div({ class: "shop-title-row" }, - h2({ class: "tribe-card-title" }, mapObj.title) - ) : null, - mapObj.title ? div({ class: "card-chips-row" }, - renderEncryptedChip(i18n), - renderLifespanChip(mapObj.lifetime, i18n) - ) : null, - safeText(mapObj.description) ? p({ class: "map-description" }, mapObj.description) : null, - div({ class: "map-detail-header" }, - div({ class: "map-detail-info" }, - span({ class: "map-type-badge" }, mapObj.mapType), - span({ class: "map-coords-detail" }, `📍 ${mapObj.lat.toFixed(6)}, ${mapObj.lng.toFixed(6)}`)), - div({ class: "map-detail-actions" }, - renderMapFavoriteToggle(mapObj, returnTo), - renderPMButton(mapObj.author), - (String(mapObj.author) !== String(userId) && !mapObj.tribeId && mapObj.mapType !== "OPEN") - ? a({ class: "tribe-action-btn", href: "/invites#invites-maps" }, i18n.tribeEnterInvite) - : null, - ...ownerActions)), - renderMapUrl(mapObj), - br(), - form({ method: "GET", action: `/maps/${encodeURIComponent(mapObj.key)}` }, - label(i18n.mapZoomLabel || "Zoom"), - br(), - select({ name: "zoom" }, - [2, 3, 4, 5, 6, 7, 8].map(z => - option({ value: String(z), ...(zoomVal === z ? { selected: true } : {}) }, String(z)))), - br(), br(), - button({ type: "submit", class: "filter-btn" }, i18n.mapApplyZoom || "Apply Zoom")), - br(), - renderMap(allMarkers, null, 0, { pinLabels, pinImages, pinPrefix: `detail${areaCounter}`, zoom: zoomVal, centerLat: parseFloat(mapObj.lat) || 0, centerLng: parseFloat(mapObj.lng) || 0 }), - renderMarkersList(mapObj.markers, mapObj), - renderTags(mapObj.tags), - (() => { - const btn = renderSpreadButton(mapObj.key); - return btn ? div({ class: "card-spread-centered" }, btn) : null; - })(), - br(), - p({ class: "card-footer" }, - span({ class: "date-link" }, `${moment(mapObj.createdAt).format("YYYY/MM/DD HH:mm:ss")} ${i18n.performed} `), - userLink(mapObj.author), - mapObj.updatedAt && mapObj.updatedAt !== mapObj.createdAt - ? span({ class: "votations-comment-date" }, ` · ${i18n.mapUpdatedAt}: ${moment(mapObj.updatedAt).format("YYYY/MM/DD HH:mm:ss")}`) - : null), - renderMarkerForm(mapObj, returnTo, params, tribeMembers)))); + section(div({ class: "tags-header module-header-line" }, h2(i18n.mapTitle), p(i18n.mapDescription))), + section(renderFilters(filter, q, false, null, placeSearch)), + section(mapHero, div({ class: "tribe-details map-detail-grid" }, mapSide, mapMain))); }; -exports.renderMapLocationUrl = (mapUrl) => { +exports.clearnetMapView = async (mapObj, params = {}) => { + const { escapeHtml: esc, renderRichText, renderKindTag, renderTagChips, renderClearnetPage } = require("./clearnet_view"); + const base = clearnetItemHref("maps", mapObj.title, mapObj.rootId || mapObj.key); + const allMarkers = allMarkersOf(mapObj); + const focusRaw = parseInt(lastOf(params.focus)); + const focus = Number.isInteger(focusRaw) && focusRaw >= 0 && focusRaw < allMarkers.length ? focusRaw : -1; + const explicitView = parseView(params.view); + const view = explicitView + ? makeView(explicitView.lat, explicitView.lng, explicitView.zoom) + : resolveView({ zoom: params.zoom, clat: params.clat, clng: params.clng }, allMarkers, { singleZoom: 8 }); + const viewHref = viewHrefFor(base, { focus: focus >= 0 ? focus : undefined }, "#map"); + const pinHrefs = allMarkers.map((m, i) => viewHref({ zoom: view.zoom, lat: m.lat, lng: m.lng }, { focus: i })); + const popup = focus >= 0 ? renderPinPopup(allMarkers[focus], focus, { + readOnly: true, + publicBase: "/c", + notesHtml: renderRichText, + closeHref: viewHrefFor(base, {}, "#map")(view), + centerHref: viewHref({ zoom: MAX_ZOOM, lat: allMarkers[focus].lat, lng: allMarkers[focus].lng }, { focus }) + }) : null; + const mapHtml = renderMap(allMarkers, null, 0, { id: "map", view, focus, pinHrefs, popup, nav: { href: viewHref }, viewHref, publicBase: "/c" }).outerHTML; + const name = mapObj.title || i18n.cnKindMap; + const desc = renderRichText(mapObj.description || ""); + const coord = (v) => (typeof v === "number" ? v : parseFloat(v) || 0).toFixed(4); + const extraCss = ` +.cn-map-title{color:var(--fg);margin:0 0 16px 0;font-size:32px;font-weight:700;word-break:break-word} +.cn-map-meta{display:flex;flex-wrap:wrap;gap:10px;align-items:center;margin-bottom:16px} +.cn-map-desc{color:var(--fg-soft);line-height:1.6;margin:16px 0} +.map-viewer{width:100%} +.map-stage{position:relative;width:100%;background:var(--bg);border:1px solid var(--border);border-radius:8px;overflow:hidden;line-height:0} +.map-svg{display:block;width:100%;height:auto;font-family:inherit} +.map-pins-layer{position:absolute;top:0;left:0;width:100%;height:100%;z-index:4;pointer-events:none} +.map-pins-layer a{pointer-events:auto} +.map-ocean{fill:#a7c6de} +.map-tile{image-rendering:auto} +.map-grat{fill:none;stroke:rgba(255,255,255,.4);stroke-width:.6} +.map-border{fill:none;stroke:rgba(72,52,32,.65);stroke-width:.8;stroke-linejoin:round} +.map-lake{fill:none;stroke:rgba(36,86,128,.7);stroke-width:.7;stroke-linejoin:round} +.map-state{fill:none;stroke:rgba(72,52,32,.5);stroke-width:.6;stroke-dasharray:3 2} +.map-coast{fill:none;stroke:rgba(36,86,128,.75);stroke-width:.8;stroke-linejoin:round} +.map-label-country{fill:#262c31;font-size:14px;font-weight:600;letter-spacing:.6px;text-anchor:middle;paint-order:stroke;stroke:rgba(255,255,255,.85);stroke-width:3px;stroke-linejoin:round} +.map-place-dot{fill:#1f252a;stroke:#fff;stroke-width:1} +.map-place-capital{fill:#d35400} +.map-place-label{fill:#1f252a;font-size:12px;paint-order:stroke;stroke:rgba(255,255,255,.85);stroke-width:3px;stroke-linejoin:round} +.map-place-label-capital{font-weight:700} +.map-pin-body{fill:#3498db;stroke:#fff;stroke-width:1.5} +.map-pin-main .map-pin-body{fill:#e74c3c} +.map-pin-focus .map-pin-body{fill:var(--accent);stroke-width:2.2} +.map-pin-eye{fill:#fff} +.map-pin-label-bg{fill:var(--bg-elev);stroke:var(--border);stroke-width:.8} +.map-pin-focus .map-pin-label-bg{stroke:var(--accent);stroke-width:1.2} +.map-pin-label{fill:var(--fg);font-size:12px;font-weight:700;text-anchor:middle} +.map-pin-img-frame{fill:#fff} +.map-pins-layer a:hover .map-pin-body,.map-pins a:hover .map-pin-body{fill:var(--accent)} +.map-cluster-ring{fill:var(--bg-elev);opacity:.7} +.map-cluster-dot{fill:var(--accent);stroke:#fff;stroke-width:1.6} +.map-cluster-count{fill:#000;font-size:13px;font-weight:700;text-anchor:middle} +.map-scale-line{fill:none;stroke:#1f252a;stroke-width:1.6} +.map-scale-text{fill:#1f252a;font-size:11px;font-weight:600;paint-order:stroke;stroke:rgba(255,255,255,.85);stroke-width:3px} +.map-attrib{fill:#3b434a;font-size:9px;text-anchor:end;paint-order:stroke;stroke:rgba(255,255,255,.75);stroke-width:2px} +.map-controls{position:absolute;z-index:3;display:flex;flex-direction:column;gap:4px;line-height:1} +.map-controls-zoom{top:10px;left:10px} +.map-controls-pan{top:10px;right:10px;display:grid;grid-template-columns:repeat(3,30px);grid-template-rows:repeat(3,30px);gap:2px} +.map-ctrl{width:30px;height:30px;display:inline-flex;align-items:center;justify-content:center;background:var(--bg-elev);color:var(--accent);border:1px solid var(--border);border-radius:6px;text-decoration:none;font-size:17px;font-weight:700;font-family:inherit;box-sizing:border-box} +.map-ctrl:hover{background:var(--accent);color:var(--bg);border-color:var(--accent);text-decoration:none} +.map-ctrl-off{opacity:.35} +.map-ctrl-off:hover{background:var(--bg-elev);color:var(--accent);border-color:var(--border)} +.cn-map-marker{display:flex;flex-wrap:wrap;gap:10px;align-items:center;padding:8px 6px;border-bottom:1px solid var(--border);color:var(--fg-soft)} +.cn-map-marker--focus{background:var(--bg-sub);box-shadow:inset 3px 0 0 var(--accent)} +.cn-map-marker-idx{min-width:24px;height:24px;border-radius:50%;background:#3498db;color:#fff;display:inline-flex;align-items:center;justify-content:center;font-size:12px;font-weight:700} +.cn-map-marker-idx--main{background:#e74c3c} +.cn-map-marker--focus .cn-map-marker-idx{background:var(--accent);color:#000} +.cn-map-marker-thumb{width:48px;height:48px;object-fit:cover;border-radius:4px} +.cn-map-marker-text{flex:1;min-width:160px;display:flex;flex-direction:column;gap:2px} +.cn-map-marker-coords{font-family:monospace;color:var(--fg);font-size:13px} +.cn-map-marker-label{word-break:break-word;color:var(--fg)} +.cn-map-marker-date{font-size:12px;color:var(--fg-dim)} +.cn-map-center{color:var(--accent);text-decoration:none;font-size:13px;border:1px solid var(--border);border-radius:6px;padding:4px 8px;white-space:nowrap;margin-left:auto} +.cn-map-center:hover{background:var(--accent);color:var(--bg);text-decoration:none} +.map-popup{pointer-events:auto} +.map-popup-pointer{fill:var(--bg-elev);stroke:var(--border);stroke-width:1} +.map-popup-wrap{height:100%;display:flex;flex-direction:column;justify-content:flex-end;line-height:1.35} +.map-popup--below .map-popup-wrap{justify-content:flex-start} +.map-popup-card{background:var(--bg-elev);border:1px solid var(--border);border-radius:8px;padding:10px 12px;color:var(--fg);font-size:13px;box-sizing:border-box;max-height:100%;overflow:hidden;display:flex;flex-direction:column;gap:6px;text-align:left} +.map-popup-card>*{flex-shrink:0} +.map-popup-head{display:flex;justify-content:space-between;align-items:center;gap:8px} +.map-popup-head .map-marker-idx{min-width:24px;height:24px;border-radius:50%;background:#3498db;color:#fff;display:inline-flex;align-items:center;justify-content:center;font-size:12px;font-weight:700} +.map-popup-head .map-marker-idx--main{background:#e74c3c} +.map-popup-close{color:var(--fg-dim);text-decoration:none;font-size:20px;line-height:1} +.map-popup-close:hover{color:var(--accent);text-decoration:none} +.map-popup-img{width:100%;max-height:120px;object-fit:cover;border-radius:4px;display:block} +.map-popup-text{color:var(--fg);word-break:break-word;display:-webkit-box;-webkit-line-clamp:4;-webkit-box-orient:vertical;overflow:hidden} +.map-popup-text img{max-width:100%;max-height:100px} +.map-popup-coords{font-family:monospace;font-size:12px;color:var(--fg-soft)} +.map-popup-meta{display:flex;flex-wrap:wrap;gap:6px;align-items:center;font-size:12px;color:var(--fg-dim)} +.map-popup-actions{display:flex;gap:6px;align-items:center} +.map-popup-actions .cn-map-center,.map-popup-actions .map-center-link{margin-left:0;color:var(--accent);text-decoration:none;font-size:13px;border:1px solid var(--border);border-radius:6px;padding:4px 8px} +`; + const markerRow = (mk, i) => { + const centerHref = viewHref({ zoom: MAX_ZOOM, lat: numOrNull(mk.lat) || 0, lng: numOrNull(mk.lng) || 0 }, { focus: i }); + const label = firstLine(mk.label); + return `
` + + `${mk.root ? "★" : i}` + + (isBlob(mk.image) ? `` : "") + + `${label ? `${esc(label)}` : ""}` + + `${coord(mk.lat)}, ${coord(mk.lng)}` + + (mk.createdAt ? `${esc(moment(mk.createdAt).format("YYYY/MM/DD"))}` : "") + + `${esc(i18n.mapCenterHere)}
`; + }; + const body = ` +

${esc(name)}

+
+ ${renderKindTag("map")} + ${mapObj.createdAt ? `📅 ${esc(moment(mapObj.createdAt).format("YYYY/MM/DD"))}` : ""} + 📍 ${coord(mapObj.lat)}, ${coord(mapObj.lng)} +
+ ${desc ? `

${desc}

` : ""} + ${renderTagChips(mapObj.tags)} +
+ ${mapHtml} +

${esc(i18n.mapMarkersTitle || "Markers")} (${allMarkers.length})

+ ${allMarkers.map(markerRow).join("")} +`; + return renderClearnetPage({ + title: `${name} | Oasis`, + ogTitle: name, + ogDescription: mapObj.description || "", + extraCss, + body, + hubFeedId: mapObj.author || null + }); +}; + +exports.renderMapLocationUrl = (rawMapUrl) => { + const mapUrl = localHref(rawMapUrl); if (!mapUrl) return null; return span({ class: "map-location-inline" }, span({ class: "map-location-icon" }, "ꔌ"), a({ href: mapUrl, class: "map-location-link" }, mapUrl)); }; -exports.renderMapLocationVisitLabel = (mapUrl) => { +exports.renderMapLocationVisitLabel = (rawMapUrl) => { + const mapUrl = localHref(rawMapUrl); if (!mapUrl) return null; return div({ class: "card-field" }, span({ class: "card-label" }, (i18n.mapLocationTitle || "Map Location") + ":"), @@ -501,7 +772,8 @@ exports.renderMapLocationVisitLabel = (mapUrl) => { a({ href: mapUrl, class: "map-location-link" }, i18n.mapVisitLabel || "Visit map"))); }; -exports.renderMapEmbed = (mapData, mapUrl) => { +exports.renderMapEmbed = (mapData, rawMapUrl) => { + const mapUrl = localHref(rawMapUrl); if (!mapData || (parseFloat(mapData.lat) === 0 && parseFloat(mapData.lng) === 0)) return exports.renderMapLocationVisitLabel(mapUrl); return div({ class: "map-embed-section" }, @@ -512,7 +784,8 @@ exports.renderMapEmbed = (mapData, mapUrl) => { a({ href: mapUrl, class: "map-location-link" }, mapUrl)) : null); }; -exports.renderMapEmbedWithZoom = (mapData, mapUrl, detailUrl, zoom) => { +exports.renderMapEmbedWithZoom = (mapData, rawMapUrl, detailUrl, zoom) => { + const mapUrl = localHref(rawMapUrl); if (!mapData || (parseFloat(mapData.lat) === 0 && parseFloat(mapData.lng) === 0)) return exports.renderMapLocationVisitLabel(mapUrl); const zoomVal = parseInt(zoom) || 2; @@ -520,15 +793,6 @@ exports.renderMapEmbedWithZoom = (mapData, mapUrl, detailUrl, zoom) => { const lo = parseFloat(mapData.lng) || 0; return div({ class: "map-embed-section" }, span({ class: "card-label" }, (i18n.mapLocationTitle || "Map Location") + ":"), - form({ method: "GET", action: detailUrl }, - label(i18n.mapZoomLabel || "Zoom"), - br(), - select({ name: "zoom" }, - [2, 3, 4, 5, 6, 7, 8].map(z => - option({ value: String(z), ...(zoomVal === z ? { selected: true } : {}) }, String(z)))), - br(), br(), - button({ type: "submit", class: "filter-btn" }, i18n.mapApplyZoom || "Apply Zoom")), - br(), renderMap([{ lat: la, lng: lo }], null, 0, { zoom: zoomVal, centerLat: la, centerLng: lo }), mapUrl ? div({ class: "map-embed-url" }, a({ href: mapUrl, class: "map-location-link" }, mapUrl)) : null); @@ -537,5 +801,5 @@ exports.renderMapEmbedWithZoom = (mapData, mapUrl, detailUrl, zoom) => { exports.renderMapLocationGrid = (lat, lng) => { if (lat === undefined || lng === undefined) return null; return div({ class: "map-location-embed" }, - renderMap([{ lat: parseFloat(lat) || 0, lng: parseFloat(lng) || 0 }], null, 0)); + renderMap([{ lat: parseFloat(lat) || 0, lng: parseFloat(lng) || 0 }], null, 0, { zoom: MIN_ZOOM, centerLat: parseFloat(lat) || 0, centerLng: parseFloat(lng) || 0 })); }; diff --git a/src/views/markdown.js b/src/views/markdown.js deleted file mode 100644 index 4e5d040..0000000 --- a/src/views/markdown.js +++ /dev/null @@ -1,43 +0,0 @@ -"use strict"; - -const md = require("../server/node_modules/ssb-markdown"); -const ssbMessages = require("../server/node_modules/ssb-msgs"); -const ssbRef = require("../server/node_modules/ssb-ref"); -const { span } = require("../server/node_modules/hyperaxe"); - -const toUrl = (mentions) => { - const mentionNames = []; - - const handleLink = ({ name, link }) => { - if (typeof name === "string") { - const atName = name.charAt(0) === "@" ? name : `@${name}`; - mentionNames.push({ name: atName, link }); - } - }; - - ssbMessages.links(mentions, "feed").forEach(handleLink); - - const urlHandler = (ref) => { - const found = mentionNames.find(({ name }) => name === ref); - if (found) return `/author/${encodeURIComponent(found.link)}`; - - if (ssbRef.isFeedId(ref)) return `/author/${encodeURIComponent(ref)}`; - if (ssbRef.isMsgId(ref)) return `/thread/${encodeURIComponent(ref)}`; - - const splitIndex = ref.indexOf("?"); - const blobRef = splitIndex === -1 ? ref : ref.slice(0, splitIndex); - - if (ssbRef.isBlobId(blobRef)) return `/blob/${encodeURIComponent(blobRef)}`; - if (ref && ref[0] === "#") return `/hashtag/${encodeURIComponent(ref.substr(1))}`; - - return ""; - }; - - return urlHandler; -}; - -module.exports = (input, mentions = []) => - md.block(input, { - toUrl: toUrl(mentions), - emoji: (character) => span({ class: "emoji" }, character).outerHTML, - }); diff --git a/src/views/market_view.js b/src/views/market_view.js index 019d4f3..99098c2 100644 --- a/src/views/market_view.js +++ b/src/views/market_view.js @@ -1,25 +1,31 @@ -const { div, h2, p, section, button, form, a, span, textarea, br, input, label, select, option, img, table, tr, th, td, progress, video, audio } = require("../server/node_modules/hyperaxe") -const { template, i18n, userLink, renderStateChip, renderVisibilityChip, renderLifespanChip, renderEcoTax, renderSpreadButton } = require("./main_views") +const { div, h2, p, section, button, form, a, span, textarea, br, input, label, select, option, img, table, tr, th, td, progress, video, audio, hr } = require("../server/node_modules/hyperaxe") +const { renderZoomableImage } = require("./gallery_view") +const { renderCommentsSection: renderSharedCommentsSection } = require("./comments_view"); +const { clearnetItemHref, template, i18n, userLink, renderStateChip, renderVisibilityChip, renderLifespanChip, renderEcoTax, renderSpreadButton, renderSpreadEditWarning, renderOpinionsVoting, renderEngagement , renderContentActions, renderModuleStatsBy, moduleIsEmpty, renderEcoValueChip, renderWalletChip, contentDeleteAction, paged } = require("./main_views") +const { renderReachChip, renderClearnetSelector, renderClearnetSwitch } = require("./clearnet_view") +const opinionCategories = require("../backend/opinion_categories") const moment = require("../server/node_modules/moment") const { config } = require("../server/SSB_server.js") -const { renderUrl } = require("../backend/renderUrl") +const { renderStyledText } = require("../backend/renderStyledText") const { renderMapLocationUrl, renderMapEmbed, renderMapLocationVisitLabel, renderMapEmbedWithZoom } = require("./maps_view") const renderMediaBlob = (value, fallbackSrc = null, attrs = {}) => { if (!value) return fallbackSrc ? img({ src: fallbackSrc, ...attrs }) : null const s = String(value).trim() if (!s) return fallbackSrc ? img({ src: fallbackSrc, ...attrs }) : null - if (s.startsWith('&')) return img({ src: `/blob/${encodeURIComponent(s)}`, ...attrs }) + if (s.startsWith('&')) return renderZoomableImage(`/blob/${encodeURIComponent(s)}`, { imgClass: attrs.class || 'post-image', alt: attrs.alt || '' }) const mVideo = s.match(/\[video:[^\]]*\]\(\s*(&[^)\s]+\.sha256)\s*\)/) - if (mVideo) return video({ controls: true, class: attrs.class || 'post-video', src: `/blob/${encodeURIComponent(mVideo[1])}` }) + if (mVideo) return video({ controls: true, class: attrs.class || 'post-video', src: `/blob/${encodeURIComponent(mVideo[1])}`, preload: 'metadata' }) const mAudio = s.match(/\[audio:[^\]]*\]\(\s*(&[^)\s]+\.sha256)\s*\)/) if (mAudio) return audio({ controls: true, class: attrs.class || 'post-audio', src: `/blob/${encodeURIComponent(mAudio[1])}` }) const mImg = s.match(/!\[[^\]]*\]\(\s*(&[^)\s]+\.sha256)\s*\)/) - if (mImg) return img({ src: `/blob/${encodeURIComponent(mImg[1])}`, class: attrs.class || 'post-image' }) + if (mImg) return renderZoomableImage(`/blob/${encodeURIComponent(mImg[1])}`, { imgClass: attrs.class || 'post-image', alt: attrs.alt || '' }) return fallbackSrc ? img({ src: fallbackSrc, ...attrs }) : null } const userId = config.keys.id +const MARKET_REACH = ["PUBLIC", "HIDDEN"] +const marketReachLabel = (v) => v === "HIDDEN" ? (i18n.visibilityHidden || "Hidden") : (i18n.visibilityPublic || "Public") const parseBidEntry = (raw) => { const s = String(raw || "").trim() @@ -95,17 +101,23 @@ const buildReturnTo = (filter, q, minPrice, maxPrice, sort) => { return `/market${params.length ? `?${params.join("&")}` : ""}` } + +const sumCats = (opinions = {}, cats = []) => (cats || []).reduce((acc, c) => acc + (Number((opinions || {})[c]) || 0), 0) + +const renderStarRating = (opinions, voterCount) => { + const pos = sumCats(opinions, opinionCategories.positive) + const neg = sumCats(opinions, opinionCategories.constructive) + sumCats(opinions, opinionCategories.moderation) + const total = pos + neg + const full = total > 0 ? Math.round((pos / total) * 5) : 0 + return span({ class: "shop-product-stars" }, "★".repeat(full) + "☆".repeat(5 - full) + ` (${voterCount})`) +} + const renderCardField = (labelText, value = "") => - div({ class: "card-field" }, span({ class: "card-label" }, labelText), span({ class: "card-value" }, ...renderUrl(String(value)))) + div({ class: "card-field" }, span({ class: "card-label" }, labelText), span({ class: "card-value" }, ...renderStyledText(String(value)))) const renderCardFieldRich = (labelText, parts) => div({ class: "card-field" }, span({ class: "card-label" }, labelText), span({ class: "card-value" }, ...(Array.isArray(parts) ? parts : [parts]))) -const renderPmButton = (recipientId) => - recipientId && String(recipientId) !== String(userId) - ? form({ method: "GET", action: "/pm" }, input({ type: "hidden", name: "recipients", value: recipientId }), button({ type: "submit", class: "filter-btn" }, i18n.privateMessage)) - : null - const renderStockBar = (stockValue, maxValue) => { const s = Math.max(0, Number(stockValue || 0)) const m = Math.max(1, Number(maxValue || s || 1)) @@ -121,59 +133,12 @@ const renderStockBar = (stockValue, maxValue) => { } const renderMarketCommentsSection = (itemId, returnTo, comments = []) => { - const commentsCount = Array.isArray(comments) ? comments.length : 0 - - return div( - { class: "vote-comments-section market-comments" }, - div({ class: "comments-count" }, span({ class: "card-label" }, i18n.voteCommentsLabel + ": "), span({ class: "card-value" }, String(commentsCount))), - div( - { class: "comment-form-wrapper" }, - h2({ class: "comment-form-title" }, i18n.voteNewCommentLabel), - form( - { method: "POST", action: `/market/${encodeURIComponent(itemId)}/comments`, class: "comment-form", enctype: "multipart/form-data" }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - textarea({ id: "comment-text", name: "text", rows: 4, class: "comment-textarea", placeholder: i18n.voteNewCommentPlaceholder }), - div({ class: "comment-file-upload" }, label(i18n.uploadMedia), input({ type: "file", name: "blob" })), - br(), - button({ type: "submit", class: "comment-submit-btn" }, i18n.voteNewCommentButton) - ) - ), - (() => { - const visibleComments = (comments || []).filter(c => { - const t = c && c.value && c.value.content && c.value.content.text - return t && String(t).trim() - }) - return visibleComments.length - ? div( - { class: "comments-list" }, - visibleComments.map((c) => { - const author = c.value && c.value.author ? c.value.author : "" - const ts = c.value && c.value.timestamp ? c.value.timestamp : c.timestamp - const absDate = ts ? moment(ts).format("YYYY/MM/DD HH:mm:ss") : "" - const relDate = ts ? moment(ts).fromNow() : "" - const userName = author && author.includes("@") ? author.split("@")[1] : author - const rootId = c.value && c.value.content ? c.value.content.fork || c.value.content.root : null - const text = c.value && c.value.content && c.value.content.text ? c.value.content.text : "" - - return div( - { class: "votations-comment-card" }, - span( - { class: "created-at" }, - span(i18n.createdBy), - author ? userLink(author) : span("(unknown)"), - absDate ? span(" | ") : "", - absDate ? span({ class: "votations-comment-date" }, absDate) : "", - relDate ? span({ class: "votations-comment-date" }, " | ", i18n.sendTime) : "", - relDate && rootId ? a({ href: `/thread/${encodeURIComponent(rootId)}#${encodeURIComponent(c.key)}` }, relDate) : "" - ), - p({ class: "votations-comment-text" }, ...renderUrl(text)) - ) - }) - ) - : p({ class: "votations-no-comments" }, i18n.voteNoCommentsYet) - })() - ) -} + return renderSharedCommentsSection({ + action: `/market/${encodeURIComponent(itemId)}/comments`, + comments: comments, + returnTo: returnTo + }); +}; const isMyBidItem = (item) => { const polls = Array.isArray(item.auctions_poll) ? item.auctions_poll : [] @@ -197,7 +162,7 @@ const auctionCountdownParts = (deadline) => { const renderCountdownField = (item) => { const cd = item && (item.item_type === "auction" || item.item_type === "exchange") ? auctionCountdownParts(item.deadline) : null if (!cd) return null - return renderCardFieldRich(`${cd.label}:`, [span({ class: "countdown-strong" }, cd.rel)]) + return renderCardFieldRich(`${cd.label}:`, [span({ class: "time-chip" }, cd.rel)]) } const normStatus = (s) => String(s || "").toUpperCase().replace(/_/g, " ").replace(/\s+/g, " ").trim() @@ -217,13 +182,6 @@ const renderMarketOwnerActions = (item, returnTo) => { ) ) } - out.push( - form( - { method: "POST", action: `/market/delete/${encodeURIComponent(item.id)}` }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - button({ class: "delete-btn", type: "submit" }, i18n.marketActionsDelete) - ) - ) if (canChange) { out.push( form( @@ -231,9 +189,9 @@ const renderMarketOwnerActions = (item, returnTo) => { input({ type: "hidden", name: "returnTo", value: returnTo }), select( { name: "status", class: "project-control-select" }, - option({ value: "FOR SALE", selected: cur === "FOR SALE" }, i18n.marketFilterForSale), - option({ value: "SOLD", selected: cur === "SOLD" }, i18n.marketFilterSold), - option({ value: "DISCARDED", selected: cur === "DISCARDED" }, i18n.marketFilterDiscarded) + option({ value: "FOR SALE", ...(cur === "FOR SALE" ? { selected: true } : {})}, i18n.marketFilterForSale), + option({ value: "SOLD", ...(cur === "SOLD" ? { selected: true } : {})}, i18n.marketFilterSold), + option({ value: "DISCARDED", ...(cur === "DISCARDED" ? { selected: true } : {})}, i18n.marketFilterDiscarded) ), button({ class: "status-btn project-control-btn", type: "submit" }, i18n.marketActionsChangeStatus) ) @@ -242,16 +200,22 @@ const renderMarketOwnerActions = (item, returnTo) => { return out } -const renderMarketTopbar = (item, returnTo) => { - const left = [renderPmButton(item && item.seller)].filter(Boolean) - const right = item && String(item.seller) === String(userId) ? renderMarketOwnerActions(item, returnTo) : [] - const leftNode = left.length ? div({ class: "bookmark-topbar-left transfer-topbar-left" }, ...left) : null - const rightNode = right.length ? div({ class: "bookmark-actions transfer-actions" }, ...right) : null - const children = [] - if (leftNode) children.push(leftNode) - if (rightNode) children.push(rightNode) - return children.length ? div({ class: "bookmark-topbar transfer-topbar-single" }, ...children) : null -} +const marketChipFor = (filter, list) => (mode) => { + if (mode === filter) return true; + if (!Array.isArray(list)) return true; + if (mode === "recent") return list.length > 0; + if (mode === "mine") return list.some((e) => String(e.seller) === String(userId)); + if (mode === "exchange") return list.some((e) => e.item_type === "exchange" && e.status === "FOR SALE"); + if (mode === "auctions") return list.some((e) => e.item_type === "auction" && e.status === "FOR SALE"); + if (mode === "mybids") return list.some(isMyBidItem); + if (mode === "new") return list.some((e) => e.item_status === "NEW" && e.status === "FOR SALE"); + if (mode === "used") return list.some((e) => e.item_status === "USED" && e.status === "FOR SALE"); + if (mode === "broken") return list.some((e) => e.item_status === "BROKEN" && e.status === "FOR SALE"); + if (mode === "for sale") return list.some((e) => normStatus(e.status || "FOR SALE") === "FOR SALE"); + if (mode === "sold") return list.some((e) => normStatus(e.status || "") === "SOLD"); + if (mode === "discarded") return list.some((e) => normStatus(e.status || "") === "DISCARDED"); + return true; +}; exports.marketView = async (items, filter, itemToEdit = null, params = {}) => { const list = Array.isArray(items) ? items : [] @@ -260,21 +224,7 @@ exports.marketView = async (items, filter, itemToEdit = null, params = {}) => { const maxPrice = params.maxPrice const sort = params.sort || "recent" - let title = i18n.marketAllSectionTitle - switch (filter) { - case "mine": - title = i18n.marketMineSectionTitle - break - case "create": - title = i18n.marketCreateSectionTitle - break - case "edit": - title = i18n.marketUpdateSectionTitle - break - case "mybids": - title = i18n.marketFilterMyBids - break - } + const title = i18n.marketTitle let filtered = [] switch (filter) { @@ -326,6 +276,8 @@ exports.marketView = async (items, filter, itemToEdit = null, params = {}) => { filtered = filtered.filter((it) => matchesQuery(it, q)).filter((it) => withinPrice(it, minP, maxP)) filtered = sortItems(filtered, sort) + const emptyMod = moduleIsEmpty(list, filter || "all", "all", q || minPrice || maxPrice); + const marketChipVisible = marketChipFor(filter, list); const returnTo = buildReturnTo(filter, q, minPrice, maxPrice, sort) const itemEdit = itemToEdit || {} @@ -337,43 +289,47 @@ exports.marketView = async (items, filter, itemToEdit = null, params = {}) => { ] const isFormMode = filter === "create" || filter === "edit" + const spreadWarning = filter === "edit" ? await renderSpreadEditWarning(itemEdit && (itemEdit.id || itemEdit.key)) : null + const reachRaw = String(params.reach || "").toUpperCase() + const ownReach = String(itemEdit.visibility || "").toUpperCase() + const reach = MARKET_REACH.includes(reachRaw) ? reachRaw : (MARKET_REACH.includes(ownReach) ? ownReach : "PUBLIC") + const prefillKeys = ["industry", "title", "description", "price", "tags", "stock"] return template( title, section( - div({ class: "tags-header" }, h2(i18n.marketTitle), p(i18n.marketDescription)), + div({ class: "tags-header module-header-line" }, h2(i18n.marketTitle), p(i18n.marketDescription), renderWalletChip()), div( { class: "filters" }, form( { method: "GET", action: "/market", class: "ui-toolbar ui-toolbar--filters" }, ...hiddenCtx, - button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterAll), - button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterMine), - button({ type: "submit", name: "filter", value: "exchange", class: filter === "exchange" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterItems), - button({ type: "submit", name: "filter", value: "auctions", class: filter === "auctions" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterAuctions), - button({ type: "submit", name: "filter", value: "mybids", class: filter === "mybids" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterMyBids), - button({ type: "submit", name: "filter", value: "new", class: filter === "new" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterNew), - button({ type: "submit", name: "filter", value: "used", class: filter === "used" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterUsed), - button({ type: "submit", name: "filter", value: "broken", class: filter === "broken" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterBroken), - button({ type: "submit", name: "filter", value: "for sale", class: filter === "for sale" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterForSale), - button({ type: "submit", name: "filter", value: "sold", class: filter === "sold" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterSold), - button({ type: "submit", name: "filter", value: "discarded", class: filter === "discarded" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterDiscarded), - button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterRecent), + ...(emptyMod ? [] : [ + button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterAll).toUpperCase()), + ...(marketChipVisible("recent") ? [button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterRecent).toUpperCase())] : []), + ...(marketChipVisible("mine") ? [button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterMine).toUpperCase())] : []), + ...(marketChipVisible("exchange") ? [button({ type: "submit", name: "filter", value: "exchange", class: filter === "exchange" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterItems).toUpperCase())] : []), + ...(marketChipVisible("auctions") ? [button({ type: "submit", name: "filter", value: "auctions", class: filter === "auctions" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterAuctions).toUpperCase())] : []), + ...(marketChipVisible("mybids") ? [button({ type: "submit", name: "filter", value: "mybids", class: filter === "mybids" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterMyBids).toUpperCase())] : []), + ...(marketChipVisible("new") ? [button({ type: "submit", name: "filter", value: "new", class: filter === "new" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterNew).toUpperCase())] : []), + ...(marketChipVisible("used") ? [button({ type: "submit", name: "filter", value: "used", class: filter === "used" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterUsed).toUpperCase())] : []), + ...(marketChipVisible("broken") ? [button({ type: "submit", name: "filter", value: "broken", class: filter === "broken" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterBroken).toUpperCase())] : []), + ...(marketChipVisible("for sale") ? [button({ type: "submit", name: "filter", value: "for sale", class: filter === "for sale" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterForSale).toUpperCase())] : []), + ...(marketChipVisible("sold") ? [button({ type: "submit", name: "filter", value: "sold", class: filter === "sold" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterSold).toUpperCase())] : []), + ...(marketChipVisible("discarded") ? [button({ type: "submit", name: "filter", value: "discarded", class: filter === "discarded" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterDiscarded).toUpperCase())] : []), + ]), button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.marketCreateButton) ) ), - !isFormMode + !isFormMode && !emptyMod ? div( - { class: "market-search" }, + { class: "market-search activity-filter-chips activity-toolbar-row" }, + renderModuleStatsBy(filtered, e => normStatus(e.status || 'FOR SALE'), [{ value: 'FOR SALE', label: i18n.marketFilterForSale }, { value: 'SOLD', label: i18n.marketFilterSold }, { value: 'DISCARDED', label: i18n.marketFilterDiscarded }]), form( { method: "GET", action: "/market", class: "filter-box" }, input({ type: "hidden", name: "filter", value: filter || "all" }), input({ type: "text", name: "q", value: q, placeholder: i18n.marketSearchPlaceholder, class: "filter-box__input" }), - div( - { class: "filter-box__controls" }, - div( - { class: "transfer-range" }, - input({ + input({ type: "number", name: "minPrice", step: "0.000001", @@ -390,16 +346,16 @@ exports.marketView = async (items, filter, itemToEdit = null, params = {}) => { value: String(maxPrice ?? ""), placeholder: i18n.marketMaxPriceLabel, class: "filter-box__number transfer-amount-input" - }) - ), + }), + select( { name: "sort", class: "filter-box__select" }, - option({ value: "recent", selected: sort === "recent" }, i18n.marketSortRecent), - option({ value: "price", selected: sort === "price" }, i18n.marketSortPrice), - option({ value: "deadline", selected: sort === "deadline" }, i18n.marketSortDeadline) + option({ value: "recent", ...(sort === "recent" ? { selected: true } : {})}, i18n.marketSortRecent), + option({ value: "price", ...(sort === "price" ? { selected: true } : {})}, i18n.marketSortPrice), + option({ value: "deadline", ...(sort === "deadline" ? { selected: true } : {})}, i18n.marketSortDeadline) ), button({ type: "submit", class: "filter-box__button" }, i18n.marketSearchButton) - ) + ) ) : null @@ -408,26 +364,46 @@ exports.marketView = async (items, filter, itemToEdit = null, params = {}) => { isFormMode ? div( { class: "market-form" }, + spreadWarning, + form( + { method: "GET", action: filter === "edit" ? `/market/edit/${encodeURIComponent(itemEdit.id)}` : "/market" }, + filter === "edit" ? null : input({ type: "hidden", name: "filter", value: "create" }), + ...(filter === "edit" ? [] : prefillKeys.filter((k) => params[k]).map((k) => input({ type: "hidden", name: k, value: params[k] }))), + label(i18n.visibilityLabel || "Visibility"), + br(), + div({ class: "apply-row" }, + select( + { name: "visibility", class: "report-category-select" }, + option({ value: "PUBLIC", ...(reach === "PUBLIC" ? { selected: true } : {})}, i18n.visibilityPublic || "Public"), + option({ value: "HIDDEN", ...(reach === "HIDDEN" ? { selected: true } : {})}, i18n.visibilityHidden || "Hidden") + ), + button({ type: "submit", class: "create-button" }, i18n.apply || "Apply") + ) + ), + hr({ class: "form-sep" }), + h2({ class: "report-category-fixed" }, marketReachLabel(reach)), form( { action: filter === "edit" ? `/market/update/${encodeURIComponent(itemEdit.id)}` : "/market/create", method: "POST", enctype: "multipart/form-data" }, input({ type: "hidden", name: "returnTo", value: "/market?filter=mine" }), + input({ type: "hidden", name: "visibility", value: reach }), + ((itemEdit && itemEdit.industry) || params.industry) ? input({ type: "hidden", name: "industry", value: (itemEdit && itemEdit.industry) || params.industry }) : null, label(i18n.marketItemType), br(), select( { name: "item_type", id: "item_type", required: true }, - option({ value: "auction", selected: itemEdit && itemEdit.item_type === "auction" }, "Auction"), - option({ value: "exchange", selected: itemEdit && itemEdit.item_type === "exchange" }, "Exchange") + option({ value: "auction", ...(itemEdit && itemEdit.item_type === "auction" ? { selected: true } : {})}, "Auction"), + option({ value: "exchange", ...(itemEdit && itemEdit.item_type === "exchange" ? { selected: true } : {})}, "Exchange") ), br(), br(), label(i18n.marketItemTitle), br(), - input({ type: "text", name: "title", id: "title", value: (itemEdit && itemEdit.title) || "", required: true }), + input({ type: "text", name: "title", maxlength: "100", id: "title", value: (itemEdit && itemEdit.title) || params.title || "", required: true }), br(), br(), label(i18n.marketItemDescription), br(), - textarea({ name: "description", id: "description", placeholder: i18n.marketItemDescriptionPlaceholder, rows: "6", required: true }, (itemEdit && itemEdit.description) || ""), + textarea({ maxlength: "5000", name: "description", id: "description", placeholder: i18n.marketItemDescriptionPlaceholder, rows: "6", required: true }, (itemEdit && itemEdit.description) || params.description || ""), br(), br(), label(i18n.marketCreateFormImageLabel), @@ -439,15 +415,15 @@ exports.marketView = async (items, filter, itemToEdit = null, params = {}) => { br(), select( { name: "item_status", id: "item_status" }, - option({ value: "BROKEN", selected: itemEdit && itemEdit.item_status === "BROKEN" }, "BROKEN"), - option({ value: "USED", selected: itemEdit && itemEdit.item_status === "USED" }, "USED"), - option({ value: "NEW", selected: itemEdit && itemEdit.item_status === "NEW" }, "NEW") + option({ value: "BROKEN", ...(itemEdit && itemEdit.item_status === "BROKEN" ? { selected: true } : {})}, "BROKEN"), + option({ value: "USED", ...(itemEdit && itemEdit.item_status === "USED" ? { selected: true } : {})}, "USED"), + option({ value: "NEW", ...(itemEdit && itemEdit.item_status === "NEW" ? { selected: true } : {})}, "NEW") ), br(), br(), label(i18n.marketItemStock), br(), - input({ type: "number", name: "stock", id: "stock", value: (itemEdit && itemEdit.stock) || 1, required: true, min: "1", step: "1" }), + input({ type: "number", name: "stock", id: "stock", value: (itemEdit && itemEdit.stock) || params.stock || 1, required: true, min: "1", step: "1" }), br(), br(), label(i18n.mapLocationTitle || "Map Location"), @@ -455,23 +431,16 @@ exports.marketView = async (items, filter, itemToEdit = null, params = {}) => { input({ type: "text", name: "mapUrl", placeholder: i18n.mapUrlPlaceholder || "/maps/MAP_ID", value: itemEdit?.mapUrl || "" }), br(), br(), - label(i18n.visibilityLabel || "Visibility"), - br(), - select( - { name: "visibility" }, - option({ value: "PUBLIC", selected: (itemEdit?.visibility || "PUBLIC") === "PUBLIC" }, i18n.visibilityPublic || "Public"), - option({ value: "HIDDEN", selected: itemEdit?.visibility === "HIDDEN" }, i18n.visibilityHidden || "Hidden") - ), - br(), - br(), + ...(reach === "PUBLIC" ? [renderClearnetSelector(itemEdit.clearnet === true || String(itemEdit.clearnet || "") === "1", i18n), br()] : []), label(i18n.marketItemPrice), br(), - input({ type: "number", name: "price", id: "price", value: (itemEdit && itemEdit.price) || "", required: true, step: "0.000001", min: "0.000001" }), + input({ type: "number", name: "price", id: "price", value: (itemEdit && itemEdit.price) || params.price || "", required: true, step: "0.000001", min: "0.000001" }), + renderEcoValueChip(), br(), br(), label(i18n.marketItemTags), br(), - input({ type: "text", name: "tags", id: "tags", placeholder: i18n.marketItemTagsPlaceholder, value: (itemEdit && itemEdit.tags && itemEdit.tags.join(", ")) || "" }), + input({ type: "text", name: "tags", id: "tags", placeholder: i18n.marketItemTagsPlaceholder, value: (itemEdit && itemEdit.tags && itemEdit.tags.join(", ")) || params.tags || "" }), br(), br(), label(i18n.marketItemDeadline), @@ -495,7 +464,7 @@ exports.marketView = async (items, filter, itemToEdit = null, params = {}) => { name: "includesShipping", value: "1", class: "meme-checkbox", - ...(itemEdit && itemEdit.includesShipping ? { checked: true } : {}) + ...(itemEdit && [true, "1", "on", "true"].includes([].concat(itemEdit.includesShipping).pop()) ? { checked: true } : {}) }), br(),br(), button({ type: "submit" }, filter === "edit" ? i18n.marketUpdateButton : i18n.marketCreateButton) @@ -504,75 +473,45 @@ exports.marketView = async (items, filter, itemToEdit = null, params = {}) => { : div( { class: "market-grid" }, filtered.length > 0 - ? filtered.map((item) => { - const polls = Array.isArray(item.auctions_poll) ? item.auctions_poll : [] - const parsedBids = polls.map(parseBidEntry).filter(Boolean).sort((a, b) => new Date(b.time) - new Date(a.time)) - const myBid = item.item_type === "auction" ? parsedBids.some((b) => b.bidder === userId) : false - const maxStock = item.initialStock || item.stockMax || item.stock || 1 - const stockLeft = Number(item.stock || 0) + ? paged(filtered).map((item) => { const isOwner = String(item.seller) === String(userId) - const actionNodesRaw = isOwner - ? renderMarketOwnerActions(item, "/market?filter=mine") - : [ - item.status !== "SOLD" && item.status !== "DISCARDED" && item.item_type === "auction" - ? form( - { method: "POST", action: `/market/bid/${encodeURIComponent(item.id)}` }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - input({ type: "number", name: "bidAmount", step: "0.000001", min: "0.000001", placeholder: i18n.marketYourBid, required: true }), - br(), - button({ class: "buy-btn", type: "submit" }, i18n.marketPlaceBidButton) - ) - : null, - item.status === "FOR SALE" && item.item_type !== "auction" && !isOwner && stockLeft > 0 - ? form( - { method: "POST", action: `/market/buy/${encodeURIComponent(item.id)}` }, - input({ type: "hidden", name: "returnTo", value: "/inbox?filter=sent" }), - input({ type: "hidden", name: "buyerId", value: userId }), - button({ class: "buy-btn", type: "submit" }, i18n.marketActionsBuy) - ) - : null - ].filter(Boolean) - return div({ class: "tribe-card market-tribe-card" }, div({ class: "tribe-card-image-wrapper" }, a({ href: `/market/${encodeURIComponent(item.id)}` }, renderMediaBlob(item.image, '/assets/images/default-market.png', { class: 'tribe-card-hero-image' }) ) ), + div({ class: "card-header activity-card-header" }, + span(), + renderContentActions(item.id, `/market/${encodeURIComponent(item.id)}`, { spread: (params.spreadMap && params.spreadMap.get(item.id)) || null, author: item.seller, favKind: 'market', isFavorite: item.isFavorite, reportTitle: item.title, returnTo, deleteAction: isOwner ? contentDeleteAction('market', item.id) : undefined }) + ), div({ class: "tribe-card-body" }, div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, a({ href: `/market/${encodeURIComponent(item.id)}` }, item.title)) ), + renderStarRating(item.opinions, Array.isArray(item.opinions_inhabitants) ? item.opinions_inhabitants.length : 0), div({ class: "card-chips-row" }, + String(item.visibility || "PUBLIC").toUpperCase() === "HIDDEN" ? renderVisibilityChip("HIDDEN", i18n) : null, renderStateChip("encrypted", "", String(item.item_type || "").toUpperCase()), item.item_status ? renderStateChip("whole", "", String(item.item_status).toUpperCase()) : null, - String(item.visibility || "PUBLIC").toUpperCase() === "HIDDEN" ? renderVisibilityChip("HIDDEN", i18n) : null, item.includesShipping ? renderStateChip("mutuals", "📦", String(i18n.marketItemIncludesShipping || "Shipping").replace(/\?$/, "").toUpperCase()) : null, - renderLifespanChip(item.lifetime, i18n) + item.industry ? a({ href: `/industry/${encodeURIComponent(item.industry)}` }, renderStateChip("whole", "🏭", String(i18n.industryTitle || "Industry").toUpperCase())) : null, + renderLifespanChip(item.lifetime, i18n), + item.clearnet === true ? renderReachChip(true, i18n, clearnetItemHref("market", item.title, item.id)) : null ), - div({ class: "market-card-price card-date-highlight" }, `${item.price} ECO`), - div({ class: "card-visit-btn-centered" }, - form({ method: 'GET', action: `/market/${encodeURIComponent(item.id)}` }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - input({ type: "hidden", name: "filter", value: filter || "all" }), - input({ type: "hidden", name: "q", value: q }), - input({ type: "hidden", name: "minPrice", value: String(minPrice ?? "") }), - input({ type: "hidden", name: "maxPrice", value: String(maxPrice ?? "") }), - input({ type: "hidden", name: "sort", value: sort }), - button({ type: 'submit', class: 'filter-btn' }, i18n.viewItem || i18n.marketVisitItem || 'View Item') - ) - ) + div({ class: "price-chip" }, `${item.price} ECO`) ) ) }) - : p(i18n.marketNoItemsMatch || i18n.marketNoItems) + : p(q || String(minPrice ?? "") || String(maxPrice ?? "") ? (i18n.marketNoItemsMatch || i18n.marketNoItems) : (i18n.marketNoItems || i18n.marketNoItemsMatch)) ) ) ) } exports.singleMarketView = async (item, filter, comments = [], params = {}) => { + const marketChipVisible = (mode) => marketChipFor(filter, Array.isArray(params.censusList) ? params.censusList : null)(mode); const polls = Array.isArray(item.auctions_poll) ? item.auctions_poll : [] const parsedBids = polls.map(parseBidEntry).filter(Boolean).sort((a, b) => new Date(b.time) - new Date(a.time)) const q = params.q || "" @@ -580,44 +519,46 @@ exports.singleMarketView = async (item, filter, comments = [], params = {}) => { const maxPrice = params.maxPrice const sort = params.sort || "recent" const returnTo = params.returnTo || buildReturnTo(filter, q, minPrice, maxPrice, sort) - const topbar = renderMarketTopbar(item, returnTo) const stockLeft = Number(item.stock || 0) const showBuy = item.status === "FOR SALE" && item.item_type !== "auction" && String(item.seller) !== String(userId) && stockLeft > 0 const maxStock = item.initialStock || item.stockMax || item.stock || 1 return template( item.title, + section(div({ class: "tags-header module-header-line" }, h2(i18n.marketTitle), p(i18n.marketDescription), renderWalletChip())), section( div( { class: "filters" }, form( { method: "GET", action: "/market", class: "ui-toolbar ui-toolbar--filters" }, - input({ type: "hidden", name: "q", value: q }), input({ type: "hidden", name: "minPrice", value: minPrice ?? "" }), input({ type: "hidden", name: "maxPrice", value: maxPrice ?? "" }), input({ type: "hidden", name: "sort", value: sort }), - button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterAll), - button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterMine), - button({ type: "submit", name: "filter", value: "exchange", class: filter === "exchange" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterItems), - button({ type: "submit", name: "filter", value: "auctions", class: filter === "auctions" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterAuctions), - button({ type: "submit", name: "filter", value: "mybids", class: filter === "mybids" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterMyBids), - button({ type: "submit", name: "filter", value: "new", class: filter === "new" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterNew), - button({ type: "submit", name: "filter", value: "used", class: filter === "used" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterUsed), - button({ type: "submit", name: "filter", value: "broken", class: filter === "broken" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterBroken), - button({ type: "submit", name: "filter", value: "for sale", class: filter === "for sale" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterForSale), - button({ type: "submit", name: "filter", value: "sold", class: filter === "sold" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterSold), - button({ type: "submit", name: "filter", value: "discarded", class: filter === "discarded" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterDiscarded), - button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, i18n.marketFilterRecent), + button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterAll).toUpperCase()), + ...(marketChipVisible("recent") ? [button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterRecent).toUpperCase())] : []), + ...(marketChipVisible("mine") ? [button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterMine).toUpperCase())] : []), + ...(marketChipVisible("exchange") ? [button({ type: "submit", name: "filter", value: "exchange", class: filter === "exchange" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterItems).toUpperCase())] : []), + ...(marketChipVisible("auctions") ? [button({ type: "submit", name: "filter", value: "auctions", class: filter === "auctions" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterAuctions).toUpperCase())] : []), + ...(marketChipVisible("mybids") ? [button({ type: "submit", name: "filter", value: "mybids", class: filter === "mybids" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterMyBids).toUpperCase())] : []), + ...(marketChipVisible("new") ? [button({ type: "submit", name: "filter", value: "new", class: filter === "new" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterNew).toUpperCase())] : []), + ...(marketChipVisible("used") ? [button({ type: "submit", name: "filter", value: "used", class: filter === "used" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterUsed).toUpperCase())] : []), + ...(marketChipVisible("broken") ? [button({ type: "submit", name: "filter", value: "broken", class: filter === "broken" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterBroken).toUpperCase())] : []), + ...(marketChipVisible("for sale") ? [button({ type: "submit", name: "filter", value: "for sale", class: filter === "for sale" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterForSale).toUpperCase())] : []), + ...(marketChipVisible("sold") ? [button({ type: "submit", name: "filter", value: "sold", class: filter === "sold" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterSold).toUpperCase())] : []), + ...(marketChipVisible("discarded") ? [button({ type: "submit", name: "filter", value: "discarded", class: filter === "discarded" ? "filter-btn active" : "filter-btn" }, String(i18n.marketFilterDiscarded).toUpperCase())] : []), button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.marketCreateButton) ) ), (() => { const isHidden = String(item.visibility || 'PUBLIC').toUpperCase() === 'HIDDEN' + const isClearnet = !!item.clearnet const chips = [ + renderReachChip(isClearnet, i18n, isClearnet ? clearnetItemHref("market", item.title, item.id) : null), + isHidden ? renderVisibilityChip("HIDDEN", i18n) : null, renderStateChip("encrypted", "", String(item.item_type || "").toUpperCase()), item.item_status ? renderStateChip("whole", "", String(item.item_status).toUpperCase()) : null, item.includesShipping ? renderStateChip("mutuals", "📦", String(i18n.marketItemIncludesShipping || "Shipping").replace(/\?$/, "").toUpperCase()) : null, - isHidden ? renderVisibilityChip("HIDDEN", i18n) : null, + item.industry ? a({ href: `/industry/${encodeURIComponent(item.industry)}` }, renderStateChip("whole", "🏭", String(i18n.industryTitle || "Industry").toUpperCase())) : null, renderLifespanChip(item.lifetime, i18n), renderEcoTax(item.msgSize, item.id) ].filter(Boolean) @@ -634,26 +575,13 @@ exports.singleMarketView = async (item, filter, comments = [], params = {}) => { )) pushRow(i18n.marketItemSeller, userLink(item.seller)) if (item.shopId && item.shopTitle) pushRow(i18n.marketShopLabel || "Shop", a({ href: `/shops/${encodeURIComponent(item.shopId)}`, class: "user-link" }, item.shopTitle)) - if (item.deadline) pushRow(i18n.marketItemAvailable, moment(item.deadline).format("YYYY/MM/DD HH:mm:ss")) - - const itemSide = div({ class: "tribe-side" }, - div({ class: "shop-title-row" }, - h2({ class: "tribe-card-title" }, item.title) - ), - chips.length ? div({ class: "card-chips-row" }, ...chips) : null, - div({ class: "card-spread-centered" }, renderSpreadButton(item.id, params.spreads)), - renderMediaBlob(item.image, "/assets/images/default-market.png"), - div({ class: "card-date-highlight" }, `${item.price} ECO`), - renderStockBar(item.stock, maxStock), - table({ class: "tribe-info-table jobs-info-table" }, ...infoRows), - tagsNode - ) + if (item.deadline) pushRow(i18n.marketItemAvailable, moment(item.deadline).format("YYYY/MM/DD HH:mm")) const visibilityRow = String(item.seller) === String(userId) ? (() => { const vis = isHidden ? 'HIDDEN' : 'PUBLIC' const next = vis === 'PUBLIC' ? 'HIDDEN' : 'PUBLIC' - return div({ class: "tribe-side-actions" }, + return div({ class: "tribe-side-actions housing-status-row" }, span({ class: "card-label" }, `${i18n.visibilityLabel || 'Visibility'}: `), renderVisibilityChip(vis, i18n), form({ method: "POST", action: `/market/visibility/${encodeURIComponent(item.id)}`, class: "inline-form" }, @@ -666,28 +594,32 @@ exports.singleMarketView = async (item, filter, comments = [], params = {}) => { })() : null - const marketActions = [] - if (item.seller && String(item.seller) !== String(userId)) { - marketActions.push(form({ method: "GET", action: "/pm" }, - input({ type: "hidden", name: "recipients", value: item.seller }), - button({ type: "submit", class: "filter-btn" }, i18n.privateMessage) - )) - } - if (String(item.seller) === String(userId)) { - marketActions.push(form({ method: "GET", action: `/market/edit/${encodeURIComponent(item.id)}` }, - button({ type: "submit", class: "update-btn" }, i18n.marketUpdateButton || "Update") - )) - marketActions.push(form({ method: "POST", action: `/market/delete/${encodeURIComponent(item.id)}` }, - button({ type: "submit", class: "delete-btn" }, i18n.marketDeleteButton || "Delete") - )) - } - const itemMain = div({ class: "tribe-main" }, - marketActions.length ? div({ class: "tribe-side-actions" }, ...marketActions) : null, + const marketActions = String(item.seller) === String(userId) ? renderMarketOwnerActions(item, `/market/${encodeURIComponent(item.id)}`) : [] + + const itemSide = div({ class: "tribe-side" }, + div({ class: "card-header activity-card-header" }, + renderContentActions(item.id, `/market/${encodeURIComponent(item.id)}`, { spread: params.spreads || null, author: item.seller, favKind: 'market', isFavorite: item.isFavorite, reportTitle: item.title, returnTo: `/market/${encodeURIComponent(item.id)}`, deleteAction: String(item.seller) === String(userId) ? contentDeleteAction('market', item.id) : undefined }) + ), + div({ class: "shop-title-row" }, + h2({ class: "tribe-card-title" }, item.title) + ), + renderStarRating(item.opinions, Array.isArray(item.opinions_inhabitants) ? item.opinions_inhabitants.length : 0), + chips.length ? div({ class: "card-chips-row" }, ...chips) : null, + String(item.seller) === String(userId) ? renderClearnetSwitch("market", item.rootId || item.id, isClearnet) : null, + renderMediaBlob(item.image, "/assets/images/default-market.png"), + div({ class: "price-chip" }, `${item.price} ECO`), + renderStockBar(item.stock, maxStock), + table({ class: "tribe-info-table jobs-info-table" }, ...infoRows), + tagsNode, visibilityRow, + marketActions.length ? div({ class: "tribe-side-actions" }, ...marketActions) : null + ) + + const itemMain = div({ class: "tribe-main" }, item.description ? div({ class: "job-section" }, h2({ class: "job-section-title" }, i18n.marketItemDescription), - p({ class: "tribe-side-description" }, ...renderUrl(item.description)) + p({ class: "tribe-side-description" }, ...renderStyledText(item.description)) ) : null, renderCountdownField(item), @@ -701,13 +633,13 @@ exports.singleMarketView = async (item, filter, comments = [], params = {}) => { { class: "auction-bid-table" }, tr(th(i18n.marketAuctionBidTime), th(i18n.marketAuctionUser), th(i18n.marketAuctionBidAmount)), parsedBids.map((bid) => - tr(td(moment(bid.time).format("YYYY-MM-DD HH:mm:ss")), td(a({ href: `/author/${encodeURIComponent(bid.bidder)}` }, bid.bidder)), td(`${parseFloat(bid.amount).toFixed(6)} ECO`)) + tr(td(moment(bid.time).format("YYYY/MM/DD HH:mm")), td(a({ href: `/author/${encodeURIComponent(bid.bidder)}` }, bid.bidder)), td(`${parseFloat(bid.amount).toFixed(6)} ECO`)) ) ) : p({ class: "tribe-side-description" }, i18n.marketNoBids || "No bids yet"), item.status !== "SOLD" && item.status !== "DISCARDED" && String(item.seller) !== String(userId) ? form( - { method: "POST", action: `/market/bid/${encodeURIComponent(item.id)}` }, + { method: "POST", action: `/market/bid/${encodeURIComponent(item.id)}`, class: "market-bid-form" }, input({ type: "hidden", name: "returnTo", value: returnTo }), input({ type: "number", name: "bidAmount", step: "0.000001", min: "0.000001", placeholder: i18n.marketYourBid, required: true }), br(), @@ -720,13 +652,18 @@ exports.singleMarketView = async (item, filter, comments = [], params = {}) => { ? div( { class: "market-item actions" }, form( - { method: "POST", action: `/market/buy/${encodeURIComponent(item.id)}` }, + { method: "POST", action: `/market/buy/${encodeURIComponent(item.id)}`, class: "market-buy-form" }, input({ type: "hidden", name: "returnTo", value: "/inbox?filter=sent" }), button({ class: "buy-btn", type: "submit" }, i18n.marketActionsBuy) ) ) : null, - renderMarketCommentsSection(item.id, returnTo, comments) + renderEngagement(item.id, + String(item.seller) !== String(userId) && item.purchasedByViewer + ? renderOpinionsVoting("/market/opinions", item.id, item.opinions, returnTo, item.opinions_inhabitants) + : null, + renderMarketCommentsSection(item.id, returnTo, comments) + ) ) return div({ class: "tribe-details" }, itemSide, itemMain) diff --git a/src/views/melody_view.js b/src/views/melody_view.js index 8129272..fc82391 100644 --- a/src/views/melody_view.js +++ b/src/views/melody_view.js @@ -2,7 +2,7 @@ const { div, h2, p, section, audio, source, span, a, form, input, button, br, textarea, label } = require("../server/node_modules/hyperaxe"); const moment = require("../server/node_modules/moment"); -const { template, i18n, userLink, renderEcoTax } = require("./main_views"); +const { template, i18n, userLink, renderEcoTax, renderContentActions, paged } = require("./main_views"); const renderSequenceList = (sequence, availableIds = null) => { if (!sequence || sequence.length === 0) { @@ -26,7 +26,7 @@ const renderSequenceList = (sequence, availableIds = null) => { span({ class: "melody-note-name" }, noteName), span({ class: "melody-note-type" }, noteType) ]; - if (reachable) { + if (reachable && process.env.OASIS_MOBILE !== '1') { return a({ href: `/blockexplorer/block/${encodeURIComponent(blockId)}`, class: "melody-note-chip melody-note-chip-link", title: titleAttr }, ...inner); } const cls = checkAvailability && blockId && !reachable @@ -97,27 +97,31 @@ const renderBcsList = (bcsAudios) => { return p({ class: "empty" }, i18n.melodyAllEmpty || "No BCS compositions from other inhabitants yet."); } return div({ class: "melody-bcs-list" }, - bcsAudios.map(a => div({ class: "melody-bcs-card" }, + bcsAudios.map(item => div({ class: "melody-bcs-card" }, + div({ class: "card-header activity-card-header" }, + span(), + renderContentActions(item.key, `/audios/${encodeURIComponent(item.key)}`, { author: item.author, favKind: 'audios', isFavorite: item.isFavorite, torrentFrom: { blobId: item.url, name: item.title }, reportTitle: item.title }) + ), div({ class: "melody-bcs-head" }, - a.title ? h2({ class: "melody-bcs-title" }, a.title) : null, + item.title ? h2({ class: "melody-bcs-title" }, item.title) : null, div({ class: "melody-bcs-meta" }, - span({ class: "card-label" }, `${i18n.melodyByLabel || "By"}: `), - userLink(a.author), - span({ class: "melody-meta-sep" }, " · "), - span({ class: "card-value" }, moment(a.createdAt).format("YYYY/MM/DD HH:mm:ss")) + userLink(item.author), + span({ class: "melody-bcs-date" }, moment(item.createdAt).format("YYYY/MM/DD HH:mm")) ) ), - a.url + item.url ? div({ class: "audio-container melody-bcs-player" }, - audio({ controls: true, preload: "metadata", src: `/blob/${encodeURIComponent(a.url)}` }) + audio({ controls: true, preload: "metadata", src: `/blob/${encodeURIComponent(item.url)}` }), + form({ method: "GET", action: `/melody/transcode/${encodeURIComponent(item.key)}`, class: "audio-transcode-form" }, + button({ type: "submit", class: "filter-btn" }, i18n.audioTranscodeButton || "TRANSCODE") + ) ) - : null, - a.description ? p({ class: "melody-bcs-desc" }, a.description) : null, - div({ class: "melody-bcs-actions" }, - form({ method: "GET", action: `/melody/transcode/${encodeURIComponent(a.key)}` }, - button({ type: "submit", class: "filter-btn" }, i18n.audioTranscodeButton || "TRANSCODE") - ) - ) + : div({ class: "melody-bcs-actions" }, + form({ method: "GET", action: `/melody/transcode/${encodeURIComponent(item.key)}`, class: "audio-transcode-form" }, + button({ type: "submit", class: "filter-btn" }, i18n.audioTranscodeButton || "TRANSCODE") + ) + ), + item.description ? p({ class: "melody-bcs-desc" }, item.description) : null )) ); }; @@ -127,13 +131,13 @@ exports.melodyView = ({ feedId, total, sequence, filter, bcsAudios }) => { const description = i18n.melodyDescription || "Play the melody of your blockchain — each block becomes a note."; const activeFilter = filter === "all" ? "all" : "mine"; const body = activeFilter === "all" - ? renderBcsList(bcsAudios) + ? renderBcsList(paged(bcsAudios || [])) : renderOwnPanel({ feedId, total, sequence }); return template( title, section( - div({ class: "tags-header" }, + div({ class: "tags-header module-header-line" }, h2(title), p(description) ), diff --git a/src/views/mentions_view.js b/src/views/mentions_view.js new file mode 100644 index 0000000..3ef4313 --- /dev/null +++ b/src/views/mentions_view.js @@ -0,0 +1,103 @@ +const { div, h2, p, section, button, form, input, span } = require("../server/node_modules/hyperaxe"); +const { template, i18n, userLink, renderContentActions, renderModuleStats, CONTENT_SPREADABLE, CONTENT_TORRENTABLE, paged } = require("./main_views"); +const { getViewDetailsAction } = require("./activity_view"); +const moment = require("../server/node_modules/moment"); +const { config } = require("../server/SSB_server.js"); +const { renderStyledText } = require("../backend/renderStyledText"); + +const userId = config.keys.id; + +const excerpt = (text, max = 320) => { + const s = String(text == null ? "" : text).trim(); + return s.length > max ? s.slice(0, max) + "…" : s; +}; + +const typeLabel = (type) => i18n['type' + String(type || '').charAt(0).toUpperCase() + String(type || '').slice(1)] || String(type || '').toUpperCase(); + +const renderMentionCard = (item, readKeys, params = {}) => { + const href = getViewDetailsAction(item.type, { id: item.id, key: item.id, author: item.author, content: item.content }); + const isOwn = String(item.author) === String(userId); + const read = readKeys.has(String(item.key)); + return div({ class: "trending-card mention-card" + (isOwn ? " own-content" : "") }, + div({ class: "card-header activity-card-header" }, + div({ class: 'card-chips-row' }, + span({ class: 'pm-exposition-chip pm-exposition-whole' }, + span({ class: 'pm-exposition-text' }, typeLabel(item.type)) + ), + read ? null : span({ class: 'pm-exposition-chip pm-unread-chip' }, span({ class: 'pm-exposition-text' }, i18n.inboxUnreadChip)) + ), + renderContentActions(item.id, href, { + author: item.author, + reportTitle: item.title || item.text, + spread: CONTENT_SPREADABLE.has(item.type) ? ((params.spreadMap && params.spreadMap.get(item.id)) || null) : undefined, + ...(item.type === 'torrent' || item.type === 'file' ? { spreadTitle: i18n.seedAction } : {}), + ...(CONTENT_TORRENTABLE.has(item.type) && item.content && typeof item.content.url === 'string' && item.content.url.startsWith('&') + ? { torrentFrom: { blobId: item.content.url, name: item.content.title || item.content.fileName || '' } } + : {}) + }) + ), + div({ class: "card-section mention-card-body" }, + item.title + ? div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, item.title)) + : null, + item.text ? p({ class: "mention-text" }, ...renderStyledText(excerpt(item.text))) : null, + p({ class: "card-footer" }, + span({ class: "date-link" }, `${moment(item.createdAt).format("YYYY/MM/DD HH:mm")}`), + userLink(item.author) + ), + div({ class: "pm-actions mention-actions" }, + form({ method: "POST", action: `/mentions/${read ? 'unread' : 'read'}/${encodeURIComponent(item.key)}`, class: "pm-action-form" }, + button({ type: "submit", class: "pm-btn read-btn" }, String(read ? i18n.inboxMarkUnread : i18n.inboxMarkRead).toUpperCase()) + ) + ) + ) + ); +}; + +exports.mentionsView = async (items = [], filter = 'ALL', params = {}) => { + const counts = params.counts || {}; + const types = Object.keys(counts).sort((a, b) => (counts[b] - counts[a]) || a.localeCompare(b)); + const q = params.q || ''; + const readKeys = new Set(Array.isArray(params.readKeys) ? params.readKeys.map(String) : []); + const unreadShown = items.filter(x => x && x.key && !readKeys.has(String(x.key))).map(x => String(x.key)); + const emptyMentions = (!Array.isArray(items) || items.length === 0) && String(filter || 'ALL').toUpperCase() === 'ALL' && !q.trim(); + + return template( + i18n.mentions, + section( + div({ class: "tags-header module-header-line" }, + h2(i18n.mentions), + p(i18n.mentionsDescription) + ), + types.length + ? div({ class: "filters" }, + form({ method: "GET", action: "/mentions", class: "ui-toolbar ui-toolbar--filters" }, + ...types.map(t => + button({ type: "submit", name: "filter", value: t, class: filter === t ? "filter-btn active" : "filter-btn" }, + `${typeLabel(t)} (${counts[t]})`) + ) + ) + ) + : null, + emptyMentions ? null : div({ class: "filters activity-filter-chips activity-toolbar-row" }, + renderModuleStats(items.length), + unreadShown.length ? form({ method: "POST", action: "/mentions/read-all", class: "inbox-vis-toggle" }, + ...unreadShown.map(k => input({ type: "hidden", name: "keys", value: k })), + button({ type: "submit", class: "btn" }, `${i18n.inboxMarkAllRead} (${unreadShown.length})`) + ) : null, + form({ method: "GET", action: "/mentions", class: "filter-box" }, + input({ type: "hidden", name: "filter", value: filter }), + input({ type: "text", name: "q", value: q, placeholder: i18n.mentionsSearchPlaceholder, class: "filter-box__input" }), + div({ class: "filter-box__controls" }, + button({ type: "submit", class: "filter-box__button" }, i18n.searchButton) + ) + ) + ) + ), + section( + items.length + ? div({ class: "mentions-list" }, ...paged(items).map(item => renderMentionCard(item, readKeys, params))) + : div({ class: "no-content-box" }, p({ class: "empty" }, i18n.noMentions)) + ) + ); +}; diff --git a/src/views/mobile_icons.js b/src/views/mobile_icons.js new file mode 100644 index 0000000..ce067ac --- /dev/null +++ b/src/views/mobile_icons.js @@ -0,0 +1,106 @@ +"use strict"; +// Iconos del lanzador y de la barra de abajo (rama movil). +// +// Antes eran letras del alfabeto vai (ꔙ ꕿ ꖒ...): no decian nada de lo que abren, +// dependian de que el movil trajera la fuente y Karvan y Nodos llevaban casi el +// mismo reloj de arena. Estos son SVG de trazo en rejilla de 24, el mismo estilo +// que los ACTION_ICONS de las tarjetas (main_views.js), y se pintan con +// currentColor, asi que cada tema les pone su color. + +const RAW_HTML = Symbol.for('oasis.rawHtml'); +const { span } = require("../server/node_modules/hyperaxe"); + +const ICONS = { + activity: '', + feed: '', + chats: '', + rooms: '', + forum: '', + pads: '', + wiki: '', + blogs: '', + opinions: '', + mentions: '', + tags: '', + trending: '', + maps: '', + graphos: '', + inhabitants: '', + fediverse: '', + images: '', + videos: '', + audios: '', + podcasts: '', + documents: '', + files: '', + bookmarks: '', + torrents: '', + games: '', + pixelia: '', + melody: '', + events: '', + calendars: '', + tasks: '', + campaigns: '', + mailing: '', + votes: '', + polls: '', + reports: '', + tribes: '', + emergencies: '', + school: '', + larp: '', + parliament: '', + courts: '', + market: '', + shops: '', + jobs: '', + projects: '', + industry: '', + housing: '', + logistics: '', + banking: '', + transfers: '', + profile: '', + agenda: '', + favorites: '', + phone: '', + cv: '', + wallet: '', + logs: '', + invites: '', + peers: '', + bar: '', + backup: '', + cipher: '', + ai: '', + stats: '', + blockexplorer: '', + modules: '', + settings: '', + search: '', + inbox: '', + publish: '', + data: '', + mic: '', + camera: '', + hangup: '' +}; + +// la clave sale de la ruta: /settings/bottombar es "bar" +const iconKeyOf = (href) => { + const h = String(href || '').replace(/^\//, '').split(/[?#]/)[0]; + if (h === 'settings/bottombar') return 'bar'; + return h.split('/')[0]; +}; + +const svg = (key) => ICONS[key] + ? `` + : ''; + +// span con el SVG dentro; si no hay icono para esa clave, el caracter de antes +const iconNode = (cls, key, fallback) => ICONS[key] + ? span({ class: cls + ' has-svg', 'aria-hidden': 'true', [RAW_HTML]: svg(key) }) + : span({ class: cls, 'aria-hidden': 'true' }, fallback || ''); + +module.exports = { ICONS, iconKeyOf, svg, iconNode }; diff --git a/src/views/mobile_modules_view.js b/src/views/mobile_modules_view.js new file mode 100644 index 0000000..ed51f39 --- /dev/null +++ b/src/views/mobile_modules_view.js @@ -0,0 +1,103 @@ +"use strict"; +// Pantalla de Modulos del movil (la sirve el router propio, fork_routes.js). +// +// Los modulos agrupados como en el lanzador, cada uno una fila que se toca entera con +// un interruptor, y Guardar siempre a la vista. La tabla de upstream eran 52 filas +// con casillas de 16px, unas 11 pantallas. Va en un fichero propio para no tocar +// modules_view.js, que es del dev (docs/FRONTERA_UPSTREAM.md). +// +// El formulario envia a la misma ruta de upstream (/save-modules) con los mismos +// nombres de campo (Form), asi que guardar sigue siendo cosa suya. + +const { button, div, form, h2, h3, input, label, p, section, span } = require("../server/node_modules/hyperaxe"); +const { getConfig } = require('../configs/config-manager.js'); +const { iconNode } = require('./mobile_icons'); + +// clave de i18n de cada modulo (modulesLabel / modulesDescription) +const STEMS = { + agenda: 'Agenda', ai: 'AI', aiNav: 'AINav', emergencies: 'Emergencies', mailing: 'Mailing', rooms: 'Rooms', + phone: 'Phone', logistics: 'Logistics', podcasts: 'Podcasts', campaigns: 'Campaigns', audios: 'Audios', banking: 'Banking', + bookmarks: 'Bookmarks', calendars: 'Calendars', chats: 'Chats', cipher: 'Cipher', courts: 'Courts', dev: 'Dev', + docs: 'Docs', events: 'Events', favorites: 'Favorites', blogs: 'Blogs', fediverse: 'Fediverse', feed: 'Feed', + files: 'Files', forum: 'Forum', games: 'Games', graphos: 'Graphos', housing: 'Housing', images: 'Images', + industry: 'Industry', invites: 'Invites', jobs: 'Jobs', larp: 'Larp', backup: 'Backup', + logs: 'Logs', maps: 'Map', market: 'Market', melody: 'Melody', opinions: 'Opinions', pads: 'Pads', + parliament: 'Parliament', pixelia: 'Pixelia', polls: 'Polls', projects: 'Projects', reports: 'Reports', school: 'School', + shops: 'Shops', tags: 'Tags', tasks: 'Tasks', torrents: 'Torrents', transfers: 'Transfers', trending: 'Trending', + tribes: 'Tribes', videos: 'Videos', votes: 'Votations', wallet: 'Wallet', wiki: 'Wiki' +}; +// los que no van en el movil (los apaga tambien /save-modules) +const MOBILE_UNAVAILABLE = ['ai', 'aiNav', 'dev', 'games', 'logs', 'maps', 'pixelia']; + +const moduleLabel = (i18n, name) => i18n[`modules${STEMS[name]}Label`] || name; + +const mobileModulesView = (opts = {}) => { + const { template, i18n, LAUNCHER_GROUPS } = require('./main_views'); + const config = getConfig().modules || {}; + const visible = Object.keys(STEMS) + .filter(name => !MOBILE_UNAVAILABLE.includes(name)) + .map(name => ({ name, label: moduleLabel(i18n, name), description: i18n[`modules${STEMS[name]}Description`] || '' })) + .sort((x, y) => String(x.label).localeCompare(String(y.label), undefined, { sensitivity: 'base' })); + const isOn = (m) => config[`${m.name}Mod`] === 'on'; + + // grupos del lanzador; Industria va con la economia y lo demas, al final + const groupOf = {}; + (LAUNCHER_GROUPS || []).forEach(g => g.mods.forEach(m => { if (m.mod) groupOf[m.mod.replace(/Mod$/, '')] = g; })); + const economy = (LAUNCHER_GROUPS || []).find(g => g.id === 'economy'); + if (economy && !groupOf.industry) groupOf.industry = economy; + const groups = []; + const byId = {}; + visible.forEach(mod => { + const g = groupOf[mod.name] || { id: 'other', titleKey: 'menuTools' }; + if (!byId[g.id]) { byId[g.id] = { g, mods: [] }; groups.push(byId[g.id]); } + byId[g.id].mods.push(mod); + }); + const order = (LAUNCHER_GROUPS || []).map(g => g.id); + const rank = (id) => { const i = order.indexOf(id); return i === -1 ? order.length : i; }; + groups.sort((x, y) => rank(x.g.id) - rank(y.g.id)); + + const blocked = opts.blocked && visible.find(m => m.name === opts.blocked.mod); + const blockedBox = blocked ? div({ class: 'module-blocked' }, + iconNode('module-blocked-ico', blocked.name), + p({ class: 'module-blocked-text' }, String(i18n.moduleBlockedText || '{name} is turned off.').replace('{name}', blocked.label)), + form({ method: 'POST', action: '/modules/enable' }, + input({ type: 'hidden', name: 'mod', value: blocked.name }), + input({ type: 'hidden', name: 'returnTo', value: opts.blocked.path || `/${blocked.name}` }), + button({ type: 'submit', class: 'create-button' }, String(i18n.moduleBlockedAction || 'Turn on {name}').replace('{name}', blocked.label)) + ) + ) : null; + + return template( + i18n.modules, + section(div({ class: 'tags-header module-header-line' }, + h2(i18n.modulesViewTitle), + p(String(i18n.modulesOnCount || '{on} of {total} sections on') + .replace('{on}', visible.filter(isOn).length).replace('{total}', visible.length)) + )), + blockedBox, + opts.saved ? p({ class: 'module-saved', role: 'status' }, i18n.modulesSaved || 'Saved.') : null, + section( + form({ action: '/save-modules', method: 'post', class: 'module-switches' }, + ...groups.map(({ g, mods }) => div({ class: 'module-group' }, + h3({ class: 'module-group-title' }, i18n[g.titleKey] || g.id), + ...mods.map(mod => label({ class: 'module-switch', for: `${mod.name}Mod` }, + iconNode('module-switch-ico', mod.name), + span({ class: 'module-switch-text' }, + span({ class: 'module-switch-name' }, mod.label), + span({ class: 'module-switch-desc' }, mod.description) + ), + input({ + type: 'checkbox', role: 'switch', id: `${mod.name}Mod`, name: `${mod.name}Form`, value: 'on', class: 'switch-input', + checked: isOn(mod) ? true : undefined + }) + )) + )), + div({ class: 'module-save-bar' }, + button({ type: 'submit', class: 'create-button' }, i18n.saveSettings) + ) + ) + ) + ); +}; + +module.exports = { mobileModulesView, MOBILE_UNAVAILABLE, STEMS }; diff --git a/src/views/mobile_ui.js b/src/views/mobile_ui.js new file mode 100644 index 0000000..c3448e3 --- /dev/null +++ b/src/views/mobile_ui.js @@ -0,0 +1,235 @@ +"use strict"; +// Piezas de interfaz de la rama movil que se enchufan en vistas de upstream. +// +// Las vistas (vote_view, tribes_view, settings_view, main_views, welcome_view) son del +// dev y se reescriben a menudo (tribes_view en 23 de 39 releases, main_views en 31). +// Aqui vive la logica; en cada vista queda una llamada con la bandera de movil, que es +// lo unico que hay que volver a poner si el dev la reescribe. +// Ver docs/FRONTERA_UPSTREAM.md. +// +// i18n se lee al llamar (main_views lo cambia en cada peticion y este fichero se carga +// antes que el). + +const { a, button, details, div, form, img, input, label, nav, p, progress, span, summary } = require("../server/node_modules/hyperaxe"); +const { iconNode } = require("./mobile_icons"); + +const isMobile = () => process.env.OASIS_MOBILE === '1'; +const t = () => require('./main_views').i18n; + +// ---------- votaciones ---------- + +// tipos de votacion: el servidor ya guarda las opciones que se le pasen (campo +// options de /votes/create); aqui solo se ofrecen las que usan las asambleas +const VOTE_KINDS = [ + { key: 'classic', options: null }, + { key: 'traffic', options: ['GREEN', 'YELLOW', 'RED'] }, + { key: 'scale', options: ['1', '2', '3', '4', '5'] } +]; + +const voteKindPicker = (current) => { + const i18n = t(); + const cur = String(current || ''); + return div({ class: 'vote-kind' }, + label({ class: 'vote-kind-title' }, i18n.voteKindLabel || 'Type of vote'), + ...VOTE_KINDS.map((k, i) => { + const value = k.options ? k.options.join(',') : ''; + const checked = cur ? cur === value : i === 0; + return label({ class: 'vote-kind-option' }, + input(Object.assign({ type: 'radio', name: 'options', value }, checked ? { checked: 'checked' } : {})), + span({ class: 'vote-kind-name' }, i18n['voteKind_' + k.key] || k.key), + span({ class: 'vote-kind-hint' }, i18n['voteKindHint_' + k.key] || '') + ); + }) + ); +}; + +// resultados como barras, una por opcion: la tabla de 6 columnas no cabia y partia +// las palabras por silabas +const voteBars = (voteOptions, baseCounts, voteLabel) => { + const total = voteOptions.reduce((s, o) => s + (Number(baseCounts[o]) || 0), 0); + return div({ class: 'vote-bars' }, + ...voteOptions.map((opt) => { + const n = Number(baseCounts[opt]) || 0; + return div({ class: `vote-bar vote-bar-${String(opt).toLowerCase()}` }, + span({ class: 'vote-bar-label' }, voteLabel(opt)), + span({ class: 'vote-bar-count' }, String(n)), + progress({ class: 'vote-bar-meter', max: String(Math.max(total, 1)), value: String(n) }) + ); + }) + ); +}; + +// quien crea una votacion no vota (regla de votes_model.js, no se toca): se explica +const voteOwnNote = () => p({ class: 'vote-own-note' }, t().voteOwnNote || 'You created this vote, so you cannot vote in it.'); + +// ---------- tribus ---------- + +// una fila por tribu: con 12 comisiones la lista de tarjetas con la imagen generica +// eran 10 pantallas. La imagen solo sale si la tribu tiene una propia +const tribeRow = (tr, { own, imageNode }) => { + const i18n = t(); + const href = `/tribe/${encodeURIComponent(tr.id)}`; + const members = tr.memberCount != null ? tr.memberCount : (tr.members || []).length; + const facts = [ + tr.isAnonymous ? i18n.tribePrivate : i18n.tribePublic, + tr.inviteMode === 'open' ? i18n.tribeOpen : i18n.tribeStrict, + `${members} ${String(i18n.tribeMembersCount || '').toLowerCase()}` + ].filter(Boolean).join(' · '); + return a({ href, class: 'tribe-row' + (own ? ' own-content' : '') }, + tr.image && imageNode ? imageNode : iconNode('tribe-row-thumb tribe-row-icon', 'tribes'), + span({ class: 'tribe-row-text' }, + span({ class: 'tribe-row-title' }, tr.title), + span({ class: 'tribe-row-facts' }, facts), + tr.description ? span({ class: 'tribe-row-desc' }, String(tr.description).slice(0, 90)) : null + ), + span({ class: 'tribe-row-go', 'aria-hidden': 'true' }, '›') + ); +}; + +// secciones de una tribu: 25 botones a todo lo ancho eran varias pantallas antes del +// contenido. Tira de fichas con lo que mas usa una comision y el resto en "Mas" +const TRIBE_MAIN_TABS = ['activity', 'inhabitants', 'governance', 'votations', 'forum', 'chats', 'events', 'pads']; +const tribeTabs = (tribe, section, sections) => { + const i18n = t(); + const all = sections.flatMap(g => g.items); + const tab = (s) => a({ href: `/tribe/${encodeURIComponent(tribe.id)}?section=${encodeURIComponent(s.key)}`, class: section === s.key ? 'tribe-tab is-current' : 'tribe-tab', ...(section === s.key ? { 'aria-current': 'page' } : {}) }, s.label); + const main = all.filter(s => TRIBE_MAIN_TABS.includes(s.key)); + const rest = all.filter(s => !TRIBE_MAIN_TABS.includes(s.key)); + const currentRest = rest.find(s => s.key === section); + return nav({ class: 'tribe-tabs', 'aria-label': i18n.tribeSectionsLabel || 'Sections' }, + div({ class: 'tribe-tabs-strip' }, ...(currentRest ? [tab(currentRest)] : []), ...main.map(tab)), + details({ class: 'tribe-tabs-more' }, + summary(i18n.tribeSectionsMore || 'More'), + div({ class: 'tribe-tabs-grid' }, ...rest.filter(s => s !== currentRest).map(tab)) + ) + ); +}; + +// ---------- ajustes ---------- + +// primero lo que se toca (idioma, aspecto, tipo de uso, portada) y despues lo tecnico; +// se reordena por id de seccion, sin tocar las secciones +const SETTINGS_FIRST = ['update', 'language', 'ux', 'theme', 'workflows', 'home-page']; +const settingsOrder = (parts) => { + const list = parts.filter(Boolean); + const rank = (el) => { const i = SETTINGS_FIRST.indexOf(el && el.id); return i === -1 ? SETTINGS_FIRST.length : i; }; + return list.map((el, i) => ({ el, i })).sort((x, y) => rank(x.el) - rank(y.el) || x.i - y.i).map(x => x.el); +}; + +// cada idioma un boton que se aplica al tocarlo (antes desplegable y otro boton) +const LANGS = [['Español', 'es'], ['Català', 'ca'], ['Euskara', 'eu'], ['Galego', 'gl'], ['English', 'en'], ['Français', 'fr'], + ['Português', 'pt'], ['Italiano', 'it'], ['Deutsch', 'de'], ['Русский', 'ru'], ['العربية', 'ar'], ['हिन्दी', 'hi'], ['中文', 'zh']]; +// el idioma se lee al pintar: settings_view desestructura selectedLanguage al cargar y +// se queda con el valor de entonces (siempre "en") +const languagePick = () => { + const selected = require('./main_views').getLanguage(); + return form({ action: '/language', method: 'post', class: 'lang-pick' }, + ...LANGS.map(([name, code]) => button({ type: 'submit', name: 'language', value: code, class: code === selected ? 'lang-chip is-current' : 'lang-chip', lang: code }, name)) + ); +}; + +// tipo de portada (ux): cada tarjeta es un boton que guarda al tocarlo. Antes eran +// radios y un boton de guardar debajo de seis tarjetas grandes, fuera de pantalla. +// Mismos modos y mismas condiciones que settings_view y welcome_view; las rutas +// (/settings/ux, /welcome/ux) son las del dev +const uxPick = (action, cfg, withHint) => { + const i18n = t(); + const mods = (cfg && cfg.modules) || {}; + const ux = cfg && cfg.ux && cfg.ux.current; + const modes = [ + ['blocks', i18n.uxModeMenus || 'Blocks', true], + ['ainav', i18n.uxModeAINav || 'AI', mods.aiNavMod === 'on'], + ['chats', i18n.uxModeChats || 'Conversations', mods.chatsMod === 'on'], + ['feed', i18n.uxModeFeed || 'Microblogging', true], + ['phone', i18n.phoneTitle || 'Phone', mods.phoneMod !== 'off'], + ['rooms', i18n.roomsTitle || 'Rooms', mods.roomsMod !== 'off'] + ].filter(m => m[2]); + const cur = modes.some(m => m[0] === ux) ? ux : 'blocks'; + return form({ method: 'POST', action, class: 'ux-pick' }, + withHint ? p({ class: 'ux-pick-hint' }, i18n.uxPickHint || 'Tap one and it is saved. You can change it any time in Settings.') : null, + div({ class: 'ux-pick-grid' }, + ...modes.map(([value, title]) => button(Object.assign({ type: 'submit', name: 'ux', value, class: value === cur ? 'ux-pick-card is-current' : 'ux-pick-card' }, value === cur ? { 'aria-current': 'true' } : {}), + img({ src: `/assets/images/ux-${value}.png`, class: 'ux-pick-shot', alt: '' }), + span({ class: 'ux-pick-label' }, title), + value === cur ? span({ class: 'ux-pick-now' }, i18n.uxPickCurrent || 'In use') : null + )) + ) + ); +}; + +// perfil de uso (workflow): igual, un boton por opcion con una linea de para que +// sirve. Activismo y Acampada primero; el resto en el orden del dev +const WORKFLOW_FIRST = ['activists', 'acampada']; +const workflowPick = (action, current, keys, withHint) => { + const i18n = t(); + const list = WORKFLOW_FIRST.filter(k => keys.includes(k)).concat(keys.filter(k => !WORKFLOW_FIRST.includes(k))); + const name = (k) => k === 'full' ? (i18n.welcomeWorkflow_full || k) : (i18n[`workflow_${k}`] || k); + return form({ method: 'POST', action, class: 'ux-pick wf-pick' }, + withHint ? p({ class: 'ux-pick-hint' }, i18n.uxPickHint || 'Tap one and it is saved. You can change it any time in Settings.') : null, + div({ class: 'ux-pick-grid' }, + ...list.map(k => button(Object.assign({ type: 'submit', name: 'workflow', value: k, class: k === current ? 'ux-pick-card is-current' : 'ux-pick-card' }, k === current ? { 'aria-current': 'true' } : {}), + span({ class: 'ux-pick-text' }, + span({ class: 'ux-pick-label' }, name(k)), + i18n[`wfHint_${k}`] ? span({ class: 'ux-pick-sub' }, i18n[`wfHint_${k}`]) : null), + k === current ? span({ class: 'ux-pick-now' }, i18n.uxPickCurrent || 'In use') : null + )) + ) + ); +}; + +// pasos de la bienvenida en el movil: el backend del dev quita el de uso en el movil; +// aqui vuelve, el segundo, justo despues de la identidad (/welcome/workflow es suyo) +const welcomeSteps = (usable) => { + if (usable.includes('workflow')) return usable; + const out = usable.slice(); + const at = out.indexOf('profile'); + out.splice(at === -1 ? 0 : at + 1, 0, 'workflow'); + return out; +}; + +// ---------- acciones de tarjeta ---------- + +// borrar pide un segundo toque: la X abre "¿Borrar? Si, borrar" y solo ese boton +// envia. Sin JS, con
. hidden: los que lleve el formulario +const deleteConfirm = (action, hidden) => { + const i18n = t(); + return details({ class: 'content-action-confirm' }, + summary({ class: 'btn-singleview btn-delete', title: i18n.chatDelete, 'aria-label': i18n.chatDelete }, require('./main_views').renderIcon('close')), + div({ class: 'confirm-bar' }, + span({ class: 'confirm-question' }, i18n.deleteConfirmQuestion || 'Delete it for good?'), + form({ method: 'POST', action, class: 'content-action-form' }, hidden || null, + button({ type: 'submit', class: 'confirm-yes' }, i18n.deleteConfirmYes || 'Yes, delete')) + ) + ); +}; + +// ---------- pie y aviso de bienvenida ---------- + +// una linea: con cuanta gente estas sincronizando, dicho en claro, y la version. La +// clave, HcT/HcH y la licencia estan en Perfil, Estadisticas y Ajustes +const footerLine = (syncedPeers, version) => { + const i18n = t(); + const n = Number(syncedPeers) || 0; + const text = n > 0 + ? String(i18n.footerPeersSome || '{n} peers in sync').replace('{n}', String(n)) + : (i18n.footerPeersNone || 'Not syncing with anyone yet'); + return div({ class: 'oasis-footer oasis-footer-mobile' }, + a({ href: '/peers', class: n > 0 ? 'footer-sync is-on' : 'footer-sync' }, + span({ class: 'footer-sync-dot', 'aria-hidden': 'true' }), + span(text) + ), + span({ class: 'footer-version' }, `Oasis ${version} · GPLv3`) + ); +}; + +// el aviso de "pon tu avatar" solo en la portada (en cada pagina se comia una quinta +// parte de la pantalla) y diciendo para que es +const welcomeBannerHere = (here) => ['', '/', '/activity'].includes(here); +const welcomeBannerText = () => t().welcomeBannerTextMobile || t().welcomeBannerText; +const welcomeBannerAction = () => t().welcomeBannerActionMobile || t().welcomeBannerAction; + +module.exports = { + isMobile, VOTE_KINDS, voteKindPicker, voteBars, voteOwnNote, tribeRow, tribeTabs, + settingsOrder, languagePick, uxPick, workflowPick, welcomeSteps, deleteConfirm, footerLine, + welcomeBannerHere, welcomeBannerText, welcomeBannerAction +}; diff --git a/src/views/modules_view.js b/src/views/modules_view.js index 03be08b..c558559 100644 --- a/src/views/modules_view.js +++ b/src/views/modules_view.js @@ -1,6 +1,7 @@ const { form, button, div, h2, p, section, table, tr, td, input, a, br, ul, li, span, strong } = require("../server/node_modules/hyperaxe"); const { template, i18n } = require('./main_views'); const { getConfig } = require('../configs/config-manager.js'); +const { PRESETS: PRESET_LIST } = require('../models/workflows_model'); const modulesView = () => { const config = getConfig().modules; @@ -8,6 +9,13 @@ const modulesView = () => { { name: 'agenda', label: i18n.modulesAgendaLabel, description: i18n.modulesAgendaDescription }, { name: 'ai', label: i18n.modulesAILabel, description: i18n.modulesAIDescription }, { name: 'aiNav', label: i18n.modulesAINavLabel, description: i18n.modulesAINavDescription }, + { name: 'emergencies', label: i18n.modulesEmergenciesLabel, description: i18n.modulesEmergenciesDescription }, + { name: 'mailing', label: i18n.modulesMailingLabel, description: i18n.modulesMailingDescription }, + { name: 'rooms', label: i18n.modulesRoomsLabel, description: i18n.modulesRoomsDescription }, + { name: 'phone', label: i18n.modulesPhoneLabel, description: i18n.modulesPhoneDescription }, + { name: 'logistics', label: i18n.modulesLogisticsLabel, description: i18n.modulesLogisticsDescription }, + { name: 'podcasts', label: i18n.modulesPodcastsLabel, description: i18n.modulesPodcastsDescription }, + { name: 'campaigns', label: i18n.modulesCampaignsLabel, description: i18n.modulesCampaignsDescription }, { name: 'audios', label: i18n.modulesAudiosLabel, description: i18n.modulesAudiosDescription }, { name: 'banking', label: i18n.modulesBankingLabel, description: i18n.modulesBankingDescription }, { name: 'bookmarks', label: i18n.modulesBookmarksLabel, description: i18n.modulesBookmarksDescription }, @@ -15,87 +23,89 @@ const modulesView = () => { { name: 'chats', label: i18n.modulesChatsLabel, description: i18n.modulesChatsDescription }, { name: 'cipher', label: i18n.modulesCipherLabel, description: i18n.modulesCipherDescription }, { name: 'courts', label: i18n.modulesCourtsLabel, description: i18n.modulesCourtsDescription }, + { name: 'dev', label: i18n.modulesDevLabel, description: i18n.modulesDevDescription }, { name: 'docs', label: i18n.modulesDocsLabel, description: i18n.modulesDocsDescription }, { name: 'events', label: i18n.modulesEventsLabel, description: i18n.modulesEventsDescription }, { name: 'favorites', label: i18n.modulesFavoritesLabel, description: i18n.modulesFavoritesDescription }, + { name: 'blogs', label: i18n.modulesBlogsLabel, description: i18n.modulesBlogsDescription }, { name: 'fediverse', label: i18n.modulesFediverseLabel, description: i18n.modulesFediverseDescription }, { name: 'feed', label: i18n.modulesFeedLabel, description: i18n.modulesFeedDescription }, + { name: 'files', label: i18n.modulesFilesLabel, description: i18n.modulesFilesDescription }, { name: 'forum', label: i18n.modulesForumLabel, description: i18n.modulesForumDescription }, { name: 'games', label: i18n.modulesGamesLabel, description: i18n.modulesGamesDescription }, { name: 'graphos', label: i18n.modulesGraphosLabel, description: i18n.modulesGraphosDescription }, + { name: 'housing', label: i18n.modulesHousingLabel, description: i18n.modulesHousingDescription }, { name: 'images', label: i18n.modulesImagesLabel, description: i18n.modulesImagesDescription }, + { name: 'industry', label: i18n.modulesIndustryLabel, description: i18n.modulesIndustryDescription }, { name: 'invites', label: i18n.modulesInvitesLabel, description: i18n.modulesInvitesDescription }, { name: 'jobs', label: i18n.modulesJobsLabel, description: i18n.modulesJobsDescription }, - { name: 'legacy', label: i18n.modulesLegacyLabel, description: i18n.modulesLegacyDescription }, - { name: 'latest', label: i18n.modulesLatestLabel, description: i18n.modulesLatestDescription }, + { name: 'larp', label: i18n.modulesLarpLabel, description: i18n.modulesLarpDescription }, + { name: 'backup', label: i18n.modulesBackupLabel, description: i18n.modulesBackupDescription }, { name: 'logs', label: i18n.modulesLogsLabel, description: i18n.modulesLogsDescription }, { name: 'maps', label: i18n.modulesMapLabel, description: i18n.modulesMapDescription }, { name: 'market', label: i18n.modulesMarketLabel, description: i18n.modulesMarketDescription }, { name: 'melody', label: i18n.modulesMelodyLabel, description: i18n.modulesMelodyDescription }, - { name: 'multiverse', label: i18n.modulesMultiverseLabel, description: i18n.modulesMultiverseDescription }, { name: 'opinions', label: i18n.modulesOpinionsLabel, description: i18n.modulesOpinionsDescription }, { name: 'pads', label: i18n.modulesPadsLabel, description: i18n.modulesPadsDescription }, { name: 'parliament', label: i18n.modulesParliamentLabel, description: i18n.modulesParliamentDescription }, { name: 'pixelia', label: i18n.modulesPixeliaLabel, description: i18n.modulesPixeliaDescription }, + { name: 'polls', label: i18n.modulesPollsLabel, description: i18n.modulesPollsDescription }, { name: 'projects', label: i18n.modulesProjectsLabel, description: i18n.modulesProjectsDescription }, - { name: 'popular', label: i18n.modulesPopularLabel, description: i18n.modulesPopularDescription }, { name: 'reports', label: i18n.modulesReportsLabel, description: i18n.modulesReportsDescription }, + { name: 'school', label: i18n.modulesSchoolLabel, description: i18n.modulesSchoolDescription }, { name: 'shops', label: i18n.modulesShopsLabel, description: i18n.modulesShopsDescription }, - { name: 'summaries', label: i18n.modulesSummariesLabel, description: i18n.modulesSummariesDescription }, { name: 'tags', label: i18n.modulesTagsLabel, description: i18n.modulesTagsDescription }, { name: 'tasks', label: i18n.modulesTasksLabel, description: i18n.modulesTasksDescription }, - { name: 'threads', label: i18n.modulesThreadsLabel, description: i18n.modulesThreadsDescription }, { name: 'torrents', label: i18n.modulesTorrentsLabel, description: i18n.modulesTorrentsDescription }, { name: 'transfers', label: i18n.modulesTransfersLabel, description: i18n.modulesTransfersDescription }, { name: 'trending', label: i18n.modulesTrendingLabel, description: i18n.modulesTrendingDescription }, { name: 'tribes', label: i18n.modulesTribesLabel, description: i18n.modulesTribesDescription }, - { name: 'larp', label: i18n.modulesLarpLabel, description: i18n.modulesLarpDescription }, { name: 'videos', label: i18n.modulesVideosLabel, description: i18n.modulesVideosDescription }, { name: 'votes', label: i18n.modulesVotationsLabel, description: i18n.modulesVotationsDescription }, { name: 'wallet', label: i18n.modulesWalletLabel, description: i18n.modulesWalletDescription }, - { name: 'topics', label: i18n.modulesTopicsLabel, description: i18n.modulesTopicsDescription } - ]; + { name: 'wiki', label: i18n.modulesWikiLabel, description: i18n.modulesWikiDescription } + ].sort((x, y) => String(x.label || x.name).localeCompare(String(y.label || y.name), undefined, { sensitivity: 'base' })); - const moduleStates = modules.reduce((acc, mod) => { + const MOBILE_UNAVAILABLE = ['ai', 'aiNav', 'dev', 'games', 'logs', 'maps', 'pixelia']; + const isMobile = process.env.OASIS_MOBILE === '1'; + const visibleModules = isMobile ? modules.filter(mod => !MOBILE_UNAVAILABLE.includes(mod.name)) : modules; + + const moduleStates = visibleModules.reduce((acc, mod) => { acc[`${mod.name}Mod`] = config[`${mod.name}Mod`] === 'on' ? 'on' : 'off'; return acc; }, {}); - const activeModulesCount = modules.filter(mod => moduleStates[`${mod.name}Mod`] === 'on').length; - const totalModulesCount = modules.length; + const activeModulesCount = visibleModules.filter(mod => moduleStates[`${mod.name}Mod`] === 'on').length; + const totalModulesCount = visibleModules.length; const disabledModulesCount = totalModulesCount - activeModulesCount; - const header = div({ class: 'tags-header' }, + const header = [div({ class: 'tags-header module-header-line' }, h2(i18n.modulesViewTitle), - p(i18n.modulesViewDescription), - div({ style: 'margin-bottom:16px;' }, - `${i18n.modulesTotalModulesLabel}: `, - strong({ style: 'color:#888;' }, totalModulesCount), - ul({ style: 'list-style-type:none; padding:0; margin:0;' }, - li({ style: 'font-size:18px; color:#555; margin:8px 0;' }, - `${i18n.modulesEnabledModulesLabel}: `, - span({ style: 'color:#888;' }, activeModulesCount) - ), - li({ style: 'font-size:18px; color:#555; margin:8px 0;' }, - `${i18n.modulesDisabledModulesLabel}: `, - span({ style: 'color:#888;' }, disabledModulesCount) - ) + p(i18n.modulesViewDescription) + )]; + + const counts = div({ class: 'modules-counts' }, + span({ class: 'modules-count-item' }, + `${i18n.modulesTotalModulesLabel}: `, + strong({ class: 'modules-count-value' }, totalModulesCount) + ), + span({ class: 'modules-count-item' }, + `${i18n.modulesEnabledModulesLabel}: `, + strong({ class: 'modules-count-value' }, activeModulesCount) + ), + span({ class: 'modules-count-item' }, + `${i18n.modulesDisabledModulesLabel}: `, + strong({ class: 'modules-count-value' }, disabledModulesCount) ) - ) - ); + ); - const PRESETS = { - minimal: ['feed', 'forum', 'games', 'images', 'videos', 'audios', 'bookmarks', 'tags', 'trending', 'popular', 'latest', 'threads', 'opinions', 'cipher', 'legacy'], - social: ['agenda', 'audios', 'bookmarks', 'calendars', 'chats', 'cipher', 'courts', 'docs', 'events', 'favorites', 'fediverse', 'feed', 'forum', 'games', 'images', 'invites', 'larp', 'legacy', 'logs', 'maps', 'multiverse', 'opinions', 'pads', 'parliament', 'pixelia', 'melody', 'projects', 'reports', 'tags', 'tasks', 'threads', 'trending', 'tribes', 'videos', 'votes'], - economy: ['agenda', 'audios', 'bookmarks', 'calendars', 'chats', 'cipher', 'courts', 'docs', 'events', 'favorites', 'fediverse', 'feed', 'forum', 'games', 'images', 'invites', 'larp', 'legacy', 'logs', 'maps', 'multiverse', 'opinions', 'pads', 'parliament', 'pixelia', 'melody', 'projects', 'reports', 'tags', 'tasks', 'threads', 'trending', 'tribes', 'videos', 'votes', 'banking', 'wallet', 'transfers', 'market', 'jobs', 'shops'], - full: modules.map(m => m.name) - }; + const PRESETS = PRESET_LIST; - const presetButtons = div({ class: 'preset-group', style: 'display:flex;gap:8px;flex-wrap:nowrap;margin-bottom:16px;' }, + const presetButtons = div({ class: 'preset-group' }, Object.entries(PRESETS).map(([key, mods]) => { const presetLabel = (i18n[`modulesPreset_${key}`] || key).toUpperCase(); - const isActive = modules.every(m => mods.includes(m.name) === (moduleStates[`${m.name}Mod`] === 'on')); - return form({ action: "/modules/preset", method: "post", style: "display:inline;margin:0;" }, + const isActive = visibleModules.every(m => mods.includes(m.name) === (moduleStates[`${m.name}Mod`] === 'on')); + return form({ action: "/modules/preset", method: "post", class: "modules-preset-form" }, input({ type: "hidden", name: "preset", value: key }), button({ type: 'submit', @@ -109,8 +119,8 @@ const modulesView = () => { i18n.modules, section(header), section( - h2(i18n.modulesPresetTitle || "Common Configurations"), - presetButtons, + ...(isMobile ? [] : [h2(i18n.modulesPresetTitle || "Common Configurations"), presetButtons]), + counts, form( { action: "/save-modules", method: "post" }, table( @@ -118,9 +128,9 @@ const modulesView = () => { tr( td(i18n.modulesModuleName), td(i18n.modulesModuleDescription), - td({ style: 'text-align: center;' }, i18n.modulesModuleStatus) + td({ class: 'module-status-cell' }, i18n.modulesModuleStatus) ), - modules.map(mod => + visibleModules.map(mod => tr( td(a({ href: `/${mod.name}` }, mod.label)), td(p(mod.description)), @@ -138,7 +148,7 @@ const modulesView = () => { ) ), div( - { class: "save-button-container", style: "margin-top: 20px; text-align: center;" }, + { class: "save-button-container" }, button({ type: "submit", class: "submit-button" }, i18n.saveSettings) ) ) diff --git a/src/views/opinions_view.js b/src/views/opinions_view.js index c742fd0..cb020b5 100644 --- a/src/views/opinions_view.js +++ b/src/views/opinions_view.js @@ -1,12 +1,13 @@ const { div, h2, p, section, button, form, a, img, video: videoHyperaxe, audio: audioHyperaxe, input, table, tr, th, td, br, span, details, summary } = require("../server/node_modules/hyperaxe"); -const { template, i18n, userLink, renderSpreadButton, renderContentActions } = require('./main_views'); +const RAW_HTML = Symbol.for('oasis.rawHtml'); +const moment = require("../server/node_modules/moment"); +const { template, i18n, userLink, renderSpreadButton, renderContentActions, renderVotesSummary, renderModuleStats, renderCardMetaRow, CONTENT_FAV_KIND, CONTENT_TORRENTABLE, paged } = require('./main_views'); +const { renderZoomableImage } = require('./gallery_view'); const { config } = require('../server/SSB_server.js'); -const { renderTextWithStyles } = require('../backend/renderTextWithStyles'); -const { renderUrl } = require('../backend/renderUrl'); +const { renderStyledText, renderStyledHtml, safeExternalHref } = require('../backend/renderStyledText'); const opinionCategories = require('../backend/opinion_categories'); const { sanitizeHtml } = require('../backend/sanitizeHtml'); -const seenDocumentTitles = new Set(); const detailHref = (type, key) => { switch (type) { @@ -15,30 +16,152 @@ const detailHref = (type, key) => { case 'video': return `/videos/${encodeURIComponent(key)}`; case 'audio': return `/audios/${encodeURIComponent(key)}`; case 'torrent': return `/torrents/${encodeURIComponent(key)}`; + case 'file': return `/files/${encodeURIComponent(key)}`; case 'document': return `/documents/${encodeURIComponent(key)}`; case 'feed': return `/feed/${encodeURIComponent(key)}`; + case 'post': return `/blogs/${encodeURIComponent(key)}`; case 'votes': return `/votes/${encodeURIComponent(key)}`; case 'transfer': return `/transfers/${encodeURIComponent(key)}`; + case 'industry': return `/industry/${encodeURIComponent(key)}`; + case 'project': return `/projects/${encodeURIComponent(key)}`; + case 'report': return `/reports/${encodeURIComponent(key)}`; + case 'task': return `/tasks/${encodeURIComponent(key)}`; + case 'event': return `/events/${encodeURIComponent(key)}`; + case 'shopProduct': return `/shops/product/${encodeURIComponent(key)}`; + case 'housing': return `/housing/${encodeURIComponent(key)}`; + case 'market': return `/market/${encodeURIComponent(key)}`; + case 'schoolCourse': return `/school/course/${encodeURIComponent(key)}`; + case 'podcast': return `/podcasts/${encodeURIComponent(key)}`; + case 'podcastEpisode': return `/podcasts/episode/${encodeURIComponent(key)}`; + case 'campaign': return `/campaigns/${encodeURIComponent(key)}`; + case 'logisticsRoute': return `/logistics/${encodeURIComponent(key)}`; default: return null; } }; const renderContentHtml = (content, key) => { switch (content.type) { + case 'industry': + return div({ class: 'opinion-industry' }, + div({ class: 'card-section industry' }, + div({ class: 'card-field' }, + span({ class: 'card-label' }, i18n.industryName + ':'), + span({ class: 'card-value' }, content.name || '') + ), + content.sector ? div({ class: 'card-field' }, + span({ class: 'card-label' }, i18n.industrySector + ':'), + span({ class: 'card-value' }, String(content.sector).toUpperCase()) + ) : "", + content.membershipPolicy ? div({ class: 'card-field' }, + span({ class: 'card-label' }, i18n.industryMembershipPolicy + ':'), + span({ class: 'card-value' }, String(i18n['industryPolicy_' + content.membershipPolicy] || content.membershipPolicy).toUpperCase()) + ) : "", + content.description ? p(...renderStyledText(content.description)) : null, + Array.isArray(content.tags) && content.tags.length + ? div({ class: 'card-tags' }, content.tags.map(tag => + a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: 'tag-link' }, `#${tag}`))) + : null + ) + ); + case 'industryBlueprint': + return div({ class: 'opinion-industry' }, + div({ class: 'card-section industry' }, + div({ class: 'card-field' }, + span({ class: 'card-label' }, i18n.industryBlueprint + ':'), + span({ class: 'card-value' }, content.name || '') + ), + content.outKind ? div({ class: 'card-field' }, + span({ class: 'card-label' }, i18n.industryOutputKind + ':'), + span({ class: 'card-value' }, String(i18n['industryKind_' + content.outKind] || content.outKind).toUpperCase()) + ) : "", + content.laborHours ? div({ class: 'card-field' }, + span({ class: 'card-label' }, i18n.industryLaborHours + ':'), + span({ class: 'card-value' }, String(content.laborHours)) + ) : "", + content.description ? p(...renderStyledText(content.description)) : null + ) + ); + case 'housing': { + const free = String(content.housing_type || '').toLowerCase() === 'couchsurfing'; + return div({ class: 'opinion-housing' }, + div({ class: 'card-section housing' }, + div({ class: 'card-field' }, + span({ class: 'card-label' }, i18n.title + ':'), + span({ class: 'card-value' }, content.title || '') + ), + content.housing_type ? div({ class: 'card-field' }, + span({ class: 'card-label' }, i18n.housingType + ':'), + span({ class: 'card-value' }, String(i18n['housingType' + String(content.housing_type).toUpperCase()] || content.housing_type).toUpperCase()) + ) : null, + content.property_type ? div({ class: 'card-field' }, + span({ class: 'card-label' }, i18n.housingProperty + ':'), + span({ class: 'card-value' }, String(i18n['housingProperty' + String(content.property_type).toUpperCase()] || content.property_type).toUpperCase()) + ) : null, + div({ class: 'card-field' }, + span({ class: 'card-label' }, i18n.housingPrice + ':'), + span({ class: 'card-value' }, free ? (i18n.housingFree || 'FREE') : `${Number(content.price || 0).toFixed(2)} ECO`) + ), + content.place ? div({ class: 'card-field' }, + span({ class: 'card-label' }, i18n.housingPlace + ':'), + span({ class: 'card-value' }, content.place) + ) : null, + Number(content.rooms) > 0 ? div({ class: 'card-field' }, + span({ class: 'card-label' }, i18n.housingRooms + ':'), + span({ class: 'card-value' }, String(content.rooms)) + ) : null, + Number(content.size) > 0 ? div({ class: 'card-field' }, + span({ class: 'card-label' }, i18n.housingSize + ':'), + span({ class: 'card-value' }, `${content.size} m²`) + ) : null, + Number(content.capacity) > 0 ? div({ class: 'card-field' }, + span({ class: 'card-label' }, i18n.housingCapacity + ':'), + span({ class: 'card-value' }, String(content.capacity)) + ) : null, + content.description ? p(...renderStyledText(content.description)) : null, + Array.isArray(content.tags) && content.tags.length + ? div({ class: 'card-tags' }, content.tags.map(tag => + a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: 'tag-link' }, `#${tag}`))) + : null + ) + ); + } + case 'market': + return div({ class: 'opinion-market' }, + div({ class: 'card-section market' }, + div({ class: 'card-field' }, + span({ class: 'card-label' }, i18n.title + ':'), + span({ class: 'card-value' }, content.title || '') + ), + content.item_type ? div({ class: 'card-field' }, + span({ class: 'card-label' }, i18n.marketItemType + ':'), + span({ class: 'card-value' }, String(content.item_type).toUpperCase()) + ) : null, + content.item_status ? div({ class: 'card-field' }, + span({ class: 'card-label' }, (i18n.marketItemCondition || i18n.status) + ':'), + span({ class: 'card-value' }, String(content.item_status).toUpperCase()) + ) : null, + div({ class: 'price-chip' }, `${content.price} ECO`), + content.description ? p(...renderStyledText(content.description)) : null, + Array.isArray(content.tags) && content.tags.length + ? div({ class: 'card-tags' }, content.tags.map(tag => + a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: 'tag-link' }, `#${tag}`))) + : null + ) + ); case 'bookmark': return div({ class: 'opinion-bookmark' }, div({ class: 'card-section bookmark' }, h2(content.url ? div({ class: 'card-field' }, - span({ class: 'card-label' }, p(a({ href: content.url, target: '_blank', class: "bookmark-url" }, content.url))) + span({ class: 'card-label' }, p(a({ href: safeExternalHref(content.url), target: '_blank', rel: 'noopener noreferrer', class: "bookmark-url" }, content.url))) ) : ""), content.lastVisit ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.bookmarkLastVisitLabel + ':'), - span({ class: 'card-value' }, new Date(content.lastVisit).toLocaleString()) + span({ class: 'card-value' }, moment(content.lastVisit).format("YYYY/MM/DD HH:mm")) ) : "", content.description ? [ span({ class: 'card-label' }, i18n.bookmarkDescriptionLabel + ":"), - p(...renderUrl(content.description)) + p(...renderStyledText(content.description)) ] : null ) @@ -53,16 +176,12 @@ const renderContentHtml = (content, key) => { content.description ? [ span({ class: 'card-label' }, i18n.imageDescriptionLabel + ":"), - p(...renderUrl(content.description)) + p(...renderStyledText(content.description)) ] : null, - content.meme ? div({ class: 'card-field' }, - span({ class: 'card-label' }, i18n.trendingCategory + ':'), - span({ class: 'card-value' }, i18n.meme) - ) : "", br(), - div({ class: 'card-field' }, - img({ src: `/blob/${encodeURIComponent(content.url)}`, class: 'feed-image' }) + div({ class: 'card-field image-container' }, + renderZoomableImage(`/blob/${encodeURIComponent(content.url)}`, { imgClass: 'post-image' }) ) ) ); @@ -76,16 +195,16 @@ const renderContentHtml = (content, key) => { content.description ? [ span({ class: 'card-label' }, i18n.videoDescriptionLabel + ":"), - p(...renderUrl(content.description)) + p(...renderStyledText(content.description)) ] : null, div({ class: 'card-field' }, videoHyperaxe({ controls: true, + class: 'post-video', src: `/blob/${encodeURIComponent(content.url)}`, type: content.mimeType || 'video/mp4', - width: '640', - height: '360' + preload: 'metadata' }) ) ) @@ -100,12 +219,13 @@ const renderContentHtml = (content, key) => { content.description ? [ span({ class: 'card-label' }, i18n.audioDescriptionLabel + ":"), - p(...renderUrl(content.description)) + p(...renderStyledText(content.description)) ] : null, - div({ class: 'card-field' }, + div({ class: 'card-field audio-container' }, audioHyperaxe({ controls: true, + class: 'post-audio', src: `/blob/${encodeURIComponent(content.url)}`, type: content.mimeType, preload: 'metadata' @@ -113,6 +233,12 @@ const renderContentHtml = (content, key) => { ) ) ); + case 'file': + return div({ class: 'opinion-file' }, + div({ class: 'card-section' }, + content.title ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.fileTitleLabel || 'Title') + ':'), span({ class: 'card-value' }, content.title)) : "" + ) + ); case 'torrent': return div({ class: 'opinion-torrent' }, div({ class: 'card-section' }, @@ -121,8 +247,6 @@ const renderContentHtml = (content, key) => { ); case 'document': { const t = content.title?.trim(); - if (t && seenDocumentTitles.has(t)) return null; - if (t) seenDocumentTitles.add(t); return div({ class: 'opinion-document' }, div({ class: 'card-section document' }, t ? div({ class: 'card-field' }, @@ -132,7 +256,7 @@ const renderContentHtml = (content, key) => { content.description ? [ span({ class: 'card-label' }, i18n.documentDescriptionLabel + ":"), - p(...renderUrl(content.description)) + p(...renderStyledText(content.description)) ] : null, div({ class: 'card-field' }, @@ -141,10 +265,20 @@ const renderContentHtml = (content, key) => { ) ); } + case 'post': + return div({ class: 'opinion-blog' }, + div({ class: 'card-section blog' }, + content.contentWarning ? div({ class: 'shop-title-row' }, h2({ class: 'tribe-card-title' }, a({ href: `/blogs/${encodeURIComponent(key)}` }, content.contentWarning))) : "", + div({ class: 'blog-card-text', [RAW_HTML]: sanitizeHtml(renderStyledHtml(String(content.text || '').trim().length > 420 ? String(content.text).trim().slice(0, 420).replace(/\s+$/, '') + '…' : String(content.text || '').trim())) }), + Array.isArray(content.tags) && content.tags.length + ? div({ class: 'card-tags' }, content.tags.map(tag => a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: 'tag-link' }, `#${tag}`))) + : null + ) + ); case 'feed': return div({ class: 'opinion-feed' }, div({ class: 'card-section feed' }, - div({ class: 'feed-text', innerHTML: sanitizeHtml(renderTextWithStyles(content.text)) }), + div({ class: 'feed-text', [RAW_HTML]: sanitizeHtml(renderStyledHtml(content.text)) }), content.refeeds ? h2({ class: 'card-field' }, span({ class: 'card-label' }, `${i18n.tribeFeedRefeeds}: `), span({ class: 'card-value' }, content.refeeds)) : "" @@ -162,7 +296,7 @@ const renderContentHtml = (content, key) => { ), div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.voteDeadline + ':'), - span({ class: 'card-value' }, content.deadline ? new Date(content.deadline).toLocaleString() : '') + span({ class: 'card-value' }, content.deadline ? moment(content.deadline).format("YYYY/MM/DD HH:mm") : '') ), div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.voteTotalVotes + ':'), @@ -184,7 +318,7 @@ const renderContentHtml = (content, key) => { ), div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.deadline + ':'), - span({ class: 'card-value' }, content.deadline ? new Date(content.deadline).toLocaleString() : '') + span({ class: 'card-value' }, content.deadline ? moment(content.deadline).format("YYYY/MM/DD HH:mm") : '') ), div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.status + ':'), @@ -198,7 +332,7 @@ const renderContentHtml = (content, key) => { span({ class: 'card-label' }, i18n.from + ':'), span({ class: 'card-value' }, userLink(content.from)) ), - div({ class: 'card-field' }, + process.env.OASIS_MOBILE === '1' ? null : div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.to + ':'), span({ class: 'card-value' }, userLink(content.to)) ), @@ -212,15 +346,19 @@ const renderContentHtml = (content, key) => { return div({ class: 'styled-text' }, div({ class: 'card-section styled-text-content' }, div({ class: 'card-field' }, - span({ class: 'card-value', innerHTML: sanitizeHtml(content.text || content.description || content.title || '[no content]') }) + span({ class: 'card-value', [RAW_HTML]: sanitizeHtml(renderStyledHtml(content.title || content.name || content.text || content.description || '[no content]')) }) ) ) ); } }; -exports.opinionsView = (items, filter, spreadMap = new Map()) => { - seenDocumentTitles.clear(); +exports.opinionsView = (items, filter, spreadMap = new Map(), q = '', allItems = null, params = {}) => { + const presentCats = new Set(); + for (const it of (Array.isArray(allItems) ? allItems : items)) { + const ops = (it.value?.content || {}).opinions || {}; + for (const [cat, n] of Object.entries(ops)) if (Number(n) > 0) presentCats.add(cat); + } items = items .filter(item => { const c = item.value?.content || item.content; @@ -236,9 +374,19 @@ exports.opinionsView = (items, filter, spreadMap = new Map()) => { }); const title = i18n.opinionsTitle; - const baseFilters = ['TOP', 'ALL', 'MINE', 'RECENT']; + const baseFilters = ['ALL', 'RECENT', 'MINE', 'TOP']; + const emptyOps = (Array.isArray(allItems) ? allItems : items).length === 0 && !String(q || '').trim(); - const cards = items + const seenDocs = new Set(); + const shownItems = items.filter(item => { + const c = item.value.content || {}; + const t = c.type === 'document' ? String(c.title || '').trim() : ''; + if (!t) return true; + if (seenDocs.has(t)) return false; + seenDocs.add(t); + return true; + }); + const cards = paged(shownItems) .map(item => { const c = item.value.content; const key = item.key; @@ -247,7 +395,7 @@ exports.opinionsView = (items, filter, spreadMap = new Map()) => { const voteEntries = Object.entries(c.opinions || {}); const total = voteEntries.reduce((sum, [, v]) => sum + v, 0); const voted = c.opinions_inhabitants?.includes(config.keys.id); - const created = new Date(item.value.timestamp).toLocaleString(); + const created = moment(item.value.timestamp).format("YYYY/MM/DD HH:mm"); const allCats = opinionCategories; const isOwn = item.value.author && String(item.value.author) === String(config.keys.id); @@ -255,40 +403,33 @@ exports.opinionsView = (items, filter, spreadMap = new Map()) => { { class: 'trending-card opinions-card' + (isOwn ? ' own-content' : '') }, div({ class: 'card-header activity-card-header' }, span({ class: 'pm-exposition-chip pm-exposition-whole' }, - span({ class: 'pm-exposition-text' }, String(c.type || '').toUpperCase()) + span({ class: 'pm-exposition-text' }, String(c.type === 'post' ? (i18n.typePost || 'BLOG') : (c.type || '')).toUpperCase()) ), - renderContentActions(key, detailHref(c.type, key)) + renderContentActions(key, detailHref(c.type, key), { + spread: spreadMap.get(key) || null, + author: item.value.author, + reportTitle: c.title || c.question || c.concept || c.name || c.contentWarning || '', + ...(CONTENT_TORRENTABLE.has(c.type) && typeof c.url === 'string' && c.url.startsWith('&') + ? { torrentFrom: { blobId: c.url, name: c.title || c.fileName || '' } } + : {}), + ...(CONTENT_FAV_KIND[c.type] + ? { favKind: CONTENT_FAV_KIND[c.type], isFavorite: params.favIndex instanceof Map ? params.favIndex.get(String(key)) === CONTENT_FAV_KIND[c.type] : false, returnTo: `/opinions?filter=${encodeURIComponent(filter)}` } + : {}) + }) ), div( { class: 'card-section opinions-card-body' }, - contentHtml, - div({ class: 'card-spread-left' }, renderSpreadButton(key, spreadMap.get(key))), - p({ class: 'card-footer' }, - span({ class: 'date-link' }, `${created} ${i18n.performed} `), - userLink(item.value.author) - ), - (() => { - const entries = voteEntries.filter(([, v]) => v > 0); - const dominantPart = (() => { - if (!entries.length) return null; - const maxVal = Math.max(...entries.map(([, v]) => v)); - const dominant = entries.filter(([, v]) => v === maxVal).map(([k]) => i18n['vote' + k.charAt(0).toUpperCase() + k.slice(1)] || k); - return [ - span({ class: 'trending-dominant-sep' }, '|'), - span({ class: 'trending-dominant-text' }, `${i18n.moreVoted || 'More Voted'}: ${dominant.join(' + ')}`) - ]; - })(); - return h2( - `${i18n.totalOpinions || i18n.opinionsTotalCount}: `, - span({ class: 'trending-total-count' }, String(total)), - ...(dominantPart || []) - ); - })(), + contentHtml + ), + renderVotesSummary(c.opinions), + renderCardMetaRow( details({ class: 'opinions-voting-collapse' }, - summary({ class: 'opinions-summary' }, `${i18n.opinionsTitle || 'Opinions'} (${Object.values(c.opinions || {}).reduce((s, n) => s + (Number(n) || 0), 0)})`), + summary({ class: 'opinions-summary' }, + span({ class: 'opinions-summary-icon' }, 'ꔍ'), + span({ class: 'opinions-summary-count' }, `(${Object.values(c.opinions || {}).reduce((s, n) => s + (Number(n) || 0), 0)})`)), div({ class: 'voting-buttons' }, allCats.map(cat => { - const label = `${i18n['vote' + cat.charAt(0).toUpperCase() + cat.slice(1)] || cat} [${c.opinions?.[cat] || 0}]`; + const label = `${String(i18n['vote' + cat.charAt(0).toUpperCase() + cat.slice(1)] || cat).toUpperCase()} [${c.opinions?.[cat] || 0}]`; if (voted) { return button({ class: 'vote-btn', type: 'button' }, label); } @@ -297,6 +438,10 @@ exports.opinionsView = (items, filter, spreadMap = new Map()) => { ); }) ) + ), + p({ class: 'card-footer' }, + span({ class: 'date-link' }, `${created}`), + userLink(item.value.author) ) ) ); @@ -304,7 +449,7 @@ exports.opinionsView = (items, filter, spreadMap = new Map()) => { .filter(Boolean); const hasDocuments = items.some(item => item.value.content?.type === 'document'); - const header = div({ class: 'tags-header' }, + const header = div({ class: 'tags-header module-header-line' }, h2(title), p(i18n.shareYourOpinions) ); @@ -313,71 +458,80 @@ exports.opinionsView = (items, filter, spreadMap = new Map()) => { title, section( header, - div({ class: 'mode-buttons' }, + emptyOps ? null : div({ class: 'mode-buttons' }, div({ class: 'column' }, baseFilters.map(mode => form({ method: 'GET', action: '/opinions' }, input({ type: 'hidden', name: 'filter', value: mode }), - button({ type: 'submit', class: filter === mode ? 'filter-btn active' : 'filter-btn' }, i18n[mode + 'Button'] || mode) + button({ type: 'submit', class: filter === mode ? 'filter-btn active' : 'filter-btn' }, String(i18n[mode + 'Button'] || mode).toUpperCase()) ) ) ), div({ class: 'column' }, - opinionCategories.positive.slice(0, 5).map(mode => + opinionCategories.positive.slice(0, 5).filter(mode => presentCats.has(mode)).map(mode => form({ method: 'GET', action: '/opinions' }, input({ type: 'hidden', name: 'filter', value: mode }), - button({ type: 'submit', class: filter === mode ? 'filter-btn active' : 'filter-btn' }, i18n[mode + 'Button'] || mode) + button({ type: 'submit', class: filter === mode ? 'filter-btn active' : 'filter-btn' }, String(i18n[mode + 'Button'] || mode).toUpperCase()) ) ) ), div({ class: 'column' }, - opinionCategories.positive.slice(5, 10).map(mode => + opinionCategories.positive.slice(5, 10).filter(mode => presentCats.has(mode)).map(mode => form({ method: 'GET', action: '/opinions' }, input({ type: 'hidden', name: 'filter', value: mode }), - button({ type: 'submit', class: filter === mode ? 'filter-btn active' : 'filter-btn' }, i18n[mode + 'Button'] || mode) + button({ type: 'submit', class: filter === mode ? 'filter-btn active' : 'filter-btn' }, String(i18n[mode + 'Button'] || mode).toUpperCase()) ) ) ), div({ class: 'column' }, - opinionCategories.positive.slice(10, 15).map(mode => + opinionCategories.positive.slice(10, 15).filter(mode => presentCats.has(mode)).map(mode => form({ method: 'GET', action: '/opinions' }, input({ type: 'hidden', name: 'filter', value: mode }), - button({ type: 'submit', class: filter === mode ? 'filter-btn active' : 'filter-btn' }, i18n[mode + 'Button'] || mode) + button({ type: 'submit', class: filter === mode ? 'filter-btn active' : 'filter-btn' }, String(i18n[mode + 'Button'] || mode).toUpperCase()) + ) + ) + ) + , + div({ class: 'column' }, + opinionCategories.constructive.slice(0, 5).filter(mode => presentCats.has(mode)).map(mode => + form({ method: 'GET', action: '/opinions' }, + input({ type: 'hidden', name: 'filter', value: mode }), + button({ type: 'submit', class: filter === mode ? 'filter-btn active' : 'filter-btn' }, String(i18n[mode + 'Button'] || mode).toUpperCase()) + ) + ) + ), + div({ class: 'column' }, + opinionCategories.constructive.slice(5, 11).filter(mode => presentCats.has(mode)).map(mode => + form({ method: 'GET', action: '/opinions' }, + input({ type: 'hidden', name: 'filter', value: mode }), + button({ type: 'submit', class: filter === mode ? 'filter-btn active' : 'filter-btn' }, String(i18n[mode + 'Button'] || mode).toUpperCase()) + ) + ) + ), + div({ class: 'column' }, + opinionCategories.moderation.slice(0, 5).filter(mode => presentCats.has(mode)).map(mode => + form({ method: 'GET', action: '/opinions' }, + input({ type: 'hidden', name: 'filter', value: mode }), + button({ type: 'submit', class: filter === mode ? 'filter-btn active' : 'filter-btn' }, String(i18n[mode + 'Button'] || mode).toUpperCase()) + ) + ) + ), + div({ class: 'column' }, + opinionCategories.moderation.slice(5, 10).filter(mode => presentCats.has(mode)).map(mode => + form({ method: 'GET', action: '/opinions' }, + input({ type: 'hidden', name: 'filter', value: mode }), + button({ type: 'submit', class: filter === mode ? 'filter-btn active' : 'filter-btn' }, String(i18n[mode + 'Button'] || mode).toUpperCase()) ) ) ) ), - div({ class: 'mode-buttons' }, - div({ class: 'column' }, - opinionCategories.constructive.slice(0, 5).map(mode => - form({ method: 'GET', action: '/opinions' }, - input({ type: 'hidden', name: 'filter', value: mode }), - button({ type: 'submit', class: filter === mode ? 'filter-btn active' : 'filter-btn' }, i18n[mode + 'Button'] || mode) - ) - ) - ), - div({ class: 'column' }, - opinionCategories.constructive.slice(5, 11).map(mode => - form({ method: 'GET', action: '/opinions' }, - input({ type: 'hidden', name: 'filter', value: mode }), - button({ type: 'submit', class: filter === mode ? 'filter-btn active' : 'filter-btn' }, i18n[mode + 'Button'] || mode) - ) - ) - ), - div({ class: 'column' }, - opinionCategories.moderation.slice(0, 5).map(mode => - form({ method: 'GET', action: '/opinions' }, - input({ type: 'hidden', name: 'filter', value: mode }), - button({ type: 'submit', class: filter === mode ? 'filter-btn active' : 'filter-btn' }, i18n[mode + 'Button'] || mode) - ) - ) - ), - div({ class: 'column' }, - opinionCategories.moderation.slice(5, 10).map(mode => - form({ method: 'GET', action: '/opinions' }, - input({ type: 'hidden', name: 'filter', value: mode }), - button({ type: 'submit', class: filter === mode ? 'filter-btn active' : 'filter-btn' }, i18n[mode + 'Button'] || mode) - ) + emptyOps ? null : div({ class: 'filters activity-filter-chips activity-toolbar-row' }, + renderModuleStats(shownItems.length), + form({ method: 'GET', action: '/opinions', class: 'filter-box' }, + input({ type: 'hidden', name: 'filter', value: filter }), + input({ type: 'text', name: 'q', value: q, placeholder: i18n.opinionsSearchPlaceholder, class: 'filter-box__input' }), + div({ class: 'filter-box__controls' }, + button({ type: 'submit', class: 'filter-box__button' }, i18n.searchButton) ) ) ), @@ -390,8 +544,7 @@ exports.opinionsView = (items, filter, spreadMap = new Map()) => { ); return `${html}${hasDocuments - ? ` - ` + ? `` : ''}`; }; diff --git a/src/views/pads_view.js b/src/views/pads_view.js index 25c9ec6..26eae79 100644 --- a/src/views/pads_view.js +++ b/src/views/pads_view.js @@ -1,5 +1,5 @@ -const { div, h2, h3, h4, p, section, button, form, a, span, br, textarea, input, label, select, option, table, tr, td } = require("../server/node_modules/hyperaxe") -const { template, i18n, userLink, renderStateChip, renderLifespanChip, renderSpreadButton } = require("./main_views") +const { div, h2, h3, h4, p, section, button, form, a, span, br, textarea, input, label, select, option, table, tr, td, details, summary } = require("../server/node_modules/hyperaxe") +const { template, i18n, userLink, renderStateChip, renderLifespanChip, renderSpreadButton , renderContentActions, renderInviteQrCard, renderSubscriptionBox, renderModuleStatsBy, moduleIsEmpty, contentDeleteAction, paged } = require("./main_views") const { renderEncryptedChip } = require("./clearnet_view") const moment = require("../server/node_modules/moment") const { config } = require("../server/SSB_server.js") @@ -82,15 +82,17 @@ const renderPadStatusChip = (status, isClosed) => { return renderStateChip(variant, icon, label) } -const renderModeButtons = (currentFilter) => +const renderModeButtons = (currentFilter, emptyMod = false, modesAvail = null) => div({ class: "tribe-mode-buttons" }, - ["all", "mine", "recent", "open", "closed"].map(f => + ...(emptyMod ? [] : [ + ["all", "recent", "mine", "open", "closed"].filter(f => f === "all" || f === currentFilter || (modesAvail && modesAvail[f] !== false)).map(f => form({ method: "GET", action: "/pads" }, input({ type: "hidden", name: "filter", value: f }), button({ type: "submit", class: currentFilter === f ? "filter-btn active" : "filter-btn" }, i18n[`padFilter${f.charAt(0).toUpperCase() + f.slice(1)}`] || f.toUpperCase()) ) ), + ]), form({ method: "GET", action: "/pads" }, input({ type: "hidden", name: "filter", value: "create" }), button({ type: "submit", class: "create-button" }, i18n.padCreate || "Create Pad") @@ -98,13 +100,20 @@ const renderModeButtons = (currentFilter) => ) -const renderPadCard = (pad, filter, spreadInfo) => { +const renderPadCard = exports.renderPadCard = (pad, filter, spreadInfo) => { const chips = [ renderPadStatusChip(pad.status, pad.isClosed), renderEncryptedChip(i18n), - renderLifespanChip(pad.lifetime, i18n) + renderLifespanChip(pad.lifetime, i18n), + pad.subscriptionIn === true + ? renderStateChip("mutuals", "✉", i18n.subscriptionOn) + : (pad.subscriptionIn === false ? renderStateChip("closed", "✉", i18n.subscriptionOff) : null) ].filter(Boolean) return div({ class: "tribe-card" }, + div({ class: "card-header activity-card-header" }, + span(), + renderContentActions(pad.rootId || pad.key, `/pads/${encodeURIComponent(pad.rootId)}`, { spread: spreadInfo || null, author: pad.author, favKind: 'pads', isFavorite: pad.isFavorite, reportTitle: pad.title, deleteAction: String(pad.author) === String(userId) ? contentDeleteAction('pad', pad.rootId || pad.key) : undefined }) + ), div({ class: "tribe-card-body" }, div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, @@ -113,17 +122,10 @@ const renderPadCard = (pad, filter, spreadInfo) => { ), chips.length ? div({ class: "card-chips-row" }, ...chips) : null, pad.deadline - ? p({ class: "job-meta-line" }, `${i18n.padDeadlineLabel || "Deadline"}: ${moment(pad.deadline).format("YYYY-MM-DD HH:mm")}`) + ? p({ class: "job-meta-line" }, `${i18n.padDeadlineLabel || "Deadline"}: ${moment(pad.deadline).format("YYYY/MM/DD HH:mm")}`) : null, div({ class: "tribe-card-members" }, span({ class: "tribe-members-count" }, `${i18n.padMembersLabel || "Members"}: ${pad.members.length}`) - ), - (() => { - const btn = renderSpreadButton(pad.rootId, spreadInfo); - return btn ? div({ class: "card-spread-left" }, btn) : null; - })(), - div({ class: "visit-btn-centered" }, - a({ href: `/pads/${encodeURIComponent(pad.rootId)}`, class: "filter-btn" }, i18n.padVisitPad || "Visit Pad") ) ) ) @@ -139,9 +141,9 @@ const renderCreateForm = (padToEdit, params) => { }, tribeId ? input({ type: "hidden", name: "tribeId", value: tribeId }) : null, span(i18n.padTitleLabel || "Title"), require("../server/node_modules/hyperaxe").br(), - input({ type: "text", name: "title", value: padToEdit ? padToEdit.title : "", placeholder: i18n.padTitlePlaceholder || "Enter pad title...", required: true }), + input({ type: "text", name: "title", maxlength: "100", value: padToEdit ? padToEdit.title : "", placeholder: i18n.padTitlePlaceholder || "Enter pad title...", required: true }), require("../server/node_modules/hyperaxe").br(), require("../server/node_modules/hyperaxe").br(), - span(i18n.padStatusLabel || "Status"), require("../server/node_modules/hyperaxe").br(), + span(i18n.padTypeLabel), require("../server/node_modules/hyperaxe").br(), select({ name: "status" }, ["OPEN", "INVITE-ONLY"].map(s => option({ value: s, ...(padToEdit && padToEdit.status === s ? { selected: true } : {}) }, s) @@ -157,7 +159,7 @@ const renderCreateForm = (padToEdit, params) => { }), require("../server/node_modules/hyperaxe").br(), require("../server/node_modules/hyperaxe").br(), span(i18n.padTagsLabel || "Tags"), require("../server/node_modules/hyperaxe").br(), - input({ type: "text", name: "tags", value: padToEdit ? padToEdit.tags.join(", ") : "", placeholder: i18n.padTagsPlaceholder || "tag1, tag2, ..." }), + input({ type: "text", name: "tags", value: padToEdit ? padToEdit.tags.join(", ") : "", placeholder: i18n.padTagsPlaceholder || "Enter tags separated by commas" }), require("../server/node_modules/hyperaxe").br(), require("../server/node_modules/hyperaxe").br(), button({ type: "submit", class: "create-button" }, padToEdit ? (i18n.padUpdate || "Update Pad") : (i18n.padCreate || "Create Pad")) ) @@ -178,33 +180,28 @@ exports.renderPadInvitePage = (code) => { exports.padsView = async (pads, filter, padToEdit, params) => { const q = String((params && params.q) || "").trim() const isForm = filter === "create" || filter === "edit" - const headerMap = { - all: i18n.padAllSectionTitle || "Pads", - mine: i18n.padMineSectionTitle || "Your Pads", - recent: i18n.padRecentSectionTitle || "Recent Pads", - open: i18n.padOpenSectionTitle || "Open Pads", - closed: i18n.padClosedSectionTitle || "Closed Pads" - } - const headerText = headerMap[filter] || headerMap.all + const headerText = i18n.padsTitle + const emptyMod = moduleIsEmpty(Array.isArray(pads) ? pads : [], filter || "all", "all", q) const filteredPads = q ? pads.filter(pd => String(pd.title || "").toLowerCase().includes(q.toLowerCase())) : pads - const body = div({ class: "main-column" }, - div({ class: "tags-header" }, + const body = section( + div({ class: "tags-header module-header-line" }, h2(headerText), p(i18n.padsDescription || "Manage collaborative encrypted text editors in your network.") ), - renderModeButtons(filter), - !isForm - ? div({ class: "filters" }, - form({ method: "GET", action: "/pads" }, + renderModeButtons(filter, emptyMod, (params && params.modesAvail) || null), + !isForm && !emptyMod + ? div({ class: "filters activity-filter-chips activity-toolbar-row" }, + renderModuleStatsBy(filteredPads, pd => pd.isClosed ? 'CLOSED' : String(pd.status || 'OPEN').toUpperCase(), [{ value: 'OPEN', label: i18n.padStatusOpen }, { value: 'INVITE-ONLY', label: i18n.padStatusInviteOnly }, { value: 'CLOSED', label: i18n.padStatusClosed }]), + form({ method: "GET", action: "/pads", class: "filter-box" }, input({ type: "hidden", name: "filter", value: filter }), - input({ type: "text", name: "q", placeholder: i18n.padSearchPlaceholder || "Search pads...", value: q }), - br(), - button({ type: "submit" }, i18n.search), - br() + input({ type: "text", name: "q", placeholder: i18n.padSearchPlaceholder || "Search pads...", value: q, class: "filter-box__input" }), + div({ class: "filter-box__controls" }, + button({ type: "submit", class: "filter-box__button" }, i18n.searchButton) + ) ) ) : null, @@ -213,7 +210,7 @@ exports.padsView = async (pads, filter, padToEdit, params) => { : div( filteredPads.length === 0 ? p({ class: "no-content" }, i18n.padsNoItems || "No pads found.") - : div({ class: "tribe-grid" }, ...filteredPads.map(pd => renderPadCard(pd, filter, params && params.spreadMap && params.spreadMap.get(pd.rootId)))) + : div({ class: "tribe-grid pads-grid" }, ...paged(filteredPads).map(pd => renderPadCard(pd, filter, params && params.spreadMap && params.spreadMap.get(pd.rootId)))) ) ) @@ -225,99 +222,122 @@ exports.singlePadView = async (pad, entries, params) => { const isMember = pad.members.includes(userId) || (!!pad.tribeId && !!pad.isTribeMember) const padClosed = pad.isClosed const returnTo = `/pads/${encodeURIComponent(pad.rootId)}` - const shareUrl = `/pads/${encodeURIComponent(pad.rootId)}` const isRestrictedInviteOnly = !isMember && !isAuthor && pad.status === "INVITE-ONLY" const tags = !isRestrictedInviteOnly && Array.isArray(pad.tags) && pad.tags.length > 0 - ? div({ class: "tribe-side-tags" }, ...pad.tags.map(t => a({ href: `/search?query=%23${encodeURIComponent(t)}` }, `#${t}`))) + ? div({ class: "tribe-side-tags" }, ...pad.tags.map(t => a({ href: `/search?query=%23${encodeURIComponent(t)}`, class: "tag-link" }, `#${t}`))) : null + const sharesPad = isAuthor || (pad.members || []).includes(userId) + const subscriptionIn = isAuthor || (pad.subscription && pad.subscription.subscribed === true) const detailChips = [ renderPadStatusChip(pad.status, padClosed), renderEncryptedChip(i18n), - renderLifespanChip(pad.lifetime, i18n) + renderLifespanChip(pad.lifetime, i18n), + sharesPad + ? renderStateChip(subscriptionIn ? "mutuals" : "closed", "✉", subscriptionIn ? i18n.subscriptionOn : i18n.subscriptionOff) + : null ].filter(Boolean) + const inviteActions = [ + isAuthor && pad.status === "INVITE-ONLY" + ? form({ method: "POST", action: `/pads/generate-invite/${encodeURIComponent(pad.rootId)}` }, + button({ type: "submit", class: "tribe-action-btn" }, i18n.tribeGenerateInvite) + ) + : null, + (() => { + if (!(isAuthor && pad.status === "INVITE-ONLY")) return null + const invs = Array.isArray(pad.invites) ? pad.invites : [] + const openInvite = invs.find(inv => typeof inv === "object" && inv && inv.public === true && inv.code) + if (openInvite) return [ + div({ class: "tribe-open-invite" }, + span({ class: "card-label" }, i18n.tribeInviteCodeText), + span({ class: "tribe-open-invite-code" }, openInvite.code), + renderInviteQrCard({ qrDataUrl: `/qr-invite-code/pads/${encodeURIComponent(openInvite.code)}` }) + ), + form({ method: "POST", action: `/pads/open-invite/remove/${encodeURIComponent(pad.rootId)}` }, + button({ type: "submit", class: "tribe-action-btn danger-btn" }, i18n.tribeRemoveInvitation) + ) + ] + return form({ method: "POST", action: `/pads/open-invite/create/${encodeURIComponent(pad.rootId)}` }, + button({ type: "submit", class: "tribe-action-btn" }, i18n.tribeOpenInvitation) + ) + })() + ].filter(Boolean) + + const topButtons = isRestrictedInviteOnly ? [] : [ + !isAuthor && !isMember && pad.status === "OPEN" && !padClosed + ? form({ method: "POST", action: `/pads/join/${encodeURIComponent(pad.rootId)}` }, + button({ type: "submit", class: "update-btn" }, i18n.padStartEditing || "EDIT") + ) + : null, + form({ method: "GET", action: `/pads/${encodeURIComponent(pad.rootId)}/pdf` }, + button({ type: "submit", class: "update-btn" }, i18n.generatePdf || "Generate PDF") + ), + isAuthor && pad.status !== "CLOSED" && !padClosed + ? form({ method: "POST", action: `/pads/close/${encodeURIComponent(pad.rootId)}` }, + button({ type: "submit", class: "update-btn" }, i18n.padClose || "Close") + ) + : null, + isAuthor + ? form({ method: "GET", action: "/pads" }, + input({ type: "hidden", name: "filter", value: "edit" }), + input({ type: "hidden", name: "id", value: pad.rootId }), + button({ type: "submit", class: "update-btn" }, i18n.padUpdate || "Update") + ) + : null + ].filter(Boolean) + const padSide = div({ class: "tribe-side" }, + div({ class: "tribe-side-actions wiki-actions-top pad-actions-top" }, + span({ class: "wiki-actions-left" }, + div({ class: "card-header activity-card-header" }, + renderContentActions(pad.rootId, returnTo, { + author: pad.author, + favKind: 'pads', + isFavorite: pad.isFavorite, + spread: (params && params.spreads) || null, + returnTo, + reportTitle: pad.title, + deleteAction: isAuthor ? contentDeleteAction('pad', pad.rootId) : undefined + }) + ) + ), + ...topButtons + ), div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, pad.title || "\u2014") ), detailChips.length ? div({ class: "card-chips-row" }, ...detailChips) : null, - div({ class: "shop-share" }, - span({ class: "tribe-info-label" }, i18n.padShareUrl || "Share URL"), - input({ type: "text", readonly: true, value: shareUrl, class: "shop-share-input" }) - ), - (() => { - const btn = renderSpreadButton(pad.rootId); - return btn ? div({ class: "card-spread-centered" }, btn) : null; - })(), - div({ class: "tribe-card-members" }, - span({ class: "tribe-members-count" }, `${i18n.padMembersLabel || "Members"}: ${pad.members.length}`) - ), - table({ class: "tribe-info-table" }, - tr(td({ class: "tribe-info-label" }, i18n.padCreated || "Created"), td({ class: "tribe-info-value", colspan: "3" }, moment(pad.createdAt).format("YYYY-MM-DD"))), - isRestrictedInviteOnly ? null : tr(td({ class: "tribe-info-value pad-author-cell", colspan: "4" }, userLink(pad.author))), - isRestrictedInviteOnly ? null : tr(td({ class: "tribe-info-label" }, i18n.padDeadlineLabel || "Deadline"), td({ class: "tribe-info-value", colspan: "3" }, pad.deadline ? moment(pad.deadline).format("YYYY-MM-DD HH:mm") : "\u2014")) - ), - isRestrictedInviteOnly ? null : div({ class: "tribe-side-actions" }, - isAuthor && pad.status === "INVITE-ONLY" - ? form({ method: "POST", action: `/pads/generate-invite/${encodeURIComponent(pad.rootId)}` }, - button({ type: "submit", class: "tribe-action-btn" }, i18n.tribeGenerateInvite) - ) - : null, - (() => { - if (!(isAuthor && pad.status === "INVITE-ONLY")) return null - const invs = Array.isArray(pad.invites) ? pad.invites : [] - const openInvite = invs.find(inv => typeof inv === "object" && inv && inv.public === true && inv.code) - if (openInvite) return [ - div({ class: "tribe-open-invite" }, - span({ class: "card-label" }, i18n.tribeInviteCodeText), - span({ class: "tribe-open-invite-code" }, openInvite.code) - ), - form({ method: "POST", action: `/pads/open-invite/remove/${encodeURIComponent(pad.rootId)}` }, - button({ type: "submit", class: "tribe-action-btn danger-btn" }, i18n.tribeRemoveInvitation) - ) - ] - return form({ method: "POST", action: `/pads/open-invite/create/${encodeURIComponent(pad.rootId)}` }, - button({ type: "submit", class: "tribe-action-btn" }, i18n.tribeOpenInvitation) - ) - })(), - form( - { method: "POST", action: pad.isFavorite ? `/pads/favorites/remove/${encodeURIComponent(pad.key)}` : `/pads/favorites/add/${encodeURIComponent(pad.key)}` }, - returnTo ? input({ type: "hidden", name: "returnTo", value: returnTo }) : null, - button({ type: "submit", class: "tribe-action-btn" }, pad.isFavorite ? (i18n.padRemoveFavorite || "Remove Favorite") : (i18n.padAddFavorite || "Add Favorite")) - ), - !isAuthor - ? a({ href: `/pm?to=${encodeURIComponent(pad.author)}`, class: "tribe-action-btn" }, "PM") - : null, - isAuthor - ? form({ method: "GET", action: "/pads" }, - input({ type: "hidden", name: "filter", value: "edit" }), - input({ type: "hidden", name: "id", value: pad.rootId }), - button({ type: "submit", class: "tribe-action-btn" }, i18n.padUpdate || "Update") - ) - : null, - isAuthor && pad.status !== "CLOSED" && !padClosed - ? form({ method: "POST", action: `/pads/close/${encodeURIComponent(pad.rootId)}` }, - button({ type: "submit", class: "tribe-action-btn" }, i18n.padClose || "Close Pad") - ) - : null, - isAuthor - ? form({ method: "POST", action: `/pads/delete/${encodeURIComponent(pad.rootId)}` }, - button({ type: "submit", class: "tribe-action-btn danger-btn" }, i18n.padDelete || "Delete") - ) - : null + div({ class: "pad-meta" }, + span({ class: "card-label" }, `${i18n.padCreated || "Created"}: `), + span(moment(pad.createdAt).format("YYYY/MM/DD HH:mm")), + (isRestrictedInviteOnly || !pad.deadline) ? null : span({ class: "card-label" }, `${i18n.padDeadlineLabel || "Deadline"}: `), + (isRestrictedInviteOnly || !pad.deadline) ? null : span(moment(pad.deadline).format("YYYY/MM/DD HH:mm")), + isRestrictedInviteOnly ? null : userLink(pad.author) ), + tags, + (isRestrictedInviteOnly || !inviteActions.length) ? null : div({ class: "tribe-side-actions" }, ...inviteActions), + (pad.subscription && sharesPad) + ? renderSubscriptionBox({ + target: pad.rootId || pad.key, + scope: "pads", + subscribed: pad.subscription.subscribed === true, + count: pad.subscription.count, + isOwner: isAuthor, + returnTo + }) + : null, !isAuthor && pad.status === "INVITE-ONLY" && !isMember ? div({ class: "pad-invite-section" }, - a({ class: "tribe-action-btn", href: "/invites#invites-pads" }, i18n.tribeEnterInvite) + a({ class: "tribe-action-btn", href: "/invites?filter=others" }, i18n.tribeEnterInvite) ) : null, - !isRestrictedInviteOnly && !isAuthor && !isMember && pad.status === "OPEN" && !padClosed - ? form({ method: "POST", action: `/pads/join/${encodeURIComponent(pad.rootId)}` }, - button({ type: "submit", class: "create-button" }, i18n.padStartEditing || "START EDITING!") - ) - : null, - tags + details({ class: "pad-members-details", ...(pad.members.length <= 8 ? { open: true } : {}) }, + summary({ class: "tribe-members-count" }, `${i18n.padMembersLabel || "Members"}: ${pad.members.length}`), + div({ class: "pad-members-chips" }, + ...pad.members.map(m => span({ class: "pad-member-chip" }, span({ class: "pad-author-swatch " + memberColorClass(pad.members, m) }), userLink(m))) + ) + ) ) let canonicalEntries = entries @@ -329,49 +349,83 @@ exports.singlePadView = async (pad, entries, params) => { const lastEntry = canonicalEntries.length > 0 ? canonicalEntries[canonicalEntries.length - 1] : null const currentText = lastEntry ? lastEntry.text : "" - const coloredView = chunks.length > 0 - ? div({ class: "pad-readonly-colored" }, - ...chunks.map(c => - span({ class: "pad-author-span " + memberColorClass(pad.members, c.author) }, c.text) - ) + const linesOfChunks = (list) => { + const rows = [[]] + for (const c of list) { + const parts = String(c.text || "").split("\n") + parts.forEach((part, i) => { + if (i > 0) rows.push([]) + if (part) rows[rows.length - 1].push({ text: part, author: c.author }) + }) + } + return rows + } + const renderNumbered = (list, minRows = 0) => { + const rows = linesOfChunks(list) + while (rows.length < minRows) rows.push([]) + return table({ class: "pad-lines" }, + ...rows.map((segs, i) => tr({ id: `L${i + 1}` }, + td({ class: "pad-line-num" }, a({ href: `#L${i + 1}` }, String(i + 1))), + td({ class: "pad-line-text" }, segs.length + ? segs.map(sg => span({ class: "pad-author-span " + memberColorClass(pad.members, sg.author) }, sg.text)) + : (i === 0 && !list.length ? span({ class: "pad-line-placeholder" }, i18n.padNoEntries || "No entries yet.") : " ")) + )) + ) + } + const documentView = div({ class: "pad-document" }, renderNumbered(chunks, 24)) + const attributionView = chunks.length > 0 + ? details({ class: "pad-attribution" }, + summary(i18n.padAttributionView || "By author"), + div({ class: "pad-readonly-colored" }, renderNumbered(chunks)) ) - : p(i18n.padNoEntries || "No entries yet.") + : null + const lineCount = String(currentText || "").split("\n").length + const editorRows = Math.max(24, lineCount + 12) + const gutter = div({ class: "pad-gutter" }, Array.from({ length: editorRows }, (_, i) => String(i + 1)).join("\n")) const visibleEntries = entries.filter(e => e.text && String(e.text).trim()) - const versionList = visibleEntries.length > 0 - ? div({ class: "pad-version-list" }, + const selectedKey = params.selectedVersion ? params.selectedVersion.key : null + const historyPanel = visibleEntries.length > 0 + ? div({ class: "pad-history" }, h4(i18n.padVersionHistory || "Version History"), ...visibleEntries.slice().reverse().map((e, idx) => - div({ class: "pad-version-item" }, - span({ class: "pad-version-date" }, moment(e.createdAt).format("YYYY-MM-DD HH:mm")), - span({ class: "pad-version-author" }, - span({ class: "pad-author-swatch " + memberColorClass(pad.members, e.author) }), - userLink(e.author) - ), - a({ href: `/pads/${encodeURIComponent(pad.rootId)}?version=${encodeURIComponent(e.key || idx)}`, class: "pad-version-link" }, i18n.padVersionView || "View") + div({ class: "pad-history-item" }, + a({ href: `/pads/${encodeURIComponent(pad.rootId)}?version=${encodeURIComponent(e.key || idx)}`, class: "pad-version-link" + (selectedKey && e.key === selectedKey ? " active" : ""), title: i18n.padVersionView || "View" }, moment(e.createdAt).format("YYYY/MM/DD HH:mm")), + span({ class: "pad-author-swatch " + memberColorClass(pad.members, e.author) }), + userLink(e.author) ) ) ) : null + const sidePanels = [historyPanel].filter(Boolean) const editorArea = isMember && !padClosed && !params.selectedVersion - ? div({ class: "pad-editor-area" }, - coloredView, - form({ method: "POST", action: `/pads/entry/${encodeURIComponent(pad.rootId)}` }, - textarea({ name: "text", rows: "12", class: "pad-editor-white", placeholder: i18n.padEditorPlaceholder || "Start writing..." }, currentText), - button({ type: "submit", class: "create-button" }, i18n.padSubmitEntry || "Submit") - ), - versionList ? div({ class: "pad-version-section" }, versionList) : null - ) - : div({ class: "pad-editor-area" }, - params.selectedVersion - ? div({ class: "pad-viewer-back" }, - a({ href: `/pads/${encodeURIComponent(pad.rootId)}`, class: "filter-btn" }, - "\u2190 " + (i18n.padBackToEditor || "Back to editor")) + ? div({ class: "pad-workspace" }, + div({ class: "pad-workspace-main" }, + form({ method: "POST", action: `/pads/entry/${encodeURIComponent(pad.rootId)}` }, + div({ class: "pad-editor-wrap" }, + gutter, + textarea({ maxlength: "3000", name: "text", rows: String(editorRows), class: "pad-editor", placeholder: i18n.padEditorPlaceholder || "Start writing...", wrap: "off" }, currentText) + ), + div({ class: "pad-editor-bar" }, + button({ type: "submit", class: "create-button" }, i18n.padSubmitEntry || "Submit") ) - : null, - coloredView, - versionList ? div({ class: "pad-version-section" }, versionList) : null + ), + attributionView + ), + sidePanels.length ? div({ class: "pad-workspace-side" }, ...sidePanels) : null + ) + : div({ class: "pad-workspace" }, + div({ class: "pad-workspace-main" }, + params.selectedVersion + ? div({ class: "pad-viewer-back" }, + a({ href: `/pads/${encodeURIComponent(pad.rootId)}`, class: "filter-btn" }, + "\u2190 " + (i18n.padBackToEditor || "Back to editor")) + ) + : null, + documentView + ), + sidePanels.length ? div({ class: "pad-workspace-side" }, ...sidePanels) : null ) const padMain = isRestrictedInviteOnly @@ -381,12 +435,12 @@ exports.singlePadView = async (pad, entries, params) => { return template( pad.title || i18n.padsTitle || "Pad", section( - div({ class: "tags-header" }, + div({ class: "tags-header module-header-line" }, h2(i18n.padsTitle || "Pads"), p(i18n.padsDescription || "Manage collaborative encrypted text editors in your network.") ), - renderModeButtons("all") + renderModeButtons("all", false, (params && params.modesAvail) || null) ), - section(div({ class: "tribe-details" }, padSide, padMain)) + section(div({ class: "tribe-details pad-page" }, padSide, padMain)) ) } diff --git a/src/views/parliament_view.js b/src/views/parliament_view.js index 7a9a9b1..0558668 100644 --- a/src/views/parliament_view.js +++ b/src/views/parliament_view.js @@ -1,11 +1,159 @@ const { form, button, div, h2, p, section, input, label, br, a, span, table, thead, tbody, tr, th, td, textarea, select, option, ul, li, img } = require('../server/node_modules/hyperaxe'); +const RAW_HTML = Symbol.for('oasis.rawHtml'); const moment = require("../server/node_modules/moment"); -const { template, i18n, userLink} = require('./main_views'); +const { template, i18n, userLink, paged } = require('./main_views'); +const { renderStyledText } = require('../backend/renderStyledText'); + +const { seatPositions } = require('../models/parliament_model').hemicycle; + +const HEMI_W = 720; +const HEMI_H = 380; + +const hemiEsc = (s) => String(s == null ? '' : s).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); + +const blocClass = (key) => { + const m = /^opp(\d+)$/.exec(String(key || '')); + return m ? `hemi-bloc-opp${Number(m[1]) % 8}` : `hemi-bloc-${key}`; +}; + +const seatClass = (seat, mode) => { + const cls = ['hemi-seat']; + if (mode === 'houses') cls.push(seat.house ? `hemi-house-${seat.house}` : 'hemi-bloc-none'); + else cls.push(blocClass(seat.bloc)); + if (seat.leader) cls.push('hemi-leader'); + if (seat.inPower) cls.push('hemi-power'); + return cls.join(' '); +}; + +const thresholdMarker = (positions, count, cls, label) => { + if (!count || count <= 0 || count > positions.length) return null; + const p = positions[count - 1]; + const q = positions[Math.min(positions.length - 1, count)]; + const angle = (p.angle + q.angle) / 2; + const cx = HEMI_W / 2; + const cy = HEMI_H - 20; + const inner = 40; + const outer = Math.min(HEMI_W / 2 - 16, HEMI_H - 60) + 14; + return { + angle, cls, text: `${label}: ${count}`, + x1: cx + inner * Math.cos(angle), y1: cy - inner * Math.sin(angle), + x2: cx + outer * Math.cos(angle), y2: cy - outer * Math.sin(angle), + tx: cx + (outer + 12) * Math.cos(angle), ty: cy - (outer + 12) * Math.sin(angle), + anchor: angle > Math.PI / 2 + 0.2 ? 'end' : (angle < Math.PI / 2 - 0.2 ? 'start' : 'middle') + }; +}; + +const LABEL_LINE = 14; +const renderMarkers = (markers) => { + const list = markers.filter(Boolean); + const placed = []; + for (const m of list.slice().sort((a, b) => a.ty - b.ty)) { + let ty = m.ty; + while (placed.some(o => Math.abs(o.ty - ty) < LABEL_LINE && Math.abs(o.tx - m.tx) < 160)) ty += LABEL_LINE; + placed.push({ ...m, ty }); + } + const drawn = new Set(); + return placed.map(m => { + const key = `${m.x2.toFixed(1)},${m.y2.toFixed(1)}`; + const line = drawn.has(key) ? '' : ``; + drawn.add(key); + return `${line}${hemiEsc(m.text)}`; + }).join(''); +}; + +const hemicycleSvg = (data, mode) => { + const seats = Array.isArray(data.seats) ? data.seats : []; + const unit = Math.max(1, Number(data.seatUnit) || 1); + const shown = unit > 1 ? seats.filter((_, i) => i % unit === 0) : seats; + const positions = seatPositions(shown.length, { width: HEMI_W, height: HEMI_H }); + const circles = shown.map((seat, i) => { + const pos = positions[i]; + if (!pos) return ''; + const title = hemiEsc(seat.name ? `${seat.name} (${seat.id})` : seat.id); + const circle = `${title}`; + return unit > 1 ? circle : `${circle}`; + }).join(''); + const t = data.thresholds || {}; + const scale = (n) => Math.ceil(Number(n || 0) / unit); + const markers = mode === 'houses' ? '' : renderMarkers([ + thresholdMarker(positions, scale(t.quorum), 'hemi-threshold-quorum', i18n.parliamentSeatsQuorum), + thresholdMarker(positions, scale(t.pass), 'hemi-threshold-pass', i18n.parliamentSeatsPass) + ]); + return `${circles}${markers}`; +}; + +const pct = (n, total) => total > 0 ? `${((n / total) * 100).toFixed(1)}%` : '0%'; + +const blocLabel = (b) => { + if (b.kind === 'government') return `${i18n.parliamentSeatsGovernment} · ${b.label}`; + if (b.kind === 'opposition') return `${i18n.parliamentSeatsOpposition} · ${i18n.parliamentSeatsVotesFor} ${b.label}`; + return i18n.parliamentSeatsAbstention; +}; + +const legendElection = (data) => ul({ class: 'hemi-legend' }, + ...(data.blocs || []).map(b => li({ class: 'hemi-legend-item' }, + span({ class: `hemi-swatch ${blocClass(b.key)}` }), + b.href ? a({ href: b.href, class: 'hemi-legend-link' }, blocLabel(b)) : span(blocLabel(b)), + span({ class: 'hemi-legend-count' }, `${b.count} ${i18n.parliamentSeatsSeats} · ${pct(b.count, data.population)}`) + )) +); + +const legendHouses = (data, houseNames = {}) => { + const entries = Object.entries(data.houses || {}).sort((a, b) => b[1] - a[1]); + const noHouse = data.population - entries.reduce((s, [, n]) => s + n, 0); + return ul({ class: 'hemi-legend' }, + ...entries.map(([key, n]) => li({ class: 'hemi-legend-item' }, + span({ class: `hemi-swatch hemi-house-${key}` }), + a({ href: `/larp/${encodeURIComponent(key)}`, class: 'hemi-legend-link' }, houseNames[key] || key), + span({ class: 'hemi-legend-count' }, `${n} ${i18n.parliamentSeatsSeats} · ${pct(n, data.population)}`) + )), + noHouse > 0 ? li({ class: 'hemi-legend-item' }, span({ class: 'hemi-swatch hemi-bloc-none' }), span(i18n.parliamentSeatsNoHouse), span({ class: 'hemi-legend-count' }, `${noHouse} ${i18n.parliamentSeatsSeats} · ${pct(noHouse, data.population)}`)) : null + ); +}; + +const renderHemicycle = (data, { mode = 'election', baseHref = '/parliament?filter=government', houseNames = {}, showHouses = true } = {}) => { + if (!data || !Array.isArray(data.seats) || !data.seats.length) return null; + const seatsMode = mode === 'houses' ? 'houses' : 'election'; + const join = baseHref.includes('?') ? '&' : '?'; + const toggle = showHouses ? div({ class: 'filters hemi-modes' }, + a({ href: baseHref, class: seatsMode === 'election' ? 'filter-btn active' : 'filter-btn' }, i18n.parliamentSeatsByElection), + a({ href: `${baseHref}${join}seats=houses`, class: seatsMode === 'houses' ? 'filter-btn active' : 'filter-btn' }, i18n.parliamentSeatsByHouses) + ) : null; + const unit = Math.max(1, Number(data.seatUnit) || 1); + return div({ class: 'hemicycle' }, + toggle, + (() => { + const svg = hemicycleSvg(data, seatsMode); + return div({ class: 'hemicycle-zoom-wrap' }, + div({ class: 'hemicycle-canvas' }, + div({ [RAW_HTML]: svg }), + a({ href: '#hemi-zoom', id: 'hemi-zoom-src', class: 'hemi-zoom-link', title: i18n.bankChartZoomHint }) + ), + div({ id: 'hemi-zoom', class: 'lightbox bank-eco-chart-lightbox' }, + a({ href: '#hemi-zoom-src', class: 'lightbox-close' }, '\u00d7'), + div({ class: 'bank-eco-chart-canvas-zoom hemi-zoom-canvas', [RAW_HTML]: svg }) + ) + ); + })(), + unit > 1 ? p({ class: 'hemi-note' }, `1 ${i18n.parliamentSeatsSeat} = ${unit}`) : null, + seatsMode === 'houses' ? legendHouses(data, houseNames) : legendElection(data), + data.leaderId ? p({ class: 'hemi-note' }, `${i18n.parliamentSeatsLeader}: `, a({ href: `/author/${encodeURIComponent(data.leaderId)}`, class: 'hemi-legend-link' }, (data.seats.find(s => s.id === data.leaderId) || {}).name || data.leaderId)) : null + ); +}; + + +const campaignBlock = (pItem) => pItem && pItem.campaignId + ? [ + div({ class: 'card-field' }, span({ class: 'card-label' }, String(i18n.campaignSignaturesLabel || 'Signatures').toUpperCase() + ': '), span({ class: 'card-value' }, `${Number(pItem.signatures) || 0}${Number(pItem.goal) > 0 ? ` / ${Number(pItem.goal)}` : ''}`)), + div({ class: 'doc-export-actions doc-export-left' }, form({ method: 'GET', action: `/campaigns/${encodeURIComponent(pItem.campaignId)}/pdf` }, button({ type: 'submit', class: 'filter-btn' }, i18n.parliamentViewReport))) + ] + : []; const TERM_DAYS = 60; -const fmt = (d) => moment(d).format('YYYY-MM-DD HH:mm:ss'); +const fmt = (d) => d ? moment(d).format('YYYY-MM-DD HH:mm:ss') : '—'; const timeLeft = (end) => { + if (!end) return '—'; const diff = moment(end).diff(moment()); if (diff <= 0) return '0d 00:00:00'; const dur = moment.duration(diff); @@ -28,26 +176,6 @@ const showVoteMetrics = (method) => { }; const applyEl = (fn, attrs, kids) => fn.apply(null, [attrs || {}].concat(kids || [])); const methodImageSrc = (method) => `assets/images/${String(method || '').toUpperCase().toLowerCase()}.png`; -const MethodBadge = (method) => { - const m = String(method || '').toUpperCase(); - const labelTxt = String(i18n[`parliamentMethod${m}`] || m).toUpperCase(); - return span( - { class: 'method-badge' }, - labelTxt, - br(),br(), - img({ src: methodImageSrc(m), alt: labelTxt, class: 'method-badge__icon' }) - ); -}; -const MethodHero = (method) => { - const m = String(method || '').toUpperCase(); - const labelTxt = String(i18n[`parliamentMethod${m}`] || m).toUpperCase(); - return span( - { class: 'method-hero' }, - labelTxt, - br(),br(), - img({ src: methodImageSrc(m), alt: labelTxt, class: 'method-hero__icon' }) - ); -}; const KPI = (labelTxt, value) => div({ class: 'kpi' }, span({ class: 'kpi__label' }, labelTxt), @@ -63,20 +191,22 @@ const CycleInfo = (start, end, labels = { KPI((labels.end + ': ').toUpperCase(), fmt(end)), KPI((labels.remaining + ': ').toUpperCase(), timeLeft(end)) ); -const Tabs = (active) => +const Tabs = (active, avail = null) => div( { class: 'filters' }, form( { method: 'GET', action: '/parliament' }, - ['government', 'candidatures', 'proposals', 'laws', 'revocations', 'historical', 'leaders', 'rules'].map(f => + ['government', 'candidatures', 'proposals', 'laws', 'revocations', 'historical', 'leaders', 'rules'] + .filter(f => !avail || f === 'government' || f === 'rules' || f === active || avail[f]) + .map(f => button({ type: 'submit', name: 'filter', value: f, class: active === f ? 'filter-btn active' : 'filter-btn' }, i18n[`parliamentFilter${f.charAt(0).toUpperCase()+f.slice(1)}`]) ) ) ); const GovHeader = (g) => { - const termStart = g && g.since ? g.since : moment().toISOString(); - const termEnd = g && g.end ? g.end : moment(termStart).add(TERM_DAYS, 'days').toISOString(); + const termStart = g && g.since ? g.since : null; + const termEnd = g && g.end ? g.end : null; const methodKeyRaw = g && g.method ? String(g.method) : 'ANARCHY'; const methodKey = methodKeyRaw.toUpperCase(); const i18nMeth = i18n[`parliamentMethod${methodKey}`]; @@ -117,9 +247,9 @@ const GovHeader = (g) => { ); }; -const GovernmentCard = (g, meta) => { - const termStart = g && g.since ? g.since : moment().toISOString(); - const termEnd = g && g.end ? g.end : moment(termStart).add(TERM_DAYS, 'days').toISOString(); +const GovernmentCard = (g, meta, hemicycle = null, seatsMode = 'election', houseNames = {}) => { + const termStart = g && g.since ? g.since : null; + const termEnd = g && g.end ? g.end : null; const actorLabel = g.powerType === 'tribe' ? (i18n.parliamentActorInPowerTribe || i18n.parliamentActorInPower || 'TRIBE RULING') @@ -153,7 +283,9 @@ const GovernmentCard = (g, meta) => { { class: 'card' }, h2(i18n.parliamentGovernmentCard), GovHeader(g), - div({ class: 'method-image-centered' }, img({ src: methodImageSrc(methodKey), alt: methodLabel })), + hemicycle + ? renderHemicycle(hemicycle, { mode: seatsMode, baseHref: '/parliament?filter=government', houseNames }) + : div({ class: 'method-image-centered' }, img({ src: methodImageSrc(methodKey), alt: methodLabel })), div( { class: 'table-wrap' }, applyEl(table, { class: 'table table--centered gov-overview' }, [ @@ -263,7 +395,7 @@ const CandidatureStats = (cands, govCard, leaderMeta, electionQuorum = 2) => { h2(i18n.parliamentElectionsStatusTitle), div({ class: 'card-field card-field--spaced' }, span({ class: 'card-label' }, winLbl + ': '), - span({ class: 'card-value' }, hasQuorum ? idLink : span({ style: 'color:#ffcc00;font-weight:bold;' }, i18n.voteNoQuorum || 'NO QUORUM')) + span({ class: 'card-value' }, hasQuorum ? idLink : span({ class: 'vote-no-quorum' }, i18n.voteNoQuorum || 'NO QUORUM')) ), hasQuorum ? div({ class: 'card-field card-field--spaced' }, @@ -274,14 +406,14 @@ const CandidatureStats = (cands, govCard, leaderMeta, electionQuorum = 2) => { hasQuorum ? div( { class: 'table-wrap mt-2' }, - applyEl(table, { class: 'parliament-gov-table' }, [ + applyEl(table, { class: 'parliament-gov-table parliament-rows-table' }, [ thead(tr( th(i18n.parliamentThLeader), th({ class: 'parliament-method-col' }, i18n.parliamentGovMethod), th({ class: 'parliament-votes-col' }, i18n.parliamentVotesReceived) )), tbody(tr( - td( + td({ 'data-label': i18n.parliamentThLeader }, div({ class: 'parliament-avatar-cell' }, avatarHref ? a({ href: avatarHref }, img({ src: avatarSrc, class: 'parliament-leader-avatar' })) @@ -289,10 +421,10 @@ const CandidatureStats = (cands, govCard, leaderMeta, electionQuorum = 2) => { idLink ? div({ class: 'parliament-avatar-name' }, idLink) : null ) ), - td({ class: 'parliament-method-col' }, + td({ class: 'parliament-method-col', 'data-label': i18n.parliamentGovMethod }, img({ src: methodImageSrc(methodKey), alt: methodLabel, class: 'parliament-method-img' }) ), - td({ class: 'parliament-votes-col' }, span({ class: 'votes-value' }, votes)) + td({ class: 'parliament-votes-col', 'data-label': i18n.parliamentVotesReceived }, span({ class: 'votes-value' }, votes)) )) ]) ) @@ -306,23 +438,19 @@ const CandidaturesTable = (candidatures) => { c.targetType === 'inhabitant' ? p(userLink(c.targetId)) : p(a({ class: 'tag-link', href: `/tribe/${encodeURIComponent(c.targetId)}?` }, c.targetTitle || c.targetId)); - const voters = Array.isArray(c.voters) ? c.voters : []; return tr( - td(idLink), - td(fmt(c.createdAt)), - td({ class: 'nowrap' }, c.method), - td(c.targetType === 'inhabitant' ? String(c.karma || 0) : '-'), - td( - span({ class: 'candidature-votes-count' }, String(c.votes || 0)), - voters.length ? div({ class: 'candidature-voters' }, ...voters.map(v => div({ class: 'candidature-voter' }, userLink(v)))) : null - ), - td(form({ method: 'POST', action: `/parliament/candidatures/${encodeURIComponent(c.id)}/vote` }, button({ class: 'vote-btn' }, i18n.parliamentVoteBtn))) + td({ 'data-label': i18n.parliamentThId }, idLink), + td({ 'data-label': i18n.parliamentThProposalDate }, fmt(c.createdAt)), + td({ class: 'nowrap', 'data-label': i18n.parliamentThMethod }, c.method), + td({ 'data-label': i18n.parliamentThKarma }, c.targetType === 'inhabitant' ? String(c.karma || 0) : '-'), + td({ 'data-label': i18n.parliamentThSupports }, span({ class: 'candidature-votes-count' }, String(c.votes || 0))), + td({ 'data-label': '' }, form({ method: 'POST', action: `/parliament/candidatures/${encodeURIComponent(c.id)}/vote` }, button({ class: 'vote-btn' }, i18n.parliamentVoteBtn))) ); }); return div( { class: 'table-wrap' }, h2(i18n.parliamentCandidaturesListTitle), - applyEl(table, { class: 'table table--centered' }, [ + applyEl(table, { class: 'table table--centered parliament-rows-table' }, [ thead(tr( th(i18n.parliamentThId), th(i18n.parliamentThProposalDate), @@ -343,7 +471,7 @@ const ProposalForm = () => form( { method: 'POST', action: '/parliament/proposals/create' }, label(i18n.parliamentProposalTitle), br(), - input({ type: 'text', name: 'title', required: true }), br(), br(), + input({ type: 'text', name: 'title', maxlength: '100', required: true }), br(), br(), label(i18n.parliamentProposalDescription), br(), textarea({ name: 'description', rows: 5, maxlength: 1000 }), br(), br(), button({ type: 'submit', class: 'create-button' }, i18n.parliamentProposalPublish) @@ -377,11 +505,11 @@ const ProposalsList = (proposals) => { span({ class: 'card-label' }, i18n.parliamentGovMethod.toUpperCase() + ': '), span({ class: 'card-value' }, pItem.method) ), - br(), div( h2(titleNode), - p(pItem.description || '') + p(...renderStyledText(pItem.description || '')) ), + ...campaignBlock(pItem), pItem.deadline ? div( { class: 'card-field' }, @@ -420,7 +548,7 @@ const ProposalsList = (proposals) => { pItem && pItem.voteId ? form( { method: 'GET', action: `/votes/${encodeURIComponent(pItem.voteId)}` }, - button({ type: 'submit', class: 'vote-btn' }, i18n.parliamentVoteAction) + button({ type: 'submit', class: 'buy-btn' }, i18n.parliamentVoteAction) ) : null ); @@ -442,7 +570,8 @@ const FutureLawsList = (rows) => { div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.parliamentThProposalDate.toUpperCase() + ': '), span({ class: 'card-value' }, fmt(pItem.createdAt))), div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.parliamentLawProposer.toUpperCase() + ': '), span({ class: 'card-value' }, userLink(pItem.proposer))), h2(pItem.title || ''), - p(pItem.description || '') + p(...renderStyledText(pItem.description || '')), + ...campaignBlock(pItem) ) ); return div( @@ -552,7 +681,7 @@ const RevocationsList = (revocations) => { pItem && pItem.voteId ? form( { method: 'GET', action: `/votes/${encodeURIComponent(pItem.voteId)}` }, - button({ type: 'submit', class: 'vote-btn' }, i18n.parliamentVoteAction) + button({ type: 'submit', class: 'buy-btn' }, i18n.parliamentVoteAction) ) : null ); @@ -593,7 +722,7 @@ const LawsStats = (laws = [], revocatedCount = 0) => { return div( { class: 'table-wrap' }, h2(i18n.parliamentPoliciesTitle || 'POLICIES'), - applyEl(table, { class: 'table table--centered' }, [ + applyEl(table, { class: 'table table--centered parliament-rows-table' }, [ thead(tr( th(i18n.parliamentThProposed), th(i18n.parliamentThApproved), @@ -603,11 +732,11 @@ const LawsStats = (laws = [], revocatedCount = 0) => { )), tbody( tr( - td(String(proposed)), - td(String(approved)), - td(String(declined)), - td(String(discarded)), - td(String(revocated)) + td({ 'data-label': i18n.parliamentThProposed }, String(proposed)), + td({ 'data-label': i18n.parliamentThApproved }, String(approved)), + td({ 'data-label': i18n.parliamentThDeclined }, String(declined)), + td({ 'data-label': i18n.parliamentThDiscarded }, String(discarded)), + td({ 'data-label': i18n.parliamentPoliciesRevocated }, String(revocated)) ) ) ]) @@ -656,19 +785,19 @@ const HistoricalGovsSummary = (rows = []) => { const entries = Array.from(byMethod.entries()).sort((a,b) => String(a[0]).localeCompare(String(b[0]))); const lines = entries.map(([method, rec]) => tr( - td(method), - td(String(rec.proposed)), - td(String(rec.approved)), - td(String(rec.declined)), - td(String(rec.discarded)), - td(String(rec.revocated)), - td(String(rec.cycles)) + td({ 'data-label': String(i18n.parliamentGovMethod).toUpperCase() }, method), + td({ 'data-label': i18n.parliamentPoliciesProposal }, String(rec.proposed)), + td({ 'data-label': i18n.parliamentPoliciesApproved }, String(rec.approved)), + td({ 'data-label': i18n.parliamentPoliciesDeclined }, String(rec.declined)), + td({ 'data-label': i18n.parliamentPoliciesDiscarded }, String(rec.discarded)), + td({ 'data-label': i18n.parliamentPoliciesRevocated }, String(rec.revocated)), + td({ 'data-label': i18n.parliamentThCycles }, String(rec.cycles)) ) ); return div( { class: 'table-wrap' }, h2(i18n.parliamentHistoricalGovernmentsTitle || 'Governments'), - applyEl(table, { class: 'table table--centered' }, [ + applyEl(table, { class: 'table table--centered parliament-rows-table' }, [ thead(tr( th(String(i18n.parliamentGovMethod).toUpperCase()), th(i18n.parliamentPoliciesProposal), @@ -781,7 +910,7 @@ const LeadersSummary = (leaders = [], candidatures = [], allGovCards = [], total return div( { class: 'table-wrap' }, h2(i18n.parliamentHistoricalLawsTitle || 'Actions'), - applyEl(table, { class: 'table table--centered' }, [ + applyEl(table, { class: 'table table--centered parliament-rows-table' }, [ thead(tr( th(i18n.parliamentThTotalCandidatures), th(i18n.parliamentThTimesInPower), @@ -794,14 +923,14 @@ const LeadersSummary = (leaders = [], candidatures = [], allGovCards = [], total )), tbody( tr( - td(String(presented)), - td(String(timesInPower)), - td(String(totals.proposed)), - td(String(totals.approved)), - td(String(totals.declined)), - td(String(totals.discarded)), - td(String(totals.revocated)), - td(`${efficiencyPct} %`) + td({ 'data-label': i18n.parliamentThTotalCandidatures }, String(presented)), + td({ 'data-label': i18n.parliamentThTimesInPower }, String(timesInPower)), + td({ 'data-label': i18n.parliamentThProposed }, String(totals.proposed)), + td({ 'data-label': i18n.parliamentThApproved }, String(totals.approved)), + td({ 'data-label': i18n.parliamentThDeclined }, String(totals.declined)), + td({ 'data-label': i18n.parliamentThDiscarded }, String(totals.discarded)), + td({ 'data-label': i18n.parliamentPoliciesRevocated }, String(totals.revocated)), + td({ 'data-label': i18n.parliamentEfficiency }, `${efficiencyPct} %`) ) ) ]) @@ -824,13 +953,13 @@ const LeadersList = (leaders, metas = {}, candidatures = []) => { : userLink(l.powerId, l.powerTitle); const leaderCell = div({ class: 'leader-cell parliament-avatar-cell' }, avatar, div({ class: 'parliament-avatar-name' }, link)); return tr( - td(leaderCell), - td(String(l.proposed || 0)), - td(String(l.approved || 0)), - td(String(l.declined || 0)), - td(String(l.discarded || 0)), - td(String(l.revocated || 0)), - td(`${(l.efficiency != null ? Math.round(l.efficiency * 100) : (l.proposed > 0 ? Math.round((l.approved / l.proposed) * 100) : 0))} %`) + td({ 'data-label': i18n.parliamentThLeader }, leaderCell), + td({ 'data-label': i18n.parliamentPoliciesProposal }, String(l.proposed || 0)), + td({ 'data-label': i18n.parliamentPoliciesApproved }, String(l.approved || 0)), + td({ 'data-label': i18n.parliamentPoliciesDeclined }, String(l.declined || 0)), + td({ 'data-label': i18n.parliamentPoliciesDiscarded }, String(l.discarded || 0)), + td({ 'data-label': i18n.parliamentPoliciesRevocated }, String(l.revocated || 0)), + td({ 'data-label': i18n.parliamentEfficiency }, `${(l.efficiency != null ? Math.round(l.efficiency * 100) : (l.proposed > 0 ? Math.round((l.approved / l.proposed) * 100) : 0))} %`) ); }); return div( @@ -855,6 +984,7 @@ const RulesContent = () => { const points = [ i18n.parliamentRulesIntro, i18n.parliamentRulesTerm, + i18n.parliamentRulesHemicycle, i18n.parliamentRulesMethods, i18n.parliamentRulesAnarchy, i18n.parliamentRulesCandidates, @@ -887,7 +1017,7 @@ const CandidaturesSection = (governmentCard, candidatures, leaderMeta, electionQ GovHeader(governmentCard || {}), CandidatureStats(candidatures || [], governmentCard || null, leaderMeta || null, electionQuorum), CandidatureForm(), - candidatures && candidatures.length ? CandidaturesTable(candidatures) : null + candidatures && candidatures.length ? CandidaturesTable(paged(candidatures)) : null ); }; @@ -896,7 +1026,7 @@ const ProposalsSection = (governmentCard, proposals, futureLaws, canPropose) => const fl = FutureLawsList(futureLaws || []); if (!has && canPropose) return div(h2(i18n.parliamentGovernmentCard), GovHeader(governmentCard || {}), ProposalForm(), fl); if (!has && !canPropose) return div(h2(i18n.parliamentGovernmentCard), GovHeader(governmentCard || {}), NoProposals(), fl); - return div(h2(i18n.parliamentGovernmentCard), GovHeader(governmentCard || {}), ProposalForm(), ProposalsList(proposals), fl); + return div(h2(i18n.parliamentGovernmentCard), GovHeader(governmentCard || {}), ProposalForm(), ProposalsList(paged(proposals)), fl); }; const RevocationsSection = (governmentCard, laws, revocations, futureRevocations) => @@ -904,7 +1034,7 @@ const RevocationsSection = (governmentCard, laws, revocations, futureRevocations h2(i18n.parliamentGovernmentCard), GovHeader(governmentCard || {}), RevocationForm(laws || []), - RevocationsList(revocations || []) || '', + RevocationsList(paged(revocations || [])) || '', FutureRevocationsList(futureRevocations || []) || '' ); @@ -929,6 +1059,9 @@ const parliamentView = async (state) => { leaders, leaderMeta, powerMeta, + hemicycle, + seatsMode, + houseNames, revocations, futureRevocations, revocationsEnactedCount, @@ -950,8 +1083,8 @@ const parliamentView = async (state) => { powerType: 'none', powerId: null, powerTitle: 'ANARCHY', - since: moment().toISOString(), - end: moment().add(TERM_DAYS, 'days').toISOString(), + since: null, + end: null, inhabitantsTotal: Number(inhabitantsTotal ?? 0) || 0 }; @@ -963,24 +1096,30 @@ const parliamentView = async (state) => { const LawsSectionWrap = () => div( LawsStats(laws || [], revocationsEnactedCount || 0), - LawsList(laws || []) + LawsList(paged(laws || [])) ); return template( i18n.parliamentTitle, - section(div({ class: 'tags-header' }, h2(i18n.parliamentTitle), p(i18n.parliamentDescription)), Tabs(filter)), + section(div({ class: 'tags-header module-header-line' }, h2(i18n.parliamentTitle), p(i18n.parliamentDescription)), Tabs(filter, { + candidatures: true, + proposals: true, + laws: (laws || []).length > 0 || (futureLaws || []).length > 0, + revocations: (revocations || []).length > 0 || (futureRevocations || []).length > 0, + historical: (historical || []).length > 0, + leaders: (leaders || []).length > 0 + })), section( - filter === 'government' ? GovernmentCard(gov, powerMeta) : null, + filter === 'government' ? GovernmentCard(gov, powerMeta, hemicycle || null, seatsMode || 'election', houseNames || {}) : null, filter === 'candidatures' ? CandidaturesSection(gov, candidatures, leaderMeta, electionQuorum) : null, filter === 'proposals' ? ProposalsSection(gov, proposals, futureLaws, canPropose) : null, filter === 'laws' ? LawsSectionWrap() : null, filter === 'revocations' ? RevocationsSection(gov, laws, revocations, futureRevocations) : null, - filter === 'historical' ? div(HistoricalGovsSummary(historical || []), HistoricalList(historical || [], historicalMetas)) : null, - filter === 'leaders' ? div(LeadersSummary(leaders || [], candidatures || [], leadersGovCards, leadersTotalCandidatures), LeadersList(leaders || [], leadersMetas, candidatures || [])) : null, + filter === 'historical' ? div(HistoricalGovsSummary(historical || []), HistoricalList(paged(historical || []), historicalMetas)) : null, + filter === 'leaders' ? div(LeadersSummary(leaders || [], candidatures || [], leadersGovCards, leadersTotalCandidatures), LeadersList(paged(leaders || []), leadersMetas, candidatures || [])) : null, filter === 'rules' ? RulesContent() : null ) ); }; -module.exports = { parliamentView, pickLeader }; - +module.exports = { parliamentView, pickLeader, renderHemicycle }; diff --git a/src/views/peers_view.js b/src/views/peers_view.js index 4f9e793..f2bd5b1 100644 --- a/src/views/peers_view.js +++ b/src/views/peers_view.js @@ -1,12 +1,35 @@ -const peersView = async ({ onlinePeers, discoveredPeers, unknownPeers, lanBroadcastActive = false, technicalPeers = [] }) => { - const { form, button, div, h2, p, section, a, hr, input, label, br, span, table, tr, td, textarea } = require("../server/node_modules/hyperaxe"); - const { template, i18n } = require('./main_views'); +const peersView = async ({ onlinePeers, discoveredPeers, unknownPeers, lanBroadcastActive = false, technicalPeers = [], versions = {}, staleKeys = [], onionKeys = [], ownVersion = '', lastErrors = {}, paused = false, filter = 'status', advanced = false, health = null, lan = null }) => { + const { form, button, div, h2, p, section, a, hr, input, label, br, span, table, tr, td, textarea, wbr } = require("../server/node_modules/hyperaxe"); + const { template, i18n, renderStateChip, renderModuleStats } = require('./main_views'); + const { NET_REASON_KEYS, canonicalKey } = require('../models/peer_health'); + const majorMinor = (v) => String(v || '').split('.').slice(0, 2).map(n => parseInt(n, 10) || 0); + const isOutdated = (v) => { + if (!v || !ownVersion) return false; + const [a1, b1] = majorMinor(v), [a2, b2] = majorMinor(ownVersion); + return a1 < a2 || (a1 === a2 && b1 < b2); + }; + const stale = new Set(staleKeys || []); + const outdatedKey = (k) => isOutdated((versions || {})[k]) || (!(versions || {})[k] && stale.has(k)); + const knowsVersion = (keys) => keys.some(k => (versions || {})[k] || stale.has(k)); + const unspecifiedHost = (h) => /^(0\.0\.0\.0|::|\[::\])$/.test(String(h || '')); + const versionCell = (k) => { + const v = (versions || {})[k]; + if (!v) return [stale.has(k) ? span({ title: i18n.peerVersionUnannounced }, renderStateChip('closed', null, String(i18n.peerOutdated).toUpperCase())) : '—']; + const old = isOutdated(v); + return [span({ title: old ? i18n.peerOutdated : '' }, renderStateChip(old ? 'closed' : 'mutuals', null, String(v)))]; + }; + const stateCell = (tp) => { + const st = String(tp.state || ''); + if (st === 'connected') return renderStateChip('mutuals', null, String(i18n.peerStateConnected).toUpperCase()); + if (st === 'connecting') return renderStateChip('whole', null, String(i18n.peerStateConnecting).toUpperCase()); + if (st === 'staged') return renderStateChip(outdatedKey(tp.key) ? 'whole' : 'mutuals', null, String(i18n.peerStateStaged).toUpperCase()); + const err = (lastErrors || {})[canonicalKey(tp.key)]; + return renderStateChip('hidden', null, String(i18n[err ? NET_REASON_KEYS[err.reason] : 'peerErrSilent'] || i18n.peerErrOther).toUpperCase()); + }; - const startButton = form({ action: "/settings/conn/start", method: "post" }, button({ type: "submit" }, i18n.startNetworking)); - const restartButton = form({ action: "/settings/conn/restart", method: "post" }, button({ type: "submit" }, i18n.restartNetworking)); - const stopButton = form({ action: "/settings/conn/stop", method: "post" }, button({ type: "submit" }, i18n.stopNetworking)); - const syncButton = form({ action: "/settings/conn/sync", method: "post" }, button({ type: "submit" }, i18n.sync)); - const connButtons = [startButton, restartButton, stopButton, syncButton]; + const pauseButton = paused + ? form({ action: "/peers/resume", method: "post" }, button({ type: "submit", class: "tribe-action-btn success-btn" }, String(i18n.peersResume || 'Resume').toUpperCase())) + : form({ action: "/peers/pause", method: "post" }, button({ type: "submit", class: "tribe-action-btn danger-btn" }, String(i18n.peersPause || 'Pause').toUpperCase())); const deduplicatePeers = (peers) => { const seen = new Set(); @@ -28,16 +51,28 @@ const peersView = async ({ onlinePeers, discoveredPeers, unknownPeers, lanBroadc return null; }; - const renderPeerRow = (peerData) => { + const shortHost = (h) => { + const s = String(h || ''); + const m = s.match(/^(?:onion:)?([a-z2-7]{16,56})\.onion(.*)$/i); + return m ? `${m[1].slice(0, 10)}….onion${m[2]}` : s; + }; + const onion = new Set(onionKeys || []); + const overTor = (peerData) => /^onion:/i.test(String(peerData[0] || '')) || /\.onion$/i.test(String((peerData[1] || {}).host || '')) || onion.has(canonicalKey((peerData[1] || {}).key)); + const renderPeerRow = (peerData, showVersion) => { const peer = peerData[1]; const { name, users, key } = peer; + const tor = overTor(peerData); const peerUrl = `/author/${encodeURIComponent(key)}`; const filteredUsers = (users || []).filter(u => u.id !== key); - const userCount = filteredUsers.length; + const userCount = Array.isArray(users) ? filteredUsers.length : null; return tr( - td({ 'data-label': i18n.peerHost || 'Pub' }, a({ href: peerUrl, class: "user-link" }, name || key.slice(0, 20) + '…')), - td({ 'data-label': 'Key' }, a({ href: peerUrl, class: 'user-link peer-key' }, key)), - td({ 'data-label': i18n.peersReplicatedFeeds || 'Feeds' }, String(userCount)) + td({ 'data-label': i18n.peerHost || 'Pub' }, unspecifiedHost(name) ? '—' : a({ href: peerUrl, class: "user-link", title: name || key }, name ? shortHost(name) : key.slice(0, 20) + '…')), + td({ 'data-label': i18n.peersOasisId || 'Oasis ID' }, a({ href: peerUrl, class: 'user-link peer-key' }, key)), + showVersion ? td({ 'data-label': i18n.peersOasisVersion || 'Version' }, ...versionCell(key)) : null, + td({ 'data-label': i18n.peersTorColumn }, tor + ? span({ class: 'ubi-tick-ok', title: i18n.peersTorYes }, '✓') + : span({ class: 'peer-tor-no', title: i18n.peersTorNo }, '—')), + td({ 'data-label': i18n.peersReplicatedFeeds || 'Replicated' }, userCount == null ? '—' : String(userCount)) ); }; @@ -51,28 +86,35 @@ const peersView = async ({ onlinePeers, discoveredPeers, unknownPeers, lanBroadc const renderPeerTable = (peers, emptyKey) => { if (peers.length === 0) return p(i18n[emptyKey] || i18n.noConnections); + const showVersion = knowsVersion(peers.map(pd => pd[1].key)); return table({ class: 'block-info-table' }, tr( td({ class: 'card-label' }, i18n.peerHost || 'Pub'), - td({ class: 'card-label' }, 'Key'), - td({ class: 'card-label' }, i18n.peersReplicatedFeeds || 'Replicated feeds') + td({ class: 'card-label' }, i18n.peersOasisId || 'Oasis ID'), + showVersion ? td({ class: 'card-label' }, i18n.peersOasisVersion || 'Version') : null, + td({ class: 'card-label' }, i18n.peersTorColumn), + td({ class: 'card-label' }, i18n.peersReplicatedFeeds || 'Replicated') ), - ...peers.map(renderPeerRow) + ...peers.map(pd => renderPeerRow(pd, showVersion)) ); }; + const techVersion = knowsVersion((technicalPeers || []).map(tp => tp.key)); const technicalRows = (technicalPeers || []).map(tp => { const k = tp.key || ''; const connected = tp.state === 'connected'; const action = connected ? 'disconnect' : 'connect'; const btnLabel = connected ? (i18n.peerDisconnect || 'Disconnect') : (i18n.peerConnect || 'Connect'); return tr( - td({ 'data-label': 'Key' }, a({ href: `/author/${encodeURIComponent(k)}`, class: 'user-link peer-key' }, k ? k.slice(0, 20) + '…' : '—')), - td({ 'data-label': 'Host' }, String(tp.host || '—')), - td({ 'data-label': i18n.peerPort || 'Port' }, String(tp.port || '—')), - td({ 'data-label': 'State' }, String(tp.state || tp.source || '—')), - td({ 'data-label': i18n.peerLastChange || 'Last change' }, String(tp.stateChange ? new Date(tp.stateChange).toISOString().slice(0, 16).replace('T', ' ') : '—')), - td({ 'data-label': '' }, + td({ 'data-label': i18n.peersOasisId }, a({ href: `/author/${encodeURIComponent(k)}`, class: 'user-link peer-key' }, k ? k.slice(0, 20) + '…' : '—')), + onion.has(canonicalKey(tp.key)) || /\.onion$/i.test(String(tp.host || '')) + ? td({ 'data-label': i18n.peerAddressLabel, class: 'peer-host' }, renderStateChip('fediverse', null, 'TOR')) + : td({ 'data-label': i18n.peerAddressLabel, title: unspecifiedHost(tp.host) ? '' : String(tp.host || ''), class: 'peer-host' }, tp.host && !unspecifiedHost(tp.host) ? shortHost(tp.host).split(/(?<=\.)/).reduce((acc, part, i) => i ? [...acc, wbr(), part] : [part], []) : '—'), + td({ 'data-label': i18n.peerPort, class: 'peer-port' }, String(tp.port || '—')), + techVersion ? td({ 'data-label': i18n.peersOasisVersion || 'Version' }, ...versionCell(k)) : null, + td({ 'data-label': i18n.peerStateLabel }, stateCell(tp)), + td({ 'data-label': i18n.peerLastChangeLabel }, String(tp.stateChange ? new Date(tp.stateChange).toISOString().slice(0, 16).replace('T', ' ') : '—')), + td( form({ method: "POST", action: `/peers/${action}`, class: "inline-form" }, input({ type: "hidden", name: "key", value: k }), input({ type: "hidden", name: "host", value: String(tp.host || '') }), @@ -82,31 +124,46 @@ const peersView = async ({ onlinePeers, discoveredPeers, unknownPeers, lanBroadc ) ); }); - const refreshButton = form({ action: "/peers/refresh", method: "post" }, button({ type: "submit" }, i18n.peerRefresh || 'Refresh')); - const pruneButton = form({ action: "/peers/prune", method: "post" }, button({ type: "submit" }, i18n.peerPruneIdle || 'Remove idle')); - const exportButton = form({ action: "/peers/export", method: "get" }, button({ type: "submit" }, i18n.peerExport || 'Export')); + const withCount = (label, n) => Number(n) > 0 ? `${label} (${n})` : label; + const peersToolbar = (tabName, total, segments) => { + if (!(Number(total) > 0)) return null; + const back = input({ type: "hidden", name: "returnTo", value: `/peers?filter=${tabName}` }); + return div({ class: 'filters activity-filter-chips activity-toolbar-row' }, + renderModuleStats(total, segments), + div({ class: "conn-actions peers-conn-actions filter-box" }, + form({ action: "/peers/refresh", method: "post" }, back, button({ type: "submit", class: "filter-btn" }, i18n.peerRefresh || 'Refresh')), + form({ action: "/peers/prune", method: "post" }, back, tabName === 'lan' ? input({ type: "hidden", name: "scope", value: "lan" }) : null, button({ type: "submit", class: "filter-btn" }, i18n.peerPruneIdle || 'Remove idle')), + form({ action: "/peers/export", method: "get" }, button({ type: "submit", class: "filter-btn" }, i18n.peerExport || 'Export')) + ) + ); + }; const importForm = form( { action: "/peers/import", method: "post", enctype: "multipart/form-data", class: "peers-import-form" }, label({ class: 'peers-import-label' }, i18n.peerImportTitle || 'Import peer list'), br(), textarea({ name: "peerList", rows: "4", placeholder: i18n.peerImportPlaceholder || 'Paste one multiserver address per line…' }), - br(), input({ type: "file", name: "peerFile", accept: ".txt,text/plain" }), br(), button({ type: "submit", class: "filter-btn" }, i18n.peerImport || 'Import') ); - const peersTechnicalBlock = div({ class: 'tags-header peers-technical-block' }, - h2(i18n.peerConnectionsTitle || 'Connections'), - div({ class: "conn-actions peers-conn-actions" }, refreshButton, pruneButton, exportButton), + const peersTechnicalBlock = div({ class: 'peers-technical-block' }, + peersToolbar('connections', technicalPeers.length, [ + { label: i18n.peerStateConnected, count: technicalPeers.filter(tp => tp.state === 'connected').length }, + { label: i18n.peerOutdated, count: technicalPeers.filter(tp => outdatedKey(tp.key)).length }, + { label: 'TOR', count: technicalPeers.filter(tp => onion.has(canonicalKey(tp.key)) || /\.onion$/i.test(String(tp.host || ''))).length } + ]), + div({ class: "tags-header" }, h2(withCount(i18n.peerConnectionsTitle || 'Connections', technicalPeers.length)), p(i18n.peersConnectionsHint)), + div({ class: "peers-pause-row" }, pauseButton, p({ class: paused ? "peers-paused-note" : "peers-pause-hint" }, paused ? i18n.peersPausedNote : i18n.peersPauseHint)), technicalPeers.length ? table({ class: 'block-info-table' }, tr( - td({ class: 'card-label' }, 'Key'), - td({ class: 'card-label' }, 'Host'), - td({ class: 'card-label' }, i18n.peerPort || 'Port'), - td({ class: 'card-label' }, 'State'), - td({ class: 'card-label' }, 'Last change'), + td({ class: 'card-label' }, i18n.peersOasisId), + td({ class: 'card-label' }, i18n.peerAddressLabel), + td({ class: 'card-label peer-port' }, i18n.peerPort), + techVersion ? td({ class: 'card-label' }, i18n.peersOasisVersion || 'Version') : null, + td({ class: 'card-label' }, i18n.peerStateLabel), + td({ class: 'card-label' }, i18n.peerLastChangeLabel), td({ class: 'card-label' }, '') ), ...technicalRows @@ -115,28 +172,136 @@ const peersView = async ({ onlinePeers, discoveredPeers, unknownPeers, lanBroadc importForm ); + const fmtDate = (ts) => ts ? new Date(ts).toISOString().slice(0, 16).replace('T', ' ') : '—'; + const healthRows = health && Array.isArray(health.rows) ? health.rows : []; + const healthKind = (r) => { + if (r.state === 'connected') return 'connected'; + if (r.dead) return 'unreachable'; + if ((Number(r.failures) || 0) > 0 || r.reason) return 'failing'; + return 'reachable'; + }; + const healthChip = (r) => { + const k = healthKind(r); + if (k === 'connected') return renderStateChip('mutuals', null, String(i18n.peerStateConnected).toUpperCase()); + if (k === 'unreachable') return renderStateChip('closed', null, String(i18n.graphosFedUnreachable).toUpperCase()); + if (k === 'failing') return renderStateChip('hidden', null, String(i18n[NET_REASON_KEYS[r.reason] || 'peerErrSilent'] || i18n.peerErrOther).toUpperCase()); + return renderStateChip(outdatedKey(r.key) ? 'whole' : 'mutuals', null, String(i18n.peersHealthReachable).toUpperCase()); + }; + const healthCount = (k) => healthRows.filter(r => healthKind(r) === k).length; + const deadCount = healthRows.filter(r => r.dead).length; + const healthVersion = knowsVersion(healthRows.map(r => r.key)); + const healthBlock = !healthRows.length ? div({ class: 'peers-list' }, p(i18n.noConnections)) : div({ class: 'peers-technical-block peers-health-block' }, + peersToolbar('health', healthRows.length, [ + { label: i18n.peersHealthReachable, count: healthCount('connected') + healthCount('reachable') }, + { label: i18n.peersHealthFailing, count: healthCount('failing') }, + { label: i18n.graphosFedUnreachable, count: deadCount }, + { label: i18n.peerOutdated, count: healthRows.filter(r => outdatedKey(r.key)).length }, + { label: 'TOR', count: healthRows.filter(r => r.tor).length } + ]), + div({ class: "tags-header" }, h2(withCount(i18n.peersTabHealth, healthRows.length)), p(i18n.peersHealthHint)), + deadCount + ? div({ class: "conn-actions peers-conn-actions filters" }, + form({ action: "/peers/clear-unreachable", method: "post" }, button({ type: "submit", class: "filter-btn" }, i18n.peersHealthForget))) + : null, + healthRows.length + ? table({ class: 'block-info-table' }, + tr( + td({ class: 'card-label' }, i18n.peerHost || 'IP'), + td({ class: 'card-label' }, i18n.peersOasisId || 'ID'), + td({ class: 'card-label' }, i18n.graphosFedTransport), + td({ class: 'card-label' }, i18n.peerStateLabel), + td({ class: 'card-label' }, i18n.peersHealthFailures), + td({ class: 'card-label' }, i18n.peersFailingSince), + healthVersion ? td({ class: 'card-label' }, i18n.peersOasisVersion || 'Version') : null + ), + ...healthRows.map(r => tr( + r.tor + ? td({ 'data-label': i18n.peerHost || 'IP', class: 'peer-host' }, renderStateChip('fediverse', null, 'TOR')) + : td({ 'data-label': i18n.peerHost || 'IP', title: unspecifiedHost(r.host) ? '' : String(r.host || ''), class: 'peer-host' }, r.host && !unspecifiedHost(r.host) ? shortHost(r.host) : '—'), + td({ 'data-label': i18n.peersOasisId || 'ID' }, a({ href: `/author/${encodeURIComponent(r.key)}`, class: 'user-link peer-key', title: r.name || r.key }, r.key)), + td({ 'data-label': i18n.graphosFedTransport }, r.tor ? renderStateChip('fediverse', null, 'TOR') : renderStateChip('transport', null, String(i18n.graphosFedClearnet).toUpperCase())), + td({ 'data-label': i18n.peerStateLabel }, healthChip(r)), + td({ 'data-label': i18n.peersHealthFailures }, String(Number(r.failures) || 0)), + td({ 'data-label': i18n.peersFailingSince }, r.failingSince ? fmtDate(r.failingSince) : '—'), + healthVersion ? td({ 'data-label': i18n.peersOasisVersion || 'Version' }, ...versionCell(r.key)) : null + )) + ) + : p(i18n.noConnections) + ); + + const lanRows = lan && Array.isArray(lan.rows) ? lan.rows : []; + const lanOn = !!(lan && lan.broadcasting); + const lanVersion = knowsVersion(lanRows.map(r => r.key)); + const lanBlock = div({ class: 'peers-technical-block peers-lan-block' }, + peersToolbar('lan', lanRows.length, [ + { label: i18n.online, count: lanRows.filter(r => r.online).length }, + { label: i18n.peerOutdated, count: lanRows.filter(r => outdatedKey(r.key)).length } + ]), + div({ class: "tags-header" }, h2(withCount(i18n.peersTabLan, lanRows.length)), p(i18n.peersLanHint)), + div({ class: "peers-pause-row" }, + p({ class: "peers-pause-hint" }, `${i18n.settingsLanTitle}: `, renderStateChip(lanOn ? 'mutuals' : 'hidden', null, String(lanOn ? i18n.modulesEnabledModulesLabel : i18n.modulesDisabledModulesLabel).toUpperCase())) + ), + lanRows.length + ? table({ class: 'block-info-table' }, + tr( + td({ class: 'card-label' }, i18n.peersOasisId || 'ID'), + td({ class: 'card-label' }, i18n.peerStateLabel), + td({ class: 'card-label' }, i18n.peersLanLastSeen), + lanVersion ? td({ class: 'card-label' }, i18n.peersOasisVersion || 'Version') : null, + td({ class: 'card-label' }, '') + ), + ...lanRows.map(r => tr( + td({ 'data-label': i18n.peersOasisId || 'ID' }, a({ href: `/author/${encodeURIComponent(r.key)}`, class: 'user-link peer-key', title: r.name || r.key }, r.key)), + td({ 'data-label': i18n.peerStateLabel }, r.online ? renderStateChip('mutuals', null, String(i18n.online).toUpperCase()) : renderStateChip('hidden', null, String(i18n.offline).toUpperCase())), + td({ 'data-label': i18n.peersLanLastSeen }, fmtDate(r.seenAt)), + lanVersion ? td({ 'data-label': i18n.peersOasisVersion || 'Version' }, ...versionCell(r.key)) : null, + td(r.online ? null : form({ method: "POST", action: "/peers/lan/forget", class: "inline-form" }, + input({ type: "hidden", name: "key", value: r.key }), + button({ type: "submit", class: "filter-btn" }, i18n.delete) + )) + )) + ) + : p(i18n.peersLanEmpty) + ); + + const tabs = ['status', 'connections'].concat(advanced ? ['lan', 'health'] : []); + const tab = tabs.includes(filter) ? filter : 'status'; + const tabLabel = { status: i18n.status || 'Status', connections: i18n.peerConnectionsTitle || 'Connections', lan: i18n.peersTabLan, health: i18n.peersTabHealth }; + const statusToolbar = peersToolbar('status', onlineCount + discoveredCount + unknownCount, [ + { label: i18n.online, count: onlineCount }, + { label: i18n.discovered, count: discoveredCount }, + { label: i18n.unknown, count: unknownCount } + ]); + const statusBlock = (onlineCount + discoveredCount + unknownCount) > 0 + ? div({ class: "peers-list" }, + statusToolbar, + ...[ + [i18n.online, onlineCount, dedupOnline, 'noConnections', i18n.peersOnlineHint], + [i18n.discovered, discoveredCount, dedupDiscovered, 'noDiscovered', i18n.peersDiscoveredHint], + [i18n.unknown, unknownCount, dedupUnknown, 'noUnknownPeers', i18n.peersUnknownHint] + ].filter(([, count]) => count > 0).flatMap(([title, count, list, emptyKey, hint], idx) => [ + idx ? hr() : null, + div({ class: "tags-header" }, h2(withCount(title, count)), p(hint)), + renderPeerTable(list, emptyKey) + ]) + ) + : div({ class: "peers-list" }, statusToolbar, p(i18n.noConnections)); + return template( i18n.peers, section( - div({ class: 'tags-header' }, + div({ class: 'tags-header module-header-line' }, h2(i18n.peers), p(i18n.peerConnectionsIntro) ), - div({ class: "conn-actions" }, ...connButtons), - (onlineCount + discoveredCount + unknownCount) > 0 - ? div({ class: "peers-list" }, - div({ class: "tags-header" }, h2(`${i18n.online} (${onlineCount})`)), - renderPeerTable(dedupOnline, 'noConnections'), - hr(), - div({ class: "tags-header" }, h2(`${i18n.discovered} (${discoveredCount})`)), - renderPeerTable(dedupDiscovered, 'noDiscovered'), - hr(), - div({ class: "tags-header" }, h2(`${i18n.unknown} (${unknownCount})`)), - renderPeerTable(dedupUnknown, 'noUnknownPeers') - ) - : null, - peersTechnicalBlock, - p(i18n.connectionActionIntro) + div({ class: 'mode-buttons-row' }, + ...tabs.map(t => a({ class: tab === t ? 'filter-btn active' : 'filter-btn', href: `/peers?filter=${t}` }, String(tabLabel[t]).toUpperCase())) + ), + tab === 'status' ? statusBlock : null, + tab === 'connections' ? peersTechnicalBlock : null, + tab === 'connections' ? p(i18n.connectionActionIntro) : null, + tab === 'lan' ? lanBlock : null, + tab === 'health' ? healthBlock : null ) ); }; diff --git a/src/views/phone_view.js b/src/views/phone_view.js new file mode 100644 index 0000000..96acdfd --- /dev/null +++ b/src/views/phone_view.js @@ -0,0 +1,299 @@ +const { div, h2, p, section, button, form, input, span, audio, table, tr, td, th, br, a } = require("../server/node_modules/hyperaxe"); +const { template, i18n, userLink, userLinkLabel, renderCallButton, liveClock, renderStateChip, renderVideoGrid, renderIcon } = require("./main_views"); +const { renderEncryptedChip } = require("./clearnet_view"); +const { phoneNumberOf } = require("../models/phone_number"); +const moment = require("../server/node_modules/moment"); + +const PAM_MAX_MS = require('../server/phone_module').VOICEMAIL_MAX_MS; +const GROUP_MAX = require('../server/phone_module').GROUP_MAX; + +const clock = (ms) => { + const s = Math.max(0, Math.floor((Number(ms) || 0) / 1000)); + return `${String(Math.floor(s / 60)).padStart(2, '0')}:${String(s % 60).padStart(2, '0')}`; +}; + +const postButton = (action, label, danger = false, fields = {}) => + form({ method: 'POST', action, class: 'phone-action-form' }, + ...Object.entries(fields).map(([name, value]) => input({ type: 'hidden', name, value: String(value) })), + button({ type: 'submit', class: danger ? 'tribe-action-btn danger-btn' : 'tribe-action-btn' }, String(label).toUpperCase()) + ); + +const linkList = (ids) => ids.reduce((acc, id, i) => i > 0 ? [...acc, ', ', userLink(id)] : [userLink(id)], []); +const peersOf = (h) => (Array.isArray(h.peers) && h.peers.length ? h.peers : [h.peer]).filter(Boolean); + +const OUTCOME_LABEL = { + ended: () => i18n.phoneOutcomeEnded, + missed: () => i18n.phoneOutcomeMissed, + rejected: () => i18n.phoneOutcomeRejected, + noanswer: () => i18n.phoneOutcomeNoanswer, + cancelled: () => i18n.phoneOutcomeCancelled, + pam: () => i18n.phoneOutcomePam, + failed: () => i18n.phoneOutcomeFailed +}; + +const renderCallPanel = (st, now) => { + if (!st) return null; + const who = st.group ? linkList(st.peers.map(x => x.id)) : [userLink(st.peer)]; + const line = (...parts) => div({ class: 'phone-call-who' }, span({ class: 'phone-call-icon' }, '✆'), ...parts); + const actions = (...btns) => div({ class: 'phone-call-actions' }, ...btns); + let body; + if (st.dir === 'in' && st.phase === 'incoming') { + body = [line(...who, ' ', i18n.phoneIsCalling), actions(process.env.OASIS_MOBILE === '1' ? postButton('/phone/accept', '\u2706 ' + i18n.phoneEarpiece) : postButton('/phone/accept', i18n.phoneAnswer), process.env.OASIS_MOBILE === '1' ? a({ href: '/phone/accept-speaker', class: 'tribe-action-btn phone-speaker-btn' }, span({ class: 'phone-ico' }, '\ud83d\udd0a'), ' ' + String(i18n.phoneSpeaker).toUpperCase()) : null, postButton('/phone/reject', i18n.phoneReject, true))]; + } else if (st.phase === 'calling') { + body = [line(st.reached ? i18n.phoneCalling : i18n.phoneConnecting, ' ', ...who, ' ', span({ class: 'phone-call-clock' }, liveClock(st.startedAt))), actions(postButton('/phone/hangup', i18n.phoneHangup, true))]; + } else if (st.phase === 'connecting' || st.phase === 'connected') { + const inCall = st.group ? linkList(st.peers.filter(x => x.phase === 'connected').map(x => x.id)) : who; + const ringing = st.group ? st.peers.filter(x => x.phase === 'calling').map(x => x.id) : []; + const silenceButton = (id, on) => on + ? postButton('/phone/silence', i18n.phoneUnsilence, true, { id, on: 0 }) + : postButton('/phone/silence', i18n.phoneSilence, false, { id, on: 1 }); + const peerRow = (pr) => div({ class: 'phone-contact room-peer' }, + span({ class: 'phone-call-icon' }, '✆'), + userLink(pr.id), + silenceButton(pr.id, pr.silenced) + ); + body = [ + line(...inCall, ' ', span({ class: 'phone-call-clock' }, liveClock(st.answeredAt || st.startedAt))), + ringing.length ? div({ class: 'phone-call-ringing' }, i18n.phoneCalling, ' ', ...linkList(ringing)) : null, + st.group ? div({ class: 'phone-contacts phone-call-peers' }, ...st.peers.filter(x => x.phase === 'connected').map(peerRow)) : null, + st.video ? renderVideoGrid([{ id: st.peer, on: st.peerCam }], st.cam) : null, + actions( + st.muted ? postButton('/phone/mute', i18n.phoneUnmute, false, { mute: 0 }) : postButton('/phone/mute', i18n.phoneMute, false, { mute: 1 }), + st.video ? (st.cam ? postButton('/phone/camera', i18n.phoneCameraOff, false, { cam: 0 }) : postButton('/phone/camera', i18n.phoneCameraOn, false, { cam: 1 })) : null, + st.group ? null : silenceButton(st.peer, st.silenced), + process.env.OASIS_MOBILE === '1' ? (() => { let on = false; try { on = require('fs').readFileSync(require('path').join(__dirname, '..', '..', '.audio_speaker'), 'utf8').trim() === '1'; } catch (_) {} return on ? a({ href: '/phone/speaker?on=0', class: 'tribe-action-btn phone-speaker-btn' }, '\u2706 ' + String(i18n.phoneEarpiece).toUpperCase()) : a({ href: '/phone/speaker?on=1', class: 'tribe-action-btn phone-speaker-btn' }, span({ class: 'phone-ico' }, '\ud83d\udd0a'), ' ' + String(i18n.phoneSpeaker).toUpperCase()); })() : null, + postButton('/phone/hangup', i18n.phoneHangup, true) + ) + ]; + } else if (st.phase === 'recording' || st.phase === 'recorded') { + const elapsed = st.phase === 'recorded' ? clock(PAM_MAX_MS) : liveClock(st.recordingStartedAt || now, { max: PAM_MAX_MS }); + body = [line(i18n.phoneRecording, ' ', ...who, ' ', span({ class: 'phone-call-clock' }, elapsed, ` / ${clock(PAM_MAX_MS)}`)), p({ class: 'phone-pam-hint' }, st.why === 'busy' ? i18n.phoneLeaveMessageBusy : st.why === 'unreachable' ? i18n.phoneLeaveMessageUnreachable : i18n.phoneLeaveMessage), actions(postButton('/phone/pam/send', i18n.phoneSendPam), postButton('/phone/pam/cancel', i18n.phoneCancel, true))]; + } else { + return null; + } + return div({ class: 'phone-call-panel' }, + div({ class: 'room-live-head' }, + div({ class: 'room-live-head-main' }, + div({ class: 'card-chips-row' }, renderEncryptedChip(i18n), st.group ? span({ class: 'pm-exposition-chip phone-joint-chip' }, span({ class: 'pm-exposition-text' }, i18n.phoneJointCall)) : null) + ), + (process.env.OASIS_MOBILE === '1' && (st.phase === 'incoming' || st.phase === 'connecting' || st.phase === 'connected')) ? null : a({ href: '/phone', class: 'tribe-action-btn room-refresh-btn' }, String(i18n.liveRefresh).toUpperCase()) + ), + ...body + ); +}; + +const dndChip = () => renderStateChip('hidden', null, String(i18n.phoneDnd).toUpperCase()); + +const renderNumberChooser = (compose) => + div({ class: 'phone-number-chooser' }, + p(String(i18n.phoneNumberChoose || '').replace('{number}', compose.number || '')), + div({ class: 'phone-contacts' }, + ...(compose.candidates || []).map(c => div({ class: 'phone-contact' }, + userLink(c.id), + span({ class: 'date-link' }, c.id), + c.dnd ? dndChip() : postButton('/phone/call', `✆ ${i18n.phoneCallButton}`, false, { to: c.to, compose: 1 }) + )), + ...(compose.rooms || []).map(r => div({ class: 'phone-contact' }, + a({ href: `/rooms/${encodeURIComponent(r.id)}`, class: 'user-link' }, r.title || i18n.roomsTitle), + span({ class: 'date-link' }, i18n.roomsTitle), + postButton(`/rooms/join/${encodeURIComponent(r.id)}`, `ꘒ ${i18n.roomJoin}`) + )) + ) + ); + +const DIAL_KEYS = ['1', '2', '3', '4', '5', '6', '7', '8', '9']; +const NUMBER_TOKEN = /^\d{3}-\d{3}$/; + +const renderCompose = (compose) => { + const recipients = Array.isArray(compose.recipients) ? compose.recipients : []; + const max = Number(compose.max) || GROUP_MAX; + const dial = String(compose.dial || ''); + const full = recipients.length >= max; + const keep = () => [input({ type: 'hidden', name: 'filter', value: 'create' }), input({ type: 'hidden', name: 'to', value: compose.to || '' })]; + const key = (value, label, cls = 'phone-key', extra = {}) => button({ type: 'submit', name: 'key', value, class: cls, ...extra }, label); + return section( + section({ class: 'phone-flash-anchor' }), + div({ class: 'pm-form phone-compose' }, + compose.choose && ((compose.candidates || []).length || (compose.rooms || []).length) ? renderNumberChooser(compose) : null, + form({ method: 'GET', action: '/phone', class: 'phone-dialer', id: 'phone-dialer' }, + ...keep(), + key('add', `+ ${i18n.phoneDialAdd}`, 'phone-dial-add', full ? { disabled: true } : {}), + div({ class: 'phone-screen' }, + input({ type: 'text', name: 'dial', value: dial, placeholder: i18n.phoneDialPlaceholder, maxlength: '64', autocomplete: 'off', ...(dial ? {} : { autofocus: true }) }) + ), + div({ class: 'phone-dialpad' }, + ...DIAL_KEYS.map(d => key(d, d)), + key('back', '⌫', 'phone-key phone-key-fn', { title: i18n.phoneDialBack }), + key('0', '0'), + key('clear', '✕', 'phone-key phone-key-fn', { title: i18n.phoneDialClear }) + ) + ), + p({ class: 'phone-empty phone-dial-hint' }, String(i18n.phoneLimitsHint).replace('{n}', String(max))), + div({ class: 'phone-recipients' }, + span({ class: 'card-label' }, `${i18n.phoneDialInhabitants}: ${recipients.length}/${max}`), + recipients.length ? div({ class: 'phone-recipient-list' }, + ...recipients.map((r, i) => span({ class: 'phone-recipient' }, + NUMBER_TOKEN.test(r) ? span(`✆ ${r}`) : [userLink(r), span(`✆ ${phoneNumberOf(r)}`)], + form({ method: 'GET', action: '/phone' }, + ...keep(), + input({ type: 'hidden', name: 'dial', value: dial }), + button({ type: 'submit', name: 'key', value: `remove:${i}`, class: 'phone-recipient-remove', title: i18n.phoneDialRemove }, '✕') + ) + )) + ) : null + ), + div({ class: 'phone-dial-call' }, + button({ type: 'submit', form: 'phone-dialer', formaction: '/phone/call', attrs: { formmethod: 'post' }, name: 'compose', value: '1', class: 'pm-btn' }, `✆ ${String(i18n.phoneCallButton).toUpperCase()}`) + ) + ) + ); +}; + +const renderRecords = (pams) => + div({ class: 'phone-block' }, + h2(i18n.phonePamTitle), + pams.length + ? div({ class: 'message-list' }, + ...pams.map(m => div({ class: 'pm-card normal-pm phone-record' }, + table({ class: 'pm-info-table' }, + tr(td({ class: 'card-label' }, i18n.pmFromLabel || 'From:'), td({ class: 'card-value' }, userLink(m.from))), + tr(td({ class: 'card-label' }, i18n.privateDate || 'Date'), td({ class: 'card-value' }, moment(m.sentAt).format('YYYY/MM/DD HH:mm:ss'))), + tr( + td({ class: 'card-label' }, i18n.phoneDuration), + td({ class: 'card-value pm-subject-cell' }, + span(clock(m.durationSec * 1000)), + m.heard ? null : span({ class: 'pm-exposition-chip pm-unread-chip' }, span({ class: 'pm-exposition-text' }, i18n.phoneNew)) + ) + ), + tr(td({ class: 'card-label' }, i18n.pmEncryptionLabel || 'Encryption'), td({ class: 'card-value pm-encryption-cell' }, renderEncryptedChip(i18n))) + ), + m.ready === false + ? div({ class: 'phone-record-pending' }, span({ class: 'pm-exposition-chip pm-unread-chip' }, span({ class: 'pm-exposition-text' }, i18n.torrentStateDownloading))) + : audio({ controls: true, preload: 'metadata', src: `/phone/pam/${encodeURIComponent(m.key)}/audio` }), + div({ class: 'pm-actions' }, + renderCallButton(m.from, { upper: true, cls: 'pm-btn' }), + form({ method: 'POST', action: `/phone/pam/${encodeURIComponent(m.key)}/delete`, class: 'pm-action-form' }, + button({ type: 'submit', class: 'pm-btn delete-btn danger-btn' }, String(i18n.phoneDelete).toUpperCase()) + ) + ) + )) + ) + : p({ class: 'phone-empty' }, i18n.phoneNoPams) + ); + +const deleteCalls = (ids) => ids.length + ? form({ method: 'POST', action: '/phone/delete-shown', class: 'content-action-form phone-row-delete' }, + ...ids.map(id => input({ type: 'hidden', name: 'calls', value: id })), + button({ type: 'submit', class: 'btn-singleview btn-delete', title: i18n.phoneDelete }, renderIcon('close')) + ) + : null; + +const renderLatest = (latest, canCall, history = []) => + div({ class: 'phone-block' }, + h2(i18n.phoneLatestTitle), + latest.length + ? div({ class: 'phone-contacts' }, + ...latest.map(c => div({ class: 'phone-contact phone-latest-row' }, + span({ class: 'phone-dir', title: c.dir === 'out' ? i18n.phoneOutgoing : i18n.phoneIncoming }, c.dir === 'out' ? '↗' : '↙'), + userLink(c.id), + span({ class: 'date-link' }, moment(c.startedAt).format('YYYY/MM/DD HH:mm')), + c.dnd ? dndChip() : canCall ? renderCallButton(c.id) : null, + deleteCalls(history.filter(h => peersOf(h).includes(c.id)).map(h => h.id)) + )) + ) + : p({ class: 'phone-empty' }, i18n.phoneNoHistory) + ); + +const renderHistory = (items) => + div({ class: 'phone-block' }, + h2(i18n.phoneHistoryTitle), + items.length + ? table({ class: 'phone-history' }, + tr(th(''), th(i18n.phoneWith), th(i18n.phoneResult), th(i18n.phoneWhen), th(i18n.phoneDuration), th('')), + ...items.map(h => tr( + td({ class: 'phone-dir', title: h.dir === 'out' ? i18n.phoneOutgoing : i18n.phoneIncoming }, h.dir === 'out' ? '↗' : '↙'), + td({ 'data-label': i18n.phoneWith }, ...linkList(peersOf(h))), + td({ 'data-label': i18n.phoneResult }, (OUTCOME_LABEL[h.outcome] || OUTCOME_LABEL.failed)()), + td({ 'data-label': i18n.phoneWhen }, moment(h.startedAt).format('YYYY/MM/DD HH:mm')), + td({ 'data-label': i18n.phoneDuration }, h.answeredAt ? clock(h.endedAt - h.answeredAt) : '—'), + td({ class: 'phone-row-actions' }, deleteCalls([h.id])) + )) + ) + : p({ class: 'phone-empty' }, i18n.phoneNoHistory) + ); + +exports.phoneView = ({ available = false, state = null, pams = [], history = [], latest = [], q = '', filter = 'all', compose = {}, now = Date.now(), refused = [] } = {}) => { + const qNorm = String(q || '').trim().toLowerCase(); + const matchId = (id) => !qNorm || [id, userLinkLabel(id)].some(v => String(v || '').toLowerCase().includes(qNorm)); + const isMissed = (h) => h.dir === 'in' && h.outcome === 'missed'; + const counts = { + all: pams.length + history.length, + records: pams.length, + missed: history.filter(isMissed).length, + incoming: history.filter(h => h.dir === 'in').length, + outgoing: history.filter(h => h.dir === 'out').length + }; + const records = pams.filter(m => matchId(m.from)); + const calls = history.filter(h => peersOf(h).some(matchId)); + const shownCalls = filter === 'missed' ? calls.filter(isMissed) + : filter === 'incoming' ? calls.filter(h => h.dir === 'in') + : filter === 'outgoing' ? calls.filter(h => h.dir === 'out') + : calls; + const showRecords = filter === 'all' || filter === 'records'; + const showCalls = filter === 'all' || filter === 'missed' || filter === 'incoming' || filter === 'outgoing'; + const unheard = showRecords ? records.filter(m => !m.heard).map(m => m.key) : []; + const deletable = [...(showRecords ? records.map(m => m.key) : []), ...(showCalls ? shownCalls.map(h => h.id) : [])]; + const canCall = available && !state; + const empty = counts.all === 0; + const filterBtn = (value, label, count) => + !empty && (value === 'all' || count > 0 || filter === value) + ? button({ type: 'submit', name: 'filter', value, class: filter === value ? 'filter-btn active' : 'filter-btn' }, `${String(label).toUpperCase()} (${count})`) + : null; + + return template( + i18n.phoneTitle, + section( + div({ class: 'tags-header module-header-line' }, + h2(i18n.phoneTitle), + p(i18n.phoneDescription), + renderEncryptedChip(i18n) + ), + available ? null : div({ class: 'pm-form-error-msg' }, p('✗ ' + i18n.phoneUnavailable)), + refused.length ? div({ class: 'phone-refused' }, span(i18n.phoneRefusedNotice), ' ', ...linkList(refused)) : null, + state ? renderCallPanel(state, now) : null, + div({ class: 'filters' }, + form({ method: 'GET', action: '/phone' }, + filterBtn('all', i18n.allButton || 'ALL', counts.all), + filterBtn('records', i18n.phonePamTitle, counts.records), + filterBtn('missed', i18n.phoneOutcomeMissed, counts.missed), + filterBtn('incoming', i18n.phoneIncoming, counts.incoming), + filterBtn('outgoing', i18n.phoneOutgoing, counts.outgoing), + canCall ? button({ type: 'submit', name: 'filter', value: 'create', class: 'create-button' }, i18n.phoneCreateButton) : null + ) + ), + (filter === 'create' || empty) ? null : div({ class: 'filters activity-filter-chips activity-toolbar-row' }, + (unheard.length || deletable.length) ? div({ class: 'pm-exposition inbox-exposition' }, + span({ class: 'inbox-filters-label' }, i18n.inboxFiltersLabel || 'Filters:'), + unheard.length ? form({ method: 'POST', action: '/phone/heard-all', class: 'inbox-vis-toggle' }, + ...unheard.map(k => input({ type: 'hidden', name: 'keys', value: k })), + button({ type: 'submit', class: 'btn' }, `${i18n.inboxMarkAllRead} (${unheard.length})`) + ) : null, + deletable.length ? form({ method: 'POST', action: '/phone/delete-shown', class: 'inbox-vis-toggle inbox-bulk-delete' }, + ...(showRecords ? records.map(m => input({ type: 'hidden', name: 'keys', value: m.key })) : []), + ...(showCalls ? shownCalls.map(h => input({ type: 'hidden', name: 'calls', value: h.id })) : []), + button({ type: 'submit', class: 'btn delete-btn' }, `${String(i18n.inboxDeleteShown).toUpperCase()} (${deletable.length})`) + ) : null + ) : null, + form({ method: 'GET', action: '/phone', class: 'filter-box' }, + input({ type: 'hidden', name: 'filter', value: filter }), + input({ type: 'text', name: 'q', value: String(q || ''), placeholder: i18n.phoneSearchPlaceholder, class: 'filter-box__input' }), + button({ type: 'submit', class: 'filter-box__button' }, i18n.searchButton) + ) + ), + filter === 'create' && canCall ? renderCompose(compose) : null, + showRecords ? renderRecords(records) : null, + filter === 'all' ? renderLatest(latest.filter(c => matchId(c.id)), canCall, history) : null, + showCalls ? renderHistory(shownCalls) : null + ) + ); +}; diff --git a/src/views/pixelia_view.js b/src/views/pixelia_view.js index 8cc81ae..d42de5e 100644 --- a/src/views/pixelia_view.js +++ b/src/views/pixelia_view.js @@ -1,7 +1,7 @@ const { div, h2, p, section, form, input, label, select, option, button, table, tr, td, hr, ul, li, a, br } = require("../server/node_modules/hyperaxe"); -const { template, i18n, userLink} = require('./main_views'); +const { template, i18n, userLink, renderDocumentActions } = require('./main_views'); -exports.pixeliaView = (pixelArt, errorMessage) => { +exports.pixeliaView = (pixelArt) => { const title = i18n.pixeliaTitle; const description = i18n.pixeliaDescription; const gridWidth = 50; @@ -18,7 +18,7 @@ exports.pixeliaView = (pixelArt, errorMessage) => { return td( { id: cellId, - title: pixel ? `By: ${pixel.author}` : "", + title: pixel ? `${pixel.author}` : "", class: `pixel-cell ${colorClass}` }, "" @@ -33,7 +33,7 @@ exports.pixeliaView = (pixelArt, errorMessage) => { return template( title, section( - div({ class: "tags-header" }, + div({ class: "tags-header module-header-line" }, h2(title), p(description) ) @@ -47,27 +47,26 @@ exports.pixeliaView = (pixelArt, errorMessage) => { input({ type: "number", id: "y", name: "y", min: 1, max: gridHeight, required: true }), label({ for: "color" }, i18n.colorLabel), select({ id: "color", name: "color", required: true }, - option({ value: "#000000", style: "background-color:#000000;" }, "Black"), - option({ value: "#ffffff", style: "background-color:#ffffff;" }, "White"), - option({ value: "#17f018", style: "background-color:#17f018;" }, "Green"), - option({ value: "#ffbb00", style: "background-color:#ffbb00;" }, "Yellow"), - option({ value: "#ff0000", style: "background-color:#ff0000;" }, "Red"), - option({ value: "#0000ff", style: "background-color:#0000ff;" }, "Blue"), - option({ value: "#ffff00", style: "background-color:#ffff00;" }, "Lime"), - option({ value: "#00ff00", style: "background-color:#00ff00;" }, "Spring Green"), - option({ value: "#00ffff", style: "background-color:#00ffff;" }, "Aqua"), - option({ value: "#ff00ff", style: "background-color:#ff00ff;" }, "Fuchsia"), - option({ value: "#a52a2a", style: "background-color:#a52a2a;" }, "Brown"), - option({ value: "#800080", style: "background-color:#800080;" }, "Purple"), - option({ value: "#808000", style: "background-color:#808000;" }, "Olive"), - option({ value: "#00bfff", style: "background-color:#00bfff;" }, "Deep Sky Blue"), - option({ value: "#d3d3d3", style: "background-color:#d3d3d3;" }, "Light Grey"), - option({ value: "#ff6347", style: "background-color:#ff6347;" }, "Tomato") + option({ value: "#000000", class: "pixelia-swatch pixelia-swatch-000000" }, "Black"), + option({ value: "#ffffff", class: "pixelia-swatch pixelia-swatch-ffffff" }, "White"), + option({ value: "#17f018", class: "pixelia-swatch pixelia-swatch-17f018" }, "Green"), + option({ value: "#ffbb00", class: "pixelia-swatch pixelia-swatch-ffbb00" }, "Yellow"), + option({ value: "#ff0000", class: "pixelia-swatch pixelia-swatch-ff0000" }, "Red"), + option({ value: "#0000ff", class: "pixelia-swatch pixelia-swatch-0000ff" }, "Blue"), + option({ value: "#ffff00", class: "pixelia-swatch pixelia-swatch-ffff00" }, "Lime"), + option({ value: "#00ff00", class: "pixelia-swatch pixelia-swatch-00ff00" }, "Spring Green"), + option({ value: "#00ffff", class: "pixelia-swatch pixelia-swatch-00ffff" }, "Aqua"), + option({ value: "#ff00ff", class: "pixelia-swatch pixelia-swatch-ff00ff" }, "Fuchsia"), + option({ value: "#a52a2a", class: "pixelia-swatch pixelia-swatch-a52a2a" }, "Brown"), + option({ value: "#800080", class: "pixelia-swatch pixelia-swatch-800080" }, "Purple"), + option({ value: "#808000", class: "pixelia-swatch pixelia-swatch-808000" }, "Olive"), + option({ value: "#00bfff", class: "pixelia-swatch pixelia-swatch-00bfff" }, "Deep Sky Blue"), + option({ value: "#d3d3d3", class: "pixelia-swatch pixelia-swatch-d3d3d3" }, "Light Grey"), + option({ value: "#ff6347", class: "pixelia-swatch pixelia-swatch-ff6347" }, "Tomato") ), - button({ type: "submit" }, i18n.paintButton) + button({ type: "submit", class: "filter-btn" }, String(i18n.paintButton).toUpperCase()) ) ), - errorMessage ? div({ class: "error-message" }, errorMessage) : null, div({ class: "total-pixels" }, h2(`${i18n.totalPixels}: ${pixelArt.length}`) ) @@ -76,6 +75,7 @@ exports.pixeliaView = (pixelArt, errorMessage) => { section( div({ class: "main_content" }, div({ class: "pixelia-grid-wrap" }, grid), + pixelArt.length > 0 ? renderDocumentActions('pixelia', null) : null, pixelArt.length > 0 ? div({ class: "contributors" }, h2(i18n.contributorsTitle), diff --git a/src/views/pm_view.js b/src/views/pm_view.js index 3612627..e31a20c 100644 --- a/src/views/pm_view.js +++ b/src/views/pm_view.js @@ -1,26 +1,25 @@ -const { div, h2, p, section, button, form, input, textarea, br, label, pre, span, strong } = require("../server/node_modules/hyperaxe"); +const { div, h2, p, section, button, form, input, textarea, br, label, pre, span, strong, a, select, option, datalist } = require("../server/node_modules/hyperaxe"); +const { TEXT_CAP } = require('../backend/long_text'); const { template, i18n } = require('./main_views'); const { getConfig } = require('../configs/config-manager.js'); -exports.pmView = async (initialRecipients = '', initialSubject = '', initialText = '', showPreview = false, sentKey = '', crypterError = false, crypterPreview = null, recipientError = false) => { +exports.pmView = async (initialRecipients = '', initialSubject = '', initialText = '', showPreview = false, sentKey = '', crypterError = false, crypterPreview = null, recipientError = false, fileError = '', fileSharePreview = null, opts = {}) => { + const mailingLists = Array.isArray(opts.lists) ? opts.lists : []; + const selectedList = String(opts.selectedList || ''); const title = i18n.pmSendTitle; const description = i18n.pmDescription; const textLen = (initialText || '').length; + const { renderEncryptedChip, renderDoubleEncryptionChip } = require('./clearnet_view'); return template( title, section( - div({ class: "tags-header" }, - div({ class: "title-with-chip" }, h2(title), renderEncryptedChip(i18n)), - p(description) + div({ class: "tags-header module-header-line" }, + h2(title), + p(description), + renderEncryptedChip(i18n) ), - crypterError - ? div({ class: "pm-form-error-msg" }, p('✗ ' + i18n.pmCrypterTooLong)) - : null, - recipientError - ? div({ class: "pm-form-error-msg" }, p('✗ ' + i18n.pmInvalidRecipients)) - : null, sentKey ? div({ class: "pm-sent-key" }, input({ type: "text", readonly: true, value: sentKey, class: "pm-sent-key-value" }) @@ -28,35 +27,54 @@ exports.pmView = async (initialRecipients = '', initialSubject = '', initialText : null, section( div({ class: "pm-form" }, - form({ method: "POST", action: "/pm", id: "pm-form" }, - label({ for: "recipients" }, i18n.pmLimitsHint), - br(), + h2({ class: "pm-section-title" }, i18n.pmComposeTitle || 'Send a message'), + form({ method: "POST", action: "/pm/preview", id: "pm-form" }, input({ type: "text", name: "recipients", - placeholder: i18n.pmRecipientsHint, - required: true, + placeholder: `${i18n.pmRecipientsHint}. ${i18n.pmLimitsHint}`, + ...(mailingLists.length ? {} : { required: true }), value: initialRecipients, maxlength: "511" }), br(), + mailingLists.length + ? div({ class: "pm-list-row" }, + label({ for: "pm-list" }, i18n.pmListLabel), + br(), + mailingLists.length <= 12 + ? select({ name: "list", id: "pm-list" }, + option({ value: "" }, i18n.pmListNone), + mailingLists.map(l => + option({ value: l.target, ...(selectedList === l.target ? { selected: true } : {}) }, `${l.label} (${l.count})`) + ) + ) + : div( + input({ type: "text", name: "list", id: "pm-list", list: "pm-list-options", value: (mailingLists.find(l => l.target === selectedList) || {}).label || selectedList, placeholder: i18n.pmListNone }), + datalist({ id: "pm-list-options" }, + mailingLists.map(l => option({ value: l.label }, `(${l.count})`)) + ) + ), + br() + ) + : null, label({ for: "subject" }, i18n.pmSubject), br(), input({ type: "text", name: "subject", placeholder: i18n.pmSubjectHint, value: initialSubject, maxlength: "150" }), br(), label({ for: "text" }, i18n.pmText), br(), - textarea({ name: "text", rows: "6", cols: "50", id: "pm-text", maxlength: "8096" }, initialText), + textarea({ name: "text", rows: "6", cols: "50", id: "pm-text", maxlength: String(TEXT_CAP), placeholder: i18n.pmTextPlaceholder || '' }, initialText), div({ class: "pm-crypter-row" }, label({ for: "pm-crypter" }, - input({ type: "checkbox", name: "crypter", value: "1", id: "pm-crypter" }), + input({ type: "checkbox", name: "crypter", value: "1", id: "pm-crypter", ...(crypterPreview ? { checked: true } : {}) }), renderDoubleEncryptionChip(i18n) ) ), div({ class: "pm-actions-block" }, div({ class: "pm-actions" }, - button({ type: "submit", formaction: "/pm/preview", formmethod: "POST" }, i18n.pmPreview), - button({ type: "submit", class: "btn-compact" }, i18n.pmSend) + button({ type: "submit", class: "pm-btn" }, i18n.pmPreview), + button({ type: "reset", class: "pm-btn danger-btn" }, i18n.pmReset || 'Reset') ) ) ), @@ -66,31 +84,112 @@ exports.pmView = async (initialRecipients = '', initialSubject = '', initialText div({ class: "title-with-chip" }, h2(i18n.pmPreviewTitle), renderEncryptedChip(i18n)), p({ class: "pm-preview-count" }, `${(initialText || '').length} ${i18n.pmCrypterCharsLabel}`), div({ class: "pm-preview-content" }, pre({ class: "pm-pre" }, initialText || '')), + div({ class: "title-with-chip" }, h2(i18n.pmCrypterCipherLabel), renderDoubleEncryptionChip(i18n)), + p({ class: "pm-key-hint" }, i18n.pmSharedKeyHint), div({ class: "pm-sent-key" }, input({ type: "text", readonly: true, value: crypterPreview.key, class: "pm-sent-key-value" }) ), - div({ class: "title-with-chip" }, h2(i18n.pmCrypterCipherLabel), renderDoubleEncryptionChip(i18n)), p({ class: "pm-preview-count" }, `${(crypterPreview.cipher || '').length} ${i18n.pmCrypterCharsLabel}`), div({ class: "pm-preview-content" }, pre({ class: "pm-pre" }, crypterPreview.cipher || '')), form({ method: "POST", action: "/pm", class: "pm-crypter-send-form" }, input({ type: "hidden", name: "recipients", value: initialRecipients }), + input({ type: "hidden", name: "list", value: selectedList }), input({ type: "hidden", name: "subject", value: initialSubject }), input({ type: "hidden", name: "text", value: initialText }), input({ type: "hidden", name: "crypter", value: "1" }), input({ type: "hidden", name: "crypterKey", value: crypterPreview.key }), input({ type: "hidden", name: "precomputed", value: crypterPreview.cipher }), - button({ type: "submit", class: "btn-compact" }, i18n.pmSend) + div({ class: "pm-actions" }, + button({ type: "submit", class: "pm-btn" }, i18n.pmSend), + a({ href: "/pm", class: "pm-btn danger-btn" }, i18n.pmCancel || 'Cancel') + ) ) ) : div({ id: "pm-preview-area", class: "pm-preview" }, div({ class: "title-with-chip" }, h2(i18n.pmPreviewTitle), renderEncryptedChip(i18n)), - p({ id: "pm-preview-count", class: "pm-preview-count" }, `${textLen}/8096`), + p({ id: "pm-preview-count", class: "pm-preview-count" }, `${textLen}/7000`), div({ id: "pm-preview-content", class: "pm-preview-content" }, pre({ class: "pm-pre" }, initialText || '') + ), + form({ method: "POST", action: "/pm", class: "pm-send-form" }, + input({ type: "hidden", name: "recipients", value: initialRecipients }), + input({ type: "hidden", name: "list", value: selectedList }), + input({ type: "hidden", name: "subject", value: initialSubject }), + input({ type: "hidden", name: "text", value: initialText }), + div({ class: "pm-actions" }, + button({ type: "submit", class: "pm-btn" }, i18n.pmSend), + a({ href: "/pm", class: "pm-btn danger-btn" }, i18n.pmCancel || 'Cancel') + ) ) )) : null ) + ), + section({ id: "fileshare" }, + div({ class: "pm-form pm-fileshare-form-wrap" }, + h2({ class: "pm-section-title" }, i18n.fileShareTitle || 'Share a file'), + form({ method: "POST", action: "/pm/file/preview#fileshare", enctype: "multipart/form-data", class: "pm-fileshare-form" }, + input({ id: "fs-recipient", type: "text", name: "recipient", placeholder: i18n.fileShareRecipientPlaceholder || 'Enter Oasis ID (@....ed25519)', required: true, value: initialRecipients, maxlength: "120" }), + br(), + label({ for: "fs-subject" }, i18n.pmSubject), + br(), + input({ id: "fs-subject", type: "text", name: "subject", placeholder: i18n.pmSubjectHint, value: initialSubject, maxlength: "150" }), + br(), + label({ for: "fs-file" }, i18n.fileShareFileLabel || 'File'), + br(), + input({ id: "fs-file", type: "file", name: "file", required: true }), + div({ class: "pm-crypter-row" }, + label({ for: "fs-crypter" }, + input({ type: "checkbox", name: "crypter", value: "1", id: "fs-crypter", ...(fileSharePreview && fileSharePreview.crypter ? { checked: true } : {}) }), + renderDoubleEncryptionChip(i18n) + ) + ), + div({ class: "pm-actions-block" }, + div({ class: "pm-actions" }, + button({ type: "submit", class: "pm-btn" }, i18n.pmPreview), + button({ type: "reset", class: "pm-btn danger-btn" }, i18n.pmReset || 'Reset') + ) + ) + ), + fileSharePreview + ? div({ class: "pm-preview pm-fileshare-preview" }, + div({ class: "title-with-chip" }, h2(i18n.pmPreviewTitle), renderEncryptedChip(i18n)), + fileSharePreview.crypter + ? div( + div({ class: "title-with-chip" }, h2(i18n.pmCrypterCipherLabel), renderDoubleEncryptionChip(i18n)), + p({ class: "pm-key-hint" }, i18n.pmSharedKeyHint), + div({ class: "pm-sent-key" }, input({ type: "text", readonly: true, value: fileSharePreview.sharedKey, class: "pm-sent-key-value" })) + ) + : null, + div({ class: "pm-fileshare-info" }, + span({ class: "pm-fileshare-icon" }, '📎'), + span({ class: "pm-fileshare-name" }, String(fileSharePreview.filename || 'file')), + span({ class: "pm-fileshare-meta" }, `${fileSharePreview.sizeLabel || ''} · ${String(fileSharePreview.mime || 'application/octet-stream')}`) + ), + form({ method: "POST", action: "/pm/file", class: "pm-fileshare-send-form" }, + fileSharePreview.recipient + ? input({ type: "hidden", name: "recipient", value: fileSharePreview.recipient }) + : div({ class: "pm-fileshare-recipient" }, + label({ for: "fs-preview-recipient" }, i18n.pmRecipients), + br(), + input({ id: "fs-preview-recipient", type: "text", name: "recipient", placeholder: i18n.fileShareRecipientPlaceholder || 'Enter Oasis ID (@....ed25519)', required: true, maxlength: "120" }) + ), + input({ type: "hidden", name: "subject", value: fileSharePreview.subject || '' }), + input({ type: "hidden", name: "manifestBlobId", value: fileSharePreview.manifestBlobId }), + input({ type: "hidden", name: "keyHex", value: fileSharePreview.keyHex }), + input({ type: "hidden", name: "filename", value: fileSharePreview.filename || 'file' }), + input({ type: "hidden", name: "mime", value: fileSharePreview.mime || 'application/octet-stream' }), + input({ type: "hidden", name: "size", value: String(fileSharePreview.size || 0) }), + fileSharePreview.crypter ? input({ type: "hidden", name: "crypter", value: "1" }) : null, + fileSharePreview.crypter ? input({ type: "hidden", name: "sharedKey", value: fileSharePreview.sharedKey }) : null, + div({ class: "pm-actions" }, + button({ type: "submit", class: "pm-btn" }, i18n.pmSend), + a({ href: "/pm", class: "pm-btn danger-btn" }, i18n.pmCancel || 'Cancel') + ) + ) + ) + : null + ) ) ) ); diff --git a/src/views/podcasts_view.js b/src/views/podcasts_view.js new file mode 100644 index 0000000..4c7141f --- /dev/null +++ b/src/views/podcasts_view.js @@ -0,0 +1,321 @@ +const { div, h2, h3, p, section, button, form, a, span, br, textarea, input, label, select, option, img, table, tr, td, audio: audioHyperaxe, video: videoHyperaxe } = require("../server/node_modules/hyperaxe"); +const { clearnetItemHref, template, i18n, userLink, renderStateChip, renderContentActions, renderSubscriptionBox, renderModuleStats, renderOpinionsVoting, renderEngagement, moduleIsEmpty, contentDeleteAction, paged } = require("./main_views"); +const { renderCommentsSection } = require("./comments_view"); +const { renderStyledText } = require("../backend/renderStyledText"); +const { renderReachChip, renderClearnetSelector, renderClearnetSwitch } = require("./clearnet_view"); +const moment = require("../server/node_modules/moment"); +const { config } = require("../server/SSB_server.js"); + + +const userId = config.keys.id; +const BASE_FILTERS = ["ALL", "RECENT", "MINE", "TOP", "VIEWERS"]; +const CATEGORIES = ["NEWS", "MUSIC", "TALK", "EDUCATION", "TECH", "CULTURE", "COMMUNITY", "OASIS", "OTHER"]; + +const cap = (s) => s.charAt(0) + s.slice(1).toLowerCase(); +const filterLabel = (f) => String(i18n[`podcastFilter${cap(f)}`] || f).toUpperCase(); +const catLabel = (c) => String(i18n[`podcastCategory${cap(c)}`] || c).toUpperCase(); +const channelHref = (ch) => `/podcasts/${encodeURIComponent(ch.id)}`; +const episodeHref = (ep) => `/podcasts/episode/${encodeURIComponent(ep.id)}`; +const fmt = (d) => moment(d).format("YYYY/MM/DD HH:mm"); +const blobSrc = (m) => `/blob/${encodeURIComponent(m.blobId)}`; + +const renderTags = (tags) => (Array.isArray(tags) && tags.length) + ? div({ class: "card-tags" }, ...tags.map(t => a({ href: `/search?query=%23${encodeURIComponent(t)}`, class: "tag-link" }, `#${t}`))) + : null; + +const renderCover = (ch, cls = "podcast-cover") => { + if (!ch.cover || !ch.cover.blobId) return null; + if (ch.cover.kind === "video") return videoHyperaxe({ class: cls, src: blobSrc(ch.cover), controls: true, preload: "metadata", muted: true }); + return img({ class: cls, src: blobSrc(ch.cover), alt: ch.title || "" }); +}; + +const countsLine = (ch) => div({ class: "tribe-card-members" }, + span({ class: "tribe-members-count" }, `${i18n.podcastPlaysLabel}: ${ch.playCount || 0}`) +); +const infoTable = (item) => table({ class: "tribe-info-table jobs-info-table" }, + tr( + td({ class: "tribe-info-label" }, i18n.createdAtLabel || "Created at"), + td({ class: "tribe-info-value", colspan: "3" }, fmt(item.createdAt)) + ), + tr( + td({ class: "tribe-info-value", colspan: "4" }, userLink(item.author)) + ) +); + +const podcastChipFor = (mode, census) => { + if (mode === "ALL") return true; + if (!Array.isArray(census)) return true; + if (mode === "MINE") return census.some(c => String(c.author) === String(userId)); + if (mode === "RECENT") return census.length > 0; + if (mode === "TOP") return census.some(c => (c.opinionCount || 0) > 0 || (c.spreadCount || 0) > 0); + if (mode === "VIEWERS") return census.some(c => (c.playCount || 0) > 0); + if (CATEGORIES.includes(mode)) return census.some(c => c.category === mode); + return census.length > 0; +}; +exports.podcastChipFor = podcastChipFor; + +const renderChannelCard = (ch, params = {}) => + div({ class: "tribe-card podcast-card" }, + div({ class: "card-header activity-card-header" }, + span(), + renderContentActions(ch.id, channelHref(ch), { author: ch.author, favKind: "podcasts", isFavorite: ch.isFavorite, reportTitle: ch.title, spread: (params.spreadMap && params.spreadMap.get(ch.id)) || null, deleteAction: String(ch.author) === String(userId) ? contentDeleteAction("podcast", ch.id) : undefined }) + ), + div({ class: "tribe-card-body" }, + ch.cover && ch.cover.blobId ? a({ href: channelHref(ch) }, renderCover(ch, "podcast-card-cover")) : null, + div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, a({ href: channelHref(ch) }, ch.title || "—"))), + div({ class: "card-chips-row" }, renderStateChip("neutral", "", catLabel(ch.category)), ch.clearnet === true ? renderReachChip(true, i18n, clearnetItemHref('podcasts', ch.title, ch.id)) : null), + countsLine(ch) + ) + ); + +const chipButtons = (modes, filter, census, labelOf) => + modes.filter(m => m === filter || podcastChipFor(m, census)).map(m => button({ type: "submit", name: "filter", value: m, class: filter === m ? "filter-btn active" : "filter-btn" }, labelOf(m))); + +const renderFilters = (filter, q, census, emptyMod) => + section( + div({ class: "filters" }, + form({ method: "GET", action: "/podcasts", class: "ui-toolbar ui-toolbar--filters" }, + ...(emptyMod ? [] : [ + ...chipButtons(BASE_FILTERS, filter, census, filterLabel), + ...chipButtons(CATEGORIES, filter, census, catLabel) + ]), + button({ type: "submit", name: "filter", value: "CREATE", class: "create-button" }, i18n.podcastCreate) + ) + ) + ); + +const renderChannelForm = (ch) => + div({ class: "div-center audio-form" }, + h2(ch ? i18n.podcastUpdateSectionTitle : i18n.podcastCreateSectionTitle), + form({ method: "POST", action: ch ? `/podcasts/update/${encodeURIComponent(ch.id)}` : "/podcasts/create", enctype: "multipart/form-data" }, + label(i18n.podcastTitleLabel), br(), + input({ type: "text", name: "title", maxlength: "100", required: true, placeholder: i18n.podcastTitlePlaceholder, value: ch ? ch.title : "" }), br(), + label(i18n.podcastDescriptionLabel), br(), + textarea({ name: "description", rows: 5, maxlength: "3000", placeholder: i18n.podcastDescriptionPlaceholder }, ch ? ch.description : ""), br(), + label(i18n.podcastCoverLabel), br(), + input({ type: "file", name: "cover", accept: "image/*,video/*" }), br(), br(), + label(i18n.podcastCategoryLabel), br(), + select({ name: "category" }, ...CATEGORIES.map(c => option({ value: c, ...((ch ? ch.category : "TALK") === c ? { selected: true } : {}) }, catLabel(c)))), br(), br(), + label(i18n.podcastTagsLabel), br(), + input({ type: "text", name: "tags", maxlength: "200", placeholder: i18n.podcastTagsPlaceholder, value: ch ? ch.tags.join(", ") : "" }), br(), br(), + renderClearnetSelector(ch ? !!ch.clearnet : false, i18n), br(), + button({ type: "submit", class: "create-button" }, ch ? i18n.podcastUpdate : i18n.podcastCreate) + ) + ); + +const renderEpisodeForm = (ch, ep) => + div({ class: "div-center audio-form" }, + h2(ep ? i18n.podcastEpisodeUpdateTitle : i18n.podcastEpisodeCreateTitle), + form({ method: "POST", action: ep ? `/podcasts/episode/update/${encodeURIComponent(ep.id)}` : `/podcasts/${encodeURIComponent(ch.id)}/episodes`, enctype: "multipart/form-data" }, + label(i18n.podcastEpisodeTitleLabel), br(), + input({ type: "text", name: "title", maxlength: "120", required: true, placeholder: i18n.podcastEpisodeTitlePlaceholder, value: ep ? ep.title : "" }), br(), + label(i18n.podcastEpisodeDescriptionLabel), br(), + textarea({ name: "description", rows: 6, maxlength: "5000", placeholder: i18n.podcastEpisodeDescriptionPlaceholder }, ep ? ep.description : ""), br(), + label(i18n.podcastEpisodeAudioLabel), br(), + input({ type: "file", name: "media", accept: "audio/*,video/*", ...(ep ? {} : { required: true }) }), br(), br(), + label(i18n.podcastTagsLabel), br(), + input({ type: "text", name: "tags", maxlength: "200", placeholder: i18n.podcastTagsPlaceholder, value: ep ? ep.tags.join(", ") : "" }), br(), br(), + button({ type: "submit", class: "create-button" }, ep ? i18n.podcastUpdate : i18n.podcastEpisodePublish) + ) + ); + +exports.podcastsView = async (channels, filter = "ALL", params = {}) => { + const list = Array.isArray(channels) ? channels : []; + const q = String(params.q || "").trim(); + const census = Array.isArray(params.censusList) ? params.censusList : list; + const f = String(filter || "ALL").toUpperCase(); + const isForm = f === "CREATE" || f === "EDIT"; + const emptyMod = !isForm && moduleIsEmpty(census, "ALL", "ALL", q); + return template( + i18n.podcastsTitle, + section( + div({ class: "tags-header module-header-line" }, h2(i18n.podcastsTitle), p(i18n.podcastsDescription)), + renderFilters(isForm ? "ALL" : f, q, isForm ? [] : census, emptyMod) + ), + section( + isForm + ? renderChannelForm(f === "EDIT" ? params.channel : null) + : [ + emptyMod ? null : div({ class: "filters activity-filter-chips activity-toolbar-row" }, + renderModuleStats(list.length, [ + { label: i18n.podcastEpisodesLabel, count: list.reduce((s, c) => s + (c.episodeCount || 0), 0) }, + { label: i18n.podcastPlaysLabel, count: list.reduce((s, c) => s + (c.playCount || 0), 0) } + ]), + form({ method: "GET", action: "/podcasts", class: "filter-box" }, + input({ type: "hidden", name: "filter", value: f }), + input({ type: "text", name: "q", value: q, placeholder: i18n.podcastSearchPlaceholder, class: "filter-box__input" }), + div({ class: "filter-box__controls" }, button({ type: "submit", class: "filter-box__button" }, i18n.searchButton)) + ) + ), + div({ class: "tribe-grid" }, list.length ? paged(list).map(c => renderChannelCard(c, params)) : p(i18n.podcastNoItems)) + ] + ) + ); +}; + +const episodeChips = (ep) => + div({ class: "card-chips-row" }, + renderStateChip("neutral", "", `${i18n.podcastPlaysLabel}: ${ep.playCount || 0}`), + ep.opinionCount > 0 ? renderStateChip("mutuals", "ꔍ", String(ep.opinionCount)) : null + ); + +const episodeActions = (ep, params = {}, returnTo) => + renderContentActions(ep.id, episodeHref(ep), { + author: ep.author, + favKind: "podcasts", + isFavorite: ep.isFavorite, + reportTitle: ep.title, + spread: (params.spreadMap && params.spreadMap.get(ep.id)) || null, + returnTo, + torrentFrom: { blobId: ep.media && ep.media.blobId, name: ep.title }, + deleteAction: String(ep.author) === String(userId) ? contentDeleteAction("podcastEpisode", ep.id) : undefined + }); + +const episodeRow = (ch, ep, params = {}, returnTo) => + div({ class: "podcast-episode-row" }, + span({ class: "podcast-episode-number" }, `#${ep.number}`), + div({ class: "podcast-episode-main" }, + div({ class: "emergency-update-head podcast-episode-head" }, + div({ class: "podcast-episode-head-left" }, + a({ href: episodeHref(ep), class: "podcast-episode-title" }, ep.title), + episodeChips(ep) + ), + episodeActions(ep, params, returnTo) + ), + ep.description ? p({ class: "tribe-side-description" }, ...renderStyledText(ep.description)) : null, + p({ class: "card-footer" }, span({ class: "date-link" }, fmt(ep.createdAt))) + ) + ); + +exports.singleChannelView = async (ch, params = {}) => { + const isAuthor = String(ch.author) === String(userId); + const href = channelHref(ch); + const census = Array.isArray(params.censusList) ? params.censusList : []; + const mode = String(params.mode || "").toUpperCase(); + const ownerActions = isAuthor + ? [ + form({ method: "GET", action: "/podcasts" }, input({ type: "hidden", name: "filter", value: "edit" }), input({ type: "hidden", name: "id", value: ch.id }), button({ type: "submit", class: "update-btn" }, i18n.podcastUpdate)) + ] + : []; + const allEpisodes = Array.isArray(ch.episodes) ? ch.episodes.slice().reverse() : []; + const episodeFilter = String(params.episodes || "").toUpperCase() === "VIEWERS" && allEpisodes.some(e => (e.playCount || 0) > 0) ? "VIEWERS" : "RECENT"; + const episodes = episodeFilter === "VIEWERS" ? allEpisodes.slice().sort((x, y) => (y.playCount || 0) - (x.playCount || 0) || y.publishedTs - x.publishedTs) : allEpisodes; + const episodeModes = ["RECENT", ...(allEpisodes.some(e => (e.playCount || 0) > 0) ? ["VIEWERS"] : [])]; + const episodeChipsRow = allEpisodes.length + ? div({ class: "activity-sub-filter" }, + ...episodeModes.map(m => form({ class: "sub-filter-form", method: "GET", action: `${href}#episodes` }, + input({ type: "hidden", name: "episodes", value: m }), + button({ type: "submit", class: episodeFilter === m ? "filter-btn active" : "filter-btn" }, filterLabel(m)) + )) + ) + : null; + const side = div({ class: "tribe-side" }, + div({ class: "card-header activity-card-header" }, + renderContentActions(ch.id, href, { author: ch.author, favKind: "podcasts", isFavorite: ch.isFavorite, reportTitle: ch.title, spread: params.spread || null, returnTo: href, deleteAction: isAuthor ? contentDeleteAction("podcast", ch.id) : undefined }) + ), + div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, ch.title), renderReachChip(!!ch.clearnet, i18n, ch.clearnet ? clearnetItemHref('podcasts', ch.title, ch.id) : null)), + isAuthor ? renderClearnetSwitch("podcasts", ch.rootId || ch.id, !!ch.clearnet) : null, + div({ class: "card-chips-row" }, renderStateChip("neutral", "", catLabel(ch.category))), + ch.cover && ch.cover.blobId ? a({ href: channelHref(ch), class: "podcast-cover-link" }, renderCover(ch, "podcast-cover podcast-cover-large")) : null, + ch.description ? p({ class: "tribe-side-description" }, ...renderStyledText(ch.description)) : null, + infoTable(ch), + countsLine(ch), + isAuthor + ? div({ class: "tribe-side-actions podcast-episode-add" }, + form({ method: "GET", action: href }, input({ type: "hidden", name: "mode", value: "episode" }), button({ type: "submit", class: "tribe-action-btn" }, String(i18n.podcastEpisodeAdd).toUpperCase())) + ) + : null, + renderTags(ch.tags), + ownerActions.length ? div({ class: "tribe-side-actions owner-actions" }, ...ownerActions) : null, + params.subscription + ? renderSubscriptionBox({ target: ch.id, scope: "podcasts", subscribed: params.subscription.subscribed, count: params.subscription.count, isOwner: isAuthor, returnTo: href, canWrite: isAuthor }) + : null + ); + const main = div({ class: "tribe-main" }, + isAuthor && mode === "EPISODE" ? renderEpisodeForm(ch, null) : null, + allEpisodes.length + ? div({ class: "podcast-featured" }, + div({ class: "emergency-update-head podcast-episode-head" }, + div({ class: "podcast-episode-head-left" }, + h2({ class: "tribe-card-title" }, a({ href: episodeHref(allEpisodes[0]) }, `#${allEpisodes[0].number} · ${allEpisodes[0].title}`)), + episodeChips(allEpisodes[0]) + ), + episodeActions(allEpisodes[0], params, href) + ), + renderPlayer(allEpisodes[0]), + allEpisodes[0].description ? p({ class: "tribe-side-description" }, ...renderStyledText(allEpisodes[0].description)) : null, + p({ class: "card-footer" }, span({ class: "date-link" }, fmt(allEpisodes[0].createdAt))) + ) + : null, + div({ class: "card-section podcast-episodes", id: "episodes" }, + h3(`${i18n.podcastEpisodesTitle} (${episodes.length})`), + episodeChipsRow, + episodes.length ? div({ class: "podcast-episode-list" }, ...episodes.map(ep => episodeRow(ch, ep, params, href))) : p(i18n.podcastNoEpisodes) + ), + renderEngagement(ch.id, + renderOpinionsVoting("/podcasts/opinions", ch.id, ch.opinions, href, ch.opinions_inhabitants), + renderCommentsSection({ action: `/podcasts/${encodeURIComponent(ch.id)}/comments`, comments: params.comments || [], returnTo: href }) + ) + ); + return template( + ch.title, + section(div({ class: "tags-header module-header-line" }, h2(i18n.podcastsTitle), p(i18n.podcastsDescription)), renderFilters("ALL", "", census, census.length === 0)), + section(div({ class: "tribe-details" }, side, main)) + ); +}; + +const renderPlayer = (ep) => { + if (!ep.media || !ep.media.blobId) return p(i18n.audioNoFile); + if (ep.media.kind === "video") return div({ class: "podcast-player video-container video-container-row" }, videoHyperaxe({ controls: true, src: blobSrc(ep.media), preload: "metadata", class: "podcast-video" })); + return div({ class: "podcast-player audio-container" }, audioHyperaxe({ controls: true, src: blobSrc(ep.media), preload: "metadata" })); +}; + +exports.singleEpisodeView = async (ep, params = {}) => { + const ch = ep.channel || { id: ep.channelId, title: "", author: ep.author, episodes: [] }; + const isAuthor = String(ep.author) === String(userId); + const href = episodeHref(ep); + const census = Array.isArray(params.censusList) ? params.censusList : []; + const played = Array.isArray(ep.listeners) && ep.listeners.includes(userId); + const mode = String(params.mode || "").toUpperCase(); + const headActions = [ + played + ? span({ class: "status supporting" }, `✓ ${i18n.podcastPlayed}`) + : form({ method: "POST", action: `/podcasts/episode/${encodeURIComponent(ep.id)}/play` }, button({ type: "submit", class: "tribe-action-btn" }, String(i18n.podcastMarkPlayed).toUpperCase())) + ].filter(Boolean); + const ownerActions = isAuthor + ? [ + form({ method: "GET", action: href }, input({ type: "hidden", name: "mode", value: "edit" }), button({ type: "submit", class: "update-btn" }, i18n.podcastUpdate)) + ] + : []; + const side = div({ class: "tribe-side" }, + div({ class: "card-header activity-card-header" }, + renderContentActions(ep.id, href, { author: ep.author, favKind: "podcasts", isFavorite: ep.isFavorite, reportTitle: ep.title, spread: params.spread || null, returnTo: href, torrentFrom: { blobId: ep.media && ep.media.blobId, name: ep.title }, deleteAction: isAuthor ? contentDeleteAction("podcastEpisode", ep.id) : undefined }) + ), + div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, a({ href: channelHref(ch), class: "user-link" }, ch.title || i18n.podcastsTitle)), renderReachChip(!!ch.clearnet, i18n, ch.clearnet ? clearnetItemHref('podcasts', ch.title, ch.id) : null)), + ch.cover && ch.cover.blobId ? a({ href: channelHref(ch), class: "podcast-cover-link" }, renderCover(ch, "podcast-cover podcast-cover-large")) : null, + ep.description ? p({ class: "tribe-side-description" }, ...renderStyledText(ep.description)) : null, + infoTable(ep), + renderTags(ep.tags), + ownerActions.length ? div({ class: "tribe-side-actions owner-actions" }, ...ownerActions) : null + ); + const main = div({ class: "tribe-main" }, + div({ class: "emergency-update-head podcast-episode-head" }, + div({ class: "podcast-episode-head-left" }, + h2({ class: "tribe-card-title" }, `#${ep.number} · ${ep.title}`), + episodeChips(ep) + ), + headActions.length ? div({ class: "tribe-side-actions emergency-update-actions" }, ...headActions) : null + ), + renderPlayer(ep), + renderEngagement(ep.id, + renderOpinionsVoting("/podcasts/opinions", ep.id, ep.opinions, href, ep.opinions_inhabitants), + renderCommentsSection({ action: `/podcasts/episode/${encodeURIComponent(ep.id)}/comments`, comments: params.comments || [], returnTo: href }) + ), + isAuthor && mode === "EDIT" ? renderEpisodeForm(ch, ep) : null + ); + return template( + ep.title, + section(div({ class: "tags-header module-header-line" }, h2(i18n.podcastsTitle), p(i18n.podcastsDescription)), renderFilters("ALL", "", census, census.length === 0)), + section(div({ class: "tribe-details" }, side, main)) + ); +}; diff --git a/src/views/polls_view.js b/src/views/polls_view.js new file mode 100644 index 0000000..61461fd --- /dev/null +++ b/src/views/polls_view.js @@ -0,0 +1,329 @@ +const { div, h2, p, section, button, form, a, input, label, span, textarea, br, table, tr, td } = require("../server/node_modules/hyperaxe"); +const { renderCommentsSection: renderSharedCommentsSection } = require("./comments_view"); +const { template, i18n, userLink, renderOpinionsVoting, renderEngagement, renderSpreadButton, renderContentActions, renderStateChip, renderLifespanChip, renderSpreadEditWarning, renderModuleStatsBy, moduleIsEmpty, contentDeleteAction, paged, renderCreatedRows } = require("./main_views"); +const moment = require("../server/node_modules/moment"); +const { config } = require("../server/SSB_server.js"); +const { MAX_OPTIONS, MIN_OPTIONS, MAX_OPTION_LENGTH } = require("../models/polls_model_limits"); + +const userId = config.keys.id; + +const FILTERS = [ + { key: "ALL", i18n: "pollFilterAll" }, + { key: "RECENT", i18n: "pollFilterRecent" }, + { key: "MINE", i18n: "pollFilterMine" }, + { key: "TOP", i18n: "pollFilterTop" }, + { key: "VOTED", i18n: "pollFilterVoted" }, + { key: "OPEN", i18n: "pollFilterOpen" }, + { key: "CLOSED", i18n: "pollFilterClosed" } +]; + +const safeArr = (v) => (Array.isArray(v) ? v : []); + +const pct = (n, total) => (total > 0 ? Math.round((Number(n) || 0) / total * 100) : 0); +const letterOf = (i) => { + let n = Number(i) || 0; + let out = ""; + do { + out = String.fromCharCode(65 + (n % 26)) + out; + n = Math.floor(n / 26) - 1; + } while (n >= 0); + return out; +}; +const pctStep = (n, total) => Math.round(pct(n, total) / 5) * 5; + +const outcomeOf = (poll) => { + if (!poll.totalVoters) return i18n.pollNoVotesYet; + const max = Math.max(...poll.options.map(o => poll.counts[o] || 0)); + if (!max) return i18n.pollNoVotesYet; + const winners = poll.options + .map((o, i) => ({ o, letter: letterOf(i) })) + .filter(({ o }) => (poll.counts[o] || 0) === max); + if (winners.length > 1) return `${i18n.pollTie}: ${winners.map(w => `${w.letter})`).join(", ")}`; + return `${winners[0].letter}) ${pct(max, poll.totalVoters)}%`; +}; + +const renderStatusChip = (poll) => + renderStateChip(poll.status === "OPEN" ? "whole" : "mutuals", "◆", + poll.status === "OPEN" ? i18n.pollStatusOpen : i18n.pollStatusClosed); + +const renderModeChips = (poll) => [ + poll.anonymous ? renderStateChip("mutuals", "◈", i18n.pollAnonymous) : null, + poll.multiple ? renderStateChip("whole", "☰", i18n.pollMultiple) : null +].filter(Boolean); + +const renderResults = (poll) => + table({ class: "poll-results" }, + ...poll.options.map((opt, i) => + tr({ class: poll.myChoices.includes(opt) ? "poll-result-row poll-result-mine" : "poll-result-row" }, + td({ class: "poll-result-option" }, `${letterOf(i)})`), + td({ class: "poll-result-bar" }, + div({ class: "poll-bar" }, + div({ class: `poll-bar-fill poll-bar-fill-${pctStep(poll.counts[opt], poll.totalVoters)} poll-hue-${i % 8}` }) + ) + ), + td({ class: "poll-result-count" }, + `${poll.counts[opt] || 0} (${pct(poll.counts[opt], poll.totalVoters)}%)`) + ) + ) + ); + +const renderBallot = (poll, returnTo, basePath, extra = null) => { + if (poll.status === "CLOSED" || poll.hasVoted) return null; + const inputType = poll.multiple ? "checkbox" : "radio"; + return form({ method: "POST", action: `${basePath}/vote/${encodeURIComponent(poll.id)}`, class: "poll-ballot" }, + input({ type: "hidden", name: "returnTo", value: returnTo }), + ...poll.options.map((opt, i) => + div({ class: "poll-option-row" }, + label({ for: `opt-${i}-${poll.id}` }, + input({ + type: inputType, name: "choices", value: opt, id: `opt-${i}-${poll.id}`, + ...(poll.myChoices.includes(opt) ? { checked: true } : {}) + }), + " ", + `${letterOf(i)}) ${opt}` + ) + ) + ), + div({ class: "poll-ballot-actions" }, + button({ type: "submit", class: "filter-btn" }, i18n.pollVoteButton), + ...(Array.isArray(extra) ? extra.filter(Boolean) : []) + ) + ); +}; + +const renderPollCard = (poll, filter, spreadInfo, basePath = "/polls") => { + const href = `${basePath}/${encodeURIComponent(poll.id)}`; + const isOwn = String(poll.author) === String(userId); + const chips = [ + renderStatusChip(poll), + ...renderModeChips(poll), + poll.deadline + ? renderStateChip(poll.status === "OPEN" ? "whole" : "mutuals", "◷", `${i18n.pollDeadline}: ${moment(poll.deadline).format("YYYY/MM/DD HH:mm")}`) + : null, + renderLifespanChip(poll.lifetime, i18n) + ].filter(Boolean); + return div({ class: "tribe-card poll-card" + (isOwn ? " own-content" : "") }, + div({ class: "card-header activity-card-header" }, + span(), + renderContentActions(poll.id, href, { spread: spreadInfo || null, author: poll.author, favKind: 'polls', isFavorite: poll.isFavorite, reportTitle: poll.question, deleteAction: isOwn ? contentDeleteAction('poll', poll.id) : undefined }) + ), + div({ class: "tribe-card-body" }, + div({ class: "shop-title-row" }, + h2({ class: "tribe-card-title" }, a({ href }, poll.question)) + ), + chips.length ? div({ class: "card-chips-row" }, ...chips) : null, + div({ class: "tribe-card-members" }, + span({ class: "tribe-members-count" }, `${i18n.pollVoters}: ${poll.totalVoters}`) + ) + ) + ); +}; + +const renderCreateForm = (poll = null, params = {}) => { + const isEdit = !!poll; + const action = isEdit ? `/polls/update/${encodeURIComponent(poll.id)}` : "/polls/create"; + return section( + params.spreadWarning || null, + div({ class: "div-center audio-form" }, + h2(isEdit ? i18n.pollEditTitle : i18n.pollCreateTitle), + form({ method: "POST", action }, + label(i18n.pollQuestion), + br(), + input({ type: "text", name: "question", required: true, maxlength: "300", value: isEdit ? poll.question : "", placeholder: i18n.pollQuestionPlaceholder }), + br(), br(), + label(i18n.pollOptions), + br(), + textarea({ + name: "options", + rows: String(MAX_OPTIONS), + required: true, + maxlength: String(MAX_OPTIONS * (MAX_OPTION_LENGTH + 1)) + }, isEdit ? poll.options.join("\n") : ""), + br(), + div({ class: "poll-switch" }, + input({ type: "hidden", name: "anonymous", value: "0" }), + label( + input({ type: "checkbox", name: "anonymous", value: "1", ...(isEdit && poll.anonymous ? { checked: true } : {}) }), + " ", i18n.pollAnonymousLabel + ) + ), + div({ class: "poll-switch" }, + input({ type: "hidden", name: "multiple", value: "0" }), + label( + input({ type: "checkbox", name: "multiple", value: "1", ...(isEdit && poll.multiple ? { checked: true } : {}) }), + " ", i18n.pollMultipleLabel + ) + ), + br(), + label(i18n.pollDeadline), + br(), + input({ + type: "datetime-local", name: "deadline", + min: moment().format("YYYY-MM-DDTHH:mm"), + value: isEdit && poll.deadline ? moment(poll.deadline).format("YYYY-MM-DDTHH:mm") : "" + }), + br(), br(), + label(i18n.pollTags), + br(), + input({ type: "text", name: "tags", value: isEdit ? poll.tags.join(", ") : "" }), + br(), br(), + button({ type: "submit", class: "create-button" }, isEdit ? i18n.pollUpdateButton : i18n.pollPublishButton) + ) + ) + ); +}; + +const renderComments = (poll, comments, basePath) => { + const href = `${basePath}/${encodeURIComponent(poll.id)}`; + return renderSharedCommentsSection({ + action: `${href}/comments`, + comments: comments, + returnTo: href + }); +}; + +const renderFilterBar = (filter, q, showSearch = true, items = null, censusList = null) => { + const emptyMod = items !== null && moduleIsEmpty(items, filter, "ALL", q); + const censusPl = Array.isArray(censusList) ? censusList : (Array.isArray(items) ? items : []); + const pollChip = (x) => { + const m = x.key; + if (m === filter) return true; + if (m === "TOP") return censusPl.length > 0; + if (m === "MINE") return censusPl.some(pl => String(pl.author || pl.createdBy) === String(userId)); + if (m === "RECENT") return censusPl.length > 0; + if (m === "VOTED") return censusPl.some(pl => pl.hasVoted || safeArr(pl.myChoices).length > 0); + if (m === "OPEN" || m === "CLOSED") return censusPl.some(pl => String(pl.status || "OPEN").toUpperCase() === m); + return true; + }; + return section( + div({ class: "filters" }, + form({ method: "GET", action: "/polls", class: "ui-toolbar ui-toolbar--filters" }, + ...(emptyMod ? [] : FILTERS.filter(pollChip).map(f => + button({ type: "submit", name: "filter", value: f.key, class: filter === f.key ? "filter-btn active" : "filter-btn" }, String(i18n[f.i18n]).toUpperCase()) + )), + button({ type: "submit", name: "filter", value: "CREATE", class: "create-button" }, i18n.pollCreateButton) + ) + ), + showSearch && !emptyMod ? div({ class: "filters activity-filter-chips activity-toolbar-row" }, + items ? renderModuleStatsBy(items, pl => String(pl.status || 'OPEN').toUpperCase(), [{ value: 'OPEN', label: i18n.pollStatusOpen }, { value: 'CLOSED', label: i18n.pollStatusClosed }]) : null, + form({ method: "GET", action: "/polls", class: "filter-box" }, + input({ type: "hidden", name: "filter", value: filter }), + input({ type: "text", name: "q", value: q || "", placeholder: i18n.pollSearchPlaceholder, class: "filter-box__input" }), + div({ class: "filter-box__controls" }, + button({ type: "submit", class: "filter-box__button" }, i18n.searchButton) + ) + ) + ) : null + ); +}; + +exports.pollsView = async (polls = [], filter = "ALL", params = {}) => { + const mode = String(filter).toUpperCase(); + const spreadMap = params.spreadMap instanceof Map ? params.spreadMap : new Map(); + if (mode === "CREATE" || mode === "EDIT") { + const editing = params.poll || null; + if (editing) params = { ...params, spreadWarning: await renderSpreadEditWarning(editing.id) }; + return template( + i18n.pollsTitle, + section(div({ class: "tags-header module-header-line" }, h2(i18n.pollsTitle), p(i18n.pollsDescription))), + renderFilterBar(mode, params.q, false), + renderCreateForm(editing, params) + ); + } + return template( + i18n.pollsTitle, + section(div({ class: "tags-header module-header-line" }, h2(i18n.pollsTitle), p(i18n.pollsDescription))), + renderFilterBar(mode, params.q, true, polls, params.censusList), + section( + polls.length + ? div({ class: "jobs-grid" }, ...paged(polls).map(pl => renderPollCard(pl, mode, spreadMap.get(pl.id)))) + : div({ class: "no-content-box" }, p({ class: "no-content" }, i18n.pollsNoItems)) + ) + ); +}; + +exports.singlePollView = async (poll, comments = [], params = {}) => { + const basePath = "/polls"; + const href = `${basePath}/${encodeURIComponent(poll.id)}`; + const isOwn = String(poll.author) === String(userId); + const chips = [renderStatusChip(poll), ...renderModeChips(poll), renderLifespanChip(poll.lifetime, i18n)].filter(Boolean); + + const infoRows = []; + const pushRow = (labelText, valueNode) => + infoRows.push(tr( + td({ class: "tribe-info-label" }, labelText), + td({ class: "tribe-info-value" }, valueNode) + )); + if (poll.deadline) pushRow(i18n.pollDeadline, moment(poll.deadline).format("YYYY/MM/DD HH:mm")); + pushRow(i18n.pollOutcome, outcomeOf(poll)); + + const tagsNode = poll.tags.length + ? div({ class: "card-tags" }, poll.tags.map(tag => + a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link" }, `#${tag}`))) + : null; + + const ownerActions = isOwn && poll.totalVoters === 0 && poll.status === "OPEN" + ? div({ class: "tribe-side-actions" }, + form({ method: "GET", action: "/polls" }, + input({ type: "hidden", name: "filter", value: "EDIT" }), + input({ type: "hidden", name: "id", value: poll.id }), + button({ type: "submit", class: "update-btn" }, i18n.pollUpdateButton) + ) + ) + : null; + + const pollSide = div({ class: "tribe-side" }, + div({ class: "card-header activity-card-header" }, + renderContentActions(poll.id, href, { spread: params.spreads || null, author: poll.author, favKind: 'polls', isFavorite: poll.isFavorite, returnTo: href, reportTitle: poll.question, deleteAction: isOwn ? contentDeleteAction('poll', poll.id) : undefined }) + ), + div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, poll.question)), + chips.length ? div({ class: "card-chips-row" }, ...chips) : null, + table({ class: "tribe-info-table jobs-info-table" }, ...infoRows, ...renderCreatedRows(poll.createdAt, poll.author)), + tagsNode, + div({ class: "tribe-card-members" }, + span({ class: "tribe-members-count" }, `${i18n.pollVoters}: ${poll.totalVoters}`) + ), + isOwn + ? div({ class: "tribe-side-actions housing-status-row" }, + span({ class: "card-label" }, `${i18n.pollStatusLabel}: `), + renderStatusChip(poll), + poll.status === "OPEN" + ? form({ method: "POST", action: `/polls/close/${encodeURIComponent(poll.id)}` }, + button({ type: "submit", class: "status-btn project-control-btn" }, i18n.pollCloseButton) + ) + : null + ) + : null, + ownerActions + ); + + const pollMain = div({ class: "tribe-main" }, + div({ class: "job-section" }, + h2({ class: "job-section-title" }, i18n.pollOutcome), + renderBallot(poll, href, basePath), + renderResults(poll) + ), + poll.votersHidden + ? null + : (poll.voters.length + ? div({ class: "card-assigned-list" }, ...poll.voters.map(v => userLink(v))) + : null), + poll.tribeId ? null : renderEngagement(poll.id, + renderOpinionsVoting('/polls/opinions', poll.id, poll.opinions, href, poll.opinions_inhabitants), + renderComments(poll, comments, basePath) + ) + ); + + return template( + poll.question, + section(div({ class: "tags-header module-header-line" }, h2(i18n.pollsTitle), p(i18n.pollsDescription))), + renderFilterBar(String(params.filter || "ALL").toUpperCase(), params.q, false), + section(div({ class: "tribe-details" }, pollSide, pollMain)) + ); +}; + +exports.renderPollCard = renderPollCard; +exports.renderResults = renderResults; +exports.renderBallot = renderBallot; +exports.outcomeOf = outcomeOf; +exports.letterOf = letterOf; diff --git a/src/views/projects_view.js b/src/views/projects_view.js index 8c17d80..df75a33 100644 --- a/src/views/projects_view.js +++ b/src/views/projects_view.js @@ -1,21 +1,24 @@ const { form, button, div, h2, p, section, input, label, textarea, br, a, span, select, option, img, ul, li, table, thead, tbody, tr, th, td, progress, video, audio } = require("../server/node_modules/hyperaxe") -const { template, i18n, renderOpinionsVoting, userLink, renderStateChip, renderLifespanChip, renderEcoTax, renderSpreadButton, renderContentActions } = require("./main_views") +const { renderZoomableImage } = require("./gallery_view") +const { renderCommentsSection: renderSharedCommentsSection } = require("./comments_view"); +const { clearnetItemHref, template, i18n, renderOpinionsVoting, renderEngagement, userLink, renderStateChip, renderLifespanChip, renderEcoTax, renderSpreadButton, renderContentActions, renderSpreadEditWarning, renderSubscriptionBox, renderModuleStatsBy, moduleIsEmpty, renderEcoValueChip, contentDeleteAction, paged, renderCreatedRows } = require("./main_views") const moment = require("../server/node_modules/moment") const { config } = require("../server/SSB_server.js") -const { renderUrl } = require("../backend/renderUrl") +const { renderStyledText } = require("../backend/renderStyledText") const { renderMapLocationUrl, renderMapEmbed, renderMapLocationVisitLabel, renderMapEmbedWithZoom } = require("./maps_view") +const { renderReachChip, renderClearnetSelector, renderClearnetSwitch } = require("./clearnet_view") const renderMediaBlob = (value, attrs = {}) => { if (!value) return null const s = String(value).trim() if (!s) return null - if (s.startsWith('&')) return img({ src: `/blob/${encodeURIComponent(s)}`, ...attrs }) + if (s.startsWith('&')) return renderZoomableImage(`/blob/${encodeURIComponent(s)}`, { imgClass: attrs.class || 'post-image', alt: attrs.alt || '' }) const mVideo = s.match(/\[video:[^\]]*\]\(\s*(&[^)\s]+\.sha256)\s*\)/) - if (mVideo) return video({ controls: true, class: attrs.class || 'post-video', src: `/blob/${encodeURIComponent(mVideo[1])}` }) + if (mVideo) return video({ controls: true, class: attrs.class || 'post-video', src: `/blob/${encodeURIComponent(mVideo[1])}`, preload: 'metadata' }) const mAudio = s.match(/\[audio:[^\]]*\]\(\s*(&[^)\s]+\.sha256)\s*\)/) if (mAudio) return audio({ controls: true, class: attrs.class || 'post-audio', src: `/blob/${encodeURIComponent(mAudio[1])}` }) const mImg = s.match(/!\[[^\]]*\]\(\s*(&[^)\s]+\.sha256)\s*\)/) - if (mImg) return img({ src: `/blob/${encodeURIComponent(mImg[1])}`, class: attrs.class || 'post-image' }) + if (mImg) return renderZoomableImage(`/blob/${encodeURIComponent(mImg[1])}`, { imgClass: attrs.class || 'post-image', alt: attrs.alt || '' }) return null } @@ -23,19 +26,20 @@ const userId = config.keys.id const FILTERS = [ { key: "ALL", i18n: "projectFilterAll", title: "projectAllTitle" }, + { key: "RECENT", i18n: "projectFilterRecent", title: "projectRecentTitle" }, { key: "MINE", i18n: "projectFilterMine", title: "projectMineTitle" }, { key: "APPLIED", i18n: "projectFilterApplied", title: "projectAppliedTitle" }, { key: "ACTIVE", i18n: "projectFilterActive", title: "projectActiveTitle" }, { key: "PAUSED", i18n: "projectFilterPaused", title: "projectPausedTitle" }, { key: "COMPLETED", i18n: "projectFilterCompleted", title: "projectCompletedTitle" }, { key: "FOLLOWING", i18n: "projectFilterFollowing", title: "projectFollowingTitle" }, - { key: "RECENT", i18n: "projectFilterRecent", title: "projectRecentTitle" }, { key: "TOP", i18n: "projectFilterTop", title: "projectTopTitle" }, { key: "BACKERS", i18n: "projectFilterBackers", title: "projectBackersLeaderboardTitle" } ] const safeArr = (v) => (Array.isArray(v) ? v : []) const safeText = (v) => String(v || "").trim() +const clearnetEligible = (pr) => String(pr.status || "").toUpperCase() !== "CANCELLED" const toNum = (v) => { const s = v === null || v === undefined ? "" : String(v) @@ -77,7 +81,7 @@ const renderCardFieldRich = (labelText, children) => const renderProgressBlock = (labelText, valueText, value, maxValue) => div( - { class: "confirmations-block progress-block" }, + { class: "confirmations-block progress-block project-progress-block" }, div( { class: "card-field" }, span({ class: "card-label" }, labelText), @@ -214,6 +218,7 @@ const renderPledgeBox = (project, filter, isAuthor) => { { method: "POST", action: `/projects/pledge/${encodeURIComponent(project.id)}` }, input({ type: "hidden", name: "returnTo", value: rt }), input({ type: "number", name: "amount", min: "0.01", step: "0.01", required: true, placeholder: i18n.projectPledgePlaceholder }), + renderEcoValueChip(), select( { name: "milestoneOrBounty" }, option({ value: "" }, i18n.projectSelectMilestoneOrBounty), @@ -259,7 +264,7 @@ const renderMilestonesAndBounties = (project, filter, editable) => { { class: "milestone-head" }, span({ class: "milestone-title" }, m.title), m.dueDate ? span({ class: "chip chip-due" }, `${i18n.projectMilestoneDue}: ${moment(m.dueDate).format("YYYY/MM/DD HH:mm")}`) : null, - safeText(m.description) ? p(...renderUrl(m.description)) : null, + safeText(m.description) ? p(...renderStyledText(m.description)) : null, editable && !m.done ? form( { method: "POST", action: `/projects/milestones/complete/${encodeURIComponent(project.id)}/${idx}` }, @@ -283,7 +288,7 @@ const renderMilestonesAndBounties = (project, filter, editable) => { span({ class: "bounty-title" }, b.title), span({ class: "bounty-amount" }, `${b.amount} ECO`) ), - safeText(b.description) ? p(...renderUrl(b.description)) : null, + safeText(b.description) ? p(...renderStyledText(b.description)) : null, renderCardField(i18n.projectBountyStatus + ":", statusText), b.claimedBy ? renderCardField(i18n.projectBountyClaimedBy + ":", userLink(b.claimedBy)) : null, !editable && !b.done && !b.claimedBy && project.author !== userId @@ -308,8 +313,8 @@ const renderMilestonesAndBounties = (project, filter, editable) => { br(), select( { name: "milestoneIndex" }, - option({ value: "", selected: b.milestoneIndex == null }, "-"), - ...milestones.map((m2, idx2) => option({ value: String(idx2), selected: b.milestoneIndex === idx2 }, m2.title)) + option({ value: "", ...(b.milestoneIndex == null ? { selected: true } : {})}, "-"), + ...milestones.map((m2, idx2) => option({ value: String(idx2), ...(b.milestoneIndex === idx2 ? { selected: true } : {})}, m2.title)) ), br(), br(), @@ -342,7 +347,7 @@ const renderMilestonesAndBounties = (project, filter, editable) => { span({ class: "bounty-title" }, b.title), span({ class: "bounty-amount" }, `${b.amount} ECO`) ), - safeText(b.description) ? p(...renderUrl(b.description)) : null, + safeText(b.description) ? p(...renderStyledText(b.description)) : null, renderCardField(i18n.projectBountyStatus + ":", statusText), b.claimedBy ? renderCardField(i18n.projectBountyClaimedBy + ":", userLink(b.claimedBy)) : null, !editable && !b.done && !b.claimedBy && project.author !== userId @@ -400,7 +405,11 @@ const renderProjectList = exports.renderProjectList = (projects, filter, spreadM : null const chips = [ renderProjectStatusChip(pr.status), - renderLifespanChip(pr.lifetime, i18n) + renderLifespanChip(pr.lifetime, i18n), + pr.clearnet === true && clearnetEligible(pr) ? renderReachChip(true, i18n, clearnetItemHref('projects', pr.title, pr.id || pr.key)) : null, + pr.subscriptionIn === true + ? renderStateChip("mutuals", "✉", i18n.subscriptionOn) + : (pr.subscriptionIn === false ? renderStateChip("closed", "✉", i18n.subscriptionOff) : null) ].filter(Boolean) const isOwn = pr.author && String(pr.author) === String(userId) @@ -408,7 +417,7 @@ const renderProjectList = exports.renderProjectList = (projects, filter, spreadM div( { class: "card-header activity-card-header" }, span(), - renderContentActions(pr.id || pr.key, `/projects/${encodeURIComponent(pr.id)}`) + renderContentActions(pr.id || pr.key, `/projects/${encodeURIComponent(pr.id)}`, { spread: spreadMap.get(pr.id || pr.key) || null, author: pr.author, favKind: 'projects', isFavorite: pr.isFavorite, reportTitle: pr.title, deleteAction: isOwn ? contentDeleteAction('project', pr.id || pr.key) : undefined }) ), div({ class: "card-section project-card-body" }, heroNode, @@ -419,22 +428,21 @@ const renderProjectList = exports.renderProjectList = (projects, filter, spreadM ), chips.length ? div({ class: "card-chips-row" }, ...chips) : null, goal > 0 - ? div({ class: "card-date-highlight" }, `${pr.goal} ECO`) + ? div({ class: "price-chip" }, `${pr.goal} ECO`) : null, goal > 0 ? renderProgressBlock(i18n.projectFunding + ":", `${fundingPct}%`, fundingPct, 100) : null, div({ class: "tribe-card-members" }, span({ class: "tribe-members-count" }, `${i18n.projectFollowers}: ${followersCount(pr)}`) - ), - div({ class: "card-spread-centered" }, renderSpreadButton(pr.id || pr.key, spreadMap.get(pr.id || pr.key))) + ) ) ) }) ) } -const renderProjectForm = (project, mode) => { +const renderProjectForm = (project, mode, spreadWarning = null) => { const pr = project || {} const isEdit = mode === "edit" const nowLocal = moment().format("YYYY-MM-DDTHH:mm") @@ -444,6 +452,7 @@ const renderProjectForm = (project, mode) => { return div( { class: "div-center project-form" }, + isEdit ? spreadWarning : null, form( { action: isEdit ? `/projects/update/${encodeURIComponent(pr.id)}` : "/projects/create", @@ -453,21 +462,25 @@ const renderProjectForm = (project, mode) => { input({ type: "hidden", name: "returnTo", value: returnTo }), label(i18n.projectTitle), br(), - input({ type: "text", name: "title", required: true, placeholder: i18n.projectTitlePlaceholder, value: pr.title || "" }), + input({ type: "text", name: "title", maxlength: "100", required: true, placeholder: i18n.projectTitlePlaceholder, value: pr.title || "" }), br(), label(i18n.projectDescription), br(), - textarea({ name: "description", rows: "6", required: true, placeholder: i18n.projectDescriptionPlaceholder }, pr.description || ""), + textarea({ maxlength: "5000", name: "description", rows: "6", required: true, placeholder: i18n.projectDescriptionPlaceholder }, pr.description || ""), br(), label(i18n.projectImage), br(), input({ type: "file", name: "image" }), br(), + label(i18n.uploadMedia), br(), + input({ type: "file", name: "blob" }), + br(), pr.image ? renderMediaBlob(pr.image) : null, br(), label(i18n.projectGoal), br(), input({ type: "number", step: "0.01", min: "0.01", name: "goal", required: true, placeholder: i18n.projectGoalPlaceholder, value: pr.goal || "" }), + renderEcoValueChip(), br(), br(), label(i18n.mapLocationTitle || "Map Location"), @@ -487,7 +500,7 @@ const renderProjectForm = (project, mode) => { br(), label(i18n.projectMilestoneDescription), br(), - textarea({ name: "milestoneDescription", rows: "3", placeholder: i18n.projectMilestoneDescriptionPlaceholder }), + textarea({ maxlength: "5000", name: "milestoneDescription", rows: "3", placeholder: i18n.projectMilestoneDescriptionPlaceholder }), br(), label(i18n.projectMilestoneTargetPercent), br(), @@ -499,43 +512,70 @@ const renderProjectForm = (project, mode) => { input({ type: "datetime-local", name: "milestoneDueDate", min: nowLocal, max: milestoneMax }), br(), br(), + !isEdit || clearnetEligible(pr) ? renderClearnetSelector(isEdit ? !!pr.clearnet : false, i18n) : null, button({ type: "submit" }, isEdit ? i18n.projectUpdateButton : i18n.projectCreateButton) ) ) } exports.projectsView = async (projectsOrForm, filter, _unused, params = {}) => { + if (String(filter).toUpperCase() === "EDIT") { + const editing = Array.isArray(projectsOrForm) ? projectsOrForm[0] : projectsOrForm + params = { ...params, spreadWarning: await renderSpreadEditWarning(editing && (editing.id || editing.key)) } + } const f = String(filter || "ALL").toUpperCase() const filterObj = FILTERS.find((x) => x.key === f) || FILTERS[0] const sectionTitle = i18n[filterObj.title] || i18n.projectAllTitle - const { renderReachChip: renderReachChipProjects } = require('./clearnet_view'); - const viewerClearnetProjects = !!(params.viewerPrefs && params.viewerPrefs.clearnetProjects); + const emptyMod = moduleIsEmpty(Array.isArray(projectsOrForm) ? projectsOrForm : [], f, "ALL", params.q); + const censusP = Array.isArray(params.censusList) ? params.censusList : (Array.isArray(projectsOrForm) ? projectsOrForm : []); + const projChip = (x) => { + const m = x.key; + if (m === f) return true; + if (m === "TOP") return censusP.length > 0; + if (m === "MINE") return censusP.some(pr => String(pr.author) === String(userId)); + if (m === "APPLIED") return censusP.some(pr => String(pr.author) !== String(userId) && [pr.members, pr.participants, pr.applicants].some(l => Array.isArray(l) && l.includes(userId))); + if (m === "ACTIVE" || m === "PAUSED" || m === "COMPLETED") return censusP.some(pr => String(pr.status || "ACTIVE").toUpperCase() === m); + if (m === "FOLLOWING") return censusP.some(pr => Array.isArray(pr.followers) && pr.followers.includes(userId)); + if (m === "RECENT") return censusP.length > 0; + if (m === "BACKERS") return censusP.some(pr => safeArr(pr.backers).length > 0); + return true; + }; return template( i18n.projectsTitle, section( - div({ class: "tags-header" }, + div({ class: "tags-header module-header-line" }, h2(sectionTitle), p(i18n.projectsDescription) ), - div({ class: "shop-title-row" }, renderReachChipProjects(viewerClearnetProjects, i18n)), - br(), div( { class: "filters" }, form( { method: "GET", action: "/projects", class: "ui-toolbar ui-toolbar--filters" }, - FILTERS.map((x) => button({ type: "submit", name: "filter", value: x.key, class: f === x.key ? "filter-btn active" : "filter-btn" }, i18n[x.i18n])) + (emptyMod ? [] : FILTERS.filter(projChip).map((x) => button({ type: "submit", name: "filter", value: x.key, class: f === x.key ? "filter-btn active" : "filter-btn" }, String(i18n[x.i18n]).toUpperCase()))) .concat(button({ type: "submit", name: "filter", value: "CREATE", class: "create-button" }, i18n.projectCreateProject)) ) ), + f === "CREATE" || f === "EDIT" + ? null + : emptyMod ? null : div({ class: "filters activity-filter-chips activity-toolbar-row" }, + renderModuleStatsBy(projectsOrForm, pr => String(pr.status || 'ACTIVE').toUpperCase(), [{ value: 'ACTIVE', label: i18n.projectStatusACTIVE }, { value: 'PAUSED', label: i18n.projectStatusPAUSED }, { value: 'COMPLETED', label: i18n.projectStatusCOMPLETED }, { value: 'CANCELLED', label: i18n.projectStatusCANCELLED }]), + form({ method: "GET", action: "/projects", class: "filter-box" }, + input({ type: "hidden", name: "filter", value: f }), + input({ type: "text", name: "q", value: safeText(params.q), placeholder: i18n.projectSearchPlaceholder, class: "filter-box__input" }), + div({ class: "filter-box__controls" }, + button({ type: "submit", class: "filter-box__button" }, i18n.searchButton) + ) + ) + ), f === "CREATE" || f === "EDIT" ? (() => { - const prToEdit = f === "EDIT" ? (safeArr(projectsOrForm)[0] || {}) : {} - return renderProjectForm(prToEdit, f === "EDIT" ? "edit" : "create") + const prToEdit = f === "EDIT" ? (safeArr(projectsOrForm)[0] || {}) : (params.prefill || {}) + return renderProjectForm(prToEdit, f === "EDIT" ? "edit" : "create", params.spreadWarning) })() : (f === "BACKERS" ? renderBackersLeaderboard(projectsOrForm) - : div({ class: "projects-list" }, renderProjectList(projectsOrForm, f, params.spreadMap)) + : div({ class: "projects-list" }, renderProjectList(paged(projectsOrForm), f, params.spreadMap)) ) ) ) @@ -548,6 +588,8 @@ exports.singleProjectView = async (project, filter, comments, params = {}) => { const isFollower = safeArr(pr.followers).includes(userId) const statusUpper = String(pr.status || "ACTIVE").toUpperCase() + const canClearnet = clearnetEligible(pr) + const isClearnet = canClearnet && !!pr.clearnet const pctRaw = toNum(pr.progress) const pct = clamp(Math.round(Number.isFinite(pctRaw) ? pctRaw : 0), 0, 100) const goal = Math.max(0, toNum(pr.goal) || 0) @@ -560,16 +602,17 @@ exports.singleProjectView = async (project, filter, comments, params = {}) => { renderProjectStatusChip(pr.status), isFollower ? renderStateChip("whole", "★", i18n.projectFollowing || "FOLLOWING") : null, renderLifespanChip(pr.lifetime, i18n), - renderEcoTax(pr.msgSize, pr.id || pr.key) + renderEcoTax(pr.msgSize, pr.id || pr.key), + renderReachChip(isClearnet, i18n, isClearnet ? clearnetItemHref('projects', pr.title, pr.id || pr.key) : null), + isAuthor && canClearnet ? renderClearnetSwitch('projects', pr.rootId || pr.id || pr.key, isClearnet) : null, + pr.subscription + ? ((isAuthor || pr.subscription.subscribed === true) + ? renderStateChip("mutuals", "✉", i18n.subscriptionOn) + : renderStateChip("closed", "✉", i18n.subscriptionOff)) + : null ].filter(Boolean) const sideActions = [] - if (!isAuthor && pr.author) { - sideActions.push(form({ method: "GET", action: "/pm" }, - input({ type: "hidden", name: "recipients", value: pr.author }), - button({ type: "submit", class: "filter-btn" }, i18n.privateMessage) - )) - } if (!isAuthor && statusUpper === "ACTIVE") { sideActions.push(isFollower ? form({ method: "POST", action: `/projects/unfollow/${encodeURIComponent(pr.id)}` }, @@ -584,21 +627,17 @@ exports.singleProjectView = async (project, filter, comments, params = {}) => { } if (isAuthor) { sideActions.push(form({ method: "GET", action: `/projects/edit/${encodeURIComponent(pr.id)}` }, - button({ class: "update-btn", type: "submit" }, i18n.projectUpdateButton) - )) - sideActions.push(form({ method: "POST", action: `/projects/delete/${encodeURIComponent(pr.id)}` }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - button({ class: "delete-btn", type: "submit" }, i18n.projectDeleteButton) + button({ class: "tribe-action-btn", type: "submit" }, i18n.projectUpdateButton) )) sideActions.push(form( { method: "POST", action: `/projects/status/${encodeURIComponent(pr.id)}`, class: "project-control-form project-control-form--status" }, input({ type: "hidden", name: "returnTo", value: returnTo }), select( { name: "status", class: "project-control-select" }, - option({ value: "ACTIVE", selected: statusUpper === "ACTIVE" }, i18n.projectStatusACTIVE), - option({ value: "PAUSED", selected: statusUpper === "PAUSED" }, i18n.projectStatusPAUSED), - option({ value: "COMPLETED", selected: statusUpper === "COMPLETED" }, i18n.projectStatusCOMPLETED), - option({ value: "CANCELLED", selected: statusUpper === "CANCELLED" }, i18n.projectStatusCANCELLED) + option({ value: "ACTIVE", ...(statusUpper === "ACTIVE" ? { selected: true } : {})}, i18n.projectStatusACTIVE), + option({ value: "PAUSED", ...(statusUpper === "PAUSED" ? { selected: true } : {})}, i18n.projectStatusPAUSED), + option({ value: "COMPLETED", ...(statusUpper === "COMPLETED" ? { selected: true } : {})}, i18n.projectStatusCOMPLETED), + option({ value: "CANCELLED", ...(statusUpper === "CANCELLED" ? { selected: true } : {})}, i18n.projectStatusCANCELLED) ), button({ class: "status-btn project-control-btn", type: "submit" }, i18n.projectSetStatus) )) @@ -611,17 +650,31 @@ exports.singleProjectView = async (project, filter, comments, params = {}) => { } const projectSide = div({ class: "tribe-side" }, + div({ class: "card-header activity-card-header" }, + renderContentActions(pr.id || pr.key, `/projects/${encodeURIComponent(pr.id)}`, { spread: params.spreads || null, author: pr.author, favKind: 'projects', isFavorite: pr.isFavorite, reportTitle: pr.title, returnTo: `/projects/${encodeURIComponent(pr.id)}`, deleteAction: isAuthor ? contentDeleteAction('project', pr.id || pr.key) : undefined }) + ), div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, pr.title) ), chips.length ? div({ class: "card-chips-row" }, ...chips) : null, - div({ class: "card-spread-centered" }, renderSpreadButton(pr.id || pr.key, params.spreads)), pr.image ? renderMediaBlob(pr.image, { class: "tribe-detail-image" }) : null, - div({ class: "job-price-line card-salary" }, `${pr.goal || 0} ECO`), + div({ class: "price-chip" }, `${pr.goal || 0} ECO`), div({ class: "job-price-line card-salary" }, `${i18n.projectFollowers}: ${followersCount(pr)}`), renderProgressBlock(i18n.projectProgress + ":", `${pct}%`, pct, 100), goal > 0 ? renderProgressBlock(i18n.projectFunding + ":", `${fundingPct}%`, fundingPct, 100) : null, - sideActions.length ? div({ class: "tribe-side-actions" }, ...sideActions) : null + table({ class: "tribe-info-table" }, ...renderCreatedRows(pr.createdAt, pr.author)), + sideActions.length ? div({ class: "tribe-side-actions" }, ...sideActions) : null, + pr.subscription + ? renderSubscriptionBox({ + target: pr.rootId || pr.id, + scope: "projects", + subscribed: pr.subscription.subscribed === true, + count: pr.subscription.count, + isOwner: isAuthor, + canWrite: isAuthor, + returnTo + }) + : null ) const projectMain = div({ class: "tribe-main" }, @@ -629,65 +682,34 @@ exports.singleProjectView = async (project, filter, comments, params = {}) => { safeText(pr.description) ? div({ class: "job-section" }, h2({ class: "job-section-title" }, i18n.projectDescription), - p({ class: "tribe-side-description" }, ...renderUrl(pr.description)) + p({ class: "tribe-side-description" }, ...renderStyledText(pr.description)) ) : null, pr.mapUrl ? div({ class: "job-section" }, renderMapEmbedWithZoom(params.mapData, pr.mapUrl, `/projects/${encodeURIComponent(pr.id || pr.key)}`, params.zoom)) : null, renderBudget(pr), renderBackers(pr, f), renderMilestonesAndBounties(pr, f, isAuthor), - renderFollowers(pr), renderPledgeBox(pr, f, isAuthor), - div({ class: "card-footer" }, - span({ class: "date-link" }, `${moment(pr.createdAt).format("YYYY/MM/DD HH:mm")} ${i18n.performed} `), - userLink(pr.author) + renderFollowers(pr), + renderEngagement(pr.id || pr.key, + renderOpinionsVoting('/projects/opinions', pr.id || pr.key, pr.opinions, null, pr.opinions_inhabitants), + renderSharedCommentsSection({ + action: `/projects/${encodeURIComponent(pr.id || pr.key)}/comments`, + comments, + returnTo: `/projects/${encodeURIComponent(pr.id || pr.key)}` + }) ), - renderOpinionsVoting('/projects/opinions', pr.id || pr.key, pr.opinions, null, pr.opinions_inhabitants), - div( - { id: "comments", class: "vote-comments-section" }, - div({ class: "comment-form-wrapper" }, - h2({ class: "comment-form-title" }, i18n.voteNewCommentLabel), - form( - { method: "POST", action: `/projects/${encodeURIComponent(pr.id || pr.key)}/comments`, class: "comment-form", enctype: "multipart/form-data" }, - textarea({ id: "comment-text", name: "text", rows: 4, class: "comment-textarea", placeholder: i18n.voteNewCommentPlaceholder }), - div({ class: "comment-file-upload" }, label(i18n.uploadMedia), br(), input({ type: "file", name: "blob" })), - br(), - button({ type: "submit", class: "comment-submit-btn" }, i18n.voteNewCommentButton) - ) - ), - (() => { - const visibleComments = (comments || []).filter(c => { - const t = c && c.value && c.value.content && c.value.content.text - return t && String(t).trim() - }) - return visibleComments.length - ? div({ class: "comments-list" }, - visibleComments.map((c) => { - const author = c?.value?.author || "" - const ts = c?.value?.timestamp || c?.timestamp - const absDate = ts ? moment(ts).format("YYYY/MM/DD HH:mm:ss") : "" - const relDate = ts ? moment(ts).fromNow() : "" - return div({ class: "comment-card" }, - div({ class: "comment-header" }, userLink(author)), - div({ class: "comment-date" }, span({ title: absDate }, relDate)), - div({ class: "comment-body" }, ...renderUrl(c?.value?.content?.text || "")) - ) - }) - ) - : p({ class: "votations-no-comments" }, i18n.voteNoCommentsYet) - })() - ) ) return template( i18n.projectsTitle, section( - div({ class: "tags-header" }, h2(i18n.projectsTitle), p(i18n.projectsDescription)), + div({ class: "tags-header module-header-line" }, h2(i18n.projectsTitle), p(i18n.projectsDescription)), div( { class: "filters" }, form( { method: "GET", action: "/projects", class: "ui-toolbar ui-toolbar--filters" }, - FILTERS.map((x) => button({ type: "submit", name: "filter", value: x.key, class: f === x.key ? "filter-btn active" : "filter-btn" }, i18n[x.i18n])) + FILTERS.map((x) => button({ type: "submit", name: "filter", value: x.key, class: f === x.key ? "filter-btn active" : "filter-btn" }, String(i18n[x.i18n]).toUpperCase())) .concat(button({ type: "submit", name: "filter", value: "CREATE", class: "create-button" }, i18n.projectCreateProject)) ) ), @@ -697,10 +719,10 @@ exports.singleProjectView = async (project, filter, comments, params = {}) => { } exports.clearnetProjectView = async (project) => { - const { escapeHtml: esc, blobUrl: cnBlob, renderClearnetPage } = require('./clearnet_view'); + const { escapeHtml: esc, renderRichText, renderKindTag, blobUrl: cnBlob, renderClearnetPage } = require('./clearnet_view'); const pr = project || {}; - const title = esc(pr.title || 'Project'); - const desc = esc(pr.description || ''); + const title = esc(pr.title || i18n.cnKindProject); + const desc = renderRichText(pr.description || ''); const goal = Math.max(0, toNum(pr.goal) || 0); const pledged = Math.max(0, toNum(pr.pledged) || 0); const fundingPct = goal > 0 ? Math.min(100, Math.round((pledged / goal) * 100)) : 0; @@ -709,17 +731,18 @@ exports.clearnetProjectView = async (project) => { const deadline = pr.deadline ? new Date(pr.deadline).toISOString().slice(0, 10) : ''; const milestones = Array.isArray(pr.milestones) ? pr.milestones.slice(0, 10) : []; const milestonesBlock = milestones.length - ? `

Milestones

    ${milestones.map(m => `
  1. ${esc(m.title || '')}${m.targetPercent ? ` — ${m.targetPercent}%` : ''}
  2. `).join('')}
` + ? `

${esc(i18n.projectMilestones)}

    ${milestones.map(m => `
  1. ${esc(m.title || '')}${m.targetPercent ? ` — ${m.targetPercent}%` : ''}
  2. `).join('')}
` : ''; const extraCss = ` .cn-prj-title{color:var(--fg);margin:0 0 12px 0;font-size:32px;font-weight:700} -.cn-prj-status{display:inline-block;background:var(--bg-sub);border:1px solid var(--fg);color:var(--fg);padding:6px 12px;border-radius:6px;font-weight:600;text-transform:uppercase;letter-spacing:1px;font-size:12px;margin-bottom:16px} +.cn-prj-status{display:inline-flex;align-items:center;background:var(--bg-sub);border:1px solid var(--fg);color:var(--fg);padding:6px 12px;border-radius:6px;font-weight:600;text-transform:uppercase;letter-spacing:1px;font-size:12px;margin:0} .cn-prj-img{display:block;max-width:100%;border:1px solid var(--border);border-radius:8px;margin-bottom:20px} .cn-prj-funding{background:var(--bg-sub);border:1px solid var(--border);border-radius:8px;padding:14px;margin-bottom:20px} .cn-prj-funding-label{color:var(--fg-dim);font-size:12px;text-transform:uppercase;letter-spacing:1px;margin-bottom:6px} .cn-prj-funding-amount{color:var(--fg);font-size:18px;font-weight:700;margin-bottom:8px} .cn-prj-bar{height:8px;background:#000;border-radius:4px;overflow:hidden} .cn-prj-bar-fill{height:100%;background:var(--fg);border-radius:4px} +${Array.from({ length: 21 }, (_, i) => `.cn-prj-bar-fill-${i * 5}{width:${i * 5}%}`).join('\n')} .cn-prj-section{margin-top:24px} .cn-prj-section h2{color:var(--fg);font-size:18px;text-transform:uppercase;letter-spacing:2px;margin:0 0 12px;padding-bottom:6px;border-bottom:1px solid var(--border)} .cn-prj-section p{color:var(--fg-soft);white-space:pre-wrap;line-height:1.6;font-size:15px;margin:0} @@ -733,22 +756,24 @@ exports.clearnetProjectView = async (project) => { const body = `

${title}

- ${esc(status)} + ${renderKindTag('project')} + ${esc(i18n['projectStatus' + status] || status)} ${pr.createdAt ? `📅 ${esc(new Date(pr.createdAt).toISOString().slice(0,10))}` : ''}
+
${projectImg ? `${title}` : ''} ${goal > 0 ? `
-
Funding
+
${esc(i18n.projectFunding)}
${pledged.toFixed(2)} / ${goal.toFixed(2)} ECO · ${fundingPct}%
-
- ${deadline ? `
Deadline: ${deadline}
` : ''} +
+ ${deadline ? `
${esc(i18n.deadline)}: ${deadline}
` : ''}
` : ''} - ${desc ? `

Description

${desc}

` : ''} + ${desc ? `

${esc(i18n.description)}

${desc}

` : ''} ${milestonesBlock} `; return renderClearnetPage({ - title: `${pr.title || 'Project'} — Oasis`, + title: `${pr.title || 'Project'} | Oasis`, ogTitle: pr.title || 'Project', ogDescription: pr.description || '', ogImage: projectImg, diff --git a/src/views/report_view.js b/src/views/report_view.js index c3f261c..3dc904f 100644 --- a/src/views/report_view.js +++ b/src/views/report_view.js @@ -1,20 +1,22 @@ -const { div, h2, p, section, button, form, a, textarea, br, input, img, span, label, select, option, video, audio, table, tr, td } = require("../server/node_modules/hyperaxe"); -const { template, i18n, renderOpinionsVoting, userLink, renderStateChip, renderLifespanChip, renderEcoTax, renderSpreadButton } = require("./main_views"); +const { hr, div, h2, p, section, button, form, a, textarea, br, input, img, span, label, select, option, video, audio, table, tr, td } = require("../server/node_modules/hyperaxe"); +const { renderCommentsSection: renderSharedCommentsSection } = require("./comments_view"); +const { template, i18n, renderOpinionsVoting, renderEngagement, renderStateChip, renderLifespanChip, renderEcoTax, renderSpreadButton, renderSpreadEditWarning, renderContentActions, renderDocumentActions, renderModuleStatsBy, moduleIsEmpty, contentDeleteAction, paged, renderCreatedRows } = require("./main_views"); +const { renderPhotoGallery, renderGalleryFields, renderZoomableImage } = require("./gallery_view"); +const { renderTribeOriginChip } = require("./clearnet_view"); const { config } = require("../server/SSB_server.js"); -const moment = require("../server/node_modules/moment"); -const { renderUrl } = require("../backend/renderUrl"); +const { renderStyledText } = require("../backend/renderStyledText"); const renderMediaBlob = (value, attrs = {}) => { if (!value) return null; const s = String(value).trim(); if (!s) return null; - if (s.startsWith('&')) return img({ src: `/blob/${encodeURIComponent(s)}`, ...attrs }); + if (s.startsWith('&')) return renderZoomableImage(`/blob/${encodeURIComponent(s)}`, { imgClass: attrs.class || 'post-image', alt: attrs.alt || '' }); const mVideo = s.match(/\[video:[^\]]*\]\(\s*(&[^)\s]+\.sha256)\s*\)/); - if (mVideo) return video({ controls: true, class: attrs.class || 'post-video', src: `/blob/${encodeURIComponent(mVideo[1])}` }); + if (mVideo) return video({ controls: true, class: attrs.class || 'post-video', src: `/blob/${encodeURIComponent(mVideo[1])}`, preload: 'metadata' }); const mAudio = s.match(/\[audio:[^\]]*\]\(\s*(&[^)\s]+\.sha256)\s*\)/); if (mAudio) return audio({ controls: true, class: attrs.class || 'post-audio', src: `/blob/${encodeURIComponent(mAudio[1])}` }); const mImg = s.match(/!\[[^\]]*\]\(\s*(&[^)\s]+\.sha256)\s*\)/); - if (mImg) return img({ src: `/blob/${encodeURIComponent(mImg[1])}`, class: attrs.class || 'post-image' }); + if (mImg) return renderZoomableImage(`/blob/${encodeURIComponent(mImg[1])}`, { imgClass: attrs.class || 'post-image', alt: attrs.alt || '' }); return null; }; @@ -38,8 +40,15 @@ const STATUS_BY_FILTER = { closed: "CLOSED" }; +const SEVERITY_BY_FILTER = { + sev_low: "LOW", + sev_medium: "MEDIUM", + sev_high: "HIGH", + sev_critical: "CRITICAL" +}; + const opt = (value, isSelected, text) => - option(Object.assign({ value }, isSelected ? { selected: "selected" } : {}), text); + option(Object.assign({ value }, isSelected ? { ...("selected" ? { selected: true } : {})} : {}), text); const hasAnyTemplateValue = (t) => { if (!t || typeof t !== "object") return false; @@ -50,7 +59,7 @@ const renderCardField = (labelText, value = "") => div( { class: "card-field" }, span({ class: "card-label" }, labelText), - span({ class: "card-value" }, ...renderUrl(String(value ?? ""))) + span({ class: "card-value" }, ...renderStyledText(String(value ?? ""))) ); const renderStackedTextField = (lbl, val) => @@ -58,16 +67,7 @@ const renderStackedTextField = (lbl, val) => ? div( { class: "card-field card-field-stacked" }, span({ class: "card-label" }, lbl), - span({ class: "card-value" }, ...renderUrl(String(val))) - ) - : null; - -const renderPmButton = (recipientId) => - recipientId && String(recipientId) !== String(userId) - ? form( - { method: "GET", action: "/pm" }, - input({ type: "hidden", name: "recipients", value: recipientId }), - button({ type: "submit", class: "filter-btn" }, i18n.privateMessage) + span({ class: "card-value" }, ...renderStyledText(String(val))) ) : null; @@ -135,7 +135,6 @@ const renderTemplateDetails = (report) => { h2({ class: "report-template-title" }, i18n.reportsContentTemplateTitle), renderStackedTextField(i18n.reportsContentLocationLabel + ":", t.contentLocation), renderStackedTextField(i18n.reportsWhyInappropriateLabel + ":", t.whyInappropriate), - renderStackedTextField(i18n.reportsRequestedActionLabel + ":", t.requestedAction), renderStackedTextField(i18n.reportsEvidenceLinksLabel + ":", t.evidenceLinks) ); } @@ -144,73 +143,11 @@ const renderTemplateDetails = (report) => { }; const renderReportCommentsSection = (reportId, comments = []) => { - const commentsCount = Array.isArray(comments) ? comments.length : 0; - - return div( - { class: "vote-comments-section" }, - div( - { class: "comments-count" }, - span({ class: "card-label" }, i18n.voteCommentsLabel + ": "), - span({ class: "card-value" }, String(commentsCount)) - ), - div( - { class: "comment-form-wrapper" }, - h2({ class: "comment-form-title" }, i18n.voteNewCommentLabel), - form( - { - method: "POST", - action: `/reports/${encodeURIComponent(reportId)}/comments`, - class: "comment-form", - enctype: "multipart/form-data" - }, - textarea({ - id: "comment-text", - name: "text", - rows: 4, - class: "comment-textarea", - placeholder: i18n.voteNewCommentPlaceholder - }), - div({ class: "comment-file-upload" }, label(i18n.uploadMedia), input({ type: "file", name: "blob" })), - br(), - button({ type: "submit", class: "comment-submit-btn" }, i18n.voteNewCommentButton) - ) - ), - (() => { - const visibleComments = (comments || []).filter(c => { - const t = c && c.value && c.value.content && c.value.content.text; - return t && String(t).trim(); - }); - return visibleComments.length - ? div( - { class: "comments-list" }, - visibleComments.map((c) => { - const author = c.value && c.value.author ? c.value.author : ""; - const ts = c.value && c.value.timestamp ? c.value.timestamp : c.timestamp; - const absDate = ts ? moment(ts).format("YYYY/MM/DD HH:mm:ss") : ""; - const relDate = ts ? moment(ts).fromNow() : ""; - - const content = c.value && c.value.content ? c.value.content : {}; - const root = content.fork || content.root || ""; - const text = content.text || ""; - - return div( - { class: "votations-comment-card" }, - span( - { class: "created-at" }, - span(i18n.createdBy), - author ? userLink(author) : span("(unknown)"), - absDate ? span(" | ") : "", - absDate ? span({ class: "votations-comment-date" }, absDate) : "", - relDate ? span({ class: "votations-comment-date" }, " | ", i18n.sendTime) : "", - relDate && root ? a({ href: `/thread/${encodeURIComponent(root)}#${encodeURIComponent(c.key)}` }, relDate) : "" - ), - p({ class: "votations-comment-text" }, ...renderUrl(text)) - ); - }) - ) - : p({ class: "votations-no-comments" }, i18n.voteNoCommentsYet); - })() - ); + return renderSharedCommentsSection({ + action: `/reports/${encodeURIComponent(reportId)}/comments`, + comments: comments, + returnTo: null + }); }; const renderTemplateForCategory = (category, templateData = {}) => { @@ -225,23 +162,19 @@ const renderTemplateForCategory = (category, templateData = {}) => { h2({ class: "report-template-title" }, i18n.reportsBugTemplateTitle), label(i18n.reportsStepsToReproduceLabel), br(), - textarea({ name: "stepsToReproduce", rows: "4", placeholder: i18n.reportsStepsToReproducePlaceholder }, tval("stepsToReproduce")), - br(), + textarea({ maxlength: "5000", name: "stepsToReproduce", rows: "4", placeholder: i18n.reportsStepsToReproducePlaceholder }, tval("stepsToReproduce")), br(), label(i18n.reportsExpectedBehaviorLabel), br(), - textarea({ name: "expectedBehavior", rows: "3", placeholder: i18n.reportsExpectedBehaviorPlaceholder }, tval("expectedBehavior")), - br(), + textarea({ maxlength: "5000", name: "expectedBehavior", rows: "3", placeholder: i18n.reportsExpectedBehaviorPlaceholder }, tval("expectedBehavior")), br(), label(i18n.reportsActualBehaviorLabel), br(), - textarea({ name: "actualBehavior", rows: "3", placeholder: i18n.reportsActualBehaviorPlaceholder }, tval("actualBehavior")), - br(), + textarea({ maxlength: "5000", name: "actualBehavior", rows: "3", placeholder: i18n.reportsActualBehaviorPlaceholder }, tval("actualBehavior")), br(), label(i18n.reportsEnvironmentLabel), br(), - textarea({ name: "environment", rows: "3", placeholder: i18n.reportsEnvironmentPlaceholder }, tval("environment")), - br(), + textarea({ maxlength: "5000", name: "environment", rows: "3", placeholder: i18n.reportsEnvironmentPlaceholder }, tval("environment")), br(), label(i18n.reportsReproduceRateLabel), br(), @@ -257,48 +190,21 @@ const renderTemplateForCategory = (category, templateData = {}) => { ); } - if (cat === "ABUSE") { - return div( - { class: "report-template-block" }, - h2({ class: "report-template-title" }, i18n.reportsAbuseTemplateTitle), - label(i18n.reportsWhatHappenedLabel), - br(), - textarea({ name: "whatHappened", rows: "4", placeholder: i18n.reportsWhatHappenedPlaceholder }, tval("whatHappened")), - br(), - br(), - label(i18n.reportsReportedUserLabel), - br(), - textarea({ name: "reportedUser", rows: "2", placeholder: i18n.reportsReportedUserPlaceholder }, tval("reportedUser")), - br(), - br(), - label(i18n.reportsEvidenceLinksLabel), - br(), - textarea({ name: "evidenceLinks", rows: "3", placeholder: i18n.reportsEvidenceLinksPlaceholder }, tval("evidenceLinks")) - ); - } - if (cat === "CONTENT") { return div( { class: "report-template-block" }, h2({ class: "report-template-title" }, i18n.reportsContentTemplateTitle), label(i18n.reportsContentLocationLabel), br(), - textarea({ name: "contentLocation", rows: "3", placeholder: i18n.reportsContentLocationPlaceholder }, tval("contentLocation")), - br(), + textarea({ maxlength: "5000", name: "contentLocation", rows: "3", placeholder: i18n.reportsContentLocationPlaceholder }, tval("contentLocation")), br(), label(i18n.reportsWhyInappropriateLabel), br(), - textarea({ name: "whyInappropriate", rows: "4", placeholder: i18n.reportsWhyInappropriatePlaceholder }, tval("whyInappropriate")), - br(), - br(), - label(i18n.reportsRequestedActionLabel), - br(), - textarea({ name: "requestedAction", rows: "3", placeholder: i18n.reportsRequestedActionPlaceholder }, tval("requestedAction")), - br(), + textarea({ maxlength: "5000", name: "whyInappropriate", rows: "4", placeholder: i18n.reportsWhyInappropriatePlaceholder }, tval("whyInappropriate")), br(), label(i18n.reportsEvidenceLinksLabel), br(), - textarea({ name: "evidenceLinks", rows: "3", placeholder: i18n.reportsEvidenceLinksPlaceholder }, tval("evidenceLinks")) + textarea({ maxlength: "5000", name: "evidenceLinks", rows: "3", placeholder: i18n.reportsEvidenceLinksPlaceholder }, tval("evidenceLinks")) ); } @@ -307,17 +213,15 @@ const renderTemplateForCategory = (category, templateData = {}) => { h2({ class: "report-template-title" }, i18n.reportsFeatureTemplateTitle), label(i18n.reportsProblemStatementLabel), br(), - textarea({ name: "problemStatement", rows: "4", placeholder: i18n.reportsProblemStatementPlaceholder }, tval("problemStatement")), - br(), + textarea({ maxlength: "5000", name: "problemStatement", rows: "4", placeholder: i18n.reportsProblemStatementPlaceholder }, tval("problemStatement")), br(), label(i18n.reportsUserStoryLabel), br(), - textarea({ name: "userStory", rows: "3", placeholder: i18n.reportsUserStoryPlaceholder }, tval("userStory")), - br(), + textarea({ maxlength: "5000", name: "userStory", rows: "3", placeholder: i18n.reportsUserStoryPlaceholder }, tval("userStory")), br(), label(i18n.reportsAcceptanceCriteriaLabel), br(), - textarea({ name: "acceptanceCriteria", rows: "4", placeholder: i18n.reportsAcceptanceCriteriaPlaceholder }, tval("acceptanceCriteria")) + textarea({ maxlength: "5000", name: "acceptanceCriteria", rows: "4", placeholder: i18n.reportsAcceptanceCriteriaPlaceholder }, tval("acceptanceCriteria")) ); }; @@ -368,146 +272,191 @@ const renderReportCategoryChip = (category) => { const renderReportCard = (report, userId, currentFilter = "all", spreadInfo) => { const confirmations = Array.isArray(report.confirmations) ? report.confirmations : []; + const origin = report.tribeOrigin || null; const chips = [ renderReportStatusChip(report.status), renderReportSeverityChip(report.severity), - renderReportCategoryChip(report.category), - renderLifespanChip(report.lifetime, i18n) + origin && !report.category ? null : renderReportCategoryChip(report.category), + renderLifespanChip(report.lifetime, i18n), + origin ? renderTribeOriginChip(origin) : null ].filter(Boolean); return div({ class: "tribe-card report-card" }, + origin ? null : div({ class: "card-header activity-card-header" }, + span(), + renderContentActions(report.id, `/reports/${encodeURIComponent(report.id)}`, { spread: spreadInfo || null, author: report.author, favKind: 'reports', isFavorite: report.isFavorite, reportTitle: report.title, report: false, deleteAction: String(report.author) === String(userId) ? contentDeleteAction('report', report.id) : undefined }) + ), div({ class: "tribe-card-body" }, div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, - a({ href: `/reports/${encodeURIComponent(report.id)}` }, report.title || i18n.reportsTitle) + a({ href: origin ? origin.href : `/reports/${encodeURIComponent(report.id)}` }, report.title || i18n.reportsTitle) ) ), chips.length ? div({ class: "card-chips-row" }, ...chips) : null, - div({ class: "tribe-card-members" }, + origin ? null : div({ class: "tribe-card-members" }, span({ class: "tribe-members-count" }, `${i18n.reportsConfirmations}: ${confirmations.length}`) - ), - div({ class: "card-spread-centered" }, renderSpreadButton(report.id, spreadInfo)), - div({ class: "card-visit-btn-centered" }, - form({ method: "GET", action: `/reports/${encodeURIComponent(report.id)}` }, - input({ type: "hidden", name: "filter", value: currentFilter }), - button({ type: "submit", class: "filter-btn" }, i18n.viewReport || "View Report") - ) ) ) ); }; +const reportChipFor = (filter, censusReports) => (mode) => { + if (mode === filter || mode === "all") return true; + if (!Array.isArray(censusReports)) return true; + if (mode === "top") return censusReports.length > 0; + if (mode === "mine") return censusReports.some((r) => r.author === userId); + if (mode === "recent") return censusReports.length > 0; + if (mode === "confirmed") return censusReports.some((r) => Array.isArray(r.confirmations) && r.confirmations.includes(userId)); + if (CATEGORY_BY_FILTER[mode]) return censusReports.some((r) => normU(r.category) === CATEGORY_BY_FILTER[mode]); + if (STATUS_BY_FILTER[mode]) return censusReports.some((r) => normalizeStatus(r.status) === STATUS_BY_FILTER[mode]); + if (SEVERITY_BY_FILTER[mode]) return censusReports.some((r) => normU(r.severity) === SEVERITY_BY_FILTER[mode]); + return true; +}; + exports.reportView = async (reports, filter, reportId, createCategory, params = {}) => { - const title = - filter === "create" ? i18n.reportsCreateButton : - filter === "edit" ? i18n.reportsUpdateButton : - filter === "mine" ? i18n.reportsMineSectionTitle : - filter === "features" ? i18n.reportsFeaturesSectionTitle : - filter === "bugs" ? i18n.reportsBugsSectionTitle : - filter === "abuse" ? i18n.reportsAbuseSectionTitle : - filter === "content" ? i18n.reportsContentSectionTitle : - filter === "confirmed" ? i18n.reportsConfirmedSectionTitle : - filter === "open" ? i18n.reportsOpenSectionTitle : - filter === "under_review" ? i18n.reportsUnderReviewSectionTitle : - filter === "resolved" ? i18n.reportsResolvedSectionTitle : - filter === "invalid" ? i18n.reportsInvalidSectionTitle : - i18n.reportsAllSectionTitle; + const title = i18n.reportsTitle; let filtered = Array.isArray(reports) ? reports : []; + const dayAgoReports = Date.now() - 86400000; + const confirmationsOf = (r) => (Array.isArray(r.confirmations) ? r.confirmations.length : 0); if (filter === "mine") { filtered = filtered.filter((r) => r.author === userId); + } else if (filter === "recent") { + filtered = filtered.filter((r) => new Date(r.createdAt).getTime() >= dayAgoReports); } else if (filter === "confirmed") { filtered = filtered.filter((r) => Array.isArray(r.confirmations) && r.confirmations.includes(userId)); + } else if (filter === "top") { + filtered = filtered.filter((r) => !r.tribeOrigin); } else if (CATEGORY_BY_FILTER[filter]) { const wanted = CATEGORY_BY_FILTER[filter]; filtered = filtered.filter((r) => normU(r.category) === wanted); } else if (STATUS_BY_FILTER[filter]) { const wanted = STATUS_BY_FILTER[filter]; filtered = filtered.filter((r) => normalizeStatus(r.status) === wanted); + } else if (SEVERITY_BY_FILTER[filter]) { + const wanted = SEVERITY_BY_FILTER[filter]; + filtered = filtered.filter((r) => normU(r.severity) === wanted); } - filtered = filtered.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)); + filtered = filter === "top" + ? filtered.sort((a, b) => confirmationsOf(b) - confirmationsOf(a) || new Date(b.createdAt) - new Date(a.createdAt)) + : filtered.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)); + const emptyMod = moduleIsEmpty(filtered, filter, "all", params.q); + const censusReports = Array.isArray(reports) ? reports : []; + const reportChip = reportChipFor(filter, censusReports); + const chipBtn = (mode, label) => reportChip(mode) + ? [button({ type: "submit", name: "filter", value: mode, class: btnClass(mode) }, String(label).toUpperCase())] + : []; const reportToEdit = filter === "edit" ? (Array.isArray(reports) ? reports.find((r) => r.id === reportId) : null) : null; + const formData = reportToEdit || params.draft || {}; const btnClass = (v) => (filter === v ? "filter-btn active" : "filter-btn"); - const selectedCategory = normU( + const rawCategory = normU( filter === "create" ? (createCategory || "FEATURES") : (reportToEdit?.category || "FEATURES") ); + const selectedCategory = ["FEATURES", "BUGS", "CONTENT"].includes(rawCategory) ? rawCategory : "FEATURES"; const selectedTemplate = reportToEdit?.template && typeof reportToEdit.template === "object" ? reportToEdit.template : {}; const applyLabel = i18n.apply || "Apply"; - const sev = String(reportToEdit?.severity || "low"); + const sev = String(formData.severity || "low"); const hiddenDescription = String(reportToEdit?.description || ""); return template( title, section( div( - { class: "tags-header" }, + { class: "tags-header module-header-line" }, h2(i18n.reportsTitle), p(i18n.reportsDescription) ), div( { class: "filters" }, + ...(emptyMod ? [] : [form( + { method: "GET", action: "/reports", class: "ui-toolbar ui-toolbar--filters" }, + ...chipBtn("all", i18n.reportsFilterAll), + ...chipBtn("recent", i18n.reportsFilterRecent), + ...chipBtn("mine", i18n.reportsFilterMine), + ...chipBtn("top", i18n.reportsFilterTop), + ...chipBtn("features", i18n.reportsFilterFeatures), + ...chipBtn("bugs", i18n.reportsFilterBugs), + ...chipBtn("content", i18n.reportsFilterContent), + ...chipBtn("confirmed", i18n.reportsFilterConfirmed), + )]), form( - { method: "GET", action: "/reports" }, - button({ type: "submit", name: "filter", value: "all", class: btnClass("all") }, i18n.reportsFilterAll), - button({ type: "submit", name: "filter", value: "mine", class: btnClass("mine") }, i18n.reportsFilterMine), - button({ type: "submit", name: "filter", value: "features", class: btnClass("features") }, i18n.reportsFilterFeatures), - button({ type: "submit", name: "filter", value: "bugs", class: btnClass("bugs") }, i18n.reportsFilterBugs), - button({ type: "submit", name: "filter", value: "abuse", class: btnClass("abuse") }, i18n.reportsFilterAbuse), - button({ type: "submit", name: "filter", value: "content", class: btnClass("content") }, i18n.reportsFilterContent), - button({ type: "submit", name: "filter", value: "confirmed", class: btnClass("confirmed") }, i18n.reportsFilterConfirmed), - button({ type: "submit", name: "filter", value: "open", class: btnClass("open") }, i18n.reportsFilterOpen), - button({ type: "submit", name: "filter", value: "under_review", class: btnClass("under_review") }, i18n.reportsFilterUnderReview), - button({ type: "submit", name: "filter", value: "resolved", class: btnClass("resolved") }, i18n.reportsFilterResolved), - button({ type: "submit", name: "filter", value: "invalid", class: btnClass("invalid") }, i18n.reportsFilterInvalid), + { method: "GET", action: "/reports", class: "ui-toolbar ui-toolbar--filters reports-subfilters" }, + ...(emptyMod ? [] : [ + ...chipBtn("open", i18n.reportsFilterOpen), + ...chipBtn("under_review", i18n.reportsFilterUnderReview), + ...chipBtn("resolved", i18n.reportsFilterResolved), + ...chipBtn("invalid", i18n.reportsFilterInvalid), + ...chipBtn("sev_low", i18n.reportsSeverityLow), + ...chipBtn("sev_medium", i18n.reportsSeverityMedium), + ...chipBtn("sev_high", i18n.reportsSeverityHigh), + ...chipBtn("sev_critical", i18n.reportsSeverityCritical), + ]), button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.reportsCreateButton) ) - ) + ), + filter === "edit" || filter === "create" + ? null + : emptyMod ? null : div({ class: "filters activity-filter-chips activity-toolbar-row" }, + renderModuleStatsBy(filtered, r => normalizeStatus(r.status || 'OPEN'), [{ value: 'OPEN', label: i18n.reportsStatusOpen }, { value: 'UNDER_REVIEW', label: i18n.reportsStatusUnderReview }, { value: 'RESOLVED', label: i18n.reportsStatusResolved }, { value: 'CLOSED', label: i18n.reportsStatusClosed }, { value: 'INVALID', label: i18n.reportsStatusInvalid }]), + form({ method: "GET", action: "/reports", class: "filter-box" }, + input({ type: "hidden", name: "filter", value: filter }), + input({ type: "text", name: "q", value: params.q || "", placeholder: i18n.reportsSearchPlaceholder, class: "filter-box__input" }), + div({ class: "filter-box__controls" }, + button({ type: "submit", class: "filter-box__button" }, i18n.searchButton) + ) + ) + ) ), section( filter === "edit" || filter === "create" ? div( { class: "report-form" }, + filter === "edit" ? await renderSpreadEditWarning(reportToEdit && (reportToEdit.id || reportToEdit.key)) : null, filter === "create" - ? div( - label(i18n.reportsTitleLabel), - br(), - input({ type: "text", name: "title", required: true, value: "", form: "report-create-form" }), - br(), - br(), + ? [ form( { id: "report-category-form", method: "GET", action: "/reports" }, input({ type: "hidden", name: "filter", value: "create" }), - label(i18n.reportsCategory), - br(), - select( - { name: "category" }, - opt("FEATURES", selectedCategory === "FEATURES", i18n.reportsCategoryFeatures), - opt("BUGS", selectedCategory === "BUGS", i18n.reportsCategoryBugs), - opt("ABUSE", selectedCategory === "ABUSE", i18n.reportsCategoryAbuse), - opt("CONTENT", selectedCategory === "CONTENT", i18n.reportsCategoryContent) - ), - br(), - br(), - button({ type: "submit", class: "create-button" }, applyLabel) + div({ class: "apply-row" }, + select( + { name: "category", class: "report-category-select" }, + opt("FEATURES", selectedCategory === "FEATURES", i18n.reportsCategoryFeatures), + opt("BUGS", selectedCategory === "BUGS", i18n.reportsCategoryBugs), + opt("CONTENT", selectedCategory === "CONTENT", i18n.reportsCategoryContent) + ), + button({ type: "submit", class: "create-button" }, applyLabel) + ) ), - br(), + hr({ class: "form-sep" }), h2({ class: "report-category-fixed" }, selectedCategory), - br(), form( { id: "report-create-form", action: "/reports/create", method: "POST", enctype: "multipart/form-data" }, input({ type: "hidden", name: "category", value: selectedCategory }), input({ type: "hidden", name: "description", value: "" }), + label(i18n.reportsTitleLabel), + br(), + input({ type: "text", name: "title", maxlength: "100", required: true, value: params.prefillTitle || "" }), + br(), + renderTemplateForCategory(selectedCategory, params.draft || {}), + br(), + ...renderGalleryFields(formData, false, 8), + label(i18n.uploadMedia), br(), + input({ type: "file", name: "blob" }), br(), br(), + label("Tags"), + br(), + input({ type: "text", name: "tags", placeholder: i18n.tagsPlaceholder, value: Array.isArray(formData.tags) ? formData.tags.join(", ") : (formData.tags || "") }), + br(), label(i18n.reportsSeverity), br(), select( @@ -519,28 +468,15 @@ exports.reportView = async (reports, filter, reportId, createCategory, params = ), br(), br(), - h2({ class: "report-template-main-title" }, i18n.reportsTemplateSectionTitle), - renderTemplateForCategory(selectedCategory, {}), - label(i18n.reportsUploadFile), - br(), - input({ type: "file", name: "image" }), - br(), - br(), - label("Tags"), - br(), - input({ type: "text", name: "tags", value: "" }), - br(), - br(), button({ type: "submit", class: "create-button" }, i18n.reportsCreateButton) ) - ) - : div( + ] + : [ form( { id: "report-edit-form", action: `/reports/update/${encodeURIComponent(reportId)}`, method: "POST", enctype: "multipart/form-data" }, label(i18n.reportsTitleLabel), br(), - input({ type: "text", name: "title", required: true, value: reportToEdit?.title || "" }), - br(), + input({ type: "text", name: "title", maxlength: "100", required: true, value: reportToEdit?.title || "" }), br(), input({ type: "hidden", name: "description", value: hiddenDescription }), label(i18n.reportsCategory), @@ -549,10 +485,15 @@ exports.reportView = async (reports, filter, reportId, createCategory, params = { name: "category", required: true }, opt("FEATURES", selectedCategory === "FEATURES", i18n.reportsCategoryFeatures), opt("BUGS", selectedCategory === "BUGS", i18n.reportsCategoryBugs), - opt("ABUSE", selectedCategory === "ABUSE", i18n.reportsCategoryAbuse), opt("CONTENT", selectedCategory === "CONTENT", i18n.reportsCategoryContent) ), br(), + renderTemplateForCategory(selectedCategory, selectedTemplate), + br(), + ...renderGalleryFields(reportToEdit || {}, true, 8), + label("Tags"), + br(), + input({ type: "text", name: "tags", placeholder: i18n.tagsPlaceholder, value: reportToEdit?.tags?.join(", ") || "" }), br(), label(i18n.reportsSeverity), br(), @@ -565,32 +506,19 @@ exports.reportView = async (reports, filter, reportId, createCategory, params = ), br(), br(), - h2({ class: "report-template-main-title" }, i18n.reportsTemplateSectionTitle), - renderTemplateForCategory(selectedCategory, selectedTemplate), - br(), - br(), - label(i18n.reportsUploadFile), - br(), - input({ type: "file", name: "image" }), - br(), - br(), - label("Tags"), - br(), - input({ type: "text", name: "tags", value: reportToEdit?.tags?.join(", ") || "" }), - br(), - br(), button({ type: "submit" }, i18n.reportsUpdateButton) ) - ) + ] ) : filtered.length > 0 - ? div({ class: "jobs-grid" }, filtered.map((r) => renderReportCard(r, userId, filter, params.spreadMap && params.spreadMap.get(r.id)))) - : p(i18n.reportsNoItems) + ? div({ class: "jobs-grid" }, paged(filtered).map((r) => renderReportCard(r, userId, filter, params.spreadMap && params.spreadMap.get(r.id)))) + : div({ class: "no-content-box" }, p(i18n.reportsNoItems)) ) ); }; exports.singleReportView = async (report, filter, comments = [], params = {}) => { + const reportChip = (mode) => reportChipFor(filter, Array.isArray(params.censusList) ? params.censusList : null)(mode); const btnClass = (v) => (filter === v ? "filter-btn active" : "filter-btn"); const confirmations = Array.isArray(report.confirmations) ? report.confirmations : []; const isAuthor = String(report.author) === String(userId); @@ -605,21 +533,13 @@ exports.singleReportView = async (report, filter, comments = [], params = {}) => ].filter(Boolean); const sideActions = []; - const pm = renderPmButton(report.author); - if (pm) sideActions.push(pm); sideActions.push(form({ method: "POST", action: `/reports/confirm/${encodeURIComponent(report.id)}` }, - button({ type: "submit", class: "filter-btn" }, i18n.reportsConfirmButton) - )); - sideActions.push(a({ href: "/tasks?filter=create", target: "_blank" }, - button({ type: "button", class: "filter-btn" }, i18n.reportsCreateTaskButton) + button({ type: "submit", class: "tribe-action-btn" }, i18n.reportsConfirmButton) )); + const ownerActions = []; if (isAuthor) { - sideActions.push(renderReportStatusSetter(report)); - sideActions.push(form({ method: "GET", action: `/reports/edit/${encodeURIComponent(report.id)}` }, - button({ type: "submit", class: "update-btn" }, i18n.reportsUpdateButton) - )); - sideActions.push(form({ method: "POST", action: `/reports/delete/${encodeURIComponent(report.id)}` }, - button({ type: "submit", class: "delete-btn" }, i18n.reportsDeleteButton) + ownerActions.push(form({ method: "GET", action: `/reports/edit/${encodeURIComponent(report.id)}` }, + button({ type: "submit", class: "tribe-action-btn" }, i18n.reportsUpdateButton) )); } @@ -640,53 +560,68 @@ exports.singleReportView = async (report, filter, comments = [], params = {}) => pushRow(i18n.reportsCategory, report.category); const reportSide = div({ class: "tribe-side" }, + div({ class: "card-header activity-card-header" }, + renderContentActions(report.id, `/reports/${encodeURIComponent(report.id)}`, { spread: params.spreads || null, author: report.author, favKind: 'reports', isFavorite: report.isFavorite, reportTitle: report.title, report: false, returnTo: `/reports/${encodeURIComponent(report.id)}`, deleteAction: isAuthor ? contentDeleteAction('report', report.id) : undefined }) + ), div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, report.title) ), chips.length ? div({ class: "card-chips-row" }, ...chips) : null, - div({ class: "card-spread-centered" }, renderSpreadButton(report.id, params.spreads)), - table({ class: "tribe-info-table jobs-info-table" }, ...infoRows), + table({ class: "tribe-info-table jobs-info-table" }, ...infoRows, ...renderCreatedRows(report.createdAt, report.author)), tagsNode, div({ class: "tribe-card-members" }, span({ class: "tribe-members-count" }, `${i18n.reportsConfirmations}: ${confirmations.length}`) ), - sideActions.length ? div({ class: "tribe-side-actions" }, ...sideActions) : null + sideActions.length ? div({ class: "tribe-side-actions" }, ...sideActions) : null, + isAuthor + ? div({ class: "tribe-side-actions housing-status-row" }, + span({ class: "card-label" }, `${i18n.reportsStatus}: `), + renderStateChip(normalizeStatus(report.status) === "RESOLVED" ? "mutuals" : "whole", "◆", String(report.status || "OPEN").toUpperCase()), + renderReportStatusSetter(report) + ) + : null, + ownerActions.length ? div({ class: "tribe-side-actions owner-actions" }, ...ownerActions) : null, + renderDocumentActions('reports', report.id, [ + a({ href: "/tasks?filter=create", class: "filter-btn" }, i18n.reportsCreateTaskButton) + ]) ); const opinionsBar = renderOpinionsVoting('/reports/opinions', report.id, report.opinions, null, report.opinions_inhabitants); const reportMain = div({ class: "tribe-main" }, - (details || report.image) ? div({ class: "job-section" }, - details || null, - report.image ? renderMediaBlob(report.image, { class: "report-detail-image" }) : null - ) : null, - p({ class: "card-footer" }, - span({ class: "date-link" }, `${moment(report.createdAt).format("YYYY/MM/DD HH:mm")} ${i18n.performed} `), - userLink(report.author) - ), - opinionsBar, - renderReportCommentsSection(report.id, comments) + details ? div({ class: "job-section" }, details) : null, + renderPhotoGallery(report, 'report'), + renderEngagement(report.id, opinionsBar, renderReportCommentsSection(report.id, comments)) ); return template( report.title, section( + div({ class: "tags-header module-header-line" }, h2(i18n.reportsTitle), p(i18n.reportsDescription)), div( { class: "filters" }, form( - { method: "GET", action: "/reports" }, - button({ type: "submit", name: "filter", value: "all", class: btnClass("all") }, i18n.reportsFilterAll), - button({ type: "submit", name: "filter", value: "mine", class: btnClass("mine") }, i18n.reportsFilterMine), - button({ type: "submit", name: "filter", value: "features", class: btnClass("features") }, i18n.reportsFilterFeatures), - button({ type: "submit", name: "filter", value: "bugs", class: btnClass("bugs") }, i18n.reportsFilterBugs), - button({ type: "submit", name: "filter", value: "abuse", class: btnClass("abuse") }, i18n.reportsFilterAbuse), - button({ type: "submit", name: "filter", value: "content", class: btnClass("content") }, i18n.reportsFilterContent), - button({ type: "submit", name: "filter", value: "confirmed", class: btnClass("confirmed") }, i18n.reportsFilterConfirmed), - button({ type: "submit", name: "filter", value: "open", class: btnClass("open") }, i18n.reportsFilterOpen), - button({ type: "submit", name: "filter", value: "under_review", class: btnClass("under_review") }, i18n.reportsFilterUnderReview), - button({ type: "submit", name: "filter", value: "resolved", class: btnClass("resolved") }, i18n.reportsFilterResolved), - button({ type: "submit", name: "filter", value: "invalid", class: btnClass("invalid") }, i18n.reportsFilterInvalid), + { method: "GET", action: "/reports", class: "ui-toolbar ui-toolbar--filters" }, + button({ type: "submit", name: "filter", value: "all", class: btnClass("all") }, String(i18n.reportsFilterAll).toUpperCase()), + ...(reportChip("recent") ? [button({ type: "submit", name: "filter", value: "recent", class: btnClass("recent") }, String(i18n.reportsFilterRecent).toUpperCase())] : []), + ...(reportChip("mine") ? [button({ type: "submit", name: "filter", value: "mine", class: btnClass("mine") }, String(i18n.reportsFilterMine).toUpperCase())] : []), + button({ type: "submit", name: "filter", value: "top", class: btnClass("top") }, String(i18n.reportsFilterTop).toUpperCase()), + ...(reportChip("features") ? [button({ type: "submit", name: "filter", value: "features", class: btnClass("features") }, String(i18n.reportsFilterFeatures).toUpperCase())] : []), + ...(reportChip("bugs") ? [button({ type: "submit", name: "filter", value: "bugs", class: btnClass("bugs") }, String(i18n.reportsFilterBugs).toUpperCase())] : []), + ...(reportChip("content") ? [button({ type: "submit", name: "filter", value: "content", class: btnClass("content") }, String(i18n.reportsFilterContent).toUpperCase())] : []), + ...(reportChip("confirmed") ? [button({ type: "submit", name: "filter", value: "confirmed", class: btnClass("confirmed") }, String(i18n.reportsFilterConfirmed).toUpperCase())] : []), button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.reportsCreateButton) + ), + form( + { method: "GET", action: "/reports", class: "ui-toolbar ui-toolbar--filters reports-subfilters" }, + ...(reportChip("open") ? [button({ type: "submit", name: "filter", value: "open", class: btnClass("open") }, String(i18n.reportsFilterOpen).toUpperCase())] : []), + ...(reportChip("under_review") ? [button({ type: "submit", name: "filter", value: "under_review", class: btnClass("under_review") }, String(i18n.reportsFilterUnderReview).toUpperCase())] : []), + ...(reportChip("resolved") ? [button({ type: "submit", name: "filter", value: "resolved", class: btnClass("resolved") }, String(i18n.reportsFilterResolved).toUpperCase())] : []), + ...(reportChip("invalid") ? [button({ type: "submit", name: "filter", value: "invalid", class: btnClass("invalid") }, String(i18n.reportsFilterInvalid).toUpperCase())] : []), + ...(reportChip("sev_low") ? [button({ type: "submit", name: "filter", value: "sev_low", class: btnClass("sev_low") }, String(i18n.reportsSeverityLow).toUpperCase())] : []), + ...(reportChip("sev_medium") ? [button({ type: "submit", name: "filter", value: "sev_medium", class: btnClass("sev_medium") }, String(i18n.reportsSeverityMedium).toUpperCase())] : []), + ...(reportChip("sev_high") ? [button({ type: "submit", name: "filter", value: "sev_high", class: btnClass("sev_high") }, String(i18n.reportsSeverityHigh).toUpperCase())] : []), + ...(reportChip("sev_critical") ? [button({ type: "submit", name: "filter", value: "sev_critical", class: btnClass("sev_critical") }, String(i18n.reportsSeverityCritical).toUpperCase())] : []) ) ), div({ class: "tribe-details" }, reportSide, reportMain) diff --git a/src/views/rooms_view.js b/src/views/rooms_view.js new file mode 100644 index 0000000..ba9efc7 --- /dev/null +++ b/src/views/rooms_view.js @@ -0,0 +1,526 @@ +const { div, h2, p, section, button, form, a, span, br, hr, label, textarea, input, select, option, img, table, tr, td, details, summary, ul, li } = require("../server/node_modules/hyperaxe") +const { clearnetItemHref, template, i18n, userLink, renderStateChip, renderLifespanChip, renderContentActions, renderInviteQrCard, renderModuleStatsBy, moduleIsEmpty, liveClock, renderPhoneChip, contentDeleteAction, paged, renderVideoGrid, renderLiveFeed, renderIcon } = require("./main_views") +const { renderEncryptedChip, renderTransportChip, renderReachChip, renderClearnetSelector, renderClearnetSwitch } = require("./clearnet_view") +const { renderStyledText } = require("../backend/renderStyledText") +const moment = require("../server/node_modules/moment") +const { config } = require("../server/SSB_server.js") + +const userId = config.keys.id +const ROOM_MAX = 50 +const MODES = ["all", "recent", "mine", "live", "open", "invite", "closed"] + +const safeArr = (v) => (Array.isArray(v) ? v : []) +const roomHref = (room) => `/rooms/${encodeURIComponent(room.rootId)}` +const occOf = (params, room) => (params && params.occupancy && params.occupancy.get(room.rootId)) || null +const modeLabel = (f) => String(i18n[`roomFilter${f.charAt(0).toUpperCase() + f.slice(1)}`] || f).toUpperCase() +const ROOM_REACH = ["OPEN", "INVITE-ONLY"] +const roomReachLabel = (s) => s === "INVITE-ONLY" ? i18n.roomStatusInviteOnly : i18n.roomStatusOpen +const clearnetEligible = (room) => !!room && !room.tribeId && !room.encrypted && room.type === "OPEN" && String(room.status || "").toUpperCase() === "OPEN" && !room.isClosed +const roomClearnetHref = (room) => clearnetItemHref("rooms", room.title, room.rootId) + +const renderRoomStatusChip = (room) => { + const s = room.isClosed ? "CLOSED" : room.type + const variant = s === "CLOSED" ? "closed" : s === "INVITE-ONLY" ? "whole" : "mutuals" + const icon = s === "CLOSED" ? "✗" : s === "INVITE-ONLY" ? "🔑" : "✓" + const label = s === "CLOSED" ? i18n.roomStatusClosed : s === "INVITE-ONLY" ? i18n.roomStatusInviteOnly : i18n.roomStatusOpen + return renderStateChip(variant, icon, label) +} + +const renderRoomNumberChip = (room, occ, inside) => { + if (!room.number) return null + if (room.line === "SWITCHBOARD" && !inside && occ && Number(occ.count) > 0) return renderPhoneChip(room.number, i18n.roomNumberHint) + return span({ title: room.line !== "SWITCHBOARD" ? i18n.roomLineDndError : inside ? i18n.roomYouAreIn : i18n.roomNobodyInside }, renderStateChip("whole", "✆", room.number)) +} +const renderRoomLineChip = (room) => room.isClosed || String(room.author) === String(userId) + ? null + : renderStateChip(room.line === "SWITCHBOARD" ? "mutuals" : "hidden", null, String(room.line === "SWITCHBOARD" ? i18n.roomLineSwitchboard : i18n.phoneDnd).toUpperCase()) + +const renderLiveChip = (occ) => { + if (!occ) return null + const count = Number(occ.count) || 0 + const max = occ.max || ROOM_MAX + return count > 0 ? renderStateChip("mutuals", "●", `${i18n.roomLiveChip} ${count}/${max}`) : renderStateChip("neutral", "●", `${count}/${max}`) +} + +const roomChips = (room, occ, inside = false) => [ + renderRoomStatusChip(room), + renderEncryptedChip(i18n), + room.type === "OPEN" && !room.tribeId ? renderTransportChip(i18n) : null, + renderRoomNumberChip(room, occ, inside), + renderRoomLineChip(room), + renderLiveChip(occ), + renderLifespanChip(room.lifetime, i18n) +].filter(Boolean) + +const renderModeButtons = (currentFilter, emptyMod = false, modesAvail = null) => + div({ class: "tribe-mode-buttons" }, + ...(emptyMod ? [] : [ + MODES.filter(f => f === "all" || f === currentFilter || (modesAvail && modesAvail[f] === true)).map(f => + form({ method: "GET", action: "/rooms" }, + input({ type: "hidden", name: "filter", value: f }), + button({ type: "submit", class: currentFilter === f ? "filter-btn active" : "filter-btn" }, modeLabel(f)) + ) + ) + ]), + form({ method: "GET", action: "/rooms" }, + input({ type: "hidden", name: "filter", value: "create" }), + button({ type: "submit", class: "create-button" }, i18n.roomCreate) + ) + ) + +const renderCover = (room, cls) => room.image ? img({ loading: "lazy", src: String(room.image).startsWith("/blob/sealed/") ? room.image : `/blob/${encodeURIComponent(room.image)}`, class: cls, alt: "" }) : null + +const roomIsFull = (occ) => !!occ && occ.count >= ((occ && occ.max) || ROOM_MAX) + +const joinForm = (room, occ, cls = "tribe-action-btn") => { + const max = (occ && occ.max) || ROOM_MAX + const count = occ ? occ.count : null + const full = count !== null && count >= max + return form({ method: "POST", action: `/rooms/join/${encodeURIComponent(room.rootId)}`, class: "room-join-form" }, + button({ type: "submit", class: cls, ...(full ? { disabled: true } : {}) }, + String(full ? i18n.roomFull : i18n.roomJoin).toUpperCase()) + ) +} + +const renderRoomItem = (room, params = {}) => { + const inside = params.live && params.live.ref === room.rootId + const occ = inside && Number.isFinite(Number(params.live.count)) ? { count: Number(params.live.count), max: params.live.max } : occOf(params, room) + const canJoin = !room.isClosed && !inside && (room.type === "OPEN" || room.tribeId || String(room.author) === String(userId) || safeArr(room.members).includes(userId)) + return li({ class: "mailing-archive-item room-item" }, + div({ class: "emergency-update-head mailing-archive-head" }, + div({ class: "mailing-archive-meta" }, + a({ href: roomHref(room), class: "user-link" }, room.title || "—"), + span({ class: "card-label activity-update-counts mailing-counts" }, `👥: ${safeArr(room.members).length}`), + ...roomChips(room, occ, inside), + room.clearnet === true && clearnetEligible(room) ? renderReachChip(true, i18n, roomClearnetHref(room)) : null, + inside ? renderStateChip("whole", "ꘒ", String(i18n.roomYouAreIn).toUpperCase()) : null + ), + renderContentActions(room.rootId, roomHref(room), { spread: (params.spreadMap && params.spreadMap.get(room.rootId)) || null, author: room.author, favKind: "rooms", isFavorite: room.isFavorite, reportTitle: room.title, deleteAction: String(room.author) === String(userId) ? contentDeleteAction("room", room.rootId) : undefined }) + ) + ) +} + +const renderRoomForm = (room, params = {}) => { + const tribeId = String(params.tribeId || "") + const isEdit = !!room + const draft = isEdit ? room : (params.draft || {}) + const reachRaw = String(params.reach || draft.status || "").toUpperCase() + const reach = ROOM_REACH.includes(reachRaw) ? reachRaw : "OPEN" + const withReach = !isEdit && !tribeId + const showClearnet = isEdit ? clearnetEligible(room) : (withReach && reach === "OPEN") + return div({ class: "div-center audio-form" }, + h2(isEdit ? i18n.roomUpdateSectionTitle : i18n.roomCreateSectionTitle), + withReach + ? [ + form({ method: "GET", action: "/rooms" }, + input({ type: "hidden", name: "filter", value: "create" }), + label(i18n.roomTypeLabel), br(), + div({ class: "apply-row" }, + select({ name: "status", class: "report-category-select" }, + ...ROOM_REACH.map(s => option({ value: s, ...(reach === s ? { selected: true } : {}) }, roomReachLabel(s))) + ), + button({ type: "submit", class: "create-button" }, i18n.apply || "Apply") + ) + ), + hr({ class: "form-sep" }), + h2({ class: "report-category-fixed" }, roomReachLabel(reach)) + ] + : null, + form({ method: "POST", action: isEdit ? `/rooms/update/${encodeURIComponent(room.rootId)}` : "/rooms/create", enctype: "multipart/form-data" }, + tribeId ? input({ type: "hidden", name: "tribeId", value: tribeId }) : null, + withReach ? input({ type: "hidden", name: "status", value: reach }) : null, + span(i18n.title || "Title"), br(), + input({ type: "text", name: "title", maxlength: "100", required: true, placeholder: i18n.roomTitlePlaceholder, value: draft.title || "" }), br(), + span(i18n.roomDescriptionLabel), br(), + textarea({ name: "description", rows: 4, maxlength: "1000", placeholder: i18n.roomDescriptionPlaceholder }, draft.description || ""), br(), + span(i18n.uploadMedia), br(), + input({ type: "file", name: "image", accept: "image/*" }), br(), br(), + span(i18n.roomTagsLabel), br(), + input({ type: "text", name: "tags", placeholder: i18n.tagsPlaceholder, value: safeArr(draft.tags).join(", ") }), br(), + showClearnet ? renderClearnetSelector(draft.clearnet === true || String(draft.clearnet || "") === "1", i18n) : null, + button({ type: "submit", class: "create-button" }, isEdit ? i18n.roomUpdate : i18n.roomCreate) + ) + ) +} + +exports.renderRoomInvitePage = (code) => + template(i18n.roomInviteMode || i18n.tribeInviteCodeText, + section(div({ class: "invite-page" }, + h2(i18n.tribeInviteCodeText, code), + form({ method: "GET", action: "/rooms" }, + input({ type: "hidden", name: "filter", value: "all" }), + button({ type: "submit", class: "filter-btn" }, i18n.walletBack) + ) + )) + ) + +exports.roomsView = async (rooms, filter, roomToEdit, params = {}) => { + const list = safeArr(rooms) + const q = String(params.q || "").trim() + const isForm = filter === "create" || filter === "edit" + const emptyMod = moduleIsEmpty(list, filter || "all", "all", q) + const shown = q + ? list.filter(r => [r.title, r.description, r.number, ...safeArr(r.tags)].some(v => String(v || "").toLowerCase().includes(q.toLowerCase()))) + : list + return template(i18n.roomsTitle, + section( + div({ class: "tags-header module-header-line" }, h2(i18n.roomsTitle), p(i18n.roomsDescription)), + renderModeButtons(filter, emptyMod, params.modesAvail || null), + !isForm && !emptyMod + ? div({ class: "filters activity-filter-chips activity-toolbar-row" }, + renderModuleStatsBy(shown, r => r.isClosed ? "CLOSED" : r.type, [{ value: "OPEN", label: i18n.roomStatusOpen }, { value: "INVITE-ONLY", label: i18n.roomStatusInviteOnly }, { value: "CLOSED", label: i18n.roomStatusClosed }]), + form({ method: "GET", action: "/rooms", class: "filter-box" }, + input({ type: "hidden", name: "filter", value: filter }), + input({ type: "text", name: "q", placeholder: i18n.roomSearchPlaceholder, value: q, class: "filter-box__input" }), + div({ class: "filter-box__controls" }, button({ type: "submit", class: "filter-box__button" }, i18n.searchButton)) + ) + ) + : null, + isForm + ? renderRoomForm(filter === "edit" ? roomToEdit : null, params) + : (shown.length + ? ul({ class: "mailing-archive" }, ...paged(shown).map(r => renderRoomItem(r, params))) + : div({ class: "tribe-grid" }, p(i18n.roomsNoItems))) + ) + ) +} + +const eventLine = (ev) => { + const key = ev.t === "join" ? "roomEventJoined" : ev.t === "leave" ? "roomEventLeft" : ev.t === "recStart" ? "roomEventRecStart" : ev.t === "mute" ? "roomEventMuted" : ev.t === "unmute" ? "roomEventUnmuted" : ev.t === "hand" ? "roomEventHand" : "roomEventRecStop" + const [before, after] = String(i18n[key] || "{name}").split("{name}") + return [before, userLink(ev.id), after] +} +const renderRecChip = () => span({ class: "room-rec-chip" }, "● " + String(i18n.roomRecording).toUpperCase()) +const fmtDuration = (ms) => { const s = Math.max(0, Math.floor((Number(ms) || 0) / 1000)); return `${String(Math.floor(s / 60)).padStart(2, "0")}:${String(s % 60).padStart(2, "0")}` } +const refreshLink = (room) => a({ href: roomHref(room), class: "tribe-action-btn room-refresh-btn" }, String(i18n.liveRefresh).toUpperCase()) +const renderLiveControls = (room, live) => div({ class: "room-live-controls" }, + form({ method: "POST", action: "/rooms/mute", class: "phone-action-form" }, + input({ type: "hidden", name: "mute", value: live.muted ? "0" : "1" }), + input({ type: "hidden", name: "returnTo", value: roomHref(room) }), + button({ type: "submit", class: "tribe-action-btn" }, String(live.muted ? i18n.phoneUnmute : i18n.phoneMute).toUpperCase()) + ), + live.videoAvailable + ? form({ method: "POST", action: "/rooms/camera", class: "phone-action-form" }, + input({ type: "hidden", name: "cam", value: live.cam ? "0" : "1" }), + input({ type: "hidden", name: "returnTo", value: `${roomHref(room)}#room-video` }), + button({ type: "submit", class: live.cam ? "tribe-action-btn room-cam-btn-on" : "tribe-action-btn" }, String(live.cam ? i18n.phoneCameraOff : i18n.phoneCameraOn).toUpperCase()) + ) + : null, + live.streaming != null + ? form({ method: "POST", action: "/rooms/stream", class: "phone-action-form" }, + input({ type: "hidden", name: "stop", value: "1" }), + input({ type: "hidden", name: "returnTo", value: roomHref(room) }), + button({ type: "submit", class: "tribe-action-btn room-cam-btn-on" }, String(i18n.roomStreamStop).toUpperCase()) + ) + : live.streamAvailable + ? form({ method: "POST", action: "/rooms/stream", class: "phone-action-form room-stream-form" }, + input({ type: "hidden", name: "returnTo", value: `${roomHref(room)}#room-video` }), + input({ type: "file", id: "room-stream-file", name: "video", accept: "video/*", required: true, class: "room-stream-input" }), + label({ for: "room-stream-file", class: "tribe-action-btn room-stream-pick" }, String(i18n.roomStreamFile).toUpperCase()), + button({ type: "submit", class: "tribe-action-btn room-cam-btn-on room-stream-go", title: i18n.roomStreamGo }, "▶") + ) + : null, + form({ method: "POST", action: "/rooms/hand", class: "phone-action-form" }, + input({ type: "hidden", name: "hand", value: live.hand ? "0" : "1" }), + input({ type: "hidden", name: "returnTo", value: roomHref(room) }), + button({ type: "submit", class: live.hand ? "tribe-action-btn room-hand-btn-on" : "tribe-action-btn" }, "✋ " + String(live.hand ? i18n.roomLowerHand : i18n.roomRaiseHand).toUpperCase()) + ), + live.recording + ? form({ method: "POST", action: "/rooms/rec/stop", class: "phone-action-form" }, button({ type: "submit", class: "tribe-action-btn danger-btn" }, String(i18n.roomRecStop).toUpperCase())) + : form({ method: "POST", action: "/rooms/rec/start", class: "phone-action-form" }, button({ type: "submit", class: "tribe-action-btn" }, String(i18n.roomRec).toUpperCase())), + live.recording || safeArr(live.recordingBy).length ? renderRecChip() : null, + form({ method: "POST", action: "/rooms/notify", class: "phone-action-form" }, + input({ type: "hidden", name: "on", value: live.notify ? "0" : "1" }), + button({ type: "submit", class: live.notify ? "tribe-action-btn" : "tribe-action-btn room-notify-off" }, "🔔 " + String(live.notify ? i18n.roomNotificationsOn : i18n.roomNotificationsOff).toUpperCase()) + ), + live.notify && safeArr(live.events).length + ? form({ method: "POST", action: "/rooms/notify/clear", class: "phone-action-form" }, button({ type: "submit", class: "tribe-action-btn" }, String(i18n.roomClearNotices).toUpperCase())) + : null, + refreshLink(room), + form({ method: "POST", action: "/rooms/leave", class: "phone-action-form" }, + input({ type: "hidden", name: "returnTo", value: roomHref(room) }), + button({ type: "submit", class: "tribe-action-btn danger-btn" }, String(i18n.roomLeave).toUpperCase()) + ) +) +const handOrder = (live) => { + const all = [{ you: true, hand: !!live.hand, since: Number(live.handSince) || 0 }, ...safeArr(live.peers).map(peer => ({ peer, hand: !!peer.hand, since: Number(peer.handSince) || 0 }))] + const raised = all.filter(e => e.hand).sort((a, b) => a.since - b.since) + raised.forEach((e, i) => { e.turn = i + 1 }) + return [...raised, ...all.filter(e => !e.hand)] +} +const renderRoomEvents = (live) => live.notify && safeArr(live.events).length + ? ul({ class: "room-events" }, ...safeArr(live.events).slice().reverse().map(ev => li({ class: "room-event" }, span({ class: "date-link" }, moment(ev.ts).format("HH:mm:ss")), span({ class: "room-event-text" }, ...eventLine(ev))))) + : null +const renderRecordings = (recordings) => safeArr(recordings).length + ? div({ class: "phone-block room-recordings-block" }, + h2(i18n.roomRecordings), + ul({ class: "room-recordings" }, ...recordings.map(r => li({ class: "room-recording" }, + span({ class: "date-link" }, moment(r.at).format("YYYY/MM/DD HH:mm")), + span({ class: "phone-call-clock" }, fmtDuration(r.durationMs)), + a({ href: `/rooms/recordings/${encodeURIComponent(r.name)}`, class: "tribe-action-btn", download: r.name }, String(i18n.fileDownload).toUpperCase()), + form({ method: "POST", action: `/rooms/recordings/${encodeURIComponent(r.name)}/delete`, class: "phone-action-form" }, + button({ type: "submit", class: "btn-singleview btn-delete", title: i18n.delete }, renderIcon("close"))) + ))) + ) + : null +const renderLivePanel = (room, live, occ, canJoin = false) => { + const head = (left, controls = null) => div({ class: "room-live-head" }, + div({ class: "room-live-head-main" }, left), + controls || refreshLink(room) + ) + if (live) { + const silenceForm = (id, on) => form({ method: "POST", action: "/rooms/silence", class: "phone-action-form" }, + input({ type: "hidden", name: "id", value: id }), + input({ type: "hidden", name: "on", value: on ? "0" : "1" }), + button({ type: "submit", class: on ? "tribe-action-btn danger-btn" : "tribe-action-btn" }, String(on ? i18n.phoneUnsilence : i18n.phoneSilence).toUpperCase()) + ) + const isPublic = !!live.public || (room.clearnet === true && clearnetEligible(room)) + const who = (id, muted, you, silenced = false, recording = false, turn = 0, cam = false, film = false, viewer = false) => viewer + ? div({ class: "phone-contact room-peer" }, userLink(id), renderReachChip(true, i18n)) + : div({ class: "phone-contact room-peer" + (turn ? " room-peer-raised" : "") }, + turn ? span({ class: "room-peer-hand" }, `✋ ${turn}`) : null, + userLink(id), + you ? span({ class: "room-peer-you" }, i18n.roomYou) : null, + muted ? span({ class: "room-peer-muted" }, String(i18n.roomMuted).toUpperCase()) : null, + cam ? span({ class: "room-peer-cam" }, "CAM") : null, + film ? span({ class: "room-peer-cam" }, "▶") : null, + recording ? renderRecChip() : null, + you ? null : silenceForm(id, silenced) + ) + return div({ class: "phone-call-panel room-live-panel" }, + head(div({ class: "card-chips-row" }, renderEncryptedChip(i18n), isPublic ? renderReachChip(true, i18n, roomClearnetHref(room)) : null, renderLiveChip({ count: live.count, max: live.max }), live.joinedAt ? span({ class: "phone-call-clock" }, liveClock(live.joinedAt)) : null, live.streaming != null ? span({ class: "room-stream-chip", title: live.streaming || "" }, "▶ " + String(live.streaming || i18n.roomStreaming)) : null), renderLiveControls(room, live)), + live.grid + ? (safeArr(live.gridOrder).length + ? div({ id: "room-video", class: safeArr(live.gridOrder).length === 1 ? "video-grid video-grid-one video-grid-solo" : "video-grid video-grid-one" }, + ...renderLiveFeed("/phone/video/grid", "video-feed video-feed-grid"), + div({ class: "video-grid-names" }, ...safeArr(live.gridOrder).map(key => { + const film = String(key).startsWith("film:") + const id = film ? String(key).slice(5) : key + const name = id === userId ? i18n.roomYou : userLink(id) + return film ? span({ class: "video-tile-name video-tile-film" }, "▶ ", name) : (id === userId ? span({ class: "video-tile-name" }, name) : name) + })) + ) + : null) + : div({ id: "room-video" }, renderVideoGrid(safeArr(live.peers).map(p => ({ id: p.id, on: !!p.cam, film: !!p.film })), !!live.cam, { film: live.streaming != null })), + div({ class: "phone-contacts" }, ...handOrder(live).map(e => e.you + ? who(userId, live.muted, true, false, !!live.recording, e.turn, !!live.cam, live.streaming != null) + : who(e.peer.id, e.peer.muted, false, !!e.peer.silenced, !!e.peer.recording, e.turn, !!e.peer.cam, !!e.peer.film, !!e.peer.viewer))), + renderRoomEvents(live) + ) + } + const count = occ ? occ.count : 0 + return div({ class: "phone-block room-live-panel" }, + head(p({ class: "phone-empty" }, count > 0 ? String(i18n.roomSomeone).replace("{n}", String(count)) : i18n.roomNobody), canJoin ? div({ class: "room-live-controls" }, refreshLink(room), joinForm(room, occ)) : null), + canJoin && room.clearnet === true && clearnetEligible(room) ? p({ class: "room-public-warning" }, i18n.roomPublicWarning) : null + ) +} + +exports.singleRoomView = async (room, params = {}) => { + const isAuthor = String(room.author) === String(userId) + const isMember = safeArr(room.members).includes(userId) || (!!room.tribeId && !!room.isTribeMember) + const restricted = !isMember && !isAuthor && room.type === "INVITE-ONLY" + const live = params.live && params.live.ref === room.rootId ? params.live : null + const occ = params.occ || null + const canJoin = !restricted && !room.isClosed && params.available !== false + const canClearnet = clearnetEligible(room) + const isClearnet = canClearnet && room.clearnet === true + + const inviteActions = isAuthor && room.type === "INVITE-ONLY" && !room.tribeId && !room.isClosed + ? [ + form({ method: "POST", action: `/rooms/generate-invite/${encodeURIComponent(room.rootId)}` }, + button({ type: "submit", class: "tribe-action-btn" }, i18n.tribeGenerateInvite) + ), + (() => { + const openInvite = safeArr(room.invites).find(inv => typeof inv === "object" && inv && inv.public === true && inv.code) + if (openInvite) return [ + div({ class: "tribe-open-invite" }, + span({ class: "card-label" }, i18n.tribeInviteCodeText), + span({ class: "tribe-open-invite-code" }, openInvite.code), + renderInviteQrCard({ qrDataUrl: `/qr-invite-code/rooms/${encodeURIComponent(openInvite.code)}` }) + ), + form({ method: "POST", action: `/rooms/open-invite/remove/${encodeURIComponent(room.rootId)}` }, + button({ type: "submit", class: "tribe-action-btn danger-btn" }, i18n.tribeRemoveInvitation) + ) + ] + return form({ method: "POST", action: `/rooms/open-invite/create/${encodeURIComponent(room.rootId)}` }, + button({ type: "submit", class: "tribe-action-btn" }, i18n.tribeOpenInvitation) + ) + })() + ].flat().filter(Boolean) + : [] + + const ownerActions = isAuthor + ? [ + room.isClosed + ? form({ method: "POST", action: `/rooms/reopen/${encodeURIComponent(room.rootId)}` }, + button({ type: "submit", class: "update-btn" }, i18n.roomReopen) + ) + : form({ method: "POST", action: `/rooms/close/${encodeURIComponent(room.rootId)}` }, + button({ type: "submit", class: "update-btn delete-btn" }, i18n.roomClose) + ), + form({ method: "GET", action: "/rooms" }, + input({ type: "hidden", name: "filter", value: "edit" }), + input({ type: "hidden", name: "id", value: room.rootId }), + button({ type: "submit", class: "update-btn" }, i18n.roomUpdate) + ) + ].filter(Boolean) + : [] + + const side = div({ class: "tribe-side" }, + div({ class: "card-header activity-card-header" }, + renderContentActions(room.rootId, roomHref(room), { spread: params.spreads || null, author: room.author, favKind: "rooms", isFavorite: room.isFavorite, returnTo: roomHref(room), reportTitle: room.title, deleteAction: isAuthor ? contentDeleteAction("room", room.rootId) : undefined }) + ), + div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, restricted ? i18n.roomStatusInviteOnly : (room.title || "—"))), + div({ class: "card-chips-row" }, + ...roomChips(room, live ? { count: live.count, max: live.max } : occ, !!live), + canClearnet ? renderReachChip(isClearnet, i18n, isClearnet ? roomClearnetHref(room) : null) : null, + isAuthor && canClearnet ? renderClearnetSwitch("rooms", room.rootId, isClearnet) : null + ), + restricted ? null : renderCover(room, "tribe-detail-image"), + restricted || !room.description ? null : p({ class: "tribe-side-description" }, ...renderStyledText(room.description)), + table({ class: "tribe-info-table jobs-info-table" }, + tr( + td({ class: "tribe-info-label" }, i18n.createdAtLabel || "Created at"), + td({ class: "tribe-info-value", colspan: "3" }, moment(room.createdAt).format("YYYY/MM/DD HH:mm")) + ), + restricted ? null : tr(td({ class: "tribe-info-value", colspan: "4" }, userLink(room.author))) + ), + live || restricted + ? div({ class: "tribe-card-members" }, + span({ class: "tribe-members-count" }, `${i18n.roomParticipants}: ${safeArr(room.members).length}`) + ) + : null, + inviteActions.length ? div({ class: "tribe-side-actions" }, ...inviteActions) : null, + isAuthor && !room.isClosed + ? div({ class: "tribe-side-actions housing-status-row" }, + span({ class: "card-label" }, `${i18n.statusLabel || "Status"}: `), + renderStateChip(room.line === "SWITCHBOARD" ? "mutuals" : "hidden", "✆", String(room.line === "SWITCHBOARD" ? i18n.roomLineSwitchboard : i18n.phoneDnd).toUpperCase()), + form({ method: "POST", action: `/rooms/line/${encodeURIComponent(room.rootId)}`, class: "inline-form" }, + button({ class: "tribe-action-btn", type: "submit", name: "line", value: room.line === "SWITCHBOARD" ? "DND" : "SWITCHBOARD" }, + String(room.line === "SWITCHBOARD" ? i18n.phoneDnd : i18n.roomLineSwitchboard).toUpperCase()) + ) + ) + : null, + restricted + ? div({ class: "tribe-side-actions" }, a({ class: "tribe-action-btn", href: "/invites?filter=others" }, i18n.tribeEnterInvite)) + : null, + restricted || !safeArr(room.tags).length + ? null + : div({ class: "card-tags" }, ...room.tags.map(t => a({ href: `/search?query=%23${encodeURIComponent(t)}`, class: "tag-link" }, `#${t}`))), + ownerActions.length ? div({ class: "tribe-side-actions owner-actions" }, ...ownerActions) : null + ) + + const main = div({ class: "tribe-main" }, + restricted + ? p({ class: "access-denied-msg" }, i18n.roomAccessDenied) + : [ + params.available === false ? div({ class: "pm-form-error-msg" }, p("✗ " + i18n.phoneUnavailable)) : null, + live + ? renderLivePanel(room, live, occ) + : [ + renderLivePanel(room, null, occ, canJoin), + details({ class: "pad-members-details", ...(safeArr(room.members).length <= 8 ? { open: true } : {}) }, + summary({ class: "tribe-members-count" }, `${i18n.roomParticipants}: ${safeArr(room.members).length}`), + div({ class: "pad-members-chips" }, ...safeArr(room.members).map(m => span({ class: "pad-member-chip" }, userLink(m)))) + ) + ], + renderRecordings(params.recordings) + ] + ) + + return template(room.title || i18n.roomsTitle, + section(div({ class: "tags-header module-header-line" }, h2(i18n.roomsTitle), p(i18n.roomsDescription)), renderModeButtons("all", false, params.modesAvail || null)), + section(div({ class: "tribe-details" }, side, main)) + ) +} + +exports.renderTribeRoomsSection = (tribe, rooms, occupancy, live, toolbar = null) => { + const items = safeArr(rooms) + const createBtn = form({ method: "GET", action: "/rooms" }, + input({ type: "hidden", name: "filter", value: "create" }), + input({ type: "hidden", name: "tribeId", value: tribe.id }), + button({ type: "submit", class: "create-button" }, i18n.tribeRoomCreate)) + const head = toolbar || div({ class: "tribe-content-header" }, h2(i18n.tribeSectionRooms), createBtn) + if (!items.length) return div({ class: "tribe-content-list" }, head, div({ class: "no-content-box" }, p(i18n.tribeRoomsEmpty))) + return div({ class: "tribe-content-list" }, + head, + ul({ class: "mailing-archive" }, ...paged(items).map(r => renderRoomItem(r, { occupancy, live }))) + ) +} + +exports.renderRoomItem = renderRoomItem +exports.roomStatsBy = (rooms) => renderModuleStatsBy(safeArr(rooms), r => r.isClosed ? "CLOSED" : r.type, [{ value: "OPEN", label: i18n.roomStatusOpen }, { value: "INVITE-ONLY", label: i18n.roomStatusInviteOnly }, { value: "CLOSED", label: i18n.roomStatusClosed }]) + +exports.clearnetRoomView = async (room, { live = false, sound = true, base = "" } = {}) => { + const { escapeHtml: esc, renderKindTag, renderClearnetPage, cnZoom } = require("./clearnet_view") + const name = room.title || i18n.cnKindRoom + const extraCss = ` +.cn-room-title{color:var(--fg);margin:0 0 16px 0;font-size:32px;font-weight:700;word-break:break-word} +.cn-room-meta{display:flex;flex-wrap:wrap;gap:10px;align-items:center} +.cn-room-number{display:inline-flex;align-items:center;gap:6px;border:1px solid var(--fg);border-radius:10px;padding:4px 12px;font-size:16px;font-weight:600;color:var(--fg);background:var(--bg-sub)} +.cn-room-number .pm-exposition-icon{font-size:18px;line-height:1} +.cn-room-number .pm-exposition-text{line-height:1;letter-spacing:1px;font-family:monospace} +.cn-room-live{display:flex;flex-direction:column;align-items:center;gap:12px;margin-top:24px} +.cn-room-live-chip{align-self:flex-start;border:1px solid #2ecc71;border-radius:10px;padding:3px 10px;color:#2ecc71;font-size:13px;font-weight:700;letter-spacing:1px} +.cn-room-live .cn-zoom{display:block;width:100%;max-width:960px} +.cn-room-grid{width:100%;aspect-ratio:4/3;object-fit:contain;background:#000;border-radius:8px} +.cn-room-sound{width:100%;max-width:960px} +.cn-room-live .cn-lightbox-img{width:92vw;height:88vh;object-fit:contain} +.cn-room-live .cn-lightbox-full .cn-lightbox-img{width:100vw;height:100vh} +` + const body = ` +

${esc(name)}

+
+ ${renderKindTag("room")} + ${room.number ? `✆${esc(room.number)}` : ""} +
+ ${live && base ? `
+ ● ${esc(String(i18n.roomLiveChip || "LIVE").toUpperCase())} + ${cnZoom("room-live", `${base}/live`, name, { full: true, imgClass: "cn-room-grid" })} + ${sound ? `` : ""} +
` : ""} +` + return renderClearnetPage({ + title: `${name} | Oasis`, + ogTitle: name, + ogDescription: "", + extraCss, + body, + hubFeedId: room.author || null + }) +} + +exports.roomModesFromCensus = (census, me, occupancy) => { + const list = safeArr(census) + return { + mine: list.some(x => String(x.author) === String(me)), + recent: list.length > 0, + live: list.some(x => { const o = occupancy && occupancy.get(x.rootId); return !!(o && o.count > 0) }), + open: list.some(x => x.type === "OPEN" && !x.isClosed), + invite: list.some(x => x.type === "INVITE-ONLY" && !x.isClosed), + closed: list.some(x => x.isClosed) + } +} + +const pickerSize = (bytes) => { + const n = Number(bytes) || 0 + if (n >= 1073741824) return `${(n / 1073741824).toFixed(1)} GB` + if (n >= 1048576) return `${(n / 1048576).toFixed(1)} MB` + return `${Math.max(1, Math.round(n / 1024))} KB` +} + +exports.roomStreamMatchesView = ({ files, returnTo }) => template( + i18n.roomStreamPickTitle, + section( + div({ class: "tags-header module-header-line" }, h2(i18n.roomStreamPickTitle), p(i18n.roomStreamPickHint)), + div({ class: "mode-buttons-row" }, a({ href: returnTo, class: "filter-btn" }, String(i18n.phoneCancel).toUpperCase())), + ul({ class: "stream-picker-list" }, ...safeArr(files).map(f => li({ class: "stream-picker-item" }, + span({ class: "stream-picker-link" }, renderIcon("film"), span({ class: "stream-picker-name" }, f.path), span({ class: "stream-picker-size" }, pickerSize(f.size))), + form({ method: "POST", action: "/rooms/stream", class: "phone-action-form" }, + input({ type: "hidden", name: "file", value: f.path }), + input({ type: "hidden", name: "returnTo", value: returnTo }), + button({ type: "submit", class: "tribe-action-btn" }, String(i18n.roomStreamFile).toUpperCase()) + ) + ))) + ) +) diff --git a/src/views/school_view.js b/src/views/school_view.js new file mode 100644 index 0000000..eb6c031 --- /dev/null +++ b/src/views/school_view.js @@ -0,0 +1,945 @@ +const { div, h2, h3, p, section, button, form, a, span, textarea, br, input, label, select, option, table, tr, td, th, details, summary, datalist, progress, hr } = require("../server/node_modules/hyperaxe") +const RAW_HTML = Symbol.for('oasis.rawHtml'); +const moment = require("../server/node_modules/moment"); +const { clearnetItemHref, template, i18n, userLink, renderStateChip, renderContentActions, renderOpinionsVoting, renderEngagement, renderInviteQrCard, renderSubscriptionBox, renderModuleStatsBy, moduleIsEmpty, renderEcoValueChip, renderWalletChip, contentDeleteAction, paged } = require("./main_views") +const opinionCategories = require("../backend/opinion_categories") +const { config } = require("../server/SSB_server.js") +const { renderStyledText, renderStyledHtml } = require("../backend/renderStyledText") +const nameCache = require("../backend/nameCache") +const { sanitizeHtml } = require("../backend/sanitizeHtml") +const { renderReachChip, renderClearnetSelector, renderClearnetSwitch } = require("./clearnet_view") +const renderMd = (text) => div({ class: "styled-text", [RAW_HTML]: sanitizeHtml(renderStyledHtml(String(text || ""))) }) + +const userId = config.keys.id +const safeArr = (v) => (Array.isArray(v) ? v : []) +const safeText = (v) => String(v || "").trim() +const isFree = (course) => !(Number(course.price) > 0) +const isProtected = (course) => !isFree(course) || course.visibility === "INVITE" +const clearnetEligible = (course) => course.visibility === "PUBLIC" && isFree(course) && !course.encrypted +const COURSE_TYPES = ["OPEN", "PAID", "INVITE"] +const courseTypeOf = (course) => COURSE_TYPES.includes(String(course.courseType || "").toUpperCase()) ? String(course.courseType).toUpperCase() : (course.visibility === "INVITE" ? "INVITE" : (Number(course.price) > 0 ? "PAID" : "OPEN")) +const courseTypeLabel = (v) => v === "INVITE" ? i18n.schoolTypeInvite : (v === "PAID" ? i18n.schoolTypePaid : i18n.schoolTypeOpen) +const sumCats = (opinions = {}, cats = []) => (cats || []).reduce((sum, cat) => sum + (Number((opinions || {})[cat]) || 0), 0) +const renderStarRating = (opinions, voterCount) => { + const pos = sumCats(opinions, opinionCategories.positive) + const neg = sumCats(opinions, opinionCategories.constructive) + sumCats(opinions, opinionCategories.moderation) + const totalVotes = pos + neg + const full = totalVotes > 0 ? Math.round((pos / totalVotes) * 5) : 0 + const stars = "\u2605".repeat(full) + "\u2606".repeat(5 - full) + return span({ class: "shop-product-stars" }, `${stars} (${voterCount})`) +} + +const renderMediaBlob = (value, fallbackSrc = null, attrs = {}) => { + if (!value) return fallbackSrc ? require("../server/node_modules/hyperaxe").img({ src: fallbackSrc, ...attrs }) : null + const { img } = require("../server/node_modules/hyperaxe") + const s = String(value).trim() + if (s.startsWith('&')) return img({ loading: 'lazy', src: `/blob/${encodeURIComponent(s)}`, ...attrs }) + return fallbackSrc ? img({ src: fallbackSrc, ...attrs }) : null +} + +const buildReturnTo = (filter, params = {}) => { + const f = safeText(filter || "all") + const q = safeText(params.q || "") + const sort = safeText(params.sort || "recent") + const parts = [`filter=${encodeURIComponent(f)}`] + if (q) parts.push(`q=${encodeURIComponent(q)}`) + if (sort) parts.push(`sort=${encodeURIComponent(sort)}`) + return `/school?${parts.join("&")}` +} + +const renderModeButtons = (currentFilter, emptyMod = false, modesAvail = null) => + div({ class: "tribe-mode-buttons" }, + ...(emptyMod ? [] : [ + ["all", "recent", "mine", "top", "applied", "open", "favorites"].filter(f => f === "all" || f === currentFilter || (modesAvail && modesAvail[f] !== false)).map(f => + form({ method: "GET", action: "/school" }, + input({ type: "hidden", name: "filter", value: f }), + button({ type: "submit", class: currentFilter === f ? "filter-btn active" : "filter-btn" }, i18n[`schoolFilter${f.charAt(0).toUpperCase() + f.slice(1)}`] || f.toUpperCase()) + ) + ), + ]), + form({ method: "GET", action: "/school" }, + input({ type: "hidden", name: "filter", value: "create" }), + button({ type: "submit", class: "create-button" }, i18n.schoolCreateButton) + ) + ) + +const renderPriceChip = (course) => + isFree(course) + ? renderStateChip("mutuals", "✓", i18n.schoolFree) + : span({ class: "price-chip" }, `${Number(course.price).toFixed(2)} ECO`) + +const renderCourseChips = (course, subscription = null) => + div({ class: "card-chips-row" }, + course.visibility === "INVITE" ? renderStateChip("whole", "\uD83D\uDD11", i18n.chatStatusInviteOnly) : null, + course.status === "CLOSED" + ? renderStateChip("closed", "✗", i18n.schoolClosed) + : renderStateChip("mutuals", "✓", i18n.schoolOngoing), + renderPriceChip(course), + course.clearnet === true && clearnetEligible(course) ? renderReachChip(true, i18n, clearnetItemHref('school', course.title, course.id)) : null, + subscription + ? (subscription.in + ? renderStateChip("mutuals", "\u2709", i18n.subscriptionOn) + : renderStateChip("closed", "\u2709", i18n.subscriptionOff)) + : null + ) + +const renderCourseCard = (course, filter, params = {}) => { + const url = `/school/course/${encodeURIComponent(course.id)}` + const isOwn = String(course.author) === String(userId) + return div({ class: "tribe-card" }, + div({ class: "card-header activity-card-header" }, + span(), + renderContentActions(course.id, url, { + spread: (params.spreadMap && params.spreadMap.get(course.id)) || null, + author: course.author, + favKind: 'school', + isFavorite: course.isFavorite, + reportTitle: course.title, + returnTo: buildReturnTo(filter, params), + deleteAction: isOwn ? contentDeleteAction('schoolCourse', course.id) : undefined + }) + ), + div({ class: "tribe-card-image-wrapper" }, + a({ href: url }, + renderMediaBlob(course.image, '/assets/images/default-avatar.png', { class: 'tribe-card-hero-image' }) + ) + ), + div({ class: "tribe-card-body" }, + h2({ class: "tribe-card-title" }, a({ href: url }, course.title)), + renderStarRating(course.opinions, safeArr(course.opinions_inhabitants).length), + renderCourseChips(course, params.subscriptions && (String(course.author) === String(userId) || safeArr(course.students).includes(userId)) ? { in: params.subscriptions.mine.has(course.rootId || course.id) || String(course.author) === String(userId) } : null), + course.description ? p({ class: "tribe-card-description" }, ...renderStyledText(course.description)) : null, + div({ class: "tribe-card-members" }, + span({ class: "tribe-members-count" }, `${i18n.schoolStudents}: ${course.students.length}`) + ), + course.startDate ? div({ class: "tribe-card-members" }, + span({ class: "tribe-members-count" }, `${i18n.schoolStartDate}: ${moment(course.startDate).format("YYYY/MM/DD HH:mm")}`) + ) : null + ) + ) +} + +const renderCourseForm = (filter, course = {}, params = {}) => { + const isEdit = filter === "edit" + const reachRaw = String(params.reach || "").toUpperCase() + const reach = COURSE_TYPES.includes(reachRaw) ? reachRaw : courseTypeOf(course) + return div({ class: "create-tribe-form" }, + h2(isEdit ? i18n.schoolUpdateSectionTitle : i18n.schoolCreateSectionTitle), + form({ method: "GET", action: "/school" }, + input({ type: "hidden", name: "filter", value: isEdit ? "edit" : "create" }), + isEdit ? input({ type: "hidden", name: "courseId", value: course.id || "" }) : null, + label(i18n.schoolCourseType), br(), + div({ class: "apply-row" }, + select({ name: "courseType", class: "report-category-select" }, + option({ value: "OPEN", ...(reach === "OPEN" ? { selected: true } : {}) }, i18n.schoolTypeOpen), + option({ value: "PAID", ...(reach === "PAID" ? { selected: true } : {}) }, i18n.schoolTypePaid), + option({ value: "INVITE", ...(reach === "INVITE" ? { selected: true } : {}) }, i18n.schoolTypeInvite) + ), + button({ type: "submit", class: "create-button" }, i18n.apply || "Apply") + ) + ), + hr({ class: "form-sep" }), + h2({ class: "report-category-fixed" }, courseTypeLabel(reach)), + form({ action: isEdit ? `/school/update/${encodeURIComponent(course.id || "")}` : "/school/create", method: "POST", enctype: "multipart/form-data" }, + input({ type: "hidden", name: "courseType", value: reach }), + label(i18n.title), br, + input({ type: "text", name: "title", maxlength: "120", required: true, placeholder: i18n.schoolCourseTitlePlaceholder, value: course.title || "" }), br(), + label(i18n.description), br, + textarea({ name: "description", rows: 5, required: true, maxlength: "2000", placeholder: i18n.schoolCourseDescriptionPlaceholder }, course.description || ""), br, + label(i18n.blogImage), br, + input({ type: "file", name: "image", accept: "image/*" }), br(), br(), + label(i18n.schoolTags), br, + input({ type: "text", name: "tags", placeholder: i18n.schoolTagsPlaceholder, value: Array.isArray(course.tags) ? course.tags.join(", ") : (course.tags || "") }), br, + ...(reach === "PAID" + ? [ + label(i18n.schoolPrice), br, + input({ type: "number", name: "price", step: "0.000001", min: "0.000001", required: true, value: course.price && Number(course.price) > 0 ? course.price : "" }), renderEcoValueChip(), br(), br() + ] + : reach === "INVITE" + ? [ + label(i18n.schoolPrice), br, + input({ type: "number", name: "price", step: "0.000001", min: "0", value: course.price && Number(course.price) > 0 ? course.price : "0" }), renderEcoValueChip(), br(), br() + ] + : [input({ type: "hidden", name: "price", value: "0" })]), + label(i18n.schoolStartDate), br, + input({ type: "datetime-local", name: "startDate", min: moment().format("YYYY-MM-DDTHH:mm"), value: course.startDate ? moment(course.startDate).format("YYYY-MM-DDTHH:mm") : "" }), br(), + isEdit + ? div( + label(i18n.schoolStatus), br, + select({ name: "status" }, + option({ value: "ONGOING", ...((course.status || "ONGOING") === "ONGOING" ? { selected: true } : {}) }, i18n.schoolOngoing), + option({ value: "CLOSED", ...(course.status === "CLOSED" ? { selected: true } : {}) }, i18n.schoolClosed) + ), br() + ) + : null, + reach === "OPEN" && clearnetEligible({ ...course, visibility: "PUBLIC", price: "0" }) ? renderClearnetSelector(course.clearnet === true || String(course.clearnet || "") === "1", i18n) : null, + br(), + button({ type: "submit" }, isEdit ? i18n.schoolUpdate : i18n.schoolCreate) + ) + ) +} + +exports.schoolView = async (courses, filter, courseToEdit = null, params = {}) => { + const q = safeText(params.q || "") + const sort = safeText(params.sort || "recent") + const list = safeArr(courses) + const emptyMod = moduleIsEmpty(list, filter || "all", "all", q) + const title = i18n.schoolTitle + const isForm = filter === "create" || filter === "edit" + + return template( + title, + section(div({ class: "tags-header module-header-line" }, h2(title), p(i18n.schoolDescription), renderWalletChip())), + section(renderModeButtons(filter, emptyMod, (params && params.modesAvail) || null)), + !isForm && !emptyMod + ? section( + div({ class: "filters activity-filter-chips activity-toolbar-row" }, + renderModuleStatsBy(list, c => String(c.status || 'ONGOING').toUpperCase(), [{ value: 'ONGOING', label: i18n.schoolOngoing }, { value: 'CLOSED', label: i18n.schoolClosed }]), + form({ method: "GET", action: "/school", class: "filter-box" }, + input({ type: "hidden", name: "filter", value: filter }), + input({ type: "text", name: "q", value: q, placeholder: i18n.schoolSearchPlaceholder, class: "filter-box__input" }), + div({ class: "filter-box__controls" }, + select({ name: "sort", class: "filter-box__select" }, + option({ value: "recent", ...(sort === "recent" ? { selected: true } : {}) }, i18n.documentSortRecent), + option({ value: "students", ...(sort === "students" ? { selected: true } : {}) }, i18n.documentSortTop), + option({ value: "title", ...(sort === "title" ? { selected: true } : {}) }, i18n.schoolSortTitle) + ), + button({ type: "submit", class: "filter-box__button" }, i18n.searchButton) + ) + ) + ) + ) + : null, + section( + isForm + ? renderCourseForm(filter, filter === "edit" ? (courseToEdit || {}) : (params.draft || {}), params) + : div({ class: "tribe-grid" }, + list.length + ? paged(list).map(course => renderCourseCard(course, filter, params)) + : p(i18n.schoolNoCourses) + ) + ) + ) +} + +const renderLesson = (lesson, course, isTeacher, returnTo, isStudent = false, isNext = false) => + lesson.locked + ? div({ class: "school-lesson school-lesson-locked" }, + h3("🔒"), + p(i18n.schoolLessonLocked), + div({ class: "school-lesson-meta" }, span(moment(lesson.createdAt).format("YYYY/MM/DD"))) + ) + : div({ class: isNext ? "school-lesson school-lesson-next" : "school-lesson" }, + details({ class: "school-lesson-details", ...(isNext ? { open: true } : {}) }, + summary({ class: "school-lesson-summary" }, + span({ class: "school-lesson-summary-title" }, (lesson.completed ? "✓ " : isNext ? "▶ " : "") + lesson.title), + span({ class: "school-lesson-summary-meta" }, + lesson.sessionDate ? `${moment(lesson.sessionDate).format("YYYY/MM/DD HH:mm")} · ` : "", + moment(lesson.createdAt).format("YYYY/MM/DD") + ) + ), + div({ class: "school-lesson-body" }, + renderMd(lesson.text), + div({ class: "school-lesson-actions" }, + a({ href: `/school/lesson/${encodeURIComponent(course.id)}/${encodeURIComponent(lesson.id)}`, class: "filter-btn" }, i18n.schoolLessonOpen || "Open lesson"), + isTeacher + ? a({ href: `/school/lesson/${encodeURIComponent(course.id)}/${encodeURIComponent(lesson.id)}?edit=1`, class: "filter-btn" }, i18n.chatUpdate) + : null, + isStudent + ? form({ method: "POST", action: `/school/lesson/complete/${encodeURIComponent(course.id)}/${encodeURIComponent(lesson.id)}`, class: "inline-form" }, + input({ type: "hidden", name: "value", value: lesson.completed ? "false" : "true" }), + input({ type: "hidden", name: "returnTo", value: returnTo }), + button({ type: "submit", class: lesson.completed ? "tribe-action-btn school-complete-btn school-complete-btn--done" : "tribe-action-btn school-complete-btn" }, lesson.completed ? `✓ ${i18n.schoolProgressDone}` : i18n.schoolProgressMark) + ) + : null + ) + ) + ) + ) + +const renderTeacherPanel = (course, certificates, returnTo, lessons = [], exams = [], progress = {}, certStudent = "") => + div({ class: "create-tribe-form school-teacher-panel" }, + h2(i18n.schoolTeacherPanel), + div({ class: "school-panel-box" }, + h3(i18n.schoolAddLesson), + form({ method: "POST", action: `/school/lesson/add/${encodeURIComponent(course.id)}` }, + input({ type: "hidden", name: "returnTo", value: returnTo }), + input({ type: "text", name: "title", maxlength: "120", required: true, placeholder: i18n.schoolLessonTitlePlaceholder }), br(), + textarea({ name: "text", rows: 5, required: true, maxlength: "4000", placeholder: i18n.schoolLessonTextPlaceholder }), br, + label(i18n.schoolLessonUnit), br, + input({ type: "text", name: "unit", maxlength: "60", placeholder: i18n.schoolLessonUnitPlaceholder }), br(), + label(i18n.schoolLessonOrder), br, + input({ type: "number", name: "order", min: "0", step: "1" }), br(), br(), + label(i18n.schoolSessionDate), br, + input({ type: "datetime-local", name: "sessionDate", min: moment().format("YYYY-MM-DDTHH:mm") }), br(), br(), + button({ type: "submit" }, i18n.schoolAddLesson) + ) + ), + isProtected(course) && safeArr(lessons).length + ? div({ class: "school-panel-box" }, + h3(i18n.schoolExamCreate), + form({ method: "POST", action: `/school/exam/create/${encodeURIComponent(course.id)}` }, + input({ type: "hidden", name: "returnTo", value: returnTo }), + input({ type: "text", name: "title", maxlength: "120", required: true, placeholder: i18n.schoolExamTitlePlaceholder }), br(), + label(i18n.schoolLessons), br, + select({ name: "lessonId" }, + safeArr(lessons).map(lesson => option({ value: lesson.id }, lesson.title || lesson.id)) + ), br(), br(), + button({ type: "submit" }, i18n.schoolExamCreate) + ) + ) + : null, + (() => { + const openExams = safeArr(exams).filter(x => !x.locked) + return openExams.length + ? div({ class: "school-panel-box" }, + h3(i18n.schoolExamCreateQuestion), + form({ method: "POST", action: `/school/exam/question/add/${encodeURIComponent(course.id)}` }, + input({ type: "hidden", name: "returnTo", value: returnTo }), + select({ name: "examId" }, + openExams.map(x => option({ value: x.id }, x.title || x.id)) + ), br(), br(), + label(i18n.schoolExamAddQuestion), br, + input({ type: "text", name: "q", required: true, maxlength: "300" }), br(), + input({ type: "text", name: "o1", required: true, maxlength: "200", placeholder: `${i18n.schoolExamOption} 1` }), br(), + input({ type: "text", name: "o2", required: true, maxlength: "200", placeholder: `${i18n.schoolExamOption} 2` }), br(), + input({ type: "text", name: "o3", required: true, maxlength: "200", placeholder: `${i18n.schoolExamOption} 3` }), br(), + input({ type: "text", name: "o4", required: true, maxlength: "200", placeholder: `${i18n.schoolExamOption} 4` }), br(), + label(i18n.schoolExamCorrect), br, + select({ name: "correct" }, + option({ value: "0" }, `${i18n.schoolExamOption} 1`), + option({ value: "1" }, `${i18n.schoolExamOption} 2`), + option({ value: "2" }, `${i18n.schoolExamOption} 3`), + option({ value: "3" }, `${i18n.schoolExamOption} 4`) + ), br(), br(), + button({ type: "submit" }, i18n.schoolExamAddQuestion) + ) + ) + : null + })(), + course.students.length + ? div({ class: "school-panel-box", id: "school-cert" }, + h3(i18n.schoolIssueCertificate), + form({ method: "POST", action: `/school/certificate/${encodeURIComponent(course.id)}` }, + input({ type: "hidden", name: "returnTo", value: returnTo }), + input({ type: "text", name: "student", required: true, list: "school-cert-students", placeholder: "@…", maxlength: "120", value: (() => { + const id = safeText(certStudent) + if (!id) return "" + const nick = String(nameCache.get(id) || "").trim() + return nick ? `${id} (${nick})` : id + })() }), + datalist({ id: "school-cert-students" }, + (() => { + const readyExams = safeArr(exams).filter(x => !x.locked && safeArr(x.questions).length) + return course.students + .filter(s => !safeArr(certificates).some(cert => cert.student === s)) + .filter(s => (Number((progress || {})[s]) || 0) >= safeArr(lessons).length) + .filter(s => readyExams.filter(x => safeArr(x.results).some(r => r.author === s && r.passed)).length >= readyExams.length) + .map(s => { + const nick = String(nameCache.get(s) || "").trim() + return option({ value: nick ? `${s} (${nick})` : s }) + }) + })() + ), br(), + textarea({ name: "text", rows: 3, maxlength: "500", placeholder: i18n.schoolCertificateTextPlaceholder }), br, + button({ type: "submit" }, i18n.schoolIssueCertificate) + ) + ) + : null, + safeArr(course.pending).length + ? div({ class: "school-pending-list" }, + p(`${i18n.schoolPendingStudents}: `), + course.pending.map(pen => + p( + userLink(pen.author), + pen.transferId ? span(" — ") : null, + pen.transferId ? a({ href: `/transfers/${encodeURIComponent(pen.transferId)}` }, i18n.schoolViewBill) : null + ) + ), + br() + ) + : null, + !isFree(course) || course.visibility === "INVITE" + ? (() => { + const granted = safeArr(course.granted) + const ungrantedStudents = safeArr(course.students).filter(st => !granted.includes(st)) + return ungrantedStudents.length + ? div({ class: "school-grant-list" }, + p(`${i18n.schoolGrantAccess}: `), + ungrantedStudents.map(st => + form({ method: "POST", action: `/school/grant/${encodeURIComponent(course.id)}` }, + input({ type: "hidden", name: "student", value: st }), + input({ type: "hidden", name: "returnTo", value: returnTo }), + button({ type: "submit", class: "filter-btn" }, `🔑 ${st.slice(0, 10)}...`) + ) + ), + br() + ) + : null + })() + : null, + null + ) + +exports.singleCourseView = async (course, lessons = [], certificates = [], params = {}) => { + const courseApproved = params.approved === true + const returnTo = `/school/course/${encodeURIComponent(course.id)}` + const isTeacher = String(course.author) === String(userId) + const isStudent = course.students.includes(userId) + const myPending = safeArr(course.pending).find(p => p.author === userId) || null + const isPaid = !isFree(course) + const canClearnet = clearnetEligible(course) + const isClearnet = canClearnet && !!course.clearnet + lessons = safeArr(lessons) + certificates = safeArr(certificates) + const myCompleted = lessons.filter(lesson => lesson.completed).length + const nextLesson = isStudent ? lessons.find(l => !l.locked && !l.completed) : null + + const courseSide = div({ class: "tribe-side" }, + div({ class: "card-header activity-card-header" }, + renderContentActions(course.id, returnTo, { spread: params.spreads || null, author: course.author, favKind: 'school', isFavorite: course.isFavorite, returnTo, reportTitle: course.title, deleteAction: isTeacher ? contentDeleteAction('schoolCourse', course.id) : undefined }) + ), + h2({ class: "tribe-card-title" }, course.title), + renderStarRating(course.opinions, safeArr(course.opinions_inhabitants).length), + div({ class: "card-chips-row" }, + course.visibility === "INVITE" ? renderStateChip("whole", "\uD83D\uDD11", i18n.chatStatusInviteOnly) : null, + course.status === "CLOSED" + ? renderStateChip("closed", "✗", i18n.schoolClosed) + : renderStateChip("mutuals", "✓", i18n.schoolOngoing), + renderPriceChip(course), + renderReachChip(isClearnet, i18n, isClearnet ? clearnetItemHref('school', course.title, course.id) : null), + isTeacher && canClearnet ? renderClearnetSwitch('school', course.rootId || course.id, isClearnet) : null, + courseApproved ? renderStateChip("mutuals", "🎓", i18n.schoolApproved) : null, + params.subscription && (isTeacher || isStudent) + ? ((isTeacher || params.subscription.subscribed === true) + ? renderStateChip("mutuals", "\u2709", i18n.subscriptionOn) + : renderStateChip("closed", "\u2709", i18n.subscriptionOff)) + : null + ), + renderMediaBlob(course.image, '/assets/images/default-avatar.png', { class: 'tribe-detail-image' }), + course.description ? p({ class: "tribe-side-description" }, ...renderStyledText(course.description)) : null, + table({ class: "tribe-info-table" }, + tr( + td({ class: "tribe-info-label" }, i18n.schoolCreatedAt), + td({ class: "tribe-info-value", colspan: "3" }, moment(course.createdAt).format("YYYY/MM/DD HH:mm")) + ), + course.startDate ? tr( + td({ class: "tribe-info-label" }, i18n.schoolStartDate), + td({ class: "tribe-info-value", colspan: "3" }, moment(course.startDate).format("YYYY/MM/DD HH:mm")) + ) : null, + tr( + td({ class: "tribe-info-label" }, i18n.schoolTeacher), + td({ class: "tribe-info-value", colspan: "3" }, userLink(course.author)) + ) + ), + div({ class: "tribe-card-members" }, + span({ class: "tribe-members-count" }, `${i18n.schoolStudents}: ${course.students.length}`) + ), + course.chatId && (isTeacher || isStudent || (course.visibility === "PUBLIC" && isFree(course))) + ? div({ class: "tribe-side-actions shop-visibility-row stacked-action-row" }, + span({ class: "card-label" }, `${i18n.schoolCourseChat}:`), + a({ href: `/chats/${encodeURIComponent(course.chatId)}#chat-latest`, class: "tribe-action-btn" }, i18n.chatVisitChat) + ) + : null, + isStudent && lessons.length + ? div({ class: "tribe-side-actions shop-visibility-row school-progress-side" }, + span({ class: "card-label" }, `${i18n.schoolProgress}:`), + progress({ class: "school-progress-bar", value: String(myCompleted), max: String(lessons.length) }), + span({ class: "school-progress-count" }, `${myCompleted}/${lessons.length}`) + ) + : null, + !isTeacher + ? (myPending + ? div({ class: "school-pending-block" }, + div({ class: "tribe-side-actions school-pending-row" }, + renderStateChip("closed", "⧖", i18n.schoolPendingPayment) + ), + myPending.transferId + ? div({ class: "doc-export-actions" }, + form({ method: "GET", action: `/transfers/contract/${encodeURIComponent(myPending.transferId)}`, class: "transfer-contract-form" }, + button({ type: "submit", class: "filter-btn" }, i18n.transfersExportContract) + ), + form({ method: "POST", action: `/transfers/${encodeURIComponent(myPending.transferId)}/share` }, + button({ type: "submit", class: "filter-btn" }, i18n.sharePm) + ) + ) + : null + ) + : !isStudent && course.status === "ONGOING" && (course.visibility === "PUBLIC" || course.invited.includes(userId)) + ? div({ class: "tribe-side-actions shop-visibility-row stacked-action-row" }, + span({ class: "card-label" }, `${i18n.schoolEnrollLabel}:`), + form({ method: "POST", action: `/school/enroll/${encodeURIComponent(course.id)}` }, + button({ type: "submit", class: "tribe-action-btn" }, isPaid ? `${i18n.schoolEnrollButton} (${Number(course.price).toFixed(2)} ECO)` : i18n.schoolEnrollButton) + ) + ) + : !isStudent + ? p(i18n.schoolInviteRequired) + : null) + : null, + isTeacher && course.visibility === "INVITE" && !course.inviteCode + ? div({ class: "tribe-side-actions shop-visibility-row" }, + span({ class: "card-label" }, `${i18n.schoolVisibilityInvite}: `), + form({ method: "POST", action: `/school/generate-invite/${encodeURIComponent(course.id)}` }, + button({ type: "submit", class: "tribe-action-btn" }, i18n.tribeGenerateInvite) + ) + ) + : null, + isTeacher && course.visibility === "INVITE" && course.inviteCode + ? div({ class: "tribe-side-actions school-invite-code-block" }, + div({ class: "tribe-open-invite" }, + span({ class: "card-label" }, i18n.tribeInviteCodeText), + span({ class: "tribe-open-invite-code" }, course.inviteCode), + renderInviteQrCard({ qrDataUrl: `/qr-invite-code/school/${encodeURIComponent(course.inviteCode)}` }) + ) + ) + : null, + isTeacher && course.visibility === "INVITE" + ? div({ class: "tribe-side-actions stacked-action-row school-invite-students" }, + form({ method: "POST", action: `/school/invite/${encodeURIComponent(course.id)}` }, + label({ class: "card-label" }, `${i18n.schoolInviteStudents}:`), br, + input({ type: "text", name: "students", required: true, placeholder: i18n.schoolInvitePlaceholder }), br(), + button({ type: "submit", class: "tribe-action-btn" }, i18n.schoolInviteButton) + ), + course.invited.length + ? div({ class: "school-invited-list" }, + p(`${i18n.schoolInvited}: `), + course.invited.map(s => p(userLink(s))) + ) + : null + ) + : null, + params.subscription && (isTeacher || isStudent) + ? renderSubscriptionBox({ + target: course.rootId || course.id, + scope: "school", + subscribed: params.subscription.subscribed === true, + count: params.subscription.count, + isOwner: isTeacher, + returnTo + }) + : null, + safeArr(course.tags).length + ? div({ class: "tribe-side-tags" }, safeArr(course.tags).map(tag => a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link" }, `#${tag}`))) + : null, + isTeacher + ? div({ class: "tribe-side-actions shop-visibility-row" }, + span({ class: "card-label" }, `${i18n.schoolStatus}: `), + course.status === "CLOSED" + ? renderStateChip("closed", "✗", i18n.schoolClosed) + : renderStateChip("mutuals", "✓", i18n.schoolOngoing), + form({ method: "POST", action: `/school/status/${encodeURIComponent(course.id)}`, class: "inline-form" }, + input({ type: "hidden", name: "returnTo", value: returnTo }), + input({ type: "hidden", name: "status", value: course.status === "CLOSED" ? "ONGOING" : "CLOSED" }), + button({ type: "submit", class: "tribe-action-btn" }, course.status === "CLOSED" ? i18n.schoolReopenCourse : i18n.schoolCloseCourse) + ) + ) + : null, + isTeacher + ? div({ class: "tribe-side-actions owner-actions" }, + form({ method: "GET", action: "/school" }, + input({ type: "hidden", name: "filter", value: "edit" }), + input({ type: "hidden", name: "courseId", value: course.id }), + button({ type: "submit", class: "tribe-action-btn" }, i18n.chatUpdate) + ) + ) + : null, + !isTeacher && (isStudent || myPending) + ? div({ class: "tribe-side-actions owner-actions" }, + form({ method: "POST", action: `/school/unenroll/${encodeURIComponent(course.id)}` }, + button({ type: "submit", class: "tribe-action-btn danger-btn" }, i18n.schoolUnenrollButton) + ) + ) + : null + ) + + const exams = safeArr(params.exams) + const readyExams = exams.filter(x => !x.locked && safeArr(x.questions).length) + + const teacherProgress = isTeacher && safeArr(course.students).length + ? div({ class: "school-progress-list" }, + h2(`${i18n.schoolStudents} (${course.students.length})`), + table({ class: "school-students-table" }, + tr( + th(i18n.schoolStudents), + th(i18n.schoolLessons), + th(i18n.schoolExams), + th(i18n.schoolCertified) + ), + course.students.map(student => { + const done = Number((params.progress || {})[student]) || 0 + const passedExams = readyExams.filter(x => safeArr(x.results).some(r => r.author === student && r.passed)).length + const failedExams = readyExams.filter(x => safeArr(x.results).some(r => r.author === student && !r.passed) && !safeArr(x.results).some(r => r.author === student && r.passed)) + const cert = safeArr(certificates).find(c => c.student === student) || null + const eligible = done >= lessons.length && passedExams >= readyExams.length + return tr({ class: eligible ? "school-row-passed" : "" }, + td(userLink(student)), + td(`${done}/${lessons.length}`), + td( + readyExams.length ? `${passedExams}/${readyExams.length}` : "—", + failedExams.length + ? span({ class: "school-progress-failed", title: failedExams.map(x => x.title).join(", ") }, ` ✗${failedExams.length}`) + : null + ), + td(cert + ? a({ href: `/school/certificate/pdf/${encodeURIComponent(course.id)}/${encodeURIComponent(cert.id)}`, class: "filter-btn school-cert-pdf" }, `⬇ ${i18n.schoolCertificatePdf || "PDF"}`) + : eligible + ? a({ href: `${returnTo}?cert=${encodeURIComponent(student)}#school-cert`, class: "filter-btn school-cert-pdf" }, i18n.schoolIssueCertificate) + : span({ class: "school-progress-failed" }, "✗")) + ) + }) + ) + ) + : null + + const studentPanel = isStudent && !isTeacher + ? (() => { + const myPassed = readyExams.filter(x => x.myResult && x.myResult.passed).length + const myFailed = readyExams.filter(x => x.myResult && !x.myResult.passed) + return div({ class: "school-progress-list" }, + h2(i18n.schoolStudentPanel), + table({ class: "school-students-table" }, + tr( + th(i18n.schoolLessons), + th(i18n.schoolExams), + th(i18n.schoolCertified) + ), + tr( + td(`${myCompleted}/${lessons.length}`), + td( + readyExams.length ? `${myPassed}/${readyExams.length}` : "—", + myFailed.length + ? span({ class: "school-progress-failed", title: myFailed.map(x => x.title).join(", ") }, ` ✗${myFailed.length}`) + : null + ), + td((() => { + const cert = safeArr(certificates).find(c => c.student === userId) || null + return cert + ? a({ href: `/school/certificate/pdf/${encodeURIComponent(course.id)}/${encodeURIComponent(cert.id)}`, class: "filter-btn school-cert-pdf" }, `⬇ ${i18n.schoolCertificatePdf || "PDF"}`) + : span({ class: "school-progress-failed" }, "✗") + })()) + ) + ) + ) + })() + : null + + const examsBlock = (isTeacher || isStudent) && exams.length + ? div({ class: "school-exams" }, + h2(`${i18n.schoolExams} (${isTeacher ? exams.length : readyExams.length})`), + (isTeacher ? exams : readyExams).map(exam => + exam.locked + ? null + : div({ class: "school-lesson school-exam" }, + isTeacher + ? details({ class: "school-lesson-details" }, + summary({ class: "school-lesson-summary" }, + span({ class: "school-lesson-summary-title" }, `${exam.title} (${safeArr(exam.questions).length})`) + ), + div({ class: "school-lesson-body" }, + exam.questions.map((question, qi) => + div({ class: "school-exam-question" }, + p(`${qi + 1}. ${question.q}`), + question.options.map((opt, oi) => + p({ class: oi === question.correct ? "school-exam-correct" : "school-exam-option-row" }, `${oi === question.correct ? "✓" : "·"} ${opt}`) + ), + form({ method: "POST", action: `/school/exam/question/delete/${encodeURIComponent(course.id)}/${encodeURIComponent(exam.id)}/${encodeURIComponent(question.id)}` }, + input({ type: "hidden", name: "returnTo", value: returnTo }), + button({ type: "submit", class: "tribe-action-btn danger-btn" }, i18n.chatDelete) + ) + ) + ), + form({ method: "POST", action: `/school/exam/delete/${encodeURIComponent(course.id)}/${encodeURIComponent(exam.id)}` }, + input({ type: "hidden", name: "returnTo", value: returnTo }), + button({ type: "submit", class: "tribe-action-btn danger-btn" }, i18n.chatDelete) + ) + ) + ) + : (() => { + const res = exam.myResult + const blocked = res && (res.passed || Date.now() - Number(res.at || 0) < 24 * 60 * 60 * 1000) + const chip = res && res.passed + ? renderStateChip("mutuals", "✓", i18n.schoolExamPassed) + : blocked + ? renderStateChip("closed", "✗", i18n.schoolExamFailed) + : span({ class: "state-chip" }, i18n.schoolExamPending) + return details({ class: "school-lesson-details" }, + summary({ class: "school-lesson-summary" }, + span({ class: "school-lesson-summary-title" }, `${exam.title} (${safeArr(exam.questions).length})`), + chip + ), + div({ class: "school-lesson-body" }, + blocked + ? div( + p(`${i18n.schoolExamScore}: ${res.score}/10 — ${res.passed ? i18n.schoolExamPassed : i18n.schoolExamFailed}`), + !res.passed ? p({ class: "school-exam-retry" }, i18n.schoolExamRetryLater) : null + ) + : form({ method: "POST", action: `/school/exam/take/${encodeURIComponent(course.id)}/${encodeURIComponent(exam.id)}` }, + input({ type: "hidden", name: "returnTo", value: returnTo }), + exam.questions.map((question, qi) => + div({ class: "school-exam-question" }, + p(question.q), + question.options.map((opt, oi) => + label({ class: "school-exam-option" }, + input({ type: "radio", name: `q${qi}`, value: String(oi), required: true }), + ` ${opt}` + ) + ) + ) + ), + button({ type: "submit" }, i18n.schoolExamSubmit) + ) + ) + ) + })() + ) + ), + null + ) + : null + + const lessonItem = (lesson) => renderLesson(lesson, course, isTeacher, returnTo, isStudent, nextLesson && lesson.id === nextLesson.id) + const unitGroups = (() => { + const groups = new Map() + for (const lesson of lessons) { + const unit = safeText(lesson.unit) + if (!groups.has(unit)) groups.set(unit, []) + groups.get(unit).push(lesson) + } + return [...groups.entries()] + })() + + const lessonsBlock = div({ class: "school-lessons" }, + h2(`${i18n.schoolLessons} (${lessons.length})`), + isTeacher || isStudent || isFree(course) + ? (lessons.length + ? unitGroups.map(([unit, items]) => + unit + ? details({ class: "school-unit", ...(isTeacher ? {} : { open: true }) }, + summary({ class: "school-unit-summary" }, `${unit} (${items.length})`), + items.map(lessonItem) + ) + : items.map(lessonItem) + ) + : p(i18n.schoolNoLessons)) + : p(i18n.schoolEnrollToSee) + ) + + return template( + course.title, + section(div({ class: "tags-header module-header-line" }, h2(i18n.schoolTitle), p(i18n.schoolDescription), renderWalletChip())), + section(renderModeButtons("all", false, (params && params.modesAvail) || null)), + section( + div({ class: "tribe-details" }, + courseSide, + div({ class: "tribe-main" }, + lessonsBlock, + examsBlock, + teacherProgress, + studentPanel, + isTeacher ? renderTeacherPanel(course, certificates, returnTo, lessons, exams, params.progress, params.certStudent) : null, + renderEngagement(course.id, + isStudent && !isTeacher + ? renderOpinionsVoting('/school/opinions', course.id, course.opinions, returnTo, course.opinions_inhabitants) + : null, + null + ) + ) + ) + ) + ) +} + +exports.clearnetCourseView = async (course, lessons = []) => { + const { escapeHtml: esc, blobUrl: cnBlob, renderRichText, renderKindTag, renderClearnetPage } = require("./clearnet_view") + const title = esc(course.title || i18n.cnKindCourse) + const desc = renderRichText(course.description || "") + const courseImg = cnBlob(course.image) + const visibleLessons = safeArr(lessons).filter(lesson => !lesson.locked) + const extraCss = ` +.cn-course-title{color:var(--fg);margin:0 0 16px 0;font-size:32px;font-weight:700} +.cn-course-meta{display:flex;flex-wrap:wrap;gap:10px;margin-bottom:20px} +.cn-course-meta-item{background:var(--bg-sub);border:1px solid var(--border);border-radius:6px;padding:8px 14px;font-size:14px;color:var(--fg-soft);display:inline-flex;align-items:center;gap:6px} +.cn-course-img{display:block;max-width:100%;border:1px solid var(--border);border-radius:8px;margin-bottom:20px} +.cn-course-section h2{color:var(--fg);font-size:18px;text-transform:uppercase;letter-spacing:2px;margin:24px 0 10px;padding-bottom:6px;border-bottom:1px solid var(--border)} +.cn-course-section p{color:var(--fg-soft);white-space:pre-wrap;line-height:1.6;font-size:15px} +.cn-lesson{background:var(--bg-sub);border:1px solid var(--border);border-radius:8px;padding:14px 18px;margin-bottom:12px} +.cn-lesson h3{color:var(--fg);margin:0 0 8px 0;font-size:16px} +.cn-lesson p{color:var(--fg-soft);white-space:pre-wrap;line-height:1.6;font-size:14px;margin:0} +.cn-lesson-date{color:var(--fg-dim);font-size:12px;margin-top:8px} +` + const body = ` +

${title}

+
+ ${renderKindTag('course')} + 🎓 ${esc(i18n.cnOpenCourse)} + ${course.startDate ? `📅 ${esc(new Date(course.startDate).toISOString().slice(0, 10))}` : ""} + 👥 ${safeArr(course.students).length} ${esc(i18n.schoolStudents)} +
+
+ ${courseImg ? `${title}` : ""} + ${desc ? `

${esc(i18n.description)}

${desc}

` : ""} + ${visibleLessons.length ? `

${esc(i18n.schoolLessons)} (${visibleLessons.length})

${visibleLessons.map(lesson => ` +
+

${esc(lesson.title || "")}

+

${esc(lesson.text || "")}

+ ${lesson.sessionDate ? `
📅 ${esc(new Date(lesson.sessionDate).toISOString().slice(0, 10))}
` : ""} +
`).join("")}
` : ""} +` + return renderClearnetPage({ + title: `${course.title || "Course"} | Oasis`, + ogTitle: course.title || "Course", + ogDescription: course.description || "", + ogImage: courseImg, + extraCss, + body, + hubFeedId: course.author || null + }) +} + +const renderMaterial = (material, course, isTeacher) => { + const { img, video, audio } = require("../server/node_modules/hyperaxe") + const m = String(material.media || "") + const ref = m.match(/\[(image|audio|video|pdf|torrent):([^\]]*)\]\(\s*(&[^)\s]+\.sha256)\s*\)/) || m.match(/^!?\[([^\]]*)\]\(\s*(&[^)\s]+\.sha256)\s*\)$/m) + let body + if (material.locked) body = p(i18n.schoolLessonLocked) + else if (ref && ref.length === 4) { + const kind = ref[1], name = ref[2], blobId = ref[3] + const url = `/blob/${encodeURIComponent(blobId)}` + if (kind === "pdf") body = div({ class: "pdf-viewer-container", id: `pdf-${material.id.slice(1, 9)}`, "data-pdf-url": url }, a({ href: url }, `📎 ${name || "PDF"}`)) + else if (kind === "image") body = img({ src: url, alt: name, class: "school-material-image" }) + else if (kind === "video") body = video({ controls: true, src: url, class: "post-video", preload: 'metadata' }) + else if (kind === "audio") body = audio({ controls: true, src: url }) + else body = p(a({ href: url }, `📎 ${name || blobId}`)) + } else if (ref) { + body = p(a({ href: `/blob/${encodeURIComponent(ref[2])}` }, `📎 ${ref[1] || ref[2]}`)) + } else { + body = renderMd(m) + } + return div({ class: "school-material" }, + body, + material.caption ? p({ class: "school-material-caption" }, material.caption) : null, + isTeacher + ? form({ method: "POST", action: `/school/lesson/media/delete/${encodeURIComponent(course.id)}/${encodeURIComponent(material.lessonId)}/${encodeURIComponent(material.id)}` }, + button({ type: "submit", class: "tribe-action-btn danger-btn" }, "DELETE") + ) + : null + ) +} + +exports.singleLessonView = async (course, lesson, materials = [], params = {}) => { + const isTeacher = String(course.author) === String(userId) + const isStudent = course.students.includes(userId) + const courseUrl = `/school/course/${encodeURIComponent(course.id)}` + const lessonUrl = `/school/lesson/${encodeURIComponent(course.id)}/${encodeURIComponent(lesson.id)}` + + const lessonSide = div({ class: "tribe-side" }, + div({ class: "card-header activity-card-header" }, + renderContentActions(lesson.id, lessonUrl, { + author: lesson.author || course.author, + reportTitle: lesson.title || course.title, + returnTo: courseUrl, + deleteAction: String(lesson.author || course.author) === String(userId) ? `/school/lesson/delete/${encodeURIComponent(course.id)}/${encodeURIComponent(lesson.id)}` : undefined + }) + ), + h2({ class: "tribe-card-title" }, a({ href: courseUrl }, course.title)), + renderCourseChips(course), + renderMediaBlob(course.image, '/assets/images/default-avatar.png', { class: 'tribe-detail-image' }), + table({ class: "tribe-info-table" }, + lesson.unit ? tr( + td({ class: "tribe-info-label" }, i18n.schoolLessonUnit), + td({ class: "tribe-info-value", colspan: "3" }, lesson.unit) + ) : null, + lesson.sessionDate ? tr( + td({ class: "tribe-info-label" }, i18n.schoolSessionDate), + td({ class: "tribe-info-value", colspan: "3" }, moment(lesson.sessionDate).format("YYYY/MM/DD HH:mm")) + ) : null, + tr( + td({ class: "tribe-info-label" }, i18n.schoolTeacher), + td({ class: "tribe-info-value", colspan: "3" }, userLink(course.author)) + ) + ), + isStudent + ? div({ class: "tribe-side-actions" }, + form({ method: "POST", action: `/school/lesson/complete/${encodeURIComponent(course.id)}/${encodeURIComponent(lesson.id)}` }, + input({ type: "hidden", name: "value", value: lesson.completed ? "false" : "true" }), + input({ type: "hidden", name: "returnTo", value: lessonUrl }), + button({ type: "submit", class: "tribe-action-btn" }, lesson.completed ? `✓ ${i18n.schoolProgressDone}` : i18n.schoolProgressMark) + ) + ) + : null, + isTeacher + ? div({ class: "tribe-side-actions owner-actions" }, + form({ method: "GET", action: lessonUrl }, + input({ type: "hidden", name: "edit", value: "1" }), + button({ type: "submit", class: "tribe-action-btn" }, i18n.chatUpdate) + ) + ) + : null + ) + + const lessonMain = div({ class: "tribe-main" }, + params.edit && isTeacher + ? div({ class: "create-tribe-form" }, + h2(i18n.chatUpdate), + form({ method: "POST", action: `/school/lesson/update/${encodeURIComponent(course.id)}/${encodeURIComponent(lesson.id)}` }, + label(i18n.title), br, + input({ type: "text", name: "title", maxlength: "120", required: true, value: lesson.title || "" }), br(), + textarea({ name: "text", rows: 5, required: true, maxlength: "4000" }, lesson.text || ""), br, + label(i18n.schoolLessonUnit), br, + input({ type: "text", name: "unit", maxlength: "60", value: lesson.unit || "" }), br(), + label(i18n.schoolLessonOrder), br, + input({ type: "number", name: "order", min: "0", step: "1", value: lesson.order != null ? String(lesson.order) : "" }), br(), br(), + label(i18n.schoolSessionDate), br, + input({ type: "datetime-local", name: "sessionDate", min: moment().format("YYYY-MM-DDTHH:mm"), value: lesson.sessionDate ? moment(lesson.sessionDate).format("YYYY-MM-DDTHH:mm") : "" }), br(), br(), + button({ type: "submit" }, i18n.chatUpdate) + ) + ) + : div({ class: "school-lesson" }, + h2(lesson.title), + renderMd(lesson.text) + ), + div({ class: "school-materials" }, + h2(`${i18n.schoolMaterials} (${safeArr(materials).length})`), + safeArr(materials).length + ? materials.map(material => renderMaterial(material, course, isTeacher)) + : p(i18n.schoolNoMaterials) + ), + isTeacher + ? div({ class: "create-tribe-form" }, + h2(i18n.schoolAddMaterial), + form({ method: "POST", action: `/school/lesson/media/${encodeURIComponent(course.id)}/${encodeURIComponent(lesson.id)}`, enctype: "multipart/form-data" }, + label(i18n.blogImage), br, + input({ type: "file", name: "files", multiple: true, accept: "image/*,video/*,audio/*,application/pdf,.md,.txt,.epub,.zip" }), br(), br(), + label(i18n.title), br, + input({ type: "text", name: "caption", maxlength: "120" }), br(), br(), + label(i18n.description), br, + textarea({ name: "text", rows: 3, maxlength: "2000", placeholder: i18n.schoolMaterialTextPlaceholder }), br, + button({ type: "submit" }, i18n.schoolAddMaterial) + ) + ) + : null + ) + + const tpl = template( + lesson.title, + section(div({ class: "tags-header module-header-line" }, h2(i18n.schoolTitle), p(i18n.schoolDescription), renderWalletChip())), + section(renderModeButtons("all", false, (params && params.modesAvail) || null)), + section(div({ class: "tribe-details" }, lessonSide, lessonMain)) + ) + const hasPdf = safeArr(materials).some(m => !m.locked && /\[pdf:/.test(String(m.media || ""))) + return hasPdf ? `${tpl}` : tpl +} + +exports.schoolInvitePage = (code) => { + return template( + i18n.schoolTitle, + section( + div({ class: "invite-page" }, + h2(i18n.tribeInviteCodeText, " ", code), + form({ method: "GET", action: "/school" }, + button({ type: "submit", class: "filter-btn" }, i18n.walletBack) + ) + ) + ) + ) +} diff --git a/src/views/search_view.js b/src/views/search_view.js index 055519a..e494b3d 100644 --- a/src/views/search_view.js +++ b/src/views/search_view.js @@ -1,77 +1,99 @@ const { form, button, div, h2, p, section, input, label, select, option, img, audio: audioHyperaxe, video: videoHyperaxe, table, hr, hd, br, td, tr, th, a, span } = require("../server/node_modules/hyperaxe"); -const { template, i18n, userLink, renderContentActions } = require('./main_views'); +const RAW_HTML = Symbol.for('oasis.rawHtml'); +const { template, i18n, userLink, renderContentActions, renderModuleStats, CONTENT_FAV_KIND, CONTENT_SPREADABLE, CONTENT_TORRENTABLE, paged, currentPerPage, LIST_PAGE_SIZES, pageSizeLabel } = require('./main_views'); const moment = require("../server/node_modules/moment"); -const { renderTextWithStyles } = require('../backend/renderTextWithStyles'); -const { renderUrl } = require('../backend/renderUrl'); +const { renderStyledHtml, safeExternalHref } = require('../backend/renderStyledText'); +const { renderStyledText } = require('../backend/renderStyledText'); const { sanitizeHtml } = require('../backend/sanitizeHtml'); +const { renderZoomableImage } = require('./gallery_view'); const { config } = require("../server/SSB_server.js"); const userId = config.keys.id; -const decodeMaybe = (s) => { - try { return decodeURIComponent(String(s || '')); } catch { return String(s || ''); } + +const industryStatusLabel = (status) => { + const s = String(status || 'ACTIVE').toUpperCase(); + if (s === 'PAUSED') return i18n.industryStatusPaused || 'PAUSED'; + if (s === 'DISSOLVED') return i18n.industryStatusDissolved || 'DISSOLVED'; + return i18n.industryStatusActive || 'WORKING'; }; -const rewriteHashtagLinks = (html) => { - const s = String(html || ''); - return s.replace(/href=(["'])(?:https?:\/\/[^"']+)?\/hashtag\/([^"'?#]+)([^"']*)\1/g, (m, q, tag, rest) => { - const t = decodeMaybe(tag).replace(/^#/, '').trim().toLowerCase(); - const href = `/search?query=%23${encodeURIComponent(t)}`; - return `href=${q}${href}${q}`; - }); -}; -const searchView = ({ messages = [], blobs = {}, query = "", type = "", types = [], hashtag = null, results = {}, resultCount = "10" }) => { - const searchInput = input({ - id: "search_query", - name: "query", - required: false, - type: "search", - value: query, - placeholder: i18n.searchPlaceholder - }); +const renderSearchForm = ({ action, query = "", queryName = "query", hidden = [] } = {}) => { + const searchInput = input({ id: "search_query", name: queryName, required: false, type: "search", value: query, placeholder: i18n.searchPlaceholder }); searchInput.setAttribute("minlength", 3); + const resultCount = currentPerPage(); + const resultsPerPageSelect = select( + { id: "results-per-page", name: "perPage", class: "input-select search-select" }, + LIST_PAGE_SIZES.map(s => option({ value: s, selected: resultCount === s ? "selected" : undefined }, pageSizeLabel(s))) + ); + return form( + { action, method: "GET", class: "search-form" }, + ...hidden.map(([name, value]) => input({ type: "hidden", name, value })), + div({ class: "search-filters-row" }, + table({ class: "search-filters-table" }, + tr( + td({ class: 'card-label' }, label({ for: "search_from" }, i18n.searchFromLabel || "From")), + td(input({ id: "search_from", type: "datetime-local", name: "from" })) + ), + tr( + td({ class: 'card-label' }, label({ for: "search_to" }, i18n.searchToLabel || "To")), + td(input({ id: "search_to", type: "datetime-local", name: "to" })) + ), + tr( + td({ class: 'card-label' }, label({ for: "search_inhabitant" }, "Oasis ID")), + td(input({ id: "search_inhabitant", type: "text", name: "inhabitant", placeholder: "@...=.ed25519", pattern: "@[A-Za-z0-9+/_\\-]{43}=\\.ed25519", maxlength: 56, class: "search-oasis-id" })) + ) + ), + table({ class: "search-filters-table" }, + tr( + td({ class: 'card-label' }, label({ for: "search_query" }, i18n.searchQueryLabel || "Query")), + td(searchInput) + ), + tr( + td({ class: 'card-label' }, label({ for: "results-per-page" }, i18n.searchPerPageLabel || "Results per page")), + td(resultsPerPageSelect) + ) + ) + ), + div({ class: "search-submit-row" }, + button({ type: "submit" }, i18n.searchSubmit) + ) + ); +}; +exports.renderSearchForm = renderSearchForm; + +const searchView = ({ messages = [], blobs = {}, query = "", type = "", types = [], hashtag = null, results = {}, spreadMap = null, favIndex = null }) => { const contentTypes = [ "post", "about", "curriculum", "tribe", "market", "transfer", "feed", "votes", - "report", "task", "event", "bookmark", "image", "audio", "video", "document", "torrent", - "bankWallet", "bankClaim", "project", "job", "forum", "vote", "contact", "pub", "map", "shop", "shopProduct", "chat", "pad", "all" + "report", "task", "event", "bookmark", "image", "audio", "video", "document", "torrent", "file", + "bankWallet", "bankClaim", "project", "job", "industry", "industryBlueprint", "forum", "vote", "contact", "map", "shop", "shopProduct", "chat", "pad", "room", "schoolCourse", "wikiPage", "emergency", "mailingList", "logisticsRoute", "podcast", "podcastEpisode", "campaign", "all" ]; const filterSelect = select( { id: "search-type", name: "type", - class: "input-select", - style: "position:relative; z-index:10;" + class: "input-select search-select" }, contentTypes.map(type => option({ value: type === 'all' ? "" : type, - selected: (types.length === 0 && type === 'all') || types.includes(type) + ...((types.length === 0 && type === 'all') || types.includes(type) ? { selected: true } : {}) }, i18n[type + "Label"] || type.toUpperCase()) ) ); - const resultsPerPageSelect = select( - { - id: "results-per-page", - name: "resultsPerPage", - class: "input-select", - style: "position:relative; z-index:10;margin-left:10px;" - }, - option({ value: "100", selected: resultCount === "100" ? "selected" : undefined }, "100"), - option({ value: "50", selected: resultCount === "50" ? "selected" : undefined }, "50"), - option({ value: "10", selected: resultCount === "10" ? "selected" : undefined }, "10"), - option({ value: "all", selected: resultCount === "all" ? "selected" : undefined }, i18n.allTypesLabel) - ); const getViewDetailsActionForSearch = (type, contentId, content) => { switch (type) { case 'votes': return `/votes/${encodeURIComponent(contentId)}`; case 'transfer': return `/transfers/${encodeURIComponent(contentId)}`; case 'tribe': return `/tribe/${encodeURIComponent(contentId)}`; - case 'about': return content && (content.about || content.author) ? `/inhabitant/${encodeURIComponent(content.about || content.author)}` : '#'; - case 'curriculum': return content && content.author ? `/inhabitant/${encodeURIComponent(content.author)}` : '#'; + case 'about': return content && (content.about || content.author) ? `/inhabitant/${encodeURIComponent(content.about || content.author)}` : null; + case 'curriculum': return content && content.author ? `/inhabitant/${encodeURIComponent(content.author)}` : null; + case 'feed': return `/feed/${encodeURIComponent(contentId)}`; + case 'poll': return `/polls/${encodeURIComponent(contentId)}`; case 'image': return `/images/${encodeURIComponent(contentId)}`; case 'audio': return `/audios/${encodeURIComponent(contentId)}`; case 'video': return `/videos/${encodeURIComponent(contentId)}`; @@ -79,15 +101,21 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types = case 'bookmark': return `/bookmarks/${encodeURIComponent(contentId)}`; case 'event': return `/events/${encodeURIComponent(contentId)}`; case 'task': return `/tasks/${encodeURIComponent(contentId)}`; - case 'post': return `/thread/${encodeURIComponent(contentId)}#${encodeURIComponent(contentId)}`; + case 'post': return content && typeof content.root === 'string' + ? `/thread/${encodeURIComponent(content.root)}#${encodeURIComponent(contentId)}` + : `/blogs/${encodeURIComponent(contentId)}`; case 'market': return `/market/${encodeURIComponent(contentId)}`; case 'report': return `/reports/${encodeURIComponent(contentId)}`; case 'project': return `/projects/${encodeURIComponent(contentId)}`; + case 'schoolCourse': return `/school/course/${encodeURIComponent(contentId)}`; + case 'housing': return `/housing/${encodeURIComponent(contentId)}`; case 'job': return `/jobs/${encodeURIComponent(contentId)}`; - case 'forum': return `/forum/${encodeURIComponent(contentId)}`; - case 'vote': return content && content.vote && content.vote.link ? `/thread/${encodeURIComponent(content.vote.link)}#${encodeURIComponent(content.vote.link)}` : '#'; - case 'contact': return content && content.contact ? `/author/${encodeURIComponent(content.contact)}` : '#'; - case 'pub': return '#'; + case 'industry': return `/industry/${encodeURIComponent(contentId)}`; + case 'industryBlueprint': return `/industry/blueprint/${encodeURIComponent(contentId)}`; + case 'forum': return content && content.root ? `/forum/${encodeURIComponent(typeof content.root === 'string' ? content.root : (content.root.key || content.root.id || ''))}#msg-${String(contentId || '').replace(/[^a-zA-Z0-9]/g, '')}` : `/forum/${encodeURIComponent(contentId)}`; + case 'vote': return content && content.vote && content.vote.link ? `/thread/${encodeURIComponent(content.vote.link)}#${encodeURIComponent(content.vote.link)}` : null; + case 'contact': return content && content.contact ? `/author/${encodeURIComponent(content.contact)}` : null; + case 'pub': return null; case 'bankWallet': return `/banking`; case 'bankClaim': return `/banking`; case 'map': return `/maps/${encodeURIComponent(contentId)}`; @@ -95,9 +123,18 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types = case 'shopProduct': return `/shops/product/${encodeURIComponent(contentId)}`; case 'chat': return `/chats/${encodeURIComponent(contentId)}`; case 'pad': return `/pads/${encodeURIComponent(contentId)}`; + case 'room': return `/rooms/${encodeURIComponent(contentId)}`; + case 'wikiPage': return `/wiki/${encodeURIComponent(contentId)}`; + case 'emergency': return `/emergencies/${encodeURIComponent(contentId)}`; + case 'mailingList': return `/mailing/${encodeURIComponent(contentId)}`; + case 'logisticsRoute': return `/logistics/${encodeURIComponent(contentId)}`; + case 'podcast': return `/podcasts/${encodeURIComponent(contentId)}`; + case 'podcastEpisode': return `/podcasts/episode/${encodeURIComponent(contentId)}`; + case 'campaign': return `/campaigns/${encodeURIComponent(contentId)}`; case 'torrent': return `/torrents/${encodeURIComponent(contentId)}`; + case 'file': return `/files/${encodeURIComponent(contentId)}`; case 'gameScore': return content && content.game ? `/games/${encodeURIComponent(content.game)}` : '/games'; - default: return '#'; + default: return null; } }; @@ -108,28 +145,39 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types = const s = String(value).trim().replace(/&/g, '&'); const m = s.match(/!\[[^\]]*\]\(\s*(&[^)\s]+\.sha256)\s*\)/); const src = m ? m[1] : s; - return src.startsWith('&') ? img({ src: `/blob/${encodeURIComponent(src)}`, class: 'search-result-image' }) : null; + return src.startsWith('&') ? renderZoomableImage(`/blob/${encodeURIComponent(src)}`, { imgClass: 'search-result-image' }) : null; }; const renderContentHtml = (content) => { switch (content.type) { case 'post': return div({ class: 'search-post' }, - content.contentWarning ? h2({ class: 'card-field' }, span({ class: 'card-value' }, content.contentWarning)) : null, - content.text ? div({ class: 'card-field' }, span({ class: 'card-value', innerHTML: sanitizeHtml(content.text) })) : null + content.contentWarning ? div({ class: 'shop-title-row' }, h2({ class: 'tribe-card-title' }, content.contentWarning)) : null, + content.text ? div({ class: 'blog-card-text post-text-clamped', [RAW_HTML]: sanitizeHtml(renderStyledHtml(String(content.text).trim().length > 1000 ? String(content.text).trim().slice(0, 1000).replace(/\s+$/, '') + '…' : String(content.text).trim())) }) : null, + Array.isArray(content.tags) && content.tags.length + ? div({ class: 'card-tags' }, content.tags.map(tag => a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: 'tag-link' }, `#${tag}`))) + : null ); case 'about': - return div({ class: 'search-about' }, - content.name ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.name + ':'), span({ class: 'card-value' }, content.name)) : null, - content.description ? div({ class: 'card-field card-field-stacked' }, span({ class: 'card-label' }, i18n.description + ':'), span({ class: 'card-value' }, content.description)) : null, - content.image ? img({ src: `/image/64/${encodeURIComponent(content.image)}` }) : null + return div({ class: 'card-section about' }, + div({ class: 'about-card-cols' }, + div({ class: 'about-card-col-img' }, + content.image + ? renderZoomableImage(`/blob/${encodeURIComponent(content.image)}`, { imgClass: 'activity-avatar', alt: content.name || '', linkClass: 'zoom-inline' }) + : img({ src: '/assets/images/default-avatar.png', alt: content.name || '', class: 'activity-avatar' }) + ), + div({ class: 'about-card-col-desc' }, + h2({ class: 'about-card-header' }, userLink(content.about, content.name)), + content.description ? p({ class: 'tribe-side-description' }, ...renderStyledText(String(content.description))) : null + ) + ) ); case 'feed': { const rawText = typeof content.text === 'string' ? content.text.trim() : ''; - const htmlText = rawText ? rewriteHashtagLinks(renderTextWithStyles(rawText)) : ''; + const htmlText = rawText ? renderStyledHtml(rawText) : ''; const refeedsNum = Number(content.refeeds || 0) || 0; return div({ class: 'search-feed' }, - rawText ? div({ class: 'card-field' }, span({ class: 'card-value', innerHTML: sanitizeHtml(htmlText) })) : null, + rawText ? div({ class: 'card-field' }, span({ class: 'card-value', [RAW_HTML]: sanitizeHtml(htmlText) })) : null, refeedsNum > 0 ? h2({ class: 'card-field' }, span({ class: 'card-label' }, i18n.tribeFeedRefeeds + ':'), @@ -144,13 +192,13 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types = const cleanEvDesc = rawEvDesc.replace(/!\[[^\]]*\]\(&[^)]+\.sha256\)/g, '').trim(); return div({ class: 'search-event' }, content.title ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.eventTitleLabel + ':'), span({ class: 'card-value' }, content.title)) : null, - cleanEvDesc ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.searchDescription + ':'), span({ class: 'card-value' }, cleanEvDesc)) : null, + cleanEvDesc ? div({ class: 'card-field' }, span({ class: 'card-value' }, cleanEvDesc)) : null, blobImg(content.image || blobInEvDesc), - content.date ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.eventDate + ':'), span({ class: 'card-value' }, new Date(content.date).toLocaleString())) : null, + content.date ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.eventDate + ':'), span({ class: 'card-value' }, moment(content.date).format("YYYY/MM/DD HH:mm"))) : null, content.location ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.eventLocation + ':'), span({ class: 'card-value' }, content.location)) : null, content.isPublic ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.eventPrivacyLabel + ':'), span({ class: 'card-value' }, content.isPublic)) : null, content.status ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.eventStatus + ':'), span({ class: 'card-value' }, content.status)) : null, - content.eventUrl ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.eventUrlLabel + ':'), span({ class: 'card-value' }, a({ href: content.eventUrl, target: '_blank' }, content.eventUrl))) : null, + content.eventUrl ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.eventUrlLabel + ':'), span({ class: 'card-value' }, a({ href: safeExternalHref(content.eventUrl), target: '_blank', rel: 'noopener noreferrer' }, content.eventUrl))) : null, content.price ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.eventPrice + ':'), span({ class: 'card-value' }, content.price)) : null, content.tags && content.tags.length ? div({ class: 'card-tags' }, content.tags.map(tag => @@ -175,7 +223,7 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types = ) : null, content.deadline ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.voteDeadline + ':' ), - span({ class: 'card-value' }, content.deadline ? new Date(content.deadline).toLocaleString() : '') + span({ class: 'card-value' }, content.deadline ? moment(content.deadline).format("YYYY/MM/DD HH:mm") : '') ) : null, div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.voteTotalVotes + ':' ), @@ -192,9 +240,9 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types = case 'tribe': return div({ class: 'search-tribe' }, content.title ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.title + ':'), span({ class: 'card-value' }, content.title)) : null, - (() => { const s = String(content.image || '').trim().replace(/&/g, '&'); const m = s.match(/!\[[^\]]*\]\(\s*(&[^)\s]+\.sha256)\s*\)/); const src = m ? m[1] : s; return src.startsWith('&') ? img({ src: `/blob/${encodeURIComponent(src)}`, class: 'feed-image' }) : img({ src: '/assets/images/default-tribe.png', class: 'feed-image' }); })(), - content.description ? div({ class: 'card-field card-field-stacked' }, span({ class: 'card-label' }, i18n.searchDescription + ':'), span({ class: 'card-value' }, ...renderUrl(content.description))) : null, - content.location ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.tribeLocationLabel + ':'), span({ class: 'card-value' }, ...renderUrl(content.location))) : null, + (() => { const s = String(content.image || '').trim().replace(/&/g, '&'); const m = s.match(/!\[[^\]]*\]\(\s*(&[^)\s]+\.sha256)\s*\)/); const src = m ? m[1] : s; return src.startsWith('&') ? img({ loading: 'lazy', src: `/blob/${encodeURIComponent(src)}`, class: 'feed-image' }) : img({ src: '/assets/images/default-tribe.png', class: 'feed-image' }); })(), + content.description ? div({ class: 'card-field card-field-stacked' }, span({ class: 'card-value' }, ...renderStyledText(content.description))) : null, + content.location ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.tribeLocationLabel + ':'), span({ class: 'card-value' }, ...renderStyledText(content.location))) : null, div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.tribeIsAnonymousLabel + ':'), span({ class: 'card-value' }, content.isAnonymous ? i18n.tribePrivate : i18n.tribePublic)), content.inviteMode ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.tribeModeLabel + ':'), span({ class: 'card-value' }, String(content.inviteMode).toUpperCase())) : null, Array.isArray(content.members) @@ -209,7 +257,7 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types = case 'audio': return content.url ? div({ class: 'search-audio' }, content.title ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.audioTitleLabel + ':'), span({ class: 'card-value' }, content.title)) : null, - content.description ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.audioDescriptionLabel + ':'), span({ class: 'card-value' }, content.description)) : null, + content.description ? div({ class: 'card-field' }, span({ class: 'card-value' }, content.description)) : null, br(), audioHyperaxe({ controls: true, src: `/blob/${encodeURIComponent(content.url)}`, type: content.mimeType, preload: 'metadata' }), br(), @@ -222,10 +270,9 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types = case 'image': return content.url ? div({ class: 'search-image' }, content.title ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.imageTitleLabel + ':'), span({ class: 'card-value' }, content.title)) : null, - content.description ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.imageDescriptionLabel + ':'), span({ class: 'card-value' }, content.description)) : null, - content.meme ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.trendingCategory + ':'), span({ class: 'card-value' }, i18n.meme)) : null, + content.description ? div({ class: 'card-field' }, span({ class: 'card-value' }, content.description)) : null, br(), - img({ src: `/blob/${encodeURIComponent(content.url)}` }), + renderZoomableImage(`/blob/${encodeURIComponent(content.url)}`, { imgClass: 'search-result-image' }), br(), content.tags && content.tags.length ? div({ class: 'card-tags' }, content.tags.map(tag => @@ -236,10 +283,10 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types = case 'video': return content.url ? div({ class: 'search-video' }, content.title ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.videoTitleLabel + ':'), span({ class: 'card-value' }, content.title)) : null, - content.description ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.videoDescriptionLabel + ':'), span({ class: 'card-value' }, content.description)) : null, - br(), - videoHyperaxe({ controls: true, src: `/blob/${encodeURIComponent(content.url)}`, type: content.mimeType || 'video/mp4', width: '640', height: '360' }), - br(), + content.description ? div({ class: 'card-field' }, span({ class: 'card-value' }, content.description)) : null, + div({ class: 'video-container' }, + videoHyperaxe({ controls: true, src: `/blob/${encodeURIComponent(content.url)}`, type: content.mimeType || 'video/mp4', width: '640', height: '360', preload: 'metadata' }) + ), content.tags && content.tags.length ? div({ class: 'card-tags' }, content.tags.map(tag => a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: 'tag-link' }, `#${tag}`) @@ -264,10 +311,16 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types = )) : null ); + case 'file': + return div({ class: 'search-file' }, + content.title ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.fileTitleLabel || i18n.title) + ':'), span({ class: 'card-value' }, content.title)) : null, + content.description ? div({ class: 'card-field' }, span({ class: 'card-value' }, content.description)) : null, + content.size ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.fileSizeLabel || 'Size') + ':'), span({ class: 'card-value' }, String(content.size))) : null + ); case 'torrent': return div({ class: 'search-torrent' }, content.title ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.torrentTitleLabel || i18n.title) + ':'), span({ class: 'card-value' }, content.title)) : null, - content.description ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.torrentDescriptionLabel || i18n.searchDescription) + ':'), span({ class: 'card-value' }, content.description)) : null, + content.description ? div({ class: 'card-field' }, span({ class: 'card-value' }, content.description)) : null, content.size ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.torrentSizeLabel || 'Size') + ':'), span({ class: 'card-value' }, String(content.size))) : null, content.tags && content.tags.length ? div({ class: 'card-tags' }, content.tags.map(tag => @@ -278,17 +331,17 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types = case 'market': return div({ class: 'search-market' }, content.title ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.title + ':'), span({ class: 'card-value' }, content.title)) : null, - content.description ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.searchDescription + ':'), span({ class: 'card-value' }, content.description)) : null, + content.description ? div({ class: 'card-field' }, span({ class: 'card-value' }, content.description)) : null, content.item_type ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.marketItemType + ':'), span({ class: 'card-value' }, content.item_type.toUpperCase())) : null, content.status ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.marketItemCondition + ':'), span({ class: 'card-value' }, content.status)) : null, - content.deadline ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.marketItemDeadline + ':'), span({ class: 'card-value' }, new Date(content.deadline).toLocaleString())) : null, + content.deadline ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.marketItemDeadline + ':'), span({ class: 'card-value' }, moment(content.deadline).format("YYYY/MM/DD HH:mm"))) : null, br(), blobImg(content.image), br(), content.seller ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.marketItemSeller + ':'), span({ class: 'card-value' }, userLink(content.seller))) : null, content.stock ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.marketItemStock + ':'), span({ class: 'card-value' }, content.stock || 'N/A')) : null, - content.price ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.searchPriceLabel + ':'), span({ class: 'card-value' }, `${content.price} ECO`)) : null, - content.condition ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.searchDescription + ':'), span({ class: 'card-value' }, content.condition)) : null, + content.price ? div({ class: 'price-chip' }, `${content.price} ECO`) : null, + content.condition ? div({ class: 'card-field' }, span({ class: 'card-value' }, content.condition)) : null, content.includesShipping ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.marketItemIncludesShipping + ':'), span({ class: 'card-value' }, `${content.includesShipping ? i18n.YESLabel : i18n.NOLabel}`)) : null, content.auctions_poll && content.auctions_poll.length > 0 ? div({ class: 'auction-info' }, @@ -318,10 +371,9 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types = ); case 'bookmark': return div({ class: 'search-bookmark' }, - content.url ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.bookmarkUrlLabel + ':'), span({ class: 'card-value' }, a({ href: content.url, target: '_blank' }, content.url))) : null, - content.description ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.searchDescription + ':'), span({ class: 'card-value' }, content.description)) : null, - content.category ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.bookmarkCategory + ':'), span({ class: 'card-value' }, content.category)) : null, - content.lastVisit ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.bookmarkLastVisit + ':'), span({ class: 'card-value' }, new Date(content.lastVisit).toLocaleString())) : null, + content.url ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.bookmarkUrlLabel + ':'), span({ class: 'card-value' }, a({ href: safeExternalHref(content.url), target: '_blank', rel: 'noopener noreferrer' }, content.url))) : null, + content.description ? div({ class: 'card-field' }, span({ class: 'card-value' }, content.description)) : null, + content.lastVisit ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.bookmarkLastVisit + ':'), span({ class: 'card-value' }, moment(content.lastVisit).format("YYYY/MM/DD HH:mm"))) : null, content.tags && content.tags.length ? div({ class: 'card-tags' }, content.tags.map(tag => a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: 'tag-link' }, `#${tag}`) @@ -331,13 +383,13 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types = case 'task': return div({ class: 'search-task' }, content.title ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.taskTitleLabel + ':'), span({ class: 'card-value' }, content.title)) : null, - content.description ? div({ class: 'card-field', style: 'display:flex;flex-direction:column;' }, span({ class: 'card-label' }, i18n.searchDescription + ':'), p({ class: 'card-value', style: 'white-space:pre-wrap;margin-top:4px;' }, content.description)) : null, + content.description ? div({ class: 'card-field card-field-stacked' }, p({ class: 'card-value card-value-pre' }, content.description)) : null, content.location ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.searchLocationLabel + ':'), span({ class: 'card-value' }, content.location)) : null, content.status ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.searchStatusLabel + ':'), span({ class: 'card-value' }, content.status)) : null, content.priority ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.searchPriorityLabel + ':'), span({ class: 'card-value' }, content.priority)) : null, typeof content.isPublic === 'boolean' ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.searchIsPublicLabel + ':'), span({ class: 'card-value' }, content.isPublic ? i18n.YESLabel : i18n.NOLabel)) : null, - content.startTime ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.taskStartTimeLabel + ':'), span({ class: 'card-value' }, new Date(content.startTime).toLocaleString())) : null, - content.endTime ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.taskEndTimeLabel + ':'), span({ class: 'card-value' }, new Date(content.endTime).toLocaleString())) : null, + content.startTime ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.taskStartTimeLabel + ':'), span({ class: 'card-value' }, moment(content.startTime).format("YYYY/MM/DD HH:mm"))) : null, + content.endTime ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.taskEndTimeLabel + ':'), span({ class: 'card-value' }, moment(content.endTime).format("YYYY/MM/DD HH:mm"))) : null, Array.isArray(content.assignees) ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.taskAssignees + ':'), span({ class: 'card-value' }, content.assignees.length)) : null, content.tags && content.tags.length ? div({ class: 'card-tags' }, content.tags.map(tag => @@ -351,7 +403,7 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types = content.status ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.searchStatusLabel + ':'), span({ class: 'card-value' }, content.status)) : null, content.severity ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.reportsSeverity + ':'), span({ class: 'card-value' }, content.severity)) : null, content.category ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.searchCategoryLabel + ':'), span({ class: 'card-value' }, content.category)) : null, - content.description ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.searchDescription + ':'), span({ class: 'card-value' }, content.description)) : null, + content.description ? div({ class: 'card-field' }, span({ class: 'card-value' }, content.description)) : null, br(), blobImg(content.image), br(), @@ -370,7 +422,7 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types = content.amount ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.transfersAmount + ':'), span({ class: 'card-value' }, content.amount)) : null, br(), content.from ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.transfersFrom + ':'), span({ class: 'card-value' }, userLink(content.from))) : null, - content.to ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.transfersTo + ':'), span({ class: 'card-value' }, userLink(content.to))) : null, + content.to && process.env.OASIS_MOBILE !== '1' ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.transfersTo + ':'), span({ class: 'card-value' }, userLink(content.to))) : null, br(), content.confirmedBy && content.confirmedBy.length ? h2({ class: 'card-field' }, span({ class: 'card-label' }, i18n.transfersConfirmations + ':'), span({ class: 'card-value' }, content.confirmedBy.length)) @@ -384,8 +436,8 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types = case 'curriculum': return div({ class: 'search-curriculum' }, content.name ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.cvNameLabel + ':'), span({ class: 'card-value' }, content.name)) : null, - content.description ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.cvDescriptionLabel + ':'), span({ class: 'card-value' }, content.description)) : null, - content.photo ? img({ src: `/blob/${encodeURIComponent(content.photo)}`, class: 'curriculum-photo' }) : null, + content.description ? div({ class: 'card-field' }, span({ class: 'card-value' }, content.description)) : null, + content.photo ? renderZoomableImage(`/blob/${encodeURIComponent(content.photo)}`, { imgClass: 'curriculum-photo' }) : null, content.location ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.cvLocationLabel + ':'), span({ class: 'card-value' }, content.location)) : null, content.status ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.cvStatusLabel + ':'), span({ class: 'card-value' }, content.status)) : null, content.preferences ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.cvPreferencesLabel + ':'), span({ class: 'card-value' }, content.preferences)) : null, @@ -435,9 +487,28 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types = ) : null, content.txid ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.bankTx + ':' ), - a({ href: `https://ecoin.03c8.net/blockexplorer/search?q=${content.txid}`, target: '_blank' }, content.txid) + span({ class: 'card-value bank-address-code' }, content.txid) ) : null ); + case 'schoolCourse': + return div({ class: 'search-school' }, + content.title ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.title + ':'), span({ class: 'card-value' }, content.title)) : null, + div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.schoolCourseType || 'Course type') + ':'), span({ class: 'card-value' }, String(content.visibility || '').toUpperCase() === 'INVITE' ? 'INVITE-ONLY' : (Number(content.price) > 0 ? 'PAID' : 'OPEN'))), + Number(content.price) > 0 ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.schoolPrice || 'Price (ECO)') + ':'), span({ class: 'card-value' }, `${Number(content.price).toFixed(2)} ECO`)) : null, + content.startDate ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.schoolStartDate || 'Start date') + ':'), span({ class: 'card-value' }, moment(content.startDate).format("YYYY/MM/DD HH:mm"))) : null, + content.description ? div({ class: 'card-field card-field-stacked' }, span({ class: 'card-value' }, String(content.description).length > 220 ? String(content.description).slice(0, 220) + '…' : content.description)) : null, + Array.isArray(content.tags) && content.tags.length ? div({ class: 'card-tags' }, + content.tags.map(tag => a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: 'tag-link' }, `#${tag}`)) + ) : null + ); + case 'housing': + return div({ class: 'search-housing' }, + content.title ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.title + ':'), span({ class: 'card-value' }, content.title)) : null, + content.housing_type ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.housingType + ':'), span({ class: 'card-value' }, String(content.housing_type).toUpperCase())) : null, + content.place ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.housingPlace + ':'), span({ class: 'card-value' }, content.place)) : null, + String(content.housing_type || '').toLowerCase() !== 'couchsurfing' && content.price ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.housingPrice + ':'), span({ class: 'card-value' }, `${content.price} ECO`)) : null, + content.status ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.housingStatus + ':'), span({ class: 'card-value' }, String(content.status).toUpperCase())) : null + ); case 'job': return div({ class: 'search-job' }, content.title ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.title + ':'), span({ class: 'card-value' }, content.title)) : null, @@ -448,11 +519,24 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types = content.vacants ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.jobVacants + ':'), span({ class: 'card-value' }, content.vacants)) : null, Array.isArray(content.subscribers) ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.jobSubscribers + ':'), span({ class: 'card-value' }, `${content.subscribers.length}`)) : null ); + case 'industry': + return div({ class: 'search-industry' }, + content.name ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.industryName + ':'), span({ class: 'card-value' }, content.name)) : null, + content.sector ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.industrySector + ':'), span({ class: 'card-value' }, String(content.sector).toUpperCase())) : null, + content.status ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.industryStatusLabel + ':'), span({ class: 'card-value' }, industryStatusLabel(content.status))) : null, + content.description ? div({ class: "card-field card-field-stacked" }, span({ class: "card-value" }, content.description)) : null + ); + case "industryBlueprint": + return div({ class: 'search-industry' }, + content.name ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.industryBlueprint + ':'), span({ class: 'card-value' }, content.name)) : null, + content.outKind ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.industryOutputKind + ':'), span({ class: 'card-value' }, String(i18n['industryKind_' + content.outKind] || content.outKind).toUpperCase())) : null, + content.description ? div({ class: 'card-field card-field-stacked' }, span({ class: 'card-value' }, content.description)) : null + ); case 'forum': return div({ class: 'search-forum' }, content.title ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.title + ':'), span({ class: 'card-value' }, content.title)) : null, content.category ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.searchCategoryLabel + ':'), span({ class: 'card-value' }, content.category)) : null, - content.text ? div({ class: 'card-field card-field-stacked' }, span({ class: 'card-label' }, i18n.searchDescription + ':'), span({ class: 'card-value' }, content.text)) : null + content.text ? div({ class: 'card-field card-field-stacked' }, span({ class: 'card-value' }, content.text)) : null ); case 'vote': return div({ class: 'search-vote-link' }, @@ -470,7 +554,7 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types = return div({ class: 'search-shop' }, content.title ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.title + ':'), span({ class: 'card-value' }, content.title)) : null, blobImg(content.image), - content.shortDescription ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.searchDescription + ':'), span({ class: 'card-value' }, content.shortDescription)) : null, + content.shortDescription ? div({ class: 'card-field' }, span({ class: 'card-value' }, content.shortDescription)) : null, content.visibility ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.shopStatus || 'STATUS') + ':'), span({ class: 'card-value' }, content.visibility)) : null, content.location ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.searchLocationLabel || 'LOCATION') + ':'), span({ class: 'card-value' }, content.location)) : null, content.tags && content.tags.length @@ -481,8 +565,8 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types = return div({ class: 'search-shop-product' }, content.title ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.title + ':'), span({ class: 'card-value' }, content.title)) : null, blobImg(content.image), - content.description ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.searchDescription + ':'), span({ class: 'card-value' }, content.description)) : null, - content.price ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.searchPriceLabel || 'PRICE') + ':'), span({ class: 'card-value' }, `${content.price} ECO`)) : null, + content.description ? div({ class: 'card-field' }, span({ class: 'card-value' }, content.description)) : null, + content.price ? div({ class: 'price-chip' }, `${content.price} ECO`) : null, content.stock !== undefined ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.marketItemStock + ':'), span({ class: 'card-value' }, content.stock)) : null ); case 'chat': { @@ -493,7 +577,7 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types = ); return div({ class: 'search-chat' }, content.title ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.chatsTitle || 'Chat') + ':'), span({ class: 'card-value' }, content.title)) : null, - content.description ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.chatDescription || 'Description') + ':'), span({ class: 'card-value' }, content.description)) : null, + content.description ? div({ class: 'card-field' }, span({ class: 'card-value' }, content.description)) : null, content.category ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.chatCategoryLabel || 'Category') + ':'), span({ class: 'card-value' }, content.category)) : null, content.status ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.chatStatus || 'STATUS') + ':'), span({ class: 'card-value' }, content.status)) : null ); @@ -510,6 +594,11 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types = content.deadline ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.padDeadlineLabel || 'Deadline') + ':'), span({ class: 'card-value' }, content.deadline)) : null ); } + case 'room': + return div({ class: 'search-room' }, + content.title ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.roomsTitle || 'Rooms') + ':'), span({ class: 'card-value' }, content.title)) : null, + content.description ? div({ class: 'card-field' }, span({ class: 'card-value' }, content.description)) : null + ); case 'gameScore': return div({ class: 'search-game' }, content.game ? div({ class: 'game-row' }, @@ -524,7 +613,7 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types = case 'map': return div({ class: 'search-map' }, content.title ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.title + ':'), span({ class: 'card-value' }, content.title)) : null, - content.description ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.mapDescriptionLabel + ':'), span({ class: 'card-value' }, content.description)) : null, + content.description ? div({ class: 'card-field' }, span({ class: 'card-value' }, content.description)) : null, content.lat && content.lng ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.mapLocationTitle + ':'), span({ class: 'card-value' }, `${content.lat}, ${content.lng}`)) : null, content.mapType ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.mapTypeLabel + ':'), span({ class: 'card-value' }, content.mapType)) : null, content.tags && content.tags.length @@ -534,17 +623,25 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types = : null ); default: - return div({ class: 'styled-text', innerHTML: sanitizeHtml(renderTextWithStyles(content.text || content.description || content.title || '[no content]')) }); + return div({ class: 'styled-text', [RAW_HTML]: sanitizeHtml(renderStyledHtml(content.text || content.description || content.title || '[no content]')) }); } }; + const typeLabel = (type) => String(i18n['type' + String(type || '').charAt(0).toUpperCase() + String(type || '').slice(1)] || type || '').toUpperCase(); + const totalResults = Object.values(results).reduce((n, msgs) => n + (Array.isArray(msgs) ? msgs.length : 0), 0); + const resultStats = renderModuleStats(totalResults, + Object.entries(results) + .map(([type, msgs]) => ({ label: typeLabel(type), count: Array.isArray(msgs) ? msgs.length : 0 })) + .sort((x, y) => y.count - x.count || x.label.localeCompare(y.label))); + const pageGroups = paged(Object.entries(results).flatMap(([key, msgs]) => msgs.map((msg) => [key, msg]))) + .reduce((acc, [key, msg]) => { (acc[key] = acc[key] || []).push(msg); return acc; }, {}); const resultSection = Object.entries(results).length > 0 - ? Object.entries(results).map(([key, msgs]) => + ? Object.entries(pageGroups).map(([key, msgs]) => div( { class: "search-result-group" }, ...msgs.map((msg) => { const content = msg.value.content || {}; - const created = new Date(msg.timestamp).toLocaleString(); + const created = moment(msg.timestamp).format("YYYY/MM/DD HH:mm"); if (content.type === 'document') hasDocument = true; const contentHtml = renderContentHtml(content); let author; @@ -582,16 +679,26 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types = div({ class: 'card-header activity-card-header' }, div({ class: 'card-chips-row' }, span({ class: 'pm-exposition-chip pm-exposition-whole' }, - span({ class: 'pm-exposition-text' }, String(content.type || '').toUpperCase()) + span({ class: 'pm-exposition-text' }, String(i18n['type' + String(content.type || '').charAt(0).toUpperCase() + String(content.type || '').slice(1)] || content.type || '').toUpperCase()) ) ), - renderContentActions(contentId, getViewDetailsActionForSearch(content.type, contentId, content)) + renderContentActions(contentId, getViewDetailsActionForSearch(content.type, contentId, content), { + author: msg.value.author, + reportTitle: content.title || content.question || content.concept || '', + spread: CONTENT_SPREADABLE.has(content.type) ? ((spreadMap instanceof Map && spreadMap.get(contentId)) || null) : undefined, + ...(CONTENT_TORRENTABLE.has(content.type) && typeof content.url === 'string' && content.url.startsWith('&') + ? { torrentFrom: { blobId: content.url, name: content.title || content.fileName || '' } } + : {}), + ...(CONTENT_FAV_KIND[content.type] + ? { favKind: CONTENT_FAV_KIND[content.type], isFavorite: favIndex instanceof Map ? favIndex.get(String(contentId)) === CONTENT_FAV_KIND[content.type] : false, returnTo: `/search?query=${encodeURIComponent(hashtag ? `#${hashtag}` : String(query || ''))}` } + : {}) + }) ), div({ class: 'card-section search-card-body' }, contentHtml, author ? p({ class: 'card-footer' }, - span({ class: 'date-link' }, `${created} ${i18n.performed} `), + span({ class: 'date-link' }, `${created}`), (authorUrl && authorUrl !== '#' && authorUrl.startsWith('/author/')) ? userLink(decodeURIComponent(authorUrl.replace(/^\/author\//, ''))) : a({ href: authorUrl, class: 'user-link' }, `${author}`) @@ -601,55 +708,28 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types = }) ) ) - : div({ class: 'no-results' }, p(i18n.noResultsFound)); + : (String(query || '').trim() || hashtag + ? div({ class: 'no-results' }, p(i18n.noResultsFound)) + : null); let html = template( - hashtag ? `#${hashtag}` : i18n.searchTitle, + i18n.searchTitle, section( - div({ class: "tags-header" }, - h2(hashtag ? `#${hashtag}` : i18n.searchTitle), + div({ class: "tags-header module-header-line" }, + h2(i18n.searchTitle), p(hashtag ? i18n.hashtagDescription : i18n.searchDescriptionLabel) ), - form( - { action: "/search", method: "POST", class: "search-form" }, - div({ class: "search-filters-row" }, - table({ class: "search-filters-table" }, - tr( - td({ class: 'card-label' }, label({ for: "search_from" }, i18n.searchFromLabel || "From")), - td(input({ id: "search_from", type: "datetime-local", name: "from" })) - ), - tr( - td({ class: 'card-label' }, label({ for: "search_to" }, i18n.searchToLabel || "To")), - td(input({ id: "search_to", type: "datetime-local", name: "to" })) - ), - tr( - td({ class: 'card-label' }, label({ for: "search_inhabitant" }, "Oasis ID")), - td(input({ id: "search_inhabitant", type: "text", name: "inhabitant", placeholder: "@...=.ed25519", pattern: "@[A-Za-z0-9+/_\\-]{43}=\\.ed25519", maxlength: 56, class: "search-oasis-id" })) - ) - ), - table({ class: "search-filters-table" }, - tr( - td({ class: 'card-label' }, label({ for: "search_query" }, i18n.searchQueryLabel || "Query")), - td(searchInput) - ), - tr( - td({ class: 'card-label' }, label({ for: "results-per-page" }, i18n.searchPerPageLabel || "Results per page")), - td(resultsPerPageSelect) - ) - ) - ), - div({ class: "search-submit-row" }, - button({ type: "submit" }, i18n.searchSubmit) - ) - ) + renderSearchForm({ action: "/search", query }) ), - section(resultSection) + section( + resultStats ? div({ class: 'filters activity-filter-chips activity-toolbar-row search-stats-row' }, resultStats) : null, + resultSection + ) ); if (hasDocument) { html += ` - - + `; } diff --git a/src/views/settings_view.js b/src/views/settings_view.js index 0969d08..6ce4758 100644 --- a/src/views/settings_view.js +++ b/src/views/settings_view.js @@ -1,13 +1,31 @@ -const { form, button, div, h2, h3, p, section, select, option, input, br, a, label, span } = require("../server/node_modules/hyperaxe"); +const { form, button, div, h2, h3, p, section, select, option, input, br, a, label, span, img, strong, table, tr, td, ul, li } = require("../server/node_modules/hyperaxe"); const fs = require('fs'); const path = require('path'); const { getConfig } = require('../configs/config-manager.js'); -const { template, selectedLanguage, i18n, setLanguage } = require('./main_views'); +const { template, selectedLanguage, i18n, setLanguage, INBOX_BOT_ORDER, inboxBotLabel, THEMES, MOBILE_THEMES, userLink } = require('./main_views'); const i18nBase = require("../client/assets/translations/i18n"); +const moment = require("../server/node_modules/moment"); +const { WORKFLOWS, currentWorkflow } = require('../models/workflows_model'); +const mobileUi = require('./mobile_ui'); +const html = require("../server/node_modules/hyperaxe"); +// selector de identidades: la implementacion vive en ./identities_view.js +const { renderIdentities } = require('./identities_view')({ i18n, html }); +const { renderVerificationReport, renderRebuildReport } = require('./backup_view'); const snhUrl = "https://wiki.solarnethub.com/socialnet/overview"; +const BLOB_CACHE_OPTIONS = [512, 1024, 2048, 5120, 10240, 0]; +const plainLabel = (s) => String(s || '').replace(/!\[[^\]]*\]\([^)]*\)/g, '').replace(/\[([^\]]*)\]\([^)]*\)/g, '$1').replace(/\s+/g, ' ').trim(); +const hiddenAt = (at) => at ? moment(at).format("YYYY/MM/DD HH:mm") : "—"; +const sizeLabel = (bytes) => { + let value = Number(bytes) || 0; + const units = ['B', 'KB', 'MB', 'GB']; + let unit = 0; + while (value >= 1024 && unit < units.length - 1) { value /= 1024; unit += 1; } + return `${unit === 0 ? Math.round(value) : value.toFixed(1)} ${units[unit]}`; +}; +const blobCacheOptionLabel = (mb) => mb === 0 ? i18n.settingsBlobCacheUnlimited : (mb >= 1024 ? `${mb / 1024} GB` : `${mb} MB`); -const themeFilePath = path.join(__dirname, '../configs/oasis-config.json'); +const themeFilePath = require('../configs/config-manager').configFilePath; const getThemeConfig = () => { try { const configData = fs.readFileSync(themeFilePath); @@ -18,27 +36,61 @@ const getThemeConfig = () => { } }; -const settingsView = ({ version, aiPrompt, fediverseAccount, fediverseError }) => { +const settingsView = (params) => { + const { version, aiPrompt, aiExportCount = 0, blobCache = null, fediverseAccount, telegramAccount = null, telegramLogin = null, peertubeAccount = null, nextcloudAccount = null, verification = null, rebuild = null } = params; const currentThemeConfig = getThemeConfig(); const theme = currentThemeConfig.themes?.current || "Dark-SNH"; const currentConfig = getConfig(); let serverConfig = {}; - try { serverConfig = JSON.parse(fs.readFileSync(path.join(__dirname, '../configs/server-config.json'), 'utf8')); } catch (_) {} + try { serverConfig = require('../configs/config-manager.js').readServerConfig(); } catch (_) {} const currentHops = (serverConfig.friends && Number.isFinite(serverConfig.friends.hops)) ? serverConfig.friends.hops : 2; const walletUrl = currentConfig.wallet.url; const walletUser = currentConfig.wallet.user; const walletFee = currentConfig.wallet.fee; - const pubId = currentConfig.walletPub?.pubId || ''; - const currentWish = currentConfig.wish === 'mutuals' ? 'mutuals' : 'whole'; - const currentPmVisibility = currentConfig.pmVisibility === 'mutuals' ? 'mutuals' : 'whole'; + const currentWish = ['mutuals', 'only-lan', 'local'].includes(currentConfig.wish) ? currentConfig.wish : 'whole'; + const currentPmVisibility = ['mutuals', 'following'].includes(currentConfig.pmVisibility) ? currentConfig.pmVisibility : 'whole'; + const currentMentionsVisibility = ['mutuals', 'following'].includes(currentConfig.mentionsVisibility) ? currentConfig.mentionsVisibility : 'whole'; + const viewerHidden = (() => { try { const vf = require('../models/viewer_filters'); return { muted: vf.mutedList(), hidden: vf.hiddenList() }; } catch (_) { return { muted: [], hidden: [] }; } })(); + const currentPhone = currentConfig.phone && typeof currentConfig.phone === 'object' ? currentConfig.phone : {}; - const themeElements = [ - option({ value: "Dark-SNH", selected: theme === "Dark-SNH" ? true : undefined }, "Dark-SNH"), - option({ value: "Clear-SNH", selected: theme === "Clear-SNH" ? true : undefined }, "Clear-SNH"), - option({ value: "Purple-SNH", selected: theme === "Purple-SNH" ? true : undefined }, "Purple-SNH"), - option({ value: "Matrix-SNH", selected: theme === "Matrix-SNH" ? true : undefined }, "Matrix-SNH"), - option({ value: "OasisMobile", selected: theme === "OasisMobile" ? true : undefined }, "Oasis-Mobile") - ]; + const themeElements = THEMES.map(([value, name]) => option({ value, ...(theme === value ? { selected: true } : {}) }, name)); + + const activeWorkflow = currentWorkflow(currentConfig) || ''; + const modOn = (name) => (currentConfig.modules || {})[`${name}Mod`] === 'on'; + const isMobile = process.env.OASIS_MOBILE === '1'; + + // movil: skins y workflows como tarjetas que se aplican al tocarlas (un boton + // de envio por opcion); el desplegable de escritorio quedaba enterrado y dejaba + // poner temas de escritorio, que en el movil se quedan sin maqueta + const skinPicker = () => section({ id: "theme" }, + div({ class: "tags-header" }, h2(i18n.theme), p(i18n.skinIntroMobile)), + form({ action: "/settings/theme", method: "post", class: "skin-pick" }, + ...MOBILE_THEMES.map(([value, name]) => button({ type: "submit", name: "theme", value, class: theme === value ? "skin-card is-current" : "skin-card" }, + img({ src: `/assets/images/theme-${value.toLowerCase()}.png`, alt: "", class: "skin-shot" }), + span({ class: "skin-name" }, name))) + ) + ); + const workflowPicker = () => section({ id: "workflows" }, + div({ class: "tags-header" }, h2(i18n.workflowsTitle), p(i18n.workflowsDescriptionMobile)), + mobileUi.workflowPick("/settings/workflow", activeWorkflow, WORKFLOWS.map(w => w.key)) + ); + + const workflowSection = isMobile ? workflowPicker() : section({ id: "workflows" }, + div({ class: "tags-header" }, + h2(i18n.workflowsTitle), + p(i18n.workflowsDescription), + form( + { action: "/settings/workflow", method: "POST" }, + select({ name: "workflow" }, + ...WORKFLOWS.map(w => activeWorkflow === w.key + ? option({ value: w.key, selected: true }, i18n[`workflow_${w.key}`]) + : option({ value: w.key }, i18n[`workflow_${w.key}`])) + ), + br(), br(), + button({ type: "submit" }, i18n.workflowsSet) + ) + ) + ); const languageOption = (longName, shortName) => { return shortName === selectedLanguage @@ -51,48 +103,57 @@ const settingsView = ({ version, aiPrompt, fediverseAccount, fediverseError }) = button({ type: "submit" }, i18n.rebuildName) ); - const updateFlagPath = path.join(__dirname, '../server/.update_required'); - let updateButton = null; - if (fs.existsSync(updateFlagPath)) { - updateButton = form( - { action: "/update", method: "post" }, - button({ type: "submit" }, i18n.updateit) + const updateInfo = (() => { try { return require('../backend/updater').readUpdateInfo(); } catch (_) { return null; } })(); + let updatePanel = null; + if (updateInfo) { + const d = updateInfo.details || {}; + const day = (iso) => String(iso || '').slice(0, 10); + const versionLine = (v, c) => `${v || '?'}${c ? ` · ${c.hash} · ${day(c.date)}` : ''}`; + const row = (name, ...value) => tr(td({ class: "tribe-info-label" }, name), td({ class: "tribe-info-value" }, ...value)); + updatePanel = section({ id: "update" }, + div({ class: "torrent-download update-panel" }, + div({ class: "torrent-oasis torrent-detail-section" }, + h3({ class: "torrent-section-title" }, i18n.notifyUpdateLabel), + table({ class: "tribe-info-table torrent-file-info" }, + row(i18n.updateInstalledLabel, versionLine(updateInfo.installedVersion, d.installed)), + row(i18n.updateAvailableLabel, versionLine(updateInfo.version, d.available)), + Array.isArray(d.commits) && d.commits.length + ? row(i18n.updateChangesLabel, ul({ class: "update-changes" }, + ...d.commits.map(c => li(`${day(c.date)} · ${c.subject}`)), + d.count > d.commits.length ? li(`+${d.count - d.commits.length}`) : null)) + : null, + d.files ? row(i18n.updateFilesLabel, `${d.files} · +${d.insertions} −${d.deletions}`) : null, + d.origin ? row(i18n.updateOriginLabel, span({ class: "bank-address-code" }, d.origin)) : null + ), + isMobile + ? ((updateInfo.apk || updateInfo.page) + ? a({ href: updateInfo.apk || updateInfo.page, class: "filter-btn update-download-link" }, `\u2B07 OASIS${updateInfo.version ? ` ${updateInfo.version}` : ''}`) + : null) + : form({ action: "/update", method: "post" }, + button({ type: "submit", class: "filter-btn" }, `\u2B07 OASIS${updateInfo.version ? ` ${updateInfo.version}` : ''}`) + ) + ) + ) ); } - return template( - i18n.settings, - section( - div({ class: "tags-header" }, - h2(i18n.settings), - p(a({ href: snhUrl, target: "_blank" }, i18n.settingsIntro({ version }))), - updateButton - ) - ), - section( - div({ class: "tags-header" }, - h2(i18n.theme), - p(i18n.themeIntro), - form( - { action: "/settings/theme", method: "post" }, - select({ name: "theme" }, ...themeElements), - br(), - br(), - button({ type: "submit" }, i18n.setTheme) - ) - ) - ), - section( + const filter = params.filter === 'multiverse' && modOn('fediverse') ? 'multiverse' : params.filter === 'content' ? 'content' : 'oasis'; + const oasisParts = [ + updatePanel, + renderIdentities(require('../server/ssb_config').account), + section({ id: "language" }, div({ class: "tags-header" }, h2(i18n.language), p(i18n.languageDescription), - form( + isMobile ? mobileUi.languagePick() : form( { action: "/language", method: "post" }, select({ name: "language" }, [ languageOption("English", "en"), languageOption("Español", "es"), languageOption("Français", "fr"), languageOption("Euskara", "eu"), + languageOption("Català", "ca"), + languageOption("Galego", "gl"), languageOption("Deutsch", "de"), languageOption("Italiano", "it"), languageOption("Português", "pt"), @@ -107,53 +168,111 @@ const settingsView = ({ version, aiPrompt, fediverseAccount, fediverseError }) = ) ) ), - section( + section({ id: "ux" }, div({ class: "tags-header" }, h2(i18n.uxModeTitle || "UX"), p(i18n.uxModeDescription || "Select which UX navigation mode you want for your GUI."), - form( + isMobile ? mobileUi.uxPick("/settings/ux", currentConfig) : form( { action: "/settings/ux", method: "POST" }, (() => { const aiNavEnabled = currentConfig.modules && currentConfig.modules.aiNavMod === 'on'; - const opts = [ - option({ value: "blocks", selected: (currentConfig.ux?.current !== "ainav") ? true : undefined }, i18n.uxModeMenus || "Blocks") - ]; - if (aiNavEnabled) { - opts.push(option({ value: "ainav", selected: currentConfig.ux?.current === "ainav" ? true : undefined }, i18n.uxModeAINav || "AI")); - } - return select({ name: "ux" }, ...opts); + const chatsEnabled = currentConfig.modules && currentConfig.modules.chatsMod === 'on'; + const phoneEnabled = !!(currentConfig.modules && currentConfig.modules.phoneMod !== 'off'); + const roomsEnabled = !!(currentConfig.modules && currentConfig.modules.roomsMod !== 'off'); + const cur = currentConfig.ux?.current === "ainav" ? "ainav" : currentConfig.ux?.current === "chats" ? "chats" : currentConfig.ux?.current === "feed" ? "feed" : currentConfig.ux?.current === "phone" ? "phone" : currentConfig.ux?.current === "rooms" ? "rooms" : "blocks"; + const uxCard = (value, title, image) => label({ class: "welcome-ux-option" }, + input({ type: "radio", name: "ux", value, ...(cur === value ? { checked: true } : {}) }), + img({ src: image, class: "welcome-ux-shot", alt: title }), + span({ class: "welcome-ux-label" }, title) + ); + return div({ class: "welcome-ux-grid" }, + uxCard("blocks", i18n.uxModeMenus || "Blocks", "/assets/images/ux-blocks.png"), + aiNavEnabled ? uxCard("ainav", i18n.uxModeAINav || "AI", "/assets/images/ux-ainav.png") : null, + chatsEnabled ? uxCard("chats", i18n.uxModeChats || "Conversations", "/assets/images/ux-chats.png") : null, + uxCard("feed", i18n.uxModeFeed || "Microblogging", "/assets/images/ux-feed.png"), + phoneEnabled ? uxCard("phone", i18n.phoneTitle || "Phone", "/assets/images/ux-phone.png") : null, + roomsEnabled ? uxCard("rooms", i18n.roomsTitle || "Rooms", "/assets/images/ux-rooms.png") : null + ); })(), - br(), br(), button({ type: "submit" }, i18n.saveSettings) ) ) ), - section( + isMobile ? skinPicker() : section({ id: "theme" }, + div({ class: "tags-header" }, + h2(i18n.theme), + p(i18n.themeIntro), + form( + { action: "/settings/theme", method: "post" }, + select({ name: "theme" }, ...themeElements), + br(), + br(), + button({ type: "submit" }, i18n.setTheme) + ) + ) + ), + workflowSection, + section({ id: "home-page" }, div({ class: "tags-header" }, h2(i18n.homePageTitle), p(i18n.homePageDescription), form( { action: "/settings/home-page", method: "POST" }, select({ name: "homePage" }, - option({ value: "activity", selected: currentConfig.homePage === "activity" ? true : undefined }, i18n.activityTitle), - option({ value: "ai", selected: currentConfig.homePage === "ai" ? true : undefined }, i18n.aiTitle), - option({ value: "trending", selected: currentConfig.homePage === "trending" ? true : undefined }, i18n.trendingTitle), - option({ value: "opinions", selected: currentConfig.homePage === "opinions" ? true : undefined }, i18n.opinionsTitle), - option({ value: "forum", selected: currentConfig.homePage === "forum" ? true : undefined }, i18n.forumTitle), - option({ value: "feed", selected: currentConfig.homePage === "feed" ? true : undefined }, i18n.feedTitle), - option({ value: "mentions", selected: currentConfig.homePage === "mentions" ? true : undefined }, i18n.mentions), - option({ value: "inbox", selected: currentConfig.homePage === "inbox" ? true : undefined }, i18n.inbox), - option({ value: "agenda", selected: currentConfig.homePage === "agenda" ? true : undefined }, i18n.agendaTitle), - option({ value: "favorites", selected: currentConfig.homePage === "favorites" ? true : undefined }, i18n.favoritesTitle), - option({ value: "stats", selected: currentConfig.homePage === "stats" ? true : undefined }, i18n.statsTitle), - option({ value: "blockexplorer", selected: currentConfig.homePage === "blockexplorer" ? true : undefined }, i18n.blockchain) + ...[ + { value: "activity", label: i18n.activityTitle }, + { value: "ai", label: i18n.aiTitle, mod: "ai" }, + { value: "trending", label: i18n.trendingTitle, mod: "trending" }, + { value: "forum", label: i18n.forumTitle, mod: "forum" }, + { value: "feed", label: i18n.feedTitle, mod: "feed" }, + { value: "chats", label: i18n.chatsTitle, mod: "chats" }, + { value: "inbox", label: i18n.inbox }, + { value: "mentions", label: i18n.mentions }, + { value: "agenda", label: i18n.agendaTitle, mod: "agenda" }, + { value: "market", label: i18n.marketTitle, mod: "market" }, + { value: "favorites", label: i18n.favoritesTitle, mod: "favorites" } + ].filter(o => !o.mod || modOn(o.mod)).map(o => currentConfig.homePage === o.value + ? option({ value: o.value, selected: true }, o.label) + : option({ value: o.value }, o.label)) ), br(), br(), button({ type: "submit" }, i18n.saveHomePage) ) ) ), - section( + modOn('ai') ? section({ id: "ai" }, + div({ class: "tags-header" }, + h2(i18n.aiTitle), + p(i18n.aiSettingsDescription), + form( + { action: "/settings/ai", method: "POST" }, + input({ + type: "text", + id: "ai_prompt", + name: "ai_prompt", + placeholder: aiPrompt, + value: aiPrompt, + maxlength: "128", + required: true + }), br(), + label({ for: "aiSuggestions", class: "lan-checkbox-label" }, + input({ + type: "checkbox", + id: "aiSuggestions", + name: "ai_suggestions", + value: "on", + class: "lan-checkbox-input", + checked: currentConfig.ai?.suggestions !== false ? true : undefined + }), + span({ class: "lan-checkbox-text" }, i18n.aiSuggestionsEnable) + ), + br(), + button({ type: "submit" }, i18n.saveSettings), + Number(aiExportCount) > 0 ? button({ type: "submit", formaction: "/ai/export", attrs: { formmethod: "GET" }, class: "ai-export-btn" }, `${i18n.aiExportFineTuning} (${aiExportCount})`) : null + ) + ) + ) : null, + section({ id: "logstream" }, div({ class: "tags-header" }, h2(i18n.ssbLogStream), p(i18n.ssbLogStreamDescription), @@ -171,7 +290,127 @@ const settingsView = ({ version, aiPrompt, fediverseAccount, fediverseError }) = ) ) ), - section( + section({ id: "lan" }, + div({ class: "tags-header" }, + h2(i18n.settingsLanTitle || 'LAN Broadcasting'), + p(i18n.settingsLanDesc || 'Periodically announce this peer to other Oasis instances on the same local network.'), + form( + { action: "/settings/lan-broadcasting", method: "POST" }, + label({ for: "lanBroadcasting", class: "lan-checkbox-label" }, + input({ + type: "checkbox", + id: "lanBroadcasting", + name: "lanBroadcasting", + value: "on", + class: "lan-checkbox-input", + checked: currentConfig.lanBroadcasting !== false ? true : undefined + }), + span({ class: "lan-checkbox-text" }, i18n.settingsLanEnable || 'Enable LAN broadcasting') + ), + br(), + button({ type: "submit" }, i18n.saveSettings) + ) + ) + ), + modOn('phone') ? section({ id: "phone" }, + div({ class: "tags-header" }, + h2(i18n.phoneTitle), + p(i18n.phoneSettingsDesc), + form( + { action: "/settings/phone", method: "POST" }, + select({ name: "visibility" }, + option({ value: "whole", ...(!currentPhone.dnd && currentPhone.visibility !== "mutuals" ? { selected: true } : {})}, i18n.settingsPmVisibilityWhole), + option({ value: "mutuals", ...(!currentPhone.dnd && currentPhone.visibility === "mutuals" ? { selected: true } : {})}, i18n.settingsPmVisibilityMutuals), + option({ value: "dnd", ...(currentPhone.dnd ? { selected: true } : {})}, i18n.phoneDnd) + ), br(), br(), + label(i18n.settingsPhoneVideo), br(), + select({ name: "video" }, + option({ value: "on", ...(currentPhone.video !== false ? { selected: true } : {}) }, i18n.settingsPhoneVideoOn), + option({ value: "off", ...(currentPhone.video === false ? { selected: true } : {}) }, i18n.settingsPhoneVideoOff) + ), br(), br(), + button({ type: "submit" }, i18n.saveSettings) + ) + ) + ) : null, + modOn('wallet') ? section( + { id: "wallet" }, + div({ class: "tags-header" }, + h2(i18n.wallet), + p( + isMobile ? i18n.walletSettingsMobileDescription : i18n.walletSettingsDescription + ), + isMobile ? a({ href: require('../backend/updater.js').ecoinWalletDownloadUrl(), class: "update-download-link" }, i18n.walletDownloadEcoin) : null, + form( + { action: "/settings/wallet", method: "POST" }, + label({ for: "wallet_url" }, i18n.walletRpcUrl), br(), + input({ type: "text", id: "wallet_url", name: "wallet_url", placeholder: "http://localhost:7474", value: walletUrl || "http://localhost:7474" }), br(), + label({ for: "wallet_user" }, i18n.walletUser), br(), + input({ type: "text", id: "wallet_user", name: "wallet_user", placeholder: "ecoinrpc", value: walletUser }), br(), + label({ for: "wallet_pass" }, i18n.walletPass), br(), + input({ type: "password", id: "wallet_pass", name: "wallet_pass" }), br(), + label({ for: "wallet_fee" }, i18n.walletFee), br(), + input({ type: "text", id: "wallet_fee", name: "wallet_fee", placeholder: "5", value: walletFee || "5" }), br(), + button({ type: "submit" }, i18n.walletConfiguration), + walletUser ? " " : null, + walletUser ? button({ type: "submit", class: "delete-btn", formaction: "/settings/wallet/disconnect", attrs: { formmethod: "POST" } }, i18n.walletDisconnectButton) : null + ) + ) + ) : null, + section({ id: "verification" }, + div({ class: "tags-header" }, + h2(i18n.verificationTitle), + p(i18n.verificationDescription), + form({ action: "/settings/verify", method: "post" }, button({ type: "submit" }, i18n.verificationRun)), + verification && verification.error ? p({ class: "backup-check-bad" }, verification.error) : renderVerificationReport(verification) + ) + ), + section({ id: "indexes" }, + div({ class: "tags-header" }, + h2(i18n.indexes), + p(i18n.indexesDescription), + rebuildButton, + renderRebuildReport(rebuild) + ) + ), + section({ id: "export" }, + div({ class: "tags-header" }, + h2(i18n.exportDataTitle), + p(i18n.exportDataDescription), + form( + { action: "/export/create", method: "POST", id: "exportForm" }, + button({ type: "submit" }, i18n.exportDataButton) + ) + ) + ), + section({ id: "panic" }, + div({ class: "tags-header" }, + h2(i18n.panicMode), + p(i18n.removeDataDescription), + form( + { action: "/panic/remove", method: "POST", id: "removeForm" }, + button({ type: "submit" }, i18n.removePanicButton) + ) + ) + ) + ]; + const contentParts = [ + section({ id: "wish" }, + div({ class: "tags-header" }, + h2(i18n.settingsWishTitle), + p(i18n.settingsWishDesc), + form( + { action: "/settings/wish", method: "POST" }, + select({ name: "wish" }, + option({ value: "whole", ...(currentWish === "whole" ? { selected: true } : {})}, i18n.settingsWishWhole), + option({ value: "mutuals", ...(currentWish === "mutuals" ? { selected: true } : {})}, i18n.settingsWishMutuals), + option({ value: "only-lan", ...(currentWish === "only-lan" ? { selected: true } : {})}, i18n.settingsWishOnlyLan || "Only LAN"), + option({ value: "local", ...(currentWish === "local" ? { selected: true } : {})}, i18n.settingsWishLocal || "Local") + ), br(), br(), + button({ type: "submit" }, i18n.saveSettings) + ) + ) + ), + section({ id: "replication" }, div({ class: "tags-header" }, h2(i18n.settingsReplicationTitle || 'Replication'), p(i18n.settingsReplicationDesc || 'Configure the number of hops your peer follows out from your own feed when replicating content.'), @@ -192,175 +431,248 @@ const settingsView = ({ version, aiPrompt, fediverseAccount, fediverseError }) = ) ) ), - section( + blobCache ? section({ id: "blobcache" }, div({ class: "tags-header" }, - h2(i18n.settingsLanTitle || 'LAN Broadcasting'), - p(i18n.settingsLanDesc || 'Periodically announce this peer to other Oasis instances on the same local network. Disable to stop UDP broadcasts.'), + h2(i18n.settingsBlobCacheTitle), + p(i18n.settingsBlobCacheDesc), + blobCache.usage ? p({ class: "blob-cache-usage" }, strong(`${i18n.settingsBlobCacheUsage}: `), `${sizeLabel(blobCache.usage.bytes)} · ${blobCache.usage.count} ${i18n.settingsBlobCacheFiles}`) : null, + blobCache.cleaned !== null ? p({ class: "blob-cache-notice" }, String(i18n.settingsBlobCacheCleaned).replace('{n}', String(blobCache.cleaned)).replace('{size}', sizeLabel(blobCache.freed))) : null, form( - { action: "/settings/lan-broadcasting", method: "POST" }, - label({ for: "lanBroadcasting", class: "lan-checkbox-label" }, - input({ - type: "checkbox", - id: "lanBroadcasting", - name: "lanBroadcasting", - value: "on", - class: "lan-checkbox-input", - checked: currentConfig.lanBroadcasting !== false ? true : undefined - }), - span({ class: "lan-checkbox-text" }, i18n.settingsLanEnable || 'Enable LAN broadcasting') - ), + { action: "/settings/blob-cache", method: "POST" }, + label({ for: "blob_cache_mb" }, i18n.settingsBlobCacheLimit), br(), - button({ type: "submit" }, i18n.saveSettings) + select({ id: "blob_cache_mb", name: "blob_cache_mb" }, ...BLOB_CACHE_OPTIONS.map(mb => option({ value: String(mb), ...(mb === blobCache.maxMB ? { selected: true } : {}) }, blobCacheOptionLabel(mb)))), + br(), br(), + button({ type: "submit" }, i18n.saveSettings), + blobCache.maxMB > 0 ? button({ type: "submit", formaction: "/settings/blob-cache/collect", class: "blob-cache-clean-btn" }, i18n.settingsBlobCacheCleanNow) : null ) ) - ), - section( - div({ class: "tags-header" }, - h2(i18n.settingsWishTitle), - p(i18n.settingsWishDesc), - form( - { action: "/settings/wish", method: "POST" }, - select({ name: "wish" }, - option({ value: "whole", selected: currentWish === "whole" ? true : undefined }, i18n.settingsWishWhole), - option({ value: "mutuals", selected: currentWish === "mutuals" ? true : undefined }, i18n.settingsWishMutuals), - option({ value: "only-lan", selected: currentWish === "only-lan" ? true : undefined }, i18n.settingsWishOnlyLan || "Only LAN") - ), br(), br(), - button({ type: "submit" }, i18n.saveSettings) - ) - ) - ), - section( + ) : null, + section({ id: "pm-visibility" }, div({ class: "tags-header" }, h2(i18n.settingsPmVisibilityTitle), p(i18n.settingsPmVisibilityDesc), form( { action: "/settings/pm-visibility", method: "POST" }, select({ name: "pmVisibility" }, - option({ value: "whole", selected: currentPmVisibility === "whole" ? true : undefined }, i18n.settingsPmVisibilityWhole), - option({ value: "mutuals", selected: currentPmVisibility === "mutuals" ? true : undefined }, i18n.settingsPmVisibilityMutuals) + option({ value: "whole", ...(currentPmVisibility === "whole" ? { selected: true } : {})}, i18n.settingsPmVisibilityWhole), + option({ value: "following", ...(currentPmVisibility === "following" ? { selected: true } : {})}, i18n.settingsPmVisibilityFollowing), + option({ value: "mutuals", ...(currentPmVisibility === "mutuals" ? { selected: true } : {})}, i18n.settingsPmVisibilityMutuals) ), br(), br(), button({ type: "submit" }, i18n.saveSettings) ) ) ), - section( - { id: "wallet" }, + section({ id: "mentions-visibility" }, div({ class: "tags-header" }, - h2(i18n.wallet), - p( - i18n.walletSettingsDescription, " ", - a({ href: "docs/ecoin.md", target: "_blank", rel: "noopener" }, `[${i18n.walletSettingsDocLink}]`) - ), + h2(i18n.settingsMentionsVisibilityTitle), + p(i18n.settingsMentionsVisibilityDesc), form( - { action: "/settings/wallet", method: "POST" }, - label({ for: "wallet_url" }, i18n.walletAddress), br(), - input({ type: "text", id: "wallet_url", name: "wallet_url", placeholder: walletUrl, value: walletUrl }), br(), - label({ for: "wallet_user" }, i18n.walletUser), br(), - input({ type: "text", id: "wallet_user", name: "wallet_user", placeholder: walletUser, value: walletUser }), br(), - label({ for: "wallet_pass" }, i18n.walletPass), br(), - input({ type: "password", id: "wallet_pass", name: "wallet_pass" }), br(), - label({ for: "wallet_fee" }, i18n.walletFee), br(), - input({ type: "text", id: "wallet_fee", name: "wallet_fee", placeholder: walletFee, value: walletFee }), br(), - button({ type: "submit" }, i18n.walletConfiguration) + { action: "/settings/mentions-visibility", method: "POST" }, + select({ name: "mentionsVisibility" }, + option({ value: "whole", ...(currentMentionsVisibility === "whole" ? { selected: true } : {})}, i18n.settingsPmVisibilityWhole), + option({ value: "following", ...(currentMentionsVisibility === "following" ? { selected: true } : {})}, i18n.settingsPmVisibilityFollowing), + option({ value: "mutuals", ...(currentMentionsVisibility === "mutuals" ? { selected: true } : {})}, i18n.settingsPmVisibilityMutuals) + ), br(), br(), + button({ type: "submit" }, i18n.saveSettings) ) ) ), - section( + modOn('inbox') ? section({ id: "inbox-bots" }, div({ class: "tags-header" }, - h2(i18n.pubIdTitle || "PUB Wallet"), - p(i18n.pubIdDescription || "Set the PUB OASIS ID. This will be used for PUB transactions (including the UBI)."), + h2(i18n.settingsInboxBotsTitle), + p(i18n.settingsInboxBotsDesc), form( - { action: "/settings/pub-id", method: "POST" }, - input({ - type: "text", - id: "pub_id", - name: "pub_id", - value: pubId, - placeholder: i18n.pubIdPlaceholder || "@example.ed25519" - }), br(), - button({ type: "submit" }, i18n.pubIdSave || "Save configuration") + { action: "/settings/inbox-bots", method: "POST" }, + ...INBOX_BOT_ORDER.map(bot => + label({ for: `inbox-bot-${bot}`, class: "lan-checkbox-label inbox-bot-label" }, + input({ + type: "checkbox", + id: `inbox-bot-${bot}`, + name: "bots", + value: bot, + class: "lan-checkbox-input", + checked: (Array.isArray(currentConfig.inboxMutedBots) && currentConfig.inboxMutedBots.includes(bot)) ? undefined : true + }), + span({ class: "lan-checkbox-text" }, inboxBotLabel(bot)) + ) + ), + br(), + button({ type: "submit" }, i18n.saveSettings) ) ) - ), - section( + ) : null, + section({ id: "hidden" }, div({ class: "tags-header" }, - h2(i18n.aiTitle), - p(i18n.aiSettingsDescription), - form( - { action: "/settings/ai", method: "POST" }, - input({ - type: "text", - id: "ai_prompt", - name: "ai_prompt", - placeholder: aiPrompt, - value: aiPrompt, - maxlength: "128", - required: true - }), br(), - button({ type: "submit" }, i18n.aiConfiguration) - ) + h2(i18n.settingsHiddenTitle), + p(i18n.settingsHiddenDesc), + p(strong(`${i18n.settingsMutedLabel} (${viewerHidden.muted.length})`)), + viewerHidden.muted.length + ? div({ class: "viewer-hidden-block" }, table({ class: "block-info-table" }, + tr( + td({ class: "card-label" }, i18n.inhabitantProfileTitle), + td({ class: "card-label" }, i18n.date), + td({ class: "card-label" }, "") + ), + ...viewerHidden.muted.map(m => tr( + td({ "data-label": i18n.inhabitantProfileTitle }, userLink(m.id)), + td({ "data-label": i18n.date }, hiddenAt(m.at)), + td({ "data-label": "" }, form({ action: `/unmute/${encodeURIComponent(m.id)}`, method: "POST" }, + input({ type: "hidden", name: "returnTo", value: "/settings?filter=content#hidden" }), + button({ type: "submit", class: "filter-btn" }, i18n.unmute))) + )) + )) + : p({ class: "muted" }, i18n.settingsMutedEmpty), + p(strong(`${i18n.settingsHiddenLabel} (${viewerHidden.hidden.length})`)), + viewerHidden.hidden.length + ? div({ class: "viewer-hidden-block" }, table({ class: "block-info-table" }, + tr( + td({ class: "card-label" }, i18n.settingsTabContent), + td({ class: "card-label" }, i18n.date), + td({ class: "card-label" }, "") + ), + ...viewerHidden.hidden.map(h => tr( + td({ "data-label": i18n.settingsTabContent }, h.href ? a({ href: h.href }, plainLabel(h.label) || h.id) : span(plainLabel(h.label) || h.id)), + td({ "data-label": i18n.date }, hiddenAt(h.at)), + td({ "data-label": "" }, form({ action: `/unhide/${encodeURIComponent(h.id)}`, method: "POST" }, + input({ type: "hidden", name: "returnTo", value: "/settings?filter=content#hidden" }), + button({ type: "submit", class: "filter-btn" }, i18n.unhideContent))) + )) + )) + : p({ class: "muted" }, i18n.settingsHiddenEmpty) ) ), - section( - { id: "fediverse" }, + ]; + const multiverseParts = modOn('fediverse') ? [ + section({ id: "mastodon" }, div({ class: "tags-header" }, - h2(i18n.fediverseSettingsTitle), - div({ class: "fediverse-network" }, - h3("Mastodon"), - p(i18n.fediverseTokenHelp), - fediverseError ? p({ class: "fediverse-error" }, i18n[fediverseError] || i18n.fediverseError) : "", - fediverseAccount - ? (() => { - const host = String(fediverseAccount.instance || "").replace(/^https?:\/\//, ""); - const profileUrl = `${fediverseAccount.instance}/@${fediverseAccount.acct}`; - const link = (txt) => a({ href: profileUrl, target: "_blank", rel: "noopener noreferrer" }, txt); - return form( - { action: "/settings/fediverse/disconnect", method: "POST" }, - p( - `${i18n.fediverseConnectedAs}: `, - link(`${fediverseAccount.acct}@${host}`) - ), - button({ type: "submit" }, i18n.fediverseDisconnect) - ); - })() - : form( - { action: "/settings/fediverse", method: "POST" }, - label({ for: "fediverse_instance" }, i18n.fediverseInstanceLabel), br(), - input({ type: "text", id: "fediverse_instance", name: "instance", placeholder: "mastodon.social", required: true }), br(), - label({ for: "fediverse_token" }, i18n.fediverseTokenLabel), br(), - input({ type: "password", id: "fediverse_token", name: "token", autocomplete: "off", required: true }), br(), - button({ type: "submit" }, i18n.fediverseConnect) + h2("Mastodon"), + fediverseAccount + ? (() => { + const host = String(fediverseAccount.instance || "").replace(/^https?:\/\//, ""); + const profileUrl = `${fediverseAccount.instance}/@${fediverseAccount.acct}`; + const link = (txt) => a({ href: profileUrl, target: "_blank", rel: "noopener noreferrer" }, txt); + return form( + { action: "/settings/fediverse/disconnect", method: "POST" }, + p( + `${i18n.fediverseConnectedAs}: `, + link(`${fediverseAccount.acct}@${host}`) + ), + button({ type: "submit" }, i18n.fediverseDisconnect) + ); + })() + : form( + { action: "/settings/fediverse", method: "POST" }, + label({ for: "fediverse_instance" }, i18n.fediverseInstanceLabel), br(), + input({ type: "text", id: "fediverse_instance", name: "instance", placeholder: "mastodon.social", required: true }), br(), + label({ for: "fediverse_token" }, i18n.fediverseTokenLabel), br(), + input({ type: "password", id: "fediverse_token", name: "token", autocomplete: "off", required: true }), br(), + button({ type: "submit" }, i18n.fediverseConnect) + ) + ) + ), + section({ id: "telegram" }, + div({ class: "tags-header" }, + h2("Telegram"), + telegramAccount + ? form( + { action: "/settings/telegram/disconnect", method: "POST" }, + p(`${i18n.fediverseConnectedAs}: `, strong(telegramAccount.displayName || ""), telegramAccount.username ? span(` (@${telegramAccount.username})`) : ""), + br(), + button({ type: "submit" }, i18n.fediverseDisconnect) + ) + : telegramLogin && telegramLogin.step === "code" + ? form( + { action: "/settings/telegram/code", method: "POST" }, + br(), + p(i18n.telegramCodeHelp), + input({ type: "text", id: "telegram_code", name: "code", autocomplete: "off", inputmode: "numeric", placeholder: "12345", required: true }), br(), br(), + button({ type: "submit" }, i18n.telegramVerify), + " ", + button({ type: "submit", formaction: "/settings/telegram/cancel", class: "delete-btn" }, i18n.telegramCancel) ) - ) + : telegramLogin && telegramLogin.step === "password" + ? form( + { action: "/settings/telegram/password", method: "POST" }, + br(), + p(i18n.telegramPasswordHelp), + input({ type: "password", id: "telegram_password", name: "password", autocomplete: "off", required: true }), br(), br(), + button({ type: "submit" }, i18n.telegramVerify), + " ", + button({ type: "submit", formaction: "/settings/telegram/cancel", class: "delete-btn" }, i18n.telegramCancel) + ) + : form( + { action: "/settings/telegram/start", method: "POST" }, + label({ for: "telegram_api_id" }, i18n.telegramApiIdLabel), br(), + input({ type: "text", id: "telegram_api_id", name: "apiId", inputmode: "numeric", placeholder: "12345678", required: true }), br(), + label({ for: "telegram_api_hash" }, i18n.fediverseTokenLabel), br(), + input({ type: "password", id: "telegram_api_hash", name: "apiHash", autocomplete: "off", placeholder: "0123456789abcdef0123456789abcdef", required: true }), br(), + label({ for: "telegram_phone" }, i18n.telegramPhoneLabel), br(), + input({ type: "tel", id: "telegram_phone", name: "phone", placeholder: "+34 60000000", required: true }), br(), br(), + button({ type: "submit" }, i18n.fediverseConnect) + ) ) ), - section( + section({ id: "peertube" }, div({ class: "tags-header" }, - h2(i18n.indexes), - p(i18n.indexesDescription), - rebuildButton - ) - ), - section( - div({ class: "tags-header" }, - h2(i18n.exportDataTitle), - p(i18n.exportDataDescription), - form( - { action: "/export/create", method: "POST", id: "exportForm" }, - button({ type: "submit" }, i18n.exportDataButton) - ) - ) - ), - section( - div({ class: "tags-header" }, - h2(i18n.panicMode), - p(i18n.removeDataDescription), - form( - { action: "/panic/remove", method: "POST", id: "removeForm" }, - button({ type: "submit" }, i18n.removePanicButton) - ) + h2("PeerTube"), + peertubeAccount + ? form( + { action: "/settings/peertube/disconnect", method: "POST" }, + p(`${i18n.fediverseConnectedAs}: `, a({ href: peertubeAccount.channelUrl || peertubeAccount.profileUrl, target: "_blank", rel: "noopener noreferrer" }, peertubeAccount.handle)), + button({ type: "submit" }, i18n.fediverseDisconnect) + ) + : form( + { action: "/settings/peertube", method: "POST" }, + label({ for: "peertube_instance" }, i18n.fediverseInstanceLabel), br(), + input({ type: "text", id: "peertube_instance", name: "instance", placeholder: "peertube.example.org", required: true }), br(), + label({ for: "peertube_username" }, i18n.peertubeUsernameLabel), br(), + input({ type: "text", id: "peertube_username", name: "username", autocomplete: "off", required: true }), br(), + label({ for: "peertube_password" }, i18n.peertubePasswordLabel), br(), + input({ type: "password", id: "peertube_password", name: "password", autocomplete: "off", required: true }), br(), + button({ type: "submit" }, i18n.fediverseConnect) + ) ) ) + , + section({ id: "nextcloud" }, + div({ class: "tags-header" }, + h2("Nextcloud"), + nextcloudAccount + ? form( + { action: "/settings/nextcloud/disconnect", method: "POST" }, + p(`${i18n.fediverseConnectedAs}: `, a({ href: nextcloudAccount.server, target: "_blank", rel: "noopener noreferrer" }, `${nextcloudAccount.user}@${String(nextcloudAccount.server).replace(/^https?:\/\//, "")}`), ` (${nextcloudAccount.folder})`), + button({ type: "submit" }, i18n.fediverseDisconnect) + ) + : form( + { action: "/settings/nextcloud", method: "POST" }, + label({ for: "nextcloud_server" }, i18n.nextcloudServerLabel), br(), + input({ type: "text", id: "nextcloud_server", name: "server", placeholder: "cloud.example.org", required: true }), br(), + label({ for: "nextcloud_user" }, i18n.nextcloudUserLabel), br(), + input({ type: "text", id: "nextcloud_user", name: "user", autocomplete: "off", required: true }), br(), + label({ for: "nextcloud_password" }, i18n.peertubePasswordLabel), br(), + input({ type: "password", id: "nextcloud_password", name: "password", autocomplete: "off", required: true }), br(), + label({ for: "nextcloud_folder" }, i18n.nextcloudFolderLabel), br(), + input({ type: "text", id: "nextcloud_folder", name: "folder", placeholder: "Oasis" }), br(), + button({ type: "submit" }, i18n.fediverseConnect) + ) + ) + ) + ] : []; + return template( + i18n.settings, + section( + div({ class: "tags-header module-header-line" }, + h2(i18n.settings), + p(i18n.settingsDescription) + ), + div({ class: "mode-buttons-row" }, + a({ class: filter === 'oasis' ? 'filter-btn active' : 'filter-btn', href: '/settings?filter=oasis' }, String(i18n.linkBoxOasis).toUpperCase()), + a({ class: filter === 'content' ? 'filter-btn active' : 'filter-btn', href: '/settings?filter=content' }, String(i18n.settingsTabContent).toUpperCase()), + modOn('fediverse') ? a({ class: filter === 'multiverse' ? 'filter-btn active' : 'filter-btn', href: '/settings?filter=multiverse' }, String(i18n.multiverse).toUpperCase()) : null + ) + ), + ...(filter === 'multiverse' ? multiverseParts : filter === 'content' ? contentParts : (isMobile ? mobileUi.settingsOrder(oasisParts) : oasisParts)) ); }; diff --git a/src/views/shops_view.js b/src/views/shops_view.js index 7f50ab0..ce324ef 100644 --- a/src/views/shops_view.js +++ b/src/views/shops_view.js @@ -1,15 +1,19 @@ -const { div, h2, p, section, button, form, a, span, textarea, br, input, label, select, option, img, progress, video, table, tr, td } = require("../server/node_modules/hyperaxe") -const { template, i18n, userLink, renderStateChip, renderLifespanChip, renderSpreadButton, renderOpinionsVoting, renderInviteQrCard } = require("./main_views") +const { div, h2, p, section, button, form, a, span, textarea, br, input, label, select, option, img, progress, video, audio, table, tr, td, hr } = require("../server/node_modules/hyperaxe") +const { renderCommentsSection: renderSharedCommentsSection } = require("./comments_view"); +const { renderCallButton, clearnetItemHref, template, i18n, userLink, renderStateChip, renderLifespanChip, renderSpreadButton, renderOpinionsVoting, renderEngagement, renderInviteQrCard , renderSpreadEditWarning, renderContentActions, renderSubscriptionBox, renderModuleStats, renderModuleStatsBy, moduleIsEmpty, renderEcoValueChip, renderWalletChip, contentDeleteAction, paged } = require("./main_views") const moment = require("../server/node_modules/moment") const { config } = require("../server/SSB_server.js") -const { renderUrl } = require("../backend/renderUrl") +const { renderStyledText } = require("../backend/renderStyledText") const { renderMapLocationUrl, renderMapEmbed, renderMapLocationVisitLabel } = require("./maps_view") const opinionCategories = require("../backend/opinion_categories") -const { renderReachChip, renderClearnetUrlBlock, renderClearnetPage, renderClearnetSearchForm, renderEncryptedChip, blobUrl: cnBlobUrl, escapeHtml: cnEscapeHtml } = require("./clearnet_view") +const { renderReachChip, renderClearnetSelector, renderClearnetSwitch, renderClearnetUrlBlock, renderClearnetPage, renderClearnetSearchForm, renderEncryptedChip, blobUrl: cnBlobUrl, escapeHtml: cnEscapeHtml, renderRichText: cnRichText, renderKindTag: cnKindTag } = require("./clearnet_view") const userId = config.keys.id const safeArr = (v) => (Array.isArray(v) ? v : []) const safeText = (v) => String(v || "").trim() +const clearnetEligible = (shop) => String(shop.visibility || "").toUpperCase() !== "CLOSED" && !shop.encrypted +const SHOP_REACH = ["OPEN", "CLOSED"] +const shopReachLabel = (v) => v === "CLOSED" ? i18n.shopClosed : i18n.shopOpen const voteSum = (opinions = {}) => Object.values(opinions || {}).reduce((s, n) => s + (Number(n) || 0), 0) const sumCats = (opinions = {}, cats = []) => (cats || []).reduce((s, c) => s + (Number((opinions || {})[c]) || 0), 0) const renderStarRating = (opinions, voterCount) => { @@ -25,11 +29,13 @@ const renderMediaBlob = (value, fallbackSrc = null, attrs = {}) => { if (!value) return fallbackSrc ? img({ src: fallbackSrc, ...attrs }) : null const s = String(value).trim() if (!s) return fallbackSrc ? img({ src: fallbackSrc, ...attrs }) : null - if (s.startsWith('&')) return img({ src: `/blob/${encodeURIComponent(s)}`, ...attrs }) + if (s.startsWith('&')) return img({ loading: 'lazy', src: `/blob/${encodeURIComponent(s)}`, ...attrs }) const mVideo = s.match(/\[video:[^\]]*\]\(\s*(&[^)\s]+\.sha256)\s*\)/) - if (mVideo) return video({ controls: true, class: attrs.class || 'post-video', src: `/blob/${encodeURIComponent(mVideo[1])}` }) + if (mVideo) return video({ controls: true, class: attrs.class || 'post-video', src: `/blob/${encodeURIComponent(mVideo[1])}`, preload: 'metadata' }) + const mAudio = s.match(/\[audio:[^\]]*\]\(\s*(&[^)\s]+\.sha256)\s*\)/) + if (mAudio) return audio({ controls: true, class: attrs.class || 'post-audio', src: `/blob/${encodeURIComponent(mAudio[1])}`, preload: 'metadata' }) const mImg = s.match(/!\[[^\]]*\]\(\s*(&[^)\s]+\.sha256)\s*\)/) - if (mImg) return img({ src: `/blob/${encodeURIComponent(mImg[1])}`, ...attrs }) + if (mImg) return img({ loading: 'lazy', src: `/blob/${encodeURIComponent(mImg[1])}`, ...attrs }) return fallbackSrc ? img({ src: fallbackSrc, ...attrs }) : null } @@ -43,35 +49,33 @@ const buildReturnTo = (filter, params = {}) => { return `/shops?${parts.join("&")}` } -const renderModeButtons = (currentFilter) => +const renderModeButtons = (currentFilter, emptyMod = false, modesAvail = null) => div({ class: "tribe-mode-buttons" }, - ["all", "recent", "mine", "top", "products", "prices", "favorites"].map(f => + ...(emptyMod ? [] : [ + ["all", "recent", "mine", "top", "products", "prices", "favorites"].filter(f => f === "all" || f === currentFilter || (modesAvail && modesAvail[f] !== false)).map(f => form({ method: "GET", action: "/shops" }, input({ type: "hidden", name: "filter", value: f }), button({ type: "submit", class: currentFilter === f ? "filter-btn active" : "filter-btn" }, i18n[`shopFilter${f.charAt(0).toUpperCase() + f.slice(1)}`] || f.toUpperCase()) ) ), - form({ method: "GET", action: "/shops/purchases" }, + ...((currentFilter === "purchases" || !modesAvail || modesAvail.purchases !== false) ? [form({ method: "GET", action: "/shops/purchases" }, button({ type: "submit", class: currentFilter === "purchases" ? "filter-btn active" : "filter-btn" }, (i18n.shopPurchasesButton || "Purchases").toUpperCase()) - ), + )] : []), + ]), form({ method: "GET", action: "/shops" }, input({ type: "hidden", name: "filter", value: "create" }), button({ type: "submit", class: "create-button" }, i18n.shopUpload) ) ) -const renderFavoriteToggle = (shop, returnTo) => - form( - { method: "POST", action: shop.isFavorite ? `/shops/favorites/remove/${encodeURIComponent(shop.key)}` : `/shops/favorites/add/${encodeURIComponent(shop.key)}` }, - returnTo ? input({ type: "hidden", name: "returnTo", value: returnTo }) : null, - button({ type: "submit", class: "filter-btn" }, shop.isFavorite ? i18n.shopRemoveFavorite : i18n.shopAddFavorite) - ) - const renderShopCard = exports.renderShopCard = (shop, filter, params = {}) => { const returnTo = buildReturnTo(filter, params) const isAuthor = String(shop.author) === String(userId) return div({ class: "tribe-card" }, + div({ class: "card-header activity-card-header" }, + renderContentActions(shop.key, `/shops/${encodeURIComponent(shop.key)}`, { spread: params.spreadMap && params.spreadMap.get(shop.key) || null, author: shop.author, favKind: 'shops', isFavorite: shop.isFavorite, reportTitle: shop.title, returnTo, deleteAction: isAuthor ? contentDeleteAction('shop', shop.key) : undefined }) + ), div({ class: "tribe-card-image-wrapper" }, a({ href: `/shops/${encodeURIComponent(shop.key)}` }, renderMediaBlob(shop.image, '/assets/images/default-avatar.png', { class: 'tribe-card-hero-image' }) @@ -85,35 +89,36 @@ const renderShopCard = exports.renderShopCard = (shop, filter, params = {}) => { shop.visibility === "CLOSED" ? renderStateChip("closed", "✗", i18n.shopClosed) : renderStateChip("mutuals", "✓", i18n.shopOpen), - renderLifespanChip(shop.lifetime, i18n) + renderLifespanChip(shop.lifetime, i18n), + shop.clearnet === true && clearnetEligible(shop) ? renderReachChip(true, i18n, clearnetItemHref('shops', shop.title, shop.key)) : null, + shop.subscriptionIn === true + ? renderStateChip("mutuals", "✉", i18n.subscriptionOn) + : (shop.subscriptionIn === false ? renderStateChip("closed", "✉", i18n.subscriptionOff) : null) ), div({ class: "tribe-card-members" }, span({ class: "tribe-members-count" }, `${i18n.shopProducts}: ${shop.productCount || 0}`) - ), - div({ class: "card-spread-centered" }, renderSpreadButton(shop.key, params.spreadMap && params.spreadMap.get(shop.key))), - div({ class: "card-visit-btn-centered" }, - form({ method: 'GET', action: `/shops/${encodeURIComponent(shop.key)}` }, - button({ type: 'submit', class: 'filter-btn' }, i18n.viewShop || i18n.shopVisitShop || 'View Shop') - ) ) ) ) } -const renderProductCard = (product, shopId, returnTo) => { +const renderProductCard = (product, shopId, returnTo, params = {}) => { const isAuthor = String(product.author) === String(userId) const stock = Number(product.stock) || 0 const voterCount = safeArr(product.opinions_inhabitants).length const productUrl = `/shops/product/${encodeURIComponent(product.key)}?shopId=${encodeURIComponent(shopId)}` return div({ class: "shop-product-card" }, + div({ class: "card-header activity-card-header" }, + renderContentActions(product.key, `/shops/product/${encodeURIComponent(product.key)}`, { spread: params.spreadMap && params.spreadMap.get(product.key) || null, author: product.author, favKind: 'shopProducts', isFavorite: product.isFavorite, reportTitle: product.title, returnTo, deleteAction: isAuthor ? contentDeleteAction('shopProduct', product.key) : undefined }) + ), product.image ? div({ class: "shop-product-media" }, a({ href: productUrl }, renderMediaBlob(product.image))) : null, div({ class: "shop-product-body" }, product.shopTitle ? p(a({ href: `/shops/${encodeURIComponent(shopId)}`, class: "user-link" }, product.shopTitle)) : null, h2(a({ href: productUrl }, product.title || i18n.shopProductUntitled)), renderStarRating(product.opinions, voterCount), - product.description ? p(...renderUrl(product.description)) : null, - div({ class: "shop-product-price" }, `${Number(product.price || 0).toFixed(6)} ECO`), + product.description ? p(...renderStyledText(product.description)) : null, + div({ class: "price-chip" }, `${Number(product.price || 0).toFixed(6)} ECO`), div({ class: "confirmations-block stock-block" }, div({ class: "card-field" }, span({ class: "card-label" }, `${i18n.shopProductStock}: `), @@ -126,9 +131,6 @@ const renderProductCard = (product, shopId, returnTo) => { if (!isAuthor && stock > 0) { actions.push(a({ href: productUrl, class: "buy-btn" }, i18n.marketActionsBuy || i18n.shopBuy)); } - actions.push(form({ method: "POST", action: product.isFavorite ? `/shops/favorites/remove/${encodeURIComponent(product.key)}` : `/shops/favorites/add/${encodeURIComponent(product.key)}` }, - returnTo ? input({ type: "hidden", name: "returnTo", value: returnTo }) : null, - button({ type: "submit", class: "filter-btn" }, product.isFavorite ? i18n.shopRemoveFavorite : i18n.shopAddFavorite))); return actions.length ? div({ class: "shop-product-actions" }, ...actions) : null; })() ) @@ -136,55 +138,48 @@ const renderProductCard = (product, shopId, returnTo) => { } const renderCommentsSection = (parentId, returnTo, comments = []) => { - const count = Array.isArray(comments) ? comments.length : 0 - return div({ class: "vote-comments-section market-comments" }, - div({ class: "comments-count" }, span({ class: "card-label" }, i18n.voteCommentsLabel + ": "), span({ class: "card-value" }, String(count))), - div({ class: "comment-form-wrapper" }, - h2({ class: "comment-form-title" }, i18n.voteNewCommentLabel), - form({ method: "POST", action: `/shops/${encodeURIComponent(parentId)}/comments`, class: "comment-form", enctype: "multipart/form-data" }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - textarea({ name: "text", rows: 4, class: "comment-textarea", placeholder: i18n.voteNewCommentPlaceholder }), - div({ class: "comment-file-upload" }, label(i18n.uploadMedia), input({ type: "file", name: "blob" })), - br(), - button({ type: "submit", class: "comment-submit-btn" }, i18n.voteNewCommentButton) - ) - ), - count - ? div({ class: "comments-list" }, - comments.map(c => { - const author = c.value?.author || "" - const ts = c.value?.timestamp || c.timestamp - const text = c.value?.content?.text || "" - const rootId = c.value?.content?.fork || c.value?.content?.root || null - return div({ class: "votations-comment-card" }, - span({ class: "created-at" }, - span(i18n.createdBy), - author ? userLink(author) : span("(unknown)"), - ts ? span(" | ", span({ class: "votations-comment-date" }, moment(ts).format("YYYY/MM/DD HH:mm:ss"))) : "", - ts && rootId ? span(" | ", a({ href: `/thread/${encodeURIComponent(rootId)}#${encodeURIComponent(c.key)}` }, moment(ts).fromNow())) : "" - ), - p({ class: "votations-comment-text" }, ...renderUrl(text)) - ) - }) - ) - : p({ class: "votations-no-comments" }, i18n.voteNoCommentsYet) - ) -} + return renderSharedCommentsSection({ + action: `/shops/${encodeURIComponent(parentId)}/comments`, + comments: comments, + returnTo: returnTo + }); +}; const renderShopForm = (filter, shop = {}, params = {}) => { const isEdit = filter === "edit" const returnTo = safeText(params.returnTo) || buildReturnTo("all") + const reachRaw = String(params.reach || "").toUpperCase() + const ownReach = String(shop.visibility || "").toUpperCase() + const reach = SHOP_REACH.includes(reachRaw) ? reachRaw : (SHOP_REACH.includes(ownReach) ? ownReach : "OPEN") + const showClearnet = isEdit ? clearnetEligible(shop) : reach === "OPEN" && clearnetEligible({ ...shop, visibility: reach }) return div({ class: "create-tribe-form" }, h2(isEdit ? i18n.shopUpdateSectionTitle : i18n.shopCreateSectionTitle), + isEdit ? (params.spreadWarning || null) : null, + ...(isEdit ? [] : [ + form({ method: "GET", action: "/shops" }, + input({ type: "hidden", name: "filter", value: "create" }), + label(i18n.shopVisibility), br(), + div({ class: "apply-row" }, + select({ name: "visibility", class: "report-category-select" }, + option({ value: "OPEN", ...(reach === "OPEN" ? { selected: true } : {}) }, i18n.shopOpen), + option({ value: "CLOSED", ...(reach === "CLOSED" ? { selected: true } : {}) }, i18n.shopClosed) + ), + button({ type: "submit", class: "create-button" }, i18n.apply || "Apply") + ) + ), + hr({ class: "form-sep" }), + h2({ class: "report-category-fixed" }, shopReachLabel(reach)) + ]), form({ action: isEdit ? `/shops/update/${encodeURIComponent(shop.key || "")}` : "/shops/create", method: "POST", enctype: "multipart/form-data" }, input({ type: "hidden", name: "returnTo", value: returnTo }), + isEdit ? null : input({ type: "hidden", name: "visibility", value: reach }), label(i18n.title || "Title"), br, - input({ type: "text", name: "title", required: true, placeholder: i18n.shopTitlePlaceholder || "Name of your shop", value: shop.title || "" }), br(), + input({ type: "text", name: "title", maxlength: "100", required: true, placeholder: i18n.shopTitlePlaceholder || "Name of your shop", value: shop.title || "" }), br(), label(i18n.shopShortDescription), br, input({ type: "text", name: "shortDescription", required: true, maxlength: 160, placeholder: i18n.shopShortDescriptionPlaceholder || "Brief description of your shop", value: shop.shortDescription || "" }), br(), label(i18n.description || "Description"), br, - textarea({ name: "description", rows: 4, placeholder: i18n.shopDescriptionPlaceholder || "Detailed description of your shop" }, shop.description || ""), br, - label(i18n.blogImage || "Upload media (max-size: 50MB)"), br, + textarea({ maxlength: "5000", name: "description", rows: 4, placeholder: i18n.shopDescriptionPlaceholder || "Detailed description of your shop" }, shop.description || ""), br, + label(i18n.blogImage || "Upload media (max-size: 75MB)"), br, input({ type: "file", name: "image", accept: "image/*,video/*" }), br(), br(), label(i18n.shopUrl), br, input({ type: "text", name: "url", placeholder: "https://", value: shop.url || "" }), br, @@ -193,35 +188,30 @@ const renderShopForm = (filter, shop = {}, params = {}) => { label(i18n.mapLocationTitle || "Map Location"), br, input({ type: "text", name: "mapUrl", placeholder: i18n.mapUrlPlaceholder || "/maps/MAP_ID", value: shop.mapUrl || "" }), br, label(i18n.shopTags), br, - input({ type: "text", name: "tags", placeholder: i18n.shopTagsPlaceholder || "tag1, tag2, tag3", value: safeArr(shop.tags).join(", ") }), br, - isEdit ? null : label(i18n.shopVisibility), - isEdit ? null : br, - isEdit ? null : select({ name: "visibility" }, - option({ value: "OPEN", selected: (shop.visibility || "OPEN") === "OPEN" }, i18n.shopOpen), - option({ value: "CLOSED", selected: shop.visibility === "CLOSED" }, i18n.shopClosed) - ), - isEdit ? null : br(), + input({ type: "text", name: "tags", placeholder: i18n.shopTagsPlaceholder || "Enter tags separated by commas", value: Array.isArray(shop.tags) ? shop.tags.join(", ") : (shop.tags || "") }), br, + showClearnet ? renderClearnetSelector(shop.clearnet === true || String(shop.clearnet || "") === "1", i18n) : null, br(), button({ type: "submit" }, isEdit ? i18n.shopUpdate : i18n.shopCreate) ) ) } -const renderProductForm = (shopId, product = {}, isEdit = false, returnTo = "") => { +const renderProductForm = (shopId, product = {}, isEdit = false, returnTo = "", spreadWarning = null) => { return div({ class: "create-tribe-form" }, h2(isEdit ? i18n.shopProductUpdate : i18n.shopProductAdd), + isEdit ? spreadWarning : null, form({ action: isEdit ? `/shops/product/update/${encodeURIComponent(product.key || "")}` : "/shops/product/create", method: "POST", enctype: "multipart/form-data" }, input({ type: "hidden", name: "shopId", value: shopId }), input({ type: "hidden", name: "returnTo", value: returnTo }), label(i18n.title || "Title"), br, - input({ type: "text", name: "title", required: true, value: product.title || "" }), br(), + input({ type: "text", name: "title", maxlength: "100", required: true, value: product.title || "" }), br(), label(i18n.description || "Description"), br, - textarea({ name: "description", rows: 4 }, product.description || ""), br, + textarea({ maxlength: "5000", name: "description", rows: 4 }, product.description || ""), br, label(i18n.shopProductPrice), br, - input({ type: "number", name: "price", step: "0.000001", min: "0.000001", required: true, value: product.price || "" }), br(), br(), + input({ type: "number", name: "price", step: "0.000001", min: "0.000001", required: true, value: product.price || "" }), renderEcoValueChip(), br(), br(), label(i18n.shopProductStock), br, input({ type: "number", name: "stock", min: "0", value: product.stock !== undefined ? product.stock : 1 }), br(), br(), - label(i18n.blogImage || "Upload media (max-size: 50MB)"), br, + label(i18n.blogImage || "Upload media (max-size: 75MB)"), br, input({ type: "file", name: "image", accept: "image/*,video/*" }), br(), br(), input({ type: "hidden", name: "featured", value: "0" }), label(i18n.shopProductFeatured), @@ -241,66 +231,49 @@ const renderProductForm = (shopId, product = {}, isEdit = false, returnTo = "") } exports.shopsView = async (shops, filter, shopToEdit = null, params = {}) => { + if (filter === "edit") params = { ...params, spreadWarning: await renderSpreadEditWarning(shopToEdit && (shopToEdit.key || shopToEdit.id)) }; const q = safeText(params.q || "") const sort = safeText(params.sort || "recent") const list = safeArr(shops) - - const title = - filter === "mine" ? i18n.shopMineSectionTitle : - filter === "recent" ? i18n.shopRecentSectionTitle : - filter === "top" ? i18n.shopTopSectionTitle : - filter === "products" ? i18n.shopProductsSectionTitle : - filter === "prices" ? (i18n.shopPricesSectionTitle || "Products by Price") : - filter === "favorites" ? i18n.shopFavoritesSectionTitle : - filter === "create" ? i18n.shopCreateSectionTitle : - filter === "edit" ? i18n.shopUpdateSectionTitle : - i18n.shopAllSectionTitle - + const emptyMod = moduleIsEmpty(list, filter || "all", "all", q) + const title = i18n.shopsTitle const isForm = filter === "create" || filter === "edit" - - const viewerClearnet = !!(params.viewerPrefs && params.viewerPrefs.clearnetShops) - const header = [ - div({ class: "tags-header" }, - h2(title), - p(i18n.shopDescription) - ), - div({ class: "shop-title-row" }, renderReachChip(viewerClearnet, i18n)), - br() - ] - - const searchBar = div({ class: "filters" }, - form({ method: "GET", action: "/shops" }, - input({ type: "hidden", name: "filter", value: filter }), - input({ type: "text", name: "q", placeholder: i18n.shopSearchPlaceholder, value: q }), - br(), - button({ type: "submit" }, i18n.search), - br() - ) - ) - - const sortedProducts = filter === "products" - ? [...list].sort((a, b) => safeArr(b.opinions_inhabitants).length - safeArr(a.opinions_inhabitants).length) - : filter === "prices" - ? [...list].sort((a, b) => Number(b.price || 0) - Number(a.price || 0)) - : list + const isProducts = filter === "products" || filter === "prices" return template( title, - section(...header), - section(renderModeButtons(filter)), - !isForm ? section(searchBar) : null, + section(div({ class: "tags-header module-header-line" }, h2(title), p(i18n.shopDescription), renderWalletChip())), + section(renderModeButtons(filter, emptyMod, (params && params.modesAvail) || null)), + !isForm && !emptyMod + ? section( + div({ class: "filters activity-filter-chips activity-toolbar-row" }, + isProducts ? renderModuleStats(list.length) : renderModuleStatsBy(list, s => String(s.visibility || '').toUpperCase(), [{ value: 'OPEN', label: i18n.shopOpen }, { value: 'CLOSED', label: i18n.shopClosed }]), + form({ method: "GET", action: "/shops", class: "filter-box" }, + input({ type: "hidden", name: "filter", value: filter }), + input({ type: "text", name: "q", value: q, placeholder: i18n.shopSearchPlaceholder, class: "filter-box__input" }), + div({ class: "filter-box__controls" }, + select({ name: "sort", class: "filter-box__select" }, + option({ value: "recent", ...(sort === "recent" ? { selected: true } : {}) }, i18n.documentSortRecent), + option({ value: "top", ...(sort === "top" ? { selected: true } : {}) }, i18n.documentSortTop) + ), + button({ type: "submit", class: "filter-box__button" }, i18n.searchButton) + ) + ) + ) + ) + : null, section( isForm - ? renderShopForm(filter, filter === "edit" ? (shopToEdit || {}) : {}, params) - : filter === "products" || filter === "prices" + ? renderShopForm(filter, filter === "edit" ? (shopToEdit || {}) : (params.draft || {}), params) + : isProducts ? div({ class: "shop-products-grid" }, - sortedProducts.length - ? sortedProducts.map(prod => renderProductCard(prod, prod.shopId, buildReturnTo(filter, params))) - : p(i18n.shopNoItems) + list.length + ? paged(list).map(prod => renderProductCard(prod, prod.shopId, buildReturnTo(filter, { q }), params)) + : p(i18n.shopNoProducts) ) : div({ class: "tribe-grid" }, list.length - ? list.map(shop => renderShopCard(shop, filter, { q, sort, spreadMap: params.spreadMap })) + ? paged(list).map(shopItem => renderShopCard(shopItem, filter, params)) : p(i18n.shopNoItems) ) ) @@ -309,13 +282,16 @@ exports.shopsView = async (shops, filter, shopToEdit = null, params = {}) => { exports.singleShopView = async (shop, filter, products = [], comments = [], params = {}) => { const q = safeText(params.q || "") - const sort = safeText(params.sort || "recent") - const returnTo = safeText(params.returnTo) || buildReturnTo(filter, { q, sort }) + const returnTo = safeText(params.returnTo) || buildReturnTo(filter, { q }) const isAuthor = String(shop.author) === String(userId) - const fullShareUrl = `/shops/${encodeURIComponent(shop.key)}` + const canClearnet = clearnetEligible(shop) + const isClearnet = canClearnet && !!shop.clearnet + products = safeArr(products) - const isClearnet = !!(params.authorPrefs && params.authorPrefs.clearnetShops && shop.visibility !== 'CLOSED'); const shopSide = div({ class: "tribe-side" }, + div({ class: "card-header activity-card-header" }, + renderContentActions(shop.key, `/shops/${encodeURIComponent(shop.key)}`, { spread: params.spreads || null, author: shop.author, favKind: 'shops', isFavorite: shop.isFavorite, reportTitle: shop.title, deleteAction: isAuthor ? contentDeleteAction('shop', shop.key) : undefined }) + ), div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, shop.title || i18n.shopUntitled) ), @@ -325,26 +301,24 @@ exports.singleShopView = async (shop, filter, products = [], comments = [], para : renderStateChip("mutuals", "✓", i18n.shopOpen), shop.encrypted ? renderStateChip("encrypted", "🔒", i18n.encryptedChipLabel || "E2E") : null, renderLifespanChip(shop.lifetime, i18n), - renderReachChip(isClearnet, i18n) + renderReachChip(isClearnet, i18n, isClearnet ? clearnetItemHref('shops', shop.title, shop.key) : null), + isAuthor && canClearnet ? renderClearnetSwitch('shops', shop.rootId || shop.key, isClearnet) : null, + shop.subscription + ? ((isAuthor || shop.subscription.subscribed === true) + ? renderStateChip("mutuals", "✉", i18n.subscriptionOn) + : renderStateChip("closed", "✉", i18n.subscriptionOff)) + : null ), - div({ class: "card-spread-centered" }, renderSpreadButton(shop.key, params.spreads)), renderMediaBlob(shop.image, '/assets/images/default-avatar.png', { class: 'tribe-detail-image' }), - shop.description ? p({ class: "tribe-side-description" }, ...renderUrl(shop.description)) : null, - div({ class: "shop-share" }, - span({ class: "tribe-info-label" }, `${i18n.shopShareUrl}: `), - input({ type: "text", value: fullShareUrl, readonly: true, class: "shop-share-input" }) - ), + shop.description ? p({ class: "tribe-side-description" }, ...renderStyledText(shop.description)) : null, table({ class: "tribe-info-table" }, tr( td({ class: "tribe-info-label" }, i18n.shopCreatedAt || "CREATED"), - td({ class: "tribe-info-value", colspan: "3" }, new Date(shop.createdAt).toLocaleString()) - ), - tr( - td({ class: "tribe-info-value", colspan: "4" }, userLink(shop.author)) + td({ class: "tribe-info-value", colspan: "3" }, moment(shop.createdAt).format("YYYY/MM/DD HH:mm")) ), shop.location ? tr( td({ class: "tribe-info-label" }, i18n.shopLocation), - td({ class: "tribe-info-value", colspan: "3" }, ...renderUrl(shop.location)) + td({ class: "tribe-info-value", colspan: "3" }, ...renderStyledText(shop.location)) ) : null, tr( td({ class: "tribe-info-label" }, i18n.shopStatus || "STATUS"), @@ -352,37 +326,16 @@ exports.singleShopView = async (shop, filter, products = [], comments = [], para ), shop.url ? tr( td({ class: "tribe-info-label" }, i18n.shopUrl), - td({ class: "tribe-info-value", colspan: "3" }, ...renderUrl(shop.url)) - ) : null + td({ class: "tribe-info-value", colspan: "3" }, ...renderStyledText(shop.url)) + ) : null, + tr( + td({ class: "tribe-info-value", colspan: "4" }, userLink(shop.author)) + ) ), renderMapEmbed(params.mapData, shop.mapUrl), div({ class: "tribe-card-members" }, span({ class: "tribe-members-count" }, `${i18n.shopProducts}: ${shop.productCount || 0}`) ), - div({ class: "tribe-side-actions" }, - renderFavoriteToggle(shop, returnTo), - shop.author && String(shop.author) !== String(userId) - ? form({ method: "GET", action: "/pm" }, input({ type: "hidden", name: "recipients", value: shop.author }), button({ type: "submit", class: "tribe-action-btn" }, i18n.privateMessage)) - : null, - isAuthor - ? form({ method: "GET", action: `/shops/edit/${encodeURIComponent(shop.key)}` }, - button({ type: "submit", class: "tribe-action-btn" }, i18n.shopUpdate) - ) - : null, - isAuthor - ? form({ method: "GET", action: `/shops/${encodeURIComponent(shop.key)}/orders` }, - button({ type: "submit", class: "tribe-action-btn" }, - i18n.shopOrdersTitle || "Orders", - Number(shop.pendingOrders || 0) > 0 ? span({ class: "shop-orders-badge", title: i18n.shopOrdersPending || "Pending orders" }, ` ● ${shop.pendingOrders}`) : null - ) - ) - : null, - isAuthor - ? form({ method: "POST", action: `/shops/delete/${encodeURIComponent(shop.key)}` }, - button({ type: "submit", class: "tribe-action-btn danger-btn" }, i18n.shopDelete) - ) - : null - ), isAuthor ? div({ class: "tribe-side-actions shop-visibility-row" }, span({ class: "card-label" }, `${i18n.visibilityLabel || "Visibility"}: `), @@ -416,6 +369,34 @@ exports.singleShopView = async (shop, filter, products = [], comments = [], para shop.encrypted ? i18n.shopMakePublic : i18n.shopMakePrivate)) ) : null, + shop.subscription + ? renderSubscriptionBox({ + target: shop.rootId || shop.key, + scope: "shops", + subscribed: shop.subscription.subscribed === true, + count: shop.subscription.count, + isOwner: isAuthor, + canWrite: isAuthor, + returnTo + }) + : null, + isAuthor + ? div({ class: "tribe-side-actions" }, + form({ method: "GET", action: `/shops/${encodeURIComponent(shop.key)}/orders` }, + button({ type: "submit", class: "tribe-action-btn" }, + i18n.shopOrdersTitle || "Orders", + Number(shop.pendingOrders || 0) > 0 ? span({ class: "shop-orders-badge", title: i18n.shopOrdersPending || "Pending orders" }, ` ● ${shop.pendingOrders}`) : null + ) + ) + ) + : null, + isAuthor + ? div({ class: "tribe-side-actions owner-actions" }, + form({ method: "GET", action: `/shops/edit/${encodeURIComponent(shop.key)}` }, + button({ type: "submit", class: "tribe-action-btn" }, i18n.shopUpdate) + ) + ) + : null, safeArr(shop.tags).length ? div({ class: "tribe-side-tags" }, safeArr(shop.tags).map(tag => a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link" }, `#${tag}`))) : null @@ -429,14 +410,15 @@ exports.singleShopView = async (shop, filter, products = [], comments = [], para : null, div({ class: "shop-products-grid" }, products.length - ? products.map(prod => renderProductCard(prod, shop.rootId || shop.key, returnTo)) + ? paged(products).map(prod => renderProductCard(prod, shop.rootId || shop.key, `/shops/${encodeURIComponent(shop.key)}`, params)) : p(i18n.shopNoProducts) ) ) return template( shop.title || i18n.shopTitle, - section(renderModeButtons(filter)), + section(div({ class: "tags-header module-header-line" }, h2(i18n.shopsTitle), p(i18n.shopDescription), renderWalletChip())), + section(renderModeButtons(filter, false, (params && params.modesAvail) || null)), section( div({ class: "tribe-details" }, shopSide, @@ -451,89 +433,103 @@ exports.singleProductView = async (product, shop, comments = [], params = {}) => const isAuthor = String(product.author) === String(userId) const stock = Number(product.stock) || 0 - return template( - product.title || i18n.shopProductTitle, - section(renderModeButtons("products")), - section( - div({ class: "shop-detail" }, - div({ class: "bookmark-topbar transfer-topbar-single" }, - div({ class: "bookmark-topbar-left" }, - product.author && String(product.author) !== String(userId) - ? form({ method: "GET", action: "/pm" }, input({ type: "hidden", name: "recipients", value: product.author }), button({ type: "submit", class: "filter-btn" }, i18n.privateMessage)) - : null, - form({ method: "GET", action: `/shops/${encodeURIComponent(product.shopId)}` }, - button({ type: "submit", class: "filter-btn" }, `← ${i18n.shopBackToShop}`)) - ), - isAuthor - ? div({ class: "bookmark-actions transfer-actions" }, - form({ method: "GET", action: `/shops/product/edit/${encodeURIComponent(product.key)}` }, - input({ type: "hidden", name: "shopId", value: product.shopId }), - input({ type: "hidden", name: "returnTo", value: returnTo }), - button({ class: "update-btn", type: "submit" }, i18n.shopUpdate) - ), - form({ method: "POST", action: `/shops/product/delete/${encodeURIComponent(product.key)}` }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - button({ class: "delete-btn", type: "submit" }, i18n.shopDelete) - ) - ) - : null - ), - product.image ? div({ class: "shop-detail-media" }, renderMediaBlob(product.image)) : null, - h2(product.title), - renderStarRating(product.opinions, safeArr(product.opinions_inhabitants).length), - product.description ? div({ class: "shop-detail-desc" }, ...renderUrl(product.description)) : null, - div({ class: "shop-product-price" }, `${Number(product.price || 0).toFixed(6)} ECO`), - div({ class: "confirmations-block stock-block" }, - div({ class: "card-field" }, - span({ class: "card-label" }, `${i18n.shopProductStock}: `), - span({ class: "card-value" }, stock > 0 ? String(stock) : i18n.shopOutOfStock) - ), - progress({ class: "confirmations-progress stock-progress", value: Math.min(stock, 100), max: 100 }) - ), - !isAuthor && stock > 0 - ? form({ method: "POST", action: `/shops/product/buy/${encodeURIComponent(product.key)}`, class: "shop-buy-form" }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - p({ class: "shop-buy-form-note" }, i18n.shopBuyEncryptedNote || "Your delivery details are sent encrypted only to the shop owner."), - label(i18n.shopBuyDeliveryAddress || "Delivery address"), - br(), - textarea({ name: "deliveryAddress", required: true, rows: 3, placeholder: i18n.shopBuyDeliveryAddressPlaceholder || "" }), - br(), - br(), - label(i18n.shopBuyContact || "Contact"), - br(), - input({ type: "text", name: "contact", placeholder: i18n.shopBuyContactPlaceholder || "email, phone, etc." }), - br(), - br(), - label(i18n.shopBuyNotes || "Notes"), - br(), - textarea({ name: "notes", rows: 2, placeholder: i18n.shopBuyNotesPlaceholder || "" }), - br(), - br(), - button({ type: "submit", class: "buy-btn" }, i18n.marketActionsBuy || i18n.shopBuy) - ) - : null, - br(), - p({ class: "card-footer" }, - span({ class: "date-link" }, moment(product.createdAt).format("YYYY-MM-DD HH:mm")), - " ", - userLink(product.author) - ), - !isAuthor && params.canRate - ? renderOpinionsVoting('/shops/product/opinions', product.key, product.opinions, returnTo, product.opinions_inhabitants) - : null + const productSide = div({ class: "tribe-side" }, + div({ class: "card-header activity-card-header" }, + renderContentActions(product.key, `/shops/product/${encodeURIComponent(product.key)}`, { spread: params.spreads || null, author: product.author, favKind: 'shopProducts', isFavorite: product.isFavorite, returnTo, reportTitle: product.title, deleteAction: isAuthor ? contentDeleteAction('shopProduct', product.key) : undefined }) + ), + div({ class: "shop-title-row" }, + h2({ class: "tribe-card-title" }, product.title || i18n.shopProductUntitled) + ), + renderStarRating(product.opinions, safeArr(product.opinions_inhabitants).length), + product.image ? renderMediaBlob(product.image, null, { class: "tribe-detail-image" }) : null, + div({ class: "price-chip" }, `${Number(product.price || 0).toFixed(6)} ECO`), + div({ class: "confirmations-block stock-block" }, + div({ class: "card-field" }, + span({ class: "card-label" }, `${i18n.shopProductStock}: `), + span({ class: "card-value" }, stock > 0 ? String(stock) : i18n.shopOutOfStock) ), + progress({ class: "confirmations-progress stock-progress", value: Math.min(stock, 100), max: 100 }) + ), + table({ class: "tribe-info-table jobs-info-table" }, + tr( + td({ class: "tribe-info-label" }, i18n.shopCreatedAt || "CREATED"), + td({ class: "tribe-info-value", colspan: "3" }, moment(product.createdAt).format("YYYY/MM/DD HH:mm")) + ), + tr( + td({ class: "tribe-info-value", colspan: "4" }, userLink(product.author)) + ) + ), + div({ class: "tribe-side-actions" }, + form({ method: "GET", action: `/shops/${encodeURIComponent(product.shopId)}` }, + button({ type: "submit", class: "tribe-action-btn" }, `← ${i18n.shopBackToShop}`) + ) + ), + isAuthor + ? div({ class: "tribe-side-actions owner-actions" }, + form({ method: "GET", action: `/shops/product/edit/${encodeURIComponent(product.key)}` }, + input({ type: "hidden", name: "shopId", value: product.shopId }), + input({ type: "hidden", name: "returnTo", value: returnTo }), + button({ class: "tribe-action-btn", type: "submit" }, i18n.shopUpdate) + ) + ) + : null + ) + + const productMain = div({ class: "tribe-main" }, + product.description + ? div({ class: "job-section" }, + h2({ class: "job-section-title" }, i18n.marketItemDescription || i18n.shopDescription), + p({ class: "tribe-side-description" }, ...renderStyledText(product.description)) + ) + : null, + !isAuthor && stock > 0 + ? div({ class: "job-section" }, + form({ method: "POST", action: `/shops/product/buy/${encodeURIComponent(product.key)}`, class: "shop-buy-form" }, + input({ type: "hidden", name: "returnTo", value: returnTo }), + p({ class: "shop-buy-form-note" }, i18n.shopBuyEncryptedNote || "Your delivery details are sent encrypted only to the shop owner."), + label(i18n.shopBuyDeliveryAddress || "Delivery address"), + br(), + textarea({ maxlength: "5000", name: "deliveryAddress", required: true, rows: 3, placeholder: i18n.shopBuyDeliveryAddressPlaceholder || "" }), + br(), + br(), + label(i18n.shopBuyContact || "Contact"), + br(), + input({ type: "text", name: "contact", placeholder: i18n.shopBuyContactPlaceholder || "email, phone, etc." }), + br(), + br(), + label(i18n.shopBuyNotes || "Notes"), + br(), + textarea({ maxlength: "5000", name: "notes", rows: 2, placeholder: i18n.shopBuyNotesPlaceholder || "" }), + br(), + br(), + button({ type: "submit", class: "buy-btn" }, i18n.marketActionsBuy || i18n.shopBuy) + ) + ) + : null, + renderEngagement(product.key, + !isAuthor && params.canRate + ? renderOpinionsVoting('/shops/product/opinions', product.key, product.opinions, returnTo, product.opinions_inhabitants) + : null, renderCommentsSection(product.key, returnTo, comments) ) ) + + return template( + product.title || i18n.shopProductTitle, + section(div({ class: "tags-header module-header-line" }, h2(i18n.shopsTitle), p(i18n.shopDescription), renderWalletChip())), + section(renderModeButtons("products", false, (params && params.modesAvail) || null)), + section(div({ class: "tribe-details" }, productSide, productMain)) + ) } + exports.editProductView = async (product, shopId, params = {}) => { const returnTo = safeText(params.returnTo) || `/shops/${encodeURIComponent(shopId)}` return template( i18n.shopProductUpdate, section( div({ class: "tags-header" }, h2(i18n.shopProductUpdate)), - renderProductForm(shopId, product, true, returnTo) + renderProductForm(shopId, product, true, returnTo, await renderSpreadEditWarning(product && (product.key || product.id))) ) ) } @@ -576,9 +572,10 @@ exports.shopOrdersView = async (shop, orders) => { acts.push(statusForm(o.id, "SHIPPED", i18n.shopOrderMarkShipped || "Mark shipped")) } acts.push(a({ href: `/pm?recipients=${encodeURIComponent(o.buyer)}`, class: "tribe-action-btn" }, i18n.shopOrderPmBuyer || i18n.privateMessage || "PM")) + { const cb = renderCallButton(o.buyer, { cls: "tribe-action-btn", pam: true }); if (cb) acts.push(cb); } return div({ class: "tribe-side-actions" }, ...acts) } - const rows = (orders || []).map(o => div({ class: "shop-order-card card-section" }, + const rows = paged(orders || []).map(o => div({ class: "shop-order-card card-section" }, div({ class: "card-chips-row" }, renderEncryptedChip(i18n), orderStatusChip(o.status)), div({ class: "card-field" }, span({ class: "card-label" }, `${i18n.shopOrderProduct || "Product"}:`), span({ class: "card-value" }, String(o.title || o.productId || ""))), div({ class: "card-field" }, span({ class: "card-label" }, `${i18n.shopOrderPrice || "Price"}:`), span({ class: "card-value" }, `${Number(o.price || 0).toFixed(6)} ECO`)), @@ -586,13 +583,13 @@ exports.shopOrdersView = async (shop, orders) => { div({ class: "card-field" }, span({ class: "card-label" }, `${i18n.shopBuyDeliveryAddress || "Delivery address"}:`), span({ class: "card-value" }, String(o.deliveryAddress || ""))), o.contact ? div({ class: "card-field" }, span({ class: "card-label" }, `${i18n.shopBuyContact || "Contact"}:`), span({ class: "card-value" }, String(o.contact))) : null, o.notes ? div({ class: "card-field" }, span({ class: "card-label" }, `${i18n.shopBuyNotes || "Notes"}:`), span({ class: "card-value" }, String(o.notes))) : null, - p({ class: "card-footer" }, span({ class: "date-link" }, moment(o.createdAt || o.ts).format("YYYY-MM-DD HH:mm"))), + p({ class: "card-footer" }, span({ class: "date-link" }, moment(o.createdAt || o.ts).format("YYYY/MM/DD HH:mm"))), orderActions(o) )) return template( title, section( - div({ class: "tags-header" }, h2(title), p(i18n.shopOrdersDescription || "Encrypted purchase orders received by this shop.")), + div({ class: "tags-header module-header-line" }, h2(title), p(i18n.shopOrdersDescription || "Encrypted purchase orders received by this shop.")), a({ href: `/shops/${encodeURIComponent(shop.key || shop.id || "")}`, class: "filter-btn" }, i18n.goBack || "Go back") ), section( @@ -601,11 +598,12 @@ exports.shopOrdersView = async (shop, orders) => { ) } -exports.myPurchasesView = async (purchases) => { - const title = i18n.shopMyOrdersTitle || "My orders" +exports.myPurchasesView = async (purchases, params = {}) => { + const title = i18n.shopsTitle const buyerActions = (o) => { const acts = [] if (o.seller) acts.push(a({ href: `/pm?recipients=${encodeURIComponent(o.seller)}`, class: "tribe-action-btn" }, i18n.shopOrderPmSeller || i18n.privateMessage || "PM")) + if (o.seller) { const cb = renderCallButton(o.seller, { cls: "tribe-action-btn", pam: true }); if (cb) acts.push(cb); } if (String(o.status || "PENDING").toUpperCase() === "SHIPPED") { acts.push(form({ method: "POST", action: `/shops/orders/${encodeURIComponent(o.id)}/status`, class: "inline-form" }, input({ type: "hidden", name: "status", value: "RECEIVED" }), @@ -615,7 +613,7 @@ exports.myPurchasesView = async (purchases) => { } return acts.length ? div({ class: "tribe-side-actions" }, ...acts) : null } - const rows = (purchases || []).map(o => div({ class: "shop-order-card card-section" }, + const rows = paged(purchases || []).map(o => div({ class: "shop-order-card card-section" }, div({ class: "card-chips-row" }, renderEncryptedChip(i18n), orderStatusChip(o.status)), div({ class: "card-field" }, span({ class: "card-label" }, `${i18n.shopOrderProduct || "Product"}:`), span({ class: "card-value" }, String(o.title || o.productId || ""))), div({ class: "card-field" }, span({ class: "card-label" }, `${i18n.shopOrderPrice || "Price"}:`), span({ class: "card-value" }, `${Number(o.price || 0).toFixed(6)} ECO`)), @@ -623,17 +621,17 @@ exports.myPurchasesView = async (purchases) => { div({ class: "card-field" }, span({ class: "card-label" }, `${i18n.shopBuyDeliveryAddress || "Delivery address"}:`), span({ class: "card-value" }, String(o.deliveryAddress || ""))), o.contact ? div({ class: "card-field" }, span({ class: "card-label" }, `${i18n.shopBuyContact || "Contact"}:`), span({ class: "card-value" }, String(o.contact))) : null, o.notes ? div({ class: "card-field" }, span({ class: "card-label" }, `${i18n.shopBuyNotes || "Notes"}:`), span({ class: "card-value" }, String(o.notes))) : null, - p({ class: "card-footer" }, span({ class: "date-link" }, moment(o.createdAt || o.ts).format("YYYY-MM-DD HH:mm"))), + p({ class: "card-footer" }, span({ class: "date-link" }, moment(o.createdAt || o.ts).format("YYYY/MM/DD HH:mm"))), buyerActions(o) )) return template( title, section( - div({ class: "tags-header" }, h2(title), p(i18n.shopMyOrdersDescription || "Your encrypted purchase orders.")) + div({ class: "tags-header module-header-line" }, h2(title), p(i18n.shopMyOrdersDescription || "Your encrypted purchase orders.")) ), - section(renderModeButtons("purchases")), + section(renderModeButtons("purchases", false, params.modesAvail || null)), section( - rows.length ? div({ class: "shop-orders-list" }, ...rows) : p(i18n.shopMyOrdersEmpty || "You have no purchases yet.") + rows.length ? div({ class: "shop-orders-list" }, ...rows) : div({ class: "tribe-grid" }, p(i18n.shopMyOrdersEmpty || "You have no purchases yet.")) ) ) } @@ -645,17 +643,17 @@ exports.clearnetShopView = async (shop, products = []) => { }; const productCards = (products || []).filter(p => Number(p.stock) > 0 || p.featured).map(prod => { const pImg = cnBlobUrl(prod.image); + const pKind = /^\[video:/.test(String(prod.image || '')) ? 'video' : /^\[audio:/.test(String(prod.image || '')) ? 'audio' : 'img'; return `
- ${pImg ? `` : ''} + ${pImg ? (pKind === 'img' ? `` : `<${pKind} class="cn-product-img" src="${pImg}" controls preload="metadata">`) : ''}

${cnEscapeHtml(prod.title || '')}

- ${prod.description ? `

${cnEscapeHtml(prod.description)}

` : ''} + ${prod.description ? `

${cnRichText(prod.description)}

` : ''}

${fmtPrice(prod.price)} ECO

- ${Number(prod.stock) > 0 ? `

Stock: ${prod.stock}

` : ''}
`; }).join('\n'); const shopBlobUrl = cnBlobUrl(shop.image); const shopImg = shopBlobUrl ? `${cnEscapeHtml(shop.title || '')}` : ''; - const desc = cnEscapeHtml(shop.shortDescription || shop.description || ''); + const desc = cnRichText(shop.shortDescription || shop.description || ''); const extraCss = ` .cn-hero{display:flex;gap:24px;margin-bottom:24px;flex-wrap:wrap;align-items:flex-start} .cn-shop-img{display:block;max-width:280px;width:100%;border:3px solid var(--fg);border-radius:8px;background:#000} @@ -681,16 +679,17 @@ exports.clearnetShopView = async (shop, products = []) => {

${cnEscapeHtml(shop.title || '')}

${desc ? `

${desc}

` : ''}
+ ${cnKindTag('shop')} ${shop.createdAt ? `📅 ${new Date(shop.createdAt).toISOString().slice(0,10)}` : ''} - ${shop.location ? `📍 ${cnEscapeHtml(shop.location)}` : ''}
-

Products

- ${productCards ? `
${productCards}
` : '
No products available.
'} +
+

${cnEscapeHtml(i18n.shopProducts)}

+ ${productCards ? `
${productCards}
` : `
${cnEscapeHtml(i18n.shopNoProducts)}
`} `; return renderClearnetPage({ - title: `${shop.title || 'Shop'} — Oasis`, + title: `${shop.title || 'Shop'} | Oasis`, ogTitle: shop.title || 'Oasis', ogDescription: shop.shortDescription || shop.description || '', ogImage: shopBlobUrl, diff --git a/src/views/stats_view.js b/src/views/stats_view.js index a2cd700..337b0ce 100644 --- a/src/views/stats_view.js +++ b/src/views/stats_view.js @@ -1,5 +1,6 @@ const { div, h2, p, section, button, form, input, ul, li, a, h3, span, strong, table, thead, tbody, tr, td, th } = require("../server/node_modules/hyperaxe"); const { template, i18n, userLink } = require('./main_views'); +const sharedState = require('../configs/shared-state'); Object.assign(i18n, { statsChat: "Chats", @@ -36,10 +37,11 @@ exports.statsView = (stats, filter) => { const description = i18n.statsDescription; const modes = ['ALL', 'MINE', 'TOMBSTONE']; const types = [ - 'bookmark', 'event', 'task', 'votes', 'report', 'feed', 'project', - 'image', 'torrent', 'audio', 'video', 'document', 'transfer', 'post', 'tribe', + 'bookmark', 'event', 'task', 'votes', 'report', 'feed', 'project', 'industry', 'industryBlueprint', + 'image', 'torrent', 'file', 'audio', 'video', 'document', 'transfer', 'post', 'tribe', 'market', 'forum', 'job', 'aiExchange', 'map', 'shop', 'shopProduct', - 'chat', 'chatMessage', 'pad', 'padEntry', 'gameScore', 'calendar', 'calendarDate', 'calendarNote', + 'chat', 'chatMessage', 'pad', 'padEntry', 'room', 'roomMember', 'wikiPage', 'emergency', 'emergencyConfirm', 'emergencyUpdate', 'mailingList', 'logisticsRoute', 'logisticsRating', 'podcast', 'podcastEpisode', 'podcastPlay', 'campaign', 'campaignSignature', 'campaignUpdate', 'gameScore', 'calendar', 'calendarDate', 'calendarNote', + 'schoolCourse', 'schoolLesson', 'schoolEnroll', 'schoolCertificate', 'parliamentCandidature','parliamentTerm','parliamentProposal','parliamentRevocation','parliamentLaw', 'courtsCase','courtsEvidence','courtsAnswer','courtsVerdict','courtsSettlement','courtsSettlementProposal','courtsSettlementAccepted','courtsNomination','courtsNominationVote' ]; @@ -51,8 +53,11 @@ exports.statsView = (stats, filter) => { report: i18n.statsReport, feed: i18n.statsFeed, project: i18n.statsProject, + industry: i18n.industryTitle, + industryBlueprint: i18n.industryBlueprints, image: i18n.statsImage, torrent: i18n.statsTorrent, + file: i18n.statsFile, audio: i18n.statsAudio, video: i18n.statsVideo, document: i18n.statsDocument, @@ -70,7 +75,26 @@ exports.statsView = (stats, filter) => { chatMessage: i18n.statsChatMessage, pad: i18n.statsPad, padEntry: i18n.statsPadEntry, + room: i18n.statsRoom, + roomMember: i18n.statsRoomMember, + wikiPage: i18n.statsWiki, + emergency: i18n.statsEmergency, + emergencyConfirm: i18n.statsEmergencyConfirm, + emergencyUpdate: i18n.statsEmergencyUpdate, + mailingList: i18n.statsMailingList, + logisticsRoute: i18n.statsLogisticsRoute, + logisticsRating: i18n.statsLogisticsRating, + podcast: i18n.statsPodcast, + podcastEpisode: i18n.statsPodcastEpisode, + podcastPlay: i18n.statsPodcastPlay, + campaign: i18n.statsCampaign, + campaignSignature: i18n.statsCampaignSignature, + campaignUpdate: i18n.statsCampaignUpdate, gameScore: i18n.statsGameScore, + schoolCourse: i18n.statsSchoolCourse, + schoolLesson: i18n.statsSchoolLesson, + schoolEnroll: i18n.statsSchoolEnroll, + schoolCertificate: i18n.statsSchoolCertificate, calendar: i18n.statsCalendar, calendarDate: i18n.statsCalendarDate, calendarNote: i18n.statsCalendarNote, @@ -169,25 +193,26 @@ exports.statsView = (stats, filter) => { const networkCO2 = parseFloat((totalMB * kWhPerMB * gCO2PerKWh).toFixed(2)); const inhabitants = stats.usersKPIs?.totalInhabitants || stats.inhabitants || 1; const userCO2 = parseFloat((networkCO2 / Math.max(1, inhabitants)).toFixed(2)); - const maxAnnualCO2 = 500; + const gCO2PerInhabitantYear = 50; + const maxAnnualCO2 = Math.max(500, Math.max(1, inhabitants) * gCO2PerInhabitantYear); if (filter === 'MINE') { const pct = networkCO2 > 0 ? Math.min(100, (userCO2 / networkCO2) * 100) : 0; return div({ class: 'carbon-chart' }, div({ class: 'carbon-bar-label' }, - span(i18n.statsCarbonUser || 'Your footprint'), - span(`${userCO2} g CO₂`) - ), - div({ class: 'carbon-bar-track' }, - div({ class: `carbon-bar-fill carbon-bar-mine ${wClass(pct)}` }) - ), - div({ class: 'carbon-bar-label' }, - span(i18n.statsCarbonNetwork || 'Network total'), + span(`HcT — ${i18n.statsCarbonNetwork || 'Network total'}`), span(`${networkCO2} g CO₂`) ), div({ class: 'carbon-bar-track' }, div({ class: 'carbon-bar-fill carbon-bar-network stats-w-100' }) ), + div({ class: 'carbon-bar-label' }, + span(`HcH — ${i18n.statsCarbonUser || 'Your footprint'}`), + span(`${userCO2} g CO₂`) + ), + div({ class: 'carbon-bar-track' }, + div({ class: `carbon-bar-fill carbon-bar-mine ${wClass(pct)}` }) + ), p({ class: 'carbon-bar-note' }, strong(`${pct.toFixed(1)}%`), ` ${i18n.statsCarbonOfNetwork || 'of network total'}`), p({ class: 'carbon-bar-formula' }, 'Based on local data storage weight ', strong('(0.0002 kWh/MB × 475 g CO₂/kWh)')) ); @@ -199,13 +224,6 @@ exports.statsView = (stats, filter) => { const tombCO2 = parseFloat((tombMB * kWhPerMB * gCO2PerKWh).toFixed(4)); const tombPct = networkCO2 > 0 ? Math.min(100, (tombCO2 / networkCO2) * 100) : 0; return div({ class: 'carbon-chart' }, - div({ class: 'carbon-bar-label' }, - span(i18n.statsCarbonTombstone || 'Tombstoning footprint'), - span(`${tombCO2} g CO₂`) - ), - div({ class: 'carbon-bar-track' }, - div({ class: `carbon-bar-fill carbon-bar-mine ${wClass(tombPct)}` }) - ), div({ class: 'carbon-bar-label' }, span(i18n.statsCarbonNetwork || 'Network total'), span(`${networkCO2} g CO₂`) @@ -213,62 +231,59 @@ exports.statsView = (stats, filter) => { div({ class: 'carbon-bar-track' }, div({ class: 'carbon-bar-fill carbon-bar-network stats-w-100' }) ), + div({ class: 'carbon-bar-label' }, + span(i18n.statsCarbonTombstone || 'Tombstoning footprint'), + span(`${tombCO2} g CO₂`) + ), + div({ class: 'carbon-bar-track' }, + div({ class: `carbon-bar-fill carbon-bar-mine ${wClass(tombPct)}` }) + ), p({ class: 'carbon-bar-note' }, strong(`${tombPct.toFixed(1)}%`), ` ${i18n.statsCarbonOfNetwork || 'of network total'} (${tombCount} tombstones × ~${avgTombBytes} bytes)`), p({ class: 'carbon-bar-formula' }, 'Based on estimated tombstone message size ', strong('(0.0002 kWh/MB × 475 g CO₂/kWh)')) ); } const pct = Math.min(100, (networkCO2 / maxAnnualCO2) * 100); + const now = new Date(); + const yearStart = new Date(now.getFullYear(), 0, 1).getTime(); + const yearEnd = new Date(now.getFullYear() + 1, 0, 1).getTime(); + const yearPct = Math.min(100, ((now.getTime() - yearStart) / (yearEnd - yearStart)) * 100); return div({ class: 'carbon-chart' }, - div({ class: 'carbon-bar-label' }, - span(i18n.statsCarbonNetwork || 'Network footprint'), - span(`${networkCO2} g CO₂`) - ), - div({ class: 'carbon-bar-track' }, - div({ class: `carbon-bar-fill carbon-bar-network ${wClass(pct)}` }) - ), div({ class: 'carbon-bar-label' }, span(i18n.statsCarbonMaxAnnual || 'Annual max estimate'), span(`${maxAnnualCO2} g CO₂`) ), div({ class: 'carbon-bar-track' }, - div({ class: 'carbon-bar-fill carbon-bar-max stats-w-100' }) + div({ class: `carbon-bar-fill carbon-bar-max ${wClass(yearPct)}` }) ), - p({ class: 'carbon-bar-note' }, strong(`${pct.toFixed(1)}%`), ` ${i18n.statsCarbonOfEstMax || 'of estimated max capacity'}`), - p({ class: 'carbon-bar-formula' }, 'Based on local data storage weight ', strong('(0.0002 kWh/MB × 475 g CO₂/kWh)')) + div({ class: 'carbon-bar-label' }, + span(`HcT — ${i18n.statsCarbonNetwork || 'Network footprint'}`), + span(`${networkCO2} g CO₂`) + ), + div({ class: 'carbon-bar-track' }, + div({ class: `carbon-bar-fill carbon-bar-network ${wClass(pct)}` }) + ), + p({ class: 'carbon-bar-note' }, strong(`${pct.toFixed(1)}%`), ` ${i18n.statsCarbonOfEstMax || 'of estimated max capacity'} · `, strong(`${yearPct.toFixed(1)}%`), ` ${i18n.statsCarbonYearProgress || 'of the year elapsed'}`), + p({ class: 'carbon-bar-formula' }, 'Based on local data storage weight ', strong('(0.0002 kWh/MB × 475 g CO₂/kWh)'), ` · max: ${gCO2PerInhabitantYear} g CO₂ × ${Math.max(1, inhabitants)} inhabitants`) ); })(); - const headerCard = div({ class: 'stats-card' }, + const storageCard = div({ class: 'stats-card' }, + h3({ class: 'stats-section-h' }, i18n.statsStorageTitle || 'Storage'), table({ class: 'block-info-table' }, - tr(td({ class: 'card-label' }, i18n.statsCreatedAt), td({ class: 'card-value' }, stats.createdAt)), - tr(td({ class: 'card-label' }, 'ID'), td({ class: 'card-value' }, userLink(stats.id))), tr(td({ class: 'card-label' }, i18n.statsBlobsSize), td({ class: 'card-value' }, stats.statsBlobsSize)), tr(td({ class: 'card-label' }, i18n.statsBlockchainSize), td({ class: 'card-value' }, stats.statsBlockchainSize)), tr(td({ class: 'card-label' }, i18n.statsSize), td({ class: 'card-value' }, stats.folderSize)) ) ); - const totalInhabitants = stats.usersKPIs?.totalInhabitants || stats.inhabitants || 0; - const networkKPIs = stats.networkKPIs || {}; - - const topStrip = div({ class: 'stats-block' }, - kpiGrid( - kpi(i18n.bankingUserEngagementScore, C(stats, 'karmaScore')), - kpi(i18n.statsUsersTitle, totalInhabitants), - kpi(i18n.statsTotalMsgs || 'Total messages', networkKPIs.totalMsgs || 0), - kpi(i18n.statsLogsTitle || 'Logs', stats?.logsCount || 0), - kpi(i18n.statsAITraining, C(stats, 'aiExchange') || 0), - kpi(i18n.statsPUBs, stats.pubsCount || 0) - ) - ); - - const carbonCard = div({ class: 'stats-card' }, - h3({ class: 'stats-section-h' }, i18n.statsCarbonFootprintTitle || 'Carbon Footprint'), - carbonChart - ); - - const bankingCard = div({ class: 'stats-card' }, + const accountCard = div({ class: 'stats-card' }, + h3({ class: 'stats-section-h' }, i18n.statsAccountTitle || 'Account'), table({ class: 'block-info-table' }, + tr(td({ class: 'card-label' }, 'ID'), td({ class: 'card-value' }, userLink(stats.id))), + tr(td({ class: 'card-label' }, i18n.statsCreatedAt), td({ class: 'card-value' }, stats.createdAt)), + C(stats, 'karmaScore') > 0 + ? tr(td({ class: 'card-label' }, i18n.bankingUserEngagementScore), td({ class: 'card-value' }, String(C(stats, 'karmaScore')))) + : null, tr(td({ class: 'card-label' }, i18n.statsEcoWalletLabel), td({ class: 'card-value' }, a({ href: '/wallet', class: 'stats-link-break' }, stats?.banking?.myAddress || i18n.statsEcoWalletNotConfigured))), tr(td({ class: 'card-label' }, i18n.profileGpgChip || 'GPG'), td({ class: 'card-value' }, stats?.gpgFingerprint @@ -278,23 +293,67 @@ exports.statsView = (stats, filter) => { ) ); - const networkBlock = div({ class: 'stats-block' }, - kpiGrid( - filter === 'MINE' - ? kpi(i18n.statsMyShare || 'Your share of the network', `${fmtNum(networkKPIs.myShare || 0)}%`) - : null, - kpi(i18n.statsAvgPerInhabitant || 'Avg per inhabitant', fmtNum(networkKPIs.avgMsgsPerInhabitant || 0)), - kpi(i18n.statsMsgsPerDay || 'Messages/day (lifetime)', fmtNum(networkKPIs.networkMsgsPerDay || 0)), - kpi(i18n.statsNetworkSpan || 'Network span', `${fmtNum(networkKPIs.networkSpanDays || 0)} d`), - kpi(i18n.statsTombstoneRatioLabel || 'Tombstone ratio', `${fmtNum(stats.tombstoneKPIs?.ratio || 0)}%`) - ) + const totalInhabitants = stats.usersKPIs?.totalInhabitants || stats.inhabitants || 0; + const networkKPIs = stats.networkKPIs || {}; + + const kvTable = (rows) => { + const shown = rows.filter(([, v]) => !isZero(v)); + if (!shown.length) return p({ class: 'no-content' }, i18n.no_results || 'No data'); + return table({ class: 'tag-table' }, + thead(tr( + th(i18n.statsContentTypeColumn || 'Type'), + th(i18n.statsContentCountColumn || 'Count') + )), + tbody(...shown.map(([label, value]) => tr(td(label), td(String(value))))) + ); + }; + + const blockWithTiles = (cls, title, tiles) => { + const shown = (Array.isArray(tiles) ? tiles : []).filter(Boolean); + if (!shown.length) return null; + return div({ class: cls }, h3({ class: 'stats-section-h' }, title), kpiGrid(...shown)); + }; + + const networkRows = [ + [i18n.statsUsersTitle, totalInhabitants], + [i18n.statsTotalMsgs || 'Total messages', networkKPIs.totalMsgs || 0], + [i18n.statsLogsTitle || 'Logs', stats?.logsCount || 0], + [i18n.statsAITraining, C(stats, 'aiExchange') || 0], + [i18n.statsPUBs, stats.pubsCount || 0], + [i18n.statsSyncedPeers, sharedState.getSyncedPeerCount() || 0] + ]; + const networkStrip = networkRows.some(([, v]) => !isZero(v)) + ? div({ class: 'stats-block' }, + h3({ class: 'stats-section-h' }, i18n.statsNetworkTitle || 'Network'), + kvTable(networkRows) + ) + : null; + + const carbonCard = div({ id: 'carbon', class: 'stats-card' }, + h3({ class: 'stats-section-h' }, i18n.statsCarbonFootprintTitle || 'Carbon Footprint'), + carbonChart ); + const networkBlock = blockWithTiles('stats-block stats-block-boxed', i18n.statsAveragesTitle || 'Averages', [ + filter === 'MINE' + ? kpi(i18n.statsMyShare || 'Your share of the network', `${fmtNum(networkKPIs.myShare || 0)}%`) + : null, + kpi(i18n.statsAvgPerInhabitant || 'Avg per inhabitant', fmtNum(networkKPIs.avgMsgsPerInhabitant || 0)), + kpi(i18n.statsMsgsPerDay || 'Messages/day (lifetime)', fmtNum(networkKPIs.networkMsgsPerDay || 0)), + kpi(i18n.statsNetworkSpan || 'Network span', `${fmtNum(networkKPIs.networkSpanDays || 0)} d`), + kpi(i18n.statsTombstoneRatioLabel || 'Tombstone ratio', `${fmtNum(stats.tombstoneKPIs?.ratio || 0)}%`) + ]); + const activityBlock = (() => { const rows = Array.isArray(stats.activity?.daily7) ? stats.activity.daily7 : []; const max = Math.max(1, ...rows.map(r => Number(r.count) || 0)); + if (!rows.length && isZero(stats.activity?.daily7Total || 0) && isZero(stats.activity?.daily30Total || 0)) return null; return div({ class: 'stats-block' }, h2(i18n.statsActivity7d), + kpiGrid( + kpi(i18n.statsActivity7dTotal, stats.activity?.daily7Total || 0), + kpi(i18n.statsActivity30dTotal, stats.activity?.daily30Total || 0) + ), rows.length ? ul({ class: 'stats-toplist' }, ...rows.map(row => { @@ -309,11 +368,7 @@ exports.statsView = (stats, filter) => { ); }) ) - : p({ class: 'no-content' }, i18n.no_results || 'No data'), - div({ class: 'stats-activity-totals' }, - span(`${i18n.statsActivity7dTotal}: `, strong(String(stats.activity?.daily7Total || 0))), - span(`${i18n.statsActivity30dTotal}: `, strong(String(stats.activity?.daily30Total || 0))) - ) + : p({ class: 'no-content' }, i18n.no_results || 'No data') ); })(); @@ -389,8 +444,17 @@ exports.statsView = (stats, filter) => { ); }; - const buildOpinionTiles = () => - types.map(t => O(stats, t) > 0 ? kpi(labels[t], O(stats, t)) : null).filter(Boolean); + const buildOpinionTable = () => { + const rows = types.map(t => [labels[t], O(stats, t)]).filter(([, n]) => n > 0).sort((a, b) => b[1] - a[1]); + if (!rows.length) return p({ class: 'no-content' }, i18n.no_results || 'No data'); + return table({ class: 'tag-table' }, + thead(tr( + th(i18n.statsContentTypeColumn || 'Type'), + th(i18n.statsContentCountColumn || 'Count') + )), + tbody(...rows.map(([label, count]) => tr(td(label), td(String(count))))) + ); + }; const tribeListBlock = (label, list) => div({ class: 'stats-block' }, h2(`${label}: ${list.length}`), @@ -403,16 +467,15 @@ exports.statsView = (stats, filter) => { const allMode = filter === 'ALL' ? div({ class: 'stats-container' }, [ - networkBlock, activityBlock, totalOpinions > 0 - ? div({ class: 'stats-block' }, + ? div({ class: 'stats-block stats-block-boxed' }, h2(`${i18n.statsNetworkOpinions}: ${totalOpinions}`), - kpiGrid(...buildOpinionTiles()) + buildOpinionTable() ) : null, totalContent > 0 - ? div({ class: 'stats-block' }, + ? div({ class: 'stats-block stats-block-boxed' }, h2(`${i18n.statsNetworkContent}: ${totalContent}`), buildContentTable() ) @@ -422,16 +485,15 @@ exports.statsView = (stats, filter) => { const mineMode = filter === 'MINE' ? div({ class: 'stats-container' }, [ - networkBlock, activityBlock, totalOpinions > 0 - ? div({ class: 'stats-block' }, + ? div({ class: 'stats-block stats-block-boxed' }, h2(`${i18n.statsYourOpinions}: ${totalOpinions}`), - kpiGrid(...buildOpinionTiles()) + buildOpinionTable() ) : null, totalContent > 0 - ? div({ class: 'stats-block' }, + ? div({ class: 'stats-block stats-block-boxed' }, h2(`${i18n.statsYourContent}: ${totalContent}`), buildContentTable() ) @@ -444,26 +506,23 @@ exports.statsView = (stats, filter) => { const userTomb = Number(stats.userTombstoneCount || 0); const netTomb = Number(stats.tombstoneKPIs?.networkTombstoneCount || 0); if (userTomb === 0 && netTomb === 0) { - return div({ class: 'stats-container' }, - div({ class: 'stats-block' }, - p({ class: 'no-content' }, i18n.statsTombstoneEmpty || 'No tombstones in the network yet.') - ) + return div({ class: 'stats-block' }, + p({ class: 'no-content' }, i18n.statsTombstoneEmpty || 'No tombstones in the network yet.') ); } - return div({ class: 'stats-container' }, [ - div({ class: 'stats-block' }, - kpiGrid( - kpi(i18n.TOMBSTONEButton, userTomb), - kpi(i18n.statsTombstoneRatio, `${(stats.tombstoneKPIs?.ratio || 0).toFixed(2)}%`) - ) + return div({ class: 'stats-block stats-block-boxed' }, + kpiGrid( + kpi(i18n.TOMBSTONEButton, userTomb), + kpi(i18n.statsTombstoneRatio, `${(stats.tombstoneKPIs?.ratio || 0).toFixed(2)}%`) ) - ]); + ); })() : null; const ets = stats.ecoTaxStats || null; const userTax = Number(stats.userEcoinTax || 0); const ecoTaxBlock = ets ? div({ class: 'stats-card' }, + h3({ class: 'stats-section-h' }, i18n.statsEcoTaxTitle || 'ECO Tax'), table({ class: 'block-info-table' }, filter === 'MINE' ? tr(td({ class: 'card-label' }, i18n.bankTaxesUserAmount || 'Your ECO tax'), td({ class: 'card-value' }, `${userTax.toFixed(6)} ECO`)) @@ -479,7 +538,7 @@ exports.statsView = (stats, filter) => { return template( title, section( - div({ class: 'tags-header' }, + div({ class: 'tags-header module-header-line' }, h2(title), p(description) ), @@ -487,18 +546,20 @@ exports.statsView = (stats, filter) => { modes.map(m => form({ method: 'GET', action: '/stats' }, input({ type: 'hidden', name: 'filter', value: m }), - button({ type: 'submit', class: filter === m ? 'filter-btn active' : 'filter-btn' }, i18n[m + 'Button']) + button({ type: 'submit', class: filter === m ? 'filter-btn active' : 'filter-btn' }, String(i18n[m + 'Button']).toUpperCase()) ) ) ), section( - topStrip, - headerCard, + filter === 'MINE' ? accountCard : null, + filter === 'ALL' ? storageCard : null, + filter === 'ALL' ? networkStrip : null, + tombMode, + filter === 'TOMBSTONE' ? null : networkBlock, + filter === 'TOMBSTONE' && !(Number(stats.userTombstoneCount || 0) || Number(stats.tombstoneKPIs?.networkTombstoneCount || 0)) ? null : carbonCard, filter !== 'TOMBSTONE' ? ecoTaxBlock : null, - bankingCard, allMode, - mineMode, - tombMode + mineMode ) ) ); diff --git a/src/views/tags_view.js b/src/views/tags_view.js index 2976f5b..eb7069d 100644 --- a/src/views/tags_view.js +++ b/src/views/tags_view.js @@ -1,14 +1,12 @@ -const { form, button, div, h2, p, section, table, thead, tr, th, td, a, tbody } = require("../server/node_modules/hyperaxe"); -const { template, i18n } = require('./main_views'); +const { form, button, div, h2, p, section, table, thead, tr, th, td, a, tbody, input } = require("../server/node_modules/hyperaxe"); +const { template, i18n, renderModuleStats, paged } = require('./main_views'); const getFilteredTags = (filter, tags) => { - let filteredTags = Array.isArray(tags) ? tags : []; - if (filter === 'top') { - filteredTags = filteredTags.sort((a, b) => b.count - a.count || a.name.localeCompare(b.name)); - } else { - filteredTags = filteredTags.sort((a, b) => a.name.localeCompare(b.name)); - } - return filteredTags; + const filteredTags = Array.isArray(tags) ? [...tags] : []; + if (filter === 'top') return filteredTags.sort((a, b) => b.count - a.count || a.name.localeCompare(b.name)); + if (filter === 'mine') return filteredTags.sort((a, b) => (b.mine || 0) - (a.mine || 0) || a.name.localeCompare(b.name)); + if (filter === 'recent') return filteredTags.sort((a, b) => (b.lastTs || 0) - (a.lastTs || 0)); + return filteredTags.sort((a, b) => a.name.localeCompare(b.name)); }; const renderTagsTable = (filteredTags) => { @@ -33,56 +31,51 @@ const renderTagsTable = (filteredTags) => { const renderTagsCloud = (mergedTags) => { const maxCount = Math.max(...mergedTags.map(t => t.count), 1); return div({ class: 'tag-cloud-wrap' }, - mergedTags.map(tag => { - const angle = Math.random() * 2 * Math.PI; - const radius = 10 + Math.random() * 40; - const x = 50 + Math.cos(angle) * radius; - const y = 50 + Math.sin(angle) * radius; - const weight = tag.count / maxCount; - const fontSize = 12 + Math.round(weight * 32); - const hue = 200 + Math.round(weight * 120); - - return a({ - href: `/search?query=%23${encodeURIComponent(tag.name)}`, - class: 'tag-cloud-item', - style: ` - left: ${x}%; - top: ${y}%; - font-size: ${fontSize}px; - color: hsl(${hue},70%,60%); - ` - }, tag.name); - }) + mergedTags.map(tag => a({ + href: `/search?query=%23${encodeURIComponent(tag.name)}`, + class: 'tag-cloud-item', + 'data-weight': String(1 + Math.round((tag.count / maxCount) * 4)) + }, tag.name)) ); }; -exports.tagsView = async (tags, filter) => { +exports.tagsView = async (tags, filter, search = '') => { const filteredTags = getFilteredTags(filter, tags); + const query = String(search || '').trim(); - const title = - filter === 'top' ? i18n.tagsTopSectionTitle : - filter === 'cloud' ? i18n.tagsCloudSectionTitle : - i18n.tagsAllSectionTitle; + const title = i18n.tagsTitle; + const emptyTags = filteredTags.length === 0 && ['top', 'all'].includes(String(filter || 'top')) && !query; return template( title, section( - div({ class: 'tags-header' }, + div({ class: 'tags-header module-header-line' }, h2(i18n.tagsTitle), p(i18n.tagsDescription) ), - div({ class: 'filters' }, - form({ method: 'GET', action: '/tags' }, - button({ type: 'submit', name: 'filter', value: 'all', class: filter === 'all' ? 'filter-btn active' : 'filter-btn' }, i18n.tagsFilterAll), - button({ type: 'submit', name: 'filter', value: 'top', class: filter === 'top' ? 'filter-btn active' : 'filter-btn' }, i18n.tagsFilterTop), - button({ type: 'submit', name: 'filter', value: 'cloud', class: filter === 'cloud' ? 'filter-btn active' : 'filter-btn' }, i18n.tagsFilterCloud) + emptyTags ? null : div({ class: 'filters' }, + form({ method: 'GET', action: '/tags', class: 'ui-toolbar ui-toolbar--filters' }, + input({ type: 'hidden', name: 'search', value: query }), + button({ type: 'submit', name: 'filter', value: 'top', class: filter === 'top' ? 'filter-btn active' : 'filter-btn' }, String(i18n.tagsFilterTop).toUpperCase()), + button({ type: 'submit', name: 'filter', value: 'all', class: filter === 'all' ? 'filter-btn active' : 'filter-btn' }, String(i18n.tagsFilterAll).toUpperCase()), + button({ type: 'submit', name: 'filter', value: 'recent', class: filter === 'recent' ? 'filter-btn active' : 'filter-btn' }, String(i18n.tagsFilterRecent).toUpperCase()), + button({ type: 'submit', name: 'filter', value: 'mine', class: filter === 'mine' ? 'filter-btn active' : 'filter-btn' }, String(i18n.tagsFilterMine).toUpperCase()), + button({ type: 'submit', name: 'filter', value: 'cloud', class: filter === 'cloud' ? 'filter-btn active' : 'filter-btn' }, String(i18n.tagsFilterCloud).toUpperCase()) + ) + ), + emptyTags ? null : div({ class: 'tags-search activity-filter-chips activity-toolbar-row' }, + renderModuleStats(filteredTags.length), + form({ method: 'GET', action: '/tags', class: 'filter-box' }, + input({ type: 'hidden', name: 'filter', value: filter || 'top' }), + input({ type: 'text', name: 'search', value: query, placeholder: i18n.tagsSearchPlaceholder, class: 'filter-box__input' }), + div({ class: 'filter-box__controls' }, button({ type: 'submit', class: 'filter-box__button' }, i18n.searchButton)) ) ), div({ class: 'tags-list' }, filteredTags.length === 0 - ? p(i18n.tagsNoItems) + ? p(query ? i18n.noResultsFound : i18n.tagsNoItems) : filter !== 'cloud' - ? renderTagsTable(filteredTags) + ? renderTagsTable(paged(filteredTags)) : renderTagsCloud(filteredTags) ) ) diff --git a/src/views/task_view.js b/src/views/task_view.js index 40a2624..089e534 100644 --- a/src/views/task_view.js +++ b/src/views/task_view.js @@ -1,25 +1,28 @@ const { div, h2, p, section, button, form, input, select, option, a, br, textarea, label, span, table, tr, td, img, video } = require("../server/node_modules/hyperaxe"); +const { renderCommentsSection: renderSharedCommentsSection } = require("./comments_view"); const moment = require("../server/node_modules/moment"); -const { template, i18n, renderOpinionsVoting, userLink, renderStateChip, renderPrivacyChip, renderLifespanChip, renderEcoTax, renderSpreadButton } = require("./main_views"); +const { template, i18n, renderOpinionsVoting, renderEngagement, userLink, renderStateChip, renderPrivacyChip, renderLifespanChip, renderEcoTax, renderSpreadButton, renderSpreadEditWarning, renderContentActions, renderDocumentActions, renderModuleStatsBy, moduleIsEmpty, contentDeleteAction, paged, renderCreatedRows } = require("./main_views"); +const { renderPhotoGallery, renderGalleryFields, imagesOf, renderZoomableImage } = require("./gallery_view"); +const { renderTribeOriginChip } = require("./clearnet_view"); const { config } = require("../server/SSB_server.js"); -const { renderUrl } = require("../backend/renderUrl"); +const { renderStyledText } = require("../backend/renderStyledText"); const renderTaskMediaBlob = (value, attrs = {}) => { if (!value) return null; const s = String(value).trim(); if (!s) return null; - if (s.startsWith('&')) return img({ src: `/blob/${encodeURIComponent(s)}`, ...attrs }); + if (s.startsWith('&')) return renderZoomableImage(`/blob/${encodeURIComponent(s)}`, { imgClass: attrs.class || 'post-image', alt: attrs.alt || '' }); const mImg = s.match(/!\[[^\]]*\]\(\s*(&[^)\s]+\.sha256)\s*\)/); - if (mImg) return img({ src: `/blob/${encodeURIComponent(mImg[1])}`, class: attrs.class || 'post-image' }); + if (mImg) return renderZoomableImage(`/blob/${encodeURIComponent(mImg[1])}`, { imgClass: attrs.class || 'post-image', alt: attrs.alt || '' }); const mVideo = s.match(/\[video:[^\]]*\]\(\s*(&[^)\s]+\.sha256)\s*\)/); - if (mVideo) return video({ controls: true, class: attrs.class || 'post-video', src: `/blob/${encodeURIComponent(mVideo[1])}` }); + if (mVideo) return video({ controls: true, class: attrs.class || 'post-video', src: `/blob/${encodeURIComponent(mVideo[1])}`, preload: 'metadata' }); return null; }; const userId = config.keys.id; const opt = (value, isSelected, text) => - option(Object.assign({ value }, isSelected ? { selected: "selected" } : {}), text); + option(Object.assign({ value }, isSelected ? { ...("selected" ? { selected: true } : {})} : {}), text); const safeArray = (v) => Array.isArray(v) ? v : []; @@ -52,100 +55,50 @@ const renderTaskOwnerActions = (task, returnTo) => { input({ type: "hidden", name: "returnTo", value: returnTo }), button({ type: "submit", class: "update-btn" }, i18n.taskUpdateButton) ), + ]; +}; + +const renderTaskStatusRow = exports.renderTaskStatusRow = (task, returnTo, action = null) => { + const st = normalizeStatus(task.status || "OPEN"); + return div({ class: "tribe-side-actions housing-status-row" }, + span({ class: "card-label" }, `${i18n.taskStatus}: `), + renderStateChip(st === "CLOSED" ? "hidden" : "mutuals", st === "CLOSED" ? "🔒" : "👁", statusLabel(task.status)), form( - { method: "POST", action: `/tasks/delete/${encodeURIComponent(task.id)}` }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - button({ type: "submit", class: "delete-btn" }, i18n.taskDeleteButton) - ), - form( - { method: "POST", action: `/tasks/status/${encodeURIComponent(task.id)}`, class: "project-control-form project-control-form--status" }, + { method: "POST", action: action || `/tasks/status/${encodeURIComponent(task.id)}`, class: "project-control-form project-control-form--status" }, input({ type: "hidden", name: "returnTo", value: returnTo }), select( { name: "status", class: "project-control-select" }, - option({ value: "OPEN", selected: st === "OPEN" }, i18n.taskStatusOpen), - option({ value: "IN-PROGRESS", selected: st === "IN-PROGRESS" }, i18n.taskStatusInProgress), - option({ value: "CLOSED", selected: st === "CLOSED" }, i18n.taskStatusClosed) + option({ value: "OPEN", ...(st === "OPEN" ? { selected: true } : {})}, i18n.taskStatusOpen), + option({ value: "IN-PROGRESS", ...(st === "IN-PROGRESS" ? { selected: true } : {})}, i18n.taskStatusInProgress), + option({ value: "CLOSED", ...(st === "CLOSED" ? { selected: true } : {})}, i18n.taskStatusClosed) ), - button({ class: "status-btn project-control-btn", type: "submit" }, setStatusLabel) + button({ class: "status-btn project-control-btn", type: "submit" }, i18n.taskSetStatus) + ) + ); +}; + +const renderTaskAssignAction = exports.renderTaskAssignAction = (task, isAssignedToMe, returnTo, action = null) => { + const st = normalizeStatus(task.status || "OPEN"); + if (st === "CLOSED") return null; + return [ + span({ class: "card-label" }, `${i18n.taskAssignedTo}: `), + renderStateChip(isAssignedToMe ? "mutuals" : "whole", isAssignedToMe ? "👤" : "○", + String(isAssignedToMe ? i18n.taskAssignedChip : i18n.taskUnassignedChip).toUpperCase()), + form( + { method: "POST", action: action || `/tasks/assign/${encodeURIComponent(task.id)}`, class: "project-control-form" }, + input({ type: "hidden", name: "returnTo", value: returnTo }), + button({ type: "submit", class: "status-btn project-control-btn" }, isAssignedToMe ? i18n.taskUnassignButton : i18n.taskAssignButton) ) ]; }; -const renderTaskAssignAction = (task, isAssignedToMe, returnTo) => { - const st = normalizeStatus(task.status || "OPEN"); - if (st === "CLOSED") return null; - return form( - { method: "POST", action: `/tasks/assign/${encodeURIComponent(task.id)}` }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - button({ type: "submit", class: "filter-btn" }, isAssignedToMe ? i18n.taskUnassignButton : i18n.taskAssignButton) - ); -}; - const renderTaskCommentsSection = (taskId, comments = [], currentFilter = "all") => { - const commentsCount = Array.isArray(comments) ? comments.length : 0; const returnTo = `/tasks/${encodeURIComponent(taskId)}?filter=${encodeURIComponent(currentFilter || "all")}`; - - return div( - { class: "vote-comments-section" }, - div( - { class: "comments-count" }, - span({ class: "card-label" }, i18n.voteCommentsLabel + ": "), - span({ class: "card-value" }, String(commentsCount)) - ), - div( - { class: "comment-form-wrapper" }, - h2({ class: "comment-form-title" }, i18n.voteNewCommentLabel), - form( - { method: "POST", action: `/tasks/${encodeURIComponent(taskId)}/comments`, class: "comment-form", enctype: "multipart/form-data" }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - textarea({ - id: "comment-text", - name: "text", - rows: 4, - class: "comment-textarea", - placeholder: i18n.voteNewCommentPlaceholder - }), - div({ class: "comment-file-upload" }, label(i18n.uploadMedia), input({ type: "file", name: "blob" })), - br(), - button({ type: "submit", class: "comment-submit-btn" }, i18n.voteNewCommentButton) - ) - ), - (() => { - const visibleComments = (comments || []).filter(c => { - const t = c && c.value && c.value.content && c.value.content.text; - return t && String(t).trim(); - }); - return visibleComments.length - ? div( - { class: "comments-list" }, - visibleComments.map((c) => { - const author = c.value && c.value.author ? c.value.author : ""; - const ts = c.value && c.value.timestamp ? c.value.timestamp : c.timestamp; - const absDate = ts ? moment(ts).format("YYYY/MM/DD HH:mm:ss") : ""; - const relDate = ts ? moment(ts).fromNow() : ""; - - const content = c.value && c.value.content ? c.value.content : {}; - const root = content.fork || content.root || ""; - const text = content.text || ""; - - return div( - { class: "votations-comment-card" }, - span( - { class: "created-at" }, - span(i18n.createdBy), - author ? userLink(author) : span("(unknown)"), - absDate ? span(" | ") : "", - absDate ? span({ class: "votations-comment-date" }, absDate) : "", - relDate ? span({ class: "votations-comment-date" }, " | ", i18n.sendTime) : "", - relDate && root ? a({ href: `/thread/${encodeURIComponent(root)}#${encodeURIComponent(c.key)}` }, relDate) : "" - ), - p({ class: "votations-comment-text" }, ...renderUrl(String(text))) - ); - }) - ) - : p({ class: "votations-no-comments" }, i18n.voteNoCommentsYet); - })() - ); + return renderSharedCommentsSection({ + action: `/tasks/${encodeURIComponent(taskId)}/comments`, + comments: comments, + returnTo: returnTo + }); }; const renderTaskStatusChip = (status) => { @@ -159,11 +112,12 @@ const renderTaskPriorityChip = (priority) => { const p = String(priority || "").toUpperCase(); if (!p) return null; const variant = - p === "URGENT" ? "closed" : + p === "URGENT" || p === "CRITICAL" ? "closed" : p === "HIGH" ? "lifespan-orange" : p === "MEDIUM" ? "whole" : "mutuals"; const localized = + p === "CRITICAL" ? i18n.tribePriorityCritical : p === "URGENT" ? i18n.taskPriorityUrgent : p === "HIGH" ? i18n.taskPriorityHigh : p === "MEDIUM" ? i18n.taskPriorityMedium : @@ -171,15 +125,19 @@ const renderTaskPriorityChip = (priority) => { return renderStateChip(variant, "⚑", localized || p); }; -const renderTaskItem = (task, filter, spreadInfo) => { +const renderTaskItem = exports.renderTaskItem = (task, filter, spreadInfo, opts = {}) => { const currentFilter = filter || "all"; const assignees = safeArray(task.assignees); const isPrivate = String(task.isPublic || "").toUpperCase() === "PRIVATE"; + const isAuthor = String(task.author) === String(userId); + const origin = task.tribeOrigin || null; + const href = opts.href !== undefined ? opts.href : (origin ? origin.href : `/tasks/${encodeURIComponent(task.id)}`); - const heroNode = task.image + const cover = origin ? null : imagesOf(task)[0] + const heroNode = cover ? div({ class: "tribe-card-image-wrapper" }, a({ href: `/tasks/${encodeURIComponent(task.id)}` }, - renderTaskMediaBlob(task.image, { class: "tribe-card-hero-image" }) + renderTaskMediaBlob(cover, { class: "tribe-card-hero-image" }) ), div({ class: "tribe-visit-btn-wrapper" }, form({ method: "GET", action: `/tasks/${encodeURIComponent(task.id)}` }, @@ -191,58 +149,54 @@ const renderTaskItem = (task, filter, spreadInfo) => { : null; const chips = [ - renderTaskStatusChip(task.status), isPrivate ? renderPrivacyChip(true, i18n) : null, + renderTaskStatusChip(task.status), renderTaskPriorityChip(task.priority), - renderLifespanChip(task.lifetime, i18n) + renderLifespanChip(task.lifetime, i18n), + origin ? renderTribeOriginChip(origin) : null, + ...(Array.isArray(opts.extraChips) ? opts.extraChips : []) ].filter(Boolean); - const start = task.startTime ? moment(task.startTime).format("YYYY/MM/DD HH:mm") : ""; - const end = task.endTime ? moment(task.endTime).format("YYYY/MM/DD HH:mm") : ""; + const headerActions = opts.headerActions !== undefined + ? opts.headerActions + : origin ? null : renderContentActions(task.id, `/tasks/${encodeURIComponent(task.id)}`, { spread: spreadInfo || null, author: task.author, favKind: 'tasks', isFavorite: task.isFavorite, reportTitle: task.title, returnTo: `/tasks?filter=${encodeURIComponent(currentFilter)}`, deleteAction: isAuthor ? contentDeleteAction('task', task.id) : undefined }); - return div({ class: "tribe-card task-card" }, + return div({ class: "tribe-card task-card" + (isAuthor ? " own-content" : "") }, + headerActions ? div({ class: "card-header activity-card-header" }, span(), headerActions) : null, heroNode, div({ class: "tribe-card-body" }, div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, - a({ href: `/tasks/${encodeURIComponent(task.id)}` }, task.title || i18n.tasksTitle) + href ? a({ href }, task.title || i18n.tasksTitle) : (task.title || i18n.tasksTitle) ) ), chips.length ? div({ class: "card-chips-row" }, ...chips) : null, - (start || end) ? p({ class: "card-date-highlight" }, start && end ? `${start} → ${end}` : (start || end)) : null, - div({ class: "tribe-card-members" }, + origin ? null : div({ class: "tribe-card-members" }, span({ class: "tribe-members-count" }, `${i18n.taskAssignedTo}: ${assignees.length}`) ), - div({ class: "card-spread-centered" }, renderSpreadButton(task.id, spreadInfo)), - !heroNode - ? div({ class: "card-visit-btn-centered" }, - form({ method: "GET", action: `/tasks/${encodeURIComponent(task.id)}` }, - input({ type: "hidden", name: "filter", value: currentFilter }), - button({ type: "submit", class: "filter-btn" }, i18n.viewTask || "View Task") - ) - ) - : null + ...(Array.isArray(opts.bodyExtra) ? opts.bodyExtra.filter(Boolean) : []) ) ); }; +const taskChipFor = (currentFilter, visible) => (mode) => { + if (mode === currentFilter) return true; + if (!Array.isArray(visible)) return true; + if (mode === "mine") return visible.some((t) => t.author === userId); + if (mode === "recent") return visible.length > 0; + if (mode === "assigned") return visible.some((t) => safeArray(t.assignees).includes(userId)); + if (mode === "open") return visible.some((t) => normalizeStatus(t.status) === "OPEN"); + if (mode === "in-progress") return visible.some((t) => normalizeStatus(t.status) === "IN-PROGRESS"); + if (mode === "closed") return visible.some((t) => normalizeStatus(t.status) === "CLOSED"); + if (mode.indexOf("priority-") === 0) { const pr = mode.slice(9).toUpperCase(); return visible.some((t) => String(t.priority).toUpperCase() === pr); } + return true; +}; + exports.taskView = async (tasks, filter, taskId, returnTo, params = {}) => { const list = Array.isArray(tasks) ? tasks : [tasks]; const currentFilter = filter || "all"; - const title = - currentFilter === "mine" ? i18n.taskMineSectionTitle : - currentFilter === "create" ? i18n.taskCreateSectionTitle : - currentFilter === "edit" ? i18n.taskUpdateSectionTitle : - currentFilter === "open" ? i18n.taskOpenTitle : - currentFilter === "in-progress" ? i18n.taskInProgressTitle : - currentFilter === "closed" ? i18n.taskClosedTitle : - currentFilter === "assigned" ? i18n.taskAssignedTitle : - currentFilter === "priority-urgent" ? i18n.taskFilterUrgent : - currentFilter === "priority-high" ? i18n.taskFilterHigh : - currentFilter === "priority-medium" ? i18n.taskFilterMedium : - currentFilter === "priority-low" ? i18n.taskFilterLow : - i18n.taskAllSectionTitle; + const title = i18n.tasksTitle; const canSee = (t) => { const vis = String(t.isPublic || "").toUpperCase(); @@ -254,7 +208,8 @@ exports.taskView = async (tasks, filter, taskId, returnTo, params = {}) => { const visible = list.filter(canSee); let filtered; - if (currentFilter === "mine") filtered = visible.filter((t) => t.author === userId); + if (currentFilter === "recent") filtered = visible.filter((t) => (Date.parse(t.createdAt || "") || 0) >= Date.now() - 86400000); + else if (currentFilter === "mine") filtered = visible.filter((t) => t.author === userId); else if (currentFilter === "assigned") filtered = visible.filter((t) => safeArray(t.assignees).includes(userId)); else if (currentFilter === "open") filtered = visible.filter((t) => normalizeStatus(t.status) === "OPEN"); else if (currentFilter === "in-progress") filtered = visible.filter((t) => normalizeStatus(t.status) === "IN-PROGRESS"); @@ -268,18 +223,21 @@ exports.taskView = async (tasks, filter, taskId, returnTo, params = {}) => { filtered = filtered.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)); const editTask = list.find((t) => t.id === taskId) || {}; - const editTags = Array.isArray(editTask.tags) ? editTask.tags : []; + const formData = currentFilter === "edit" ? editTask : (params.draft || {}); + const editTags = Array.isArray(formData.tags) ? formData.tags : (typeof formData.tags === "string" ? formData.tags.split(",").map(t => t.trim()).filter(Boolean) : []); const minCreate = moment().add(1, "minute").format("YYYY-MM-DDTHH:mm"); const ret = typeof returnTo === "string" && returnTo.startsWith("/tasks") ? returnTo : "/tasks?filter=mine"; + const emptyMod = moduleIsEmpty(filtered, currentFilter, "all", params.q); + const taskChipVisible = taskChipFor(currentFilter, visible); return template( title, section( div( - { class: "tags-header" }, + { class: "tags-header module-header-line" }, h2(i18n.tasksTitle), p(i18n.tasksDescription) ), @@ -287,78 +245,95 @@ exports.taskView = async (tasks, filter, taskId, returnTo, params = {}) => { { class: "filters" }, form( { method: "GET", action: "/tasks" }, - button({ type: "submit", name: "filter", value: "all", class: currentFilter === "all" ? "filter-btn active" : "filter-btn" }, i18n.taskFilterAll), - button({ type: "submit", name: "filter", value: "mine", class: currentFilter === "mine" ? "filter-btn active" : "filter-btn" }, i18n.taskFilterMine), - button({ type: "submit", name: "filter", value: "assigned", class: currentFilter === "assigned" ? "filter-btn active" : "filter-btn" }, i18n.taskFilterAssigned), - button({ type: "submit", name: "filter", value: "open", class: currentFilter === "open" ? "filter-btn active" : "filter-btn" }, i18n.taskFilterOpen), - button({ type: "submit", name: "filter", value: "in-progress", class: currentFilter === "in-progress" ? "filter-btn active" : "filter-btn" }, i18n.taskFilterInProgress), - button({ type: "submit", name: "filter", value: "closed", class: currentFilter === "closed" ? "filter-btn active" : "filter-btn" }, i18n.taskFilterClosed), - button({ type: "submit", name: "filter", value: "priority-low", class: currentFilter === "priority-low" ? "filter-btn active" : "filter-btn" }, i18n.taskFilterLow), - button({ type: "submit", name: "filter", value: "priority-medium", class: currentFilter === "priority-medium" ? "filter-btn active" : "filter-btn" }, i18n.taskFilterMedium), - button({ type: "submit", name: "filter", value: "priority-high", class: currentFilter === "priority-high" ? "filter-btn active" : "filter-btn" }, i18n.taskFilterHigh), - button({ type: "submit", name: "filter", value: "priority-urgent", class: currentFilter === "priority-urgent" ? "filter-btn active" : "filter-btn" }, i18n.taskFilterUrgent), + ...(emptyMod ? [] : [ + button({ type: "submit", name: "filter", value: "all", class: currentFilter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.taskFilterAll).toUpperCase()), + ...(taskChipVisible("recent") ? [button({ type: "submit", name: "filter", value: "recent", class: currentFilter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.taskFilterRecent).toUpperCase())] : []), + ...(taskChipVisible("mine") ? [button({ type: "submit", name: "filter", value: "mine", class: currentFilter === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.taskFilterMine).toUpperCase())] : []), + ...(taskChipVisible("assigned") ? [button({ type: "submit", name: "filter", value: "assigned", class: currentFilter === "assigned" ? "filter-btn active" : "filter-btn" }, String(i18n.taskFilterAssigned).toUpperCase())] : []), + ...(taskChipVisible("open") ? [button({ type: "submit", name: "filter", value: "open", class: currentFilter === "open" ? "filter-btn active" : "filter-btn" }, String(i18n.taskFilterOpen).toUpperCase())] : []), + ...(taskChipVisible("in-progress") ? [button({ type: "submit", name: "filter", value: "in-progress", class: currentFilter === "in-progress" ? "filter-btn active" : "filter-btn" }, String(i18n.taskFilterInProgress).toUpperCase())] : []), + ...(taskChipVisible("closed") ? [button({ type: "submit", name: "filter", value: "closed", class: currentFilter === "closed" ? "filter-btn active" : "filter-btn" }, String(i18n.taskFilterClosed).toUpperCase())] : []), + ...(taskChipVisible("priority-low") ? [button({ type: "submit", name: "filter", value: "priority-low", class: currentFilter === "priority-low" ? "filter-btn active" : "filter-btn" }, String(i18n.taskFilterLow).toUpperCase())] : []), + ...(taskChipVisible("priority-medium") ? [button({ type: "submit", name: "filter", value: "priority-medium", class: currentFilter === "priority-medium" ? "filter-btn active" : "filter-btn" }, String(i18n.taskFilterMedium).toUpperCase())] : []), + ...(taskChipVisible("priority-high") ? [button({ type: "submit", name: "filter", value: "priority-high", class: currentFilter === "priority-high" ? "filter-btn active" : "filter-btn" }, String(i18n.taskFilterHigh).toUpperCase())] : []), + ...(taskChipVisible("priority-urgent") ? [button({ type: "submit", name: "filter", value: "priority-urgent", class: currentFilter === "priority-urgent" ? "filter-btn active" : "filter-btn" }, String(i18n.taskFilterUrgent).toUpperCase())] : []), + ]), button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.taskCreateButton) ) - ) + ), + currentFilter === "edit" || currentFilter === "create" + ? null + : emptyMod ? null : div({ class: "filters activity-filter-chips activity-toolbar-row" }, + renderModuleStatsBy(filtered, t => normalizeStatus(t.status || 'OPEN'), [{ value: 'OPEN', label: i18n.taskStatusOpen }, { value: 'IN-PROGRESS', label: i18n.taskStatusInProgress }, { value: 'CLOSED', label: i18n.taskStatusClosed }]), + form({ method: "GET", action: "/tasks", class: "filter-box" }, + input({ type: "hidden", name: "filter", value: currentFilter }), + input({ type: "text", name: "q", value: params.q || "", placeholder: i18n.taskSearchPlaceholder, class: "filter-box__input" }), + div({ class: "filter-box__controls" }, + button({ type: "submit", class: "filter-box__button" }, i18n.searchButton) + ) + ) + ) ), section( currentFilter === "edit" || currentFilter === "create" ? div( { class: "task-form" }, + currentFilter === "edit" ? await renderSpreadEditWarning(taskId) : null, form( { action: currentFilter === "edit" ? `/tasks/update/${encodeURIComponent(taskId)}` : "/tasks/create", method: "POST", enctype: "multipart/form-data" }, input({ type: "hidden", name: "returnTo", value: ret }), label(i18n.taskTitleLabel), br(), - input({ type: "text", name: "title", required: true, value: currentFilter === "edit" ? (editTask.title || "") : "" }), br(), + input({ type: "text", name: "title", maxlength: "100", required: true, value: formData.title || params.prefillTitle || "" }), br(), label(i18n.taskDescriptionLabel), br(), - textarea({ name: "description", required: true, placeholder: i18n.taskDescriptionPlaceholder, rows: "4" }, currentFilter === "edit" ? (editTask.description || "") : ""), br(), - label(i18n.uploadMedia), br(), - input({ type: "file", name: "image", accept: "image/*" }), br(),br(), + textarea({ maxlength: "5000", name: "description", required: true, placeholder: i18n.taskDescriptionPlaceholder, rows: "4" }, formData.description || params.prefillDescription || ""), br(), + ...renderGalleryFields(formData, currentFilter === "edit"), + br(), label(i18n.taskStartTimeLabel), br(), input({ type: "datetime-local", name: "startTime", required: true, - min: currentFilter === "create" ? minCreate : undefined, - value: currentFilter === "edit" && editTask.startTime ? moment(editTask.startTime).format("YYYY-MM-DDTHH:mm") : "" + min: minCreate, + value: formData.startTime ? moment(formData.startTime).format("YYYY-MM-DDTHH:mm") : "" }), br(), br(), label(i18n.taskEndTimeLabel), br(), input({ type: "datetime-local", name: "endTime", required: true, - min: currentFilter === "create" ? minCreate : undefined, - value: currentFilter === "edit" && editTask.endTime ? moment(editTask.endTime).format("YYYY-MM-DDTHH:mm") : "" + min: minCreate, + value: formData.endTime ? moment(formData.endTime).format("YYYY-MM-DDTHH:mm") : "" }), br(), br(), label(i18n.taskPriorityLabel), br(), select( { name: "priority", required: true }, - opt("URGENT", String(editTask.priority || "").toUpperCase() === "URGENT", i18n.taskPriorityUrgent), - opt("HIGH", String(editTask.priority || "").toUpperCase() === "HIGH", i18n.taskPriorityHigh), - opt("MEDIUM", String(editTask.priority || "").toUpperCase() === "MEDIUM", i18n.taskPriorityMedium), - opt("LOW", !editTask.priority || String(editTask.priority || "").toUpperCase() === "LOW", i18n.taskPriorityLow) + opt("URGENT", String(formData.priority || "").toUpperCase() === "URGENT", i18n.taskPriorityUrgent), + opt("HIGH", String(formData.priority || "").toUpperCase() === "HIGH", i18n.taskPriorityHigh), + opt("MEDIUM", String(formData.priority || "").toUpperCase() === "MEDIUM", i18n.taskPriorityMedium), + opt("LOW", !formData.priority || String(formData.priority || "").toUpperCase() === "LOW", i18n.taskPriorityLow) ), br(), br(), label(i18n.taskLocationLabel), br(), - input({ type: "text", name: "location", value: editTask.location || "" }), br(), + input({ type: "text", name: "location", value: formData.location || "" }), br(), label(i18n.taskTagsLabel), br(), input({ type: "text", name: "tags", value: editTags.join(", ") }), br(), label(i18n.taskVisibilityLabel), br(), select( { name: "isPublic", id: "isPublic" }, - opt("PUBLIC", String(editTask.isPublic || "PUBLIC").toUpperCase() === "PUBLIC", i18n.taskPublic), - opt("PRIVATE", String(editTask.isPublic || "").toUpperCase() === "PRIVATE", i18n.taskPrivate) + opt("PUBLIC", String(formData.isPublic || "PUBLIC").toUpperCase() === "PUBLIC", i18n.taskPublic), + opt("PRIVATE", String(formData.isPublic || "").toUpperCase() === "PRIVATE", i18n.taskPrivate) ), br(), br(), button({ type: "submit" }, currentFilter === "edit" ? i18n.taskUpdateButton : i18n.taskCreateButton) ) ) : filtered.length > 0 - ? div({ class: "jobs-grid" }, filtered.map((t) => renderTaskItem(t, currentFilter, params.spreadMap && params.spreadMap.get(t.id)))) - : p(i18n.notasks) + ? div({ class: "jobs-grid" }, paged(filtered).map((t) => renderTaskItem(t, currentFilter, params.spreadMap && params.spreadMap.get(t.id)))) + : div({ class: "no-content-box" }, p(i18n.notasks)) ) ); }; exports.singleTaskView = async (task, filter, comments = [], params = {}) => { + const taskChipVisible = (mode) => taskChipFor(currentFilter, Array.isArray(params.censusList) ? params.censusList : null)(mode); const currentFilter = filter || "all"; const assignees = safeArray(task.assignees); const commentCount = typeof task.commentCount === "number" ? task.commentCount : 0; @@ -371,16 +346,17 @@ exports.singleTaskView = async (task, filter, comments = [], params = {}) => { { class: "filters" }, form( { method: "GET", action: "/tasks" }, - button({ type: "submit", name: "filter", value: "all", class: currentFilter === "all" ? "filter-btn active" : "filter-btn" }, i18n.taskFilterAll), - button({ type: "submit", name: "filter", value: "mine", class: currentFilter === "mine" ? "filter-btn active" : "filter-btn" }, i18n.taskFilterMine), - button({ type: "submit", name: "filter", value: "assigned", class: currentFilter === "assigned" ? "filter-btn active" : "filter-btn" }, i18n.taskFilterAssigned), - button({ type: "submit", name: "filter", value: "open", class: currentFilter === "open" ? "filter-btn active" : "filter-btn" }, i18n.taskFilterOpen), - button({ type: "submit", name: "filter", value: "in-progress", class: currentFilter === "in-progress" ? "filter-btn active" : "filter-btn" }, i18n.taskFilterInProgress), - button({ type: "submit", name: "filter", value: "closed", class: currentFilter === "closed" ? "filter-btn active" : "filter-btn" }, i18n.taskFilterClosed), - button({ type: "submit", name: "filter", value: "priority-low", class: currentFilter === "priority-low" ? "filter-btn active" : "filter-btn" }, i18n.taskFilterLow), - button({ type: "submit", name: "filter", value: "priority-medium", class: currentFilter === "priority-medium" ? "filter-btn active" : "filter-btn" }, i18n.taskFilterMedium), - button({ type: "submit", name: "filter", value: "priority-high", class: currentFilter === "priority-high" ? "filter-btn active" : "filter-btn" }, i18n.taskFilterHigh), - button({ type: "submit", name: "filter", value: "priority-urgent", class: currentFilter === "priority-urgent" ? "filter-btn active" : "filter-btn" }, i18n.taskFilterUrgent), + button({ type: "submit", name: "filter", value: "all", class: currentFilter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.taskFilterAll).toUpperCase()), + ...(taskChipVisible("recent") ? [button({ type: "submit", name: "filter", value: "recent", class: currentFilter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.taskFilterRecent).toUpperCase())] : []), + ...(taskChipVisible("mine") ? [button({ type: "submit", name: "filter", value: "mine", class: currentFilter === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.taskFilterMine).toUpperCase())] : []), + ...(taskChipVisible("assigned") ? [button({ type: "submit", name: "filter", value: "assigned", class: currentFilter === "assigned" ? "filter-btn active" : "filter-btn" }, String(i18n.taskFilterAssigned).toUpperCase())] : []), + ...(taskChipVisible("open") ? [button({ type: "submit", name: "filter", value: "open", class: currentFilter === "open" ? "filter-btn active" : "filter-btn" }, String(i18n.taskFilterOpen).toUpperCase())] : []), + ...(taskChipVisible("in-progress") ? [button({ type: "submit", name: "filter", value: "in-progress", class: currentFilter === "in-progress" ? "filter-btn active" : "filter-btn" }, String(i18n.taskFilterInProgress).toUpperCase())] : []), + ...(taskChipVisible("closed") ? [button({ type: "submit", name: "filter", value: "closed", class: currentFilter === "closed" ? "filter-btn active" : "filter-btn" }, String(i18n.taskFilterClosed).toUpperCase())] : []), + ...(taskChipVisible("priority-low") ? [button({ type: "submit", name: "filter", value: "priority-low", class: currentFilter === "priority-low" ? "filter-btn active" : "filter-btn" }, String(i18n.taskFilterLow).toUpperCase())] : []), + ...(taskChipVisible("priority-medium") ? [button({ type: "submit", name: "filter", value: "priority-medium", class: currentFilter === "priority-medium" ? "filter-btn active" : "filter-btn" }, String(i18n.taskFilterMedium).toUpperCase())] : []), + ...(taskChipVisible("priority-high") ? [button({ type: "submit", name: "filter", value: "priority-high", class: currentFilter === "priority-high" ? "filter-btn active" : "filter-btn" }, String(i18n.taskFilterHigh).toUpperCase())] : []), + ...(taskChipVisible("priority-urgent") ? [button({ type: "submit", name: "filter", value: "priority-urgent", class: currentFilter === "priority-urgent" ? "filter-btn active" : "filter-btn" }, String(i18n.taskFilterUrgent).toUpperCase())] : []), button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.taskCreateButton) ) ); @@ -401,8 +377,8 @@ exports.singleTaskView = async (task, filter, comments = [], params = {}) => { const ecoTaxChipNode = renderEcoTax(task.msgSize, task.id); const chips = [ - renderTaskStatusChip(task.status), renderPrivacyChip(isPrivate, i18n), + renderTaskStatusChip(task.status), renderTaskPriorityChip(task.priority), lifespanChipNode, ecoTaxChipNode @@ -421,15 +397,8 @@ exports.singleTaskView = async (task, filter, comments = [], params = {}) => { : null; const sideActions = []; - if (task.author && task.author !== userId) { - sideActions.push(form({ method: "GET", action: "/pm" }, - input({ type: "hidden", name: "recipients", value: task.author }), - button({ type: "submit", class: "filter-btn" }, i18n.privateMessage) - )); - } const assignNode = renderTaskAssignAction(task, isAssignedToMe, returnToSelf); - if (assignNode) sideActions.push(assignNode); - if (isAuthor) sideActions.push(...renderTaskOwnerActions(task, returnToSelf)); + const ownerActions = isAuthor ? renderTaskOwnerActions(task, returnToSelf) : []; const infoRows = []; const pushRow = (labelText, valueNode) => @@ -444,19 +413,25 @@ exports.singleTaskView = async (task, filter, comments = [], params = {}) => { if (task.location && String(task.location).trim()) pushRow(i18n.taskLocationLabel, task.location); const taskSide = div({ class: "tribe-side" }, + div({ class: "card-header activity-card-header" }, + renderContentActions(task.id, `/tasks/${encodeURIComponent(task.id)}`, { spread: params.spreads || null, author: task.author, favKind: 'tasks', isFavorite: task.isFavorite, reportTitle: task.title, deleteAction: isAuthor ? contentDeleteAction('task', task.id) : undefined }) + ), div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, task.title) ), chips.length ? div({ class: "card-chips-row" }, ...chips) : null, - div({ class: "card-spread-centered" }, renderSpreadButton(task.id, params.spreads)), - task.image ? renderTaskMediaBlob(task.image, { class: "tribe-detail-image" }) : null, - table({ class: "tribe-info-table jobs-info-table" }, ...infoRows), + renderPhotoGallery(task, 'task'), + table({ class: "tribe-info-table jobs-info-table" }, ...infoRows, ...renderCreatedRows(task.createdAt, task.author)), + assignNode ? div({ class: "tribe-side-actions housing-status-row task-assign-row" }, ...assignNode) : null, + isAuthor ? renderTaskStatusRow(task, returnToSelf) : null, tagsNode, div({ class: "tribe-card-members" }, span({ class: "tribe-members-count" }, `${i18n.taskAssignedTo}: ${assignees.length}`) ), assigneesListNode, - sideActions.length ? div({ class: "tribe-side-actions" }, ...sideActions) : null + sideActions.length ? div({ class: "tribe-side-actions" }, ...sideActions) : null, + ownerActions.length ? div({ class: "tribe-side-actions owner-actions" }, ...ownerActions) : null, + renderDocumentActions('tasks', task.id) ); const returnToOpinions = `/tasks/${encodeURIComponent(task.id)}?filter=${encodeURIComponent(currentFilter)}`; @@ -466,20 +441,16 @@ exports.singleTaskView = async (task, filter, comments = [], params = {}) => { task.description ? div({ class: "job-section" }, h2({ class: "job-section-title" }, i18n.taskDescriptionLabel), - p({ class: "tribe-side-description" }, ...renderUrl(task.description)) + p({ class: "tribe-side-description" }, ...renderStyledText(task.description)) ) : null, - p({ class: "card-footer" }, - span({ class: "date-link" }, `${moment(task.createdAt).format("YYYY/MM/DD HH:mm")} ${i18n.performed} `), - userLink(task.author) - ), - opinionsBar, - renderTaskCommentsSection(task.id, comments, currentFilter) + renderEngagement(task.id, opinionsBar, renderTaskCommentsSection(task.id, comments, currentFilter)) ); return template( task.title, section( + div({ class: "tags-header module-header-line" }, h2(i18n.tasksTitle), p(i18n.tasksDescription)), filterBar, div({ class: "tribe-details" }, taskSide, taskMain) ) diff --git a/src/views/torrents_view.js b/src/views/torrents_view.js index dad95bc..7f50d31 100644 --- a/src/views/torrents_view.js +++ b/src/views/torrents_view.js @@ -1,28 +1,11 @@ -const { - form, - button, - div, - h2, - p, - section, - input, - br, - a, - span, - textarea, - select, - label, - option, - table, - tr, - th, - td -} = require("../server/node_modules/hyperaxe"); +const { form, button, div, h2, h3, p, section, input, br, a, span, textarea, select, label, option, table, tr, th, td, progress, strong } = require("../server/node_modules/hyperaxe"); +const { renderCommentsSection: renderSharedCommentsSection } = require("./comments_view"); +const { renderReachChip, renderClearnetSelector, renderClearnetSwitch, renderTribeOriginChip } = require('./clearnet_view'); -const { template, i18n, renderOpinionsVoting, userLink, renderSpreadButton, renderEcoTax, renderLifespanChip } = require("./main_views"); +const { clearnetItemHref, template, i18n, renderOpinionsVoting, renderEngagement, userLink, renderSpreadButton, renderEcoTax, renderLifespanChip , renderSpreadEditWarning, renderContentActions, renderModuleStats, moduleIsEmpty, renderTorrentDownload, torrentDownloadHref, renderTorrentSourceDownload, contentDeleteAction, paged } = require("./main_views"); const moment = require("../server/node_modules/moment"); const { config } = require("../server/SSB_server.js"); -const { renderUrl } = require("../backend/renderUrl"); +const { renderStyledText } = require("../backend/renderStyledText"); const userId = config.keys.id; @@ -49,21 +32,6 @@ const renderTags = (tags) => { : null; }; -const renderTorrentFavoriteToggle = (torrentObj, returnTo = "") => - form( - { - method: "POST", - action: torrentObj.isFavorite - ? `/torrents/favorites/remove/${encodeURIComponent(torrentObj.key)}` - : `/torrents/favorites/add/${encodeURIComponent(torrentObj.key)}` - }, - returnTo ? input({ type: "hidden", name: "returnTo", value: returnTo }) : null, - button( - { type: "submit", class: "filter-btn" }, - torrentObj.isFavorite ? i18n.torrentRemoveFavoriteButton : i18n.torrentAddFavoriteButton - ) - ); - const renderTorrentOwnerActions = (filter, torrentObj, params = {}) => { const returnTo = buildReturnTo(filter, params); const isAuthor = String(torrentObj.author) === String(userId); @@ -81,75 +49,22 @@ const renderTorrentOwnerActions = (filter, torrentObj, params = {}) => { ) ); } - items.push( - form( - { method: "POST", action: `/torrents/delete/${encodeURIComponent(torrentObj.key)}` }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - button({ class: "delete-btn", type: "submit" }, i18n.torrentDeleteButton) - ) - ); return items; }; const renderTorrentCommentsSection = (torrentId, comments = [], returnTo = null) => { - const list = safeArr(comments); - const commentsCount = list.length; + return renderSharedCommentsSection({ + action: `/torrents/${encodeURIComponent(torrentId)}/comments`, + comments: comments, + returnTo: returnTo + }); +}; - return div( - { class: "vote-comments-section" }, - div( - { class: "comments-count" }, - span({ class: "card-label" }, i18n.voteCommentsLabel + ": "), - span({ class: "card-value" }, String(commentsCount)) - ), - div( - { class: "comment-form-wrapper" }, - h2({ class: "comment-form-title" }, i18n.voteNewCommentLabel), - form( - { method: "POST", action: `/torrents/${encodeURIComponent(torrentId)}/comments`, class: "comment-form", enctype: "multipart/form-data" }, - returnTo ? input({ type: "hidden", name: "returnTo", value: returnTo }) : null, - textarea({ - id: "comment-text", - name: "text", - rows: 4, - class: "comment-textarea", - placeholder: i18n.voteNewCommentPlaceholder - }), - div({ class: "comment-file-upload" }, label(i18n.uploadMedia), input({ type: "file", name: "blob" })), - br(), - button({ type: "submit", class: "comment-submit-btn" }, i18n.voteNewCommentButton) - ) - ), - list.length - ? div( - { class: "comments-list" }, - list.map((c) => { - const author = c?.value?.author || ""; - const ts = c?.value?.timestamp || c?.timestamp; - const absDate = ts ? moment(ts).format("YYYY/MM/DD HH:mm:ss") : ""; - const relDate = ts ? moment(ts).fromNow() : ""; - const content = c?.value?.content || {}; - const rootId = content.fork || content.root || null; - const text = content.text || ""; - - return div( - { class: "votations-comment-card" }, - span( - { class: "created-at" }, - span(i18n.createdBy), - author ? userLink(author) : span("(unknown)"), - absDate ? span(" | ") : "", - absDate ? span({ class: "votations-comment-date" }, absDate) : "", - relDate ? span({ class: "votations-comment-date" }, " | ", i18n.sendTime) : "", - relDate && rootId ? a({ href: `/thread/${encodeURIComponent(rootId)}#${encodeURIComponent(c.key)}` }, relDate) : "" - ), - p({ class: "votations-comment-text" }, ...renderUrl(text)) - ); - }) - ) - : p({ class: "votations-no-comments" }, i18n.voteNoCommentsYet) - ); +const blobSha256Hex = (blobId) => { + const m = /^&([A-Za-z0-9+/=]+)\.sha256$/.exec(String(blobId || "")); + if (!m) return ""; + try { return Buffer.from(m[1], "base64").toString("hex"); } catch (_) { return ""; } }; const formatSize = (bytes) => { @@ -160,8 +75,16 @@ const formatSize = (bytes) => { return (n / (1024 * 1024)).toFixed(1) + " MB"; }; +const spreadInfoOf = (t, spreadMap) => (spreadMap instanceof Map && spreadMap.get(t.key)) || null; +const renderTorrentSeeds = (t, spreadMap) => { + const info = spreadInfoOf(t, spreadMap); + return String(info && typeof info.count === "number" ? info.count : (info && Array.isArray(info.voters) ? info.voters.length : 0)); +}; +const renderTorrentSpread = (t, spreadMap) => String(t.author) === String(userId) ? "—" : renderSpreadButton(t.key, spreadInfoOf(t, spreadMap), i18n.seedAction, false); + const renderTorrentTable = exports.renderTorrentTable = (torrents, filter, params = {}) => { const returnTo = buildReturnTo(filter, params); + const extraColumns = typeof params.rowFor === "function"; if (!torrents.length) return p(params.q ? i18n.torrentNoMatch : i18n.noTorrents); @@ -169,43 +92,51 @@ const renderTorrentTable = exports.renderTorrentTable = (torrents, filter, param { border: "1", class: "torrent-table" }, tr( th(i18n.createdAt || "DATE"), - th(i18n.authorLabel || "AUTHOR"), th(i18n.torrentTitleLabel || "TITLE"), th(i18n.torrentSizeLabel || "SIZE"), + th({ class: "torrent-spread-cell" }, i18n.seedsLabel), + th({ class: "torrent-spread-cell" }, i18n.seedAction), th(""), - th("") + ...(extraColumns ? [th(""), th("")] : []) ), - torrents.map((t) => - tr( - td(moment(t.createdAt).format("YYYY/MM/DD HH:mm")), - td(userLink(t.author)), - td(t.title || ""), - td(formatSize(t.size)), + torrents.map((t) => { + const row = typeof params.rowFor === "function" ? params.rowFor(t) : null; + return tr( + td({ class: "row-chips" }, t.tribeOrigin ? renderTribeOriginChip(t.tribeOrigin) : t.clearnet === true ? renderReachChip(true, i18n, clearnetItemHref('torrents', t.title, t.key)) : null, ...(row && Array.isArray(row.titleChips) ? row.titleChips.filter(Boolean) : [])), + td({ "data-label": i18n.createdAt || "DATE" }, moment(t.createdAt).format("YYYY/MM/DD HH:mm")), + td({ class: "row-title", "data-label": i18n.torrentTitleLabel || "TITLE" }, t.title || ""), + td({ "data-label": i18n.torrentSizeLabel || "SIZE" }, formatSize(t.size)), + td({ class: "torrent-spread-cell", "data-label": i18n.seedsLabel }, t.tribeOrigin || row ? "" : renderTorrentSeeds(t, params.spreadMap)), + td({ class: "torrent-spread-cell", "data-label": i18n.seedAction }, t.tribeOrigin || row ? "" : renderTorrentSpread(t, params.spreadMap)), td( - form( - { method: "GET", action: `/torrents/${encodeURIComponent(t.key)}` }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - input({ type: "hidden", name: "filter", value: filter || "all" }), - params.q ? input({ type: "hidden", name: "q", value: params.q }) : null, - params.sort ? input({ type: "hidden", name: "sort", value: params.sort }) : null, - button({ type: "submit", class: "filter-btn" }, i18n.viewDetails) - ) + row + ? (row.details || "") + : t.tribeOrigin + ? a({ href: t.tribeOrigin.href, class: "filter-btn" }, i18n.torrentDetailsButton) + : form( + { method: "GET", action: `/torrents/${encodeURIComponent(t.key)}` }, + input({ type: "hidden", name: "returnTo", value: returnTo }), + input({ type: "hidden", name: "filter", value: filter || "all" }), + params.q ? input({ type: "hidden", name: "q", value: params.q }) : null, + params.sort ? input({ type: "hidden", name: "sort", value: params.sort }) : null, + button({ type: "submit", class: "filter-btn" }, i18n.torrentDetailsButton) + ) ), - td( - t.url && t.url.startsWith("&") - ? a({ href: `/blob/${encodeURIComponent(t.url)}`, class: "filter-btn" }, i18n.torrentDownloadButton || "DOWNLOAD IT!") - : "" - ) - ) - ) + ...(extraColumns ? [td(row ? (row.download || "") : ""), td({ class: "row-actions" }, row ? (row.actions || "") : "")] : []) + ); + }) ); }; const renderTorrentForm = (filter, torrentId, torrentToEdit, params = {}) => { const returnTo = safeText(params.returnTo) || buildReturnTo("all", params); const tribeId = safeText(params.tribeId || ""); + const fromBlob = filter === "edit" ? "" : safeText(params.fromBlob || ""); + const fromName = safeText(params.fromName || ""); + const clearnetAllowed = !tribeId && !torrentToEdit?.tribeId && !torrentToEdit?.encrypted && !torrentToEdit?.cipher; return div( { class: "div-center audio-form" }, + params.spreadWarning || null, form( { action: filter === "edit" ? `/torrents/update/${encodeURIComponent(torrentId)}` : "/torrents/create", @@ -214,18 +145,29 @@ const renderTorrentForm = (filter, torrentId, torrentToEdit, params = {}) => { }, input({ type: "hidden", name: "returnTo", value: returnTo }), tribeId ? input({ type: "hidden", name: "tribeId", value: tribeId }) : null, - span(i18n.torrentFileLabel), - br(), - input({ type: "file", name: "torrent", accept: ".torrent", required: filter !== "edit" }), - br(), - br(), + fromBlob + ? div({ class: "torrent-from-notice" }, + input({ type: "hidden", name: "fromBlob", value: fromBlob }), + input({ type: "hidden", name: "fromName", value: fromName }), + table({ class: "tribe-info-table torrent-file-info" }, + tr(td({ class: "tribe-info-label" }, i18n.fileShareFileLabel), td({ class: "tribe-info-value" }, fromName || fromBlob.slice(0, 16))), + tr(td({ class: "tribe-info-label" }, i18n.torrentSizeLabel), td({ class: "tribe-info-value" }, formatSize(Number(params.fromSize) || 0))), + tr(td({ class: "tribe-info-label" }, "SHA-256"), td({ class: "tribe-info-value" }, span({ class: "bank-address-code" }, blobSha256Hex(fromBlob)))) + ) + ) + : null, + fromBlob ? null : span(i18n.torrentFileLabel), + fromBlob ? null : br(), + fromBlob ? null : input({ type: "file", name: "torrent", accept: ".torrent", required: filter !== "edit" }), + fromBlob ? null : br(), + fromBlob ? null : br(), span(i18n.torrentTitleLabel), br(), - input({ type: "text", name: "title", placeholder: i18n.torrentTitlePlaceholder, value: torrentToEdit?.title || "", required: true }), + input({ type: "text", name: "title", maxlength: "100", placeholder: i18n.torrentTitlePlaceholder, value: torrentToEdit?.title || fromName || "", required: true }), br(), span(i18n.torrentDescriptionLabel), br(), - textarea({ name: "description", placeholder: i18n.torrentDescriptionPlaceholder, rows: "4" }, torrentToEdit?.description || ""), + textarea({ maxlength: "5000", name: "description", placeholder: i18n.torrentDescriptionPlaceholder, rows: "4" }, torrentToEdit?.description || ""), br(), span(i18n.torrentTagsLabel), br(), @@ -237,60 +179,128 @@ const renderTorrentForm = (filter, torrentId, torrentToEdit, params = {}) => { }), br(), br(), + ...(clearnetAllowed ? [renderClearnetSelector(filter === "edit" ? !!torrentToEdit?.clearnet : false, i18n), br()] : []), button({ type: "submit" }, filter === "edit" ? i18n.torrentUpdateButton : i18n.torrentCreateButton) ) ); }; +const torrentStateLabel = (state) => { + const map = { queued: i18n.torrentStateQueued, probing: i18n.torrentStateProbing, downloading: i18n.torrentStateDownloading, done: i18n.torrentStateDone, failed: i18n.torrentStateFailed, nosource: i18n.torrentStateNoSource, cancelled: i18n.torrentStateCancelled }; + return map[state] || String(state || ""); +}; + +const sourceSaveHref = (blobId, name) => `/blob/${encodeURIComponent(blobId)}?download=1&name=${encodeURIComponent(String(name || "download"))}`; + +const renderDownloadRow = (d) => { + const pct = d.size > 0 ? Math.min(100, Math.floor((d.received / d.size) * 100)) : (d.state === "done" ? 100 : 0); + const titleNode = d.torrentKey ? a({ href: `/torrents/${encodeURIComponent(d.torrentKey)}` }, d.name || d.blobId.slice(0, 12)) : span(d.name || d.blobId.slice(0, 12)); + const actions = d.state === "done" + ? a({ href: sourceSaveHref(d.blobId, d.name), class: "filter-btn" }, i18n.torrentSaveFile) + : d.active + ? form({ method: "POST", action: `/torrents/downloads/${encodeURIComponent(d.blobId)}/cancel` }, button({ type: "submit", class: "filter-btn" }, i18n.torrentCancel)) + : form({ method: "POST", action: `/torrents/downloads/${encodeURIComponent(d.blobId)}/remove` }, button({ type: "submit", class: "filter-btn" }, "✕")); + return tr({ class: "torrent-download-row torrent-state-" + d.state }, + td(titleNode), + td(d.size ? formatSize(d.size) : "—"), + td(String(d.seeds || 0)), + td({ class: "torrent-progress-cell" }, progress({ value: String(pct), max: "100", class: "torrent-progress" }), span({ class: "torrent-progress-text" }, ` ${pct}%`)), + td(torrentStateLabel(d.state)), + td(actions) + ); +}; + +const renderDownloads = (downloads) => { + const list = safeArr(downloads); + if (!list.length) return p(i18n.torrentNoDownloads); + return table( + { border: "1", class: "torrent-table torrent-downloads" }, + tr( + th(i18n.torrentSourceFile), + th(i18n.torrentSizeLabel), + th(i18n.torrentSeeds), + th(""), + th(""), + th("") + ), + ...list.map(renderDownloadRow) + ); +}; + +const renderOasisDownload = (torrentObj, params = {}) => { + if (!torrentObj.source || params.isPublic) return null; + const d = params.download || null; + const rows = [ + tr(td({ class: "tribe-info-label" }, i18n.fileShareFileLabel), td({ class: "tribe-info-value" }, torrentObj.sourceName || torrentObj.source.slice(0, 16))), + tr(td({ class: "tribe-info-label" }, i18n.torrentSizeLabel), td({ class: "tribe-info-value" }, formatSize(torrentObj.size))), + tr(td({ class: "tribe-info-label" }, "SHA-256"), td({ class: "tribe-info-value" }, span({ class: "bank-address-code" }, blobSha256Hex(torrentObj.source)))) + ]; + let action; + if (d && d.active) { + const pct = d.size > 0 ? Math.min(100, Math.floor((d.received / d.size) * 100)) : 0; + action = div({ class: "torrent-progress-cell" }, + progress({ value: String(pct), max: "100", class: "torrent-progress" }), + span({ class: "torrent-progress-text" }, ` ${pct}% · ${torrentStateLabel(d.state)} · ${d.seeds || 0} ${i18n.torrentSeeds}`), + br(), + a({ href: "/torrents?filter=downloads", class: "filter-btn" }, i18n.torrentDownloads) + ); + } else { + action = renderTorrentSourceDownload(torrentObj.key, torrentObj.source); + } + return div({ class: "torrent-oasis torrent-detail-section" }, + h3({ class: "torrent-section-title" }, i18n.torrentOriginalSection || "Original"), + table({ class: "tribe-info-table torrent-file-info" }, ...rows), + action + ); +}; + +const mediaChipFor = (filter, censusM) => (mode) => { + if (mode === filter) return true; + if (!Array.isArray(censusM)) return true; + if (mode === "top") return censusM.some((x) => !x.tribeOrigin); + if (mode === "mine") return censusM.some((x) => String(x.author) === String(userId)); + if (mode === "recent") return censusM.length > 0; + if (mode === "favorites") return censusM.some((x) => x.isFavorite); + if (mode === "bcs") return censusM.some((x) => String(x.title || "").toUpperCase().startsWith("BCS-")); + return true; +}; + exports.torrentsView = async (torrents, filter = "all", torrentId = null, params = {}) => { - const title = - filter === "mine" - ? i18n.torrentMineSectionTitle - : filter === "create" - ? i18n.torrentCreateSectionTitle - : filter === "edit" - ? i18n.torrentUpdateSectionTitle - : filter === "recent" - ? i18n.torrentRecentSectionTitle - : filter === "top" - ? i18n.torrentTopSectionTitle - : filter === "favorites" - ? i18n.torrentFavoritesSectionTitle - : i18n.torrentAllSectionTitle; + if (filter === "edit") params = { ...params, spreadWarning: await renderSpreadEditWarning(torrentId) }; + const title = i18n.torrentsTitle; const q = safeText(params.q || ""); const sort = safeText(params.sort || "recent"); const list = safeArr(torrents); + const emptyMod = moduleIsEmpty(list, filter, "all", q); + const mediaChip = mediaChipFor(filter, Array.isArray(params.censusList) ? params.censusList : list); const torrentToEdit = torrentId ? list.find((t) => t.key === torrentId) : null; return template( title, section( - div({ class: "tags-header" }, + div({ class: "tags-header module-header-line" }, h2(title), p(i18n.torrentsDescription) ), - (() => { - const { renderReachChip } = require('./clearnet_view'); - const isClearnet = !!(params.viewerPrefs && params.viewerPrefs.clearnetTorrents); - return div({ class: "shop-title-row" }, renderReachChip(isClearnet, i18n)); - })(), - br(), div( { class: "filters" }, form( { method: "GET", action: "/torrents", class: "ui-toolbar ui-toolbar--filters" }, - input({ type: "hidden", name: "q", value: q }), input({ type: "hidden", name: "sort", value: sort }), - button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, i18n.torrentFilterAll), - button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, i18n.torrentFilterMine), - button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, i18n.torrentFilterRecent), - button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, i18n.torrentFilterTop), - button( + ...(emptyMod ? [] : [ + button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.torrentFilterAll).toUpperCase()), + ...(mediaChip("recent") ? [button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.torrentFilterRecent).toUpperCase())] : []), + ...(mediaChip("mine") ? [button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.torrentFilterMine).toUpperCase())] : []), + ...(mediaChip("favorites") ? [button( { type: "submit", name: "filter", value: "favorites", class: filter === "favorites" ? "filter-btn active" : "filter-btn" }, - i18n.torrentFilterFavorites - ), + String(i18n.torrentFilterFavorites).toUpperCase() + )] : []), + + ...(mediaChip("top") ? [button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, String(i18n.torrentFilterTop).toUpperCase())] : []), + ...(params.isPublic || filter === "create" ? [] : [button({ type: "submit", name: "filter", value: "downloads", class: filter === "downloads" ? "filter-btn active" : "filter-btn" }, String(i18n.torrentDownloads).toUpperCase())]), + ]), button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.torrentCreateButton) ) ) @@ -298,9 +308,12 @@ exports.torrentsView = async (torrents, filter = "all", torrentId = null, params section( filter === "create" || filter === "edit" ? renderTorrentForm(filter, torrentId, torrentToEdit, { ...params, filter }) + : filter === "downloads" + ? section(renderDownloads(params.downloads)) : section( - div( - { class: "audios-search" }, + emptyMod ? null : div( + { class: "audios-search activity-filter-chips activity-toolbar-row" }, + renderModuleStats(list.length), form( { method: "GET", action: "/torrents", class: "filter-box" }, input({ type: "hidden", name: "filter", value: filter }), @@ -315,28 +328,31 @@ exports.torrentsView = async (torrents, filter = "all", torrentId = null, params { class: "filter-box__controls" }, select( { name: "sort", class: "filter-box__select" }, - option({ value: "recent", selected: sort === "recent" }, i18n.torrentSortRecent), - option({ value: "oldest", selected: sort === "oldest" }, i18n.torrentSortOldest), - option({ value: "top", selected: sort === "top" }, i18n.torrentSortTop) + option({ value: "recent", ...(sort === "recent" ? { selected: true } : {})}, i18n.torrentSortRecent), + option({ value: "oldest", ...(sort === "oldest" ? { selected: true } : {})}, i18n.torrentSortOldest), + option({ value: "top", ...(sort === "top" ? { selected: true } : {})}, i18n.torrentSortTop) ), button({ type: "submit", class: "filter-box__button" }, i18n.torrentSearchButton) ) ) ), - div({ class: "audios-list" }, renderTorrentTable(list, filter, { q, sort })) + div({ class: "audios-list" }, renderTorrentTable(paged(list), filter, { q, sort, spreadMap: params.spreadMap })) ) ) ); }; exports.singleTorrentView = async (torrentObj, filter = "all", comments = [], params = {}) => { + const mediaChip = mediaChipFor(filter, Array.isArray(params.censusList) ? params.censusList : null); const q = safeText(params.q || ""); const sort = safeText(params.sort || "recent"); const returnTo = safeText(params.returnTo) || buildReturnTo(filter, { q, sort }); const title = safeText(torrentObj.title); - const { renderReachChip } = require('./clearnet_view'); - const isClearnet = !!(params.authorPrefs && params.authorPrefs.clearnetTorrents); + const { renderEncryptedChip, renderTransportChip } = require('./clearnet_view'); + const isAuthor = String(torrentObj.author) === String(userId); + const clearnetAllowed = !torrentObj.tribeId && !torrentObj.encrypted && !torrentObj.cipher; + const isClearnet = !!torrentObj.clearnet; const chips = [ renderLifespanChip(torrentObj.lifetime, i18n), @@ -345,36 +361,54 @@ exports.singleTorrentView = async (torrentObj, filter = "all", comments = [], pa const ownerActions = renderTorrentOwnerActions(filter, torrentObj, { q, sort }); const sideActions = []; - sideActions.push(renderTorrentFavoriteToggle(torrentObj, returnTo)); - if (torrentObj.author && String(torrentObj.author) !== String(userId)) { - sideActions.push(form( - { method: "GET", action: "/pm" }, - input({ type: "hidden", name: "recipients", value: torrentObj.author }), - button({ type: "submit", class: "filter-btn" }, i18n.privateMessage) - )); - } for (const a of ownerActions) sideActions.push(a); const tagsNode = renderTags(torrentObj.tags); + const detailActions = div({ class: "card-header activity-card-header" }, + renderContentActions(torrentObj.key, `/torrents/${encodeURIComponent(torrentObj.key)}`, { + spreadTitle: i18n.seedAction, + author: torrentObj.author, + favKind: 'torrents', + isFavorite: torrentObj.isFavorite, + spread: params.spreads || null, + returnTo, + reportTitle: torrentObj.title, + deleteAction: String(torrentObj.author) === String(userId) ? contentDeleteAction('torrent', torrentObj.key) : undefined + }) + ); + const torrentSide = div({ class: "tribe-side" }, div({ class: "shop-title-row" }, title ? h2({ class: "tribe-card-title" }, title) : null, - renderReachChip(isClearnet, i18n) + torrentObj.tribeId && torrentObj.cipher ? renderEncryptedChip(i18n) : renderTransportChip(i18n), + clearnetAllowed ? renderReachChip(isClearnet, i18n, isClearnet ? clearnetItemHref('torrents', torrentObj.title, torrentObj.key) : null) : null, + clearnetAllowed && isAuthor ? renderClearnetSwitch('torrents', torrentObj.rootId || torrentObj.key, isClearnet) : null ), chips.length ? div({ class: "card-chips-row" }, ...chips) : null, safeText(torrentObj.description) - ? p({ class: "tribe-side-description" }, ...renderUrl(torrentObj.description)) + ? p({ class: "tribe-side-description" }, ...renderStyledText(torrentObj.description)) : null, tagsNode, - div({ class: "card-spread-centered" }, renderSpreadButton(torrentObj.key, params.spreads)), sideActions.length ? div({ class: "tribe-side-actions" }, ...sideActions) : null ); const torrentMain = div({ class: "tribe-main" }, + detailActions, torrentObj.url && torrentObj.url.startsWith("&") ? div({ class: "torrent-download" }, - a({ href: `/blob/${encodeURIComponent(torrentObj.url)}?name=${encodeURIComponent((torrentObj.title || 'download').replace(/\.torrent$/i, '') + '.torrent')}` , class: "filter-btn" }, i18n.torrentDownloadButton || "DOWNLOAD IT!") + renderOasisDownload(torrentObj, params), + div({ class: "torrent-detail-section" }, + h3({ class: "torrent-section-title" }, "Torrent"), + table({ class: "tribe-info-table torrent-file-info" }, + tr(td({ class: "tribe-info-label" }, i18n.fileShareFileLabel), td({ class: "tribe-info-value" }, `${String(torrentObj.title || "download").replace(/\.torrent$/i, "")}.torrent`)), + torrentObj.source ? null : tr(td({ class: "tribe-info-label" }, i18n.torrentSizeLabel), td({ class: "tribe-info-value" }, formatSize(torrentObj.size))), + torrentObj.cipher + ? tr(td({ class: "tribe-info-label" }, i18n.encryptedChipLabel || "E2E"), td({ class: "tribe-info-value" }, "AES-256-GCM")) + : tr(td({ class: "tribe-info-label" }, "SHA-256"), td({ class: "tribe-info-value" }, span({ class: "bank-address-code" }, blobSha256Hex(torrentObj.url)))) + ), + renderTorrentDownload(torrentObj.cipher ? `/torrents/${encodeURIComponent(torrentObj.key)}/file` : torrentDownloadHref(torrentObj.url, torrentObj.title)) + ) ) : p(i18n.torrentNoFile), (() => { @@ -384,41 +418,42 @@ exports.singleTorrentView = async (torrentObj, filter = "all", comments = [], pa return p( { class: "card-footer" }, - span({ class: "date-link" }, `${moment(torrentObj.createdAt).format("YYYY/MM/DD HH:mm:ss")} ${i18n.performed} `), + span({ class: "date-link" }, `${moment(torrentObj.createdAt).format("YYYY/MM/DD HH:mm")}`), userLink(torrentObj.author), showUpdated ? span( { class: "votations-comment-date" }, - ` | ${i18n.torrentUpdatedAt}: ${moment(torrentObj.updatedAt).format("YYYY/MM/DD HH:mm:ss")}` + ` | ${i18n.torrentUpdatedAt}: ${moment(torrentObj.updatedAt).format("YYYY/MM/DD HH:mm")}` ) : null ); })(), - renderOpinionsVoting('/torrents/opinions', torrentObj.key, torrentObj.opinions, returnTo, torrentObj.opinions_inhabitants), - renderTorrentCommentsSection(torrentObj.key, comments, returnTo) + torrentObj.tribeId ? null : renderEngagement(torrentObj.key, + renderOpinionsVoting('/torrents/opinions', torrentObj.key, torrentObj.opinions, returnTo, torrentObj.opinions_inhabitants), + renderTorrentCommentsSection(torrentObj.key, comments, returnTo) + ) ); return template( i18n.torrentsTitle, section( - div({ class: "tags-header" }, + div({ class: "tags-header module-header-line" }, h2(i18n.torrentAllSectionTitle || i18n.torrentsTitle), - p(i18n.torrentDescription) + p(i18n.torrentsDescription) ), div( { class: "filters" }, form( { method: "GET", action: "/torrents", class: "ui-toolbar ui-toolbar--filters" }, - input({ type: "hidden", name: "q", value: q }), input({ type: "hidden", name: "sort", value: sort }), - button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, i18n.torrentFilterAll), - button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, i18n.torrentFilterMine), - button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, i18n.torrentFilterRecent), - button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, i18n.torrentFilterTop), - button( + button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.torrentFilterAll).toUpperCase()), ...(mediaChip("favorites") ? [button( + ...(mediaChip("recent") ? [button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.torrentFilterRecent).toUpperCase())] : []), + ...(mediaChip("mine") ? [button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.torrentFilterMine).toUpperCase())] : []), { type: "submit", name: "filter", value: "favorites", class: filter === "favorites" ? "filter-btn active" : "filter-btn" }, - i18n.torrentFilterFavorites - ), + String(i18n.torrentFilterFavorites).toUpperCase() + )] : []), + + button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, String(i18n.torrentFilterTop).toUpperCase()), button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.torrentCreateButton) ) ), diff --git a/src/views/transfer_view.js b/src/views/transfer_view.js index e3c3de5..f712fe6 100644 --- a/src/views/transfer_view.js +++ b/src/views/transfer_view.js @@ -1,5 +1,7 @@ -const { div, h2, p, section, button, form, a, input, br, span, label, select, option, progress, table, tr, td } = require("../server/node_modules/hyperaxe") -const { template, i18n, renderOpinionsVoting, userLink, renderStateChip, renderLifespanChip, renderEcoTax, renderSpreadButton, renderContentActions } = require("./main_views") +const { hr, div, h2, p, section, button, form, a, input, br, span, label, select, option, progress, table, tr, td } = require("../server/node_modules/hyperaxe") +const { getConfig } = require('../configs/config-manager.js'); +const { template, i18n, renderOpinionsVoting, renderEngagement, userLink, renderStateChip, renderLifespanChip, renderEcoTax, renderSpreadButton, renderContentActions, renderModuleStatsBy, moduleIsEmpty, renderEcoValueChip, renderWalletChip, contentDeleteAction, paged, renderCreatedRows } = require("./main_views") +const { renderCommentsSection: renderSharedCommentsSection } = require("./comments_view") const moment = require("../server/node_modules/moment") const { config } = require("../server/SSB_server.js") @@ -89,7 +91,7 @@ const renderBlockInfoCard = (block) => { div({ class: "card-field" }, span({ class: "card-label" }, `${i18n.blockchainBlockID || "Block ID"}: `), span({ class: "card-value" }, - a({ href: `/blockexplorer/block/${encodeURIComponent(block.id)}` }, block.id) + process.env.OASIS_MOBILE === '1' ? block.id : a({ href: `/blockexplorer/block/${encodeURIComponent(block.id)}` }, block.id) ) ) ) @@ -116,25 +118,10 @@ const renderOwnerActions = (transfer, returnTo) => { { method: "GET", action: `/transfers/edit/${encodeURIComponent(transfer.id)}` }, input({ type: "hidden", name: "returnTo", value: returnTo }), button({ type: "submit", class: "update-btn" }, i18n.transfersUpdateButton) - ), - form( - { method: "POST", action: `/transfers/delete/${encodeURIComponent(transfer.id)}` }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - button({ type: "submit", class: "delete-btn" }, i18n.transfersDeleteButton) ) ] } -const renderUpdatedLabel = (createdAt, updatedAt) => { - const createdTs = createdAt ? new Date(createdAt).getTime() : NaN - const updatedTs = updatedAt ? new Date(updatedAt).getTime() : NaN - const showUpdated = Number.isFinite(updatedTs) && (!Number.isFinite(createdTs) || updatedTs !== createdTs) - return showUpdated - ? span({ class: "votations-comment-date" }, ` | ${i18n.transfersUpdatedAt}: ${moment(updatedAt).format("YYYY-MM-DD HH:mm")}`) - : null -} - - const renderTransferStatusChip = (status) => { const up = String(status || "").toUpperCase() const variant = @@ -145,12 +132,25 @@ const renderTransferStatusChip = (status) => { return renderStateChip(variant, icon, i18n[statusKey(up)] || up) } +const ubiEpochLabel = (t) => { + const tags = safeArr(t.tags).map(x => String(x)) + const fromTag = tags.find(x => /^epoch:\d{4}-\d{2}$/i.test(x)) + const epoch = fromTag ? fromTag.slice(6) : ((String(t.concept || "").match(/(\d{4}-\d{2})/) || [])[1] || "") + return epoch ? `UBI - ${epoch}` : "UBI" +} +const transferTitle = (t) => { + const tags = safeArr(t.tags).map(x => String(x).toUpperCase()) + if (tags.includes("UBI") && /UBI Payment|^UBI\b/.test(String(t.concept || ""))) return ubiEpochLabel(t) + return t.concept || i18n.transfersTitle +} + const renderTransferCategoryChip = (cat) => renderStateChip("encrypted", "", categoryLabel(cat)) const generateTransferCard = (transfer, filter, params = {}) => { const confirmedBy = safeArr(transfer.confirmedBy) - const required = transfer.from === transfer.to ? 1 : 2 + const settledUbiTx = (Array.isArray(transfer.tags) ? transfer.tags : []).some(t => String(t).toUpperCase() === "UBI") && /^[0-9a-f]{64}$/i.test(String(transfer.txid || "")) + const required = (transfer.from === transfer.to || settledUbiTx) ? Math.max(1, Math.min(confirmedBy.length, 2)) : 2 const confirmedCount = confirmedBy.length const dl = transfer.deadline ? moment(transfer.deadline) : null const isExpired = dl && dl.isValid() ? dl.isBefore(moment()) : false @@ -160,7 +160,7 @@ const generateTransferCard = (transfer, filter, params = {}) => { const chips = [ renderTransferStatusChip(transfer.status), - isUbi ? renderStateChip("mutuals", "🎁", "UBI") : null, + isUbi ? renderStateChip("mutuals", "", "UBI") : null, isExpired ? renderStateChip("closed", "⏰", i18n.transfersExpiredBadge) : null, renderLifespanChip(transfer.lifetime, i18n) ].filter(Boolean) @@ -170,59 +170,56 @@ const generateTransferCard = (transfer, filter, params = {}) => { const returnTo = buildReturnTo(filter, params) const isOwn = transfer.from && String(transfer.from) === String(userId) + const canDelete = isOwn && String(transfer.status || "").toUpperCase() === "UNCONFIRMED" && !isExpired && confirmedCount < required return div({ class: "trending-card transfer-card" + (isOwn ? " own-content" : "") }, div( { class: "card-header activity-card-header" }, span(), - renderContentActions(transfer.id, `/transfers/${encodeURIComponent(transfer.id)}`) + renderContentActions(transfer.id, `/transfers/${encodeURIComponent(transfer.id)}`, { spread: params.spreadMap && params.spreadMap.get(transfer.id) || null, author: transfer.from, favKind: 'transfers', isFavorite: transfer.isFavorite, reportTitle: transfer.concept, returnTo, deleteAction: canDelete ? contentDeleteAction('transfer', transfer.id) : undefined }) ), div({ class: "card-section transfer-card-body" }, div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, - a({ href: `/transfers/${encodeURIComponent(transfer.id)}` }, transfer.concept || i18n.transfersTitle) + a({ href: `/transfers/${encodeURIComponent(transfer.id)}` }, transferTitle(transfer)) ) ), chips.length ? div({ class: "card-chips-row" }, ...chips) : null, !isUbi && dl && dl.isValid() - ? p({ class: "card-date-highlight" }, dl.format("YYYY-MM-DD HH:mm")) + ? p({ class: "time-chip" }, dl.format("YYYY/MM/DD HH:mm")) : null, cat !== "TRUST" - ? div({ class: "job-price-line card-salary" }, fmtAmountWithUnit(transfer)) - : null, - div({ class: "tribe-card-members" }, - span({ class: "tribe-members-count" }, `${i18n.transfersConfirmations}: ${confirmedCount}/${required}`) - ), - div( - { class: "card-comments-summary feed-opinions-section" }, - div( - { class: "comments-count" }, - span({ class: "card-label" }, (i18n.opinionsTitle || "Opinions") + ": "), - span({ class: "card-value" }, String(Object.values(transfer.opinions || {}).reduce((s, n) => s + (Number(n) || 0), 0))) - ), - renderOpinionsVoting('/transfers/opinions', transfer.id, transfer.opinions, returnTo, transfer.opinions_inhabitants) - ), - div({ class: "card-spread-centered" }, renderSpreadButton(transfer.id, params.spreadMap && params.spreadMap.get(transfer.id))) + ? div({ class: "tribe-card-members" }, span({ class: "price-chip" }, fmtAmountWithUnit(transfer))) + : null ) ) } +const isUbiTagged = (t) => safeArr(t.tags).some(tag => String(tag).toUpperCase() === "UBI") + +const trChipFor = (normalizedFilter, censusT) => (mode) => { + if (mode === normalizedFilter) return true + if (!Array.isArray(censusT)) return true; + const stT = (t) => String(t.status || "").toUpperCase() + if (mode === "mine") return censusT.some(t => t.from === userId || t.to === userId) + if (mode === "recent") return censusT.length > 0 + if (mode === "mine-pending") return censusT.some(t => (t.from === userId || t.to === userId) && String(t.status || "").toUpperCase() === "UNCONFIRMED") + if (mode === "mine-confirmed") return censusT.some(t => (t.from === userId || t.to === userId) && String(t.status || "").toUpperCase() === "CLOSED") + if (mode === "ubi") return censusT.some(t => safeArr(t.tags).some(tag => String(tag).toUpperCase() === "UBI")) + if (mode === "economic") return censusT.some(t => categoryOf(t) === "ECONOMIC") + if (mode === "time") return censusT.some(t => categoryOf(t) === "TIME") + if (mode === "trust") return censusT.some(t => categoryOf(t) === "TRUST") + if (mode === "market") return censusT.some(t => !isUbiTagged(t)) + if (mode === "pending") return censusT.some(t => stT(t) === "UNCONFIRMED" && t.to === userId && !safeArr(t.confirmedBy).includes(userId)) + if (mode === "unconfirmed") return censusT.some(t => stT(t) === "UNCONFIRMED") + if (mode === "closed" || mode === "top") return censusT.some(t => stT(t) === "CLOSED") + if (mode === "discarded") return censusT.some(t => stT(t) === "DISCARDED") + return true +} + exports.transferView = async (transfers, filter, transferId, params = {}) => { const normalizedFilter = filter === "favs" ? "all" : (filter || "all") - const title = - normalizedFilter === "mine" ? i18n.transfersMineSectionTitle : - normalizedFilter === "ubi" ? i18n.transfersUBISectionTitle : - normalizedFilter === "pending" ? i18n.transfersPendingSectionTitle : - normalizedFilter === "top" ? i18n.transfersTopSectionTitle : - normalizedFilter === "unconfirmed" ? i18n.transfersUnconfirmedSectionTitle : - normalizedFilter === "closed" ? i18n.transfersClosedSectionTitle : - normalizedFilter === "discarded" ? i18n.transfersDiscardedSectionTitle : - normalizedFilter === "create" ? i18n.transfersCreateSectionTitle : - normalizedFilter === "edit" ? i18n.transfersUpdateSectionTitle : - normalizedFilter === "economic" ? (i18n.transfersEconomicSectionTitle || (i18n.transfersCategoryEconomic + " " + i18n.transfersTitle)) : - normalizedFilter === "time" ? (i18n.transfersTimeSectionTitle || (i18n.transfersCategoryTime + " " + i18n.transfersTitle)) : - normalizedFilter === "trust" ? (i18n.transfersTrustSectionTitle || (i18n.transfersCategoryTrust + " " + i18n.transfersTitle)) : - i18n.transfersAllSectionTitle + const title = i18n.transfersTitle const q = safeText(params.q || "") const minAmountRaw = params.minAmount ?? "" @@ -234,7 +231,10 @@ exports.transferView = async (transfers, filter, transferId, params = {}) => { const list = safeArr(transfers) let filtered = + normalizedFilter === "recent" ? list.filter(t => (Date.parse(t.createdAt || "") || 0) >= Date.now() - 86400000) : normalizedFilter === "mine" ? list.filter(t => t.from === userId || t.to === userId) : + normalizedFilter === "mine-pending" ? list.filter(t => (t.from === userId || t.to === userId) && String(t.status || "").toUpperCase() === "UNCONFIRMED") : + normalizedFilter === "mine-confirmed" ? list.filter(t => (t.from === userId || t.to === userId) && String(t.status || "").toUpperCase() === "CLOSED") : normalizedFilter === "ubi" ? list.filter(t => safeArr(t.tags).some(tag => String(tag).toUpperCase() === "UBI")) : normalizedFilter === "pending" ? list.filter(t => String(t.status || "").toUpperCase() === "UNCONFIRMED" && t.to === userId && !safeArr(t.confirmedBy).includes(userId)) : normalizedFilter === "top" ? list.filter(t => String(t.status || "").toUpperCase() === "CLOSED") : @@ -244,7 +244,7 @@ exports.transferView = async (transfers, filter, transferId, params = {}) => { normalizedFilter === "economic" ? list.filter(t => categoryOf(t) === "ECONOMIC") : normalizedFilter === "time" ? list.filter(t => categoryOf(t) === "TIME") : normalizedFilter === "trust" ? list.filter(t => categoryOf(t) === "TRUST") : - normalizedFilter === "market" ? list : + normalizedFilter === "market" ? list.filter(t => !isUbiTagged(t)) : list if (q) { @@ -283,32 +283,45 @@ exports.transferView = async (transfers, filter, transferId, params = {}) => { selectedCategory === "TRUST" ? (i18n.transfersUnitTrust || "trust") : (i18n.transfersUnitEco || "ECO") + const emptyMod = moduleIsEmpty(list, normalizedFilter, "all", q || String(minAmountRaw || "") || String(maxAmountRaw || "")); + const censusT = Array.isArray(params.censusList) ? params.censusList : list + const stT = (t) => String(t.status || "").toUpperCase() + const trChip = trChipFor(normalizedFilter, censusT) return template( title, section( - div({ class: "tags-header" }, h2(title), p(i18n.transfersDescription)), + div({ class: "tags-header module-header-line" }, h2(title), p(i18n.transfersDescription), renderWalletChip()), div( { class: "filters" }, form( { method: "GET", action: "/transfers", class: "ui-toolbar ui-toolbar--filters" }, - input({ type: "hidden", name: "q", value: q }), input({ type: "hidden", name: "minAmount", value: String(minAmountRaw ?? "") }), input({ type: "hidden", name: "maxAmount", value: String(maxAmountRaw ?? "") }), input({ type: "hidden", name: "sort", value: sort }), - button({ type: "submit", name: "filter", value: "all", class: normalizedFilter === "all" ? "filter-btn active" : "filter-btn" }, i18n.transfersFilterAll), - button({ type: "submit", name: "filter", value: "mine", class: normalizedFilter === "mine" ? "filter-btn active" : "filter-btn" }, i18n.transfersFilterMine), - button({ type: "submit", name: "filter", value: "ubi", class: normalizedFilter === "ubi" ? "filter-btn active" : "filter-btn" }, i18n.transfersFilterUBI), - button({ type: "submit", name: "filter", value: "economic", class: normalizedFilter === "economic" ? "filter-btn active" : "filter-btn" }, i18n.transfersFilterEconomic || (i18n.transfersCategoryEconomic || "ECONOMIC")), - button({ type: "submit", name: "filter", value: "time", class: normalizedFilter === "time" ? "filter-btn active" : "filter-btn" }, i18n.transfersFilterTime || (i18n.transfersCategoryTime || "TIME")), - button({ type: "submit", name: "filter", value: "trust", class: normalizedFilter === "trust" ? "filter-btn active" : "filter-btn" }, i18n.transfersFilterTrust || (i18n.transfersCategoryTrust || "TRUST")), - button({ type: "submit", name: "filter", value: "market", class: normalizedFilter === "market" ? "filter-btn active" : "filter-btn" }, i18n.transfersFilterMarket), - button({ type: "submit", name: "filter", value: "pending", class: normalizedFilter === "pending" ? "filter-btn active" : "filter-btn" }, i18n.transfersFilterPending), - button({ type: "submit", name: "filter", value: "unconfirmed", class: normalizedFilter === "unconfirmed" ? "filter-btn active" : "filter-btn" }, i18n.transfersFilterUnconfirmed), - button({ type: "submit", name: "filter", value: "closed", class: normalizedFilter === "closed" ? "filter-btn active" : "filter-btn" }, i18n.transfersFilterClosed), - button({ type: "submit", name: "filter", value: "discarded", class: normalizedFilter === "discarded" ? "filter-btn active" : "filter-btn" }, i18n.transfersFilterDiscarded), - button({ type: "submit", name: "filter", value: "top", class: normalizedFilter === "top" ? "filter-btn active" : "filter-btn" }, i18n.transfersFilterTop), + ...(emptyMod ? [] : [ + button({ type: "submit", name: "filter", value: "all", class: normalizedFilter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.transfersFilterAll).toUpperCase()), + ...(trChip("recent") ? [button({ type: "submit", name: "filter", value: "recent", class: normalizedFilter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.transfersFilterRecent).toUpperCase())] : []), + ...(trChip("mine") ? [button({ type: "submit", name: "filter", value: "mine", class: normalizedFilter === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.transfersFilterMine).toUpperCase())] : []), + ...(trChip("ubi") ? [button({ type: "submit", name: "filter", value: "ubi", class: normalizedFilter === "ubi" ? "filter-btn active" : "filter-btn" }, String(i18n.transfersFilterUBI).toUpperCase())] : []), + ...(trChip("economic") ? [button({ type: "submit", name: "filter", value: "economic", class: normalizedFilter === "economic" ? "filter-btn active" : "filter-btn" }, i18n.transfersFilterEconomic || (i18n.transfersCategoryEconomic || "ECONOMIC"))] : []), + ...(trChip("time") ? [button({ type: "submit", name: "filter", value: "time", class: normalizedFilter === "time" ? "filter-btn active" : "filter-btn" }, i18n.transfersFilterTime || (i18n.transfersCategoryTime || "TIME"))] : []), + ...(trChip("trust") ? [button({ type: "submit", name: "filter", value: "trust", class: normalizedFilter === "trust" ? "filter-btn active" : "filter-btn" }, i18n.transfersFilterTrust || (i18n.transfersCategoryTrust || "TRUST"))] : []), + ...(trChip("market") ? [button({ type: "submit", name: "filter", value: "market", class: normalizedFilter === "market" ? "filter-btn active" : "filter-btn" }, String(i18n.transfersFilterMarket).toUpperCase())] : []), + ...(trChip("pending") ? [button({ type: "submit", name: "filter", value: "pending", class: normalizedFilter === "pending" ? "filter-btn active" : "filter-btn" }, String(i18n.transfersFilterPending).toUpperCase())] : []), + ...(trChip("unconfirmed") ? [button({ type: "submit", name: "filter", value: "unconfirmed", class: normalizedFilter === "unconfirmed" ? "filter-btn active" : "filter-btn" }, String(i18n.transfersFilterUnconfirmed).toUpperCase())] : []), + ...(trChip("closed") ? [button({ type: "submit", name: "filter", value: "closed", class: normalizedFilter === "closed" ? "filter-btn active" : "filter-btn" }, String(i18n.transfersFilterClosed).toUpperCase())] : []), + ...(trChip("discarded") ? [button({ type: "submit", name: "filter", value: "discarded", class: normalizedFilter === "discarded" ? "filter-btn active" : "filter-btn" }, String(i18n.transfersFilterDiscarded).toUpperCase())] : []), + ...(trChip("top") ? [button({ type: "submit", name: "filter", value: "top", class: normalizedFilter === "top" ? "filter-btn active" : "filter-btn" }, String(i18n.transfersFilterTop).toUpperCase())] : []), + ]), button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.transfersCreateButton) - ) + ), + ["mine", "mine-pending", "mine-confirmed"].includes(normalizedFilter) + ? div({ class: "transfers-subfilters" }, + span({ class: "activity-subchip-label" }, "\u21b3"), + ...[["mine", i18n.transfersFilterAll], ["mine-pending", i18n.transfersFilterMinePending], ["mine-confirmed", i18n.transfersFilterMineConfirmed]] + .map(([mode, label]) => a({ href: `/transfers?filter=${mode}`, class: normalizedFilter === mode ? "activity-chip active" : "activity-chip" }, String(label).toUpperCase())) + ) + : null ) ), section( @@ -321,15 +334,16 @@ exports.transferView = async (transfers, filter, transferId, params = {}) => { input({ type: "hidden", name: "filter", value: "create" }), label(i18n.transfersCategory || "Category"), br(), - select( - { name: "category", class: "transfer-category-select" }, - option({ value: "ECONOMIC", selected: selectedCategory === "ECONOMIC" ? "selected" : undefined }, i18n.transfersCategoryEconomic || "Economic"), - option({ value: "TIME", selected: selectedCategory === "TIME" ? "selected" : undefined }, i18n.transfersCategoryTime || "Time"), - option({ value: "TRUST", selected: selectedCategory === "TRUST" ? "selected" : undefined }, i18n.transfersCategoryTrust || "Trust") + div({ class: "apply-row" }, + select( + { name: "category", class: "transfer-category-select" }, + option({ value: "ECONOMIC", selected: selectedCategory === "ECONOMIC" ? "selected" : undefined }, i18n.transfersCategoryEconomic || "Economic"), + option({ value: "TIME", selected: selectedCategory === "TIME" ? "selected" : undefined }, i18n.transfersCategoryTime || "Time"), + option({ value: "TRUST", selected: selectedCategory === "TRUST" ? "selected" : undefined }, i18n.transfersCategoryTrust || "Trust") + ), + button({ type: "submit", class: "create-button" }, i18n.apply || "Apply") ), - " ", - button({ type: "submit", class: "filter-btn" }, i18n.transfersCategoryApply || "Apply"), - br(), br() + hr({ class: "form-sep" }) ) : null, form( @@ -363,6 +377,7 @@ exports.transferView = async (transfers, filter, transferId, params = {}) => { label(`${i18n.transfersAmount} (${amountUnitLabel})`), br(), input({ type: "number", name: "amount", step: "0.000001", required: true, min: "0.000001", value: transferToEdit.amount || "" }), + renderEcoValueChip(), br(), br() ], label(i18n.transfersDeadline), @@ -379,19 +394,16 @@ exports.transferView = async (transfers, filter, transferId, params = {}) => { ) ) : section( - div( - { class: "transfers-search" }, + emptyMod ? null : div( + { class: "transfers-search activity-filter-chips activity-toolbar-row" }, + renderModuleStatsBy(filtered, t => String(t.status || '').toUpperCase(), [{ value: 'UNCONFIRMED', label: i18n.transfersFilterUnconfirmed }, { value: 'CLOSED', label: i18n.transfersFilterClosed }, { value: 'DISCARDED', label: i18n.transfersFilterDiscarded }]), form( { method: "GET", action: "/transfers", class: "filter-box" }, input({ type: "hidden", name: "filter", value: normalizedFilter || "all" }), input({ type: "text", name: "q", value: q, placeholder: i18n.transfersSearchPlaceholder, class: "filter-box__input" }), - div( - { class: "filter-box__controls" }, - div( - { class: "transfer-range" }, - input({ type: "number", name: "minAmount", step: "0.000001", min: "0", value: String(minAmountRaw ?? ""), placeholder: i18n.transfersMinAmountLabel, class: "filter-box__number transfer-amount-input" }), - input({ type: "number", name: "maxAmount", step: "0.000001", min: "0", value: String(maxAmountRaw ?? ""), placeholder: i18n.transfersMaxAmountLabel, class: "filter-box__number transfer-amount-input" }) - ), + input({ type: "number", name: "minAmount", step: "0.000001", min: "0", value: String(minAmountRaw ?? ""), placeholder: i18n.transfersMinAmountLabel, class: "filter-box__number transfer-amount-input" }), + input({ type: "number", name: "maxAmount", step: "0.000001", min: "0", value: String(maxAmountRaw ?? ""), placeholder: i18n.transfersMaxAmountLabel, class: "filter-box__number transfer-amount-input" }), + select( { name: "sort", class: "filter-box__select" }, option({ value: "recent", selected: sort === "recent" ? "selected" : undefined }, i18n.transfersSortRecent), @@ -399,52 +411,52 @@ exports.transferView = async (transfers, filter, transferId, params = {}) => { option({ value: "deadline", selected: sort === "deadline" ? "selected" : undefined }, i18n.transfersSortDeadline) ), button({ type: "submit", class: "filter-box__button" }, i18n.transfersSearchButton) - ) + ) ), - br(), filtered.length - ? div({ class: "jobs-grid" }, - filtered.map(t => generateTransferCard(t, normalizedFilter, { q, minAmount: minAmountRaw, maxAmount: maxAmountRaw, sort, spreadMap: params.spreadMap })) + ? div({ class: "jobs-grid transfers-grid" }, + paged(filtered).map(t => generateTransferCard(t, normalizedFilter, { q, minAmount: minAmountRaw, maxAmount: maxAmountRaw, sort, spreadMap: params.spreadMap })) ) - : p(q || String(minAmountRaw) || String(maxAmountRaw) ? i18n.transfersNoMatch : i18n.transfersNoItems) + : div({ class: "no-content-box" }, p(q || String(minAmountRaw) || String(maxAmountRaw) ? i18n.transfersNoMatch : i18n.transfersNoItems)) ) ) ) } exports.singleTransferView = async (transfer, filter, params = {}) => { + const trChip = (mode) => trChipFor(normalizedFilter, Array.isArray(params.censusList) ? params.censusList : null)(mode) const normalizedFilter = filter === "favs" ? "all" : (filter || "all") const q = safeText(params.q || "") const sort = safeText(params.sort || "recent") const returnTo = safeText(params.returnTo) || buildReturnTo(normalizedFilter, { ...params, q, sort }) const confirmedBy = safeArr(transfer.confirmedBy) - const required = transfer.from === transfer.to ? 1 : 2 + const settledUbiTx = (Array.isArray(transfer.tags) ? transfer.tags : []).some(t => String(t).toUpperCase() === "UBI") && /^[0-9a-f]{64}$/i.test(String(transfer.txid || "")) + const required = (transfer.from === transfer.to || settledUbiTx) ? Math.max(1, Math.min(confirmedBy.length, 2)) : 2 const confirmedCount = confirmedBy.length const isUnconfirmed = String(transfer.status || "").toUpperCase() === "UNCONFIRMED" const dl = transfer.deadline ? moment(transfer.deadline) : null const isExpired = dl && dl.isValid() ? dl.isBefore(moment()) : false const tags = Array.isArray(transfer.tags) ? transfer.tags.map(t => String(t).toUpperCase()) : [] const isUbi = tags.includes("UBI") - const showConfirm = isUnconfirmed && transfer.to === userId && !confirmedBy.includes(userId) && !isExpired && !tags.includes("PENDING") + const settledUbi = isUbi && /^[0-9a-f]{64}$/i.test(String(transfer.txid || "")) + const showConfirm = isUnconfirmed && transfer.to === userId && !confirmedBy.includes(userId) && !isExpired && !tags.includes("PENDING") && !settledUbi const tagsNode = renderTags(transfer.tags) const cat = categoryOf(transfer) const otherParty = transfer.from === userId ? transfer.to : transfer.from const chips = [ renderTransferStatusChip(transfer.status), - isUbi ? renderStateChip("mutuals", "🎁", "UBI") : null, + isUbi ? renderStateChip("mutuals", "", "UBI") : null, isExpired ? renderStateChip("closed", "⏰", i18n.transfersExpiredBadge) : null, renderLifespanChip(transfer.lifetime, i18n) ].filter(Boolean) const sideActions = [] - if (otherParty && String(otherParty) !== String(userId)) { - sideActions.push(form({ method: "GET", action: "/pm" }, - input({ type: "hidden", name: "recipients", value: otherParty }), - button({ type: "submit", class: "filter-btn" }, i18n.privateMessage) - )) + const isPayer = String(transfer.from) === String(userId) + if (isPayer && String(transfer.status || "").toUpperCase() === "UNCONFIRMED" && getConfig().modules.walletMod === "on") { + sideActions.push(a({ href: `/wallet/send?transfer=${encodeURIComponent(transfer.id)}`, class: "filter-btn" }, i18n.transfersPayWithWallet)) } if (showConfirm) { sideActions.push(form({ method: "POST", action: `/transfers/confirm/${encodeURIComponent(transfer.id)}` }, @@ -458,14 +470,16 @@ exports.singleTransferView = async (transfer, filter, params = {}) => { input({ type: "hidden", name: "returnTo", value: returnTo }), button({ type: "submit", class: "update-btn" }, i18n.transfersUpdateButton) )) - sideActions.push(form({ method: "POST", action: `/transfers/delete/${encodeURIComponent(transfer.id)}` }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - button({ type: "submit", class: "delete-btn" }, i18n.transfersDeleteButton) - )) } - sideActions.push(form({ method: "GET", action: `/transfers/contract/${encodeURIComponent(transfer.id)}`, class: "transfer-contract-form" }, - button({ type: "submit", class: "filter-btn" }, i18n.transfersExportContract || 'Create Contract') - )) + const canDelete = canEdit && !isExpired && confirmedCount < required + const exportActions = div({ class: "doc-export-actions" }, + form({ method: "GET", action: `/transfers/contract/${encodeURIComponent(transfer.id)}`, class: "transfer-contract-form" }, + button({ type: "submit", class: "filter-btn" }, i18n.transfersExportContract) + ), + form({ method: "POST", action: `/transfers/${encodeURIComponent(transfer.id)}/share` }, + button({ type: "submit", class: "filter-btn" }, i18n.sharePm) + ) + ) const infoRows = [] const pushRow = (labelText, valueNode) => @@ -477,15 +491,26 @@ exports.singleTransferView = async (transfer, filter, params = {}) => { pushRow(i18n.transfersTo, userLink(transfer.to)) if (cat !== "TRUST") pushRow(i18n.transfersAmount, fmtAmountWithUnit(transfer)) pushRow(i18n.transfersCategory || "Category", categoryLabel(cat)) - if (!isUbi) pushRow(i18n.transfersDeadline, dl && dl.isValid() ? dl.format("YYYY-MM-DD HH:mm") : "") + if (!isUbi) pushRow(i18n.transfersDeadline, dl && dl.isValid() ? dl.format("YYYY/MM/DD HH:mm") : "") pushRow(i18n.transfersStatus, i18n[statusKey(transfer.status)] || String(transfer.status || "")) const transferSide = div({ class: "tribe-side" }, + div({ class: "card-header activity-card-header" }, + renderContentActions(transfer.id, `/transfers/${encodeURIComponent(transfer.id)}`, { + spread: params.spreads || null, + author: transfer.from, + favKind: 'transfers', + isFavorite: transfer.isFavorite, + returnTo, + reportTitle: transfer.concept, + deleteAction: canDelete ? contentDeleteAction('transfer', transfer.id) : undefined + }) + ), div({ class: "shop-title-row" }, - h2({ class: "tribe-card-title" }, transfer.concept || i18n.transfersTitle) + h2({ class: "tribe-card-title" }, transferTitle(transfer)) ), chips.length ? div({ class: "card-chips-row" }, ...chips) : null, - div({ class: "card-spread-centered" }, renderSpreadButton(transfer.id, params.spreads)), + table({ class: "tribe-info-table" }, ...renderCreatedRows(transfer.createdAt, transfer.from, transfer.updatedAt)), tagsNode, div({ class: "tribe-card-members" }, span({ class: "tribe-members-count" }, `${i18n.transfersConfirmations}: ${confirmedCount}/${required}`) @@ -493,74 +518,61 @@ exports.singleTransferView = async (transfer, filter, params = {}) => { params.block && params.block.id ? div({ class: "card-field" }, span({ class: "card-label" }, `${i18n.blockchainBlockID || "Block ID"}: `), - span({ class: "card-value" }, a({ class: "user-link", href: `/blockexplorer/block/${encodeURIComponent(params.block.id)}` }, params.block.id)) + span({ class: "card-value" }, process.env.OASIS_MOBILE === '1' ? params.block.id : a({ class: "user-link", href: `/blockexplorer/block/${encodeURIComponent(params.block.id)}` }, params.block.id)) ) : null, - sideActions.length ? div({ class: "tribe-side-actions" }, ...sideActions) : null + sideActions.length ? div({ class: "tribe-side-actions" }, ...sideActions) : null, + exportActions ) + const confirmedList = Array.isArray(transfer.confirmedBy) ? transfer.confirmedBy.filter(Boolean) : [] + const transferMain = div({ class: "tribe-main" }, div({ class: "job-section" }, - div({ class: "card-field" }, - span({ class: "card-label" }, `${i18n.transfersFrom}: `), - span({ class: "card-value" }, userLink(transfer.from)) - ), - div({ class: "card-field" }, - span({ class: "card-label" }, `${i18n.transfersTo}: `), - span({ class: "card-value" }, userLink(transfer.to)) - ), - cat !== "TRUST" ? div({ class: "card-field" }, - span({ class: "card-label" }, `${i18n.transfersAmount}: `), - span({ class: "card-value card-salary" }, fmtAmountWithUnit(transfer)) - ) : null, - div({ class: "card-field" }, - span({ class: "card-label" }, `${i18n.transfersCategory || "Category"}: `), - span({ class: "card-value" }, categoryLabel(cat)) - ), - div({ class: "card-field" }, - span({ class: "card-label" }, `${i18n.transfersConcept}: `), - span({ class: "card-value" }, transfer.concept || "") - ), - !isUbi && dl && dl.isValid() ? div({ class: "card-field" }, - span({ class: "card-label" }, `${i18n.transfersDeadline}: `), - span({ class: "card-value" }, dl.format("YYYY-MM-DD HH:mm")) - ) : null, - div({ class: "card-field" }, - span({ class: "card-label" }, `${i18n.transfersStatus}: `), - span({ class: "card-value" }, i18n[statusKey(transfer.status)] || String(transfer.status || "")) - ) + h2({ class: "job-section-title" }, i18n.transfersConcept), + p({ class: "tribe-side-description" }, transfer.concept || "") ), - p({ class: "card-footer" }, - span({ class: "date-link" }, `${moment(transfer.createdAt).format("YYYY-MM-DD HH:mm")} ${i18n.performed} `), - userLink(transfer.from), - renderUpdatedLabel(transfer.createdAt, transfer.updatedAt) + div({ class: "job-section" }, + h2({ class: "job-section-title" }, i18n.transfersConfirmations), + p({ class: "tribe-side-description" }, `${confirmedCount}/${required}`), + confirmedList.length + ? div({ class: "card-assigned-list" }, ...confirmedList.map(id => userLink(id))) + : null ), - renderOpinionsVoting('/transfers/opinions', transfer.id, transfer.opinions, returnTo, transfer.opinions_inhabitants) + renderEngagement(transfer.id, + renderOpinionsVoting('/transfers/opinions', transfer.id, transfer.opinions, returnTo, transfer.opinions_inhabitants), + renderSharedCommentsSection({ + action: `/transfers/${encodeURIComponent(transfer.id)}/comments`, + comments: params.comments || [], + returnTo + }) + ) ) return template( transfer.concept, + section(div({ class: "tags-header module-header-line" }, h2(i18n.transfersTitle), p(i18n.transfersDescription), renderWalletChip())), section( div( { class: "filters" }, form( { method: "GET", action: "/transfers", class: "ui-toolbar ui-toolbar--filters" }, - input({ type: "hidden", name: "q", value: q }), input({ type: "hidden", name: "minAmount", value: String(params.minAmount ?? "") }), input({ type: "hidden", name: "maxAmount", value: String(params.maxAmount ?? "") }), input({ type: "hidden", name: "sort", value: sort }), - button({ type: "submit", name: "filter", value: "all", class: normalizedFilter === "all" ? "filter-btn active" : "filter-btn" }, i18n.transfersFilterAll), - button({ type: "submit", name: "filter", value: "mine", class: normalizedFilter === "mine" ? "filter-btn active" : "filter-btn" }, i18n.transfersFilterMine), - button({ type: "submit", name: "filter", value: "ubi", class: normalizedFilter === "ubi" ? "filter-btn active" : "filter-btn" }, i18n.transfersFilterUBI), - button({ type: "submit", name: "filter", value: "economic", class: normalizedFilter === "economic" ? "filter-btn active" : "filter-btn" }, i18n.transfersFilterEconomic || (i18n.transfersCategoryEconomic || "ECONOMIC")), - button({ type: "submit", name: "filter", value: "time", class: normalizedFilter === "time" ? "filter-btn active" : "filter-btn" }, i18n.transfersFilterTime || (i18n.transfersCategoryTime || "TIME")), - button({ type: "submit", name: "filter", value: "trust", class: normalizedFilter === "trust" ? "filter-btn active" : "filter-btn" }, i18n.transfersFilterTrust || (i18n.transfersCategoryTrust || "TRUST")), - button({ type: "submit", name: "filter", value: "market", class: normalizedFilter === "market" ? "filter-btn active" : "filter-btn" }, i18n.transfersFilterMarket), - button({ type: "submit", name: "filter", value: "pending", class: normalizedFilter === "pending" ? "filter-btn active" : "filter-btn" }, i18n.transfersFilterPending), - button({ type: "submit", name: "filter", value: "unconfirmed", class: normalizedFilter === "unconfirmed" ? "filter-btn active" : "filter-btn" }, i18n.transfersFilterUnconfirmed), - button({ type: "submit", name: "filter", value: "closed", class: normalizedFilter === "closed" ? "filter-btn active" : "filter-btn" }, i18n.transfersFilterClosed), - button({ type: "submit", name: "filter", value: "discarded", class: normalizedFilter === "discarded" ? "filter-btn active" : "filter-btn" }, i18n.transfersFilterDiscarded), - button({ type: "submit", name: "filter", value: "top", class: normalizedFilter === "top" ? "filter-btn active" : "filter-btn" }, i18n.transfersFilterTop), + button({ type: "submit", name: "filter", value: "all", class: normalizedFilter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.transfersFilterAll).toUpperCase()), + ...(trChip("recent") ? [button({ type: "submit", name: "filter", value: "recent", class: normalizedFilter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.transfersFilterRecent).toUpperCase())] : []), + ...(trChip("mine") ? [button({ type: "submit", name: "filter", value: "mine", class: normalizedFilter === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.transfersFilterMine).toUpperCase())] : []), + ...(trChip("ubi") ? [button({ type: "submit", name: "filter", value: "ubi", class: normalizedFilter === "ubi" ? "filter-btn active" : "filter-btn" }, String(i18n.transfersFilterUBI).toUpperCase())] : []), + ...(trChip("economic") ? [button({ type: "submit", name: "filter", value: "economic", class: normalizedFilter === "economic" ? "filter-btn active" : "filter-btn" }, i18n.transfersFilterEconomic || (i18n.transfersCategoryEconomic || "ECONOMIC"))] : []), + ...(trChip("time") ? [button({ type: "submit", name: "filter", value: "time", class: normalizedFilter === "time" ? "filter-btn active" : "filter-btn" }, i18n.transfersFilterTime || (i18n.transfersCategoryTime || "TIME"))] : []), + ...(trChip("trust") ? [button({ type: "submit", name: "filter", value: "trust", class: normalizedFilter === "trust" ? "filter-btn active" : "filter-btn" }, i18n.transfersFilterTrust || (i18n.transfersCategoryTrust || "TRUST"))] : []), + ...(trChip("market") ? [button({ type: "submit", name: "filter", value: "market", class: normalizedFilter === "market" ? "filter-btn active" : "filter-btn" }, String(i18n.transfersFilterMarket).toUpperCase())] : []), + ...(trChip("pending") ? [button({ type: "submit", name: "filter", value: "pending", class: normalizedFilter === "pending" ? "filter-btn active" : "filter-btn" }, String(i18n.transfersFilterPending).toUpperCase())] : []), + ...(trChip("unconfirmed") ? [button({ type: "submit", name: "filter", value: "unconfirmed", class: normalizedFilter === "unconfirmed" ? "filter-btn active" : "filter-btn" }, String(i18n.transfersFilterUnconfirmed).toUpperCase())] : []), + ...(trChip("closed") ? [button({ type: "submit", name: "filter", value: "closed", class: normalizedFilter === "closed" ? "filter-btn active" : "filter-btn" }, String(i18n.transfersFilterClosed).toUpperCase())] : []), + ...(trChip("discarded") ? [button({ type: "submit", name: "filter", value: "discarded", class: normalizedFilter === "discarded" ? "filter-btn active" : "filter-btn" }, String(i18n.transfersFilterDiscarded).toUpperCase())] : []), + button({ type: "submit", name: "filter", value: "top", class: normalizedFilter === "top" ? "filter-btn active" : "filter-btn" }, String(i18n.transfersFilterTop).toUpperCase()), button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.transfersCreateButton) ) ), diff --git a/src/views/trending_view.js b/src/views/trending_view.js index cbdb142..9a6a518 100644 --- a/src/views/trending_view.js +++ b/src/views/trending_view.js @@ -1,28 +1,34 @@ const { div, h2, p, section, button, form, a, textarea, br, input, table, tr, th, td, img, video: videoHyperaxe, audio: audioHyperaxe, span, details, summary} = require("../server/node_modules/hyperaxe"); -const { template, i18n, userLink, renderSpreadButton, renderContentActions} = require('./main_views'); -const { renderTextWithStyles } = require('../backend/renderTextWithStyles'); +const RAW_HTML = Symbol.for('oasis.rawHtml'); +const moment = require("../server/node_modules/moment"); +const { template, i18n, userLink, renderSpreadButton, renderContentActions, renderVotesSummary, renderModuleStats, renderCardMetaRow, renderTorrentDownload, renderTorrentSourceDownload, renderFileDownloads, torrentDownloadHref, CONTENT_FAV_KIND, CONTENT_TORRENTABLE, paged } = require('./main_views'); +const { renderStyledHtml, safeExternalHref } = require('../backend/renderStyledText'); +const { renderZoomableImage } = require('./gallery_view'); const { config } = require('../server/SSB_server.js'); -const { renderUrl } = require('../backend/renderUrl'); +const { renderStyledText } = require('../backend/renderStyledText'); const opinionCategories = require('../backend/opinion_categories'); const { sanitizeHtml } = require('../backend/sanitizeHtml'); const userId = config.keys.id; +const capFirst = (s) => String(s || '').charAt(0).toUpperCase() + String(s || '').slice(1); +const typeLabel = (type) => i18n['type' + capFirst(type)] || type; + const filterButton = (mode, currentFilter) => form({ method: 'GET', action: '/trending' }, input({ type: 'hidden', name: 'filter', value: mode }), button( { type: 'submit', class: currentFilter === mode ? 'filter-btn active' : 'filter-btn' }, - i18n[mode + 'Button'] || mode + String(i18n[mode + 'Button'] || typeLabel(mode)).toUpperCase() ) ); const voteLabelFor = (cat) => i18n['vote' + cat.charAt(0).toUpperCase() + cat.slice(1)] || cat; -const renderTrendingCard = (item, votes, categories, seenTitles, spreadMap = new Map()) => { +const renderTrendingCard = (item, votes, categories, seenTitles, spreadMap = new Map(), params = {}) => { const c = item.value.content; - const created = new Date(item.value.timestamp).toLocaleString(); + const created = moment(item.value.timestamp).format("YYYY/MM/DD HH:mm"); let contentHtml; @@ -30,25 +36,24 @@ const renderTrendingCard = (item, votes, categories, seenTitles, spreadMap = new const { url, description, lastVisit } = c; contentHtml = div({ class: 'trending-bookmark' }, div({ class: 'card-section bookmark' }, - url ? h2(p(a({ href: url, target: '_blank', class: "bookmark-url" }, url))) : "", + url ? h2(p(a({ href: safeExternalHref(url), target: '_blank', rel: 'noopener noreferrer', class: "bookmark-url" }, url))) : "", lastVisit ? div( { class: 'card-field' }, span({ class: 'card-label' }, i18n.bookmarkLastVisitLabel + ':'), - span({ class: 'card-value' }, new Date(lastVisit).toLocaleString()) + span({ class: 'card-value' }, moment(lastVisit).format("YYYY/MM/DD HH:mm")) ) : "", - description ? [span({ class: 'card-label' }, i18n.bookmarkDescriptionLabel + ":"), p(...renderUrl(description))] : null + description ? [span({ class: 'card-label' }, i18n.bookmarkDescriptionLabel + ":"), p(...renderStyledText(description))] : null ) ); } else if (c.type === 'image') { - const { url, title, description, meme } = c; + const { url, title, description } = c; contentHtml = div({ class: 'trending-image' }, div({ class: 'card-section image' }, title ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.imageTitleLabel + ':'), span({ class: 'card-value' }, title)) : "", - description ? [span({ class: 'card-label' }, i18n.imageDescriptionLabel + ":"), p(...renderUrl(description))] : null, - meme ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.trendingCategory + ':'), span({ class: 'card-value' }, i18n.meme)) : "", - div({ class: 'card-field' }, img({ src: `/blob/${encodeURIComponent(url)}`, class: 'feed-image' })) + description ? [span({ class: 'card-label' }, i18n.imageDescriptionLabel + ":"), p(...renderStyledText(description))] : null, + div({ class: 'card-field image-container' }, renderZoomableImage(`/blob/${encodeURIComponent(url)}`, { imgClass: 'post-image' })) ) ); } else if (c.type === 'audio') { @@ -56,9 +61,9 @@ const renderTrendingCard = (item, votes, categories, seenTitles, spreadMap = new contentHtml = div({ class: 'trending-audio' }, div({ class: 'card-section audio' }, title?.trim() ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.audioTitleLabel + ':'), span({ class: 'card-value' }, title)) : "", - description ? [span({ class: 'card-label' }, i18n.audioDescriptionLabel + ":"), p(...renderUrl(description))] : null, + description ? [span({ class: 'card-label' }, i18n.audioDescriptionLabel + ":"), p(...renderStyledText(description))] : null, url - ? div({ class: 'card-field audio-container' }, audioHyperaxe({ controls: true, src: `/blob/${encodeURIComponent(url)}`, type: mimeType })) + ? div({ class: 'card-field audio-container' }, audioHyperaxe({ controls: true, class: 'post-audio', src: `/blob/${encodeURIComponent(url)}`, type: mimeType })) : div({ class: 'card-field' }, p(i18n.audioNoFile)) ) ); @@ -67,21 +72,32 @@ const renderTrendingCard = (item, votes, categories, seenTitles, spreadMap = new contentHtml = div({ class: 'trending-video' }, div({ class: 'card-section video' }, title?.trim() ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.videoTitleLabel + ':'), span({ class: 'card-value' }, title)) : "", - description ? [span({ class: 'card-label' }, i18n.videoDescriptionLabel + ":"), p(...renderUrl(description))] : null, + description ? [span({ class: 'card-label' }, i18n.videoDescriptionLabel + ":"), p(...renderStyledText(description))] : null, br(), url - ? div({ class: 'card-field video-container' }, videoHyperaxe({ controls: true, src: `/blob/${encodeURIComponent(url)}`, type: mimeType, preload: 'metadata', width: '640', height: '360' })) + ? div({ class: 'card-field video-container' }, videoHyperaxe({ controls: true, class: 'post-video', src: `/blob/${encodeURIComponent(url)}`, type: mimeType, preload: 'metadata' })) : div({ class: 'card-field' }, p(i18n.videoNoFile)) ) ); + } else if (c.type === 'file') { + const { url, title, description, fileName, torrentUrl } = c; + contentHtml = div({ class: 'trending-file' }, + div({ class: 'card-section file' }, + title?.trim() ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.fileTitleLabel || 'Title') + ':'), span({ class: 'card-value' }, title)) : "", + description ? [span({ class: 'card-label' }, (i18n.fileDescriptionLabel || 'Description') + ":"), p(...renderStyledText(description))] : null, + url && url.startsWith("&") + ? div({ class: 'card-field' }, renderFileDownloads(item.key, torrentUrl, fileName || title)) + : div({ class: 'card-field' }, p(i18n.fileNoFile || 'No file')) + ) + ); } else if (c.type === 'torrent') { - const { url, title, description } = c; + const { url, title, description, source, sourceName } = c; contentHtml = div({ class: 'trending-torrent' }, div({ class: 'card-section torrent' }, title?.trim() ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.torrentTitleLabel || 'Title') + ':'), span({ class: 'card-value' }, title)) : "", - description ? [span({ class: 'card-label' }, (i18n.torrentDescriptionLabel || 'Description') + ":"), p(...renderUrl(description))] : null, + description ? [span({ class: 'card-label' }, (i18n.torrentDescriptionLabel || 'Description') + ":"), p(...renderStyledText(description))] : null, url && url.startsWith("&") - ? div({ class: 'card-field' }, a({ href: `/blob/${encodeURIComponent(url)}`, class: 'filter-btn' }, i18n.torrentDownload || 'Download')) + ? div({ class: 'card-field torrent-card-actions' }, renderTorrentSourceDownload(item.key, source), renderTorrentDownload(torrentDownloadHref(url, title))) : div({ class: 'card-field' }, p(i18n.torrentNoFile || 'No file')) ) ); @@ -94,21 +110,82 @@ const renderTrendingCard = (item, votes, categories, seenTitles, spreadMap = new contentHtml = div({ class: 'trending-document' }, div({ class: 'card-section document' }, t ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.documentTitleLabel + ':'), span({ class: 'card-value' }, t)) : "", - description ? [span({ class: 'card-label' }, i18n.documentDescriptionLabel + ":"), p(...renderUrl(description))] : null, + description ? [span({ class: 'card-label' }, i18n.documentDescriptionLabel + ":"), p(...renderStyledText(description))] : null, div({ id: `pdf-container-${item.key}`, class: 'pdf-viewer-container', 'data-pdf-url': `/blob/${encodeURIComponent(url)}` }) ) ); + } else if (c.type === 'industry') { + contentHtml = div({ class: 'trending-industry' }, + div({ class: 'card-section industry' }, + div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.industryName + ':'), span({ class: 'card-value' }, c.name || '')), + c.sector ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.industrySector + ':'), span({ class: 'card-value' }, String(c.sector).toUpperCase())) : "", + c.membershipPolicy ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.industryMembershipPolicy + ':'), span({ class: 'card-value' }, String(i18n['industryPolicy_' + c.membershipPolicy] || c.membershipPolicy).toUpperCase())) : "", + c.description ? p(...renderStyledText(c.description)) : null, + Array.isArray(c.tags) && c.tags.length + ? div({ class: 'card-tags' }, c.tags.map(tag => a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: 'tag-link' }, `#${tag}`))) + : null + ) + ); + } else if (c.type === 'industryBlueprint') { + contentHtml = div({ class: 'trending-industry' }, + div({ class: 'card-section industry' }, + div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.industryBlueprint + ':'), span({ class: 'card-value' }, c.name || '')), + c.outKind ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.industryOutputKind + ':'), span({ class: 'card-value' }, String(i18n['industryKind_' + c.outKind] || c.outKind).toUpperCase())) : "", + c.laborHours ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.industryLaborHours + ':'), span({ class: 'card-value' }, String(c.laborHours))) : "", + c.description ? p(...renderStyledText(c.description)) : null + ) + ); + } else if (c.type === 'housing') { + const free = String(c.housing_type || '').toLowerCase() === 'couchsurfing'; + contentHtml = div({ class: 'trending-housing' }, + div({ class: 'card-section housing' }, + div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.title + ':'), span({ class: 'card-value' }, c.title || '')), + c.housing_type ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.housingType + ':'), span({ class: 'card-value' }, String(i18n['housingType' + String(c.housing_type).toUpperCase()] || c.housing_type).toUpperCase())) : "", + c.property_type ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.housingProperty + ':'), span({ class: 'card-value' }, String(i18n['housingProperty' + String(c.property_type).toUpperCase()] || c.property_type).toUpperCase())) : "", + div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.housingPrice + ':'), span({ class: 'card-value' }, free ? (i18n.housingFree || 'FREE') : `${Number(c.price || 0).toFixed(2)} ECO`)), + c.place ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.housingPlace + ':'), span({ class: 'card-value' }, c.place)) : "", + Number(c.rooms) > 0 ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.housingRooms + ':'), span({ class: 'card-value' }, String(c.rooms))) : "", + Number(c.size) > 0 ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.housingSize + ':'), span({ class: 'card-value' }, `${c.size} m²`)) : "", + c.description ? p(...renderStyledText(c.description)) : null, + Array.isArray(c.tags) && c.tags.length + ? div({ class: 'card-tags' }, c.tags.map(tag => a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: 'tag-link' }, `#${tag}`))) + : null + ) + ); + } else if (c.type === 'market') { + contentHtml = div({ class: 'trending-market' }, + div({ class: 'card-section market' }, + div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.title + ':'), span({ class: 'card-value' }, c.title || '')), + c.item_type ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.marketItemType + ':'), span({ class: 'card-value' }, String(c.item_type).toUpperCase())) : "", + c.item_status ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.marketItemCondition || i18n.status) + ':'), span({ class: 'card-value' }, String(c.item_status).toUpperCase())) : "", + div({ class: 'price-chip' }, `${c.price} ECO`), + c.description ? p(...renderStyledText(c.description)) : null, + Array.isArray(c.tags) && c.tags.length + ? div({ class: 'card-tags' }, c.tags.map(tag => a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: 'tag-link' }, `#${tag}`))) + : null + ) + ); + } else if (c.type === 'post') { + contentHtml = div({ class: 'trending-blog' }, + div({ class: 'card-section blog' }, + c.contentWarning ? div({ class: 'shop-title-row' }, h2({ class: 'tribe-card-title' }, a({ href: `/blogs/${encodeURIComponent(item.key)}` }, c.contentWarning))) : "", + div({ class: 'blog-card-text', [RAW_HTML]: sanitizeHtml(renderStyledHtml(String(c.text || '').trim().length > 420 ? String(c.text).trim().slice(0, 420).replace(/\s+$/, '') + '…' : String(c.text || '').trim())) }), + Array.isArray(c.tags) && c.tags.length + ? div({ class: 'card-tags' }, c.tags.map(tag => a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: 'tag-link' }, `#${tag}`))) + : null + ) + ); } else if (c.type === 'feed') { const { text, refeeds } = c; contentHtml = div({ class: 'trending-feed' }, div({ class: 'card-section feed' }, - div({ class: 'feed-text', innerHTML: sanitizeHtml(renderTextWithStyles(text)) }), + div({ class: 'feed-text' }, ...renderStyledText(String(text || ''), { zoomImages: true })), refeeds ? h2({ class: 'card-field' }, span({ class: 'card-label' }, i18n.tribeFeedRefeeds + ': '), span({ class: 'card-value' }, refeeds)) : "" ) ); - } else if (c.type === 'votes') { + } else if (c.type === 'votes' || c.type === 'poll') { const { question, deadline, votes: vmap, totalVotes } = c; const votesList = vmap && typeof vmap === 'object' ? Object.entries(vmap).map(([o, cnt]) => ({ option: o, count: cnt })) @@ -116,7 +193,7 @@ const renderTrendingCard = (item, votes, categories, seenTitles, spreadMap = new contentHtml = div({ class: 'trending-votes' }, div({ class: 'card-section votes' }, div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.voteQuestionLabel + ':'), span({ class: 'card-value' }, question)), - div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.voteDeadline + ':'), span({ class: 'card-value' }, deadline ? new Date(deadline).toLocaleString() : '')), + div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.voteDeadline + ':'), span({ class: 'card-value' }, deadline ? moment(deadline).format("YYYY/MM/DD HH:mm") : '')), div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.voteTotalVotes + ':'), span({ class: 'card-value' }, totalVotes)), table( tr(...votesList.map(v => th(i18n[v.option] || v.option))), @@ -129,11 +206,11 @@ const renderTrendingCard = (item, votes, categories, seenTitles, spreadMap = new contentHtml = div({ class: 'trending-transfer' }, div({ class: 'card-section transfer' }, div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.concept + ':'), span({ class: 'card-value' }, concept)), - div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.deadline + ':'), span({ class: 'card-value' }, deadline ? new Date(deadline).toLocaleString() : '')), + div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.deadline + ':'), span({ class: 'card-value' }, deadline ? moment(deadline).format("YYYY/MM/DD HH:mm") : '')), div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.status + ':'), span({ class: 'card-value' }, status)), div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.amount + ':'), span({ class: 'card-value' }, amount)), - div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.from + ':'), span({ class: 'card-value' }, a({ href: `/author/${encodeURIComponent(from)}`, target: '_blank' }, from))), - div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.to + ':'), span({ class: 'card-value' }, a({ href: `/author/${encodeURIComponent(to)}`, target: '_blank' }, to))), + div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.from + ':'), span({ class: 'card-value' }, a({ href: `/author/${encodeURIComponent(from)}`, target: '_blank', rel: 'noopener noreferrer' }, from))), + process.env.OASIS_MOBILE === '1' ? null : div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.to + ':'), span({ class: 'card-value' }, a({ href: `/author/${encodeURIComponent(to)}`, target: '_blank', rel: 'noopener noreferrer' }, to))), h2({ class: 'card-field' }, span({ class: 'card-label' }, i18n.transfersConfirmations + ': '), span({ class: 'card-value' }, `${confirmedBy.length}/2`)) ) ); @@ -142,7 +219,7 @@ const renderTrendingCard = (item, votes, categories, seenTitles, spreadMap = new div({ class: 'card-section styled-text-content' }, div( { class: 'card-field' }, - span({ class: 'card-value', innerHTML: sanitizeHtml(renderTextWithStyles(c.text || c.description || c.title || '[no content]')) }) + span({ class: 'card-value', [RAW_HTML]: sanitizeHtml(renderStyledHtml(c.title || c.name || c.text || c.description || '[no content]')) }) ) ) ); @@ -154,10 +231,27 @@ const renderTrendingCard = (item, votes, categories, seenTitles, spreadMap = new audio: 'audios', video: 'videos', torrent: 'torrents', + file: 'files', document: 'documents', feed: 'feed', + post: 'blogs', votes: 'votes', - transfer: 'transfers' + transfer: 'transfers', + industry: 'industry', + project: 'projects', + report: 'reports', + task: 'tasks', + event: 'events', + shopProduct: 'shops/product', + housing: 'housing', + market: 'market', + podcast: 'podcasts', + podcastEpisode: 'podcasts/episode', + campaign: 'campaigns', + logisticsRoute: 'logistics', + poll: 'polls', + schoolCourse: 'school/course', + industryBlueprint: 'industry/blueprint' }; const detailHref = detailPaths[c.type] ? `/${detailPaths[c.type]}/${encodeURIComponent(item.key)}` @@ -169,69 +263,70 @@ const renderTrendingCard = (item, votes, categories, seenTitles, spreadMap = new div( { class: 'card-header activity-card-header' }, span({ class: 'pm-exposition-chip pm-exposition-whole' }, - span({ class: 'pm-exposition-text' }, String(c.type || '').toUpperCase()) + span({ class: 'pm-exposition-text' }, String(typeLabel(c.type) || '').toUpperCase()) ), - renderContentActions(item.key, detailHref) + renderContentActions(item.key, detailHref, { + spread: spreadMap.get(item.key) || null, + author: item.value.author, + reportTitle: c.title || c.question || c.concept || c.contentWarning || '', + ...(CONTENT_TORRENTABLE.has(c.type) && typeof c.url === 'string' && c.url.startsWith('&') + ? { torrentFrom: { blobId: c.url, name: c.title || c.fileName || '' } } + : {}), + ...(CONTENT_FAV_KIND[c.type] + ? { favKind: CONTENT_FAV_KIND[c.type], isFavorite: params.favIndex instanceof Map ? params.favIndex.get(String(item.key)) === CONTENT_FAV_KIND[c.type] : false, returnTo: params.returnTo } + : {}) + }) ), div( { class: 'card-section trending-card-body' }, - contentHtml, - div({ class: 'card-spread-left' }, renderSpreadButton(item.key, spreadMap.get(item.key))), - p( - { class: 'card-footer' }, - span({ class: 'date-link' }, `${created} ${i18n.performed} `), - userLink(item.value.author) - ), - (() => { - const ops = c.opinions || {}; - const entries = Object.entries(ops).filter(([, v]) => v > 0); - const dominantPart = (() => { - if (!entries.length) return null; - const maxVal = Math.max(...entries.map(([, v]) => v)); - const dominant = entries.filter(([, v]) => v === maxVal).map(([k]) => voteLabelFor(k)); - return [ - span({ class: 'trending-dominant-sep' }, '|'), - span({ class: 'trending-dominant-text' }, `${i18n.moreVoted || 'More Voted'}: ${dominant.join(' + ')}`) - ]; - })(); - return h2( - `${i18n.trendingTotalOpinions || i18n.trendingTotalCount}: `, - span({ class: 'trending-total-count' }, String(votes)), - ...(dominantPart || []) - ); - })(), + contentHtml + ), + renderVotesSummary(c.opinions), + renderCardMetaRow( details({ class: 'opinions-voting-collapse' }, - summary({ class: 'opinions-summary' }, `${i18n.opinionsTitle || 'Opinions'} (${Object.values(c.opinions || {}).reduce((s, n) => s + (Number(n) || 0), 0)})`), + summary({ class: 'opinions-summary' }, + span({ class: 'opinions-summary-icon' }, 'ꔍ'), + span({ class: 'opinions-summary-count' }, `(${Object.values(c.opinions || {}).reduce((s, n) => s + (Number(n) || 0), 0)})`)), div( { class: 'voting-buttons' }, categories.map(cat => form({ method: 'POST', action: `/trending/${encodeURIComponent(item.key)}/${cat}` }, button( { class: 'vote-btn' }, - `${voteLabelFor(cat)} [${c.opinions?.[cat] || 0}]` + `${String(voteLabelFor(cat)).toUpperCase()} [${c.opinions?.[cat] || 0}]` ) ) ) ) + ), + p( + { class: 'card-footer' }, + span({ class: 'date-link' }, `${created}`), + userLink(item.value.author) ) ) ); }; -exports.trendingView = (items, filter, categories = opinionCategories, spreadMap = new Map()) => { +exports.trendingView = (items, filter, categories = opinionCategories, spreadMap = new Map(), q = '', allItems = null, params = {}) => { const seenDocumentTitles = new Set(); const title = i18n.trendingTitle; - const baseFilters = ['TOP', 'ALL', 'MINE', 'RECENT']; + const baseFilters = ['ALL', 'RECENT', 'MINE', 'TOP']; const contentFilters = [ - ['votes', 'feed', 'transfer'], - ['bookmark', 'image', 'video', 'audio', 'document', 'torrent'] + ['votes', 'event', 'task', 'report'], + ['feed', 'post', 'project', 'industry', 'shopProduct', 'transfer'], + ['audio', 'bookmark', 'document', 'file', 'image', 'torrent', 'video'], + ['podcast', 'podcastEpisode', 'campaign', 'logisticsRoute'] ]; let filteredItems = items.filter(item => { const c = item.value?.content || item.content; return c && typeof c === 'object' && c.type !== 'tombstone'; }); + const censusItems = Array.isArray(allItems) ? allItems : filteredItems; + const presentTypes = new Set(censusItems.map(item => (item.value?.content || item.content || {}).type).filter(Boolean)); + const emptyTrend = censusItems.length === 0 && !String(q || '').trim(); if (filter === 'MINE') { filteredItems = filteredItems.filter(item => item.value.author === userId); @@ -245,7 +340,9 @@ exports.trendingView = (items, filter, categories = opinionCategories, spreadMap return bVotes !== aVotes ? bVotes - aVotes : b.value.timestamp - a.value.timestamp; }); } else if (contentFilters.flat().includes(filter)) { - filteredItems = filteredItems.filter(item => item.value.content.type === filter); + filteredItems = filteredItems.filter(item => + item.value.content.type === filter || + (filter === 'votes' && item.value.content.type === 'poll')); } else if (filter !== 'ALL') { filteredItems = filteredItems.filter(item => (item.value.content.opinions_inhabitants || []).length > 0); } @@ -254,15 +351,25 @@ exports.trendingView = (items, filter, categories = opinionCategories, spreadMap filteredItems.sort((a, b) => b.value.timestamp - a.value.timestamp); } - const header = div({ class: 'tags-header' }, h2(title), p(i18n.exploreTrending)); - const cards = filteredItems + const header = div({ class: 'tags-header module-header-line' }, h2(title), p(i18n.exploreTrending)); + const seenDocs = new Set(); + const shownItems = filteredItems.filter(item => { + const c = item.value.content || {}; + const t = c.type === 'document' ? String(c.title || '').trim() : ''; + if (!t) return true; + if (seenDocs.has(t)) return false; + seenDocs.add(t); + return true; + }); + const cards = paged(shownItems) .map(item => renderTrendingCard( item, Object.values(item.value.content.opinions || {}).reduce((s, n) => s + (n || 0), 0), categories, seenDocumentTitles, - spreadMap + spreadMap, + { favIndex: params.favIndex, returnTo: `/trending?filter=${encodeURIComponent(filter)}` } ) ) .filter(Boolean); @@ -273,16 +380,27 @@ exports.trendingView = (items, filter, categories = opinionCategories, spreadMap title, section( header, - div( + emptyTrend ? null : div( { class: 'mode-buttons' }, div({ class: 'column' }, baseFilters.map(mode => filterButton(mode, filter))), - ...contentFilters.map(row => - div({ class: 'column' }, row.map(mode => filterButton(mode, filter))) + ...contentFilters.map(row => { + const visible = row.filter(mode => presentTypes.has(mode) || (mode === 'votes' && presentTypes.has('poll'))); + return visible.length ? div({ class: 'column' }, visible.map(mode => filterButton(mode, filter))) : null; + }) + ), + emptyTrend ? null : div({ class: 'filters activity-filter-chips activity-toolbar-row' }, + renderModuleStats(shownItems.length), + form({ method: 'GET', action: '/trending', class: 'filter-box' }, + input({ type: 'hidden', name: 'filter', value: filter }), + input({ type: 'text', name: 'q', value: q, placeholder: i18n.trendingSearchPlaceholder, class: 'filter-box__input' }), + div({ class: 'filter-box__controls' }, + button({ type: 'submit', class: 'filter-box__button' }, i18n.searchButton) + ) ) ), section( cards.length - ? div({ class: 'trending-container', style: 'display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:20px;' }, ...cards) + ? div({ class: 'trending-container' }, ...cards) : div({ class: 'no-results' }, p(i18n.trendingNoContentMessage)) ) ) @@ -290,8 +408,7 @@ exports.trendingView = (items, filter, categories = opinionCategories, spreadMap if (hasDocument) { html += ` - - + `; } diff --git a/src/views/tribes_view.js b/src/views/tribes_view.js index 13cf41e..a0f8087 100644 --- a/src/views/tribes_view.js +++ b/src/views/tribes_view.js @@ -1,14 +1,84 @@ const { div, h2, h3, p, section, button, form, a, input, img, label, select, option, br, textarea, h1, span, nav, ul, li, video, audio, table, tr, td, thead, tbody, th } = require("../server/node_modules/hyperaxe"); +const { TEXT_CAP } = require('../backend/long_text'); const moment = require("../server/node_modules/moment"); -const { template, i18n, userLink, renderStateChip, renderPrivacyChip, renderLifespanChip, renderModeChip, renderInviteQrCard, renderContentActions } = require('./main_views'); -const { renderEncryptedChip: renderTribeEncryptedChip } = require('./clearnet_view'); +const { template, i18n, userLink, renderStateChip, renderPrivacyChip, renderLifespanChip, renderModeChip, renderInviteQrCard, renderContentActions, renderSubscriptionBox, renderModuleStats, renderModuleStatsBy, moduleIsEmpty, renderTorrentDownload, torrentDownloadHref, contentDeleteAction, clearnetSlugFor, renderEngagement, renderOpinionsVoting, renderVotesSummary, paged, CLEARNET_HUB_CSS } = require('./main_views'); +const { renderTribeWikiSection } = require('./wiki_view'); +const mobileUi = require('./mobile_ui'); +const { renderEncryptedChip: renderTribeEncryptedChip, renderLicenseSelect, renderReachChip, renderClearnetPage, escapeHtml, renderRichText, blobUrl: cnBlobUrl, paginateClearnet, renderClearnetPager, CLEARNET_PAGER_CSS } = require('./clearnet_view'); +const { renderPollCard } = require('./polls_view'); +const { renderFeedCard } = require('./feed_view'); +const { renderEventItem } = require('./event_view'); +const { renderTaskItem, renderTaskAssignAction, renderTaskStatusRow } = require('./task_view'); +const { renderVoteListItem, renderVoteOptionsBlock } = require('./vote_view'); +const { renderImageList } = require('./image_view'); +const { renderAudioList } = require('./audio_view'); +const { renderVideoList } = require('./video_view'); +const { renderDocumentList } = require('./document_view'); +const { renderBookmarkList } = require('./bookmark_view'); +const { renderTorrentTable } = require('./torrents_view'); +const { renderFileTable } = require('./files_view'); +const { renderPadCard } = require('./pads_view'); +const { renderChatCard } = require('./chats_view'); +const { renderCalendarCard } = require('./calendars_view'); +const { renderInhabitantCard } = require('./inhabitants_view'); const { config } = require('../server/SSB_server.js'); -const { renderUrl } = require('../backend/renderUrl'); -const { renderMapLocationUrl, renderMapLocationGrid, renderMapLocationVisitLabel } = require("./maps_view"); -const opinion_categories = require('../backend/opinion_categories.js'); +const { renderStyledText, safeExternalHref } = require('../backend/renderStyledText'); +const { renderSearchForm } = require('./search_view'); +const { renderMapCard, renderMapLocationVisitLabel, renderMapEmbed } = require("./maps_view"); +const { renderTribeRoomsSection, roomStatsBy } = require('./rooms_view'); const userId = config.keys.id; const isLarpHouseTribe = (t) => Array.isArray(t && t.tags) && t.tags.some(x => String(x).startsWith('larp-')); +const isTribeMember = (t) => !!t && (t.author === userId || (Array.isArray(t.members) && t.members.includes(userId))); + +const REACH_LEVELS = ['tribe', 'oasis', 'clearnet']; +const REACH_CONTENT_TYPES = new Set(['event', 'task', 'votation', 'forum', 'forum-reply', 'media', 'feed']); +const reachLevelOf = (item) => REACH_LEVELS.includes(item && item.reach) ? item.reach : 'tribe'; +const tribeReachTribeLabel = () => String(i18n.tribeReachTribe || 'TRIBE').toUpperCase(); +const tribeClearnetHref = (t) => `/c/tribe/${encodeURIComponent(clearnetSlugFor(t.title, t.rootId || t.id))}`; + +const renderTribeReachChip = (level, tribe) => level === 'oasis' || level === 'clearnet' + ? renderReachChip(level === 'clearnet', i18n, level === 'clearnet' && tribe ? tribeClearnetHref(tribe) : null) + : renderStateChip('encrypted', '🔒', i18n.tribeReachTribe || 'TRIBE'); + +const renderTribeReachSwitch = (tribe, item, returnTo) => { + const current = reachLevelOf(item); + const levelBtn = (level, text) => button({ type: 'submit', name: 'reach', value: level, class: current === level ? 'clearnet-switch-btn active' : 'clearnet-switch-btn', ...(current === level ? { disabled: true } : {}) }, text); + return form({ method: 'POST', action: `/tribe/${encodeURIComponent(tribe.id)}/content/${encodeURIComponent(item.id)}/reach`, class: 'clearnet-switch', title: i18n.tribeReachChange }, + input({ type: 'hidden', name: 'returnTo', value: returnTo }), + levelBtn('tribe', tribeReachTribeLabel()), + levelBtn('oasis', 'OASIS'), + levelBtn('clearnet', 'CLEARNET') + ); +}; + +const renderTribeReachSelector = () => + div({ class: 'clearnet-choice' }, + div({ class: 'clearnet-choice-options' }, + label({ class: 'clearnet-choice-option' }, input({ type: 'radio', name: 'reach', value: 'tribe', checked: true }), renderTribeReachChip('tribe')), + label({ class: 'clearnet-choice-option' }, input({ type: 'radio', name: 'reach', value: 'oasis' }), renderTribeReachChip('oasis')), + label({ class: 'clearnet-choice-option' }, input({ type: 'radio', name: 'reach', value: 'clearnet' }), renderTribeReachChip('clearnet')) + ), + p({ class: 'tribe-meta-label' }, i18n.tribeReachNotice) + ); + +const reachIndexOf = (items) => { + const byId = new Map(); + (Array.isArray(items) ? items : []).forEach(i => { + if (!i || i.contentType !== 'forum') return; + if (i.id) byId.set(i.id, i); + if (i.originId) byId.set(i.originId, i); + }); + return byId; +}; + +const renderTribeReachListChip = (tribe, item, threadsById) => { + if (!item || !REACH_CONTENT_TYPES.has(item.contentType)) return null; + const parent = item.contentType === 'forum-reply' && threadsById ? threadsById.get(item.parentId) : null; + const level = reachLevelOf(parent || item); + if (!tribe.reachAllowed && level === 'tribe') return null; + return div({ class: 'card-chips-row' }, renderTribeReachChip(level, item.tribeName ? null : tribe)); +}; const DEFAULT_HASH_ENC = "%260000000000000000000000000000000000000000000%3D.sha256"; const DEFAULT_HASH_PATH_RE = /\/image\/\d+\/%260000000000000000000000000000000000000000000%3D\.sha256$/; @@ -34,27 +104,27 @@ const resolvePhoto = (photoField) => { }; const MS_PER_DAY = 86400000; -const FEED_ITEMS_PER_PAGE = 5; const MAX_MESSAGE_LENGTH = 280; const renderMediaBlob = (value, fallbackSrc = null, attrs = {}) => { if (!value) return fallbackSrc ? img({ src: fallbackSrc, ...attrs }) : null const s = String(value).trim() if (!s) return fallbackSrc ? img({ src: fallbackSrc, ...attrs }) : null - if (s.startsWith('/assets/') || s.startsWith('/blob/') || s.startsWith('/image/')) return img({ src: s, ...attrs }) - if (s.startsWith('&')) return img({ src: `/blob/${encodeURIComponent(s)}`, ...attrs }) - const mVideo = s.match(/\[video:[^\]]*\]\(\s*(&[^)\s]+\.sha256)\s*\)/) - if (mVideo) return video({ controls: true, class: attrs.class || 'post-video', src: `/blob/${encodeURIComponent(mVideo[1])}` }) - const mAudio = s.match(/\[audio:[^\]]*\]\(\s*(&[^)\s]+\.sha256)\s*\)/) - if (mAudio) return audio({ controls: true, class: attrs.class || 'post-audio', src: `/blob/${encodeURIComponent(mAudio[1])}` }) - const mImg = s.match(/!\[[^\]]*\]\(\s*(&[^)\s]+\.sha256)\s*\)/) - if (mImg) return img({ src: `/blob/${encodeURIComponent(mImg[1])}`, ...attrs }) + if (s.startsWith('/assets/') || s.startsWith('/blob/') || s.startsWith('/image/')) return img({ loading: 'lazy', src: s, ...attrs }) + if (s.startsWith('&')) return img({ loading: 'lazy', src: `/blob/${encodeURIComponent(s)}`, ...attrs }) + const mVideo = s.match(/\[video:[^\]]*\]\(\s*(&[^)\s]+\.sha256|\/blob\/sealed\/[^)\s]+)\s*\)/) + if (mVideo) return video({ controls: true, class: attrs.class || 'post-video', src: toBlobUrl(mVideo[1]), preload: 'metadata' }) + const mAudio = s.match(/\[audio:[^\]]*\]\(\s*(&[^)\s]+\.sha256|\/blob\/sealed\/[^)\s]+)\s*\)/) + if (mAudio) return audio({ controls: true, class: attrs.class || 'post-audio', src: toBlobUrl(mAudio[1]) }) + const mImg = s.match(/!\[[^\]]*\]\(\s*(&[^)\s]+\.sha256|\/blob\/sealed\/[^)\s]+)\s*\)/) + if (mImg) return img({ loading: 'lazy', src: toBlobUrl(mImg[1]), ...attrs }) return fallbackSrc ? img({ src: fallbackSrc, ...attrs }) : null } const toBlobUrl = (raw) => { if (!raw) return null const s = String(raw).trim() + if (s.startsWith('/blob/sealed/')) return s if (s.startsWith('&')) return `/blob/${encodeURIComponent(s)}` const m = s.match(/\((&[^)\s]+\.sha256)\s*\)/) return m ? `/blob/${encodeURIComponent(m[1])}` : null @@ -69,23 +139,11 @@ const toImageUrl = (raw, fallback) => { return url || fallback } -const filterAndSortFeed = (feed, feedFilter) => { - const parseDate = (d) => typeof d === 'string' ? Date.parse(d) : d; - if (feedFilter === 'MINE') return feed.filter(m => m.author === userId); - if (feedFilter === 'RECENT') { - const last24h = Date.now() - MS_PER_DAY; - return [...feed].filter(m => parseDate(m.createdAt) >= last24h).sort((a, b) => parseDate(b.createdAt) - parseDate(a.createdAt)); - } - if (feedFilter === 'ALL') return [...feed].sort((a, b) => parseDate(b.createdAt) - parseDate(a.createdAt)); - if (feedFilter === 'TOP') return [...feed].sort((a, b) => (b.refeeds || 0) - (a.refeeds || 0)); - return feed; -}; - const renderGallery = (sortedTribes) => { return div({ class: "gallery" }, sortedTribes.length ? sortedTribes.map(t => - a({ href: `#tribe-${encodeURIComponent(t.id)}`, class: "gallery-item" }, + a({ href: `#tribe-${encodeURIComponent(t.id)}`, id: `tribe-${encodeURIComponent(t.id)}-src`, class: "gallery-item" }, img({ src: toImageUrl(t.image, '/assets/images/default-tribe.png'), alt: t.title || "", class: "gallery-image" }) ) ) @@ -97,7 +155,7 @@ const renderLightbox = (sortedTribes) => { return sortedTribes.map(t => div( { id: `tribe-${encodeURIComponent(t.id)}`, class: "lightbox" }, - a({ href: "#", class: "lightbox-close" }, "×"), + a({ href: `#tribe-${encodeURIComponent(t.id)}-src`, class: "lightbox-close" }, "×"), img({ src: toImageUrl(t.image, '/assets/images/default-tribe.png'), class: "lightbox-image", alt: t.title || "" }) ) ); @@ -110,7 +168,7 @@ exports.renderInvitePage = (inviteCode) => { input({ type: 'text', value: inviteCode, readonly: 'readonly', class: 'tribe-invite-code-input' }), br(), form({ method: "GET", action: `/tribes` }, - button({ type: "submit", class: "filter-btn" }, i18n.walletBack) + button({ type: "submit", class: "tribe-action-btn" }, i18n.walletBack) ), ); return template(i18n.tribeInviteMode, section(pageContent)); @@ -166,30 +224,34 @@ exports.tribesView = async (tribes, filter, tribeId, query = {}, allTribes = nul return cb - ca; }); - const title = - filter === 'recent' ? i18n.tribeRecentSectionTitle : - filter === 'mine' ? i18n.tribeMineSectionTitle : - filter === 'create' ? i18n.tribeCreateSectionTitle : - filter === 'edit' ? i18n.tribeUpdateSectionTitle : - filter === 'gallery' ? i18n.tribeGallerySectionTitle : - filter === 'top' ? i18n.tribeTopSectionTitle : - filter === 'subtribes' ? (i18n.tribeSubTribes || 'SUB-TRIBES') : - i18n.tribeAllSectionTitle; + const title = i18n.tribesTitle; - const header = div({ class: 'tags-header' }, h2(title), p(i18n.tribeDescription)); + const header = div({ class: 'tags-header module-header-line' }, h2(title), p(i18n.tribeDescription)); - const filters = div({ class: 'filters' }, - form({ method: 'GET', action: '/tribes' }, + const emptyMod = moduleIsEmpty(Array.isArray(tribes) ? tribes : [], filter || 'all', 'all', query.search); + const tribeChip = (m) => { + if (m === filter || m === 'all') return true; + if (m === 'top' || m === 'gallery') return tribes.length > 0; + if (m === 'mine') return tribes.some(t => t.author === userId); + if (m === 'membership') return tribes.some(t => Array.isArray(t.members) && t.members.includes(userId)); + if (m === 'subtribes') return tribes.some(t => !!t.parentTribeId); + if (m === 'recent') return tribes.length > 0; + return true; + }; + const filters = emptyMod ? null : div({ class: 'filters activity-filter-chips activity-toolbar-row' }, + renderModuleStatsBy(sorted, t => t.isAnonymous ? 'PRIVATE' : 'PUBLIC', [{ value: 'PUBLIC', label: i18n.tribePublic }, { value: 'PRIVATE', label: i18n.tribePrivate }]), + form({ method: 'GET', action: '/tribes', class: 'filter-box' }, input({ type: 'hidden', name: 'filter', value: filter }), - input({ type: 'text', name: 'search', placeholder: i18n.searchTribesPlaceholder, value: query.search || '' }), - br(), - button({ type: 'submit' }, i18n.applyFilters), - br() + input({ type: 'text', name: 'search', placeholder: i18n.searchTribesPlaceholder, value: query.search || '', class: 'filter-box__input' }), + div({ class: 'filter-box__controls' }, + button({ type: 'submit', class: 'filter-box__button' }, i18n.searchButton) + ) ) ); const modeButtons = div({ class: 'tribe-mode-buttons' }, - ['all','recent','mine','membership','subtribes','top','gallery'].map(f => + ...(emptyMod ? [] : [ + ['all','recent','mine','membership','subtribes','top','gallery'].filter(tribeChip).map(f => form({ method: 'GET', action: '/tribes' }, input({ type: 'hidden', name: 'filter', value: f }), button({ type: 'submit', class: filter === f ? 'filter-btn active' : 'filter-btn' }, @@ -197,6 +259,7 @@ exports.tribesView = async (tribes, filter, tribeId, query = {}, allTribes = nul ) ) ), + ]), form({ method: 'GET', action: '/tribes/create' }, button({ type: 'submit', class: 'create-button' }, i18n.tribeCreateButton) ) @@ -214,17 +277,17 @@ exports.tribesView = async (tribes, filter, tribeId, query = {}, allTribes = nul }, label({ for: 'title' }, i18n.tribeTitleLabel), br, - input({ type: 'text', name: 'title', id: 'title', required: true, placeholder: i18n.tribeTitlePlaceholder, value: tribeToEdit.title || '' }), + input({ type: 'text', name: 'title', maxlength: '100', id: 'title', required: true, placeholder: i18n.tribeTitlePlaceholder, value: tribeToEdit.title || '' }), br(), label({ for: 'description' }, i18n.tribeDescriptionLabel), br, - textarea({ name: 'description', id: 'description', required: true, rows: 4, cols: 50, placeholder: i18n.tribeDescriptionPlaceholder }, tribeToEdit.description || ''), + textarea({ maxlength: "3000", name: 'description', id: 'description', required: true, rows: 4, cols: 50, placeholder: i18n.tribeDescriptionPlaceholder }, tribeToEdit.description || ''), br, label({ for: 'location' }, i18n.tribeLocationLabel), br, input({ type: 'text', name: 'location', id: 'location', placeholder: i18n.tribeLocationPlaceholder, value: tribeToEdit.location || '' }), br, - label(i18n.mapLocationTitle || 'Map Location'), + label(process.env.OASIS_MOBILE === '1' ? (i18n.tribeMapUrlLabel || 'Map link (optional)') : (i18n.mapLocationTitle || 'Map Location')), br, input({ type: 'text', name: 'mapUrl', placeholder: i18n.mapUrlPlaceholder || '/maps/MAP_ID', value: tribeToEdit.mapUrl || '' }), br, @@ -242,6 +305,7 @@ exports.tribesView = async (tribes, filter, tribeId, query = {}, allTribes = nul option({ value: 'true', selected: tribeToEdit.isAnonymous === true ? 'selected' : undefined }, i18n.tribePrivate), option({ value: 'false', selected: tribeToEdit.isAnonymous === false ? 'selected' : undefined }, i18n.tribePublic) ), + !isSubEdit && mobileUi.isMobile() ? p({ class: 'form-hint' }, i18n.tribeHintPrivacy) : null, isSubEdit ? null : br(), isSubEdit ? null : br(), label({ for: 'inviteMode' }, i18n.tribeModeLabel), @@ -250,6 +314,7 @@ exports.tribesView = async (tribes, filter, tribeId, query = {}, allTribes = nul option({ value: 'strict', selected: tribeToEdit.inviteMode === 'strict' ? 'selected' : undefined }, i18n.tribeStrict), option({ value: 'open', selected: tribeToEdit.inviteMode === 'open' ? 'selected' : undefined }, i18n.tribeOpen) ), + mobileUi.isMobile() ? p({ class: 'form-hint' }, i18n.tribeHintMode) : null, br(), br(), button({ type: 'submit' }, isEdit ? i18n.tribeUpdateButton : i18n.tribeCreateButton) ) @@ -257,26 +322,23 @@ exports.tribesView = async (tribes, filter, tribeId, query = {}, allTribes = nul const allT = allTribes || tribes; - const tribeCards = sorted.map(t => { - const isMember = t.members.includes(userId); + const shownTribes = filter === 'create' || filter === 'edit' ? [] : paged(sorted); + + const tribeCards = shownTribes.map(t => { + if (mobileUi.isMobile()) return mobileUi.tribeRow(t, { own: String(t.author) === String(userId), imageNode: t.image ? renderMediaBlob(t.image, null, { class: 'tribe-row-thumb' }) : null }); const parentTribe = t.parentTribeId ? allT.find(p => p.id === t.parentTribeId) : null; const isOwn = String(t.author) === String(userId); return div({ class: 'trending-card tribes-card' + (isOwn ? ' own-content' : '') }, div({ class: 'card-header activity-card-header' }, span(), - renderContentActions(t.id, `/tribe/${encodeURIComponent(t.id)}`) + renderContentActions(t.id, `/tribe/${encodeURIComponent(t.id)}`, { author: t.author, reportTitle: t.title, deleteAction: isOwn && !isLarpHouseTribe(t) ? contentDeleteAction('tribe', t.id) : undefined }) ), div({ class: 'card-section tribes-card-body' }, div({ class: 'tribe-card-image-wrapper' }, a({ href: `/tribe/${encodeURIComponent(t.id)}` }, renderMediaBlob(t.image, '/assets/images/default-tribe.png', { class: 'tribe-card-hero-image' }) - ), - isMember - ? form({ method: 'GET', action: `/tribe/${encodeURIComponent(t.id)}`, class: 'tribe-visit-btn-wrapper' }, - button({ type: 'submit', class: 'filter-btn' }, t.parentTribeId ? String(i18n.tribeviewSubTribeButton || 'VISIT SUB-TRIBE').toUpperCase() : String(i18n.tribeviewTribeButton || '').toUpperCase()) - ) - : null + ) ), div({ class: 'tribe-card-body' }, div({ class: 'shop-title-row' }, @@ -286,45 +348,22 @@ exports.tribesView = async (tribes, filter, tribeId, query = {}, allTribes = nul t.isAnonymous ? renderPrivacyChip(true, i18n) : renderPrivacyChip(false, i18n), t.isAnonymous ? renderTribeEncryptedChip(i18n) : null, renderModeChip(t.inviteMode, i18n), - renderLifespanChip(t.lifetime, i18n) + renderLifespanChip(t.lifetime, i18n), + t.subscriptionIn === true + ? renderStateChip('mutuals', '\u2709', i18n.subscriptionOn) + : (t.subscriptionIn === false ? renderStateChip('closed', '\u2709', i18n.subscriptionOff) : null) ), - t.description ? p({ class: 'tribe-card-description' }, ...renderUrl(t.description)) : null, + t.description ? p({ class: 'tribe-card-description' }, ...renderStyledText(t.description)) : null, renderMapLocationVisitLabel(t.mapUrl), - (parentTribe || t.location) ? table({ class: 'tribe-info-table' }, - parentTribe ? tr( + parentTribe ? table({ class: 'tribe-info-table' }, + tr( td({ class: 'tribe-info-label' }, i18n.tribeRootLabel || 'ROOT'), td({ class: 'tribe-info-value', colspan: '3' }, a({ href: `/tribe/${encodeURIComponent(parentTribe.id)}` }, parentTribe.title)) - ) : null, - t.location ? tr( - td({ class: 'tribe-info-label' }, i18n.tribeLocationLabel || 'LOCATION'), - td({ class: 'tribe-info-value', colspan: '3' }, ...renderUrl(t.location)) - ) : null + ) ) : null, div({ class: 'tribe-card-members' }, span({ class: 'tribe-members-count' }, `${i18n.tribeMembersCount}: ${t.memberCount != null ? t.memberCount : t.members.length}`) - ), - (t.openInviteCode && !isMember && String(t.author) !== String(userId)) ? div({ class: 'tribe-card-join' }, - form({ method: 'POST', action: '/tribes/open-invite/join' }, - input({ type: 'hidden', name: 'tribeId', value: t.id }), - button({ type: 'submit', class: 'tribe-action-btn' }, i18n.tribeJoinOpen) - ), - renderInviteQrCard({ qrDataUrl: t.openInviteQr, name: t.title }) - ) : null, - isMember ? div({ class: 'tribe-card-actions' }, - !isLarpHouseTribe(t) - ? form({ method: 'POST', action: '/tribes/generate-invite' }, - input({ type: 'hidden', name: 'tribeId', value: t.id }), - button({ type: 'submit', class: 'tribe-action-btn' }, i18n.tribeGenerateInvite) - ) - : null, - form({ method: 'POST', action: `/tribes/leave/${encodeURIComponent(t.id)}` }, - button({ type: 'submit', class: 'tribe-action-btn danger-btn' }, i18n.tribeLeaveButton) - ) - ) : null, - filter === 'mine' ? div({ class: 'tribe-actions' }, - form({ method: 'GET', action: `/tribes/edit/${encodeURIComponent(t.id)}` }, button({ type: 'submit' }, i18n.tribeUpdateButton)), - form({ method: 'POST', action: `/tribes/delete/${encodeURIComponent(t.id)}` }, button({ type: 'submit', class: 'danger-btn' }, i18n.tribeDeleteButton)) - ) : null + ) ) ) ); @@ -333,83 +372,54 @@ exports.tribesView = async (tribes, filter, tribeId, query = {}, allTribes = nul return template( title, section(header), - section(filters), section(modeButtons), + filters ? section(filters) : null, section( (filter === 'create' || filter === 'edit') ? createForm : filter === 'gallery' - ? renderGallery(sorted) - : div({ class: 'tribe-grid' }, - tribeCards.length > 0 ? tribeCards : p(i18n.noTribes) - ) + ? renderGallery(shownTribes) + : tribeCards.length > 0 + ? div({ class: mobileUi.isMobile() ? 'tribe-list' : 'tribe-grid' }, tribeCards) + : div({ class: "no-content-box" }, p(i18n.noTribes)) ), - ...renderLightbox(sorted) - ); -}; - -const renderFeedTribeView = async (feedItems, tribe, query = {}, filter) => { - const feed = Array.isArray(feedItems) ? feedItems : []; - const feedFilter = (query.feedFilter || 'RECENT').toUpperCase(); - const filteredFeed = filterAndSortFeed(feed, feedFilter); - return div({ class: 'tribe-feed-full' }, - div({ class: 'feed-actions' }, - ['TOP', 'MINE', 'ALL', 'RECENT'].map(f => - form({ method: 'GET', action: `/tribe/${encodeURIComponent(tribe.id)}` }, - input({ type: 'hidden', name: 'section', value: 'feed' }), - input({ type: 'hidden', name: 'feedFilter', value: f }), - button({ type: 'submit', class: feedFilter === f ? 'filter-btn active' : 'filter-btn' }, i18n[`tribeFeedFilter${f}`]) - ) - ) - ), - filteredFeed.length === 0 - ? p(i18n.tribeFeedEmpty) - : div({ class: 'feed-list' }, - filteredFeed.map(m => div({ class: 'feed-item' }, - div({ class: 'feed-row' }, - div({ class: 'refeed-column' }, - h1(`${m.refeeds || 0}`), - !(m.refeeds_inhabitants || []).includes(userId) - ? form( - { method: 'POST', action: `/tribe/${encodeURIComponent(tribe.id)}/refeed/${encodeURIComponent(m.id)}` }, - button({ class: 'refeed-btn' }, i18n.tribeFeedRefeed) - ) - : null - ), - div({ class: 'feed-main' }, - p(`${new Date(m.createdAt).toLocaleString()} — `, userLink(m.author)), - br, - p(...renderUrl(m.description)) - ) - ) - )) - ) + ...renderLightbox(shownTribes) ); }; const sectionLink = (tribe, sectionKey, label, currentSection) => form({ method: 'GET', action: `/tribe/${encodeURIComponent(tribe.id)}` }, input({ type: 'hidden', name: 'section', value: sectionKey }), - button({ type: 'submit', class: currentSection === sectionKey ? 'filter-btn active' : 'filter-btn' }, label) + button({ type: 'submit', class: currentSection === sectionKey ? 'filter-btn active' : 'filter-btn' }, String(label).toUpperCase()) ); +const tribeTagHref = (tribe) => (tag) => `/tribe/${encodeURIComponent(tribe.id)}?section=search&q=${encodeURIComponent('#' + String(tag).toLowerCase().replace(/^#+/, ''))}`; +const tribeText = (tribe, text, opts = {}) => renderStyledText(String(text || ''), { ...opts, ...(tribe && tribe.id ? { hashtagHref: tribeTagHref(tribe) } : {}) }); +const tribeExcerpt = (tribe, text, max = 200) => { + const full = String(text || '').trim(); + if (!full) return null; + const cut = full.length > max ? full.slice(0, max).replace(/\s+$/, '') + '…' : full; + return p({ class: 'post-text post-text-pre' }, ...tribeText(tribe, cut)); +}; + const renderSectionNav = (tribe, section) => { const firstGroup = [{ key: 'activity', label: i18n.tribeSectionActivity }, { key: 'inhabitants', label: i18n.tribeSectionInhabitants }]; if (!tribe.parentTribeId) firstGroup.push({ key: 'subtribes', label: i18n.tribeSubTribes }); if (!tribe.parentTribeId) firstGroup.push({ key: 'governance', label: i18n.tribeSectionGovernance || 'GOVERNANCE' }); const sections = [ { items: firstGroup }, - { items: [{ key: 'votations', label: i18n.tribeSectionVotations }, { key: 'events', label: i18n.tribeSectionEvents }, { key: 'tasks', label: i18n.tribeSectionTasks }] }, - { items: [{ key: 'feed', label: i18n.tribeSectionFeed }, { key: 'forum', label: i18n.tribeSectionForum }, { key: 'maps', label: i18n.tribeSectionMaps || 'MAPS' }, { key: 'torrents', label: i18n.tribeSectionTorrents || 'TORRENTS' }, { key: 'pads', label: i18n.tribeSectionPads || 'PADS' }, { key: 'chats', label: i18n.tribeSectionChats || 'CHATS' }, { key: 'calendars', label: i18n.tribeSectionCalendars || 'CALENDARS' }] }, - { items: [{ key: 'images', label: i18n.tribeSectionImages || 'IMAGES' }, { key: 'audios', label: i18n.tribeSectionAudios || 'AUDIOS' }, { key: 'videos', label: i18n.tribeSectionVideos || 'VIDEOS' }, { key: 'documents', label: i18n.tribeSectionDocuments || 'DOCUMENTS' }, { key: 'bookmarks', label: i18n.tribeSectionBookmarks || 'BOOKMARKS' }] }, + { items: [{ key: 'votations', label: i18n.tribeSectionVotations }, { key: 'polls', label: i18n.pollsTitle }, { key: 'events', label: i18n.tribeSectionEvents }, { key: 'tasks', label: i18n.tribeSectionTasks }] }, + { items: [{ key: 'feed', label: i18n.tribeSectionFeed }, { key: 'forum', label: i18n.tribeSectionForum }, { key: 'maps', label: i18n.tribeSectionMaps || 'MAPS' }, { key: 'torrents', label: i18n.tribeSectionTorrents || 'TORRENTS' }, { key: 'pads', label: i18n.tribeSectionPads || 'PADS' }, { key: 'wiki', label: i18n.tribeSectionWiki || 'WIKI' }, { key: 'chats', label: i18n.tribeSectionChats || 'CHATS' }, { key: 'rooms', label: i18n.tribeSectionRooms || 'ROOMS' }, { key: 'calendars', label: i18n.tribeSectionCalendars || 'CALENDARS' }] }, + { items: [{ key: 'images', label: i18n.tribeSectionImages || 'IMAGES' }, { key: 'audios', label: i18n.tribeSectionAudios || 'AUDIOS' }, { key: 'videos', label: i18n.tribeSectionVideos || 'VIDEOS' }, { key: 'documents', label: i18n.tribeSectionDocuments || 'DOCUMENTS' }, { key: 'files', label: i18n.tribeSectionFiles || 'FILES' }, { key: 'bookmarks', label: i18n.tribeSectionBookmarks || 'BOOKMARKS' }] }, { items: [{ key: 'tags', label: i18n.tribeSectionTags || 'TAGS' }, { key: 'search', label: i18n.tribeSectionSearch }] }, ]; + if (mobileUi.isMobile()) return mobileUi.tribeTabs(tribe, section, sections); return div({ class: 'tribe-section-nav no-border' }, sections.map(g => div({ class: 'tribe-section-group no-border' }, g.items.map(s => s.href ? form({ method: 'GET', action: s.href }, - button({ type: 'submit', class: 'filter-btn' }, s.label) + button({ type: 'submit', class: 'filter-btn' }, String(s.label).toUpperCase()) ) : sectionLink(tribe, s.key, s.label, section)) ) @@ -430,11 +440,6 @@ const priorityI18n = () => ({ 'CRITICAL': i18n.tribePriorityCritical, }); -const inviteModeI18n = () => ({ - 'strict': i18n.tribeStrict, - 'open': i18n.tribeOpen, -}); - const forumCatI18n = () => ({ 'GENERAL': i18n.tribeForumCatGeneral, 'PROPOSAL': i18n.tribeForumCatProposal, @@ -442,22 +447,6 @@ const forumCatI18n = () => ({ 'ANNOUNCEMENT': i18n.tribeForumCatAnnouncement, }); -const mediaTypeI18n = () => ({ - 'all': i18n.tribeMediaFilterAll, - 'image': i18n.tribeMediaTypeImage, - 'video': i18n.tribeMediaTypeVideo, - 'audio': i18n.tribeMediaTypeAudio, - 'document': i18n.tribeMediaTypeDocument, - 'bookmark': i18n.tribeMediaTypeBookmark, -}); - -const taskFilterI18n = () => ({ - 'all': i18n.tribeTaskFilterAll, - 'open': i18n.tribeStatusOpen, - 'in-progress': i18n.tribeStatusInProgress, - 'closed': i18n.tribeStatusClosed, -}); - const statusBadge = (status) => { const cls = status === 'OPEN' ? 'tribe-content-status-open' : status === 'CLOSED' ? 'tribe-content-status-closed' @@ -476,27 +465,27 @@ const contentTypeVerb = (ct) => { }; const contentTypeName = (ct) => { - const map = { event: i18n.tribeSectionEvents, task: i18n.tribeSectionTasks, votation: i18n.tribeSectionVotations, forum: i18n.tribeSectionForum, 'forum-reply': i18n.tribeSectionForum, media: i18n.tribeSectionMedia, feed: i18n.tribeSectionFeed, pad: i18n.tribeSectionPads || 'PADS', chat: i18n.tribeSectionChats || 'CHATS', calendar: i18n.tribeSectionCalendars || 'CALENDARS', map: i18n.tribeSectionMaps || 'MAPS' }; + const map = { event: i18n.tribeSectionEvents, task: i18n.tribeSectionTasks, votation: i18n.tribeSectionVotations, poll: String(i18n.pollsTitle || 'POLLS').toUpperCase(), forum: i18n.tribeSectionForum, 'forum-reply': i18n.tribeSectionForum, blog: String(i18n.blogTitle || 'BLOGS').toUpperCase(), media: i18n.tribeSectionMedia, feed: i18n.tribeSectionFeed, pad: i18n.tribeSectionPads || 'PADS', chat: i18n.tribeSectionChats || 'CHATS', calendar: i18n.tribeSectionCalendars || 'CALENDARS', map: i18n.tribeSectionMaps || 'MAPS', wiki: i18n.tribeSectionWiki || 'WIKI' }; return map[ct] || ct; }; const activitySectionMap = { - event: 'events', task: 'tasks', votation: 'votations', + event: 'events', task: 'tasks', votation: 'votations', poll: 'polls', forum: 'forum', 'forum-reply': 'forum', feed: 'feed', - pad: 'pads', chat: 'chats', calendar: 'calendars', map: 'maps', torrent: 'torrents' + pad: 'pads', chat: 'chats', calendar: 'calendars', map: 'maps', wiki: 'wiki', torrent: 'torrents', file: 'files' }; const activitySectionForItem = (item) => { if (item.contentType === 'media' && item.mediaType) { - const map = { image: 'images', audio: 'audios', video: 'videos', document: 'documents', bookmark: 'bookmarks', torrent: 'torrents' }; + const map = { image: 'images', audio: 'audios', video: 'videos', document: 'documents', bookmark: 'bookmarks', torrent: 'torrents', file: 'files' }; return map[item.mediaType] || 'media'; } return activitySectionMap[item.contentType] || 'activity'; }; const activityMediaTypeName = (mt) => { - const map = { image: i18n.tribeSectionImages, audio: i18n.tribeSectionAudios, video: i18n.tribeSectionVideos, document: i18n.tribeSectionDocuments, bookmark: i18n.tribeSectionBookmarks, torrent: i18n.tribeSectionTorrents }; + const map = { image: i18n.tribeSectionImages, audio: i18n.tribeSectionAudios, video: i18n.tribeSectionVideos, document: i18n.tribeSectionDocuments, bookmark: i18n.tribeSectionBookmarks, torrent: i18n.tribeSectionTorrents, file: i18n.tribeSectionFiles }; return map[mt] || i18n.tribeSectionMedia || 'MEDIA'; }; @@ -504,10 +493,39 @@ const renderTribeActivitySection = (tribe, sectionData) => { const { activities } = sectionData || { activities: [] }; if (activities.length === 0) return div({ class: 'tribe-content-list' }, p(i18n.tribeActivityEmpty)); const tribeUrl = `/tribe/${encodeURIComponent(tribe.id)}`; + const threadsById = reachIndexOf(activities); return div({ class: 'tribe-content-list tribe-content-list-spaced' }, - activities.slice(0, 50).map(item => { + paged(activities).map(item => { if (item.encrypted) return div({ class: 'card card-rpg' }, div({ class: 'tribe-card-body' }, p({ class: 'tribe-meta-label' }, i18n.tribeContentEncrypted || 'Encrypted content'))); - const date = item.timestamp ? new Date(item.timestamp).toLocaleString() : ''; + if (item.contentType === 'chatThread') { + const replies = Array.isArray(item.replies) ? item.replies : []; + const tDate = item.timestamp ? moment(item.timestamp).format("YYYY/MM/DD HH:mm") : ''; + const headerText = item.tribeName + ? `[${String(i18n.typeChat || 'CHAT').toUpperCase()} · THREAD · ${item.tribeName}]` + : `[${String(i18n.typeChat || 'CHAT').toUpperCase()} · THREAD]`; + return div({ class: 'card card-rpg tribe-card-padded chat-thread' }, + div({ class: 'card-header' }, + h2({ class: 'card-label' }, headerText), + a({ href: item.directUrl, class: 'tribe-action-btn' }, i18n.viewDetails || 'View Details') + ), + div({ class: 'tribe-card-body' }, + item.title ? div({ class: 'card-field' }, span({ class: 'card-value' }, item.title)) : null, + tribeExcerpt(tribe, item.description), + replies.map(r => div({ class: 'thread-reply-item' }, + r.text ? p({ class: 'post-text post-text-pre' }, String(r.text)) : null, + div({ class: 'card-footer thread-reply-footer' }, + span({ class: 'date-link' }, r.createdAt ? moment(r.createdAt).format("YYYY/MM/DD HH:mm") : ''), + userLink(r.author, '') + ) + )) + ), + p({ class: 'card-footer' }, + span({ class: 'date-link' }, `${tDate}`), + userLink(item.author, item.authorName) + ) + ); + } + const date = item.timestamp ? moment(item.timestamp).format("YYYY/MM/DD HH:mm") : ''; const typeLabel = item.contentType === 'media' && item.mediaType ? activityMediaTypeName(item.mediaType) : contentTypeName(item.contentType); @@ -518,35 +536,36 @@ const renderTribeActivitySection = (tribe, sectionData) => { const blobUrl = item.contentType === 'media' ? toBlobUrl(item.image) : null; const mediaContent = item.contentType === 'media' && item.mediaType === 'image' && blobUrl - ? a({ href: blobUrl, target: '_blank' }, img({ src: blobUrl, alt: item.title || '', class: 'tribe-media-thumb' })) + ? a({ href: blobUrl, target: '_blank', rel: 'noopener noreferrer' }, img({ src: blobUrl, alt: item.title || '', class: 'tribe-media-thumb' })) : item.contentType === 'media' && item.mediaType === 'audio' && blobUrl ? audio({ src: blobUrl, controls: true, class: 'tribe-media-audio' }) : item.contentType === 'media' && item.mediaType === 'video' && blobUrl - ? video({ src: blobUrl, controls: true, class: 'tribe-media-thumb' }) + ? video({ src: blobUrl, controls: true, class: 'tribe-media-thumb', preload: 'metadata' }) : item.contentType === 'media' && item.mediaType === 'document' && blobUrl - ? a({ href: blobUrl, target: '_blank', class: 'tribe-action-btn' }, i18n.readDocument || 'Read Document') + ? a({ href: blobUrl, target: '_blank', rel: 'noopener noreferrer', class: 'tribe-action-btn' }, i18n.readDocument || 'Read Document') : item.contentType === 'media' && item.mediaType === 'bookmark' && (item.url || item.description) - ? a({ href: item.url || item.description, target: '_blank', class: 'tribe-action-btn' }, item.url || item.description) + ? a({ href: safeExternalHref(item.url || item.description), target: '_blank', rel: 'noopener noreferrer', class: 'tribe-action-btn' }, item.url || item.description) : null; return div({ class: 'card card-rpg tribe-card-padded' }, div({ class: 'card-header' }, h2({ class: 'card-label' }, headerText), item.directUrl - ? a({ href: item.directUrl, class: 'filter-btn' }, i18n.viewDetails || 'View Details') + ? a({ href: item.directUrl, class: 'tribe-action-btn' }, i18n.viewDetails || 'View Details') : form({ method: 'GET', action: tribeUrl }, input({ type: 'hidden', name: 'section', value: targetSection }), - button({ type: 'submit', class: 'filter-btn' }, i18n.viewDetails || 'View Details') + button({ type: 'submit', class: 'tribe-action-btn' }, i18n.viewDetails || 'View Details') ) ), div({ class: 'tribe-card-body' }, + renderTribeReachListChip(tribe, item, threadsById), item.title ? div({ class: 'card-field' }, span({ class: 'card-value' }, item.title) ) : null, mediaContent, - item.description && !(item.contentType === 'media' && item.mediaType === 'bookmark' && item.description === item.url) ? p(item.description.substring(0, 200)) : null + item.description && !(item.contentType === 'media' && item.mediaType === 'bookmark' && item.description === item.url) ? tribeExcerpt(tribe, item.description) : null ), p({ class: 'card-footer' }, - span({ class: 'date-link' }, `${date} ${i18n.performed || ''} `), + span({ class: 'date-link' }, `${date}`), userLink(item.author, item.authorName) ) ); @@ -564,7 +583,7 @@ const renderTribeTrendingSection = (tribe, sectionData, query) => { div({ class: 'tribe-content-header' }, h2(i18n.tribeSectionTrending)), div({ class: 'tribe-filter-bar' }, periodBtn('day', i18n.tribeTrendingPeriodDay), periodBtn('week', i18n.tribeTrendingPeriodWeek), periodBtn('all', i18n.tribeTrendingPeriodAll)), items.length === 0 ? p(i18n.tribeTrendingEmpty) : - items.slice(0, 30).map((item, idx) => { + paged(items.map((item, idx) => [item, idx])).map(([item, idx]) => { if (item.encrypted) return div({ class: 'tribe-content-card' }, div({ class: 'tribe-content-meta' }, span(`#${idx + 1}`)), p({ class: 'tribe-meta-label' }, i18n.tribeContentEncrypted || 'Encrypted content')); return div({ class: 'tribe-content-card' }, div({ class: 'tribe-content-meta' }, @@ -573,6 +592,7 @@ const renderTribeTrendingSection = (tribe, sectionData, query) => { span(`${i18n.tribeTrendingEngagement}: ${engagementScore(item)}`), (() => { const entries = Object.entries(item.opinions || {}); if (!entries.length) return null; const top = entries.reduce((a, b) => b[1] > a[1] ? b : a); return span(`| ${i18n.tribeTopCategory}: ${i18n['opinionCat' + top[0].charAt(0).toUpperCase() + top[0].slice(1)] || top[0]}`); })() ), + renderTribeReachListChip(tribe, item, null), item.title ? h2(item.title) : item.description ? h2(item.description) : null, div({ class: 'tribe-content-meta' }, item.refeeds ? span(`${i18n.tribeActivityRefeed}: ${item.refeeds}`) : null, @@ -585,12 +605,10 @@ const renderTribeTrendingSection = (tribe, sectionData, query) => { const renderTribeTagsSection = (tribe, sectionData, query) => { const tagsList = Array.isArray(sectionData?.tags) ? sectionData.tags : []; - const selectedTag = sectionData?.selectedTag || ''; - const filteredItems = Array.isArray(sectionData?.filteredItems) ? sectionData.filteredItems : []; - const tribeUrl = `/tribe/${encodeURIComponent(tribe.id)}`; const sortedTags = tagsList.slice().sort((a, b) => b.count - a.count || a.tag.localeCompare(b.tag)); + const tagHref = tribeTagHref(tribe); return section( - div({ class: 'tags-header' }, + div({ class: 'tags-header module-header-line' }, h2(i18n.tribeSectionTags || i18n.tagsTitle || 'TAGS'), p(i18n.tagsDescription || '') ), @@ -603,28 +621,13 @@ const renderTribeTagsSection = (tribe, sectionData, query) => { th(i18n.tagsTableHeaderCount || 'Count') )), tbody( - sortedTags.map(t => tr( - td(a({ href: `${tribeUrl}?section=tags&tag=${encodeURIComponent(t.tag)}` }, t.tag)), + paged(sortedTags).map(t => tr( + td(a({ href: tagHref(t.tag) }, t.tag)), td(`${t.count}`) )) ) ) - ), - selectedTag ? div({ class: 'tribe-content-list' }, - h2(`#${selectedTag} (${filteredItems.length})`), - filteredItems.length === 0 ? p(i18n.tribeTagsEmpty || 'No items') : - filteredItems.slice(0, 50).map(item => div({ class: 'card card-rpg' }, - div({ class: 'card-header' }, h2({ class: 'card-label' }, `[${String(contentTypeName(item.contentType)).toUpperCase()}]`)), - div({ class: 'card-body' }, - item.title ? div({ class: 'card-field' }, span({ class: 'card-value' }, item.title)) : null, - item.description ? p(item.description.substring(0, 200)) : null - ), - p({ class: 'card-footer' }, - span({ class: 'date-link' }, new Date(item.createdAt).toLocaleString()), - userLink(item.author) - ) - )) - ) : null + ) ); }; @@ -632,30 +635,35 @@ const renderTribeSearchSection = (tribe, sectionData, query) => { const { results } = sectionData || { results: [] }; const sq = sectionData?.query || ''; const tribeUrl = `/tribe/${encodeURIComponent(tribe.id)}`; + const threadsById = reachIndexOf(results); + const hashtag = sq.trim().startsWith('#') ? sq.trim().replace(/^#+/, '').toLowerCase() : ''; + const typeCounts = new Map(); + for (const item of results) { + const label = String(contentTypeName(item.contentType)).toUpperCase(); + typeCounts.set(label, (typeCounts.get(label) || 0) + 1); + } + const stats = renderModuleStats(results.length, [...typeCounts].map(([label, count]) => ({ label, count })).sort((x, y) => y.count - x.count || x.label.localeCompare(y.label))); return div({ class: 'tribe-content-list' }, - div({ class: 'tribe-content-header' }, h2(i18n.tribeSectionSearch)), - form({ method: 'GET', action: tribeUrl, class: 'tribe-search-form' }, - input({ type: 'hidden', name: 'section', value: 'search' }), - input({ type: 'text', name: 'q', value: sq, placeholder: i18n.tribeSearchPlaceholder, minlength: 2 }), - button({ type: 'submit', class: 'create-button' }, i18n.tribeSectionSearch) - ), + div({ class: 'tags-header module-header-line' }, h2(i18n.tribeSectionSearch), p(hashtag ? i18n.hashtagDescription : i18n.searchDescriptionLabel)), + renderSearchForm({ action: tribeUrl, query: sq, queryName: 'q', hidden: [['section', 'search']] }), sq.length > 0 && sq.length < 2 ? p(i18n.tribeSearchMinChars) : null, sq.length >= 2 ? div( - h2(`${i18n.tribeSearchResults}: ${results.length}`), + stats ? div({ class: 'filters activity-filter-chips activity-toolbar-row search-stats-row' }, stats) : null, results.length === 0 ? p(i18n.tribeSearchEmpty) : - results.map(item => div({ class: 'card card-rpg' }, + paged(results).map(item => div({ class: 'card card-rpg' }, div({ class: 'card-header' }, h2({ class: 'card-label' }, `[${String(contentTypeName(item.contentType)).toUpperCase()}]`) ), div({ class: 'card-body' }, + renderTribeReachListChip(tribe, item, threadsById), item.title ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.title || 'Title') + ':'), span({ class: 'card-value' }, item.title) ) : null, - item.description ? p(item.description.substring(0, 200)) : null + tribeExcerpt(tribe, item.description) ), p({ class: 'card-footer' }, - span({ class: 'date-link' }, new Date(item.createdAt).toLocaleDateString()), + span({ class: 'date-link' }, moment(item.createdAt).format("YYYY/MM/DD HH:mm")), userLink(item.author) ) )) @@ -677,8 +685,8 @@ const renderOverviewSection = (tribe, query, sectionData) => { recentFeed.length === 0 ? p(i18n.tribeFeedEmpty) : recentFeed.map(m => div({ class: 'feed-item' }, - p(`${new Date(m.createdAt).toLocaleString()} — `, userLink(m.author)), - p(...renderUrl(m.description)) + p(`${moment(m.createdAt).format("YYYY/MM/DD HH:mm")} — `, userLink(m.author)), + p(...tribeText(tribe, m.description)) ) ) ), @@ -719,23 +727,831 @@ const renderOverviewSection = (tribe, query, sectionData) => { ); }; -const renderInhabitantsSection = (tribe, members) => { - const resolved = Array.isArray(members) ? members : []; - if (resolved.length === 0) return p(i18n.tribeInhabitantsEmpty); - return div({ class: 'tribe-thumb-grid' }, - resolved.map(m => - a({ href: `/author/${encodeURIComponent(m.id)}`, class: 'tribe-thumb-link', title: m.name || m.id }, - img({ src: resolvePhoto(m.photo), class: 'tribe-thumb-img', alt: m.name || m.id }) +const tribeUrlOf = (tribe) => `/tribe/${encodeURIComponent(tribe.id)}`; +const sectionHref = (tribe, sectionKey, params = {}) => { + const qs = Object.entries({ section: sectionKey, ...params }) + .filter(([, v]) => v !== undefined && v !== null && String(v) !== '') + .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`) + .join('&'); + return `${tribeUrlOf(tribe)}?${qs}`; +}; +const asList = (v) => Array.isArray(v) ? v : []; +const tsOf = (item) => Date.parse(item.createdAt) || item._ts || 0; +const isRecentItem = (item) => tsOf(item) >= Date.now() - MS_PER_DAY; +const isMineItem = (item) => String(item.author) === String(userId); +const tribeStatusOf = (item) => String(item.status || 'OPEN').toUpperCase(); +const capFirst = (s) => String(s).charAt(0).toUpperCase() + String(s).slice(1); +const blobIdOf = (raw) => { + const s = String(raw || '').trim(); + if (s.startsWith('&') || s.startsWith('/blob/sealed/')) return s; + const m = s.match(/\((&[^)\s]+\.sha256)\s*\)/); + return m ? m[1] : null; +}; +const matchesTribeQuery = (item, q) => { + if (!q) return true; + const needle = q.toLowerCase(); + return [item.title, item.description, item.question, item.url, item.location, item.number, ...asList(item.tags)] + .some(v => String(v || '').toLowerCase().includes(needle)); +}; +const sortRecentFirst = (items) => [...items].sort((a, b) => tsOf(b) - tsOf(a)); +const BASE_MODES = { recent: isRecentItem, mine: isMineItem, all: () => true, top: () => true }; +const applyTribeFilter = (items, filter, q, modes = BASE_MODES, score = engagementScore) => { + const pred = modes[filter] || modes.all || (() => true); + const kept = asList(items).filter(it => pred(it) && matchesTribeQuery(it, q)); + return filter === 'top' ? [...kept].sort((a, b) => score(b) - score(a) || tsOf(b) - tsOf(a)) : sortRecentFirst(kept); +}; +const tribeChipVisible = (items, filter, modes) => (mode) => + mode === filter || mode === 'all' || mode === 'ALL' || asList(items).some(it => (modes[mode] || (() => true))(it)); +const openClosedModes = (isClosed) => ({ ...BASE_MODES, open: it => !isClosed(it), closed: isClosed }); +const openClosedStats = (items, isClosed, openLabel, closedLabel) => + renderModuleStatsBy(items, it => isClosed(it) ? 'CLOSED' : 'OPEN', [{ value: 'OPEN', label: openLabel }, { value: 'CLOSED', label: closedLabel }]); + +const renderTribeSectionHeader = (title, description) => + div({ class: 'tags-header module-header-line' }, h2(title), description ? p(description) : null); + +const renderTribeModeChips = (tribe, sectionKey, { modes = [], filter = 'all', labelOf = (m) => m, visible = null, emptyMod = false, create = null }) => + div({ class: 'mode-buttons-row' }, + ...(emptyMod ? [] : modes.filter(m => !visible || visible(m)).map(m => + form({ method: 'GET', action: tribeUrlOf(tribe) }, + input({ type: 'hidden', name: 'section', value: sectionKey }), + input({ type: 'hidden', name: 'filter', value: m }), + button({ type: 'submit', class: filter === m ? 'filter-btn active' : 'filter-btn' }, String(labelOf(m) || m).toUpperCase()) + ) + )), + create + ? form({ method: 'GET', action: create.action || tribeUrlOf(tribe) }, + ...Object.entries(create.hidden || { section: sectionKey, action: 'create' }).map(([k, v]) => input({ type: 'hidden', name: k, value: v })), + button({ type: 'submit', class: 'create-button' }, create.label) + ) + : null + ); + +const renderTribeSearchRow = (tribe, sectionKey, filter, q, statsNode, placeholder, paramName = 'q') => + div({ class: 'filters activity-filter-chips activity-toolbar-row' }, + statsNode, + form({ method: 'GET', action: tribeUrlOf(tribe), class: 'filter-box' }, + input({ type: 'hidden', name: 'section', value: sectionKey }), + filter ? input({ type: 'hidden', name: 'filter', value: filter }) : null, + input({ type: 'text', name: paramName, value: q || '', placeholder, class: 'filter-box__input' }), + div({ class: 'filter-box__controls' }, button({ type: 'submit', class: 'filter-box__button' }, i18n.searchButton)) + ) + ); + +const renderTribeEmpty = (text) => div({ class: 'no-content-box' }, p(text)); + +const tribeDescription = (text) => text ? p({ class: 'tribe-card-description' }, ...renderStyledText(String(text))) : null; + +const tribeReachChipFor = (tribe, item) => { + const level = reachLevelOf(item); + return tribe.reachAllowed || level !== 'tribe' ? renderTribeReachChip(level, tribe) : null; +}; + +const renderTribeReachControls = (tribe, item, returnTo) => { + const levelSwitch = returnTo && tribe.reachAllowed && isTribeMember(tribe) && item.contentType !== 'forum-reply' && item.id ? renderTribeReachSwitch(tribe, item, returnTo) : null; + const openedBy = item.exposedBy && item.exposedBy !== item.author + ? span({ class: 'tribe-meta-label' }, `${i18n.tribeReachOpenedBy || 'Opened by'}: `, userLink(item.exposedBy)) + : null; + return levelSwitch || openedBy ? div({ class: 'card-chips-row' }, levelSwitch, openedBy) : null; +}; + +const tribeItemActions = (tribe, item, member, title) => + renderContentActions(item.id, null, { + author: item.author, + reportTitle: title || item.title || item.description, + deleteAction: member && isMineItem(item) && item.id ? `${tribeUrlOf(tribe)}/content/delete/${encodeURIComponent(item.id)}` : undefined + }); + +const tribeEngagement = (tribe, item, member, returnTo) => { + if (member) { + return renderEngagement(item.id, renderOpinionsVoting(null, item.id, item.opinions, returnTo, item.opinions_inhabitants, + (id, category) => `${tribeUrlOf(tribe)}/content/${encodeURIComponent(id)}/opinion/${category}`)); + } + const summary = renderVotesSummary(item.opinions); + return summary ? renderEngagement(item.id, summary) : null; +}; + +const toFeedShape = (m) => ({ + key: m.id, + value: { author: m.author, timestamp: tsOf(m), content: { text: m.description, author: m.author, refeeds: m.refeeds, refeeds_inhabitants: m.refeeds_inhabitants, opinions: m.opinions, opinions_inhabitants: m.opinions_inhabitants } } +}); + +const renderFeedComposeForm = (tribe) => + form({ method: 'POST', action: `${tribeUrlOf(tribe)}/message` }, + textarea({ name: 'message', required: true, minlength: '1', maxlength: String(MAX_MESSAGE_LENGTH), rows: '4', cols: 50, placeholder: i18n.feedPlaceholder }), + tribe.reachAllowed ? renderTribeReachSelector() : null, + div({ class: 'feed-compose-actions' }, + button({ type: 'submit', class: 'create-button' }, i18n.createFeedButton || 'Send Feed!') + ) + ); + +const FEED_MODES = ['ALL', 'RECENT', 'MINE', 'TOP']; +const renderFeedTribeView = (feedItems, tribe, query = {}) => { + const feed = asList(feedItems); + const member = isTribeMember(tribe); + const filter = String(query.filter || query.feedFilter || 'ALL').toUpperCase(); + const q = String(query.q || '').trim(); + const isCreate = query.action === 'create' && member; + const tribeUrl = tribeUrlOf(tribe); + const header = renderTribeSectionHeader(isCreate ? i18n.createFeedTitle : i18n.tribeSectionFeed, i18n.FeedshareYourOpinions); + const emptyMod = !isCreate && moduleIsEmpty(feed, filter, 'ALL', q); + const visible = tribeChipVisible(feed, filter, { RECENT: isRecentItem, MINE: isMineItem, ALL: () => true, TOP: () => true }); + const chips = renderTribeModeChips(tribe, 'feed', { modes: FEED_MODES, filter: isCreate ? 'CREATE' : filter, labelOf: m => i18n[`${m}Button`], visible, emptyMod, create: member ? { label: i18n.createFeedTitle || 'Create Feed' } : null }); + if (isCreate) return div({ class: 'tribe-content-list' }, header, chips, renderFeedComposeForm(tribe)); + const items = applyTribeFilter(feed, filter.toLowerCase(), q, BASE_MODES, it => Number(it.refeeds || 0)); + const returnTo = sectionHref(tribe, 'feed', { filter, q }); + const banner = query.sent ? div({ class: 'feed-success-msg' }, p('✓ ' + (i18n.feedPublishedSuccess || i18n.tribeFeedSent))) : null; + const cards = paged(items).map(m => { + const chip = tribeReachChipFor(tribe, m); + const controls = renderTribeReachControls(tribe, m, returnTo); + return renderFeedCard(toFeedShape(m), null, { + hashtagHref: tribeTagHref(tribe), + refeedAction: `${tribeUrl}/refeed/${encodeURIComponent(m.id)}`, + canRefeed: member, + headerActions: tribeItemActions(tribe, m, member, m.description), + chips: chip || controls ? [chip ? div({ class: 'card-chips-row' }, chip) : null, controls] : null, + engagement: tribeEngagement(tribe, m, member, returnTo) + }); + }).filter(Boolean); + return div({ class: 'tribe-content-list' }, + header, + banner, + chips, + emptyMod ? null : renderTribeSearchRow(tribe, 'feed', filter, q, renderModuleStats(items.length), i18n.feedSearchPlaceholder), + cards.length ? div({ class: 'feed-container' }, ...cards) : renderTribeEmpty(i18n.noFeedsFound || i18n.tribeFeedEmpty) + ); +}; + +const EVENT_MODES = { + ...BASE_MODES, + today: e => moment(e.date).isSame(moment(), 'day'), + week: e => moment(e.date).isBetween(moment(), moment().add(7, 'days'), null, '[]'), + month: e => moment(e.date).isBetween(moment(), moment().add(1, 'month'), null, '[]'), + year: e => moment(e.date).isBetween(moment(), moment().add(1, 'year'), null, '[]'), + archived: e => tribeStatusOf(e) === 'CLOSED' +}; +const EVENT_MODE_LIST = ['all', 'recent', 'mine', 'today', 'week', 'month', 'year', 'archived']; + +const renderTribeEventCard = (tribe, e, member, returnTo) => { + const tribeUrl = tribeUrlOf(tribe); + const isOpen = tribeStatusOf(e) === 'OPEN'; + const attending = asList(e.attendees).includes(userId); + return renderEventItem({ ...e, organizer: e.author, isPublic: reachLevelOf(e) === 'tribe' ? 'private' : 'public' }, 'all', null, { + href: null, + headerActions: tribeItemActions(tribe, e, member, e.title), + extraChips: [tribeReachChipFor(tribe, e)], + bodyExtra: [ + tribeDescription(e.description), + renderTribeReachControls(tribe, e, returnTo), + member && isOpen + ? div({ class: 'tribe-side-actions' }, + form({ method: 'POST', action: `${tribeUrl}/events/attend/${encodeURIComponent(e.id)}` }, + button({ type: 'submit', class: 'tribe-action-btn' }, attending ? i18n.tribeEventUnattend : i18n.tribeEventAttend) + ) + ) + : null, + tribeEngagement(tribe, e, member, returnTo) + ] + }); +}; + +const renderEventsSection = (tribe, items, query = {}) => { + const events = asList(items); + const member = isTribeMember(tribe); + const filter = String(query.filter || 'all').toLowerCase(); + const q = String(query.q || '').trim(); + const isCreate = query.action === 'create' && member; + const tribeUrl = tribeUrlOf(tribe); + const header = renderTribeSectionHeader(i18n.tribeSectionEvents, i18n.eventsDescription); + const emptyMod = !isCreate && moduleIsEmpty(events, filter, 'all', q); + const chips = renderTribeModeChips(tribe, 'events', { modes: EVENT_MODE_LIST, filter: isCreate ? 'create' : filter, labelOf: m => i18n[`eventFilter${capFirst(m)}`], visible: tribeChipVisible(events, filter, EVENT_MODES), emptyMod, create: member ? { label: i18n.eventCreateButton } : null }); + if (isCreate) { + const today = new Date().toISOString().split('T')[0]; + return div({ class: 'tribe-content-list' }, header, chips, + div({ class: 'event-form' }, + form({ method: 'POST', action: `${tribeUrl}/events/create` }, + label(i18n.eventTitleLabel), br(), + input({ type: 'text', name: 'title', id: 'title', maxlength: '100', required: true }), br(), + label(i18n.eventDescriptionLabel), br(), + textarea({ name: 'description', id: 'description', rows: '4', maxlength: String(TEXT_CAP), required: true, placeholder: i18n.eventDescriptionPlaceholder }), br(), + label(i18n.eventDateLabel), br(), + input({ type: 'date', name: 'date', id: 'date', required: true, min: today }), br(), br(), + label(i18n.eventLocationLabel), br(), + input({ type: 'text', name: 'location', id: 'location' }), br(), br(), + tribe.reachAllowed ? renderTribeReachSelector() : null, + button({ type: 'submit' }, i18n.eventCreateButton) + ) + ) + ); + } + const list = applyTribeFilter(events, filter, q, EVENT_MODES); + const returnTo = sectionHref(tribe, 'events', { filter, q }); + return div({ class: 'tribe-content-list' }, + header, + chips, + emptyMod ? null : renderTribeSearchRow(tribe, 'events', filter, q, openClosedStats(list, e => tribeStatusOf(e) === 'CLOSED', i18n.eventStatusOpen, i18n.eventStatusClosed), i18n.eventSearchPlaceholder), + list.length ? div({ class: 'jobs-grid' }, ...paged(list).map(e => renderTribeEventCard(tribe, e, member, returnTo))) : renderTribeEmpty(i18n.noevents || i18n.tribeEventsEmpty) + ); +}; + +const TASK_PRIORITIES = ['LOW', 'MEDIUM', 'HIGH', 'CRITICAL']; +const TASK_MODES = { + ...BASE_MODES, + assigned: t => asList(t.assignees).includes(userId), + open: t => tribeStatusOf(t) === 'OPEN', + 'in-progress': t => tribeStatusOf(t) === 'IN-PROGRESS', + closed: t => tribeStatusOf(t) === 'CLOSED', + ...Object.fromEntries(TASK_PRIORITIES.map(pr => [`priority-${pr.toLowerCase()}`, t => String(t.priority || '').toUpperCase() === pr])) +}; +const TASK_MODE_LIST = ['all', 'recent', 'mine', 'assigned', 'open', 'in-progress', 'closed', ...TASK_PRIORITIES.map(pr => `priority-${pr.toLowerCase()}`)]; +const taskModeLabel = (m) => { + if (m === 'in-progress') return i18n.taskFilterInProgress; + if (m === 'priority-critical') return i18n.tribePriorityCritical; + if (m.startsWith('priority-')) return i18n[`taskFilter${capFirst(m.slice(9))}`]; + return i18n[`taskFilter${capFirst(m)}`]; +}; + +const renderTribeTaskCard = (tribe, t, member, returnTo) => { + const tribeUrl = tribeUrlOf(tribe); + const assigned = asList(t.assignees).includes(userId); + const assignRow = member ? renderTaskAssignAction(t, assigned, returnTo, `${tribeUrl}/tasks/assign/${encodeURIComponent(t.id)}`) : null; + return renderTaskItem({ ...t, isPublic: reachLevelOf(t) === 'tribe' ? 'PRIVATE' : 'PUBLIC' }, 'all', null, { + href: null, + headerActions: tribeItemActions(tribe, t, member, t.title), + extraChips: [tribeReachChipFor(tribe, t)], + bodyExtra: [ + tribeDescription(t.description), + t.deadline ? p({ class: 'job-meta-line' }, `${i18n.tribeTaskDeadline}: ${t.deadline}`) : null, + renderTribeReachControls(tribe, t, returnTo), + assignRow ? div({ class: 'tribe-side-actions housing-status-row task-assign-row' }, ...assignRow) : null, + member && isMineItem(t) ? renderTaskStatusRow(t, returnTo, `${tribeUrl}/tasks/status/${encodeURIComponent(t.id)}`) : null, + tribeEngagement(tribe, t, member, returnTo) + ] + }); +}; + +const renderTasksSection = (tribe, items, query = {}) => { + const tasks = asList(items); + const member = isTribeMember(tribe); + const filter = String(query.filter || 'all').toLowerCase(); + const q = String(query.q || '').trim(); + const isCreate = query.action === 'create' && member; + const tribeUrl = tribeUrlOf(tribe); + const header = renderTribeSectionHeader(i18n.tribeSectionTasks, i18n.tasksDescription); + const emptyMod = !isCreate && moduleIsEmpty(tasks, filter, 'all', q); + const chips = renderTribeModeChips(tribe, 'tasks', { modes: TASK_MODE_LIST, filter: isCreate ? 'create' : filter, labelOf: taskModeLabel, visible: tribeChipVisible(tasks, filter, TASK_MODES), emptyMod, create: member ? { label: i18n.taskCreateButton } : null }); + if (isCreate) { + const today = new Date().toISOString().split('T')[0]; + return div({ class: 'tribe-content-list' }, header, chips, + div({ class: 'task-form' }, + form({ method: 'POST', action: `${tribeUrl}/tasks/create` }, + label(i18n.taskTitleLabel), br(), + input({ type: 'text', name: 'title', id: 'title', maxlength: '100', required: true }), br(), + label(i18n.taskDescriptionLabel), br(), + textarea({ name: 'description', id: 'description', rows: '4', maxlength: String(TEXT_CAP), required: true, placeholder: i18n.taskDescriptionPlaceholder }), br(), + label(i18n.taskPriorityLabel), br(), + select({ name: 'priority', id: 'priority', required: true }, + ...TASK_PRIORITIES.map(pr => option({ value: pr }, i18n[`tribePriority${capFirst(pr.toLowerCase())}`])) + ), br(), br(), + label(i18n.tribeTaskDeadline), br(), + input({ type: 'date', name: 'deadline', id: 'deadline', min: today }), br(), br(), + tribe.reachAllowed ? renderTribeReachSelector() : null, + button({ type: 'submit' }, i18n.taskCreateButton) + ) + ) + ); + } + const list = applyTribeFilter(tasks, filter, q, TASK_MODES); + const returnTo = sectionHref(tribe, 'tasks', { filter, q }); + return div({ class: 'tribe-content-list' }, + header, + chips, + emptyMod ? null : renderTribeSearchRow(tribe, 'tasks', filter, q, renderModuleStatsBy(list, tribeStatusOf, [{ value: 'OPEN', label: i18n.taskStatusOpen }, { value: 'IN-PROGRESS', label: i18n.taskStatusInProgress }, { value: 'CLOSED', label: i18n.taskStatusClosed }]), i18n.taskSearchPlaceholder), + list.length ? div({ class: 'jobs-grid' }, ...paged(list).map(t => renderTribeTaskCard(tribe, t, member, returnTo))) : renderTribeEmpty(i18n.notasks || i18n.tribeTasksEmpty) + ); +}; + +const toVoteShape = (v) => { + const options = asList(v.options).map(o => String(o)); + const votes = v.votes || {}; + const counts = {}; + options.forEach((o, i) => { counts[o] = (counts[o] || 0) + asList(votes[String(i)]).length; }); + const totalVotes = Object.values(counts).reduce((s, n) => s + n, 0); + return { ...v, question: v.title, createdBy: v.author, options, votes: counts, totalVotes }; +}; +const VOTE_MODES = openClosedModes(v => tribeStatusOf(v) === 'CLOSED'); +const VOTE_MODE_LIST = ['all', 'recent', 'mine', 'open', 'closed']; + +const renderTribeVotationCard = (tribe, v, member, returnTo) => { + const tribeUrl = tribeUrlOf(tribe); + const shaped = toVoteShape(v); + const isOpen = tribeStatusOf(v) === 'OPEN'; + const hasVoted = Object.values(v.votes || {}).some(arr => asList(arr).includes(userId)); + return renderVoteListItem(shaped, shaped.options, 'all', null, { + href: null, + headerActions: tribeItemActions(tribe, v, member, v.title), + extraChips: [tribeReachChipFor(tribe, v)], + bodyExtra: [ + tribeDescription(v.description), + renderTribeReachControls(tribe, v, returnTo), + ...renderVoteOptionsBlock(shaped, shaped.options, `${tribeUrl}/votations/${encodeURIComponent(v.id)}/vote`, returnTo, member && isOpen && !hasVoted), + member && isMineItem(v) && isOpen + ? div({ class: 'tribe-side-actions' }, + form({ method: 'POST', action: `${tribeUrl}/votations/close/${encodeURIComponent(v.id)}` }, + button({ type: 'submit', class: 'tribe-action-btn' }, i18n.tribeVotationClose) + ) + ) + : null, + tribeEngagement(tribe, v, member, returnTo) + ] + }); +}; + +const renderVotationsSection = (tribe, items, query = {}) => { + const votations = asList(items); + const member = isTribeMember(tribe); + const filter = String(query.filter || 'all').toLowerCase(); + const q = String(query.q || '').trim(); + const isCreate = query.action === 'create' && member; + const tribeUrl = tribeUrlOf(tribe); + const header = renderTribeSectionHeader(i18n.tribeSectionVotations, i18n.votationsDescription); + const emptyMod = !isCreate && moduleIsEmpty(votations, filter, 'all', q); + const chips = renderTribeModeChips(tribe, 'votations', { modes: VOTE_MODE_LIST, filter: isCreate ? 'create' : filter, labelOf: m => i18n[`voteFilter${capFirst(m)}`], visible: tribeChipVisible(votations, filter, VOTE_MODES), emptyMod, create: member ? { label: i18n.voteCreateButton } : null }); + if (isCreate) { + const today = new Date().toISOString().split('T')[0]; + return div({ class: 'tribe-content-list' }, header, chips, + div({ class: 'vote-form' }, + form({ method: 'POST', action: `${tribeUrl}/votations/create` }, + h2(i18n.voteQuestionLabel), + input({ type: 'text', name: 'title', id: 'title', maxlength: '100', required: true }), br(), br(), + label(i18n.tribeVotationDescription), br(), + textarea({ name: 'description', id: 'description', rows: '3', maxlength: String(TEXT_CAP) }), br(), br(), + label(i18n.voteDeadlineLabel), br(), + input({ type: 'date', name: 'deadline', id: 'deadline', min: today }), br(), br(), + label(i18n.tribeVotationOptions), br(), + input({ type: 'text', name: 'option1', placeholder: `${i18n.tribeVotationOptionPlaceholder} 1`, required: true }), br(), + input({ type: 'text', name: 'option2', placeholder: `${i18n.tribeVotationOptionPlaceholder} 2`, required: true }), br(), + input({ type: 'text', name: 'option3', placeholder: `${i18n.tribeVotationOptionPlaceholder} 3` }), br(), + input({ type: 'text', name: 'option4', placeholder: `${i18n.tribeVotationOptionPlaceholder} 4` }), br(), br(), + tribe.reachAllowed ? renderTribeReachSelector() : null, + button({ type: 'submit' }, i18n.voteCreateButton) + ) + ) + ); + } + const list = applyTribeFilter(votations, filter, q, VOTE_MODES); + const returnTo = sectionHref(tribe, 'votations', { filter, q }); + return div({ class: 'tribe-content-list' }, + header, + chips, + emptyMod ? null : renderTribeSearchRow(tribe, 'votations', filter, q, openClosedStats(list, v => tribeStatusOf(v) === 'CLOSED', i18n.voteStatusOpen, i18n.voteStatusClosed), i18n.votesSearchPlaceholder), + list.length ? div({ class: 'jobs-grid' }, ...paged(list).map(v => renderTribeVotationCard(tribe, v, member, returnTo))) : renderTribeEmpty(i18n.novotes || i18n.tribeVotationsEmpty) + ); +}; + +const POLL_MODES = { + RECENT: isRecentItem, MINE: isMineItem, ALL: () => true, TOP: () => true, + VOTED: pl => !!pl.hasVoted || asList(pl.myChoices).length > 0, + OPEN: pl => tribeStatusOf(pl) === 'OPEN', CLOSED: pl => tribeStatusOf(pl) === 'CLOSED' +}; +const POLL_MODE_LIST = ['ALL', 'RECENT', 'MINE', 'TOP', 'VOTED', 'OPEN', 'CLOSED']; +const pollModeLabel = (m) => i18n[`pollFilter${capFirst(m.toLowerCase())}`]; + +const renderTribePollsSection = (tribe, items, query = {}) => { + const polls = asList(items).filter(pl => !pl.undecryptable); + const member = isTribeMember(tribe); + const filter = String(query.filter || 'ALL').toUpperCase(); + const q = String(query.q || '').trim(); + const isCreate = query.action === 'create' && member; + const tribeUrl = tribeUrlOf(tribe); + const header = renderTribeSectionHeader(i18n.pollsTitle, i18n.pollsDescription); + const emptyMod = !isCreate && moduleIsEmpty(polls, filter, 'ALL', q); + const chips = renderTribeModeChips(tribe, 'polls', { modes: POLL_MODE_LIST, filter: isCreate ? 'CREATE' : filter, labelOf: pollModeLabel, visible: tribeChipVisible(polls, filter, POLL_MODES), emptyMod, create: member ? { label: i18n.pollCreateButton } : null }); + if (isCreate) { + return div({ class: 'tribe-content-list' }, header, chips, + div({ class: 'div-center audio-form' }, + h2(i18n.pollCreateTitle), + form({ method: 'POST', action: `${tribeUrl}/polls/create` }, + label(i18n.pollQuestion), br(), + input({ type: 'text', name: 'question', required: true, maxlength: '300', placeholder: i18n.pollQuestionPlaceholder }), br(), br(), + label(i18n.pollOptions), br(), + textarea({ name: 'options', rows: '5', required: true, maxlength: '3000', placeholder: i18n.pollOptionsPlaceholder }, ''), br(), + div({ class: 'poll-switch' }, + input({ type: 'hidden', name: 'anonymous', value: '0' }), + label(input({ type: 'checkbox', name: 'anonymous', value: '1' }), ' ', i18n.pollAnonymousLabel) + ), + div({ class: 'poll-switch' }, + input({ type: 'hidden', name: 'multiple', value: '0' }), + label(input({ type: 'checkbox', name: 'multiple', value: '1' }), ' ', i18n.pollMultipleLabel) + ), + br(), + button({ type: 'submit', class: 'create-button' }, i18n.pollPublishButton) + ) + ) + ); + } + const pred = POLL_MODES[filter] || (() => true); + const kept = polls.filter(pl => pred(pl) && matchesTribeQuery(pl, q)); + const list = filter === 'TOP' ? [...kept].sort((a, b) => (b.totalVoters || 0) - (a.totalVoters || 0) || tsOf(b) - tsOf(a)) : sortRecentFirst(kept); + return div({ class: 'tribe-content-list' }, + header, + chips, + emptyMod ? null : renderTribeSearchRow(tribe, 'polls', filter, q, openClosedStats(list, pl => tribeStatusOf(pl) === 'CLOSED', i18n.pollStatusOpen, i18n.pollStatusClosed), i18n.pollSearchPlaceholder), + list.length ? div({ class: 'jobs-grid' }, ...paged(list).map(pl => renderPollCard(pl, filter, null))) : renderTribeEmpty(i18n.pollsNoItems) + ); +}; + +const FORUM_CATEGORIES = ['GENERAL', 'PROPOSAL', 'QUESTION', 'ANNOUNCEMENT']; +const forumCatLabel = (c) => forumCatI18n()[c] || c; +const forumScoreBox = (tribe, item, member, thread) => + div({ class: 'forum-score-box' }, + member + ? form({ method: 'POST', action: `${tribeUrlOf(tribe)}/forum/${encodeURIComponent(item.id)}/refeed${thread ? `?thread=${encodeURIComponent(thread)}` : ''}`, class: 'forum-score-form' }, + button({ type: 'submit', class: 'score-btn', title: i18n.refeedButton }, '▲'), + div({ class: 'score-total' }, String(item.refeeds || 0)) + ) + : div({ class: 'score-total' }, String(item.refeeds || 0)) + ); + +const renderForumThreadDetail = (tribe, allItems, threadId, member) => { + const tribeUrl = tribeUrlOf(tribe); + const thread = allItems.find(i => i.id === threadId); + if (!thread) return renderTribeEmpty(i18n.tribeForumEmpty); + const replies = allItems.filter(i => i.contentType === 'forum-reply' && i.parentId === threadId) + .sort((a, b) => (b.refeeds || 0) - (a.refeeds || 0) || tsOf(a) - tsOf(b)); + const participants = new Set([thread.author, ...replies.map(r => r.author)].filter(Boolean)); + const returnTo = sectionHref(tribe, 'forum', { thread: threadId }); + const reachChip = tribeReachChipFor(tribe, thread); + return div({ class: 'forum-thread-container' }, + div({ class: 'forum-card forum-thread-header' }, + div({ class: 'root-vote-col' }, forumScoreBox(tribe, thread, member, threadId)), + div({ class: 'forum-main-col' }, + div({ class: 'card-header activity-card-header' }, + renderContentActions(thread.id, null, { author: thread.author, reportTitle: thread.title, deleteAction: member && isMineItem(thread) ? `${tribeUrl}/content/delete/${encodeURIComponent(thread.id)}` : undefined }) + ), + div({ class: 'forum-header-row' }, + thread.category ? span({ class: 'forum-category' }, `[${forumCatLabel(thread.category)}]`) : null, + span({ class: 'forum-title' }, thread.title), + reachChip + ), + renderTribeReachControls(tribe, thread, returnTo), + div({ class: 'forum-body' }, ...tribeText(tribe, thread.description || '')), + div({ class: 'forum-meta' }, + span({ class: 'forum-positive-votes' }, `▲: ${thread.refeeds || 0}`), + span({ class: 'forum-participants' }, `${String(i18n.forumParticipants).toUpperCase()}: ${participants.size}`), + span({ class: 'forum-messages' }, `${String(i18n.forumMessages).toUpperCase()}: ${replies.length}`) + ), + div({ class: 'forum-footer' }, + span({ class: 'date-link' }, `${moment(thread.createdAt).format("YYYY/MM/DD HH:mm")}`), + userLink(thread.author) + ), + tribeEngagement(tribe, thread, member, returnTo) + ) + ), + member + ? div({ class: 'new-message-wrapper' }, + form({ method: 'POST', action: `${tribeUrl}/forum/${encodeURIComponent(threadId)}/reply`, class: 'new-message-form' }, + textarea({ maxlength: String(TEXT_CAP), name: 'description', rows: '4', required: true, placeholder: i18n.forumMessagePlaceholder, class: 'new-message-textarea' }), br(), + button({ type: 'submit', class: 'forum-send-btn' }, i18n.forumSendButton) + ) + ) + : null, + ...replies.map((r, idx) => + div({ class: `forum-comment level-0${idx === 0 ? ' highlighted-reply' : ''}` }, + div({ class: 'comment-header' }, + span({ class: 'date-link' }, `${moment(r.createdAt).format("YYYY/MM/DD HH:mm")}`), + userLink(r.author), + div({ class: 'comment-votes' }, + span({ class: 'votes-count' }, `▲: ${r.refeeds || 0}`), + tribeItemActions(tribe, r, member, r.description) + ) + ), + div({ class: 'comment-body-row' }, + div({ class: 'comment-vote-col' }, forumScoreBox(tribe, r, member, threadId)), + div({ class: 'comment-text-col' }, + div(...String(r.description || '').split('\n').map(l => l.trim()).filter(Boolean).map(l => p(...tribeText(tribe, l, { zoomImages: true })))) + ) + ) ) ) ); }; +const renderForumSection = (tribe, items, query = {}) => { + const allItems = asList(items); + const threads = allItems.filter(i => i.contentType === 'forum'); + const member = isTribeMember(tribe); + const filter = String(query.filter || 'all'); + const q = String(query.q || '').trim(); + const isCreate = query.action === 'create' && member; + const tribeUrl = tribeUrlOf(tribe); + const header = renderTribeSectionHeader(i18n.tribeSectionForum, i18n.forumDescription); + const replyCount = new Map(); + allItems.filter(i => i.contentType === 'forum-reply').forEach(r => replyCount.set(r.parentId, (replyCount.get(r.parentId) || 0) + 1)); + const forumScore = (t) => (t.refeeds || 0) + (replyCount.get(t.id) || 0); + const modes = { ...BASE_MODES, ...Object.fromEntries(FORUM_CATEGORIES.map(c => [c, t => t.category === c])) }; + const emptyMod = !isCreate && !query.thread && moduleIsEmpty(threads, filter, 'all', q); + const present = new Set(threads.map(t => t.category).filter(Boolean)); + const visible = (m) => FORUM_CATEGORIES.includes(m) ? (present.has(m) || m === filter) : tribeChipVisible(threads, filter, modes)(m); + const labelOf = (m) => FORUM_CATEGORIES.includes(m) ? forumCatLabel(m) : i18n[`forumFilter${capFirst(m)}`]; + const chips = renderTribeModeChips(tribe, 'forum', { modes: ['all', 'recent', 'mine', 'top', ...FORUM_CATEGORIES], filter: isCreate ? 'create' : filter, labelOf, visible, emptyMod, create: member ? { label: i18n.forumCreateButton } : null }); + + if (isCreate) { + return div({ class: 'tribe-content-list' }, header, chips, + div({ class: 'forum-form' }, + form({ method: 'POST', action: `${tribeUrl}/forum/create` }, + label(i18n.forumCategoryLabel), br(), + select({ name: 'category', required: true }, ...FORUM_CATEGORIES.map(c => option({ value: c }, forumCatLabel(c)))), br(), br(), + label(i18n.forumTitleLabel), br(), + input({ type: 'text', name: 'title', maxlength: '100', required: true, placeholder: i18n.forumTitlePlaceholder }), br(), br(), + label(i18n.forumMessageLabel), br(), + textarea({ maxlength: String(TEXT_CAP), name: 'description', required: true, rows: '4', placeholder: i18n.forumMessagePlaceholder }), br(), br(), + tribe.reachAllowed ? renderTribeReachSelector() : null, + button({ type: 'submit' }, i18n.forumCreateButton) + ) + ) + ); + } + + if (query.thread) { + return div({ class: 'tribe-content-list' }, header, chips, + form({ method: 'GET', action: tribeUrl, class: 'forum-back-form' }, + input({ type: 'hidden', name: 'section', value: 'forum' }), + button({ type: 'submit', class: 'tribe-action-btn' }, i18n.walletBack) + ), + renderForumThreadDetail(tribe, allItems, query.thread, member) + ); + } + + const list = applyTribeFilter(threads, filter, q, modes, forumScore); + return div({ class: 'tribe-content-list' }, + header, + chips, + emptyMod ? null : renderTribeSearchRow(tribe, 'forum', filter, q, renderModuleStats(list.length), i18n.forumSearchPlaceholder), + list.length + ? ul({ class: 'mailing-archive' }, + ...paged(list).map(t => { + const participants = new Set([t.author, ...allItems.filter(i => i.contentType === 'forum-reply' && i.parentId === t.id).map(r => r.author)].filter(Boolean)); + return li({ class: 'mailing-archive-item' }, + div({ class: 'emergency-update-head mailing-archive-head' }, + div({ class: 'mailing-archive-meta' }, + a({ href: sectionHref(tribe, 'forum', { thread: t.id }), class: 'user-link' }, t.title || '—'), + t.category ? span({ class: 'forum-category' }, `[${forumCatLabel(t.category)}]`) : null, + span({ class: 'card-label activity-update-counts mailing-counts' }, `👥: ${participants.size} · 💬 ${replyCount.get(t.id) || 0} · ▲ ${t.refeeds || 0}`), + tribeReachChipFor(tribe, t) + ), + tribeItemActions(tribe, t, member, t.title) + ) + ); + }) + ) + : renderTribeEmpty(i18n.noForums || i18n.tribeForumEmpty) + ); +}; + +const MEDIA_SECTIONS = { + image: { key: 'images', list: renderImageList, wrapper: 'images-list', formClass: 'image-form', title: 'tribeSectionImages', description: 'imageDescription', create: 'imageCreateButton', search: 'imageSearchPlaceholder', empty: 'noImages', file: 'imageFileLabel', titleLabel: 'imageTitleLabel', descLabel: 'imageDescriptionLabel', filter: 'imageFilter', accept: 'image/*' }, + audio: { key: 'audios', list: renderAudioList, wrapper: 'audios-list', formClass: 'audio-form', title: 'tribeSectionAudios', description: 'audioDescription', create: 'audioCreateButton', search: 'audioSearchPlaceholder', empty: 'noAudios', file: 'audioFileLabel', titleLabel: 'audioTitleLabel', descLabel: 'audioDescriptionLabel', filter: 'audioFilter', accept: 'audio/*' }, + video: { key: 'videos', list: renderVideoList, wrapper: 'videos-list', formClass: 'video-form', title: 'tribeSectionVideos', description: 'videoDescription', create: 'videoCreateButton', search: 'videoSearchPlaceholder', empty: 'noVideos', file: 'videoFileLabel', titleLabel: 'videoTitleLabel', descLabel: 'videoDescriptionLabel', filter: 'videoFilter', accept: 'video/*' }, + document: { key: 'documents', list: renderDocumentList, wrapper: 'documents-list', formClass: 'document-form', title: 'tribeSectionDocuments', description: 'documentDescription', create: 'documentCreateButton', search: 'documentSearchPlaceholder', empty: 'noDocuments', file: 'documentFileLabel', titleLabel: 'documentTitleLabel', descLabel: 'documentDescriptionLabel', filter: 'documentFilter', accept: 'application/pdf,.pdf,.doc,.docx,.txt,.odt' }, + bookmark: { key: 'bookmarks', list: renderBookmarkList, wrapper: 'bookmark-list', formClass: 'bookmark-form', title: 'tribeSectionBookmarks', description: 'bookmarkDescription', create: 'bookmarkCreateButton', search: 'bookmarkSearchPlaceholder', empty: 'noBookmarks', filter: 'bookmarkFilter' } +}; +const toMediaShape = (m) => ({ ...m, key: m.id, url: m.mediaType === 'bookmark' ? (m.url || m.description || '') : blobIdOf(m.image), updatedAt: undefined }); + +const renderTribeMediaTypeSection = (tribe, items, query = {}, mediaType) => { + const cfg = MEDIA_SECTIONS[mediaType]; + const media = asList(items).filter(m => m.mediaType === mediaType); + const member = isTribeMember(tribe); + const filter = String(query.filter || 'all').toLowerCase(); + const q = String(query.q || '').trim(); + const isCreate = query.action === 'create' && member; + const tribeUrl = tribeUrlOf(tribe); + const header = renderTribeSectionHeader(i18n[cfg.title], i18n[cfg.description]); + const emptyMod = !isCreate && moduleIsEmpty(media, filter, 'all', q); + const chips = renderTribeModeChips(tribe, cfg.key, { modes: ['all', 'recent', 'mine', 'top'], filter: isCreate ? 'create' : filter, labelOf: m => i18n[`${cfg.filter}${capFirst(m)}`], visible: tribeChipVisible(media, filter, BASE_MODES), emptyMod, create: member ? { label: i18n[cfg.create] } : null }); + + if (isCreate) { + const common = [ + input({ type: 'hidden', name: 'mediaType', value: mediaType }), + input({ type: 'hidden', name: 'returnSection', value: cfg.key }) + ]; + const formNode = mediaType === 'bookmark' + ? form({ method: 'POST', action: `${tribeUrl}/media/upload` }, + ...common, + label(i18n.bookmarkUrlLabel), br(), + input({ type: 'url', name: 'url', id: 'url', required: true, placeholder: i18n.bookmarkUrlPlaceholder }), br(), br(), + label(i18n.tribeMediaTitle), br(), + input({ type: 'text', name: 'title', id: 'title', maxlength: '100', required: true }), br(), br(), + label(i18n.bookmarkDescriptionLabel), br(), + textarea({ maxlength: String(TEXT_CAP), name: 'description', id: 'description', rows: '4', placeholder: i18n.bookmarkDescriptionPlaceholder }, ''), br(), + tribe.reachAllowed ? renderTribeReachSelector() : null, br(), + button({ type: 'submit' }, i18n[cfg.create]) + ) + : form({ method: 'POST', action: `${tribeUrl}/media/upload`, enctype: 'multipart/form-data' }, + ...common, + label(i18n[cfg.file]), br(), + input({ type: 'file', name: 'media', id: 'media', accept: cfg.accept, required: true }), br(), br(), + label(i18n[cfg.titleLabel]), br(), + input({ type: 'text', name: 'title', id: 'title', maxlength: '100', required: true }), br(), + label(i18n[cfg.descLabel]), br(), + textarea({ maxlength: String(TEXT_CAP), name: 'description', id: 'description', rows: '4' }, ''), br(), + ...renderLicenseSelect('', i18n), br(), + tribe.reachAllowed ? renderTribeReachSelector() : null, br(), + button({ type: 'submit' }, i18n[cfg.create]) + ); + return div({ class: 'tribe-content-list' }, header, chips, div({ class: `div-center ${cfg.formClass}` }, formNode)); + } + + const list = applyTribeFilter(media, filter, q, BASE_MODES).map(toMediaShape); + const returnTo = sectionHref(tribe, cfg.key, { filter, q }); + const params = { + q, + hideLastVisit: true, + headerActions: (m) => tribeItemActions(tribe, m, member, m.title), + titleChips: (m) => [tribeReachChipFor(tribe, m)], + bodyExtra: (m) => [tribeDescription(m.description && m.description !== m.url ? m.description : null), renderTribeReachControls(tribe, m, returnTo)], + engagement: (m) => tribeEngagement(tribe, m, member, returnTo) + }; + return div({ class: 'tribe-content-list' }, + header, + chips, + emptyMod ? null : renderTribeSearchRow(tribe, cfg.key, filter, q, renderModuleStats(list.length), i18n[cfg.search]), + list.length ? div({ class: cfg.wrapper }, cfg.list(paged(list), filter, params)) : renderTribeEmpty(i18n[cfg.empty] || i18n.tribeMediaEmpty) + ); +}; + +const renderStandaloneSection = (tribe, sectionKey, items, query, cfg) => { + const list = asList(items); + const member = isTribeMember(tribe); + const filter = String(query.filter || 'all').toLowerCase(); + const q = String(query.q || '').trim(); + const modes = cfg.modes || BASE_MODES; + const modeList = cfg.modeList || ['all', 'recent', 'mine']; + const header = renderTribeSectionHeader(cfg.title, cfg.description); + const emptyMod = moduleIsEmpty(list, filter, 'all', q); + const chips = renderTribeModeChips(tribe, sectionKey, { + modes: modeList, filter, labelOf: cfg.labelOf, visible: tribeChipVisible(list, filter, modes), emptyMod, + create: member && cfg.createAction ? { action: cfg.createAction, hidden: { filter: 'create', tribeId: tribe.id }, label: cfg.createLabel } : null + }); + const shown = applyTribeFilter(list, filter, q, modes, cfg.score || (() => 0)); + return div({ class: 'tribe-content-list' }, + header, + chips, + emptyMod ? null : renderTribeSearchRow(tribe, sectionKey, filter, q, cfg.stats ? cfg.stats(shown) : renderModuleStats(shown.length), cfg.placeholder), + shown.length ? cfg.render(paged(shown), member) : renderTribeEmpty(cfg.empty) + ); +}; + +const withModuleReach = (tribe, item, contentType, card, returnTo) => { + if (item.tribeOrigin) return card; + const it = { ...item, id: item.rootId || item.id || item.key, contentType }; + const chip = tribeReachChipFor(tribe, it); + return div(card, chip ? div({ class: 'card-chips-row' }, chip) : null, renderTribeReachControls(tribe, it, returnTo)); +}; + +const renderTribeMapsSection = (tribe, maps, query = {}) => + renderStandaloneSection(tribe, 'maps', maps, query, { + title: i18n.tribeSectionMaps || 'MAPS', description: i18n.mapDescription, createAction: '/maps', createLabel: i18n.mapCreateButton || 'Create Map', + labelOf: m => i18n[`mapFilter${capFirst(m)}`], placeholder: i18n.mapSearchPlaceholder, empty: i18n.noMaps, + render: (list) => { + const returnTo = sectionHref(tribe, 'maps', { filter: query.filter, q: query.q }); + return div({ class: 'jobs-grid' }, ...list.map(m => withModuleReach(tribe, m, 'map', renderMapCard(m, 'all', { q: query.q || '', returnTo }), returnTo))); + } + }); + +const renderTribeTorrentsSection = (tribe, torrents, query = {}) => + renderStandaloneSection(tribe, 'torrents', torrents, query, { + title: i18n.tribeSectionTorrents || 'TORRENTS', description: i18n.torrentsDescription, createAction: '/torrents', createLabel: i18n.tribeCreateTorrent || 'Upload Torrent', + labelOf: m => i18n[`torrentFilter${capFirst(m)}`], placeholder: i18n.torrentSearchPlaceholder, empty: i18n.tribeTorrentsEmpty || i18n.noTorrents, + render: (list, member) => { + const returnTo = sectionHref(tribe, 'torrents', { filter: query.filter, q: query.q }); + return div({ class: 'audios-list' }, renderTorrentTable(list, 'all', { + q: query.q || '', + rowFor: (t) => t._isMedia + ? { + titleChips: [tribeReachChipFor(tribe, t)], + details: renderTribeReachControls(tribe, { ...t, id: t.rootId || t.key, contentType: 'media' }, returnTo), + download: t.url ? renderTorrentDownload(torrentDownloadHref(t.url, t.title)) : null, + actions: tribeItemActions(tribe, { ...t, id: t.rootId || t.key }, member, t.title) + } + : null + })); + } + }); + +const renderTribeFilesSection = (tribe, files, query = {}) => + renderStandaloneSection(tribe, 'files', files, query, { + title: i18n.tribeSectionFiles || 'FILES', description: i18n.filesDescription, createAction: '/files', createLabel: i18n.tribeCreateFile || 'Upload File', + labelOf: m => i18n[`fileFilter${capFirst(m)}`], placeholder: i18n.fileSearchPlaceholder, empty: i18n.tribeFilesEmpty || i18n.noFiles, + render: (list) => div({ class: 'audios-list' }, renderFileTable(list, 'all', { q: query.q || '' })) + }); + +const closedByFlag = (it) => !!it.isClosed || String(it.status || '').toUpperCase() === 'CLOSED'; + +const renderTribePadsSection = (tribe, pads, query = {}) => + renderStandaloneSection(tribe, 'pads', pads, query, { + title: i18n.tribeSectionPads || 'PADS', description: i18n.padsDescription, createAction: '/pads', createLabel: i18n.tribePadCreate || 'Create Pad', + modes: openClosedModes(closedByFlag), modeList: ['all', 'recent', 'mine', 'open', 'closed'], + labelOf: m => i18n[`padFilter${capFirst(m)}`], placeholder: i18n.padSearchPlaceholder, empty: i18n.tribePadsEmpty || i18n.padsNoItems, + stats: (list) => renderModuleStatsBy(list, pd => pd.isClosed ? 'CLOSED' : String(pd.status || 'OPEN').toUpperCase(), [{ value: 'OPEN', label: i18n.padStatusOpen }, { value: 'INVITE-ONLY', label: i18n.padStatusInviteOnly }, { value: 'CLOSED', label: i18n.padStatusClosed }]), + render: (list) => div({ class: 'tribe-grid pads-grid' }, ...list.map(pd => renderPadCard(pd, 'all', null))) + }); + +const renderTribeChatsSection = (tribe, chats, query = {}) => + renderStandaloneSection(tribe, 'chats', chats, query, { + title: i18n.tribeSectionChats || 'CHATS', description: i18n.modulesChatsDescription, createAction: '/chats', createLabel: i18n.tribeChatCreate || 'Create Chat', + modes: openClosedModes(closedByFlag), modeList: ['all', 'recent', 'mine', 'open', 'closed'], + labelOf: m => i18n[`chatFilter${capFirst(m)}`], placeholder: i18n.chatSearchPlaceholder, empty: i18n.tribeChatsEmpty || i18n.chatNoItems, + stats: (list) => renderModuleStatsBy(list, c => String(c.status || 'OPEN').toUpperCase(), [{ value: 'OPEN', label: i18n.chatStatusOpen }, { value: 'INVITE-ONLY', label: i18n.chatStatusInviteOnly }, { value: 'CLOSED', label: i18n.chatStatusClosed }]), + render: (list) => div({ class: 'tribe-grid' }, ...list.map(c => renderChatCard(c, 'all', { q: query.q || '' }))) + }); + +const renderTribeCalendarsSection = (tribe, calendars, query = {}) => + renderStandaloneSection(tribe, 'calendars', calendars, query, { + title: i18n.tribeSectionCalendars || 'CALENDARS', description: i18n.calendarsDescription, createAction: '/calendars', createLabel: i18n.tribeCalendarCreate || 'Create Calendar', + modes: openClosedModes(closedByFlag), modeList: ['all', 'recent', 'mine', 'open', 'closed'], + labelOf: m => i18n[`calendarFilter${capFirst(m)}`], placeholder: i18n.calendarSearchPlaceholder, empty: i18n.tribeCalendarsEmpty || i18n.calendarsNoItems, + stats: (list) => openClosedStats(list, closedByFlag, i18n.calendarStatusOpen, i18n.calendarStatusClosed), + render: (list) => { + const returnTo = sectionHref(tribe, 'calendars', { filter: query.filter, q: query.q }); + return div({ class: 'tribe-grid' }, ...list.map(c => withModuleReach(tribe, c, 'calendar', renderCalendarCard(c, null), returnTo))); + } + }); + +const renderTribeWikiSectionPage = (tribe, pages, query = {}) => { + const list = asList(pages); + const member = isTribeMember(tribe); + const filter = String(query.filter || 'all').toLowerCase(); + const q = String(query.q || '').trim(); + const emptyMod = moduleIsEmpty(list, filter, 'all', q); + const shown = applyTribeFilter(list, filter, q, BASE_MODES, () => 0); + const toolbar = div({ class: 'tribe-content-list' }, + renderTribeSectionHeader(i18n.tribeSectionWiki || 'WIKI', i18n.wikiDescription), + div({ class: 'mode-buttons-row' }, + ...(emptyMod ? [] : [ + ...['all', 'recent', 'mine'].filter(tribeChipVisible(list, filter, BASE_MODES)).map(m => + form({ method: 'GET', action: tribeUrlOf(tribe) }, + input({ type: 'hidden', name: 'section', value: 'wiki' }), + input({ type: 'hidden', name: 'filter', value: m }), + button({ type: 'submit', class: filter === m ? 'filter-btn active' : 'filter-btn' }, String(i18n[`wikiFilter${capFirst(m)}`] || m).toUpperCase()) + )), + form({ method: 'GET', action: '/wiki' }, + input({ type: 'hidden', name: 'filter', value: 'changes' }), + input({ type: 'hidden', name: 'tribeId', value: tribe.id }), + button({ type: 'submit', class: 'filter-btn' }, String(i18n.wikiChanges).toUpperCase()) + ) + ]), + member + ? form({ method: 'GET', action: '/wiki' }, + input({ type: 'hidden', name: 'filter', value: 'create' }), + input({ type: 'hidden', name: 'tribeId', value: tribe.id }), + button({ type: 'submit', class: 'create-button' }, i18n.tribeWikiCreate) + ) + : null + ), + emptyMod ? null : renderTribeSearchRow(tribe, 'wiki', filter, q, renderModuleStats(shown.length), i18n.wikiSearchPlaceholder) + ); + const returnTo = sectionHref(tribe, 'wiki', { filter: query.filter, q: query.q }); + return renderTribeWikiSection(tribe, shown, toolbar, (pg, card) => withModuleReach(tribe, pg, 'wiki', card, returnTo)); +}; + +const ROOM_MODES = { ...openClosedModes(r => !!r.isClosed), live: () => false }; +const renderTribeRoomsSectionPage = (tribe, sectionData, query = {}) => { + const rooms = asList(sectionData && sectionData.rooms); + const occupancy = sectionData && sectionData.occupancy; + const live = sectionData && sectionData.live; + const member = isTribeMember(tribe); + const filter = String(query.filter || 'all').toLowerCase(); + const q = String(query.q || '').trim(); + const modes = { ...ROOM_MODES, live: r => !!(occupancy && occupancy.get && occupancy.get(r.rootId) && occupancy.get(r.rootId).count > 0) }; + const emptyMod = moduleIsEmpty(rooms, filter, 'all', q); + const shown = applyTribeFilter(rooms, filter, q, modes, () => 0); + const toolbar = div({ class: 'tribe-content-list' }, + renderTribeSectionHeader(i18n.tribeSectionRooms || 'ROOMS', i18n.roomsDescription), + renderTribeModeChips(tribe, 'rooms', { + modes: ['all', 'recent', 'mine', 'live', 'open', 'closed'], filter, labelOf: m => i18n[`roomFilter${capFirst(m)}`], visible: tribeChipVisible(rooms, filter, modes), emptyMod, + create: member ? { action: '/rooms', hidden: { filter: 'create', tribeId: tribe.id }, label: i18n.tribeRoomCreate } : null + }), + emptyMod ? null : renderTribeSearchRow(tribe, 'rooms', filter, q, roomStatsBy(shown), i18n.roomSearchPlaceholder) + ); + return renderTribeRoomsSection(tribe, shown, occupancy, live, toolbar); +}; + +const renderInhabitantsSection = (tribe, members, query = {}) => { + const resolved = asList(members); + const search = String(query.search || '').trim().toLowerCase(); + const shown = search + ? resolved.filter(u => [u.name, u.id, u.description].some(v => String(v || '').toLowerCase().includes(search))) + : resolved; + return div({ class: 'tribe-content-list' }, + renderTribeSectionHeader(i18n.tribeSectionInhabitants, i18n.discoverPeople), + resolved.length ? renderTribeSearchRow(tribe, 'inhabitants', null, search, renderModuleStats(shown.length), i18n.searchInhabitantsPlaceholder, 'search') : null, + shown.length + ? div({ class: 'inhabitants-list inhabitants-grid' }, ...paged(shown).map(u => renderInhabitantCard(u, 'all', userId, false))) + : renderTribeEmpty(i18n.tribeInhabitantsEmpty || i18n.noInhabitantsFound) + ); +}; + const createButtonI18n = { - events: () => i18n.tribeEventCreate || 'Create Event', - tasks: () => i18n.tribeTaskCreate || 'Create Task', - votations: () => i18n.tribeVotationCreate || 'Create Votation', - forum: () => i18n.tribeForumCreate || 'Create Forum', subtribes: () => i18n.tribeSubTribesCreate || 'Create Sub-Tribe', }; @@ -749,10 +1565,11 @@ const renderCreateForm = (tribe, contentType, fields) => { form(formAttrs, ...fields.map(f => { const prefix = f.spaceBefore ? [br()] : []; - if (f.type === 'textarea') return [...prefix, label({ for: f.name }, f.label), br, textarea({ name: f.name, id: f.name, rows: f.rows || 4, required: f.required, placeholder: f.placeholder }, ''), br()]; + if (f.type === 'textarea') return [...prefix, label({ for: f.name }, f.label), br, textarea({ maxlength: String(TEXT_CAP), name: f.name, id: f.name, rows: f.rows || 4, required: f.required, placeholder: f.placeholder }, ''), br()]; if (f.type === 'select') return [...prefix, label({ for: f.name }, f.label), br, select({ name: f.name, id: f.name }, ...f.options.map(o => option({ value: o.value }, o.label))), br()]; if (f.type === 'file') return [...prefix, label({ for: f.name }, f.label), br, input({ type: 'file', name: f.name, id: f.name, accept: f.accept || '*/*' }), br()]; const attrs = { type: f.type || 'text', name: f.name, id: f.name, required: f.required, placeholder: f.placeholder }; + if (f.maxlength) attrs.maxlength = f.maxlength; if (f.min) attrs.min = f.min; return [...prefix, br, label({ for: f.name }, f.label), br, input(attrs), br()]; }).flat(),br(), @@ -761,514 +1578,6 @@ const renderCreateForm = (tribe, contentType, fields) => { ); }; -const renderEventsSection = (tribe, items, query) => { - const events = Array.isArray(items) ? items : []; - const action = query.action; - const today = new Date().toISOString().split('T')[0]; - if (action === 'create') { - return renderCreateForm(tribe, 'events', [ - { name: 'title', label: i18n.tribeEventTitle, required: true, placeholder: i18n.tribeEventTitle }, - { name: 'description', type: 'textarea', label: i18n.tribeEventDescription, required: true, placeholder: i18n.tribeEventDescription }, - { name: 'date', type: 'date', label: i18n.tribeEventDate, required: true, min: today }, - { name: 'location', label: i18n.tribeEventLocation, placeholder: i18n.tribeEventLocation }, - ]); - } - const tribeUrl = `/tribe/${encodeURIComponent(tribe.id)}`; - return div({ class: 'tribe-content-list' }, - div({ class: 'tribe-content-header' }, - h2(i18n.tribeSectionEvents), - form({ method: 'GET', action: tribeUrl }, - input({ type: 'hidden', name: 'section', value: 'events' }), - input({ type: 'hidden', name: 'action', value: 'create' }), - button({ type: 'submit', class: 'create-button' }, i18n.tribeEventCreate) - ) - ), - events.length === 0 ? p(i18n.tribeEventsEmpty) : - events.map(e => div({ class: 'tribe-content-card' }, - h2(e.title), - e.description ? p(...renderUrl(e.description)) : null, - e.date ? div({ class: 'card-field' }, - span({ class: 'card-label' }, (i18n.tribeEventDate || 'Date') + ':'), - span({ class: 'card-value' }, e.date) - ) : null, - e.location ? div({ class: 'card-field' }, - span({ class: 'card-label' }, (i18n.tribeEventLocation || 'Location') + ':'), - span({ class: 'card-value' }, ...renderUrl(e.location)) - ) : null, - div({ class: 'card-field' }, - span({ class: 'card-label' }, (i18n.tribeEventAttendees || 'Attendees') + ':'), - span({ class: 'card-value' }, String((e.attendees || []).length)) - ), - statusBadge(e.status), - div({ class: 'tribe-content-actions' }, - form({ method: 'POST', action: `${tribeUrl}/events/attend/${encodeURIComponent(e.id)}` }, - button({ type: 'submit', class: 'filter-btn' }, - (e.attendees || []).includes(userId) ? i18n.tribeEventUnattend : i18n.tribeEventAttend - ) - ), - e.author === userId ? form({ method: 'POST', action: `${tribeUrl}/content/delete/${encodeURIComponent(e.id)}` }, - button({ type: 'submit', class: 'filter-btn danger-btn' }, i18n.tribeContentDelete) - ) : null - ) - )) - ); -}; - -const renderTasksSection = (tribe, items, query) => { - const tasks = Array.isArray(items) ? items : []; - const action = query.action; - const today = new Date().toISOString().split('T')[0]; - if (action === 'create') { - return renderCreateForm(tribe, 'tasks', [ - { name: 'title', label: i18n.tribeTaskTitle, required: true, placeholder: i18n.tribeTaskTitle }, - { name: 'description', type: 'textarea', label: i18n.tribeTaskDescription, required: true, placeholder: i18n.tribeTaskDescription }, - { name: 'priority', type: 'select', label: i18n.tribeTaskPriority, options: [ - { value: 'LOW', label: i18n.tribePriorityLow }, { value: 'MEDIUM', label: i18n.tribePriorityMedium }, - { value: 'HIGH', label: i18n.tribePriorityHigh }, { value: 'CRITICAL', label: i18n.tribePriorityCritical } - ]}, - { name: 'deadline', type: 'date', label: i18n.tribeTaskDeadline, min: today }, - ]); - } - const tribeUrl = `/tribe/${encodeURIComponent(tribe.id)}`; - return div({ class: 'tribe-content-list' }, - div({ class: 'tribe-content-header' }, - h2(i18n.tribeSectionTasks), - form({ method: 'GET', action: tribeUrl }, - input({ type: 'hidden', name: 'section', value: 'tasks' }), - input({ type: 'hidden', name: 'action', value: 'create' }), - button({ type: 'submit', class: 'create-button' }, i18n.tribeTaskCreate) - ) - ), - tasks.length === 0 ? p(i18n.tribeTasksEmpty) : - tasks.map(t => div({ class: 'tribe-content-card' }, - h2(t.title), - t.description ? p(...renderUrl(t.description)) : null, - div({ class: 'card-field' }, - span({ class: 'card-label' }, (i18n.tribeTaskPriority || 'Priority') + ':'), - priorityLabel(t.priority) - ), - div({ class: 'card-field' }, - span({ class: 'card-label' }, (i18n.tribeStatusLabel || 'Status') + ':'), - statusBadge(t.status) - ), - div({ class: 'card-field' }, - span({ class: 'card-label' }, (i18n.tribeTaskAssignees || 'Assignees') + ':'), - span({ class: 'card-value' }, String((t.assignees || []).length)) - ), - br(), - t.deadline ? div({ class: 'card-field' }, - span({ class: 'card-label' }, (i18n.tribeTaskDeadline || 'Deadline') + ':'), - span({ class: 'card-value' }, t.deadline) - ) : null, - div({ class: 'tribe-content-actions' }, - form({ method: 'POST', action: `${tribeUrl}/tasks/assign/${encodeURIComponent(t.id)}` }, - button({ type: 'submit', class: 'filter-btn' }, - (t.assignees || []).includes(userId) ? i18n.tribeTaskUnassign : i18n.tribeTaskAssign - ) - ), - t.status !== 'IN-PROGRESS' && t.author === userId ? form({ method: 'POST', action: `${tribeUrl}/tasks/status/${encodeURIComponent(t.id)}` }, - input({ type: 'hidden', name: 'status', value: 'IN-PROGRESS' }), - button({ type: 'submit', class: 'filter-btn' }, i18n.tribeTaskStatusInProgress) - ) : null, - t.status !== 'CLOSED' && t.author === userId ? form({ method: 'POST', action: `${tribeUrl}/tasks/status/${encodeURIComponent(t.id)}` }, - input({ type: 'hidden', name: 'status', value: 'CLOSED' }), - button({ type: 'submit', class: 'filter-btn' }, i18n.tribeTaskStatusClosed) - ) : null, - t.author === userId ? form({ method: 'POST', action: `${tribeUrl}/content/delete/${encodeURIComponent(t.id)}` }, - button({ type: 'submit', class: 'filter-btn danger-btn' }, i18n.tribeContentDelete) - ) : null - ) - )) - ); -}; - -const renderVotationsSection = (tribe, items, query) => { - const votations = Array.isArray(items) ? items : []; - const action = query.action; - const today = new Date().toISOString().split('T')[0]; - if (action === 'create') { - return div({ class: 'create-tribe-form' }, - form({ method: 'POST', action: `/tribe/${encodeURIComponent(tribe.id)}/votations/create` }, - label({ for: 'title' }, i18n.tribeVotationTitle), br, - input({ type: 'text', name: 'title', id: 'title', required: true, placeholder: i18n.tribeVotationTitle }), br(), - label({ for: 'description' }, i18n.tribeVotationDescription), br, - textarea({ name: 'description', id: 'description', rows: 3, placeholder: i18n.tribeVotationDescription }, ''), br(), - label({ for: 'deadline' }, i18n.tribeVotationDeadline), br, - input({ type: 'date', name: 'deadline', id: 'deadline', min: today }), br(), - br(), - label(i18n.tribeVotationOptions), br, - input({ type: 'text', name: 'option1', placeholder: `${i18n.tribeVotationOptionPlaceholder} 1`, required: true }), br(), - input({ type: 'text', name: 'option2', placeholder: `${i18n.tribeVotationOptionPlaceholder} 2`, required: true }), br(), - input({ type: 'text', name: 'option3', placeholder: `${i18n.tribeVotationOptionPlaceholder} 3` }), br(), - input({ type: 'text', name: 'option4', placeholder: `${i18n.tribeVotationOptionPlaceholder} 4` }), br(), - button({ type: 'submit', class: 'create-button' }, i18n.tribeVotationCreate) - ) - ); - } - const tribeUrl = `/tribe/${encodeURIComponent(tribe.id)}`; - return div({ class: 'tribe-content-list' }, - div({ class: 'tribe-content-header' }, - h2(i18n.tribeSectionVotations), - form({ method: 'GET', action: tribeUrl }, - input({ type: 'hidden', name: 'section', value: 'votations' }), - input({ type: 'hidden', name: 'action', value: 'create' }), - button({ type: 'submit', class: 'create-button' }, i18n.tribeVotationCreate) - ) - ), - votations.length === 0 ? p(i18n.tribeVotationsEmpty) : - votations.map(v => { - const opts = Array.isArray(v.options) ? v.options : []; - const votes = v.votes || {}; - const totalVotes = Object.values(votes).reduce((sum, arr) => sum + (Array.isArray(arr) ? arr.length : 0), 0); - const hasVoted = Object.values(votes).some(arr => Array.isArray(arr) && arr.includes(userId)); - const isOpen = v.status === 'OPEN'; - - return div({ class: 'tribe-content-card' }, - h2(v.title), - v.description ? p(...renderUrl(v.description)) : null, - statusBadge(v.status), - v.deadline ? div({ class: 'card-field' }, - span({ class: 'card-label' }, (i18n.tribeVotationDeadline || 'Deadline') + ':'), - span({ class: 'card-value' }, v.deadline) - ) : null, - div({ class: 'card-field' }, - span({ class: 'card-label' }, (i18n.tribeVotationResults || 'Votes') + ':'), - span({ class: 'card-value' }, String(totalVotes)) - ), - br(), - div({ class: 'tribe-votation-options' }, - opts.map((opt, idx) => { - const count = Array.isArray(votes[String(idx)]) ? votes[String(idx)].length : 0; - const pct = totalVotes > 0 ? Math.round((count / totalVotes) * 100) : 0; - const roundedPct = Math.round(pct / 5) * 5; - return div({ class: 'tribe-votation-option' }, - span({ class: 'tribe-votation-label' }, opt), - div({ class: 'tribe-votation-bar' }, - div({ class: `tribe-votation-fill tribe-fill-${roundedPct}` }) - ), - span({ class: 'tribe-votation-count' }, `${count} (${pct}%)`), - isOpen && !hasVoted ? form({ method: 'POST', action: `${tribeUrl}/votations/${encodeURIComponent(v.id)}/vote` }, - input({ type: 'hidden', name: 'optionIndex', value: String(idx) }), - button({ type: 'submit', class: 'filter-btn' }, i18n.tribeVotationVote) - ) : null - ); - }) - ), - v.author === userId ? div({ class: 'tribe-content-actions' }, - isOpen ? form({ method: 'POST', action: `${tribeUrl}/votations/close/${encodeURIComponent(v.id)}` }, - button({ type: 'submit', class: 'tribe-action-btn' }, i18n.tribeVotationClose) - ) : null, - form({ method: 'POST', action: `${tribeUrl}/content/delete/${encodeURIComponent(v.id)}` }, - button({ type: 'submit', class: 'tribe-action-btn danger-btn' }, i18n.tribeContentDelete) - ) - ) : null - ); - }) - ); -}; - -const renderForumSection = (tribe, items, query) => { - const allItems = Array.isArray(items) ? items : []; - const threads = allItems.filter(i => i.contentType === 'forum'); - const action = query.action; - const threadId = query.thread; - const tribeUrl = `/tribe/${encodeURIComponent(tribe.id)}`; - - if (action === 'create') { - return renderCreateForm(tribe, 'forum', [ - { name: 'title', label: i18n.tribeForumTitle, required: true, placeholder: i18n.tribeForumTitle }, - { name: 'description', type: 'textarea', label: i18n.tribeForumText, required: true, placeholder: i18n.tribeForumText, rows: 6 }, - { name: 'category', type: 'select', label: i18n.tribeForumCategory, options: [ - { value: 'GENERAL', label: i18n.tribeForumCatGeneral }, { value: 'PROPOSAL', label: i18n.tribeForumCatProposal }, - { value: 'QUESTION', label: i18n.tribeForumCatQuestion }, { value: 'ANNOUNCEMENT', label: i18n.tribeForumCatAnnouncement } - ]} - ]); - } - - if (threadId) { - const thread = allItems.find(i => i.id === threadId); - const replies = allItems.filter(i => i.contentType === 'forum-reply' && i.parentId === threadId) - .sort((a, b) => (a.refeeds || 0) - (b.refeeds || 0) !== 0 ? (b.refeeds || 0) - (a.refeeds || 0) : (Date.parse(a.createdAt) || 0) - (Date.parse(b.createdAt) || 0)); - if (!thread) return p(i18n.tribeForumEmpty); - const replyCount = replies.length; - return div({ class: 'tribe-content-list' }, - form({ method: 'GET', action: tribeUrl }, - input({ type: 'hidden', name: 'section', value: 'forum' }), - button({ type: 'submit', class: 'filter-btn' }, i18n.walletBack) - ), - div({ class: 'forum-card forum-thread-header' }, - div({ class: 'forum-score-col' }, - div({ class: 'forum-score-box' }, - form({ method: 'POST', action: `${tribeUrl}/forum/${encodeURIComponent(thread.id)}/refeed` }, - button({ type: 'submit', class: 'score-btn' }, '▲') - ), - div({ class: 'score-total' }, String(thread.refeeds || 0)), - ) - ), - div({ class: 'forum-main-col' }, - div({ class: 'forum-header-row' }, - thread.category ? span({ class: 'forum-category' }, `[${forumCatI18n()[thread.category] || thread.category}]`) : null, - span({ class: 'forum-title' }, thread.title) - ), - div({ class: 'forum-footer' }, - span({ class: 'date-link' }, `${new Date(thread.createdAt).toLocaleString()} ${i18n.performed || ''}`), - userLink(thread.author) - ), - div({ class: 'forum-body' }, ...renderUrl(thread.description || '')), - div({ class: 'forum-meta' }, - span({ class: 'forum-positive-votes' }, `▲: ${thread.refeeds || 0}`), - span({ class: 'forum-messages' }, `${(i18n.forumMessages || i18n.tribeForumReplies || 'MESSAGES').toUpperCase()}: ${replyCount}`) - ) - ) - ), - div({ class: 'tribe-forum-reply-form' }, - form({ method: 'POST', action: `${tribeUrl}/forum/${encodeURIComponent(threadId)}/reply` }, - textarea({ name: 'description', rows: 3, required: true, placeholder: i18n.tribeForumReply }), - br(), - button({ type: 'submit', class: 'forum-send-btn' }, i18n.tribeForumReply) - ) - ), - replies.length > 0 - ? replies.map((r, idx) => - div({ class: `forum-comment${idx === 0 ? ' highlighted-reply' : ''}` }, - div({ class: 'comment-header' }, - span({ class: 'date-link' }, `${new Date(r.createdAt).toLocaleString()} ${i18n.performed || ''}`), - userLink(r.author), - div({ class: 'comment-votes' }, - span({ class: 'forum-positive-votes' }, `▲: ${r.refeeds || 0}`) - ) - ), - div({ class: 'comment-body-row' }, - div({ class: 'comment-vote-col' }, - div({ class: 'forum-score-box' }, - form({ method: 'POST', action: `${tribeUrl}/forum/${encodeURIComponent(r.id)}/refeed` }, - button({ type: 'submit', class: 'score-btn' }, '▲') - ), - div({ class: 'score-total' }, String(r.refeeds || 0)) - ) - ), - div({ class: 'comment-text-col' }, - ...(r.description || '').split('\n').map(l => l.trim()).filter(l => l).map(l => p(...renderUrl(l))) - ) - ), - r.author === userId ? div({ class: 'tribe-content-actions' }, - form({ method: 'POST', action: `${tribeUrl}/content/delete/${encodeURIComponent(r.id)}` }, - button({ type: 'submit', class: 'tribe-action-btn danger-btn' }, i18n.tribeContentDelete) - ) - ) : null - ) - ) - : null - ); - } - - const replyMap = new Map(); - allItems.filter(i => i.contentType === 'forum-reply').forEach(r => { replyMap.set(r.parentId, (replyMap.get(r.parentId) || 0) + 1); }); - const sortedThreads = [...threads].sort((a, b) => ((b.refeeds || 0) + (replyMap.get(b.id) || 0)) - ((a.refeeds || 0) + (replyMap.get(a.id) || 0))); - - return div({ class: 'tribe-content-list' }, - div({ class: 'tribe-content-header' }, - h2(i18n.tribeSectionForum), - form({ method: 'GET', action: tribeUrl }, - input({ type: 'hidden', name: 'section', value: 'forum' }), - input({ type: 'hidden', name: 'action', value: 'create' }), - button({ type: 'submit', class: 'create-button' }, i18n.tribeForumCreate) - ) - ), - sortedThreads.length === 0 ? p(i18n.tribeForumEmpty) : - div({ class: 'forum-list' }, - sortedThreads.map(t => { - const replyCount = allItems.filter(i => i.contentType === 'forum-reply' && i.parentId === t.id).length; - return div({ class: 'forum-card' }, - div({ class: 'forum-score-col' }, - div({ class: 'forum-score-box' }, - form({ method: 'POST', action: `${tribeUrl}/forum/${encodeURIComponent(t.id)}/refeed` }, - button({ type: 'submit', class: 'score-btn' }, '▲') - ), - div({ class: 'score-total' }, String(t.refeeds || 0)) - ) - ), - div({ class: 'forum-main-col' }, - div({ class: 'forum-header-row' }, - t.category ? span({ class: 'forum-category' }, `[${forumCatI18n()[t.category] || t.category}]`) : null, - form({ method: 'GET', action: tribeUrl, class: 'forum-title-form' }, - input({ type: 'hidden', name: 'section', value: 'forum' }), - input({ type: 'hidden', name: 'thread', value: t.id }), - button({ type: 'submit', class: 'forum-title' }, t.title) - ) - ), - t.description ? div({ class: 'forum-body' }, ...renderUrl((t.description || '').substring(0, 200))) : null, - div({ class: 'forum-meta' }, - span({ class: 'forum-positive-votes' }, `▲: ${t.refeeds || 0}`), - span({ class: 'forum-messages' }, `${(i18n.forumMessages || i18n.tribeForumReplies || 'MESSAGES').toUpperCase()}: ${replyCount}`), - form({ method: 'GET', action: tribeUrl, class: 'visit-forum-form' }, - input({ type: 'hidden', name: 'section', value: 'forum' }), - input({ type: 'hidden', name: 'thread', value: t.id }), - button({ type: 'submit', class: 'filter-btn' }, i18n.forumVisitButton || 'VISIT') - ) - ), - div({ class: 'forum-footer' }, - span({ class: 'date-link' }, `${new Date(t.createdAt).toLocaleString()} ${i18n.performed || ''}`), - userLink(t.author) - ), - t.author === userId ? div({ class: 'forum-owner-actions' }, - form({ method: 'POST', action: `${tribeUrl}/content/delete/${encodeURIComponent(t.id)}`, class: 'forum-delete-form' }, - button({ type: 'submit', class: 'tribe-action-btn danger-btn' }, i18n.tribeContentDelete) - ) - ) : null - ) - ); - }) - ) - ); -}; - -const sectionKeyForMediaType = { image: 'images', audio: 'audios', video: 'videos', document: 'documents', bookmark: 'bookmarks', torrent: 'torrents' }; -const acceptForMediaType = { image: 'image/*', audio: 'audio/*', video: 'video/*', document: 'application/pdf,.pdf,.doc,.docx,.txt,.odt', bookmark: null, torrent: '.torrent' }; -const sectionTitleForMediaType = (mt) => { - const map = { image: i18n.tribeSectionImages, audio: i18n.tribeSectionAudios, video: i18n.tribeSectionVideos, document: i18n.tribeSectionDocuments, bookmark: i18n.tribeSectionBookmarks, torrent: i18n.tribeSectionTorrents }; - return map[mt] || mt; -}; - -const renderTribeMediaTypeSection = (tribe, items, query, mediaType) => { - const allMedia = Array.isArray(items) ? items : []; - const media = allMedia.filter(m => m.mediaType === mediaType); - const action = query.action; - const tribeUrl = `/tribe/${encodeURIComponent(tribe.id)}`; - const sectionKey = sectionKeyForMediaType[mediaType] || 'media'; - const sTitle = sectionTitleForMediaType(mediaType); - const createMediaLabel = { - image: () => i18n.tribeCreateImage || 'Create Image', - audio: () => i18n.tribeCreateAudio || 'Create Audio', - video: () => i18n.tribeCreateVideo || 'Create Video', - document: () => i18n.tribeCreateDocument || 'Create Document', - bookmark: () => i18n.tribeCreateBookmark || 'Create Bookmark', - torrent: () => i18n.tribeCreateTorrent || 'Upload Torrent', - }; - const mediaBtnLabel = createMediaLabel[mediaType] ? createMediaLabel[mediaType]() : i18n.tribeCreateButton; - - if (action === 'create') { - if (mediaType === 'bookmark') { - return div({ class: 'create-tribe-form' }, - form({ method: 'POST', action: `${tribeUrl}/media/upload` }, - input({ type: 'hidden', name: 'mediaType', value: 'bookmark' }), - input({ type: 'hidden', name: 'returnSection', value: sectionKey }), - label({ for: 'title' }, i18n.tribeMediaTitle), br, - input({ type: 'text', name: 'title', id: 'title', required: true, placeholder: i18n.tribeMediaTitle }), br(), - label({ for: 'url' }, i18n.bookmarkUrlLabel || 'URL'), br, - input({ type: 'url', name: 'url', id: 'url', required: true, placeholder: 'https://' }), br(),br(), - label({ for: 'description' }, i18n.tribeMediaDescription), br, - textarea({ name: 'description', id: 'description', rows: 3, placeholder: i18n.tribeMediaDescription }, ''), br(), - button({ type: 'submit', class: 'create-button' }, mediaBtnLabel) - ) - ); - } - return div({ class: 'create-tribe-form' }, - form({ method: 'POST', action: `${tribeUrl}/media/upload`, enctype: 'multipart/form-data' }, - input({ type: 'hidden', name: 'mediaType', value: mediaType }), - input({ type: 'hidden', name: 'returnSection', value: sectionKey }), - label({ for: 'title' }, i18n.tribeMediaTitle), br, - input({ type: 'text', name: 'title', id: 'title', required: true, placeholder: i18n.tribeMediaTitle }), br(), - label({ for: 'description' }, i18n.tribeMediaDescription), br, - textarea({ name: 'description', id: 'description', rows: 3, placeholder: i18n.tribeMediaDescription }, ''), br(), - label({ for: 'media' }, i18n.tribeMediaUpload), br, - input({ type: 'file', name: 'media', id: 'media', accept: acceptForMediaType[mediaType] || '*/*', required: true }), br(), br(), - button({ type: 'submit', class: 'create-button' }, mediaBtnLabel) - ) - ); - } - - const mediaFooter = (m) => [ - p({ class: 'tribe-media-date' }, span({ class: 'date-link' }, new Date(m.createdAt).toLocaleString())), - p({ class: 'tribe-media-author' }, userLink(m.author)), - m.author === userId ? form({ method: 'POST', action: `${tribeUrl}/content/delete/${encodeURIComponent(m.id)}` }, - button({ type: 'submit', class: 'tribe-action-btn danger-btn' }, i18n.tribeContentDelete) - ) : null - ]; - - const renderMediaItem = (m) => { - const blobUrl = toBlobUrl(m.image); - if (mediaType === 'image') { - return div({ class: 'tribe-media-item' }, - blobUrl ? a({ href: blobUrl, target: '_blank' }, img({ src: blobUrl, alt: m.title || '', class: 'tribe-media-thumb' })) : null, - div({ class: 'tribe-media-item-info' }, - m.title ? h2(m.title) : null, - m.description ? p(...renderUrl(m.description)) : null, - ...mediaFooter(m) - ) - ); - } - if (mediaType === 'audio') { - return div({ class: 'tribe-media-item' }, - blobUrl ? audio({ src: blobUrl, controls: true, class: 'tribe-media-audio' }) : p(i18n.tribeMediaEmpty), - div({ class: 'tribe-media-item-info' }, - m.title ? h2(m.title) : null, - m.description ? p(...renderUrl(m.description)) : null, - ...mediaFooter(m) - ) - ); - } - if (mediaType === 'video') { - return div({ class: 'tribe-media-item' }, - blobUrl ? video({ src: blobUrl, controls: true, class: 'tribe-media-thumb' }) : p(i18n.tribeMediaEmpty), - div({ class: 'tribe-media-item-info' }, - m.title ? h2(m.title) : null, - m.description ? p(...renderUrl(m.description)) : null, - ...mediaFooter(m) - ) - ); - } - if (mediaType === 'document') { - return div({ class: 'tribe-media-item' }, - blobUrl ? a({ href: blobUrl, target: '_blank', class: 'tribe-action-btn' }, i18n.readDocument || 'Read Document') : p(i18n.tribeMediaEmpty), - div({ class: 'tribe-media-item-info' }, - m.title ? h2(m.title) : null, - m.description ? p(...renderUrl(m.description)) : null, - ...mediaFooter(m) - ) - ); - } - if (mediaType === 'bookmark') { - const url = m.url || m.description || ''; - return div({ class: 'tribe-media-item' }, - div({ class: 'tribe-media-item-info' }, - m.title ? h2(m.title) : null, - url ? div({ class: 'card-field' }, - span({ class: 'card-label' }, 'URL:'), - a({ href: url, target: '_blank', class: 'card-value' }, url) - ) : null, - m.description && m.description !== url ? p(...renderUrl(m.description)) : null, - ...mediaFooter(m) - ) - ); - } - if (mediaType === 'torrent') { - return div({ class: 'tribe-media-item' }, - blobUrl ? a({ href: blobUrl, class: 'tribe-action-btn' }, i18n.torrentDownloadButton || 'DOWNLOAD IT!') : p(i18n.tribeMediaEmpty), - div({ class: 'tribe-media-item-info' }, - m.title ? h2(m.title) : null, - m.description ? p(...renderUrl(m.description)) : null, - ...mediaFooter(m) - ) - ); - } - return null; - }; - - return div({ class: 'tribe-content-list' }, - div({ class: 'tribe-content-header' }, - h2(sTitle), - form({ method: 'GET', action: tribeUrl }, - input({ type: 'hidden', name: 'section', value: sectionKey }), - input({ type: 'hidden', name: 'action', value: 'create' }), - button({ type: 'submit', class: 'create-button' }, mediaBtnLabel) - ) - ), - media.length === 0 ? p(i18n.tribeMediaEmpty) : - div({ class: 'tribe-media-grid' }, media.map(renderMediaItem)) - ); -}; - const renderSubTribesSection = (tribe, items, query) => { const action = query.action; const isMemberSub = Array.isArray(tribe.members) && tribe.members.includes(userId); @@ -1277,7 +1586,7 @@ const renderSubTribesSection = (tribe, items, query) => { if (action === 'create' && canCreate) { return renderCreateForm(tribe, 'subtribes', [ - { name: 'title', label: i18n.tribeTitleLabel, required: true, placeholder: 'Name of the sub-tribe' }, + { name: 'title', label: i18n.tribeTitleLabel, maxlength: '100', required: true, placeholder: 'Name of the sub-tribe' }, { name: 'description', type: 'textarea', label: i18n.tribeDescriptionLabel, required: true, placeholder: 'Description of the sub-tribe' }, { name: 'location', label: i18n.tribeLocationLabel, placeholder: 'Where is this sub-tribe located?' }, { name: 'mapUrl', label: i18n.mapLocationTitle || 'Map Location', placeholder: i18n.mapUrlPlaceholder || '/maps/MAP_ID', spaceBefore: true }, @@ -1304,7 +1613,7 @@ const renderSubTribesSection = (tribe, items, query) => { subTribes.length === 0 ? null : div({ class: 'tribe-thumb-grid' }, - subTribes.map(st => { + paged(subTribes).map(st => { return a({ href: `/tribe/${encodeURIComponent(st.id)}`, class: 'tribe-thumb-link', title: st.title }, img({ src: toImageUrl(st.image, '/assets/images/default-tribe.png'), class: 'tribe-thumb-img', alt: st.title }) ); @@ -1313,176 +1622,6 @@ const renderSubTribesSection = (tribe, items, query) => { ); }; -const renderTribeMapsSection = (tribe, maps) => { - const items = Array.isArray(maps) ? maps : []; - const createBtn = form({ method: 'GET', action: '/maps' }, - input({ type: 'hidden', name: 'filter', value: 'create' }), - input({ type: 'hidden', name: 'tribeId', value: tribe.id }), - button({ type: 'submit', class: 'create-button' }, i18n.mapUploadButton || 'Create Map')); - if (items.length === 0) return div({ class: 'tribe-content-list' }, div({ class: 'tribe-content-header' }, h2(i18n.tribeSectionMaps || 'MAPS'), createBtn), p(i18n.noMaps || 'No maps yet')); - return div({ class: 'tribe-content-list' }, - div({ class: 'tribe-content-header' }, h2(i18n.tribeSectionMaps || 'MAPS'), createBtn), - items.map(m => - div({ class: 'card card-rpg tribe-card-padded' }, - div({ class: 'card-header' }, - h2({ class: 'card-label' }, `[${(i18n.typeMap || 'MAP').toUpperCase()}]`), - form({ method: 'GET', action: `/maps/${encodeURIComponent(m.key)}` }, - button({ type: 'submit', class: 'filter-btn' }, i18n.viewDetails || 'View Details')) - ), - div({ class: 'tribe-card-body' }, - m.title ? div({ class: 'card-field' }, - span({ class: 'card-label' }, (i18n.title || 'Title') + ':'), - span({ class: 'card-value' }, a({ href: `/maps/${encodeURIComponent(m.key)}` }, m.title)) - ) : null, - m.description ? p(m.description.substring(0, 200)) : null, - m.lat && m.lng ? span({ class: 'map-coords' }, `📍 ${m.lat.toFixed(4)}, ${m.lng.toFixed(4)}`) : null - ), - p({ class: 'card-footer' }, - span({ class: 'date-link' }, new Date(m.createdAt).toLocaleString()), - userLink(m.author) - ) - ) - ) - ); -}; - -const renderTribeTorrentsSection = (tribe, torrents) => { - const items = Array.isArray(torrents) ? torrents : []; - const createBtn = form({ method: 'GET', action: '/torrents' }, - input({ type: 'hidden', name: 'filter', value: 'create' }), - input({ type: 'hidden', name: 'tribeId', value: tribe.id }), - button({ type: 'submit', class: 'create-button' }, i18n.tribeCreateTorrent || 'Upload Torrent')); - if (items.length === 0) return div({ class: 'tribe-content-list' }, div({ class: 'tribe-content-header' }, h2(i18n.tribeSectionTorrents || 'TORRENTS'), createBtn), p(i18n.tribeTorrentsEmpty || 'No torrents, yet.')); - return div({ class: 'tribe-content-list' }, - div({ class: 'tribe-content-header' }, h2(i18n.tribeSectionTorrents || 'TORRENTS'), createBtn), - items.map(m => { - const blobName = encodeURIComponent((m.title || 'download').replace(/\.torrent$/i, '') + '.torrent'); - const blobUrl = m.url ? `/blob/${encodeURIComponent(m.url)}?name=${blobName}` : null; - return div({ class: 'card card-rpg tribe-card-padded' }, - div({ class: 'card-header' }, - h2({ class: 'card-label' }, `[${(i18n.typeTorrent || 'TORRENT').toUpperCase()}]`), - m._isMedia - ? (blobUrl ? a({ href: blobUrl, class: 'filter-btn' }, i18n.torrentDownload || 'Download') : null) - : form({ method: 'GET', action: `/torrents/${encodeURIComponent(m.rootId || m.key)}` }, - button({ type: 'submit', class: 'filter-btn' }, i18n.viewDetails || 'View Details')) - ), - div({ class: 'tribe-card-body' }, - m.title ? div({ class: 'card-field' }, - span({ class: 'card-label' }, (i18n.title || 'Title') + ':'), - span({ class: 'card-value' }, m._isMedia - ? (blobUrl ? a({ href: blobUrl }, m.title) : m.title) - : a({ href: `/torrents/${encodeURIComponent(m.rootId || m.key)}` }, m.title)) - ) : null, - m.description ? p(String(m.description).substring(0, 200)) : null, - blobUrl && !m._isMedia ? div({ class: 'card-field' }, a({ href: blobUrl, class: 'filter-btn' }, i18n.torrentDownload || 'Download')) : null - ), - p({ class: 'card-footer' }, - span({ class: 'date-link' }, new Date(m.createdAt).toLocaleString()), - userLink(m.author) - ) - ); - }) - ); -}; - -const renderTribePadsSection = (tribe, pads) => { - const items = Array.isArray(pads) ? pads : []; - const createBtn = form({ method: 'GET', action: '/pads' }, - input({ type: 'hidden', name: 'filter', value: 'create' }), - input({ type: 'hidden', name: 'tribeId', value: tribe.id }), - button({ type: 'submit', class: 'create-button' }, i18n.tribePadCreate || 'Create Pad')); - if (items.length === 0) return div({ class: 'tribe-content-list' }, div({ class: 'tribe-content-header' }, h2(i18n.tribeSectionPads || 'PADS'), createBtn), p(i18n.tribePadsEmpty || 'No pads, yet.')); - return div({ class: 'tribe-content-list' }, - div({ class: 'tribe-content-header' }, h2(i18n.tribeSectionPads || 'PADS'), createBtn), - items.map(m => - div({ class: 'card card-rpg tribe-card-padded' }, - div({ class: 'card-header' }, - h2({ class: 'card-label' }, `[${(i18n.typePad || 'PAD').toUpperCase()}]`), - form({ method: 'GET', action: `/pads/${encodeURIComponent(m.rootId)}` }, - button({ type: 'submit', class: 'filter-btn' }, i18n.viewDetails || 'View Details')) - ), - div({ class: 'tribe-card-body' }, - m.title ? div({ class: 'card-field' }, - span({ class: 'card-label' }, (i18n.title || 'Title') + ':'), - span({ class: 'card-value' }, a({ href: `/pads/${encodeURIComponent(m.rootId)}` }, m.title)) - ) : null - ), - p({ class: 'card-footer' }, - span({ class: 'date-link' }, new Date(m.createdAt).toLocaleString()), - userLink(m.author) - ) - ) - ) - ); -}; - -const renderTribeChatsSection = (tribe, chats) => { - const items = Array.isArray(chats) ? chats : []; - const createBtn = form({ method: 'GET', action: '/chats' }, - input({ type: 'hidden', name: 'filter', value: 'create' }), - input({ type: 'hidden', name: 'tribeId', value: tribe.id }), - button({ type: 'submit', class: 'create-button' }, i18n.tribeChatCreate || 'Create Chat')); - if (items.length === 0) return div({ class: 'tribe-content-list' }, div({ class: 'tribe-content-header' }, h2(i18n.tribeSectionChats || 'CHATS'), createBtn), p(i18n.tribeChatsEmpty || 'No chats, yet.')); - return div({ class: 'tribe-content-list' }, - div({ class: 'tribe-content-header' }, h2(i18n.tribeSectionChats || 'CHATS'), createBtn), - items.map(m => - div({ class: 'card card-rpg tribe-card-padded' }, - div({ class: 'card-header' }, - h2({ class: 'card-label' }, `[${(i18n.typeChat || 'CHAT').toUpperCase()}]`), - form({ method: 'GET', action: `/chats/${encodeURIComponent(m.key)}` }, - button({ type: 'submit', class: 'filter-btn' }, i18n.viewDetails || 'View Details')) - ), - div({ class: 'tribe-card-body' }, - m.title ? div({ class: 'card-field' }, - span({ class: 'card-label' }, (i18n.title || 'Title') + ':'), - span({ class: 'card-value' }, a({ href: `/chats/${encodeURIComponent(m.key)}` }, m.title)) - ) : null, - m.description ? p(m.description.substring(0, 200)) : null - ), - p({ class: 'card-footer' }, - span({ class: 'date-link' }, new Date(m.createdAt).toLocaleString()), - userLink(m.author) - ) - ) - ) - ); -}; - -const renderTribeCalendarsSection = (tribe, calendars) => { - const items = Array.isArray(calendars) ? calendars : []; - const createBtn = form({ method: 'GET', action: '/calendars' }, - input({ type: 'hidden', name: 'filter', value: 'create' }), - input({ type: 'hidden', name: 'tribeId', value: tribe.id }), - button({ type: 'submit', class: 'create-button' }, i18n.tribeCalendarCreate || 'Create Calendar')); - if (items.length === 0) return div({ class: 'tribe-content-list' }, div({ class: 'tribe-content-header' }, h2(i18n.tribeSectionCalendars || 'CALENDARS'), createBtn), p(i18n.tribeCalendarsEmpty || 'No calendars, yet.')); - return div({ class: 'tribe-content-list' }, - div({ class: 'tribe-content-header' }, h2(i18n.tribeSectionCalendars || 'CALENDARS'), createBtn), - items.map(m => - div({ class: 'card card-rpg tribe-card-padded' }, - div({ class: 'card-header' }, - h2({ class: 'card-label' }, `[${(i18n.typeCalendar || 'CALENDAR').toUpperCase()}]`), - form({ method: 'GET', action: `/calendars/${encodeURIComponent(m.rootId)}` }, - button({ type: 'submit', class: 'filter-btn' }, i18n.viewDetails || 'View Details')) - ), - div({ class: 'tribe-card-body' }, - m.title ? div({ class: 'card-field' }, - span({ class: 'card-label' }, (i18n.title || 'Title') + ':'), - span({ class: 'card-value' }, a({ href: `/calendars/${encodeURIComponent(m.rootId)}` }, m.title)) - ) : null, - m.deadline ? div({ class: 'card-field' }, - span({ class: 'card-label' }, (i18n.calendarDeadlineLabel || 'Deadline') + ':'), - span({ class: 'card-value' }, new Date(m.deadline).toLocaleDateString()) - ) : null - ), - p({ class: 'card-footer' }, - span({ class: 'date-link' }, new Date(m.createdAt).toLocaleString()), - userLink(m.author) - ) - ) - ) - ); -}; - exports.tribeView = async (tribe, userIdParam, query, section, sectionData) => { if (!tribe) { return div({ class: 'error' }, i18n.tribeNotFound); @@ -1496,24 +1635,13 @@ exports.tribeView = async (tribe, userIdParam, query, section, sectionData) => { let sectionContent; switch (section) { - case 'inhabitants': sectionContent = renderInhabitantsSection(tribe, sectionData); break; - case 'feed': - sectionContent = div( - query.sent ? div({ class: 'card card-rpg tribe-banner' }, - p({ class: 'bold' }, i18n.tribeFeedSent || 'Message sent successfully!') - ) : null, - tribe.members.includes(config.keys.id) - ? form({ class: 'tribe-feed-compose', method: 'POST', action: `/tribe/${encodeURIComponent(tribe.id)}/message` }, - textarea({ name: 'message', rows: 4, maxlength: MAX_MESSAGE_LENGTH, placeholder: i18n.tribeFeedMessagePlaceholder }), - button({ type: 'submit', class: 'tribe-feed-send' }, i18n.tribeFeedSend) - ) - : null, - await renderFeedTribeView(sectionData, tribe, query, query.filter) - ); - break; + case 'inhabitants': sectionContent = renderInhabitantsSection(tribe, sectionData, query); break; + case 'overview': sectionContent = renderOverviewSection(tribe, query, sectionData); break; + case 'feed': sectionContent = renderFeedTribeView(sectionData, tribe, query); break; case 'events': sectionContent = renderEventsSection(tribe, sectionData, query); break; case 'tasks': sectionContent = renderTasksSection(tribe, sectionData, query); break; case 'votations': sectionContent = renderVotationsSection(tribe, sectionData, query); break; + case 'polls': sectionContent = renderTribePollsSection(tribe, sectionData, query); break; case 'forum': sectionContent = renderForumSection(tribe, sectionData, query); break; case 'subtribes': sectionContent = renderSubTribesSection(tribe, sectionData, query); break; case 'search': sectionContent = renderTribeSearchSection(tribe, sectionData, query); break; @@ -1522,11 +1650,14 @@ exports.tribeView = async (tribe, userIdParam, query, section, sectionData) => { case 'videos': sectionContent = renderTribeMediaTypeSection(tribe, sectionData, query, 'video'); break; case 'documents': sectionContent = renderTribeMediaTypeSection(tribe, sectionData, query, 'document'); break; case 'bookmarks': sectionContent = renderTribeMediaTypeSection(tribe, sectionData, query, 'bookmark'); break; - case 'torrents': sectionContent = renderTribeTorrentsSection(tribe, sectionData); break; - case 'maps': sectionContent = renderTribeMapsSection(tribe, sectionData); break; - case 'pads': sectionContent = renderTribePadsSection(tribe, sectionData); break; - case 'chats': sectionContent = renderTribeChatsSection(tribe, sectionData); break; - case 'calendars': sectionContent = renderTribeCalendarsSection(tribe, sectionData); break; + case 'torrents': sectionContent = renderTribeTorrentsSection(tribe, sectionData, query); break; + case 'files': sectionContent = renderTribeFilesSection(tribe, sectionData, query); break; + case 'maps': sectionContent = renderTribeMapsSection(tribe, sectionData, query); break; + case 'pads': sectionContent = renderTribePadsSection(tribe, sectionData, query); break; + case 'wiki': sectionContent = renderTribeWikiSectionPage(tribe, sectionData, query); break; + case 'chats': sectionContent = renderTribeChatsSection(tribe, sectionData, query); break; + case 'rooms': sectionContent = renderTribeRoomsSectionPage(tribe, sectionData, query); break; + case 'calendars': sectionContent = renderTribeCalendarsSection(tribe, sectionData, query); break; case 'governance': sectionContent = renderGovernance(tribe, sectionData); break; case 'trending': sectionContent = renderTribeTrendingSection(tribe, sectionData, query); break; case 'tags': sectionContent = renderTribeTagsSection(tribe, sectionData, query); break; @@ -1546,7 +1677,7 @@ exports.tribeView = async (tribe, userIdParam, query, section, sectionData) => { : (isAuthorOfTribe || (tribe.inviteMode === 'open' && isMemberOfTribe)); const canLeave = isMemberOfTribe && (isLarpHouse || !isAuthorOfTribe); const showOwnerControls = !isLarpHouse && isAuthorOfTribe; - const inviteHref = isLarpHouse ? '/invites#invites-houses' : '/invites#invites-tribes'; + const inviteHref = '/invites?filter=others'; const showInviteTop = isOutsider && !tribe.parentTribeId; const hasOpenInvite = !!tribe.openInviteCode; const canManageOpenInvite = hasOpenInvite && (tribe.openInviteBy === userId || isAuthorOfTribe); @@ -1554,6 +1685,9 @@ exports.tribeView = async (tribe, userIdParam, query, section, sectionData) => { const tribeDetails = div({ class: 'tribe-details' }, div({ class: 'tribe-side' }, + div({ class: 'card-header activity-card-header' }, + renderContentActions(tribe.id, `/tribe/${encodeURIComponent(tribe.id)}`, { author: tribe.author, reportTitle: tribe.title, deleteAction: showOwnerControls ? contentDeleteAction('tribe', tribe.id) : undefined }) + ), tribe.parentTribe ? div({ class: 'tribe-parent-box' }, h2({ class: 'tribe-info-label' }, i18n.tribeMainTribeLabel || 'MAIN TRIBE'), @@ -1575,18 +1709,27 @@ exports.tribeView = async (tribe, userIdParam, query, section, sectionData) => { table({ class: 'tribe-info-table' }, tr( td({ class: 'tribe-info-label' }, i18n.tribeCreatedAt || 'CREATED'), - td({ class: 'tribe-info-value', colspan: '3' }, new Date(tribe.createdAt).toLocaleString()) + td({ class: 'tribe-info-value', colspan: '3' }, moment(tribe.createdAt).format("YYYY/MM/DD HH:mm")) ), tr( td({ class: 'tribe-info-value tribe-author-cell', colspan: '4' }, userLink(tribe.author)) ), tribe.location ? tr( td({ class: 'tribe-info-label' }, i18n.tribeLocationLabel || 'LOCATION'), - td({ class: 'tribe-info-value', colspan: '3' }, ...renderUrl(tribe.location)) + td({ class: 'tribe-info-value', colspan: '3' }, ...renderStyledText(tribe.location)) ) : null ), h2({ class: 'tribe-members-count' }, `${i18n.tribeMembersCount}: ${tribe.memberCount != null ? tribe.memberCount : tribe.members.length}`), - (canGenerateInvite || (isOutsider && !showInviteTop)) ? div({ class: 'tribe-side-actions' }, + (isOutsider && hasOpenInvite) + ? div({ class: 'tribe-side-actions tribe-side-center' }, + renderInviteQrCard({ qrDataUrl: tribe.openInviteQr, name: tribe.title }), + form({ method: 'POST', action: '/tribes/open-invite/join' }, + input({ type: 'hidden', name: 'tribeId', value: tribe.id }), + button({ type: 'submit', class: 'tribe-action-btn' }, i18n.tribeJoinOpen) + ) + ) + : null, + (canGenerateInvite || (isOutsider && !showInviteTop)) ? div({ class: canGenerateInvite ? 'tribe-side-actions' : 'tribe-side-actions tribe-side-center tribe-enter-invite' }, canGenerateInvite ? (isLarpHouse ? form({ method: 'POST', action: '/larp/invite/create' }, @@ -1608,6 +1751,25 @@ exports.tribeView = async (tribe, userIdParam, query, section, sectionData) => { ? a({ class: 'tribe-action-btn', href: inviteHref }, i18n.tribeEnterInvite) : null ) : null, + (tribe.subscription && !isOutsider) + ? renderSubscriptionBox({ + target: tribe.id, + scope: 'tribes', + subscribed: tribe.subscription.subscribed === true, + count: tribe.subscription.count, + isOwner: String(tribe.author) === String(userIdParam), + returnTo: `/tribe/${encodeURIComponent(tribe.id)}` + }) + : null, + (isLarpHouse && larpHouseKey === 'academia' && isOutsider && String(tribe.viewerHouse || '') !== 'academia') + ? div({ class: 'tribe-side-actions' }, + form({ method: 'POST', action: '/larp/join' }, + input({ type: 'hidden', name: 'house', value: 'academia' }), + input({ type: 'hidden', name: 'returnTo', value: `/tribe/${encodeURIComponent(tribe.id)}` }), + button({ type: 'submit', class: 'tribe-action-btn' }, i18n.larpInviteRedeem || 'Join House') + ) + ) + : null, (hasOpenInvite && canManageOpenInvite) ? div({ class: 'tribe-open-invite-panel' }, div({ class: 'tribe-open-invite' }, span({ class: 'tribe-members-count' }, i18n.tribeInviteCodeText), @@ -1619,9 +1781,12 @@ exports.tribeView = async (tribe, userIdParam, query, section, sectionData) => { button({ type: 'submit', class: 'tribe-action-btn danger-btn' }, i18n.tribeRemoveInvitation) ) ) : null, - tribe.description ? p({ class: 'tribe-side-description' }, ...renderUrl(tribe.description)) : null, - renderMapLocationVisitLabel(tribe.mapUrl), - (!tribe.parentTribeId && (canCreateSub || subTribes.length > 0 || showInviteTop)) ? div({ class: 'tribe-side-subtribes' }, + tribe.description ? p({ class: 'tribe-side-description' }, ...renderStyledText(tribe.description)) : null, + renderMapEmbed(tribe.mapData, tribe.mapUrl), + showInviteTop + ? div({ class: 'tribe-side-actions tribe-side-center tribe-enter-invite' }, a({ class: 'tribe-action-btn', href: inviteHref }, i18n.tribeEnterInvite)) + : null, + (!tribe.parentTribeId && (canCreateSub || subTribes.length > 0)) ? div({ class: 'tribe-side-subtribes' }, canCreateSub ? form({ method: 'GET', action: `/tribe/${encodeURIComponent(tribe.id)}` }, input({ type: 'hidden', name: 'section', value: 'subtribes' }), @@ -1629,9 +1794,6 @@ exports.tribeView = async (tribe, userIdParam, query, section, sectionData) => { button({ type: 'submit', class: 'tribe-action-btn' }, i18n.tribeSubTribesCreate) ) : null, - showInviteTop - ? a({ class: 'tribe-action-btn', href: inviteHref }, i18n.tribeEnterInvite) - : null, subTribes.length > 0 ? div({ class: 'tribe-subtribes-list' }, subTribes.map(st => @@ -1651,11 +1813,6 @@ exports.tribeView = async (tribe, userIdParam, query, section, sectionData) => { button({ type: 'submit', class: 'tribe-action-btn' }, i18n.tribeUpdateButton) ) : null, - showOwnerControls - ? form({ method: 'POST', action: `/tribes/delete/${encodeURIComponent(tribe.id)}` }, - button({ type: 'submit', class: 'tribe-action-btn danger-btn' }, i18n.tribeDeleteButton) - ) - : null, canLeave ? (isLarpHouse ? form({ method: 'POST', action: '/larp/leave' }, @@ -1676,10 +1833,132 @@ exports.tribeView = async (tribe, userIdParam, query, section, sectionData) => { ) ); - return template( + const page = template( pageTitle, tribeDetails ); + return section === 'documents' ? `${page}` : page; +}; + +exports.clearnetTribeView = async ({ tribe, items, names = {}, slug, page = 1, filterType = '', filterAuthor = '' }) => { + const t = tribe || {}; + const all = (Array.isArray(items) ? items : []).filter(it => it && it.contentType !== 'forum-reply'); + const kindKey = (it) => it.contentType === 'media' ? `media-${String(it.mediaType || '')}` : String(it.contentType || ''); + const kindName = (it) => it.contentType === 'media' ? activityMediaTypeName(String(it.mediaType || '')) : contentTypeName(it.contentType); + const activeType = String(filterType || '').toLowerCase(); + const activeAuthor = String(filterAuthor || ''); + const byAuthor = activeAuthor ? all.filter(it => it.author === activeAuthor) : all; + const list = activeType ? byAuthor.filter(it => kindKey(it) === activeType) : byAuthor; + const base = `/c/tribe/${encodeURIComponent(slug || '')}`; + const hrefFor = (type, author) => { + const q = new URLSearchParams(); + if (type) q.set('type', type); + if (author) q.set('author', author); + const qs = q.toString(); + return escapeHtml(qs ? `${base}?${qs}` : base); + }; + const paged = paginateClearnet(list, page); + const pager = renderClearnetPager({ base, params: { type: activeType, author: activeAuthor }, page: paged.page, pages: paged.pages, per: paged.per, total: paged.total }); + const name = t.title || i18n.cnUntitled || 'Untitled'; + const cover = cnBlobUrl(t.image); + const desc = renderRichText(t.description || ''); + const day = (v) => { const d = new Date(v); return v && !isNaN(d) ? d.toISOString().slice(0, 10) : ''; }; + const who = (id) => (names && names[id]) || (id ? `${String(id).slice(0, 10)}…` : ''); + const countBy = (arr, fn) => { const out = new Map(); for (const it of arr) { const k = fn(it); if (k) out.set(k, (out.get(k) || 0) + 1); } return out; }; + const typeCounts = countBy(byAuthor, kindKey); + const authorCounts = countBy(activeType ? all.filter(it => kindKey(it) === activeType) : all, it => it.author); + const btn = (href, label, count, active) => `${escapeHtml(label)} (${count})`; + const typeRow = typeCounts.size > 1 + ? `
${btn(hrefFor('', activeAuthor), i18n.all, byAuthor.length, !activeType)}${[...typeCounts.entries()].map(([k, n]) => btn(hrefFor(k, activeAuthor), kindName(byAuthor.find(it => kindKey(it) === k)), n, activeType === k)).join('')}
` + : ''; + const authorRow = authorCounts.size > 1 + ? `
${[...authorCounts.entries()].map(([id, n]) => btn(hrefFor(activeType, activeAuthor === id ? '' : id), who(id), n, activeAuthor === id)).join('')}
` + : ''; + const renderItem = (it) => { + const mt = it.contentType === 'media' ? String(it.mediaType || '') : ''; + const kind = kindName(it); + const title = escapeHtml(it.title || ''); + const blob = cnBlobUrl(it.image); + const media = !blob ? '' + : mt === 'image' ? `${title}` + : mt === 'audio' ? `` + : mt === 'video' ? `` + : ['document', 'file', 'torrent'].includes(mt) ? `⇩ ${title || escapeHtml(kind)}` + : ''; + const url = mt === 'bookmark' ? String(it.url || '') : ''; + const href = url ? safeExternalHref(url) : ''; + const bookmark = !url ? '' + : href ? `${escapeHtml(url)}` + : `${escapeHtml(url)}`; + const details = [ + it.date ? `${i18n.tribeEventDate || 'Date'}: ${it.date}` : '', + it.deadline ? `${i18n.tribeTaskDeadline || 'Deadline'}: ${it.deadline}` : '', + it.location ? `${i18n.tribeEventLocation || 'Location'}: ${it.location}` : '', + it.status ? (statusI18n()[it.status] || it.status) : '', + it.contentType === 'map' && Number.isFinite(Number(it.lat)) ? `📍 ${Number(it.lat).toFixed(4)}, ${Number(it.lng).toFixed(4)}` : '', + ...(it.contentType === 'map' && Array.isArray(it.markers) ? it.markers.map(mk => `📍 ${Number(mk.lat).toFixed(4)}, ${Number(mk.lng).toFixed(4)}${mk.label ? ` · ${mk.label}` : ''}`) : []), + ...(it.contentType === 'calendar' && Array.isArray(it.dates) ? it.dates.map(d => `📅 ${day(d.date)}${d.label ? ` · ${d.label}` : ''}`) : []) + ].filter(Boolean).map(d => `${escapeHtml(String(d))}`).join(''); + const options = it.contentType === 'votation' && Array.isArray(it.options) && it.options.length + ? `
    ${it.options.map(o => `
  • ${escapeHtml(String(o))}
  • `).join('')}
` + : ''; + const replies = it.contentType === 'forum' && Array.isArray(it.replies) ? it.replies : []; + const repliesHtml = replies.length + ? `
${escapeHtml(String(i18n.forumMessages || i18n.tribeForumReplies || 'MESSAGES').toUpperCase())}: ${replies.length}
${replies.map(r => `
${escapeHtml(who(r.author))}${day(r.createdAt) ? ` · ${escapeHtml(day(r.createdAt))}` : ''}

${renderRichText(r.description || '')}

`).join('')}
` + : ''; + const text = it.description && it.description !== url ? renderRichText(it.description) : ''; + const when = day(it.createdAt); + return `
+
[${escapeHtml(String(kind).toUpperCase())}]${when ? `📅 ${escapeHtml(when)}` : ''}${it.author ? `${escapeHtml(who(it.author))}` : ''}
+ ${title ? `

${it.href ? `${title}` : title}

` : ''} + ${media} + ${text ? `

${text}

` : ''} + ${bookmark} + ${details ? `
${details}
` : ''} + ${options} + ${repliesHtml} +
`; + }; + const extraCss = ` +.cn-tribe-head{display:flex;gap:24px;flex-wrap:wrap;align-items:flex-start;margin-bottom:24px} +.cn-tribe-cover{width:160px;height:160px;border-radius:8px;border:3px solid var(--fg);object-fit:cover;background:#000;flex:0 0 auto} +.cn-tribe-head-body{flex:1 1 280px;min-width:0} +.cn-tribe-name{color:var(--fg);margin:0 0 8px 0;font-size:28px;font-weight:700;word-break:break-word} +.cn-tribe-desc{color:var(--fg-soft);white-space:pre-wrap;margin:0} +.cn-tribe-list{display:flex;flex-direction:column;gap:14px;margin-top:16px} +.cn-tribe-item{background:var(--bg-elev);border:1px solid var(--border);border-radius:8px;padding:14px 16px;min-width:0} +.cn-tribe-meta{display:flex;gap:12px;flex-wrap:wrap;align-items:baseline;color:var(--fg-dim);font-size:12px;word-break:break-all} +.cn-tribe-title{color:var(--fg);font-size:18px;font-weight:700;margin:8px 0;word-break:break-word} +.cn-tribe-text{color:var(--fg-soft);line-height:1.5;margin:8px 0;word-break:break-word} +.cn-tribe-link{display:inline-block;margin:8px 0;padding:6px 12px;background:var(--bg-sub);border:1px solid var(--border);border-radius:6px;color:var(--fg);word-break:break-all} +.cn-tribe-options{margin:8px 0;padding-left:20px;color:var(--fg-soft)} +.cn-tribe-replies{margin-top:12px;border-top:1px solid var(--border);padding-top:8px} +.cn-tribe-reply{border-left:2px solid var(--border);padding:4px 0 4px 12px;margin:8px 0} +.cn-tribe-reply-meta{color:var(--fg-dim);font-size:12px;word-break:break-all} +.cn-tribe-reply p{margin:4px 0 0 0;color:var(--fg-soft);line-height:1.5;word-break:break-word} +.cn-empty-content{background:var(--bg-elev);border:1px dashed var(--border);border-radius:8px;padding:24px;text-align:center;color:var(--fg-dim);font-size:14px} +` + CLEARNET_HUB_CSS + CLEARNET_PAGER_CSS; + const body = ` +
+ ${escapeHtml(name)} +
+

${escapeHtml(name)}

+ ${desc ? `

${desc}

` : ''} +
+
+ ${typeRow}${authorRow} + ${list.length + ? `

${escapeHtml(i18n.cnPublicContent || 'Content')} (${list.length})

${paged.items.map(renderItem).join('')}
${pager}` + : `
${escapeHtml(i18n.cnCategoryEmpty || '')}
`} +`; + return renderClearnetPage({ + title: `${name} | Oasis`, + ogTitle: name, + ogDescription: t.description || '', + ogImage: cover, + extraCss, + body + }); }; const GOVERNANCE_METHODS = ['DEMOCRACY', 'MAJORITY', 'MINORITY', 'DICTATORSHIP', 'KARMATOCRACY']; @@ -1719,8 +1998,9 @@ const governanceFilterBar = (tribeId, currentFilter, showPublish) => { ); }; -const fmtTermDate = (d) => moment(d).format('YYYY-MM-DD HH:mm:ss'); +const fmtTermDate = (d) => d ? moment(d).format('YYYY-MM-DD HH:mm:ss') : '—'; const fmtTimeLeft = (end) => { + if (!end) return '—'; const diff = moment(end).diff(moment()); if (diff <= 0) return '0d 00:00:00'; const dur = moment.duration(diff); @@ -1743,9 +2023,9 @@ const governmentCard = (tribe, term, leaders) => { const isAnarchy = method === 'ANARCHY'; const i18nMeth = i18n[`parliamentMethod${method}`]; const methodLabel = (i18nMeth && String(i18nMeth).trim() ? String(i18nMeth) : method).toUpperCase(); - const termStart = term.startAt || moment().toISOString(); - const termEnd = term.endAt || moment(termStart).add(60, 'days').toISOString(); - const population = Array.isArray(tribe.members) ? tribe.members.length : 0; + const termStart = term.startAt || null; + const termEnd = term.endAt || null; + const population = tribe.memberCount != null ? tribe.memberCount : (Array.isArray(tribe.members) ? tribe.members.length : 0); const winnerVotes = Number(term.winnerVotes || 0); const totalVotes = Number(term.totalVotes || 0); const votesDisplay = `${winnerVotes} (${totalVotes})`; @@ -1826,7 +2106,7 @@ const candidaturesBlock = (tribe, candidatures, alreadyPublishedThisGlobalCycle) ' ', form({ method: 'POST', action: `/tribe/${encodeURIComponent(tribe.id)}/governance/candidature/vote`, class: 'inline-form' }, input({ type: 'hidden', name: 'candidatureId', value: c.id }), - button({ type: 'submit', class: 'filter-btn' }, i18n.vote || 'Vote') + button({ type: 'submit', class: 'tribe-action-btn' }, i18n.vote || 'Vote') ) ) )) @@ -1839,9 +2119,9 @@ const rulesBlock = (tribe, rules, isCreator) => div({}, h3(i18n.tribeGovernanceAddRule || 'Add rule'), form({ method: 'POST', action: `/tribe/${encodeURIComponent(tribe.id)}/governance/rule/add` }, label({}, i18n.tribeGovRuleTitle || 'Title'), br(), - input({ type: 'text', name: 'title', required: true }), br(), br(), + input({ type: 'text', name: 'title', maxlength: '100', required: true }), br(), br(), label({}, i18n.tribeGovRuleBody || 'Body'), br(), - textarea({ name: 'body', rows: 4 }), br(), br(), + textarea({ maxlength: "3000", name: 'body', rows: 4 }), br(), br(), button({ type: 'submit', class: 'create-button' }, i18n.save || 'Save') ) ) @@ -1856,7 +2136,7 @@ const rulesBlock = (tribe, rules, isCreator) => div({}, isCreator ? form({ method: 'POST', action: `/tribe/${encodeURIComponent(tribe.id)}/governance/rule/delete`, class: 'inline-form' }, input({ type: 'hidden', name: 'ruleId', value: r.id }), - button({ type: 'submit', class: 'filter-btn danger-btn' }, i18n.delete || 'Delete') + button({ type: 'submit', class: 'tribe-action-btn danger-btn' }, i18n.delete || 'Delete') ) : null ) @@ -1866,7 +2146,7 @@ const rulesBlock = (tribe, rules, isCreator) => div({}, const renderGovernance = (tribe, data) => { const { filter, term, candidatures, rules, leaders, isCreator, canPublishToGlobal, alreadyPublishedThisGlobalCycle, hasElectedCandidate } = data || {}; const f = filter || 'government'; - const header = div({ class: 'tags-header' }, + const header = div({ class: 'tags-header module-header-line' }, h2(i18n.tribeSectionGovernance || 'GOVERNANCE'), p(i18n.tribeGovernanceDesc || 'Internal governance for this tribe. Propose candidatures, debate rules, elect leaders — mirrors the global Parliament.') ); diff --git a/src/views/video_view.js b/src/views/video_view.js index 5dbfe9c..8c64b43 100644 --- a/src/views/video_view.js +++ b/src/views/video_view.js @@ -15,12 +15,14 @@ const { select, option } = require("../server/node_modules/hyperaxe"); +const { renderCommentsSection: renderSharedCommentsSection, renderCommentsLink } = require("./comments_view"); const moment = require("../server/node_modules/moment"); -const { template, i18n, renderOpinionsVoting, userLink, renderSpreadButton, renderEcoTax, renderLifespanChip, renderContentActions } = require("./main_views"); +const { renderLicenseChip, renderLicenseSelect, renderReachChip, renderClearnetSelector, renderClearnetSwitch, renderTribeOriginChip } = require('./clearnet_view'); +const { clearnetItemHref, template, i18n, renderOpinionsVoting, renderEngagement, userLink, renderSpreadButton, renderEcoTax, renderLifespanChip, renderContentActions, renderSpreadEditWarning, renderModuleStats, moduleIsEmpty, contentDeleteAction, paged, blobUrlOf } = require("./main_views"); const { config } = require("../server/SSB_server.js"); -const { renderUrl } = require("../backend/renderUrl") -const { renderMapLocationVisitLabel } = require("./maps_view"); +const { renderStyledText } = require("../backend/renderStyledText") +const { renderMapLocationVisitLabel, renderMapEmbed } = require("./maps_view"); const userId = config.keys.id; @@ -37,18 +39,6 @@ const buildReturnTo = (filter, params = {}) => { return `/videos?${parts.join("&")}`; }; -const renderPMButton = (recipient, className = "filter-btn") => { - const r = safeText(recipient); - if (!r) return null; - if (String(r) === String(userId)) return null; - - return form( - { method: "GET", action: "/pm" }, - input({ type: "hidden", name: "recipients", value: r }), - button({ type: "submit", class: className }, i18n.privateMessage) - ); -}; - const renderTags = (tags) => { const list = safeArr(tags).map((t) => String(t || "").trim()).filter(Boolean); return list.length @@ -59,28 +49,13 @@ const renderTags = (tags) => { : null; }; -const renderVideoFavoriteToggle = (videoObj, returnTo = "") => - form( - { - method: "POST", - action: videoObj.isFavorite - ? `/videos/favorites/remove/${encodeURIComponent(videoObj.key)}` - : `/videos/favorites/add/${encodeURIComponent(videoObj.key)}` - }, - returnTo ? input({ type: "hidden", name: "returnTo", value: returnTo }) : null, - button( - { type: "submit", class: "filter-btn" }, - videoObj.isFavorite ? i18n.videoRemoveFavoriteButton : i18n.videoAddFavoriteButton - ) - ); - const renderVideoPlayer = (videoObj) => videoObj?.url ? div( - { class: "video-container", style: "display:flex;gap:8px;align-items:center;flex-wrap:wrap;" }, + { class: "video-container video-container-row" }, videoHyperaxe({ controls: true, - src: `/blob/${encodeURIComponent(videoObj.url)}`, + src: blobUrlOf(videoObj.url), preload: "metadata" }) ) @@ -103,78 +78,16 @@ const renderVideoOwnerActions = (filter, videoObj, params = {}) => { ) ); } - items.push( - form( - { method: "POST", action: `/videos/delete/${encodeURIComponent(videoObj.key)}` }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - button({ class: "delete-btn", type: "submit" }, i18n.videoDeleteButton) - ) - ); return items; }; const renderVideoCommentsSection = (videoId, comments = [], returnTo = null) => { - const list = safeArr(comments).filter(c => { - const t = c && c.value && c.value.content && c.value.content.text; - return t && String(t).trim(); + return renderSharedCommentsSection({ + action: `/videos/${encodeURIComponent(videoId)}/comments`, + comments: comments, + returnTo: returnTo }); - const commentsCount = list.length; - - return div( - { class: "vote-comments-section" }, - div( - { class: "comments-count" }, - span({ class: "card-label" }, i18n.voteCommentsLabel + ": "), - span({ class: "card-value" }, String(commentsCount)) - ), - div( - { class: "comment-form-wrapper" }, - h2({ class: "comment-form-title" }, i18n.voteNewCommentLabel), - form( - { method: "POST", action: `/videos/${encodeURIComponent(videoId)}/comments`, class: "comment-form", enctype: "multipart/form-data" }, - returnTo ? input({ type: "hidden", name: "returnTo", value: returnTo }) : null, - textarea({ - id: "comment-text", - name: "text", - rows: 4, - class: "comment-textarea", - placeholder: i18n.voteNewCommentPlaceholder - }), - div({ class: "comment-file-upload" }, label(i18n.uploadMedia), input({ type: "file", name: "blob" })), - br(), - button({ type: "submit", class: "comment-submit-btn" }, i18n.voteNewCommentButton) - ) - ), - list.length - ? div( - { class: "comments-list" }, - list.map((c) => { - const author = c?.value?.author || ""; - const ts = c?.value?.timestamp || c?.timestamp; - const absDate = ts ? moment(ts).format("YYYY/MM/DD HH:mm:ss") : ""; - const relDate = ts ? moment(ts).fromNow() : ""; - const content = c?.value?.content || {}; - const rootId = content.fork || content.root || null; - const text = content.text || ""; - - return div( - { class: "votations-comment-card" }, - span( - { class: "created-at" }, - span(i18n.createdBy), - author ? userLink(author) : span("(unknown)"), - absDate ? span(" | ") : "", - absDate ? span({ class: "votations-comment-date" }, absDate) : "", - relDate ? span({ class: "votations-comment-date" }, " | ", i18n.sendTime) : "", - relDate && rootId ? a({ href: `/thread/${encodeURIComponent(rootId)}#${encodeURIComponent(c.key)}` }, relDate) : "" - ), - p({ class: "votations-comment-text" }, ...renderUrl(text)) - ); - }) - ) - : p({ class: "votations-no-comments" }, i18n.voteNoCommentsYet) - ); }; const renderVideoList = exports.renderVideoList = (videos, filter, params = {}) => { @@ -184,57 +97,34 @@ const renderVideoList = exports.renderVideoList = (videos, filter, params = {}) ? videos.map((videoObj) => { const commentCount = typeof videoObj.commentCount === "number" ? videoObj.commentCount : 0; const title = safeText(videoObj.title); - const ownerActions = renderVideoOwnerActions(filter, videoObj, params); const isOwn = videoObj.author && String(videoObj.author) === String(userId); + const headerActions = typeof params.headerActions === "function" + ? params.headerActions(videoObj) + : videoObj.tribeOrigin + ? renderContentActions(null, videoObj.tribeOrigin.href) + : renderContentActions(videoObj.key, `/videos/${encodeURIComponent(videoObj.key)}`, { spread: (params.spreadMap && params.spreadMap.get(videoObj.key)) || params.spreads || null, author: videoObj.author, favKind: 'videos', torrentFrom: { blobId: videoObj.url, name: videoObj.title }, isFavorite: videoObj.isFavorite, reportTitle: videoObj.title, returnTo, deleteAction: isOwn ? contentDeleteAction('video', videoObj.key) : undefined }); + const titleHref = typeof params.headerActions === "function" ? null : videoObj.tribeOrigin ? videoObj.tribeOrigin.href : `/videos/${encodeURIComponent(videoObj.key)}`; + const engagement = typeof params.engagement === "function" + ? params.engagement(videoObj) + : videoObj.tribeOrigin ? null : renderEngagement(videoObj.key, + renderOpinionsVoting('/videos/opinions', videoObj.key, videoObj.opinions, returnTo, videoObj.opinions_inhabitants), + renderCommentsLink({ href: `/videos/${encodeURIComponent(videoObj.key)}`, count: commentCount }) + ); return div( { class: "trending-card video-card" + (isOwn ? " own-content" : "") }, div( { class: "card-header activity-card-header" }, span(), - renderContentActions(videoObj.key, `/videos/${encodeURIComponent(videoObj.key)}`) + headerActions ), div( { class: "card-section video-card-body" }, - div( - { class: "bookmark-topbar" }, - div( - { class: "bookmark-topbar-left" }, - renderVideoFavoriteToggle(videoObj, returnTo), - renderPMButton(videoObj.author) - ), - ownerActions.length - ? div({ class: "bookmark-topbar-right" }, div({ class: "bookmark-actions" }, ...ownerActions)) - : null - ), - title ? h2(title) : null, + div({ class: "shop-title-row" }, title ? h2(titleHref ? a({ href: titleHref, class: "card-title-link" }, title) : title) : null, videoObj.tribeOrigin ? renderTribeOriginChip(videoObj.tribeOrigin) : videoObj.clearnet === true ? renderReachChip(true, i18n, clearnetItemHref('videos', videoObj.title, videoObj.key)) : null, renderLicenseChip(videoObj.license), ...(typeof params.titleChips === "function" ? params.titleChips(videoObj) : [])), videoObj.lifetime ? div({ class: "card-chips-row" }, renderLifespanChip(videoObj.lifetime, i18n)) : null, renderVideoPlayer(videoObj), - div( - { class: "card-comments-summary" }, - span({ class: "card-label" }, i18n.voteCommentsLabel + ":"), - span({ class: "card-value" }, String(commentCount)), - br(), - br(), - form( - { method: "GET", action: `/videos/${encodeURIComponent(videoObj.key)}` }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - input({ type: "hidden", name: "filter", value: filter || "all" }), - params.q ? input({ type: "hidden", name: "q", value: params.q }) : null, - params.sort ? input({ type: "hidden", name: "sort", value: params.sort }) : null, - button({ type: "submit", class: "filter-btn" }, i18n.voteCommentsForumButton) - ) - ), - div( - { class: "card-comments-summary feed-opinions-section" }, - div( - { class: "comments-count" }, - span({ class: "card-label" }, (i18n.opinionsTitle || "Opinions") + ": "), - span({ class: "card-value" }, String(Object.values(videoObj.opinions || {}).reduce((s, n) => s + (Number(n) || 0), 0))) - ), - renderOpinionsVoting('/videos/opinions', videoObj.key, videoObj.opinions, returnTo, videoObj.opinions_inhabitants) - ), - div({ class: "card-spread-left" }, renderSpreadButton(videoObj.key, (params.spreadMap && params.spreadMap.get(videoObj.key)) || params.spreads)), + ...(typeof params.bodyExtra === "function" ? params.bodyExtra(videoObj) : []), + engagement, renderMapLocationVisitLabel(videoObj.mapUrl), br(), (() => { @@ -244,12 +134,12 @@ const renderVideoList = exports.renderVideoList = (videos, filter, params = {}) return p( { class: "card-footer" }, - span({ class: "date-link" }, `${moment(videoObj.createdAt).format("YYYY/MM/DD HH:mm:ss")} ${i18n.performed} `), + span({ class: "date-link" }, `${moment(videoObj.createdAt).format("YYYY/MM/DD HH:mm")}`), userLink(videoObj.author), showUpdated ? span( { class: "votations-comment-date" }, - ` | ${i18n.videoUpdatedAt}: ${moment(videoObj.updatedAt).format("YYYY/MM/DD HH:mm:ss")}` + ` | ${i18n.videoUpdatedAt}: ${moment(videoObj.updatedAt).format("YYYY/MM/DD HH:mm")}` ) : null ); @@ -265,6 +155,7 @@ const renderVideoForm = (filter, videoId, videoToEdit, params = {}) => { return div( { class: "div-center video-form" }, + params.spreadWarning || null, form( { action: filter === "edit" ? `/videos/update/${encodeURIComponent(videoId)}` : "/videos/create", @@ -279,11 +170,11 @@ const renderVideoForm = (filter, videoId, videoToEdit, params = {}) => { br(), span(i18n.videoTitleLabel), br(), - input({ type: "text", name: "title", placeholder: i18n.videoTitlePlaceholder, value: videoToEdit?.title || "" }), + input({ type: "text", name: "title", maxlength: "100", placeholder: i18n.videoTitlePlaceholder, value: videoToEdit?.title || "" }), br(), span(i18n.videoDescriptionLabel), br(), - textarea({ name: "description", placeholder: i18n.videoDescriptionPlaceholder, rows: "4" }, videoToEdit?.description || ""), + textarea({ maxlength: "5000", name: "description", placeholder: i18n.videoDescriptionPlaceholder, rows: "4" }, videoToEdit?.description || ""), br(), span(i18n.mapLocationTitle || "Map Location"), br(), @@ -298,61 +189,61 @@ const renderVideoForm = (filter, videoId, videoToEdit, params = {}) => { value: safeArr(videoToEdit?.tags).join(", ") }), br(), + ...renderLicenseSelect(videoToEdit?.license, i18n), + br(), + renderClearnetSelector(filter === "edit" ? !!videoToEdit?.clearnet : false, i18n), br(), button({ type: "submit" }, filter === "edit" ? i18n.videoUpdateButton : i18n.videoCreateButton) ) ); }; +const mediaChipFor = (filter, censusM) => (mode) => { + if (mode === filter) return true; + if (!Array.isArray(censusM)) return true; + if (mode === "top") return censusM.some((x) => !x.tribeOrigin); + if (mode === "mine") return censusM.some((x) => String(x.author) === String(userId)); + if (mode === "recent") return censusM.length > 0; + if (mode === "favorites") return censusM.some((x) => x.isFavorite); + if (mode === "bcs") return censusM.some((x) => String(x.title || "").toUpperCase().startsWith("BCS-")); + return true; +}; + exports.videoView = async (videos, filter = "all", videoId = null, params = {}) => { - const title = - filter === "mine" - ? i18n.videoMineSectionTitle - : filter === "create" - ? i18n.videoCreateSectionTitle - : filter === "edit" - ? i18n.videoUpdateSectionTitle - : filter === "recent" - ? i18n.videoRecentSectionTitle - : filter === "top" - ? i18n.videoTopSectionTitle - : filter === "favorites" - ? i18n.videoFavoritesSectionTitle - : i18n.videoAllSectionTitle; + if (filter === "edit") params = { ...params, spreadWarning: await renderSpreadEditWarning(videoId) }; + const title = i18n.videoTitle; const q = safeText(params.q || ""); const sort = safeText(params.sort || "recent"); const list = safeArr(videos); + const emptyMod = moduleIsEmpty(list, filter, "all", q); + const mediaChip = mediaChipFor(filter, Array.isArray(params.censusList) ? params.censusList : list); const videoToEdit = videoId ? list.find((v) => v.key === videoId) : null; return template( title, section( - div({ class: "tags-header" }, + div({ class: "tags-header module-header-line" }, h2(title), p(i18n.videoDescription) ), - (() => { - const { renderReachChip } = require('./clearnet_view'); - const isClearnet = !!(params.viewerPrefs && params.viewerPrefs.clearnetVideos); - return div({ class: "shop-title-row" }, renderReachChip(isClearnet, i18n)); - })(), - br(), div( { class: "filters" }, form( { method: "GET", action: "/videos", class: "ui-toolbar ui-toolbar--filters" }, - input({ type: "hidden", name: "q", value: q }), input({ type: "hidden", name: "sort", value: sort }), - button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, i18n.videoFilterAll), - button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, i18n.videoFilterMine), - button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, i18n.videoFilterRecent), - button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, i18n.videoFilterTop), - button( + ...(emptyMod ? [] : [ + button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.videoFilterAll).toUpperCase()), + ...(mediaChip("recent") ? [button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.videoFilterRecent).toUpperCase())] : []), + ...(mediaChip("mine") ? [button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.videoFilterMine).toUpperCase())] : []), + ...(mediaChip("favorites") ? [button( { type: "submit", name: "filter", value: "favorites", class: filter === "favorites" ? "filter-btn active" : "filter-btn" }, - i18n.videoFilterFavorites - ), + String(i18n.videoFilterFavorites).toUpperCase() + )] : []), + + ...(mediaChip("top") ? [button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, String(i18n.videoFilterTop).toUpperCase())] : []), + ]), button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.videoCreateButton) ) ) @@ -361,8 +252,9 @@ exports.videoView = async (videos, filter = "all", videoId = null, params = {}) filter === "create" || filter === "edit" ? renderVideoForm(filter, videoId, videoToEdit, { ...params, filter }) : section( - div( - { class: "videos-search" }, + emptyMod ? null : div( + { class: "videos-search activity-filter-chips activity-toolbar-row" }, + renderModuleStats(list.length), form( { method: "GET", action: "/videos", class: "filter-box" }, input({ type: "hidden", name: "filter", value: filter }), @@ -377,28 +269,29 @@ exports.videoView = async (videos, filter = "all", videoId = null, params = {}) { class: "filter-box__controls" }, select( { name: "sort", class: "filter-box__select" }, - option({ value: "recent", selected: sort === "recent" }, i18n.videoSortRecent), - option({ value: "oldest", selected: sort === "oldest" }, i18n.videoSortOldest), - option({ value: "top", selected: sort === "top" }, i18n.videoSortTop) + option({ value: "recent", ...(sort === "recent" ? { selected: true } : {})}, i18n.videoSortRecent), + option({ value: "oldest", ...(sort === "oldest" ? { selected: true } : {})}, i18n.videoSortOldest), + option({ value: "top", ...(sort === "top" ? { selected: true } : {})}, i18n.videoSortTop) ), button({ type: "submit", class: "filter-box__button" }, i18n.videoSearchButton) ) ) ), - div({ class: "videos-list" }, renderVideoList(list, filter, { q, sort })) + div({ class: "videos-list" }, renderVideoList(paged(list), filter, { q, sort, spreadMap: params.spreadMap })) ) ) ); }; exports.singleVideoView = async (videoObj, filter = "all", comments = [], params = {}) => { + const mediaChip = mediaChipFor(filter, Array.isArray(params.censusList) ? params.censusList : null); const q = safeText(params.q || ""); const sort = safeText(params.sort || "recent"); const returnTo = safeText(params.returnTo) || buildReturnTo(filter, { q, sort }); const title = safeText(videoObj.title); - const { renderReachChip } = require('./clearnet_view'); - const isClearnet = !!(params.authorPrefs && params.authorPrefs.clearnetVideos); + const isAuthor = String(videoObj.author) === String(userId); + const isClearnet = !!videoObj.clearnet; const chips = [ renderLifespanChip(videoObj.lifetime, i18n), @@ -407,30 +300,40 @@ exports.singleVideoView = async (videoObj, filter = "all", comments = [], params const ownerActions = renderVideoOwnerActions(filter, videoObj, { q, sort }); const sideActions = []; - sideActions.push(renderVideoFavoriteToggle(videoObj, returnTo)); - if (videoObj.author && String(videoObj.author) !== String(userId)) { - sideActions.push(renderPMButton(videoObj.author)); - } for (const a of ownerActions) sideActions.push(a); const tagsNode = renderTags(videoObj.tags); + const detailActions = div({ class: "card-header activity-card-header" }, + renderContentActions(videoObj.key, `/videos/${encodeURIComponent(videoObj.key)}`, { + author: videoObj.author, + favKind: 'videos', torrentFrom: { blobId: videoObj.url, name: videoObj.title }, + isFavorite: videoObj.isFavorite, + spread: params.spreads || null, + returnTo, + reportTitle: videoObj.title, + deleteAction: String(videoObj.author) === String(userId) ? contentDeleteAction('video', videoObj.key) : undefined + }) + ); + const videoSide = div({ class: "tribe-side" }, div({ class: "shop-title-row" }, title ? h2({ class: "tribe-card-title" }, title) : null, - renderReachChip(isClearnet, i18n) + renderReachChip(isClearnet, i18n, isClearnet ? clearnetItemHref('videos', videoObj.title, videoObj.key) : null), + isAuthor ? renderClearnetSwitch('videos', videoObj.rootId || videoObj.key, isClearnet) : null, + renderLicenseChip(videoObj.license) ), chips.length ? div({ class: "card-chips-row" }, ...chips) : null, safeText(videoObj.description) - ? p({ class: "tribe-side-description" }, ...renderUrl(videoObj.description)) + ? p({ class: "tribe-side-description" }, ...renderStyledText(videoObj.description)) : null, tagsNode, - div({ class: "card-spread-centered" }, renderSpreadButton(videoObj.key, params.spreads)), - renderMapLocationVisitLabel(videoObj.mapUrl), + renderMapEmbed(params.mapData, videoObj.mapUrl), sideActions.length ? div({ class: "tribe-side-actions" }, ...sideActions) : null ); const videoMain = div({ class: "tribe-main" }, + detailActions, renderVideoPlayer(videoObj), (() => { const createdTs = videoObj.createdAt ? new Date(videoObj.createdAt).getTime() : NaN; @@ -439,24 +342,26 @@ exports.singleVideoView = async (videoObj, filter = "all", comments = [], params return p( { class: "card-footer" }, - span({ class: "date-link" }, `${moment(videoObj.createdAt).format("YYYY/MM/DD HH:mm:ss")} ${i18n.performed} `), + span({ class: "date-link" }, `${moment(videoObj.createdAt).format("YYYY/MM/DD HH:mm")}`), userLink(videoObj.author), showUpdated ? span( { class: "votations-comment-date" }, - ` | ${i18n.videoUpdatedAt}: ${moment(videoObj.updatedAt).format("YYYY/MM/DD HH:mm:ss")}` + ` | ${i18n.videoUpdatedAt}: ${moment(videoObj.updatedAt).format("YYYY/MM/DD HH:mm")}` ) : null ); })(), - renderOpinionsVoting('/videos/opinions', videoObj.key, videoObj.opinions, returnTo, videoObj.opinions_inhabitants), - renderVideoCommentsSection(videoObj.key, comments, returnTo) + renderEngagement(videoObj.key, + renderOpinionsVoting('/videos/opinions', videoObj.key, videoObj.opinions, returnTo, videoObj.opinions_inhabitants), + renderVideoCommentsSection(videoObj.key, comments, returnTo) + ) ); return template( i18n.videoTitle, section( - div({ class: "tags-header" }, + div({ class: "tags-header module-header-line" }, h2(i18n.videoAllSectionTitle || i18n.videoTitle), p(i18n.videoDescription) ), @@ -464,16 +369,15 @@ exports.singleVideoView = async (videoObj, filter = "all", comments = [], params { class: "filters" }, form( { method: "GET", action: "/videos", class: "ui-toolbar ui-toolbar--filters" }, - input({ type: "hidden", name: "q", value: q }), input({ type: "hidden", name: "sort", value: sort }), - button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, i18n.videoFilterAll), - button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, i18n.videoFilterMine), - button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, i18n.videoFilterRecent), - button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, i18n.videoFilterTop), - button( + button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.videoFilterAll).toUpperCase()), ...(mediaChip("favorites") ? [button( + ...(mediaChip("recent") ? [button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.videoFilterRecent).toUpperCase())] : []), + ...(mediaChip("mine") ? [button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.videoFilterMine).toUpperCase())] : []), { type: "submit", name: "filter", value: "favorites", class: filter === "favorites" ? "filter-btn active" : "filter-btn" }, - i18n.videoFilterFavorites - ), + String(i18n.videoFilterFavorites).toUpperCase() + )] : []), + + button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, String(i18n.videoFilterTop).toUpperCase()), button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.videoCreateButton) ) ), diff --git a/src/views/vote_view.js b/src/views/vote_view.js index 557c64f..003882a 100644 --- a/src/views/vote_view.js +++ b/src/views/vote_view.js @@ -1,8 +1,10 @@ const { div, h2, p, section, button, form, a, textarea, br, input, table, tr, th, td, label, span } = require("../server/node_modules/hyperaxe"); -const { template, i18n, renderOpinionsVoting, userLink, renderOpenClosedChip, renderLifespanChip, renderEcoTax, renderSpreadButton, renderContentActions } = require("./main_views"); +const { renderCommentsSection: renderSharedCommentsSection } = require("./comments_view"); +const { template, i18n, renderOpinionsVoting, renderEngagement, renderOpenClosedChip, renderLifespanChip, renderEcoTax, renderSpreadButton, renderContentActions, renderSpreadEditWarning, renderDocumentActions, renderModuleStatsBy, moduleIsEmpty, contentDeleteAction, paged, renderCreatedRows } = require("./main_views"); const moment = require("../server/node_modules/moment"); const { config } = require("../server/SSB_server.js"); -const { renderUrl } = require("../backend/renderUrl"); +const { renderTribeOriginChip } = require("./clearnet_view"); +const mobileUi = require("./mobile_ui"); const userId = config.keys.id; const VOTE_QUORUM = 2; @@ -13,15 +15,15 @@ const voteLabel = (opt) => i18n["vote" + opt.split("_").map(w => w.charAt(0) + w.slice(1).toLowerCase()).join("")] || opt; const computeVoteOutcome = (baseCounts, voteOptions, totalVotesNum) => { - const noResult = { text: i18n.voteNoQuorum || "NO QUORUM", color: "#ffcc00" }; + const noResult = { text: i18n.voteNoQuorum || "NO QUORUM", variant: "pending" }; if (totalVotesNum < VOTE_QUORUM) return noResult; const opts = voteOptions.filter((o) => o !== "FOLLOW_MAJORITY"); const maxCount = opts.reduce((m, o) => Math.max(m, baseCounts[o] || 0), 0); const topOpts = opts.filter((o) => (baseCounts[o] || 0) === maxCount); if (maxCount === 0 || topOpts.length > 1) return noResult; const winner = topOpts[0]; - const color = winner === "YES" ? "#4caf50" : winner === "NO" ? "#e53935" : "#ffcc00"; - return { text: voteLabel(winner), color }; + const variant = (winner === "YES" || winner === "GREEN") ? "yes" : (winner === "NO" || winner === "RED") ? "no" : "pending"; + return { text: voteLabel(winner), variant }; }; const normalizeStatus = (v) => { @@ -37,9 +39,12 @@ const statusLabel = (s) => { return up; }; -const renderVoteOwnerActions = (v, returnTo, mode) => { - const showUpdateButton = mode === "mine" && !Object.keys(v.opinions || {}).length; - const showDeleteButton = mode === "mine"; +const { isVoteLocked } = require("../models/votes_model"); +const isVoteAuthor = (v) => !!(v && v.createdBy && String(v.createdBy) === String(userId)); +const canModifyVote = (v) => isVoteAuthor(v) && !isVoteLocked(v); + +const renderVoteOwnerActions = (v, returnTo) => { + const showUpdateButton = canModifyVote(v); const actions = []; if (showUpdateButton) { @@ -51,15 +56,6 @@ const renderVoteOwnerActions = (v, returnTo, mode) => { ) ); } - if (showDeleteButton) { - actions.push( - form( - { method: "POST", action: `/votes/delete/${encodeURIComponent(v.id)}` }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - button({ class: "delete-btn", type: "submit" }, i18n.voteDeleteButton) - ) - ); - } return actions; }; @@ -91,7 +87,7 @@ const renderVoteStatusChip = (status) => { return renderOpenClosedChip(status, { statusChipOPEN: localized, statusChipCLOSED: localized }); }; -const renderVoteListItem = (v, voteOptionsDefault, activeFilter, spreadInfo) => { +const renderVoteListItem = exports.renderVoteListItem = (v, voteOptionsDefault, activeFilter, spreadInfo, opts = {}) => { const voteOptions = Array.isArray(v.options) && v.options.length ? v.options : voteOptionsDefault; const baseCounts = voteOptions.reduce((acc, opt) => { acc[opt] = (v.votes && v.votes[opt]) ? v.votes[opt] : 0; @@ -99,36 +95,69 @@ const renderVoteListItem = (v, voteOptionsDefault, activeFilter, spreadInfo) => }, {}); const totalVotesNum = typeof v.totalVotes === "number" ? v.totalVotes : parseInt(String(v.totalVotes || "0"), 10) || 0; const outcome = computeVoteOutcome(baseCounts, voteOptions, totalVotesNum); + const origin = v.tribeOrigin || null; + const href = opts.href !== undefined ? opts.href : (origin ? origin.href : `/votes/${encodeURIComponent(v.id)}`); const chips = [ renderVoteStatusChip(v.status), - renderLifespanChip(v.lifetime, i18n) + renderLifespanChip(v.lifetime, i18n), + origin ? renderTribeOriginChip(origin) : null, + ...(Array.isArray(opts.extraChips) ? opts.extraChips : []) ].filter(Boolean); const returnTo = `/votes?filter=${encodeURIComponent(activeFilter || "all")}`; const totalOpinions = Object.values(v.opinions || {}).reduce((s, n) => s + (Number(n) || 0), 0); - const isOwn = v.createdBy && String(v.createdBy) === String(userId); + const isOwn = isVoteAuthor(v); + const headerActions = opts.headerActions !== undefined + ? opts.headerActions + : origin ? null : renderContentActions(v.id, `/votes/${encodeURIComponent(v.id)}`, { spread: spreadInfo || null, author: v.createdBy, favKind: 'votes', isFavorite: v.isFavorite, reportTitle: v.question, returnTo, deleteAction: canModifyVote(v) ? contentDeleteAction('votes', v.id) : undefined }); return div({ class: "trending-card vote-card" + (isOwn ? " own-content" : "") }, - div({ class: "card-header activity-card-header" }, + headerActions ? div({ class: "card-header activity-card-header" }, span(), - renderContentActions(v.id, `/votes/${encodeURIComponent(v.id)}`) - ), + headerActions + ) : null, div({ class: "card-section vote-card-body" }, div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, - a({ href: `/votes/${encodeURIComponent(v.id)}` }, v.question || i18n.votationsTitle) + href ? a({ href }, v.question || i18n.votationsTitle) : (v.question || i18n.votationsTitle) ) ), chips.length ? div({ class: "card-chips-row" }, ...chips) : null, - v.deadline ? p({ class: "card-date-highlight" }, moment(v.deadline).format("YYYY/MM/DD HH:mm")) : null, - div({ class: "tribe-card-members" }, + v.deadline ? p({ class: "time-chip" }, moment(v.deadline).format("YYYY/MM/DD HH:mm")) : null, + origin ? null : div({ class: "tribe-card-members" }, span({ class: "tribe-members-count" }, `${i18n.eventAttendees}: ${totalVotesNum}`) ), - div({ class: "job-meta-line", style: `color:${outcome.color};font-weight:bold;` }, `${i18n.voteResults || "Results"}: ${outcome.text}`), - div({ class: "card-spread-centered" }, renderSpreadButton(v.id, spreadInfo)) + origin ? null : div({ class: `job-meta-line vote-outcome vote-outcome-${outcome.variant}` }, `${i18n.voteResults || "Results"}: ${outcome.text}`), + ...(Array.isArray(opts.bodyExtra) ? opts.bodyExtra.filter(Boolean) : []) ) ); }; +exports.renderVoteOptionsBlock = (v, voteOptions, action, returnTo, canVote) => { + const baseCounts = voteOptions.reduce((acc, opt) => { acc[opt] = (v.votes && v.votes[opt]) ? v.votes[opt] : 0; return acc; }, {}); + return [ + canVote ? div({ class: "job-section" }, + h2({ class: "job-section-title" }, i18n.voteCastTitle || "Cast Vote"), + div({ class: "vote-buttons-block" }, + div({ class: "vote-buttons-row-single" }, + ...voteOptions.map((opt, idx) => + form({ method: "POST", action }, + returnTo ? input({ type: "hidden", name: "returnTo", value: returnTo }) : null, + input({ type: "hidden", name: "optionIndex", value: String(idx) }), + button({ type: "submit", name: "choice", value: opt }, voteLabel(opt)) + ) + ) + ) + ) + ) : null, + div({ class: "vote-table" }, + table( + tr(...voteOptions.map((opt) => th(voteLabel(opt)))), + tr(...voteOptions.map((opt) => td(String(baseCounts[opt] || 0)))) + ) + ) + ]; +}; + const renderVoteDetail = (v, voteOptionsDefault, firstRow, secondRow, mode, activeFilter, params = {}) => { const voteOptions = Array.isArray(v.options) && v.options.length ? v.options : voteOptionsDefault; const baseCounts = voteOptions.reduce((acc, opt) => { @@ -147,13 +176,7 @@ const renderVoteDetail = (v, voteOptionsDefault, firstRow, secondRow, mode, acti ].filter(Boolean); const sideActions = []; - if (v.createdBy && v.createdBy !== userId) { - sideActions.push(form({ method: "GET", action: "/pm" }, - input({ type: "hidden", name: "recipients", value: v.createdBy }), - button({ type: "submit", class: "filter-btn" }, i18n.privateMessage) - )); - } - for (const a of renderVoteOwnerActions(v, returnTo, mode || "")) sideActions.push(a); + for (const a of renderVoteOwnerActions(v, returnTo)) sideActions.push(a); const cleanTags = (Array.isArray(v.tags) ? v.tags : []).filter((t) => t && !String(t).includes(":")); const tagsNode = cleanTags.length @@ -172,22 +195,26 @@ const renderVoteDetail = (v, voteOptionsDefault, firstRow, secondRow, mode, acti pushRow(i18n.voteStatus, statusLabel(v.status)); const voteSide = div({ class: "tribe-side" }, + div({ class: "card-header activity-card-header" }, + renderContentActions(v.id, `/votes/${encodeURIComponent(v.id)}`, { spread: params.spreads || null, author: v.createdBy, favKind: 'votes', isFavorite: v.isFavorite, reportTitle: v.question, deleteAction: canModifyVote(v) ? contentDeleteAction('votes', v.id) : undefined }) + ), div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, v.question) ), chips.length ? div({ class: "card-chips-row" }, ...chips) : null, - div({ class: "card-spread-centered" }, renderSpreadButton(v.id, params.spreads)), - table({ class: "tribe-info-table jobs-info-table" }, ...infoRows), + table({ class: "tribe-info-table jobs-info-table" }, ...infoRows, ...renderCreatedRows(v.createdAt, v.createdBy)), tagsNode, div({ class: "tribe-card-members" }, span({ class: "tribe-members-count" }, `${i18n.eventAttendees}: ${totalVotesNum}`) ), - sideActions.length ? div({ class: "tribe-side-actions" }, ...sideActions) : null + sideActions.length ? div({ class: "tribe-side-actions" }, ...sideActions) : null, + renderDocumentActions('votes', v.id) ); const voteButtonsNode = renderVoteButtons(v, voteOptions, firstRow, secondRow, returnTo); const voteMain = div({ class: "tribe-main" }, + mobileUi.isMobile() && !voteButtonsNode && isVoteAuthor(v) && normalizeStatus(v.status) === "OPEN" ? mobileUi.voteOwnNote() : null, voteButtonsNode ? div({ class: "job-section" }, h2({ class: "job-section-title" }, i18n.voteCastTitle || "Cast Vote"), @@ -197,84 +224,29 @@ const renderVoteDetail = (v, voteOptionsDefault, firstRow, secondRow, mode, acti div({ class: "job-section" }, h2({ class: "job-section-title" }, `${i18n.voteResults || "Results"}: `, - span({ style: `color:${outcome.color} !important;font-weight:bold;` }, outcome.text) + span({ class: `vote-outcome vote-outcome-${outcome.variant}` }, outcome.text) ), - div({ class: "vote-table" }, + mobileUi.isMobile() ? mobileUi.voteBars(voteOptions, baseCounts, voteLabel) : div({ class: "vote-table" }, table( tr(...voteOptions.map((opt) => th(voteLabel(opt)))), tr(...voteOptions.map((opt) => td(baseCounts[opt]))) ) ) ), - p({ class: "card-footer" }, - span({ class: "date-link" }, `${moment(v.createdAt).format("YYYY/MM/DD HH:mm")} ${i18n.performed} `), - userLink(v.createdBy) - ), - renderOpinionsBar(v, returnTo) + renderEngagement(v.id, renderOpinionsBar(v, returnTo), + mode === "detail" ? renderCommentsSection(v.id, params.comments || [], activeFilter) : null) ); return div({ class: "tribe-details" }, voteSide, voteMain); }; const renderCommentsSection = (voteId, comments, activeFilter) => { - const commentsCount = Array.isArray(comments) ? comments.length : 0; const returnTo = `/votes/${encodeURIComponent(voteId)}?filter=${encodeURIComponent(activeFilter || "all")}`; - - return div( - { class: "vote-comments-section" }, - div( - { class: "comments-count" }, - span({ class: "card-label" }, i18n.voteCommentsLabel + ": "), - span({ class: "card-value" }, String(commentsCount)) - ), - div( - { class: "comment-form-wrapper" }, - h2({ class: "comment-form-title" }, i18n.voteNewCommentLabel), - form( - { method: "POST", action: `/votes/${encodeURIComponent(voteId)}/comments`, class: "comment-form", enctype: "multipart/form-data" }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - textarea({ - id: "comment-text", - name: "text", - rows: 4, - class: "comment-textarea", - placeholder: i18n.voteNewCommentPlaceholder - }), - div({ class: "comment-file-upload" }, label(i18n.uploadMedia), input({ type: "file", name: "blob" })), - br(), - button({ type: "submit", class: "comment-submit-btn" }, i18n.voteNewCommentButton) - ) - ), - comments && comments.length - ? div( - { class: "comments-list" }, - comments.map((c) => { - const author = c.value && c.value.author ? c.value.author : ""; - const ts = c.value && c.value.timestamp ? c.value.timestamp : c.timestamp; - const absDate = ts ? moment(ts).format("YYYY/MM/DD HH:mm:ss") : ""; - const relDate = ts ? moment(ts).fromNow() : ""; - - const content = c.value && c.value.content ? c.value.content : {}; - const root = content.fork || content.root || ""; - const text = content.text || ""; - - return div( - { class: "votations-comment-card" }, - span( - { class: "created-at" }, - span(i18n.createdBy), - author ? userLink(author) : span("(unknown)"), - absDate ? span(" | ") : "", - absDate ? span({ class: "votations-comment-date" }, absDate) : "", - relDate ? span({ class: "votations-comment-date" }, " | ", i18n.sendTime) : "", - relDate && root ? a({ href: `/thread/${encodeURIComponent(root)}#${encodeURIComponent(c.key)}` }, relDate) : "" - ), - p({ class: "votations-comment-text" }, ...renderUrl(String(text))) - ); - }) - ) - : p({ class: "votations-no-comments" }, i18n.voteNoCommentsYet) - ); + return renderSharedCommentsSection({ + action: `/votes/${encodeURIComponent(voteId)}/comments`, + comments: comments, + returnTo: returnTo + }); }; exports.voteView = async (votes, mode, voteId, comments = [], activeFilterParam, params = {}) => { @@ -285,14 +257,7 @@ exports.voteView = async (votes, mode, voteId, comments = [], activeFilterParam, ? activeFilterParam : (standardFilters.includes(mode) ? mode : "all"); - const title = - mode === "mine" ? i18n.voteMineSectionTitle : - mode === "create" ? i18n.voteCreateSectionTitle : - mode === "edit" ? i18n.voteUpdateSectionTitle : - mode === "open" ? i18n.voteOpenTitle : - mode === "closed" ? i18n.voteClosedTitle : - mode === "detail" ? (i18n.voteDetailSectionTitle || i18n.voteAllSectionTitle) : - i18n.voteAllSectionTitle; + const title = i18n.votationsTitle; const voteToEdit = list.find((v) => v.id === voteId) || {}; const editTags = Array.isArray(voteToEdit.tags) ? voteToEdit.tags.filter(Boolean) : []; @@ -310,15 +275,31 @@ exports.voteView = async (votes, mode, voteId, comments = [], activeFilterParam, const secondRow = ["CONFUSED", "FOLLOW_MAJORITY", "NOT_INTERESTED"]; const header = div( - { class: "tags-header" }, + { class: "tags-header module-header-line" }, h2(i18n.votationsTitle), p(i18n.votationsDescription) ); const listReturnTo = standardFilters.includes(activeFilter) ? `/votes?filter=${encodeURIComponent(activeFilter)}` : "/votes"; + const emptyMod = moduleIsEmpty(filtered, mode, "all", (params && params.q) || ""); + const censusV = Array.isArray(params && params.censusList) ? params.censusList : list; + const voteChip = (m) => { + if (m === mode) return true; + if (m === "mine") return censusV.some((v) => v.createdBy === userId); + if (m === "recent") return censusV.length > 0; + if (m === "open") return censusV.some((v) => normalizeStatus(v.status) === "OPEN"); + if (m === "closed") return censusV.some((v) => normalizeStatus(v.status) === "CLOSED"); + return true; + }; - const deadlineMin = moment().add(1, "minute").format("YYYY-MM-DDTHH:mm"); - const deadlineValue = voteToEdit.deadline ? moment(voteToEdit.deadline).format("YYYY-MM-DDTHH:mm") : ""; + const minVoteDays = Number(params.minVoteDays) > 0 ? Number(params.minVoteDays) : 7; + const deadlineMin = moment().add(minVoteDays, "days").format("YYYY-MM-DDTHH:mm"); + const draft = params.draft || {}; + const deadlineValue = draft.deadline + ? String(draft.deadline) + : (voteToEdit.deadline ? moment(voteToEdit.deadline).format("YYYY-MM-DDTHH:mm") : ""); + const questionValue = draft.question !== undefined && draft.question !== "" ? String(draft.question) : (voteToEdit.question || ""); + const tagsValue = draft.tags ? String(draft.tags) : editTags.join(", "); return template( title, @@ -328,43 +309,59 @@ exports.voteView = async (votes, mode, voteId, comments = [], activeFilterParam, { class: "filters" }, form( { method: "GET", action: "/votes" }, - button({ type: "submit", name: "filter", value: "all", class: mode === "all" ? "filter-btn active" : "filter-btn" }, i18n.voteFilterAll), - button({ type: "submit", name: "filter", value: "mine", class: mode === "mine" ? "filter-btn active" : "filter-btn" }, i18n.voteFilterMine), - button({ type: "submit", name: "filter", value: "open", class: mode === "open" ? "filter-btn active" : "filter-btn" }, i18n.voteFilterOpen), - button({ type: "submit", name: "filter", value: "closed", class: mode === "closed" ? "filter-btn active" : "filter-btn" }, i18n.voteFilterClosed), + ...(emptyMod ? [] : [ + button({ type: "submit", name: "filter", value: "all", class: mode === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.voteFilterAll).toUpperCase()), + ...(voteChip("recent") ? [button({ type: "submit", name: "filter", value: "recent", class: mode === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.voteFilterRecent).toUpperCase())] : []), + ...(voteChip("mine") ? [button({ type: "submit", name: "filter", value: "mine", class: mode === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.voteFilterMine).toUpperCase())] : []), + ...(voteChip("open") ? [button({ type: "submit", name: "filter", value: "open", class: mode === "open" ? "filter-btn active" : "filter-btn" }, String(i18n.voteFilterOpen).toUpperCase())] : []), + ...(voteChip("closed") ? [button({ type: "submit", name: "filter", value: "closed", class: mode === "closed" ? "filter-btn active" : "filter-btn" }, String(i18n.voteFilterClosed).toUpperCase())] : []), + ]), button({ type: "submit", name: "filter", value: "create", class: mode === "create" ? "create-button active" : "create-button" }, i18n.voteCreateButton) ) - ) + ), + mode !== "create" && mode !== "edit" && !emptyMod + ? div({ class: "filters activity-filter-chips activity-toolbar-row" }, + renderModuleStatsBy(filtered, v => normalizeStatus(v.status || 'OPEN'), [{ value: 'OPEN', label: i18n.voteStatusOpen }, { value: 'CLOSED', label: i18n.voteStatusClosed }]), + form({ method: "GET", action: "/votes", class: "filter-box" }, + input({ type: "hidden", name: "filter", value: mode }), + input({ type: "text", name: "q", value: (params && params.q) || "", placeholder: i18n.votesSearchPlaceholder, class: "filter-box__input" }), + div({ class: "filter-box__controls" }, + button({ type: "submit", class: "filter-box__button" }, i18n.searchButton) + ) + ) + ) + : null ), section( (mode === "edit" || mode === "create") ? div( { class: "vote-form" }, + mode === "edit" ? await renderSpreadEditWarning(voteId) : null, form( { action: mode === "edit" ? `/votes/update/${encodeURIComponent(voteId)}` : "/votes/create", method: "POST" }, input({ type: "hidden", name: "returnTo", value: listReturnTo }), h2(i18n.voteQuestionLabel), - input({ type: "text", name: "question", id: "question", required: true, value: voteToEdit.question || "" }), br(), br(), + input({ type: "text", name: "question", id: "question", required: true, value: questionValue }), br(), br(), label(i18n.voteDeadlineLabel), br(), input({ type: "datetime-local", name: "deadline", id: "deadline", required: true, - min: mode === "create" ? deadlineMin : undefined, + min: deadlineMin, value: deadlineValue }), br(), br(), label(i18n.voteTagsLabel), br(), - input({ type: "text", name: "tags", id: "tags", value: editTags.join(", ") }), br(), br(), + input({ type: "text", name: "tags", id: "tags", value: tagsValue }), br(), br(), + mobileUi.isMobile() && mode === "create" ? mobileUi.voteKindPicker(draft.options) : null, button({ type: "submit" }, mode === "edit" ? i18n.voteUpdateButton : i18n.voteCreateButton) ) ) : mode === "detail" && voteId - ? renderVoteDetail(filtered[0] || list.find(v => v.id === voteId) || {}, voteOptions, firstRow, secondRow, mode, activeFilter, params) + ? renderVoteDetail(filtered[0] || list.find(v => v.id === voteId) || {}, voteOptions, firstRow, secondRow, mode, activeFilter, { ...params, comments }) : filtered.length > 0 - ? div({ class: "jobs-grid" }, filtered.map((v) => renderVoteListItem(v, voteOptions, activeFilter, params.spreadMap && params.spreadMap.get(v.id)))) - : p(i18n.novotes), - (mode === "detail" && voteId) ? renderCommentsSection(voteId, comments, activeFilter) : null + ? div({ class: "jobs-grid" }, paged(filtered).map((v) => renderVoteListItem(v, voteOptions, activeFilter, params.spreadMap && params.spreadMap.get(v.id)))) + : div({ class: "no-content-box" }, p(i18n.novotes)), ) ); }; diff --git a/src/views/wallet_view.js b/src/views/wallet_view.js index ee8ca73..6c99485 100644 --- a/src/views/wallet_view.js +++ b/src/views/wallet_view.js @@ -1,67 +1,86 @@ const { form, button, div, h2, p, section, input, span, table, thead, tbody, tr, td, th, ul, li, a, br, label, img } = require("../server/node_modules/hyperaxe"); -const QRCode = require('../server/node_modules/qrcode'); -const { template, i18n } = require('./main_views'); +const moment = require("../server/node_modules/moment"); +const { template, i18n, userLink, renderWalletChip, renderStateChip, paged } = require('./main_views'); -const walletViewRender = (balance, address, ...elements) => { - const header = div({ class: 'tags-header' }, h2(i18n.walletTitle), p(i18n.walletDescription)); +const TX_CATEGORY_CHIPS = { + receive: ["mutuals", "\u2B07"], + send: ["whole", "\u2B06"], + generate: ["mutuals", "\u2713"], + stake: ["mutuals", "\u2713"], + immature: ["lifespan", "\u23F3"], + orphan: ["closed", "\u2717"] +}; +const renderTxCategory = (category) => { + const key = String(category || "").toLowerCase(); + const [variant, icon] = TX_CATEGORY_CHIPS[key] || ["encrypted", ""]; + return renderStateChip(variant, icon, (key || "-").toUpperCase()); +}; + +const WALLET_MODES = [ + { key: "send", href: "/wallet/send", label: () => i18n.walletSend }, + { key: "receive", href: "/wallet/receive", label: () => i18n.walletReceive }, + { key: "history", href: "/wallet/history", label: () => i18n.walletHistory } +]; + +const walletModeButtons = (current) => + div({ class: "mode-buttons-row" }, + ...WALLET_MODES.map(mode => + form({ method: "GET", action: mode.href }, + button({ type: "submit", class: current === mode.key ? "filter-btn active" : "filter-btn" }, String(mode.label()).toUpperCase()) + ) + ) + ); + +const walletViewRender = (balance, address, current, ...elements) => { + const header = div({ class: 'tags-header module-header-line' }, h2(i18n.walletTitle), p(i18n.walletDescription), renderWalletChip()); return template( i18n.walletTitle, section( header, + walletModeButtons(current), div({ class: "wallet-section" }, h2(i18n.walletAddress), div({ class: "wallet-address" }, h2({ class: "element" }, address || "-")), h2(i18n.walletBalanceTitle), - div({ class: "div-center" }, h2(i18n.walletBalanceLine({ balance: Number(balance || 0).toFixed(6) }))), - div({ class: "div-center" }, - span({ class: "wallet-form-button-group-center" }, - form({ action: "/wallet/send", method: "get" }, button({ type: 'submit' }, i18n.walletSend)), - form({ action: "/wallet/receive", method: "get" }, button({ type: 'submit' }, i18n.walletReceive)), - form({ action: "/wallet/history", method: "get" }, button({ type: 'submit' }, i18n.walletHistory)) - ) - ) + div({ class: "div-center" }, h2(i18n.walletBalanceLine({ balance: Number(balance || 0).toFixed(6) }))) ) ), - elements.length > 0 ? section(...elements) : null + elements.length > 0 ? section({ id: `wallet-${current || 'main'}` }, ...elements) : null ); }; exports.walletView = async (balance, address) => { - return walletViewRender(balance, address); + return walletViewRender(balance, address, null); }; exports.walletHistoryView = async (balance, transactions, address) => { - const rows = Array.isArray(transactions) ? transactions : []; + const rows = (Array.isArray(transactions) ? transactions.slice() : []).sort((a, b) => ((b.time || b.timereceived || 0) - (a.time || a.timereceived || 0))); return walletViewRender( balance, address, - h2(i18n.walletHistoryTitle), + "history", table( { class: "wallet-history" }, thead( tr( { class: "full-center" }, - th({ class: "col-10" }, i18n.walletCnfrs), - th(i18n.walletDate), + th({ class: "wallet-col-confirmations" }, i18n.walletCnfrs), + th({ class: "wallet-col-date" }, i18n.walletDate), th(i18n.walletType), - th(i18n.walletAmount), - th({ class: "col-30" }, i18n.walletTxId) + th(i18n.walletAmount) ) ), tbody( - ...rows.map(tx => { + ...paged(rows).map(tx => { const date = new Date((tx.time || tx.timereceived || 0) * 1000); const amount = Number(tx.amount) || 0; const fee = Number(tx.fee) || 0; const totalAmount = amount + fee; return tr( td({ class: "full-center" }, String(tx.confirmations || 0)), - td(date.toLocaleDateString(), br(), date.toLocaleTimeString()), - td(tx.category || "-"), - td(totalAmount.toFixed(6)), - td({ width: "30%", class: "tcell-ellipsis" }, - a({ href: `https://ecoin.03c8.net/blockexplorer/search?q=${encodeURIComponent(tx.txid || '')}`, target: "_blank" }, tx.txid || "-") - ) + td({ class: "wallet-col-date" }, moment(date).format("YYYY/MM/DD HH:mm")), + td(renderTxCategory(tx.category)), + td(totalAmount.toFixed(6)) ); }) ) @@ -70,42 +89,63 @@ exports.walletHistoryView = async (balance, transactions, address) => { }; exports.walletReceiveView = async (balance, address) => { - const qrDataUrl = await QRCode.toDataURL(address || '', { type: 'image/png', width: 320, margin: 1 }); return walletViewRender( balance, address, - h2(i18n.walletReceiveTitle), - div({ class: 'div-center qr-code' }, img({ src: qrDataUrl, alt: 'QR', class: 'wallet-qr-img' })) + "receive", + address + ? div({ class: 'div-center qr-code' }, img({ src: `/wallet/qr/${encodeURIComponent(address)}`, alt: 'QR', class: 'wallet-qr-img' })) + : null ); }; -exports.walletSendFormView = async (balance, destination, amount, fee, statusMessages, address) => { +exports.walletSendFormView = async (balance, destination, amount, fee, statusMessages, address, options = {}) => { + const transferCtx = options.transfer || null; + const paymentRef = options.payment || null; const type = statusMessages?.type || 'info'; const titleKey = statusMessages?.title || ''; const messages = statusMessages?.messages || []; - const statusBlock = messages.length > 0 - ? div( - { class: `wallet-status-${type}` }, - span(i18n.walletStatusMessages[titleKey] || titleKey || ''), - ul(...messages.map(error => li(i18n.walletStatusMessages[error] || error))) - ) + const statusText = [i18n.walletStatusMessages[titleKey] || titleKey || '', ...messages.map(error => i18n.walletStatusMessages[error] || error)].filter(Boolean).join(': '); + const statusBlock = messages.length > 0 && type !== 'error' + ? div({ class: 'flash-banner wallet-status' }, p(statusText)) : null; return walletViewRender( balance, address, - h2(i18n.walletWalletSendTitle), + "send", div( { class: "div-center" }, statusBlock, form( { action: '/wallet/send', method: 'POST' }, label({ for: 'destination' }, i18n.walletAddress), br(), - input({ type: 'text', id: 'destination', name: 'destination', placeholder: 'ETQ17sBv8QFoiCPGKDQzNcDJeXmB2317HX', value: destination || '' }), br(), + input({ type: 'text', id: 'destination', name: 'destination', placeholder: 'ETQ17sBv8QFoiCPGKDQzNcDJeXmB2317HX', value: destination || '', required: true, pattern: 'E[1-9A-HJ-NP-Za-km-z]{32,34}', maxlength: '35' }), br(), label({ for: 'amount' }, i18n.walletAmount), br(), - input({ type: 'text', id: 'amount', name: 'amount', placeholder: '0.25', value: amount || '' }), br(), + input({ type: 'number', id: 'amount', name: 'amount', placeholder: '0.25', value: amount || '', required: true, min: '0.000001', step: '0.000001', inputmode: 'decimal' }), br(), br(), label({ for: 'fee' }, i18n.walletFee), br(), - input({ type: 'text', id: 'fee', name: 'fee', placeholder: '0.01', value: fee || '' }), br(), + input({ type: 'number', id: 'fee', name: 'fee', placeholder: '5', value: fee || '', required: true, min: '0', step: '0.000001', inputmode: 'decimal' }), br(), br(), + paymentRef + ? div({ class: 'wallet-transfer-ctx' }, + div({ class: 'wallet-transfer-ctx-line' }, span({ class: 'wallet-transfer-ctx-label' }, `${i18n.walletPayeeLabel}: `), userLink(paymentRef.payeeId)), + paymentRef.concept ? div({ class: 'wallet-transfer-ctx-line' }, span({ class: 'wallet-transfer-ctx-label' }, `${i18n.walletConceptLabel}: `), span(paymentRef.concept)) : null, + input({ type: 'hidden', name: 'payeeId', value: paymentRef.payeeId }), + input({ type: 'hidden', name: 'concept', value: paymentRef.concept || '' }), + input({ type: 'hidden', name: 'refHref', value: paymentRef.href || '' }), + input({ type: 'hidden', name: 'refTag', value: paymentRef.tag || 'WALLET' }) + ) + : null, + transferCtx + ? div({ class: 'wallet-transfer-ctx' }, + span({ class: 'wallet-transfer-ctx-label' }, `${i18n.walletPayingTransfer}: `), + a({ href: `/transfers/${encodeURIComponent(transferCtx.id)}` }, transferCtx.concept || transferCtx.id), + input({ type: 'hidden', name: 'transferId', value: transferCtx.id }) + ) + : label({ class: 'wallet-create-transfer' }, + input({ type: 'checkbox', name: 'createTransfer', value: '1', ...(options.createTransfer ? { checked: true } : {}) }), + ` ${i18n.walletCreateTransferLabel}` + ), + br(), input({ type: 'hidden', name: 'action', value: 'confirm' }), div({ class: 'wallet-form-button-group-center' }, button({ type: 'submit' }, i18n.walletSend), @@ -116,25 +156,37 @@ exports.walletSendFormView = async (balance, destination, amount, fee, statusMes ); }; -exports.walletSendConfirmView = async (balance, destination, amount, fee) => { - const a = Number(amount || 0); - const f = Number(fee || 0); - const totalCost = a + f; +exports.walletSendConfirmView = async (balance, destination, amount, fee, options = {}) => { + const amountNum = Number(amount || 0); + const feeNum = Number(fee || 0); + const totalCost = amountNum + feeNum; return walletViewRender( balance, destination, + "send", p( i18n.walletAddressLine({ address: destination || '-' }), br(), - i18n.walletAmountLine({ amount: a.toFixed(6) }), br(), - i18n.walletFeeLine({ fee: f.toFixed(6) }), br(), + i18n.walletAmountLine({ amount: amountNum.toFixed(6) }), br(), + i18n.walletFeeLine({ fee: feeNum.toFixed(6) }), br(), i18n.walletTotalCostLine({ totalCost: totalCost.toFixed(6) }) ), form( { action: '/wallet/send', method: 'POST' }, input({ type: 'hidden', name: 'action', value: 'send' }), input({ type: 'hidden', name: 'destination', value: destination || '' }), - input({ type: 'hidden', name: 'amount', value: String(a) }), - input({ type: 'hidden', name: 'fee', value: String(f) }), + input({ type: 'hidden', name: 'amount', value: String(amountNum) }), + input({ type: 'hidden', name: 'fee', value: String(feeNum) }), + options.transfer ? input({ type: 'hidden', name: 'transferId', value: options.transfer.id }) : null, + options.createTransfer ? input({ type: 'hidden', name: 'createTransfer', value: '1' }) : null, + options.transfer ? p({ class: 'wallet-transfer-ctx' }, `${i18n.walletPayingTransfer}: ${options.transfer.concept || options.transfer.id}`) : null, + options.payment ? input({ type: 'hidden', name: 'payeeId', value: options.payment.payeeId }) : null, + options.payment ? input({ type: 'hidden', name: 'concept', value: options.payment.concept || '' }) : null, + options.payment ? input({ type: 'hidden', name: 'refHref', value: options.payment.href || '' }) : null, + options.payment ? input({ type: 'hidden', name: 'refTag', value: options.payment.tag || 'WALLET' }) : null, + options.payment ? div({ class: 'wallet-transfer-ctx' }, + div({ class: 'wallet-transfer-ctx-line' }, span({ class: 'wallet-transfer-ctx-label' }, `${i18n.walletPayeeLabel}: `), userLink(options.payment.payeeId)), + div({ class: 'wallet-transfer-ctx-line' }, span({ class: 'wallet-transfer-ctx-label' }, `${i18n.walletConceptLabel}: `), options.payment.concept || options.payment.href) + ) : null, div({ class: 'form-button-group-center' }, button({ type: 'submit' }, i18n.walletConfirm), a({ href: `/wallet/send`, class: "button-like-link" }, i18n.walletBack) @@ -143,20 +195,26 @@ exports.walletSendConfirmView = async (balance, destination, amount, fee) => { ); }; -exports.walletSendResultView = async (balance, destination, amount, txId) => { +exports.walletSendResultView = async (balance, destination, amount, txId, note = null) => { return walletViewRender( balance, destination, + "send", p( i18n.walletSentToLine({ destination: destination || '-', amount: Number(amount || 0).toFixed(6) }), br(), `${i18n.walletTransactionId}: `, - a({ href: `https://ecoin.03c8.net/blockexplorer/search?q=${encodeURIComponent(txId || '')}`, target: "_blank" }, txId || '-') - ) + span({ class: "bank-address-code" }, txId || '-') + ), + note ? p({ class: 'wallet-transfer-ctx' }, note) : null ); }; exports.walletErrorView = async (error) => { - const header = div({ class: 'tags-header' }, h2(i18n.walletTitle), p(i18n.walletDescription)); + const header = div({ class: 'tags-header module-header-line' }, h2(i18n.walletTitle), p(i18n.walletDescription), renderWalletChip()); + const w = (require('../configs/config-manager.js').getConfig() || {}).wallet || {}; + if (![w.url, w.user, w.pass].every(v => String(v || '').trim())) { + return template(i18n.walletTitle, section(header, div({ class: "wallet-error" }, h2(i18n.walletStatus), p(i18n.walletNotConfigured, ' ', a({ href: '/settings#wallet' }, i18n.walletConfiguration)), process.env.OASIS_MOBILE === '1' ? div({ class: "wallet-error-actions" }, a({ href: require('../backend/updater.js').ecoinWalletDownloadUrl(), class: "update-download-link" }, i18n.walletDownloadEcoin)) : null))); + } return template( i18n.walletTitle, section( diff --git a/src/views/welcome_view.js b/src/views/welcome_view.js new file mode 100644 index 0000000..7b39341 --- /dev/null +++ b/src/views/welcome_view.js @@ -0,0 +1,151 @@ +const { form, button, div, h2, p, section, a, span, select, option, label, input, textarea, img, details, summary } = require("../server/node_modules/hyperaxe") +const { template, i18n, WIZARD_THEMES } = require("./main_views") +const { renderRestoreStatus } = require("./backup_view") +const { renderSuggestedPub } = require("./invites_view") +const { config } = require("../server/SSB_server.js") +const mobileUi = require("./mobile_ui") +const { WIZARD_WORKFLOWS } = require("../models/workflows_model") + +const FEED_TEXT_MIN = Number(config?.feed?.minLength ?? 1) +const FEED_TEXT_MAX = Number(config?.feed?.maxLength ?? 280) + +const SECTIONS = { + profile: ["welcomeStepIdentity", "welcomeHintIdentity"], + ux: ["welcomeStepType", "welcomeHintType"], + workflow: ["welcomeStepUse", "welcomeHintUse"], + theme: ["welcomeStepLook", "welcomeHintLook"], + community: ["welcomeStepCommunity", "welcomeHintCommunity"], + greeting: ["welcomeStepIntro", "welcomeHintIntro"] +} +if (mobileUi.isMobile()) SECTIONS.workflow = ["welcomeStepUseMobile", "welcomeHintUseMobile"] + +const submitRow = (text) => div({ class: "welcome-action" }, button({ type: "submit" }, text)) + +const pickCard = (name, value, current, title, image) => label({ class: "welcome-ux-option" }, + input({ type: "radio", name, value, ...(value === current ? { checked: true } : {}) }), + img({ src: image, class: "welcome-ux-shot", alt: title }), + span({ class: "welcome-ux-label" }, title) +) + +const stepAction = (key, profile, status) => { + if (key === "profile") return form({ method: "POST", action: "/welcome/profile", enctype: "multipart/form-data", class: "welcome-profile-form" }, + label(i18n.profileName), + input({ type: "text", name: "name", maxlength: "80", value: profile.name || "", placeholder: profile.name || String(profile.id || "").slice(1, 9) }), + label(i18n.profileImage), + profile.image ? img({ loading: "lazy", class: "welcome-profile-avatar", src: `/image/256/${encodeURIComponent(profile.image)}` }) : null, + input({ type: "file", name: "image", accept: "image/*" }), + submitRow(i18n.welcomeStepProfileAction) + ) + if (key === "ux") { + let cfg = {} + try { cfg = require("../configs/config-manager.js").getConfig() || {} } catch (_) {} + const cur = ["ainav", "chats", "feed", "phone", "rooms"].includes(cfg.ux?.current) ? cfg.ux.current : "blocks" + if (mobileUi.isMobile()) return mobileUi.uxPick("/welcome/ux", cfg, true) + return form({ method: "POST", action: "/welcome/ux", class: "welcome-ux-form" }, + div({ class: "welcome-ux-grid" }, + pickCard("ux", "blocks", cur, i18n.uxModeMenus, "/assets/images/ux-blocks.png"), + cfg.modules?.aiNavMod === "on" ? pickCard("ux", "ainav", cur, i18n.uxModeAINav, "/assets/images/ux-ainav.png") : null, + cfg.modules?.chatsMod === "on" ? pickCard("ux", "chats", cur, i18n.uxModeChats, "/assets/images/ux-chats.png") : null, + pickCard("ux", "feed", cur, i18n.uxModeFeed, "/assets/images/ux-feed.png"), + cfg.modules?.phoneMod !== "off" ? pickCard("ux", "phone", cur, i18n.phoneTitle, "/assets/images/ux-phone.png") : null, + cfg.modules?.roomsMod !== "off" ? pickCard("ux", "rooms", cur, i18n.roomsTitle, "/assets/images/ux-rooms.png") : null + ), + submitRow(i18n.welcomeStepUxAction) + ) + } + if (key === "workflow") { + const selected = WIZARD_WORKFLOWS.includes(status.workflow) ? status.workflow : "full" + if (mobileUi.isMobile()) return mobileUi.workflowPick("/welcome/workflow", WIZARD_WORKFLOWS.includes(status.workflow) ? status.workflow : null, WIZARD_WORKFLOWS, true) + return form({ method: "POST", action: "/welcome/workflow", class: "welcome-select-form" }, + select({ name: "workflow" }, + WIZARD_WORKFLOWS.map(k => option({ value: k, ...(k === selected ? { selected: true } : {}) }, k === "full" ? i18n.welcomeWorkflow_full : (i18n[`workflow_${k}`] || k))) + ), + submitRow(i18n.welcomeStepWorkflowAction) + ) + } + if (key === "theme") return form({ method: "POST", action: "/welcome/theme", class: "welcome-ux-form" }, + div({ class: "welcome-ux-grid" }, + WIZARD_THEMES.map(([value, name]) => pickCard("theme", value, status.theme, name, `/assets/images/theme-${value.toLowerCase()}.png`)) + ), + submitRow(i18n.welcomeStepThemeAction) + ) + if (key === "community") { + const pubs = Array.isArray(status.pubs) ? status.pubs : [] + const bootstrap = status.bootstrap + const canJoin = status.canJoin !== false + return form({ method: "POST", action: "/welcome/community", class: "welcome-community-form" }, + bootstrap && bootstrap.running ? p(i18n.welcomeBootstrapRunning) : null, + bootstrap ? renderRestoreStatus(bootstrap) : null, + pubs.length && !canJoin + ? p({ class: "invites-join-note" }, status.publicMode === true ? i18n.invitesJoinPubMode : i18n.invitesJoinLocalOnly) + : null, + ...pubs.map(entry => label({ class: "welcome-pick" }, + canJoin && !entry.exhausted ? input({ type: "checkbox", name: "pub", value: entry.code }) : null, + renderSuggestedPub(entry, false) + )), + !pubs.length ? div({ class: "welcome-action" }, a({ href: "/invites", class: "filter-btn" }, i18n.invites)) : null, + div({ class: "welcome-action" }, + canJoin && pubs.some(e => !e.exhausted) ? button({ type: "submit", name: "action", value: "join" }, i18n.welcomeJoinPubs) : null, + button({ type: "submit", name: "action", value: "skip" }, i18n.welcomeNoFederation) + ) + ) + } + return form({ method: "POST", action: "/welcome/greeting", class: "welcome-greeting-form" }, + textarea({ + name: "text", + required: true, + minlength: String(FEED_TEXT_MIN), + maxlength: String(FEED_TEXT_MAX), + rows: 4, + cols: 50, + placeholder: i18n.welcomePmSubject + }), + submitRow(i18n.welcomeStepGreetingAction) + ) +} + +exports.welcomeView = async (status, profile = {}) => { + const steps = status.steps || {} + const usable = mobileUi.isMobile() ? mobileUi.welcomeSteps(status.usable || []) : (status.usable || []) + const opened = usable.find(key => !steps[key]) + const html = template( + i18n.welcomeTitle, + section( + div({ class: "tags-header module-header-line" }, + h2(i18n.welcomeTitle), + p(i18n.welcomeDescription) + ), + status.pending && !mobileUi.isMobile() + ? form({ method: "POST", action: "/welcome/dismiss", class: "welcome-action" }, + input({ type: "hidden", name: "returnTo", value: "/" }), + button({ type: "submit" }, i18n.welcomeSkip) + ) + : null + ), + status.complete + ? section( + div({ class: "welcome-complete" }, + h2(i18n.welcomeCompleteTitle), + p(i18n.welcomeCompleteText), + div({ class: "welcome-action" }, a({ href: "/inhabitants", class: "filter-btn" }, i18n.welcomeFindPeople)) + ) + ) + : null, + ...usable.map(key => section( + details({ id: `step-${key}`, class: "welcome-step", ...(key === opened ? { open: true } : {}) }, + summary( + h2(`${steps[key] ? "✓ " : ""}${i18n[SECTIONS[key][0]]}`), + span({ class: "welcome-step-text" }, i18n[SECTIONS[key][1]]) + ), + stepAction(key, profile || {}, status) + ) + )), + // movil: "Ahora no" al final y discreto (arriba era lo primero que se tocaba) + status.pending && mobileUi.isMobile() + ? section(form({ method: "POST", action: "/welcome/dismiss", class: "welcome-action welcome-skip-end" }, + input({ type: "hidden", name: "returnTo", value: "/" }), + button({ type: "submit", class: "welcome-skip-btn" }, i18n.welcomeSkip))) + : null + ) + return status.bootstrap && status.bootstrap.running ? html.replace('', '') : html +} diff --git a/src/views/wiki_view.js b/src/views/wiki_view.js new file mode 100644 index 0000000..4caba70 --- /dev/null +++ b/src/views/wiki_view.js @@ -0,0 +1,382 @@ +const { div, h2, h3, p, section, button, form, a, span, br, textarea, input, label, select, option, ul, li, img } = require("../server/node_modules/hyperaxe"); +const { clearnetItemHref, template, i18n, userLink, renderStateChip, renderContentActions, renderSubscriptionBox, renderModuleStats, moduleIsEmpty, renderCardMetaRow, paged } = require("./main_views"); +const { renderEncryptedChip, renderReachChip, renderClearnetSelector, renderClearnetSwitch, renderLicenseChip, renderLicenseSelect, renderTribeOriginChip } = require("./clearnet_view"); +const { renderStyledText, richTextarea } = require("../backend/renderStyledText"); +const { WIKILINK_RE, slugify, linkTarget } = require("../models/wiki_model"); +const moment = require("../server/node_modules/moment"); +const { config } = require("../server/SSB_server.js"); + +const userId = config.keys.id; +const FILTERS = ["all", "recent", "mine", "linked"]; + +const pageHref = (idOrSlug, tribeId = null) => + `/wiki/${encodeURIComponent(idOrSlug)}${tribeId ? `?tribeId=${encodeURIComponent(tribeId)}` : ""}`; + +const listHref = (filter, q, tribeId = null) => { + const parts = [`filter=${encodeURIComponent(filter || "all")}`]; + if (q) parts.push(`q=${encodeURIComponent(q)}`); + if (tribeId) parts.push(`tribeId=${encodeURIComponent(tribeId)}`); + return `/wiki?${parts.join("&")}`; +}; + +const renderWikiBody = (body, opts = {}) => { + const missing = new Set(opts.missing || []); + const tribeId = opts.tribeId || null; + const wikiLink = (target, slug, label) => { + const isMissing = missing.has(slug); + return a({ + href: isMissing ? `/wiki?filter=create&title=${encodeURIComponent(target)}${tribeId ? `&tribeId=${encodeURIComponent(tribeId)}` : ""}` : pageHref(slug, tribeId), + class: isMissing ? "wiki-link wiki-link-missing" : "wiki-link", + title: isMissing ? (i18n.wikiMissingLink || "Page does not exist yet") : label + }, label); + }; + const paragraphs = String(body || "").replace(/\r\n/g, "\n").split(/\n{2,}/); + return div({ class: "wiki-body" }, + ...paragraphs.filter(par => par.trim()).map(par => + p({ class: "wiki-paragraph" }, ...renderStyledText(par, { wikiLink, zoomImages: true })) + ) + ); +}; + +const diffLines = (oldText, newText) => { + const a = String(oldText || "").replace(/\r\n/g, "\n").split("\n"); + const b = String(newText || "").replace(/\r\n/g, "\n").split("\n"); + const n = a.length, m = b.length; + const lcs = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0)); + for (let i = n - 1; i >= 0; i--) { + for (let j = m - 1; j >= 0; j--) { + lcs[i][j] = a[i] === b[j] ? lcs[i + 1][j + 1] + 1 : Math.max(lcs[i + 1][j], lcs[i][j + 1]); + } + } + const out = []; + let i = 0, j = 0; + while (i < n && j < m) { + if (a[i] === b[j]) { out.push({ type: "ctx", text: a[i] }); i++; j++; } + else if (lcs[i + 1][j] >= lcs[i][j + 1]) { out.push({ type: "del", text: a[i] }); i++; } + else { out.push({ type: "add", text: b[j] }); j++; } + } + while (i < n) { out.push({ type: "del", text: a[i++] }); } + while (j < m) { out.push({ type: "add", text: b[j++] }); } + return out; +}; + +const renderDiff = (oldText, newText) => + div({ class: "wiki-diff" }, + ...diffLines(oldText, newText).map(l => + div({ class: `wiki-diff-line wiki-diff-${l.type}` }, span({ class: "wiki-diff-sign" }, l.type === "add" ? "+" : l.type === "del" ? "−" : " "), span({ class: "wiki-diff-text" }, l.text || " ")) + ) + ); + +const reachChip = (page) => + page.tribeId || page.encrypted + ? null + : renderReachChip(!!page.clearnet, i18n, page.clearnet ? clearnetItemHref('wiki', page.title, page.id) : null); + +const statusChip = (page) => + renderStateChip(page.editPolicy === "author" ? "closed" : "open", "", page.editPolicy === "author" ? i18n.wikiStatusClosed : i18n.wikiStatusOpen); + +const pageChips = (page, params = {}, { withStatus = true } = {}) => [ + page.encrypted ? renderEncryptedChip(i18n) : null, + withStatus ? statusChip(page) : null, + page.versionCount > 1 ? renderStateChip("neutral", "", `${page.versionCount} ${i18n.wikiVersions || "versions"}`) : null, + page.isLinked ? renderStateChip("open", "", i18n.wikiLinkedChip || "Linked") : null +].filter(Boolean); + +const renderTags = (tags) => (Array.isArray(tags) && tags.length) + ? div({ class: "card-tags" }, ...tags.map(t => a({ href: `/search?query=%23${encodeURIComponent(t)}`, class: "tag-link" }, `#${t}`))) + : null; + +const renderPageCard = (page, params = {}) => { + const origin = page.tribeOrigin || null; + const href = origin ? origin.href : pageHref(page.tribeId ? page.id : page.slug, page.tribeId); + return div({ class: "tribe-card wiki-card" }, + div({ class: "card-header activity-card-header" }, + span(), + renderContentActions(page.id, href, origin + ? { author: page.author, reportTitle: page.title } + : { author: page.author, reportTitle: page.title, favKind: 'wiki', isFavorite: page.isFavorite, spread: (params.spreadMap && params.spreadMap.get(page.id)) || null, deleteAction: !page.tribeId && String(page.author) === String(userId) ? `/wiki/delete/${encodeURIComponent(page.id)}` : undefined }) + ), + div({ class: "tribe-card-body" }, + page.image ? a({ href }, img({ loading: 'lazy', class: "wiki-card-cover", src: `/blob/${encodeURIComponent(page.image)}`, alt: page.title || "" })) : null, + div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, a({ href }, page.title || "—"))), + div({ class: "card-chips-row" }, ...(origin ? [renderTribeOriginChip(origin)] : pageChips(page)), !origin && page.clearnet === true ? reachChip(page) : null, renderLicenseChip(page.license)), + origin && params.withBody && page.body ? renderWikiBody(page.body) : null + ) + ); +}; + +const renderFilters = (filter, q, params = {}, census = []) => { + const tribeId = params.tribeId || null; + const chipVisible = (mode) => { + if (mode === filter || mode === "all") return true; + if (mode === "mine") return census.some(p => String(p.author) === String(userId) || (p.versions || []).some(v => String(v.author) === String(userId))); + if (mode === "recent") return census.length > 0; + if (mode === "linked") return census.some(p => p.isLinked); + return true; + }; + const labelOf = (mode) => String(i18n[`wikiFilter${mode.charAt(0).toUpperCase() + mode.slice(1)}`] || mode).toUpperCase(); + return div({ class: "filters" }, + form({ method: "GET", action: "/wiki", class: "ui-toolbar ui-toolbar--filters" }, + tribeId ? input({ type: "hidden", name: "tribeId", value: tribeId }) : null, + ...(params.emptyMod ? [] : [ + ...FILTERS.filter(chipVisible).map(mode => button({ type: "submit", name: "filter", value: mode, class: filter === mode ? "filter-btn active" : "filter-btn" }, labelOf(mode))), + ...((census.length > 0 || filter === "changes") ? [button({ type: "submit", name: "filter", value: "changes", class: filter === "changes" ? "filter-btn active" : "filter-btn" }, String(i18n.wikiChanges || "Recent changes").toUpperCase())] : []) + ]), + button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.wikiCreate) + ) + ); +}; + +const renderForm = (page, params = {}) => { + const tribeId = (page && page.tribeId) || params.tribeId || ""; + const draft = params.draft || null; + const status = draft ? (String(draft.status || "OPEN").toUpperCase() === "CLOSED" ? "CLOSED" : "OPEN") : (page && page.editPolicy === "author" ? "CLOSED" : "OPEN"); + const titleValue = draft ? draft.title : (page ? page.title : (params.title || "")); + const bodyValue = draft ? draft.body : (page ? page.body : ""); + const tagsValue = draft ? draft.tags : (page ? page.tags.join(", ") : ""); + const summaryValue = draft ? draft.summary : ""; + const licenseValue = draft && draft.license ? draft.license : (page ? page.license : ""); + const canLicense = !page || String(page.author) === String(userId); + const statusOptions = [ + { value: "OPEN", label: i18n.wikiStatusOpen }, + { value: "CLOSED", label: i18n.wikiStatusClosed } + ]; + return div({ class: "div-center audio-form" }, + h2(page ? i18n.wikiUpdateSectionTitle : i18n.wikiCreateSectionTitle), + draft + ? section({ class: "post-preview wiki-preview" }, + div({ class: "preview-content" }, + h2(i18n.messagePreview), + h3({ class: "wiki-page-title" }, titleValue || "—"), + renderWikiBody(bodyValue, { missing: [], tribeId: tribeId || null }) + ) + ) + : null, + form({ method: "POST", action: page ? `/wiki/update/${encodeURIComponent(page.id)}` : "/wiki/create", enctype: "multipart/form-data" }, + tribeId ? input({ type: "hidden", name: "tribeId", value: tribeId }) : null, + page ? input({ type: "hidden", name: "pageId", value: page.id }) : null, + params.returnTo ? input({ type: "hidden", name: "returnTo", value: params.returnTo }) : null, + label(i18n.wikiTitleLabel), br(), + input({ type: "text", name: "title", maxlength: "100", required: true, placeholder: i18n.wikiTitlePlaceholder, value: titleValue }), br(), + label(i18n.wikiBodyLabel), br(), + richTextarea({ name: "body", rows: 14, maxlength: "20000", placeholder: i18n.wikiBodyPlaceholder }, bodyValue), br(), + label(i18n.uploadMedia), br(), + input({ type: "file", name: "blob", accept: "image/*,video/*,audio/*,application/pdf,.torrent" }), br(), br(), + page ? [label(i18n.wikiSummaryLabel), br(), input({ type: "text", name: "summary", maxlength: "200", placeholder: i18n.wikiSummaryPlaceholder, value: summaryValue }), br()] : null, + page + ? null + : [label(i18n.wikiStatusLabel), br(), select({ name: "status" }, ...statusOptions.map(o => option({ value: o.value, ...(o.value === status ? { selected: true } : {}) }, o.label))), br(), br()], + label(i18n.wikiTagsLabel), br(), + input({ type: "text", name: "tags", maxlength: "200", placeholder: i18n.wikiTagsPlaceholder, value: tagsValue }), br(), + canLicense ? renderLicenseSelect(licenseValue, i18n) : null, + !tribeId && canLicense ? renderClearnetSelector(draft ? draft.clearnet === true : !!(page && page.clearnet), i18n) : null, + br(), + button({ type: "submit", class: "filter-btn", formaction: "/wiki/preview", attrs: { formmethod: "POST" } }, i18n.wikiPreview), + " ", + button({ type: "submit", class: "create-button" }, page ? i18n.wikiUpdate : i18n.wikiCreate) + ) + ); +}; + +const renderHeader = (tribe = null) => + div({ class: "tags-header module-header-line" }, + h2(tribe ? `${i18n.wikiTitle} · ${tribe.title}` : i18n.wikiTitle), + p(i18n.wikiDescription) + ); + +exports.wikiView = async (pages, filter = "all", params = {}) => { + const list = Array.isArray(pages) ? pages : []; + const q = String(params.q || "").trim(); + const census = Array.isArray(params.censusList) ? params.censusList : list; + const isForm = filter === "create" || filter === "edit"; + const emptyMod = !isForm && census.length === 0 && !q; + return template( + i18n.wikiTitle, + section( + div({ class: "tags-header module-header-line" }, + h2(params.tribe ? `${i18n.wikiTitle} · ${params.tribe.title}` : i18n.wikiTitle), + p(i18n.wikiDescription) + ), + renderFilters(isForm ? "all" : filter, q, { ...params, emptyMod }, census) + ), + section( + isForm + ? renderForm(filter === "edit" ? params.page : null, params) + : [ + emptyMod ? null : div({ class: "filters activity-filter-chips activity-toolbar-row" }, + renderModuleStats(list.length), + form({ method: "GET", action: "/wiki", class: "filter-box" }, + input({ type: "hidden", name: "filter", value: filter }), + params.tribeId ? input({ type: "hidden", name: "tribeId", value: params.tribeId }) : null, + input({ type: "text", name: "q", value: q, placeholder: i18n.wikiSearchPlaceholder, class: "filter-box__input" }), + div({ class: "filter-box__controls" }, button({ type: "submit", class: "filter-box__button" }, i18n.searchButton)) + ) + ), + div({ class: "tribe-grid" }, + list.length ? paged(list).map(pg => renderPageCard(pg, params)) : p(i18n.wikiNoPages) + ) + ] + ) + ); +}; + +exports.wikiChangesView = async (changes, params = {}) => { + const list = Array.isArray(changes) ? changes : []; + const census = Array.isArray(params.censusList) ? params.censusList : []; + return template( + i18n.wikiTitle, + section(renderHeader(params.tribe || null), renderFilters("changes", "", { ...params, emptyMod: census.length === 0 }, census)), + section( + div({ class: "tags-header" }, h2(i18n.wikiChanges)), + list.length + ? div({ class: "wiki-changes" }, ...paged(list).map(c => + div({ class: "card-section wiki-change" }, + div({ class: "card-field" }, + span({ class: "card-label" }, `#${c.index}`), + span({ class: "card-value" }, a({ href: `${pageHref(c.pageId, c.tribeId)}${c.tribeId ? "&" : "?"}version=${encodeURIComponent(c.versionKey)}` }, c.title || c.slug)) + ), + c.summary ? p({ class: "wiki-change-summary" }, c.summary.startsWith("restore:") ? i18n.wikiRestoredSummary : c.summary) : null, + p({ class: "card-footer" }, span({ class: "date-link" }, `${moment(c.createdAt).format("YYYY/MM/DD HH:mm")}`), userLink(c.author)) + ) + )) + : div({ class: "no-content-box" }, p({ class: "no-content" }, i18n.wikiNoChanges)) + ) + ); +}; + +const versionRow = (page, v, idx, current, tribeId) => { + const isCurrent = v.key === page.tipId; + const base = pageHref(page.tribeId ? page.id : page.slug, tribeId); + const sep = tribeId ? "&" : "?"; + return div({ class: "card-section wiki-version" + (isCurrent ? " wiki-version-current" : "") }, + div({ class: "card-field" }, + span({ class: "card-label" }, `${i18n.wikiVersion} ${idx + 1}`), + isCurrent ? span({ class: "card-value" }, renderStateChip("mutuals", "", i18n.wikiCurrentVersion)) : span({ class: "card-value" }) + ), + v.summary ? [br(), p({ class: "wiki-change-summary" }, v.summary.startsWith("restore:") ? i18n.wikiRestoredSummary : v.summary), br()] : null, + div({ class: "tribe-side-actions wiki-version-actions" }, + form({ method: "GET", action: base }, + tribeId ? input({ type: "hidden", name: "tribeId", value: tribeId }) : null, + input({ type: "hidden", name: "version", value: v.key }), + button({ type: "submit", class: "update-btn" }, i18n.wikiViewVersion)), + idx > 0 + ? form({ method: "GET", action: base }, + tribeId ? input({ type: "hidden", name: "tribeId", value: tribeId }) : null, + input({ type: "hidden", name: "diff", value: v.key }), + button({ type: "submit", class: "update-btn" }, i18n.wikiDiff)) + : null, + !isCurrent && page.canEdit + ? form({ method: "POST", action: `/wiki/restore/${encodeURIComponent(page.id)}` }, + input({ type: "hidden", name: "version", value: v.key }), + tribeId ? input({ type: "hidden", name: "tribeId", value: tribeId }) : null, + button({ type: "submit", class: "update-btn" }, i18n.wikiRestore)) + : null + ), + p({ class: "card-footer" }, span({ class: "date-link" }, `${moment(v.createdAt).format("YYYY/MM/DD HH:mm")}`), userLink(v.author)) + ); +}; + +exports.wikiHistoryView = async (page, params = {}) => { + const tribeId = page.tribeId || null; + const versions = page.versions.slice().reverse(); + const census = Array.isArray(params.censusList) ? params.censusList : []; + return template( + page.title, + section(renderHeader(params.tribe || null), renderFilters("all", "", { ...params, emptyMod: census.length === 0 }, census)), + section( + div({ class: "tags-header" }, h2(i18n.wikiHistory)), + div({ class: "wiki-history" }, ...versions.map((v, i) => versionRow(page, v, page.versions.length - 1 - i, page.tipId, tribeId))) + ) + ); +}; + +exports.wikiPageView = async (page, params = {}) => { + const tribeId = page.tribeId || null; + const base = pageHref(tribeId ? page.id : page.slug, tribeId); + const sep = tribeId ? "&" : "?"; + const version = params.version || null; + const diffVersion = params.diff || null; + const shownTitle = version ? version.title : page.title; + const shownBody = version ? version.body : page.body; + const census = Array.isArray(params.censusList) ? params.censusList : []; + const subscriptionNodes = params.subscription + ? renderSubscriptionBox({ target: page.id, scope: "wiki", subscribed: params.subscription.subscribed, count: params.subscription.count, isOwner: page.isOwner, returnTo: base, inline: true, compact: true }) + : null; + const barChips = [reachChip(page), statusChip(page)].filter(Boolean); + const actions = div({ class: "tribe-side-actions wiki-actions-top" }, + span({ class: "wiki-actions-left" }, ...barChips), + page.isOwner && !tribeId && !page.encrypted ? renderClearnetSwitch("wiki", page.rootId || page.id, !!page.clearnet) : null, + ...(subscriptionNodes && subscriptionNodes.length ? subscriptionNodes : []), + page.canEdit && !version + ? form({ method: "GET", action: "/wiki" }, + input({ type: "hidden", name: "filter", value: "edit" }), + input({ type: "hidden", name: "id", value: page.id }), + tribeId ? input({ type: "hidden", name: "tribeId", value: tribeId }) : null, + button({ type: "submit", class: "update-btn" }, i18n.wikiEdit)) + : null, + form({ method: "GET", action: `/wiki/${encodeURIComponent(page.id)}/history` }, + tribeId ? input({ type: "hidden", name: "tribeId", value: tribeId }) : null, + button({ type: "submit", class: "update-btn" }, `${i18n.wikiHistory} (${page.versionCount})`)), + form({ method: "GET", action: `/wiki/${encodeURIComponent(page.id)}/pdf` }, + button({ type: "submit", class: "update-btn" }, i18n.generatePdf)), + page.isOwner && tribeId + ? form({ method: "POST", action: `/wiki/delete/${encodeURIComponent(page.id)}` }, input({ type: "hidden", name: "tribeId", value: tribeId }), button({ type: "submit", class: "delete-btn" }, i18n.wikiDelete)) + : null + ); + let diffBlock = null; + if (diffVersion) { + const idx = page.versions.findIndex(v => v.key === diffVersion.key); + const prev = idx > 0 ? page.versions[idx - 1] : null; + diffBlock = div({ class: "card-section" }, + h3(`${i18n.wikiDiff}: ${i18n.wikiVersion} ${idx} → ${idx + 1}`), + renderDiff(prev ? prev.body : "", diffVersion.body) + ); + } + return template( + page.title, + section(renderHeader(params.tribe || null), renderFilters("all", "", { ...params, emptyMod: census.length === 0 }, census)), + section( + div({ class: "trending-card wiki-page" }, + div({ class: "card-header activity-card-header" }, + span(), + renderContentActions(page.id, base, { author: page.author, reportTitle: page.title, favKind: 'wiki', isFavorite: page.isFavorite, spread: params.spread || null, deleteAction: page.isOwner && !tribeId ? `/wiki/delete/${encodeURIComponent(page.id)}` : undefined }) + ), + div({ class: "card-section" }, + actions, + div({ class: "shop-title-row" }, h2({ class: "wiki-page-title" }, shownTitle), renderLicenseChip(page.license)), + (() => { const rest = pageChips(page, params, { withStatus: false }); return rest.length ? div({ class: "card-chips-row" }, ...rest) : null; })(), + version ? p({ class: "wiki-version-notice" }, `${i18n.wikiViewingVersion} (${moment(version.createdAt).format("YYYY/MM/DD HH:mm")}) — `, a({ href: base }, i18n.wikiBackToPage)) : null, + diffBlock || renderWikiBody(shownBody, { missing: page.missingLinks, tribeId }), + renderTags(page.tags), + renderCardMetaRow(p({ class: "card-footer" }, + span({ class: "date-link" }, `${moment(page.createdAt).format("YYYY/MM/DD HH:mm")}`), + userLink(page.author) + )) + ), + ), + div({ class: "card-section wiki-backlinks" }, + h3(i18n.wikiLinkedPages), + page.linkedPages && page.linkedPages.length + ? ul(...page.linkedPages.map(b => li(a({ href: pageHref(b.tribeId ? b.id : b.slug, b.tribeId) }, b.title)))) + : p(i18n.wikiNoLinkedPages) + ) + ) + ); +}; + +exports.renderTribeWikiSection = (tribe, pages, toolbar = null, wrap = (pg, card) => card) => { + const items = Array.isArray(pages) ? pages : []; + const createBtn = form({ method: "GET", action: "/wiki" }, + input({ type: "hidden", name: "filter", value: "create" }), + input({ type: "hidden", name: "tribeId", value: tribe.id }), + button({ type: "submit", class: "create-button" }, i18n.tribeWikiCreate)); + const header = toolbar || div({ class: "tribe-content-header" }, h2(i18n.tribeSectionWiki), createBtn); + if (!items.length) return div({ class: "tribe-content-list" }, header, div({ class: "no-content-box" }, p(i18n.tribeWikiEmpty))); + return div({ class: "tribe-content-list" }, + header, + div({ class: "tribe-grid" }, ...paged(items).map(pg => wrap(pg, renderPageCard(pg, { tribeId: tribe.id, withBody: true })))) + ); +}; + +exports.renderWikiBody = renderWikiBody; +exports.diffLines = diffLines;